chore: Ensure refund doesn't exceed captured amount (#13744)
* wip * chore: prepare for PR * move to end * Change order of operations in refundPaymentWorkflow Updated the order of operations in the refundPaymentWorkflow. * chore: Add validation
This commit is contained in:
5
.changeset/strange-worms-invent.md
Normal file
5
.changeset/strange-worms-invent.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/core-flows": patch
|
||||
---
|
||||
|
||||
chore: Change order of operations in refundPaymentWorkflow
|
||||
@@ -254,7 +254,7 @@ medusaIntegrationTestRunner({
|
||||
refund_reason_id: refundReason.id,
|
||||
refund_reason: expect.objectContaining({
|
||||
label: "test",
|
||||
code: "test"
|
||||
code: "test",
|
||||
}),
|
||||
}),
|
||||
],
|
||||
@@ -369,6 +369,73 @@ medusaIntegrationTestRunner({
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should throw if the refund amount exceeds the payment amount", async () => {
|
||||
const payment = order.payment_collections[0].payments[0]
|
||||
|
||||
const refundReason = (
|
||||
await api.post(
|
||||
`/admin/refund-reasons`,
|
||||
{ label: "Test", code: "test" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.refund_reason
|
||||
|
||||
await api.post(
|
||||
`/admin/payments/${payment.id}/capture`,
|
||||
undefined,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/payments/${payment.id}/refund`,
|
||||
{
|
||||
amount: 50,
|
||||
refund_reason_id: refundReason.id,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const updatedOrder = (
|
||||
await api.get(`/admin/orders/${order.id}`, adminHeaders)
|
||||
).data.order
|
||||
|
||||
expect(updatedOrder.credit_line_total).toEqual(50)
|
||||
expect(updatedOrder.credit_lines).toEqual([
|
||||
expect.objectContaining({
|
||||
reference: "Test",
|
||||
reference_id: "test",
|
||||
}),
|
||||
])
|
||||
|
||||
try {
|
||||
await api.post(
|
||||
`/admin/payments/${payment.id}/refund`,
|
||||
{
|
||||
amount: 5000,
|
||||
refund_reason_id: refundReason.id,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
} catch (error) {
|
||||
expect(error.response.status).toBe(400)
|
||||
expect(error.response.data.message).toBe(
|
||||
"You are not allowed to refund more than the captured amount"
|
||||
)
|
||||
}
|
||||
|
||||
const updatedUpdatedOrder = (
|
||||
await api.get(`/admin/orders/${order.id}`, adminHeaders)
|
||||
).data.order
|
||||
|
||||
expect(updatedUpdatedOrder.credit_line_total).toEqual(50)
|
||||
expect(updatedUpdatedOrder.credit_lines).toEqual([
|
||||
expect.objectContaining({
|
||||
reference: "Test",
|
||||
reference_id: "test",
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import type { BigNumberInput, OrderDTO } from "@medusajs/framework/types"
|
||||
import { ChangeActionType, OrderChangeStatus, OrderChangeType, } from "@medusajs/framework/utils"
|
||||
import { createStep, createWorkflow, transform, WorkflowData, } from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
ChangeActionType,
|
||||
OrderChangeStatus,
|
||||
OrderChangeType,
|
||||
} from "@medusajs/framework/utils"
|
||||
import {
|
||||
createStep,
|
||||
createWorkflow,
|
||||
transform,
|
||||
WorkflowData,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { useQueryGraphStep } from "../../../common"
|
||||
import { confirmOrderChanges } from "../../steps/confirm-order-changes"
|
||||
import { createOrderChangeStep } from "../../steps/create-order-change"
|
||||
@@ -27,8 +36,8 @@ export const createOrderRefundCreditLinesWorkflow = createWorkflow(
|
||||
function (
|
||||
input: WorkflowData<{
|
||||
order_id: string
|
||||
amount: BigNumberInput,
|
||||
reference?: string,
|
||||
amount: BigNumberInput
|
||||
reference?: string
|
||||
referenceId?: string
|
||||
created_by?: string
|
||||
}>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { BigNumberInput, IPaymentModuleService } from "@medusajs/framework/types"
|
||||
import {
|
||||
BigNumberInput,
|
||||
IPaymentModuleService,
|
||||
} from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import { BigNumberInput } from "@medusajs/framework/types"
|
||||
import { MathBN, PaymentEvents } from "@medusajs/framework/utils"
|
||||
import { createWorkflow, transform, when, WorkflowData, WorkflowResponse, } from "@medusajs/framework/workflows-sdk"
|
||||
import { BigNumberInput, PaymentDTO } from "@medusajs/framework/types"
|
||||
import {
|
||||
BigNumber,
|
||||
MathBN,
|
||||
MedusaError,
|
||||
PaymentEvents,
|
||||
} from "@medusajs/framework/utils"
|
||||
import {
|
||||
createStep,
|
||||
createWorkflow,
|
||||
transform,
|
||||
when,
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { emitEventStep, useRemoteQueryStep } from "../../common"
|
||||
import { addOrderTransactionStep } from "../../order/steps/add-order-transaction"
|
||||
import { refundPaymentStep } from "../steps/refund-payment"
|
||||
import { createOrderRefundCreditLinesWorkflow } from "../../order/workflows/payments/create-order-refund-credit-lines"
|
||||
import { refundPaymentStep } from "../steps/refund-payment"
|
||||
|
||||
/**
|
||||
* The data to refund a payment.
|
||||
@@ -32,6 +44,49 @@ export type RefundPaymentWorkflowInput = {
|
||||
refund_reason_id?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* This step validates that an order refund credit line can be issued
|
||||
*/
|
||||
export const validateRefundPaymentExceedsCapturedAmountStep = createStep(
|
||||
"validate-refund-payment-exceeds-captured-amount",
|
||||
async function ({
|
||||
payment,
|
||||
refundAmount,
|
||||
}: {
|
||||
payment: PaymentDTO
|
||||
refundAmount: BigNumberInput
|
||||
}) {
|
||||
const capturedAmount = (payment.captures || []).reduce(
|
||||
(captureAmount, next) => {
|
||||
const amountAsBigNumber = new BigNumber(
|
||||
next.raw_amount as BigNumberInput
|
||||
)
|
||||
return MathBN.add(captureAmount, amountAsBigNumber)
|
||||
},
|
||||
MathBN.convert(0)
|
||||
)
|
||||
|
||||
const refundedAmount = (payment.refunds || []).reduce(
|
||||
(refundedAmount, next) => {
|
||||
const amountAsBigNumber = new BigNumber(
|
||||
next.raw_amount as BigNumberInput
|
||||
)
|
||||
return MathBN.add(refundedAmount, amountAsBigNumber)
|
||||
},
|
||||
MathBN.convert(0)
|
||||
)
|
||||
|
||||
const totalRefundedAmount = MathBN.add(refundedAmount, refundAmount)
|
||||
|
||||
if (MathBN.lt(capturedAmount, totalRefundedAmount)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`You are not allowed to refund more than the captured amount`
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const refundPaymentWorkflowId = "refund-payment-workflow"
|
||||
/**
|
||||
* This workflow refunds a payment. It's used by the
|
||||
@@ -63,12 +118,21 @@ export const refundPaymentWorkflow = createWorkflow(
|
||||
"currency_code",
|
||||
"amount",
|
||||
"raw_amount",
|
||||
"captures.raw_amount",
|
||||
"refunds.raw_amount",
|
||||
],
|
||||
variables: { id: input.payment_id },
|
||||
list: false,
|
||||
throw_if_key_not_found: true,
|
||||
})
|
||||
|
||||
when({ input }, ({ input }) => !!input.amount).then(() =>
|
||||
validateRefundPaymentExceedsCapturedAmountStep({
|
||||
payment,
|
||||
refundAmount: input.amount as BigNumberInput,
|
||||
})
|
||||
)
|
||||
|
||||
const orderPaymentCollection = useRemoteQueryStep({
|
||||
entry_point: "order_payment_collection",
|
||||
fields: ["order.id"],
|
||||
@@ -87,8 +151,8 @@ export const refundPaymentWorkflow = createWorkflow(
|
||||
|
||||
const refundReason = when(
|
||||
"fetch-refund-reason",
|
||||
{ input }, ({ input }) =>
|
||||
!!input.refund_reason_id
|
||||
{ input },
|
||||
({ input }) => !!input.refund_reason_id
|
||||
).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
entry_point: "refund_reason",
|
||||
@@ -99,34 +163,30 @@ export const refundPaymentWorkflow = createWorkflow(
|
||||
}).config({ name: "refund-reason" })
|
||||
})
|
||||
|
||||
const creditLineAmount = transform({ order, payment, input }, ({ order, payment, input }) => {
|
||||
const pendingDifference = order.summary?.raw_pending_difference! ?? order.summary?.pending_difference! ?? 0
|
||||
const amountToRefund = input.amount ?? payment.raw_amount ?? payment.amount
|
||||
|
||||
if (MathBN.lt(pendingDifference, 0)) {
|
||||
const amountOwed = MathBN.mult(pendingDifference, -1)
|
||||
|
||||
return MathBN.gt(amountToRefund, amountOwed) ? MathBN.sub(amountToRefund, amountOwed) : 0
|
||||
}
|
||||
|
||||
return amountToRefund
|
||||
})
|
||||
|
||||
when(
|
||||
{ creditLineAmount, refundReason }, ({ creditLineAmount, refundReason }) => MathBN.gt(creditLineAmount, 0)
|
||||
).then(() => {
|
||||
createOrderRefundCreditLinesWorkflow.runAsStep({
|
||||
input: {
|
||||
order_id: order.id,
|
||||
amount: creditLineAmount,
|
||||
reference: refundReason?.label,
|
||||
referenceId: refundReason?.code
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const refundPayment = refundPaymentStep(input)
|
||||
|
||||
const creditLineAmount = transform(
|
||||
{ order, payment, input },
|
||||
({ order, payment, input }) => {
|
||||
const pendingDifference =
|
||||
order.summary?.raw_pending_difference! ??
|
||||
order.summary?.pending_difference! ??
|
||||
0
|
||||
const amountToRefund =
|
||||
input.amount ?? payment.raw_amount ?? payment.amount
|
||||
|
||||
if (MathBN.lt(pendingDifference, 0)) {
|
||||
const amountOwed = MathBN.mult(pendingDifference, -1)
|
||||
|
||||
return MathBN.gt(amountToRefund, amountOwed)
|
||||
? MathBN.sub(amountToRefund, amountOwed)
|
||||
: 0
|
||||
}
|
||||
|
||||
return amountToRefund
|
||||
}
|
||||
)
|
||||
|
||||
when({ orderPaymentCollection }, ({ orderPaymentCollection }) => {
|
||||
return !!orderPaymentCollection?.order?.id
|
||||
}).then(() => {
|
||||
@@ -151,6 +211,19 @@ export const refundPaymentWorkflow = createWorkflow(
|
||||
addOrderTransactionStep(orderTransactionData)
|
||||
})
|
||||
|
||||
when({ creditLineAmount }, ({ creditLineAmount }) =>
|
||||
MathBN.gt(creditLineAmount, 0)
|
||||
).then(() => {
|
||||
createOrderRefundCreditLinesWorkflow.runAsStep({
|
||||
input: {
|
||||
order_id: order.id,
|
||||
amount: creditLineAmount,
|
||||
reference: refundReason?.label,
|
||||
referenceId: refundReason?.code,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
emitEventStep({
|
||||
eventName: PaymentEvents.REFUNDED,
|
||||
data: { id: payment.id },
|
||||
|
||||
Reference in New Issue
Block a user