feat: Add support to update account holder (#11499)

This commit is contained in:
Stevche Radevski
2025-02-18 11:04:25 +01:00
committed by GitHub
parent 32ad13813b
commit 99a6ecc12d
6 changed files with 242 additions and 1 deletions

View File

@@ -1,6 +1,10 @@
import { BigNumberInput } from "../totals"
import { PaymentCollectionStatus } from "./common"
import { PaymentCustomerDTO, PaymentProviderContext } from "./provider"
import {
PaymentAccountHolderDTO,
PaymentCustomerDTO,
PaymentProviderContext,
} from "./provider"
/**
* The payment collection to be created.
@@ -275,6 +279,33 @@ export interface CreateAccountHolderDTO {
}
}
export interface UpdateAccountHolderDTO {
/**
* The ID of the account holder.
*/
id: string
/**
* The provider's ID.
*/
provider_id: string
/**
* Necessary context data for the associated payment provider.
*/
context: PaymentProviderContext & {
/**
* The account holder information from Medusa.
*/
account_holder: PaymentAccountHolderDTO
}
/**
* Holds custom data in key-value pairs.
*/
metadata?: Record<string, unknown>
}
/**
* The details of the webhook event payload.
*/

View File

@@ -193,6 +193,18 @@ export type CreateAccountHolderInput = PaymentProviderInput & {
}
}
export type UpdateAccountHolderInput = PaymentProviderInput & {
/**
* The context of creating the account holder.
*/
context: PaymentProviderContext & {
/**
* The account holder's details.
*/
account_holder: PaymentAccountHolderDTO
}
}
/**
* @interface
*
@@ -323,6 +335,8 @@ export type CreateAccountHolderOutput = PaymentProviderOutput & {
id: string
}
export type UpdateAccountHolderOutput = PaymentProviderOutput
export type DeleteAccountHolderOutput = PaymentProviderOutput
export type ListPaymentMethodsOutput = (PaymentProviderOutput & {
@@ -466,6 +480,50 @@ export interface IPaymentProvider {
data: CreateAccountHolderInput
): Promise<CreateAccountHolderOutput>
/**
* This method is used when updating an account holder in Medusa, allowing you to update
* the equivalent account in the third-party service.
*
* The returned data will be stored in the account holder created in Medusa. For example,
* the returned `id` property will be stored in the account holder's `external_id` property.
*
* @param data - Input data including the details of the account holder to update.
* @returns The result of updating the account holder. If an error occurs, throw it.
*
* @version 2.6.0
*
* @example
* import { MedusaError } from "@medusajs/framework/utils"
*
* class MyPaymentProviderService extends AbstractPaymentProvider<
* Options
* > {
* async updateAccountHolder({ context, data }: UpdateAccountHolderInput) {
* const { account_holder, customer } = context
*
* if (!account_holder?.data?.id) {
* throw new MedusaError(
* MedusaError.Types.INVALID_DATA,
* "Missing account holder ID."
* )
* }
*
* // assuming you have a client that updates the account holder
* const providerAccountHolder = await this.client.updateAccountHolder({
* id: account_holder.data.id,
* ...data
* })
*
* return {
* id: providerAccountHolder.id,
* data: providerAccountHolder as unknown as Record<string, unknown>
* }
* }
*/
updateAccountHolder?(
data: UpdateAccountHolderInput
): Promise<UpdateAccountHolderOutput>
/**
* This method is used when an account holder is deleted in Medusa, allowing you
* to also delete the equivalent account holder in the third-party service.

View File

@@ -35,6 +35,7 @@ import {
CreateAccountHolderDTO,
UpsertPaymentCollectionDTO,
CreatePaymentMethodDTO,
UpdateAccountHolderDTO,
} from "./mutations"
import { WebhookActionResult } from "./provider"
@@ -788,6 +789,37 @@ export interface IPaymentModuleService extends IModuleService {
sharedContext?: Context
): Promise<AccountHolderDTO>
/**
* This method updates(if supported by provider) the account holder in the payment provider.
*
* @param {UpdateAccountHolderDTO} data - The details of the account holder.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<Record<string, unknown>>} The account holder's details in the payment provider, typically just the ID.
*
* @example
* const accountHolder =
* await paymentModuleService.updateAccountHolder(
* {
* provider_id: "stripe",
* context: {
* account_holder: {
* data: {
* id: "acc_holder_123",
* },
* },
* customer: {
* id: "cus_123",
* company_name: "new_name",
* },
* },
* }
* )
*/
updateAccountHolder(
input: UpdateAccountHolderDTO,
sharedContext?: Context
): Promise<AccountHolderDTO>
/**
* This method deletes the account holder in the payment provider.
*

View File

@@ -36,6 +36,8 @@ import {
WebhookActionResult,
CreateAccountHolderOutput,
InitiatePaymentOutput,
UpdateAccountHolderDTO,
UpdateAccountHolderOutput,
} from "@medusajs/framework/types"
import {
BigNumber,
@@ -1027,6 +1029,45 @@ export default class PaymentModuleService
return await this.baseRepository_.serialize(accountHolder)
}
@InjectManager()
async updateAccountHolder(
input: UpdateAccountHolderDTO,
@MedusaContext() sharedContext?: Context
): Promise<AccountHolderDTO> {
if (!input.context?.account_holder) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Missing account holder data while updating account holder."
)
}
let accountHolder: InferEntityType<typeof AccountHolder> | undefined
let providerAccountHolder: UpdateAccountHolderOutput | undefined
providerAccountHolder =
await this.paymentProviderService_.updateAccountHolder(
input.provider_id,
{
context: input.context,
}
)
// The data field can be empty when either the method is not supported or an account holder wasn't updated
// We still want to do the update as we might only be updating the metadata
accountHolder = await this.accountHolderService_.update(
{
id: input.id,
...(providerAccountHolder?.data
? { data: providerAccountHolder.data }
: {}),
metadata: input.metadata,
},
sharedContext
)
return await this.baseRepository_.serialize(accountHolder)
}
@InjectManager()
async deleteAccountHolder(
id: string,

View File

@@ -25,6 +25,8 @@ import {
RefundPaymentOutput,
SavePaymentMethodInput,
SavePaymentMethodOutput,
UpdateAccountHolderInput,
UpdateAccountHolderOutput,
UpdatePaymentInput,
UpdatePaymentOutput,
WebhookActionResult,
@@ -149,6 +151,21 @@ Please make sure that the provider is registered in the container and it is conf
return await provider.createAccountHolder(input)
}
async updateAccountHolder(
providerId: string,
input: UpdateAccountHolderInput
): Promise<UpdateAccountHolderOutput> {
const provider = this.retrieveProvider(providerId)
if (!provider.updateAccountHolder) {
this.#logger.warn(
`Provider ${providerId} does not support updating account holders`
)
return {} as unknown as UpdateAccountHolderOutput
}
return await provider.updateAccountHolder(input)
}
async deleteAccountHolder(
providerId: string,
input: DeleteAccountHolderInput

View File

@@ -26,6 +26,8 @@ import {
RetrievePaymentOutput,
SavePaymentMethodInput,
SavePaymentMethodOutput,
UpdateAccountHolderInput,
UpdateAccountHolderOutput,
UpdatePaymentInput,
UpdatePaymentOutput,
WebhookActionResult,
@@ -378,6 +380,66 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
}
}
async updateAccountHolder({
context,
}: UpdateAccountHolderInput): Promise<UpdateAccountHolderOutput> {
const { account_holder, customer, idempotency_key } = context
if (!account_holder?.data?.id) {
throw this.buildError(
"No account holder in context",
new Error("No account holder provided while updating account holder")
)
}
// If no customer context was provided, we simply don't update anything within the provider
if (!customer) {
return {}
}
const accountHolderId = account_holder.data.id as string
const shipping = customer.billing_address
? ({
address: {
city: customer.billing_address.city,
country: customer.billing_address.country_code,
line1: customer.billing_address.address_1,
line2: customer.billing_address.address_2,
postal_code: customer.billing_address.postal_code,
state: customer.billing_address.province,
},
} as Stripe.CustomerCreateParams.Shipping)
: undefined
try {
const stripeCustomer = await this.stripe_.customers.update(
accountHolderId,
{
email: customer.email,
name:
customer.company_name ||
`${customer.first_name ?? ""} ${customer.last_name ?? ""}`.trim() ||
undefined,
phone: customer.phone as string | undefined,
...shipping,
},
{
idempotencyKey: idempotency_key,
}
)
return {
data: stripeCustomer as unknown as Record<string, unknown>,
}
} catch (e) {
throw this.buildError(
"An error occurred in updateAccountHolder when updating a Stripe customer",
e
)
}
}
async deleteAccountHolder({
context,
}: DeleteAccountHolderInput): Promise<DeleteAccountHolderOutput> {