SyncMadeEasy/force-app/main/default/classes/SfUtility.cls

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());
}
}
}
}
}