Skip to content

Commit

Permalink
fix(core-flows): capture before order created (#9980)
Browse files Browse the repository at this point in the history
What:
 When `autocapture` is enabled, the webhook is processed before the order was created.
 The payment processing workflows were merged into a single one
 
FIXES: SUP-118, SUP-9

#9998
  • Loading branch information
carlos-r-l-rodrigues authored Nov 12, 2024
1 parent 9e40f34 commit 2344012
Show file tree
Hide file tree
Showing 12 changed files with 136 additions and 69 deletions.
6 changes: 6 additions & 0 deletions .changeset/thirty-lamps-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@medusajs/core-flows": patch
"@medusajs/medusa": patch
---

Create Order before payment capture
15 changes: 13 additions & 2 deletions packages/core/core-flows/src/cart/workflows/complete-cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const completeCartWorkflow = createWorkflow(

const paymentSessions = validateCartPaymentsStep({ cart })

authorizePaymentSessionStep({
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,
Expand All @@ -103,7 +103,17 @@ export const completeCartWorkflow = createWorkflow(
}
})

const cartToOrder = transform({ cart }, ({ cart }) => {
const cartToOrder = transform({ cart, payment }, ({ cart, payment }) => {
const transactions =
payment?.captures?.map((capture) => {
return {
amount: capture.raw_amount ?? capture.amount,
currency_code: payment.currency_code,
reference: "capture",
reference_id: capture.id,
}
}) ?? []

const allItems = (cart.items ?? []).map((item) => {
return prepareLineItemData({
item,
Expand Down Expand Up @@ -158,6 +168,7 @@ export const completeCartWorkflow = createWorkflow(
shipping_methods: shippingMethods,
metadata: cart.metadata,
promo_codes: promoCodes,
transactions,
}
})

Expand Down
35 changes: 30 additions & 5 deletions packages/core/core-flows/src/order/steps/add-order-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,44 @@ import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"

export const addOrderTransactionStepId = "add-order-transaction"
/**
* This step creates an order transaction.
* This step creates order transactions.
*/
export const addOrderTransactionStep = createStep(
addOrderTransactionStepId,
async (data: CreateOrderTransactionDTO, { container }) => {
async (
data: CreateOrderTransactionDTO | CreateOrderTransactionDTO[],
{ container }
) => {
const service = container.resolve(Modules.ORDER)

const created = await service.addOrderTransactions(data)
const trxsData = Array.isArray(data) ? data : [data]

return new StepResponse(created, created.id)
for (const trx of trxsData) {
const existing = await service.listOrderTransactions(
{
order_id: trx.order_id,
reference: trx.reference,
reference_id: trx.reference_id,
},
{
select: ["id"],
}
)

if (existing.length) {
return new StepResponse(null)
}
}

const created = await service.addOrderTransactions(trxsData)

return new StepResponse(
Array.isArray(data) ? created : created[0],
created.map((c) => c.id)
)
},
async (id, { container }) => {
if (!id) {
if (!id?.length) {
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ export const authorizePaymentSessionStep = createStep(
)
}

const paymentSession = await paymentModule.retrievePaymentSession(input.id)
const paymentSession = await paymentModule.retrievePaymentSession(
input.id,
{
relations: ["payment", "payment.captures"],
}
)

// Throw a special error type when the status is requires_more as it requires a specific further action
// from the consumer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ export const capturePaymentWorkflow = createWorkflow(
const orderTransactionData = transform(
{ input, payment, orderPayment },
({ input, payment, orderPayment }) => {
return {
order_id: orderPayment.order.id,
amount: input.amount ?? payment.raw_amount ?? payment.amount,
currency_code: payment.currency_code,
reference_id: payment.id,
reference: "capture",
}
return payment.captures?.map((capture) => {
return {
order_id: orderPayment.order.id,
amount: input.amount ?? capture.raw_amount ?? capture.amount,
currency_code: payment.currency_code,
reference_id: capture.id,
reference: "capture",
}
})
}
)

Expand Down
2 changes: 0 additions & 2 deletions packages/core/core-flows/src/payment/workflows/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
export * from "./capture-payment"
export * from "./on-payment-processed"
export * from "./process-payment"
export * from "./refund-payment"

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { WebhookActionResult } from "@medusajs/types"
import { PaymentActions } from "@medusajs/utils"
import { createWorkflow, when } from "@medusajs/workflows-sdk"
import { completeCartWorkflow } from "../../cart/workflows/complete-cart"
import { useQueryStep } from "../../common/steps/use-query"
import { authorizePaymentSessionStep } from "../steps"
import { capturePaymentWorkflow } from "./capture-payment"
Expand All @@ -15,6 +16,41 @@ export const processPaymentWorkflow = createWorkflow(
entity: "payment",
fields: ["id"],
filters: { payment_session_id: input.data?.session_id },
}).config({
name: "payment-query",
})

const paymentSessionResult = useQueryStep({
entity: "payment_session",
fields: ["payment_collection_id"],
filters: { id: input.data?.session_id },
}).config({
name: "payment-session-query",
})

const cartPaymentCollection = useQueryStep({
entity: "cart_payment_collection",
fields: ["cart_id"],
filters: {
payment_collection_id:
paymentSessionResult.data[0].payment_collection_id,
},
}).config({
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 }, ({ input }) => {
Expand All @@ -31,8 +67,12 @@ export const processPaymentWorkflow = createWorkflow(
})

when({ input }, ({ input }) => {
// Authorize payment session if no Cart is linked to the payment
// When associated with a Cart, the complete cart workflow will handle the authorization
return (
input.action === PaymentActions.AUTHORIZED && !!input.data?.session_id
!cartPaymentCollection.data.length &&
input.action === PaymentActions.AUTHORIZED &&
!!input.data?.session_id
)
}).then(() => {
authorizePaymentSessionStep({
Expand Down
10 changes: 10 additions & 0 deletions packages/core/types/src/payment/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,11 @@ export interface CaptureDTO {
*/
amount: BigNumberValue

/**
* The raw captured amount.
*/
raw_amount?: BigNumberValue

/**
* The creation date of the capture.
*/
Expand Down Expand Up @@ -502,6 +507,11 @@ export interface RefundDTO {
*/
amount: BigNumberValue

/**
* The raw refunded amount.
*/
raw_amount?: BigNumberValue

/**
* The id of the refund_reason that is associated with the refund
*/
Expand Down
12 changes: 2 additions & 10 deletions packages/medusa/src/subscribers/payment-webhook.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
onPaymentProcessedWorkflow,
processPaymentWorkflow,
} from "@medusajs/core-flows"
import { processPaymentWorkflow } from "@medusajs/core-flows"
import {
IPaymentModuleService,
ProviderWebhookPayload,
Expand Down Expand Up @@ -29,7 +26,7 @@ export default async function paymentWebhookhandler({
const input = event.data

if (
(input.payload.rawData as unknown as SerializedBuffer).type === "Buffer"
(input.payload?.rawData as unknown as SerializedBuffer)?.type === "Buffer"
) {
input.payload.rawData = Buffer.from(
(input.payload.rawData as unknown as SerializedBuffer).data
Expand All @@ -49,11 +46,6 @@ export default async function paymentWebhookhandler({
await processPaymentWorkflow(container).run({
input: processedEvent,
})

// We process the intended side effects of payment processing separately.
await onPaymentProcessedWorkflow(container).run({
input: processedEvent,
})
}

export const config: SubscriberConfig = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ describe("LocalEventBusService", () => {
data: { test: "1234" },
metadata: { eventGroupId: "test" },
name: "test-event",
options: {},
})

jest.clearAllMocks()
Expand Down
21 changes: 18 additions & 3 deletions packages/modules/event-bus-local/src/services/event-bus-local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "@medusajs/framework/types"
import { AbstractEventBusModuleService } from "@medusajs/framework/utils"
import { EventEmitter } from "events"
import { setTimeout } from "timers/promises"
import { ulid } from "ulid"

type InjectedDependencies = {
Expand Down Expand Up @@ -69,7 +70,10 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
)
}

await this.groupOrEmitEvent(eventData)
await this.groupOrEmitEvent({
...eventData,
options,
})
}
}

Expand All @@ -86,7 +90,13 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
await this.groupEvent(eventGroupId, eventData)
} else {
const { options, ...eventBody } = eventData
this.eventEmitter_.emit(eventData.name, eventBody)

const options_ = options as { delay: number }
const delay = options?.delay ? setTimeout : async () => {}

delay(options_?.delay).then(() =>
this.eventEmitter_.emit(eventData.name, eventBody)
)
}
}

Expand All @@ -108,7 +118,12 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
for (const event of groupedEvents) {
const { options, ...eventBody } = event

this.eventEmitter_.emit(event.name, eventBody)
const options_ = options as { delay: number }
const delay = options?.delay ? setTimeout : async () => {}

delay(options_?.delay).then(() =>
this.eventEmitter_.emit(event.name, eventBody)
)
}

await this.clearGroupedEvents(eventGroupId)
Expand Down

0 comments on commit 2344012

Please sign in to comment.