Endpoints de Negotiation — listar, criar e enviar
TLDR: o model
Negotiationexiste 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 emdraftou já emwaiting_response, dependendo do que o atendente escolhe no modal do front (salvar rascunho vs. gerar proposta) — mesmo padrão queContractjá usa comISSUABLE_STATUSES(draft/awaiting_signaturelá). send: muda a negotiation prawaiting_response, mas só a partir dedraftouwaiting_response. Bloqueado a partir deapproved,rejected,cancelledouexpired— nenhum desses é reversível por aqui. Simplificado pra uma função direta no model (com guarda), sem use case.- Resposta do cliente (
approved/rejected) ecancel: 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 viaDebitsController#index).POST /api/v1/debits/:debit_id/negotiations— criar uma negotiation para um débito, emdraftouwaiting_responseconforme ostatusenviado no payload.PATCH /api/v1/negotiations/:id/send— enviar a proposta ao cliente (→ waiting_response), a partir dedraftouwaiting_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. cancelde 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::Repaymenta partir de qualquer um destes endpoints — nenhum deles leva a negotiation aapproved. 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
BaseControllerjá garante — os três endpoints exigem só autenticação, sem escopo porcurrent_user(diferente deContractsController, que restringe porattendant). 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] })quandocustomer_idpresente, senão lista tudo; paginado comPaginatable(mesmo padrão deContractsController); serializado comNegotiationSerializer.create— recebedebit_idda rota aninhada; montaNegotiation.new(debit:, user: current_user, **params_negotiation)esave;statusaceita sódraftouwaiting_response(demais valores são rejeitados — não dá pra criar jáapproved); sucesso devolve201com o registro serializado, falha devolve422comerrors.full_messages.send— senegotiation.sendable?, chamanegotiation.update_status(:waiting_response)e devolve200com o registro serializado; se não, devolve422com uma mensagem ("Esta negociação não pode mais ser enviada"). Sem use case — lógica simples demais pra justificar umMicro::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:
indexsemcustomer_id: devolve todas as negotiations (paginado).indexcomcustomer_id: devolve só as negotiations cujodebit.customer_idbate, via fixtures de clientes diferentes.createcomstatus: draft(ou omitido): cria emdraft, associada aodebit_idda rota e aocurrent_user.createcomstatus: waiting_response: cria já aguardando resposta.createcomstatus: approved(ou qualquer valor fora dedraft/waiting_response): devolve422.createcom payload inválido (ex.:installments_countfora de1..12): devolve422com as mensagens de erro do model.createquando odebitjá tem uma negotiation (índice únicodebit_id): devolve422.senda partir dedraftouwaiting_response: muda parawaiting_response, devolve200.senda partir deapproved,rejected,cancelledouexpired: devolve422, 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/sendagora existem, mas sem validação das regrasRN-REPARC-1aRN-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.