fix(payment): Capture payment (#9469)

What
- Add missing `captured_at` field to payment retrieval
- Properly delete Medusa captures in case 3rd party capture call fails
This commit is contained in:
Oli Juhl
2024-10-04 15:15:10 +00:00
committed by GitHub
parent eea2bfdef1
commit 67e08dc989
3 changed files with 120 additions and 26 deletions
@@ -1,11 +1,12 @@
import { ClaimType } from "@medusajs/utils"
import { adminHeaders } from "../../../../helpers/create-admin-user"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import { createAdminUser } from "../../../../helpers/create-admin-user"
import {
adminHeaders,
createAdminUser,
} from "../../../../helpers/create-admin-user"
import { createOrderSeeder } from "../../fixtures/order"
jest.setTimeout(30000)
jest.setTimeout(50000)
medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => {
@@ -78,6 +79,92 @@ medusaIntegrationTestRunner({
expect(response.status).toEqual(200)
})
it("should throw if capture amount is greater than authorized amount", async () => {
const payment = order.payment_collections[0].payments[0]
const response = await api.post(
`/admin/payments/${payment.id}/capture`,
{ amount: 75 },
adminHeaders
)
expect(response.data.payment).toEqual(
expect.objectContaining({
id: payment.id,
captured_at: null, // not fully captured yet
captures: [
expect.objectContaining({
id: expect.any(String),
amount: 75,
}),
],
refunds: [],
amount: 100,
})
)
expect(response.status).toEqual(200)
const errResponse = await api
.post(
`/admin/payments/${payment.id}/capture`,
{ amount: 75 },
adminHeaders
)
.catch((e) => e)
expect(errResponse.response.data.message).toEqual(
"You cannot capture more than the authorized amount substracted by what is already captured."
)
})
it("should return payment if payment is already fully captured", async () => {
const payment = order.payment_collections[0].payments[0]
const response = await api.post(
`/admin/payments/${payment.id}/capture`,
undefined,
adminHeaders
)
expect(response.data.payment).toEqual(
expect.objectContaining({
id: payment.id,
captured_at: expect.any(String),
captures: [
expect.objectContaining({
id: expect.any(String),
amount: 100,
}),
],
refunds: [],
amount: 100,
})
)
expect(response.status).toEqual(200)
const anotherResponse = await api.post(
`/admin/payments/${payment.id}/capture`,
undefined,
adminHeaders
)
expect(anotherResponse.data.payment).toEqual(
expect.objectContaining({
id: payment.id,
captured_at: expect.any(String),
captures: [
expect.objectContaining({
id: expect.any(String),
amount: 100,
}),
],
refunds: [],
amount: 100,
})
)
expect(anotherResponse.status).toEqual(200)
})
it("should refund a captured payment", async () => {
const payment = order.payment_collections[0].payments[0]
@@ -636,21 +636,28 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
)
})
it("should fail to capture already captured payment", async () => {
it("should return payment if payment is already captured", 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)
const capturedPayment = await service.capturePayment({
amount: 100,
payment_id: "pay-id-1",
})
expect(error.message).toEqual(
"You cannot capture more than the authorized amount substracted by what is already captured."
expect(capturedPayment).toEqual(
expect.objectContaining({
id: "pay-id-1",
amount: 100,
captures: [
expect.objectContaining({
amount: 100,
}),
],
captured_at: expect.any(Date),
})
)
})
@@ -583,7 +583,7 @@ export default class PaymentModuleService
data: CreateCaptureDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<PaymentDTO> {
const [payment, isFullyCaptured] = await this.capturePayment_(
const { payment, isFullyCaptured, capture } = await this.capturePayment_(
data,
sharedContext
)
@@ -595,7 +595,9 @@ export default class PaymentModuleService
sharedContext
)
} catch (error) {
await super.deleteCaptures(data.payment_id, sharedContext)
if (capture?.id) {
await super.deleteCaptures({ id: capture.id }, sharedContext)
}
throw error
}
@@ -615,7 +617,11 @@ export default class PaymentModuleService
private async capturePayment_(
data: CreateCaptureDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<[Payment, boolean]> {
): Promise<{
payment: Payment
isFullyCaptured: boolean
capture?: Capture
}> {
const payment = await this.paymentService_.retrieve(
data.payment_id,
{
@@ -626,6 +632,7 @@ export default class PaymentModuleService
"payment_collection_id",
"amount",
"raw_amount",
"captured_at",
"canceled_at",
],
relations: ["captures.raw_amount"],
@@ -646,14 +653,7 @@ export default class PaymentModuleService
}
if (payment.captured_at) {
return [
(await this.retrievePayment(
data.payment_id,
{ relations: ["captures"] },
sharedContext
)) as unknown as Payment,
true,
]
return { payment, isFullyCaptured: true }
}
const capturedAmount = payment.captures.reduce((captureAmount, next) => {
@@ -677,7 +677,7 @@ export default class PaymentModuleService
)
const isFullyCaptured = MathBN.gte(totalCaptured, authorizedAmount)
await this.captureService_.create(
const capture = await this.captureService_.create(
{
payment: data.payment_id,
amount: data.amount,
@@ -686,7 +686,7 @@ export default class PaymentModuleService
sharedContext
)
return [payment, isFullyCaptured]
return { payment, isFullyCaptured, capture }
}
@InjectManager()
private async capturePaymentFromProvider_(