monk-code-screen
Billing/invoicing/payments backend skeleton. Bun + Hono + bun:sqlite (zero network deps at runtime; TS runs natively, no build step).
⚡ START COMMAND (sticky)
bun run dev # http://localhost:3000, hot reload
Other commands:
bun run start # no watch
bun run db:reset # wipe data.db and reseed
bun run test # e2e API tests (boots a throwaway server + DB, pretty output)
bunx tsc --noEmit # typecheck
Layout
src/index.ts Hono app + routes
src/ingest.ts CSV parser/normalizer + transactional insert (shared by seed and POST /ingest)
src/db.ts opens data.db, applies schema, seeds from invoices.csv if empty (runs on import)
src/schema.sql customers / invoices / payments (idempotent)
invoices.csv seed data — deliberately messy; DO NOT MODIFY
DB is data.db at repo root (WAL, FK on). Boot always re-applies schema, so adding a table = edit schema.sql + restart.
System design (from the tldraw canvas)
Requirements: POST submit invoices · GET return invoices with their status · POST pay open invoices.
Data model
erDiagram
CUSTOMER ||--o{ INVOICE : has
INVOICE ||--o{ PAYMENT : "paid by"
CUSTOMER {
text id PK
text customer_name
text customer_email UK
text last_updated "Index"
}
INVOICE {
text id PK
text customer_id FK "Index"
text status "Status enum"
text created_at "Index"
text due_on "Index"
int quantity
int unit_price "cents"
int amount "cents"
text currency
}
PAYMENT {
text id PK
text idempotency_key UK "Index"
text invoice_id FK "Index"
text status "Index; owned by external PSP"
text last_updated "Index"
text completed_at "Index"
bigint amount "cents"
text currency
}
Invoice status
stateDiagram-v2
state "Partially Paid" as PartiallyPaid
[*] --> Open : ingest (valid row)
[*] --> Corrupted : ingest (invalid row)
Open --> Paid
Open --> PartiallyPaid
Open --> Void
Corrupted --> Open : PUT repair (supply missing fields)
Canvas states are Open → Paid, Partially Paid, Void; Corrupted and its repair edge are our implementation extension for rows that fail ingest validation.
API surface
flowchart LR
client([Client])
psp[External PSP]
subgraph api[REST API]
ingest["POST /ingest"]
getInv["GET /invoice/:id"]
putInv["PUT /invoice/:id"]
pay["POST /payment/:invoice_id"]
end
subgraph db[data.db]
customers[(customers)]
invoices[(invoices)]
payments[(payments)]
end
client -->|CSV as request body| ingest
ingest -->|"normalize rows; negative values / empty currency → status corrupted"| invoices
ingest -->|dedupe by email| customers
ingest -->|"status + ingested count + error count"| client
client --> getInv
getInv -->|"invoice with status + payment statuses"| client
client -->|"non-unique fields"| putInv
putInv -->|"updated invoice JSON"| client
client -->|"idempotency_key (client-side UUID), amount, currency"| pay
pay -->|"pending payment (only open / partially paid invoices)"| payments
psp -.->|owns payment status| payments
Routes
curl localhost:3000/health
curl localhost:3000/customers
curl localhost:3000/customers/<cus_id>/invoices
curl -X PUT localhost:3000/invoice/INV-1011 \
-H 'content-type: application/json' -d '{"description":"amended","quantity":2}'
curl localhost:3000/invoice/INV-1011 # by invoice_number or surrogate inv_ id
curl -X POST localhost:3000/ingest --data-binary @invoices.csv
curl -X POST localhost:3000/payment/INV-1011 \
-H 'content-type: application/json' \
-d "{\"idempotency_key\":\"$(uuidgen)\",\"amount\":1500000,\"currency\":\"USD\"}"
POST /ingest: CSV as raw request body. Rows failing validation (negative values, empty currency, missing/unparseable amount or dates) are still stored, with statuscorrupted. Returns{ status, ingested, errors, rows }. No idempotency — re-posting the same file appends duplicates.GET /invoice/:id: invoice (with status) + its payments (with PSP-owned status). Resolves surrogate id or invoice_number (newest wins on duplicate numbers).PUT /invoice/:id: partial update of non-identity fields (status,due_on,description,quantity,unit_price,amount,currency); immutable/unknown fields → 400. Returns the updated invoice. Repairing acorruptedinvoice = supply the missing fields + newstatusin one PUT (schema CHECK enforces completeness).POST /payment/:invoice_id: zod-validated{idempotency_key: uuid, amount, currency}; onlyopen/partially_paidinvoices are payable (409 otherwise — includescorrupted), repeated key replays the original payment (200), else inserts apendingpayment (201).
Money is stored as integer minor units (cents): CSV 150 → 15000. Full API contract: openapi.yaml (OpenAPI 3.1).
Seed data
Seeded by ingesting invoices.csv (25 rows → 22 open + 3 corrupted, 21 customers):
INV-1006(line 7): empty currency codeINV-1010: negative quantity and amountINV-1020: missing amount
Duplicate invoice numbers (INV-1001 ×2, INV-1006 reused) are ingested as-is — not a validation rule on the canvas. Customers dedupe by email case-insensitively (Acme Corp/ACME CORPORATION/acme corp → one customer); rows without email get a customer keyed by name.
Extension cheatsheet (interview)
- New route: add to
src/index.ts,db.query(...).all()/get(),db.run(sql, [params]) - Multi-write:
db.transaction(() => { ... })() - Validation:
z.object({...}).safeParse(await c.req.json()) - New table: append
CREATE TABLE IF NOT EXISTSto schema.sql, restart (ordb:resetif changing existing tables)