# F-113 — Email al cliente en procesando/enviado con tracking y courier editable ## Backend - **Migración 039** (`039_order_courier.js`): `orders_orders.courier varchar(120) NULL`. Aplicada (`db:status`). - **Dominio/ports**: `Order.courier?: string | null`; `updateState(id, state, trackingNumber?, courier?)`; `transitionAdmin(id, next, trackingNumber?, courier?)`. - **Repositorio** (`pg-order-repository.ts`): persiste `courier` con `COALESCE($4, courier)` y lo mapea en `toOrder`. - **Servicio** (`order-service.ts`): `transitionAdmin` reenvía courier a `updateState`. - **Mailer** (`order-status-mailer.ts`): - `sendOrderStatusEmail` ahora acepta `courier`. - Extraído `buildOrderStatusEmail(input)` puro y reutilizable; tanto el email de texto como el HTML incluyen la línea `Transportista: X` junto a `Número de seguimiento: Y` cuando proceda. - **Rutas admin** (`orders.routes.ts`): - Esquema `/orders/:id/transitions/admin` ahora acepta `courier`. `state === 'SHIPPED'` exige tracking **y** courier (422 `COURIER_REQUIRED`). - El courier y el tracking se pasan al mailer; la entrada del historial incluye `Transportista` cuando aplica. - La ruta `/orders/:id/shipping` ahora acepta `courier` opcional para corregirlo tras enviar. - `serializeOrder` expone `courier` (default `null`). - **Ajustes** (`store-settings/api/settings.routes.ts`): - `couriers: string[]` (zod: array de 1..60, max 30). Guardado en `store_settings.shipping_couriers` como JSON. - `DEFAULT_COURIERS = ['Correos','SEUR','MRW','GLS','DHL','UPS']`; `parseCouriers` valida JSON, filtra no-strings, recorta espacios, limita a 30 y cae al default si falla. - **Bug fijado**: `apps/admin` apuntaba a `/orders/:id/transitions` (ruta de cliente, sin email ni tracking). Ahora apunta a `/orders/:id/transitions/admin`, que es el endpoint que realmente envía el email. ## Admin UI (apps/admin) - `lib/api-client.ts`: - `ordersApi.transition(id, state, trackingNumber?, courier?)` → `POST /api/orders/{id}/transitions/admin` con courier si está. - `ordersApi.updateShipping(id, trackingNumber, note?, courier?)`. - `StoreSettings.couriers?: string[]`. - `types/index.ts`: `Order.courier?: string | null`. - **Página de pedido** (`orders/[id]/page.tsx`): - Carga couriers desde `settingsApi.get()`. - Modal de confirmación SHIPPED: select de transportista (con aviso si no hay configurados) + tracking; ambos obligatorios. - Sección "Envío": select de transportista arriba del tracking. - El courier seleccionado se envía a `transitionAdmin` y a `updateShipping`. - **Ajustes** (`settings/page.tsx`): nueva pestaña "Transportistas" con textarea de uno por línea; al guardar, se envía el array a la API. ## Tests - `order-status-mailer.test.ts` (4): tracking+courier en SHIPPED, ausencia en PROCESSING, omisión con courier en blanco, escape HTML en courier/tracking. - `settings-couriers.test.ts` (5): parseCouriers con defaults, JSON válido, entradas no-string, JSON inválido, límite 30. - `order-service.test.ts` (+1): el repositorio recibe courier y trackingNumber en `transitionAdmin`. ## Evidencia - `npm run typecheck` (backend) OK. - `npm test`: 145 passed | 0 failed (de 135 previos; +10 nuevos). - `apps/admin`: `npx tsc --noEmit` OK. - ESLint sobre `src/modules/orders`, `src/modules/store-settings` y la migración 039: OK. - Migración 039 aplicada (`db:status`).