# HANDOFF — 2026-05-20 (C10 ✅ + C11 ✅ + C12 ⏳ deferido · testando C13)

Documento de continuidade. **Quando voltar, comece lendo esta página até o fim.**

> **🎯 SE A FORÇA CAIR / SESSÃO PERDER CONTEXTO:** C10 e C11 fechados.
> **C12 fluxo crítico OK no DB mas UX confusa** — adiado pra iterar
> pós-Rail/Clínica (memória project_c12_antecipar_iterar). Agora
> **testando C13** (edit cobrada — invariante imutabilidade SimplePractice).
> Implementação JÁ existe (Fase 6 do commit 1feb711 — Message com cadeado +
> AgendaEventoFinanceiroPanel embedded). Só validação visual + persistência.

> **🟢 14 COMMITS NO DIA**. C10 (5/5), C11 (4/4), C12 deferred (DB OK),
> reverse transition trava implementada, popover watch sync implementado.
> Pós-C13: replicar Rail (AgendaTerapeutaPage) + Clínica (AgendaClinicaPage)
> + iterar C12 UX + doc de ajuda (pendência separada).

### C13 — passos de teste (próximo)
Paciente: **João Almeida Martins** (sessão 20/05 9:00 realizada + paid R$ 40 maquininha) ou **André Green 20/05** (paid PIX).

Esperado ao abrir o AgendaEventDialog:
- Message azul com cadeado: "Cobrança de R$ X já emitida..."
- AgendaEventoFinanceiroPanel renderiza embaixo do Message
- Card "Aplicar alterações em" oculto (v-if="!occFinancialRecord")
- Só horário/observações editáveis; valor/serviços/tipo travados

### C11 sub-test results
| # | Teste | DB validado |
|---|---|---|
| 11A | Realizada + markPaid PIX | sessions_used 0→1, record paid R$ 40 PIX |
| 11B | Falta + Descontar saldo | sessions_used 1→2, sem multa |
| 11C | Falta + Multa SEM consumir | sessions_used stays 2, multa pending R$ 30 |
| 11D | Cancelado + default_consume_on_miss=true | sessions_used 2→3, sem multa (>2h) |

### Bugs descobertos + corrigidos durante C11
- UI "Como cobrar?" com options "Já recebi" misturadas → refatorado pra "Já recebi?" radio Sim/Não + select condicional
- `billing_contracts` sem coluna `updated_at` → UPDATE falhava silently em Promise.allSettled (root cause do saldo não incrementar). Trocado pra awaits sequenciais com error handling explícito
- Reverse transitions deixavam multa órfã → dialog reverse implementado com radio "cancelar pending" + "devolver saldo" + warning pra paid
- Botão "Gerar cobrança" em sessão encerrada → bloqueado
- Lock total em cancelado/faltou: Editar sessão some, status mudanças disabled exceto Agendada (recovery)
- Label "A cobrar R$ X" em pacote saldo state=none → "Aguardando uso do pacote"
- Badge $ amber em pacote saldo state=none → suprimido
- billing_contract_id não amarrado em alguns flows → link universal antes dos blocos forward
- Reverse saldo decrementar: refresh sessions_used FRESH do DB antes do UPDATE (anti-race)

### Pendências mapeadas pós-C13
- **Popover snapshot**: `eventoSelecionado.value = ev` é snapshot. Fix: guardar ev.id, derivar via computed
- ~~Reverse transitions~~ ✓ implementado ahead of schedule
- **Cleanup teste**: Otto sessão 5364f631 leftover (não-critical)

### C10 sub-test results
| # | Teste | DB validado | Notas |
|---|---|---|---|
| A | Realizada sem markPaid | ✅ status=realizado, record=pending | Bubble do C9 funcionou |
| A2 | Realizada + markPaid maquininha | ✅ status=realizado, record=paid, payment_method=cartao_maquininha, paid_at set | João Almeida |
| B | Faltou + multa R$ 30 (fixed_fee) | ✅ original cancelled + nova multa "Multa por falta · sessão dd/mm/aa" | Otto Rank |
| C | Cancelado >2h antecedência | ✅ original cancelled, sem multa | Otto / Karen |
| C2 | Cancelado tardio (<2h) full charge | ✅ original cancelled + nova "Taxa de cancelamento tardio · sessão dd/mm/aa" | Karen Horney |

### Pendências mapeadas durante C10 — pós-C13
- **Reverse transitions**: faltou/cancelado → agendado deixa multa órfã. Implementar confirm dialog oferecendo auto-cancelar multa.
- **Popover snapshot**: `eventoSelecionado.value = ev` é snapshot, não acompanha _paymentStateMap. Fix: guardar ev.id, derivar via computed.
- **Cleanup teste**: Otto sessão 5364f631 às 19:30 UTC tem record pending R$ 40 leftover do teste A original. Apagar quando convenient.

Memórias relevantes:
- `project_agenda_reverse_transitions.md`
- `project_melissa_popover_snapshot.md`

### Code-fix aplicado em 20/05 (pré-C10)
- **`useMelissaAgenda.js:1450-1505`** — `_applyStatusDecisions` agora cancela
  o `ctx.pendingRecord` quando faltou/cancelado (com ou sem multa). Antes
  inseria a multa mas DEIXAVA o original pending → cobrança dupla
  (R$ 200 + R$ 30 = R$ 230). Audit trail vai em `notes` do record
  cancelado, descrição da multa nova carrega data: "Multa por falta · sessão 20/05/26".
- **`useAgendaFinanceiro.js:59`** — fix dormente `'fixed'` → `'fixed_fee'`
  (off-by-key contra schema; path nunca exercitado na Melissa, mas iria
  quebrar se algum dia fosse).

### Financial exceptions seedadas (tenant Bruno Terapeuta / owner Leonardo)
- `patient_no_show` → `fixed_fee R$ 30`
- `patient_cancellation` → `full`, `min_hours_notice=2`, `default_consume_on_miss=true`

---

## 🔴 PRÓXIMO PASSO IMEDIATO — Cenário 10 (Status change AVULSA)

Doc HTML diz: testar status change numa sessão avulsa com cobrança pendente,
mudando entre realizado / faltou / cancelado. As consequências financeiras
seguem `financial_exceptions` (regras configuradas pelo terapeuta sobre o
que acontece com a cobrança nesses casos).

Possíveis pacientes pra teste: usar Joyce, Sándor ou outro com cobrança
avulsa pendente já criada.

**Esperado** (depende das `financial_exceptions` configuradas no tenant):
- Realizada: status muda; cobrança permanece (caminho default)
- Faltou: pode ter regra → cobrança 100% (paciente paga falta) ou cancela
- Cancelado: pode ter regra → cancelar cobrança ou cobrar parcial

Conferir:
- `STATUS_TO_EXCEPTION` mapping em `useAgendaFinanceiro.js`
- `getFinancialExceptionRule(tenantId, exceptionType)` retorna a regra
- `handleStatusChange` orquestra: agenda update + financial adjust

Após C10: C11 (status change pacote saldo — usar a infra do Usar/Revogar)
→ C12 (antecipar pagamento) → C13 (edit cobrada).

Quando todos passarem, replicar em **Rail** (`AgendaTerapeutaPage.vue`) e
**Clínica** (`AgendaClinicaPage.vue`).

---

## 📦 O que foi feito em 20/05 madrugada (C9 + rowGroup financeiro + bubble cobranca-atualizada)

### Cenário 9 ✅ (Per-session — Michael Balint 12 × R$ 150)
Testado e passou. Criou-se 1 rule + 12 agenda_eventos materializadas + 12 financial_records pending. Sem billing_contract. Cada sessão com badge $ amber individual. **Sem nenhuma `linha de pacote`** no popover (não tem contract → não aparece). Conforme esperado.

### `/melissa/financeiro-lancamentos` agrupado por paciente
- DataTable com `rowGroupMode='subheader'` + `groupRowsBy='patient_id'`
- Default: todos os grupos da página expandidos (watcher popula `expandedGroups` com unique patient_ids quando `recordsGrouped` muda)
- Header de grupo: avatar pequeno + nome + badge "N lançamento(s)"
- Click no chevron contrai/expande (auto via PrimeVue `expandableRowGroups`)
- Sort estável: ordena outer por nome do paciente, preserva inner order (pai → filhos de multas/taxas)

### Bubble-up `@cobranca-atualizada`
Antes: `AgendaEventoFinanceiroPanel.@cobranca-atualizada` disparava só `loadOccFinancialRecord` (interno do dialog). O `_paymentStateMap` da agenda ficava stale → card no FC só atualizava ao trocar de view.

Agora: `AgendaEventDialog._onCobrancaAtualizada` faz duas coisas:
1. `loadOccFinancialRecord()` — refresca estado interno do dialog
2. `emit('cobranca-atualizada')` — bubble pra MelissaLayout

MelissaLayout escuta nos 2 dialogs (principal + occurrenceMode) e chama `onCobrancaAtualizada` que dispara `M.refetch() + refetchEventosHoje()`. Resultado: card na agenda passa pra borda verde imediatamente após marcar pago.

---

## 📦 O que foi feito em 19/05 madrugada (C8 + Usar/Revogar saldo + UI de pacote)

### Cenário 8 ✅ (Pacote SALDO — Otávio Souza Ferreira 12 × R$ 50)
Testado e validado. Contract criado com `charging_style='saldo'`, 0 events materializadas, 0 records. Modelo Cliniko: sessões materializam on-demand via Usar.

### UI do pacote (saldo + upfront)
- **`_ruleContractMap`** em useMelissaAgenda: bulk-load agora popula contract info (id, style, totalSessions, sessionsUsed, packagePrice) por `recurrence_id`. Query usa `recurrence_rules.patient_id` como fonte autoritativa (cobre saldo sem materializadas).
- **Normalize** injeta `contract` no evento → popover acessa via `ev.contract`.
- **Popover** (`MelissaEventoPanel`): nova linha colorida abaixo do payment:
  - Saldo: violeta `"Pacote saldo · N/M usadas"` + botão verde **"Usar"** (paymentState=none) OU vermelho **"Revogar"** (paymentState=pending)
  - Upfront: verde `"Pacote · N/M realizadas"` (sem botão; cobrança já tratada)
- **AgendaEventDialog**: info card mt-4 (saldo violeta / upfront emerald) com header (pacote+contador), body (total/per-session/restam), botão "Usar agora" ou "Revogar uso", hint explicando o modelo. Gateado por `occFinancialLoading` (spinner durante carga) pra evitar piscar entre estados.

### Handlers Usar/Revogar atômicos
**`onUsarSessao`** em MelissaLayout (aceita payload do popover OU do dialog):
1. Materializa virtual se necessário (preserva `determined_commitment_id` da regra)
2. Status='realizado' + link `billing_contract_id`
3. `create_financial_record_for_session` RPC com per-session amount
4. Incrementa `billing_contracts.sessions_used`
5. Se atingiu total → contract `status='completed'`
6. Toast verde + fecha popover/dialog

**`onRevogarSessao`** desfaz tudo:
1. Cancela financial_record (status='cancelled')
2. Decrementa sessions_used (não fica negativo)
3. Reativa contract se estava completed
4. Status volta pra 'agendado'
5. Bloqueia se record já está `paid` (precisa estorno formal pelo Financeiro)
6. **Backfill** de `determined_commitment_id` se NULL (fix de legado)

### Fix: enum status_evento_agenda
Era `'realizada'` no insert/update, DB exige `'realizado'` (masculino). Corrigido em todas as ocorrências.

### Fix: campo "Título" indevido no dialog
Sessão sem `determined_commitment_id` → `selectedCommitment=null` → `isSessionEvent=false` → mostra campo Título (que é só pra não-sessão). Fix: 
- Materialize do Usar inclui `determined_commitment_id` da regra
- Update path do Usar (sessão real após revogar) backfilla via query da rule
- Revogar também backfilla — garante consistência mesmo sem novo Usar
- SQL massivo de backfill disponível no HANDOFF pra limpar rows legadas

### Fix: "Gerar fatura" não cabe em sessão de saldo
Hide do botão "Gerar fatura" no popover quando há `contractInfo`. Geraria cobrança solta sem incrementar saldo → duplicação. Fluxo correto: usar "Usar".

### Recorrências Aplicadas: cores + badges
- Header stats: total **azul**, realizadas **verde**, faltaram **amber**, canceladas **cinza**, remarcadas **violeta**
- Pills: badge sólido por status (Realizado=emerald-600, Faltou=amber-600, Cancelado=stone-500, Remarcado=violet-600)

### Race condition no dialog
- AgendaEventDialog mostrava botões "Usar"/"Revogar" baseado em `occFinancialRecord` que carrega async
- Durante load (~500ms), botão errado podia aparecer → snap pro correto depois
- Fix: spinner "Verificando estado…" enquanto `occFinancialLoading=true`; botões só renderizam após
- Popover decidiu manter como está (race window pequena, fechar/reabrir resolve)

---

## 📦 O que foi feito em 19/05 noite (C7 + lock-edit + propagação cross-week)

### Cenário 7 ✅ (Pacote UPFRONT — Ana Souza Ferreira)
Testado e validado. Usuária criou Ana, R$ 200/sessão × 4 = R$ 800, marcou como pago em dinheiro pelo Financeiro. Visualização correta em mês AND em semana navegando pelas 4 semanas.

### Fase 6 (lock-edit cobrada) ativada em Melissa
Antes: `loadOccFinancialRecord` tinha guard `if (!props.occurrenceMode) return;` — só carregava em Rail/Clínica (edição de ocorrência). Em Melissa, `sessionPaymentRecord` paralelo alimentava só o Resumo lateral, sem trigger de lock.

Agora unificado: `occFinancialRecord` carrega em ambos modos:
- Card Sessão / Honorários ganha **Tag** (em vez de Select billingType) quando há cobrança
- Body do card mostra **Message "Cobrança de R$ X já emitida"** + cadeado
- Tipo de cobrança (Particular/Convênio/Gratuito) bloqueado
- Edição de serviços/preço bloqueada

### Propagação cross-week de pacote upfront pago/pendente
**Bug descoberto durante C7:** ao navegar pra semanas futuras (onde só virtual da Ana 2/3/4 aparecia, sem real event paid na view), o `_rulePaymentMap` era zerado pelo else branch do bulk-load → virtuais perdiam estado paid.

Fix em `useMelissaAgenda.js _reloadRange`:
- Maps (paymentStateMap, amountMap, rulePaymentMap) inicializados SEMPRE no início
- Propagação agora roda **independente de realIds.length** (ie, mesmo em semanas só-com-virtuais)
- Coleta `ruleIdsInView` de TODOS eventos da view (reais + virtuais com recurrence_id)
- Cross-week query: pra cada rule em view, busca QUALQUER evento sibling (inclusive em outras semanas) + seus records paid/pending → determina estado do contrato
- Propaga estado pra eventos reais (via map) + virtuais (via rulePaymentMap acessado pelo normalize)

### Atalho "Gerar fatura" no popover
- Pill amber pequeno ao lado de "A cobrar R$ X" no popover (`paymentVariant === 'none' && !is_occurrence`)
- Click → `gerarCobrancaManual` direto, fecha popover pra impedir double-click
- Tooltip: "Gerar fatura agora"

### Info de pacote no popover
- Header agora mostra `Sessão · Pacote · N sessões` (computed `seriesLabel` lê de `_raw` do rule)

### Botão "Excluir série inteira"
- Novo emit `delete-series` em `MelissaEventoPanel` + botão ao lado de "Excluir sessão" quando evento tem `recurrence_id`
- Handler `onDeleteSeries` em MelissaLayout faz hard delete: `financial_records` pendentes → `agenda_eventos` materializados → `recurrence_rules` (CASCADE leva exceptions + rule_services)
- Bloqueia se algum record tem `status='paid'` (estornar primeiro)

### RPC `create_financial_record_for_session` ignora cancelled
**Migration 20260519000001:** idempotência da RPC passou a filtrar `AND status != 'cancelled'` além de `deleted_at IS NULL`. Antes: cancelar cobrança sem querer → todo "Gerar fatura" subsequente retornava o cancelado em vez de criar nova. Toast verde mentindo.

Memória durável em `memory/project_rpc_idempotency_cancelled.md`.

### `cancel_session` exception some da agenda
- `useRecurrence.expandRules` agora pula ocorrência com `exception.type === 'cancel_session'` (era visível com status cancelado; doc dizia "some da agenda" mas código mantinha)
- `patient_missed` / `therapist_canceled` / `holiday_block` permanecem visíveis como histórico

### `recurrence_exceptions` cancel idempotente
- Cancel de ocorrência (virtual e materializada) usa `upsert` com `onConflict: 'recurrence_id,original_date'` — não quebra mais com unique violation quando há exception zumbi de tentativa anterior.

### Visualização paid/pending de upfront em virtuais
- `MelissaEventoPanel.showPaymentRow` antes excluía virtuais incondicionalmente. Agora só esconde quando `paymentState === 'none'` (saldo/sem pacote continua limpo; upfront propagado mostra).
- `MelissaAgenda.fcEvents`: removida exigência de `!is_occurrence` no `isPaidSession` e no badge $ pendente. Virtuais herdadas via propagação mostram borda verde/badge amber.

### `onVerLancamentos` cobre virtual de upfront
- Antes: virtual sempre toast "Sem lançamentos". Agora: busca records via siblings da série pra encontrar o do pacote. Saldo/sem pacote continua com toast.

### Confirmação 3 decisões UX (não codar)
Antes de C7, user perguntou e concordou:
1. Editar serviço já lançado e pago → **NÃO** (cobrança fiscal imutável)
2. Alternar Particular/Convênio/Gratuito em série com cobrança ativa → **NÃO** (mesma razão)
3. "Gerar fatura" extra em sessão coberta por contrato upfront → **NÃO** (duplicaria cobrança)
Tudo isso o lock-edit (Fase 6 ativada acima) cobre.

---

## 📦 O que foi feito em 18/05

### Cenário 4 (Joyce · "Já recebi") ✅
- Testado e passou: toast "Cobrança paga R$ 180,00 recebido via PIX", record nasceu `paid + payment_method=pix + paid_at=now()`.

### Novo indicador: barra esquerda verde para sessão paga
- Brainstorm de 6 opções; user escolheu #6 (3 canais visuais distintos por estado).
- `MelissaAgenda.vue:395-419` — computa `isPaidSession` (sessão+paciente+não-virtual+`paymentState==='paid'`) e adiciona classe `ma-evt--paid` ao FC event (combina com `ma-evt--inactive-patient` se ambos).
- `MelissaAgenda.vue:2325-2335` — CSS força `border-left-color: #10b981 !important` (emerald-500, 4px). `!important` necessário porque FC seta `borderColor` inline. Trata também list view (`.fc-list-event-dot`).
- Doc HTML atualizado: legenda "Indicadores visuais" agora descreve **3 estados** (pendente / pago / neutro) com 3 mocks empilhados; estado-alvo do C4 reescrito mencionando a barra verde.
- Decisão salva em `memory/project_agenda_payment_indicators.md`.

### Linha "Cobrança" no popover + Resumo do dialog
- **Popover `MelissaEventoPanel`** — antes só mostrava amber "A receber R$ X" pra pendente. Agora cobre os 3 estados, com cor + ícone por variante:
  - `paid` → `pi-check-circle` verde, label **"Pago · R$ X,XX"**
  - `pending` → `pi-dollar` amber, label **"A receber R$ X (cobrança pendente)"** (mantido)
  - `none` → `pi-dollar` amber, label **"A cobrar R$ X"** ou **"Cobrança ainda não gerada"** (mantido)
  - CSS reescrito em 3 modificadores `.evento-row--pay-{paid|pending|none}` (com dark mode).
- **Resumo lateral do `AgendaEventDialog`** — nova linha entre `pi-clock` e `pi-map-marker` em ambas as cópias (mobile inline + desktop floating).
  - Novo ref `sessionPaymentRecord` em `useAgendaEventLifecycle.js:104+` (sem guard de `occurrenceMode`, contrário ao `occFinancialRecord` que continua só pra Rail/Clínica). Loader `loadSessionPaymentRecord` chamado no mesmo lifecycle.
  - Computed `paymentSummary` em `AgendaEventDialog.vue:951+` retorna `{icon, cls, label}` pra 5 casos: paid (verde + paid_at), overdue (vermelho + due_date), pending (amber + due_date), sem cobrança c/ valor (neutro), sem cobrança s/ valor (neutro).
  - `@cobranca-atualizada` do `AgendaEventoFinanceiroPanel` agora também dispara `loadSessionPaymentRecord` pra a linha refrescar.
  - **Importante:** `occFinancialRecord` (que aciona lock-edit) NÃO foi tocado de propósito — esse é território da Fase 6/C13 (Edit cobrada). Manter dois refs separados evita ativar lock prematuro em Melissa.

### Preparação do C5 (Sándor + Unimed Nacional) — UX de convênio refinado (3 issues)

User tentou rodar C5 e bateu em 3 problemas seguidos. Cada um virou um fix:

1. **Botão "Cadastrar" do procedimento navegava pra `/pages/notfound`**
   - Root cause: `goToConveniosConfig` em `AgendaEventDialog.vue` prefixava com `/therapist` ou `/admin`, mas `/configuracoes/*` é rota **raiz** sob `AppLayout` (sibling, não filho). Em Melissa, convênios mora dentro do próprio layout via `secao: 'cfg-convenios'` (sem URL própria).
   - Fix descartado: o user não queria sair da agenda. Em vez disso, criamos um quick-create inline (ver #2). `goToConveniosConfig` foi removida (dead code virou armadilha).

2. **Quick-create de procedimento inline (sem sair da agenda)**
   - Novo componente `InsurancePlanServiceQuickCreateDialog.vue` (modelo do `InsurancePlanQuickCreateDialog`). 2 campos: nome do procedimento + valor que o convênio paga. Insere em `insurance_plan_services` pro `insurance_plan_id` ativo.
   - Wiring em `useAgendaEventLifecycle.js`: novo `planServiceQuickDlgOpen` + `openPlanServiceQuickCreate()` + `onPlanServiceCreated(service)`. Após criar, recarrega `loadInsurancePlans` e **auto-seleciona** o novo procedimento **só quando nada estava selecionado antes** (preserva escolha quando user já tinha selecionado X e está só cadastrando Y pra próxima).
   - UI refatorada (`AgendaEventDialog.vue:3110+`): a caixa cinza com botão "Cadastrar" agora aparece **sempre** que um convênio está selecionado. Quando 0 procedimentos: **"Este convênio ainda não tem procedimentos cadastrados."** Quando 1+: **"Se quiser adicionar mais procedimentos a este convênio:"**.
   - `planServiceQuickDlgOpen` adicionado ao `anyChildDialogOpen` pra esconder o Resumo flutuante enquanto o quick-create está aberto.

3. **Botão "+ Novo convênio" faltando em `/melissa/cfg-convenios` (e na rota canônica também)**
   - Root cause: `ConfiguracoesConveniosPage.vue` tinha o form de "Novo convênio" condicionado a `addingNew === true`, mas **nenhum botão setava esse flag**. Empty state mandava "Clique em 'Novo convênio'" sem botão pra clicar.
   - Fix: toolbar simples no topo do template `<template v-else>` com `<Button label="Novo convênio" icon="pi pi-plus" @click="addingNew = true">`. Empty state corrigida pra apontar pro botão certo.

### Hint contextual abaixo do card Sessão / Honorários

- User pediu mensagem clarificando que "Nº da guia" é opcional em convênio.
- **Tentativa 1 (errou o lugar):** coloquei o hint em `AgendaEventDialog.vue:1826` dentro do bloco `v-if="occurrenceMode"` (só edita ocorrência em Rail/Clínica). User não viu.
- **Tentativa 2 (correta):** adicionado em `AgendaEventDialog.vue:2305+` (fluxo principal Melissa, fora do occurrenceMode). Mantive a tentativa 1 também — não atrapalha, só ativa em outro contexto.
- Texto: convênio = **"Nº da guia é opcional — você pode salvar a sessão e preencher depois, quando o convênio responder."** Gratuito = **"Sessão gratuita — nenhum lançamento será gerado no Financeiro."** Particular = sem hint (não há ambiguidade).
- Condição: `isSessionEvent && !occFinancialRecord && billingType === 'convenio'|'gratuito'`. Esconde quando há cobrança paga/pendente (lock-edit) — Message do panel já cobre.
- CSS: `.aed-billing-hint` em `AgendaEventDialog.vue:3558+` — barra esquerda primary, fundo neutro leve, fonte 0.78rem.
- Label do "Nº da Guia" no service-picker dialog também ganhou **(opcional)**.

---

## 📦 O que foi feito antes (16/05 noite/madrugada)

### Cenário 1 (Bloqueio) ✅

1. **Fix `bloqueioCobrindo is not defined`** — função estava no escopo de `useMelissaAgenda` mas `onSelectTime` mora no `_buildHandlers` (outro escopo). Passada via `deps`. Mesmo padrão que `_openStatusDialog`.
2. **Soft warn dentro do dialog** em vez de toast atrás do overlay — novo ref `dialogBlockOverlap` no composable + nova prop `blockOverlapWarning` no `AgendaEventDialog` + Message warn no topo do step 1. Reset nos outros openers (`onCreateEvento`, `onCreateEventoForPatient`, `onEditEvento`).
3. **Doc HTML Cenário 1 expandido** em 1a (criar bloqueio) + 1b (agendar sobre bloqueio), com mock visual da Message + comparação com agendador público (que veta).

### Cenário 2 (Avulsa sem cobrança) ✅

4. **Fonte da hint chargeMode** subiu de `0.72rem` → `0.8125rem` (acima de `text-xs`).
5. **Card Frequência avulsa** refeito — antes era empty state convidando configurar; agora renderiza com `.aed-pay-summary` (mesma estrutura do estado configurado: "Tipo: Avulsa · Sessão única, sem repetição" + botão Editar).
6. Doc HTML Cenário 2 atualizado.

### Cenário 3 (Avulsa cobrar ao salvar) ✅

7. **Refactor payment: `paymentSettlement` → `paymentMethod` + `markPaidNow`**
   - UI antiga misturava método e status num único Select ("Já recebi — PIX").
   - Agora 2 controles: Select forma (Enviar link / PIX / Dinheiro / Depósito / Cartão maquininha — SEM prefixo "Já recebi —") + SelectButton status (Cobrança pendente / Já recebi (dar baixa)).
   - SelectButton só aparece quando método ≠ link (Asaas só liquida via webhook).
   - Watcher força `markPaidNow=false` se voltar pra 'link'.
   - Wire: AgendaEventDialog → useAgendaEventActions → useMelissaAgenda (handler avulsa + `_createPackageContract`).
8. **Indicadores visuais de pagamento** (novidade da sessão):
   - Bulk-load de `financial_records` em `_reloadRange` etapa 4 (1 query única, mapa eventId → 'paid' | 'pending' | 'none').
   - `normalizeForMelissa` agora injeta `paymentState` + `price` no evento.
   - **Badge $ no canto** dos eventos da agenda — círculo amber 16px no canto superior direito. Só pra sessão + paciente + não-virtual + paymentState !== 'paid'.
   - **Linha "A receber"** no popover (`MelissaEventoPanel`) — texto adaptativo: "A receber R$ X (cobrança pendente)" se pending, "A cobrar R$ X" se none, "Cobrança ainda não gerada" se sem valor.
9. **🐛 Bug fix `pickDbFields` faltando `modalidade`** — sessões avulsas eram salvas sem modalidade, DB caía no default 'presencial' independente da escolha. Adicionado ao whitelist em `useMelissaAgenda.js:74`. **TODAS as sessões avulsas criadas no Melissa antes desse fix estão como 'presencial' no DB** — pode precisar rodar UPDATE manual no banco se quiser corrigir histórico. Gotcha salvo em `memory/project_pickdbfields_whitelist.md`.
10. **Doc HTML atualizada amplamente**:
    - Nova seção topo `★ Indicadores visuais de pagamento` com mocks (badge $ + linha popover) e link em violeta no TOC.
    - Caixa violeta "Indicadores visuais" em cada cenário relevante (C2-C9).
    - C4 ganhou caixa verde "estado-alvo" (sem badge, sem linha — pago).
    - Receita do C3 e C4 atualizadas com os 3 controles (Cobrança ao salvar / Forma de pagamento / Status do pagamento) e opções limpas (sem prefixo "Já recebi —").

---

## 🧭 Onde estamos no plano de 9 fases

| Fase | Status |
|---|---|
| **1** Compromisso SEM paciente | ✅ |
| **2** Compromisso COM paciente | ✅ testado (C1-C3 done) |
| **3** Recorrência + replicar occurrenceMode Rail/Clínica | ⏳ |
| **4** Modo disparo cobrança híbrido | ⚠️ parcial |
| **5** Status change → confirm dialog | 🔄 Melissa codado + indicadores visuais done; falta testar (C10-C12) + replicar Rail/Clínica |
| **6** Edit cobrada | ✅ |
| **7** Pagamento separado | ⏳ |
| **8** Refund/credit note | ⏳ |
| **9** Plano Inicial | 📋 |

---

## 📋 Roteiro de testes restantes (`src/docs/agenda-compromisso-financeiro-cenarios.html`)

| # | Cenário | Status |
|---|---|---|
| 1 | Bloqueio (criar + agendar sobre) | ✅ |
| 2 | Avulsa sem cobrança | ✅ |
| 3 | Avulsa cobrar ao salvar | ✅ |
| 4 | Avulsa "já recebi" no salvar | ✅ |
| 5 | Avulsa convênio (Sándor + Unimed) | ✅ |
| 6 | Recorrente sem pacote (Maria Magali / Anna Freud) | ✅ |
| 7 | Pacote upfront (Ana Souza Ferreira 4 × R$ 200) | ✅ |
| 8 | Pacote saldo (Otávio 12 × R$ 50) | ✅ |
| 9 | 1 por sessão (Michael Balint 12 × R$ 150) | ✅ |
| **10** | **Status change avulsa (realizado/faltou/cancelado)** | 🔴 **PRÓXIMO** |
| 10 | Status change avulsa (realizado/faltou/cancelado) | ⏳ |
| 11 | Status change pacote saldo | ⏳ |
| 12 | Antecipar pagamento (Carl Jung) | ⏳ |
| 13 | Edit cobrada | ⏳ (parcialmente — lock ativo em Melissa pós-19/05 noite) |

---

## 📋 Como retomar amanhã (cego)

1. `git status` — confirmar working tree intacto
2. **Ler HANDOFF até o fim**
3. Abrir `src/docs/agenda-compromisso-financeiro-cenarios.html` no browser pra ver o estado atual do doc viva
4. **Começar pelo Cenário 4** (Joyce, "Já recebi (dar baixa)")
5. Cada cenário que passar:
   - Atualizar status pra ✅ aqui no HANDOFF
   - Se descobrir bug ou texto divergente, corrigir código + doc na hora
6. Quando todos os 13 passarem: replicar em **Rail** e **Clínica**
7. Adicionar `professional_cancellation` no `STATUS_TO_EXCEPTION`
8. Marcar Fase 5 como ✅
9. Decidir Fase 4 (modo disparo cobrança híbrido) OU Fase 3 (replicar occurrenceMode)

---

## 🚨 Pendência IMPORTANTE — não esquecer

**Pós-Fase 9** (quando concluirmos TODAS as fases 1-9):
- User vai passar prompt específico pra criar **documentação completa da parte de ajuda** do sistema
- Está em `memory/project_pendencia_doc_ajuda.md`
- O doc `agenda-compromisso-financeiro-cenarios.html` já está sendo escrito de forma que vira a doc final pra usuário (cada teste validado vira parte da doc)

**Histórico modalidade='presencial' no DB:**
- Bug do `pickDbFields` afetou TODAS as sessões avulsas criadas no Melissa até 16/05/2026
- Se quiser corrigir histórico, rodar UPDATE manual identificando sessões cuja modalidade visual era online (não há como saber retroativamente — perdido)
- Going forward o fix já cobre

---

## ⚠️ Gotchas duráveis (atualizados)

- **`MelissaBloqueios.vue` admin ≠ `BloqueioDialog` (4 modos)** — casos distintos
- **`agenda_excecoes` foi dropada** em 13/05
- **`financial_records.type` undefined sem `type` no BASE_SELECT** — fix 14/05 cedo
- **`financial_records.description` undefined sem `description` no BASE_SELECT** — fix 14/05 noite
- **`handleStatusChange` em `useAgendaFinanceiro.js` está ÓRFÃO** — não reativar
- **`_openStatusDialog` + `bloqueioCobrindo` + `dialogBlockOverlap`** declarados no `useMelissaAgenda` mas usados em `_buildHandlers` — passados via `deps`. **NÃO ESQUECER ao replicar em Rail/Clínica**
- **`billing_contracts.charging_style`** distingue upfront/saldo/per_session
- **Ocorrência virtual tem `id="rec::<rule>::<date>"`** — detectar via `typeof === 'string' && startsWith('rec::')` antes de query Supabase
- **`chargeMode` default dinâmico:** `'session'` em avulsa, `'none'` em recorrente
- **Toast atrás do overlay do dialog** — usar Message no topo do dialog em vez de toast quando contexto for dentro de dialog modal
- **Cuidado com `pickDbFields` whitelist** — `useMelissaAgenda.js:74` descarta campos não listados silenciosamente. Sintoma: campo escolhido na UI mas DB tem valor default. Memória: `memory/project_pickdbfields_whitelist.md`
- **`paymentSettlement` foi renomeado** em 16/05 — agora `paymentMethod` (string) + `markPaidNow` (bool). Handler aplica `payment_method` sempre, `status='paid'` só quando markPaidNow=true && method!='link'
- **Bulk-load de paymentState em `_reloadRange` etapa 4** — 1 query única em `financial_records` mapeada por `agenda_evento_id`. Anota `paymentState` no normalize. Badge na agenda + linha popover lêem daqui

---

## 🧠 Decisões persistidas (memory/)

**Indicadores visuais (16/05):**
- Badge $ no canto: só sessão + paciente + não-virtual + !paid
- Linha popover: 3 textos (a receber pending / a cobrar none / cobrança não gerada)
- Bulk-load 1x por _reloadRange, não query por evento
- Ocorrências virtuais sempre paymentState='none' (cobertas por contrato)

**Payment refactor (16/05):**
- Separar método (forma) de status (já pago?) — controles independentes na UI
- Método 'link' (Asaas) força markPaidNow=false (gateway externo)
- Wire format: `arg.paymentMethod` + `arg.markPaidNow` (no lugar de `arg.paymentSettlement`)

**Bugs evitar repetir:**
- Sempre adicionar campo novo ao `pickDbFields.allowed` quando adicionar coluna em agenda_eventos
- Sempre adicionar campo novo ao `BASE_SELECT` quando query custom
- Detectar `is_occurrence` ou `rec::` antes de query por UUID
- Refs/funções do composable principal NÃO ficam acessíveis em `_buildHandlers` — passar via `deps`
- Toast dentro de dialog modal fica atrás do overlay — usar Message
