Salesforce Order Management Payment Gateway Part 1
Developing a Payment Gateway Part 1
A key component of the order management life cycle is being able to capture payments once orders have been fulfilled. Salesforce Order Management (OM) provides this capability with a number of App Exchange plugins for integrating with a variety of payment gateways. But what if your payment gateway doesn t have a pre-built plugin that is compatible with Salesforce Order Management? Luckily, Salesforce provides the CommercePayments namespace which provides developers a safe and customizable platform for managing customer payments and refunds. This blog will outline the steps required to build your own integration with any payment gateway. The payment gateway can be invoked by Salesforce Order Management to capture payments once orders are fulfilled. The steps for developing your own integration include implementing the commercepayments.PaymentGatewayAdapter interface in Apex, creating a named credential, creating a PaymentGatewayProvider, and creating a new PaymentGateway record. The following sections go into the steps in more detail.
Apex Code
Apex coding is required to make the callout to the third-party payment gateway. Fortunately, Salesforce has the PaymentGatewayAdapter interface which provides a uniform structure for creating the integration.
XC_PaymentGatewayAdapter
Figure 1 below illustrates the implementation of the PaymentGatewayAdapter interface. The function of the custom payment gateway adapter is simple. It checks the gateway context type (i.e. capture payment, refund payment, etc.), instantiates a transaction service helper, and executes the service.
public class XC_PaymentGatewayAdapter implements commercepayments.PaymentGatewayAdapter {
/**
* Function to process transaction requests
* This is the entry point for the adapter class
*/
public commercepayments.GatewayResponse processRequest(commercepayments.paymentGatewayContext gatewayContext) {
if (null == gatewayContext) {
return new commercepayments.GatewayErrorResponse('400','Gateway context cannot be null');
}
//Get the instance of service based on the request type
XC_AbstractTransactionService service;
String requestType = gatewayContext.getPaymentRequestType().name();
if (requestType == commercepayments.RequestType.Capture.name()) {
service = XC_CaptureTransactionService(gatewayContext);
} else {
service = null;
}
if (null != service) {
return service.execute();
} else {
return new commercepayments.GatewayErrorResponse('400','Service not found');
}
}
}
Figure 1 - XC_PaymentGatewayAdapter
XC_AbstractTransactionService
Figure 2 below illustrates the base class for all transaction services (i.e. capture, refund, etc.) for the payment gateway adapter. The class defines common methods which each service must implement. For example, it defines abstract build request and response methods for creating a service request and handling a service response. Also, you can see the logic each service takes when it gets invoked. In short, each service when called builds its service request, sends the request, and finally processes the response. Finally, it is important to note that if your payment gateway requires additional authorization headers you could add a new method to this class to create those authorization headers for all services.
/**
* Abstract class for all transaction service classes, contains common methods for all service classes
* Every service class should extend the AbstractTransactionService
*/
public abstract class XC_AbstractTransactionService {
protected final commercepayments.SalesforceResultCode SUCCESS_SALESFORCE_RESULT_CODE =
commercepayments.SalesforceResultCode.SUCCESS;
protected final commercepayments.SalesforceResultCode DECLINE_SALESFORCE_RESULT_CODE =
commercepayments.SalesforceResultCode.DECLINE;
protected commercepayments.paymentGatewayContext gatewayContext;
public XC_AbstractTransactionService(commercepayments.paymentGatewayContext gatewayContext) {
this.gatewayContext = gatewayContext;
}
public abstract HttpRequest buildRequest();
public abstract commercepayments.GatewayResponse buildResponse(HttpResponse response);
public commercepayments.GatewayResponse execute() {
HttpRequest req;
try {
req = buildRequest();
} catch(Exception e) {
return new commercepayments.GatewayErrorResponse('400', e.getMessage());
}
commercepayments.PaymentsHttp http = new commercepayments.PaymentsHttp();
HttpResponse res = null;
try {
res = http.send(req);
} catch(CalloutException ce) {
return new commercepayments.GatewayErrorResponse('500', ce.getMessage());
}
try {
return buildResponse(res);
} catch(Exception e) {
return new commercepayments.GatewayErrorResponse('400',
'Something went wrong in the Gateway Adapter : ' + e.getMessage());
}
}
protected commercepayments.SalesforceResultCodeInfo getSalesforceResultCodeInfo(String resultCode) {
if (resultCode == SUCCESS_SALESFORCE_RESULT_CODE.name()) {
return new commercepayments.SalesforceResultCodeInfo(commercepayments.SalesforceResultCode.Success);
} else {
return new commercepayments.SalesforceResultCodeInfo(commercepayments.SalesforceResultCode.Decline);
}
}
}
Figure 2 - XC_AbstractTransactionService
XC_CaptureTransactionService
The type of context for capture payment is commercepayments.CaptureRequest. This object has two important fields: the amount to capture, and the ID to the payment authorization record. Figure 3 below illustrates the build request logic for payment capture. The build request method sets the HTTP endpoint and the body with the capture amount. In addition, this implementation needs the transaction ID for the payment authorization. This transaction ID can be retrieved off of the PaymentAuthorization record.
public class XC_CaptureTransactionService extends XC_AbstractTransactionService {
protected commercepayments.CaptureRequest captureRequest;
public XC_CaptureTransactionService(commercepayments.paymentGatewayContext gatewayContext){
super(gatewayContext);
this.captureRequest = (commercepayments.CaptureRequest)gatewayContext.getPaymentRequest();
}
private PaymentAuthorization getAuthObjectForCapture(String authorizationId, Boolean
IS_MULTICURRENCY_ORG){
List listOfFields = new List{'GatewayAuthCode','GatewayRefNumber','GatewayRefDetails' };
if (IS_MULTICURRENCY_ORG) {
listOfFields.add('CurrencyIsoCode');
}
PaymentAuthorization authObject = (PaymentAuthorization)DaoService.getSobjectById(authorizationId,
listOfFields, PaymentAuthorization.SObjectType);
return authObject;
}
public override HttpRequest buildRequest(){
HttpRequest req = new HttpRequest();
req.setEndpoint('/transactions/{$TRANSACTION_ID}/submit_for_settlement');
req.setMethod('PUT');
req.setBody(System.JSON.serialize(new Map{ 'amount' => this.captureRequest.amount }));
// Gets the transaction ID for the endpoint
PaymentAuthorization authObject = getAuthObjectForCapture(this.captureRequest.paymentAuthorizationId,
UserInfo.isMultiCurrencyOrganization());
req.setEndpoint(req.getEndpoint().replace('{$TRANSACTION_ID}', authObject.GatewayRefNumber));
return req;
}
Figure 3 - XC_CaptureTransactionService Build Request
Figure 4 below illustrates the build response method. This example assumes the response is in XML format. The method returns a commercePayments.CaptureResponse object which has a couple of attributes for transaction reference number, result code, result code description, and reference details. The import attribute is transaction reference number; this stores the transaction ID from the payment gateway for the new transaction that was created as a result of the capture. This ID is what will be sent to the payment gateway if you need to issue a refund to the customer.
public override commercepayments.GatewayResponse buildResponse(HttpResponse response) {
commercepayments.CaptureResponse captureResponse = new commercepayments.CaptureResponse();
String salesforceResultCode = null;
String statusCode = String.valueOf(response.getStatusCode());
Dom.Document doc = new Dom.Document();
doc.load(response.getBody());
Dom.XmlNode rootNode = doc.getRootElement();
// Checks the transaction status
if (null != rootNode.getChildElement('transaction_status', null)) {
String responseCode = rootNode.getChildElement('gateway_response_code',null).getText();
String transactionStatus = rootNode.getChildElement('transaction_status', null).getText();
if ('approved' == transactionStatus) {
salesforceResultCode= SUCCESS_SALESFORCE_RESULT_CODE.name();
// Successful gateway response
Map refDetails = new Map();
refDetails.put('processor_authorization_code', rootNode.getChildElement( code', null).getText());
refDetails.put('method', rootNode.getChildElement('payment_method', null).getText());
captureResponse.setGatewayReferenceDetails(JSON.Serialize(refDetails));
captureResponse.setGatewayResultCode(responseCode);
captureResponse.setGatewayResultCodeDescription(rootNode.getChildElement('msg', null).getText());
captureResponse.setGatewayReferenceNumber(rootNode.getChildElement('trans_id', null).getText());
} else {
salesforceResultCode= DECLINE_SALESFORCE_RESULT_CODE.name();
captureResponse.setGatewayResultCodeDescription(responseCode);
captureResponse.setGatewayResultCodeDescription(transactionStatus);
}
captureResponse.setSalesforceResultCodeInfo(getSalesforceResultCodeInfo(salesforceResultCode));
} else {
String message = rootNode.getChildElement('message', null).getText();
return new commercepayments.GatewayErrorResponse(statusCode, message);
}
if(null != rootNode.getChildElement('amount', null)) {
captureResponse.setAmount(Double.valueOf(rootNode.getChildElement('amount', null).getText()));
}
captureResponse.setGatewayDate(System.now());
return captureResponse;
}
}
Figure 4 - XC_CaptureTransactionService Build Response
Metadata and Salesforce Records
In addition to the Apex Code required, there are three metadata and/or data records needed to let Salesforce Order Management know which payment gateway to utilize. First a named credential is needed to define the callout to the third-party payment gateway. Second a PaymentGatewayProvider record is needed to point Salesforce to the Apex code we just created. Finally, a PaymentGateway is required to tie the named credential and payment gateway provider together.
Named Credential
A named credential is required to specify the endpoint that the callout utilizes which is made by the payment gateway provider. The named credential whitelists the endpoint and allows callouts to be made to a remote system. Figure 5 below illustrates setting up a named credential from the setup menu in Salesforce. It is important to note that you will need to setup multiple named credentials if you plan to connect your payment gateway provider to both a production and sandbox payment gateway. One named credential is needed for each environment.
Figure 5 - Named Credential Setup
PaymentGatewayProvider
Next, we need to setup the payment gateway provider which currently can only be done via Workbench. To do this, we need to insert a new PaymentGatewayProvider record and point it to our main Apex class we created. Figure 6 below shows how to setup the provider via Workbench.
Figure 6 - Payment Gateway Provider Setup
PaymentGateway
The last record we need is the payment gateway which ties the named credential and gateway provider together. The payment gateway is what Salesforce Order Management uses to link Salesforce Commerce Cloud (SFCC) payments to Salesforce payments. You can create a new PaymentGateway by going to the PaymentGateway tab in the Order Management App. Figure 7 below shows how to setup a PaymentGateway record. The payment gateway is linked to the gateway provider and the named credential that we created earlier. Also, you need to mark the gateway as Active for everything to work properly. Finally, last but not least the, external reference must match the name of the SFCC payment processor that was utilized to authorize the payment. This external reference field is how Salesforce Order Management links SFCC payments OM payments.
Figure 7 - Payment Gateway Setup
Conclusion
Now that we have the code created and configured a few records, we can test out our new payment capture integration. Salesforce Order Management captures payments once orders are fulfilled. To simulate this, you need to update the Status field on an FulfillmentOrder record to Fulfilled. This status change causes Salesforce Order Management to invoke a flow which uses our payment gateway adapter to capture the funds. Now you can integrate with your preferred payment provider even if does not have a prebuilt plugin.