Merge branch 'develop' of github.com:medusajs/medusa into develop

This commit is contained in:
olivermrbl
2022-11-02 18:20:54 +01:00
28 changed files with 2002 additions and 1315 deletions
+24 -16
View File
@@ -1,59 +1,67 @@
import { AbstractPaymentService } from "@medusajs/medusa";
import { AbstractPaymentService } from "@medusajs/medusa"
class TestPayService extends AbstractPaymentService {
static identifier = "test-pay";
static identifier = "test-pay"
constructor(_) {
super(_);
super(_)
}
async getStatus(paymentData) {
return "authorized";
return "authorized"
}
async retrieveSavedMethods(customer) {
return Promise.resolve([]);
return Promise.resolve([])
}
async createPayment() {
return {};
return {}
}
async createPaymentNew() {
return {}
}
async retrievePayment(data) {
return {};
return {}
}
async getPaymentData(sessionData) {
return {};
return {}
}
async authorizePayment(sessionData, context = {}) {
return {};
return {}
}
async updatePaymentData(sessionData, update) {
return {};
return {}
}
async updatePayment(sessionData, cart) {
return {};
return {}
}
async updatePaymentNew(sessionData, paymentInput) {
return {}
}
async deletePayment(payment) {
return {};
return {}
}
async capturePayment(payment) {
return {};
return {}
}
async refundPayment(payment, amountToRefund) {
return {};
return {}
}
async cancelPayment(payment) {
return {};
return {}
}
}
export default TestPayService;
export default TestPayService
@@ -33,6 +33,10 @@ class TestPayService extends AbstractPaymentService {
return data
}
async createPaymentNew(inputData) {
return inputData
}
async retrievePayment(data) {
return {}
}
@@ -59,6 +63,10 @@ class TestPayService extends AbstractPaymentService {
return {}
}
async updatePaymentNew(sessionData) {
return sessionData
}
async deletePayment(payment) {
return {}
}
+9
View File
@@ -93,6 +93,15 @@ const bootstrapApp = async () => {
}
const app = express()
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", req.headers.origin)
res.header("Access-Control-Allow-Methods", "*")
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept"
)
next()
})
const dir = path.resolve(
path.join(__dirname, "../../packages/medusa/src/loaders")
@@ -201,6 +201,10 @@ class AdyenService extends BaseService {
return { cart_id: cart.id }
}
async createPaymentNew(paymentInput) {
return { resource_id: paymentInput.resource_id }
}
/**
* Retrieves Adyen payment. This is not supported by adyen, so we simply
* return the current payment method data
@@ -322,6 +326,10 @@ class AdyenService extends BaseService {
return paymentData
}
async updatePaymentNew(paymentData, details) {
return paymentData
}
/**
* Additional details
* @param {object} paymentData - payment data
@@ -1,8 +1,10 @@
import { MedusaError } from "medusa-core-utils"
export default async (req, res) => {
const { klarna_order_id } = req.query
function isPaymentCollection(id) {
return id && id.startsWith("paycol")
}
try {
const orderService = req.scope.resolve("orderService")
const klarnaProviderService = req.scope.resolve("pp_klarna")
@@ -11,10 +13,18 @@ export default async (req, res) => {
klarna_order_id
)
const cartId = klarnaOrder.merchant_data
const order = await orderService.retrieveByCartId(cartId)
const resourceId = klarnaOrder.merchant_data
await klarnaProviderService.acknowledgeOrder(klarnaOrder.order_id, order.id)
if (isPaymentCollection(resourceId)) {
await klarnaProviderService.acknowledgeOrder(klarnaOrder.order_id)
} else {
const order = await orderService.retrieveByCartId(resourceId)
await klarnaProviderService.acknowledgeOrder(
klarnaOrder.order_id,
order.id
)
}
res.sendStatus(200)
} catch (error) {
@@ -230,6 +230,84 @@ class KlarnaProviderService extends PaymentService {
return order
}
validateKlarnaOrderUrls(property) {
const required = ["terms", "checkout", "confirmation"]
const isMissing = required.some((prop) => !this.options_[property]?.[prop])
if (isMissing) {
throw new Error(
`options.${property} is required to create a Klarna Order.\n` +
`medusa-config.js file has to contain ${property} { ${required.join(
", "
)}}`
)
}
}
replaceStringWithPropertyValue(string, obj) {
const keys = Object.keys(obj)
for (const key of keys) {
if (string.includes(`{${key}}`)) {
string = string.replace(`{${key}}`, obj[key])
}
}
return string
}
async paymentInputToKlarnaOrder(paymentInput) {
if (paymentInput.cart) {
this.validateKlarnaOrderUrls("merchant_urls")
return this.cartToKlarnaOrder(paymentInput.cart)
}
this.validateKlarnaOrderUrls("payment_collection_urls")
let order = {
// Custom id is stored, such that we can use it for hooks
merchant_data: paymentInput.resource_id,
locale: "en-US",
}
const { currency_code, amount } = paymentInput
order.order_lines = [
{
name: "Payment Collection",
quantity: 1,
unit_price: amount,
tax_rate: 0,
total_amount: amount,
total_tax_amount: 0,
},
]
// Defaults to Sweden
order.purchase_country = "SE"
order.order_amount = amount
order.order_tax_amount = 0
order.purchase_currency = currency_code.toUpperCase()
order.merchant_urls = {
terms: this.replaceStringWithPropertyValue(
this.options_.payment_collection_urls.terms,
paymentInput
),
checkout: this.replaceStringWithPropertyValue(
this.options_.payment_collection_urls.checkout,
paymentInput
),
confirmation: this.replaceStringWithPropertyValue(
this.options_.payment_collection_urls.confirmation,
paymentInput
),
push: `${this.backendUrl_}/klarna/push?klarna_order_id={checkout.order.id}`,
}
return order
}
/**
* Status for Klarna order.
* @param {Object} paymentData - payment method data from cart
@@ -251,7 +329,7 @@ class KlarnaProviderService extends PaymentService {
}
/**
* Creates Stripe PaymentIntent.
* Creates Klarna PaymentIntent.
* @param {string} cart - the cart to create a payment for
* @param {number} amount - the amount to create a payment for
* @returns {string} id of payment intent
@@ -271,6 +349,21 @@ class KlarnaProviderService extends PaymentService {
}
}
async createPaymentNew(paymentInput) {
try {
const order = await this.paymentInputToKlarnaOrder(paymentInput)
const klarnaPayment = await this.klarna_
.post(this.klarnaOrderUrl_, order)
.then(({ data }) => data)
return klarnaPayment
} catch (error) {
this.logger_.error(error)
throw error
}
}
/**
* Retrieves Klarna Order.
* @param {string} cart - the cart to retrieve order for
@@ -338,18 +431,20 @@ class KlarnaProviderService extends PaymentService {
* @param {string} klarnaOrderId - id of the order to acknowledge
* @returns {string} id of acknowledged order
*/
async acknowledgeOrder(klarnaOrderId, orderId) {
async acknowledgeOrder(klarnaOrderId, orderId = null) {
try {
await this.klarna_.post(
`${this.klarnaOrderManagementUrl_}/${klarnaOrderId}/acknowledge`
)
await this.klarna_.patch(
`${this.klarnaOrderManagementUrl_}/${klarnaOrderId}/merchant-references`,
{
merchant_reference1: orderId,
}
)
if (orderId !== null) {
await this.klarna_.patch(
`${this.klarnaOrderManagementUrl_}/${klarnaOrderId}/merchant-references`,
{
merchant_reference1: orderId,
}
)
}
return klarnaOrderId
} catch (error) {
@@ -408,6 +503,22 @@ class KlarnaProviderService extends PaymentService {
return paymentData
}
async updatePaymentNew(paymentData, paymentInput) {
if (paymentInput.amount !== paymentData.order_amount) {
const order = await this.paymentInputToKlarnaOrder(paymentInput)
return this.klarna_
.post(`${this.klarnaOrderUrl_}/${paymentData.order_id}`, order)
.then(({ data }) => data)
.catch(async (_) => {
return this.klarna_
.post(this.klarnaOrderUrl_, order)
.then(({ data }) => data)
})
}
return paymentData
}
/**
* Captures Klarna order.
* @param {Object} paymentData - payment method data from cart
@@ -26,6 +26,10 @@ class ManualPaymentService extends PaymentService {
return { status: "pending" }
}
async createPaymentNew() {
return { status: "pending" }
}
/**
* Retrieves payment
* @param {object} data - the data of the payment to retrieve
@@ -52,6 +56,10 @@ class ManualPaymentService extends PaymentService {
return sessionData.data
}
async updatePaymentNew(sessionData) {
return sessionData.data
}
/**
.
* @param {object} sessionData - payment session data.
@@ -21,21 +21,11 @@ export default async (req, res) => {
return
}
try {
const body = req.body
const authId = body.resource.id
const auth = await paypalService.retrieveAuthorization(authId)
const order = await paypalService.retrieveOrderFromAuth(auth)
const purchaseUnit = order.purchase_units[0]
const cartId = purchaseUnit.custom_id
if (!cartId) {
res.sendStatus(200)
return
}
function isPaymentCollection(id) {
return id && id.startsWith("paycol")
}
async function autorizeCart(req, cartId) {
const manager = req.scope.resolve("manager")
const cartService = req.scope.resolve("cartService")
const swapService = req.scope.resolve("swapService")
@@ -78,6 +68,42 @@ export default async (req, res) => {
}
}
})
}
async function autorizePaymentCollection(req, payColId) {
const manager = req.scope.resolve("manager")
const paymentCollectionService = req.scope.resolve(
"paymentCollectonService"
)
await manager.transaction(async (m) => {
const payCol = await paymentCollectionService
.withTransaction(m)
.retrieve(payColId)
})
// TODO: complete authorization
}
try {
const body = req.body
const authId = body.resource.id
const auth = await paypalService.retrieveAuthorization(authId)
const order = await paypalService.retrieveOrderFromAuth(auth)
const purchaseUnit = order.purchase_units[0]
const customId = purchaseUnit.custom_id
if (!customId) {
res.sendStatus(200)
return
}
if (isPaymentCollection(customId)) {
await autorizePaymentCollection(req, customId)
} else {
await autorizeCart(req, customId)
}
res.sendStatus(200)
} catch (err) {
@@ -118,6 +118,34 @@ class PayPalProviderService extends PaymentService {
return { id: res.result.id }
}
async createPaymentNew(paymentInput) {
const { resource_id, currency_code, amount } = paymentInput
const request = new PayPal.orders.OrdersCreateRequest()
request.requestBody({
intent: "AUTHORIZE",
application_context: {
shipping_preference: "NO_SHIPPING",
},
purchase_units: [
{
custom_id: resource_id,
amount: {
currency_code: currency_code.toUpperCase(),
value: roundToTwo(
humanizeAmount(amount, currency_code),
currency_code
),
},
},
],
})
const res = await this.paypal_.execute(request)
return { id: res.result.id }
}
/**
* Retrieves a PayPal order.
* @param {object} data - the data stored with the payment
@@ -216,6 +244,35 @@ class PayPalProviderService extends PaymentService {
}
}
async updatePaymentNew(sessionData, paymentInput) {
try {
const { currency_code, amount } = paymentInput
const request = new PayPal.orders.OrdersPatchRequest(sessionData.id)
request.requestBody([
{
op: "replace",
path: "/purchase_units/@reference_id=='default'",
value: {
amount: {
currency_code: currency_code.toUpperCase(),
value: roundToTwo(
humanizeAmount(amount, currency_code),
currency_code
),
},
},
},
])
await this.paypal_.execute(request)
return sessionData
} catch (error) {
return this.createPaymentNew(paymentInput)
}
}
/**
* Not suported
*/
@@ -10,40 +10,53 @@ export default async (req, res) => {
return
}
const paymentIntent = event.data.object
const manager = req.scope.resolve("manager")
const cartService = req.scope.resolve("cartService")
const orderService = req.scope.resolve("orderService")
const cartId = paymentIntent.metadata.cart_id
const order = await orderService
.retrieveByCartId(cartId)
.catch(() => undefined)
// handle payment intent events
switch (event.type) {
case "payment_intent.succeeded":
if (order && order.payment_status !== "captured") {
await manager.transaction(async (manager) => {
await orderService.withTransaction(manager).capturePayment(order.id)
})
}
break
case "payment_intent.amount_capturable_updated":
if (!order) {
await manager.transaction(async (manager) => {
const cartServiceTx = cartService.withTransaction(manager)
await cartServiceTx.setPaymentSession(cartId, "stripe")
await cartServiceTx.authorizePayment(cartId)
await orderService.withTransaction(manager).createFromCart(cartId)
})
}
break
default:
res.sendStatus(204)
return
function isPaymentCollection(id) {
return id && id.startsWith("paycol")
}
res.sendStatus(200)
async function handleCartPayments(event, req, res, cartId) {
const manager = req.scope.resolve("manager")
const cartService = req.scope.resolve("cartService")
const orderService = req.scope.resolve("orderService")
const order = await orderService
.retrieveByCartId(cartId)
.catch(() => undefined)
// handle payment intent events
switch (event.type) {
case "payment_intent.succeeded":
if (order && order.payment_status !== "captured") {
await manager.transaction(async (manager) => {
await orderService.withTransaction(manager).capturePayment(order.id)
})
}
break
case "payment_intent.amount_capturable_updated":
if (!order) {
await manager.transaction(async (manager) => {
const cartServiceTx = cartService.withTransaction(manager)
await cartServiceTx.setPaymentSession(cartId, "stripe")
await cartServiceTx.authorizePayment(cartId)
await orderService.withTransaction(manager).createFromCart(cartId)
})
}
break
default:
res.sendStatus(204)
return
}
res.sendStatus(200)
}
const paymentIntent = event.data.object
const cartId = paymentIntent.metadata.cart_id
const resourceId = paymentIntent.metadata.resource_id
if (isPaymentCollection(resourceId)) {
// TODO: handle payment collection
} else {
await handleCartPayments(event, req, res, resourceId ?? cartId)
}
}
@@ -0,0 +1,235 @@
import Stripe from "stripe"
import { AbstractPaymentService, PaymentSessionData } from "@medusajs/medusa"
class StripeBase extends AbstractPaymentService {
static identifier = null
constructor(
{
stripeProviderService,
customerService,
totalsService,
regionService,
manager,
},
options,
paymentMethodTypes
) {
super(
{
stripeProviderService,
customerService,
totalsService,
regionService,
manager,
},
options
)
/** @private @const {string[]} */
this.paymentMethodTypes = paymentMethodTypes
/**
* Required Stripe options:
* {
* api_key: "stripe_secret_key", REQUIRED
* webhook_secret: "stripe_webhook_secret", REQUIRED
* // Use this flag to capture payment immediately (default is false)
* capture: true
* }
*/
this.options_ = options
/** @private @const {Stripe} */
this.stripe_ = Stripe(options.api_key)
/** @private @const {CustomerService} */
this.stripeProviderService_ = stripeProviderService
/** @private @const {CustomerService} */
this.customerService_ = customerService
/** @private @const {RegionService} */
this.regionService_ = regionService
/** @private @const {TotalsService} */
this.totalsService_ = totalsService
/** @private @const {EntityManager} */
this.manager_ = manager
}
/**
* Fetches Stripe payment intent. Check its status and returns the
* corresponding Medusa status.
* @param {PaymentSessionData} paymentSessionData - payment method data from cart
* @return {Promise<PaymentSessionStatus>} the status of the payment intent
*/
async getStatus(paymentSessionData) {
return await this.stripeProviderService_.getStatus(paymentSessionData)
}
/**
* Fetches a customers saved payment methods if registered in Stripe.
* @param {object} customer - customer to fetch saved cards for
* @return {Promise<Data[]>} saved payments methods
*/
async retrieveSavedMethods(customer) {
return Promise.resolve([])
}
/**
* Fetches a Stripe customer
* @param {string} customerId - Stripe customer id
* @return {Promise<object>} Stripe customer
*/
async retrieveCustomer(customerId) {
return await this.stripeProviderService_.retrieveCustomer(customerId)
}
/**
* Creates a Stripe customer using a Medusa customer.
* @param {object} customer - Customer data from Medusa
* @return {Promise<object>} Stripe customer
*/
async createCustomer(customer) {
return await this.stripeProviderService_
.withTransaction(this.manager_)
.createCustomer(customer)
}
/**
* Creates a Stripe payment intent.
* If customer is not registered in Stripe, we do so.
* @param {Cart} cart - cart to create a payment for
* @return {Promise<PaymentSessionData>} Stripe payment intent
*/
async createPayment(cart) {
const intentRequest = {
payment_method_types: this.paymentMethodTypes,
capture_method: "automatic",
}
return await this.stripeProviderService_.createPayment(cart, intentRequest)
}
async createPaymentNew(paymentInput) {
const intentRequest = {
payment_method_types: this.paymentMethodTypes,
capture_method: "automatic",
}
return await this.stripeProviderService_.createPaymentNew(
paymentInput,
intentRequest
)
}
/**
* Retrieves Stripe payment intent.
* @param {PaymentData} paymentData - the data of the payment to retrieve
* @return {Promise<Data>} Stripe payment intent
*/
async retrievePayment(paymentData) {
return await this.stripeProviderService_.retrievePayment(paymentData)
}
/**
* Gets a Stripe payment intent and returns it.
* @param {PaymentSession} paymentSession - the data of the payment to retrieve
* @return {Promise<PaymentData>} Stripe payment intent
*/
async getPaymentData(paymentSession) {
return await this.stripeProviderService_.getPaymentData(paymentSession)
}
/**
* Authorizes Stripe payment intent by simply returning
* the status for the payment intent in use.
* @param {PaymentSession} paymentSession - payment session data
* @param {object} context - properties relevant to current context
* @return {Promise<{data: PaymentSessionData; status: PaymentSessionStatus}>} result with data and status
*/
async authorizePayment(paymentSession, context = {}) {
return await this.stripeProviderService_.authorizePayment(
paymentSession,
context
)
}
async updatePaymentData(paymentSessionData, data) {
return await this.stripeProviderService_.updatePaymentData(
paymentSessionData,
data
)
}
/**
* Updates Stripe payment intent.
* @param {PaymentSessionData} paymentSessionData - payment session data.
* @param {Cart} cart
* @return {Promise<PaymentSessionData>} Stripe payment intent
*/
async updatePayment(paymentSessionData, cart) {
return await this.stripeProviderService_.updatePayment(
paymentSessionData,
cart
)
}
async updatePaymentNew(paymentSessionData, paymentInput) {
return await this.stripeProviderService_.updatePaymentNew(
paymentSessionData,
paymentInput
)
}
async deletePayment(paymentSession) {
return await this.stripeProviderService_.deletePayment(paymentSession)
}
/**
* Updates customer of Stripe payment intent.
* @param {string} paymentIntentId - id of payment intent to update
* @param {string} customerId - id of new Stripe customer
* @return {object} Stripe payment intent
*/
async updatePaymentIntentCustomer(paymentIntentId, customerId) {
return await this.stripeProviderService_.updatePaymentIntentCustomer(
paymentIntentId,
customerId
)
}
/**
* Captures payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @return {Promise<PaymentData>} Stripe payment intent
*/
async capturePayment(payment) {
return await this.stripeProviderService_.capturePayment(payment)
}
/**
* Refunds payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @param {number} refundAmount - amount to refund
* @return {Promise<PaymentData>} refunded payment intent
*/
async refundPayment(payment, refundAmount) {
return await this.stripeProviderService_.refundPayment(
payment,
refundAmount
)
}
/**
* Cancels payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @return {Promise<PaymentData>} canceled payment intent
*/
async cancelPayment(payment) {
return await this.stripeProviderService_.cancelPayment(payment)
}
}
export default StripeBase
@@ -1,7 +1,6 @@
import Stripe from "stripe"
import { AbstractPaymentService, PaymentSessionData } from "@medusajs/medusa"
import StripeBase from "../helpers/stripe-base"
class BancontactProviderService extends AbstractPaymentService {
class BancontactProviderService extends StripeBase {
static identifier = "stripe-bancontact"
constructor(
@@ -22,245 +21,9 @@ class BancontactProviderService extends AbstractPaymentService {
regionService,
manager,
},
options
options,
["bancontact"]
)
/**
* Required Stripe options:
* {
* api_key: "stripe_secret_key", REQUIRED
* webhook_secret: "stripe_webhook_secret", REQUIRED
* // Use this flag to capture payment immediately (default is false)
* capture: true
* }
*/
this.options_ = options
/** @private @const {Stripe} */
this.stripe_ = Stripe(options.api_key)
/** @private @const {CustomerService} */
this.stripeProviderService_ = stripeProviderService
/** @private @const {CustomerService} */
this.customerService_ = customerService
/** @private @const {RegionService} */
this.regionService_ = regionService
/** @private @const {TotalsService} */
this.totalsService_ = totalsService
/** @private @const {EntityManager} */
this.manager_ = manager
}
/**
* Fetches Stripe payment intent. Check its status and returns the
* corresponding Medusa status.
* @param {PaymentSessionData} paymentSessionData - payment method data from cart
* @return {Promise<PaymentSessionStatus>} the status of the payment intent
*/
async getStatus(paymentSessionData) {
return await this.stripeProviderService_.getStatus(paymentSessionData)
}
/**
* Fetches a customers saved payment methods if registered in Stripe.
* @param {object} customer - customer to fetch saved cards for
* @return {Promise<Data[]>} saved payments methods
*/
async retrieveSavedMethods(customer) {
return Promise.resolve([])
}
/**
* Fetches a Stripe customer
* @param {string} customerId - Stripe customer id
* @return {Promise<object>} Stripe customer
*/
async retrieveCustomer(customerId) {
return await this.stripeProviderService_.retrieveCustomer(customerId)
}
/**
* Creates a Stripe customer using a Medusa customer.
* @param {object} customer - Customer data from Medusa
* @return {Promise<object>} Stripe customer
*/
async createCustomer(customer) {
return await this.stripeProviderService_
.withTransaction(this.manager_)
.createCustomer(customer)
}
/**
* Creates a Stripe payment intent.
* If customer is not registered in Stripe, we do so.
* @param {Cart} cart - cart to create a payment for
* @return {Promise<PaymentSessionData>} Stripe payment intent
*/
async createPayment(cart) {
const { customer_id, region_id, email } = cart
const region = await this.regionService_
.withTransaction(this.manager_)
.retrieve(region_id)
const { currency_code } = region
const amount = await this.totalsService_
.withTransaction(this.manager_)
.getTotal(cart)
const intentRequest = {
amount: Math.round(amount),
description:
cart?.context?.payment_description ?? this.options?.payment_description,
currency: currency_code,
payment_method_types: ["bancontact"],
capture_method: "automatic",
metadata: { cart_id: `${cart.id}` },
}
if (customer_id) {
const customer = await this.customerService_
.withTransaction(this.manager_)
.retrieve(customer_id)
if (customer.metadata?.stripe_id) {
intentRequest.customer = customer.metadata.stripe_id
} else {
const stripeCustomer = await this.createCustomer({
email,
id: customer_id,
})
intentRequest.customer = stripeCustomer.id
}
} else {
const stripeCustomer = await this.createCustomer({
email,
})
intentRequest.customer = stripeCustomer.id
}
return await this.stripe_.paymentIntents.create(intentRequest)
}
/**
* Retrieves Stripe payment intent.
* @param {PaymentData} paymentData - the data of the payment to retrieve
* @return {Promise<Data>} Stripe payment intent
*/
async retrievePayment(paymentData) {
return await this.stripeProviderService_.retrievePayment(paymentData)
}
/**
* Gets a Stripe payment intent and returns it.
* @param {PaymentSession} paymentSession - the data of the payment to retrieve
* @return {Promise<PaymentData>} Stripe payment intent
*/
async getPaymentData(paymentSession) {
return await this.stripeProviderService_.getPaymentData(paymentSession)
}
/**
* Authorizes Stripe payment intent by simply returning
* the status for the payment intent in use.
* @param {PaymentSession} paymentSession - payment session data
* @param {object} context - properties relevant to current context
* @return {Promise<{data: PaymentSessionData; status: PaymentSessionStatus}>} result with data and status
*/
async authorizePayment(paymentSession, context = {}) {
return await this.stripeProviderService_.authorizePayment(
paymentSession,
context
)
}
async updatePaymentData(paymentSessionData, data) {
return await this.stripeProviderService_.updatePaymentData(
paymentSessionData,
data
)
}
/**
* Updates Stripe payment intent.
* @param {PaymentSessionData} paymentSessionData - payment session data.
* @param {Cart} cart
* @return {Promise<PaymentSessionData>} Stripe payment intent
*/
async updatePayment(paymentSessionData, cart) {
try {
const stripeId = cart.customer?.metadata?.stripe_id || undefined
if (stripeId !== paymentSessionData.customer) {
return this.createPayment(cart)
} else {
if (
cart.total &&
paymentSessionData.amount === Math.round(cart.total)
) {
return sessionData
}
return this.stripe_.paymentIntents.update(paymentSessionData.id, {
amount: Math.round(cart.total),
})
}
} catch (error) {
throw error
}
}
async deletePayment(paymentSession) {
return await this.stripeProviderService_.deletePayment(paymentSession)
}
/**
* Updates customer of Stripe payment intent.
* @param {string} paymentIntentId - id of payment intent to update
* @param {string} customerId - id of new Stripe customer
* @return {object} Stripe payment intent
*/
async updatePaymentIntentCustomer(paymentIntentId, customerId) {
return await this.stripeProviderService_.updatePaymentIntentCustomer(
paymentIntentId,
customerId
)
}
/**
* Captures payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @return {Promise<PaymentData>} Stripe payment intent
*/
async capturePayment(payment) {
return await this.stripeProviderService_.capturePayment(payment)
}
/**
* Refunds payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @param {number} refundAmount - amount to refund
* @return {Promise<PaymentData>} refunded payment intent
*/
async refundPayment(payment, refundAmount) {
return await this.stripeProviderService_.refundPayment(
payment,
refundAmount
)
}
/**
* Cancels payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @return {Promise<PaymentData>} canceled payment intent
*/
async cancelPayment(payment) {
return await this.stripeProviderService_.cancelPayment(payment)
}
}
@@ -1,7 +1,6 @@
import Stripe from "stripe"
import { AbstractPaymentService, PaymentSessionStatus } from "@medusajs/medusa"
import StripeBase from "../helpers/stripe-base"
class BlikProviderService extends AbstractPaymentService {
class BlikProviderService extends StripeBase {
static identifier = "stripe-blik"
constructor(
@@ -22,242 +21,9 @@ class BlikProviderService extends AbstractPaymentService {
regionService,
manager,
},
options
options,
["blik"]
)
/**
* Required Stripe options:
* {
* api_key: "stripe_secret_key", REQUIRED
* webhook_secret: "stripe_webhook_secret", REQUIRED
* // Use this flag to capture payment immediately (default is false)
* capture: true
* }
*/
this.options_ = options
/** @private @const {Stripe} */
this.stripe_ = Stripe(options.api_key)
/** @private @const {CustomerService} */
this.stripeProviderService_ = stripeProviderService
/** @private @const {CustomerService} */
this.customerService_ = customerService
/** @private @const {RegionService} */
this.regionService_ = regionService
/** @private @const {TotalsService} */
this.totalsService_ = totalsService
this.manager_ = manager
}
/**
* Fetches Stripe payment intent. Check its status and returns the
* corresponding Medusa status.
* @param {PaymentSessionData} paymentSessionData - payment method data from cart
* @return {Promise<PaymentSessionStatus>} the status of the payment intent
*/
async getStatus(paymentSessionData) {
return await this.stripeProviderService_.getStatus(paymentSessionData)
}
/**
* Fetches a customers saved payment methods if registered in Stripe.
* @param {object} customer - customer to fetch saved cards for
* @return {Promise<Data[]>} saved payments methods
*/
async retrieveSavedMethods(customer) {
return Promise.resolve([])
}
/**
* Fetches a Stripe customer
* @param {string} customerId - Stripe customer id
* @return {Promise<object>} Stripe customer
*/
async retrieveCustomer(customerId) {
return await this.stripeProviderService_.retrieveCustomer(customerId)
}
/**
* Creates a Stripe customer using a Medusa customer.
* @param {object} customer - Customer data from Medusa
* @return {Promise<object>} Stripe customer
*/
async createCustomer(customer) {
return await this.stripeProviderService_
.withTransaction(this.manager_)
.createCustomer(customer)
}
/**
* Creates a Stripe payment intent.
* If customer is not registered in Stripe, we do so.
* @param {Cart} cart - cart to create a payment for
* @returns {PaymentSessionData} Stripe payment intent
*/
async createPayment(cart) {
const { customer_id, region_id, email } = cart
const region = await this.regionService_
.withTransaction(this.manager_)
.retrieve(region_id)
const { currency_code } = region
const amount = await this.totalsService_
.withTransaction(this.manager_)
.getTotal(cart)
const intentRequest = {
amount: Math.round(amount),
currency: currency_code,
payment_method_types: ["blik"],
capture_method: "automatic",
metadata: { cart_id: `${cart.id}` },
}
if (customer_id) {
const customer = await this.customerService_
.withTransaction(this.manager_)
.retrieve(customer_id)
if (customer.metadata?.stripe_id) {
intentRequest.customer = customer.metadata.stripe_id
} else {
const stripeCustomer = await this.createCustomer({
email,
id: customer_id,
})
intentRequest.customer = stripeCustomer.id
}
} else {
const stripeCustomer = await this.createCustomer({
email,
})
intentRequest.customer = stripeCustomer.id
}
return await this.stripe_.paymentIntents.create(intentRequest)
}
/**
* Retrieves Stripe payment intent.
* @param {PaymentData} paymentData - the data of the payment to retrieve
* @return {Promise<Data>} Stripe payment intent
*/
async retrievePayment(paymentData) {
return await this.stripeProviderService_.retrievePayment(paymentData)
}
/**
* Gets a Stripe payment intent and returns it.
* @param {PaymentSession} paymentSession - the data of the payment to retrieve
* @return {Promise<PaymentData>} Stripe payment intent
*/
async getPaymentData(paymentSession) {
return await this.stripeProviderService_.getPaymentData(paymentSession)
}
/**
* Authorizes Stripe payment intent by simply returning
* the status for the payment intent in use.
* @param {PaymentSession} paymentSession - payment session data
* @param {object} context - properties relevant to current context
* @return {Promise<{data: PaymentSessionData; status: PaymentSessionStatus}>} result with data and status
*/
async authorizePayment(paymentSession, context = {}) {
return await this.stripeProviderService_.authorizePayment(
paymentSession,
context
)
}
async updatePaymentData(paymentSessionData, data) {
return await this.stripeProviderService_.updatePaymentData(
paymentSessionData,
data
)
}
/**
* Updates Stripe payment intent.
* @param {PaymentSessionData} paymentSessionData - payment session data.
* @param {Cart} cart
* @return {Promise<PaymentSessionData>} Stripe payment intent
*/
async updatePayment(paymentSessionData, cart) {
try {
const stripeId = cart.customer?.metadata?.stripe_id || undefined
if (stripeId !== paymentSessionData.customer) {
return this.createPayment(cart)
} else {
if (
cart.total &&
paymentSessionData.amount === Math.round(cart.total)
) {
return sessionData
}
return this.stripe_.paymentIntents.update(paymentSessionData.id, {
amount: Math.round(cart.total),
})
}
} catch (error) {
throw error
}
}
async deletePayment(paymentSession) {
return await this.stripeProviderService_.deletePayment(paymentSession)
}
/**
* Updates customer of Stripe payment intent.
* @param {string} paymentIntentId - id of payment intent to update
* @param {string} customerId - id of new Stripe customer
* @return {object} Stripe payment intent
*/
async updatePaymentIntentCustomer(paymentIntentId, customerId) {
return await this.stripeProviderService_.updatePaymentIntentCustomer(
paymentIntentId,
customerId
)
}
/**
* Captures payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @return {Promise<PaymentData>} Stripe payment intent
*/
async capturePayment(payment) {
return await this.stripeProviderService_.capturePayment(payment)
}
/**
* Refunds payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @param {number} refundAmount - amount to refund
* @return {Promise<PaymentData>} refunded payment intent
*/
async refundPayment(payment, refundAmount) {
return await this.stripeProviderService_.refundPayment(
payment,
refundAmount
)
}
/**
* Cancels payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @return {Promise<PaymentData>} canceled payment intent
*/
async cancelPayment(payment) {
return await this.stripeProviderService_.cancelPayment(payment)
}
}
@@ -1,7 +1,6 @@
import Stripe from "stripe"
import { AbstractPaymentService, PaymentSessionStatus } from "@medusajs/medusa"
import StripeBase from "../helpers/stripe-base"
class GiropayProviderService extends AbstractPaymentService {
class GiropayProviderService extends StripeBase {
static identifier = "stripe-giropay"
constructor(
@@ -22,245 +21,9 @@ class GiropayProviderService extends AbstractPaymentService {
regionService,
manager,
},
options
options,
["giropay"]
)
/**
* Required Stripe options:
* {
* api_key: "stripe_secret_key", REQUIRED
* webhook_secret: "stripe_webhook_secret", REQUIRED
* // Use this flag to capture payment immediately (default is false)
* capture: true
* }
*/
this.options_ = options
/** @private @const {Stripe} */
this.stripe_ = Stripe(options.api_key)
/** @private @const {CustomerService} */
this.stripeProviderService_ = stripeProviderService
/** @private @const {CustomerService} */
this.customerService_ = customerService
/** @private @const {RegionService} */
this.regionService_ = regionService
/** @private @const {TotalsService} */
this.totalsService_ = totalsService
/** @private @const {EntityManager} */
this.manager_ = manager
}
/**
* Fetches Stripe payment intent. Check its status and returns the
* corresponding Medusa status.
* @param {PaymentSessionData} paymentSessionData - payment method data from cart
* @return {Promise<PaymentSessionStatus>} the status of the payment intent
*/
async getStatus(paymentSessionData) {
return await this.stripeProviderService_.getStatus(paymentSessionData)
}
/**
* Fetches a customers saved payment methods if registered in Stripe.
* @param {Customer} customer - customer to fetch saved cards for
* @return {Promise<Data[]>} saved payments methods
*/
async retrieveSavedMethods(customer) {
return Promise.resolve([])
}
/**
* Fetches a Stripe customer
* @param {string} customerId - Stripe customer id
* @return {Promise<object>} Stripe customer
*/
async retrieveCustomer(customerId) {
return await this.stripeProviderService_.retrieveCustomer(customerId)
}
/**
* Creates a Stripe customer using a Medusa customer.
* @param {object} customer - Customer data from Medusa
* @return {Promise<object>} Stripe customer
*/
async createCustomer(customer) {
return await this.stripeProviderService_
.withTransaction(this.manager_)
.createCustomer(customer)
}
/**
* Creates a Stripe payment intent.
* If customer is not registered in Stripe, we do so.
* @param {Cart} cart - cart to create a payment for
* @return {Promise<PaymentSessionData>} Stripe payment intent
*/
async createPayment(cart) {
const { customer_id, region_id, email } = cart
const region = await this.regionService_
.withTransaction(this.manager_)
.retrieve(region_id)
const { currency_code } = region
const amount = await this.totalsService_
.withTransaction(this.manager_)
.getTotal(cart)
const intentRequest = {
amount: Math.round(amount),
description:
cart?.context?.payment_description ?? this.options?.payment_description,
currency: currency_code,
payment_method_types: ["giropay"],
capture_method: "automatic",
metadata: { cart_id: `${cart.id}` },
}
if (customer_id) {
const customer = await this.customerService_
.withTransaction(this.manager_)
.retrieve(customer_id)
if (customer.metadata?.stripe_id) {
intentRequest.customer = customer.metadata.stripe_id
} else {
const stripeCustomer = await this.createCustomer({
email,
id: customer_id,
})
intentRequest.customer = stripeCustomer.id
}
} else {
const stripeCustomer = await this.createCustomer({
email,
})
intentRequest.customer = stripeCustomer.id
}
return await this.stripe_.paymentIntents.create(intentRequest)
}
/**
* Retrieves Stripe payment intent.
* @param {PaymentData} paymentData - the data of the payment to retrieve
* @return {Promise<Data>} Stripe payment intent
*/
async retrievePayment(paymentData) {
return await this.stripeProviderService_.retrievePayment(paymentData)
}
/**
* Gets a Stripe payment intent and returns it.
* @param {PaymentSession} paymentSession - the data of the payment to retrieve
* @return {Promise<PaymentData>} Stripe payment intent
*/
async getPaymentData(paymentSession) {
return await this.stripeProviderService_.getPaymentData(paymentSession)
}
/**
* Authorizes Stripe payment intent by simply returning
* the status for the payment intent in use.
* @param {PaymentSession} paymentSession - payment session data
* @param {Data} context - properties relevant to current context
* @return {Promise<{data: PaymentSessionData; status: PaymentSessionStatus}>} result with data and status
*/
async authorizePayment(paymentSession, context = {}) {
return await this.stripeProviderService_.authorizePayment(
paymentSession,
context
)
}
async updatePaymentData(paymentSessionData, data) {
return await this.stripeProviderService_.updatePaymentData(
paymentSessionData,
data
)
}
/**
* Updates Stripe payment intent.
* @param {PaymentSessionData} paymentSessionData - payment session data.
* @param {Cart} cart
* @return {Promise<PaymentSessionData>} Stripe payment intent
*/
async updatePayment(paymentSessionData, cart) {
try {
const stripeId = cart.customer?.metadata?.stripe_id || undefined
if (stripeId !== paymentSessionData.customer) {
return this.createPayment(cart)
} else {
if (
cart.total &&
paymentSessionData.amount === Math.round(cart.total)
) {
return paymentSessionData
}
return this.stripe_.paymentIntents.update(paymentSessionData.id, {
amount: Math.round(cart.total),
})
}
} catch (error) {
throw error
}
}
async deletePayment(paymentSession) {
return await this.stripeProviderService_.deletePayment(paymentSession)
}
/**
* Updates customer of Stripe payment intent.
* @param {string} paymentIntentId - id of payment intent to update
* @param {string} customerId - id of new Stripe customer
* @return {object} Stripe payment intent
*/
async updatePaymentIntentCustomer(paymentIntentId, customerId) {
return await this.stripeProviderService_.updatePaymentIntentCustomer(
paymentIntentId,
customerId
)
}
/**
* Captures payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @return {Promise<PaymentData>} Stripe payment intent
*/
async capturePayment(payment) {
return await this.stripeProviderService_.capturePayment(payment)
}
/**
* Refunds payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @param {number} refundAmount - amount to refund
* @return {Promise<PaymentData>} refunded payment intent
*/
async refundPayment(payment, refundAmount) {
return await this.stripeProviderService_.refundPayment(
payment,
refundAmount
)
}
/**
* Cancels payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @return {Promise<PaymentData>} canceled payment intent
*/
async cancelPayment(payment) {
return await this.stripeProviderService_.cancelPayment(payment)
}
}
@@ -1,7 +1,6 @@
import Stripe from "stripe"
import { PaymentService } from "medusa-interfaces"
import StripeBase from "../helpers/stripe-base"
class IdealProviderService extends PaymentService {
class IdealProviderService extends StripeBase {
static identifier = "stripe-ideal"
constructor(
@@ -22,242 +21,9 @@ class IdealProviderService extends PaymentService {
regionService,
manager,
},
options
options,
["ideal"]
)
/**
* Required Stripe options:
* {
* api_key: "stripe_secret_key", REQUIRED
* webhook_secret: "stripe_webhook_secret", REQUIRED
* // Use this flag to capture payment immediately (default is false)
* capture: true
* }
*/
this.options_ = options
/** @private @const {Stripe} */
this.stripe_ = Stripe(options.api_key)
/** @private @const {CustomerService} */
this.stripeProviderService_ = stripeProviderService
/** @private @const {CustomerService} */
this.customerService_ = customerService
/** @private @const {RegionService} */
this.regionService_ = regionService
/** @private @const {TotalsService} */
this.totalsService_ = totalsService
this.manager_ = manager
}
/**
* Fetches Stripe payment intent. Check its status and returns the
* corresponding Medusa status.
* @param {PaymentSessionData} paymentSessionData - payment method data from cart
* @return {Promise<PaymentSessionStatus>} the status of the payment intent
*/
async getStatus(paymentData) {
return await this.stripeProviderService_.getStatus(paymentData)
}
/**
* Fetches a customers saved payment methods if registered in Stripe.
* @param {object} customer - customer to fetch saved cards for
* @return {Promise<Data[]>} saved payments methods
*/
async retrieveSavedMethods(customer) {
return Promise.resolve([])
}
/**
* Fetches a Stripe customer
* @param {string} customerId - Stripe customer id
* @return {Promise<object>} Stripe customer
*/
async retrieveCustomer(customerId) {
return await this.stripeProviderService_.retrieveCustomer(customerId)
}
/**
* Creates a Stripe customer using a Medusa customer.
* @param {object} customer - Customer data from Medusa
* @return {Promise<object>} Stripe customer
*/
async createCustomer(customer) {
return await this.stripeProviderService_
.withTransaction(this.manager_)
.createCustomer(customer)
}
/**
* Creates a Stripe payment intent.
* If customer is not registered in Stripe, we do so.
* @param {Cart} cart - cart to create a payment for
* @return {Promise<PaymentSessionData>} Stripe payment intent
*/
async createPayment(cart) {
const { customer_id, region_id, email } = cart
const region = await this.regionService_
.withTransaction(this.manager_)
.retrieve(region_id)
const { currency_code } = region
const amount = await this.totalsService_
.withTransaction(this.manager_)
.getTotal(cart)
const intentRequest = {
amount: Math.round(amount),
description:
cart?.context?.payment_description ??
this.options_?.payment_description,
currency: currency_code,
payment_method_types: ["ideal"],
capture_method: "automatic",
metadata: { cart_id: `${cart.id}` },
}
if (customer_id) {
const customer = await this.customerService_
.withTransaction(this.manager_)
.retrieve(customer_id)
if (customer.metadata?.stripe_id) {
intentRequest.customer = customer.metadata.stripe_id
} else {
const stripeCustomer = await this.createCustomer({
email,
id: customer_id,
})
intentRequest.customer = stripeCustomer.id
}
} else {
const stripeCustomer = await this.createCustomer({
email,
})
intentRequest.customer = stripeCustomer.id
}
return await this.stripe_.paymentIntents.create(intentRequest)
}
/**
* Retrieves Stripe payment intent.
* @param {PaymentData} paymentData - the data of the payment to retrieve
* @return {Promise<Data>} Stripe payment intent
*/
async retrievePayment(paymentData) {
return await this.stripeProviderService_.retrievePayment(paymentData)
}
/**
* Gets a Stripe payment intent and returns it.
* @param {PaymentSession} paymentSession - the data of the payment to retrieve
* @return {Promise<PaymentData>} Stripe payment intent
*/
async getPaymentData(paymentSession) {
return await this.stripeProviderService_.getPaymentData(paymentSession)
}
/**
* Authorizes Stripe payment intent by simply returning
* the status for the payment intent in use.
* @param {PaymentSession} paymentSession - payment session data
* @param {object} context - properties relevant to current context
* @return {Promise<{data: PaymentSessionData; status: PaymentSessionStatus}>} result with data and status
*/
async authorizePayment(paymentSession, context = {}) {
return await this.stripeProviderService_.authorizePayment(
paymentSession,
context
)
}
async updatePaymentData(paymentSessionData, data) {
return await this.stripeProviderService_.updatePaymentData(
paymentSessionData,
data
)
}
/**
* Updates Stripe payment intent.
* @param {PaymentSessionData} paymentSessionData - payment session data.
* @param {Cart} cart
* @return {Promise<PaymentSessionData>} Stripe payment intent
*/
async updatePayment(sessionData, cart) {
try {
const stripeId = cart.customer?.metadata?.stripe_id || undefined
if (stripeId !== sessionData.customer) {
return this.createPayment(cart)
} else {
if (cart.total && sessionData.amount === Math.round(cart.total)) {
return sessionData
}
return this.stripe_.paymentIntents.update(sessionData.id, {
amount: Math.round(cart.total),
})
}
} catch (error) {
throw error
}
}
async deletePayment(paymentSession) {
return await this.stripeProviderService_.deletePayment(paymentSession)
}
/**
* Updates customer of Stripe payment intent.
* @param {string} paymentIntentId - id of payment intent to update
* @param {string} customerId - id of new Stripe customer
* @return {object} Stripe payment intent
*/
async updatePaymentIntentCustomer(paymentIntentId, customerId) {
return await this.stripeProviderService_.updatePaymentIntentCustomer(
paymentIntentId,
customerId
)
}
/**
* Captures payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @return {Promise<PaymentData>} Stripe payment intent
*/
async capturePayment(payment) {
return await this.stripeProviderService_.capturePayment(payment)
}
/**
* Refunds payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @param {number} refundAmount - amount to refund
* @return {Promise<PaymentData>} refunded payment intent
*/
async refundPayment(payment, refundAmount) {
return await this.stripeProviderService_.refundPayment(
payment,
refundAmount
)
}
/**
* Cancels payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @return {Promise<PaymentData>} canceled payment intent
*/
async cancelPayment(payment) {
return await this.stripeProviderService_.cancelPayment(payment)
}
}
@@ -129,7 +129,7 @@ class StripeProviderService extends AbstractPaymentService {
* @param {Cart} cart - cart to create a payment for
* @return {Promise<PaymentSessionData>} Stripe payment intent
*/
async createPayment(cart) {
async createPayment(cart, intentRequestData = {}) {
const { customer_id, region_id, email } = cart
const { currency_code } = await this.regionService_
.withTransaction(this.manager_)
@@ -143,9 +143,10 @@ class StripeProviderService extends AbstractPaymentService {
this.options_?.payment_description,
amount: Math.round(amount),
currency: currency_code,
metadata: { cart_id: `${cart.id}` },
setup_future_usage: "on_session",
capture_method: this.options_.capture ? "automatic" : "manual",
metadata: { cart_id: `${cart.id}` },
...intentRequestData,
}
if (this.options_?.automatic_payment_methods) {
@@ -178,6 +179,44 @@ class StripeProviderService extends AbstractPaymentService {
return await this.stripe_.paymentIntents.create(intentRequest)
}
async createPaymentNew(paymentInput, intentRequestData = {}) {
const { customer, currency_code, amount, resource_id, cart } = paymentInput
const { id: customer_id, email } = customer
let intentRequest = {
description:
cart?.context?.payment_description ??
this.options_?.payment_description,
amount: Math.round(amount),
currency: currency_code,
metadata: { resource_id },
setup_future_usage: "on_session",
capture_method: this.options_.capture ? "automatic" : "manual",
...intentRequestData,
}
if (customer_id) {
if (customer.metadata?.stripe_id) {
intentRequest.customer = customer.metadata.stripe_id
} else {
const stripeCustomer = await this.createCustomer({
email,
id: customer_id,
})
intentRequest.customer = stripeCustomer.id
}
} else {
const stripeCustomer = await this.createCustomer({
email,
})
intentRequest.customer = stripeCustomer.id
}
return await this.stripe_.paymentIntents.create(intentRequest)
}
/**
* Retrieves Stripe payment intent.
* @param {PaymentData} paymentData - the data of the payment to retrieve
@@ -213,7 +252,6 @@ class StripeProviderService extends AbstractPaymentService {
*/
async authorizePayment(paymentSession, context = {}) {
const stat = await this.getStatus(paymentSession.data)
try {
return { data: paymentSession.data, status: stat }
} catch (error) {
@@ -242,7 +280,7 @@ class StripeProviderService extends AbstractPaymentService {
const stripeId = cart.customer?.metadata?.stripe_id || undefined
if (stripeId !== sessionData.customer) {
return this.createPayment(cart)
return await this.createPayment(cart)
} else {
if (cart.total && sessionData.amount === Math.round(cart.total)) {
return sessionData
@@ -257,6 +295,26 @@ class StripeProviderService extends AbstractPaymentService {
}
}
async updatePaymentNew(paymentSessionData, paymentInput) {
try {
const stripeId = paymentInput.customer?.metadata?.stripe_id
if (stripeId !== paymentInput.customer_id) {
return await this.createPaymentNew(paymentInput)
} else {
if (paymentSessionData.amount === Math.round(paymentInput.amount)) {
return sessionData
}
return this.stripe_.paymentIntents.update(paymentSessionData.id, {
amount: Math.round(paymentInput.amount),
})
}
} catch (error) {
throw error
}
}
async deletePayment(payment) {
try {
const { id } = payment.data
@@ -1,7 +1,6 @@
import Stripe from "stripe"
import { AbstractPaymentService, PaymentSessionStatus } from "@medusajs/medusa"
import StripeBase from "../helpers/stripe-base"
class Przelewy24ProviderService extends AbstractPaymentService {
class Przelewy24ProviderService extends StripeBase {
static identifier = "stripe-przelewy24"
constructor(
@@ -22,239 +21,9 @@ class Przelewy24ProviderService extends AbstractPaymentService {
regionService,
manager,
},
options
options,
["p24"]
)
/**
* Required Stripe options:
* {
* api_key: "stripe_secret_key", REQUIRED
* webhook_secret: "stripe_webhook_secret", REQUIRED
* // Use this flag to capture payment immediately (default is false)
* capture: true
* }
*/
this.options_ = options
/** @private @const {Stripe} */
this.stripe_ = Stripe(options.api_key)
/** @private @const {CustomerService} */
this.stripeProviderService_ = stripeProviderService
/** @private @const {CustomerService} */
this.customerService_ = customerService
/** @private @const {RegionService} */
this.regionService_ = regionService
/** @private @const {TotalsService} */
this.totalsService_ = totalsService
this.manager_ = manager
}
/**
* Fetches Stripe payment intent. Check its status and returns the
* corresponding Medusa status.
* @param {PaymentSessionData} paymentSessionData - payment method data from cart
* @return {Promise<PaymentSessionStatus>} the status of the payment intent
*/
async getStatus(paymentData) {
return await this.stripeProviderService_.getStatus(paymentData)
}
/**
* Fetches a customers saved payment methods if registered in Stripe.
* @param {object} customer - customer to fetch saved cards for
* @return {Promise<Data[]>} saved payments methods
*/
async retrieveSavedMethods(customer) {
return Promise.resolve([])
}
/**
* Fetches a Stripe customer
* @param {string} customerId - Stripe customer id
* @return {Promise<object>} Stripe customer
*/
async retrieveCustomer(customerId) {
return await this.stripeProviderService_.retrieveCustomer(customerId)
}
/**
* Creates a Stripe customer using a Medusa customer.
* @param {object} customer - Customer data from Medusa
* @return {Promise<object>} Stripe customer
*/
async createCustomer(customer) {
return await this.stripeProviderService_
.withTransaction(this.manager_)
.createCustomer(customer)
}
/**
* Creates a Stripe payment intent.
* If customer is not registered in Stripe, we do so.
* @param {object} cart - cart to create a payment for
* @returns {object} Stripe payment intent
*/
async createPayment(cart) {
const { customer_id, region_id, email } = cart
const region = await this.regionService_
.withTransaction(this.manager_)
.retrieve(region_id)
const { currency_code } = region
const amount = await this.totalsService_
.withTransaction(this.manager_)
.getTotal(cart)
const intentRequest = {
amount: Math.round(amount),
currency: currency_code,
payment_method_types: ["p24"],
capture_method: "automatic",
metadata: { cart_id: `${cart.id}` },
}
if (customer_id) {
const customer = await this.customerService_
.withTransaction(this.manager_)
.retrieve(customer_id)
if (customer.metadata?.stripe_id) {
intentRequest.customer = customer.metadata.stripe_id
} else {
const stripeCustomer = await this.createCustomer({
email,
id: customer_id,
})
intentRequest.customer = stripeCustomer.id
}
} else {
const stripeCustomer = await this.createCustomer({
email,
})
intentRequest.customer = stripeCustomer.id
}
return await this.stripe_.paymentIntents.create(intentRequest)
}
/**
* Retrieves Stripe payment intent.
* @param {PaymentData} paymentData - the data of the payment to retrieve
* @return {Promise<Data>} Stripe payment intent
*/
async retrievePayment(paymentData) {
return await this.stripeProviderService_.retrievePayment(paymentData)
}
/**
* Gets a Stripe payment intent and returns it.
* @param {PaymentSession} paymentSession - the data of the payment to retrieve
* @return {Promise<PaymentData>} Stripe payment intent
*/
async getPaymentData(paymentSession) {
return await this.stripeProviderService_.getPaymentData(paymentSession)
}
/**
* Authorizes Stripe payment intent by simply returning
* the status for the payment intent in use.
* @param {PaymentSession} paymentSession - payment session data
* @param {object} context - properties relevant to current context
* @return {Promise<{data: PaymentSessionData; status: PaymentSessionStatus}>} result with data and status
*/
async authorizePayment(paymentSession, context = {}) {
return await this.stripeProviderService_.authorizePayment(
paymentSession,
context
)
}
async updatePaymentData(paymentSessionData, data) {
return await this.stripeProviderService_.updatePaymentData(
paymentSessionData,
data
)
}
/**
* Updates Stripe payment intent.
* @param {PaymentSessionData} paymentSessionData - payment session data.
* @param {Cart} cart
* @return {Promise<PaymentSessionData>} Stripe payment intent
*/
async updatePayment(sessionData, cart) {
try {
const stripeId = cart.customer?.metadata?.stripe_id || undefined
if (stripeId !== sessionData.customer) {
return this.createPayment(cart)
} else {
if (cart.total && sessionData.amount === Math.round(cart.total)) {
return sessionData
}
return this.stripe_.paymentIntents.update(sessionData.id, {
amount: Math.round(cart.total),
})
}
} catch (error) {
throw error
}
}
async deletePayment(paymentSession) {
return await this.stripeProviderService_.deletePayment(paymentSession)
}
/**
* Updates customer of Stripe payment intent.
* @param {string} paymentIntentId - id of payment intent to update
* @param {string} customerId - id of new Stripe customer
* @return {object} Stripe payment intent
*/
async updatePaymentIntentCustomer(paymentIntentId, customerId) {
return await this.stripeProviderService_.updatePaymentIntentCustomer(
paymentIntentId,
customerId
)
}
/**
* Captures payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @return {Promise<PaymentData>} Stripe payment intent
*/
async capturePayment(payment) {
return await this.stripeProviderService_.capturePayment(payment)
}
/**
* Refunds payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @param {number} refundAmount - amount to refund
* @return {Promise<PaymentData>} refunded payment intent
*/
async refundPayment(payment, refundAmount) {
return await this.stripeProviderService_.refundPayment(
payment,
refundAmount
)
}
/**
* Cancels payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @return {Promise<PaymentData>} canceled payment intent
*/
async cancelPayment(payment) {
return await this.stripeProviderService_.cancelPayment(payment)
}
}
@@ -7,6 +7,7 @@ import {
PaymentSessionStatus,
} from "../models"
import { PaymentService } from "medusa-interfaces"
import { PaymentProviderDataInput } from "../types/payment-collection"
export type Data = Record<string, unknown>
export type PaymentData = Data
@@ -76,6 +77,9 @@ export abstract class AbstractPaymentService
): Promise<PaymentSessionData>
public abstract createPayment(cart: Cart): Promise<PaymentSessionData>
public abstract createPaymentNew(
paymentInput: PaymentProviderDataInput
): Promise<PaymentSessionData>
public abstract retrievePayment(paymentData: PaymentData): Promise<Data>
@@ -84,6 +88,11 @@ export abstract class AbstractPaymentService
cart: Cart
): Promise<PaymentSessionData>
public abstract updatePaymentNew(
paymentSessionData: PaymentSessionData,
paymentInput: PaymentProviderDataInput
): Promise<PaymentSessionData>
public abstract authorizePayment(
paymentSession: PaymentSession,
context: Data
@@ -27,6 +27,7 @@ export class paymentCollection1664880666982 implements MigrationInterface {
description text NULL,
amount integer NOT NULL,
authorized_amount integer NULL,
captured_amount integer NULL,
refunded_amount integer NULL,
region_id character varying NOT NULL,
currency_code character varying NOT NULL,
@@ -71,6 +72,12 @@ export class paymentCollection1664880666982 implements MigrationInterface {
ALTER TABLE "order_edit" ADD CONSTRAINT "FK_order_edit_payment_collection_id" FOREIGN KEY ("payment_collection_id") REFERENCES "payment_collection"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE payment_session ADD COLUMN payment_authorized_at timestamp WITH time zone NULL;
ALTER TABLE payment_session ADD COLUMN amount integer NULL;
ALTER TABLE payment_session ALTER COLUMN cart_id DROP NOT NULL;
ALTER TABLE refund ADD COLUMN payment_id character varying NULL;
CREATE INDEX "IDX_refund_payment_id" ON "refund" ("payment_id");
ALTER TABLE "refund" ADD CONSTRAINT "FK_refund_payment_id" FOREIGN KEY ("payment_id") REFERENCES "payment"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
`)
// Add missing indexes
@@ -85,6 +92,12 @@ export class paymentCollection1664880666982 implements MigrationInterface {
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DROP INDEX "IDX_order_edit_payment_collection_id";
ALTER TABLE order_edit DROP CONSTRAINT "FK_order_edit_payment_collection_id";
DROP INDEX "IDX_refund_payment_id";
ALTER TABLE refund DROP CONSTRAINT "FK_refund_payment_id";
ALTER TABLE payment_collection DROP CONSTRAINT "FK_payment_collection_region_id";
ALTER TABLE payment_collection_sessions DROP CONSTRAINT "FK_payment_collection_sessions_payment_collection_id";
ALTER TABLE payment_collection_sessions DROP CONSTRAINT "FK_payment_collection_sessions_payment_session_id";
@@ -92,6 +105,9 @@ export class paymentCollection1664880666982 implements MigrationInterface {
ALTER TABLE payment_collection_payments DROP CONSTRAINT "FK_payment_collection_payments_payment_id";
ALTER TABLE order_edit DROP COLUMN payment_collection_id;
ALTER TABLE payment_session DROP COLUMN payment_authorized_at;
ALTER TABLE payment_session DROP COLUMN amount;
ALTER TABLE payment_session ALTER COLUMN cart_id SET NOT NULL;
ALTER TABLE refund DROP COLUMN payment_id;
DROP TABLE payment_collection;
DROP TABLE payment_collection_sessions;
@@ -99,10 +115,6 @@ export class paymentCollection1664880666982 implements MigrationInterface {
DROP TYPE "PAYMENT_COLLECTION_TYPE_ENUM";
DROP TYPE "PAYMENT_COLLECTION_STATUS_ENUM";
DROP INDEX "IDX_order_edit_payment_collection_id";
ALTER TABLE order_edit DROP CONSTRAINT "FK_order_edit_payment_collection_id";
`)
await queryRunner.query(`
@@ -50,6 +50,9 @@ export class PaymentCollection extends SoftDeletableEntity {
@Column({ type: "int", nullable: true })
authorized_amount: number
@Column({ type: "int", nullable: true })
captured_amount: number
@Column({ type: "int", nullable: true })
refunded_amount: number
@@ -28,7 +28,7 @@ export enum PaymentSessionStatus {
@Entity()
export class PaymentSession extends BaseEntity {
@Index()
@Column()
@Column({ nullable: true })
cart_id: string
@ManyToOne(() => Cart, (cart) => cart.payment_sessions)
@@ -51,6 +51,11 @@ export class PaymentSession extends BaseEntity {
@Column({ nullable: true })
idempotency_key: string
@FeatureFlagDecorators(OrderEditingFeatureFlag.key, [
Column({ type: "integer", nullable: true }),
])
amount: number
@FeatureFlagDecorators(OrderEditingFeatureFlag.key, [
Column({ type: resolveDbType("timestamptz"), nullable: true }),
])
+17 -1
View File
@@ -5,12 +5,16 @@ import {
Index,
JoinColumn,
ManyToOne,
OneToOne,
} from "typeorm"
import { BaseEntity } from "../interfaces/models/base-entity"
import { DbAwareColumn } from "../utils/db-aware-column"
import { Order } from "./order"
import { generateEntityId } from "../utils/generate-entity-id"
import { Payment } from "./payment"
import { FeatureFlagDecorators } from "../utils/feature-flag-decorators"
import OrderEditingFeatureFlag from "../loaders/feature-flags/order-editing"
export enum RefundReason {
DISCOUNT = "discount",
@@ -23,13 +27,25 @@ export enum RefundReason {
@Entity()
export class Refund extends BaseEntity {
@Index()
@Column()
@Column({ nullable: true })
order_id: string
@FeatureFlagDecorators(OrderEditingFeatureFlag.key, [
Index(),
Column({ nullable: true }),
])
payment_id: string
@ManyToOne(() => Order, (order) => order.payments)
@JoinColumn({ name: "order_id" })
order: Order
@FeatureFlagDecorators(OrderEditingFeatureFlag.key, [
OneToOne(() => Payment, { nullable: true }),
JoinColumn({ name: "payment_id" }),
])
payment: Payment
@Column({ type: "int" })
amount: number
@@ -1,6 +1,72 @@
import { MedusaError } from "medusa-core-utils"
import { PaymentCollection } from "./../models/payment-collection"
import { EntityRepository, Repository } from "typeorm"
import { FindConfig } from "../types/common"
import { PaymentSession } from "../models"
@EntityRepository(PaymentCollection)
// eslint-disable-next-line max-len
export class PaymentCollectionRepository extends Repository<PaymentCollection> {}
export class PaymentCollectionRepository extends Repository<PaymentCollection> {
async getPaymentCollectionIdBySessionId(
sessionId: string,
config: FindConfig<PaymentCollection> = {}
): Promise<PaymentCollection> {
const paymentCollection = await this.find({
join: {
alias: "payment_col",
innerJoin: { payment_sessions: "payment_col.payment_sessions" },
},
where: (qb) => {
qb.where(
"payment_col_payment_sessions.payment_session_id = :sessionId",
{ sessionId }
)
},
relations: config.relations,
select: config.select,
})
if (!paymentCollection.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Payment collection related to Payment Session id ${sessionId} was not found`
)
}
return paymentCollection[0]
}
async getPaymentCollectionIdByPaymentId(
paymentId: string,
config: FindConfig<PaymentCollection> = {}
): Promise<PaymentCollection> {
const paymentCollection = await this.find({
join: {
alias: "payment_col",
innerJoin: { payments: "payment_col.payments" },
},
where: (qb) => {
qb.where("payment_col_payments.payment_id = :paymentId", { paymentId })
},
relations: config.relations,
select: config.select,
})
if (!paymentCollection.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Payment collection related to Payment id ${paymentId} was not found`
)
}
return paymentCollection[0]
}
async deleteMultiple(ids: string[]): Promise<void> {
await this.createQueryBuilder()
.delete()
.from(PaymentSession)
.where("id IN (:...ids)", { ids })
.execute()
}
}
@@ -1,5 +1,5 @@
export const DefaultProviderMock = {
getStatus: jest.fn().mockImplementation(data => {
getStatus: jest.fn().mockImplementation((data) => {
if (data.money_id === "success") {
return Promise.resolve("authorized")
}
@@ -10,7 +10,7 @@ export const DefaultProviderMock = {
return Promise.resolve("initial")
}),
retrievePayment: jest.fn().mockImplementation(data => {
retrievePayment: jest.fn().mockImplementation((data) => {
return Promise.resolve(data)
}),
list: jest.fn().mockImplementation(() => {
@@ -19,15 +19,26 @@ export const DefaultProviderMock = {
capturePayment: jest.fn().mockReturnValue(Promise.resolve()),
refundPayment: jest.fn().mockReturnValue(Promise.resolve()),
cancelPayment: jest.fn().mockReturnValue(Promise.resolve({})),
deletePayment: jest.fn().mockReturnValue(Promise.resolve({})),
authorizePayment: jest.fn().mockReturnValue(Promise.resolve({})),
}
export const PaymentProviderServiceMock = {
withTransaction: function () {
return this
},
updateSession: jest.fn().mockImplementation((session, cart) => {
return Promise.resolve({
...session.data,
id: `${session.data.id}_updated`,
})
}),
updateSessionNew: jest.fn().mockImplementation((session, sessionInput) => {
return Promise.resolve({
...session,
id: `${session.id}_updated`,
})
}),
list: jest.fn().mockImplementation(() => {
return Promise.resolve()
}),
@@ -40,12 +51,31 @@ export const PaymentProviderServiceMock = {
cartId: cart._id,
})
}),
retrieveProvider: jest.fn().mockImplementation(providerId => {
createSessionNew: jest.fn().mockImplementation((sessionInput) => {
return Promise.resolve({
id: `${sessionInput.providerId}_session`,
})
}),
retrieveProvider: jest.fn().mockImplementation((providerId) => {
if (providerId === "default_provider") {
return DefaultProviderMock
}
throw new Error("Provider Not Found")
}),
refreshSessionNew: jest.fn().mockImplementation((session, inputData) => {
DefaultProviderMock.deletePayment()
PaymentProviderServiceMock.createSessionNew(inputData)
return Promise.resolve({
...session,
id: `${session.id}_refreshed`,
})
}),
authorizePayment: jest
.fn()
.mockReturnValue(Promise.resolve({ status: "authorized" })),
createPaymentNew: jest.fn().mockImplementation((session, inputData) => {
Promise.resolve(inputData)
}),
}
const mock = jest.fn().mockImplementation(() => {
@@ -1,7 +1,22 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import { EventBusService, PaymentCollectionService } from "../index"
import { PaymentCollectionStatus, PaymentCollectionType } from "../../models"
import {
CustomerService,
EventBusService,
PaymentCollectionService,
PaymentProviderService,
} from "../index"
import {
PaymentCollectionStatus,
PaymentCollectionType,
PaymentCollection,
} from "../../models"
import { EventBusServiceMock } from "../__mocks__/event-bus"
import {
DefaultProviderMock,
PaymentProviderServiceMock,
} from "../__mocks__/payment-provider"
import { CustomerServiceMock } from "../__mocks__/customer"
import { PaymentCollectionSessionInput } from "../../types/payment-collection"
describe("PaymentCollectionService", () => {
afterEach(() => {
@@ -11,6 +26,23 @@ describe("PaymentCollectionService", () => {
const paymentCollectionSample = {
id: IdMap.getId("payment-collection-id1"),
region_id: IdMap.getId("region1"),
region: {
payment_providers: [
{
id: IdMap.getId("region1_provider1"),
},
{
id: IdMap.getId("region1_provider2"),
},
],
},
payment_sessions: [
{
id: IdMap.getId("payCol_session1"),
provider_id: IdMap.getId("region1_provider1"),
amount: 100,
},
],
amount: 100,
created_at: new Date(),
metadata: {
@@ -19,6 +51,28 @@ describe("PaymentCollectionService", () => {
status: PaymentCollectionStatus.NOT_PAID,
}
const paymentCollectionWithSessions = {
id: IdMap.getId("payment-collection-session"),
region_id: IdMap.getId("region1"),
region: {
payment_providers: [
{
id: IdMap.getId("region1_provider1"),
},
],
},
payment_sessions: [
{
id: IdMap.getId("payCol_session1"),
provider_id: IdMap.getId("region1_provider1"),
amount: 100,
},
],
amount: 100,
created_at: new Date(),
status: PaymentCollectionStatus.NOT_PAID,
} as PaymentCollection
const paymentCollectionAuthorizedSample = {
id: IdMap.getId("payment-collection-id2"),
region_id: IdMap.getId("region1"),
@@ -26,16 +80,123 @@ describe("PaymentCollectionService", () => {
status: PaymentCollectionStatus.AUTHORIZED,
}
const zeroSample = {
id: IdMap.getId("payment-collection-zero"),
region_id: IdMap.getId("region1"),
amount: 0,
status: PaymentCollectionStatus.NOT_PAID,
}
const noSessionSample = {
id: IdMap.getId("payment-collection-zero"),
region_id: IdMap.getId("region1"),
amount: 10000,
status: PaymentCollectionStatus.NOT_PAID,
}
const fullyAuthorizedSample = {
id: IdMap.getId("payment-collection-fully"),
region_id: IdMap.getId("region1"),
amount: 35000,
authorized_amount: 35000,
region: {
payment_providers: [
{
id: IdMap.getId("region1_provider1"),
},
],
},
payment_sessions: [
{
id: IdMap.getId("payCol_session1"),
payment_authorized_at: Date.now(),
provider_id: IdMap.getId("region1_provider1"),
amount: 35000,
},
],
payments: [
{
id: IdMap.getId("payment-123"),
amount: 35000,
captured_amount: 0,
},
],
status: PaymentCollectionStatus.AUTHORIZED,
} as unknown as PaymentCollection
const partiallyAuthorizedSample = {
id: IdMap.getId("payment-collection-partial"),
region_id: IdMap.getId("region1"),
amount: 70000,
authorized_amount: 35000,
region: {
payment_providers: [
{
id: IdMap.getId("region1_provider1"),
},
],
},
payment_sessions: [
{
id: IdMap.getId("payCol_session1"),
provider_id: IdMap.getId("region1_provider1"),
amount: 35000,
},
{
id: IdMap.getId("payCol_session2"),
payment_authorized_at: Date.now(),
provider_id: IdMap.getId("region1_provider1"),
amount: 35000,
},
],
payments: [],
status: PaymentCollectionStatus.PARTIALLY_AUTHORIZED,
}
const notAuthorizedSample = {
id: IdMap.getId("payment-collection-not-authorized"),
region_id: IdMap.getId("region1"),
amount: 70000,
region: {
payment_providers: [
{
id: IdMap.getId("region1_provider1"),
},
],
},
payment_sessions: [
{
id: IdMap.getId("payCol_session1"),
provider_id: IdMap.getId("region1_provider1"),
amount: 35000,
},
{
id: IdMap.getId("payCol_session2"),
provider_id: IdMap.getId("region1_provider1"),
amount: 35000,
},
],
payments: [],
status: PaymentCollectionStatus.PARTIALLY_AUTHORIZED,
} as unknown as PaymentCollection
const paymentCollectionRepository = MockRepository({
findOne: (query) => {
find: (query) => {
const map = {
[IdMap.getId("payment-collection-id1")]: paymentCollectionSample,
[IdMap.getId("payment-collection-id2")]:
paymentCollectionAuthorizedSample,
[IdMap.getId("payment-collection-session")]:
paymentCollectionWithSessions,
[IdMap.getId("payment-collection-zero")]: zeroSample,
[IdMap.getId("payment-collection-no-session")]: noSessionSample,
[IdMap.getId("payment-collection-fully")]: fullyAuthorizedSample,
[IdMap.getId("payment-collection-partial")]: partiallyAuthorizedSample,
[IdMap.getId("payment-collection-not-authorized")]: notAuthorizedSample,
}
if (map[query?.where?.id]) {
return { ...map[query?.where?.id] }
return [{ ...map[query?.where?.id] }]
}
return
},
@@ -45,20 +206,38 @@ describe("PaymentCollectionService", () => {
...data,
}
},
save: (data) => {
return data
},
})
paymentCollectionRepository.deleteMultiple = jest
.fn()
.mockImplementation(() => {
return Promise.resolve()
})
paymentCollectionRepository.getPaymentCollectionIdBySessionId = jest
.fn()
.mockImplementation(async () => {
return paymentCollectionWithSessions
})
const paymentCollectionService = new PaymentCollectionService({
manager: MockManager,
paymentCollectionRepository,
eventBusService: EventBusServiceMock as unknown as EventBusService,
paymentProviderService:
PaymentProviderServiceMock as unknown as PaymentProviderService,
customerService: CustomerServiceMock as unknown as CustomerService,
})
it("should retrieve a payment collection", async () => {
await paymentCollectionService.retrieve(
IdMap.getId("payment-collection-id1")
)
expect(paymentCollectionRepository.findOne).toHaveBeenCalledTimes(1)
expect(paymentCollectionRepository.findOne).toHaveBeenCalledWith({
expect(paymentCollectionRepository.find).toHaveBeenCalledTimes(1)
expect(paymentCollectionRepository.find).toHaveBeenCalledWith({
where: { id: IdMap.getId("payment-collection-id1") },
})
})
@@ -68,8 +247,8 @@ describe("PaymentCollectionService", () => {
IdMap.getId("payment-collection-non-existing-id")
)
expect(paymentCollectionRepository.findOne).toHaveBeenCalledTimes(1)
expect(payCol).rejects.toThrow(Error)
expect(paymentCollectionRepository.find).toBeCalledTimes(1)
})
it("should create a payment collection", async () => {
@@ -155,9 +334,10 @@ describe("PaymentCollectionService", () => {
IdMap.getId("payment-collection-non-existing"),
submittedChanges
)
expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(0)
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(0)
expect(payCol).rejects.toThrow(Error)
expect(paymentCollectionRepository.save).toBeCalledTimes(0)
expect(EventBusServiceMock.emit).toBeCalledTimes(0)
})
it("should delete a payment collection", async () => {
@@ -197,4 +377,312 @@ describe("PaymentCollectionService", () => {
expect(entity).rejects.toThrow(Error)
})
describe("Manage Payment Sessions", () => {
afterEach(() => {
jest.clearAllMocks()
})
it("should throw error if payment collection doesn't have the correct status", async () => {
const inp: PaymentCollectionSessionInput = {
amount: 100,
provider_id: IdMap.getId("region1_provider1"),
customer_id: "customer1",
}
const ret = paymentCollectionService.setPaymentSessions(
IdMap.getId("payment-collection-id2"),
inp
)
expect(ret).rejects.toThrowError(
new Error(
`Cannot set payment sessions for a payment collection with status ${PaymentCollectionStatus.AUTHORIZED}`
)
)
expect(PaymentProviderServiceMock.createSessionNew).toBeCalledTimes(0)
})
it("should throw error if amount is different than requested", async () => {
const inp: PaymentCollectionSessionInput = {
amount: 101,
provider_id: IdMap.getId("region1_provider1"),
customer_id: "customer1",
}
const ret = paymentCollectionService.setPaymentSessions(
IdMap.getId("payment-collection-id1"),
inp
)
expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes(
0
)
expect(ret).rejects.toThrow(
`The sum of sessions is not equal to 100 on Payment Collection`
)
const multInp: PaymentCollectionSessionInput[] = [
{
amount: 51,
provider_id: IdMap.getId("region1_provider1"),
customer_id: "customer1",
},
{
amount: 50,
provider_id: IdMap.getId("region1_provider2"),
customer_id: "customer1",
},
]
const multiRet = paymentCollectionService.setPaymentSessions(
IdMap.getId("payment-collection-id1"),
multInp
)
expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes(
0
)
expect(multiRet).rejects.toThrow(
`The sum of sessions is not equal to 100 on Payment Collection`
)
})
it("should ignore sessions where provider doesn't belong to the region", async () => {
const multInp: PaymentCollectionSessionInput[] = [
{
amount: 50,
provider_id: IdMap.getId("region1_provider1"),
customer_id: "customer1",
},
{
amount: 50,
provider_id: IdMap.getId("region1_invalid_provider"),
customer_id: "customer1",
},
]
const multiRet = paymentCollectionService.setPaymentSessions(
IdMap.getId("payment-collection-id1"),
multInp
)
expect(multiRet).rejects.toThrow(
`The sum of sessions is not equal to 100 on Payment Collection`
)
expect(PaymentProviderServiceMock.createSessionNew).toBeCalledTimes(0)
})
it("should add a new session and update existing one", async () => {
const inp: PaymentCollectionSessionInput[] = [
{
session_id: IdMap.getId("payCol_session1"),
amount: 50,
provider_id: IdMap.getId("region1_provider1"),
customer_id: IdMap.getId("lebron"),
},
{
amount: 50,
provider_id: IdMap.getId("region1_provider1"),
customer_id: IdMap.getId("lebron"),
},
]
await paymentCollectionService.setPaymentSessions(
IdMap.getId("payment-collection-session"),
inp
)
expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes(
1
)
expect(PaymentProviderServiceMock.updateSessionNew).toHaveBeenCalledTimes(
1
)
expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1)
})
it("should add a new session and delete existing one", async () => {
const inp: PaymentCollectionSessionInput[] = [
{
amount: 100,
provider_id: IdMap.getId("region1_provider1"),
customer_id: IdMap.getId("lebron"),
},
]
await paymentCollectionService.setPaymentSessions(
IdMap.getId("payment-collection-session"),
inp
)
expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes(
1
)
expect(PaymentProviderServiceMock.updateSessionNew).toHaveBeenCalledTimes(
0
)
expect(paymentCollectionRepository.deleteMultiple).toHaveBeenCalledTimes(
1
)
expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1)
})
it("should refresh a payment session", async () => {
await paymentCollectionService.refreshPaymentSession(
IdMap.getId("payment-collection-session"),
IdMap.getId("payCol_session1"),
{
customer_id: "customer1",
amount: 100,
provider_id: IdMap.getId("region1_provider1"),
}
)
expect(
PaymentProviderServiceMock.refreshSessionNew
).toHaveBeenCalledTimes(1)
expect(DefaultProviderMock.deletePayment).toHaveBeenCalledTimes(1)
expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes(
1
)
})
it("should fail to refresh a payment session if the amount is different", async () => {
const sess = paymentCollectionService.refreshPaymentSession(
IdMap.getId("payment-collection-session"),
IdMap.getId("payCol_session1"),
{
customer_id: "customer1",
amount: 80,
provider_id: IdMap.getId("region1_provider1"),
}
)
expect(sess).rejects.toThrow(
"The amount has to be the same as the existing payment session"
)
expect(PaymentProviderServiceMock.refreshSessionNew).toBeCalledTimes(0)
expect(DefaultProviderMock.deletePayment).toBeCalledTimes(0)
expect(PaymentProviderServiceMock.createSessionNew).toBeCalledTimes(0)
})
})
describe("Authorize Payments", () => {
afterEach(() => {
jest.clearAllMocks()
})
it("should mark as paid if amount is 0", async () => {
await paymentCollectionService.authorize(
IdMap.getId("payment-collection-zero")
)
expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes(
0
)
})
it("should reject payment collection without payment sessions", async () => {
const ret = paymentCollectionService.authorize(
IdMap.getId("payment-collection-no-session")
)
expect(ret).rejects.toThrowError(
new Error(
"You cannot complete a Payment Collection without a payment session."
)
)
})
it("should call authorizePayments for all sessions", async () => {
await paymentCollectionService.authorize(
IdMap.getId("payment-collection-not-authorized")
)
expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes(
2
)
expect(PaymentProviderServiceMock.createPaymentNew).toHaveBeenCalledTimes(
2
)
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1)
})
it("should skip authorized sessions - partially authorized", async () => {
await paymentCollectionService.authorize(
IdMap.getId("payment-collection-partial")
)
expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes(
1
)
expect(PaymentProviderServiceMock.createPaymentNew).toHaveBeenCalledTimes(
1
)
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1)
})
it("should skip authorized sessions - fully authorized", async () => {
await paymentCollectionService.authorize(
IdMap.getId("payment-collection-fully")
)
expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes(
0
)
expect(PaymentProviderServiceMock.createPaymentNew).toHaveBeenCalledTimes(
0
)
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(0)
})
})
describe("Capture Payments", () => {
afterEach(() => {
jest.clearAllMocks()
})
it("should throw error if the status is not authorized", async () => {
paymentCollectionRepository.getPaymentCollectionIdByPaymentId = jest
.fn()
.mockReturnValue(Promise.resolve(notAuthorizedSample))
PaymentProviderServiceMock.capturePayment = jest
.fn()
.mockReturnValue(Promise.resolve())
const ret = paymentCollectionService.capture(
IdMap.getId("payment-collection-not-authorized")
)
expect(ret).rejects.toThrowError(
new Error(
`A Payment Collection with status ${PaymentCollectionStatus.PARTIALLY_AUTHORIZED} cannot capture payment`
)
)
expect(PaymentProviderServiceMock.capturePayment).toBeCalledTimes(0)
})
it("should emit PAYMENT_CAPTURE_FAILED if payment capture has failed", async () => {
paymentCollectionRepository.getPaymentCollectionIdByPaymentId = jest
.fn()
.mockReturnValue(Promise.resolve(fullyAuthorizedSample))
PaymentProviderServiceMock.retrievePayment = jest.fn().mockReturnValue(
Promise.resolve({
id: IdMap.getId("payment-123"),
amount: 35000,
captured_amount: 0,
})
)
PaymentProviderServiceMock.capturePayment = jest
.fn()
.mockRejectedValue("capture failed")
const ret = paymentCollectionService.capture(IdMap.getId("payment-123"))
expect(ret).rejects.toThrowError(
new Error(`Failed to capture Payment ${IdMap.getId("payment-123")}`)
)
})
})
})
@@ -1,19 +1,37 @@
import { DeepPartial, EntityManager, IsNull } from "typeorm"
import { DeepPartial, EntityManager, Equal } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { FindConfig } from "../types/common"
import { buildQuery, isDefined, setMetadata } from "../utils"
import { PaymentCollectionRepository } from "../repositories/payment-collection"
import { PaymentCollection, PaymentCollectionStatus } from "../models"
import {
Customer,
Payment,
PaymentCollection,
PaymentCollectionStatus,
PaymentSession,
PaymentSessionStatus,
Refund,
} from "../models"
import { TransactionBaseService } from "../interfaces"
import { EventBusService } from "./index"
import {
CustomerService,
EventBusService,
PaymentProviderService,
} from "./index"
import { CreatePaymentCollectionInput } from "../types/payment-collection"
import {
CreatePaymentCollectionInput,
PaymentCollectionSessionInput,
PaymentProviderDataInput,
} from "../types/payment-collection"
type InjectedDependencies = {
manager: EntityManager
paymentCollectionRepository: typeof PaymentCollectionRepository
paymentProviderService: PaymentProviderService
eventBusService: EventBusService
customerService: CustomerService
}
export default class PaymentCollectionService extends TransactionBaseService {
@@ -21,17 +39,26 @@ export default class PaymentCollectionService extends TransactionBaseService {
CREATED: "payment-collection.created",
UPDATED: "payment-collection.updated",
DELETED: "payment-collection.deleted",
PAYMENT_AUTHORIZED: "payment-collection.payment_authorized",
PAYMENT_CAPTURED: "payment-collection.payment_captured",
PAYMENT_CAPTURE_FAILED: "payment-collection.payment_capture_failed",
REFUND_CREATED: "payment-collection.payment_refund_created",
REFUND_FAILED: "payment-collection.payment_refund_failed",
}
protected readonly manager_: EntityManager
protected transactionManager_: EntityManager | undefined
protected readonly eventBusService_: EventBusService
protected readonly paymentProviderService_: PaymentProviderService
protected readonly customerService_: CustomerService
// eslint-disable-next-line max-len
protected readonly paymentCollectionRepository_: typeof PaymentCollectionRepository
constructor({
manager,
paymentCollectionRepository,
paymentProviderService,
customerService,
eventBusService,
}: InjectedDependencies) {
// eslint-disable-next-line prefer-rest-params
@@ -39,7 +66,9 @@ export default class PaymentCollectionService extends TransactionBaseService {
this.manager_ = manager
this.paymentCollectionRepository_ = paymentCollectionRepository
this.paymentProviderService_ = paymentProviderService
this.eventBusService_ = eventBusService
this.customerService_ = customerService
}
async retrieve(
@@ -52,24 +81,24 @@ export default class PaymentCollectionService extends TransactionBaseService {
)
const query = buildQuery({ id: paymentCollectionId }, config)
const paymentCollection = await paymentCollectionRepository.findOne(query)
if (!paymentCollection) {
const paymentCollection = await paymentCollectionRepository.find(query)
if (!paymentCollection.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Payment collection with id ${paymentCollectionId} was not found`
)
}
return paymentCollection
return paymentCollection[0]
}
async create(data: CreatePaymentCollectionInput): Promise<PaymentCollection> {
return await this.atomicPhase_(async (transactionManager) => {
const paymentCollectionRepository =
transactionManager.getCustomRepository(
this.paymentCollectionRepository_
)
return await this.atomicPhase_(async (manager) => {
const paymentCollectionRepository = manager.getCustomRepository(
this.paymentCollectionRepository_
)
const paymentCollectionToCreate = paymentCollectionRepository.create({
region_id: data.region_id,
@@ -87,7 +116,7 @@ export default class PaymentCollectionService extends TransactionBaseService {
)
await this.eventBusService_
.withTransaction(transactionManager)
.withTransaction(manager)
.emit(PaymentCollectionService.Events.CREATED, paymentCollection)
return paymentCollection
@@ -160,4 +189,493 @@ export default class PaymentCollectionService extends TransactionBaseService {
return paymentCollection
})
}
private isValidTotalAmount(
total: number,
sessionsInput: PaymentCollectionSessionInput[]
): boolean {
const sum = sessionsInput.reduce((cur, sess) => cur + sess.amount, 0)
return total === sum
}
async setPaymentSessions(
paymentCollectionId: string,
sessions: PaymentCollectionSessionInput[] | PaymentCollectionSessionInput
): Promise<PaymentCollection> {
let sessionsInput = Array.isArray(sessions) ? sessions : [sessions]
return await this.atomicPhase_(async (manager: EntityManager) => {
const paymentCollectionRepository = manager.getCustomRepository(
this.paymentCollectionRepository_
)
const payCol = await this.retrieve(paymentCollectionId, {
relations: ["region", "region.payment_providers", "payment_sessions"],
})
if (payCol.status !== PaymentCollectionStatus.NOT_PAID) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Cannot set payment sessions for a payment collection with status ${payCol.status}`
)
}
sessionsInput = sessionsInput.filter((session) => {
return !!payCol.region.payment_providers.find(({ id }) => {
return id === session.provider_id
})
})
if (!this.isValidTotalAmount(payCol.amount, sessionsInput)) {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
`The sum of sessions is not equal to ${payCol.amount} on Payment Collection`
)
}
let customer: Customer | undefined = undefined
const selectedSessionIds: string[] = []
const paymentSessions: PaymentSession[] = []
for (const session of sessionsInput) {
if (!customer) {
customer = await this.customerService_
.withTransaction(manager)
.retrieve(session.customer_id, {
select: ["id", "email", "metadata"],
})
}
const existingSession = payCol.payment_sessions?.find(
(sess) => session.session_id === sess?.id
)
const inputData: PaymentProviderDataInput = {
resource_id: payCol.id,
currency_code: payCol.currency_code,
amount: session.amount,
provider_id: session.provider_id,
customer,
metadata: {
resource_id: payCol.id,
},
}
if (existingSession) {
const paymentSession = await this.paymentProviderService_
.withTransaction(manager)
.updateSessionNew(existingSession, inputData)
selectedSessionIds.push(existingSession.id)
paymentSessions.push(paymentSession)
} else {
const paymentSession = await this.paymentProviderService_
.withTransaction(manager)
.createSessionNew(inputData)
selectedSessionIds.push(paymentSession.id)
paymentSessions.push(paymentSession)
}
}
if (payCol.payment_sessions?.length) {
const removeIds: string[] = payCol.payment_sessions
.map((sess) => sess.id)
.filter((id) => !selectedSessionIds.includes(id))
if (removeIds.length) {
await paymentCollectionRepository.deleteMultiple(removeIds)
}
}
payCol.payment_sessions = paymentSessions
return await paymentCollectionRepository.save(payCol)
})
}
async refreshPaymentSession(
paymentCollectionId: string,
sessionId: string,
sessionInput: PaymentCollectionSessionInput
): Promise<PaymentSession> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const paymentCollectionRepository = manager.getCustomRepository(
this.paymentCollectionRepository_
)
const payCol =
await paymentCollectionRepository.getPaymentCollectionIdBySessionId(
sessionId,
{
relations: [
"region",
"region.payment_providers",
"payment_sessions",
],
}
)
if (paymentCollectionId !== payCol.id) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Payment Session ${sessionId} does not belong to Payment Collection ${paymentCollectionId}`
)
}
const session = payCol.payment_sessions.find(
(sess) => sessionId === sess?.id
)
if (session?.amount !== sessionInput.amount) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"The amount has to be the same as the existing payment session"
)
}
const customer = await this.customerService_
.withTransaction(manager)
.retrieve(sessionInput.customer_id, {
select: ["id", "email", "metadata"],
})
const inputData: PaymentProviderDataInput = {
resource_id: payCol.id,
currency_code: payCol.currency_code,
amount: session.amount,
provider_id: session.provider_id,
customer,
}
const sessionRefreshed = await this.paymentProviderService_
.withTransaction(manager)
.refreshSessionNew(session, inputData)
payCol.payment_sessions = payCol.payment_sessions.map((sess) => {
if (sess.id === sessionId) {
return sessionRefreshed
}
return sess
})
if (session.payment_authorized_at) {
payCol.authorized_amount -= session.amount
}
await paymentCollectionRepository.save(payCol)
return sessionRefreshed
})
}
async authorize(
paymentCollectionId: string,
context: Record<string, unknown> = {}
): Promise<PaymentCollection> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const paymentCollectionRepository = manager.getCustomRepository(
this.paymentCollectionRepository_
)
const payCol = await this.retrieve(paymentCollectionId, {
relations: ["payment_sessions", "payments"],
})
if (payCol.authorized_amount === payCol.amount) {
return payCol
}
// If cart total is 0, we don't perform anything payment related
if (payCol.amount <= 0) {
payCol.authorized_amount = 0
return await paymentCollectionRepository.save(payCol)
}
if (!payCol.payment_sessions?.length) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"You cannot complete a Payment Collection without a payment session."
)
}
let authorizedAmount = 0
for (const session of payCol.payment_sessions) {
if (session.payment_authorized_at) {
authorizedAmount += session.amount
continue
}
const auth = await this.paymentProviderService_
.withTransaction(manager)
.authorizePayment(session, context)
if (auth?.status === PaymentSessionStatus.AUTHORIZED) {
authorizedAmount += session.amount
const inputData: Omit<PaymentProviderDataInput, "customer"> = {
amount: session.amount,
currency_code: payCol.currency_code,
provider_id: session.provider_id,
resource_id: payCol.id,
}
payCol.payments.push(
await this.paymentProviderService_
.withTransaction(manager)
.createPaymentNew(inputData)
)
}
}
if (authorizedAmount === 0) {
payCol.status = PaymentCollectionStatus.AWAITING
} else if (authorizedAmount < payCol.amount) {
payCol.status = PaymentCollectionStatus.PARTIALLY_AUTHORIZED
} else if (authorizedAmount === payCol.amount) {
payCol.status = PaymentCollectionStatus.AUTHORIZED
}
payCol.authorized_amount = authorizedAmount
const payColCopy = await paymentCollectionRepository.save(payCol)
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentCollectionService.Events.PAYMENT_AUTHORIZED, payColCopy)
return payCol
})
}
private async capturePayment(
payCol: PaymentCollection,
payment: Payment
): Promise<Payment> {
if (payment?.captured_at) {
return payment
}
return await this.atomicPhase_(async (manager: EntityManager) => {
const allowedStatuses = [
PaymentCollectionStatus.AUTHORIZED,
PaymentCollectionStatus.PARTIALLY_CAPTURED,
PaymentCollectionStatus.REQUIRES_ACTION,
]
if (!allowedStatuses.includes(payCol.status)) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`A Payment Collection with status ${payCol.status} cannot capture payment`
)
}
let captureError: Error | null = null
const capturedPayment = await this.paymentProviderService_
.withTransaction(manager)
.capturePayment(payment)
.catch((err) => {
captureError = err
})
payCol.captured_amount = payCol.captured_amount ?? 0
if (capturedPayment) {
payCol.captured_amount += payment.amount
}
if (payCol.captured_amount === 0) {
payCol.status = PaymentCollectionStatus.REQUIRES_ACTION
} else if (payCol.captured_amount === payCol.amount) {
payCol.status = PaymentCollectionStatus.CAPTURED
} else {
payCol.status = PaymentCollectionStatus.PARTIALLY_CAPTURED
}
const paymentCollectionRepository = manager.getCustomRepository(
this.paymentCollectionRepository_
)
await paymentCollectionRepository.save(payCol)
if (!capturedPayment) {
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentCollectionService.Events.PAYMENT_CAPTURE_FAILED, {
...payment,
error: captureError,
})
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
`Failed to capture Payment ${payment.id}`
)
}
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentCollectionService.Events.PAYMENT_CAPTURED, capturedPayment)
return capturedPayment
})
}
async capture(paymentId: string): Promise<Payment> {
const manager = this.transactionManager_ ?? this.manager_
const paymentCollectionRepository = manager.getCustomRepository(
this.paymentCollectionRepository_
)
const payCol =
await paymentCollectionRepository.getPaymentCollectionIdByPaymentId(
paymentId,
{
relations: ["payments"],
}
)
const payment = payCol.payments.find((payment) => paymentId === payment?.id)
return await this.capturePayment(payCol, payment!)
}
async captureAll(paymentCollectionId: string): Promise<Payment[]> {
const payCol = await this.retrieve(paymentCollectionId, {
relations: ["payments"],
})
const allPayments: Payment[] = []
for (const payment of payCol.payments) {
const captured = await this.capturePayment(payCol, payment).catch(
() => void 0
)
if (captured) {
allPayments.push(captured)
}
}
return allPayments
}
private async refundPayment(
payCol: PaymentCollection,
payment: Payment,
amount: number,
reason: string,
note?: string
): Promise<Refund> {
return await this.atomicPhase_(async (manager: EntityManager) => {
if (!payment.captured_at) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Payment ${payment.id} is not captured`
)
}
const refundable = payment.amount - payment.amount_refunded
if (amount > refundable) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Only ${refundable} can be refunded from Payment ${payment.id}`
)
}
let refundError: Error | null = null
const refund = await this.paymentProviderService_
.withTransaction(manager)
.refundFromPayment(payment, amount, reason, note)
.catch((err) => {
refundError = err
})
payCol.refunded_amount = payCol.refunded_amount ?? 0
if (refund) {
payCol.refunded_amount += refund.amount
}
if (payCol.refunded_amount === 0) {
payCol.status = PaymentCollectionStatus.REQUIRES_ACTION
} else if (payCol.refunded_amount === payCol.amount) {
payCol.status = PaymentCollectionStatus.REFUNDED
} else {
payCol.status = PaymentCollectionStatus.PARTIALLY_REFUNDED
}
const paymentCollectionRepository = manager.getCustomRepository(
this.paymentCollectionRepository_
)
await paymentCollectionRepository.save(payCol)
if (!refund) {
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentCollectionService.Events.REFUND_FAILED, {
...payment,
error: refundError,
})
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
`Failed to refund Payment ${payment.id}`
)
}
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentCollectionService.Events.REFUND_CREATED, refund)
return refund
})
}
async refund(
paymentId: string,
amount: number,
reason: string,
note?: string
): Promise<Refund> {
const manager = this.transactionManager_ ?? this.manager_
const paymentCollectionRepository = manager.getCustomRepository(
this.paymentCollectionRepository_
)
const payCol =
await paymentCollectionRepository.getPaymentCollectionIdByPaymentId(
paymentId
)
const payment = await this.paymentProviderService_.retrievePayment(
paymentId
)
return await this.refundPayment(payCol, payment, amount, reason, note)
}
async refundAll(
paymentCollectionId: string,
reason: string,
note?: string
): Promise<Refund[]> {
const payCol = await this.retrieve(paymentCollectionId, {
relations: ["payments"],
})
const allRefunds: Refund[] = []
for (const payment of payCol.payments) {
const refunded = await this.refundPayment(
payCol,
payment,
payment.amount,
reason,
note
).catch(() => void 0)
if (refunded) {
allRefunds.push(refunded)
}
}
return allRefunds
}
}
@@ -16,6 +16,7 @@ import {
PaymentSessionStatus,
Refund,
} from "../models"
import { PaymentProviderDataInput } from "../types/payment-collection"
type PaymentProviderKey = `pp_${string}` | "systemPaymentProviderService"
type InjectedDependencies = {
@@ -179,6 +180,33 @@ export default class PaymentProviderService extends TransactionBaseService {
})
}
async createSessionNew(
sessionInput: PaymentProviderDataInput
): Promise<PaymentSession> {
return await this.atomicPhase_(async (transactionManager) => {
const provider: AbstractPaymentService = this.retrieveProvider(
sessionInput.provider_id
)
const sessionData = await provider
.withTransaction(transactionManager)
.createPaymentNew(sessionInput)
const sessionRepo = transactionManager.getCustomRepository(
this.paymentSessionRepository_
)
const toCreate = {
provider_id: sessionInput.provider_id,
data: sessionData,
status: "pending",
amount: sessionInput.amount,
} as PaymentSession
const created = sessionRepo.create(toCreate)
return await sessionRepo.save(created)
})
}
/**
* Refreshes a payment session with the given provider.
* This means, that we delete the current one and create a new.
@@ -219,6 +247,26 @@ export default class PaymentProviderService extends TransactionBaseService {
})
}
async refreshSessionNew(
paymentSession: PaymentSession,
sessionInput: PaymentProviderDataInput
): Promise<PaymentSession> {
return this.atomicPhase_(async (transactionManager) => {
const session = await this.retrieveSession(paymentSession.id)
const provider = this.retrieveProvider(paymentSession.provider_id)
await provider.withTransaction(transactionManager).deletePayment(session)
const sessionRepo = transactionManager.getCustomRepository(
this.paymentSessionRepository_
)
await sessionRepo.remove(session)
return await this.createSessionNew(sessionInput)
})
}
/**
* Updates an existing payment session.
* @param paymentSession - the payment session object to
@@ -240,7 +288,29 @@ export default class PaymentProviderService extends TransactionBaseService {
const sessionRepo = transactionManager.getCustomRepository(
this.paymentSessionRepository_
)
return sessionRepo.save(session)
return await sessionRepo.save(session)
})
}
async updateSessionNew(
paymentSession: PaymentSession,
sessionInput: PaymentProviderDataInput
): Promise<PaymentSession> {
return await this.atomicPhase_(async (transactionManager) => {
const session = await this.retrieveSession(paymentSession.id)
const provider = this.retrieveProvider(paymentSession.provider_id)
session.amount = sessionInput.amount
paymentSession.data.amount = sessionInput.amount
session.data = await provider
.withTransaction(transactionManager)
.updatePaymentNew(paymentSession.data, sessionInput)
const sessionRepo = transactionManager.getCustomRepository(
this.paymentSessionRepository_
)
return await sessionRepo.save(session)
})
}
@@ -265,7 +335,7 @@ export default class PaymentProviderService extends TransactionBaseService {
this.paymentSessionRepository_
)
return sessionRepo.remove(session)
return await sessionRepo.remove(session)
})
}
@@ -321,7 +391,34 @@ export default class PaymentProviderService extends TransactionBaseService {
cart_id: cart.id,
})
return paymentRepo.save(created)
return await paymentRepo.save(created)
})
}
async createPaymentNew(
paymentInput: Omit<PaymentProviderDataInput, "customer">
): Promise<Payment> {
return await this.atomicPhase_(async (transactionManager) => {
const { payment_session, currency_code, amount, provider_id } =
paymentInput
const provider = this.retrieveProvider(provider_id)
const paymentData = await provider
.withTransaction(transactionManager)
.getPaymentData(payment_session)
const paymentRepo = transactionManager.getCustomRepository(
this.paymentRepository_
)
const created = paymentRepo.create({
provider_id,
amount,
currency_code,
data: paymentData,
})
return await paymentRepo.save(created)
})
}
@@ -343,7 +440,7 @@ export default class PaymentProviderService extends TransactionBaseService {
const payRepo = transactionManager.getCustomRepository(
this.paymentRepository_
)
return payRepo.save(payment)
return await payRepo.save(payment)
})
}
@@ -371,7 +468,7 @@ export default class PaymentProviderService extends TransactionBaseService {
const sessionRepo = transactionManager.getCustomRepository(
this.paymentSessionRepository_
)
return sessionRepo.save(session)
return await sessionRepo.save(session)
})
}
@@ -392,7 +489,7 @@ export default class PaymentProviderService extends TransactionBaseService {
const sessionRepo = transactionManager.getCustomRepository(
this.paymentSessionRepository_
)
return sessionRepo.save(session)
return await sessionRepo.save(session)
})
}
@@ -437,7 +534,7 @@ export default class PaymentProviderService extends TransactionBaseService {
const paymentRepo = transactionManager.getCustomRepository(
this.paymentRepository_
)
return paymentRepo.save(payment)
return await paymentRepo.save(payment)
})
}
@@ -521,7 +618,47 @@ export default class PaymentProviderService extends TransactionBaseService {
}
const created = refundRepo.create(toCreate)
return refundRepo.save(created)
return await refundRepo.save(created)
})
}
async refundFromPayment(
payment: Payment,
amount: number,
reason: string,
note?: string
): Promise<Refund> {
return await this.atomicPhase_(async (manager) => {
const refundable = payment.amount - payment.amount_refunded
if (refundable < amount) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Refund amount is higher that the refundable amount"
)
}
const provider = this.retrieveProvider(payment.provider_id)
payment.data = await provider
.withTransaction(manager)
.refundPayment(payment, amount)
payment.amount_refunded += amount
const paymentRepo = manager.getCustomRepository(this.paymentRepository_)
await paymentRepo.save(payment)
const refundRepo = manager.getCustomRepository(this.refundRepository_)
const toCreate = {
payment_id: payment.id,
amount,
reason,
note,
}
const created = refundRepo.create(toCreate)
return await refundRepo.save(created)
})
}
@@ -1,4 +1,10 @@
import { PaymentCollection, PaymentCollectionType } from "../models"
import {
Cart,
Customer,
PaymentCollection,
PaymentCollectionType,
PaymentSession,
} from "../models"
export type CreatePaymentCollectionInput = {
region_id: string
@@ -10,6 +16,25 @@ export type CreatePaymentCollectionInput = {
description?: string
}
export type PaymentCollectionSessionInput = {
provider_id: string
amount: number
session_id?: string
customer_id: string
}
export type PaymentProviderDataInput = {
resource_id: string
customer: Partial<Customer>
currency_code: string
provider_id: string
amount: number
payment_session?: PaymentSession
payment_description?: string
cart_id?: string
cart?: Cart
metadata?: any
}
export const defaultPaymentCollectionRelations = [
"region",
"region.payment_providers",