diff --git a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts index 9ee3d811de..3274f93a05 100644 --- a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts @@ -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( diff --git a/integration-tests/modules/__tests__/cart/store/carts.spec.ts b/integration-tests/modules/__tests__/cart/store/carts.spec.ts index 1c19c8cced..cf2e908c8a 100644 --- a/integration-tests/modules/__tests__/cart/store/carts.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/carts.spec.ts @@ -1330,7 +1330,6 @@ medusaIntegrationTestRunner({ ).data.shipping_option paymentCollection = await paymentService.createPaymentCollections({ - region_id: region.id, amount: 1000, currency_code: "usd", }) diff --git a/packages/core/types/src/payment/common.ts b/packages/core/types/src/payment/common.ts index 2b3de29fd1..9450ad949c 100644 --- a/packages/core/types/src/payment/common.ts +++ b/packages/core/types/src/payment/common.ts @@ -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 -} - /** * 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 + + /** + * The ID of the associated payment provider. + */ + provider_id: string +} diff --git a/packages/core/types/src/payment/mutations.ts b/packages/core/types/src/payment/mutations.ts index d1bc6c92c7..72e42ba323 100644 --- a/packages/core/types/src/payment/mutations.ts +++ b/packages/core/types/src/payment/mutations.ts @@ -318,3 +318,23 @@ export interface UpdateRefundReasonDTO { */ metadata?: Record | 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 + + /** + * Necessary context data for the associated payment provider. + */ + context: PaymentProviderContext +} diff --git a/packages/core/types/src/payment/provider.ts b/packages/core/types/src/payment/provider.ts index 5a7f5b8820..2fcc63ff90 100644 --- a/packages/core/types/src/payment/provider.ts +++ b/packages/core/types/src/payment/provider.ts @@ -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 + + /** + * The context of the payment provider, such as the customer ID. + */ + context: PaymentProviderContext +} + /** * @interface * @@ -118,6 +130,18 @@ export type PaymentProviderSessionResponse = { data: Record } +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 +} + /** * @interface * @@ -254,10 +278,14 @@ export interface IPaymentProvider { paymentSessionData: Record ): Promise - listPaymentMethods( + listPaymentMethods?( context: PaymentProviderContext ): Promise + savePaymentMethod?( + input: SavePaymentMethod + ): Promise + getPaymentStatus( paymentSessionData: Record ): Promise diff --git a/packages/core/types/src/payment/service.ts b/packages/core/types/src/payment/service.ts index f19d8b0f40..c72501563d 100644 --- a/packages/core/types/src/payment/service.ts +++ b/packages/core/types/src/payment/service.ts @@ -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} 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} 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, + sharedContext?: Context + ): Promise + + /** + * 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} 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, + sharedContext?: Context + ): Promise<[PaymentMethodDTO[], number]> + /** * This method retrieves a paginated list of captures based on optional filters and configuration. * diff --git a/packages/core/utils/src/payment/abstract-payment-provider.ts b/packages/core/utils/src/payment/abstract-payment-provider.ts index 62e0fee02f..abcfc39f23 100644 --- a/packages/core/utils/src/payment/abstract-payment-provider.ts +++ b/packages/core/utils/src/payment/abstract-payment-provider.ts @@ -1,8 +1,6 @@ import { CreatePaymentProviderSession, IPaymentProvider, - PaymentMethodResponse, - PaymentProviderContext, PaymentProviderError, PaymentProviderSessionResponse, PaymentSessionStatus, @@ -625,59 +623,6 @@ export abstract class AbstractPaymentProvider> context: UpdatePaymentProviderSession ): Promise - /** - * 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 { - * 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 - /** * 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. diff --git a/packages/modules/payment/src/providers/system.ts b/packages/modules/payment/src/providers/system.ts index f5765140e7..f5901bf449 100644 --- a/packages/modules/payment/src/providers/system.ts +++ b/packages/modules/payment/src/providers/system.ts @@ -1,6 +1,5 @@ import { CreatePaymentProviderSession, - PaymentMethodResponse, PaymentProviderError, PaymentProviderSessionResponse, ProviderWebhookPayload, @@ -73,10 +72,6 @@ export class SystemProviderService extends AbstractPaymentProvider { return {} } - async listPaymentMethods(_): Promise { - return [] - } - async getWebhookActionAndData( data: ProviderWebhookPayload["payload"] ): Promise { diff --git a/packages/modules/payment/src/services/payment-module.ts b/packages/modules/payment/src/services/payment-module.ts index dc0f2f391d..71791454be 100644 --- a/packages/modules/payment/src/services/payment-module.ts +++ b/packages/modules/payment/src/services/payment-module.ts @@ -4,6 +4,7 @@ import { Context, CreateCaptureDTO, CreatePaymentCollectionDTO, + CreatePaymentMethodDTO, CreatePaymentSessionDTO, CreateRefundDTO, DAL, @@ -914,10 +915,16 @@ export default class PaymentModuleService config: FindConfig = {}, @MedusaContext() sharedContext?: Context ): Promise { - 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 + + createPaymentMethods( + data: CreatePaymentMethodDTO[], + sharedContext?: Context + ): Promise + @InjectManager() + async createPaymentMethods( + data: CreatePaymentMethodDTO | CreatePaymentMethodDTO[], + @MedusaContext() sharedContext?: Context + ): Promise { + 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() diff --git a/packages/modules/payment/src/services/payment-provider.ts b/packages/modules/payment/src/services/payment-provider.ts index fb75896092..171240c8f1 100644 --- a/packages/modules/payment/src/services/payment-provider.ts +++ b/packages/modules/payment/src/services/payment-provider.ts @@ -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 | undefined> { + ): Promise { 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 { 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 { + 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"] diff --git a/packages/modules/providers/payment-stripe/src/core/stripe-base.ts b/packages/modules/providers/payment-stripe/src/core/stripe-base.ts index 3db9cf86cd..8ac308e054 100644 --- a/packages/modules/providers/payment-stripe/src/core/stripe-base.ts +++ b/packages/modules/providers/payment-stripe/src/core/stripe-base.ts @@ -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 { })) } + async savePaymentMethod( + input: SavePaymentMethod + ): Promise { + 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 } + } + async getWebhookActionAndData( webhookData: ProviderWebhookPayload["payload"] ): Promise {