1163 lines
60 KiB
JavaScript
1163 lines
60 KiB
JavaScript
'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') },
|
|
}
|