From 77f9cbab55efe4ca40761666983d6f3244b35c87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahmet=20=C3=96z?= Date: Tue, 10 Dec 2019 17:40:32 +0100 Subject: [PATCH] #64 move 'charge' transactions to 'authorization' transactions according to the adyen best practices. (#85) * add details for checkout steps. * update Charge to Authorization. * Update extension/docs/IntegrationGuide.md Co-Authored-By: andreas Halberkamp * Update extension/docs/IntegrationGuide.md Co-Authored-By: andreas Halberkamp * Link integration guide with Refund/Cancel guide #71 * additional unit test for the manual capture cases * add additional integration test for manual capture notification * Split unit and integration test commands (#82) * bump version to 3.0.0 * Update extension/docs/IntegrationGuide.md Co-Authored-By: Roman Butenko * Update extension/docs/IntegrationGuide.md Co-Authored-By: Roman Butenko * Update extension/docs/IntegrationGuide.md Co-Authored-By: Roman Butenko * Update extension/docs/IntegrationGuide.md Co-Authored-By: Roman Butenko * Update notification/test/unit/notification.handler.spec.js Co-Authored-By: Hasan * add quotes for the adyen events. * add nyc to integration tests. * revert changes for test command. * add explanation for why originalReference should use the AUTHORIZATION notification event. * make cancel or refund documentation more clear. * handle the cancel or refund notification correctly on notification module. --- extension/docs/CancelRefundPayment.md | 10 +- extension/docs/CreditCardIntegration.md | 12 +- extension/docs/IntegrationGuide.md | 11 +- extension/docs/KcpIntegration.md | 4 +- extension/docs/PaypalIntegration.md | 8 +- extension/package-lock.json | 786 +++++++++++++++++- extension/package.json | 7 +- .../cancel-or-refund.handler.js | 4 +- .../credit-card-complete-payment.handler.js | 2 +- .../credit-card-make-payment.handler.js | 4 +- .../creditCard/credit-card.handler.js | 4 +- .../fetch-payment-methods.handler.js | 2 +- .../kcp/kcp-make-payment.handler.js | 4 +- .../paymentHandler/kcp/kcp-payment.handler.js | 2 +- extension/src/paymentHandler/payment-utils.js | 24 +- .../paypal/paypal-complete-payment.handler.js | 2 +- .../paypal/paypal-make-payment.handler.js | 4 +- .../paymentHandler/paypal/paypal.handler.js | 4 +- extension/src/validator/error-messages.js | 6 +- extension/src/validator/validator-builder.js | 22 +- extension/test/fixtures/ctp-payment.json | 2 +- .../test/fixtures/payment-credit-card-3d.json | 4 +- .../test/fixtures/payment-credit-card.json | 4 +- extension/test/fixtures/payment-kcp.json | 4 +- extension/test/fixtures/payment-paypal.json | 4 +- .../cancel-or-refund.handler.spec.js | 8 +- .../credit-card-make-payment.handler.spec.js | 4 +- notification/package-lock.json | 2 +- notification/package.json | 6 +- notification/resources/adyen-events.json | 4 +- .../notification/notification.handler.js | 30 +- .../integration/notification.handler.spec.js | 188 ++++- .../test/resources/payment-credit-card.json | 16 +- .../test/resources/payment-draft.json | 2 +- .../test/unit/notification.handler.spec.js | 401 +++++++-- 35 files changed, 1429 insertions(+), 172 deletions(-) diff --git a/extension/docs/CancelRefundPayment.md b/extension/docs/CancelRefundPayment.md index 665943797..041429dcd 100644 --- a/extension/docs/CancelRefundPayment.md +++ b/extension/docs/CancelRefundPayment.md @@ -41,10 +41,12 @@ ] } ``` - Both transaction types work the same, because Adyen will always pick the right action for the current transaction. - If the transaction is not authorized yet, Adyen will try to cancel. - If the transaction is already charged, Adyen will do refund. -1. In `Payment.transactions`, extension module finds the first transaction with `type='Charge' and state='Success'`. + +- Both transaction types work the same, because Adyen will always pick the right action for the current transaction. + - This will either: + - Cancel the payment – in case it has not yet been captured. + - Refund the payment – in case it has already been captured. +1. In `Payment.transactions`, extension module finds the first transaction with `type='Authorization' and state='Success'`. Extension module takes `amount` and `transactionId` from this transaction and makes a 'Cancel or refund' request. 1. Extension module saves following information to the payment object: * `Payment.interfaceInteractions` with `type='cancelOrRefund'` that contains request and response with Adyen diff --git a/extension/docs/CreditCardIntegration.md b/extension/docs/CreditCardIntegration.md index faf309037..0beac2484 100644 --- a/extension/docs/CreditCardIntegration.md +++ b/extension/docs/CreditCardIntegration.md @@ -21,7 +21,7 @@ The following features are not supported: ### Credit card 1. Shop collects shopper details according to the [Adyen documentation](https://docs.adyen.com/developers/payment-methods/cards-with-3d-secure#step1collectshopperdetails) and creates a payment with following criteria ([Example payment](../test/fixtures/payment-credit-card.json)): * `Payment.paymentMethodInfo.method = 'creditCard'` - * `Payment.transactions` contains a transaction with `type='Charge' and state='Initial'` + * `Payment.transactions` contains a transaction with `type='Authorization' and state='Initial'` * `Payment.custom.fields.encryptedCardNumber` contains credit card number encrypted in the previous step. * `Payment.custom.fields.encryptedExpiryMonth` contains expiry month encrypted in the previous step. * `Payment.custom.fields.encryptedExpiryYear` contains expiry year encrypted in the previous step. @@ -30,7 +30,7 @@ The following features are not supported: * *Optional*: `paymentObject.custom.fields.holderName` 1. Extension module makes a payment request and save following information to the payment object: * `Payment.interfaceInteractions` with `type='makePayment'` that contains request and response with Adyen - * `Payment.transactions` with a transaction `type='Charge' and state='Initial'` will be changed to a new state according to [the returned result code](./IntegrationGuide.md#mapping-from-adyen-result-codes-to-ctp-transaction-state). + * `Payment.transactions` with a transaction `type='Authorization' and state='Initial'` will be changed to a new state according to [the returned result code](./IntegrationGuide.md#mapping-from-adyen-result-codes-to-ctp-transaction-state). * `pspReference` will be saved in a matching transaction from the previous point in a field `Payment.transactions.interactionId` 1. Shop validates the payment and presents the payment result to the shopper. @@ -39,7 +39,7 @@ The following features are not supported: ### Credit card with 3D Secure 1. Shop collects shopper details according to the [Adyen documentation](https://docs.adyen.com/developers/payment-methods/cards-with-3d-secure#step1collectshopperdetails) and creates a payment with following criteria ([Example payment](../test/fixtures/payment-credit-card-3d.json)): * `Payment.paymentMethodInfo.method = 'creditCard_3d'` - * `Payment.transactions` contains a transaction with `type='Charge' and state='Initial'` + * `Payment.transactions` contains a transaction with `type='Authorization' and state='Initial'` * `Payment.custom.fields.encryptedCardNumber` contains credit card number encrypted in the previous step. * `Payment.custom.fields.encryptedExpiryMonth` contains expiry month encrypted in the previous step. * `Payment.custom.fields.encryptedExpiryYear` contains expiry year encrypted in the previous step. @@ -49,7 +49,7 @@ The following features are not supported: * *Optional*: `paymentObject.custom.fields.holderName` 1. Extension module makes a payment request and save following information to the payment object (for explanation of each field, see [Adyen's documentations](https://docs.adyen.com/developers/payment-methods/cards-with-3d-secure#step2makeapayment)): * `Payment.interfaceInteractions` with `type='makePayment'` that contains request and response with Adyen - * `Payment.transactions` with a transaction `type='Charge' and state='Initial'` will be changed to `state='Pending'`. + * `Payment.transactions` with a transaction `type='Authorization' and state='Initial'` will be changed to `state='Pending'`. * `Payment.custom.fields.MD` * `Payment.custom.fields.PaReq` * `Payment.custom.fields.paymentData` @@ -69,7 +69,7 @@ The following features are not supported: ``` 1. Extension module makes a [payment request](https://docs.adyen.com/developers/payment-methods/cards-with-3d-secure#step4completepayment) and save following information to the payment object: * `Payment.interfaceInteractions` with `type='completePayment'` that contains request and response with Adyen - * `Payment.transactions` with a transaction `type='Charge' and state='Pending'` will be changed to a new state according to [the returned result code](IntegrationGuide.md#mapping-from-adyen-result-codes-to-ctp-transaction-state). + * `Payment.transactions` with a transaction `type='Authorization' and state='Pending'` will be changed to a new state according to [the returned result code](IntegrationGuide.md#mapping-from-adyen-result-codes-to-ctp-transaction-state). * `pspReference` will be saved in a matching transaction from the previous point in a field `Payment.transactions.interactionId` 1. Shop validates the payment and presents the payment result to the shopper. @@ -78,4 +78,4 @@ The following features are not supported: ![3D Secure flow](https://user-images.githubusercontent.com/803826/56141166-93c1cf80-5f9c-11e9-88f4-95a694ad4227.png) ### Other -Please consult [Adyen documentation](https://docs.adyen.com/developers/payment-methods/cards) for additional information. \ No newline at end of file +Please consult [Adyen documentation](https://docs.adyen.com/developers/payment-methods/cards) for additional information. diff --git a/extension/docs/IntegrationGuide.md b/extension/docs/IntegrationGuide.md index dba48af9c..6197fa2fc 100644 --- a/extension/docs/IntegrationGuide.md +++ b/extension/docs/IntegrationGuide.md @@ -27,8 +27,9 @@ In this process, there are 3 parties involved: - **Shopper** - a person that's using the shop Other used terms in the documentation: -- **Cancel** - cancel the authorisation on an uncaptured payment. -- **Refund** - refund a payment back to the shopper. +- [**Cancel**](CancelRefundPayment.md#cancel-or-refund-a-payment) - cancel the authorisation on an uncaptured payment. +- [**Refund**](CancelRefundPayment.md#cancel-or-refund-a-payment) - refund a payment back to the shopper. + ## Requirements for CTP project All the requirements below are automatically created by the Extension module. @@ -63,12 +64,14 @@ If all above validations are passed then order can be created right away and ord Otherwise shopper might continue with further payment steps. 1. **Get available payment methods** + > Before user can proceed with particular payment method below steps describes how to get and present the list of available payment methods: 1. [Create/update a CTP Payment](https://docs.commercetools.com/http-api-projects-payments) with following properties: - `Payment.paymentMethodInfo.method = empty or undefined` - `Payment.custom.fields.countryCode != null` - set the country of a shopper. Please [consult with Adyen](https://docs.adyen.com/api-explorer/#/PaymentSetupAndVerificationService/v41/paymentMethods) for the right format. 1. Extension module will make a [request](https://docs.adyen.com/checkout/api-integration/#step-1-get-available-payment-methods) to Adyen API and the response will be saved in interface interaction with `type='ctp-adyen-integration-interaction'` and `fields.type='getAvailablePaymentMethods'` as stringified JSON. 1. Before presenting the payment methods in the response from Adyen, please check Extension module documentation for supported payment methods. 1. **Continue with one of the supported payment methods** + > Below guides describe how to handle different type of payment methods selected by the user: - [Credit card payment](./CreditCardIntegration.md) - [Paypal payment](./PaypalIntegration.md) @@ -107,8 +110,8 @@ The combination of `Payment.custom.fields.merchantReference` and `Payment.paymen ### Validate payment transaction Cart's payment counts as successful if there is at least one payment object -with only successful (`Payment.Transaction.state=Success`) -payment transactions of type `Charge`. +with successful transaction state (`Payment.Transaction.state=Success`) +and transactions type `Authorization` or `Charge`. ### Mapping from Adyen result codes to CTP transaction state |Adyen result code| CTP transaction state diff --git a/extension/docs/KcpIntegration.md b/extension/docs/KcpIntegration.md index 6f55b1094..7ba54b145 100644 --- a/extension/docs/KcpIntegration.md +++ b/extension/docs/KcpIntegration.md @@ -12,13 +12,13 @@ 1. Contact Adyen to enable KCP 1. Shop creates a payment with following criteria: * `Payment.paymentMethodInfo.method = 'kcp_creditcard' OR payment.paymentMethodInfo.method = 'kcp_banktransfer'` - * `Payment.transactions` contains a transaction with `type='Charge' and state='Initial'` + * `Payment.transactions` contains a transaction with `type='Authorization' and state='Initial'` * `Payment.custom.fields.returnUrl` contains return URL to which the shopper will be redirected after completion. 1. Extension module makes a `Redirect shopper` request and saves following information to the payment object: * `Payment.interfaceInteractions` with `type='makePayment'` that contains request and response with Adyen * `payment.custom.fields.redirectUrl` * `payment.custom.fields.redirectMethod` - * `Charge` transaction state will be updated to `Pending` + * `Authorization` transaction state will be updated to `Pending` 1. Shop redirects user to KCP using the custom fields above **TODO: to be continued** diff --git a/extension/docs/PaypalIntegration.md b/extension/docs/PaypalIntegration.md index 78d1e8f3c..079b2d60e 100644 --- a/extension/docs/PaypalIntegration.md +++ b/extension/docs/PaypalIntegration.md @@ -12,11 +12,11 @@ 1. [Enable Paypal in Adyen](https://docs.adyen.com/developers/payment-methods/paypal#prerequisites) 1. Shop creates a payment with following criteria ([Example payment](../test/fixtures/payment-paypal.json)): * `Payment.paymentMethodInfo.method = 'paypal'` - * `Payment.transactions` contains a transaction with `type='Charge' and state='Initial'` + * `Payment.transactions` contains a transaction with `type='Authorization' and state='Initial'` * `Payment.custom.fields.returnUrl` contains return URL to which the shopper will be redirected after completion. 1. Extension module make a payment request and save following information to the payment object: * `Payment.interfaceInteractions` with `type='makePayment'` that contains request and response with Adyen - * `Payment.transactions` with a transaction `type='Charge' and state='Initial'` will be changed to `'Pending'`. + * `Payment.transactions` with a transaction `type='Authorization' and state='Initial'` will be changed to `'Pending'`. * `Payment.custom.fields.redirectUrl` * `Payment.custom.fields.redirectMethod` 1. Shop [redirects shopper](https://docs.adyen.com/developers/payment-methods/paypal#step2redirectshopper) to Paypal. @@ -33,7 +33,7 @@ ``` 1. Extension module make a [payment request](https://docs.adyen.com/developers/payment-methods/paypal#step4presentpaymentresult) and save following information to the payment object: * `Payment.interfaceInteractions` with `type='completePayment'` that contains request and response with Adyen - * `Payment.transactions` with a transaction `type='Charge' and state='Pending'` will be changed to a new state according to [the returned result code](IntegrationGuide.md#mapping-from-adyen-result-codes-to-ctp-transaction-state). + * `Payment.transactions` with a transaction `type='Authorization' and state='Pending'` will be changed to a new state according to [the returned result code](IntegrationGuide.md#mapping-from-adyen-result-codes-to-ctp-transaction-state). * `pspReference` will be saved in a matching transaction from the previous point in a field `transactionInteractionId` 1. Shop validates the payment and presents the payment result to the shopper. @@ -41,4 +41,4 @@ Funds has already been reserved/transferred after the shopper confirms payment on Paypal. Nevertheless, it's important to follow all the steps as it's the only way to get `pspReference` from Adyen. -![Paypal flow](https://user-images.githubusercontent.com/803826/56141239-b9e76f80-5f9c-11e9-95e7-7358903121cd.png) \ No newline at end of file +![Paypal flow](https://user-images.githubusercontent.com/803826/56141239-b9e76f80-5f9c-11e9-95e7-7358903121cd.png) diff --git a/extension/package-lock.json b/extension/package-lock.json index caee32630..3fd0e07c4 100644 --- a/extension/package-lock.json +++ b/extension/package-lock.json @@ -1,6 +1,6 @@ { "name": "commercetools-adyen-integration", - "version": "0.0.1", + "version": "3.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -544,6 +544,21 @@ "color-convert": "^1.9.0" } }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "requires": { + "default-require-extensions": "^2.0.0" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -778,6 +793,18 @@ "os-homedir": "^1.0.1" } }, + "caching-transform": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz", + "integrity": "sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w==", + "dev": true, + "requires": { + "hasha": "^3.0.0", + "make-dir": "^2.0.0", + "package-hash": "^3.0.0", + "write-file-atomic": "^2.4.2" + } + }, "callsites": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.0.0.tgz", @@ -1026,6 +1053,12 @@ "babel-runtime": "^6.18.0" } }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1074,6 +1107,15 @@ "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", "dev": true }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, "core-js": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.3.tgz", @@ -1086,6 +1128,27 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "cp-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz", + "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "make-dir": "^2.0.0", + "nested-error-stacks": "^2.0.0", + "pify": "^4.0.1", + "safe-buffer": "^5.0.1" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + } + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -1393,6 +1456,15 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "dev": true, + "requires": { + "strip-bom": "^3.0.0" + } + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -1571,6 +1643,12 @@ "is-symbol": "^1.0.2" } }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -2020,6 +2098,77 @@ "merge-descriptors": "~1.0.0" } }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + } + } + }, "find-up": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", @@ -2059,6 +2208,28 @@ "write": "^0.2.1" } }, + "foreground-child": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", + "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", + "dev": true, + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + } + } + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -2194,6 +2365,26 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "handlebars": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.3.tgz", + "integrity": "sha512-B0W4A2U1ww3q7VVthTKfh+epHx+q4mCt6iK+zEAzbMBpWQAwxCeKxEGpj/1oQTpzPXDNSOG7hmG14TsISH50yw==", + "dev": true, + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -2247,6 +2438,15 @@ "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", "dev": true }, + "hasha": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", + "integrity": "sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=", + "dev": true, + "requires": { + "is-stream": "^1.0.1" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -2587,6 +2787,203 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "dev": true }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", + "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", + "dev": true, + "requires": { + "append-transform": "^1.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + }, + "dependencies": { + "@babel/generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.4.tgz", + "integrity": "sha512-jsBuXkFoZxk0yWLyGI9llT9oiQ2FeTASmRFE32U+aaDTfoE92t78eroO7PTpU/OrYq38hlcDM6vbfLDaOLy+7w==", + "dev": true, + "requires": { + "@babel/types": "^7.6.3", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", + "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/parser": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.4.tgz", + "integrity": "sha512-D8RHPW5qd0Vbyo3qb+YjO5nvUVRTXFLQ/FsDxJU2Nqz4uB5EnUN0ZQSEYpvTIbRuttig1XbHWU5oMeQwQSAA+A==", + "dev": true + }, + "@babel/template": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz", + "integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.0" + } + }, + "@babel/traverse": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.3.tgz", + "integrity": "sha512-unn7P4LGsijIxaAJo/wpoU11zN+2IaClkQAxcJWBNCMS6cmVh802IyLHNkAjQ0iYnRS3nnxk5O3fuXW28IMxTw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.6.3", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/parser": "^7.6.3", + "@babel/types": "^7.6.3", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + } + } + }, + "@babel/types": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.3.tgz", + "integrity": "sha512-CqbcpTxMcpuQTMhjI37ZHVgjBkysg5icREQIEZ0eG1yCNwg3oy+5AaLiOKmjsCj6nqOsa6Hf0ObjRVwokb7srA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", + "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", + "dev": true, + "requires": { + "handlebars": "^4.1.2" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2615,6 +3012,12 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -3021,6 +3424,12 @@ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, "lodash.foreach": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", @@ -3124,6 +3533,34 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + } + } + }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -3170,6 +3607,23 @@ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", "dev": true }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "mime-db": { "version": "1.37.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", @@ -3410,6 +3864,18 @@ "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", "optional": true }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "nested-error-stacks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz", + "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==", + "dev": true + }, "ngrok": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/ngrok/-/ngrok-3.2.4.tgz", @@ -3501,6 +3967,100 @@ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true }, + "nyc": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.1.tgz", + "integrity": "sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "caching-transform": "^3.0.2", + "convert-source-map": "^1.6.0", + "cp-file": "^6.2.0", + "find-cache-dir": "^2.1.0", + "find-up": "^3.0.0", + "foreground-child": "^1.5.6", + "glob": "^7.1.3", + "istanbul-lib-coverage": "^2.0.5", + "istanbul-lib-hook": "^2.0.7", + "istanbul-lib-instrument": "^3.3.0", + "istanbul-lib-report": "^2.0.8", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^2.2.4", + "js-yaml": "^3.13.1", + "make-dir": "^2.1.0", + "merge-source-map": "^1.1.0", + "resolve-from": "^4.0.0", + "rimraf": "^2.6.3", + "signal-exit": "^3.0.2", + "spawn-wrap": "^1.4.2", + "test-exclude": "^5.2.3", + "uuid": "^3.3.2", + "yargs": "^13.2.2", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -3582,6 +4142,24 @@ "mimic-fn": "^1.0.0" } }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, "optionator": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", @@ -3772,6 +4350,18 @@ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, + "package-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-3.0.0.tgz", + "integrity": "sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^3.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, "parent-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.0.tgz", @@ -3985,6 +4575,12 @@ "event-stream": "=3.3.4" } }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, "psl": { "version": "1.1.31", "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", @@ -4101,6 +4697,15 @@ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "dev": true }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, "remark-frontmatter": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-1.3.1.tgz", @@ -4373,6 +4978,20 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, + "spawn-wrap": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.3.tgz", + "integrity": "sha512-IgB8md0QW/+tWqcavuFgKYR/qIRvJkRLPJDFaoXtLLUaVcCDK0+HeFTkmQHj3eprcYhc+gOl0aEA1w7qZlYezw==", + "dev": true, + "requires": { + "foreground-child": "^1.5.6", + "mkdirp": "^0.5.0", + "os-homedir": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "which": "^1.3.0" + } + }, "spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", @@ -4598,6 +5217,127 @@ "string-width": "^2.1.1" } }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + } + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4746,6 +5486,33 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "uglify-js": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.1.tgz", + "integrity": "sha512-+dSJLJpXBb6oMHP+Yvw8hUgElz4gLTh82XuX68QiJVTXaE5ibl6buzhNkQdYhBlIhozWOC9ge16wyRmjG4TwVQ==", + "dev": true, + "optional": true, + "requires": { + "commander": "2.20.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "dev": true, + "optional": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + } + } + }, "underscore": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", @@ -5032,6 +5799,17 @@ "mkdirp": "^0.5.1" } }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, "x-is-string": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", @@ -5050,6 +5828,12 @@ "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", "dev": true }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, "yargs": { "version": "13.2.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", diff --git a/extension/package.json b/extension/package.json index a50513922..e554054f4 100644 --- a/extension/package.json +++ b/extension/package.json @@ -1,10 +1,12 @@ { "name": "commercetools-adyen-integration", - "version": "0.0.1", + "version": "3.0.0", "description": "Integration between Commercetools and Adyen payment service provider", "license": "MIT", "scripts": { - "test": "mocha --exit --timeout 30000 --full-trace test/**/*.spec.js", + "test": "nyc mocha --exit --timeout 30000 --full-trace test/**/*.spec.js", + "unit": "nyc mocha --exit --timeout 30000 --full-trace test/unit/**/*.spec.js", + "integration": "nyc mocha --exit --timeout 30000 --full-trace test/integration/**/*.spec.js", "start": "node ./src/init.js", "lint": "eslint .", "cypress:run": "cypress run", @@ -55,6 +57,7 @@ "eslint-plugin-react": "^7.12.4", "mocha": "^6.2.0", "ngrok": "^3.2.4", + "nyc": "^14.1.1", "proxyquire": "^2.1.2", "sinon": "^7.4.1", "start-server-and-test": "^1.10.0" diff --git a/extension/src/paymentHandler/cancel-or-refund.handler.js b/extension/src/paymentHandler/cancel-or-refund.handler.js index a85706002..d05e0c4fc 100644 --- a/extension/src/paymentHandler/cancel-or-refund.handler.js +++ b/extension/src/paymentHandler/cancel-or-refund.handler.js @@ -30,7 +30,9 @@ async function handlePayment (paymentObject) { } async function _cancelOrRefundPayment (paymentObject) { - const transaction = pU.getChargeTransactionSuccess(paymentObject) + const transaction = pU.getAuthorizationTransactionSuccess(paymentObject) + // "originalReference: The original pspReference of the payment that you want to cancel or refund. + // This reference is returned in the response to your payment request, and in the AUTHORISATION notification." const body = { merchantAccount: config.adyen.merchantAccount, originalReference: transaction.interactionId, diff --git a/extension/src/paymentHandler/creditCard/credit-card-complete-payment.handler.js b/extension/src/paymentHandler/creditCard/credit-card-complete-payment.handler.js index 55907786e..208877a44 100644 --- a/extension/src/paymentHandler/creditCard/credit-card-complete-payment.handler.js +++ b/extension/src/paymentHandler/creditCard/credit-card-complete-payment.handler.js @@ -23,7 +23,7 @@ async function handlePayment (paymentObject) { }) ] if (responseBody.resultCode) { - const transaction = pU.getChargeTransactionPending(paymentObject) + const transaction = pU.getAuthorizationTransactionPending(paymentObject) const transactionState = pU.getMatchingCtpState(responseBody.resultCode.toLowerCase()) actions.push( pU.createChangeTransactionStateAction(transaction.id, transactionState) diff --git a/extension/src/paymentHandler/creditCard/credit-card-make-payment.handler.js b/extension/src/paymentHandler/creditCard/credit-card-make-payment.handler.js index 8c5149951..56bb22b56 100644 --- a/extension/src/paymentHandler/creditCard/credit-card-make-payment.handler.js +++ b/extension/src/paymentHandler/creditCard/credit-card-make-payment.handler.js @@ -22,7 +22,7 @@ async function handlePayment (paymentObject) { }) ] if (responseBody.resultCode) { - const transaction = pU.getChargeTransactionInit(paymentObject) + const transaction = pU.getAuthorizationTransactionInit(paymentObject) const resultCode = responseBody.resultCode.toLowerCase() if (resultCode === c.REDIRECT_SHOPPER.toLowerCase()) { const { MD } = responseBody.redirect.data @@ -79,7 +79,7 @@ function _validatePayment (paymentObject) { } async function _makePayment (paymentObject) { - const transaction = pU.getChargeTransactionInitOrPending(paymentObject) + const transaction = pU.getAuthorizationTransactionInitOrPending(paymentObject) const body = { amount: { currency: transaction.amount.currencyCode, diff --git a/extension/src/paymentHandler/creditCard/credit-card.handler.js b/extension/src/paymentHandler/creditCard/credit-card.handler.js index 47bd8b27a..714d9f429 100644 --- a/extension/src/paymentHandler/creditCard/credit-card.handler.js +++ b/extension/src/paymentHandler/creditCard/credit-card.handler.js @@ -6,10 +6,10 @@ const creditCardMakePayment = require('./credit-card-make-payment.handler') const creditCardCompletePayment = require('./credit-card-complete-payment.handler') async function handlePayment (paymentObject) { - const hasPendingTransaction = _.isObject(pU.getChargeTransactionPending(paymentObject)) + const hasPendingTransaction = _.isObject(pU.getAuthorizationTransactionPending(paymentObject)) if (hasPendingTransaction) return creditCardCompletePayment.handlePayment(paymentObject) - const hasInitTransaction = _.isObject(pU.getChargeTransactionInit(paymentObject)) + const hasInitTransaction = _.isObject(pU.getAuthorizationTransactionInit(paymentObject)) if (hasInitTransaction) return creditCardMakePayment.handlePayment(paymentObject) return { diff --git a/extension/src/paymentHandler/fetch-payment-methods.handler.js b/extension/src/paymentHandler/fetch-payment-methods.handler.js index 2e4f28da9..d0aa66ad6 100644 --- a/extension/src/paymentHandler/fetch-payment-methods.handler.js +++ b/extension/src/paymentHandler/fetch-payment-methods.handler.js @@ -18,7 +18,7 @@ async function handlePayment (paymentObject) { } function _getTransaction (paymentObject) { - return paymentObject.transactions.find(t => t.type.toLowerCase() === 'charge' + return paymentObject.transactions.find(t => t.type.toLowerCase() === 'authorization' && (t.state.toLowerCase() === 'initial' || t.state.toLowerCase() === 'pending')) } diff --git a/extension/src/paymentHandler/kcp/kcp-make-payment.handler.js b/extension/src/paymentHandler/kcp/kcp-make-payment.handler.js index c74d9274d..11babed52 100644 --- a/extension/src/paymentHandler/kcp/kcp-make-payment.handler.js +++ b/extension/src/paymentHandler/kcp/kcp-make-payment.handler.js @@ -22,7 +22,7 @@ async function handlePayment (paymentObject) { }) ] if (responseBody.resultCode === c.REDIRECT_SHOPPER) { - const transaction = pU.getChargeTransactionInit(paymentObject) + const transaction = pU.getAuthorizationTransactionInit(paymentObject) const redirectUrl = responseBody.redirect.url actions.push( pU.createSetCustomFieldAction('redirectUrl', redirectUrl) @@ -49,7 +49,7 @@ function _validatePayment (paymentObject) { } async function _callAdyen (paymentObject) { - const transaction = pU.getChargeTransactionInit(paymentObject) + const transaction = pU.getAuthorizationTransactionInit(paymentObject) const paymentMethodType = paymentObject.paymentMethodInfo.method const requestBody = { amount: { diff --git a/extension/src/paymentHandler/kcp/kcp-payment.handler.js b/extension/src/paymentHandler/kcp/kcp-payment.handler.js index 35ac654e9..67167c310 100644 --- a/extension/src/paymentHandler/kcp/kcp-payment.handler.js +++ b/extension/src/paymentHandler/kcp/kcp-payment.handler.js @@ -4,7 +4,7 @@ const pU = require('../payment-utils') const kcpMakePayment = require('./kcp-make-payment.handler') async function handlePayment (paymentObject) { - const hasInitTransaction = _.isObject(pU.getChargeTransactionInit(paymentObject)) + const hasInitTransaction = _.isObject(pU.getAuthorizationTransactionInit(paymentObject)) if (hasInitTransaction) return kcpMakePayment.handlePayment(paymentObject) return { diff --git a/extension/src/paymentHandler/payment-utils.js b/extension/src/paymentHandler/payment-utils.js index 095934d7e..8f17495e6 100644 --- a/extension/src/paymentHandler/payment-utils.js +++ b/extension/src/paymentHandler/payment-utils.js @@ -1,21 +1,21 @@ const _ = require('lodash') const c = require('../config/constants') -function getChargeTransactionInitOrPending (paymentObject) { +function getAuthorizationTransactionInitOrPending (paymentObject) { return getTransactionWithTypesAndStates(paymentObject, - ['Charge'], + ['Authorization'], ['Initial', 'Pending']) } -function getChargeTransactionPending (paymentObject) { +function getAuthorizationTransactionPending (paymentObject) { return getTransactionWithTypesAndStates(paymentObject, - ['Charge'], + ['Authorization'], ['Pending']) } -function getChargeTransactionSuccess (paymentObject) { +function getAuthorizationTransactionSuccess (paymentObject) { return getTransactionWithTypesAndStates(paymentObject, - ['Charge'], + ['Authorization'], ['Success']) } @@ -31,9 +31,9 @@ function getRefundTransactionInit (paymentObject) { ['Initial']) } -function getChargeTransactionInit (paymentObject) { +function getAuthorizationTransactionInit (paymentObject) { return getTransactionWithTypesAndStates(paymentObject, - ['Charge'], + ['Authorization'], ['Initial']) } @@ -118,10 +118,10 @@ function createChangeTransactionInteractionId (transactionId, interactionId) { } module.exports = { - getChargeTransactionInitOrPending, - getChargeTransactionPending, - getChargeTransactionInit, - getChargeTransactionSuccess, + getAuthorizationTransactionInitOrPending, + getAuthorizationTransactionPending, + getAuthorizationTransactionInit, + getAuthorizationTransactionSuccess, getCancelAuthorizationTransactionInit, getRefundTransactionInit, getMatchingCtpState, diff --git a/extension/src/paymentHandler/paypal/paypal-complete-payment.handler.js b/extension/src/paymentHandler/paypal/paypal-complete-payment.handler.js index bc253023c..9943d0220 100644 --- a/extension/src/paymentHandler/paypal/paypal-complete-payment.handler.js +++ b/extension/src/paymentHandler/paypal/paypal-complete-payment.handler.js @@ -23,7 +23,7 @@ async function handlePayment (paymentObject) { }) ] if (responseBody.resultCode) { - const transaction = pU.getChargeTransactionPending(paymentObject) + const transaction = pU.getAuthorizationTransactionPending(paymentObject) const transactionState = pU.getMatchingCtpState(responseBody.resultCode.toLowerCase()) actions.push( pU.createChangeTransactionStateAction(transaction.id, transactionState) diff --git a/extension/src/paymentHandler/paypal/paypal-make-payment.handler.js b/extension/src/paymentHandler/paypal/paypal-make-payment.handler.js index f4190a19d..5ab01eab8 100644 --- a/extension/src/paymentHandler/paypal/paypal-make-payment.handler.js +++ b/extension/src/paymentHandler/paypal/paypal-make-payment.handler.js @@ -22,7 +22,7 @@ async function handlePayment (paymentObject) { }) ] if (responseBody.resultCode === c.REDIRECT_SHOPPER) { - const transaction = pU.getChargeTransactionInit(paymentObject) + const transaction = pU.getAuthorizationTransactionInit(paymentObject) const redirectUrl = responseBody.redirect.url actions.push( pU.createSetCustomFieldAction('redirectUrl', redirectUrl) @@ -49,7 +49,7 @@ function _validatePayment (paymentObject) { } async function _callAdyen (paymentObject) { - const transaction = pU.getChargeTransactionInit(paymentObject) + const transaction = pU.getAuthorizationTransactionInit(paymentObject) const body = { merchantAccount: config.adyen.merchantAccount, amount: { diff --git a/extension/src/paymentHandler/paypal/paypal.handler.js b/extension/src/paymentHandler/paypal/paypal.handler.js index a8e477dc6..d08123a6f 100644 --- a/extension/src/paymentHandler/paypal/paypal.handler.js +++ b/extension/src/paymentHandler/paypal/paypal.handler.js @@ -5,10 +5,10 @@ const paypalMakePayment = require('./paypal-make-payment.handler') const paypalCompletePayment = require('./paypal-complete-payment.handler') async function handlePayment (paymentObject) { - const hasPendingTransaction = _.isObject(pU.getChargeTransactionPending(paymentObject)) + const hasPendingTransaction = _.isObject(pU.getAuthorizationTransactionPending(paymentObject)) if (hasPendingTransaction) return paypalCompletePayment.handlePayment(paymentObject) - const hasInitTransaction = _.isObject(pU.getChargeTransactionInit(paymentObject)) + const hasInitTransaction = _.isObject(pU.getAuthorizationTransactionInit(paymentObject)) if (hasInitTransaction) return paypalMakePayment.handlePayment(paymentObject) diff --git a/extension/src/validator/error-messages.js b/extension/src/validator/error-messages.js index 65fb2c5ad..e053f780b 100644 --- a/extension/src/validator/error-messages.js +++ b/extension/src/validator/error-messages.js @@ -2,8 +2,8 @@ module.exports = { MISSING_PAYMENT_INTERFACE: 'Set paymentMethodInfo.paymentInterface = \'ctp-adyen-integration\'', /* eslint-disable-next-line max-len */ INVALID_PAYMENT_METHOD: 'Set paymentMethodInfo.method to one of the supported methods or leave empty for fetching available payment methods from Adyen', - MISSING_TXN_CHARGE_PENDING: 'Have one transaction with type=\'Charge\' AND state=\'Pending\'', - MISSING_TXN_CHARGE_INIT: 'Have one transaction with type=\'Charge\' AND state=\'Initial\'', + MISSING_TXN_AUTHORIZATION_PENDING: 'Have one transaction with type=\'Authorization\' AND state=\'Pending\'', + MISSING_TXN_AUTHORIZATION_INIT: 'Have one transaction with type=\'Authorization\' AND state=\'Initial\'', MISSING_CARD_NUMBER: 'Set custom.fields.encryptedCardNumber', MISSING_EXPIRY_MONTH: 'Set custom.fields.encryptedExpiryMonth', MISSING_EXPIRY_YEAR: 'Set custom.fields.encryptedExpiryYear', @@ -14,5 +14,5 @@ module.exports = { MISSING_PAYMENT_DATA: 'Set custom.fields.paymentData', MISSING_PARES: 'Set custom.fields.PaRes', MISSING_MD: 'Set custom.fields.MD', - MISSING_TXN_CHARGE_INIT_PENDING: 'Have one Charge transaction in state=\'Initial\' or state=\'Pending\'' + MISSING_TXN_AUTHORIZATION_INIT_PENDING: 'Have one Authorization transaction in state=\'Initial\' or state=\'Pending\'' } diff --git a/extension/src/validator/validator-builder.js b/extension/src/validator/validator-builder.js index 16c954c31..3d401aa6e 100644 --- a/extension/src/validator/validator-builder.js +++ b/extension/src/validator/validator-builder.js @@ -21,7 +21,7 @@ function withPayment (paymentObject) { return this }, isCancelOrRefund () { - return _.isObject(pU.getChargeTransactionSuccess(paymentObject)) + return _.isObject(pU.getAuthorizationTransactionSuccess(paymentObject)) && (_.isObject(pU.getCancelAuthorizationTransactionInit(paymentObject)) || _.isObject(pU.getRefundTransactionInit(paymentObject))) }, @@ -36,18 +36,18 @@ function withPayment (paymentObject) { return paymentObject.paymentMethodInfo.method === 'creditCard' || paymentObject.paymentMethodInfo.method === 'creditCard_3d' }, - validateChargeTransactionPending () { - const transaction = pU.getChargeTransactionPending(paymentObject) - const hasChargeTransactionPending = _.isObject(transaction) - if (!hasChargeTransactionPending) - errors.hasChargeTransactionPending = errorMessages.MISSING_TXN_CHARGE_PENDING + validateAuthorizationTransactionPending () { + const transaction = pU.getAuthorizationTransactionPending(paymentObject) + const hasAuthorizationTransactionPending = _.isObject(transaction) + if (!hasAuthorizationTransactionPending) + errors.hasAuthorizationTransactionPending = errorMessages.MISSING_TXN_AUTHORIZATION_PENDING return this }, - validateChargeTransactionInit () { - const transaction = pU.getChargeTransactionInit(paymentObject) - const hasChargeTransactionInit = _.isObject(transaction) - if (!hasChargeTransactionInit) - errors.hasChargeTransactionInit = errorMessages.MISSING_TXN_CHARGE_INIT + validateAuthorizationTransactionInit () { + const transaction = pU.getAuthorizationTransactionInit(paymentObject) + const hasAuthorizationTransactionInit = _.isObject(transaction) + if (!hasAuthorizationTransactionInit) + errors.hasAuthorizationTransactionInit = errorMessages.MISSING_TXN_AUTHORIZATION_INIT return this }, validateEncryptedCardNumberField () { diff --git a/extension/test/fixtures/ctp-payment.json b/extension/test/fixtures/ctp-payment.json index b10c446fb..afa9fc172 100644 --- a/extension/test/fixtures/ctp-payment.json +++ b/extension/test/fixtures/ctp-payment.json @@ -47,7 +47,7 @@ "transactions": [ { "id": "6b1d4c68-8e31-4528-9af8-dc09ad653d91", - "type": "Charge", + "type": "Authorization", "amount": { "type": "centPrecision", "currencyCode": "EUR", diff --git a/extension/test/fixtures/payment-credit-card-3d.json b/extension/test/fixtures/payment-credit-card-3d.json index 8434095e5..f89f70198 100644 --- a/extension/test/fixtures/payment-credit-card-3d.json +++ b/extension/test/fixtures/payment-credit-card-3d.json @@ -29,7 +29,7 @@ "paymentStatus": {}, "transactions": [ { - "type": "Charge", + "type": "Authorization", "amount": { "type": "centPrecision", "currencyCode": "EUR", @@ -41,4 +41,4 @@ ], "interfaceInteractions": [ ] -} \ No newline at end of file +} diff --git a/extension/test/fixtures/payment-credit-card.json b/extension/test/fixtures/payment-credit-card.json index 2b81caf9d..3a2caf36d 100644 --- a/extension/test/fixtures/payment-credit-card.json +++ b/extension/test/fixtures/payment-credit-card.json @@ -27,7 +27,7 @@ "paymentStatus": {}, "transactions": [ { - "type": "Charge", + "type": "Authorization", "amount": { "type": "centPrecision", "currencyCode": "EUR", @@ -39,4 +39,4 @@ ], "interfaceInteractions": [ ] -} \ No newline at end of file +} diff --git a/extension/test/fixtures/payment-kcp.json b/extension/test/fixtures/payment-kcp.json index 1a4838726..99015646a 100644 --- a/extension/test/fixtures/payment-kcp.json +++ b/extension/test/fixtures/payment-kcp.json @@ -22,7 +22,7 @@ "paymentStatus": {}, "transactions": [ { - "type": "Charge", + "type": "Authorization", "amount": { "type": "centPrecision", "currencyCode": "EUR", @@ -34,4 +34,4 @@ ], "interfaceInteractions": [ ] -} \ No newline at end of file +} diff --git a/extension/test/fixtures/payment-paypal.json b/extension/test/fixtures/payment-paypal.json index 64a7926e9..d2afb72c1 100644 --- a/extension/test/fixtures/payment-paypal.json +++ b/extension/test/fixtures/payment-paypal.json @@ -22,7 +22,7 @@ "paymentStatus": {}, "transactions": [ { - "type": "Charge", + "type": "Authorization", "amount": { "type": "centPrecision", "currencyCode": "EUR", @@ -34,4 +34,4 @@ ], "interfaceInteractions": [ ] -} \ No newline at end of file +} diff --git a/extension/test/integration/cancel-or-refund.handler.spec.js b/extension/test/integration/cancel-or-refund.handler.spec.js index b5b01505a..9266055f4 100644 --- a/extension/test/integration/cancel-or-refund.handler.spec.js +++ b/extension/test/integration/cancel-or-refund.handler.spec.js @@ -27,7 +27,7 @@ describe('Cancel or refund', () => { const payment = response.body const paymentId = payment.id const paymentVersion = payment.version - const chargeTransaction = payment.transactions[0] + const transaction = payment.transactions[0] const response2 = await ctpClient.update(ctpClient.builder.payments, paymentId, paymentVersion, [ @@ -36,8 +36,8 @@ describe('Cancel or refund', () => { transaction: { type: 'Refund', amount: { - currencyCode: chargeTransaction.amount.currencyCode, - centAmount: chargeTransaction.amount.centAmount + currencyCode: transaction.amount.currencyCode, + centAmount: transaction.amount.centAmount }, state: 'Initial' } @@ -53,7 +53,7 @@ describe('Cancel or refund', () => { const interfaceInteractionFields = updatedPayment.interfaceInteractions[1].fields //interfaceInteractionFields.request is a stringify json const adyenRequestBody = JSON.parse(JSON.parse(interfaceInteractionFields.request)) - expect(adyenRequestBody.originalReference).to.equal(chargeTransaction.interactionId) + expect(adyenRequestBody.originalReference).to.equal(transaction.interactionId) const adyenResponse = JSON.parse(interfaceInteractionFields.response) expect(adyenResponse.response).to.equal('[cancelOrRefund-received]') diff --git a/extension/test/integration/credit-card-make-payment.handler.spec.js b/extension/test/integration/credit-card-make-payment.handler.spec.js index b5d555cb6..68b1f7da8 100644 --- a/extension/test/integration/credit-card-make-payment.handler.spec.js +++ b/extension/test/integration/credit-card-make-payment.handler.spec.js @@ -52,7 +52,7 @@ describe('credit card payment', () => { const { transactions } = response.body expect(transactions).to.have.lengthOf(1) - expect(transactions[0].type).to.equal('Charge') + expect(transactions[0].type).to.equal('Authorization') expect(transactions[0].state).to.equal('Success') expect(transactions[0].interactionId).to.match(/^[0-9a-zA-Z]*$/) }) @@ -126,7 +126,7 @@ describe('credit card payment', () => { expect(transactions).to.have.lengthOf(1) const transaction = transactions[0] expect(transaction.interactionId).to.be.undefined - expect(transaction.type).to.equal('Charge') + expect(transaction.type).to.equal('Authorization') expect(transaction.state).to.equal('Initial') const response2 = await ctpClient.update(ctpClient.builder.payments, diff --git a/notification/package-lock.json b/notification/package-lock.json index 85c72d537..3c5fc6ffa 100644 --- a/notification/package-lock.json +++ b/notification/package-lock.json @@ -1,6 +1,6 @@ { "name": "commercetools-adyen-integration", - "version": "0.0.1", + "version": "3.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/notification/package.json b/notification/package.json index a44806f9f..32cfc8440 100644 --- a/notification/package.json +++ b/notification/package.json @@ -1,9 +1,11 @@ { "name": "commercetools-adyen-integration", - "version": "0.0.1", + "version": "3.0.0", "description": "Part of the integration of Adyen with commercetools responsible to receive and process notifications from Adyen", "scripts": { - "test": "nyc mocha --check-leaks --timeout 30000 --full-trace --recursive \"./test/**/*.spec.js\"", + "test": "npm run unit && npm run integration", + "unit": "nyc mocha --check-leaks --timeout 30000 --full-trace --recursive \"./test/unit/**/*.spec.js\"", + "integration": "nyc mocha --check-leaks --timeout 30000 --full-trace --recursive \"./test/integration/**/*.spec.js\"", "start": "node ./src/init.js", "coverage": "nyc report" }, diff --git a/notification/resources/adyen-events.json b/notification/resources/adyen-events.json index e278d4f3a..f992872ff 100644 --- a/notification/resources/adyen-events.json +++ b/notification/resources/adyen-events.json @@ -38,13 +38,13 @@ { "eventCode": "CANCEL_OR_REFUND", "success": "false", - "transactionType": "Refund", + "transactionType": null, "transactionState": "Failure" }, { "eventCode": "CANCEL_OR_REFUND", "success": "true", - "transactionType": "Refund", + "transactionType": null, "transactionState": "Success" }, { diff --git a/notification/src/handler/notification/notification.handler.js b/notification/src/handler/notification/notification.handler.js index dcc7d4f08..2940d50ca 100644 --- a/notification/src/handler/notification/notification.handler.js +++ b/notification/src/handler/notification/notification.handler.js @@ -65,8 +65,6 @@ async function updatePaymentWithRepeater (payment, notification, ctpClient) { function calculateUpdateActionsForPayment (payment, notification) { const updateActions = [] const notificationRequestItem = notification.NotificationRequestItem - const notificationEventCode = notificationRequestItem.eventCode - const notificationSuccess = notificationRequestItem.success const stringifiedNotification = JSON.stringify(notification) // check if the interfaceInteraction is already on payment or not const isNotificationInInterfaceInteraction = payment.interfaceInteractions.some( @@ -78,7 +76,7 @@ function calculateUpdateActionsForPayment (payment, notification) { const { transactionType, transactionState - } = getTransactionTypeAndStateOrNull(notificationEventCode, notificationSuccess) + } = getTransactionTypeAndStateOrNull(notificationRequestItem) if (transactionType !== null) { // if there is already a transaction with type `transactionType` then update its `transactionState` if necessary, // otherwise create a transaction with type `transactionType` and state `transactionState` @@ -123,10 +121,30 @@ function getChangeTransactionStateUpdateAction (transactionId, newTransactionSta } } -function getTransactionTypeAndStateOrNull (adyenEventCode, adyenEventSuccess) { +function getTransactionTypeAndStateOrNull (notificationRequestItem) { + const adyenEventCode = notificationRequestItem.eventCode + const adyenEventSuccess = notificationRequestItem.success + // eslint-disable-next-line max-len - return _.find(adyenEvents, adyenEvent => adyenEvent.eventCode === adyenEventCode && adyenEvent.success === adyenEventSuccess) - || { + const adyenEvent = _.find(adyenEvents, adyenEvent => adyenEvent.eventCode === adyenEventCode && adyenEvent.success === adyenEventSuccess) + if (adyenEvent && adyenEventCode === 'CANCEL_OR_REFUND') { + /* we need to get correct action from the additional data, for example: + "NotificationRequestItem":{ + "additionalData":{ + "modification.action":"refund" + } + ... + } + */ + const modificationAction = notificationRequestItem.additionalData ? + notificationRequestItem.additionalData['modification.action'] : null; + if (modificationAction === 'refund') { + adyenEvent.transactionType = 'Refund' + } else if (modificationAction === 'cancel') { + adyenEvent.transactionType = 'CancelAuthorization' + } + } + return adyenEvent || { eventCode: adyenEventCode, success: adyenEventSuccess, transactionType: null, diff --git a/notification/test/integration/notification.handler.spec.js b/notification/test/integration/notification.handler.spec.js index 9f2bc1855..c0922bc0d 100644 --- a/notification/test/integration/notification.handler.spec.js +++ b/notification/test/integration/notification.handler.spec.js @@ -30,11 +30,12 @@ describe('notification module', () => { await iTSetUp.cleanupProject(ctpClient) }) - it('should update the transaction state when receives a correct notification', async () => { + it('should update the pending authorization transaction state to success state ' + + 'when receives a successful AUTHORIZATION notification', async () => { const { body: { results: [ paymentBefore ] } } = await ctpClient.fetch(ctpClient.builder.payments) expect(paymentBefore.transactions).to.have.lengthOf(1) expect(paymentBefore.transactions[0].type).to.equal('Authorization') - expect(paymentBefore.transactions[0].state).to.equal('Initial') + expect(paymentBefore.transactions[0].state).to.equal('Pending') expect(paymentBefore.interfaceInteractions).to.have.lengthOf(0) // Simulating a notification from Adyen @@ -58,12 +59,60 @@ describe('notification module', () => { expect(paymentAfter.interfaceInteractions[0].fields.notification).to.equal(JSON.stringify(notification)) }) + it('should add a charge transaction when receives a successful manual CAPTURE notification', async () => { + const { body: { results: [ paymentBefore ] } } = await ctpClient.fetch(ctpClient.builder.payments) + expect(paymentBefore.transactions).to.have.lengthOf(1) + expect(paymentBefore.transactions[0].type).to.equal('Authorization') + expect(paymentBefore.transactions[0].state).to.equal('Pending') + expect(paymentBefore.interfaceInteractions).to.have.lengthOf(0) + + //update payment transaction + const actions = [{ + action: "changeTransactionState", + state: "Success", + transactionId: paymentBefore.transactions[0].id + }] + + const { body: updatedPayment } = + await ctpClient.update(ctpClient.builder.payments, paymentBefore.id, paymentBefore.version, actions) + + expect(updatedPayment.transactions).to.have.lengthOf(1) + expect(updatedPayment.transactions[0].type).to.equal('Authorization') + expect(updatedPayment.transactions[0].state).to.equal('Success') + + const modifiedNotification = cloneDeep(notifications) + modifiedNotification.notificationItems[0].NotificationRequestItem.eventCode = 'CAPTURE' + + // Simulating a notification from Adyen + const response = await fetch(`http://${localhostIp}:8000`, { + method: 'post', + body: JSON.stringify(modifiedNotification), + headers: { 'Content-Type': 'application/json' }, + }) + const { status } = response + const responseBody = await response.json() + + expect(responseBody).to.deep.equal({ notificationResponse: '[accepted]' }) + expect(status).to.equal(200) + + const { body: { results: [ paymentAfter ] } } = await ctpClient.fetch(ctpClient.builder.payments) + expect(paymentAfter.transactions).to.have.lengthOf(2) + expect(paymentAfter.transactions[0].type).to.equal('Authorization') + expect(paymentAfter.transactions[0].state).to.equal('Success') + expect(paymentAfter.transactions[1].type).to.equal('Charge') + expect(paymentAfter.transactions[1].state).to.equal('Success') + + expect(paymentAfter.interfaceInteractions).to.have.lengthOf(1) + const notification = modifiedNotification.notificationItems[0] + expect(paymentAfter.interfaceInteractions[0].fields.notification).to.equal(JSON.stringify(notification)) + }) + it('should not update transaction when the notification event ' + 'is not mapped to any CTP payment state', async () => { const { body: { results: [ paymentBefore ] } } = await ctpClient.fetch(ctpClient.builder.payments) expect(paymentBefore.transactions).to.have.lengthOf(1) expect(paymentBefore.transactions[0].type).to.equal('Authorization') - expect(paymentBefore.transactions[0].state).to.equal('Initial') + expect(paymentBefore.transactions[0].state).to.equal('Pending') expect(paymentBefore.interfaceInteractions).to.have.lengthOf(0) const modifiedNotification = cloneDeep(notifications) @@ -83,7 +132,7 @@ describe('notification module', () => { const { body: { results: [ paymentAfter ] } } = await ctpClient.fetch(ctpClient.builder.payments) expect(paymentAfter.transactions).to.have.lengthOf(1) expect(paymentAfter.transactions[0].type).to.equal('Authorization') - expect(paymentAfter.transactions[0].state).to.equal('Initial') + expect(paymentAfter.transactions[0].state).to.equal('Pending') expect(paymentAfter.interfaceInteractions).to.have.lengthOf(1) const notification = modifiedNotification.notificationItems[0] expect(paymentAfter.interfaceInteractions[0].fields.notification).to.equal(JSON.stringify(notification)) @@ -93,7 +142,7 @@ describe('notification module', () => { const { body: { results: [ paymentBefore ] } } = await ctpClient.fetch(ctpClient.builder.payments) expect(paymentBefore.transactions).to.have.lengthOf(1) expect(paymentBefore.transactions[0].type).to.equal('Authorization') - expect(paymentBefore.transactions[0].state).to.equal('Initial') + expect(paymentBefore.transactions[0].state).to.equal('Pending') expect(paymentBefore.interfaceInteractions).to.have.lengthOf(0) const modifiedNotification = cloneDeep(notifications) @@ -113,7 +162,134 @@ describe('notification module', () => { const { body: { results: [ paymentAfter ] } } = await ctpClient.fetch(ctpClient.builder.payments) expect(paymentAfter.transactions).to.have.lengthOf(1) expect(paymentAfter.transactions[0].type).to.equal('Authorization') - expect(paymentAfter.transactions[0].state).to.equal('Initial') + expect(paymentAfter.transactions[0].state).to.equal('Pending') expect(paymentAfter.interfaceInteractions).to.have.lengthOf(0) }) + + it('should udpate the pending Refund transaction state to success state ' + + 'when receives a successful CANCEL_OR_REFUND notification with refund action', async () => { + const { body: { results: [ paymentBefore ] } } = await ctpClient.fetch(ctpClient.builder.payments) + expect(paymentBefore.transactions).to.have.lengthOf(1) + expect(paymentBefore.transactions[0].type).to.equal('Authorization') + expect(paymentBefore.transactions[0].state).to.equal('Pending') + expect(paymentBefore.interfaceInteractions).to.have.lengthOf(0) + + const actions = [ + { + action: "changeTransactionState", + state: "Success", + transactionId: paymentBefore.transactions[0].id + }, + { + action: 'addTransaction', + transaction: { + type: 'Refund', + amount: { + currencyCode: paymentBefore.transactions[0].amount.currencyCode, + centAmount: paymentBefore.transactions[0].amount.centAmount + }, + state: 'Pending' + } + } + ] + + const { body: updatedPayment } = + await ctpClient.update(ctpClient.builder.payments, paymentBefore.id, paymentBefore.version, actions) + + expect(updatedPayment.transactions).to.have.lengthOf(2) + expect(updatedPayment.transactions[0].type).to.equal('Authorization') + expect(updatedPayment.transactions[0].state).to.equal('Success') + expect(updatedPayment.transactions[1].type).to.equal('Refund') + expect(updatedPayment.transactions[1].state).to.equal('Pending') + + const modifiedNotification = cloneDeep(notifications) + modifiedNotification.notificationItems[0].NotificationRequestItem.eventCode = 'CANCEL_OR_REFUND' + modifiedNotification.notificationItems[0].NotificationRequestItem.additionalData = { + "modification.action": "refund" + } + + // Simulating a notification from Adyen + const response = await fetch(`http://${localhostIp}:8000`, { + method: 'post', + body: JSON.stringify(modifiedNotification), + headers: { 'Content-Type': 'application/json' }, + }) + const { status } = response + const responseBody = await response.json() + + expect(responseBody).to.deep.equal({ notificationResponse: '[accepted]' }) + expect(status).to.equal(200) + + const { body: { results: [ paymentAfter ] } } = await ctpClient.fetch(ctpClient.builder.payments) + expect(paymentAfter.transactions).to.have.lengthOf(2) + expect(paymentAfter.transactions[0].type).to.equal('Authorization') + expect(paymentAfter.transactions[0].state).to.equal('Success') + expect(paymentAfter.transactions[1].type).to.equal('Refund') + expect(paymentAfter.transactions[1].state).to.equal('Success') + + expect(paymentAfter.interfaceInteractions).to.have.lengthOf(1) + const notification = modifiedNotification.notificationItems[0] + expect(paymentAfter.interfaceInteractions[0].fields.notification).to.equal(JSON.stringify(notification)) + }) + + it('should udpate the pending CancelAuthorization transaction state to success state ' + + 'when receives a successful CANCEL_OR_REFUND notification with cancel action', async () => { + const { body: { results: [ paymentBefore ] } } = await ctpClient.fetch(ctpClient.builder.payments) + expect(paymentBefore.transactions).to.have.lengthOf(1) + expect(paymentBefore.transactions[0].type).to.equal('Authorization') + expect(paymentBefore.transactions[0].state).to.equal('Pending') + expect(paymentBefore.interfaceInteractions).to.have.lengthOf(0) + + const actions = [ + { + action: 'addTransaction', + transaction: { + type: 'CancelAuthorization', + amount: { + currencyCode: paymentBefore.transactions[0].amount.currencyCode, + centAmount: paymentBefore.transactions[0].amount.centAmount + }, + state: 'Pending' + } + } + ] + + const { body: updatedPayment } = + await ctpClient.update(ctpClient.builder.payments, paymentBefore.id, paymentBefore.version, actions) + + expect(updatedPayment.transactions).to.have.lengthOf(2) + expect(updatedPayment.transactions[0].type).to.equal('Authorization') + expect(updatedPayment.transactions[0].state).to.equal('Pending') + expect(updatedPayment.transactions[1].type).to.equal('CancelAuthorization') + expect(updatedPayment.transactions[1].state).to.equal('Pending') + + const modifiedNotification = cloneDeep(notifications) + modifiedNotification.notificationItems[0].NotificationRequestItem.eventCode = 'CANCEL_OR_REFUND' + modifiedNotification.notificationItems[0].NotificationRequestItem.additionalData = { + "modification.action": "cancel" + } + + // Simulating a notification from Adyen + const response = await fetch(`http://${localhostIp}:8000`, { + method: 'post', + body: JSON.stringify(modifiedNotification), + headers: { 'Content-Type': 'application/json' }, + }) + const { status } = response + const responseBody = await response.json() + + expect(responseBody).to.deep.equal({ notificationResponse: '[accepted]' }) + expect(status).to.equal(200) + + const { body: { results: [ paymentAfter ] } } = await ctpClient.fetch(ctpClient.builder.payments) + expect(paymentAfter.transactions).to.have.lengthOf(2) + expect(paymentAfter.transactions[0].type).to.equal('Authorization') + expect(paymentAfter.transactions[0].state).to.equal('Pending') + expect(paymentAfter.transactions[1].type).to.equal('CancelAuthorization') + expect(paymentAfter.transactions[1].state).to.equal('Success') + + expect(paymentAfter.interfaceInteractions).to.have.lengthOf(1) + const notification = modifiedNotification.notificationItems[0] + expect(paymentAfter.interfaceInteractions[0].fields.notification).to.equal(JSON.stringify(notification)) + }) }) diff --git a/notification/test/resources/payment-credit-card.json b/notification/test/resources/payment-credit-card.json index 62e2569c9..0c1df5394 100644 --- a/notification/test/resources/payment-credit-card.json +++ b/notification/test/resources/payment-credit-card.json @@ -27,19 +27,7 @@ } }, "paymentStatus": {}, - "transactions": [ - { - "id": "9ca92d05-ba63-47dc-8f83-95b08d539646", - "type": "Charge", - "amount": { - "type": "centPrecision", - "currencyCode": "EUR", - "centAmount": 495, - "fractionDigits": 2 - }, - "state": "Initial" - } - ], + "transactions": [], "interfaceInteractions": [ ] -} \ No newline at end of file +} diff --git a/notification/test/resources/payment-draft.json b/notification/test/resources/payment-draft.json index 76258a066..6b74ced5e 100644 --- a/notification/test/resources/payment-draft.json +++ b/notification/test/resources/payment-draft.json @@ -24,7 +24,7 @@ "fractionDigits": 2 }, "interactionId": "8835526703686369", - "state": "Initial" + "state": "Pending" } ] } diff --git a/notification/test/unit/notification.handler.spec.js b/notification/test/unit/notification.handler.spec.js index 2c25c0ca4..f405d50a7 100644 --- a/notification/test/unit/notification.handler.spec.js +++ b/notification/test/unit/notification.handler.spec.js @@ -20,17 +20,52 @@ const config = { describe('notification module', () => { afterEach(() => sandbox.restore()) - it('should update payment with a new InterfaceInteraction and payment status ' - + 'when current payment does not have the interfaceInteraction and the transaction' - + 'which are going to be set', async () => { + it(`given that ADYEN sends an "AUTHORISATION is successful" notification + when payment has a pending authorization transaction + then notification module should add notification to the interface interaction + and should update pending authorization state to the success`, async () => { + // prepare data + const notifications = [{ + NotificationRequestItem: { + amount: { + currency: "EUR", + value: 10100 + }, + eventCode: "AUTHORISATION", + eventDate: "2019-01-30T18:16:22+01:00", + merchantAccountCode: "CommercetoolsGmbHDE775", + merchantReference: "8313842560770001", + operations: [ + "CANCEL", + "CAPTURE", + "REFUND" + ], + paymentMethod: "visa", + pspReference: "test_AUTHORISATION_1", + success: "true" + } + }] + const payment = cloneDeep(paymentMock) + payment.transactions.push({ + id: "9ca92d05-ba63-47dc-8f83-95b08d539646", + type: "Authorization", + amount: { + type: "centPrecision", + currencyCode: "EUR", + centAmount: 495, + fractionDigits: 2 + }, + state: "Initial" + }) const ctpClient = ctpClientMock.get(config) sandbox.stub(ctpClient, 'fetch').callsFake(() => ({ body: { - results: [paymentMock] + results: [payment] } })) const ctpClientUpdateSpy = sandbox.spy(ctpClient, 'update') - await notificationHandler.processNotifications(notificationsMock, ctpClient) + // process + await notificationHandler.processNotifications(notifications, ctpClient) const expectedUpdateActions = [ { action: 'addInterfaceInteraction', @@ -40,22 +75,17 @@ describe('notification module', () => { }, fields: { status: 'AUTHORISATION', - notification: JSON.stringify(notificationsMock[0]) + notification: JSON.stringify(notifications[0]) } }, { - action: 'addTransaction', - transaction: { - type: 'Authorization', - amount: { - currencyCode: 'EUR', - centAmount: 10100 - }, - state: 'Success' - } + action: "changeTransactionState", + state: "Success", + transactionId: "9ca92d05-ba63-47dc-8f83-95b08d539646" } ] + //assert update actions // createdAt is set to the current date during the update action calculation // We can't know what is set there expect(ctpClientUpdateSpy.args[0][3][0].fields.createdAt).to.exist @@ -64,28 +94,52 @@ describe('notification module', () => { expect(actualUpdateActionsWithoutCreatedAt).to.deep.equal(expectedUpdateActions) }) - it('should update payment with a new InterfaceInteraction but not payment status ' - + 'when current payment does not have the interfaceInteraction which is going to be set' - + 'but has a transaction with the correct status', async () => { - const modifiedPaymentMock = cloneDeep(paymentMock) - modifiedPaymentMock.transactions.push({ - type: 'Authorization', + it(`given that ADYEN sends an "AUTHORISATION is not successful" notification + when payment has a pending authorization transaction + then notification module should add notification to the interface interaction + and should not update the pending transaction `, async () => { + // prepare data + const notifications = [{ + NotificationRequestItem: { + amount: { + currency: "EUR", + value: 10100 + }, + eventCode: "AUTHORISATION", + eventDate: "2019-01-30T18:16:22+01:00", + merchantAccountCode: "CommercetoolsGmbHDE775", + merchantReference: "8313842560770001", + operations: [ + "CANCEL", + "CAPTURE", + "REFUND" + ], + paymentMethod: "visa", + pspReference: "test_AUTHORISATION_1", + success: "false" + } + }] + const payment = cloneDeep(paymentMock) + payment.transactions.push({ + id: "9ca92d05-ba63-47dc-8f83-95b08d539646", + type: "Authorization", amount: { - type: 'centPrecision', - currencyCode: 'EUR', + type: "centPrecision", + currencyCode: "EUR", centAmount: 495, fractionDigits: 2 }, - state: 'Success' + state: "Pending" }) const ctpClient = ctpClientMock.get(config) sandbox.stub(ctpClient, 'fetch').callsFake(() => ({ body: { - results: [modifiedPaymentMock] + results: [payment] } })) const ctpClientUpdateSpy = sandbox.spy(ctpClient, 'update') - await notificationHandler.processNotifications(notificationsMock, ctpClient) + // process + await notificationHandler.processNotifications(notifications, ctpClient) const expectedUpdateActions = [ { action: 'addInterfaceInteraction', @@ -95,11 +149,12 @@ describe('notification module', () => { }, fields: { status: 'AUTHORISATION', - notification: JSON.stringify(notificationsMock[0]) + notification: JSON.stringify(notifications[0]) } } ] + //assert update actions // createdAt is set to the current date during the update action calculation // We can't know what is set there expect(ctpClientUpdateSpy.args[0][3][0].fields.createdAt).to.exist @@ -108,17 +163,50 @@ describe('notification module', () => { expect(actualUpdateActionsWithoutCreatedAt).to.deep.equal(expectedUpdateActions) }) - it('should update payment with a payment status but not new InterfaceInteraction ' - + 'when current payment does not have the transaction which is going to be set' - + 'but has the interfaceInteraction', async () => { - const modifiedPaymentMock = cloneDeep(paymentMock) - modifiedPaymentMock.interfaceInteractions.push({ + it(`given that ADYEN sends an "AUTHORISATION is successful" notification + when payment has a success authorization transaction + and has already has the same notification saved in interface interaction + then should not update interface interaction and transaction`, async () => { + // prepare data + const notifications = [{ + NotificationRequestItem: { + amount: { + currency: "EUR", + value: 10100 + }, + eventCode: "AUTHORISATION", + eventDate: "2019-01-30T18:16:22+01:00", + merchantAccountCode: "CommercetoolsGmbHDE775", + merchantReference: "8313842560770001", + operations: [ + "CANCEL", + "CAPTURE", + "REFUND" + ], + paymentMethod: "visa", + pspReference: "test_AUTHORISATION_1", + success: "true" + } + }] + const payment = cloneDeep(paymentMock) + payment.transactions.push({ + id: "9ca92d05-ba63-47dc-8f83-95b08d539646", + type: "Authorization", + amount: { + type: "centPrecision", + currencyCode: "EUR", + centAmount: 495, + fractionDigits: 2 + }, + state: "Success" + }) + payment.interfaceInteractions.push({ type: { typeId: 'type', id: '3fd15a04-b460-4a88-a911-0472c4c080b3' }, fields: { - notification: JSON.stringify(notificationsMock[0]), + notification: JSON.stringify(notifications[0]), status: 'SUCCESS', createdAt: '2019-02-05T12:29:36.028Z' } @@ -126,63 +214,254 @@ describe('notification module', () => { const ctpClient = ctpClientMock.get(config) sandbox.stub(ctpClient, 'fetch').callsFake(() => ({ body: { - results: [modifiedPaymentMock] + results: [payment] + } + })) + const ctpClientUpdateSpy = sandbox.spy(ctpClient, 'update') + // process + await notificationHandler.processNotifications(notifications, ctpClient) + // assert + expect(ctpClientUpdateSpy.args[0][3]).to.have.lengthOf(0) + }) + + it(`given that ADYEN sends a "CANCELLATION is successful" notification + when payment has a pending authorization transaction + then notification module should add notification to the interface interaction + and should add additional CancelAuthorization transaction`, async () => { + // prepare data + const notifications = [{ + NotificationRequestItem: { + amount: { + currency: "EUR", + value: 10100 + }, + eventCode: "CANCELLATION", + eventDate: "2019-01-30T18:16:22+01:00", + merchantAccountCode: "CommercetoolsGmbHDE775", + merchantReference: "8313842560770001", + operations: [ + "CANCEL", + "CAPTURE", + "REFUND" + ], + paymentMethod: "visa", + pspReference: "test_AUTHORISATION_1", + success: "true" + } + }] + const payment = cloneDeep(paymentMock) + payment.transactions.push({ + id: "9ca92d05-ba63-47dc-8f83-95b08d539646", + type: "Authorization", + amount: { + type: "centPrecision", + currencyCode: "EUR", + centAmount: 495, + fractionDigits: 2 + }, + state: "Pending" + }) + const ctpClient = ctpClientMock.get(config) + sandbox.stub(ctpClient, 'fetch').callsFake(() => ({ + body: { + results: [payment] } })) const ctpClientUpdateSpy = sandbox.spy(ctpClient, 'update') - await notificationHandler.processNotifications(notificationsMock, ctpClient) + // process + await notificationHandler.processNotifications(notifications, ctpClient) const expectedUpdateActions = [ { - action: 'addTransaction', + action: 'addInterfaceInteraction', + type: { + key: 'ctp-adyen-integration-interaction-notification', + typeId: 'type' + }, + fields: { + status: 'CANCELLATION', + notification: JSON.stringify(notifications[0]) + } + }, + { + action: "addTransaction", transaction: { - type: 'Authorization', amount: { - currencyCode: 'EUR', - centAmount: 10100 + centAmount: 10100, + currencyCode: "EUR" }, - state: 'Success' + state: "Success", + type: "CancelAuthorization" } } ] - expect(ctpClientUpdateSpy.args[0][3]).to.deep.equal(expectedUpdateActions) + //assert update actions + // createdAt is set to the current date during the update action calculation + // We can't know what is set there + expect(ctpClientUpdateSpy.args[0][3][0].fields.createdAt).to.exist + const actualUpdateActionsWithoutCreatedAt = ctpClientUpdateSpy.args[0][3] + delete actualUpdateActionsWithoutCreatedAt[0].fields.createdAt + expect(actualUpdateActionsWithoutCreatedAt).to.deep.equal(expectedUpdateActions) }) - it('should update transaction with a new state', async () => { - const modifiedPaymentMock = cloneDeep(paymentMock) - const notificationsMockClone = cloneDeep(notificationsMock) - notificationsMockClone[0].NotificationRequestItem.eventCode = 'CAPTURE' - notificationsMockClone[0].NotificationRequestItem.success = 'false' - modifiedPaymentMock.interfaceInteractions.push({ - type: { - typeId: 'type', - id: '3fd15a04-b460-4a88-a911-0472c4c080b3' - }, - fields: { - createdAt: '2019-02-05T12:29:36.028Z', - notification: JSON.stringify(notificationsMockClone[0]), - status: 'SUCCESS' + it(`given that ADYEN sends a "CAPTURE is successful" notification + when payment has a successful authorization transaction + then notification module should add notification to the interface interaction + and should add a success Charge transaction`, async () => { + // prepare data + const notifications = [{ + NotificationRequestItem: { + amount: { + currency: "EUR", + value: 10100 + }, + eventCode: "CAPTURE", + eventDate: "2019-01-30T18:16:22+01:00", + merchantAccountCode: "CommercetoolsGmbHDE775", + merchantReference: "8313842560770001", + operations: [ + "CANCEL", + "CAPTURE", + "REFUND" + ], + paymentMethod: "visa", + pspReference: "test_AUTHORISATION_1", + success: "true" } + }] + const payment = cloneDeep(paymentMock) + payment.transactions.push({ + id: "9ca92d05-ba63-47dc-8f83-95b08d539646", + type: "Authorization", + amount: { + type: "centPrecision", + currencyCode: "EUR", + centAmount: 495, + fractionDigits: 2 + }, + state: "Success" }) const ctpClient = ctpClientMock.get(config) sandbox.stub(ctpClient, 'fetch').callsFake(() => ({ body: { - results: [modifiedPaymentMock] + results: [payment] } })) const ctpClientUpdateSpy = sandbox.spy(ctpClient, 'update') + // process + await notificationHandler.processNotifications(notifications, ctpClient) + const expectedUpdateActions = [ + { + action: 'addInterfaceInteraction', + type: { + key: 'ctp-adyen-integration-interaction-notification', + typeId: 'type' + }, + fields: { + status: 'CAPTURE', + notification: JSON.stringify(notifications[0]) + } + }, + { + action: "addTransaction", + transaction: { + amount: { + centAmount: 10100, + currencyCode: "EUR" + }, + state: "Success", + type: "Charge" + } + } + ] + //assert update actions + // createdAt is set to the current date during the update action calculation + // We can't know what is set there + expect(ctpClientUpdateSpy.args[0][3][0].fields.createdAt).to.exist + const actualUpdateActionsWithoutCreatedAt = ctpClientUpdateSpy.args[0][3] + delete actualUpdateActionsWithoutCreatedAt[0].fields.createdAt + expect(actualUpdateActionsWithoutCreatedAt).to.deep.equal(expectedUpdateActions) + }) - await notificationHandler.processNotifications(notificationsMockClone, ctpClient) + it(`given that ADYEN sends a "CAPTURE_FAILED notification" + when payment has a successful authorization transaction + then notification module should add notification to the interface interaction + and should add a failed Charge transaction`, async () => { + // prepare data + const notifications = [{ + NotificationRequestItem: { + amount: { + currency: "EUR", + value: 10100 + }, + eventCode: "CAPTURE_FAILED", + eventDate: "2019-01-30T18:16:22+01:00", + merchantAccountCode: "CommercetoolsGmbHDE775", + merchantReference: "8313842560770001", + operations: [ + "CANCEL", + "CAPTURE", + "REFUND" + ], + paymentMethod: "visa", + pspReference: "test_AUTHORISATION_1", + success: "true" + } + }] + const payment = cloneDeep(paymentMock) + payment.transactions.push({ + id: "9ca92d05-ba63-47dc-8f83-95b08d539646", + type: "Authorization", + amount: { + type: "centPrecision", + currencyCode: "EUR", + centAmount: 495, + fractionDigits: 2 + }, + state: "Success" + }) + const ctpClient = ctpClientMock.get(config) + sandbox.stub(ctpClient, 'fetch').callsFake(() => ({ + body: { + results: [payment] + } + })) + const ctpClientUpdateSpy = sandbox.spy(ctpClient, 'update') + // process + await notificationHandler.processNotifications(notifications, ctpClient) const expectedUpdateActions = [ { - action: 'changeTransactionState', - state: 'Failure', - transactionId: '9ca92d05-ba63-47dc-8f83-95b08d539646' + action: 'addInterfaceInteraction', + type: { + key: 'ctp-adyen-integration-interaction-notification', + typeId: 'type' + }, + fields: { + status: 'CAPTURE_FAILED', + notification: JSON.stringify(notifications[0]) + } + }, + { + action: "addTransaction", + transaction: { + amount: { + centAmount: 10100, + currencyCode: "EUR" + }, + state: "Failure", + type: "Charge" + } } ] - expect(ctpClientUpdateSpy.args[0][3]).to.deep.equal(expectedUpdateActions) + //assert update actions + // createdAt is set to the current date during the update action calculation + // We can't know what is set there + expect(ctpClientUpdateSpy.args[0][3][0].fields.createdAt).to.exist + const actualUpdateActionsWithoutCreatedAt = ctpClientUpdateSpy.args[0][3] + delete actualUpdateActionsWithoutCreatedAt[0].fields.createdAt + expect(actualUpdateActionsWithoutCreatedAt).to.deep.equal(expectedUpdateActions) }) it('should repeat on concurrent modification errors ', async () => {