ERP Ferreira

Materiais para Construção — Sistema de Gestão com ANIMUS IA

Node.js · Railway SQLite · Railpack n8n · Groq · Gemini WhatsApp · Evolution API 326 produtos · 8 categorias

Visão Geral

O ERP Ferreira é o sistema de gestão da empresa Ferreira Materiais para Construção, hospedado na Railway. Combina um ERP completo (catálogo, estoque, clientes, orçamentos, vendas) com dois agentes de IA:

Empresa

CampoValor
NomeFerreira Materiais para Construção
SegmentoAlumínios, Esquadrias, Portas, Janelas, Vidros, Ferragens
WhatsApp comercial+55 13 98805-9066
Horário técnico8h–18h seg–sex, 8h–12h sáb
ERP URLerp-ferreira-production.up.railway.app
Acesso inicialadmin / admin123

Stack Técnica

ComponenteTecnologiaOnde
ERP BackendNode.js (sem framework indicado — 1 dependência)Railway · erp-ferreira
Banco de dadosSQLite (embutido no container)Railway · volume persistente
BuildRailpack (detecta Node → npm install)Railway CI
RuntimeNode 24.21.0 (mise)Railway
ANIMUS ERPn8n workflow kBqcUrXg8WhD8uk0Railway · nexus-connecta
LLM primárioGroq — openai/gpt-oss-120bGroq Cloud
LLM fallbackGemini — gemini-3.1-flash-liteGoogle AI
WhatsAppEvolution API + n8n workflow Yeb01bUwkg1ucV1PRailway · nexus-connecta
Integração NexusWorker CF proxy /ferreiranexus-empresa.app-ai.workers.dev

Arquitetura

Fluxo do ERP

Browser / Admin
→
GET/POST /api/*
→
Node.js ERP
→
SQLite (produtos, clientes, vendas)

Fluxo do ANIMUS ERP

ERP UI (chat)
→
POST /webhook/ferreira-animus-v1
→
n8n ANIMUS Ferreira
→
Groq (primário)
→
Sentinela valida JSON
→
Resposta validada

Fluxo do WhatsApp Bot

Cliente WhatsApp
→
Evolution API
→
n8n Workflow Ferreira
→
Groq LLM + contexto
→
Atualiza lead SQLite
→
Responde WhatsApp

Integração com Nexus OFFICE

CF Worker /ferreira/*
→
Proxy → ERP Railway
→
Nexus ERP_DB (D1)

O Worker Cloudflare expõe os endpoints do ERP Ferreira sob /ferreira para o painel Nexus OFFICE (setores Ferreira no pixel art).

Modelo de Dados — SQLite

Produtos / Estoque

CREATE TABLE produtos (
  id           INTEGER PRIMARY KEY AUTOINCREMENT,
  codigo       TEXT UNIQUE,         -- código interno (ex: "203")
  nome         TEXT NOT NULL,
  categoria_id INTEGER,
  unidade      TEXT,                   -- UN, MT, KG, HR, L, M2, M3, PC
  preco_custo  INTEGER,                -- em centavos (3 decimais p/ metro/hora/kg)
  preco_venda  INTEGER NOT NULL,     -- em centavos
  estoque      NUMERIC DEFAULT 0,
  estoque_min  NUMERIC DEFAULT 0,
  imagem       TEXT,
  descricao    TEXT,
  ativo        INTEGER DEFAULT 1,
  criado_em    TEXT
);

Categorias

CREATE TABLE categorias (
  id   INTEGER PRIMARY KEY AUTOINCREMENT,
  nome TEXT UNIQUE NOT NULL,
  ordem INTEGER
);
-- 8 categorias confirmadas via API pública:
-- Portas, Janelas, Venezianas, Vidros, Acessórios, Serviços, Ferragens, Elétrica

Clientes

CREATE TABLE clientes (
  id        INTEGER PRIMARY KEY AUTOINCREMENT,
  nome      TEXT NOT NULL,
  telefone  TEXT,
  email     TEXT,
  endereco  TEXT,
  bairro    TEXT,
  criado_em TEXT
);

Orçamentos

CREATE TABLE orcamentos (
  id         INTEGER PRIMARY KEY AUTOINCREMENT,
  cliente_id INTEGER REFERENCES clientes(id),
  status     TEXT,    -- rascunho, enviado, aprovado, recusado, expirado
  total      INTEGER, -- em centavos
  criado_em  TEXT,
  validade   TEXT
);
CREATE TABLE orcamento_itens (
  id           INTEGER PRIMARY KEY,
  orcamento_id INTEGER REFERENCES orcamentos(id),
  produto_id   INTEGER REFERENCES produtos(id),
  quantidade   NUMERIC,
  preco_unit   INTEGER  -- snapshot do preço no momento do orçamento
);

Vendas

CREATE TABLE vendas (
  id           INTEGER PRIMARY KEY AUTOINCREMENT,
  cliente_id   INTEGER REFERENCES clientes(id),
  status       TEXT,    -- pendente, pago, cancelado
  total        INTEGER,
  criado_em    TEXT,
  recebido_em  TEXT     -- recebimento manual
);
CREATE TABLE venda_itens (
  id         INTEGER PRIMARY KEY,
  venda_id   INTEGER REFERENCES vendas(id),
  produto_id INTEGER REFERENCES produtos(id),
  quantidade NUMERIC,
  preco_unit INTEGER
);
-- Venda fecha com baixa atômica de estoque (transação SQLite)
Regras de negócio importantes:
• Valores monetários: sempre em centavos (÷100 para R$)
• Unidades MT/HR/KG: aceitam até 3 casas decimais
• Unidade PC: somente inteiros
• Baixa de estoque: atômica — rejeita se saldo insuficiente
• Revisão otimista de produto: controle de concorrência via versão
• 326 produtos importados do ERP original; 20 com imagem de referência

API Endpoints

Base: https://erp-ferreira-production.up.railway.app

🌐 Públicos (sem autenticação — confirmados)

GET/api/produtos — lista todos os produtos com id, codigo, nome, categoria_id, unidade, preco_custo, preco_venda, estoque, estoque_min, imagem, descricao, ativo, criado_em, categoria
GET/api/produtos/:id — produto específico
GET/api/categorias — array com id, nome, ordem — 8 categorias: Portas, Janelas, Venezianas, Vidros, Acessórios, Serviços, Ferragens, Elétrica
GET/api/config — configuração da loja: { loja_nome, loja_documento, loja_telefone, loja_endereco, loja_cidade }
GET/api/produtos?categoria_id=N — filtro por categoria
GET/api/produtos?busca=termo — busca por nome/código

🔒 Autenticados — requer sessão cookie (login admin)

GET/api/clientes — lista clientes (401 sem auth)
POST/api/clientes — cria cliente { nome, telefone, email, endereco, bairro }
GET/api/orcamentos — lista orçamentos (401 sem auth)
POST/api/orcamentos — cria orçamento com itens
PUT/api/orcamentos/:id/status — atualiza status
GET/api/vendas — lista vendas (401 sem auth)
POST/api/vendas — cria venda + baixa atômica de estoque
GET/api/usuarios — lista usuários (401 sem auth)
GET/api/dashboard — dashboard com KPIs (401 sem auth)
POST/api/produtos — cadastra produto (admin)
PUT/api/produtos/:id — atualiza produto (revisão otimista)

❌ Rotas não existentes (404)

/api/financeiro — não implementado (financeiro é calculado em /api/dashboard)
/api/relatorios — não implementado
/api/estoque — estoque está embutido em cada produto no /api/produtos
/api/health — não implementado (ERP não expõe healthcheck)

Resposta de erro padrão

{ "erro": "Rota não encontrada: GET /api/health" }  // 404
{ "erro": "Saldo insuficiente para o produto X" }    // 422
{ "erro": "Revisão expirada — produto foi atualizado" } // 409

Autenticação do ERP

POST/api/auth/login — body: { username, password } → sessão cookie
POST/api/auth/logout — encerra sessão

Usuários e Permissões

ContaPapelAcesso
admin / admin123AdministradorTudo — cadastro, escrita, auditoria
Usuário FerreiraOperadorLeitura + operações (sem deletar)
Proprietário ConnectaOwnerVia SSO Connecta Central

Autorização no Nexus (Cloudflare Worker)

O Worker verifica o módulo lojas na empresa ferreira antes de proxiar para o ERP Railway. Escritas exigem papel admin.

Módulos do ERP

📦 Catálogo & Estoque

👥 Clientes & Leads

📋 Orçamentos

💰 Vendas & Financeiro

ANIMUS ERP — Assistente do Sistema

Workflow n8n: kBqcUrXg8WhD8uk0 · Webhook: POST /webhook/ferreira-animus-v1

Autenticação do webhook: header CONNECTA | Ingresso Evolution (credential id qv57h8FTVyD7lMTi). Não aceita chamadas sem autenticação.

Contrato de Entrada

{
  "contract_version": "ferreira.assistant.v1",  // obrigatório e exato
  "company": "ferreira",                      // obrigatório e exato
  "message": "qual o preço da caixa 1000L?", // máx 1200 chars
  "products": [...],                        // array com produtos do contexto (máx 20)
  "summary": "...",                          // resumo do carrinho/contexto
  "memory": [...],                           // histórico da conversa
  "time": "2026-11-04T10:00:00"
}

Contrato de Saída (JSON validado)

{
  "action": "help",               // sempre "help" para o ERP
  "conversation": "",            // vazio (campo de protocolo)
  "reason": "Consulta de preço", // máx 500 chars — para auditoria
  "reply": "A caixa 1000L custa R$450,00..." // máx 1200 chars — para o usuário
}

Pipeline de IA (Groq + Gemini Fallback)

Webhook entry
→
CORE: valida contrato
→
Groq (gpt-oss-120b, temp 0.1)
→
SENTINELA: valida JSON
↙ inválido
Gemini flash-lite (fallback)
→
SENTINELA: valida
→
Resposta validada

Validação SENTINELA

O nó SENTINELA rejeita qualquer resposta que:

Regras do Sistema Prompt

ANIMUS WhatsApp — Bot Comercial

Workflow n8n: Yeb01bUwkg1ucV1P · Integração via Evolution API · Grupo WhatsApp: 120363430918159913@g.us

Capacidades

Extração Automática de Lead

CampoTriggerExemplo
nome"sou X" / "me chamo X""sou João" → nome=João
bairro"moro em/no X" / nome de bairro"Jardim Acapulco" → bairro=Jardim Acapulco
problemaDescrição de defeito"caixa rachada" / "fagulha na tomada"
interesseIntenção de compra"quero comprar caixa 1000L"
urgenciaPalavras-chave de riscofagulha/fumaça/choque/vazamento grave → ALTA
scoreIntenção de açãoQUENTE=quer orçar/agendar; MORNO=pesquisando; FRIO=só dúvida

Configuração de Negócio (cfg)

{
  "horario": "8h-18h seg-sex, 8h-12h sab",
  "visita_preco": "R$200 (abatida se fechar)",
  "mao_obra_preco": "a partir R$150"
}
// Tabela de preços aproximados (confirmação com Ferreira no horário comercial):
// Caixa 500L: R$270–310 | 1000L: R$410–510
// Tubo 25mm: R$18–25 | Registro 25mm: R$22–35
// Fio 2.5mm 100m: R$150–220 | Disjuntor: R$15–30 | Chuveiro: R$70–150
// Janela 1x1: R$450–650 | Porta correr 2.10x2.00: R$1200–1800
// Box 8mm: R$650–950

Pipeline n8n — Fluxo Interno WhatsApp

Nós do Workflow Yeb01bUwkg1ucV1P

NóTipoFunção
Entrada EvolutionWebhookRecebe mensagem do WhatsApp via Evolution API
NormalizarCodeExtrai: numero, message, instance, isImage, pushName, hasMedia
Controle FerreiraCodePermite /pausar e /iniciar bot (só número do Ferreira: +55 13 98805-9066)
É Grupo?IfFiltra para só o grupo Ferreira (120363430918159913@g.us)
Carregar LeadDB QueryBusca lead por telefone no SQLite
Load HistoryDB QueryCarrega histórico de conversa
Carregar ConfigDB QueryCarrega configurações de negócio (horário, preços)
Build Contexto FerreiraCodeMonta system prompt completo + mensagens do histórico
Groq LLMHTTP RequestChama Groq API (openai/gpt-oss-120b, temp 0.1, max 1500 tokens)
Parsear GroqCodeExtrai JSON da resposta, valida campos
Classificação de SegurançaCodeReutilizado do ANIMUS Connecta — detecta pedido de humano, urgência
Atualizar LeadDB QuerySalva/atualiza lead com campos extraídos
Salvar HistóricoDB QueryAppenda turno ao histórico da conversa
Enviar WhatsAppHTTP RequestResponde via Evolution API
Human TakeoverIf + NotifyNotifica o Ferreira se human_takeover=true

Integração com NEXUS OFFICE

O Ferreira aparece no painel NEXUS como um andar separado. Dados fluem via:

Endpoints do Worker CF para o Ferreira

GET/api/connecta/health → inclui ferreira-erp e ferreira-imoveis no healthcheck
GET/api/connecta/empresas → retorna empresa ferreira com status de serviços

ERP_DB (D1) — atividades do Ferreira

O Nexus grava em erp_activities com companyId = "ferreira" quando eventos chegam via ingress. O painel Sentinela mostra métricas agregadas das duas empresas.

ANIMUS Ferreira no Nexus OFFICE (pixel art)

O agente aparece na sala core:gestao como personagem. Status baseado nas execuções do workflow kBqcUrXg8WhD8uk0 consultadas via API n8n.

Workflows n8n — Ferreira

IDNomeTriggerFunção
kBqcUrXg8WhD8uk0 FERREIRA | ANIMUS · Atendimento do ERP POST /webhook/ferreira-animus-v1 ANIMUS para o ERP: Groq + Gemini fallback, contrato ferreira.assistant.v1
Yeb01bUwkg1ucV1P FERREIRA | Bot WhatsApp Evolution API Webhook Atendimento 24h, qualificação de leads, /pausar /iniciar

WhatsApp Bot — Configuração

Instância Evolution API

Conectada ao número +55 13 98805-9066 (comercial do Ferreira). Webhooks apontam para o n8n em Railway (nexus-connecta).

Credentials n8n

CredentialIDUso
CONNECTA | GroqRYItbqdE4ckIi1mnLLM primário (todos os workflows Ferreira)
CONNECTA | Gemini1Y4HSBdoSmMLDL9SLLM fallback do ANIMUS ERP
CONNECTA | Ingresso Evolutionqv57h8FTVyD7lMTiAuth do webhook ferreira-animus-v1

Controle do Bot WhatsApp

O Ferreira (número +55 13 98805-9066) pode controlar o bot enviando comandos no grupo WhatsApp:

ComandoEfeitoResposta do bot
/iniciarAtiva o bot — começa a responder clientes"✅ Bot ativado! Já estou respondendo os clientes."
/pausarPausa o bot — não responde clientes (Ferreira atende manualmente)"⏸️ Bot pausado! Não vou responder até /iniciar."
Implementação: o estado é armazenado em globalThis.__botAtivo no container n8n. Reiniciar o container reseta para ativo=true.

Gestão de Leads

Estágios

NOVO
→
QUALIFICANDO
→
OPORTUNIDADE
→
ORÇAMENTO
→
AGENDAMENTO
→
EM_ATENDIMENTO
→
FECHADO

Score

ScoreCritérioAção sugerida
QUENTEQuer orçar, agendar ou é urgenteContato imediato do Ferreira
MORNOPesquisando, sem decisãoFollow-up em 24h
FRIOSó dúvida, sem intenção claraManter no funil, responder info

Pendências conhecidas

Deploy

ERP Railway

Projeto: erp-ferreira (id: 032aeeca-d231-4d10-aa40-3ac1b5340ed9)

Serviço: erp-ferreira (id: 6920f73f-7e57-4149-82e1-2c31e6b4b7b9)

Build: Railpack · Node 24.21.0 · npm install · 1 dependência

# Verificar status
railway status --service erp-ferreira

# Ver logs de deploy
railway logs --service erp-ferreira

# Deploy manual (a partir da pasta do ERP)
railway up

Variáveis de Ambiente

0 variáveis definidas no painel Railway (tudo hardcoded ou no container). Para produção real, configurar via Railway Variables:

Variável sugeridaDescrição
SESSION_SECRETSecret para sessões HTTP
ADMIN_PASSWORDSubstituir o admin123 de demo
DB_PATHPath do SQLite (default: /app/data/erp.db)
⚠️ Ação necessária: trocar a senha admin123 antes de usar em produção. Configurar volume persistente no Railway para o SQLite sobreviver a redeploys.