From de6485aa9999baa678a47e3453b5c604193199a5 Mon Sep 17 00:00:00 2001 From: shij Date: Wed, 28 Aug 2024 18:49:11 +0000 Subject: [PATCH] tmp --- .../steps/create-payment-session.ts | 2 - .../steps/delete-payment-sessions.ts | 1 - .../src/payment/steps/capture-payment.ts | 5 +- .../src/payment/steps/refund-payment.ts | 7 +- packages/core/types/src/payment/common.ts | 13 +- packages/core/types/src/payment/mutations.ts | 38 +- packages/core/types/src/payment/provider.ts | 237 ++--- packages/core/types/src/payment/service.ts | 32 +- .../src/payment/abstract-payment-provider.ts | 48 +- .../utils/src/payment/payment-collection.ts | 22 +- .../core/utils/src/payment/payment-session.ts | 20 +- .../payment/src/models/payment-collection.ts | 4 +- .../modules/payment/src/providers/system.ts | 202 +++- .../payment/src/services/payment-module.ts | 912 ++++++++++-------- .../payment/src/services/payment-provider.ts | 121 +-- .../payment-stripe/src/core/stripe-base.ts | 398 ++++---- .../payment-stripe/src/types/index.ts | 4 + 17 files changed, 1118 insertions(+), 948 deletions(-) diff --git a/packages/core/core-flows/src/payment-collection/steps/create-payment-session.ts b/packages/core/core-flows/src/payment-collection/steps/create-payment-session.ts index 152c7630f41c9..82feeb12b8ef3 100644 --- a/packages/core/core-flows/src/payment-collection/steps/create-payment-session.ts +++ b/packages/core/core-flows/src/payment-collection/steps/create-payment-session.ts @@ -13,7 +13,6 @@ interface StepInput { currency_code: string provider_token?: string context?: PaymentProviderContext - data?: Record } export const createPaymentSessionStepId = "create-payment-session" @@ -31,7 +30,6 @@ export const createPaymentSessionStep = createStep( provider_token: input.provider_token, currency_code: input.currency_code, amount: input.amount, - data: input.data ?? {}, context: input.context, } ) diff --git a/packages/core/core-flows/src/payment-collection/steps/delete-payment-sessions.ts b/packages/core/core-flows/src/payment-collection/steps/delete-payment-sessions.ts index 03413ccbaf2a4..8daf6e620b721 100644 --- a/packages/core/core-flows/src/payment-collection/steps/delete-payment-sessions.ts +++ b/packages/core/core-flows/src/payment-collection/steps/delete-payment-sessions.ts @@ -80,7 +80,6 @@ export const deletePaymentSessionsStep = createStep( provider_id: paymentSession.provider_id, currency_code: paymentSession.currency_code, amount: paymentSession.amount, - data: paymentSession.data ?? {}, context: paymentSession.context, } diff --git a/packages/core/core-flows/src/payment/steps/capture-payment.ts b/packages/core/core-flows/src/payment/steps/capture-payment.ts index c339c615fb274..57ddf5d1391fc 100644 --- a/packages/core/core-flows/src/payment/steps/capture-payment.ts +++ b/packages/core/core-flows/src/payment/steps/capture-payment.ts @@ -16,7 +16,10 @@ export const capturePaymentStep = createStep( ModuleRegistrationName.PAYMENT ) - const payment = await paymentModule.capturePayment(input) + const payment = await paymentModule.capturePayment(input.payment_id, { + amount: input.amount, + captured_by: input.captured_by, + }) return new StepResponse(payment) } diff --git a/packages/core/core-flows/src/payment/steps/refund-payment.ts b/packages/core/core-flows/src/payment/steps/refund-payment.ts index 6d18a4d6981ab..59355331a9415 100644 --- a/packages/core/core-flows/src/payment/steps/refund-payment.ts +++ b/packages/core/core-flows/src/payment/steps/refund-payment.ts @@ -4,7 +4,7 @@ import { StepResponse, createStep } from "@medusajs/workflows-sdk" type StepInput = { payment_id: string - created_by?: string + refunded_by?: string amount?: BigNumberInput } @@ -16,7 +16,10 @@ export const refundPaymentStep = createStep( ModuleRegistrationName.PAYMENT ) - const payment = await paymentModule.refundPayment(input) + const payment = await paymentModule.refundPayment(input.payment_id, { + amount: input.amount, + refunded_by: input.refunded_by, + }) return new StepResponse(payment) } diff --git a/packages/core/types/src/payment/common.ts b/packages/core/types/src/payment/common.ts index 2dbc248c36862..7ff267cf92d4c 100644 --- a/packages/core/types/src/payment/common.ts +++ b/packages/core/types/src/payment/common.ts @@ -4,19 +4,24 @@ import { BigNumberValue } from "../totals" /* ********** PAYMENT COLLECTION ********** */ export type PaymentCollectionStatus = - | "not_paid" - | "awaiting" + | "pending" + | "paid" + | "partially_paid" | "authorized" | "partially_authorized" - | "canceled" + | "refunded" + | "partially_refunded" export type PaymentSessionStatus = | "authorized" | "captured" + | "partially_captured" + | "refunded" + | "partially_refunded" | "pending" | "requires_more" - | "error" | "canceled" + | "processing" /** * The payment collection details. diff --git a/packages/core/types/src/payment/mutations.ts b/packages/core/types/src/payment/mutations.ts index dc1a1ec750477..205e579d07839 100644 --- a/packages/core/types/src/payment/mutations.ts +++ b/packages/core/types/src/payment/mutations.ts @@ -186,11 +186,6 @@ export interface CreateCaptureDTO { */ amount?: BigNumberInput - /** - * The associated payment's ID. - */ - payment_id: string - /** * Who captured the payment. For example, * a user's ID. @@ -207,16 +202,11 @@ export interface CreateRefundDTO { */ amount?: BigNumberInput - /** - * The associated payment's ID. - */ - payment_id: string - /** * Who refunded the payment. For example, * a user's ID. */ - created_by?: string + refunded_by?: string } /** @@ -243,11 +233,6 @@ export interface CreatePaymentSessionDTO { */ amount: BigNumberInput - /** - * Necessary data for the associated payment provider to process the payment. - */ - data: Record - /** * Necessary context data for the associated payment provider. */ @@ -264,19 +249,14 @@ export interface UpdatePaymentSessionDTO { id: string /** - * Necessary data for the associated payment provider to process the payment. - */ - data: Record - - /** - * The ISO 3 character currency code. + * The provider's payment method token */ - currency_code: string + provider_token?: string /** * The amount to be authorized. */ - amount: BigNumberInput + amount?: BigNumberInput /** * Necessary context data for the associated payment provider. @@ -284,6 +264,16 @@ export interface UpdatePaymentSessionDTO { context?: PaymentProviderContext } +/** + * The attributes to authorize in a payment session. + */ +export interface AuthorizePaymentSessionDTO { + /** + * The provider token to authorize payment session + */ + provider_token?: string +} + /** * The payment provider to be created. */ diff --git a/packages/core/types/src/payment/provider.ts b/packages/core/types/src/payment/provider.ts index 0f65e9f62a480..91c1ba39998da 100644 --- a/packages/core/types/src/payment/provider.ts +++ b/packages/core/types/src/payment/provider.ts @@ -10,25 +10,16 @@ import { ProviderWebhookPayload } from "./mutations" export type PaymentAddressDTO = Partial /** - * The customer associated with the payment. + * The customer of the payment. */ export type PaymentCustomerDTO = Partial -/** - * Normalized events from payment provider to internal payment module events. - */ -export type PaymentActions = - | "authorized" - | "captured" - | "failed" - | "not_supported" - /** * @interface * - * Context data provided to the payment provider when authorizing a payment session. + * Context data provided to the payment provider. */ -export type PaymentProviderContext = { +export type PaymentProviderContext = Record & { /** * The payment's billing address. */ @@ -55,21 +46,15 @@ export type PaymentProviderContext = { order_id?: string /** - * The customer associated with this payment. + * The associated customer detail */ customer?: PaymentCustomerDTO - - /** - * The extra fields specific to the provider session. - */ - extra?: Record } /** * @interface * - * The data used initiate a payment in a provider when a payment - * session is created. + * The data used initiate a payment in a provider */ export type CreatePaymentProviderSession = { /** @@ -96,76 +81,115 @@ export type CreatePaymentProviderSession = { /** * @interface * - * The attributes to update a payment related to a payment session in a provider. + * The attributes to update a payment in a provider. */ export type UpdatePaymentProviderSession = { - /** - * A payment's context. - */ - context: PaymentProviderContext - /** * The `data` field of the payment session. */ data: Record /** - * The payment session's amount. + * A context necessary for the payment provider. */ - amount: BigNumberInput + context?: PaymentProviderContext /** - * The ISO 3 character code of the payment session. + * The amount to be authorized. */ - currency_code: string + amount?: BigNumberInput + + /* + * The payment method token + */ + token?: string } /** * @interface * - * The response of operations on a payment. + * The attributes to authorize a payment in a provider. */ -export type PaymentProviderSessionResponse = { +export type AuthorizePaymentProviderSession = { /** - * The data to be stored in the `data` field of the Payment Session to be created. - * The `data` field is useful to hold any data required by the third-party provider to process the payment or retrieve its details at a later point. + * The `data` field of the payment session. */ data: Record + + /* + * The payment method token + */ + token?: string } /** * @interface * - * The successful result of authorizing a payment session using a payment provider. + * The response of operations on a payment. */ -export type PaymentProviderAuthorizeResponse = { +export type PaymentProviderSessionResponse = { /** * The status of the payment, which will be stored in the payment session's `status` field. */ status: PaymentSessionStatus /** - * The `data` to be stored in the payment session's `data` field. + * The captured amount of the payment */ - data: PaymentProviderSessionResponse["data"] -} + capturedAmount: BigNumberValue -/** - * @interface - * - * The details of which payment provider to use to perform an action, and what - * data to be passed to that provider. - */ -export type PaymentProviderDataInput = { /** - * The ID of the provider to be used to perform an action. + * The refunded amount of the payment */ - provider_id: string + refundedAmount: BigNumberValue /** - * The data to be passed to the provider. + * The data to be stored in the `data` field of the Payment Session to be created. + * The `data` field is useful to hold any data required by the third-party provider to process the payment or retrieve its details at a later point. */ data: Record + + /** + * A context necessary for the payment provider. + */ + context: PaymentProviderContext + + /** + * The related event or mutation occurred on a payment. + */ + event?: { + /** + * Event type that related to changes in provider payment + */ + type: "authorize" | "capture" | "refund" | "cancel" + + /** + * The event's details. + */ + detail?: { + /** + * The amount to be changed + */ + amount?: BigNumberValue + + /** + * Who captured the payment. For example, + * a user's ID. + */ + captured_by?: string + + /** + * Who refunded the payment. For example, + * a user's ID. + */ + refunded_by?: string + + /** + * The reason to be canceled + */ + reason?: string + } + } } /** @@ -188,57 +212,6 @@ export interface PaymentProviderError { detail?: any } -/** - * @interface - * - * The details of an action to be performed as a result of a received webhook event. - */ -export type WebhookActionData = { - /** - * The associated payment session's ID. - */ - session_id: string - - /** - * The associated cart's ID. - */ - cart_id: string - - /** - * The associated order's ID. - */ - order_id: string - - /** - * The amount to be captured or authorized (based on the action's type.) - */ - amount: BigNumberValue -} - -/** - * @interface - * - * The actions that the payment provider informs the Payment Module to perform. - */ -export type WebhookActionResult = - | { - /** - * Received an event that is not processable. - */ - action: "not_supported" - } - | { - /** - * Normalized events from payment provider to internal payment module events. - */ - action: PaymentActions - - /** - * The webhook action's details. - */ - data: WebhookActionData - } - export interface IPaymentProvider { /** * @ignore @@ -253,8 +226,7 @@ export interface IPaymentProvider { * For example, in the Stripe provider, this method is used to create a Payment Intent for the customer. * * @param {CreatePaymentProviderSession} data - The data necessary to initiate the payment. - * @returns {Promise} Either the payment's data, which is stored in the `data` field - * of the payment session, or an error object. + * @returns {Promise} Either the payment's status and data or an error object. */ initiatePayment( data: CreatePaymentProviderSession @@ -263,22 +235,22 @@ export interface IPaymentProvider { /** * This method is used to update a payment associated with a session in the third-party provider. * - * @param {UpdatePaymentProviderSession} context - The data related to the update. - * @returns {Promise} Either the payment's data or an error object. + * @param {UpdatePaymentProviderSession} data - The data related to the update. + * @returns {Promise} Either the payment's status and data or an error object. */ updatePayment( - context: UpdatePaymentProviderSession + data: UpdatePaymentProviderSession ): Promise /** * This method is called before a payment session is deleted. It's used to perform any actions necessary before the deletion. * * @param {Record} paymentSessionData - The `data` field of the Payment Session. - * @returns {Promise} Either an error object or an empty object. + * @returns {Promise} Either an error object or null if successful. */ deletePayment( paymentSessionData: Record - ): Promise + ): Promise /** * This method is called when a payment session should be authorized. @@ -287,15 +259,12 @@ export interface IPaymentProvider { * Refer to [this guide](https://docs.medusajs.com/experimental/payment/payment-flow/#3-authorize-payment-session) * to learn more about how this fits into the payment flow and how to handle required actions. * - * @param {Record} paymentSessionData - The `data` field of the payment session. - * @param {Record} context - The context of the authorization. - * @returns {Promise} The authorization details or an error object. If - * the authorization details are returned, the `data` and `status` field are set in the associated payment session. + * @param {AuthorizePaymentProviderSession} data - The data related to authorize. + * @returns {Promise} Either the payment's status and data or an error object. */ authorizePayment( - paymentSessionData: Record, - context: Record - ): Promise + data: AuthorizePaymentProviderSession + ): Promise /** * This method is called when a payment should be captured. The payment is captured in one of the following scenarios: @@ -306,64 +275,52 @@ export interface IPaymentProvider { * * In this method, you can interact with the third-party provider and perform any actions necessary to capture the payment. * - * @param {Record} paymentSessionData - The `data` field of the payment. + * @param {Record} paymentSessionData - The `data` field of the Payment Session. * @param {BigNumberInput} captureAmount - The amount to capture. - * @returns {Promise} Either an error object or a value that's stored in the `data` field of the payment capture. + * @returns {Promise} Either the payment's status and data or an error object. */ capturePayment( paymentSessionData: Record, captureAmount?: BigNumberInput - ): Promise + ): Promise /** * This method is called when a payment should be refunded. This is typically triggered manually by the merchant. * * In this method, you can interact with the third-party provider and perform any actions necessary to refund the payment. * - * @param {Record} paymentSessionData - The `data` field of a Payment. + * @param {Record} paymentSessionData - The `data` field of the Payment Session. * @param {BigNumberInput} refundAmount - The amount to refund. - * @returns {Promise} Either an error object or an object that's stored in the `data` field of the payment refund. + * @returns {Promise} Either the payment's status and data or an error object. */ refundPayment( paymentSessionData: Record, - refundAmount: BigNumberInput - ): Promise + refundAmount?: BigNumberInput + ): Promise /** * This method is used to provide a uniform way of retrieving the payment information from the third-party provider. * * For example, in Stripe’s payment provider this method is used to retrieve the payment intent details from Stripe. * - * @param {Record} paymentSessionData - - * The `data` field of a payment session. Make sure to store in the `data` field any necessary data that would allow you to retrieve the payment data from the third-party provider. - * @returns {Promise} Either an error object or the payment's data retrieved from a third-party provider. + * @param {Record} paymentSessionData - The `data` field of the Payment Session. + * @returns {Promise} Either the payment's status and data or an error object. */ retrievePayment( paymentSessionData: Record - ): Promise + ): Promise /** * This method is called when a payment is canceled. * * In this method, you can interact with the third-party provider and perform any actions necessary to cancel the payment. * - * @param {Record} paymentSessionData - The `data` field of the payment. - * @returns {Promise} Either an error object or a value that's stored in the `data` field of the payment. + * @param {Record} paymentSessionData - The `data` field of the Payment Session. + * @returns {Promise} Either the payment's status and data or an error object. */ cancelPayment( paymentSessionData: Record - ): Promise - - /** - * This method is used to get the status of a payment or a payment session. - * - * @param {Record} paymentSessionData - - * The `data` field of a payment as a parameter. You can use this data to interact with the third-party provider to check the status of the payment if necessary. - * @returns {Promise} The status of the payment or payment session. - */ - getPaymentStatus( - paymentSessionData: Record - ): Promise + ): Promise /** * The method is called when a webhook event is received for this provider. @@ -376,5 +333,5 @@ export interface IPaymentProvider { */ getWebhookActionAndData( data: ProviderWebhookPayload["payload"] - ): Promise + ): Promise } diff --git a/packages/core/types/src/payment/service.ts b/packages/core/types/src/payment/service.ts index a3413e7bab577..066197ddbf043 100644 --- a/packages/core/types/src/payment/service.ts +++ b/packages/core/types/src/payment/service.ts @@ -16,6 +16,7 @@ import { RefundDTO, } from "./common" import { + AuthorizePaymentSessionDTO, CreateCaptureDTO, CreatePaymentCollectionDTO, CreatePaymentSessionDTO, @@ -424,7 +425,7 @@ export interface IPaymentModuleService extends IModuleService { * provider_id: "stripe", * currency_code: "usd", * amount: 3000, - * data: {}, + * context: {}, * } * ) */ @@ -460,7 +461,7 @@ export interface IPaymentModuleService extends IModuleService { * * @param {string} id - The ID of the payment session. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise} Resolves whent the payment session is deleted successfully. + * @returns {Promise} Resolves when the payment session is deleted successfully. * * @example * await paymentModuleService.deletePaymentSession("payses_123") @@ -473,20 +474,16 @@ export interface IPaymentModuleService extends IModuleService { * Learn more about the payment flow in [this guide](https://docs.medusajs.com/experimental/payment/payment-flow/) * * @param {string} id - The payment session's ID. - * @param {Record} context - Context data to pass to the associated payment provider. + * @param {AuthorizePaymentSessionDTO} data - The attributes to authorize in a payment session. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The created payment. * * @example - * const payment = - * await paymentModuleService.authorizePaymentSession( - * "payses_123", - * {} - * ) + * await paymentModuleService.authorizePaymentSession("payses_123", { provider_token: "" }) */ authorizePaymentSession( id: string, - context: Record, + data?: AuthorizePaymentSessionDTO, sharedContext?: Context ): Promise @@ -622,35 +619,34 @@ export interface IPaymentModuleService extends IModuleService { * * Learn more about the payment flow in [this guide](https://docs.medusajs.com/experimental/payment/payment-flow/) * + * @param {string} paymentId - The ID of the payment to create the capture for. * @param {CreateCaptureDTO} data - The payment capture to be created. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The payment's details. * * @example - * const payment = await paymentModuleService.capturePayment({ - * payment_id: "pay_123", - * }) + * const payment = await paymentModuleService.capturePayment("pay_123") */ capturePayment( - data: CreateCaptureDTO, + paymentId: string, + data?: CreateCaptureDTO, sharedContext?: Context ): Promise /** * This method refunds a payment using its associated payment provider. An amount can only be refunded if it has been captured first. * + * @param {string} paymentId - The ID of the payment to create the refund for. * @param {CreateRefundDTO} data - The refund to be created. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The payment's details. * * @example - * const payment = await paymentModuleService.refundPayment({ - * payment_id: "pay_123", - * amount: 300, - * }) + * const payment = await paymentModuleService.refundPayment("pay_123", { amount: 300 }) */ refundPayment( - data: CreateRefundDTO, + paymentId: string, + data?: CreateRefundDTO, sharedContext?: Context ): Promise diff --git a/packages/core/utils/src/payment/abstract-payment-provider.ts b/packages/core/utils/src/payment/abstract-payment-provider.ts index 4b22a0fbefd1e..dadd0a4a7e32c 100644 --- a/packages/core/utils/src/payment/abstract-payment-provider.ts +++ b/packages/core/utils/src/payment/abstract-payment-provider.ts @@ -1,14 +1,13 @@ import { + AuthorizePaymentProviderSession, BigNumberInput, CreatePaymentProviderSession, IPaymentProvider, MedusaContainer, PaymentProviderError, PaymentProviderSessionResponse, - PaymentSessionStatus, ProviderWebhookPayload, UpdatePaymentProviderSession, - WebhookActionResult, } from "@medusajs/types" /** @@ -123,7 +122,7 @@ import { * * // used in other methods * async retrievePayment( - * paymentSessionData: Record + * paymentSessionData: PaymentProviderSessionResponse["data"] * ): Promise< * PaymentProviderError | * PaymentProviderSessionResponse["session_data"] @@ -216,53 +215,42 @@ export abstract class AbstractPaymentProvider> } abstract capturePayment( - paymentSessionData: Record, + paymentSessionData: PaymentProviderSessionResponse["data"], captureAmount?: BigNumberInput - ): Promise + ): Promise abstract authorizePayment( - paymentSessionData: Record, - context: Record - ): Promise< - | PaymentProviderError - | { - status: PaymentSessionStatus - data: PaymentProviderSessionResponse["data"] - } - > + data: AuthorizePaymentProviderSession + ): Promise abstract cancelPayment( - paymentSessionData: Record - ): Promise + paymentSessionData: PaymentProviderSessionResponse["data"] + ): Promise abstract initiatePayment( - context: CreatePaymentProviderSession + data: CreatePaymentProviderSession ): Promise abstract deletePayment( - paymentSessionData: Record - ): Promise - - abstract getPaymentStatus( - paymentSessionData: Record - ): Promise + paymentSessionData: PaymentProviderSessionResponse["data"] + ): Promise abstract refundPayment( - paymentSessionData: Record, - refundAmount: BigNumberInput - ): Promise + paymentSessionData: PaymentProviderSessionResponse["data"], + refundAmount?: BigNumberInput + ): Promise abstract retrievePayment( - paymentSessionData: Record - ): Promise + paymentSessionData: PaymentProviderSessionResponse["data"] + ): Promise abstract updatePayment( - context: UpdatePaymentProviderSession + data: UpdatePaymentProviderSession ): Promise abstract getWebhookActionAndData( data: ProviderWebhookPayload["payload"] - ): Promise + ): Promise } /** diff --git a/packages/core/utils/src/payment/payment-collection.ts b/packages/core/utils/src/payment/payment-collection.ts index 600445705f50d..ff85d306ca82c 100644 --- a/packages/core/utils/src/payment/payment-collection.ts +++ b/packages/core/utils/src/payment/payment-collection.ts @@ -5,23 +5,31 @@ */ export enum PaymentCollectionStatus { /** - * The payment collection isn't paid. + * The payment collection is pending. */ - NOT_PAID = "not_paid", + PENDING = "pending", /** - * The payment collection is awaiting payment. + * The payment collection is paid. */ - AWAITING = "awaiting", + PAID = "paid", + /** + * The payment collection is partially paid. + */ + PARTIALLY_PAID = "partially_paid", /** * The payment collection is authorized. */ AUTHORIZED = "authorized", /** - * Some of the payments in the payment collection are authorized. + * The payment collection is partially authorized. */ PARTIALLY_AUTHORIZED = "partially_authorized", /** - * The payment collection is canceled. + * The payment collection is refunded. + */ + REFUNDED = "refunded", + /** + * The payment collection is refunded. */ - CANCELED = "canceled", + PARTIALLY_REFUNDED = "partially_refunded", } diff --git a/packages/core/utils/src/payment/payment-session.ts b/packages/core/utils/src/payment/payment-session.ts index 65e127daa0680..13a47d31d6b9e 100644 --- a/packages/core/utils/src/payment/payment-session.ts +++ b/packages/core/utils/src/payment/payment-session.ts @@ -12,6 +12,18 @@ export enum PaymentSessionStatus { * The payment is captured. */ CAPTURED = "captured", + /** + * The payment is partially captured. + */ + PARTIALLY_CAPTURED = "partially_captured", + /** + * The payment is refunded. + */ + REFUNDED = "refunded", + /** + * The payment is refunded. + */ + PARTIALLY_REFUNDED = "partially_refunded", /** * The payment is pending. */ @@ -20,12 +32,12 @@ export enum PaymentSessionStatus { * The payment requires an action. */ REQUIRES_MORE = "requires_more", - /** - * An error occurred while processing the payment. - */ - ERROR = "error", /** * The payment is canceled. */ CANCELED = "canceled", + /** + * The payment is being processing. + */ + PROCESSING = "processing", } diff --git a/packages/modules/payment/src/models/payment-collection.ts b/packages/modules/payment/src/models/payment-collection.ts index fefa36eb74328..3a06b3f240d4b 100644 --- a/packages/modules/payment/src/models/payment-collection.ts +++ b/packages/modules/payment/src/models/payment-collection.ts @@ -95,9 +95,9 @@ export default class PaymentCollection { @Enum({ items: () => PaymentCollectionStatus, - default: PaymentCollectionStatus.NOT_PAID, + default: PaymentCollectionStatus.PENDING, }) - status: PaymentCollectionStatus = PaymentCollectionStatus.NOT_PAID + status: PaymentCollectionStatus = PaymentCollectionStatus.PENDING @ManyToMany(() => PaymentProvider) payment_providers = new Collection>(this) diff --git a/packages/modules/payment/src/providers/system.ts b/packages/modules/payment/src/providers/system.ts index 88c61e5675490..28a61cf8124c0 100644 --- a/packages/modules/payment/src/providers/system.ts +++ b/packages/modules/payment/src/providers/system.ts @@ -1,82 +1,192 @@ import { + AuthorizePaymentProviderSession, + BigNumberInput, + BigNumberValue, CreatePaymentProviderSession, + PaymentProviderContext, PaymentProviderError, PaymentProviderSessionResponse, + PaymentSessionStatus, ProviderWebhookPayload, - WebhookActionResult, + UpdatePaymentProviderSession, } from "@medusajs/types" -import { - AbstractPaymentProvider, - PaymentActions, - PaymentSessionStatus, -} from "@medusajs/utils" +import { AbstractPaymentProvider, BigNumber, MathBN } from "@medusajs/utils" + +type SystemProviderPaymentSession = { + status: PaymentSessionStatus + amount: BigNumberValue + capturedAmount: BigNumberValue + refundedAmount: BigNumberValue + currency_code: string + context: PaymentProviderContext +} export class SystemProviderService extends AbstractPaymentProvider { static identifier = "system" static PROVIDER = "system" - async getStatus(_): Promise { - return "authorized" - } - - async getPaymentData(_): Promise> { - return {} - } - async initiatePayment( - context: CreatePaymentProviderSession + data: CreatePaymentProviderSession ): Promise { - return { data: {} } + const paymentSessionData: SystemProviderPaymentSession = { + status: "captured", + amount: new BigNumber(data.amount).valueOf(), + capturedAmount: new BigNumber(data.amount).valueOf(), + refundedAmount: 0, + currency_code: data.currency_code, + context: data.context, + } + return { + status: paymentSessionData.status, + capturedAmount: paymentSessionData.capturedAmount, + refundedAmount: paymentSessionData.refundedAmount, + context: paymentSessionData.context, + data: paymentSessionData, + } } - async getPaymentStatus( - paymentSessionData: Record - ): Promise { - throw new Error("Method not implemented.") + async retrievePayment( + paymentSessionData: SystemProviderPaymentSession + ): Promise { + return { + status: paymentSessionData.status, + capturedAmount: paymentSessionData.capturedAmount, + refundedAmount: paymentSessionData.refundedAmount, + context: paymentSessionData.context, + data: paymentSessionData, + } } - async retrievePayment( - paymentSessionData: Record - ): Promise | PaymentProviderError> { - return {} + async authorizePayment({ + data, + }: AuthorizePaymentProviderSession): Promise< + PaymentProviderError | PaymentProviderSessionResponse + > { + const paymentSessionData = data as SystemProviderPaymentSession + return { + status: paymentSessionData.status, + capturedAmount: paymentSessionData.capturedAmount, + refundedAmount: paymentSessionData.refundedAmount, + context: paymentSessionData.context, + data: paymentSessionData, + } } - async authorizePayment(_): Promise< - | PaymentProviderError - | { - status: PaymentSessionStatus - data: PaymentProviderSessionResponse["data"] - } + async updatePayment({ + data, + context, + amount, + }: UpdatePaymentProviderSession): Promise< + PaymentProviderError | PaymentProviderSessionResponse > { - return { data: {}, status: PaymentSessionStatus.AUTHORIZED } + const paymentSessionData = data as SystemProviderPaymentSession + if (amount && amount < paymentSessionData.capturedAmount) { + return { + error: + "You cannot update payment amount less than the captured amount, please use refund", + code: "", + detail: "", + } + } + const toUpdate: Partial = {} + if (amount) { + toUpdate.amount = new BigNumber(amount).valueOf() + toUpdate.capturedAmount = toUpdate.amount + } + if (context) toUpdate.context = context + const paymentSessionData_ = { ...paymentSessionData, ...toUpdate } + return { + status: paymentSessionData_.status, + capturedAmount: paymentSessionData_.capturedAmount, + refundedAmount: paymentSessionData_.refundedAmount, + context: paymentSessionData_.context, + data: paymentSessionData_, + } } - async updatePayment( - _ + async deletePayment( + paymentSessionData: SystemProviderPaymentSession + ): Promise {} + + async capturePayment( + paymentSessionData: SystemProviderPaymentSession, + captureAmount?: BigNumberInput ): Promise { - return { data: {} } as PaymentProviderSessionResponse + // already captured when payment was created + return { + error: + "You cannot capture more than the authorized amount subtracted by what is already captured.", + code: "", + detail: "", + } } - async deletePayment(_): Promise> { - return {} - } + async refundPayment( + paymentSessionData: SystemProviderPaymentSession, + amount?: BigNumberInput + ): Promise { + const refundableAmount = MathBN.sub( + paymentSessionData.capturedAmount, + paymentSessionData.refundedAmount + ) + const refundAmount = amount ? new BigNumber(amount) : refundableAmount - async capturePayment(_): Promise> { - return {} - } + if ( + MathBN.eq(refundableAmount, 0) || + MathBN.gt(refundAmount, refundableAmount) + ) { + return { + error: "You cannot refund more than what is captured on the payment.", + code: "", + detail: "", + } + } + + if (MathBN.lte(refundAmount, 0)) { + return { + error: "You must refund amount more than 0.", + code: "", + detail: "", + } + } - async refundPayment(_): Promise> { - return {} + const refundedAmount = MathBN.add( + paymentSessionData.refundedAmount, + refundAmount + ) + const isFullyRefunded = MathBN.eq( + paymentSessionData.capturedAmount, + refundedAmount + ) + const status = isFullyRefunded ? "refunded" : "partially_refunded" + + return { + status, + capturedAmount: paymentSessionData.capturedAmount, + refundedAmount, + context: paymentSessionData.context, + data: { + ...paymentSessionData, + status, + refundedAmount, + }, + } } - async cancelPayment(_): Promise> { - return {} + async cancelPayment( + paymentSessionData: SystemProviderPaymentSession + ): Promise { + return { + error: "Captured payment cannot be canceled, please use refund", + code: "", + detail: "", + } } async getWebhookActionAndData( data: ProviderWebhookPayload["payload"] - ): Promise { - return { action: PaymentActions.NOT_SUPPORTED } + ): Promise { + throw new Error("Not supported") } } diff --git a/packages/modules/payment/src/services/payment-module.ts b/packages/modules/payment/src/services/payment-module.ts index d8be042e41c25..8dc79def1db1e 100644 --- a/packages/modules/payment/src/services/payment-module.ts +++ b/packages/modules/payment/src/services/payment-module.ts @@ -1,5 +1,5 @@ import { - BigNumberInput, + AuthorizePaymentSessionDTO, CaptureDTO, Context, CreateCaptureDTO, @@ -19,6 +19,7 @@ import { PaymentCollectionUpdatableFields, PaymentDTO, PaymentProviderDTO, + PaymentProviderSessionResponse, PaymentSessionDTO, ProviderWebhookPayload, RefundDTO, @@ -36,7 +37,6 @@ import { MedusaContext, MedusaError, ModulesSdkUtils, - PaymentActions, PaymentCollectionStatus, PaymentSessionStatus, promiseAll, @@ -139,20 +139,18 @@ export default class PaymentModuleService sharedContext ) - return await this.baseRepository_.serialize( + return this.baseRepository_.serialize( Array.isArray(data) ? collections : collections[0], - { - populate: true, - } + { populate: true } ) } - @InjectTransactionManager("baseRepository_") + @InjectManager("baseRepository_") async createPaymentCollections_( data: CreatePaymentCollectionDTO[], @MedusaContext() sharedContext?: Context ): Promise { - return await this.paymentCollectionService_.create(data, sharedContext) + return this.paymentCollectionService_.create(data, sharedContext) } // @ts-ignore @@ -200,11 +198,9 @@ export default class PaymentModuleService sharedContext ) - return await this.baseRepository_.serialize( + return this.baseRepository_.serialize( Array.isArray(data) ? result : result[0], - { - populate: true, - } + { populate: true } ) } @@ -213,7 +209,7 @@ export default class PaymentModuleService data: UpdatePaymentCollectionDTO[], @MedusaContext() sharedContext?: Context ): Promise { - return await this.paymentCollectionService_.update(data, sharedContext) + return this.paymentCollectionService_.update(data, sharedContext) } upsertPaymentCollections( @@ -249,9 +245,9 @@ export default class PaymentModuleService const result = (await promiseAll(operations)).flat() - return await this.baseRepository_.serialize< - PaymentCollectionDTO[] | PaymentCollectionDTO - >(Array.isArray(data) ? result : result[0]) + return this.baseRepository_.serialize( + Array.isArray(data) ? result : result[0] + ) } completePaymentCollections( @@ -282,7 +278,7 @@ export default class PaymentModuleService sharedContext ) - return await this.baseRepository_.serialize( + return this.baseRepository_.serialize( Array.isArray(paymentCollectionId) ? updated : updated[0], { populate: true } ) @@ -291,89 +287,43 @@ export default class PaymentModuleService @InjectManager("baseRepository_") async createPaymentSession( paymentCollectionId: string, - input: CreatePaymentSessionDTO, + data: CreatePaymentSessionDTO, @MedusaContext() sharedContext?: Context ): Promise { - let paymentSession: PaymentSession | undefined - let providerSessionData: Record | undefined + let paymentSession: PaymentSession + let res: PaymentProviderSessionResponse - try { - paymentSession = await this.createPaymentSession_( - paymentCollectionId, - input, - sharedContext - ) - providerSessionData = await this.paymentProviderService_.createSession( - input.provider_id, - { - context: { ...input.context, session_id: paymentSession.id }, - amount: input.amount, - currency_code: input.currency_code, - token: input.provider_token, - } - ) - paymentSession = ( - await this.paymentSessionService_.update( - { - id: paymentSession.id, - data: { ...input.data, ...providerSessionData }, - }, - sharedContext - ) - )[0] - if (input.provider_token) { - await this.authorizePaymentSession( - paymentSession!.id, - {}, - sharedContext - ) - paymentSession = await this.paymentSessionService_.retrieve( - paymentSession!.id, - {}, - sharedContext - ) - } - } catch (error) { - if (providerSessionData) { - await this.paymentProviderService_.deleteSession({ - provider_id: input.provider_id, - data: providerSessionData, - }) - } - if (paymentSession) { - await this.paymentSessionService_.delete( - paymentSession.id, - sharedContext - ) - } - - throw error - } - - return await this.baseRepository_.serialize(paymentSession, { - populate: true, - }) - } - - @InjectTransactionManager("baseRepository_") - async createPaymentSession_( - paymentCollectionId: string, - data: CreatePaymentSessionDTO, - @MedusaContext() sharedContext?: Context - ): Promise { - const paymentSession = await this.paymentSessionService_.create( + paymentSession = await this.paymentSessionService_.create( { payment_collection_id: paymentCollectionId, provider_id: data.provider_id, amount: data.amount, currency_code: data.currency_code, context: data.context, - data: data.data, }, sharedContext ) - return paymentSession + try { + res = await this.paymentProviderService_.createSession(data.provider_id, { + context: { ...data.context, session_id: paymentSession.id }, + amount: data.amount, + currency_code: data.currency_code, + token: data.provider_token, + }) + } catch (err) { + await this.paymentSessionService_.delete(paymentSession.id, sharedContext) + throw err + } + + await this.handleProviderSessionResponse_(res, sharedContext) + paymentSession = await this.paymentSessionService_.retrieve( + paymentSession.id, + {}, + sharedContext + ) + + return this.baseRepository_.serialize(paymentSession, { populate: true }) } @InjectManager("baseRepository_") @@ -381,23 +331,42 @@ export default class PaymentModuleService data: UpdatePaymentSessionDTO, @MedusaContext() sharedContext?: Context ): Promise { + if (!data.amount && !data.context && !data.provider_token) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + `The payment session update requires at least an amount, context, or provider token.` + ) + } + const session = await this.paymentSessionService_.retrieve( data.id, { select: ["id", "data", "provider_id"] }, sharedContext ) - const updated = await this.paymentSessionService_.update( - { - id: session.id, + const context = data.context && { ...data.context, session_id: session.id } + await this.handleProviderSessionResponse_( + await this.paymentProviderService_.updateSession(session.provider_id, { + data: session.data, + context, amount: data.amount, - currency_code: data.currency_code, - data: data.data, - }, + token: data.provider_token, + }), sharedContext ) - return await this.baseRepository_.serialize(updated[0], { populate: true }) + if (data.amount || data.context) { + const toUpdate: any = {} + if (data.amount) toUpdate.amount = data.amount + if (data.context) toUpdate.context = data.context + const updated = await this.paymentSessionService_.update( + { id: session.id, ...toUpdate }, + sharedContext + ) + return this.baseRepository_.serialize(updated[0], { populate: true }) + } else { + return this.retrievePaymentSession(session.id, {}, sharedContext) + } } @InjectManager("baseRepository_") @@ -411,10 +380,10 @@ export default class PaymentModuleService sharedContext ) - await this.paymentProviderService_.deleteSession({ - provider_id: session.provider_id, - data: session.data, - }) + await this.paymentProviderService_.deleteSession( + session.provider_id, + session.data + ) await this.paymentSessionService_.delete(id, sharedContext) } @@ -422,109 +391,85 @@ export default class PaymentModuleService @InjectManager("baseRepository_") async authorizePaymentSession( id: string, - context: Record, + data?: AuthorizePaymentSessionDTO, @MedusaContext() sharedContext?: Context ): Promise { + const paymentSession = await this.paymentSessionService_.retrieve( + id, + { select: ["data", "provider_id", "context"] }, + sharedContext + ) + + await this.handleProviderSessionResponse_( + await this.paymentProviderService_.authorizePayment( + paymentSession.provider_id, + { + data: paymentSession.data, + token: data?.provider_token, + } + ), + sharedContext + ) + + const payment = await this.paymentService_.retrieve( + { payment_session_id: paymentSession.id }, + {}, + sharedContext + ) + + return this.baseRepository_.serialize(payment) + } + + @InjectTransactionManager("baseRepository_") + private async authorizePaymentSession_( + id: string, + data?: PaymentProviderSessionResponse["data"], + @MedusaContext() sharedContext?: Context + ): Promise { const session = await this.paymentSessionService_.retrieve( id, { select: [ "id", - "data", - "provider_id", - "amount", "raw_amount", "currency_code", "payment_collection_id", + "provider_id", + "status", ], + relations: ["payment.id"], }, sharedContext ) - // this method needs to be idempotent - if (session.authorized_at) { - const payment = await this.paymentService_.retrieve( - { session_id: session.id }, - { relations: ["payment_collection"] }, - sharedContext - ) - return await this.baseRepository_.serialize(payment, { populate: true }) - } - - let { data, status } = await this.paymentProviderService_.authorizePayment( - { - provider_id: session.provider_id, - data: session.data, - }, - context - ) - - if ( - status !== PaymentSessionStatus.AUTHORIZED && - status !== PaymentSessionStatus.CAPTURED - ) { + if (session.status === PaymentSessionStatus.CANCELED) { throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - `Session: ${session.id} is not authorized with the provider.` + MedusaError.Types.INVALID_DATA, + `The payment session: ${session.id} has been canceled.` ) } - let payment - try { - payment = await this.authorizePaymentSession_( - session, - data, - status as PaymentSessionStatus, + if (session.payment) { + return this.paymentService_.retrieve( + session.payment.id, + {}, sharedContext ) - } catch (error) { - await this.paymentProviderService_.cancelPayment({ - provider_id: session.provider_id, - data, - }) - - throw error - } - - await this.maybeUpdatePaymentCollection_( - session.payment_collection_id, - sharedContext - ) - - return await this.retrievePayment( - payment.id, - { relations: ["payment_collection"] }, - sharedContext - ) - } - - @InjectTransactionManager("baseRepository_") - async authorizePaymentSession_( - session: PaymentSession, - data: Record, - status: PaymentSessionStatus, - @MedusaContext() sharedContext?: Context - ): Promise { - let autoCapture = false - if (status === PaymentSessionStatus.CAPTURED) { - status = PaymentSessionStatus.AUTHORIZED - autoCapture = true } await this.paymentSessionService_.update( { - id: session.id, + id, data, - status, - authorized_at: - status === PaymentSessionStatus.AUTHORIZED ? new Date() : null, + status: PaymentSessionStatus.AUTHORIZED, + authorized_at: new Date(), }, sharedContext ) - const payment = await this.paymentService_.create( + return this.paymentService_.create( { - amount: session.amount, + amount: session.raw_amount, currency_code: session.currency_code, payment_session: session.id, payment_collection_id: session.payment_collection_id, @@ -533,15 +478,6 @@ export default class PaymentModuleService }, sharedContext ) - - if (autoCapture) { - await this.capturePayment( - { payment_id: payment.id, amount: session.amount as BigNumberInput }, - sharedContext - ) - } - - return payment } @InjectManager("baseRepository_") @@ -557,7 +493,7 @@ export default class PaymentModuleService sharedContext ) - return await this.baseRepository_.serialize(session) + return this.baseRepository_.serialize(session) } @InjectManager("baseRepository_") @@ -573,7 +509,7 @@ export default class PaymentModuleService sharedContext ) - return await this.baseRepository_.serialize(sessions) + return this.baseRepository_.serialize(sessions) } @InjectManager("baseRepository_") @@ -584,42 +520,39 @@ export default class PaymentModuleService // NOTE: currently there is no update with the provider but maybe data could be updated const result = await this.paymentService_.update(data, sharedContext) - return await this.baseRepository_.serialize(result[0], { - populate: true, - }) + return this.baseRepository_.serialize(result[0], { populate: true }) } @InjectManager("baseRepository_") async capturePayment( - data: CreateCaptureDTO, - @MedusaContext() sharedContext: Context = {} + paymentId: string, + data?: CreateCaptureDTO, + @MedusaContext() sharedContext?: Context ): Promise { - const [capture, isFullyCaptured] = await this.capturePayment_( - data, + const payment = await this.paymentService_.retrieve( + paymentId, + { select: ["provider_id", "data"] }, sharedContext ) - try { - await this.capturePaymentFromProvider_(capture, sharedContext) - } catch (error) { - await super.deleteCaptures(capture.id, sharedContext) - throw error - } - - if (isFullyCaptured) { - await this.paymentService_.update( - { id: data.payment_id, captured_at: new Date() }, - sharedContext - ) + const res = await this.paymentProviderService_.capturePayment( + payment.provider_id, + payment.data!, + data?.amount + ) + res.event = { + ...res.event, + type: "capture", + detail: { + ...res.event?.detail, + captured_by: data?.captured_by, + }, } - await this.maybeUpdatePaymentCollection_( - capture.payment.payment_collection_id, - sharedContext - ) + await this.handleProviderSessionResponse_(res, sharedContext) - return await this.retrievePayment( - data.payment_id, + return this.retrievePayment( + paymentId, { relations: ["captures"] }, sharedContext ) @@ -627,21 +560,15 @@ export default class PaymentModuleService @InjectTransactionManager("baseRepository_") private async capturePayment_( - data: CreateCaptureDTO, - @MedusaContext() sharedContext: Context = {} - ): Promise<[Capture, boolean]> { + paymentId: string, + data?: PaymentProviderSessionResponse["data"], + { amount, captured_by }: CreateCaptureDTO = {}, + @MedusaContext() sharedContext?: Context + ): Promise { const payment = await this.paymentService_.retrieve( - data.payment_id, + paymentId, { - select: [ - "id", - "data", - "provider_id", - "payment_collection_id", - "amount", - "raw_amount", - "canceled_at", - ], + select: ["id", "raw_amount", "canceled_at", "captured_at"], relations: ["captures.raw_amount"], }, sharedContext @@ -661,14 +588,12 @@ export default class PaymentModuleService ) } - const capturedAmount = payment.captures.reduce((captureAmount, next) => { - return MathBN.add(captureAmount, next.raw_amount) - }, MathBN.convert(0)) - + const capturedAmount = payment.captures.reduce( + (amount, { raw_amount }) => MathBN.add(amount, raw_amount), + MathBN.convert(0) + ) const remainingToCapture = MathBN.sub(payment.raw_amount, capturedAmount) - const newCaptureAmount = data.amount - ? new BigNumber(data.amount) - : remainingToCapture + const newCaptureAmount = amount ? new BigNumber(amount) : remainingToCapture if ( MathBN.eq(remainingToCapture, 0) || @@ -676,7 +601,14 @@ export default class PaymentModuleService ) { throw new MedusaError( MedusaError.Types.INVALID_DATA, - `You cannot capture more than the authorized amount substracted by what is already captured.` + `You cannot capture more than the authorized amount subtracted by what is already captured.` + ) + } + + if (MathBN.lte(newCaptureAmount, 0)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `You must capture amount more than 0.` ) } @@ -686,58 +618,54 @@ export default class PaymentModuleService ) const isFullyCaptured = MathBN.gte(totalCaptured, payment.raw_amount) - return [ - await this.captureService_.create( - { - payment: data.payment_id, - amount: newCaptureAmount, - captured_by: data.captured_by, - }, - sharedContext - ), - isFullyCaptured, - ] - } - @InjectManager("baseRepository_") - private async capturePaymentFromProvider_( - capture: Capture, - @MedusaContext() sharedContext: Context = {} - ) { - const captureData = await this.paymentProviderService_.capturePayment( + const capture = await this.captureService_.create( { - data: capture.payment.data!, - provider_id: capture.payment.provider_id, + payment: paymentId, + amount: newCaptureAmount, + created_by: captured_by, + data, }, - capture.raw_amount + sharedContext ) - return this.captureService_.update( - { id: capture.id, data: captureData }, + await this.paymentService_.update( + { id: paymentId, captured_at: isFullyCaptured ? new Date() : null, data }, sharedContext ) + + return capture } @InjectManager("baseRepository_") async refundPayment( - data: CreateRefundDTO, - @MedusaContext() sharedContext: Context = {} + paymentId: string, + data?: CreateRefundDTO, + @MedusaContext() sharedContext?: Context ): Promise { - const refund = await this.refundPayment_(data, sharedContext) + const payment = await this.paymentService_.retrieve( + paymentId, + { select: ["provider_id", "data"] }, + sharedContext + ) - try { - await this.refundPaymentFromProvider_(refund, sharedContext) - } catch (error) { - await super.deleteRefunds(refund.id, sharedContext) - throw error + const res = await this.paymentProviderService_.refundPayment( + payment.provider_id, + payment.data!, + data?.amount + ) + res.event = { + ...res.event, + type: "refund", + detail: { + ...res.event?.detail, + refunded_by: data?.refunded_by, + }, } - await this.maybeUpdatePaymentCollection_( - refund.payment.payment_collection_id, - sharedContext - ) + await this.handleProviderSessionResponse_(res, sharedContext) - return await this.retrievePayment( - data.payment_id, + return this.retrievePayment( + paymentId, { relations: ["refunds"] }, sharedContext ) @@ -745,36 +673,31 @@ export default class PaymentModuleService @InjectTransactionManager("baseRepository_") private async refundPayment_( - data: CreateRefundDTO, - @MedusaContext() sharedContext: Context = {} + paymentId: string, + data?: PaymentProviderSessionResponse["data"], + { amount, refunded_by }: CreateRefundDTO = {}, + @MedusaContext() sharedContext?: Context ): Promise { const payment = await this.paymentService_.retrieve( - data.payment_id, - { - select: [ - "id", - "data", - "provider_id", - "payment_collection_id", - "amount", - "raw_amount", - ], - relations: ["captures.raw_amount"], - }, + paymentId, + { relations: ["captures.raw_amount", "refunds.raw_amount"] }, sharedContext ) - const capturedAmount = payment.captures.reduce((captureAmount, next) => { - return MathBN.add(captureAmount, next.raw_amount) - }, MathBN.convert(0)) - - const refundAmount = data.amount - ? new BigNumber(data.amount) - : capturedAmount + const capturedAmount = payment.captures.reduce( + (amount, { raw_amount }) => MathBN.add(amount, raw_amount), + MathBN.convert(0) + ) + const refundedAmount = payment.refunds.reduce( + (amount, { raw_amount }) => MathBN.add(amount, raw_amount), + MathBN.convert(0) + ) + const refundableAmount = MathBN.sub(capturedAmount, refundedAmount) + const refundAmount = amount ? new BigNumber(amount) : refundableAmount if ( - MathBN.eq(capturedAmount, 0) || - MathBN.lt(capturedAmount, refundAmount) + MathBN.eq(refundableAmount, 0) || + MathBN.gt(refundAmount, refundableAmount) ) { throw new MedusaError( MedusaError.Types.INVALID_DATA, @@ -782,33 +705,26 @@ export default class PaymentModuleService ) } - return this.refundService_.create( + if (MathBN.lte(refundAmount, 0)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `You must refund amount more than 0.` + ) + } + + const refund = await this.refundService_.create( { - payment: data.payment_id, + payment: paymentId, amount: refundAmount, - created_by: data.created_by, + data, + created_by: refunded_by, }, sharedContext ) - } - @InjectManager("baseRepository_") - private async refundPaymentFromProvider_( - refund: Refund, - @MedusaContext() sharedContext: Context = {} - ) { - const refundData = await this.paymentProviderService_.refundPayment( - { - data: refund.payment.data!, - provider_id: refund.payment.provider_id, - }, - refund.raw_amount - ) + await this.paymentService_.update({ id: paymentId, data }, sharedContext) - return this.refundService_.update( - { id: refund.id, data: refundData }, - sharedContext - ) + return refund } @InjectManager("baseRepository_") @@ -822,25 +738,15 @@ export default class PaymentModuleService sharedContext ) - // TODO: revisit when totals are implemented - // if (payment.captured_amount !== 0) { - // throw new MedusaError( - // MedusaError.Types.INVALID_DATA, - // `Cannot cancel a payment: ${payment.id} that has been captured.` - // ) - // } - - await this.paymentProviderService_.cancelPayment({ - data: payment.data!, - provider_id: payment.provider_id, - }) - - await this.paymentService_.update( - { id: paymentId, canceled_at: new Date() }, + await this.handleProviderSessionResponse_( + await this.paymentProviderService_.cancelPayment( + payment.provider_id, + payment.data! + ), sharedContext ) - return await this.retrievePayment(payment.id, {}, sharedContext) + return this.retrievePayment(payment.id, {}, sharedContext) } @InjectManager("baseRepository_") @@ -848,50 +754,13 @@ export default class PaymentModuleService eventData: ProviderWebhookPayload, @MedusaContext() sharedContext?: Context ): Promise { - const providerId = `pp_${eventData.provider}` - - const event = await this.paymentProviderService_.getWebhookActionAndData( - providerId, - eventData.payload + await this.handleProviderSessionResponse_( + await this.paymentProviderService_.getWebhookActionAndData( + `pp_${eventData.provider}`, + eventData.payload + ), + sharedContext ) - - if (event.action === PaymentActions.NOT_SUPPORTED) { - return - } - - switch (event.action) { - case PaymentActions.SUCCESSFUL: { - const [payment] = await this.listPayments( - { - payment_session_id: event.data.session_id, - }, - {}, - sharedContext - ) - - if (event.data.order_id && !event.data.cart_id) { - await this.authorizePaymentSession( - event.data.session_id, - {}, - sharedContext - ) - } - - if (payment && !payment.captured_at) { - await this.capturePayment( - { payment_id: payment.id, amount: event.data.amount }, - sharedContext - ) - } - break - } - case PaymentActions.AUTHORIZED: - await this.authorizePaymentSession( - event.data.session_id, - {}, - sharedContext - ) - } } @InjectManager("baseRepository_") @@ -906,12 +775,7 @@ export default class PaymentModuleService sharedContext ) - return await this.baseRepository_.serialize( - providers, - { - populate: true, - } - ) + return this.baseRepository_.serialize(providers, { populate: true }) } @InjectManager("baseRepository_") @@ -927,13 +791,82 @@ export default class PaymentModuleService ) return [ - await this.baseRepository_.serialize(providers, { - populate: true, - }), + await this.baseRepository_.serialize(providers, { populate: true }), count, ] } + @InjectManager("baseRepository_") + private async maybeUpdatePaymentSession_( + paymentSessionId: string, + data?: PaymentProviderSessionResponse["data"], + sharedContext?: Context + ) { + const paymentSession = await this.paymentSessionService_.retrieve( + paymentSessionId, + { + select: ["raw_amount", "status"], + relations: [ + "payment.captures.raw_amount", + "payment.refunds.raw_amount", + ], + }, + sharedContext + ) + + if (paymentSession.status === PaymentSessionStatus.CANCELED) return + + if ( + paymentSession.status === PaymentSessionStatus.PENDING || + paymentSession.status === PaymentSessionStatus.REQUIRES_MORE + ) { + if (data) { + await this.paymentSessionService_.update( + { id: paymentSession.id, data }, + sharedContext + ) + } + } else { + let status: PaymentSessionStatus = paymentSession.status + + if (paymentSession.payment!.canceled_at) { + status = PaymentSessionStatus.CANCELED + } else { + const capturedAmount = paymentSession.payment!.captures.reduce( + (amount, { raw_amount }) => MathBN.add(amount, raw_amount), + MathBN.convert(0) + ) + const refundedAmount = paymentSession.payment!.refunds.reduce( + (amount, { raw_amount }) => MathBN.add(amount, raw_amount), + MathBN.convert(0) + ) + + if (MathBN.gt(capturedAmount, 0)) { + if (MathBN.gt(refundedAmount, 0)) { + if (MathBN.eq(capturedAmount, refundedAmount)) { + status = PaymentSessionStatus.REFUNDED + } else { + status = PaymentSessionStatus.PARTIALLY_REFUNDED + } + } else if ( + MathBN.eq(capturedAmount, paymentSession.payment!.raw_amount) + ) { + status = PaymentSessionStatus.CAPTURED + } else { + status = PaymentSessionStatus.PARTIALLY_CAPTURED + } + } + } + + if (data || status !== paymentSession.status) { + await this.paymentSessionService_.update( + { id: paymentSession.id, data, status }, + sharedContext + ) + } + } + } + @InjectManager("baseRepository_") private async maybeUpdatePaymentCollection_( paymentCollectionId: string, @@ -942,65 +875,212 @@ export default class PaymentModuleService const paymentCollection = await this.paymentCollectionService_.retrieve( paymentCollectionId, { - select: ["amount", "raw_amount", "status"], + select: ["raw_amount", "status"], relations: [ - "payment_sessions.amount", - "payment_sessions.raw_amount", - "payments.captures.amount", "payments.captures.raw_amount", - "payments.refunds.amount", "payments.refunds.raw_amount", ], }, sharedContext ) + const payments = paymentCollection.payments.filter( + (payment) => !payment.canceled_at + ) + let status: PaymentCollectionStatus = paymentCollection.status - const paymentSessions = paymentCollection.payment_sessions - const captures = paymentCollection.payments - .map((pay) => [...pay.captures]) + const authorizedAmount = payments.reduce( + (amount, { raw_amount }) => MathBN.add(amount, raw_amount), + MathBN.convert(0) + ) + const capturedAmount = payments + .map(({ captures }) => captures.slice()) .flat() - const refunds = paymentCollection.payments - .map((pay) => [...pay.refunds]) + .reduce( + (amount, { raw_amount }) => MathBN.add(amount, raw_amount), + MathBN.convert(0) + ) + const refundedAmount = payments + .map(({ refunds }) => refunds.slice()) .flat() + .reduce( + (amount, { raw_amount }) => MathBN.add(amount, raw_amount), + MathBN.convert(0) + ) - let authorizedAmount = MathBN.convert(0) - let capturedAmount = MathBN.convert(0) - let refundedAmount = MathBN.convert(0) - - for (const ps of paymentSessions) { - if (ps.status === PaymentSessionStatus.AUTHORIZED) { - authorizedAmount = MathBN.add(authorizedAmount, ps.amount) + if (MathBN.gt(capturedAmount, 0)) { + if (MathBN.gt(refundedAmount, 0)) { + if (MathBN.eq(capturedAmount, refundedAmount)) { + status = PaymentCollectionStatus.REFUNDED + } else { + status = PaymentCollectionStatus.PARTIALLY_REFUNDED + } + } else if (MathBN.eq(capturedAmount, paymentCollection.raw_amount)) { + status = PaymentCollectionStatus.PAID + } else { + status = PaymentCollectionStatus.PARTIALLY_PAID + } + } else if (MathBN.gt(authorizedAmount, 0)) { + if (MathBN.eq(authorizedAmount, paymentCollection.raw_amount)) { + status = PaymentCollectionStatus.AUTHORIZED + } else { + status = PaymentCollectionStatus.PARTIALLY_AUTHORIZED } } - for (const capture of captures) { - capturedAmount = MathBN.add(capturedAmount, capture.amount) + if (status !== paymentCollection.status) { + await this.paymentCollectionService_.update( + { + id: paymentCollectionId, + status, + authorized_amount: authorizedAmount, + captured_amount: capturedAmount, + refunded_amount: refundedAmount, + }, + sharedContext + ) } + } - for (const refund of refunds) { - refundedAmount = MathBN.add(refundedAmount, refund.amount) - } + @InjectManager("baseRepository_") + private async handleProviderSessionResponse_( + { + status, + capturedAmount, + refundedAmount, + data, + context, + event, + }: PaymentProviderSessionResponse, + @MedusaContext() sharedContext?: Context + ) { + const session_id = context.session_id! - let status = - paymentSessions.length === 0 - ? PaymentCollectionStatus.NOT_PAID - : PaymentCollectionStatus.AWAITING + switch (status) { + case "authorized": { + const payment = await this.authorizePaymentSession_( + session_id, + data, + sharedContext + ) + await this.maybeUpdatePaymentCollection_( + payment.payment_collection_id, + sharedContext + ) + break + } - if (MathBN.gt(authorizedAmount, 0)) { - status = MathBN.gte(authorizedAmount, paymentCollection.amount) - ? PaymentCollectionStatus.AUTHORIZED - : PaymentCollectionStatus.PARTIALLY_AUTHORIZED - } + case "captured": + case "partially_captured": + case "refunded": + case "partially_refunded": { + const session = await this.paymentSessionService_.retrieve( + session_id, + { + select: ["payment_collection_id"], + relations: [ + "payment.id", + "payment.raw_amount", + "payment.captures.raw_amount", + "payment.refunds.raw_amount", + ], + }, + sharedContext + ) - await this.paymentCollectionService_.update( - { - id: paymentCollectionId, - status, - authorized_amount: authorizedAmount, - captured_amount: capturedAmount, - refunded_amount: refundedAmount, - }, - sharedContext - ) + let payment = session.payment + + if (!payment) { + const { id } = await this.authorizePaymentSession_( + session_id, + data, + sharedContext + ) + payment = await this.paymentService_.retrieve( + id, + { + select: ["id", "raw_amount"], + relations: ["captures.raw_amount", "refunds.raw_amount"], + }, + sharedContext + ) + } + + const _capturedAmount = payment.captures.reduce( + (amount, { raw_amount }) => MathBN.add(amount, raw_amount), + MathBN.convert(0) + ) + const _refundedAmount = payment.refunds.reduce( + (amount, { raw_amount }) => MathBN.add(amount, raw_amount), + MathBN.convert(0) + ) + + if (MathBN.gt(capturedAmount, _capturedAmount)) { + await this.capturePayment_( + payment.id, + data, + { + amount: MathBN.sub(capturedAmount, _capturedAmount), + captured_by: event?.detail?.captured_by, + }, + sharedContext + ) + } + if (MathBN.gt(refundedAmount, _refundedAmount)) { + await this.refundPayment_( + payment.id, + data, + { + amount: MathBN.sub(refundedAmount, _refundedAmount), + refunded_by: event?.detail?.refunded_by, + }, + sharedContext + ) + } + await this.maybeUpdatePaymentSession_(session_id, data, sharedContext) + await this.maybeUpdatePaymentCollection_( + session.payment_collection_id, + sharedContext + ) + break + } + + case "pending": + case "requires_more": + case "processing": { + await this.paymentSessionService_.update( + { id: session_id, data, status }, + sharedContext + ) + break + } + case "canceled": { + await this.paymentSessionService_.update( + { id: session_id, data, status }, + sharedContext + ) + const session = await this.paymentSessionService_.retrieve( + session_id, + { select: ["payment_collection_id"], relations: ["payment.id"] }, + sharedContext + ) + if (session.payment) { + await this.paymentService_.update( + { id: session.payment.id, canceled_at: new Date() }, + sharedContext + ) + await this.maybeUpdatePaymentCollection_( + session.payment_collection_id, + sharedContext + ) + } + break + } + default: { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Received invalid payment status: ${status}` + ) + } + } } } diff --git a/packages/modules/payment/src/services/payment-provider.ts b/packages/modules/payment/src/services/payment-provider.ts index 1e465ec727c96..daafbe1744eb3 100644 --- a/packages/modules/payment/src/services/payment-provider.ts +++ b/packages/modules/payment/src/services/payment-provider.ts @@ -1,4 +1,5 @@ import { + AuthorizePaymentProviderSession, BigNumberInput, Context, CreatePaymentProviderDTO, @@ -8,15 +9,11 @@ import { FindConfig, InternalModuleDeclaration, IPaymentProvider, - PaymentProviderAuthorizeResponse, - PaymentProviderDataInput, PaymentProviderDTO, PaymentProviderError, PaymentProviderSessionResponse, - PaymentSessionStatus, ProviderWebhookPayload, UpdatePaymentProviderSession, - WebhookActionResult, } from "@medusajs/types" import { InjectManager, @@ -100,108 +97,122 @@ export default class PaymentProviderService { } } - async createSession( + async retrieveSession( providerId: string, - sessionInput: CreatePaymentProviderSession - ): Promise { + paymentSessionData: PaymentProviderSessionResponse["data"] + ): Promise { const provider = this.retrieveProvider(providerId) + const res = await provider.retrievePayment(paymentSessionData) + + if (isPaymentProviderError(res)) { + this.throwPaymentProviderError(res) + } + + return res as PaymentProviderSessionResponse + } - const paymentResponse = await provider.initiatePayment(sessionInput) + async createSession( + providerId: string, + data: CreatePaymentProviderSession + ): Promise { + const provider = this.retrieveProvider(providerId) + const res = await provider.initiatePayment(data) - if (isPaymentProviderError(paymentResponse)) { - this.throwPaymentProviderError(paymentResponse) + if (isPaymentProviderError(res)) { + this.throwPaymentProviderError(res) } - return (paymentResponse as PaymentProviderSessionResponse).data + return res as PaymentProviderSessionResponse } async updateSession( providerId: string, - sessionInput: UpdatePaymentProviderSession - ): Promise | undefined> { + data: UpdatePaymentProviderSession + ): Promise { const provider = this.retrieveProvider(providerId) + const res = await provider.updatePayment(data) - const paymentResponse = await provider.updatePayment(sessionInput) - - if (isPaymentProviderError(paymentResponse)) { - this.throwPaymentProviderError(paymentResponse) + if (isPaymentProviderError(res)) { + this.throwPaymentProviderError(res) } - return (paymentResponse as PaymentProviderSessionResponse)?.data + return res as PaymentProviderSessionResponse } - async deleteSession(input: PaymentProviderDataInput): Promise { - const provider = this.retrieveProvider(input.provider_id) + async deleteSession( + providerId: string, + paymentSessionData: PaymentProviderSessionResponse["data"] + ): Promise { + const provider = this.retrieveProvider(providerId) + const res = await provider.deletePayment(paymentSessionData) - const error = await provider.deletePayment(input.data) - if (isPaymentProviderError(error)) { - this.throwPaymentProviderError(error) + if (isPaymentProviderError(res)) { + this.throwPaymentProviderError(res) } } async authorizePayment( - input: PaymentProviderDataInput, - context: Record - ): Promise<{ data: Record; status: PaymentSessionStatus }> { - const provider = this.retrieveProvider(input.provider_id) + providerId: string, + data: AuthorizePaymentProviderSession + ): Promise { + const provider = this.retrieveProvider(providerId) + const res = await provider.authorizePayment(data) - const res = await provider.authorizePayment(input.data, context) if (isPaymentProviderError(res)) { this.throwPaymentProviderError(res) } - const { data, status } = res as PaymentProviderAuthorizeResponse - return { data, status } - } - - async getStatus( - input: PaymentProviderDataInput - ): Promise { - const provider = this.retrieveProvider(input.provider_id) - return await provider.getPaymentStatus(input.data) + return res as PaymentProviderSessionResponse } async capturePayment( - input: PaymentProviderDataInput, - amount?: BigNumberInput - ): Promise> { - const provider = this.retrieveProvider(input.provider_id) + providerId: string, + paymentSessionData: PaymentProviderSessionResponse["data"], + captureAmount?: BigNumberInput + ): Promise { + const provider = this.retrieveProvider(providerId) + const res = await provider.capturePayment(paymentSessionData, captureAmount) - const res = await provider.capturePayment(input.data, amount) if (isPaymentProviderError(res)) { this.throwPaymentProviderError(res) } - return res as Record + return res as PaymentProviderSessionResponse } - async cancelPayment(input: PaymentProviderDataInput): Promise { - const provider = this.retrieveProvider(input.provider_id) + async cancelPayment( + providerId: string, + paymentSessionData: PaymentProviderSessionResponse["data"] + ): Promise { + const provider = this.retrieveProvider(providerId) + const res = await provider.cancelPayment(paymentSessionData) - const error = await provider.cancelPayment(input.data) - if (isPaymentProviderError(error)) { - this.throwPaymentProviderError(error) + if (isPaymentProviderError(res)) { + this.throwPaymentProviderError(res) } + + return res as PaymentProviderSessionResponse } async refundPayment( - input: PaymentProviderDataInput, - amount: BigNumberInput - ): Promise> { - const provider = this.retrieveProvider(input.provider_id) + providerId: string, + paymentSessionData: PaymentProviderSessionResponse["data"], + refundAmount?: BigNumberInput + ): Promise { + const provider = this.retrieveProvider(providerId) + const res = await provider.refundPayment(paymentSessionData, refundAmount) - const res = await provider.refundPayment(input.data, amount) if (isPaymentProviderError(res)) { this.throwPaymentProviderError(res) } - return res as Record + return res as PaymentProviderSessionResponse } async getWebhookActionAndData( providerId: string, data: ProviderWebhookPayload["payload"] - ): Promise { + ): Promise { const provider = this.retrieveProvider(providerId) return await provider.getWebhookActionAndData(data) 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 7094fa518f206..04d0b6e9a1429 100644 --- a/packages/modules/providers/payment-stripe/src/core/stripe-base.ts +++ b/packages/modules/providers/payment-stripe/src/core/stripe-base.ts @@ -3,29 +3,22 @@ import { EOL } from "os" import Stripe from "stripe" import { + AuthorizePaymentProviderSession, BigNumberInput, CreatePaymentProviderSession, MedusaContainer, PaymentProviderError, PaymentProviderSessionResponse, + PaymentSessionStatus, ProviderWebhookPayload, UpdatePaymentProviderSession, - WebhookActionResult, } from "@medusajs/types" import { AbstractPaymentProvider, - MedusaError, - PaymentActions, - PaymentSessionStatus, isDefined, isPaymentProviderError, } from "@medusajs/utils" -import { - ErrorCodes, - ErrorIntentStatus, - PaymentIntentOptions, - StripeOptions, -} from "../types" +import { ErrorCodes, PaymentIntentOptions, StripeOptions } from "../types" import { getAmountFromSmallestUnit, getSmallestUnit, @@ -83,39 +76,21 @@ abstract class StripeBase extends AbstractPaymentProvider { return options } - async getPaymentStatus( - paymentSessionData: Record - ): Promise { - const id = paymentSessionData.id as string - const paymentIntent = await this.stripe_.paymentIntents.retrieve(id) - - switch (paymentIntent.status) { - case "requires_payment_method": - case "requires_confirmation": - case "processing": - return PaymentSessionStatus.PENDING - case "requires_action": - return PaymentSessionStatus.REQUIRES_MORE - case "canceled": - return PaymentSessionStatus.CANCELED - case "requires_capture": - return PaymentSessionStatus.AUTHORIZED - case "succeeded": - return PaymentSessionStatus.CAPTURED - default: - return PaymentSessionStatus.PENDING - } - } - async initiatePayment( - input: CreatePaymentProviderSession + data: CreatePaymentProviderSession ): Promise { const intentRequestData = this.getPaymentIntentOptions() - const { email, extra, session_id, cart_id, order_id, customer } = - input.context - const { currency_code, amount, token } = input - - const description = (extra?.payment_description ?? + const { + email, + session_id, + cart_id, + order_id, + customer, + payment_description, + } = data.context + const { currency_code, amount, token } = data + + const description = (payment_description ?? this.options_?.paymentDescription) as string const intentRequest: Stripe.PaymentIntentCreateParams = { @@ -125,7 +100,7 @@ abstract class StripeBase extends AbstractPaymentProvider { payment_method: token, confirm: !!token, metadata: { - session_id: session_id!, + session_id: session_id as string | null, cart_id: cart_id as string | null, order_id: order_id as string | null, }, @@ -143,7 +118,7 @@ abstract class StripeBase extends AbstractPaymentProvider { if (customer?.metadata?.stripe_id) { intentRequest.customer = customer.metadata.stripe_id as string - } else { + } else if (this.options_.createCustomer) { let stripeCustomer try { stripeCustomer = await this.stripe_.customers.create({ @@ -159,56 +134,61 @@ abstract class StripeBase extends AbstractPaymentProvider { intentRequest.customer = stripeCustomer.id } - let sessionData try { - sessionData = (await this.stripe_.paymentIntents.create( - intentRequest - )) as unknown as Record + const intent = await this.stripe_.paymentIntents.create(intentRequest) + return { + ...this.buildResponse(intent), + data: intent as unknown as Record, + context: intent.metadata, + } } catch (e) { return this.buildError( - "An error occurred in InitiatePayment during the creation of the stripe payment intent", + "An error occurred in initiatePayment during the creation of the stripe payment intent", e ) } - - return { - data: sessionData, - // TODO: REVISIT - // update_requests: customer?.metadata?.stripe_id - // ? undefined - // : { - // customer_metadata: { - // stripe_id: intentRequest.customer, - // }, - // }, - } } async authorizePayment( - paymentSessionData: Record, - context: Record - ): Promise< - | PaymentProviderError - | { - status: PaymentSessionStatus - data: PaymentProviderSessionResponse["data"] + data: AuthorizePaymentProviderSession + ): Promise { + const { id } = data.data as unknown as Stripe.PaymentIntent + try { + const intent = await this.stripe_.paymentIntents.confirm(id, { + payment_method: data.token, + }) + return { + ...this.buildResponse(intent), + data: intent as unknown as Record, + context: intent.metadata, } - > { - const status = await this.getPaymentStatus(paymentSessionData) - return { data: paymentSessionData, status } + } catch (e) { + return this.buildError( + "An error occurred in authorizePayment during the confirm of the stripe payment intent", + e + ) + } } async cancelPayment( - paymentSessionData: Record - ): Promise { + paymentSessionData: PaymentProviderSessionResponse["data"] + ): Promise { + const { id } = paymentSessionData as unknown as Stripe.PaymentIntent try { - const id = paymentSessionData.id as string - return (await this.stripe_.paymentIntents.cancel( - id - )) as unknown as PaymentProviderSessionResponse["data"] + const intent = await this.stripe_.paymentIntents.cancel(id) + return { + ...this.buildResponse(intent), + data: intent as unknown as Record, + context: intent.metadata, + } } catch (error) { - if (error.payment_intent?.status === ErrorIntentStatus.CANCELED) { - return error.payment_intent + const intent = error.payment_intent as Stripe.PaymentIntent | undefined + if (intent?.status === "canceled") { + return { + ...this.buildResponse(intent), + data: intent as unknown as Record, + context: intent.metadata, + } } return this.buildError("An error occurred in cancelPayment", error) @@ -216,22 +196,32 @@ abstract class StripeBase extends AbstractPaymentProvider { } async capturePayment( - paymentSessionData: Record, - captureAmount?: number - ): Promise { - const id = paymentSessionData.id as string + paymentSessionData: PaymentProviderSessionResponse["data"], + captureAmount?: BigNumberInput + ): Promise { + const { id, currency } = + paymentSessionData as unknown as Stripe.PaymentIntent try { - const { currency } = paymentSessionData const intent = await this.stripe_.paymentIntents.capture(id, { - amount_to_capture: - captureAmount && getSmallestUnit(captureAmount, currency as string), - final_capture: !captureAmount, + amount_to_capture: captureAmount + ? getSmallestUnit(captureAmount, currency) + : undefined, + final_capture: false, }) - return intent as unknown as PaymentProviderSessionResponse["data"] + return { + ...this.buildResponse(intent), + data: intent as unknown as Record, + context: intent.metadata, + } } catch (error) { if (error.code === ErrorCodes.PAYMENT_INTENT_UNEXPECTED_STATE) { - if (error.payment_intent?.status === ErrorIntentStatus.SUCCEEDED) { - return error.payment_intent + const intent = error.payment_intent as Stripe.PaymentIntent | undefined + if (intent?.status === "succeeded") { + return { + ...this.buildResponse(intent), + data: intent as unknown as Record, + context: intent.metadata, + } } } @@ -240,144 +230,89 @@ abstract class StripeBase extends AbstractPaymentProvider { } async deletePayment( - paymentSessionData: Record - ): Promise { - return await this.cancelPayment(paymentSessionData) + paymentSessionData: PaymentProviderSessionResponse["data"] + ): Promise { + const res = await this.cancelPayment(paymentSessionData) + if (isPaymentProviderError(res)) return res } async refundPayment( - paymentSessionData: Record, - refundAmount: BigNumberInput - ): Promise { - const id = paymentSessionData.id as string - + paymentSessionData: PaymentProviderSessionResponse["data"], + refundAmount?: BigNumberInput + ): Promise { + const { id, currency } = + paymentSessionData as unknown as Stripe.PaymentIntent try { - const { currency } = paymentSessionData await this.stripe_.refunds.create({ - amount: getSmallestUnit(refundAmount, currency as string), - payment_intent: id as string, + payment_intent: id, + amount: refundAmount + ? getSmallestUnit(refundAmount, currency) + : undefined, }) + const intent = await this.stripe_.paymentIntents.retrieve(id) + return { + ...this.buildResponse(intent), + data: intent as unknown as Record, + context: intent.metadata, + } } catch (e) { return this.buildError("An error occurred in refundPayment", e) } - - return paymentSessionData } async retrievePayment( - paymentSessionData: Record - ): Promise { + paymentSessionData: PaymentProviderSessionResponse["data"] + ): Promise { + const { id } = paymentSessionData as unknown as Stripe.PaymentIntent try { - const id = paymentSessionData.id as string const intent = await this.stripe_.paymentIntents.retrieve(id) - - intent.amount = getAmountFromSmallestUnit(intent.amount, intent.currency) - - return intent as unknown as PaymentProviderSessionResponse["data"] + return { + ...this.buildResponse(intent), + data: intent as unknown as Record, + context: intent.metadata, + } } catch (e) { return this.buildError("An error occurred in retrievePayment", e) } } async updatePayment( - input: UpdatePaymentProviderSession + data: UpdatePaymentProviderSession ): Promise { - const { context, data, currency_code, amount } = input - - const amountNumeric = getSmallestUnit(amount, currency_code) - - const stripeId = context.customer?.metadata?.stripe_id - - if (stripeId !== data.customer) { - const result = await this.initiatePayment(input) - if (isPaymentProviderError(result)) { - return this.buildError( - "An error occurred in updatePayment during the initiate of the new payment for the new customer", - result - ) - } - - return result - } else { - if (amount && data.amount === amountNumeric) { - return { data } - } - - try { - const id = data.id as string - const sessionData = (await this.stripe_.paymentIntents.update(id, { - amount: amountNumeric, - })) as unknown as PaymentProviderSessionResponse["data"] - - return { data: sessionData } - } catch (e) { - return this.buildError("An error occurred in updatePayment", e) - } - } - } - - async updatePaymentData(sessionId: string, data: Record) { + const { id, currency } = data.data as unknown as Stripe.PaymentIntent + const { context, amount, token } = data try { - // Prevent from updating the amount from here as it should go through - // the updatePayment method to perform the correct logic - if (data.amount) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Cannot update amount, use updatePayment instead" - ) + const toUpdate: Stripe.PaymentIntentUpdateParams = {} + if (context) { + toUpdate.metadata = { + session_id: context.session_id as string | null, + cart_id: context.cart_id as string | null, + order_id: context.order_id as string | null, + } + } + if (amount) toUpdate.amount = getSmallestUnit(amount, currency) + if (token) toUpdate.payment_method = token + const intent = await this.stripe_.paymentIntents.update(id, toUpdate) + return { + ...this.buildResponse(intent), + data: intent as unknown as Record, + context: intent.metadata, } - - return (await this.stripe_.paymentIntents.update(sessionId, { - ...data, - })) as unknown as PaymentProviderSessionResponse["data"] } catch (e) { - return this.buildError("An error occurred in updatePaymentData", e) + return this.buildError("An error occurred in updatePayment", e) } } async getWebhookActionAndData( - webhookData: ProviderWebhookPayload["payload"] - ): Promise { - const event = this.constructWebhookEvent(webhookData) + data: ProviderWebhookPayload["payload"] + ): Promise { + const event = this.constructWebhookEvent(data) const intent = event.data.object as Stripe.PaymentIntent - const { currency } = intent - switch (event.type) { - case "payment_intent.amount_capturable_updated": - return { - action: PaymentActions.AUTHORIZED, - data: { - session_id: intent.metadata.session_id, - cart_id: intent.metadata.cart_id, - order_id: intent.metadata.order_id, - amount: getAmountFromSmallestUnit( - intent.amount_capturable, - currency - ), // NOTE: revisit when implementing multicapture - }, - } - case "payment_intent.succeeded": - return { - action: PaymentActions.SUCCESSFUL, - data: { - session_id: intent.metadata.session_id, - cart_id: intent.metadata.cart_id, - order_id: intent.metadata.order_id, - amount: getAmountFromSmallestUnit(intent.amount_received, currency), - }, - } - case "payment_intent.payment_failed": - return { - action: PaymentActions.FAILED, - data: { - session_id: intent.metadata.session_id, - cart_id: intent.metadata.cart_id, - order_id: intent.metadata.order_id, - amount: getAmountFromSmallestUnit(intent.amount, currency), - }, - } - default: - return { action: PaymentActions.NOT_SUPPORTED } + return { + ...this.buildResponse(intent), + data: intent as unknown as Record, + context: intent.metadata, } } @@ -396,6 +331,7 @@ abstract class StripeBase extends AbstractPaymentProvider { this.config.webhookSecret ) } + protected buildError( message: string, error: Stripe.StripeRawError | PaymentProviderError | Error @@ -410,6 +346,76 @@ abstract class StripeBase extends AbstractPaymentProvider { : error.message ?? "", } } + + protected buildResponse( + intent: Stripe.PaymentIntent + ): Pick< + PaymentProviderSessionResponse, + "status" | "capturedAmount" | "refundedAmount" + > { + let status: PaymentSessionStatus + + switch (intent.status) { + case "requires_action": { + status = "requires_more" + break + } + case "processing": { + status = "processing" + break + } + case "requires_capture": { + status = "authorized" + break + } + case "succeeded": { + status = "captured" + break + } + case "canceled": { + status = "canceled" + break + } + default: + status = "pending" + } + + let capturedAmount = 0 + let refundedAmount = 0 + + if (status === "authorized" || status === "captured") { + const charge = intent.latest_charge as Stripe.Charge + const authorizedAmount = getAmountFromSmallestUnit( + charge.amount, + charge.currency + ) + capturedAmount = getAmountFromSmallestUnit( + charge.amount_captured, + charge.currency + ) + refundedAmount = getAmountFromSmallestUnit( + charge.amount_refunded, + charge.currency + ) + if (capturedAmount > 0) { + if (refundedAmount > 0) { + if (capturedAmount === refundedAmount) { + status = "refunded" + } else { + status = "partially_refunded" + } + } else if (capturedAmount < authorizedAmount) { + status = "partially_captured" + } + } + } + + return { + status, + capturedAmount, + refundedAmount, + } + } } export default StripeBase diff --git a/packages/modules/providers/payment-stripe/src/types/index.ts b/packages/modules/providers/payment-stripe/src/types/index.ts index 8211e2b91e526..edcd6330034ba 100644 --- a/packages/modules/providers/payment-stripe/src/types/index.ts +++ b/packages/modules/providers/payment-stripe/src/types/index.ts @@ -23,6 +23,10 @@ export interface StripeOptions { * Set a default description on the intent if the context does not provide one */ paymentDescription?: string + /** + * Create a stripe customer if not found + */ + createCustomer?: boolean } export interface PaymentIntentOptions {