A cancellation handler that updates the booking status in the database but never calls the payment provider's refund API silently keeps customer money. Customers who cancel within the refund window see a "Cancelled" status but receive no refund — the money transfer is one-way. CWE-841 (behavioral workflow violation) applies: the business workflow specifies refund-on-cancel, but the code path that executes cancellation does not include the corresponding refund step. Customers discover the missing refund days later via their bank statement, producing chargebacks that are more expensive than the original refund would have been.
Medium because the defect silently retains customer funds on cancellation, which is both a trust violation and a chargeback risk, though it requires cancellation of a paid booking to manifest.
Trigger the refund API call inside the cancellation handler before updating the booking status. Implement this in src/lib/booking-service.ts or src/app/api/bookings/[id]/cancel/route.ts.
export async function cancelBooking(bookingId: string) {
const booking = await db.booking.findUniqueOrThrow({ where: { id: bookingId } });
if (booking.paymentIntentId && booking.status === 'CONFIRMED') {
await stripe.refunds.create({
payment_intent: booking.paymentIntentId,
reason: 'requested_by_customer',
});
}
await db.booking.update({
where: { id: bookingId },
data: { status: 'CANCELLED', cancelledAt: new Date() }
});
}
ID: booking-flow-lifecycle.payment.refund-logic
Label: Refund logic exists
Severity: medium
What to look for: Count all cancellation code paths for paid bookings and verify each one includes an automated refund API call. Search for stripe.refunds.create, paypal.capture.refund, square.refundsApi.refundPayment, or equivalent. Also check for partial refund support (e.g., late cancellation penalty). Enumerate: "X of Y cancellation paths for paid bookings trigger automated refunds."
Pass criteria: The cancellation handler triggers an automated refund if the booking was paid and the refund policy allows it. At least 1 call to the payment provider's refund API must exist in a cancellation path. The refund must reference the original payment ID (not a manual amount). Report: "X of Y paid cancellation paths trigger refund via [provider API] in [file:line]."
Fail criteria: Cancellation is DB-only; no refund API is called despite the booking having a payment record. Money is kept without explicit policy documentation. Do NOT pass when a comment says "TODO: add refund" — unimplemented refunds fail.
Skip (N/A) when: No payment integration, or policy explicitly does not allow refunds (documented in code, config, or README as a no-refund policy).
Detail on fail: "The cancelBooking function updates the database but does not trigger a Stripe refund. Customers must contact support for refunds."
Cross-reference: The cancellation-state-machine check in Lifecycle Management verifies the cancellation handler where refunds should be triggered.
Cross-reference: The payment-booking-coupling check in this category verifies the payment ID is available for the refund call.
Cross-reference: The deadline-enforcement check in Lifecycle Management may affect refund eligibility (no refund for late cancellations).
Remediation: Integrate refund API call in your cancellation handler (e.g., src/lib/booking-service.ts or src/app/api/bookings/[id]/cancel/route.ts).
export async function cancelBooking(bookingId: string) {
const booking = await db.booking.findUniqueOrThrow({
where: { id: bookingId }
});
if (booking.paymentIntentId && booking.status === 'CONFIRMED') {
// Initiate refund
await stripe.refunds.create({
payment_intent: booking.paymentIntentId,
reason: 'requested_by_customer'
});
}
// Update booking
await db.booking.update({
where: { id: bookingId },
data: { status: 'CANCELLED', refundedAt: new Date() }
});
}