create or replace function public.get_my_role() returns text as $$ select role from public.profiles where id = auth.uid(); $$ language sql security definer stable set search_path = public; create or replace function public.is_external() returns boolean as $$ select get_my_role() = 'external'; $$ language sql security definer stable set search_path = public; create or replace function public.get_my_company_id() returns uuid as $$ select company_id from public.profiles where id = auth.uid(); $$ language sql security definer stable set search_path = public; create or replace function public.has_company_access(company uuid) returns boolean as $$ select exists ( select 1 from public.profiles p where p.id = auth.uid() and ( p.company_id = company or exists ( select 1 from public.company_members cm where cm.profile_id = auth.uid() and cm.company_id = company ) ) ); $$ language sql security definer stable set search_path = public; create or replace function public.guard_task_update() returns trigger as $$ declare caller_role text; begin select role into caller_role from public.profiles where id = auth.uid(); if caller_role = 'client' then new.project_id := old.project_id; new.invoiced := old.invoiced; if not ( new.status = 'not_started' and coalesce(new.current_version, 0) > coalesce(old.current_version, 0) and new.assigned_to is null and new.assigned_name is null ) then new.assigned_to := old.assigned_to; new.assigned_name := old.assigned_name; end if; elsif caller_role = 'external' then new.project_id := old.project_id; new.invoiced := old.invoiced; end if; return new; end; $$ language plpgsql security definer set search_path = public; create or replace function public.handle_new_user() returns trigger as $$ begin insert into public.profiles (id, name, email, role) values ( new.id, coalesce(new.raw_user_meta_data->>'name', ''), new.email, coalesce(new.raw_user_meta_data->>'role', 'client') ); return new; end; $$ language plpgsql security definer set search_path = public; create or replace function public.prevent_duplicate_active_subcontractor_po_task() returns trigger as $$ declare current_po_status text; duplicate_po_number text; begin if new.task_id is null then return new; end if; select status into current_po_status from public.subcontractor_payments where id = new.po_id; if current_po_status = 'cancelled' then return new; end if; select sp.po_number into duplicate_po_number from public.subcontractor_po_items item join public.subcontractor_payments sp on sp.id = item.po_id where item.task_id = new.task_id and item.id <> coalesce(new.id, '00000000-0000-0000-0000-000000000000'::uuid) and sp.status <> 'cancelled' limit 1; if duplicate_po_number is not null then raise exception 'Task is already attached to active subcontractor PO %', duplicate_po_number using errcode = '23505'; end if; return new; end; $$ language plpgsql security definer set search_path = public; create or replace function public.prevent_duplicate_on_subcontractor_po_reactivation() returns trigger as $$ declare duplicate_task_title text; begin if old.status = new.status or new.status = 'cancelled' then return new; end if; select t.title into duplicate_task_title from public.subcontractor_po_items current_item join public.subcontractor_po_items other_item on other_item.task_id = current_item.task_id and other_item.po_id <> current_item.po_id join public.subcontractor_payments other_po on other_po.id = other_item.po_id and other_po.status <> 'cancelled' left join public.tasks t on t.id = current_item.task_id where current_item.po_id = new.id and current_item.task_id is not null limit 1; if duplicate_task_title is not null then raise exception 'Cannot reactivate PO because task "%" is already attached to another active subcontractor PO', duplicate_task_title using errcode = '23505'; end if; return new; end; $$ language plpgsql security definer set search_path = public; create or replace function public.sync_subcontractor_project_member() returns trigger as $$ begin if new.profile_id is not null and new.project_id is not null then insert into public.project_members (project_id, profile_id) values (new.project_id, new.profile_id) on conflict (project_id, profile_id) do nothing; end if; return new; end; $$ language plpgsql security definer set search_path = public; create or replace function public.sync_project_member_on_task_assign() returns trigger as $$ begin if new.assigned_to is not null and new.project_id is not null then insert into public.project_members (project_id, profile_id) values (new.project_id, new.assigned_to) on conflict (project_id, profile_id) do nothing; end if; return new; end; $$ language plpgsql security definer set search_path = public; do $$ declare fn record; begin for fn in select n.nspname as schema_name, p.proname as function_name, pg_get_function_identity_arguments(p.oid) as identity_args from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.proname in ( 'notify_company_folder_sync', 'notify_profile_folder_sync', 'sync_project_status' ) loop execute format( 'drop function if exists %I.%I(%s) cascade;', fn.schema_name, fn.function_name, fn.identity_args ); end loop; end $$;