feat(medusa): Load PaymentProcessors + integrate in PaymentProviderService (#2978)
* feat: Add payment process support into the loader and payment provider * WIP * feat: continue payment provider alignment * fix tests and defer payment service resolution * continue to add support to payment provider * continue to add support to payment provider * fix fixtures * chore: add updateSessionData unsupported error * chore: Adress feedback * chore: Adress feedback * chore: fix default loader * cleanup * cleanup * fix unit tests * Create purple-sloths-confess.md * address feedback * minor changes * fix unit test --------- Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
1c40346e9e
commit
f43e9f0f20
8
.changeset/purple-sloths-confess.md
Normal file
8
.changeset/purple-sloths-confess.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
feat(medusa): Load PaymentProcessors
|
||||
- Add loading of PaymentProcessors
|
||||
- Add PaymentProcessor support in the payment-provider
|
||||
- Add backward compatibility for the PaymentService
|
||||
@@ -10,4 +10,5 @@ export * from "./models/base-entity"
|
||||
export * from "./models/soft-deletable-entity"
|
||||
export * from "./search-service"
|
||||
export * from "./payment-service"
|
||||
export * from "./payment-processor"
|
||||
export * from "./services"
|
||||
|
||||
@@ -6,21 +6,21 @@ export type PaymentProcessorContext = {
|
||||
email: string
|
||||
currency_code: string
|
||||
amount: number
|
||||
resource_id?: string
|
||||
resource_id: string
|
||||
customer?: Customer
|
||||
context: Record<string, unknown>
|
||||
paymentSessionData: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type PaymentProcessorSessionResponse = {
|
||||
update_requests: { customer_metadata: Record<string, unknown> }
|
||||
update_requests?: { customer_metadata?: Record<string, unknown> }
|
||||
session_data: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface PaymentProcessorError {
|
||||
error: string
|
||||
code: number
|
||||
details: any
|
||||
code?: string
|
||||
detail?: any
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,42 +51,60 @@ export interface PaymentProcessor {
|
||||
*/
|
||||
updatePayment(
|
||||
context: PaymentProcessorContext
|
||||
): Promise<PaymentProcessorError | void>
|
||||
): Promise<PaymentProcessorError | PaymentProcessorSessionResponse | void>
|
||||
|
||||
/**
|
||||
* Refund an existing session
|
||||
* @param context
|
||||
* @param paymentSessionData
|
||||
* @param refundAmount
|
||||
*/
|
||||
refundPayment(
|
||||
context: PaymentProcessorContext
|
||||
): Promise<PaymentProcessorError | void>
|
||||
paymentSessionData: Record<string, unknown>,
|
||||
refundAmount: number
|
||||
): Promise<
|
||||
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||
>
|
||||
|
||||
/**
|
||||
* Authorize an existing session if it is not already authorized
|
||||
* @param paymentSessionData
|
||||
* @param context
|
||||
*/
|
||||
authorizePayment(
|
||||
context: PaymentProcessorContext
|
||||
): Promise<PaymentProcessorError | void>
|
||||
paymentSessionData: Record<string, unknown>,
|
||||
context: Record<string, unknown>
|
||||
): Promise<
|
||||
| PaymentProcessorError
|
||||
| {
|
||||
status: PaymentSessionStatus
|
||||
data: PaymentProcessorSessionResponse["session_data"]
|
||||
}
|
||||
>
|
||||
|
||||
/**
|
||||
* Capture an existing session
|
||||
* @param context
|
||||
* @param paymentSessionData
|
||||
*/
|
||||
capturePayment(
|
||||
context: PaymentProcessorContext
|
||||
): Promise<PaymentProcessorError | void>
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<
|
||||
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||
>
|
||||
|
||||
/**
|
||||
* Delete an existing session
|
||||
*/
|
||||
deletePayment(paymentId: string): Promise<PaymentProcessorError | void>
|
||||
deletePayment(
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<
|
||||
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||
>
|
||||
|
||||
/**
|
||||
* Retrieve an existing session
|
||||
*/
|
||||
retrievePayment(
|
||||
paymentId: string
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<
|
||||
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||
>
|
||||
@@ -94,12 +112,18 @@ export interface PaymentProcessor {
|
||||
/**
|
||||
* Cancel an existing session
|
||||
*/
|
||||
cancelPayment(paymentId: string): Promise<PaymentProcessorError | void>
|
||||
cancelPayment(
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<
|
||||
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||
>
|
||||
|
||||
/**
|
||||
* Return the status of the session
|
||||
*/
|
||||
getPaymentStatus(paymentId: string): Promise<PaymentSessionStatus>
|
||||
getPaymentStatus(
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<PaymentSessionStatus>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,7 +135,7 @@ export abstract class AbstractPaymentProcessor implements PaymentProcessor {
|
||||
protected readonly config?: Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
) {}
|
||||
|
||||
protected static identifier: string
|
||||
public static identifier: string
|
||||
|
||||
public getIdentifier(): string {
|
||||
const ctr = this.constructor as typeof AbstractPaymentProcessor
|
||||
@@ -126,40 +150,58 @@ export abstract class AbstractPaymentProcessor implements PaymentProcessor {
|
||||
abstract init(): Promise<void>
|
||||
|
||||
abstract capturePayment(
|
||||
context: PaymentProcessorContext
|
||||
): Promise<PaymentProcessorError | void>
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<
|
||||
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||
>
|
||||
|
||||
abstract authorizePayment(
|
||||
context: PaymentProcessorContext
|
||||
): Promise<PaymentProcessorError | void>
|
||||
paymentSessionData: Record<string, unknown>,
|
||||
context: Record<string, unknown>
|
||||
): Promise<
|
||||
| PaymentProcessorError
|
||||
| {
|
||||
status: PaymentSessionStatus
|
||||
data: PaymentProcessorSessionResponse["session_data"]
|
||||
}
|
||||
>
|
||||
|
||||
abstract cancelPayment(
|
||||
paymentId: string
|
||||
): Promise<PaymentProcessorError | void>
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<
|
||||
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||
>
|
||||
|
||||
abstract initiatePayment(
|
||||
context: PaymentProcessorContext
|
||||
): Promise<PaymentProcessorError | PaymentProcessorSessionResponse>
|
||||
|
||||
abstract deletePayment(
|
||||
paymentId: string
|
||||
): Promise<PaymentProcessorError | void>
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<
|
||||
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||
>
|
||||
|
||||
abstract getPaymentStatus(paymentId: string): Promise<PaymentSessionStatus>
|
||||
abstract getPaymentStatus(
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<PaymentSessionStatus>
|
||||
|
||||
abstract refundPayment(
|
||||
context: PaymentProcessorContext
|
||||
): Promise<PaymentProcessorError | void>
|
||||
paymentSessionData: Record<string, unknown>,
|
||||
refundAmount: number
|
||||
): Promise<
|
||||
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||
>
|
||||
|
||||
abstract retrievePayment(
|
||||
paymentId: string
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<
|
||||
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||
>
|
||||
|
||||
abstract updatePayment(
|
||||
context: PaymentProcessorContext
|
||||
): Promise<PaymentProcessorError | void>
|
||||
): Promise<PaymentProcessorError | PaymentProcessorSessionResponse | void>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,3 +211,13 @@ export abstract class AbstractPaymentProcessor implements PaymentProcessor {
|
||||
export function isPaymentProcessor(obj: unknown): boolean {
|
||||
return obj instanceof AbstractPaymentProcessor
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to determine if an object is a processor error
|
||||
* @param obj
|
||||
*/
|
||||
export function isPaymentProcessorError(
|
||||
obj: any
|
||||
): obj is PaymentProcessorError {
|
||||
return obj && typeof obj === "object" && (obj.error || obj.code || obj.detail)
|
||||
}
|
||||
|
||||
@@ -21,11 +21,13 @@ export type PaymentContext = {
|
||||
email: string
|
||||
shipping_address: Address | null
|
||||
shipping_methods: ShippingMethod[]
|
||||
billing_address?: Address | null
|
||||
}
|
||||
currency_code: string
|
||||
amount: number
|
||||
resource_id?: string
|
||||
resource_id: string
|
||||
customer?: Customer
|
||||
paymentSessionData: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type PaymentSessionResponse = {
|
||||
@@ -148,7 +150,7 @@ export abstract class AbstractPaymentService
|
||||
super(container, config)
|
||||
}
|
||||
|
||||
protected static identifier: string
|
||||
public static identifier: string
|
||||
|
||||
public getIdentifier(): string {
|
||||
if (!(this.constructor as typeof AbstractPaymentService).identifier) {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { asValue, createContainer } from "awilix"
|
||||
import { MockManager, MockRepository } from "medusa-test-utils"
|
||||
import { StoreServiceMock } from "../../services/__mocks__/store"
|
||||
import { ShippingProfileServiceMock } from "../../services/__mocks__/shipping-profile"
|
||||
import {
|
||||
ShippingProfileServiceMock
|
||||
} from "../../services/__mocks__/shipping-profile"
|
||||
import Logger from "../logger"
|
||||
import featureFlagsLoader from "../feature-flags"
|
||||
import { default as defaultLoader } from "../defaults"
|
||||
import { SalesChannelServiceMock } from "../../services/__mocks__/sales-channel"
|
||||
import { PaymentProviderServiceMock } from "../../services/__mocks__/payment-provider"
|
||||
import {
|
||||
PaymentProviderServiceMock
|
||||
} from "../../services/__mocks__/payment-provider"
|
||||
|
||||
describe("default", () => {
|
||||
describe("sales channel default", () => {
|
||||
@@ -40,14 +44,23 @@ describe("default", () => {
|
||||
paymentProviderService: asValue(PaymentProviderServiceMock),
|
||||
notificationProviders: asValue([]),
|
||||
notificationService: asValue({
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
registerInstalledProviders: jest.fn(),
|
||||
}),
|
||||
fulfillmentProviders: asValue([]),
|
||||
fulfillmentProviderService: asValue({
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
registerInstalledProviders: jest.fn(),
|
||||
}),
|
||||
taxProviders: asValue([]),
|
||||
taxProviderService: asValue({
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
registerInstalledProviders: jest.fn(),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -21,7 +21,11 @@ import {
|
||||
import { CurrencyRepository } from "../repositories/currency"
|
||||
import { FlagRouter } from "../utils/flag-router"
|
||||
import SalesChannelFeatureFlag from "./feature-flags/sales-channels"
|
||||
import { AbstractPaymentService, AbstractTaxService } from "../interfaces"
|
||||
import {
|
||||
AbstractPaymentProcessor,
|
||||
AbstractPaymentService,
|
||||
AbstractTaxService,
|
||||
} from "../interfaces"
|
||||
|
||||
const silentResolution = <T>(
|
||||
container: AwilixContainer,
|
||||
@@ -122,66 +126,167 @@ export default async ({
|
||||
|
||||
await entityManager.transaction(async (manager: EntityManager) => {
|
||||
await storeService.withTransaction(manager).create()
|
||||
const profileServiceTx = profileService.withTransaction(manager)
|
||||
|
||||
const payProviders =
|
||||
silentResolution<(typeof BasePaymentService | AbstractPaymentService)[]>(
|
||||
container,
|
||||
"paymentProviders",
|
||||
logger
|
||||
) || []
|
||||
const payIds = payProviders.map((p) => p.getIdentifier())
|
||||
const context = { container, manager, logger }
|
||||
|
||||
const pProviderService = container.resolve<PaymentProviderService>(
|
||||
"paymentProviderService"
|
||||
)
|
||||
await pProviderService.registerInstalledProviders(payIds)
|
||||
await Promise.all([
|
||||
registerPaymentProvider(context),
|
||||
registerPaymentProcessor(context),
|
||||
registerNotificationProvider(context),
|
||||
registerFulfillmentProvider(context),
|
||||
registerTaxProvider(context),
|
||||
profileServiceTx.createDefault(),
|
||||
profileServiceTx.createGiftCardDefault(),
|
||||
(async () => {
|
||||
const isSalesChannelEnabled = featureFlagRouter.isFeatureEnabled(
|
||||
SalesChannelFeatureFlag.key
|
||||
)
|
||||
if (isSalesChannelEnabled) {
|
||||
return await salesChannelService
|
||||
.withTransaction(manager)
|
||||
.createDefault()
|
||||
}
|
||||
|
||||
const notiProviders =
|
||||
silentResolution<typeof BaseNotificationService[]>(
|
||||
container,
|
||||
"notificationProviders",
|
||||
logger
|
||||
) || []
|
||||
const notiIds = notiProviders.map((p) => p.getIdentifier())
|
||||
|
||||
const nProviderService = container.resolve<NotificationService>(
|
||||
"notificationService"
|
||||
)
|
||||
await nProviderService.registerInstalledProviders(notiIds)
|
||||
|
||||
const fulfilProviders =
|
||||
silentResolution<typeof BaseFulfillmentService[]>(
|
||||
container,
|
||||
"fulfillmentProviders",
|
||||
logger
|
||||
) || []
|
||||
const fulfilIds = fulfilProviders.map((p) => p.getIdentifier())
|
||||
|
||||
const fProviderService = container.resolve<FulfillmentProviderService>(
|
||||
"fulfillmentProviderService"
|
||||
)
|
||||
await fProviderService.registerInstalledProviders(fulfilIds)
|
||||
|
||||
const taxProviders =
|
||||
silentResolution<AbstractTaxService[]>(
|
||||
container,
|
||||
"taxProviders",
|
||||
logger
|
||||
) || []
|
||||
const taxIds = taxProviders.map((p) => p.getIdentifier())
|
||||
|
||||
const tProviderService =
|
||||
container.resolve<TaxProviderService>("taxProviderService")
|
||||
await tProviderService.registerInstalledProviders(taxIds)
|
||||
|
||||
await profileService.withTransaction(manager).createDefault()
|
||||
await profileService.withTransaction(manager).createGiftCardDefault()
|
||||
|
||||
const isSalesChannelEnabled = featureFlagRouter.isFeatureEnabled(
|
||||
SalesChannelFeatureFlag.key
|
||||
)
|
||||
if (isSalesChannelEnabled) {
|
||||
await salesChannelService.withTransaction(manager).createDefault()
|
||||
}
|
||||
return
|
||||
})(),
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
async function registerPaymentProvider({
|
||||
manager,
|
||||
container,
|
||||
logger,
|
||||
}: {
|
||||
container: AwilixContainer
|
||||
manager: EntityManager
|
||||
logger: Logger
|
||||
}): Promise<void> {
|
||||
const payProviders = (
|
||||
silentResolution<
|
||||
(
|
||||
| typeof BasePaymentService
|
||||
| AbstractPaymentService
|
||||
| AbstractPaymentProcessor
|
||||
)[]
|
||||
>(container, "paymentProviders", logger) || []
|
||||
).filter((provider) => !(provider instanceof AbstractPaymentProcessor))
|
||||
|
||||
const payIds = payProviders.map((paymentProvider) => {
|
||||
return paymentProvider.getIdentifier()
|
||||
})
|
||||
|
||||
const pProviderService = container.resolve<PaymentProviderService>(
|
||||
"paymentProviderService"
|
||||
)
|
||||
await pProviderService
|
||||
.withTransaction(manager)
|
||||
.registerInstalledProviders(payIds)
|
||||
}
|
||||
|
||||
async function registerPaymentProcessor({
|
||||
manager,
|
||||
container,
|
||||
logger,
|
||||
}: {
|
||||
container: AwilixContainer
|
||||
manager: EntityManager
|
||||
logger: Logger
|
||||
}): Promise<void> {
|
||||
const payProviders = (
|
||||
silentResolution<
|
||||
(
|
||||
| typeof BasePaymentService
|
||||
| AbstractPaymentService
|
||||
| AbstractPaymentProcessor
|
||||
)[]
|
||||
>(container, "paymentProviders", logger) || []
|
||||
).filter((provider) => provider instanceof AbstractPaymentProcessor)
|
||||
|
||||
const payIds: string[] = []
|
||||
await Promise.all(
|
||||
payProviders.map((paymentProvider) => {
|
||||
payIds.push(paymentProvider.getIdentifier())
|
||||
return paymentProvider.init()
|
||||
})
|
||||
)
|
||||
|
||||
const pProviderService = container.resolve<PaymentProviderService>(
|
||||
"paymentProviderService"
|
||||
)
|
||||
await pProviderService
|
||||
.withTransaction(manager)
|
||||
.registerInstalledProviders(payIds)
|
||||
}
|
||||
|
||||
async function registerNotificationProvider({
|
||||
manager,
|
||||
container,
|
||||
logger,
|
||||
}: {
|
||||
container: AwilixContainer
|
||||
manager: EntityManager
|
||||
logger: Logger
|
||||
}): Promise<void> {
|
||||
const notiProviders =
|
||||
silentResolution<typeof BaseNotificationService[]>(
|
||||
container,
|
||||
"notificationProviders",
|
||||
logger
|
||||
) || []
|
||||
const notiIds = notiProviders.map((p) => p.getIdentifier())
|
||||
|
||||
const nProviderService = container.resolve<NotificationService>(
|
||||
"notificationService"
|
||||
)
|
||||
await nProviderService
|
||||
.withTransaction(manager)
|
||||
.registerInstalledProviders(notiIds)
|
||||
}
|
||||
|
||||
async function registerFulfillmentProvider({
|
||||
manager,
|
||||
container,
|
||||
logger,
|
||||
}: {
|
||||
container: AwilixContainer
|
||||
manager: EntityManager
|
||||
logger: Logger
|
||||
}): Promise<void> {
|
||||
const fulfilProviders =
|
||||
silentResolution<typeof BaseFulfillmentService[]>(
|
||||
container,
|
||||
"fulfillmentProviders",
|
||||
logger
|
||||
) || []
|
||||
const fulfilIds = fulfilProviders.map((p) => p.getIdentifier())
|
||||
|
||||
const fProviderService = container.resolve<FulfillmentProviderService>(
|
||||
"fulfillmentProviderService"
|
||||
)
|
||||
await fProviderService
|
||||
.withTransaction(manager)
|
||||
.registerInstalledProviders(fulfilIds)
|
||||
}
|
||||
|
||||
async function registerTaxProvider({
|
||||
manager,
|
||||
container,
|
||||
logger,
|
||||
}: {
|
||||
container: AwilixContainer
|
||||
manager: EntityManager
|
||||
logger: Logger
|
||||
}): Promise<void> {
|
||||
const taxProviders =
|
||||
silentResolution<AbstractTaxService[]>(container, "taxProviders", logger) ||
|
||||
[]
|
||||
const taxIds = taxProviders.map((p) => p.getIdentifier())
|
||||
|
||||
const tProviderService =
|
||||
container.resolve<TaxProviderService>("taxProviderService")
|
||||
await tProviderService
|
||||
.withTransaction(manager)
|
||||
.registerInstalledProviders(taxIds)
|
||||
}
|
||||
|
||||
62
packages/medusa/src/loaders/helpers/plugins.ts
Normal file
62
packages/medusa/src/loaders/helpers/plugins.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { ClassConstructor, MedusaContainer } from "../../types/global"
|
||||
import {
|
||||
AbstractPaymentProcessor,
|
||||
AbstractPaymentService,
|
||||
isPaymentProcessor,
|
||||
isPaymentService,
|
||||
} from "../../interfaces"
|
||||
import { aliasTo, asFunction } from "awilix"
|
||||
|
||||
type Context = {
|
||||
container: MedusaContainer
|
||||
pluginDetails: Record<string, unknown>
|
||||
registrationName: string
|
||||
}
|
||||
|
||||
export function registerPaymentServiceFromClass(
|
||||
klass: ClassConstructor<AbstractPaymentService>,
|
||||
context: Context
|
||||
): void {
|
||||
if (!isPaymentService(klass.prototype)) {
|
||||
return
|
||||
}
|
||||
|
||||
const { container, pluginDetails, registrationName } = context
|
||||
|
||||
container.registerAdd(
|
||||
"paymentProviders",
|
||||
asFunction((cradle) => new klass(cradle, pluginDetails.options))
|
||||
)
|
||||
|
||||
container.register({
|
||||
[registrationName]: asFunction(
|
||||
(cradle) => new klass(cradle, pluginDetails.options)
|
||||
),
|
||||
[`pp_${(klass as unknown as typeof AbstractPaymentService).identifier}`]:
|
||||
aliasTo(registrationName),
|
||||
})
|
||||
}
|
||||
|
||||
export function registerPaymentProcessorFromClass(
|
||||
klass: ClassConstructor<AbstractPaymentProcessor>,
|
||||
context: Context
|
||||
): void {
|
||||
if (!isPaymentProcessor(klass.prototype)) {
|
||||
return
|
||||
}
|
||||
|
||||
const { container, pluginDetails, registrationName } = context
|
||||
|
||||
container.registerAdd(
|
||||
"paymentProviders",
|
||||
asFunction((cradle) => new klass(cradle, pluginDetails.options))
|
||||
)
|
||||
|
||||
container.register({
|
||||
[registrationName]: asFunction(
|
||||
(cradle) => new klass(cradle, pluginDetails.options)
|
||||
),
|
||||
[`pp_${(klass as unknown as typeof AbstractPaymentProcessor).identifier}`]:
|
||||
aliasTo(registrationName),
|
||||
})
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
isCartCompletionStrategy,
|
||||
isFileService,
|
||||
isNotificationService,
|
||||
isPaymentService,
|
||||
isPriceSelectionStrategy,
|
||||
isSearchService,
|
||||
isTaxCalculationStrategy,
|
||||
@@ -35,6 +34,10 @@ import {
|
||||
} from "../types/global"
|
||||
import formatRegistrationName from "../utils/format-registration-name"
|
||||
import logger from "./logger"
|
||||
import {
|
||||
registerPaymentProcessorFromClass,
|
||||
registerPaymentServiceFromClass,
|
||||
} from "./helpers/plugins"
|
||||
|
||||
type Options = {
|
||||
rootDirectory: string
|
||||
@@ -371,22 +374,12 @@ export async function registerServices(
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
if (isPaymentService(loaded.prototype)) {
|
||||
// Register our payment providers to paymentProviders
|
||||
container.registerAdd(
|
||||
"paymentProviders",
|
||||
asFunction((cradle) => new loaded(cradle, pluginDetails.options))
|
||||
)
|
||||
const context = { container, pluginDetails, registrationName: name }
|
||||
|
||||
// Add the service directly to the container in order to make simple
|
||||
// resolution if we already know which payment provider we need to use
|
||||
container.register({
|
||||
[name]: asFunction(
|
||||
(cradle) => new loaded(cradle, pluginDetails.options)
|
||||
),
|
||||
[`pp_${loaded.identifier}`]: aliasTo(name),
|
||||
})
|
||||
} else if (loaded.prototype instanceof OauthService) {
|
||||
registerPaymentServiceFromClass(loaded, context)
|
||||
registerPaymentProcessorFromClass(loaded, context)
|
||||
|
||||
if (loaded.prototype instanceof OauthService) {
|
||||
const appDetails = loaded.getAppDetails(pluginDetails.options)
|
||||
|
||||
const oauthService =
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { asClass, asValue, createContainer } from "awilix"
|
||||
import { asClass, asFunction, asValue, createContainer } from "awilix"
|
||||
import { MockManager, MockRepository } from "medusa-test-utils"
|
||||
import PaymentProviderService from "../payment-provider";
|
||||
import { PaymentProviderServiceMock } from "../__mocks__/payment-provider";
|
||||
import { CustomerServiceMock } from "../__mocks__/customer";
|
||||
import { FlagRouter } from "../../utils/flag-router";
|
||||
import Logger from "../../loaders/logger";
|
||||
import {
|
||||
AbstractPaymentProcessor,
|
||||
PaymentProcessorContext,
|
||||
PaymentProcessorError,
|
||||
PaymentProcessorSessionResponse
|
||||
} from "../../interfaces";
|
||||
import { PaymentSessionStatus } from "../../models";
|
||||
import { PaymentServiceMock } from "../__mocks__/payment";
|
||||
|
||||
export const defaultContainer = createContainer()
|
||||
defaultContainer.register("paymentProviderService", asClass(PaymentProviderService))
|
||||
defaultContainer.register("paymentService", asValue(PaymentServiceMock))
|
||||
defaultContainer.register("manager", asValue(MockManager))
|
||||
defaultContainer.register("paymentSessionRepository", asValue(MockRepository()))
|
||||
defaultContainer.register("paymentProviderRepository", asValue(PaymentProviderServiceMock))
|
||||
@@ -16,3 +25,77 @@ defaultContainer.register("refundRepository", asValue(MockRepository()))
|
||||
defaultContainer.register("customerService", asValue(CustomerServiceMock))
|
||||
defaultContainer.register("featureFlagRouter", asValue(new FlagRouter({})))
|
||||
defaultContainer.register("logger", asValue(Logger))
|
||||
defaultContainer.register("pp_payment_processor", asFunction((cradle) => new PaymentProcessor(cradle)))
|
||||
|
||||
export class PaymentProcessor extends AbstractPaymentProcessor {
|
||||
constructor(container) {
|
||||
super(container);
|
||||
}
|
||||
authorizePayment(context: PaymentProcessorContext): Promise<
|
||||
| PaymentProcessorError
|
||||
| {
|
||||
status: PaymentSessionStatus
|
||||
data: PaymentProcessorSessionResponse["session_data"]
|
||||
}
|
||||
> {
|
||||
return Promise.resolve({ } as any);
|
||||
}
|
||||
|
||||
getPaymentStatus(paymentSessionData: Record<string, unknown>): Promise<PaymentSessionStatus> {
|
||||
return Promise.resolve(PaymentSessionStatus.PENDING);
|
||||
}
|
||||
|
||||
init(): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
initiatePayment(context: PaymentProcessorContext): Promise<PaymentProcessorError | PaymentProcessorSessionResponse> {
|
||||
return Promise.resolve({ } as PaymentProcessorSessionResponse);
|
||||
}
|
||||
|
||||
retrievePayment(paymentSessionData: Record<string, unknown>): Promise<PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]> {
|
||||
return Promise.resolve({ });
|
||||
}
|
||||
|
||||
updatePayment(context: PaymentProcessorContext): Promise<PaymentProcessorError | void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
capturePayment(paymentSessionData: Record<string, unknown>): Promise<PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]> {
|
||||
return Promise.resolve({ });
|
||||
}
|
||||
|
||||
refundPayment(paymentSessionData: Record<string, unknown>): Promise<PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]> {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
cancelPayment(paymentSessionData: Record<string, unknown>): Promise<PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]> {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
deletePayment(paymentSessionData: Record<string, unknown>): Promise<PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]> {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultPaymentSessionInputData = {
|
||||
provider_id: "payment_processor",
|
||||
cart: {
|
||||
context: {},
|
||||
id: "cart-test",
|
||||
email: "test@medusajs.com",
|
||||
shipping_address: {},
|
||||
shipping_methods: [],
|
||||
billing_address: {
|
||||
first_name: "Virgil",
|
||||
last_name: "Van Dijk",
|
||||
address_1: "24 Dunks Drive",
|
||||
city: "Los Angeles",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "93011",
|
||||
},
|
||||
},
|
||||
currency_code: "usd",
|
||||
amount: 1000,
|
||||
}
|
||||
@@ -1,360 +0,0 @@
|
||||
import { asValue, createContainer } from "awilix"
|
||||
import { MockRepository } from "medusa-test-utils"
|
||||
import PaymentProviderService from "../payment-provider"
|
||||
import { defaultContainer } from "../__fixtures__/payment-provider"
|
||||
import { testPayServiceMock } from "../__mocks__/test-pay"
|
||||
|
||||
describe("PaymentProviderService", () => {
|
||||
describe("retrieveProvider", () => {
|
||||
const container = createContainer({}, defaultContainer)
|
||||
container.register("pp_default_provider", asValue("good"))
|
||||
|
||||
const providerService = container.resolve("paymentProviderService")
|
||||
|
||||
it("successfully retrieves payment provider", () => {
|
||||
const provider = providerService.retrieveProvider("default_provider")
|
||||
expect(provider).toEqual("good")
|
||||
})
|
||||
|
||||
it("fails when payment provider not found", () => {
|
||||
try {
|
||||
providerService.retrieveProvider("unregistered")
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
"Could not find a payment provider with id: unregistered"
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("createSession", () => {
|
||||
const container = createContainer({}, defaultContainer)
|
||||
container.register(
|
||||
"pp_default_provider",
|
||||
asValue({
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
createPayment: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||
})
|
||||
)
|
||||
|
||||
const providerService = container.resolve("paymentProviderService")
|
||||
|
||||
it("successfully creates session", async () => {
|
||||
await providerService.createSession("default_provider", {
|
||||
object: "cart",
|
||||
region: {
|
||||
currency_code: "usd",
|
||||
},
|
||||
total: 100,
|
||||
})
|
||||
|
||||
const defaultProvider = container.resolve("pp_default_provider")
|
||||
|
||||
expect(defaultProvider.createPayment).toBeCalledTimes(1)
|
||||
expect(defaultProvider.createPayment).toBeCalledWith({
|
||||
amount: 100,
|
||||
object: "cart",
|
||||
total: 100,
|
||||
region: {
|
||||
currency_code: "usd",
|
||||
},
|
||||
cart: {
|
||||
context: undefined,
|
||||
email: undefined,
|
||||
id: undefined,
|
||||
shipping_address: undefined,
|
||||
shipping_methods: undefined,
|
||||
},
|
||||
currency_code: "usd",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateSession", () => {
|
||||
const container = createContainer({}, defaultContainer)
|
||||
container.register(
|
||||
"paymentSessionRepository",
|
||||
asValue(
|
||||
MockRepository({
|
||||
findOne: () =>
|
||||
Promise.resolve({
|
||||
id: "session",
|
||||
provider_id: "default_provider",
|
||||
data: {
|
||||
id: "1234",
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
)
|
||||
container.register(
|
||||
"pp_default_provider",
|
||||
asValue({
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
updatePayment: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||
})
|
||||
)
|
||||
|
||||
const providerService = container.resolve("paymentProviderService")
|
||||
|
||||
it("successfully creates session", async () => {
|
||||
await providerService.updateSession(
|
||||
{
|
||||
id: "session",
|
||||
provider_id: "default_provider",
|
||||
data: {
|
||||
id: "1234",
|
||||
},
|
||||
},
|
||||
{
|
||||
object: "cart",
|
||||
total: 100,
|
||||
}
|
||||
)
|
||||
|
||||
const defaultProvider = container.resolve("pp_default_provider")
|
||||
|
||||
expect(defaultProvider.updatePayment).toBeCalledTimes(1)
|
||||
expect(defaultProvider.updatePayment).toBeCalledWith(
|
||||
{ id: "1234" },
|
||||
{
|
||||
object: "cart",
|
||||
amount: 100,
|
||||
total: 100,
|
||||
cart: {
|
||||
context: undefined,
|
||||
email: undefined,
|
||||
id: undefined,
|
||||
shipping_address: undefined,
|
||||
shipping_methods: undefined,
|
||||
},
|
||||
currency_code: undefined,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe(`PaymentProviderService`, () => {
|
||||
const container = createContainer({}, defaultContainer)
|
||||
container.register("pp_default_provider", asValue(testPayServiceMock))
|
||||
container.register(
|
||||
"paymentSessionRepository",
|
||||
asValue(
|
||||
MockRepository({
|
||||
findOne: () =>
|
||||
Promise.resolve({
|
||||
id: "session",
|
||||
provider_id: "default_provider",
|
||||
data: {
|
||||
id: "1234",
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
)
|
||||
container.register(
|
||||
"paymentRepository",
|
||||
asValue(
|
||||
MockRepository({
|
||||
findOne: () =>
|
||||
Promise.resolve({
|
||||
id: "pay_jadazdjk",
|
||||
provider_id: "default_provider",
|
||||
data: {
|
||||
id: "1234",
|
||||
},
|
||||
}),
|
||||
find: () =>
|
||||
Promise.resolve([
|
||||
{
|
||||
id: "pay_jadazdjk",
|
||||
provider_id: "default_provider",
|
||||
data: {
|
||||
id: "1234",
|
||||
},
|
||||
captured_at: new Date(),
|
||||
amount: 100,
|
||||
amount_refunded: 0,
|
||||
},
|
||||
]),
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const providerService = container.resolve("paymentProviderService")
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully retrieves payment provider", () => {
|
||||
const provider = providerService.retrieveProvider("default_provider")
|
||||
expect(provider.identifier).toEqual("test-pay")
|
||||
})
|
||||
|
||||
it("successfully creates session", async () => {
|
||||
await providerService.createSession("default_provider", {
|
||||
object: "cart",
|
||||
region: {
|
||||
currency_code: "usd",
|
||||
},
|
||||
total: 100,
|
||||
})
|
||||
|
||||
expect(testPayServiceMock.createPayment).toBeCalledTimes(1)
|
||||
expect(testPayServiceMock.createPayment).toBeCalledWith({
|
||||
amount: 100,
|
||||
object: "cart",
|
||||
total: 100,
|
||||
region: {
|
||||
currency_code: "usd",
|
||||
},
|
||||
cart: {
|
||||
context: undefined,
|
||||
email: undefined,
|
||||
id: undefined,
|
||||
shipping_address: undefined,
|
||||
shipping_methods: undefined,
|
||||
},
|
||||
currency_code: "usd",
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully update session", async () => {
|
||||
await providerService.updateSession(
|
||||
{
|
||||
id: "session",
|
||||
provider_id: "default_provider",
|
||||
data: {
|
||||
id: "1234",
|
||||
},
|
||||
},
|
||||
{
|
||||
object: "cart",
|
||||
total: 100,
|
||||
}
|
||||
)
|
||||
|
||||
expect(testPayServiceMock.updatePayment).toBeCalledTimes(1)
|
||||
expect(testPayServiceMock.updatePayment).toBeCalledWith(
|
||||
{ id: "1234" },
|
||||
{
|
||||
amount: 100,
|
||||
object: "cart",
|
||||
total: 100,
|
||||
cart: {
|
||||
context: undefined,
|
||||
email: undefined,
|
||||
id: undefined,
|
||||
shipping_address: undefined,
|
||||
shipping_methods: undefined,
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("successfully refresh session", async () => {
|
||||
await providerService.refreshSession(
|
||||
{
|
||||
id: "session",
|
||||
provider_id: "default_provider",
|
||||
data: {
|
||||
id: "1234",
|
||||
},
|
||||
},
|
||||
{
|
||||
provider_id: "default_provider",
|
||||
amount: 100,
|
||||
currency_code: "usd",
|
||||
}
|
||||
)
|
||||
|
||||
expect(testPayServiceMock.deletePayment).toBeCalledTimes(1)
|
||||
expect(testPayServiceMock.createPayment).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it("successfully delete session", async () => {
|
||||
await providerService.deleteSession({
|
||||
id: "session",
|
||||
provider_id: "default_provider",
|
||||
data: {
|
||||
id: "1234",
|
||||
},
|
||||
})
|
||||
|
||||
expect(testPayServiceMock.deletePayment).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it("successfully delete session", async () => {
|
||||
await providerService.deleteSession({
|
||||
id: "session",
|
||||
provider_id: "default_provider",
|
||||
data: {
|
||||
id: "1234",
|
||||
},
|
||||
})
|
||||
|
||||
expect(testPayServiceMock.deletePayment).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it("successfully authorize payment", async () => {
|
||||
await providerService.authorizePayment(
|
||||
{
|
||||
id: "session",
|
||||
provider_id: "default_provider",
|
||||
data: {
|
||||
id: "1234",
|
||||
},
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
expect(testPayServiceMock.authorizePayment).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it("successfully update session data", async () => {
|
||||
await providerService.updateSessionData(
|
||||
{
|
||||
id: "session",
|
||||
provider_id: "default_provider",
|
||||
data: {
|
||||
id: "1234",
|
||||
},
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
expect(testPayServiceMock.updatePaymentData).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it("successfully cancel payment", async () => {
|
||||
await providerService.cancelPayment({
|
||||
id: "pay_jadazdjk",
|
||||
})
|
||||
expect(testPayServiceMock.cancelPayment).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it("successfully capture payment", async () => {
|
||||
await providerService.capturePayment({
|
||||
id: "pay_jadazdjk",
|
||||
})
|
||||
expect(testPayServiceMock.capturePayment).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it("successfully refund payment", async () => {
|
||||
await providerService.refundPayment(
|
||||
[
|
||||
{
|
||||
id: "pay_jadazdjk",
|
||||
},
|
||||
],
|
||||
50
|
||||
)
|
||||
expect(testPayServiceMock.refundPayment).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
1009
packages/medusa/src/services/__tests__/payment-provider.ts
Normal file
1009
packages/medusa/src/services/__tests__/payment-provider.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,11 @@ import { isDefined, MedusaError } from "medusa-core-utils"
|
||||
import { BasePaymentService } from "medusa-interfaces"
|
||||
import { EntityManager } from "typeorm"
|
||||
import {
|
||||
AbstractPaymentProcessor,
|
||||
AbstractPaymentService,
|
||||
isPaymentProcessorError,
|
||||
PaymentContext,
|
||||
PaymentProcessorError,
|
||||
PaymentSessionResponse,
|
||||
TransactionBaseService,
|
||||
} from "../interfaces"
|
||||
@@ -26,6 +29,7 @@ import { buildQuery, isString } from "../utils"
|
||||
import { FlagRouter } from "../utils/flag-router"
|
||||
import { CustomerService } from "./index"
|
||||
import PaymentService from "./payment"
|
||||
import { EOL } from "os"
|
||||
|
||||
type PaymentProviderKey = `pp_${string}` | "systemPaymentProviderService"
|
||||
type InjectedDependencies = {
|
||||
@@ -53,6 +57,10 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
// eslint-disable-next-line max-len
|
||||
protected readonly paymentProviderRepository_: typeof PaymentProviderRepository
|
||||
protected readonly paymentRepository_: typeof PaymentRepository
|
||||
protected get paymentService_(): PaymentService {
|
||||
// defer resolution. then it will use the cached resolved service
|
||||
return this.container_.paymentService
|
||||
}
|
||||
protected readonly refundRepository_: typeof RefundRepository
|
||||
protected readonly customerService_: CustomerService
|
||||
protected readonly logger_: Logger
|
||||
@@ -98,15 +106,27 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
return await ppRepo.find()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a payment entity with the given id.
|
||||
* @param paymentId
|
||||
* @param relations
|
||||
*/
|
||||
async retrievePayment(
|
||||
id: string,
|
||||
paymentId: string,
|
||||
relations: string[] = []
|
||||
): Promise<Payment | never> {
|
||||
if (!isDefined(paymentId)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`"paymentId" must be defined`
|
||||
)
|
||||
}
|
||||
|
||||
const paymentRepo = this.activeManager_.withRepository(
|
||||
this.paymentRepository_
|
||||
)
|
||||
const query = {
|
||||
where: { id },
|
||||
where: { id: paymentId },
|
||||
relations: [] as string[],
|
||||
}
|
||||
|
||||
@@ -119,13 +139,18 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
if (!payment) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Payment with ${id} was not found`
|
||||
`Payment with ${paymentId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return payment
|
||||
}
|
||||
|
||||
/**
|
||||
* List all the payments according to the given selector and config.
|
||||
* @param selector
|
||||
* @param config
|
||||
*/
|
||||
async listPayments(
|
||||
selector: Selector<Payment>,
|
||||
config: FindConfig<Payment> = {
|
||||
@@ -139,35 +164,54 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
return await payRepo.find(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the payment session for the given id.
|
||||
* @param paymentSessionId
|
||||
* @param relations
|
||||
*/
|
||||
async retrieveSession(
|
||||
id: string,
|
||||
paymentSessionId: string,
|
||||
relations: string[] = []
|
||||
): Promise<PaymentSession | never> {
|
||||
if (!isDefined(paymentSessionId)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`"paymentSessionId" must be defined`
|
||||
)
|
||||
}
|
||||
|
||||
const sessionRepo = this.activeManager_.withRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
|
||||
const query = {
|
||||
where: { id },
|
||||
relations: [] as string[],
|
||||
}
|
||||
|
||||
if (relations.length) {
|
||||
query.relations = relations
|
||||
}
|
||||
|
||||
const query = buildQuery({ id: paymentSessionId }, { relations })
|
||||
const session = await sessionRepo.findOne(query)
|
||||
|
||||
if (!session) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Payment Session with ${id} was not found`
|
||||
`Payment Session with ${paymentSessionId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @param providerId
|
||||
* @param cart
|
||||
*/
|
||||
async createSession(providerId: string, cart: Cart): Promise<PaymentSession>
|
||||
|
||||
/**
|
||||
* Creates a payment session with the given provider.
|
||||
* @param sessionInput
|
||||
*/
|
||||
async createSession(
|
||||
sessionInput: PaymentSessionInput
|
||||
): Promise<PaymentSession>
|
||||
|
||||
/**
|
||||
* Creates a payment session with the given provider.
|
||||
* @param providerIdOrSessionInput - the id of the provider to create payment with or the input data
|
||||
@@ -184,11 +228,14 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
const providerId = isString(providerIdOrSessionInput)
|
||||
? providerIdOrSessionInput
|
||||
: providerIdOrSessionInput.provider_id
|
||||
|
||||
const data = (
|
||||
isString(providerIdOrSessionInput) ? cart : providerIdOrSessionInput
|
||||
) as Cart | PaymentSessionInput
|
||||
|
||||
const provider = this.retrieveProvider<AbstractPaymentService>(providerId)
|
||||
const provider = this.retrieveProvider<
|
||||
AbstractPaymentService | AbstractPaymentProcessor
|
||||
>(providerId)
|
||||
const context = this.buildPaymentProcessorContext(data)
|
||||
|
||||
if (!isDefined(context.currency_code) || !isDefined(context.amount)) {
|
||||
@@ -198,9 +245,28 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
)
|
||||
}
|
||||
|
||||
const paymentResponse = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.createPayment(context)
|
||||
let paymentResponse
|
||||
if (provider instanceof AbstractPaymentProcessor) {
|
||||
paymentResponse = await provider.initiatePayment({
|
||||
amount: context.amount,
|
||||
context: context.context,
|
||||
currency_code: context.currency_code,
|
||||
customer: context.customer,
|
||||
email: context.email,
|
||||
billing_address: context.billing_address,
|
||||
resource_id: context.resource_id,
|
||||
paymentSessionData: {},
|
||||
})
|
||||
|
||||
if ("error" in paymentResponse) {
|
||||
this.throwFromPaymentProcessorError(paymentResponse)
|
||||
}
|
||||
} else {
|
||||
// Added to stay backward compatible
|
||||
paymentResponse = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.createPayment(context)
|
||||
}
|
||||
|
||||
const sessionData = paymentResponse.session_data ?? paymentResponse
|
||||
|
||||
@@ -242,10 +308,21 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
): Promise<PaymentSession> {
|
||||
return this.atomicPhase_(async (transactionManager) => {
|
||||
const session = await this.retrieveSession(paymentSession.id)
|
||||
const provider = this.retrieveProvider<AbstractPaymentService>(
|
||||
paymentSession.provider_id
|
||||
)
|
||||
await provider.withTransaction(transactionManager).deletePayment(session)
|
||||
|
||||
const provider = this.retrieveProvider<
|
||||
AbstractPaymentService | AbstractPaymentProcessor
|
||||
>(paymentSession.provider_id)
|
||||
|
||||
if (provider instanceof AbstractPaymentProcessor) {
|
||||
const error = await provider.deletePayment(session.data)
|
||||
if (isPaymentProcessorError(error)) {
|
||||
this.throwFromPaymentProcessorError(error)
|
||||
}
|
||||
} else {
|
||||
await provider
|
||||
.withTransaction(transactionManager)
|
||||
.deletePayment(session)
|
||||
}
|
||||
|
||||
const sessionRepo = transactionManager.withRepository(
|
||||
this.paymentSessionRepository_
|
||||
@@ -271,16 +348,42 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
sessionInput: Cart | PaymentSessionInput
|
||||
): Promise<PaymentSession> {
|
||||
return await this.atomicPhase_(async (transactionManager) => {
|
||||
const provider = this.retrieveProvider(paymentSession.provider_id)
|
||||
const provider = this.retrieveProvider<
|
||||
AbstractPaymentService | AbstractPaymentProcessor
|
||||
>(paymentSession.provider_id)
|
||||
|
||||
const context = this.buildPaymentProcessorContext(sessionInput)
|
||||
|
||||
const paymentResponse = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.updatePayment(paymentSession.data, context)
|
||||
let paymentResponse
|
||||
if (provider instanceof AbstractPaymentProcessor) {
|
||||
paymentResponse =
|
||||
(await provider.updatePayment({
|
||||
amount: context.amount,
|
||||
context: context.context,
|
||||
currency_code: context.currency_code,
|
||||
customer: context.customer,
|
||||
email: context.email,
|
||||
billing_address: context.billing_address,
|
||||
resource_id: context.resource_id,
|
||||
paymentSessionData: paymentSession.data,
|
||||
})) ?? {}
|
||||
|
||||
if (paymentResponse && "error" in paymentResponse) {
|
||||
this.throwFromPaymentProcessorError(paymentResponse)
|
||||
}
|
||||
} else {
|
||||
paymentResponse = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.updatePayment(paymentSession.data, context)
|
||||
}
|
||||
|
||||
const sessionData = paymentResponse.session_data ?? paymentResponse
|
||||
|
||||
// If no update occurs, return the original session
|
||||
if (!sessionData) {
|
||||
return await this.retrieveSession(paymentSession.id)
|
||||
}
|
||||
|
||||
await this.processUpdateRequestsData(
|
||||
{
|
||||
customer: { id: context.customer?.id },
|
||||
@@ -309,10 +412,20 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
return
|
||||
}
|
||||
|
||||
const provider = this.retrieveProvider(paymentSession.provider_id)
|
||||
await provider
|
||||
.withTransaction(transactionManager)
|
||||
.deletePayment(paymentSession)
|
||||
const provider = this.retrieveProvider<
|
||||
AbstractPaymentService | AbstractPaymentProcessor
|
||||
>(paymentSession.provider_id)
|
||||
|
||||
if (provider instanceof AbstractPaymentProcessor) {
|
||||
const error = await provider.deletePayment(paymentSession.data)
|
||||
if (isPaymentProcessorError(error)) {
|
||||
this.throwFromPaymentProcessorError(error)
|
||||
}
|
||||
} else {
|
||||
await provider
|
||||
.withTransaction(transactionManager)
|
||||
.deletePayment(paymentSession)
|
||||
}
|
||||
|
||||
const sessionRepo = transactionManager.withRepository(
|
||||
this.paymentSessionRepository_
|
||||
@@ -324,15 +437,20 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
|
||||
/**
|
||||
* Finds a provider given an id
|
||||
* @param {string} providerId - the id of the provider to get
|
||||
* @return {PaymentService} the payment provider
|
||||
* @param providerId - the id of the provider to get
|
||||
* @return the payment provider
|
||||
*/
|
||||
retrieveProvider<
|
||||
TProvider extends AbstractPaymentService | typeof BasePaymentService
|
||||
TProvider extends
|
||||
| AbstractPaymentService
|
||||
| typeof BasePaymentService
|
||||
| AbstractPaymentProcessor
|
||||
>(
|
||||
providerId: string
|
||||
): TProvider extends AbstractPaymentService
|
||||
? AbstractPaymentService
|
||||
: TProvider extends AbstractPaymentProcessor
|
||||
? AbstractPaymentProcessor
|
||||
: typeof BasePaymentService {
|
||||
try {
|
||||
let provider
|
||||
@@ -356,10 +474,25 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
const { payment_session, currency_code, amount, provider_id } = data
|
||||
const providerId = provider_id ?? payment_session.provider_id
|
||||
|
||||
const provider = this.retrieveProvider<AbstractPaymentService>(providerId)
|
||||
const paymentData = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.getPaymentData(payment_session)
|
||||
const provider = this.retrieveProvider<
|
||||
AbstractPaymentService | AbstractPaymentProcessor
|
||||
>(providerId)
|
||||
|
||||
let paymentData: Record<string, unknown> = {}
|
||||
|
||||
if (provider instanceof AbstractPaymentProcessor) {
|
||||
const res = await provider.retrievePayment(payment_session.data)
|
||||
if ("error" in res) {
|
||||
this.throwFromPaymentProcessorError(res as PaymentProcessorError)
|
||||
} else {
|
||||
// Use else to avoid casting the object and infer the type instead
|
||||
paymentData = res
|
||||
}
|
||||
} else {
|
||||
paymentData = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.getPaymentData(payment_session)
|
||||
}
|
||||
|
||||
const paymentRepo = transactionManager.withRepository(
|
||||
this.paymentRepository_
|
||||
@@ -382,8 +515,7 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
data: { order_id?: string; swap_id?: string }
|
||||
): Promise<Payment> {
|
||||
return await this.atomicPhase_(async (transactionManager) => {
|
||||
const paymentService = this.container_.paymentService
|
||||
return await paymentService
|
||||
return await this.paymentService_
|
||||
.withTransaction(transactionManager)
|
||||
.update(paymentId, data)
|
||||
})
|
||||
@@ -403,14 +535,28 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
}
|
||||
|
||||
const provider = this.retrieveProvider(paymentSession.provider_id)
|
||||
const { status, data } = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.authorizePayment(session, context)
|
||||
|
||||
session.data = data
|
||||
session.status = status
|
||||
if (provider instanceof AbstractPaymentProcessor) {
|
||||
const res = await provider.authorizePayment(
|
||||
paymentSession.data,
|
||||
context
|
||||
)
|
||||
if ("error" in res) {
|
||||
this.throwFromPaymentProcessorError(res)
|
||||
} else {
|
||||
// Use else to avoid casting the object and infer the type instead
|
||||
session.data = res.data
|
||||
session.status = res.status
|
||||
}
|
||||
} else {
|
||||
const { status, data } = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.authorizePayment(session, context)
|
||||
session.data = data
|
||||
session.status = status
|
||||
}
|
||||
|
||||
if (status === PaymentSessionStatus.AUTHORIZED) {
|
||||
if (session.status === PaymentSessionStatus.AUTHORIZED) {
|
||||
session.payment_authorized_at = new Date()
|
||||
}
|
||||
|
||||
@@ -430,10 +576,17 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
|
||||
const provider = this.retrieveProvider(paymentSession.provider_id)
|
||||
|
||||
session.data = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.updatePaymentData(paymentSession.data, data)
|
||||
session.status = paymentSession.status
|
||||
if (provider instanceof AbstractPaymentProcessor) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`The payment provider ${paymentSession.provider_id} is of type PaymentProcessor. PaymentProcessors cannot update payment session data.`
|
||||
)
|
||||
} else {
|
||||
session.data = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.updatePaymentData(paymentSession.data, data)
|
||||
session.status = paymentSession.status
|
||||
}
|
||||
|
||||
const sessionRepo = transactionManager.withRepository(
|
||||
this.paymentSessionRepository_
|
||||
@@ -448,9 +601,17 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
return await this.atomicPhase_(async (transactionManager) => {
|
||||
const payment = await this.retrievePayment(paymentObj.id)
|
||||
const provider = this.retrieveProvider(payment.provider_id)
|
||||
payment.data = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.cancelPayment(payment)
|
||||
|
||||
if (provider instanceof AbstractPaymentProcessor) {
|
||||
const error = await provider.cancelPayment(payment.data)
|
||||
if (isPaymentProcessorError(error)) {
|
||||
this.throwFromPaymentProcessorError(error)
|
||||
}
|
||||
} else {
|
||||
payment.data = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.cancelPayment(payment)
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
payment.canceled_at = now.toISOString()
|
||||
@@ -464,6 +625,10 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
|
||||
async getStatus(payment: Payment): Promise<PaymentSessionStatus> {
|
||||
const provider = this.retrieveProvider(payment.provider_id)
|
||||
if (provider instanceof AbstractPaymentProcessor) {
|
||||
return await provider.getPaymentStatus(payment.data)
|
||||
}
|
||||
|
||||
return await provider
|
||||
.withTransaction(this.activeManager_)
|
||||
.getStatus(payment.data)
|
||||
@@ -475,9 +640,20 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
return await this.atomicPhase_(async (transactionManager) => {
|
||||
const payment = await this.retrievePayment(paymentObj.id)
|
||||
const provider = this.retrieveProvider(payment.provider_id)
|
||||
payment.data = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.capturePayment(payment)
|
||||
|
||||
if (provider instanceof AbstractPaymentProcessor) {
|
||||
const res = await provider.capturePayment(payment.data)
|
||||
if ("error" in res) {
|
||||
this.throwFromPaymentProcessorError(res as PaymentProcessorError)
|
||||
} else {
|
||||
// Use else to avoid casting the object and infer the type instead
|
||||
payment.data = res
|
||||
}
|
||||
} else {
|
||||
payment.data = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.capturePayment(payment)
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
payment.captured_at = now.toISOString()
|
||||
@@ -536,9 +712,23 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
const refundAmount = Math.min(currentRefundable, balance)
|
||||
|
||||
const provider = this.retrieveProvider(paymentToRefund.provider_id)
|
||||
paymentToRefund.data = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.refundPayment(paymentToRefund, refundAmount)
|
||||
|
||||
if (provider instanceof AbstractPaymentProcessor) {
|
||||
const res = await provider.refundPayment(
|
||||
paymentToRefund.data,
|
||||
refundAmount
|
||||
)
|
||||
if (isPaymentProcessorError(res)) {
|
||||
this.throwFromPaymentProcessorError(res as PaymentProcessorError)
|
||||
} else {
|
||||
// Use else to avoid casting the object and infer the type instead
|
||||
paymentToRefund.data = res
|
||||
}
|
||||
} else {
|
||||
paymentToRefund.data = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.refundPayment(paymentToRefund, refundAmount)
|
||||
}
|
||||
|
||||
paymentToRefund.amount_refunded += refundAmount
|
||||
await paymentRepo.save(paymentToRefund)
|
||||
@@ -591,9 +781,20 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
}
|
||||
|
||||
const provider = this.retrieveProvider(payment.provider_id)
|
||||
payment.data = await provider
|
||||
.withTransaction(manager)
|
||||
.refundPayment(payment, amount)
|
||||
|
||||
if (provider instanceof AbstractPaymentProcessor) {
|
||||
const res = await provider.refundPayment(payment.data, amount)
|
||||
if (isPaymentProcessorError(res)) {
|
||||
this.throwFromPaymentProcessorError(res as PaymentProcessorError)
|
||||
} else {
|
||||
// Use else to avoid casting the object and infer the type instead
|
||||
payment.data = res
|
||||
}
|
||||
} else {
|
||||
payment.data = await provider
|
||||
.withTransaction(manager)
|
||||
.refundPayment(payment, amount)
|
||||
}
|
||||
|
||||
payment.amount_refunded += amount
|
||||
|
||||
@@ -652,19 +853,21 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
context.cart = {
|
||||
context: cart.context,
|
||||
shipping_address: cart.shipping_address,
|
||||
billing_address: cart.billing_address,
|
||||
id: cart.id,
|
||||
email: cart.email,
|
||||
shipping_methods: cart.shipping_methods,
|
||||
}
|
||||
context.amount = cart.total!
|
||||
context.currency_code = cart.region?.currency_code
|
||||
context.resource_id = cart.id
|
||||
Object.assign(context, cart)
|
||||
} else {
|
||||
const data = cartOrData as PaymentSessionInput
|
||||
context.cart = data.cart
|
||||
context.amount = data.amount
|
||||
context.currency_code = data.currency_code
|
||||
context.resource_id = data.resource_id
|
||||
context.resource_id = data.resource_id ?? data.cart.id
|
||||
Object.assign(context, cart)
|
||||
}
|
||||
|
||||
@@ -743,4 +946,12 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private throwFromPaymentProcessorError(errObj: PaymentProcessorError) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`${errObj.error}${errObj.detail ? `:${EOL}${errObj.detail}` : ""}`,
|
||||
errObj.code
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,13 @@ export type PaymentSessionInput = {
|
||||
email: string
|
||||
shipping_address: Address | null
|
||||
shipping_methods: ShippingMethod[]
|
||||
billing_address?: Address | null
|
||||
}
|
||||
customer?: Customer | null
|
||||
currency_code: string
|
||||
amount: number
|
||||
resource_id?: string
|
||||
paymentSessionData?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type CreatePaymentInput = {
|
||||
|
||||
Reference in New Issue
Block a user