Replaces MongoDB support with PostgreSQL (#151)

- All schemas have been rewritten to a relational model
- All services have been rewritten to accommodate the new data model
- Adds idempotency keys to core endpoints allowing you to retry requests with no additional side effects
- Adds staged jobs to avoid putting jobs in the queue when transactions abort
- Adds atomic transactions to all methods with access to the data layer

Co-authored-by: Oliver Windall Juhl <oliver@mrbltech.com>
This commit is contained in:
Sebastian RindomandOliver Windall Juhl committed 2021-01-26 10:26:14 +01:00
1 parent 5f819486fc
commit f1baca3cbd
499 files changed
+25909 -16128

No files matched your search

@@ -0,0 +1,17 @@
export default async (req, res) => {
const adyenService = req.scope.resolve("adyenService")
const eventBus = req.scope.resolve("eventBusService")
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
}
eventBus.emit("adyen.notification_received", event)
res.status(200).send("[accepted]")
}
@@ -1,41 +0,0 @@
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
}
if (event.success === "true" && event.eventCode === "AUTHORISATION") {
const orderService = req.scope.resolve("orderService")
const cartService = req.scope.resolve("cartService")
const cartId = event.additionalData["metadata.cart_id"]
try {
const order = await orderService.retrieveByCartId(cartId)
console.log(order)
} catch (error) {
const cart = await cartService.retrieve(cartId)
await orderService.createFromCart(cart)
}
// Create from cart
res.status(200).send("[accepted]")
return
}
if (event.success === "true" && event.eventCode === "CAPTURE") {
// Create from cart
console.log("Captured")
res.status(200).send("[accepted]")
return
}
res.status(200).send("[accepted]")
}
@@ -18,12 +18,12 @@ export default (app, rootDirectory) => {
})
)
app.use("/adyen-hooks", route)
app.use("/adyen/webhooks", route)
route.post(
"/capture",
"/notification",
bodyParser.json(),
middlewares.wrap(require("./capture-hook").default)
middlewares.wrap(require("./adyen-notification").default)
)
return app
@@ -1,58 +0,0 @@
import { Validator, MedusaError } from "medusa-core-utils"
export default async (req, res) => {
const schema = Validator.object().keys({
cart_id: Validator.string().required(),
provider_id: Validator.string().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 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_data.paymentMethod
)
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
)
await cartService.setPaymentMethod(cart._id, {
provider_id: value.provider_id,
data,
})
res.status(200).json({ data })
} catch (err) {
throw err
}
}
@@ -1,30 +0,0 @@
import { Validator, MedusaError } from "medusa-core-utils"
export default async (req, res) => {
const schema = Validator.object().keys({
payload: Validator.string().required(),
payment_data: Validator.string().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 adyen = req.scope.resolve("adyenService")
const paymentProvider = req.scope.resolve(`pp_${value.provider_id}`)
const { data } = await adyen.checkPaymentResult(
value.payment_data,
value.payload
)
const status = await paymentProvider.getStatus(data)
res.status(200).json({ status })
} catch (err) {
throw err
}
}
@@ -26,29 +26,5 @@ export default (app, rootDirectory) => {
middlewares.wrap(require("./retrieve-payment-methods").default)
)
route.post(
"/authorize",
bodyParser.json(),
middlewares.wrap(require("./authorize-payment").default)
)
route.post(
"/update",
bodyParser.json(),
middlewares.wrap(require("./update-payment").default)
)
route.post(
"/payment-status",
bodyParser.json(),
middlewares.wrap(require("./check-payment-status").default)
)
route.get(
"/payment-status",
bodyParser.json(),
middlewares.wrap(require("./check-payment-status").default)
)
return app
}
@@ -13,39 +13,44 @@ export default async (req, res) => {
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 cart = await cartService.retrieve(value.cart_id, {
select: ["total"],
relations: ["region", "region.payment_providers", "payment_sessions"],
})
const allowedMethods = cart.payment_sessions.map(
(ps) => ps.provider_id.split("Adyen")[0]
)
const allowedMethods = cart.payment_sessions.map((ps) => {
if (ps.provider_id.includes("adyen")) {
return ps.provider_id.split("-adyen")[0]
}
})
if (allowedMethods.length === 0) {
res.status(200).json({ paymentMethods: {} })
return
}
const { data } = await adyenService.retrievePaymentMethods(
cart,
const pmMethods = await adyenService.retrievePaymentMethods(
allowedMethods,
total,
region.currency_code
cart.total,
cart.currency_code,
cart.customer_id || ""
)
// Adyen does not behave 100% correctly in regards to allowed methods
// Therefore, we sanity filter before sending them to the storefront
const { paymentMethods, groups } = data
const { paymentMethods, groups, storedPaymentMethods } = pmMethods
const methods = paymentMethods.filter((pm) =>
allowedMethods.includes(pm.type)
)
res
.status(200)
.json({ paymentMethods: { paymentMethods: methods, groups } })
const response = {
paymentMethods: methods,
groups,
storedPaymentMethods,
}
res.status(200).json({ payment_methods: response })
} catch (err) {
throw err
}
@@ -1,32 +0,0 @@
import { Validator, MedusaError } from "medusa-core-utils"
export default async (req, res) => {
const schema = Validator.object().keys({
cart_id: Validator.string().required(),
provider_id: Validator.string().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 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.updatePayment(
value.payment_data.paymentData,
value.payment_data.details
)
await cartService.updatePaymentSession(cart._id, value.provider_id, data)
res.status(200).json({ data })
} catch (err) {
throw err
}
}
@@ -2,72 +2,141 @@ import axios from "axios"
import _ from "lodash"
import { hmacValidator } from "@adyen/api-library"
import { BaseService } from "medusa-interfaces"
import { Client, Config, CheckoutAPI } from "@adyen/api-library"
class AdyenService extends BaseService {
constructor({ regionService, cartService, totalsService }, options) {
constructor({ cartService }, options) {
super()
this.regionService_ = regionService
/** @private @constant {CartService} */
this.cartService_ = cartService
this.totalsService_ = totalsService
/**
* {
* api_key: "",
* notification_hmac: "",
* return_url: "",
* merchant_account: "",
* origin: "",
* environment: "",
* live_endpoint_prefix: "",
* payment_endpoint: ""
* }
*/
this.options_ = options
this.adyenCheckoutApi = axios.create({
baseURL: "https://checkout-test.adyen.com/v53",
headers: {
"Content-Type": "application/json",
"x-API-key": this.options_.api_key,
},
/** @private @constant {AxiosClient} */
this.adyenClient_ = this.initAdyenClient()
/** @private @constant {AdyenClient} */
this.adyenPaymentApi = this.initPaymentClient()
}
withTransaction(transactionManager) {
if (!transactionManager) {
return this
}
const cloned = new AdyenService({
cartService: this.cartService_,
totalsService: this.totalsService_,
})
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,
},
})
this.transactionManager_ = transactionManager
return cloned
}
getOptions() {
return this.options_
}
validateNotification(event) {
initPaymentClient() {
return axios.create({
baseURL: this.options_.payment_endpoint,
headers: {
"Content-Type": "application/json",
"x-API-key": this.options_.api_key,
},
})
}
initAdyenClient() {
const config = new Config()
config.apiKey = this.options_.api_key
config.merchantAccount = this.options_.merchant_account
const client = new Client({
config,
})
client.setEnvironment(
this.options_.environment,
this.options_.live_endpoint_prefix
)
return client
}
/**
* Validates an Adyen webhook notification
* @param {object} notification - notification to validate
* @returns {string} the status of the payment
*/
validateNotification(notification) {
const validator = new hmacValidator()
const validated = validator.validateHMAC(
event,
notification,
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
* Retrieve stored payment methods from Adyen.
* @param {Customer} customer - customer to retrieve methods for
* @returns {Promise} result containing the stored payment methods from Adyen
*/
async retrievePaymentMethods(cart, allowedMethods, total, currency) {
async retrieveSavedMethods(customer) {
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
channel: "Web",
shopperReference: customer.id,
}
try {
return await this.adyenCheckoutApi.post("/paymentMethods", request)
const checkout = new CheckoutAPI(this.adyenClient_)
const methods = await checkout.paymentMethods(request)
return methods.storedPaymentMethods || []
} catch (error) {
throw error
}
}
/**
* Retrieve payment methods from Adyen.
* @param {string[]} allowedMethods - the allowed methods based on region
* @param {number} total - total amount to be paid with payment methods
* @param {string} currency - currency code to use for the payment
* @param {string} customerId - id of the customer paying
* @returns {Promise} result containing the payment methods from Adyen
*/
async retrievePaymentMethods(allowedMethods, total, currency, customerId) {
let request = {
allowedPaymentMethods: allowedMethods,
amount: {
value: total,
currency: currency,
},
merchantAccount: this.options_.merchant_account,
channel: "Web",
shopperReference: customerId,
}
try {
const checkout = new CheckoutAPI(this.adyenClient_)
return checkout.paymentMethods(request)
} catch (error) {
throw error
}
@@ -75,89 +144,218 @@ class AdyenService extends BaseService {
/**
* Status for Adyen payment.
* @param {Object} paymentData - payment method data from cart
* @param {object} paymentData - payment method data from cart
* @returns {string} the status of the payment
*/
async getStatus(_) {
let status = "initial"
getStatus(paymentData) {
const { resultCode } = paymentData
let status = "pending"
if (resultCode === "Pending") {
return status
}
if (resultCode === "Refused") {
return status
}
if (resultCode === "Error") {
status = "error"
}
if (resultCode === "Authorised") {
status = "authorized"
}
if (resultCode === "Canceled") {
status = "canceled"
}
if (resultCode === "ChallengeShopper") {
status = "requires_more"
}
if (resultCode === "RedirectShopper") {
status = "requires_more"
}
if (resultCode === "IdentifyShopper") {
status = "requires_more"
}
return status
}
/**
* Creates Adyen payment object.
* @param {any} _ - placeholder object
* @returns {Object} empty payment data
* @param {Cart} cart - cart to initiate payment for
* @returns {object} empty payment data
*/
async createPayment(_) {
return {}
}
async retrievePayment(data) {
return data
}
async updatePayment(paymentData, details) {
const request = {
paymentData,
details,
}
return this.adyenCheckoutApi.post("/payments/details", request)
async createPayment(cart) {
return { cart_id: cart.id }
}
/**
* Creates and authorizes an Ayden payment
* @returns {Object} payment data result
* Retrieves Adyen payment. This is not supported by adyen, so we simply
* return the current payment method data
* @param {object} data - payment session
* @returns {object} payment method data
*/
async authorizePayment(cart, paymentMethod) {
const region = await this.regionService_.retrieve(cart.region_id)
const total = await this.totalsService_.getTotal(cart)
async getPaymentData(paymentSession) {
return { ...paymentSession.data }
}
/**
* Retrieves Adyen payment. This is not supported by adyen, so we simply
* return the current payment method data
* @param {object} sessionData - the data of the payment to retrieve
* @returns {Promise<object>} Stripe payment intent
*/
async retrieve(sessionData) {
return sessionData
}
/**
* Creates and authorizes an Adyen payment.
* Requires cart_id in context for authorization.
* Return status of authorization result.
* @param {object} sessionData - payment session data
* @param {object} context - properties relevant to current context
* @returns {Promise<{ status: string, data: object }>} result with data and status
*/
async authorizePayment(session, context) {
const sessionData = session.data
const status = this.getStatus(sessionData)
// If session data is present, we already called authorize once.
// Therefore, this is most likely a call for getting additional details
if (status === "requires_more") {
const updated = await this.updatePaymentData(sessionData, {
details: sessionData.details,
paymentData: sessionData.paymentData,
})
return { data: updated, status: this.getStatus(updated) }
}
if (status === "authorized") {
return { data: sessionData, status: "authorized" }
}
const cart = await this.cartService_.retrieve(session.cart_id, {
select: ["total"],
relations: ["region", "shipping_address"],
})
const amount = {
currency: cart.region.currency_code.toUpperCase(),
value: cart.total,
}
let request = {
amount: {
currency: region.currency_code,
value: total * 100,
},
amount,
shopperIP: context.ip_address || "",
shopperReference: cart.customer_id,
paymentMethod,
reference: cart._id,
paymentMethod: sessionData.paymentData.paymentMethod,
reference: cart.id,
merchantAccount: this.options_.merchant_account,
returnUrl: this.options_.return_url,
origin: this.options_.origin,
channel: "Web",
redirectFromIssuerMethod: "GET",
browserInfo: sessionData.browserInfo || {},
billingAddress: {
city: cart.shipping_address.city,
country: cart.shipping_address.country_code,
houseNumberOrName: cart.shipping_address.address_2 || "",
postalCode: cart.shipping_address.postal_code,
stateOrProvice: cart.shipping_address.province || "",
street: cart.shipping_address.address_1,
},
metadata: {
cart_id: cart._id,
cart_id: cart.id,
},
}
if (paymentMethod.storedPaymentMethodId) {
// If customer chose to save the payment method
if (sessionData.storePaymentMethod) {
request.storePaymentMethod = "true"
request.shopperInteraction = "Ecommerce"
request.recurringProcessingModel = "CardOnFile"
}
return await this.adyenCheckoutApi.post("/payments", request)
const checkout = new CheckoutAPI(this.adyenClient_)
try {
const authorizedPayment = await checkout.payments(request, {
idempotencyKey: context.idempotency_key || "",
})
return {
data: authorizedPayment,
status: this.getStatus(authorizedPayment),
}
} catch (error) {
throw error
}
}
async checkPaymentResult(paymentData, payload) {
const request = {
paymentData,
details: {
payload,
},
async updatePaymentData(sessionData, update) {
if (_.isEmpty(update.details)) {
return { ...sessionData, ...update }
}
return this.adyenCheckoutApi.post("/payments/details", request)
const checkout = new CheckoutAPI(this.adyenClient_)
const updated = await checkout.paymentsDetails(update)
return updated
}
/**
* Captures an Ayden payment
* @param {Object} data - payment data to capture
* @returns {Object} payment data result of capture
* Updates an Adyen payment.
* @param {object} paymentData - payment data to update
* @param {details} details - details to update
* @returns {Promise} result of the update operation
*/
async capturePayment(data) {
const { pspReference, amount } = data
async updatePayment(paymentData, details) {
return paymentData
}
/**
* Additional details
* @param {object} paymentData - payment data
* @param {object} details - payment details
* @returns {Promise} current payment result
*/
async additionalDetails(paymentData, details) {
const request = {
paymentData,
details,
}
const checkout = new CheckoutAPI(this.adyenClient_)
return checkout.paymentsDetails(request)
}
/**
* Captures an Adyen payment
* @param {object} payment - payment to capture
* @returns {string} status = processing_captures
*/
async capturePayment(payment) {
const { pspReference, merchantReference } = payment.data
const { amount, currency_code } = payment
try {
const captured = await this.adyenPaymentApi.post("/capture", {
originalReference: pspReference,
modificationAmount: amount,
modificationAmount: {
value: amount,
currency: currency_code.toUpperCase(),
},
merchantAccount: this.options_.merchant_account,
reference: merchantReference,
})
if (
@@ -170,40 +368,55 @@ class AdyenService extends BaseService {
)
}
return captured
return { originalReference: pspReference, ...captured.data }
} catch (error) {
console.log(error)
throw error
}
}
/**
* Refunds an Ayden payment
* @param {Object} paymentData - payment data to refund
* Refunds an Adyen payment
* @param {object} payment - payment to refund
* @param {number} amountToRefund - amount to refund
* @returns {Object} payment data result of refund
* @returns {object} payment data result of refund
*/
async refundPayment(data) {
const { pspReference, amount } = data
async refundPayment(payment, amountToRefund) {
const { originalReference, merchantReference } = payment.data
const { currency_code } = payment
const refundAmount = {
currency: currency_code.toUpperCase(),
value: amountToRefund,
}
try {
return this.adyenPaymentApi.post("/refund", {
originalReference: pspReference,
const refunded = await this.adyenPaymentApi.post("/refund", {
originalReference,
merchantAccount: this.options_.merchant_account,
modificationAmount: amount,
modificationAmount: refundAmount,
reference: merchantReference,
})
return { originalReference, ...refunded.data }
} catch (error) {
throw error
}
}
/**
* Cancels an Ayden payment
* @param {Object} paymentData - payment data to cancel
* @returns {Object} payment data result of cancel
* Adyen does not have a way of deleting payments, hence the empty impl.
*/
async cancelPayment(paymentData) {
const { pspReference } = paymentData
async deletePayment(_) {
return {}
}
/**
* Cancels an Adyen payment.
* @param {object} payment - payment to cancel
* @returns {object} payment data result of cancel
*/
async cancelPayment(payment) {
const { pspReference } = payment.data
try {
return this.adyenPaymentApi.post("/cancel", {
@@ -0,0 +1,104 @@
import _ from "lodash"
import https from "https"
import fs from "fs"
import axios from "axios"
import { PaymentService } from "medusa-interfaces"
class ApplePayAdyenService extends PaymentService {
static identifier = "applepay-adyen"
constructor({ adyenService }, options) {
super()
this.adyenService_ = adyenService
this.options_ = options
}
/**
* 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 getApplePaySession(validationUrl) {
let certificate
try {
// Place certificate in root folder
certificate = fs.readFileSync("./apple-pay-cert.pem")
} catch (error) {
throw new Error(
"Could not find ApplePay certificate. Make sure to place it in root folder of your server"
)
}
const httpsAgent = new https.Agent({
cert: certificate,
key: certificate,
rejectUnauthorized: false,
})
const request = {
merchantIdentifier: this.options_.applepay_merchant_id,
displayName: this.options_.applepay_display_name,
initiative: "web",
initiativeContext: this.options_.applepay_initiative_context,
}
return axios.post(validationUrl, request, {
httpsAgent,
})
}
async authorizePayment(sessionData, context) {
return this.adyenService_.authorizePayment(sessionData, context)
}
async getPaymentData(data) {
return this.adyenService_.getPaymentData(data)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async updatePaymentData(sessionData, update) {
return this.adyenService_.updatePaymentData(sessionData, update)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
return this.adyenService_.capturePayment(data)
}
async refundPayment(data, amountToRefund) {
return this.adyenService_.refundPayment(data, amountToRefund)
}
async cancelPayment(data) {
return this.adyenService_.cancelPayment(data)
}
}
export default ApplePayAdyenService
@@ -1,74 +0,0 @@
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
@@ -0,0 +1,62 @@
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class CardAdyenService extends PaymentService {
static identifier = "scheme-adyen"
constructor({ adyenService }) {
super()
this.adyenService_ = adyenService
}
async retrieveSavedMethods(customer) {
return this.adyenService_.retrieveSavedMethods(customer)
}
async getStatus(paymentData) {
return this.adyenService_.getStatus(paymentData)
}
async createPayment(data) {
return this.adyenService_.createPayment(data)
}
async authorizePayment(sessionData, context) {
return this.adyenService_.authorizePayment(sessionData, context)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async getPaymentData(data) {
return this.adyenService_.getPaymentData(data)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async updatePaymentData(sessionData, update) {
return this.adyenService_.updatePaymentData(sessionData, update)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
return this.adyenService_.capturePayment(data)
}
async refundPayment(data, amountToRefund) {
return this.adyenService_.refundPayment(data, amountToRefund)
}
async cancelPayment(data) {
return this.adyenService_.cancelPayment(data)
}
}
export default CardAdyenService
@@ -1,74 +0,0 @@
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
@@ -0,0 +1,58 @@
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class GooglePayAdyenService extends PaymentService {
static identifier = "paywithgoogle-adyen"
constructor({ adyenService }) {
super()
this.adyenService_ = adyenService
}
async getStatus(paymentData) {
return this.adyenService_.getStatus(paymentData)
}
async createPayment(data) {
return this.adyenService_.createPayment(data)
}
async authorizePayment(sessionData, context) {
return this.adyenService_.authorizePayment(sessionData, context)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async updatePaymentData(sessionData, update) {
return this.adyenService_.updatePaymentData(sessionData, update)
}
async getPaymentData(data) {
return this.adyenService_.getPaymentData(data)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
return this.adyenService_.capturePayment(data)
}
async refundPayment(data, amountToRefund) {
return this.adyenService_.refundPayment(data, amountToRefund)
}
async cancelPayment(data) {
return this.adyenService_.cancelPayment(data)
}
}
export default GooglePayAdyenService
@@ -1,74 +0,0 @@
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
@@ -0,0 +1,62 @@
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class IdealAdyenService extends PaymentService {
static identifier = "ideal-adyen"
constructor({ adyenService }) {
super()
this.adyenService_ = adyenService
}
async getStatus(paymentData) {
return this.adyenService_.getStatus(paymentData)
}
async createPayment(data) {
return this.adyenService_.createPayment(data)
}
async authorizePayment(sessionData, context) {
return this.adyenService_.authorizePayment(sessionData, context)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async getPaymentData(data) {
return this.adyenService_.getPaymentData(data)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async updatePaymentData(sessionData, update) {
return this.adyenService_.updatePaymentData(sessionData, update)
}
async getPaymentData(data) {
return this.adyenService_.getPaymentData(data)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
return this.adyenService_.capturePayment(data)
}
async refundPayment(data, amountToRefund) {
return this.adyenService_.refundPayment(data, amountToRefund)
}
async cancelPayment(data) {
return this.adyenService_.cancelPayment(data)
}
}
export default IdealAdyenService
@@ -1,74 +0,0 @@
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
@@ -0,0 +1,58 @@
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class MobilePayAdyenService extends PaymentService {
static identifier = "mobilepay-adyen"
constructor({ adyenService }) {
super()
this.adyenService_ = adyenService
}
async getStatus(paymentData) {
return this.adyenService_.getStatus(paymentData)
}
async createPayment(data) {
return this.adyenService_.createPayment(data)
}
async authorizePayment(sessionData, context) {
return this.adyenService_.authorizePayment(sessionData, context)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async updatePaymentData(sessionData, update) {
return this.adyenService_.updatePaymentData(sessionData, update)
}
async getPaymentData(data) {
return this.adyenService_.getPaymentData(data)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
return this.adyenService_.capturePayment(data)
}
async refundPayment(data, amountToRefund) {
return this.adyenService_.refundPayment(data, amountToRefund)
}
async cancelPayment(data) {
return this.adyenService_.cancelPayment(data)
}
}
export default MobilePayAdyenService
@@ -1,74 +0,0 @@
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 "authorized"
}
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
@@ -0,0 +1,58 @@
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class PayPalAdyenService extends PaymentService {
static identifier = "paypal-adyen"
constructor({ adyenService }) {
super()
this.adyenService_ = adyenService
}
async getStatus(paymentData) {
return this.adyenService_.getStatus(paymentData)
}
async createPayment(data) {
return this.adyenService_.createPayment(data)
}
async authorizePayment(sessionData, context) {
return this.adyenService_.authorizePayment(sessionData, context)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async getPaymentData(data) {
return this.adyenService_.getPaymentData(data)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async updatePaymentData(sessionData, update) {
return this.adyenService_.updatePaymentData(sessionData, update)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
return this.adyenService_.capturePayment(data)
}
async refundPayment(data, amountToRefund) {
return this.adyenService_.refundPayment(data, amountToRefund)
}
async cancelPayment(data) {
return this.adyenService_.cancelPayment(data)
}
}
export default PayPalAdyenService
@@ -1,74 +0,0 @@
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
@@ -0,0 +1,159 @@
class AdyenSubscriber {
constructor(container) {
/** @private @const {CartService} */
this.cartService_ = container.cartService
/** @private @const {OrderService} */
this.orderService_ = container.orderService
/** @private @const {EventBusService} */
this.eventBus_ = container.eventBusService
/** @private @const {PaymentRepository} */
this.paymentRepository_ = container.paymentRepository
this.manager_ = container.manager
this.eventBus_.subscribe(
"adyen.notification_received",
async (notification) => this.handleAdyenNotification(notification)
)
}
/**
* Webhook handler for Adyen payment.
* @param {object} notification - webhook notification object
* @returns {string} the status of the payment intent
*/
async handleAdyenNotification(notification) {
switch (true) {
case notification.success === "true" &&
notification.eventCode === "AUTHORISATION": {
this.handleAuthorization_(notification)
break
}
case notification.success === "false" &&
notification.eventCode === "AUTHORISATION": {
this.handleFailedAuthorization_(notification)
break
}
case notification.success === "true" &&
notification.eventCode === "CAPTURE_FAILED": {
this.handleFailedCapture_(notification)
break
}
case notification.success === "false" &&
notification.eventCode === "CAPTURE": {
this.handleFailedCapture_(notification)
break
}
case notification.success === "false" &&
notification.eventCode === "REFUND": {
this.handleFailedRefund_(notification)
break
}
default:
break
}
}
async handleFailedAuthorization_(notification) {
const cartId = notification.additionalData["metadata.cart_id"]
const cart = await this.cartService_.retrieve(cartId, {
relations: ["payment_sessions"],
})
const { payment_session } = cart
const updated = {
...payment_session,
status: "error",
data: {
...payment_session.data,
pspReference: notification.pspReference,
},
}
await this.cartService_.updatePaymentSession(cart.id, updated)
}
async handleAuthorization_(notification) {
const cartId = notification.additionalData["metadata.cart_id"]
const paymentRepository = this.manager_.getCustomRepository(
this.paymentRepository_
)
// We need to ensure, that an order is created in situations, where the
// customer might have closed their browser prior to order creation
try {
const orderPayment = await paymentRepository.findOne({
where: { cart_id: cartId },
})
if (!orderPayment) {
throw new Error("Payment not found")
}
const updatedPayment = {
...orderPayment,
data: {
...orderPayment.data,
resultCode: "Authorised",
pspReference: notification.pspReference,
},
}
await this.paymentRepository_.save(updatedPayment)
} catch (error) {
console.log(error)
await this.manager_.transaction(async (manager) => {
const session = {
pspReference: notification.pspReference,
resultCode: "Authorised",
}
await this.cartService_
.withTransaction(manager)
.updatePaymentSession(cartId, session)
await this.cartService_
.withTransaction(manager)
.authorizePayment(cartId)
await this.orderService_.withTransaction(manager).createFromCart(cartId)
})
}
}
async handleFailedCapture_(notification) {
const cartId = notification.additionalData["metadata.cart_id"]
const order = await this.orderService_.retrieveByCartId(cartId)
await this.orderService_.update(order.id, {
payment_status: "requires_action",
})
await this.eventBus_.emit("order.payment_capture_failed", {
order,
error: new Error(`Adyen payment capture: ${notification.reason}`),
})
}
async handleFailedRefund_(notification) {
const cartId = notification.additionalData["metadata.cart_id"]
const order = await this.orderService_.retrieveByCartId(cartId)
await this.orderService_.update(order.id, {
payment_status: "requires_action",
})
await this.eventBus_.emit("order.refund_failed", {
order,
error: new Error(`Adyen payment capture: ${notification.reason}`),
})
}
}
export default AdyenSubscriber