begin; -- A subject deletion cascades through reports/jobs before a worker can observe -- the missing subject. Release the reservation in that same transaction. create or replace function public.release_deleted_report_reservation() returns trigger language plpgsql security definer set search_path = '' as $$ declare released record; begin -- A ready document has already been delivered, even if the worker is between -- its durable ready write and billing completion. Do not refund that window. if old.status = 'ready' then return old; end if; -- Account removal may already have cascaded away the wallet itself. if not exists (select 1 from public.profiles where id = old.user_id) then return old; end if; select * into released from public.release_usage(old.user_id, old.request_id::text, 'report_deleted'); -- Completed usage remains charged. Any other failure must roll back deletion -- rather than orphan a paid reservation with no durable job left to refund it. if not released.success and released.error_code is distinct from 'request_completed' then raise exception 'report_reservation_release_failed'; end if; return old; end; $$; revoke all on function public.release_deleted_report_reservation() from public, anon, authenticated; create trigger personal_reports_release_before_delete before delete on public.personal_reports for each row execute function public.release_deleted_report_reservation(); -- Match the worker's job -> report lock order before the cascading delete. -- Definer is needed only for job locks; the caller identity/ownership check is -- explicit and no arbitrary refund operation is exposed to authenticated. create or replace function public.delete_chart_subject(p_id uuid) returns void language plpgsql security definer set search_path = '' as $$ declare owner uuid := (select auth.uid()); begin if owner is null then raise exception 'chart_subject_unauthenticated'; end if; perform 1 from public.chart_profiles where id = p_id and user_id = owner and role = 'other' for update; if not found then raise exception 'chart_subject_not_found'; end if; perform 1 from public.personal_report_jobs j join public.personal_reports r on r.user_id = j.user_id and r.request_id = j.request_id where r.user_id = owner and r.chart_profile_id = p_id order by j.id for update of j; perform 1 from public.personal_reports where user_id = owner and chart_profile_id = p_id order by id for update; delete from public.chat_sessions where user_id = owner and chart_profile_id = p_id::text; delete from public.personal_reports where user_id = owner and chart_profile_id = p_id; delete from public.chart_profiles where id = p_id and user_id = owner and role = 'other'; end; $$; revoke all on function public.delete_chart_subject(uuid) from public, anon; grant execute on function public.delete_chart_subject(uuid) to authenticated; commit;