diff --git a/force-app/main/default/classes/AdyenPBLHelper.cls b/force-app/main/default/classes/AdyenPBLHelper.cls new file mode 100644 index 0000000..b4e54a0 --- /dev/null +++ b/force-app/main/default/classes/AdyenPBLHelper.cls @@ -0,0 +1,72 @@ +public with sharing class AdyenPBLHelper { + + public static PaymentLinkRequest buildPaymentLinkRequest(Adyen_Adapter__mdt adyenAdapter, Amount amount, String reference) { + PaymentLinkRequest paymentLinkRequest = new PaymentLinkRequest(); + paymentLinkRequest.amount = amount; + paymentLinkRequest.reference = reference; + paymentLinkRequest.merchantAccount = adyenAdapter.Merchant_Account__c; + paymentLinkRequest.applicationInfo = AdyenPaymentUtility.getApplicationInfo(adyenAdapter.System_Integrator_Name__c); + paymentLinkRequest.returnUrl = adyenAdapter.Payment_Link_Return_Url__c; + paymentLinkRequest.themeId = adyenAdapter.Payment_Link_Theme_Id__c; + paymentLinkRequest.expiresAt = adyenAdapter.Payment_Link_Expiry_Duration__c != null + ? getLinkExpiryDate(adyenAdapter.Payment_Link_Expiry_Duration__c.intValue()) + : null; + + return paymentLinkRequest; + } + + public static PaymentLinkResponse generatePaymentLink(Adyen_Adapter__mdt adyenAdapter, PaymentLinkRequest paymentLinkRequest) { + if (adyenAdapter == null || paymentLinkRequest == null) { + throw new IllegalArgumentException('One or more input parameters are null'); + } + + try { + String endpoint = buildEndpoint(adyenAdapter, 'Payment_Link_Endpoint__c'); + String body = AdyenPaymentUtility.makeAdyenCompatible(JSON.serialize(paymentLinkRequest, true)); + + HttpRequest request = new HttpRequest(); + request.setEndpoint(endpoint); + request.setMethod('POST'); + request.setHeader('Content-Type', 'application/json'); + request.setBody(body); + + HttpResponse response = new Http().send(request); + + if (response.getStatusCode() != 201) { + throw new AdyenGatewayAdapter.GatewayException('Adyen Checkout API returned: ' + response.getStatusCode() + ', body: ' + response.getBody()); + } else { + String salesforceCompatibleBody = AdyenPaymentUtility.makeSalesforceCompatible(response.getBody()); + PaymentLinkResponse paymentLinkResponse = (PaymentLinkResponse)JSON.deserialize(salesforceCompatibleBody, PaymentLinkResponse.class); + return paymentLinkResponse; + } + } catch (Exception e) { + throw new AdyenGatewayAdapter.GatewayException('Error generating payment link: ' + e.getMessage()); + } + } + + private static String buildEndpoint(Adyen_Adapter__mdt adyenAdapter, String endpointFieldName) { + String namedCredential = adyenAdapter.Named_Credential__c; + String namespace = String.isNotBlank(adyenAdapter.Package_Namespace__c) ? adyenAdapter.Package_Namespace__c + '__' : ''; + String endpointUrl = 'callout:' + namespace + namedCredential; + String apiVersion = adyenAdapter.Endpoint_Api_Version__c; + String endpointPath = (String) adyenAdapter.get(endpointFieldName); + + return endpointUrl + apiVersion + endpointPath; + } + + @TestVisible + private static String getLinkExpiryDate(Integer days) { + Datetime currentDateTime = Datetime.now(); + Datetime expiryDateTime = currentDateTime.addDays(days); + + return formatDateTimeToISO8601(expiryDateTime); + } + + private static String formatDateTimeToISO8601(Datetime dt) { + String iso8601Date = dt.formatGmt('yyyy-MM-dd\'T\'HH:mm:ss'); + String timeZoneOffset = Datetime.now().format('Z'); + String formattedOffset = timeZoneOffset.substring(0, 3) + ':' + timeZoneOffset.substring(3); + + return iso8601Date + formattedOffset; + } +} \ No newline at end of file diff --git a/force-app/main/default/classes/AdyenPBLHelper.cls-meta.xml b/force-app/main/default/classes/AdyenPBLHelper.cls-meta.xml new file mode 100644 index 0000000..7d5f9e8 --- /dev/null +++ b/force-app/main/default/classes/AdyenPBLHelper.cls-meta.xml @@ -0,0 +1,5 @@ + + + 61.0 + Active + \ No newline at end of file diff --git a/force-app/main/default/classes/AdyenPBLHelperTest.cls b/force-app/main/default/classes/AdyenPBLHelperTest.cls new file mode 100644 index 0000000..cfe29d4 --- /dev/null +++ b/force-app/main/default/classes/AdyenPBLHelperTest.cls @@ -0,0 +1,105 @@ +@IsTest +private class AdyenPBLHelperTest { + + @IsTest + static void buildPaymentLinkRequestSuccessTest() { + // Given + Adyen_Adapter__mdt adyenAdapter = [SELECT Id, Payment_Link_Endpoint__c, Endpoint_Api_Version__c, Merchant_Account__c, + Named_Credential__c, Package_Namespace__c, Payment_Link_Return_Url__c, + Payment_Link_Theme_Id__c, System_Integrator_Name__c, Payment_Link_Expiry_Duration__c + FROM Adyen_Adapter__mdt LIMIT 1]; + Amount amount = new Amount(); + amount.currency_x = 'EUR'; + amount.value = 1000; + String reference = 'TestReference123'; + + // When + PaymentLinkRequest paymentLinkRequest = AdyenPBLHelper.buildPaymentLinkRequest(adyenAdapter, amount, reference); + + // Then + Assert.areNotEqual(null, paymentLinkRequest); + Assert.areEqual(amount, paymentLinkRequest.amount); + Assert.areEqual(reference, paymentLinkRequest.reference); + Assert.areEqual(adyenAdapter.Merchant_Account__c, paymentLinkRequest.merchantAccount); + Assert.areEqual(adyenAdapter.Payment_Link_Return_Url__c, paymentLinkRequest.returnUrl); + Assert.areEqual(adyenAdapter.Payment_Link_Theme_Id__c, paymentLinkRequest.themeId); + } + + @IsTest + static void generatePaymentLinkSuccessTest() { + // Given + Adyen_Adapter__mdt adyenAdapter = [SELECT Id, Payment_Link_Endpoint__c, Endpoint_Api_Version__c, Merchant_Account__c, + Named_Credential__c, Package_Namespace__c, Payment_Link_Return_Url__c, + Payment_Link_Theme_Id__c, System_Integrator_Name__c, Payment_Link_Expiry_Duration__c + FROM Adyen_Adapter__mdt LIMIT 1]; + Amount amount = new Amount(); + amount.currency_x = 'USD'; + amount.value = 2000; + String reference = 'TestReference456'; + + PaymentLinkRequest paymentLinkRequest = AdyenPBLHelper.buildPaymentLinkRequest(adyenAdapter, amount, reference); + + Test.setMock(HttpCalloutMock.class, new TestDataFactory.PBLMockResponse()); + + // When + PaymentLinkResponse response = AdyenPBLHelper.generatePaymentLink(adyenAdapter, paymentLinkRequest); + + // Then + Assert.areNotEqual(null, response); + Assert.areEqual('https://test.payment.link', response.url); + Assert.areEqual('2024-01-01T12:00:00+00:00', response.expiresAt); + } + + @IsTest + static void generatePaymentLinkNullParametersTest() { + // Given + Adyen_Adapter__mdt adyenAdapter = null; + PaymentLinkRequest paymentLinkRequest = null; + + // When/Then + try { + AdyenPBLHelper.generatePaymentLink(adyenAdapter, paymentLinkRequest); + Assert.fail('Expected Exception was not thrown'); + } catch (Exception e) { + Assert.isInstanceOfType(e, IllegalArgumentException.class); + } + } + + @IsTest + static void generatePaymentLinkErrorResponseTest() { + // Given + Adyen_Adapter__mdt adyenAdapter = [SELECT Id, Payment_Link_Endpoint__c, Endpoint_Api_Version__c, Merchant_Account__c, + Named_Credential__c, Package_Namespace__c, Payment_Link_Return_Url__c, + Payment_Link_Theme_Id__c, System_Integrator_Name__c, Payment_Link_Expiry_Duration__c + FROM Adyen_Adapter__mdt LIMIT 1]; + Amount amount = new Amount(); + amount.currency_x = 'EUR'; + amount.value = 2000; + String reference = 'TestReference789'; + + PaymentLinkRequest paymentLinkRequest = AdyenPBLHelper.buildPaymentLinkRequest(adyenAdapter, amount, reference); + + Test.setMock(HttpCalloutMock.class, new TestDataFactory.FailureResponse()); + + // When/Then + try { + AdyenPBLHelper.generatePaymentLink(adyenAdapter, paymentLinkRequest); + Assert.fail('Expected Exception was not thrown'); + } catch (Exception e) { + Assert.isInstanceOfType(e, AdyenGatewayAdapter.GatewayException.class); + } + } + + @IsTest + static void testGetLinkExpiryDate() { + // Given + Integer days = 5; + + // When + String expectedDate = Datetime.now().addDays(days).formatGMT('yyyy-MM-dd\'T\'HH:mm:ss'); + String expiryDate = AdyenPBLHelper.getLinkExpiryDate(days); + + // Then + Assert.isTrue(expiryDate.contains(expectedDate), 'Expiry date should contain the correct future date.'); + } +} diff --git a/force-app/main/default/classes/AdyenPBLHelperTest.cls-meta.xml b/force-app/main/default/classes/AdyenPBLHelperTest.cls-meta.xml new file mode 100644 index 0000000..7d5f9e8 --- /dev/null +++ b/force-app/main/default/classes/AdyenPBLHelperTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 61.0 + Active + \ No newline at end of file diff --git a/force-app/main/default/classes/TestDataFactory.cls b/force-app/main/default/classes/TestDataFactory.cls index 5a1494d..14aa3a3 100644 --- a/force-app/main/default/classes/TestDataFactory.cls +++ b/force-app/main/default/classes/TestDataFactory.cls @@ -315,6 +315,12 @@ public class TestDataFactory { } } + public class PBLMockResponse implements HttpCalloutMock { + public HttpResponse respond(HttpRequest req) { + return mockHttpResponse(pblResponse(), 201); + } + } + public static HttpResponse mockHttpResponse(String body, Integer code) { HttpResponse res = new HttpResponse(); res.setHeader('Content-Type', 'text/json'); @@ -331,6 +337,10 @@ public class TestDataFactory { return '{"merchantAccount":"PluginDemo_Danilo_TEST","paymentPspReference":"NWQTRSVVQ8Q94875","pspReference":"M4CV9RJFPT4CZX65","reference":"PA-000000037","status":"received"}'; } + private static String pblResponse() { + return '{ "url": "https://test.payment.link", "expiresAt": "2024-01-01T12:00:00+00:00" }'; + } + public static String mockWebhookRequest(String eventCode, String pspReference, String originalReference, String merchantRef, Boolean success) { String merchantAccountName = [SELECT Merchant_Account__c FROM Adyen_Adapter__mdt LIMIT 1].Merchant_Account__c; return '{"live": "false", "notificationItems": [{"NotificationRequestItem": {"additionalData": {"hmacSignature": "testsignature"}, "amount": {"currency": "'+ ACTIVE_CURRENCY +'", "value": 1000}, "eventCode": "' + eventCode + '", "eventDate": "2024-01-01T01:00:00+01:00", "merchantAccountCode": "' + merchantAccountName + '", "merchantReference": "' + merchantRef + '", "paymentMethod": "visa", "pspReference": "' + pspReference + '", "originalReference": "' + originalReference + '", "reason": "null", "success": "' + success + '"}}]}'; diff --git a/force-app/main/default/customMetadata/Adyen_Adapter.AdyenDefault.md-meta.xml b/force-app/main/default/customMetadata/Adyen_Adapter.AdyenDefault.md-meta.xml index 04e6040..e06c853 100644 --- a/force-app/main/default/customMetadata/Adyen_Adapter.AdyenDefault.md-meta.xml +++ b/force-app/main/default/customMetadata/Adyen_Adapter.AdyenDefault.md-meta.xml @@ -26,6 +26,18 @@ Merchant_Account__c Merchant_Account_Name + + Named_Credential__c + AdyenCheckout + + + Package_Namespace__c + adyen_payment + + + Payment_Link_Endpoint__c + /paymentLinks + Refund_Endpoint__c /payments/{paymentPspReference}/refunds diff --git a/force-app/main/default/layouts/Adyen_Adapter__mdt-Adyen Adapter Layout.layout-meta.xml b/force-app/main/default/layouts/Adyen_Adapter__mdt-Adyen Adapter Layout.layout-meta.xml index c2e45eb..937c52c 100644 --- a/force-app/main/default/layouts/Adyen_Adapter__mdt-Adyen Adapter Layout.layout-meta.xml +++ b/force-app/main/default/layouts/Adyen_Adapter__mdt-Adyen Adapter Layout.layout-meta.xml @@ -18,6 +18,10 @@ Required HMAC_Key__c + + Edit + Payment_Link_Expiry_Duration__c + @@ -28,6 +32,14 @@ Edit System_Integrator_Name__c + + Edit + Payment_Link_Return_Url__c + + + Edit + Payment_Link_Theme_Id__c + @@ -53,6 +65,14 @@ Edit Endpoint_Path__c + + Edit + Package_Namespace__c + + + Edit + Named_Credential__c + @@ -71,6 +91,10 @@ Edit Refund_Endpoint__c + + Edit + Payment_Link_Endpoint__c + @@ -104,7 +128,7 @@ false false - 00ha5000004n2Xq + 00ha5000003qQO5 4 0 Default diff --git a/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Named_Credential__c.field-meta.xml b/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Named_Credential__c.field-meta.xml new file mode 100644 index 0000000..fb7ef49 --- /dev/null +++ b/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Named_Credential__c.field-meta.xml @@ -0,0 +1,13 @@ + + + Named_Credential__c + Named credential containing the associated external credential with the API Key for this merchant account. + false + DeveloperControlled + Used for calling Adyen with the correct Api Key + + 64 + false + Text + false + diff --git a/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Package_Namespace__c.field-meta.xml b/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Package_Namespace__c.field-meta.xml new file mode 100644 index 0000000..60f5373 --- /dev/null +++ b/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Package_Namespace__c.field-meta.xml @@ -0,0 +1,13 @@ + + + Package_Namespace__c + This field is used to hold the namespace for the Named Credential + false + DeveloperControlled + Do not change this value + + 15 + false + Text + false + diff --git a/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Payment_Link_Endpoint__c.field-meta.xml b/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Payment_Link_Endpoint__c.field-meta.xml new file mode 100644 index 0000000..fb5b388 --- /dev/null +++ b/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Payment_Link_Endpoint__c.field-meta.xml @@ -0,0 +1,12 @@ + + + Payment_Link_Endpoint__c + Endpoint used to create a payment link + false + DeveloperControlled + + 255 + false + Text + false + diff --git a/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Payment_Link_Expiry_Duration__c.field-meta.xml b/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Payment_Link_Expiry_Duration__c.field-meta.xml new file mode 100644 index 0000000..953cb53 --- /dev/null +++ b/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Payment_Link_Expiry_Duration__c.field-meta.xml @@ -0,0 +1,13 @@ + + + Payment_Link_Expiry_Duration__c + Expiration time of the payment link in days + false + SubscriberControlled + + 2 + false + 0 + Number + false + diff --git a/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Payment_Link_Return_Url__c.field-meta.xml b/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Payment_Link_Return_Url__c.field-meta.xml new file mode 100644 index 0000000..c619da8 --- /dev/null +++ b/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Payment_Link_Return_Url__c.field-meta.xml @@ -0,0 +1,12 @@ + + + Payment_Link_Return_Url__c + Website URL used for redirection after payment is completed + false + SubscriberControlled + + 255 + false + Text + false + diff --git a/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Payment_Link_Theme_Id__c.field-meta.xml b/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Payment_Link_Theme_Id__c.field-meta.xml new file mode 100644 index 0000000..8c33519 --- /dev/null +++ b/force-app/main/default/objects/Adyen_Adapter__mdt/fields/Payment_Link_Theme_Id__c.field-meta.xml @@ -0,0 +1,11 @@ + + + Payment_Link_Theme_Id__c + false + SubscriberControlled + + 255 + false + Text + false +