Saltar al contenido principal

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.

El número gap-free NO es el number humano del gauging ticket

El 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ónAutoridadRol
Emitir el ticketcustody:issuesupervisor (único)
Anular (VOID) el ticketcustody:voidsupervisor (ú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:

  1. SELECT batch FOR UPDATE → se afirma que el lote está en GAUGING_CLOSE.
  2. Se verifica que todos los gauges requeridos estén capturados (2 para OUTBOUND/INBOUND, 4 para TRANSFER).
  3. Se evalúa el staleness gate de laboratorio (D-18 — ver abajo).
  4. El lote pasa a CALCULATING.
  5. Se ejecuta el motor MPMS de 18 etapas (TOV → GOV → GSV → NSV → Masa).
  6. Si el cálculo falla (CALC_FAILED): rollback a GAUGING_CLOSE persistiendo lastCalcWarnings (D-07) — no se asigna número, no hay hueco.
  7. Para TRANSFER, se valida la tolerancia de conservación (422 INVENTORY_GRADE_BLOCKED si queda fuera sin override).
  8. Última operación SQL: se asigna el número correlativo gap-free.
  9. El lote pasa a TICKETED (ticketNumber, ticketedAt, ticketedById).
  10. 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 número se asigna como ÚLTIMA operación — y un rollback no deja hueco

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 avanzacero 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:

MagnitudSignificado
TOVTotal Observed VolumeVolumen total observado, tal cual se lee del tanque
GOVGross Observed VolumeVolumen bruto observado tras corregir el efecto de la temperatura sobre la pared del tanque (CTSh)
GSVGross Standard VolumeVolumen bruto a condiciones estándar (15 °C) tras corregir el líquido por temperatura (CTL) y presión (CPL)
NSVNet Standard VolumeVolumen neto estándar, ya descontados el agua y los sedimentos (CSW)
MasaMasa del producto a partir del NSV y la densidad estándar

Los factores de corrección que aplica la cadena son:

FactorCorrige por
CTShCorrection for Temperature of the ShellDilatación/contracción de la pared del tanque por temperatura
CTLCorrection for Temperature of the LiquidExpansión térmica del líquido
CPLCorrection for Pressure of the LiquidCompresibilidad del líquido bajo presión
CSWCorrection for Sediment and WaterAgua 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

  1. Lote listo. El lote está en GAUGING_CLOSE con todos los gauges capturados (ver lotes de custodia).
  2. Emitir. El supervisor pulsa "Emitir ticket" en el footer sticky de /custody/batches/[id].
  3. Validaciones. El sistema verifica la SoD triple, el staleness gate y la presencia de todos los gauges — todo dentro de la transacción serializable.
  4. Cálculo. Ejecuta el motor MPMS de 18 etapas (CALCULATING).
  5. 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

Separación de funciones servidor-side — un intento directo recibe 403

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

El ticket es un documento firmado y verificable, legible en SI o imperial

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.

Captura pendiente

Screenshot pendiente del barrido diferido — ver 61-DEFERRED-SWEEP.md (/custody/tickets archivo por año).

Captura pendiente

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 nunca se edita: se anula y se reemite

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.