Agregar El Salvador y actualizar dist validado

This commit is contained in:
2026-09-07 15:14:41 -04:00
parent 043a442639
commit 2495256856
22 changed files with 3387 additions and 301 deletions
+13 -3
View File
@@ -1,9 +1,8 @@
# Portal multinacional - Cruce de Cuentas GLM # Portal multinacional - Cruce de Cuentas GLM
# Copiar como .env.local y ajustar los valores por ambiente. # Copiar como .env.local y ajustar los valores por ambiente.
# Ruta pública actual. Puede cambiarse a /cruce-cuentas/ cuando el portal # Ruta pública del portal multinacional centralizado.
# multinacional sea publicado en una ruta general. VITE_BASE_PATH="/cruce-cuentas/"
VITE_BASE_PATH="/cruce-cuentas/guatemala/"
# Guatemala: servicio principal que ejecuta el cruce Nómina vs Banco. # Guatemala: servicio principal que ejecuta el cruce Nómina vs Banco.
VITE_N8N_GUATEMALA_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/nominagt-bamboo-test" VITE_N8N_GUATEMALA_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/nominagt-bamboo-test"
@@ -19,6 +18,14 @@ VITE_MARCAR_RESUELTO_GT_URL="https://agenteit.digitalcompass.agency/webhook/cruc
VITE_N8N_TRINIDAD_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/nominatt-bamboo-test" VITE_N8N_TRINIDAD_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/nominatt-bamboo-test"
VITE_HISTORICOS_TT_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-tt-historicos" VITE_HISTORICOS_TT_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-tt-historicos"
VITE_MARCAR_RESUELTO_TT_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-tt-marcar-resuelto" VITE_MARCAR_RESUELTO_TT_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-tt-marcar-resuelto"
VITE_N8N_PANAMA_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/nominapa-bamboo-test"
VITE_HISTORICOS_PA_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-pa-historicos"
VITE_MARCAR_RESUELTO_PA_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-pa-marcar-resuelto"
# El Salvador: cruce, históricos y resolución.
VITE_N8N_EL_SALVADOR_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/nominasv-bamboo-test"
VITE_HISTORICOS_SV_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-sv-historicos"
VITE_MARCAR_RESUELTO_SV_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-sv-marcar-resuelto"
# Endpoint opcional de n8n + Gemini para analizar el nombre del archivo de nómina. # Endpoint opcional de n8n + Gemini para analizar el nombre del archivo de nómina.
# La app ya incluye detección local como respaldo. # La app ya incluye detección local como respaldo.
@@ -27,6 +34,9 @@ VITE_ANALIZAR_NOMINA_URL="https://agenteit.digitalcompass.agency/webhook/cruce-c
# Reporte manual de falsos positivos Banco sin BambooHR -> RRHH. # Reporte manual de falsos positivos Banco sin BambooHR -> RRHH.
VITE_REPORTAR_BAMBOO_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-bamboo-correccion" VITE_REPORTAR_BAMBOO_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-bamboo-correccion"
# Reporte general de errores/incidencias de la aplicación -> IT.
VITE_REPORTAR_APP_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-reportar"
# Login Google vía Supabase. # Login Google vía Supabase.
VITE_ENABLE_SUPABASE_AUTH="true" VITE_ENABLE_SUPABASE_AUTH="true"
VITE_SUPABASE_URL="https://dbit.digitalcompass.agency" VITE_SUPABASE_URL="https://dbit.digitalcompass.agency"
+11
View File
@@ -0,0 +1,11 @@
# Cambios 07-09-2026
1. El cuadro **Período calculado** muestra las fechas en formato `DD-MM-YYYY` sin modificar las fechas ISO que se envían al backend.
2. Se añadió el botón **Reportar** en el header de **Cruce de Cuentas** y **Reportes Históricos**. Abre un modal con **Descripción del Reporte** y envía el reporte a IT mediante `VITE_REPORTAR_APP_URL`.
3. Se incluye `WORKFLOW_REPORTE_USUARIO_CRUCE_CUENTAS.json`, siguiendo el mismo patrón seguro del Cotizador Walmart Connect: valida la sesión Supabase, valida el acceso a Cruce de Cuentas y envía el correo a `iaracena@gomezleemarketing.com`.
4. Se incluye `LIMPIAR_REPORTES_HISTORICOS_PANAMA_UNA_VEZ.sql` para borrar una sola vez los reportes de prueba de Panamá y las correcciones asociadas, sin tocar Guatemala, Trinidad y Tobago ni los aliases BambooHR.
## Acciones únicas al publicar esta versión
- Importar `WORKFLOW_REPORTE_USUARIO_CRUCE_CUENTAS.json` en n8n y activarlo para habilitar el botón **Reportar**.
- Ejecutar una sola vez `LIMPIAR_REPORTES_HISTORICOS_PANAMA_UNA_VEZ.sql` en el SQL Editor de Supabase. Al terminar, la consulta final debe devolver `0` reportes de Panamá. Los reportes nuevos creados por los usuarios seguirán guardándose normalmente.
+16
View File
@@ -0,0 +1,16 @@
# Corrección de versión antigua después del login
Se corrigió el caso en que, después del OAuth de Google, el navegador podía
restaurar temporalmente un `index.html` perteneciente a un despliegue anterior.
Defensas implementadas:
1. Antes de iniciar OAuth, la app revalida la URL de entrada usando `fetch` con
`cache: reload`, para refrescar la caché HTTP del documento.
2. Si el navegador restaura la página desde BFCache, se fuerza una recarga única.
3. `index.html` incluye directivas defensivas de no-cache para el documento.
4. El `dist` incluido ya contiene la corrección y utiliza un nombre nuevo para
el bundle JavaScript, evitando reutilizar el asset JavaScript anterior.
No se modificó la lógica funcional de cruces, Bono 14, históricos, BambooHR,
Supabase, RRHH, permisos, archivos o reportes.
+61
View File
@@ -0,0 +1,61 @@
# Cruce de Cuentas GLM · El Salvador (SV)
## Estado
Integración preparada para producción con la misma arquitectura multinacional del portal.
## Endpoints n8n
- Cruce principal: `https://agenteit.digitalcompass.agency/webhook/nominasv-bamboo-test`
- Históricos: `https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-sv-historicos`
- Marcar resuelto: `https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-sv-marcar-resuelto`
- Analizador de período compartido: `https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-analizar-nomina`
- Corrección BambooHR compartida: `https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-bamboo-correccion`
## Archivos de nómina
El Salvador admite múltiples Excel por quincena. Se validaron los formatos GLM de:
- General
- Karla Elias / Trade Marketing
- Nestlé
- Motorola
Q1 puede contener únicamente los archivos aplicables a la primera quincena; Q2 puede incluir todos los archivos aplicables al cierre de mes. La app no impone una cantidad fija.
## Banco
Admite múltiples CSV `Consulta Detalle de Envío` de planes 4503 y ABG3. El parser usa las columnas `Cuenta Destino`, `Nombre en Archivo`, `Nombre del Cuentahabiente`, `Monto`, `Concepto` y metadatos de plan/envío.
## Períodos
Año, mes y quincena son dinámicos. El analizador compartido con Gemini reconoce nombres en español/inglés y calcula:
- Quincena 15: día 1 al 15.
- Quincena 30 / fin de mes: día 16 al último día real del mes (28, 29, 30 o 31).
## Moneda y zona horaria
- Moneda: USD
- Locale: `es-SV`
- Zona horaria: `America/El_Salvador`
## Bandera
`https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/Flag_of_El_Salvador.svg.webp`
## Correcciones BambooHR
- El Salvador: Carlos Antonio Malta Méndez — `cmalta@gomezleemarketing.com`.
- Panamá queda corregido para enviar únicamente a Delarys Peñalba — `dpenalba@gomezleemarketing.com`.
## Supabase
Ejecutar una sola vez `SUPABASE_El_Salvador_READY.sql`. Amplía los CHECK y RPC multi-país a `SV` sin borrar datos existentes.
## Orden de implementación en producción
1. Ejecutar `SUPABASE_El_Salvador_READY.sql` una sola vez.
2. Importar y publicar `Portal de Verificación de Nómina - El Salvador.json`.
3. Importar y publicar `Portal de Verificación de Nómina El Salvador - Históricos.json`.
4. Importar y publicar `Portal de Verificación de Nómina El Salvador - Envío de Reporte.json`.
5. Actualizar el workflow compartido del analizador de nómina con `Portal de Verificación de Nómina - Analizar Nómina con Gemini GT TT PA SV.json`.
6. Actualizar el workflow compartido de correcciones con `Portal de Verificación de Nómina - Reportar corrección BambooHR a RRHH - GT TT PA SV.json`.
7. Desplegar el `dist` incluido en el ZIP de la aplicación.
8. Dar acceso a `SV` en Supabase a los usuarios que deban operar El Salvador, si su registro de acceso restringe países.
9. Hacer una ejecución controlada de Q15 y Q30 antes de entregar el país a usuarios finales.
### Importante para los dos workflows compartidos
Los JSON usan los mismos paths de webhook que los workflows actuales. No deben quedar publicadas simultáneamente dos copias con el mismo path. Actualiza/reemplaza el workflow existente o desactiva el anterior antes de publicar el nuevo.
Los workflows entregados quedan `active: false` dentro del JSON para que la importación sea segura. Publícalos después de verificar que n8n haya conservado/asignado las credenciales correspondientes.
+129
View File
@@ -0,0 +1,129 @@
# Cruce de Cuentas GLM · Panamá (PA) · Instalación
Este paquete agrega **Panamá** como tercer país del portal centralizado de Cruce de Cuentas GLM sin reemplazar la lógica productiva de Guatemala, Trinidad y Tobago ni Bono 14.
## Archivos incluidos
### Workflows n8n
1. `Portal de Verificación de Nómina - Panamá.json`
- Workflow principal Panamá.
- Webhook: `POST /webhook/nominapa-bamboo-test`.
2. `Portal de Verificación de Nómina Panamá - Históricos.json`
- Respaldo de Históricos Panamá.
- Webhook: `GET /webhook/cruce-cuentas-pa-historicos`.
3. `Portal de Verificación de Nómina Panamá - Envío de Reporte.json`
- Marcar reporte como resuelto + correo.
- Webhook: `POST /webhook/cruce-cuentas-pa-marcar-resuelto`.
4. `Portal de Verificación de Nómina - Analizar Nómina con Gemini Guatemala Trinidad y Tobago y Panamá.json`
- Sustituye el analizador compartido actual y conserva GT/TT agregando PA.
- Webhook compartido: `POST /webhook/cruce-cuentas-analizar-nomina`.
5. `Portal de Verificación de Nómina - Reportar corrección BambooHR a RRHH - GT TT PA.json`
- Sustituye el workflow compartido actual y conserva GT/TT agregando PA.
- Webhook compartido: `POST /webhook/cruce-cuentas-bamboo-correccion`.
### Supabase
- `SUPABASE_Panama_READY.sql`
- Amplía los checks de país a `GT`, `TT`, `PA`.
- Habilita aliases y correcciones BambooHR para PA.
- Actualiza las RPC de Históricos y Banco sin Bamboo para PA.
- Mantiene la tabla histórica legada `public.cruces_cuentas_gt_reportes` como tabla multi-país por la columna `country`.
- No elimina reportes ni datos históricos.
## Orden de instalación recomendado
### 1. Respaldo
Antes de tocar producción, exporta los workflows actuales compartidos y conserva una copia del ZIP actualmente desplegado de la app.
### 2. Supabase
Ejecuta `SUPABASE_Panama_READY.sql` en el SQL Editor del Supabase empresarial.
El bloque final incluye consultas de validación de solo lectura. Deben ejecutarse sin error.
### 3. Workflows compartidos
Desactiva temporalmente los workflows actuales que ocupan estas rutas:
- `cruce-cuentas-analizar-nomina`
- `cruce-cuentas-bamboo-correccion`
Importa las dos versiones `GT TT PA`, verifica credenciales y publícalas. No pueden quedar dos workflows activos utilizando el mismo path.
### 4. Workflows nuevos de Panamá
Importa y publica:
- Principal Panamá.
- Históricos Panamá.
- Envío de Reporte / Marcar Resuelto Panamá.
### 5. Credenciales n8n
Los JSON hacen referencia a credenciales ya usadas por los workflows productivos. Verifica que n8n las resuelva después de importar:
- `BambooHR GLM Full Access`
- `Google account 2`
- `Google account - Isaac Producción`
- `Gmail account 3`
- `Isaac - Gemini Api Pago`
**No hay `$env`, `process.env` ni variables `VITE_` en los workflows de n8n.** Los endpoints y valores requeridos por n8n están puestos directamente en los JSON, siguiendo la forma actual de producción.
### 6. Acceso de usuarios
La app sigue usando `cruce_cuentas_mi_acceso` para validar la sesión. Si la RPC devuelve una lista `paises`, agrega `PA` a los usuarios que deban reportar correcciones de Panamá a RRHH.
### 7. App
El ZIP de la app contiene:
- Código fuente React/TypeScript/Vite actualizado.
- `dist/` listo para servir en `/cruce-cuentas/`.
- `.env.local` con los endpoints actuales, incluyendo PA.
- `.env.example` actualizado.
Si tu despliegue sirve archivos estáticos directamente, publica el contenido de `dist/` bajo `/cruce-cuentas/`.
Si prefieres compilar en tu infraestructura, ejecuta normalmente `npm ci` y `npm run build` en un ambiente con dependencias nativas compatibles con su sistema operativo.
## Reglas Panamá V1 implementadas
- País: `PA` / Panamá.
- Moneda: USD.
- Bandera: `https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/Flag_of_Panama.svg`.
- BambooHR usa `onlyCurrent=false`.
- Permite múltiples planillas en un mismo período: quincenal, extraordinarias, vacaciones y XIII Mes/Décimo.
- Soporta Plan 1582 y Plan 1644.
- `Referencia` se usa como fallback controlado cuando `Cuenta Destino` no es una cuenta válida; no se equiparan ambos campos globalmente.
- La inclusión bancaria es **fila por fila y guiada por la nómina**, no por el nombre del plan ni por el texto de `Concepto`.
- Una fila respaldada por nómina puede entrar aunque el concepto diga `PAGO A PROVEEDORES` o contenga texto desactualizado.
- Honorarios, Pluxee, proveedores/acreedores, pensiones, préstamos y viáticos sin respaldo de nómina permanecen fuera de Panamá V1.
- No se fuerzan matches ambiguos de nombres/BambooHR.
- Las cuentas y nombres mantienen trazabilidad al archivo/envío/plan de origen.
- Si ya existe el reporte del mismo país/año/mes/período, reutiliza el Google Sheet, preserva resoluciones que sigan correspondiendo y envía correo de **Reporte actualizado**.
- Si es una ejecución nueva, envía correo de **Nuevo reporte generado**.
- Marcar un reporte como resuelto actualiza Supabase y envía el correo de resolución.
- `Banco sin Bamboo` puede enviarse a RRHH mediante el mismo flujo compartido usado por GT/TT.
## RRHH Panamá
La versión entregada envía las correcciones de identidad de Panamá a:
- Delarys Peñalba — `dpenalba@gomezleemarketing.com`
- Ariadne Cortéz — `acortez02@gomezleemarketing.com`
## Estructura del reporte
Se conserva la misma estructura del portal:
1. `01 Nómina vs Banco`
2. `02 Banco sin Nómina`
3. `03 Banco sin Bamboo`
4. `04 Diferencias nombre banco`
5. `05 Cuenta Mal Digitada` cuando existan casos
6. `06 Resumen` si existe la hoja anterior; de lo contrario `05 Resumen`
La columna `Resolución` se conserva en las hojas de revisión para permitir actualización sin perder los comentarios vigentes que sigan correspondiendo al mismo caso.
## Regresión recomendada
1. Probar **enero 2026 Q15** y **enero 2026 Q30**.
2. Probar **febrero 2026 Q15/Q30** para validar archivos mixtos y conceptos engañosos.
3. Probar **marzo 2026** para validar discrepancias/pagos sin nómina.
4. Probar **abril 2026** para validar XIII Mes/Décimo y suplementos.
5. Validar que el correo cambie de `Nuevo reporte generado` a `Reporte actualizado` al ejecutar de nuevo el mismo período.
6. Desde Históricos, marcar un reporte resuelto y confirmar correo.
7. Abrir un caso Banco sin Bamboo, reportarlo a RRHH y comprobar que el alias quede guardado para futuras ejecuciones.
8. Ejecutar una prueba rápida de GT y TT para confirmar que los dos workflows compartidos siguen sin regresión.
## Nota de seguridad operativa
No publiques simultáneamente dos versiones de un workflow compartido con el mismo webhook. Importa primero, valida credenciales y luego cambia cuál versión queda activa.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,159 @@
{
"name": "Portal de Verificación de Nómina El Salvador - Históricos",
"nodes": [
{
"parameters": {
"path": "cruce-cuentas-sv-historicos",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
608,
400
],
"id": "485c6599-40fd-491b-b62d-e3acb92c23d8",
"name": "Webhook Históricos SV",
"webhookId": "b225465a-eaa6-4d3e-ab6b-9ea4fc7375d1"
},
{
"parameters": {
"jsCode": "const items = $input.all();\n\nfunction formatDate(value) {\n if (!value) return '';\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return String(value).slice(0, 10);\n return date.toISOString().slice(0, 10);\n}\n\nfunction toNumber(value) {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : 0;\n}\n\nfunction normalizeStatus(value, discrepancias) {\n const status = String(value || '').toLowerCase();\n\n if (status === 'resuelto') return 'resuelto';\n if (status === 'procesado' && toNumber(discrepancias) === 0) return 'procesado';\n\n return 'pendiente_revision';\n}\n\nfunction statusLabel(status) {\n if (status === 'resuelto') return 'RESUELTO';\n if (status === 'procesado') return 'PROCESADO';\n return 'PENDIENTE REVISIÓN';\n}\n\nconst reports = items.map((item) => {\n const row = item.json || {};\n\n const discrepancias = toNumber(row.discrepancias);\n const bancoSinNomina = toNumber(row.banco_sin_nomina);\n const nominaSinCuenta = toNumber(row.nomina_sin_cuenta);\n const bancoSinBamboo = toNumber(row.banco_sin_bamboo);\n const nominaSinBamboo = toNumber(row.nomina_sin_bamboo);\n const bambooSinNomina = toNumber(row.bamboo_sin_nomina);\n\n const totalPendientes =\n discrepancias +\n bancoSinNomina +\n nominaSinCuenta +\n bancoSinBamboo +\n nominaSinBamboo +\n bambooSinNomina;\n\n const estado = normalizeStatus(row.estado, totalPendientes);\n\n return {\n id: row.id,\n\n fecha: formatDate(row.period_end || row.ejecutado_en || row.created_at),\n fechaEjecucion: row.ejecutado_en || row.created_at,\n\n periodo: row.period_label || '',\n year: row.year,\n month: row.month,\n periodType: row.period_type,\n periodStart: row.period_start,\n periodEnd: row.period_end,\n\n archivoNomina: row.payroll_file_name || '',\n archivosBanco: row.bank_file_names || [],\n\n coincidencias: toNumber(row.coincidencias),\n discrepancias,\n bancoSinNomina,\n nominaSinCuenta,\n bancoSinBamboo,\n nominaSinBamboo,\n bambooSinNomina,\n totalPendientes,\n\n totalNomina: toNumber(row.total_nomina),\n totalBanco: toNumber(row.total_banco),\n diferenciaTotal: toNumber(row.diferencia_total),\n\n estado,\n estadoLabel: statusLabel(estado),\n\n reportUrl: row.report_url || '',\n spreadsheetId: row.spreadsheet_id || '',\n\n ejecutadoPorNombre: row.ejecutado_por_nombre || '',\n ejecutadoPorEmail: row.ejecutado_por_email || '',\n\n resueltoPorNombre: row.resuelto_por_nombre || '',\n resueltoPorEmail: row.resuelto_por_email || '',\n resueltoEn: row.resuelto_en || null,\n comentarioResolucion: row.comentario_resolucion || '',\n };\n});\n\nreturn [\n {\n json: {\n ok: true,\n stage: 'historicos_sv_listos',\n count: reports.length,\n reports,\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1024,
400
],
"id": "9531b25a-916e-4788-841a-68ccb2a42d3d",
"name": "Normalizar históricos para app"
},
{
"parameters": {
"url": "https://dbit.digitalcompass.agency/rest/v1/cruces_cuentas_gt_reportes?select=*&country=eq.SV&order=ejecutado_en.desc&limit=100",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Authorization",
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
816,
400
],
"id": "bb61d9d5-8258-44b7-9d4b-18633fdc8bc4",
"name": "Consultar históricos Supabase"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Access-Control-Allow-Origin",
"value": "*"
},
{
"name": "Access-Control-Allow-Methods",
"value": "GET,POST,OPTIONS"
},
{
"name": "Access-Control-Allow-Headers",
"value": "Content-Type, Authorization"
}
]
}
}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
1248,
400
],
"id": "769a572b-e78a-4327-a397-66438783bbef",
"name": "Responder históricos app"
},
{
"parameters": {
"content": "# 📚 HISTÓRICOS DE VERIFICACIÓN — EL SALVADOR\n\nEntrega a la aplicación el historial de reportes procesados para El Salvador.\n\n- Consulta únicamente registros con `country = SV`.\n- Supabase sigue usando la tabla histórica compartida `cruces_cuentas_gt_reportes`; el nombre es legado, pero la tabla es multi-país.\n- Devuelve la misma estructura que Guatemala y Trinidad y Tobago.\n- Este webhook sirve como respaldo; la app intenta primero la RPC paginada de Supabase.",
"height": 496,
"width": 1360,
"color": 6
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
304,
128
],
"id": "60352e6e-7a0c-4a73-adf2-0183a78e1796",
"name": "Sticky Note"
}
],
"pinData": {},
"connections": {
"Consultar históricos Supabase": {
"main": [
[
{
"node": "Normalizar históricos para app",
"type": "main",
"index": 0
}
]
]
},
"Normalizar históricos para app": {
"main": [
[
{
"node": "Responder históricos app",
"type": "main",
"index": 0
}
]
]
},
"Webhook Históricos SV": {
"main": [
[
{
"node": "Consultar históricos Supabase",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate"
},
"tags": []
}
+347
View File
@@ -0,0 +1,347 @@
-- ============================================================
-- CRUCE DE CUENTAS GLM · HABILITAR EL SALVADOR (SV)
-- ============================================================
-- Ejecutar UNA VEZ en el SQL Editor del Supabase empresarial.
-- Es idempotente y conserva los datos existentes de Guatemala, Trinidad y Tobago y Panamá.
-- La tabla de reportes conserva su nombre legado cruces_cuentas_gt_reportes,
-- pero funciona como tabla multi-país mediante la columna country.
begin;
-- ------------------------------------------------------------
-- 1. Checks de país en aliases/correcciones BambooHR
-- ------------------------------------------------------------
do $$
declare
r record;
begin
if to_regclass('public.cruce_cuentas_bamboo_aliases') is null then
raise exception 'No existe public.cruce_cuentas_bamboo_aliases. Ejecuta primero el SQL base de aliases BambooHR.';
end if;
if to_regclass('public.cruce_cuentas_bamboo_correcciones') is null then
raise exception 'No existe public.cruce_cuentas_bamboo_correcciones. Ejecuta primero el SQL base de correcciones BambooHR.';
end if;
for r in
select c.conname
from pg_constraint c
where c.conrelid = 'public.cruce_cuentas_bamboo_aliases'::regclass
and c.contype = 'c'
and pg_get_constraintdef(c.oid) ilike '%pais%'
and pg_get_constraintdef(c.oid) ilike '%GT%'
and pg_get_constraintdef(c.oid) ilike '%TT%'
and pg_get_constraintdef(c.oid) not ilike '%SV%'
loop
execute format('alter table public.cruce_cuentas_bamboo_aliases drop constraint %I', r.conname);
end loop;
if not exists (
select 1 from pg_constraint
where conrelid = 'public.cruce_cuentas_bamboo_aliases'::regclass
and conname = 'ck_cruce_cuentas_bamboo_aliases_pais'
) then
alter table public.cruce_cuentas_bamboo_aliases
add constraint ck_cruce_cuentas_bamboo_aliases_pais
check (upper(trim(pais)) in ('GT', 'TT', 'PA', 'SV'));
end if;
for r in
select c.conname
from pg_constraint c
where c.conrelid = 'public.cruce_cuentas_bamboo_correcciones'::regclass
and c.contype = 'c'
and pg_get_constraintdef(c.oid) ilike '%pais%'
and pg_get_constraintdef(c.oid) ilike '%GT%'
and pg_get_constraintdef(c.oid) ilike '%TT%'
and pg_get_constraintdef(c.oid) not ilike '%SV%'
loop
execute format('alter table public.cruce_cuentas_bamboo_correcciones drop constraint %I', r.conname);
end loop;
if not exists (
select 1 from pg_constraint
where conrelid = 'public.cruce_cuentas_bamboo_correcciones'::regclass
and conname = 'ck_cruce_cuentas_bamboo_correcciones_pais'
) then
alter table public.cruce_cuentas_bamboo_correcciones
add constraint ck_cruce_cuentas_bamboo_correcciones_pais
check (upper(trim(pais)) in ('GT', 'TT', 'PA', 'SV'));
end if;
end;
$$;
-- Si la tabla histórica tiene un CHECK antiguo limitado a GT/TT, se amplía.
do $$
declare
r record;
begin
if to_regclass('public.cruces_cuentas_gt_reportes') is null then
raise exception 'No existe public.cruces_cuentas_gt_reportes.';
end if;
for r in
select c.conname
from pg_constraint c
where c.conrelid = 'public.cruces_cuentas_gt_reportes'::regclass
and c.contype = 'c'
and pg_get_constraintdef(c.oid) ilike '%country%'
and pg_get_constraintdef(c.oid) ilike '%GT%'
and pg_get_constraintdef(c.oid) ilike '%TT%'
and pg_get_constraintdef(c.oid) not ilike '%SV%'
loop
execute format('alter table public.cruces_cuentas_gt_reportes drop constraint %I', r.conname);
end loop;
if not exists (
select 1 from pg_constraint
where conrelid = 'public.cruces_cuentas_gt_reportes'::regclass
and conname = 'ck_cruces_cuentas_reportes_country'
) then
alter table public.cruces_cuentas_gt_reportes
add constraint ck_cruces_cuentas_reportes_country
check (country is null or upper(trim(country)) in ('GT', 'TT', 'PA', 'SV'));
end if;
end;
$$;
-- ------------------------------------------------------------
-- 2. Alias BambooHR activos: GT + TT + PA + SV
-- ------------------------------------------------------------
create or replace function public.cruce_cuentas_bamboo_aliases_activos(p_pais text)
returns jsonb
language sql
stable
security definer
set search_path = public
as $$
select jsonb_build_object(
'ok', true,
'pais', upper(trim(coalesce(p_pais, ''))),
'aliases', coalesce(
jsonb_agg(
jsonb_build_object(
'id', a.id,
'pais', a.pais,
'nombre_origen', a.nombre_origen,
'nombre_origen_normalizado', a.nombre_origen_normalizado,
'cuenta_bancaria', a.cuenta_bancaria,
'bamboo_employee_number', a.bamboo_employee_number,
'bamboo_employee_id', a.bamboo_employee_id,
'bamboo_nombre', a.bamboo_nombre,
'motivo', a.motivo,
'activo', a.activo
)
order by a.updated_at desc
) filter (where a.id is not null),
'[]'::jsonb
)
)
from public.cruce_cuentas_bamboo_aliases a
where a.activo = true
and a.pais = upper(trim(coalesce(p_pais, '')))
and upper(trim(coalesce(p_pais, ''))) in ('GT', 'TT', 'PA', 'SV');
$$;
revoke all on function public.cruce_cuentas_bamboo_aliases_activos(text) from public, anon, authenticated;
grant execute on function public.cruce_cuentas_bamboo_aliases_activos(text) to service_role;
-- ------------------------------------------------------------
-- 3. Históricos paginados: GT + TT + PA + SV
-- ------------------------------------------------------------
create index if not exists idx_cruces_cuentas_reportes_country_created
on public.cruces_cuentas_gt_reportes (country, created_at desc);
create index if not exists idx_cruces_cuentas_reportes_country_status_created
on public.cruces_cuentas_gt_reportes (country, estado, created_at desc);
create or replace function public.cruce_cuentas_get_historicos(
p_country text,
p_page integer default 1,
p_page_size integer default 10,
p_search text default null,
p_status text default null
)
returns jsonb
language plpgsql
security definer
set search_path = public
as $$
declare
v_country text := upper(trim(coalesce(p_country, ''));
v_page integer := greatest(coalesce(p_page, 1), 1);
v_page_size integer := least(greatest(coalesce(p_page_size, 10), 1), 100);
v_offset integer;
v_search text := nullif(trim(coalesce(p_search, '')), '');
v_status text := nullif(trim(coalesce(p_status, '')), '');
v_total bigint;
v_filtered_total bigint;
v_reports jsonb;
begin
if v_country not in ('GT', 'TT', 'PA', 'SV') then
raise exception 'País no permitido: %', v_country;
end if;
v_offset := (v_page - 1) * v_page_size;
select count(*) into v_total
from public.cruces_cuentas_gt_reportes r
where upper(coalesce(r.country, 'GT')) = v_country;
select count(*) into v_filtered_total
from public.cruces_cuentas_gt_reportes r
where upper(coalesce(r.country, 'GT')) = v_country
and (v_status is null or r.estado = v_status)
and (
v_search is null
or coalesce(r.period_label, '') ilike '%' || v_search || '%'
or coalesce(r.payroll_file_name, '') ilike '%' || v_search || '%'
or coalesce(r.ejecutado_por_nombre, '') ilike '%' || v_search || '%'
or coalesce(r.resuelto_por_nombre, '') ilike '%' || v_search || '%'
or r.id::text ilike '%' || v_search || '%'
);
select coalesce(
jsonb_agg((to_jsonb(q) - 'detalle_banco_sin_bamboo') order by q.created_at desc),
'[]'::jsonb
) into v_reports
from (
select r.*
from public.cruces_cuentas_gt_reportes r
where upper(coalesce(r.country, 'GT')) = v_country
and (v_status is null or r.estado = v_status)
and (
v_search is null
or coalesce(r.period_label, '') ilike '%' || v_search || '%'
or coalesce(r.payroll_file_name, '') ilike '%' || v_search || '%'
or coalesce(r.ejecutado_por_nombre, '') ilike '%' || v_search || '%'
or coalesce(r.resuelto_por_nombre, '') ilike '%' || v_search || '%'
or r.id::text ilike '%' || v_search || '%'
)
order by r.created_at desc
limit v_page_size
offset v_offset
) q;
return jsonb_build_object(
'ok', true,
'country', v_country,
'page', v_page,
'page_size', v_page_size,
'total', v_total,
'filtered_total', v_filtered_total,
'total_pages', greatest(ceil(v_filtered_total::numeric / v_page_size)::integer, 1),
'reports', v_reports
);
end;
$$;
revoke all on function public.cruce_cuentas_get_historicos(text, integer, integer, text, text) from public, anon;
grant execute on function public.cruce_cuentas_get_historicos(text, integer, integer, text, text) to authenticated;
-- ------------------------------------------------------------
-- 4. Detalle Banco sin Bamboo desde Históricos: GT + TT + PA + SV
-- ------------------------------------------------------------
create or replace function public.cruce_cuentas_get_banco_sin_bamboo_reporte(
p_report_id text,
p_country text
)
returns jsonb
language plpgsql
stable
security definer
set search_path = public
as $$
declare
v_country text := upper(trim(coalesce(p_country, ''));
v_report public.cruces_cuentas_gt_reportes%rowtype;
v_corrections jsonb := '[]'::jsonb;
begin
if auth.uid() is null then
raise exception 'No autenticado.';
end if;
if v_country not in ('GT', 'TT', 'PA', 'SV') then
raise exception 'País no permitido: %', v_country;
end if;
select r.* into v_report
from public.cruces_cuentas_gt_reportes r
where r.id::text = trim(coalesce(p_report_id, ''))
and upper(coalesce(r.country, 'GT')) = v_country
limit 1;
if not found then
return jsonb_build_object('ok', false, 'message', 'No se encontró el reporte solicitado.');
end if;
select coalesce(
jsonb_agg(
jsonb_build_object(
'id', c.id,
'pais', c.pais,
'nombre_origen_principal', c.nombre_origen_principal,
'nombres_origen', c.nombres_origen,
'cuenta_bancaria', c.cuenta_bancaria,
'bamboo_employee_number', c.bamboo_employee_number,
'bamboo_nombre', c.bamboo_nombre,
'estado', c.estado,
'correo_enviado', c.correo_enviado,
'correo_enviado_at', c.correo_enviado_at,
'created_at', c.created_at
) order by c.created_at desc
),
'[]'::jsonb
) into v_corrections
from public.cruce_cuentas_bamboo_correcciones c
where c.pais = v_country
and (
c.execution_id = v_report.id::text
or (
nullif(trim(coalesce(c.execution_id, '')), '') is null
and nullif(trim(coalesce(c.report_url, '')), '') is not null
and c.report_url = v_report.report_url
)
);
return jsonb_build_object(
'ok', true,
'report', jsonb_build_object(
'id', v_report.id,
'country', v_report.country,
'period_label', v_report.period_label,
'period_start', v_report.period_start,
'period_end', v_report.period_end,
'report_url', v_report.report_url,
'banco_sin_bamboo', v_report.banco_sin_bamboo
),
'cases', coalesce(v_report.detalle_banco_sin_bamboo, '[]'::jsonb),
'corrections', v_corrections
);
end;
$$;
revoke all on function public.cruce_cuentas_get_banco_sin_bamboo_reporte(text, text) from public, anon;
grant execute on function public.cruce_cuentas_get_banco_sin_bamboo_reporte(text, text) to authenticated;
commit;
-- ============================================================
-- VALIDACIONES OPCIONALES (solo lectura)
-- ============================================================
-- Debe devolver ok=true y aliases (aunque sea []).
select public.cruce_cuentas_bamboo_aliases_activos('SV');
-- Debe devolver country=SV, aunque todavía no existan reportes.
select public.cruce_cuentas_get_historicos('SV', 1, 10, null, null);
-- Verificar constraints finales.
select conrelid::regclass as tabla, conname, pg_get_constraintdef(oid) as definicion
from pg_constraint
where conrelid in (
'public.cruce_cuentas_bamboo_aliases'::regclass,
'public.cruce_cuentas_bamboo_correcciones'::regclass,
'public.cruces_cuentas_gt_reportes'::regclass
)
and contype = 'c'
order by 1, 2;
+347
View File
@@ -0,0 +1,347 @@
-- ============================================================
-- CRUCE DE CUENTAS GLM · HABILITAR PANAMÁ (PA)
-- ============================================================
-- Ejecutar UNA VEZ en el SQL Editor del Supabase empresarial.
-- Es idempotente y conserva los datos existentes de Guatemala/Trinidad.
-- La tabla de reportes conserva su nombre legado cruces_cuentas_gt_reportes,
-- pero funciona como tabla multi-país mediante la columna country.
begin;
-- ------------------------------------------------------------
-- 1. Checks de país en aliases/correcciones BambooHR
-- ------------------------------------------------------------
do $$
declare
r record;
begin
if to_regclass('public.cruce_cuentas_bamboo_aliases') is null then
raise exception 'No existe public.cruce_cuentas_bamboo_aliases. Ejecuta primero el SQL base de aliases BambooHR.';
end if;
if to_regclass('public.cruce_cuentas_bamboo_correcciones') is null then
raise exception 'No existe public.cruce_cuentas_bamboo_correcciones. Ejecuta primero el SQL base de correcciones BambooHR.';
end if;
for r in
select c.conname
from pg_constraint c
where c.conrelid = 'public.cruce_cuentas_bamboo_aliases'::regclass
and c.contype = 'c'
and pg_get_constraintdef(c.oid) ilike '%pais%'
and pg_get_constraintdef(c.oid) ilike '%GT%'
and pg_get_constraintdef(c.oid) ilike '%TT%'
and pg_get_constraintdef(c.oid) not ilike '%PA%'
loop
execute format('alter table public.cruce_cuentas_bamboo_aliases drop constraint %I', r.conname);
end loop;
if not exists (
select 1 from pg_constraint
where conrelid = 'public.cruce_cuentas_bamboo_aliases'::regclass
and conname = 'ck_cruce_cuentas_bamboo_aliases_pais'
) then
alter table public.cruce_cuentas_bamboo_aliases
add constraint ck_cruce_cuentas_bamboo_aliases_pais
check (upper(trim(pais)) in ('GT', 'TT', 'PA'));
end if;
for r in
select c.conname
from pg_constraint c
where c.conrelid = 'public.cruce_cuentas_bamboo_correcciones'::regclass
and c.contype = 'c'
and pg_get_constraintdef(c.oid) ilike '%pais%'
and pg_get_constraintdef(c.oid) ilike '%GT%'
and pg_get_constraintdef(c.oid) ilike '%TT%'
and pg_get_constraintdef(c.oid) not ilike '%PA%'
loop
execute format('alter table public.cruce_cuentas_bamboo_correcciones drop constraint %I', r.conname);
end loop;
if not exists (
select 1 from pg_constraint
where conrelid = 'public.cruce_cuentas_bamboo_correcciones'::regclass
and conname = 'ck_cruce_cuentas_bamboo_correcciones_pais'
) then
alter table public.cruce_cuentas_bamboo_correcciones
add constraint ck_cruce_cuentas_bamboo_correcciones_pais
check (upper(trim(pais)) in ('GT', 'TT', 'PA'));
end if;
end;
$$;
-- Si la tabla histórica tiene un CHECK antiguo limitado a GT/TT, se amplía.
do $$
declare
r record;
begin
if to_regclass('public.cruces_cuentas_gt_reportes') is null then
raise exception 'No existe public.cruces_cuentas_gt_reportes.';
end if;
for r in
select c.conname
from pg_constraint c
where c.conrelid = 'public.cruces_cuentas_gt_reportes'::regclass
and c.contype = 'c'
and pg_get_constraintdef(c.oid) ilike '%country%'
and pg_get_constraintdef(c.oid) ilike '%GT%'
and pg_get_constraintdef(c.oid) ilike '%TT%'
and pg_get_constraintdef(c.oid) not ilike '%PA%'
loop
execute format('alter table public.cruces_cuentas_gt_reportes drop constraint %I', r.conname);
end loop;
if not exists (
select 1 from pg_constraint
where conrelid = 'public.cruces_cuentas_gt_reportes'::regclass
and conname = 'ck_cruces_cuentas_reportes_country'
) then
alter table public.cruces_cuentas_gt_reportes
add constraint ck_cruces_cuentas_reportes_country
check (country is null or upper(trim(country)) in ('GT', 'TT', 'PA'));
end if;
end;
$$;
-- ------------------------------------------------------------
-- 2. Alias BambooHR activos: GT + TT + PA
-- ------------------------------------------------------------
create or replace function public.cruce_cuentas_bamboo_aliases_activos(p_pais text)
returns jsonb
language sql
stable
security definer
set search_path = public
as $$
select jsonb_build_object(
'ok', true,
'pais', upper(trim(coalesce(p_pais, ''))),
'aliases', coalesce(
jsonb_agg(
jsonb_build_object(
'id', a.id,
'pais', a.pais,
'nombre_origen', a.nombre_origen,
'nombre_origen_normalizado', a.nombre_origen_normalizado,
'cuenta_bancaria', a.cuenta_bancaria,
'bamboo_employee_number', a.bamboo_employee_number,
'bamboo_employee_id', a.bamboo_employee_id,
'bamboo_nombre', a.bamboo_nombre,
'motivo', a.motivo,
'activo', a.activo
)
order by a.updated_at desc
) filter (where a.id is not null),
'[]'::jsonb
)
)
from public.cruce_cuentas_bamboo_aliases a
where a.activo = true
and a.pais = upper(trim(coalesce(p_pais, '')))
and upper(trim(coalesce(p_pais, ''))) in ('GT', 'TT', 'PA');
$$;
revoke all on function public.cruce_cuentas_bamboo_aliases_activos(text) from public, anon, authenticated;
grant execute on function public.cruce_cuentas_bamboo_aliases_activos(text) to service_role;
-- ------------------------------------------------------------
-- 3. Históricos paginados: GT + TT + PA
-- ------------------------------------------------------------
create index if not exists idx_cruces_cuentas_reportes_country_created
on public.cruces_cuentas_gt_reportes (country, created_at desc);
create index if not exists idx_cruces_cuentas_reportes_country_status_created
on public.cruces_cuentas_gt_reportes (country, estado, created_at desc);
create or replace function public.cruce_cuentas_get_historicos(
p_country text,
p_page integer default 1,
p_page_size integer default 10,
p_search text default null,
p_status text default null
)
returns jsonb
language plpgsql
security definer
set search_path = public
as $$
declare
v_country text := upper(trim(coalesce(p_country, ''));
v_page integer := greatest(coalesce(p_page, 1), 1);
v_page_size integer := least(greatest(coalesce(p_page_size, 10), 1), 100);
v_offset integer;
v_search text := nullif(trim(coalesce(p_search, '')), '');
v_status text := nullif(trim(coalesce(p_status, '')), '');
v_total bigint;
v_filtered_total bigint;
v_reports jsonb;
begin
if v_country not in ('GT', 'TT', 'PA') then
raise exception 'País no permitido: %', v_country;
end if;
v_offset := (v_page - 1) * v_page_size;
select count(*) into v_total
from public.cruces_cuentas_gt_reportes r
where upper(coalesce(r.country, 'GT')) = v_country;
select count(*) into v_filtered_total
from public.cruces_cuentas_gt_reportes r
where upper(coalesce(r.country, 'GT')) = v_country
and (v_status is null or r.estado = v_status)
and (
v_search is null
or coalesce(r.period_label, '') ilike '%' || v_search || '%'
or coalesce(r.payroll_file_name, '') ilike '%' || v_search || '%'
or coalesce(r.ejecutado_por_nombre, '') ilike '%' || v_search || '%'
or coalesce(r.resuelto_por_nombre, '') ilike '%' || v_search || '%'
or r.id::text ilike '%' || v_search || '%'
);
select coalesce(
jsonb_agg((to_jsonb(q) - 'detalle_banco_sin_bamboo') order by q.created_at desc),
'[]'::jsonb
) into v_reports
from (
select r.*
from public.cruces_cuentas_gt_reportes r
where upper(coalesce(r.country, 'GT')) = v_country
and (v_status is null or r.estado = v_status)
and (
v_search is null
or coalesce(r.period_label, '') ilike '%' || v_search || '%'
or coalesce(r.payroll_file_name, '') ilike '%' || v_search || '%'
or coalesce(r.ejecutado_por_nombre, '') ilike '%' || v_search || '%'
or coalesce(r.resuelto_por_nombre, '') ilike '%' || v_search || '%'
or r.id::text ilike '%' || v_search || '%'
)
order by r.created_at desc
limit v_page_size
offset v_offset
) q;
return jsonb_build_object(
'ok', true,
'country', v_country,
'page', v_page,
'page_size', v_page_size,
'total', v_total,
'filtered_total', v_filtered_total,
'total_pages', greatest(ceil(v_filtered_total::numeric / v_page_size)::integer, 1),
'reports', v_reports
);
end;
$$;
revoke all on function public.cruce_cuentas_get_historicos(text, integer, integer, text, text) from public, anon;
grant execute on function public.cruce_cuentas_get_historicos(text, integer, integer, text, text) to authenticated;
-- ------------------------------------------------------------
-- 4. Detalle Banco sin Bamboo desde Históricos: GT + TT + PA
-- ------------------------------------------------------------
create or replace function public.cruce_cuentas_get_banco_sin_bamboo_reporte(
p_report_id text,
p_country text
)
returns jsonb
language plpgsql
stable
security definer
set search_path = public
as $$
declare
v_country text := upper(trim(coalesce(p_country, ''));
v_report public.cruces_cuentas_gt_reportes%rowtype;
v_corrections jsonb := '[]'::jsonb;
begin
if auth.uid() is null then
raise exception 'No autenticado.';
end if;
if v_country not in ('GT', 'TT', 'PA') then
raise exception 'País no permitido: %', v_country;
end if;
select r.* into v_report
from public.cruces_cuentas_gt_reportes r
where r.id::text = trim(coalesce(p_report_id, ''))
and upper(coalesce(r.country, 'GT')) = v_country
limit 1;
if not found then
return jsonb_build_object('ok', false, 'message', 'No se encontró el reporte solicitado.');
end if;
select coalesce(
jsonb_agg(
jsonb_build_object(
'id', c.id,
'pais', c.pais,
'nombre_origen_principal', c.nombre_origen_principal,
'nombres_origen', c.nombres_origen,
'cuenta_bancaria', c.cuenta_bancaria,
'bamboo_employee_number', c.bamboo_employee_number,
'bamboo_nombre', c.bamboo_nombre,
'estado', c.estado,
'correo_enviado', c.correo_enviado,
'correo_enviado_at', c.correo_enviado_at,
'created_at', c.created_at
) order by c.created_at desc
),
'[]'::jsonb
) into v_corrections
from public.cruce_cuentas_bamboo_correcciones c
where c.pais = v_country
and (
c.execution_id = v_report.id::text
or (
nullif(trim(coalesce(c.execution_id, '')), '') is null
and nullif(trim(coalesce(c.report_url, '')), '') is not null
and c.report_url = v_report.report_url
)
);
return jsonb_build_object(
'ok', true,
'report', jsonb_build_object(
'id', v_report.id,
'country', v_report.country,
'period_label', v_report.period_label,
'period_start', v_report.period_start,
'period_end', v_report.period_end,
'report_url', v_report.report_url,
'banco_sin_bamboo', v_report.banco_sin_bamboo
),
'cases', coalesce(v_report.detalle_banco_sin_bamboo, '[]'::jsonb),
'corrections', v_corrections
);
end;
$$;
revoke all on function public.cruce_cuentas_get_banco_sin_bamboo_reporte(text, text) from public, anon;
grant execute on function public.cruce_cuentas_get_banco_sin_bamboo_reporte(text, text) to authenticated;
commit;
-- ============================================================
-- VALIDACIONES OPCIONALES (solo lectura)
-- ============================================================
-- Debe devolver ok=true y aliases (aunque sea []).
select public.cruce_cuentas_bamboo_aliases_activos('PA');
-- Debe devolver country=PA, aunque todavía no existan reportes.
select public.cruce_cuentas_get_historicos('PA', 1, 10, null, null);
-- Verificar constraints finales.
select conrelid::regclass as tabla, conname, pg_get_constraintdef(oid) as definicion
from pg_constraint
where conrelid in (
'public.cruce_cuentas_bamboo_aliases'::regclass,
'public.cruce_cuentas_bamboo_correcciones'::regclass,
'public.cruces_cuentas_gt_reportes'::regclass
)
and contype = 'c'
order by 1, 2;
+58
View File
@@ -0,0 +1,58 @@
# VALIDACIÓN FINAL · EL SALVADOR
Fecha de preparación: 07-09-2026
## Cobertura funcional validada
- País: El Salvador (`SV`).
- Moneda: USD.
- Zona horaria de correos/reportes: `America/El_Salvador`.
- Quincena 15: 01 al 15.
- Quincena 30: 16 al último día real del mes.
- Febrero normal y bisiesto incluidos en la validación de cálculo de período.
- Múltiples Excel de nómina por quincena.
- Múltiples CSV bancarios de planes 4503 y ABG3.
- Cruce Nómina vs Banco por cuenta y monto.
- Validación contra BambooHR.
- Banco sin BambooHR y reporte de corrección a RRHH.
- Históricos paginados en Supabase.
- Marcar histórico como resuelto.
- Google Sheet y correo de reporte.
- Analizador de período compartido con Gemini ampliado a SV.
- Bandera oficial configurada desde Supabase Storage.
## Casos reales usados para validar el parser y el cruce
| Período | Filas nómina | Total nómina | Coincidencias | Discrepancias | Banco sin nómina |
|---|---:|---:|---:|---:|---:|
| Junio 2026 · Q15 | 57 | USD 16,015.06 | 57 | 0 | 0 |
| Junio 2026 · Q30 | 108 | USD 35,828.25 | 108 | 0 | 0 |
| Julio 2026 · Q15 | 55 | USD 15,925.41 | 55 | 0 | 0 |
| Julio 2026 · Q30 | 107 | USD 35,532.76 | 102 | 5 | 0 |
Para Julio Q30 el banco totaliza USD 34,802.76 y la diferencia total esperada es USD 730.00.
## BambooHR
El normalizador se ejecutó contra el reporte completo proporcionado y detectó 423 empleados de El Salvador, con validación disponible y sin errores del normalizador.
## Banco
El parser se ejecutó directamente contra los 11 CSV de Julio Q30 y obtuvo 102 transacciones por USD 34,802.76, sin archivos con error de formato.
## Períodos futuros
Se probaron adicionalmente nombres de archivo de 2027, 2028 y 2031, incluyendo:
- 28-02-2027 → Q30, 16-02-2027 al 28-02-2027.
- 29-02-2028 → Q30, 16-02-2028 al 29-02-2028.
- 31-12-2031 → Q30, 16-12-2031 al 31-12-2031.
## Correcciones BambooHR
- SV → Carlos Antonio Malta Méndez: `cmalta@gomezleemarketing.com`.
- PA → únicamente Delarys Peñalba: `dpenalba@gomezleemarketing.com`.
- Ariadne Cortez no queda como destinataria de Panamá.
## Dist de producción
El `dist` incluido contiene El Salvador, sus tres endpoints propios, el flujo compartido de corrección, carga múltiple de nóminas, históricos y la URL de bandera indicada. Se validó sintaxis JavaScript y que los assets respondan correctamente al servir el `dist` bajo `/cruce-cuentas/`.
File diff suppressed because one or more lines are too long
-270
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -9,8 +9,8 @@
<title>Portal de Cruce de Cuentas GLM</title> <title>Portal de Cruce de Cuentas GLM</title>
<link rel="icon" type="image/png" href="/cruce-cuentas/favicon.png" /> <link rel="icon" type="image/png" href="/cruce-cuentas/favicon.png" />
<link rel="apple-touch-icon" href="/cruce-cuentas/favicon.png" /> <link rel="apple-touch-icon" href="/cruce-cuentas/favicon.png" />
<script type="module" crossorigin src="/cruce-cuentas/assets/index-D9yGuvWp.js"></script> <script type="module" crossorigin src="/cruce-cuentas/assets/index-BfZQMoC1.js"></script>
<link rel="stylesheet" crossorigin href="/cruce-cuentas/assets/index-CDcNm7E6.css"> <link rel="stylesheet" crossorigin href="/cruce-cuentas/assets/index-CLG5SAqX.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+3 -2
View File
@@ -65,8 +65,9 @@ export default function BambooCorrectionModal({
const recipientLabel = useMemo(() => { const recipientLabel = useMemo(() => {
if (country.code === 'TT') return 'Laurele Nicome-Jack'; if (country.code === 'TT') return 'Laurele Nicome-Jack';
if (country.code === 'PA') return 'Delarys Peñalba y Ariadne Cortéz'; if (country.code === 'PA') return 'Delarys Peñalba';
return 'Pablo Gamboa Cú, Sara Meyer Lima y María Morales Adler'; if (country.code === 'SV') return 'Carlos Antonio Malta Méndez';
return 'haguirre@gomezleemarketing.com';
}, [country.code]); }, [country.code]);
const sourceNames = useMemo( const sourceNames = useMemo(
+1 -1
View File
@@ -36,7 +36,7 @@ export default function CountrySelector({ onSelect }: CountrySelectorProps) {
</div> </div>
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{COUNTRIES.map((country) => ( {COUNTRIES.map((country) => (
<button <button
key={country.code} key={country.code}
+30 -19
View File
@@ -84,6 +84,13 @@ function getReconciliationUrl(countryCode: string) {
); );
} }
if (countryCode === 'SV') {
return (
(import.meta.env.VITE_N8N_EL_SALVADOR_WEBHOOK_URL as string | undefined) ||
'https://agenteit.digitalcompass.agency/webhook/nominasv-bamboo-test'
);
}
return ( return (
(import.meta.env.VITE_N8N_GUATEMALA_WEBHOOK_URL as string | undefined) || (import.meta.env.VITE_N8N_GUATEMALA_WEBHOOK_URL as string | undefined) ||
(import.meta.env.VITE_N8N_WEBHOOK_URL as string | undefined) || (import.meta.env.VITE_N8N_WEBHOOK_URL as string | undefined) ||
@@ -787,6 +794,8 @@ export default function Dashboard({
}: DashboardProps) { }: DashboardProps) {
const defaultPeriod = useMemo(() => readStoredPeriod(country.code), [country.code]); const defaultPeriod = useMemo(() => readStoredPeriod(country.code), [country.code]);
const isPanama = country.code === 'PA'; const isPanama = country.code === 'PA';
const isElSalvador = country.code === 'SV';
const isMultiPayrollCountry = isPanama || isElSalvador;
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const [processingStep, setProcessingStep] = useState(''); const [processingStep, setProcessingStep] = useState('');
const [filterMode, setFilterMode] = useState< const [filterMode, setFilterMode] = useState<
@@ -1000,11 +1009,11 @@ export default function Dashboard({
} }
}; };
const addPanamaPayrollFiles = async (files: File[]) => { const addMultiPayrollFiles = async (files: File[]) => {
const excelFiles = files.filter(isExcelFile); const excelFiles = files.filter(isExcelFile);
if (excelFiles.length === 0) { if (excelFiles.length === 0) {
onShowToast('Formato inválido. Panamá requiere uno o varios archivos Excel (.xlsx, .xls).', 'error'); onShowToast(`Formato inválido. ${country.name} requiere uno o varios archivos Excel (.xlsx, .xls).`, 'error');
return; return;
} }
@@ -1027,7 +1036,7 @@ export default function Dashboard({
const detected = await applyDetectedPayrollPeriod(detectionFile, combined.length); const detected = await applyDetectedPayrollPeriod(detectionFile, combined.length);
if (!detected) { if (!detected) {
onShowToast(`${newFiles.length} archivo(s) de nómina de Panamá cargado(s) con éxito.`, 'success'); onShowToast(`${newFiles.length} archivo(s) de nómina de ${country.name} cargado(s) con éxito.`, 'success');
} }
}; };
@@ -1040,8 +1049,8 @@ export default function Dashboard({
const files = Array.from(e.dataTransfer.files) as File[]; const files = Array.from(e.dataTransfer.files) as File[];
if (files.length === 0) return; if (files.length === 0) return;
if (isPanama) { if (isMultiPayrollCountry) {
await addPanamaPayrollFiles(files); await addMultiPayrollFiles(files);
return; return;
} }
@@ -1072,8 +1081,8 @@ export default function Dashboard({
const files = Array.from(e.target.files || []) as File[]; const files = Array.from(e.target.files || []) as File[];
if (files.length === 0) return; if (files.length === 0) return;
if (isPanama) { if (isMultiPayrollCountry) {
await addPanamaPayrollFiles(files); await addMultiPayrollFiles(files);
return; return;
} }
@@ -1085,7 +1094,7 @@ export default function Dashboard({
} }
}; };
const removePanamaPayrollFile = (indexToRemove: number) => { const removeMultiPayrollFile = (indexToRemove: number) => {
setUploadedPayrollFiles( setUploadedPayrollFiles(
uploadedPayrollFiles.filter((_, index) => index !== indexToRemove) uploadedPayrollFiles.filter((_, index) => index !== indexToRemove)
); );
@@ -1117,7 +1126,7 @@ export default function Dashboard({
onShowToast('Planilla de banco removida.', 'info'); onShowToast('Planilla de banco removida.', 'info');
}; };
const clearPanamaUploadedFiles = () => { const clearUploadedFiles = () => {
setUploadedPayroll(null); setUploadedPayroll(null);
setUploadedPayrollFiles([]); setUploadedPayrollFiles([]);
setUploadedBankFiles([]); setUploadedBankFiles([]);
@@ -1208,7 +1217,7 @@ export default function Dashboard({
return; return;
} }
const payrollFilesForRun = isPanama const payrollFilesForRun = isMultiPayrollCountry
? uploadedPayrollFiles ? uploadedPayrollFiles
: (uploadedPayroll ? [uploadedPayroll] : []); : (uploadedPayroll ? [uploadedPayroll] : []);
@@ -1217,8 +1226,8 @@ export default function Dashboard({
payrollFilesForRun.some((file) => !file.rawFile) payrollFilesForRun.some((file) => !file.rawFile)
) { ) {
onShowToast( onShowToast(
isPanama isMultiPayrollCountry
? 'Debes cargar la nómina principal y los suplementos de Panamá antes de ejecutar el cruce.' ? `Debes cargar todos los archivos de nómina de ${country.name} correspondientes al período antes de ejecutar el cruce.`
: 'Debes cargar la nómina antes de ejecutar el cruce.', : 'Debes cargar la nómina antes de ejecutar el cruce.',
'error' 'error'
); );
@@ -1297,7 +1306,7 @@ export default function Dashboard({
Object.entries(metadata).forEach(([key, value]) => { Object.entries(metadata).forEach(([key, value]) => {
formData.append(key, Array.isArray(value) ? JSON.stringify(value) : String(value)); formData.append(key, Array.isArray(value) ? JSON.stringify(value) : String(value));
}); });
if (isPanama) { if (isMultiPayrollCountry) {
payrollFilesForRun.forEach((file, index) => { payrollFilesForRun.forEach((file, index) => {
if (!file.rawFile) return; if (!file.rawFile) return;
formData.append('payroll_files', file.rawFile, file.name); formData.append('payroll_files', file.rawFile, file.name);
@@ -1639,11 +1648,11 @@ export default function Dashboard({
</div> </div>
</section> </section>
{isPanama && (uploadedPayrollFiles.length > 0 || uploadedBankFiles.length > 0) && ( {isMultiPayrollCountry && (uploadedPayrollFiles.length > 0 || uploadedBankFiles.length > 0) && (
<div className="flex justify-end" style={{ marginBottom: '-20px' }}> <div className="flex justify-end" style={{ marginBottom: '-20px' }}>
<button <button
type="button" type="button"
onClick={clearPanamaUploadedFiles} onClick={clearUploadedFiles}
className="inline-flex items-center gap-2 border border-red-100 bg-white px-3 py-2 text-[10px] font-black text-red-600 hover:bg-red-50 transition-colors uppercase tracking-wide shadow-sm" className="inline-flex items-center gap-2 border border-red-100 bg-white px-3 py-2 text-[10px] font-black text-red-600 hover:bg-red-50 transition-colors uppercase tracking-wide shadow-sm"
aria-label="Borrar todos los archivos cargados" aria-label="Borrar todos los archivos cargados"
title="Eliminar de una vez todas las nóminas y planillas del banco cargadas" title="Eliminar de una vez todas las nóminas y planillas del banco cargadas"
@@ -1668,11 +1677,11 @@ export default function Dashboard({
ref={payrollInputRef} ref={payrollInputRef}
onChange={handlePayrollSelect} onChange={handlePayrollSelect}
accept=".xlsx,.xls" accept=".xlsx,.xls"
multiple={isPanama} multiple={isMultiPayrollCountry}
className="hidden" className="hidden"
/> />
{isPanama ? ( {isMultiPayrollCountry ? (
uploadedPayrollFiles.length > 0 ? ( uploadedPayrollFiles.length > 0 ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="border border-[#4F758B]/20 bg-[#4F758B]/5 px-4 py-3"> <div className="border border-[#4F758B]/20 bg-[#4F758B]/5 px-4 py-3">
@@ -1680,7 +1689,9 @@ export default function Dashboard({
{uploadedPayrollFiles.length} archivo(s) de nómina cargado(s) {uploadedPayrollFiles.length} archivo(s) de nómina cargado(s)
</p> </p>
<p className="text-[10px] text-neutral-500 mt-1"> <p className="text-[10px] text-neutral-500 mt-1">
Incluye la quincenal y todas las planillas extraordinarias, vacaciones o XIII Mes que correspondan al período. {isPanama
? 'Incluye la quincenal y todas las planillas extraordinarias, vacaciones o XIII Mes que correspondan al período.'
: 'Incluye todos los archivos de nómina de El Salvador que correspondan a la quincena seleccionada (General, Karla Elias, Nestlé, Motorola u otros del mismo formato).'}
</p> </p>
</div> </div>
@@ -1697,7 +1708,7 @@ export default function Dashboard({
</div> </div>
</div> </div>
<button <button
onClick={() => removePanamaPayrollFile(index)} onClick={() => removeMultiPayrollFile(index)}
className="p-1.5 text-neutral-400 hover:text-red-500 hover:bg-neutral-100 transition-all rounded" className="p-1.5 text-neutral-400 hover:text-red-500 hover:bg-neutral-100 transition-all rounded"
aria-label={`Remover ${file.name}`} aria-label={`Remover ${file.name}`}
> >
+16 -2
View File
@@ -40,6 +40,13 @@ function getHistoryUrl(countryCode: string) {
); );
} }
if (countryCode === 'SV') {
return (
(import.meta.env.VITE_HISTORICOS_SV_URL as string | undefined) ||
'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-sv-historicos'
);
}
return ( return (
(import.meta.env.VITE_HISTORICOS_GT_URL as string | undefined) || (import.meta.env.VITE_HISTORICOS_GT_URL as string | undefined) ||
'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-gt-historicos' 'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-gt-historicos'
@@ -58,6 +65,13 @@ function getMarkResolvedUrl(countryCode: string) {
); );
} }
if (countryCode === 'SV') {
return (
(import.meta.env.VITE_MARCAR_RESUELTO_SV_URL as string | undefined) ||
'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-sv-marcar-resuelto'
);
}
return ( return (
(import.meta.env.VITE_MARCAR_RESUELTO_GT_URL as string | undefined) || (import.meta.env.VITE_MARCAR_RESUELTO_GT_URL as string | undefined) ||
'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-gt-marcar-resuelto' 'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-gt-marcar-resuelto'
@@ -143,7 +157,7 @@ function normalizeReports(input: unknown, country: CountryConfig): HistoricalRep
return { return {
id: String(report.id ?? `reporte-${index + 1}`), id: String(report.id ?? `reporte-${index + 1}`),
country: (rawCountry === 'TT' ? 'TT' : rawCountry === 'PA' ? 'PA' : 'GT') as HistoricalReport['country'], country: (rawCountry === 'TT' ? 'TT' : rawCountry === 'PA' ? 'PA' : rawCountry === 'SV' ? 'SV' : 'GT') as HistoricalReport['country'],
date: formatDate(report.fecha ?? report.period_end ?? rawExecutionDate, country.locale), date: formatDate(report.fecha ?? report.period_end ?? rawExecutionDate, country.locale),
executionDate: formatDateTime(rawExecutionDate, country.locale), executionDate: formatDateTime(rawExecutionDate, country.locale),
period: String(report.periodo ?? report.period_label ?? report.period ?? 'Período no especificado'), period: String(report.periodo ?? report.period_label ?? report.period ?? 'Período no especificado'),
@@ -457,7 +471,7 @@ export default function History({
if (!isCurrentRequest()) return; if (!isCurrentRequest()) return;
// Respaldo temporal: webhook histórico existente. También se envía el país // Respaldo temporal: webhook histórico existente. También se envía el país
// para impedir que se mezclen reportes de Guatemala, Trinidad y Tobago y Panamá. // para impedir que se mezclen reportes de Guatemala, Trinidad y Tobago, Panamá y El Salvador.
const historyUrl = getHistoryUrl(country.code); const historyUrl = getHistoryUrl(country.code);
if (!historyUrl) { if (!historyUrl) {
throw new Error(`No está configurado el origen histórico de ${country.name}.`); throw new Error(`No está configurado el origen histórico de ${country.name}.`);
+8
View File
@@ -25,6 +25,14 @@ export const COUNTRIES: CountryConfig[] = [
locale: 'es-PA', locale: 'es-PA',
enabled: true, enabled: true,
}, },
{
code: 'SV',
name: 'El Salvador',
flag: 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/Flag_of_El_Salvador.svg.webp',
currency: 'USD',
locale: 'es-SV',
enabled: true,
},
]; ];
export const DEFAULT_COUNTRY_CODE: CountryCode = 'GT'; export const DEFAULT_COUNTRY_CODE: CountryCode = 'GT';
+1 -1
View File
@@ -4,7 +4,7 @@ export type ViewType = 'cruce' | 'historial';
export type ToastType = 'success' | 'error' | 'info'; export type ToastType = 'success' | 'error' | 'info';
export type CountryCode = 'GT' | 'TT' | 'PA'; export type CountryCode = 'GT' | 'TT' | 'PA' | 'SV';
export interface CountryConfig { export interface CountryConfig {
code: CountryCode; code: CountryCode;