Files
agenciapsilmno/database-novo/schema/04_tables/comunica_o_notifica_es.sql
T
Leonardo 7c20b518d4 Sessoes 1-6 acumuladas: hardening B2, defesa em camadas, +192 testes
Repositorio estava ha ~5 sessoes sem commit. Consolida tudo desde d088a89.

Ver commit.md na raiz para descricao completa por sessao.

# Numeros
- A# auditoria abertos: 0/30
- V# verificacoes abertos: 5/52 (todos adiados com plano)
- T# testes escritos: 10/10
- Vitest: 192/192
- SQL integration: 33/33
- E2E (Playwright, novo): 5/5
- Migrations: 17 (10 novas Sessao 6)
- Areas auditadas: 7 (+documentos com 10 V#)

# Highlights Sessao 6 (hoje)
- V#34/V#41 Opcao B2: tenant_features com plano + override (RPC SECURITY DEFINER, tela /saas/tenant-features)
- A#20 rev2 self-hosted: defesa em 5 camadas (honeypot + rate limit + math captcha condicional + paranoid mode + dashboard /saas/security)
- Documentos hardening (V#43-V#49): tenant scoping em storage policies (vazamento entre clinicas eliminado), RPC validate_share_token, signatures policy granular
- SaaS Twilio Config (/saas/twilio-config): UI editavel para SID/webhook/cotacao; AUTH_TOKEN permanece em env var
- T#9 + T#10: useAgendaEvents.spec.js + Playwright E2E (descobriu bug no front que foi corrigido)

# Sessoes anteriores (1-5) consolidadas
- Sessao 1: auth/router/session, normalizeRole extraido
- Sessao 2: agenda - composables/services consolidados
- Sessao 3: pacientes - tenant_id em todas queries
- Sessao 4: security review pagina publica - 14/15 vulnerabilidades corrigidas
- Sessao 5: SaaS - P0 (A#30: 7 tabelas com RLS off corrigidas)

# .gitignore ajustado
- supabase/* + !supabase/functions/ (mantem 10 edge functions, ignora .temp/migrations gerados pelo CLI)
- database-novo/backups/ (regeneravel via db.cjs backup)
- test-results/ + playwright-report/
- .claude/settings.local.json (config local com senha de dev removida do tracking)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:42:46 -03:00

285 lines
13 KiB
SQL

-- Tables: Comunicação / Notificações
-- Gerado automaticamente em 2026-04-17T12:23:05.230Z
-- Total: 14
CREATE TABLE public.email_layout_config (
id uuid DEFAULT gen_random_uuid() NOT NULL,
tenant_id uuid NOT NULL,
header_config jsonb DEFAULT '{"layout": null, "content": "", "enabled": false}'::jsonb NOT NULL,
footer_config jsonb DEFAULT '{"layout": null, "content": "", "enabled": false}'::jsonb NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE public.email_templates_global (
id uuid DEFAULT gen_random_uuid() NOT NULL,
key text NOT NULL,
domain text NOT NULL,
channel text DEFAULT 'email'::text NOT NULL,
subject text NOT NULL,
body_html text NOT NULL,
body_text text,
version integer DEFAULT 1 NOT NULL,
is_active boolean DEFAULT true NOT NULL,
variables jsonb,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE public.email_templates_tenant (
id uuid DEFAULT gen_random_uuid() NOT NULL,
tenant_id uuid NOT NULL,
owner_id uuid,
template_key text NOT NULL,
subject text,
body_html text,
body_text text,
enabled boolean DEFAULT true NOT NULL,
synced_version integer,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE public.global_notices (
id uuid DEFAULT gen_random_uuid() NOT NULL,
title text,
message text DEFAULT ''::text NOT NULL,
variant text DEFAULT 'info'::text NOT NULL,
roles text[] DEFAULT '{}'::text[] NOT NULL,
contexts text[] DEFAULT '{}'::text[] NOT NULL,
starts_at timestamp with time zone,
ends_at timestamp with time zone,
is_active boolean DEFAULT true NOT NULL,
priority integer DEFAULT 0 NOT NULL,
dismissible boolean DEFAULT true NOT NULL,
persist_dismiss boolean DEFAULT true NOT NULL,
dismiss_scope text DEFAULT 'device'::text NOT NULL,
show_once boolean DEFAULT false NOT NULL,
max_views integer,
cooldown_minutes integer,
version integer DEFAULT 1 NOT NULL,
action_type text DEFAULT 'none'::text NOT NULL,
action_label text,
action_url text,
action_route text,
views_count integer DEFAULT 0 NOT NULL,
clicks_count integer DEFAULT 0 NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
created_by uuid,
content_align text DEFAULT 'left'::text NOT NULL,
link_target text DEFAULT '_blank'::text NOT NULL,
CONSTRAINT global_notices_action_type_check CHECK ((action_type = ANY (ARRAY['none'::text, 'internal'::text, 'external'::text]))),
CONSTRAINT global_notices_content_align_check CHECK ((content_align = ANY (ARRAY['left'::text, 'center'::text, 'right'::text, 'justify'::text]))),
CONSTRAINT global_notices_dismiss_scope_check CHECK ((dismiss_scope = ANY (ARRAY['session'::text, 'device'::text, 'user'::text]))),
CONSTRAINT global_notices_link_target_check CHECK ((link_target = ANY (ARRAY['_blank'::text, '_self'::text, '_parent'::text, '_top'::text]))),
CONSTRAINT global_notices_variant_check CHECK ((variant = ANY (ARRAY['info'::text, 'success'::text, 'warning'::text, 'error'::text])))
);
CREATE TABLE public.login_carousel_slides (
id uuid DEFAULT gen_random_uuid() NOT NULL,
title text NOT NULL,
body text NOT NULL,
icon text DEFAULT 'pi-star'::text NOT NULL,
ordem integer DEFAULT 0 NOT NULL,
ativo boolean DEFAULT true NOT NULL,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now()
);
CREATE TABLE public.notice_dismissals (
id uuid DEFAULT gen_random_uuid() NOT NULL,
notice_id uuid NOT NULL,
user_id uuid NOT NULL,
version integer DEFAULT 1 NOT NULL,
dismissed_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE public.notification_channels (
id uuid DEFAULT gen_random_uuid() NOT NULL,
tenant_id uuid NOT NULL,
owner_id uuid NOT NULL,
channel text NOT NULL,
provider text NOT NULL,
is_active boolean DEFAULT false NOT NULL,
display_name text,
sender_address text,
credentials jsonb DEFAULT '{}'::jsonb NOT NULL,
connection_status text DEFAULT 'disconnected'::text,
last_health_check timestamp with time zone,
metadata jsonb DEFAULT '{}'::jsonb,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
deleted_at timestamp with time zone,
twilio_subaccount_sid text,
twilio_phone_number text,
twilio_phone_sid text,
webhook_url text,
cost_per_message_usd numeric(8,6) DEFAULT 0,
price_per_message_brl numeric(8,4) DEFAULT 0,
provisioned_at timestamp with time zone,
CONSTRAINT notification_channels_channel_check CHECK ((channel = ANY (ARRAY['whatsapp'::text, 'email'::text, 'sms'::text]))),
CONSTRAINT notification_channels_connection_status_check CHECK ((connection_status = ANY (ARRAY['connected'::text, 'disconnected'::text, 'connecting'::text, 'qr_pending'::text, 'error'::text]))),
CONSTRAINT notification_channels_provider_check CHECK ((provider = ANY (ARRAY['evolution_api'::text, 'meta_official'::text, 'twilio'::text, 'zenvia'::text, 'sendgrid'::text, 'resend'::text, 'smtp'::text, 'zapi'::text])))
);
CREATE TABLE public.notification_logs (
id uuid DEFAULT gen_random_uuid() NOT NULL,
tenant_id uuid NOT NULL,
owner_id uuid NOT NULL,
queue_id uuid,
agenda_evento_id uuid,
patient_id uuid NOT NULL,
channel text NOT NULL,
template_key text NOT NULL,
schedule_key text,
recipient_address text NOT NULL,
resolved_message text,
resolved_vars jsonb,
status text NOT NULL,
provider text,
provider_message_id text,
provider_status text,
provider_response jsonb,
sent_at timestamp with time zone,
delivered_at timestamp with time zone,
read_at timestamp with time zone,
failed_at timestamp with time zone,
failure_reason text,
estimated_cost_brl numeric(8,4) DEFAULT 0,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT notification_logs_status_check CHECK ((status = ANY (ARRAY['sent'::text, 'delivered'::text, 'read'::text, 'failed'::text, 'bounced'::text, 'opted_out'::text])))
);
CREATE TABLE public.notification_preferences (
id uuid DEFAULT gen_random_uuid() NOT NULL,
tenant_id uuid NOT NULL,
owner_id uuid NOT NULL,
patient_id uuid NOT NULL,
whatsapp_opt_in boolean DEFAULT true NOT NULL,
email_opt_in boolean DEFAULT true NOT NULL,
sms_opt_in boolean DEFAULT false NOT NULL,
preferred_time_start time without time zone DEFAULT '08:00:00'::time without time zone,
preferred_time_end time without time zone DEFAULT '20:00:00'::time without time zone,
lgpd_consent_given boolean DEFAULT false NOT NULL,
lgpd_consent_date timestamp with time zone,
lgpd_consent_version text,
lgpd_consent_ip inet,
lgpd_opt_out_date timestamp with time zone,
lgpd_opt_out_reason text,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
deleted_at timestamp with time zone
);
CREATE TABLE public.notification_queue (
id uuid DEFAULT gen_random_uuid() NOT NULL,
tenant_id uuid NOT NULL,
owner_id uuid NOT NULL,
agenda_evento_id uuid,
patient_id uuid NOT NULL,
channel text NOT NULL,
template_key text NOT NULL,
schedule_key text NOT NULL,
resolved_vars jsonb DEFAULT '{}'::jsonb NOT NULL,
recipient_address text NOT NULL,
status text DEFAULT 'pendente'::text NOT NULL,
scheduled_at timestamp with time zone NOT NULL,
sent_at timestamp with time zone,
next_retry_at timestamp with time zone,
attempts integer DEFAULT 0 NOT NULL,
max_attempts integer DEFAULT 5 NOT NULL,
last_error text,
idempotency_key text NOT NULL,
provider_message_id text,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT notification_queue_channel_check CHECK ((channel = ANY (ARRAY['whatsapp'::text, 'email'::text, 'sms'::text]))),
CONSTRAINT notification_queue_status_check CHECK ((status = ANY (ARRAY['pendente'::text, 'processando'::text, 'enviado'::text, 'falhou'::text, 'cancelado'::text, 'ignorado'::text])))
);
CREATE TABLE public.notification_schedules (
id uuid DEFAULT gen_random_uuid() NOT NULL,
tenant_id uuid NOT NULL,
owner_id uuid NOT NULL,
schedule_key text NOT NULL,
event_type text NOT NULL,
trigger_type text NOT NULL,
offset_minutes integer DEFAULT 0,
whatsapp_enabled boolean DEFAULT true NOT NULL,
email_enabled boolean DEFAULT true NOT NULL,
sms_enabled boolean DEFAULT false NOT NULL,
allowed_time_start time without time zone DEFAULT '08:00:00'::time without time zone,
allowed_time_end time without time zone DEFAULT '20:00:00'::time without time zone,
skip_weekends boolean DEFAULT false,
skip_holidays boolean DEFAULT false,
is_active boolean DEFAULT true NOT NULL,
sort_order integer DEFAULT 0,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
deleted_at timestamp with time zone,
CONSTRAINT notification_schedules_event_type_check CHECK ((event_type = ANY (ARRAY['lembrete_sessao'::text, 'confirmacao_sessao'::text, 'cancelamento_sessao'::text, 'reagendamento'::text, 'cobranca_pendente'::text, 'boas_vindas_paciente'::text]))),
CONSTRAINT notification_schedules_trigger_type_check CHECK ((trigger_type = ANY (ARRAY['before_event'::text, 'after_event'::text, 'immediate'::text])))
);
CREATE TABLE public.notification_templates (
id uuid DEFAULT gen_random_uuid() NOT NULL,
tenant_id uuid,
owner_id uuid,
key text NOT NULL,
domain text NOT NULL,
channel text NOT NULL,
event_type text NOT NULL,
body_text text NOT NULL,
meta_template_name text,
meta_template_namespace text,
meta_components jsonb,
meta_status text DEFAULT 'draft'::text,
variables jsonb DEFAULT '[]'::jsonb,
version integer DEFAULT 1 NOT NULL,
is_active boolean DEFAULT true NOT NULL,
is_default boolean DEFAULT false NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
deleted_at timestamp with time zone,
CONSTRAINT notification_templates_channel_check CHECK ((channel = ANY (ARRAY['whatsapp'::text, 'sms'::text]))),
CONSTRAINT notification_templates_domain_check CHECK ((domain = ANY (ARRAY['session'::text, 'intake'::text, 'billing'::text, 'system'::text]))),
CONSTRAINT notification_templates_event_type_check CHECK ((event_type = ANY (ARRAY['lembrete_sessao'::text, 'confirmacao_sessao'::text, 'cancelamento_sessao'::text, 'reagendamento'::text, 'cobranca_pendente'::text, 'boas_vindas_paciente'::text, 'intake_recebido'::text, 'intake_aprovado'::text, 'intake_rejeitado'::text]))),
CONSTRAINT notification_templates_meta_status_check CHECK ((meta_status = ANY (ARRAY['draft'::text, 'pending_approval'::text, 'approved'::text, 'rejected'::text])))
);
CREATE TABLE public.notifications (
id uuid DEFAULT gen_random_uuid() NOT NULL,
owner_id uuid NOT NULL,
tenant_id uuid,
type text NOT NULL,
ref_id uuid,
ref_table text,
payload jsonb DEFAULT '{}'::jsonb NOT NULL,
read_at timestamp with time zone,
archived boolean DEFAULT false NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT notifications_type_check CHECK ((type = ANY (ARRAY['new_scheduling'::text, 'new_patient'::text, 'recurrence_alert'::text, 'session_status'::text])))
);
CREATE TABLE public.twilio_subaccount_usage (
id uuid DEFAULT gen_random_uuid() NOT NULL,
tenant_id uuid NOT NULL,
channel_id uuid NOT NULL,
twilio_subaccount_sid text NOT NULL,
period_start date NOT NULL,
period_end date NOT NULL,
messages_sent integer DEFAULT 0 NOT NULL,
messages_delivered integer DEFAULT 0 NOT NULL,
messages_failed integer DEFAULT 0 NOT NULL,
cost_usd numeric(12,6) DEFAULT 0 NOT NULL,
cost_brl numeric(12,4) DEFAULT 0 NOT NULL,
revenue_brl numeric(12,4) DEFAULT 0 NOT NULL,
margin_brl numeric(12,4) GENERATED ALWAYS AS ((revenue_brl - cost_brl)) STORED,
usd_brl_rate numeric(8,4) DEFAULT 0,
synced_at timestamp with time zone DEFAULT now(),
created_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT twilio_subaccount_usage_period_check CHECK ((period_end >= period_start))
);