Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core-flows): capture before order created #9980

Merged
merged 15 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
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
Loading