325 lines
21 KiB
OpenEdge ABL
325 lines
21 KiB
OpenEdge ABL
/**
|
|
* ─────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
* This Utility class is use for Authenticate user before excute the webhooks.
|
|
* When Shopify hit into the Salesforce we will authenticate user using refresh token which saved into the config custom setting
|
|
* Shopify API Documentation: https://shopify.dev/docs/admin-api/rest/reference
|
|
* Create Shopify Store: https://www.shopify.com/signup?itcat=identity&itterm=create-new-store
|
|
* Shopify API Release 2020: https://www.shopify.in/partners/blog/shopify-api-release-january-2020
|
|
* What is Person Account in Salesforce ? https://help.salesforce.com/articleView?id=account_person.htm&type=5
|
|
* ──────────────────────────────────────────────────────────────────────────────────────────────────
|
|
* @author Sumit <sumit@hicShopify.com>
|
|
* ──────────────────────────────────────────────────────────────────────────────────────────────────
|
|
*/
|
|
public with sharing class SfUtility {
|
|
|
|
//Deprecated
|
|
public static void InsertRefreshToken(string JSONResponse, string urlMapping){
|
|
try{
|
|
list<HIC_ShopifySync__Configuration__c> listofConfiguration = new list<HIC_ShopifySync__Configuration__c>();
|
|
|
|
String query = 'Select '+CheckAccessibility.isAccessible('id,name,HIC_ShopifySync__Salesforce_Site_URL__c','HIC_ShopifySync__Configuration__c')+' from HIC_ShopifySync__Configuration__c where '+CheckAccessibility.isAccessible('name','HIC_ShopifySync__Configuration__c')+' = \'Default\' LIMIT 1';
|
|
listofConfiguration = database.query(query);
|
|
|
|
// listofConfiguration = [select id,name,HIC_ShopifySync__Salesforce_Site_URL__c from HIC_ShopifySync__Configuration__c where name='Default' limit 1];
|
|
if(!listofConfiguration.isEmpty() || listofConfiguration[0].HIC_ShopifySync__Salesforce_Site_URL__c != null || listofConfiguration[0].HIC_ShopifySync__Salesforce_Site_URL__c != '' )
|
|
{
|
|
string sfUrl = listofConfiguration[0].HIC_ShopifySync__Salesforce_Site_URL__c;
|
|
Http http = new Http();
|
|
HttpRequest requestRemotesiteSetting = new HttpRequest();
|
|
requestRemotesiteSetting.setEndpoint(sfUrl+'/services/apexrest/HIC_ShopifySync/UpdateRefreshToken');
|
|
requestRemotesiteSetting.setMethod('POST');
|
|
requestRemotesiteSetting.setHeader('Content-Type', 'application/json;charset=UTF-8');
|
|
|
|
requestRemotesiteSetting.setBody(JSONResponse);
|
|
requestRemotesiteSetting.setTimeout(120000);
|
|
if(!Test.isRunningTest()){
|
|
HttpResponse responseRemotesiteSetting = http.send(requestRemotesiteSetting);
|
|
}
|
|
}
|
|
}catch(Exception e){
|
|
//CustomSettingRecordInsert.SuccessMailSend(String.valueof('Error message from sf class--> ' + e.getMessage()+ 'at line number '+ e.getLineNumber()));
|
|
}
|
|
}
|
|
|
|
public static void postToSF(string body, string urlMapping, String Store){}
|
|
|
|
public static void postToSF(string body, string urlMapping, String Store, string objectname){
|
|
// REVERTED (2026-08-10): DirectWebhookDispatcher's in-process dispatch broke production sync.
|
|
// The HTTP self-callout below was not only about tokens — running the handler over a real HTTP
|
|
// call also authenticated it as the privileged OAuth-token owner. Every handler's config queries
|
|
// are built via CheckAccessibility.isAccessible(), which silently drops fields the RUNNING USER
|
|
// can't see. In-process dispatch runs as the Site Guest User, which has no FLS on
|
|
// HIC_ShopifySync__HIC_Configuration__c, so every field was stripped and the query collapsed to
|
|
// "SELECT FROM ..." -> QueryException on every object type (confirmed live on Product2). Do
|
|
// NOT re-enable DirectWebhookDispatcher without first converting every handler's internal config
|
|
// queries off CheckAccessibility (plain SOQL, as TokenService.lockDefaultRow already does) —
|
|
// removing the self-callout is worthless if it takes sync down to do it.
|
|
list<HIC_ShopifySync__Configuration__c> listofConfiguration = new list<HIC_ShopifySync__Configuration__c>();
|
|
listofConfiguration = [select id,name,HIC_ShopifySync__ClientId__c,HIC_ShopifySync__ClientSecret__c from HIC_ShopifySync__Configuration__c where name= 'Default' limit 1];
|
|
if(listofConfiguration.isEmpty()){
|
|
// Not configured yet — this webhook cannot be processed. Request a re-delivery rather than
|
|
// silently acknowledging it; the bare return here used to lose the record permanently.
|
|
markRetryable();
|
|
return;
|
|
}
|
|
|
|
String clientIdProduction = listofConfiguration[0].HIC_ShopifySync__ClientId__c;
|
|
String clientSecretproduction = listofConfiguration[0].HIC_ShopifySync__ClientSecret__c;
|
|
try{
|
|
List<HIC_ShopifySync__Configuration__c> configrationRecord = new list<HIC_ShopifySync__Configuration__c>();
|
|
for(HIC_ShopifySync__Configuration__c configration : [Select id,HIC_ShopifySync__Access_Token__c,HIC_ShopifySync__Account_Contact__c,HIC_ShopifySync__Account_Record_Type__c,HIC_ShopifySync__API_Key__c,HIC_ShopifySync__Collection__c,HIC_ShopifySync__Collection_Sync__c,HIC_ShopifySync__Contact_Record_Type__c,HIC_ShopifySync__Customer__c,HIC_ShopifySync__Customer_Sync__c,HIC_ShopifySync__Default_Account__c,HIC_ShopifySync__Draft_Order_Sync__c,HIC_ShopifySync__Enable_Draft_Order__c,HIC_ShopifySync__Inventory_Location__c,HIC_ShopifySync__Inventory_Location_Sync__c,HIC_ShopifySync__Number_of_pages_for_Account__c,HIC_ShopifySync__Opportunity__c,HIC_ShopifySync__Opportunity_Close_Date__c,HIC_ShopifySync__Opportunity_Line_Item__c,HIC_ShopifySync__Opportunity_Record_Type__c,HIC_ShopifySync__Opportunity_Stage__c,HIC_ShopifySync__Opportunity_Sync__c,HIC_ShopifySync__Order__c,HIC_ShopifySync__Order_Fulfillment__c,HIC_ShopifySync__Order_Line_Item__c,HIC_ShopifySync__Order_Record_Type__c,HIC_ShopifySync__Order_Sync__c,HIC_ShopifySync__Person_Account__c,HIC_ShopifySync__Private_Key_Password__c,HIC_ShopifySync__Product_Sync__c,HIC_ShopifySync__Refresh_Token__c,HIC_ShopifySync__Salesforce_Pricebook__c,HIC_ShopifySync__Salesforce_Site_URL__c,HIC_ShopifySync__Shopify_URL__c from HIC_ShopifySync__Configuration__c where name='Default' limit 1]){
|
|
if(configration.HIC_ShopifySync__Refresh_Token__c != null ){
|
|
configrationRecord.add(configration);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!configrationRecord.isEmpty() && configrationRecord != null ){
|
|
string sfUrl = URL.getOrgDomainUrl().toexternalform();
|
|
string token = configrationRecord[0].HIC_ShopifySync__Access_Token__c;
|
|
|
|
// ROTATION-SAFE: this high-frequency runtime path NEVER rotates the token inline. All
|
|
// rotation is centralized in TokenService (serialized, fail-loud, no stale wipe). Here we
|
|
// simply use the stored access token; if it is missing/stale we ask TokenService to refresh
|
|
// it for the NEXT delivery and signal Shopify (503) to re-deliver this webhook. We must not
|
|
// retry the upsert inline after a refresh — that would be a callout after TokenService's
|
|
// save DML and would roll back the freshly-rotated token.
|
|
if(Test.isRunningTest()) return;
|
|
|
|
if(String.isBlank(token)){
|
|
TokenService.tryRefresh(null);
|
|
markRetryable();
|
|
return;
|
|
}
|
|
|
|
Http http = new Http();
|
|
HttpRequest request = new HttpRequest();
|
|
String endpointUrl = sfUrl + '/services/apexrest/' + urlMapping + '?objectname=' + objectName + '&store=' + Store;
|
|
System.debug('End Point URL --> ' + endpointUrl);
|
|
request.setEndpoint(endpointUrl);
|
|
request.setMethod('POST');
|
|
request.setHeader('Content-Type', 'application/json;charset=UTF-8');
|
|
request.setHeader('Authorization', 'Bearer ' + token);
|
|
request.setBody(body);
|
|
request.setTimeout(120000);
|
|
|
|
HttpResponse response = http.send(request);
|
|
system.debug('****response'+response);
|
|
|
|
if(response.getStatusCode() == 401){
|
|
// Stale access token. Refresh via the serialized authority for the next delivery.
|
|
// Do NOT retry the upsert here (callout-after-DML). Shopify re-delivers on the 503.
|
|
TokenService.tryRefresh(token);
|
|
markRetryable();
|
|
}else if(response.getStatusCode() >= 400){
|
|
// The inner sync endpoint rejected the payload. Do NOT report success to Shopify —
|
|
// that silently discards the record. Ask for a re-delivery instead.
|
|
System.debug(LoggingLevel.ERROR, 'SME_SYNC relay failed HTTP '
|
|
+ response.getStatusCode() + ' for ' + objectName + ' — requesting redelivery');
|
|
markRetryable();
|
|
}
|
|
}else{
|
|
// No usable configuration (no refresh token stored). The webhook cannot be processed now,
|
|
// so it must be re-delivered rather than acknowledged and lost.
|
|
markRetryable();
|
|
}
|
|
}catch(Exception e){
|
|
// CRITICAL: this catch used to be empty. Any exception here (callout timeout, unauthorized
|
|
// endpoint, DML error) meant the webhook was silently ACKNOWLEDGED with HTTP 200 and the
|
|
// Shopify record was lost forever with no log and no retry. Always ask for a re-delivery.
|
|
System.debug(LoggingLevel.ERROR, 'SME_SYNC postToSF threw — requesting redelivery: '
|
|
+ e.getTypeName() + ' | ' + e.getMessage() + ' | line ' + e.getLineNumber());
|
|
markRetryable();
|
|
}
|
|
|
|
|
|
}
|
|
|
|
//Deprecated
|
|
public static void postToSF(string body, string urlMapping){
|
|
list<HIC_ShopifySync__Configuration__c> listofConfiguration = new list<HIC_ShopifySync__Configuration__c>();
|
|
listofConfiguration = [select id,name,HIC_ShopifySync__ClientId__c,HIC_ShopifySync__ClientSecret__c from HIC_ShopifySync__Configuration__c where name= 'Default' limit 1];
|
|
if(listofConfiguration.isEmpty()) return ;
|
|
|
|
String clientIdProduction = listofConfiguration[0].HIC_ShopifySync__ClientId__c;
|
|
String clientSecretproduction = listofConfiguration[0].HIC_ShopifySync__ClientSecret__c;
|
|
try{
|
|
List<HIC_ShopifySync__Configuration__c> configrationRecord = new list<HIC_ShopifySync__Configuration__c>();
|
|
for(HIC_ShopifySync__Configuration__c configration : [Select id,HIC_ShopifySync__Account_Contact__c,HIC_ShopifySync__Account_Record_Type__c,HIC_ShopifySync__API_Key__c,HIC_ShopifySync__Collection__c,HIC_ShopifySync__Collection_Sync__c,HIC_ShopifySync__Contact_Record_Type__c,HIC_ShopifySync__Customer__c,HIC_ShopifySync__Customer_Sync__c,HIC_ShopifySync__Default_Account__c,HIC_ShopifySync__Draft_Order_Sync__c,HIC_ShopifySync__Enable_Draft_Order__c,HIC_ShopifySync__Inventory_Location__c,HIC_ShopifySync__Inventory_Location_Sync__c,HIC_ShopifySync__Number_of_pages_for_Account__c,HIC_ShopifySync__Opportunity__c,HIC_ShopifySync__Opportunity_Close_Date__c,HIC_ShopifySync__Opportunity_Line_Item__c,HIC_ShopifySync__Opportunity_Record_Type__c,HIC_ShopifySync__Opportunity_Stage__c,HIC_ShopifySync__Opportunity_Sync__c,HIC_ShopifySync__Order__c,HIC_ShopifySync__Order_Fulfillment__c,HIC_ShopifySync__Order_Line_Item__c,HIC_ShopifySync__Order_Record_Type__c,HIC_ShopifySync__Order_Sync__c,HIC_ShopifySync__Person_Account__c,HIC_ShopifySync__Private_Key_Password__c,HIC_ShopifySync__Product_Sync__c,HIC_ShopifySync__Refresh_Token__c,HIC_ShopifySync__Salesforce_Pricebook__c,HIC_ShopifySync__Salesforce_Site_URL__c,HIC_ShopifySync__Shopify_URL__c from HIC_ShopifySync__Configuration__c where name='Default' limit 1]){
|
|
if(configration.HIC_ShopifySync__Refresh_Token__c != null ){
|
|
configrationRecord.add(configration);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!configrationRecord.isEmpty() && configrationRecord != null ){
|
|
string clientId = clientIdProduction;
|
|
string clientSecret = clientSecretproduction;
|
|
string sfUrl = configrationRecord[0].HIC_ShopifySync__Salesforce_Site_URL__c;//system.url.getsalesforcebaseurl().toexternalform();
|
|
string token = oauthLogin(configrationRecord[0].HIC_ShopifySync__Refresh_Token__c,clientIdProduction,clientSecretproduction);
|
|
system.debug('****'+token);
|
|
if(token != null){
|
|
Http http = new Http();
|
|
HttpRequest request = new HttpRequest();
|
|
request.setEndpoint(sfUrl+'/services/apexrest/HIC_ShopifySync/'+urlMapping);
|
|
request.setMethod('POST');
|
|
request.setHeader('Content-Type', 'application/json;charset=UTF-8');
|
|
String authorizationHeader = 'Bearer ' +token;
|
|
request.setHeader('Authorization', authorizationHeader);
|
|
request.setBody(body);
|
|
request.setTimeout(120000);
|
|
if(!Test.isRunningTest()){
|
|
HttpResponse response = http.send(request);
|
|
system.debug('****response'+response);
|
|
}
|
|
}
|
|
}
|
|
}catch(Exception e){
|
|
}
|
|
|
|
|
|
}
|
|
|
|
/**
|
|
* ROTATION-SAFE: no longer performs its own grant_type=refresh_token exchange.
|
|
*
|
|
* The removed implementation rotated the shared token and then persisted the replacement via
|
|
* persistRotatedRefreshToken(), which built its query through CheckAccessibility.isAccessible()
|
|
* (empty string when the caller lacks FLS), ran it through
|
|
* String.escapeSingleQuotes(soql).remove('\\'), and swallowed every exception. A rotation whose
|
|
* persistence silently failed left the org holding a dead token. It also held no row lock, so it
|
|
* could rotate concurrently with TokenService and invalidate TokenService's token.
|
|
*
|
|
* TokenService is the only rotation authority now.
|
|
*/
|
|
public static string oauthLogin(String refreshToken,String clientIdProduction,String clientSecretproduction) {
|
|
if(String.isBlank(refreshToken)) return null;
|
|
try{
|
|
String token = TokenService.getFreshAccessToken(null, false);
|
|
return String.isNotBlank(token) ? token : null;
|
|
}catch(Exception e){
|
|
// Transient or reauth-required: return null so the caller retries later. Never wipes.
|
|
System.debug('SfUtility.oauthLogin delegated refresh failed: '+e.getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ROTATION-SAFE: no longer performs its own grant_type=refresh_token exchange.
|
|
*
|
|
* This method had NO callers outside tests, but it was a live rotation implementation sitting in the
|
|
* codebase waiting to be wired up — and it deliberately persisted nothing, relying on the caller to
|
|
* remember to call saveTokens(). Under mandatory rotation that contract is a trap: one caller that
|
|
* forgets kills the connection. Delegates to TokenService, which persists fail-loud under a row lock.
|
|
*/
|
|
public static Map<String,String> refreshTokensNoDml(String refreshToken, String clientId, String clientSecret){
|
|
Map<String,String> out = new Map<String,String>();
|
|
if(String.isBlank(refreshToken)) return out;
|
|
try{
|
|
String token = TokenService.getFreshAccessToken(null, false);
|
|
if(String.isNotBlank(token)) out.put('access_token', token);
|
|
}catch(Exception e){
|
|
System.debug('refreshTokensNoDml delegated refresh failed: '+e.getMessage());
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Persists access + rotated refresh token. Call ONLY AFTER all callouts (callout-before-DML rule).
|
|
*/
|
|
public static void saveTokens(String accessToken, String refreshToken){
|
|
try{
|
|
if(String.isBlank(accessToken) && String.isBlank(refreshToken)) return;
|
|
list<HIC_ShopifySync__Configuration__c> cfg = [select id from HIC_ShopifySync__Configuration__c where name='Default' limit 1];
|
|
if(!cfg.isEmpty()){
|
|
if(String.isNotBlank(accessToken)) cfg[0].HIC_ShopifySync__Access_Token__c = accessToken;
|
|
if(String.isNotBlank(refreshToken)) cfg[0].HIC_ShopifySync__Refresh_Token__c = refreshToken;
|
|
update cfg[0];
|
|
}
|
|
}catch(Exception e){
|
|
System.debug('saveTokens error: '+e.getMessage());
|
|
}
|
|
}
|
|
|
|
// Signal Shopify to re-deliver the current webhook (used when the access token was stale and has
|
|
// been refreshed for the next delivery). A 5xx makes Shopify retry so the order is not lost.
|
|
private static void markRetryable(){
|
|
try{
|
|
if(RestContext.response != null){
|
|
RestContext.response.statusCode = 503;
|
|
}
|
|
}catch(Exception e){
|
|
System.debug('markRetryable skipped: '+e.getMessage());
|
|
}
|
|
}
|
|
|
|
// persistRotatedRefreshToken() was removed with its only caller. It wrote a rotated token through a
|
|
// CheckAccessibility-filtered dynamic query and swallowed every failure, so a rotation could be lost
|
|
// without a trace. Token persistence now lives solely in TokenService.persistOrThrow (fail-loud) and
|
|
// AuthenticateAllApexClass.persistRotatedToken (reports failure to the caller).
|
|
|
|
public class readToken{
|
|
public string access_token{get;set;}
|
|
public string instance_url{get;set;}
|
|
}
|
|
|
|
public static string fetchFieldType(object value, string fieldTypeExcluded){
|
|
string returnType = '';
|
|
try{
|
|
if(value != null){
|
|
if(!fieldTypeExcluded.contains('Boolean') && (value == 'true' || value == 'false' || value == true || value == false)){
|
|
fieldTypeExcluded += 'Boolean,';
|
|
boolean booleanCheck = Boolean.valueOf(value);
|
|
system.debug('** value type '+booleanCheck+ ' >>> '+ fieldTypeExcluded+ ' - field value is -- '+value);
|
|
return 'Boolean';
|
|
}else if(!fieldTypeExcluded.contains('Decimal')){
|
|
fieldTypeExcluded += 'Decimal,';
|
|
Decimal decimalCheck = (Decimal)value;
|
|
return 'Decimal';
|
|
}else if(!fieldTypeExcluded.contains('DateTime')){
|
|
fieldTypeExcluded += 'DateTime,';
|
|
DateTime dateCheck = DateTime.valueOf(string.valueOf(value).replace('T',' '));
|
|
system.debug('** value type '+dateCheck+ ' >>> '+ fieldTypeExcluded+ ' - field value is -- '+value);
|
|
return 'DateTime';
|
|
}else if(!fieldTypeExcluded.contains('DateField')){
|
|
fieldTypeExcluded += 'DateField';
|
|
DateTime dateCheck = DateTime.valueOf(string.valueOf(value));
|
|
return 'Date';
|
|
}else{
|
|
return 'string';
|
|
}
|
|
}else{
|
|
return 'string';
|
|
}
|
|
}catch(Exception e){
|
|
//system.debug('** Not '+ fieldTypeExcluded+ ' - field value is -- '+value);
|
|
returnType = fetchFieldType(value,fieldTypeExcluded);
|
|
}
|
|
return returnType;
|
|
}
|
|
public static void saveAccessToken(String accessToken ){
|
|
|
|
if(accessToken != null && accessToken != ''){
|
|
list<HIC_ShopifySync__Configuration__c> listofConfiguration = new list<HIC_ShopifySync__Configuration__c>();
|
|
listofConfiguration = [select id from HIC_ShopifySync__Configuration__c where name= 'Default' limit 1];
|
|
|
|
//listofConfiguration = database.query(query);
|
|
system.debug('***listofConfiguration ' +listofConfiguration);
|
|
if(listofConfiguration.isEmpty()) return;
|
|
|
|
listofConfiguration[0].HIC_ShopifySync__Access_Token__c = accessToken;
|
|
|
|
if(listofConfiguration != null && !listofConfiguration.isEmpty()){
|
|
try{
|
|
update listofConfiguration;
|
|
}catch(Exception e){
|
|
|
|
System.debug('Error '+ e.getMessage());
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
} |