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.

Countshelf counts per cafe, reference only
Deliverone menu for every cafe, live totals, special orders
Invoiceher Canva form: Print, PDF, Email, Edit, Void
Ledgerwho owes what, overdue, paid, void
Customersthe menu once, then each cafe's details

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.

Create invoice, from tap to printed form The Deliver form in the browser sends the delivery to the invoices API on the server. The API checks it against the menu and saves an invoice snapshot in Postgres. The browser then opens the Invoice page, which reads the snapshot and draws Eileen's Canva form. BROWSER SERVER (Vercel) STORAGE (Neon) 1 · Deliver formDeliveryForm save() 2 · POST /api/invoicesparseDelivery → createInvoice invoices + invoice_linesthe saved snapshot Her Canva invoicewhat Eileen sees 3 · Invoice pageInvoicePage → InvoiceDocument reads it back

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 orderseed/mock-data.ts (mockProducts); real data at Handoff. Read by lib/queries/products.ts (listProducts)
The invoice layout or fontscomponents/invoice/InvoiceDocument.tsx and its .module.css
Lines that fit on one pagelib/invoice-capacity.ts (MAX_LINES_ONE_PAGE)
The suggested invoice numberlib/invoice-number.ts (suggestNumber)
The email's subject and bodylib/email/invoice-email.ts (buildInvoiceEmail); the sender comes from EMAIL_FROM in env, never code
Ledger stats or overdue ruleslib/ledger-stats.ts (ledgerStats, rowState)
Button wording on the Invoice pagecomponents/invoice/InvoiceActions.tsx
The "?" How it works pageapp/(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.