mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-02 05:09:27 +08:00
263 lines
8.8 KiB
OpenEdge ABL
263 lines
8.8 KiB
OpenEdge ABL
/**
|
|
* @description Asynchronous REST Callout using Queueable
|
|
*
|
|
* Use Case: Callouts triggered from DML operations
|
|
* - Trigger-based integrations
|
|
* - Process Builder / Flow callouts
|
|
* - Any callout after DML (insert, update, delete)
|
|
*
|
|
* CRITICAL: Synchronous callouts are NOT allowed after DML.
|
|
* Always use Queueable with Database.AllowsCallouts for such scenarios.
|
|
*
|
|
* Features:
|
|
* - Implements Database.AllowsCallouts for HTTP requests
|
|
* - Processes records in batches
|
|
* - Includes error handling and logging
|
|
* - Supports job chaining for pagination
|
|
*
|
|
* @author {{Author}}
|
|
* @date {{Date}}
|
|
*/
|
|
public with sharing class {{ServiceName}}QueueableCallout implements Queueable, Database.AllowsCallouts {
|
|
|
|
private static final String NAMED_CREDENTIAL = 'callout:{{NamedCredentialName}}';
|
|
private static final Integer DEFAULT_TIMEOUT = 120000;
|
|
private static final Integer BATCH_SIZE = 10; // Callouts per job (max 100 per transaction)
|
|
|
|
private List<Id> recordIds;
|
|
private String operation;
|
|
private Integer startIndex;
|
|
|
|
/**
|
|
* @description Constructor for processing records
|
|
* @param recordIds List of record IDs to process
|
|
* @param operation Operation type (CREATE, UPDATE, DELETE, SYNC)
|
|
*/
|
|
public {{ServiceName}}QueueableCallout(List<Id> recordIds, String operation) {
|
|
this(recordIds, operation, 0);
|
|
}
|
|
|
|
/**
|
|
* @description Constructor with pagination support
|
|
* @param recordIds List of record IDs to process
|
|
* @param operation Operation type
|
|
* @param startIndex Starting index for batch processing
|
|
*/
|
|
public {{ServiceName}}QueueableCallout(List<Id> recordIds, String operation, Integer startIndex) {
|
|
this.recordIds = recordIds;
|
|
this.operation = operation;
|
|
this.startIndex = startIndex;
|
|
}
|
|
|
|
/**
|
|
* @description Execute the queueable job
|
|
* @param context QueueableContext
|
|
*/
|
|
public void execute(QueueableContext context) {
|
|
if (recordIds == null || recordIds.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// Calculate batch bounds
|
|
Integer endIndex = Math.min(startIndex + BATCH_SIZE, recordIds.size());
|
|
List<Id> batchIds = new List<Id>();
|
|
for (Integer i = startIndex; i < endIndex; i++) {
|
|
batchIds.add(recordIds[i]);
|
|
}
|
|
|
|
try {
|
|
// Query records for this batch
|
|
List<{{ObjectName}}> records = [
|
|
SELECT Id, Name{{AdditionalFields}}
|
|
FROM {{ObjectName}}
|
|
WHERE Id IN :batchIds
|
|
WITH USER_MODE
|
|
];
|
|
|
|
// Process each record
|
|
List<{{ObjectName}}> toUpdate = new List<{{ObjectName}}>();
|
|
for ({{ObjectName}} record : records) {
|
|
try {
|
|
processRecord(record, operation);
|
|
// Mark success on record if needed
|
|
record.Integration_Status__c = 'Success';
|
|
record.Integration_Date__c = Datetime.now();
|
|
toUpdate.add(record);
|
|
} catch (Exception e) {
|
|
// Log individual record failure
|
|
System.debug(LoggingLevel.ERROR,
|
|
'Failed to process record ' + record.Id + ': ' + e.getMessage());
|
|
record.Integration_Status__c = 'Error';
|
|
record.Integration_Error__c = e.getMessage().left(255);
|
|
toUpdate.add(record);
|
|
}
|
|
}
|
|
|
|
// Update records with sync status
|
|
if (!toUpdate.isEmpty()) {
|
|
update toUpdate;
|
|
}
|
|
|
|
// Chain next batch if more records remain
|
|
if (endIndex < recordIds.size()) {
|
|
System.enqueueJob(
|
|
new {{ServiceName}}QueueableCallout(recordIds, operation, endIndex)
|
|
);
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
System.debug(LoggingLevel.ERROR, '{{ServiceName}}QueueableCallout Error: ' + e.getMessage());
|
|
System.debug(LoggingLevel.ERROR, 'Stack Trace: ' + e.getStackTraceString());
|
|
// Consider: Create Integration_Log__c record, send notification, etc.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @description Process a single record with external API
|
|
* @param record The record to process
|
|
* @param operation The operation type
|
|
*/
|
|
private void processRecord({{ObjectName}} record, String operation) {
|
|
switch on operation {
|
|
when 'CREATE' {
|
|
createExternalRecord(record);
|
|
}
|
|
when 'UPDATE' {
|
|
updateExternalRecord(record);
|
|
}
|
|
when 'DELETE' {
|
|
deleteExternalRecord(record.Id);
|
|
}
|
|
when 'SYNC' {
|
|
syncExternalRecord(record);
|
|
}
|
|
when else {
|
|
throw new IntegrationException('Unknown operation: ' + operation);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @description Create record in external system
|
|
* @param record Salesforce record
|
|
*/
|
|
private void createExternalRecord({{ObjectName}} record) {
|
|
Map<String, Object> payload = buildPayload(record);
|
|
|
|
HttpRequest req = new HttpRequest();
|
|
req.setEndpoint(NAMED_CREDENTIAL + '/{{Endpoint}}');
|
|
req.setMethod('POST');
|
|
req.setHeader('Content-Type', 'application/json');
|
|
req.setTimeout(DEFAULT_TIMEOUT);
|
|
req.setBody(JSON.serialize(payload));
|
|
|
|
HttpResponse res = new Http().send(req);
|
|
handleResponse(res, record.Id);
|
|
}
|
|
|
|
/**
|
|
* @description Update record in external system
|
|
* @param record Salesforce record
|
|
*/
|
|
private void updateExternalRecord({{ObjectName}} record) {
|
|
if (String.isBlank(record.External_Id__c)) {
|
|
throw new IntegrationException('No external ID for record: ' + record.Id);
|
|
}
|
|
|
|
Map<String, Object> payload = buildPayload(record);
|
|
|
|
HttpRequest req = new HttpRequest();
|
|
req.setEndpoint(NAMED_CREDENTIAL + '/{{Endpoint}}/' + record.External_Id__c);
|
|
req.setMethod('PUT');
|
|
req.setHeader('Content-Type', 'application/json');
|
|
req.setTimeout(DEFAULT_TIMEOUT);
|
|
req.setBody(JSON.serialize(payload));
|
|
|
|
HttpResponse res = new Http().send(req);
|
|
handleResponse(res, record.Id);
|
|
}
|
|
|
|
/**
|
|
* @description Delete record from external system
|
|
* @param recordId Salesforce record ID
|
|
*/
|
|
private void deleteExternalRecord(Id recordId) {
|
|
// Query for external ID
|
|
{{ObjectName}} record = [
|
|
SELECT External_Id__c
|
|
FROM {{ObjectName}}
|
|
WHERE Id = :recordId
|
|
WITH USER_MODE
|
|
LIMIT 1
|
|
];
|
|
|
|
if (String.isBlank(record.External_Id__c)) {
|
|
return; // Nothing to delete externally
|
|
}
|
|
|
|
HttpRequest req = new HttpRequest();
|
|
req.setEndpoint(NAMED_CREDENTIAL + '/{{Endpoint}}/' + record.External_Id__c);
|
|
req.setMethod('DELETE');
|
|
req.setTimeout(DEFAULT_TIMEOUT);
|
|
|
|
HttpResponse res = new Http().send(req);
|
|
handleResponse(res, recordId);
|
|
}
|
|
|
|
/**
|
|
* @description Sync record with external system (get latest and update)
|
|
* @param record Salesforce record
|
|
*/
|
|
private void syncExternalRecord({{ObjectName}} record) {
|
|
if (String.isBlank(record.External_Id__c)) {
|
|
createExternalRecord(record);
|
|
} else {
|
|
updateExternalRecord(record);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @description Build API payload from Salesforce record
|
|
* @param record Salesforce record
|
|
* @return Payload map for API
|
|
*/
|
|
private Map<String, Object> buildPayload({{ObjectName}} record) {
|
|
return new Map<String, Object>{
|
|
'salesforceId' => record.Id,
|
|
'name' => record.Name
|
|
// Add more field mappings as needed
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @description Handle HTTP response
|
|
* @param res HTTP response
|
|
* @param recordId Salesforce record ID (for logging)
|
|
*/
|
|
private void handleResponse(HttpResponse res, Id recordId) {
|
|
Integer statusCode = res.getStatusCode();
|
|
|
|
if (statusCode >= 200 && statusCode < 300) {
|
|
System.debug(LoggingLevel.DEBUG, 'Success for ' + recordId + ': ' + res.getBody());
|
|
return;
|
|
}
|
|
|
|
if (statusCode >= 400 && statusCode < 500) {
|
|
throw new IntegrationException(
|
|
'Client Error (' + statusCode + ') for ' + recordId + ': ' + res.getBody()
|
|
);
|
|
}
|
|
|
|
if (statusCode >= 500) {
|
|
throw new IntegrationException(
|
|
'Server Error (' + statusCode + ') for ' + recordId + ': ' + res.getBody()
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @description Custom exception for integration errors
|
|
*/
|
|
public class IntegrationException extends Exception {}
|
|
}
|