Integração backend da fila de negativação — Plano de implementação

TLDR: job diário popula registry_negativations; GET endpoint lê do registry; frontend conectado sem mocks.

Spec: .project/docs/specs/20260917115417_negativation_backend_integration.md Branch: feat/RegistryNegativation

Arquitetura: NegativationQueueJob roda às 01:00 via solid_queue, avalia Debit.eligible_for_negativation e popula registry_negativations. O controller lê do registry diretamente — sem derivar elegibilidade em tempo real.

Stack: Rails 8.1 + solid_queue (minitest, fixtures), React + TypeScript (vitest).

Status das tasks

Task Descrição Status
1 Migration + Model RegistryNegativation ✅ feito
2 NegativationQueueJob + solid_queue ✅ feito
3 Controller + Rota (queue) + Serializer ✅ feito
4 Debit.eligible_for_negativation scope + testes unitários diretos ✅ feito
5 Testes para NegativationQueueJob ✅ feito
6 Controller lendo do registry + testes ✅ feito

Task 1: Migration + Model RegistryNegativation ✅

Files criados/modificados: - db/migrate/20260917143700_create_registry_negativations.rb - db/schema.rb - app/models/registry_negativation.rb - app/models/debit.rb — has_many :registry_negativations - app/models/installment.rb — belongs_to :registry_negativation, optional: true - test/fixtures/registry_negativations.yml - test/models/registry_negativation_test.rb

O que foi feito: - Tabela registry_negativations com colunas: debit_id, executed_by_id, closed_by_id, executed_at, closed_at, gateway_ref, status (default pending) - Coluna registry_negativation_id adicionada em installments (FK opcional) - Status enumerado: pending, negative, closed, cancelled - Scope active_with_debit no model: exclui closed e cancelled, faz eager load de debit: [:customer, :installments, :product_debits, :user] - Schema atualizado: installments_count com default 0, total_cents removido, status default de installment alterado para upcoming

Testes implementados (registry_negativation_test.rb): - defaults to pending status - requires executed_at - debit has many registry_negativations - associates installments directly


Task 2: NegativationQueueJob + solid_queue ✅

Files criados/modificados: - Gemfile + Gemfile.lock — gem solid_queue - config/environments/production.rb — adapter solid_queue, connects_to queue - config/queue.yml — workers com 3 threads, polling 1s - config/recurring.yml — NegativationQueueJob todo dia às 01:00 (production) - bin/jobs — entrypoint do solid_queue CLI - db/queue_schema.rb — schema separado para o banco de filas - app/jobs/negativation_queue_job.rb - app/use_cases/negativation/register_eligible_debit.rb

O que foi feito: - NegativationQueueJob#perform chama Negativation::RegisterEligibleDebit.call(manager:), onde o manager é buscado via ENV["MANAGER_DEFAULT_ID"] - Negativation::RegisterEligibleDebit.call: itera Debit.open.eligible_for_negativation e faz find_or_create_by! em RegistryNegativation — idempotente por design - Job configurado na fila :default


Task 3: Controller + Rota + Serializer ✅

Files criados/modificados: - config/routes.rb — GET /api/v1/negativation/queue - app/controllers/api/v1/negativation_controller.rb - app/serializers/negativation_queue_serializer.rb

O que foi feito: - Controller usa RegistryNegativation.active_with_debit (scope do model) - NegativationQueueSerializer.call(registries) — retorna array de rows, uma por product_debit - Cada row: name, product, progress, installments (contagem overdue/defaulted), amount (centavos → reais), status (pending / negativated / eligible_for_removal), collaborator - Lógica de frontend_status: pending se registry pendente; se negativado, verifica se ainda há installments overdue para distinguir negativated de eligible_for_removal

Testes implementados (negativation_controller_test.rb): - GET queue retorna 200 com array - GET queue retorna 401 sem token - GET queue retorna uma row por product_debit no registry - GET queue retorna amount em reais (74_100 centavos × 3 = 2223.0) - GET queue exclui registries closed e cancelled


Task 4: Debit.eligible_for_negativation ⚠️

Files modificados: - app/models/debit.rb — scope implementado - test/models/debit_test.rb — sem testes novos (só atualização de schema comment)

O que foi feito: ruby scope :eligible_for_negativation, -> { joins(:product_debits) .where(product_debits: { consumer_progress: 26.. }) .joins(:installments) .where(installments: { status: %w[overdue defaulted] }) .group("debits.id") .having("COUNT(DISTINCT installments.id) >= 3") }

Pendente: - [ ] Adicionar testes unitários diretos em test/models/debit_test.rb: - retorna debit com progress > 25 e 3+ installments overdue - exclui debit com progress ≤ 25 - exclui debit com menos de 3 installments overdue

Cobertura indireta já existe em register_eligible_debit_test.rb (test “skips ineligible debits”), mas testes diretos no model são necessários.

bash docker compose exec backend-api bundle exec rails test test/models/debit_test.rb


Task 5: Testes para NegativationQueueJob ✅

Files criados: - test/jobs/negativation_queue_job_test.rb

Testes implementados: - cria RegistryNegativation para debit elegível - não duplica registry para o mesmo debit (idempotência)



Task 6: Controller lendo do registry ✅

Implementado junto com a Task 3. O controller já lê de RegistryNegativation.active_with_debit desde o início — não houve need de refatoração intermediária. Os testes do controller criam RegistryNegativation explicitamente antes do GET.