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
+