Simplificar a criação de invoice (parte 2 de 4)

TLDR: Consolida 5 use cases em 1, elimina o model InvoiceError e 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:

  • ProfessionalFinancialAccount tinha apenas 1 linha de lógica (9 linhas no total).
  • Validation calculava o payout e SetParams recalculava — duplicação.
  • 5 arquivos + 5 specs para 1 operação de negócio.
  • Se as meetings mudassem entre Validation e SetParams, 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 InvoiceError e sua tabela.
  • Validar MIN_PAYOUT explicitamente no fluxo de criação.
  • Reaproveitar os métodos dos concerns criados na parte 1.

Fora de escopo

  • Campo last_error_message na própria ProfessionalPaymentInvoice — entra na parte 3. Aqui apenas a criação/destruição de InvoiceError no 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::Create foi entregue e a tabela invoice_errors foi 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.rb e spec/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 de FinancialSetting::MIN_PAYOUT, e cria a invoice com status: :pending (o enum atual é pending/paid/canceled, sem created).

Como verificar

bash make test test=spec/use_cases/professional_payment_invoices/create_spec.rb

  • Confirmar que ProfessionalPaymentInvoices::Create é um UseCaseBase, não um Flow.
  • Confirmar que a tabela invoice_errors nã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.