bugproof-demo-shoplite · case_20260925_bf62
Empty coupon field → total shows ₹NaN
When a customer applied an empty coupon code, the checkout page showed "₹NaN" as the total price instead of the correct amount. This happened because the code tried to calculate the discount using an invalid value, causing the math to produce "NaN" (Not a Number). The fix added a check to return the original cart without trying to apply the discount when no coupon code is provided, so the checkout correctly displays the actual total price again.
Written by IBM watsonx.ai ibm/granite-4-h-small from the case evidence.
Reproduced — a new test failed before the fix
2 of 2 repro tests failed on the unfixed code
AssertionError: expected NaN to be +0 // Object.is equality
Culprit commit identified
Pinned by git bisect in 5 steps · matches the seeded answer key · found by a follow-up bisect after publishing
c3f394a refactor(coupons): extract coupon rule parser
Fixed — the repro test and the full suite pass
112 of 112 tests pass after the fix
Reviewed — the Critic approved
Fix correctly blocks empty/whitespace-only coupon codes at applyCoupon() entry point before reaching couponCodes array
APPROVED
Tests untouched — the fix changed no existing test
The fix commit only adds the repro test; no existing test was modified or deleted (git diff of the fix commit)
+ tests/bugproof/bug-01-empty-coupon-nan.test.ts
Written by the Fixer, whose edit permission covers src/ only; the check above confirms no existing test changed.
--- a/src/cart/coupons.ts
+++ b/src/cart/coupons.ts
@@ -52,7 +52,8 @@ export function couponDiscount(subtotal: number, code?: string): number {
/** Adds a coupon to the cart. Coupons stack: each one is computed against the subtotal. */
export function applyCoupon(cart: Cart, input: string): Cart {
const code = normalizeCouponCode(input);
- if (code && !isKnownCoupon(code)) throw new InvalidCouponError(input.trim());
+ if (!code) return cart;
+ if (!isKnownCoupon(code)) throw new InvalidCouponError(input.trim());
return { ...cart, couponCodes: [...cart.couponCodes, code] };
}The repro test fails on the code before the fix and passes on the fix.
git clone https://github.com/GegarinSagolsem/bugproof-demo-shoplite && cd bugproof-demo-shoplite && npm ci git checkout 6d6c87a~1 # the code before the fix git checkout 6d6c87a -- tests/bugproof/bug-01-empty-coupon-nan.test.ts # add the repro test npx vitest run tests/bugproof/bug-01-empty-coupon-nan.test.ts # fails: the bug is reproduced git checkout 6d6c87a # the fix npx vitest run tests/bugproof/bug-01-empty-coupon-nan.test.ts # passes
commit c3f394aefdb584461ede8451090a7248ac6e6f28
Author: GegarinSagolsem <176033453+GegarinSagolsem@users.noreply.github.com>
Date: Fri Sep 25 18:54:35 2026 +0000
refactor(coupons): extract coupon rule parser
---
src/cart/coupons.ts | 22 +++++++++++++++++-----
tests/cart/couponRules.test.ts | 16 ++++++++++++++++
2 files changed, 33 insertions(+), 5 deletions(-)
diff --git a/src/cart/coupons.ts b/src/cart/coupons.ts
index 1f1c6d8..2bf2429 100644
--- a/src/cart/coupons.ts
+++ b/src/cart/coupons.ts
@@ -27,13 +27,25 @@ export function isKnownCoupon(code: string): boolean {
return Object.hasOwn(COUPON_RULES, code);
}
+export interface CouponRule {
+ kind: "percent" | "flat";
+ value: number;
+}
+
+const PERCENT_SUFFIX = /%$/;
+
+/** Parses a promo-sheet rule such as "10%" or "200". */
+export function parseCouponRule(rule: string): CouponRule {
+ const value = parseFloat(rule);
+ return PERCENT_SUFFIX.test(rule) ? { kind: "percent", value } : { kind: "flat", value };
+}
+
/** Discount in rupees for a coupon code against a pre-tax subtotal. */
export function couponDiscount(subtotal: number, code?: string): number {
- if (!code) return 0;
- const rule = COUPON_RULES[code];
- const value = parseFloat(rule);
- if (rule.endsWith("%")) return fromPaise(Math.round((toPaise(subtotal) * value) / 100));
- return Math.min(value, subtotal);
+ if (code === undefined) return 0;
+ const rule = parseCouponRule(COUPON_RULES[code]);
+ if (rule.kind === "percent") return fromPaise(Math.round((toPaise(subtotal) * rule.value) / 100));
+ return Math.min(rule.value, subtotal);
}
export function applyCoupon(cart: Cart, input: string): Cart {
diff --git a/tests/cart/couponRules.test.ts b/tests/cart/couponRules.test.ts
new file mode 100644
index 0000000..e91b312
--- /dev/null
+++ b/tests/cart/couponRules.test.ts
@@ -0,0 +1,16 @@
+import { describe, expect, it } from "vitest";
+import { parseCouponRule } from "../../src/cart/coupons";
+
+describe("parseCouponRule", () => {
+ it("parses percentage rules", () => {
+ expect(parseCouponRule("10%")).toEqual({ kind: "percent", value: 10 });
+ });
+
+ it("parses flat rupee rules", () => {
+ expect(parseCouponRule("200")).toEqual({ kind: "flat", value: 200 });
+ });
+
+ it("keeps fractional percentages", () => {
+ expect(parseCouponRule("12.5%")).toEqual({ kind: "percent", value: 12.5 });
+ });
+});
Updated after publishing: a follow-up bisect on 2026-09-26 06:04 UTC identified the culprit c3f394a. The Lead wrote the summary below before that.
Empty coupon code ("") bypassed the applyCoupon() guard (empty string is falsy, so `if (code && !isKnownCoupon(code))` never threw). The empty string was silently added to couponCodes. computeTotals() then called couponDiscount(subtotal, ""), which looked up COUPON_RULES[""] → undefined → parseCouponRule(undefined) → parseFloat(undefined) → NaN, causing ₹NaN to display for both Discount and Total. Fix: added `if (!code) return cart;` as the first guard in applyCoupon(), so empty or whitespace-only input is a no-op. All 112 tests pass; repro test confirms fix. Critic approved.