Files
agenciapsilmno/database-novo/migrations/20260421000007_whatsapp_credits.sql
T
Leonardo 2644e60bb6 CRM WhatsApp Grupo 3 completo + Marco A/B (Asaas) + admin SaaS + refactors polimórficos
Sessão 11+: fechamento do CRM de WhatsApp com dois providers (Evolution/Twilio),
sistema de créditos com Asaas/PIX, polimorfismo de telefones/emails, e integração
admin SaaS no /saas/addons existente.

═══════════════════════════════════════════════════════════════════════════
GRUPO 3 — WORKFLOW / CRM (completo)
═══════════════════════════════════════════════════════════════════════════

3.1 Tags · migration conversation_tags + seed de 5 system tags · composable
useConversationTags.js · popover + pills no drawer e nos cards do Kanban.

3.2 Atribuição de conversa a terapeuta · migration 20260421000012 com PK
(tenant_id, thread_key), UPSERT, RLS que valida assignee como membro ativo
do mesmo tenant · view conversation_threads expandida com assigned_to +
assigned_at · composable useConversationAssignment.js · drawer com Select
filtrável + botão "Assumir" · inbox com filtro aside (Todas/Minhas/Não
atribuídas) e chip do responsável em cada card (destaca "Eu" em azul).

3.3 Notas internas · migration conversation_notes · composable + seção
colapsável no drawer · apenas o criador pode editar/apagar (RLS).

3.5 Converter desconhecido em paciente · botão + dialog quick-cadastro ·
"Vincular existente" com Select filter de até 500 pacientes · cria
telefone WhatsApp (vinculado) via upsertWhatsappForExisting.

3.6 Histórico de conversa no prontuário · nova aba "Conversas" em
PatientProntuario.vue · PatientConversationsTab.vue com stats (total /
recebidas / enviadas / primeira / última), SelectButton de filtro, timeline
com bolhas por direção, mídia inline (imagem/áudio/vídeo/doc via signed
URL), indicadores ✓ ✓✓ de delivery, botão "Abrir no CRM".

═══════════════════════════════════════════════════════════════════════════
MARCO A — UNIFICAÇÃO WHATSAPP (dois providers mutuamente exclusivos)
═══════════════════════════════════════════════════════════════════════════

- Página chooser ConfiguracoesWhatsappChooserPage.vue com 2 cards (Pessoal/
  Oficial), deactivate via edge function deactivate-notification-channel
- send-whatsapp-message refatorada com roteamento por provider; Twilio deduz
  1 crédito antes do envio e refunda em falha
- Paridade Twilio (novo): módulo compartilhado supabase/functions/_shared/
  whatsapp-hooks.ts com lógica provider-agnóstica (opt-in, opt-out, auto-
  reply, schedule helpers em TZ São Paulo, makeTwilioCreditedSendFn que
  envolve envio em dedução atômica + rollback). Consumido por Evolution E
  Twilio inbound. Evolution refatorado (~290 linhas duplicadas removidas).
- Bucket privado whatsapp-media · decrypt via Evolution getBase64From
  MediaMessage · upload com path tenant/yyyy/mm · signed URLs on-demand

═══════════════════════════════════════════════════════════════════════════
MARCO B — SISTEMA DE CRÉDITOS WHATSAPP + ASAAS
═══════════════════════════════════════════════════════════════════════════

Banco:
- Migration 20260421000007_whatsapp_credits (4 tabelas: balance,
  transactions, packages, purchases) + RPCs add_whatsapp_credits e
  deduct_whatsapp_credits (atômicas com SELECT FOR UPDATE)
- Migration 20260421000013_tenant_cpf_cnpj (coluna em tenants com CHECK
  de 11 ou 14 dígitos)

Edge functions:
- create-whatsapp-credit-charge · Asaas v3 (sandbox + prod) · PIX com
  QR code · getOrCreateAsaasCustomer patcha customer existente com CPF
  quando está faltando
- asaas-webhook · recebe PAYMENT_RECEIVED/CONFIRMED e credita balance

Frontend (tenant):
- Página /configuracoes/creditos-whatsapp com saldo + loja + histórico
- Dialog de confirmação com CPF/CNPJ (validação via isValidCPF/CNPJ de
  utils/validators, formatação on-blur, pré-fill de tenants.cpf_cnpj,
  persiste no primeiro uso) · fallback sandbox 24971563792 REMOVIDO
- Composable useWhatsappCredits extrai erros amigáveis via
  error.context.json()

Frontend (SaaS admin):
- Em /saas/addons (reuso do pattern existente, não criou página paralela):
  - Aba 4 "Pacotes WhatsApp" — CRUD whatsapp_credit_packages com DataTable,
    toggle is_active inline, dialog de edição com validação
  - Aba 5 "Topup WhatsApp" — tenant Select com saldo ao vivo · RPC
    add_whatsapp_credits com p_admin_id = auth.uid() (auditoria) · histórico
    das últimas 20 transações topup/adjustment/refund

═══════════════════════════════════════════════════════════════════════════
GRUPO 2 — AUTOMAÇÃO
═══════════════════════════════════════════════════════════════════════════

2.3 Auto-reply · conversation_autoreply_settings + conversation_autoreply_
log · 3 modos de schedule (agenda das regras semanais, business_hours
custom, custom_window) · cooldown por thread · respeita opt-out · agora
funciona em Evolution E Twilio (hooks compartilhados)

2.4 Lembretes de sessão · conversation_session_reminders_settings +
_logs · edge send-session-reminders (cron) · janelas 24h e 2h antes ·
Twilio deduz crédito com rollback em falha

═══════════════════════════════════════════════════════════════════════════
GRUPO 5 — COMPLIANCE (LGPD Art. 18 §2)
═══════════════════════════════════════════════════════════════════════════

5.2 Opt-out · conversation_optouts + conversation_optout_keywords (10 system
seed + custom por tenant) · detecção por regex word-boundary e normalização
(lowercase + strip acentos + pontuação) · ack automático (deduz crédito em
Twilio) · opt-in via "voltar", "retornar", "reativar", "restart" ·
página /configuracoes/conversas-optouts com CRUD de keywords

═══════════════════════════════════════════════════════════════════════════
REFACTOR POLIMÓRFICO — TELEFONES + EMAILS
═══════════════════════════════════════════════════════════════════════════

- contact_types + contact_phones (entity_type + entity_id) — migration
  20260421000008 · contact_email_types + contact_emails — 20260421000011
- Componentes ContactPhonesEditor.vue e ContactEmailsEditor.vue (add/edit/
  remove com confirm, primary selector, WhatsApp linked badge)
- Composables useContactPhones.js + useContactEmails.js com
  unsetOtherPrimaries() e validação
- Trocado em PatientsCadastroPage.vue e MedicosPage.vue (removidos campos
  legados telefone/telefone_alternativo e email_principal/email_alternativo)
- Migration retroativa v2 (20260421000010) detecta conversation_messages
  e cria/atualiza phone como WhatsApp vinculado

═══════════════════════════════════════════════════════════════════════════
POLIMENTO VISUAL + INFRA
═══════════════════════════════════════════════════════════════════════════

- Skeletons simplificados no dashboard do terapeuta
- Animações fade-up com stagger via [--delay:Xms] (fix specificity sobre
  .dash-card box-shadow transition)
- ConfirmDialog com group="conversation-drawer" (evita montagem duplicada)
- Image preview PrimeVue com botão de download injetado via MutationObserver
  (fetch + blob para funcionar cross-origin)
- Áudio/vídeo com preload="metadata" e controles de velocidade do browser
- friendlySendError() mapeia códigos do edge pra mensagens pt-BR via
  error.context.json()
- Teleport #cfg-page-actions para ações globais de Configurações
- Brotli/Gzip + auto-import Vue/PrimeVue + bundle analyzer
- AppLayout consolidado (removidas duplicatas por área) + RouterPassthrough
- Removido console.trace debug que estava em watch de router e queries
  Supabase (degradava perf pra todos)
- Realtime em conversation_messages via publication supabase_realtime
- Notifier global flutuante com beep + toggle mute (4 camadas: badge +
  sino + popup + browser notification)

═══════════════════════════════════════════════════════════════════════════
MIGRATIONS NOVAS (13)
═══════════════════════════════════════════════════════════════════════════

20260420000001_patient_intake_invite_info_rpc
20260420000002_audit_logs_lgpd
20260420000003_audit_logs_unified_view
20260420000004_lgpd_export_patient_rpc
20260420000005_conversation_messages
20260420000005_search_global_rpc
20260420000006_conv_messages_notifications
20260420000007_notif_channels_saas_admin_insert
20260420000008_conv_messages_realtime
20260420000009_conv_messages_delivery_status
20260421000001_whatsapp_media_bucket
20260421000002_conversation_notes
20260421000003_conversation_tags
20260421000004_conversation_autoreply
20260421000005_conversation_optouts
20260421000006_session_reminders
20260421000007_whatsapp_credits
20260421000008_contact_phones
20260421000009_retroactive_whatsapp_link
20260421000010_retroactive_whatsapp_link_v2
20260421000011_contact_emails
20260421000012_conversation_assignments
20260421000013_tenant_cpf_cnpj

═══════════════════════════════════════════════════════════════════════════
EDGE FUNCTIONS NOVAS / MODIFICADAS
═══════════════════════════════════════════════════════════════════════════

Novas:
- _shared/whatsapp-hooks.ts (módulo compartilhado)
- asaas-webhook
- create-whatsapp-credit-charge
- deactivate-notification-channel
- evolution-webhook-provision
- evolution-whatsapp-inbound
- get-intake-invite-info
- notification-webhook
- send-session-reminders
- send-whatsapp-message
- submit-patient-intake
- twilio-whatsapp-inbound

═══════════════════════════════════════════════════════════════════════════
FRONTEND — RESUMO
═══════════════════════════════════════════════════════════════════════════

Composables novos: useAddonExtrato, useAuditoria, useAutoReplySettings,
useClinicKPIs, useContactEmails, useContactPhones, useConversationAssignment,
useConversationNotes, useConversationOptouts, useConversationTags,
useConversations, useLgpdExport, useSessionReminders, useWhatsappCredits

Stores: conversationDrawerStore

Componentes novos: ConversationDrawer, GlobalInboundNotifier, GlobalSearch,
ContactEmailsEditor, ContactPhonesEditor

Páginas novas: CRMConversasPage, PatientConversationsTab, AddonsExtratoPage,
AuditoriaPage, NotificationsHistoryPage, ConfiguracoesWhatsappChooserPage,
ConfiguracoesConversasAutoreplyPage, ConfiguracoesConversasOptoutsPage,
ConfiguracoesConversasTagsPage, ConfiguracoesCreditosWhatsappPage,
ConfiguracoesLembretesSessaoPage

Utils novos: addonExtratoExport, auditoriaExport, excelExport,
lgpdExportFormats

Páginas existentes alteradas: ClinicDashboard, PatientsCadastroPage,
PatientCadastroDialog, PatientsListPage, MedicosPage, PatientProntuario,
ConfiguracoesWhatsappPage, SaasWhatsappPage, ConfiguracoesRecursosExtrasPage,
ConfiguracoesPage, AgendaTerapeutaPage, AgendaClinicaPage, NotificationItem,
NotificationDrawer, AppLayout, AppTopbar, useMenuBadges,
patientsRepository, SaasAddonsPage (aba 4 + 5 WhatsApp)

Routes: routes.clinic, routes.configs, routes.therapist atualizados
Menus: clinic.menu, therapist.menu, saas.menu atualizados

═══════════════════════════════════════════════════════════════════════════
NOTAS

- Após subir, rodar supabase functions serve --no-verify-jwt
  --env-file supabase/functions/.env pra carregar o módulo _shared
- WHATSAPP_SETUP.md reescrito (~400 linhas) com setup completo dos 3
  providers + troubleshooting + LGPD
- HANDOFF.md atualizado com estado atual e próximos passos

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:05:24 -03:00

343 lines
14 KiB
PL/PgSQL

-- ==========================================================================
-- Agencia PSI — Migracao: Sistema de créditos WhatsApp (Marco B)
-- ==========================================================================
-- Criado por: Leonardo Nohama
-- Data: 2026-04-21 · Sao Carlos/SP — Brasil
--
-- Modelo:
-- - whatsapp_credits_balance → saldo atual por tenant (snapshot)
-- - whatsapp_credits_transactions → extrato (purchase, usage, topup, adj)
-- - whatsapp_credit_packages → pacotes oferecidos (SaaS-managed)
-- - whatsapp_credit_purchases → ordens de compra via Asaas
--
-- Helpers (RPC):
-- - add_whatsapp_credits(tenant, amount, kind, ...) → novo saldo
-- - deduct_whatsapp_credits(tenant, amount, message_id) → boolean
--
-- Creditos so sao deduzidos quando o tenant usa provider='twilio'
-- (Evolution e free).
-- ==========================================================================
-- ---------------------------------------------------------------------------
-- Saldo por tenant
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.whatsapp_credits_balance (
tenant_id UUID PRIMARY KEY REFERENCES public.tenants(id) ON DELETE CASCADE,
balance INT NOT NULL DEFAULT 0 CHECK (balance >= 0),
lifetime_purchased INT NOT NULL DEFAULT 0,
lifetime_used INT NOT NULL DEFAULT 0,
low_balance_threshold INT NOT NULL DEFAULT 20 CHECK (low_balance_threshold >= 0),
low_balance_alerted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
DROP TRIGGER IF EXISTS trg_wa_credits_balance_updated_at ON public.whatsapp_credits_balance;
CREATE TRIGGER trg_wa_credits_balance_updated_at
BEFORE UPDATE ON public.whatsapp_credits_balance
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
COMMENT ON TABLE public.whatsapp_credits_balance IS
'Saldo atual de creditos WhatsApp por tenant. 1 credito = 1 mensagem Twilio.';
-- ---------------------------------------------------------------------------
-- Extrato (transações)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.whatsapp_credits_transactions (
id BIGSERIAL PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
kind TEXT NOT NULL CHECK (kind IN ('purchase', 'usage', 'topup_manual', 'refund', 'adjustment')),
amount INT NOT NULL, -- positivo = credito, negativo = debito
balance_after INT NOT NULL,
-- Referencias opcionais
conversation_message_id BIGINT REFERENCES public.conversation_messages(id) ON DELETE SET NULL,
purchase_id UUID,
admin_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
note TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_wa_credits_tx_tenant_created
ON public.whatsapp_credits_transactions (tenant_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_wa_credits_tx_kind
ON public.whatsapp_credits_transactions (tenant_id, kind, created_at DESC);
COMMENT ON TABLE public.whatsapp_credits_transactions IS
'Extrato de creditos WhatsApp. Append-only — nao editar/deletar.';
-- ---------------------------------------------------------------------------
-- Pacotes (global, gerenciado pelo SaaS admin)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.whatsapp_credit_packages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL CHECK (length(name) > 0 AND length(name) <= 100),
description TEXT,
credits INT NOT NULL CHECK (credits > 0),
price_brl NUMERIC(10,2) NOT NULL CHECK (price_brl > 0),
is_active BOOLEAN NOT NULL DEFAULT true,
is_featured BOOLEAN NOT NULL DEFAULT false,
position INT NOT NULL DEFAULT 100,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
DROP TRIGGER IF EXISTS trg_wa_credit_packages_updated_at ON public.whatsapp_credit_packages;
CREATE TRIGGER trg_wa_credit_packages_updated_at
BEFORE UPDATE ON public.whatsapp_credit_packages
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
CREATE INDEX IF NOT EXISTS idx_wa_credit_packages_active
ON public.whatsapp_credit_packages (is_active, position, price_brl)
WHERE is_active = true;
COMMENT ON TABLE public.whatsapp_credit_packages IS
'Pacotes de creditos disponiveis pra compra. Gerenciado pelo SaaS admin.';
-- Seed: pacotes padrao
INSERT INTO public.whatsapp_credit_packages (name, description, credits, price_brl, is_featured, position) VALUES
('Iniciante', 'Ideal pra conhecer a plataforma', 100, 49.90, false, 10),
('Profissional', 'Mais vendido pra clínicas pequenas', 500, 199.90, true, 20),
('Clínica', 'Pra clínicas com alto volume', 1500, 499.90, false, 30),
('Enterprise', 'Pacote grande com desconto', 5000, 1499.90, false, 40)
ON CONFLICT DO NOTHING;
-- ---------------------------------------------------------------------------
-- Ordens de compra (Asaas integration)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.whatsapp_credit_purchases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
package_id UUID REFERENCES public.whatsapp_credit_packages(id) ON DELETE SET NULL,
-- Snapshot do pacote no momento da compra (caso mude de preço/creditos depois)
package_name TEXT NOT NULL,
credits INT NOT NULL CHECK (credits > 0),
amount_brl NUMERIC(10,2) NOT NULL CHECK (amount_brl > 0),
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'paid', 'failed', 'expired', 'refunded', 'cancelled')),
-- Asaas integration
asaas_customer_id TEXT,
asaas_payment_id TEXT,
asaas_payment_link TEXT,
asaas_pix_qrcode TEXT, -- base64 da imagem
asaas_pix_copy_paste TEXT, -- codigo PIX copia-cola
paid_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
failed_at TIMESTAMPTZ,
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
DROP TRIGGER IF EXISTS trg_wa_credit_purchases_updated_at ON public.whatsapp_credit_purchases;
CREATE TRIGGER trg_wa_credit_purchases_updated_at
BEFORE UPDATE ON public.whatsapp_credit_purchases
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
CREATE INDEX IF NOT EXISTS idx_wa_credit_purchases_tenant
ON public.whatsapp_credit_purchases (tenant_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_wa_credit_purchases_status
ON public.whatsapp_credit_purchases (status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_wa_credit_purchases_asaas_payment
ON public.whatsapp_credit_purchases (asaas_payment_id)
WHERE asaas_payment_id IS NOT NULL;
-- FK pra transactions.purchase_id (circular, então define depois)
ALTER TABLE public.whatsapp_credits_transactions
DROP CONSTRAINT IF EXISTS whatsapp_credits_transactions_purchase_id_fkey;
ALTER TABLE public.whatsapp_credits_transactions
ADD CONSTRAINT whatsapp_credits_transactions_purchase_id_fkey
FOREIGN KEY (purchase_id) REFERENCES public.whatsapp_credit_purchases(id) ON DELETE SET NULL;
COMMENT ON TABLE public.whatsapp_credit_purchases IS
'Ordens de compra de creditos via Asaas. Webhook atualiza status.';
-- ---------------------------------------------------------------------------
-- RPC: add_whatsapp_credits (SECURITY DEFINER — atualiza saldo + registra tx)
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.add_whatsapp_credits(
p_tenant_id UUID,
p_amount INT,
p_kind TEXT,
p_purchase_id UUID DEFAULT NULL,
p_admin_id UUID DEFAULT NULL,
p_note TEXT DEFAULT NULL
)
RETURNS INT
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_new_balance INT;
BEGIN
IF p_amount <= 0 THEN
RAISE EXCEPTION 'amount must be positive';
END IF;
IF p_kind NOT IN ('purchase', 'topup_manual', 'refund', 'adjustment') THEN
RAISE EXCEPTION 'invalid kind for credit: %', p_kind;
END IF;
INSERT INTO public.whatsapp_credits_balance (tenant_id, balance, lifetime_purchased)
VALUES (p_tenant_id, p_amount, CASE WHEN p_kind IN ('purchase', 'topup_manual') THEN p_amount ELSE 0 END)
ON CONFLICT (tenant_id) DO UPDATE SET
balance = whatsapp_credits_balance.balance + EXCLUDED.balance,
lifetime_purchased = whatsapp_credits_balance.lifetime_purchased
+ CASE WHEN p_kind IN ('purchase', 'topup_manual') THEN p_amount ELSE 0 END,
low_balance_alerted_at = NULL -- reset alerta quando recebe creditos
RETURNING balance INTO v_new_balance;
INSERT INTO public.whatsapp_credits_transactions
(tenant_id, kind, amount, balance_after, purchase_id, admin_id, note)
VALUES
(p_tenant_id, p_kind, p_amount, v_new_balance, p_purchase_id, p_admin_id, p_note);
RETURN v_new_balance;
END;
$$;
REVOKE ALL ON FUNCTION public.add_whatsapp_credits(UUID, INT, TEXT, UUID, UUID, TEXT) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.add_whatsapp_credits(UUID, INT, TEXT, UUID, UUID, TEXT) TO service_role;
-- ---------------------------------------------------------------------------
-- RPC: deduct_whatsapp_credits (atomico, falha se saldo insuficiente)
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.deduct_whatsapp_credits(
p_tenant_id UUID,
p_amount INT,
p_conversation_message_id BIGINT DEFAULT NULL,
p_note TEXT DEFAULT NULL
)
RETURNS INT
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_new_balance INT;
v_row RECORD;
BEGIN
IF p_amount <= 0 THEN
RAISE EXCEPTION 'amount must be positive';
END IF;
-- Lock a linha e valida saldo
SELECT balance, low_balance_threshold
INTO v_row
FROM public.whatsapp_credits_balance
WHERE tenant_id = p_tenant_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'insufficient_credits';
END IF;
IF v_row.balance < p_amount THEN
RAISE EXCEPTION 'insufficient_credits';
END IF;
UPDATE public.whatsapp_credits_balance
SET balance = balance - p_amount,
lifetime_used = lifetime_used + p_amount
WHERE tenant_id = p_tenant_id
RETURNING balance INTO v_new_balance;
INSERT INTO public.whatsapp_credits_transactions
(tenant_id, kind, amount, balance_after, conversation_message_id, note)
VALUES
(p_tenant_id, 'usage', -p_amount, v_new_balance, p_conversation_message_id, p_note);
RETURN v_new_balance;
END;
$$;
REVOKE ALL ON FUNCTION public.deduct_whatsapp_credits(UUID, INT, BIGINT, TEXT) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.deduct_whatsapp_credits(UUID, INT, BIGINT, TEXT) TO service_role;
-- ---------------------------------------------------------------------------
-- RLS
-- ---------------------------------------------------------------------------
ALTER TABLE public.whatsapp_credits_balance ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.whatsapp_credits_transactions ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.whatsapp_credit_packages ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.whatsapp_credit_purchases ENABLE ROW LEVEL SECURITY;
-- Balance: members do tenant leem, saas_admin tudo
DROP POLICY IF EXISTS "wa_credits_balance: select tenant" ON public.whatsapp_credits_balance;
CREATE POLICY "wa_credits_balance: select tenant"
ON public.whatsapp_credits_balance FOR SELECT TO authenticated
USING (
public.is_saas_admin()
OR EXISTS (
SELECT 1 FROM public.tenant_members tm
WHERE tm.user_id = auth.uid() AND tm.tenant_id = whatsapp_credits_balance.tenant_id AND tm.status = 'active'
)
);
-- Settings update: members do tenant podem alterar low_balance_threshold
DROP POLICY IF EXISTS "wa_credits_balance: update tenant" ON public.whatsapp_credits_balance;
CREATE POLICY "wa_credits_balance: update tenant"
ON public.whatsapp_credits_balance FOR UPDATE TO authenticated
USING (
public.is_saas_admin()
OR EXISTS (
SELECT 1 FROM public.tenant_members tm
WHERE tm.user_id = auth.uid() AND tm.tenant_id = whatsapp_credits_balance.tenant_id AND tm.status = 'active'
)
)
WITH CHECK (
public.is_saas_admin()
OR EXISTS (
SELECT 1 FROM public.tenant_members tm
WHERE tm.user_id = auth.uid() AND tm.tenant_id = whatsapp_credits_balance.tenant_id AND tm.status = 'active'
)
);
-- Transactions: read-only pra tenant members, write via RPC
DROP POLICY IF EXISTS "wa_credits_tx: select tenant" ON public.whatsapp_credits_transactions;
CREATE POLICY "wa_credits_tx: select tenant"
ON public.whatsapp_credits_transactions FOR SELECT TO authenticated
USING (
public.is_saas_admin()
OR EXISTS (
SELECT 1 FROM public.tenant_members tm
WHERE tm.user_id = auth.uid() AND tm.tenant_id = whatsapp_credits_transactions.tenant_id AND tm.status = 'active'
)
);
-- Packages: todos leem os ativos; saas_admin gerencia
DROP POLICY IF EXISTS "wa_packages: select active" ON public.whatsapp_credit_packages;
CREATE POLICY "wa_packages: select active"
ON public.whatsapp_credit_packages FOR SELECT TO authenticated
USING (is_active = true OR public.is_saas_admin());
DROP POLICY IF EXISTS "wa_packages: manage saas admin" ON public.whatsapp_credit_packages;
CREATE POLICY "wa_packages: manage saas admin"
ON public.whatsapp_credit_packages FOR ALL TO authenticated
USING (public.is_saas_admin())
WITH CHECK (public.is_saas_admin());
-- Purchases: members do tenant leem as proprias
DROP POLICY IF EXISTS "wa_purchases: select tenant" ON public.whatsapp_credit_purchases;
CREATE POLICY "wa_purchases: select tenant"
ON public.whatsapp_credit_purchases FOR SELECT TO authenticated
USING (
public.is_saas_admin()
OR EXISTS (
SELECT 1 FROM public.tenant_members tm
WHERE tm.user_id = auth.uid() AND tm.tenant_id = whatsapp_credit_purchases.tenant_id AND tm.status = 'active'
)
);
-- ==========================================================================
-- FIM DA MIGRACAO
-- ==========================================================================