feat: Add support for managing account holder in payment module (#11015)

This commit is contained in:
Stevche Radevski
2025-01-28 08:55:15 +01:00
committed by GitHub
parent a37a9c8023
commit 59cbc0ec77
29 changed files with 1328 additions and 803 deletions
@@ -57,13 +57,13 @@ export const THREE_DAYS = 60 * 60 * 24 * 3
export const completeCartWorkflowId = "complete-cart"
/**
* This workflow completes a cart and places an order for the customer. It's executed by the
* This workflow completes a cart and places an order for the customer. It's executed by the
* [Complete Cart Store API Route](https://docs.medusajs.com/api/store#carts_postcartsidcomplete).
*
*
* You can use this workflow within your own customizations or custom workflows, allowing you to wrap custom logic around completing a cart.
* For example, in the [Subscriptions recipe](https://docs.medusajs.com/resources/recipes/subscriptions/examples/standard#create-workflow),
* For example, in the [Subscriptions recipe](https://docs.medusajs.com/resources/recipes/subscriptions/examples/standard#create-workflow),
* this workflow is used within another workflow that creates a subscription order.
*
*
* @example
* const { result } = await completeCartWorkflow(container)
* .run({
@@ -71,11 +71,11 @@ export const completeCartWorkflowId = "complete-cart"
* id: "cart_123"
* }
* })
*
*
* @summary
*
*
* Complete a cart and place an order.
*
*
* @property hooks.validate - This hook is executed before all operations. You can consume this hook to perform any custom validation. If validation fails, you can throw an error to stop the workflow execution.
*/
export const completeCartWorkflow = createWorkflow(
@@ -118,7 +118,6 @@ export const completeCartWorkflow = createWorkflow(
// 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) => {
@@ -26,14 +26,14 @@ export type ThrowUnlessPaymentCollectionNotePaidInput = {
/**
* This step validates that the payment collection is not paid. If not valid,
* the step will throw an error.
*
*
* :::note
*
*
* You can retrieve a payment collection's details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query),
* or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep).
*
*
* :::
*
*
* @example
* const data = throwUnlessPaymentCollectionNotPaid({
* paymentCollection: {
@@ -77,10 +77,10 @@ export const markPaymentCollectionAsPaidId = "mark-payment-collection-as-paid"
/**
* This workflow marks a payment collection for an order as paid. It's used by the
* [Mark Payment Collection as Paid Admin API Route](https://docs.medusajs.com/api/admin#payment-collections_postpaymentcollectionsidmarkaspaid).
*
*
* You can use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around
* marking a payment collection for an order as paid.
*
*
* @example
* const { result } = await markPaymentCollectionAsPaid(container)
* .run({
@@ -89,16 +89,14 @@ export const markPaymentCollectionAsPaidId = "mark-payment-collection-as-paid"
* payment_collection_id: "paycol_123",
* }
* })
*
*
* @summary
*
*
* Mark a payment collection for an order as paid.
*/
export const markPaymentCollectionAsPaid = createWorkflow(
markPaymentCollectionAsPaidId,
(
input: WorkflowData<MarkPaymentCollectionAsPaidInput>
) => {
(input: WorkflowData<MarkPaymentCollectionAsPaidInput>) => {
const paymentCollection = useRemoteQueryStep({
entry_point: "payment_collection",
fields: ["id", "status", "amount"],
@@ -120,7 +118,6 @@ export const markPaymentCollectionAsPaid = createWorkflow(
const payment = authorizePaymentSessionStep({
id: paymentSession.id,
context: { order_id: input.order_id },
})
capturePaymentWorkflow.runAsStep({
@@ -0,0 +1,29 @@
import {
IPaymentModuleService,
CreateAccountHolderDTO,
} from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
export const createPaymentAccountHolderStepId = "create-payment-account-holder"
/**
* This step creates the account holder in the payment provider.
*/
export const createPaymentAccountHolderStep = createStep(
createPaymentAccountHolderStepId,
async (data: CreateAccountHolderDTO, { container }) => {
const service = container.resolve<IPaymentModuleService>(Modules.PAYMENT)
const accountHolder = await service.createAccountHolder(data)
return new StepResponse(accountHolder, accountHolder)
},
async (createdAccountHolder, { container }) => {
if (!createdAccountHolder) {
return
}
const service = container.resolve<IPaymentModuleService>(Modules.PAYMENT)
await service.deleteAccountHolder(createdAccountHolder.id)
}
)
@@ -24,7 +24,7 @@ export interface CreatePaymentSessionStepInput {
amount: BigNumberInput
/**
* The currency code of the payment session.
*
*
* @example
* usd
*/
@@ -42,7 +42,7 @@ export interface CreatePaymentSessionStepInput {
export const createPaymentSessionStepId = "create-payment-session"
/**
* This step creates a payment session.
* This step creates a payment session.
*/
export const createPaymentSessionStep = createStep(
createPaymentSessionStepId,
@@ -5,3 +5,4 @@ export * from "./delete-refund-reasons"
export * from "./update-payment-collection"
export * from "./update-refund-reasons"
export * from "./validate-deleted-payment-sessions"
export * from "./create-payment-account-holder"
@@ -1,5 +1,6 @@
import {
PaymentProviderContext,
AccountHolderDTO,
CustomerDTO,
PaymentSessionDTO,
} from "@medusajs/framework/types"
import {
@@ -8,10 +9,15 @@ import {
createWorkflow,
parallelize,
transform,
when,
} from "@medusajs/framework/workflows-sdk"
import { useRemoteQueryStep } from "../../common"
import { createPaymentSessionStep } from "../steps"
import { createRemoteLinkStep, useRemoteQueryStep } from "../../common"
import {
createPaymentSessionStep,
createPaymentAccountHolderStep,
} from "../steps"
import { deletePaymentSessionsWorkflow } from "./delete-payment-sessions"
import { isPresent, Modules } from "@medusajs/framework/utils"
/**
* The data to create payment sessions.
@@ -26,25 +32,31 @@ export interface CreatePaymentSessionsWorkflowInput {
* This provider is used to later process the payment sessions and their payments.
*/
provider_id: string
/**
* The ID of the customer that the payment session should be associated with.
*/
customer_id?: string
/**
* Custom data relevant for the payment provider to process the payment session.
* Learn more in [this documentation](https://docs.medusajs.com/resources/commerce-modules/payment/payment-session#data-property).
*/
data?: Record<string, unknown>
/**
* Additional context that's useful for the payment provider to process the payment session.
* Currently all of the context is calculated within the workflow.
*/
context?: PaymentProviderContext
context?: Record<string, unknown>
}
export const createPaymentSessionsWorkflowId = "create-payment-sessions"
/**
* This workflow creates payment sessions. It's used by the
* [Initialize Payment Session Store API Route](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions).
*
*
* You can use this workflow within your own customizations or custom workflows, allowing you
* to create payment sessions in your custom flows.
*
*
* @example
* const { result } = await createPaymentSessionsWorkflow(container)
* .run({
@@ -53,9 +65,9 @@ export const createPaymentSessionsWorkflowId = "create-payment-sessions"
* provider_id: "pp_system"
* }
* })
*
*
* @summary
*
*
* Create payment sessions.
*/
export const createPaymentSessionsWorkflow = createWorkflow(
@@ -68,16 +80,89 @@ export const createPaymentSessionsWorkflow = createWorkflow(
fields: ["id", "amount", "currency_code", "payment_sessions.*"],
variables: { id: input.payment_collection_id },
list: false,
}).config({ name: "get-payment-collection" })
const { paymentCustomer, accountHolder } = when(
"customer-id-exists",
{ input },
(data) => {
return !!data.input.customer_id
}
).then(() => {
const customer: CustomerDTO & { account_holder: AccountHolderDTO } =
useRemoteQueryStep({
entry_point: "customer",
fields: [
"id",
"email",
"company_name",
"first_name",
"last_name",
"phone",
"addresses.*",
"account_holder.*",
"metadata",
],
variables: { id: input.customer_id },
list: false,
}).config({ name: "get-customer" })
const paymentCustomer = transform({ customer }, (data) => {
return {
...data.customer,
billing_address:
data.customer.addresses?.find((a) => a.is_default_billing) ??
data.customer.addresses?.[0],
}
})
const accountHolderInput = {
provider_id: input.provider_id,
context: {
// The module is idempotent, so if there already is a linked account holder, the module will simply return it back.
account_holder: customer.account_holder,
customer: paymentCustomer,
},
}
const accountHolder = createPaymentAccountHolderStep(accountHolderInput)
return { paymentCustomer, accountHolder }
})
when(
"account-holder-created",
{ paymentCustomer, accountHolder },
(data) => {
return (
!isPresent(data.paymentCustomer?.account_holder) &&
isPresent(data.accountHolder)
)
}
).then(() => {
createRemoteLinkStep([
{
[Modules.CUSTOMER]: {
customer_id: paymentCustomer.id,
},
[Modules.PAYMENT]: {
account_holder_id: accountHolder.id,
},
},
])
})
const paymentSessionInput = transform(
{ paymentCollection, input },
{ paymentCollection, paymentCustomer, accountHolder, input },
(data) => {
return {
payment_collection_id: data.input.payment_collection_id,
provider_id: data.input.provider_id,
data: data.input.data,
context: data.input.context,
context: {
...data.input.context,
customer: data.paymentCustomer,
account_holder: data.accountHolder,
},
amount: data.paymentCollection.amount,
currency_code: data.paymentCollection.currency_code,
}
@@ -23,13 +23,13 @@ export type AuthorizePaymentSessionStepInput = {
* The context to authorize the payment session with.
* This context is passed to the payment provider associated with the payment session.
*/
context: Record<string, unknown>
context?: Record<string, unknown>
}
export const authorizePaymentSessionStepId = "authorize-payment-session-step"
/**
* This step authorizes a payment session.
*
*
* @example
* const data = authorizePaymentSessionStep({
* id: "payses_123",
@@ -31,12 +31,12 @@ export type CapturePaymentWorkflowInput = {
export const capturePaymentWorkflowId = "capture-payment-workflow"
/**
* This workflow captures a payment. It's used by the
* This workflow captures a payment. It's used by the
* [Capture Payment Admin API Route](https://docs.medusajs.com/api/admin#payments_postpaymentsidcapture).
*
*
* You can use this workflow within your own customizations or custom workflows, allowing you
* to capture a payment in your custom flows.
*
*
* @example
* const { result } = await capturePaymentWorkflow(container)
* .run({
@@ -44,9 +44,9 @@ export const capturePaymentWorkflowId = "capture-payment-workflow"
* payment_id: "pay_123"
* }
* })
*
*
* @summary
*
*
* Capture a payment.
*/
export const capturePaymentWorkflow = createWorkflow(