Audit log do cliente — estrutura e listagem
TLDR: nova tabela
audit_logssó 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 oGET /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_logse modelAuditLog, só de inserção. GET /api/v1/customers/:customer_id/audit_logspaginado, 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_typee como cada ação grava. - Integração com o checkout e com o Asaas. Os tipos
checkouteasaasjá 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_idobrigató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,checkouteasaasnã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_typeeevent_typepresentes;sender_idobrigatório quandosender_type = user, e nulo nos outros tipos.
event_typesó 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?retornatruequandopersisted?, entãoupdateedestroyde um registro gravado levantamActiveRecord::ReadOnlyRecord. paginates_per 9emax_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 devolve404pelorescue_fromdoBaseController.customer.audit_logs.includes(:sender).recent_first.page(params[:page]).per(params[:per_page]).- Resposta no padrão do
CustomersController, comPaginatable:
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 }quandosender_type = user, enullnos outros casos. O front mostra “Sistema”, “Checkout” ou “Asaas” a partir dosender_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: useresender; - inválido com
sender_type: usersemsender; - inválido com
sender_typesystem,checkoutouasaasesenderpreenchido; payloaddefault{};updateedestroyde registro persistido levantamActiveRecord::ReadOnlyRecord.
- válido com
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=3devolve 3; per_pageacima de 50 fica limitado a 50;metacomtotal_countcorreto;sendercomidenameparauser,nullparasystem;404para cliente inexistente;401sem 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.mdcom a spec e a regra nova.