Tela admin de impostos passa a exportar no layout eNotas

TLDR: /admin/taxes deixa de listar professional_payment_invoice por ano/status e passa a listar sessões (Meeting) elegíveis para nota fiscal, uma linha por sessão, nas colunas exigidas pela importação de notas do eNotas — substituindo a query SQL manual rodada hoje para gerar esse export.

Contexto

A emissão de notas fiscais dos terapeutas é feita hoje rodando manualmente uma query SQL direto no banco, que monta um CSV no layout de importação do eNotas: uma linha por sessão paga, com identificador único, dados do terapeuta (nome, documento, e-mail, endereço com cidade/UF/CEP/país) e o valor da taxa retida.

A tela /admin/taxes e o endpoint GET /api/v1/admin/taxes já existiam (spec 20260727165953_admin_taxes_endpoint), mas listavam professional_payment_invoice (uma linha por invoice mensal, com status/subtotal/taxas separadas) filtrando por year. Esse formato não corresponde ao que o eNotas espera para importar notas, exigindo que a query manual continue sendo rodada à parte.

Esta entrega faz a tela e o endpoint produzirem diretamente as colunas do layout eNotas, eliminando a necessidade da query manual.

Objetivos

  • Trocar a base da listagem de ProfessionalPaymentInvoice para Meeting: uma linha por sessão elegível para nota, não por invoice mensal
  • Trocar o filtro year por um intervalo start_period/end_period (formato MM/AAAA), filtrando pela data de atualização do pedido (orders.updated_at) — o eNotas exporta por período arbitrário, não só por ano
  • Reescrever TaxInvoiceSerializer para expor as colunas do layout eNotas: chave_unica, cliente_nome_razao_social, cliente_documento, cliente_email, cliente_endereco (cidade/UF, CEP, país concatenados), produto_nome (fixo, "ATENDIMENTO TERAPEUTICO") e venda_valor_total/venda_data
  • Restringir a listagem a sessões realmente faturáveis: finished, com pedido pago (orders.payments.status = paid), pagamento não gratuito (payment_method != free) e com taxa de invoice (professional_payment_invoice_meetings.total) maior que zero
  • Manter a tela /admin/taxes e o endpoint JSON consumindo a mesma lógica (FetchTaxSummary/TaxSummaryQuery), como já era antes

Fora de escopo

  • Gerar o CSV de fato (export/download) — esta entrega só ajusta a listagem em tela/JSON para as colunas corretas; a geração do arquivo para upload no eNotas continua manual por ora
  • Automatizar ou versionar a query SQL manual usada como referência de layout — continua fora do código, roda direto no banco quando necessário
  • Autenticação, paginação e estrutura de resposta do endpoint — inalteradas nesta entrega

Mudanças

  • app/queries/professional_payment_invoices/tax_summary_query.rb — initialize passa a partir de Meeting.finished.joins(:professional_payment_invoice_meeting).joins(user_client: :professional).joins(order: :payment).where(orders: {payments: {status: :paid}}).where.not(orders: {payment_method: :free}).where("professional_payment_invoice_meetings.total > 0"), com preload de order, professional_payment_invoice_meeting e user_client: {professional: [:user_profile, :address]}, ordenando por users.name. #by_year(year) é substituído por #by_period(start_period, end_period), que faz Date.strptime(period, "%m/%Y") e filtra orders.updated_at entre o início do primeiro mês e o fim do último; retorna self sem filtrar se qualquer um dos dois for blank?
  • app/use_cases/professional_payment_invoices/fetch_tax_summary.rb — chama TaxSummaryQuery#by_period(context.start_period, context.end_period) em vez de #by_year(context.year)
  • app/controllers/api/v1/admin/taxes_controller.rb e app/controllers/admin/taxes_controller.rb — passam params[:start_period]/params[:end_period] para o use case em vez de params[:year]
  • app/serializers/tax_invoice_serializer.rb — reescrito para o layout eNotas. object agora é um Meeting. Novos atributos: chave_unica (object.id), cliente_nome_razao_social/cliente_documento/cliente_email (do terapeuta, via user_client.professional), cliente_endereco (monta "rua, número, bairro, cidade - UF, CEP, país" a partir do address do terapeuta, pulando partes ausentes), produto_nome (constante "ATENDIMENTO TERAPEUTICO"), venda_valor_total (ProfessionalPaymentInvoices::CalculateFee.call(meeting.professional_payment_invoice_meeting.total), taxa de 10%) e venda_data (order.updated_at formatado dd/mm/aaaa). UF é abreviada via mapa STATE_ABBREVIATIONS com remoção de acentos (tr); CEP é normalizado para 8 dígitos via StripNonNumbers, tratando "00000000" como ausente; país usa "Brasil" como padrão quando country_code for BR ou ausente
  • app/views/admin/taxes/index.html.erb — troca o campo único “Ano” por dois campos “De”/”Até” (start_period/end_period, maxlength="7", formato MM/AAAA); tabela passa a ter uma coluna por atributo do serializer (Sessão, Terapeuta, Documento, Email, Endereço, Produto, Valor da taxa, Data), lendo de TaxInvoiceSerializer.new(meeting) por linha; subtítulo passa a mencionar o formato eNotas
  • spec/queries/professional_payment_invoices/tax_summary_query_spec.rb, spec/use_cases/professional_payment_invoices/fetch_tax_summary_spec.rb, spec/requests/api/v1/admin/taxes_spec.rb, spec/features/admin/taxes_index_spec.rb — reescritos para a nova base (Meeting em vez de ProfessionalPaymentInvoice), com um helper local create_exportable_meeting repetido em cada arquivo (sessão :finished, pedido :paid_order, professional_payment_invoice_meeting com total) e casos novos para as exclusões (sessão não finalizada, pagamento não pago, pagamento gratuito, taxa zerada)

Sem spec dedicada para o serializer

O repo não tem spec/serializers/ — nenhum dos 35 serializers em app/serializers/ tem spec unitária; a cobertura é sempre indireta via request/feature spec. Esta entrega manteve o padrão: a lógica nova do serializer (documento, endereço com UF/CEP/país, produto, valor, data) não ganhou spec própria. A feature/request spec cobre nome, email e valor da taxa; UF, CEP, país e formatação de endereço ficaram sem asserção direta.

Como verificar

  • make test test=spec/queries/professional_payment_invoices/tax_summary_query_spec.rb
  • make test test=spec/use_cases/professional_payment_invoices/fetch_tax_summary_spec.rb
  • make test test=spec/requests/api/v1/admin/taxes_spec.rb
  • make test test=spec/features/admin/taxes_index_spec.rb
  • Manual: acessar /admin/taxes, filtrar por start_period/end_period (MM/AAAA) e comparar as linhas retornadas com o resultado da query manual de referência rodada para o mesmo período
  • Manual: conferir que terapeutas com endereço em estado abreviado ("SP") e em nome completo ("São Paulo") resolvem para a mesma UF na coluna Endereço

Documentação

Nenhuma mudança em rules/ ou reference/ — não introduz regra de negócio nova (o cálculo da taxa de 10% já existe em ProfessionalPaymentInvoices::CalculateFee), só reformata como um dado existente é listado. O fluxo de repasse/taxas continua descrito em invoice_payment_flow.