Commit Graph

16 Commits

Author SHA1 Message Date
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 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 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 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 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 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 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 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 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 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 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 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 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 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