Commit Graph

470 Commits

Author SHA1 Message Date
Leonardo 6383a550a6 log: handoff + runbooks 2026-06-13 20:50:11 -03:00
Leonardo c3dac5eeec docs: handoff completo (onde paramos + riscos + passo a passo de teste)
docs/HANDOFF_E_TESTES.md consolida os dois epicos:
- estado atual (branches, banco local, o que falta em cada um)
- TODOS os riscos (3 criticos, 6 importantes, 5 menores)
- passo a passo de teste local (0..7) cobrindo schema-per-tenant + freemium

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:49:44 -03:00
Leonardo 9acce9c19d log: runbook schema-per-tenant 2026-06-13 20:46:56 -03:00
Leonardo 91b89b7b5d docs: runbook de deploy da schema-per-tenant (hosted)
docs/DEPLOY_SCHEMA_PER_TENANT.md — pre-requisito do freemium. Cutover em fases:
- estrategia copia-nao-move (public + schemas coexistem ate o DROP)
- Risco #1 hosted: exposicao dinamica de schemas no PostgREST (ALTER ROLE
  authenticator) + fallback Exposed schemas no dashboard
- Fase A migrations aditivas / B manual privilegiados / C pgrst dinamico (testar
  cedo) / D migracao de dados (janela) / E frontend+edge / F smoke+soak /
  G F6.3 DROP (gated, irreversivel)
- rollback por fase (botao de panico = redeploy do codigo antigo ate o DROP)
- freemium pode entrar apos as Fases A-F, sem depender do DROP

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:46:56 -03:00
Leonardo 1082123967 freemium F4: referencia o runbook no wiki/log
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:42:47 -03:00
Leonardo 2f72886d4b freemium F4: runbook de deploy hosted
docs/DEPLOY_FREEMIUM_F4.md — passo a passo ordenado:
- PRE-REQUISITO #0: schema-per-tenant precisa estar no hosted antes (freemium
  depende de tenant_schemas/clone/is_saas_admin/pgrst schemas)
- migrations 05/06/07 + 5 manual supabase_admin (ordem + nota de permissoes hosted)
- Auth dashboard (confirm email + redirect URLs + SMTP GoTrue)
- deploy das edges recover-access/send-welcome-email + secrets SMTP
- rebuild front + smoke test (8 passos) + rollback/kill-switch

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:41:28 -03:00
Leonardo 403b7234a9 freemium F2: registra polish concluido no wiki/log
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:38:07 -03:00
Leonardo 52c34cf63a freemium F2 polish: welcome email + plano gratuito na vitrine
- edge function send-welcome-email: e-mail de boas-vindas ao DONO do tenant
  recem-provisionado (destinatario do JWT, SMTP global/sistema, defaults Mailpit).
  Best-effort, disparada fire-and-forget no OnboardingPage so no provisionamento novo.
- vitrine: seed plan_public + bullets dos planos free (cartao "Gratis"); Landingpage
  passa a mostrar "Gratis para sempre" (isFreePlan) em vez de "—".
- build OK

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:36:33 -03:00
Leonardo f6470718b7 freemium F3: registra conclusao no wiki/log
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:26:42 -03:00
Leonardo 3730b71150 freemium F3: UI de config (blacklist CRUD + toggle root_redirect)
- SaasAppConfigPage + rota /saas/app-config + menu "Config / Bloqueios"
- gerencia blacklist (email/slug, add/remove) e o root_redirect
- build OK

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:26:40 -03:00
Leonardo d50073da1a freemium F3: frontend dos extras (usuarios, esqueci-email, root_redirect, sino)
- SaasUsuariosPage + rota /saas/usuarios + menu: 1 linha/tenant com dono/slug/
  email/plano, realce verde + selo "Novo" 24h (saas_list_account_owners)
- esqueci-email no Login: dialog que chama a edge recover-access (acha dono por
  slug, manda magic link, mostra so dica mascarada). Edge function recover-access.
- root_redirect: guard roteia "/" do visitante nao-logado pra /lp ou /auth/login
  conforme get_root_redirect (cache TTL 5min)
- pegadinha #4: notificationStore.reset() no logout (limpa sino ao trocar user)
- build OK

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:21:46 -03:00
Leonardo 03790ecb9e freemium F3c: root_redirect (config + RPC publica)
- saas_app_config singleton (root_redirect landing|login, RLS saas_admin write)
- get_root_redirect() anon-callable; default 'landing'
- guard/front usam pra rotear "/" do visitante nao-logado (front na sequencia)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:02:40 -03:00
Leonardo cb153165c3 freemium F3b: /saas/usuarios (donos) + notify_all_devs
- saas_list_account_owners(): 1 linha/tenant com dono (master), nome/slug/email/
  plano + selo "novo" (24h). Dev-only (is_saas_admin), cast text p/ varchar.
- notify_all_devs() insere em notifications_sistema p/ cada saas_admin
- trigger AFTER INSERT/UPDATE OF status em subscriptions avisa os devs com
  deeplink /saas/usuarios
- testado em ROLLBACK: lista + notify ao inserir subscription

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:01:27 -03:00
Leonardo c189906c58 freemium F3a: blacklist de e-mails e slugs
- tabela blacklist (kind email|slug, value normalizado, RLS saas_admin)
- is_email_blacklisted (exato + '@dominio.com') / is_slug_blacklisted
- trigger BEFORE INSERT em auth.users bloqueia cadastro DE VERDADE (EMAIL_BLOCKED)
- slug_disponivel passa a retornar motivo 'bloqueado'
- testado em ROLLBACK: email exato/dominio/signUp/slug

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:30:55 -03:00
Leonardo 5a87c29dd0 freemium F2: registra nucleo no wiki/log
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:05:55 -03:00
Leonardo a2f3b9fae4 freemium F2: signup self-service com confirmacao + /onboarding
- Signup.vue reescrito: coleta tipo de conta + nome + nome do negocio + slug
  (disponibilidade ao vivo via slug_disponivel) + email/senha; grava tudo no
  raw_user_meta_data do signUp; PEGADINHA #2: signOut scope:local se nao veio
  sessao + tela "confirme seu e-mail". Removido provisionamento/intent inline.
- OnboardingPage.vue (PEGADINHA #3): 1o login chama auto_provision_free_tenant
  + processar_pos_signup; resolve estados provisionando/slug-colidiu/erro;
  redireciona pro painel conforme kind.
- guard: logado-sem-tenant (nao saas_admin) -> /onboarding em vez de /login
- rota /onboarding (meta.public; a pagina exige sessao)

NOTA: supabase/config.toml e gitignored — enable_confirmations=true foi setado
local (ativa no proximo restart do stack). No hosted, ligar em Auth>Email>Confirm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:00:22 -03:00
Leonardo 1594dc9426 freemium F2: RPCs idempotentes do self-service
- slug_disponivel(p_slug) -> {ok, motivo} (formato + reservados + uso),
  chamavel por anon (signup acontece antes do login)
- auto_provision_free_tenant(p_slug_override): le raw_user_meta_data, cria
  tenant (slug escolhido OU auto), vira master, clona schema, cria subscription
  gratuita ativa (XOR clinic->tenant_id / therapist->user_id). Idempotente.
- processar_pos_signup(): cria intent SO pro caminho pago, na tabela real por
  target (a view subscription_intents nao propaga user_id). Idempotente.
- testado em ROLLBACK: clinica + terapeuta, provision+idempotencia+intents OK

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:20:52 -03:00
Leonardo 31c4f08451 fix(audit): log_audit_change quebrava INSERT em tabelas globais
Regressao do schema-per-tenant: o trigger de auditoria deriva tenant_id via
tenant_id_for_schema(TG_TABLE_SCHEMA), que retorna NULL para public.* (ex.:
tenant_members) -> violava audit_logs.tenant_id NOT NULL -> QUALQUER novo
membership (provisionamento, aceite de convite) falhava.

Fix: quando o schema nao resolve tenant, cai no tenant_id da propria linha;
se ainda NULL, nao audita mas nunca quebra a operacao.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:20:51 -03:00
Leonardo 12d5c3b6dc freemium F1: registra conclusao no wiki/log
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:07:00 -03:00
Leonardo a979bdf1de freemium F1: frontend do enforcement (toast + botao Upgrade PRO)
- utils/planLimit.js: parsePlanLimitError + maybeShowPlanLimitToast (traduz
  PLAN_LIMIT_REACHED em toast amigavel com CTA via grupo system-alerts)
- AppTopbar: botao "Upgrade PRO" quando plano ativo e gratuito (reusa
  resolveActiveSubscriptionContext; plan_key nos selects)
- ligado nos 3 pontos de criacao de paciente: PatientsCadastroPage,
  CadastrosRecebidos (intake), ComponentCadastroRapido (quick-create)
- build OK

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:05:28 -03:00
Leonardo a73b82fa86 freemium F1: enforcement de limite de pacientes (schema-per-tenant)
- therapist_free ganha max_patients=20 (clinic_free ja tinha 30)
- trigger BEFORE INSERT em patients le plan_features.limits em runtime,
  resolve tenant por TG_TABLE_SCHEMA, plano ativo (clinica via tenant_id +
  pessoal via owner user_id), conta vivos (status<>Arquivado) e da RAISE
  PLAN_LIMIT_REACHED|patients|<n>
- helpers tenant_active_plan_id / plan_feature_limit (globais, sobrevivem F6.3)
- wiring: tenants novos ganham via trg_attach_business_triggers; 9 existentes backfill
- testado: clinic_free bloqueia em 30, therapist_free em 20, PRO ilimitado (rollback)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:05:19 -03:00
Leonardo 98fe183bac log: sessao schema-per-tenant (parada antes do F6.3 DROP)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 17:14:23 -03:00
Leonardo 8b620f9b04 F6.3: doc de rollback (branch-safety + cenarios pre/pos-DROP)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 17:13:38 -03:00
Leonardo 96f4543138 wiki: F6.4 SaaS-admin resolvida, F6.3 desbloqueada
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 17:02:53 -03:00
Leonardo dc2363b4e1 F6.4: resolve superficie SaaS-admin (destrava F6.3 DROP)
DB (supabase_admin, manual/f6_4_saas_admin_rpcs.supabase_admin.sql): RPCs
gated por is_saas_admin que operam no _tenant_template + fan-out pros schemas:
- Feriados defaults: saas_list/add/remove_default_feriado (template + todos schemas)
- Notif templates defaults: saas_list/upsert/set_active/delete_default_notif_
  template + saas_count_notif_template_overrides (so a copia-default owner_id NULL;
  preserva overrides do tenant)
- saas_list_all_whatsapp_channels: fan-out cross-tenant (substitui a view
  v_twilio_whatsapp_overview) com tenant_name + open_incident por canal

Frontend:
- SaasFeriadosPage / SaasNotificationTemplatesPage: supabase.from(tenant) ->
  RPCs saas_*_default
- SaasWhatsappPage: gestao por-tenant-selecionado via supabase.schema(tenant_
  <slug>) (RLS permite saas_admin); overview via saas_list_all_whatsapp_channels
- twilioWhatsappService.getAllChannels: v_twilio_whatsapp_overview -> RPC

Verificado: ZERO supabase.from('<tabela_tenant>') publico restante no FE
(so SaaS-admin estava pendente). Build passa. F6.3 agora desbloqueada.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 17:01:46 -03:00
Leonardo 4493e78349 wiki: F6.3 preparada + itens SaaS-admin em aberto
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 16:45:36 -03:00
Leonardo cdb9ce10ee F6.3 (PREPARADA, NAO APLICADA): DROP das tabelas tenant em public
manual/f6_3_drop_public_tenant_tables.supabase_admin.sql — ponto de nao-retorno,
revisar antes de aplicar:
- pre-flight assert (tenants=schemas, 78 tabelas, 6 anon NAO no template)
- 2 FKs public->tenant viram coluna solta (document_share_links.documento_id,
  whatsapp_credits_transactions.conversation_message_id)
- dropa 9 views public (6 recriadas por schema + 2 dead + v_twilio item#1)
- DROP CASCADE das 78 tabelas (derivado de _tenant_template; dados ja nos schemas)
- limpa financial_records_inject_tenant (obsoleto)

ITENS EM ABERTO verificados (16 reads FE remanescentes, TODOS na superficie
SaaS-admin ja flagada na F3): SaasFeriadosPage (feriados defaults),
SaasNotificationTemplatesPage (templates default), SaasWhatsappPage +
v_twilio_whatsapp_overview/getAllChannels (whatsapp admin cross-tenant).
Resolver (apontar pra _tenant_template ou fan-out por schema) ANTES do DROP.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 16:44:37 -03:00
Leonardo af2395c723 wiki: F6 wiring done, app testavel
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 16:26:39 -03:00
Leonardo dc7826d0b5 F6.2 wiring: tenants novos nascem com todos os triggers de negocio
- attach_agnostic_triggers reescrito SELF-CONTAINED (dirigido por colunas: set_
  updated_at em toda tabela com updated_at + prevent_* em patient_groups). Nao
  le mais de public -> sobrevive ao DROP da F6.3.
- trigger AFTER INSERT em tenant_schemas (trg_attach_business_triggers) dispara
  os 3 attach (agnostic + schema_aware + notif) pro schema novo. clone_tenant_
  template nao precisou ser tocado (ele insere em tenant_schemas). GRANT EXECUTE
  dos attach pra postgres/service_role.
- provision_account_tenant: clone ANTES do seed (seed_determined_commitments e
  no-op se schema nao existe; precisa do schema criado primeiro)

Smoke: tenant novo nasce com 84 triggers automaticamente (vs 81 nos existentes,
que sao mais que suficientes). Provision order corrigida.

App agora testavel: dados nos schemas (F6.1) + funcoes/triggers/RPCs roteiam
(F6.2). Falta so F6.3 DROP (com OK + app testado).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 16:25:58 -03:00
Leonardo 218d342181 wiki: F6.2 completa (Lote G)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 16:01:29 -03:00
Leonardo ee82985dc3 F6.2 Lote G: funcoes SQL puras -> plpgsql + roteamento (completa F6.2)
DB (supabase_admin, manual/f6_2g_sql_to_plpgsql.supabase_admin.sql): SQL puro
nao permite set_config dinamico -> converte 5 pra plpgsql + p_tenant_id +
_tenant_route:
- get_financial_summary, get_financial_report, get_patient_session_counts
  (remove filtro tenant_id IN, schema-scoped)
- list_financial_records: RETURNS SETOF financial_records -> jsonb (array,
  transparente no FE)
- get_entity_primary_phone (interno, 0 callers): herda search_path do chamador
  (sem SET, unqualified)
Smoke: get_financial_summary + list_financial_records (array 5) OK.

Frontend (3 arquivos): p_tenant_id de activeTenantId/resolveTenantId nas
chamadas de get_financial_summary/list_financial_records/get_patient_session_
counts. Build passa.

=== F6.2 COMPLETA: 66 funcoes migradas (triggers A/B/C + RPCs D/E/F/G) ===

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 16:00:16 -03:00
Leonardo 423aa5ac2a wiki: F6.2 Lote F done
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 15:51:49 -03:00
Leonardo 1243a12ced F6.2 Lote F: RPCs anon/token resolvem tenant por token/slug + roteiam
DB (supabase_admin, manual/f6_2f_anon_token_rpcs.supabase_admin.sql):
- Documentos anon (token): validate_share_token, get_signable_document_by_token,
  sign_document_by_token resolvem tenant de document_share_links.tenant_id
  (public/F1b) -> set_config search_path; documents/document_signatures/
  document_access_logs no schema (RECORD em vez de %ROWTYPE; RETURNS
  document_signatures->jsonb; document_access_logs sem tenant_id).
  document_share_links continua public.
- sign_document_by_signature_id (paciente LOGADO, nao e tenant_member):
  +p_tenant_id via _tenant_schema_unchecked + autorizacao por LINHA (signatario_id
  =uid OU email OU documento do paciente). RETURNS->jsonb.
- agendador_dias/slots_disponiveis (anon): resolvem tenant de agendador_
  configuracoes.tenant_id (public/F1b); agenda/recurrence no schema;
  agendador_configuracoes/solicitacoes ficam public.
- match_patient_by_phone (edge service): _tenant_schema_unchecked + REVOKE de
  anon/authenticated, GRANT service_role.
- list_my_signatures: CROSS-TENANT -> fan-out por schema (RETURN QUERY EXECUTE
  por tenant_schemas, tenant_id injetado; document_share_links global).
- RPCs public-only (create_patient_intake_request, get_patient_intake_invite_info,
  issue_patient_invite, rotate_*, agendador_gerar_slug): SEM mudanca (F1b public).

Frontend: signByPortal(tenantId, signatureId, hash) + composable resolve
tenant_id da linha (list_my_signatures retorna tenant_id). Build passa.

Gotcha: paciente assinante NAO e tenant_member -> auth por linha, nao membership.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 15:51:22 -03:00
Leonardo f079192698 wiki: F6.2 Lote E done
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 15:26:27 -03:00
Leonardo 02acc88da5 F6.2 Lote E: RPCs de cron/global roteadas/loopadas por tenant
DB (supabase_admin, manual/f6_2e_cron_rpcs.supabase_admin.sql):
- E2 (varrem TODOS os tenants via loop FROM tenant_schemas): cleanup/unstick_
  notification_queue, sync_overdue_financial_records (EXECUTE format por schema),
  populate_notification_queue (set_config search_path por tenant; profiles global)
- E1 (per-tenant via service_role): novo helper _tenant_schema_unchecked (sem
  is_tenant_member — service_role nao e membro; REVOKE de anon/authenticated).
  sla_open_breach, sla_mark_notified(+p_tenant_id), whatsapp_heartbeat_open_
  incident/mark_notified/resolve(+p_tenant_id). convert_abandoned_intake_to_lead
  resolve tenant internamente (intake public/F1b -> writes no schema).
  first_response_stats/_runs: _tenant_route (user-facing, frontend ja passa
  p_tenant_id); _first_response_runs computa thread_key (coluna nao existe).
- REVOKE das RPCs de servico de anon/authenticated; GRANT service_role

Edge: whatsapp-heartbeat-check (tdb.rpc->admin.rpc + p_tenant_id nos heartbeat
RPCs), conversation-sla-check (sla_mark_notified + p_tenant_id).

Gotchas: (1) service_role nao e tenant_member -> helper unchecked + REVOKE;
(2) conversation_messages nao tem coluna thread_key (computar); (3) DROP+CREATE
de nova assinatura precisa dropar ambas p/ idempotencia.

Smoke: E2 sync_overdue 13 across tenants; E1 sla_open_breach roteia; first_
response_stats user OK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 15:25:36 -03:00
Leonardo d3620f99ea wiki: F6.2 Lote D done
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 15:13:59 -03:00
Leonardo d240c6678f F6.2 Lote D: RPCs user-facing roteadas pro schema do tenant
DB (supabase_admin, manual/f6_2d_user_rpcs.supabase_admin.sql): 14 RPCs.
Helper _tenant_route(p_tenant_id) valida is_tenant_member + retorna schema
(retorna, nao seta — set_config em helper com SET search_path proprio seria
revertido na saida). Cada RPC: set_config search_path pro schema + unqualify
tabelas tenant + remove WHERE tenant_id= e tenant_id de inserts.
- Grupo 1 (ja tinham p_tenant_id, jsonb/void): delete_commitment_full,
  delete_determined_commitment, seed_default_patient_groups,
  seed_determined_commitments (no-op se schema nao existe)
- Grupo 2 (novo p_tenant_id 1o param, DROP+CREATE): cancel_recurrence_from,
  cancelar_eventos_serie, split_recurrence_at, safe_delete_patient,
  export_patient_data (audit_logs global mantido), search_global
  (patient_intake_requests fica em public/F1b -> qualificado + filtro tenant_id)
- Grupo 3 (RETURNS <tabela>->jsonb): mark_as_paid, create_financial_record_
  for_session, mark_payout_as_paid, create_therapist_payout
- can_delete_patient: unqualified, herda search_path do chamador
Smoke: mark_as_paid (status=paid, jsonb) + search_global (acha paciente) OK.

Frontend (18 sites): p_tenant_id de useTenantStore().activeTenantId (ou helper
local resolveTenantId/currentTenantId). create_financial_record_for_session ja
passava tenant; retorno SETOF->jsonb transparente (nenhum consumidor indexava
array). Build passa.

list_my_signatures (cross-tenant) -> Lote F.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 15:13:16 -03:00
Leonardo 120b1e44d8 wiki: F6.2 Lote D scoped + checkpoint
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 14:37:23 -03:00
Leonardo b6bd6fdd89 wiki: F6.2 Lote C done
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 14:10:07 -03:00
Leonardo bedbb9bafc F6.2 Lote C: split de notifications (tenant-local + notifications_sistema)
DB (supabase_admin):
- public.notifications_sistema (cross-tenant SaaS->tenant: suporte, billing;
  vazio hoje, future-proof) + RLS owner_id + realtime + notify_user_sistema()
- notify_on_session_status, fanout_inbound_message_to_notifications,
  cancel_notifications_on_opt_out/session_cancel reescritos schema-aware
  (search_path dinamico; notifications/notification_queue no schema;
  tenant_members/patients global/schema)
- notify_on_intake/scheduling disparam em tabelas PUBLIC (F1b) -> roteiam pro
  schema via tenant_schema_for + EXECUTE format
- cancel_patient_pending_notifications: notification_queue unqualified (herda
  search_path do trigger chamador)
- detach dos 4 notif-triggers tenant de public; attach_notif_triggers recria
  5 notif triggers/schema
- smoke: msg inbound -> notification no schema, destinatario correto

Frontend (notificationStore.js): load le das 2 fontes (tenantDb + public.
notifications_sistema), merge por created_at, campo _origem; realtime 2 canais;
markRead/markAllRead/archive roteiam por _origem

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 14:06:58 -03:00
Leonardo 77ef06fde7 wiki: F6.2 Lote B done
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 13:34:51 -03:00
Leonardo 5741e10e28 F6.2 Lote B: triggers schema-aware (rewrite + detach public + attach schemas)
Aplicado como supabase_admin (trigger functions sao owned por supabase_admin).
14 funcoes reescritas pra operar no schema do TG_TABLE_SCHEMA (set_config
search_path dinamico + tenant_id_for_schema p/ tabelas globais audit_logs):
log_audit_change, trg_fn_patient_status_history/timeline/risco,
auto_create_financial_record_from_session, fn_sla_resolve_on_outbound,
fn_clinical_note_version, fn_document_signature_timeline,
fn_documents_timeline_insert, sync_legacy_email/phone_fields,
fn_agenda_regras_semanais_no_overlap, patients_validate_member_consistency.
sync_busy_mirror_agenda_eventos: cross-tenant via tenant_schema_for +
EXECUTE format (espelha "Ocupado" nos schemas das clinicas).
financial_records_inject_tenant: obsoleto, nao anexado nos schemas.

Detach dos 14 schema-aware das tabelas tenant em public (quebrariam la);
attach_schema_aware_triggers recria 22 triggers/schema (defs reais, tenant_id
removido de WHEN/UPDATE OF). agenda_cfg_sync e trg_fn_financial_records_auto_
overdue (agnosticos) ficam em public E nos schemas.

Smoke: sessao->realizado cria financial_record (R$250) no schema + marca billed;
audit roteia tenant_id correto; patient status muda -> timeline no schema.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 13:21:00 -03:00
Leonardo d58b939e1c F6.2 Lote A: anexa triggers schema-agnosticos aos schemas tenant
attach_agnostic_triggers(schema) recria nos schemas os triggers de public cuja
funcao e provadamente schema-agnostica (so mexe em NEW/OLD): familia updated_at
(8: set_updated_at, fn_clinical_notes_updated_at, set_insurance_plans/medicos/
services_updated_at, set_updated_at_recurrence, update_payment_settings/
professional_pricing_updated_at) + prevent_promoting_to_system +
prevent_system_group_changes. Backfill dos 9 (54 triggers/schema). Smoke:
set_updated_at dispara no schema. Schema-aware vem no Lote B; wiring no clone
no fim da F6.2

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 12:57:43 -03:00
Leonardo c3220f159c wiki: F6.0+F6.1 done, plano F6.2
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 12:53:22 -03:00
Leonardo 003f2eb2a6 F6.0+F6.1 schema-per-tenant: clones dos 9 tenants + migracao de dados
- F6.0 (migration): clone_tenant_template pros 9 tenants existentes (schemas
  vazios; dispara trigger F5 -> expostos no PostgREST)
- F6.1 (manual supabase_admin): copia dados public -> schemas com
  session_replication_role=replica (desabilita FK check, so supabase_admin).
  Tabelas com tenant_id por filtro; 3 filhas sem tenant_id (commitment_services,
  insurance_plan_services, recurrence_rule_services) por JOIN no pai; exclui
  colunas GENERATED (net_amount, margin_brl); reset de 4 sequences; ON CONFLICT
  DO NOTHING (idempotente). Dados continuam em public (DROP so na F6.3)
- Verificado: contagens public vs schemas batem (35 patients, 37 eventos,
  355 mensagens, 54 financeiro...); seeds default replicados por schema

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 09:41:03 -03:00
Leonardo a85716b0ea wiki: F5 schema-per-tenant
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 09:26:16 -03:00
Leonardo 6b542cd03a F5 schema-per-tenant: PostgREST expoe schemas tenant dinamicamente
- manual/f5_pgrst_refresh_schemas.supabase_admin.sql: refresh_pgrst_schemas()
  (owned supabase_admin, postgres nao e superuser neste stack) seta
  pgrst.db_schemas in-database na role authenticator a partir de tenant_schemas
  + NOTIFY pgrst reload config/schema. Expoe/retira schema SEM restart; GUC
  persiste entre stop/start
- migration 20260613000002: trigger em tenant_schemas dispara o refresh a cada
  clone/drop (clone/drop nao precisam ser tocados)
- config.toml: baseline public,graphql_public + comentario explicando que a
  config in-db supersede em runtime
- E2E testado via HTTP: clone -> pgrst.db_schemas inclui tenant_x -> REST
  Accept-Profile retorna 200 (vs 406 schema inexistente); drop -> volta 406.
  Sem restart de container

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 09:25:25 -03:00
Leonardo 07437c9ff4 wiki: F4 + F1b schema-per-tenant
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 09:10:16 -03:00
Leonardo f17e9ee786 F1b: 6 tabelas anon-facing ficam em public (decisao roteamento anon)
Fluxos anon identificam tenant por token/slug e nao resolvem o schema fisico.
Decisao (opcao C): manter em public com RLS por token. Volta a global:
patient_intake_requests, patient_invites, patient_invite_attempts,
document_share_links, agendador_configuracoes, agendador_solicitacoes.

- migration 20260613000001_f1b: remove as 6 do _tenant_template (template v2,
  78 tabelas). Smoke: clone gera 78, zero tabelas anon no schema, drop limpo
- frontend: 38 cadeias em 14 arquivos revertidas tenantDb().from() ->
  supabase.from() com tenant_id/owner_id restaurado (via comparacao com main)
- edge: convert-abandoned-intakes restaurada do main (SELECT global)
- save-intake-progress: ja usava public, sem mudanca
- doc F0 atualizado: 78 tenant + 59 global

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 09:09:46 -03:00
Leonardo 9b21642e15 F4 schema-per-tenant: edge functions roteiam pro schema do tenant
- _shared/tenant.ts: helper (adminClient, tenantDbForId, schemaForTenant,
  listTenantSchemas, resolveTenantByChannel, tenantSchemaName)
- _shared/whatsapp-hooks.ts: hooks de tabela tenant recebem tdb; RPCs de
  credito (deduct/add_whatsapp_credits) e tenant_members seguem em supa+p_tenant_id
- inbound (twilio/evolution): tenant_id da URL -> tdb pra conversation_messages
  e notification_channels
- crons de fila (process-notification/email/sms/whatsapp-queue): varrem
  listTenantSchemas e drenam a fila de cada schema (Q3: filas sao per-tenant);
  modo single-tenant se body.tenant_id vier
- crons reminders/checks (send-session-reminders, conversation-sla-check,
  whatsapp-heartbeat-check, convert-abandoned-intakes, sync-email-templates):
  loop por tenant
- routing por tenant_id (send-whatsapp-message, send-session-reminder-manual,
  twilio-provision, de/reactivate-channel, twilio-webhook): tenantDbForId;
  channel-actions sem tenant_id varrem schemas por channel_id
- asaas-*: tenant_id do body -> tdb; asaas-webhook fica global (whatsapp_credit_purchases)
- notification-webhook (Meta): resolve tenant via channel_routing por phone_number_id,
  fan-out por message_id quando nao resolve
- caller send-session-reminder-manual passa tenant_id (evento vive no schema)

Pendente: save-intake-progress e fluxos anon por token (decisao de roteamento)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 08:44:09 -03:00