docs: unifica documentação em doc/ (fonte única, sem sobreposição)
build-and-push / build (push) Successful in 1m10s
build-and-push / deploy (push) Has been skipped

- 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>
This commit is contained in:
VPS 4 Builder
2026-06-17 12:04:08 +02:00
parent 0f50835114
commit 5049ce9b8b
14 changed files with 0 additions and 713 deletions
+165
View File
@@ -0,0 +1,165 @@
# Auditoria Funcional — ScoreOdonto (estado atual + roadmap V1/V2/V3)
> Documento mestre da auto-auditoria (read-only). Gerado a partir do código em 2026-06-15.
> **Nota de nomenclatura:** NÃO existe módulo "Delivery" separado. O antigo "Delivery" foi
> renomeado e é o marketplace de **Profissionais** (propostas clínica→profissional).
> Onde o prompt de benchmark cita "Delivery", leia-se **Profissionais**.
---
## PARTE I — Arquitetura funcional atual
**Papéis (9):** `superadmin, admin, donoclinica, donoconsultorio, dentista, biomedico, protetico, funcionario, paciente`.
**Modelo multi-tenant:** workspaces = linhas em `clinicas` (tipo `clinica` ou `pessoal`); acesso via `vinculos`; isolamento por `clinica_id` (auditado e endurecido em 8 rodadas de segurança). É um diferencial maduro.
### Inventário de telas (40+), por domínio
| Domínio | Telas | Acesso |
|---|---|---|
| Núcleo clínico | Dashboard, Agenda, Pacientes, Ortodontia (OrthoView/PhotoWorkspace/Reports/Annotator), Meus Tratamentos, Lançar GTO | dono/admin/funcionário/dentista/bio/protético |
| Catálogos | Dentistas, Especialidades, Procedimentos, Planos/Convênios | dono/admin/funcionário |
| Financeiro/Comercial | Financeiro, Relatórios, Orçamentos, **Contratos (motor)**, Gestão de Leads | dono/admin/funcionário |
| Marketplace/Recursos | Locação de Salas, **Profissionais (marketplace)**, Diretório Prof., RX/Radiografias, Dispositivos RX | quase todos |
| Tutoria (orto) | Ser Tutor, Painel Tutor, Gestão Tutores | dentista/admin |
| Gestão | Minhas Clínicas, Equipe/Usuários (RBAC), Plugins, Configurações, Notificações | dono/admin |
| Onboarding/Auth | Landing, Login, OnboardingChat, Cadastro Dentista, Criar Clínica, Aguardando Convite, Contato Público | público |
| Superadmin | SuperAdminView (stats/usuários/clínicas/logs/financeiro/notificações), UpdateDb, Sync | superadmin |
### Menu (real)
Visão Geral · Agenda · Pacientes · Lançar GTO · Ortodontia · Financeiro · Relatórios · Orçamentos · Contratos · Gestão de Leads · Meus Tratamentos · Minhas Clínicas · Dentistas · Especialidades · Procedimentos · Planos/Convênios · Equipe/Usuários · Diretório Prof. · Tutoria (3) · Notificações · Plugins · Configurações.
Menu base + filtro `accessByRole` + RBAC por cargo (`funcoes.permissoes/nivel`; array vazio herda o menu do papel). Paciente vê só Tratamentos/Notificações/Config.
Não há: menus contextuais (botão direito), menus dentro de tabelas, breadcrumbs. Navegação é hash-route + cards/modais.
### Backend
≈150 endpoints. Guards: `authGuard / tenantGuard / superadminGuard / adminGuard`. Auditoria via `audit_log`. Tempo real em `/api/ws`.
---
## PARTE II — Gap analysis
### Forte (vantagem competitiva)
- Multitenant/multiempresa (workspaces universais; isolamento por clínica auditado).
- Agenda — anti-overbooking GLOBAL (mesma pessoa não dobra entre clínicas), faltas/reagendar/confirmar/reencaminhar, pendências "a reagendar", presença em tempo real, importar Google.
- Prontuário clínico — odontograma FDI + fotos (avaliação); Ortodontia completa (tratamento/evoluções/documentação fotográfica com anotador/escala + PDFs de tutoria); RX (plugin + laudos); GTO/guias.
- Contratos — motor universal (modelos+variáveis, gerador, assinatura interna com IP/hash, vigência/alertas, 28 modelos de catálogo). Acima da média de mercado.
- Marketplace (salas anti-conflito triplo + **Profissionais** com propostas/diretório).
- RBAC por cargo/nível + realtime.
### Parcial
- Financeiro — só lançamentos receita/despesa + dashboard. Falta contas a pagar/receber estruturado, vencimentos, baixa.
- CRM — só Leads + conversão→paciente. Sem funil/oportunidades/campanhas.
- Convênios — "Planos/Convênios" é catálogo simples; sem regras de cobertura/repasse/glosa.
- Comunicação — notificações in-app/realtime + webhooks WhatsApp na infra; sem disparo de WhatsApp/SMS/e-mail pelo app; sem lembretes automáticos de consulta.
- Relatórios — só `productivity`.
- RH — dentistas/horários/equipe/funções; sem escalas/ponto/folha; repasse só na tutoria (manual).
- Mobile — web responsivo; sem PWA/app.
### Ausente
- Estoque (produtos, lotes, validade, inventário, compras, fornecedores).
- Financeiro avançado: fluxo de caixa, DRE, centros de custo, conciliação bancária, comissões/rateios/repasse estruturados, gateways PIX/cartão/boleto, carnês/parcelamento.
- Prontuário: anamnese estruturada, periodontograma, assinatura do paciente no prontuário, consentimento (TCLE).
- Agenda: lista de espera, recorrência, bloqueios, recursos/equipamentos como agendáveis.
- CRM: funil, campanhas, automações (WhatsApp), pipeline de orçamento.
- **Profissionais:** atendimento geolocalizado/geodistância (gap já mapeado), avaliações/reputação. (Não confundir com "Delivery" — é a evolução do módulo Profissionais.)
### Obsoleto / dívida técnica
- `UpdateDbView`/`Sync` (legado de planilha) — manter só superadmin.
- `/fotos/:id/raw` públicas por id (mitigado com URL assinada; ideal evoluir).
- Orçamento gera um "Contrato" (documento) — convém unificar o fluxo orçamento→contrato no motor.
---
## PARTE III — Roadmap (V1 / V2 / V3)
Matriz: Impacto (I) · Retorno financeiro (R) · Esforço (E) — escala 15.
### V1 — Quick wins (alto I, baixo E)
| Item | I | R | E |
|---|---|---|---|
| Anamnese + TCLE estruturados no prontuário | 5 | 3 | 2 |
| Lembretes automáticos de consulta (WhatsApp/e-mail via webhook existente) | 5 | 4 | 2 |
| Agenda: bloqueios + lista de espera (reaproveitar "interesses"/anti-overbooking) | 4 | 3 | 2 |
| Contas a pagar/receber sobre o `financeiro` atual (status/vencimento/baixa) | 5 | 4 | 3 |
| Orçamento → Contrato → Financeiro (ligar os 3 que já existem) | 5 | 5 | 3 |
| Pacote de relatórios essenciais (produção, faltas, financeiro por período, conversão de leads) | 4 | 3 | 2 |
### V2 — Diferenciação e receita (alto R)
| Item | I | R | E |
|---|---|---|---|
| Comissões/Repasse profissional (agenda→procedimento→financeiro→profissional) | 5 | 5 | 4 |
| Gateways de pagamento (PIX/cartão/boleto) + carnês/parcelamento | 4 | 5 | 4 |
| CRM com funil + WhatsApp + automações (sobre Leads) | 5 | 5 | 4 |
| Fluxo de caixa + centros de custo | 4 | 4 | 3 |
| Periodontograma + evolução clínica padronizada | 4 | 2 | 3 |
| Contratos: partes externas por link + PDF server-side + Clicksign | 4 | 3 | 4 |
### V3 — Plataforma
| Item | I | R | E |
|---|---|---|---|
| Estoque completo (lotes/validade/inventário/compras/fornecedores) + integração financeira | 4 | 3 | 5 |
| DRE + conciliação bancária | 3 | 4 | 5 |
| RH: escalas/ponto/produção/folha | 3 | 3 | 5 |
| App/PWA mobile | 4 | 3 | 4 |
| Profissionais: atendimento/geodistância + avaliações/reputação | 3 | 3 | 4 |
| BI/Dashboards configuráveis + alertas automatizados | 3 | 3 | 4 |
---
## Mapa de conexões entre telas
### Já existe
- Agenda → Novo Agendamento → busca Paciente → cria agendamento.
- Lead → Converter → Paciente (+ 1º agendamento).
- Agendamento → Solicitar Avaliação → novo slot vinculado.
- Paciente → tile Orçamentos → gera contrato/print.
- Sala → Reserva → anti-conflito.
- Profissional (marketplace) → Proposta → notificação.
### Falta ligar (o "tecido conjuntivo" — maior ganho de V1/V2)
- Procedimento realizado (agenda) → lançamento Financeiro → Comissão → Repasse profissional.
- Orçamento aprovado → Contrato (motor) → Financeiro (parcelas).
- Contrato assinado → vigência → cobrança recorrente.
- Estoque ← consumo por procedimento.
> Diagnóstico central: módulos fortes, pouco conectados. O maior retorno não é criar
> módulos novos, e sim LIGAR agenda↔financeiro↔contrato↔profissional.
---
## Síntese por cenário
- Clínica/Consultório: prontos; faltam financeiro avançado + comissões.
- Dentista autônomo: workspace "pessoal" já funciona; falta repasse/produção.
- Biomédico: áreas/especialidades ok; harmonização (contrato já criado); falta agenda estética dedicada.
- Locação de salas: maduro; falta integrar reserva→financeiro/contrato de locação (o motor já tem o modelo).
- **Profissionais (marketplace, ex-"Delivery"):** maduro; falta avaliações/reputação, fechamento via contrato e geodistância.
- Multiempresa/Multitenant: o ponto mais forte — base sólida para escalar.
---
# DEEP-DIVE 1 — Financeiro + Comissões/Repasse (2026-06-15)
## A) Checklist da tela Financeiro
- **Objetivo:** registrar/acompanhar lançamentos (receita/despesa) por status.
- **Acesso:** Menu → Financeiro; dono/admin/funcionário (escopo por clínica, `tenantGuard`).
- **Dados:** lista (paciente, descrição, valor, vencimento, status) + KPIs Recebido/Pendente/Atrasado.
- **Tabela `financeiro`:** `id, clinica_id, pacientenome(texto), descricao, valor, datavencimento, status(Pago/Pendente/Atrasado), formapagamento, tipo(RECEITA/DESPESA)`. 0 linhas.
- **Botões:** Filtrar(status) · Novo lançamento · linha: Confirmar pagamento, Gerar recibo (download), Excluir.
- **Integrações:** PARCIAL — `LancarProcedimentoModal → saveFinanceiro` (procedimento gera 1 lançamento). Nenhuma outra.
- **Limitações:** soma por status; "Atrasado" manual.
## B) Diagnóstico — Financeiro é uma "ilha"
A `financeiro` não tem chaves de ligação: `pacientenome` é texto (não FK); não há `profissional_id`, `agendamento_id`, `procedimento_id`, `orcamento_id`, `contrato_id`. Despesa = só `tipo=DESPESA` (sem fornecedor/centro de custo/recorrência). **Comissão/Repasse inexistente** (`procedimentos.valorparticular` sem %; `dentistas` sem repasse). A maior alavanca é a FK **`profissional_id`** — sem ela, não há comissão, repasse nem relatório de produção.
## C) Fluxo conectado proposto
Agenda (paciente+dentista+procedimento) → concluído → LANÇAMENTO (com FKs) → regra de COMISSÃO → REPASSE por período. Orçamento aprovado → Contrato (motor) → parcelas (contas a receber). Despesa → fornecedor/centro de custo → fluxo de caixa/DRE.
## D) Evolução do modelo
- **V1:** FKs em `financeiro` (`paciente_id, profissional_id, procedimento_id, agendamento_id, contrato_id, origem`); `pago_em`; job de "Atrasado"; contas a pagar (fornecedor/centro de custo/recorrente); relatórios essenciais.
- **V2:** `comissao_regras` (por procedimento/profissional/convênio; %/fixo/produção; base bruta/líquida); `comissao_valor` no lançamento; `repasses` (consolida por profissional/período) formalizado pelos modelos "Contrato por Comissão/Produção/Percentual" do motor.
## E) Regras de comissão por cenário
Dentista da clínica (% líquido por especialidade/convênio); autônomo (100%, ou taxa de sala); sala alugada (receita aluguel, liga `sala_reservas`); biomédico/harmonização (% + material parametrizável); Profissionais/marketplace (contrato de comissão define %); multiempresa (repasse consolidado por unidade).
> Backlog acionável do V1 Financeiro: ver `BACKLOG-FINANCEIRO-V1.md`.
+156
View File
@@ -0,0 +1,156 @@
# 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.
+160
View File
@@ -0,0 +1,160 @@
# Estratégia de Banco DEV — Relatório + Plano de Implementação
> Documento de **decisão e plano** (16/jun/2026). A 1ª parte é o relatório técnico baseado em dados reais; a 2ª é o plano de implementação profissional, escalável para **todos os projetos atuais e futuros**.
> Relacionado: `arquitetura.md`, `PENDENCIAS.md` (P0 isolar dev), `VERDADE-HOJE.md`.
---
# PARTE 1 — Relatório técnico (dados reais, read-only)
**Servidor (VPS3):** PostgreSQL **18.4** · **PostGIS 3.6.2** (suíte completa disponível).
**Bancos (8 no total, ~175 MB somados):**
| Banco | Tamanho | Projeto | DEV na VPS4? | Tabelas | Índices | PostGIS |
|-------|--------:|---------|:------------:|--------:|--------:|:-------:|
| newwhats | 91 MB | NewWhats (VPS1) | não | — | — | — |
| giteadb | 22 MB | Gitea (infra) | não | — | — | não |
| **scoreodonto** | **21 MB** | **ScoreOdonto** | **sim** | 104 | 168 | **SIM (3 geog)** |
| mercado | 13 MB | Mercado (VPS1) | não | — | 187 | não |
| **dental_images** | **11 MB** | **RX** | **sim** | 8 | 25 | não |
| clube67 | 8.8 MB | Clube67 | não | — | — | — |
| **subdominio** | **8 MB** | **Subdomínio** | **sim** | 3 | 7 | não |
| postgres | 7.7 MB | default | — | — | — | — |
**Conclusões:**
- Os 3 bancos de DEV (scoreodonto, dental_images, subdominio) somam **~40 MB**. São minúsculos.
- **Só o ScoreOdonto usa PostGIS.** RX e Subdomínio não.
- Sem dependências entre bancos. Sem dados de crescimento histórico (não medível sem superuser); porte indica crescimento lento.
**Veredito:** a proposta "mirror + dev por projeto" (6 containers PostgreSQL/PostGIS) é **viável mas exagerada** para esse porte. Um "mirror" *rodando* não agrega sobre um **template database** ou um **dump**; é só RAM e complexidade a mais (~300600 MB RAM e 6× manutenção).
**Recomendação final:** **um único container PostGIS na VPS4** (rootless), multi-banco, com `*_ref` (referência/template, restaurado da VPS3) + `*_dev` (de trabalho, criado via `CREATE DATABASE ... TEMPLATE`). Reset instantâneo, custo ~50100 MB RAM, manutenção mínima, e **escala para qualquer projeto novo**.
---
# PARTE 2 — Plano de implementação (profissional, escalável)
> Objetivo: desgrudar **todos** os ambientes DEV do banco de produção (VPS3), com um padrão único que serve projetos atuais e futuros. **Produção (VPS3/VPS1) nunca é alterada — só lida (pg_dump).**
## 2.1 Arquitetura
```
VPS3 (produção, fonte da verdade)
│ pg_dump (somente leitura, one-way)
┌──────────────── VPS4 (rootless) — container único "pgdev" ───────────────┐
│ postgis/postgis 18-3.6 (porta interna, rede 'soc', sem exposição pública) │
│ │
│ scoreodonto_ref ──TEMPLATE──► scoreodonto_dev ◄── backend dev ScoreOdonto │
│ dental_images_ref ─TEMPLATE──► dental_images_dev ◄── backend dev RX │
│ subdominio_ref ──TEMPLATE──► subdominio_dev ◄── backend dev Subdomínio │
│ <novo>_ref ──TEMPLATE──► <novo>_dev ◄── (projeto futuro) │
└────────────────────────────────────────────────────────────────────────────┘
```
- **`*_ref`**: cópia fiel da produção (restaurada por `pg_dump`). Não se mexe — é a referência/"mirror".
- **`*_dev`**: banco de trabalho, recriável do `_ref` em segundos via `TEMPLATE`. É onde rodam migrações e testes.
- **Um container só** atende todos (Postgres lida com dezenas de bancos pequenos sem esforço). PostGIS coberto para quem precisa (ScoreOdonto); os demais simplesmente não usam.
## 2.2 Componentes (a criar em `/home/deploy/stack/pg-dev/`)
**a) `docker-compose.yml`** (container do banco DEV):
```yaml
services:
pgdev:
image: postgis/postgis:18-3.6 # confirmar tag p/ PG18+PostGIS3.6 (alt.: imresamu/postgis:18-3.6)
container_name: pgdev
restart: unless-stopped
environment:
POSTGRES_USER: devadmin
POSTGRES_PASSWORD: ${PGDEV_PASSWORD}
volumes:
- pgdev-data:/var/lib/postgresql/data
networks: [soc] # mesma rede dos backends de dev (acesso por DNS 'pgdev')
# SEM ports: publicado → só acessível internamente (segurança)
volumes:
pgdev-data:
networks:
soc:
external: true
```
**b) `pg-dev-projects.conf`** (manifesto — adicionar 1 linha por projeto novo):
```
# nome_projeto : banco_origem_VPS3 : usuario_origem
scoreodonto : scoreodonto : scoreodonto_user
rx : dental_images : clube67
subdominio : subdominio : mercado_admin
```
**c) `.env` / `secrets`** (gitignored, fora do repo): senha do `pgdev` (`PGDEV_PASSWORD`) e as senhas de origem da VPS3. **Nunca** versionar.
**d) `pg-dev-refresh.sh <projeto>`** — atualiza o `_ref` a partir da VPS3 e recria o `_dev`:
```
1. pg_dump -Fc da VPS3 (banco_origem) usando a credencial do manifesto (read-only).
2. DROP/CREATE <projeto>_ref no pgdev; pg_restore do dump → _ref.
3. DROP <projeto>_dev; CREATE DATABASE <projeto>_dev TEMPLATE <projeto>_ref.
```
**e) `pg-dev-reset.sh <projeto>`** — reset rápido (sem ir à VPS3):
```
DROP <projeto>_dev; CREATE DATABASE <projeto>_dev TEMPLATE <projeto>_ref;
```
## 2.3 Apontar os backends de DEV para o pgdev (sem afetar produção)
Parametrizar a `DATABASE_URL` no compose de cada projeto para usar variáveis com **default = produção**, e na VPS4 criar um `.env` (gitignored) que sobrescreve só no dev:
```
# no docker-compose.yml (default mantém produção intacta):
DATABASE_URL=postgresql://${DB_USER}:${DB_PASS}@${DB_HOST:-10.99.0.3}:5432/${DB_NAME:-scoreodonto}
# .env SÓ na VPS4 (dev):
DB_HOST=pgdev
DB_NAME=scoreodonto_dev
DB_USER=devadmin
DB_PASS=<senha pgdev>
```
- Na VPS1 (produção) não existe esse `.env` → resolve para `10.99.0.3/scoreodonto`. **Produção nunca muda.**
- Rebuild/recreate do backend de dev → passa a falar com `pgdev`.
## 2.4 Passos de implantação (quando aprovado)
1. Criar `/home/deploy/stack/pg-dev/` com o compose + scripts + `.env` (secrets).
2. `docker compose up -d pgdev` (rootless) + adicionar `pgdev` à rede `soc`.
3. Rodar `pg-dev-refresh.sh scoreodonto` (e rx, subdominio) → cria os `_ref` e `_dev`.
4. Criar o `.env` de dev em cada projeto e recriar os backends de dev.
5. Verificar: `dev.scoreodonto.com/api/version` sobe; conferir que escrita em dev cai em `scoreodonto_dev` (e produção em `scoreodonto` permanece intacta).
6. (privacidade) opcional: script de anonimização de dados sensíveis nos `_dev`.
## 2.5 Operação do dia a dia
- **Quer dados atualizados da produção?** `pg-dev-refresh.sh <projeto>`.
- **Sujou o dev e quer voltar ao limpo?** `pg-dev-reset.sh <projeto>` (segundos, via TEMPLATE).
- **Projeto novo?** adiciona 1 linha no `pg-dev-projects.conf` + `pg-dev-refresh.sh <novo>`. Pronto — sem novo container.
## 2.6 Custo e ganho
- **Custo:** 1 container (~50100 MB RAM, ~100 MB disco). Disco da VPS4 hoje: ~64 GB livres → folga total.
- **Ganho:** dev deixa de poder corromper produção; resets instantâneos; PostGIS coberto; padrão único que escala para N projetos sem multiplicar containers.
## 2.7 O que este plano deliberadamente NÃO faz
- Não cria 2 containers por projeto (over-engineering p/ bancos de 821 MB).
- Não usa réplica streaming (exigiria config superuser na VPS3 + WAL; canhão p/ 40 MB).
- Não toca em produção (VPS3/VPS1) — refresh é one-way, só `pg_dump`.
---
# PARTE 3 — Status de implementação (16/jun/2026)
**Implementado em `/home/deploy/stack/pg-dev/`** (Docker rootless da VPS4):
- ✅ Container **`pgdev`** (`postgis/postgis:18-3.6`) na rede `soc`, sem porta pública, volume `pgdev-data` em `/var/lib/postgresql`.
- ✅ Scripts `pg-dev-refresh.sh` / `pg-dev-reset.sh` + manifesto `pg-dev-projects.conf` + `secrets.env` (gitignored).
- ✅ Bancos criados (ref+dev): **scoreodonto** (20 MB, PostGIS 3.6.3, 1750 pacientes), **rx** (dental_images), **subdominio**.
**Backends apontados (via `.env` gitignored + `DATABASE_URL`/`PG_*` parametrizados com default=produção):**
-**ScoreOdonto**`pgdev/scoreodonto_dev` (validado: backend conecta, migrações OK; `dev.scoreodonto.com` no ar). Exigiu ajuste de SSL no backend (não força SSL p/ host `@pgdev`).
-**Subdomínio**`pgdev/subdominio_dev` (env confirmado, site no ar).
-**RX**`pgdev/rx_dev` (`rx.scoreodonto.com` no ar). Solução: app adicionado à rede `soc` + `DB_HOST/DB_NAME` parametrizados; role `clube67` criado no pgdev com a senha do `.secrets` (assim DB_USER/senha não mudam).
**Produção (VPS3/VPS1) intocada.** Para um projeto novo: adicionar linha no `pg-dev-projects.conf`, `SRC_<proj>_PASS` no `secrets.env`, rodar `./pg-dev-refresh.sh <proj>` e parametrizar o compose do projeto + `.env`.
+497
View File
@@ -0,0 +1,497 @@
# CRM ORTODONTIA + TUTORIA ORTODÔNTICA
---
# LISTA DE TAREFAS
## FASE 1 — Documentação Fotográfica (armazenamento local)
### Backend
- [x] Tabelas orto_photo_session, orto_photo, orto_photo_annotation no PostgreSQL
- [x] multer configurado — upload local em /app/media/orto-fotos/
- [x] express.static servindo /app/media em /media
- [x] POST /api/orto/:tratamentoId/sessoes — criar/buscar sessão por tipo
- [x] GET /api/orto/:tratamentoId/sessoes — listar sessões com fotos
- [x] POST /api/orto/sessoes/:sessaoId/fotos — upload de foto para slot
- [x] DELETE /api/orto/fotos/:fotoId — excluir foto
- [x] GET /api/orto/:tratamentoId/completude — % de documentação preenchida
### Frontend
- [x] OrthoPhotoWorkspace — substitui PhotoCompare placeholder
- [x] Tabs de sessão (Inicial / 3m / 6m / 12m / Finalização / Pós-contenção)
- [x] Grid de 15 slots (5 extraoral + 5 intraoral + 5 complementar)
- [x] Upload por clique no slot (file input)
- [x] Visualizador de foto (modal fullscreen)
- [x] Exclusão de foto com confirmação
- [x] Indicador de completude (%)
- [x] Comparador lado a lado (selecionar 2 slots e comparar)
- [x] backend.ts — métodos getOrtoSessoes, uploadOrtoFoto, deleteOrtoFoto, getOrtoCompletude
## FASE 2 — Tutoria Integrada
- [x] Tabelas ortho_tutor_profile, ortho_tutoring_request, ortho_tutoring_message, ortho_tutoring_photo_package
- [x] Botão "Solicitar Tutoria" no painel do paciente (TutoriaSection.tsx)
- [x] Lista de tutores disponíveis com SLA e preço
- [x] Formulário de solicitação (escolha tutor, tipo, descrição)
- [x] Pacote automático gerado (todas as fotos do tratamento incluídas)
- [x] Chat inline por caso (CLINICA ↔ TUTOR)
- [x] Painel do tutor — receber, responder, fechar (TutorPainelView.tsx)
- [x] Status tracking: AGUARDANDO → EM_ANALISE → RESPONDIDO → FECHADO
- [x] Menu "PAINEL TUTOR" no Sidebar
- [x] Rota /tutoria-painel no App.tsx
## FASE 3 — Planejamento Visual e PDFs
- [x] Marcações clínicas nas fotos — PhotoAnnotator.tsx (SVG overlay)
- Ferramentas: Seta, Linha, Círculo, Retângulo, Texto, Caneta livre
- Cores (6) + espessura (P/M/G) + desfazer + limpar
- Anotações persistidas no banco (orto_photo_annotation)
- Botão ANOTAR no viewer de fotos
- [x] PDF — Documentação fotográfica (grade 15 slots + fotos base64)
- [x] PDF — Evolução ortodôntica (timeline cronológico)
- [x] PDF — Caso clínico para tutoria (triagem + diagnóstico + fotos)
- [x] jsPDF instalado (client-side, sem servidor)
- [x] OrthoReports.tsx integrado no OrthoDetailPanel
- [ ] Planejamento visual (vetores de movimentação) — Fase 4
- [ ] Tutor anota diretamente no painel de tutoria — próximo passo
## MARKETPLACE DE TUTORES (GLOBAL)
- [x] Fluxo de candidatura self-service para dentistas (TutorCandidaturaView)
- [x] Formulário acadêmico em 3 passos (dados pessoais / formação / perfil público)
- [x] Status tracking: PENDENTE → AGUARDANDO_PAGAMENTO → ATIVO / REJEITADO
- [x] Painel admin de gestão (AdminGestaoTutoresView) — 3 abas: Candidaturas / Ativos / Configurações
- [x] Aprovação/rejeição com campo de observação
- [x] Configuração de taxa de inscrição + chave PIX fallback
- [x] Integração MercadoPago — pagamento único (PIX / cartão / boleto)
- [x] Webhook automático — ativa perfil ao detectar pagamento aprovado
- [x] Confirmação manual de pagamento pelo admin (fallback)
- [x] Tutor global — disponível para TODAS as clínicas do sistema
- [x] Criação automática de ortho_tutor_profile após ativação
- [x] Tabelas: tutor_candidatura, tutor_config, tutor_pagamento
- [x] Sidebar: "SER TUTOR ORTO" (dentistas) + "GESTÃO TUTORES" (admin)
## FASE 4 — Marketplace e IA
- [ ] Marketplace público de tutores
- [ ] Sistema de pagamento
- [ ] IA clínica (sugestão de diagnóstico, sequência de fios)
- [ ] Rede colaborativa entre clínicas
- [ ] App mobile para registro fotográfico
## MIGRAÇÃO STORAGE
- [ ] Substituir multer local por Wasabi S3
- [ ] Manter URLs compatíveis (alterar apenas o driver de storage)
- [ ] Migração das fotos existentes para o Wasabi
---
## Visão Estratégica
Projeto de CRM Ortodôntico integrado com Marketplace de Tutoria Ortodôntica, Documentação Fotográfica, Planejamento Clínico e Colaboração entre Profissionais.
---
# MÓDULO CRM ORTODONTIA
## Componentes Principais
- PatientHeader
- ClinicalTriage
- Diagnosis
- TreatmentPlan
- Timeline
- NewEvolution
- PhotoCompare
- ContencaoTermo
- ClinicalRequests
- MaintenanceReport
- Finalization
## Fluxo do Tratamento
Triagem → Diagnóstico → Planejamento → Evoluções → Controle Fotográfico → Relatórios → Finalização → Contenção
---
# MÓDULO DE TUTORIA ORTODÔNTICA
## Objetivo
Permitir que ortodontistas experientes ofereçam:
- Segunda opinião
- Planejamento ortodôntico
- Mentoria clínica
- Discussão de casos
- Encaminhamento de pacientes
## Perfis
### Tutor Ortodôntico
- Publicar portfólio
- Receber solicitações
- Definir preços
- Definir SLA
- Receber pagamentos
### Solicitante
- Enviar casos
- Escolher tutor
- Acompanhar status
- Receber pareceres
---
# INTEGRAÇÃO DA TUTORIA AO CRM
A tutoria não será um sistema isolado.
Será incorporada ao painel do paciente:
Paciente
├── Triagem
├── Diagnóstico
├── Plano
├── Evoluções
├── Fotografias
├── Tutoria Ortodôntica
├── Relatórios
└── Finalização
## Solicitar Tutoria
Botão:
[ Solicitar Tutoria ]
### Tipos
- Segunda opinião
- Planejamento completo
- Discussão de caso
- Alinhadores
- Orto-cirúrgico
- Encaminhamento
---
# PACOTE AUTOMÁTICO PARA O TUTOR
Ao solicitar uma tutoria o sistema gera automaticamente:
## Dados Clínicos
- Triagem
- Diagnóstico
- Planejamento
- Evoluções
## Dados Ortodônticos
- Classe de Angle
- Fio atual
- Cooperação
- Status do tratamento
## Arquivos
- Fotografias
- Radiografias
- Tomografias
- STL
- PDFs
---
# MÓDULO DE DOCUMENTAÇÃO FOTOGRÁFICA
## Objetivo
Transformar as fotografias em uma documentação ortodôntica completa.
---
# WORKSPACE FOTOGRÁFICO
Novo módulo:
OrthoPhotoWorkspace
Recursos:
- Upload
- Organização
- Comparação
- Anotações
- Compartilhamento
- Exportação
---
# SESSÕES FOTOGRÁFICAS
Cada paciente pode possuir:
- Inicial
- Controle 3 meses
- Controle 6 meses
- Controle 12 meses
- Finalização
- Pós contenção
---
# TEMPLATE ORTODÔNTICO PADRÃO
## Extraoral
- Frontal sorrindo
- Frontal repouso
- Perfil direito
- Perfil esquerdo
- Perfil sorrindo
## Intraoral
- Frontal oclusão
- Lateral direita
- Lateral esquerda
- Oclusal superior
- Oclusal inferior
## Complementares
- Linha média
- Overjet
- Overbite
- Mordida cruzada
- Mordida aberta
---
# SISTEMA DE SLOTS
Cada fotografia ocupa posição específica.
Garantindo padronização clínica.
---
# EDITOR DE IMAGENS
Ferramentas:
- Zoom
- Crop
- Rotação
- Contraste
- Brilho
- Espelhamento
---
# MARCAÇÕES CLÍNICAS
Permitir desenhar diretamente nas imagens.
Tipos:
- Seta
- Linha
- Texto
- Círculo
- Área destacada
Exemplos:
- Linha média desviada
- Apinhamento
- Torque inadequado
- Mordida cruzada
---
# COMPARADOR FOTOGRÁFICO
Modos:
- Inicial × Atual
- Inicial × Final
- 6 meses × 12 meses
Visualizações:
- Slider
- Lado a lado
- Sobreposição
---
# CHECKLIST DE DOCUMENTAÇÃO
Validação automática:
- Frontal
- Perfil direito
- Perfil esquerdo
- Oclusal superior
- Oclusal inferior
- Oclusão frontal
Indicador:
Documentação Completa (%)
---
# TUTORIA SOBRE FOTOGRAFIAS
## Enviar para Tutor
O sistema envia:
- Fotos
- Diagnóstico
- Planejamento
- Timeline
- Evoluções
---
# RESPOSTA DO TUTOR
Tutor pode:
- Escrever parecer
- Marcar fotografias
- Desenhar vetores
- Apontar correções
- Criar planejamento visual
---
# PLANEJAMENTO VISUAL
Ferramenta Premium.
Permite:
- Vetores de movimentação
- Correção de linha média
- Planejamento de fechamento de espaços
- Estratégia de extrações
- Planejamento de alinhadores
---
# RELATÓRIOS
## PDF Documentação
Documentação completa organizada.
## PDF Evolução
Comparação cronológica.
## PDF Tutoria
- Caso clínico
- Fotografias
- Comentários
- Marcações
- Planejamento
---
# BANCO DE DADOS
## ortho_tutor_profile
Cadastro do tutor.
## ortho_tutoring_request
Solicitações de tutoria.
## ortho_tutoring_message
Chat do caso.
## ortho_tutoring_financial
Controle financeiro.
## ortho_photo_session
Sessões fotográficas.
## ortho_photo
Fotografias.
## ortho_photo_annotation
Marcações sobre imagens.
## ortho_photo_compare
Comparações.
## ortho_tutoring_photo_package
Pacotes enviados para tutores.
---
# ROADMAP
## Fase 1
- Persistência das fotos
- Sessões fotográficas
- Upload real
- Comparação
## Fase 2
- Tutoria integrada
- Chat
- Envio de documentação
## Fase 3
- Planejamento visual
- Marcações do tutor
- PDFs avançados
## Fase 4
- Marketplace público
- IA clínica
- Rede colaborativa de ortodontistas
---
# DIFERENCIAL
CRM Ortodontia
+
Documentação Fotográfica Profissional
+
Tutoria Ortodôntica
+
Planejamento Visual
+
Marketplace de Especialistas
+
Colaboração Entre Clínicas
+142
View File
@@ -0,0 +1,142 @@
# Deploy & Versionamento — Runbook ScoreOdonto
> Auditado contra o sistema em **17/jun/2026**. Cada item carrega seu **status real** — o doc
> responde *"o que existe hoje?"*, não *"o que gostaríamos?"*.
>
> **Legenda de status (convenção do projeto):**
> - ✅ **EM PRODUÇÃO** — está no ar em `scoreodonto.com` (VPS1), verificável agora.
> - 🧪 **EM DEV** — implementado no working tree da VPS4 / `dev.scoreodonto.com`; **não commitado, não promovido**.
> - 🎯 **ALVO** — desenhado/aprovado, **ainda não implementado**.
>
> **Produção AGORA:** `v1.0.15 · commit f2eb90c · 2026-06-17` (`curl https://scoreodonto.com/api/version`).
> Os Passos 1/3/4 (versão em runtime) foram **promovidos a produção** em 17/jun (tag `v1.0.15`).
---
## 1. O modelo
> **Push** na `main` produz imagem por **commit** no registry; a **tag `vX.Y.Z`** **promove** essa imagem para produção. **Produção nunca builda — só executa imagens.**
```
PUSH main (commit C) TAG vX.Y.Z (no commit C)
│ │
▼ ▼
CI: build CI: deploy
publica :C, :<versão>, :latest ✅ hoje: REBUILDA + promove + deploya
▼ 🎯 alvo (Passo 5): só re-tag :C→:vX.Y.Z, SEM build
registry ▼
VPS1: pull + up --no-build ✅
APP_VERSION=tag em runtime ✅ (Passo 3, em prod)
```
---
## 2. Estado por componente (verificado)
| Componente | Status | Detalhe |
|---|---|---|
| Infra VPS1/3/4 + Docker rootless | ✅ | Mapa na §3 |
| Registry (`git.clube67.com/ruicesar/...`) | ✅ | Ver `REGISTRY.md` |
| CI: `act_runner` "vps4-builder" builda+publica em push | ✅ | `.gitea/workflows/build.yml`; runner systemd na VPS4 |
| Deploy por tag (job `deploy` SSH VPS4→VPS1) | ✅ | `deploy-prod.sh` na VPS1 |
| Produção não builda (`image:` + `pull` + `up --no-build`) | ✅ | `docker-compose.prod.yml` |
| Banco DEV isolado em `pgdev`; prod no Postgres da VPS3 | ✅ | Ver `BANCO-DEV-ESTRATEGIA.md` |
| `GET /api/version` responde (de `process.env`) | ✅ | No ar: v1.0.15 · f2eb90c · PROD |
| **Versão exibida em Configurações → "Sobre o sistema"** | ✅ | Passo 1 (promovido em v1.0.15; saiu do Sidebar) |
| **Front/telas consomem `/api/version` (`useAppVersion`); app não usa `APP_VERSION`** | ✅ | Passos 1/4 (em prod desde v1.0.15) |
| **Deploy injeta `APP_VERSION`/`APP_ENV` em runtime (`environment`)** | ✅ | Passo 3 (`compose.prod`/`deploy-prod.sh` em prod) |
| **Tag promove sem rebuild + validação de integridade** | 🎯 | Passo 5 (§7) — único item pendente |
---
## 3. Mapa de máquinas ✅
| Onde | Host | Papel | Docker |
|---|---|---|---|
| VPS1 | `10.99.0.1` | **Produção** (Traefik + apps) | rootless (`unix:///run/user/1000/docker.sock`) |
| VPS3 | `10.99.0.3` | Postgres (prod) + Gitea + registry | — |
| VPS4 | `10.99.0.4` | **Dev + Builder** (código; runner CI; `pgdev`) | rootless |
- Repo (fonte da verdade): `ssh://git@10.99.0.3/ruicesar/scoreodonto.com.git`.
- Project Compose: `scoreodontocom`. Diretório nas duas máquinas: `/home/deploy/stack/scoreodonto.com`.
---
## 4. De onde vem a versão (contrato `/api/version`)
```json
{ "name": "ScoreOdonto API", "version": "v1.0.15", "commit": "f2eb90c",
"build": "2026-06-17 03:37 UTC", "environment": "PROD" }
```
- **commit / build** = carimbados no **build** da imagem (imutáveis — identidade do artefato). ✅
- **version / environment** = **injetados no deploy em runtime** (a partir da tag) via `environment` do `compose.prod` ✅ (Passo 3, em prod) — a mesma imagem pode ser promovida sob qualquer tag sem rebuild.
- **Frontend** = lê tudo de `/api/version` via `useAppVersion()`; **não embute mais a versão** ✅ (Passos 1/4, em prod). Fonte única = backend.
---
## 5. Runbook — subir um release (fluxo ATUAL ✅)
> ⚠️ **Regra de ouro:** confirme **DEV ou PRODUÇÃO** antes de agir. `dev.scoreodonto.com` = VPS4 (banco `pgdev`, descartável). `scoreodonto.com` = VPS1 (real). **Nunca** `git push --force` na `main`. Push/tag exigem autorização explícita.
```bash
# 1) VPS4: validar em DEV (build local do container de dev)
cd /home/deploy/stack/scoreodonto.com
node --check backend/server.js
docker compose build scoreodonto-backend scoreodonto-frontend
docker compose up -d --force-recreate scoreodonto-backend scoreodonto-frontend
# conferir em https://dev.scoreodonto.com
# 2) ⚠️ transitório (até Passo 5): a TAG precisa casar com frontend/constants.ts
node update-version.js # bump (ex. V1.0.14 → V1.0.15)
# 3) commit + push → CI builda e publica :<commit> + :<versão> + :latest (NÃO deploya)
git add -A && git commit -m "feat: <descrição> (vX.Y.Z)"
git push origin main
# 4) tag = "aprovado p/ produção" → CI (hoje REBUILDA ✅) promove + deploya na VPS1
git tag v1.0.15 && git push origin v1.0.15
# 5) verificar SEM SSH:
curl -s https://scoreodonto.com/api/version # → version/commit/environment
```
**Backup do banco de PRODUÇÃO antes de releases sensíveis** (migrações rodam no boot; aditivas/idempotentes):
```bash
ssh deploy@10.99.0.3 'pg_dump -Fc scoreodonto -f ~/backups/scoreodonto_$(date +%Y%m%d_%H%M).dump'
```
> ### ⚠️ Fase transitória — por que `APP_VERSION`/`constants.ts` ainda existem
> Estamos entre o **modelo antigo** (versão escrita à mão em `constants.ts`) e o **novo**
> (identidade pelo **commit**; tags só promovem). Até o **Passo 5**, o CI ainda lê `APP_VERSION`
> para nomear a imagem → o bump manual continua **necessário para evitar colisão de tags no
> registry** (republicar `:vX.Y.Z` com outro código quebraria *"uma tag = um artefato"*).
> **Após o Passo 5:** `constants.ts` e `update-version.js` são removidos, a identidade passa a
> ser o commit, e tags apenas **promovem** artefatos já construídos.
---
## 6. Rollback ✅
Reverter o **código** é seguro (migrações só adicionam). Por imagem, **sem rebuild**:
```bash
ssh deploy@10.99.0.1
/home/deploy/stack/scoreodonto.com/deploy-prod.sh v1.0.13 # tag anterior → pull + up --no-build
```
Restaurar o dump do banco só em caso extremo (`pg_restore --clean`).
---
## 7. Passo 5 — "tag promove, não builda" 🎯
Desenho **aprovado (17/jun/2026), não aplicado**. Refina o CI existente para Build Once → Promote → Deploy:
- `build.yml` → 2 jobs por ref: **build** (só push; publica `:<commit>`+`:latest`, sem `APP_VERSION`); **promote** (só tag; re-tag `:<commit>→:vX.Y.Z` via `docker buildx imagetools create`, sem rebuild) → deploy.
- **Validação de integridade antes do deploy:** existe `backend:<C>` e `frontend:<C>`? digests válidos? alias `:vX.Y.Z` == digest de `:<C>`? front e back do **mesmo commit**? Só então deploy (evita deploy parcial).
- Decisões fechadas: manter **`:<commit>`** (sem prefixo); **falhar** se o commit não foi pushado (nunca buildar na tag); **`:latest`** só conveniência.
- Eliminar `update-version.js` + `frontend/constants.ts` (a versão passa a nascer da **tag git**).
- Follow-ups: `GET /api/health`; **política de retenção** do registry.
Racional completo em `deploy.md` e na memória `project-cicd-tag-promove-nao-rebuilda`.
---
> **Sequência acordada para fechar o drift:** (1) doc honesta com status ✅/🧪/🎯 [este doc] → (2) commitar Passos 1/3/4 → (3) promover → (4) confirmar `/api/version` → (5) elevar `VERDADE-HOJE.md` a doc principal (com esta convenção e precedência) → (6) Passo 5 → (7) atualizar docs. À medida que cada item 🧪 for promovido, vira ✅ aqui.
+89
View File
@@ -0,0 +1,89 @@
# Registry de imagens — Container Registry do Gitea
> **Estado: ATIVO e validado** (16/jun/2026, Etapa 4). Não há serviço novo a manter: usamos o **Container Registry embutido do Gitea** (v1.26.1) na VPS3.
---
## 1. O que é
O Gitea já expõe um **registry OCI/Docker** nativo. Não foi preciso instalar `registry:2` nem tocar em `daemon.json`/`insecure-registries`. As imagens do ScoreOdonto ficam versionadas junto do código, no mesmo Gitea que é a fonte da verdade.
| Item | Valor |
|------|-------|
| Endereço (push/pull) | **`git.clube67.com`** (HTTPS, TLS válido via Traefik) |
| Namespace | `ruicesar/` (dono do repo no Gitea) |
| Alternativa interna | `10.99.0.3:3000`**NÃO usar**: HTTP exige `insecure-registries` + restart do Docker (derruba produção) |
| Auth | usuário Gitea + **token** com escopo `write:package` / `read:package` |
> Usamos sempre o domínio **HTTPS** justamente para **não** precisar reconfigurar nem reiniciar o Docker em VPS4/VPS1 (Live Restore está desligado → restart bouncearia todos os containers).
---
## 2. Login (uma vez por máquina)
Em cada máquina que vai dar push/pull (VPS4 builda; VPS1 consome):
```bash
docker login git.clube67.com -u <usuario_gitea>
# senha = TOKEN do Gitea (Configurações → Aplicações → Gerar Token, escopos write:package + read:package)
```
A credencial fica no keystore do Docker (`~/.docker/config.json`). **Nunca** colocar token em código, doc, script ou log. Se um token vazar (ex.: colado em chat), **revogue no Gitea** e gere outro.
---
## 3. Convenção de nomes
```
git.clube67.com/ruicesar/scoreodonto-backend:<versão>
git.clube67.com/ruicesar/scoreodonto-frontend:<versão>
```
- `<versão>` = semver com o mesmo valor de `APP_VERSION` (ex.: `v1.0.13`).
- Mantém-se também a tag móvel `:latest` apontando para a última.
- A tag semântica é o que a **Etapa 6 (deploy por tag)** usa para promover à produção.
---
## 4. Build + push (fluxo manual de hoje, na VPS4)
```bash
cd /home/deploy/stack/scoreodonto.com
export APP_VERSION=$(grep -oE 'V[0-9]+\.[0-9]+\.[0-9]+' frontend/constants.ts | head -1)
export GIT_COMMIT=$(git rev-parse --short HEAD)
export BUILD_TIME=$(date -u +'%Y-%m-%d %H:%M UTC')
export APP_ENV=PROD # ou DEV para imagens de teste
docker compose build scoreodonto-backend scoreodonto-frontend
V=$(echo "$APP_VERSION" | tr 'V' 'v') # V1.0.13 -> v1.0.13
docker tag scoreodontocom-scoreodonto-backend:latest git.clube67.com/ruicesar/scoreodonto-backend:$V
docker tag scoreodontocom-scoreodonto-frontend:latest git.clube67.com/ruicesar/scoreodonto-frontend:$V
docker push git.clube67.com/ruicesar/scoreodonto-backend:$V
docker push git.clube67.com/ruicesar/scoreodonto-frontend:$V
```
> ⚠️ **HTTP 499 em camada grande = timeout transitório do proxy.** O push é resumível: basta **repetir o `docker push`** que as camadas já enviadas são puladas e a faltante completa. (Visto e contornado na validação inicial.)
---
## 5. Pull (na VPS1, quando a Etapa 6 estiver pronta)
```bash
export DOCKER_HOST=unix:///run/user/1000/docker.sock
docker pull git.clube67.com/ruicesar/scoreodonto-backend:v1.0.13
docker pull git.clube67.com/ruicesar/scoreodonto-frontend:v1.0.13
# depois: compose apontando para essas imagens + docker compose up -d
```
Rollback = `docker pull` da tag anterior + `up -d`. Sem rebuild.
---
## 6. Validação feita (16/jun/2026)
- `docker login git.clube67.com` (usuário `ruicesar`) → **Login Succeeded**.
- Push de `scoreodonto-backend:v1.0.13`/`:latest` e `scoreodonto-frontend:v1.0.13`/`:latest` → OK (com 1 retry por 499 transitório).
- Teste de pull (remover tag local + `docker pull`) → **Downloaded** OK.
Próximos: **Etapa 5** (automatizar build+push no Gitea Actions) e **Etapa 6** (deploy por tag na VPS1 consumindo este registry).
@@ -0,0 +1,233 @@
# Arquitetura RX — scoreodonto.com → rx.scoreodonto.com → clientWindows.exe → Wasabi
> Documento de **revisão / engenharia reversa** (gerado em 2026-06-12, sem alterar código).
> Fonte da verdade: `rx.scoreodonto.com/dental-server` (servidor) lido linha a linha.
> O `.exe` do client Windows está em repo separado (Gitea) — aqui seu comportamento é
> descrito pelo **protocolo** que o servidor expõe/aceita (Socket.IO + `machine_token`).
---
## 1. Visão geral (quem é quem)
```
┌──────────────────────────┐ ┌───────────────────────────────────────────┐
│ CLÍNICA (PC com sensor) │ │ rx.scoreodonto.com (dental-server) │
│ │ │ Node + Express + Socket.IO │
│ clientWindows.exe │ WSS │ │
│ - captura RX do sensor │────────▶│ io.on('send-image') │
│ - auth: machine_token │ socket │ → storage.saveImage() │
│ - emite 'send-image' │ .io │ → INSERT images (Postgres dental_images) │
└──────────────────────────┘ │ │
│ HTTP API (/api/images, /api/admin/...) │
┌──────────────────────────┐ HTTPS │ - lista pacientes/imagens │
│ scoreodonto.com (plugin)│────────▶│ - cria devices (/api/admin/clinics) │
│ - superadmin: config │ proxy │ │
│ global (rx_url+token) │ no nosso│ │ saveImage / getImageBuffer │
│ - backend proxeia leitura│ backend │ ▼ │
└──────────────────────────┘ │ ┌─────────────────────────────────────┐ │
│ │ WASABI (S3) bucket │ │
│ │ key: clinic/client/patient/file.png │ │
│ └─────────────────────────────────────┘ │
│ (+ cache local em ./uploads como fallback)│
└───────────────────────────────────────────┘
```
Quatro peças:
1. **clientWindows.exe** — instalado no PC da clínica; captura a radiografia e **envia via Socket.IO**.
2. **rx.scoreodonto.com / dental-server** — recebe, persiste metadados no Postgres e o binário no Wasabi; serve listagens e imagens.
3. **Wasabi** — storage S3-compatível (objeto = `clinic/client/patient/arquivo`).
4. **scoreodonto.com (plugin RX)** — administra/visualiza via API, com token global (nosso trabalho recente).
---
## 2. Banco de dados do RX (Postgres `dental_images`, em `database.js`)
| Tabela | Campos-chave | Papel |
|---|---|---|
| **clinics_devices** | `clinic_name`, `email` (UNIQUE), `password_hash`, `pc_name`, `machine_token` (UUID UNIQUE), `is_active`, `last_ip` | **1 linha por (clínica, PC)**. Credencial do client Windows. Criada por `POST /api/admin/clinics`. |
| **images** | `image_guid`, `patient_name`, `client_name` (=PC), `clinic_name`, `filename`, `thumb_filename`, `doctor`, `remark`, `enabled`, `created_at` | 1 linha por radiografia. **Filtros de leitura hoje: `client_name` e `patient_name`** (e agora `clinic_name`, ver §7). |
| **users** | `username`, `email`, `password_hash`, `is_admin` | Usuários admin/web (painel). |
| **api_tokens** | `name`, `token` | **Tokens estáticos de API** (viram admin, não expiram). |
| **system_config** | `key`, `value` | Key-value. Guarda `wasabi_config`. |
| **gtos / gto_images** | `gto_number`, `patient_name`, … | Documentos GTO por paciente. |
---
## 3. clientWindows.exe — como envia os RX (protocolo)
O envio NÃO é HTTP de upload; é **Socket.IO** (`server.js`):
1. **Conexão / auth do socket** (`io.use(...)`, server.js:823):
- O client conecta com `handshake.auth.token = machine_token` (UUID gerado no cadastro do device).
- Regex confirma UUID → busca em `clinics_devices` → se `is_active`, autoriza.
- Opcional: se `email`+`password` também vierem, são validados (compat).
- Resultado: `socket.user = { role:'client_device', clinicId, clinicName, pcName }`
**vindos do registro do device** (não do client). Atualiza `last_ip`.
- Também aceita `CLIENT_API_KEY` (sensor legado) e JWT (painel web / app novo).
2. **Envio da imagem — DIRECT UPLOAD pro Wasabi (fluxo REAL de produção)**:
Confirmado pelos logs do client (`🌐 Fazendo Direct Upload pro Wasabi (Bypass VPS)`).
O binário **NÃO** trafega pela VPS — vai direto pro Wasabi via URL pré-assinada:
1. Client monitora `C:\ProgramData\RF\Dental Sensor\Images` (Chokidar) e gera thumb local.
2. `socket.emit('request-presigned-urls', { metadata:{guid}, patientData:{name,...} })`
→ servidor responde `{ originalUrl, thumbnailUrl, filename, thumbFilename }`
(`storage.getUploadPresignedUrl`, server.js:1218). **A key do Wasabi já é fixada aqui.**
3. Client faz **PUT direto no Wasabi** (original + thumb) usando as presigned URLs.
4. `socket.emit('image-upload-direct', {...})` → servidor só **grava os metadados** em
`images` (server.js:1258). Não recebe os bytes.
- Em ambos (2 e 4) o eixo de clínica é o MESMO:
`clinicName = socket.user?.clinicName || clientInfo?.clinicName || 'Clínica não identificada'`
→ vem do **`clinic_name` do device** (machine_token). `clientName` = PC; `patientName` = paciente.
> ⚙️ Existe também o caminho legado `socket.on('send-image')` (base64 pela VPS, server.js:1031)
> e `POST /api/images/upload` (upload manual). Mas o client Windows real usa o **Direct Upload**.
> Há ainda `POST /api/client/login` (JWT 2h `client_device`) para fluxos HTTP.
3.1. **⚠️ Estado real dos dados (2026-06-12, banco compartilhado dev+prod):**
- **1 device**: `clinic_name="Consultt Clinic"`, `pc="RX"`.
- `images`: **2552 imgs / 503 pacientes com `clinic_name = NULL`** (subiram antes do clinic_name
ser setado → Wasabi key `desconhecido/RX/paciente/…`) + **222 imgs / 37 pacientes** sob
`"Consultt Clinic"`. → A maioria das radiografias está "órfã" de clínica.
---
## 4. Wasabi storage (`storage.js`) — a lógica que NÃO pode quebrar
**Config**: lida do banco em `system_config.wasabi_config`
(`{ wasabiAccessKey, wasabiSecretKey, wasabiBucket, wasabiRegion, wasabiEndpoint }`),
carregada por `loadConfigFromDb()` (server.js:1428, e ao salvar em `routes/system.js`).
SDK: `@aws-sdk/client-s3` com `forcePathStyle:true`, endpoint `s3.wasabisys.com`.
**Chave do objeto (CRÍTICO):**
```
key = sanitize(clinic_name) / sanitize(client_name) / sanitize(patient_name) / filename
```
`sanitizePathSegment` troca `/\?#%*:"<>|` por `_`. (storage.js:103, 120, 153, 220, 298, 340, 366, 414)
**saveImage** (storage.js:139):
1. grava local em `./uploads/<filename>`;
2. se Wasabi ativo → `Upload` (lib-storage) para a key acima;
3. se subiu OK → **apaga o local** (Wasabi vira fonte). Se falhar → mantém local (fallback).
**getImageBuffer** (storage.js:190): local primeiro → senão Wasabi (key vinda dos args
ou de um `SELECT` em `images` por `filename`/`thumb_filename`) → fallback key na raiz do bucket
(arquivos legados) → cacheia local. `getImageUrl`/`getDownloadPresignedUrl` geram **URL assinada (1h)**.
**deleteImage**: apaga local (uploads + processed) e a key no Wasabi (+ key raiz, redundância legada).
> Wasabi é **opcional**: sem config, tudo funciona só em disco local. Por isso "desligar/errar"
> a config do Wasabi não derruba o servidor — só muda onde o binário vive.
---
## 5. Autenticação — matriz completa
| Quem | Como autentica | Onde |
|---|---|---|
| **client Windows (socket)** | `machine_token` (UUID) — suficiente; email+senha opcionais | `io.use` server.js:823 |
| **client Windows (HTTP)** | `POST /api/client/login` → JWT 2h `role:client_device` | client-auth.js |
| **sensor legado** | `CLIENT_API_KEY` (`rf-dental-secure-key-2026` default) | io.use / `x-api-key` |
| **admin / painel web** | usuário+senha → JWT 7d (`is_admin`) **OU** `api_token` estático | auth.js `authenticateToken` |
| **público** | sem auth | só `GET /api/images/:id/thumbnail` |
`authenticateToken` (auth.js:12): tenta JWT; se falhar, procura o token em `api_tokens`
se achar, injeta um **admin mock** (`is_admin:true`). É isso que o **scoreodonto usa**
(token global) para chamar `/api/admin/clinics` e as listagens.
---
## 6. Integração scoreodonto.com (plugin RX) — estado atual
- **Config GLOBAL** (só superadmin) em `settings.rx_config_global = { rx_url, api_token }`
no backend do scoreodonto. O `api_token` é um **token de `api_tokens` do RX** (admin, não expira).
- **Criar PC** (`POST /api/rx/devices` no nosso backend) → `POST {rx_url}/api/admin/clinics`
com **`clinic_name = clinicaId`** (a clínica do scoreodonto) + `pc_name` + email/senha por PC.
Devolve o `machine_token` que vai no client Windows.
- **Ler** (`GET /api/rx/patients`, `/api/rx/images`) → proxeia `/api/images/patients` e
`/api/images/by-patient` com `?clinic=<clinicaId>` + Bearer `api_token`. Isolamento por vínculo.
- **Thumbnails**: públicos, carregados direto de `rx_url`.
---
## 7. ⚠️ Pontos de atenção para NÃO quebrar o que já funciona
1. **`clinic_name` é o eixo de tudo** (Wasabi key + filtro de listagem). Os clients Windows
**já em produção** sobem imagens com o `clinic_name` que foi gravado **no cadastro do device deles**
(provavelmente o nome legível da clínica).
2. **Descompasso scoreodonto × RX:** o plugin novo cria device com `clinic_name = clinicaId`
(ID opaco do scoreodonto). Logo:
- Imagens **antigas** estão sob `clinic_name = <nome legível>` no Wasabi e no `images`.
- Imagens **novas** (via plugin) ficariam sob `clinic_name = <clinicaId>`.
- Filtrar por `?clinic=<clinicaId>` **não acha as antigas** (clinic_name diferente) — mas
**não quebra** o envio existente (cada client continua com o seu clinic_name).
- → Para enxergar clínicas já ativas, é preciso um **mapeamento clinicaId ↔ clinic_name atual**
(tabela/coluna de-para), NÃO renomear objetos no Wasabi (arriscado e custoso).
3. **Hierarquia desejada (4 níveis) × Wasabi (3 níveis):**
- Desejado: `scoreodonto / consulttclinic@gmail.com / clinicaID / paciente`.
- Wasabi real: `clinic_name / client_name / paciente / arquivo` (3 níveis, sem o nível "conta").
- A "conta-mestre" (email) é **identidade de auth** (api_token/users.email), **não faz parte da key**.
- Realizar 4 níveis exigiria mudar o esquema de key — **risco alto para objetos já gravados**.
Caminho seguro: manter 3 níveis e usar `clinic_name = clinicaId` só para o que for novo,
com de-para para o legado.
4. **`npm run dev` do RX = `node server.js` (sem nodemon)** → mudanças no dental-server exigem
`docker restart rx_dev_api`. (Em prod o deploy é via `deploy-prod.sh` / webhook.)
5. **Não tocar**: `io.on('send-image')`, `storage.saveImage/getImageBuffer`, o esquema da key,
nem o cadastro de `clinics_devices` dos PCs que já enviam. Toda evolução deve ser **aditiva**
(novos endpoints/colunas), preservando o caminho do client Windows.
---
## 8. Endpoints-chave (referência rápida)
| Endpoint | Auth | Uso |
|---|---|---|
| `WSS send-image` | machine_token | client Windows envia RX |
| `POST /api/admin/clinics` | admin/api_token | cria device (clinic_name, email, pc_name) → machine_token |
| `GET /api/admin/clinics` | admin/api_token | lista devices |
| `GET /api/images?clinic=&client=&search=` | admin | lista imagens (filtros) |
| `GET /api/images/patients?clinic=&search=&page=` | admin | pacientes agrupados |
| `GET /api/images/by-patient?name=&clinic=` | admin | imagens de um paciente |
| `GET /api/images/:id/thumbnail` | **público** | miniatura |
| `GET /api/version` · `/download-client` | público | versão / instalador do client |
| `POST /api/auth/login` → JWT · `POST /api/auth/tokens` → api_token | admin | obter credenciais de API |
---
## 9. Multi-tenant 2 níveis: `tenant_id` / `clinica_id` (implementado 2026-06-12, DEV)
**Hierarquia** (espelha o scoreodonto, que tem conta multi-clínica no plano):
`tenant_id (conta/dono = usuarios.id "u_…") / clinica_id (clinicas.id "c_…") / pc / paciente / arquivo`.
O `tenant_id` é resolvido pelo **vínculo `dono*`** da clínica (owner_id está vazio hoje).
Para a Consultt: `tenant_id=u_1780870994865_3876n` (Rui Cesar) / `clinica_id=c_1780871001549_koqa3o`.
- Colunas `tenant_id` **e** `clinica_id` em `clinics_devices` e `images` (+índices); `clinic_name`
vira rótulo. Backfill: device + **2776 imgs** → tenant+clínica da Consultt.
- Verificado: `?tenant_id=` (conta) e `?clinica_id=` (clínica) isolam; tenant/clínica inexistente → 0.
- scoreodonto: cadastro de device resolve+envia `tenant_id`; proxy de leitura libera **conta**
(`tenant_id` = própria conta do dono) ou **clínica** (`clinica_id` com vínculo).
(Resto desta seção descreve a base por `clinica_id`.)
- **Schema RX**: coluna `clinica_id` em `clinics_devices` e `images` (+ índices). `clinic_name`
vira só rótulo legível. Backfill: device + **2776 imgs** existentes → `c_1780871001549_koqa3o`.
- **Fluxo**: `POST /api/admin/clinics` grava `clinica_id`; o socket (machine_token) resolve
`socket.user.clinicaId` do device; `image-upload-direct` (e o legado `send-image`) gravam
`images.clinica_id`. **Wasabi key ainda é `clinic_name/…`** (mudança física = fase de
reconciliação, pendente da resposta sobre os PNGs de origem).
- **Isolamento**: listagens (`/api/images`, `/patients`, `/by-patient`, `/unique-clinics`)
filtram por `?clinica_id=`. Verificado: tenant real retorna dados, tenant inexistente → 0.
- **scoreodonto**: `/api/rx/*` envia/filtra por `clinica_id` (e `clinic_name`=nome_fantasia no
cadastro); proxy de leitura valida vínculo (`_rxClinicScopeOk`) → 403 fora do tenant.
- **Pendente**: reconciliação física no Wasabi (`clinica_id/…`) + remoção do `send-image`
legado (após confirmar que nenhum `.exe` em campo usa versão pré-Direct-Upload).
### TL;DR
O client Windows manda RX por **Socket.IO autenticado por `machine_token`**; o servidor grava
metadados no Postgres e o arquivo no **Wasabi sob `clinic_name/client_name/patient_name/arquivo`**.
O scoreodonto administra isso com um **token de API global**, filtrando por `clinic_name = clinicaId`.
O risco de integração está em **`clinic_name`**: alinhar o ID do scoreodonto com o `clinic_name`
que os PCs já usam, sem renomear nada no Wasabi nem mexer no caminho de envio.
@@ -0,0 +1,93 @@
# RX — Backlog de robustez, segurança e limpeza
> Tarefas derivadas da revisão de 2026-06-12 (multi-tenant + client.exe + storage).
> Prioridade: **P1** (faria primeiro) · **P2** · **P3**. Marcar `[x]` ao concluir.
> Regra do projeto: nada vai pra prod sem autorização; dev.rx e prod.rx **compartilham
> DB+Wasabi** (ver T3.1 — separar é o que mais reduz risco).
---
## 3. Robustez do RX / resync
- [ ] **T3.1 (P1) — Separar DEV de PROD (DB + Wasabi).** Hoje compartilham o mesmo
Postgres (`dental_images`) e o mesmo bucket → testes mexem em dado real. Opções:
banco `dental_images_dev` e/ou prefixo/bucket Wasabi por ambiente (`RX_ENV`).
- [x] **T3.2 (P1) — Healthcheck de reconciliação Wasabi↔banco** por `clinica_id`/`tenant_id`.
FEITO (dev): `GET /api/admin/rx-health` (RX) — contagens por (tenant,clínica) + cron +
`?clinica_id=&checkLimit=N` (HEAD-check de ausentes). Proxy `GET /api/rx/health`
(scoreodonto, superadmin). Validado: Consultt 2723 imgs/513 pac, 0 ausentes em 15. Commit 016305e.
- [ ] **T3.3 (P2) — Índice único parcial** `images(original_filename) WHERE enabled=1`
como trava de duplicata no banco. ⚠️ Criar CONCURRENTLY e fora do boot (se houver
duplicata, a criação no boot derrubaria o servidor).
- [ ] **T3.4 (P2) — Remover `send-image` legado** após confirmar que todo `.exe` em campo
é ≥ Direct Upload (1.3.x). Elimina um caminho de upload + base64 pela VPS.
- [ ] **T3.5 (P2) — Throttle/progresso no resync.** `trigger-sync` reescaneia todos os PNGs;
em clínica grande é pesado → lote + reporte de progresso.
- [ ] **T3.6 (P3) — `check-image-exists`**: garantir que checa linha no banco **E** objeto no
Wasabi (senão objeto sumido nunca é reenviado).
## 4. Client.exe
- [ ] **T4.1 (P1) — Code signing do instalador NSIS** (evita SmartScreen nas clínicas).
- [ ] **T4.2 (P1) — `machine_token` em repouso**: proteger no config local (DPAPI/cripto).
- [ ] **T4.3 (P2) — Retry/backoff + fila persistente** nos PUT do Wasabi e na `uploadQueue`
(sobreviver a restart / internet ruim).
- [ ] **T4.4 (P2) — Telemetria pro `/clients`**: client envia `last_sync`, profundidade da
fila e últimos erros → página mostra saúde real.
- [ ] **T4.5 (P2) — `lookupPatientInfo` tolerante a versão** do banco do sensor RF.
- [ ] **T4.6 (P3) — Pipeline de auto-update**: automatizar envio de `Setup x.y.z.exe` +
`latest.yml` pro `updates/` do prod ao rebuildar.
## 5. Segurança / hardening
- [ ] **T5.1 (P1) — `api_token` global criptografado em repouso** (hoje texto puro em
`settings.rx_config_global`) + rotação + idealmente token de escopo reduzido (não-god).
- [x] **T5.2 (P1) — Auditar escopo GLOBAL do `tenantGuard`** — FEITO (dev). 🔴 Achado crítico:
a família `/api/sync/*` (sync GLOBAL legado) usava `tenantGuard`**burlável sem clinicaId**;
`DELETE /api/sync/truncate-all` fazia `TRUNCATE pacientes,agendamentos,financeiro,leads,guias`
de **todas as clínicas** → qualquer usuário logado (até funcionário) apagava o banco inteiro.
**Corrigido**: os 13 endpoints `/api/sync/*``superadminGuard`. Provado: não-superadmin
agora recebe **403** (antes truncava). 2º achado: **`/api/settings/google-credentials`** GET+POST
(credenciais OAuth GLOBAIS da plataforma) também era `tenantGuard` → qualquer logado sobrescrevia →
**`superadminGuard`** (403 p/ não-super; front só chama p/ superadmin). **Auditoria completa**:
todos os demais `tenantGuard` (clinica-sync, dashboard, agendamentos, avaliacoes, financeiro,
guias, leads/:id/converter, DELETE :id, rx/devices) exigem `req.clinicaId` + `WHERE clinica_id` → seguros.
- [ ] **T5.3 (P1) — Auth do `remote-crud-patient`** (socket escreve no banco local do client):
validar papel/escopo forte no servidor.
- [ ] **T5.4 (P2) — Defesa em profundidade no RX server**: validar tenant nos endpoints admin
(não confiar só no proxy do scoreodonto).
- [ ] **T5.5 (P2) — Revisar os 9 dias de WIP** (`auth.js`, `database.js`, `storage.js`, páginas)
antes do prod.
- [!] **T5.6 (P3→P0) — Auditar histórico do git por segredos** — FEITO. 🔴🔴🔴 ACHADOS CRÍTICOS:
- `dental-server/bkp/docs/ENV-CONFIG.txt` tem o **`JWT_SECRET` REAL** (confirmado == o do
servidor) commitado → **qualquer um com acesso ao repo forja JWT de admin do RX**.
- `INSTRUCOES-SERVIDOR-VPS.txt` tem o **`DB_PASSWORD`/`REDIS_PASSWORD` reais** em texto puro.
AÇÃO OBRIGATÓRIA (decisão do usuário, não automatizei):
1. **Rotacionar JWT_SECRET** (invalida tokens, força re-login) — fecha a forja.
2. **Rotacionar senha do Postgres/Redis** (10.99.0.3).
3. Remover os segredos dos arquivos versionados + **scrub do histórico** (git filter-repo/BFG).
`.secrets` já está no .gitignore (feito antes); isso NÃO resolve os já commitados → rotação é mandatória.
## 6. Limpeza / tech-debt
- [ ] **T6.1 (P1) — Consolidar os 4 caminhos de upload** (`image-upload-direct`, `send-image`,
`/api/images/upload`, `/upload-file`) → manter 12, remover o resto.
- [ ] **T6.2 (P2) — Simplificar `storage.js` pós-resync** (remover fallback legado quando todos
os objetos estiverem na key nova).
- [ ] **T6.3 (P2) — Remover diretórios mortos** (`bkp/`, `public_legacy/`, `old-root-files/`).
- [ ] **T6.4 (P3) — Upgrade Node 20 → 22** (AWS SDK exigirá ≥22 em 2027).
- [ ] **T6.5 (P3) — Log levels** (info/debug/error) no servidor RX.
- [ ] **T6.6 (P3) — Passada final de comentários/padronização** no fluxo RX.
---
### Log
- **2026-06-12** — T3.2 concluída (healthcheck de reconciliação, read-only).
- **2026-06-12** — T5.2 concluída. 🔴 Fechado RCE-like: `/api/sync/truncate-all` (e família) +
`/api/settings/google-credentials` deixavam qualquer logado truncar o banco / trocar OAuth global →
agora `superadminGuard` (403 p/ não-super). Mudanças no **scoreodonto (dev), não commitadas**.
- **2026-06-12** — T3.6 auditada: `check-image-exists` já está correto (checa banco+Wasabi, apaga órfão). Sem fix.
- **2026-06-12** — T5.6 🔴🔴🔴: JWT_SECRET REAL + senha de DB/Redis commitados (ENV-CONFIG.txt,
EXEMPLO-ENV.txt, INSTRUCOES-SERVIDOR-VPS.txt, test-db2.js). **Passo 3 FEITO** (redigidos do working
tree, commits 4616f20+2b679a5). **PENDENTE/MANDATÓRIO (usuário): rotacionar JWT_SECRET + senha
Postgres/Redis + scrub do histórico** — até lá, a exposição persiste no histórico do Gitea.
+42
View File
@@ -0,0 +1,42 @@
# Qual é a verdade hoje? — Auditoria de infra ScoreOdonto
> Resposta direta às 6 perguntas essenciais, **verificada em 17/jun/2026**. Somente fatos do estado atual.
> **Documento principal: em caso de divergência entre docs, este prevalece.**
> Como operar → `DEPLOY-PRODUCAO.md` (runbook). Para onde vamos → `deploy.md` (arquitetura-alvo).
---
### 1. Onde está o código?
- **Fonte da verdade:** Gitea na **VPS3**, repo `ruicesar/scoreodonto.com`.
- **Dev (trabalho):** VPS4 em `/home/deploy/stack/scoreodonto.com`.
- **Produção:** VPS1 roda **imagens versionadas do registry** (não código-fonte). No ar: **v1.0.15 · commit `f2eb90c`**.
- **Documentação:** centralizada em `scoreodonto.com/documentação/`.
### 2. Onde está o banco?
- **Produção:** PostgreSQL no host da **VPS3** (`10.99.0.3:5432`), banco `scoreodonto`, user `scoreodonto_user`.
-**DEV ISOLADO:** a VPS4 usa um container PostGIS próprio **`pgdev`** (banco `scoreodonto_dev`), **NÃO mais o banco de produção**. Dev e prod **não compartilham** mais. (Ver `BANCO-DEV-ESTRATEGIA.md`.)
- Cache **DragonflyDB** (`10.99.0.3:6379`, índice `/1`). **NATS** (`:4222`) e **Temporal** (`:7233`).
### 3. Onde estão os containers?
- **Produção (VPS1, docker rootless):** `scoreodontocom-scoreodonto-{frontend,backend}-1` rodam a **imagem do registry** (`git.clube67.com/ruicesar/scoreodonto-*:<tag>`). Produção **não builda**. Também na VPS1: `traefik` (borda TLS), rx, newwhats, subdominio, mercado.
- **Dev (VPS4, docker rootless):** `nginx-1` (`8020→80`, alvo do Traefik para `dev.`), `frontend-1` (`:3013`), `backend-1` (`:8018`) — **buildados localmente** e apontados ao `pgdev`.
- **Roteamento:** Traefik na VPS1 → `scoreodonto.com` (containers locais) e `dev.scoreodonto.com``http://10.99.0.4:8020`.
### 4. Quem faz deploy?
- **CI/CD por tag (Gitea Actions).** Push na `main` → o runner `act_runner` "vps4-builder" (VPS4, rootless) builda e **publica no registry** (NÃO deploya). `git tag vX.Y.Z && git push` → CI + job `deploy` promove para a VPS1 (`deploy-prod.sh`).
- **Produção consome imagem** (`pull` + `up -d --no-build`).
- ⚠️ Hoje a **tag rebuilda** antes de promover; o **Passo 5** mudará para a tag apenas **re-taguear** o artefato já construído por commit.
### 5. Como versiona?
- A versão exibida vem do **backend em runtime**: `GET /api/version` (de `process.env`), mostrada em **Configurações → "Sobre o sistema"** e nas telas públicas (o frontend consome via `useAppVersion()`, não embute mais a versão). No ar: **v1.0.15 · `f2eb90c` · PROD**.
- No deploy, `APP_VERSION` (= tag) e `APP_ENV=PROD` são **injetados em runtime**; `commit`/`build` vêm carimbados na imagem.
- ⚠️ **Legado transitório:** `frontend/constants.ts` (`APP_VERSION`) + `update-version.js` ainda existem **só** para o CI nomear a imagem (bump manual evita colisão de tag). Serão **eliminados no Passo 5** (a versão nascerá da tag git).
### 6. Como volta uma versão (rollback)?
- **Por imagem, sem rebuild:** na VPS1, `./deploy-prod.sh <ref-anterior>``pull` + `up -d --no-build`.
- ⚠️ A referência confiável é o **commit** (`:<sha>`): tags `:vX.Y.Z` podem ter sido **republicadas** com código diferente (colisão de tags pré-Passo 5 — confirmado: `:v1.0.14` do registry ≠ o que rodava). Migrações são **aditivas** (voltar o código é seguro). Dump em `/home/deploy/backups/` só em caso extremo.
---
## Veredito de uma linha
> O código vive no Gitea (VPS3); a **produção (VPS1) roda imagens versionadas do registry** (hoje **v1.0.15**), sem buildar; o **DEV está isolado no `pgdev`** (não toca o banco de produção). **CI/CD por tag ativo**, com **versão visível em runtime** (`/api/version` → Configurações). Pendência técnica principal: **Passo 5** (tag promove sem rebuild; eliminar o `APP_VERSION` manual).