Audit log do cliente — estrutura e listagem

TLDR: nova tabela audit_logs só de inserção, onde cada linha é um evento de negócio sobre um cliente (affected_id), disparado por um usuário, pelo sistema, pelo checkout ou pelo Asaas (sender_type/sender_id). Entra também o GET /api/v1/customers/:customer_id/audit_logs, paginado e com filtro por categoria e origem no servidor, ligado ao card “Últimas movimentações” e à aba Auditoria no front. Gravar logs a partir das ações de negócio fica para uma spec seguinte.

Contexto

A ficha do cliente tem a aba “Auditoria” e um atalho “Ver auditoria completa” na aba Pessoal (R-007). Hoje não existe nenhum registro de quem fez o quê com um cliente no backend.

O audit log aqui registra eventos de negócio (ex.: “contrato cancelado”, “carteira transferida”), e não diffs genéricos de coluna. Por isso a gravação é explícita nos use cases, sem callback de model e sem gem (audited, paper_trail). Callback não sabe o motivo da mudança e não dispara em update_all (usado em Debits::TransferPortfolio).

Todo evento é sobre um cliente: Debit e Contract pertencem a Customer, e Installment, Negotiation e RegistryNegativation pertencem a um Debit. O registro específico (débito, contrato, parcela) vai no payload.

Quem dispara o evento (sender) é de um destes quatro tipos:

  • user: um colaborador logado, pela API;
  • system: jobs, posts do cobrança;
  • checkout: eventos vindos do checkout, que ainda não tem integração no código;
  • asaas: eventos vindos do Asaas, que ainda não tem integração no código;

Objetivos

  • Tabela audit_logs e model AuditLog, só de inserção.
  • GET /api/v1/customers/:customer_id/audit_logs paginado, mais recente primeiro.

Fora de escopo

  • Gravar logs a partir das ações existentes (contratos, débitos, negativação, import do checkout). Vai numa spec própria, que também define a lista fechada de event_type e como cada ação grava.
  • Integração com o checkout e com o Asaas. Os tipos checkout e asaas já existem no enum, mas nada grava com eles ainda.
  • Ações de colaborador (convidar, ativar, desativar, trocar papel, trocar senha). Não têm cliente, então não cabem numa tabela com affected_id obrigatório.
  • Autorização por papel no endpoint. Decisão do usuário: a restrição a gestor (admin/director) fica só no front (R-007). Qualquer colaborador autenticado consegue ler o histórico chamando a API direto. Isso contraria a nota de restrições da própria R-007 (“não substitui a validação de alçada no servidor”) e foi aceito conscientemente.
  • Filtros no endpoint (por event_type, sender_type, data).
  • Retenção, arquivamento ou particionamento da tabela.

Mudanças

Todos os caminhos são relativos a modules/backend/.

Migration — db/migrate/<ts>_create_audit_logs.rb

Coluna Tipo Regra
id bigint PK
affected_id bigint, not null FK para customers, on_delete: :restrict. O cliente afetado
sender_type string, not null user · system · checkout · asaas
sender_id bigint, null FK para users. Preenchido só quando sender_type = user
event_type string, not null ex.: contract.cancelled
payload jsonb, not null, default {} detalhe do evento (debit_id, contract_id, antes/depois, motivo)
created_at datetime, not null sem updated_at

Índices:

  • (affected_id, created_at): a timeline do cliente, que é a consulta do endpoint;
  • (sender_type, sender_id): “o que esse usuário fez”, para uso futuro;
  • event_type.

Model — app/models/audit_log.rb

  • belongs_to :affected, class_name: "Customer".
  • belongs_to :sender, class_name: "User", optional: true. Não é polimórfico: system, checkout e asaas não são registros.
  • enumerize :sender_type, in: %i[user system checkout asaas], com predicates prefixados (sender_type_user?) para não colidir com nada do model.
  • Validações:
    • affected, sender_type e event_type presentes;
    • sender_id obrigatório quando sender_type = user, e nulo nos outros tipos.
  • event_type só tem validação de presença. A lista fechada entra junto com os primeiros eventos instrumentados (ver “Fora de escopo”).
  • Só de inserção: readonly? retorna true quando persisted?, então update e destroy de um registro gravado levantam ActiveRecord::ReadOnlyRecord.
  • paginates_per 9 e max_paginates_per 50.
  • Scope recent_first: order(created_at: :desc, id: :desc).

Customer — app/models/customer.rb

  • has_many :audit_logs, foreign_key: :affected_id, inverse_of: :affected, dependent: :restrict_with_error.

Rota — config/routes.rb

Aninhada em customers, igual a charges e summary:

ruby resources :customers, only: [ :index, :show ] do resources :charges, only: [ :index ], controller: :customer_charges resource :summary, only: [ :show ], controller: :customer_charges resources :audit_logs, only: [ :index ], controller: :customer_audit_logs end

Controller — app/controllers/api/v1/customer_audit_logs_controller.rb

  • index: Customer.find(params[:customer_id]). Cliente inexistente devolve 404 pelo rescue_from do BaseController.
  • customer.audit_logs.includes(:sender).recent_first.page(params[:page]).per(params[:per_page]).
  • Resposta no padrão do CustomersController, com Paginatable:

json { "data": [ { "id": 1, "event_type": "contract.cancelled", "sender_type": "user", "sender": { "id": 7, "name": "Maria" }, "payload": { "contract_id": 34, "debit_id": 12, "message": "Emitiu reparcelamento 8x R$ 250,01 no Asaas " }, "created_at": "2026-09-29T10:00:00-03:00" } ], "meta": { "current_page": 1, "next_page": 2, "prev_page": null, "total_pages": 3, "total_count": 27 } }

Os dois usos no front:

  • Card (3 mais recentes): ?per_page=3;
  • Tabela: ?page=N, com 9 itens por página (default do model).

Serializer — app/serializers/audit_log_serializer.rb

  • id, event_type, sender_type (string), payload, created_at.
  • sender: { id, name } quando sender_type = user, e null nos outros casos. O front mostra “Sistema”, “Checkout” ou “Asaas” a partir do sender_type.

Seeds

Seguindo commons:seed: alguns AuditLog de exemplo por cliente do seed, cobrindo os quatro sender_type, para a aba Auditoria ter conteúdo no ambiente local.

Como verificar

Testes (Minitest, padrão do projeto):

  • test/models/audit_log_test.rb
    • válido com sender_type: user e sender;
    • inválido com sender_type: user sem sender;
    • inválido com sender_type system, checkout ou asaas e sender preenchido;
    • payload default {};
    • update e destroy de registro persistido levantam ActiveRecord::ReadOnlyRecord.
  • test/controllers/api/v1/customer_audit_logs_controller_test.rb
    • devolve só os logs do cliente pedido, do mais recente para o mais antigo;
    • 9 por página por default, e per_page=3 devolve 3;
    • per_page acima de 50 fica limitado a 50;
    • meta com total_count correto;
    • sender com id e name para user, null para system;
    • 404 para cliente inexistente;
    • 401 sem token.

Manual: make do backend com seed, GET /api/v1/customers/<id>/audit_logs?per_page=3 com o JWT de um colaborador.

Documentação

  • Criar .project/docs/rules/audit/customer_audit_log.md: o que é um evento de auditoria, os quatro tipos de sender, a regra de só inserção, e a decisão de autorização só no front.
  • Atualizar o índice .project/docs/README.md com a spec e a regra nova.