-- Views -- Gerado automaticamente em 2026-04-21T23:16:34.958Z -- Total: 29 CREATE VIEW public.audit_log_unified WITH (security_invoker='true') AS SELECT ('audit:'::text || (al.id)::text) AS uid, al.tenant_id, al.user_id, al.entity_type, al.entity_id, al.action, CASE al.action WHEN 'insert'::text THEN ('Criou '::text || al.entity_type) WHEN 'update'::text THEN (('Alterou '::text || al.entity_type) || COALESCE(((' ('::text || array_to_string(al.changed_fields, ', '::text)) || ')'::text), ''::text)) WHEN 'delete'::text THEN ('Excluiu '::text || al.entity_type) ELSE NULL::text END AS description, al.created_at AS occurred_at, 'audit_logs'::text AS source, jsonb_build_object('old_values', al.old_values, 'new_values', al.new_values, 'changed_fields', al.changed_fields) AS details FROM public.audit_logs al UNION ALL SELECT ('doc_access:'::text || (dal.id)::text) AS uid, dal.tenant_id, dal.user_id, 'document'::text AS entity_type, (dal.documento_id)::text AS entity_id, dal.acao AS action, CASE dal.acao WHEN 'visualizou'::text THEN 'Visualizou documento'::text WHEN 'baixou'::text THEN 'Baixou documento'::text WHEN 'imprimiu'::text THEN 'Imprimiu documento'::text WHEN 'compartilhou'::text THEN 'Compartilhou documento'::text WHEN 'assinou'::text THEN 'Assinou documento'::text ELSE dal.acao END AS description, dal.acessado_em AS occurred_at, 'document_access_logs'::text AS source, jsonb_build_object('ip', (dal.ip)::text, 'user_agent', dal.user_agent) AS details FROM public.document_access_logs dal UNION ALL SELECT ('psh:'::text || (psh.id)::text) AS uid, psh.tenant_id, psh.alterado_por AS user_id, 'patient_status'::text AS entity_type, (psh.patient_id)::text AS entity_id, 'status_change'::text AS action, (((('Status do paciente: '::text || COALESCE(psh.status_anterior, '—'::text)) || ' → '::text) || psh.status_novo) || COALESCE(((' ('::text || psh.motivo) || ')'::text), ''::text)) AS description, psh.alterado_em AS occurred_at, 'patient_status_history'::text AS source, jsonb_build_object('status_anterior', psh.status_anterior, 'status_novo', psh.status_novo, 'motivo', psh.motivo, 'encaminhado_para', psh.encaminhado_para, 'data_saida', psh.data_saida) AS details FROM public.patient_status_history psh UNION ALL SELECT ('notif:'::text || (nl.id)::text) AS uid, nl.tenant_id, nl.owner_id AS user_id, 'notification'::text AS entity_type, (nl.patient_id)::text AS entity_id, nl.status AS action, (((('Notificação '::text || nl.channel) || ' '::text) || nl.status) || COALESCE((' para '::text || nl.recipient_address), ''::text)) AS description, nl.created_at AS occurred_at, 'notification_logs'::text AS source, jsonb_build_object('channel', nl.channel, 'template_key', nl.template_key, 'status', nl.status, 'provider', nl.provider, 'failure_reason', nl.failure_reason) AS details FROM public.notification_logs nl UNION ALL SELECT ('addon:'::text || (at.id)::text) AS uid, at.tenant_id, at.admin_user_id AS user_id, 'addon_transaction'::text AS entity_type, (at.id)::text AS entity_id, at.type AS action, CASE at.type WHEN 'purchase'::text THEN ((('Compra de '::text || at.amount) || ' créditos de '::text) || at.addon_type) WHEN 'consumption'::text THEN ((('Consumo de '::text || abs(at.amount)) || ' crédito(s) '::text) || at.addon_type) WHEN 'adjustment'::text THEN ('Ajuste de créditos '::text || at.addon_type) WHEN 'refund'::text THEN ((('Reembolso de '::text || abs(at.amount)) || ' créditos '::text) || at.addon_type) ELSE ((at.type || ' '::text) || at.addon_type) END AS description, at.created_at AS occurred_at, 'addon_transactions'::text AS source, jsonb_build_object('addon_type', at.addon_type, 'amount', at.amount, 'balance_after', at.balance_after, 'price_cents', at.price_cents, 'payment_reference', at.payment_reference) AS details FROM public.addon_transactions at; CREATE VIEW public.conversation_threads WITH (security_invoker='true') AS WITH base AS ( SELECT cm.id, cm.tenant_id, cm.patient_id, cm.channel, cm.body, cm.direction, cm.kanban_status, cm.read_at, cm.created_at, CASE WHEN (cm.direction = 'inbound'::text) THEN cm.from_number ELSE cm.to_number END AS contact_number, COALESCE((cm.patient_id)::text, ('anon:'::text || COALESCE( CASE WHEN (cm.direction = 'inbound'::text) THEN cm.from_number ELSE cm.to_number END, 'unknown'::text))) AS thread_key FROM public.conversation_messages cm ), latest AS ( SELECT DISTINCT ON (base.tenant_id, base.thread_key) base.tenant_id, base.thread_key, base.patient_id, base.channel, base.contact_number, base.body AS last_message_body, base.direction AS last_message_direction, base.kanban_status, base.created_at AS last_message_at FROM base ORDER BY base.tenant_id, base.thread_key, base.created_at DESC ), counts AS ( SELECT base.tenant_id, base.thread_key, count(*) AS message_count, count(*) FILTER (WHERE ((base.direction = 'inbound'::text) AND (base.read_at IS NULL))) AS unread_count FROM base GROUP BY base.tenant_id, base.thread_key ) SELECT l.tenant_id, l.thread_key, l.patient_id, p.nome_completo AS patient_name, l.contact_number, l.channel, c.message_count, c.unread_count, l.last_message_at, l.last_message_body, l.last_message_direction, l.kanban_status FROM ((latest l JOIN counts c ON (((c.tenant_id = l.tenant_id) AND (c.thread_key = l.thread_key)))) LEFT JOIN public.patients p ON ((p.id = l.patient_id))); CREATE VIEW public.current_tenant_id AS SELECT current_setting('request.jwt.claim.tenant_id'::text, true) AS current_setting; CREATE VIEW public.owner_feature_entitlements AS WITH base AS ( SELECT s.user_id AS owner_id, f.key AS feature_key, pf.limits, 'plan'::text AS source FROM ((public.subscriptions s JOIN public.plan_features pf ON (((pf.plan_id = s.plan_id) AND (pf.enabled = true)))) JOIN public.features f ON ((f.id = pf.feature_id))) WHERE ((s.status = 'active'::text) AND (s.user_id IS NOT NULL)) UNION ALL SELECT tm.owner_id, f.key AS feature_key, mf.limits, 'module'::text AS source FROM (((public.tenant_modules tm JOIN public.modules m ON (((m.id = tm.module_id) AND (m.is_active = true)))) JOIN public.module_features mf ON (((mf.module_id = m.id) AND (mf.enabled = true)))) JOIN public.features f ON ((f.id = mf.feature_id))) WHERE ((tm.status = 'active'::text) AND (tm.owner_id IS NOT NULL)) ) SELECT owner_id, feature_key, array_agg(DISTINCT source) AS sources, jsonb_agg(limits) FILTER (WHERE (limits IS NOT NULL)) AS limits_list FROM base GROUP BY owner_id, feature_key; CREATE VIEW public.subscription_intents AS SELECT t.id, t.user_id, t.created_by_user_id, t.email, t.plan_id, t.plan_key, t."interval", t.amount_cents, t.currency, t.status, t.source, t.notes, t.created_at, t.paid_at, t.tenant_id, t.subscription_id, 'clinic'::text AS plan_target FROM public.subscription_intents_tenant t UNION ALL SELECT p.id, p.user_id, p.created_by_user_id, p.email, p.plan_id, p.plan_key, p."interval", p.amount_cents, p.currency, p.status, p.source, p.notes, p.created_at, p.paid_at, NULL::uuid AS tenant_id, p.subscription_id, 'therapist'::text AS plan_target FROM public.subscription_intents_personal p; CREATE VIEW public.v_auth_users_public AS SELECT id AS user_id, email, created_at, last_sign_in_at FROM auth.users u; CREATE VIEW public.v_cashflow_projection WITH (security_invoker='on') AS SELECT gs.mes, to_char(gs.mes, 'YYYY-MM'::text) AS mes_label, COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'receita'::public.financial_record_type) AND (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])))), (0)::numeric) AS receitas_projetadas, COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'despesa'::public.financial_record_type) AND (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])))), (0)::numeric) AS despesas_projetadas, COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'receita'::public.financial_record_type) AND (fr.status = 'pending'::text))), (0)::numeric) AS receitas_pendentes, COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'receita'::public.financial_record_type) AND (fr.status = 'overdue'::text))), (0)::numeric) AS receitas_vencidas, COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'despesa'::public.financial_record_type) AND (fr.status = 'pending'::text))), (0)::numeric) AS despesas_pendentes, COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'despesa'::public.financial_record_type) AND (fr.status = 'overdue'::text))), (0)::numeric) AS despesas_vencidas, (COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'receita'::public.financial_record_type) AND (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])))), (0)::numeric) - COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'despesa'::public.financial_record_type) AND (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])))), (0)::numeric)) AS saldo_projetado, count(fr.id) FILTER (WHERE (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text]))) AS count_registros FROM (generate_series(((date_trunc('month'::text, (CURRENT_DATE)::timestamp with time zone))::date)::timestamp with time zone, (((date_trunc('month'::text, (CURRENT_DATE)::timestamp with time zone) + '5 mons'::interval))::date)::timestamp with time zone, '1 mon'::interval) gs(mes) LEFT JOIN public.financial_records fr ON (((fr.deleted_at IS NULL) AND (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])) AND ((date_trunc('month'::text, (fr.due_date)::timestamp with time zone))::date = gs.mes)))) GROUP BY gs.mes ORDER BY gs.mes; CREATE VIEW public.v_commitment_totals AS SELECT c.tenant_id, c.id AS commitment_id, (COALESCE(sum(l.minutes), (0)::bigint))::integer AS total_minutes FROM (public.determined_commitments c LEFT JOIN public.commitment_time_logs l ON ((l.commitment_id = c.id))) GROUP BY c.tenant_id, c.id; CREATE VIEW public.v_patient_engajamento WITH (security_invoker='on') AS WITH sessoes AS ( SELECT ae.patient_id, ae.tenant_id, count(*) FILTER (WHERE (ae.status = 'realizado'::public.status_evento_agenda)) AS total_realizadas, count(*) FILTER (WHERE (ae.status = ANY (ARRAY['realizado'::public.status_evento_agenda, 'cancelado'::public.status_evento_agenda, 'faltou'::public.status_evento_agenda]))) AS total_marcadas, count(*) FILTER (WHERE (ae.status = 'faltou'::public.status_evento_agenda)) AS total_faltas, max(ae.inicio_em) FILTER (WHERE (ae.status = 'realizado'::public.status_evento_agenda)) AS ultima_sessao_em, min(ae.inicio_em) FILTER (WHERE (ae.status = 'realizado'::public.status_evento_agenda)) AS primeira_sessao_em, count(*) FILTER (WHERE ((ae.status = 'realizado'::public.status_evento_agenda) AND (ae.inicio_em >= (now() - '30 days'::interval)))) AS sessoes_ultimo_mes FROM public.agenda_eventos ae WHERE (ae.patient_id IS NOT NULL) GROUP BY ae.patient_id, ae.tenant_id ), financeiro AS ( SELECT fr.patient_id, fr.tenant_id, COALESCE(sum(fr.final_amount) FILTER (WHERE (fr.status = 'paid'::text)), (0)::numeric) AS total_pago, COALESCE(avg(fr.final_amount) FILTER (WHERE (fr.status = 'paid'::text)), (0)::numeric) AS ticket_medio, count(*) FILTER (WHERE ((fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])) AND (fr.due_date < now()))) AS cobr_vencidas, count(*) FILTER (WHERE (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text]))) AS cobr_pendentes, count(*) FILTER (WHERE ((fr.type = 'receita'::public.financial_record_type) AND (fr.status = 'paid'::text))) AS cobr_pagas FROM public.financial_records fr WHERE ((fr.patient_id IS NOT NULL) AND (fr.deleted_at IS NULL)) GROUP BY fr.patient_id, fr.tenant_id ) SELECT p.id AS patient_id, p.tenant_id, p.nome_completo, p.status, p.risco_elevado, COALESCE(s.total_realizadas, (0)::bigint) AS total_sessoes, COALESCE(s.sessoes_ultimo_mes, (0)::bigint) AS sessoes_ultimo_mes, s.primeira_sessao_em, s.ultima_sessao_em, (EXTRACT(day FROM (now() - s.ultima_sessao_em)))::integer AS dias_sem_sessao, CASE WHEN (COALESCE(s.total_marcadas, (0)::bigint) = 0) THEN NULL::numeric ELSE round((((s.total_realizadas)::numeric / (s.total_marcadas)::numeric) * (100)::numeric), 1) END AS taxa_comparecimento, COALESCE(f.total_pago, (0)::numeric) AS ltv_total, round(COALESCE(f.ticket_medio, (0)::numeric), 2) AS ticket_medio, COALESCE(f.cobr_vencidas, (0)::bigint) AS cobr_vencidas, COALESCE(f.cobr_pagas, (0)::bigint) AS cobr_pagas, CASE WHEN (COALESCE((f.cobr_pagas + f.cobr_vencidas), (0)::bigint) = 0) THEN NULL::numeric ELSE round((((f.cobr_pagas)::numeric / ((f.cobr_pagas + f.cobr_vencidas))::numeric) * (100)::numeric), 1) END AS taxa_pagamentos_dia, round(LEAST((100)::numeric, COALESCE((( CASE WHEN (COALESCE(s.total_marcadas, (0)::bigint) = 0) THEN (50)::numeric ELSE LEAST((50)::numeric, (((s.total_realizadas)::numeric / (s.total_marcadas)::numeric) * (50)::numeric)) END + CASE WHEN (COALESCE((f.cobr_pagas + f.cobr_vencidas), (0)::bigint) = 0) THEN (30)::numeric ELSE LEAST((30)::numeric, (((f.cobr_pagas)::numeric / ((f.cobr_pagas + f.cobr_vencidas))::numeric) * (30)::numeric)) END) + ( CASE WHEN (s.ultima_sessao_em IS NULL) THEN 0 WHEN (EXTRACT(day FROM (now() - s.ultima_sessao_em)) <= (14)::numeric) THEN 20 WHEN (EXTRACT(day FROM (now() - s.ultima_sessao_em)) <= (30)::numeric) THEN 15 WHEN (EXTRACT(day FROM (now() - s.ultima_sessao_em)) <= (60)::numeric) THEN 8 ELSE 0 END)::numeric), (0)::numeric)), 0) AS engajamento_score, CASE WHEN (s.primeira_sessao_em IS NULL) THEN NULL::integer ELSE (EXTRACT(day FROM (now() - s.primeira_sessao_em)))::integer END AS duracao_tratamento_dias FROM ((public.patients p LEFT JOIN sessoes s ON (((s.patient_id = p.id) AND (s.tenant_id = p.tenant_id)))) LEFT JOIN financeiro f ON (((f.patient_id = p.id) AND (f.tenant_id = p.tenant_id)))); CREATE VIEW public.v_patient_groups_with_counts AS SELECT pg.id, pg.nome, pg.cor, pg.owner_id, pg.is_system, pg.is_active, pg.created_at, pg.updated_at, (COALESCE(count(pgp.patient_id), (0)::bigint))::integer AS patients_count FROM (public.patient_groups pg LEFT JOIN public.patient_group_patient pgp ON ((pgp.patient_group_id = pg.id))) GROUP BY pg.id, pg.nome, pg.cor, pg.owner_id, pg.is_system, pg.is_active, pg.created_at, pg.updated_at; CREATE VIEW public.v_patients_risco WITH (security_invoker='on') AS SELECT p.id, p.tenant_id, p.nome_completo, p.status, p.risco_elevado, p.risco_nota, p.risco_sinalizado_em, e.dias_sem_sessao, e.engajamento_score, e.taxa_comparecimento, CASE WHEN p.risco_elevado THEN 'risco_sinalizado'::text WHEN ((COALESCE(e.dias_sem_sessao, 999) > 30) AND (p.status = 'Ativo'::text)) THEN 'sem_sessao_30d'::text WHEN (COALESCE(e.taxa_comparecimento, (100)::numeric) < (60)::numeric) THEN 'baixo_comparecimento'::text WHEN (COALESCE(e.cobr_vencidas, (0)::bigint) > 0) THEN 'cobranca_vencida'::text ELSE 'ok'::text END AS alerta_tipo FROM (public.patients p JOIN public.v_patient_engajamento e ON ((e.patient_id = p.id))) WHERE ((p.status = 'Ativo'::text) AND ((p.risco_elevado = true) OR (COALESCE(e.dias_sem_sessao, 999) > 30) OR (COALESCE(e.taxa_comparecimento, (100)::numeric) < (60)::numeric) OR (COALESCE(e.cobr_vencidas, (0)::bigint) > 0))); CREATE VIEW public.v_plan_active_prices AS SELECT plan_id, max( CASE WHEN (("interval" = 'month'::text) AND is_active) THEN amount_cents ELSE NULL::integer END) AS monthly_cents, max( CASE WHEN (("interval" = 'year'::text) AND is_active) THEN amount_cents ELSE NULL::integer END) AS yearly_cents, max( CASE WHEN (("interval" = 'month'::text) AND is_active) THEN currency ELSE NULL::text END) AS monthly_currency, max( CASE WHEN (("interval" = 'year'::text) AND is_active) THEN currency ELSE NULL::text END) AS yearly_currency FROM public.plan_prices GROUP BY plan_id; CREATE VIEW public.v_public_pricing AS SELECT p.id AS plan_id, p.key AS plan_key, p.name AS plan_name, COALESCE(pp.public_name, ''::text) AS public_name, COALESCE(pp.public_description, ''::text) AS public_description, pp.badge, COALESCE(pp.is_featured, false) AS is_featured, COALESCE(pp.is_visible, true) AS is_visible, COALESCE(pp.sort_order, 0) AS sort_order, ap.monthly_cents, ap.yearly_cents, ap.monthly_currency, ap.yearly_currency, COALESCE(( SELECT jsonb_agg(jsonb_build_object('id', b.id, 'text', b.text, 'highlight', b.highlight, 'sort_order', b.sort_order) ORDER BY b.sort_order, b.created_at) AS jsonb_agg FROM public.plan_public_bullets b WHERE (b.plan_id = p.id)), '[]'::jsonb) AS bullets, p.target AS plan_target FROM ((public.plans p LEFT JOIN public.plan_public pp ON ((pp.plan_id = p.id))) LEFT JOIN public.v_plan_active_prices ap ON ((ap.plan_id = p.id))) ORDER BY COALESCE(pp.sort_order, 0), p.key; CREATE VIEW public.v_subscription_feature_mismatch AS WITH expected AS ( SELECT s.user_id AS owner_id, f.key AS feature_key FROM ((public.subscriptions s JOIN public.plan_features pf ON (((pf.plan_id = s.plan_id) AND (pf.enabled = true)))) JOIN public.features f ON ((f.id = pf.feature_id))) WHERE ((s.status = 'active'::text) AND (s.tenant_id IS NULL) AND (s.user_id IS NOT NULL)) ), actual AS ( SELECT e.owner_id, e.feature_key FROM public.owner_feature_entitlements e ) SELECT COALESCE(expected.owner_id, actual.owner_id) AS owner_id, COALESCE(expected.feature_key, actual.feature_key) AS feature_key, CASE WHEN ((expected.feature_key IS NOT NULL) AND (actual.feature_key IS NULL)) THEN 'missing_entitlement'::text WHEN ((expected.feature_key IS NULL) AND (actual.feature_key IS NOT NULL)) THEN 'unexpected_entitlement'::text ELSE NULL::text END AS mismatch_type FROM (expected FULL JOIN actual ON (((expected.owner_id = actual.owner_id) AND (expected.feature_key = actual.feature_key)))) WHERE ((expected.feature_key IS NULL) OR (actual.feature_key IS NULL)); CREATE VIEW public.v_subscription_health AS SELECT s.id AS subscription_id, s.user_id AS owner_id, s.status, s.plan_id, p.key AS plan_key, s.current_period_start, s.current_period_end, s.updated_at, CASE WHEN (s.plan_id IS NULL) THEN 'missing_plan'::text WHEN (p.id IS NULL) THEN 'invalid_plan'::text WHEN ((s.status = 'active'::text) AND (s.current_period_end IS NOT NULL) AND (s.current_period_end < now())) THEN 'expired_but_active'::text WHEN ((s.status = 'canceled'::text) AND (s.current_period_end > now())) THEN 'canceled_but_still_in_period'::text ELSE 'ok'::text END AS health_status, CASE WHEN (s.tenant_id IS NOT NULL) THEN 'clinic'::text ELSE 'therapist'::text END AS owner_type, COALESCE(s.tenant_id, s.user_id) AS owner_ref FROM (public.subscriptions s LEFT JOIN public.plans p ON ((p.id = s.plan_id))); CREATE VIEW public.v_subscription_health_v2 AS SELECT s.id AS subscription_id, s.user_id AS owner_id, CASE WHEN (s.tenant_id IS NOT NULL) THEN 'clinic'::text ELSE 'therapist'::text END AS owner_type, COALESCE(s.tenant_id, s.user_id) AS owner_ref, s.status, s.plan_id, p.key AS plan_key, s.current_period_start, s.current_period_end, s.updated_at, CASE WHEN (s.plan_id IS NULL) THEN 'missing_plan'::text WHEN (p.id IS NULL) THEN 'invalid_plan'::text WHEN ((s.status = 'active'::text) AND (s.current_period_end IS NOT NULL) AND (s.current_period_end < now())) THEN 'expired_but_active'::text WHEN ((s.status = 'canceled'::text) AND (s.current_period_end > now())) THEN 'canceled_but_still_in_period'::text ELSE 'ok'::text END AS health_status FROM (public.subscriptions s LEFT JOIN public.plans p ON ((p.id = s.plan_id))); CREATE VIEW public.v_tag_patient_counts AS SELECT t.id, t.owner_id, t.nome, t.cor, t.is_padrao, t.created_at, t.updated_at, (COALESCE(count(ppt.patient_id), (0)::bigint))::integer AS pacientes_count, (COALESCE(count(ppt.patient_id), (0)::bigint))::integer AS patient_count FROM (public.patient_tags t LEFT JOIN public.patient_patient_tag ppt ON (((ppt.tag_id = t.id) AND (ppt.owner_id = t.owner_id)))) GROUP BY t.id, t.owner_id, t.nome, t.cor, t.is_padrao, t.created_at, t.updated_at; CREATE VIEW public.v_tenant_active_subscription AS SELECT DISTINCT ON (tenant_id) tenant_id, plan_id, plan_key, "interval", status, current_period_start, current_period_end, created_at FROM public.subscriptions s WHERE ((tenant_id IS NOT NULL) AND (status = 'active'::text) AND ((current_period_end IS NULL) OR (current_period_end > now()))) ORDER BY tenant_id, created_at DESC; CREATE VIEW public.v_tenant_entitlements AS SELECT a.tenant_id, f.key AS feature_key, true AS allowed FROM ((public.v_tenant_active_subscription a JOIN public.plan_features pf ON (((pf.plan_id = a.plan_id) AND (pf.enabled = true)))) JOIN public.features f ON ((f.id = pf.feature_id))); CREATE VIEW public.v_tenant_entitlements_full AS SELECT a.tenant_id, f.key AS feature_key, (pf.enabled = true) AS allowed, pf.limits, a.plan_id, p.key AS plan_key FROM (((public.v_tenant_active_subscription a JOIN public.plan_features pf ON ((pf.plan_id = a.plan_id))) JOIN public.features f ON ((f.id = pf.feature_id))) JOIN public.plans p ON ((p.id = a.plan_id))); CREATE VIEW public.v_tenant_entitlements_json AS SELECT tenant_id, max(plan_key) AS plan_key, jsonb_object_agg(feature_key, jsonb_build_object('allowed', allowed, 'limits', COALESCE(limits, '{}'::jsonb)) ORDER BY feature_key) AS entitlements FROM public.v_tenant_entitlements_full GROUP BY tenant_id; CREATE VIEW public.v_tenant_feature_exceptions AS SELECT tf.tenant_id, a.plan_key, tf.feature_key, 'commercial_exception'::text AS exception_type FROM ((public.tenant_features tf JOIN public.v_tenant_active_subscription a ON ((a.tenant_id = tf.tenant_id))) LEFT JOIN public.v_tenant_entitlements_full v ON (((v.tenant_id = tf.tenant_id) AND (v.feature_key = tf.feature_key)))) WHERE ((tf.enabled = true) AND (COALESCE(v.allowed, false) = false)); CREATE VIEW public.v_tenant_feature_mismatch AS WITH plan_allowed AS ( SELECT v.tenant_id, v.feature_key, v.allowed FROM public.v_tenant_entitlements_full v ), overrides AS ( SELECT tf.tenant_id, tf.feature_key, tf.enabled FROM public.tenant_features tf ) SELECT o.tenant_id, o.feature_key, CASE WHEN ((o.enabled = true) AND (COALESCE(p.allowed, false) = false)) THEN 'unexpected_override'::text ELSE NULL::text END AS mismatch_type FROM (overrides o LEFT JOIN plan_allowed p ON (((p.tenant_id = o.tenant_id) AND (p.feature_key = o.feature_key)))) WHERE ((o.enabled = true) AND (COALESCE(p.allowed, false) = false)); CREATE VIEW public.v_tenant_members_with_profiles AS SELECT tm.id AS tenant_member_id, tm.tenant_id, tm.user_id, tm.role, tm.status, tm.created_at, p.full_name, au.email FROM ((public.tenant_members tm LEFT JOIN public.profiles p ON ((p.id = tm.user_id))) LEFT JOIN auth.users au ON ((au.id = tm.user_id))); CREATE VIEW public.v_tenant_people AS SELECT 'member'::text AS type, m.tenant_id, m.user_id, u.email, m.role, m.status, NULL::uuid AS invite_token, NULL::timestamp with time zone AS expires_at FROM (public.tenant_members m JOIN auth.users u ON ((u.id = m.user_id))) UNION ALL SELECT 'invite'::text AS type, i.tenant_id, NULL::uuid AS user_id, i.email, i.role, 'invited'::text AS status, i.token AS invite_token, i.expires_at FROM public.tenant_invites i WHERE ((i.accepted_at IS NULL) AND (i.revoked_at IS NULL)); CREATE VIEW public.v_tenant_staff AS SELECT ('m_'::text || (tm.id)::text) AS row_id, tm.tenant_id, tm.user_id, tm.role, tm.status, tm.created_at, p.full_name, au.email, NULL::uuid AS invite_token FROM ((public.tenant_members tm LEFT JOIN public.profiles p ON ((p.id = tm.user_id))) LEFT JOIN auth.users au ON ((au.id = tm.user_id))) UNION ALL SELECT ('i_'::text || (ti.id)::text) AS row_id, ti.tenant_id, NULL::uuid AS user_id, ti.role, 'invited'::text AS status, ti.created_at, NULL::text AS full_name, ti.email, ti.token AS invite_token FROM public.tenant_invites ti WHERE ((ti.accepted_at IS NULL) AND (ti.revoked_at IS NULL) AND (ti.expires_at > now())); CREATE VIEW public.v_twilio_whatsapp_overview AS SELECT nc.id AS channel_id, nc.tenant_id, nc.owner_id, nc.is_active, nc.connection_status, nc.display_name, nc.twilio_subaccount_sid, nc.twilio_phone_number, nc.twilio_phone_sid, nc.cost_per_message_usd, nc.price_per_message_brl, nc.provisioned_at, nc.created_at, nc.updated_at, COALESCE(u.messages_sent, 0) AS current_month_sent, COALESCE(u.messages_delivered, 0) AS current_month_delivered, COALESCE(u.messages_failed, 0) AS current_month_failed, COALESCE(u.cost_usd, (0)::numeric) AS current_month_cost_usd, COALESCE(u.cost_brl, (0)::numeric) AS current_month_cost_brl, COALESCE(u.revenue_brl, (0)::numeric) AS current_month_revenue_brl, COALESCE(u.margin_brl, (0)::numeric) AS current_month_margin_brl FROM (public.notification_channels nc LEFT JOIN public.twilio_subaccount_usage u ON (((u.channel_id = nc.id) AND (u.period_start = (date_trunc('month'::text, (CURRENT_DATE)::timestamp with time zone))::date)))) WHERE ((nc.channel = 'whatsapp'::text) AND (nc.provider = 'twilio'::text) AND (nc.deleted_at IS NULL)); CREATE VIEW public.v_user_active_subscription AS SELECT DISTINCT ON (user_id) user_id, plan_id, plan_key, "interval", status, current_period_start, current_period_end, created_at FROM public.subscriptions s WHERE ((tenant_id IS NULL) AND (user_id IS NOT NULL) AND (status = 'active'::text) AND ((current_period_end IS NULL) OR (current_period_end > now()))) ORDER BY user_id, created_at DESC; CREATE VIEW public.v_user_entitlements AS SELECT a.user_id, f.key AS feature_key, true AS allowed FROM ((public.v_user_active_subscription a JOIN public.plan_features pf ON (((pf.plan_id = a.plan_id) AND (pf.enabled = true)))) JOIN public.features f ON ((f.id = pf.feature_id)));