diff --git a/packages/core/core-flows/src/definition/cart/steps/index.ts b/packages/core/core-flows/src/definition/cart/steps/index.ts index 9b18ae517019a..896597ae90dea 100644 --- a/packages/core/core-flows/src/definition/cart/steps/index.ts +++ b/packages/core/core-flows/src/definition/cart/steps/index.ts @@ -5,6 +5,7 @@ export * from "./create-line-item-adjustments" export * from "./create-line-items" export * from "./create-order-from-cart" export * from "./create-shipping-method-adjustments" +export * from "./create-payment-collection" export * from "./find-one-or-any-region" export * from "./find-or-create-customer" export * from "./find-sales-channel" diff --git a/packages/core/core-flows/src/order/utils/aggregate-status.ts b/packages/core/core-flows/src/order/utils/aggregate-status.ts index 9d0733e0f3477..200fbdb45429b 100644 --- a/packages/core/core-flows/src/order/utils/aggregate-status.ts +++ b/packages/core/core-flows/src/order/utils/aggregate-status.ts @@ -1,7 +1,11 @@ -import { OrderDetailDTO } from "@medusajs/types" +import { FulfillmentDTO, PaymentCollectionDTO } from "@medusajs/types" import { MathBN } from "@medusajs/utils" -export const getLastPaymentStatus = (order: OrderDetailDTO) => { +export const getLastPaymentStatus = ({ + payment_collections, +}: { + payment_collections: PaymentCollectionDTO[] +}) => { const PaymentStatus = { NOT_PAID: "not_paid", AWAITING: "awaiting", @@ -20,7 +24,7 @@ export const getLastPaymentStatus = (order: OrderDetailDTO) => { paymentStatus[PaymentStatus[status]] = 0 } - for (const paymentCollection of order.payment_collections) { + for (const paymentCollection of payment_collections) { if (MathBN.gt(paymentCollection.captured_amount ?? 0, 0)) { paymentStatus[PaymentStatus.CAPTURED] += MathBN.eq( paymentCollection.captured_amount as number, @@ -42,7 +46,7 @@ export const getLastPaymentStatus = (order: OrderDetailDTO) => { paymentStatus[paymentCollection.status] += 1 } - const totalPayments = order.payment_collections.length + const totalPayments = payment_collections.length const totalPaymentExceptCanceled = totalPayments - paymentStatus[PaymentStatus.CANCELED] @@ -90,7 +94,11 @@ export const getLastPaymentStatus = (order: OrderDetailDTO) => { return PaymentStatus.NOT_PAID } -export const getLastFulfillmentStatus = (order: OrderDetailDTO) => { +export const getLastFulfillmentStatus = ({ + fulfillments, +}: { + fulfillments: FulfillmentDTO[] +}) => { const FulfillmentStatus = { NOT_FULFILLED: "not_fulfilled", PARTIALLY_FULFILLED: "partially_fulfilled", @@ -113,7 +121,7 @@ export const getLastFulfillmentStatus = (order: OrderDetailDTO) => { delivered_at: FulfillmentStatus.DELIVERED, canceled_at: FulfillmentStatus.CANCELED, } - for (const fulfillmentCollection of order.fulfillments) { + for (const fulfillmentCollection of fulfillments) { for (const key in statusMap) { if (fulfillmentCollection[key]) { fulfillmentStatus[statusMap[key]] += 1 @@ -122,7 +130,7 @@ export const getLastFulfillmentStatus = (order: OrderDetailDTO) => { } } - const totalFulfillments = order.fulfillments.length + const totalFulfillments = fulfillments.length const totalFulfillmentsExceptCanceled = totalFulfillments - fulfillmentStatus[FulfillmentStatus.CANCELED] diff --git a/packages/core/core-flows/src/payment-collection/steps/create-payment-session.ts b/packages/core/core-flows/src/payment-collection/steps/create-payment-session.ts index 00f2764584dc0..152c7630f41c9 100644 --- a/packages/core/core-flows/src/payment-collection/steps/create-payment-session.ts +++ b/packages/core/core-flows/src/payment-collection/steps/create-payment-session.ts @@ -11,6 +11,7 @@ interface StepInput { provider_id: string amount: BigNumberInput currency_code: string + provider_token?: string context?: PaymentProviderContext data?: Record } @@ -27,6 +28,7 @@ export const createPaymentSessionStep = createStep( input.payment_collection_id, { provider_id: input.provider_id, + provider_token: input.provider_token, currency_code: input.currency_code, amount: input.amount, data: input.data ?? {}, diff --git a/packages/core/core-flows/src/payment-collection/workflows/create-payment-session.ts b/packages/core/core-flows/src/payment-collection/workflows/create-payment-session.ts index f2176f40d87a8..2ee8b76709af4 100644 --- a/packages/core/core-flows/src/payment-collection/workflows/create-payment-session.ts +++ b/packages/core/core-flows/src/payment-collection/workflows/create-payment-session.ts @@ -1,4 +1,9 @@ -import { PaymentProviderContext, PaymentSessionDTO } from "@medusajs/types" +import { + BigNumberInput, + PaymentProviderContext, + PaymentSessionDTO, +} from "@medusajs/types" +import { MathBN, PaymentSessionStatus } from "@medusajs/utils" import { WorkflowData, createWorkflow, @@ -12,8 +17,10 @@ import { deletePaymentSessionsWorkflow } from "./delete-payment-sessions" interface WorkflowInput { payment_collection_id: string provider_id: string + provider_token?: string data?: Record context?: PaymentProviderContext + amount?: BigNumberInput } export const createPaymentSessionsWorkflowId = "create-payment-sessions" @@ -22,7 +29,13 @@ export const createPaymentSessionsWorkflow = createWorkflow( (input: WorkflowData): WorkflowData => { const paymentCollection = useRemoteQueryStep({ entry_point: "payment_collection", - fields: ["id", "amount", "currency_code", "payment_sessions.*"], + fields: [ + "id", + "raw_amount", + "raw_authorized_amount", + "currency_code", + "payment_sessions.*", + ], variables: { id: input.payment_collection_id }, list: false, }) @@ -30,12 +43,17 @@ export const createPaymentSessionsWorkflow = createWorkflow( const paymentSessionInput = transform( { paymentCollection, input }, (data) => { + const balance = MathBN.sub( + data.paymentCollection.raw_amount, + data.paymentCollection.raw_authorized_amount || 0 + ) return { payment_collection_id: data.input.payment_collection_id, provider_id: data.input.provider_id, + provider_token: data.input.provider_token, data: data.input.data, context: data.input.context, - amount: data.paymentCollection.amount, + amount: MathBN.min(data.input.amount || balance, balance), currency_code: data.paymentCollection.currency_code, } } @@ -46,15 +64,14 @@ export const createPaymentSessionsWorkflow = createWorkflow( (data) => { return { ids: - data.paymentCollection?.payment_sessions?.map((ps) => ps.id) || [], + data.paymentCollection?.payment_sessions + ?.filter((ps) => ps.status !== PaymentSessionStatus.AUTHORIZED) + ?.map((ps) => ps.id) || [], } } ) - // Note: We are deleting an existing active session before creating a new one - // for a payment collection as we don't support split payments at the moment. - // When we are ready to accept split payments, this along with other workflows - // need to be handled correctly + // Note: We are deleting all existing non-authorized session before creating a new one const [created] = parallelize( createPaymentSessionStep(paymentSessionInput), deletePaymentSessionsWorkflow.runAsStep({ diff --git a/packages/core/js-sdk/src/store/index.ts b/packages/core/js-sdk/src/store/index.ts index 78ec553880892..cc460fa06a059 100644 --- a/packages/core/js-sdk/src/store/index.ts +++ b/packages/core/js-sdk/src/store/index.ts @@ -313,6 +313,22 @@ export class Store { query, }) }, + + addPaymentSession: async ( + paymentCollectionId: string, + body: Record, + query?: SelectParams, + headers?: ClientHeaders + ) => { + return this.client.fetch<{ + payment_collection: HttpTypes.StorePaymentCollection + }>(`/store/payment-collections/${paymentCollectionId}/payment-sessions`, { + method: "POST", + headers, + body, + query, + }) + }, } public order = { diff --git a/packages/core/types/src/payment/mutations.ts b/packages/core/types/src/payment/mutations.ts index bfc0e7a1e257c..dc1a1ec750477 100644 --- a/packages/core/types/src/payment/mutations.ts +++ b/packages/core/types/src/payment/mutations.ts @@ -228,6 +228,11 @@ export interface CreatePaymentSessionDTO { */ provider_id: string + /** + * The provider's payment method token + */ + provider_token?: string + /** * The ISO 3 character currency code of the payment session. */ diff --git a/packages/core/types/src/payment/provider.ts b/packages/core/types/src/payment/provider.ts index 9d17846cb28ec..81447ea9e6cc2 100644 --- a/packages/core/types/src/payment/provider.ts +++ b/packages/core/types/src/payment/provider.ts @@ -40,10 +40,20 @@ export type PaymentProviderContext = { email?: string /** - * The ID of payment session the provider payment is associated with. + * The associated payment session's ID. */ session_id?: string + /** + * The associated cart's ID. + */ + cart_id?: string + + /** + * The associated order's ID. + */ + order_id?: string + /** * The customer associated with this payment. */ @@ -76,6 +86,11 @@ export type CreatePaymentProviderSession = { * The ISO 3 character currency code. */ currency_code: string + + /* + * The payment method token + */ + token?: string } /** @@ -184,6 +199,16 @@ export type WebhookActionData = { */ session_id: string + /** + * The associated cart's ID. + */ + cart_id: string + + /** + * The associated order's ID. + */ + order_id: string + /** * The amount to be captured or authorized (based on the action's type.) */ diff --git a/packages/medusa/src/api/store/orders/[id]/route.ts b/packages/medusa/src/api/store/orders/[id]/route.ts index 217e87cbea7f9..a4eab69080ad6 100644 --- a/packages/medusa/src/api/store/orders/[id]/route.ts +++ b/packages/medusa/src/api/store/orders/[id]/route.ts @@ -1,5 +1,5 @@ +import { getOrderDetailWorkflow } from "@medusajs/core-flows" import { MedusaRequest, MedusaResponse } from "../../../../types/routing" -import { refetchOrder } from "../helpers" import { StoreGetOrdersParamsType } from "../validators" // TODO: Do we want to apply some sort of authentication here? My suggestion is that we do @@ -7,11 +7,12 @@ export const GET = async ( req: MedusaRequest, res: MedusaResponse ) => { - const order = await refetchOrder( - req.params.id, - req.scope, - req.remoteQueryConfig.fields - ) + const { result } = await getOrderDetailWorkflow(req.scope).run({ + input: { + fields: req.remoteQueryConfig.fields, + order_id: req.params.id, + }, + }) - res.json({ order }) + res.json({ order: result }) } diff --git a/packages/medusa/src/api/store/payment-collections/[id]/payment-sessions/route.ts b/packages/medusa/src/api/store/payment-collections/[id]/payment-sessions/route.ts index e11e4552679b9..ffc56ff0db179 100644 --- a/packages/medusa/src/api/store/payment-collections/[id]/payment-sessions/route.ts +++ b/packages/medusa/src/api/store/payment-collections/[id]/payment-sessions/route.ts @@ -11,7 +11,7 @@ export const POST = async ( res: MedusaResponse ) => { const collectionId = req.params.id - const { context = {}, data, provider_id } = req.body + const { context = {}, data, provider_id, amount } = req.body // If the customer is logged in, we auto-assign them to the payment collection if (req.auth_context?.actor_id) { @@ -21,7 +21,8 @@ export const POST = async ( } const workflowInput = { payment_collection_id: collectionId, - provider_id: provider_id, + provider_id, + amount, data, context, } diff --git a/packages/medusa/src/api/store/payment-collections/validators.ts b/packages/medusa/src/api/store/payment-collections/validators.ts index d7b2514bc1814..559c54c09ad46 100644 --- a/packages/medusa/src/api/store/payment-collections/validators.ts +++ b/packages/medusa/src/api/store/payment-collections/validators.ts @@ -14,6 +14,7 @@ export const StoreCreatePaymentSession = z provider_id: z.string(), context: z.record(z.unknown()).optional(), data: z.record(z.unknown()).optional(), + amount: z.number().optional(), }) .strict() diff --git a/packages/modules/payment/src/services/payment-module.ts b/packages/modules/payment/src/services/payment-module.ts index eee96f5fa1ed8..5eaf4c903e146 100644 --- a/packages/modules/payment/src/services/payment-module.ts +++ b/packages/modules/payment/src/services/payment-module.ts @@ -295,6 +295,7 @@ export default class PaymentModuleService @MedusaContext() sharedContext?: Context ): Promise { let paymentSession: PaymentSession | undefined + let providerSessionData: Record | undefined try { paymentSession = await this.createPaymentSession_( @@ -302,31 +303,44 @@ export default class PaymentModuleService input, sharedContext ) - - const providerSessionSession = - await this.paymentProviderService_.createSession(input.provider_id, { + providerSessionData = await this.paymentProviderService_.createSession( + input.provider_id, + { context: { ...input.context, session_id: paymentSession.id }, amount: input.amount, currency_code: input.currency_code, - }) - + token: input.provider_token, + } + ) paymentSession = ( await this.paymentSessionService_.update( { id: paymentSession.id, - data: { ...input.data, ...providerSessionSession }, + data: { ...input.data, ...providerSessionData }, }, sharedContext ) )[0] + if (input.provider_token) { + await this.authorizePaymentSession( + paymentSession!.id, + {}, + sharedContext + ) + paymentSession = await this.paymentSessionService_.retrieve( + paymentSession!.id, + {}, + sharedContext + ) + } } catch (error) { - if (paymentSession) { - // In case the session is created, but fails to be updated in Medusa, - // we catch the error and delete the session and rethrow. + if (providerSessionData) { await this.paymentProviderService_.deleteSession({ provider_id: input.provider_id, - data: input.data, + data: providerSessionData, }) + } + if (paymentSession) { await this.paymentSessionService_.delete( paymentSession.id, sharedContext @@ -862,6 +876,15 @@ export default class PaymentModuleService {}, sharedContext ) + + if (event.data.order_id && !event.data.cart_id) { + await this.authorizePaymentSession( + event.data.session_id, + {}, + sharedContext + ) + } + if (payment && !payment.captured_at) { await this.capturePayment( { payment_id: payment.id, amount: event.data.amount }, diff --git a/packages/modules/providers/payment-stripe/src/core/stripe-base.ts b/packages/modules/providers/payment-stripe/src/core/stripe-base.ts index a8d0dc5576864..927984bdeb1bf 100644 --- a/packages/modules/providers/payment-stripe/src/core/stripe-base.ts +++ b/packages/modules/providers/payment-stripe/src/core/stripe-base.ts @@ -110,8 +110,9 @@ abstract class StripeBase extends AbstractPaymentProvider { input: CreatePaymentProviderSession ): Promise { const intentRequestData = this.getPaymentIntentOptions() - const { email, extra, session_id, customer } = input.context - const { currency_code, amount } = input + const { email, extra, session_id, cart_id, order_id, customer } = + input.context + const { currency_code, amount, token } = input const description = (extra?.payment_description ?? this.options_?.paymentDescription) as string @@ -120,13 +121,23 @@ abstract class StripeBase extends AbstractPaymentProvider { description, amount: getSmallestUnit(amount, currency_code), currency: currency_code, - metadata: { session_id: session_id! }, + payment_method: token, + confirm: !!token, + metadata: { + session_id: session_id!, + cart_id: cart_id as string | null, + order_id: order_id as string | null, + }, capture_method: this.options_.capture ? "automatic" : "manual", ...intentRequestData, } - if (this.options_?.automaticPaymentMethods) { - intentRequest.automatic_payment_methods = { enabled: true } + const automaticPaymentMethods = this.options_?.automaticPaymentMethods + if (automaticPaymentMethods) { + intentRequest.automatic_payment_methods = + typeof automaticPaymentMethods === "boolean" + ? { enabled: true } + : automaticPaymentMethods } if (customer?.metadata?.stripe_id) { @@ -330,6 +341,8 @@ abstract class StripeBase extends AbstractPaymentProvider { action: PaymentActions.AUTHORIZED, data: { session_id: intent.metadata.session_id, + cart_id: intent.metadata.cart_id, + order_id: intent.metadata.order_id, amount: getAmountFromSmallestUnit( intent.amount_capturable, currency @@ -341,6 +354,8 @@ abstract class StripeBase extends AbstractPaymentProvider { action: PaymentActions.SUCCESSFUL, data: { session_id: intent.metadata.session_id, + cart_id: intent.metadata.cart_id, + order_id: intent.metadata.order_id, amount: getAmountFromSmallestUnit(intent.amount_received, currency), }, } @@ -349,6 +364,8 @@ abstract class StripeBase extends AbstractPaymentProvider { action: PaymentActions.FAILED, data: { session_id: intent.metadata.session_id, + cart_id: intent.metadata.cart_id, + order_id: intent.metadata.order_id, amount: getAmountFromSmallestUnit(intent.amount, currency), }, } diff --git a/packages/modules/providers/payment-stripe/src/types/index.ts b/packages/modules/providers/payment-stripe/src/types/index.ts index abeb3cb7aaaf1..8211e2b91e526 100644 --- a/packages/modules/providers/payment-stripe/src/types/index.ts +++ b/packages/modules/providers/payment-stripe/src/types/index.ts @@ -1,3 +1,5 @@ +import Stripe from "stripe" + export interface StripeOptions { /** * The API key for the Stripe account @@ -12,9 +14,11 @@ export interface StripeOptions { */ capture?: boolean /** - * set `automatic_payment_methods` on the intent request to `{ enabled: true }` + * set `automatic_payment_methods` on the intent request */ - automaticPaymentMethods?: boolean + automaticPaymentMethods?: + | boolean + | Stripe.PaymentIntentCreateParams.AutomaticPaymentMethods /** * Set a default description on the intent if the context does not provide one */