Files
agenciapsilmno/database-novo/migrations/20260417000001_dev_tables.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

276 lines
14 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- =============================================================================
-- Migration: 20260417000001_dev_tables
-- Área de Desenvolvimento (dev_*) — roadmap, auditoria, concorrentes, logs
-- -----------------------------------------------------------------------------
-- Tabelas usadas pela página /saas/desenvolvimento. Todas restritas a
-- saas_admins via RLS (helper public.is_saas_admin()).
-- =============================================================================
-- -----------------------------------------------------------------------------
-- Helper trigger: updated_at
-- -----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.dev_set_updated_at()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at := now();
RETURN NEW;
END;
$$;
-- =============================================================================
-- 1. dev_roadmap_phases — Fases (1, 2, 3...)
-- =============================================================================
CREATE TABLE IF NOT EXISTS public.dev_roadmap_phases (
id BIGSERIAL PRIMARY KEY,
numero INTEGER NOT NULL UNIQUE,
nome VARCHAR(160) NOT NULL,
objetivo TEXT,
timeline_sugerida VARCHAR(160),
criterio_saida TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'planejada'
CHECK (status IN ('planejada','em_andamento','concluida','arquivada')),
data_inicio DATE,
data_fim DATE,
ordem INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_dev_roadmap_phases_status ON public.dev_roadmap_phases(status);
CREATE INDEX IF NOT EXISTS idx_dev_roadmap_phases_ordem ON public.dev_roadmap_phases(ordem);
DROP TRIGGER IF EXISTS trg_dev_roadmap_phases_updated_at ON public.dev_roadmap_phases;
CREATE TRIGGER trg_dev_roadmap_phases_updated_at
BEFORE UPDATE ON public.dev_roadmap_phases
FOR EACH ROW EXECUTE FUNCTION public.dev_set_updated_at();
-- =============================================================================
-- 2. dev_roadmap_items — Itens das fases
-- =============================================================================
CREATE TABLE IF NOT EXISTS public.dev_roadmap_items (
id BIGSERIAL PRIMARY KEY,
phase_id BIGINT NOT NULL REFERENCES public.dev_roadmap_phases(id) ON DELETE CASCADE,
numero INTEGER,
bloco VARCHAR(160),
feature TEXT NOT NULL,
descricao TEXT,
esforco VARCHAR(4)
CHECK (esforco IS NULL OR esforco IN ('S','M','L','XL')),
prioridade VARCHAR(20)
CHECK (prioridade IS NULL OR prioridade IN ('bloqueador','alta','media','diferencial')),
status VARCHAR(20) NOT NULL DEFAULT 'pendente'
CHECK (status IN ('pendente','em_andamento','concluido','cancelado','bloqueado')),
notas TEXT,
assignee VARCHAR(120),
data_inicio DATE,
data_conclusao DATE,
ordem INTEGER NOT NULL DEFAULT 0,
tags TEXT[] DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_dev_roadmap_items_phase ON public.dev_roadmap_items(phase_id);
CREATE INDEX IF NOT EXISTS idx_dev_roadmap_items_status ON public.dev_roadmap_items(status);
CREATE INDEX IF NOT EXISTS idx_dev_roadmap_items_prior ON public.dev_roadmap_items(prioridade);
CREATE INDEX IF NOT EXISTS idx_dev_roadmap_items_ordem ON public.dev_roadmap_items(phase_id, ordem);
DROP TRIGGER IF EXISTS trg_dev_roadmap_items_updated_at ON public.dev_roadmap_items;
CREATE TRIGGER trg_dev_roadmap_items_updated_at
BEFORE UPDATE ON public.dev_roadmap_items
FOR EACH ROW EXECUTE FUNCTION public.dev_set_updated_at();
-- =============================================================================
-- 3. dev_auditoria_items — Bugs / débitos técnicos / decisões
-- =============================================================================
CREATE TABLE IF NOT EXISTS public.dev_auditoria_items (
id BIGSERIAL PRIMARY KEY,
categoria VARCHAR(120),
titulo TEXT NOT NULL,
descricao_problema TEXT,
solucao TEXT,
severidade VARCHAR(20)
CHECK (severidade IS NULL OR severidade IN ('critico','alto','medio','baixo')),
status VARCHAR(20) NOT NULL DEFAULT 'aberto'
CHECK (status IN ('aberto','em_analise','resolvido','wontfix','duplicado')),
resolvido_em DATE,
sessao_resolucao VARCHAR(160),
arquivo_afetado TEXT,
tags TEXT[] DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_dev_auditoria_items_status ON public.dev_auditoria_items(status);
CREATE INDEX IF NOT EXISTS idx_dev_auditoria_items_severidade ON public.dev_auditoria_items(severidade);
CREATE INDEX IF NOT EXISTS idx_dev_auditoria_items_categoria ON public.dev_auditoria_items(categoria);
DROP TRIGGER IF EXISTS trg_dev_auditoria_items_updated_at ON public.dev_auditoria_items;
CREATE TRIGGER trg_dev_auditoria_items_updated_at
BEFORE UPDATE ON public.dev_auditoria_items
FOR EACH ROW EXECUTE FUNCTION public.dev_set_updated_at();
-- =============================================================================
-- 4. dev_competitors — Concorrentes
-- =============================================================================
CREATE TABLE IF NOT EXISTS public.dev_competitors (
id BIGSERIAL PRIMARY KEY,
slug VARCHAR(80) NOT NULL UNIQUE,
nome VARCHAR(160) NOT NULL,
pais VARCHAR(40),
foco VARCHAR(160),
pricing TEXT,
posicionamento TEXT,
url TEXT,
ultima_pesquisa DATE,
notas TEXT,
ativo BOOLEAN NOT NULL DEFAULT true,
ordem INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_dev_competitors_ativo ON public.dev_competitors(ativo);
CREATE INDEX IF NOT EXISTS idx_dev_competitors_pais ON public.dev_competitors(pais);
DROP TRIGGER IF EXISTS trg_dev_competitors_updated_at ON public.dev_competitors;
CREATE TRIGGER trg_dev_competitors_updated_at
BEFORE UPDATE ON public.dev_competitors
FOR EACH ROW EXECUTE FUNCTION public.dev_set_updated_at();
-- =============================================================================
-- 5. dev_competitor_features — features de cada concorrente
-- =============================================================================
CREATE TABLE IF NOT EXISTS public.dev_competitor_features (
id BIGSERIAL PRIMARY KEY,
competitor_id BIGINT NOT NULL REFERENCES public.dev_competitors(id) ON DELETE CASCADE,
categoria VARCHAR(120),
nome TEXT NOT NULL,
descricao TEXT,
fonte VARCHAR(20) NOT NULL DEFAULT 'publico'
CHECK (fonte IN ('fetched','observacao','publico','hipotese')),
fonte_url TEXT,
data_fonte DATE,
destaque BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_dev_competitor_features_comp ON public.dev_competitor_features(competitor_id);
CREATE INDEX IF NOT EXISTS idx_dev_competitor_features_cat ON public.dev_competitor_features(categoria);
CREATE INDEX IF NOT EXISTS idx_dev_competitor_features_destaque ON public.dev_competitor_features(destaque);
DROP TRIGGER IF EXISTS trg_dev_competitor_features_updated_at ON public.dev_competitor_features;
CREATE TRIGGER trg_dev_competitor_features_updated_at
BEFORE UPDATE ON public.dev_competitor_features
FOR EACH ROW EXECUTE FUNCTION public.dev_set_updated_at();
-- =============================================================================
-- 6. dev_comparison_matrix — AgenciaPsi × features-de-concorrente
-- =============================================================================
CREATE TABLE IF NOT EXISTS public.dev_comparison_matrix (
id BIGSERIAL PRIMARY KEY,
dominio VARCHAR(120),
feature TEXT NOT NULL,
nosso_status VARCHAR(20) NOT NULL DEFAULT 'a_definir'
CHECK (nosso_status IN ('tem','parcial','gap','na','a_definir')),
nossa_nota TEXT,
importancia VARCHAR(20)
CHECK (importancia IS NULL OR importancia IN ('alta','media','baixa')),
ordem INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_dev_comparison_matrix_dominio ON public.dev_comparison_matrix(dominio);
CREATE INDEX IF NOT EXISTS idx_dev_comparison_matrix_status ON public.dev_comparison_matrix(nosso_status);
DROP TRIGGER IF EXISTS trg_dev_comparison_matrix_updated_at ON public.dev_comparison_matrix;
CREATE TRIGGER trg_dev_comparison_matrix_updated_at
BEFORE UPDATE ON public.dev_comparison_matrix
FOR EACH ROW EXECUTE FUNCTION public.dev_set_updated_at();
-- dev_comparison_competitor_status — opcional: status por concorrente por feature
-- (se quisermos marcar que competitor X tem feature Y). Tabela ponte N-N.
CREATE TABLE IF NOT EXISTS public.dev_comparison_competitor_status (
id BIGSERIAL PRIMARY KEY,
comparison_id BIGINT NOT NULL REFERENCES public.dev_comparison_matrix(id) ON DELETE CASCADE,
competitor_id BIGINT NOT NULL REFERENCES public.dev_competitors(id) ON DELETE CASCADE,
status VARCHAR(20) NOT NULL DEFAULT 'a_definir'
CHECK (status IN ('tem','parcial','gap','na','a_definir')),
nota TEXT,
fonte VARCHAR(20)
CHECK (fonte IS NULL OR fonte IN ('fetched','observacao','publico','hipotese')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (comparison_id, competitor_id)
);
CREATE INDEX IF NOT EXISTS idx_dev_ccs_comp ON public.dev_comparison_competitor_status(competitor_id);
CREATE INDEX IF NOT EXISTS idx_dev_ccs_comparison ON public.dev_comparison_competitor_status(comparison_id);
DROP TRIGGER IF EXISTS trg_dev_ccs_updated_at ON public.dev_comparison_competitor_status;
CREATE TRIGGER trg_dev_ccs_updated_at
BEFORE UPDATE ON public.dev_comparison_competitor_status
FOR EACH ROW EXECUTE FUNCTION public.dev_set_updated_at();
-- =============================================================================
-- 7. dev_generation_log — histórico de execuções (backup, dashboard, export...)
-- =============================================================================
CREATE TABLE IF NOT EXISTS public.dev_generation_log (
id BIGSERIAL PRIMARY KEY,
tipo VARCHAR(40) NOT NULL,
comando TEXT,
sucesso BOOLEAN NOT NULL DEFAULT false,
stdout TEXT,
stderr TEXT,
duration_ms INTEGER,
metadata JSONB DEFAULT '{}'::jsonb,
trigger_user_id UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_dev_generation_log_tipo ON public.dev_generation_log(tipo);
CREATE INDEX IF NOT EXISTS idx_dev_generation_log_created ON public.dev_generation_log(created_at DESC);
-- =============================================================================
-- RLS — tudo restrito a saas_admins (helper existente: public.is_saas_admin())
-- =============================================================================
DO $$
DECLARE
t TEXT;
dev_tables TEXT[] := ARRAY[
'dev_roadmap_phases',
'dev_roadmap_items',
'dev_auditoria_items',
'dev_competitors',
'dev_competitor_features',
'dev_comparison_matrix',
'dev_comparison_competitor_status',
'dev_generation_log'
];
BEGIN
FOREACH t IN ARRAY dev_tables
LOOP
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY;', t);
-- Drop policy se existir (idempotente)
EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I;', t || '_saas_admin_all', t);
-- Cria policy que permite tudo pra saas_admin
EXECUTE format(
'CREATE POLICY %I ON public.%I FOR ALL TO authenticated
USING (public.is_saas_admin())
WITH CHECK (public.is_saas_admin());',
t || '_saas_admin_all',
t
);
END LOOP;
END $$;
-- =============================================================================
-- Comentários
-- =============================================================================
COMMENT ON TABLE public.dev_roadmap_phases IS 'Fases do roadmap (MVP, Paridade, Diferenciação). Visível só pra saas_admins.';
COMMENT ON TABLE public.dev_roadmap_items IS 'Itens de cada fase do roadmap.';
COMMENT ON TABLE public.dev_auditoria_items IS 'Bugs, dívidas técnicas e decisões arquiteturais.';
COMMENT ON TABLE public.dev_competitors IS 'Concorrentes analisados no benchmark.';
COMMENT ON TABLE public.dev_competitor_features IS 'Features catalogadas de cada concorrente.';
COMMENT ON TABLE public.dev_comparison_matrix IS 'Matriz de comparação AgenciaPsi × features esperadas do mercado.';
COMMENT ON TABLE public.dev_comparison_competitor_status IS 'Qual concorrente tem qual feature (ponte N-N com matrix).';
COMMENT ON TABLE public.dev_generation_log IS 'Histórico de execuções (backup, dashboard, export, seed, etc).';