Agregar El Salvador y actualizar dist validado
This commit is contained in:
+13
-3
@@ -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"
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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
+510
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": []
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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/`.
|
||||||
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-270
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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}.`);
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user