Files
scoreodonto.com/doc/BACKLOG-FINANCEIRO-V1.md
VPS 4 Builder 5049ce9b8b
build-and-push / build (push) Successful in 1m10s
build-and-push / deploy (push) Has been skipped
docs: unifica documentação em doc/ (fonte única, sem sobreposição)
- Pasta única scoreodonto.com/doc/ com 9 docs que prestam:
  VERDADE-HOJE (estado, principal), DEPLOY-PRODUCAO (runbook), BANCO-DEV-ESTRATEGIA,
  REGISTRY, AUDITORIA-FUNCIONAL, BACKLOG-FINANCEIRO-V1, CRM_Ortodontia, RX_ARQUITETURA, RX_BACKLOG.
- Removidos os docs de infra sobrepostos/desatualizados (estado consolidado em VERDADE-HOJE;
  histórico fica no git): arquitetura.md, deploy.md, CHECKLIST-DEPLOY-PRODUCAO.md,
  PENDENCIAS.md, PENDENCIAS_E_DEPLOY.md.
- Pasta documentação/ eliminada.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 12:04:08 +02:00

157 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Backlog V1 — Financeiro (ScoreOdonto)
> Derivado do `AUDITORIA-FUNCIONAL-SCOREODONTO.md` (Deep-dive 1). Apenas planejamento — nada implementado.
> Padrões do projeto a seguir em toda história: `tenantGuard` + escopo `clinica_id`, soft delete onde fizer sentido,
> `registrarAudit` nas mutações, helpers `buildInsert/buildUpdate`, frontend `useToast`/`useConfirm`,
> rebuild da imagem DEV (frontend nginx + backend) — ver `feedback_deploy_workflow`.
> Estimativa: P (≤0,5 dia) · M (12 dias) · G (3+ dias).
Estado atual relevante: tabela `financeiro(id, clinica_id, pacientenome[texto], descricao, valor, datavencimento, status[Pago/Pendente/Atrasado], formapagamento, tipo[RECEITA/DESPESA])`, 0 linhas. Único elo: `LancarProcedimentoModal → saveFinanceiro`.
---
## ÉPICO 1 — Ligar o Financeiro (FKs de origem) — ✅ IMPLEMENTADO (DEV)
**Por quê:** sem `profissional_id`/`paciente_id` não há comissão, repasse, nem relatório por paciente/profissional. É o pré-requisito de tudo.
> **Status (DEV):** H1.1 ✅ (FKs + índices + soft delete + cron "Atrasado" — também cobre H2.2/H2.4) · H1.2 ✅ (`FinanceiroView` envia `paciente_id`) · H1.3 ✅ (`LancarProcedimentoModal` envia `paciente_id`/`procedimento_id`/`origem='procedimento'`). **Extra entregue:** prontuário de procedimentos realizados (`procedimentos_realizados` + fotos antes/depois) com regra de **sem comissão** em reabertura pelo mesmo profissional e em "refazer na garantia".
### H1.1 — Adicionar chaves de ligação ao lançamento — **M**
- **Como** sistema, **quero** que cada lançamento referencie paciente/profissional/origem **para** rastrear e relatar.
- **Migração:** `ALTER TABLE financeiro ADD COLUMN IF NOT EXISTS paciente_id TEXT, profissional_id TEXT, procedimento_id TEXT, agendamento_id TEXT, contrato_id TEXT, origem TEXT DEFAULT 'manual', pago_em TIMESTAMPTZ;` + índices `(clinica_id, status, datavencimento)` e `(clinica_id, profissional_id)`.
- **Backend:** `POST/PUT /api/financeiro` aceitam os novos campos (já é `buildInsert`); manter `clinica_id` do `tenantGuard`.
- **Critérios:** lançamento salva e retorna os campos; antigos (sem FK) continuam válidos; `node --check` + E2E (criar com `profissional_id` e ler).
- **Risco:** `pacientenome` legado permanece (não migrar dado, base vazia em DEV).
### H1.2 — Vincular paciente real (não só nome) no lançamento — **M**
- **Como** recepção, **quero** selecionar o paciente cadastrado **para** o financeiro aparecer no histórico do paciente.
- **Frontend:** no modal de novo lançamento (`FinanceiroView`), trocar campo de nome livre por **busca de paciente** (reusar `getPacientes` + componente do `AppointmentModal`); preencher `paciente_id` + `pacientenome` (snapshot).
- **Critérios:** ao gravar, `paciente_id` preenchido; busca por nome/CPF/telefone funciona; manter compatível com lançamento sem paciente (despesa).
### H1.3 — Procedimento concluído gera lançamento com FKs — **M**
- **Como** sistema, **quero** que ao concluir um procedimento na agenda o lançamento já venha com `paciente_id/profissional_id/procedimento_id/agendamento_id`.
- **Backend/Frontend:** `LancarProcedimentoModal` (que já chama `saveFinanceiro`) passa as FKs; valor sugerido = `procedimentos.valorparticular`.
- **Critérios:** lançamento criado a partir de procedimento tem `origem='procedimento'` + as 4 FKs; valor pré-preenchido.
---
## ÉPICO 2 — Contas a Receber / a Pagar
**Por quê:** o Financeiro hoje é uma lista plana; falta gestão de vencimentos, baixa e despesas estruturadas.
### H2.1 — Baixa de pagamento com data e meio — **P** — ✅ IMPLEMENTADO (DEV)
> Modal `BaixaPagamentoModal` (data + forma) no `FinanceiroView` substitui o "marcar Pago" direto; `useConfirm` p/ valores ≥ R$ 1.000. `PUT /api/financeiro/:id` carimba `pago_em` (se ausente) e registra `registrarAudit` (`baixa_pagamento`, idempotente). **Bug latente corrigido:** `enforceUppercase` (service) mandava `PAGO`/`PIX`, violando os CHECK do banco (`Pago`/`Pix`); backend agora normaliza `status`/`formapagamento`/`tipo` no POST e no PUT (`normalizeFinanceiro`). Era a causa de a tabela `financeiro` ficar sempre vazia.
- **Como** financeiro, **quero** confirmar pagamento registrando `pago_em` e a forma **para** ter caixa real.
- **Backend:** `PUT /api/financeiro/:id` aceita `status='Pago'` + `pago_em=NOW()` (ou data informada). (PUT já é tenant-scoped.)
- **Frontend:** botão "Confirmar pagamento" abre mini-form (data + forma) em vez de marcar direto; `useConfirm` para valores altos.
- **Critérios:** ao confirmar, grava `pago_em` e `formapagamento`; KPI Recebido considera `pago_em`.
### H2.2 — Job de "Atrasado" automático — **P**
- **Como** sistema, **quero** marcar lançamentos vencidos como Atrasado **para** o KPI ser fiel.
- **Backend:** cron (boot + 6h, padrão `processarVigenciasContratos`): `UPDATE financeiro SET status='Atrasado' WHERE status='Pendente' AND datavencimento < CURRENT_DATE AND deleted_at IS NULL`.
- **Pré-req:** adicionar `deleted_at` ao `financeiro` (soft delete) — hoje DELETE é físico (`H2.4`).
- **Critérios:** lançamento vencido vira Atrasado sem ação manual.
### H2.3 — Despesas com fornecedor e centro de custo — **M** — ✅ IMPLEMENTADO (DEV)
> Migração `fornecedor/centro_custo/recorrente`. `NovoLancamentoModal` ganhou toggle **Receita/Despesa**: em DESPESA o paciente fica opcional e aparecem fornecedor + centro de custo (datalist com presets) + recorrente. Normalização de enums já cobre o casing. **H3.3 entregue junto:** `porCategoria` (despesas por centro de custo) no `GET /api/financeiro/relatorio` + bloco no `ReportsView`.
- **Como** financeiro, **quero** classificar despesas (fornecedor, categoria/centro de custo, recorrente) **para** análise de saída.
- **Migração:** `ALTER TABLE financeiro ADD COLUMN IF NOT EXISTS fornecedor TEXT, centro_custo TEXT, recorrente BOOLEAN DEFAULT false;`
- **Frontend:** quando `tipo=DESPESA`, exibir fornecedor + centro de custo + recorrente.
- **Critérios:** despesa salva com classificação; filtro por categoria no relatório (Épico 3).
### H2.4 — Soft delete + auditoria do lançamento — **P**
- **Migração:** `ALTER TABLE financeiro ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;`
- **Backend:** `DELETE /api/financeiro/:id``UPDATE ... SET deleted_at=NOW()`; GET filtra `deleted_at IS NULL`; `registrarAudit`.
- **Critérios:** excluir não apaga fisicamente; some das listas; audit registra autor.
---
## ÉPICO 3 — Relatórios financeiros essenciais
**Por quê:** hoje só existe `/api/reports/productivity`.
> **Status (DEV):** H3.1 ✅ + H3.2 ✅ — endpoint único `GET /api/financeiro/relatorio?inicio&fim&pacienteId&profissionalId` (tenantGuard, escopo `clinica_id`, soft delete) retornando `resumo` (recebido/a_receber/atrasado/despesa), `porMes`, `porForma` e `lancamentos`. UI em `ReportsView` (componente `RelatorioFinanceiro`): KPIs + barras por mês + por forma de pagamento + tabela + export CSV, com filtros de profissional (select) e paciente (busca). H3.3 (despesas por categoria/centro de custo) depende de H2.3 — pendente.
### H3.1 — A receber/recebido por período — **M**
- **Backend:** `GET /api/financeiro/relatorio?inicio=&fim=` (tenantGuard) → totais por status, por dia/mês, por forma de pagamento.
- **Frontend:** seção em `ReportsView` (ou aba no Financeiro) com gráfico simples (reaproveitar padrão do Dashboard).
- **Critérios:** filtros de período; soma confere com lançamentos.
### H3.2 — Financeiro por paciente e por profissional — **M**
- **Backend:** `GET /api/financeiro/relatorio?pacienteId=` e `?profissionalId=` (usa as FKs do Épico 1).
- **Critérios:** lista lançamentos do paciente/profissional no período; base para o repasse (V2).
### H3.3 — Despesas por categoria/centro de custo — **P** — ✅ IMPLEMENTADO (DEV, junto com H2.3)
- **Backend:** agregação por `centro_custo` no relatório (`porCategoria`). ✓
- **Critérios:** total por categoria no período. ✓ (bloco "Despesas por centro de custo" no `ReportsView`)
---
## ÉPICO 4 — Orçamento → Contrato → Financeiro
**Por quê:** os 3 módulos existem isolados; ligá-los fecha o ciclo comercial.
> **Status (DEV):** H4.1 ✅ — orçamento **Assinado** gera contas a receber automaticamente (entrada + N parcelas mensais a partir de `data_inicio`, `origem='orcamento'`, FKs paciente/profissional/`contrato_id`), idempotente via `orcamentos.financeiro_gerado`; helper `gerarFinanceiroOrcamento`; toast no `OrcamentoModal`.
> **H4.2 ✅** — contrato (motor) com **cobrança recorrente**: campos `contratos.{cobranca_automatica,valor_recorrente,periodicidade(mensal|anual),dia_cobranca}` + `financeiro.competencia`. Helper `gerarCobrancaContratoCompetencia` (idempotente por contrato_id+competência, origem='contrato', dia clamp 28) e `gerarCobrancasRecorrentes` rodando no cron `processarVigenciasContratos` (boot+6h) sobre contratos **vigentes**. Endpoint manual `POST /api/contratos/instancias/:id/gerar-cobranca`. UI: config no `GeradorModal` + botão "Gerar cobrança do mês" nos contratos vigentes (`ContratosView`).
> **Renovação automática ✅** — no mesmo cron, contratos `renovacao='automatica'` com `data_fim` vencido estendem a vigência por nova duração igual à anterior (`data_inicio=data_fim+1`, `data_fim += (data_fimdata_inicio)+1`; fallback +12 meses sem início) e reabilitam o aviso de vencimento.
### H4.1 — Orçamento aprovado gera contas a receber — **M** — ✅
- **Como** recepção, **quero** que ao aprovar um orçamento as parcelas entrem no Financeiro **para** não relançar manualmente.
- **Backend:** ao mudar status do orçamento para "Aprovado/Assinado", criar N lançamentos `RECEITA` (`origem='orcamento'`, `paciente_id`, vencimentos por parcela) a partir de `valor_total/entrada/parcelas`.
- **Critérios:** aprovar orçamento de 3x cria 3 lançamentos pendentes com vencimentos; idempotente (não duplica).
### H4.2 — Contrato (motor) com cobrança recorrente — **M** (ponte p/ V2)
- **Como** sistema, **quero** que um contrato vigente com vigência/parcelas gere lançamentos **para** cobrança recorrente.
- **Backend:** estender o cron de vigência (`processarVigenciasContratos`) para emitir lançamentos conforme `valores`/periodicidade do contrato (`contrato_id` na FK).
- **Critérios:** contrato mensal gera lançamento por competência; não duplica.
---
## Sequência sugerida (entrega incremental)
1. **H1.1 → H2.4 → H2.2** (FKs + soft delete + atrasado) — base sólida e segura.
2. **H1.2 → H1.3** (paciente real + procedimento→lançamento) — liga clínica↔financeiro.
3. **H2.1 → H2.3** (baixa + despesas) — caixa fiel.
4. **H3.1 → H3.2 → H3.3** (relatórios) — visão gerencial.
5. **H4.1 → H4.2** (orçamento/contrato→financeiro) — fecha o ciclo e prepara o V2 (comissão/repasse).
## Definição de pronto (toda história)
`node --check` (backend) + build OK (frontend) · endpoints com `tenantGuard` + escopo `clinica_id` · mutações com `registrarAudit` · E2E mínimo (criar/ler/escopo cross-tenant 403) · rebuild + recreate DEV · validado em `dev.scoreodonto.com`.
## Riscos / observações
- Não migrar dados legados (base de DEV vazia); em PROD, planejar backfill de `paciente_id` por `pacientenome` (fuzzy) — fora do V1.
- `formapagamento` é texto livre hoje; padronizar enum (PIX/Cartão/Boleto/Dinheiro) facilita relatórios e o V2 (gateways).
- Comissão/Repasse é **V2** — depende do Épico 1 (FK `profissional_id`) entregue aqui.
---
## V2 — Comissão / Repasse — ✅ IMPLEMENTADO (DEV)
Decisões: base **selecionável** (recebido=caixa via status Pago/pago_em · produção=competência via vencimento); percentual **por procedimento** com **regra padrão** de fallback (`comissao_regras.procedimento_id NULL`); **fechamento persistido** (snapshot imutável `repasses` + `registrarAudit`).
- **Tabelas:** `comissao_regras (clinica_id, procedimento_id NULL=padrão, percentual)` (unique por clínica+proc); `repasses (snapshot por profissional: período, base, base_valor, comissao_valor, detalhes JSONB, status, criado_por)`.
- **Cálculo** (`computeComissoes`): RECEITA, `sem_comissao=false`, `profissional_id NOT NULL`; pct = regra do procedimento → senão padrão → senão 0; agrupa por profissional com detalhe por lançamento. Garantia/reabertura do mesmo profissional (sem_comissao) já saem de fora.
- **Endpoints:** `GET/PUT/DELETE /api/comissoes/regras`, `GET /api/comissoes/relatorio`, `POST /api/repasses/fechar`, `GET /api/repasses[/:id]`, `POST /api/repasses/:id/cancelar`.
- **Frontend:** `ComissoesView` (menu COMISSÕES, owner-only) com abas Relatório/Fechamento (toggle recebido/produção, expandível por profissional, botão Fechar), Regras de % (padrão + por procedimento) e Histórico (lista de fechamentos + cancelar).
- **Gaps (✅ implementados DEV):** (1) **override de % por profissional** (`comissao_regras.profissional_id`; prioridade prof > procedimento > padrão; unique inclui profissional); (2) **custo de laboratório** por procedimento (`comissao_regras.custo_lab`) deduzido do valor antes da comissão (base líquida); (3) **marcar repasse como pago** (`POST /api/repasses/:id/pagar`) que gera **DESPESA** no financeiro (origem='repasse', centro_custo='COMISSÕES', sem_comissao, Pago) e grava `repasses.pago_em/financeiro_id`; repasse pago não pode ser cancelado.
- **Estorno ao cancelar (✅):** cancelar repasse **pago** agora soft-deleta a DESPESA gerada (`financeiro.deleted_at`) e zera `repasses.financeiro_id`; auditoria registra `despesa_estornada`. UI: botão cancelar disponível p/ status fechado e pago (confirm avisa do estorno).
- **GTO → Financeiro (✅ — gap descoberto):** `PUT /api/gto-builder/:id/finalizar` agora gera **RECEITA** (convênio a receber, vencimento +30d, origem='gto', FKs paciente/profissional) — idempotente via `gto_builders.financeiro_id`. **A tela `LancarGTO` não chamava os endpoints** (`handleFinalize` era stub); agora persiste builder+itens+finaliza (service `createGtoBuilder/addGtoItem/finalizarGto`). Assim a produção por convênio entra no financeiro e na comissão (profissional=dentistaid).
- **Convênios padronizados (✅):** GTO passou a usar **convênios reais** da tabela `planos` (menu PLANOS/CONVÊNIOS) via dropdown no `LancarGTO` (carrega `getPlanos`); `credenciadaid` agora guarda o **id do plano** (obrigatório). O lançamento GTO grava `financeiro.convenio_id` e a descrição traz o nome do convênio (`GTO <id> — <nome>`).
- **Relatório por convênio (✅):** `GET /api/financeiro/relatorio` agora retorna `porConvenio` (RECEITA agrupada por `convenio_id`, join `planos`, com total/recebido/a_receber/qtd; sem convênio = "Particular"); bloco "Receitas por convênio" no `ReportsView`. Respeita os filtros de período/paciente/profissional.
- **Comprovante do repasse (✅):** `repasses.comprovante_{filename,mimetype,nome}` + `POST /api/repasses/:id/comprovante` (multer memória, imagem/PDF, MEDIA_ROOT/repasses/<id>/) e `GET /api/repasses/comprovante/:id/raw` (URL assinada `signMedia`/`verifyMedia`); listagem retorna `comprovante_url`. UI no Histórico (`ComissoesView`): anexar (clip) / ver (PDF) nos repasses pagos.
- **Competência por conclusão (✅):** `financeiro.data_competencia` (DATE) gravada na origem — procedimento realizado usa `data_conclusao` (campo "Concluído em" no prontuário; default hoje), GTO usa a data de finalização. `computeComissoes` base **produção** passou a usar `COALESCE(data_competencia, datavencimento)` (fallback p/ vencimento em lançamentos antigos). Recebido segue por `pago_em`.
- **Gaps remanescentes:** glosa por item de GTO; recurso/protocolo de glosa com prazos.
---
## GLOSAS DE CONVÊNIO — ✅ IMPLEMENTADO (DEV)
**Por quê:** convênio nega (glosa) parte do recebível; precisa marcar/gerenciar e recorrer.
- **Modelo:** `financeiro.valor_glosa` (acumulado) + tabela `glosas (financeiro_id, valor, motivo, status[glosado|recuperado], autor)`. Líquido a receber = `valor valor_glosa`.
- **Endpoints:** `GET /api/glosas[?status=]`, `GET /api/glosas/recebiveis` (RECEITA de convênio/GTO não totalmente glosada), `POST /api/glosas` (valida não exceder o restante; incrementa `valor_glosa`), `POST /api/glosas/:id/recuperar` (recurso ganho → devolve valor), `DELETE /api/glosas/:id` (correção). Todos tenantGuard + `registrarAudit`.
- **Frontend:** `GlosasView` (menu GLOSAS, visível p/ donos e funcionário) — aba "Recebíveis/Marcar glosa" (busca + modal valor+motivo) e aba "Glosas" (filtro glosado/recuperado, recuperar/excluir).
- **Glosa no relatório (✅):** `GET /api/financeiro/relatorio``resumo.glosa` + `resumo.a_receber_liquido` (= a_receber+atrasado glosa) e coluna `glosa` em `porConvenio`; UI mostra KPI Glosa (com líquido) e coluna no relatório por convênio.
- **Glosa por item + recurso (✅):** `glosas.{gto_item_id,item_descricao,protocolo,prazo_recurso}`; status agora `glosado|recurso|recuperado`. `GET /api/glosas/itens/:financeiroId` lista os `gto_items` da guia (via `gto_builders.financeiro_id`) com flag `ja_glosado`; POST aceita item/protocolo/prazo e bloqueia item já glosado (409). `POST /api/glosas/:id/recorrer` (protocolo+prazo, mantém valor_glosa); recuperar/excluir tratam status recurso. UI: seletor de item no `GlosaModal` + protocolo/prazo, filtro "Em recurso", ação Recorrer e exibição de item/protocolo/prazo.
---
## Varredura final do módulo financeiro (DEV)
- **Isolamento multi-tenant:** todos os endpoints novos (financeiro/relatorio, comissoes/*, repasses/*, glosas/*, contratos/.../gerar-cobranca) usam `tenantGuard`; teste cross-tenant retornou **403** em todos (exceto os `*/raw` de mídia, protegidos por URL assinada). Mutações por `:id` validam `clinica_id` antes de alterar.
- **Índices adicionados:** `idx_financeiro_contrato_comp (contrato_id,competencia)`, `idx_financeiro_convenio`, `idx_glosas_financeiro`, `idx_glosas_item`, `idx_gto_builders_financeiro` (além dos já existentes `idx_financeiro_clinica/_prof`, `idx_glosas_clinica`, `idx_repasses_clinica`, `uq_comissao_regra2`).
- **Limitações conhecidas (futuro):** (1) glosa não reduz automaticamente a base de comissão "recebido" se um recebível glosado for marcado Pago com valor cheio — ao dar baixa em recebível de convênio com glosa, reduzir o valor pago; (2) orçamento `financeiro_gerado=true` evita duplicar parcelas, mas não regenera se o valor mudar após assinado (falta ação "regenerar").
## Contrato para dentista que atende convênios (✅)
Novo modelo global `cmod_odo_14`**"Atendimento a Convênios / Repasse (Dentista Credenciado)"** (tipo `dentista-clinica`), com cláusulas de: objeto (convênios atendidos), guias/GTO e faturamento, repasse (% sobre produção, base recebido/produção, prazo), custo de laboratório, glosas (responsável + dedução do repasse + registro de protocolo/prazo), obrigações, ausência de vínculo, vigência/rescisão, LGPD e foro. Novas variáveis em `VARIAVEIS_CONTRATO`: `{{CONVENIOS}}`, `{{PERCENTUAL_REPASSE}}`, `{{BASE_REPASSE}}`, `{{PRAZO_REPASSE}}`, `{{RESPONSAVEL_GLOSA}}`, `{{CUSTO_LABORATORIO}}` — aparecem automaticamente no Gerador de contrato (data-driven). Liga-se ao financeiro: repasse (base recebido/produção), custo de lab e glosas implementados no módulo.