- reúne todos os .md de ScoreOdonto numa pasta única (fonte única, dentro do repo versionado pelo Gitea) - arquitetura.md: reescrito p/ o estado REAL (3 VPS, banco compartilhado dev/prod, sem staging/listener/registry fantasmas) - deploy.md: reescrito p/ o estado-ALVO (Gitea CI/CD: push->build->registry->deploy por tag) - DEPLOY-PRODUCAO.md: runbook operacional executável (fluxo manual real de hoje) - VERDADE-HOJE.md: auditoria (codigo/banco/containers/deploy/versao/rollback) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
19 KiB
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+ escopoclinica_id, soft delete onde fizer sentido,registrarAuditnas mutações, helpersbuildInsert/buildUpdate, frontenduseToast/useConfirm, rebuild da imagem DEV (frontend nginx + backend) — verfeedback_deploy_workflow. Estimativa: P (≤0,5 dia) · M (1–2 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 ✅ (
FinanceiroViewenviapaciente_id) · H1.3 ✅ (LancarProcedimentoModalenviapaciente_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/financeiroaceitam os novos campos (já ébuildInsert); manterclinica_iddotenantGuard. - Critérios: lançamento salva e retorna os campos; antigos (sem FK) continuam válidos;
node --check+ E2E (criar comprofissional_ide ler). - Risco:
pacientenomelegado 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 (reusargetPacientes+ componente doAppointmentModal); preencherpaciente_id+pacientenome(snapshot). - Critérios: ao gravar,
paciente_idpreenchido; 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á chamasaveFinanceiro) 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) noFinanceiroViewsubstitui o "marcar Pago" direto;useConfirmp/ valores ≥ R$ 1.000.PUT /api/financeiro/:idcarimbapago_em(se ausente) e registraregistrarAudit(baixa_pagamento, idempotente). Bug latente corrigido:enforceUppercase(service) mandavaPAGO/PIX, violando os CHECK do banco (Pago/Pix); backend agora normalizastatus/formapagamento/tipono POST e no PUT (normalizeFinanceiro). Era a causa de a tabelafinanceiroficar sempre vazia.
- Como financeiro, quero confirmar pagamento registrando
pago_eme a forma para ter caixa real. - Backend:
PUT /api/financeiro/:idaceitastatus='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;
useConfirmpara valores altos. - Critérios: ao confirmar, grava
pago_emeformapagamento; KPI Recebido considerapago_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_ataofinanceiro(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.NovoLancamentoModalganhou 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) noGET /api/financeiro/relatorio+ bloco noReportsView.
- 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 filtradeleted_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, escopoclinica_id, soft delete) retornandoresumo(recebido/a_receber/atrasado/despesa),porMes,porFormaelancamentos. UI emReportsView(componenteRelatorioFinanceiro): 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_custono 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 viaorcamentos.financeiro_gerado; helpergerarFinanceiroOrcamento; toast noOrcamentoModal. H4.2 ✅ — contrato (motor) com cobrança recorrente: camposcontratos.{cobranca_automatica,valor_recorrente,periodicidade(mensal|anual),dia_cobranca}+financeiro.competencia. HelpergerarCobrancaContratoCompetencia(idempotente por contrato_id+competência, origem='contrato', dia clamp 28) egerarCobrancasRecorrentesrodando no cronprocessarVigenciasContratos(boot+6h) sobre contratos vigentes. Endpoint manualPOST /api/contratos/instancias/:id/gerar-cobranca. UI: config noGeradorModal+ botão "Gerar cobrança do mês" nos contratos vigentes (ContratosView). Renovação automática ✅ — no mesmo cron, contratosrenovacao='automatica'comdata_fimvencido estendem a vigência por nova duração igual à anterior (data_inicio=data_fim+1,data_fim += (data_fim−data_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 devalor_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 conformevalores/periodicidade do contrato (contrato_idna FK). - Critérios: contrato mensal gera lançamento por competência; não duplica.
Sequência sugerida (entrega incremental)
- H1.1 → H2.4 → H2.2 (FKs + soft delete + atrasado) — base sólida e segura.
- H1.2 → H1.3 (paciente real + procedimento→lançamento) — liga clínica↔financeiro.
- H2.1 → H2.3 (baixa + despesas) — caixa fiel.
- H3.1 → H3.2 → H3.3 (relatórios) — visão gerencial.
- 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_idporpacientenome(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 gravarepasses.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 zerarepasses.financeiro_id; auditoria registradespesa_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/finalizaragora gera RECEITA (convênio a receber, vencimento +30d, origem='gto', FKs paciente/profissional) — idempotente viagto_builders.financeiro_id. A telaLancarGTOnão chamava os endpoints (handleFinalizeera stub); agora persiste builder+itens+finaliza (servicecreateGtoBuilder/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 noLancarGTO(carregagetPlanos);credenciadaidagora guarda o id do plano (obrigatório). O lançamento GTO gravafinanceiro.convenio_ide a descrição traz o nome do convênio (GTO <id> — <nome>). - Relatório por convênio (✅):
GET /api/financeiro/relatorioagora retornaporConvenio(RECEITA agrupada porconvenio_id, joinplanos, com total/recebido/a_receber/qtd; sem convênio = "Particular"); bloco "Receitas por convênio" noReportsView. 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//) eGET /api/repasses/comprovante/:id/raw(URL assinadasignMedia/verifyMedia); listagem retornacomprovante_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 usadata_conclusao(campo "Concluído em" no prontuário; default hoje), GTO usa a data de finalização.computeComissoesbase produção passou a usarCOALESCE(data_competencia, datavencimento)(fallback p/ vencimento em lançamentos antigos). Recebido segue porpago_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) + tabelaglosas (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; incrementavalor_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 colunaglosaemporConvenio; 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 agoraglosado|recurso|recuperado.GET /api/glosas/itens/:financeiroIdlista osgto_itemsda guia (viagto_builders.financeiro_id) com flagja_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 noGlosaModal+ 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*/rawde mídia, protegidos por URL assinada). Mutações por:idvalidamclinica_idantes 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á existentesidx_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=trueevita 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.