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

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

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
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
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
Loading