feat(core-flows, types): try payment cancelation if cart complete fails in the webhook (#11832)
* fix: try compensating if cart complete fails, reorder process paymnet workflow steps * fix: condition * fix: feedback * feat: changes to the compensation step * fix: build * fix: remove payment id as arg * fix: when input * chore: remove captures realtion, changeset * fix: return lates payment object from authorize payment
This commit is contained in:
6
.changeset/flat-keys-shave.md
Normal file
6
.changeset/flat-keys-shave.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@medusajs/core-flows": patch
|
||||
"@medusajs/types": patch
|
||||
---
|
||||
|
||||
fix(core-flows): cancel/refund payment on cart complete error
|
||||
@@ -0,0 +1,80 @@
|
||||
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
PaymentSessionStatus,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
import { Logger } from "@medusajs/framework/types"
|
||||
import { IPaymentModuleService } from "@medusajs/framework/types"
|
||||
|
||||
/**
|
||||
* The payment session's details for compensation.
|
||||
*/
|
||||
export interface CompensatePaymentIfNeededStepInput {
|
||||
/**
|
||||
* The payment to compensate.
|
||||
*/
|
||||
payment_session_id: string
|
||||
}
|
||||
|
||||
export const compensatePaymentIfNeededStepId = "compensate-payment-if-needed"
|
||||
/**
|
||||
* Purpose of this step is to be the last compensation in cart completion workflow.
|
||||
* If the cart completion fails, this step tries to cancel or refund the payment.
|
||||
*
|
||||
* @example
|
||||
* const data = compensatePaymentIfNeededStep({
|
||||
* payment_session_id: "pay_123"
|
||||
* })
|
||||
*/
|
||||
export const compensatePaymentIfNeededStep = createStep(
|
||||
compensatePaymentIfNeededStepId,
|
||||
async (data: CompensatePaymentIfNeededStepInput, { container }) => {
|
||||
const { payment_session_id } = data
|
||||
|
||||
return new StepResponse(payment_session_id)
|
||||
},
|
||||
async (paymentSessionId, { container }) => {
|
||||
if (!paymentSessionId) {
|
||||
return
|
||||
}
|
||||
|
||||
const logger = container.resolve<Logger>(ContainerRegistrationKeys.LOGGER)
|
||||
const paymentModule = container.resolve<IPaymentModuleService>(
|
||||
Modules.PAYMENT
|
||||
)
|
||||
|
||||
const paymentSession = await paymentModule.retrievePaymentSession(
|
||||
paymentSessionId,
|
||||
{
|
||||
relations: ["payment"],
|
||||
}
|
||||
)
|
||||
|
||||
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.status === PaymentSessionStatus.CAPTURED &&
|
||||
paymentSession.payment?.id
|
||||
) {
|
||||
try {
|
||||
await paymentModule.refundPayment({
|
||||
payment_id: paymentSession.payment.id,
|
||||
note: "Refunded due to cart completion failure",
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Error was thrown trying to refund payment - ${paymentSession.payment?.id} - ${e}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
PrepareLineItemDataInput,
|
||||
prepareTaxLinesData,
|
||||
} from "../utils/prepare-line-item-data"
|
||||
|
||||
import { compensatePaymentIfNeededStep } from "../steps/compensate-payment-if-needed"
|
||||
/**
|
||||
* The data to complete a cart and place an order.
|
||||
*/
|
||||
@@ -111,6 +111,23 @@ 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
|
||||
})
|
||||
|
||||
const validate = createHook("validate", {
|
||||
input,
|
||||
cart,
|
||||
@@ -135,8 +152,6 @@ export const completeCartWorkflow = createWorkflow(
|
||||
|
||||
validateShippingStep({ cart, shippingOptions })
|
||||
|
||||
const paymentSessions = validateCartPaymentsStep({ cart })
|
||||
|
||||
createHook("beforePaymentAuthorization", {
|
||||
input,
|
||||
})
|
||||
@@ -144,7 +159,7 @@ export const completeCartWorkflow = createWorkflow(
|
||||
const payment = authorizePaymentSessionStep({
|
||||
// We choose the first payment session, as there will only be one active payment session
|
||||
// This might change in the future.
|
||||
id: paymentSessions[0].id,
|
||||
id: paymentSessions![0].id,
|
||||
})
|
||||
|
||||
const { variants, sales_channel_id } = transform({ cart }, (data) => {
|
||||
|
||||
@@ -85,7 +85,7 @@ export const authorizePaymentSessionStep = createStep(
|
||||
)
|
||||
}
|
||||
|
||||
return new StepResponse(payment)
|
||||
return new StepResponse(paymentSession.payment)
|
||||
},
|
||||
// If payment or any other part of complete cart fails post payment step, we cancel any payments made
|
||||
async (payment, { container }) => {
|
||||
|
||||
@@ -16,10 +16,10 @@ export const processPaymentWorkflowId = "process-payment-workflow"
|
||||
* This workflow processes a payment to either complete its associated cart,
|
||||
* capture the payment, or authorize the payment session. It's used when a
|
||||
* [Webhook Event is received](https://docs.medusajs.com/resources/commerce-modules/payment/webhook-events).
|
||||
*
|
||||
*
|
||||
* You can use this workflow within your own customizations or custom workflows, allowing you
|
||||
* to process a payment in your custom flows based on a webhook action.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const { result } = await processPaymentWorkflow(container)
|
||||
* .run({
|
||||
@@ -31,9 +31,9 @@ export const processPaymentWorkflowId = "process-payment-workflow"
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
*
|
||||
*
|
||||
* @summary
|
||||
*
|
||||
*
|
||||
* Process a payment based on a webhook event.
|
||||
*/
|
||||
export const processPaymentWorkflow = createWorkflow(
|
||||
@@ -66,20 +66,6 @@ export const processPaymentWorkflow = createWorkflow(
|
||||
name: "cart-payment-query",
|
||||
})
|
||||
|
||||
when({ cartPaymentCollection }, ({ cartPaymentCollection }) => {
|
||||
return !!cartPaymentCollection.data.length
|
||||
}).then(() => {
|
||||
completeCartWorkflow
|
||||
.runAsStep({
|
||||
input: {
|
||||
id: cartPaymentCollection.data[0].cart_id,
|
||||
},
|
||||
})
|
||||
.config({
|
||||
continueOnPermanentFailure: true, // Continue payment processing even if cart completion fails
|
||||
})
|
||||
})
|
||||
|
||||
when({ input, paymentData }, ({ input, paymentData }) => {
|
||||
return (
|
||||
input.action === PaymentActions.SUCCESSFUL && !!paymentData.data.length
|
||||
@@ -110,5 +96,19 @@ export const processPaymentWorkflow = createWorkflow(
|
||||
context: {},
|
||||
})
|
||||
})
|
||||
|
||||
when({ cartPaymentCollection }, ({ cartPaymentCollection }) => {
|
||||
return !!cartPaymentCollection.data.length
|
||||
}).then(() => {
|
||||
completeCartWorkflow
|
||||
.runAsStep({
|
||||
input: {
|
||||
id: cartPaymentCollection.data[0].cart_id,
|
||||
},
|
||||
})
|
||||
.config({
|
||||
continueOnPermanentFailure: true, // Continue payment processing even if cart completion fails
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -449,6 +449,12 @@ export interface IPaymentModuleService extends IModuleService {
|
||||
sharedContext?: Context
|
||||
): Promise<PaymentSessionDTO>
|
||||
|
||||
retrievePayment(
|
||||
paymentId: string,
|
||||
config?: FindConfig<PaymentDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<PaymentDTO>
|
||||
|
||||
/* ********** PAYMENT SESSION ********** */
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user