fix(payment): Partial refunds (#8603)

* fix(payment): Partial payment provider refunds

* add tests
This commit is contained in:
Oli Juhl
2024-08-15 19:01:22 +02:00
committed by GitHub
parent c92aa3e397
commit 9de9b3825f
3 changed files with 168 additions and 26 deletions

View File

@@ -170,6 +170,104 @@ medusaIntegrationTestRunner({
)
})
it("should issue multiple refunds", async () => {
await api.post(
`/admin/payments/${payment.id}/capture`,
undefined,
adminHeaders
)
const refundReason = (
await api.post(`/admin/refund-reasons`, { label: "test" }, adminHeaders)
).data.refund_reason
await api.post(
`/admin/payments/${payment.id}/refund`,
{
amount: 250,
refund_reason_id: refundReason.id,
note: "Do not like it",
},
adminHeaders
)
await api.post(
`/admin/payments/${payment.id}/refund`,
{
amount: 250,
refund_reason_id: refundReason.id,
note: "Do not like it",
},
adminHeaders
)
const refundedPayment = (
await api.get(`/admin/payments/${payment.id}`, adminHeaders)
).data.payment
expect(refundedPayment).toEqual(
expect.objectContaining({
id: payment.id,
currency_code: "usd",
amount: 1000,
captured_at: expect.any(String),
captures: [
expect.objectContaining({
amount: 1000,
}),
],
refunds: [
expect.objectContaining({
amount: 250,
note: "Do not like it",
}),
expect.objectContaining({
amount: 250,
note: "Do not like it",
}),
],
})
)
})
it("should throw if refund exceeds captured total", async () => {
await api.post(
`/admin/payments/${payment.id}/capture`,
undefined,
adminHeaders
)
const refundReason = (
await api.post(`/admin/refund-reasons`, { label: "test" }, adminHeaders)
).data.refund_reason
await api.post(
`/admin/payments/${payment.id}/refund`,
{
amount: 250,
refund_reason_id: refundReason.id,
note: "Do not like it",
},
adminHeaders
)
const e = await api
.post(
`/admin/payments/${payment.id}/refund`,
{
amount: 1000,
refund_reason_id: refundReason.id,
note: "Do not like it",
},
adminHeaders
)
.catch((e) => e)
expect(e.response.data.message).toEqual(
"You cannot refund more than what is captured on the payment."
)
})
it("should not update payment collection of other orders", async () => {
await setupTaxStructure(container.resolve(ModuleRegistrationName.TAX))
await seedStorefrontDefaults(container, "dkk")

View File

@@ -685,6 +685,52 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
)
})
it("should fully refund a payment through two refunds", async () => {
await service.capturePayment({
amount: 100,
payment_id: "pay-id-2",
})
const refundedPaymentOne = await service.refundPayment({
amount: 50,
payment_id: "pay-id-2",
})
const refundedPaymentTwo = await service.refundPayment({
amount: 50,
payment_id: "pay-id-2",
})
expect(refundedPaymentOne).toEqual(
expect.objectContaining({
id: "pay-id-2",
amount: 100,
refunds: [
expect.objectContaining({
created_by: null,
amount: 50,
}),
],
})
)
expect(refundedPaymentTwo).toEqual(
expect.objectContaining({
id: "pay-id-2",
amount: 100,
refunds: [
expect.objectContaining({
created_by: null,
amount: 50,
}),
expect.objectContaining({
created_by: null,
amount: 50,
}),
],
})
)
})
it("should throw if refund is greater than captured amount", async () => {
await service.capturePayment({
amount: 50,

View File

@@ -717,10 +717,25 @@ export default class PaymentModuleService
data: CreateRefundDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<PaymentDTO> {
const payment = await this.refundPayment_(data, sharedContext)
const payment = await this.paymentService_.retrieve(
data.payment_id,
{
select: [
"id",
"data",
"provider_id",
"payment_collection_id",
"amount",
"raw_amount",
],
relations: ["captures.raw_amount", "refunds.raw_amount"],
},
sharedContext
)
const refund = await this.refundPayment_(payment, data, sharedContext)
try {
await this.refundPaymentFromProvider_(payment, sharedContext)
await this.refundPaymentFromProvider_(payment, refund, sharedContext)
} catch (error) {
await super.deleteRefunds(data.payment_id, sharedContext)
throw error
@@ -740,25 +755,10 @@ export default class PaymentModuleService
@InjectTransactionManager("baseRepository_")
private async refundPayment_(
payment: Payment,
data: CreateRefundDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<Payment> {
const payment = await this.paymentService_.retrieve(
data.payment_id,
{
select: [
"id",
"data",
"provider_id",
"payment_collection_id",
"amount",
"raw_amount",
],
relations: ["captures.raw_amount", "refunds.raw_amount"],
},
sharedContext
)
): Promise<Refund> {
if (!data.amount) {
data.amount = payment.amount as BigNumberInput
}
@@ -771,10 +771,7 @@ export default class PaymentModuleService
return MathBN.add(refundedAmount, next.raw_amount)
}, MathBN.convert(0))
const totalRefundedAmount = MathBN.add(
refundedAmount,
data.amount
)
const totalRefundedAmount = MathBN.add(refundedAmount, data.amount)
if (MathBN.lt(capturedAmount, totalRefundedAmount)) {
throw new MedusaError(
@@ -783,7 +780,7 @@ export default class PaymentModuleService
)
}
await this.refundService_.create(
const refund = await this.refundService_.create(
{
payment: data.payment_id,
amount: data.amount,
@@ -794,12 +791,13 @@ export default class PaymentModuleService
sharedContext
)
return payment
return refund
}
@InjectManager("baseRepository_")
private async refundPaymentFromProvider_(
payment: Payment,
refund: Refund,
@MedusaContext() sharedContext: Context = {}
) {
const paymentData = await this.paymentProviderService_.refundPayment(
@@ -807,7 +805,7 @@ export default class PaymentModuleService
data: payment.data!,
provider_id: payment.provider_id,
},
payment.raw_amount
refund.raw_amount
)
await this.paymentService_.update(