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 { ClaimType } from "@medusajs/utils"
import { adminHeaders } from "../../../../helpers/create-admin-user"
import { medusaIntegrationTestRunner } from "medusa-test-utils" 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" import { createOrderSeeder } from "../../fixtures/order"
jest.setTimeout(30000) jest.setTimeout(50000)
medusaIntegrationTestRunner({ medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => { testSuite: ({ dbConnection, getContainer, api }) => {
@@ -78,6 +79,92 @@ medusaIntegrationTestRunner({
expect(response.status).toEqual(200) 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 () => { it("should refund a captured payment", async () => {
const payment = order.payment_collections[0].payments[0] 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({ await service.capturePayment({
amount: 100, amount: 100,
payment_id: "pay-id-1", payment_id: "pay-id-1",
}) })
const error = await service const capturedPayment = await service.capturePayment({
.capturePayment({ amount: 100,
amount: 100, payment_id: "pay-id-1",
payment_id: "pay-id-1", })
})
.catch((e) => e)
expect(error.message).toEqual( expect(capturedPayment).toEqual(
"You cannot capture more than the authorized amount substracted by what is already captured." 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, data: CreateCaptureDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<PaymentDTO> { ): Promise<PaymentDTO> {
const [payment, isFullyCaptured] = await this.capturePayment_( const { payment, isFullyCaptured, capture } = await this.capturePayment_(
data, data,
sharedContext sharedContext
) )
@@ -595,7 +595,9 @@ export default class PaymentModuleService
sharedContext sharedContext
) )
} catch (error) { } catch (error) {
await super.deleteCaptures(data.payment_id, sharedContext) if (capture?.id) {
await super.deleteCaptures({ id: capture.id }, sharedContext)
}
throw error throw error
} }
@@ -615,7 +617,11 @@ export default class PaymentModuleService
private async capturePayment_( private async capturePayment_(
data: CreateCaptureDTO, data: CreateCaptureDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<[Payment, boolean]> { ): Promise<{
payment: Payment
isFullyCaptured: boolean
capture?: Capture
}> {
const payment = await this.paymentService_.retrieve( const payment = await this.paymentService_.retrieve(
data.payment_id, data.payment_id,
{ {
@@ -626,6 +632,7 @@ export default class PaymentModuleService
"payment_collection_id", "payment_collection_id",
"amount", "amount",
"raw_amount", "raw_amount",
"captured_at",
"canceled_at", "canceled_at",
], ],
relations: ["captures.raw_amount"], relations: ["captures.raw_amount"],
@@ -646,14 +653,7 @@ export default class PaymentModuleService
} }
if (payment.captured_at) { if (payment.captured_at) {
return [ return { payment, isFullyCaptured: true }
(await this.retrievePayment(
data.payment_id,
{ relations: ["captures"] },
sharedContext
)) as unknown as Payment,
true,
]
} }
const capturedAmount = payment.captures.reduce((captureAmount, next) => { const capturedAmount = payment.captures.reduce((captureAmount, next) => {
@@ -677,7 +677,7 @@ export default class PaymentModuleService
) )
const isFullyCaptured = MathBN.gte(totalCaptured, authorizedAmount) const isFullyCaptured = MathBN.gte(totalCaptured, authorizedAmount)
await this.captureService_.create( const capture = await this.captureService_.create(
{ {
payment: data.payment_id, payment: data.payment_id,
amount: data.amount, amount: data.amount,
@@ -686,7 +686,7 @@ export default class PaymentModuleService
sharedContext sharedContext
) )
return [payment, isFullyCaptured] return { payment, isFullyCaptured, capture }
} }
@InjectManager() @InjectManager()
private async capturePaymentFromProvider_( private async capturePaymentFromProvider_(