Prompts
Coding

Review a code change for bugs before it ships

Find the bugs, security holes and missing tests in a diff, ranked by severity, with a concrete failing input for each.

by Appndo

Tested on GPT-6.1 Sol, Gemini 3.8 Flash · Verified Nov 2, 2026

The prompt

Review this code change before it ships.

The change (diff or new code):
{{diff}}

What it's for, and facts the code doesn't show (data types, how it's called, the rules it must follow):
{{context}}

1. List each problem with the line or function, what goes wrong, and a concrete input or sequence of events that triggers it. Check correctness against the rules I gave, security (injection, access control, secrets), data loss, concurrency and repeated runs, error handling, and wrong assumptions about types, units or time zones.
2. Rank the problems: blocks shipping (wrong results, security hole, data loss), should fix before merge, worth a follow-up. Leave out style preferences.
3. Mark each problem as certain from the code shown, or depending on code or behaviour you can't see. For the second kind, say what to check.
4. Give the smallest fix for each blocking problem, as code.
5. List the tests that would have caught the blocking problems, one line each.

Don't report problems in code you weren't shown, and don't call code wrong only because you would have written it differently.

How to use it

Paste the diff or the new function, plus the facts a reviewer would ask for: what calls it, the types and units of the data, and any business rules it must follow. Remove secrets first. Fix the blocking problems, then add the listed tests.

Example

Inputs

diff
// POST /api/cart/apply-coupon export async function applyCoupon(req: Request, user: User) { const { code } = await req.json(); const coupon = await db.query(`SELECT * FROM coupons WHERE code = '${code}'`); if (!coupon) return json({ error: 'Invalid code' }, 400); if (coupon.expires_at < Date.now()) return json({ error: 'Expired' }, 400); if (coupon.uses >= coupon.max_uses) return json({ error: 'Used up' }, 400); const cart = await getCart(user.id); cart.discount = cart.total * coupon.percent / 100; cart.total = cart.total - cart.discount; saveCart(cart); await db.query('UPDATE coupons SET uses = uses + 1 WHERE id = $1', [coupon.id]); return json({ total: cart.total }); }
context
This change adds coupon codes to checkout. db.query returns an array of rows (Postgres). coupons.expires_at is a timestamptz column and comes back as a JavaScript Date. Cart totals are stored as integer cents. Product rule: each customer can use a given coupon once. saveCart returns a Promise.

Output

The change blocks shipping. The array returned by db.query breaks the handler immediately; several additional defects become reachable once that is fixed.

Blocks shipping

  1. SQL injection — first db.query. Certain. code is inserted directly into SQL. Input such as {"code":"' OR TRUE --"} changes the query to return all coupons. The current array bug may prevent successful checkout, but the injected query still executes.

    Smallest fix:

    const rows = await db.query(
      'SELECT * FROM coupons WHERE code = $1',
      [code],
    );
    
  2. Treating an array as a coupon — lookup and all subsequent coupon.* accesses. Certain. No match returns [], which is truthy, so !coupon never rejects it. A match also returns an array rather than a row. For either result, fields such as coupon.percent are undefined; the discount and total become NaN, and coupon.id is undefined. This can attempt to save an invalid cart and then fail the update.

    Smallest fix, combined with parameterization:

    const [coupon] = await db.query(
      'SELECT * FROM coupons WHERE code = $1',
      [code],
    );
    if (!coupon) return json({ error: 'Invalid code' }, 400);
    
  3. No enforcement of one use per customer — applyCoupon. Certain in this handler; end-to-end enforcement depends on unseen behavior. After fixing the lookup, the same customer can submit the same code twice. Nothing shown records or checks (customer, coupon). Check whether another layer already enforces this rule atomically; a client-side check would not suffice.

    Smallest reliable fix: record redemption with a database uniqueness constraint and reject duplicates. Run this inside the same transaction as the cart change and usage increment:

    CREATE TABLE coupon_redemptions (
      customer_id bigint NOT NULL,
      coupon_id bigint NOT NULL,
      PRIMARY KEY (customer_id, coupon_id)
    );
    

    Use the actual ID types from your schema.

    const redeemed = await tx.query(
      `INSERT INTO coupon_redemptions (customer_id, coupon_id)
       VALUES ($1, $2)
       ON CONFLICT DO NOTHING
       RETURNING coupon_id`,
      [user.id, coupon.id],
    );
    if (redeemed.length === 0) {
      // Abort/roll back the transaction before returning this response.
      throw new CouponError('Already used');
    }
    
  4. Global usage limit can be exceeded — separate usage check and increment. Certain race in the shown logic; database protection is unknown. With uses = 9 and max_uses = 10, two customers can both read 9, both receive discounts, and increment usage to 11. Check for database constraints or surrounding serialization.

    Smallest fix: replace the separate check and unconditional increment with an atomic reservation, inside the transaction:

    const reserved = await tx.query(
      `UPDATE coupons
       SET uses = uses + 1
       WHERE id = $1 AND uses < max_uses
       RETURNING id`,
      [coupon.id],
    );
    if (reserved.length === 0) throw new CouponError('Used up');
    
  5. Cart persistence is not awaited, and the writes are not coordinated — saveCart(cart) through the final update. Certain missing await; partial-state consequences depend on persistence behavior. If saveCart rejects after the coupon update succeeds, a use is consumed without a saved discount, and the handler may already have returned success. Conversely, if the cart saves but the coupon update fails, the discount persists without consuming a use. Concurrent requests can also overwrite cart changes if persistence has no locking or version check.

    Smallest complete fix: await saving and commit all related writes together, locking the cart or using an equivalent version check. The following is structural code: adapt transaction and locking calls to your database layer.

    const total = await db.transaction(async (tx) => {
      const cart = await getCartForUpdate(tx, user.id);
      // Fetch coupon, validate it, insert redemption, and reserve a use here.
      // Any failure must roll back all writes.
      cart.discount = discount;
      cart.total = discountedTotal;
      await saveCart(cart, tx);
      return cart.total;
    });
    return json({ total });
    

    Merely adding await saveCart(cart) fixes premature success but does not fix partial commits. Check whether cart storage can participate in the same transaction; if it cannot, this needs a coordinated persistence design.

  6. Percentage arithmetic can violate integer cents — discount calculation. Depends on allowed totals and percentages. A 10% coupon on a 101-cent cart produces a 10.1-cent discount and a 90.9-cent total. Check whether such combinations are permitted and which rounding rule the product requires. If they are permitted, this blocks shipping.

    Smallest fix, assuming the agreed rule is rounding the discount to the nearest cent:

    const discount = Math.round(cart.total * coupon.percent / 100);
    const discountedTotal = cart.total - discount;
    

    Also check the database type returned for percent and its enforced range. The shown code alone does not establish either.

Should fix before merge

  1. Malformed requests are not handled — req.json() and destructuring. Certain missing handling locally; response behavior depends on outer error handling. An invalid JSON body makes req.json() reject; JSON null makes destructuring throw. Missing or non-string code also reaches the database. Check whether the router already converts these cases into controlled client errors. Otherwise, catch parsing failures and require a nonempty string before querying.

Worth a follow-up / rules to confirm

  1. Discounts use the current total — discount calculation. Depends on cart semantics and coupon stacking rules. If a saved cart already has a coupon discount, applying another coupon calculates against that discounted total and replaces cart.discount. For example, 1,000 cents becomes 900 after 10%, then 810 after another 10%, while the recorded discount becomes only 90. Check whether coupons may stack and whether cart.total includes existing discounts. If this contradicts checkout rules or accounting invariants, promote it to a shipping blocker and calculate from the defined eligible base.

The Date comparison is valid: JavaScript converts the returned Date to epoch milliseconds when comparing it with Date.now(). There is no demonstrated time-zone or seconds-versus-milliseconds bug. Whether equality at the expiration instant is allowed is a rule to confirm.

No access-control or secret-disclosure defect is established by this snippet: cart ownership uses user.id; authentication of that supplied user is outside the code shown.

Tests that catch the blockers

  • Submit "' OR TRUE --"; verify it is treated as a literal code and changes no cart or usage count.
  • Return [] from the lookup; expect 400 and no persistence calls.
  • Return [validCoupon]; expect a finite, correct total and an update using that coupon’s ID.
  • Apply expired and exhausted coupon rows; expect rejection with no writes.
  • Redeem the same coupon twice sequentially for one customer; only the first succeeds.
  • Redeem the same coupon concurrently for one customer; exactly one succeeds and usage increases once.
  • Have two customers race for the final available use; exactly one receives the discount.
  • Delay cart saving; verify success is not returned before persistence commits.
  • Fail either cart persistence or the usage update; verify cart, redemption, and usage all remain unchanged.
  • Race coupon application against another cart mutation; verify there is no lost update or discount based on a stale total.
  • Apply 10% to 101 cents; verify the agreed rounding result and integer-cent persistence.

One real run with the example above. Results vary between runs and models.