feat(payment): payment and session methods (#6138)

This commit is contained in:
Frane Polić
2024-02-06 13:40:22 +01:00
committed by GitHub
parent 5cabe9585f
commit 2104843826
13 changed files with 1239 additions and 319 deletions
@@ -0,0 +1,67 @@
export const defaultPaymentCollectionData = [
{
id: "pay-col-id-1",
amount: 100,
region_id: "region-id-1",
currency_code: "usd",
},
{
id: "pay-col-id-2",
amount: 200,
region_id: "region-id-1",
currency_code: "usd",
},
{
id: "pay-col-id-3",
amount: 300,
region_id: "region-id-2",
currency_code: "usd",
},
]
export const defaultPaymentSessionData = [
{
id: "pay-sess-id-1",
amount: 100,
currency_code: "usd",
provider_id: "manual",
payment_collection: "pay-col-id-1",
},
{
id: "pay-sess-id-2",
amount: 100,
currency_code: "usd",
provider_id: "manual",
payment_collection: "pay-col-id-2",
},
{
id: "pay-sess-id-3",
amount: 100,
currency_code: "usd",
provider_id: "manual",
payment_collection: "pay-col-id-2",
},
]
export const defaultPaymentData = [
{
id: "pay-id-1",
amount: 100,
currency_code: "usd",
payment_collection: "pay-col-id-1",
payment_session: "pay-sess-id-1",
provider_id: "manual",
authorized_amount: 100,
data: {},
},
{
id: "pay-id-2",
amount: 100,
authorized_amount: 100,
currency_code: "usd",
payment_collection: "pay-col-id-2",
payment_session: "pay-sess-id-2",
provider_id: "manual",
data: {},
},
]
@@ -0,0 +1,53 @@
import { EntityName } from "@mikro-orm/core"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { Payment, PaymentSession, PaymentCollection } from "@models"
import {
defaultPaymentCollectionData,
defaultPaymentData,
defaultPaymentSessionData,
} from "./data"
export * from "./data"
async function createEntities<
T extends EntityName<Payment | PaymentCollection | PaymentSession>
>(manager: SqlEntityManager, entity: T, data: any[]) {
const created: T[] = []
for (let record of data) {
created.push(manager.create(entity, record))
}
await manager.persistAndFlush(created)
return created
}
export async function createPaymentCollections(
manager: SqlEntityManager,
paymentCollectionData = defaultPaymentCollectionData
): Promise<PaymentCollection[]> {
return await createEntities<PaymentCollection>(
manager,
PaymentCollection,
paymentCollectionData
)
}
export async function createPaymentSessions(
manager: SqlEntityManager,
paymentSessionData = defaultPaymentSessionData
): Promise<PaymentCollection[]> {
return await createEntities<PaymentSession>(
manager,
PaymentSession,
paymentSessionData
)
}
export async function createPayments(
manager: SqlEntityManager,
paymentData = defaultPaymentData
): Promise<PaymentCollection[]> {
return await createEntities<Payment>(manager, Payment, paymentData)
}
@@ -1,20 +0,0 @@
export const defaultPaymentCollectionData = [
{
id: "pay-col-id-1",
amount: 100,
region_id: "region-id-1",
currency_code: "usd",
},
{
id: "pay-col-id-2",
amount: 200,
region_id: "region-id-1",
currency_code: "usd",
},
{
id: "pay-col-id-3",
amount: 300,
region_id: "region-id-2",
currency_code: "usd",
},
]
@@ -1,22 +0,0 @@
import { CreatePaymentCollectionDTO } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { PaymentCollection } from "../../../src/models"
import { defaultPaymentCollectionData } from "./data"
export * from "./data"
export async function createPaymentCollections(
manager: SqlEntityManager,
paymentCollectionData: CreatePaymentCollectionDTO[] = defaultPaymentCollectionData
): Promise<PaymentCollection[]> {
const collections: PaymentCollection[] = []
for (let data of paymentCollectionData) {
let collection = manager.create(PaymentCollection, data)
await manager.persistAndFlush(collection)
}
return collections
}
@@ -1,230 +1,586 @@
import { IPaymentModuleService } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { Modules } from "@medusajs/modules-sdk"
import { initModules } from "medusa-test-utils"
import { initialize } from "../../../../src/initialize"
import { createPaymentCollections } from "../../../__fixtures__/payment-collection"
import { initialize } from "../../../../src"
import { DB_URL, MikroOrmWrapper } from "../../../utils"
import {
createPaymentCollections,
createPaymentSessions,
createPayments,
} from "../../../__fixtures__"
import { getInitModuleConfig } from "../../../utils/get-init-module-config"
import { initModules } from "medusa-test-utils"
import { Modules } from "@medusajs/modules-sdk"
jest.setTimeout(30000)
describe("Payment Module Service", () => {
let service: IPaymentModuleService
let repositoryManager: SqlEntityManager
let shutdownFunc: () => Promise<void>
beforeAll(async () => {
const initModulesConfig = getInitModuleConfig()
describe("PaymentCollection", () => {
let repositoryManager: SqlEntityManager
let shutdownFunc: () => Promise<void>
const { medusaApp, shutdown } = await initModules(initModulesConfig)
beforeAll(async () => {
const initModulesConfig = getInitModuleConfig()
service = medusaApp.modules[Modules.PAYMENT]
const { medusaApp, shutdown } = await initModules(initModulesConfig)
shutdownFunc = shutdown
})
service = medusaApp.modules[Modules.PAYMENT]
afterAll(async () => {
await shutdownFunc()
})
beforeEach(async () => {
await MikroOrmWrapper.setupDatabase()
repositoryManager = await MikroOrmWrapper.forkManager()
service = await initialize({
database: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_PAYMNET_DB_SCHEMA,
},
shutdownFunc = shutdown
})
await createPaymentCollections(repositoryManager)
})
afterEach(async () => {
await MikroOrmWrapper.clearDatabase()
})
describe("create", () => {
it("should throw an error when required params are not passed", async () => {
let error = await service
.createPaymentCollection([
{
amount: 200,
region_id: "req_123",
} as any,
])
.catch((e) => e)
expect(error.message).toContain(
"Value for PaymentCollection.currency_code is required, 'undefined' found"
)
error = await service
.createPaymentCollection([
{
currency_code: "USD",
region_id: "req_123",
} as any,
])
.catch((e) => e)
expect(error.message).toContain(
"Value for PaymentCollection.amount is required, 'undefined' found"
)
error = await service
.createPaymentCollection([
{
currency_code: "USD",
amount: 200,
} as any,
])
.catch((e) => e)
expect(error.message).toContain(
"Value for PaymentCollection.region_id is required, 'undefined' found"
)
afterAll(async () => {
await shutdownFunc()
})
it("should create a payment collection successfully", async () => {
const [createdPaymentCollection] = await service.createPaymentCollection([
{ currency_code: "USD", amount: 200, region_id: "reg_123" },
])
beforeEach(async () => {
await MikroOrmWrapper.setupDatabase()
repositoryManager = await MikroOrmWrapper.forkManager()
expect(createdPaymentCollection).toEqual(
expect.objectContaining({
id: expect.any(String),
status: "not_paid",
payment_providers: [],
payment_sessions: [],
payments: [],
currency_code: "USD",
amount: 200,
})
)
})
})
describe("delete", () => {
it("should delete a Payment Collection", async () => {
let collection = await service.listPaymentCollections({
id: ["pay-col-id-1"],
})
expect(collection.length).toEqual(1)
await service.deletePaymentCollections(["pay-col-id-1"])
collection = await service.listPaymentCollections({
id: ["pay-col-id-1"],
})
expect(collection.length).toEqual(0)
})
})
describe("retrieve", () => {
it("should retrieve a Payment Collection", async () => {
let collection = await service.retrievePaymentCollection("pay-col-id-2")
expect(collection).toEqual(
expect.objectContaining({
id: "pay-col-id-2",
amount: 200,
region_id: "region-id-1",
currency_code: "usd",
})
)
})
it("should fail to retrieve a non existent Payment Collection", async () => {
let error = await service
.retrievePaymentCollection("pay-col-id-not-exists")
.catch((e) => e)
expect(error.message).toContain(
"PaymentCollection with id: pay-col-id-not-exists was not found"
)
})
})
describe("list", () => {
it("should list and count Payment Collection", async () => {
let [collections, count] = await service.listAndCountPaymentCollections()
expect(count).toEqual(3)
expect(collections).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "pay-col-id-1",
amount: 100,
region_id: "region-id-1",
currency_code: "usd",
}),
expect.objectContaining({
id: "pay-col-id-2",
amount: 200,
region_id: "region-id-1",
currency_code: "usd",
}),
expect.objectContaining({
id: "pay-col-id-3",
amount: 300,
region_id: "region-id-2",
currency_code: "usd",
}),
])
)
})
it("should list Payment Collections by region_id", async () => {
let collections = await service.listPaymentCollections(
{
region_id: "region-id-1",
service = await initialize({
database: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_PAYMNET_DB_SCHEMA,
},
{ select: ["id", "amount", "region_id"] }
)
})
expect(collections.length).toEqual(2)
await createPaymentCollections(repositoryManager)
await createPaymentSessions(repositoryManager)
await createPayments(repositoryManager)
})
expect(collections).toEqual(
expect.arrayContaining([
afterEach(async () => {
await MikroOrmWrapper.clearDatabase()
})
describe("create", () => {
it("should throw an error when required params are not passed", async () => {
let error = await service
.createPaymentCollection([
{
amount: 200,
region_id: "req_123",
} as any,
])
.catch((e) => e)
expect(error.message).toContain(
"Value for PaymentCollection.currency_code is required, 'undefined' found"
)
error = await service
.createPaymentCollection([
{
currency_code: "USD",
region_id: "req_123",
} as any,
])
.catch((e) => e)
expect(error.message).toContain(
"Value for PaymentCollection.amount is required, 'undefined' found"
)
error = await service
.createPaymentCollection([
{
currency_code: "USD",
amount: 200,
} as any,
])
.catch((e) => e)
expect(error.message).toContain(
"Value for PaymentCollection.region_id is required, 'undefined' found"
)
})
it("should create a payment collection successfully", async () => {
const [createdPaymentCollection] =
await service.createPaymentCollection([
{ currency_code: "USD", amount: 200, region_id: "reg_123" },
])
expect(createdPaymentCollection).toEqual(
expect.objectContaining({
id: "pay-col-id-1",
amount: 100,
region_id: "region-id-1",
}),
id: expect.any(String),
status: "not_paid",
payment_providers: [],
payment_sessions: [],
payments: [],
currency_code: "USD",
amount: 200,
})
)
})
})
describe("delete", () => {
it("should delete a Payment Collection", async () => {
let collection = await service.listPaymentCollections({
id: ["pay-col-id-1"],
})
expect(collection.length).toEqual(1)
await service.deletePaymentCollections(["pay-col-id-1"])
collection = await service.listPaymentCollections({
id: ["pay-col-id-1"],
})
expect(collection.length).toEqual(0)
})
})
describe("retrieve", () => {
it("should retrieve a Payment Collection", async () => {
let collection = await service.retrievePaymentCollection("pay-col-id-2")
expect(collection).toEqual(
expect.objectContaining({
id: "pay-col-id-2",
amount: 200,
region_id: "region-id-1",
}),
])
)
currency_code: "usd",
})
)
})
it("should fail to retrieve a non existent Payment Collection", async () => {
let error = await service
.retrievePaymentCollection("pay-col-id-not-exists")
.catch((e) => e)
expect(error.message).toContain(
"PaymentCollection with id: pay-col-id-not-exists was not found"
)
})
})
describe("list", () => {
it("should list and count Payment Collection", async () => {
let [collections, count] =
await service.listAndCountPaymentCollections()
expect(count).toEqual(3)
expect(collections).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "pay-col-id-1",
amount: 100,
region_id: "region-id-1",
currency_code: "usd",
}),
expect.objectContaining({
id: "pay-col-id-2",
amount: 200,
region_id: "region-id-1",
currency_code: "usd",
}),
expect.objectContaining({
id: "pay-col-id-3",
amount: 300,
region_id: "region-id-2",
currency_code: "usd",
}),
])
)
})
it("should list Payment Collections by region_id", async () => {
let collections = await service.listPaymentCollections(
{
region_id: "region-id-1",
},
{ select: ["id", "amount", "region_id"] }
)
expect(collections.length).toEqual(2)
expect(collections).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "pay-col-id-1",
amount: 100,
region_id: "region-id-1",
}),
expect.objectContaining({
id: "pay-col-id-2",
amount: 200,
region_id: "region-id-1",
}),
])
)
})
})
describe("update", () => {
it("should update a Payment Collection", async () => {
await service.updatePaymentCollection({
id: "pay-col-id-2",
currency_code: "eur",
authorized_amount: 200,
})
const collection = await service.retrievePaymentCollection(
"pay-col-id-2"
)
expect(collection).toEqual(
expect.objectContaining({
id: "pay-col-id-2",
authorized_amount: 200,
currency_code: "eur",
})
)
})
})
})
describe("update", () => {
it("should update a Payment Collection", async () => {
await service.updatePaymentCollection({
id: "pay-col-id-2",
currency_code: "eur",
authorized_amount: 200,
describe("PaymentSession", () => {
let repositoryManager: SqlEntityManager
beforeEach(async () => {
await MikroOrmWrapper.setupDatabase()
repositoryManager = await MikroOrmWrapper.forkManager()
service = await initialize({
database: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_PAYMNET_DB_SCHEMA,
},
})
const collection = await service.retrievePaymentCollection("pay-col-id-2")
await createPaymentCollections(repositoryManager)
await createPaymentSessions(repositoryManager)
})
expect(collection).toEqual(
expect.objectContaining({
id: "pay-col-id-2",
authorized_amount: 200,
currency_code: "eur",
afterEach(async () => {
await MikroOrmWrapper.clearDatabase()
})
describe("create", () => {
it("should create a payment session successfully", async () => {
const paymentCollection = await service.createPaymentSession(
"pay-col-id-1",
{
amount: 200,
provider_id: "manual",
currency_code: "usd",
}
)
expect(paymentCollection).toEqual(
expect.objectContaining({
id: "pay-col-id-1",
status: "not_paid",
payment_sessions: expect.arrayContaining([
{
id: expect.any(String),
data: null,
status: "pending",
authorized_at: null,
currency_code: "usd",
amount: 200,
provider_id: "manual",
payment_collection: expect.objectContaining({
id: paymentCollection.id,
}),
},
]),
})
)
})
})
})
describe("Payment", () => {
let repositoryManager: SqlEntityManager
beforeEach(async () => {
await MikroOrmWrapper.setupDatabase()
repositoryManager = await MikroOrmWrapper.forkManager()
service = await initialize({
database: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_PAYMNET_DB_SCHEMA,
},
})
await createPaymentCollections(repositoryManager)
await createPaymentSessions(repositoryManager)
await createPayments(repositoryManager)
})
afterEach(async () => {
await MikroOrmWrapper.clearDatabase()
})
describe("create", () => {
it("should create a payment successfully", async () => {
let paymentCollection = await service.createPaymentCollection({
currency_code: "usd",
amount: 200,
region_id: "reg",
})
)
paymentCollection = await service.createPaymentSession(
paymentCollection.id,
{
amount: 200,
provider_id: "manual",
currency_code: "usd",
}
)
const createdPayment = await service.createPayment({
data: {},
amount: 200,
provider_id: "manual",
currency_code: "usd",
payment_collection_id: paymentCollection.id,
payment_session_id: paymentCollection.payment_sessions![0].id,
})
expect(createdPayment).toEqual(
expect.objectContaining({
id: expect.any(String),
authorized_amount: null,
cart_id: null,
order_id: null,
order_edit_id: null,
customer_id: null,
data: {},
deleted_at: null,
captured_at: null,
canceled_at: null,
refunds: [],
captures: [],
amount: 200,
currency_code: "usd",
provider_id: "manual",
payment_collection: expect.objectContaining({
id: paymentCollection.id,
}),
payment_session: expect.objectContaining({
id: paymentCollection.payment_sessions![0].id,
}),
})
)
})
})
describe("update", () => {
it("should update a payment successfully", async () => {
const updatedPayment = await service.updatePayment({
id: "pay-id-1",
cart_id: "new-cart",
})
expect(updatedPayment).toEqual(
expect.objectContaining({
id: "pay-id-1",
cart_id: "new-cart",
})
)
})
})
describe("capture", () => {
it("should capture a payment successfully", async () => {
const capturedPayment = await service.capturePayment({
amount: 100,
payment_id: "pay-id-1",
})
expect(capturedPayment).toEqual(
expect.objectContaining({
id: "pay-id-1",
amount: 100,
captures: [
expect.objectContaining({
created_by: null,
amount: 100,
}),
],
// TODO: uncomment when totals calculations are implemented
// captured_amount: 100,
// captured_at: expect.any(Date),
})
)
})
it("should capture payments in bulk successfully", async () => {
const capturedPayments = await service.capturePayment([
{
amount: 50, // partially captured
payment_id: "pay-id-1",
},
{
amount: 100, // fully captured
payment_id: "pay-id-2",
},
])
expect(capturedPayments).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "pay-id-1",
amount: 100,
authorized_amount: 100,
captured_at: null,
captures: [
expect.objectContaining({
created_by: null,
amount: 50,
}),
],
// captured_amount: 50,
}),
expect.objectContaining({
id: "pay-id-2",
amount: 100,
authorized_amount: 100,
// captured_at: expect.any(Date),
captures: [
expect.objectContaining({
created_by: null,
amount: 100,
}),
],
// captured_amount: 100,
}),
])
)
})
// TODO: uncomment when totals are implemented
// it("should fail to capture payments in bulk if one of the captures fail", async () => {
// const error = await service
// .capturePayment([
// {
// amount: 50,
// payment_id: "pay-id-1",
// },
// {
// amount: 200, // exceeds authorized amount
// payment_id: "pay-id-2",
// },
// ])
// .catch((e) => e)
//
// expect(error.message).toEqual(
// "Total captured amount for payment: pay-id-2 exceeds authorized amount."
// )
// })
// it("should fail to capture amount greater than authorized", async () => {
// const error = await service
// .capturePayment({
// amount: 200,
// payment_id: "pay-id-1",
// })
// .catch((e) => e)
//
// expect(error.message).toEqual(
// "Total captured amount for payment: pay-id-1 exceeds authorized amount."
// )
// })
// it("should fail to capture already captured payment", async () => {
// await service.capturePayment({
// amount: 100,
// payment_id: "pay-id-1",
// })
//
// const error = await service
// .capturePayment({
// amount: 100,
// payment_id: "pay-id-1",
// })
// .catch((e) => e)
//
// expect(error.message).toEqual("The payment is already fully captured.")
// })
})
describe("refund", () => {
it("should refund a payments in bulk successfully", async () => {
await service.capturePayment({
amount: 100,
payment_id: "pay-id-1",
})
await service.capturePayment({
amount: 100,
payment_id: "pay-id-2",
})
const refundedPayment = await service.refundPayment([
{
amount: 100,
payment_id: "pay-id-1",
},
{
amount: 100,
payment_id: "pay-id-2",
},
])
expect(refundedPayment).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "pay-id-1",
amount: 100,
refunds: [
expect.objectContaining({
created_by: null,
amount: 100,
}),
],
// captured_amount: 100,
// refunded_amount: 100,
}),
expect.objectContaining({
id: "pay-id-2",
amount: 100,
refunds: [
expect.objectContaining({
created_by: null,
amount: 100,
}),
],
// captured_amount: 100,
// refunded_amount: 100,
}),
])
)
})
// it("should throw if refund is greater than captured amount", async () => {
// await service.capturePayment({
// amount: 50,
// payment_id: "pay-id-1",
// })
//
// const error = await service
// .refundPayment({
// amount: 100,
// payment_id: "pay-id-1",
// })
// .catch((e) => e)
//
// expect(error.message).toEqual(
// "Refund amount for payment: pay-id-1 cannot be greater than the amount captured on the payment."
// )
// })
})
})
})
+1 -1
View File
@@ -31,7 +31,7 @@ export default class Capture {
index: "IDX_capture_payment_id",
fieldName: "payment_id",
})
payment: Payment
payment!: Payment
@Property({
onCreate: () => new Date(),
@@ -5,6 +5,7 @@ import {
ManyToOne,
OneToOne,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
@@ -15,6 +16,8 @@ import Payment from "./payment"
@Entity({ tableName: "payment_session" })
export default class PaymentSession {
[OptionalProps]?: "status"
@PrimaryKey({ columnType: "text" })
id: string
@@ -36,7 +39,7 @@ export default class PaymentSession {
@Enum({
items: () => PaymentSessionStatus,
})
status: PaymentSessionStatus
status: PaymentSessionStatus = PaymentSessionStatus.PENDING
@Property({
columnType: "timestamptz",
@@ -47,15 +50,17 @@ export default class PaymentSession {
@ManyToOne({
index: "IDX_payment_session_payment_collection_id",
fieldName: "payment_collection_id",
onDelete: "cascade",
})
payment_collection!: PaymentCollection
@OneToOne({
entity: () => Payment,
mappedBy: (payment) => payment.session,
mappedBy: (payment) => payment.payment_session,
cascade: ["soft-remove"] as any,
nullable: true,
})
payment!: Payment
payment?: Payment | null
@BeforeCreate()
onCreate() {
+5 -4
View File
@@ -115,16 +115,17 @@ export default class Payment {
@ManyToOne({
index: "IDX_payment_payment_collection_id",
fieldName: "payment_collection_id",
onDelete: "cascade",
})
payment_collection: PaymentCollection
payment_collection!: PaymentCollection
@OneToOne({ owner: true, fieldName: "session_id" })
session: PaymentSession
payment_session!: PaymentSession
/** COMPUTED PROPERTIES START **/
// captured_amount: number // sum of the associated captures
// refunded_amount: number // sum of the associated refunds
captured_amount: number // sum of the associated captures
refunded_amount: number // sum of the associated refunds
/** COMPUTED PROPERTIES END **/
+1 -1
View File
@@ -26,7 +26,7 @@ export default class Refund {
index: "IDX_refund_payment_id",
fieldName: "payment_id",
})
payment: Payment
payment!: Payment
@Property({
onCreate: () => new Date(),
+301 -67
View File
@@ -1,8 +1,11 @@
import {
CaptureDTO,
Context,
CreateCaptureDTO,
CreatePaymentCollectionDTO,
CreatePaymentDTO,
CreatePaymentSessionDTO,
CreateRefundDTO,
DAL,
InternalModuleDeclaration,
IPaymentModuleService,
@@ -10,6 +13,8 @@ import {
ModulesSdkTypes,
PaymentCollectionDTO,
PaymentDTO,
PaymentSessionDTO,
RefundDTO,
SetPaymentSessionsDTO,
UpdatePaymentCollectionDTO,
UpdatePaymentDTO,
@@ -18,6 +23,8 @@ import {
InjectTransactionManager,
MedusaContext,
ModulesSdkUtils,
MedusaError,
InjectManager,
} from "@medusajs/utils"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
@@ -25,49 +32,58 @@ import {
Capture,
Payment,
PaymentCollection,
PaymentMethodToken,
PaymentProvider,
PaymentSession,
Refund,
} from "@models"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
paymentService: ModulesSdkTypes.InternalModuleService<any>
captureService: ModulesSdkTypes.InternalModuleService<any>
refundService: ModulesSdkTypes.InternalModuleService<any>
paymentSessionService: ModulesSdkTypes.InternalModuleService<any>
paymentCollectionService: ModulesSdkTypes.InternalModuleService<any>
}
const generateMethodForModels = [
Capture,
PaymentCollection,
PaymentMethodToken,
PaymentProvider,
PaymentSession,
Refund,
]
const generateMethodForModels = [PaymentCollection, PaymentSession]
export default class PaymentModuleService<
TPaymentCollection extends PaymentCollection = PaymentCollection
TPaymentCollection extends PaymentCollection = PaymentCollection,
TPayment extends Payment = Payment,
TCapture extends Capture = Capture,
TRefund extends Refund = Refund,
TPaymentSession extends PaymentSession = PaymentSession
>
extends ModulesSdkUtils.abstractModuleServiceFactory<
// TODO revisit when moving forward frane
InjectedDependencies,
PaymentDTO,
PaymentCollectionDTO,
{
Capture: { dto: any }
PaymentCollection: { dto: any }
PaymentMethodToken: { dto: any }
PaymentProvider: { dto: any }
PaymentSession: { dto: any }
Refund: { dto: any }
PaymentCollection: { dto: PaymentCollectionDTO }
PaymentSession: { dto: PaymentSessionDTO }
Payment: { dto: PaymentDTO }
Capture: { dto: CaptureDTO }
Refund: { dto: RefundDTO }
}
>(Payment, generateMethodForModels, entityNameToLinkableKeysMap)
>(PaymentCollection, generateMethodForModels, entityNameToLinkableKeysMap)
implements IPaymentModuleService
{
protected baseRepository_: DAL.RepositoryService
protected paymentService_: ModulesSdkTypes.InternalModuleService<TPayment>
protected captureService_: ModulesSdkTypes.InternalModuleService<TCapture>
protected refundService_: ModulesSdkTypes.InternalModuleService<TRefund>
protected paymentSessionService_: ModulesSdkTypes.InternalModuleService<TPaymentSession>
protected paymentCollectionService_: ModulesSdkTypes.InternalModuleService<TPaymentCollection>
constructor(
{ baseRepository, paymentCollectionService }: InjectedDependencies,
{
baseRepository,
paymentService,
captureService,
refundService,
paymentSessionService,
paymentCollectionService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
// @ts-ignore
@@ -75,6 +91,10 @@ export default class PaymentModuleService<
this.baseRepository_ = baseRepository
this.refundService_ = refundService
this.captureService_ = captureService
this.paymentService_ = paymentService
this.paymentSessionService_ = paymentSessionService
this.paymentCollectionService_ = paymentCollectionService
}
@@ -140,6 +160,256 @@ export default class PaymentModuleService<
)
}
createPayment(
data: CreatePaymentDTO,
sharedContext?: Context
): Promise<PaymentDTO>
createPayment(
data: CreatePaymentDTO[],
sharedContext?: Context
): Promise<PaymentDTO[]>
@InjectTransactionManager("baseRepository_")
async createPayment(
data: CreatePaymentDTO | CreatePaymentDTO[],
@MedusaContext() sharedContext?: Context
): Promise<PaymentDTO | PaymentDTO[]> {
let input = Array.isArray(data) ? data : [data]
input = input.map((inputData) => ({
payment_collection: inputData.payment_collection_id,
payment_session: inputData.payment_session_id,
...inputData,
}))
const payments = await this.paymentService_.create(input, sharedContext)
return await this.baseRepository_.serialize<PaymentDTO[]>(
Array.isArray(data) ? payments : payments[0],
{
populate: true,
}
)
}
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],
{ 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[],
@MedusaContext() sharedContext?: Context
): Promise<PaymentCollectionDTO> {
let input = Array.isArray(data) ? data : [data]
input = input.map((inputData) => ({
payment_collection: paymentCollectionId,
...inputData,
}))
await this.paymentSessionService_.create(input, sharedContext)
return await this.retrievePaymentCollection(
paymentCollectionId,
{
relations: ["payment_sessions"],
},
sharedContext
)
}
/**
* TODO
*/
@@ -156,56 +426,20 @@ export default class PaymentModuleService<
): Promise<PaymentCollectionDTO> {
throw new Error("Method not implemented.")
}
createPayment(data: CreatePaymentDTO): Promise<PaymentDTO>
createPayment(data: CreatePaymentDTO[]): Promise<PaymentDTO[]>
createPayment(data: unknown): Promise<PaymentDTO | PaymentDTO[]> {
throw new Error("Method not implemented.")
}
capturePayment(
paymentId: string,
amount: number,
sharedContext?: Context | undefined
): Promise<PaymentDTO> {
throw new Error("Method not implemented.")
}
refundPayment(
paymentId: string,
amount: number,
sharedContext?: Context | undefined
): Promise<PaymentDTO> {
throw new Error("Method not implemented.")
}
updatePayment(
data: UpdatePaymentDTO,
sharedContext?: Context | undefined
): Promise<PaymentDTO>
updatePayment(
data: UpdatePaymentDTO[],
sharedContext?: Context | undefined
cancelPayment(paymentId: string, sharedContext?: Context): Promise<PaymentDTO>
cancelPayment(
paymentId: string[],
sharedContext?: Context
): Promise<PaymentDTO[]>
updatePayment(
data: unknown,
sharedContext?: unknown
cancelPayment(
paymentId: string | string[],
sharedContext?: Context
): Promise<PaymentDTO | PaymentDTO[]> {
throw new Error("Method not implemented.")
}
createPaymentSession(
paymentCollectionId: string,
data: CreatePaymentSessionDTO,
sharedContext?: Context | undefined
): Promise<PaymentCollectionDTO>
createPaymentSession(
paymentCollectionId: string,
data: CreatePaymentSessionDTO[],
sharedContext?: Context | undefined
): Promise<PaymentCollectionDTO>
createPaymentSession(
paymentCollectionId: unknown,
data: unknown,
sharedContext?: unknown
): Promise<PaymentCollectionDTO> {
throw new Error("Method not implemented.")
}
authorizePaymentSessions(
paymentCollectionId: string,
sessionIds: string[],
+206 -1
View File
@@ -36,6 +36,76 @@ export interface PaymentCollectionDTO {
* The ID of the Payment Collection
*/
id: string
/**
* The currency of the payments/sessions associated with payment collection
*/
currency_code: string
/**
* The id of the region
*/
region_id: string
/**
* The amount
*/
amount: number
/**
* The amount authorized within associated payment sessions
*/
authorized_amount?: number
/**
* The amount refunded from associated payments
*/
refunded_amount?: number
/**
* When the payment collection was completed
*/
completed_at?: string | Date
/**
* When the payment collection was created
*/
created_at?: string | Date
/**
* When the payment collection was updated
*/
updated_at?: string | Date
/**
* Holds custom data in key-value pairs
*/
metadata?: Record<string, unknown> | null
/**
* The status of the payment collection
*/
status: PaymentCollectionStatus
/**
* The payment provider for the payments
*
* @expandable
*/
payment_providers: PaymentProviderDTO[]
/**
* The associated payment sessions
*
* @expandable
*/
payment_sessions?: PaymentSessionDTO[]
/**
* The associated payments
*
* @expandable
*/
payments?: PaymentDTO[]
}
export interface FilterablePaymentCollectionProps
@@ -52,7 +122,142 @@ export interface FilterablePaymentCollectionProps
export interface PaymentDTO {
/**
* The ID of the Payment Collection
* The ID of the Payment
*/
id: string
/**
* The payment amount
*/
amount: number
authorized_amount?: number
/**
* Payment currency
*/
currency_code: string
/**
* The ID of payment provider
*/
provider_id: string
cart_id?: string
order_id?: string
order_edit_id?: string
customer_id?: string
/**
* Payment provider data
*/
data?: Record<string, unknown>
/**
* When the payment collection was created
*/
created_at?: string | Date
/**
* When the payment collection was updated
*/
updated_at?: string | Date
/**
* When the payment was captured
*/
captured_at?: string | Date
/**
* When the payment was canceled
*/
canceled_at?: string | Date
/**
* The sum of the associated captures
*/
captured_amount?: number
/**
* The sum of the associated refunds
*/
refunded_amount?: number
/**
* The associated payment captures
*
* @expandable
*/
captures?: CaptureDTO[]
/**
* The associated refunds of the payment
*
* @expandable
*/
refunds?: RefundDTO[]
/**
* The payment collection the payment is associated with
*
* @expandable
*/
payment_collection?: PaymentCollectionDTO
/**
* The payment session from which the payment is created
*
* @expandable
*/
payment_session?: PaymentSessionDTO
}
export interface CaptureDTO {
/**
* The ID of the Capture
*/
id: string
/**
* Captured amount
*/
amount: number
created_at: Date
created_by?: string
payment: PaymentDTO
}
export interface RefundDTO {
/**
* The ID of the Refund
*/
id: string
/**
* Refunded amount
*/
amount: number
created_at: Date
created_by?: string
payment: PaymentDTO
}
/* ********** PAYMENT ********** */
export interface PaymentSessionDTO {
/**
* The ID of the Payment Session
*/
id: string
}
export interface PaymentProviderDTO {
id: string
is_enabled: string
}
+21 -1
View File
@@ -17,7 +17,6 @@ export interface UpdatePaymentCollectionDTO
authorized_amount?: number
refunded_amount?: number
completed_at?: number
status?: PaymentCollectionStatus
}
@@ -31,6 +30,9 @@ export interface CreatePaymentDTO {
provider_id: string
data: Record<string, unknown>
payment_session_id: string
payment_collection_id: string
cart_id?: string
order_id?: string
order_edit_id?: string
@@ -38,10 +40,28 @@ export interface CreatePaymentDTO {
}
export interface UpdatePaymentDTO {
id: string
cart_id?: string
order_id?: string
order_edit_id?: string
customer_id?: string
data?: Record<string, unknown>
}
export interface CreateCaptureDTO {
amount: number
payment_id: string
captured_by?: string
}
export interface CreateRefundDTO {
amount: number
payment_id: string
created_by?: string
}
/**
+28 -7
View File
@@ -1,9 +1,11 @@
import { IModuleService } from "../modules-sdk"
import { Context } from "../shared-context"
import {
CreateCaptureDTO,
CreatePaymentCollectionDTO,
CreatePaymentDTO,
CreatePaymentSessionDTO,
CreateRefundDTO,
SetPaymentSessionsDTO,
UpdatePaymentCollectionDTO,
UpdatePaymentDTO,
@@ -75,19 +77,38 @@ export interface IPaymentModuleService extends IModuleService {
/* ********** PAYMENT ********** */
createPayment(data: CreatePaymentDTO): Promise<PaymentDTO>
createPayment(data: CreatePaymentDTO[]): Promise<PaymentDTO[]>
createPayment(
data: CreatePaymentDTO,
sharedContext?: Context
): Promise<PaymentDTO>
createPayment(
data: CreatePaymentDTO[],
sharedContext?: Context
): Promise<PaymentDTO[]>
capturePayment(
paymentId: string,
amount: number,
data: CreateCaptureDTO,
sharedContext?: Context
): Promise<PaymentDTO>
capturePayment(
data: CreateCaptureDTO[],
sharedContext?: Context
): Promise<PaymentDTO[]>
refundPayment(
data: CreateRefundDTO,
sharedContext?: Context
): Promise<PaymentDTO>
refundPayment(
paymentId: string,
amount: number,
data: CreateRefundDTO[],
sharedContext?: Context
): Promise<PaymentDTO>
): Promise<PaymentDTO[]>
cancelPayment(paymentId: string, sharedContext?: Context): Promise<PaymentDTO>
cancelPayment(
paymentId: string[],
sharedContext?: Context
): Promise<PaymentDTO[]>
updatePayment(
data: UpdatePaymentDTO,