fix(core-flows): Refund and recreate payment session on cart complete failure (#12263)
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
import { IPaymentModuleService, Logger } from "@medusajs/framework/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
Modules,
|
||||
PaymentSessionStatus,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { Logger } from "@medusajs/framework/types"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
import { refundPaymentAndRecreatePaymentSessionWorkflow } from "../workflows/refund-payment-recreate-payment-session"
|
||||
|
||||
/**
|
||||
* The payment session's details for compensation.
|
||||
@@ -39,35 +36,45 @@ export const compensatePaymentIfNeededStep = createStep(
|
||||
}
|
||||
|
||||
const logger = container.resolve<Logger>(ContainerRegistrationKeys.LOGGER)
|
||||
const paymentModule = container.resolve<IPaymentModuleService>(
|
||||
Modules.PAYMENT
|
||||
)
|
||||
const query = container.resolve(ContainerRegistrationKeys.QUERY)
|
||||
|
||||
const paymentSession = await paymentModule.retrievePaymentSession(
|
||||
paymentSessionId,
|
||||
{
|
||||
relations: ["payment"],
|
||||
}
|
||||
)
|
||||
const { data: paymentSessions } = await query.graph({
|
||||
entity: "payment_session",
|
||||
fields: [
|
||||
"id",
|
||||
"payment_collection_id",
|
||||
"amount",
|
||||
"raw_amount",
|
||||
"provider_id",
|
||||
"data",
|
||||
"payment.id",
|
||||
"payment.captured_at",
|
||||
"payment.customer.id",
|
||||
],
|
||||
filters: {
|
||||
id: paymentSessionId,
|
||||
},
|
||||
})
|
||||
const paymentSession = paymentSessions[0]
|
||||
|
||||
if (paymentSession.status === PaymentSessionStatus.AUTHORIZED) {
|
||||
try {
|
||||
await paymentModule.cancelPayment(paymentSession.id)
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Error was thrown trying to cancel payment session - ${paymentSession.id} - ${e}`
|
||||
)
|
||||
}
|
||||
if (!paymentSession) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
paymentSession.status === PaymentSessionStatus.CAPTURED &&
|
||||
paymentSession.payment?.id
|
||||
) {
|
||||
if (paymentSession.payment?.captured_at) {
|
||||
try {
|
||||
await paymentModule.refundPayment({
|
||||
const workflowInput = {
|
||||
payment_collection_id: paymentSession.payment_collection_id,
|
||||
provider_id: paymentSession.provider_id,
|
||||
customer_id: paymentSession.payment?.customer?.id,
|
||||
data: paymentSession.data,
|
||||
amount: paymentSession.raw_amount ?? paymentSession.amount,
|
||||
payment_id: paymentSession.payment.id,
|
||||
note: "Refunded due to cart completion failure",
|
||||
}
|
||||
|
||||
await refundPaymentAndRecreatePaymentSessionWorkflow(container).run({
|
||||
input: workflowInput,
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
|
||||
@@ -112,21 +112,12 @@ export const completeCartWorkflow = createWorkflow(
|
||||
name: "cart-query",
|
||||
})
|
||||
|
||||
// this is only run when the cart is completed for the first time (same condition as below)
|
||||
// but needs to be before the validation step
|
||||
const paymentSessions = when(
|
||||
"create-order-payment-compensation",
|
||||
{ orderId },
|
||||
({ orderId }) => !orderId
|
||||
).then(() => {
|
||||
const paymentSessions = validateCartPaymentsStep({ cart })
|
||||
// purpose of this step is to run compensation if cart completion fails
|
||||
// and tries to cancel or refund the payment depending on the status.
|
||||
compensatePaymentIfNeededStep({
|
||||
payment_session_id: paymentSessions[0].id,
|
||||
})
|
||||
|
||||
return paymentSessions
|
||||
// this needs to be before the validation step
|
||||
const paymentSessions = validateCartPaymentsStep({ cart })
|
||||
// purpose of this step is to run compensation if cart completion fails
|
||||
// and tries to refund the payment if captured
|
||||
compensatePaymentIfNeededStep({
|
||||
payment_session_id: paymentSessions[0].id,
|
||||
})
|
||||
|
||||
const validate = createHook("validate", {
|
||||
|
||||
@@ -11,6 +11,7 @@ export * from "./list-shipping-options-for-cart-with-pricing"
|
||||
export * from "./refresh-cart-items"
|
||||
export * from "./refresh-cart-shipping-methods"
|
||||
export * from "./refresh-payment-collection"
|
||||
export * from "./refund-payment-recreate-payment-session"
|
||||
export * from "./transfer-cart-customer"
|
||||
export * from "./update-cart"
|
||||
export * from "./update-cart-promotions"
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { BigNumberInput, PaymentSessionDTO } from "@medusajs/framework/types"
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { createPaymentSessionsWorkflow } from "../../payment-collection/workflows/create-payment-session"
|
||||
import { refundPaymentsWorkflow } from "../../payment/workflows/refund-payments"
|
||||
|
||||
/**
|
||||
* The data to create payment sessions.
|
||||
*/
|
||||
export interface refundPaymentAndRecreatePaymentSessionWorkflowInput {
|
||||
/**
|
||||
* The ID of the payment collection to create payment sessions for.
|
||||
*/
|
||||
payment_collection_id: string
|
||||
/**
|
||||
* The ID of the payment provider that the payment sessions are associated with.
|
||||
* This provider is used to later process the payment sessions and their payments.
|
||||
*/
|
||||
provider_id: string
|
||||
/**
|
||||
* The ID of the customer that the payment session should be associated with.
|
||||
*/
|
||||
customer_id?: string
|
||||
/**
|
||||
* Custom data relevant for the payment provider to process the payment session.
|
||||
* Learn more in [this documentation](https://docs.medusajs.com/resources/commerce-modules/payment/payment-session#data-property).
|
||||
*/
|
||||
data?: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Additional context that's useful for the payment provider to process the payment session.
|
||||
* Currently all of the context is calculated within the workflow.
|
||||
*/
|
||||
context?: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* The ID of the payment to refund.
|
||||
*/
|
||||
payment_id: string
|
||||
|
||||
/**
|
||||
* The amount to refund.
|
||||
*/
|
||||
amount: BigNumberInput
|
||||
|
||||
/**
|
||||
* The note to attach to the refund.
|
||||
*/
|
||||
note?: string
|
||||
}
|
||||
|
||||
export const refundPaymentAndRecreatePaymentSessionWorkflowId =
|
||||
"refund-payment-and-recreate-payment-session"
|
||||
/**
|
||||
* This workflow refunds a payment and creates a new payment session.
|
||||
*
|
||||
* @summary
|
||||
*
|
||||
* Refund a payment and create a new payment session.
|
||||
*/
|
||||
export const refundPaymentAndRecreatePaymentSessionWorkflow = createWorkflow(
|
||||
refundPaymentAndRecreatePaymentSessionWorkflowId,
|
||||
(
|
||||
input: WorkflowData<refundPaymentAndRecreatePaymentSessionWorkflowInput>
|
||||
): WorkflowResponse<PaymentSessionDTO> => {
|
||||
refundPaymentsWorkflow.runAsStep({
|
||||
input: [
|
||||
{
|
||||
payment_id: input.payment_id,
|
||||
note: input.note,
|
||||
amount: input.amount,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const paymentSession = createPaymentSessionsWorkflow.runAsStep({
|
||||
input: {
|
||||
payment_collection_id: input.payment_collection_id,
|
||||
provider_id: input.provider_id,
|
||||
customer_id: input.customer_id,
|
||||
data: input.data,
|
||||
},
|
||||
})
|
||||
|
||||
return new WorkflowResponse(paymentSession)
|
||||
}
|
||||
)
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
WorkflowData,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { useQueryGraphStep } from "../../../common"
|
||||
import { refundPaymentsWorkflow } from "../../../payment"
|
||||
import { refundPaymentsWorkflow } from "../../../payment/workflows/refund-payments"
|
||||
|
||||
export const refundCapturedPaymentsWorkflowId =
|
||||
"refund-captured-payments-workflow"
|
||||
@@ -20,6 +20,7 @@ export const refundCapturedPaymentsWorkflow = createWorkflow(
|
||||
input: WorkflowData<{
|
||||
order_id: string
|
||||
created_by?: string
|
||||
note?: string
|
||||
}>
|
||||
) => {
|
||||
const orderQuery = useQueryGraphStep({
|
||||
@@ -74,6 +75,7 @@ export const refundCapturedPaymentsWorkflow = createWorkflow(
|
||||
payment_id: payment.id,
|
||||
created_by: input.created_by,
|
||||
amount: amountToRefund,
|
||||
note: input.note,
|
||||
}
|
||||
})
|
||||
.filter((payment) => MathBN.gt(payment.amount, 0))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from "./create-payment-session"
|
||||
export * from "./create-refund-reasons"
|
||||
export * from "./delete-payment-sessions"
|
||||
export * from "./delete-refund-reasons"
|
||||
export * from "./update-refund-reasons"
|
||||
export * from "./delete-refund-reasons"
|
||||
@@ -28,6 +28,10 @@ export type RefundPaymentsStepInput = {
|
||||
* The ID of the user that refunded the payment.
|
||||
*/
|
||||
created_by?: string
|
||||
/**
|
||||
* The note to attach to the refund.
|
||||
*/
|
||||
note?: string
|
||||
}[]
|
||||
|
||||
export const refundPaymentsStepId = "refund-payments-step"
|
||||
|
||||
@@ -71,12 +71,41 @@ export const processPaymentWorkflow = createWorkflow(
|
||||
input.action === PaymentActions.SUCCESSFUL && !!paymentData.data.length
|
||||
)
|
||||
}).then(() => {
|
||||
capturePaymentWorkflow.runAsStep({
|
||||
input: {
|
||||
payment_id: paymentData.data[0].id,
|
||||
amount: input.data?.amount,
|
||||
},
|
||||
capturePaymentWorkflow
|
||||
.runAsStep({
|
||||
input: {
|
||||
payment_id: paymentData.data[0].id,
|
||||
amount: input.data?.amount,
|
||||
},
|
||||
})
|
||||
.config({
|
||||
name: "capture-payment",
|
||||
})
|
||||
})
|
||||
|
||||
when({ input, paymentData }, ({ input, paymentData }) => {
|
||||
// payment is captured with the provider but we dont't have any payment data which means we didn't call authorize yet - autocapture flow
|
||||
return (
|
||||
input.action === PaymentActions.SUCCESSFUL && !paymentData.data.length
|
||||
)
|
||||
}).then(() => {
|
||||
const payment = authorizePaymentSessionStep({
|
||||
id: input.data!.session_id,
|
||||
context: {},
|
||||
}).config({
|
||||
name: "authorize-payment-session-autocapture",
|
||||
})
|
||||
|
||||
capturePaymentWorkflow
|
||||
.runAsStep({
|
||||
input: {
|
||||
payment_id: payment.id,
|
||||
amount: input.data?.amount,
|
||||
},
|
||||
})
|
||||
.config({
|
||||
name: "capture-payment-autocapture",
|
||||
})
|
||||
})
|
||||
|
||||
when(
|
||||
@@ -94,6 +123,8 @@ export const processPaymentWorkflow = createWorkflow(
|
||||
authorizePaymentSessionStep({
|
||||
id: input.data!.session_id,
|
||||
context: {},
|
||||
}).config({
|
||||
name: "authorize-payment-session",
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BigNumberInput, PaymentDTO } from "@medusajs/framework/types"
|
||||
import { MathBN, MedusaError } from "@medusajs/framework/utils"
|
||||
import { isDefined, MathBN, MedusaError } from "@medusajs/framework/utils"
|
||||
import {
|
||||
createStep,
|
||||
createWorkflow,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { useQueryGraphStep } from "../../common"
|
||||
import { addOrderTransactionStep } from "../../order"
|
||||
import { addOrderTransactionStep } from "../../order/steps/add-order-transaction"
|
||||
import { refundPaymentsStep } from "../steps/refund-payments"
|
||||
|
||||
/**
|
||||
@@ -29,14 +29,14 @@ export type ValidatePaymentsRefundStepInput = {
|
||||
* This step validates that the refund is valid for the payment.
|
||||
* If the payment's refundable amount is less than the amount to be refunded,
|
||||
* the step throws an error.
|
||||
*
|
||||
*
|
||||
* :::note
|
||||
*
|
||||
*
|
||||
* You can retrieve a payment's details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query),
|
||||
* or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep).
|
||||
*
|
||||
*
|
||||
* :::
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const data = validatePaymentsRefundStep({
|
||||
* payment: [{
|
||||
@@ -53,10 +53,7 @@ export type ValidatePaymentsRefundStepInput = {
|
||||
*/
|
||||
export const validatePaymentsRefundStep = createStep(
|
||||
"validate-payments-refund-step",
|
||||
async function ({
|
||||
payments,
|
||||
input,
|
||||
}: ValidatePaymentsRefundStepInput) {
|
||||
async function ({ payments, input }: ValidatePaymentsRefundStepInput) {
|
||||
const paymentIdAmountMap = new Map<string, BigNumberInput>(
|
||||
input.map(({ payment_id, amount }) => [payment_id, amount])
|
||||
)
|
||||
@@ -101,15 +98,19 @@ export type RefundPaymentsWorkflowInput = {
|
||||
* The ID of the user that's refunding the payment.
|
||||
*/
|
||||
created_by?: string
|
||||
/**
|
||||
* The note to attach to the refund.
|
||||
*/
|
||||
note?: string
|
||||
}[]
|
||||
|
||||
export const refundPaymentsWorkflowId = "refund-payments-workflow"
|
||||
/**
|
||||
* This workflow refunds payments.
|
||||
*
|
||||
*
|
||||
* You can use this workflow within your customizations or your own custom workflows, allowing you to
|
||||
* refund payments in your custom flow.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const { result } = await refundPaymentsWorkflow(container)
|
||||
* .run({
|
||||
@@ -120,9 +121,9 @@ export const refundPaymentsWorkflowId = "refund-payments-workflow"
|
||||
* }
|
||||
* ]
|
||||
* })
|
||||
*
|
||||
*
|
||||
* @summary
|
||||
*
|
||||
*
|
||||
* Refund one or more payments.
|
||||
*/
|
||||
export const refundPaymentsWorkflow = createWorkflow(
|
||||
@@ -171,18 +172,24 @@ export const refundPaymentsWorkflow = createWorkflow(
|
||||
paymentsMap[payment.id] = payment
|
||||
}
|
||||
|
||||
return input.map((paymentInput) => {
|
||||
const payment = paymentsMap[paymentInput.payment_id]!
|
||||
const order = payment.payment_collection.order
|
||||
return input
|
||||
.map((paymentInput) => {
|
||||
const payment = paymentsMap[paymentInput.payment_id]!
|
||||
const order = payment.payment_collection?.order
|
||||
|
||||
return {
|
||||
order_id: order.id,
|
||||
amount: MathBN.mult(paymentInput.amount, -1),
|
||||
currency_code: payment.currency_code,
|
||||
reference_id: payment.id,
|
||||
reference: "refund",
|
||||
}
|
||||
})
|
||||
if (!order) {
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
order_id: order.id,
|
||||
amount: MathBN.mult(paymentInput.amount, -1),
|
||||
currency_code: payment.currency_code,
|
||||
reference_id: payment.id,
|
||||
reference: "refund",
|
||||
}
|
||||
})
|
||||
.filter(isDefined)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user