Simplificar a criação de invoice (parte 2 de 4)
TLDR: Consolida 5 use cases em 1, elimina o model
InvoiceErrore torna a validação de valor mínimo explícita no fluxo de criação da invoice.
Sequência: parte 2 de uma iniciativa de 4 specs escritas entre 27/01 e 03/02/2026 — depende da parte 1: refatoração da elegibilidade.
Contexto
Fragmentação excessiva
O fluxo de criação de invoice estava dividido em 5 use cases:
ruby
flow(
ProfessionalPaymentInvoices::Creation::Validation, # Valida + calcula
ProfessionalPaymentInvoices::ProfessionalFinancialAccount, # Busca conta (1 linha!)
ProfessionalPaymentInvoices::Creation::SetParams, # Cria invoice object
ProfessionalPaymentInvoices::BuildPaymentDetails, # Monta string details
ProfessionalPaymentInvoices::Creation::Create # Salva + agenda job
)
Problemas:
ProfessionalFinancialAccounttinha apenas 1 linha de lógica (9 linhas no total).Validationcalculava o payout eSetParamsrecalculava — duplicação.- 5 arquivos + 5 specs para 1 operação de negócio.
- Se as meetings mudassem entre
ValidationeSetParams, os valores podiam divergir.
Model InvoiceError desnecessário
ruby
# no job
if result.success?
InvoiceError.where(professional: professional).destroy_all
else
InvoiceError.find_or_initialize_by(professional: professional, error_message: result.message).save!
end
Model separado apenas para guardar uma mensagem de erro temporária, usado só para exibição no Avo, não consumido pelo sistema de retry, com tabela de 3 colunas criada/destruída a cada execução do job.
Falta de validação de MIN_PAYOUT no flow
A validação do mínimo de R$ 100 só existia no concern invoiceable?, sem validação explícita no fluxo de criação. Se o concern falhasse, a invoice podia ser criada com valor inválido.
Objetivos
- Consolidar a criação de invoice em um único use case.
- Remover o model
InvoiceErrore sua tabela. - Validar
MIN_PAYOUTexplicitamente no fluxo de criação. - Reaproveitar os métodos dos concerns criados na parte 1.
Fora de escopo
- Campo
last_error_messagena própriaProfessionalPaymentInvoice— entra na parte 3. Aqui apenas a criação/destruição deInvoiceErrorno job é removida.
Mudanças
1. Consolidar em um use case único
De CreateFlow (5 passos) para:
```ruby module ProfessionalPaymentInvoices class Create < UseCaseBase transactional
required_attributes(:professional, :admin_user, :creation_index)
def call
validate_professional!
load_data!
validate_payout!
build_invoice!
save_and_schedule!
end
private
def validate_professional!
unless professional.has_approved_financial_account?
context.fail!(message: "Precisa ter uma conta bancária aprovada.")
end
end
def load_data!
context.meetings = professional.invoiceable_meetings
context.financial_account = professional.financial_accounts.approved.last
context.payout = ProfessionalPaymentInvoices::CalculatePayout.call(context.meetings)
end
def validate_payout!
return context.fail!(message: "Não possui sessões a serem pagas.") if context.meetings.blank?
return context.fail!(message: "Payout inválido.") if context.payout.total.zero?
if context.payout.total < FinancialSetting::MIN_PAYOUT
context.fail!(message: "Valor mínimo de R$ #{FinancialSetting::MIN_PAYOUT} não atingido.")
end
if context.payout.subtotal > professional.financial_setting.max_payout
context.fail!(message: "Valor máximo de R$ #{professional.financial_setting.max_payout} excedido.")
end
end
def build_invoice!
context.invoice = ProfessionalPaymentInvoice.new(
professional: professional,
admin_user: admin_user,
total: context.payout.total,
subtotal: context.payout.subtotal,
fee_total: context.payout.total_fee,
transfer_fee_total: context.payout.transfer_fee_total,
scheduled_at: scheduled_at_delay,
status: :created,
payment_details: build_payment_details
)
context.meetings.each do |meeting|
context.invoice.professional_payment_invoice_meetings.new(
meeting: meeting,
total: meeting.payment_unit_price
)
end
end
def save_and_schedule!
context.invoice.save!
ProfessionalPaymentInvoiceTransferCreationJob.set(
wait_until: context.invoice.scheduled_at
).perform_later(context.invoice.id)
end
def scheduled_at_delay
wait_until = (creation_index || 1) * rand(30..45)
Time.current + wait_until.seconds
end end end ```
Ganhos: 1 arquivo em vez de 5; payout calculado uma única vez; menos estado compartilhado; validação de mínimo explícita.
2. Remover o model InvoiceError
A remover: app/models/invoice_error.rb, app/avo/resources/invoice_error.rb, app/avo/controllers/invoice_errors_controller.rb, app/policies/invoice_error_policy.rb, spec/factories/invoice_errors.rb, e uma migration para dropar a tabela.
3. Usar os métodos dos concerns
```ruby # antes meetings = Meeting.from_professional(professional.id).invoiceable
depois
meetings = professional.invoiceable_meetings ```
Arquivos
Modificados: app/use_cases/professional_payment_invoices/create.rb (de Flow para UseCaseBase), o spec correspondente, app/jobs/professional_payment_invoice_creation_job.rb e app/jobs/professional_payment_invoice_transfer_creation_job.rb (remoção de InvoiceError).
Removidos: creation/validation.rb, creation/set_params.rb, creation/create.rb, professional_financial_account.rb, build_payment_details.rb e os specs correspondentes, além dos arquivos de InvoiceError.
Novos: migration db/migrate/*_drop_invoice_errors.rb.
Desvio verificado em 2026-08-06: a consolidação em
ProfessionalPaymentInvoices::Createfoi entregue e a tabelainvoice_errorsfoi dropada (db/migrate/20260116123532_drop_invoice_errors_table.rb), mas os arquivos antigos não foram apagados. Continuam no repo, órfãos e sem nenhuma referência:app/use_cases/professional_payment_invoices/creation/validation.rb,creation/set_params.rb,creation/create.rb,build_payment_details.rb,professional_financial_account.rbespec/factories/invoice_errors.rb.O código entregue também difere da proposta em dois pontos: usa a constante global
MINIMUM_PAYMENT_AMOUNT(config/initializers/invoices.rb) em vez deFinancialSetting::MIN_PAYOUT, e cria a invoice comstatus: :pending(o enum atual épending/paid/canceled, semcreated).
Como verificar
bash
make test test=spec/use_cases/professional_payment_invoices/create_spec.rb
- Confirmar que
ProfessionalPaymentInvoices::Createé umUseCaseBase, não umFlow. - Confirmar que a tabela
invoice_errorsnão existe mais. - Criar uma invoice abaixo de R$ 100 e confirmar a falha com a mensagem de mínimo não atingido.
Documentação
O fluxo resultante está descrito em invoice_payment_flow.