Endpoints de Negotiation — listar, criar e enviar

TLDR: o model Negotiation existe há tempo, mas nenhuma rota HTTP o expõe. Esta spec cria os três endpoints que faltam — listar propostas reais de um cliente, criar uma negotiation e enviar uma proposta em rascunho pro cliente.

Contexto

O frontend já tem toda a UI de simulação de negociação pronta (ChargeNegotiationDrawer, CustomersNegotiationDrawer, NegotiationTab, services/negotiation/engine.ts), mas ela não persiste nada — só calcula e gera texto de proposta pra copiar/enviar manualmente. O model Negotiation (app/models/negotiation.rb) já tem toda a estrutura (status, kind, valores propostos, etc.), e o use case Debits::Repayment (app/use_cases/debits/repayment.rb) já sabe efetivar um reparcelamento aprovado no Asaas — mas exige negotiation.approved? e hoje nada cria ou envia uma negotiation. routes.rb não tem nenhum recurso negotiations.

O spec anterior do Debits::Repayment já deixou isso registrado como fora de escopo: “Aprovar o Negotiation e validar a R-001 … ainda não foi implementada.”

A R-001 descreve regras de negócio mais amplas (máx. 2 reparcelamentos por produto, 1 acordo ativo por produto, com prioridade de produto em caso de concorrência) que dependem de olhar o histórico de negociações do cliente — isso é o “motor de negociação” (USER-015) e fica fora desta spec (ver Fora de escopo).

Nesta mesma branch, o status de Negotiation ganhou dois ajustes: o inicial foi renomeado de simulated para draft (migração 20261001010000, alinhando com o padrão já usado em Contract), e foi adicionado o status waiting_response entre draft e approved/rejected. O enum final é:

ruby enumerize :status, in: %i[draft waiting_response approved rejected cancelled expired], default: :draft, predicates: true, scope: true

Decidido em conversa

  • Criar (POST): nasce em draft ou já em waiting_response, dependendo do que o atendente escolhe no modal do front (salvar rascunho vs. gerar proposta) — mesmo padrão que Contract já usa com ISSUABLE_STATUSES (draft/awaiting_signature lá).
  • send: muda a negotiation pra waiting_response, mas só a partir de draft ou waiting_response. Bloqueado a partir de approved, rejected, cancelled ou expired — nenhum desses é reversível por aqui. Simplificado pra uma função direta no model (com guarda), sem use case.
  • Resposta do cliente (approved/rejected) e cancel: fora desta spec, porque ainda não existe fluxo definido pra elas (ver Fora de escopo). Quando existir, usa-se o mesmo método do model.

Objetivos

  • GET /api/v1/negotiations?customer_id= — listar as negotiations reais de um cliente (hoje só dá pra ver dados de negociação indiretamente via DebitsController#index).
  • POST /api/v1/debits/:debit_id/negotiations — criar uma negotiation para um débito, em draft ou waiting_response conforme o status enviado no payload.
  • PATCH /api/v1/negotiations/:id/send — enviar a proposta ao cliente (→ waiting_response), a partir de draft ou waiting_response.

Fora de escopo

  • Validar a R-001 (limite de 2 reparcelamentos por produto, 1 acordo ativo por produto, prioridade de produto) — depende de consultar o histórico de negociações por produto e é o “motor de negociação” completo (USER-015). Decisão confirmada em conversa: esta spec cobre só o que o model já garante hoje (MAX_INSTALLMENTS, discount_only_on_settlement).
  • Resposta do cliente (waiting_response → approved/rejected) — decisão confirmada em conversa: não faz sentido implementar agora porque ainda não existe fluxo definido pra isso. Fica para uma spec futura, reaproveitando o mesmo método do model.
  • cancel de negotiation — mesma razão acima, fica fora.
  • expired — nenhuma rotina (job, rake task) que expire uma negotiation vencida. Fica como trabalho futuro; hoje é só um status que existe no enum sem nada que o produza.
  • Chamar Debits::Repayment a partir de qualquer um destes endpoints — nenhum deles leva a negotiation a approved. Acionar o reparcelamento de fato fica para quando a resposta do cliente existir.
  • Qualquer mudança no frontend (services/negotiation, drawers, NegotiationTab) — esta spec é só backend. Conectar o frontend a estes endpoints é trabalho futuro.
  • Autorização por papel (admin/director vs attendant) além do que BaseController já garante — os três endpoints exigem só autenticação, sem escopo por current_user (diferente de ContractsController, que restringe por attendant). Revisar se isso for levantado depois.

Mudanças

config/routes.rb

ruby resources :negotiations, only: [ :index ] do patch :send, on: :member end

E a linha existente resources :debits, only: [ :index, :show ] vira bloco, pra aninhar o create:

ruby resources :debits, only: [ :index, :show ] do resources :negotiations, only: [ :create ] end

app/models/negotiation.rb

```ruby SENDABLE_STATUSES = %w[draft waiting_response].freeze

def sendable? = SENDABLE_STATUSES.include?(status)

def update_status(new_status) = update(status: new_status) ```

update_status é genérica e reaproveitável pras próximas transições (resposta do cliente, cancelamento) quando elas existirem. sendable? é a guarda específica do send — bloqueia approved, rejected, cancelled e expired.

app/controllers/api/v1/negotiations_controller.rb (novo)

  • index — Negotiation.joins(:debit).where(debits: { customer_id: params[:customer_id] }) quando customer_id presente, senão lista tudo; paginado com Paginatable (mesmo padrão de ContractsController); serializado com NegotiationSerializer.
  • create — recebe debit_id da rota aninhada; monta Negotiation.new(debit:, user: current_user, **params_negotiation) e save; status aceita só draft ou waiting_response (demais valores são rejeitados — não dá pra criar já approved); sucesso devolve 201 com o registro serializado, falha devolve 422 com errors.full_messages.
  • send — se negotiation.sendable?, chama negotiation.update_status(:waiting_response) e devolve 200 com o registro serializado; se não, devolve 422 com uma mensagem ("Esta negociação não pode mais ser enviada"). Sem use case — lógica simples demais pra justificar um Micro::Case (decisão confirmada em conversa).

app/serializers/negotiation_serializer.rb (novo)

Campos: id, debit_id, customer_id (via object.debit.customer_id), user_id, kind, status, proposed_total_cents, installments_count, installment_amount_cents, first_due_on, discount_percent, extension_days, billing_type, provider_status, generated_debit_id.

Permitir parâmetros de create

params.permit(:kind, :proposed_total_cents, :installments_count, :first_due_on, :discount_percent, :extension_days, :billing_type, :status) — os mesmos campos que já existem como atributos validados no model, mais :status restrito a draft/waiting_response no controller.

Como verificar

cd modules/backend && bin/rails t, fixtures YAML (sem factory), seguindo o padrão de test/controllers/api/v1/contracts_controller_test.rb.

Novo test/controllers/api/v1/negotiations_controller_test.rb cobrindo:

  • index sem customer_id: devolve todas as negotiations (paginado).
  • index com customer_id: devolve só as negotiations cujo debit.customer_id bate, via fixtures de clientes diferentes.
  • create com status: draft (ou omitido): cria em draft, associada ao debit_id da rota e ao current_user.
  • create com status: waiting_response: cria já aguardando resposta.
  • create com status: approved (ou qualquer valor fora de draft/waiting_response): devolve 422.
  • create com payload inválido (ex.: installments_count fora de 1..12): devolve 422 com as mensagens de erro do model.
  • create quando o debit já tem uma negotiation (índice único debit_id): devolve 422.
  • send a partir de draft ou waiting_response: muda para waiting_response, devolve 200.
  • send a partir de approved, rejected, cancelled ou expired: devolve 422, sem mutar o registro.

Novo test/models/negotiation_test.rb (adicionar aos testes existentes) cobrindo sendable? pros seis status, e update_status mudando o status e persistindo.

Manual: criar uma negotiation via POST, enviar via PATCH .../send, e conferir no banco que o status virou waiting_response.

Documentação

  • Atualizar R-001: registrar que create/send agora existem, mas sem validação das regras RN-REPARC-1 a RN-REPARC-6 (continuam pendentes do motor de negociação completo) e sem fluxo de resposta do cliente.
  • Adicionar este spec ao índice .project/docs/README.md.