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

This commit is contained in:
Frane Polić authored and GitHub committed 2024-02-06 13:40:22 +01:00
1 parent 5cabe9585f
commit 2104843826
13 files changed
+1239 -319

No files matched your search

@@ -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 { IPaymentModuleService } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
import { Modules } from "@medusajs/modules-sdk" import { initialize } from "../../../../src"
import { initModules } from "medusa-test-utils"
import { initialize } from "../../../../src/initialize"
import { createPaymentCollections } from "../../../__fixtures__/payment-collection"
import { DB_URL, MikroOrmWrapper } from "../../../utils" import { DB_URL, MikroOrmWrapper } from "../../../utils"
import {
createPaymentCollections,
createPaymentSessions,
createPayments,
} from "../../../__fixtures__"
import { getInitModuleConfig } from "../../../utils/get-init-module-config" import { getInitModuleConfig } from "../../../utils/get-init-module-config"
import { initModules } from "medusa-test-utils"
import { Modules } from "@medusajs/modules-sdk"
jest.setTimeout(30000) jest.setTimeout(30000)
describe("Payment Module Service", () => { describe("Payment Module Service", () => {
let service: IPaymentModuleService let service: IPaymentModuleService
let repositoryManager: SqlEntityManager
let shutdownFunc: () => Promise<void>
beforeAll(async () => { describe("PaymentCollection", () => {
const initModulesConfig = getInitModuleConfig() 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 () => { shutdownFunc = shutdown
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,
},
}) })
await createPaymentCollections(repositoryManager) afterAll(async () => {
}) await shutdownFunc()
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 () => { beforeEach(async () => {
const [createdPaymentCollection] = await service.createPaymentCollection([ await MikroOrmWrapper.setupDatabase()
{ currency_code: "USD", amount: 200, region_id: "reg_123" }, repositoryManager = await MikroOrmWrapper.forkManager()
])
expect(createdPaymentCollection).toEqual( service = await initialize({
expect.objectContaining({ database: {
id: expect.any(String), clientUrl: DB_URL,
status: "not_paid", schema: process.env.MEDUSA_PAYMNET_DB_SCHEMA,
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) await createPaymentCollections(repositoryManager)
await createPaymentSessions(repositoryManager)
await createPayments(repositoryManager)
})
expect(collections).toEqual( afterEach(async () => {
expect.arrayContaining([ 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({ expect.objectContaining({
id: "pay-col-id-1", id: expect.any(String),
amount: 100, status: "not_paid",
region_id: "region-id-1", 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({ expect.objectContaining({
id: "pay-col-id-2", id: "pay-col-id-2",
amount: 200, amount: 200,
region_id: "region-id-1", 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", () => { describe("PaymentSession", () => {
it("should update a Payment Collection", async () => { let repositoryManager: SqlEntityManager
await service.updatePaymentCollection({
id: "pay-col-id-2", beforeEach(async () => {
currency_code: "eur", await MikroOrmWrapper.setupDatabase()
authorized_amount: 200, 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( afterEach(async () => {
expect.objectContaining({ await MikroOrmWrapper.clearDatabase()
id: "pay-col-id-2", })
authorized_amount: 200,
currency_code: "eur", 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", index: "IDX_capture_payment_id",
fieldName: "payment_id", fieldName: "payment_id",
}) })
payment: Payment payment!: Payment
@Property({ @Property({
onCreate: () => new Date(), onCreate: () => new Date(),
@@ -5,6 +5,7 @@ import {
ManyToOne, ManyToOne,
OneToOne, OneToOne,
OnInit, OnInit,
OptionalProps,
PrimaryKey, PrimaryKey,
Property, Property,
} from "@mikro-orm/core" } from "@mikro-orm/core"
@@ -15,6 +16,8 @@ import Payment from "./payment"
@Entity({ tableName: "payment_session" }) @Entity({ tableName: "payment_session" })
export default class PaymentSession { export default class PaymentSession {
[OptionalProps]?: "status"
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id: string id: string
@@ -36,7 +39,7 @@ export default class PaymentSession {
@Enum({ @Enum({
items: () => PaymentSessionStatus, items: () => PaymentSessionStatus,
}) })
status: PaymentSessionStatus status: PaymentSessionStatus = PaymentSessionStatus.PENDING
@Property({ @Property({
columnType: "timestamptz", columnType: "timestamptz",
@@ -47,15 +50,17 @@ export default class PaymentSession {
@ManyToOne({ @ManyToOne({
index: "IDX_payment_session_payment_collection_id", index: "IDX_payment_session_payment_collection_id",
fieldName: "payment_collection_id", fieldName: "payment_collection_id",
onDelete: "cascade",
}) })
payment_collection!: PaymentCollection payment_collection!: PaymentCollection
@OneToOne({ @OneToOne({
entity: () => Payment, entity: () => Payment,
mappedBy: (payment) => payment.session, mappedBy: (payment) => payment.payment_session,
cascade: ["soft-remove"] as any, cascade: ["soft-remove"] as any,
nullable: true,
}) })
payment!: Payment payment?: Payment | null
@BeforeCreate() @BeforeCreate()
onCreate() { onCreate() {
+5 -4
View File
@@ -115,16 +115,17 @@ export default class Payment {
@ManyToOne({ @ManyToOne({
index: "IDX_payment_payment_collection_id", index: "IDX_payment_payment_collection_id",
fieldName: "payment_collection_id", fieldName: "payment_collection_id",
onDelete: "cascade",
}) })
payment_collection: PaymentCollection payment_collection!: PaymentCollection
@OneToOne({ owner: true, fieldName: "session_id" }) @OneToOne({ owner: true, fieldName: "session_id" })
session: PaymentSession payment_session!: PaymentSession
/** COMPUTED PROPERTIES START **/ /** COMPUTED PROPERTIES START **/
// captured_amount: number // sum of the associated captures captured_amount: number // sum of the associated captures
// refunded_amount: number // sum of the associated refunds refunded_amount: number // sum of the associated refunds
/** COMPUTED PROPERTIES END **/ /** COMPUTED PROPERTIES END **/
+1 -1
View File
@@ -26,7 +26,7 @@ export default class Refund {
index: "IDX_refund_payment_id", index: "IDX_refund_payment_id",
fieldName: "payment_id", fieldName: "payment_id",
}) })
payment: Payment payment!: Payment
@Property({ @Property({
onCreate: () => new Date(), onCreate: () => new Date(),
+301 -67
View File
@@ -1,8 +1,11 @@
import { import {
CaptureDTO,
Context, Context,
CreateCaptureDTO,
CreatePaymentCollectionDTO, CreatePaymentCollectionDTO,
CreatePaymentDTO, CreatePaymentDTO,
CreatePaymentSessionDTO, CreatePaymentSessionDTO,
CreateRefundDTO,
DAL, DAL,
InternalModuleDeclaration, InternalModuleDeclaration,
IPaymentModuleService, IPaymentModuleService,
@@ -10,6 +13,8 @@ import {
ModulesSdkTypes, ModulesSdkTypes,
PaymentCollectionDTO, PaymentCollectionDTO,
PaymentDTO, PaymentDTO,
PaymentSessionDTO,
RefundDTO,
SetPaymentSessionsDTO, SetPaymentSessionsDTO,
UpdatePaymentCollectionDTO, UpdatePaymentCollectionDTO,
UpdatePaymentDTO, UpdatePaymentDTO,
@@ -18,6 +23,8 @@ import {
InjectTransactionManager, InjectTransactionManager,
MedusaContext, MedusaContext,
ModulesSdkUtils, ModulesSdkUtils,
MedusaError,
InjectManager,
} from "@medusajs/utils" } from "@medusajs/utils"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
@@ -25,49 +32,58 @@ import {
Capture, Capture,
Payment, Payment,
PaymentCollection, PaymentCollection,
PaymentMethodToken,
PaymentProvider,
PaymentSession, PaymentSession,
Refund, Refund,
} from "@models" } from "@models"
type InjectedDependencies = { type InjectedDependencies = {
baseRepository: DAL.RepositoryService baseRepository: DAL.RepositoryService
paymentService: ModulesSdkTypes.InternalModuleService<any>
captureService: ModulesSdkTypes.InternalModuleService<any>
refundService: ModulesSdkTypes.InternalModuleService<any>
paymentSessionService: ModulesSdkTypes.InternalModuleService<any>
paymentCollectionService: ModulesSdkTypes.InternalModuleService<any> paymentCollectionService: ModulesSdkTypes.InternalModuleService<any>
} }
const generateMethodForModels = [ const generateMethodForModels = [PaymentCollection, PaymentSession]
Capture,
PaymentCollection,
PaymentMethodToken,
PaymentProvider,
PaymentSession,
Refund,
]
export default class PaymentModuleService< 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< extends ModulesSdkUtils.abstractModuleServiceFactory<
// TODO revisit when moving forward frane
InjectedDependencies, InjectedDependencies,
PaymentDTO, PaymentCollectionDTO,
{ {
Capture: { dto: any } PaymentCollection: { dto: PaymentCollectionDTO }
PaymentCollection: { dto: any } PaymentSession: { dto: PaymentSessionDTO }
PaymentMethodToken: { dto: any } Payment: { dto: PaymentDTO }
PaymentProvider: { dto: any } Capture: { dto: CaptureDTO }
PaymentSession: { dto: any } Refund: { dto: RefundDTO }
Refund: { dto: any }
} }
>(Payment, generateMethodForModels, entityNameToLinkableKeysMap) >(PaymentCollection, generateMethodForModels, entityNameToLinkableKeysMap)
implements IPaymentModuleService implements IPaymentModuleService
{ {
protected baseRepository_: DAL.RepositoryService 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> protected paymentCollectionService_: ModulesSdkTypes.InternalModuleService<TPaymentCollection>
constructor( constructor(
{ baseRepository, paymentCollectionService }: InjectedDependencies, {
baseRepository,
paymentService,
captureService,
refundService,
paymentSessionService,
paymentCollectionService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration protected readonly moduleDeclaration: InternalModuleDeclaration
) { ) {
// @ts-ignore // @ts-ignore
@@ -75,6 +91,10 @@ export default class PaymentModuleService<
this.baseRepository_ = baseRepository this.baseRepository_ = baseRepository
this.refundService_ = refundService
this.captureService_ = captureService
this.paymentService_ = paymentService
this.paymentSessionService_ = paymentSessionService
this.paymentCollectionService_ = paymentCollectionService 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 * TODO
*/ */
@@ -156,56 +426,20 @@ export default class PaymentModuleService<
): Promise<PaymentCollectionDTO> { ): Promise<PaymentCollectionDTO> {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
createPayment(data: CreatePaymentDTO): Promise<PaymentDTO>
createPayment(data: CreatePaymentDTO[]): Promise<PaymentDTO[]> cancelPayment(paymentId: string, sharedContext?: Context): Promise<PaymentDTO>
createPayment(data: unknown): Promise<PaymentDTO | PaymentDTO[]> { cancelPayment(
throw new Error("Method not implemented.") paymentId: string[],
} sharedContext?: Context
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
): Promise<PaymentDTO[]> ): Promise<PaymentDTO[]>
updatePayment(
data: unknown, cancelPayment(
sharedContext?: unknown paymentId: string | string[],
sharedContext?: Context
): Promise<PaymentDTO | PaymentDTO[]> { ): Promise<PaymentDTO | PaymentDTO[]> {
throw new Error("Method not implemented.") 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( authorizePaymentSessions(
paymentCollectionId: string, paymentCollectionId: string,
sessionIds: string[], sessionIds: string[],
+206 -1
View File
@@ -36,6 +36,76 @@ export interface PaymentCollectionDTO {
* The ID of the Payment Collection * The ID of the Payment Collection
*/ */
id: string 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 export interface FilterablePaymentCollectionProps
@@ -52,7 +122,142 @@ export interface FilterablePaymentCollectionProps
export interface PaymentDTO { 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 id: string
} }
export interface PaymentProviderDTO {
id: string
is_enabled: string
}
+21 -1
View File
@@ -17,7 +17,6 @@ export interface UpdatePaymentCollectionDTO
authorized_amount?: number authorized_amount?: number
refunded_amount?: number refunded_amount?: number
completed_at?: number
status?: PaymentCollectionStatus status?: PaymentCollectionStatus
} }
@@ -31,6 +30,9 @@ export interface CreatePaymentDTO {
provider_id: string provider_id: string
data: Record<string, unknown> data: Record<string, unknown>
payment_session_id: string
payment_collection_id: string
cart_id?: string cart_id?: string
order_id?: string order_id?: string
order_edit_id?: string order_edit_id?: string
@@ -38,10 +40,28 @@ export interface CreatePaymentDTO {
} }
export interface UpdatePaymentDTO { export interface UpdatePaymentDTO {
id: string
cart_id?: string cart_id?: string
order_id?: string order_id?: string
order_edit_id?: string order_edit_id?: string
customer_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 { IModuleService } from "../modules-sdk"
import { Context } from "../shared-context" import { Context } from "../shared-context"
import { import {
CreateCaptureDTO,
CreatePaymentCollectionDTO, CreatePaymentCollectionDTO,
CreatePaymentDTO, CreatePaymentDTO,
CreatePaymentSessionDTO, CreatePaymentSessionDTO,
CreateRefundDTO,
SetPaymentSessionsDTO, SetPaymentSessionsDTO,
UpdatePaymentCollectionDTO, UpdatePaymentCollectionDTO,
UpdatePaymentDTO, UpdatePaymentDTO,
@@ -75,19 +77,38 @@ export interface IPaymentModuleService extends IModuleService {
/* ********** PAYMENT ********** */ /* ********** PAYMENT ********** */
createPayment(data: CreatePaymentDTO): Promise<PaymentDTO> createPayment(
createPayment(data: CreatePaymentDTO[]): Promise<PaymentDTO[]> data: CreatePaymentDTO,
sharedContext?: Context
): Promise<PaymentDTO>
createPayment(
data: CreatePaymentDTO[],
sharedContext?: Context
): Promise<PaymentDTO[]>
capturePayment( capturePayment(
paymentId: string, data: CreateCaptureDTO,
amount: number, sharedContext?: Context
): Promise<PaymentDTO>
capturePayment(
data: CreateCaptureDTO[],
sharedContext?: Context
): Promise<PaymentDTO[]>
refundPayment(
data: CreateRefundDTO,
sharedContext?: Context sharedContext?: Context
): Promise<PaymentDTO> ): Promise<PaymentDTO>
refundPayment( refundPayment(
paymentId: string, data: CreateRefundDTO[],
amount: number,
sharedContext?: Context sharedContext?: Context
): Promise<PaymentDTO> ): Promise<PaymentDTO[]>
cancelPayment(paymentId: string, sharedContext?: Context): Promise<PaymentDTO>
cancelPayment(
paymentId: string[],
sharedContext?: Context
): Promise<PaymentDTO[]>
updatePayment( updatePayment(
data: UpdatePaymentDTO, data: UpdatePaymentDTO,