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:
@@ -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";
|
static identifier = "test-pay";
|
||||||
|
|
||||||
constructor() {
|
constructor(_) {
|
||||||
super();
|
super(_);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStatus(paymentData) {
|
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:
|
The first step to create a payment provider is to create a file in `src/services` with the following content:
|
||||||
|
|
||||||
```jsx
|
```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 {
|
const {
|
||||||
simpleProductTaxRateFactory,
|
simpleProductTaxRateFactory,
|
||||||
simpleShippingTaxRateFactory,
|
|
||||||
simpleShippingOptionFactory,
|
|
||||||
simpleCartFactory,
|
simpleCartFactory,
|
||||||
simpleRegionFactory,
|
simpleRegionFactory,
|
||||||
simpleProductFactory,
|
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"
|
static identifier = "test-pay"
|
||||||
|
|
||||||
constructor() {
|
constructor(_) {
|
||||||
super()
|
super(_)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStatus(paymentData) {
|
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"
|
static identifier = "test-pay"
|
||||||
|
|
||||||
constructor() {
|
constructor(_) {
|
||||||
super()
|
super(_)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStatus(paymentData) {
|
async getStatus(paymentData) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export { default as BaseService } from "./base-service"
|
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 FulfillmentService } from "./fulfillment-service"
|
||||||
|
export { default as FileService } from "./file-service"
|
||||||
export { default as NotificationService } from "./notification-service"
|
export { default as NotificationService } from "./notification-service"
|
||||||
export { default as OauthService } from "./oauth-service"
|
export { default as OauthService } from "./oauth-service"
|
||||||
export { default as PaymentService } from "./payment-service"
|
|
||||||
export { default as SearchService } from "./search-service"
|
export { default as SearchService } from "./search-service"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { defaultStoreCartFields, defaultStoreCartRelations } from "."
|
|||||||
import { CartService } from "../../../../services"
|
import { CartService } from "../../../../services"
|
||||||
import { decorateLineItemsWithTotals } from "./decorate-line-items-with-totals"
|
import { decorateLineItemsWithTotals } from "./decorate-line-items-with-totals"
|
||||||
import { EntityManager } from "typeorm";
|
import { EntityManager } from "typeorm";
|
||||||
|
import IdempotencyKeyService from "../../../../services/idempotency-key";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [post] /carts/{id}/payment-sessions
|
* @oas [post] /carts/{id}/payment-sessions
|
||||||
@@ -26,17 +27,97 @@ export default async (req, res) => {
|
|||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
|
|
||||||
const cartService: CartService = req.scope.resolve("cartService")
|
const cartService: CartService = req.scope.resolve("cartService")
|
||||||
|
const idempotencyKeyService: IdempotencyKeyService = req.scope.resolve(
|
||||||
|
"idempotencyKeyService"
|
||||||
|
)
|
||||||
const manager: EntityManager = req.scope.resolve("manager")
|
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, {
|
const headerKey = req.get("Idempotency-Key") || ""
|
||||||
select: defaultStoreCartFields,
|
|
||||||
relations: defaultStoreCartRelations,
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await decorateLineItemsWithTotals(cart, req)
|
let idempotencyKey
|
||||||
res.status(200).json({ cart: data })
|
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 (
|
export const decorateLineItemsWithTotals = async (
|
||||||
cart: Cart,
|
cart: Cart,
|
||||||
req: Request,
|
req: Request,
|
||||||
options: { force_taxes: boolean } = { force_taxes: false }
|
options: { force_taxes: boolean, transactionManager?: EntityManager } = { force_taxes: false }
|
||||||
): Promise<Cart> => {
|
): Promise<Cart> => {
|
||||||
const totalsService: TotalsService = req.scope.resolve("totalsService")
|
const totalsService: TotalsService = req.scope.resolve("totalsService")
|
||||||
|
|
||||||
if (cart.items && cart.region) {
|
if (cart.items && cart.region) {
|
||||||
const manager: EntityManager = req.scope.resolve("manager")
|
const getItems = async (manager) => {
|
||||||
const items = await manager.transaction(async (transactionManager) => {
|
const totalsServiceTx = totalsService.withTransaction(manager)
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
cart.items.map(async (item: LineItem) => {
|
cart.items.map(async (item: LineItem) => {
|
||||||
const itemTotals = await totalsService
|
const itemTotals = await totalsServiceTx
|
||||||
.withTransaction(transactionManager)
|
|
||||||
.getLineItemTotals(item, cart, {
|
.getLineItemTotals(item, cart, {
|
||||||
include_tax: options.force_taxes || cart.region.automatic_taxes,
|
include_tax: options.force_taxes || cart.region.automatic_taxes,
|
||||||
})
|
})
|
||||||
@@ -24,7 +23,17 @@ export const decorateLineItemsWithTotals = async (
|
|||||||
return Object.assign(item, itemTotals)
|
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 })
|
return Object.assign(cart, { items })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,5 +53,5 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
export class StorePostCartsCartPaymentSessionUpdateReq {
|
export class StorePostCartsCartPaymentSessionUpdateReq {
|
||||||
@IsObject()
|
@IsObject()
|
||||||
data: object
|
data: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ export * from "./price-selection-strategy"
|
|||||||
export * from "./models/base-entity"
|
export * from "./models/base-entity"
|
||||||
export * from "./models/soft-deletable-entity"
|
export * from "./models/soft-deletable-entity"
|
||||||
export * from "./search-service"
|
export * from "./search-service"
|
||||||
|
export * from "./payment-service"
|
||||||
|
|||||||
@@ -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 { currencies } from "../utils/currencies"
|
||||||
import { countries } from "../utils/countries"
|
import { countries } from "../utils/countries"
|
||||||
import { AwilixContainer } from "awilix"
|
import { AwilixContainer } from "awilix"
|
||||||
@@ -15,11 +19,15 @@ import {
|
|||||||
TaxProviderService,
|
TaxProviderService,
|
||||||
} from "../services"
|
} from "../services"
|
||||||
import { CurrencyRepository } from "../repositories/currency"
|
import { CurrencyRepository } from "../repositories/currency"
|
||||||
import { AbstractTaxService } from "../interfaces"
|
|
||||||
import { FlagRouter } from "../utils/flag-router";
|
import { FlagRouter } from "../utils/flag-router";
|
||||||
import SalesChannelFeatureFlag from "./feature-flags/sales-channels";
|
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 {
|
try {
|
||||||
return container.resolve<T>(name)
|
return container.resolve<T>(name)
|
||||||
} catch (err) {
|
} 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.`
|
`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 storeService = container.resolve<StoreService>("storeService")
|
||||||
const currencyRepository = container.resolve<typeof CurrencyRepository>("currencyRepository")
|
const currencyRepository =
|
||||||
const countryRepository = container.resolve<typeof CountryRepository>("countryRepository")
|
container.resolve<typeof CurrencyRepository>("currencyRepository")
|
||||||
const profileService = container.resolve<ShippingProfileService>("shippingProfileService")
|
const countryRepository =
|
||||||
|
container.resolve<typeof CountryRepository>("countryRepository")
|
||||||
|
const profileService = container.resolve<ShippingProfileService>(
|
||||||
|
"shippingProfileService"
|
||||||
|
)
|
||||||
const salesChannelService = container.resolve<SalesChannelService>("salesChannelService")
|
const salesChannelService = container.resolve<SalesChannelService>("salesChannelService")
|
||||||
const logger = container.resolve<Logger>("logger")
|
const logger = container.resolve<Logger>("logger")
|
||||||
const featureFlagRouter = container.resolve<FlagRouter>("featureFlagRouter")
|
const featureFlagRouter = container.resolve<FlagRouter>("featureFlagRouter")
|
||||||
@@ -104,32 +120,54 @@ export default async ({ container }: { container: AwilixContainer }): Promise<vo
|
|||||||
await storeService.withTransaction(manager).create()
|
await storeService.withTransaction(manager).create()
|
||||||
|
|
||||||
const payProviders =
|
const payProviders =
|
||||||
silentResolution<typeof BasePaymentService[]>(container, "paymentProviders", logger) || []
|
silentResolution<(typeof BasePaymentService | AbstractPaymentService<never>)[]>(
|
||||||
|
container,
|
||||||
|
"paymentProviders",
|
||||||
|
logger
|
||||||
|
) || []
|
||||||
const payIds = payProviders.map((p) => p.getIdentifier())
|
const payIds = payProviders.map((p) => p.getIdentifier())
|
||||||
|
|
||||||
const pProviderService = container.resolve<PaymentProviderService>("paymentProviderService")
|
const pProviderService = container.resolve<PaymentProviderService>(
|
||||||
|
"paymentProviderService"
|
||||||
|
)
|
||||||
await pProviderService.registerInstalledProviders(payIds)
|
await pProviderService.registerInstalledProviders(payIds)
|
||||||
|
|
||||||
const notiProviders =
|
const notiProviders =
|
||||||
silentResolution<typeof BaseNotificationService[]>(container, "notificationProviders", logger) || []
|
silentResolution<typeof BaseNotificationService[]>(
|
||||||
|
container,
|
||||||
|
"notificationProviders",
|
||||||
|
logger
|
||||||
|
) || []
|
||||||
const notiIds = notiProviders.map((p) => p.getIdentifier())
|
const notiIds = notiProviders.map((p) => p.getIdentifier())
|
||||||
|
|
||||||
const nProviderService = container.resolve<NotificationService>("notificationService")
|
const nProviderService = container.resolve<NotificationService>(
|
||||||
|
"notificationService"
|
||||||
|
)
|
||||||
await nProviderService.registerInstalledProviders(notiIds)
|
await nProviderService.registerInstalledProviders(notiIds)
|
||||||
|
|
||||||
|
|
||||||
const fulfilProviders =
|
const fulfilProviders =
|
||||||
silentResolution<typeof BaseFulfillmentService[]>(container, "fulfillmentProviders", logger) || []
|
silentResolution<typeof BaseFulfillmentService[]>(
|
||||||
|
container,
|
||||||
|
"fulfillmentProviders",
|
||||||
|
logger
|
||||||
|
) || []
|
||||||
const fulfilIds = fulfilProviders.map((p) => p.getIdentifier())
|
const fulfilIds = fulfilProviders.map((p) => p.getIdentifier())
|
||||||
|
|
||||||
const fProviderService = container.resolve<FulfillmentProviderService>("fulfillmentProviderService")
|
const fProviderService = container.resolve<FulfillmentProviderService>(
|
||||||
|
"fulfillmentProviderService"
|
||||||
|
)
|
||||||
await fProviderService.registerInstalledProviders(fulfilIds)
|
await fProviderService.registerInstalledProviders(fulfilIds)
|
||||||
|
|
||||||
const taxProviders =
|
const taxProviders =
|
||||||
silentResolution<AbstractTaxService[]>(container, "taxProviders", logger) || []
|
silentResolution<AbstractTaxService[]>(
|
||||||
|
container,
|
||||||
|
"taxProviders",
|
||||||
|
logger
|
||||||
|
) || []
|
||||||
const taxIds = taxProviders.map((p) => p.getIdentifier())
|
const taxIds = taxProviders.map((p) => p.getIdentifier())
|
||||||
|
|
||||||
const tProviderService = container.resolve<TaxProviderService>("taxProviderService")
|
const tProviderService =
|
||||||
|
container.resolve<TaxProviderService>("taxProviderService")
|
||||||
await tProviderService.registerInstalledProviders(taxIds)
|
await tProviderService.registerInstalledProviders(taxIds)
|
||||||
|
|
||||||
await profileService.withTransaction(manager).createDefault()
|
await profileService.withTransaction(manager).createDefault()
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
FileService,
|
FileService,
|
||||||
FulfillmentService,
|
FulfillmentService,
|
||||||
OauthService,
|
OauthService,
|
||||||
PaymentService,
|
|
||||||
} from "medusa-interfaces"
|
} from "medusa-interfaces"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { EntitySchema } from "typeorm"
|
import { EntitySchema } from "typeorm"
|
||||||
@@ -20,6 +19,7 @@ import {
|
|||||||
isCartCompletionStrategy,
|
isCartCompletionStrategy,
|
||||||
isFileService,
|
isFileService,
|
||||||
isNotificationService,
|
isNotificationService,
|
||||||
|
isPaymentService,
|
||||||
isPriceSelectionStrategy,
|
isPriceSelectionStrategy,
|
||||||
isSearchService,
|
isSearchService,
|
||||||
isTaxCalculationStrategy,
|
isTaxCalculationStrategy,
|
||||||
@@ -366,7 +366,7 @@ export async function registerServices(
|
|||||||
throw new Error(message)
|
throw new Error(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loaded.prototype instanceof PaymentService) {
|
if (isPaymentService(loaded.prototype)) {
|
||||||
// Register our payment providers to paymentProviders
|
// Register our payment providers to paymentProviders
|
||||||
container.registerAdd(
|
container.registerAdd(
|
||||||
"paymentProviders",
|
"paymentProviders",
|
||||||
|
|||||||
+13
@@ -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("OneSelected", ["cart_id", "is_selected"])
|
||||||
|
@Unique("UniqPaymentSessionCartIdProviderId", ["cart_id", "provider_id"])
|
||||||
@Entity()
|
@Entity()
|
||||||
export class PaymentSession extends BaseEntity {
|
export class PaymentSession extends BaseEntity {
|
||||||
@Index()
|
@Index()
|
||||||
|
|||||||
@@ -63,10 +63,10 @@ export class Payment extends BaseEntity {
|
|||||||
data: Record<string, unknown>
|
data: Record<string, unknown>
|
||||||
|
|
||||||
@Column({ type: resolveDbType("timestamptz"), nullable: true })
|
@Column({ type: resolveDbType("timestamptz"), nullable: true })
|
||||||
captured_at: Date
|
captured_at: Date | string
|
||||||
|
|
||||||
@Column({ type: resolveDbType("timestamptz"), nullable: true })
|
@Column({ type: resolveDbType("timestamptz"), nullable: true })
|
||||||
canceled_at: Date
|
canceled_at: Date | string
|
||||||
|
|
||||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||||
metadata: Record<string, unknown>
|
metadata: Record<string, unknown>
|
||||||
|
|||||||
@@ -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,
|
undefined,
|
||||||
{
|
{
|
||||||
where: { id: IdMap.getId("emptyCart") },
|
where: { id: IdMap.getId("emptyCart") },
|
||||||
|
select: undefined,
|
||||||
|
relations: undefined,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { MockManager, MockRepository } from "medusa-test-utils"
|
import { MockManager, MockRepository } from "medusa-test-utils"
|
||||||
import PaymentProviderService from "../payment-provider"
|
import PaymentProviderService from "../payment-provider"
|
||||||
|
import { testPayServiceMock } from "../__mocks__/test-pay"
|
||||||
|
|
||||||
describe("ProductService", () => {
|
describe("PaymentProviderService", () => {
|
||||||
describe("retrieveProvider", () => {
|
describe("retrieveProvider", () => {
|
||||||
const container = {
|
const container = {
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
@@ -33,6 +34,9 @@ describe("ProductService", () => {
|
|||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
paymentSessionRepository: MockRepository(),
|
paymentSessionRepository: MockRepository(),
|
||||||
pp_default_provider: {
|
pp_default_provider: {
|
||||||
|
withTransaction: function () {
|
||||||
|
return this
|
||||||
|
},
|
||||||
createPayment,
|
createPayment,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -67,6 +71,9 @@ describe("ProductService", () => {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
pp_default_provider: {
|
pp_default_provider: {
|
||||||
|
withTransaction: function () {
|
||||||
|
return this
|
||||||
|
},
|
||||||
updatePayment,
|
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 { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||||
import SwapService from "../swap"
|
import SwapService from "../swap"
|
||||||
import { InventoryServiceMock } from "../__mocks__/inventory"
|
import { InventoryServiceMock } from "../__mocks__/inventory"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { DeepPartial, EntityManager, In } from "typeorm"
|
|||||||
import { TransactionBaseService } from "../interfaces"
|
import { TransactionBaseService } from "../interfaces"
|
||||||
import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy"
|
import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy"
|
||||||
import {
|
import {
|
||||||
DiscountRuleType,
|
|
||||||
Address,
|
Address,
|
||||||
Cart,
|
Cart,
|
||||||
CustomShippingOption,
|
CustomShippingOption,
|
||||||
@@ -13,6 +12,8 @@ import {
|
|||||||
LineItem,
|
LineItem,
|
||||||
ShippingMethod,
|
ShippingMethod,
|
||||||
SalesChannel,
|
SalesChannel,
|
||||||
|
DiscountRuleType,
|
||||||
|
PaymentSession,
|
||||||
} from "../models"
|
} from "../models"
|
||||||
import { AddressRepository } from "../repositories/address"
|
import { AddressRepository } from "../repositories/address"
|
||||||
import { CartRepository } from "../repositories/cart"
|
import { CartRepository } from "../repositories/cart"
|
||||||
@@ -1310,7 +1311,10 @@ class CartService extends TransactionBaseService<CartService> {
|
|||||||
* @param update - the data to update the payment session with
|
* @param update - the data to update the payment session with
|
||||||
* @return the resulting cart
|
* @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_(
|
return await this.atomicPhase_(
|
||||||
async (transactionManager: EntityManager) => {
|
async (transactionManager: EntityManager) => {
|
||||||
const cart = await this.retrieve(cartId, {
|
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)
|
.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"],
|
select: ["total"],
|
||||||
relations: ["payment_sessions", "items", "items.adjustments"],
|
relations: ["payment_sessions", "items", "items.adjustments"],
|
||||||
})
|
})) as Cart & { payment_session: PaymentSession }
|
||||||
|
|
||||||
if (session.status === "authorized") {
|
if (session.status === "authorized") {
|
||||||
freshCart.payment = await this.paymentProviderService_
|
freshCart.payment = await this.paymentProviderService_
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
Order,
|
Order,
|
||||||
OrderStatus,
|
OrderStatus,
|
||||||
Payment,
|
Payment,
|
||||||
|
PaymentSession,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
Return,
|
Return,
|
||||||
Swap,
|
Swap,
|
||||||
@@ -531,7 +532,6 @@ class OrderService extends TransactionBaseService<OrderService> {
|
|||||||
// Would be the case if a discount code is applied that covers the item
|
// Would be the case if a discount code is applied that covers the item
|
||||||
// total
|
// total
|
||||||
if (total !== 0) {
|
if (total !== 0) {
|
||||||
// Throw if payment method does not exist
|
|
||||||
if (!payment) {
|
if (!payment) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_ARGUMENT,
|
MedusaError.Types.INVALID_ARGUMENT,
|
||||||
@@ -543,7 +543,6 @@ class OrderService extends TransactionBaseService<OrderService> {
|
|||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
.getStatus(payment)
|
.getStatus(payment)
|
||||||
|
|
||||||
// If payment status is not authorized, we throw
|
|
||||||
if (paymentStatus !== "authorized") {
|
if (paymentStatus !== "authorized") {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_ARGUMENT,
|
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
|
|
||||||
@@ -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"
|
static identifier = "system"
|
||||||
|
|
||||||
constructor(_) {
|
constructor(_) {
|
||||||
super()
|
super(_)
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPayment(_) {
|
async createPayment(_) {
|
||||||
|
|||||||
@@ -22,14 +22,11 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"downlevelIteration": true // to use ES5 specific tooling
|
"downlevelIteration": true // to use ES5 specific tooling
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["./src/**/*", "index.d.ts"],
|
||||||
"./src/**/*",
|
|
||||||
"index.d.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"./dist/**/*",
|
"./dist/**/*",
|
||||||
"./src/**/__tests__",
|
"./src/**/__tests__",
|
||||||
"./src/**/__mocks__",
|
"./src/**/__mocks__",
|
||||||
"node_modules"
|
"node_modules"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user