# 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 `