feat(payment): provider service (#6308)

This commit is contained in:
Frane Polić
2024-02-12 20:57:41 +01:00
committed by GitHub
parent 869dc751a0
commit a6a4b3f01a
21 changed files with 1511 additions and 637 deletions
+10
View File
@@ -0,0 +1,10 @@
import { IPaymentModuleService, LoaderOptions } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
export default async ({ container }: LoaderOptions): Promise<void> => {
const paymentModuleService: IPaymentModuleService = container.resolve(
ModuleRegistrationName.PAYMENT
)
await paymentModuleService.createProvidersOnLoad()
}
+1 -1
View File
@@ -1,4 +1,4 @@
export * from "./connection"
export * from "./container"
export * from "./providers"
export * from "./defaults"
+8 -4
View File
@@ -3,9 +3,11 @@ import { moduleProviderLoader } from "@medusajs/modules-sdk"
import { LoaderOptions, ModuleProvider, ModulesSdkTypes } from "@medusajs/types"
import { Lifetime, asFunction } from "awilix"
import * as providers from "../providers"
const registrationFn = async (klass, container, pluginOptions) => {
container.register({
[`payment_provider_${klass.prototype}`]: asFunction(
[`pp_${klass.identifier}`]: asFunction(
(cradle) => new klass(cradle, pluginOptions),
{
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
@@ -30,12 +32,14 @@ export default async ({
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
) & { providers: ModuleProvider[] }
>): Promise<void> => {
const pluginProviders =
options?.providers?.filter((provider) => provider.resolve) || []
// Local providers
for (const provider of Object.values(providers)) {
await registrationFn(provider, container, {})
}
await moduleProviderLoader({
container,
providers: pluginProviders,
providers: options?.providers || [],
registerServiceFn: registrationFn,
})
}
@@ -43,19 +43,21 @@ export default class PaymentCollection {
})
amount: number
@Property({
columnType: "numeric",
nullable: true,
serializer: optionalNumericSerializer,
})
authorized_amount: number | null = null
// TODO: make this computed properties
@Property({
columnType: "numeric",
nullable: true,
serializer: optionalNumericSerializer,
})
refunded_amount: number | null = null
// @Property({
// columnType: "numeric",
// nullable: true,
// serializer: optionalNumericSerializer,
// })
// authorized_amount: number | null = null
//
// @Property({
// columnType: "numeric",
// nullable: true,
// serializer: optionalNumericSerializer,
// })
// refunded_amount: number | null = null
@Property({ columnType: "text", index: "IDX_payment_collection_region_id" })
region_id: string
@@ -16,7 +16,7 @@ import Payment from "./payment"
@Entity({ tableName: "payment_session" })
export default class PaymentSession {
[OptionalProps]?: "status"
[OptionalProps]?: "status" | "data"
@PrimaryKey({ columnType: "text" })
id: string
@@ -33,8 +33,8 @@ export default class PaymentSession {
@Property({ columnType: "text" })
provider_id: string
@Property({ columnType: "jsonb", nullable: true })
data: Record<string, unknown> | null = null
@Property({ columnType: "jsonb" })
data: Record<string, unknown> = {}
@Enum({
items: () => PaymentSessionStatus,
+7 -1
View File
@@ -5,6 +5,7 @@ import { PaymentModuleService } from "@services"
import loadConnection from "./loaders/connection"
import loadContainer from "./loaders/container"
import loadProviders from "./loaders/providers"
import loadDefaults from "./loaders/defaults"
import { Modules } from "@medusajs/modules-sdk"
import { ModulesSdkUtils } from "@medusajs/utils"
@@ -25,7 +26,12 @@ export const revertMigration = ModulesSdkUtils.buildRevertMigrationScript(
)
const service = PaymentModuleService
const loaders = [loadContainer, loadConnection, loadProviders] as any
const loaders = [
loadContainer,
loadConnection,
loadProviders,
loadDefaults,
] as any
export const moduleDefinition: ModuleExports = {
service,
+1
View File
@@ -0,0 +1 @@
export { default as SystemPaymentProvider } from "./system"
+71
View File
@@ -0,0 +1,71 @@
import {
PaymentProviderContext,
PaymentProviderError,
PaymentProviderSessionResponse,
PaymentSessionStatus,
} from "@medusajs/types"
import { AbstractPaymentProvider } from "@medusajs/utils"
export class SystemProviderService extends AbstractPaymentProvider {
static identifier = "system"
async getStatus(_): Promise<string> {
return "authorized"
}
async getPaymentData(_): Promise<Record<string, unknown>> {
return {}
}
async initiatePayment(
context: PaymentProviderContext
): Promise<PaymentProviderSessionResponse> {
return { data: {} }
}
async getPaymentStatus(
paymentSessionData: Record<string, unknown>
): Promise<PaymentSessionStatus> {
throw new Error("Method not implemented.")
}
async retrievePayment(
paymentSessionData: Record<string, unknown>
): Promise<Record<string, unknown> | PaymentProviderError> {
return {}
}
async authorizePayment(_): Promise<
| PaymentProviderError
| {
status: PaymentSessionStatus
data: PaymentProviderSessionResponse["data"]
}
> {
return { data: {}, status: PaymentSessionStatus.AUTHORIZED }
}
async updatePayment(
_
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
return { data: {} } as PaymentProviderSessionResponse
}
async deletePayment(_): Promise<Record<string, unknown>> {
return {}
}
async capturePayment(_): Promise<Record<string, unknown>> {
return {}
}
async refundPayment(_): Promise<Record<string, unknown>> {
return {}
}
async cancelPayment(_): Promise<Record<string, unknown>> {
return {}
}
}
export default SystemProviderService
+1
View File
@@ -1 +1,2 @@
export { default as PaymentModuleService } from "./payment-module"
export { default as PaymentProviderService } from "./payment-provider"
+343 -281
View File
@@ -4,6 +4,7 @@ import {
CreateCaptureDTO,
CreatePaymentCollectionDTO,
CreatePaymentDTO,
CreatePaymentProviderDTO,
CreatePaymentSessionDTO,
CreateRefundDTO,
DAL,
@@ -14,20 +15,18 @@ import {
PaymentCollectionDTO,
PaymentDTO,
PaymentSessionDTO,
PaymentSessionStatus,
RefundDTO,
SetPaymentSessionsDTO,
UpdatePaymentCollectionDTO,
UpdatePaymentDTO,
UpdatePaymentSessionDTO,
} from "@medusajs/types"
import {
InjectTransactionManager,
MedusaContext,
ModulesSdkUtils,
MedusaError,
InjectManager,
ModulesSdkUtils,
} from "@medusajs/utils"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import {
Capture,
Payment,
@@ -36,6 +35,9 @@ import {
Refund,
} from "@models"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import PaymentProviderService from "./payment-provider"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
paymentService: ModulesSdkTypes.InternalModuleService<any>
@@ -43,9 +45,10 @@ type InjectedDependencies = {
refundService: ModulesSdkTypes.InternalModuleService<any>
paymentSessionService: ModulesSdkTypes.InternalModuleService<any>
paymentCollectionService: ModulesSdkTypes.InternalModuleService<any>
paymentProviderService: PaymentProviderService
}
const generateMethodForModels = [PaymentCollection, PaymentSession]
const generateMethodForModels = [PaymentCollection, Payment]
export default class PaymentModuleService<
TPaymentCollection extends PaymentCollection = PaymentCollection,
@@ -74,6 +77,7 @@ export default class PaymentModuleService<
protected refundService_: ModulesSdkTypes.InternalModuleService<TRefund>
protected paymentSessionService_: ModulesSdkTypes.InternalModuleService<TPaymentSession>
protected paymentCollectionService_: ModulesSdkTypes.InternalModuleService<TPaymentCollection>
protected paymentProviderService_: PaymentProviderService
constructor(
{
@@ -82,6 +86,7 @@ export default class PaymentModuleService<
captureService,
refundService,
paymentSessionService,
paymentProviderService,
paymentCollectionService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
@@ -95,6 +100,7 @@ export default class PaymentModuleService<
this.captureService_ = captureService
this.paymentService_ = paymentService
this.paymentSessionService_ = paymentSessionService
this.paymentProviderService_ = paymentProviderService
this.paymentCollectionService_ = paymentCollectionService
}
@@ -102,18 +108,18 @@ export default class PaymentModuleService<
return joinerConfig
}
createPaymentCollection(
createPaymentCollections(
data: CreatePaymentCollectionDTO,
sharedContext?: Context
): Promise<PaymentCollectionDTO>
createPaymentCollection(
createPaymentCollections(
data: CreatePaymentCollectionDTO[],
sharedContext?: Context
): Promise<PaymentCollectionDTO[]>
@InjectTransactionManager("baseRepository_")
async createPaymentCollection(
async createPaymentCollections(
data: CreatePaymentCollectionDTO | CreatePaymentCollectionDTO[],
@MedusaContext() sharedContext?: Context
): Promise<PaymentCollectionDTO | PaymentCollectionDTO[]> {
@@ -132,17 +138,17 @@ export default class PaymentModuleService<
)
}
updatePaymentCollection(
updatePaymentCollections(
data: UpdatePaymentCollectionDTO[],
sharedContext?: Context
): Promise<PaymentCollectionDTO[]>
updatePaymentCollection(
updatePaymentCollections(
data: UpdatePaymentCollectionDTO,
sharedContext?: Context
): Promise<PaymentCollectionDTO>
@InjectTransactionManager("baseRepository_")
async updatePaymentCollection(
async updatePaymentCollections(
data: UpdatePaymentCollectionDTO | UpdatePaymentCollectionDTO[],
sharedContext?: Context
): Promise<PaymentCollectionDTO | PaymentCollectionDTO[]> {
@@ -160,305 +166,361 @@ export default class PaymentModuleService<
)
}
createPayment(
data: CreatePaymentDTO,
completePaymentCollections(
paymentCollectionId: string,
sharedContext?: Context
): Promise<PaymentDTO>
createPayment(
data: CreatePaymentDTO[],
): Promise<PaymentCollectionDTO>
completePaymentCollections(
paymentCollectionId: string[],
sharedContext?: Context
): Promise<PaymentDTO[]>
): Promise<PaymentCollectionDTO[]>
@InjectTransactionManager("baseRepository_")
async createPayment(
data: CreatePaymentDTO | CreatePaymentDTO[],
async completePaymentCollections(
paymentCollectionId: string | string[],
@MedusaContext() sharedContext?: Context
): Promise<PaymentDTO | PaymentDTO[]> {
let input = Array.isArray(data) ? data : [data]
): Promise<PaymentCollectionDTO | PaymentCollectionDTO[]> {
const input = Array.isArray(paymentCollectionId)
? paymentCollectionId.map((id) => ({
id,
completed_at: new Date(),
}))
: [{ id: paymentCollectionId, completed_at: new Date() }]
input = input.map((inputData) => ({
payment_collection: inputData.payment_collection_id,
payment_session: inputData.payment_session_id,
...inputData,
}))
// TODO: what checks should be done here? e.g. captured_amount === amount?
const payments = await this.paymentService_.create(input, sharedContext)
return await this.baseRepository_.serialize<PaymentDTO[]>(
Array.isArray(data) ? payments : payments[0],
{
populate: true,
}
const updated = await this.paymentCollectionService_.update(
input,
sharedContext
)
}
updatePayment(
data: UpdatePaymentDTO,
sharedContext?: Context | undefined
): Promise<PaymentDTO>
updatePayment(
data: UpdatePaymentDTO[],
sharedContext?: Context | undefined
): Promise<PaymentDTO[]>
@InjectTransactionManager("baseRepository_")
async updatePayment(
data: UpdatePaymentDTO | UpdatePaymentDTO[],
@MedusaContext() sharedContext?: Context
): Promise<PaymentDTO | PaymentDTO[]> {
const input = Array.isArray(data) ? data : [data]
const result = await this.paymentService_.update(input, sharedContext)
return await this.baseRepository_.serialize<PaymentDTO[]>(
Array.isArray(data) ? result : result[0],
{
populate: true,
}
)
}
capturePayment(
data: CreateCaptureDTO,
sharedContext?: Context
): Promise<PaymentDTO>
capturePayment(
data: CreateCaptureDTO[],
sharedContext?: Context
): Promise<PaymentDTO[]>
@InjectManager("baseRepository_")
async capturePayment(
data: CreateCaptureDTO | CreateCaptureDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PaymentDTO | PaymentDTO[]> {
const input = Array.isArray(data) ? data : [data]
const payments = await this.capturePaymentBulk_(input, sharedContext)
return await this.baseRepository_.serialize(
Array.isArray(data) ? payments : payments[0],
Array.isArray(paymentCollectionId) ? updated : updated[0],
{ populate: true }
)
}
@InjectTransactionManager("baseRepository_")
protected async capturePaymentBulk_(
data: CreateCaptureDTO[],
@MedusaContext() sharedContext?: Context
): Promise<Payment[]> {
let payments = await this.paymentService_.list(
{ id: data.map((d) => d.payment_id) },
{},
sharedContext
)
const inputMap = new Map(data.map((d) => [d.payment_id, d]))
for (const payment of payments) {
const input = inputMap.get(payment.id)!
if (payment.captured_at) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The payment is already fully captured."
)
}
// TODO: revisit when https://github.com/medusajs/medusa/pull/6253 is merged
// if (payment.captured_amount + input.amount > payment.authorized_amount) {
// throw new MedusaError(
// MedusaError.Types.INVALID_DATA,
// `Total captured amount for payment: ${payment.id} exceeds authorized amount.`
// )
// }
}
await this.captureService_.create(
data.map((d) => ({
payment: d.payment_id,
amount: d.amount,
captured_by: d.captured_by,
})),
sharedContext
)
let fullyCapturedPaymentsId: string[] = []
for (const payment of payments) {
const input = inputMap.get(payment.id)!
// TODO: revisit when https://github.com/medusajs/medusa/pull/6253 is merged
// if (payment.captured_amount + input.amount === payment.amount) {
// fullyCapturedPaymentsId.push(payment.id)
// }
}
if (fullyCapturedPaymentsId.length) {
await this.paymentService_.update(
fullyCapturedPaymentsId.map((id) => ({ id, captured_at: new Date() })),
sharedContext
)
}
// TODO: set PaymentCollection status if fully captured
return await this.paymentService_.list(
{ id: data.map((d) => d.payment_id) },
{
relations: ["captures"],
},
sharedContext
)
}
refundPayment(
data: CreateRefundDTO,
sharedContext?: Context
): Promise<PaymentDTO>
refundPayment(
data: CreateRefundDTO[],
sharedContext?: Context
): Promise<PaymentDTO[]>
@InjectManager("baseRepository_")
async refundPayment(
data: CreateRefundDTO | CreateRefundDTO[],
@MedusaContext() sharedContext?: Context
): Promise<PaymentDTO | PaymentDTO[]> {
const input = Array.isArray(data) ? data : [data]
const payments = await this.refundPaymentBulk_(input, sharedContext)
return await this.baseRepository_.serialize(
Array.isArray(data) ? payments : payments[0],
{ populate: true }
)
}
@InjectTransactionManager("baseRepository_")
async refundPaymentBulk_(
data: CreateRefundDTO[],
@MedusaContext() sharedContext?: Context
): Promise<Payment[]> {
const payments = await this.paymentService_.list(
{ id: data.map(({ payment_id }) => payment_id) },
{},
sharedContext
)
const inputMap = new Map(data.map((d) => [d.payment_id, d]))
// TODO: revisit when https://github.com/medusajs/medusa/pull/6253 is merged
// for (const payment of payments) {
// const input = inputMap.get(payment.id)!
// if (payment.captured_amount < input.amount) {
// throw new MedusaError(
// MedusaError.Types.INVALID_DATA,
// `Refund amount for payment: ${payment.id} cannot be greater than the amount captured on the payment.`
// )
// }
// }
await this.refundService_.create(
data.map((d) => ({
payment: d.payment_id,
amount: d.amount,
captured_by: d.created_by,
})),
sharedContext
)
return await this.paymentService_.list(
{ id: data.map(({ payment_id }) => payment_id) },
{
relations: ["refunds"],
},
sharedContext
)
}
createPaymentSession(
paymentCollectionId: string,
data: CreatePaymentSessionDTO,
sharedContext?: Context | undefined
): Promise<PaymentCollectionDTO>
createPaymentSession(
paymentCollectionId: string,
data: CreatePaymentSessionDTO[],
sharedContext?: Context | undefined
): Promise<PaymentCollectionDTO>
@InjectTransactionManager("baseRepository_")
async createPaymentSession(
paymentCollectionId: string,
data: CreatePaymentSessionDTO | CreatePaymentSessionDTO[],
data: CreatePaymentSessionDTO,
@MedusaContext() sharedContext?: Context
): Promise<PaymentCollectionDTO> {
let input = Array.isArray(data) ? data : [data]
): Promise<PaymentSessionDTO> {
const sessionData = await this.paymentProviderService_.createSession(
data.provider_id,
data.providerContext
)
input = input.map((inputData) => ({
payment_collection: paymentCollectionId,
...inputData,
}))
await this.paymentSessionService_.create(input, sharedContext)
return await this.retrievePaymentCollection(
paymentCollectionId,
const created = await this.paymentSessionService_.create(
{
relations: ["payment_sessions"],
provider_id: data.provider_id,
amount: data.providerContext.amount,
currency_code: data.providerContext.currency_code,
payment_collection: paymentCollectionId,
data: sessionData,
},
sharedContext
)
return await this.baseRepository_.serialize(created, { populate: true })
}
@InjectTransactionManager("baseRepository_")
async updatePaymentSession(
data: UpdatePaymentSessionDTO,
@MedusaContext() sharedContext?: Context
): Promise<PaymentSessionDTO> {
const session = await this.paymentSessionService_.retrieve(
data.id,
{ select: ["id", "data", "provider_id"] },
sharedContext
)
const sessionData = await this.paymentProviderService_.updateSession(
session.provider_id,
data.providerContext
)
const updated = await this.paymentSessionService_.update(
{
id: session.id,
amount: data.providerContext.amount,
currency_code: data.providerContext.currency_code,
data: sessionData,
},
sharedContext
)
return await this.baseRepository_.serialize(updated[0], { populate: true })
}
@InjectTransactionManager("baseRepository_")
async deletePaymentSession(
id: string,
@MedusaContext() sharedContext?: Context
): Promise<void> {
const session = await this.paymentSessionService_.retrieve(
id,
{ select: ["id", "data", "provider_id"] },
sharedContext
)
await this.paymentProviderService_.deleteSession({
provider_id: session.provider_id,
data: session.data,
})
await this.paymentSessionService_.delete(id, sharedContext)
}
@InjectTransactionManager("baseRepository_")
async authorizePaymentSession(
id: string,
context: Record<string, unknown>,
@MedusaContext() sharedContext?: Context
): Promise<PaymentDTO> {
const session = await this.paymentSessionService_.retrieve(
id,
{
select: ["id", "data", "provider_id", "amount", "currency_code"],
relations: ["payment_collection"],
},
sharedContext
)
if (session.authorized_at) {
const payment = await this.paymentService_.retrieve(
{ session_id: session.id },
{},
sharedContext
)
return await this.baseRepository_.serialize(payment, { populate: true })
}
const { data, status } =
await this.paymentProviderService_.authorizePayment(
{
provider_id: session.provider_id,
data: session.data,
},
context
)
await this.paymentSessionService_.update(
{
id: session.id,
data,
status,
authorized_at:
status === PaymentSessionStatus.AUTHORIZED ? new Date() : null,
},
sharedContext
)
if (status !== PaymentSessionStatus.AUTHORIZED) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Session: ${session.id} is not authorized with the provider.`
)
}
// TODO: update status on payment collection if authorized_amount === amount - depends on the BigNumber PR
const payment = await this.paymentService_.create(
{
amount: session.amount,
currency_code: session.currency_code,
authorized_amount: session.amount,
payment_session: session.id,
payment_collection: session.payment_collection!.id,
provider_id: session.provider_id,
// customer_id: context.customer.id,
data,
},
sharedContext
)
return await this.retrievePayment(payment.id, {}, sharedContext)
}
@InjectTransactionManager("baseRepository_")
async updatePayment(
data: UpdatePaymentDTO,
@MedusaContext() sharedContext?: Context
): Promise<PaymentDTO> {
// NOTE: currently there is no update with the provider but maybe data could be updated
const result = await this.paymentService_.update(data, sharedContext)
return await this.baseRepository_.serialize<PaymentDTO>(result[0], {
populate: true,
})
}
@InjectTransactionManager("baseRepository_")
async capturePayment(
data: CreateCaptureDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<PaymentDTO> {
const payment = await this.paymentService_.retrieve(
data.payment_id,
{ select: ["id", "data", "provider_id"] },
sharedContext
)
if (payment.canceled_at) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The payment: ${payment.id} has been canceled.`
)
}
if (payment.captured_at) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The payment: ${payment.id} is already fully captured.`
)
}
// TODO: revisit when https://github.com/medusajs/medusa/pull/6253 is merged
// if (payment.captured_amount + input.amount > payment.authorized_amount) {
// throw new MedusaError(
// MedusaError.Types.INVALID_DATA,
// `Total captured amount for payment: ${payment.id} exceeds authorized amount.`
// )
// }
const paymentData = await this.paymentProviderService_.capturePayment({
data: payment.data!,
provider_id: payment.provider_id,
})
await this.captureService_.create(
{
payment: data.payment_id,
amount: data.amount,
captured_by: data.captured_by,
},
sharedContext
)
await this.paymentService_.update(
{ id: payment.id, data: paymentData },
sharedContext
)
// TODO: revisit when https://github.com/medusajs/medusa/pull/6253 is merged
// if (payment.captured_amount + data.amount === payment.amount) {
// await this.paymentService_.update(
// { id: payment.id, captured_at: new Date() },
// sharedContext
// )
// }
return await this.retrievePayment(
payment.id,
{ relations: ["captures"] },
sharedContext
)
}
/**
* TODO
*/
@InjectTransactionManager("baseRepository_")
async refundPayment(
data: CreateRefundDTO,
@MedusaContext() sharedContext?: Context
): Promise<PaymentDTO> {
const payment = await this.paymentService_.retrieve(
data.payment_id,
{ select: ["id", "data", "provider_id"] },
sharedContext
)
authorizePaymentCollection(
paymentCollectionId: string,
sharedContext?: Context | undefined
): Promise<PaymentCollectionDTO> {
throw new Error("Method not implemented.")
}
completePaymentCollection(
paymentCollectionId: string,
sharedContext?: Context | undefined
): Promise<PaymentCollectionDTO> {
throw new Error("Method not implemented.")
// TODO: revisit when https://github.com/medusajs/medusa/pull/6253 is merged
// if (payment.captured_amount < input.amount) {
// throw new MedusaError(
// MedusaError.Types.INVALID_DATA,
// `Refund amount for payment: ${payment.id} cannot be greater than the amount captured on the payment.`
// )
// }
const paymentData = await this.paymentProviderService_.refundPayment(
{
data: payment.data!,
provider_id: payment.provider_id,
},
data.amount
)
await this.refundService_.create(
{
payment: data.payment_id,
amount: data.amount,
created_by: data.created_by,
},
sharedContext
)
await this.paymentService_.update({ id: payment.id, data: paymentData })
return await this.retrievePayment(
payment.id,
{ relations: ["refunds"] },
sharedContext
)
}
cancelPayment(paymentId: string, sharedContext?: Context): Promise<PaymentDTO>
cancelPayment(
paymentId: string[],
sharedContext?: Context
): Promise<PaymentDTO[]>
@InjectTransactionManager("baseRepository_")
async cancelPayment(
paymentId: string,
@MedusaContext() sharedContext?: Context
): Promise<PaymentDTO> {
const payment = await this.paymentService_.retrieve(
paymentId,
{ select: ["id", "data", "provider_id"] },
sharedContext
)
cancelPayment(
paymentId: string | string[],
sharedContext?: Context
): Promise<PaymentDTO | PaymentDTO[]> {
throw new Error("Method not implemented.")
// TODO: revisit when totals are implemented
// if (payment.captured_amount !== 0) {
// throw new MedusaError(
// MedusaError.Types.INVALID_DATA,
// `Cannot cancel a payment: ${payment.id} that has been captured.`
// )
// }
await this.paymentProviderService_.cancelPayment({
data: payment.data!,
provider_id: payment.provider_id,
})
await this.paymentService_.update(
{ id: paymentId, canceled_at: new Date() },
sharedContext
)
return await this.retrievePayment(payment.id, {}, sharedContext)
}
authorizePaymentSessions(
paymentCollectionId: string,
sessionIds: string[],
sharedContext?: Context | undefined
): Promise<PaymentCollectionDTO> {
throw new Error("Method not implemented.")
}
completePaymentSessions(
paymentCollectionId: string,
sessionIds: string[],
sharedContext?: Context | undefined
): Promise<PaymentCollectionDTO> {
throw new Error("Method not implemented.")
}
setPaymentSessions(
paymentCollectionId: string,
data: SetPaymentSessionsDTO[],
sharedContext?: Context | undefined
): Promise<PaymentCollectionDTO> {
throw new Error("Method not implemented.")
async createProvidersOnLoad() {
const providersToLoad = this.__container__["payment_providers"]
const providers = await this.paymentProviderService_.list({
// @ts-ignore TODO
id: providersToLoad.map((p) => p.getIdentifier()),
})
const loadedProvidersMap = new Map(providers.map((p) => [p.id, p]))
const providersToCreate: CreatePaymentProviderDTO[] = []
for (const provider of providersToLoad) {
if (loadedProvidersMap.has(provider.getIdentifier())) {
continue
}
providersToCreate.push({
id: provider.getIdentifier(),
})
}
await this.paymentProviderService_.create(providersToCreate)
}
}
@@ -0,0 +1,183 @@
import { EOL } from "os"
import { isDefined, MedusaError } from "medusa-core-utils"
import {
Context,
CreatePaymentProviderDTO,
DAL,
InternalModuleDeclaration,
IPaymentProvider,
PaymentProviderAuthorizeResponse,
PaymentProviderContext,
PaymentProviderDataInput,
PaymentProviderError,
PaymentProviderSessionResponse,
PaymentSessionStatus,
} from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
isPaymentProviderError,
MedusaContext,
} from "@medusajs/utils"
import { PaymentProvider } from "@models"
type InjectedDependencies = {
paymentProviderRepository: DAL.RepositoryService
[key: `pp_${string}`]: IPaymentProvider
}
export default class PaymentProviderService {
protected readonly container_: InjectedDependencies
protected readonly paymentProviderRepository_: DAL.RepositoryService
constructor(
container: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
this.container_ = container
this.paymentProviderRepository_ = container.paymentProviderRepository
}
@InjectTransactionManager("paymentProviderRepository_")
async create(
data: CreatePaymentProviderDTO[],
@MedusaContext() sharedContext?: Context
): Promise<PaymentProvider[]> {
return await this.paymentProviderRepository_.create(data, sharedContext)
}
@InjectManager("paymentProviderRepository_")
async list(
@MedusaContext() sharedContext?: Context
): Promise<PaymentProvider[]> {
return await this.paymentProviderRepository_.find(undefined, sharedContext)
}
retrieveProvider(providerId: string): IPaymentProvider {
try {
return this.container_[`pp_${providerId}`] as IPaymentProvider
} catch (e) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Could not find a payment provider with id: ${providerId}`
)
}
}
async createSession(
providerId: string,
sessionInput: PaymentProviderContext
): Promise<PaymentProviderSessionResponse["data"]> {
const provider = this.retrieveProvider(providerId)
if (
!isDefined(sessionInput.currency_code) ||
!isDefined(sessionInput.amount)
) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"`currency_code` and `amount` are required to create payment session."
)
}
const paymentResponse = await provider.initiatePayment(sessionInput)
if (isPaymentProviderError(paymentResponse)) {
this.throwPaymentProviderError(paymentResponse)
}
return (paymentResponse as PaymentProviderSessionResponse).data
}
async updateSession(
providerId: string,
sessionInput: PaymentProviderContext
): Promise<Record<string, unknown> | undefined> {
const provider = this.retrieveProvider(providerId)
const paymentResponse = await provider.updatePayment(sessionInput)
if (isPaymentProviderError(paymentResponse)) {
this.throwPaymentProviderError(paymentResponse)
}
return (paymentResponse as PaymentProviderSessionResponse)?.data
}
async deleteSession(input: PaymentProviderDataInput): Promise<void> {
const provider = this.retrieveProvider(input.provider_id)
const error = await provider.deletePayment(input.data)
if (isPaymentProviderError(error)) {
this.throwPaymentProviderError(error)
}
}
async authorizePayment(
input: PaymentProviderDataInput,
context: Record<string, unknown>
): Promise<{ data: Record<string, unknown>; status: PaymentSessionStatus }> {
const provider = this.retrieveProvider(input.provider_id)
const res = await provider.authorizePayment(input.data, context)
if (isPaymentProviderError(res)) {
this.throwPaymentProviderError(res)
}
const { data, status } = res as PaymentProviderAuthorizeResponse
return { data, status }
}
async getStatus(
input: PaymentProviderDataInput
): Promise<PaymentSessionStatus> {
const provider = this.retrieveProvider(input.provider_id)
return await provider.getPaymentStatus(input.data)
}
async capturePayment(
input: PaymentProviderDataInput
): Promise<Record<string, unknown>> {
const provider = this.retrieveProvider(input.provider_id)
const res = await provider.capturePayment(input.data)
if (isPaymentProviderError(res)) {
this.throwPaymentProviderError(res)
}
return res as Record<string, unknown>
}
async cancelPayment(input: PaymentProviderDataInput): Promise<void> {
const provider = this.retrieveProvider(input.provider_id)
const error = await provider.cancelPayment(input.data)
if (isPaymentProviderError(error)) {
this.throwPaymentProviderError(error)
}
}
async refundPayment(
input: PaymentProviderDataInput,
amount: number
): Promise<Record<string, unknown>> {
const provider = this.retrieveProvider(input.provider_id)
const res = await provider.refundPayment(input.data, amount)
if (isPaymentProviderError(res)) {
this.throwPaymentProviderError(res)
}
return res as Record<string, unknown>
}
private throwPaymentProviderError(errObj: PaymentProviderError) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`${errObj.error}${errObj.detail ? `:${EOL}${errObj.detail}` : ""}`,
errObj.code
)
}
}