feat(medusa:) Convert PaymentProvider + PaymentProviderInterface to TS + (#1773)
* feat(payments): Refactor core Payment related * fix(medusa): typings * test(unit): fix suite * test(unit): fix suite * feat(medusa): Improve payment provider container typings * fix(medusa): typings * styles(medusa): renove comments * feat(medusa): cleanup * feat(medusa): Add uniq constraint on payment session and idem key on create-payment-session end point * fix(medusa): migration * fix(medusa): create payment session * feat(medusa): cleanup
This commit is contained in:
committed by
GitHub
parent
987ce2ab6d
commit
bd031ef7ad
@@ -1,10 +1,10 @@
|
||||
import { PaymentService } from "medusa-interfaces";
|
||||
import { AbstractPaymentService } from "@medusajs/medusa";
|
||||
|
||||
class TestPayService extends PaymentService {
|
||||
class TestPayService extends AbstractPaymentService {
|
||||
static identifier = "test-pay";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(_) {
|
||||
super(_);
|
||||
}
|
||||
|
||||
async getStatus(paymentData) {
|
||||
|
||||
@@ -47,9 +47,9 @@ These methods are used at different points in the Checkout flow as well as when
|
||||
The first step to create a payment provider is to create a file in `src/services` with the following content:
|
||||
|
||||
```jsx
|
||||
import { PaymentService } from "medusa-interfaces"
|
||||
import { AbstractPaymentService } from "@medusajs/medusa"
|
||||
|
||||
class MyPaymentService extends PaymentService {
|
||||
class MyPaymentService extends AbstractPaymentService {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ const { initDb, useDb } = require("../../../helpers/use-db")
|
||||
|
||||
const {
|
||||
simpleProductTaxRateFactory,
|
||||
simpleShippingTaxRateFactory,
|
||||
simpleShippingOptionFactory,
|
||||
simpleCartFactory,
|
||||
simpleRegionFactory,
|
||||
simpleProductFactory,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { PaymentService } from "medusa-interfaces"
|
||||
import { AbstractPaymentService } from "@medusajs/medusa"
|
||||
|
||||
class TestPayService extends PaymentService {
|
||||
class TestPayService extends AbstractPaymentService {
|
||||
static identifier = "test-pay"
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
constructor(_) {
|
||||
super(_)
|
||||
}
|
||||
|
||||
async getStatus(paymentData) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { PaymentService } from "medusa-interfaces"
|
||||
import { AbstractPaymentService } from "@medusajs/medusa"
|
||||
|
||||
class TestPayService extends PaymentService {
|
||||
class TestPayService extends AbstractPaymentService {
|
||||
static identifier = "test-pay"
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
constructor(_) {
|
||||
super(_)
|
||||
}
|
||||
|
||||
async getStatus(paymentData) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export { default as BaseService } from "./base-service"
|
||||
export { default as FileService } from "./file-service"
|
||||
export { default as PaymentService } from "./payment-service"
|
||||
export { default as FulfillmentService } from "./fulfillment-service"
|
||||
export { default as FileService } from "./file-service"
|
||||
export { default as NotificationService } from "./notification-service"
|
||||
export { default as OauthService } from "./oauth-service"
|
||||
export { default as PaymentService } from "./payment-service"
|
||||
export { default as SearchService } from "./search-service"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { defaultStoreCartFields, defaultStoreCartRelations } from "."
|
||||
import { CartService } from "../../../../services"
|
||||
import { decorateLineItemsWithTotals } from "./decorate-line-items-with-totals"
|
||||
import { EntityManager } from "typeorm";
|
||||
import IdempotencyKeyService from "../../../../services/idempotency-key";
|
||||
|
||||
/**
|
||||
* @oas [post] /carts/{id}/payment-sessions
|
||||
@@ -26,17 +27,97 @@ export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const cartService: CartService = req.scope.resolve("cartService")
|
||||
|
||||
const idempotencyKeyService: IdempotencyKeyService = req.scope.resolve(
|
||||
"idempotencyKeyService"
|
||||
)
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
return await cartService.withTransaction(transactionManager).setPaymentSessions(id)
|
||||
})
|
||||
|
||||
const cart = await cartService.retrieve(id, {
|
||||
select: defaultStoreCartFields,
|
||||
relations: defaultStoreCartRelations,
|
||||
})
|
||||
const headerKey = req.get("Idempotency-Key") || ""
|
||||
|
||||
const data = await decorateLineItemsWithTotals(cart, req)
|
||||
res.status(200).json({ cart: data })
|
||||
let idempotencyKey
|
||||
try {
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
idempotencyKey = await idempotencyKeyService.withTransaction(transactionManager).initializeRequest(
|
||||
headerKey,
|
||||
req.method,
|
||||
req.params,
|
||||
req.path
|
||||
)
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(409).send("Failed to create idempotency key")
|
||||
return
|
||||
}
|
||||
|
||||
res.setHeader("Access-Control-Expose-Headers", "Idempotency-Key")
|
||||
res.setHeader("Idempotency-Key", idempotencyKey.idempotency_key)
|
||||
|
||||
try {
|
||||
let inProgress = true
|
||||
let err: unknown = false
|
||||
|
||||
while (inProgress) {
|
||||
switch (idempotencyKey.recovery_point) {
|
||||
case "started": {
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
const { key, error } = await idempotencyKeyService
|
||||
.withTransaction(transactionManager)
|
||||
.workStage(
|
||||
idempotencyKey.idempotency_key,
|
||||
async (stageManager) => {
|
||||
await cartService.withTransaction(stageManager).setPaymentSessions(id)
|
||||
|
||||
const cart = await cartService.withTransaction(stageManager).retrieve(id, {
|
||||
select: defaultStoreCartFields,
|
||||
relations: defaultStoreCartRelations,
|
||||
})
|
||||
|
||||
const data = await decorateLineItemsWithTotals(cart, req, {
|
||||
force_taxes: false,
|
||||
transactionManager: stageManager
|
||||
})
|
||||
|
||||
return {
|
||||
response_code: 200,
|
||||
response_body: { cart: data },
|
||||
}
|
||||
})
|
||||
|
||||
if (error) {
|
||||
inProgress = false
|
||||
err = error
|
||||
} else {
|
||||
idempotencyKey = key
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "finished": {
|
||||
inProgress = false
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
idempotencyKey = await idempotencyKeyService
|
||||
.withTransaction(transactionManager)
|
||||
.update(
|
||||
idempotencyKey.idempotency_key,
|
||||
{
|
||||
recovery_point: "finished",
|
||||
response_code: 500,
|
||||
response_body: { message: "Unknown recovery point" },
|
||||
}
|
||||
)
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
res.status(idempotencyKey.response_code).json(idempotencyKey.response_body)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,16 @@ import { EntityManager } from "typeorm";
|
||||
export const decorateLineItemsWithTotals = async (
|
||||
cart: Cart,
|
||||
req: Request,
|
||||
options: { force_taxes: boolean } = { force_taxes: false }
|
||||
options: { force_taxes: boolean, transactionManager?: EntityManager } = { force_taxes: false }
|
||||
): Promise<Cart> => {
|
||||
const totalsService: TotalsService = req.scope.resolve("totalsService")
|
||||
|
||||
if (cart.items && cart.region) {
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
const items = await manager.transaction(async (transactionManager) => {
|
||||
const getItems = async (manager) => {
|
||||
const totalsServiceTx = totalsService.withTransaction(manager)
|
||||
return await Promise.all(
|
||||
cart.items.map(async (item: LineItem) => {
|
||||
const itemTotals = await totalsService
|
||||
.withTransaction(transactionManager)
|
||||
const itemTotals = await totalsServiceTx
|
||||
.getLineItemTotals(item, cart, {
|
||||
include_tax: options.force_taxes || cart.region.automatic_taxes,
|
||||
})
|
||||
@@ -24,7 +23,17 @@ export const decorateLineItemsWithTotals = async (
|
||||
return Object.assign(item, itemTotals)
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
let items
|
||||
if (options.transactionManager) {
|
||||
items = await getItems(options.transactionManager)
|
||||
} else {
|
||||
const manager: EntityManager = options.transactionManager ?? req.scope.resolve("manager")
|
||||
items = await manager.transaction(async (transactionManager) => {
|
||||
return await getItems(transactionManager)
|
||||
})
|
||||
}
|
||||
|
||||
return Object.assign(cart, { items })
|
||||
}
|
||||
|
||||
@@ -53,5 +53,5 @@ export default async (req, res) => {
|
||||
|
||||
export class StorePostCartsCartPaymentSessionUpdateReq {
|
||||
@IsObject()
|
||||
data: object
|
||||
data: Record<string, unknown>
|
||||
}
|
||||
|
||||
@@ -9,3 +9,4 @@ export * from "./price-selection-strategy"
|
||||
export * from "./models/base-entity"
|
||||
export * from "./models/soft-deletable-entity"
|
||||
export * from "./search-service"
|
||||
export * from "./payment-service"
|
||||
|
||||
116
packages/medusa/src/interfaces/payment-service.ts
Normal file
116
packages/medusa/src/interfaces/payment-service.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { TransactionBaseService } from "./transaction-base-service"
|
||||
import {
|
||||
Cart,
|
||||
Customer,
|
||||
Payment,
|
||||
PaymentSession,
|
||||
PaymentSessionStatus,
|
||||
} from "../models"
|
||||
import { PaymentService } from "medusa-interfaces"
|
||||
|
||||
export type Data = Record<string, unknown>
|
||||
export type PaymentData = Data
|
||||
export type PaymentSessionData = Data
|
||||
|
||||
export interface PaymentService<T extends TransactionBaseService<never>>
|
||||
extends TransactionBaseService<T> {
|
||||
getIdentifier(): string
|
||||
|
||||
getPaymentData(paymentSession: PaymentSession): Promise<PaymentData>
|
||||
|
||||
updatePaymentData(
|
||||
paymentSessionData: PaymentSessionData,
|
||||
data: Data
|
||||
): Promise<PaymentSessionData>
|
||||
|
||||
createPayment(cart: Cart): Promise<PaymentSessionData>
|
||||
|
||||
retrievePayment(paymentData: PaymentData): Promise<Data>
|
||||
|
||||
updatePayment(
|
||||
paymentSessionData: PaymentSessionData,
|
||||
cart: Cart
|
||||
): Promise<PaymentSessionData>
|
||||
|
||||
authorizePayment(
|
||||
paymentSession: PaymentSession,
|
||||
context: Data
|
||||
): Promise<{ data: PaymentSessionData; status: PaymentSessionStatus }>
|
||||
|
||||
capturePayment(payment: Payment): Promise<PaymentData>
|
||||
|
||||
refundPayment(payment: Payment, refundAmount: number): Promise<PaymentData>
|
||||
|
||||
cancelPayment(payment: Payment): Promise<PaymentData>
|
||||
|
||||
deletePayment(paymentSession: PaymentSession): Promise<void>
|
||||
|
||||
retrieveSavedMethods(customer: Customer): Promise<Data[]>
|
||||
|
||||
getStatus(data: Data): Promise<PaymentSessionStatus>
|
||||
}
|
||||
|
||||
export abstract class AbstractPaymentService<
|
||||
T extends TransactionBaseService<never>
|
||||
>
|
||||
extends TransactionBaseService<T>
|
||||
implements PaymentService<T>
|
||||
{
|
||||
protected constructor(container: unknown, config?: Record<string, unknown>) {
|
||||
super(container, config)
|
||||
}
|
||||
|
||||
protected static identifier: string
|
||||
|
||||
public getIdentifier(): string {
|
||||
if (!(<typeof AbstractPaymentService>this.constructor).identifier) {
|
||||
throw new Error('Missing static property "identifier".')
|
||||
}
|
||||
return (<typeof AbstractPaymentService>this.constructor).identifier
|
||||
}
|
||||
|
||||
public abstract getPaymentData(
|
||||
paymentSession: PaymentSession
|
||||
): Promise<PaymentData>
|
||||
|
||||
public abstract updatePaymentData(
|
||||
paymentSessionData: PaymentSessionData,
|
||||
data: Data
|
||||
): Promise<PaymentSessionData>
|
||||
|
||||
public abstract createPayment(cart: Cart): Promise<PaymentSessionData>
|
||||
|
||||
public abstract retrievePayment(paymentData: PaymentData): Promise<Data>
|
||||
|
||||
public abstract updatePayment(
|
||||
paymentSessionData: PaymentSessionData,
|
||||
cart: Cart
|
||||
): Promise<PaymentSessionData>
|
||||
|
||||
public abstract authorizePayment(
|
||||
paymentSession: PaymentSession,
|
||||
context: Data
|
||||
): Promise<{ data: PaymentSessionData; status: PaymentSessionStatus }>
|
||||
|
||||
public abstract capturePayment(payment: Payment): Promise<PaymentData>
|
||||
|
||||
public abstract refundPayment(
|
||||
payment: Payment,
|
||||
refundAmount: number
|
||||
): Promise<PaymentData>
|
||||
|
||||
public abstract cancelPayment(payment: Payment): Promise<PaymentData>
|
||||
|
||||
public abstract deletePayment(paymentSession: PaymentSession): Promise<void>
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public retrieveSavedMethods(customer: Customer): Promise<Data[]> {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
|
||||
public abstract getStatus(data: Data): Promise<PaymentSessionStatus>
|
||||
}
|
||||
|
||||
export function isPaymentService(obj: unknown): boolean {
|
||||
return obj instanceof AbstractPaymentService || obj instanceof PaymentService
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import { BasePaymentService, BaseNotificationService, BaseFulfillmentService } from 'medusa-interfaces'
|
||||
import {
|
||||
BaseNotificationService,
|
||||
BaseFulfillmentService,
|
||||
BasePaymentService,
|
||||
} from "medusa-interfaces"
|
||||
import { currencies } from "../utils/currencies"
|
||||
import { countries } from "../utils/countries"
|
||||
import { AwilixContainer } from "awilix"
|
||||
@@ -15,11 +19,15 @@ import {
|
||||
TaxProviderService,
|
||||
} from "../services"
|
||||
import { CurrencyRepository } from "../repositories/currency"
|
||||
import { AbstractTaxService } from "../interfaces"
|
||||
import { FlagRouter } from "../utils/flag-router";
|
||||
import SalesChannelFeatureFlag from "./feature-flags/sales-channels";
|
||||
import { AbstractPaymentService, AbstractTaxService } from "../interfaces"
|
||||
|
||||
const silentResolution = <T>(container: AwilixContainer, name: string, logger: Logger): T | never | undefined => {
|
||||
const silentResolution = <T>(
|
||||
container: AwilixContainer,
|
||||
name: string,
|
||||
logger: Logger
|
||||
): T | never | undefined => {
|
||||
try {
|
||||
return container.resolve<T>(name)
|
||||
} catch (err) {
|
||||
@@ -44,15 +52,23 @@ const silentResolution = <T>(container: AwilixContainer, name: string, logger: L
|
||||
`You don't have any ${identifier} provider plugins installed. You may want to add one to your project.`
|
||||
)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export default async ({ container }: { container: AwilixContainer }): Promise<void> => {
|
||||
export default async ({
|
||||
container,
|
||||
}: {
|
||||
container: AwilixContainer
|
||||
}): Promise<void> => {
|
||||
const storeService = container.resolve<StoreService>("storeService")
|
||||
const currencyRepository = container.resolve<typeof CurrencyRepository>("currencyRepository")
|
||||
const countryRepository = container.resolve<typeof CountryRepository>("countryRepository")
|
||||
const profileService = container.resolve<ShippingProfileService>("shippingProfileService")
|
||||
const currencyRepository =
|
||||
container.resolve<typeof CurrencyRepository>("currencyRepository")
|
||||
const countryRepository =
|
||||
container.resolve<typeof CountryRepository>("countryRepository")
|
||||
const profileService = container.resolve<ShippingProfileService>(
|
||||
"shippingProfileService"
|
||||
)
|
||||
const salesChannelService = container.resolve<SalesChannelService>("salesChannelService")
|
||||
const logger = container.resolve<Logger>("logger")
|
||||
const featureFlagRouter = container.resolve<FlagRouter>("featureFlagRouter")
|
||||
@@ -104,32 +120,54 @@ export default async ({ container }: { container: AwilixContainer }): Promise<vo
|
||||
await storeService.withTransaction(manager).create()
|
||||
|
||||
const payProviders =
|
||||
silentResolution<typeof BasePaymentService[]>(container, "paymentProviders", logger) || []
|
||||
silentResolution<(typeof BasePaymentService | AbstractPaymentService<never>)[]>(
|
||||
container,
|
||||
"paymentProviders",
|
||||
logger
|
||||
) || []
|
||||
const payIds = payProviders.map((p) => p.getIdentifier())
|
||||
|
||||
const pProviderService = container.resolve<PaymentProviderService>("paymentProviderService")
|
||||
const pProviderService = container.resolve<PaymentProviderService>(
|
||||
"paymentProviderService"
|
||||
)
|
||||
await pProviderService.registerInstalledProviders(payIds)
|
||||
|
||||
const notiProviders =
|
||||
silentResolution<typeof BaseNotificationService[]>(container, "notificationProviders", logger) || []
|
||||
silentResolution<typeof BaseNotificationService[]>(
|
||||
container,
|
||||
"notificationProviders",
|
||||
logger
|
||||
) || []
|
||||
const notiIds = notiProviders.map((p) => p.getIdentifier())
|
||||
|
||||
const nProviderService = container.resolve<NotificationService>("notificationService")
|
||||
const nProviderService = container.resolve<NotificationService>(
|
||||
"notificationService"
|
||||
)
|
||||
await nProviderService.registerInstalledProviders(notiIds)
|
||||
|
||||
|
||||
const fulfilProviders =
|
||||
silentResolution<typeof BaseFulfillmentService[]>(container, "fulfillmentProviders", logger) || []
|
||||
silentResolution<typeof BaseFulfillmentService[]>(
|
||||
container,
|
||||
"fulfillmentProviders",
|
||||
logger
|
||||
) || []
|
||||
const fulfilIds = fulfilProviders.map((p) => p.getIdentifier())
|
||||
|
||||
const fProviderService = container.resolve<FulfillmentProviderService>("fulfillmentProviderService")
|
||||
const fProviderService = container.resolve<FulfillmentProviderService>(
|
||||
"fulfillmentProviderService"
|
||||
)
|
||||
await fProviderService.registerInstalledProviders(fulfilIds)
|
||||
|
||||
const taxProviders =
|
||||
silentResolution<AbstractTaxService[]>(container, "taxProviders", logger) || []
|
||||
silentResolution<AbstractTaxService[]>(
|
||||
container,
|
||||
"taxProviders",
|
||||
logger
|
||||
) || []
|
||||
const taxIds = taxProviders.map((p) => p.getIdentifier())
|
||||
|
||||
const tProviderService = container.resolve<TaxProviderService>("taxProviderService")
|
||||
const tProviderService =
|
||||
container.resolve<TaxProviderService>("taxProviderService")
|
||||
await tProviderService.registerInstalledProviders(taxIds)
|
||||
|
||||
await profileService.withTransaction(manager).createDefault()
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
FileService,
|
||||
FulfillmentService,
|
||||
OauthService,
|
||||
PaymentService,
|
||||
} from "medusa-interfaces"
|
||||
import path from "path"
|
||||
import { EntitySchema } from "typeorm"
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
isCartCompletionStrategy,
|
||||
isFileService,
|
||||
isNotificationService,
|
||||
isPaymentService,
|
||||
isPriceSelectionStrategy,
|
||||
isSearchService,
|
||||
isTaxCalculationStrategy,
|
||||
@@ -366,7 +366,7 @@ export async function registerServices(
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
if (loaded.prototype instanceof PaymentService) {
|
||||
if (isPaymentService(loaded.prototype)) {
|
||||
// Register our payment providers to paymentProviders
|
||||
container.registerAdd(
|
||||
"paymentProviders",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
|
||||
export class paymentSessionUniqCartIdProviderId1660040729000 implements MigrationInterface {
|
||||
name = "paymentSessionUniqCartIdProviderId1660040729000"
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "UniqPaymentSessionCartIdProviderId" ON "payment_session" ("cart_id", "provider_id")`)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "UniqPaymentSessionCartIdProviderId"`)
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export enum PaymentSessionStatus {
|
||||
}
|
||||
|
||||
@Unique("OneSelected", ["cart_id", "is_selected"])
|
||||
@Unique("UniqPaymentSessionCartIdProviderId", ["cart_id", "provider_id"])
|
||||
@Entity()
|
||||
export class PaymentSession extends BaseEntity {
|
||||
@Index()
|
||||
|
||||
@@ -63,10 +63,10 @@ export class Payment extends BaseEntity {
|
||||
data: Record<string, unknown>
|
||||
|
||||
@Column({ type: resolveDbType("timestamptz"), nullable: true })
|
||||
captured_at: Date
|
||||
captured_at: Date | string
|
||||
|
||||
@Column({ type: resolveDbType("timestamptz"), nullable: true })
|
||||
canceled_at: Date
|
||||
canceled_at: Date | string
|
||||
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: Record<string, unknown>
|
||||
|
||||
43
packages/medusa/src/services/__mocks__/test-pay.js
Normal file
43
packages/medusa/src/services/__mocks__/test-pay.js
Normal file
@@ -0,0 +1,43 @@
|
||||
export const testPayServiceMock = {
|
||||
identifier: "test-pay",
|
||||
getIdentifier: "test-pay",
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
getStatus: jest.fn().mockResolvedValue(Promise.resolve("authorised")),
|
||||
retrieveSavedMethods: jest.fn().mockResolvedValue(Promise.resolve([])),
|
||||
getPaymentData: jest.fn().mockResolvedValue(Promise.resolve({})),
|
||||
createPayment: jest.fn().mockImplementation(() => {
|
||||
return {}
|
||||
}),
|
||||
retrievePayment: jest.fn().mockImplementation(() => {
|
||||
return {}
|
||||
}),
|
||||
updatePayment: jest.fn().mockImplementation(() => {
|
||||
return {}
|
||||
}),
|
||||
deletePayment: jest.fn().mockImplementation(() => {
|
||||
return {}
|
||||
}),
|
||||
authorizePayment: jest.fn().mockImplementation(() => {
|
||||
return {}
|
||||
}),
|
||||
updatePaymentData: jest.fn().mockImplementation(() => {
|
||||
return {}
|
||||
}),
|
||||
cancelPayment: jest.fn().mockImplementation(() => {
|
||||
return {}
|
||||
}),
|
||||
capturePayment: jest.fn().mockImplementation(() => {
|
||||
return {}
|
||||
}),
|
||||
refundPayment: jest.fn().mockImplementation(() => {
|
||||
return {}
|
||||
})
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return testPayServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
@@ -61,6 +61,8 @@ describe("CartService", () => {
|
||||
undefined,
|
||||
{
|
||||
where: { id: IdMap.getId("emptyCart") },
|
||||
select: undefined,
|
||||
relations: undefined,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { MockManager, MockRepository } from "medusa-test-utils"
|
||||
import PaymentProviderService from "../payment-provider"
|
||||
import { testPayServiceMock } from "../__mocks__/test-pay"
|
||||
|
||||
describe("ProductService", () => {
|
||||
describe("PaymentProviderService", () => {
|
||||
describe("retrieveProvider", () => {
|
||||
const container = {
|
||||
manager: MockManager,
|
||||
@@ -33,6 +34,9 @@ describe("ProductService", () => {
|
||||
manager: MockManager,
|
||||
paymentSessionRepository: MockRepository(),
|
||||
pp_default_provider: {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
createPayment,
|
||||
},
|
||||
}
|
||||
@@ -67,6 +71,9 @@ describe("ProductService", () => {
|
||||
}),
|
||||
}),
|
||||
pp_default_provider: {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
updatePayment,
|
||||
},
|
||||
}
|
||||
@@ -97,3 +104,183 @@ describe("ProductService", () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe(`PaymentProviderService`, () => {
|
||||
const container = {
|
||||
manager: MockManager,
|
||||
paymentSessionRepository: MockRepository({
|
||||
findOne: () =>
|
||||
Promise.resolve({
|
||||
id: "session",
|
||||
provider_id: "default_provider",
|
||||
data: {
|
||||
id: "1234",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
paymentRepository: 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
|
||||
}]),
|
||||
}),
|
||||
refundRepository: MockRepository(),
|
||||
pp_default_provider: testPayServiceMock,
|
||||
}
|
||||
const providerService = new PaymentProviderService(container)
|
||||
|
||||
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", {
|
||||
total: 100,
|
||||
})
|
||||
|
||||
expect(testPayServiceMock.createPayment).toBeCalledTimes(1)
|
||||
expect(testPayServiceMock.createPayment).toBeCalledWith({
|
||||
total: 100,
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully update session", async () => {
|
||||
await providerService.updateSession(
|
||||
{
|
||||
id: "session",
|
||||
provider_id: "default_provider",
|
||||
data: {
|
||||
id: "1234",
|
||||
},
|
||||
},
|
||||
{
|
||||
total: 100,
|
||||
}
|
||||
)
|
||||
|
||||
expect(testPayServiceMock.updatePayment).toBeCalledTimes(1)
|
||||
expect(testPayServiceMock.updatePayment).toBeCalledWith(
|
||||
{ id: "1234" },
|
||||
{
|
||||
total: 100,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("successfully refresh session", async () => {
|
||||
await providerService.refreshSession(
|
||||
{
|
||||
id: "session",
|
||||
provider_id: "default_provider",
|
||||
data: {
|
||||
id: "1234",
|
||||
},
|
||||
},
|
||||
{
|
||||
total: 100,
|
||||
}
|
||||
)
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import paymentService from "medusa-interfaces/dist/payment-service"
|
||||
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||
import SwapService from "../swap"
|
||||
import { InventoryServiceMock } from "../__mocks__/inventory"
|
||||
|
||||
@@ -4,7 +4,6 @@ import { DeepPartial, EntityManager, In } from "typeorm"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy"
|
||||
import {
|
||||
DiscountRuleType,
|
||||
Address,
|
||||
Cart,
|
||||
CustomShippingOption,
|
||||
@@ -13,6 +12,8 @@ import {
|
||||
LineItem,
|
||||
ShippingMethod,
|
||||
SalesChannel,
|
||||
DiscountRuleType,
|
||||
PaymentSession,
|
||||
} from "../models"
|
||||
import { AddressRepository } from "../repositories/address"
|
||||
import { CartRepository } from "../repositories/cart"
|
||||
@@ -1310,7 +1311,10 @@ class CartService extends TransactionBaseService<CartService> {
|
||||
* @param update - the data to update the payment session with
|
||||
* @return the resulting cart
|
||||
*/
|
||||
async updatePaymentSession(cartId: string, update: object): Promise<Cart> {
|
||||
async updatePaymentSession(
|
||||
cartId: string,
|
||||
update: Record<string, unknown>
|
||||
): Promise<Cart> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const cart = await this.retrieve(cartId, {
|
||||
@@ -1385,14 +1389,14 @@ class CartService extends TransactionBaseService<CartService> {
|
||||
)
|
||||
}
|
||||
|
||||
const session = await this.paymentProviderService_
|
||||
const session = (await this.paymentProviderService_
|
||||
.withTransaction(transactionManager)
|
||||
.authorizePayment(cart.payment_session, context)
|
||||
.authorizePayment(cart.payment_session, context)) as PaymentSession
|
||||
|
||||
const freshCart = await this.retrieve(cart.id, {
|
||||
const freshCart = (await this.retrieve(cart.id, {
|
||||
select: ["total"],
|
||||
relations: ["payment_sessions", "items", "items.adjustments"],
|
||||
})
|
||||
})) as Cart & { payment_session: PaymentSession }
|
||||
|
||||
if (session.status === "authorized") {
|
||||
freshCart.payment = await this.paymentProviderService_
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
Order,
|
||||
OrderStatus,
|
||||
Payment,
|
||||
PaymentSession,
|
||||
PaymentStatus,
|
||||
Return,
|
||||
Swap,
|
||||
@@ -531,7 +532,6 @@ class OrderService extends TransactionBaseService<OrderService> {
|
||||
// Would be the case if a discount code is applied that covers the item
|
||||
// total
|
||||
if (total !== 0) {
|
||||
// Throw if payment method does not exist
|
||||
if (!payment) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
@@ -543,7 +543,6 @@ class OrderService extends TransactionBaseService<OrderService> {
|
||||
.withTransaction(manager)
|
||||
.getStatus(payment)
|
||||
|
||||
// If payment status is not authorized, we throw
|
||||
if (paymentStatus !== "authorized") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
|
||||
@@ -1,438 +0,0 @@
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
/**
|
||||
* Helps retrive payment providers
|
||||
*/
|
||||
class PaymentProviderService extends BaseService {
|
||||
constructor(container) {
|
||||
super()
|
||||
|
||||
/** @private {logger} */
|
||||
this.container_ = container
|
||||
|
||||
this.manager_ = container.manager
|
||||
|
||||
this.paymentSessionRepository_ = container.paymentSessionRepository
|
||||
|
||||
this.paymentRepository_ = container.paymentRepository
|
||||
|
||||
this.refundRepository_ = container.refundRepository
|
||||
}
|
||||
|
||||
withTransaction(manager) {
|
||||
if (!manager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new PaymentProviderService(this.container_)
|
||||
cloned.transactionManager_ = manager
|
||||
cloned.manager_ = manager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
async registerInstalledProviders(providers) {
|
||||
const { manager, paymentProviderRepository } = this.container_
|
||||
|
||||
const model = manager.getCustomRepository(paymentProviderRepository)
|
||||
await model.update({}, { is_installed: false })
|
||||
|
||||
for (const p of providers) {
|
||||
const n = model.create({ id: p, is_installed: true })
|
||||
await model.save(n)
|
||||
}
|
||||
}
|
||||
|
||||
async list() {
|
||||
const { manager, paymentProviderRepository } = this.container_
|
||||
const ppRepo = manager.getCustomRepository(paymentProviderRepository)
|
||||
|
||||
return await ppRepo.find({})
|
||||
}
|
||||
|
||||
async retrievePayment(id, relations = []) {
|
||||
const paymentRepo = this.manager_.getCustomRepository(
|
||||
this.paymentRepository_
|
||||
)
|
||||
const validatedId = this.validateId_(id)
|
||||
|
||||
const query = {
|
||||
where: { id: validatedId },
|
||||
}
|
||||
|
||||
if (relations.length) {
|
||||
query.relations = relations
|
||||
}
|
||||
|
||||
const payment = await paymentRepo.findOne(query)
|
||||
|
||||
if (!payment) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Payment with ${id} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return payment
|
||||
}
|
||||
|
||||
listPayments(
|
||||
selector,
|
||||
config = { skip: 0, take: 50, order: { created_at: "DESC" } }
|
||||
) {
|
||||
const payRepo = this.manager_.getCustomRepository(this.paymentRepository_)
|
||||
const query = this.buildQuery_(selector, config)
|
||||
return payRepo.find(query)
|
||||
}
|
||||
|
||||
async retrieveSession(id, relations = []) {
|
||||
const sessionRepo = this.manager_.getCustomRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
const validatedId = this.validateId_(id)
|
||||
|
||||
const query = {
|
||||
where: { id: validatedId },
|
||||
}
|
||||
|
||||
if (relations.length) {
|
||||
query.relations = relations
|
||||
}
|
||||
|
||||
const session = await sessionRepo.findOne(query)
|
||||
|
||||
if (!session) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Payment Session with ${id} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a payment session with the given provider.
|
||||
* @param {string} providerId - the id of the provider to create payment with
|
||||
* @param {Cart} cart - a cart object used to calculate the amount, etc. from
|
||||
* @return {Promise} the payment session
|
||||
*/
|
||||
async createSession(providerId, cart) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const provider = this.retrieveProvider(providerId)
|
||||
const sessionData = await provider.createPayment(cart)
|
||||
|
||||
const sessionRepo = manager.getCustomRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
|
||||
const toCreate = {
|
||||
cart_id: cart.id,
|
||||
provider_id: providerId,
|
||||
data: sessionData,
|
||||
status: "pending",
|
||||
}
|
||||
|
||||
const created = sessionRepo.create(toCreate)
|
||||
const result = await sessionRepo.save(created)
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes a payment session with the given provider.
|
||||
* This means, that we delete the current one and create a new.
|
||||
* @param {PaymentSession} paymentSession - the payment session object to
|
||||
* update
|
||||
* @param {Cart} cart - a cart object used to calculate the amount, etc. from
|
||||
* @return {Promise} the payment session
|
||||
*/
|
||||
async refreshSession(paymentSession, cart) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const session = await this.retrieveSession(paymentSession.id)
|
||||
|
||||
const provider = this.retrieveProvider(paymentSession.provider_id)
|
||||
await provider.deletePayment(session)
|
||||
|
||||
const sessionRepo = manager.getCustomRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
|
||||
await sessionRepo.remove(session)
|
||||
|
||||
const sessionData = await provider.createPayment(cart)
|
||||
const toCreate = {
|
||||
cart_id: cart.id,
|
||||
provider_id: session.provider_id,
|
||||
data: sessionData,
|
||||
is_selected: true,
|
||||
status: "pending",
|
||||
}
|
||||
|
||||
const created = sessionRepo.create(toCreate)
|
||||
const result = await sessionRepo.save(created)
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing payment session.
|
||||
* @param {PaymentSession} paymentSession - the payment session object to
|
||||
* update
|
||||
* @param {Cart} cart - the cart object to update for
|
||||
* @return {Promise} the updated payment session
|
||||
*/
|
||||
updateSession(paymentSession, cart) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const session = await this.retrieveSession(paymentSession.id)
|
||||
|
||||
const provider = this.retrieveProvider(paymentSession.provider_id)
|
||||
session.data = await provider.updatePayment(paymentSession.data, cart)
|
||||
|
||||
const sessionRepo = manager.getCustomRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
return sessionRepo.save(session)
|
||||
})
|
||||
}
|
||||
|
||||
deleteSession(paymentSession) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const session = await this.retrieveSession(paymentSession.id).catch(
|
||||
(_) => undefined
|
||||
)
|
||||
|
||||
if (!session) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const provider = this.retrieveProvider(paymentSession.provider_id)
|
||||
await provider.deletePayment(paymentSession)
|
||||
|
||||
const sessionRepo = manager.getCustomRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
|
||||
return sessionRepo.remove(session)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a provider given an id
|
||||
* @param {string} providerId - the id of the provider to get
|
||||
* @return {PaymentService} the payment provider
|
||||
*/
|
||||
retrieveProvider(providerId) {
|
||||
try {
|
||||
let provider
|
||||
if (providerId === "system") {
|
||||
provider = this.container_[`systemPaymentProviderService`]
|
||||
} else {
|
||||
provider = this.container_[`pp_${providerId}`]
|
||||
}
|
||||
|
||||
return provider
|
||||
} catch (err) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Could not find a payment provider with id: ${providerId}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async createPayment(cart) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const { payment_session: paymentSession, region, total } = cart
|
||||
const provider = this.retrieveProvider(paymentSession.provider_id)
|
||||
const paymentData = await provider.getPaymentData(paymentSession)
|
||||
|
||||
const paymentRepo = manager.getCustomRepository(this.paymentRepository_)
|
||||
|
||||
const created = paymentRepo.create({
|
||||
provider_id: paymentSession.provider_id,
|
||||
amount: total,
|
||||
currency_code: region.currency_code,
|
||||
data: paymentData,
|
||||
cart_id: cart.id,
|
||||
})
|
||||
|
||||
return paymentRepo.save(created)
|
||||
})
|
||||
}
|
||||
|
||||
async updatePayment(paymentId, update) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const payment = await this.retrievePayment(paymentId)
|
||||
|
||||
if ("order_id" in update) {
|
||||
payment.order_id = update.order_id
|
||||
}
|
||||
|
||||
if ("swap_id" in update) {
|
||||
payment.swap_id = update.swap_id
|
||||
}
|
||||
|
||||
const payRepo = manager.getCustomRepository(this.paymentRepository_)
|
||||
return payRepo.save(payment)
|
||||
})
|
||||
}
|
||||
|
||||
async authorizePayment(paymentSession, context) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const session = await this.retrieveSession(paymentSession.id).catch(
|
||||
(_) => undefined
|
||||
)
|
||||
|
||||
if (!session) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const provider = this.retrieveProvider(paymentSession.provider_id)
|
||||
const { status, data } = await provider
|
||||
.withTransaction(manager)
|
||||
.authorizePayment(session, context)
|
||||
|
||||
session.data = data
|
||||
session.status = status
|
||||
|
||||
const sessionRepo = manager.getCustomRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
return sessionRepo.save(session)
|
||||
})
|
||||
}
|
||||
|
||||
async updateSessionData(paySession, update) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const session = await this.retrieveSession(paySession.id)
|
||||
|
||||
const provider = this.retrieveProvider(paySession.provider_id)
|
||||
|
||||
session.data = await provider.updatePaymentData(paySession.data, update)
|
||||
session.status = paySession.status
|
||||
|
||||
const sessionRepo = manager.getCustomRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
return sessionRepo.save(session)
|
||||
})
|
||||
}
|
||||
|
||||
async cancelPayment(paymentObj) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const payment = await this.retrievePayment(paymentObj.id)
|
||||
const provider = this.retrieveProvider(payment.provider_id)
|
||||
payment.data = await provider.cancelPayment(payment)
|
||||
|
||||
const now = new Date()
|
||||
payment.canceled_at = now.toISOString()
|
||||
|
||||
const paymentRepo = manager.getCustomRepository(this.paymentRepository_)
|
||||
return await paymentRepo.save(payment)
|
||||
})
|
||||
}
|
||||
|
||||
async getStatus(payment) {
|
||||
const provider = this.retrieveProvider(payment.provider_id)
|
||||
return provider.getStatus(payment.data)
|
||||
}
|
||||
|
||||
async capturePayment(paymentObj) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const payment = await this.retrievePayment(paymentObj.id)
|
||||
|
||||
const provider = this.retrieveProvider(payment.provider_id)
|
||||
payment.data = await provider.capturePayment(payment)
|
||||
|
||||
const now = new Date()
|
||||
payment.captured_at = now.toISOString()
|
||||
|
||||
const paymentRepo = manager.getCustomRepository(this.paymentRepository_)
|
||||
return paymentRepo.save(payment)
|
||||
})
|
||||
}
|
||||
|
||||
async refundPayment(payObjs, amount, reason, note) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const payments = await this.listPayments({ id: payObjs.map((p) => p.id) })
|
||||
|
||||
let order_id
|
||||
const refundable = payments.reduce((acc, next) => {
|
||||
order_id = next.order_id
|
||||
if (next.captured_at) {
|
||||
return (acc += next.amount - next.amount_refunded)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, 0)
|
||||
|
||||
if (refundable < amount) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Refund amount is too high"
|
||||
)
|
||||
}
|
||||
|
||||
let balance = amount
|
||||
|
||||
const used = []
|
||||
|
||||
const paymentRepo = manager.getCustomRepository(this.paymentRepository_)
|
||||
let toRefund = payments.find((p) => p.amount - p.amount_refunded > 0)
|
||||
while (toRefund) {
|
||||
const currentRefundable = toRefund.amount - toRefund.amount_refunded
|
||||
|
||||
const refundAmount = Math.min(currentRefundable, balance)
|
||||
|
||||
const provider = this.retrieveProvider(toRefund.provider_id)
|
||||
toRefund.data = await provider.refundPayment(toRefund, refundAmount)
|
||||
toRefund.amount_refunded += refundAmount
|
||||
await paymentRepo.save(toRefund)
|
||||
|
||||
balance -= refundAmount
|
||||
|
||||
used.push(toRefund.id)
|
||||
|
||||
if (balance > 0) {
|
||||
toRefund = payments.find(
|
||||
(p) => p.amount - p.amount_refunded > 0 && !used.includes(p.id)
|
||||
)
|
||||
} else {
|
||||
toRefund = null
|
||||
}
|
||||
}
|
||||
|
||||
const refundRepo = manager.getCustomRepository(this.refundRepository_)
|
||||
|
||||
const toCreate = {
|
||||
order_id,
|
||||
amount,
|
||||
reason,
|
||||
note,
|
||||
}
|
||||
|
||||
const created = refundRepo.create(toCreate)
|
||||
return refundRepo.save(created)
|
||||
})
|
||||
}
|
||||
|
||||
async retrieveRefund(id, config = {}) {
|
||||
const refRepo = this.manager_.getCustomRepository(this.refundRepository_)
|
||||
const query = this.buildQuery_({ id }, config)
|
||||
const refund = await refRepo.findOne(query)
|
||||
|
||||
if (!refund) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`A refund with ${id} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return refund
|
||||
}
|
||||
}
|
||||
|
||||
export default PaymentProviderService
|
||||
544
packages/medusa/src/services/payment-provider.ts
Normal file
544
packages/medusa/src/services/payment-provider.ts
Normal file
@@ -0,0 +1,544 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { BasePaymentService } from "medusa-interfaces"
|
||||
import { AbstractPaymentService, TransactionBaseService } from "../interfaces"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { PaymentSessionRepository } from "../repositories/payment-session"
|
||||
import { PaymentRepository } from "../repositories/payment"
|
||||
import { RefundRepository } from "../repositories/refund"
|
||||
import { PaymentProviderRepository } from "../repositories/payment-provider"
|
||||
import { buildQuery } from "../utils"
|
||||
import { FindConfig, Selector } from "../types/common"
|
||||
import {
|
||||
Cart,
|
||||
Payment,
|
||||
PaymentProvider,
|
||||
PaymentSession,
|
||||
PaymentSessionStatus,
|
||||
Refund,
|
||||
} from "../models"
|
||||
|
||||
type PaymentProviderKey = `pp_${string}` | "systemPaymentProviderService"
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
paymentSessionRepository: typeof PaymentSessionRepository
|
||||
paymentProviderRepository: typeof PaymentProviderRepository
|
||||
paymentRepository: typeof PaymentRepository
|
||||
refundRepository: typeof RefundRepository
|
||||
} & {
|
||||
[key in `${PaymentProviderKey}`]:
|
||||
| AbstractPaymentService<never>
|
||||
| typeof BasePaymentService
|
||||
}
|
||||
|
||||
/**
|
||||
* Helps retrieve payment providers
|
||||
*/
|
||||
export default class PaymentProviderService extends TransactionBaseService<PaymentProviderService> {
|
||||
protected manager_: EntityManager
|
||||
protected transactionManager_: EntityManager | undefined
|
||||
protected readonly container_: InjectedDependencies
|
||||
protected readonly paymentSessionRepository_: typeof PaymentSessionRepository
|
||||
protected readonly paymentProviderRepository_: typeof PaymentProviderRepository
|
||||
protected readonly paymentRepository_: typeof PaymentRepository
|
||||
protected readonly refundRepository_: typeof RefundRepository
|
||||
|
||||
constructor(container: InjectedDependencies) {
|
||||
super(container)
|
||||
|
||||
this.container_ = container
|
||||
this.manager_ = container.manager
|
||||
this.paymentSessionRepository_ = container.paymentSessionRepository
|
||||
this.paymentProviderRepository_ = container.paymentProviderRepository
|
||||
this.paymentRepository_ = container.paymentRepository
|
||||
this.refundRepository_ = container.refundRepository
|
||||
}
|
||||
|
||||
async registerInstalledProviders(providerIds: string[]): Promise<void> {
|
||||
return await this.atomicPhase_(async (transactionManager) => {
|
||||
const model = transactionManager.getCustomRepository(
|
||||
this.paymentProviderRepository_
|
||||
)
|
||||
await model.update({}, { is_installed: false })
|
||||
|
||||
await Promise.all(
|
||||
providerIds.map(async (providerId) => {
|
||||
const provider = model.create({
|
||||
id: providerId,
|
||||
is_installed: true,
|
||||
})
|
||||
return await model.save(provider)
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async list(): Promise<PaymentProvider[]> {
|
||||
const ppRepo = this.manager_.getCustomRepository(
|
||||
this.paymentProviderRepository_
|
||||
)
|
||||
return await ppRepo.find()
|
||||
}
|
||||
|
||||
async retrievePayment(
|
||||
id: string,
|
||||
relations: string[] = []
|
||||
): Promise<Payment | never> {
|
||||
const paymentRepo = this.manager_.getCustomRepository(
|
||||
this.paymentRepository_
|
||||
)
|
||||
const query = {
|
||||
where: { id },
|
||||
relations: [] as string[],
|
||||
}
|
||||
|
||||
if (relations.length) {
|
||||
query.relations = relations
|
||||
}
|
||||
|
||||
const payment = await paymentRepo.findOne(query)
|
||||
|
||||
if (!payment) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Payment with ${id} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return payment
|
||||
}
|
||||
|
||||
async listPayments(
|
||||
selector: Selector<Payment>,
|
||||
config: FindConfig<Payment> = {
|
||||
skip: 0,
|
||||
take: 50,
|
||||
order: { created_at: "DESC" },
|
||||
}
|
||||
): Promise<Payment[]> {
|
||||
const payRepo = this.manager_.getCustomRepository(this.paymentRepository_)
|
||||
const query = buildQuery(selector, config)
|
||||
return await payRepo.find(query)
|
||||
}
|
||||
|
||||
async retrieveSession(
|
||||
id: string,
|
||||
relations: string[] = []
|
||||
): Promise<PaymentSession | never> {
|
||||
const sessionRepo = this.manager_.getCustomRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
|
||||
const query = {
|
||||
where: { id },
|
||||
relations: [] as string[],
|
||||
}
|
||||
|
||||
if (relations.length) {
|
||||
query.relations = relations
|
||||
}
|
||||
|
||||
const session = await sessionRepo.findOne(query)
|
||||
|
||||
if (!session) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Payment Session with ${id} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a payment session with the given provider.
|
||||
* @param providerId - the id of the provider to create payment with
|
||||
* @param cart - a cart object used to calculate the amount, etc. from
|
||||
* @return the payment session
|
||||
*/
|
||||
async createSession(providerId: string, cart: Cart): Promise<PaymentSession> {
|
||||
return await this.atomicPhase_(async (transactionManager) => {
|
||||
const provider = this.retrieveProvider(providerId)
|
||||
const sessionData = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.createPayment(cart)
|
||||
|
||||
const sessionRepo = transactionManager.getCustomRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
|
||||
const toCreate = {
|
||||
cart_id: cart.id,
|
||||
provider_id: providerId,
|
||||
data: sessionData,
|
||||
status: "pending",
|
||||
}
|
||||
|
||||
const created = sessionRepo.create(toCreate)
|
||||
return await sessionRepo.save(created)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes a payment session with the given provider.
|
||||
* This means, that we delete the current one and create a new.
|
||||
* @param paymentSession - the payment session object to
|
||||
* update
|
||||
* @param cart - a cart object used to calculate the amount, etc. from
|
||||
* @return the payment session
|
||||
*/
|
||||
async refreshSession(
|
||||
paymentSession: PaymentSession,
|
||||
cart: Cart
|
||||
): Promise<PaymentSession> {
|
||||
return this.atomicPhase_(async (transactionManager) => {
|
||||
const session = await this.retrieveSession(paymentSession.id)
|
||||
const provider = this.retrieveProvider(paymentSession.provider_id)
|
||||
await provider.withTransaction(transactionManager).deletePayment(session)
|
||||
|
||||
const sessionRepo = transactionManager.getCustomRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
|
||||
await sessionRepo.remove(session)
|
||||
|
||||
const sessionData = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.createPayment(cart)
|
||||
|
||||
const toCreate = {
|
||||
cart_id: cart.id,
|
||||
provider_id: session.provider_id,
|
||||
data: sessionData,
|
||||
is_selected: true,
|
||||
status: "pending",
|
||||
}
|
||||
|
||||
const created = sessionRepo.create(toCreate)
|
||||
return await sessionRepo.save(created)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing payment session.
|
||||
* @param paymentSession - the payment session object to
|
||||
* update
|
||||
* @param cart - the cart object to update for
|
||||
* @return the updated payment session
|
||||
*/
|
||||
async updateSession(
|
||||
paymentSession: PaymentSession,
|
||||
cart: Cart
|
||||
): Promise<PaymentSession> {
|
||||
return await this.atomicPhase_(async (transactionManager) => {
|
||||
const session = await this.retrieveSession(paymentSession.id)
|
||||
const provider = this.retrieveProvider(paymentSession.provider_id)
|
||||
session.data = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.updatePayment(paymentSession.data, cart)
|
||||
|
||||
const sessionRepo = transactionManager.getCustomRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
return sessionRepo.save(session)
|
||||
})
|
||||
}
|
||||
|
||||
async deleteSession(
|
||||
paymentSession: PaymentSession
|
||||
): Promise<PaymentSession | undefined> {
|
||||
return await this.atomicPhase_(async (transactionManager) => {
|
||||
const session = await this.retrieveSession(paymentSession.id).catch(
|
||||
() => void 0
|
||||
)
|
||||
|
||||
if (!session) {
|
||||
return
|
||||
}
|
||||
|
||||
const provider = this.retrieveProvider(paymentSession.provider_id)
|
||||
await provider
|
||||
.withTransaction(transactionManager)
|
||||
.deletePayment(paymentSession)
|
||||
|
||||
const sessionRepo = transactionManager.getCustomRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
|
||||
return sessionRepo.remove(session)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a provider given an id
|
||||
* @param {string} providerId - the id of the provider to get
|
||||
* @return {PaymentService} the payment provider
|
||||
*/
|
||||
retrieveProvider<
|
||||
TProvider extends AbstractPaymentService<never> | typeof BasePaymentService
|
||||
>(
|
||||
providerId: string
|
||||
): TProvider extends AbstractPaymentService<never>
|
||||
? AbstractPaymentService<never>
|
||||
: typeof BasePaymentService {
|
||||
try {
|
||||
let provider
|
||||
if (providerId === "system") {
|
||||
provider = this.container_[`systemPaymentProviderService`]
|
||||
} else {
|
||||
provider = this.container_[`pp_${providerId}`]
|
||||
}
|
||||
|
||||
return provider
|
||||
} catch (err) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Could not find a payment provider with id: ${providerId}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async createPayment(
|
||||
cart: Cart & { payment_session: PaymentSession }
|
||||
): Promise<Payment> {
|
||||
return await this.atomicPhase_(async (transactionManager) => {
|
||||
const { payment_session: paymentSession, region, total } = cart
|
||||
|
||||
const provider = this.retrieveProvider(paymentSession.provider_id)
|
||||
const paymentData = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.getPaymentData(paymentSession)
|
||||
|
||||
const paymentRepo = transactionManager.getCustomRepository(
|
||||
this.paymentRepository_
|
||||
)
|
||||
|
||||
const created = paymentRepo.create({
|
||||
provider_id: paymentSession.provider_id,
|
||||
amount: total,
|
||||
currency_code: region.currency_code,
|
||||
data: paymentData,
|
||||
cart_id: cart.id,
|
||||
})
|
||||
|
||||
return paymentRepo.save(created)
|
||||
})
|
||||
}
|
||||
|
||||
async updatePayment(
|
||||
paymentId: string,
|
||||
data: { order_id?: string; swap_id?: string }
|
||||
): Promise<Payment> {
|
||||
return await this.atomicPhase_(async (transactionManager) => {
|
||||
const payment = await this.retrievePayment(paymentId)
|
||||
|
||||
if (data?.order_id) {
|
||||
payment.order_id = data.order_id
|
||||
}
|
||||
|
||||
if (data?.swap_id) {
|
||||
payment.swap_id = data.swap_id
|
||||
}
|
||||
|
||||
const payRepo = transactionManager.getCustomRepository(
|
||||
this.paymentRepository_
|
||||
)
|
||||
return payRepo.save(payment)
|
||||
})
|
||||
}
|
||||
|
||||
async authorizePayment(
|
||||
paymentSession: PaymentSession,
|
||||
context: Record<string, unknown>
|
||||
): Promise<PaymentSession | undefined> {
|
||||
return await this.atomicPhase_(async (transactionManager) => {
|
||||
const session = await this.retrieveSession(paymentSession.id).catch(
|
||||
() => void 0
|
||||
)
|
||||
|
||||
if (!session) {
|
||||
return
|
||||
}
|
||||
|
||||
const provider = this.retrieveProvider(paymentSession.provider_id)
|
||||
const { status, data } = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.authorizePayment(session, context)
|
||||
|
||||
session.data = data
|
||||
session.status = status
|
||||
|
||||
const sessionRepo = transactionManager.getCustomRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
return sessionRepo.save(session)
|
||||
})
|
||||
}
|
||||
|
||||
async updateSessionData(
|
||||
paymentSession: PaymentSession,
|
||||
data: Record<string, unknown>
|
||||
): Promise<PaymentSession> {
|
||||
return await this.atomicPhase_(async (transactionManager) => {
|
||||
const session = await this.retrieveSession(paymentSession.id)
|
||||
|
||||
const provider = this.retrieveProvider(paymentSession.provider_id)
|
||||
|
||||
session.data = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.updatePaymentData(paymentSession.data, data)
|
||||
session.status = paymentSession.status
|
||||
|
||||
const sessionRepo = transactionManager.getCustomRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
return sessionRepo.save(session)
|
||||
})
|
||||
}
|
||||
|
||||
async cancelPayment(
|
||||
paymentObj: Partial<Payment> & { id: string }
|
||||
): Promise<Payment> {
|
||||
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)
|
||||
|
||||
const now = new Date()
|
||||
payment.canceled_at = now.toISOString()
|
||||
|
||||
const paymentRepo = transactionManager.getCustomRepository(
|
||||
this.paymentRepository_
|
||||
)
|
||||
return await paymentRepo.save(payment)
|
||||
})
|
||||
}
|
||||
|
||||
async getStatus(payment: Payment): Promise<PaymentSessionStatus> {
|
||||
const provider = this.retrieveProvider(payment.provider_id)
|
||||
return await provider.withTransaction(this.manager_).getStatus(payment.data)
|
||||
}
|
||||
|
||||
async capturePayment(
|
||||
paymentObj: Partial<Payment> & { id: string }
|
||||
): Promise<Payment> {
|
||||
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)
|
||||
|
||||
const now = new Date()
|
||||
payment.captured_at = now.toISOString()
|
||||
|
||||
const paymentRepo = transactionManager.getCustomRepository(
|
||||
this.paymentRepository_
|
||||
)
|
||||
return paymentRepo.save(payment)
|
||||
})
|
||||
}
|
||||
|
||||
async refundPayment(
|
||||
payObjs: Payment[],
|
||||
amount: number,
|
||||
reason: string,
|
||||
note?: string
|
||||
): Promise<Refund> {
|
||||
return await this.atomicPhase_(async (transactionManager) => {
|
||||
const payments = await this.listPayments({
|
||||
id: payObjs.map((p) => p.id),
|
||||
})
|
||||
|
||||
let order_id!: string
|
||||
const refundable = payments.reduce((acc, next) => {
|
||||
order_id = next.order_id
|
||||
if (next.captured_at) {
|
||||
return (acc += next.amount - next.amount_refunded)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, 0)
|
||||
|
||||
if (refundable < amount) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Refund amount is higher that the refundable amount"
|
||||
)
|
||||
}
|
||||
|
||||
let balance = amount
|
||||
|
||||
const used: string[] = []
|
||||
|
||||
const paymentRepo = transactionManager.getCustomRepository(
|
||||
this.paymentRepository_
|
||||
)
|
||||
let paymentToRefund = payments.find(
|
||||
(payment) => payment.amount - payment.amount_refunded > 0
|
||||
)
|
||||
|
||||
while (paymentToRefund) {
|
||||
const currentRefundable =
|
||||
paymentToRefund.amount - paymentToRefund.amount_refunded
|
||||
|
||||
const refundAmount = Math.min(currentRefundable, balance)
|
||||
|
||||
const provider = this.retrieveProvider(paymentToRefund.provider_id)
|
||||
paymentToRefund.data = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.refundPayment(paymentToRefund, refundAmount)
|
||||
|
||||
paymentToRefund.amount_refunded += refundAmount
|
||||
await paymentRepo.save(paymentToRefund)
|
||||
|
||||
balance -= refundAmount
|
||||
|
||||
used.push(paymentToRefund.id)
|
||||
|
||||
if (balance > 0) {
|
||||
paymentToRefund = payments.find(
|
||||
(payment) =>
|
||||
payment.amount - payment.amount_refunded > 0 &&
|
||||
!used.includes(payment.id)
|
||||
)
|
||||
} else {
|
||||
paymentToRefund = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const refundRepo = transactionManager.getCustomRepository(
|
||||
this.refundRepository_
|
||||
)
|
||||
|
||||
const toCreate = {
|
||||
order_id,
|
||||
amount,
|
||||
reason,
|
||||
note,
|
||||
}
|
||||
|
||||
const created = refundRepo.create(toCreate)
|
||||
return refundRepo.save(created)
|
||||
})
|
||||
}
|
||||
|
||||
async retrieveRefund(
|
||||
id: string,
|
||||
config: FindConfig<Refund> = {}
|
||||
): Promise<Refund | never> {
|
||||
const refRepo = this.manager_.getCustomRepository(this.refundRepository_)
|
||||
const query = buildQuery({ id }, config)
|
||||
const refund = await refRepo.findOne(query)
|
||||
|
||||
if (!refund) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`A refund with ${id} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return refund
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
|
||||
class SystemProviderService extends BaseService {
|
||||
class SystemProviderService extends TransactionBaseService {
|
||||
static identifier = "system"
|
||||
|
||||
constructor(_) {
|
||||
super()
|
||||
super(_)
|
||||
}
|
||||
|
||||
async createPayment(_) {
|
||||
|
||||
@@ -22,14 +22,11 @@
|
||||
"skipLibCheck": true,
|
||||
"downlevelIteration": true // to use ES5 specific tooling
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*",
|
||||
"index.d.ts"
|
||||
],
|
||||
"include": ["./src/**/*", "index.d.ts"],
|
||||
"exclude": [
|
||||
"./dist/**/*",
|
||||
"./src/**/__tests__",
|
||||
"./src/**/__mocks__",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user