Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(payment): Partial refunds #8603

Merged
merged 2 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions integration-tests/http/__tests__/payment/admin/payment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
50 changes: 24 additions & 26 deletions packages/modules/payment/src/services/payment-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -794,20 +791,21 @@ 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(
{
data: payment.data!,
provider_id: payment.provider_id,
},
payment.raw_amount
refund.raw_amount
)

await this.paymentService_.update(
Expand Down
Loading