diff --git a/packages/core/core-flows/src/payment-collection/steps/cancel-payment-session.ts b/packages/core/core-flows/src/payment-collection/steps/cancel-payment-session.ts new file mode 100644 index 0000000000000..3d17762a303d0 --- /dev/null +++ b/packages/core/core-flows/src/payment-collection/steps/cancel-payment-session.ts @@ -0,0 +1,46 @@ +import { + IPaymentModuleService, + Logger, + PaymentSessionDTO, +} from "@medusajs/types" +import { + ContainerRegistrationKeys, + ModuleRegistrationName, + promiseAll, +} from "@medusajs/utils" +import { createStep, StepResponse } from "@medusajs/workflows-sdk" + +export type CancelPaymentSessionStepInput = { + id: string | string[] +} + +export const cancelPaymentSessionStepId = "cancel-payment-session-step" + +/** + * This step cancels one or more payment sessions. + */ +export const cancelPaymentSessionStep = createStep( + cancelPaymentSessionStepId, + async (input: CancelPaymentSessionStepInput, { container }) => { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) + const paymentModule = container.resolve( + ModuleRegistrationName.PAYMENT + ) + const ids = Array.isArray(input.id) ? input.id : [input.id] + const sessions = await promiseAll( + ids.map((id) => + paymentModule.cancelPaymentSession(id).catch((e) => { + logger.error( + `Error was thrown trying to cancel payment session - ${id} - ${e}` + ) + }) + ) + ) + + return new StepResponse( + Array.isArray(input.id) + ? (sessions.filter(Boolean) as PaymentSessionDTO[]) + : sessions[0] + ) + } +) diff --git a/packages/core/core-flows/src/payment-collection/workflows/cancel-payment-session.ts b/packages/core/core-flows/src/payment-collection/workflows/cancel-payment-session.ts new file mode 100644 index 0000000000000..c35c761c18ca5 --- /dev/null +++ b/packages/core/core-flows/src/payment-collection/workflows/cancel-payment-session.ts @@ -0,0 +1,18 @@ +import { + WorkflowData, + WorkflowResponse, + createWorkflow, +} from "@medusajs/workflows-sdk" +import { cancelPaymentSessionStep } from "../steps/cancel-payment-session" + +export const cancelPaymentSessionWorkflowId = "cancel-payment-session-workflow" + +/** + * This workflow cancels one or more payment sessions. + */ +export const cancelPaymentSessionWorkflow = createWorkflow( + cancelPaymentSessionWorkflowId, + (input: WorkflowData<{ id: string | string[] }>) => { + return new WorkflowResponse(cancelPaymentSessionStep(input)) + } +) diff --git a/packages/core/types/src/payment/service.ts b/packages/core/types/src/payment/service.ts index d68a0ec89b43e..22c919fd37c1d 100644 --- a/packages/core/types/src/payment/service.ts +++ b/packages/core/types/src/payment/service.ts @@ -529,6 +529,21 @@ export interface IPaymentModuleService extends IModuleService { sharedContext?: Context ): Promise + /** + * This method cancels a payment session. + * + * @param {string} id - The ID of the payment session. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The payment session's details. + * + * @example + * const paymentSession = await paymentModuleService.cancelPaymentSession("payses_123") + */ + cancelPaymentSession( + id: string, + sharedContext?: Context + ): Promise + /** * This method retrieves a paginated list of payment sessions based on optional filters and configuration. * diff --git a/packages/modules/payment/src/services/payment-module.ts b/packages/modules/payment/src/services/payment-module.ts index 70c519f0433bd..d8c2ea191e076 100644 --- a/packages/modules/payment/src/services/payment-module.ts +++ b/packages/modules/payment/src/services/payment-module.ts @@ -489,6 +489,77 @@ export default class PaymentModuleService ) } + @InjectManager("baseRepository_") + async cancelPaymentSession( + id: string, + @MedusaContext() sharedContext?: Context + ): Promise { + const paymentSession = await this.paymentSessionService_.retrieve( + id, + { select: ["data", "provider_id"] }, + sharedContext + ) + + await this.handleProviderSessionResponse_( + await this.paymentProviderService_.cancelPayment( + paymentSession.provider_id, + paymentSession.data + ), + sharedContext + ) + + return this.retrievePaymentSession(paymentSession.id, {}, sharedContext) + } + + @InjectManager("baseRepository_") + private async cancelPaymentSession_( + id: string, + data?: PaymentProviderSessionResponse["data"], + @MedusaContext() sharedContext?: Context + ): Promise { + let session = await this.paymentSessionService_.retrieve( + id, + { select: ["status"], relations: ["payment"] }, + sharedContext + ) + + if (session.status === PaymentSessionStatus.CANCELED) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `The payment session: ${session.id} has been canceled.` + ) + } + + if ( + session.status !== PaymentSessionStatus.PENDING && + session.status !== PaymentSessionStatus.REQUIRES_MORE && + session.status !== PaymentSessionStatus.AUTHORIZED + ) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `The payment session: ${session.id} cannot be canceled.` + ) + } + + if (session.payment) { + await this.paymentService_.update( + { + id: session.payment.id, + canceled_at: new Date(), + data, + }, + sharedContext + ) + } + + session = await this.paymentSessionService_.update( + { id, data, status: PaymentSessionStatus.CANCELED }, + sharedContext + ) + + return session + } + @InjectManager("baseRepository_") // @ts-expect-error async retrievePaymentSession( @@ -1075,43 +1146,12 @@ export default class PaymentModuleService break } case "canceled": { - await this.paymentSessionService_.update( - { id: session_id, data, status }, - sharedContext - ) - const session = await this.paymentSessionService_.retrieve( + const session = await this.cancelPaymentSession_( session_id, - { - select: ["payment_collection_id"], - relations: ["payment", "payment.captures"], - }, + data, sharedContext ) - if (session.payment) { - if (session.payment.captures.length > 0) { - await this.paymentService_.update( - { - id: session.payment.id, - captured_at: new Date(), - data, - }, - sharedContext - ) - } else { - await this.paymentService_.update( - { - id: session.payment.id, - canceled_at: new Date(), - data, - }, - sharedContext - ) - } - - await this.maybeUpdatePaymentCollection_( - session.payment_collection_id - ) - } + await this.maybeUpdatePaymentCollection_(session.payment_collection_id) break } default: {