From efbbaf8d66f2cd7905367309386886d8df384bdb Mon Sep 17 00:00:00 2001 From: Kwok He Chu <105217051+Kwok-he-Chu@users.noreply.github.com> Date: Mon, 13 May 2024 23:56:15 +0200 Subject: [PATCH 01/15] Added support for payment failure 124, 125, 126, 127, 128 and 134 Updated README Allow application to load request-response pairs when multiple *Requests and *Responses can be found in one directory --- README.md | 40 +++-- index.js | 2 +- playwright.config.js | 164 +++++++++--------- public/css/style.css | 4 +- public/js/animations/indicator-animations.js | 6 +- public/js/http/http-client.js | 8 +- .../code-blocks-interactions.js | 12 +- .../terminal-screen-interactions.js | 11 +- .../124_paymentNotEnoughBalanceRequest.json | 28 +++ .../124_paymentNotEnoughBalanceResponse.json | 63 +++++++ .../125_paymentCardBlockedRequest.json | 28 +++ .../125_paymentCardBlockedResponse.json | 63 +++++++ .../126_paymentCardExpiredRequest.json | 28 +++ .../126_paymentCardExpiredResponse.json | 63 +++++++ .../127_paymentInvalidAmountRequest.json | 28 +++ .../127_paymentInvalidAmountResponse.json | 63 +++++++ .../128_paymentInvalidCardRequest.json | 28 +++ .../128_paymentInvalidCardResponse.json | 63 +++++++ .../payment/134_paymentInvalidPinRequest.json | 28 +++ .../134_paymentInvalidPinResponse.json | 62 +++++++ public/payloads/payment/paymentResponse.json | 2 +- src/routes/defaultRoutes.js | 53 ++++-- src/services/payloadsService.js | 84 +++++---- src/services/userInteractionService.js | 40 ++--- src/views/index.hbs | 2 +- src/views/layouts/layout.hbs | 12 +- 26 files changed, 789 insertions(+), 196 deletions(-) create mode 100644 public/payloads/payment/124_paymentNotEnoughBalanceRequest.json create mode 100644 public/payloads/payment/124_paymentNotEnoughBalanceResponse.json create mode 100644 public/payloads/payment/125_paymentCardBlockedRequest.json create mode 100644 public/payloads/payment/125_paymentCardBlockedResponse.json create mode 100644 public/payloads/payment/126_paymentCardExpiredRequest.json create mode 100644 public/payloads/payment/126_paymentCardExpiredResponse.json create mode 100644 public/payloads/payment/127_paymentInvalidAmountRequest.json create mode 100644 public/payloads/payment/127_paymentInvalidAmountResponse.json create mode 100644 public/payloads/payment/128_paymentInvalidCardRequest.json create mode 100644 public/payloads/payment/128_paymentInvalidCardResponse.json create mode 100644 public/payloads/payment/134_paymentInvalidPinRequest.json create mode 100644 public/payloads/payment/134_paymentInvalidPinResponse.json diff --git a/README.md b/README.md index 559ef6a..71312cb 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,25 @@ > [!IMPORTANT] -> This mock application is currently in its alpha-release and is not supporting every request and response. This application is **not** able to reject all invalid requests. -> -> We currently support the following [Terminal API requests](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/): -> - [PaymentRequest](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/terminal-api-reference/#comadyennexopaymentrequest) | [PaymentResponse](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/terminal-api-reference/#comadyennexopaymentresponse) | [PaymentBusyResponse](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/terminal-api-reference/#comadyennexopaymentresponse) -> - [ReversalRequest](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/terminal-api-reference/#comadyennexoreversalrequest) | [ReversalResponse](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/terminal-api-reference/#comadyennexoreversalresponse) -> - [AbortRequest](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/terminal-api-reference/#comadyennexoabortrequest) -> - [TransactionStatusRequest](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/terminal-api-reference/#comadyennexotransactionstatusrequest) | [TransactionStatusResponse](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/terminal-api-reference/#comadyennexotransactionstatusresponse) +> This mock application is currently in its alpha-release and is not supporting every request and response. This application **cannot** reject all invalid requests. +> **Always test** your own request and responses on a physical terminal beforehand. The following mock payloads were constructed using a V400M-terminal device (card inserted & pin entered, no tap). + +> We currently support the following [Terminal API requests/responses](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/) below. +- [PaymentRequest](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/terminal-api-reference/#comadyennexopaymentrequest) & [PaymentResponse](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/terminal-api-reference/#comadyennexopaymentresponse) +- [PaymentBusyResponse](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/terminal-api-reference/#comadyennexopaymentresponse) +- [ReversalRequest](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/terminal-api-reference/#comadyennexoreversalrequest) & [ReversalResponse](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/terminal-api-reference/#comadyennexoreversalresponse) +- [AbortRequest](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/terminal-api-reference/#comadyennexoabortrequest) +- [TransactionStatusRequest](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/terminal-api-reference/#comadyennexotransactionstatusrequest) & [TransactionStatusResponse](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/terminal-api-reference/#comadyennexotransactionstatusresponse) + +> We currently support the following [Payment Refusal Codes](https://docs.adyen.com/point-of-sale/testing-pos-payments/test-card-v1/#testing-declines) below. + +| Amount ending in | Result | Error Condition | Refusal Reason | Message | +|-----------------:|--------------|-----------------|------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------| +| 124 | Failure | Refusal | 210 Not enough balance | NOT_ENOUGH_BALANCE | +| 125 | Failure | Refusal | 199 Card blocked | BLOCK_CARD | +| 126 | Failure | Refusal | 228 Card expired | CARD_EXPIRED | +| 127 | Failure | Refusal | 214 Declined online | INVALID_AMOUNT | +| 128 | Failure | InvalidCard | 214 Declined online | INVALID_CARD | +| 134 | Failure | WrongPIN | 129 Invalid online PIN | INVALID_PIN - **Remark:** The terminal shows "Incorrect PIN" and then "Enter PIN". Cancel the payment on the terminal to get the failure response. | + # Adyen Mock Terminal-API Application @@ -45,7 +59,7 @@ Visit [http://localhost:3000/](http://localhost:3000/) to see the mock Terminal There are two ways in which you can use the application. -1. We recommend to clone on of our our In-Person Payment Integration examples in [**.NET**](https://github.com/adyen-examples/adyen-dotnet-online-payments/tree/main/in-person-payments-example), [**Java**](https://github.com/adyen-examples/adyen-java-spring-online-payments/tree/main/in-person-payments-example) or [**Node.js**](https://github.com/adyen-examples/adyen-node-online-payments/tree/main/in-person-payments-example). +1. We recommend to clone one of our In-Person Payment Integration examples in [**.NET**](https://github.com/adyen-examples/adyen-dotnet-online-payments/tree/main/in-person-payments-example), [**Java**](https://github.com/adyen-examples/adyen-java-spring-online-payments/tree/main/in-person-payments-example) or [**Node.js**](https://github.com/adyen-examples/adyen-node-online-payments/tree/main/in-person-payments-example). Once you've cloned the example, you can point the application to use `http://localhost:3000`, this configurable by overriding the `CloudApiEndpoint` URI. Now your application is ready to communicate to the terminal @@ -72,15 +86,17 @@ We commit all our new features directly into our GitHub repository. Feel free to ### Example: Add your own mock request/response payload 1. Fork this repository and create a new branch. -2. The example below adds `paymentRequest.json` and `paymentResponse.json` (prefixed by `payment`). The `src/routes/services/payloadService` will automatically add this payload if the JSON is valid. +2. The example below adds `paymentRequest.json` and `paymentResponse.json`. The `src/routes/services/payloadService` will automatically load these files, if it's suffixed with `*Request`/`*Response` **and** if the JSON is valid. + - Create a new folder, in this example we use the existing **{payment}**-folder. - Add your `Request` to `/public/payloads/**{payment}**/paymentRequest - Add your `Response` to `/public/payloads/**{payment}**/paymentResponse - - Note: Every `-Request` should have a `-Response`. Except for those that require some kind of (state) logic (f.e. "paymentBusyResponse" triggers when a payment request is in-progress). -3. In `/src/routes/defaultRoutes.js`, find the `/sync`-endpoint and the following code snippet: + - **Note:** Every `*Request` should have a `*Response`, except for those that require some kind of state or logic (f.e: "paymentBusyResponse" triggers when a payment request is in-progress). + - **Note 2:** Keep naming-conventions camelCased and prefixed with its root-folder. Example: if the root-folder is located in `/payloads/example`, we name the jsons accordingly: `exampleRequest.json`/`exampleResponse.json`. +3. In `/src/routes/defaultRoutes.js`, find the `/sync`-endpoint and add the logic needed to trigger your added request-and-response. ```js if (req.body.SaleToPOIRequest.PaymentRequest) { - sendResponse(res, "payment"); + sendOKResponse(res, "payment"); return; } ``` diff --git a/index.js b/index.js index 0fe5020..946c2d4 100644 --- a/index.js +++ b/index.js @@ -18,7 +18,7 @@ app.engine("hbs", handlebars({ toUpperCase: function (string) { return string.charAt(0).toUpperCase() + string.slice(1); }, - isEqual: function(stringA, stringB) { + isEqual: function (stringA, stringB) { return stringA === stringB; } } diff --git a/playwright.config.js b/playwright.config.js index 794f154..3237bb1 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -8,92 +8,92 @@ const { defineConfig, devices } = require('@playwright/test'); // require('dotenv').config(); module.exports = defineConfig({ - testDir: './tests', - - /* Maximum time one test can run for. */ - timeout: 5 * 60 * 1000, - - /* Run tests in files in parallel */ - fullyParallel: false, - - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - - /* Retry on CI only */ - retries: process.env.CI ? 1 : 0, - - /* Tests on CI. */ - workers: process.env.CI ? 1 : undefined, - - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', - - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://localhost:3000', - baseURL: process.env.URL || 'http://localhost:3000', - - /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ - actionTimeout: 30 * 1000, - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, - - expect: { - /** - * Maximum time expect() should wait for the condition to be met. - * For example in `await expect(locator).toHaveText();` - * - * Note: waiting longer on CI - */ - timeout: process.env.CI ? 30 * 1000 : 20 * 1000 - }, - - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, + testDir: './tests', - /*{ - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, + /* Maximum time one test can run for. */ + timeout: 5 * 60 * 1000, - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - },*/ + /* Run tests in files in parallel */ + fullyParallel: false, - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + /* Retry on CI only */ + retries: process.env.CI ? 1 : 0, + + /* Tests on CI. */ + workers: process.env.CI ? 1 : undefined, + + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + baseURL: process.env.URL || 'http://localhost:3000', + + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 30 * 1000, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + * + * Note: waiting longer on CI + */ + timeout: process.env.CI ? 30 * 1000 : 20 * 1000 + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: {...devices['Desktop Chrome']}, + }, + + /*{ + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + },*/ + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, // }, - ], - - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:3000', - // reuseExistingServer: !process.env.CI, - // }, }); diff --git a/public/css/style.css b/public/css/style.css index a3266d8..c3ad12f 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -26,7 +26,7 @@ body { /* Sections start */ .trigger-request-section { display: flex; - width: 860px; + width: 1200px; padding: 0 32px; } @@ -38,7 +38,7 @@ body { .terminal-section { display: inline-grid; - margin: 0 96px; + margin: 0 128px; width: 360px; padding-bottom: 16px; } diff --git a/public/js/animations/indicator-animations.js b/public/js/animations/indicator-animations.js index 01c6ae0..a9ed8d7 100644 --- a/public/js/animations/indicator-animations.js +++ b/public/js/animations/indicator-animations.js @@ -1,11 +1,11 @@ /** - Helper functions that allow us to highlight elements within the page. -**/ + Helper functions that allow us to highlight elements within the page. + **/ async function addIndicator(elementId, delayInMilliseconds = 0) { await new Promise(resolve => setTimeout(resolve, delayInMilliseconds)); - + const element = document.getElementById(elementId); if (!element.classList.contains("indicator-highlight-start")) { element.classList.add("indicator-highlight-start"); diff --git a/public/js/http/http-client.js b/public/js/http/http-client.js index bba3ae6..4c953f6 100644 --- a/public/js/http/http-client.js +++ b/public/js/http/http-client.js @@ -1,6 +1,6 @@ /** - Collection of helper functions that allow you to send GET/POST requests or poll endpoints. -**/ + Collection of helper functions that allow you to send GET/POST requests or poll endpoints. + **/ async function sendGetRequest(url) { const res = await fetch(url, { @@ -22,7 +22,7 @@ async function sendPostRequest(url, data) { "Keep-Alive": "timeout=180, max=180" }, }); - + return res.json(); } @@ -31,7 +31,7 @@ async function pollEndpoint(endpoint, callback, milliseconds = 400) { async function poll() { try { - const response = await sendGetRequest(endpoint); + const response = await sendGetRequest(endpoint); callback(response); } catch (error) { console.error(error); diff --git a/public/js/user-interaction/code-blocks-interactions.js b/public/js/user-interaction/code-blocks-interactions.js index d67d56a..b9ed6a4 100644 --- a/public/js/user-interaction/code-blocks-interactions.js +++ b/public/js/user-interaction/code-blocks-interactions.js @@ -1,6 +1,6 @@ /** - Handles polling & highlighting of the request/response code blocks. -**/ + Handles polling & highlighting of the request/response code blocks. + **/ async function sendRequestButtonOnClick() { try { @@ -16,7 +16,7 @@ async function sendRequestButtonOnClick() { // Send the request to the sync endpoint. const response = await sendPostRequest("/sync", requestToSend); updateResponseCodeblock(response); - } catch(error) { + } catch (error) { console.error(error); } } @@ -26,7 +26,7 @@ async function clearCodeblockButtonOnClick() { const result = await sendPostRequest("/user-interaction/clear-codeblocks-button"); updateRequestCodeblock(result); updateResponseCodeblock(result); - } catch(error) { + } catch (error) { console.error(error); } } @@ -51,11 +51,11 @@ function updateResponseCodeblock(response) { if (responseElement.textContent === jsonResponse) { return; // Only update the JsonResponse element when there are changes. } - + if (responseElement.hasAttribute('data-highlighted')) { responseElement.removeAttribute('data-highlighted'); } - + responseElement.textContent = jsonResponse; hljs.highlightElement(document.getElementById('json-responses')); } diff --git a/public/js/user-interaction/terminal-screen-interactions.js b/public/js/user-interaction/terminal-screen-interactions.js index 6ddbee1..8ebbd15 100644 --- a/public/js/user-interaction/terminal-screen-interactions.js +++ b/public/js/user-interaction/terminal-screen-interactions.js @@ -1,6 +1,6 @@ /** - Handles anything that happens on the terminal, e.g. pin, buttons, screen text etc. -**/ + Handles user-interactions that happens on the terminal, e.g. pin, buttons, screen text etc. + **/ function getScreenPinElement() { @@ -32,6 +32,7 @@ function updateScreenText(text) { } let currentState = "READY"; + function updateState(response) { if (response.state === "BUSY") { // Pin. if (getScreenPinElement().classList.contains("hidden")) { @@ -65,7 +66,7 @@ function bindAllTerminalButtons() { if (response && Object.keys(response).length > 0) { updatePin(response.pin); } - } catch(e) { + } catch (e) { console.error(e); } }); @@ -77,12 +78,12 @@ function bindAllTerminalButtons() { if (!isAllowedToEnterPin()) { return; } - + if (getScreenPinElement().textContent.length < 4) { updateScreenText("Invalid pin, must be 4-digits, enter pin again:"); return; } - + await sendPostRequest("/user-interaction/confirm-button"); }); diff --git a/public/payloads/payment/124_paymentNotEnoughBalanceRequest.json b/public/payloads/payment/124_paymentNotEnoughBalanceRequest.json new file mode 100644 index 0000000..baddaa1 --- /dev/null +++ b/public/payloads/payment/124_paymentNotEnoughBalanceRequest.json @@ -0,0 +1,28 @@ +{ + "SaleToPOIRequest": { + "MessageHeader": { + "MessageCategory": "Payment", + "MessageClass": "Service", + "MessageType": "Request", + "POIID": "V400m-123456789", + "ProtocolVersion": "3.0", + "SaleID": "SALE_ID_42", + "ServiceID": "1234567890AB" + }, + "PaymentRequest": { + "SaleData": { + "SaleToAcquirerData": "tenderOption=ReceiptHandler", + "SaleTransactionID": { + "TransactionID": "21f1268f-9126-4bce-b127-9c2d5ffa024e", + "TimeStamp": "2023-06-12T12:08:36+00:00" + } + }, + "PaymentTransaction": { + "AmountsReq": { + "Currency": "EUR", + "RequestedAmount": 11.24 + } + } + } + } +} \ No newline at end of file diff --git a/public/payloads/payment/124_paymentNotEnoughBalanceResponse.json b/public/payloads/payment/124_paymentNotEnoughBalanceResponse.json new file mode 100644 index 0000000..fb2545e --- /dev/null +++ b/public/payloads/payment/124_paymentNotEnoughBalanceResponse.json @@ -0,0 +1,63 @@ +{ + "SaleToPOIResponse": { + "MessageHeader": { + "MessageCategory": "Payment", + "MessageClass": "Service", + "MessageType": "Response", + "POIID": "V400m-123456789", + "ProtocolVersion": "3.0", + "SaleID": "SALE_ID_42", + "ServiceID": "1234567890AB" + }, + "PaymentResponse": { + "POIData": { + "POIReconciliationID": "1000", + "POITransactionID": { + "TimeStamp": "2024-05-13T20:58:35.212Z", + "TransactionID": "v4W5001715633915004.TSXKSFKNS75ZGN82" + } + }, + "PaymentReceipt": [], + "PaymentResult": { + "AuthenticationMethod": [ + "OfflinePIN" + ], + "OnlineFlag": true, + "PaymentAcquirerData": { + "MerchantID": "ADYEN_MERCHANT_ACCOUNT", + "AcquirerPOIID": "V400m-123456789", + "AcquirerTransactionID": { + "TimeStamp": "2024-05-13T20:58:35.212Z", + "TransactionID": "TSXKSFKNS75ZGN82" + } + }, + "PaymentInstrumentData": { + "CardData": { + "CardCountryCode": "528", + "EntryMode": [ + "ICC" + ], + "MaskedPan": "600007 **** 7003", + "PaymentBrand": "maestro", + "SensitiveCardData": { + "CardSeqNumb": "23", + "ExpiryDate": "0330" + } + }, + "PaymentInstrumentType": "Card" + } + }, + "Response": { + "AdditionalResponse": "...", + "ErrorCondition": "Refusal", + "Result": "Failure" + }, + "SaleData": { + "SaleTransactionID": { + "TimeStamp": "2024-05-13T20:58:34.464Z", + "TransactionID": "21f1268f-9126-4bce-b127-9c2d5ffa024e" + } + } + } + } +} \ No newline at end of file diff --git a/public/payloads/payment/125_paymentCardBlockedRequest.json b/public/payloads/payment/125_paymentCardBlockedRequest.json new file mode 100644 index 0000000..004f71e --- /dev/null +++ b/public/payloads/payment/125_paymentCardBlockedRequest.json @@ -0,0 +1,28 @@ +{ + "SaleToPOIRequest": { + "MessageHeader": { + "MessageCategory": "Payment", + "MessageClass": "Service", + "MessageType": "Request", + "POIID": "V400m-123456789", + "ProtocolVersion": "3.0", + "SaleID": "SALE_ID_42", + "ServiceID": "1234567890AB" + }, + "PaymentRequest": { + "SaleData": { + "SaleToAcquirerData": "tenderOption=ReceiptHandler", + "SaleTransactionID": { + "TransactionID": "21f1268f-9126-4bce-b127-9c2d5ffa024e", + "TimeStamp": "2023-06-12T12:08:36+00:00" + } + }, + "PaymentTransaction": { + "AmountsReq": { + "Currency": "EUR", + "RequestedAmount": 11.25 + } + } + } + } +} \ No newline at end of file diff --git a/public/payloads/payment/125_paymentCardBlockedResponse.json b/public/payloads/payment/125_paymentCardBlockedResponse.json new file mode 100644 index 0000000..7635b0b --- /dev/null +++ b/public/payloads/payment/125_paymentCardBlockedResponse.json @@ -0,0 +1,63 @@ +{ + "SaleToPOIResponse": { + "MessageHeader": { + "MessageCategory": "Payment", + "MessageClass": "Service", + "MessageType": "Response", + "POIID": "V400m-123456789", + "ProtocolVersion": "3.0", + "SaleID": "SALE_ID_42", + "ServiceID": "1234567890AB" + }, + "PaymentResponse": { + "POIData": { + "POIReconciliationID": "1000", + "POITransactionID": { + "TimeStamp": "2024-05-13T21:08:45.752Z", + "TransactionID": "v4W5001715634525005.MGX7GX332G2LWPT5" + } + }, + "PaymentReceipt": [], + "PaymentResult": { + "AuthenticationMethod": [ + "OfflinePIN" + ], + "OnlineFlag": true, + "PaymentAcquirerData": { + "AcquirerPOIID": "S1F2-000158213602061", + "AcquirerTransactionID": { + "TimeStamp": "2024-05-13T21:08:45.752Z", + "TransactionID": "MGX7GX332G2LWPT5" + }, + "MerchantID": "ADYEN_MERCHANT_ACCOUNT" + }, + "PaymentInstrumentData": { + "CardData": { + "CardCountryCode": "528", + "EntryMode": [ + "ICC" + ], + "MaskedPan": "600007 **** 7003", + "PaymentBrand": "maestro", + "SensitiveCardData": { + "CardSeqNumb": "23", + "ExpiryDate": "0330" + } + }, + "PaymentInstrumentType": "Card" + } + }, + "Response": { + "AdditionalResponse": "...", + "ErrorCondition": "Refusal", + "Result": "Failure" + }, + "SaleData": { + "SaleTransactionID": { + "TimeStamp": "2024-05-13T21:08:44.650Z", + "TransactionID": "21f1268f-9126-4bce-b127-9c2d5ffa024e" + } + } + } + } +} \ No newline at end of file diff --git a/public/payloads/payment/126_paymentCardExpiredRequest.json b/public/payloads/payment/126_paymentCardExpiredRequest.json new file mode 100644 index 0000000..92bc978 --- /dev/null +++ b/public/payloads/payment/126_paymentCardExpiredRequest.json @@ -0,0 +1,28 @@ +{ + "SaleToPOIRequest": { + "MessageHeader": { + "MessageCategory": "Payment", + "MessageClass": "Service", + "MessageType": "Request", + "POIID": "V400m-123456789", + "ProtocolVersion": "3.0", + "SaleID": "SALE_ID_42", + "ServiceID": "1234567890AB" + }, + "PaymentRequest": { + "SaleData": { + "SaleToAcquirerData": "tenderOption=ReceiptHandler", + "SaleTransactionID": { + "TransactionID": "21f1268f-9126-4bce-b127-9c2d5ffa024e", + "TimeStamp": "2023-06-12T12:08:36+00:00" + } + }, + "PaymentTransaction": { + "AmountsReq": { + "Currency": "EUR", + "RequestedAmount": 11.26 + } + } + } + } +} \ No newline at end of file diff --git a/public/payloads/payment/126_paymentCardExpiredResponse.json b/public/payloads/payment/126_paymentCardExpiredResponse.json new file mode 100644 index 0000000..82eb74e --- /dev/null +++ b/public/payloads/payment/126_paymentCardExpiredResponse.json @@ -0,0 +1,63 @@ +{ + "SaleToPOIResponse": { + "MessageHeader": { + "MessageCategory": "Payment", + "MessageClass": "Service", + "MessageType": "Response", + "POIID": "V400m-123456789", + "ProtocolVersion": "3.0", + "SaleID": "SALE_ID_42", + "ServiceID": "1234567890AB" + }, + "PaymentResponse": { + "POIData": { + "POIReconciliationID": "1000", + "POITransactionID": { + "TimeStamp": "2024-05-13T21:18:34.552Z", + "TransactionID": "v4W5001715635114006.JTM7N2Q83TF92P65" + } + }, + "PaymentReceipt": [], + "PaymentResult": { + "AuthenticationMethod": [ + "OfflinePIN" + ], + "OnlineFlag": true, + "PaymentAcquirerData": { + "AcquirerPOIID": "V400m-123456789", + "AcquirerTransactionID": { + "TimeStamp": "2024-05-13T21:18:34.552Z", + "TransactionID": "JTM7N2Q83TF92P65" + }, + "MerchantID": "ADYEN_MERCHANT_ACCOUNT" + }, + "PaymentInstrumentData": { + "CardData": { + "CardCountryCode": "528", + "EntryMode": [ + "ICC" + ], + "MaskedPan": "600007 **** 7003", + "PaymentBrand": "maestro", + "SensitiveCardData": { + "CardSeqNumb": "23", + "ExpiryDate": "0330" + } + }, + "PaymentInstrumentType": "Card" + } + }, + "Response": { + "AdditionalResponse": "...", + "ErrorCondition": "Refusal", + "Result": "Failure" + }, + "SaleData": { + "SaleTransactionID": { + "TimeStamp": "2024-05-13T21:18:32.998Z", + "TransactionID": "21f1268f-9126-4bce-b127-9c2d5ffa024e" + } + } + } + } +} \ No newline at end of file diff --git a/public/payloads/payment/127_paymentInvalidAmountRequest.json b/public/payloads/payment/127_paymentInvalidAmountRequest.json new file mode 100644 index 0000000..238ec78 --- /dev/null +++ b/public/payloads/payment/127_paymentInvalidAmountRequest.json @@ -0,0 +1,28 @@ +{ + "SaleToPOIRequest": { + "MessageHeader": { + "MessageCategory": "Payment", + "MessageClass": "Service", + "MessageType": "Request", + "POIID": "V400m-123456789", + "ProtocolVersion": "3.0", + "SaleID": "SALE_ID_42", + "ServiceID": "1234567890AB" + }, + "PaymentRequest": { + "SaleData": { + "SaleToAcquirerData": "tenderOption=ReceiptHandler", + "SaleTransactionID": { + "TransactionID": "21f1268f-9126-4bce-b127-9c2d5ffa024e", + "TimeStamp": "2023-06-12T12:08:36+00:00" + } + }, + "PaymentTransaction": { + "AmountsReq": { + "Currency": "EUR", + "RequestedAmount": 11.27 + } + } + } + } +} \ No newline at end of file diff --git a/public/payloads/payment/127_paymentInvalidAmountResponse.json b/public/payloads/payment/127_paymentInvalidAmountResponse.json new file mode 100644 index 0000000..3905ff7 --- /dev/null +++ b/public/payloads/payment/127_paymentInvalidAmountResponse.json @@ -0,0 +1,63 @@ +{ + "SaleToPOIResponse": { + "MessageHeader": { + "MessageCategory": "Payment", + "MessageClass": "Service", + "MessageType": "Response", + "POIID": "V400m-123456789", + "ProtocolVersion": "3.0", + "SaleID": "SALE_ID_42", + "ServiceID": "1234567890AB" + }, + "PaymentResponse": { + "POIData": { + "POIReconciliationID": "1000", + "POITransactionID": { + "TimeStamp": "2024-05-13T21:22:22.652Z", + "TransactionID": "v4W5001715635342007.ZF6647432G2LWPT5" + } + }, + "PaymentReceipt": [], + "PaymentResult": { + "AuthenticationMethod": [ + "OfflinePIN" + ], + "OnlineFlag": true, + "PaymentAcquirerData": { + "AcquirerPOIID": "V400m-123456789", + "AcquirerTransactionID": { + "TimeStamp": "2024-05-13T21:22:22.652Z", + "TransactionID": "ZF6647432G2LWPT5" + }, + "MerchantID": "ADYEN_MERCHANT_ACCOUNT" + }, + "PaymentInstrumentData": { + "CardData": { + "CardCountryCode": "528", + "EntryMode": [ + "ICC" + ], + "MaskedPan": "600007 **** 7003", + "PaymentBrand": "maestro", + "SensitiveCardData": { + "CardSeqNumb": "23", + "ExpiryDate": "0330" + } + }, + "PaymentInstrumentType": "Card" + } + }, + "Response": { + "AdditionalResponse": "...", + "ErrorCondition": "Refusal", + "Result": "Failure" + }, + "SaleData": { + "SaleTransactionID": { + "TimeStamp": "2024-05-13T21:22:22.235Z", + "TransactionID": "21f1268f-9126-4bce-b127-9c2d5ffa024e" + } + } + } + } +} \ No newline at end of file diff --git a/public/payloads/payment/128_paymentInvalidCardRequest.json b/public/payloads/payment/128_paymentInvalidCardRequest.json new file mode 100644 index 0000000..4de8b46 --- /dev/null +++ b/public/payloads/payment/128_paymentInvalidCardRequest.json @@ -0,0 +1,28 @@ +{ + "SaleToPOIRequest": { + "MessageHeader": { + "MessageCategory": "Payment", + "MessageClass": "Service", + "MessageType": "Request", + "POIID": "V400m-123456789", + "ProtocolVersion": "3.0", + "SaleID": "SALE_ID_42", + "ServiceID": "1234567890AB" + }, + "PaymentRequest": { + "SaleData": { + "SaleToAcquirerData": "tenderOption=ReceiptHandler", + "SaleTransactionID": { + "TransactionID": "21f1268f-9126-4bce-b127-9c2d5ffa024e", + "TimeStamp": "2023-06-12T12:08:36+00:00" + } + }, + "PaymentTransaction": { + "AmountsReq": { + "Currency": "EUR", + "RequestedAmount": 11.28 + } + } + } + } +} \ No newline at end of file diff --git a/public/payloads/payment/128_paymentInvalidCardResponse.json b/public/payloads/payment/128_paymentInvalidCardResponse.json new file mode 100644 index 0000000..a0616c1 --- /dev/null +++ b/public/payloads/payment/128_paymentInvalidCardResponse.json @@ -0,0 +1,63 @@ +{ + "SaleToPOIResponse": { + "MessageHeader": { + "MessageCategory": "Payment", + "MessageClass": "Service", + "MessageType": "Response", + "POIID": "V400m-123456789", + "ProtocolVersion": "3.0", + "SaleID": "SALE_ID_42", + "ServiceID": "1234567890AB" + }, + "PaymentResponse": { + "POIData": { + "POIReconciliationID": "1000", + "POITransactionID": { + "TimeStamp": "2024-05-13T21:28:13.130Z", + "TransactionID": "v4W5001715635693008.LK38D7QTDNBZWST5" + } + }, + "PaymentReceipt": [], + "PaymentResult": { + "AuthenticationMethod": [ + "OfflinePIN" + ], + "OnlineFlag": true, + "PaymentAcquirerData": { + "AcquirerPOIID": "V400m-123456789", + "AcquirerTransactionID": { + "TimeStamp": "2024-05-13T21:28:13.130Z", + "TransactionID": "LK38D7QTDNBZWST5" + }, + "MerchantID": "ADYEN_MERCHANT_ACCOUNT" + }, + "PaymentInstrumentData": { + "CardData": { + "CardCountryCode": "528", + "EntryMode": [ + "ICC" + ], + "MaskedPan": "600007 **** 7003", + "PaymentBrand": "maestro", + "SensitiveCardData": { + "CardSeqNumb": "23", + "ExpiryDate": "0330" + } + }, + "PaymentInstrumentType": "Card" + } + }, + "Response": { + "AdditionalResponse": "...", + "ErrorCondition": "InvalidCard", + "Result": "Failure" + }, + "SaleData": { + "SaleTransactionID": { + "TimeStamp": "2024-05-13T21:28:10.153Z", + "TransactionID": "21f1268f-9126-4bce-b127-9c2d5ffa024e" + } + } + } + } +} \ No newline at end of file diff --git a/public/payloads/payment/134_paymentInvalidPinRequest.json b/public/payloads/payment/134_paymentInvalidPinRequest.json new file mode 100644 index 0000000..365e22d --- /dev/null +++ b/public/payloads/payment/134_paymentInvalidPinRequest.json @@ -0,0 +1,28 @@ +{ + "SaleToPOIRequest": { + "MessageHeader": { + "MessageCategory": "Payment", + "MessageClass": "Service", + "MessageType": "Request", + "POIID": "V400m-123456789", + "ProtocolVersion": "3.0", + "SaleID": "SALE_ID_42", + "ServiceID": "1234567890AB" + }, + "PaymentRequest": { + "SaleData": { + "SaleToAcquirerData": "tenderOption=ReceiptHandler", + "SaleTransactionID": { + "TransactionID": "21f1268f-9126-4bce-b127-9c2d5ffa024e", + "TimeStamp": "2023-06-12T12:08:36+00:00" + } + }, + "PaymentTransaction": { + "AmountsReq": { + "Currency": "EUR", + "RequestedAmount": 11.34 + } + } + } + } +} \ No newline at end of file diff --git a/public/payloads/payment/134_paymentInvalidPinResponse.json b/public/payloads/payment/134_paymentInvalidPinResponse.json new file mode 100644 index 0000000..d1ae21c --- /dev/null +++ b/public/payloads/payment/134_paymentInvalidPinResponse.json @@ -0,0 +1,62 @@ +{ + "SaleToPOIResponse": { + "MessageHeader": { + "MessageCategory": "Payment", + "MessageClass": "Service", + "MessageType": "Response", + "POIID": "V400m-123456789", + "ProtocolVersion": "3.0", + "SaleID": "SALE_ID_42", + "ServiceID": "1234567890AB" + }, + "PaymentResponse": { + "POIData": { + "POIReconciliationID": "1000", + "POITransactionID": { + "TimeStamp": "2024-05-13T21:30:08.296Z", + "TransactionID": "v4W5001715635808009.DZBJ7ZKNS75ZGN82" + } + }, + "PaymentResult": { + "AuthenticationMethod": [ + "OfflinePIN" + ], + "OnlineFlag": true, + "PaymentAcquirerData": { + "AcquirerPOIID": "V400m-123456789", + "AcquirerTransactionID": { + "TimeStamp": "2024-05-13T21:30:08.296Z", + "TransactionID": "DZBJ7ZKNS75ZGN82" + }, + "MerchantID": "ADYEN_MERCHANT_ACCOUNT" + }, + "PaymentInstrumentData": { + "CardData": { + "CardCountryCode": "528", + "EntryMode": [ + "ICC" + ], + "MaskedPan": "600007 **** 7003", + "PaymentBrand": "maestro", + "SensitiveCardData": { + "CardSeqNumb": "23", + "ExpiryDate": "0330" + } + }, + "PaymentInstrumentType": "Card" + } + }, + "Response": { + "AdditionalResponse": "...", + "ErrorCondition": "WrongPIN", + "Result": "Failure" + }, + "SaleData": { + "SaleTransactionID": { + "TimeStamp": "2024-05-13T21:30:07.317Z", + "TransactionID": "21f1268f-9126-4bce-b127-9c2d5ffa024e" + } + } + } + } +} \ No newline at end of file diff --git a/public/payloads/payment/paymentResponse.json b/public/payloads/payment/paymentResponse.json index fb59790..2b685e3 100644 --- a/public/payloads/payment/paymentResponse.json +++ b/public/payloads/payment/paymentResponse.json @@ -23,7 +23,7 @@ "Currency": "EUR" }, "PaymentAcquirerData": { - "MerchantID": "YOUR_MERCHANT_ACCOUNT", + "MerchantID": "ADYEN_MERCHANT_ACCOUNT", "AcquirerPOIID": "V400m-123456789" }, "PaymentInstrumentData": { diff --git a/src/routes/defaultRoutes.js b/src/routes/defaultRoutes.js index 02141a5..37278fa 100644 --- a/src/routes/defaultRoutes.js +++ b/src/routes/defaultRoutes.js @@ -3,11 +3,20 @@ const { userInteractionService, STATES } = require('../services/userInteractionS const express = require('express'); const router = express.Router(); +const paymentFailureMap = { + "124": "124_paymentNotEnoughBalance", + "125": "125_paymentCardBlocked", + "126": "126_paymentCardExpired", + "127": "127_paymentInvalidAmount", + "128": "128_paymentInvalidCard", + "134": "134_paymentInvalidPin" +}; + // Root index page. router.get("/", async (req, res) => { const data = payloadService.getPayloads(); res.render("index", { - keys : Object.keys(data).filter(key => data[key][0] !== null) // Filter out empty requests, e.g. paymentBusy response + keys: Object.keys(data).filter(key => data[key][0] !== null) // Filter out empty requests, e.g. paymentBusy response }); }); @@ -22,7 +31,7 @@ router.post("/sync", async (req, res) => { console.info("Incoming /sync request ..."); userInteractionService.setLastRequest(req.body); userInteractionService.setLastResponse({}); - + try { // Handle abort request. if (req.body.SaleToPOIRequest.AbortRequest) { @@ -30,13 +39,13 @@ router.post("/sync", async (req, res) => { sendOKResponse(res, "abortPayment"); return; } - + // Handle paymentBusy response. if (userInteractionService.getState() === STATES.BUSY) { sendOKResponse(res, "paymentBusy"); return; } - + // Handle payment request. if (req.body.SaleToPOIRequest.PaymentRequest) { // Set terminal to busy and start handling logic. @@ -47,31 +56,45 @@ router.post("/sync", async (req, res) => { userInteractionService.setState(STATES.READY); if (isPinEntered) { + const amount = req.body.SaleToPOIRequest.PaymentRequest.PaymentTransaction.AmountsReq.RequestedAmount; + const lastThreeDigits = amount.toString() + .replace('.', '') + .padStart(3, '0') + .slice(-3); + + // Look up error code from payment failure. + const paymentFailure = paymentFailureMap[lastThreeDigits]; + + if (paymentFailure) { + sendOKResponse(res, paymentFailure); + return; + } + sendOKResponse(res, "payment"); return; } - + res.status(200).send({}); return; } - + // Handle reversal request. if (req.body.SaleToPOIRequest.ReversalRequest) { sendOKResponse(res, "reversal"); return; } - + // Handle paymentTransactionStatus request. if (req.body.SaleToPOIRequest.TransactionStatusRequest) { sendOKResponse(res, "paymentTransactionStatus"); return; } - + // Handle requests not found. userInteractionService.setState(STATES.READY); console.error("Request not found: " + req.body); res.status(404).send({}); - } catch(e) { + } catch (e) { userInteractionService.setState(STATES.READY); console.error(e); } @@ -88,9 +111,9 @@ function sendOKResponse(res, prefix) { /** -* Blocking call that waits until user has entered the pin. Returns a boolean if successful. -* @returns True, when pin is entered - False, when request is cancelled/time-out. -*/ + * Blocking call that waits until user has entered the pin. Returns a boolean if successful. + * @returns True, when pin is entered - False, when request is cancelled/time-out. + */ const getPinResultAsync = async () => { return new Promise(async resolve => { let totalSeconds = 180; @@ -99,16 +122,16 @@ const getPinResultAsync = async () => { // Pin code entered, accept any pin for now. console.info("Confirm-button is pressed."); resolve(true); - } else if (userInteractionService.getState() === STATES.READY) { + } else if (userInteractionService.getState() === STATES.READY) { // State changed, f.e. red `cancel-button` is pressed. console.info("Cancelled entering-pin by user."); resolve(false); - } else if (totalSeconds > 0) { + } else if (totalSeconds > 0) { // Waiting `totalSeconds` for user to enter pin... console.info("Waiting for pin input... " + totalSeconds); totalSeconds--; setTimeout(waitForPinResult, 1000); - } else { + } else { // Time out. console.info("Request has timed-out."); resolve(false); diff --git a/src/services/payloadsService.js b/src/services/payloadsService.js index df77c48..fc0551c 100644 --- a/src/services/payloadsService.js +++ b/src/services/payloadsService.js @@ -6,7 +6,7 @@ class PayloadsService { if (!PayloadsService.instance) { this.data = getRequestsAndResponses(path.join(__dirname, '../../public/payloads')); } - + return PayloadsService.instance; } @@ -14,16 +14,16 @@ class PayloadsService { * Gets the JsonObjectRequest and JsonObjectResponse. * Example entry: { "key", [JsonObjectRequest, JsonObjectResponse] } * Example entry: { "payment", [PaymentRequest, PaymentResponse] } - * @returns {Map} + * @returns {Map} */ getPayloads() { return this.data; } - + /** * Gets the request pair by prefix (key). - * @param {string} prefix - The key which stores the JsonObjectRequest and JsonObjectResponse. - * @returns {string} - JsonObject. + * @param {String} prefix - The key which stores the JsonObjectRequest and JsonObjectResponse. + * @returns {String} - JsonObject. */ getRequestByPrefix(prefix) { return this.data[prefix][0]; @@ -31,8 +31,8 @@ class PayloadsService { /** * Gets the response by prefix (key). - * @param {string} prefix - The key which stores the JsonObjectRequest and JsonObjectResponse. - * @returns {string} - JsonObject. + * @param {String} prefix - The key which stores the JsonObjectRequest and JsonObjectResponse. + * @returns {String} - JsonObject. */ getResponseByPrefix(prefix) { return this.data[prefix][1]; @@ -40,8 +40,8 @@ class PayloadsService { /** * Gets the request and response pair by prefix (key). - * @param {string} prefix - The key which stores the JsonObjectRequest and JsonObjectResponse. - * @returns {string[]} - [JsonObject, JsonObject] + * @param {String} prefix - The key which stores the JsonObjectRequest and JsonObjectResponse. + * @returns {String[]} - [JsonObject, JsonObject] */ getValueByPrefix(prefix) { return this.data[prefix]; @@ -50,41 +50,49 @@ class PayloadsService { /** * Use reflection (e.g. '/public/payloads'-folder) to load and parse the '-Request.json' and '-Response.json' objects from each directory. - * @param {string} rootPath - The path of the directory to extract the JSONs from. - * @returns {Map} + * @param {String} rootPath - The path of the directory to extract the JSONs from. + * @returns {Map} */ function getRequestsAndResponses(rootPath) { - const map = {}; + let map = {}; const root = fs.readdirSync(rootPath); + const encoding = 'utf-8'; root.forEach(file => { - const filePath = path.join(rootPath, file); - const stat = fs.statSync(filePath); - - if (stat.isDirectory()) { - const files = fs.readdirSync(filePath); - - // Find `Request.json` and `Response.json` files. - const requestFile = files.find(f => f.endsWith('Request.json')); - const responseFile = files.find(f => f.endsWith('Response.json')); - - // Parse (optional) `Request.json`. - let requestJson = null; - if (requestFile) { - const requestFilePath = path.join(filePath, requestFile); - requestJson = JSON.parse(fs.readFileSync(requestFilePath, 'utf8')); - } - - // Parse (mandatory) `Response.json`. - const responseFilePath = path.join(filePath, responseFile); - const responseJson = JSON.parse(fs.readFileSync(responseFilePath, 'utf8')); - - // Default - Insert into our map: { "payment", [PaymentRequestJson, PaymentResponseJson] } - // If no `Request.json` is found (f.e. for `paymentBusy`), insert only the response into our map: { "paymentBusy", [null, PaymentBusyResponseJson] } - map[file] = [requestJson, responseJson]; - } + const filePath = path.join(rootPath, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + const files = fs.readdirSync(filePath); + const responseFiles = files.filter(file => file.endsWith('Response.json')); + + // Retrieve the `*Request.json` and `*Response.json` files and return them as an array of objects. + const pairs = responseFiles.map(responseFile => { + // Prefix, e.g. filename "paymentResponse.json" returns "payment". + const prefix = path.basename(responseFile, 'Response.json'); + + // Find the respective `*Request,json` pair for `*Response.json` if any. + const requestFile = files.find(file => file === prefix + 'Request.json'); + + return ( + { + prefix: prefix, + requestFileName: requestFile ? requestFile : null, + requestJson: requestFile ? JSON.parse(fs.readFileSync(path.join(filePath, requestFile), encoding)) : null, + responseFileName: responseFile, + responseJson: JSON.parse(fs.readFileSync(path.join(filePath, responseFile), encoding)) + } + ); + }); + + // Default - Insert into our map: { "payment", [PaymentRequestJson, PaymentResponseJson] } + // If no `Request.json` is found (f.e. for `paymentBusy`), insert only the response into our map: { "paymentBusy", [null, PaymentBusyResponseJson] } + pairs.forEach(pair => { + map[pair.prefix] = [pair.requestJson, pair.responseJson] + }); + } }); - + console.info("Found " + Object.keys(map).length + " mock payloads.") return map; diff --git a/src/services/userInteractionService.js b/src/services/userInteractionService.js index 9fde622..b93d595 100644 --- a/src/services/userInteractionService.js +++ b/src/services/userInteractionService.js @@ -12,7 +12,7 @@ class UserInteractionService { this.pin = ""; this.isConfirmed = false; } - + return UserInteractionService.instance; } @@ -20,20 +20,20 @@ class UserInteractionService { * Get the state of the terminal. * @returns {STATES} - String. */ - getState() { + getState() { return this.state; } /** * Set the state of the terminal. - * @param {STATES state} - State of the terminal. + * @param {STATES} state - State of the terminal. */ setState(state) { if (this.state === state) { console.info("State remains unchanged."); return; } - + this.clearPin(); this.setIsConfirmed(false); this.state = state; @@ -50,13 +50,13 @@ class UserInteractionService { /** * Set the last response returned by the terminal. - * @param {JsonObject lastResponse} JsonObject + * @param {JsonObject} lastResponse JsonObject */ - setLastResponse(response) { - this.lastResponse = response; + setLastResponse(lastResponse) { + this.lastResponse = lastResponse; console.info("Last response is set to: " + JSON.stringify(this.lastResponse, null, 2)); } - + /** * Get the last request returned by the terminal. * @returns {JsonObject} - JsonObject. @@ -66,11 +66,11 @@ class UserInteractionService { } /** - * Sets the last response returned by the terminal. - * @param {JsonObject request} - JsonObject + * Sets the last request returned by the terminal. + * @param {JsonObject} lastRequest - JsonObject */ - setLastRequest(request) { - this.lastRequest = request; + setLastRequest(lastRequest) { + this.lastRequest = lastRequest; console.info("Last request is set to: " + JSON.stringify(this.lastRequest, null, 2)); } @@ -84,30 +84,30 @@ class UserInteractionService { /** * Gets the pin-code. - * @returns {string pin} - Pin-code. + * @returns {String} - Pin-code. */ getPin() { return this.pin; } - + /** * Sets the pin-code. - * @param {string pin} - Pin-code. + * @param {String} pin - Pin-code. */ setPin(pin) { this.pin = pin; } - + /** * Removes the pin-code. */ clearPin() { this.pin = ""; } - + /** * Returns true when the green confirm-button is pressed on the terminal. - * @returns {boolean} - True when the confirm-button has been pressed. + * @returns {Boolean} - True when the confirm-button has been pressed. */ getIsConfirmed() { return this.isConfirmed; @@ -115,7 +115,7 @@ class UserInteractionService { /** * Set to true when to green confirm-button is pressed on the terminal. - * @param {boolean isConfirmed} - True when the confirm-button has been pressed. + * @param {Boolean} isConfirmed - Set to true when the confirm-button has been pressed. */ setIsConfirmed(isConfirmed) { this.isConfirmed = isConfirmed; @@ -123,4 +123,4 @@ class UserInteractionService { } const userInteractionService = new UserInteractionService(); -module.exports = { userInteractionService, STATES: userInteractionService.STATES }; \ No newline at end of file +module.exports = {userInteractionService, STATES: userInteractionService.STATES}; \ No newline at end of file diff --git a/src/views/index.hbs b/src/views/index.hbs index beead80..18743f6 100644 --- a/src/views/index.hbs +++ b/src/views/index.hbs @@ -1,5 +1,5 @@
-
Adyen Mock Terminal-API
+
Adyen Mock Terminal-API Application