hbox-docs: entrada, incidencias, standup y agentes (27-oct); propuesta del cotizador

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013SfRSvDd335NPYEbBrDFGE
This commit is contained in:
Leopoldo Rodríguez 2026-10-27 14:24:08 -06:00
parent 1b940e8ff5
commit e1d04d5901
5 changed files with 309 additions and 3 deletions

View File

@ -0,0 +1,150 @@
---
title: "Instrucciones para el proyecto del cotizador: ligar órdenes de compra al centro de mando"
date: 2026-10-27
tags: [project-drogheda, cotizador, compras, inventario, integracion, instrucciones]
---
# Instrucciones para el proyecto del cotizador
**27-oct-2026 · v1.0.** Para la sesión de Claude que trabaja en `cotizador-helgen` (Apps Script + Sheets + Cloudflare).
Diseño completo y decisiones: [Cotizador ↔ centro de mando](integracion-cotizador.md). Aquí está **qué cambiar en el
cotizador**, en orden, con sus pruebas. Todo respeta las reglas del `CLAUDE.md` del cotizador (§7): totales en el servidor,
datos de compras solo en su libro, `CAMBIOS_API`, manual actualizado en el mismo turno, pruebas en PRUEBAS antes del real.
## Para pegar en el proyecto del cotizador
> Lee `CLAUDE.md` completo y después este documento (`2026-10-27_Drogheda_Instrucciones-Cotizador-Integracion_ES_v1.0.md`).
> Vamos a ligar las órdenes de compra de los proyectos ligados (hoy DROGHEDA, `PRY-2026-0002`) con el centro de mando
> drogheda.helgen.online, siguiendo el contrato de la sección «Contrato». Trabaja por pasos (1 a 7), con respaldos antes
> de tocar hojas, pruebas en Node y una ronda en PRUEBAS con `"prueba": true` antes de publicar. No mandes montos al centro
> de mando. No pongas el secreto en el código ni en el chat: va en las propiedades del script (`PORTAL_SECRETO`) y me lo
> pides a mí cuando haga falta. Al terminar, actualiza `CLAUDE.md` (§4.5.3, §4.5.4, §5, §6.12, §6.13 y una §6.15 nueva)
> y el Manual del Cotizador.
## Lo que decidió Leo (27-oct)
- El material se **recibe en el centro de mando** (el responsable del sitio da «Recibir»). El cotizador **cierra solo**
la requisición cuando el centro de mando dice que llegó todo. «Recibí el material» en el cotizador se queda como respaldo
para proyectos no ligados y para cuando el centro de mando no esté disponible.
- La autorización, los montos, los pagos y los proveedores **se quedan en el cotizador**.
- Cada OC de un proyecto ligado lleva un **sitio de entrega** de 3 letras: `VER` Veracruz (ICAVE) · `ZLO` Manzanillo
(TIMSA) · `ESE` Ensenada (EIT) · `LZC` Lázaro Cárdenas (LCT) · `QRO` Querétaro.
## Paso 1 · Propiedades del script
| Propiedad | Valor | Nota |
|---|---|---|
| `PORTAL_URL` | `https://drogheda.helgen.online` | |
| `PORTAL_SECRETO` | (lo genera Leo; el mismo que `COTIZADOR_SECRETO` en el servidor) | Nunca en el código, en `CLAUDE.md` ni en el chat |
| `PORTAL_PROYECTOS` | `{"PRY-2026-0002": "drogheda"}` | Llave interna del proyecto → proyecto del centro de mando |
| `PORTAL_SITIOS` | `VER,ZLO,ESE,LZC,QRO` | Con sus nombres en la interfaz (Veracruz · ICAVE, Manzanillo · TIMSA, …) |
Agregar `script.external_request` (ya existe por la IA de claves SAT si está activa) y `script.scriptapp` (para el
activador del paso 6) a `appsscript.json`: el dueño (efrain@) tendrá que volver a autorizar.
## Paso 2 · Sitio de entrega e ítem del inventario en la requisición
- `Requisiciones`: columna nueva **`sitio_entrega`** (al final, como se agregó `req_folio`; las filas viejas quedan vacías).
Obligatoria cuando el proyecto está en `PORTAL_PROYECTOS`; el servidor lo revisa en `guardarRequisicion` (no solo la
interfaz). En la interfaz: un selector con los 5 sitios, junto al proyecto.
- `RequisicionesRenglones`: columna nueva **`inventario_item`** (código del ítem, opcional). En la interfaz, cada renglón
de un proyecto ligado tiene un buscador con los ítems del centro de mando (`GET /api/integracion/inventario/items`,
guardado en `CacheService` 6 h). Se puede dejar vacío: el centro de mando lo liga después.
- `OrdenesCompra`: columnas nuevas **`sitio_entrega`** (se copia de la requisición al autorizar), **`paqueteria`**,
**`guia`**, **`fecha_estimada`**, **`en_camino_en`**, **`en_camino_por`**. Los renglones de la OC guardan el
`inventario_item` de su renglón de requisición (si compras divide o junta renglones, conserva el ítem).
## Paso 3 · Estado «En camino»
- Nueva función *(UI)* **`marcarEnCamino(folio, {paqueteria, guia, fechaEstimada})`**: la puede usar **compras** o **el
solicitante de esa requisición**. Solo desde Autorizada/Emitida o Pagada; guía obligatoria salvo «lo trae el proveedor»;
fecha estimada no anterior a hoy. Con candado e idempotencia (`ENC_<idSolicitud>`), como las demás.
- Requisición y OC pasan a **«En camino»** (estado nuevo entre Pagada y Recibida; también puede venir de Autorizada si se
paga después). El solicitante lo ve en su historial con la fecha, sin montos.
- Una OC «En camino» ya no se cancela desde el cotizador sin motivo explícito (avisa al centro de mando con `oc.cancelada`).
- `CAMBIOS_API`: `marcarEnCamino` cambia `requisiciones` y `oc`.
## Paso 4 · Avisos al centro de mando (empujar)
Función interna **`avisarPortal_(tipo, folioOC)`**: arma el cuerpo del contrato con los datos de la OC **sin montos**
(nada de costo, importe, subtotal, IVA, total, tipo de cambio ni margen), lo firma y lo manda con `UrlFetchApp.fetch`
(`muteHttpExceptions: true`). Se llama **después** de que el cambio quedó guardado:
| Momento | `tipo` |
|---|---|
| `autorizarRequisicion` (emite la OC) | `oc.autorizada` |
| `registrarPagoProveedor` | `oc.pagada` |
| `marcarEnCamino` | `oc.en_camino` |
| `cancelarOrdenCompra` | `oc.cancelada` |
| `cerrarRequisicionCompras` (compras cierra a mano) | `req.cerrada` |
- Solo si el proyecto de la OC está en `PORTAL_PROYECTOS`.
- `evento_id` = `<folio OC>:<tipo>:<n>` (n = cuántas veces se mandó ese tipo para esa OC), para que un reintento no duplique.
- Si la respuesta no es 200 `ok`, el aviso se guarda en una pestaña nueva **`PortalPendientes`** del libro de compras
(`evento_id`, `cuerpo`, `intentos`, `ultimo_error`, `creado`) y lo reintenta el activador del paso 6. La acción del
usuario **no** falla por eso: se le muestra «Guardado. El centro de mando se actualizará en unos minutos».
- Firma: `X-Hbox-Tiempo` = hora ISO con zona; `X-Hbox-Firma` = `sha256=` + hex de
`Utilities.computeHmacSha256Signature(tiempo + "\n" + cuerpo, PORTAL_SECRETO)`.
## Paso 5 · Recepciones (consultar y cerrar solo)
Función **`traerRecepcionesPortal()`**: `GET /api/integracion/cotizador/recepciones?desde=<PORTAL_ULTIMA_CONSULTA>`
(firmada igual, cuerpo vacío). Por cada recepción:
- `completa: true` → cierra la requisición con `cerrarReq_` (estado Cerrada, OC Recibida) y nota «Recibido en ZLO por
jesush en el centro de mando, 30-oct-2026». Si la requisición ya estaba Cerrada, no hace nada.
- `completa: false` → solo agrega una nota de recepción parcial («Llegaron 3 de 4 soportes») y deja la requisición En camino.
- Guarda `PORTAL_ULTIMA_CONSULTA` al final, solo si todo salió bien.
- Bitácora en cada cierre automático (quién: «centro de mando»).
## Paso 6 · Activador cada 15 minutos
`instalarActivadorPortal()` (se corre una vez desde el editor): `ScriptApp.newTrigger('cadaQuinceMinutos').timeBased()
.everyMinutes(15)`. `cadaQuinceMinutos` = reintentar `PortalPendientes` + `traerRecepcionesPortal`. Aprovecha para llenar el
caché (la idea de §6.14).
## Paso 7 · Pruebas y publicación
1. **Node** (`tests/portal.test.js`): el cuerpo nunca lleva montos; la firma coincide con un vector de prueba; sitio
obligatorio solo en proyectos ligados; `evento_id` estable; un 500 del portal deja el aviso en `PortalPendientes`;
recepción completa cierra, parcial solo anota, repetida no hace nada.
2. **PRUEBAS** con `"prueba": true`: requisición → autorizar → en camino → (Leo da «Recibir» en el centro de mando) →
la requisición se cierra sola en ≤ 15 min. El centro de mando guarda las de prueba aparte y no toca el inventario.
3. Respaldos de los libros (como siempre), `clasp push`, comparar el editor, `clasp deploy` (misma API_URL),
`wrangler deploy`.
4. Primera OC real de DROGHEDA: revisar juntos en el centro de mando (Compras y la pestaña del sitio en Inventario).
## Contrato (lo que espera el centro de mando)
```json
POST /api/integracion/cotizador/evento
{ "evento_id": "OC-2026-0012:oc.en_camino:1",
"tipo": "oc.en_camino",
"cuando": "2026-10-28T10:15:00-06:00",
"quien": "tania@helgen.tech",
"prueba": false,
"oc": { "folio": "OC-2026-0012", "req_folio": "REQ-2026-0031",
"proyecto_folio": "PRY-2026-0002", "proyecto_clave": "DRO",
"sitio": "ZLO", "proveedor": "Ferretería del Pacífico",
"fecha_entrega": "2026-10-30", "paqueteria": "Estafeta", "guia": "123456789",
"solicitante": "jesus@helgen.tech", "autorizo": "tania@helgen.tech",
"renglones": [ { "n": 1, "descripcion": "Soporte para RTG", "unidad": "pza", "cantidad": 4, "item": "SOP-RTG" },
{ "n": 2, "descripcion": "Tornillo 3/8 x 2 in", "unidad": "pza", "cantidad": 32, "item": null } ] } }
→ 200 { "ok": true, "estado_portal": "en-camino", "tarea_id": "t-oc-2026-0012", "por_ligar": [2] }
→ 200 { "ok": true, "repetido": true } (mismo evento_id)
→ 400 { "ok": false, "error": "Sitio inválido: MZO (usa VER, ZLO, ESE, LZC o QRO)" }
→ 401 { "ok": false, "error": "Firma inválida o vencida" }
GET /api/integracion/inventario/items
→ 200 { "ok": true, "items": [ { "codigo": "SOP-RTG", "descripcion": "Soporte para RTG", "unidad": "pza", "categoria": "instalacion" } ] }
GET /api/integracion/cotizador/recepciones?desde=2026-10-28T00:00:00-06:00
→ 200 { "ok": true, "hasta": "2026-10-30T12:40:00-06:00",
"recepciones": [ { "oc_folio": "OC-2026-0012", "req_folio": "REQ-2026-0031", "sitio": "ZLO", "completa": false,
"recibio": "jesush", "cuando": "2026-10-30T12:31:00-06:00",
"renglones": [ { "n": 1, "pedido": 4, "recibido": 4 }, { "n": 2, "pedido": 32, "recibido": 20 } ] } ] }
```
Hasta que el centro de mando publique estas rutas (fase 1 del lado del sitio), el cotizador puede programar y probar todo
contra un servidor simulado en Node con estas mismas respuestas.

View File

@ -0,0 +1,154 @@
---
title: "Cotizador ↔ centro de mando: órdenes de compra, inventario por sitio y tareas"
date: 2026-10-27
tags: [project-drogheda, hbox, compras, inventario, cotizador, integracion, sql]
estado: propuesta con 3 decisiones tomadas (27-oct); falta construir
---
# Cotizador ↔ centro de mando: órdenes de compra, inventario por sitio y tareas
**27-oct-2026 · v1.0 · Claude (CTO/CSO) con Leo.** Responde al pendiente `t-cotizador-integracion`: que la
requisición, la orden de compra y la llegada del material sean un solo hilo que termine en el inventario de cada sitio,
con su pendiente en el tablero. Las instrucciones para el proyecto del cotizador están en
[Instrucciones para el cotizador](integracion-cotizador-instrucciones.md).
![Flujo de una orden de compra del cotizador al inventario del sitio](img/2026-10-27_Drogheda_Flujo-OC-Inventario_v1.0.png)
## Resumen
1. **El cotizador se queda con el dinero y la autorización.** Ya tiene requisición → autorización de compras → orden de
compra (OC) → pago → «Recibí el material», con roles y montos protegidos. No se duplica en el sitio.
2. **El centro de mando recibe lo operativo de cada OC, sin montos:** folio, proyecto, **sitio de entrega** (código de 3
letras), proveedor, renglones con cantidad y su ítem del inventario, fechas, paquetería y guía.
3. **«En camino» es nuevo:** cuando el proveedor manda el material, compras o quien pidió lo confirma en el cotizador con
la guía. En ese momento el material aparece **en camino** en la pestaña del sitio del inventario.
4. **Se recibe en el sitio, en el centro de mando** (decisión de Leo, 27-oct): el responsable del sitio da **Recibir** con
lo que llegó de verdad; el cotizador se entera solo y cierra la requisición.
5. **Cada OC abre su pendiente** (pista Compras, su sitio, el responsable del sitio, vence en la fecha de entrega) y se
cierra solo cuando se recibe todo.
6. **Base de datos: SQLite** en el servidor del sitio (decisión de Leo, 27-oct), empezando por las órdenes de compra; el
inventario pasa después, con una prueba de que los números no cambian.
## Decisiones
| # | Decisión | Estado |
|---|---|---|
| D-27-1 | El material de una OC se **recibe en el centro de mando** (pestaña del sitio, botón Recibir). El cotizador cierra la requisición solo. | **Tomada** (Leo, 27-oct) |
| D-27-2 | Base de datos **SQLite** en el servidor del sitio (`/srv/projects/drogheda/drogheda.db`). PostgreSQL solo si otra aplicación tiene que escribir directo en ella. | **Tomada** (Leo, 27-oct) |
| D-27-3 | Código de 3 letras por sitio = el de su aeropuerto y su puerto (UN/LOCODE): **VER** Veracruz (ICAVE) · **ZLO** Manzanillo (TIMSA) · **ESE** Ensenada (EIT) · **LZC** Lázaro Cárdenas (LCT) · **QRO** Querétaro. Son los que vienen en guías y documentos de embarque; el sitio ya los acepta. | Propuesta de Claude (Leo pidió códigos de 3 letras) |
| D-27-4 | La **autorización** de la OC se queda en el cotizador (rol de compras). El centro de mando **nunca** muestra montos: todo el equipo lo ve. | Propuesta |
| D-27-5 | Solo las OC de proyectos ligados van al centro de mando. Hoy: DROGHEDA = `PRY-2026-0002-DRO` → proyecto `drogheda`. | Propuesta |
## Lo que hay hoy
| | Cotizador | Centro de mando (drogheda.helgen.online) |
|---|---|---|
| Dónde vive | Google Apps Script + Sheets + Drive; interfaz en Cloudflare; entra con cuenta @helgen.tech | Servidor de Helgen (Flask + nginx), página de entrada propia desde el 27-oct |
| Proyecto | `PRY-2026-0002-DRO` «Helgen — DROGHEDA», MXN, sin contrato | `drogheda` |
| Requisición | `REQ-AAAA-NNNN`: Solicitada → Autorizada → Pagada → Cerrada (también Devuelta, Rechazada, Cancelada). Piden sin precios: adriana@, miguel@, paola@, jesus@ (solo requisiciones) y efrain@, tania@, leo@ | — |
| Orden de compra | `OC-AAAA-NNNN`: Emitida → Pagada → Recibida, o Cancelada. Sale siempre de una requisición autorizada. **Sin recepciones parciales** | — |
| Sitio de entrega | No existe (solo «lugar de entrega» en texto) | `icave` · `timsa` · `eit` · `lzc` · `qro` en cada pendiente, movimiento y miembro |
| Material | Renglones de texto libre (descripción, unidad, cantidad) | Ítems con código, existencia por sitio, envíos «en camino» con Recibir (también parcial), kit por instalación |
| Tareas | — | Pendientes con pista, sitio, responsable y vencimiento |
**Lo que falta para unirlos:** el sitio de entrega en la requisición, el ítem del inventario en cada renglón, el estado
«En camino», un canal entre los dos y una forma de cerrar la requisición desde la recepción.
**Una restricción que define el diseño:** el cotizador puede llamar al centro de mando (Apps Script tiene `UrlFetchApp`),
pero el centro de mando **no** puede llamar al cotizador, porque cada llamada al cotizador exige una sesión de Google de
una persona (su regla 10, que no se quita). Por eso: el cotizador **avisa** (empuja) cada cambio de una OC, y **consulta**
cada 15 minutos lo que ya se recibió.
## El flujo, paso por paso
| Paso | Dónde | Quién | Qué pasa |
|---|---|---|---|
| 1. Pedir | Cotizador | Quien necesita el material | Requisición en DROGHEDA con **sitio de entrega** (obligatorio en proyectos ligados) y renglones; cada renglón puede llevar su **ítem del inventario** (lista que el cotizador baja del centro de mando). |
| 2. Autorizar | Cotizador | Compras (Efraín, Tania, Leo) | Emite la OC como hoy. Aviso `oc.autorizada` → el centro de mando crea la OC (sin montos) y el pendiente «Recibir OC-… en ZLO» para el responsable del sitio, que vence en la fecha de entrega. |
| 3. Pagar | Cotizador | Compras | Como hoy. Aviso `oc.pagada` (solo el estado). |
| 4. En camino | Cotizador | Compras o quien pidió | «El proveedor ya lo mandó»: paquetería, guía y fecha estimada. Aviso `oc.en_camino` → en la pestaña del sitio aparece cada renglón **en camino** (un envío desde «Proveedor o fuera del inventario», con el folio de la OC como referencia). Los renglones sin ítem quedan en **Compras → por ligar** del centro de mando. |
| 5. Recibir | Centro de mando | Responsable del sitio (Alán VER, Jesús Honorato ZLO, Miguel ESE, Antonio LZC, Jesús Romero QRO) | **Recibir** con lo que llegó de verdad (puede ser parcial). La OC queda «recibida» o «recibida en parte»; el pendiente se cierra solo cuando llega todo. |
| 6. Cerrar | Cotizador (solo) | — | Cada 15 min el cotizador consulta las recepciones: si llegó todo, cierra la requisición («Recibido en ZLO por jesush en el centro de mando, 30-oct») y la OC queda Recibida. Si llegó una parte, lo anota en las notas de recepción. |
| Cancelar | Cotizador | Compras | Aviso `oc.cancelada` → el centro de mando cancela los envíos abiertos y el pendiente. |
## Datos (SQLite)
Un archivo, `/srv/projects/drogheda/drogheda.db`, en modo WAL (aguanta a los dos procesos del servicio). Se respalda
con el resto de `/srv/projects/drogheda`.
```sql
CREATE TABLE sitios (codigo TEXT PRIMARY KEY, -- VER ZLO ESE LZC QRO
id_portal TEXT UNIQUE NOT NULL, -- icave timsa eit lzc qro
nombre TEXT NOT NULL, responsable TEXT);
CREATE TABLE proyectos (id TEXT PRIMARY KEY, -- drogheda
pry_folio TEXT UNIQUE NOT NULL, -- PRY-2026-0002
clave TEXT, nombre TEXT);
CREATE TABLE ordenes (folio TEXT PRIMARY KEY, -- OC-2026-0012
req_folio TEXT, proyecto_id TEXT REFERENCES proyectos(id),
sitio TEXT REFERENCES sitios(codigo), proveedor TEXT,
estado TEXT CHECK (estado IN ('autorizada','pagada','en-camino','recibida-parcial','recibida','cancelada')),
fecha_entrega TEXT, paqueteria TEXT, guia TEXT, en_camino_en TEXT,
solicitante TEXT, autorizo TEXT, tarea_id TEXT, prueba INTEGER DEFAULT 0,
creado_en TEXT, actualizado_en TEXT);
CREATE TABLE renglones (oc_folio TEXT REFERENCES ordenes(folio), n INTEGER,
descripcion TEXT, unidad TEXT, cantidad REAL,
item_codigo TEXT, -- ítem del inventario (NULL = por ligar)
envio_id TEXT, -- el envío «en camino» que se creó en el inventario
recibido REAL DEFAULT 0, PRIMARY KEY (oc_folio, n));
CREATE TABLE eventos (evento_id TEXT PRIMARY KEY, -- idempotencia: el mismo aviso dos veces no duplica
oc_folio TEXT, tipo TEXT, cuando TEXT, quien TEXT,
origen TEXT CHECK (origen IN ('cotizador','portal')), datos TEXT);
```
**Fases.** 1) Órdenes de compra en SQLite; el inventario sigue en sus JSON y la OC crea sus envíos con la API de hoy.
2) El inventario (ítems y movimientos) pasa a SQLite con una comparación de existencias por sitio antes y después.
3) **Activos**: una fila por unidad y por pieza con serie (HBOX, tarjetas, módulos) con su sitio, estado, vehículo y actas
— lo que hoy se arma leyendo las actas.
## Cómo se hablan (API del centro de mando para el cotizador)
**Seguridad.** Las rutas `/api/integracion/…` no usan la sesión de una persona: cada llamada va firmada con un secreto
compartido (HMAC-SHA256 del cuerpo y la hora). El secreto vive en las propiedades del script del cotizador
(`PORTAL_SECRETO`) y en el servicio del servidor (`COTIZADOR_SECRETO`); **nunca en el código ni en un chat**. Se rechaza
una firma de hace más de 5 minutos.
```
POST /api/integracion/cotizador/evento
Encabezados: X-Hbox-Tiempo: 2026-10-28T10:15:00-06:00
X-Hbox-Firma: sha256=<hex de HMAC(secreto, tiempo + "\n" + cuerpo)>
Cuerpo:
{ "evento_id": "OC-2026-0012:oc.en_camino:1",
"tipo": "oc.en_camino", // oc.autorizada · oc.pagada · oc.en_camino · oc.cancelada · req.cerrada
"cuando": "2026-10-28T10:15:00-06:00", "quien": "tania@helgen.tech", "prueba": false,
"oc": { "folio": "OC-2026-0012", "req_folio": "REQ-2026-0031",
"proyecto_folio": "PRY-2026-0002", "proyecto_clave": "DRO", "sitio": "ZLO",
"proveedor": "Ferretería del Pacífico", "fecha_entrega": "2026-10-30",
"paqueteria": "Estafeta", "guia": "123456789", "solicitante": "jesus@helgen.tech",
"renglones": [ { "n": 1, "descripcion": "Soporte para RTG", "unidad": "pza", "cantidad": 4, "item": "SOP-RTG" } ] } }
Respuesta: { "ok": true, "estado_portal": "en-camino", "tarea_id": "t-oc-2026-0012", "por_ligar": [] }
(el mismo evento_id otra vez → { "ok": true, "repetido": true })
GET /api/integracion/inventario/items → [{codigo, descripcion, unidad, categoria}] (para el selector)
GET /api/integracion/cotizador/recepciones?desde=… → [{oc_folio, req_folio, sitio, completa, recibio, cuando,
renglones: [{n, pedido, recibido}]}]
```
`"prueba": true` (rondas del proyecto de PRUEBAS del cotizador) se guarda aparte y **no** toca el inventario ni el tablero.
## Qué se construye y quién
| Lado | Qué | Quién |
|---|---|---|
| Centro de mando | `backend/compras.py` (SQLite, las 3 rutas firmadas, envíos en camino, pendiente por OC, cierre al recibir); página **Compras** (OC por sitio y estado, renglones por ligar, liga desde el envío en camino del inventario); ruta en nginx sin sesión para `/api/integracion/` (la firma la revisa el backend); pruebas | Claude (fase 1), Leo despliega |
| Cotizador | Sitio de entrega y su regla; ítem por renglón; estado «En camino»; avisos firmados con reintento; consulta de recepciones cada 15 min y cierre automático; pruebas en Node y en PRUEBAS | El proyecto del cotizador (Claude con Efraín) — ver [instrucciones](integracion-cotizador-instrucciones.md) |
| Los dos | El secreto compartido (Leo lo genera y lo pone en los dos lados) y una prueba de punta a punta con una OC de PRUEBA | Leo |
## Riesgos y cómo se cubren
- **El centro de mando no contesta** cuando el cotizador avisa → el cotizador guarda el aviso en una pestaña de
pendientes y lo reintenta; el `evento_id` evita duplicados.
- **Una OC llega antes que su proyecto o su sitio** → se rechaza con un error claro (sitio inválido, proyecto no ligado)
y el cotizador lo muestra a compras.
- **Doble captura durante la transición** → las OC de antes no se migran; solo las nuevas de proyectos ligados.
- **Fuga del secreto** → se cambia en los dos lados; las firmas viejas dejan de valer al momento.
- **Montos** → el centro de mando no los recibe; si un aviso los trae, se descartan antes de guardar.

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 5c937e44bdbe1deeb52f6404b6d9458f46a90112 Subproject commit 742e534af20f8d78c50c55ea9c5eccc6d707a746