padronizacao: foundation Fase 0+0.5 — blueprints + auditoria + clinical_notes
Pre-MVP: 3 blueprints canonicos (repository, composable, quick-create overlay), AUDIT_BASELINE com 51 divergencias em 6 modulos, estrategia PADRONIZACAO de 4 fases, DESIGN_BILLING_ORCHESTRATOR. Schema clinical notes pronto pra Fase B (4 migrations + seed templates). AgendaEvent Dialog.vue.bak deletado (lixo de refator anterior). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
# Audit Baseline — 6 Módulos vs Blueprints
|
||||
|
||||
> **Data:** 2026-05-20
|
||||
> **Método:** 6 agentes Explore em paralelo, cada um auditou 1 módulo contra os 3 blueprints (repository, composable, quick-create overlay)
|
||||
> **Saída:** mapa exato do trabalho da Fase 1 da Padronização Sweep
|
||||
|
||||
---
|
||||
|
||||
## Sumário Executivo
|
||||
|
||||
| # | Módulo | Estado | Alta | Média | Baixa | Bloqueador |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | Home / Components | Parcial | 3 | 2 | 2 | — |
|
||||
| 2 | Pacientes | Parcial | 4 | 6 | 2 | — |
|
||||
| 3 | Prontuário | **Embrionário** | 3 | 3 | 0 | Schema clínico ausente |
|
||||
| 4 | Financeiro | **Órfão** | 6 | 3 | 1 | Overlap com agenda + double-billing risk |
|
||||
| 5 | Multi-tenant | Parcial | 2 | 3 | 2 | **Convites/membership inexistem** |
|
||||
| 6 | Notificações | **Embrionário** | 5 | 3 | 1 | 3 canais fragmentados, SMS envio só stub |
|
||||
|
||||
**Totais:** 23 alta · 20 média · 8 baixa = **51 divergências catalogadas** + 4 gaps estruturais.
|
||||
|
||||
---
|
||||
|
||||
## Surpresas (descobertas que mudam o plano)
|
||||
|
||||
### 🚨 1. Convites/membership de tenant — gap apenas no front (CORRIGIDO 2026-05-20)
|
||||
|
||||
Agente Multi-tenant disse: **não existe** repository/composable pra `sendInvite(tenantId, email)`, `acceptInvite(inviteId)`, `listTenantMembers(tenantId)`. **Correção:** a tabela `public.tenant_invites` JÁ EXISTE no schema (`tenants_multi_tenant.sql:100`) com campos completos (id, tenant_id, email, role CHECK ['therapist','secretary'], token, invited_by, expires_at default 7d, accepted_at/by, revoked_at/by). Falta APENAS UI + composables/services no front.
|
||||
|
||||
Recomendação: criar `features/tenantship/` com services + composables + página `/admin/members` usando a tabela existente. Sem migration de schema necessária. Reduz escopo de 0.5.D.
|
||||
|
||||
### 🚨 2. Lógica de billing duplicada agenda ↔ financeiro — risco de double-billing
|
||||
|
||||
`useAgendaFinanceiro.gerarCobrancaManual()` (composables raiz) e `useFinancialRecords.createRecord()` (composables raiz) **chamam a mesma RPC** `create_financial_record_for_session`. Sem coordenação = race condition silenciosa.
|
||||
|
||||
`useAgendaFinanceiro.handleStatusChange()` ainda relê `financial_records` direto via `.select('id').eq('agenda_evento_id', ...)` — query que deveria viver só no useFinancialRecords.
|
||||
|
||||
Recomendação: consolidar em 1 composable orquestrador, ou separar responsabilidades com coordenação explícita via fila.
|
||||
|
||||
### 🚨 3. Quick-create overlay — promotion criteria atingida ANTES da hora
|
||||
|
||||
Blueprint documentei como "agenda-only, promover quando aparecer 2º caso de uso". Agente Home descobriu **3 quick-create candidatos JÁ em produção**, fora da agenda:
|
||||
|
||||
- `src/components/CadastroRapidoMedico.vue` (supabase direto)
|
||||
- `src/components/CadastroRapidoConvenio.vue` (supabase direto)
|
||||
- `src/components/ComponentCadastroRapido.vue` (genérico, supabase direto)
|
||||
|
||||
São o 2º, 3º e 4º casos. **Promover agora** muda o blueprint de "agenda-only" pra universal, e dá fix em 3 componentes ao mesmo tempo.
|
||||
|
||||
### 🚨 4. Prontuário sem schema clínico
|
||||
|
||||
Agente Prontuário: o "Prontuário" hoje é shell de abas que reusa `usePatientSessions`. Schema vazio pra anamnese, evolução clínica, plano terapêutico. **Não dá pra padronizar antes de modelar.**
|
||||
|
||||
Recomendação: adicionar etapa "modelagem schema clínico" como pré-requisito pro módulo 3 da Fase 1.
|
||||
|
||||
### 🟡 5. `error = ref(null)` vs `ref('')` confirmado como divergência sistêmica
|
||||
|
||||
Aparece em pacientes, financeiro, alguns lugares de notificações. Confirma a canonicalização do composable blueprint (`''` default). Fix mecânico, fácil de aplicar.
|
||||
|
||||
### 🟡 6. Setup Wizard tem 8 queries supabase inline
|
||||
|
||||
Já estava no `project_graphify_findings_20260504` ("Setup Wizard cohesion 0.05"). Agora quantificado: 8 queries (linhas 419, 429, 446, 595, 626, 656, 681, 706). Fix: criar `setupRepository.js` + `useSetupWizard.js`.
|
||||
|
||||
---
|
||||
|
||||
## Cross-cutting patterns (não específicos a 1 módulo)
|
||||
|
||||
| Pattern | Onde aparece | Severidade | Fix |
|
||||
|---|---|---|---|
|
||||
| `error = ref(null)` | Pacientes, Financeiro, partes de Notificações | Média | Mecânico: `ref('')` |
|
||||
| `supabase.from(...)` em composable | Pacientes (4 composables), Financeiro (2), Notificações (3+), Multi-tenant (1) | Alta | Extrair pra repository |
|
||||
| SELECT inline em vez de constante | Pacientes (3), Financeiro, Notificações | Média | Extrair pra `<feature>Selects.js` |
|
||||
| UPDATE/DELETE sem `.eq('tenant_id', tid)` | Pacientes (2), Financeiro (3) | Alta | Defesa em profundidade |
|
||||
| `getUid()` / `useTenantStore()` duplicados | Múltiplos composables | Baixa | Helper compartilhado |
|
||||
| State em variável módulo (vaza entre instâncias) | `usePatientFinancial._lastPatientId` | Alta | Mover DENTRO da `function use*()` |
|
||||
| `_tenantGuards.js` ausente em todo módulo não-agenda | Todos | Média | Replicar pattern |
|
||||
|
||||
---
|
||||
|
||||
## Detalhamento por Módulo
|
||||
|
||||
### 1. Home / Components base
|
||||
|
||||
**Estado:** Parcial. Falta camada de repository. 3 quick-creates espalhados fazem supabase direto.
|
||||
|
||||
**Arquivos-chave:**
|
||||
- `src/views/pages/HomeCards.vue` — roteador de perfis + RPC de auditoria interna
|
||||
- `src/layout/melissa/MelissaLayout.vue` — orquestrador (~90 imports, monolítico)
|
||||
- `src/components/CadastroRapidoMedico.vue` — quick-create supabase direto
|
||||
- `src/components/CadastroRapidoConvenio.vue` — quick-create supabase direto
|
||||
- `src/components/ComponentCadastroRapido.vue` — quick-create genérico supabase direto
|
||||
|
||||
**Divergências:**
|
||||
|
||||
| Sev | Tipo | Local | Problema | Fix |
|
||||
|---|---|---|---|---|
|
||||
| ~~Alta~~ ✅ | quick-create | `CadastroRapidoMedico.vue:150` | ~~`supabase.from()` direto sem repository~~ | **RESOLVIDO 2026-05-20 (M1.1):** `features/medicos/services/medicosRepository.js` criado + componente refatorado pra usar `useMedicos` composable |
|
||||
| ~~Alta~~ ✅ | quick-create | `CadastroRapidoConvenio.vue:98` | ~~`supabase.from()` inline~~ | **RESOLVIDO 2026-05-20 (M1.2):** `features/insurance/services/insurancePlansRepository.js` criado + componente usa `useInsurancePlans` composable. Bônus: agenda `InsurancePlanQuickCreateDialog.vue` também migrado. |
|
||||
| ~~Alta~~ ✅ | quick-create | `ComponentCadastroRapido.vue:263` | ~~`insert()` sem validação `tenant_id`+`owner_id`~~ | **RESOLVIDO 2026-05-20 (M1.3):** componente usa `usePatients.create()`; tenant resolvido via `getMyActiveMember()` (helper novo em tenantship); repository injeta `owner_id = auth.uid()` sempre, ignora payload. |
|
||||
| ~~Média~~ ✅ | composable | `CadastroRapidoMedico.vue:49-58` | ~~`getTenantId()` via fallback query em vez de store~~ | **RESOLVIDO 2026-05-20 (M1.1):** removido — repository usa `resolveTenantId()` canônico |
|
||||
| ~~Média~~ ✅ | composable | `CadastroRapidoConvenio.vue:94-100` | ~~`loadPlans()` sem `.eq('tenant_id', tid)`~~ | **RESOLVIDO 2026-05-20 (M1.2):** repository agora filtra por tenant_id + owner_id |
|
||||
| ~~Baixa~~ ✅ | outro | `HomeCards.vue:23-33` | ~~`TEST_ACCOUNTS` hardcoded~~ | **RESOLVIDO 2026-05-20 (M1.4):** extraído pra `src/config/devTestAccounts.js` |
|
||||
| Baixa | outro | `MelissaLayout.vue:1-150` | 90+ imports, monolítico | Refactor Fase 2 (M1.6 — sessão dedicada) |
|
||||
|
||||
### 2. Pacientes
|
||||
|
||||
**Estado:** Parcial. `patientsRepository` é o ÚNICO repo padronizado fora da agenda (8/10 conformidade). Composables têm violações de camada.
|
||||
|
||||
**Arquivos-chave:**
|
||||
- `src/features/patients/services/patientsRepository.js` — referência parcial
|
||||
- `src/features/patients/composables/usePatients.js` — thin wrapper (falha em error type)
|
||||
- `src/features/patients/composables/usePatientDetail.js` — supabase direto
|
||||
- `src/features/patients/composables/usePatientFinancial.js` — supabase direto + estado módulo
|
||||
- `src/features/patients/composables/usePatientSessions.js` — supabase direto
|
||||
- `src/components/ui/PatientCreatePopover.vue` — padrão OK (não é quick-create overlay)
|
||||
|
||||
**Divergências:**
|
||||
|
||||
| Sev | Tipo | Local | Problema | Fix |
|
||||
|---|---|---|---|---|
|
||||
| ~~Alta~~ ✅ | repo | `patientsRepository.js:64` | ~~`createPatient` aceita `owner_id` do payload~~ | **RESOLVIDO 2026-05-20 (spillover M1.3):** repository sempre injeta `owner_id = await getUid()`; strip `owner_id` do payload via destructure. |
|
||||
| ~~Alta~~ ✅ | composable | `usePatientDetail.js:13-40` | ~~supabase direto em 4 funções~~ | **RESOLVIDO 2026-05-20 (M2.2):** 4 funções migradas pra patientsRepository |
|
||||
| ~~Alta~~ ✅ | composable | `usePatientFinancial.js:21,156-164` | ~~`_lastPatientId` em variável módulo + supabase direto~~ | **RESOLVIDO 2026-05-20 (M2.3):** state movido DENTRO da function; mutations via repository |
|
||||
| ~~Alta~~ ✅ | composable | `usePatientSessions.js:33-44, 127-182` | ~~supabase direto em 2 mutations~~ | **RESOLVIDO 2026-05-20 (M2.4):** list+create+updateStatus via repository (com helper findSessionByRecurrence pra materialização) |
|
||||
| ~~Média~~ ✅ | composable | `usePatients.js:22` | ~~`error = ref(null)` viola canon~~ | **RESOLVIDO 2026-05-20 (spillover M1.3):** `error = ref('')` canon do composable-blueprint. |
|
||||
| ~~Média~~ ✅ | composable | `usePatientDetail.js:69` | ~~Funções internas retornam `null` silencioso em erro~~ | **RESOLVIDO 2026-05-20 (M2.2):** repository functions throw em vez de return null |
|
||||
| Média | composable | `usePatientFinancial.js:149-191, usePatientSessions.js:140-182` | Mutations retornam `{ok, data?, error?}` em vez de throw | Padrão preservado por compat com callers; fix posterior em sessão dedicada |
|
||||
| ~~Média~~ ✅ | composable | `usePatientRecurrences.js:34` | ~~`.select('*')` inline~~ | **RESOLVIDO 2026-05-20 (M2.1):** `PATIENT_RECURRENCE_RULES_SELECT` em patientsSelects.js |
|
||||
| ~~Média~~ ✅ | composable | `usePatientMessages.js:29, usePatientDocuments.js:30, usePatientSessions.js:38` | ~~SELECT inline sem constante~~ | **RESOLVIDO 2026-05-20 (M2.1):** 5 constantes em patientsSelects.js |
|
||||
| ~~Média~~ ✅ | composable | `usePatientFinancial.js:127-130, usePatientSessions.js:211-274` | ~~Mutações sem `.eq('tenant_id', tid)`~~ | **RESOLVIDO 2026-05-20 (M2.3+M2.4):** todas mutations no repository usam `.eq('tenant_id', tid)` |
|
||||
| ~~Baixa~~ ✅ | composable | `usePatients.js:45` | ~~`remove` não re-throw~~ | **RESOLVIDO 2026-05-20 (M2.6):** Tipo A canônico completo |
|
||||
| Baixa | composable | `usePatientSessions.js:67` | Filtro de virtual occurrences encapsulado no composable | Documentar como legado pré-refactor |
|
||||
|
||||
### 3. Prontuário/Evolução
|
||||
|
||||
**Estado:** **Embrionário.** Mal-existe. Aba "Prontuário evolutivo" é placeholder vazio.
|
||||
|
||||
**Arquivos-chave:**
|
||||
- `src/features/patients/prontuario/PatientProntuario.vue` (188 KB) — shell de abas
|
||||
- `src/features/patients/prontuario/PatientConversationsTab.vue` (11.8 KB) — timeline supabase direto
|
||||
- `usePatientSessions.js` reusado (não é prontuário-específico)
|
||||
|
||||
**Divergências:**
|
||||
|
||||
| Sev | Tipo | Local | Problema | Fix |
|
||||
|---|---|---|---|---|
|
||||
| Alta | composable | `PatientProntuario.vue:29` | supabase direto pra `conversation_messages`, `agenda_eventos`, `financial_records`, `documents`, `patient_groups`, `patient_tags` | Extrair `usePatientConversations`, `usePatientFinancial`, `usePatientDocuments` |
|
||||
| Alta | repo | (não existe) | Nenhum repository pras tabelas do prontuário | Criar `patientFinancialRepository.js`, `patientDocumentsRepository.js` |
|
||||
| Alta | composable | `PatientConversationsTab.vue:8` | Query direto a `conversation_messages` | Mover pra repository |
|
||||
| Média | gap | `PatientProntuario.vue:1950` | Aba "Prontuário" é placeholder vazio — schema clínico não modelado | **Decidir modelo: `patient_notes`? `clinic_sessions`? `patient_clinical_notes`?** |
|
||||
| Média | composable | `usePatientSessions.js:38` | Queries inline a `agenda_eventos`, `recurrence` | Mover pra `patientSessionsRepository.js` |
|
||||
| Média | repo | `PatientProntuario.vue:381-384` | `updateSessionStatus` mutação inline em componente | Mover pra repository |
|
||||
|
||||
**Gaps estruturais:**
|
||||
1. Repository layer ausente (3 repositories a criar)
|
||||
2. Composable layer incompleto (3 composables a criar)
|
||||
3. **Schema clínico inexistente** — anamnese, evolução, plano terapêutico não modelados
|
||||
4. PatientProntuario.vue é monolítico (188 KB) — refactor candidate
|
||||
|
||||
### 4. Financeiro
|
||||
|
||||
**Estado:** **Órfão.** Módulo existe mas sem camada repository; composables raiz fazem supabase direto.
|
||||
|
||||
**Arquivos-chave:**
|
||||
- `src/features/financeiro/pages/FinanceiroPage.vue` — supabase direto inline
|
||||
- `src/features/financeiro/pages/FinanceiroDashboardPage.vue` — RPC direto inline
|
||||
- `src/composables/useFinancialRecords.js` — composable raiz com supabase inline (sem repository)
|
||||
- `src/composables/useAgendaFinanceiro.js` — orquestrador agenda-financeiro com lógica duplicada
|
||||
|
||||
**Divergências:**
|
||||
|
||||
| Sev | Tipo | Local | Problema | Fix |
|
||||
|---|---|---|---|---|
|
||||
| Alta | repo | `useFinancialRecords.js:294` | UPDATE sem `.eq('tenant_id', tid)` | Defesa em profundidade |
|
||||
| Alta | repo | `useAgendaFinanceiro.js:194, 215` | UPDATE sem `.eq('tenant_id', tid)` em 2 pontos | Idem |
|
||||
| Alta | composable | `useFinancialRecords.js:58` | `error = ref(null)` | `ref('')` |
|
||||
| Alta | camada | `useFinancialRecords.js` (todo) | supabase direto viola blueprint | Extrair pra `financeiro/services/financialRecordsRepository.js` |
|
||||
| Alta | camada | `useAgendaFinanceiro.js:191, 205, 209` | supabase direto | Mover pra repository ou RPC wrapper |
|
||||
| Alta | overlap | `useAgendaFinanceiro.js:114-151` + `useFinancialRecords.js:157-189` | **Lógica duplicada de criação de cobrança** — ambos chamam mesma RPC | Consolidar em 1 composable orquestrador |
|
||||
| Média | convenção | `FinanceiroPage.vue:22-51` | supabase direto em componente | Mover pra composable |
|
||||
| Média | convenção | `FinanceiroDashboardPage.vue:68, 78, 144` | RPC inline em componente | Criar `useFinancialDashboard` |
|
||||
| Média | SELECT | `useFinancialRecords.js:40-51` | BASE_SELECT constante OK, mas sem `flatten<Feature>Row` | Adicionar helper se joins aninhados |
|
||||
| Baixa | cosmético | `FinanceiroPage.vue:27-40` | Formatadores BRL/Date duplicados na dashboard | Extrair pra `financeiro/utils/formatters.js` |
|
||||
|
||||
**Overlap crítico com agenda:**
|
||||
- `useAgendaFinanceiro.gerarCobrancaManual()` vs `useFinancialRecords.createRecord()` chamam mesma RPC — **risco double-billing em race condition**
|
||||
- `useAgendaFinanceiro.handleStatusChange()` relê `financial_records` (linhas 191, 205) — query que pertence a `useFinancialRecords`
|
||||
- Ambos importam `useTenantStore` + `getUid()` inline (duplicação)
|
||||
|
||||
### 5. Multi-tenant
|
||||
|
||||
**Estado:** Parcial. Stores OK. **Gap crítico: convites/membership inexistem.** SetupWizard e SaasTenantFeaturesPage com queries inline.
|
||||
|
||||
**Arquivos-chave:**
|
||||
- `src/stores/tenantStore.js` — Pinia store + memberships read-only via RPC
|
||||
- `src/stores/tenantFeaturesStore.js` — computed store + TTL cache + RPC
|
||||
- `src/stores/entitlementsStore.js` — view-based (`v_tenant_entitlements`, `v_user_entitlements`)
|
||||
- `src/features/setup/SetupWizardPage.vue` — 8 queries supabase inline
|
||||
- `src/views/pages/saas/SaasTenantFeaturesPage.vue` — 4 queries inline
|
||||
- `src/features/clinic/components/ModuleRow.vue` — dumb component OK
|
||||
|
||||
**Divergências:**
|
||||
|
||||
| Sev | Tipo | Local | Problema | Fix |
|
||||
|---|---|---|---|---|
|
||||
| ~~Alta~~ ✅ | composable | `SaasTenantFeaturesPage.vue:124-129` | ~~4 queries supabase direto~~ | **RESOLVIDO 2026-05-20 (M5 quick win):** extraído pra `src/services/tenantFeatureAdminService.js`. |
|
||||
| Alta | repository | `SetupWizardPage.vue:419, 429, 446, 595, 626, 656, 681, 706` | 8 supabase queries inline em página | Criar `setupRepository.js` + `useSetupWizard.js` |
|
||||
| Média | store | `tenantFeaturesStore.js:134` | `fetchForTenant` faz `from('tenant_features')` direto | Wrapper em `tenantFeaturesRepository.js` |
|
||||
| Média | store | `entitlementsStore.js:136, 177` | Queries em views direto | Aceitar como read-only com comentário |
|
||||
| Média | convention | `SaasTenantFeaturesPage.vue:33-53` | Error pattern inconsistente | Usar toast |
|
||||
| Baixa | naming | `tenantFeaturesStore.js:52` | `loadedForTenantId` vs `tenantId` ambíguo | Renomear |
|
||||
| Baixa | cosmetic | `SetupWizardPage.vue:60` | `isClinicRole` via string matching | Usar `useRoleGuard` |
|
||||
|
||||
**Gap crítico — convites/membership:**
|
||||
|
||||
Grep por `tenant_members`, `tenant_invite`, `convite`, `invitation` retornou **zero** em `features/`. Não existe:
|
||||
- Repository para `sendInvite(tenantId, email)`
|
||||
- Repository para `acceptInvite(inviteId)`
|
||||
- Repository para `listTenantMembers(tenantId)`
|
||||
- Composable wrapper
|
||||
- Página `/admin/members` pra gestão
|
||||
|
||||
**Recomendação:** criar `features/tenantship/` (ou `features/team/`) completo. Bloqueador de MVP.
|
||||
|
||||
### 6. Notificações
|
||||
|
||||
**Estado:** **Embrionário.** Fragmentado em 3+ canais (WhatsApp Evolution, WhatsApp Twilio, SMS Twilio, in-app, notices globais), sem padronização.
|
||||
|
||||
**Arquivos-chave:**
|
||||
- `src/features/notices/noticeService.js` — supabase direto sem repository
|
||||
- `src/features/conversations/CRMConversasPage.vue` — página complexa, lógica não extraída
|
||||
- `src/composables/useConversations.js` — query + business logic + supabase direto
|
||||
- `src/composables/useNotifications.js` — toast + realtime + polling
|
||||
- `src/stores/notificationStore.js` — in-app puro (OK)
|
||||
- `src/stores/conversationDrawerStore.js` — mistura send WhatsApp/SMS + templates
|
||||
- `src/stores/twilioWhatsappStore.js` — estado Twilio subcontas
|
||||
- `src/views/pages/notifications/SmsChannelSetupPage.vue` — credenciais via supabase
|
||||
- `src/views/pages/therapist/NotificationsHistoryPage.vue` — sync com store
|
||||
|
||||
**Divergências:**
|
||||
|
||||
| Sev | Tipo | Local | Problema | Fix |
|
||||
|---|---|---|---|---|
|
||||
| Alta | repo | `noticeService.js:28-44` | SELECT inline | Criar `notices/noticeSelects.js` |
|
||||
| Alta | composable | `useConversations.js:85-91` | supabase direto, sem repository | Criar `conversations/services/conversationsRepository.js` |
|
||||
| Alta | repo | `useConversations.js:229-244` | SELECT inline em `loadThreadMessages()` | Extrair pra repository |
|
||||
| Alta | gap | `conversationDrawerStore.js:339-346` | Edge function invoke direto sem fallback/retry | Criar `sendMessageService.js` com error handling |
|
||||
| Alta | canais | `conversationDrawerStore.js:327-377` | Lógica de envio WhatsApp (Evolution + Twilio) sem abstração | Factory por canal |
|
||||
| Média | error | `useNotifications.js:117-145` | Realtime/polling sem try/catch | Wrap |
|
||||
| Média | repo | `conversationDrawerStore.js:414-449` | `loadTemplates()` sem `.eq('tenant_id', ...)` no 2º select | Adicionar guard |
|
||||
| Média | naming | `SmsChannelSetupPage.vue:84-102` | Query sem SELECT canônico | Extrair |
|
||||
| Baixa | cosmético | `useConversations.js:165-184` | Channel filter hardcoded ['whatsapp','sms','email'] | Exportar `CHANNEL_TYPES` const |
|
||||
|
||||
**Canais identificados:**
|
||||
1. WhatsApp (Evolution API) — Parcial
|
||||
2. WhatsApp (Twilio) — Parcial
|
||||
3. SMS (Twilio) — **Stub** (só setup, sem envio)
|
||||
4. In-app (browser notifications) — Funcional
|
||||
5. Global Notices — Funcional
|
||||
|
||||
**Gaps estruturais:**
|
||||
- Repositórios inexistem (conversas, mensagens, canais)
|
||||
- `_tenantGuards.js` ausente
|
||||
- SELECT canônico fragmentado
|
||||
- Composables fat (`useConversations` faz 3 coisas)
|
||||
- SMS envio não implementado (só credenciais)
|
||||
|
||||
---
|
||||
|
||||
## Próximos passos
|
||||
|
||||
### Ajustes ao plano original
|
||||
|
||||
**Fase 0** — concluída. Audit baseline pronto.
|
||||
|
||||
**Fase 1** — sequenciamento revisado considerando as 4 surpresas:
|
||||
|
||||
| Ordem | Módulo | Pré-requisito | Observação |
|
||||
|---|---|---|---|
|
||||
| 1 | **Home/Components** | — | Inclui promover quick-create blueprint (3 candidates já existem) + criar `medicos/` e `insurance/` features |
|
||||
| 1.5 | **Quick-create blueprint promotion** | — | Mover blueprint de "agenda-only" pra universal; refatorar 3 CadastroRapido components em paralelo |
|
||||
| 2 | **Pacientes** | — | `patientsRepository` já parcial; fix 4 composables com supabase direto |
|
||||
| 3 | **Prontuário (parcial)** | **Decisão de schema clínico** | Sem schema, só dá pra criar repositories pras tabelas existentes (financial_records, documents) |
|
||||
| 4 | **Financeiro** | Decisão sobre overlap com agenda | Resolver double-billing risk ANTES de refactor |
|
||||
| 5 | **Multi-tenant + Convites** | — | Criar `tenantship/` feature inteiro (gap crítico) |
|
||||
| 6 | **Notificações** | — | Pesado: 3 canais, abstração por factory |
|
||||
|
||||
### Decisões pendentes (precisa de você)
|
||||
|
||||
1. **Quick-create blueprint:** promover pra universal agora ou manter agenda-only? (recomendo promover — promotion criteria atingida)
|
||||
2. **Schema clínico do prontuário:** modelar agora (bloqueador) ou empurrar pra Fase 1 estendida?
|
||||
3. **Overlap billing agenda↔financeiro:** consolidar em 1 composable OU separar com coordenação via fila? (recomendo consolidar)
|
||||
4. **Convites/membership:** criar feature `tenantship/` separada OU absorver em `clinic/`? (recomendo separada — semântica diferente)
|
||||
5. **`dev_auditoria_items` no banco:** popular agora os 51 itens via SQL OU UI uma a uma? (recomendo SQL batch insert — mais rápido pra começar Fase 1)
|
||||
|
||||
---
|
||||
|
||||
## Referências
|
||||
|
||||
- Blueprints: `blueprints/repository-blueprint.md`, `composable-blueprint.md`, `quick-create-overlay-blueprint.md`
|
||||
- Estratégia: `development/02-auditoria/PADRONIZACAO.md`
|
||||
- Memória: `project_padronizacao_sweep.md`, `project_graphify_findings_20260504.md`
|
||||
@@ -0,0 +1,480 @@
|
||||
# Design — useBillingOrchestrator
|
||||
|
||||
> **Data:** 2026-05-20
|
||||
> **Tipo:** Design doc (sem código). Implementação fica pra Módulo 4 (Financeiro) da Fase 1.
|
||||
> **Resolve:** decisão 7 do PADRONIZACAO.md — overlap billing agenda ↔ financeiro com risco de double-billing.
|
||||
|
||||
---
|
||||
|
||||
## 1. Problema atual
|
||||
|
||||
### 1.1 Três caminhos pra criar cobrança
|
||||
|
||||
Cobrança de sessão hoje pode ser criada por **3 lugares diferentes**:
|
||||
|
||||
| # | Caminho | Arquivo | Quando |
|
||||
|---|---|---|---|
|
||||
| A | Botão "Gerar cobrança" manual | `useAgendaFinanceiro.gerarCobrancaManual()` (linha 114) | User clica explicitamente em sessão sem cobrança |
|
||||
| B | Mudança de status na agenda | `useAgendaFinanceiro.handleStatusChange()` (linha 163) | User troca status (agendado→faltou, etc) |
|
||||
| C | Decisões aplicadas no Melissa | `useMelissaAgenda._applyStatusDecisions()` (linha 1450) | User confirma transição de status no fluxo Melissa |
|
||||
|
||||
**Os 3 chamam a mesma RPC** `create_financial_record_for_session`. Sem coordenação central. Resultado: race condition silenciosa possível.
|
||||
|
||||
### 1.2 UPDATEs diretos espalhados
|
||||
|
||||
`handleStatusChange` também faz UPDATE/SELECT em `financial_records` direto (linhas 191, 194, 205, 208) — queries que **pertencem ao useFinancialRecords** mas são duplicadas aqui pra evitar import circular.
|
||||
|
||||
### 1.3 State em variável de módulo (vaza)
|
||||
|
||||
`useAgendaFinanceiro.js:38`:
|
||||
```js
|
||||
const _exceptionsCache = new Map(); // ← módulo-level, vaza entre instâncias
|
||||
```
|
||||
|
||||
Quando user troca de tenant, cache não invalida automaticamente. Memória `useAgendaFinanceiro.invalidateExceptionsCache()` precisa ser chamada manualmente em vários lugares.
|
||||
|
||||
### 1.4 Cenários de double-billing concretos
|
||||
|
||||
1. **Race manual + status:** user clica "Gerar cobrança" + muda status pra "faltou" em < 200ms. Path A insere registro pending; Path B detecta sessão sem `billed` (já que ainda não chegou) e cria outro registro pela exceção.
|
||||
2. **Realizado vindo de faltou paid:** sessão estava `faltou` com multa paid. User volta pra `agendado` → `realizado`. Path B/C podem regerar cobrança em cima da multa paid existente (memória `project_rpc_idempotency_cancelled` foi um fix relacionado mas não cobre todo o problema).
|
||||
3. **Pacote saldo + adicional:** sessão de pacote `billing_contract_id` setado bloqueia Path A (linha 116). Mas Path B/C podem **não checar** esse campo em alguma branch — risco de cobrança individual em sessão de pacote.
|
||||
|
||||
---
|
||||
|
||||
## 2. Goals & Non-goals
|
||||
|
||||
### Goals
|
||||
1. **Single entry point** pra qualquer mudança de billing relacionada a evento da agenda.
|
||||
2. **Idempotência garantida** — chamar 2× a mesma intenção produz o mesmo resultado.
|
||||
3. **State machine explícito** de transições de status com consequências financeiras claras.
|
||||
4. **Reverse transitions** tratadas (realizado→agendado, faltou→agendado, cancelado→agendado).
|
||||
5. **Orchestrador NUNCA toca supabase direto** — só via repository e composable de financeiro.
|
||||
6. **Cache de regras de exceção** vive na instância do composable, não em módulo.
|
||||
|
||||
### Non-goals (fora deste escopo)
|
||||
1. Implementação — só design. Código vem na Fase 1 Módulo 4.
|
||||
2. Refator de `useFinancialRecords` em si (extrair pra repository) — vai junto no Módulo 4.
|
||||
3. Gateway de pagamento (Asaas) — Fase 3 do ROADMAP.
|
||||
4. Repasse a terapeutas — `therapist_payouts` separado.
|
||||
5. UI/UX de confirmação de reverse transitions — já mapeado em memória `project_agenda_reverse_transitions`, implementação no Módulo 4.
|
||||
|
||||
---
|
||||
|
||||
## 3. State machine de transições
|
||||
|
||||
### 3.1 Status válidos
|
||||
|
||||
Enum `status_evento_agenda` (do schema): `agendado | realizado | faltou | cancelado | remarcar`
|
||||
|
||||
### 3.2 Matriz `from → to`
|
||||
|
||||
| | →agendado | →realizado | →faltou | →cancelado | →remarcar |
|
||||
|---|---|---|---|---|---|
|
||||
| **agendado→** | — | ✅ FORWARD | ✅ FORWARD | ✅ FORWARD | ✅ FORWARD |
|
||||
| **realizado→** | ⚠️ REVERSE | — | ⚠️ REVERSE | ⚠️ REVERSE | ❌ inválida |
|
||||
| **faltou→** | ⚠️ REVERSE | ⚠️ CROSS | — | ⚠️ CROSS | ❌ inválida |
|
||||
| **cancelado→** | ⚠️ REVERSE | ⚠️ CROSS | ⚠️ CROSS | — | ❌ inválida |
|
||||
| **remarcar→** | ✅ FORWARD | ✅ FORWARD | ✅ FORWARD | ✅ FORWARD | — |
|
||||
|
||||
### 3.3 Tabela de consequências financeiras
|
||||
|
||||
| Transição | Ação financeira default | Decisão do user (override) |
|
||||
|---|---|---|
|
||||
| `agendado→realizado` | Criar pending (se ainda não billed) com `amount = event.price` | Marcar como já recebido (forma de pagamento) |
|
||||
| `agendado→faltou` | Consultar `financial_exceptions[patient_no_show]` → criar multa OR cancelar existente | Consumir saldo de pacote (se aplicável) |
|
||||
| `agendado→cancelado` | Consultar `financial_exceptions[patient_cancellation]` + `min_hours_notice` → criar taxa de cancelamento tardio OR cancelar existente | — |
|
||||
| `realizado→agendado` | **REVERSE:** se record `paid` existe → confirm dialog (refund_paid). Se `pending` → soft-cancel. Se `paid+package`: refund + devolver saldo | Reverter manualmente sem auto |
|
||||
| `realizado→faltou` | **CROSS:** reverter realizado + aplicar regra de no-show. Se já paid → manter pago e converter em multa | — |
|
||||
| `faltou→agendado` | **REVERSE:** cancelar multa pending. Se multa paid → confirm dialog (refund) | — |
|
||||
| `cancelado→agendado` | **REVERSE:** cancelar taxa de cancelamento (se houver) | — |
|
||||
| `*→remarcar` | Manter cobrança existente, atualizar `due_date` quando reagendar | — |
|
||||
|
||||
### 3.4 Pacote (billing_contract_id presente)
|
||||
|
||||
**Sobre qualquer transição:** se `event.billing_contract_id` não-nulo, **não criar nem cancelar `financial_records` individual**. Em vez disso:
|
||||
- `agendado→realizado`: incrementa `billing_contracts.sessions_used`
|
||||
- `agendado→faltou` ou `agendado→cancelado` com `default_consume_on_miss=true`: incrementa `sessions_used`
|
||||
- `realizado→agendado`: decrementa `sessions_used` (refresh FRESH do DB antes, memória `project_agenda_reverse_transitions`)
|
||||
|
||||
Memória relevante: `project_cross_week_propagation` — bulk-load tem que rodar mesmo sem reais na view + query records cross-week por recurrence_id.
|
||||
|
||||
---
|
||||
|
||||
## 4. API shape
|
||||
|
||||
### 4.1 Signature do composable
|
||||
|
||||
```js
|
||||
export function useBillingOrchestrator() {
|
||||
// ─── State ──────────────────────────────────────────────────
|
||||
const loading = ref(false); // operação async em andamento
|
||||
const error = ref(''); // string vazia default (canon do composable-blueprint)
|
||||
|
||||
// ─── Public actions ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Orquestra mudança de status de um evento + consequências financeiras.
|
||||
* Single entry point — substitui os 3 caminhos atuais.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {Object} params.event - row de agenda_eventos completa
|
||||
* @param {string} params.fromStatus
|
||||
* @param {string} params.toStatus
|
||||
* @param {Object} [params.decisions] - overrides do user (ver decisões pendentes seção 5)
|
||||
* @returns {Promise<{ok, actions: Array<BillingAction>, error?}>}
|
||||
*/
|
||||
async function applyStatusChange(params) { ... }
|
||||
|
||||
/**
|
||||
* Gera cobrança manual pra evento sem cobrança ainda. Idempotente.
|
||||
* Bloqueia se billing_contract_id presente.
|
||||
*/
|
||||
async function generateChargeForEvent(event, options = {}) { ... }
|
||||
|
||||
/**
|
||||
* Lista records financeiros vinculados ao evento.
|
||||
*/
|
||||
async function fetchRecordsForEvent(eventId) { ... }
|
||||
|
||||
/**
|
||||
* Cancela TODOS os records pending/overdue de um evento (soft).
|
||||
* Use APENAS em reverse transitions confirmadas pelo user.
|
||||
*/
|
||||
async function cancelRecordsForEvent(eventId, reason) { ... }
|
||||
|
||||
/**
|
||||
* Lê regra de exceção financeira com cache local (instância).
|
||||
*/
|
||||
async function getExceptionRule(tenantId, exceptionType) { ... }
|
||||
|
||||
function invalidateRules() { ... } // chama em troca de tenant
|
||||
|
||||
return {
|
||||
loading, error,
|
||||
applyStatusChange, generateChargeForEvent,
|
||||
fetchRecordsForEvent, cancelRecordsForEvent,
|
||||
getExceptionRule, invalidateRules
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Tipos relevantes
|
||||
|
||||
```js
|
||||
/** @typedef {Object} BillingAction
|
||||
* Resultado de uma operação. Compõe o array `actions` retornado por applyStatusChange.
|
||||
* @property {string} type - 'created' | 'updated' | 'cancelled' | 'paid' | 'package_consumed' | 'package_returned' | 'noop'
|
||||
* @property {string} [recordId]
|
||||
* @property {number} [amount]
|
||||
* @property {string} [reason]
|
||||
*/
|
||||
|
||||
/** @typedef {Object} BillingDecisions
|
||||
* Overrides explícitos do user. Quando ausente, orchestrator decide via regras.
|
||||
* @property {boolean} [consumePackageSession] - faltou/cancelado: consumir saldo de pacote
|
||||
* @property {'auto'|'always'|'never'} [applyNoShowFee] - aplicar multa em faltou
|
||||
* @property {'cancel_pending'|'refund_paid'|'manual'} [reverseCleanup] - reverse: como tratar records existentes
|
||||
*/
|
||||
```
|
||||
|
||||
### 4.3 Exemplo de uso (caller)
|
||||
|
||||
```js
|
||||
// Em AgendaEventDialog.vue (ou onde quer que aplique status change)
|
||||
import { useBillingOrchestrator } from '@/features/financeiro/composables/useBillingOrchestrator';
|
||||
|
||||
const billing = useBillingOrchestrator();
|
||||
|
||||
async function onStatusChange(novoStatus, decisoes) {
|
||||
const result = await billing.applyStatusChange({
|
||||
event: eventoAtual.value,
|
||||
fromStatus: eventoAtual.value.status,
|
||||
toStatus: novoStatus,
|
||||
decisions: decisoes // pode ser undefined — orchestrator usa regras default
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
toast.add({ severity: 'error', summary: 'Falha', detail: result.error });
|
||||
return;
|
||||
}
|
||||
|
||||
// result.actions é narrativa do que aconteceu — use pra UI feedback
|
||||
for (const action of result.actions) {
|
||||
if (action.type === 'created') showCreatedToast(action.amount);
|
||||
else if (action.type === 'cancelled') showCancelledToast();
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Arquitetura interna
|
||||
|
||||
### 5.1 Dependências (camadas)
|
||||
|
||||
```
|
||||
useBillingOrchestrator
|
||||
│
|
||||
├──> useFinancialRecords (thin wrapper)
|
||||
│ │
|
||||
│ └──> financialRecordsRepository
|
||||
│ │
|
||||
│ └──> supabase (RPC + tabela)
|
||||
│
|
||||
├──> useBillingContracts (composable a criar — pacotes)
|
||||
│ │
|
||||
│ └──> billingContractsRepository
|
||||
│
|
||||
├──> useFinancialExceptions (composable — regras de exceção)
|
||||
│ │
|
||||
│ └──> financialExceptionsRepository
|
||||
│
|
||||
└──> useAgendaEvents (THIN — só pra propagação reativa, não pra writes)
|
||||
│
|
||||
└──> agendaRepository (já existe)
|
||||
```
|
||||
|
||||
**Regra absoluta:** orchestrator não importa `supabase` diretamente. Só dos composables/repositories acima.
|
||||
|
||||
### 5.2 Internals
|
||||
|
||||
```js
|
||||
// PRIVATE — não exportado
|
||||
const _rulesCache = new Map(); // ← agora DENTRO da function, vive com a instância
|
||||
|
||||
async function _resolveBillingState(eventId) {
|
||||
// Snapshot completo: records[], contract?, exceptionRule?
|
||||
// Pra decidir transição sem race conditions.
|
||||
const records = await financialRecords.fetchByEvent(eventId);
|
||||
const packageInfo = event.billing_contract_id
|
||||
? await billingContracts.fetch(event.billing_contract_id)
|
||||
: null;
|
||||
return { records, packageInfo };
|
||||
}
|
||||
|
||||
async function _runTransition(event, fromStatus, toStatus, decisions, state) {
|
||||
const key = `${fromStatus}→${toStatus}`;
|
||||
const handler = TRANSITION_HANDLERS[key];
|
||||
if (!handler) {
|
||||
throw new Error(`Transição inválida: ${key}`);
|
||||
}
|
||||
return handler({ event, decisions, state });
|
||||
}
|
||||
|
||||
const TRANSITION_HANDLERS = {
|
||||
'agendado→realizado': _handleRealizado,
|
||||
'agendado→faltou': _handleFaltou,
|
||||
// ... 1 handler por transição válida
|
||||
};
|
||||
|
||||
async function _handleRealizado({ event, decisions, state }) {
|
||||
if (event.billing_contract_id) {
|
||||
return _consumePackageSession(event);
|
||||
}
|
||||
|
||||
// Sessão avulsa — criar pending se não tem record ativo
|
||||
const hasActive = state.records.some(r => ['pending','overdue','paid'].includes(r.status));
|
||||
if (hasActive) {
|
||||
return [{ type: 'noop', reason: 'Record já existe' }];
|
||||
}
|
||||
const record = await financialRecords.create({
|
||||
patient_id: event.patient_id,
|
||||
agenda_evento_id: event.id,
|
||||
amount: event.price,
|
||||
due_date: _eventDateISO(event)
|
||||
});
|
||||
return [{ type: 'created', recordId: record.id, amount: event.price }];
|
||||
}
|
||||
|
||||
async function _handleFaltou({ event, decisions, state }) {
|
||||
if (event.billing_contract_id) {
|
||||
return decisions?.consumePackageSession ?? state.exceptionRule?.default_consume_on_miss
|
||||
? _consumePackageSession(event)
|
||||
: [{ type: 'noop' }];
|
||||
}
|
||||
|
||||
const rule = await getExceptionRule(event.tenant_id, 'patient_no_show');
|
||||
if (!rule || rule.charge_mode === 'none') {
|
||||
return _cancelExistingPending(state.records);
|
||||
}
|
||||
// ... lógica completa
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Idempotência — como o orchestrator garante
|
||||
|
||||
Antes de criar record:
|
||||
```js
|
||||
// 1. Snapshot da state ANTES da decisão (já feito em _resolveBillingState)
|
||||
// 2. Verificar se record ativo já existe pro evento+intenção
|
||||
const existingActive = state.records.find(r =>
|
||||
['pending', 'overdue', 'paid'].includes(r.status)
|
||||
);
|
||||
if (existingActive) {
|
||||
// Decidir: noop, update, ou criar segundo (raro — multa em cima de sessão paid)
|
||||
}
|
||||
|
||||
// 3. Locks otimistas via UPDATE com .eq('status', expectedStatus)
|
||||
// Se conflito, refresh + re-decide
|
||||
```
|
||||
|
||||
Antes de update/cancel:
|
||||
```js
|
||||
// Sempre filtra .eq('tenant_id', tid) defesa em profundidade
|
||||
// (corrige divergências do audit baseline)
|
||||
```
|
||||
|
||||
Para mudanças de pacote:
|
||||
```js
|
||||
// REFRESH FRESH do banco antes do UPDATE (memória project_agenda_reverse_transitions)
|
||||
const currentContract = await billingContracts.fetch(id);
|
||||
await billingContracts.update(id, {
|
||||
sessions_used: currentContract.sessions_used - 1
|
||||
});
|
||||
```
|
||||
|
||||
### 5.4 RPC `create_financial_record_for_session` — usar como single insert path
|
||||
|
||||
A RPC já existe e tem idempotência (memória `project_rpc_idempotency_cancelled` foi fix recente). Orchestrator usa ela como **única forma de criar record de sessão**. INSERT direto fica APENAS pra `createManualRecord` (lançamento avulso sem evento), que continua em `useFinancialRecords.createManualRecord`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Plano de migração (faseado, Módulo 4)
|
||||
|
||||
### Fase A — Foundation (preparar terreno)
|
||||
1. Criar `features/financeiro/services/` com:
|
||||
- `_tenantGuards.js` (copy do agenda)
|
||||
- `financialSelects.js` (extrair `BASE_SELECT` atual)
|
||||
- `financialRecordsRepository.js` (extrair queries do `useFinancialRecords`)
|
||||
- `financialExceptionsRepository.js` (novo — pra `financial_exceptions`)
|
||||
- `billingContractsRepository.js` (novo — pra `billing_contracts`)
|
||||
2. Adicionar `.eq('tenant_id', tid)` em todas operações (fix do audit alta sev).
|
||||
|
||||
### Fase B — Composables refatorados (sem mudar callers)
|
||||
1. Mover `useFinancialRecords.js` pra `features/financeiro/composables/`.
|
||||
2. Refatorar pra usar repository (thin wrapper). Aplicar canon `error = ref('')`.
|
||||
3. Criar `features/financeiro/composables/useFinancialExceptions.js`.
|
||||
4. Criar `features/financeiro/composables/useBillingContracts.js`.
|
||||
5. Criar `features/financeiro/composables/useBillingOrchestrator.js` com signature acima.
|
||||
6. Callers ainda usam `useFinancialRecords` direto + `useAgendaFinanceiro` — **nada quebra ainda**.
|
||||
|
||||
### Fase C — Migração de callers (1 por vez)
|
||||
1. Migrar `AgendaEventDialog.vue` pra `useBillingOrchestrator.applyStatusChange` em vez de `useAgendaFinanceiro.handleStatusChange`.
|
||||
2. Migrar `useMelissaAgenda._applyStatusDecisions` (linha 1450-1505) pra orchestrator.
|
||||
3. Migrar todos os callers de `useAgendaFinanceiro.gerarCobrancaManual` → `useBillingOrchestrator.generateChargeForEvent`.
|
||||
|
||||
### Fase D — Cleanup
|
||||
1. Deletar `src/composables/useAgendaFinanceiro.js` (callers todos migrados).
|
||||
2. Deletar `src/composables/useFinancialRecords.js` raiz (versão refatorada vive em `features/financeiro/composables/`).
|
||||
3. Remover `_exceptionsCache` módulo-level (já estava no novo composable).
|
||||
|
||||
### Sanity checks pós-migração
|
||||
- E2E Playwright: criar sessão → realizar com pagamento → mudar pra faltou → confirmar reverse → verificar contract.sessions_used. **NUNCA double-billing.**
|
||||
- Memory `project_agenda_billing_decisoes` — confirmar 5 decisões mantidas (#1 híbrido, #4 semi-auto no-show, #5 bloqueia edit cobrada, #7 credit note, #8 pagamento separado).
|
||||
|
||||
---
|
||||
|
||||
## 7. Decisões resolvidas (2026-05-20)
|
||||
|
||||
### 7.1 ✅ `applyStatusChange` faz APENAS financeiro
|
||||
|
||||
Signature final: `applyStatusChange({ event, fromStatus, toStatus, decisions })` retorna `{ ok, actions[], needsConfirmation?, error? }`. Caller é responsável por atualizar a agenda separadamente (`agendaRepository.update()`).
|
||||
|
||||
**Consequência:** orchestrator stateless quanto à agenda. Caller faz wrapping:
|
||||
```js
|
||||
await agendaEvents.update(event.id, { status: novoStatus });
|
||||
const billing = await billingOrchestrator.applyStatusChange({ ... });
|
||||
// Se billing.needsConfirmation, mostrar dialog. Se erro, considerar rollback do status.
|
||||
```
|
||||
|
||||
### 7.2 ✅ Reverse confirm via `needsConfirmation` no return
|
||||
|
||||
Quando orchestrator detecta reverse com record paid (realizado paid → agendado) ou pacote saldo consumido:
|
||||
|
||||
```js
|
||||
// Primeira chamada — sem decisions
|
||||
const r = await billingOrchestrator.applyStatusChange({ event, fromStatus: 'realizado', toStatus: 'agendado' });
|
||||
// r = { ok: false, needsConfirmation: true, options: [
|
||||
// { key: 'cancel_pending', label: 'Cancelar cobrança pendente', amount: 200 },
|
||||
// { key: 'refund_paid', label: 'Estornar pagamento', amount: 200 },
|
||||
// { key: 'manual', label: 'Resolver manualmente depois' }
|
||||
// ] }
|
||||
|
||||
// Caller mostra dialog → user escolhe → re-chama
|
||||
const r2 = await billingOrchestrator.applyStatusChange({
|
||||
event, fromStatus: 'realizado', toStatus: 'agendado',
|
||||
decisions: { reverseCleanup: 'refund_paid' }
|
||||
});
|
||||
// r2 = { ok: true, actions: [{ type: 'refunded', recordId, amount }] }
|
||||
```
|
||||
|
||||
### 7.3 ✅ Transação via RPC `apply_billing_status_transition`
|
||||
|
||||
Toda mudança financeira de transição roda em RPC dedicada. Tudo ou nada. RPC entra como migration durante Módulo 4. Composable orchestrator faz apenas:
|
||||
1. Resolve state atual (snapshot read-only)
|
||||
2. Calcula decisões (state machine no JS)
|
||||
3. Chama RPC com plano completo (`p_actions jsonb`)
|
||||
4. RPC executa em ordem dentro de uma transação SQL
|
||||
|
||||
Signature proposta da RPC:
|
||||
```sql
|
||||
CREATE FUNCTION public.apply_billing_status_transition(
|
||||
p_tenant_id uuid,
|
||||
p_event_id uuid,
|
||||
p_actions jsonb -- [{ kind: 'create_record', amount, due_date }, { kind: 'cancel_record', record_id }, ...]
|
||||
) RETURNS jsonb; -- { ok, applied: [...], failed?: { kind, reason } }
|
||||
```
|
||||
|
||||
### 7.4 ✅ Decisões #2/#3/#6 de billing — sessão dedicada antes do Módulo 4
|
||||
|
||||
Marcar sessão dedicada (~1h) pra fechar memória `project_agenda_billing_decisoes` antes da implementação. **Bloqueador parcial do Módulo 4** — orchestrator pode ser parcialmente implementado, mas a state machine só fica completa após resolver essas 3 decisões.
|
||||
|
||||
> ⚠️ **Pendência rastreada:** adicionar item em `dev_auditoria_items` ou agenda recorrente. Vai aparecer como **TODO inicial** na sessão de implementação do Módulo 4.
|
||||
|
||||
---
|
||||
|
||||
## 8. Open questions (não-bloqueantes pro design)
|
||||
|
||||
1. **`patient_timeline` integration:** quando orchestrator cria/cancela record, devia emitir evento `pagamento_recebido` / `pagamento_vencido` em `patient_timeline`? Hoje o enum suporta, mas não vejo inserts. **Sugestão:** adicionar como trigger no banco (não no orchestrator) — fica resiliente a chamadas que escapam do orchestrator.
|
||||
|
||||
2. **Gateway webhook (Asaas):** quando Asaas dispara webhook "paid", quem processa? Edge function dedicada que chama `financialRecords.markAsPaid(id, 'pix')`, sem passar por orchestrator (orchestrator é só pra mudanças via agenda). Documentar como caminho separado válido.
|
||||
|
||||
3. **Repasse a terapeuta (`therapist_payouts`):** quando record fica `paid`, gera entrada em `therapist_payout_records`? Hoje não. Decisão: trigger no banco que escuta UPDATE em `financial_records.status` → cria payout. Fora do escopo do orchestrator.
|
||||
|
||||
4. **`patient_assessments` (Fase 2):** notas clínicas com escalas têm relação com billing? Improvável — assessments são clínicos, não-monetizados. Confirmar quando implementar.
|
||||
|
||||
---
|
||||
|
||||
## 9. Cross-references
|
||||
|
||||
- Memórias relevantes:
|
||||
- `project_rpc_idempotency_cancelled.md` — RPCs ignoram cancelled
|
||||
- `project_billing_contracts_no_updated_at.md` — gotcha de UPDATE silently failing
|
||||
- `project_agenda_billing_decisoes.md` — 5 decisões base
|
||||
- `project_agenda_reverse_transitions.md` — confirm dialogs pra reverter
|
||||
- `project_cross_week_propagation.md` — pacote upfront cross-week
|
||||
- `project_c12_antecipar_iterar.md` — antecipar pacote (Watch sync resolveu snapshot stale)
|
||||
- Audit baseline divergências Financeiro: ver `AUDIT_BASELINE.md` seção 4
|
||||
- Blueprints: `repository-blueprint.md` + `composable-blueprint.md`
|
||||
- ROADMAP: Fase 1 itens 1-4 (Monetização)
|
||||
|
||||
---
|
||||
|
||||
## 10. Checklist pra implementação (Módulo 4 da Fase 1)
|
||||
|
||||
- [ ] 5 repositories criados em `features/financeiro/services/`
|
||||
- [ ] 4 composables criados em `features/financeiro/composables/`
|
||||
- [ ] `useBillingOrchestrator` com signature acima
|
||||
- [ ] State machine completa (todas transições da matriz 3.2)
|
||||
- [ ] Cache de regras dentro da instância (não módulo-level)
|
||||
- [ ] Idempotência testada (chamada 2× = noop)
|
||||
- [ ] `.eq('tenant_id', tid)` em todas mutações (defesa em profundidade)
|
||||
- [ ] RPC `apply_billing_status_transition` (decisão 7.3 opção B)
|
||||
- [ ] AgendaEventDialog migrado pra orchestrator
|
||||
- [ ] useMelissaAgenda._applyStatusDecisions migrado
|
||||
- [ ] gerarCobrancaManual callers migrados
|
||||
- [ ] useAgendaFinanceiro.js deletado
|
||||
- [ ] useFinancialRecords.js raiz deletado
|
||||
- [ ] E2E test cobrindo reverse transition (realizado paid → agendado)
|
||||
- [ ] Confirm dialogs UI implementados (memória project_agenda_reverse_transitions)
|
||||
@@ -0,0 +1,161 @@
|
||||
# Padronização — Sweep estrutural pré-MVP
|
||||
|
||||
> **Iniciado:** 2026-05-20
|
||||
> **Deadline MVP:** 3+ meses (lançamento limpo, sem usuários)
|
||||
> **Escopo MVP:** Pacientes + Agenda + Billing + Prontuário + Financeiro avançado + Multi-tenant
|
||||
|
||||
---
|
||||
|
||||
## Diagnóstico
|
||||
|
||||
Sistema tem ~487 arquivos Vue, ~75-80% MVP-ready, padronização irregular. **Agenda passou por C1-C13 + análise sênior** — é o único módulo com profundidade arquitetural. Outros módulos têm divergências (composables sem `tenant_id` consistente, SELECTs inline, page layouts inconsistentes, dialogs que não seguem o blueprint).
|
||||
|
||||
Sem padronizar antes de escalar features novas (Fase 1 do ROADMAP), cada gap vira dívida composta.
|
||||
|
||||
---
|
||||
|
||||
## Estratégia em 1 frase
|
||||
|
||||
**Agenda é a referência madura.** Extrair seus padrões em blueprints → propagar módulo a módulo → tracking em `dev_auditoria_items` com tag `padronizacao:<modulo>`. **Não tocar agenda** até a Fase 4 (apenas pendências residuais).
|
||||
|
||||
---
|
||||
|
||||
## Fases
|
||||
|
||||
### Fase 0 — Fundação (3-5 dias)
|
||||
|
||||
Extrair os blueprints faltantes a partir da agenda:
|
||||
|
||||
- [x] `melissa-page-blueprint.md` — já existe
|
||||
- [x] `melissa-table-page-blueprint.md` — já existe
|
||||
- [x] `dialog-blueprint.md` — já existe
|
||||
- [x] `repository-blueprint.md` — entregável 1 (2026-05-20)
|
||||
- [x] `composable-blueprint.md` — entregável 2 (2026-05-20)
|
||||
- [x] `quick-create-overlay-blueprint.md` — entregável 3 (2026-05-20). Documentado como **agenda-only** com promotion criteria explícito.
|
||||
|
||||
E baseline:
|
||||
|
||||
- [x] Update graphify do código atual (`graphify update src/` rodou 2026-05-20)
|
||||
- [x] Audit baseline por módulo → ver `development/02-auditoria/AUDIT_BASELINE.md` (2026-05-20). 51 divergências catalogadas + 4 surpresas estruturais. **Pendente:** popular `dev_auditoria_items` no banco (decisão de batch insert vs UI).
|
||||
|
||||
### Fase 0.5 — Pré-Fase 1 setups (pós-audit baseline)
|
||||
|
||||
Decisões 6-9 introduzem 4 entregáveis novos antes da Fase 1:
|
||||
|
||||
- [x] **0.5.A** Atualizar `quick-create-overlay-blueprint.md` de "agenda-only" pra universal (2026-05-20) — header reescrito, seção 9 virou "Promotion History", path convention adicionada, checklist generalizado, memória `feedback_agenda_inline_quick_create` marcada como superseded
|
||||
- [x] **0.5.B** Schema clínico modelado (2026-05-20). 4 migrations + 1 seed escritos em `database-novo/migrations/20260520000001..4_clinical_notes_*.sql` e `database-novo/seeds/seed_040_clinical_note_templates.sql`. **Não executado** — aguardando review do user pra rodar `node db.cjs migrate`. Tabelas: `clinical_notes` + `clinical_note_versions` (audit trail via trigger snapshot) + `clinical_note_templates` (6 templates do sistema: anamnese, SOAP, DAP, BIRP, evolução livre, plano padrão). FK órfã `documents.session_note_id` renomeada pra `clinical_note_id` com constraint. RLS: owner-only (CFP sigilo).
|
||||
- [x] **0.5.C** Design doc `useBillingOrchestrator` (2026-05-20) — `development/02-auditoria/DESIGN_BILLING_ORCHESTRATOR.md`. State machine de 10 transições, dependências em 4 composables/5 repositories, plano de migração faseado em 4 fases (A/B/C/D), checklist de implementação. **4 decisões resolvidas:** (1) `applyStatusChange` só financeiro, agenda separada; (2) reverse confirm via `needsConfirmation` no return; (3) transação via RPC dedicada `apply_billing_status_transition`; (4) sessão dedicada pra decisões billing #2/#3/#6 antes do Módulo 4.
|
||||
- [x] **0.5.D** Scaffold da feature `tenantship/` (2026-05-20). 7 arquivos criados em `src/features/tenantship/`: `_tenantGuards.js`, `tenantInvitesSelects.js` + `Repository.js`, `tenantMembersSelects.js` + `Repository.js`, `useTenantInvites.js`, `useTenantMembers.js`. Funções funcionais (não-stub) usando tabela existente `tenant_invites` + view `v_tenant_members_with_profiles`. **2 pendências documentadas no código:** (1) `acceptInvite()` é stub PT-BR explicando que precisa de RPC `accept_tenant_invite(p_token uuid)` — migration a criar; (2) `sendInvite()` só insere row — envio de email/WhatsApp fica pra Módulo 6 (Notificações).
|
||||
|
||||
### Fase 1 — Padronização módulo a módulo (4-6 semanas) — **em andamento**
|
||||
|
||||
Ordem confirmada com usuário:
|
||||
|
||||
| # | Módulo | Por que aqui | Status |
|
||||
|---|---|---|---|
|
||||
| 1 | **Home/Dashboard + Components base** | Alta visibilidade. Inclui refactor dos 3 quick-creates promovidos (decisão 6). | ✅ **Concluído 2026-05-20** (exceto M1.6 — MelissaLayout decomposition, deferida) |
|
||||
| 2 | **Pacientes** | Núcleo de dados — alimenta agenda, prontuário, financeiro. | ✅ **Concluído 2026-05-20** (aguarda teste batch) |
|
||||
| 3 | **Prontuário/Evolução** | Schema clínico já modelado (decisão 7). | ✅ **Foundation 2026-05-20** — repositories + composables prontos. Ativa quando migrations 0.5.B rodarem. UI da aba "Prontuário evolutivo" fica pra sessão dedicada. |
|
||||
| 4 | **Financeiro/Billing** | `useBillingOrchestrator` desenhado (decisão 8). | ✅ **Foundation 2026-05-20** — 3 repositories + 4 composables novos. State machine completa + migração dos 3 callers BLOQUEADA pelas decisões #2/#3/#6 — fica pra sessão dedicada. Old `useAgendaFinanceiro.js` + `useFinancialRecords.js` continuam em paralelo. |
|
||||
| 5 | **Multi-tenant + Tenantship** | Inclui implementar feature `tenantship/` (decisão 9). | ✅ **Concluído 2026-05-20.** MembersPage em `src/views/pages/admin/MembersPage.vue` (CRUD de membros + convites) + rota `/admin/members` registrada em `routes.clinic.js`. Migration `20260520000005_accept_tenant_invite_rpc.sql` criada (RPC SECURITY DEFINER com lock FOR UPDATE anti-race). `tenantInvitesRepository.acceptInvite` agora chama RPC real (não mais stub). SaasTenantFeaturesPage refatorada — 4 queries inline + 1 RPC extraídos pra `src/services/tenantFeatureAdminService.js`. **SetupWizardPage (2648 linhas) deferido** — refator de arquivo monolítico precisa sessão dedicada. |
|
||||
| 6 | **Notificações (WhatsApp/Email)** | Depende de tudo acima. | ✅ **Foundation 2026-05-20** — `noticesSelects.js` criado + `noticeService.js` refatorado pra usar constantes. `features/conversations/services/` com repository + selects. Channel factory + refactor de `useConversations.js` (fat composable) deferidos — sessão dedicada. SMS send ainda stub. |
|
||||
|
||||
#### Módulo 1 — sub-entregáveis
|
||||
|
||||
- [x] **M1.1** Criar `features/medicos/` (`_tenantGuards.js`, `medicosSelects.js`, `medicosRepository.js`, `useMedicos.js`) + refatorar `CadastroRapidoMedico.vue` pra usar repository (2026-05-20). Caller único (PatientsCadastroPage) — props/emits preservados. Zero referências `supabase`/`useTenantStore`/`getOwnerId`/`getTenantId` legacy no componente. **Testado pelo user ✅**
|
||||
- [x] **M1.2** Criar `features/insurance/` (`_tenantGuards.js`, `insurancePlansSelects.js`, `insurancePlansRepository.js` com `findByName` pra uniqueness check, `useInsurancePlans.js`) + refatorar `CadastroRapidoConvenio.vue` + `InsurancePlanQuickCreateDialog.vue` (bônus da agenda) (2026-05-20). Repository agora faz duplicate check case-insensitive antes de criar — quick-create blueprint compliance. Aguardando teste.
|
||||
- [x] **M1.3** Refatorado `ComponentCadastroRapido.vue` pra usar `usePatients` composable + `getMyActiveMember()` (novo helper em `tenantship/services/tenantMembersRepository.js`) (2026-05-20). Path NÃO mudou (continua em `src/components/`) — move pra `features/patients/components/` fica pra cleanup de M2. Props deprecated mantidas pra backwards-compat dos 8 callers. **Spillover M2:** fix `patientsRepository.createPatient` (audit alta — sempre injeta owner_id do uid logado, ignora payload) + `usePatients.error: ref('')` (audit média — canon). Aguardando teste.
|
||||
- [x] **M1.4** Extraído `TEST_ACCOUNTS` de `HomeCards.vue` pra `src/config/devTestAccounts.js` (2026-05-20). HomeCards importa do novo arquivo; 6 usages funcionam idênticos.
|
||||
- [x] **M1.5** Deletado `AgendaEventDialog.vue.bak` (não estava no git, 155KB órfão no disco) (2026-05-20).
|
||||
- [ ] **M1.6** Decomposição parcial `MelissaLayout.vue` (sessão dedicada — pode ficar pra depois)
|
||||
|
||||
#### Módulo 2 — sub-entregáveis (2026-05-20, sem pausas de teste)
|
||||
|
||||
- [x] **M2.1** Criar `patientsSelects.js` (11 constantes — PATIENTS_SELECT_BASE + cross-feature: sessões, financial, documents, messages, recurrences, support_contacts, groups, tags). Estender `patientsRepository.js` com 15 funções novas + `resolveTenantId` helper local. Cross-feature reads ficam no patients até M4/M6 padronizarem (documentado nos comments).
|
||||
- [x] **M2.2** `usePatientDetail.js` refatorado — 4 funções internas (getPatientById, getPatientRelations, getGroupsByIds, getTagsByIds) movidas pra repository. Composable agora é thin wrapper.
|
||||
- [x] **M2.3** `usePatientFinancial.js` refatorado — `_lastPatientId` movido DENTRO da function (audit alta resolvida: state não vaza mais entre instâncias). 4 mutations (load/markPaid/markUnpaid/createRecord) delegam ao repository.
|
||||
- [x] **M2.4** `usePatientSessions.js` refatorado — list+create+updateStatus via repository. Pattern virtual→materialize preservado usando `findSessionByRecurrence` + `createPatientSession` com recurrence_id/date. Recurrence expansion (useRecurrence) intacta. `supabase.auth.getUser()` mantido como context resolution (não é data query).
|
||||
- [x] **M2.5** Quatro composables simples refatorados em paralelo: `usePatientMessages`, `usePatientDocuments`, `usePatientRecurrences`, `usePatientSupportContacts`. Cada um delega list+mutations ao repository.
|
||||
- [x] **M2.6** Cleanups: `usePatients.js` upgraded pra Tipo A canônico completo (load/getById/create/update/remove com loading/error/re-throw consistente).
|
||||
|
||||
**Resultado:** zero `supabase.from(...)` em qualquer composable de `features/patients/composables/`. Todos os 8 composables seguem Tipo A do blueprint. `_lastPatientId` não vaza mais.
|
||||
| 2 | **Pacientes** | Núcleo de dados — alimenta agenda, prontuário, financeiro. |
|
||||
| 3 | **Prontuário/Evolução** | Schema clínico já modelado (decisão 7). Migration já aplicada. |
|
||||
| 4 | **Financeiro/Billing** | `useBillingOrchestrator` já desenhado (decisão 8). |
|
||||
| 5 | **Multi-tenant + Tenantship** | Inclui implementar feature `tenantship/` (decisão 9). |
|
||||
| 6 | **Notificações (WhatsApp/Email)** | Depende de tudo acima. |
|
||||
|
||||
**Por módulo, sempre:**
|
||||
1. Ler estado atual
|
||||
2. Diff vs blueprints
|
||||
3. Listar divergências em `dev_auditoria_items` (tag `padronizacao:<modulo>` + severidade)
|
||||
4. Plano de fix
|
||||
5. Executar
|
||||
6. Testar (persistir HANDOFF+wiki+memória **antes** do teste manual)
|
||||
7. Commit
|
||||
8. Atualizar tracker
|
||||
9. Log no wiki
|
||||
|
||||
### Fase 2 — Hotspots Graphify (paralela)
|
||||
|
||||
Do `project_graphify_findings_20260504`:
|
||||
- [ ] `convertToPatient` duplicado
|
||||
- [ ] Supabase client triplo
|
||||
- [ ] 348 nós fracos
|
||||
- [ ] Setup Wizard cohesion 0.05
|
||||
|
||||
### Fase 3 — Gaps de MVP (Fase 1 do ROADMAP)
|
||||
|
||||
- [🟡] **Gateway Asaas (Fase A foundation 2026-05-21)** — Design doc + 2 migrations (tables + RLS) + client service + 3 Edge Function stubs (create-payment-record, cancel-payment, sync-payment). Schema: `asaas_customers`, `asaas_payments`, `asaas_webhook_events` + 5 colunas em `payment_settings`. Fase B (implementação real) depende de credenciais + decisão modelo negócio (A/B/C). Ver `development/02-auditoria/DESIGN_ASAAS_GATEWAY.md`.
|
||||
- [🟡] **Compliance CFP (#5/#8/#9 done · #6/#7 deferred · 2026-05-21)** —
|
||||
- #5 (registro profissional): migration `20260521000003_profiles_professional_registration.sql` — adiciona `professional_registration_type` (CHECK 8 conselhos) + `_number` + `_uf`.
|
||||
- #8 (nome social): JÁ INTEGRADO — `patients.nome_social` schema existia + UI em 7 arquivos.
|
||||
- #9 (especialidades): `20260521000004_specialties.sql` (tabela + profile_specialties M:N + RLS) + `seed_050_specialties.sql` (33 specialties) + `src/services/specialtiesService.js`.
|
||||
- **#6 consent forms DEFERRED**: schema `document_templates` existe; falta seed + UI editor + workflow.
|
||||
- **#7 assinatura DEFERRED**: schema `document_signatures` existe com status flow completo; falta portal UI pra paciente.
|
||||
- [ ] E2E Playwright crítico (#16)
|
||||
- [ ] Sentry (#18)
|
||||
|
||||
### Fase 4 — Agenda residual (por último)
|
||||
|
||||
- [ ] Popover snapshot stale → `ev.id` + computed
|
||||
- [ ] Reverse transition confirm dialogs (realizado paid, faltou multa, pacote saldo)
|
||||
- [ ] Replicação Rail (AgendaTerapeutaPage) + Clínica (AgendaClinicaPage)
|
||||
- [ ] C12 antecipar — iterar UX
|
||||
- [ ] Doc de ajuda completa
|
||||
|
||||
---
|
||||
|
||||
## Decisões tomadas
|
||||
|
||||
### Estratégicas iniciais (2026-05-20)
|
||||
1. **Ordem dos módulos** ✅ Confirmada acima
|
||||
2. **Tracker** ✅ Reusar `dev_auditoria_items` com tag `padronizacao:<modulo>`. Zero migration. View materializada agrupa por módulo se virar útil pra UI.
|
||||
3. **Cadência** ✅ Validação por entregável (cada blueprint vira sessão antes de propagar)
|
||||
4. **Agenda intocada** ✅ Até Fase 4
|
||||
5. **Skills novas** Só `/audit-module <nome>` — minimalista. Outras só se emergir necessidade.
|
||||
|
||||
### Pós-audit baseline (2026-05-20)
|
||||
6. **Quick-create blueprint** ✅ **Promover pra universal** (3 candidates já existem fora da agenda). Refatorar `CadastroRapidoMedico.vue` + `CadastroRapidoConvenio.vue` + `ComponentCadastroRapido.vue` em paralelo no módulo 1.
|
||||
7. **Schema clínico do prontuário** ✅ **Modelar agora** (sessão dedicada antes da Fase 1). Sai com migration pronta de `patient_notes`/`clinic_sessions`/anamnese/evolução/plano terapêutico.
|
||||
8. **Overlap billing agenda ↔ financeiro** ✅ **Consolidar em 1 composable orquestrador** (`useBillingOrchestrator`). Resolve risco double-billing antes do refactor de Financeiro.
|
||||
9. **Convites/membership** ✅ **Feature separada `tenantship/`** com services + composables + página `/admin/members`. Semântica clara: gestão de membership.
|
||||
|
||||
---
|
||||
|
||||
## Antipadrão a evitar
|
||||
|
||||
- ❌ Refatorar tudo de uma vez (= nunca lançar)
|
||||
- ❌ Tocar agenda durante a sweep (= regressão garantida)
|
||||
- ❌ Pular o tracking em `dev_auditoria_items` (= perder rastreabilidade)
|
||||
- ❌ Aplicar blueprint sem antes auditar divergências (= refazer trabalho)
|
||||
|
||||
---
|
||||
|
||||
## Referências
|
||||
|
||||
- Blueprints: `D:\leonohama\AgenciaPsi.com.br\Sistema\agenciapsi-primesakai\blueprints/`
|
||||
- Agenda canônica: `src/features/agenda/services/` + `src/features/agenda/composables/useAgendaEvents.js`
|
||||
- Tracker: tabela `dev_auditoria_items` (UI em `/saas/desenvolvimento`)
|
||||
- Memória: `project_padronizacao_sweep.md`
|
||||
- ROADMAP de features (não confundir): `development/04-roadmap/ROADMAP.md`
|
||||
Reference in New Issue
Block a user