Drogheda/02-Architecture-Software/2026-10-27_Drogheda_Instrucciones-Cotizador-Integracion_ES_v1.0.md

151 lines
9.9 KiB
Markdown

---
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.