NWP-201: Issue virtual cards from the console - #177
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eshold Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stores last four and a reference, never the full number. A reused idempotency key returns the original card id instead of minting again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… PATCH POST /api/cards requires an Idempotency-Key and returns the full number exactly once. GET list and detail carry last four only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Freeze/unfreeze from the list refreshes in place. Cancel needs a confirm and renders the terminal state. Detail shows spend against the limit and the status history. Drawer close button gets an accessible name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sortPayments compared String(amount), so 900 sorted after 25000. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude Code 101 — Repo Rescue🏆 Build Battle Score: 88 / 100One-line verdict: The most complete submission of the field — real idempotency, currency/merchant matching, audit trail, and an honest spend model, all backed by tests that would actually fail without the change; docked mainly because the diff is truncated and several load-bearing files ( Core criteria — 90 / 100 (35%)
Correctness rules — 90 / 100 (20%)
Context and planning — 70 / 100 (10%)PR claims a spec at Code quality — 85 / 100 (15%)Tests sit next to the code they cover ( PR description — 90 / 100 (5%)Detailed, itemized against every core/stretch criterion, includes an honest "not verified" section (animation/focus-return caveat) rather than overclaiming. Strong. Stretch goals — 95 / 100 (15%)Tier 1: ✅ freeze/unfreeze without reload, ✅ amber progress bar (integer-compared, tested), ✅ category lock, ✅ Luhn/transition unit tests, ✅ written empty/error states — all five, visible in diff. Breakdown: Core (90 × 0.35) + Rules (90 × 0.20) + Context (70 × 0.10) + Quality (85 × 0.15) + PR (90 × 0.05) + Stretch (95 × 0.15) = 88 / 100 One thing to do differently next time: Include the spec file itself in the diff (or at minimum quote its key sections in the PR body) — the planning claim is entirely unverifiable as submitted, which is the single biggest gap between "stated" and "shown."
Powered by Anthropic and Tenex |
NWP-201: Issue virtual cards from the console
Spec:
docs/specs/NWP-201-issue-cards.md, written before any code. One commit per step of the spec.What was built
src/lib/cardNumber.tsgenerates numbers on the4242BIN with a Luhn check digit.src/data/cardRules.tsholds the allowlists, validation, state machine, masking, and spend threshold.src/data/cards.tsholds the store operations.src/app/api/cardsandsrc/app/api/cards/[id]are the routes./cardslist, the issue drawer, and/cards/[id]detail. Cards is also in the sidebar and breadcrumbs.Core criteria
•••• 1234, limit, status, and created date.4242with a valid Luhn digit.Cardhas no field for it, and client state is cleared when the drawer closes."250.00"), and a currency outside USD/EUR/GBP all return 400 with per-field messages.Stretch goals
router.refresh())cardNumber.test.ts,cardRules.test.ts, andcards.test.ts, with Luhn, every transition, validation, idempotency, and no full number storedIdempotency-Keyheader is required, and the server maps key → card. A replay returns 409 with the original id. Submit is also disabled while in flight.mch_05).spentis 0 at issue and stays 0, because nothing links payments to cards. The detail page says so.card.historywith a UTC timestamp and shown on the detail page.Bugs fixed along the way
src/data/queries.tssortPayments: amounts were compared withString(a.amount).localeCompare(...), so 900 sorted after 25000. It now compares the integer minor units numerically. Covered bysrc/data/queries.test.ts.src/components/Drawer.tsx: the close button had no accessible name. It now hasaria-label="Close".How it was verified
npm test: 59/59 pass.tsc --noEmitandnext lintare clean.4242number"250.00",XYZ, and GBP on an EUR merchant each return 400bogusreturns 400•••• 2560and the full number is gone from the DOM_rscfetch with no document reloadanimationendnever fired there). Escape sets the Radix state to closed. This is the stock Tremor/Radix component, but I did not confirm focus return on close in a real browser.Out of scope
🤖 Generated with Claude Code