Files

421 lines
13 KiB
YAML
Raw Permalink Normal View History

openapi: 3.1.0
info:
title: monk-code-screen billing API
version: 1.0.0
description: |
Billing/invoicing/payments backend. Invoices are ingested from CSV; rows failing
validation (negative values, empty currency, missing/unparseable amount or dates)
are stored with status `corrupted` rather than rejected.
Money fields (`unit_price`, `amount`) are integer minor units (cents): CSV `150` → `15000`.
servers:
- url: http://localhost:3000
# Explicitly unauthenticated: local interview skeleton, no auth layer
security: []
paths:
/health:
get:
summary: Liveness check
operationId: getHealth
responses:
"200":
description: Service is up
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
const: true
required: [ok]
/customers:
get:
summary: List customers
description: Ordered by `last_updated`. Customers are deduplicated by email (case-insensitive); rows without an email are keyed by name.
operationId: listCustomers
responses:
"200":
description: All customers
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Customer"
/customers/{id}/invoices:
get:
summary: List a customer's invoices
description: Newest first. Unknown customer ids return an empty list.
operationId: listCustomerInvoices
parameters:
- name: id
in: path
required: true
schema:
type: string
example: cus_1a2b3c4d
responses:
"200":
description: The customer's invoices (empty array when the customer is unknown)
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Invoice"
/ingest:
post:
summary: Ingest invoices from CSV
description: |
CSV attached as the raw request body. Header must be exactly:
`invoice_number,customer_name,customer_email,invoice_date,due_date,description,quantity,unit_price,amount,currency`
Messy-but-parseable values are normalized (`1/12/26`, `Jan 16 2026`, `"$12,000.00"`, `"€1,850.00"`).
Rows failing validation are still inserted with status `corrupted`.
Not idempotent: re-posting the same file appends duplicate invoices (customers are deduplicated).
operationId: ingestCsv
requestBody:
required: true
content:
text/csv:
schema:
type: string
example: |
invoice_number,customer_name,customer_email,invoice_date,due_date,description,quantity,unit_price,amount,currency
INV-1001,Acme Corp,billing@acme.com,2026-01-05,2026-02-04,Consulting services,10,150,1500,USD
responses:
"200":
description: Ingest summary with per-row results
content:
application/json:
schema:
$ref: "#/components/schemas/IngestResult"
"400":
description: Empty body or unexpected CSV header
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/invoice/{id}:
parameters:
- name: id
in: path
required: true
description: Surrogate id (`inv_...`) or invoice number (`INV-1001`). Duplicate invoice numbers resolve to the newest invoice.
schema:
type: string
example: INV-1001
get:
summary: Get an invoice with its payments
operationId: getInvoice
responses:
"200":
description: The invoice (with status) and its payments (status owned by the external PSP)
content:
application/json:
schema:
type: object
properties:
invoice:
$ref: "#/components/schemas/Invoice"
payments:
type: array
items:
$ref: "#/components/schemas/Payment"
required: [invoice, payments]
"404":
description: Invoice not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
put:
summary: Update an invoice
description: |
Partial update of non-identity fields. `id`, `invoice_number`, `customer_id`, and
`created_at` are immutable and rejected. At least one field is required.
A `corrupted` invoice can be repaired by supplying the missing fields together with a
new `status` in the same request; moving off `corrupted` while money/date fields are
still missing fails the database CHECK and returns 400.
operationId: updateInvoice
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/InvoiceUpdate"
responses:
"200":
description: The updated invoice
content:
application/json:
schema:
$ref: "#/components/schemas/Invoice"
"400":
description: Validation error, unknown/immutable field, or integrity CHECK failure
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: Invoice not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/payment/{invoice_id}:
post:
summary: Pay an invoice
description: |
Only `open` and `partially_paid` invoices are payable. Payments start as `pending`;
status transitions are owned by the external PSP. `idempotency_key` is generated
client-side: repeating a key replays the original payment instead of double-charging.
operationId: createPayment
parameters:
- name: invoice_id
in: path
required: true
description: Surrogate id (`inv_...`) or invoice number (`INV-1001`).
schema:
type: string
example: INV-1011
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PaymentCreate"
responses:
"201":
description: Payment created (status `pending`)
content:
application/json:
schema:
$ref: "#/components/schemas/PaymentView"
"200":
description: Idempotent replay — the original payment for this key
content:
application/json:
schema:
$ref: "#/components/schemas/PaymentView"
"400":
description: Validation error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: Invoice not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Invoice is not payable (paid, void, or corrupted)
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
schemas:
Customer:
type: object
properties:
id:
type: string
example: cus_1a2b3c4d
customer_name:
type: string
example: Acme Corp
customer_email:
type: [string, "null"]
description: Unique when present; null for rows ingested without an email.
example: billing@acme.com
last_updated:
type: string
example: "2026-08-10 14:33:55"
required: [id, customer_name, customer_email, last_updated]
Invoice:
type: object
description: Money fields are integer minor units (cents). Nullable fields are only null on `corrupted` invoices.
properties:
id:
type: string
example: inv_f726dd40
invoice_number:
type: string
description: Not unique — duplicate numbers in source data are ingested as-is.
example: INV-1011
customer_id:
type: [string, "null"]
example: cus_1a2b3c4d
status:
$ref: "#/components/schemas/InvoiceStatus"
created_at:
type: string
example: "2026-01-22"
due_on:
type: [string, "null"]
example: "2026-02-21"
description:
type: [string, "null"]
example: Security audit
quantity:
type: [integer, "null"]
example: 1
unit_price:
type: [integer, "null"]
description: Minor units (cents).
example: 1500000
amount:
type: [integer, "null"]
description: Minor units (cents).
example: 1500000
currency:
type: [string, "null"]
example: USD
required:
[id, invoice_number, customer_id, status, created_at, due_on, description, quantity, unit_price, amount, currency]
InvoiceStatus:
type: string
enum: [open, partially_paid, paid, void, corrupted]
InvoiceUpdate:
type: object
description: All fields optional, at least one required. Unknown or immutable fields are rejected.
additionalProperties: false
minProperties: 1
properties:
status:
$ref: "#/components/schemas/InvoiceStatus"
due_on:
type: string
format: date
example: "2026-03-01"
description:
type: [string, "null"]
quantity:
type: integer
minimum: 1
unit_price:
type: integer
minimum: 0
description: Minor units (cents).
amount:
type: integer
minimum: 0
description: Minor units (cents).
currency:
type: string
minLength: 1
description: Normalized to uppercase.
example: USD
Payment:
type: object
properties:
id:
type: string
example: pay_4696225e
idempotency_key:
type: string
format: uuid
invoice_id:
type: string
example: inv_f726dd40
status:
type: string
description: Owned by the external PSP; starts as `pending`.
example: pending
last_updated:
type: string
example: "2026-08-10 14:34:11"
completed_at:
type: [string, "null"]
amount:
type: integer
description: Minor units (cents).
example: 1500000
currency:
type: string
example: USD
required: [id, idempotency_key, invoice_id, status, last_updated, completed_at, amount, currency]
PaymentCreate:
type: object
properties:
idempotency_key:
type: string
format: uuid
description: Generated client-side to prevent duplicate charges.
amount:
type: integer
minimum: 1
description: Minor units (cents).
currency:
type: string
minLength: 1
required: [idempotency_key, amount, currency]
PaymentView:
type: object
properties:
payment:
$ref: "#/components/schemas/Payment"
required: [payment]
IngestResult:
type: object
properties:
status:
type: string
const: ok
ingested:
type: integer
description: Total rows inserted, including corrupted ones.
errors:
type: integer
description: Count of rows stored with status `corrupted`.
rows:
type: array
items:
type: object
properties:
line:
type: integer
description: 1-based CSV line number (header is line 1).
id:
type: string
invoice_number:
type: string
status:
type: string
enum: [open, corrupted]
errors:
type: array
items:
type: string
required: [line, id, invoice_number, status, errors]
required: [status, ingested, errors, rows]
Error:
type: object
properties:
error:
description: Message string, or a zod error tree for validation failures.
oneOf:
- type: string
- type: object
required: [error]