Sumário
Reiniciando Sistemas: Documentação do Sistema
Versão documentada: CakePHP 3.4.14 / PHP 7.4.33 / MariaDB 11.8.8 / AdminLTE + Bootstrap 3.3.7 + jQuery 3.5.1
Organização deste documento: um arquivo único, dividido em blocos por módulo ou categoria funcional. O volume atual não justifica arquivos separados por pasta de módulo. Blocos de funcionalidades exclusivas do Super estão reunidos em seção própria ao final, em vez de distribuídos em cada módulo.
Sumário
- Stack e Infraestrutura
- Arquitetura Multi-Tenant
- Planos de Assinatura
- Sistema de Permissões e Acesso
- Fluxo de Login e Sessão
- Organização do Código
- Módulo: Contas a Pagar/Receber (Moviments)
- Módulo: Extratos Bancários (MovimentBanks)
- Módulo: Extratos de Caixa (MovimentBoxes)
- Módulo: Lançamentos de Cartão (MovimentCards)
- Módulo: Transferências (Transfers)
- Módulo: Planejamentos
- Módulo: Saldos Financeiros (Balances)
- Módulo: Produtos e Cadastros de Estoque
- Módulo: Compras (Purchases)
- Módulo: Vendas (Sells)
- Módulo: Requisições (Requisitions)
- Módulo: Inventários (Inventories)
- Módulo: Industrialização (Industrializations)
- Módulo: Saldos de Estoque (StockBalances)
- Módulo: Notas Fiscais (Invoices)
- Cadastros de Apoio Financeiro
- Cadastros de Apoio de Estoque
- Cadastros de Pessoas (Customers, Providers, Transporters)
- Assinaturas (Stripe)
- Tarefas Automáticas (Cron)
- Particularidades e Regras de Negócio Transversais
- Telas Complementares do Dashboard (drill-down)
- Padrões Visuais e de UI
- Segurança
- Funcionalidades Exclusivas do Usuário Super
1. Stack e Infraestrutura
| Componente | Versão |
|---|---|
| Linguagem | PHP 7.4.33 |
| Framework | CakePHP 3.4.14 |
| Banco de dados | MariaDB 11.8.8 (banco principal: reinicia_financeiro) |
| Template engine | CTP (PHP nativo, server-rendered, sem build de front-end) |
| UI | AdminLTE + Bootstrap 3.3.7 + jQuery 3.5.1 |
| Servidor local | Apache (XAMPP) |
| Servidor de produção | nginx (.htaccess ignorado em produção) |
| Internacionalização | Cake\I18n\I18n; strings de interface via __() |
| Idioma do projeto | Português (comentários, commits, UI) |
Além do português (idioma padrão da interface), há uma versão em inglês (en_US) revisada das strings de interface em src/Locale/en_US/default.po.
O banco de testes (reinicia_financeiro_test) é separado do principal. Scripts de criação em Documents/cria banco de teste.bat. Dumps e scripts de restauração em Documents/.
Migrações de banco executadas em produção ficam em webroot/database/, nomeadas por data (YYYYMMDD descrição.sql). Após executadas em produção, os arquivos são apagados. Nunca editar um arquivo já executado, criar sempre um novo com a data atual. Após qualquer ALTER TABLE, executar php bin/cake.php orm_cache clear para que o ORM reconheça as novas colunas.
2. Arquitetura Multi-Tenant
O sistema usa multi-tenancy por "perfil" (parameters). Cada empresa, pessoa ou contexto de uso é um registro na tabela parameters. O perfil ativo é armazenado na sessão como sessionParameterControl.
Um usuário pode pertencer a mais de um perfil. A relação é mantida em users_parameters. A troca de perfil é feita em UsersParametersController::changeParameter. O sistema identifica o último perfil usado pelo usuário e o reativa na sessão após o login.
Quase toda tabela tem a coluna parameters_id, e toda query de negócio deve filtrá-la pelo valor da sessão. O padrão dominante é:
$this->Model->findByParametersId($this->request->Session()->read('sessionParameterControl'))
A ausência de parameters_id em uma query de mutação é um ponto de atenção de IDOR (acesso cross-perfil). Ver seção de Segurança.
3. Planos de Assinatura
Apenas dois planos ativos:
| ID | Nome | Descrição |
|---|---|---|
| 1 | Pessoal | Financeiro simplificado, sem módulos de estoque/faturamento. |
| 3 | Empresa | Acesso completo a financeiro e estoque. |
Os planos 2 (Simples) e 4 (Limitado) foram descontinuados. Os IDs não foram renumerados.
O plano do perfil é determinado pelo campo Parameters.plans_id. Durante o beforeFilter de cada request, SystemFunctionsComponent::validatePlans() verifica o plano e toma as seguintes ações:
- Plano Pessoal (1): redireciona as actions
index,add,edit,view,editBaixado,lowereportFormdos módulos financeiros para suas variantes*Simple(indexSimple,addSimple, etc.), que renderizam templates emTemplate/<Modulo>/simple/. - Plano Empresa (3): não acessa telas
*Simple. Se uma URL*Simplefor acessada diretamente, é redirecionada para a versão completa equivalente.
Nomenclaturas que diferem entre planos:
| Tabela/Módulo | Nome no Plano Pessoal | Nome no Plano Empresa |
|---|---|---|
costs |
Categorias | Centro de Custos |
boxes |
Carteira | Caixa |
Restrições de controller por plano (checagem pelo validatePlanController, válida inclusive para o Super). Os dois planos usam modelos opostos:
-
Plano Pessoal (1), allowlist (
planControllers[1]), modelo default-deny: só pode acessar os controllers explicitamente listados; todo controller fora da lista é bloqueado e redirecionado ao dashboard. Controllers próprios do Pessoal:Moviments,MovimentBanks,MovimentBoxes,MovimentCards,Transfers,Costs,Banks,Boxes,Cards,PlanningsSomados aos controllers comuns (
commonControllers, liberados em qualquer plano):Pages,Users,Parameters,UsersParameters,SupportContacts,Knowledges,Settings,Cron,Regs,Backups,SystemLogs,IpBlocker,Error,App,Balances,FeatureUsages,Subscriptions,StripeWebhooks.Além do default-deny, o Pessoal tem uma denylist própria (
planDeniedControllers[1] = ['StockBalances']):StockBalances(Saldos de Estoque) é vedado ao Pessoal. Ele já cairia na regra default-deny (não está na allowlist), mas fica na denylist para que o bloqueio seja registrado comoinfo(política intencional) em vez deerror(tela legítima faltante). -
Plano Empresa (3), sem allowlist (
planControllers[3] = null) + denylist (planDeniedControllers[3]): alcança todos os controllers, exceto os explicitamente vedados:Cards,MovimentCards,Plannings.StockBalancesé acessível no Empresa (não consta da denylist do plano 3).
Consequência para os módulos exclusivos do Empresa (bloqueados no Plano Pessoal): como o Pessoal é default-deny, todos os módulos abaixo (incluindo todas as suas actions de relatório) são inacessíveis ao plano Pessoal (não constam da allowlist):
| Módulo | Controller(s) bloqueado(s) no Pessoal |
|---|---|
| Plano de Contas | AccountPlans |
| Tipos de Documento | DocumentTypes |
| Tipos de Evento | EventTypes |
| Produtos | Products |
| Tipos de Produto | ProductTypes |
| Grupos de Produto | ProductGroups |
| Inventário | Inventories |
| Pedidos de Compra / Solicitações | Purchases, PurchaseRequests |
| Vendas | Sells |
| Requisições | Requisitions |
| Industrialização | Industrializations |
| Notas Fiscais | Invoices |
| Saldos de Estoque | StockBalances (via denylist planDeniedControllers[1]) |
| Clientes / Fornecedores / Transportadoras | Customers, Providers, Transporters |
Como a checagem é por controller, ela cobre automaticamente todas as actions daquele controller, inclusive reportForm, report, cashFlow e demais relatórios. Não é preciso listar action por action.
StockBalancesera, até então, um controller comum (commonControllers), o único módulo de estoque tecnicamente alcançável pelo Pessoal. Foi removido decommonControllerse vedado ao Pessoal via denylist, ficando exclusivo do Empresa. Sua exibição no menu depende ainda do módulo do usuário (ver abaixo).
Duas camadas independentes de restrição por plano:
- Restrição de controller (esta seção,
validatePlanController): bloqueia módulos inteiros que não pertencem ao plano. - Restrição de action (
validatePlans, ver acima): dentro dos controllers financeiros que o Pessoal acessa, força as actions completas (index,add,edit,view,editBaixado,low,reportForm) para suas variantes*Simple. Ou seja, o Pessoal não abre os templates de visão completos (view.ctp,add.ctp, etc.) desses módulos, apenas os desimple/.
Qualquer tentativa de acesso a tela vedada redireciona ao dashboard. O bloqueio por denylist é registrado como info (política intencional); o bloqueio por controller fora da allowlist é registrado como error no error.log (para identificar telas legítimas eventualmente faltantes). Comportamento coberto por tests/TestCase/Controller/PlanAccessControlTest.php.
Limites por plano:
- Pessoal: máximo de 5 usuários por perfil; máximo de 1 perfil (não pode criar/editar
Parameters). - Empresa: sem limite de usuários ou perfis.
4. Sistema de Permissões e Acesso
As permissões são validadas em AppController::beforeFilter a cada request, via SystemFunctionsComponent::validaAcesso().
O mapa de permissões $permissions em SystemFunctionsComponent define três camadas:
| Camada | Chave | Quem se aplica |
|---|---|---|
| Por função do usuário | rules |
admin: CRUD completo + operações administrativas. user: CRUD básico + operações de movimento. cont: somente leitura (index, view). |
| Por plano do perfil | plans |
Actions específicas de cada plano (ex.: homePlan1, homePlan3, modais dos dashboards). |
| Comuns | common |
Qualquer usuário logado (login, logout, relatórios, trocar senha, mudar perfil, etc.). |
Usuário Super não passa pelo mapa $permissions (acesso total). Ainda assim, a restrição de controller por plano (validatePlanController) aplica-se inclusive para o Super, o plano do perfil ativo limita quais controllers podem ser usados, para evitar registros inválidos (ex.: lançamentos de cartão num perfil Empresa).
Toda nova action criada deve ser adicionada ao mapa $permissions, caso contrário qualquer usuário não-super é bloqueado e a tentativa é registrada no error.log com instrução de correção.
A action delete tem verificação extra: o AppController carrega o registro pelo ORM e confirma que pertence ao parameters_id da sessão antes de executar, salvo para os controllers Parameters e Users.
Telemetria de uso: após a autorização de cada request, o serviço FeatureUsageService::record() registra o acesso (controller+action) por perfil na tabela feature_usages. A telemetria é blind (try/catch), falhas não interrompem a navegação.
Módulo do usuário (Financeiro / Estoque / Ambos)
No Plano Empresa, cada usuário tem um atributo de módulo (na sessão como module), que separa quem opera o Financeiro do quem opera o Estoque:
| Valor | Significado | Acesso |
|---|---|---|
F |
Financeiro | Menus e controllers financeiros; bloqueado nos de estoque. |
S |
Estoque | Menus e controllers de estoque; bloqueado nos financeiros (validatePlans nega Moviments, MovimentBanks, Transfers, Costs, AccountPlans, DocumentTypes, EventTypes, Balances, etc., e os modais financeiros do dashboard). |
A |
Ambos | Acesso aos dois grupos. |
O menu lateral do Empresa (Element/menu/sidebar-plan3.ctp) usa esse atributo: o grupo "Financeiro" aparece para F/A; o grupo "Lançamentos de Estoque" (Pedidos de Vendas/Compras, Notas Fiscais, Solicitações, Inventários e Saldos de Estoque) aparece para S/A. Ou seja, Saldos de Estoque só é exibido a usuários com acesso a Estoque (S) ou Ambos (A). Os dashboards (homePlan1/homePlan3) não são bloqueados por módulo, o template oculta os cards do grupo que o usuário não opera.
5. Fluxo de Login e Sessão
- Autenticação via
CakePHP Authcom Blowfish hash. Login emPages/login. - Após login bem-sucedido,
AppController::session()executa:- Grava
userid,usernameeuser_namena sessão. - Identifica os perfis do usuário (
UsersParameters) e seleciona o último perfil usado (SystemFunctions::GETlastParameter). Se o usuário não tiver mais acesso ao último perfil, seleciona o primeiro da lista. - Define
sessionRule(função do usuário:super/admin/user/cont) esessionRuleIdna sessão. - Alimenta o "brand" da navbar (nome/razão social do perfil ativo).
- Atualiza a tabela de saldos do dia via
GeneralBalanceComponent::updateBalance. - Gera documentos recorrentes via
CronController::recurrent. - Verifica a data de validade do sistema (
Parameters.dtvalidade).
- Grava
AppController::beforeFilter(a cada request):- Verifica modo de manutenção via
MaintenanceService. - Registra
last_activitydo usuário na tabelausers(throttling: no máximo 1 UPDATE por minuto por sessão). - Chama
SystemFunctions::validatePlanspara verificar restrições de plano. - Chama
SystemFunctions::validaAcessopara verificar permissões de action. - Registra telemetria de uso.
- Verifica modo de manutenção via
O dashboard exibido após login varia por plano: homePlan1 (Pessoal) ou homePlan3 (Empresa). O roteamento interno é feito em PagesController::dashboard.
6. Organização do Código
src/
Controller/
AppController.php - base: autenticação, permissões, sessão, paginação
PagesController.php - telas pseudo-estáticas/infra: login, cadastro, recuperação de senha, home, dashboards, auditoria (os drill-downs do dashboard migraram para os controllers de domínio - ver §28)
CronController.php - tarefas agendadas (backup, recorrentes, e-mails)
*Controller.php - um por módulo de negócio
Component/
SystemFunctionsComponent.php - permissões, planos, regras de usuário
GeneralBalanceComponent.php - atualização da tabela de saldos
RegisterMovimentsComponent.php - registro de movimentos financeiros
*FunctionsComponent.php - lógica de domínio por módulo (legado)
Service/
BalanceService.php - consulta de saldos
MovimentCardService.php - cálculo de vencimento de cartão
MovimentMergeService.php - vínculos entre movimentos
MovimentRecurrentService.php - geração de lançamentos recorrentes
MovimentRegistrationService.php - cadastro de movimentos (parcelas)
MovimentBaixaService.php - baixa de movimentos
MovimentDashboardService.php - dados do dashboard de movimentos
MovimentCashFlowService.php - fluxo de caixa projetado
MovimentPersonalService.php - dados do dashboard Pessoal
MovimentReportService.php - relatórios de movimentos
MovimentDependencyService.php - dependências entre movimentos
BalanceService.php - saldos de bancos/caixas/cartões/planejamentos
SearchFilterService.php - helpers seguros para filtros e ORDER BY
ReportGuardService.php - validação de período e teto de linhas
PhoneService.php - normalização e sincronização de telefones
PasswordPolicyService.php - validação de política de senha
PasswordResetService.php - fluxo de reset de senha por token
IpBlockerService.php - bloqueio de bots por padrão de família de IP
SequenceService.php - numeração atômica de documentos por perfil
StripeService.php - integração de assinaturas Stripe
FeatureUsageService.php - telemetria de uso de funcionalidades
MaintenanceService.php - modo de manutenção
Model/
Table/ - um por tabela de banco; belongsTo/hasMany definidos aqui
Entity/ - _accessible (allowlist de mass-assignment)
Behavior/
TrimBehavior.php - trim() automático em campos de texto
LogBehavior.php - log de operações
View/
Helper/
MyHtmlHelper.php - formatação de datas/horas (usar sempre nas views)
MyFormHelper.php - helpers de formulário customizados
*Helper.php - um por módulo (status, tipo, labels de domínio)
Template/
<Modulo>/
index.ctp / add.ctp / edit.ctp / view.ctp
simple/ - variantes simplificadas para o Plano Pessoal
reports/ - templates de relatórios por módulo
Element/
dashboard/ - cards dos dashboards por plano
report-headers/ - cabeçalhos de filtro de relatório
Layout/
default2.ctp / ajax.ctp / register.ctp / layout-clean.ctp
Convenção de nomenclatura dos components: um *FunctionsComponent por módulo contém a lógica de domínio legada (ex.: MovimentsFunctionsComponent, SellsFunctionsComponent). O código mais novo usa classes Service em src/Service/.
7. Módulo: Contas a Pagar/Receber (Moviments)
Planos: Pessoal (versão simplificada) e Empresa (versão completa).
Gerencia os títulos financeiros do perfil: contas a pagar (débito, creditodebito='D') e contas a receber (crédito, creditodebito='C'). É o módulo financeiro central do sistema.
Campos principais da tabela moviments:
| Campo | Descrição |
|---|---|
historico |
Título/descrição do lançamento |
documento |
Número do documento |
data |
Data de lançamento |
vencimento |
Data de vencimento |
dtbaixa |
Data da baixa (pagamento/recebimento) |
valor |
Valor original |
valorbaixa |
Valor efetivamente pago/recebido |
status |
Ciclo de vida do título (ver mapa completo abaixo) |
ordem |
Número sequencial do lançamento no perfil (server-only) |
creditodebito |
C=Crédito (receber), D=Débito (pagar) |
costs_id |
Centro de Custos (Empresa) / Categoria (Pessoal) |
banks_id / boxes_id / cards_id |
Conta destino da baixa |
providers_id / customers_id |
Fornecedor ou cliente vinculado |
document_types_id |
Tipo de documento |
event_types_id |
Tipo de evento |
account_plans_id |
Plano de contas (Empresa) |
plannings_id |
Planejamento vinculado (Pessoal) |
Mapa completo de status (MovimentsHelper::status()):
| Status | Rótulo | Significado |
|---|---|---|
A |
Aberto | Título sem baixa (pai em aberto) |
P |
Parcial | Título pai com baixa(s) parcial(is) via moviment_mergeds |
O |
B.Parcial | Registro-filho de uma baixa parcial (criado por MovimentBaixaService, valor pago) |
B |
Baixado | Título quitado |
V |
Vinculado | Título vinculado a outro (mesclado) |
G |
Agrupado | Título agrupado em uma baixa consolidada |
C |
Cancelado | Baixa cancelada |
D |
Excluído | Lógica de exclusão suave (soft delete) |
Campo ordem (número do lançamento): número sequencial por perfil, gerado como MAX(Moviments.ordem)+1 (RegisterMovimentsComponent) e incrementado a cada parcela em MovimentRegistrationService. Serve como número visível/de referência do lançamento (não é o id). É server-only (bloqueado no _accessible), atribuído por setter no código. Cada tabela de movimento (moviment_banks, moviment_boxes) tem seu próprio contador ordem independente.
Parcelamento: ao criar um lançamento com mais de uma parcela, MovimentRegistrationService::calculaVencimento() gera as parcelas subsequentes com vencimentos calculados mensalmente, cada uma com seu próprio ordem incremental.
Recorrência: lançamentos recorrentes são definidos em moviment_recurrents e gerados automaticamente pelo CRON diário e a cada login (CronController::recurrent).
Competência do lançamento (regime de competência: plano Pessoal)
A coluna moviments.competencia (tipo DATE, gravada sempre como o 1º dia do mês, Y-m-01) define o mês/ano de referência contábil do título, independente da data de vencimento ou de pagamento. É um recurso do plano Pessoal (regime de competência): o campo aparece nos formulários simple/ de inclusão, edição e baixa (add, edit, edit_baixado, low) como um monthpicker mm/aaaa, e é editável por título, inclusive em títulos ainda em aberto.
- Normalização: o valor cru do monthpicker (
mm/aaaa) é convertido paraY-m-01porMovimentReportService::normalizeCompetencia()antes de gravar (opções e default do select vêm decompetenciaOptions()/competenciaDefault()). - Default: quando o usuário não informa a competência, ela assume o mês do próprio vencimento, ou seja, a coluna está sempre preenchida (e permanece editável).
- Deslocamento por parcela/recorrência (
shiftCompetencia): em parcelamentos (MovimentRegistrationService::calculaVencimento) e em recorrências (MovimentRecurrentService), a competência de cada parcela/ocorrência desloca acompanhando o vencimento, preservando o "gap" que o usuário escolheu entre competência e vencimento (MovimentRegistrationService::shiftCompetencia()). Ex.: salário com vencimento 30/jul e competência ago → a próxima ocorrência avança as duas na mesma medida. - Consumo (dashboard Pessoal e relatório mensal): ao alocar um título a um mês, usa-se o mês da competência quando preenchida e pertencente ao ano corrente; caso contrário, cai no fallback, vencimento para títulos em aberto e
dtbaixapara baixados. A regra canônica (espelhada emMovimentDashboardService::competenciaMonth()e no cash flow) écompetencia = X OR (competencia IS NULL AND dtbaixa BETWEEN ...). Assim, um título pago em um mês mas de competência de outro é contabilizado no mês de competência. - Retroação em produção: os SQLs
webroot/database/20260704 competencia abertos vencimento.sql(preenche pelos vencimentos) e20260704 competencia baixados dtbaixa.sql(preenche pelas datas de baixa) fizeram o backfill inicial da coluna.
Movimentos vinculados (moviment_mergeds): permitem associar um pagamento parcial a um título em aberto, ou agregar vários lançamentos em uma única baixa. Campos relevantes (valor, status) estão em moviments, não em moviment_mergeds. Ver seção de Particularidades para a armadilha de alias ORM.
Tipo de Documento, dois flags de comportamento (document_types): cada tipo de documento tem dois flags S/N independentes que mudam o comportamento dos lançamentos daquele tipo:
duplicadoc(controle de duplicidade): comduplicadoc='N',MovimentRegistrationService::validaDocumento()impede lançar dois títulos com o mesmodocumentopara o mesmo fornecedor (providers_id) ou cliente (customers_id), retorna oordemdo lançamento já existente para avisar o usuário.vinculapgto(agregador / "fatura"): comvinculapgto='S', um título desse tipo funciona como agregador, uma fatura que vincula vários outros títulos e permite baixar todos em um único lançamento (simula uma fatura de cartão para qualquer fornecedor/cliente). Detalhado no tópico "Agregador de títulos" abaixo.
Agregador de títulos por Tipo de Documento (vinculapgto)
Permite consolidar vários títulos em aberto sob uma "fatura" única e quitá-los de uma vez, o mesmo conceito da fatura de cartão, generalizado para qualquer fornecedor/cliente.
Ciclo completo:
- Tipo de documento agregador: cadastrar um
DocumentTypecomvinculapgto='S'(ex.: "Fatura"). - Título-fatura (pai): lançar um título usando esse tipo de documento. Na listagem (
index.ctp), títulos cujo tipo temvinculapgto='S'e semcards_idexibem o ícone de vínculo, que abre a actiongroup. - Vinculação (
group→MovimentMergeService::vinculaPagamentos): a tela lista os demais títulos do mesmo fornecedor/cliente comstatus IN ['A','V'](e que não sejam eles próprios agregadores). Os títulos marcados viram filhos:- cada filho passa a
status='V'(Vinculado) e some da lista de "em aberto"; - grava-se o vínculo em
moviment_mergeds(moviments_id=fatura,moviments_merged=filho); - a fatura passa a
status='G'(Agrupado) e tem seuvalorrecalculado como a soma dos filhos (vinculaConsulta). - Desmarcar todos reverte: filhos voltam a
status='A', vínculos apagados, fatura volta aA.
- cada filho passa a
- Baixa única (
low→MovimentBaixaService::lowVinculados): ao baixar a fatura (statusG), a fatura-pai é quitada e gera um único movimento de banco/caixa (o pagamento consolidado). Filhos que já eram baixa parcial (status='O') são ignorados. - Cancelamento da baixa:
lowVinculados(..., true)reverte o vínculo.
Estado terminal do título-filho (comportamento intencional): após a baixa da fatura, o título-filho permanece
status='V'(Vinculado), semdtbaixa/valorbaixapróprios, quem carrega a data e o valor da baixa é a fatura-pai. Não existe (nem se espera) um estado "vinculado e baixado" simultâneo: lê-se o pagamento de um título vinculado a partir da fatura à qual ele está amarrado, assumindo que foi pago na data da fatura e pelo seu valor original. Por isso o filho fica fora das listas de "em aberto" (Vinculado) e não aparece individualmente como "Baixado".Implementação:
MovimentBaixaService::lowVinculadosmonta umpre_savecom os dados da baixa e fazpatchEntity($pai, ['id'=>filho...]); como a entityMovimenttemid => falseno_accessible, oidé ignorado e os dados recaem sobre o próprio pai, que é justamente o efeito desejado aqui. Comportamento fixado por teste emtests/TestCase/Service/MovimentMergeServiceVinculosTest.php(linhas 267-272); ao mexer nesse método, preservar essa semântica.
Distinção importante: este agregador (vinculapgto) é diferente da baixa parcial (seção "Fluxo de baixa"). Na baixa parcial, um filho status='O' é criado como fração paga de um único título. No agregador, vários títulos independentes (status='V') são amarrados a uma fatura status='G' e quitados juntos. Ambos usam a mesma tabela moviment_mergeds, distinguidos pelo status do filho (O = baixa parcial, V = vinculado à fatura).
Conversão de datas: os campos data, vencimento e dtbaixa aceitam entrada no formato d/m/Y (pt-BR). A conversão é feita em dois hooks:
beforeMarshal: para dados vindos de formulário (newEntity/patchEntity).beforeSave: rede de segurança para entidades montadas diretamente no código (services, baixas programáticas). Os dois hooks são necessários e não devem ser removidos.
Diferenças por plano:
| Funcionalidade | Pessoal | Empresa |
|---|---|---|
| Tela de listagem | indexSimple (template simple/) |
index |
| Formulário de inclusão | addSimple |
add |
| Relatório | cashFlowSimple (form em simple/cash_flow.ctp) |
reportForm (form em report_form.ctp), cashFlow e reportRp |
| Clientes/Fornecedores | Sem cadastro separado | Com Customers/Providers |
| Plano de contas | Não disponível | AccountPlans |
| Tipo de documento | Não disponível (Pessoal) | DocumentTypes |
Nota sobre o relatório no Pessoal: ao contrário dos demais módulos, o
MovimentsControllernão tem uma actionreportFormSimple. O relatório financeiro do plano Pessoal é a action própriacashFlowSimple(menu "Fluxo de Caixa"), que renderiza o formuláriosimple/cash_flow.ctpe os resultados emreports/simple/cash_flow_*. O redirecionamento genéricoreportForm → reportFormSimplefeito porvalidatePlans(que existe e tem alvo emTransferseMovimentCards) não se aplica aqui, no Pessoal o link já aponta direto paracashFlowSimple. No Empresa, o ponto de entrada de relatórios éreportForm(report_form.ctp), além decashFlow(extrato/fluxo consolidado) ereportRp(relação de pagamentos).
8. Módulo: Extratos Bancários (MovimentBanks)
Planos: Pessoal (simplificado) e Empresa (completo).
Registra os lançamentos no extrato de contas bancárias. Cada registro em moviment_banks está associado a um banco (banks_id) e pode estar vinculado a um movimento de CPR (moviments_id) ou a uma transferência (transfers_id). Campos de valor (valor, valorbaixa), data e status seguem a mesma semântica de moviments.
O extrato bancário é gerado a partir dos movimentos baixados no banco, e o saldo acumulado é mantido na tabela balances.
9. Módulo: Extratos de Caixa (MovimentBoxes)
Planos: Pessoal (simplificado como "Carteira") e Empresa (completo como "Caixa").
Funciona de forma análoga a MovimentBanks, mas para contas do tipo caixa físico. Cada registro em moviment_boxes está associado a um caixa (boxes_id) e pode estar vinculado a um movimento de CPR ou transferência.
10. Módulo: Lançamentos de Cartão (MovimentCards)
Planos: exclusivo do Plano Pessoal. O Plano Empresa tem Cards e MovimentCards na denylist e não acessa este módulo.
Gerencia as faturas e lançamentos de cartão de crédito. Cada registro em moviment_cards está associado a um cartão (cards_id). O vencimento da fatura é calculado por MovimentCardService::calcVencimento() com base no melhor dia de compra e vencimento definidos no cadastro do cartão.
O campo cards.saldo_aberto (Limite Utilizado no dashboard) é recalculado automaticamente em MovimentsTable::recalcCardSaldoAberto() (chamado em afterSave/afterDelete) quando há cards_id no movimento.
O módulo usa apenas as actions *Simple (a versão sem sufixo foi removida). Templates em Template/MovimentCards/simple/.
11. Módulo: Transferências (Transfers)
Planos: Pessoal (simplificado) e Empresa (completo).
Registra transferências entre contas do mesmo perfil: de banco para banco, de banco para caixa, de caixa para banco, de caixa para caixa. Cada transferência gera automaticamente entradas em moviment_banks e/ou moviment_boxes para as contas de origem e destino.
Campos principais: banks_id (origem banco), boxes_id (origem caixa), banks_dest (destino banco), boxes_dest (destino caixa), costs_id, event_types_id, valor, historico, documento, data.
12. Módulo: Planejamentos
Planos: exclusivo do Plano Pessoal. Plano Empresa tem Plannings na denylist.
Planejamentos funcionam como reservas financeiras forçadas. São sempre do tipo despesa (creditodebito='D') e não possuem fornecedor/cliente vinculado (campo providers_id é nullable).
Ao criar um planejamento, RegisterMovimentsComponent::moviment_add gera parcelas mensais no módulo de Contas a Pagar. As parcelas herdam o costs_id (Categoria) do planejamento.
O saldo reservado para planejamentos é exibido no dashboard Pessoal, na seção de saldos de planejamentos.
13. Módulo: Saldos Financeiros (Balances)
Planos: ambos (infraestrutura comum).
A tabela balances mantém o saldo diário de cada banco, caixa, cartão e planejamento do perfil. É atualizada automaticamente a cada login via GeneralBalanceComponent::updateBalance.
BalanceService::getSaldos() consulta os saldos mais recentes de bancos, caixas, cartões e planejamentos para o dashboard e telas de saldo.
Fluxo de Caixa Projetado (Moviments::fluxoCaixaProjetado): tela complementar do dashboard Empresa (homePlan3). Parte do saldo atual consolidado de bancos e caixas (BalancesFunctions::getSaldos) e projeta a evolução futura com base nos títulos a vencer (a pagar e a receber), via DashboardAnalytics::projectedCashFlow() (série da projeção) e projectedCashFlowDetails() (títulos que compõem cada período). Permite antecipar dias/meses de saldo negativo. Consolidada, com as demais telas de drill-down do dashboard, em §28.
14. Módulo: Produtos e Cadastros de Estoque
Planos: exclusivo do Plano Empresa.
Produtos (Products): cadastro central do estoque. Cada produto tem tipo (product_types_id), grupo (product_groups_id), código, título, unidade, custo, preço de venda, estoque mínimo e máximo. O ProductsController gerencia o CRUD; ProductFunctionsComponent contém lógica auxiliar.
Tipos de Produto (ProductTypes): classificação de produtos (ex.: matéria-prima, produto acabado). Cadastro simples com código e título.
Grupos de Produto (ProductGroups): agrupamento adicional de produtos. Cadastro simples com código e título.
15. Módulo: Compras (Purchases)
Planos: exclusivo do Plano Empresa.
Gerencia pedidos de compra emitidos para fornecedores. Cada pedido (purchases) pode conter múltiplos itens (purchase_items). O pedido pode ser vinculado a uma solicitação de compra (purchase_requests) via purchases_purchase_requests.
O pedido gera, ao ser confirmado, um lançamento financeiro em moviments (conta a pagar ao fornecedor) e pode ser vinculado a uma nota fiscal em invoices via invoices_purchases_sells.
O código do pedido é gerado de forma atômica via SequenceService (migração de MAX(code)+1).
PurchaseRequests (Solicitações de Compra): tela separada para solicitação interna de compra, que pode ser aprovada e convertida em pedido de compra. Usuários com função user têm acesso a PurchaseRequests via permissão explícita.
16. Módulo: Vendas (Sells)
Planos: exclusivo do Plano Empresa.
Gerencia pedidos de venda para clientes. Cada venda (sells) pode conter múltiplos itens (sell_items) e pode ser associada a um transportador. A venda pode gerar uma nota fiscal em invoices via invoices_purchases_sells e um lançamento financeiro em moviments (conta a receber do cliente).
O código da venda é gerado de forma atômica via SequenceService.
Uma venda pode originar um processo de industrialização (Industrializations).
Mapa de status da venda (sells.status):
| Status | Significado |
|---|---|
P |
Pendente (pedido em aberto) |
G |
Em entrega (em rota) |
E |
Entrega parcial |
F |
Finalizado |
C |
Cancelado |
D |
Excluído |
Finalização (finish): finaliza a venda (status='F'). A action valida que existe ao menos uma nota fiscal finalizada vinculada (invoices_purchases_sells → invoices.status='F') e bloqueia se houver NF pendente (invoices.status='P'). A movimentação de estoque não ocorre aqui, é feita em InvoicesController::finish quando a NF é finalizada. Status da NF: P=em entrega/pendente, C=cancelada, F=finalizada.
Tela de Pedidos (Purchases::pedidos): visão consolidada, no dashboard Empresa, dos pedidos ainda em aberto (compras com status P/A e vendas com status P/G/E) para acompanhamento do que está pendente de entrega/faturamento. Há também PurchaseRequests::solicitacoes (solicitações de compra abertas). Ambas consolidadas em §28 (telas complementares do dashboard).
17. Módulo: Requisições (Requisitions)
Planos: exclusivo do Plano Empresa.
Gerencia saídas de estoque por requisição interna (retirada de materiais para produção ou uso). Cada requisição (requisitions) pode conter múltiplos itens (requisition_items) e pode estar vinculada a um processo de industrialização. O campo type distingue o tipo da requisição; o campo applicant registra o solicitante.
O código é gerado via SequenceService.
18. Módulo: Inventários (Inventories)
Planos: exclusivo do Plano Empresa.
Gerencia a contagem física do estoque. Cada inventário (inventories) contém itens (inventory_items) com a quantidade contada por produto. A confirmação do inventário ajusta o saldo em stock_balances.
O código é gerado via SequenceService.
19. Módulo: Industrialização (Industrializations)
Planos: exclusivo do Plano Empresa.
Registra ordens de produção/transformação de materiais em produtos acabados. Cada ordem (industrializations) pode estar associada a uma venda (sells_id) e ao cliente correspondente. O módulo fornece relatório analítico e rastreamento por tracker_analitico.
O código é gerado via SequenceService (piloto de migração do MAX(code)+1 para sequências atômicas).
20. Módulo: Saldos de Estoque (StockBalances)
Planos: exclusivo do Plano Empresa. Vedado ao Plano Pessoal via planDeniedControllers[1] (o Pessoal não opera estoque). No Empresa, aparece no menu e é acessível apenas a usuários de módulo Estoque (S) ou Ambos (A), ver "Módulo do usuário" no bloco 4.
A tabela stock_balances mantém o saldo atual de cada produto no estoque do perfil, com data, quantidade, unidade e custo médio (vlcost). É atualizada pelas operações de compra, venda, requisição e inventário.
A tela de listagem exibe o saldo atual com informações de estoque mínimo/máximo e alertas de ruptura. Item de menu: grupo "Lançamentos de Estoque" do menu lateral do Empresa (Element/menu/sidebar-plan3.ctp).
21. Módulo: Notas Fiscais (Invoices)
Planos: exclusivo do Plano Empresa.
Gerencia notas fiscais de entrada (compras) e saída (vendas). Cada nota (invoices) pode conter múltiplos itens (invoice_items) e é vinculada a compras ou vendas via invoices_purchases_sells. O campo invoice_type distingue entrada de saída. O sistema permite o upload do PDF da NF (invoice_pdf).
22. Cadastros de Apoio Financeiro
Todos com parameters_id (multi-tenant).
| Módulo | Controller | Planos | Descrição |
|---|---|---|---|
| Bancos | BanksController |
Ambos | Contas bancárias do perfil. |
| Caixas | BoxesController |
Ambos (Pessoal: "Carteira") | Caixas físicos do perfil. |
| Cartões | CardsController |
Pessoal apenas | Cartões de crédito. Campos: limite, vencimento, melhor dia, saldo_aberto. |
| Categorias / Centro de Custos | CostsController |
Ambos | costs: "Categorias" no Pessoal, "Centro de Custos" no Empresa. |
| Tipos de Documento | DocumentTypesController |
Empresa | Tipos de documento financeiro; flags duplicadoc (duplicidade) e vinculapgto (agregador/fatura). Não disponível no Pessoal. |
| Tipos de Evento | EventTypesController |
Empresa | Classificação transversal de eventos financeiros. |
| Plano de Contas | AccountPlansController |
Empresa | Estrutura hierárquica (threaded) de plano de contas contábil. |
Tipos de Documento (DocumentTypes), detalhe: classificam a natureza do documento de cada lançamento (moviments.document_types_id) e carregam dois flags S/N independentes:
duplicadoc: comN, bloqueia repetir o mesmodocumentopara o mesmo fornecedor/cliente (validaDocumento()).vinculapgto: comS, torna o título um agregador/fatura que vincula vários outros títulos e permite baixá-los juntos (ver "Agregador de títulos por Tipo de Documento" no bloco 7).
No Plano Pessoal o módulo não existe, então document_types_id fica sempre nulo nos lançamentos.
Tipos de Evento (EventTypes), detalhe: classificação livre de eventos financeiros, usada como campo opcional (event_types_id) em CPR (moviments), extratos (moviment_banks/moviment_boxes), cartões (moviment_cards) e transferências (transfers). Serve para agrupar lançamentos em relatórios por tipo de evento (ex.: "folha de pagamento", "impostos"). O status tem valores próprios: A=Ativo, I=Inativo, T=Automático (evento gerado pelo sistema), D=Excluído (EventTypesHelper::status()).
Plano de Contas (AccountPlans), hierarquia: cada conta tem classification (código pontilhado, ex.: 01, 01.01, 01.01.02) e plangroup (id da conta-pai). A profundidade é dada pela contagem de pontos no classification. Ao incluir uma subconta, new_classification() calcula o próximo código dentro do grupo-pai automaticamente. A tela de listagem usa find('threaded') (relação pai/filho via plangroup) e listPlans() monta um treeview (bootstrap treeview, classes lp-) expansível. Nos lançamentos, account_plans_id referencia normalmente uma conta-folha. Contas em uso não são excluídas, são inativadas (status='I').
23. Cadastros de Apoio de Estoque
Todos com parameters_id (multi-tenant), exclusivos do Plano Empresa.
| Módulo | Controller | Descrição |
|---|---|---|
| Tipos de Produto | ProductTypesController |
Classificação de produtos (matéria-prima, produto acabado, etc.). |
| Grupos de Produto | ProductGroupsController |
Agrupamento adicional de produtos. |
24. Cadastros de Pessoas (Customers, Providers, Transporters)
Planos: exclusivo do Plano Empresa. O Plano Pessoal não tem cadastro separado de clientes e fornecedores.
Clientes (Customers): cadastro de clientes com CPF/CNPJ, razão social, endereço e telefones polimórficos.
Fornecedores (Providers): cadastro de fornecedores com CPF/CNPJ, razão social, endereço e telefones polimórficos.
Transportadoras (Transporters): cadastro de transportadoras vinculadas a compras e vendas.
Telefones polimórficos (phones): a tabela phones centraliza todos os telefones de Customers, Providers e Transporters. Cada registro tem: model (nome do model), foreign_key (ID do cadastro), numero, principal (S/N), obs, parameters_id. As colunas legadas telefone1..4 são mantidas como backup e ressincronizadas a cada gravação por PhoneService::syncLegacyColumns().
25. Assinaturas (Stripe)
Planos: ambos.
Integração de cobrança recorrente via Stripe. O fluxo é hospedado: o pagamento ocorre numa página do Stripe (Checkout, mode=subscription) e a gestão da assinatura (trocar cartão, upgrade/downgrade, cancelar) num Billing Portal do Stripe, nenhum dado de cartão trafega ou fica armazenado neste servidor. A baixa efetiva da cobrança (empurrar validade/plano) nunca é feita nas telas de retorno; é sempre o webhook que decide (fonte da verdade).
Componentes
| Componente | Papel |
|---|---|
StripeService (src/Service/) |
Wrapper fino sobre o SDK stripe/stripe-php. Concentra: criação do Customer por perfil, sessões de Checkout e de Billing Portal, verificação de assinatura do webhook e sincronização do perfil a partir da Subscription. |
SubscriptionsController |
Telas do assinante: assinatura, checkout, portal, sucesso, cancelado. |
StripeWebhooksController::webhook |
Endpoint público que recebe os eventos do Stripe e sincroniza o perfil. Fonte da verdade da cobrança. |
Planos, periodicidades e preços
- Planos (
StripeService::PLAN_KEYS):1 => pessoal,3 => empresa(os mesmos 2 planos ativos do sistema). - Periodicidades (
StripeService::INTERVALS):mensal,semestral,anual. - Os IDs de
Pricedo Stripe (price_...) ficam emconfig/app.phpcomoStripe.prices.<plano>.<periodicidade>(ex.:Stripe.prices.empresa.anual).priceId()resolve plano+periodicidade →price_...;planIdFromPrice()faz o caminho inverso (price_...→plans_id), usado pelo webhook para saber qual plano foi contratado.
Colunas de vínculo em parameters
A assinatura de cada perfil é rastreada por quatro colunas em parameters, gravadas pelo Checkout e mantidas pelo webhook:
| Coluna | Conteúdo |
|---|---|
stripe_customer_id |
cus_..., o Customer do Stripe do perfil (criado por ensureCustomer, com metadata.parameters_id). |
stripe_subscription_id |
sub_..., a assinatura vigente. |
stripe_price_id |
price_..., o preço/periodicidade atual. |
stripe_status |
Status da assinatura no Stripe (active, trialing, canceled, unpaid, ...). É o campo consultado no login para o convite. |
Fluxo de contratação
assinatura(rota amigável/assinatura): monta as periodicidades disponíveis para o plano do perfil (ou os dois planos, se ainda sem plano) e mostra o estado atual.checkout?plano=…&periodicidade=…: resolve oPrice, garante oCustomer(ensureCustomer, cria e persistestripe_customer_idna 1ª vez) e cria a sessão de Checkout, redirecionando para a URL hospedada do Stripe. A sessão levaclient_reference_id/subscription_data.metadata=parameters_id,allow_promotion_codes,locale=pt-BRe não fixapayment_method_types(deixa o Stripe escolher dinamicamente).sucesso(retorno após pagar) /cancelado(retorno se desistir). A tela de sucesso apenas informa, a validade/plano só é aplicada quando o webhook chega.portal: abre o Billing Portal do Stripe para autoatendimento (troca de cartão, upgrade/downgrade, cancelamento).
Webhook: fonte da verdade (StripeWebhooksController::webhook)
- Público, sem login (
$this->Auth->allow(['webhook']), molde doCronController). A autenticidade não vem de login nem de token de URL, e sim da verificação da assinatura do payload (cabeçalhoStripe-SignaturecontraStripe.webhookSecret, viaconstructWebhookEvent). Payload/assinatura inválidos → 400. Rota:/stripe-webhooks/webhook. - Eventos tratados:
| Evento | Ação |
|---|---|
invoice.paid |
Pagamento inicial e renovações, recarrega a Subscription e empurra validade/plano. |
checkout.session.completed |
Grava os vínculos já na primeira volta do Checkout. |
customer.subscription.created / updated |
Upgrade/downgrade, troca de período. |
customer.subscription.deleted |
Marca stripe_status; a validade vence naturalmente (não mexe em dtvalidade). |
applySubscriptionToParameterlocaliza o perfil porsubscription.metadata.parameters_id(ou pelostripe_customer_idjá gravado), atualiza sempre os vínculos/stripe_status, e só empurraplans_idedtvalidadequando a assinatura está vigente (active/trialing). Status terminais (canceled/unpaid) apenas marcam o status, o acesso permanece até adtvalidadejá paga expirar (regra existente de validade do sistema).- Tratamento de erro: eventos reconhecidos com falha de processamento sobem para 500 (o Stripe re-tenta); eventos verificados e processados retornam 200 (
ok). Perfil não localizado é logado comoerror.
Armadilha de versão de API (
current_period_end): a partir da API2026-06-24(dahlia, fixada pelostripe-php20.x),current_period_endsaiu daSubscriptione passou para o item da assinatura.applySubscriptionToParameterlê do item (subscription.items.data[0].current_period_end), com fallback ao campo legado da subscription. Ao mexer nesse cálculo de validade, preservar os dois caminhos.
Convite para assinar (no login)
Em AppController::validade (rodada no fluxo de login/sessão), se o stripe_status do perfil não for active/trialing, é exibido um flash de convite com link para a tela de Assinatura (Subscriptions::assinatura). É o gancho que leva perfis sem assinatura vigente a contratar.
Segurança
- Chave da API (
Stripe.apiKey) e segredo do webhook (Stripe.webhookSecret) ficam emconfig/app.php, fora do versionamento. Recomendado usar chave restrita (rk_...), nunca a secret plena. - O webhook é autenticado por assinatura do payload (não por login/token), ver acima.
- Nenhum dado de cartão passa pelo servidor (fluxo 100% hospedado no Stripe).
26. Tarefas Automáticas (Cron)
Controller: CronController.
O cron é acionado por URL com token de segurança (config/app.php > Cron.token): GET /cron/cron?token=SEU_TOKEN. Destinado a chamada externa pelo servidor (ex.: cronjob do servidor de produção).
A cada execução com token válido:
- Backup do banco (
BackupsFunctions::dbExportAutomated): executa independentemente do marcador diário (roda antes do corte), mas só gera se ainda não houver um backup de hoje (backupExistsForToday()), assim o CRON pode ser chamado várias vezes no mesmo dia sem empilhar backups nem estourar o disco; se a 1ª chamada falhou, uma posterior ainda gera. - Alerta de disco (
updateSizeAlert/notifyDiskAlert): recalcula o uso da pasta de backups e, só na transição para acima do limite, dispara e-mail de alerta ao operador (Cron.alertEmail). - Marcador diário: se o cron já rodou hoje (registro na tabela
cron), as rotinas abaixo são ignoradas. - Geração de lançamentos recorrentes (
recurrent): cria os lançamentos do dia a partir dos registros emmoviment_recurrents. - Resumo diário (
resumoDiario): envia e-mail com resumo financeiro do dia (títulos que vencem hoje). - Resumo semanal (
resumoSemanal): envia e-mail com resumo da semana. Executa apenas aos domingos (date('w') == 0). - Resumo de atrasados (
resumoAtrasados): envia e-mail com lançamentos em atraso. Executa apenas no dia 1º de cada mês (date('d') == '01').
Cada rotina roda isolada em try/catch: a falha de uma não impede as demais, e o marcador do dia (tabela cron) só é gravado ao final se todas rodarem sem exceção (senão há retry na próxima chamada, em vez de pular o dia). Falhas acumuladas na execução geram um e-mail de alerta ao operador (notifyCronFailure, throttle de 1x/dia via Settings.cron_alert_date).
O método recurrent também é executado a cada login (via AppController::session), como fallback para perfis sem execução do cron externo.
No dashboard do Super, se o cron não foi executado hoje, aparece um alerta com link para acionamento manual.
Catálogo de e-mails transacionais
Todos os e-mails do sistema (os do CRON acima são um subconjunto) são enviados pelo helper único EmailFunctionsComponent::sendMail(['subject','template','vars','toEmail']). Regras comuns a todos: formato HTML, remetente suporte@reiniciando.com.br ("Reiniciando Sistemas"), BCC sempre para o suporte, e quando toEmail é vazio o destinatário padrão é o próprio suporte. Falhas de envio são engolidas e logadas em debug (não interrompem o fluxo).
O "chrome" (cabeçalho/rodapé/paleta/logo) é compartilhado pelos elements em Template/Element/email/ (top.ctp, bottom.ctp, totais.ctp, moviment_item.ctp) sobre o layout Template/Layout/Email/html/default.ctp. Os corpos ficam em Template/Email/html/.
Templates ativos (com disparo em produção):
Template (Email/html/) |
Gatilho | Origem | Destinatário |
|---|---|---|---|
boas_vindas |
Cadastro de novo usuário concluído | PagesController::cadastrar |
usuário recém-cadastrado |
novo_cliente |
Mesmo cadastro, aviso interno (marketing) | PagesController::cadastrar |
suporte (fallback) |
token_redefinicao |
Solicitação "Esqueci minha senha" | PagesController (reset) |
usuário |
login_bloqueio |
IP bloqueado pelo throttle de login (temporário ou definitivo) | PagesController (login) |
operador (Cron.alertEmail) |
email_teste_sistema |
Botão "enviar e-mail de teste" nas Configurações (Super) | SettingsController |
e-mail informado |
cron_mail_diario |
Resumo financeiro do dia (CRON) | CronController::resumoDiario |
usuários do perfil (opt-in sendmail) |
cron_mail_semanal |
Resumo semanal, aos domingos (CRON) | CronController::resumoSemanal |
usuários do perfil (opt-in sendmail) |
cron_mail_atrasados |
Lançamentos em atraso (CRON) | CronController::resumoAtrasados |
usuários do perfil (opt-in sendmail) |
backup_disk_alert_template |
Alerta de disco cheio no backup automático (CRON) | CronController |
operador (Cron.alertEmail) |
error_email_template |
Erro durante a execução do CRON | CronController |
operador (Cron.alertEmail) |
Opt-in por usuário (users_parameters.sendmail), self-service: os três resumos do CRON acima (cron_mail_diario, cron_mail_semanal, cron_mail_atrasados) só são enviados aos usuários do perfil que optaram por recebê-los (CronController filtra por UsersParameters.sendmail = 'S'). Cada usuário liga/desliga a própria preferência em "Minha Conta" → "Dados da Empresa" (Parameters::view do perfil em uso): um card com toggle Sim/Não grava via AJAX em ParametersController::toggleSendmail (sem fechar o modal nem recarregar). O alvo é sempre o vínculo do usuário logado no perfil em uso (nunca vem do request); a action é liberada a qualquer usuário, não passa pelo bloqueio de escrita do perfil (blockParameterWriteIfNotSuper), pois mexe só na própria notificação. Para o super consultando outro perfil, o card é ocultado (podeAlternarEmail).
Templates inativos / código morto: arquivos preservados mas sem disparo real: tentativa_logon, fora_validade e support_contact têm as chamadas sendMail comentadas (AppController, PagesController, SupportContactsController); cron_recurrents, reenvia_senha, system_customers, backup_email_template e default não têm referência de disparo no código; falha_email_template só é usado pela classe Mailer\UserMailer, que não tem chamador (getMailer('User') não é invocado). Não recriar disparos para esses ao evoluir o sistema sem antes confirmar a intenção.
27. Particularidades e Regras de Negócio Transversais
Cap de 300 registros nas listagens
Toda tela de listagem (index/indexSimple) é limitada a 300 registros. O limite é implementado via override de AppController::paginate(): o contador da paginação usa COUNT(*) sobre uma subconsulta com LIMIT 300, evitando varredura completa da tabela. Quando o total atinge 300, o rodapé da listagem (Element/pagination.ctp) exibe nota informando o limite. O usuário deve usar os filtros de busca ou os relatórios para acessar registros além do limite.
Padrão de performance em listagens: paginar primeiro, depois carregar dados relacionados filtrando pelos IDs da página atual (consultas IN com ~15 IDs). Nunca materializar (->toArray()) o resultado filtrado completo antes de paginar.
Movimentos vinculados (moviment_mergeds): armadilha de alias
A tabela moviment_mergeds vincula dois registros de moviments: moviments_id = movimento pai; moviments_merged = movimento filho. Os dados relevantes (valor, status) estão em moviments, não em moviment_mergeds.
MovimentMergedsTable define belongsTo('Moviment_Mergeds') sem className, o que faria o ORM resolver para a própria tabela moviment_mergeds. Por isso, não usar innerJoinWith('Moviment_Mergeds') para alcançar o filho. O padrão correto é usar join() cru, aliasando ambas as pontas para a tabela moviments (referência: homePlan1 em MovimentsController).
Fluxo de baixa e geração de entrada no extrato
A "baixa" é o registro do pagamento/recebimento de um título de CPR (moviments) e o momento em que o valor reflete no extrato de banco/caixa e no saldo.
- Baixa total: o título é quitado (
status='B'), gravam-sevalorbaixa,userbaixaedtbaixa, e é criada a entrada correspondente no extrato (moviment_banksoumoviment_boxes, conformebanks_id/boxes_idinformado na baixa). O extrato aponta de volta para o título viamoviments_id. - Baixa parcial: o título pai passa a
status='P'(Parcial).MovimentBaixaServicecria um registro-filho emmovimentscomstatus='O'(B.Parcial), no valor pago (valor = valorbaixado pai), e liga pai↔filho emmoviment_mergeds(moviments_id=pai,moviments_merged=filho). O saldo em aberto do pai é ovalormenos a soma das baixas parciais. - Entrada manual vs. automática no extrato: um lançamento de CPR só gera entrada no extrato quando é baixado informando a conta (banco/caixa). Lançamentos diretos em
MovimentBanks/MovimentBoxes(sem CPR de origem) são entradas manuais do extrato, nesse casomoviments_idfica nulo. Transferências (Transfers) geram entradas automáticas em ambas as pontas. - Reflexo no saldo/cartão: após
save/delete,MovimentsTable::recalcCardSaldoAberto()recalculacards.saldo_abertoquando hácards_id; o saldo consolidado de bancos/caixas é mantido embalances(atualizado no login viaGeneralBalanceComponent::updateBalance).
Relacionamento das tabelas envolvidas. Uma única tabela moviments guarda tanto o título (pai) quanto os registros-filho de baixa parcial; moviment_mergeds liga as duas pontas, e ambas as pontas (moviments_id e moviments_merged) apontam para moviments, nunca para moviment_mergeds (ver "armadilha de alias" acima). As entradas de extrato ficam em moviment_banks/moviment_boxes, com moviments_id apontando para o registro de moviments que originou a baixa (nulo quando a entrada é manual).
Sequência de uma baixa parcial (o caso completo, exercita todas as tabelas; a baixa total faz apenas o passo do extrato + status='B', sem criar filho nem vínculo):
Conversão de data pt-BR em MovimentsTable
Os campos de data (data, vencimento, dtbaixa) aceitam entrada em d/m/Y. A conversão ocorre em beforeMarshal (dados de formulário) e beforeSave (entidades montadas via setter no código). Os dois hooks são necessários; remover o beforeSave causa corrupção silenciosa de data em saves programáticos.
Dicas Financeiras e "Análise com IA" (dashboard Pessoal): sem IA no backend
O dashboard do plano Pessoal (home-plan1.ctp) tem dois recursos analíticos que, apesar do rótulo, não fazem chamada a nenhum serviço de IA/LLM no servidor:
- Dicas Financeiras: carrossel rotativo de textos analíticos gerados por regras em
FinancialAnalyticsComponent::getDicas()(variação de receitas/despesas ao longo do ano, proporção de despesas variáveis, contas em atraso, concentração em categoria não essencial, meta de reserva de emergência, saldo em aplicações vs. consumo de cartão, previsão anual etc.). O PHP monta a lista e o front apenas alterna as dicas na tela (#dicasFinanceiras). - Botão "Análise com IA": o link
#btnCopyYearDashboardDatamonta um texto-resumo da análise anual (receitas, despesas, investimentos, saldos) e o copia para a área de transferência do usuário (navigator.clipboard, com fallbackexecCommand('copy')). Não há processamento de IA embutido, o texto é preparado para o usuário colar em uma ferramenta de IA externa de sua preferência.
Sequências de numeração de documento
O padrão MAX(code)+1 foi substituído por SequenceService (tabela sequences), que reserva o próximo número de forma atômica via INSERT ... ON DUPLICATE KEY UPDATE com LAST_INSERT_ID(). Primeiro módulo migrado: Industrializations. Oito módulos com campo code ainda usam o padrão antigo (pendentes de migração).
TrimBehavior
TrimBehavior (src/Model/Behavior/TrimBehavior.php) aplica trim() automático em todos os campos de texto (descobertos pelo schema) em beforeMarshal e beforeSave. Deve ser adicionado a toda nova tabela de cadastro/lançamento via $this->addBehavior('Trim').
Modo de Manutenção
Controlado via tabela settings (chaves maintenance_mode e affected_plans). O MaintenanceService verifica a cada request se o modo está ativo e, se for, lança ServiceUnavailableException (tela error503.ctp) para os planos afetados. O Super não é afetado pelo modo de manutenção. Rotas permitidas mesmo em manutenção: login, logout, cron, backup, webhook.
Indicador de sessão ativa
A coluna users.last_activity é atualizada a cada request do usuário logado, com throttle de 1 UPDATE por minuto por sessão (via flag lastActivityPing na sessão). A tela de lista de usuários exibe o indicador de sessão como verde (ativo recentemente), amarelo (ativo há mais tempo) ou cinza (sem atividade).
Fluxo ponta a ponta: Venda → NF → Financeiro → Estoque
Cruza os módulos Vendas (16), Notas Fiscais (21) e Saldos de Estoque (20). O ponto central (e não óbvio) é que o pedido de venda, sozinho, não movimenta nem estoque nem financeiro: tudo acontece através da Nota Fiscal, e em dois momentos distintos (emissão gera o financeiro; finalização movimenta o estoque). O mesmo encadeamento vale para Compras, trocando "receber/saída" por "pagar/entrada".
-
Pedido de venda (
SellsController::add), criasells+sell_itemscomstatus='P'(Pendente) ecodeatômico viaSequenceService. Pode vincular transportador e originar uma Industrialização. Nada de financeiro/estoque ainda. -
Emissão da NF de saída (
InvoicesController::add/addjson,invoice_type='S'), criainvoices(nascestatus='P'= Em Entrega) +invoice_items, e dispara, viaInvoicesFunctionsComponent:addInvoicesPurchasesSells: liga NF ↔ venda eminvoices_purchases_sells;purchasesSells: recalcula ostatusda venda conforme as NFs vinculadas (P → em entrega/parcial);generateCPR: gera o financeiro: um lançamento emmovimentspor vencimento (o campovencimentoda NF é um JSON de datas), como conta a receber (creditodebito='C'),status='A'(em aberto), tipo de documento FATURA,valor = grandtotal ÷ nº de vencimentos,documento/historico=NF <nf> (i/n)einvoices_idpreenchido (âncora para estorno). Em compra (P/DP) seriacreditodebito='D'(conta a pagar).
-
Finalização da NF (
InvoicesController::finish), gravastatus='F'e então:stock($invoice): movimenta o estoque: para cadainvoice_item, quantidade negativa na venda (S/DS) ou positiva na compra (P/DP), aplicada emstock_balancesviaStockBalancesFunctionsComponent::balance()na dataendingdate;purchasesSells: reatualiza o status da venda.
-
Finalização da venda (
SellsController::finish), valida que existe ao menos uma NF finalizada vinculada (bloqueia se houver NF pendenteP) e marcasells.status='F'. Não toca estoque nem financeiro (o estoque já saiu no passo 3). -
Recebimento: a conta a receber gerada no passo 2 é quitada pelo fluxo de baixa comum (ver "Fluxo de baixa e geração de entrada no extrato"): ao baixar informando banco/caixa, gera a entrada no extrato e reflete no saldo.
Estorno/cancelamento (InvoicesController::cancel) desfaz na ordem inversa: generateCPR(..., cancel=true) apaga os moviments da NF (apenas se ainda em aberto A/D, recusa se já baixados) e stock(..., cancel=true) estorna o estoque.
Mapa de status dos módulos de estoque
Cada módulo de estoque tem seu próprio conjunto de status, gravados no banco como letra única e traduzidos para exibição pelo método status() do Helper do módulo (src/View/Helper/<Modulo>Helper.php), fonte da verdade da apresentação e da cor. Nunca escrever a tradução inline na view; sempre $this->Modulo->status($registro->status).
| Módulo | P |
G |
E |
A |
F |
C |
D |
Helper |
|---|---|---|---|---|---|---|---|---|
| Purchases (Compras) | Pendente | Em Entrega | Entrega Parcial | - | Finalizado | Cancelado | Excluído | PurchasesHelper |
| Sells (Vendas) | Pendente | Em Entrega | Entrega Parcial | - | Finalizado | Cancelado | Excluído | SellsHelper |
| PurchaseRequests (Requisições de compra) | Pendente | - | - | Em Andamento | Finalizado | Cancelado | - | PurchaseRequestsHelper |
| Invoices (Notas Fiscais) | Em Entrega | - | - | - | Finalizado | Cancelado | - | InvoicesHelper |
| Inventories (Inventários) | - | - | - | Ativo | - | Cancelado | - | InventoriesHelper |
| Requisitions (Requisições de estoque) | - | - | - | - | Finalizado | Cancelado | - | RequisitionsHelper |
| Industrializations (Industrialização) | Em Processo | - | - | - | Finalizado | Cancelado | - | IndustrializationsHelper |
Cores dos rótulos (classe Bootstrap label-*): Pendente = verde (success); Em Entrega = amarelo (warning); Entrega Parcial / Ativo / Em Processo (inicial) = azul (primary); Finalizado = cinza (default); Cancelado / Excluído = vermelho (danger).
Armadilhas ao ler o status cru:
- A letra
Pnão é universal: é Pendente em Purchases/Sells/PurchaseRequests, mas Em Entrega em Invoices e Em Processo em Industrializations. Aé Em Andamento em PurchaseRequests, mas Ativo em Inventories.F(Finalizado) eC(Cancelado) são os únicos consistentes em todos os módulos.D(Excluído, exclusão lógica) só existe em Purchases e Sells.
Campos adjacentes de tipo (não confundir com status), também via Helper: Invoices.type (S Venda, P Compra, DS Saída Avulsa, DP Entrada Avulsa, InvoicesHelper::type()); Requisitions.type (I Entrada, O Saída, RequisitionsHelper::type()); frete freighttype (C CIF, F FOB) em Purchases/Sells/Invoices.
28. Telas Complementares do Dashboard (drill-down)
Os cards dos dashboards (homePlan1 Pessoal e homePlan3 Empresa) oferecem telas de detalhamento ("Ver Mais" / modais) que aprofundam o resumo do card. Todas abrem em modal AJAX (link .btn_modal, view com $this->layout = 'ajax') a partir do card correspondente.
Migração (2026-07-05): essas telas foram movidas do PagesController para os controllers de domínio correspondentes (action + template + links de origem no dashboard). O PagesController passou a concentrar apenas as telas pseudo-estáticas/infra (login, cadastro, recuperação de senha, home, dashboards, auditoria). O Comparativo Mensal também migrou nesse movimento.
| Tela (rótulo no dashboard) | Controller::action | Template | Fonte de dados | Plano |
|---|---|---|---|---|
| Extratos Financeiros › Receitas | Moviments::contasReceber |
Moviments/contas_receber.ctp |
moviments crédito (creditodebito='C', status A/P/G, vencimento ≤ +1 mês) + recorrentes (moviment_recurrents) + vínculos (moviment_mergeds) |
Empresa (3) |
| Extratos Financeiros › Despesas | Moviments::contasPagar |
Moviments/contas_pagar.ctp |
Idem Receitas, para débito (creditodebito='D') |
Empresa (3) |
| Comparativo Mensal / Categorias por Mês | Moviments::categoriasMensal |
Moviments/categorias_mensal.ctp |
MovimentDashboardService::yearMoviments() (12 meses) + saldo do ano anterior (BalancesFunctions::getSaldos) + projeção de recorrentes |
Pessoal (1) e Empresa (3) |
| Fluxo de Caixa Projetado › Ver Mais | Moviments::fluxoCaixaProjetado |
Moviments/fluxo_caixa_projetado.ctp |
DashboardAnalytics::projectedCashFlow() + projectedCashFlowDetails() |
Empresa (3) |
| Margem por Produto e Cliente › Ver Mais | Sells::margensDetalhe |
Sells/margens_detalhe.ctp |
DashboardAnalytics::marginByProducts() + marginByCustomers() (12 meses) |
Empresa (3) |
| Compras por NF × Financeiro › Ver Mais | Purchases::comprasDetalhe |
Purchases/compras_detalhe.ctp |
DashboardAnalytics::purchaseInvoiceAnalysis() + purchaseInvoicesWithoutMoviment() (NFs de entrada sem título) |
Empresa (3) |
| Rotatividade e Distribuição de Produtos › Ver Mais | Products::distribuicaoProdutos |
Products/distribuicao_produtos.ctp |
DashboardAnalytics::productTurnover() + productDistribution() |
Empresa (3) |
| Relação de Pedidos | Purchases::pedidos |
Purchases/pedidos.ctp |
Compras abertas (P/A) + vendas abertas (P/G/E) |
Empresa (3) |
| Relação de Solicitações | PurchaseRequests::solicitacoes |
PurchaseRequests/solicitacoes.ctp |
purchase_requests com status P/A |
Empresa (3) |
Permissões. As actions estão registradas em $permissions['plans'] de SystemFunctionsComponent por nome de action (independente do controller que as hospeda): categoriasMensal consta em plans[1] e plans[3]; as demais, apenas em plans[3]. O gate validatePlanController (restrição de controller por plano) também passa: no Empresa a allowlist é null (todos os controllers liberados); no Pessoal, categoriasMensal roda em Moviments, que está na allowlist do plano 1.
Cruzamento com os módulos. Três dessas telas também são citadas no capítulo do seu módulo, mas a documentação consolidada é esta seção: fluxoCaixaProjetado (§13), pedidos (§16) e solicitacoes (§16).
29. Padrões Visuais e de UI
Padrão vw- (telas de visualização e formulários)
Padrão profissional em cards para view.ctp, add.ctp e edit.ctp. CSS já existente na seção "Padrão de Visualização (view.ctp)" de maksoud.css/maksoud.min.css (manter os dois em sincronia), prefixo vw-, escopado em .vw-detail. Implementado como referência em src/Template/Parameters/.
Estrutura principal: .vw-hero (cabeçalho com logo/avatar, título, badges e ações), .vw-stats (métricas-chave), .vw-grid + .vw-card (seções de conteúdo, com .vw-card-head + .vw-card-body), .vw-table-card (tabelas relacionadas) e .vw-dl (listas rótulo/valor dt/dd; valor ausente usa <span class="vw-muted">).
Cores de header obrigatórias por assunto (modificador de .vw-card-head, baixa intensidade): --system (amarelo: metadados/registro/acesso), --main (verde: dados principais), --finance (azul: financeiro), --payment (vermelho: pagamentos), --receipt (verde: recebimentos). Badges e ícones de métrica têm modificadores semânticos próprios: .vw-badge com --ok/--warn/--danger/--muted e .vw-stat-ico com is-ok/is-warn/is-danger/is-muted.
Painéis suspensos (vw-card--overflow): o .vw-card tem overflow: hidden (para arredondar os cantos), o que corta qualquer painel posicionado de forma absoluta. Cards que contenham autocomplete typeahead (.tt-menu dos list-*.js de Cliente, Fornecedor, Banco/Caixa, Tipo de Documento etc.) ou bootstrap-multiselect (banco/caixa, plano de contas) precisam da classe modificadora vw-card--overflow. Nos bootstrap-multiselect dentro de card, usar sempre dropUp: false (abrir para baixo), inclusive no de Plano de Contas, que no layout legado vinha com dropUp: true e escapava do modal.
Regras ao migrar uma tela para o padrão:
- Preservar IDs/classes/atributos que o JS usa:
#ajax-retorno, máscaras (cpfcnpjmask/cepmask/phonemask/datemask),focus,disabled/requirede omodal-footer. - Datas/horas sempre via
MyHtmlHelpere envoltas emtext-nowrap. - Em
view.ctp(que já abre em#myModal), os botõesbtn_modaldevem usardata-target="#myModal2". - Na
index.ctpdo módulo, remover'data-size' => 'sm'dosbtn_modal(Incluir/Visualizar/Editar): o layout em cards precisa da largura padrão do modal.
Padrão pf- (dashboards)
Prefixo de classes pf- para os dashboards Pessoal (home-plan1.ctp) e Empresa (home-plan3.ctp). A referência canônica é a skill painel-financeiro (.claude/skills/painel-financeiro/SKILL.md), que cataloga estrutura, paleta, layouts de corpo e gráficos.
- CSS: seção "Painel Financeiro (redesign), pf-" em
webroot/css/dashboard-maksoud.css(+.min.css, atualizar os dois). Paleta oficial: acento#2f72b8, positivo#2e9e6b, negativo#d65a4f, alerta#c9952e, neutros#7c8a9c/#9aa7b6, linhas#e7edf3. Fonte Plus Jakarta Sans (carregada emhead.ctp, escopada em.pf-card,font-variant-numeric: tabular-nums). - Chrome do card: manter o wrapper
<div class="box pf-card">(oboxdo AdminLTE preserva o collapse (data-widget="collapse")) com headerpf-head(ícone em quadrado + título + subtítulo) e corpopf-body. - Layouts de corpo reutilizáveis:
pf-total,statgrid,prev3,resumo3,ca3,ext,kv,op,fluxo, entre outros. Gráficos via Chart.js com pluginshighlightToday/highlightCurrentMonth.
Padrão rs-toggle
Segmented toggle (pill/segmented control) com prefixo rs-toggle que substitui todo controle binário de 2 opções (antes Form->radio ou Form->select com 2 valores) em todo o sistema. CSS na seção "Segmented Toggle" de maksoud.css/maksoud.min.css; JS genérico (aplica .is-active no rótulo selecionado, delegado para funcionar em modais AJAX) em webroot/js/maksoud-radiooptions.js (+ .min.js).
- Estrutura:
<div class="rs-toggle [modificadores]">com dois<label class="rs-toggle-opt [semântica] [is-active]"><input type="radio" name="X" value="Y" [checked]><i class="fa ..."></i> Texto</label>. Preservar semprename/valuedos inputs, os handlers de change porname(show/hide de banco/caixa, C/D etc.) dependem deles. - Modificadores:
--cd(Receita verde / Despesa vermelha, viars-opt--receita/rs-opt--despesa),rs-opt--on/rs-opt--off(Ativo verde / Inativo cinza),rs-opt--sim/rs-opt--nao(resposta Sim verde / Não vermelha, marcados automaticamente pelo texto do rótulo viawindow.rsTagSimNao(), que roda no load e após cada modal; detectar pelo texto evita afetar togglesS/Nde outro significado, ex.: Essencial/Não essencial),--block(largura total, para alinhar como campo de formulário) e--disabled(edição travada; inputsdisablednão são submetidos). - Add vs Edit: no add,
checked/is-activeficam fixos no default; no edit, são dinâmicos via valor da entidade (ex.:$moviment['creditodebito'] == 'C'). O estado inicial é definido no PHP, não depende de JS. - Campos já convertidos:
creditodebito, banco/caixa, origem/destino,type(Requisitions),status(Customers/Providers/Transporters),contabil(Moviments/MovimentBanks/MovimentBoxes/MovimentCards/Transfers/Costs/Plannings). Não convertidos os filtros de relatório que têm "Todos"/vazio (3+ opções).
Modais AJAX
Links com classe .btn_modal (tratados por webroot/js/maksoud-modal.js) carregam actions via AJAX; a view dos modais define $this->layout = 'ajax'. O #myModal é o modal de topo (sentinela, não renomear): é usado por add-json.js (que faz location.reload() ao gravar quando o topo é #myModal) e pelas páginas Pages/contas_pagar/contas_receber/extratos. Qualquer outro valor de data-target (#myModal2, #myModal3, ausente) gera um índice único automático (#myModal-1, #myModal-2…), o valor literal é ignorado, sem limite de profundidade nem colisão. Convenção: topo = #myModal; aninhado = #myModal2 (apenas marcador).
Destaque do item de menu ativo (server-side)
O realce do item ativo no menu lateral (classe .active no <li> + submenu treeview aberto quando a barra está expandida) é resolvido server-side, nos próprios sidebars (Element/menu/sidebar-plan1.ctp e sidebar-plan3.ctp), comparando o controller da tela atual ($this->request->params['controller']) com os controllers de cada item. Cada sidebar define dois helpers locais, $menuActive($controllers) (devolve ' active' para concatenar numa classe existente, ex.: treeview) e $menuActiveLi($controllers) (devolve o atributo class="active" inteiro, para <li> de submenu sem classe própria), ambos aceitando um controller ou uma lista. Itens sem controller próprio (ex.: Início = Pages::home) comparam também a action.
Esse mecanismo substituiu o antigo match por URL feito em JavaScript no footer.ctp (que quebrava com paginação/âncoras e dependia do nome do projeto na URL). Ao adicionar um item de menu, incluir seu controller na lista passada ao helper correspondente.
Apresentação de dados nas views
Datas e horas devem sempre usar MyHtmlHelper ($this->MyHtml->date(), tinyDate(), hour()). Nunca usar date()/strtotime()/i18nFormat() cru nas views. Valores de domínio (status, tipo, plano) devem usar o Helper do módulo correspondente.
30. Segurança
Blindagem de SQL Injection em filtros e relatórios
Filtros de busca e relatórios nunca concatenam dados do request diretamente em SQL. Valores vão por parameter binding do ORM (['Tabela.col LIKE' => '%'.$v.'%']). Identificadores (ORDER BY, coluna de data) não são bindáveis pelo PDO e passam por whitelist via SearchFilterService::safeOrder(). Listas de IDs passam por SearchFilterService::intList().
Mass-assignment por allowlist em Moviment
A entity src/Model/Entity/Moviment.php usa _accessible como allowlist. Campos server-only (parameters_id, valorbaixa, userbaixa, username, ordem, id) são bloqueados para mass-assignment e definidos diretamente no controller/service. Services confiáveis liberam campos pontuais via patchEntity(..., ['accessibleFields' => [...]]).
Bloqueio server-side de campos disabled
Um campo HTML com atributo disabled não é enviado no POST, mas pode ser reabilitado no navegador (DevTools/manipulação do DOM) e passar a ser submetido. Por isso, os campos que a tela trava (disabled) porque o registro não pode tê-los alterados (tipicamente valor/valorbaixa/creditodebito de registros vinculados (movimentos gerados por CPR/transferência ou com baixa parcial/vínculos)) são forçados no servidor a partir do próprio registro do banco, sobrepondo o que veio no request->data antes do patchEntity/save. A auditoria de 2026-07-05 estendeu essa blindagem para os quatro controllers de movimento: MovimentsController, MovimentBanksController, MovimentBoxesController e MovimentCardsController. (As entidades desses módulos de movimento não usam allowlist de mass-assignment como Moviment, daí a proteção ser feita no controller.)
Guarda de relatórios (ReportGuardService)
Dois limites em todos os relatórios financeiros e de estoque com volume:
- Período máximo: 12 meses (
MAX_MONTHS). - Teto de linhas: 5.000 registros (
MAX_ROWS).
Política: bloquear e avisar, nunca truncar.
Política de senha (PasswordPolicyService)
Requisitos mínimos: 8+ caracteres, ao menos uma minúscula, uma maiúscula e um caractere especial. Validado server-side em PagesController::cadastrar e em UsersTable::validationDefault (add/edit/changePass). O front-end exibe checklist de regras apenas como feedback visual.
Reset de senha por token (PasswordResetService)
Fluxo "Esqueci minha senha" via token de 8 caracteres, TTL de 10 minutos. Armazenado como hash('sha256', token) na tabela global password_resets (sem parameters_id, tabela de sistema). Anti-enumeração (mensagem genérica), honeypot e reCAPTCHA. Usuário não-Super não pode alterar o próprio e-mail (UsersController::edit trava o campo username).
Endurecimento anti-bot dos formulários públicos
Todos os formulários acessíveis sem autenticação (login, cadastro (cadastrar), esqueci minha senha (rememberPassword, que dispara o envio do código) e redefinição de senha (recuperaSenha)) recebem a mesma dupla camada anti-bot:
- Honeypot: campo isca invisível ao usuário (
website). Se vier preenchido, é bot, a requisição é descartada em silêncio (sem pista de que foi detectada) e, no login, o IP já é bloqueado (IpBlockerService::forceBlockIpManually), pois é tentativa inequívoca. - Google reCAPTCHA v3: token validado server-side em
PagesController::verificaRecaptcha()(chaves emconfig/app.php>Recaptcha.siteKey/secretKey/minScore/action). Score abaixo do mínimo, action divergente ou token ausente/inválido barram a operação. Fail-open enquanto as chaves não estiverem configuradas e emlocalhost(a siteKey só vale no domínio de produção), para não travar desenvolvimento.
Throttle de login (LoginThrottleService)
Proteção anti-força-bruta / credential stuffing da tela de login, por IP, usando as tabelas login_attempts e login_blocks:
- A cada 4 falhas consecutivas (
MAX_CONSECUTIVE_FAILS) → bloqueio temporário de 20 minutos (TEMP_BLOCK_MINUTES, tabelalogin_blocks) + alerta por e-mail ao operador (Cron.alertEmail, templatelogin_bloqueio). - Reincidência no mesmo dia (novo estouro do limite após já ter sido bloqueado hoje) → bloqueio definitivo, reaproveitando
IpBlockerService(blocked_ips+ middleware, que passa a responder 403 direto). - O IP em bloqueio temporário é barrado antes de tentar autenticar (
blockedUntil()); login bem-sucedido zera o contador (clearFailures()). - Resiliência (fail-open): toda operação de banco é protegida por
try/catch. Se a infraestrutura do throttle falhar (tabela ausente, banco fora), o serviço nunca propaga a exceção, o login segue funcionando. Um recurso de segurança auxiliar não pode derrubar a autenticação.
Bloqueio de bots por IP (IpBlockerService)
Match por padrão de família (substring sobre o path normalizado) e regex de extensão de script. IP bloqueado com path proibido retorna HTTP 403 direto no middleware (sem subir exceção). robots.txt e sitemap.xml são arquivos físicos em webroot/ (crawlers legítimos não são bloqueados).
Proteção IDOR em operações de mutação
O padrão Table->get($id) sem filtro por parameters_id em actions de edição/exclusão é vulnerável a IDOR cross-perfil. Foi corrigido em Sells, Purchases e Requisitions. Outros controllers devem ser auditados.
31. Funcionalidades Exclusivas do Usuário Super
O usuário com sessionRule = 'super' tem acesso irrestrito ao mapa de permissões (validaAcesso retorna true diretamente). A restrição de controller por plano ainda se aplica ao Super.
Funcionalidades acessíveis somente ao Super:
Gestão de Perfis (Parameters)
O Super visualiza todos os perfis cadastrados no sistema na listagem de Parameters (outros usuários veem apenas o próprio perfil). Pode criar, editar e excluir qualquer perfil.
Escrita exclusiva do Super, alteração de dados do perfil via suporte. As actions de escrita de Parameters (edit, editNovo e admin) são barradas para não-super por blockParameterWriteIfNotSuper(), que exibe um Flash->warning ("Alterações nos dados do perfil devem ser solicitadas pelo suporte.") e redireciona. O plano/validade (admin) sempre foi só do Super. Ao não-super sobra a consulta somente-leitura dos dados da empresa: o item "Dados da Empresa" do menu superior "Minha Conta" abre Parameters::view do perfil em uso (sessionParameterControl). O view blinda contra IDOR, se o não-super adulterar o id na URL para outro perfil, é redirecionado ("Você só pode visualizar os dados do perfil em uso."); para consultar outro perfil vinculado, precisa antes trocar de perfil (changeParameter). A view.ctp mostra ao não-super um aviso ("somente para consulta… abra um chamado no suporte") e oculta os botões de ação.
Gestão de Usuários
O Super visualiza todos os usuários do sistema, independente do perfil ativo. Não-supers veem apenas os usuários do próprio perfil. O Super pode auditar o histórico de acesso dos usuários (auditoria via RegsController).
Escrita exclusiva do Super, alteração de dados de usuário via suporte. As actions de escrita de Users (add, edit, changePass, delete) são barradas para não-super por blockUserWriteIfNotSuper() (mesmo padrão do Parameters). Ao não-super sobra a consulta somente-leitura do próprio cadastro: o item "Meu Usuário" do menu superior "Minha Conta" abre Users::view do próprio id (userid da sessão). O view blinda contra IDOR, o não-super só visualiza o próprio usuário (adulterar o id para outro redireciona: "Você só pode visualizar o seu próprio usuário."). A view.ctp exibe o aviso de consulta + link para o suporte.
Exceção, troca da própria senha é self-service (não passa pelo suporte). A view.ctp do não-super traz o botão "Trocar minha senha" (postLink → Users::solicitarTrocaSenha), que reaproveita o fluxo de "Esqueci minha senha": gera um código de uso único (PasswordResetService::createReset), envia-o ao e-mail do próprio usuário autenticado (template token_redefinicao, TTL curto, gravado só como hash) e redireciona para Pages::recuperaSenha já com o e-mail preenchido. Como o alvo é sempre o usuário logado (nunca vindo do request) e o código vai para o e-mail cadastrado dele, dispensa reCAPTCHA. A action está na allowlist common do mapa de permissões.
Registros de Atividade (Regs)
RegsController (tela "Registros") lista as operações registradas pelo LogBehavior. Não é exclusivo do Super: a action index está no mapa de permissões da função admin, então admins também acessam, mas o filter() do controller restringe usuários não-super ao próprio parameters_id (só veem os registros do próprio perfil, e o filtro por perfil na busca é ignorado para eles). O Super vê os registros de todos os perfis e pode filtrar por perfil/usuário/período. view e delete seguem a mesma regra de escopo por parameters_id.
Logs do Sistema (SystemLogs)
SystemLogsController::viewSystemLog exibe o conteúdo dos arquivos error.log, debug.log e cancellations.log. viewUpdateLog exibe git.log e logBD.log (log de operações no banco). Tela exclusiva do Super.
Modo de Manutenção (Settings)
SettingsController permite ao Super ativar/desativar o modo de manutenção do sistema e selecionar quais planos são afetados. O beforeFilter do SettingsController bloqueia o acesso de qualquer não-Super com redirect ao login.
Bloqueador de IPs (IpBlocker)
IpBlockerController::viewIpBlockerLog exibe o log de acessos bloqueados. A action clearLogs permite limpar o arquivo de log (requer função admin, portanto o Super também acessa). A gestão de IPs bloqueados na tabela blocked_ips é feita via interface exclusiva.
Throttle de login (força bruta / credential stuffing), LoginThrottleService: o login também alimenta o bloqueio de IPs. A cada 4 falhas consecutivas (MAX_CONSECUTIVE_FAILS) do mesmo IP, aplica-se um bloqueio temporário de 20 minutos (TEMP_BLOCK_MINUTES), registrado na tabela login_blocks (as tentativas ficam em login_attempts). Se o mesmo IP voltar a estourar o limite no mesmo dia (reincidência), escala para bloqueio definitivo, reaproveitando a infraestrutura do IpBlocker (IpBlockerService::forceBlockIpManually grava em blocked_ips e o middleware passa a responder 403 direto). Fluxo no PagesController::login:
- Antes de autenticar,
blockedUntil()barra o POST cedo se o IP estiver em bloqueio temporário vigente (o definitivo nem chega aqui, já é 403 no middleware). - Login bem-sucedido chama
clearFailures()(zera o contador do IP). - Cada falha chama
registerFailure(), que retornanone/temp/permanent; emtemp/permanentdispara e-mail de alerta ao operador (Cron.alertEmail, eventologin_bloqueio).
O serviço é fail-open: toda operação de banco é protegida por try/catch e nunca propaga exceção, se a infraestrutura do throttle falhar (tabela ausente, banco fora), o login segue funcionando (um recurso de segurança auxiliar não pode derrubar a autenticação). O login ainda conta com honeypot (campo isca) e reCAPTCHA (verificaRecaptcha, fail-open enquanto as chaves não estiverem configuradas).
Telemetria de Uso (FeatureUsages)
FeatureUsagesController exibe o painel de uso de funcionalidades: lista todos os controllers/actions do sistema (descobertos por reflexão) cruzados com o contador de acessos por perfil, inclusive os com zero acessos. Permite identificar telas não usadas e as mais acessadas. Exclusivo do Super (beforeFilter bloqueia não-super).
Reset dos contadores: não há rotina periódica/automática de limpeza, os contadores (feature_usages.hits, first_access, last_access) acumulam indefinidamente. A reinicialização é manual, pela action reset() (POST), que zera todos os contadores (deleteAll) para recomeçar a medição a partir de uma data.
Backups
BackupsController gerencia os dumps .sql do banco, gravados em BACKUP_DIR (ROOT/backups/, fora do webroot). Actions:
index: lista os backups locais (nome, data de modificação e tamanho em MB, ordenados do mais recente para o mais antigo) e permite a exclusão em lote dos selecionados (POST comrecord[]). Após excluir, chamaupdateSizeAlert()para recalcular o uso de disco (o alerta do super some se a limpeza trouxe a pasta de volta para dentro do limite).download($file): baixa um arquivo de backup; exige funçãoadmin(ou superior).delete($file): exclui um único backup (POST/DELETE viapostLink); exige funçãosuper. Também recalculaupdateSizeAlert().backupAutomated: dispara manualmente um backup completo (BackupsFunctions::dbExportAutomated()).
Geração automática: o backup do banco é gerado pelo CronController::cron (protegido por token, config/app.php > Cron.token), antes do corte diário e independentemente do marcador do dia. É idempotente por dia: backupExistsForToday() verifica se já existe um dump com o prefixo da data de hoje e, se houver, pula a geração, assim o CRON pode ser chamado várias vezes no mesmo dia (retry se a 1ª falhou, ex.: banco em manutenção) sem empilhar arquivos e estourar o disco. A geração roda em best-effort (try/catch): falha é logada e reportada por e-mail ao operador, sem interromper as demais rotinas do cron.
Segurança: os nomes de arquivo vindos de POST/URL passam por basename(utf8_decode(...)), neutralizando path traversal (../). A pasta fica fora do webroot (não é servida pela web).
Saldos (Balances: administração)
BalancesController::index (tela de administração de saldos) requer action super explícita e só é acessível pelo Super. Permite visualizar e corrigir saldos consolidados.
BalancesController::superAddBalance permite incluir um saldo manualmente para um banco, caixa, cartão ou planejamento em uma data específica.
Verificação de Saldos (Balances: checagem)
BalancesController::checkBalanceBanks e checkBalanceBoxes verificam a consistência dos saldos calculados de bancos e caixas respectivamente, comparando com os movimentos registrados.
Alertas de Diagnóstico no Dashboard
O dashboard do Super exibe um alerta quando o cron diário não foi executado no dia, com link para acionamento manual.
Alerta de consumo de disco (backups > 3 GB): quando a pasta de backups ultrapassa o limite de 3 GiB (BackupsFunctionsComponent::BACKUP_DIR_ALERT_BYTES = 3221225472 bytes), o Super é avisado para fazer a limpeza manual dos backups antigos. O updateSizeAlert() (chamado a cada execução do CRON e após exclusões manuais de backup) recalcula o tamanho da pasta e grava em Settings as flags backup_dir_size (bytes) e backup_dir_oversize ('1'/'0'). Como o CRON roda sem sessão, o alerta não sai de lá: o AppController lê a flag backup_dir_oversize no próximo acesso e, se for super e em navegação normal (não-AJAX), dispara um Flash->error informando o tamanho atual. Na transição de dentro→acima do limite (justCrossed), o CRON envia uma única vez um e-mail ao operador (Cron.alertEmail, template backup_disk_alert_template), sem repetir a cada execução enquanto seguir cheio. O alerta some automaticamente quando a limpeza traz a pasta de volta para dentro do limite.
Base de Conhecimento (Knowledges): global, segmentada por plano
KnowledgesController gerencia artigos de base de conhecimento. A base é global (não é multi-tenant): index/view não filtram por parameters_id, os artigos não pertencem a um perfil específico. A leitura (index/view) está disponível para qualquer usuário logado; o CRUD de escrita (add/edit/delete) exige função super (validado por validaAcesso dentro da própria action).
Segmentação por plano (plans_id): cada artigo pode ser destinado a um plano específico (1=Pessoal, 3=Empresa) ou a ambos (plans_id NULL, "Ambos os Planos"). No index, o usuário não-super só enxerga os artigos do plano do seu perfil ativo (sessionPlan) somados aos gerais (plans_id NULL); o super gerencia e vê todos. O formulário normaliza o campo via normalizePlan() (valor vazio ou fora de {1,3} → NULL). A busca do index filtra por título (title_search) e por plano (plans_search, com whitelist {1,3}), ambos por binding do ORM (blindagem SQLi).
Contatos de Suporte (SupportContacts): segmentado por perfil, com histórico de mensagens
SupportContactsController gerencia os chamados de suporte. É segmentado por perfil: usuários não-super só veem/editam os chamados do próprio parameters_id (o index/view adicionam esse where); o Super vê os chamados de todos os perfis. Cada chamado tem número sequencial próprio por perfil (ordem, via MAX(ordem)+1).
Ciclo do chamado (status): A=Aberto, B=Respondido, F=Finalizado.
- O solicitante abre o chamado (
add); o texto de abertura fica emsupport_contacts.descricao. - O Super responde (
addResponse→B) e finaliza (finaliza→F). - Sobre um chamado já respondido (
B), o solicitante pode replicar (addMessage), o que reabre o chamado (status='A') para nova análise do suporte. Só chamadosBaceitam réplica (os em aberto já estão na fila; os finalizados estão encerrados). - Edição/exclusão só em
A: depois de respondido, o chamado preserva o histórico,editedeletesão barrados fora do statusA.
Histórico de mensagens (thread), tabela support_contact_messages: as respostas do suporte e as réplicas do solicitante são registradas como mensagens (origem U=solicitante / S=suporte, autor, mensagem ≤300 chars), preservando o encadeamento da conversa. A entity usa allowlist de mass-assignment (só mensagem é marshalável; support_contacts_id, parameters_id, origem e autor são server-only, origem não pode ser forjada para se passar por suporte). Mensagem + novo status são gravados em transação (connection()->transactional), mantendo histórico e estado juntos. A migração 20260706 support_contact_messages.sql cria a tabela e faz o backfill da coluna legada support_contacts.resposta como a primeira mensagem do suporte.
Notificação no dashboard (AppController::session): o Super é avisado dos chamados abertos (A = novos + réplicas) de todos os perfis (fila de atendimento); o solicitante é avisado dos seus chamados respondidos (B) que aguardam conferência, contando apenas o perfil ativo.
Os disparos de e-mail de notificação de resposta existem no código mas seguem comentados/desativados (template support_contact, ver §26).
Assinaturas (observação)
Não há, hoje, gestão global de assinaturas pelo Super. Todas as actions de SubscriptionsController (assinatura, checkout, portal) operam exclusivamente sobre o perfil ativo (currentParameter()), sem parâmetro para atuar em outro perfil. Para efeito administrativo, o Super ajusta plano/validade de um perfil diretamente pela Gestão de Perfis (Parameters), os campos plans_id, dtvalidade e stripe_customer_id. A baixa efetiva de cobrança continua vindo do webhook do Stripe.
Documento gerado em 2026-07-04 e revisado no mesmo dia após investigação no código-fonte (resolução das pendências listadas em MELHORIAS_DOCUMENTACAO.md). Revisado em 2026-07-06 para incorporar os commits de 03–05/07: competência do lançamento (§7), Dicas Financeiras/"Análise com IA" (§27), bloqueio server-side de campos disabled (§30), destaque server-side do item de menu ativo (§29) e a versão em inglês da interface (§1). Revisado novamente em 2026-07-06 para os commits de 06/07: histórico de mensagens (thread) dos Chamados de Suporte (§31), opt-in self-service dos e-mails do CRON (§26) e o modificador Sim/Não do rs-toggle (§29). Fonte primária: CLAUDE.md e código-fonte em src/.