f94a4ae97f
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>
481 lines
22 KiB
Markdown
481 lines
22 KiB
Markdown
# 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)
|