feat: Add support for managing account holder in payment module (#11015)

This commit is contained in:
Stevche Radevski authored and GitHub committed 2025-01-28 08:55:15 +01:00
1 parent a37a9c8023
commit 59cbc0ec77
27 files changed
+1263 -738

No files matched your search

@@ -118,7 +118,6 @@ export const completeCartWorkflow = createWorkflow(
// We choose the first payment session, as there will only be one active payment session // We choose the first payment session, as there will only be one active payment session
// This might change in the future. // This might change in the future.
id: paymentSessions[0].id, id: paymentSessions[0].id,
context: { cart_id: cart.id },
}) })
const { variants, sales_channel_id } = transform({ cart }, (data) => { const { variants, sales_channel_id } = transform({ cart }, (data) => {
@@ -96,9 +96,7 @@ export const markPaymentCollectionAsPaidId = "mark-payment-collection-as-paid"
*/ */
export const markPaymentCollectionAsPaid = createWorkflow( export const markPaymentCollectionAsPaid = createWorkflow(
markPaymentCollectionAsPaidId, markPaymentCollectionAsPaidId,
( (input: WorkflowData<MarkPaymentCollectionAsPaidInput>) => {
input: WorkflowData<MarkPaymentCollectionAsPaidInput>
) => {
const paymentCollection = useRemoteQueryStep({ const paymentCollection = useRemoteQueryStep({
entry_point: "payment_collection", entry_point: "payment_collection",
fields: ["id", "status", "amount"], fields: ["id", "status", "amount"],
@@ -120,7 +118,6 @@ export const markPaymentCollectionAsPaid = createWorkflow(
const payment = authorizePaymentSessionStep({ const payment = authorizePaymentSessionStep({
id: paymentSession.id, id: paymentSession.id,
context: { order_id: input.order_id },
}) })
capturePaymentWorkflow.runAsStep({ capturePaymentWorkflow.runAsStep({
@@ -0,0 +1,29 @@
import {
IPaymentModuleService,
CreateAccountHolderDTO,
} from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
export const createPaymentAccountHolderStepId = "create-payment-account-holder"
/**
* This step creates the account holder in the payment provider.
*/
export const createPaymentAccountHolderStep = createStep(
createPaymentAccountHolderStepId,
async (data: CreateAccountHolderDTO, { container }) => {
const service = container.resolve<IPaymentModuleService>(Modules.PAYMENT)
const accountHolder = await service.createAccountHolder(data)
return new StepResponse(accountHolder, accountHolder)
},
async (createdAccountHolder, { container }) => {
if (!createdAccountHolder) {
return
}
const service = container.resolve<IPaymentModuleService>(Modules.PAYMENT)
await service.deleteAccountHolder(createdAccountHolder.id)
}
)
@@ -5,3 +5,4 @@ export * from "./delete-refund-reasons"
export * from "./update-payment-collection" export * from "./update-payment-collection"
export * from "./update-refund-reasons" export * from "./update-refund-reasons"
export * from "./validate-deleted-payment-sessions" export * from "./validate-deleted-payment-sessions"
export * from "./create-payment-account-holder"
@@ -1,5 +1,6 @@
import { import {
PaymentProviderContext, AccountHolderDTO,
CustomerDTO,
PaymentSessionDTO, PaymentSessionDTO,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
@@ -8,10 +9,15 @@ import {
createWorkflow, createWorkflow,
parallelize, parallelize,
transform, transform,
when,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { useRemoteQueryStep } from "../../common" import { createRemoteLinkStep, useRemoteQueryStep } from "../../common"
import { createPaymentSessionStep } from "../steps" import {
createPaymentSessionStep,
createPaymentAccountHolderStep,
} from "../steps"
import { deletePaymentSessionsWorkflow } from "./delete-payment-sessions" import { deletePaymentSessionsWorkflow } from "./delete-payment-sessions"
import { isPresent, Modules } from "@medusajs/framework/utils"
/** /**
* The data to create payment sessions. * The data to create payment sessions.
@@ -26,15 +32,21 @@ export interface CreatePaymentSessionsWorkflowInput {
* This provider is used to later process the payment sessions and their payments. * This provider is used to later process the payment sessions and their payments.
*/ */
provider_id: string provider_id: string
/**
* The ID of the customer that the payment session should be associated with.
*/
customer_id?: string
/** /**
* Custom data relevant for the payment provider to process the payment session. * Custom data relevant for the payment provider to process the payment session.
* Learn more in [this documentation](https://docs.medusajs.com/resources/commerce-modules/payment/payment-session#data-property). * Learn more in [this documentation](https://docs.medusajs.com/resources/commerce-modules/payment/payment-session#data-property).
*/ */
data?: Record<string, unknown> data?: Record<string, unknown>
/** /**
* Additional context that's useful for the payment provider to process the payment session. * Additional context that's useful for the payment provider to process the payment session.
* Currently all of the context is calculated within the workflow.
*/ */
context?: PaymentProviderContext context?: Record<string, unknown>
} }
export const createPaymentSessionsWorkflowId = "create-payment-sessions" export const createPaymentSessionsWorkflowId = "create-payment-sessions"
@@ -68,16 +80,89 @@ export const createPaymentSessionsWorkflow = createWorkflow(
fields: ["id", "amount", "currency_code", "payment_sessions.*"], fields: ["id", "amount", "currency_code", "payment_sessions.*"],
variables: { id: input.payment_collection_id }, variables: { id: input.payment_collection_id },
list: false, list: false,
}).config({ name: "get-payment-collection" })
const { paymentCustomer, accountHolder } = when(
"customer-id-exists",
{ input },
(data) => {
return !!data.input.customer_id
}
).then(() => {
const customer: CustomerDTO & { account_holder: AccountHolderDTO } =
useRemoteQueryStep({
entry_point: "customer",
fields: [
"id",
"email",
"company_name",
"first_name",
"last_name",
"phone",
"addresses.*",
"account_holder.*",
"metadata",
],
variables: { id: input.customer_id },
list: false,
}).config({ name: "get-customer" })
const paymentCustomer = transform({ customer }, (data) => {
return {
...data.customer,
billing_address:
data.customer.addresses?.find((a) => a.is_default_billing) ??
data.customer.addresses?.[0],
}
})
const accountHolderInput = {
provider_id: input.provider_id,
context: {
// The module is idempotent, so if there already is a linked account holder, the module will simply return it back.
account_holder: customer.account_holder,
customer: paymentCustomer,
},
}
const accountHolder = createPaymentAccountHolderStep(accountHolderInput)
return { paymentCustomer, accountHolder }
})
when(
"account-holder-created",
{ paymentCustomer, accountHolder },
(data) => {
return (
!isPresent(data.paymentCustomer?.account_holder) &&
isPresent(data.accountHolder)
)
}
).then(() => {
createRemoteLinkStep([
{
[Modules.CUSTOMER]: {
customer_id: paymentCustomer.id,
},
[Modules.PAYMENT]: {
account_holder_id: accountHolder.id,
},
},
])
}) })
const paymentSessionInput = transform( const paymentSessionInput = transform(
{ paymentCollection, input }, { paymentCollection, paymentCustomer, accountHolder, input },
(data) => { (data) => {
return { return {
payment_collection_id: data.input.payment_collection_id, payment_collection_id: data.input.payment_collection_id,
provider_id: data.input.provider_id, provider_id: data.input.provider_id,
data: data.input.data, data: data.input.data,
context: data.input.context, context: {
...data.input.context,
customer: data.paymentCustomer,
account_holder: data.accountHolder,
},
amount: data.paymentCollection.amount, amount: data.paymentCollection.amount,
currency_code: data.paymentCollection.currency_code, currency_code: data.paymentCollection.currency_code,
} }
@@ -23,7 +23,7 @@ export type AuthorizePaymentSessionStepInput = {
* The context to authorize the payment session with. * The context to authorize the payment session with.
* This context is passed to the payment provider associated with the payment session. * This context is passed to the payment provider associated with the payment session.
*/ */
context: Record<string, unknown> context?: Record<string, unknown>
} }
export const authorizePaymentSessionStepId = "authorize-payment-session-step" export const authorizePaymentSessionStepId = "authorize-payment-session-step"
@@ -8,11 +8,6 @@ export interface StoreInitializePaymentSession {
* for. * for.
*/ */
provider_id: string provider_id: string
/**
* The payment's context, such as the customer or address details. if the customer is logged-in,
* the customer id is set in the context under a `customer.id` property.
*/
context?: Record<string, unknown>
/** /**
* Any data necessary for the payment provider to process the payment. * Any data necessary for the payment provider to process the payment.
* *
+42
View File
@@ -662,3 +662,45 @@ export interface PaymentMethodDTO {
*/ */
provider_id: string provider_id: string
} }
export interface AccountHolderDTO {
/**
* The ID of the account holder.
*/
id: string
/**
* The ID of the associated payment provider.
*/
provider_id: string
/**
* The external ID of the account holder in the payment provider system.
*/
external_id: string
/**
* The email of the account holder.
*/
email: string | null
/**
* The data of the account holder, as returned by the payment provider.
*/
data: Record<string, unknown>
/**
* When the account holder was created.
*/
created_at?: string | Date | null
/**
* When the account holder was updated.
*/
updated_at?: string | Date | null
/**
* Holds custom data in key-value pairs.
*/
metadata?: Record<string, unknown> | null
}
+21 -1
View File
@@ -1,6 +1,6 @@
import { BigNumberInput } from "../totals" import { BigNumberInput } from "../totals"
import { PaymentCollectionStatus } from "./common" import { PaymentCollectionStatus } from "./common"
import { PaymentProviderContext } from "./provider" import { PaymentCustomerDTO, PaymentProviderContext } from "./provider"
/** /**
* The payment collection to be created. * The payment collection to be created.
@@ -255,6 +255,26 @@ export interface CreatePaymentProviderDTO {
is_enabled?: boolean is_enabled?: boolean
} }
/**
* The payment session to be created.
*/
export interface CreateAccountHolderDTO {
/**
* The provider's ID.
*/
provider_id: string
/**
* Necessary context data for the associated payment provider.
*/
context: PaymentProviderContext & {
/**
* The customer information from Medusa.
*/
customer: PaymentCustomerDTO
}
}
/** /**
* The details of the webhook event payload. * The details of the webhook event payload.
*/ */
+119 -131
View File
@@ -1,7 +1,6 @@
import { AddressDTO } from "../address" import { AddressDTO } from "../address"
import { CustomerDTO } from "../customer"
import { BigNumberInput, BigNumberValue } from "../totals" import { BigNumberInput, BigNumberValue } from "../totals"
import { PaymentSessionStatus } from "./common" import { AccountHolderDTO, PaymentSessionStatus } from "./common"
import { ProviderWebhookPayload } from "./mutations" import { ProviderWebhookPayload } from "./mutations"
/** /**
@@ -12,7 +11,19 @@ export type PaymentAddressDTO = Partial<AddressDTO>
/** /**
* The customer associated with the payment. * The customer associated with the payment.
*/ */
export type PaymentCustomerDTO = Partial<CustomerDTO> export type PaymentCustomerDTO = {
id: string
email: string
company_name?: string | null
first_name?: string | null
last_name?: string | null
phone?: string | null
billing_address?: PaymentAddressDTO | null
}
export type PaymentAccountHolderDTO = {
data: Record<string, unknown>
}
/** /**
* Normalized events from payment provider to internal payment module events. * Normalized events from payment provider to internal payment module events.
@@ -26,33 +37,26 @@ export type PaymentActions =
/** /**
* @interface * @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 = {
/** /**
* The payment's billing address. * The account holder information, if available for the payment provider.
*/ */
billing_address?: PaymentAddressDTO account_holder?: PaymentAccountHolderDTO
/** /**
* The associated customer's email. * The customer information from Medusa.
*/
email?: string
/**
* The ID of payment session the provider payment is associated with.
*/
session_id?: string
/**
* The customer associated with this payment.
*/ */
customer?: PaymentCustomerDTO customer?: PaymentCustomerDTO
}
/** export type PaymentProviderInput = {
* The extra fields specific to the provider session. // Data is a combination of the input from the user and whatever is stored in the DB for this entity.
*/ data?: Record<string, unknown>
extra?: Record<string, unknown>
// The context for this payment operation. The data is guaranteed to be validated and not directly provided by the user.
context?: PaymentProviderContext
} }
/** /**
@@ -61,12 +65,7 @@ export type PaymentProviderContext = {
* The data used initiate a payment in a provider when a payment * The data used initiate a payment in a provider when a payment
* session is created. * session is created.
*/ */
export type CreatePaymentProviderSession = { export type InitiatePaymentInput = PaymentProviderInput & {
/**
* A context necessary for the payment provider.
*/
context: PaymentProviderContext
/** /**
* The amount to be authorized. * The amount to be authorized.
*/ */
@@ -78,34 +77,12 @@ export type CreatePaymentProviderSession = {
currency_code: string currency_code: string
} }
export type SavePaymentMethod = {
/**
* Any data that should be used by the provider for saving the payment method.
*/
data: Record<string, unknown>
/**
* The context of the payment provider, such as the customer ID.
*/
context: PaymentProviderContext
}
/** /**
* @interface * @interface
* *
* The attributes to update a payment related to a payment session in a provider. * The attributes to update a payment related to a payment session in a provider.
*/ */
export type UpdatePaymentProviderSession = { export type UpdatePaymentInput = PaymentProviderInput & {
/**
* A payment's context.
*/
context: PaymentProviderContext
/**
* The `data` field of the payment session.
*/
data: Record<string, unknown>
/** /**
* The payment session's amount. * The payment session's amount.
*/ */
@@ -117,29 +94,58 @@ export type UpdatePaymentProviderSession = {
currency_code: string currency_code: string
} }
export type DeletePaymentInput = PaymentProviderInput
export type AuthorizePaymentInput = PaymentProviderInput
export type CapturePaymentInput = PaymentProviderInput
export type RefundPaymentInput = PaymentProviderInput & {
/**
* The amount to refund.
*/
amount: BigNumberInput
}
export type RetrievePaymentInput = PaymentProviderInput
export type CancelPaymentInput = PaymentProviderInput
export type CreateAccountHolderInput = PaymentProviderInput & {
context: Omit<PaymentProviderContext, "customer"> & {
customer: PaymentCustomerDTO
}
}
export type DeleteAccountHolderInput = PaymentProviderInput & {
context: Omit<PaymentProviderContext, "account_holder"> & {
account_holder: Partial<AccountHolderDTO>
}
}
export type ListPaymentMethodsInput = PaymentProviderInput
export type SavePaymentMethodInput = PaymentProviderInput
export type GetPaymentStatusInput = PaymentProviderInput
/** /**
* @interface * @interface
* *
* The response of operations on a payment. * The response of operations on a payment.
*/ */
export type PaymentProviderSessionResponse = { export type PaymentProviderOutput = {
/** /**
* The data to be stored in the `data` field of the Payment Session to be created. * The unstrucvtured data returned from the payment provider. The content will vary between providers.
* 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<string, unknown> data?: Record<string, unknown>
} }
export type SavePaymentMethodResponse = { export type InitiatePaymentOutput = PaymentProviderOutput & {
/** /**
* The ID of the payment method in the payment provider. * The ID of the payment session in the payment provider.
*/ */
id: string id: string
/**
* The data returned from the payment provider after saving the payment method.
*/
data: Record<string, unknown>
} }
/** /**
@@ -147,59 +153,53 @@ export type SavePaymentMethodResponse = {
* *
* The successful result of authorizing a payment session using a payment provider. * The successful result of authorizing a payment session using a payment provider.
*/ */
export type PaymentProviderAuthorizeResponse = { export type AuthorizePaymentOutput = PaymentProviderOutput & {
/** /**
* The status of the payment, which will be stored in the payment session's `status` field. * The status of the payment, which will be stored in the payment session's `status` field.
*/ */
status: PaymentSessionStatus status: PaymentSessionStatus
/**
* The `data` to be stored in the payment session's `data` field.
*/
data: PaymentProviderSessionResponse["data"]
} }
export type PaymentMethodResponse = { export type UpdatePaymentOutput = PaymentProviderOutput
export type DeletePaymentOutput = PaymentProviderOutput
export type CapturePaymentOutput = PaymentProviderOutput
export type RefundPaymentOutput = PaymentProviderOutput
export type RetrievePaymentOutput = PaymentProviderOutput
export type CancelPaymentOutput = PaymentProviderOutput
export type CreateAccountHolderOutput = PaymentProviderOutput & {
/**
* The ID of the account holder in the payment provider.
*/
id: string id: string
data: Record<string, unknown>
} }
/** export type DeleteAccountHolderOutput = PaymentProviderOutput
* @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.
*/
provider_id: string
export type ListPaymentMethodsOutput = (PaymentProviderOutput & {
/** /**
* The data to be passed to the provider. * The ID of the payment method in the payment provider.
*/ */
data: Record<string, unknown> id: string
})[]
export type SavePaymentMethodOutput = PaymentProviderOutput & {
/**
* The ID of the payment method in the payment provider.
*/
id: string
} }
/** export type GetPaymentStatusOutput = PaymentProviderOutput & {
* An object that is returned in case of an error.
*/
export interface PaymentProviderError {
/** /**
* The error message * The status of the payment, which will be stored in the payment session's `status` field.
*/ */
error: string status: PaymentSessionStatus
/**
* The error code.
*/
code?: string
/**
* Any additional helpful details.
*/
detail?: any
} }
/** /**
@@ -244,51 +244,39 @@ export interface IPaymentProvider {
*/ */
getIdentifier(): string getIdentifier(): string
initiatePayment( initiatePayment(data: InitiatePaymentInput): Promise<InitiatePaymentOutput>
data: CreatePaymentProviderSession
): Promise<PaymentProviderError | PaymentProviderSessionResponse>
updatePayment( updatePayment(data: UpdatePaymentInput): Promise<UpdatePaymentOutput>
context: UpdatePaymentProviderSession
): Promise<PaymentProviderError | PaymentProviderSessionResponse>
deletePayment( deletePayment(data: DeletePaymentInput): Promise<DeletePaymentOutput>
paymentSessionData: Record<string, unknown>
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]>
authorizePayment( authorizePayment(data: AuthorizePaymentInput): Promise<AuthorizePaymentOutput>
paymentSessionData: Record<string, unknown>,
context: Record<string, unknown>
): Promise<PaymentProviderError | PaymentProviderAuthorizeResponse>
capturePayment( capturePayment(data: CapturePaymentInput): Promise<CapturePaymentOutput>
paymentSessionData: Record<string, unknown>
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]>
refundPayment( refundPayment(data: RefundPaymentInput): Promise<RefundPaymentOutput>
paymentSessionData: Record<string, unknown>,
refundAmount: BigNumberInput
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]>
retrievePayment( retrievePayment(data: RetrievePaymentInput): Promise<RetrievePaymentOutput>
paymentSessionData: Record<string, unknown>
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]>
cancelPayment( cancelPayment(data: CancelPaymentInput): Promise<CancelPaymentOutput>
paymentSessionData: Record<string, unknown>
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> createAccountHolder?(
data: CreateAccountHolderInput
): Promise<CreateAccountHolderOutput>
deleteAccountHolder?(
data: DeleteAccountHolderInput
): Promise<DeleteAccountHolderOutput>
listPaymentMethods?( listPaymentMethods?(
context: PaymentProviderContext data: ListPaymentMethodsInput
): Promise<PaymentMethodResponse[]> ): Promise<ListPaymentMethodsOutput>
savePaymentMethod?( savePaymentMethod?(
input: SavePaymentMethod data: SavePaymentMethodInput
): Promise<PaymentProviderError | SavePaymentMethodResponse> ): Promise<SavePaymentMethodOutput>
getPaymentStatus( getPaymentStatus(data: GetPaymentStatusInput): Promise<GetPaymentStatusOutput>
paymentSessionData: Record<string, unknown>
): Promise<PaymentSessionStatus>
getWebhookActionAndData( getWebhookActionAndData(
data: ProviderWebhookPayload["payload"] data: ProviderWebhookPayload["payload"]
@@ -3,6 +3,7 @@ import { RestoreReturn, SoftDeleteReturn } from "../dal"
import { IModuleService } from "../modules-sdk" import { IModuleService } from "../modules-sdk"
import { Context } from "../shared-context" import { Context } from "../shared-context"
import { import {
AccountHolderDTO,
CaptureDTO, CaptureDTO,
FilterableCaptureProps, FilterableCaptureProps,
FilterablePaymentCollectionProps, FilterablePaymentCollectionProps,
@@ -31,6 +32,7 @@ import {
UpdatePaymentDTO, UpdatePaymentDTO,
UpdatePaymentSessionDTO, UpdatePaymentSessionDTO,
UpdateRefundReasonDTO, UpdateRefundReasonDTO,
CreateAccountHolderDTO,
UpsertPaymentCollectionDTO, UpsertPaymentCollectionDTO,
} from "./mutations" } from "./mutations"
import { WebhookActionResult } from "./provider" import { WebhookActionResult } from "./provider"
@@ -751,6 +753,63 @@ export interface IPaymentModuleService extends IModuleService {
sharedContext?: Context sharedContext?: Context
): Promise<[PaymentProviderDTO[], number]> ): Promise<[PaymentProviderDTO[], number]>
/**
* This method creates(if supported by provider) the account holder in the payment provider.
*
* @param {CreateAccountHolderDTO} data - The details of the account holder.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<Record<string, unknown>>} The account holder's details in the payment provider, typically just the ID.
*
* @example
* const accountHolder =
* await paymentModuleService.createAccountHolder(
* {
* provider_id: "stripe",
* context: {
* customer: {
* id: "cus_123",
* },
* },
* }
* )
*
* remoteLink.create([{
* [Modules.CUSTOMER]: {
* customer_id: "cus_123",
* },
* [Modules.PAYMENT]: {
* account_holder_id: accountHolder.id,
* },
* }])
*/
createAccountHolder(
input: CreateAccountHolderDTO,
sharedContext?: Context
): Promise<AccountHolderDTO>
/**
* This method deletes the account holder in the payment provider.
*
* @param {string} id - The account holder's ID.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<void>} Resolves when the account holder is deleted successfully.
*
* @example
* await paymentModuleService.deleteAccountHolder({
* id: "acc_holder_123",
* })
*
* remoteLink.dismiss([{
* [Modules.CUSTOMER]: {
* customer_id: "cus_123",
* },
* [Modules.PAYMENT]: {
* account_holder_id: "acc_holder_123",
* },
* }])
*/
deleteAccountHolder(id: string, sharedContext?: Context): Promise<void>
/** /**
* This method retrieves all payment methods based on the context and configuration. * This method retrieves all payment methods based on the context and configuration.
* *
+6
View File
@@ -122,4 +122,10 @@ export const LINKS = {
Modules.FULFILLMENT, Modules.FULFILLMENT,
"shipping_profile_id" "shipping_profile_id"
), ),
CustomerAccountHolder: composeLinkName(
Modules.CUSTOMER,
"customer_id",
Modules.PAYMENT,
"account_holder_id"
),
} }
@@ -1,12 +1,25 @@
import { import {
CreatePaymentProviderSession,
IPaymentProvider, IPaymentProvider,
PaymentProviderError,
PaymentProviderSessionResponse,
PaymentSessionStatus,
ProviderWebhookPayload, ProviderWebhookPayload,
UpdatePaymentProviderSession,
WebhookActionResult, WebhookActionResult,
CapturePaymentInput,
CapturePaymentOutput,
AuthorizePaymentInput,
AuthorizePaymentOutput,
CancelPaymentInput,
CancelPaymentOutput,
InitiatePaymentInput,
InitiatePaymentOutput,
DeletePaymentInput,
DeletePaymentOutput,
GetPaymentStatusInput,
GetPaymentStatusOutput,
RefundPaymentInput,
RefundPaymentOutput,
RetrievePaymentInput,
RetrievePaymentOutput,
UpdatePaymentInput,
UpdatePaymentOutput,
} from "@medusajs/types" } from "@medusajs/types"
export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>> export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
@@ -147,48 +160,34 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* *
* In this method, use the third-party provider to capture the payment. * In this method, use the third-party provider to capture the payment.
* *
* @param paymentData - The `data` property of the payment. Make sure to store in it * @param input - The input to capture the payment. The `data` field should contain the data from the payment provider. when the payment was created.
* any helpful identification for your third-party integration. * @returns The new data to store in the payment's `data` property. Throws in case of an error.
* @returns The new data to store in the payment's `data` property, or an error object.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* PaymentProviderError, * CapturePaymentInput,
* PaymentProviderSessionResponse, * CapturePaymentOutput,
* } from "@medusajs/framework/types" * } from "@medusajs/framework/types"
* *
* class MyPaymentProviderService extends AbstractPaymentProvider< * class MyPaymentProviderService extends AbstractPaymentProvider<
* Options * Options
* > { * > {
* async capturePayment( * async capturePayment(
* paymentData: Record<string, unknown> * input: CapturePaymentInput
* ): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> { * ): Promise<CapturePaymentOutput> {
* const externalId = paymentData.id * const externalId = input.data?.id
* *
* try {
* // assuming you have a client that captures the payment * // assuming you have a client that captures the payment
* const newData = await this.client.capturePayment(externalId) * const newData = await this.client.capturePayment(externalId)
* * return {data: newData}
* return {
* ...newData,
* id: externalId
* } * }
* } catch (e) {
* return {
* error: e,
* code: "unknown",
* detail: e
* }
* }
* }
*
* // ... * // ...
* } * }
*/ */
abstract capturePayment( abstract capturePayment(
paymentData: Record<string, unknown> input: CapturePaymentInput
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> ): Promise<CapturePaymentOutput>
/** /**
* This method authorizes a payment session. When authorized successfully, a payment is created by the Payment * This method authorizes a payment session. When authorized successfully, a payment is created by the Payment
@@ -199,18 +198,14 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* *
* To automatically capture the payment after authorization, return the status `captured`. * To automatically capture the payment after authorization, return the status `captured`.
* *
* @param paymentSessionData - The `data` property of the payment session. Make sure to store in it * @param input - The input to authorize the payment. The `data` field should contain the data from the payment provider. when the payment was created.
* any helpful identification for your third-party integration. * @returns The status of the authorization, along with the `data` field about the payment. Throws in case of an error.
* @param context - The context in which the payment is being authorized. For example, in checkout,
* the context has a `cart_id` property indicating the ID of the associated cart.
* @returns Either an object of the new data to store in the created payment's `data` property and the
* payment's status, or an error object. Make sure to set in `data` anything useful to later retrieve the session.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* PaymentProviderError, * AuthorizePaymentInput,
* PaymentProviderSessionResponse, * AuthorizePaymentOutput,
* PaymentSessionStatus * PaymentSessionStatus
* } from "@medusajs/framework/types" * } from "@medusajs/framework/types"
* *
@@ -219,62 +214,31 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* Options * Options
* > { * > {
* async authorizePayment( * async authorizePayment(
* paymentSessionData: Record<string, unknown>, * input: AuthorizePaymentInput
* context: Record<string, unknown> * ): Promise<AuthorizePaymentOutput> {
* ): Promise< * const externalId = input.data?.id
* PaymentProviderError | {
* status: PaymentSessionStatus
* data: PaymentProviderSessionResponse["data"]
* }
* > {
* const externalId = paymentSessionData.id
* *
* try {
* // assuming you have a client that authorizes the payment * // assuming you have a client that authorizes the payment
* const paymentData = await this.client.authorizePayment(externalId) * const paymentData = await this.client.authorizePayment(externalId)
* *
* return { * return {
* data: { * data: paymentData,
* ...paymentData,
* id: externalId
* },
* status: "authorized" * status: "authorized"
* } * }
* } catch (e) {
* return {
* error: e,
* code: "unknown",
* detail: e
* }
* }
* } * }
* *
* // ... * // ...
* } * }
*/ */
abstract authorizePayment( abstract authorizePayment(
paymentSessionData: Record<string, unknown>, input: AuthorizePaymentInput
context: Record<string, unknown> ): Promise<AuthorizePaymentOutput>
): Promise<
| PaymentProviderError
| {
/**
* The new status of the payment.
*/
status: PaymentSessionStatus
/**
* The data to store in the created payment's `data` property.
*/
data: PaymentProviderSessionResponse["data"]
}
>
/** /**
* This method cancels a payment. * This method cancels a payment.
* *
* @param paymentData - The `data` property of the payment. Make sure to store in it * @param input - The input to cancel the payment. The `data` field should contain the data from the payment provider. when the payment was created.
* any helpful identification for your third-party integration. * @returns The new data to store in the payment's `data` property, if any. Throws in case of an error.
* @returns An error object if an error occurs, or the data received from the integration.
* *
* @example * @example
* // other imports... * // other imports...
@@ -288,42 +252,34 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* Options * Options
* > { * > {
* async cancelPayment( * async cancelPayment(
* paymentData: Record<string, unknown> * input: CancelPaymentInput
* ): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> { * ): Promise<CancelPaymentOutput> {
* const externalId = paymentData.id * const externalId = input.data?.id
* *
* try {
* // assuming you have a client that cancels the payment * // assuming you have a client that cancels the payment
* const paymentData = await this.client.cancelPayment(externalId) * const paymentData = await this.client.cancelPayment(externalId)
* } catch (e) { * return { data: paymentData }
* return {
* error: e,
* code: "unknown",
* detail: e
* }
* }
* } * }
* *
* // ... * // ...
* } * }
*/ */
abstract cancelPayment( abstract cancelPayment(
paymentData: Record<string, unknown> input: CancelPaymentInput
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> ): Promise<CancelPaymentOutput>
/** /**
* This method is used when a payment session is created. It can be used to initiate the payment * This method is used when a payment session is created. It can be used to initiate the payment
* in the third-party session, before authorizing or capturing the payment later. * in the third-party session, before authorizing or capturing the payment later.
* *
* @param context - The details of the payment session and its context. * @param input - The input to create the payment session.
* @returns An object whose `data` property is set in the created payment session, or an error * @returns The new data to store in the payment's `data` property. Throws in case of an error.
* object. Make sure to set in `data` anything useful to later retrieve the session.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* PaymentProviderError, * InitiatePaymentInput,
* PaymentProviderSessionResponse, * InitiatePaymentOutput,
* } from "@medusajs/framework/types" * } from "@medusajs/framework/types"
* *
* *
@@ -331,32 +287,22 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* Options * Options
* > { * > {
* async initiatePayment( * async initiatePayment(
* context: CreatePaymentProviderSession * input: InitiatePaymentInput
* ): Promise<PaymentProviderError | PaymentProviderSessionResponse> { * ): Promise<InitiatePaymentOutput> {
* const { * const {
* amount, * amount,
* currency_code, * currency_code,
* context: customerDetails * context: customerDetails
* } = context * } = input
* *
* try {
* // assuming you have a client that initializes the payment * // assuming you have a client that initializes the payment
* const response = await this.client.init( * const response = await this.client.init(
* amount, currency_code, customerDetails * amount, currency_code, customerDetails
* ) * )
* *
* return { * return {
* ...response,
* data: {
* id: response.id * id: response.id
* } * data: response,
* }
* } catch (e) {
* return {
* error: e,
* code: "unknown",
* detail: e
* }
* } * }
* } * }
* *
@@ -364,23 +310,22 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* } * }
*/ */
abstract initiatePayment( abstract initiatePayment(
context: CreatePaymentProviderSession input: InitiatePaymentInput
): Promise<PaymentProviderError | PaymentProviderSessionResponse> ): Promise<InitiatePaymentOutput>
/** /**
* This method is used when a payment session is deleted, which can only happen if it isn't authorized, yet. * This method is used when a payment session is deleted, which can only happen if it isn't authorized, yet.
* *
* Use this to delete or cancel the payment in the third-party service. * Use this to delete or cancel the payment in the third-party service.
* *
* @param paymentSessionData - The `data` property of the payment session. Make sure to store in it * @param input - The input to delete the payment session. The `data` field should contain the data from the payment provider. when the payment was created.
* any helpful identification for your third-party integration. * @returns The new data to store in the payment's `data` property, if any. Throws in case of an error.
* @returns An error object or the response from the third-party service.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* PaymentProviderError, * DeletePaymentInput,
* PaymentProviderSessionResponse, * DeletePaymentOutput,
* } from "@medusajs/framework/types" * } from "@medusajs/framework/types"
* *
* *
@@ -388,21 +333,13 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* Options * Options
* > { * > {
* async deletePayment( * async deletePayment(
* paymentSessionData: Record<string, unknown> * input: DeletePaymentInput
* ): Promise< * ): Promise<DeletePaymentOutput> {
* PaymentProviderError | PaymentProviderSessionResponse["data"] * const externalId = input.data?.id
* > {
* const externalId = paymentSessionData.id
* *
* try {
* // assuming you have a client that cancels the payment * // assuming you have a client that cancels the payment
* await this.client.cancelPayment(externalId) * await this.client.cancelPayment(externalId)
* } catch (e) { * return {}
* return {
* error: e,
* code: "unknown",
* detail: e
* }
* } * }
* } * }
* *
@@ -410,19 +347,20 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* } * }
*/ */
abstract deletePayment( abstract deletePayment(
paymentSessionData: Record<string, unknown> input: DeletePaymentInput
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> ): Promise<DeletePaymentOutput>
/** /**
* This method gets the status of a payment session based on the status in the third-party integration. * This method gets the status of a payment session based on the status in the third-party integration.
* *
* @param paymentSessionData - The `data` property of the payment session. Make sure to store in it * @param input - The input to get the payment status. The `data` field should contain the data from the payment provider. when the payment was created.
* any helpful identification for your third-party integration. * @returns The payment session's status. It can also return additional `data` from the payment provider.
* @returns The payment session's status.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* GetPaymentStatusInput,
* GetPaymentStatusOutput,
* PaymentSessionStatus * PaymentSessionStatus
* } from "@medusajs/framework/types" * } from "@medusajs/framework/types"
* *
@@ -431,26 +369,22 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* Options * Options
* > { * > {
* async getPaymentStatus( * async getPaymentStatus(
* paymentSessionData: Record<string, unknown> * input: GetPaymentStatusInput
* ): Promise<PaymentSessionStatus> { * ): Promise<GetPaymentStatusOutput> {
* const externalId = paymentSessionData.id * const externalId = input.data?.id
* *
* try {
* // assuming you have a client that retrieves the payment status * // assuming you have a client that retrieves the payment status
* const status = await this.client.getStatus(externalId) * const status = await this.client.getStatus(externalId)
* *
* switch (status) { * switch (status) {
* case "requires_capture": * case "requires_capture":
* return "authorized" * return {status: "authorized"}
* case "success": * case "success":
* return "captured" * return {status: "captured"}
* case "canceled": * case "canceled":
* return "canceled" * return {status: "canceled"}
* default: * default:
* return "pending" * return {status: "pending"}
* }
* } catch (e) {
* return "error"
* } * }
* } * }
* *
@@ -458,22 +392,20 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* } * }
*/ */
abstract getPaymentStatus( abstract getPaymentStatus(
paymentSessionData: Record<string, unknown> input: GetPaymentStatusInput
): Promise<PaymentSessionStatus> ): Promise<GetPaymentStatusOutput>
/** /**
* This method refunds an amount of a payment previously captured. * This method refunds an amount of a payment previously captured.
* *
* @param paymentData - The `data` property of the payment. Make sure to store in it * @param input - The input to refund the payment. The `data` field should contain the data from the payment provider. when the payment was created.
* any helpful identification for your third-party integration.
* @param refundAmount The amount to refund.
* @returns The new data to store in the payment's `data` property, or an error object. * @returns The new data to store in the payment's `data` property, or an error object.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* PaymentProviderError, * RefundPaymentInput,
* PaymentProviderSessionResponse, * RefundPaymentOutput,
* } from "@medusajs/framework/types" * } from "@medusajs/framework/types"
* *
* *
@@ -481,53 +413,36 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* Options * Options
* > { * > {
* async refundPayment( * async refundPayment(
* paymentData: Record<string, unknown>, * input: RefundPaymentInput
* refundAmount: number * ): Promise<RefundPaymentOutput> {
* ): Promise< * const externalId = input.data?.id
* PaymentProviderError | PaymentProviderSessionResponse["data"]
* > {
* const externalId = paymentData.id
* *
* try {
* // assuming you have a client that refunds the payment * // assuming you have a client that refunds the payment
* const newData = await this.client.refund( * const newData = await this.client.refund(
* externalId, * externalId,
* refundAmount * input.amount
* ) * )
* *
* return { * return {data: newData}
* ...newData,
* id: externalId
* } * }
* } catch (e) {
* return {
* error: e,
* code: "unknown",
* detail: e
* }
* }
* }
*
* // ... * // ...
* } * }
*/ */
abstract refundPayment( abstract refundPayment(
paymentData: Record<string, unknown>, input: RefundPaymentInput
refundAmount: number ): Promise<RefundPaymentOutput>
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]>
/** /**
* Retrieves the payment's data from the third-party service. * Retrieves the payment's data from the third-party service.
* *
* @param paymentSessionData - The `data` property of the payment. Make sure to store in it * @param input - The input to retrieve the payment. The `data` field should contain the data from the payment provider when the payment was created.
* any helpful identification for your third-party integration. * @returns The payment's data as found in the the payment provider.
* @returns An object to be stored in the payment's `data` property, or an error object.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* PaymentProviderError, * RetrievePaymentInput,
* PaymentProviderSessionResponse, * RetrievePaymentOutput,
* } from "@medusajs/framework/types" * } from "@medusajs/framework/types"
* *
* *
@@ -535,44 +450,31 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* Options * Options
* > { * > {
* async retrievePayment( * async retrievePayment(
* paymentSessionData: Record<string, unknown> * input: RetrievePaymentInput
* ): Promise< * ): Promise<RetrievePaymentOutput> {
* PaymentProviderError | PaymentProviderSessionResponse["data"] * const externalId = input.data?.id
* > {
* const externalId = paymentSessionData.id
* *
* try {
* // assuming you have a client that retrieves the payment * // assuming you have a client that retrieves the payment
* return await this.client.retrieve(externalId) * return await this.client.retrieve(externalId)
* } catch (e) {
* return {
* error: e,
* code: "unknown",
* detail: e
* } * }
* }
* }
*
* // ... * // ...
* } * }
*/ */
abstract retrievePayment( abstract retrievePayment(
paymentSessionData: Record<string, unknown> input: RetrievePaymentInput
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> ): Promise<RetrievePaymentOutput>
/** /**
* Update a payment in the third-party service that was previously initiated with the {@link initiatePayment} method. * Update a payment in the third-party service that was previously initiated with the {@link initiatePayment} method.
* *
* @param context - The details of the payment session and its context. * @param input - The input to update the payment. The `data` field should contain the data from the payment provider. when the payment was created.
* @returns An object whose `data` property is set in the updated payment session, or an error * @returns The new data to store in the payment's `data` property. Throws in case of an error.
* object. Make sure to set in `data` anything useful to later retrieve the session.
* *
* @example * @example
* // other imports... * // other imports...
* import { * import {
* UpdatePaymentProviderSession, * UpdatePaymentInput,
* PaymentProviderError, * UpdatePaymentOutput,
* PaymentProviderSessionResponse,
* } from "@medusajs/framework/types" * } from "@medusajs/framework/types"
* *
* *
@@ -580,48 +482,30 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* Options * Options
* > { * > {
* async updatePayment( * async updatePayment(
* context: UpdatePaymentProviderSession * input: UpdatePaymentInput
* ): Promise<PaymentProviderError | PaymentProviderSessionResponse> { * ): Promise<UpdatePaymentOutput> {
* const { * const { amount, currency_code, context } = input
* amount, * const externalId = input.data?.id
* currency_code,
* context: customerDetails,
* data
* } = context
* const externalId = data.id
* *
* try {
* // assuming you have a client that updates the payment * // assuming you have a client that updates the payment
* const response = await this.client.update( * const response = await this.client.update(
* externalId, * externalId,
* { * {
* amount, * amount,
* currency_code, * currency_code,
* customerDetails * context.customer
* } * }
* ) * )
* *
* return { * return response
* ...response,
* data: {
* id: response.id
* }
* }
* } catch (e) {
* return {
* error: e,
* code: "unknown",
* detail: e
* }
* }
* } * }
* *
* // ... * // ...
* } * }
*/ */
abstract updatePayment( abstract updatePayment(
context: UpdatePaymentProviderSession input: UpdatePaymentInput
): Promise<PaymentProviderError | PaymentProviderSessionResponse> ): Promise<UpdatePaymentOutput>
/** /**
* This method is executed when a webhook event is received from the third-party payment provider. Use it * This method is executed when a webhook event is received from the third-party payment provider. Use it
@@ -11,19 +11,13 @@ export const POST = async (
res: MedusaResponse<HttpTypes.StorePaymentCollectionResponse> res: MedusaResponse<HttpTypes.StorePaymentCollectionResponse>
) => { ) => {
const collectionId = req.params.id const collectionId = req.params.id
const { context = {}, data, provider_id } = req.body const { provider_id, data } = req.body
// If the customer is logged in, we auto-assign them to the payment collection
if (req.auth_context?.actor_id) {
;(context as any).customer = {
id: req.auth_context?.actor_id,
}
}
const workflowInput = { const workflowInput = {
payment_collection_id: collectionId, payment_collection_id: collectionId,
provider_id: provider_id, provider_id: provider_id,
customer_id: req.auth_context?.actor_id,
data, data,
context,
} }
await createPaymentSessionsWorkflow(req.scope).run({ await createPaymentSessionsWorkflow(req.scope).run({
@@ -12,7 +12,6 @@ export type StoreCreatePaymentSessionType = z.infer<
export const StoreCreatePaymentSession = z export const StoreCreatePaymentSession = z
.object({ .object({
provider_id: z.string(), provider_id: z.string(),
context: z.record(z.unknown()).optional(),
data: z.record(z.unknown()).optional(), data: z.record(z.unknown()).optional(),
}) })
.strict() .strict()
@@ -0,0 +1,68 @@
import { ModuleJoinerConfig } from "@medusajs/framework/types"
import { LINKS, Modules } from "@medusajs/framework/utils"
export const CustomerAccountHolder: ModuleJoinerConfig = {
serviceName: LINKS.CustomerAccountHolder,
isLink: true,
databaseConfig: {
tableName: "customer_account_holder",
idPrefix: "custacchldr",
},
alias: [
{
name: ["customer_account_holder", "customer_account_holders"],
entity: "LinkCustomerAccountHolder",
},
],
primaryKeys: ["id", "customer_id", "account_holder_id"],
relationships: [
{
serviceName: Modules.CUSTOMER,
entity: "Customer",
primaryKey: "id",
foreignKey: "customer_id",
alias: "customer",
args: {
methodSuffix: "Customers",
},
},
{
serviceName: Modules.PAYMENT,
entity: "AccountHolder",
primaryKey: "id",
foreignKey: "account_holder_id",
alias: "account_holder",
args: {
methodSuffix: "AccountHolders",
},
},
],
extends: [
{
serviceName: Modules.CUSTOMER,
entity: "Customer",
fieldAlias: {
account_holder: "account_holder_link.account_holder",
},
relationship: {
serviceName: LINKS.CustomerAccountHolder,
primaryKey: "customer_id",
foreignKey: "id",
alias: "account_holder_link",
},
},
{
serviceName: Modules.PAYMENT,
entity: "AccountHolder",
fieldAlias: {
customer: "customer_link.customer",
},
relationship: {
serviceName: LINKS.CustomerAccountHolder,
primaryKey: "account_holder_id",
foreignKey: "id",
alias: "customer_link",
},
},
],
}
@@ -16,3 +16,4 @@ export * from "./region-payment-provider"
export * from "./sales-channel-location" export * from "./sales-channel-location"
export * from "./shipping-option-price-set" export * from "./shipping-option-price-set"
export * from "./product-shipping-profile" export * from "./product-shipping-profile"
export * from "./customer-account-holder"
@@ -23,12 +23,13 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
service: PaymentModuleService, service: PaymentModuleService,
}).linkable }).linkable
expect(Object.keys(linkable)).toHaveLength(5) expect(Object.keys(linkable)).toHaveLength(6)
expect(Object.keys(linkable)).toEqual([ expect(Object.keys(linkable)).toEqual([
"paymentCollection", "paymentCollection",
"paymentSession", "paymentSession",
"payment", "payment",
"refundReason", "refundReason",
"accountHolder",
"paymentProvider", "paymentProvider",
]) ])
@@ -73,6 +74,15 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
field: "refundReason", field: "refundReason",
}, },
}, },
accountHolder: {
id: {
linkable: "account_holder_id",
entity: "AccountHolder",
primaryKey: "id",
serviceName: "payment",
field: "accountHolder",
},
},
paymentProvider: { paymentProvider: {
id: { id: {
linkable: "payment_provider_id", linkable: "payment_provider_id",
@@ -100,10 +110,7 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
currency_code: "usd", currency_code: "usd",
data: {}, data: {},
context: { context: {
extra: {}, customer: { id: "cus-id-1", email: "new@test.tsst" },
customer: {},
billing_address: {},
email: "test@test.test.com",
}, },
} }
) )
@@ -347,11 +354,11 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
currency_code: "usd", currency_code: "usd",
data: {}, data: {},
context: { context: {
extra: {}, customer: {
customer: {}, id: "cus-id-1",
billing_address: {},
email: "test@test.test.com", email: "test@test.test.com",
}, },
},
}) })
const paymentCollection = await service.retrievePaymentCollection( const paymentCollection = await service.retrievePaymentCollection(
@@ -402,10 +409,7 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
currency_code: "usd", currency_code: "usd",
data: {}, data: {},
context: { context: {
extra: {}, customer: { id: "cus-id-1", email: "test@test.test.com" },
customer: {},
billing_address: {},
email: "test@test.test.com",
}, },
}) })
.catch((e) => e) .catch((e) => e)
@@ -439,10 +443,7 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
currency_code: "usd", currency_code: "usd",
data: {}, data: {},
context: { context: {
extra: {}, customer: { id: "cus-id-1", email: "test@test.test.com" },
customer: {},
billing_address: {},
email: "test@test.test.com",
}, },
}) })
.catch((e) => e) .catch((e) => e)
@@ -461,10 +462,7 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
currency_code: "usd", currency_code: "usd",
data: {}, data: {},
context: { context: {
extra: {}, customer: { id: "cus-id-1", email: "new@test.tsst" },
customer: {},
billing_address: {},
email: "test@test.test.com",
}, },
}) })
@@ -474,10 +472,7 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
currency_code: "eur", currency_code: "eur",
data: {}, data: {},
context: { context: {
extra: {}, customer: { id: "cus-id-1", email: "new@test.tsst" },
customer: {},
billing_address: {},
email: "new@test.tsst",
}, },
}) })
@@ -505,10 +500,7 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
currency_code: "usd", currency_code: "usd",
data: {}, data: {},
context: { context: {
extra: {}, customer: { id: "cus-id-1", email: "new@test.tsst" },
email: "test@test.com",
billing_address: {},
customer: {},
}, },
}) })
@@ -5,6 +5,7 @@ import {
PaymentProvider, PaymentProvider,
PaymentSession, PaymentSession,
RefundReason, RefundReason,
AccountHolder,
} from "@models" } from "@models"
import { default as schema } from "./schema" import { default as schema } from "./schema"
@@ -16,12 +17,14 @@ export const joinerConfig = defineJoinerConfig(Modules.PAYMENT, {
PaymentProvider, PaymentProvider,
PaymentSession, PaymentSession,
RefundReason, RefundReason,
AccountHolder,
], ],
linkableKeys: { linkableKeys: {
payment_id: Payment.name, payment_id: Payment.name,
payment_collection_id: PaymentCollection.name, payment_collection_id: PaymentCollection.name,
payment_provider_id: PaymentProvider.name, payment_provider_id: PaymentProvider.name,
refund_reason_id: RefundReason.name, refund_reason_id: RefundReason.name,
account_holder_id: AccountHolder.name,
}, },
alias: [ alias: [
{ {
@@ -4,6 +4,132 @@
], ],
"name": "public", "name": "public",
"tables": [ "tables": [
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"provider_id": {
"name": "provider_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"external_id": {
"name": "external_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"email": {
"name": "email",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"data": {
"name": "data",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "'{}'",
"mappedType": "json"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"deleted_at": {
"name": "deleted_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
}
},
"name": "account_holder",
"schema": "public",
"indexes": [
{
"keyName": "IDX_account_holder_deleted_at",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_account_holder_deleted_at\" ON \"account_holder\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_account_holder_provider_id_external_id_unique",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_account_holder_provider_id_external_id_unique\" ON \"account_holder\" (provider_id, external_id) WHERE deleted_at IS NULL"
},
{
"keyName": "account_holder_pkey",
"columnNames": [
"id"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {},
"nativeEnums": {}
},
{ {
"columns": { "columns": {
"id": { "id": {
@@ -172,6 +298,7 @@
"keyName": "IDX_payment_collection_deleted_at", "keyName": "IDX_payment_collection_deleted_at",
"columnNames": [], "columnNames": [],
"composite": false, "composite": false,
"constraint": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_collection_deleted_at\" ON \"payment_collection\" (deleted_at) WHERE deleted_at IS NULL" "expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_collection_deleted_at\" ON \"payment_collection\" (deleted_at) WHERE deleted_at IS NULL"
@@ -182,12 +309,14 @@
"id" "id"
], ],
"composite": false, "composite": false,
"constraint": true,
"primary": true, "primary": true,
"unique": true "unique": true
} }
], ],
"checks": [], "checks": [],
"foreignKeys": {} "foreignKeys": {},
"nativeEnums": {}
}, },
{ {
"columns": { "columns": {
@@ -294,6 +423,7 @@
"keyName": "IDX_payment_method_token_deleted_at", "keyName": "IDX_payment_method_token_deleted_at",
"columnNames": [], "columnNames": [],
"composite": false, "composite": false,
"constraint": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_method_token_deleted_at\" ON \"payment_method_token\" (deleted_at) WHERE deleted_at IS NULL" "expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_method_token_deleted_at\" ON \"payment_method_token\" (deleted_at) WHERE deleted_at IS NULL"
@@ -304,12 +434,14 @@
"id" "id"
], ],
"composite": false, "composite": false,
"constraint": true,
"primary": true, "primary": true,
"unique": true "unique": true
} }
], ],
"checks": [], "checks": [],
"foreignKeys": {} "foreignKeys": {},
"nativeEnums": {}
}, },
{ {
"columns": { "columns": {
@@ -372,6 +504,7 @@
"keyName": "IDX_payment_provider_deleted_at", "keyName": "IDX_payment_provider_deleted_at",
"columnNames": [], "columnNames": [],
"composite": false, "composite": false,
"constraint": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_provider_deleted_at\" ON \"payment_provider\" (deleted_at) WHERE deleted_at IS NULL" "expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_provider_deleted_at\" ON \"payment_provider\" (deleted_at) WHERE deleted_at IS NULL"
@@ -382,12 +515,14 @@
"id" "id"
], ],
"composite": false, "composite": false,
"constraint": true,
"primary": true, "primary": true,
"unique": true "unique": true
} }
], ],
"checks": [], "checks": [],
"foreignKeys": {} "foreignKeys": {},
"nativeEnums": {}
}, },
{ {
"columns": { "columns": {
@@ -420,14 +555,15 @@
"payment_provider_id" "payment_provider_id"
], ],
"composite": true, "composite": true,
"constraint": true,
"primary": true, "primary": true,
"unique": true "unique": true
} }
], ],
"checks": [], "checks": [],
"foreignKeys": { "foreignKeys": {
"payment_collection_payment_providers_payment_coll_aa276_foreign": { "payment_collection_payment_providers_payment_col_aa276_foreign": {
"constraintName": "payment_collection_payment_providers_payment_coll_aa276_foreign", "constraintName": "payment_collection_payment_providers_payment_col_aa276_foreign",
"columnNames": [ "columnNames": [
"payment_collection_id" "payment_collection_id"
], ],
@@ -439,8 +575,8 @@
"deleteRule": "cascade", "deleteRule": "cascade",
"updateRule": "cascade" "updateRule": "cascade"
}, },
"payment_collection_payment_providers_payment_provider_id_foreign": { "payment_collection_payment_providers_payment_pro_2d555_foreign": {
"constraintName": "payment_collection_payment_providers_payment_provider_id_foreign", "constraintName": "payment_collection_payment_providers_payment_pro_2d555_foreign",
"columnNames": [ "columnNames": [
"payment_provider_id" "payment_provider_id"
], ],
@@ -452,7 +588,8 @@
"deleteRule": "cascade", "deleteRule": "cascade",
"updateRule": "cascade" "updateRule": "cascade"
} }
} },
"nativeEnums": {}
}, },
{ {
"columns": { "columns": {
@@ -606,6 +743,7 @@
"keyName": "IDX_payment_session_payment_collection_id", "keyName": "IDX_payment_session_payment_collection_id",
"columnNames": [], "columnNames": [],
"composite": false, "composite": false,
"constraint": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_session_payment_collection_id\" ON \"payment_session\" (payment_collection_id) WHERE deleted_at IS NULL" "expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_session_payment_collection_id\" ON \"payment_session\" (payment_collection_id) WHERE deleted_at IS NULL"
@@ -614,6 +752,7 @@
"keyName": "IDX_payment_session_deleted_at", "keyName": "IDX_payment_session_deleted_at",
"columnNames": [], "columnNames": [],
"composite": false, "composite": false,
"constraint": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_session_deleted_at\" ON \"payment_session\" (deleted_at) WHERE deleted_at IS NULL" "expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_session_deleted_at\" ON \"payment_session\" (deleted_at) WHERE deleted_at IS NULL"
@@ -624,6 +763,7 @@
"id" "id"
], ],
"composite": false, "composite": false,
"constraint": true,
"primary": true, "primary": true,
"unique": true "unique": true
} }
@@ -643,7 +783,8 @@
"deleteRule": "cascade", "deleteRule": "cascade",
"updateRule": "cascade" "updateRule": "cascade"
} }
} },
"nativeEnums": {}
}, },
{ {
"columns": { "columns": {
@@ -788,6 +929,7 @@
"keyName": "IDX_payment_payment_collection_id", "keyName": "IDX_payment_payment_collection_id",
"columnNames": [], "columnNames": [],
"composite": false, "composite": false,
"constraint": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_payment_collection_id\" ON \"payment\" (payment_collection_id) WHERE deleted_at IS NULL" "expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_payment_collection_id\" ON \"payment\" (payment_collection_id) WHERE deleted_at IS NULL"
@@ -796,6 +938,7 @@
"keyName": "IDX_payment_payment_session_id_unique", "keyName": "IDX_payment_payment_session_id_unique",
"columnNames": [], "columnNames": [],
"composite": false, "composite": false,
"constraint": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_payment_payment_session_id_unique\" ON \"payment\" (payment_session_id) WHERE deleted_at IS NULL" "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_payment_payment_session_id_unique\" ON \"payment\" (payment_session_id) WHERE deleted_at IS NULL"
@@ -804,6 +947,7 @@
"keyName": "IDX_payment_deleted_at", "keyName": "IDX_payment_deleted_at",
"columnNames": [], "columnNames": [],
"composite": false, "composite": false,
"constraint": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_deleted_at\" ON \"payment\" (deleted_at) WHERE deleted_at IS NULL" "expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_deleted_at\" ON \"payment\" (deleted_at) WHERE deleted_at IS NULL"
@@ -812,6 +956,7 @@
"keyName": "IDX_payment_provider_id", "keyName": "IDX_payment_provider_id",
"columnNames": [], "columnNames": [],
"composite": false, "composite": false,
"constraint": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_provider_id\" ON \"payment\" (provider_id) WHERE deleted_at IS NULL" "expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_provider_id\" ON \"payment\" (provider_id) WHERE deleted_at IS NULL"
@@ -820,6 +965,7 @@
"keyName": "IDX_payment_payment_session_id", "keyName": "IDX_payment_payment_session_id",
"columnNames": [], "columnNames": [],
"composite": false, "composite": false,
"constraint": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_payment_session_id\" ON \"payment\" (payment_session_id) WHERE deleted_at IS NULL" "expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_payment_session_id\" ON \"payment\" (payment_session_id) WHERE deleted_at IS NULL"
@@ -830,6 +976,7 @@
"id" "id"
], ],
"composite": false, "composite": false,
"constraint": true,
"primary": true, "primary": true,
"unique": true "unique": true
} }
@@ -861,7 +1008,8 @@
"referencedTableName": "public.payment_session", "referencedTableName": "public.payment_session",
"updateRule": "cascade" "updateRule": "cascade"
} }
} },
"nativeEnums": {}
}, },
{ {
"columns": { "columns": {
@@ -959,6 +1107,7 @@
"keyName": "IDX_capture_payment_id", "keyName": "IDX_capture_payment_id",
"columnNames": [], "columnNames": [],
"composite": false, "composite": false,
"constraint": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_capture_payment_id\" ON \"capture\" (payment_id) WHERE deleted_at IS NULL" "expression": "CREATE INDEX IF NOT EXISTS \"IDX_capture_payment_id\" ON \"capture\" (payment_id) WHERE deleted_at IS NULL"
@@ -967,6 +1116,7 @@
"keyName": "IDX_capture_deleted_at", "keyName": "IDX_capture_deleted_at",
"columnNames": [], "columnNames": [],
"composite": false, "composite": false,
"constraint": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_capture_deleted_at\" ON \"capture\" (deleted_at) WHERE deleted_at IS NULL" "expression": "CREATE INDEX IF NOT EXISTS \"IDX_capture_deleted_at\" ON \"capture\" (deleted_at) WHERE deleted_at IS NULL"
@@ -977,6 +1127,7 @@
"id" "id"
], ],
"composite": false, "composite": false,
"constraint": true,
"primary": true, "primary": true,
"unique": true "unique": true
} }
@@ -996,7 +1147,8 @@
"deleteRule": "cascade", "deleteRule": "cascade",
"updateRule": "cascade" "updateRule": "cascade"
} }
} },
"nativeEnums": {}
}, },
{ {
"columns": { "columns": {
@@ -1076,6 +1228,7 @@
"keyName": "IDX_refund_reason_deleted_at", "keyName": "IDX_refund_reason_deleted_at",
"columnNames": [], "columnNames": [],
"composite": false, "composite": false,
"constraint": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_refund_reason_deleted_at\" ON \"refund_reason\" (deleted_at) WHERE deleted_at IS NULL" "expression": "CREATE INDEX IF NOT EXISTS \"IDX_refund_reason_deleted_at\" ON \"refund_reason\" (deleted_at) WHERE deleted_at IS NULL"
@@ -1086,12 +1239,14 @@
"id" "id"
], ],
"composite": false, "composite": false,
"constraint": true,
"primary": true, "primary": true,
"unique": true "unique": true
} }
], ],
"checks": [], "checks": [],
"foreignKeys": {} "foreignKeys": {},
"nativeEnums": {}
}, },
{ {
"columns": { "columns": {
@@ -1207,6 +1362,7 @@
"keyName": "IDX_refund_payment_id", "keyName": "IDX_refund_payment_id",
"columnNames": [], "columnNames": [],
"composite": false, "composite": false,
"constraint": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_refund_payment_id\" ON \"refund\" (payment_id) WHERE deleted_at IS NULL" "expression": "CREATE INDEX IF NOT EXISTS \"IDX_refund_payment_id\" ON \"refund\" (payment_id) WHERE deleted_at IS NULL"
@@ -1215,6 +1371,7 @@
"keyName": "IDX_refund_refund_reason_id", "keyName": "IDX_refund_refund_reason_id",
"columnNames": [], "columnNames": [],
"composite": false, "composite": false,
"constraint": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_refund_refund_reason_id\" ON \"refund\" (refund_reason_id) WHERE deleted_at IS NULL" "expression": "CREATE INDEX IF NOT EXISTS \"IDX_refund_refund_reason_id\" ON \"refund\" (refund_reason_id) WHERE deleted_at IS NULL"
@@ -1223,6 +1380,7 @@
"keyName": "IDX_refund_deleted_at", "keyName": "IDX_refund_deleted_at",
"columnNames": [], "columnNames": [],
"composite": false, "composite": false,
"constraint": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_refund_deleted_at\" ON \"refund\" (deleted_at) WHERE deleted_at IS NULL" "expression": "CREATE INDEX IF NOT EXISTS \"IDX_refund_deleted_at\" ON \"refund\" (deleted_at) WHERE deleted_at IS NULL"
@@ -1233,6 +1391,7 @@
"id" "id"
], ],
"composite": false, "composite": false,
"constraint": true,
"primary": true, "primary": true,
"unique": true "unique": true
} }
@@ -1265,7 +1424,9 @@
"deleteRule": "set null", "deleteRule": "set null",
"updateRule": "cascade" "updateRule": "cascade"
} }
},
"nativeEnums": {}
} }
} ],
] "nativeEnums": {}
} }
@@ -0,0 +1,27 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20250123122334 extends Migration {
override async up(): Promise<void> {
this.addSql(`create table if not exists "account_holder" ("id" text not null, "provider_id" text not null, "external_id" text not null, "email" text null, "data" jsonb not null default '{}', "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "account_holder_pkey" primary key ("id"));`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_account_holder_deleted_at" ON "account_holder" (deleted_at) WHERE deleted_at IS NULL;`);
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_account_holder_provider_id_external_id_unique" ON "account_holder" (provider_id, external_id) WHERE deleted_at IS NULL;`);
this.addSql(`alter table if exists "payment_collection_payment_providers" drop constraint if exists "payment_collection_payment_providers_payment_coll_aa276_foreign";`);
this.addSql(`alter table if exists "payment_collection_payment_providers" drop constraint if exists "payment_collection_payment_providers_payment_provider_id_foreign";`);
this.addSql(`alter table if exists "payment_collection_payment_providers" add constraint "payment_collection_payment_providers_payment_col_aa276_foreign" foreign key ("payment_collection_id") references "payment_collection" ("id") on update cascade on delete cascade;`);
this.addSql(`alter table if exists "payment_collection_payment_providers" add constraint "payment_collection_payment_providers_payment_pro_2d555_foreign" foreign key ("payment_provider_id") references "payment_provider" ("id") on update cascade on delete cascade;`);
}
override async down(): Promise<void> {
this.addSql(`drop table if exists "account_holder" cascade;`);
this.addSql(`alter table if exists "payment_collection_payment_providers" drop constraint if exists "payment_collection_payment_providers_payment_col_aa276_foreign";`);
this.addSql(`alter table if exists "payment_collection_payment_providers" drop constraint if exists "payment_collection_payment_providers_payment_pro_2d555_foreign";`);
this.addSql(`alter table if exists "payment_collection_payment_providers" add constraint "payment_collection_payment_providers_payment_coll_aa276_foreign" foreign key ("payment_collection_id") references "payment_collection" ("id") on update cascade on delete cascade;`);
this.addSql(`alter table if exists "payment_collection_payment_providers" add constraint "payment_collection_payment_providers_payment_provider_id_foreign" foreign key ("payment_provider_id") references "payment_provider" ("id") on update cascade on delete cascade;`);
}
}
@@ -0,0 +1,19 @@
import { model } from "@medusajs/framework/utils"
const AccountHolder = model
.define("AccountHolder", {
id: model.id({ prefix: "acchld" }).primaryKey(),
provider_id: model.text(),
external_id: model.text(),
email: model.text().nullable(),
data: model.json().default({}),
metadata: model.json().nullable(),
})
.indexes([
{
on: ["provider_id", "external_id"],
unique: true,
},
])
export default AccountHolder
@@ -6,3 +6,4 @@ export { default as PaymentProvider } from "./payment-provider"
export { default as PaymentSession } from "./payment-session" export { default as PaymentSession } from "./payment-session"
export { default as Refund } from "./refund" export { default as Refund } from "./refund"
export { default as RefundReason } from "./refund-reason" export { default as RefundReason } from "./refund-reason"
export { default as AccountHolder } from "./account-holder"
@@ -1,8 +1,25 @@
import crypto from "crypto"
import { import {
CreatePaymentProviderSession, AuthorizePaymentInput,
PaymentProviderError, AuthorizePaymentOutput,
PaymentProviderSessionResponse, CancelPaymentInput,
CancelPaymentOutput,
CapturePaymentInput,
CapturePaymentOutput,
DeletePaymentInput,
DeletePaymentOutput,
GetPaymentStatusInput,
GetPaymentStatusOutput,
InitiatePaymentInput,
InitiatePaymentOutput,
ProviderWebhookPayload, ProviderWebhookPayload,
RefundPaymentInput,
RefundPaymentOutput,
RetrievePaymentInput,
RetrievePaymentOutput,
UpdatePaymentInput,
UpdatePaymentOutput,
WebhookActionResult, WebhookActionResult,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
@@ -23,53 +40,49 @@ export class SystemProviderService extends AbstractPaymentProvider {
} }
async initiatePayment( async initiatePayment(
context: CreatePaymentProviderSession input: InitiatePaymentInput
): Promise<PaymentProviderSessionResponse> { ): Promise<InitiatePaymentOutput> {
return { data: {} } return { data: {}, id: crypto.randomUUID() }
} }
async getPaymentStatus( async getPaymentStatus(
paymentSessionData: Record<string, unknown> input: GetPaymentStatusInput
): Promise<PaymentSessionStatus> { ): Promise<GetPaymentStatusOutput> {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
async retrievePayment( async retrievePayment(
paymentSessionData: Record<string, unknown> input: RetrievePaymentInput
): Promise<Record<string, unknown> | PaymentProviderError> { ): Promise<RetrievePaymentOutput> {
return {} return {}
} }
async authorizePayment(_): Promise< async authorizePayment(
| PaymentProviderError input: AuthorizePaymentInput
| { ): Promise<AuthorizePaymentOutput> {
status: PaymentSessionStatus
data: PaymentProviderSessionResponse["data"]
}
> {
return { data: {}, status: PaymentSessionStatus.AUTHORIZED } return { data: {}, status: PaymentSessionStatus.AUTHORIZED }
} }
async updatePayment( async updatePayment(input: UpdatePaymentInput): Promise<UpdatePaymentOutput> {
_ return { data: {} }
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
return { data: {} } as PaymentProviderSessionResponse
} }
async deletePayment(_): Promise<Record<string, unknown>> { async deletePayment(input: DeletePaymentInput): Promise<DeletePaymentOutput> {
return {} return { data: {} }
} }
async capturePayment(_): Promise<Record<string, unknown>> { async capturePayment(
return {} input: CapturePaymentInput
): Promise<CapturePaymentOutput> {
return { data: {} }
} }
async refundPayment(_): Promise<Record<string, unknown>> { async refundPayment(input: RefundPaymentInput): Promise<RefundPaymentOutput> {
return {} return { data: {} }
} }
async cancelPayment(_): Promise<Record<string, unknown>> { async cancelPayment(input: CancelPaymentInput): Promise<CancelPaymentOutput> {
return {} return { data: {} }
} }
async getWebhookActionAndData( async getWebhookActionAndData(
@@ -7,6 +7,7 @@ import {
CreatePaymentMethodDTO, CreatePaymentMethodDTO,
CreatePaymentSessionDTO, CreatePaymentSessionDTO,
CreateRefundDTO, CreateRefundDTO,
AccountHolderDTO,
DAL, DAL,
FilterablePaymentCollectionProps, FilterablePaymentCollectionProps,
FilterablePaymentMethodProps, FilterablePaymentMethodProps,
@@ -31,13 +32,17 @@ import {
UpdatePaymentCollectionDTO, UpdatePaymentCollectionDTO,
UpdatePaymentDTO, UpdatePaymentDTO,
UpdatePaymentSessionDTO, UpdatePaymentSessionDTO,
CreateAccountHolderDTO,
UpsertPaymentCollectionDTO, UpsertPaymentCollectionDTO,
WebhookActionResult, WebhookActionResult,
CreateAccountHolderOutput,
InitiatePaymentOutput,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
BigNumber, BigNumber,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
isPresent,
isString, isString,
MathBN, MathBN,
MedusaContext, MedusaContext,
@@ -48,6 +53,7 @@ import {
promiseAll, promiseAll,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { import {
AccountHolder,
Capture, Capture,
Payment, Payment,
PaymentCollection, PaymentCollection,
@@ -66,6 +72,7 @@ type InjectedDependencies = {
refundService: ModulesSdkTypes.IMedusaInternalService<any> refundService: ModulesSdkTypes.IMedusaInternalService<any>
paymentSessionService: ModulesSdkTypes.IMedusaInternalService<any> paymentSessionService: ModulesSdkTypes.IMedusaInternalService<any>
paymentCollectionService: ModulesSdkTypes.IMedusaInternalService<any> paymentCollectionService: ModulesSdkTypes.IMedusaInternalService<any>
accountHolderService: ModulesSdkTypes.IMedusaInternalService<any>
paymentProviderService: PaymentProviderService paymentProviderService: PaymentProviderService
} }
@@ -76,6 +83,7 @@ const generateMethodForModels = {
Capture, Capture,
Refund, Refund,
RefundReason, RefundReason,
AccountHolder,
} }
export default class PaymentModuleService export default class PaymentModuleService
@@ -86,6 +94,7 @@ export default class PaymentModuleService
Capture: { dto: CaptureDTO } Capture: { dto: CaptureDTO }
Refund: { dto: RefundDTO } Refund: { dto: RefundDTO }
RefundReason: { dto: RefundReasonDTO } RefundReason: { dto: RefundReasonDTO }
AccountHolder: { dto: AccountHolderDTO }
}>(generateMethodForModels) }>(generateMethodForModels)
implements IPaymentModuleService implements IPaymentModuleService
{ {
@@ -107,6 +116,9 @@ export default class PaymentModuleService
typeof PaymentCollection typeof PaymentCollection
> >
protected paymentProviderService_: PaymentProviderService protected paymentProviderService_: PaymentProviderService
protected accountHolderService_: ModulesSdkTypes.IMedusaInternalService<
typeof AccountHolder
>
constructor( constructor(
{ {
@@ -117,6 +129,7 @@ export default class PaymentModuleService
paymentSessionService, paymentSessionService,
paymentProviderService, paymentProviderService,
paymentCollectionService, paymentCollectionService,
accountHolderService,
}: InjectedDependencies, }: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration protected readonly moduleDeclaration: InternalModuleDeclaration
) { ) {
@@ -131,6 +144,7 @@ export default class PaymentModuleService
this.paymentSessionService_ = paymentSessionService this.paymentSessionService_ = paymentSessionService
this.paymentProviderService_ = paymentProviderService this.paymentProviderService_ = paymentProviderService
this.paymentCollectionService_ = paymentCollectionService this.paymentCollectionService_ = paymentCollectionService
this.accountHolderService_ = accountHolderService
} }
__joinerConfig(): ModuleJoinerConfig { __joinerConfig(): ModuleJoinerConfig {
@@ -316,7 +330,7 @@ export default class PaymentModuleService
@MedusaContext() sharedContext?: Context @MedusaContext() sharedContext?: Context
): Promise<PaymentSessionDTO> { ): Promise<PaymentSessionDTO> {
let paymentSession: InferEntityType<typeof PaymentSession> | undefined let paymentSession: InferEntityType<typeof PaymentSession> | undefined
let providerPaymentSession: Record<string, unknown> | undefined let providerPaymentSession: InitiatePaymentOutput | undefined
try { try {
paymentSession = await this.createPaymentSession_( paymentSession = await this.createPaymentSession_(
@@ -328,7 +342,8 @@ export default class PaymentModuleService
providerPaymentSession = await this.paymentProviderService_.createSession( providerPaymentSession = await this.paymentProviderService_.createSession(
input.provider_id, input.provider_id,
{ {
context: { ...input.context, session_id: paymentSession!.id }, context: input.context,
data: { ...input.data, session_id: paymentSession!.id },
amount: input.amount, amount: input.amount,
currency_code: input.currency_code, currency_code: input.currency_code,
} }
@@ -338,15 +353,14 @@ export default class PaymentModuleService
await this.paymentSessionService_.update( await this.paymentSessionService_.update(
{ {
id: paymentSession!.id, id: paymentSession!.id,
data: { ...input.data, ...providerPaymentSession }, data: { ...input.data, ...providerPaymentSession.data },
}, },
sharedContext sharedContext
) )
)[0] )[0]
} catch (error) { } catch (error) {
if (providerPaymentSession) { if (providerPaymentSession) {
await this.paymentProviderService_.deleteSession({ await this.paymentProviderService_.deleteSession(input.provider_id, {
provider_id: input.provider_id,
data: input.data, data: input.data,
}) })
} }
@@ -420,8 +434,7 @@ export default class PaymentModuleService
sharedContext sharedContext
) )
await this.paymentProviderService_.deleteSession({ await this.paymentProviderService_.deleteSession(session.provider_id, {
provider_id: session.provider_id,
data: session.data, data: session.data,
}) })
@@ -460,11 +473,11 @@ export default class PaymentModuleService
} }
let { data, status } = await this.paymentProviderService_.authorizePayment( let { data, status } = await this.paymentProviderService_.authorizePayment(
session.provider_id,
{ {
provider_id: session.provider_id,
data: session.data, data: session.data,
}, context,
context }
) )
if ( if (
@@ -486,8 +499,7 @@ export default class PaymentModuleService
sharedContext sharedContext
) )
} catch (error) { } catch (error) {
await this.paymentProviderService_.cancelPayment({ await this.paymentProviderService_.cancelPayment(session.provider_id, {
provider_id: session.provider_id,
data, data,
}) })
@@ -509,7 +521,7 @@ export default class PaymentModuleService
@InjectTransactionManager() @InjectTransactionManager()
async authorizePaymentSession_( async authorizePaymentSession_(
session: InferEntityType<typeof PaymentSession>, session: InferEntityType<typeof PaymentSession>,
data: Record<string, unknown>, data: Record<string, unknown> | undefined,
status: PaymentSessionStatus, status: PaymentSessionStatus,
@MedusaContext() sharedContext?: Context @MedusaContext() sharedContext?: Context
): Promise<InferEntityType<typeof Payment>> { ): Promise<InferEntityType<typeof Payment>> {
@@ -711,15 +723,17 @@ export default class PaymentModuleService
isFullyCaptured: boolean, isFullyCaptured: boolean,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
) { ) {
const paymentData = await this.paymentProviderService_.capturePayment({ const paymentData = await this.paymentProviderService_.capturePayment(
payment.provider_id,
{
data: payment.data!, data: payment.data!,
provider_id: payment.provider_id, }
}) )
await this.paymentService_.update( await this.paymentService_.update(
{ {
id: payment.id, id: payment.id,
data: paymentData, data: paymentData.data,
captured_at: isFullyCaptured ? new Date() : undefined, captured_at: isFullyCaptured ? new Date() : undefined,
}, },
sharedContext sharedContext
@@ -817,15 +831,15 @@ export default class PaymentModuleService
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
) { ) {
const paymentData = await this.paymentProviderService_.refundPayment( const paymentData = await this.paymentProviderService_.refundPayment(
payment.provider_id,
{ {
data: payment.data!, data: payment.data!,
provider_id: payment.provider_id, amount: refund.raw_amount as BigNumberInput,
}, }
refund.raw_amount as BigNumberInput
) )
await this.paymentService_.update( await this.paymentService_.update(
{ id: payment.id, data: paymentData }, { id: payment.id, data: paymentData.data },
sharedContext sharedContext
) )
@@ -843,9 +857,8 @@ export default class PaymentModuleService
sharedContext sharedContext
) )
await this.paymentProviderService_.cancelPayment({ await this.paymentProviderService_.cancelPayment(payment.provider_id, {
data: payment.data!, data: payment.data!,
provider_id: payment.provider_id,
}) })
await this.paymentService_.update( await this.paymentService_.update(
@@ -909,6 +922,82 @@ export default class PaymentModuleService
] ]
} }
@InjectManager()
async createAccountHolder(
input: CreateAccountHolderDTO,
@MedusaContext() sharedContext?: Context
): Promise<AccountHolderDTO> {
if (input.context?.account_holder) {
return input.context.account_holder as AccountHolderDTO
}
let accountHolder: InferEntityType<typeof AccountHolder> | undefined
let providerAccountHolder: CreateAccountHolderOutput | undefined
try {
providerAccountHolder =
await this.paymentProviderService_.createAccountHolder(
input.provider_id,
{ context: input.context }
)
// This can be empty when either the method is not supported or an account holder wasn't created
if (isPresent(providerAccountHolder)) {
accountHolder = await this.accountHolderService_.create(
{
external_id: providerAccountHolder.id,
email: input.context.customer?.email,
data: providerAccountHolder.data,
provider_id: input.provider_id,
},
sharedContext
)
}
} catch (error) {
if (providerAccountHolder) {
await this.paymentProviderService_.deleteAccountHolder(
input.provider_id,
{
context: {
account_holder: providerAccountHolder as {
data: Record<string, unknown>
},
},
}
)
}
if (accountHolder) {
await this.accountHolderService_.delete(accountHolder.id, sharedContext)
}
throw error
}
return await this.baseRepository_.serialize(accountHolder)
}
@InjectManager()
async deleteAccountHolder(
id: string,
@MedusaContext() sharedContext?: Context
): Promise<void> {
const accountHolder = await this.accountHolderService_.retrieve(
id,
{ select: ["id", "provider_id", "external_id", "email", "data"] },
sharedContext
)
await this.accountHolderService_.delete(id, sharedContext)
await this.paymentProviderService_.deleteAccountHolder(
accountHolder.provider_id,
{
context: { account_holder: accountHolder },
}
)
}
@InjectManager() @InjectManager()
async listPaymentMethods( async listPaymentMethods(
filters: FilterablePaymentMethodProps, filters: FilterablePaymentMethodProps,
@@ -917,12 +1006,12 @@ export default class PaymentModuleService
): Promise<PaymentMethodDTO[]> { ): Promise<PaymentMethodDTO[]> {
const res = await this.paymentProviderService_.listPaymentMethods( const res = await this.paymentProviderService_.listPaymentMethods(
filters.provider_id, filters.provider_id,
filters.context { context: filters.context }
) )
return res.map((item) => ({ return res.map((item) => ({
id: item.id, id: item.id,
data: item.data, data: item.data!,
provider_id: filters.provider_id, provider_id: filters.provider_id,
})) }))
} }
@@ -936,12 +1025,12 @@ export default class PaymentModuleService
const paymentMethods = const paymentMethods =
await this.paymentProviderService_.listPaymentMethods( await this.paymentProviderService_.listPaymentMethods(
filters.provider_id, filters.provider_id,
filters.context { context: filters.context }
) )
const normalizedResponse = paymentMethods.map((item) => ({ const normalizedResponse = paymentMethods.map((item) => ({
id: item.id, id: item.id,
data: item.data, data: item.data!,
provider_id: filters.provider_id, provider_id: filters.provider_id,
})) }))
@@ -950,9 +1039,9 @@ export default class PaymentModuleService
// @ts-ignore // @ts-ignore
createPaymentMethods( createPaymentMethods(
data: CreatePaymentCollectionDTO, data: CreatePaymentMethodDTO,
sharedContext?: Context sharedContext?: Context
): Promise<PaymentCollectionDTO> ): Promise<PaymentMethodDTO>
createPaymentMethods( createPaymentMethods(
data: CreatePaymentMethodDTO[], data: CreatePaymentMethodDTO[],
@@ -975,7 +1064,7 @@ export default class PaymentModuleService
const normalizedResponse = result.map((item, i) => { const normalizedResponse = result.map((item, i) => {
return { return {
id: item.id, id: item.id,
data: item.data, data: item.data!,
provider_id: input[i].provider_id, provider_id: input[i].provider_id,
} }
}) })
@@ -1,25 +1,36 @@
import { import {
BigNumberInput, AuthorizePaymentInput,
CreatePaymentProviderSession, AuthorizePaymentOutput,
CancelPaymentInput,
CancelPaymentOutput,
CapturePaymentInput,
CapturePaymentOutput,
CreateAccountHolderInput,
CreateAccountHolderOutput,
DAL, DAL,
DeleteAccountHolderInput,
DeleteAccountHolderOutput,
DeletePaymentInput,
DeletePaymentOutput,
GetPaymentStatusInput,
GetPaymentStatusOutput,
InitiatePaymentInput,
InitiatePaymentOutput,
IPaymentProvider, IPaymentProvider,
ListPaymentMethodsInput,
ListPaymentMethodsOutput,
Logger, Logger,
PaymentMethodResponse,
PaymentProviderAuthorizeResponse,
PaymentProviderContext,
PaymentProviderDataInput,
PaymentProviderError,
PaymentProviderSessionResponse,
PaymentSessionStatus,
ProviderWebhookPayload, ProviderWebhookPayload,
SavePaymentMethod, RefundPaymentInput,
SavePaymentMethodResponse, RefundPaymentOutput,
UpdatePaymentProviderSession, SavePaymentMethodInput,
SavePaymentMethodOutput,
UpdatePaymentInput,
UpdatePaymentOutput,
WebhookActionResult, WebhookActionResult,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { MedusaError, ModulesSdkUtils } from "@medusajs/framework/utils" import { ModulesSdkUtils } from "@medusajs/framework/utils"
import { PaymentProvider } from "@models" import { PaymentProvider } from "@models"
import { EOL } from "os"
type InjectedDependencies = { type InjectedDependencies = {
logger?: Logger logger?: Logger
@@ -59,135 +70,128 @@ Please make sure that the provider is registered in the container and it is conf
async createSession( async createSession(
providerId: string, providerId: string,
sessionInput: CreatePaymentProviderSession sessionInput: InitiatePaymentInput
): Promise<PaymentProviderSessionResponse["data"]> { ): Promise<InitiatePaymentOutput> {
const provider = this.retrieveProvider(providerId) const provider = this.retrieveProvider(providerId)
const paymentResponse = await provider.initiatePayment(sessionInput) return await provider.initiatePayment(sessionInput)
if (isPaymentProviderError(paymentResponse)) {
this.throwPaymentProviderError(paymentResponse)
}
return (paymentResponse as PaymentProviderSessionResponse).data
} }
async updateSession( async updateSession(
providerId: string, providerId: string,
sessionInput: UpdatePaymentProviderSession sessionInput: UpdatePaymentInput
): Promise<PaymentProviderSessionResponse["data"]> { ): Promise<UpdatePaymentOutput> {
const provider = this.retrieveProvider(providerId) const provider = this.retrieveProvider(providerId)
const paymentResponse = await provider.updatePayment(sessionInput) return await provider.updatePayment(sessionInput)
if (isPaymentProviderError(paymentResponse)) {
this.throwPaymentProviderError(paymentResponse)
} }
return (paymentResponse as PaymentProviderSessionResponse)?.data async deleteSession(
} providerId: string,
input: DeletePaymentInput
async deleteSession(input: PaymentProviderDataInput): Promise<void> { ): Promise<DeletePaymentOutput> {
const provider = this.retrieveProvider(input.provider_id) const provider = this.retrieveProvider(providerId)
return await provider.deletePayment(input)
const error = await provider.deletePayment(input.data)
if (isPaymentProviderError(error)) {
this.throwPaymentProviderError(error)
}
} }
async authorizePayment( async authorizePayment(
input: PaymentProviderDataInput, providerId: string,
context: Record<string, unknown> input: AuthorizePaymentInput
): Promise<{ data: Record<string, unknown>; status: PaymentSessionStatus }> { ): Promise<AuthorizePaymentOutput> {
const provider = this.retrieveProvider(input.provider_id) const provider = this.retrieveProvider(providerId)
return await provider.authorizePayment(input)
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( async getStatus(
input: PaymentProviderDataInput providerId: string,
): Promise<PaymentSessionStatus> { input: GetPaymentStatusInput
const provider = this.retrieveProvider(input.provider_id) ): Promise<GetPaymentStatusOutput> {
return await provider.getPaymentStatus(input.data) const provider = this.retrieveProvider(providerId)
return await provider.getPaymentStatus(input)
} }
async capturePayment( async capturePayment(
input: PaymentProviderDataInput providerId: string,
): Promise<Record<string, unknown>> { input: CapturePaymentInput
const provider = this.retrieveProvider(input.provider_id) ): Promise<CapturePaymentOutput> {
const provider = this.retrieveProvider(providerId)
const res = await provider.capturePayment(input.data) return await provider.capturePayment(input)
if (isPaymentProviderError(res)) {
this.throwPaymentProviderError(res)
} }
return res as Record<string, unknown> async cancelPayment(
} providerId: string,
input: CancelPaymentInput
async cancelPayment(input: PaymentProviderDataInput): Promise<void> { ): Promise<CancelPaymentOutput> {
const provider = this.retrieveProvider(input.provider_id) const provider = this.retrieveProvider(providerId)
return await provider.cancelPayment(input)
const error = await provider.cancelPayment(input.data)
if (isPaymentProviderError(error)) {
this.throwPaymentProviderError(error)
}
} }
async refundPayment( async refundPayment(
input: PaymentProviderDataInput, providerId: string,
amount: BigNumberInput input: RefundPaymentInput
): Promise<Record<string, unknown>> { ): Promise<RefundPaymentOutput> {
const provider = this.retrieveProvider(input.provider_id) const provider = this.retrieveProvider(providerId)
return await provider.refundPayment(input)
const res = await provider.refundPayment(input.data, amount)
if (isPaymentProviderError(res)) {
this.throwPaymentProviderError(res)
} }
return res as Record<string, unknown> async createAccountHolder(
providerId: string,
input: CreateAccountHolderInput
): Promise<CreateAccountHolderOutput> {
const provider = this.retrieveProvider(providerId)
if (!provider.createAccountHolder) {
this.#logger.warn(
`Provider ${providerId} does not support creating account holders`
)
return {} as unknown as CreateAccountHolderOutput
}
return await provider.createAccountHolder(input)
}
async deleteAccountHolder(
providerId: string,
input: DeleteAccountHolderInput
): Promise<DeleteAccountHolderOutput> {
const provider = this.retrieveProvider(providerId)
if (!provider.deleteAccountHolder) {
this.#logger.warn(
`Provider ${providerId} does not support deleting account holders`
)
return {}
}
return await provider.deleteAccountHolder(input)
} }
async listPaymentMethods( async listPaymentMethods(
providerId: string, providerId: string,
context: PaymentProviderContext input: ListPaymentMethodsInput
): Promise<PaymentMethodResponse[]> { ): Promise<ListPaymentMethodsOutput> {
const provider = this.retrieveProvider(providerId) const provider = this.retrieveProvider(providerId)
if (!provider.listPaymentMethods) { if (!provider.listPaymentMethods) {
throw new MedusaError( this.#logger.warn(
MedusaError.Types.INVALID_DATA,
`Provider ${providerId} does not support listing payment methods` `Provider ${providerId} does not support listing payment methods`
) )
return []
} }
return await provider.listPaymentMethods(context) return await provider.listPaymentMethods(input)
} }
async savePaymentMethod( async savePaymentMethod(
providerId: string, providerId: string,
input: SavePaymentMethod input: SavePaymentMethodInput
): Promise<SavePaymentMethodResponse> { ): Promise<SavePaymentMethodOutput> {
const provider = this.retrieveProvider(providerId) const provider = this.retrieveProvider(providerId)
if (!provider.savePaymentMethod) { if (!provider.savePaymentMethod) {
throw new MedusaError( this.#logger.warn(
MedusaError.Types.INVALID_DATA,
`Provider ${providerId} does not support saving payment methods` `Provider ${providerId} does not support saving payment methods`
) )
return {} as unknown as SavePaymentMethodOutput
} }
const res = await provider.savePaymentMethod(input) return await provider.savePaymentMethod(input)
if (isPaymentProviderError(res)) {
this.throwPaymentProviderError(res)
}
return res as SavePaymentMethodResponse
} }
async getWebhookActionAndData( async getWebhookActionAndData(
@@ -198,22 +202,4 @@ Please make sure that the provider is registered in the container and it is conf
return await provider.getWebhookActionAndData(data) return await provider.getWebhookActionAndData(data)
} }
private throwPaymentProviderError(errObj: PaymentProviderError) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`${errObj.error}${errObj.detail ? `:${EOL}${errObj.detail}` : ""}`,
errObj.code
)
}
}
function isPaymentProviderError(obj: any): obj is PaymentProviderError {
return (
obj &&
typeof obj === "object" &&
"error" in obj &&
"code" in obj &&
"detail" in obj
)
} }
@@ -1,15 +1,33 @@
import Stripe from "stripe" import Stripe from "stripe"
import { import {
CreatePaymentProviderSession, AuthorizePaymentInput,
PaymentMethodResponse, AuthorizePaymentOutput,
PaymentProviderContext, CancelPaymentInput,
PaymentProviderError, CancelPaymentOutput,
PaymentProviderSessionResponse, CapturePaymentInput,
CapturePaymentOutput,
CreateAccountHolderInput,
CreateAccountHolderOutput,
DeleteAccountHolderInput,
DeleteAccountHolderOutput,
DeletePaymentInput,
DeletePaymentOutput,
GetPaymentStatusInput,
GetPaymentStatusOutput,
InitiatePaymentInput,
InitiatePaymentOutput,
ListPaymentMethodsInput,
ListPaymentMethodsOutput,
ProviderWebhookPayload, ProviderWebhookPayload,
SavePaymentMethod, RefundPaymentInput,
SavePaymentMethodResponse, RefundPaymentOutput,
UpdatePaymentProviderSession, RetrievePaymentInput,
RetrievePaymentOutput,
SavePaymentMethodInput,
SavePaymentMethodOutput,
UpdatePaymentInput,
UpdatePaymentOutput,
WebhookActionResult, WebhookActionResult,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
@@ -95,62 +113,55 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
return res return res
} }
async getPaymentStatus( async getPaymentStatus({
paymentSessionData: Record<string, unknown> data,
): Promise<PaymentSessionStatus> { }: GetPaymentStatusInput): Promise<GetPaymentStatusOutput> {
const id = paymentSessionData.id as string const id = data?.id as string
if (!id) {
throw this.buildError(
"No payment intent ID provided while getting payment status",
new Error("No payment intent ID provided")
)
}
const paymentIntent = await this.stripe_.paymentIntents.retrieve(id) const paymentIntent = await this.stripe_.paymentIntents.retrieve(id)
switch (paymentIntent.status) { switch (paymentIntent.status) {
case "requires_payment_method": case "requires_payment_method":
case "requires_confirmation": case "requires_confirmation":
case "processing": case "processing":
return PaymentSessionStatus.PENDING return { status: PaymentSessionStatus.PENDING }
case "requires_action": case "requires_action":
return PaymentSessionStatus.REQUIRES_MORE return { status: PaymentSessionStatus.REQUIRES_MORE }
case "canceled": case "canceled":
return PaymentSessionStatus.CANCELED return { status: PaymentSessionStatus.CANCELED }
case "requires_capture": case "requires_capture":
return PaymentSessionStatus.AUTHORIZED return { status: PaymentSessionStatus.AUTHORIZED }
case "succeeded": case "succeeded":
return PaymentSessionStatus.CAPTURED return { status: PaymentSessionStatus.CAPTURED }
default: default:
return PaymentSessionStatus.PENDING return { status: PaymentSessionStatus.PENDING }
} }
} }
async initiatePayment( async initiatePayment({
input: CreatePaymentProviderSession currency_code,
): Promise<PaymentProviderError | PaymentProviderSessionResponse> { amount,
const { email, extra, session_id, customer } = input.context data,
const { currency_code, amount } = input context,
}: InitiatePaymentInput): Promise<InitiatePaymentOutput> {
const additionalParameters = this.normalizePaymentIntentParameters(extra) const additionalParameters = this.normalizePaymentIntentParameters(data)
const intentRequest: Stripe.PaymentIntentCreateParams = { const intentRequest: Stripe.PaymentIntentCreateParams = {
amount: getSmallestUnit(amount, currency_code), amount: getSmallestUnit(amount, currency_code),
currency: currency_code, currency: currency_code,
metadata: { session_id: session_id! }, metadata: { session_id: data?.session_id as string },
...additionalParameters, ...additionalParameters,
} }
if (customer?.metadata?.stripe_id) { intentRequest.customer = context?.account_holder?.data?.id as
intentRequest.customer = customer.metadata.stripe_id as string | string
} else { | undefined
let stripeCustomer
try {
stripeCustomer = await this.stripe_.customers.create({
email,
})
} catch (e) {
return this.buildError(
"An error occurred in initiatePayment when creating a Stripe customer",
e
)
}
intentRequest.customer = stripeCustomer.id
}
let sessionData let sessionData
try { try {
@@ -158,158 +169,215 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
intentRequest intentRequest
)) as unknown as Record<string, unknown> )) as unknown as Record<string, unknown>
} catch (e) { } catch (e) {
return this.buildError( throw 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 e
) )
} }
return { return {
id: sessionData.id,
data: sessionData, data: sessionData,
// TODO: REVISIT
// update_requests: customer?.metadata?.stripe_id
// ? undefined
// : {
// customer_metadata: {
// stripe_id: intentRequest.customer,
// },
// },
} }
} }
async authorizePayment( async authorizePayment(
paymentSessionData: Record<string, unknown>, input: AuthorizePaymentInput
context: Record<string, unknown> ): Promise<AuthorizePaymentOutput> {
): Promise< const statusResponse = await this.getPaymentStatus(input)
| PaymentProviderError return statusResponse
| {
status: PaymentSessionStatus
data: PaymentProviderSessionResponse["data"]
}
> {
const status = await this.getPaymentStatus(paymentSessionData)
return { data: paymentSessionData, status }
} }
async cancelPayment( async cancelPayment({
paymentSessionData: Record<string, unknown> data,
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> { }: CancelPaymentInput): Promise<CancelPaymentOutput> {
try { try {
const id = paymentSessionData.id as string const id = data?.id as string
if (!id) { if (!id) {
return paymentSessionData return { data: data }
} }
return (await this.stripe_.paymentIntents.cancel( const res = await this.stripe_.paymentIntents.cancel(id)
id return { data: res as unknown as Record<string, unknown> }
)) as unknown as PaymentProviderSessionResponse["data"]
} catch (error) { } catch (error) {
if (error.payment_intent?.status === ErrorIntentStatus.CANCELED) { if (error.payment_intent?.status === ErrorIntentStatus.CANCELED) {
return error.payment_intent return { data: error.payment_intent }
} }
return this.buildError("An error occurred in cancelPayment", error) throw this.buildError("An error occurred in cancelPayment", error)
} }
} }
async capturePayment( async capturePayment({
paymentSessionData: Record<string, unknown> data,
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> { }: CapturePaymentInput): Promise<CapturePaymentOutput> {
const id = paymentSessionData.id as string const id = data?.id as string
try { try {
const intent = await this.stripe_.paymentIntents.capture(id) const intent = await this.stripe_.paymentIntents.capture(id)
return intent as unknown as PaymentProviderSessionResponse["data"] return { data: intent as unknown as Record<string, unknown> }
} catch (error) { } catch (error) {
if (error.code === ErrorCodes.PAYMENT_INTENT_UNEXPECTED_STATE) { if (error.code === ErrorCodes.PAYMENT_INTENT_UNEXPECTED_STATE) {
if (error.payment_intent?.status === ErrorIntentStatus.SUCCEEDED) { if (error.payment_intent?.status === ErrorIntentStatus.SUCCEEDED) {
return error.payment_intent return { data: error.payment_intent }
} }
} }
return this.buildError("An error occurred in capturePayment", error) throw this.buildError("An error occurred in capturePayment", error)
} }
} }
async deletePayment( async deletePayment(input: DeletePaymentInput): Promise<DeletePaymentOutput> {
paymentSessionData: Record<string, unknown> return await this.cancelPayment(input)
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> {
return await this.cancelPayment(paymentSessionData)
} }
async refundPayment( async refundPayment({
paymentSessionData: Record<string, unknown>, amount,
refundAmount: number data,
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> { }: RefundPaymentInput): Promise<RefundPaymentOutput> {
const id = paymentSessionData.id as string const id = data?.id as string
if (!id) {
throw this.buildError(
"No payment intent ID provided while refunding payment",
new Error("No payment intent ID provided")
)
}
try { try {
const { currency } = paymentSessionData const currencyCode = data?.currency as string
await this.stripe_.refunds.create({ await this.stripe_.refunds.create({
amount: getSmallestUnit(refundAmount, currency as string), amount: getSmallestUnit(amount, currencyCode),
payment_intent: id as string, payment_intent: id as string,
}) })
} catch (e) { } catch (e) {
return this.buildError("An error occurred in refundPayment", e) throw this.buildError("An error occurred in refundPayment", e)
} }
return paymentSessionData return { data }
} }
async retrievePayment( async retrievePayment({
paymentSessionData: Record<string, unknown> data,
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> { }: RetrievePaymentInput): Promise<RetrievePaymentOutput> {
try { try {
const id = paymentSessionData.id as string const id = data?.id as string
const intent = await this.stripe_.paymentIntents.retrieve(id) const intent = await this.stripe_.paymentIntents.retrieve(id)
intent.amount = getAmountFromSmallestUnit(intent.amount, intent.currency) intent.amount = getAmountFromSmallestUnit(intent.amount, intent.currency)
return intent as unknown as PaymentProviderSessionResponse["data"] return { data: intent as unknown as Record<string, unknown> }
} catch (e) { } catch (e) {
return this.buildError("An error occurred in retrievePayment", e) throw this.buildError("An error occurred in retrievePayment", e)
} }
} }
async updatePayment( async updatePayment({
input: UpdatePaymentProviderSession data,
): Promise<PaymentProviderError | PaymentProviderSessionResponse> { currency_code,
const { context, data, currency_code, amount } = input amount,
}: UpdatePaymentInput): Promise<UpdatePaymentOutput> {
const amountNumeric = getSmallestUnit(amount, currency_code) const amountNumeric = getSmallestUnit(amount, currency_code)
if (isPresent(amount) && data?.amount === amountNumeric) {
const stripeId = context.customer?.metadata?.stripe_id
if (stripeId !== data.customer) {
return await this.initiatePayment(input)
} else {
if (isPresent(amount) && data.amount === amountNumeric) {
return { data } return { data }
} }
try { try {
const id = data.id as string const id = data?.id as string
const sessionData = (await this.stripe_.paymentIntents.update(id, { const sessionData = (await this.stripe_.paymentIntents.update(id, {
amount: amountNumeric, amount: amountNumeric,
})) as unknown as PaymentProviderSessionResponse["data"] })) as unknown as Record<string, unknown>
return { data: sessionData } return { data: sessionData }
} catch (e) { } catch (e) {
return this.buildError("An error occurred in updatePayment", e) throw this.buildError("An error occurred in updatePayment", e)
}
} }
} }
async listPaymentMethods( async createAccountHolder({
context: PaymentProviderContext context,
): Promise<PaymentMethodResponse[]> { }: CreateAccountHolderInput): Promise<CreateAccountHolderOutput> {
const customerId = context.customer?.metadata?.stripe_id const { account_holder, customer } = context
if (!customerId) {
if (account_holder?.data?.id) {
return { id: account_holder.data.id as string }
}
if (!customer) {
throw this.buildError(
"No customer in context",
new Error("No customer provided while creating account holder")
)
}
const shipping = customer.billing_address
? ({
address: {
city: customer.billing_address.city,
country: customer.billing_address.country_code,
line1: customer.billing_address.address_1,
line2: customer.billing_address.address_2,
postal_code: customer.billing_address.postal_code,
state: customer.billing_address.province,
},
} as Stripe.CustomerCreateParams.Shipping)
: undefined
try {
const stripeCustomer = await this.stripe_.customers.create({
email: customer.email,
name:
customer.company_name ||
`${customer.first_name ?? ""} ${customer.last_name ?? ""}`.trim() ||
undefined,
phone: customer.phone as string | undefined,
...shipping,
})
return {
id: stripeCustomer.id,
data: stripeCustomer as unknown as Record<string, unknown>,
}
} catch (e) {
throw this.buildError(
"An error occurred in createAccountHolder when creating a Stripe customer",
e
)
}
}
async deleteAccountHolder({
context,
}: DeleteAccountHolderInput): Promise<DeleteAccountHolderOutput> {
const { account_holder } = context
const accountHolderId = account_holder?.data?.id as string | undefined
if (!accountHolderId) {
throw this.buildError(
"No account holder in context",
new Error("No account holder provided while deleting account holder")
)
}
try {
await this.stripe_.customers.del(accountHolderId)
return {}
} catch (e) {
throw this.buildError("An error occurred in deleteAccountHolder", e)
}
}
async listPaymentMethods({
context,
}: ListPaymentMethodsInput): Promise<ListPaymentMethodsOutput> {
const accountHolderId = context?.account_holder?.data?.id as
| string
| undefined
if (!accountHolderId) {
return [] return []
} }
const paymentMethods = await this.stripe_.customers.listPaymentMethods( const paymentMethods = await this.stripe_.customers.listPaymentMethods(
customerId as string, accountHolderId,
// In order to keep the interface simple, we just list the maximum payment methods, which should be enough in almost all cases. // In order to keep the interface simple, we just list the maximum payment methods, which should be enough in almost all cases.
// We can always extend the interface to allow additional filtering, if necessary. // We can always extend the interface to allow additional filtering, if necessary.
{ limit: 100 } { limit: 100 }
@@ -321,21 +389,23 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
})) }))
} }
async savePaymentMethod( async savePaymentMethod({
input: SavePaymentMethod context,
): Promise<PaymentProviderError | SavePaymentMethodResponse> { data,
const { context, data } = input }: SavePaymentMethodInput): Promise<SavePaymentMethodOutput> {
const customer = context?.customer const accountHolderId = context?.account_holder?.data?.id as
| string
| undefined
if (!customer?.metadata?.stripe_id) { if (!accountHolderId) {
return this.buildError( throw this.buildError(
"Account holder not set while saving a payment method", "Account holder not set while saving a payment method",
new Error("Missing account holder") new Error("Missing account holder")
) )
} }
const resp = await this.stripe_.setupIntents.create({ const resp = await this.stripe_.setupIntents.create({
customer: customer.metadata.stripe_id as string, customer: accountHolderId,
...data, ...data,
}) })
@@ -397,18 +467,15 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
this.options_.webhookSecret this.options_.webhookSecret
) )
} }
protected buildError(message: string, error: Error): PaymentProviderError { protected buildError(message: string, error: Error): Error {
const errorDetails = const errorDetails =
"raw" in error ? (error.raw as Stripe.StripeRawError) : error "raw" in error ? (error.raw as Stripe.StripeRawError) : error
return { return new Error(
error: `${message}: ${error.message}`, `${message}: ${error.message}. ${
code: "code" in errorDetails ? errorDetails.code : "unknown", "detail" in errorDetails ? errorDetails.detail : ""
detail: }`.trim()
"detail" in errorDetails )
? `${error.message}: ${errorDetails.detail}`
: error.message,
}
} }
} }