All-or-nothing refunds create a real business problem: a customer receives a damaged item among a multi-item order and is entitled to a partial refund. If the refund handler always calls stripe.refunds.create() with no amount parameter, the only option is a full refund — over-refunding destroys margin. The iso-25010:2011 functional correctness gap here is the absence of an amount field on the refund interface, and the GDPR Art. 17 dimension is the inability to correctly reflect the agreed financial correction in the stored record. The validation preventing total refunds from exceeding order total is equally important — without it, a bug or a race condition can issue more money than was collected.
Medium because all-or-nothing refunds force over-refunding in partial-damage scenarios and prevent accurate financial reconciliation across multi-item orders.
Accept an amount parameter in your refund handler at app/api/orders/[id]/refund/route.ts, validate it against the refundable balance, and pass it explicitly to the payment provider.
const alreadyRefunded = order.refunds.reduce((sum, r) => sum + r.amount, 0)
const maxRefundable = order.total - alreadyRefunded
if (amount > maxRefundable) {
return Response.json(
{ error: `Refund amount exceeds refundable balance of ${maxRefundable}` },
{ status: 400 }
)
}
const providerRefund = await stripe.refunds.create({
payment_intent: order.paymentIntentId,
amount, // explicit partial amount in cents
})
ID: ecommerce-order-management.cancellation-refunds.partial-refunds
Severity: medium
What to look for: If refunds are supported, check whether the refund logic allows partial refunds — a refund amount that is less than the full order total. Count the number of refund-related fields in the data model: look for an amount field, a currency field, and a running total or sum mechanism. Check whether the refund handler accepts an amount parameter or always uses the full charge. Also check whether validation exists preventing the total refunded amount from exceeding the order total — quote the exact validation expression if found.
Pass criteria: The refund system supports partial refunds. An amount field exists on the refund model and is used when calling the payment provider. Multiple refund records can exist per order (the schema allows at least 1 refund-to-order relationship). There is validation preventing the total refunded amount from exceeding the order total. A refund handler that always refunds order.total with no amount parameter does not count as pass.
Fail criteria: Refunds are all-or-nothing only — the refund logic always refunds the full order amount and does not accept an amount parameter (0 amount fields in the refund interface). No support for issuing a partial refund.
Skip (N/A) when: The project does not support refunds at all (covered by the refund-status-separate check). Or the project only sells single-item orders where a partial refund is not a meaningful concept. No refund handler exists.
Detail on fail: "The refund handler at src/app/api/orders/[id]/refund/route.ts always calls stripe.refunds.create({ payment_intent: order.paymentIntentId }) with no amount parameter, which refunds the full charge. 0 partial refund support exists."
Remediation: Accept and validate an amount parameter in your refund handler at app/api/orders/[id]/refund/route.ts:
// app/api/orders/[id]/refund/route.ts
export async function POST(req: Request, { params }) {
const { amount, reason } = await req.json()
const order = await db.orders.findUnique({
where: { id: params.id },
include: { refunds: true },
})
const alreadyRefunded = order.refunds.reduce((sum, r) => sum + r.amount, 0)
const maxRefundable = order.total - alreadyRefunded
if (amount > maxRefundable) {
return Response.json(
{ error: `Refund amount exceeds refundable balance of ${maxRefundable}` },
{ status: 400 }
)
}
// Pass amount to payment provider
const providerRefund = await stripe.refunds.create({
payment_intent: order.paymentIntentId,
amount, // partial refund amount in cents
})
await db.refund.create({
data: { orderId: params.id, amount, reason, providerRefundId: providerRefund.id, status: 'processing' },
})
return Response.json({ success: true })
}