feat: Add support for creating payment methods to payment module (#11063)

CLOSES CLO-407
This commit is contained in:
Stevche Radevski
2025-01-21 12:31:44 +01:00
committed by GitHub
parent cd758067d4
commit 05c8a67d8e
11 changed files with 240 additions and 79 deletions

View File

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

View File

@@ -1330,7 +1330,6 @@ medusaIntegrationTestRunner({
).data.shipping_option
paymentCollection = await paymentService.createPaymentCollections({
region_id: region.id,
amount: 1000,
currency_code: "usd",
})

View File

@@ -585,18 +585,6 @@ export interface PaymentProviderDTO {
is_enabled: boolean
}
export interface PaymentMethodDTO {
/**
* The ID of the payment method in the payment provider's system.
*/
id: string
/**
* The data of the payment method, as returned by the payment provider.
*/
data: Record<string, unknown>
}
/**
* The filters to apply on the retrieved payment providers.
*/
@@ -657,3 +645,20 @@ export interface RefundReasonDTO {
*/
updated_at: Date | string
}
export interface PaymentMethodDTO {
/**
* The ID of the payment method.
*/
id: string
/**
* The data of the payment method, as returned by the payment provider.
*/
data: Record<string, unknown>
/**
* The ID of the associated payment provider.
*/
provider_id: string
}

View File

@@ -318,3 +318,23 @@ export interface UpdateRefundReasonDTO {
*/
metadata?: Record<string, unknown> | null
}
/**
* The payment method to be created.
*/
export interface CreatePaymentMethodDTO {
/**
* The provider's ID.
*/
provider_id: string
/**
* Necessary data for the associated payment provider to process the payment.
*/
data: Record<string, unknown>
/**
* Necessary context data for the associated payment provider.
*/
context: PaymentProviderContext
}

View File

@@ -78,6 +78,18 @@ export type CreatePaymentProviderSession = {
currency_code: string
}
export type SavePaymentMethod = {
/**
* Any data that should be used by the provider for saving the payment method.
*/
data: Record<string, unknown>
/**
* The context of the payment provider, such as the customer ID.
*/
context: PaymentProviderContext
}
/**
* @interface
*
@@ -118,6 +130,18 @@ export type PaymentProviderSessionResponse = {
data: Record<string, unknown>
}
export type SavePaymentMethodResponse = {
/**
* The ID of the payment method in the payment provider.
*/
id: string
/**
* The data returned from the payment provider after saving the payment method.
*/
data: Record<string, unknown>
}
/**
* @interface
*
@@ -254,10 +278,14 @@ export interface IPaymentProvider {
paymentSessionData: Record<string, unknown>
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]>
listPaymentMethods(
listPaymentMethods?(
context: PaymentProviderContext
): Promise<PaymentMethodResponse[]>
savePaymentMethod?(
input: SavePaymentMethod
): Promise<PaymentProviderError | SavePaymentMethodResponse>
getPaymentStatus(
paymentSessionData: Record<string, unknown>
): Promise<PaymentSessionStatus>

View File

@@ -6,6 +6,7 @@ import {
CaptureDTO,
FilterableCaptureProps,
FilterablePaymentCollectionProps,
FilterablePaymentMethodProps,
FilterablePaymentProps,
FilterablePaymentProviderProps,
FilterablePaymentSessionProps,
@@ -13,6 +14,7 @@ import {
FilterableRefundReasonProps,
PaymentCollectionDTO,
PaymentDTO,
PaymentMethodDTO,
PaymentProviderDTO,
PaymentSessionDTO,
RefundDTO,
@@ -749,6 +751,74 @@ export interface IPaymentModuleService extends IModuleService {
sharedContext?: Context
): Promise<[PaymentProviderDTO[], number]>
/**
* This method retrieves all payment methods based on the context and configuration.
*
* @param {FilterablePaymentMethodProps} filters - The filters to apply on the retrieved payment methods.
* @param {FindConfig<PaymentMethodDTO>} config - The configurations determining how the payment method is retrieved. Its properties, such as `select` or `relations`, accept the
* attributes or relations associated with a payment method.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<PaymentMethodDTO[]>} The list of payment methods.
*
* @example
* To retrieve a list of payment methods for a customer:
*
* ```ts
* const paymentMethods =
* await paymentModuleService.listPaymentMethods({
* provider_id: "pp_stripe_stripe",
* context: {
* customer: {
* id: "cus_123",
* metadata: {
* pp_stripe_stripe_customer_id: "str_1234"
* }
* },
* },
* })
* ```
*
*/
listPaymentMethods(
filters: FilterablePaymentMethodProps,
config: FindConfig<PaymentMethodDTO>,
sharedContext?: Context
): Promise<PaymentMethodDTO[]>
/**
* This method retrieves all payment methods along with the total count of available payment methods, based on the context and configuration.
*
* @param {FilterablePaymentMethodProps} filters - The filters to apply on the retrieved payment methods.
* @param {FindConfig<PaymentMethodDTO>} config - The configurations determining how the payment method is retrieved. Its properties, such as `select` or `relations`, accept the
* attributes or relations associated with a payment method.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<[PaymentMethodDTO[], number]>} The list of payment methods along with their total count.
*
* @example
* To retrieve a list of payment methods for a customer:
*
* ```ts
* const [paymentMethods, count] =
* await paymentModuleService.listAndCountPaymentMethods({
* provider_id: "pp_stripe_stripe",
* context: {
* customer: {
* id: "cus_123",
* metadata: {
* pp_stripe_stripe_customer_id: "str_1234"
* }
* },
* },
* })
* ```
*
*/
listAndCountPaymentMethods(
filters: FilterablePaymentMethodProps,
config: FindConfig<PaymentMethodDTO>,
sharedContext?: Context
): Promise<[PaymentMethodDTO[], number]>
/**
* This method retrieves a paginated list of captures based on optional filters and configuration.
*

View File

@@ -1,8 +1,6 @@
import {
CreatePaymentProviderSession,
IPaymentProvider,
PaymentMethodResponse,
PaymentProviderContext,
PaymentProviderError,
PaymentProviderSessionResponse,
PaymentSessionStatus,
@@ -625,59 +623,6 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
context: UpdatePaymentProviderSession
): Promise<PaymentProviderError | PaymentProviderSessionResponse>
/**
* List the payment methods associated with the context (eg. customer) of the payment provider, if any.
*
* @param context - The context for which the payment methods are listed. Usually the customer should be provided.
* @returns An object whose `payment_methods` property is set to the data returned by the payment provider.
*
* @example
* // other imports...
* import {
* PaymentProviderContext,
* PaymentProviderError,
* PaymentMethodResponse
* PaymentProviderSessionResponse,
* } from "@medusajs/framework/types"
*
*
* class MyPaymentProviderService extends AbstractPaymentProvider<
* Options
* > {
* async listPaymentMethods(
* context: PaymentProviderContext
* ): Promise<PaymentMethodResponse> {
* const {
* customer,
* } = context
* const externalCustomerId = customer.metadata.stripe_id
*
* try {
* // assuming you have a client that updates the payment
* const response = await this.client.listPaymentMethods(
* {customer: externalCustomerId}
* )
*
* return response.map((method) => ({
* id: method.id,
* data: method
* }))
* } catch (e) {
* return {
* error: e,
* code: "unknown",
* detail: e
* }
* }
* }
*
* // ...
* }
*/
abstract listPaymentMethods(
context: PaymentProviderContext
): Promise<PaymentMethodResponse[]>
/**
* This method is executed when a webhook event is received from the third-party payment provider. Use it
* to process the action of the payment provider.

View File

@@ -1,6 +1,5 @@
import {
CreatePaymentProviderSession,
PaymentMethodResponse,
PaymentProviderError,
PaymentProviderSessionResponse,
ProviderWebhookPayload,
@@ -73,10 +72,6 @@ export class SystemProviderService extends AbstractPaymentProvider {
return {}
}
async listPaymentMethods(_): Promise<PaymentMethodResponse[]> {
return []
}
async getWebhookActionAndData(
data: ProviderWebhookPayload["payload"]
): Promise<WebhookActionResult> {

View File

@@ -4,6 +4,7 @@ import {
Context,
CreateCaptureDTO,
CreatePaymentCollectionDTO,
CreatePaymentMethodDTO,
CreatePaymentSessionDTO,
CreateRefundDTO,
DAL,
@@ -914,10 +915,16 @@ export default class PaymentModuleService
config: FindConfig<PaymentMethodDTO> = {},
@MedusaContext() sharedContext?: Context
): Promise<PaymentMethodDTO[]> {
return await this.paymentProviderService_.listPaymentMethods(
const res = await this.paymentProviderService_.listPaymentMethods(
filters.provider_id,
filters.context
)
return res.map((item) => ({
id: item.id,
data: item.data,
provider_id: filters.provider_id,
}))
}
@InjectManager()
@@ -932,7 +939,48 @@ export default class PaymentModuleService
filters.context
)
return [paymentMethods, paymentMethods.length]
const normalizedResponse = paymentMethods.map((item) => ({
id: item.id,
data: item.data,
provider_id: filters.provider_id,
}))
return [normalizedResponse, paymentMethods.length]
}
// @ts-ignore
createPaymentMethods(
data: CreatePaymentCollectionDTO,
sharedContext?: Context
): Promise<PaymentCollectionDTO>
createPaymentMethods(
data: CreatePaymentMethodDTO[],
sharedContext?: Context
): Promise<PaymentMethodDTO[]>
@InjectManager()
async createPaymentMethods(
data: CreatePaymentMethodDTO | CreatePaymentMethodDTO[],
@MedusaContext() sharedContext?: Context
): Promise<PaymentMethodDTO | PaymentMethodDTO[]> {
const input = Array.isArray(data) ? data : [data]
const result = await promiseAll(
input.map((item) =>
this.paymentProviderService_.savePaymentMethod(item.provider_id, item)
),
{ aggregateErrors: true }
)
const normalizedResponse = result.map((item, i) => {
return {
id: item.id,
data: item.data,
provider_id: input[i].provider_id,
}
})
return Array.isArray(data) ? normalizedResponse : normalizedResponse[0]
}
@InjectManager()

View File

@@ -12,6 +12,8 @@ import {
PaymentProviderSessionResponse,
PaymentSessionStatus,
ProviderWebhookPayload,
SavePaymentMethod,
SavePaymentMethodResponse,
UpdatePaymentProviderSession,
WebhookActionResult,
} from "@medusajs/framework/types"
@@ -73,7 +75,7 @@ Please make sure that the provider is registered in the container and it is conf
async updateSession(
providerId: string,
sessionInput: UpdatePaymentProviderSession
): Promise<Record<string, unknown> | undefined> {
): Promise<PaymentProviderSessionResponse["data"]> {
const provider = this.retrieveProvider(providerId)
const paymentResponse = await provider.updatePayment(sessionInput)
@@ -157,9 +159,37 @@ Please make sure that the provider is registered in the container and it is conf
context: PaymentProviderContext
): Promise<PaymentMethodResponse[]> {
const provider = this.retrieveProvider(providerId)
if (!provider.listPaymentMethods) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Provider ${providerId} does not support listing payment methods`
)
}
return await provider.listPaymentMethods(context)
}
async savePaymentMethod(
providerId: string,
input: SavePaymentMethod
): Promise<SavePaymentMethodResponse> {
const provider = this.retrieveProvider(providerId)
if (!provider.savePaymentMethod) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Provider ${providerId} does not support saving payment methods`
)
}
const res = await provider.savePaymentMethod(input)
if (isPaymentProviderError(res)) {
this.throwPaymentProviderError(res)
}
return res as SavePaymentMethodResponse
}
async getWebhookActionAndData(
providerId: string,
data: ProviderWebhookPayload["payload"]

View File

@@ -7,6 +7,8 @@ import {
PaymentProviderError,
PaymentProviderSessionResponse,
ProviderWebhookPayload,
SavePaymentMethod,
SavePaymentMethodResponse,
UpdatePaymentProviderSession,
WebhookActionResult,
} from "@medusajs/framework/types"
@@ -319,6 +321,27 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
}))
}
async savePaymentMethod(
input: SavePaymentMethod
): Promise<PaymentProviderError | SavePaymentMethodResponse> {
const { context, data } = input
const customer = context?.customer
if (!customer?.metadata?.stripe_id) {
return this.buildError(
"Account holder not set while saving a payment method",
new Error("Missing account holder")
)
}
const resp = await this.stripe_.setupIntents.create({
customer: customer.metadata.stripe_id as string,
...data,
})
return { id: resp.id, data: resp as unknown as Record<string, unknown> }
}
async getWebhookActionAndData(
webhookData: ProviderWebhookPayload["payload"]
): Promise<WebhookActionResult> {