Endpoint de débitos por cliente

TLDR: novo GET /api/v1/customers/:customer_id/charges/ que devolve todos os débitos de um cliente com parcelas, totais e produtos, para hidratar a aba Financeiro do detalhe do cliente.

Contexto

A aba Financeiro (ChargesTab) do detalhe do cliente mostra, por débito, a tabela de parcelas (Nº parcela, Valor original, Vencimento, Juros, Desconto, Valor atual, Status, Data de pagamento) e os totais do débito. Hoje não existe endpoint que traga os débitos de um cliente específico: GET /api/v1/debits só filtra por search (nome/CPF via LIKE) e status, e o DebitSerializer não inclui as parcelas.

Objetivos

  • Expor GET /api/v1/customers/:customer_id/charges/ com todos os débitos do cliente, sem paginação.
  • Cada débito traz: parcelas, total de parcelas, total a pagar e produtos do débito.
  • Cada parcela traz: número, valor original, vencimento, juros, desconto, valor atual, status e data de pagamento.
  • Criar em installments os campos de juros e desconto, que não existem hoje.
  • Mostrar todos os débitos do cliente para qualquer usuário autenticado, sem a restrição por responsável do DebitsController (current_user.debits).
  • Débitos em aberto (Debit::OPEN_STATUSES) vêm primeiro, depois os demais.

Fora de escopo

  • Preenchimento de juros e desconto: esta mudança só cria as colunas (com 0 por padrão) e as expõe. Calcular ou sincronizar os valores (gateway, regras de quitação R-002) é uma mudança separada.
  • Recalcular o valor atual: current_amount_cents continua sendo payable_amount_cents; os novos campos não entram nesse cálculo.
  • Integração do front-end: o detalhe do cliente ainda lê o cliente de mock (INITIAL_CUSTOMERS), então ligar a ChargesTab neste endpoint é uma mudança separada.
  • Paginação, filtros e ordenação configurável pelo cliente da API.

Mudanças

modules/backend/db/migrate/<timestamp>_add_interest_and_discount_to_installments.rb (novo)

Hoje installments não tem campo de juros nem de desconto (só amount_cents e updated_amount_cents, que já vem com os ajustes somados). A migration cria os dois:

ruby class AddInterestAndDiscountToInstallments < ActiveRecord::Migration[8.1] def change add_column :installments, :interest_cents, :integer, default: 0, null: false add_column :installments, :discount_cents, :integer, default: 0, null: false end end

Parcelas existentes ficam com 0 nos dois campos.

modules/backend/app/models/installment.rb

  • Validações: interest_cents e discount_cents inteiros, >= 0.
  • Testes em test/models/installment_test.rb: valor negativo em interest_cents ou discount_cents é inválido.
  • Atualizar o bloco de anotação do schema (também em test/models/installment_test.rb e test/fixtures/installments.yml).

modules/backend/db/seeds.rb

  • Parcelas overdue do seed passam a ter interest_cents aleatório (> 0); discount_cents segue 0.

modules/backend/config/routes.rb

Nova rota dentro de namespace :api / :v1:

ruby resources :customers, only: [] do resources :charges, only: [ :index ], controller: :customer_charges end

modules/backend/app/controllers/api/v1/customer_charges_controller.rb (novo)

Api::V1::CustomerChargesController < BaseController, action index: renderiza os débitos como { data: [...] } com DebitSerializer. Cliente existente sem débitos → 200 com data: [].

A consulta fica no método privado debits do controller:

  • Busca o cliente com Customer.find(params[:customer_id]); cliente inexistente levanta ActiveRecord::RecordNotFound, que o BaseController responde com 404.
  • Escopo: customer.debits — todos os débitos do cliente, para qualquer usuário autenticado.
  • includes(:customer, :installments, :product_debits).
  • Ordenação no banco com in_order_of(:status, Debit::OPEN_STATUSES, filter: false): primeiro os débitos em aberto, na ordem de OPEN_STATUSES (pending, no_forecast, no_response, bureau_report); depois os demais (negotiated, cancelled, negativated), sem ordem definida entre si.

modules/backend/app/serializers/debit_serializer.rb

Serializer único das listagens (GET /debits e GET /customers/:customer_id/charges). Os campos abaixo foram acrescentados aos que já existiam (customer_*, installment_amount_cents, next_due_on, opened_at, first_installment_due_on).

Campo Origem
id debit.id
status debit.status
kind debit.payment_type
installments_count quantidade de parcelas carregadas (Total de parcelas)
total_cents soma de installments.amount_cents (valor original)
total_payable_cents soma de installments.payable_amount_cents de todas as parcelas, pagas inclusas (Total a pagar)
products [{ name: product_debit.product_name, external_id: product_debit.external_id }]
installments lista ordenada por number (ver abaixo)

Cada item de installments:

Campo Coluna da tela Origem
number Nº parcela installment.number
amount_cents Valor original installment.amount_cents
due_on Vencimento installment.due_on (ISO 8601)
interest_cents Juros installment.interest_cents
discount_cents Desconto installment.discount_cents
current_amount_cents Valor atual installment.payable_amount_cents (updated_amount_cents ou, na falta, amount_cents)
status Status installment.status (paid, upcoming, overdue, defaulted)
paid_at Data de pagamento installment.paid_at (ISO 8601 ou null)

Exemplo:

json { "data": [{ "id": 7, "status": "pending", "kind": "installment", "installments_count": 12, "total_cents": 120000, "total_payable_cents": 126000, "products": [{ "name": "Curso X", "external_id": "abc" }], "installments": [{ "number": 1, "amount_cents": 10000, "due_on": "2026-08-10", "interest_cents": 500, "discount_cents": 0, "current_amount_cents": 10500, "status": "overdue", "paid_at": null }] }] }

modules/backend/test/controllers/api/v1/customer_charges_controller_test.rb (novo)

Testes de integração usando as fixtures existentes (customers, debits, installments, product_debits, users):

  • lista todos os débitos do cliente, com os campos e totais esperados.
  • attendant vê todos os débitos do cliente, inclusive os que não são atribuídos a ele.
  • débitos em aberto vêm antes dos pagos.
  • parcelas vêm ordenadas por number, e current_amount_cents usa updated_amount_cents quando existe.
  • interest_cents e discount_cents vêm na parcela.
  • débitos de outro cliente não aparecem.
  • cliente inexistente → 404.
  • sem token → 401.

Como verificar

  • bin/rails db:migrate em modules/backend roda sem erro e db/schema.rb passa a ter interest_cents e discount_cents em installments.
  • make backend.run.test na raiz (ou bin/rails test test/controllers/api/v1/customer_charges_controller_test.rb em modules/backend) passa.
  • Com o servidor rodando e autenticado como admin, GET /api/v1/customers/:customer_id/charges/ de um cliente com seed devolve o payload no formato acima.

Documentação

  • .project/docs/README.md: adicionar esta spec ao índice de specs/.