Agregar Puerto Rico y actualizar dist validado para produccion

This commit is contained in:
2026-09-11 09:51:42 -04:00
parent 389fabe3f2
commit 8ff13ceb9f
27 changed files with 4487 additions and 1950 deletions
+5
View File
@@ -32,6 +32,11 @@ VITE_N8N_HONDURAS_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/no
VITE_HISTORICOS_HN_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-hn-historicos" VITE_HISTORICOS_HN_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-hn-historicos"
VITE_MARCAR_RESUELTO_HN_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-hn-marcar-resuelto" VITE_MARCAR_RESUELTO_HN_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-hn-marcar-resuelto"
# Puerto Rico: cruce, históricos y resolución.
VITE_N8N_PUERTO_RICO_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/nominapr-bamboo-test"
VITE_HISTORICOS_PR_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-pr-historicos"
VITE_MARCAR_RESUELTO_PR_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-pr-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.
VITE_ANALIZAR_NOMINA_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-analizar-nomina" VITE_ANALIZAR_NOMINA_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-analizar-nomina"
@@ -0,0 +1,22 @@
# Corrección final de conciliación — Honduras · 08-09-2026
Se corrigió el workflow principal de Honduras después de comparar la ejecución real de Junio Q30 con los archivos fuente y con el reporte de referencia.
Cambios aplicados:
- Tolerancia monetaria de ±0.05 para diferencias puramente de redondeo.
- La primera pasada de conciliación busca montos dentro de la misma tolerancia, no solo el centavo exacto.
- `Banco sin Nómina` se conserva por transacción bancaria individual y no se agrupa por persona/cuenta.
- Los truncamientos bancarios de un carácter al final (ej. `ALVARAD` → `ALVARADO`) no se reportan como diferencia de nombre.
- No se toleran sustituciones ortográficas internas (ej. `GONZALES` → `GONZALEZ`), que siguen requiriendo revisión.
- Para BambooHR, el nombre legal del cuentahabiente y la pertenencia al período tienen prioridad sobre un nombre corto ambiguo. Esto corrige el falso positivo de Suyapa Palma.
- La auditoría directa del Excel original confirmó que Harold Panchame tiene `756669711` en nómina y banco; por lo tanto no es una cuenta mal digitada. El caso real confirmado es Brenda Baquedano (`745899511` → `745899611`).
Resultados de referencia finales:
| Período | Coincidencias | Discrepancias | Banco sin nómina | Banco sin Bamboo | Diferencias nombre banco | Cuenta mal digitada |
|---|---:|---:|---:|---:|---:|---:|
| Junio Q15 | 143 | 122 | 0 | 5 | 0 | 1 |
| Junio Q30 | 331 | 0 | 176 | 5 | 5 | 1 |
| Julio Q15 | 263 | 0 | 0 | 4 | 1 | 1 |
| Julio Q30 | 335 | 0 | 1 | 5 | 3 | 1 |
+16
View File
@@ -0,0 +1,16 @@
# Puerto Rico — integración READY
Esta versión parte del paquete corregido existente y conserva sin reemplazo la autenticación Supabase/Google y la lógica de históricos de los países existentes.
Puerto Rico:
- Código: PR
- Moneda: USD
- Locale: es-PR
- Nómina: un Excel por período
- Banco: PDF de Banco Popular de Puerto Rico
- Webhook principal: /webhook/nominapr-bamboo-test
- Históricos: /webhook/cruce-cuentas-pr-historicos
- Marcar resuelto: /webhook/cruce-cuentas-pr-marcar-resuelto
- Correcciones BambooHR: Noralmy Laureano
La compilación `dist/` se genera usando el mismo `.env.local` del paquete base, con las tres variables PR agregadas. Esto conserva el login y la conexión a Supabase usada por Históricos.
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
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,163 @@
{
"name": "Portal de Verificación de Nómina Puerto Rico - Históricos",
"nodes": [
{
"parameters": {
"path": "cruce-cuentas-pr-historicos",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
304,
272
],
"id": "dff3b1cf-ebbe-4525-b367-0479d76a102d",
"name": "Webhook Históricos PR",
"webhookId": "24baab72-bc55-439d-96db-94780a3dffdf"
},
{
"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_pr_listos',\n count: reports.length,\n reports,\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
272
],
"id": "9c4a3deb-631f-409f-b3ac-a86c75514f14",
"name": "Normalizar históricos para app"
},
{
"parameters": {
"url": "https://dbit.digitalcompass.agency/rest/v1/cruces_cuentas_gt_reportes?select=*&country=eq.PR&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": [
512,
272
],
"id": "70248bd5-e8ff-4ca5-9770-3a68ca418a12",
"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": [
944,
272
],
"id": "65b35ac9-9272-4abf-86e4-c9330322c8ab",
"name": "Responder históricos app"
},
{
"parameters": {
"content": "# 📚 HISTÓRICOS DE VERIFICACIÓN — PUERTO RICO\n\nEntrega a la aplicación el historial de reportes procesados para Puerto Rico.\n\n- Consulta únicamente registros con `country = PR`.\n- Supabase continúa usando la tabla histórica compartida `cruces_cuentas_gt_reportes`; el nombre de la tabla es legado y funciona como tabla multi-país.\n- Devuelve la misma estructura de históricos utilizada por los demás países.\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": [
0,
0
],
"id": "8c0b0a43-8bfe-443b-8795-06d79512ca64",
"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 PR": {
"main": [
[
{
"node": "Consultar históricos Supabase",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate"
},
"versionId": "ffb6f922-6c37-498e-9dec-0011deddb49e",
"meta": {
"instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70"
},
"tags": []
}
+311
View File
@@ -0,0 +1,311 @@
-- ============================================================
-- CRUCE DE CUENTAS GLM · HABILITAR PUERTO RICO (PR)
-- GT + TT + PA + SV + HN + PR
-- ============================================================
-- Ejecutar una sola vez en el SQL Editor del Supabase empresarial.
-- Es idempotente respecto a PR: conserva datos existentes y puede
-- volver a ejecutarse si se interrumpe antes del COMMIT.
begin;
-- 1) Validar tablas requeridas y ampliar constraints de país.
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.'; end if;
if to_regclass('public.cruce_cuentas_bamboo_correcciones') is null then raise exception 'No existe public.cruce_cuentas_bamboo_correcciones.'; end if;
if to_regclass('public.cruces_cuentas_gt_reportes') is null then raise exception 'No existe public.cruces_cuentas_gt_reportes.'; end if;
if to_regclass('public.cruce_cuentas_usuarios_autorizados') is null then raise exception 'No existe public.cruce_cuentas_usuarios_autorizados.'; end if;
-- Alias BambooHR.
for r in
select conname
from pg_constraint
where conrelid='public.cruce_cuentas_bamboo_aliases'::regclass
and contype='c'
and pg_get_constraintdef(oid) ilike '%pais%'
and pg_get_constraintdef(oid) ilike '%GT%'
and pg_get_constraintdef(oid) not ilike '%PR%'
loop
execute format('alter table public.cruce_cuentas_bamboo_aliases drop constraint %I',r.conname);
end loop;
alter table public.cruce_cuentas_bamboo_aliases drop constraint if exists ck_cruce_cuentas_bamboo_aliases_pais;
alter table public.cruce_cuentas_bamboo_aliases
add constraint ck_cruce_cuentas_bamboo_aliases_pais
check (upper(trim(pais)) in ('GT','TT','PA','SV','HN','PR'));
-- Solicitudes/correcciones BambooHR.
for r in
select conname
from pg_constraint
where conrelid='public.cruce_cuentas_bamboo_correcciones'::regclass
and contype='c'
and pg_get_constraintdef(oid) ilike '%pais%'
and pg_get_constraintdef(oid) ilike '%GT%'
and pg_get_constraintdef(oid) not ilike '%PR%'
loop
execute format('alter table public.cruce_cuentas_bamboo_correcciones drop constraint %I',r.conname);
end loop;
alter table public.cruce_cuentas_bamboo_correcciones drop constraint if exists ck_cruce_cuentas_bamboo_correcciones_pais;
alter table public.cruce_cuentas_bamboo_correcciones
add constraint ck_cruce_cuentas_bamboo_correcciones_pais
check (upper(trim(pais)) in ('GT','TT','PA','SV','HN','PR'));
-- Históricos multi-país (el nombre de la tabla es legado).
for r in
select conname
from pg_constraint
where conrelid='public.cruces_cuentas_gt_reportes'::regclass
and contype='c'
and pg_get_constraintdef(oid) ilike '%country%'
and pg_get_constraintdef(oid) ilike '%GT%'
and pg_get_constraintdef(oid) not ilike '%PR%'
loop
execute format('alter table public.cruces_cuentas_gt_reportes drop constraint %I',r.conname);
end loop;
alter table public.cruces_cuentas_gt_reportes drop constraint if exists ck_cruces_cuentas_reportes_country;
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','HN','PR'));
-- Accesos: text[] de países habilitados.
alter table public.cruce_cuentas_usuarios_autorizados drop constraint if exists cruce_cuentas_paises_validos_check;
alter table public.cruce_cuentas_usuarios_autorizados
add constraint cruce_cuentas_paises_validos_check
check (paises is null or paises <@ array['GT','TT','PA','SV','HN','PR']::text[]);
end;
$$;
-- 2) Dar PR a todos los usuarios actualmente autorizados, preservando permisos.
update public.cruce_cuentas_usuarios_autorizados
set paises = case
when paises is null then array['GT','TT','PA','SV','HN','PR']::text[]
when not ('PR'=any(paises)) then array_append(paises,'PR')
else paises
end,
updated_at = now();
-- 3) Alias BambooHR activos: habilitar PR en la función compartida.
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','HN','PR');
$$;
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;
-- 4) Históricos paginados multi-país, incluyendo PR.
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','HN','PR') 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;
-- 5) Detalle Banco sin BambooHR de históricos, incluyendo PR.
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','HN','PR') 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;
-- ============================================================
-- VALIDACIÓN (solo lectura)
-- ============================================================
select email,nombre,paises
from public.cruce_cuentas_usuarios_autorizados
order by email;
select 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,
'public.cruce_cuentas_usuarios_autorizados'::regclass
)
and contype='c'
and (pg_get_constraintdef(oid) ilike '%pais%' or pg_get_constraintdef(oid) ilike '%country%')
order by conname;
select public.cruce_cuentas_get_historicos('PR',1,10,null,null) as historicos_pr;
+18
View File
@@ -0,0 +1,18 @@
# Validación final · Puerto Rico
Fecha: 10-09-2026
Este paquete parte de la versión `GT-TT-PA-SV-HN-BONO14-FINAL-READY-CORREGIDO` y agrega Puerto Rico sin sustituir la autenticación ni la lógica histórica existente.
Validaciones realizadas:
- `src/main.tsx` y el árbol completo `src/` están incluidos.
- `App.tsx`, `Login.tsx`, `lib/auth.ts`, `lib/sessionHealth.ts`, `lib/supabase.ts` y `lib/tabSessionState.ts` se conservaron idénticos a la base corregida.
- Puerto Rico está habilitado como `PR`, moneda `USD`, locale `es-PR` y bandera oficial configurada.
- La carga bancaria para Puerto Rico acepta PDF; los demás países conservan CSV.
- Históricos de PR usan `cruce_cuentas_get_historicos` y el fallback `cruce-cuentas-pr-historicos`.
- Resolución de PR usa `cruce-cuentas-pr-marcar-resuelto`.
- El cruce de PR usa `nominapr-bamboo-test`.
- El frontend de correcciones muestra a Noralmy Laureano para Puerto Rico.
- `.env.local` conserva Supabase Auth y agrega únicamente los tres endpoints PR.
- El `dist/` incluido contiene login habilitado, Supabase, históricos y endpoints PR, con cache-buster nuevo.
- Los cinco workflows PR/compartidos incluidos son JSON válidos; sus conexiones apuntan a nodos existentes y los Code Nodes pasan validación sintáctica.
-270
View File
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
+1 -1
View File
@@ -9,7 +9,7 @@
<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-BmyDbce7.js"></script> <script type="module" crossorigin src="/cruce-cuentas/assets/index-saVPcx_I.js"></script>
<link rel="stylesheet" crossorigin href="/cruce-cuentas/assets/index-CLG5SAqX.css"> <link rel="stylesheet" crossorigin href="/cruce-cuentas/assets/index-CLG5SAqX.css">
</head> </head>
<body> <body>
+1
View File
@@ -68,6 +68,7 @@ export default function BambooCorrectionModal({
if (country.code === 'PA') return 'Delarys Peñalba'; if (country.code === 'PA') return 'Delarys Peñalba';
if (country.code === 'SV') return 'Carlos Antonio Malta Méndez'; if (country.code === 'SV') return 'Carlos Antonio Malta Méndez';
if (country.code === 'HN') return 'Melody Chang Blandin'; if (country.code === 'HN') return 'Melody Chang Blandin';
if (country.code === 'PR') return 'Noralmy Laureano';
return 'haguirre@gomezleemarketing.com'; return 'haguirre@gomezleemarketing.com';
}, [country.code]); }, [country.code]);
+41 -19
View File
@@ -98,6 +98,13 @@ function getReconciliationUrl(countryCode: string) {
); );
} }
if (countryCode === 'PR') {
return (
(import.meta.env.VITE_N8N_PUERTO_RICO_WEBHOOK_URL as string | undefined) ||
'https://agenteit.digitalcompass.agency/webhook/nominapr-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) ||
@@ -152,6 +159,10 @@ function isCsvFile(file: File) {
return file.name.toLowerCase().endsWith('.csv'); return file.name.toLowerCase().endsWith('.csv');
} }
function isPdfFile(file: File) {
return file.name.toLowerCase().endsWith('.pdf');
}
function toUploadedPayrollFiles(files: FileList | File[]) { function toUploadedPayrollFiles(files: FileList | File[]) {
return Array.from(files).map((file) => ({ return Array.from(files).map((file) => ({
name: file.name, name: file.name,
@@ -814,7 +825,10 @@ export default function Dashboard({
const isPanama = country.code === 'PA'; const isPanama = country.code === 'PA';
const isElSalvador = country.code === 'SV'; const isElSalvador = country.code === 'SV';
const isHonduras = country.code === 'HN'; const isHonduras = country.code === 'HN';
const isPuertoRico = country.code === 'PR';
const isMultiPayrollCountry = isPanama || isElSalvador; const isMultiPayrollCountry = isPanama || isElSalvador;
const bankFileLabel = isPuertoRico ? 'PDF' : 'CSV';
const bankFileExtensionLabel = isPuertoRico ? '.pdf' : '.csv';
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<
@@ -1084,16 +1098,19 @@ export default function Dashboard({
const handleBankDrop = (e: React.DragEvent) => { const handleBankDrop = (e: React.DragEvent) => {
e.preventDefault(); e.preventDefault();
const files = Array.from(e.dataTransfer.files) as File[]; const files = Array.from(e.dataTransfer.files) as File[];
const csvFiles = files.filter(isCsvFile); const validBankFiles = files.filter(isPuertoRico ? isPdfFile : isCsvFile);
if (csvFiles.length === 0) { if (validBankFiles.length === 0) {
onShowToast('Formato inválido. Por favor carga uno o varios archivos CSV (.csv).', 'error'); onShowToast(
`Formato inválido. Por favor carga uno o varios archivos ${bankFileLabel} (${bankFileExtensionLabel}).`,
'error'
);
return; return;
} }
setUploadedBankFiles([...uploadedBankFiles, ...toUploadedBankFiles(csvFiles)]); setUploadedBankFiles([...uploadedBankFiles, ...toUploadedBankFiles(validBankFiles)]);
resetResults(); resetResults();
onShowToast(`${csvFiles.length} planilla(s) del banco cargada(s) con éxito.`, 'success'); onShowToast(`${validBankFiles.length} archivo(s) del banco cargado(s) con éxito.`, 'success');
}; };
const handlePayrollSelect = async (e: React.ChangeEvent<HTMLInputElement>) => { const handlePayrollSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -1126,16 +1143,19 @@ export default function Dashboard({
const files = e.target.files; const files = e.target.files;
if (!files || files.length === 0) return; if (!files || files.length === 0) return;
const csvFiles = (Array.from(files) as File[]).filter(isCsvFile); const validBankFiles = (Array.from(files) as File[]).filter(isPuertoRico ? isPdfFile : isCsvFile);
if (csvFiles.length === 0) { if (validBankFiles.length === 0) {
onShowToast('Formato inválido. Por favor carga uno o varios archivos CSV (.csv).', 'error'); onShowToast(
`Formato inválido. Por favor carga uno o varios archivos ${bankFileLabel} (${bankFileExtensionLabel}).`,
'error'
);
return; return;
} }
setUploadedBankFiles([...uploadedBankFiles, ...toUploadedBankFiles(csvFiles)]); setUploadedBankFiles([...uploadedBankFiles, ...toUploadedBankFiles(validBankFiles)]);
resetResults(); resetResults();
onShowToast(`${csvFiles.length} planilla(s) del banco cargada(s) con éxito.`, 'success'); onShowToast(`${validBankFiles.length} archivo(s) del banco cargado(s) con éxito.`, 'success');
}; };
const removeBankFile = (indexToRemove: number) => { const removeBankFile = (indexToRemove: number) => {
@@ -1263,7 +1283,9 @@ export default function Dashboard({
setCurrentPage(1); setCurrentPage(1);
try { try {
const duplicatedShipments = await findDuplicateBankShipments(uploadedBankFiles); const duplicatedShipments = isPuertoRico
? []
: await findDuplicateBankShipments(uploadedBankFiles);
if (duplicatedShipments.length > 0) { if (duplicatedShipments.length > 0) {
const details = duplicatedShipments const details = duplicatedShipments
.map((item) => `Número de envío ${item.shipmentNumber}: ${item.fileNames.join(', ')}`) .map((item) => `Número de envío ${item.shipmentNumber}: ${item.fileNames.join(', ')}`)
@@ -1667,7 +1689,7 @@ export default function Dashboard({
</div> </div>
</section> </section>
{(isMultiPayrollCountry || isHonduras) && {(isMultiPayrollCountry || isHonduras || isPuertoRico) &&
(uploadedPayroll !== null || uploadedPayrollFiles.length > 0 || uploadedBankFiles.length > 0) && ( (uploadedPayroll !== null || uploadedPayrollFiles.length > 0 || uploadedBankFiles.length > 0) && (
<div className="flex justify-end" style={{ marginBottom: '-20px' }}> <div className="flex justify-end" style={{ marginBottom: '-20px' }}>
<button <button
@@ -1823,7 +1845,7 @@ export default function Dashboard({
type="file" type="file"
ref={bankInputRef} ref={bankInputRef}
onChange={handleBankSelect} onChange={handleBankSelect}
accept=".csv" accept={isPuertoRico ? ".pdf" : ".csv"}
multiple multiple
className="hidden" className="hidden"
/> />
@@ -1835,8 +1857,8 @@ export default function Dashboard({
className="border-2 border-dashed border-[#D0D0D0] bg-neutral-50 hover:bg-[#4F758B]/5 hover:border-[#4F758B]/40 transition-colors flex flex-col items-center justify-center py-8 px-4 text-center cursor-pointer min-h-[130px]" className="border-2 border-dashed border-[#D0D0D0] bg-neutral-50 hover:bg-[#4F758B]/5 hover:border-[#4F758B]/40 transition-colors flex flex-col items-center justify-center py-8 px-4 text-center cursor-pointer min-h-[130px]"
> >
<Upload className="w-7 h-7 text-neutral-300 mb-2" /> <Upload className="w-7 h-7 text-neutral-300 mb-2" />
<p className="text-xs font-bold text-neutral-700">Arrastre y suelte archivo CSV</p> <p className="text-xs font-bold text-neutral-700">{`Arrastre y suelte archivo ${bankFileLabel}`}</p>
<p className="text-[10px] text-neutral-400 mt-1 font-medium">Formato .csv</p> <p className="text-[10px] text-neutral-400 mt-1 font-medium">{`Formato ${bankFileExtensionLabel}`}</p>
<button <button
type="button" type="button"
className="mt-4 border border-[#D0D0D0] bg-white px-4 py-1.5 text-[10px] font-bold text-neutral-600 hover:border-[#4F758B] hover:text-[#4F758B] transition-colors shadow-sm uppercase tracking-wide" className="mt-4 border border-[#D0D0D0] bg-white px-4 py-1.5 text-[10px] font-bold text-neutral-600 hover:border-[#4F758B] hover:text-[#4F758B] transition-colors shadow-sm uppercase tracking-wide"
@@ -1878,9 +1900,9 @@ export default function Dashboard({
value={bankFileSearch} value={bankFileSearch}
onChange={(event) => setBankFileSearch(event.target.value)} onChange={(event) => setBankFileSearch(event.target.value)}
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
placeholder="Buscar archivo CSV por nombre..." placeholder={`Buscar archivo ${bankFileLabel} por nombre...`}
className="w-full h-9 pl-9 pr-3 border border-[#D0D0D0] bg-white text-[11px] text-neutral-700 outline-none focus:border-[#4F758B] focus:ring-2 focus:ring-[#4F758B]/10 transition-all" className="w-full h-9 pl-9 pr-3 border border-[#D0D0D0] bg-white text-[11px] text-neutral-700 outline-none focus:border-[#4F758B] focus:ring-2 focus:ring-[#4F758B]/10 transition-all"
aria-label="Buscar archivos CSV cargados" aria-label={`Buscar archivos ${bankFileLabel} cargados`}
/> />
</div> </div>
@@ -1912,7 +1934,7 @@ export default function Dashboard({
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-1">
<span className="text-[9px] text-neutral-400 font-mono">{file.size}</span> <span className="text-[9px] text-neutral-400 font-mono">{file.size}</span>
<span className="text-[8px] font-bold uppercase tracking-wide text-[#4F758B] bg-[#4F758B]/5 border border-[#4F758B]/10 px-1.5 py-0.5"> <span className="text-[8px] font-bold uppercase tracking-wide text-[#4F758B] bg-[#4F758B]/5 border border-[#4F758B]/10 px-1.5 py-0.5">
CSV {bankFileLabel}
</span> </span>
</div> </div>
</div> </div>
@@ -1935,7 +1957,7 @@ export default function Dashboard({
<div className="py-8 px-4 text-center"> <div className="py-8 px-4 text-center">
<Search className="w-5 h-5 text-neutral-300 mx-auto mb-2" /> <Search className="w-5 h-5 text-neutral-300 mx-auto mb-2" />
<p className="text-[11px] font-bold text-neutral-600">No encontramos archivos con ese nombre.</p> <p className="text-[11px] font-bold text-neutral-600">No encontramos archivos con ese nombre.</p>
<p className="text-[9px] text-neutral-400 mt-1">Pruebe con otra parte del nombre del CSV.</p> <p className="text-[9px] text-neutral-400 mt-1">Pruebe con otra parte del nombre del archivo.</p>
</div> </div>
)} )}
</div> </div>
+16 -2
View File
@@ -54,6 +54,13 @@ function getHistoryUrl(countryCode: string) {
); );
} }
if (countryCode === 'PR') {
return (
(import.meta.env.VITE_HISTORICOS_PR_URL as string | undefined) ||
'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-pr-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'
@@ -86,6 +93,13 @@ function getMarkResolvedUrl(countryCode: string) {
); );
} }
if (countryCode === 'PR') {
return (
(import.meta.env.VITE_MARCAR_RESUELTO_PR_URL as string | undefined) ||
'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-pr-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'
@@ -171,7 +185,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' : rawCountry === 'SV' ? 'SV' : rawCountry === 'HN' ? 'HN' : 'GT') as HistoricalReport['country'], country: (rawCountry === 'TT' ? 'TT' : rawCountry === 'PA' ? 'PA' : rawCountry === 'SV' ? 'SV' : rawCountry === 'HN' ? 'HN' : rawCountry === 'PR' ? 'PR' : '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'),
@@ -485,7 +499,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, Panamá, El Salvador y Honduras. // para impedir que se mezclen reportes de Guatemala, Trinidad y Tobago, Panamá, El Salvador, Honduras y Puerto Rico.
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
@@ -41,6 +41,14 @@ export const COUNTRIES: CountryConfig[] = [
locale: 'es-HN', locale: 'es-HN',
enabled: true, enabled: true,
}, },
{
code: 'PR',
name: 'Puerto Rico',
flag: 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/bandera-nacional-tela-puerto-rico_113767-1759.avif',
currency: 'USD',
locale: 'es-PR',
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' | 'SV' | 'HN'; export type CountryCode = 'GT' | 'TT' | 'PA' | 'SV' | 'HN' | 'PR';
export interface CountryConfig { export interface CountryConfig {
code: CountryCode; code: CountryCode;
+3
View File
@@ -7,6 +7,7 @@ interface ImportMetaEnv {
readonly VITE_N8N_PANAMA_WEBHOOK_URL?: string; readonly VITE_N8N_PANAMA_WEBHOOK_URL?: string;
readonly VITE_N8N_EL_SALVADOR_WEBHOOK_URL?: string; readonly VITE_N8N_EL_SALVADOR_WEBHOOK_URL?: string;
readonly VITE_N8N_HONDURAS_WEBHOOK_URL?: string; readonly VITE_N8N_HONDURAS_WEBHOOK_URL?: string;
readonly VITE_N8N_PUERTO_RICO_WEBHOOK_URL?: string;
readonly VITE_HISTORICOS_GT_URL?: string; readonly VITE_HISTORICOS_GT_URL?: string;
readonly VITE_MARCAR_RESUELTO_GT_URL?: string; readonly VITE_MARCAR_RESUELTO_GT_URL?: string;
readonly VITE_HISTORICOS_TT_URL?: string; readonly VITE_HISTORICOS_TT_URL?: string;
@@ -17,6 +18,8 @@ interface ImportMetaEnv {
readonly VITE_MARCAR_RESUELTO_SV_URL?: string; readonly VITE_MARCAR_RESUELTO_SV_URL?: string;
readonly VITE_HISTORICOS_HN_URL?: string; readonly VITE_HISTORICOS_HN_URL?: string;
readonly VITE_MARCAR_RESUELTO_HN_URL?: string; readonly VITE_MARCAR_RESUELTO_HN_URL?: string;
readonly VITE_HISTORICOS_PR_URL?: string;
readonly VITE_MARCAR_RESUELTO_PR_URL?: string;
readonly VITE_ANALIZAR_NOMINA_URL?: string; readonly VITE_ANALIZAR_NOMINA_URL?: string;
readonly VITE_REPORTAR_BAMBOO_URL?: string; readonly VITE_REPORTAR_BAMBOO_URL?: string;
readonly VITE_REPORTAR_APP_URL?: string; readonly VITE_REPORTAR_APP_URL?: string;
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,163 @@
{
"name": "Portal de Verificación de Nómina Puerto Rico - Históricos",
"nodes": [
{
"parameters": {
"path": "cruce-cuentas-pr-historicos",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
304,
272
],
"id": "dff3b1cf-ebbe-4525-b367-0479d76a102d",
"name": "Webhook Históricos PR",
"webhookId": "24baab72-bc55-439d-96db-94780a3dffdf"
},
{
"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_pr_listos',\n count: reports.length,\n reports,\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
272
],
"id": "9c4a3deb-631f-409f-b3ac-a86c75514f14",
"name": "Normalizar históricos para app"
},
{
"parameters": {
"url": "https://dbit.digitalcompass.agency/rest/v1/cruces_cuentas_gt_reportes?select=*&country=eq.PR&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": [
512,
272
],
"id": "70248bd5-e8ff-4ca5-9770-3a68ca418a12",
"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": [
944,
272
],
"id": "65b35ac9-9272-4abf-86e4-c9330322c8ab",
"name": "Responder históricos app"
},
{
"parameters": {
"content": "# 📚 HISTÓRICOS DE VERIFICACIÓN — PUERTO RICO\n\nEntrega a la aplicación el historial de reportes procesados para Puerto Rico.\n\n- Consulta únicamente registros con `country = PR`.\n- Supabase continúa usando la tabla histórica compartida `cruces_cuentas_gt_reportes`; el nombre de la tabla es legado y funciona como tabla multi-país.\n- Devuelve la misma estructura de históricos utilizada por los demás países.\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": [
0,
0
],
"id": "8c0b0a43-8bfe-443b-8795-06d79512ca64",
"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 PR": {
"main": [
[
{
"node": "Consultar históricos Supabase",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate"
},
"versionId": "ffb6f922-6c37-498e-9dec-0011deddb49e",
"meta": {
"instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70"
},
"tags": []
}