fix: Idempotent cart completion (#9231)

What
- Store result of cart-completion workflow for three days by default
  - This enables the built-in idempotency mechanism to kick-in, provided the same transaction ID is used on workflow executions
- Return order from cart-completion workflow if the cart has already been completed
  - In case transaction ID is not used on workflow executions, we still only want to complete a cart once
This commit is contained in:
Oli Juhl
2024-10-04 12:01:09 +00:00
committed by GitHub
parent 6055f4c9cf
commit f7472a6fa6
14 changed files with 809 additions and 525 deletions
@@ -20,7 +20,7 @@ import {
ProductStatus, ProductStatus,
PromotionRuleOperator, PromotionRuleOperator,
PromotionType, PromotionType,
RuleOperator, RuleOperator
} from "@medusajs/utils" } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "medusa-test-utils" import { medusaIntegrationTestRunner } from "medusa-test-utils"
import { import {
@@ -3085,6 +3085,67 @@ medusaIntegrationTestRunner({
}) })
}) })
it("should return order when cart is already completed", async () => {
const cart = (
await api.post(
`/store/carts`,
{
currency_code: "usd",
email: "tony@stark-industries.com",
shipping_address: {
address_1: "test address 1",
address_2: "test address 2",
city: "ny",
country_code: "us",
province: "ny",
postal_code: "94016",
},
sales_channel_id: salesChannel.id,
items: [{ quantity: 1, variant_id: product.variants[0].id }],
},
storeHeaders
)
).data.cart
const paymentCollection = (
await api.post(
`/store/payment-collections`,
{
cart_id: cart.id,
},
storeHeaders
)
).data.payment_collection
await api.post(
`/store/payment-collections/${paymentCollection.id}/payment-sessions`,
{ provider_id: "pp_system_default" },
storeHeaders
)
await api.post(`/store/carts/${cart.id}/complete`, {}, storeHeaders)
const cartRefetch = (
await api.get(`/store/carts/${cart.id}`, storeHeaders)
).data.cart
expect(cartRefetch.completed_at).toBeTruthy()
const order = await api.post(
`/store/carts/${cart.id}/complete`,
{},
storeHeaders
)
expect(order.status).toEqual(200)
expect(order.data).toEqual({
type: "order",
order: expect.objectContaining({
id: expect.any(String),
}),
})
})
it("should return cart when payment authorization fails", async () => { it("should return cart when payment authorization fails", async () => {
const paymentModuleService = appContainer.resolve(Modules.PAYMENT) const paymentModuleService = appContainer.resolve(Modules.PAYMENT)
const authorizePaymentSessionSpy = jest.spyOn( const authorizePaymentSessionSpy = jest.spyOn(
@@ -34,6 +34,7 @@ export const validateCartPaymentsStep = createStep(
const processablePaymentStatuses = [ const processablePaymentStatuses = [
PaymentSessionStatus.PENDING, PaymentSessionStatus.PENDING,
PaymentSessionStatus.REQUIRES_MORE, PaymentSessionStatus.REQUIRES_MORE,
PaymentSessionStatus.AUTHORIZED, // E.g. payment was authorized, but the cart was not completed
] ]
const paymentsToProcess = paymentCollection.payment_sessions?.filter((ps) => const paymentsToProcess = paymentCollection.payment_sessions?.filter((ps) =>
@@ -1,7 +1,6 @@
import { import {
CartWorkflowDTO, CartWorkflowDTO,
OrderDTO, UsageComputedActions
UsageComputedActions,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
Modules, Modules,
@@ -12,6 +11,7 @@ import {
createWorkflow, createWorkflow,
parallelize, parallelize,
transform, transform,
when,
WorkflowData, WorkflowData,
WorkflowResponse, WorkflowResponse,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
@@ -20,12 +20,12 @@ import {
emitEventStep, emitEventStep,
useRemoteQueryStep, useRemoteQueryStep,
} from "../../common" } from "../../common"
import { useQueryStep } from "../../common/steps/use-query"
import { createOrdersStep } from "../../order/steps/create-orders" import { createOrdersStep } from "../../order/steps/create-orders"
import { authorizePaymentSessionStep } from "../../payment/steps/authorize-payment-session" import { authorizePaymentSessionStep } from "../../payment/steps/authorize-payment-session"
import { registerUsageStep } from "../../promotion/steps/register-usage" import { registerUsageStep } from "../../promotion/steps/register-usage"
import { updateCartsStep, validateCartPaymentsStep } from "../steps" import { updateCartsStep, validateCartPaymentsStep } from "../steps"
import { reserveInventoryStep } from "../steps/reserve-inventory" import { reserveInventoryStep } from "../steps/reserve-inventory"
import { validateCartStep } from "../steps/validate-cart"
import { completeCartFields } from "../utils/fields" import { completeCartFields } from "../utils/fields"
import { prepareConfirmInventoryInput } from "../utils/prepare-confirm-inventory-input" import { prepareConfirmInventoryInput } from "../utils/prepare-confirm-inventory-input"
import { import {
@@ -38,36 +38,56 @@ export type CompleteCartWorkflowInput = {
id: string id: string
} }
export const THREE_DAYS = 60 * 60 * 24 * 3
export const completeCartWorkflowId = "complete-cart" export const completeCartWorkflowId = "complete-cart"
/** /**
* This workflow completes a cart. * This workflow completes a cart.
*/ */
export const completeCartWorkflow = createWorkflow( export const completeCartWorkflow = createWorkflow(
completeCartWorkflowId, {
name: completeCartWorkflowId,
store: true,
idempotent: true,
// 3 days of retention time
retentionTime: THREE_DAYS,
},
( (
input: WorkflowData<CompleteCartWorkflowInput> input: WorkflowData<CompleteCartWorkflowInput>
): WorkflowResponse<OrderDTO> => { ): WorkflowResponse<{ id: string }> => {
const cart = useRemoteQueryStep({ const orderCart = useQueryStep({
entry_point: "cart", entity: "order_cart",
fields: completeCartFields, fields: ["cart_id", "order_id"],
variables: { id: input.id }, filters: { cart_id: input.id },
list: false,
}) })
validateCartStep({ cart }) const orderId = transform({ orderCart }, ({ orderCart }) => {
return orderCart.data[0]?.order_id
const paymentSessions = validateCartPaymentsStep({ cart })
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,
context: { cart_id: cart.id },
}) })
const { variants, sales_channel_id } = transform({ cart }, (data) => { // If order ID does not exist, we are completing the cart for the first time
const allItems: any[] = [] const order = when({ orderId }, ({ orderId }) => {
const allVariants: any[] = [] return !orderId
}).then(() => {
const cart = useRemoteQueryStep({
entry_point: "cart",
fields: completeCartFields,
variables: { id: input.id },
list: false,
})
const paymentSessions = validateCartPaymentsStep({ cart })
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,
context: { cart_id: cart.id },
})
const { variants, sales_channel_id } = transform({ cart }, (data) => {
const allItems: any[] = []
const allVariants: any[] = []
data.cart?.items?.forEach((item) => { data.cart?.items?.forEach((item) => {
allItems.push({ allItems.push({
@@ -78,163 +98,164 @@ export const completeCartWorkflow = createWorkflow(
allVariants.push(item.variant) allVariants.push(item.variant)
}) })
return {
variants: allVariants,
items: allItems,
sales_channel_id: data.cart.sales_channel_id,
}
})
const finalCart = useRemoteQueryStep({
entry_point: "cart",
fields: completeCartFields,
variables: { id: input.id },
list: false,
}).config({ name: "final-cart" })
const cartToOrder = transform({ cart }, ({ cart }) => {
const allItems = (cart.items ?? []).map((item) => {
return prepareLineItemData({
item,
variant: item.variant,
unitPrice: item.raw_unit_price ?? item.unit_price,
isTaxInclusive: item.is_tax_inclusive,
quantity: item.raw_quantity ?? item.quantity,
metadata: item?.metadata,
taxLines: item.tax_lines ?? [],
adjustments: item.adjustments ?? [],
})
})
const shippingMethods = (cart.shipping_methods ?? []).map((sm) => {
return { return {
name: sm.name, variants: allVariants,
description: sm.description, items: allItems,
amount: sm.raw_amount ?? sm.amount, sales_channel_id: data.cart.sales_channel_id,
is_tax_inclusive: sm.is_tax_inclusive,
shipping_option_id: sm.shipping_option_id,
data: sm.data,
metadata: sm.metadata,
tax_lines: prepareTaxLinesData(sm.tax_lines ?? []),
adjustments: prepareAdjustmentsData(sm.adjustments ?? []),
} }
}) })
const itemAdjustments = allItems const cartToOrder = transform({ cart }, ({ cart }) => {
.map((item) => item.adjustments ?? []) const allItems = (cart.items ?? []).map((item) => {
.flat(1) return prepareLineItemData({
const shippingAdjustments = shippingMethods item,
.map((sm) => sm.adjustments ?? []) variant: item.variant,
.flat(1) unitPrice: item.raw_unit_price ?? item.unit_price,
isTaxInclusive: item.is_tax_inclusive,
quantity: item.raw_quantity ?? item.quantity,
metadata: item?.metadata,
taxLines: item.tax_lines ?? [],
adjustments: item.adjustments ?? [],
})
})
const promoCodes = [...itemAdjustments, ...shippingAdjustments] const shippingMethods = (cart.shipping_methods ?? []).map((sm) => {
.map((adjustment) => adjustment.code) return {
.filter((code) => Boolean) as string[] name: sm.name,
description: sm.description,
amount: sm.raw_amount ?? sm.amount,
is_tax_inclusive: sm.is_tax_inclusive,
shipping_option_id: sm.shipping_option_id,
data: sm.data,
metadata: sm.metadata,
tax_lines: prepareTaxLinesData(sm.tax_lines ?? []),
adjustments: prepareAdjustmentsData(sm.adjustments ?? []),
}
})
return { const itemAdjustments = allItems
region_id: cart.region?.id, .map((item) => item.adjustments ?? [])
customer_id: cart.customer?.id, .flat(1)
sales_channel_id: cart.sales_channel_id, const shippingAdjustments = shippingMethods
status: OrderStatus.PENDING, .map((sm) => sm.adjustments ?? [])
email: cart.email, .flat(1)
currency_code: cart.currency_code,
shipping_address: cart.shipping_address,
billing_address: cart.billing_address,
no_notification: false,
items: allItems,
shipping_methods: shippingMethods,
metadata: cart.metadata,
promo_codes: promoCodes,
}
})
const createdOrders = createOrdersStep([cartToOrder]) const promoCodes = [...itemAdjustments, ...shippingAdjustments]
.map((adjustment) => adjustment.code)
.filter(Boolean)
const order = transform( return {
{ createdOrders }, region_id: cart.region?.id,
({ createdOrders }) => createdOrders[0] customer_id: cart.customer?.id,
) sales_channel_id: cart.sales_channel_id,
status: OrderStatus.PENDING,
email: cart.email,
currency_code: cart.currency_code,
shipping_address: cart.shipping_address,
billing_address: cart.billing_address,
no_notification: false,
items: allItems,
shipping_methods: shippingMethods,
metadata: cart.metadata,
promo_codes: promoCodes,
}
})
const reservationItemsData = transform({ order }, ({ order }) => const createdOrders = createOrdersStep([cartToOrder])
order.items!.map((i) => ({
variant_id: i.variant_id,
quantity: i.quantity,
id: i.id,
}))
)
const formatedInventoryItems = transform( const createdOrder = transform({ createdOrders }, ({ createdOrders }) => {
{ return createdOrders?.[0] ?? undefined
input: { })
sales_channel_id,
variants,
items: reservationItemsData,
},
},
prepareConfirmInventoryInput
)
const updateCompletedAt = transform({ cart }, ({ cart }) => { const reservationItemsData = transform(
return { { createdOrder },
id: cart.id, ({ createdOrder }) =>
completed_at: new Date(), createdOrder.items!.map((i) => ({
} variant_id: i.variant_id,
}) quantity: i.quantity,
id: i.id,
}))
)
parallelize( const formatedInventoryItems = transform(
createRemoteLinkStep([
{ {
[Modules.ORDER]: { order_id: order.id }, input: {
[Modules.CART]: { cart_id: finalCart.id }, sales_channel_id,
}, variants,
{ items: reservationItemsData,
[Modules.ORDER]: { order_id: order.id },
[Modules.PAYMENT]: {
payment_collection_id: cart.payment_collection.id,
}, },
}, },
]), prepareConfirmInventoryInput
updateCartsStep([updateCompletedAt]), )
reserveInventoryStep(formatedInventoryItems),
emitEventStep({ const updateCompletedAt = transform({ cart }, ({ cart }) => {
eventName: OrderWorkflowEvents.PLACED, return {
data: { id: order.id }, id: cart.id,
completed_at: new Date(),
}
}) })
)
const promotionUsage = transform( parallelize(
{ cart }, createRemoteLinkStep([
({ cart }: { cart: CartWorkflowDTO }) => { {
const promotionUsage: UsageComputedActions[] = [] [Modules.ORDER]: { order_id: createdOrder.id },
[Modules.CART]: { cart_id: cart.id },
},
{
[Modules.ORDER]: { order_id: createdOrder.id },
[Modules.PAYMENT]: {
payment_collection_id: cart.payment_collection.id,
},
},
]),
updateCartsStep([updateCompletedAt]),
reserveInventoryStep(formatedInventoryItems),
emitEventStep({
eventName: OrderWorkflowEvents.PLACED,
data: { id: createdOrder.id },
})
)
const itemAdjustments = (cart.items ?? []) const promotionUsage = transform(
.map((item) => item.adjustments ?? []) { cart },
.flat(1) ({ cart }: { cart: CartWorkflowDTO }) => {
const promotionUsage: UsageComputedActions[] = []
const shippingAdjustments = (cart.shipping_methods ?? []) const itemAdjustments = (cart.items ?? [])
.map((item) => item.adjustments ?? []) .map((item) => item.adjustments ?? [])
.flat(1) .flat(1)
for (const adjustment of itemAdjustments) { const shippingAdjustments = (cart.shipping_methods ?? [])
promotionUsage.push({ .map((item) => item.adjustments ?? [])
amount: adjustment.amount, .flat(1)
code: adjustment.code!,
}) for (const adjustment of itemAdjustments) {
promotionUsage.push({
amount: adjustment.amount,
code: adjustment.code!,
})
}
for (const adjustment of shippingAdjustments) {
promotionUsage.push({
amount: adjustment.amount,
code: adjustment.code!,
})
}
return promotionUsage
} }
)
for (const adjustment of shippingAdjustments) { registerUsageStep(promotionUsage)
promotionUsage.push({
amount: adjustment.amount,
code: adjustment.code!,
})
}
return promotionUsage return createdOrder
} })
)
registerUsageStep(promotionUsage) const result = transform({ order, orderId }, ({ order, orderId }) => {
return { id: order?.id ?? orderId }
})
return new WorkflowResponse(order) return new WorkflowResponse(result)
} }
) )
@@ -0,0 +1,21 @@
import { ContainerRegistrationKeys } from "@medusajs/utils"
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
interface QueryInput {
entity: string
fields: string[]
filters?: Record<string, unknown>
context?: any
}
export const useQueryStepId = "use-query"
export const useQueryStep = createStep(
useQueryStepId,
async (data: QueryInput, { container }) => {
const query = container.resolve(ContainerRegistrationKeys.QUERY)
const result = await query.graph(data)
return new StepResponse(result)
}
)
@@ -1,2 +1,5 @@
export * from "./capture-payment" export * from "./capture-payment"
export * from "./on-payment-processed"
export * from "./process-payment"
export * from "./refund-payment" export * from "./refund-payment"
@@ -0,0 +1,38 @@
import { WebhookActionResult } from "@medusajs/types"
import { createWorkflow, when } from "@medusajs/workflows-sdk"
import { completeCartWorkflow } from "../../cart"
import { useRemoteQueryStep } from "../../common"
import { useQueryStep } from "../../common/steps/use-query"
export const onPaymentProcessedWorkflowId = "on-payment-processed-workflow"
export const onPaymentProcessedWorkflow = createWorkflow(
onPaymentProcessedWorkflowId,
(input: WebhookActionResult) => {
const paymentSessionResult = useRemoteQueryStep({
entry_point: "payment_session",
fields: ["payment_collection_id"],
variables: { filters: { id: input.data?.session_id } },
list: false,
})
const cartPaymentCollection = useQueryStep({
entity: "cart_payment_collection",
fields: ["cart_id"],
filters: {
payment_collection_id: paymentSessionResult.payment_collection_id,
},
})
when({ cartPaymentCollection }, ({ cartPaymentCollection }) => {
return !!cartPaymentCollection.data.length
}).then(() => {
completeCartWorkflow.runAsStep({
input: {
id: cartPaymentCollection.data[0].cart_id,
},
})
})
// TODO: Add more cases down the line, e.g. order payments
}
)
@@ -0,0 +1,44 @@
import { WebhookActionResult } from "@medusajs/types"
import { PaymentActions } from "@medusajs/utils"
import { createWorkflow, when } from "@medusajs/workflows-sdk"
import { useQueryStep } from "../../common/steps/use-query"
import { authorizePaymentSessionStep } from "../steps"
import { capturePaymentWorkflow } from "./capture-payment"
interface ProcessPaymentWorkflowInput extends WebhookActionResult {}
export const processPaymentWorkflowId = "process-payment-workflow"
export const processPaymentWorkflow = createWorkflow(
processPaymentWorkflowId,
(input: ProcessPaymentWorkflowInput) => {
const paymentData = useQueryStep({
entity: "payment",
fields: ["id"],
filters: { payment_session_id: input.data?.session_id },
})
when({ input }, ({ input }) => {
return (
input.action === PaymentActions.SUCCESSFUL && !!paymentData.data.length
)
}).then(() => {
capturePaymentWorkflow.runAsStep({
input: {
payment_id: paymentData.data[0].id,
amount: input.data?.amount,
},
})
})
when({ input }, ({ input }) => {
return (
input.action === PaymentActions.AUTHORIZED && !!input.data?.session_id
)
}).then(() => {
authorizePaymentSessionStep({
id: input.data!.session_id,
context: {},
})
})
}
)
@@ -1148,7 +1148,7 @@ export class TransactionOrchestrator extends EventEmitter {
queue.push({ obj: obj[key], level: [...level] }) queue.push({ obj: obj[key], level: [...level] })
} else if (key === "action") { } else if (key === "action") {
if (actionNames.has(obj.action)) { if (actionNames.has(obj.action)) {
throw new Error(`Action "${obj.action}" is already defined.`) throw new Error(`Step ${obj.action} is already defined in workflow.`)
} }
actionNames.add(obj.action) actionNames.add(obj.action)
+10 -17
View File
@@ -195,24 +195,17 @@ export type WebhookActionData = {
* *
* The actions that the payment provider informs the Payment Module to perform. * The actions that the payment provider informs the Payment Module to perform.
*/ */
export type WebhookActionResult = export type WebhookActionResult = {
| { /**
/** * Normalized events from payment provider to internal payment module events.
* Received an event that is not processable. */
*/ action: PaymentActions
action: "not_supported"
}
| {
/**
* Normalized events from payment provider to internal payment module events.
*/
action: PaymentActions
/** /**
* The webhook action's details. * The webhook action's details.
*/ */
data: WebhookActionData data?: WebhookActionData
} }
export interface IPaymentProvider { export interface IPaymentProvider {
/** /**
+4 -3
View File
@@ -31,6 +31,7 @@ import {
UpdateRefundReasonDTO, UpdateRefundReasonDTO,
UpsertPaymentCollectionDTO, UpsertPaymentCollectionDTO,
} from "./mutations" } from "./mutations"
import { WebhookActionResult } from "./provider"
/** /**
* The main service interface for the Payment Module. * The main service interface for the Payment Module.
@@ -1055,7 +1056,7 @@ export interface IPaymentModuleService extends IModuleService {
/* ********** HOOKS ********** */ /* ********** HOOKS ********** */
/** /**
* This method handles a webhook event with the associated payment provider. * This method retrieves webhook event data with the associated payment provider.
* *
* Learn more about handling webhook events in [this guide](https://docs.medusajs.com/experimental/payment/webhook-events/) * Learn more about handling webhook events in [this guide](https://docs.medusajs.com/experimental/payment/webhook-events/)
* *
@@ -1066,7 +1067,7 @@ export interface IPaymentModuleService extends IModuleService {
* In the following example, `req` is an instance of `MedusaRequest`: * In the following example, `req` is an instance of `MedusaRequest`:
* *
* ```ts * ```ts
* await paymentModuleService.processEvent({ * const dataAndAction = await paymentModuleService.getWebhookActionAndData({
* provider: "stripe", * provider: "stripe",
* payload: { * payload: {
* data: req.body, * data: req.body,
@@ -1076,7 +1077,7 @@ export interface IPaymentModuleService extends IModuleService {
* }) * })
* ``` * ```
*/ */
processEvent(data: ProviderWebhookPayload): Promise<void> getWebhookActionAndData(data: ProviderWebhookPayload): Promise<WebhookActionResult>
} }
/** /**
@@ -1,11 +1,13 @@
import { completeCartWorkflow } from "@medusajs/core-flows" import { completeCartWorkflow } from "@medusajs/core-flows"
import { MedusaError } from "@medusajs/framework/utils"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { prepareRetrieveQuery } from "@medusajs/framework" import { prepareRetrieveQuery } from "@medusajs/framework"
import { refetchOrder } from "../../../orders/helpers" import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { HttpTypes } from "@medusajs/framework/types"
import {
ContainerRegistrationKeys,
MedusaError,
} from "@medusajs/framework/utils"
import { refetchCart } from "../../helpers" import { refetchCart } from "../../helpers"
import { defaultStoreCartFields } from "../../query-config" import { defaultStoreCartFields } from "../../query-config"
import { HttpTypes } from "@medusajs/framework/types"
export const POST = async ( export const POST = async (
req: MedusaRequest, req: MedusaRequest,
@@ -19,6 +21,8 @@ export const POST = async (
throwOnError: false, throwOnError: false,
}) })
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
// When an error occurs on the workflow, its potentially got to with cart validations, payments // When an error occurs on the workflow, its potentially got to with cart validations, payments
// or inventory checks. Return the cart here along with errors for the consumer to take more action // or inventory checks. Return the cart here along with errors for the consumer to take more action
// and fix them // and fix them
@@ -58,14 +62,14 @@ export const POST = async (
}) })
} }
const order = await refetchOrder( const { data } = await query.graph({
result.id, entity: "order",
req.scope, fields: req.remoteQueryConfig.fields,
req.remoteQueryConfig.fields filters: { id: result.id },
) })
res.status(200).json({ res.status(200).json({
type: "order", type: "order",
order, order: data[0],
}) })
} }
@@ -1,8 +1,16 @@
import {
onPaymentProcessedWorkflow,
processPaymentWorkflow,
} from "@medusajs/core-flows"
import { import {
IPaymentModuleService, IPaymentModuleService,
ProviderWebhookPayload, ProviderWebhookPayload,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { Modules, PaymentWebhookEvents } from "@medusajs/framework/utils" import {
Modules,
PaymentActions,
PaymentWebhookEvents,
} from "@medusajs/framework/utils"
import { SubscriberArgs, SubscriberConfig } from "../types/subscribers" import { SubscriberArgs, SubscriberConfig } from "../types/subscribers"
type SerializedBuffer = { type SerializedBuffer = {
@@ -27,7 +35,25 @@ export default async function paymentWebhookhandler({
(input.payload.rawData as unknown as SerializedBuffer).data (input.payload.rawData as unknown as SerializedBuffer).data
) )
} }
await paymentService.processEvent(input)
const processedEvent = await paymentService.getWebhookActionAndData(input)
if (processedEvent?.action === PaymentActions.NOT_SUPPORTED) {
return
}
if (!processedEvent.data) {
return
}
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 = { export const config: SubscriberConfig = {
@@ -27,6 +27,7 @@ import {
UpdatePaymentDTO, UpdatePaymentDTO,
UpdatePaymentSessionDTO, UpdatePaymentSessionDTO,
UpsertPaymentCollectionDTO, UpsertPaymentCollectionDTO,
WebhookActionResult,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
BigNumber, BigNumber,
@@ -37,7 +38,6 @@ import {
MedusaContext, MedusaContext,
MedusaError, MedusaError,
ModulesSdkUtils, ModulesSdkUtils,
PaymentActions,
PaymentCollectionStatus, PaymentCollectionStatus,
PaymentSessionStatus, PaymentSessionStatus,
promiseAll, promiseAll,
@@ -425,20 +425,19 @@ export default class PaymentModuleService
"amount", "amount",
"raw_amount", "raw_amount",
"currency_code", "currency_code",
"authorized_at",
"payment_collection_id", "payment_collection_id",
], ],
relations: ["payment", "payment_collection"],
}, },
sharedContext sharedContext
) )
// this method needs to be idempotent // this method needs to be idempotent
if (session.authorized_at) { if (session.payment && session.authorized_at) {
const payment = await this.paymentService_.retrieve( return await this.baseRepository_.serialize(session.payment, {
{ session_id: session.id }, populate: true,
{ relations: ["payment_collection"] }, })
sharedContext
)
return await this.baseRepository_.serialize(payment, { populate: true })
} }
let { data, status } = await this.paymentProviderService_.authorizePayment( let { data, status } = await this.paymentProviderService_.authorizePayment(
@@ -849,45 +848,16 @@ export default class PaymentModuleService
} }
@InjectManager() @InjectManager()
async processEvent( async getWebhookActionAndData(
eventData: ProviderWebhookPayload, eventData: ProviderWebhookPayload,
@MedusaContext() sharedContext?: Context @MedusaContext() sharedContext?: Context
): Promise<void> { ): Promise<WebhookActionResult> {
const providerId = `pp_${eventData.provider}` const providerId = `pp_${eventData.provider}`
const event = await this.paymentProviderService_.getWebhookActionAndData( return await this.paymentProviderService_.getWebhookActionAndData(
providerId, providerId,
eventData.payload eventData.payload
) )
if (event.action === PaymentActions.NOT_SUPPORTED) {
return
}
switch (event.action) {
case PaymentActions.SUCCESSFUL: {
const [payment] = await this.listPayments(
{
payment_session_id: event.data.session_id,
},
{},
sharedContext
)
if (payment && !payment.captured_at) {
await this.capturePayment(
{ payment_id: payment.id, amount: event.data.amount },
sharedContext
)
}
break
}
case PaymentActions.AUTHORIZED:
await this.authorizePaymentSession(
event.data.session_id,
{},
sharedContext
)
}
} }
@InjectManager() @InjectManager()
+397 -296
View File
File diff suppressed because it is too large Load Diff