Tickets de custodia
La emisión del ticket cierra el lote de custodia: convierte un lote en GAUGING_CLOSE —con todos sus
gauges capturados— en un documento custody-grade numerado sin huecos. Es una acción exclusiva del
supervisor (custody:issue). Esta página está orientada al supervisor y cubre cómo emitir un
ticket y, si fuera necesario, cómo anularlo sin romper la trazabilidad. El ciclo de vida completo del
lote —los 9 estados, el wizard, la captura de gauges— vive en la página de
lotes de custodia; aquí no se duplica el diagrama de estados.
number humano del gauging ticketEl gauging ticket de laboratorio recibe un number humano
mediante una secuencia simple legible. El ticket de transferencia custody es distinto: usa el
contador correlativo gap-free (sin huecos), asignado bajo aislamiento serializable. Son dos
numeraciones diferentes con reglas diferentes; esta página documenta la gap-free real.
Quién emite y quién anula
| Acción | Autoridad | Rol |
|---|---|---|
| Emitir el ticket | custody:issue | supervisor (único) |
| Anular (VOID) el ticket | custody:void | supervisor (único) |
Ni el operador, ni el ingeniero, ni el administrador pueden emitir o anular tickets de custodia: es una responsabilidad del supervisor, sujeta además a la separación de funciones (ver abajo).
La emisión es atómica y serializable
La emisión (POST /api/v1/custody/batches/:id/issue) no es una secuencia de pasos sueltos: corre
dentro de una $transaction(Serializable) con reintento ante P2034 (hasta 5 intentos, backoff
exponencial). Dentro de esa transacción única ocurre, en orden:
SELECT batch FOR UPDATE→ se afirma que el lote está enGAUGING_CLOSE.- Se verifica que todos los gauges requeridos estén capturados (2 para
OUTBOUND/INBOUND, 4 paraTRANSFER). - Se evalúa el staleness gate de laboratorio (D-18 — ver abajo).
- El lote pasa a
CALCULATING. - Se ejecuta el motor MPMS de 18 etapas (TOV → GOV → GSV → NSV → Masa).
- Si el cálculo falla (
CALC_FAILED): rollback aGAUGING_CLOSEpersistiendolastCalcWarnings(D-07) — no se asigna número, no hay hueco. - Para
TRANSFER, se valida la tolerancia de conservación (422INVENTORY_GRADE_BLOCKEDsi queda fuera sin override). - Última operación SQL: se asigna el número correlativo gap-free.
- El lote pasa a
TICKETED(ticketNumber,ticketedAt,ticketedById). - Se escribe el audit row
action = 'TICKET_ISSUED'.
Tras el commit, un trabajo en cola (BullMQ) genera el PDF (ver PDF/A-3 abajo).
Numeración gap-free serializable
El contador correlativo se incrementa DENTRO de la misma transacción serializable que ticketea el
lote: SELECT nextSeq FROM custody_ticket_counter WHERE year = $y FOR UPDATE, se incrementa y se asigna
como ticketNumber. Como es la última operación SQL de la transacción, cualquier rollback previo
(staleness, CALC_FAILED, tolerancia, conflicto serializable) ocurre antes de tocar el contador →
el contador no avanza → cero huecos (CUS-06). El aislamiento serializable, además, impide que dos
emisiones simultáneas obtengan el mismo número.
Por esta razón un ticket nunca se "edita": cualquier corrección se hace mediante un VOID seguido de la reemisión de un lote correctivo (ver Correctivo en VOID). Esto garantiza que la secuencia sea consecutiva y sin huecos: no puede existir el ticket N+2 sin que el N+1 haya sido emitido o anulado con su propio número en la secuencia — exactamente la definición de ticket gap-free del glosario.
El motor MPMS de 18 etapas
Durante el estado CALCULATING, el sistema ejecuta el motor de cálculo MPMS de 18 etapas, que
transforma la medición física en los volúmenes netos custody-grade aplicando una cadena de factores
de corrección. La progresión de magnitudes es:
| Magnitud | Significado |
|---|---|
| TOV — Total Observed Volume | Volumen total observado, tal cual se lee del tanque |
| GOV — Gross Observed Volume | Volumen bruto observado tras corregir el efecto de la temperatura sobre la pared del tanque (CTSh) |
| GSV — Gross Standard Volume | Volumen bruto a condiciones estándar (15 °C) tras corregir el líquido por temperatura (CTL) y presión (CPL) |
| NSV — Net Standard Volume | Volumen neto estándar, ya descontados el agua y los sedimentos (CSW) |
| Masa | Masa del producto a partir del NSV y la densidad estándar |
Los factores de corrección que aplica la cadena son:
| Factor | Corrige por |
|---|---|
| CTSh — Correction for Temperature of the Shell | Dilatación/contracción de la pared del tanque por temperatura |
| CTL — Correction for Temperature of the Liquid | Expansión térmica del líquido |
| CPL — Correction for Pressure of the Liquid | Compresibilidad del líquido bajo presión |
| CSW — Correction for Sediment and Water | Agua y sedimentos (BSW) presentes en el producto |
Las definiciones formales de cada magnitud y factor están en el glosario de dominio. El resultado del motor queda congelado en el ticket (snapshot inmutable): el cálculo es reproducible y auditable — la misma entrada produce siempre la misma salida.
Emitir un ticket paso a paso
- Lote listo. El lote está en
GAUGING_CLOSEcon todos los gauges capturados (ver lotes de custodia). - Emitir. El supervisor pulsa "Emitir ticket" en el footer sticky de
/custody/batches/[id]. - Validaciones. El sistema verifica la SoD triple, el staleness gate y la presencia de todos los gauges — todo dentro de la transacción serializable.
- Cálculo. Ejecuta el motor MPMS de 18 etapas (
CALCULATING). - Numeración y commit. Asigna el número gap-free como última operación SQL y el lote pasa a
TICKETED; tras el commit, se encola la generación del PDF/A-3.
SoD triple — el ticketador no puede ser ninguno de los gaugers
La regla custody es gauger ≠ ticketer, y aquí es triple: el ticketador no puede ser ni el
gauger de apertura ni el de cierre (ticketerId ≠ openGaugerId AND ticketerId ≠ closeGaugerId,
isCustodySoDViolation). Quien aforó la apertura, quien aforó el cierre y quien emite deben ser
personas distintas. La regla se aplica en el servidor (@EnforceSoD) antes de la mutación: una
llamada directa al endpoint, saltándose el UI, recibe 403. Solo el supervisor posee custody:issue,
y aun así no puede emitir un lote que él mismo aforó.
PDF/A-3 firmado + verificación SHA-256
Tras pasar a TICKETED, se encola el trabajo BullMQ custody.pdf.generate, que genera el ticket
como PDF/A-3 firmado digitalmente (firma PAdES). El SHA-256 del PDF se persiste en
batch.pdfSha256 y el documento se sube a MinIO. Un cron de recuperación reintenta cualquier
ticket que se quedara sin PDF. La integridad se comprueba con GET /:id/verify, que recalcula el
SHA-256 y lo compara con el almacenado.
El ticket se puede leer en SI o en imperial según la preferencia de cada usuario, y el valor custody almacenado es reproducible bit a bit con independencia de la unidad mostrada — la conversión ocurre solo al renderizar. Ver el sistema de unidades y la hermana preferencias de unidades.
Ver el ticket emitido
El archivo regulatorio (/custody/tickets) lista los tickets definitivos —TICKETED y VOIDED—
filtrados por año (?year=). El detalle (/custody/tickets/[id]) presenta el ticket en formato
"comprobante de instrumento" (mono fixed-grid, MPMS 3.1A): número, tipo, producto, tanques, volumen
planificado vs. real (NSV m³ + masa kg), timestamps y contraparte. Es read-only tras la emisión.
Screenshot pendiente del barrido diferido — ver 61-DEFERRED-SWEEP.md (/custody/tickets archivo por año).
Screenshot pendiente del barrido diferido — ver 61-DEFERRED-SWEEP.md (/custody/tickets/[id] detalle receipt mono).
Correctivo en VOID — la secuencia nunca se rompe
Un ticket en TICKETED se anula (VOID) solo desde TICKETED, solo por el supervisor
(custody:void), con una razón de al menos 20 caracteres (D-06) y sujeto a la SoD (el anulador no
puede ser el ticketador). El VOID auto-crea un lote correctivo (corrective DRAFT) con supersedesId
apuntando al lote anulado. Así, el número anulado permanece en la secuencia (no se reutiliza) y el
reemplazo recibe su propio número correlativo: el gap-free se preserva.
¿Por qué nunca se edita un ticket? Porque la trazabilidad custody exige que la secuencia de números sea
inmutable y completa, incluso para los errores. Borrar o reescribir un ticket dejaría un hueco —o
peor, un número que afirma dos cosas distintas en el tiempo—. El patrón anular + reemitir deja un
rastro auditable: el ticket erróneo queda anulado con su número intacto y el corrective lo
reemplaza apuntando explícitamente a él (supersedesId). La auditoría de toda esta cadena se podrá
consultar desde el visor de auditoría (próximamente).
Páginas relacionadas
- El lote de custodia cubre los 9 estados, el wizard de 5 pasos y la captura de gauges que anteceden a la emisión.
- El gauging ticket de laboratorio usa una numeración humana distinta de la gap-free de custodia.
- La densidad y el BSW que evalúa el staleness gate provienen de la muestra de laboratorio.
- Las definiciones de TOV/GOV/GSV/NSV/Masa, los factores CTSh/CTL/CPL/CSW y el sistema de unidades SI ⇄ Imperial están en el glosario.