Document Print Module
Backend module for queuing and dispatching thermal receipt print jobs to the Print Bridge agent.
Purpose
The document-print module provides a REST API that:
- Accepts print job creation requests from the PWA (Firebase-authenticated).
- Fetches and serialises the full document payload from the database.
- Persists the job with
status: pending. - Exposes a poll endpoint and emits events so the Print Bridge agent can pick up and process jobs.
Architecture
The module follows strict Hexagonal Architecture.
document-print/
├── domain/
│ ├── document-print-job-repository.domain.ts ← IDocumentPrintJobRepository (port)
│ └── document-print-payload-port.domain.ts ← IDocumentPrintPayloadPort (port)
├── application/
│ ├── document-print-job.service.ts ← use cases (depends only on ports)
│ └── events/
│ └── document-print-job.events.ts ← domain events
├── infrastructure/
│ ├── document-print-job.repository.ts ← Kysely adapter for jobs table
│ └── document-print-payload.repository.ts ← Kysely adapter for payload assembly
└── interfaces/
├── document-print.controller.ts
└── dtos/
├── create-document-print-job.dto.ts
├── update-document-print-job.dto.ts
└── document-print-job-response.dto.ts
Dependency rule
interfaces → application → domain ← infrastructure
The application service depends only on two domain ports:
IDocumentPrintJobRepository— CRUD ondocument_print_jobtable.IDocumentPrintPayloadPort— assembles typed print payloads from business documents.
The application service never touches KyselyDatabase directly.
Domain concepts
DocumentPrintJob
A queued print request stored in document_print_job. Fields:
| Field | Description |
|---|---|
jobKind | sale_receipt | order_receipt | fel_invoice |
status | pending → sent → printed | failed |
payload | Serialised print data (varies by jobKind) |
sourceType / sourceId | Links job to origin document |
targetPrinterId | Optional — routes to a specific printer |
claimedBy | Printer ID that claimed the job |
attempts | Incremented atomically on each failure |
Job lifecycle
pending →(bridge claims)→ sent →(bridge prints)→ printed
→(bridge fails)→ failed
Claiming is optimistic — the claimJob repository method uses a conditional UPDATE (WHERE status = 'pending') that atomically prevents two bridge instances from claiming the same job.
Payload types
Defined in @flowpos-workspace/global/types/document-print.types:
SaleReceiptPayload— retail sale receiptOrderReceiptPayload— restaurant order receiptFelInvoicePayload extends SaleReceiptPayload— FEL electronic invoice
Authentication model
The module uses two distinct auth modes depending on the caller:
| Caller | Auth | Endpoints |
|---|---|---|
| PWA frontend | Firebase ID token (flowpos-id-token cookie or Authorization: Bearer) | POST /document-print-jobs, GET thermal-availability |
| Print Bridge agent | Device token (validated by DeviceGuard) | GET /document-print-jobs, GET /document-print-jobs/:id, PATCH /document-print-jobs/:id |
Bridge-facing endpoints are marked @IsPublic() (skip Firebase guard) and protected by @UseGuards(DeviceGuard) instead.
API Endpoints
GET /document-print-jobs/thermal-availability
Check if a paired print_bridge device exists for a location.
Auth: Firebase
Query: locationId (required)
Response: { "available": true | false }
POST /document-print-jobs
Enqueue a print job.
Auth: Firebase
Query: locationId, businessId (both required)
Body:
{
"jobKind": "sale_receipt",
"sourceId": "<saleId>",
"targetPrinterId": "printer-usb-001"
}
Responses:
| Code | Reason |
|---|---|
| 201 | Job created { id, status: "pending" } |
| 400 | No printer paired, invalid jobKind, missing params |
| 404 | Source document not found |
| 409 | Job already in-flight for this document |
GET /document-print-jobs
List pending jobs for a location (bridge poll fallback).
Auth: Device token
Query: locationId (required), jobKind (optional, repeatable)
Response: Array of DocumentPrintJob
GET /document-print-jobs/:id
Get a single job by ID.
Auth: Device token
Response: DocumentPrintJob or 404
PATCH /document-print-jobs/:id
Update job status. Used by the bridge to claim and report outcome.
Auth: Device token
Body:
// Claim (pending → sent):
{ "status": "sent", "printerId": "printer-usb-001" }
// Report success:
{ "status": "printed" }
// Report failure:
{ "status": "failed", "error": "USB timeout after 5s" }
Responses:
| Code | Body |
|---|---|
| 200 | { id, status } |
| 200 | { id, status: "already_claimed" } when optimistic claim loses |
| 400 | Invalid status |
| 404 | Job not found |
Events
The service emits domain events via EventEmitter2:
| Event name | Class | Payload |
|---|---|---|
document_print_job.new | OnDocumentPrintJobNewEvent | full job + businessId, locationId, jobKind, targetPrinterId |
document_print_job.updated | OnDocumentPrintJobUpdatedEvent | full job + businessId, locationId, new status |
Other modules (e.g., WebSocket gateway) listen to document_print_job.new to push jobs to the bridge in real time, reducing latency vs. polling.
Important design decisions
Why IDocumentPrintPayloadPort is a separate port
Payload assembly requires joining multiple tables (sale, business, location, orderItem, product, diningTable, employee, felCertifierConfig). This complexity belongs in infrastructure, not the application service. The port keeps the service free of Kysely and framework imports.
Optimistic job claiming
claimJob uses a conditional UPDATE (WHERE status = 'pending') rather than a SELECT-then-UPDATE. This prevents race conditions when multiple bridge instances poll simultaneously. The service treats a null return as "already claimed" and responds with { status: "already_claimed" }.
Atomic attempt counter
updateStatus on failed uses sql\attempts + 1`` — a single atomic SQL expression — instead of a read-modify-write. This ensures correctness under retries.
FEL invoice re-uses sale receipt payload
buildFelInvoicePayload calls buildSaleReceiptPayload internally and extends it with FEL-specific fields (felCertifierName, felCertifierNit, felDteNumber, felEmissionDate). If the sale lacks FEL authorization data, it throws 400 before creating the job.
Related documentation
- Print Bridge spec — bridge agent architecture and requirements
- Print Bridge deployment — hardware setup and Docker configuration