PHP 8.3 & Slim 4 Dual PostgreSQL Vanilla JS SPA REST API + OpenAPI

Guia de Engenharia: Módulo Representante

Bem-vindo ao manual técnico de arquitetura do Módulo de Representante Comercial (crm_rep) do CRM Brasil Componentes. Este documento foi escrito de desenvolvedor para desenvolvedor para você entender cada decisão de design, modelo de dados híbrido, pegadinhas de framework e regras de segurança sem perder tempo descobrindo na tentativa e erro.

4
Submódulos Integrados
2
Bancos PostgreSQL
15/15
Testes Backend (100%)
5 Etapas
Validação no ERP
01

O Padrão Dual Database (ERP Legado + CRM Local)

A espinha dorsal deste projeto baseia-se em um modelo híbrido. A empresa possui um ERP robusto em PostgreSQL rodando em produção há anos (banco external). Entretanto, o CRM precisa de recursos adicionais que o ERP não possui (Kanban, dados de compradores da Receita Federal, flags de fixação de cartões, logs de auditoria).

🚨
REGRA DE OURO #1 DA EQUIPE: NUNCA GRAVE DADOS NO BANCO DO ERP!
O banco do ERP (external) deve ser tratado como estritamente READ-ONLY para o CRM. Qualquer escrita, enriquecimento cadastral ou alteração de rotas deve ser gravada no Banco Local do CRM (default). Se você tentar rodar um INSERT ou UPDATE na tabela cliente ou pedidovenda, você colocará a operação fabril em risco!
🏭 ERP Externo (brcomp / external)
Apenas Leituras (SELECT):
• cliente, cidade, classe
• pedidovenda, pedidovendaitem
• valida_pedido, validacao
• transportadora, produto
PDO Read ────────► CRM BACKEND ◄──────── PDO Read/Write
💾 CRM Local (crm_brc / default)
Leituras e Escritas (CRUD):
• crm_client_info (Aniversário, OBS, CNPJ)
• crm_kanban_tasks (Quadro de tarefas)
• crm_user_seller_mapping (Vínculos)
• crm_system_logs (Auditoria)

No código, o acesso aos bancos é centralizado na classe App\Connection\DatabaseConnection:

// Conexão com o Banco Local do CRM (Tabelas crm_*)
$localDb = (new DatabaseConnection())->getPrimaryConnection(); // ou getConnection('default')

// Conexão com o ERP Externo (Apenas Leitura de Clientes, Pedidos e Validações)
$erpDb   = (new DatabaseConnection())->getConnection('external');
02

A Regra Estrita: Zero SQL no Controller

Nosso padrão arquitetural exige que nenhum comando SQL (SELECT, INSERT, UPDATE, DELETE) nem conexão PDO exista dentro de Controllers. Os Controllers devem ser camadas finas responsáveis apenas por:

// ❌ ERRADO: Fazer query direta no Controller
$stmt = $db->prepare("SELECT area_code FROM crm_user_seller_mapping WHERE email = :e");
$stmt->execute([':e' => $userEmail]);

// ✅ CORRETO: Chamar método especializado no Model
$userArea = $this->model->getUserAreaCodeByEmail($userEmail);
03

Autenticação JWT, Middleware e Comunicação Frontend

O backend Slim 4 protege todos os endpoints via App\Security\AuthMiddleware. O token JWT trafega no cabeçalho Authorization: Bearer <token>. No frontend, as chamadas são unificadas na função crmApiRequest() em frontend/assets/javascript/rep_modules.js.

Papel (Role) Identificador JWT Permissões de Acesso Regra de Área Comercial
Representante crm_rep Acesso ao Painel, Clientes da Área, Kanban próprio e Pedidos da Área Travado estritamente na sua própria área (ex: Área 9)
Supervisor crm_sup Acesso à gestão de equipes, relatórios consolidados e metas Livre para filtrar qualquer área ou vendedor
Gerente crm_ger Visão executiva, transferência de carteira e metas anuais Livre para filtrar qualquer área ou vendedor
Administrador crm_admin Controle total, mapeamentos, logs de sistema e testes Superusuário sem restrições
04

Meus Clientes & Enriquecimento Local (clients.php)

O módulo de clientes une os dados cadastrais do ERP com o enriquecimento local do CRM:

  1. Busca Paginada no ERP: GET /api/v1/clients/{cd_area}?limit=50&status=ATIVO&search=termo (traz nome, CNPJ, cidade, classe e código da empresa c.i_cdcliente AS cd_cliente).
  2. Enriquecimento de Dados no CRM Local: Ao abrir o modal de detalhes, o CRM consulta crm_client_info por CNPJ. Caso não exista, o usuário pode clicar em "Buscar Dados CNPJ" (consulta API da Receita Federal) e salvar anotações, comprador e aniversário via POST /api/v1/clients/info.
  3. Histórico de Pedidos & PDF com Fotos: GET /api/v1/clients/{cd_cliente}/orders busca os pedidos anteriores. Clicando em "📄 Visualizar PDF", o sistema monta o espelho oficial com fotos dos produtos via CDN https://brasilcompautomotivos.com.br/service/imagem/{codigo_item}?size=p.
05

Rota de Prioridades da Semana (Kanban - tasks.php)

O Kanban estilo Trello permite ao representante organizar sua esteira comercial em 4 colunas: a_atender ➔ em_atendimento ➔ aguardando_retorno ➔ concluido.

🔥
A GRANDE PEGADINHA: POR QUE O CARTÃO VOLTAVA PARA A COLUNA ANTERIOR?
Causa Raiz: No Slim 4 e no PHP nativo, requisições com método PATCH e PUT enviadas como formulário comum (application/x-www-form-urlencoded) NÃO preenchem a superglobal $_POST!
Como o frontend enviava PATCH /api/v1/kanban/tasks/1/status sem cabeçalho JSON explícito, o json_decode($body) no backend retornava null.
O backend retornava erro 400 (Coluna não informada), e o frontend disparava o rollback automático (loadTasks()), fazendo o cartão voltar magicamente para onde estava!

Como resolvemos definitivamente para que nunca mais quebre:

// Garante Content-Type e JSON.stringify para todos os métodos com payload
const upperMethod = (method || 'GET').toUpperCase();
const hasPayload = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(upperMethod) && data !== null;

if (hasPayload) {
    headers['Content-Type'] = 'application/json';
}

$.ajax({
    url: baseUrl + endpoint,
    type: upperMethod,
    headers: headers,
    data: hasPayload ? JSON.stringify(data) : data, // <-- Crucial!
    dataType: 'json',
    ...
});
// Extrai o payload de forma resiliente em qualquer cenário
private function getRequestData(Request $request): array
{
    $parsed = $request->getParsedBody();
    if (!empty($parsed) && is_array($parsed)) {
        return $parsed;
    }

    $raw = (string)$request->getBody();
    $decoded = json_decode($raw, true);
    if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
        return $decoded;
    }

    // Fallback se alguém enviar form-encoded em PATCH
    $form = [];
    parse_str($raw, $form);
    return $form;
}
💡
EVITE ACÚMULO DE LISTENERS DE DRAG & DROP
Nunca chame listEl.addEventListener('drop', ...) dentro de renderColumns()! Se você renderizar as colunas 5 vezes, haverá 5 listeners executando no mesmo drop, disparando 5 requisições simultâneas. No nosso código, os listeners dos dropzones são registrados apenas uma vez em initKanbanModule()!
06

Pedidos Enviados & Validações do ERP (orders.php)

Permite acompanhar todos os pedidos da área nos últimos 60 dias (janela padrão), com as 5 etapas da esteira interna do ERP:

Etapa Código Validação Permissão Autorização Descrição no ERP Situações Possíveis
Etapa 1 i_cdvalidacao = 9 i_cdpermautorizacao = 20 LIBERAÇÃO DO PEDIDO Autorizado (A) / Liberado (L) / Aguardando (N)
Etapa 2 i_cdvalidacao = 13 i_cdpermautorizacao = 17 SEPARAÇÃO DE ITENS Autorizado (A) / Liberado (L) / Aguardando (N)
Etapa 3 i_cdvalidacao = 10 i_cdpermautorizacao = 25 PENDÊNCIAS DE ESTOQUE Liberado (L) / Em Análise / Pendente (N)
Etapa 4 i_cdvalidacao = 4 i_cdpermautorizacao = 9 FATURAMENTO Autorizado (A) / Faturado (L)
Etapa 5 - (Logística) pedidovenda.d_despacho DESPACHO LOGÍSTICO & TRANSPORTE Despachado (Data gravada) / Aguardando Coleta
🔒
SEGURANÇA RBAC: POR QUE O REPRESENTANTE NÃO PODE MUDAR A ÁREA?
Um representante comercial só pode enxergar dados comerciais da sua própria carteira. Portanto:
  • Na Interface: O campo de área é renderizado como uma trava fixa com cadeado (Área 9 🔒) e não como um input editável.
  • No Backend: O método PedidosController::getAreaOrders inspeciona o token JWT. Se o usuário for crm_rep, a rota ignora qualquer código de área enviado na URL e força estritamente o código do login (via PedidosModel::getUserAreaCodeByEmail).
  • Supervisores e Gerentes: Se o token possuir crm_sup, crm_ger ou crm_admin, o filtro de área é desbloqueado.
07

Guia de Testes Automatizados & Troubleshooting

Sempre que alterar qualquer model, controller ou rota, você DEVE rodar a suíte de testes de integração para garantir que nenhuma regressão ocorra:

# Executa todos os 15 testes de integração ponta a ponta
php backend/src/test/test_backend.php

# Saída esperada (100% de sucesso):
[PASS] 1. Swagger JSON endpoint (200 OK, valid OpenAPI)
[PASS] 2. Swagger UI endpoint (200 OK)
[PASS] 3. Security: Unauthorized without token (401)
[PASS] 4. Security: Authorized crm_sup accessed /teams (200 OK)
[PASS] 5. RBAC: Forbidden for crm_rep on /admin/logs (403)
[PASS] 6. RBAC: Allowed for crm_admin on /admin/logs (200 OK)
[PASS] 7. Client Enrichment: POST /api/v1/clients/info saved to local database (200 OK)
[PASS] 8. Client Enrichment: GET /api/v1/clients/{cnpj}/info retrieved from local DB (200 OK)
[PASS] 9. Client Pagination & Filter: GET /api/v1/clients/9 (200 OK)
[PASS] 10. Client Orders: GET /api/v1/clients/21869/orders (200 OK)
[PASS] 11. Order Details & Modern PDF: GET /api/v1/orders/9695 and /pdf (200 OK)
[PASS] 12. Kanban Tasks: Full CRUD, Drag & Drop status update, and Summary (200/201 OK)
[PASS] 13. Kanban Client Autocomplete: GET /api/v1/kanban/clients from ERP (200 OK)
[PASS] 14. Area Orders: GET /api/v1/orders/area/349 retrieved real orders with KPIs (200 OK)
[PASS] 15. Order Validations: GET /api/v1/orders/34900227/validations retrieved 5 steps (200 OK)

ALL BACKEND TESTS PASSED SUCCESSFULLY!
🚀
CHECKLIST RÁPIDO PARA SUBIR EM PRODUÇÃO OU ESCALAR O PROJETO:
1. Migrações: Rodar vendor/bin/phinx migrate para garantir todas as colunas locais criadas.
2. Variáveis de Ambiente: Conferir .env com credenciais do banco local e ERP.
3. Swagger OpenAPI: Acessar http://10.1.1.133:8001/docs para testar qualquer rota interativamente.
4. Logs: Em caso de exceções, checar a tabela local crm_system_logs.