Skip to content

Commit

Permalink
feat: Add support for managing account holder in payment module
Browse files Browse the repository at this point in the history
  • Loading branch information
sradevski committed Jan 17, 2025
1 parent 7be4735 commit 35b9fd7
Show file tree
Hide file tree
Showing 17 changed files with 606 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2543,7 +2543,6 @@ medusaIntegrationTestRunner({
await paymentModule.createPaymentCollections({
amount: 5001,
currency_code: "dkk",
region_id: defaultRegion.id,
})

const paymentSession = await paymentModule.createPaymentSession(
Expand Down Expand Up @@ -2615,7 +2614,6 @@ medusaIntegrationTestRunner({
await paymentModule.createPaymentCollections({
amount: 5000,
currency_code: "dkk",
region_id: defaultRegion.id,
})

const paymentSession = await paymentModule.createPaymentSession(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1330,7 +1330,6 @@ medusaIntegrationTestRunner({
).data.shipping_option

paymentCollection = await paymentService.createPaymentCollections({
region_id: region.id,
amount: 1000,
currency_code: "usd",
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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({
Expand All @@ -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"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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, { input: data, accountHolder })
},
async (createdAccountHolder, { container }) => {
if (!createdAccountHolder) {
return
}

const service = container.resolve<IPaymentModuleService>(Modules.PAYMENT)
const input = {
provider_id: createdAccountHolder.input.provider_id,
context: {
...createdAccountHolder.input.context,
customer: {
...createdAccountHolder.input.context?.customer,
metadata: {
...createdAccountHolder.input.context?.customer?.metadata,
...createdAccountHolder.accountHolder,
},
},
},
}

await service.deleteAccountHolder(input)
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface CreatePaymentSessionStepInput {
amount: BigNumberInput
/**
* The currency code of the payment session.
*
*
* @example
* usd
*/
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import {
AddressDTO,
CustomerDTO,
PaymentCustomerDTO,
PaymentProviderContext,
PaymentSessionDTO,
} from "@medusajs/framework/types"
Expand All @@ -8,10 +11,16 @@ import {
createWorkflow,
parallelize,
transform,
when,
} from "@medusajs/framework/workflows-sdk"
import { useRemoteQueryStep } from "../../common"
import { createPaymentSessionStep } from "../steps"
import {
createPaymentSessionStep,
createPaymentAccountHolderStep,
} from "../steps"
import { deletePaymentSessionsWorkflow } from "./delete-payment-sessions"
import { updateCustomersStep } from "../../customer"
import { isPresent } from "@medusajs/framework/utils"

/**
* The data to create payment sessions.
Expand All @@ -26,6 +35,10 @@ 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).
Expand All @@ -41,10 +54,10 @@ 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({
Expand All @@ -53,9 +66,9 @@ export const createPaymentSessionsWorkflowId = "create-payment-sessions"
* provider_id: "pp_system"
* }
* })
*
*
* @summary
*
*
* Create payment sessions.
*/
export const createPaymentSessionsWorkflow = createWorkflow(
Expand All @@ -68,16 +81,93 @@ 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 { customer, accountHolder } = when(
"customer-id-exists",
{ input },
(data) => {
return !!data.input.customer_id
}
).then(() => {
const customer: CustomerDTO = useRemoteQueryStep({
entry_point: "customer",
fields: [
"id",
"email",
"company_name",
"first_name",
"last_name",
"phone",
"addresses.*",
"metadata",
],
variables: { id: input.customer_id },
list: false,
}).config({ name: "get-customer" })

const accountHolderInput = transform({ input, customer }, (data) => {
return {
provider_id: data.input.provider_id,
context: {
...data.input.context,
customer: data.customer,
},
}
})

const accountHolder = createPaymentAccountHolderStep(accountHolderInput)
return { customer, accountHolder }
})

const updatedCustomer = when(
"account-holder-exists",
{ accountHolder },
(data) => {
return isPresent(data.accountHolder)
}
).then(() => {
updateCustomersStep({
selector: {
id: input.context?.customer?.id,
},
update: {
metadata: accountHolder,
},
})

const updatedCustomer = transform({ customer, accountHolder }, (data) => {
return {
...data.customer,
metadata: {
...data.customer.metadata,
...data.accountHolder,
},
}
})

return updatedCustomer
})

const updatedContext = transform({ input, updatedCustomer }, (data) => {
return {
...data.input.context,
email: data.input.context?.email ?? data.updatedCustomer?.email,
billing_address: (data.input.context?.billing_address ??
data.updatedCustomer?.addresses?.find((a) => a.is_default_billing) ??
data.updatedCustomer?.addresses?.[0]) as Partial<AddressDTO>,
customer: data.updatedCustomer as PaymentCustomerDTO,
}
})

const paymentSessionInput = transform(
{ paymentCollection, input },
{ paymentCollection, updatedContext, 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.updatedContext ?? data.input.context,
amount: data.paymentCollection.amount,
currency_code: data.paymentCollection.currency_code,
}
Expand Down
27 changes: 27 additions & 0 deletions packages/core/types/src/payment/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,33 @@ export interface CreatePaymentProviderDTO {
is_enabled?: boolean
}

/**
* The payment session to be created.
*/
export interface CreateAccountHolderDTO {
/**
* The provider's ID.
*/
provider_id: string

/**
* Necessary context data for the associated payment provider.
*/
context: PaymentProviderContext
}

export interface DeleteAccountHolderDTO {
/**
* The provider's ID.
*/
provider_id: string

/**
* Necessary context data for the associated payment provider.
*/
context: PaymentProviderContext
}

/**
* The details of the webhook event payload.
*/
Expand Down
21 changes: 21 additions & 0 deletions packages/core/types/src/payment/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ export type PaymentProviderSessionResponse = {
data: Record<string, unknown>
}

/**
* @interface
*
* The response of operations on a payment account holder.
*/
export type PaymentAccountHolderResponse = {
/**
* The data field can be stored in eg. `customer.metadata`, which should then be passed to other methods as part of the context.
* The field can contain external customer IDs, and other information that is necessary for the account holder when performing a payment.
*/
data: Record<string, unknown>
}

/**
* @interface
*
Expand Down Expand Up @@ -254,6 +267,14 @@ export interface IPaymentProvider {
paymentSessionData: Record<string, unknown>
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]>

createAccountHolder(
context: PaymentProviderContext
): Promise<PaymentProviderError | PaymentAccountHolderResponse>

deleteAccountHolder(
context: PaymentProviderContext
): Promise<PaymentProviderError | PaymentAccountHolderResponse>

listPaymentMethods(
context: PaymentProviderContext
): Promise<PaymentMethodResponse[]>
Expand Down
Loading

0 comments on commit 35b9fd7

Please sign in to comment.