'use strict' /** * database.js — Camada de compatibilidade PostgreSQL * * Mantém a mesma API do módulo sql.js anterior (runQuery/getOne/getAll/saveDatabase) * para que os controllers e plugins existentes não precisem ser alterados. * * As funções agora são ASSÍNCRONAS internamente mas retornam Promises, * e o server.js chama initialize() com await antes de iniciar. */ const { execute, queryOne, query } = require('./postgres') // Compatibilidade: controllers antigos usam runQuery/getOne/getAll de forma síncrona // via require('../database/database'). Como o Express suporta async handlers, // os controllers foram atualizados para async/await nos models. // Este arquivo apenas exporta os helpers para código legado restante (plugins). async function initialize() { const row = await queryOne('SELECT NOW() as now') console.log('✅ PostgreSQL conectado:', row.now) // Fase 7: colunas idempotentes await execute(`ALTER TABLE users ADD COLUMN IF NOT EXISTS whatsapp_optin BOOLEAN DEFAULT TRUE`) await execute(`ALTER TABLE raffles ADD COLUMN IF NOT EXISTS last_hours_notif_sent TIMESTAMPTZ`) await execute(`ALTER TABLE raffle_claims ADD COLUMN IF NOT EXISTS notified_at TIMESTAMPTZ`) console.log('✅ Colunas Fase 7 verificadas') // ── Fase 9B — Auth email + senha ────────────────────────────────────────── await execute(`ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash VARCHAR(255)`) await execute(`CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_tenant ON users(email, tenant_id) WHERE email IS NOT NULL`) await execute(` CREATE TABLE IF NOT EXISTS password_reset_tokens ( id SERIAL PRIMARY KEY, user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE, token VARCHAR(128) NOT NULL UNIQUE, expires_at TIMESTAMPTZ NOT NULL, used BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_prt_token ON password_reset_tokens(token)`) await execute(`CREATE INDEX IF NOT EXISTS idx_prt_user ON password_reset_tokens(user_id)`) console.log('✅ Fase 9B — Auth email+senha verificada') // ── Fase 9A — Multi-tenant ───────────────────────────────────────────────── await execute(` CREATE TABLE IF NOT EXISTS tenants ( id SERIAL PRIMARY KEY, slug VARCHAR(80) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, domain VARCHAR(255), active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(` CREATE TABLE IF NOT EXISTS tenant_config ( tenant_id INT NOT NULL PRIMARY KEY REFERENCES tenants(id) ON DELETE CASCADE, primary_color VARCHAR(20) NOT NULL DEFAULT '#f59e0b', secondary_color VARCHAR(20) NOT NULL DEFAULT '#10b981', logo_url TEXT, favicon_url TEXT, contact_email VARCHAR(255), contact_phone VARCHAR(30), app_url VARCHAR(255), asaas_key VARCHAR(255) ) `) // Tenant padrão — Alemão Conveniências await execute(` INSERT INTO tenants (id, slug, name, domain, active) VALUES (1, 'alemao', 'Alemão Conveniências', 'alemaoconveniencias.local', true) ON CONFLICT (id) DO NOTHING `) await execute(` INSERT INTO tenant_config (tenant_id, primary_color, secondary_color, app_url) VALUES (1, '#f59e0b', '#10b981', 'http://alemaoconveniencias.local:4001') ON CONFLICT (tenant_id) DO NOTHING `) // tenant_id nas tabelas existentes await execute(`ALTER TABLE users ADD COLUMN IF NOT EXISTS tenant_id INT REFERENCES tenants(id)`) await execute(`ALTER TABLE raffles ADD COLUMN IF NOT EXISTS tenant_id INT REFERENCES tenants(id)`) await execute(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS tenant_id INT REFERENCES tenants(id)`) await execute(`ALTER TABLE tickets ADD COLUMN IF NOT EXISTS tenant_id INT REFERENCES tenants(id)`) await execute(`ALTER TABLE promotions ADD COLUMN IF NOT EXISTS tenant_id INT REFERENCES tenants(id)`) await execute(`ALTER TABLE whatsapp_logs ADD COLUMN IF NOT EXISTS tenant_id INT REFERENCES tenants(id)`) // Popular registros existentes sem tenant_id await execute(`UPDATE users SET tenant_id = 1 WHERE tenant_id IS NULL`) await execute(`UPDATE raffles SET tenant_id = 1 WHERE tenant_id IS NULL`) await execute(`UPDATE orders SET tenant_id = 1 WHERE tenant_id IS NULL`) await execute(`UPDATE tickets SET tenant_id = 1 WHERE tenant_id IS NULL`) await execute(`UPDATE promotions SET tenant_id = 1 WHERE tenant_id IS NULL`) await execute(`UPDATE whatsapp_logs SET tenant_id = 1 WHERE tenant_id IS NULL`) // Índices de performance await execute(`CREATE INDEX IF NOT EXISTS idx_users_tenant ON users(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_raffles_tenant ON raffles(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_orders_tenant ON orders(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_tickets_tenant ON tickets(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_promotions_tenant ON promotions(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_wa_logs_tenant ON whatsapp_logs(tenant_id)`) // ── Fase 10A — Clube de Benefícios (tabelas com tenant_id desde o início) ── await execute(` CREATE TABLE IF NOT EXISTS benefit_clubs ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL REFERENCES tenants(id), slug VARCHAR(80) NOT NULL, name VARCHAR(255) NOT NULL, specialty VARCHAR(80) NOT NULL DEFAULT 'outro', active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(tenant_id, slug) ) `) await execute(` CREATE TABLE IF NOT EXISTS club_plans ( id SERIAL PRIMARY KEY, club_id INT NOT NULL REFERENCES benefit_clubs(id), name VARCHAR(255) NOT NULL, description TEXT, price_monthly NUMERIC(10,2) NOT NULL, max_dependents INT NOT NULL DEFAULT 0, features JSONB NOT NULL DEFAULT '[]', active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(` CREATE TABLE IF NOT EXISTS club_members ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL REFERENCES tenants(id), club_id INT NOT NULL REFERENCES benefit_clubs(id), plan_id INT NOT NULL REFERENCES club_plans(id), user_cpf VARCHAR(14) NOT NULL, user_nome VARCHAR(255) NOT NULL, user_telefone VARCHAR(20), user_email VARCHAR(255), status VARCHAR(20) NOT NULL DEFAULT 'pending_payment', asaas_subscription_id VARCHAR(255), asaas_customer_id VARCHAR(255), next_billing_at DATE, started_at TIMESTAMPTZ, canceled_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_club_members_cpf ON club_members(user_cpf)`) await execute(`CREATE INDEX IF NOT EXISTS idx_club_members_tenant ON club_members(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_club_members_status ON club_members(status)`) await execute(` CREATE TABLE IF NOT EXISTS club_dependents ( id SERIAL PRIMARY KEY, member_id INT NOT NULL REFERENCES club_members(id) ON DELETE CASCADE, nome VARCHAR(255) NOT NULL, cpf VARCHAR(14), nascimento DATE, parentesco VARCHAR(80), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(` CREATE TABLE IF NOT EXISTS club_payments ( id SERIAL PRIMARY KEY, member_id INT NOT NULL REFERENCES club_members(id), plan_id INT NOT NULL REFERENCES club_plans(id), amount NUMERIC(10,2) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending', asaas_payment_id VARCHAR(255), reference_month VARCHAR(7), paid_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_club_payments_member ON club_payments(member_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_club_payments_status ON club_payments(status)`) await execute(` CREATE TABLE IF NOT EXISTS club_partners ( id SERIAL PRIMARY KEY, club_id INT NOT NULL REFERENCES benefit_clubs(id), tenant_id INT REFERENCES tenants(id), name VARCHAR(255) NOT NULL, specialty VARCHAR(255), phone VARCHAR(20), address TEXT, discount_percent INT NOT NULL DEFAULT 0, notes TEXT, active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) // Migração: renomear colunas PT → EN (idempotente via DO $$) await execute(` DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='club_partners' AND column_name='nome') THEN ALTER TABLE club_partners RENAME COLUMN nome TO name; ALTER TABLE club_partners RENAME COLUMN especialidade TO specialty; ALTER TABLE club_partners RENAME COLUMN telefone TO phone; ALTER TABLE club_partners RENAME COLUMN endereco TO address; ALTER TABLE club_partners RENAME COLUMN desconto_percent TO discount_percent; ALTER TABLE club_partners RENAME COLUMN ativo TO active; END IF; END $$ `) await execute(`ALTER TABLE club_partners ADD COLUMN IF NOT EXISTS tenant_id INT REFERENCES tenants(id)`) await execute(`ALTER TABLE club_partners ADD COLUMN IF NOT EXISTS notes TEXT`) await execute(`CREATE INDEX IF NOT EXISTS idx_club_partners_club ON club_partners(club_id)`) // Dados iniciais — clubes e planos do Alemão await execute(` INSERT INTO benefit_clubs (tenant_id, slug, name, specialty, active) VALUES (1, 'odonto', 'Clube Odonto', 'odonto', true), (1, 'medico', 'Clube Médico', 'medico', true) ON CONFLICT (tenant_id, slug) DO NOTHING `) const odonto = await queryOne(`SELECT id FROM benefit_clubs WHERE tenant_id=1 AND slug='odonto'`) const medico = await queryOne(`SELECT id FROM benefit_clubs WHERE tenant_id=1 AND slug='medico'`) if (odonto) { const existing = await queryOne(`SELECT id FROM club_plans WHERE club_id = $1 LIMIT 1`, [odonto.id]) if (!existing) { await execute( `INSERT INTO club_plans (club_id, name, description, price_monthly, max_dependents, features) VALUES ($1, 'Individual', 'Plano para uma pessoa', 25.00, 0, $2), ($1, 'Família', 'Plano para até 4 dependentes', 80.00, 4, $3)`, [ odonto.id, JSON.stringify(['Limpezas e Prevenção','Descontos em Tratamentos','Extrações e Obturações','Sem Carência']), JSON.stringify(['Até 4 Dependentes','Limpezas e Prevenção','Descontos em Tratamentos','Urgência e Emergência']), ] ) } } if (medico) { const existing = await queryOne(`SELECT id FROM club_plans WHERE club_id = $1 LIMIT 1`, [medico.id]) if (!existing) { await execute( `INSERT INTO club_plans (club_id, name, description, price_monthly, max_dependents, features) VALUES ($1, 'Individual', 'Plano médico individual', 35.00, 0, $2), ($1, 'Família', 'Plano médico familiar', 99.00, 4, $3)`, [ medico.id, JSON.stringify(['Consultas com Clínico Geral','Descontos em Exames','Pediatria','Sem Carência']), JSON.stringify(['Até 4 Dependentes','Clínico Geral e Especialistas','Descontos em Exames','Urgência e Emergência']), ] ) } } // ── Fase 10B — Condições, Regras e FAQ por clube ────────────────────────── await execute(`ALTER TABLE benefit_clubs ADD COLUMN IF NOT EXISTS conditions TEXT`) await execute(`ALTER TABLE benefit_clubs ADD COLUMN IF NOT EXISTS rules TEXT`) await execute(` CREATE TABLE IF NOT EXISTS club_faqs ( id SERIAL PRIMARY KEY, club_id INT NOT NULL REFERENCES benefit_clubs(id) ON DELETE CASCADE, tenant_id INT NOT NULL REFERENCES tenants(id), question TEXT NOT NULL, answer TEXT NOT NULL, sort_order INT NOT NULL DEFAULT 0, active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_club_faqs_club ON club_faqs(club_id)`) // Seed das FAQs padrão por clube (apenas se ainda não houver nenhuma) if (odonto) { const faqExists = await queryOne(`SELECT id FROM club_faqs WHERE club_id=$1 LIMIT 1`, [odonto.id]) if (!faqExists) { await execute( `INSERT INTO club_faqs (club_id, tenant_id, question, answer, sort_order) VALUES ($1, 1, 'Como funciona o pagamento?', 'O pagamento é mensal via PIX, gerado automaticamente. Você recebe o link no WhatsApp antes do vencimento.', 0), ($1, 1, 'Posso cancelar a qualquer momento?', 'Sim. Acesse sua área do cliente e solicite o cancelamento. O plano permanece ativo até o fim do período pago.', 1), ($1, 1, 'Meus dependentes têm acesso?', 'Sim, dependendo do plano escolhido. O plano Família inclui cobertura para até 4 dependentes.', 2), ($1, 1, 'Como acessar as clínicas parceiras?', 'Apresente sua carteirinha digital (acessível pela área do cliente) na recepção da clínica parceira.', 3)`, [odonto.id] ) } } if (medico) { const faqExists = await queryOne(`SELECT id FROM club_faqs WHERE club_id=$1 LIMIT 1`, [medico.id]) if (!faqExists) { await execute( `INSERT INTO club_faqs (club_id, tenant_id, question, answer, sort_order) VALUES ($1, 1, 'Como funciona o pagamento?', 'O pagamento é mensal via PIX, gerado automaticamente. Você recebe o link no WhatsApp antes do vencimento.', 0), ($1, 1, 'Posso cancelar a qualquer momento?', 'Sim. Acesse sua área do cliente e solicite o cancelamento. O plano permanece ativo até o fim do período pago.', 1), ($1, 1, 'Meus dependentes têm acesso?', 'Sim, dependendo do plano escolhido. O plano Família inclui cobertura para até 4 dependentes.', 2), ($1, 1, 'Como acessar as clínicas parceiras?', 'Apresente sua carteirinha digital (acessível pela área do cliente) na recepção da clínica parceira.', 3)`, [medico.id] ) } } console.log('✅ Fase 10B — club_faqs e colunas conditions/rules verificadas') console.log('✅ Fase 9A — Multi-tenant e tabelas do Clube criadas/verificadas') // ── Fase 11 — Banco de Talentos ────────────────────────────────────────── await execute(` CREATE TABLE IF NOT EXISTS jobs ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL REFERENCES tenants(id), title TEXT NOT NULL, description TEXT NOT NULL, requirements TEXT, benefits TEXT, location TEXT, work_mode TEXT NOT NULL DEFAULT 'presencial', job_type TEXT NOT NULL DEFAULT 'clt', category TEXT, salary_min NUMERIC(10,2), salary_max NUMERIC(10,2), salary_hide BOOLEAN NOT NULL DEFAULT false, slots INT NOT NULL DEFAULT 1, active BOOLEAN NOT NULL DEFAULT true, published_at TIMESTAMPTZ DEFAULT NOW(), expires_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_jobs_tenant ON jobs(tenant_id, active)`) await execute(`CREATE INDEX IF NOT EXISTS idx_jobs_published ON jobs(published_at DESC)`) await execute(` CREATE TABLE IF NOT EXISTS resumes ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL REFERENCES tenants(id), user_cpf TEXT NOT NULL, user_nome TEXT NOT NULL, email TEXT, phone TEXT, linkedin TEXT, portfolio TEXT, about TEXT, experience JSONB NOT NULL DEFAULT '[]', education JSONB NOT NULL DEFAULT '[]', skills JSONB NOT NULL DEFAULT '[]', file_url TEXT, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (tenant_id, user_cpf) ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_resumes_cpf ON resumes(tenant_id, user_cpf)`) await execute(` CREATE TABLE IF NOT EXISTS job_applications ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL REFERENCES tenants(id), job_id INT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE, resume_id INT REFERENCES resumes(id), applicant_nome TEXT NOT NULL, applicant_email TEXT, applicant_phone TEXT, applicant_cpf TEXT NOT NULL, cover_letter TEXT, status TEXT NOT NULL DEFAULT 'novo', admin_notes TEXT, applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (job_id, applicant_cpf) ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_applications_job ON job_applications(job_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_applications_cpf ON job_applications(applicant_cpf)`) console.log('✅ Fase 11 — Banco de Talentos verificado') // ── Fase 9F — Admin tenant_id ──────────────────────────────────────────── await execute(`ALTER TABLE admins ADD COLUMN IF NOT EXISTS tenant_id INT REFERENCES tenants(id)`) // Associa admins existentes sem tenant ao tenant padrão (alemao = 1) await execute(`UPDATE admins SET tenant_id=1 WHERE tenant_id IS NULL`) console.log('✅ Fase 9F — tenant_id em admins verificado') // ── Admin ↔ Account link ────────────────────────────────────────────────── // Motivo: o admin (tabela `admins`) e o colaborador (tabela `accounts`) são // entidades separadas com JWTs distintos. Sem essa ligação, quando o admin // está logado pela sessão admin, a Inbox Interna não sabe qual account usar // como identidade — e exibe "login de colaborador necessário". // Solução: um campo account_id opcional em admins. Quando preenchido, o // endpoint /api/admin/me devolve o account_id e o frontend usa esse valor // diretamente na Inbox Interna, sem exigir um segundo login. await execute(`ALTER TABLE admins ADD COLUMN IF NOT EXISTS account_id INT REFERENCES accounts(id) ON DELETE SET NULL`) console.log('✅ Admin ↔ Account link: coluna account_id adicionada em admins') // ── Fase 9E — Super Admin ───────────────────────────────────────────────── await execute(` CREATE TABLE IF NOT EXISTS super_admins ( id SERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) // Seed super admin padrão se não existir const bcrypt = require('bcryptjs') const existingSA = await queryOne('SELECT id FROM super_admins LIMIT 1') if (!existingSA) { const hash = await bcrypt.hash(process.env.SUPER_ADMIN_PASSWORD || 'superadmin123', 10) await execute( `INSERT INTO super_admins (email, password_hash, name) VALUES ($1,$2,$3)`, [process.env.SUPER_ADMIN_EMAIL || 'super@admin.local', hash, 'Super Admin'] ) console.log('✅ Super admin padrão criado — altere a senha em produção!') } console.log('✅ Fase 9E — Super Admin verificado') // ── Plugin NewWhats — tabela de eventos de inbox ────────────────────────── await execute(` CREATE TABLE IF NOT EXISTS nw_event_logs ( id SERIAL PRIMARY KEY, instance_id VARCHAR(128) NOT NULL DEFAULT '', event VARCHAR(64) NOT NULL, direction VARCHAR(8) NOT NULL DEFAULT 'in', chat_id VARCHAR(128), from_me BOOLEAN NOT NULL DEFAULT FALSE, msg_type VARCHAR(32), payload JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_nw_event_logs_instance ON nw_event_logs(instance_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_nw_event_logs_event ON nw_event_logs(event)`) await execute(`CREATE INDEX IF NOT EXISTS idx_nw_event_logs_created ON nw_event_logs(created_at DESC)`) console.log('✅ Plugin NewWhats — tabela nw_event_logs verificada') // ── Plugin NewWhats — respostas automáticas da IA ───────────────────────── await execute(` CREATE TABLE IF NOT EXISTS nw_auto_replies ( id SERIAL PRIMARY KEY, instance_id VARCHAR(128) NOT NULL, chat_id VARCHAR(256) NOT NULL, conv_id VARCHAR(64), user_msg TEXT, reply TEXT, status VARCHAR(20) NOT NULL DEFAULT 'sent', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_nw_auto_chat ON nw_auto_replies(chat_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_nw_auto_instance ON nw_auto_replies(instance_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_nw_auto_created ON nw_auto_replies(created_at DESC)`) console.log('✅ Plugin NewWhats — tabela nw_auto_replies verificada') // Multi-tenant: adiciona tenant_id nas tabelas nw_ (retroativo com DEFAULT 1) await execute(`ALTER TABLE nw_event_logs ADD COLUMN IF NOT EXISTS tenant_id INT NOT NULL DEFAULT 1`) await execute(`ALTER TABLE nw_auto_replies ADD COLUMN IF NOT EXISTS tenant_id INT NOT NULL DEFAULT 1`) await execute(`CREATE INDEX IF NOT EXISTS idx_nw_event_tenant ON nw_event_logs(tenant_id, created_at DESC)`) await execute(`CREATE INDEX IF NOT EXISTS idx_nw_auto_tenant ON nw_auto_replies(tenant_id, created_at DESC)`) // ── Handoff local — pausa da IA por chat ────────────────────────────────── await execute(` CREATE TABLE IF NOT EXISTS nw_handoff ( chat_id TEXT NOT NULL, tenant_id INT NOT NULL, human_until TIMESTAMPTZ, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (chat_id, tenant_id) ) `) console.log('✅ Plugin NewWhats — tabela nw_handoff verificada') // ── Setor identificado por chat ──────────────────────────────────────────── await execute(` CREATE TABLE IF NOT EXISTS nw_chat_sectors ( chat_id TEXT NOT NULL, tenant_id INT NOT NULL, sector_id INT REFERENCES sectors(id) ON DELETE SET NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (chat_id, tenant_id) ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_nw_chat_sectors_sector ON nw_chat_sectors(sector_id)`) // ── Avaliação de atendimento pelo cliente ────────────────────────────────── await execute(` CREATE TABLE IF NOT EXISTS protocol_ratings ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL REFERENCES tenants(id), thread_id VARCHAR(64) NOT NULL, chat_id TEXT NOT NULL, account_id INT REFERENCES accounts(id) ON DELETE SET NULL, rating SMALLINT CHECK (rating BETWEEN 1 AND 5), status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','rated','expired')), sent_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), rated_at TIMESTAMPTZ ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_protocol_ratings_tenant ON protocol_ratings(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_protocol_ratings_chat ON protocol_ratings(chat_id)`) // ── Flags de protocolo (reclamação, sugestão, elogio) ───────────────────── await execute(` CREATE TABLE IF NOT EXISTS protocol_flags ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL REFERENCES tenants(id), thread_id VARCHAR(64) NOT NULL, account_id INT REFERENCES accounts(id) ON DELETE SET NULL, type VARCHAR(20) NOT NULL DEFAULT 'complaint' CHECK (type IN ('complaint','suggestion','compliment')), note TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_protocol_flags_tenant ON protocol_flags(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_protocol_flags_thread ON protocol_flags(thread_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_protocol_flags_type ON protocol_flags(type)`) console.log('✅ Fase 4 — protocol_ratings, protocol_flags criadas/verificadas') // ── Worker — tabela de falhas definitivas de envio ──────────────────────── await execute(` CREATE TABLE IF NOT EXISTS notification_errors ( id SERIAL PRIMARY KEY, job_id VARCHAR(64), log_id INT, phone VARCHAR(20), type VARCHAR(64), error_message TEXT, payload JSONB, tenant_id INT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_notif_errors_tenant ON notification_errors(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_notif_errors_created ON notification_errors(created_at DESC)`) console.log('✅ Worker — tabela notification_errors verificada') // ── Fase 12 — Equipe Virtual ───────────────────────────────────────────── // accounts: colaboradores internos com cargo e número pessoal await execute(` CREATE TABLE IF NOT EXISTS accounts ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL REFERENCES tenants(id), name VARCHAR(255) NOT NULL, email VARCHAR(255), password_hash VARCHAR(255), role VARCHAR(20) NOT NULL DEFAULT 'agent' CHECK (role IN ('owner','manager','supervisor','secretary','agent','viewer')), phone VARCHAR(30), phone_connected BOOLEAN NOT NULL DEFAULT FALSE, availability VARCHAR(10) NOT NULL DEFAULT 'offline' CHECK (availability IN ('online','busy','offline')), avatar_url TEXT, active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_email ON accounts(tenant_id, email) WHERE email IS NOT NULL`) await execute(`CREATE INDEX IF NOT EXISTS idx_accounts_tenant ON accounts(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_accounts_role ON accounts(tenant_id, role)`) // sectors: setores operacionais da empresa await execute(` CREATE TABLE IF NOT EXISTS sectors ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL REFERENCES tenants(id), name VARCHAR(255) NOT NULL, description TEXT, icon VARCHAR(80) NOT NULL DEFAULT 'Briefcase', color VARCHAR(20) NOT NULL DEFAULT '#6366f1', sla_minutes INT NOT NULL DEFAULT 60, sort_order INT NOT NULL DEFAULT 0, active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_sectors_tenant ON sectors(tenant_id, active)`) // team_members: relacionamento conta ↔ setor await execute(` CREATE TABLE IF NOT EXISTS team_members ( id SERIAL PRIMARY KEY, account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, sector_id INT NOT NULL REFERENCES sectors(id) ON DELETE CASCADE, function VARCHAR(255), is_primary BOOLEAN NOT NULL DEFAULT FALSE, notify_escalation BOOLEAN NOT NULL DEFAULT TRUE, notify_human_api BOOLEAN NOT NULL DEFAULT TRUE, on_duty BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (account_id, sector_id) ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_team_members_account ON team_members(account_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_team_members_sector ON team_members(sector_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_team_members_duty ON team_members(sector_id, on_duty)`) // sector_brain_nodes: cérebro de conhecimento por setor await execute(` CREATE TABLE IF NOT EXISTS sector_brain_nodes ( id SERIAL PRIMARY KEY, sector_id INT NOT NULL REFERENCES sectors(id) ON DELETE CASCADE, type VARCHAR(20) NOT NULL DEFAULT 'knowledge' CHECK (type IN ('knowledge','rules','escalation','persona')), content TEXT NOT NULL, sort_order INT NOT NULL DEFAULT 0, active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_sector_brain_sector ON sector_brain_nodes(sector_id, active)`) // internal_messages: inbox interna (equipe ↔ equipe, Human API, escalações) await execute(` CREATE TABLE IF NOT EXISTS internal_messages ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL REFERENCES tenants(id), thread_id VARCHAR(64) NOT NULL, from_account INT REFERENCES accounts(id) ON DELETE SET NULL, to_account INT REFERENCES accounts(id) ON DELETE SET NULL, sector_id INT REFERENCES sectors(id) ON DELETE SET NULL, type VARCHAR(20) NOT NULL DEFAULT 'collaboration' CHECK (type IN ('human_api','escalation','collaboration','system','help_request')), content TEXT NOT NULL, ref_id VARCHAR(64), read_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_internal_msgs_tenant ON internal_messages(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_internal_msgs_thread ON internal_messages(thread_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_internal_msgs_to ON internal_messages(to_account, read_at)`) await execute(`CREATE INDEX IF NOT EXISTS idx_internal_msgs_sector ON internal_messages(sector_id)`) // human_api_requests: consultas da IA para especialistas humanos await execute(` CREATE TABLE IF NOT EXISTS human_api_requests ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL REFERENCES tenants(id), conversation_id VARCHAR(64), sector_id INT REFERENCES sectors(id) ON DELETE SET NULL, account_id INT REFERENCES accounts(id) ON DELETE SET NULL, question TEXT NOT NULL, answer TEXT, status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','answered','timeout','ignored')), delivery_mode VARCHAR(20) NOT NULL DEFAULT 'inbox_interna' CHECK (delivery_mode IN ('whatsapp','inbox_interna')), asked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), answered_at TIMESTAMPTZ, timeout_at TIMESTAMPTZ ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_human_api_tenant ON human_api_requests(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_human_api_status ON human_api_requests(status)`) await execute(`CREATE INDEX IF NOT EXISTS idx_human_api_account ON human_api_requests(account_id)`) // knowledge_learning: respostas humanas que podem virar base de conhecimento await execute(` CREATE TABLE IF NOT EXISTS knowledge_learning ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL REFERENCES tenants(id), sector_id INT REFERENCES sectors(id) ON DELETE SET NULL, account_id INT REFERENCES accounts(id) ON DELETE SET NULL, question TEXT NOT NULL, answer TEXT NOT NULL, promoted BOOLEAN NOT NULL DEFAULT FALSE, promoted_at TIMESTAMPTZ, use_count INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_knowledge_tenant ON knowledge_learning(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_knowledge_sector ON knowledge_learning(sector_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_knowledge_promoted ON knowledge_learning(promoted)`) // Adiciona chat_id em internal_messages (necessário para encerrar protocolo sem prompt) await execute(`ALTER TABLE internal_messages ADD COLUMN IF NOT EXISTS chat_id VARCHAR(128)`) // ── Direct Messages (DMs entre colaboradores) ────────────────────────────────── // Fase 13: Comunicação interna entre colaboradores (não vinculada a escalação/protocol) await execute(` CREATE TABLE IF NOT EXISTS direct_messages ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL REFERENCES tenants(id), from_account INT NOT NULL REFERENCES accounts(id) ON DELETE SET NULL, to_account INT NOT NULL REFERENCES accounts(id) ON DELETE SET NULL, content TEXT NOT NULL, read_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_dm_tenant ON direct_messages(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_dm_from_to ON direct_messages(from_account, to_account)`) await execute(`CREATE INDEX IF NOT EXISTS idx_dm_read_status ON direct_messages(to_account, read_at)`) await execute(`CREATE INDEX IF NOT EXISTS idx_dm_created ON direct_messages(created_at DESC)`) console.log('✅ Fase 12 — Equipe Virtual: accounts, sectors, team_members, sector_brain_nodes, internal_messages, human_api_requests, knowledge_learning') console.log('✅ Fase 13 — Direct Messages: DMs entre colaboradores') // ── Group Chats por Setor ───────────────────────────────────────────────── await execute(` CREATE TABLE IF NOT EXISTS group_chats ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL REFERENCES tenants(id), sector_id INT NOT NULL REFERENCES sectors(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (tenant_id, sector_id) ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_group_chats_tenant ON group_chats(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_group_chats_sector ON group_chats(sector_id)`) await execute(` CREATE TABLE IF NOT EXISTS group_messages ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL REFERENCES tenants(id), group_chat_id INT NOT NULL REFERENCES group_chats(id) ON DELETE CASCADE, from_account INT REFERENCES accounts(id) ON DELETE SET NULL, content TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_gm_group ON group_messages(group_chat_id, created_at DESC)`) await execute(`CREATE INDEX IF NOT EXISTS idx_gm_tenant ON group_messages(tenant_id)`) // Seed: cria group_chat para cada setor que ainda não tem await execute(` INSERT INTO group_chats (tenant_id, sector_id, name) SELECT s.tenant_id, s.id, s.name FROM sectors s WHERE s.active = TRUE AND NOT EXISTS ( SELECT 1 FROM group_chats g WHERE g.sector_id = s.id ) `) console.log('✅ Fase 14 — Group Chats: group_chats, group_messages (seed por setor)') // ── Broadcasts ──────────────────────────────────────────────────────────── await execute(` CREATE TABLE IF NOT EXISTS broadcasts ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL REFERENCES tenants(id), from_account INT REFERENCES accounts(id) ON DELETE SET NULL, content TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_broadcasts_tenant ON broadcasts(tenant_id, created_at DESC)`) console.log('✅ Fase 15 — Broadcasts: tabela broadcasts') // ── Leitura: grupos e broadcasts ────────────────────────────────────────── await execute(` CREATE TABLE IF NOT EXISTS group_last_read ( account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, group_chat_id INT NOT NULL REFERENCES group_chats(id) ON DELETE CASCADE, last_read_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (account_id, group_chat_id) ) `) await execute(` CREATE TABLE IF NOT EXISTS broadcast_last_seen ( account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, tenant_id INT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (account_id, tenant_id) ) `) console.log('✅ Fase 16 — Leitura: group_last_read, broadcast_last_seen') // ── Descontos por quantidade em sorteios ───────────────────────────────── await execute(` CREATE TABLE IF NOT EXISTS raffle_discounts ( id SERIAL PRIMARY KEY, raffle_id INT NOT NULL REFERENCES raffles(id) ON DELETE CASCADE, min_qty INT NOT NULL CHECK (min_qty >= 2), discount_pct NUMERIC(5,2) NOT NULL CHECK (discount_pct > 0 AND discount_pct <= 100), active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (raffle_id, min_qty) ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_raffle_discounts_raffle ON raffle_discounts(raffle_id) WHERE active = TRUE`) console.log('✅ raffle_discounts — descontos por quantidade verificados') // ── Promoções Relâmpago ─────────────────────────────────────────────────── await execute(` CREATE TABLE IF NOT EXISTS raffle_flash_promos ( id SERIAL PRIMARY KEY, raffle_id INT NOT NULL REFERENCES raffles(id) ON DELETE CASCADE, discount_pct NUMERIC(5,2) NOT NULL CHECK (discount_pct > 0 AND discount_pct <= 100), label VARCHAR(255) NOT NULL DEFAULT 'Promoção Relâmpago', starts_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), ends_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_flash_promos_raffle ON raffle_flash_promos(raffle_id, ends_at)`) console.log('✅ raffle_flash_promos — promoções relâmpago verificadas') // ── Módulo Cotação Online ────────────────────────────────────────────────── await execute(` CREATE TABLE IF NOT EXISTS clientes ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL DEFAULT 1, tipo VARCHAR(2) NOT NULL CHECK (tipo IN ('PF','PJ')), nome VARCHAR(255) NOT NULL, cpf_cnpj VARCHAR(20) NOT NULL, email VARCHAR(255), status VARCHAR(10) NOT NULL DEFAULT 'ativo' CHECK (status IN ('ativo','inativo')), user_id INT REFERENCES users(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_clientes_tenant ON clientes(tenant_id)`) await execute(`CREATE UNIQUE INDEX IF NOT EXISTS idx_clientes_cpf_tenant ON clientes(cpf_cnpj, tenant_id)`) await execute(` CREATE TABLE IF NOT EXISTS cliente_enderecos ( id SERIAL PRIMARY KEY, cliente_id INT NOT NULL REFERENCES clientes(id) ON DELETE CASCADE, tipo VARCHAR(20) NOT NULL DEFAULT 'entrega' CHECK (tipo IN ('entrega','cobranca','retirada','matriz','filial')), cep VARCHAR(10), rua VARCHAR(255), numero VARCHAR(20), complemento VARCHAR(100), bairro VARCHAR(100), cidade VARCHAR(100), estado VARCHAR(2), referencia VARCHAR(255), principal BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_enderecos_cliente ON cliente_enderecos(cliente_id)`) await execute(` CREATE TABLE IF NOT EXISTS cliente_telefones ( id SERIAL PRIMARY KEY, cliente_id INT NOT NULL REFERENCES clientes(id) ON DELETE CASCADE, tipo VARCHAR(20) NOT NULL DEFAULT 'celular' CHECK (tipo IN ('comercial','celular','whatsapp','financeiro','compras')), numero VARCHAR(20) NOT NULL, ramal VARCHAR(10), aceita_whatsapp BOOLEAN NOT NULL DEFAULT FALSE, principal BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_telefones_cliente ON cliente_telefones(cliente_id)`) await execute(` CREATE TABLE IF NOT EXISTS cliente_responsaveis ( id SERIAL PRIMARY KEY, cliente_id INT NOT NULL REFERENCES clientes(id) ON DELETE CASCADE, nome VARCHAR(255) NOT NULL, cargo VARCHAR(100), setor VARCHAR(100), telefone_id INT REFERENCES cliente_telefones(id) ON DELETE SET NULL, email VARCHAR(255), permissao VARCHAR(100), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_responsaveis_cliente ON cliente_responsaveis(cliente_id)`) await execute(` CREATE TABLE IF NOT EXISTS produtos ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL DEFAULT 1, sku VARCHAR(100), nome VARCHAR(255) NOT NULL, descricao TEXT, unidade VARCHAR(20) DEFAULT 'un', preco NUMERIC(10,2) NOT NULL DEFAULT 0, preco_promocional NUMERIC(10,2) DEFAULT NULL, estoque NUMERIC(10,3) NOT NULL DEFAULT 0, categoria VARCHAR(100), imagem TEXT, ativo BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_produtos_tenant ON produtos(tenant_id)`) await execute(`CREATE UNIQUE INDEX IF NOT EXISTS idx_produtos_sku_tenant ON produtos(sku, tenant_id) WHERE sku IS NOT NULL`) await execute(` CREATE TABLE IF NOT EXISTS cotacoes ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL DEFAULT 1, cliente_id INT REFERENCES clientes(id) ON DELETE SET NULL, user_id INT REFERENCES users(id) ON DELETE SET NULL, status VARCHAR(20) NOT NULL DEFAULT 'rascunho' CHECK (status IN ('rascunho','salva','finalizada','cancelada')), forma_pagto VARCHAR(20) CHECK (forma_pagto IN ('pix','cartao','faturado')), total NUMERIC(10,2) NOT NULL DEFAULT 0, desconto NUMERIC(10,2) NOT NULL DEFAULT 0, observacao TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_cotacoes_tenant ON cotacoes(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_cotacoes_cliente ON cotacoes(cliente_id)`) await execute(` CREATE TABLE IF NOT EXISTS cotacao_itens ( id SERIAL PRIMARY KEY, cotacao_id INT NOT NULL REFERENCES cotacoes(id) ON DELETE CASCADE, produto_id INT REFERENCES produtos(id) ON DELETE SET NULL, nome_produto VARCHAR(255) NOT NULL, sku VARCHAR(100), quantidade NUMERIC(10,3) NOT NULL DEFAULT 1, preco_unitario NUMERIC(10,2) NOT NULL, preco_promocional NUMERIC(10,2), desconto_pct NUMERIC(5,2) DEFAULT 0, subtotal NUMERIC(10,2) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_cotacao_itens_cotacao ON cotacao_itens(cotacao_id)`) await execute(` CREATE TABLE IF NOT EXISTS pedidos ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL DEFAULT 1, cotacao_id INT REFERENCES cotacoes(id) ON DELETE SET NULL, cliente_id INT REFERENCES clientes(id) ON DELETE SET NULL, user_id INT REFERENCES users(id) ON DELETE SET NULL, endereco_id INT REFERENCES cliente_enderecos(id) ON DELETE SET NULL, status VARCHAR(20) NOT NULL DEFAULT 'aguardando' CHECK (status IN ('aguardando','separacao','embalagem','auditoria','fiscal','expedicao','entregue','cancelado')), forma_pagto VARCHAR(20) CHECK (forma_pagto IN ('pix','cartao','faturado')), tipo_entrega VARCHAR(20) NOT NULL DEFAULT 'retirada' CHECK (tipo_entrega IN ('retirada','entrega')), total NUMERIC(10,2) NOT NULL DEFAULT 0, desconto NUMERIC(10,2) NOT NULL DEFAULT 0, observacao TEXT, equipe_id INT, responsavel_entrega TEXT, entregue_em TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_pedidos_tenant ON pedidos(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_pedidos_status ON pedidos(tenant_id, status)`) await execute(`CREATE INDEX IF NOT EXISTS idx_pedidos_cliente ON pedidos(cliente_id)`) // Histórico de status — registra cada transição com timestamp await execute(` CREATE TABLE IF NOT EXISTS pedido_status_log ( id SERIAL PRIMARY KEY, pedido_id INT NOT NULL REFERENCES pedidos(id) ON DELETE CASCADE, status VARCHAR(20) NOT NULL, account_id INT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_pedido_status_log ON pedido_status_log(pedido_id, created_at)`) // Seed retroativo: garante pelo menos a entrada inicial para pedidos existentes await execute(` INSERT INTO pedido_status_log (pedido_id, status, created_at) SELECT id, 'aguardando', created_at FROM pedidos WHERE NOT EXISTS ( SELECT 1 FROM pedido_status_log l WHERE l.pedido_id = pedidos.id ) `) await execute(` CREATE TABLE IF NOT EXISTS pedido_itens ( id SERIAL PRIMARY KEY, pedido_id INT NOT NULL REFERENCES pedidos(id) ON DELETE CASCADE, produto_id INT REFERENCES produtos(id) ON DELETE SET NULL, nome_produto VARCHAR(255) NOT NULL, sku VARCHAR(100), quantidade NUMERIC(10,3) NOT NULL DEFAULT 1, preco_unitario NUMERIC(10,2) NOT NULL, preco_promocional NUMERIC(10,2) DEFAULT NULL, desconto_pct NUMERIC(5,2) DEFAULT 0, subtotal NUMERIC(10,2) NOT NULL, separado BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_pedido_itens_pedido ON pedido_itens(pedido_id)`) // promotions: garantir tenant_id e ativo BOOLEAN (produto_id removido — promoções são marketing independente) await execute(`ALTER TABLE promotions ADD COLUMN IF NOT EXISTS tenant_id INT`) await execute(`ALTER TABLE promotions ALTER COLUMN ativo DROP DEFAULT`) await execute(`ALTER TABLE promotions ALTER COLUMN ativo TYPE BOOLEAN USING (CASE WHEN ativo::text IN ('1','true') THEN TRUE ELSE FALSE END)`) await execute(`ALTER TABLE promotions ALTER COLUMN ativo SET DEFAULT TRUE`) // novos campos de produto e pedido_itens await execute(`ALTER TABLE produtos ADD COLUMN IF NOT EXISTS preco_promocional NUMERIC(10,2) DEFAULT NULL`) await execute(`ALTER TABLE pedido_itens ADD COLUMN IF NOT EXISTS preco_promocional NUMERIC(10,2) DEFAULT NULL`) // EMB-02 — registro de embalador e timestamp await execute(`ALTER TABLE pedidos ADD COLUMN IF NOT EXISTS embalador TEXT`) await execute(`ALTER TABLE pedidos ADD COLUMN IF NOT EXISTS embalado_em TIMESTAMPTZ`) // WPP-10 — número de protocolo por pedido (PED-YYYYMMDD-ID) await execute(`ALTER TABLE pedidos ADD COLUMN IF NOT EXISTS protocolo VARCHAR(30)`) // PROMO-10 — promoções relâmpago para cotações (reutiliza padrão raffle_flash_promos) await execute(` CREATE TABLE IF NOT EXISTS cotacao_flash_promos ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL DEFAULT 1, produto_id INT REFERENCES produtos(id) ON DELETE SET NULL, discount_pct NUMERIC(5,2) NOT NULL, label VARCHAR(120) NOT NULL DEFAULT 'Promoção Relâmpago', starts_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), ends_at TIMESTAMPTZ NOT NULL, ativo BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_cfp_tenant ON cotacao_flash_promos(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_cfp_ativo ON cotacao_flash_promos(tenant_id, ativo) WHERE ativo = TRUE`) // WPP-09 — log de mensagens de reativação enviadas await execute(` CREATE TABLE IF NOT EXISTS wpp_reativacao_log ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL DEFAULT 1, cliente_id INT REFERENCES clientes(id) ON DELETE SET NULL, chat_id TEXT NOT NULL, mensagem TEXT, enviada_em TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_wpp_reat_tenant ON wpp_reativacao_log(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_wpp_reat_cliente ON wpp_reativacao_log(cliente_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_wpp_reat_chat ON wpp_reativacao_log(chat_id)`) // INB-01/02/03 — Inbox interno por setor vinculado ao pedido await execute(` CREATE TABLE IF NOT EXISTS mensagens_internas ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL DEFAULT 1, pedido_id INT REFERENCES pedidos(id) ON DELETE CASCADE, setor VARCHAR(30) NOT NULL DEFAULT 'geral' CHECK (setor IN ('geral','atendimento','cotacao','separacao','embalagem','auditoria','fiscal','expedicao','financeiro','suporte')), remetente VARCHAR(120) NOT NULL, mensagem TEXT NOT NULL, lida BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_msg_int_tenant ON mensagens_internas(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_msg_int_pedido ON mensagens_internas(pedido_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_msg_int_setor ON mensagens_internas(tenant_id, setor)`) await execute(`CREATE INDEX IF NOT EXISTS idx_msg_int_nao_lida ON mensagens_internas(tenant_id, lida) WHERE lida = FALSE`) // Migração: coluna chat_id para vincular alertas do Monitor IA ao chat WhatsApp await execute(`ALTER TABLE mensagens_internas ADD COLUMN IF NOT EXISTS chat_id VARCHAR(120)`) await execute(`CREATE INDEX IF NOT EXISTS idx_msg_int_chat_id ON mensagens_internas(chat_id) WHERE chat_id IS NOT NULL`) // Observação por item do carrinho (variedades, tamanhos, etc.) await execute(`ALTER TABLE cotacao_itens ADD COLUMN IF NOT EXISTS observacao VARCHAR(500)`) await execute(`ALTER TABLE pedido_itens ADD COLUMN IF NOT EXISTS observacao VARCHAR(500)`) // chat_id em human_api_requests — permite Monitor IA suprimir re-alertas await execute(`ALTER TABLE human_api_requests ADD COLUMN IF NOT EXISTS chat_id VARCHAR(120)`) await execute(`CREATE INDEX IF NOT EXISTS idx_har_chat_id ON human_api_requests(chat_id) WHERE chat_id IS NOT NULL`) // Alertas de item pelo separador (falta, marca diferente, substituição + foto) await execute(`ALTER TABLE pedido_itens ADD COLUMN IF NOT EXISTS alerta_tipo VARCHAR(30)`) await execute(`ALTER TABLE pedido_itens ADD COLUMN IF NOT EXISTS alerta_obs VARCHAR(500)`) await execute(`ALTER TABLE pedido_itens ADD COLUMN IF NOT EXISTS alerta_foto VARCHAR(500)`) await execute(`ALTER TABLE pedido_itens ADD COLUMN IF NOT EXISTS alerta_confirmado BOOLEAN DEFAULT FALSE`) await execute(`ALTER TABLE pedido_itens ADD COLUMN IF NOT EXISTS alerta_em TIMESTAMPTZ`) // ── FUT-02: Pedidos Recorrentes (recompra automática PJ) ───────────────────── await execute(` CREATE TABLE IF NOT EXISTS pedidos_recorrentes ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL DEFAULT 1, user_id INT REFERENCES users(id) ON DELETE CASCADE, cliente_id INT REFERENCES clientes(id) ON DELETE SET NULL, nome VARCHAR(200) NOT NULL DEFAULT 'Pedido Recorrente', itens JSONB NOT NULL DEFAULT '[]', frequencia VARCHAR(20) NOT NULL DEFAULT 'mensal', proximo_em DATE NOT NULL, ativo BOOLEAN NOT NULL DEFAULT TRUE, pausado BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ) `) await execute(`CREATE INDEX IF NOT EXISTS idx_rec_tenant ON pedidos_recorrentes(tenant_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_rec_user ON pedidos_recorrentes(user_id)`) await execute(`CREATE INDEX IF NOT EXISTS idx_rec_prox ON pedidos_recorrentes(proximo_em) WHERE ativo = TRUE AND pausado = FALSE`) console.log('✅ Módulo Cotação Online — tabelas verificadas') // ── Colaboradores Bridge — vínculo WA-Inbox + FK de embalador ──────────── await execute(`ALTER TABLE accounts ADD COLUMN IF NOT EXISTS nw_team_id TEXT`) await execute(`ALTER TABLE accounts ADD COLUMN IF NOT EXISTS nw_sector_id TEXT`) await execute(`ALTER TABLE pedidos ADD COLUMN IF NOT EXISTS embalador_id BIGINT REFERENCES accounts(id) ON DELETE SET NULL`) await execute(`ALTER TABLE pedidos ADD COLUMN IF NOT EXISTS expedidor_id BIGINT REFERENCES accounts(id) ON DELETE SET NULL`) await execute(`CREATE INDEX IF NOT EXISTS idx_pedidos_embalador ON pedidos(embalador_id) WHERE embalador_id IS NOT NULL`) await execute(`CREATE INDEX IF NOT EXISTS idx_pedidos_expedidor ON pedidos(expedidor_id) WHERE expedidor_id IS NOT NULL`) // Migração best-effort: texto livre → FK para registros já existentes await execute(` UPDATE pedidos p SET embalador_id = a.id FROM accounts a WHERE a.tenant_id = p.tenant_id AND LOWER(TRIM(a.name)) = LOWER(TRIM(p.embalador)) AND p.embalador IS NOT NULL AND p.embalador_id IS NULL `) console.log('✅ Colaboradores Bridge — nw_team_id, embalador_id, expedidor_id verificados') // ── Cotação Funções Operacionais — separador/auditor/fiscal ────────────── // Cada etapa do fluxo (separação, embalagem, auditoria, fiscal, expedição) é // atendida pelos colaboradores cujo setor (sectors.name via team_members) // bate com o nome da função. Sem tabela de mapeamento — o setor já é a fonte // da verdade, gerenciado em /admin-v2/equipe. await execute(`ALTER TABLE pedidos ADD COLUMN IF NOT EXISTS separador_id BIGINT REFERENCES accounts(id) ON DELETE SET NULL`) await execute(`ALTER TABLE pedidos ADD COLUMN IF NOT EXISTS auditor_id BIGINT REFERENCES accounts(id) ON DELETE SET NULL`) await execute(`ALTER TABLE pedidos ADD COLUMN IF NOT EXISTS fiscal_id BIGINT REFERENCES accounts(id) ON DELETE SET NULL`) await execute(`CREATE INDEX IF NOT EXISTS idx_pedidos_separador ON pedidos(separador_id) WHERE separador_id IS NOT NULL`) await execute(`CREATE INDEX IF NOT EXISTS idx_pedidos_auditor ON pedidos(auditor_id) WHERE auditor_id IS NOT NULL`) await execute(`CREATE INDEX IF NOT EXISTS idx_pedidos_fiscal ON pedidos(fiscal_id) WHERE fiscal_id IS NOT NULL`) console.log('✅ Cotação Funções — separador_id, auditor_id, fiscal_id verificados') // ── Cupom fiscal + saída do entregador ──────────────────────────────────── // Sem integração SEFAZ: registro manual do número/chave de acesso do cupom // emitido externamente (PDV/ECF). Permite emitir em qualquer fase pós-pagto; // a coluna emitido_por mantém quem clicou no botão para auditoria. // expedidor_saida_em é setado automaticamente quando o pedido entra em // 'expedicao' — é o "horário de saída do entregador". await execute(`ALTER TABLE pedidos ADD COLUMN IF NOT EXISTS cupom_numero VARCHAR(60)`) await execute(`ALTER TABLE pedidos ADD COLUMN IF NOT EXISTS cupom_chave VARCHAR(64)`) await execute(`ALTER TABLE pedidos ADD COLUMN IF NOT EXISTS cupom_emitido_em TIMESTAMPTZ`) await execute(`ALTER TABLE pedidos ADD COLUMN IF NOT EXISTS cupom_emitido_por BIGINT REFERENCES accounts(id) ON DELETE SET NULL`) await execute(`ALTER TABLE pedidos ADD COLUMN IF NOT EXISTS expedidor_saida_em TIMESTAMPTZ`) console.log('✅ Cotação Fiscal — cupom_numero, cupom_chave, cupom_emitido_em, cupom_emitido_por, expedidor_saida_em verificados') // ── Captive Portal Integration ───────────────────────────────────────────── // Armazena o session_id gerado pelo sistema-local para rastrear qual usuário // confirmou a landing page e liberou a internet. await execute(`ALTER TABLE users ADD COLUMN IF NOT EXISTS portal_session_id VARCHAR(64)`) await execute(`CREATE INDEX IF NOT EXISTS idx_users_portal_session ON users(portal_session_id) WHERE portal_session_id IS NOT NULL`) console.log('✅ Captive Portal — coluna portal_session_id verificada') } // saveDatabase é no-op no PostgreSQL (era necessário apenas no sql.js) function saveDatabase() {} module.exports = { initialize, saveDatabase, // Para compatibilidade com plugins que ainda usam a API síncrona do sql.js: runQuery: (...args) => { throw new Error('Use execute() do postgres.js diretamente') }, getOne: (...args) => { throw new Error('Use queryOne() do postgres.js diretamente') }, getAll: (...args) => { throw new Error('Use query() do postgres.js diretamente') }, }