-- Cruce de Cuentas GLM -- Equivalencias validadas para falsos positivos de "Banco sin BambooHR" -- Ejecutar en el SQL Editor del Supabase empresarial. -- Script idempotente: puede ejecutarse nuevamente sin borrar datos existentes. begin; create extension if not exists pgcrypto; create or replace function public.cruce_cuentas_normalizar_nombre(p_value text) returns text language sql immutable strict as $$ select trim( regexp_replace( translate( lower(coalesce(p_value, '')), 'áàäâãåéèëêíìïîóòöôõúùüûñçÁÀÄÂÃÅÉÈËÊÍÌÏÎÓÒÖÔÕÚÙÜÛÑÇ', 'aaaaaaeeeeiiiiooooouuuuncAAAAAAEEEEIIIIOOOOOUUUUNC' ), '[^a-z0-9]+', ' ', 'g' ) ); $$; create table if not exists public.cruce_cuentas_bamboo_aliases ( id uuid primary key default gen_random_uuid(), pais text not null check (pais in ('GT', 'TT')), nombre_origen text not null, nombre_origen_normalizado text not null, cuenta_bancaria text not null default '', bamboo_employee_number text, bamboo_employee_id text, bamboo_nombre text not null, motivo text, comentario text, activo boolean not null default true, validado_por_email text not null, validado_por_nombre text, last_reported_at timestamptz not null default now(), created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); -- Compatibilidad si una versión anterior del script creó la tabla sin cuenta. alter table public.cruce_cuentas_bamboo_aliases add column if not exists cuenta_bancaria text not null default ''; drop index if exists public.ux_cruce_cuentas_bamboo_aliases_pais_nombre; create unique index if not exists ux_cruce_cuentas_bamboo_aliases_pais_nombre_cuenta on public.cruce_cuentas_bamboo_aliases (pais, nombre_origen_normalizado, cuenta_bancaria); create index if not exists ix_cruce_cuentas_bamboo_aliases_employee_number on public.cruce_cuentas_bamboo_aliases (pais, bamboo_employee_number) where activo = true and bamboo_employee_number is not null; create table if not exists public.cruce_cuentas_bamboo_correcciones ( id uuid primary key default gen_random_uuid(), pais text not null check (pais in ('GT', 'TT')), nombre_origen_principal text not null, nombres_origen text[] not null default array[]::text[], cuenta_bancaria text, monto_banco numeric(18,2), moneda text, bamboo_employee_number text, bamboo_employee_id text, bamboo_nombre text not null, motivo text not null, comentario text, execution_id text, report_url text, period_label text, period_start date, period_end date, solicitado_por_email text not null, solicitado_por_nombre text, destinatarios_rrhh text[] not null default array[]::text[], alias_ids uuid[] not null default array[]::uuid[], estado text not null default 'pendiente_notificacion' check (estado in ('pendiente_notificacion', 'notificado_rrhh', 'corregido_bamboo', 'cancelado')), correo_enviado boolean not null default false, correo_enviado_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index if not exists ix_cruce_cuentas_bamboo_correcciones_pais_created on public.cruce_cuentas_bamboo_correcciones (pais, created_at desc); create index if not exists ix_cruce_cuentas_bamboo_correcciones_estado on public.cruce_cuentas_bamboo_correcciones (estado, created_at desc); create or replace function public.cruce_cuentas_bamboo_set_normalized() returns trigger language plpgsql set search_path = public as $$ begin new.pais := upper(trim(new.pais)); new.nombre_origen := trim(new.nombre_origen); new.nombre_origen_normalizado := public.cruce_cuentas_normalizar_nombre(new.nombre_origen); new.cuenta_bancaria := regexp_replace(coalesce(new.cuenta_bancaria, ''), '[^0-9A-Za-z]+', '', 'g'); new.validado_por_email := lower(trim(new.validado_por_email)); new.updated_at := now(); return new; end; $$; drop trigger if exists trg_cruce_cuentas_bamboo_alias_normalized on public.cruce_cuentas_bamboo_aliases; create trigger trg_cruce_cuentas_bamboo_alias_normalized before insert or update on public.cruce_cuentas_bamboo_aliases for each row execute function public.cruce_cuentas_bamboo_set_normalized(); create or replace function public.cruce_cuentas_bamboo_touch_updated_at() returns trigger language plpgsql set search_path = public as $$ begin new.updated_at := now(); return new; end; $$; drop trigger if exists trg_cruce_cuentas_bamboo_correcciones_updated_at on public.cruce_cuentas_bamboo_correcciones; create trigger trg_cruce_cuentas_bamboo_correcciones_updated_at before update on public.cruce_cuentas_bamboo_correcciones for each row execute function public.cruce_cuentas_bamboo_touch_updated_at(); -- RPC consumida únicamente por los workflows generadores de reportes. -- Siempre devuelve un objeto con aliases=[], incluso cuando no existen registros. 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'); $$; alter table public.cruce_cuentas_bamboo_aliases enable row level security; alter table public.cruce_cuentas_bamboo_correcciones enable row level security; -- El navegador nunca escribe ni lee estas tablas directamente. -- Toda operación pasa por n8n con service_role. revoke all on table public.cruce_cuentas_bamboo_aliases from anon, authenticated; revoke all on table public.cruce_cuentas_bamboo_correcciones from anon, authenticated; grant all on table public.cruce_cuentas_bamboo_aliases to service_role; grant all on table public.cruce_cuentas_bamboo_correcciones to service_role; 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; -- ============================================================ -- HISTÓRICOS: conservar y recuperar Banco sin Bamboo por reporte -- ============================================================ -- Los workflows ya envían el detalle de cada caso. Esta columna hace que el -- detalle quede persistido y pueda abrirse días después desde Históricos. alter table public.cruces_cuentas_gt_reportes add column if not exists detalle_banco_sin_bamboo jsonb not null default '[]'::jsonb; -- Identificador generado por el navegador para trazabilidad de una ejecución. -- No se usa como llave única porque no se reintenta automáticamente un POST -- de conciliación (evita duplicar reportes, correos o Sheets). alter table public.cruces_cuentas_gt_reportes add column if not exists client_request_id text; update public.cruces_cuentas_gt_reportes set detalle_banco_sin_bamboo = '[]'::jsonb where detalle_banco_sin_bamboo is null; create index if not exists ix_cruces_cuentas_reportes_client_request on public.cruces_cuentas_gt_reportes (client_request_id) where client_request_id is not null; create index if not exists ix_cruce_cuentas_bamboo_correcciones_execution on public.cruce_cuentas_bamboo_correcciones (execution_id, created_at desc) where execution_id is not null; -- Reemplaza la RPC histórica manteniendo exactamente la misma firma y -- estructura pública, pero excluye detalle_banco_sin_bamboo de la lista para -- no descargar todos los casos cada vez que se abre Históricos. El detalle se -- solicita únicamente cuando el usuario pulsa Banco sin Bamboo. 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') 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; -- RPC de detalle utilizada por Históricos al abrir un reporte específico. -- No otorga SELECT directo sobre las tablas de aliases/correcciones. 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') 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 después de ejecutar: -- select public.cruce_cuentas_bamboo_aliases_activos('GT'); -- select public.cruce_cuentas_bamboo_aliases_activos('TT'); -- select column_name, data_type from information_schema.columns -- where table_schema='public' and table_name='cruces_cuentas_gt_reportes' -- and column_name in ('detalle_banco_sin_bamboo', 'client_request_id');