Saltar al contenido principal

Lotes de custodia

El lote de custodia (custody batch, /custody) es la operación unitaria de despacho, recepción o traslado de producto: abre con un aforo de apertura, registra la operación física y cierra con un aforo de cierre, para luego emitir un ticket numerado. Es el flujo más complejo del sistema y el corazón del Core Value de TankOS: cálculos custody-transfer correctos y trazables. Esta página está orientada al operador y al supervisor, y cubre el ciclo de vida del lote desde planificarlo hasta dejarlo listo para emitir; la emisión del ticket —numeración gap-free, motor MPMS y PDF firmado— se documenta en su propia página.

El aforo del lote NO es el gauging ticket de laboratorio

La captura de gauge de un lote de custodia (OPENING_SRC, CLOSING_DEST, …) es interna al lote y alimenta directamente su cálculo. No debe confundirse con el gauging ticket custody-grade del laboratorio, que tiene su propia máquina de estados y su number humano. El lote de custodia usa el contador correlativo gap-free; el gauging ticket usa una secuencia simple legible.

Quién hace qué

AcciónAutoridadRoles
Planificar / cancelar el lotecustody:planoperador, supervisor, ingeniero
Abrir el lotecustody:operateoperador, supervisor, ingeniero
Capturar gauges (aforar)custody:gaugeoperador, supervisor, ingeniero
Emitir el ticketcustody:issuesupervisor (único)
Anular el ticketcustody:voidsupervisor (único)

La emisión y la anulación del ticket son exclusivas del supervisor y se documentan en la página de tickets de custodia. El resto de la operación del lote —planificar, abrir, aforar, avanzar— la realiza el operador (o el supervisor/ingeniero).

El ciclo de vida en 9 estados

El lote de custodia obedece un reductor de 9 estados (nextCustodyState, 9 estados + 11 acciones): PLANNED, OPENED, GAUGING_OPEN, MEASURING, GAUGING_CLOSE, CALCULATING, TICKETED, VOIDED y CANCELLED. Cualquier par (estado, acción) que no aparezca abajo se rechaza como transición inválida.

Estado origenAcciónEstado destinoAutoridadGuardia / Nota
PLANNEDOPENOPENEDcustody:operateopenedAt = now()
OPENEDCAPTURE_OPENINGGAUGING_OPENcustody:gaugeCongela el snapshot de telemetría
GAUGING_OPENre-gaugeGAUGING_OPENcustody:gaugeSelf-loop de re-aforo (D-02)
GAUGING_OPENCONFIRM_LARGE_DIVERGENCEGAUGING_OPENcustody:gaugeConfirma divergencia > 5% (D-04)
GAUGING_OPENADVANCE_TO_MEASURINGMEASURINGcustody:operatemeasuringStartedAt = now()
MEASURINGADVANCE_TO_CLOSINGGAUGING_CLOSEcustody:operategaugingCloseStartedAt = now()
GAUGING_CLOSEre-gaugeGAUGING_CLOSEcustody:gaugeSelf-loop de re-aforo de cierre
GAUGING_CLOSESTART_CALCCALCULATINGcustody:issueSolo supervisor (parte de emitir)
CALCULATINGCALC_FAILEDGAUGING_CLOSE(sistema)Rollback con lastCalcWarnings (D-07)
CALCULATING(éxito)TICKETEDcustody:issueNúmero gap-free como última op. SQL
TICKETEDVOIDVOIDEDcustody:voidSolo supervisor, reason ≥ 20 chars
cualquiera pre-TICKETEDCANCELCANCELLEDcustody:planreason ≥ 10 chars
Estados terminales

VOIDED y CANCELLED no tienen transiciones salientes: una vez ahí, el lote está cerrado. No se "reabre" ni se "edita". Si un ticket emitido necesita corrección, se anula (VOID) y el sistema crea un lote correctivo de reemplazo — el detalle se documenta en tickets de custodia.

El wizard de nuevo lote (5 pasos)

Crear un lote arranca en /custody/new con un wizard de 5 pasos (CustodyWizardStepper). El paso activo se refleja en la URL (?step=…), de modo que se puede compartir o retomar exactamente donde se dejó. El paso final hace POST /api/v1/custody/batches (autoridad custody:plan) y el lote nace en estado PLANNED.

Wizard de nuevo lote — Paso 1: Tipo de operación

#Paso (?step=)Qué se eligeRegla
1Tipo (type)OUTBOUND / INBOUND / TRANSFERDetermina qué tanques pide el paso 2
2Tanques (tanks)Tanque(s) de origen y/o destinoOUTBOUND → solo origen; INBOUND → solo destino; TRANSFER → ambos
3Producto (product)productId del catálogoEl producto fija las propiedades del cálculo
4Volumen (volume)plannedVolumeM3 (Decimal, SI)counterpartyName requerido para OUTBOUND/INBOUND, prohibido para TRANSFER
5Revisión (review)Resumen + CTA "Crear lote"POST …/batches → estado PLANNED

Los 3 tipos de operación

  • OUTBOUND (salida): despacho de producto. Solo se elige el tanque de origen (sourceTankId) y se exige la contraparte (counterpartyName).
  • INBOUND (entrada): recepción de producto. Solo se elige el tanque de destino (destinationTankId) y se exige la contraparte.
  • TRANSFER (traslado interno): movimiento tanque → tanque dentro de la instalación. Se eligen ambos tanques; la contraparte no aplica (es interno).
Pre-llenado desde el detalle de tanque

El wizard acepta parámetros ?type=&sourceTankId= para pre-llenar el tipo y el tanque de origen cuando se abre desde el detalle de un tanque. Así, planificar un despacho desde el tanque que se está viendo es un solo clic.

Captura pendiente

Screenshot pendiente del barrido diferido — ver 61-DEFERRED-SWEEP.md (/custody/new pasos 2–5 del wizard (tanques/producto/volumen/revisión)).

Operar el lote paso a paso

El lote se opera desde su vista operativa /custody/batches/[id]: un header sticky con el número, el badge de estado y el badge de tipo, los CTA de lifecycle (Abrir lote / Iniciar medición / Cerrar medición), una columna izquierda (60%) con los GaugePanels de apertura y cierre, y una columna derecha (40%) con el LabStalenessBadge, el ConservationDeltaBadge (en TRANSFER), una card de previsualización del cálculo (NSV / masa totales) y la lista de warnings. El footer sticky lleva los CTA Cancelar lote y Emitir ticket.

  1. Crear el lote (→ PLANNED). El wizard de 5 pasos crea el lote en PLANNED.
  2. Abrir el lote (PLANNED → OPENED). Con autoridad custody:operate; se sella openedAt = now().
  3. Capturar el gauging de apertura (OPENED → GAUGING_OPEN). El operador captura el aforo de apertura (OPENING_SRC y/o OPENING_DEST) — ver Captura de gauge y snapshot inmutable abajo.
  4. Avanzar a medición (GAUGING_OPEN → MEASURING). Con custody:operate; se sella measuringStartedAt. Aquí ocurre la operación física (bombeo, carga/descarga).
  5. Avanzar al cierre (MEASURING → GAUGING_CLOSE). Con custody:operate; se sella gaugingCloseStartedAt.
  6. Capturar el gauging de cierre (en GAUGING_CLOSE). El operador captura el aforo de cierre (CLOSING_SRC y/o CLOSING_DEST), mismo proceso que la apertura.
  7. Emitir el ticket. Con todos los gauges capturados y el lote en GAUGING_CLOSE, el supervisor emite el ticket desde el footer — ver la página de tickets de custodia.
Conteo de gauges por tipo

OUTBOUND e INBOUND requieren 2 gauges (apertura + cierre del tanque relevante). TRANSFER requiere 4 gauges: OPENING_SRC, CLOSING_SRC, OPENING_DEST y CLOSING_DEST. El botón Emitir ticket permanece deshabilitado hasta que todos los gauges requeridos estén capturados.

Captura pendiente

Screenshot pendiente del barrido diferido — ver 61-DEFERRED-SWEEP.md (/custody/batches/[id] en PLANNED / GAUGING_OPEN / GAUGING_CLOSE (GaugePanels)).

Captura de gauge y snapshot inmutable

Al capturar un gauge, el operador ingresa las magnitudes del aforo en unidades SI: nivel (levelMm), TOV (tovM3), GOV (govM3), GSV (gsvM3), NSV (nsvM3), masa (massKg) y temperatura del producto (productTempC). En el mismo acto, el servicio congela el snapshot de telemetría ACTUAL del tanque (snapshotAt) dentro del campo telemetrySnapshot.

El snapshot de telemetría es INMUTABLE

El telemetrySnapshot es un JSONB inmutable: un trigger de base de datos impide modificarlo después de insertarlo (D-21). Es precisamente esta inmutabilidad la que hace trazable el aforo: deja constancia de qué decía el radar en el instante exacto del aforo manual, sin posibilidad de reescribir la historia. Si el valor del operador difiere del snapshot, el sistema no sobrescribe el snapshot: crea una fila CustodyGaugeOverride por cada campo divergente (append-only, con overrideReason ≥ 10 caracteres, validado por un CHECK en base de datos).

El gauger puede re-aforar mientras el lote está en GAUGING_OPEN (o en GAUGING_CLOSE para el cierre): el reductor permite el self-loop de re-gauge (D-02) sin sacar el lote del estado.

Divergencia de nivel mayor al 5%

Una discrepancia grande nunca pasa en silencio

Si el nivel del operador difiere del nivel de telemetría en más del 5% (|operatorLevel − telemetryLevel| / telemetryLevel > 5%, D-04), el sistema exige confirmación explícita del gauger: confirmedByGauger = true más una razón extra (≥ 10 caracteres) más un audit row con action = 'LARGE_LEVEL_DIVERGENCE'. Es una guardia custody clave: el operador debe reconocer y justificar una discrepancia grande para que el aforo siga adelante. Nada se asume; todo queda registrado.

Staleness gate de laboratorio

El cálculo custody-grade necesita densidad y BSW recientes del laboratorio. Por eso, al emitir el ticket, el motor revisa la antigüedad de las muestras de laboratorio (D-18):

Antigüedad de la muestraResultado
< 72 hFlujo normal — la emisión procede
72 h – 14 díasRequiere staleLabOverride + reason + autoridad ops:override
> 14 díasHARD BLOCK — sin override posible
Una muestra de laboratorio vencida bloquea la emisión

Si la muestra de laboratorio tiene más de 14 días, la emisión se bloquea sin excepción: la densidad y el BSW del cálculo custody deben ser frescos. Entre 72 h y 14 días, solo un usuario con ops:override puede continuar, dejando una razón. Por debajo de 72 h, el flujo es normal. El gate se evalúa en el momento de emitir — su detalle vive en la página de tickets de custodia.

Bloqueo de grado de inventario (INVENTORY_GRADE_BLOCKED, 422)

Además del staleness, la emisión puede devolver INVENTORY_GRADE_BLOCKED (422) cuando un TRANSFER queda fuera de la tolerancia de conservación sin override, o cuando un tanque no tiene tabla de calibración (strapping) publicada o sus medidas caen fuera de la tabla. Sin una tabla de aforo vigente, el lote no alcanza grado de transferencia. La gestión de la tabla de aforo se ve en Calibración y strapping.

Workspace de lotes

El workspace de custodia (/custody) organiza los lotes en 4 pestañas (la pestaña activa va en la URL como ?tab=…): active (lotes en curso), tickets (tickets recientes), voided (anulados) y cancelled (cancelados). Un indicador de tiempo real en la barra de herramientas refleja los cambios de estado en vivo. El CTA "Nuevo lote" abre el wizard y solo aparece para quien tiene custody:plan (oculto, por ejemplo, para el técnico de laboratorio y el administrador).

Workspace de lotes de custodia

Páginas relacionadas

  • La emisión y anulación del ticket —numeración gap-free, motor MPMS de 18 etapas, PDF/A-3 firmado y el correctivo en VOID— es la segunda mitad de este flujo.
  • La densidad y el BSW que evalúa el staleness gate provienen de la muestra de laboratorio.
  • Los aforos custody-grade y el override de radar se documentan en gauging tickets.
  • El pre-llenado del wizard y el banner de override activo se ven en el detalle de tanque.
  • El ticket de custodia se puede leer en SI o en imperial según la preferencia del usuario, sin alterar el valor almacenado — ver el sistema de unidades.
  • Las guías por rol del lote de custodia llegarán en una próxima sección del manual (próximamente).