Skip to content

Instantly share code, notes, and snippets.

@panchicore
Last active May 25, 2026 22:45
Show Gist options
  • Select an option

  • Save panchicore/05750ad39fb43a96a8060f0c37f99178 to your computer and use it in GitHub Desktop.

Select an option

Save panchicore/05750ad39fb43a96a8060f0c37f99178 to your computer and use it in GitHub Desktop.
Feature: Confirmar Transportista (CARRIER_CONFIRMED) - Work Plan for Juan Miguel

Feature: Confirmar Transportista (CARRIER_CONFIRMED)

Asignado a: Juan Miguel Revisado por: Luis Pallares Cliente: TLC (Eduardo Galvan) Estimado: ~10 horas


Contexto del negocio (lee esto primero)

TLC mueve contenedores entre Colombia y Venezuela. Cuando Eduardo cotiza un envio, pone un transportista estimado (ej: "Transportes Pallares"), pero no sabe con certeza cual transportista usara hasta el dia anterior al envio. El sistema actual copia ese transportista al shipment y lo muestra como solo lectura — Eduardo no puede cambiarlo.

Esto se agrava con multiples envios por cotizacion: si cotizo con Pallares pero necesita 5 envios con 5 transportistas distintos, cada envio hereda "Pallares" y no puede cambiarlo.

Lo que vamos a construir: un paso obligatorio "Confirmar Transportista" antes de "Asignar Vehiculo", donde el usuario confirma o cambia el transportista y el costo del flete.


Contexto tecnico (entiende el sistema)

El workflow actual del tramo (shipment leg)

PENDING → TRUCK_ASSIGNED → PICKUP_ARRIVED → IN_TRANSIT → DELIVERED

Lo que vamos a hacer

PENDING → CARRIER_CONFIRMED → TRUCK_ASSIGNED → PICKUP_ARRIVED → IN_TRANSIT → DELIVERED

Arquitectura del sistema

UI (React) → React Query Hook → Server Action → Service → Repository → Database

Cada capa tiene una responsabilidad:

  • UI: formularios, botones, feedback visual
  • React Query: cache, mutations, invalidacion
  • Server Actions: validacion con Zod, autenticacion, delega al service
  • Service: logica de negocio, transacciones, side effects
  • Repository: queries SQL via Drizzle ORM

El patron de workflow

El sistema usa un framework llamado UAF (Unified Actions Framework). La "fuente de verdad" de cada workflow es un archivo *.workflow.definition.ts que define estados, eventos, transiciones y guards. La UI se genera automaticamente desde esa definicion.

Para agregar un nuevo paso al workflow, tocas estas capas en orden:

1. Constants (schema)     → nuevo status string
2. Workflow Definition    → nueva transicion
3. Zod Schema             → validacion del input
4. Guards                 → reglas de negocio pre-transicion
5. Service                → logica + side effects
6. Actions                → entry point del servidor
7. UI Form                → formulario del usuario
8. Form Registration      → conectar form al workflow engine
9. Status Display         → badge/chip visual
10. Derived Status Calc   → como afecta al shipment padre
11. Process Graph         → visualizacion del flujo
12. Tests                 → verificar todo

Cada una de estas capas ya tiene ejemplos funcionando (TRUCK_ASSIGNED, IN_TRANSIT, etc). Tu trabajo es seguir el patron exacto.


Instrucciones de implementacion

Antes de empezar

git checkout main && git pull
git checkout -b panchicore/feature-carrier-confirmed
pnpm install

Lee estos archivos para entender el patron existente (no los modifiques todavia, solo leelos):

src/db/schema/shipment-legs.ts                    → status constants
src/features/shipments/workflow/shipment-leg.workflow.definition.ts → workflow definition
src/features/shipments/lib/guards.ts              → guard functions
src/features/shipments/lib/schemas.ts             → Zod schemas
src/features/shipments/services/shipment-leg-service.ts → service layer
src/features/shipments/actions/shipment-leg-actions.ts  → server actions
src/features/shipments/components/workflow/forms/  → transition forms (lee todos)
src/features/shipments/components/workflow/ShipmentLegCard.tsx → form registration

Fase 1: Backend (constants, workflow, guards, schema, service, actions)

1.1 — Agregar status constant

Archivo: src/db/schema/shipment-legs.ts

Agrega CARRIER_CONFIRMED: 'CARRIER_CONFIRMED' al objeto SHIPMENT_LEG_STATUSES. No se necesita migracion de DB porque el campo status es text, no un enum de Postgres.

1.2 — Actualizar workflow definition

Archivo: src/features/shipments/workflow/shipment-leg.workflow.definition.ts

  • Agrega 'carrier_confirmed' a SHIPMENT_LEG_STATES (entre 'pending' y 'truck_assigned')
  • Agrega 'CARRIER_CONFIRMED' a SHIPMENT_LEG_EVENTS (primera posicion)
  • Agrega nueva transicion al array transitions (primera posicion):
    • from: 'pending', event: 'CARRIER_CONFIRMED', to: 'carrier_confirmed'
    • guard: 'canConfirmCarrier'
    • icon: UserCheck (importar de lucide-react)
    • title: 'Confirmar Transportista'
    • description: 'Confirme o cambie el transportista y costo de flete para este tramo.'
    • hasInlineForm: true
    • roles: ['OPERATIONS']
    • agentMessage: funcion que muestre carrier actual, ruta y costo del flete (usa los helpers getLegFreightCost y getSequencingNote que ya existen)
    • formId: (ctx) => \carrier-confirm-form-${ctx.leg.id}``
  • Cambia la transicion de TRUCK_ASSIGNED: from: 'pending'from: 'carrier_confirmed'
  • Actualiza los agentMessage de TODAS las transiciones: "Paso X de 4" → "Paso X de 5" (CARRIER_CONFIRMED es paso 1, TRUCK_ASSIGNED es paso 2, etc)
  • Agrega carrier_confirmed: SHIPMENT_LEG_STATUSES.CARRIER_CONFIRMED a dbStatusCodes
  • Agrega CARRIER_CONFIRMED a getLegStatusInfo: { label: 'Transp. Confirmado', description: 'Transportista confirmado para este tramo', isFinal: false }

1.3 — Agregar Zod schema

Archivo: src/features/shipments/lib/schemas.ts

export const executeShipmentLegTransitionCarrierConfirmedSchema = z.object({
  carrierPartyId: z.string().uuid('Seleccione un transportista'),
  estimatedFreightCost: z.string().regex(/^\d+(\.\d{1,2})?$/, 'Formato de costo inválido'),
});
export type ConfirmCarrierInput = z.infer<typeof executeShipmentLegTransitionCarrierConfirmedSchema>;

1.4 — Agregar guard

Archivo: src/features/shipments/lib/guards.ts

  • Agrega estimatedFreightCost?: string a la interface LegEventInput
  • Agrega funcion canConfirmCarrier (valida que carrierPartyId este presente). Sigue el patron exacto de canAssignTruck
  • En validateLegTransition:
    • Agrega caso: event === 'CARRIER_CONFIRMED' && statusCode === SHIPMENT_LEG_STATUSES.PENDINGcanConfirmCarrier
    • Cambia el caso de TRUCK_ASSIGNED: statusCode === SHIPMENT_LEG_STATUSES.PENDINGstatusCode === SHIPMENT_LEG_STATUSES.CARRIER_CONFIRMED

1.5 — Agregar metodo en service

Archivo: src/features/shipments/services/shipment-leg-service.ts

Agrega metodo publico executeShipmentLegTransitionCarrierConfirmed. Este tiene un side effect que actualiza el cost item de FREIGHT.

CRITICO: El side effect DEBE ejecutarse dentro de la MISMA transaccion que la transicion del leg. Mira como IN_TRANSIT lo hace con el anticipo en handleAdvancePaymentSideEffect — ese es tu modelo exacto. NO uses ShipmentCostItemService.updateShipmentCostItem() porque ese metodo abre su propia transaccion interna. Debes usar los repos directamente dentro del runTransaction().

Pseudocodigo del metodo (sigue esta estructura exacta):

async executeShipmentLegTransitionCarrierConfirmed(
  organizationId: string,
  legId: string,
  input: ConfirmCarrierInput,
  userId: string,
): Promise<ExecuteLegTransitionResultResponse> {
  // 1. Preparar inputs para el core (igual que los otros metodos)
  const legEventInput: LegEventInput = {
    carrierPartyId: input.carrierPartyId,
  };
  const legStatusUpdate: Partial<InsertShipmentLeg> = {
    carrierPartyId: input.carrierPartyId,
  };

  // 2. Ejecutar el core — PERO necesitas el side effect transaccional
  //    Sigue el patron de IN_TRANSIT (lineas 249-283 del service):
  //
  //    a) Carga el leg, valida topologia, corre guards (todo esto lo hace executeTransitionCore)
  //    b) PERO en vez de delegar a executeTransitionCore directamente,
  //       necesitas wrappear en runTransaction para incluir el side effect.
  //
  //    La forma mas limpia: agrega el side effect DESPUES de executeTransitionCore
  //    dentro de una transaccion, igual que IN_TRANSIT.
  //    Mira como IN_TRANSIT hace el if (eventType === 'IN_TRANSIT') en executeTransitionCore
  //    y agrega tu caso: if (eventType === 'CARRIER_CONFIRMED')

  // 3. Dentro del runTransaction:
  //    a) this.legRepo.executeShipmentLegTransition({...}, tx)
  //    b) this.handleCarrierConfirmationSideEffect(orgId, legId, shipmentId, input, userId, tx)

  // 4. El side effect (metodo privado nuevo):
  //    a) const costItems = await this.costItemRepo.findShipmentCostItemsByLegId(orgId, legId, tx)
  //    b) const freightItem = costItems.find(ci => ci.chargeTypeCode === 'FREIGHT')
  //    c) if (freightItem) {
  //         const derived = computeCostDerivedFields(
  //           input.estimatedFreightCost,
  //           freightItem.customerUnitPrice,
  //           freightItem.quantity
  //         );
  //         await this.costItemRepo.updateShipmentCostItem(orgId, freightItem.id, {
  //           vendorPartyId: input.carrierPartyId,
  //           estimatedCost: input.estimatedFreightCost,
  //           ...derived,
  //         }, userId, tx);
  //         await this.costItemRepo.recalculateShipmentTotals(orgId, shipmentId, userId, tx);
  //       }
}

Importaciones que necesitas: ConfirmCarrierInput del schema, computeCostDerivedFields de @/features/shipments/lib/cost-calculations.

1.6 — Agregar case en actions

Archivo: src/features/shipments/actions/shipment-leg-actions.ts

Agrega case 'CARRIER_CONFIRMED': en el switch. Sigue el patron exacto de los otros cases (parse → service call → return success).

Checkpoint: despues de esto, corre tsc --noEmit para verificar tipos. No deberia haber errores.


Fase 2: Frontend (form, registration, badges, calculator, graph)

2.1 — Crear formulario

Archivo nuevo: src/features/shipments/components/workflow/forms/shipment-leg.carrier-confirmed.transition-form.tsx

Sigue el patron de StartTransitForm (que tambien recibe costItems como prop):

  • Props: { leg: ShipmentLegWithLocationsResponse, costItems: ShipmentCostItemWithVendorResponse[], onConfirm: (data: ConfirmCarrierInput) => void, formId?: string }
  • Carrier selector: reutiliza PartySelector de @/shared/components/selectors/PartySelector + useVendorPartiesForLineItems() de @/features/quotations/api/lookups-api. Pre-llenado con leg.carrierPartyId. Filtra client-side para mostrar solo parties con partyTypeCode === 'CARRIER'

    NO crees un hook nuevo tipo useCarrierParties. El hook useVendorPartiesForLineItems() ya trae carriers, fixers y service providers. Filtra en el componente: parties.filter(p => p.partyTypeCode === 'CARRIER'). Crear hooks nuevos rompe DRY y el PR sera rechazado.

  • Freight cost input: reutiliza CurrencyInput de @/shared/components/form/CurrencyInput. Pre-llenado desde el cost item FREIGHT del leg: costItems.find(ci => ci.shipmentLegId === leg.id && ci.chargeTypeCode === 'FREIGHT')?.estimatedCost
  • AgentMessage: muestra carrier actual y costo, explica que puede confirmar o cambiar
  • handleSubmit: construye ConfirmCarrierInput y llama onConfirm(data)

2.2 — Registrar formulario

Archivo: src/features/shipments/components/workflow/ShipmentLegCard.tsx

  • Importa CarrierConfirmationForm y ConfirmCarrierInput
  • Agrega case 'CARRIER_CONFIRMED': en el switch de renderForm — pasa leg, costItems, onConfirm, formId

2.3 — Agregar badge de status

Archivo: src/features/shipments/components/ShipmentDerivedStatusChip.tsx

Agrega a LEG_STATUS_COMPACT:

CARRIER_CONFIRMED: { label: 'Transp. confirmado', className: 'bg-cyan-100 text-cyan-700' },

2.4 — Actualizar derived status calculator

Archivo: src/features/shipments/lib/shipment-status-calculator.ts

Agrega CARRIER_CONFIRMED a la funcion isInProgress (linea 70-73):

status === SHIPMENT_LEG_STATUSES.CARRIER_CONFIRMED ||

2.5 — Actualizar process graph

Archivo: src/features/shipments/lib/shipment-process-graph.ts

  • Agrega CARRIER_CONFIRMED: 'carrier_confirmed' a dbStatusToStateKey
  • Agrega carrier_confirmed: 'CARRIER_CONFIRMED' a getSpanishLabel

Checkpoint: corre tsc --noEmit && pnpm lint — sin errores.


Fase 3: Tests

Actualiza los tests existentes y agrega nuevos. Los archivos de test estan en tests/unit/workflow/ y tests/unit/services/. Lee los tests existentes primero para entender la estructura.

Cambios esperados:

  • Workflow topology: counts de estados/eventos cambian (5→6 estados, 4→5 eventos)
  • Transiciones: PENDING ya no va a TRUCK_ASSIGNED, va a CARRIER_CONFIRMED
  • Guards: nuevo test suite para canConfirmCarrier
  • Calculator: CARRIER_CONFIRMED cuenta como "in progress"
  • Process graph: node/edge counts cambian
  • Service: test para executeShipmentLegTransitionCarrierConfirmed (happy path, cambio de carrier, sin FREIGHT item)
pnpm test:run

Fase 4: Verificacion (LA MAS IMPORTANTE)

Juan Miguel: esta fase es donde demuestras que la feature funciona. No es opcional. Cada item debe quedar verificado antes de pedir PR review.

4.1 — Verificacion automatizada

pnpm qa    # lint + type-check + unit tests (todo debe pasar)

4.2 — Verificacion manual en browser

Levanta el dev server:

pnpm dev

Escenario 1: Confirmar sin cambios (happy path)

  1. Ve a una cotizacion existente con transportista asignado
  2. Acepta la cotizacion y crea un envio
  3. Ve al detalle del envio → tab Acciones
  4. Verifica: la primera accion disponible es "Confirmar Transportista" (NO "Asignar Vehiculo")
  5. Abre el formulario — verifica que el carrier y costo estan pre-llenados
  6. Confirma sin cambiar nada
  7. Verifica: el leg pasa a estado CARRIER_CONFIRMED
  8. Verifica: la siguiente accion es "Asignar Vehiculo"
  9. Completa el flujo: asigna vehiculo → llegada → transito → entrega

Escenario 2: Cambiar transportista

  1. Crea otro envio desde la misma cotizacion
  2. En "Confirmar Transportista", cambia el carrier a uno diferente
  3. Cambia tambien el costo del flete
  4. Confirma
  5. Verifica en tab Cargos: el cost item FREIGHT tiene el nuevo carrier como vendor
  6. Verifica en tab Cargos: el monto del flete cambio al que ingresaste
  7. Verifica: los totales del envio (costo interno, precio cliente, margen) se recalcularon
  8. Ve a "Asignar Vehiculo" — verifica que los conductores y vehiculos son del NUEVO carrier

Escenario 3: Multiples envios con diferentes carriers

  1. Desde una cotizacion con carrier "Transportes A", crea envio 1
  2. Confirma transportista con "Transportes A" (sin cambio)
  3. Crea envio 2 desde la misma cotizacion
  4. Confirma transportista con "Transportes B" (cambio)
  5. Verifica: envio 1 tiene carrier A, envio 2 tiene carrier B
  6. Verifica: los costos de flete son independientes entre envios

4.3 — Verificacion en base de datos

Usa Claude Code para verificar directamente en DB. Pide a Claude que ejecute queries contra Supabase para confirmar:

- shipment_legs.carrier_party_id se actualizo al carrier confirmado
- shipment_legs.status = 'CARRIER_CONFIRMED'
- shipment_cost_items WHERE charge_type_code = 'FREIGHT': vendor_party_id = carrier confirmado
- shipment_cost_items WHERE charge_type_code = 'FREIGHT': estimated_cost = monto ingresado
- shipments: total_internal_cost, total_customer_price, total_profit recalculados
- workflow_events: existe registro con event_type = 'CARRIER_CONFIRMED', from_status = 'PENDING', to_status = 'CARRIER_CONFIRMED'

4.4 — Verificacion de no-regresion

  • Cotizaciones: crear, editar, enviar, aceptar — todo sigue funcionando
  • Envios: el flujo completo CARRIER_CONFIRMED → TRUCK_ASSIGNED → PICKUP → TRANSIT → DELIVERED funciona
  • Process graph: la visualizacion muestra 6 nodos por tramo (incluye carrier_confirmed)
  • Anticipo: al iniciar transito, el calculo de anticipo sigue funcionando con el carrier confirmado

4.5 — Checklist final

[ ] pnpm qa pasa limpio
[ ] Escenario 1 verificado (confirmar sin cambios)
[ ] Escenario 2 verificado (cambiar carrier + costo)
[ ] Escenario 3 verificado (multiples envios, diferentes carriers)
[ ] Datos verificados en DB
[ ] No-regresion verificada
[ ] Commit con mensaje descriptivo
[ ] Push al branch
[ ] PR creado apuntando a main

Reglas de trabajo

  1. Sigue los patrones existentes. Si ves algo que no sabes como hacer, busca como lo hace TRUCK_ASSIGNED o IN_TRANSIT. No inventes.
  2. No crees abstracciones nuevas. Reutiliza PartySelector, CurrencyInput, AgentMessage, computeCostDerivedFields. Todo ya existe.
  3. Corre tsc --noEmit frecuentemente. Despues de cada fase, no solo al final.
  4. Si te atascas, lee el archivo que sigue el patron que necesitas. Claude Code puede ayudarte a encontrarlo.
  5. No hagas PR hasta completar la Fase 4 completa. El PR review sera rapido si la verificacion esta hecha.

Archivos que vas a tocar

Modificar (11 archivos)

src/db/schema/shipment-legs.ts
src/features/shipments/workflow/shipment-leg.workflow.definition.ts
src/features/shipments/lib/schemas.ts
src/features/shipments/lib/guards.ts
src/features/shipments/services/shipment-leg-service.ts
src/features/shipments/actions/shipment-leg-actions.ts
src/features/shipments/components/workflow/ShipmentLegCard.tsx
src/features/shipments/components/ShipmentDerivedStatusChip.tsx
src/features/shipments/lib/shipment-status-calculator.ts
src/features/shipments/lib/shipment-process-graph.ts
tests/unit/workflow/*.test.ts (varios)

Crear (1 archivo)

src/features/shipments/components/workflow/forms/shipment-leg.carrier-confirmed.transition-form.tsx

Cuando termines

  1. Crea el PR apuntando a main
  2. En la descripcion del PR incluye screenshots de:
    • El formulario de "Confirmar Transportista" pre-llenado
    • El formulario con un carrier diferente seleccionado
    • El tab Cargos mostrando el vendor actualizado
  3. Avisa a Luis para PR review
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment