Decisiones técnicas clave (ADRs)
Cada ADR (Architecture Decision Record) documenta una decisión en formato Nygard: contexto, decisión, consecuencias. Aquí se destacan las 7 más relevantes para entender la solución. El listado completo de 36 ADRs está en Anexos.
Cada ADR tiene dos pestañas:
- Resumen ejecutivo — para directivos y operaciones.
- Detalle técnico — para IT y arquitectos.
ADR-038 — Zona como unidad operativa mínima
Qué se decidió: la zona (50-200m, ej. “Zona Norte”) es la unidad mínima de ubicación de un trailer. No hay slots ni cajones dentro de las zonas.
Por qué: el patio del cliente no tiene marcas físicas en el piso (es terraplén irregular), no se pueden poner postes con QR (riesgo de colisión con tractos), y el GPS comercial tiene precisión de ±15-30m — distingue zonas, no posiciones dentro de ellas.
Beneficio: modelo simple, captura rápida (dropdown de 10-20 zonas), sin infraestructura física requerida en el patio.
Trade-off aceptado: no se podrá consultar “qué trailer está en la fila 3, cajón 7” — sólo “qué trailers están en Zona Norte”. El cliente lo aceptó como reflejo fiel de su operación real.
Modelo de datos (§14.5 de la propuesta-maestra):
Assignment→ZoneId(no a sub-posición).UNIQUE FILTERED INDEXsobre(YardId, TrailerId)conStatus = 'Confirmed'garantiza una asignación activa por trailer por yard. La zona NO es exclusiva — N trailers pueden coexistir en una zona.MovementEvent→ZoneId. Tipos:EntryRegistered,ExitRegistered,OperatorZoneConfirmed,OperatorZoneCorrected,GpsConfirmedEntry,GpsConfirmedExit,InternalMove.Anomaly→ZoneId.LayoutVersionversiona el conjunto de Zones; cada publicación archiva la anterior y generaLayoutSnapshotpara histórico.
Tablero SVG (§14.3): renderiza ~10-20 polígonos con contador agregado de trailers por zona.
Editor de zonas: el YardManager crea/edita zonas como polígonos con nombre y código.
ADRs relacionados: ADR-014 (amended), ADR-039 (GPS confirma, personal de patio registra).
ADR-039 — GPS como mecanismo de confirmación, no fuente de verdad
Qué se decidió: el personal de patio es la fuente de verdad de toda entrada, salida y movimiento. El GPS Samsara confirma lo que el personal ya capturó, no genera eventos por sí solo.
Por qué: el cliente clarificó que su cultura es “el personal captura todo, el GPS valida”. No quieren un sistema donde “el GPS decidió” sin intervención humana. Además, el GPS Samsara tiene ±15-30m de precisión — no es suficiente para confiar en él como única fuente.
Beneficio: cultura alineada con el cliente, trailers visitantes (~5% sin GPS) tienen flujo de primera clase (no excepción), anomalías más accionables.
Trade-off aceptado: menos automatización vs propuesta original; latencia mayor en detección de salidas fantasma (mitigado con anomalía UnexpectedManualGap).
Reorientación del pipeline Samsara (Sprints 14-15):
- El webhook sigue recibiendo posiciones GPS sin cambios.
- El worker async ya no genera
MovementEventautomáticos de tipo EnteredYard/LeftYard/MovedToZone. - En su lugar, el worker:
- Busca eventos manuales recientes pendientes de confirmación GPS.
- Si encuentra match (ej.
EntryRegisteredhace 10 min + GPS reporta cruce de geofence ahora): actualizaMovementEvent.GpsConfirmedAt. - Si GPS reporta evento sin contraparte manual: genera
Anomaly.UnexpectedManualGap(severidad Medium).
Confirmación de zona: GPS asigna zona automáticamente (señal débil) → personal confirma o corrige en rondín → si zona corregida ≠ zona GPS, anomalía OperatorCorrectedGpsZone (Low, informativa).
Trailers sin Samsara (IsGpsCapable = false): no hay confirmación GPS posible. La zona DEBE confirmarse en rondín dentro de 4h o se genera VisitorZoneUnconfirmed.
Supersedes: ADR-011 (Samsara como motor de eventos). Extiende: ADR-014.
ADR-040 — ShipmentDocument con QR como entrada primaria
Qué se decidió: el documento físico que trae el transportista (la “carta de instrucciones / hoja de salida”) se modela como entidad ShipmentDocument. El personal de patio escanea su QR como primer paso del registro de entrada — el sistema pre-llena ~90% de los campos.
Por qué: el propio cliente identificó este Quick Win. Hoy el personal de patio transcribe a mano lo que el QR ya contiene; reducir captura de ~20 campos a “scan + confirmar” baja el tiempo de 3 minutos a 30 segundos.
Beneficio: ~70-80% de reducción en tiempo de captura, sellos como entidad de primera clase, trailers visitantes tienen identificación estructurada.
Trade-off aceptado: nueva entidad agregada al modelo (CRUD adicional); si el TMS del cliente no emite QR todavía, el flujo degrada a captura manual (variante 2).
Modelo:
public class ShipmentDocument : AuditableEntity { public string Folio { get; private set; } public string QrCode { get; private set; } public DateTime IssuedAt { get; private set; } public Guid? CarrierId { get; private set; } public Guid? LineId { get; private set; } public string? BoxNumber { get; private set; } public Guid? TripTypeId { get; private set; } public Guid? ContentTypeId { get; private set; } public string? DriverName { get; private set; } public string? DriverLicense { get; private set; } public Guid? TrailerId { get; private set; } public Guid? MovementEventId { get; private set; }}
public class TrailerSeal : AuditableEntity { public Guid TrailerId { get; private set; } public int Position { get; private set; } // 1, 2, 3 public string Number { get; private set; } public Guid? AppliedInDocumentId { get; private set; } public DateTime AppliedAt { get; private set; } public DateTime? RemovedAt { get; private set; } public Guid? RemovalPhotoId { get; private set; }}Variantes del flujo (Sprint 11/11.5):
- QR escaneado (~80%):
MovementEvent.EntryRegisteredconSource = ManualEntryyShipmentDocumentIdenlazado. - Manual fallback (~20%): captura solo campos críticos desde catálogos cerrados.
Sellos: capturados al entrar, verificados al salir — si no coinciden, anomalía SealMismatch (High).
ADR-008 — Multi-yard nativo desde Sprint 1
Qué se decidió: el sistema soporta multiple yards desde el día uno, con permisos por yard. Un mismo usuario puede tener acceso a varios patios con roles diferenciados.
Por qué: el cliente ya opera multi-patio (Dulces Nombres + Saltillo) pero su sistema actual no diferencia permisos. Diseñar multi-yard desde el inicio evita re-arquitecturas costosas después.
Beneficio: sin retrabajo cuando el cliente agregue un nuevo patio; permisos granulares por yard desde el inicio.
Entidad UserYardAccess(UserId, YardId, Role). Todas las queries de operación filtran por YardId desde el repositorio. Trailer.CurrentYardId es nullable para soportar trailers visitantes que aún no entran a ningún yard.
Particionamiento de tablas históricas (MovementEvent, AuditLog) por YardId previsto en ADR-007 para escalabilidad.
ADR-001 — Result pattern obligatorio
Qué se decidió: los errores de negocio (validaciones, reglas, conflictos) no se lanzan como excepciones. Se retornan como objetos Result<T> con error tipado.
Beneficio: flujo de errores predecible y testeable; los logs no se contaminan con BusinessException; los controllers traducen Result a HTTP status code de forma uniforme.
Sólo excepciones para errores inesperados (BD caída, deserialización imposible, bug). Toda regla de dominio retorna Result.Failure<T>(ErrorCode, message).
Pipeline Behaviors de MediatR transforman Result.Failure en respuestas HTTP apropiadas (400/404/409) automáticamente.
ADR-002 — CQRS con MediatR y Pipeline Behaviors
Qué se decidió: comandos (escritura) y queries (lectura) están separados, ambos pasan por MediatR. Aspectos transversales (validación, audit, logging) son pipeline behaviors reutilizables.
Beneficio: cada handler tiene una sola responsabilidad; la validación/auditoría no se duplica; nuevos endpoints pasan por las mismas garantías sin código duplicado.
Estructura típica: Command → ValidationBehavior → AuthorizationBehavior → AuditBehavior → Handler → DbContext.SaveChanges → DomainEvents → Webhooks.
Queries son read-only — usan proyecciones específicas a DTOs sin tracking del DbContext.
ADR-035 — data-testid obligatorio para selectores E2E
Qué se decidió: los componentes interactivos del frontend llevan atributo data-testid estable, usado por las pruebas end-to-end (Playwright).
Beneficio: las pruebas no se rompen cuando un diseñador cambia clases CSS o estructura HTML. Las pruebas describen qué hace el usuario, no qué tag hay.
Convención: data-testid="<contexto>-<componente>-<acción>" (ej. data-testid="entry-form-submit").
Playwright E2E selecciona exclusivamente por data-testid; los selectores CSS y XPath están prohibidos en pruebas E2E (lint rule).
Listado completo (36 ADRs)
Para el inventario completo en formato Nygard, ver Anexos → ADRs completos o descargar directamente el archivo architecture-decision-records.md.