feat: Capture payment (#6601)

* feat: Capture payment

* add amount to workflow input
This commit is contained in:
Oli Juhl authored and GitHub committed 2024-03-07 11:02:26 +01:00
1 parent 12b035cb18
commit 3d0da980cf
23 files changed
+584 -86

No files matched your search

@@ -444,7 +444,6 @@ moduleIntegrationTestRunner({
data: {},
cart_id: null,
order_id: null,
order_edit_id: null,
customer_id: null,
deleted_at: null,
captured_at: null,
@@ -498,7 +497,7 @@ moduleIntegrationTestRunner({
})
describe("capture", () => {
it("should capture a payment successfully", async () => {
it("should capture a payment successfully and update captured_at", async () => {
const capturedPayment = await service.capturePayment({
amount: 100,
payment_id: "pay-id-1",
@@ -515,61 +514,104 @@ moduleIntegrationTestRunner({
amount: 100,
}),
],
// TODO: uncomment when totals calculations are implemented
// captured_amount: 100,
// captured_at: expect.any(Date),
captured_at: expect.any(Date),
})
)
})
// TODO: uncomment when totals are implemented
it("should split a payment in two captures a payment successfully", async () => {
await service.capturePayment({
amount: 50,
payment_id: "pay-id-1",
})
// 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 authorised 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: pay-id-1 is already fully captured."
// )
// })
//
// it("should fail to capture a canceled payment", async () => {
// await service.cancelPayment("pay-id-1")
//
// const error = await service
// .capturePayment({
// amount: 100,
// payment_id: "pay-id-1",
// })
// .catch((e) => e)
//
// expect(error.message).toEqual(
// "The payment: pay-id-1 has been canceled."
// )
// })
const capturedPayment = await service.capturePayment({
amount: 50,
payment_id: "pay-id-1",
})
expect(capturedPayment).toEqual(
expect.objectContaining({
id: "pay-id-1",
amount: 100,
captures: [
expect.objectContaining({
created_by: null,
amount: 50,
}),
expect.objectContaining({
created_by: null,
amount: 50,
}),
],
})
)
})
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(
"You cannot capture more than the authorized amount substracted by what is already captured."
)
})
it("should fail to capture amount greater than what is already captured", async () => {
await service.capturePayment({
amount: 99,
payment_id: "pay-id-1",
})
const error = await service
.capturePayment({
amount: 2,
payment_id: "pay-id-1",
})
.catch((e) => e)
expect(error.message).toEqual(
"You cannot capture more than the authorized amount substracted by what is already captured."
)
})
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(
"You cannot capture more than the authorized amount substracted by what is already captured."
)
})
it("should fail to capture a canceled payment", async () => {
await service.cancelPayment("pay-id-1")
const error = await service
.capturePayment({
amount: 100,
payment_id: "pay-id-1",
})
.catch((e) => e)
expect(error.message).toEqual(
"The payment: pay-id-1 has been canceled."
)
})
})
describe("refund", () => {
+1
View File
@@ -56,6 +56,7 @@
"@mikro-orm/migrations": "5.9.7",
"@mikro-orm/postgresql": "5.9.7",
"awilix": "^8.0.0",
"bignumber.js": "^9.1.2",
"dotenv": "^16.1.4",
"knex": "2.4.2"
}
@@ -6,7 +6,7 @@ export class Migration20240225134525 extends Migration {
const paymentCollectionExists = await this.execute(
`SELECT * FROM information_schema.tables where table_name = 'payment_collection' and table_schema = 'public';`
)
if (paymentCollectionExists.length) {
this.addSql(`
${generatePostgresAlterColummnIfExistStatement(
@@ -14,10 +14,11 @@ export class Migration20240225134525 extends Migration {
["type", "created_by"],
"DROP NOT NULL"
)}
ALTER TABLE IF EXISTS "payment_collection" ADD COLUMN IF NOT EXISTS "completed_at" TIMESTAMPTZ NULL;
ALTER TABLE IF EXISTS "payment_collection" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
ALTER TABLE IF EXISTS "payment_collection" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
ALTER TABLE "payment_collection" DROP CONSTRAINT "FK_payment_collection_region_id";
ALTER TABLE IF EXISTS "payment_provider" ADD COLUMN IF NOT EXISTS "is_enabled" BOOLEAN NOT NULL DEFAULT TRUE;
@@ -34,6 +35,7 @@ export class Migration20240225134525 extends Migration {
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "payment_session_id" TEXT NOT NULL;
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "customer_id" TEXT NULL;
ALTER TABLE IF EXISTS "refund" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
ALTER TABLE IF EXISTS "refund" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
@@ -101,6 +103,7 @@ export class Migration20240225134525 extends Migration {
CREATE INDEX IF NOT EXISTS "IDX_capture_deleted_at" ON "payment" ("deleted_at") WHERE "deleted_at" IS NOT NULL;
CREATE INDEX IF NOT EXISTS "IDX_payment_session_payment_collection_id" ON "payment_session" ("payment_collection_id") WHERE "deleted_at" IS NULL;
`)
} else {
this.addSql(`
@@ -170,7 +173,6 @@ export class Migration20240225134525 extends Migration {
"provider_id" TEXT NOT NULL,
"cart_id" TEXT NULL,
"order_id" TEXT NULL,
"order_edit_id" TEXT NULL,
"customer_id" TEXT NULL,
"data" JSONB NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-3
View File
@@ -52,9 +52,6 @@ export default class Payment {
@Property({ columnType: "text", nullable: true })
order_id: string | null = null
@Property({ columnType: "text", nullable: true })
order_edit_id: string | null = null
@Property({ columnType: "text", nullable: true })
customer_id: string | null = null
+48 -19
View File
@@ -38,6 +38,7 @@ import {
PaymentSession,
Refund,
} from "@models"
import BigNumber from "bignumber.js"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import PaymentProviderService from "./payment-provider"
@@ -51,7 +52,13 @@ type InjectedDependencies = {
paymentProviderService: PaymentProviderService
}
const generateMethodForModels = [PaymentCollection, Payment, PaymentSession]
const generateMethodForModels = [
PaymentCollection,
Payment,
PaymentSession,
Capture,
Refund,
]
export default class PaymentModuleService<
TPaymentCollection extends PaymentCollection = PaymentCollection,
@@ -409,10 +416,25 @@ export default class PaymentModuleService<
): Promise<PaymentDTO> {
const payment = await this.paymentService_.retrieve(
data.payment_id,
{ select: ["id", "data", "provider_id"] },
{
select: [
"id",
"data",
"provider_id",
"amount",
"raw_amount",
"canceled_at",
],
relations: ["captures.raw_amount"],
},
sharedContext
)
// If no custom amount is passed, we assume the full amount needs to be captured
if (!data.amount) {
data.amount = payment.amount as number
}
if (payment.canceled_at) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
@@ -420,22 +442,29 @@ export default class PaymentModuleService<
)
}
// this method needs to be idempotent
if (payment.captured_at) {
return this.retrievePayment(
return await this.retrievePayment(
data.payment_id,
{ relations: ["captures"] },
sharedContext
)
}
// TODO: revisit when https://github.com/medusajs/medusa/pull/6253 is merged
// if (payment.captured_amount + input.amount > payment.amount) {
// throw new MedusaError(
// MedusaError.Types.INVALID_DATA,
// `Total captured amount for payment: ${payment.id} exceeds authorized amount.`
// )
// }
const capturedAmount = payment.captures.reduce((acc, next) => {
const bn = new BigNumber(next.raw_amount.value)
return acc.plus(bn)
}, BigNumber(0))
const authorizedAmount = BigNumber(payment.raw_amount.value)
const newCaptureAmount = BigNumber(data.amount)
const remainingToCapture = authorizedAmount.minus(capturedAmount)
if (newCaptureAmount.gt(remainingToCapture)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`You cannot capture more than the authorized amount substracted by what is already captured.`
)
}
const paymentData = await this.paymentProviderService_.capturePayment({
data: payment.data!,
@@ -456,13 +485,13 @@ export default class PaymentModuleService<
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
// )
// }
// When the entire authorized amount has been captured, we mark it fully capture by setting the captured_at field
if (capturedAmount.plus(newCaptureAmount).eq(authorizedAmount)) {
await this.paymentService_.update(
{ id: payment.id, captured_at: new Date() },
sharedContext
)
}
return await this.retrievePayment(
payment.id,
@@ -495,7 +524,7 @@ export default class PaymentModuleService<
data: payment.data!,
provider_id: payment.provider_id,
},
data.amount
data.amount as number
)
await this.refundService_.create(