# 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`).
