first commit

This commit is contained in:
2026-08-14 17:35:24 -03:00
commit 04472fcd2b
118 changed files with 19173 additions and 0 deletions
@@ -0,0 +1,451 @@
-- CreateEnum
CREATE TYPE "tenant_status" AS ENUM ('trial', 'ativo', 'inadimplente', 'suspenso', 'cancelado');
-- CreateEnum
CREATE TYPE "cobranca_status" AS ENUM ('pendente', 'pago', 'atrasado', 'cancelado');
-- CreateEnum
CREATE TYPE "user_role" AS ENUM ('tenant_admin', 'vendedor', 'financeiro', 'visualizador');
-- CreateEnum
CREATE TYPE "item_tipo" AS ENUM ('produto', 'servico');
-- CreateEnum
CREATE TYPE "venda_status" AS ENUM ('orcamento', 'confirmada', 'cancelada', 'concluida');
-- CreateEnum
CREATE TYPE "parcela_status" AS ENUM ('pendente', 'pago', 'atrasado', 'cancelado');
-- CreateTable
CREATE TABLE "master_admins" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"nome" VARCHAR(150) NOT NULL,
"email" VARCHAR(150) NOT NULL,
"senha_hash" TEXT NOT NULL,
"ativo" BOOLEAN NOT NULL DEFAULT true,
"mfa_secret" TEXT,
"ultimo_login" TIMESTAMPTZ(6),
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"atualizado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "master_admins_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "evolution_api_config" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"nome" VARCHAR(100) NOT NULL,
"base_url" VARCHAR(255) NOT NULL,
"api_key_cripto" TEXT NOT NULL,
"versao" VARCHAR(20) NOT NULL DEFAULT '2.3',
"ativo" BOOLEAN NOT NULL DEFAULT true,
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "evolution_api_config_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "master_email_config" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"smtp_host" VARCHAR(150) NOT NULL,
"smtp_porta" INTEGER NOT NULL,
"smtp_usuario" VARCHAR(150) NOT NULL,
"smtp_senha_cripto" TEXT NOT NULL,
"email_remetente" VARCHAR(150) NOT NULL,
"ativo" BOOLEAN NOT NULL DEFAULT true,
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "master_email_config_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "planos" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"nome" VARCHAR(100) NOT NULL,
"descricao" TEXT,
"valor_mensal" DECIMAL(10,2) NOT NULL,
"limite_customers" INTEGER,
"limite_usuarios" INTEGER,
"limite_instancias_whatsapp" INTEGER NOT NULL DEFAULT 1,
"ativo" BOOLEAN NOT NULL DEFAULT true,
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "planos_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tenants" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"razao_social" VARCHAR(200) NOT NULL,
"nome_fantasia" VARCHAR(200),
"documento" VARCHAR(20),
"email_contato" VARCHAR(150) NOT NULL,
"telefone" VARCHAR(20),
"logo_url" TEXT,
"cor_primaria" VARCHAR(7),
"plano_id" UUID,
"status" "tenant_status" NOT NULL DEFAULT 'trial',
"dia_vencimento_mensalidade" SMALLINT NOT NULL DEFAULT 10,
"trial_expira_em" TIMESTAMPTZ(6),
"criado_por" UUID,
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"atualizado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "tenants_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tenant_faturas" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tenant_id" UUID NOT NULL,
"referencia" VARCHAR(7) NOT NULL,
"valor" DECIMAL(10,2) NOT NULL,
"vencimento" DATE NOT NULL,
"pago_em" TIMESTAMPTZ(6),
"status" "cobranca_status" NOT NULL DEFAULT 'pendente',
"alerta_enviado_em" TIMESTAMPTZ(6),
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "tenant_faturas_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tenant_users" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tenant_id" UUID NOT NULL,
"nome" VARCHAR(150) NOT NULL,
"email" VARCHAR(150) NOT NULL,
"senha_hash" TEXT NOT NULL,
"role" "user_role" NOT NULL DEFAULT 'vendedor',
"ativo" BOOLEAN NOT NULL DEFAULT true,
"mfa_secret" TEXT,
"ultimo_login" TIMESTAMPTZ(6),
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "tenant_users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "auth_refresh_tokens" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"usuario_tipo" VARCHAR(20) NOT NULL,
"usuario_id" UUID NOT NULL,
"token_hash" TEXT NOT NULL,
"expira_em" TIMESTAMPTZ(6) NOT NULL,
"revogado" BOOLEAN NOT NULL DEFAULT false,
"ip_origem" INET,
"user_agent" TEXT,
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "auth_refresh_tokens_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "password_reset_tokens" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"usuario_tipo" VARCHAR(20) NOT NULL,
"usuario_id" UUID NOT NULL,
"token_hash" TEXT NOT NULL,
"expira_em" TIMESTAMPTZ(6) NOT NULL,
"usado" BOOLEAN NOT NULL DEFAULT false,
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "password_reset_tokens_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "login_attempts" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"email" VARCHAR(150) NOT NULL,
"usuario_tipo" VARCHAR(20) NOT NULL,
"sucesso" BOOLEAN NOT NULL,
"ip_origem" INET,
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "login_attempts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tenant_whatsapp_instances" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tenant_id" UUID NOT NULL,
"evolution_config_id" UUID,
"instance_name" VARCHAR(100) NOT NULL,
"numero_conectado" VARCHAR(20),
"status" VARCHAR(30) NOT NULL DEFAULT 'desconectado',
"qr_code_gerado_em" TIMESTAMPTZ(6),
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "tenant_whatsapp_instances_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tenant_email_config" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tenant_id" UUID NOT NULL,
"smtp_host" VARCHAR(150),
"smtp_porta" INTEGER,
"smtp_usuario" VARCHAR(150),
"smtp_senha_cripto" TEXT,
"email_remetente" VARCHAR(150),
"usar_config_padrao" BOOLEAN NOT NULL DEFAULT true,
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "tenant_email_config_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "alert_templates" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tenant_id" UUID,
"tipo" VARCHAR(50) NOT NULL,
"canal" VARCHAR(20) NOT NULL,
"assunto" VARCHAR(200),
"corpo" TEXT NOT NULL,
"ativo" BOOLEAN NOT NULL DEFAULT true,
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "alert_templates_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "alert_logs" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tenant_id" UUID,
"destinatario_tipo" VARCHAR(20) NOT NULL,
"destinatario_id" UUID NOT NULL,
"canal" VARCHAR(20) NOT NULL,
"template_id" UUID,
"conteudo_enviado" TEXT,
"status" VARCHAR(20) NOT NULL DEFAULT 'enviado',
"erro" TEXT,
"enviado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "alert_logs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "customers" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tenant_id" UUID NOT NULL,
"razao_social" VARCHAR(200) NOT NULL,
"nome_fantasia" VARCHAR(200),
"documento" VARCHAR(20),
"email" VARCHAR(150),
"telefone_whatsapp" VARCHAR(20),
"endereco" TEXT,
"cidade" VARCHAR(100),
"uf" CHAR(2),
"observacoes" TEXT,
"ativo" BOOLEAN NOT NULL DEFAULT true,
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"atualizado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "customers_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "customer_contacts" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"customer_id" UUID NOT NULL,
"nome" VARCHAR(150) NOT NULL,
"cargo" VARCHAR(100),
"email" VARCHAR(150),
"telefone_whatsapp" VARCHAR(20),
"principal" BOOLEAN NOT NULL DEFAULT false,
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "customer_contacts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "products_services" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tenant_id" UUID NOT NULL,
"tipo" "item_tipo" NOT NULL,
"nome" VARCHAR(200) NOT NULL,
"descricao" TEXT,
"sku" VARCHAR(50),
"valor_venda" DECIMAL(12,2) NOT NULL,
"valor_custo" DECIMAL(12,2),
"unidade" VARCHAR(20) NOT NULL DEFAULT 'un',
"recorrente" BOOLEAN NOT NULL DEFAULT false,
"ativo" BOOLEAN NOT NULL DEFAULT true,
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"atualizado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "products_services_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sales" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tenant_id" UUID NOT NULL,
"customer_id" UUID NOT NULL,
"vendedor_id" UUID,
"numero_venda" SERIAL NOT NULL,
"status" "venda_status" NOT NULL DEFAULT 'confirmada',
"valor_total" DECIMAL(12,2) NOT NULL DEFAULT 0,
"desconto" DECIMAL(12,2) NOT NULL DEFAULT 0,
"observacoes" TEXT,
"data_venda" DATE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"atualizado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "sales_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sale_items" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"sale_id" UUID NOT NULL,
"product_service_id" UUID NOT NULL,
"quantidade" DECIMAL(10,2) NOT NULL DEFAULT 1,
"valor_unitario" DECIMAL(12,2) NOT NULL,
"valor_total" DECIMAL(12,2) NOT NULL,
CONSTRAINT "sale_items_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sale_payments" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"sale_id" UUID NOT NULL,
"numero_parcela" SMALLINT NOT NULL DEFAULT 1,
"valor" DECIMAL(12,2) NOT NULL,
"vencimento" DATE NOT NULL,
"pago_em" TIMESTAMPTZ(6),
"forma_pagamento" VARCHAR(30),
"status" "parcela_status" NOT NULL DEFAULT 'pendente',
"alerta_enviado_em" TIMESTAMPTZ(6),
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "sale_payments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "audit_logs" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tenant_id" UUID,
"usuario_tipo" VARCHAR(20) NOT NULL,
"usuario_id" UUID,
"acao" VARCHAR(100) NOT NULL,
"entidade" VARCHAR(50),
"entidade_id" UUID,
"detalhes" JSONB,
"ip_origem" INET,
"criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "master_admins_email_key" ON "master_admins"("email");
-- CreateIndex
CREATE INDEX "idx_tenants_status" ON "tenants"("status");
-- CreateIndex
CREATE UNIQUE INDEX "tenant_faturas_tenant_id_referencia_key" ON "tenant_faturas"("tenant_id", "referencia");
-- CreateIndex
CREATE INDEX "idx_tenant_users_tenant" ON "tenant_users"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "tenant_users_tenant_id_email_key" ON "tenant_users"("tenant_id", "email");
-- CreateIndex
CREATE INDEX "idx_refresh_tokens_usuario" ON "auth_refresh_tokens"("usuario_tipo", "usuario_id");
-- CreateIndex
CREATE INDEX "idx_login_attempts_email_data" ON "login_attempts"("email", "criado_em");
-- CreateIndex
CREATE UNIQUE INDEX "tenant_whatsapp_instances_instance_name_key" ON "tenant_whatsapp_instances"("instance_name");
-- CreateIndex
CREATE UNIQUE INDEX "tenant_email_config_tenant_id_key" ON "tenant_email_config"("tenant_id");
-- CreateIndex
CREATE INDEX "idx_alert_logs_tenant" ON "alert_logs"("tenant_id", "enviado_em");
-- CreateIndex
CREATE INDEX "idx_customers_tenant" ON "customers"("tenant_id");
-- CreateIndex
CREATE INDEX "idx_products_tenant" ON "products_services"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "products_services_tenant_id_sku_key" ON "products_services"("tenant_id", "sku");
-- CreateIndex
CREATE INDEX "idx_sales_tenant_data" ON "sales"("tenant_id", "data_venda");
-- CreateIndex
CREATE INDEX "idx_sales_customer" ON "sales"("customer_id");
-- CreateIndex
CREATE INDEX "idx_sale_items_sale" ON "sale_items"("sale_id");
-- CreateIndex
CREATE INDEX "idx_audit_tenant_data" ON "audit_logs"("tenant_id", "criado_em");
-- AddForeignKey
ALTER TABLE "tenants" ADD CONSTRAINT "tenants_plano_id_fkey" FOREIGN KEY ("plano_id") REFERENCES "planos"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenants" ADD CONSTRAINT "tenants_criado_por_fkey" FOREIGN KEY ("criado_por") REFERENCES "master_admins"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_faturas" ADD CONSTRAINT "tenant_faturas_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_users" ADD CONSTRAINT "tenant_users_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_whatsapp_instances" ADD CONSTRAINT "tenant_whatsapp_instances_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_whatsapp_instances" ADD CONSTRAINT "tenant_whatsapp_instances_evolution_config_id_fkey" FOREIGN KEY ("evolution_config_id") REFERENCES "evolution_api_config"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_email_config" ADD CONSTRAINT "tenant_email_config_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "alert_templates" ADD CONSTRAINT "alert_templates_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "alert_logs" ADD CONSTRAINT "alert_logs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "alert_logs" ADD CONSTRAINT "alert_logs_template_id_fkey" FOREIGN KEY ("template_id") REFERENCES "alert_templates"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "customers" ADD CONSTRAINT "customers_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "customer_contacts" ADD CONSTRAINT "customer_contacts_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "products_services" ADD CONSTRAINT "products_services_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sales" ADD CONSTRAINT "sales_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sales" ADD CONSTRAINT "sales_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sales" ADD CONSTRAINT "sales_vendedor_id_fkey" FOREIGN KEY ("vendedor_id") REFERENCES "tenant_users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sale_items" ADD CONSTRAINT "sale_items_sale_id_fkey" FOREIGN KEY ("sale_id") REFERENCES "sales"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sale_items" ADD CONSTRAINT "sale_items_product_service_id_fkey" FOREIGN KEY ("product_service_id") REFERENCES "products_services"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sale_payments" ADD CONSTRAINT "sale_payments_sale_id_fkey" FOREIGN KEY ("sale_id") REFERENCES "sales"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,119 @@
-- Migration manual: construtos que o Prisma DSL não expressa nativamente.
-- Fonte de verdade: Etapas/seconectai-vendas-schema.sql
-- Contém: extensão pgcrypto, CHECK constraint, índices parciais, views de
-- dashboard e Row Level Security (ENABLE + CREATE POLICY) para isolamento
-- entre tenants.
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- ============================================================
-- CHECK constraints
-- ============================================================
ALTER TABLE "tenants"
ADD CONSTRAINT "tenants_dia_vencimento_mensalidade_check"
CHECK ("dia_vencimento_mensalidade" BETWEEN 1 AND 28);
-- ============================================================
-- Índices parciais
-- ============================================================
CREATE INDEX "idx_tenant_faturas_venc"
ON "tenant_faturas"("vencimento")
WHERE "status" IN ('pendente', 'atrasado');
CREATE INDEX "idx_sale_payments_venc"
ON "sale_payments"("vencimento")
WHERE "status" IN ('pendente', 'atrasado');
-- ============================================================
-- Views de dashboard (painel do tenant)
-- ============================================================
CREATE VIEW "vw_resumo_vendas_semanal" AS
SELECT
s.tenant_id,
date_trunc('week', s.data_venda)::date AS semana,
COUNT(*) AS qtd_vendas,
SUM(s.valor_total) AS total_vendido
FROM sales s
WHERE s.status IN ('confirmada', 'concluida')
GROUP BY s.tenant_id, date_trunc('week', s.data_venda);
CREATE VIEW "vw_resumo_vendas_mensal" AS
SELECT
s.tenant_id,
date_trunc('month', s.data_venda)::date AS mes,
COUNT(*) AS qtd_vendas,
SUM(s.valor_total) AS total_vendido
FROM sales s
WHERE s.status IN ('confirmada', 'concluida')
GROUP BY s.tenant_id, date_trunc('month', s.data_venda);
CREATE VIEW "vw_recebimentos" AS
SELECT
sp.id,
s.tenant_id,
s.customer_id,
sp.valor,
sp.vencimento,
sp.status,
sp.pago_em
FROM sale_payments sp
JOIN sales s ON s.id = sp.sale_id;
-- ============================================================
-- Row Level Security — isolamento entre tenants
-- ============================================================
-- A aplicação deve definir, por conexão/transação:
-- SET app.current_tenant_id = '<uuid-do-tenant-logado>';
-- Usuários master (que enxergam todos os tenants) usam uma role de banco
-- separada (ex: app_master) que faz BYPASS RLS, ou uma policy adicional
-- que libera quando app.is_master = 'true'. Implementado na Etapa 2.
ALTER TABLE "customers" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "customer_contacts" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "products_services" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "sales" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "sale_items" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "sale_payments" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "tenant_users" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "tenant_whatsapp_instances" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "tenant_email_config" ENABLE ROW LEVEL SECURITY;
CREATE POLICY "tenant_isolation_customers" ON "customers"
USING ("tenant_id" = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY "tenant_isolation_products" ON "products_services"
USING ("tenant_id" = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY "tenant_isolation_sales" ON "sales"
USING ("tenant_id" = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY "tenant_isolation_tenant_users" ON "tenant_users"
USING ("tenant_id" = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY "tenant_isolation_whatsapp" ON "tenant_whatsapp_instances"
USING ("tenant_id" = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY "tenant_isolation_email_config" ON "tenant_email_config"
USING ("tenant_id" = current_setting('app.current_tenant_id', true)::uuid);
-- sale_items e customer_contacts não têm tenant_id direto: isolar via join
CREATE POLICY "tenant_isolation_sale_items" ON "sale_items"
USING ("sale_id" IN (
SELECT id FROM sales
WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid
));
CREATE POLICY "tenant_isolation_sale_payments" ON "sale_payments"
USING ("sale_id" IN (
SELECT id FROM sales
WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid
));
CREATE POLICY "tenant_isolation_customer_contacts" ON "customer_contacts"
USING ("customer_id" IN (
SELECT id FROM customers
WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid
));
@@ -0,0 +1,53 @@
-- Migration manual: roles de aplicação usadas pela Etapa 2 (Autenticação/RLS).
--
-- Por que duas roles novas em vez de reaproveitar a role do DATABASE_URL
-- (`seconectai`, dono das tabelas): a role criada pelo `POSTGRES_USER` do
-- docker-compose é a role bootstrap do initdb, portanto é SUPERUSER — e
-- superusers do Postgres **sempre** ignoram Row Level Security, mesmo com
-- `ENABLE ROW LEVEL SECURITY`/`FORCE ROW LEVEL SECURITY` nas tabelas. Se a
-- aplicação continuasse consultando o banco com essa role em runtime, o
-- isolamento por tenant seria só decorativo. Por isso:
--
-- * `app_master` — LOGIN + BYPASSRLS, mas SEM ser superuser. Usada via
-- `DATABASE_URL_MASTER` pelo painel master (enxerga todos os tenants),
-- como pedido no prompt da etapa.
-- * `app_tenant` — LOGIN comum, SEM BYPASSRLS e SEM ser superuser. Usada
-- via `DATABASE_URL_TENANT` para toda query autenticada como
-- tenant_user, dentro da transação que executa
-- `SET LOCAL app.current_tenant_id`; é essa role que faz as policies de
-- RLS (`prisma/migrations/20260812161100_rls_views_constraints`) valerem
-- de fato. (Desvio registrado em Etapas/09-controle-de-progresso.md: o
-- prompt original só citava `DATABASE_URL_MASTER`.)
--
-- `DATABASE_URL` (role `seconectai`) continua sendo usada só para
-- migrations e para o `PrismaService` "admin" (health check, bootstrap de
-- login antes de existir um tenant_id de sessão — ver AuthService).
--
-- As senhas abaixo são placeholders de desenvolvimento, no mesmo espírito
-- de `POSTGRES_PASSWORD`/`JWT_SECRET` em `.env.example`: troque-as em
-- produção com `ALTER ROLE ... WITH PASSWORD '...'` e atualize
-- `DATABASE_URL_MASTER`/`DATABASE_URL_TENANT` de acordo.
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'app_master') THEN
CREATE ROLE app_master WITH LOGIN PASSWORD 'app_master_changeme' NOSUPERUSER BYPASSRLS;
END IF;
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'app_tenant') THEN
CREATE ROLE app_tenant WITH LOGIN PASSWORD 'app_tenant_changeme' NOSUPERUSER NOBYPASSRLS;
END IF;
END
$$;
GRANT USAGE ON SCHEMA public TO app_master, app_tenant;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_master, app_tenant;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_master, app_tenant;
-- Cobre tabelas/sequences criadas por migrations futuras (etapas 3+),
-- desde que aplicadas com a mesma role que rodou esta migration.
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_master, app_tenant;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO app_master, app_tenant;
+3
View File
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
+438
View File
@@ -0,0 +1,438 @@
// SeConectai Vendas — Prisma schema
// Traduzido 1:1 a partir de `Etapas/seconectai-vendas-schema.sql` (fonte de verdade).
// Constructos que o Prisma DSL não expressa nativamente (views, índices parciais,
// CHECK constraints e as policies de RLS) são criados por uma migration manual
// em `prisma/migrations/<timestamp>_rls_views_constraints/migration.sql`.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ============================================================
// ENUMS
// ============================================================
enum tenant_status {
trial
ativo
inadimplente
suspenso
cancelado
}
enum cobranca_status {
pendente
pago
atrasado
cancelado
}
enum user_role {
tenant_admin
vendedor
financeiro
visualizador
}
enum item_tipo {
produto
servico
}
enum venda_status {
orcamento
confirmada
cancelada
concluida
}
enum parcela_status {
pendente
pago
atrasado
cancelado
}
// ============================================================
// 1. NÍVEL MASTER
// ============================================================
model master_admins {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
nome String @db.VarChar(150)
email String @unique @db.VarChar(150)
senha_hash String
ativo Boolean @default(true)
mfa_secret String?
ultimo_login DateTime? @db.Timestamptz(6)
criado_em DateTime @default(now()) @db.Timestamptz(6)
atualizado_em DateTime @default(now()) @db.Timestamptz(6)
tenants_criados tenants[]
}
model evolution_api_config {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
nome String @db.VarChar(100)
base_url String @db.VarChar(255)
api_key_cripto String
versao String @default("2.3") @db.VarChar(20)
ativo Boolean @default(true)
criado_em DateTime @default(now()) @db.Timestamptz(6)
instancias tenant_whatsapp_instances[]
}
model master_email_config {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
smtp_host String @db.VarChar(150)
smtp_porta Int
smtp_usuario String @db.VarChar(150)
smtp_senha_cripto String
email_remetente String @db.VarChar(150)
ativo Boolean @default(true)
criado_em DateTime @default(now()) @db.Timestamptz(6)
}
model planos {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
nome String @db.VarChar(100)
descricao String?
valor_mensal Decimal @db.Decimal(10, 2)
limite_customers Int?
limite_usuarios Int?
limite_instancias_whatsapp Int @default(1)
ativo Boolean @default(true)
criado_em DateTime @default(now()) @db.Timestamptz(6)
tenants tenants[]
}
// ============================================================
// 2. TENANTS
// ============================================================
model tenants {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
razao_social String @db.VarChar(200)
nome_fantasia String? @db.VarChar(200)
documento String? @db.VarChar(20)
email_contato String @db.VarChar(150)
telefone String? @db.VarChar(20)
logo_url String?
cor_primaria String? @db.VarChar(7)
plano_id String? @db.Uuid
status tenant_status @default(trial)
dia_vencimento_mensalidade Int @default(10) @db.SmallInt
trial_expira_em DateTime? @db.Timestamptz(6)
criado_por String? @db.Uuid
criado_em DateTime @default(now()) @db.Timestamptz(6)
atualizado_em DateTime @default(now()) @db.Timestamptz(6)
plano planos? @relation(fields: [plano_id], references: [id])
criador master_admins? @relation(fields: [criado_por], references: [id])
faturas tenant_faturas[]
usuarios tenant_users[]
whatsapp_instances tenant_whatsapp_instances[]
email_config tenant_email_config?
alert_templates alert_templates[]
alert_logs alert_logs[]
customers customers[]
produtos products_services[]
vendas sales[]
audit_logs audit_logs[]
@@index([status], map: "idx_tenants_status")
}
model tenant_faturas {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
tenant_id String @db.Uuid
referencia String @db.VarChar(7)
valor Decimal @db.Decimal(10, 2)
vencimento DateTime @db.Date
pago_em DateTime? @db.Timestamptz(6)
status cobranca_status @default(pendente)
alerta_enviado_em DateTime? @db.Timestamptz(6)
criado_em DateTime @default(now()) @db.Timestamptz(6)
tenant tenants @relation(fields: [tenant_id], references: [id], onDelete: Cascade)
@@unique([tenant_id, referencia])
}
// ============================================================
// 3. USUÁRIOS E AUTENTICAÇÃO
// ============================================================
model tenant_users {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
tenant_id String @db.Uuid
nome String @db.VarChar(150)
email String @db.VarChar(150)
senha_hash String
role user_role @default(vendedor)
ativo Boolean @default(true)
mfa_secret String?
ultimo_login DateTime? @db.Timestamptz(6)
criado_em DateTime @default(now()) @db.Timestamptz(6)
tenant tenants @relation(fields: [tenant_id], references: [id], onDelete: Cascade)
vendas sales[]
@@unique([tenant_id, email])
@@index([tenant_id], map: "idx_tenant_users_tenant")
}
model auth_refresh_tokens {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
usuario_tipo String @db.VarChar(20)
usuario_id String @db.Uuid
token_hash String
expira_em DateTime @db.Timestamptz(6)
revogado Boolean @default(false)
ip_origem String? @db.Inet
user_agent String?
criado_em DateTime @default(now()) @db.Timestamptz(6)
@@index([usuario_tipo, usuario_id], map: "idx_refresh_tokens_usuario")
}
model password_reset_tokens {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
usuario_tipo String @db.VarChar(20)
usuario_id String @db.Uuid
token_hash String
expira_em DateTime @db.Timestamptz(6)
usado Boolean @default(false)
criado_em DateTime @default(now()) @db.Timestamptz(6)
}
model login_attempts {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
email String @db.VarChar(150)
usuario_tipo String @db.VarChar(20)
sucesso Boolean
ip_origem String? @db.Inet
criado_em DateTime @default(now()) @db.Timestamptz(6)
@@index([email, criado_em], map: "idx_login_attempts_email_data")
}
// ============================================================
// 4. INTEGRAÇÕES POR TENANT
// ============================================================
model tenant_whatsapp_instances {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
tenant_id String @db.Uuid
evolution_config_id String? @db.Uuid
instance_name String @unique @db.VarChar(100)
numero_conectado String? @db.VarChar(20)
status String @default("desconectado") @db.VarChar(30)
qr_code_gerado_em DateTime? @db.Timestamptz(6)
criado_em DateTime @default(now()) @db.Timestamptz(6)
tenant tenants @relation(fields: [tenant_id], references: [id], onDelete: Cascade)
evolution_config evolution_api_config? @relation(fields: [evolution_config_id], references: [id])
}
model tenant_email_config {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
tenant_id String @unique @db.Uuid
smtp_host String? @db.VarChar(150)
smtp_porta Int?
smtp_usuario String? @db.VarChar(150)
smtp_senha_cripto String?
email_remetente String? @db.VarChar(150)
usar_config_padrao Boolean @default(true)
criado_em DateTime @default(now()) @db.Timestamptz(6)
tenant tenants @relation(fields: [tenant_id], references: [id], onDelete: Cascade)
}
model alert_templates {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
tenant_id String? @db.Uuid
tipo String @db.VarChar(50)
canal String @db.VarChar(20)
assunto String? @db.VarChar(200)
corpo String
ativo Boolean @default(true)
criado_em DateTime @default(now()) @db.Timestamptz(6)
tenant tenants? @relation(fields: [tenant_id], references: [id], onDelete: Cascade)
alert_logs alert_logs[]
}
model alert_logs {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
tenant_id String? @db.Uuid
destinatario_tipo String @db.VarChar(20)
destinatario_id String @db.Uuid
canal String @db.VarChar(20)
template_id String? @db.Uuid
conteudo_enviado String?
status String @default("enviado") @db.VarChar(20)
erro String?
enviado_em DateTime @default(now()) @db.Timestamptz(6)
tenant tenants? @relation(fields: [tenant_id], references: [id], onDelete: Cascade)
template alert_templates? @relation(fields: [template_id], references: [id])
@@index([tenant_id, enviado_em], map: "idx_alert_logs_tenant")
}
// ============================================================
// 5. CLIENTES DO TENANT ("customers")
// ============================================================
model customers {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
tenant_id String @db.Uuid
razao_social String @db.VarChar(200)
nome_fantasia String? @db.VarChar(200)
documento String? @db.VarChar(20)
email String? @db.VarChar(150)
telefone_whatsapp String? @db.VarChar(20)
endereco String?
cidade String? @db.VarChar(100)
uf String? @db.Char(2)
observacoes String?
ativo Boolean @default(true)
criado_em DateTime @default(now()) @db.Timestamptz(6)
atualizado_em DateTime @default(now()) @db.Timestamptz(6)
tenant tenants @relation(fields: [tenant_id], references: [id], onDelete: Cascade)
contatos customer_contacts[]
vendas sales[]
@@index([tenant_id], map: "idx_customers_tenant")
}
model customer_contacts {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
customer_id String @db.Uuid
nome String @db.VarChar(150)
cargo String? @db.VarChar(100)
email String? @db.VarChar(150)
telefone_whatsapp String? @db.VarChar(20)
principal Boolean @default(false)
criado_em DateTime @default(now()) @db.Timestamptz(6)
customer customers @relation(fields: [customer_id], references: [id], onDelete: Cascade)
}
// ============================================================
// 6. PRODUTOS E SERVIÇOS DO TENANT
// ============================================================
model products_services {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
tenant_id String @db.Uuid
tipo item_tipo
nome String @db.VarChar(200)
descricao String?
sku String? @db.VarChar(50)
valor_venda Decimal @db.Decimal(12, 2)
valor_custo Decimal? @db.Decimal(12, 2)
unidade String @default("un") @db.VarChar(20)
recorrente Boolean @default(false)
ativo Boolean @default(true)
criado_em DateTime @default(now()) @db.Timestamptz(6)
atualizado_em DateTime @default(now()) @db.Timestamptz(6)
tenant tenants @relation(fields: [tenant_id], references: [id], onDelete: Cascade)
itens_venda sale_items[]
@@unique([tenant_id, sku])
@@index([tenant_id], map: "idx_products_tenant")
}
// ============================================================
// 7. VENDAS
// ============================================================
model sales {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
tenant_id String @db.Uuid
customer_id String @db.Uuid
vendedor_id String? @db.Uuid
numero_venda Int @default(autoincrement())
status venda_status @default(confirmada)
valor_total Decimal @default(0) @db.Decimal(12, 2)
desconto Decimal @default(0) @db.Decimal(12, 2)
observacoes String?
data_venda DateTime @default(now()) @db.Date
criado_em DateTime @default(now()) @db.Timestamptz(6)
atualizado_em DateTime @default(now()) @db.Timestamptz(6)
tenant tenants @relation(fields: [tenant_id], references: [id], onDelete: Cascade)
customer customers @relation(fields: [customer_id], references: [id])
vendedor tenant_users? @relation(fields: [vendedor_id], references: [id])
itens sale_items[]
parcelas sale_payments[]
@@index([tenant_id, data_venda], map: "idx_sales_tenant_data")
@@index([customer_id], map: "idx_sales_customer")
}
model sale_items {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
sale_id String @db.Uuid
product_service_id String @db.Uuid
quantidade Decimal @default(1) @db.Decimal(10, 2)
valor_unitario Decimal @db.Decimal(12, 2)
valor_total Decimal @db.Decimal(12, 2)
sale sales @relation(fields: [sale_id], references: [id], onDelete: Cascade)
produto products_services @relation(fields: [product_service_id], references: [id])
@@index([sale_id], map: "idx_sale_items_sale")
}
model sale_payments {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
sale_id String @db.Uuid
numero_parcela Int @default(1) @db.SmallInt
valor Decimal @db.Decimal(12, 2)
vencimento DateTime @db.Date
pago_em DateTime? @db.Timestamptz(6)
forma_pagamento String? @db.VarChar(30)
status parcela_status @default(pendente)
alerta_enviado_em DateTime? @db.Timestamptz(6)
criado_em DateTime @default(now()) @db.Timestamptz(6)
sale sales @relation(fields: [sale_id], references: [id], onDelete: Cascade)
}
// ============================================================
// 8. AUDITORIA GERAL
// ============================================================
model audit_logs {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
tenant_id String? @db.Uuid
usuario_tipo String @db.VarChar(20)
usuario_id String? @db.Uuid
acao String @db.VarChar(100)
entidade String? @db.VarChar(50)
entidade_id String? @db.Uuid
detalhes Json?
ip_origem String? @db.Inet
criado_em DateTime @default(now()) @db.Timestamptz(6)
tenant tenants? @relation(fields: [tenant_id], references: [id], onDelete: SetNull)
@@index([tenant_id, criado_em], map: "idx_audit_tenant_data")
}