Cola de Impresion y Reintentos
Cuando una impresion falla en el cliente (impresora apagada, sin papel, sin red, navegador sin permiso, etc.), Core360 puede encolar el trabajo en una cola persistente del backend que reintenta automaticamente con backoff exponencial. Un supervisor puede consultar, reintentar manualmente o cancelar trabajos pendientes.
Endpoint API: /api/printjobs
Worker: PrintQueueWorker (BackgroundService de la API).
Cuando se encola un trabajo
La WebApp / app movil intenta primero las estrategias configuradas en la terminal. Si todas fallan, el cliente puede pedirle al backend que persista el trabajo:
POST /api/printjobs
{
"posTerminalId": 5,
"invoiceId": 12345,
"payloadJson": "{...ReceiptPayloadDTO serializado...}",
"strategy": "webbluetooth",
"lastError": "GATT operation failed"
}
El job se crea con status Pending y queda disponible para retry.
Lifecycle de un PrintJob
Pending ──primer intento exitoso──→ Sent
│
└──fallo─→ Retrying (AttemptCount++, NextAttemptAt = now + backoff)
│
├── 5 intentos fallidos ──→ Failed (sin mas reintentos automaticos)
│
└── operador cancela ─────→ Cancelled
| Status | Significado |
|---|---|
Pending | Recien creado, sin intentos aun |
Sent | Impresion exitosa |
Retrying | Falló, esperando proxima ventana de reintento |
Failed | Agotó los 5 intentos automaticos |
Cancelled | Operador decidió descartarlo |
Backoff exponencial
| Intento | Espera tras fallo |
|---|---|
| 1 | 30 segundos |
| 2 | 1 minuto |
| 3 | 5 minutos |
| 4 | 15 minutos |
| 5 | 1 hora |
| 6+ | (status pasa a Failed) |
El campo NextAttemptAt indica cuando el worker considerará el job nuevamente.
El worker
PrintQueueWorker es un BackgroundService que se ejecuta dentro del proceso de la API. Cada 30 segundos:
- Consulta jobs con
Status IN (Failed, Retrying) AND AttemptCount < 5 AND NextAttemptAt <= NOW(). - Marca cada uno como
Retrying. - Emite el evento SignalR
printJobReadyal grupo de la empresa (company-{id}). - Los clientes POS conectados (Blazor o MAUI) reciben el evento y ejecutan la impresion con la estrategia configurada en la terminal.
- Tras imprimir, el cliente llama
POST /api/printjobs/{id}/retry(exito) o el worker volvera a intentar.
El worker tambien notifica a supervisores via SignalR cuando un job pasa a Failed definitivo.
API
GET /api/printjobs/bycompany
Lista jobs de la empresa autenticada, opcionalmente filtrados por status.
Query params:
| Param | Tipo | Default |
|---|---|---|
status | Pending / Sent / Failed / Retrying / Cancelled | (todos) |
take | int | 100 |
Response 200: array de PrintJob.
POST /api/printjobs
Encola un nuevo trabajo. El CompanyId se toma del JWT.
Body:
{
"posTerminalId": 5,
"invoiceId": 12345,
"payloadJson": "{...}",
"strategy": "qztray",
"lastError": "WebSocket disconnected"
}
Response 200: el PrintJob creado con Status = Pending.
POST /api/printjobs/{id}/retry
Reactiva un job para reintento inmediato. Util cuando el operador resolvió el problema (puso papel, conecto la impresora) y quiere reimprimir sin esperar el backoff.
POST /api/printjobs/{id}/cancel
Marca el job como Cancelled. No se intentará mas. Util cuando la factura fue anulada o el cliente ya no requiere recibo.
Modelo de datos
Tabla PrintJobs:
| Columna | Tipo | Notas |
|---|---|---|
Id | int (PK) | Identidad |
CompanyId | int (FK Companies) | Cascade delete |
POSTerminalId | int? | Terminal origen |
InvoiceId | int? | Factura origen (si aplica) |
PayloadJson | text | ReceiptPayloadDTO serializado |
Strategy | string(40)? | Ultima estrategia intentada |
Status | enum (int) | PrintJobStatus |
AttemptCount | int | Numero de intentos |
LastAttemptAt | timestamptz? | Ultimo intento |
NextAttemptAt | timestamptz? | Proximo intento programado |
LastError | string(2000)? | Ultimo error reportado |
CompletedAt | timestamptz? | Cuando paso a Sent o Cancelled |
CreatedAt, UpdatedAt, CreationUser, ModificationUser | (heredado de AuditEntity) |
Indices:
| Nombre | Columnas | Uso |
|---|---|---|
IX_PrintJobs_Company_Status_Created | (CompanyId, Status, CreatedAt) | Listado por empresa filtrado por status |
IX_PrintJobs_Status_NextAttempt | (Status, NextAttemptAt) | Worker selecciona retryables |
Buenas practicas
Cliente: cuando encolar
Solo encolar tras agotar todas las estrategias del cliente:
const result = await PrintService.PrintInvoiceAsync(invoiceId, terminal);
if (!result.Success && result.Strategy === "none") {
// Solo aqui — el cliente probó todo y fallo
await Repository.PostAsync("/api/printjobs", {
posTerminalId: terminal.Id,
invoiceId: invoiceId,
payloadJson: JSON.stringify(payload),
strategy: "all-failed",
lastError: result.Message
});
}
Operador: dashboard de monitoreo
Una pagina recomendada (no incluida en Phase 1, pero el endpoint esta listo):
- Listar jobs
FailedyRetryingde la empresa. - Mostrar columna Intento (
AttemptCount/5). - Boton Reintentar (
POST .../retry). - Boton Cancelar (
POST .../cancel). - Notificacion SignalR cuando aparece un nuevo
Failed.
Limpieza periodica
Jobs Sent o Cancelled con CompletedAt > 30 dias se pueden eliminar para mantener la tabla pequeña. Implementar con un job nocturno (no incluido en Phase 1).