Bulk Invoices — Vendor
HTTP surface for a vendor to batch-generate invoices for many of their own sub-orders at once, merged into a single downloadable PDF, with async status polling and retry of failed orders.
HTTP surface for a vendor to select many of their own sub-orders and request one merged invoice PDF. A BullMQ worker generates (or reuses the cached) invoice for each selected sub-order, then concatenates every successfully-generated PDF into a single file. A batch finishes ready as soon as at least one sub-order succeeded — it only finishes failed when every one did. Failed sub-orders can be retried without re-selecting the whole batch.
Source:
api-modules/invoice/src/controllers/vendor-bulk-invoices.controller.ts.Scoped to the active vendor (
resolveActiveVendorId). A batch id belonging to another vendor returns 404. A requested sub-order that belongs to another vendor is recorded as a failed line item in the batch, not a blanket 404 for the whole request.
Conventions
Authentication
All endpoints require a Better-Auth vendor session. Every read/write is scoped to the caller's vendor id.
Response shape
- Create / retry / status endpoints return the standard JSON envelope:
{ data, message, statusCode }. - List returns the standard paginated envelope:
{ data, message, statusCode, metadata: { total, limit, offset, hasMore } }. - Download (
GET …/:id/download) streams rawapplication/pdfbytes withContent-Disposition: attachment— no JSON envelope.
Generation flow
POST /vendor/bulk-invoices { orderVendorIds: [...] }
│
▼
bulk_invoice_job created (status: pending) — one item per requested order
│
▼
background worker generates each order's invoice, then merges the
successful PDFs into one file
│
├─ at least one order succeeded ──► status: "ready" (fileName/fileSize set)
└─ every order failed ──► status: "failed"
GET /vendor/bulk-invoices — poll the batch list (or a single id) for status
GET /vendor/bulk-invoices/:id/download — download the merged PDF once ready
POST /vendor/bulk-invoices/:id/retry — re-run only the failed orders in the backgroundEndpoints
Start a batch
POST /vendor/bulk-invoices
Request body:
{
"orderVendorIds": ["…", "…"], // 1–200 sub-order ids
"regenerate": false // optional, defaults to false
}| Field | Effect |
|---|---|
regenerate: false (default) | A sub-order whose invoice PDF is already cached is reused as-is. Only sub-orders without a PDF are rendered. |
regenerate: true | Every selected sub-order is re-rendered from current data. |
On a regenerate, each invoice keeps its existing invoice number and its issuance-time seller snapshot (a tax invoice's number and issuing entity are immutable once issued). Everything else is rebuilt from current data — tax rows, store branding, invoice presentation config — and the stored PDF is overwritten. Same semantics as POST /vendor/orders/:orderId/invoice with { "regenerate": true }, applied across the batch.
The flag is persisted on the batch, so POST …/:id/retry re-renders on the same terms as the original pass.
An id that doesn't belong to the caller's vendor (but does exist) is recorded as a failed line item, not rejected outright. An id that doesn't exist at all fails the whole request with 400 (the vendor-admin UI only ever sends ids off the caller's own orders list, so this indicates a malformed request).
Response (data field) — a batch row:
{
"id": "…",
"status": "pending" | "processing" | "ready" | "failed",
"orderCount": 20,
"successCount": 0,
"failedCount": 0,
"fileName": null,
"fileSize": null,
"retryCount": 0,
"regenerate": false, // the batch re-rendered already-cached invoices
"error": null,
"createdAt": "…",
"startedAt": null,
"completedAt": null,
"downloadUrl": null, // set only when status = ready
"requestedBy": { "id": "…", "name": "…" } // null once that user is deleted
}List my batches
GET /vendor/bulk-invoices
Standard offset/limit pagination (querySchema) plus an optional status filter. Rows are the same shape as the create response, newest first.
Batch status + per-order breakdown
GET /vendor/bulk-invoices/:id
Same row shape as above, plus an items array — one entry per requested sub-order:
{
"...": "batch row fields",
"items": [
{
"orderVendorId": "…",
"orderId": "…" | null,
"orderNumber": "ORD-1042" | null,
"status": "pending" | "success" | "failed",
"error": "…" | null
}
]
}Retry failed orders
POST /vendor/bulk-invoices/:id/retry
Allowed only when the batch is ready or failed and has at least one failed item — returns 409 otherwise (including while a batch is still processing). Resets every failed item back to pending and re-runs them in the background; the merged PDF is rebuilt from the full current set of successful invoices (previously-succeeded ones are not re-rendered), so a partially-fixed retry still produces one complete file.
Response: the updated batch row (status: "processing", retryCount incremented).
Download the merged PDF
GET /vendor/bulk-invoices/:id/download
Streams the merged application/pdf once the batch is ready. Returns 404 otherwise (not yet ready, or the batch failed outright).
Filename: bulk-invoices-{id}.pdf (or the batch's stored fileName).
Analytics Module — Vendor surface
Vendor-facing HTTP surface for precomputed dashboard snapshots (sales, earnings, fulfilment, inventory KPIs for today / 7d / 30d / all-time) and a live SSE paid-order feed.
Cart Module — Vendor surface
Vendor-facing HTTP endpoints for browsing carts that contain the vendor's own products and for vendor-scoped cart funnel analytics. There are no write operations: a vendor cannot…