/** * ─────────────────────────────────────────────────────────────────────────────────────────────────┐ * 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 * ────────────────────────────────────────────────────────────────────────────────────────────────── */ public with sharing class SfUtility { //Deprecated public static void InsertRefreshToken(string JSONResponse, string urlMapping){ try{ list listofConfiguration = new list(); 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 listofConfiguration = new list(); 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 configrationRecord = new list(); 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 listofConfiguration = new list(); 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 configrationRecord = new list(); 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 refreshTokensNoDml(String refreshToken, String clientId, String clientSecret){ Map out = new Map(); 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 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 listofConfiguration = new list(); 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()); } } } } }