From 20c8dc23a1ac35458461aea2cb5a616dbabd38bd Mon Sep 17 00:00:00 2001 From: olivermrbl Date: Thu, 6 Aug 2020 12:12:31 +0200 Subject: [PATCH] Adds Adyen payment provider --- package.json | 3 + packages/medusa-core-utils/src/validator.js | 2 +- packages/medusa-payment-adyen/.babelrc | 13 ++ packages/medusa-payment-adyen/.eslintrc | 9 + packages/medusa-payment-adyen/.gitignore | 16 ++ packages/medusa-payment-adyen/.npmignore | 9 + packages/medusa-payment-adyen/.prettierrc | 7 + packages/medusa-payment-adyen/index.js | 1 + packages/medusa-payment-adyen/package.json | 47 +++++ .../medusa-payment-adyen/src/api/index.js | 10 + .../src/api/middlewares/await-middleware.js | 1 + .../src/api/middlewares/index.js | 5 + .../src/api/routes/hooks/capture-hook.js | 22 ++ .../src/api/routes/hooks/index.js | 30 +++ .../src/api/routes/store/authorize-payment.js | 53 +++++ .../api/routes/store/check-payment-status.js | 32 +++ .../src/api/routes/store/index.js | 42 ++++ .../routes/store/retrieve-payment-methods.js | 38 ++++ .../src/services/adyen.js | 199 ++++++++++++++++++ .../src/services/applepay.js | 74 +++++++ .../medusa-payment-adyen/src/services/card.js | 74 +++++++ .../src/services/googlepay.js | 74 +++++++ .../src/services/ideal.js | 74 +++++++ .../src/services/mobilepay.js | 74 +++++++ .../src/services/paypal.js | 74 +++++++ packages/medusa/src/loaders/plugins.js | 6 +- packages/medusa/src/services/cart.js | 17 +- packages/medusa/src/services/order.js | 37 +++- packages/medusa/src/subscribers/order.js | 4 - yarn.lock | 16 +- 30 files changed, 1047 insertions(+), 16 deletions(-) create mode 100644 packages/medusa-payment-adyen/.babelrc create mode 100644 packages/medusa-payment-adyen/.eslintrc create mode 100644 packages/medusa-payment-adyen/.gitignore create mode 100644 packages/medusa-payment-adyen/.npmignore create mode 100644 packages/medusa-payment-adyen/.prettierrc create mode 100644 packages/medusa-payment-adyen/index.js create mode 100644 packages/medusa-payment-adyen/package.json create mode 100644 packages/medusa-payment-adyen/src/api/index.js create mode 100644 packages/medusa-payment-adyen/src/api/middlewares/await-middleware.js create mode 100644 packages/medusa-payment-adyen/src/api/middlewares/index.js create mode 100644 packages/medusa-payment-adyen/src/api/routes/hooks/capture-hook.js create mode 100644 packages/medusa-payment-adyen/src/api/routes/hooks/index.js create mode 100644 packages/medusa-payment-adyen/src/api/routes/store/authorize-payment.js create mode 100644 packages/medusa-payment-adyen/src/api/routes/store/check-payment-status.js create mode 100644 packages/medusa-payment-adyen/src/api/routes/store/index.js create mode 100644 packages/medusa-payment-adyen/src/api/routes/store/retrieve-payment-methods.js create mode 100644 packages/medusa-payment-adyen/src/services/adyen.js create mode 100644 packages/medusa-payment-adyen/src/services/applepay.js create mode 100644 packages/medusa-payment-adyen/src/services/card.js create mode 100644 packages/medusa-payment-adyen/src/services/googlepay.js create mode 100644 packages/medusa-payment-adyen/src/services/ideal.js create mode 100644 packages/medusa-payment-adyen/src/services/mobilepay.js create mode 100644 packages/medusa-payment-adyen/src/services/paypal.js diff --git a/package.json b/package.json index 93d0635016..cba94f3426 100644 --- a/package.json +++ b/package.json @@ -3,5 +3,8 @@ "private": true, "devDependencies": { "lerna": "^3.19.0" + }, + "dependencies": { + "axios": "^0.19.2" } } diff --git a/packages/medusa-core-utils/src/validator.js b/packages/medusa-core-utils/src/validator.js index cad2195b18..e442001431 100644 --- a/packages/medusa-core-utils/src/validator.js +++ b/packages/medusa-core-utils/src/validator.js @@ -12,7 +12,7 @@ Joi.address = () => { country_code: Joi.string().required(), province: Joi.string().allow(""), postal_code: Joi.string().required(), - phone: Joi.string(), + phone: Joi.string().optional(), metadata: Joi.object(), }) } diff --git a/packages/medusa-payment-adyen/.babelrc b/packages/medusa-payment-adyen/.babelrc new file mode 100644 index 0000000000..4d2dfe8f09 --- /dev/null +++ b/packages/medusa-payment-adyen/.babelrc @@ -0,0 +1,13 @@ +{ + "plugins": [ + "@babel/plugin-proposal-class-properties", + "@babel/plugin-transform-instanceof", + "@babel/plugin-transform-classes" + ], + "presets": ["@babel/preset-env"], + "env": { + "test": { + "plugins": ["@babel/plugin-transform-runtime"] + } + } +} diff --git a/packages/medusa-payment-adyen/.eslintrc b/packages/medusa-payment-adyen/.eslintrc new file mode 100644 index 0000000000..2a889697f0 --- /dev/null +++ b/packages/medusa-payment-adyen/.eslintrc @@ -0,0 +1,9 @@ +{ + "plugins": ["prettier"], + "extends": ["prettier"], + "rules": { + "prettier/prettier": "error", + "semi": "error", + "no-unused-expressions": "true" + } +} diff --git a/packages/medusa-payment-adyen/.gitignore b/packages/medusa-payment-adyen/.gitignore new file mode 100644 index 0000000000..2ca7f03256 --- /dev/null +++ b/packages/medusa-payment-adyen/.gitignore @@ -0,0 +1,16 @@ +/lib +node_modules +.DS_store +.env* +/*.js +!index.js +yarn.lock + +/dist + +/api +/services +/models +/subscribers +/__mocks__ + diff --git a/packages/medusa-payment-adyen/.npmignore b/packages/medusa-payment-adyen/.npmignore new file mode 100644 index 0000000000..486581be18 --- /dev/null +++ b/packages/medusa-payment-adyen/.npmignore @@ -0,0 +1,9 @@ +/lib +node_modules +.DS_store +.env* +/*.js +!index.js +yarn.lock + + diff --git a/packages/medusa-payment-adyen/.prettierrc b/packages/medusa-payment-adyen/.prettierrc new file mode 100644 index 0000000000..70175ce150 --- /dev/null +++ b/packages/medusa-payment-adyen/.prettierrc @@ -0,0 +1,7 @@ +{ + "endOfLine": "lf", + "semi": false, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "es5" +} \ No newline at end of file diff --git a/packages/medusa-payment-adyen/index.js b/packages/medusa-payment-adyen/index.js new file mode 100644 index 0000000000..172f1ae6a4 --- /dev/null +++ b/packages/medusa-payment-adyen/index.js @@ -0,0 +1 @@ +// noop diff --git a/packages/medusa-payment-adyen/package.json b/packages/medusa-payment-adyen/package.json new file mode 100644 index 0000000000..04b0a0bcae --- /dev/null +++ b/packages/medusa-payment-adyen/package.json @@ -0,0 +1,47 @@ +{ + "name": "medusa-payment-adyen", + "version": "0.3.0", + "description": "Adyen Payment provider for Medusa Commerce", + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/medusa-payment-adyen" + }, + "author": "Oliver Juhl", + "license": "AGPL-3.0-or-later", + "devDependencies": { + "@babel/cli": "^7.7.5", + "@babel/core": "^7.7.5", + "@babel/node": "^7.7.4", + "@babel/plugin-proposal-class-properties": "^7.7.4", + "@babel/plugin-transform-instanceof": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.7.6", + "@babel/preset-env": "^7.7.5", + "@babel/register": "^7.7.4", + "@babel/runtime": "^7.9.6", + "client-sessions": "^0.8.0", + "cross-env": "^5.2.1", + "eslint": "^6.8.0", + "jest": "^25.5.2", + "medusa-test-utils": "^0.3.0" + }, + "scripts": { + "build": "babel src -d .", + "prepare": "cross-env NODE_ENV=production npm run build", + "watch": "babel -w src --out-dir . --ignore **/__tests__", + "test": "jest" + }, + "dependencies": { + "@adyen/api-library": "^5.0.1", + "@babel/plugin-transform-classes": "^7.9.5", + "axios": "^0.19.2", + "body-parser": "^1.19.0", + "cors": "^2.8.5", + "express": "^4.17.1", + "medusa-core-utils": "^0.1.27", + "medusa-interfaces": "^0.3.0", + "medusa-test-utils": "^0.3.0" + }, + "gitHead": "35e0930650d5f4aedf2610749cd131ae8b7e17cc" +} diff --git a/packages/medusa-payment-adyen/src/api/index.js b/packages/medusa-payment-adyen/src/api/index.js new file mode 100644 index 0000000000..c613ede408 --- /dev/null +++ b/packages/medusa-payment-adyen/src/api/index.js @@ -0,0 +1,10 @@ +import { Router } from "express" +import store from "./routes/store" +import hooks from "./routes/hooks" + +export default (rootDirectory) => { + const app = Router() + store(app, rootDirectory) + hooks(app, rootDirectory) + return app +} diff --git a/packages/medusa-payment-adyen/src/api/middlewares/await-middleware.js b/packages/medusa-payment-adyen/src/api/middlewares/await-middleware.js new file mode 100644 index 0000000000..1c3692b377 --- /dev/null +++ b/packages/medusa-payment-adyen/src/api/middlewares/await-middleware.js @@ -0,0 +1 @@ +export default (fn) => (...args) => fn(...args).catch(args[2]) diff --git a/packages/medusa-payment-adyen/src/api/middlewares/index.js b/packages/medusa-payment-adyen/src/api/middlewares/index.js new file mode 100644 index 0000000000..c784e319a9 --- /dev/null +++ b/packages/medusa-payment-adyen/src/api/middlewares/index.js @@ -0,0 +1,5 @@ +import { default as wrap } from "./await-middleware" + +export default { + wrap, +} diff --git a/packages/medusa-payment-adyen/src/api/routes/hooks/capture-hook.js b/packages/medusa-payment-adyen/src/api/routes/hooks/capture-hook.js new file mode 100644 index 0000000000..3d4e2cfc04 --- /dev/null +++ b/packages/medusa-payment-adyen/src/api/routes/hooks/capture-hook.js @@ -0,0 +1,22 @@ +export default async (req, res) => { + const adyenService = req.scope.resolve("adyenService") + + const notification = req.body + const event = notification.notificationItems[0].NotificationRequestItem + + const valid = adyenService.validateNotification(event) + + if (!valid) { + res.status(401).send(`Unauthorized webhook event`) + return + } + + switch (event.success) { + case "false": + res.status(400).send(event.reason) + return + default: + res.status(200).send("[accepted]") + return + } +} diff --git a/packages/medusa-payment-adyen/src/api/routes/hooks/index.js b/packages/medusa-payment-adyen/src/api/routes/hooks/index.js new file mode 100644 index 0000000000..fff3b71eb9 --- /dev/null +++ b/packages/medusa-payment-adyen/src/api/routes/hooks/index.js @@ -0,0 +1,30 @@ +import { Router } from "express" +import cors from "cors" +import bodyParser from "body-parser" +import middlewares from "../../middlewares" +import { getConfigFile } from "medusa-core-utils" + +const route = Router() + +export default (app, rootDirectory) => { + const { configModule } = getConfigFile(rootDirectory, `medusa-config`) + const config = (configModule && configModule.projectConfig) || {} + + const storeCors = config.store_cors || "" + route.use( + cors({ + origin: storeCors.split(","), + credentials: true, + }) + ) + + app.use("/adyen-hooks", route) + + route.post( + "/capture", + bodyParser.json(), + middlewares.wrap(require("./capture-hook").default) + ) + + return app +} diff --git a/packages/medusa-payment-adyen/src/api/routes/store/authorize-payment.js b/packages/medusa-payment-adyen/src/api/routes/store/authorize-payment.js new file mode 100644 index 0000000000..1d2397334a --- /dev/null +++ b/packages/medusa-payment-adyen/src/api/routes/store/authorize-payment.js @@ -0,0 +1,53 @@ +import { Validator, MedusaError } from "medusa-core-utils" + +export default async (req, res) => { + const schema = Validator.object().keys({ + cart_id: Validator.string().required(), + payment_method: Validator.object().required(), + provider_id: Validator.string().required(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const cartService = req.scope.resolve("cartService") + const paymentProvider = req.scope.resolve(`pp_${value.provider_id}`) + + const cart = await cartService.retrieve(value.cart_id) + + const { data } = await paymentProvider.authorizePayment( + cart, + value.payment_method + ) + + const transactionReference = data.pspReference + + let newPaymentSession = cart.payment_sessions.find( + (ps) => ps.provider_id === value.provider_id + ) + + newPaymentSession = { + ...newPaymentSession, + data, + } + + await cartService.setMetadata( + cart._id, + "adyen_transaction_ref", + transactionReference + ) + + await cartService.updatePaymentSession( + cart._id, + value.provider_id, + newPaymentSession + ) + + res.status(200).json({ data }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa-payment-adyen/src/api/routes/store/check-payment-status.js b/packages/medusa-payment-adyen/src/api/routes/store/check-payment-status.js new file mode 100644 index 0000000000..80ea95cf96 --- /dev/null +++ b/packages/medusa-payment-adyen/src/api/routes/store/check-payment-status.js @@ -0,0 +1,32 @@ +import { Validator, MedusaError } from "medusa-core-utils" + +export default async (req, res) => { + const schema = Validator.object().keys({ + payment_provider: Validator.string().required(), + payload: Validator.object().required(), + payment_data: Validator.object().required(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const adyenService = req.scope.resolve("adyenService") + const paymentProviderService = req.scope.resolve( + `${value.payment_provider}AdyenProviderService` + ) + + const adyenResultCode = await adyenService.checkPaymentResult( + value.payment_data, + value.payload + ) + + const status = paymentProviderService.getStatus(adyenResultCode) + + res.status(200).json({ status }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa-payment-adyen/src/api/routes/store/index.js b/packages/medusa-payment-adyen/src/api/routes/store/index.js new file mode 100644 index 0000000000..3aff84f40d --- /dev/null +++ b/packages/medusa-payment-adyen/src/api/routes/store/index.js @@ -0,0 +1,42 @@ +import { Router } from "express" +import cors from "cors" +import bodyParser from "body-parser" +import middlewares from "../../middlewares" +import { getConfigFile } from "medusa-core-utils" + +const route = Router() + +export default (app, rootDirectory) => { + const { configModule } = getConfigFile(rootDirectory, `medusa-config`) + const config = (configModule && configModule.projectConfig) || {} + + const storeCors = config.store_cors || "" + route.use( + cors({ + origin: storeCors.split(","), + credentials: true, + }) + ) + + app.use("/adyen", route) + + route.post( + "/payment-methods", + bodyParser.json(), + middlewares.wrap(require("./retrieve-payment-methods").default) + ) + + route.post( + "/authorize", + bodyParser.json(), + middlewares.wrap(require("./authorize-payment").default) + ) + + route.post( + "/payment-status", + bodyParser.json(), + middlewares.wrap(require("./check-payment-status").default) + ) + + return app +} diff --git a/packages/medusa-payment-adyen/src/api/routes/store/retrieve-payment-methods.js b/packages/medusa-payment-adyen/src/api/routes/store/retrieve-payment-methods.js new file mode 100644 index 0000000000..5fefd42346 --- /dev/null +++ b/packages/medusa-payment-adyen/src/api/routes/store/retrieve-payment-methods.js @@ -0,0 +1,38 @@ +import { Validator, MedusaError } from "medusa-core-utils" + +export default async (req, res) => { + const schema = Validator.object().keys({ + cart_id: Validator.string().required(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const adyenService = req.scope.resolve("adyenService") + const cartService = req.scope.resolve("cartService") + const regionService = req.scope.resolve("regionService") + const totalsService = req.scope.resolve("totalsService") + + const cart = await cartService.retrieve(value.cart_id) + const region = await regionService.retrieve(cart.region_id) + const total = await totalsService.getTotal(cart) + + const allowedMethods = cart.payment_sessions.map( + (ps) => ps.provider_id.split("Adyen")[0] + ) + + const { data } = await adyenService.retrievePaymentMethods( + cart, + allowedMethods, + total, + region.currency_code + ) + + res.status(200).json({ paymentMethods: data }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa-payment-adyen/src/services/adyen.js b/packages/medusa-payment-adyen/src/services/adyen.js new file mode 100644 index 0000000000..6dce43b702 --- /dev/null +++ b/packages/medusa-payment-adyen/src/services/adyen.js @@ -0,0 +1,199 @@ +import axios from "axios" +import _ from "lodash" +import { hmacValidator } from "@adyen/api-library" +import { BaseService } from "medusa-interfaces" + +class AdyenService extends BaseService { + constructor({ regionService, cartService, totalsService }, options) { + super() + + this.regionService_ = regionService + + this.cartService_ = cartService + + this.totalsService_ = totalsService + + this.options_ = options + + this.adyenCheckoutApi = axios.create({ + baseURL: "https://checkout-test.adyen.com/v52", + headers: { + "Content-Type": "application/json", + "x-API-key": this.options_.api_key, + }, + }) + + this.adyenPaymentApi = axios.create({ + baseURL: "https://pal-test.adyen.com/pal/servlet/Payment/v52", + headers: { + "Content-Type": "application/json", + "x-API-key": this.options_.api_key, + }, + }) + } + + getOptions() { + return this.options_ + } + + validateNotification(event) { + const validator = new hmacValidator() + const validated = validator.validateHMAC( + event, + this.options_.notification_hmac + ) + return validated + } + + /** + * Retrieve payment methods from Ayden using country as filter. + * @param {string} countryCode - country code of cart + * @param {string} shopperLocale - locale used on website + * @returns {string} the status of the payment + */ + async retrievePaymentMethods(cart, allowedMethods, total, currency) { + let request = { + allowedPaymentMethods: allowedMethods, + amount: { + value: total * 100, + currency: currency, + }, + merchantAccount: this.options_.merchant_account, + channel: this.options_.channel, + } + + if (cart.customer_id) { + request.shopperReference = cart.customer_id + } + + try { + return await this.adyenCheckoutApi.post("/paymentMethods", request) + } catch (error) { + throw error + } + } + + /** + * Status for Adyen payment. + * @param {Object} paymentData - payment method data from cart + * @returns {string} the status of the payment + */ + async getStatus(_) { + let status = "initial" + return status + } + + /** + * Creates Adyen payment object. + * @param {any} _ - placeholder object + * @returns {Object} empty payment data + */ + async createPayment(_) { + return {} + } + + async retrievePayment(data) { + return data + } + + /** + * Creates and authorizes an Ayden payment + * @returns {Object} payment data result + */ + async authorizePayment(cart, paymentMethod) { + const region = await this.regionService_.retrieve(cart.region_id) + const total = await this.totalsService_.getTotal(cart) + + let request = { + amount: { + currency: region.currency_code, + value: total * 100, + }, + shopperReference: cart.customer_id, + paymentMethod, + reference: cart._id, + merchantAccount: this.options_.merchant_account, + returnUrl: this.options_.return_url, + metadata: { + cart_id: cart._id, + }, + } + + if (paymentMethod.storedPaymentMethodId) { + request.shopperInteraction = "Ecommerce" + request.recurringProcessingModel = "CardOnFile" + } + + return await this.adyenCheckoutApi.post("/payments", request) + } + + async checkPaymentResult(paymentData, payload) { + const request = { + paymentData, + details: { + payload, + }, + } + return this.adyenCheckoutApi.post("/payments/details", request) + } + + /** + * Captures an Ayden payment + * @param {Object} data - payment data to capture + * @returns {Object} payment data result of capture + */ + async capturePayment(data) { + const { pspReference, amount } = data + + try { + return this.adyenPaymentApi.post("/capture", { + originalReference: pspReference, + modificationAmount: amount, + merchantAccount: this.options_.merchant_account, + }) + } catch (error) { + console.log(error) + throw error + } + } + + /** + * Refunds an Ayden payment + * @param {Object} paymentData - payment data to refund + * @param {number} amountToRefund - amount to refund + * @returns {Object} payment data result of refund + */ + async refundPayment(data) { + const { pspReference, amount } = data + + try { + return this.adyenPaymentApi.post("/capture", { + originalReference: pspReference, + merchantAccount: this.options_.merchant_account, + modificationAmount: amount, + }) + } catch (error) { + throw error + } + } + + /** + * Cancels an Ayden payment + * @param {Object} paymentData - payment data to cancel + * @returns {Object} payment data result of cancel + */ + async cancelPayment(paymentData) { + const { pspReference } = paymentData + + try { + return this.adyenPaymentApi.post("/capture", { + originalReference: pspReference, + merchantAccount: this.options_.merchant_account, + }) + } catch (error) { + throw error + } + } +} + +export default AdyenService diff --git a/packages/medusa-payment-adyen/src/services/applepay.js b/packages/medusa-payment-adyen/src/services/applepay.js new file mode 100644 index 0000000000..556ad3252d --- /dev/null +++ b/packages/medusa-payment-adyen/src/services/applepay.js @@ -0,0 +1,74 @@ +import _ from "lodash" +import { PaymentService } from "medusa-interfaces" + +class ApplePayAdyenService extends PaymentService { + static identifier = "applepayAdyen" + + constructor({ adyenService }) { + super() + + this.adyenService_ = adyenService + } + + /** + * Status for Adyen payment. + * @param {Object} paymentData - payment method data from cart + * @returns {string} the status of the payment + */ + async getStatus(paymentData) { + const { resultCode } = paymentData + let status = "initial" + + if (resultCode === "Authorised") { + status = "authorized" + } + + return status + } + + async createPayment(_) { + return {} + } + + async authorizePayment(cart, paymentMethod) { + return this.adyenService_.authorizePayment(cart, paymentMethod) + } + + async retrievePayment(data) { + return this.adyenService_.retrievePayment(data) + } + + async updatePayment(data, _) { + return this.adyenService_.updatePayment(data) + } + + async deletePayment(data) { + return this.adyenService_.deletePayment(data) + } + + async capturePayment(data) { + try { + return this.adyenService_.capturePayment(data) + } catch (error) { + throw error + } + } + + async refundPayment(data) { + try { + return this.adyenService_.refundPayment(data) + } catch (error) { + throw error + } + } + + async cancelPayment(data) { + try { + return this.adyenService_.cancelPayment(data) + } catch (error) { + throw error + } + } +} + +export default ApplePayAdyenService diff --git a/packages/medusa-payment-adyen/src/services/card.js b/packages/medusa-payment-adyen/src/services/card.js new file mode 100644 index 0000000000..3c6508d528 --- /dev/null +++ b/packages/medusa-payment-adyen/src/services/card.js @@ -0,0 +1,74 @@ +import _ from "lodash" +import { PaymentService } from "medusa-interfaces" + +class CardAdyenService extends PaymentService { + static identifier = "schemeAdyen" + + constructor({ adyenService }) { + super() + + this.adyenService_ = adyenService + } + + /** + * Status for Adyen payment. + * @param {Object} paymentData - payment method data from cart + * @returns {string} the status of the payment + */ + async getStatus(paymentData) { + const { resultCode } = paymentData + let status = "initial" + + if (resultCode === "Authorised") { + status = "authorized" + } + + return status + } + + async createPayment(_) { + return {} + } + + async authorizePayment(cart, paymentMethod) { + return this.adyenService_.authorizePayment(cart, paymentMethod) + } + + async retrievePayment(data) { + return this.adyenService_.retrievePayment(data) + } + + async updatePayment(data, _) { + return this.adyenService_.updatePayment(data) + } + + async deletePayment(data) { + return this.adyenService_.deletePayment(data) + } + + async capturePayment(data) { + try { + return this.adyenService_.capturePayment(data) + } catch (error) { + throw error + } + } + + async refundPayment(data) { + try { + return this.adyenService_.refundPayment(data) + } catch (error) { + throw error + } + } + + async cancelPayment(data) { + try { + return this.adyenService_.cancelPayment(data) + } catch (error) { + throw error + } + } +} + +export default CardAdyenService diff --git a/packages/medusa-payment-adyen/src/services/googlepay.js b/packages/medusa-payment-adyen/src/services/googlepay.js new file mode 100644 index 0000000000..30cbc08b88 --- /dev/null +++ b/packages/medusa-payment-adyen/src/services/googlepay.js @@ -0,0 +1,74 @@ +import _ from "lodash" +import { PaymentService } from "medusa-interfaces" + +class GooglePayAdyenService extends PaymentService { + static identifier = "googlepayAdyen" + + constructor({ adyenService }) { + super() + + this.adyenService_ = adyenService + } + + /** + * Status for Adyen payment. + * @param {Object} paymentData - payment method data from cart + * @returns {string} the status of the payment + */ + async getStatus(paymentData) { + const { resultCode } = paymentData + let status = "initial" + + if (resultCode === "Authorised") { + status = "authorized" + } + + return status + } + + async createPayment(_) { + return {} + } + + async authorizePayment(cart, paymentMethod) { + return this.adyenService_.authorizePayment(cart, paymentMethod) + } + + async retrievePayment(data) { + return this.adyenService_.retrievePayment(data) + } + + async updatePayment(data, _) { + return this.adyenService_.updatePayment(data) + } + + async deletePayment(data) { + return this.adyenService_.deletePayment(data) + } + + async capturePayment(data) { + try { + return this.adyenService_.capturePayment(data) + } catch (error) { + throw error + } + } + + async refundPayment(data) { + try { + return this.adyenService_.refundPayment(data) + } catch (error) { + throw error + } + } + + async cancelPayment(data) { + try { + return this.adyenService_.cancelPayment(data) + } catch (error) { + throw error + } + } +} + +export default GooglePayAdyenService diff --git a/packages/medusa-payment-adyen/src/services/ideal.js b/packages/medusa-payment-adyen/src/services/ideal.js new file mode 100644 index 0000000000..b2904cd80b --- /dev/null +++ b/packages/medusa-payment-adyen/src/services/ideal.js @@ -0,0 +1,74 @@ +import _ from "lodash" +import { PaymentService } from "medusa-interfaces" + +class IdealAdyenService extends PaymentService { + static identifier = "idealAdyen" + + constructor({ adyenService }) { + super() + + this.adyenService_ = adyenService + } + + /** + * Status for Adyen payment. + * @param {Object} paymentData - payment method data from cart + * @returns {string} the status of the payment + */ + async getStatus(paymentData) { + const { resultCode } = paymentData + let status = "initial" + + if (resultCode === "Authorised") { + status = "authorized" + } + + return status + } + + async createPayment(_) { + return {} + } + + async authorizePayment(cart, paymentMethod) { + return this.adyenService_.authorizePayment(cart, paymentMethod) + } + + async retrievePayment(data) { + return this.adyenService_.retrievePayment(data) + } + + async updatePayment(data, _) { + return this.adyenService_.updatePayment(data) + } + + async deletePayment(data) { + return this.adyenService_.deletePayment(data) + } + + async capturePayment(data) { + try { + return this.adyenService_.capturePayment(data) + } catch (error) { + throw error + } + } + + async refundPayment(data) { + try { + return this.adyenService_.refundPayment(data) + } catch (error) { + throw error + } + } + + async cancelPayment(data) { + try { + return this.adyenService_.cancelPayment(data) + } catch (error) { + throw error + } + } +} + +export default IdealAdyenService diff --git a/packages/medusa-payment-adyen/src/services/mobilepay.js b/packages/medusa-payment-adyen/src/services/mobilepay.js new file mode 100644 index 0000000000..3eefd15b50 --- /dev/null +++ b/packages/medusa-payment-adyen/src/services/mobilepay.js @@ -0,0 +1,74 @@ +import _ from "lodash" +import { PaymentService } from "medusa-interfaces" + +class MobilePayAdyenService extends PaymentService { + static identifier = "mobilepayAdyen" + + constructor({ adyenService }) { + super() + + this.adyenService_ = adyenService + } + + /** + * Status for Adyen payment. + * @param {Object} paymentData - payment method data from cart + * @returns {string} the status of the payment + */ + async getStatus(paymentData) { + const { resultCode } = paymentData + let status = "initial" + + if (resultCode === "Authorised") { + status = "authorized" + } + + return status + } + + async createPayment(_) { + return {} + } + + async authorizePayment(cart, paymentMethod) { + return this.adyenService_.authorizePayment(cart, paymentMethod) + } + + async retrievePayment(data) { + return this.adyenService_.retrievePayment(data) + } + + async updatePayment(data, _) { + return this.adyenService_.updatePayment(data) + } + + async deletePayment(data) { + return this.adyenService_.deletePayment(data) + } + + async capturePayment(data) { + try { + return this.adyenService_.capturePayment(data) + } catch (error) { + throw error + } + } + + async refundPayment(data) { + try { + return this.adyenService_.refundPayment(data) + } catch (error) { + throw error + } + } + + async cancelPayment(data) { + try { + return this.adyenService_.cancelPayment(data) + } catch (error) { + throw error + } + } +} + +export default MobilePayAdyenService diff --git a/packages/medusa-payment-adyen/src/services/paypal.js b/packages/medusa-payment-adyen/src/services/paypal.js new file mode 100644 index 0000000000..2cefb9861c --- /dev/null +++ b/packages/medusa-payment-adyen/src/services/paypal.js @@ -0,0 +1,74 @@ +import _ from "lodash" +import { PaymentService } from "medusa-interfaces" + +class PayPalAdyenService extends PaymentService { + static identifier = "paypalAdyen" + + constructor({ adyenService }) { + super() + + this.adyenService_ = adyenService + } + + /** + * Status for Adyen payment. + * @param {Object} paymentData - payment method data from cart + * @returns {string} the status of the payment + */ + async getStatus(paymentData) { + const { resultCode } = paymentData + let status = "initial" + + if (resultCode === "Authorised") { + status = "authorized" + } + + return status + } + + async createPayment(_) { + return {} + } + + async authorizePayment(cart, paymentMethod) { + return this.adyenService_.authorizePayment(cart, paymentMethod) + } + + async retrievePayment(data) { + return this.adyenService_.retrievePayment(data) + } + + async updatePayment(data, _) { + return this.adyenService_.updatePayment(data) + } + + async deletePayment(data) { + return this.adyenService_.deletePayment(data) + } + + async capturePayment(data) { + try { + return this.adyenService_.capturePayment(data) + } catch (error) { + throw error + } + } + + async refundPayment(data) { + try { + return this.adyenService_.refundPayment(data) + } catch (error) { + throw error + } + } + + async cancelPayment(data) { + try { + return this.adyenService_.cancelPayment(data) + } catch (error) { + throw error + } + } +} + +export default PayPalAdyenService diff --git a/packages/medusa/src/loaders/plugins.js b/packages/medusa/src/loaders/plugins.js index cf6c9a6604..67a39c6420 100644 --- a/packages/medusa/src/loaders/plugins.js +++ b/packages/medusa/src/loaders/plugins.js @@ -51,7 +51,7 @@ export default ({ rootDirectory, container, app }) => { registerModels(pluginDetails, container) registerServices(pluginDetails, container) registerMedusaApi(pluginDetails, container) - registerApi(pluginDetails, app) + registerApi(pluginDetails, app, rootDirectory) registerCoreRouters(pluginDetails, container) registerSubscribers(pluginDetails, container) }) @@ -111,10 +111,10 @@ function registerCoreRouters(pluginDetails, container) { /** * Registers the plugin's api routes. */ -function registerApi(pluginDetails, app) { +function registerApi(pluginDetails, app, rootDirectory = "") { try { const routes = require(`${pluginDetails.resolve}/api`).default - app.use("/", routes()) + app.use("/", routes(rootDirectory)) return app } catch (err) { return app diff --git a/packages/medusa/src/services/cart.js b/packages/medusa/src/services/cart.js index dcc33b3de2..ebe0b4b54a 100644 --- a/packages/medusa/src/services/cart.js +++ b/packages/medusa/src/services/cart.js @@ -817,13 +817,13 @@ class CartService extends BaseService { return null } - const data = await this.paymentProviderService_.updateSession( - pSession, - cart - ) + // const data = await this.paymentProviderService_.updateSession( + // pSession, + // cart + // ) return { provider_id: pSession.provider_id, - data, + data: {}, } }) ) @@ -981,13 +981,18 @@ class CartService extends BaseService { newMethods.push(option) } + const finalMethods = newMethods.map(m => { + const { _id, ...rest } = m + return rest + }) + return this.cartModel_ .updateOne( { _id: cart._id, }, { - $set: { shipping_methods: newMethods }, + $set: { shipping_methods: finalMethods }, } ) .then(result => { diff --git a/packages/medusa/src/services/order.js b/packages/medusa/src/services/order.js index b065ba8b6c..484b06220f 100644 --- a/packages/medusa/src/services/order.js +++ b/packages/medusa/src/services/order.js @@ -179,6 +179,28 @@ class OrderService extends BaseService { return order } + /** + * Gets an order by metadata key value pair. + * @param {string} key - key of metadata + * @param {string} value - value of metadata + * @return {Promise} the order document + */ + async retrieveByMetadata(key, value) { + const order = await this.orderModel_ + .findOne({ metadata: { [key]: value } }) + .catch(err => { + throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + }) + + if (!order) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Order with metadata ${key}: ${value} was not found` + ) + } + return order + } + /** * Checks the existence of an order by cart id. * @param {string} cartId - cart id to find order @@ -321,6 +343,7 @@ class OrderService extends BaseService { customer_id: cart.customer_id, cart_id: cart._id, currency_code: region.currency_code, + metadata: cart.metadata, } const orderDocument = await this.orderModel_.create([o], { @@ -501,7 +524,19 @@ class OrderService extends BaseService { provider_id ) - await paymentProvider.capturePayment(data) + const captureData = await paymentProvider.capturePayment(data) + + // If Adyen is used as payment provider, we need to check the + // validity of the capture request + if ( + captureData.data.pspReference && + captureData.data.response !== "[capture-received]" + ) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Could not process capture" + ) + } return this.orderModel_.updateOne( { diff --git a/packages/medusa/src/subscribers/order.js b/packages/medusa/src/subscribers/order.js index f9689029b3..d50599e21b 100644 --- a/packages/medusa/src/subscribers/order.js +++ b/packages/medusa/src/subscribers/order.js @@ -29,10 +29,6 @@ class OrderSubscriber { await this.customerService_.addAddress(order.customer_id, address) }) - - this.eventBus_.subscribe("order.placed", async order => { - await this.cartService_.delete(order.cart_id) - }) } } diff --git a/yarn.lock b/yarn.lock index a8083d925c..c64f8f35ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1069,6 +1069,13 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== +axios@^0.19.2: + version "0.19.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -1572,7 +1579,7 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -debug@3.1.0: +debug@3.1.0, debug@=3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== @@ -1976,6 +1983,13 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"