-- Seguimiento de Impuestos GLM -- Actualización: contactos BambooHR, edición ampliada, Google Calendar y bloqueo de fechas pasadas. -- Ejecutar una sola vez en Supabase SQL Editor antes de desplegar el frontend y activar los workflows. begin; -- --------------------------------------------------------------------------- -- Contactos: metadatos de BambooHR y tercer canal de notificación -- --------------------------------------------------------------------------- alter table public.tax_contacts add column if not exists department text, add column if not exists division text, add column if not exists location text, add column if not exists source text not null default 'manual', add column if not exists bamboo_employee_id text, add column if not exists bamboo_employee_number text, add column if not exists bamboo_status text, add column if not exists bamboo_synced_at timestamptz, add column if not exists calendar_enabled boolean not null default true; alter table public.tax_contacts drop constraint if exists tax_contacts_area_check; alter table public.tax_contacts add constraint tax_contacts_area_check check (area in ('admin', 'nomina', 'regional', 'other')); update public.tax_contacts set calendar_enabled = false where email is null or length(trim(email)) <= 3; alter table public.tax_contacts drop constraint if exists tax_contacts_calendar_email_when_enabled; alter table public.tax_contacts add constraint tax_contacts_calendar_email_when_enabled check (not calendar_enabled or length(trim(coalesce(email, ''))) > 3); create unique index if not exists tax_contacts_unique_active_bamboo_employee on public.tax_contacts (bamboo_employee_id) where active = true and bamboo_employee_id is not null; create index if not exists tax_contacts_bamboo_employee_number_idx on public.tax_contacts (bamboo_employee_number) where bamboo_employee_number is not null; -- Amplía la normalización existente para los nuevos campos. create or replace function public.tax_normalize_contact() returns trigger language plpgsql set search_path = '' as $$ begin new.full_name := trim(new.full_name); new.email := nullif(lower(trim(coalesce(new.email, ''))), ''); new.whatsapp_number := nullif(regexp_replace(coalesce(new.whatsapp_number, ''), '[^0-9]', '', 'g'), ''); new.job_title := nullif(trim(coalesce(new.job_title, '')), ''); new.department := nullif(trim(coalesce(new.department, '')), ''); new.division := nullif(trim(coalesce(new.division, '')), ''); new.location := nullif(trim(coalesce(new.location, '')), ''); new.source := coalesce(nullif(lower(trim(coalesce(new.source, ''))), ''), 'manual'); new.bamboo_employee_id := nullif(trim(coalesce(new.bamboo_employee_id, '')), ''); new.bamboo_employee_number := nullif(trim(coalesce(new.bamboo_employee_number, '')), ''); new.bamboo_status := nullif(trim(coalesce(new.bamboo_status, '')), ''); if new.source = 'bamboohr' then if tg_op = 'INSERT' then new.bamboo_synced_at := now(); elsif new.bamboo_employee_id is distinct from old.bamboo_employee_id or new.full_name is distinct from old.full_name or new.email is distinct from old.email or new.whatsapp_number is distinct from old.whatsapp_number then new.bamboo_synced_at := now(); end if; end if; return new; end; $$; -- --------------------------------------------------------------------------- -- No permitir obligaciones nuevas o movidas a fechas pasadas -- --------------------------------------------------------------------------- create or replace function public.tax_validate_future_obligation() returns trigger language plpgsql set search_path = '' as $$ declare local_today date := (now() at time zone 'America/Santo_Domingo')::date; begin if new.active = true and new.is_historical = false and new.due_date < local_today then if tg_op = 'INSERT' then raise exception 'No se puede crear una obligación en una fecha que ya pasó.' using errcode = '22007'; elsif new.due_date is distinct from old.due_date then raise exception 'No se puede mover una obligación a una fecha que ya pasó.' using errcode = '22007'; end if; end if; return new; end; $$; drop trigger if exists tax_validate_future_obligation_trigger on public.tax_obligations; create trigger tax_validate_future_obligation_trigger before insert or update on public.tax_obligations for each row execute function public.tax_validate_future_obligation(); -- --------------------------------------------------------------------------- -- Sincronización con Google Calendar -- Un evento de Google Calendar por obligación, con todos los destinatarios -- aplicables como invitados. -- --------------------------------------------------------------------------- create table if not exists public.tax_google_calendar_events ( obligation_id uuid primary key references public.tax_obligations(id) on delete cascade, google_event_id text, calendar_id text not null default 'primary', attendees jsonb not null default '[]'::jsonb, status text not null default 'active' check (status in ('active', 'deleted', 'failed')), last_action text, provider_response jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), synced_at timestamptz ); alter table public.tax_google_calendar_events enable row level security; revoke all on table public.tax_google_calendar_events from anon, authenticated; grant select, insert, update, delete on table public.tax_google_calendar_events to service_role; create or replace function public.tax_google_calendar_payload(p_obligation_id uuid) returns table ( obligation_id uuid, active boolean, due_date date, description text, category text, country_id uuid, country_name text, country_code text, attendees jsonb, google_event_id text, calendar_id text, sync_status text ) language sql stable security definer set search_path = '' as $$ select obligation.id, obligation.active, obligation.due_date, obligation.description, obligation.category, obligation.country_id, country.name, country.code, coalesce( ( select jsonb_agg(recipient.email order by recipient.email) from ( select distinct lower(contact.email) as email from public.tax_contacts contact join public.tax_countries contact_country on contact_country.id = contact.country_id where contact.active = true and contact.calendar_enabled = true and contact.email is not null and length(trim(contact.email)) > 3 and ( contact.area = 'regional' or ( contact_country.is_regional = true and contact.area = obligation.category ) or ( contact.country_id = obligation.country_id and contact.area = obligation.category ) ) ) recipient ), '[]'::jsonb ) as attendees, sync.google_event_id, coalesce(sync.calendar_id, 'primary') as calendar_id, sync.status from public.tax_obligations obligation join public.tax_countries country on country.id = obligation.country_id left join public.tax_google_calendar_events sync on sync.obligation_id = obligation.id where obligation.id = p_obligation_id; $$; revoke all on function public.tax_google_calendar_payload(uuid) from public, anon, authenticated; grant execute on function public.tax_google_calendar_payload(uuid) to service_role; create or replace function public.tax_record_google_calendar_sync( p_obligation_id uuid, p_google_event_id text, p_calendar_id text default 'primary', p_attendees jsonb default '[]'::jsonb, p_status text default 'active', p_last_action text default null, p_provider_response jsonb default null ) returns void language plpgsql security definer set search_path = '' as $$ begin insert into public.tax_google_calendar_events ( obligation_id, google_event_id, calendar_id, attendees, status, last_action, provider_response, synced_at, updated_at ) values ( p_obligation_id, nullif(trim(coalesce(p_google_event_id, '')), ''), coalesce(nullif(trim(coalesce(p_calendar_id, '')), ''), 'primary'), coalesce(p_attendees, '[]'::jsonb), p_status, p_last_action, p_provider_response, now(), now() ) on conflict (obligation_id) do update set google_event_id = coalesce(excluded.google_event_id, public.tax_google_calendar_events.google_event_id), calendar_id = excluded.calendar_id, attendees = excluded.attendees, status = excluded.status, last_action = excluded.last_action, provider_response = excluded.provider_response, synced_at = now(), updated_at = now(); end; $$; revoke all on function public.tax_record_google_calendar_sync(uuid, text, text, jsonb, text, text, jsonb) from public, anon, authenticated; grant execute on function public.tax_record_google_calendar_sync(uuid, text, text, jsonb, text, text, jsonb) to service_role; commit; -- Verificaciones opcionales: -- select * from public.tax_google_calendar_payload('UUID_DE_OBLIGACION'); -- select * from public.tax_google_calendar_events order by updated_at desc;