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,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)
|
||||
Reference in New Issue
Block a user