Cuenta · App map
Where things live
A take-home guide to the finished proof of concept: what the main pieces are, one action followed through the code, where to start a change, and one practice worth reusing.
What the app is
Cuenta turns Eileen's paper invoice into a phone-and-laptop app for La Condesa Baking Company. She counts what's left on a cafe's shelf, builds a delivery from her menu, gets her own Canva invoice back (print, PDF or email), and tracks who has paid in the Ledger. It's one Next.js app on Vercel, with a Neon Postgres database and Resend for email, behind a six-digit PIN.
One action, end to end: Create invoice
On Deliver, Eileen picks a cafe, types quantities and taps Create invoice. A moment later she's looking at her invoice. This is the path it takes.
Reference route (three stops)
1 · The form gathers the delivery
components/deliver/DeliveryForm.tsx, search async function save; line state in components/deliver/useDeliveryLines.ts, search payload: ()
The menu rows and special orders live in useDeliveryLines, which turns what she typed into cents and totals. save() sends the cafe, date, number and payload() lines to the server, then opens the new invoice. A taken number comes back as a 409 and shows under Invoice #.
2 · The server checks it and saves a snapshot
app/api/invoices/route.ts (export const POST) → lib/invoice-input.ts (parseDelivery) → lib/queries/invoices.ts (createInvoice)
parseDelivery accepts only products from listProducts() (the one menu) plus complete special orders, and drops blank lines. createInvoice copies the cafe's details and each line's name and price onto the invoice in one transaction. That's why later price changes never rewrite an old invoice.
3 · The Invoice page draws her form
app/(app)/invoices/[id]/page.tsx (InvoicePage) → components/invoice/InvoiceDocument.tsx (InvoiceDocument)
The page reads the snapshot back and hands it to InvoiceDocument, which places every piece in PDF points on a Letter page. The same component draws the screen, the printout and the emailed PDF, so the three always match.
How it's checked
tests/unit/invoice-input.test.ts: blank lines dropped, products off the menu refused, special order rules.tests/e2e/judge-path.spec.ts: Count → Deliver → Invoice (Print, Email) → Ledger on a running app, under three minutes.
To change this, start here
| To change… | Start in |
|---|---|
| The menu: products, prices, their order | seed/mock-data.ts (mockProducts); real data at Handoff. Read by lib/queries/products.ts (listProducts) |
| The invoice layout or fonts | components/invoice/InvoiceDocument.tsx and its .module.css |
| Lines that fit on one page | lib/invoice-capacity.ts (MAX_LINES_ONE_PAGE) |
| The suggested invoice number | lib/invoice-number.ts (suggestNumber) |
| The email's subject and body | lib/email/invoice-email.ts (buildInvoiceEmail); the sender comes from EMAIL_FROM in env, never code |
| Ledger stats or overdue rules | lib/ledger-stats.ts (ledgerStats, rowState) |
| Button wording on the Invoice page | components/invoice/InvoiceActions.tsx |
| The "?" How it works page | app/(app)/help/page.tsx (SECTIONS) |
A practice to reuse
Write sharing and "how many" as a check you can see, not just a noun.
The plan's first kernel sentence said Eileen "builds a delivery from each cafe's standing list". It was in scope.md from its first commit, then copied into the PRD, the spec, a standing_items table, and every screen and API that lists products. Every check passed, because every check tested the plan: a list per cafe. No check said what Eileen actually does: one menu, the same products and prices for every cafe. A hands-on timed run caught it, and it took a migration to fix.
The PRD now has the check that would have caught it on day one: "Picking any cafe shows every menu product…" (prd.md > Count) and "Every cafe sees the same products and prices" (prd.md > Deliver). The same run caught a second gap of the same kind: prd.md > States and Boundaries had no "working" state, so a five-second send looked frozen.
Where it came from: the per-cafe lists were inferred in a pre-planning brief from sample invoices, where one cafe's invoice had cookies and the others didn't. But an invoice shows what was ordered, not what's available. The guess entered scope as if it were already decided, so the scope interview, which checks the answers it asks for, never asked about it.
On the next project: anything brought into scope from your own notes or inferred from documents gets confirmed with the real user before it goes in the kernel. For each thing the core sentence names (a list, a price, a customer), add one observable check that states whether it's shared or per-something and how many there are. Add a "slow action shows what it's doing" line to States and Boundaries. Then a wrong assumption fails a check during planning instead of surfacing after the build.
Run it again
cp .env.example .env.local # fill in the values npm install npm run db:migrate npm run db:seed # mock data only; wipes the database it points at npm run dev # http://localhost:3000 npm test # unit tests npm run test:e2e # judge path; E2E_BASE_URL, E2E_PIN or E2E_MANUAL_PIN=1
Snapshot of the code at commit 0158483, the last code change before this map. It's a snapshot and will drift as the app changes. Open this file in any browser; it needs no network.