819 lines
29 KiB
PL/PgSQL
819 lines
29 KiB
PL/PgSQL
-- =============================================================================
|
||
-- AgenciaPsi — Functions — Financeiro
|
||
-- =============================================================================
|
||
-- auto_create_financial_record_from_session, create_financial_record_for_session,
|
||
-- create_therapist_payout, get_financial_report, get_financial_summary,
|
||
-- list_financial_records, mark_as_paid, mark_payout_as_paid,
|
||
-- seed_default_financial_categories, sync_overdue_financial_records,
|
||
-- trg_fn_financial_records_auto_overdue, set_insurance/services_updated_at
|
||
-- =============================================================================
|
||
|
||
CREATE FUNCTION public.auto_create_financial_record_from_session() RETURNS trigger
|
||
LANGUAGE plpgsql SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $$
|
||
DECLARE
|
||
v_price NUMERIC(10,2);
|
||
v_services_total NUMERIC(10,2);
|
||
v_already_billed BOOLEAN;
|
||
BEGIN
|
||
-- ── Guards de saída rápida ──────────────────────────────────────────────
|
||
|
||
-- Só processa quando o status muda PARA 'realizado'
|
||
IF NEW.status::TEXT <> 'realizado' THEN
|
||
RETURN NEW;
|
||
END IF;
|
||
|
||
-- Só processa quando houve mudança real de status
|
||
IF OLD.status IS NOT DISTINCT FROM NEW.status THEN
|
||
RETURN NEW;
|
||
END IF;
|
||
|
||
-- Só sessões (não bloqueios, feriados, etc.)
|
||
IF NEW.tipo::TEXT <> 'sessao' THEN
|
||
RETURN NEW;
|
||
END IF;
|
||
|
||
-- Paciente obrigatório para vincular a cobrança
|
||
IF NEW.patient_id IS NULL THEN
|
||
RETURN NEW;
|
||
END IF;
|
||
|
||
-- Sessões de pacote têm cobrança gerenciada por billing_contract
|
||
IF NEW.billing_contract_id IS NOT NULL THEN
|
||
RETURN NEW;
|
||
END IF;
|
||
|
||
-- Idempotência: já existe financial_record para este evento?
|
||
SELECT billed INTO v_already_billed
|
||
FROM public.agenda_eventos
|
||
WHERE id = NEW.id;
|
||
|
||
IF v_already_billed = TRUE THEN
|
||
-- Confirma no financial_records também (dupla verificação)
|
||
IF EXISTS (
|
||
SELECT 1 FROM public.financial_records
|
||
WHERE agenda_evento_id = NEW.id AND deleted_at IS NULL
|
||
) THEN
|
||
RETURN NEW;
|
||
END IF;
|
||
END IF;
|
||
|
||
-- ── Busca do preço ──────────────────────────────────────────────────────
|
||
|
||
v_price := NULL;
|
||
|
||
-- Prioridade 1: soma dos serviços da regra de recorrência
|
||
IF NEW.recurrence_id IS NOT NULL THEN
|
||
SELECT COALESCE(SUM(rrs.final_price), 0)
|
||
INTO v_services_total
|
||
FROM public.recurrence_rule_services rrs
|
||
WHERE rrs.rule_id = NEW.recurrence_id;
|
||
|
||
IF v_services_total > 0 THEN
|
||
v_price := v_services_total;
|
||
END IF;
|
||
|
||
-- Prioridade 2: price direto da regra (fallback se sem serviços)
|
||
IF v_price IS NULL OR v_price = 0 THEN
|
||
SELECT price INTO v_price
|
||
FROM public.recurrence_rules
|
||
WHERE id = NEW.recurrence_id;
|
||
END IF;
|
||
END IF;
|
||
|
||
-- Prioridade 3: price do próprio evento de agenda
|
||
IF v_price IS NULL OR v_price = 0 THEN
|
||
v_price := NEW.price;
|
||
END IF;
|
||
|
||
-- Sem preço → não criar registro (não é erro, apenas skip silencioso)
|
||
IF v_price IS NULL OR v_price <= 0 THEN
|
||
RETURN NEW;
|
||
END IF;
|
||
|
||
-- ── Criação do financial_record ─────────────────────────────────────────
|
||
|
||
INSERT INTO public.financial_records (
|
||
owner_id,
|
||
tenant_id,
|
||
patient_id,
|
||
agenda_evento_id,
|
||
type,
|
||
amount,
|
||
discount_amount,
|
||
final_amount,
|
||
clinic_fee_pct,
|
||
clinic_fee_amount,
|
||
status,
|
||
due_date
|
||
-- payment_method: NULL até o momento do pagamento (mark_as_paid preenche)
|
||
) VALUES (
|
||
NEW.owner_id,
|
||
NEW.tenant_id,
|
||
NEW.patient_id,
|
||
NEW.id,
|
||
'receita',
|
||
v_price,
|
||
0,
|
||
v_price,
|
||
0, -- clinic_fee_pct: sem campo de configuração global no schema atual.
|
||
0, -- clinic_fee_amount: calculado manualmente ou via update posterior.
|
||
'pending',
|
||
(NEW.inicio_em::DATE + 7) -- vencimento padrão: 7 dias após a sessão
|
||
);
|
||
|
||
-- ── Marca sessão como billed ────────────────────────────────────────────
|
||
-- UPDATE em billed (não em status) → não re-dispara este trigger
|
||
UPDATE public.agenda_eventos
|
||
SET billed = TRUE
|
||
WHERE id = NEW.id;
|
||
|
||
RETURN NEW;
|
||
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
-- Log silencioso: nunca bloquear a agenda por falha financeira
|
||
RAISE WARNING '[auto_create_financial_record_from_session] evento=% erro=%',
|
||
NEW.id, SQLERRM;
|
||
RETURN NEW;
|
||
END;
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.auto_create_financial_record_from_session() OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: FUNCTION auto_create_financial_record_from_session(); Type: COMMENT; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
COMMENT ON FUNCTION public.auto_create_financial_record_from_session() IS 'Trigger que cria automaticamente um financial_record (receita, pending) quando uma sessão de agenda é marcada como realizada. Prioridade de preço: recurrence_rule_services > recurrence_rules.price > agenda_eventos.price. Skip silencioso se sem preço, pacote ou registro já existente.';
|
||
|
||
|
||
--
|
||
-- Name: can_delete_patient(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.can_delete_patient(p_patient_id uuid) RETURNS boolean
|
||
CREATE FUNCTION public.create_financial_record_for_session(p_tenant_id uuid, p_owner_id uuid, p_patient_id uuid, p_agenda_evento_id uuid, p_amount numeric, p_due_date date) RETURNS SETOF public.financial_records
|
||
LANGUAGE plpgsql SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $$
|
||
DECLARE
|
||
v_existing public.financial_records%ROWTYPE;
|
||
v_new public.financial_records%ROWTYPE;
|
||
BEGIN
|
||
-- Idempotência: retorna o registro existente se já foi criado
|
||
SELECT * INTO v_existing
|
||
FROM public.financial_records
|
||
WHERE agenda_evento_id = p_agenda_evento_id
|
||
AND deleted_at IS NULL
|
||
LIMIT 1;
|
||
|
||
IF FOUND THEN
|
||
RETURN NEXT v_existing;
|
||
RETURN;
|
||
END IF;
|
||
|
||
-- Cria o novo registro
|
||
INSERT INTO public.financial_records (
|
||
tenant_id,
|
||
owner_id,
|
||
patient_id,
|
||
agenda_evento_id,
|
||
amount,
|
||
discount_amount,
|
||
final_amount,
|
||
status,
|
||
due_date
|
||
) VALUES (
|
||
p_tenant_id,
|
||
p_owner_id,
|
||
p_patient_id,
|
||
p_agenda_evento_id,
|
||
p_amount,
|
||
0,
|
||
p_amount,
|
||
'pending',
|
||
p_due_date
|
||
)
|
||
RETURNING * INTO v_new;
|
||
|
||
-- Marca o evento da agenda como billed = true
|
||
UPDATE public.agenda_eventos
|
||
SET billed = TRUE
|
||
WHERE id = p_agenda_evento_id;
|
||
|
||
RETURN NEXT v_new;
|
||
END;
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.create_financial_record_for_session(p_tenant_id uuid, p_owner_id uuid, p_patient_id uuid, p_agenda_evento_id uuid, p_amount numeric, p_due_date date) OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: create_patient_intake_request(text, text, text, text, text, boolean); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.create_patient_intake_request(p_token text, p_name text, p_email text DEFAULT NULL::text, p_phone text DEFAULT NULL::text, p_notes text DEFAULT NULL::text, p_consent boolean DEFAULT false) RETURNS uuid
|
||
CREATE FUNCTION public.create_therapist_payout(p_tenant_id uuid, p_therapist_id uuid, p_period_start date, p_period_end date) RETURNS public.therapist_payouts
|
||
LANGUAGE plpgsql SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $$
|
||
DECLARE
|
||
v_payout public.therapist_payouts%ROWTYPE;
|
||
v_total_sessions INTEGER;
|
||
v_gross NUMERIC(10,2);
|
||
v_clinic_fee NUMERIC(10,2);
|
||
v_net NUMERIC(10,2);
|
||
BEGIN
|
||
-- ── Verificação de permissão ────────────────────────────────────────────
|
||
-- Apenas o próprio terapeuta ou o tenant_admin pode criar o repasse
|
||
IF auth.uid() <> p_therapist_id AND NOT public.is_tenant_admin(p_tenant_id) THEN
|
||
RAISE EXCEPTION 'Sem permissão para criar repasse para este terapeuta.';
|
||
END IF;
|
||
|
||
-- ── Verifica se já existe repasse para o mesmo período ─────────────────
|
||
IF EXISTS (
|
||
SELECT 1 FROM public.therapist_payouts
|
||
WHERE owner_id = p_therapist_id
|
||
AND tenant_id = p_tenant_id
|
||
AND period_start = p_period_start
|
||
AND period_end = p_period_end
|
||
AND status <> 'cancelled'
|
||
) THEN
|
||
RAISE EXCEPTION
|
||
'Já existe um repasse ativo para o período % a % deste terapeuta.',
|
||
p_period_start, p_period_end;
|
||
END IF;
|
||
|
||
-- ── Agrega os financial_records elegíveis ──────────────────────────────
|
||
-- Elegíveis: paid, receita, owner=terapeuta, tenant correto, paid_at no período,
|
||
-- não soft-deleted, ainda não vinculados a nenhum payout.
|
||
SELECT
|
||
COUNT(*) AS total_sessions,
|
||
COALESCE(SUM(amount), 0) AS gross_amount,
|
||
COALESCE(SUM(clinic_fee_amount), 0) AS clinic_fee_total,
|
||
COALESCE(SUM(net_amount), 0) AS net_amount
|
||
INTO
|
||
v_total_sessions, v_gross, v_clinic_fee, v_net
|
||
FROM public.financial_records fr
|
||
WHERE fr.owner_id = p_therapist_id
|
||
AND fr.tenant_id = p_tenant_id
|
||
AND fr.type = 'receita'
|
||
AND fr.status = 'paid'
|
||
AND fr.deleted_at IS NULL
|
||
AND fr.paid_at::DATE BETWEEN p_period_start AND p_period_end
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM public.therapist_payout_records tpr
|
||
WHERE tpr.financial_record_id = fr.id
|
||
);
|
||
|
||
-- Sem registros elegíveis → não criar payout vazio
|
||
IF v_total_sessions = 0 THEN
|
||
RAISE EXCEPTION
|
||
'Nenhum registro financeiro elegível encontrado para o período % a %.',
|
||
p_period_start, p_period_end;
|
||
END IF;
|
||
|
||
-- ── Cria o repasse ─────────────────────────────────────────────────────
|
||
INSERT INTO public.therapist_payouts (
|
||
owner_id,
|
||
tenant_id,
|
||
period_start,
|
||
period_end,
|
||
total_sessions,
|
||
gross_amount,
|
||
clinic_fee_total,
|
||
net_amount,
|
||
status
|
||
) VALUES (
|
||
p_therapist_id,
|
||
p_tenant_id,
|
||
p_period_start,
|
||
p_period_end,
|
||
v_total_sessions,
|
||
v_gross,
|
||
v_clinic_fee,
|
||
v_net,
|
||
'pending'
|
||
)
|
||
RETURNING * INTO v_payout;
|
||
|
||
-- ── Vincula os financial_records ao repasse ────────────────────────────
|
||
INSERT INTO public.therapist_payout_records (payout_id, financial_record_id)
|
||
SELECT v_payout.id, fr.id
|
||
FROM public.financial_records fr
|
||
WHERE fr.owner_id = p_therapist_id
|
||
AND fr.tenant_id = p_tenant_id
|
||
AND fr.type = 'receita'
|
||
AND fr.status = 'paid'
|
||
AND fr.deleted_at IS NULL
|
||
AND fr.paid_at::DATE BETWEEN p_period_start AND p_period_end
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM public.therapist_payout_records tpr
|
||
WHERE tpr.financial_record_id = fr.id
|
||
);
|
||
|
||
RETURN v_payout;
|
||
END;
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.create_therapist_payout(p_tenant_id uuid, p_therapist_id uuid, p_period_start date, p_period_end date) OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: FUNCTION create_therapist_payout(p_tenant_id uuid, p_therapist_id uuid, p_period_start date, p_period_end date); Type: COMMENT; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
COMMENT ON FUNCTION public.create_therapist_payout(p_tenant_id uuid, p_therapist_id uuid, p_period_start date, p_period_end date) IS 'Cria um repasse para o terapeuta com todos os financial_records paid+receita do período que ainda não estejam vinculados a outro repasse. Lança exceção se não houver registros elegíveis ou se já houver repasse ativo no período.';
|
||
|
||
|
||
--
|
||
-- Name: current_member_id(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.current_member_id(p_tenant_id uuid) RETURNS uuid
|
||
CREATE FUNCTION public.get_financial_report(p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text DEFAULT 'month'::text) RETURNS TABLE(group_key text, group_label text, total_receitas numeric, total_despesas numeric, saldo numeric, total_pendente numeric, total_overdue numeric, count_records bigint)
|
||
LANGUAGE sql STABLE SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $$
|
||
|
||
-- ── Valida p_group_by antes de executar ──────────────────────────────────
|
||
-- (lança erro se valor inválido; plpgsql seria necessário para isso em SQL puro,
|
||
-- então usamos um CTE de validação com CASE WHEN para retornar vazio em vez de erro)
|
||
|
||
WITH base AS (
|
||
SELECT
|
||
fr.type,
|
||
fr.amount,
|
||
fr.final_amount,
|
||
fr.status,
|
||
fr.deleted_at,
|
||
-- Chave de agrupamento calculada conforme p_group_by
|
||
CASE p_group_by
|
||
WHEN 'month' THEN TO_CHAR(
|
||
COALESCE(fr.paid_at::DATE, fr.due_date, fr.created_at::DATE),
|
||
'YYYY-MM'
|
||
)
|
||
WHEN 'week' THEN TO_CHAR(
|
||
COALESCE(fr.paid_at::DATE, fr.due_date, fr.created_at::DATE),
|
||
'IYYY-"W"IW'
|
||
)
|
||
WHEN 'category' THEN COALESCE(fr.category_id::TEXT, fr.category, 'sem_categoria')
|
||
WHEN 'patient' THEN COALESCE(fr.patient_id::TEXT, 'sem_paciente')
|
||
ELSE NULL -- group_by inválido → group_key NULL → retorno vazio
|
||
END AS gkey,
|
||
-- Label legível (enriquecido via JOIN abaixo quando possível)
|
||
CASE p_group_by
|
||
WHEN 'month' THEN TO_CHAR(
|
||
COALESCE(fr.paid_at::DATE, fr.due_date, fr.created_at::DATE),
|
||
'YYYY-MM'
|
||
)
|
||
WHEN 'week' THEN TO_CHAR(
|
||
COALESCE(fr.paid_at::DATE, fr.due_date, fr.created_at::DATE),
|
||
'IYYY-"W"IW'
|
||
)
|
||
WHEN 'category' THEN COALESCE(fc.name, fr.category, 'Sem categoria')
|
||
WHEN 'patient' THEN COALESCE(p.nome_completo, fr.patient_id::TEXT, 'Sem paciente')
|
||
ELSE NULL
|
||
END AS glabel
|
||
FROM public.financial_records fr
|
||
LEFT JOIN public.financial_categories fc
|
||
ON fc.id = fr.category_id
|
||
LEFT JOIN public.patients p
|
||
ON p.id = fr.patient_id
|
||
WHERE fr.owner_id = p_owner_id
|
||
AND fr.deleted_at IS NULL
|
||
AND COALESCE(fr.paid_at::DATE, fr.due_date, fr.created_at::DATE)
|
||
BETWEEN p_start_date AND p_end_date
|
||
)
|
||
|
||
SELECT
|
||
gkey AS group_key,
|
||
glabel AS group_label,
|
||
|
||
COALESCE(SUM(final_amount) FILTER (WHERE type = 'receita' AND status = 'paid'), 0)
|
||
AS total_receitas,
|
||
|
||
COALESCE(SUM(final_amount) FILTER (WHERE type = 'despesa' AND status = 'paid'), 0)
|
||
AS total_despesas,
|
||
|
||
COALESCE(SUM(final_amount) FILTER (WHERE type = 'receita' AND status = 'paid'), 0)
|
||
- COALESCE(SUM(final_amount) FILTER (WHERE type = 'despesa' AND status = 'paid'), 0)
|
||
AS saldo,
|
||
|
||
COALESCE(SUM(final_amount) FILTER (WHERE status = 'pending'), 0) AS total_pendente,
|
||
|
||
COALESCE(SUM(final_amount) FILTER (WHERE status = 'overdue'), 0) AS total_overdue,
|
||
|
||
COUNT(*) AS count_records
|
||
|
||
FROM base
|
||
WHERE gkey IS NOT NULL -- descarta p_group_by inválido
|
||
GROUP BY gkey, glabel
|
||
ORDER BY gkey ASC;
|
||
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.get_financial_report(p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text) OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: FUNCTION get_financial_report(p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text); Type: COMMENT; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
COMMENT ON FUNCTION public.get_financial_report(p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text) IS 'Relatório financeiro agrupado por mês, semana ISO, categoria ou paciente. p_group_by aceita: ''month'' | ''week'' | ''category'' | ''patient''. Totais de receita/despesa consideram apenas registros com status=paid. total_pendente e total_overdue incluem todos os tipos (receita + despesa).';
|
||
|
||
|
||
--
|
||
-- Name: get_financial_summary(uuid, integer, integer); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.get_financial_summary(p_owner_id uuid, p_year integer, p_month integer) RETURNS TABLE(total_receitas numeric, total_despesas numeric, total_pendente numeric, saldo_liquido numeric, total_repasse numeric, count_receitas bigint, count_despesas bigint)
|
||
LANGUAGE sql STABLE SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $$
|
||
SELECT
|
||
-- Receitas pagas no período
|
||
COALESCE(SUM(amount) FILTER (
|
||
WHERE type = 'receita' AND status = 'paid'
|
||
), 0) AS total_receitas,
|
||
|
||
-- Despesas pagas no período
|
||
COALESCE(SUM(amount) FILTER (
|
||
WHERE type = 'despesa' AND status = 'paid'
|
||
), 0) AS total_despesas,
|
||
|
||
-- Tudo pendente ou vencido (receitas + despesas)
|
||
COALESCE(SUM(amount) FILTER (
|
||
WHERE status IN ('pending', 'overdue')
|
||
), 0) AS total_pendente,
|
||
|
||
-- Saldo líquido (receitas pagas − despesas pagas)
|
||
COALESCE(SUM(amount) FILTER (
|
||
WHERE type = 'receita' AND status = 'paid'
|
||
), 0)
|
||
- COALESCE(SUM(amount) FILTER (
|
||
WHERE type = 'despesa' AND status = 'paid'
|
||
), 0) AS saldo_liquido,
|
||
|
||
-- Total repassado à clínica (apenas receitas pagas)
|
||
COALESCE(SUM(clinic_fee_amount) FILTER (
|
||
WHERE type = 'receita' AND status = 'paid'
|
||
), 0) AS total_repasse,
|
||
|
||
-- Contadores (excluindo soft-deleted)
|
||
COUNT(*) FILTER (WHERE type = 'receita' AND deleted_at IS NULL) AS count_receitas,
|
||
COUNT(*) FILTER (WHERE type = 'despesa' AND deleted_at IS NULL) AS count_despesas
|
||
|
||
FROM public.financial_records
|
||
WHERE owner_id = p_owner_id
|
||
AND deleted_at IS NULL
|
||
AND EXTRACT(YEAR FROM COALESCE(paid_at::DATE, due_date, created_at::DATE)) = p_year
|
||
AND EXTRACT(MONTH FROM COALESCE(paid_at::DATE, due_date, created_at::DATE)) = p_month;
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.get_financial_summary(p_owner_id uuid, p_year integer, p_month integer) OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: get_my_email(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.get_my_email() RETURNS text
|
||
CREATE FUNCTION public.list_financial_records(p_owner_id uuid, p_year integer DEFAULT NULL::integer, p_month integer DEFAULT NULL::integer, p_type text DEFAULT NULL::text, p_status text DEFAULT NULL::text, p_patient_id uuid DEFAULT NULL::uuid, p_limit integer DEFAULT 50, p_offset integer DEFAULT 0) RETURNS SETOF public.financial_records
|
||
LANGUAGE sql STABLE SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $$
|
||
SELECT *
|
||
FROM public.financial_records
|
||
WHERE owner_id = p_owner_id
|
||
AND deleted_at IS NULL
|
||
AND (p_type IS NULL OR type::TEXT = p_type)
|
||
AND (p_status IS NULL OR status = p_status)
|
||
AND (p_patient_id IS NULL OR patient_id = p_patient_id)
|
||
AND (p_year IS NULL OR EXTRACT(YEAR FROM COALESCE(paid_at::DATE, due_date, created_at::DATE)) = p_year)
|
||
AND (p_month IS NULL OR EXTRACT(MONTH FROM COALESCE(paid_at::DATE, due_date, created_at::DATE)) = p_month)
|
||
ORDER BY COALESCE(paid_at, due_date::TIMESTAMPTZ, created_at) DESC
|
||
LIMIT p_limit
|
||
OFFSET p_offset;
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.list_financial_records(p_owner_id uuid, p_year integer, p_month integer, p_type text, p_status text, p_patient_id uuid, p_limit integer, p_offset integer) OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: mark_as_paid(uuid, text); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.mark_as_paid(p_financial_record_id uuid, p_payment_method text) RETURNS SETOF public.financial_records
|
||
LANGUAGE plpgsql SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $$
|
||
DECLARE
|
||
v_record public.financial_records%ROWTYPE;
|
||
BEGIN
|
||
-- Garante que o registro pertence ao usuário autenticado (RLS não aplica em SECURITY DEFINER)
|
||
SELECT * INTO v_record
|
||
FROM public.financial_records
|
||
WHERE id = p_financial_record_id
|
||
AND owner_id = auth.uid()
|
||
AND deleted_at IS NULL;
|
||
|
||
IF NOT FOUND THEN
|
||
RAISE EXCEPTION 'Registro financeiro não encontrado ou sem permissão.';
|
||
END IF;
|
||
|
||
IF v_record.status NOT IN ('pending', 'overdue') THEN
|
||
RAISE EXCEPTION 'Apenas cobranças pendentes ou vencidas podem ser marcadas como pagas.';
|
||
END IF;
|
||
|
||
UPDATE public.financial_records
|
||
SET status = 'paid',
|
||
paid_at = NOW(),
|
||
payment_method = p_payment_method,
|
||
updated_at = NOW()
|
||
WHERE id = p_financial_record_id
|
||
RETURNING * INTO v_record;
|
||
|
||
RETURN NEXT v_record;
|
||
END;
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.mark_as_paid(p_financial_record_id uuid, p_payment_method text) OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: mark_payout_as_paid(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.mark_payout_as_paid(p_payout_id uuid) RETURNS public.therapist_payouts
|
||
CREATE FUNCTION public.mark_payout_as_paid(p_payout_id uuid) RETURNS public.therapist_payouts
|
||
LANGUAGE plpgsql SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $$
|
||
DECLARE
|
||
v_payout public.therapist_payouts%ROWTYPE;
|
||
BEGIN
|
||
-- Busca o payout
|
||
SELECT * INTO v_payout
|
||
FROM public.therapist_payouts
|
||
WHERE id = p_payout_id;
|
||
|
||
IF NOT FOUND THEN
|
||
RAISE EXCEPTION 'Repasse não encontrado: %', p_payout_id;
|
||
END IF;
|
||
|
||
-- Verifica permissão: apenas tenant_admin do tenant do repasse
|
||
IF NOT public.is_tenant_admin(v_payout.tenant_id) THEN
|
||
RAISE EXCEPTION 'Apenas o administrador da clínica pode marcar repasses como pagos.';
|
||
END IF;
|
||
|
||
-- Verifica status
|
||
IF v_payout.status <> 'pending' THEN
|
||
RAISE EXCEPTION
|
||
'Repasse já está com status ''%''. Apenas repasses pendentes podem ser pagos.',
|
||
v_payout.status;
|
||
END IF;
|
||
|
||
-- Atualiza
|
||
UPDATE public.therapist_payouts
|
||
SET
|
||
status = 'paid',
|
||
paid_at = NOW(),
|
||
updated_at = NOW()
|
||
WHERE id = p_payout_id
|
||
RETURNING * INTO v_payout;
|
||
|
||
RETURN v_payout;
|
||
END;
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.mark_payout_as_paid(p_payout_id uuid) OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: FUNCTION mark_payout_as_paid(p_payout_id uuid); Type: COMMENT; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
COMMENT ON FUNCTION public.mark_payout_as_paid(p_payout_id uuid) IS 'Marca um repasse de terapeuta como pago. Apenas o tenant_admin pode chamar. Apenas repasses com status=pending podem ser finalizados.';
|
||
|
||
|
||
--
|
||
-- Name: my_tenants(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.my_tenants() RETURNS TABLE(tenant_id uuid, role text, status text, kind text)
|
||
CREATE FUNCTION public.seed_default_financial_categories(p_user_id uuid) RETURNS void
|
||
LANGUAGE plpgsql SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $$
|
||
BEGIN
|
||
INSERT INTO public.financial_categories (user_id, name, type, color, icon, sort_order)
|
||
VALUES
|
||
(p_user_id, 'Sessão', 'receita', '#22c55e', 'pi pi-heart', 1),
|
||
(p_user_id, 'Supervisão', 'receita', '#6366f1', 'pi pi-users', 2),
|
||
(p_user_id, 'Convênio', 'receita', '#3b82f6', 'pi pi-building', 3),
|
||
(p_user_id, 'Grupo terapêutico', 'receita', '#f59e0b', 'pi pi-sitemap', 4),
|
||
(p_user_id, 'Outro (receita)', 'receita', '#8b5cf6', 'pi pi-plus-circle', 5),
|
||
(p_user_id, 'Aluguel sala', 'despesa', '#ef4444', 'pi pi-home', 1),
|
||
(p_user_id, 'Plataforma/SaaS', 'despesa', '#f97316', 'pi pi-desktop', 2),
|
||
(p_user_id, 'Repasse clínica', 'despesa', '#64748b', 'pi pi-arrow-right-arrow-left', 3),
|
||
(p_user_id, 'Supervisão (custo)', 'despesa', '#6366f1', 'pi pi-users', 4),
|
||
(p_user_id, 'Outro (despesa)', 'despesa', '#94a3b8', 'pi pi-minus-circle', 5)
|
||
ON CONFLICT DO NOTHING;
|
||
END;
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.seed_default_financial_categories(p_user_id uuid) OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: seed_determined_commitments(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.seed_determined_commitments(p_tenant_id uuid) RETURNS void
|
||
CREATE FUNCTION public.set_insurance_plans_updated_at() RETURNS trigger
|
||
LANGUAGE plpgsql
|
||
AS $$
|
||
BEGIN NEW.updated_at = now(); RETURN NEW; END; $$;
|
||
|
||
|
||
ALTER FUNCTION public.set_insurance_plans_updated_at() OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: set_owner_id(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.set_owner_id() RETURNS trigger
|
||
LANGUAGE plpgsql
|
||
AS $$
|
||
begin
|
||
if new.owner_id is null then
|
||
new.owner_id := auth.uid();
|
||
end if;
|
||
return new;
|
||
end;
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.set_owner_id() OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: set_services_updated_at(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.set_services_updated_at() RETURNS trigger
|
||
LANGUAGE plpgsql
|
||
AS $$
|
||
BEGIN
|
||
NEW.updated_at = now();
|
||
RETURN NEW;
|
||
END;
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.set_services_updated_at() OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: set_tenant_feature_exception(uuid, text, boolean, text); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.set_tenant_feature_exception(p_tenant_id uuid, p_feature_key text, p_enabled boolean, p_reason text DEFAULT NULL::text) RETURNS void
|
||
CREATE FUNCTION public.set_updated_at() RETURNS trigger
|
||
LANGUAGE plpgsql
|
||
AS $$
|
||
BEGIN
|
||
NEW.updated_at = now();
|
||
RETURN NEW;
|
||
END;
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.set_updated_at() OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: set_updated_at_recurrence(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.set_updated_at_recurrence() RETURNS trigger
|
||
CREATE FUNCTION public.sync_overdue_financial_records() RETURNS integer
|
||
LANGUAGE plpgsql SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $$
|
||
DECLARE
|
||
v_count integer;
|
||
BEGIN
|
||
UPDATE public.financial_records
|
||
SET
|
||
status = 'overdue',
|
||
updated_at = NOW()
|
||
WHERE status = 'pending'
|
||
AND due_date IS NOT NULL
|
||
AND due_date < CURRENT_DATE
|
||
AND deleted_at IS NULL;
|
||
|
||
GET DIAGNOSTICS v_count = ROW_COUNT;
|
||
RETURN v_count;
|
||
END;
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.sync_overdue_financial_records() OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: FUNCTION sync_overdue_financial_records(); Type: COMMENT; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
COMMENT ON FUNCTION public.sync_overdue_financial_records() IS 'Marca como overdue todos os financial_records pendentes com due_date vencido. Pode ser chamada manualmente, via pg_cron ou via Supabase Edge Function agendada.';
|
||
|
||
|
||
--
|
||
-- Name: tenant_accept_invite(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.tenant_accept_invite(p_token uuid) RETURNS jsonb
|
||
CREATE FUNCTION public.trg_fn_financial_records_auto_overdue() RETURNS trigger
|
||
LANGUAGE plpgsql SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $$
|
||
BEGIN
|
||
IF NEW.status = 'pending'
|
||
AND NEW.due_date IS NOT NULL
|
||
AND NEW.due_date < CURRENT_DATE
|
||
THEN
|
||
NEW.status := 'overdue';
|
||
END IF;
|
||
RETURN NEW;
|
||
END;
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.trg_fn_financial_records_auto_overdue() OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: unstick_notification_queue(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.unstick_notification_queue() RETURNS integer
|
||
LANGUAGE plpgsql SECURITY DEFINER
|
||
AS $$
|
||
DECLARE
|
||
v_unstuck integer;
|
||
BEGIN
|
||
UPDATE public.notification_queue
|
||
SET status = 'pendente',
|
||
attempts = attempts + 1,
|
||
last_error = 'Timeout: preso em processando por >10min',
|
||
next_retry_at = now() + interval '2 minutes'
|
||
WHERE status = 'processando'
|
||
AND updated_at < now() - interval '10 minutes';
|
||
|
||
GET DIAGNOSTICS v_unstuck = ROW_COUNT;
|
||
RETURN v_unstuck;
|
||
END;
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.unstick_notification_queue() OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: update_payment_settings_updated_at(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.update_payment_settings_updated_at() RETURNS trigger
|
||
CREATE FUNCTION public.update_payment_settings_updated_at() RETURNS trigger
|
||
LANGUAGE plpgsql
|
||
AS $$
|
||
BEGIN
|
||
NEW.updated_at = now();
|
||
RETURN NEW;
|
||
END;
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.update_payment_settings_updated_at() OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: update_professional_pricing_updated_at(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.update_professional_pricing_updated_at() RETURNS trigger
|
||
LANGUAGE plpgsql
|
||
AS $$
|
||
BEGIN
|
||
NEW.updated_at = now();
|
||
RETURN NEW;
|
||
END;
|
||
$$;
|
||
|
||
|
||
ALTER FUNCTION public.update_professional_pricing_updated_at() OWNER TO supabase_admin;
|
||
|
||
--
|
||
-- Name: user_has_feature(uuid, text); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||
--
|
||
|
||
CREATE FUNCTION public.user_has_feature(_user_id uuid, _feature text) RETURNS boolean
|