NWP-201: issue virtual cards from the console - #138
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Issue form with one-time number reveal, /cards list and detail, server-side 4242+Luhn generation, allowlist validation, and a server-guarded status state machine. Stretch: freeze/unfreeze without reload, spend progress bar (amber past 80%), category lock, history timeline, unit tests on the Luhn generator and transitions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude Code 101 — Repo Rescue🏆 Build Battle Score: 93 / 100One-line verdict: A tight, well-planned submission that nails every core criterion and correctness rule with real server-side guards, loses a little ground only on the harder Tier 2 stretch items (currency-merchant matching, idempotency, honest seeded spend). Core criteria — 97 / 100 (35%)
Correctness rules — 100 / 100 (20%)
Context and planning — 95 / 100 (10%)
Code quality — 90 / 100 (15%)Tests sit beside the code they cover and exercise real logic (Luhn, generator distribution, all state-machine edges) — these would fail without the change. Conventions followed ( PR description — 100 / 100 (5%)Thorough and honest: every criterion checked off with a verification method, stretch goals itemized, and — notably — it reports pre-existing bugs it found but chose not to fix (correctly declining to claim credit rather than claiming a fix it didn't make). Stretch goals — 75 / 100 (15%)Tier 1: ✅ Freeze/unfreeze without reload · ✅ amber spend bar · ✅ category lock at issue + displayed · ✅ Luhn/state-machine tests · ✅ written empty/error states — all five, full 0.50. Breakdown: Core (97 × 0.35) + Rules (100 × 0.20) + Context (95 × 0.10) + Quality (90 × 0.15) + PR (100 × 0.05) + Stretch (75 × 0.15) = 93 / 100 One thing to do differently next time: Enforce currency-matches-merchant server-side (the form already knows the merchant's currency — the route just needs to check it), which would have been the cheapest Tier 2 point on the board. Powered by Anthropic and Tenex |
Ticket
Closes NWP-201
What changed
Ops can now issue a virtual card from the console instead of messaging the platform team: a drawer on the new
/cardspage takes a nickname, merchant, spend limit, and currency (defaulting to the merchant's settlement currency, since a wrong-currency request is how this ticket started), and the created card appears in the list immediately. The full card number — generated server-side on the4242test BIN with a valid Luhn check digit — is shown exactly once on the success screen; everywhere else the card is•••• 1234. Cards can be opened to a detail page showing spend against the limit, and frozen/unfrozen straight from the list. Status changes go through a server-guarded state machine wherecancelledis terminal.How I verified it
npm testpasses —Test Files 4 passed (4), Tests 40 passed (40), including 12 new tests insrc/lib/cards.test.ts(Luhn check digit against the textbook vector, 200 generated numbers all matching^4242\d{12}$and Luhn-valid, every legal and illegal state-machine edge, cancelled terminal for all three actions)npx tsc --noEmitandnext lintboth cleanJPY, unknown category — all 400 with{ error }; valid POST → 201 with a Luhn-valid4242…number;cancelthenfreezeon the same card → 409;unfreezeon an active card → 409; unknown action → 400; unknown id → 404windowflag that no full page load happened. The seeded 91%-spent card renders the amber progress bar witharia-valuenow="91".curl /cards | grep -E '[0-9]{16}'finds nothing.Acceptance criteria
Core:
/cards; the new row is behind the drawer (masked) before the success screen closes/cardsshows nickname, merchant, masked number, spend limit, status badge, created date/cards/[id]shows the full record, spend against limit, remaining, and a status history timelinesrc/lib/cards.ts),4242BIN + Luhn check digitlast4plus an opaque reference, and the drawer clears the number from client state on closeStretch:
useTransition+router.refresh())npm testpassingBugs fixed along the way
None fixed — this branch touches no existing behavior beyond the shared
StatusBadgeunion. Pre-existing issues found while running/ship-readyare listed below rather than fixed here, since each belongs to other work.Notes for the reviewer
/api/cards. Both pages are server components reading the store directly, same as the payments pages — a list route would duplicate what the pages already have.globalThis, so a server that predates thecardsslice will 500 until restarted./ship-readyreports zero issues in this change. It also surfaced pre-existing findings left untouched: local-time bucketing insrc/data/metrics.ts:25, major-unit accumulation inmetrics.ts/analytics.ts/overview/page.tsx, a lexicographic amount sort insrc/data/queries.ts:81, and unlabeled search inputs inAppSidebar.tsxandfilter-bar.tsx.🤖 Generated with Claude Code