Files

158 lines
5.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<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 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)