Skip to content

Commit

Permalink
Authorization reversal - sync part (#53)
Browse files Browse the repository at this point in the history
* feat: payment auth reversal logic - draft

* fix: missing '/' on mocked endpoint

* feat: reverse auth (sync part)

* feat: cancel endpoint field

* feat: layout including cancel endpoint field

* fix: cannot create required custom field

---------

Co-authored-by: daniloc <[email protected]>
  • Loading branch information
dcardos and daniloc authored Aug 30, 2024
1 parent 1d5537a commit 6f98e05
Show file tree
Hide file tree
Showing 18 changed files with 234 additions and 61 deletions.
50 changes: 49 additions & 1 deletion force-app/main/default/classes/AdyenAuthorisationHelper.cls
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
public with sharing class AdyenAuthorisationHelper {
public static final String PSP_MISSING_ERROR = 'PspReference Missing';
public static final String AMOUNT_MISSING_ERROR = 'Payment Amount Missing';
public static final String AMOUNT_MISMATCH_ERROR = 'Authorization reversal amount of {0} does not match available to capture amount left of: {1}';

/**
* Calls Adyen service to post an AUTH request to Adyen.
* @param authRequest from CommercePayments
* @return authResponse
*/
public static CommercePayments.GatewayResponse authorise(CommercePayments.AuthorizationRequest authRequest) {
Adyen_Adapter__mdt adyenAdapterMdt = AdyenPaymentUtility.retrieveGatewayMetadata(AdyenConstants.DEFAULT_ADAPTER_NAME);
Adyen_Adapter__mdt adyenAdapterMdt = AdyenPaymentUtility.retrieveAdapterByDeveloperName(AdyenConstants.DEFAULT_ADAPTER_NAME);

AuthorisationRequest adyenAuthorisationRequest = createAuthorisationRequest(authRequest, adyenAdapterMdt);
HttpResponse adyenHttpResponse = sendAuthorisationRequest(adyenAuthorisationRequest, adyenAdapterMdt);
Expand Down Expand Up @@ -112,4 +115,49 @@ public with sharing class AdyenAuthorisationHelper {
String endpoint = adyenAdapterMdt.Endpoint_Api_Version__c + adyenAdapterMdt.Authorize_Endpoint__c;
return AdyenPaymentUtility.makePostRequest(endpoint, body);
}

public static CommercePayments.GatewayResponse reverseAuth(CommercePayments.AuthorizationReversalRequest authReversalRequest) {
PaymentAuthorization paymentAuth = AdyenPaymentUtility.retrievePaymentAuthorization(authReversalRequest.paymentAuthorizationId);
OrderPaymentSummary orderPaymentSummary = AdyenPaymentUtility.retrieveOrderPaymentSummary(paymentAuth.OrderPaymentSummaryId);

String errorMessage;
if (String.isBlank(paymentAuth.GatewayRefNumber)) {
errorMessage = PSP_MISSING_ERROR;
} else if (authReversalRequest.amount == null) {
errorMessage = AMOUNT_MISSING_ERROR;
} else if (orderPaymentSummary.AvailableToCaptureAmount != authReversalRequest.amount) {
errorMessage = String.format(AMOUNT_MISMATCH_ERROR, new List<Object>{authReversalRequest.amount, orderPaymentSummary.AvailableToCaptureAmount});
}
if (String.isNotBlank(errorMessage)) {
throw new AdyenGatewayAdapter.GatewayException(errorMessage);
}

String pspReference = paymentAuth.GatewayRefNumber;
String merchantAccount = paymentAuth.OrderPaymentSummary.OrderSummary.SalesChannel.AdyenMerchantID__c;
Adyen_Adapter__mdt adyenAdapterMdt = AdyenPaymentUtility.chooseAdapterWithFallBack(merchantAccount);

CancelRequest cancelRequest = new CancelRequest();
cancelRequest.merchantAccount = adyenAdapterMdt.Merchant_Account__c;
cancelRequest.reference = paymentAuth.PaymentAuthorizationNumber;
cancelRequest.applicationInfo = AdyenPaymentUtility.getApplicationInfo(adyenAdapterMdt.System_Integrator_Name__c);
String endpoint = adyenAdapterMdt.Endpoint_Api_Version__c + adyenAdapterMdt.Cancel_Endpoint__c;
endpoint = endpoint.replace('{paymentPspReference}', pspReference);

HttpResponse response = AdyenPaymentUtility.makePostRequest(endpoint, JSON.serialize(cancelRequest, true));
String salesforceCompatibleBody = AdyenPaymentUtility.makeSalesforceCompatible(response.getBody());
CancelResponse cancelResponse = (CancelResponse)JSON.deserialize(salesforceCompatibleBody, CancelResponse.class);
return processCancelResponse(cancelResponse, authReversalRequest.amount);
}

private static CommercePayments.GatewayResponse processCancelResponse(CancelResponse cancelResponse, Double amount) {
CommercePayments.AuthorizationReversalResponse authReversalResponse = new CommercePayments.AuthorizationReversalResponse();
authReversalResponse.setAmount(amount);
authReversalResponse.setGatewayDate(System.now());
authReversalResponse.setGatewayReferenceDetails(cancelResponse.reference);
authReversalResponse.setGatewayResultCode(cancelResponse.status);
authReversalResponse.setGatewayReferenceNumber(cancelResponse.pspReference);
authReversalResponse.setSalesforceResultCodeInfo(AdyenConstants.SUCCESS_SALESFORCE_RESULT_CODE_INFO);
authReversalResponse.setGatewayMessage('[cancellation-received]');
return authReversalResponse;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>60.0</apiVersion>
<apiVersion>61.0</apiVersion>
<status>Active</status>
</ApexClass>
60 changes: 59 additions & 1 deletion force-app/main/default/classes/AdyenAuthorisationHelperTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ private class AdyenAuthorisationHelperTest {
@IsTest
static void createAuthorisationRequestTest() {
// given
Adyen_Adapter__mdt adyenAdapterMdt = AdyenPaymentUtility.retrieveGatewayMetadata(AdyenConstants.DEFAULT_ADAPTER_NAME);
Adyen_Adapter__mdt adyenAdapterMdt = AdyenPaymentUtility.retrieveAdapterByDeveloperName(AdyenConstants.DEFAULT_ADAPTER_NAME);
AuthorisationRequest adyenAuthRequest;
Double price;
CommercePayments.AuthorizationRequest authRequest;
Expand Down Expand Up @@ -95,4 +95,62 @@ private class AdyenAuthorisationHelperTest {
}
Test.stopTest();
}

@IsTest(SeeAllData = true)
static void reverseAuthTest() {
// given
Account acct = TestDataFactory.createAccount();
insert acct;
Order order = TestDataFactory.insertOrderAndRelatedRecords(acct.Id, 33.42, 0.96);
OrderPaymentSummary orderPaymentSummary = TestDataFactory.createOrderSummaryRecords(order.Id);
Double price = [SELECT AvailableToCaptureAmount FROM OrderPaymentSummary WHERE Id = :orderPaymentSummary.Id].AvailableToCaptureAmount;
TestDataFactory.insertBasicPaymentRecords(acct.Id, orderPaymentSummary.Id);
PaymentAuthorization payAuth = [SELECT Id, GatewayRefNumber FROM PaymentAuthorization WHERE OrderPaymentSummaryId = :orderPaymentSummary.Id];
CommercePayments.AuthorizationReversalRequest authReversalRequest = new CommercePayments.AuthorizationReversalRequest(price, payAuth.Id);
Test.setMock(HttpCalloutMock.class, new TestDataFactory.CancelsMockResponse());
// when
Test.startTest();
CommercePayments.GatewayResponse gatewayResponse = AdyenAuthorisationHelper.reverseAuth(authReversalRequest);
Test.stopTest();
// then
Assert.isTrue(gatewayResponse.toString().containsIgnoreCase('cancellation-received'));
}

@IsTest(SeeAllData = true)
static void reverseAuthValidationErrorTest() {
// no PA with gateway reference
Account acct = TestDataFactory.createAccount();
insert acct;
Order order = TestDataFactory.insertOrderAndRelatedRecords(acct.Id, 33.42, 0.96);
OrderPaymentSummary orderPaymentSummary = TestDataFactory.createOrderSummaryRecords(order.Id);
PaymentAuthorization payAuth = TestDataFactory.createPaymentAuthorization(acct.Id, null, null, orderPaymentSummary.Id, null);
insert payAuth;
CommercePayments.AuthorizationReversalRequest authReversalRequest = new CommercePayments.AuthorizationReversalRequest(Double.valueOf(1.99), payAuth.Id);
try {
AdyenAuthorisationHelper.reverseAuth(authReversalRequest);
Assert.fail();
} catch (Exception ex) {
Assert.areEqual(ex.getMessage(), AdyenAuthorisationHelper.PSP_MISSING_ERROR);
}
// Authorization Amount is null
payAuth = TestDataFactory.createPaymentAuthorization(acct.Id, null, null, orderPaymentSummary.Id, TestDataFactory.TEST_PSP_REFERENCE);
insert payAuth;
authReversalRequest = new CommercePayments.AuthorizationReversalRequest(null, payAuth.Id);
try {
AdyenAuthorisationHelper.reverseAuth(authReversalRequest);
Assert.fail();
} catch (Exception ex) {
Assert.areEqual(ex.getMessage(), AdyenAuthorisationHelper.AMOUNT_MISSING_ERROR);
}
// Amount mismatch
Decimal availableToCapture = [SELECT AvailableToCaptureAmount FROM OrderPaymentSummary WHERE Id = :orderPaymentSummary.Id].AvailableToCaptureAmount;
Double price = availableToCapture - 1;
authReversalRequest = new CommercePayments.AuthorizationReversalRequest(price, payAuth.Id);
try {
AdyenAuthorisationHelper.reverseAuth(authReversalRequest);
Assert.fail();
} catch (Exception ex) {
Assert.areEqual(ex.getMessage(), String.format(AdyenAuthorisationHelper.AMOUNT_MISMATCH_ERROR, new List<Object>{price,availableToCapture}));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>60.0</apiVersion>
<apiVersion>61.0</apiVersion>
<status>Active</status>
</ApexClass>
7 changes: 6 additions & 1 deletion force-app/main/default/classes/AdyenOMSConstants.cls
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,10 @@ public with sharing class AdyenOMSConstants {
public static final String ALTERNATIVE_PAYMENT_METHOD_OBJECT = 'AlternativePaymentMethod';

public static final Set<String> OPEN_INVOICE_METHODS = new Set<String>{'klarna', 'afterpay', 'ratepay', 'facilypay', 'zip', 'affirm', 'atome', 'walley', 'clearpay'};
public static final Set<String> VALID_NOTIFICATION_TYPES = new Set<String>{AdyenConstants.NOTIFICATION_REQUEST_TYPE_CAPTURE, AdyenConstants.NOTIFICATION_REQUEST_TYPE_REFUND, AdyenConstants.NOTIFICATION_REQUEST_TYPE_CAPTURE_FAILED, AdyenConstants.NOTIFICATION_REQUEST_TYPE_REFUND_FAILED};
public static final Set<String> VALID_NOTIFICATION_TYPES = new Set<String>{
AdyenConstants.NOTIFICATION_REQUEST_TYPE_CAPTURE,
AdyenConstants.NOTIFICATION_REQUEST_TYPE_REFUND,
AdyenConstants.NOTIFICATION_REQUEST_TYPE_CAPTURE_FAILED,
AdyenConstants.NOTIFICATION_REQUEST_TYPE_REFUND_FAILED
};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>60.0</apiVersion>
<apiVersion>61.0</apiVersion>
<status>Active</status>
</ApexClass>
2 changes: 2 additions & 0 deletions force-app/main/default/classes/AdyenPaymentHelper.cls
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public with sharing class AdyenPaymentHelper {

if (paymentRequestType == CommercePayments.RequestType.Authorize) {
return AdyenAuthorisationHelper.authorise((CommercePayments.AuthorizationRequest)paymentRequest);
} else if (paymentRequestType == CommercePayments.RequestType.AuthorizationReversal) {
return AdyenAuthorisationHelper.reverseAuth((CommercePayments.AuthorizationReversalRequest)paymentRequest);
} else if (paymentRequestType == CommercePayments.RequestType.Capture) {
return AdyenCaptureHelper.capture((CommercePayments.CaptureRequest)paymentRequest);
} else if (paymentRequestType == CommercePayments.RequestType.ReferencedRefund) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>60.0</apiVersion>
<apiVersion>61.0</apiVersion>
<status>Active</status>
</ApexClass>
70 changes: 35 additions & 35 deletions force-app/main/default/classes/AdyenPaymentUtility.cls
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
public with sharing class AdyenPaymentUtility {
@TestVisible
private static final String NO_PAYMENT_FOUND_BY_ID = 'No Payment found with this id: ';
private static final String NO_PAYMENT_FOUND_BY_ID = 'No Payment found with id: ';
@TestVisible
private static final String NO_PAYMENT_AUTH_FOUND_BY_ID = 'No payment authorization found with this id: ';
private static final String NO_ORDER_PAY_SUM_FOUND_BY_ID = 'No Order Payment Summary found with id: ';
@TestVisible
private static final String NO_ADYEN_ADAPTER_BY_NAME = 'No Adyen adapter found with this name: ';
private static final String NO_PAYMENT_AUTH_FOUND_BY_ID = 'No payment authorization found with id: ';
@TestVisible
private static final String NO_ADYEN_ADAPTER_BY_MERCHANT = 'No Adyen adapter found for this merchant account: ';
private static final String NO_ADYEN_ADAPTER_BY_NAME = 'No Adyen adapter found with name: ';
@TestVisible
private static final String NO_ADYEN_ADAPTER_BY_MERCHANT = 'No Adyen adapter found for merchant account: ';

/**
* Retrieve Payment Info.
Expand All @@ -31,51 +33,49 @@ public with sharing class AdyenPaymentUtility {
}
return payments[0];
}


/**
* Retrieves custom meta data associated with Adyen (Endpoint info) pulls all fields.
* @param developerName name of the custom metadata type with Adyen configuration
* @return Adyen_Adapter__mdt for the passed metadata type with all fields.
*/
public static Adyen_Adapter__mdt retrieveGatewayMetadata(String developerName) {
List<Adyen_Adapter__mdt> adyenAdapters = [
SELECT
DeveloperName, MasterLabel, Capture_Endpoint__c, Endpoint_Api_Version__c,
System_Integrator_Name__c, Endpoint_Path__c, Merchant_Account__c, Refund_Endpoint__c,
Authorize_Endpoint__c, HMAC_Key__c
FROM Adyen_Adapter__mdt
WHERE DeveloperName = :developerName

public static OrderPaymentSummary retrieveOrderPaymentSummary(Id orderPaySummaryId) {
List<OrderPaymentSummary> orderPaymentSummaries = [
SELECT Id, AvailableToCaptureAmount
FROM OrderPaymentSummary
WHERE Id = :orderPaySummaryId
];
if (adyenAdapters.isEmpty()) {
throw new AdyenGatewayAdapter.GatewayException(NO_ADYEN_ADAPTER_BY_NAME + developerName);
if (orderPaymentSummaries.isEmpty()) {
throw new AdyenGatewayAdapter.GatewayException(NO_ORDER_PAY_SUM_FOUND_BY_ID + orderPaySummaryId);
}
return adyenAdapters[0];
return orderPaymentSummaries[0];
}

public static Adyen_Adapter__mdt retrieveAdapterByDeveloperName(String developerName) {
return retrieveAdapter('DeveloperName', developerName, NO_ADYEN_ADAPTER_BY_NAME);
}

public static Adyen_Adapter__mdt retrieveAdapterByMerchantAcct(String merchantAccountName) {
List<Adyen_Adapter__mdt> adyenAdapters = [
SELECT
DeveloperName, MasterLabel, Capture_Endpoint__c, Endpoint_Api_Version__c,
System_Integrator_Name__c, Endpoint_Path__c, Merchant_Account__c, Refund_Endpoint__c,
Authorize_Endpoint__c, HMAC_Key__c
FROM Adyen_Adapter__mdt
WHERE Merchant_Account__c = :merchantAccountName
];
if (adyenAdapters.isEmpty()) {
throw new AdyenGatewayAdapter.GatewayException(NO_ADYEN_ADAPTER_BY_MERCHANT + merchantAccountName);
}
return adyenAdapters[0];
return retrieveAdapter('Merchant_Account__c', merchantAccountName, NO_ADYEN_ADAPTER_BY_MERCHANT);
}

public static Adyen_Adapter__mdt chooseAdapterWithFallBack(String merchantAccountName) {
if (String.isNotBlank(merchantAccountName)) {
return retrieveAdapterByMerchantAcct(merchantAccountName);
} else {
return retrieveGatewayMetadata(AdyenConstants.DEFAULT_ADAPTER_NAME);
return retrieveAdapter('DeveloperName', AdyenConstants.DEFAULT_ADAPTER_NAME, NO_ADYEN_ADAPTER_BY_NAME);
}
}

private static Adyen_Adapter__mdt retrieveAdapter(String fieldName, String fieldValue, String errorMessage) {
String query = 'SELECT DeveloperName, MasterLabel, Capture_Endpoint__c, Endpoint_Api_Version__c, ' +
'System_Integrator_Name__c, Endpoint_Path__c, Merchant_Account__c, Refund_Endpoint__c, ' +
'Authorize_Endpoint__c, HMAC_Key__c, Cancel_Endpoint__c ' +
'FROM Adyen_Adapter__mdt WHERE ' + fieldName + ' = :fieldValue';

List<Adyen_Adapter__mdt> adyenAdapters = Database.query(query);

if (adyenAdapters.isEmpty()) {
throw new AdyenGatewayAdapter.GatewayException(errorMessage + fieldValue);
}
return adyenAdapters[0];
}

public static Boolean isValidNotification(NotificationRequestItem notificationRequestItem) {
return AdyenOMSConstants.VALID_NOTIFICATION_TYPES.contains(notificationRequestItem.eventCode.toUpperCase())
&& isValidPspReference(notificationRequestItem.originalReference)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>60.0</apiVersion>
<apiVersion>61.0</apiVersion>
<status>Active</status>
</ApexClass>
44 changes: 41 additions & 3 deletions force-app/main/default/classes/AdyenPaymentUtilityTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ private class AdyenPaymentUtilityTest {
CommercePayments.CaptureRequest captureRequest;
String currencyCode = 'USD';
Double price; // request comes as Double value
Adyen_Adapter__mdt adyenAdapterMdt = AdyenPaymentUtility.retrieveGatewayMetadata(AdyenConstants.DEFAULT_ADAPTER_NAME);
Adyen_Adapter__mdt adyenAdapterMdt = AdyenPaymentUtility.retrieveAdapterByDeveloperName(AdyenConstants.DEFAULT_ADAPTER_NAME);
Decimal expectedPrice;
CheckoutCaptureRequest modificationRequest;
for (Integer i = 0; i < 10; i++) {
Expand Down Expand Up @@ -154,6 +154,46 @@ private class AdyenPaymentUtilityTest {
Assert.areNotEqual(reference1, reference2);
}

@IsTest
static void retrieveAdapterByDeveloperNameTest() {
Assert.isNotNull(AdyenPaymentUtility.retrieveAdapterByDeveloperName(AdyenConstants.DEFAULT_ADAPTER_NAME));
String notADeveloperName = '123';
try {
AdyenPaymentUtility.retrieveAdapterByDeveloperName(notADeveloperName);
Assert.fail();
} catch (Exception ex) {
Assert.areEqual(ex.getMessage(), AdyenPaymentUtility.NO_ADYEN_ADAPTER_BY_NAME + notADeveloperName);
}
}

@IsTest
static void retrieveAdapterByMerchantAcctTest() {
Adyen_Adapter__mdt adyenAdapter = AdyenPaymentUtility.retrieveAdapterByDeveloperName(AdyenConstants.DEFAULT_ADAPTER_NAME);
Assert.isNotNull(AdyenPaymentUtility.retrieveAdapterByMerchantAcct(adyenAdapter.Merchant_Account__c));
String notAMerchantAcct = '123';
try {
AdyenPaymentUtility.retrieveAdapterByMerchantAcct(notAMerchantAcct);
Assert.fail();
} catch (Exception ex) {
Assert.areEqual(ex.getMessage(), AdyenPaymentUtility.NO_ADYEN_ADAPTER_BY_MERCHANT + notAMerchantAcct);
}
}

@IsTest(SeeAllData=true)
static void retrieveOrderPaymentSummaryTest() {
Account acct = TestDataFactory.createAccount();
insert acct;
Order order = TestDataFactory.insertOrderAndRelatedRecords(acct.Id, 33.42, 0.96);
OrderPaymentSummary orderPaymentSummary = TestDataFactory.createOrderSummaryRecords(order.Id);
Assert.isNotNull(AdyenPaymentUtility.retrieveOrderPaymentSummary(orderPaymentSummary.Id));
try {
AdyenPaymentUtility.retrieveOrderPaymentSummary(acct.Id);
Assert.fail();
} catch (Exception ex) {
Assert.areEqual(ex.getMessage(), AdyenPaymentUtility.NO_ORDER_PAY_SUM_FOUND_BY_ID + acct.Id);
}
}

private static OrderPaymentSummary createInvoiceAndRelatedRecords(Decimal price, Decimal taxValue) {
Account acct = TestDataFactory.createAccount();
insert acct;
Expand All @@ -174,6 +214,4 @@ private class AdyenPaymentUtilityTest {
Id creditMemoId = TestDataFactory.createCreditMemo(orderSummaryId, changeOrderId);
return creditMemoId;
}


}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>60.0</apiVersion>
<apiVersion>61.0</apiVersion>
<status>Active</status>
</ApexClass>
Loading

0 comments on commit 6f98e05

Please sign in to comment.