docs(docs): add system design overview and e2e test command to README
This commit is contained in:
@@ -13,6 +13,7 @@ Other commands:
|
|||||||
```sh
|
```sh
|
||||||
bun run start # no watch
|
bun run start # no watch
|
||||||
bun run db:reset # wipe data.db and reseed
|
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
|
bunx tsc --noEmit # typecheck
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -28,12 +29,102 @@ 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.
|
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
|
## Routes
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl localhost:3000/health
|
curl localhost:3000/health
|
||||||
curl localhost:3000/customers
|
curl localhost:3000/customers
|
||||||
curl localhost:3000/customers/<cus_id>/invoices
|
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 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/ingest --data-binary @invoices.csv
|
||||||
curl -X POST localhost:3000/payment/INV-1011 \
|
curl -X POST localhost:3000/payment/INV-1011 \
|
||||||
@@ -43,9 +134,10 @@ curl -X POST localhost:3000/payment/INV-1011 \
|
|||||||
|
|
||||||
- `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.
|
- `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).
|
- `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).
|
- `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`.
|
Money is stored as integer minor units (cents): CSV `150` → `15000`. Full API contract: `openapi.yaml` (OpenAPI 3.1).
|
||||||
|
|
||||||
## Seed data
|
## Seed data
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user