feat: Add support for listing saved payment methods in module and Stripe (#10994)

This commit is contained in:
Stevche Radevski authored and GitHub committed 2025-01-16 16:16:04 +01:00
1 parent 114b2133aa
commit f99f720dd4
8 files changed
+171

No files matched your search

+29
View File
@@ -1,6 +1,7 @@
import { BaseFilterable } from "../dal" import { BaseFilterable } from "../dal"
import { OperatorMap } from "../dal/utils" import { OperatorMap } from "../dal/utils"
import { BigNumberValue } from "../totals" import { BigNumberValue } from "../totals"
import { PaymentProviderContext } from "./provider"
/* ********** PAYMENT COLLECTION ********** */ /* ********** PAYMENT COLLECTION ********** */
export type PaymentCollectionStatus = export type PaymentCollectionStatus =
@@ -245,6 +246,22 @@ export interface FilterableRefundProps extends BaseFilterable<RefundDTO> {
*/ */
deleted_at?: OperatorMap<string> deleted_at?: OperatorMap<string>
} }
/**
* The filters to apply on the retrieved payment sessions.
*/
export interface FilterablePaymentMethodProps {
/**
* Filter the payment methods by provider.
*/
provider_id: string
/**
* Filter the payment methods by the context of their associated payment provider.
*/
context: PaymentProviderContext
}
/* ********** PAYMENT ********** */ /* ********** PAYMENT ********** */
export interface PaymentDTO { export interface PaymentDTO {
/** /**
@@ -568,6 +585,18 @@ export interface PaymentProviderDTO {
is_enabled: boolean 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. * The filters to apply on the retrieved payment providers.
*/ */
@@ -135,6 +135,11 @@ export type PaymentProviderAuthorizeResponse = {
data: PaymentProviderSessionResponse["data"] data: PaymentProviderSessionResponse["data"]
} }
export type PaymentMethodResponse = {
id: string
data: Record<string, unknown>
}
/** /**
* @interface * @interface
* *
@@ -249,6 +254,10 @@ export interface IPaymentProvider {
paymentSessionData: Record<string, unknown> paymentSessionData: Record<string, unknown>
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> ): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]>
listPaymentMethods(
context: PaymentProviderContext
): Promise<PaymentMethodResponse[]>
getPaymentStatus( getPaymentStatus(
paymentSessionData: Record<string, unknown> paymentSessionData: Record<string, unknown>
): Promise<PaymentSessionStatus> ): Promise<PaymentSessionStatus>
@@ -1,6 +1,8 @@
import { import {
CreatePaymentProviderSession, CreatePaymentProviderSession,
IPaymentProvider, IPaymentProvider,
PaymentMethodResponse,
PaymentProviderContext,
PaymentProviderError, PaymentProviderError,
PaymentProviderSessionResponse, PaymentProviderSessionResponse,
PaymentSessionStatus, PaymentSessionStatus,
@@ -623,6 +625,59 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
context: UpdatePaymentProviderSession context: UpdatePaymentProviderSession
): Promise<PaymentProviderError | PaymentProviderSessionResponse> ): 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 * 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. * to process the action of the payment provider.
@@ -23,4 +23,13 @@ export const joinerConfig = defineJoinerConfig(Modules.PAYMENT, {
payment_provider_id: PaymentProvider.name, payment_provider_id: PaymentProvider.name,
refund_reason_id: RefundReason.name, refund_reason_id: RefundReason.name,
}, },
alias: [
{
name: ["payment_method", "payment_methods"],
entity: "PaymentMethod",
args: {
methodSuffix: "PaymentMethods",
},
},
],
}) })
@@ -1,5 +1,6 @@
import { import {
CreatePaymentProviderSession, CreatePaymentProviderSession,
PaymentMethodResponse,
PaymentProviderError, PaymentProviderError,
PaymentProviderSessionResponse, PaymentProviderSessionResponse,
ProviderWebhookPayload, ProviderWebhookPayload,
@@ -72,6 +73,10 @@ export class SystemProviderService extends AbstractPaymentProvider {
return {} return {}
} }
async listPaymentMethods(_): Promise<PaymentMethodResponse[]> {
return []
}
async getWebhookActionAndData( async getWebhookActionAndData(
data: ProviderWebhookPayload["payload"] data: ProviderWebhookPayload["payload"]
): Promise<WebhookActionResult> { ): Promise<WebhookActionResult> {
@@ -8,6 +8,7 @@ import {
CreateRefundDTO, CreateRefundDTO,
DAL, DAL,
FilterablePaymentCollectionProps, FilterablePaymentCollectionProps,
FilterablePaymentMethodProps,
FilterablePaymentProviderProps, FilterablePaymentProviderProps,
FilterablePaymentSessionProps, FilterablePaymentSessionProps,
FindConfig, FindConfig,
@@ -20,6 +21,7 @@ import {
PaymentCollectionDTO, PaymentCollectionDTO,
PaymentCollectionUpdatableFields, PaymentCollectionUpdatableFields,
PaymentDTO, PaymentDTO,
PaymentMethodDTO,
PaymentProviderDTO, PaymentProviderDTO,
PaymentSessionDTO, PaymentSessionDTO,
ProviderWebhookPayload, ProviderWebhookPayload,
@@ -906,6 +908,33 @@ export default class PaymentModuleService
] ]
} }
@InjectManager()
async listPaymentMethods(
filters: FilterablePaymentMethodProps,
config: FindConfig<PaymentMethodDTO> = {},
@MedusaContext() sharedContext?: Context
): Promise<PaymentMethodDTO[]> {
return await this.paymentProviderService_.listPaymentMethods(
filters.provider_id,
filters.context
)
}
@InjectManager()
async listAndCountPaymentMethods(
filters: FilterablePaymentMethodProps,
config: FindConfig<PaymentMethodDTO> = {},
@MedusaContext() sharedContext?: Context
): Promise<[PaymentMethodDTO[], number]> {
const paymentMethods =
await this.paymentProviderService_.listPaymentMethods(
filters.provider_id,
filters.context
)
return [paymentMethods, paymentMethods.length]
}
@InjectManager() @InjectManager()
private async maybeUpdatePaymentCollection_( private async maybeUpdatePaymentCollection_(
paymentCollectionId: string, paymentCollectionId: string,
@@ -4,7 +4,9 @@ import {
DAL, DAL,
IPaymentProvider, IPaymentProvider,
Logger, Logger,
PaymentMethodResponse,
PaymentProviderAuthorizeResponse, PaymentProviderAuthorizeResponse,
PaymentProviderContext,
PaymentProviderDataInput, PaymentProviderDataInput,
PaymentProviderError, PaymentProviderError,
PaymentProviderSessionResponse, PaymentProviderSessionResponse,
@@ -150,6 +152,14 @@ Please make sure that the provider is registered in the container and it is conf
return res as Record<string, unknown> return res as Record<string, unknown>
} }
async listPaymentMethods(
providerId: string,
context: PaymentProviderContext
): Promise<PaymentMethodResponse[]> {
const provider = this.retrieveProvider(providerId)
return await provider.listPaymentMethods(context)
}
async getWebhookActionAndData( async getWebhookActionAndData(
providerId: string, providerId: string,
data: ProviderWebhookPayload["payload"] data: ProviderWebhookPayload["payload"]
@@ -2,6 +2,8 @@ import Stripe from "stripe"
import { import {
CreatePaymentProviderSession, CreatePaymentProviderSession,
PaymentMethodResponse,
PaymentProviderContext,
PaymentProviderError, PaymentProviderError,
PaymentProviderSessionResponse, PaymentProviderSessionResponse,
ProviderWebhookPayload, ProviderWebhookPayload,
@@ -86,6 +88,8 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
res.payment_method = extra?.payment_method as string | undefined res.payment_method = extra?.payment_method as string | undefined
res.return_url = extra?.return_url as string | undefined
return res return res
} }
@@ -294,6 +298,27 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
} }
} }
async listPaymentMethods(
context: PaymentProviderContext
): Promise<PaymentMethodResponse[]> {
const customerId = context.customer?.metadata?.stripe_id
if (!customerId) {
return []
}
const paymentMethods = await this.stripe_.customers.listPaymentMethods(
customerId as string,
// In order to keep the interface simple, we just list the maximum payment methods, which should be enough in almost all cases.
// We can always extend the interface to allow additional filtering, if necessary.
{ limit: 100 }
)
return paymentMethods.data.map((method) => ({
id: method.id,
data: method as unknown as Record<string, unknown>,
}))
}
async getWebhookActionAndData( async getWebhookActionAndData(
webhookData: ProviderWebhookPayload["payload"] webhookData: ProviderWebhookPayload["payload"]
): Promise<WebhookActionResult> { ): Promise<WebhookActionResult> {