# 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) ```sh bun run dev # http://localhost:3000, hot reload ``` Other commands: ```sh 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 ```mermaid 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 ```mermaid 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 ```mermaid 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 ```sh curl localhost:3000/health curl localhost:3000/customers curl localhost:3000/customers//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 status `corrupted`. 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 a `corrupted` invoice = supply the missing fields + new `status` in one PUT (schema CHECK enforces completeness). - `POST /payment/:invoice_id`: zod-validated `{idempotency_key: uuid, amount, currency}`; only `open`/`partially_paid` invoices are payable (409 otherwise — includes `corrupted`), repeated key replays the original payment (200), else inserts a `pending` payment (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 code - `INV-1010`: negative quantity and amount - `INV-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 EXISTS` to schema.sql, restart (or `db:reset` if changing existing tables)