# Cotizador Helgen — contexto para Claude Code Léelo completo antes de cambiar algo. Resume el sistema, dónde viven los datos y las decisiones ya tomadas (con su porqué) para no repetir errores. Textos de la interfaz y comentarios del código: **español de México**. ## 1. Qué es App web interna de **Helgen Servicios Especializados** para hacer cotizaciones. Corre en **Google Apps Script** (sin servidor propio), solo para cuentas `@helgen.tech`. Flujo: 1. El usuario llena el formulario (cliente, conceptos, notas, línea de negocio Helgen / Helgen Energy). 2. El servidor **recalcula** subtotal, IVA y total, asigna un **folio único** `HLG-AAAA-0001` y genera el **PDF**. 3. El PDF se guarda en Drive (carpeta por año) y la cotización queda como una fila en Google Sheets. Además: historial con búsqueda, catálogo de productos con precios (Google Sheets aparte), clientes sugeridos con autorrelleno, y claves SAT internas por producto para contabilidad (con sugerencias por palabras o con IA). **Helgen Energy** es la submarca de paneles solares: mismo folio y diseño, pero sus propios Términos y Condiciones. ## 2. Datos clave (IDs y URLs) | Qué | Valor | |---|---| | URL de la app (la usa el equipo) | `https://script.google.com/a/macros/helgen.tech/s/AKfycbxahS3PbizPrRrQIC1m2uz-2G8PoT64BqPdtGLu0Ms5t9RKs2ucxH_c3tpJXkj8uLE/exec` | | Deployment publicado (siempre actualizar este) | `AKfycbxahS3PbizPrRrQIC1m2uz-2G8PoT64BqPdtGLu0Ms5t9RKs2ucxH_c3tpJXkj8uLE` (versión @9 al 19/10/2026) | | Proyecto Apps Script (`scriptId` en `.clasp.json`) | `1X-qSKgvqKwuDj4iPZBhXlakR5udau9ibVMevhwvlLXgtSkjV4IoQxqwU` — "Cotizador Helgen (código de la app)" | | Carpeta maestra en Drive "Cotizador Helgen" | `1lf3Z4d3e_oqFbYJse4QbyYkWowYT9HCo` | | Base de datos "Cotizador Helgen — Base de datos" (`SPREADSHEET_ID`) | `1ozFsyt1OjSnSv-PVpRWZjgqeWt6voQREbjvOJAXPEaI` | | Catálogo "Catálogo Helgen" (`CATALOGO_ID`) | `11Rr0G7ZHE8PqXWsw9OpPPfGLQx8Yz7g8h1wkjJY2ahQ` | | Carpeta PDF "Cotizaciones Helgen (PDF)" (`FOLDER_ID`) | `1CDN1LyOlA1L6z5dvBp49tlqu0fDoxdX8` | | "Catálogos SAT (CFDI 4.0)" (`SAT_ID`) | `1B2t9tXuEj1i9JO9ybqsuH4PN1K47CqMZgoEBzd-Dmc8` (subcarpeta "Catálogos SAT" `15OYcUp41onVJoW7--b3gnLZY8ymvYbUZ`) | | Interfaz web en Cloudflare (PRUEBAS) | `https://cotizador-helgen.cotizador-helgen.workers.dev/cotizador/` (portada en `/`). IDs de PRUEBAS en §10 | | Dueño de todo / cuenta de clasp | `efrain@helgen.tech` (la app se ejecuta como esta cuenta) | Los IDs reales que usa el código están en **Script Properties** (ver §5). Esta tabla es solo referencia. ## 3. Archivos del proyecto (carpeta local `~/Downloads/cotizador-helgen`) | Archivo | Para qué | |---|---| | `Code.gs` | Todo el backend (ver §4). Se sube a Apps Script. | | `Index.html` | Interfaz completa (HTML + CSS + JS en un archivo). Plantilla de HtmlService. Se sube. | | `Logo.gs` | Logos en base64: `LOGO_HELGEN_AZUL` (PDF, fondo blanco, 700×200) y `LOGO_HELGEN_BLANCO` (encabezado azul de la app). Se sube. | | `appsscript.json` | Manifiesto: zona `America/Mexico_City`, V8, web app `executeAs: USER_DEPLOYING`, `access: DOMAIN`. Se sube. | | `.clasp.json` | Liga la carpeta al proyecto `1X-qSKgv…`. **No cambiar el scriptId.** | | `.claspignore` | Lista blanca: solo se suben `Code.gs`, `Logo.gs`, `Index.html`, `appsscript.json`. | | `tests/` | Pruebas en Node con servicios de Google simulados (ver §8). No se suben. `sesion.test.js` es de PRUEBAS (Fase 2, §10). | | `herramientas/extraer-catalogos-sat.js` | Convierte el Excel oficial del SAT en los CSV que importa la app (ver §6.5). | | `GUIA.md` | Guía para personas (instalación inicial). Partes desactualizadas: hoy se usa `npx clasp` y `setup()` corre solo. | | `web/` | Portada con la marca (`index.html`, `logo-helgen-blanco.png` extraído de `Logo.gs`); su botón lleva a `web/cotizador/index.html`: la interfaz completa con una capa que imita `google.script.run` por `fetch` al `doPost` de PRUEBAS (`API_URL`). El código de pruebas (`Code.js` con `doPost`/`API_FUNCIONES`) vive solo en el proyecto de PRUEBAS, no en esta carpeta. `.assetsignore` excluye `Icon`/`.DS_Store`. No se sube a Apps Script. Ver §10. | | `wrangler.jsonc` | Publica `web/` en Cloudflare como Worker solo de archivos estáticos (ver §10). | | `package.json` | Solo `@google/clasp` como dependencia de desarrollo. | | `Icon` | Archivo vacío de macOS; ignorarlo. | No hay git inicializado en la carpeta. ## 4. Funciones de `Code.gs` Convención: las que terminan en `_` son internas; las demás las llama la interfaz con `google.script.run` o son lógica pura (sección 5 del archivo, sin servicios de Google, probables en Node). ### 4.1 Entrada e instalación - `doGet()` — sirve `Index.html` como plantilla (`createTemplateFromFile(...).evaluate()`). - `setup()` — crea la base de datos (pestañas `Cotizaciones` y `Config`) y la carpeta de PDF, y guarda sus IDs. **No hace falta correrla a mano:** `db_()` la ejecuta sola la primera vez (bajo candado). - `db_()` — abre la base de datos; si faltan `SPREADSHEET_ID`/`FOLDER_ID`, corre `setup()`. - `getConfig_()` — lee `Config` como objeto; antes llama `migrarConfig_()`. - `migrarConfig_(hoja)` — actualiza hojas `Config` viejas: `TERMINOS` → `TERMINOS_HELGEN` + `TERMINOS_ENERGY`, agrega claves nuevas de `CONFIG_INICIAL`, cambia valores de ejemplo (`CONFIG_EJEMPLO_ANTERIOR`) por los reales y repara celdas `#ERROR!`. ### 4.2 Catálogo de productos - `hojaCatalogo_()` — abre "Catálogo Helgen"; si no existe, `crearCatalogo_()` (bajo candado). Llama `asegurarColumnasSat_()`. - `crearCatalogo_()` — crea el archivo con formato, validaciones (clave única con `COUNTIF`, precio ≥ 0), migra la pestaña `Catalogo` vieja de la base de datos (sin el ejemplo `EJ-001`, luego la borra) y lo mueve junto a la base de datos. - `asegurarColumnasSat_(hoja)` — agrega encabezados `Clave SAT`/`Unidad SAT` (G:H, formato texto) a catálogos viejos. - `getCatalogo_(hoja?)` — lista `{clave, descripcion, unidad, precio, claveSat, unidadSat}`. - `getCatalogo()` *(UI)* — productos + URL del archivo + unidades SAT para la lista de selección. - `guardarProducto(p)` *(UI)* — alta o edición (`p.claveOriginal` = edición, permite cambiar la clave). Valida con `normalizarProducto`, rechaza claves duplicadas (sin distinguir mayúsculas) y claves/unidades SAT que no existan. ### 4.3 Catálogos del SAT - `libroSat_()` — abre "Catálogos SAT (CFDI 4.0)"; si no existe, `importarCatalogosSat_()`. - `importarCatalogosSat_()` — busca en Drive `SAT_c_ClaveProdServ.csv` y `SAT_c_ClaveUnidad.csv`, crea el libro con pestañas `ClaveProdServ`, `ClaveUnidad` y `Fuente`, y lo deja en la carpeta de los CSV. - `getUnidadesSat_()` — `[[clave, nombre], …]` de unidades. - `sugerirClavesSat(texto)` *(UI)* — si el texto es una clave de 7–8 dígitos la busca exacta; si no, toma ~40 candidatos (`candidatosSat`) y, si existe `ANTHROPIC_API_KEY`, la IA elige las 5 mejores con razón (`ordenarConIaSat_`); si no hay key o falla, devuelve los 8 mejores por palabras. - `ordenarConIaSat_(texto, candidatos)` — llama a la API de Claude (`UrlFetchApp`), guarda el resultado 6 h en caché; `null` si falla. - `probarIaSat()` — correr desde el editor: autoriza `UrlFetchApp` y muestra en el registro sugerencias de ejemplo. - `existeEnSat_(pestana, clave)` — valida una clave con `TextFinder` (celda completa). ### 4.4 Datos para la interfaz - `getDatosIniciales()` *(UI, al cargar)* — IVA, vigencia, marcas, catálogo, clientes, usuario. - `getClientes()` *(UI)* — clientes que ya cotizaron, calculados desde la hoja `Cotizaciones` (`clientesDesdeFilas`). - `listarCotizaciones(busqueda)` *(UI)* — últimas 100 cotizaciones, filtro por folio/cliente/empresa. ### 4.5 Guardar cotización - `guardarCotizacion(payload)` *(UI)* — normaliza, agrega clave/unidad SAT a cada concepto que venga del catálogo (si falla, guarda igual), toma el candado, revisa idempotencia (`payload.idSolicitud` en `CacheService`, 6 h), asigna folio, genera PDF, lo guarda en la carpeta del año y agrega la fila. Devuelve `{folio, url, total}`. - `siguienteFolio_(prefijo, hojaCot)` — `max(CONSECUTIVO_AAAA, mayor folio del año en la hoja) + 1`. - `carpetaDelAnio_(anio)` — subcarpeta `2026`, `2027`… dentro de la carpeta de PDF (la busca **por nombre**). - `logoDataUrl_(fileId)` — logo de Drive si `LOGO_FILE_ID` tiene valor; si no, `LOGO_HELGEN_AZUL`. ### 4.6 Lógica pura (sin Google; se prueba en Node) - `redondear(n)`, `formatoMoneda(n)`, `escaparHtml(s)`, `importeConLetra(n)` ("MIL CIENTO SESENTA PESOS 02/100 M.N."). - `normalizarCotizacion(p, ivaTasa)` — limpia cliente/conceptos, exige nombre y ≥1 concepto, calcula importes y totales, `marca`. - `formatearFolio`, `maxConsecutivo(folios, prefijo, anio)`. - `clientesDesdeFilas(filas)` — agrupa por nombre+empresa normalizados (sin acentos/mayúsculas/espacios), la fila más reciente manda, un dato vacío no borra uno anterior; `etiqueta` = nombre, o "nombre — empresa" si el nombre se repite. - `normalizarProducto(p)`, `indiceDeClave(claves, clave)`. - `terminosParaPdf(cfg, marca)` — renglones de T&C de la marca; agrega la línea de vigencia si falta. - `textoCelda(s)` — antepone `'` a texto que empiece con `= + - @` para que Sheets no lo lea como fórmula. - SAT: `palabrasSat`, `SINONIMOS_SAT`, `PALABRAS_SERVICIO_SAT`, `buscarClavesSat(consulta, filas, n)`, `candidatosSat`, `solicitudIaSat`, `leerRespuestaIaSat` (solo acepta claves que estaban entre los candidatos). Modelo: `MODELO_IA_SAT`. - `construirHtmlPdf(d, cfg, logoUrl)` — HTML del PDF (ver reglas 3–4). ### 4.7 `Index.html` (interfaz) Tres pestañas: **Nueva cotización**, **Historial**, **Catálogo**. Funciones principales: `agregarFila`, `leerItems`, `recalcular` (vista previa; el servidor recalcula), `guardar`, `limpiar`, `cargarHistorial`, `recibirClientes`, `autorrellenarCliente`, `pintarDatalists`, `cargarCatalogo`, `pintarCatalogo`, `editarProd`, `guardarProd`, `sugerirSat`, `verTab`. Autocompletado con ``: clientes (en "Nombre"), productos por descripción y por clave, unidades SAT. ## 5. Dónde se guardan los datos **Todo en la carpeta maestra "Cotizador Helgen"** de Mi unidad: ``` 📁 Cotizador Helgen ├─ 📊 Cotizador Helgen — Base de datos (Cotizaciones, Config) ├─ 📊 Catálogo Helgen (Catalogo) ├─ 📁 Cotizaciones Helgen (PDF) / 📁 2026 / HLG-2026-0001 - Cliente.pdf ├─ 📁 Catálogos SAT / SAT_c_ClaveProdServ.csv, SAT_c_ClaveUnidad.csv, 📊 Catálogos SAT (CFDI 4.0) └─ 📜 Cotizador Helgen (código de la app) ``` **Pestaña `Cotizaciones`** (16 columnas, 1 fila por cotización): Folio, Fecha, Cliente, Empresa, RFC, Correo, Teléfono, Subtotal, IVA, Total, Elaboró, PDF (link), Conceptos (JSON: clave, descripcion, unidad, cantidad, precio, importe y, si vienen del catálogo, claveSat/unidadSat), Notas, Marca (Helgen / Helgen Energy), Dirección. Las columnas Marca y Dirección se agregaron después: `guardarCotizacion` reescribe la fila de encabezados si no coincide con `ENCABEZADOS_COTIZACIONES`. En PRUEBAS la hoja tiene 19 columnas: además `vendedor_email`, `vendedor_nombre`, `vendedor_apellido` (Fase 2, §10.5). **Pestaña `Config`** (Clave | Valor, columna B en formato texto): `EMPRESA_NOMBRE` (Helgen Servicios Especializados), `EMPRESA_RFC` (HSE240304PC1), `EMPRESA_DIRECCION`, `EMPRESA_TELEFONO` (+52 442 644 5391), `EMPRESA_EMAIL`, `EMPRESA_WEB`, `PREFIJO_FOLIO` (HLG), `IVA_TASA` (0.16), `VIGENCIA_DIAS` (15), `MONEDA`, `LOGO_FILE_ID` (vacío = logo de `Logo.gs`), `TERMINOS_HELGEN`, `TERMINOS_ENERGY` (un término por renglón dentro de la celda). **"Catálogo Helgen" / pestaña `Catalogo`**: Clave (única, mayúsculas), Descripción, Unidad, Precio unitario (sin IVA), Actualizado, Actualizó, Clave SAT, Unidad SAT. A:C y G:H en formato texto. **Script Properties** (Configuración del proyecto en el editor): `SPREADSHEET_ID`, `FOLDER_ID`, `CATALOGO_ID`, `SAT_ID`, `CONSECUTIVO_AAAA` (p. ej. `CONSECUTIVO_2026`), `ANTHROPIC_API_KEY` (opcional; activa la IA para claves SAT). Nunca poner la API key en el código. ## 6. Decisiones tomadas (y por qué) ### 6.1 Folios sin duplicados - Todo el guardado va dentro de `LockService.getScriptLock()` con `tryLock(30000)` y mensaje en español si se agota. - El folio sale del **mayor** entre el contador y el último folio de la hoja: si alguien borra o edita `CONSECUTIVO_AAAA`, no se repite. - **Idempotencia:** el formulario manda `idSolicitud` (UUID por cotización, se renueva al limpiar); un doble clic o reenvío devuelve el mismo folio. - Borrar una fila no hace retroceder el contador (intencional). El consecutivo reinicia cada año. ### 6.2 Instalación y migraciones automáticas - La app se instala sola (`db_()` → `setup()`, `hojaCatalogo_()` → `crearCatalogo_()`, `libroSat_()` → `importarCatalogosSat_()`), porque correr `setup()` a mano falló la primera vez. - Toda estructura nueva se migra sola al abrir la app (Config, columnas del catálogo, encabezados de Cotizaciones). Al agregar columnas o claves, seguir este patrón: **nunca pedir al usuario que edite la hoja a mano**. ### 6.3 PDF (lecciones del conversor HTML→PDF de Google) - **No imprime colores de fondo** (`background`): con la banda azul, el logo blanco y el texto blanco desaparecieron. Por eso el PDF usa texto azul, líneas y bordes; `bgcolor` solo como extra opcional. - No soporta flex/grid: solo tablas e inline CSS. Fuente del PDF: Arial (las web fonts no son confiables ahí). - Logo azul en el PDF; el blanco solo sobre fondo azul. - No hay forma local de ver el PDF real: para revisar, renderizar el HTML **quitando los fondos** (simula al conversor). ### 6.4 Interfaz - En la plantilla, imágenes `data:` se insertan con ``: `` las bloquea por seguridad. - Fuentes de Google (League Spartan, Roboto Mono) sí funcionan en la app. - Máximo 3 módulos por vista. Mensajes de error en español. ### 6.5 Catálogo y SAT - El catálogo es un **archivo aparte** para poder compartirlo con quien mantiene precios sin exponer el historial. - La **clave SAT no reemplaza la clave interna**: muchas claves internas comparten una clave SAT. Es dato interno de contabilidad: **no sale en el PDF**; se valida contra el catálogo vigente y viaja en el JSON de conceptos. - Catálogo SAT: `catCFDI_V_4_20261015.xls` del Anexo 20 (`http://omawww.sat.gob.mx/tramitesyservicios/Paginas/anexo_20.htm`; el sitio rechaza HTTPS desde aquí, usar HTTP con `curl`). 52,513 claves de producto/servicio y 2,418 unidades vigentes. - **Actualizar el catálogo SAT:** descargar el `.xls` nuevo, correr `node herramientas/extraer-catalogos-sat.js ` (requiere `npm i xlsx@0.18.5` en una carpeta temporal), reemplazar los CSV en la subcarpeta "Catálogos SAT", **borrar la propiedad `SAT_ID`** y abrir la pestaña Catálogo. - La búsqueda por palabras funciona bien para productos (panel solar → 26111607 / 32111701 / 60104702; inversor → 32121705; cargador VE → 25175002) y mal para servicios genéricos; para eso existe la IA opcional. Ampliar `SINONIMOS_SAT` si una búsqueda falla. - **No inventar claves SAT de memoria:** una vez se afirmó que 26111604 era "Paneles solares" y es "Generadores a gas". Verificar siempre contra el CSV/hoja. Contabilidad debe validar toda clave sugerida. ### 6.6 Clientes sugeridos - No hay lista de clientes aparte: se calcula cada vez desde `Cotizaciones`. Si se borran sus filas, el cliente deja de sugerirse (lo pidió el usuario). ### 6.7 Drive - La app usa **IDs, no rutas**: se pueden mover o renombrar archivos. Rompe: copiar-y-borrar (cambia el ID), borrar, renombrar las pestañas `Cotizaciones`/`Config`, sacar o renombrar las subcarpetas de año, perder acceso de edición de efrain@helgen.tech. - **Peligro:** la carpeta local `~/Downloads` (incluida esta carpeta del proyecto) se sincroniza a Drive en una carpeta "Downloads" (`1BBB4vyr9WW1TowC6Uemtd3K6V7_NitAh`). No mover ni borrar esas copias desde Drive: afectaría la carpeta local. - Google Drive para escritorio está montado en `~/Library/CloudStorage/GoogleDrive-efrain@helgen.tech/Mi unidad/`: sirve para dejar archivos en Drive (así se subieron los CSV del SAT). ### 6.8 clasp - clasp está instalado **local** en el proyecto (instalar global pedía permisos de administrador): usar siempre `npx clasp`. - **No volver a correr `clasp create`**: crea otro proyecto y sobrescribe `appsscript.json` (pone zona de Nueva York y quita la configuración de web app). Ya pasó y hubo que borrar un proyecto duplicado. - Publicar siempre sobre el mismo deployment (misma URL para el equipo). Nunca `clasp deploy` sin `--deploymentId`. - Si se agrega un servicio nuevo de Google o `UrlFetchApp`, el dueño debe autorizar el permiso una vez desde el editor. ## 7. Reglas del proyecto (no romper) 1. Los totales SIEMPRE se recalculan en el servidor (`normalizarCotizacion`). El navegador solo muestra una vista previa. 2. Redondeo a 2 decimales por renglón (`redondear`). IVA = subtotal × tasa. 3. PDF con **tablas e inline CSS**, sin flex/grid y **sin depender de colores de fondo** (§6.3). 4. Todo texto del usuario que va al PDF pasa por `escaparHtml`. 5. Colores de marca (Brand Guidelines de Helgen en Canva): azul `#2C3C5B` y aqua `#51F3DA` (primarios), teal `#0AC5A8` (secundario), negro/blanco; fondo neutro `#F2F4F7`. Tipografías: League Spartan Bold (títulos), Roboto Mono (texto). No usar los colores viejos `#0B1C3F` / `#4CAF50`. 6. Toda cotización imprime Términos y Condiciones según la línea de negocio: - **Helgen** (`TERMINOS_HELGEN`): solo "Precios vigentes por 15 días o hasta agotar existencias, sujetos a variaciones en costos de suministro de componentes." - **Helgen Energy** (`TERMINOS_ENERGY`): metraje adicional, centro de carga, obra civil + vigencia. No agregar términos por cuenta propia (se quitó uno de "fuera de alcance" que el usuario no quería). 7. Texto del usuario que va a Sheets pasa por `textoCelda` (un "+52…" se leería como fórmula → `#ERROR!`). 8. Textos de la interfaz en español de México. 9. Las claves SAT son internas: nunca en el PDF. ## 8. Cómo trabajar ```bash # Pruebas (Node, servicios de Google simulados). Correr las tres antes de subir. mkdir -p /tmp/salida-pdf && node tests/cotizacion.test.js Code.gs /tmp/salida-pdf # folios, idempotencia, marcas, Config, clientes; deja helgen.html / energy.html node tests/catalogo.test.js . # catálogo: migración, alta, edición, duplicados node tests/sat.test.js . "$HOME/Library/CloudStorage/GoogleDrive-efrain@helgen.tech/Mi unidad/Cotizador Helgen/Catálogos SAT" node tests/probar-busqueda-sat.js Code.gs "/SAT_c_ClaveProdServ.csv" # ver qué sugiere la búsqueda por palabras node tests/sesion.test.js # Fase 2: token en doPost, vendedor_* (ver §10) # Subir y publicar (misma URL) npx clasp push --force npx clasp deploy --deploymentId AKfycbxahS3PbizPrRrQIC1m2uz-2G8PoT64BqPdtGLu0Ms5t9RKs2ucxH_c3tpJXkj8uLE -d "descripción del cambio" npx clasp list-deployments # Interfaz web en Cloudflare: ver §10 npx --yes wrangler@4 deploy ``` - Antes de `push`, confirmar que `.clasp.json` apunta a `1X-qSKgv…`. - Revisar sintaxis del JS de la interfaz: extraer el `