Invoice Module — Storefront
HTTP surface for a customer to check, generate, stream, and download the GST tax invoice(s) for their own order — async generation with SSE readiness notification.
HTTP surface for customers to access the invoice for an order they placed. Because invoices are issued per vendor, a multi-vendor order has one invoice per seller; the order-level endpoints cover all vendors merged. Generation is lazy and async: the first POST …/generate call enqueues a background render job; an SSE stream reports when it is ready; repeat downloads stream the cached PDF instantly.
Source:
api-modules/invoice/src/controllers/store-invoices.controller.ts.Scoped to the caller's own orders. A non-owned or unknown order id returns 404 (never 403), so other customers' orders can't be probed for existence. An order still in
pending_paymentreturnsstatus: "none"— invoices are issued once the order is confirmed.
Conventions
Authentication
All endpoints require a Better-Auth customer session. The order must belong to the caller. The SSE stream (/stream) is authenticated via the session cookie, which the browser EventSource sends automatically.
Response shape
- Status / generate endpoints return the standard JSON envelope:
{ data, message, statusCode }. - Download (
GET …/invoice) streams rawapplication/pdfbytes withContent-Disposition: attachment— no JSON envelope. - SSE stream (
GET …/invoice/stream) returnstext/event-stream— no JSON envelope.
Customers cannot regenerate an invoice (no regenerate endpoint at this role).
Generation flow
POST …/invoice/generate
│
├─ status: "ready" ──► GET …/invoice (streams cached PDF)
│
└─ status: "pending" ──► open GET …/invoice/stream
│
├─ { status:"pending", heartbeat:true } (periodic)
├─ { status:"ready", downloadUrl } ──► GET …/invoice
└─ { status:"failed", error }On page load, call GET …/invoice/status to render the current state without triggering a render.
Endpoints
Check invoice status
GET /store/orders/:orderId/invoice/status
GET /store/orders/:orderId/vendors/:orderVendorId/invoice/status
Returns the current status without enqueuing anything. Safe to call on every page load.
Response (data field):
{
"status": "ready" | "pending" | "failed" | "none",
"invoiceNumber": "INV-2526-000001" | null,
"downloadUrl": "…" | null, // set only when ready
"streamUrl": "…" | null, // set only when pending or failed
"jobId": "…" | null
}status: "none" is returned when the parent order is still in pending_payment or no generate has been requested yet.
Enqueue invoice generation
POST /store/orders/:orderId/invoice/generate
POST /store/orders/:orderId/vendors/:orderVendorId/invoice/generate
Enqueues the render job if not already cached. Idempotent — concurrent first-generation requests collapse onto one job.
Always returns HTTP 200. Response (data field):
{
"status": "ready" | "pending",
"jobId": "…",
"downloadUrl": "…" | null, // set when ready
"streamUrl": "…" | null // set when pending
}Download invoice PDF
GET /store/orders/:orderId/invoice
GET /store/orders/:orderId/vendors/:orderVendorId/invoice
Streams the cached application/pdf when the invoice is ready. Returns 409 while still generating — call POST …/generate first, then wait for the SSE ready frame before downloading.
- Order-level: merges every vendor's PDF into one. Filename:
invoice-{orderNumber}.pdf. - Sub-order-level: one vendor's invoice. Filename:
invoice-{serial}.pdf(e.g.invoice-INV-2526-000001.pdf).
Stream readiness (SSE)
GET /store/orders/:orderId/invoice/stream
GET /store/orders/:orderId/vendors/:orderVendorId/invoice/stream
Opens a Server-Sent Events connection (text/event-stream). Each data: frame is JSON:
// Periodic heartbeat while rendering
{ "status": "pending", "heartbeat": true }
// Terminal: ready
{ "status": "ready", "downloadUrl": "…" }
// Terminal: render failed
{ "status": "failed", "error": "…" }The stream closes automatically after ~60 s. If no terminal frame was received, re-check status with GET …/invoice/status and reconnect if still pending.
Guest Checkout Module — Storefront
HTTP surface for guest (no-account) checkout — contact capture for an anonymous cart, a non-blocking "you already have an account" hint, and public order tracking after the order…
Notifications Module — Storefront
HTTP surface for the customer app: register/unregister FCM device tokens, the in-app notification feed (bell/inbox with read/seen state, SSE live stream, images), the per-user notification preference matrix, and the public token-gated marketing-email unsubscribe. The in-app channel mirrors push.