From f733db8436e9d2da770813e2a86b15240c311036 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Fri, 6 Mar 2026 06:37:13 -0300 Subject: [PATCH] ZERADO --- .env.local | 2 + CHANGELOG.md | 163 +- .../Agencia_PSI_Billing_Mestre_v2_0.html | 672 ++ .../Agencia_PSI_Billing_Subscriptions_v1_2.html | 206 + ...documentacao-billing-completa-agencia-psi.html | 308 + ...ocumentacao-planos-seeder-complemento.html | 231 + .../dev-documentacao-planos-seeder-v1.html | 957 ++ ...iption_Health_Entitlements_2026-03-01.html | 612 ++ Nova-Dev-Doc/supervisor_fase1.sql | 414 + Novo-DB/fix_missing_subscriptions.sql | 220 + Novo-DB/fix_subscriptions_validate_scope.sql | 50 + Novo-DB/migration_001.sql | 296 + Novo-DB/migration_002_layout_variant.sql | 13 + Novo-DB/seed_001.sql | 334 + Novo-DB/seed_002.sql | 199 + Novo-DB/seed_003.sql | 283 + Pedra Angular/plataforma_saude_mental.html | 1798 ++++ .../plataforma_saude_mental_estrategia.pdf | 429 + USER_ARCHETYPES.html | 566 + schema.sql | 9444 +++++++++++++++++ src/App.vue | 132 +- src/app/bootstrapUserSettings.js | 9 +- src/components/ComponentCadastroRapido.vue | 248 +- .../agenda/AgendaOnlineGradeCard.vue | 1 - .../agenda/AgendaSlotsPorDiaCard.vue | 3 - src/components/agenda/PausasChipsEditor.vue | 85 +- src/composables/usePlatformPermissions.js | 81 + src/composables/useRoleGuard.js | 158 +- src/composables/useUserSettingsPersistence.js | 2 + .../components/AdicionarCompromissoPage.vue | 1004 -- .../agenda/components/AgendaClinicMosaic.vue | 302 +- .../agenda/components/AgendaEventDialog.vue | 2162 ++-- .../agenda/components/AgendaRightPanel.vue | 3 - .../agenda/components/AgendaToolbar.vue | 6 - .../components/DeterminedCommitmentDialog.vue | 523 + .../cards/AgendaNextSessionsCardList.vue | 7 - .../components/cards/AgendaPulseCardGrid.vue | 4 - .../composables/useAgendaClinicEvents.js | 75 + .../composables/useDeterminedCommitments.js | 45 + .../agenda/pages/AgendaClinicaPage.vue | 1519 +-- .../agenda/pages/AgendaTerapeutaPage.vue | 1283 ++- .../agenda/pages/CompromissosDeterminados.vue | 753 ++ .../agenda/services/agendaClinicRepository.js | 131 + src/features/agenda/services/agendaMappers.js | 117 +- .../agenda/services/agendaRepository.js | 30 +- src/features/clinic/components/ModuleRow.vue | 35 + src/features/patients/PatientsListPage.vue | 692 +- .../cadastro/PatientsCadastroPage.vue | 505 +- .../cadastro/PatientsExternalLinkPage.vue | 575 +- .../recebidos/CadastrosRecebidosPage.vue | 395 +- .../patients/grupos/GruposPacientesPage.vue | 476 +- .../patients/prontuario/PatientProntuario.vue | 10 - src/features/patients/tags/TagsPage.vue | 505 +- src/images/layout-design2.png | Bin 0 -> 227128 bytes src/layout/AdminLayout.vue | 7 - src/layout/AppConfigurator.vue | 11 +- src/layout/AppLayout.vue | 173 +- src/layout/AppMenu.vue | 213 +- src/layout/AppMenuFooterPanel.vue | 87 +- src/layout/AppMenuItem.vue | 107 +- src/layout/AppRail.vue | 329 + src/layout/AppRailPanel.vue | 246 + src/layout/AppRailTopbar.vue | 159 + src/layout/AppShellLayout.vue | 25 - src/layout/AppSidebar.vue | 101 +- src/layout/AppTopbar.vue | 550 +- src/layout/ConfiguracoesPage - Copia.vue | 172 + src/layout/ConfiguracoesPage.vue | 130 +- src/layout/PatientLayout.vue | 7 - src/layout/TherapistLayout.vue | 7 - src/layout/areas/AdminLayout.vue | 4 + src/layout/areas/PortalLayout.vue | 4 + src/layout/areas/TherapistLayout.vue | 4 + src/layout/composables/layout.js | 93 +- .../concepcoes/ex-header-conceitual.vue | 272 + .../configuracoes/ConfiguracoesAgendaPage.vue | 3489 ++---- .../ConfiguracoesClinicaPage.vue | 0 .../configuracoes/ConfiguracoesContaPage.vue | 0 .../configuracoes/ConfiguracoesIntakePage.vue | 0 src/main.js | 101 +- src/navigation/index.js | 231 +- src/navigation/menus/admin.menu.js | 105 - src/navigation/menus/clinic.menu.js | 82 + src/navigation/menus/editor.menu.js | 32 + src/navigation/menus/portal.menu.js | 7 +- src/navigation/menus/saas.menu.js | 34 +- src/navigation/menus/sakai.demo.menu.js | 60 +- src/navigation/menus/supervisor.menu.js | 30 + src/navigation/menus/therapist.menu.js | 13 +- src/router/guards.js | 613 +- src/router/index.js | 10 +- src/router/routes.account.js | 2 +- .../{routes.admin.js => routes.clinic.js} | 87 +- src/router/routes.configs.js | 5 - src/router/routes.demo.js | 9 +- src/router/routes.editor.js | 64 + src/router/routes.portal.js | 26 +- src/router/routes.saas.js | 9 +- src/router/routes.supervisor.js | 46 + src/router/routes.therapist.js | 78 +- src/services/GruposPacientes.service.js | 16 + src/services/subscriptionIntents.js | 172 +- src/stores/entitlementsStore.js | 209 +- src/stores/menuStore.js | 23 + src/stores/tenantFeaturesStore.js | 101 +- src/stores/tenantStore.js | 133 +- src/views/pages/HomeCards.vue | 1373 ++- src/views/pages/NotFound.vue | 1 - src/views/pages/account/ProfilePage.vue | 1463 ++- .../pages/admin/agenda/MyAppointmentsPage.vue | 9 - .../pages/admin/agenda/NewAppointmentPage.vue | 9 - .../pages/admin/clinic/ClinicFeaturesPage.vue | 330 - src/views/pages/auth/Login.vue | 715 +- src/views/pages/auth/ResetPasswordPage.vue | 535 +- src/views/pages/auth/SecurityPage.vue | 655 +- src/views/pages/auth/Welcome.vue | 159 +- .../pages/billing/ClinicMeuPlanoPage.vue | 636 ++ .../pages/billing/TherapistMeuPlanoPage.vue | 537 + .../pages/billing/TherapistUpgradePage.vue | 422 + src/views/pages/billing/UpgradePage.vue | 457 +- .../ClinicDashboard.vue} | 0 .../OnlineSchedulingAdminPage.vue | 0 .../clinic/clinic/ClinicFeaturesPage.vue | 591 ++ .../clinic/ClinicProfessionalsPage.vue | 1674 +-- src/views/pages/editor/EditorDashboard.vue | 56 + src/views/pages/portal/MinhasSessoes.vue | 58 + .../portal/agenda/MyAppointmentsPage.vue | 9 - .../portal/agenda/NewAppointmentPage.vue | 9 - src/views/pages/public/AcceptInvitePage.vue | 239 +- .../pages/public/CadastroPacienteExterno.vue | 6 - .../pages/public/Landingpage-v1 - bkp.vue | 519 + src/views/pages/public/Landingpage-v1.vue | 619 +- .../pages/public/PatientsExternalLinkPage.vue | 3 - src/views/pages/public/Signup.vue | 306 +- src/views/pages/saas/SaasDashboard.vue | 734 +- src/views/pages/saas/SaasFeaturesPage.vue | 557 +- .../pages/saas/SaasPlanFeaturesMatrixPage.vue | 603 +- src/views/pages/saas/SaasPlanLimitsPage.vue | 775 ++ src/views/pages/saas/SaasPlansPage.vue | 568 +- src/views/pages/saas/SaasPlansPublicPage.vue | 843 +- .../pages/saas/SaasSubscriptionEventsPage.vue | 556 +- .../pages/saas/SaasSubscriptionHealthPage.vue | 819 +- .../pages/saas/SaasSubscriptionsPage.vue | 682 +- .../pages/saas/SubscriptionIntentsPage.vue | 1054 +- .../pages/supervisor/SupervisaoSalaPage.vue | 40 + .../pages/supervisor/SupervisorDashboard.vue | 33 + 146 files changed, 43436 insertions(+), 12779 deletions(-) create mode 100644 Nova-Dev-Doc/Billing → Subscription → Entitlements/Agencia_PSI_Billing_Mestre_v2_0.html create mode 100644 Nova-Dev-Doc/Billing → Subscription → Entitlements/Agencia_PSI_Billing_Subscriptions_v1_2.html create mode 100644 Nova-Dev-Doc/Billing → Subscription → Entitlements/documentacao-billing-completa-agencia-psi.html create mode 100644 Nova-Dev-Doc/Planos/dev-documentacao-planos-seeder-complemento.html create mode 100644 Nova-Dev-Doc/Planos/dev-documentacao-planos-seeder-v1.html create mode 100644 Nova-Dev-Doc/Subscription Health e Entitlements/Agencia_PSI_Sessao_Subscription_Health_Entitlements_2026-03-01.html create mode 100644 Nova-Dev-Doc/supervisor_fase1.sql create mode 100644 Novo-DB/fix_missing_subscriptions.sql create mode 100644 Novo-DB/fix_subscriptions_validate_scope.sql create mode 100644 Novo-DB/migration_001.sql create mode 100644 Novo-DB/migration_002_layout_variant.sql create mode 100644 Novo-DB/seed_001.sql create mode 100644 Novo-DB/seed_002.sql create mode 100644 Novo-DB/seed_003.sql create mode 100644 Pedra Angular/plataforma_saude_mental.html create mode 100644 Pedra Angular/plataforma_saude_mental_estrategia.pdf create mode 100644 USER_ARCHETYPES.html create mode 100644 schema.sql create mode 100644 src/composables/usePlatformPermissions.js delete mode 100644 src/features/agenda/components/AdicionarCompromissoPage.vue create mode 100644 src/features/agenda/components/DeterminedCommitmentDialog.vue create mode 100644 src/features/agenda/composables/useAgendaClinicEvents.js create mode 100644 src/features/agenda/composables/useDeterminedCommitments.js create mode 100644 src/features/agenda/pages/CompromissosDeterminados.vue create mode 100644 src/features/agenda/services/agendaClinicRepository.js create mode 100644 src/features/clinic/components/ModuleRow.vue create mode 100644 src/images/layout-design2.png delete mode 100644 src/layout/AdminLayout.vue create mode 100644 src/layout/AppRail.vue create mode 100644 src/layout/AppRailPanel.vue create mode 100644 src/layout/AppRailTopbar.vue delete mode 100644 src/layout/AppShellLayout.vue create mode 100644 src/layout/ConfiguracoesPage - Copia.vue delete mode 100644 src/layout/PatientLayout.vue delete mode 100644 src/layout/TherapistLayout.vue create mode 100644 src/layout/areas/AdminLayout.vue create mode 100644 src/layout/areas/PortalLayout.vue create mode 100644 src/layout/areas/TherapistLayout.vue create mode 100644 src/layout/concepcoes/ex-header-conceitual.vue delete mode 100644 src/layout/configuracoes/ConfiguracoesClinicaPage.vue delete mode 100644 src/layout/configuracoes/ConfiguracoesContaPage.vue delete mode 100644 src/layout/configuracoes/ConfiguracoesIntakePage.vue delete mode 100644 src/navigation/menus/admin.menu.js create mode 100644 src/navigation/menus/clinic.menu.js create mode 100644 src/navigation/menus/editor.menu.js create mode 100644 src/navigation/menus/supervisor.menu.js rename src/router/{routes.admin.js => routes.clinic.js} (72%) create mode 100644 src/router/routes.editor.js create mode 100644 src/router/routes.supervisor.js create mode 100644 src/stores/menuStore.js delete mode 100644 src/views/pages/admin/agenda/MyAppointmentsPage.vue delete mode 100644 src/views/pages/admin/agenda/NewAppointmentPage.vue delete mode 100644 src/views/pages/admin/clinic/ClinicFeaturesPage.vue create mode 100644 src/views/pages/billing/ClinicMeuPlanoPage.vue create mode 100644 src/views/pages/billing/TherapistMeuPlanoPage.vue create mode 100644 src/views/pages/billing/TherapistUpgradePage.vue rename src/views/pages/{admin/AdminDashboard.vue => clinic/ClinicDashboard.vue} (100%) rename src/views/pages/{admin => clinic}/OnlineSchedulingAdminPage.vue (100%) create mode 100644 src/views/pages/clinic/clinic/ClinicFeaturesPage.vue rename src/views/pages/{admin => clinic}/clinic/ClinicProfessionalsPage.vue (58%) create mode 100644 src/views/pages/editor/EditorDashboard.vue create mode 100644 src/views/pages/portal/MinhasSessoes.vue delete mode 100644 src/views/pages/portal/agenda/MyAppointmentsPage.vue delete mode 100644 src/views/pages/portal/agenda/NewAppointmentPage.vue create mode 100644 src/views/pages/public/Landingpage-v1 - bkp.vue create mode 100644 src/views/pages/saas/SaasPlanLimitsPage.vue create mode 100644 src/views/pages/supervisor/SupervisaoSalaPage.vue create mode 100644 src/views/pages/supervisor/SupervisorDashboard.vue diff --git a/.env.local b/.env.local index 819b8e0..b06bc98 100644 --- a/.env.local +++ b/.env.local @@ -1,2 +1,4 @@ VITE_SUPABASE_URL=http://127.0.0.1:54321 VITE_SUPABASE_ANON_KEY=sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH +VITE_QA_MODE=true +VITE_QA_PASS=123Mudar@ diff --git a/CHANGELOG.md b/CHANGELOG.md index 35bb68c..d2fd7b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,70 +1,155 @@ -# Changelog +# CHANGELOG — Banco de Dados AgênciaPsi -## 4.3.0 (2025-02-26) +Registro histórico de todas as migrations aplicadas no banco. +Formato: data | arquivo | o que mudou | por quê -**Implemented New Features and Enhancements** +--- -- Update PrimeVue version +## [001] — 2026-03-03 +**Arquivo:** `migration_001.sql` +**Seed:** `seed_001.sql` -## 4.2.0 (2024-12-09) +### Contexto +O schema original foi construído de forma incremental e acumulou +inconsistências no modelo de identidade. Usuários não tinham um +tipo de conta definido formalmente, tenants não distinguiam +terapeuta de clínica, e não existia suporte a paciente como +tipo de conta de plataforma. -**Implemented New Features and Enhancements** +### O que mudou -- Refactored dashboard sections to components -- Migrate sass from @import to @use +#### `profiles` +- ✅ Adicionada coluna `account_type text NOT NULL DEFAULT 'free'` + - Valores: `free | patient | therapist | clinic` + - Imutável após sair de `free` (trigger `trg_account_type_immutable`) + - Usuários com role=`patient` migrados para `account_type='patient'` + - Usuários com tenant `saas` ativo migrados para `account_type='therapist'` -## 4.1.0 (2024-07-29) +#### `tenants` +- ✅ Novos valores aceitos em `kind`: + - `therapist` → terapeuta individual (substitui `saas`) + - `clinic_coworking` → clínica tipo 1: gestão de salas + - `clinic_reception` → clínica tipo 2: secretaria + múltiplas agendas + - `clinic_full` → clínica tipo 3: coworking + secretaria +- ✅ `kind` agora é imutável após criação (trigger `trg_tenant_kind_immutable`) +- ✅ 10 tenants `saas` órfãos (sem admin, sem subscriptions) deletados +- ✅ Tenants `saas` com admin ativo migrados para `kind='therapist'` +- ⚠️ `saas` e `clinic` (legados) mantidos no CHECK por compatibilidade. + Não criar novos tenants com esses kinds. -- Changed menu button location at topbar -- Add border to overlay menu -- Animation for mobile mask -- Fixed chart colors +#### `plans` +- ✅ Adicionado `patient` como valor válido em `target` +- ✅ Inserido plano `patient_free` (gratuito, target=patient) -## 4.0.0 (2024-07-29) +#### Novas funções +| Função | Descrição | +|--------|-----------| +| `provision_account_tenant(user_id, kind, name?)` | Cria tenant + membership + atualiza account_type. Chamar no onboarding. | +| `is_therapist_tenant(tenant_id)` | Retorna true se tenant é do tipo therapist | +| `is_clinic_tenant(tenant_id)` | Atualizada: inclui todos os subtipos de clínica | +| `guard_tenant_kind_immutable()` | Trigger: bloqueia alteração de tenants.kind | +| `guard_account_type_immutable()` | Trigger: bloqueia alteração de account_type após escolha | +| `guard_patient_cannot_own_tenant()` | Trigger: bloqueia paciente de ser tenant_admin/therapist | -- Updated to PrimeVue v4 +#### Funções atualizadas +| Função | O que mudou | +|--------|-------------| +| `handle_new_user()` | Agora insere `account_type='free'` | +| `handle_new_user_create_personal_tenant()` | Desabilitada — tenant criado no onboarding | +| `ensure_personal_tenant()` | Busca por `kind IN ('therapist','saas')` e delega para `provision_account_tenant` | -## 3.10.0 (2024-03-11) +### Regras de negócio agora garantidas no banco +1. **Paciente é para sempre paciente** — `account_type` imutável após escolha +2. **Terapeuta nunca vira clínica e vice-versa** — `tenants.kind` imutável +3. **Paciente não pode ter tenant** — trigger bloqueia na inserção +4. **Cada tipo de conta tem seu tipo de tenant** — `provision_account_tenant` garante -**Migration Guide** +### Usuários de seed (apenas dev/staging) +| Email | Tipo | Tenant | +|-------|------|--------| +| paciente@agenciapsi.com.br | patient | nenhum | +| terapeuta@agenciapsi.com.br | therapist | tenant próprio (therapist) + vinculado à Clínica 3 | +| clinica1@agenciapsi.com.br | clinic | clinic_coworking | +| clinica2@agenciapsi.com.br | clinic | clinic_reception | +| clinica3@agenciapsi.com.br | clinic | clinic_full | +| saas@agenciapsi.com.br | saas_admin | nenhum | +> Senha de todos: `Teste@123` -- Update theme files. +--- -**Implemented New Features and Enhancements** +## [002] — seed_002.sql -- Upgrade to PrimeVue 3.49.1 +**Arquivo:** `Novo-DB/seed_002.sql` -## 3.9.0 (2023-11-01) +### O que cria -**Migration Guide** +#### Migration embutida +- ✅ `profiles.platform_roles text[] NOT NULL DEFAULT '{}'` — adicionada via `ADD COLUMN IF NOT EXISTS` (idempotente) -- Update theme files. +#### Usuários de teste +| Email | Senha | Papel | Tenant | +|-------|-------|-------|--------| +| `supervisor@agenciapsi.com.br` | `Teste@123` | `supervisor` em `tenant_members` | Clínica Bem Estar (Full) | +| `editor@agenciapsi.com.br` | `Teste@123` | `therapist` em `tenant_members` + `platform_roles = '{editor}'` | Clínica Bem Estar (Full) | -**Implemented New Features and Enhancements** +UUIDs reservados: +- Supervisor: `aaaaaaaa-0007-0007-0007-000000000007` +- Editor: `aaaaaaaa-0008-0008-0008-000000000008` -- Upgrade to PrimeVue 3.39.0 +--- -## 3.8.0 (2023-07-24) +## [PENDENTE] — Migration necessária: `platform_roles` em `profiles` -**Migration Guide** +**Contexto:** +Implementação das áreas de **Supervisor** (papel de tenant) e **Editor** (papel de plataforma). +O papel de Editor é atribuído pelo `saas_admin` e armazenado diretamente no perfil do usuário, +independente de qual tenant ele pertence. -- Update theme files. -- Update assets style files -- Remove code highlight +### O que precisa ser aplicado no banco -**Implemented New Features and Enhancements** +#### `profiles` +- ⚠️ **Adicionar coluna** `platform_roles text[] NOT NULL DEFAULT '{}'` + - Armazena papéis globais de plataforma. Ex.: `'{editor}'` + - Quem pode escrever: somente `saas_admin` (via RLS ou função privilegiada) + - Quem pode ter: qualquer usuário autenticado, **exceto** `account_type = 'patient'` + - Valores previstos: `editor` (mais podem ser adicionados futuramente) -- Upgrade to PrimeVue 3.30.2 +#### SQL sugerido +```sql +ALTER TABLE public.profiles + ADD COLUMN IF NOT EXISTS platform_roles text[] NOT NULL DEFAULT '{}'; -## 3.7.0 (2023-05-06) +-- Comentário descritivo +COMMENT ON COLUMN public.profiles.platform_roles IS + 'Papéis globais de plataforma, independentes de tenant. Ex: editor de microlearning. Atribuído pelo saas_admin.'; -- Upgrade to PrimeVue 3.28.0 +-- RLS: somente saas_admin pode atualizar platform_roles (exemplo) +-- CREATE POLICY "saas_admin pode atualizar platform_roles" +-- ON public.profiles FOR UPDATE +-- USING (auth.uid() IN (SELECT id FROM public.profiles WHERE role = 'saas_admin')) +-- WITH CHECK (true); +``` -**Implemented New Features and Enhancements** +#### `tenant_members` (sem alteração necessária) +- O papel `supervisor` já é suportado como valor text em `tenant_members.role`. +- Nenhuma alteração de schema é necessária — basta inserir memberships com `role = 'supervisor'`. -## 3.6.0 (2023-04-12) +### Impacto se não aplicado +- Área do Editor (`/editor`) fica inacessível a todos (coluna ausente → `platform_roles` vem `null` → acesso negado). +- Área do Supervisor (`/supervisor`) funciona normalmente — não depende desta migration. -**Implemented New Features and Enhancements** +--- -- Upgrade to PrimeVue 3.26.1 -- Upgrade to vite 4.2.1 +## Futuro — registrado mas não implementado + +### Vínculo Terapeuta ↔ Clínica (a implementar) +- Terapeuta autoriza explicitamente que secretaria gerencie suas sessões +- Permissão só válida se clínica tiver `kind IN ('clinic_reception', 'clinic_full')` +- Secretaria acessa apenas sessões — não prontuário nem anotações +- Dissociação bloqueada se houver `agenda_eventos` futuros (`inicio_em > now()`) +- Após dissociação: cada parte fica com seus próprios pacientes +- Requer: coluna de permissão no vínculo + função de dissociação com validação + +--- + +*Última atualização: 2026-03-03* diff --git a/Nova-Dev-Doc/Billing → Subscription → Entitlements/Agencia_PSI_Billing_Mestre_v2_0.html b/Nova-Dev-Doc/Billing → Subscription → Entitlements/Agencia_PSI_Billing_Mestre_v2_0.html new file mode 100644 index 0000000..2bb38de --- /dev/null +++ b/Nova-Dev-Doc/Billing → Subscription → Entitlements/Agencia_PSI_Billing_Mestre_v2_0.html @@ -0,0 +1,672 @@ + + + + + + Documento Mestre — Billing (Plans, Pricing, Subscriptions, Entitlements) v2.0 | Agência PSI + + + +
+
+

Documento Mestre • Billing • Agência PSI

+

Plans, Pricing, Subscriptions & Entitlements — v2.0

+

+ Documento institucional do domínio Billing. Unifica: catálogo de planos, preços vigentes, + assinatura (clínica/terapeuta), guardrails e entitlements (features + limits). Este material é pensado para + reduzir regressões e orientar o operador/dev quando algo “parecer impossível” (ex.: corrigir plano core sem + desativar triggers). +

+
+ Estado: operacional (MVP) + Atualizado: 2026-03-01 10:43:18 UTC + Padrão: Supabase + Postgres + Vue/PrimeVue +
+
+ + + +
+
+ +
+

1. Contexto e objetivos

+

+ O MVP do SaaS precisa garantir que o sistema “respeite o plano”. Para isso, o domínio Billing opera em camadas: + Plans (catálogo), Pricing (vigência), Subscriptions (plano vigente por tenant/user) + e Entitlements (features + limites). +

+
+ Regra de ouro: o front nunca deve “inferir plano” por role. O plano vigente vem de subscriptions + e os limites/flags vêm de plan_features. +
+
+ +
+

2. Entidades e conceitos

+
+
+ plans +

Catálogo de planos (core e custom). Guarda key, target, preço base e metadados.

+
+
+ plan_prices +

Preço com vigência. Preço vigente: is_active=true e active_to is null.

+
+
+ subscriptions +

Assinatura ativa por tenant (clínica) ou por user (terapeuta). A view escolhe a mais recente por owner.

+
+
+ features / plan_features +

Mapa de capabilities e limites (limits jsonb). É daqui que o front deve “gatear” menus/ações.

+
+
+
+ +
+

3. Tabela plans e planos core

+

Planos core do MVP (devem existir e permanecer): clinic_free, clinic_pro, therapist_free, therapist_pro.

+
-- estrutura confirmada (resumo)
+-- plans (public)
+-- id, key, name, description, is_active, price_cents, currency, billing_interval, target
+ +
+ Observação importante: planos core têm guardrails: não podem ser deletados e sua key não pode ser alterada. +
+
+ +
+

4. Pricing e vigência

+

+ A UI pública de preços deve consumir v_public_pricing. A vigência de preço vem de plan_prices: + preço vigente é aquele com is_active=true e active_to is null. Para planos FREE, a UI pode exibir “Grátis” + mesmo sem registro em plan_prices. +

+
+ Boas práticas: a tela pública não deve depender do schema “cru”. Mantenha a view como contrato. +
+
+ +
+

5. Subscriptions: schema e regras

+

Schema confirmado via information_schema e constraints:

+
-- subscriptions (public) - colunas relevantes
+id uuid primary key default gen_random_uuid()
+tenant_id uuid null
+user_id uuid null
+plan_id uuid not null references plans(id) on delete restrict
+status text not null default 'active'
+"interval" text null check ("interval" in ('month','year'))
+current_period_start timestamptz null
+current_period_end timestamptz null
+plan_key text null
+provider text not null default 'manual'
+source text not null default 'manual'
+started_at timestamptz not null default now()
+created_at timestamptz not null default now()
+updated_at timestamptz not null default now()
+ +
+ Modelagem: clínica → usa tenant_id. Terapeuta → usa user_id (com tenant_id nulo). +
+
+ +
+

6. View: v_tenant_active_subscription

+

+ Esta view define “o plano vigente” do tenant. Regra: status active e período ainda válido. + Escolhe a assinatura mais recente por tenant (created_at DESC). +

+
select distinct on (tenant_id)
+  tenant_id,
+  plan_id,
+  plan_key,
+  "interval",
+  status,
+  current_period_start,
+  current_period_end,
+  created_at
+from subscriptions s
+where tenant_id is not null
+  and status = 'active'
+  and (current_period_end is null or current_period_end > now())
+order by tenant_id, created_at desc;
+ +
+ Diagnóstico rápido: se views de entitlements estiverem “vazias”, primeiro verifique se existe subscription ativa nesta view. +
+
+ +
+

7. Operações de assinatura: change / cancel / reactivate

+

+ O front chama RPCs, mantendo a regra de ouro: “a verdade vem do banco”. + Depois de operar, a tela recarrega para refletir o estado real. +

+
-- RPCs usadas no front
+-- change_subscription_plan(p_subscription_id uuid, p_new_plan_id uuid)
+-- cancel_subscription(p_subscription_id uuid)
+-- reactivate_subscription(p_subscription_id uuid)
+
+ Nota: se o RPC atualizar apenas plan_id, é recomendável manter plan_key e interval + consistentes (quando for relevante), para facilitar auditoria e debugging. +
+
+ +
+

8. Auditoria: subscription_events

+

+ Tela “Histórico de assinaturas” é read-only e mostra até 500 eventos mais recentes. Eventos típicos: + plan_changed, canceled, reactivated. +

+
+ UX operacional: o histórico deve permitir navegar de volta para o owner (Subscriptions) via query ?q=clinic:<uuid>. +
+
+ +
+

9. Features e plan_features

+

O MVP já possui features com keys (ex.: online_scheduling, reports_basic, etc.) e tabela plan_features:

+
-- plan_features(plan_id uuid not null, feature_id uuid not null,
+-- enabled boolean not null default true, limits jsonb null, created_at timestamptz default now())
+-- PK: (plan_id, feature_id)
+-- FK: feature_id → features(id) ON DELETE CASCADE
+-- FK: plan_id → plans(id) ON DELETE CASCADE
+ +
+ Importante: limits é um contrato com o front. Ex.: {"max_patients":30}, {"sessions_per_month":40}. +
+
+ +
+

10. Views de entitlements (com limits)

+

Para atender o front com 1 query, criamos views “full” e “json”.

+ +

10.1 v_tenant_entitlements_full

+
create or replace view public.v_tenant_entitlements_full as
+select
+  a.tenant_id,
+  f.key as feature_key,
+  (pf.enabled = true) as allowed,
+  pf.limits,
+  a.plan_id,
+  p.key as plan_key
+from public.v_tenant_active_subscription a
+join public.plan_features pf on pf.plan_id = a.plan_id
+join public.features f on f.id = pf.feature_id
+join public.plans p on p.id = a.plan_id;
+ +

10.2 v_tenant_entitlements_json

+
create or replace view public.v_tenant_entitlements_json as
+select
+  tenant_id,
+  max(plan_key) as plan_key,
+  jsonb_object_agg(
+    feature_key,
+    jsonb_build_object(
+      'allowed', allowed,
+      'limits', coalesce(limits, '{}'::jsonb)
+    )
+    order by feature_key
+  ) as entitlements
+from public.v_tenant_entitlements_full
+group by tenant_id;
+ +
+ Uso no front: uma única consulta retorna plan_key + mapa de entitlements com limits. +
+
+ +
+

11. Triggers de proteção (Guardrails)

+

Triggers confirmadas em public.plans:

+
trg_no_delete_core_plans
+trg_no_change_plan_target
+trg_no_change_core_plan_key
+ +

11.1 Funções (versões base)

+
-- guard_no_delete_core_plans(): impede deletar planos core
+-- guard_no_change_core_plan_key(): impede alterar key dos planos core
+-- guard_no_change_plan_target(): impede alterar target de qualquer plano
+ +
+ Armadilha comum: tentar “corrigir plano core” via UPDATE direto. O trigger bloqueia e isso é desejável. +
+
+ +
+

12. Correção segura de plano core (bypass controlado)

+

+ Caso real desta sessão: clinic_free estava com target incorreto. + O objetivo foi corrigir sem “desligar guardrails”. +

+ +

12.1 Patch do guardrail para bypass por transação

+
create or replace function public.guard_no_change_plan_target()
+returns trigger
+language plpgsql
+as $$
+declare
+  v_bypass text;
+begin
+  v_bypass := current_setting('app.plan_migration_bypass', true);
+
+  if v_bypass = '1' then
+    return new;
+  end if;
+
+  if new.target is distinct from old.target then
+    raise exception 'Não é permitido alterar target do plano (%) de % para %.',
+      old.key, old.target, new.target
+      using errcode = 'P0001';
+  end if;
+
+  return new;
+end
+$$;
+ +

12.2 Função administrativa (SECURITY DEFINER)

+
create or replace function public.admin_fix_plan_target(
+  p_plan_key text,
+  p_new_target text
+) returns void
+language plpgsql
+security definer
+as $$
+declare
+  v_plan_id uuid;
+begin
+  if p_new_target not in ('clinic','therapist') then
+    raise exception 'Target inválido: %', p_new_target using errcode='P0001';
+  end if;
+
+  select id into v_plan_id
+  from public.plans
+  where key = p_plan_key
+  for update;
+
+  if v_plan_id is null then
+    raise exception 'Plano não encontrado: %', p_plan_key using errcode='P0001';
+  end if;
+
+  if exists (select 1 from public.subscriptions s where s.plan_id = v_plan_id) then
+    raise exception 'Plano % possui subscriptions. Migração bloqueada.', p_plan_key using errcode='P0001';
+  end if;
+
+  perform set_config('app.plan_migration_bypass', '1', true);
+
+  update public.plans
+  set target = p_new_target
+  where id = v_plan_id;
+end
+$$;
+ +

12.3 Execução (caso real)

+
select public.admin_fix_plan_target('clinic_free', 'clinic');
+ +
+ Resultado: plano core corrigido, guardrail permanece ativo. Bypass vale apenas na transação. +
+ +

12.4 Hardening recomendado

+
revoke execute on function public.admin_fix_plan_target(text, text) from public;
+-- depois conceder apenas ao role administrativo apropriado
+
+ +
+

13. Seeder idempotente: features + plan_features

+

+ O banco já continha features. O mapeamento MVP de plan_features foi validado e segue a ideia: + PRO habilita tudo e limites “altos”; FREE habilita subset e limites menores. +

+
-- padrão do seed (exemplo):
+-- insert into features(key, descricao, description) values (...)
+-- on conflict (key) do update set ...
+
+-- insert into plan_features(plan_id, feature_id, enabled, limits) values (...)
+-- on conflict (plan_id, feature_id) do update set enabled=excluded.enabled, limits=excluded.limits;
+
+ Dica operacional: manter seed idempotente evita “duplicação” e reduz bugs em ambientes de teste. +
+
+ +
+

14. Seeder idempotente: subscription de teste

+

Como as views dependem de uma subscription ativa, criamos uma assinatura manual de teste para um tenant real.

+
insert into public.subscriptions (
+  tenant_id,
+  plan_id,
+  status,
+  plan_key,
+  "interval",
+  current_period_start,
+  current_period_end,
+  provider,
+  source
+)
+values (
+  '<TENANT_UUID>',
+  (select id from public.plans where key = 'clinic_free'),
+  'active',
+  'clinic_free',
+  'month',
+  now(),
+  null,
+  'manual',
+  'manual'
+);
+ +
+ Validação: após inserir, v_tenant_active_subscription e v_tenant_entitlements_json devem retornar dados. +
+
+ +
+

15. Front-end: padrões e telas

+

Padrões adotados nesta sessão:

+
    +
  • Em arquivos Vue: scripttemplatestyle.
  • +
  • Busca com FloatLabel + IconField + InputIcon.
  • +
  • Telas operacionais: DataTable com paginação, estados empty e UX “foco” via ?q=....
  • +
+ +
+ Melhorias aplicadas: Cards para “foco”, botão voltar no topo, textos mais claros e layout mais estável. +
+
+ +
+

16. Troubleshooting (erros reais)

+ +

16.1 “Não é permitido alterar target do plano …”

+

Causa: trigger guard_no_change_plan_target. Solução: bypass controlado + função admin (seção 12).

+ +

16.2 “Não é permitido alterar a key do plano padrão …”

+

Causa: trigger guard_no_change_core_plan_key. Solução: não renomear core; criar novo plano se necessário.

+ +

16.3 Entitlements view vazia

+

Causa: ausência de subscription ativa em v_tenant_active_subscription. Solução: inserir subscription de teste (seção 14).

+ +
+ Lembrete: quando algo “não retorna nada”, primeiro verifique as views-base antes de mexer no front. +
+
+ +
+

17. Checklist de validação

+
    +
  • Plans: core existe e está ativo; targets corretos.
  • +
  • Pricing: PRO tem preço vigente (active_to null); FREE pode ficar sem price.
  • +
  • Subscriptions: existe ao menos 1 assinatura ativa para testar.
  • +
  • Entitlements: v_tenant_entitlements_json retorna mapa com allowed + limits.
  • +
  • Guardrails: triggers ativas; correção de core somente via função admin.
  • +
  • Front: telas operacionais OK; foco via query; layout consistente.
  • +
+
+ Meta: com este checklist, qualquer dev/operador consegue diagnosticar Billing em minutos. +
+
+ +
+
+
Agência PSI — Documento Mestre Billing v2.0
+
Gerado em 2026-03-01 10:43:18 UTC. Estrutura inspirada no padrão interno com sidebar + anchors.
+
+
+
+ + diff --git a/Nova-Dev-Doc/Billing → Subscription → Entitlements/Agencia_PSI_Billing_Subscriptions_v1_2.html b/Nova-Dev-Doc/Billing → Subscription → Entitlements/Agencia_PSI_Billing_Subscriptions_v1_2.html new file mode 100644 index 0000000..f83eb47 --- /dev/null +++ b/Nova-Dev-Doc/Billing → Subscription → Entitlements/Agencia_PSI_Billing_Subscriptions_v1_2.html @@ -0,0 +1,206 @@ + + + + + +Agência PSI — Billing & Subscriptions v1.2 + + + + +

Billing & Subscriptions — v1.2

+

Agência PSI — Documento consolidado da sessão técnica sobre Subscriptions, Guardrails e Seeder.

+ +
+

1. Escopo desta versão

+

Este documento consolida tudo o que foi tratado nesta sessão:

+ +
+ +
+

2. Estrutura confirmada — subscriptions

+
id uuid PK
+tenant_id uuid NULL
+user_id uuid NULL
+plan_id uuid NOT NULL
+plan_key text NULL
+interval text CHECK ('month','year')
+status text DEFAULT 'active'
+current_period_start timestamptz
+current_period_end timestamptz
+provider text DEFAULT 'manual'
+cancel_at_period_end boolean DEFAULT false
+created_at timestamptz DEFAULT now()
+updated_at timestamptz DEFAULT now()
+ +
+Assinatura de clínica exige tenant_id. +Assinatura de terapeuta pode usar user_id. +
+
+ +
+

3. Guardrails (Proteções de Integridade)

+ +

3.1 Impedir deletar planos core

+
create or replace function guard_no_delete_core_plans()
+returns trigger language plpgsql as $$
+begin
+  if old.key in ('clinic_free','clinic_pro','therapist_free','therapist_pro') then
+    raise exception 'Plano padrão (%) não pode ser removido.', old.key;
+  end if;
+  return old;
+end $$;
+ +

3.2 Impedir alterar target

+
create or replace function guard_no_change_plan_target()
+returns trigger language plpgsql as $$
+begin
+  if new.target is distinct from old.target then
+    raise exception 'Não é permitido alterar target do plano.';
+  end if;
+  return new;
+end $$;
+ +

3.3 Impedir alterar key core

+
create or replace function guard_no_change_core_plan_key()
+returns trigger language plpgsql as $$
+begin
+  if old.key in ('clinic_free','clinic_pro','therapist_free','therapist_pro')
+     and new.key is distinct from old.key then
+    raise exception 'Não é permitido alterar a key do plano padrão.';
+  end if;
+  return new;
+end $$;
+ +
+Esses guardrails impediram alterações indevidas quando tentamos renomear planos core. +
+
+ +
+

4. Views Oficiais

+

v_public_pricing — Tela pública de preços.

+

v_tenant_active_subscription — Plano vigente do tenant.

+

v_subscription_health — Diagnóstico de inconsistências.

+
+ +
+

5. Seeder Oficial (MVP)

+ +
insert into plans (key,name,target,is_active)
+values
+('clinic_free','Clinic Free','clinic',true),
+('clinic_pro','Clinic Pro','clinic',true),
+('therapist_free','Therapist Free','therapist',true),
+('therapist_pro','Therapist Pro','therapist',true)
+on conflict (key) do update set
+name=excluded.name,
+target=excluded.target,
+is_active=excluded.is_active;
+ +
+Seeder é idempotente. Pode rodar múltiplas vezes sem duplicar. +
+
+ +
+

6. Incidentes reais resolvidos

+ +

6.1 Pricing retornando null

+

Causa: não havia preço vigente (is_active=true e active_to is null).

+ +

6.2 Erro ao alterar plano padrão

+

Causa: trigger guard_no_change_core_plan_key bloqueando alteração.

+ +

6.3 Assinatura sem tenant_id

+

Causa: regra de negócio no banco impedindo clinic sem tenant.

+ +
+ +
+

7. Diretrizes finais

+ +
+ + + + + diff --git a/Nova-Dev-Doc/Billing → Subscription → Entitlements/documentacao-billing-completa-agencia-psi.html b/Nova-Dev-Doc/Billing → Subscription → Entitlements/documentacao-billing-completa-agencia-psi.html new file mode 100644 index 0000000..528a543 --- /dev/null +++ b/Nova-Dev-Doc/Billing → Subscription → Entitlements/documentacao-billing-completa-agencia-psi.html @@ -0,0 +1,308 @@ + + + + + +Arquitetura Técnica Completa — Billing & Assinaturas | Agência PSI + + + + +

Arquitetura Técnica Completa — Billing & Assinaturas

+

Projeto: Agência PSI • Documento estrutural definitivo do domínio de Billing.

+ +
+

1. Visão Arquitetural Geral

+ +
+USUÁRIO + │ + ▼ +subscription_intents (VIEW unificada) + │ + ▼ (RPC activate_subscription_from_intent) +subscriptions + │ + ▼ +subscription_events (auditoria) + │ + ▼ +entitlements (derivados do plano) +
+ +

Separação estrutural:

+ +
+ +
+

2. Estados Oficiais da Assinatura

+
+pending
+active
+past_due
+suspended
+cancelled
+expired
+
+
+ +
+

3. Índice de Integridade de Preços

+
+create unique index if not exists uq_plan_price_active
+on plan_prices (plan_id, interval, currency)
+where is_active = true and active_to is null;
+
+
+ +
+

4. Função Completa — activate_subscription_from_intent

+
+CREATE OR REPLACE FUNCTION public.activate_subscription_from_intent(p_intent_id uuid)
+RETURNS subscriptions
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $function$
+declare
+  v_intent record;
+  v_sub public.subscriptions;
+  v_days int;
+  v_user_id uuid;
+  v_plan_id uuid;
+  v_target text;
+begin
+
+  select * into v_intent
+  from public.subscription_intents
+  where id = p_intent_id;
+
+  if not found then
+    raise exception 'Intent não encontrada';
+  end if;
+
+  if v_intent.status <> 'paid' then
+    raise exception 'Intent precisa estar paid';
+  end if;
+
+  select p.id, p.target
+    into v_plan_id, v_target
+  from public.plans p
+  where p.key = v_intent.plan_key
+  limit 1;
+
+  if v_plan_id is null then
+    raise exception 'Plano não encontrado';
+  end if;
+
+  v_target := lower(coalesce(v_target,''));
+
+  if v_target = 'clinic' and v_intent.tenant_id is null then
+    raise exception 'Intent clinic exige tenant_id';
+  end if;
+
+  if v_target = 'therapist' and v_intent.tenant_id is not null then
+    raise exception 'Intent therapist não deve ter tenant_id';
+  end if;
+
+  v_days := case when v_intent.interval = 'year' then 365 else 30 end;
+
+  v_user_id := coalesce(v_intent.created_by_user_id, v_intent.user_id);
+
+  if v_user_id is null then
+    raise exception 'user_id obrigatório';
+  end if;
+
+  if v_target = 'clinic' then
+    update subscriptions
+    set status = 'cancelled',
+        cancelled_at = now()
+    where tenant_id = v_intent.tenant_id
+      and status = 'active';
+  else
+    update subscriptions
+    set status = 'cancelled',
+        cancelled_at = now()
+    where user_id = v_user_id
+      and tenant_id is null
+      and status = 'active';
+  end if;
+
+  insert into subscriptions (
+    user_id,
+    plan_id,
+    status,
+    current_period_start,
+    current_period_end,
+    tenant_id,
+    plan_key,
+    interval,
+    provider,
+    started_at,
+    activated_at
+  )
+  values (
+    v_user_id,
+    v_plan_id,
+    'active',
+    now(),
+    now() + make_interval(days => v_days),
+    case when v_target='clinic' then v_intent.tenant_id else null end,
+    v_intent.plan_key,
+    v_intent.interval,
+    'manual',
+    now(),
+    now()
+  )
+  returning * into v_sub;
+
+  return v_sub;
+
+end;
+$function$;
+
+
+ +
+

5. Função Completa — transition_subscription (segura)

+
+create or replace function public.transition_subscription(
+  p_subscription_id uuid,
+  p_to_status text,
+  p_reason text default null,
+  p_metadata jsonb default null
+)
+returns subscriptions
+language plpgsql
+security definer
+as $$
+declare
+  v_sub subscriptions;
+  v_uid uuid;
+  v_allowed boolean := false;
+begin
+
+  v_uid := auth.uid();
+
+  select * into v_sub
+  from subscriptions
+  where id = p_subscription_id;
+
+  if not found then
+    raise exception 'Assinatura não encontrada';
+  end if;
+
+  if is_saas_admin() then
+    v_allowed := true;
+  end if;
+
+  if not v_allowed
+     and v_sub.tenant_id is null
+     and v_sub.user_id = v_uid then
+    v_allowed := true;
+  end if;
+
+  if not v_allowed
+     and v_sub.tenant_id is not null then
+
+    if exists (
+      select 1 from tenant_members tm
+      where tm.tenant_id = v_sub.tenant_id
+        and tm.user_id = v_uid
+        and tm.status = 'active'
+        and tm.role = 'tenant_admin'
+    ) then
+      v_allowed := true;
+    end if;
+
+  end if;
+
+  if not v_allowed then
+    raise exception 'Sem permissão';
+  end if;
+
+  update subscriptions
+  set status = p_to_status,
+      updated_at = now()
+  where id = p_subscription_id
+  returning * into v_sub;
+
+  insert into subscription_events (
+    subscription_id,
+    owner_id,
+    event_type,
+    created_at,
+    created_by,
+    source,
+    reason,
+    metadata
+  )
+  values (
+    v_sub.id,
+    coalesce(v_sub.tenant_id, v_sub.user_id),
+    'status_changed',
+    now(),
+    v_uid,
+    'manual_transition',
+    p_reason,
+    p_metadata
+  );
+
+  return v_sub;
+
+end;
+$$;
+
+
+ +
+

6. Máquina de Estados Recomendada

+ +
+pending → active → past_due → suspended → cancelled + ↓ + expired +
+ +

Recomendação futura: validar allowed_transitions em tabela dedicada.

+
+ +
+

7. Checklist de Validação Estrutural

+ +
+ +
+

8. Roadmap Estrutural Futuro

+ +
+ +
+

Documento técnico estrutural consolidado após implementação real validada.

+ + + diff --git a/Nova-Dev-Doc/Planos/dev-documentacao-planos-seeder-complemento.html b/Nova-Dev-Doc/Planos/dev-documentacao-planos-seeder-complemento.html new file mode 100644 index 0000000..d8b4a83 --- /dev/null +++ b/Nova-Dev-Doc/Planos/dev-documentacao-planos-seeder-complemento.html @@ -0,0 +1,231 @@ + + + + + + +Agência PSI — Billing (Arquitetura Oficial v1.1) + + + + + +
+ +
+

Billing — Arquitetura Oficial v1.1

+

Versão 1.1 inclui procedimento formal de migração controlada para planos core, mantendo guardrails ativos e auditáveis.

+
+ +
+

1. Fundamentos do Domínio

+

Billing define recursos e limites do produto. Não é camada de UI. É camada estrutural.

+
Princípio: Role (RBAC) ≠ Plano (Billing). Plano dirige features e limites; role dirige acesso.
+
+ +
+

2. Planos Core (MVP)

+
    +
  • clinic_free
  • +
  • clinic_pro
  • +
  • therapist_free
  • +
  • therapist_pro
  • +
+
Política: Planos core são estruturalmente protegidos por triggers.
+
+ +
+

3. Governança de Guardrails

+
    +
  • Impedem alterar key de plano core
  • +
  • Impedem alterar target de plano core
  • +
  • Impedem deletar plano com subscription ativa
  • +
+
Proibido: desabilitar triggers diretamente em produção.
+
+ +
+

4. Procedimento Oficial de Correção de Plano Core

+

Correções estruturais devem ocorrer via função administrativa controlada.

+ +
+create or replace function admin_fix_plan_target(
+  p_plan_key text,
+  p_new_target text
+) returns void
+language plpgsql
+security definer
+as $$
+declare
+  v_plan_id uuid;
+begin
+  select id into v_plan_id
+  from plans
+  where key = p_plan_key
+  for update;
+
+  if v_plan_id is null then
+    raise exception 'Plano não encontrado.';
+  end if;
+
+  if exists (
+    select 1 from subscriptions where plan_id = v_plan_id
+  ) then
+    raise exception 'Plano possui subscriptions ativas.';
+  end if;
+
+  update plans
+  set target = p_new_target
+  where id = v_plan_id;
+
+end;
+$$;
+
+ +
+Esta função deve ser executada apenas por role administrativa e registrada em log de auditoria. +
+
+ +
+

5. Entitlements (Contrato Oficial)

+

Entitlements são derivados exclusivamente de plan_features.

+
+plan_features (
+  plan_id uuid,
+  feature_id uuid,
+  enabled boolean,
+  limits jsonb
+)
+
+
+Formato oficial de limits: +{"max": 30} +{"per_month": 40} +{"max_users": 1} +
+
+ +
+

6. Preço Vigente

+
+create unique index uq_plan_price_active
+on plan_prices (plan_id, interval, currency)
+where is_active = true and active_to is null;
+
+
+ +
+

7. Onboarding

+
    +
  • Tenant clinic → clinic_free
  • +
  • Tenant therapist → therapist_free
  • +
+
+ +
+Agência PSI — Billing Arquitetura Oficial v1.1 +
+ +
+ + diff --git a/Nova-Dev-Doc/Planos/dev-documentacao-planos-seeder-v1.html b/Nova-Dev-Doc/Planos/dev-documentacao-planos-seeder-v1.html new file mode 100644 index 0000000..79ff2a3 --- /dev/null +++ b/Nova-Dev-Doc/Planos/dev-documentacao-planos-seeder-v1.html @@ -0,0 +1,957 @@ + + + + + + Documentação Interna — Planos, Assinaturas e Seeder (Billing) | Agência PSI + + + + +
+ +
+

Agência PSI • Documento interno

+

Planos, Assinaturas e Seeder — Billing (MVP)

+

+ Documentação interna do domínio de Billing do SaaS multi-tenant (Agência PSI), + cobrindo modelo de dados, views oficiais, catálogo de planos, + princípios de produto e um seeder idempotente para instalação nova. + O objetivo é impedir divergência entre UI, backend e banco (e evitar pricing nulo, upgrade quebrado e gating inconsistente).

Atualizado em: 2026-03-01 (após validações reais do schema e execução do seeder). +

+
+ + + +
+ +
+

1. Visão geral do domínio

+

+ O Billing define o que pode e o quanto pode dentro do produto. + Ele não é “uma tela de preço”: é a camada que decide + limites (quantidade), habilitações (booleanos) e estado de assinatura. +

+
+ Definição operacional: o Billing é composto por (1) catálogo de planos, (2) preços vigentes, + (3) assinatura ativa por tenant/usuário, e (4) entitlements derivados do plano. +
+
+ Objetivo do MVP: todo mundo começa no FREE (clínica e terapeuta). + Paciente não é pagante; o “portal do paciente” é um recurso habilitado pelo plano do terapeuta/clínica. +
+
+ +
+

2. Princípios e decisões

+
    +
  • Separação rígida: Role (RBAC) não é Plano (Billing). Plano define recursos; role define permissões de acesso.
  • +
  • Planos por target: existe plano de clinic e plano de therapist. Isso impede aplicar plano errado em outro tipo de conta.
  • +
  • Tudo começa gratuito: criação de tenant atribui automaticamente um plano *_free.
  • +
  • Pricing público por View: a UI de preços deve consumir v_public_pricing (não montar preço manual no front).
  • +
  • Preço é temporal: preço tem vigência (active_from/active_to) e um “ativo atual”.
  • +
  • Seeder é padrão: nova instalação do banco deve nascer com os 4 planos do MVP + public metadata + preços PRO.
  • +
+
+ Problema real observado: a view v_public_pricing retornou preços null porque havia histórico em plan_prices mas nenhum registro vigente (todos com is_active=false e active_to preenchido). +
+
+ +
+

3. Conceitos: role vs target vs plano vs feature

+ +
+
+
Role (RBAC)permissão de UI/rotas (clinic_admin, therapist, patient etc.)
+
Target (produto)tipo de conta: clinic ou therapist
+
Plano (billing)free/pro por target; é o “pacote” contratado
+
Feature / Limiteentitlements: booleanos e limites numéricos derivados do plano
+
+
+ +

3.1 Regra do produto: “um usuário pode ser paciente e terapeuta”

+

+ Essa regra é de identidade (um mesmo user pode estar em múltiplos contextos), + mas o plano é aplicado ao tenant (clínica/terapeuta). Assim, um usuário pode: +

+
    +
  • estar em um tenant therapist (com therapist_free/pro)
  • +
  • estar em um tenant clinic (com clinic_free/pro)
  • +
  • acessar portal de paciente como consumidor do serviço (sem plano próprio)
  • +
+ +
+ Consequência: plano nunca deve ser inferido do role. + O role dirige menus/rotas; o plano dirige features/limites. +
+
+ +
+

4. Modelo de dados (Postgres/Supabase)

+ +

4.1 Tabelas mapeadas (schema: public)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TabelaResponsabilidadeObservações práticas
plansCatálogo interno de planos (id, key, target, flags e campos legados de preço)Não usar plans.price_cents como preço público; é legado/fallback.
plan_pricesPreços por intervalo e moeda, com vigênciaFonte do valor monetário; a view pública agrega mensal/anual.
plan_featuresEntitlements por plano (limites e habilitações)Define o que o produto permite no runtime (gating).
plan_publicMarketing/metadata do plano (nome público, descrição, badge, destaque, visibilidade)Direciona a tela de preços e o “tom” comercial.
plan_public_bulletsBullets de venda por planoLista simples; a view pode agregá-las em array.
subscriptionsAssinatura ativa (por tenant ou user) e statusFonte de verdade do plano vigente do tenant.
subscription_eventsHistórico de mudanças (old/new plan)Útil para auditoria e debug de upgrades.
subscription_intentsIntenção/checkout pendenteControla upgrade antes de virar subscription.
entitlements_invalidationInvalidação de cache de entitlementsGarante refresh quando plano muda.
+ +

4.2 Padrão de “preço vigente”

+
+ Armada clássica: se não existir pelo menos 1 preço vigente por (plan_id, interval, currency), + a tela de pricing pode retornar null e o checkout fica sem referência. +
+ +
-- Um preço é considerado vigente quando:
+-- is_active = true
+-- AND active_to IS NULL
+-- AND now() >= active_from (se active_from existir)
+
+
+ +
+

5. Views oficiais (fonte de verdade)

+ +

5.1 View pública de pricing (UI deve consumir)

+
+ UI MUST: a tela de preços deve consultar v_public_pricing. + Evitar compor preços no front com join manual, pois isso cria divergência e bugs silenciosos. +
+
select
+  plan_key,
+  plan_name,
+  public_name,
+  public_description,
+  badge,
+  is_featured,
+  is_visible,
+  sort_order,
+  monthly_cents,
+  yearly_cents,
+  monthly_currency,
+  yearly_currency,
+  bullets,
+  plan_target
+from v_public_pricing
+order by plan_target, sort_order;
+ +

5.2 View de preços ativos (infra/diagnóstico)

+
select *
+from v_plan_active_prices
+order by plan_id;
+ +

5.3 View de assinatura do tenant (gating/RBAC por plano)

+
select *
+from v_tenant_active_subscription;
+ +

5.4 View de saúde de assinaturas (debug)

+
select *
+from v_subscription_health
+where status <> 'healthy';
+
+ + + +
+

7. Preços (MVP) e vigência

+ +

7.1 Preços sugeridos

+
+
    +
  • clinic_free: Grátis (sem preço, ou 0 se o front exigir número)
  • +
  • clinic_pro: mensal R$ 149 (14900), anual R$ 1490 (149000)
  • +
  • therapist_free: Grátis (sem preço, ou 0)
  • +
  • therapist_pro: mensal R$ 49 (4900), anual R$ 490 (49000)
  • +
+
+ +

7.2 Regras de vigência

+
+ Regra recomendada: 1 preço vigente por (plan_id, interval, currency). + Para prevenir inconsistência, criar índice único parcial. +
+ +
create unique index if not exists uq_plan_price_active
+on plan_prices (plan_id, interval, currency)
+where is_active = true and active_to is null;
+ +
+ Anti-padrão: encerrar todos preços e esquecer de inserir os novos. Resultado: v_public_pricing com null. +
+
+ +
+

8. Seeder (nova instalação) — SQL idempotente

+ +
+ Objetivo do seeder: instalar (1) planos, (2) metadata pública, (3) bullets, (4) preços PRO vigentes + e (opcional) (5) entitlements iniciais. + O script deve ser idempotente: rodar várias vezes sem duplicar registros. +
+ +

8.1 Convenções do seeder

+
    +
  • Usar plans.key como chave estável (única). A view pública expõe isso como plan_key.
  • +
  • Para inserts, preferir insert ... on conflict ... do update quando houver unique constraint.
  • +
  • Para preços: encerrar preço vigente anterior e inserir um novo (ou atualizar, conforme sua política).
  • +
  • Manter source='manual' no MVP (provider pode entrar depois com Stripe).
  • +
+ +

8.2 Seeder completo (MVP)

+
-- ============================================================
+-- SEEDER — BILLING (MVP) • SCHEMA REAL (confirmado)
+-- Planos finais: clinic_free, clinic_pro, therapist_free, therapist_pro
+-- Observação: v_public_pricing expõe (plan_key/plan_target), mas na tabela base é (plans.key / plans.target).
+-- ============================================================
+
+-- 0) Proteção: 1 preço vigente por (plan_id, interval, currency)
+create unique index if not exists uq_plan_price_active
+on plan_prices (plan_id, interval, currency)
+where is_active = true and active_to is null;
+
+-- 1) Plans (public.plans) — usa colunas reais: key, name, target
+insert into plans (key, name, description, is_active, price_cents, currency, billing_interval, target)
+values
+  ('clinic_free',    'CLINIC FREE',    'Plano gratuito para clínicas iniciarem.', true, 0,     'BRL', 'month', 'clinic'),
+  ('clinic_pro',     'CLINIC PRO',     'Plano completo para clínicas.',          true, 14900, 'BRL', 'month', 'clinic'),
+  ('therapist_free', 'THERAPIST FREE', 'Plano gratuito para terapeutas.',        true, 0,     'BRL', 'month', 'therapist'),
+  ('therapist_pro',  'THERAPIST PRO',  'Plano completo para terapeutas.',        true, 4900,  'BRL', 'month', 'therapist')
+on conflict (key) do update
+set name = excluded.name,
+    description = excluded.description,
+    is_active = excluded.is_active,
+    price_cents = excluded.price_cents,
+    currency = excluded.currency,
+    billing_interval = excluded.billing_interval,
+    target = excluded.target;
+
+-- 2) Plan public (public.plan_public) — metadata de pricing
+with p as (
+  select id, key from plans
+  where key in ('clinic_free','clinic_pro','therapist_free','therapist_pro')
+)
+insert into plan_public (plan_id, public_name, public_description, badge, is_featured, is_visible, sort_order)
+select
+  id,
+  case key
+    when 'clinic_free' then 'Clínica — Free'
+    when 'clinic_pro' then 'Clínica — PRO'
+    when 'therapist_free' then 'Terapeuta — Free'
+    when 'therapist_pro' then 'Terapeuta — PRO'
+  end,
+  case key
+    when 'clinic_free' then 'Para clínicas pequenas começarem sem cartão.'
+    when 'clinic_pro' then 'Para clínicas que querem recursos completos.'
+    when 'therapist_free' then 'Para começar e organizar sua prática.'
+    when 'therapist_pro' then 'Para expandir com automações e escala.'
+  end,
+  case key
+    when 'clinic_free' then 'Grátis'
+    when 'therapist_free' then 'Grátis'
+    else null
+  end,
+  case key
+    when 'clinic_pro' then true
+    when 'therapist_pro' then true
+    else false
+  end,
+  true,
+  case key
+    when 'clinic_free' then 10
+    when 'clinic_pro' then 20
+    when 'therapist_free' then 10
+    when 'therapist_pro' then 20
+  end
+from p
+on conflict (plan_id) do update
+set public_name = excluded.public_name,
+    public_description = excluded.public_description,
+    badge = excluded.badge,
+    is_featured = excluded.is_featured,
+    is_visible = excluded.is_visible,
+    sort_order = excluded.sort_order;
+
+-- 3) Bullets (public.plan_public_bullets) — reset simples para MVP
+delete from plan_public_bullets
+where plan_id in (select id from plans where key in ('clinic_free','clinic_pro','therapist_free','therapist_pro'));
+
+insert into plan_public_bullets (plan_id, text, highlight, sort_order)
+values
+  ((select id from plans where key='clinic_free'),    '1 terapeuta incluído', false, 10),
+  ((select id from plans where key='clinic_free'),    'Até 30 pacientes',     false, 20),
+  ((select id from plans where key='clinic_free'),    'Até 100 sessões/mês',  false, 30),
+
+  ((select id from plans where key='clinic_pro'),     'Terapeutas ilimitados', true,  10),
+  ((select id from plans where key='clinic_pro'),     'Pacientes ilimitados',  true,  20),
+  ((select id from plans where key='clinic_pro'),     'Relatórios e lembretes', false, 30),
+
+  ((select id from plans where key='therapist_free'), 'Até 10 pacientes',     false, 10),
+  ((select id from plans where key='therapist_free'), 'Até 40 sessões/mês',   false, 20),
+  ((select id from plans where key='therapist_free'), 'Portal do paciente',   false, 30),
+
+  ((select id from plans where key='therapist_pro'),  'Pacientes ilimitados', true,  10),
+  ((select id from plans where key='therapist_pro'),  'Sessões ilimitadas',   true,  20),
+  ((select id from plans where key='therapist_pro'),  'Relatórios e lembretes', false, 30);
+
+-- 4) Preços vigentes (public.plan_prices) — somente PRO
+do $$
+declare
+  v_clinic_pro uuid;
+  v_therapist_pro uuid;
+begin
+  select id into v_clinic_pro from plans where key='clinic_pro';
+  select id into v_therapist_pro from plans where key='therapist_pro';
+
+  update plan_prices
+  set is_active = false, active_to = now()
+  where plan_id in (v_clinic_pro, v_therapist_pro)
+    and is_active = true
+    and active_to is null;
+
+  insert into plan_prices (plan_id, currency, interval, amount_cents, is_active, active_from, active_to, source, provider, provider_price_id)
+  values
+    (v_clinic_pro, 'BRL', 'month', 14900,  true, now(), null, 'manual', null, null),
+    (v_clinic_pro, 'BRL', 'year',  149000, true, now(), null, 'manual', null, null),
+    (v_therapist_pro, 'BRL', 'month', 4900, true, now(), null, 'manual', null, null),
+    (v_therapist_pro, 'BRL', 'year',  49000, true, now(), null, 'manual', null, null);
+exception
+  when unique_violation then
+    raise notice 'Preço vigente já existe para algum (plan_id, interval, currency).';
+end $$;
+
+-- 5) (Opcional) Integridade: impedir apagar plano em uso
+-- A FK subscriptions.plan_id -> plans.id deve estar com ON DELETE RESTRICT.
+-- Se precisar aplicar:
+-- alter table public.subscriptions drop constraint if exists subscriptions_plan_id_fkey;
+-- alter table public.subscriptions add constraint subscriptions_plan_id_fkey
+--   foreign key (plan_id) references public.plans(id) on delete restrict;
+
+-- 6) Validação final (deve retornar 4 planos visíveis)
+select plan_key, plan_name, plan_target, monthly_cents, yearly_cents
+from v_public_pricing
+where is_visible = true
+order by plan_target, sort_order, plan_key;
+ +
+ Nota de adaptação: o seeder acima assume certas colunas (ex.: plans.plan_key, plans.plan_target, plans.is_active, plan_public.*). + Se o seu schema tiver nomes diferentes, ajuste no primeiro uso e depois mantenha como “padrão oficial”. +
+
+ + +
+

8B. Entitlements — Schema real (plan_features)

+

+ O MVP usa plan_features como tabela de ligação entre plano e feature. O schema confirmado é: + (plan_id uuid NOT NULL, feature_id uuid NOT NULL, enabled boolean NOT NULL default true, limits jsonb NULL). +

+
+ Padrão recomendado para limits (jsonb): padronizar chaves por tipo de limite para evitar ambiguidade no front/back. + Sugestão: + {"max": 30} (limite absoluto), + {"per_month": 40} (por período), + {"max_users": 1} (limite de assentos), + e manter enabled como flag binária. +
+
+ Pré-requisito: para seedar entitlements, é necessário listar/definir as features na tabela de features (ex.: features). + Este documento mantém os limites do MVP como referência de produto; o seeder de plan_features deve mapear essas chaves para feature_id reais. +
+ +

Template (exemplo) — como gravar limites

+
-- Exemplo: habilitar feature X com limite max=30 para clinic_free
+insert into plan_features (plan_id, feature_id, enabled, limits)
+values (
+  (select id from plans where key='clinic_free'),
+  'FEATURE_UUID_AQUI',
+  true,
+  '{"max": 30}'::jsonb
+);
+
+ +
+

8C. Regras de negócio confirmadas no banco

+
+ Regra confirmada: inserir subscription de clinic_* exige tenant_id. + Em testes, uma tentativa de inserir assinatura de clínica sem tenant resultou em erro: + “Assinatura clinic exige tenant_id.” +
+
+ Consequência: assinatura de clínica é “por tenant”; assinatura de terapeuta pode ser por tenant_id ou user_id, + conforme sua arquitetura — mas o banco já impõe pelo menos o caso de clínica. +
+
+ + +
+

9. Onboarding & Upgrade (fluxo)

+ +

9.1 Onboarding (criação de tenant)

+
    +
  • Ao criar um tenant clinic → atribuir automaticamente clinic_free.
  • +
  • Ao criar um tenant therapist → atribuir automaticamente therapist_free.
  • +
  • O plano deve ser a fonte de verdade para habilitar recursos (entitlements store).
  • +
+ +

9.2 Upgrade

+
+ Upgrade é troca de plano na assinatura: *_free → *_pro. + O sistema deve invalidar entitlements e atualizar cache (via entitlements_invalidation ou mecanismo equivalente). +
+ +

9.3 Downgrade/expiração

+

+ No MVP, a regra segura é: ao expirar, bloquear novas criações premium, + mas não apagar dados. Apenas retira capacidade. +

+
+ +
+

10. Operação (runbook rápido)

+ +
+

Incidente comum: Pricing mostra preços nulos

+
    +
  1. Rodar select * from v_public_pricing;
  2. +
  3. Rodar select * from plan_prices where plan_id = ... order by created_at desc;
  4. +
  5. Confirmar existência de preço vigente: is_active=true e active_to is null
  6. +
  7. Se não existir, inserir preços PRO vigentes (month/year) e validar view novamente.
  8. +
+
+ +
+

Incidente comum: Plano aparece errado para um tenant

+
    +
  1. Verificar v_tenant_active_subscription para o tenant em questão.
  2. +
  3. Verificar se o plano tem plan_target correto.
  4. +
  5. Verificar se o guard/menu não está inferindo plano do role (anti-padrão).
  6. +
  7. Invalidar entitlements e reavaliar.
  8. +
+
+
+ +
+

11. Checklist de QA

+
    +
  • Seeder: rodar duas vezes e confirmar que não duplica registros.
  • +
  • Pricing: v_public_pricing retorna 4 planos, com preços preenchidos para PRO.
  • +
  • Upgrade: trocar plano e confirmar mudança de entitlements no runtime.
  • +
  • FREE: criação de tenant atribui automaticamente plano free correto.
  • +
  • Target: clínica nunca recebe plano therapist (e vice-versa).
  • +
  • Vigência: inserir novo preço e confirmar que o antigo foi encerrado (active_to preenchido).
  • +
+
+ +
+

12. Prompt Mestre — Continuidade do Billing (Planos/Assinaturas)

+ +
+ Sempre que iniciar um novo chat sobre Billing/Planos, copie e cole este prompt. + Ele representa o estado oficial do domínio e da estrutura do banco para o MVP. +
+ +

+Estou desenvolvendo um SaaS clínico multi-tenant usando Supabase (Postgres + RLS + Views)
+com planos e assinaturas.
+
+══════════════════════════════════════
+📦 Domínio: Billing / Planos
+══════════════════════════════════════
+
+Decisões do MVP:
+- Tudo começa grátis (clinic e therapist).
+- Paciente não tem plano (portal do paciente é feature do plano do therapist/clinic).
+- Plano (billing) NÃO é role (RBAC). Role dirige menus/rotas; plano dirige features/limites.
+- Planos por target: clinic e therapist.
+
+Catálogo de planos (MVP):
+- clinic_free
+- clinic_pro
+- therapist_free
+- therapist_pro
+
+Views fonte de verdade:
+- v_public_pricing (tela de preços)
+- v_plan_active_prices (infra)
+- v_tenant_active_subscription (gating por tenant)
+- v_subscription_health (debug)
+
+Tabelas principais:
+- plans (colunas reais: key, target, ...)
+- plan_prices (tem vigência; preço vigente: is_active=true e active_to is null; a UI usa v_plan_active_prices)
+- plan_public + plan_public_bullets (marketing)
+- plan_features (entitlements)
+- subscriptions (+ events, intents)
+- entitlements_invalidation
+
+Preços sugeridos (MVP):
+- clinic_pro: 14900/mês e 149000/ano (BRL)
+- therapist_pro: 4900/mês e 49000/ano (BRL)
+- free: grátis (pode manter sem preços)
+
+Problema já observado:
+- v_public_pricing retornou null quando plan_prices tinha histórico mas não tinha preço vigente.
+
+Estado atual (confirmado):
+- Apenas 4 planos existem (clinic_free/clinic_pro/therapist_free/therapist_pro)
+
+Objetivo do próximo passo:
+- Seedar plan_features (entitlements) mapeando features -> feature_id e limits jsonb para nova instalação com os 4 planos + public metadata + preços PRO vigentes.
+  
+ +
+ Este prompt deve ser tratado como contexto estrutural completo do Billing no MVP. + Qualquer solução proposta deve respeitar essa organização. +
+
+ +
+

Tags

+ #Billing + #Planos + #Pricing + #Seeder + #Supabase + #Postgres + #MultiTenant + #Entitlements + #Subscriptions + #v_public_pricing + #MVP +
+ +
+ +
+ Agência PSI • Documentação interna • Billing (Planos/Assinaturas/Seeder) +
+ +
+ + diff --git a/Nova-Dev-Doc/Subscription Health e Entitlements/Agencia_PSI_Sessao_Subscription_Health_Entitlements_2026-03-01.html b/Nova-Dev-Doc/Subscription Health e Entitlements/Agencia_PSI_Sessao_Subscription_Health_Entitlements_2026-03-01.html new file mode 100644 index 0000000..c6a3499 --- /dev/null +++ b/Nova-Dev-Doc/Subscription Health e Entitlements/Agencia_PSI_Sessao_Subscription_Health_Entitlements_2026-03-01.html @@ -0,0 +1,612 @@ + + + + + + Relatório Técnico — Sessão de Correção: Subscription Health & Entitlements (2026-03-01) | Agência PSI + + + +
+
+

Relatório Técnico • Billing Health • Agência PSI

+

Subscription Health & Entitlements — Sessão de Correção (2026-03-01)

+

Este documento registra, de forma minuciosa e operacional, a sessão de diagnóstico e correção dos problemas +na página Saúde das Assinaturas (Subscription Health) e no pipeline de Entitlements. +O objetivo é permitir que qualquer programador entenda o incidente, replique o diagnóstico e aplique correções +com segurança, mesmo sem ter acompanhado a conversa original.

+
+ Estado: resolvido e hardenizado + Atualizado: 2026-03-01 11:46:44 UTC + Stack: Supabase + Postgres + Vue/PrimeVue +
+
+ + + +
+
+ +
+

1. Resumo executivo

+ +

+ Sintoma principal: a tela Saúde das Assinaturas exibia divergências e a coluna Owner aparecia vazia + (linhas com owner_id = NULL). Além disso, os botões Fix e Fix All falhavam. +

+
+ Impacto: a ferramenta de diagnóstico do Billing ficou pouco confiável e os reparos automáticos não executavam, impedindo + correções rápidas após mudanças de plano/feature. +
+
+ Resultado final: view de entitlements corrigida (filtra subscriptions.status = 'active' e exclui NULL), + funções RPC alinhadas ao schema atual (subscriptions.user_id), dados inválidos removidos e constraints/índices adicionados + para impedir regressões. +
+ +
+
+

2. Escopo e componentes envolvidos

+ +
+
+ View de saúde +

public.v_subscription_feature_mismatch — compara o esperado (plan_features do plano ativo) com o atual (entitlements).

+
+
+ Entitlements agregados +

public.owner_feature_entitlementsVIEW agregada (sources + limits_list), derivada de subscriptions e tenant_modules.

+
+
+ Rotinas de reparo +

public.rebuild_owner_entitlements(uuid) e public.fix_all_subscription_mismatches().

+
+
+ Tabelas de configuração +

plans, features, plan_features, module_features, tenant_modules, subscriptions.

+
+
+ +
+
+

3. Sintomas observados e evidências

+ +

Foram observados os seguintes indícios no banco:

+
    +
  • View v_subscription_feature_mismatch retornando owner_id = NULL tanto em missing quanto em unexpected.
  • +
  • Contagem estável em 4/4 divergências, mesmo após tentativa de reparo.
  • +
  • Existência de uma subscription com status='active' e user_id = NULL (dado inválido).
  • +
  • Falha de execução do FixAll com erro de coluna inexistente: subscriptions.owner_id (schema drift).
  • +
+ +
+ Nota de leitura: ao ver owner_id = NULL em divergências, trate como anomalia de dados ou view/joins permissivos. + Na prática, “owner nulo” não é um caso de negócio — é um caso de integridade. +
+ +
+
+

4. Diagnóstico e causa raiz

+ +

4.1 Causa raiz #1 — Schema drift nas funções RPC

+

+ As funções de reparo estavam escritas para um schema anterior, usando subscriptions.owner_id. No schema atual, o owner do contexto + “terapeuta” é subscriptions.user_id. Isso quebrou: +

+
    +
  • Fix owner: falha ao buscar o plano ativo do owner.
  • +
  • Fix all: falha ao iterar owners e chamar o rebuild.
  • +
+ +
+ Erro observado: column "owner_id" does not exist (hint citando subscriptions.user_id). +
+ +

4.2 Causa raiz #2 — View de entitlements agregados não filtrava status

+

+ A view owner_feature_entitlements agregava a fonte “plan” sem filtrar subscriptions.status, permitindo que uma subscription + inactive com user_id NULL continuasse “vazando” entitlements com owner_id NULL para o sistema. +

+ +

4.3 Causa raiz #3 — Dado inválido

+

+ Foi identificado um registro em subscriptions com user_id NULL. Mesmo após torná-lo inactive, ele continuava contaminando + a view (por ausência do filtro de status). +

+ +
+
+

5. SQLs usados no diagnóstico (playbook)

+ +

Use este bloco para reproduzir o diagnóstico com segurança.

+ +

5.1 Ver divergências e amostras

+
select mismatch_type, count(*) as qtd
+from public.v_subscription_feature_mismatch
+group by 1
+order by 2 desc;
+
+select owner_id, feature_key, mismatch_type
+from public.v_subscription_feature_mismatch
+order by owner_id nulls first, feature_key
+limit 50;
+ +

5.2 Encontrar subscriptions inválidas (user_id nulo)

+
select id, user_id, plan_id, status, created_at
+from public.subscriptions
+where user_id is null
+order by created_at desc;
+ +

5.3 Entender a origem dos entitlements agregados

+
select pg_get_viewdef('public.owner_feature_entitlements'::regclass, true) as view_sql;
+ +

5.4 Verificar tenant_modules inválidos (owner_id nulo)

+
select count(*) as qtd
+from public.tenant_modules
+where status = 'active' and owner_id is null;
+ +
+
+

6. Correções aplicadas no banco (patches)

+ +

6.1 Patch: rebuild_owner_entitlements (owner = subscriptions.user_id)

+

+ Ajuste para buscar o plano ativo por subscriptions.user_id e reconstruir entitlements com base em plan_features. +

+
create or replace function public.rebuild_owner_entitlements(p_owner_id uuid)
+returns void
+language plpgsql
+security definer
+as $$
+declare
+  v_plan_id uuid;
+begin
+  select s.plan_id
+    into v_plan_id
+  from public.subscriptions s
+  where s.user_id = p_owner_id
+    and s.status = 'active'
+  order by s.created_at desc
+  limit 1;
+
+  delete from public.owner_feature_entitlements e
+  where e.owner_id = p_owner_id;
+
+  if v_plan_id is null then
+    return;
+  end if;
+
+  insert into public.owner_feature_entitlements (owner_id, feature_key, sources, limits_list)
+  select
+    p_owner_id,
+    f.key,
+    array['plan'::text],
+    '{}'::jsonb
+  from public.plan_features pf
+  join public.features f on f.id = pf.feature_id
+  where pf.plan_id = v_plan_id;
+end;
+$$;
+ +
+ Importante: se owner_feature_entitlements for uma VIEW (como no ambiente desta sessão), + o DELETE/INSERT acima deve ser direcionado à tabela base real de entitlements, se existir. + Nesta sessão, a correção definitiva foi feita ajustando a view agregadora e limpando o dado inválido. +
+ +

6.2 Patch: fix_all_subscription_mismatches (itera subscriptions.user_id)

+
create or replace function public.fix_all_subscription_mismatches()
+returns void
+language plpgsql
+security definer
+as $$
+declare
+  r record;
+begin
+  for r in
+    select distinct s.user_id as owner_id
+    from public.subscriptions s
+    where s.status = 'active'
+      and s.user_id is not null
+  loop
+    perform public.rebuild_owner_entitlements(r.owner_id);
+  end loop;
+end;
+$$;
+ +

6.3 Patch: owner_feature_entitlements (filtra status e NULLs)

+
create or replace view public.owner_feature_entitlements as
+with base as (
+  select
+    s.user_id as owner_id,
+    f.key as feature_key,
+    pf.limits,
+    'plan'::text as source
+  from public.subscriptions s
+  join public.plan_features pf
+    on pf.plan_id = s.plan_id
+   and pf.enabled = true
+  join public.features f
+    on f.id = pf.feature_id
+  where s.status = 'active'
+    and s.user_id is not null
+
+  union all
+
+  select
+    tm.owner_id,
+    f.key as feature_key,
+    mf.limits,
+    'module'::text as source
+  from public.tenant_modules tm
+  join public.modules m
+    on m.id = tm.module_id
+   and m.is_active = true
+  join public.module_features mf
+    on mf.module_id = m.id
+   and mf.enabled = true
+  join public.features f
+    on f.id = mf.feature_id
+  where tm.status = 'active'
+    and tm.owner_id is not null
+)
+select
+  owner_id,
+  feature_key,
+  array_agg(distinct source) as sources,
+  jsonb_agg(limits) filter (where limits is not null) as limits_list
+from base
+group by owner_id, feature_key;
+ +

6.4 Limpeza do dado inválido (subscription com user_id NULL)

+
-- se for lixo de seed/teste (recomendado remover):
+delete from public.subscriptions
+where user_id is null;
+ +
+
+

7. Hardening (constraints e índices recomendados)

+ +

Após corrigir dados e views, aplique hardening para impedir regressões.

+ +

7.1 subscriptions.user_id NOT NULL

+
alter table public.subscriptions
+alter column user_id set not null;
+ +

7.2 Uma assinatura ativa por usuário

+
create unique index if not exists subscriptions_one_active_per_user
+on public.subscriptions (user_id)
+where status = 'active';
+ +

7.3 Índice de performance para consultas por owner/status

+
create index if not exists subscriptions_user_status_idx
+on public.subscriptions (user_id, status, created_at desc);
+ +

7.4 tenant_modules.owner_id NOT NULL (decisão tomada nesta sessão)

+
alter table public.tenant_modules
+alter column owner_id set not null;
+ +

7.5 Uniqueness e performance em plan_features / module_features

+
create unique index if not exists plan_features_plan_feature_ux
+on public.plan_features (plan_id, feature_id);
+
+create index if not exists plan_features_plan_enabled_idx
+on public.plan_features (plan_id, enabled);
+
+create unique index if not exists module_features_module_feature_ux
+on public.module_features (module_id, feature_id);
+ +
+ Regra prática: dados inválidos (NULL em owner) devem ser bloqueados na borda (constraints), não “corrigidos” no front. +
+ +
+
+

8. Verificação pós-correção (checklist)

+ +

8.1 Saúde deve zerar

+
select mismatch_type, count(*) as qtd
+from public.v_subscription_feature_mismatch
+group by 1
+order by 2 desc;
+ +

8.2 Não pode haver owner nulo em subscriptions / tenant_modules ativos

+
select count(*) as subs_user_null
+from public.subscriptions
+where user_id is null;
+
+select count(*) as tenant_modules_active_owner_null
+from public.tenant_modules
+where status='active' and owner_id is null;
+ +

8.3 Entitlements agregados não devem conter owner null

+
select owner_id, feature_key
+from public.owner_feature_entitlements
+where owner_id is null
+limit 20;
+ +
+ OK final: todas as queries acima retornam 0 linhas (ou contagens zero). +
+ +
+
+

9. Notas de implementação no front-end (contexto)

+ +

Durante a sessão, a UI foi ajustada para:

+
    +
  • Traduzir telas para PT-BR, melhorar títulos, descrições e mensagens.
  • +
  • Padronizar inputs com FloatLabel + IconField + InputIcon.
  • +
  • Adicionar confirmações e “alterações pendentes” em ações em massa (plan_features), evitando salvar por clique acidental.
  • +
  • Garantir que ações de correção (Fix/FixAll) reflitam erros reais (RPC quebrada vs dados inválidos).
  • +
+ +
+ Regra operacional: se a coluna Owner aparecer vazia, corrija no banco primeiro (dados/view), + antes de mexer no front. +
+ +
+
+

10. Linha do tempo da sessão (resumo)

+ +
    +
  • Detecção: tela “Saúde das Assinaturas” exibindo Owner vazio e divergências.
  • +
  • Inspeção: v_subscription_feature_mismatch mostrava owner_id NULL em missing/unexpected.
  • +
  • Erro crítico: FixAll falhava com subscriptions.owner_id inexistente.
  • +
  • Correção #1: alinhar RPCs ao schema atual (subscriptions.user_id).
  • +
  • Correção #2: identificar que owner_feature_entitlements é VIEW e filtrar status='active'.
  • +
  • Correção #3: remover subscription inválida com user_id NULL.
  • +
  • Hardening: constraints e índices para prevenir regressões.
  • +
+ +
+ Atualizado: 2026-03-01 11:46:44 UTC +
+ +
+
+
+
Agência PSI — Relatório Técnico (Billing Health)
+
Documento operacional inspirado no “Documento Mestre Billing v2.0”. Atualizado em 2026-03-01 11:46:44 UTC.
+
+
+
+ + diff --git a/Nova-Dev-Doc/supervisor_fase1.sql b/Nova-Dev-Doc/supervisor_fase1.sql new file mode 100644 index 0000000..87ecaa1 --- /dev/null +++ b/Nova-Dev-Doc/supervisor_fase1.sql @@ -0,0 +1,414 @@ +-- ============================================================ +-- SUPERVISOR — Fase 1 +-- Aplicar no Supabase SQL Editor (em ordem) +-- ============================================================ + + +-- ──────────────────────────────────────────────────────────── +-- 1. tenants.kind → adiciona 'supervisor' +-- ──────────────────────────────────────────────────────────── +ALTER TABLE public.tenants + DROP CONSTRAINT IF EXISTS tenants_kind_check; + +ALTER TABLE public.tenants + ADD CONSTRAINT tenants_kind_check + CHECK (kind = ANY (ARRAY[ + 'therapist', + 'clinic_coworking', + 'clinic_reception', + 'clinic_full', + 'clinic', + 'saas', + 'supervisor' -- ← novo + ])); + + +-- ──────────────────────────────────────────────────────────── +-- 2. plans.target → adiciona 'supervisor' +-- ──────────────────────────────────────────────────────────── +ALTER TABLE public.plans + DROP CONSTRAINT IF EXISTS plans_target_check; + +ALTER TABLE public.plans + ADD CONSTRAINT plans_target_check + CHECK (target = ANY (ARRAY[ + 'patient', + 'therapist', + 'clinic', + 'supervisor' -- ← novo + ])); + + +-- ──────────────────────────────────────────────────────────── +-- 3. plans.max_supervisees — limite de supervisionados +-- ──────────────────────────────────────────────────────────── +ALTER TABLE public.plans + ADD COLUMN IF NOT EXISTS max_supervisees integer DEFAULT NULL; + +COMMENT ON COLUMN public.plans.max_supervisees IS + 'Limite de terapeutas que podem ser supervisionados. Apenas para planos target=supervisor. NULL = sem limite.'; + + +-- ──────────────────────────────────────────────────────────── +-- 4. Planos supervisor_free e supervisor_pro +-- ──────────────────────────────────────────────────────────── +INSERT INTO public.plans (key, name, description, target, is_active, max_supervisees) +VALUES + ( + 'supervisor_free', + 'Supervisor Free', + 'Plano gratuito de supervisão. Até 3 terapeutas supervisionados.', + 'supervisor', + true, + 3 + ), + ( + 'supervisor_pro', + 'Supervisor PRO', + 'Plano profissional de supervisão. Até 20 terapeutas supervisionados.', + 'supervisor', + true, + 20 + ) +ON CONFLICT (key) DO UPDATE + SET + name = EXCLUDED.name, + description = EXCLUDED.description, + target = EXCLUDED.target, + is_active = EXCLUDED.is_active, + max_supervisees = EXCLUDED.max_supervisees; + + +-- ──────────────────────────────────────────────────────────── +-- 5. Features de supervisor +-- ──────────────────────────────────────────────────────────── +INSERT INTO public.features (key, name, descricao) +VALUES + ( + 'supervisor.access', + 'Acesso à Supervisão', + 'Acesso básico ao espaço de supervisão (sala, lista de supervisionados).' + ), + ( + 'supervisor.invite', + 'Convidar Supervisionados', + 'Permite convidar terapeutas para participar da sala de supervisão.' + ), + ( + 'supervisor.sessions', + 'Sessões de Supervisão', + 'Agendamento e registro de sessões de supervisão.' + ), + ( + 'supervisor.reports', + 'Relatórios de Supervisão', + 'Relatórios avançados de progresso e evolução dos supervisionados.' + ) +ON CONFLICT (key) DO UPDATE + SET + name = EXCLUDED.name, + descricao = EXCLUDED.descricao; + + +-- ──────────────────────────────────────────────────────────── +-- 6. plan_features — vincula features aos planos supervisor +-- ──────────────────────────────────────────────────────────── +DO $$ +DECLARE + v_free_id uuid; + v_pro_id uuid; + v_f_access uuid; + v_f_invite uuid; + v_f_sessions uuid; + v_f_reports uuid; +BEGIN + SELECT id INTO v_free_id FROM public.plans WHERE key = 'supervisor_free'; + SELECT id INTO v_pro_id FROM public.plans WHERE key = 'supervisor_pro'; + + SELECT id INTO v_f_access FROM public.features WHERE key = 'supervisor.access'; + SELECT id INTO v_f_invite FROM public.features WHERE key = 'supervisor.invite'; + SELECT id INTO v_f_sessions FROM public.features WHERE key = 'supervisor.sessions'; + SELECT id INTO v_f_reports FROM public.features WHERE key = 'supervisor.reports'; + + -- supervisor_free: access + invite (limitado por max_supervisees=3) + INSERT INTO public.plan_features (plan_id, feature_id) + VALUES + (v_free_id, v_f_access), + (v_free_id, v_f_invite) + ON CONFLICT DO NOTHING; + + -- supervisor_pro: tudo + INSERT INTO public.plan_features (plan_id, feature_id) + VALUES + (v_pro_id, v_f_access), + (v_pro_id, v_f_invite), + (v_pro_id, v_f_sessions), + (v_pro_id, v_f_reports) + ON CONFLICT DO NOTHING; +END; +$$; + + +-- ──────────────────────────────────────────────────────────── +-- 7. activate_subscription_from_intent — suporte a supervisor +-- Supervisor = pessoal (user_id), sem tenant_id (igual therapist) +-- ──────────────────────────────────────────────────────────── +CREATE OR REPLACE FUNCTION public.activate_subscription_from_intent(p_intent_id uuid) +RETURNS public.subscriptions +LANGUAGE plpgsql SECURITY DEFINER +AS $$ +declare + v_intent record; + v_sub public.subscriptions; + v_days int; + v_user_id uuid; + v_plan_id uuid; + v_target text; +begin + -- lê pela VIEW unificada + select * into v_intent + from public.subscription_intents + where id = p_intent_id; + + if not found then + raise exception 'Intent não encontrado: %', p_intent_id; + end if; + + if v_intent.status <> 'paid' then + raise exception 'Intent precisa estar paid para ativar assinatura'; + end if; + + -- resolve target e plan_id via plans.key + select p.id, p.target + into v_plan_id, v_target + from public.plans p + where p.key = v_intent.plan_key + limit 1; + + if v_plan_id is null then + raise exception 'Plano não encontrado em plans.key = %', v_intent.plan_key; + end if; + + v_target := lower(coalesce(v_target, '')); + + -- ✅ supervisor adicionado + if v_target not in ('clinic', 'therapist', 'supervisor') then + raise exception 'Target inválido em plans.target: %', v_target; + end if; + + -- regra por target + if v_target = 'clinic' then + if v_intent.tenant_id is null then + raise exception 'Intent sem tenant_id'; + end if; + else + -- therapist ou supervisor: vinculado ao user + v_user_id := v_intent.user_id; + if v_user_id is null then + v_user_id := v_intent.created_by_user_id; + end if; + end if; + + if v_target in ('therapist', 'supervisor') and v_user_id is null then + raise exception 'Não foi possível determinar user_id para assinatura %.', v_target; + end if; + + -- cancela assinatura ativa anterior + if v_target = 'clinic' then + update public.subscriptions + set status = 'cancelled', + cancelled_at = now() + where tenant_id = v_intent.tenant_id + and plan_id = v_plan_id + and status = 'active'; + else + -- therapist ou supervisor + update public.subscriptions + set status = 'cancelled', + cancelled_at = now() + where user_id = v_user_id + and plan_id = v_plan_id + and status = 'active' + and tenant_id is null; + end if; + + -- duração do plano (30 dias para mensal) + v_days := case + when lower(coalesce(v_intent.interval, 'month')) = 'year' then 365 + else 30 + end; + + -- cria nova assinatura + insert into public.subscriptions ( + user_id, + plan_id, + status, + started_at, + expires_at, + cancelled_at, + activated_at, + tenant_id, + plan_key, + interval, + source, + created_at, + updated_at + ) + values ( + case when v_target = 'clinic' then null else v_user_id end, + v_plan_id, + 'active', + now(), + now() + make_interval(days => v_days), + null, + now(), + case when v_target = 'clinic' then v_intent.tenant_id else null end, + v_intent.plan_key, + v_intent.interval, + 'manual', + now(), + now() + ) + returning * into v_sub; + + -- grava vínculo intent → subscription + if v_target = 'clinic' then + update public.subscription_intents_tenant + set subscription_id = v_sub.id + where id = p_intent_id; + else + update public.subscription_intents_personal + set subscription_id = v_sub.id + where id = p_intent_id; + end if; + + return v_sub; +end; +$$; + + +-- ──────────────────────────────────────────────────────────── +-- 8. subscriptions_validate_scope — suporte a supervisor +-- ──────────────────────────────────────────────────────────── +CREATE OR REPLACE FUNCTION public.subscriptions_validate_scope() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +DECLARE + v_target text; +BEGIN + SELECT lower(p.target) INTO v_target + FROM public.plans p + WHERE p.id = NEW.plan_id; + + IF v_target IS NULL THEN + RAISE EXCEPTION 'Plano inválido (target nulo).'; + END IF; + + IF v_target = 'clinic' THEN + IF NEW.tenant_id IS NULL THEN + RAISE EXCEPTION 'Assinatura clinic exige tenant_id.'; + END IF; + IF NEW.user_id IS NOT NULL THEN + RAISE EXCEPTION 'Assinatura clinic não pode ter user_id (XOR).'; + END IF; + + ELSIF v_target IN ('therapist', 'supervisor') THEN + -- supervisor é pessoal como therapist + IF NEW.tenant_id IS NOT NULL THEN + RAISE EXCEPTION 'Assinatura % não deve ter tenant_id.', v_target; + END IF; + IF NEW.user_id IS NULL THEN + RAISE EXCEPTION 'Assinatura % exige user_id.', v_target; + END IF; + + ELSIF v_target = 'patient' THEN + IF NEW.tenant_id IS NOT NULL THEN + RAISE EXCEPTION 'Assinatura patient não deve ter tenant_id.'; + END IF; + IF NEW.user_id IS NULL THEN + RAISE EXCEPTION 'Assinatura patient exige user_id.'; + END IF; + + ELSE + RAISE EXCEPTION 'Target de plano inválido: %', v_target; + END IF; + + RETURN NEW; +END; +$$; + + +-- ──────────────────────────────────────────────────────────── +-- 9. subscription_intents_view_insert — suporte a supervisor +-- supervisor é roteado como therapist (tabela personal) +-- ──────────────────────────────────────────────────────────── +CREATE OR REPLACE FUNCTION public.subscription_intents_view_insert() +RETURNS trigger +LANGUAGE plpgsql SECURITY DEFINER +AS $$ +declare + v_target text; + v_plan_id uuid; +begin + select p.id, p.target into v_plan_id, v_target + from public.plans p + where p.key = new.plan_key; + + if v_plan_id is null then + raise exception 'Plano inválido: plan_key=%', new.plan_key; + end if; + + if lower(v_target) = 'clinic' then + if new.tenant_id is null then + raise exception 'Intenção clinic exige tenant_id.'; + end if; + + insert into public.subscription_intents_tenant ( + id, tenant_id, created_by_user_id, email, + plan_id, plan_key, interval, amount_cents, currency, + status, source, notes, created_at, paid_at + ) values ( + coalesce(new.id, gen_random_uuid()), + new.tenant_id, new.created_by_user_id, new.email, + v_plan_id, new.plan_key, coalesce(new.interval,'month'), + new.amount_cents, coalesce(new.currency,'BRL'), + coalesce(new.status,'pending'), coalesce(new.source,'manual'), + new.notes, coalesce(new.created_at, now()), new.paid_at + ); + + new.plan_target := 'clinic'; + return new; + end if; + + -- therapist ou supervisor → tabela personal + if lower(v_target) in ('therapist', 'supervisor') then + insert into public.subscription_intents_personal ( + id, user_id, created_by_user_id, email, + plan_id, plan_key, interval, amount_cents, currency, + status, source, notes, created_at, paid_at + ) values ( + coalesce(new.id, gen_random_uuid()), + new.user_id, new.created_by_user_id, new.email, + v_plan_id, new.plan_key, coalesce(new.interval,'month'), + new.amount_cents, coalesce(new.currency,'BRL'), + coalesce(new.status,'pending'), coalesce(new.source,'manual'), + new.notes, coalesce(new.created_at, now()), new.paid_at + ); + + new.plan_target := lower(v_target); -- 'therapist' ou 'supervisor' + return new; + end if; + + raise exception 'Target de plano não suportado: %', v_target; +end; +$$; + + +-- ──────────────────────────────────────────────────────────── +-- FIM — verificação rápida +-- ──────────────────────────────────────────────────────────── +SELECT key, name, target, max_supervisees +FROM public.plans +WHERE target = 'supervisor' +ORDER BY key; diff --git a/Novo-DB/fix_missing_subscriptions.sql b/Novo-DB/fix_missing_subscriptions.sql new file mode 100644 index 0000000..898766d --- /dev/null +++ b/Novo-DB/fix_missing_subscriptions.sql @@ -0,0 +1,220 @@ +-- ============================================================================= +-- FIX: Atribuir plano free a usuários/tenants sem assinatura ativa +-- ============================================================================= +-- Execute no SQL Editor do Supabase (service_role) +-- Idempotente: só insere onde não existe assinatura ativa. +-- +-- Regras: +-- • tenant kind = 'therapist' → therapist_free (por user_id do admin) +-- • tenant kind IN (clinic_*) → clinic_free (por tenant_id) +-- • profiles.account_type = 'patient' / portal_user → patient_free (por user_id) +-- ============================================================================= + +BEGIN; + +-- ────────────────────────────────────────────────────────────────────────────── +-- DIAGNÓSTICO — mostra o estado atual antes de corrigir +-- ────────────────────────────────────────────────────────────────────────────── + +DO $$ +DECLARE + r RECORD; +BEGIN + RAISE NOTICE '=== DIAGNÓSTICO DE ASSINATURAS ==='; + RAISE NOTICE ''; + + -- Terapeutas sem plano + RAISE NOTICE '--- Terapeutas SEM assinatura ativa ---'; + FOR r IN + SELECT + tm.user_id, + p.full_name, + t.id AS tenant_id, + t.name AS tenant_name + FROM public.tenant_members tm + JOIN public.tenants t ON t.id = tm.tenant_id + JOIN public.profiles p ON p.id = tm.user_id + WHERE t.kind = 'therapist' + AND tm.role = 'tenant_admin' + AND tm.status = 'active' + AND NOT EXISTS ( + SELECT 1 FROM public.subscriptions s + WHERE s.user_id = tm.user_id + AND s.status = 'active' + ) + LOOP + RAISE NOTICE ' FALTANDO: % (%) — tenant %', r.full_name, r.user_id, r.tenant_id; + END LOOP; + + -- Clínicas sem plano + RAISE NOTICE ''; + RAISE NOTICE '--- Clínicas SEM assinatura ativa ---'; + FOR r IN + SELECT t.id, t.name, t.kind + FROM public.tenants t + WHERE t.kind IN ('clinic_coworking', 'clinic_reception', 'clinic_full', 'clinic') + AND NOT EXISTS ( + SELECT 1 FROM public.subscriptions s + WHERE s.tenant_id = t.id + AND s.status = 'active' + ) + LOOP + RAISE NOTICE ' FALTANDO: % (%) — kind %', r.name, r.id, r.kind; + END LOOP; + + -- Pacientes sem plano + RAISE NOTICE ''; + RAISE NOTICE '--- Pacientes SEM assinatura ativa ---'; + FOR r IN + SELECT p.id, p.full_name + FROM public.profiles p + WHERE p.account_type = 'patient' + AND NOT EXISTS ( + SELECT 1 FROM public.subscriptions s + WHERE s.user_id = p.id + AND s.status = 'active' + ) + LOOP + RAISE NOTICE ' FALTANDO: % (%)', r.full_name, r.id; + END LOOP; + + RAISE NOTICE ''; + RAISE NOTICE '=== FIM DO DIAGNÓSTICO — aplicando correções... ==='; +END; +$$; + + +-- ────────────────────────────────────────────────────────────────────────────── +-- CORREÇÃO 1: Terapeutas sem assinatura → therapist_free +-- Escopo: user_id do tenant_admin do tenant kind='therapist' +-- ────────────────────────────────────────────────────────────────────────────── + +INSERT INTO public.subscriptions ( + user_id, plan_id, plan_key, status, interval, + current_period_start, current_period_end, + source, started_at, activated_at +) +SELECT + tm.user_id, + p.id, + p.key, + 'active', + 'month', + now(), + now() + interval '30 days', + 'fix_seed', + now(), + now() +FROM public.tenant_members tm +JOIN public.tenants t ON t.id = tm.tenant_id +JOIN public.plans p ON p.key = 'therapist_free' +WHERE t.kind = 'therapist' + AND tm.role = 'tenant_admin' + AND tm.status = 'active' + AND NOT EXISTS ( + SELECT 1 FROM public.subscriptions s + WHERE s.user_id = tm.user_id + AND s.status = 'active' + ); + + +-- ────────────────────────────────────────────────────────────────────────────── +-- CORREÇÃO 2: Clínicas sem assinatura → clinic_free +-- Escopo: tenant_id +-- ────────────────────────────────────────────────────────────────────────────── + +INSERT INTO public.subscriptions ( + tenant_id, plan_id, plan_key, status, interval, + current_period_start, current_period_end, + source, started_at, activated_at +) +SELECT + t.id, + p.id, + p.key, + 'active', + 'month', + now(), + now() + interval '30 days', + 'fix_seed', + now(), + now() +FROM public.tenants t +JOIN public.plans p ON p.key = 'clinic_free' +WHERE t.kind IN ('clinic_coworking', 'clinic_reception', 'clinic_full', 'clinic') + AND NOT EXISTS ( + SELECT 1 FROM public.subscriptions s + WHERE s.tenant_id = t.id + AND s.status = 'active' + ); + + +-- ────────────────────────────────────────────────────────────────────────────── +-- CORREÇÃO 3: Pacientes sem assinatura → patient_free +-- Escopo: user_id +-- ────────────────────────────────────────────────────────────────────────────── + +INSERT INTO public.subscriptions ( + user_id, plan_id, plan_key, status, interval, + current_period_start, current_period_end, + source, started_at, activated_at +) +SELECT + pr.id, + p.id, + p.key, + 'active', + 'month', + now(), + now() + interval '30 days', + 'fix_seed', + now(), + now() +FROM public.profiles pr +JOIN public.plans p ON p.key = 'patient_free' +WHERE pr.account_type = 'patient' + AND NOT EXISTS ( + SELECT 1 FROM public.subscriptions s + WHERE s.user_id = pr.id + AND s.status = 'active' + ); + + +-- ────────────────────────────────────────────────────────────────────────────── +-- CONFIRMAÇÃO — mostra o que foi inserido (source = 'fix_seed') +-- ────────────────────────────────────────────────────────────────────────────── + +DO $$ +DECLARE + r RECORD; + total INT := 0; +BEGIN + RAISE NOTICE ''; + RAISE NOTICE '=== ASSINATURAS CRIADAS NESTA EXECUÇÃO ==='; + + FOR r IN + SELECT + s.plan_key, + COALESCE(pr.full_name, t.name) AS nome, + COALESCE(s.user_id::text, s.tenant_id::text) AS owner_id + FROM public.subscriptions s + LEFT JOIN public.profiles pr ON pr.id = s.user_id + LEFT JOIN public.tenants t ON t.id = s.tenant_id + WHERE s.source = 'fix_seed' + AND s.started_at >= now() - interval '5 seconds' + ORDER BY s.plan_key, nome + LOOP + RAISE NOTICE ' ✅ % → % (%)', r.plan_key, r.nome, r.owner_id; + total := total + 1; + END LOOP; + + IF total = 0 THEN + RAISE NOTICE ' (nenhuma nova assinatura criada — todos já tinham plano ativo)'; + ELSE + RAISE NOTICE ''; + RAISE NOTICE ' Total: % assinatura(s) criada(s).', total; + END IF; +END; +$$; + +COMMIT; diff --git a/Novo-DB/fix_subscriptions_validate_scope.sql b/Novo-DB/fix_subscriptions_validate_scope.sql new file mode 100644 index 0000000..8aa8b69 --- /dev/null +++ b/Novo-DB/fix_subscriptions_validate_scope.sql @@ -0,0 +1,50 @@ +-- Fix: subscriptions_validate_scope — adiciona suporte a target='patient' + +CREATE OR REPLACE FUNCTION public.subscriptions_validate_scope() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +DECLARE + v_target text; +BEGIN + SELECT lower(p.target) INTO v_target + FROM public.plans p + WHERE p.id = NEW.plan_id; + + IF v_target IS NULL THEN + RAISE EXCEPTION 'Plano inválido (target nulo).'; + END IF; + + IF v_target = 'clinic' THEN + IF NEW.tenant_id IS NULL THEN + RAISE EXCEPTION 'Assinatura clinic exige tenant_id.'; + END IF; + IF NEW.user_id IS NOT NULL THEN + RAISE EXCEPTION 'Assinatura clinic não pode ter user_id (XOR).'; + END IF; + + ELSIF v_target = 'therapist' THEN + IF NEW.tenant_id IS NOT NULL THEN + RAISE EXCEPTION 'Assinatura therapist não deve ter tenant_id.'; + END IF; + IF NEW.user_id IS NULL THEN + RAISE EXCEPTION 'Assinatura therapist exige user_id.'; + END IF; + + ELSIF v_target = 'patient' THEN + IF NEW.tenant_id IS NOT NULL THEN + RAISE EXCEPTION 'Assinatura patient não deve ter tenant_id.'; + END IF; + IF NEW.user_id IS NULL THEN + RAISE EXCEPTION 'Assinatura patient exige user_id.'; + END IF; + + ELSE + RAISE EXCEPTION 'Target de plano inválido: %', v_target; + END IF; + + RETURN NEW; +END; +$$; + +ALTER FUNCTION public.subscriptions_validate_scope() OWNER TO supabase_admin; diff --git a/Novo-DB/migration_001.sql b/Novo-DB/migration_001.sql new file mode 100644 index 0000000..74c6764 --- /dev/null +++ b/Novo-DB/migration_001.sql @@ -0,0 +1,296 @@ +-- ============================================================================= +-- SEED 001 — Usuários fictícios para teste +-- ============================================================================= +-- Execute APÓS migration_001.sql +-- +-- Usuários criados: +-- paciente@agenciapsi.com.br senha: Teste@123 → patient +-- terapeuta@agenciapsi.com.br senha: Teste@123 → therapist +-- clinica1@agenciapsi.com.br senha: Teste@123 → clinic_coworking +-- clinica2@agenciapsi.com.br senha: Teste@123 → clinic_reception +-- clinica3@agenciapsi.com.br senha: Teste@123 → clinic_full +-- saas@agenciapsi.com.br senha: Teste@123 → saas_admin +-- ============================================================================= + + +-- ============================================================ +-- Limpeza de seeds anteriores +-- ============================================================ + +ALTER TABLE public.patient_groups DISABLE TRIGGER ALL; + +DELETE FROM public.tenant_members +WHERE user_id IN ( + SELECT id FROM auth.users + WHERE email IN ( + 'paciente@agenciapsi.com.br', + 'terapeuta@agenciapsi.com.br', + 'clinica1@agenciapsi.com.br', + 'clinica2@agenciapsi.com.br', + 'clinica3@agenciapsi.com.br', + 'saas@agenciapsi.com.br' + ) +); + +DELETE FROM public.tenants WHERE id IN ( + 'bbbbbbbb-0002-0002-0002-000000000002', + 'bbbbbbbb-0003-0003-0003-000000000003', + 'bbbbbbbb-0004-0004-0004-000000000004', + 'bbbbbbbb-0005-0005-0005-000000000005' +); + +DELETE FROM auth.users WHERE email IN ( + 'paciente@agenciapsi.com.br', + 'terapeuta@agenciapsi.com.br', + 'clinica1@agenciapsi.com.br', + 'clinica2@agenciapsi.com.br', + 'clinica3@agenciapsi.com.br', + 'saas@agenciapsi.com.br' +); + +ALTER TABLE public.patient_groups ENABLE TRIGGER ALL; + + +-- ============================================================ +-- 1. Usuários no auth.users +-- ============================================================ + +INSERT INTO auth.users ( + id, email, encrypted_password, email_confirmed_at, + created_at, updated_at, raw_user_meta_data, role, aud +) +VALUES + ( + 'aaaaaaaa-0001-0001-0001-000000000001', + 'paciente@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), + '{"name": "Ana Paciente"}'::jsonb, + 'authenticated', 'authenticated' + ), + ( + 'aaaaaaaa-0002-0002-0002-000000000002', + 'terapeuta@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), + '{"name": "Bruno Terapeuta"}'::jsonb, + 'authenticated', 'authenticated' + ), + ( + 'aaaaaaaa-0003-0003-0003-000000000003', + 'clinica1@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), + '{"name": "Clinica Espaco Psi"}'::jsonb, + 'authenticated', 'authenticated' + ), + ( + 'aaaaaaaa-0004-0004-0004-000000000004', + 'clinica2@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), + '{"name": "Clinica Mente Sa"}'::jsonb, + 'authenticated', 'authenticated' + ), + ( + 'aaaaaaaa-0005-0005-0005-000000000005', + 'clinica3@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), + '{"name": "Clinica Bem Estar"}'::jsonb, + 'authenticated', 'authenticated' + ), + ( + 'aaaaaaaa-0006-0006-0006-000000000006', + 'saas@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), + '{"name": "Admin Plataforma"}'::jsonb, + 'authenticated', 'authenticated' + ); + + +-- ============================================================ +-- 2. Profiles +-- ============================================================ + +INSERT INTO public.profiles (id, role, account_type, full_name) +VALUES + ('aaaaaaaa-0001-0001-0001-000000000001', 'portal_user', 'patient', 'Ana Paciente'), + ('aaaaaaaa-0002-0002-0002-000000000002', 'portal_user', 'therapist', 'Bruno Terapeuta'), + ('aaaaaaaa-0003-0003-0003-000000000003', 'portal_user', 'clinic', 'Clinica Espaco Psi'), + ('aaaaaaaa-0004-0004-0004-000000000004', 'portal_user', 'clinic', 'Clinica Mente Sa'), + ('aaaaaaaa-0005-0005-0005-000000000005', 'portal_user', 'clinic', 'Clinica Bem Estar'), + ('aaaaaaaa-0006-0006-0006-000000000006', 'saas_admin', 'free', 'Admin Plataforma') +ON CONFLICT (id) DO UPDATE SET + role = EXCLUDED.role, + account_type = EXCLUDED.account_type, + full_name = EXCLUDED.full_name; + + +-- ============================================================ +-- 3. SaaS Admin +-- ============================================================ + +INSERT INTO public.saas_admins (user_id, created_at) +VALUES ('aaaaaaaa-0006-0006-0006-000000000006', now()) +ON CONFLICT (user_id) DO NOTHING; + + +-- ============================================================ +-- 4. Tenant do terapeuta +-- ============================================================ + +INSERT INTO public.tenants (id, name, kind, created_at) +VALUES ('bbbbbbbb-0002-0002-0002-000000000002', 'Bruno Terapeuta', 'therapist', now()) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) +VALUES ('bbbbbbbb-0002-0002-0002-000000000002', 'aaaaaaaa-0002-0002-0002-000000000002', 'tenant_admin', 'active', now()) +ON CONFLICT (tenant_id, user_id) DO NOTHING; + +DO $$ BEGIN + PERFORM public.seed_determined_commitments('bbbbbbbb-0002-0002-0002-000000000002'); +END; $$; + + +-- ============================================================ +-- 5. Tenant Clinica 1 — Coworking +-- ============================================================ + +INSERT INTO public.tenants (id, name, kind, created_at) +VALUES ('bbbbbbbb-0003-0003-0003-000000000003', 'Clinica Espaco Psi', 'clinic_coworking', now()) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) +VALUES ('bbbbbbbb-0003-0003-0003-000000000003', 'aaaaaaaa-0003-0003-0003-000000000003', 'tenant_admin', 'active', now()) +ON CONFLICT (tenant_id, user_id) DO NOTHING; + +DO $$ BEGIN + PERFORM public.seed_determined_commitments('bbbbbbbb-0003-0003-0003-000000000003'); +END; $$; + + +-- ============================================================ +-- 6. Tenant Clinica 2 — Recepcao +-- ============================================================ + +INSERT INTO public.tenants (id, name, kind, created_at) +VALUES ('bbbbbbbb-0004-0004-0004-000000000004', 'Clinica Mente Sa', 'clinic_reception', now()) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) +VALUES ('bbbbbbbb-0004-0004-0004-000000000004', 'aaaaaaaa-0004-0004-0004-000000000004', 'tenant_admin', 'active', now()) +ON CONFLICT (tenant_id, user_id) DO NOTHING; + +DO $$ BEGIN + PERFORM public.seed_determined_commitments('bbbbbbbb-0004-0004-0004-000000000004'); +END; $$; + + +-- ============================================================ +-- 7. Tenant Clinica 3 — Full +-- ============================================================ + +INSERT INTO public.tenants (id, name, kind, created_at) +VALUES ('bbbbbbbb-0005-0005-0005-000000000005', 'Clinica Bem Estar', 'clinic_full', now()) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) +VALUES ('bbbbbbbb-0005-0005-0005-000000000005', 'aaaaaaaa-0005-0005-0005-000000000005', 'tenant_admin', 'active', now()) +ON CONFLICT (tenant_id, user_id) DO NOTHING; + +DO $$ BEGIN + PERFORM public.seed_determined_commitments('bbbbbbbb-0005-0005-0005-000000000005'); +END; $$; + + +-- ============================================================ +-- 8. Subscriptions ativas +-- ============================================================ + +-- Paciente → patient_free +INSERT INTO public.subscriptions ( + user_id, plan_id, plan_key, status, interval, + current_period_start, current_period_end, source, started_at, activated_at +) +SELECT + 'aaaaaaaa-0001-0001-0001-000000000001', + p.id, p.key, 'active', 'month', + now(), now() + interval '30 days', 'seed', now(), now() +FROM public.plans p WHERE p.key = 'patient_free'; + +-- Terapeuta → therapist_free +INSERT INTO public.subscriptions ( + user_id, plan_id, plan_key, status, interval, + current_period_start, current_period_end, source, started_at, activated_at +) +SELECT + 'aaaaaaaa-0002-0002-0002-000000000002', + p.id, p.key, 'active', 'month', + now(), now() + interval '30 days', 'seed', now(), now() +FROM public.plans p WHERE p.key = 'therapist_free'; + +-- Clinica 1 → clinic_free +INSERT INTO public.subscriptions ( + tenant_id, plan_id, plan_key, status, interval, + current_period_start, current_period_end, source, started_at, activated_at +) +SELECT + 'bbbbbbbb-0003-0003-0003-000000000003', + p.id, p.key, 'active', 'month', + now(), now() + interval '30 days', 'seed', now(), now() +FROM public.plans p WHERE p.key = 'clinic_free'; + +-- Clinica 2 → clinic_free +INSERT INTO public.subscriptions ( + tenant_id, plan_id, plan_key, status, interval, + current_period_start, current_period_end, source, started_at, activated_at +) +SELECT + 'bbbbbbbb-0004-0004-0004-000000000004', + p.id, p.key, 'active', 'month', + now(), now() + interval '30 days', 'seed', now(), now() +FROM public.plans p WHERE p.key = 'clinic_free'; + +-- Clinica 3 → clinic_free +INSERT INTO public.subscriptions ( + tenant_id, plan_id, plan_key, status, interval, + current_period_start, current_period_end, source, started_at, activated_at +) +SELECT + 'bbbbbbbb-0005-0005-0005-000000000005', + p.id, p.key, 'active', 'month', + now(), now() + interval '30 days', 'seed', now(), now() +FROM public.plans p WHERE p.key = 'clinic_free'; + + +-- ============================================================ +-- 9. Vincula terapeuta à Clinica 3 (exemplo de associacao) +-- ============================================================ + +INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) +VALUES ( + 'bbbbbbbb-0005-0005-0005-000000000005', + 'aaaaaaaa-0002-0002-0002-000000000002', + 'therapist', 'active', now() +) +ON CONFLICT (tenant_id, user_id) DO NOTHING; + + +-- ============================================================ +-- Confirmacao +-- ============================================================ + +DO $$ +BEGIN + RAISE NOTICE '✅ Seed aplicado com sucesso.'; + RAISE NOTICE ' paciente@agenciapsi.com.br → patient'; + RAISE NOTICE ' terapeuta@agenciapsi.com.br → therapist'; + RAISE NOTICE ' clinica1@agenciapsi.com.br → clinic_coworking'; + RAISE NOTICE ' clinica2@agenciapsi.com.br → clinic_reception'; + RAISE NOTICE ' clinica3@agenciapsi.com.br → clinic_full'; + RAISE NOTICE ' saas@agenciapsi.com.br → saas_admin'; + RAISE NOTICE ' Senha: Teste@123'; +END; +$$; diff --git a/Novo-DB/migration_002_layout_variant.sql b/Novo-DB/migration_002_layout_variant.sql new file mode 100644 index 0000000..6a74e0c --- /dev/null +++ b/Novo-DB/migration_002_layout_variant.sql @@ -0,0 +1,13 @@ +-- ============================================================================= +-- Migration 002 — Adiciona layout_variant em user_settings +-- ============================================================================= +-- Execute no SQL Editor do Supabase (ou via Docker psql). +-- Tolerante: usa IF NOT EXISTS / DEFAULT para não quebrar dados existentes. +-- ============================================================================= + +ALTER TABLE public.user_settings + ADD COLUMN IF NOT EXISTS layout_variant TEXT NOT NULL DEFAULT 'classic'; + +-- ============================================================================= +RAISE NOTICE '✅ Coluna layout_variant adicionada a user_settings.'; +-- ============================================================================= diff --git a/Novo-DB/seed_001.sql b/Novo-DB/seed_001.sql new file mode 100644 index 0000000..c240a7e --- /dev/null +++ b/Novo-DB/seed_001.sql @@ -0,0 +1,334 @@ +-- ============================================================================= +-- SEED — Usuários fictícios para teste +-- ============================================================================= +-- IMPORTANTE: Execute APÓS a migration_001.sql +-- IMPORTANTE: Requer extensão pgcrypto (já ativa no Supabase) +-- +-- Cria os seguintes usuários de teste: +-- +-- paciente@agenciapsi.com.br senha: Teste@123 → paciente +-- terapeuta@agenciapsi.com.br senha: Teste@123 → terapeuta solo +-- clinica1@agenciapsi.com.br senha: Teste@123 → clínica coworking +-- clinica2@agenciapsi.com.br senha: Teste@123 → clínica com secretaria +-- clinica3@agenciapsi.com.br senha: Teste@123 → clínica full +-- saas@agenciapsi.com.br senha: Teste@123 → admin da plataforma +-- +-- ============================================================================= + +BEGIN; + +-- ============================================================ +-- Helper: cria usuário no auth.users + profile +-- (Supabase não expõe auth.users diretamente, mas em SQL Editor +-- com acesso de service_role podemos inserir diretamente) +-- ============================================================ + +-- Limpa seeds anteriores se existirem +DELETE FROM auth.users +WHERE email IN ( + 'paciente@agenciapsi.com.br', + 'terapeuta@agenciapsi.com.br', + 'clinica1@agenciapsi.com.br', + 'clinica2@agenciapsi.com.br', + 'clinica3@agenciapsi.com.br', + 'saas@agenciapsi.com.br' +); + +-- ============================================================ +-- 1. Cria usuários no auth.users +-- ============================================================ + +INSERT INTO auth.users ( + instance_id, + id, + email, + encrypted_password, + email_confirmed_at, + confirmed_at, + created_at, + updated_at, + raw_user_meta_data, + raw_app_meta_data, + role, + aud, + is_sso_user, + is_anonymous, + confirmation_token, + recovery_token, + email_change_token_new, + email_change_token_current, + email_change +) +VALUES + -- Paciente + ( + '00000000-0000-0000-0000-000000000000', + 'aaaaaaaa-0001-0001-0001-000000000001', + 'paciente@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), now(), + '{"name": "Ana Paciente"}'::jsonb, + '{"provider": "email", "providers": ["email"]}'::jsonb, + 'authenticated', 'authenticated', false, false, '', '', '', '', '' + ), + -- Terapeuta + ( + '00000000-0000-0000-0000-000000000000', + 'aaaaaaaa-0002-0002-0002-000000000002', + 'terapeuta@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), now(), + '{"name": "Bruno Terapeuta"}'::jsonb, + '{"provider": "email", "providers": ["email"]}'::jsonb, + 'authenticated', 'authenticated', false, false, '', '', '', '', '' + ), + -- Clínica 1 — Coworking + ( + '00000000-0000-0000-0000-000000000000', + 'aaaaaaaa-0003-0003-0003-000000000003', + 'clinica1@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), now(), + '{"name": "Clínica Espaço Psi"}'::jsonb, + '{"provider": "email", "providers": ["email"]}'::jsonb, + 'authenticated', 'authenticated', false, false, '', '', '', '', '' + ), + -- Clínica 2 — Recepção + ( + '00000000-0000-0000-0000-000000000000', + 'aaaaaaaa-0004-0004-0004-000000000004', + 'clinica2@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), now(), + '{"name": "Clínica Mente Sã"}'::jsonb, + '{"provider": "email", "providers": ["email"]}'::jsonb, + 'authenticated', 'authenticated', false, false, '', '', '', '', '' + ), + -- Clínica 3 — Full + ( + '00000000-0000-0000-0000-000000000000', + 'aaaaaaaa-0005-0005-0005-000000000005', + 'clinica3@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), now(), + '{"name": "Clínica Bem Estar"}'::jsonb, + '{"provider": "email", "providers": ["email"]}'::jsonb, + 'authenticated', 'authenticated', false, false, '', '', '', '', '' + ), + -- SaaS Admin + ( + '00000000-0000-0000-0000-000000000000', + 'aaaaaaaa-0006-0006-0006-000000000006', + 'saas@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), now(), + '{"name": "Admin Plataforma"}'::jsonb, + '{"provider": "email", "providers": ["email"]}'::jsonb, + 'authenticated', 'authenticated', false, false, '', '', '', '', '' + ); + +-- auth.identities (obrigatório para GoTrue reconhecer login email/senha) +INSERT INTO auth.identities (id, user_id, provider_id, provider, identity_data, created_at, updated_at, last_sign_in_at) +VALUES + (gen_random_uuid(), 'aaaaaaaa-0001-0001-0001-000000000001', 'paciente@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0001-0001-0001-000000000001", "email": "paciente@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now()), + (gen_random_uuid(), 'aaaaaaaa-0002-0002-0002-000000000002', 'terapeuta@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0002-0002-0002-000000000002", "email": "terapeuta@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now()), + (gen_random_uuid(), 'aaaaaaaa-0003-0003-0003-000000000003', 'clinica1@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0003-0003-0003-000000000003", "email": "clinica1@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now()), + (gen_random_uuid(), 'aaaaaaaa-0004-0004-0004-000000000004', 'clinica2@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0004-0004-0004-000000000004", "email": "clinica2@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now()), + (gen_random_uuid(), 'aaaaaaaa-0005-0005-0005-000000000005', 'clinica3@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0005-0005-0005-000000000005", "email": "clinica3@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now()), + (gen_random_uuid(), 'aaaaaaaa-0006-0006-0006-000000000006', 'saas@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0006-0006-0006-000000000006", "email": "saas@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now()) +ON CONFLICT (provider, provider_id) DO NOTHING; + + +-- ============================================================ +-- 2. Profiles (o trigger handle_new_user não dispara em inserts +-- diretos no auth.users via SQL, então criamos manualmente) +-- ============================================================ + +INSERT INTO public.profiles (id, role, account_type, full_name) +VALUES + ('aaaaaaaa-0001-0001-0001-000000000001', 'portal_user', 'patient', 'Ana Paciente'), + ('aaaaaaaa-0002-0002-0002-000000000002', 'tenant_member', 'therapist', 'Bruno Terapeuta'), + ('aaaaaaaa-0003-0003-0003-000000000003', 'tenant_member', 'clinic', 'Clínica Espaço Psi'), + ('aaaaaaaa-0004-0004-0004-000000000004', 'tenant_member', 'clinic', 'Clínica Mente Sã'), + ('aaaaaaaa-0005-0005-0005-000000000005', 'tenant_member', 'clinic', 'Clínica Bem Estar'), + ('aaaaaaaa-0006-0006-0006-000000000006', 'saas_admin', 'free', 'Admin Plataforma') +ON CONFLICT (id) DO UPDATE SET + role = EXCLUDED.role, + account_type = EXCLUDED.account_type, + full_name = EXCLUDED.full_name; + + +-- ============================================================ +-- 3. SaaS Admin na tabela saas_admins +-- ============================================================ + +INSERT INTO public.saas_admins (user_id, created_at) +VALUES ('aaaaaaaa-0006-0006-0006-000000000006', now()) +ON CONFLICT (user_id) DO NOTHING; + + +-- ============================================================ +-- 4. Tenant do terapeuta +-- ============================================================ + +INSERT INTO public.tenants (id, name, kind, created_at) +VALUES ('bbbbbbbb-0002-0002-0002-000000000002', 'Bruno Terapeuta', 'therapist', now()) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) +VALUES ('bbbbbbbb-0002-0002-0002-000000000002', 'aaaaaaaa-0002-0002-0002-000000000002', 'tenant_admin', 'active', now()) +ON CONFLICT (tenant_id, user_id) DO NOTHING; + +DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0002-0002-0002-000000000002'); END $$; + + +-- ============================================================ +-- 5. Tenant da Clínica 1 — Coworking +-- ============================================================ + +INSERT INTO public.tenants (id, name, kind, created_at) +VALUES ('bbbbbbbb-0003-0003-0003-000000000003', 'Clínica Espaço Psi', 'clinic_coworking', now()) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) +VALUES ('bbbbbbbb-0003-0003-0003-000000000003', 'aaaaaaaa-0003-0003-0003-000000000003', 'tenant_admin', 'active', now()) +ON CONFLICT (tenant_id, user_id) DO NOTHING; + +DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0003-0003-0003-000000000003'); END $$; + + +-- ============================================================ +-- 6. Tenant da Clínica 2 — Recepção +-- ============================================================ + +INSERT INTO public.tenants (id, name, kind, created_at) +VALUES ('bbbbbbbb-0004-0004-0004-000000000004', 'Clínica Mente Sã', 'clinic_reception', now()) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) +VALUES ('bbbbbbbb-0004-0004-0004-000000000004', 'aaaaaaaa-0004-0004-0004-000000000004', 'tenant_admin', 'active', now()) +ON CONFLICT (tenant_id, user_id) DO NOTHING; + +DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0004-0004-0004-000000000004'); END $$; + + +-- ============================================================ +-- 7. Tenant da Clínica 3 — Full +-- ============================================================ + +INSERT INTO public.tenants (id, name, kind, created_at) +VALUES ('bbbbbbbb-0005-0005-0005-000000000005', 'Clínica Bem Estar', 'clinic_full', now()) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) +VALUES ('bbbbbbbb-0005-0005-0005-000000000005', 'aaaaaaaa-0005-0005-0005-000000000005', 'tenant_admin', 'active', now()) +ON CONFLICT (tenant_id, user_id) DO NOTHING; + +DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0005-0005-0005-000000000005'); END $$; + + +-- ============================================================ +-- 8. Subscriptions ativas para cada conta +-- ============================================================ + +-- Terapeuta → plano therapist_free +INSERT INTO public.subscriptions ( + user_id, plan_id, plan_key, status, interval, + current_period_start, current_period_end, + source, started_at, activated_at +) +SELECT + 'aaaaaaaa-0002-0002-0002-000000000002', + p.id, p.key, 'active', 'month', + now(), now() + interval '30 days', + 'seed', now(), now() +FROM public.plans p WHERE p.key = 'therapist_free'; + +-- Clínica 1 → plano clinic_free +INSERT INTO public.subscriptions ( + tenant_id, plan_id, plan_key, status, interval, + current_period_start, current_period_end, + source, started_at, activated_at +) +SELECT + 'bbbbbbbb-0003-0003-0003-000000000003', + p.id, p.key, 'active', 'month', + now(), now() + interval '30 days', + 'seed', now(), now() +FROM public.plans p WHERE p.key = 'clinic_free'; + +-- Clínica 2 → plano clinic_free +INSERT INTO public.subscriptions ( + tenant_id, plan_id, plan_key, status, interval, + current_period_start, current_period_end, + source, started_at, activated_at +) +SELECT + 'bbbbbbbb-0004-0004-0004-000000000004', + p.id, p.key, 'active', 'month', + now(), now() + interval '30 days', + 'seed', now(), now() +FROM public.plans p WHERE p.key = 'clinic_free'; + +-- Clínica 3 → plano clinic_free +INSERT INTO public.subscriptions ( + tenant_id, plan_id, plan_key, status, interval, + current_period_start, current_period_end, + source, started_at, activated_at +) +SELECT + 'bbbbbbbb-0005-0005-0005-000000000005', + p.id, p.key, 'active', 'month', + now(), now() + interval '30 days', + 'seed', now(), now() +FROM public.plans p WHERE p.key = 'clinic_free'; + +-- Paciente → plano patient_free +INSERT INTO public.subscriptions ( + user_id, plan_id, plan_key, status, interval, + current_period_start, current_period_end, + source, started_at, activated_at +) +SELECT + 'aaaaaaaa-0001-0001-0001-000000000001', + p.id, p.key, 'active', 'month', + now(), now() + interval '30 days', + 'seed', now(), now() +FROM public.plans p WHERE p.key = 'patient_free'; + + +-- ============================================================ +-- 9. Vincula terapeuta à Clínica 3 (full) como exemplo +-- ============================================================ + +INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) +VALUES ( + 'bbbbbbbb-0005-0005-0005-000000000005', + 'aaaaaaaa-0002-0002-0002-000000000002', + 'therapist', + 'active', + now() +) +ON CONFLICT (tenant_id, user_id) DO NOTHING; + + +-- ============================================================ +-- 10. Confirma +-- ============================================================ + +DO $$ +BEGIN + RAISE NOTICE '✅ Seed aplicado com sucesso.'; + RAISE NOTICE ''; + RAISE NOTICE ' Usuários criados:'; + RAISE NOTICE ' paciente@agenciapsi.com.br → patient'; + RAISE NOTICE ' terapeuta@agenciapsi.com.br → therapist (tenant próprio + vinculado à Clínica 3)'; + RAISE NOTICE ' clinica1@agenciapsi.com.br → clinic_coworking'; + RAISE NOTICE ' clinica2@agenciapsi.com.br → clinic_reception'; + RAISE NOTICE ' clinica3@agenciapsi.com.br → clinic_full'; + RAISE NOTICE ' saas@agenciapsi.com.br → saas_admin'; + RAISE NOTICE ' Senha de todos: Teste@123'; +END; +$$; + +COMMIT; diff --git a/Novo-DB/seed_002.sql b/Novo-DB/seed_002.sql new file mode 100644 index 0000000..a9fc80f --- /dev/null +++ b/Novo-DB/seed_002.sql @@ -0,0 +1,199 @@ +-- ============================================================================= +-- SEED 002 — Supervisor e Editor +-- ============================================================================= +-- Execute APÓS seed_001.sql +-- Requer: pgcrypto (já ativo no Supabase) +-- +-- Cria os seguintes usuários de teste: +-- +-- supervisor@agenciapsi.com.br senha: Teste@123 → supervisor da Clínica 3 +-- editor@agenciapsi.com.br senha: Teste@123 → editor de conteúdo (plataforma) +-- +-- UUIDs reservados: +-- Supervisor → aaaaaaaa-0007-0007-0007-000000000007 +-- Editor → aaaaaaaa-0008-0008-0008-000000000008 +-- +-- ============================================================================= + +BEGIN; + +-- ============================================================ +-- 0. Migration: adiciona platform_roles em profiles (se não existir) +-- ============================================================ + +ALTER TABLE public.profiles + ADD COLUMN IF NOT EXISTS platform_roles text[] NOT NULL DEFAULT '{}'; + +COMMENT ON COLUMN public.profiles.platform_roles IS + 'Papéis globais de plataforma, independentes de tenant. Ex: editor de microlearning. Atribuído pelo saas_admin.'; + + +-- ============================================================ +-- 1. Remove seeds anteriores (idempotente) +-- ============================================================ + +DELETE FROM auth.users +WHERE email IN ( + 'supervisor@agenciapsi.com.br', + 'editor@agenciapsi.com.br' +); + + +-- ============================================================ +-- 2. Cria usuários no auth.users +-- ============================================================ + +INSERT INTO auth.users ( + instance_id, + id, + email, + encrypted_password, + email_confirmed_at, + created_at, + updated_at, + raw_user_meta_data, + raw_app_meta_data, + role, + aud, + is_sso_user, + is_anonymous, + confirmation_token, + recovery_token, + email_change_token_new, + email_change_token_current, + email_change +) +VALUES + -- Supervisor + ( + '00000000-0000-0000-0000-000000000000', + 'aaaaaaaa-0007-0007-0007-000000000007', + 'supervisor@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), + '{"name": "Carlos Supervisor"}'::jsonb, + '{"provider": "email", "providers": ["email"]}'::jsonb, + 'authenticated', 'authenticated', false, false, '', '', '', '', '' + ), + -- Editor de Conteúdo + ( + '00000000-0000-0000-0000-000000000000', + 'aaaaaaaa-0008-0008-0008-000000000008', + 'editor@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), + '{"name": "Diana Editora"}'::jsonb, + '{"provider": "email", "providers": ["email"]}'::jsonb, + 'authenticated', 'authenticated', false, false, '', '', '', '', '' + ); + + +-- ============================================================ +-- 3. auth.identities (obrigatório para GoTrue reconhecer login) +-- ============================================================ + +INSERT INTO auth.identities (id, user_id, provider_id, provider, identity_data, created_at, updated_at, last_sign_in_at) +VALUES + ( + gen_random_uuid(), + 'aaaaaaaa-0007-0007-0007-000000000007', + 'supervisor@agenciapsi.com.br', + 'email', + '{"sub": "aaaaaaaa-0007-0007-0007-000000000007", "email": "supervisor@agenciapsi.com.br", "email_verified": true}'::jsonb, + now(), now(), now() + ), + ( + gen_random_uuid(), + 'aaaaaaaa-0008-0008-0008-000000000008', + 'editor@agenciapsi.com.br', + 'email', + '{"sub": "aaaaaaaa-0008-0008-0008-000000000008", "email": "editor@agenciapsi.com.br", "email_verified": true}'::jsonb, + now(), now(), now() + ) +ON CONFLICT (provider, provider_id) DO NOTHING; + + +-- ============================================================ +-- 4. Profiles +-- Supervisor → tenant_member (papel no tenant via tenant_members.role) +-- Editor → tenant_member + platform_roles = '{editor}' +-- ============================================================ + +INSERT INTO public.profiles (id, role, account_type, full_name, platform_roles) +VALUES + ( + 'aaaaaaaa-0007-0007-0007-000000000007', + 'tenant_member', + 'therapist', + 'Carlos Supervisor', + '{}' + ), + ( + 'aaaaaaaa-0008-0008-0008-000000000008', + 'tenant_member', + 'therapist', + 'Diana Editora', + '{editor}' -- permissão de plataforma: acesso à área do editor + ) +ON CONFLICT (id) DO UPDATE SET + role = EXCLUDED.role, + account_type = EXCLUDED.account_type, + full_name = EXCLUDED.full_name, + platform_roles = EXCLUDED.platform_roles; + + +-- ============================================================ +-- 5. Vincula Supervisor à Clínica 3 (Full) com role 'supervisor' +-- ============================================================ + +INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) +VALUES ( + 'bbbbbbbb-0005-0005-0005-000000000005', -- Clínica Bem Estar (Full) + 'aaaaaaaa-0007-0007-0007-000000000007', -- Carlos Supervisor + 'supervisor', + 'active', + now() +) +ON CONFLICT (tenant_id, user_id) DO UPDATE SET + role = EXCLUDED.role, + status = EXCLUDED.status; + + +-- ============================================================ +-- 6. Vincula Editor à Clínica 3 como terapeuta +-- (contexto de tenant para o editor poder usar /therapist também, +-- se necessário. O papel de editor vem de platform_roles.) +-- ============================================================ + +INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) +VALUES ( + 'bbbbbbbb-0005-0005-0005-000000000005', -- Clínica Bem Estar (Full) + 'aaaaaaaa-0008-0008-0008-000000000008', -- Diana Editora + 'therapist', + 'active', + now() +) +ON CONFLICT (tenant_id, user_id) DO UPDATE SET + role = EXCLUDED.role, + status = EXCLUDED.status; + + +-- ============================================================ +-- 7. Confirma +-- ============================================================ + +DO $$ +BEGIN + RAISE NOTICE '✅ Seed 002 aplicado com sucesso.'; + RAISE NOTICE ''; + RAISE NOTICE ' Migration aplicada:'; + RAISE NOTICE ' → profiles.platform_roles text[] adicionada (se não existia)'; + RAISE NOTICE ''; + RAISE NOTICE ' Usuários criados:'; + RAISE NOTICE ' supervisor@agenciapsi.com.br → supervisor da Clínica Bem Estar (Full)'; + RAISE NOTICE ' editor@agenciapsi.com.br → editor de conteúdo (platform_roles = {editor})'; + RAISE NOTICE ' Senha de todos: Teste@123'; +END; +$$; + +COMMIT; diff --git a/Novo-DB/seed_003.sql b/Novo-DB/seed_003.sql new file mode 100644 index 0000000..e849895 --- /dev/null +++ b/Novo-DB/seed_003.sql @@ -0,0 +1,283 @@ +-- ============================================================================= +-- SEED 003 — Terapeuta 2, Terapeuta 3 e Secretária +-- ============================================================================= +-- Execute APÓS seed_001.sql (e seed_002.sql se quiser todos os seeds) +-- Requer: pgcrypto (já ativo no Supabase) +-- +-- Cria os seguintes usuários de teste: +-- +-- therapist2@agenciapsi.com.br senha: Teste@123 → terapeuta 2 (tenant próprio + Clínica 3) +-- therapist3@agenciapsi.com.br senha: Teste@123 → terapeuta 3 (tenant próprio + Clínica 3) +-- secretary@agenciapsi.com.br senha: Teste@123 → clinic_admin na Clínica 2 (Mente Sã) +-- +-- UUIDs reservados: +-- Terapeuta 2 → aaaaaaaa-0009-0009-0009-000000000009 +-- Terapeuta 3 → aaaaaaaa-0010-0010-0010-000000000010 +-- Secretária → aaaaaaaa-0011-0011-0011-000000000011 +-- Tenant Terapeuta 2 → bbbbbbbb-0009-0009-0009-000000000009 +-- Tenant Terapeuta 3 → bbbbbbbb-0010-0010-0010-000000000010 +-- ============================================================================= + +BEGIN; + +-- ============================================================ +-- 1. Remove seeds anteriores (idempotente) +-- ============================================================ + +DELETE FROM auth.users +WHERE email IN ( + 'therapist2@agenciapsi.com.br', + 'therapist3@agenciapsi.com.br', + 'secretary@agenciapsi.com.br' +); + + +-- ============================================================ +-- 2. Cria usuários no auth.users +-- ⚠️ confirmed_at é coluna gerada — NÃO incluir na lista +-- ============================================================ + +INSERT INTO auth.users ( + instance_id, + id, + email, + encrypted_password, + email_confirmed_at, + created_at, + updated_at, + raw_user_meta_data, + raw_app_meta_data, + role, + aud, + is_sso_user, + is_anonymous, + confirmation_token, + recovery_token, + email_change_token_new, + email_change_token_current, + email_change +) +VALUES + -- Terapeuta 2 + ( + '00000000-0000-0000-0000-000000000000', + 'aaaaaaaa-0009-0009-0009-000000000009', + 'therapist2@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), + '{"name": "Eva Terapeuta"}'::jsonb, + '{"provider": "email", "providers": ["email"]}'::jsonb, + 'authenticated', 'authenticated', false, false, '', '', '', '', '' + ), + -- Terapeuta 3 + ( + '00000000-0000-0000-0000-000000000000', + 'aaaaaaaa-0010-0010-0010-000000000010', + 'therapist3@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), + '{"name": "Felipe Terapeuta"}'::jsonb, + '{"provider": "email", "providers": ["email"]}'::jsonb, + 'authenticated', 'authenticated', false, false, '', '', '', '', '' + ), + -- Secretária + ( + '00000000-0000-0000-0000-000000000000', + 'aaaaaaaa-0011-0011-0011-000000000011', + 'secretary@agenciapsi.com.br', + crypt('Teste@123', gen_salt('bf')), + now(), now(), now(), + '{"name": "Gabriela Secretária"}'::jsonb, + '{"provider": "email", "providers": ["email"]}'::jsonb, + 'authenticated', 'authenticated', false, false, '', '', '', '', '' + ); + + +-- ============================================================ +-- 3. auth.identities (obrigatório para GoTrue reconhecer login) +-- ============================================================ + +INSERT INTO auth.identities (id, user_id, provider_id, provider, identity_data, created_at, updated_at, last_sign_in_at) +VALUES + ( + gen_random_uuid(), + 'aaaaaaaa-0009-0009-0009-000000000009', + 'therapist2@agenciapsi.com.br', + 'email', + '{"sub": "aaaaaaaa-0009-0009-0009-000000000009", "email": "therapist2@agenciapsi.com.br", "email_verified": true}'::jsonb, + now(), now(), now() + ), + ( + gen_random_uuid(), + 'aaaaaaaa-0010-0010-0010-000000000010', + 'therapist3@agenciapsi.com.br', + 'email', + '{"sub": "aaaaaaaa-0010-0010-0010-000000000010", "email": "therapist3@agenciapsi.com.br", "email_verified": true}'::jsonb, + now(), now(), now() + ), + ( + gen_random_uuid(), + 'aaaaaaaa-0011-0011-0011-000000000011', + 'secretary@agenciapsi.com.br', + 'email', + '{"sub": "aaaaaaaa-0011-0011-0011-000000000011", "email": "secretary@agenciapsi.com.br", "email_verified": true}'::jsonb, + now(), now(), now() + ) +ON CONFLICT (provider, provider_id) DO NOTHING; + + +-- ============================================================ +-- 4. Profiles +-- ============================================================ + +INSERT INTO public.profiles (id, role, account_type, full_name) +VALUES + ( + 'aaaaaaaa-0009-0009-0009-000000000009', + 'tenant_member', + 'therapist', + 'Eva Terapeuta' + ), + ( + 'aaaaaaaa-0010-0010-0010-000000000010', + 'tenant_member', + 'therapist', + 'Felipe Terapeuta' + ), + ( + 'aaaaaaaa-0011-0011-0011-000000000011', + 'tenant_member', + 'therapist', + 'Gabriela Secretária' + ) +ON CONFLICT (id) DO UPDATE SET + role = EXCLUDED.role, + account_type = EXCLUDED.account_type, + full_name = EXCLUDED.full_name; + + +-- ============================================================ +-- 5. Tenants pessoais dos Terapeutas 2 e 3 +-- ============================================================ + +INSERT INTO public.tenants (id, name, kind, created_at) +VALUES + ('bbbbbbbb-0009-0009-0009-000000000009', 'Eva Terapeuta', 'therapist', now()), + ('bbbbbbbb-0010-0010-0010-000000000010', 'Felipe Terapeuta', 'therapist', now()) +ON CONFLICT (id) DO NOTHING; + +-- Terapeuta 2 → tenant_admin do próprio tenant +INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) +VALUES ( + 'bbbbbbbb-0009-0009-0009-000000000009', + 'aaaaaaaa-0009-0009-0009-000000000009', + 'tenant_admin', 'active', now() +) +ON CONFLICT (tenant_id, user_id) DO NOTHING; + +-- Terapeuta 3 → tenant_admin do próprio tenant +INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) +VALUES ( + 'bbbbbbbb-0010-0010-0010-000000000010', + 'aaaaaaaa-0010-0010-0010-000000000010', + 'tenant_admin', 'active', now() +) +ON CONFLICT (tenant_id, user_id) DO NOTHING; + + +-- ============================================================ +-- 6. Vincula Terapeutas 2 e 3 à Clínica 3 — Full +-- (mesmo padrão de terapeuta@agenciapsi.com.br no seed_001) +-- ============================================================ + +INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) +VALUES + ( + 'bbbbbbbb-0005-0005-0005-000000000005', -- Clínica Bem Estar (Full) + 'aaaaaaaa-0009-0009-0009-000000000009', -- Eva Terapeuta + 'therapist', 'active', now() + ), + ( + 'bbbbbbbb-0005-0005-0005-000000000005', -- Clínica Bem Estar (Full) + 'aaaaaaaa-0010-0010-0010-000000000010', -- Felipe Terapeuta + 'therapist', 'active', now() + ) +ON CONFLICT (tenant_id, user_id) DO NOTHING; + + +-- ============================================================ +-- 7. Vincula Secretária à Clínica 2 (Recepção) como clinic_admin +-- A secretária gerencia a recepção/agenda da clínica. +-- Acessa a área /admin com o mesmo contexto de clinic_admin. +-- ============================================================ + +INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) +VALUES ( + 'bbbbbbbb-0004-0004-0004-000000000004', -- Clínica Mente Sã (Recepção) + 'aaaaaaaa-0011-0011-0011-000000000011', -- Gabriela Secretária + 'clinic_admin', 'active', now() +) +ON CONFLICT (tenant_id, user_id) DO NOTHING; + + +-- ============================================================ +-- 8. Subscriptions +-- Terapeutas 2 e 3 → therapist_free (escopo: user_id) +-- Secretária → sem assinatura própria (usa o plano da Clínica 2) +-- ============================================================ + +-- Terapeuta 2 → therapist_free +INSERT INTO public.subscriptions ( + user_id, plan_id, plan_key, status, interval, + current_period_start, current_period_end, + source, started_at, activated_at +) +SELECT + 'aaaaaaaa-0009-0009-0009-000000000009', + p.id, p.key, 'active', 'month', + now(), now() + interval '30 days', + 'seed', now(), now() +FROM public.plans p WHERE p.key = 'therapist_free' +AND NOT EXISTS ( + SELECT 1 FROM public.subscriptions s + WHERE s.user_id = 'aaaaaaaa-0009-0009-0009-000000000009' AND s.status = 'active' +); + +-- Terapeuta 3 → therapist_free +INSERT INTO public.subscriptions ( + user_id, plan_id, plan_key, status, interval, + current_period_start, current_period_end, + source, started_at, activated_at +) +SELECT + 'aaaaaaaa-0010-0010-0010-000000000010', + p.id, p.key, 'active', 'month', + now(), now() + interval '30 days', + 'seed', now(), now() +FROM public.plans p WHERE p.key = 'therapist_free' +AND NOT EXISTS ( + SELECT 1 FROM public.subscriptions s + WHERE s.user_id = 'aaaaaaaa-0010-0010-0010-000000000010' AND s.status = 'active' +); + +-- Nota: a Secretária não tem assinatura própria. +-- O acesso vem do plano da Clínica 2 (tenant_id = bbbbbbbb-0004-0004-0004-000000000004). + + +-- ============================================================ +-- 9. Confirma +-- ============================================================ + +DO $$ +BEGIN + RAISE NOTICE '✅ Seed 003 aplicado com sucesso.'; + RAISE NOTICE ''; + RAISE NOTICE ' Usuários criados:'; + RAISE NOTICE ' therapist2@agenciapsi.com.br → tenant próprio (bbbbbbbb-0009) + Clínica 3 como therapist'; + RAISE NOTICE ' therapist3@agenciapsi.com.br → tenant próprio (bbbbbbbb-0010) + Clínica 3 como therapist'; + RAISE NOTICE ' secretary@agenciapsi.com.br → clinic_admin na Clínica 2 Mente Sã (bbbbbbbb-0004)'; + RAISE NOTICE ' Senha de todos: Teste@123'; +END; +$$; + +COMMIT; diff --git a/Pedra Angular/plataforma_saude_mental.html b/Pedra Angular/plataforma_saude_mental.html new file mode 100644 index 0000000..fc0c844 --- /dev/null +++ b/Pedra Angular/plataforma_saude_mental.html @@ -0,0 +1,1798 @@ + + + + + +Plataforma de Saúde Mental — Documento Estratégico + + + + + +
+ + + + + +
+ + + + + +
+
+
+
+

Documento Estratégico Fundacional

+

Plataforma de
Saúde Mental

+

Ecossistema completo para gestão clínica, educação continuada e conexão terapêutica

+
+ Versão 1.0 + Confidencial + MVP em construção + 2025 +
+
+
+ scroll +
+
+
+ + +
+ + +
+
+ 01 +

Visão Geral do Projeto

+
+

Este documento descreve de forma completa e minuciosa o projeto de uma Plataforma de Saúde Mental — um ecossistema digital integrado que reúne gestão clínica, educação continuada e conexão entre profissionais e pacientes em um único ambiente.

+

O produto nasce com a missão de simplificar a vida do profissional de saúde mental brasileiro, ao mesmo tempo que eleva a qualidade do cuidado oferecido ao paciente. Não se trata de mais um sistema de agendamento. Trata-se de uma plataforma viva, que cresce com o profissional, conecta especialidades e educa o mercado.

+
"Não estamos construindo um software de gestão. Estamos construindo o ecossistema que o profissional de saúde mental brasileiro nunca teve."
+
+

Pilares Fundadores

+
+
+
🏛
+
Confiança Clínica
+

Seriedade, conformidade com LGPD e segurança de dados sensíveis desde a fundação.

+
+
+
+
Redução de Fricção
+

Cada funcionalidade deve economizar tempo real do profissional, nunca criar mais trabalho.

+
+
+
🕸
+
Efeito de Rede
+

Quanto mais profissionais entram, mais o produto vale para cada um deles.

+
+
+
📚
+
Educação como Aquisição
+

Conteúdo de microlearning que atrai, engaja e converte o profissional ideal.

+
+
+
+
+ +
+ + +
+
+ 02 +

Origem e Motivação

+
+

O projeto surgiu da observação direta de um problema recorrente: profissionais de saúde mental altamente capacitados clinicamente, mas completamente despreparados para gerir o aspecto administrativo e financeiro de suas práticas.

+

A pandemia de COVID-19 acelerou a digitalização do setor — o atendimento online normalizou-se, a demanda por terapia explodiu e o número de profissionais cresceu. Porém, as ferramentas disponíveis no mercado não acompanharam essa evolução em profundidade. Resolvem o agendamento, mas ignoram a clínica. Resolvem o financeiro, mas ignoram o paciente.

+

A motivação central é construir o produto que o mercado deveria ter criado há anos: uma plataforma que entende que o terapeuta também é paciente, que a clínica é feita de relações humanas, e que educação e prática clínica são inseparáveis.

+
+

O Papel do Parceiro Acadêmico

+

Um dos diferenciais estratégicos do projeto é a parceria com um profissional em formação em psicologia e já atuante como psicanalista. Essa combinação é rara: une a visão clínica prática com o rigor acadêmico, permitindo a produção de conteúdo de microlearning com autoridade real.

+

Com o tempo, esse parceiro poderá atuar também como supervisor clínico dentro da plataforma e como referência de conteúdo para o módulo de inteligência artificial de suporte clínico, previsto nas fases avançadas do produto.

+
+
+ +
+ + +
+
+ 03 +

O Problema que Resolvemos

+
+
+

1. Fragmentação de Ferramentas

+

O profissional usa Google Agenda para agendamento, WhatsApp para comunicação, planilha para financeiro, Word para prontuário e e-mail para cobrança. São 5 ferramentas para uma única prática clínica.

+

2. Ausência de Visão Clínica nos Sistemas Atuais

+

Os softwares existentes tratam o atendimento como uma transação comercial. Não há suporte para acompanhamento de evolução do paciente, aplicação de escalas validadas ou registro estruturado por abordagem terapêutica.

+

3. Isolamento Profissional

+

Terapeutas trabalham em silos. Não há plataforma que facilite encaminhamentos entre colegas, supervisão clínica estruturada ou rede de colaboração profissional.

+

4. Falta de Educação em Gestão

+

A formação em psicologia, psicanálise e psiquiatria é excelente clinicamente, mas praticamente nula em gestão. O profissional não sabe precificar, não sabe lidar com inadimplência, não conhece suas obrigações fiscais e de LGPD.

+

5. Experiência do Paciente Negligenciada

+

O paciente é tratado como destinatário passivo do serviço. Não existe ferramenta que engaje o paciente entre sessões, acompanhe seu humor, envie tarefas terapêuticas ou registre seu progresso de forma visual.

+
+
+ +
+ + +
+
+ 04 +

O Mercado e seus Concorrentes

+
+

O mercado brasileiro de software para saúde mental está em crescimento acelerado, mas ainda dominado por soluções parciais. Os principais players cobrem bem o básico administrativo, porém falham na profundidade clínica e na experiência do usuário.

+
+ + + + + + + + + +
PlayerPontos FortesLacunas Críticas
PsicoManagerProntuário, agendamento sólidoUX datada, sem app do paciente, sem microlearning
Clínica ÁgilInterface moderna, financeiroSem escalas clínicas, sem rede entre profissionais
AmplimedMulti-especialidade, robustoComplexo demais para terapeuta solo, custo elevado
Nuvem PsicologiaFoco em psicólogos, simplesFuncionalidades limitadas, sem crescimento de produto
Google Agenda + PlanilhaGratuito, familiarNão é um produto — é um remendo
+
+
Nossa plataforma compete em uma categoria nova: ecossistema terapêutico integrado. Enquanto os concorrentes vendem software de gestão, vendemos crescimento profissional, conexão clínica e educação continuada — com a gestão como consequência natural.
+
+ +
+ + +
+
+ 05 +

Lacunas de Mercado Identificadas

+
+

A análise competitiva revelou oportunidades concretas que nenhum player atual preenche de forma satisfatória. São estas lacunas que definem nossa vantagem estratégica.

+
+

Lacuna 1 — Acompanhamento de Progresso Clínico Visual

+

Nenhum sistema entrega um painel visual que mostre a evolução do paciente sessão a sessão com métricas e gráficos.

+

Lacuna 2 — Ferramentas para o Paciente entre Sessões

+

Um módulo do paciente com diário de humor, tarefas terapêuticas e check-ins automáticos é praticamente inexistente no Brasil.

+

Lacuna 3 — Escalas Psicológicas Integradas

+

PHQ-9, GAD-7, BDI, Beck e outras escalas amplamente usadas não estão integradas com envio ao paciente, correção automática e gráfico de evolução temporal.

+

Lacuna 4 — Rede de Encaminhamentos

+

Terapeutas frequentemente precisam encaminhar pacientes para colegas. Não existe plataforma que estruture esse processo de forma segura, rastreável e profissional.

+

Lacuna 5 — Módulo de Supervisão Clínica

+

Nenhum sistema tem módulo específico para supervisores acompanharem casos de supervisionados com acesso controlado e seguro.

+

Lacuna 6 — Educação em Gestão para a Área

+

Profissionais não têm acesso fácil a conteúdo específico sobre gestão de clínica, precificação, LGPD aplicada à psicologia e finanças do autônomo.

+

Lacuna 7 — IA de Suporte Clínico

+

Sugestão de bibliografias por apresentação clínica, rascunho de evoluções por voz e alertas baseados em padrões do paciente são funcionalidades com altíssimo valor e quase inexistentes.

+
+
+ +
+ + +
+
+ 06 +

Arquitetura do Produto

+
+

A plataforma é composta por dois produtos complementares que compartilham a mesma base de usuários, infraestrutura e identidade de marca:

+
+
+
+
Produto 1
+
Sistema de Gestão Clínica
+
+
Agendamento, prontuário, financeiro, multi-papel, escalas clínicas, módulo do paciente, rede de profissionais e supervisão clínica.
+
+
+
+
Produto 2
+
Plataforma de Microlearning
+
+
Trilhas de microlearning por especialidade, carrossel de slides, quiz, vídeo, banners no sistema e assinatura independente ou inclusa.
+
+
+
+

Como os Dois Produtos se Integram

+
    +
  • O mesmo login acessa os dois produtos — a conta é única e os dados são compartilhados.
  • +
  • Banners rotativos dentro do sistema de gestão exibem conteúdos do microlearning, criando exposição passiva e contínua.
  • +
  • O usuário que entra pela plataforma de conteúdo já está dentro do ecossistema quando decide usar o sistema de gestão.
  • +
  • O modelo de planos pagos pode incluir acesso ao microlearning como benefício, aumentando o valor percebido.
  • +
  • O conteúdo de gestão de clínica educa o profissional para usar melhor as funcionalidades do próprio sistema.
  • +
+
+
+ +
+ + +
+
+ 07 +

O Sistema Multi-Papel

+
+

Um dos diferenciais arquiteturais mais importantes da plataforma é o sistema de multi-papel por usuário. Na maioria dos concorrentes, um usuário é ou terapeuta ou paciente. Em nossa plataforma, um único usuário pode exercer múltiplos papéis simultaneamente, refletindo a realidade clínica.

+
Insight central: O terapeuta também faz análise pessoal. Logo, ele é simultaneamente profissional de saúde mental e paciente de outro profissional. O sistema precisa respeitar e suportar essa dualidade de forma natural e sem fricção.
+
+

Os Quatro Papéis do Sistema

+
+
+
PAPEL 01
+
Paciente
+

Agenda sessões, acessa histórico, responde escalas, usa o diário entre sessões e visualiza tarefas.

+
+
+
PAPEL 02
+
Terapeuta
+

Gerencia agenda e pacientes, registra prontuário, aplica escalas, emite cobranças e acessa relatórios.

+
+
+
PAPEL 03
+
Clínica
+

Gerencia múltiplos terapeutas, controla agenda compartilhada, realiza repasse financeiro e métricas.

+
+
+
PAPEL 04
+
Supervisor
+

Acompanha casos de supervisionados com acesso controlado e emite relatórios de supervisão.

+
+
+
+
+

Regras de Transição entre Papéis

+
    +
  • Todo usuário começa como Paciente — é o papel padrão e gratuito.
  • +
  • Um Paciente pode ativar o papel de Terapeuta mediante verificação de credencial (CRP/CRM) e adesão a um plano pago.
  • +
  • Um Terapeuta pode criar ou se vincular a uma Clínica, que tem plano e faturamento próprios.
  • +
  • Um Terapeuta pode ser designado Supervisor por outro terapeuta ou clínica.
  • +
  • Um usuário Terapeuta pode simultaneamente ser Paciente de outro terapeuta — os dados são completamente segregados.
  • +
  • A troca de contexto entre papéis é feita na interface de forma clara, sem risco de contaminação de dados.
  • +
+
+
+ +
+ + +
+
+ 08 +

Módulos do Sistema de Gestão

+
+ +
+
+
M1
+
Autenticação e Multi-Papel
+
+
+

Fundação de toda a plataforma. Gerencia login unificado, cadastro de usuário, ativação e troca de papéis, permissões granulares por contexto e segurança de sessão. Prioridade máxima — erros aqui geram dívida técnica irreversível.

+
    +
  • Login com e-mail, Google e futuramente Apple
  • +
  • Cadastro unificado com ativação progressiva de papéis
  • +
  • JWT com refresh token e controle de sessão por dispositivo
  • +
  • Middleware de permissão por papel em todas as rotas
  • +
  • Auditoria de acesso a dados sensíveis (LGPD)
  • +
+
+
+ +
+
+
M2
+
Agendamento
+
+
+

Núcleo operacional do produto para o terapeuta. Resolve o problema mais imediato do profissional: organizar sua agenda sem depender de ferramentas genéricas.

+
    +
  • Configuração de disponibilidade semanal com exceções
  • +
  • Agendamento pelo paciente dentro da disponibilidade do terapeuta
  • +
  • Confirmação automática por e-mail e WhatsApp
  • +
  • Lembrete configurável (24h, 2h antes da sessão)
  • +
  • Cancelamento e reagendamento com regras de antecedência
  • +
  • Integração com Google Calendar (fase 2)
  • +
+
+
+ +
+
+
M3
+
Cadastro e Gestão de Pacientes
+
+
+

Centraliza todas as informações do paciente vinculado ao terapeuta, respeitando os princípios de privacidade e minimização de dados da LGPD.

+
    +
  • Ficha cadastral completa com dados de contato e emergência
  • +
  • Vínculo terapeuta-paciente com histórico de sessões
  • +
  • Termo de consentimento digital integrado
  • +
  • Controle de status do paciente (ativo, em pausa, encerrado)
  • +
  • Histórico de encaminhamentos recebidos e realizados
  • +
+
+
+ +
+
+
M4
+
Prontuário Eletrônico
+
+
+

Registro clínico estruturado por sessão, com conformidade com as resoluções do CFP e máxima segurança de dados. Começa simples no MVP e evolui com a plataforma.

+
    +
  • Anotação de sessão por texto livre com formatação básica
  • +
  • Campos estruturados opcionais por abordagem (TCC, psicanálise, humanista)
  • +
  • Registro de hipótese diagnóstica por CID-10/CID-11
  • +
  • Criptografia em repouso para todo conteúdo de prontuário
  • +
  • Acesso exclusivo do terapeuta — nem a clínica vê o conteúdo clínico
  • +
  • Escalas psicológicas integradas com correção automática (fase 2)
  • +
+
+
+ +
+
+
M5
+
Financeiro e Cobrança
+
+
+

Fecha o ciclo operacional do terapeuta: sessão realizada gera cobrança automática, paciente paga, terapeuta recebe. Sem planilhas, sem constrangimento no consultório.

+
    +
  • Configuração de valor por sessão ou plano de sessões
  • +
  • Geração automática de cobrança ao confirmar sessão
  • +
  • Link de pagamento Pix e cartão via integração com gateway (Asaas)
  • +
  • Status de pagamento em tempo real: pendente, pago, atrasado
  • +
  • Repasse automático para clínica com percentual configurável
  • +
  • Relatório financeiro mensal do terapeuta
  • +
+
+
+
+ +
+ + +
+
+ 09 +

Plataforma de Microlearning

+
+

A plataforma de microlearning não é um produto secundário. É o principal motor de aquisição orgânica da plataforma e o que diferencia nosso posicionamento de todos os concorrentes.

+
Referência estratégica: HubSpot Academy — a maior escola de marketing digital do mundo foi construída para educar o mercado a usar marketing digital... com as ferramentas da HubSpot. Fazemos o mesmo para saúde mental e gestão clínica.
+
+

Trilhas de Conteúdo por Especialidade

+ +
+
Gestão de Clínica para Psicólogos
+
+
    +
  • Como precificar sua sessão com confiança
  • +
  • Inadimplência: como prevenir e como cobrar sem destruir o vínculo
  • +
  • LGPD aplicada ao consultório de psicologia
  • +
  • Prontuário eletrônico: o que é obrigatório pelo CFP
  • +
  • Como organizar sua agenda e eliminar o no-show
  • +
  • Nota fiscal e obrigações fiscais do psicólogo autônomo
  • +
+
+
+ +
+
Gestão de Clínica para Psicanalistas
+
+
    +
  • O setting e o dinheiro: como falar sobre honorários sem quebrar a transferência
  • +
  • Frequência de sessões e modelo de cobrança na psicanálise
  • +
  • Documentação mínima necessária na clínica psicanalítica
  • +
  • Como estruturar uma clínica particular sendo psicanalista
  • +
  • Supervisão e análise pessoal: como e onde buscar
  • +
+
+
+ +
+
Gestão de Clínica para Psiquiatras
+
+
    +
  • Gestão de tempo com consultas de diferentes durações
  • +
  • Receituário, laudos e documentação clínica
  • +
  • Integração com outros profissionais de saúde mental
  • +
  • Credenciamento e faturamento com planos de saúde
  • +
  • Como montar um consultório particular de psiquiatria
  • +
+
+
+ +
+
Para Terapeutas de Abordagens Integrativas
+
+
    +
  • Como comunicar o valor do seu trabalho para o paciente
  • +
  • Precificação de sessões mais longas e formatos alternativos
  • +
  • Documentação clínica em abordagens não convencionais
  • +
  • Como crescer com indicações no consultório
  • +
+
+
+ +
+
Para Clínicas Multiprofissionais
+
+
    +
  • Como estruturar o repasse financeiro entre profissionais
  • +
  • Gestão de agenda compartilhada sem conflitos
  • +
  • Contrato de parceria entre clínica e profissional
  • +
  • Como criar uma cultura clínica coesa em equipe
  • +
+
+
+
+
+ +
+ + +
+
+ 10 +

Modelo de Negócio e Monetização

+
+

O modelo de negócio é SaaS com camada freemium. A estratégia de monetização foi desenhada para alinhar o crescimento da receita com o crescimento real do profissional na plataforma.

+
+

Estrutura de Planos

+
+
+
Gratuito
+
Paciente / Terapeuta iniciante
+
    +
  • Até 10 pacientes
  • +
  • Agendamento básico
  • +
  • Conteúdo introdutório grátis
  • +
+
+
+
Profissional
+
Terapeuta autônomo
+
    +
  • Pacientes ilimitados
  • +
  • Prontuário completo
  • +
  • Financeiro + escalas
  • +
  • Todas as trilhas de conteúdo
  • +
+
+
+
Clínica
+
Múltiplos terapeutas
+
    +
  • Multi-terapeuta
  • +
  • Repasse financeiro
  • +
  • Agenda compartilhada
  • +
  • Acesso para toda a equipe
  • +
+
+
+
Conteúdo
+
Estudante / Paciente curioso
+
    +
  • Acesso completo às trilhas
  • +
  • Sem sistema de gestão
  • +
  • Assinatura mensal independente
  • +
+
+
+
+
+ +
+ + +
+
+ 11 +

Estratégia de Fases de Desenvolvimento

+
+

O desenvolvimento é organizado em quatro fases estratégicas, cada uma com objetivos, entregáveis e métricas de sucesso claros.

+ +
+
+
+ FASE 1 + MVP e Validação +
+ Meses 1–3 +
+
+
    +
  • Sistema multi-papel funcional e sólido (fundação arquitetural)
  • +
  • Agendamento completo com lembretes via e-mail e WhatsApp
  • +
  • Cadastro de pacientes com vínculo terapeuta-paciente
  • +
  • Prontuário com anotações simples de sessão
  • +
  • Financeiro básico: cobrança automática via Pix (Asaas)
  • +
  • LGPD implementada: criptografia, consentimento, política de privacidade
  • +
  • Beta fechado com 10–20 terapeutas reais
  • +
  • Frente pública de microlearning: landing page + 1 trilha inicial
  • +
+
+
+ +
+
+
+ FASE 2 + Diferenciação Clínica +
+ Meses 4–8 +
+
+
    +
  • Escalas psicológicas integradas (PHQ-9, GAD-7, BDI) com correção automática
  • +
  • Dashboard de evolução do paciente com gráficos longitudinais
  • +
  • Módulo do paciente: diário de humor, tarefas, visualização de agenda (PWA)
  • +
  • Relatórios e laudos com template e exportação em PDF
  • +
  • Módulo de Clínica: vínculo, agenda compartilhada, repasse financeiro
  • +
  • Prontuário completo com campos estruturados por abordagem
  • +
  • Expansão do microlearning: 3–5 trilhas completas
  • +
  • Meta: 100 terapeutas pagantes, churn abaixo de 5%/mês
  • +
+
+
+ +
+
+
+ FASE 3 + Rede e Crescimento +
+ Meses 9–18 +
+
+
    +
  • Diretório de terapeutas: pacientes encontram profissionais na plataforma
  • +
  • Rede de encaminhamentos estruturada entre profissionais
  • +
  • Módulo de supervisão clínica com acesso controlado por caso
  • +
  • Programa de indicação com benefícios para terapeutas indicadores
  • +
  • Expansão para clínicas multiprofissionais
  • +
  • Meta: 1.000 terapeutas ativos, 20+ clínicas, efeito de rede ativado
  • +
+
+
+ +
+
+
+ FASE 4 + Inteligência e Escala +
+ 18+ meses +
+
+
    +
  • IA de suporte clínico: sugestão bibliográfica por CID, rascunho de evolução por voz
  • +
  • Alertas de risco baseados em padrões comportamentais do paciente
  • +
  • Integração com planos de saúde: credenciamento, guias, faturamento
  • +
  • API aberta para parceiros
  • +
  • Expansão internacional para mercados lusófonos
  • +
+
+
+
+ +
+ + +
+
+ 12 +

Cronograma do MVP (3 Meses)

+
+

Cronograma desenhado para um time com frontend e backend separados, priorizando entregas que gerem valor real para o primeiro usuário no prazo de 2 a 3 meses.

+ +
+
+
MÊS 1 Fundação Sólida
+
+
+
Frontend
+
Backend
+
Entrega
+
+
+
+
Sem
1–2
+
Telas de login, cadastro, troca de papel, navegação base
+
Auth completo, JWT, estrutura de roles no banco, middleware de permissão
+
Usuário cria conta e navega entre papéis
+
+
+
Sem
3–4
+
Perfil por papel, dashboard inicial por contexto
+
API de usuário multi-papel, segregação de dados por contexto
+
Experiência multi-papel funcional e sem fricção
+
+
+
+ +
+
MÊS 2 Core do Produto
+
+
+
Frontend
+
Backend
+
Entrega
+
+
+
+
Sem
1–2
+
Tela de agenda do terapeuta, configuração de disponibilidade
+
API de slots, regras de agendamento, validações de conflito
+
Terapeuta configura agenda
+
+
+
Sem
3–4
+
Fluxo de agendamento pelo paciente, confirmações, tela de sessões
+
Notificações por e-mail, integração WhatsApp, vínculo paciente-terapeuta
+
Paciente agenda, ambos recebem confirmação
+
+
+
+ +
+
MÊS 3 Fechar o Ciclo e Lançar
+
+
+
Frontend
+
Backend
+
Entrega
+
+
+
+
Sem
1–2
+
Tela financeira, histórico de sessões, status de pagamento
+
Integração Asaas (Pix), geração automática de cobrança, webhooks
+
Ciclo financeiro completo funcionando
+
+
+
Sem
3–4
+
Anotações de sessão, UX refinado, landing page de microlearning
+
Revisão de segurança, LGPD, ambiente de produção, monitoramento
+
Beta com 10–20 terapeutas reais
+
+
+
+
+
+ +
+ + +
+
+ 13 +

Riscos e Mitigações

+
+ +
+
+
Arquitetura multi-papel mal modelada
+

Revisar o modelo de dados antes de construir qualquer módulo adicional. Garantir que 'papel' é um atributo do usuário, não uma tabela separada. Code review obrigatório nessa camada.

+
+
Impacto AltoProb. Médio
+
+ +
+
+
Integração de pagamento subestimada
+

Reservar pelo menos 2 semanas dedicadas para a integração com Asaas. Testar todos os fluxos de webhook, falha de pagamento e conciliação.

+
+
Impacto MédioProb. Alto
+
+ +
+
+
Dados sensíveis sem criptografia
+

Implementar criptografia em repouso para prontuários e anotações desde o início. Refazer depois é caro e arriscado. Tratar como requisito não-negociável.

+
+
Impacto AltoProb. Baixo
+
+ +
+
+
Desalinhamento frontend/backend
+

Definir e documentar todos os contratos de API na semana 1 (Swagger/Notion). Frontend desenvolve contra mocks. Reunião de alinhamento semanal obrigatória.

+
+
Impacto MédioProb. Alto
+
+ +
+
+
Adoção lenta pelos terapeutas
+

Beta fechado com 10–20 terapeutas antes do lançamento público. Onboarding em 5 minutos. Plano gratuito generoso. Microlearning como canal de aquisição orgânica.

+
+
Impacto AltoProb. Médio
+
+ +
+
+
Escopo crescente comprometendo o prazo
+

Definição rígida do que entra e não entra no MVP. Qualquer nova feature proposta vai para o backlog, não para o sprint atual. Product Owner com poder de veto.

+
+
Impacto AltoProb. Alto
+
+
+ +
+ + +
+
+ 14 +

Estratégia de Adoção e Crescimento

+
+
+

O Funil de Aquisição

+
+
+
📢
+
+
Topo do Funil — Conteúdo
+

Microlearning gratuito atrai terapeutas buscando aprender gestão. SEO em conteúdo de blog e trilhas. Presença no Instagram e LinkedIn com dicas de gestão clínica.

+
+
+
+
🔬
+
+
Meio do Funil — Experimentação
+

Plano gratuito com limite generoso. Onboarding em 5 minutos com dados de exemplo. O terapeuta experimenta sem compromisso e vê valor antes de pagar.

+
+
+
+
💳
+
+
Fundo do Funil — Conversão
+

Quando ultrapassa o limite do plano gratuito, a conversão é natural. O terapeuta já depende do sistema e o upgrade é a continuação lógica.

+
+
+
+
🔒
+
+
Retenção — Profundidade
+

Escalas clínicas, relatórios e histórico acumulado criam lock-in legítimo. O custo de sair é alto porque os dados clínicos estão na plataforma.

+
+
+
+
🌐
+
+
Expansão — Indicação
+

Programa de indicação: terapeuta indica colega, ambos ganham benefício. Com efeito de rede, cada terapeuta traz naturalmente sua rede de colegas.

+
+
+
+
+
+

Estratégia de Beta — Os Primeiros 20 Terapeutas

+
    +
  • Recrutar manualmente via LinkedIn, grupos de psicólogos no WhatsApp e Facebook, indicações pessoais.
  • +
  • Oferecer 6 meses gratuitos no plano Profissional em troca de uso semanal e feedback honesto.
  • +
  • Realizar call de 30 minutos com cada beta-tester após 2 semanas de uso.
  • +
  • Observar onde travam no onboarding, o que tentam fazer e não encontram, o que elogiam espontaneamente.
  • +
  • Usar esses aprendizados para uma rodada de ajustes de 2 semanas antes do lançamento público.
  • +
  • Transformar os melhores beta-testers em embaixadores com benefícios permanentes.
  • +
+
+
+ +
+ + +
+
+ 15 +

Visão de Longo Prazo

+
+

Em 5 anos, a plataforma deve ser reconhecida como a infraestrutura da saúde mental brasileira — o ambiente onde profissionais aprendem, trabalham, colaboram e crescem.

+
Visão de longo prazo: Ser para o terapeuta brasileiro o que a Bloomberg é para o mercado financeiro — a plataforma que nenhum profissional sério consegue imaginar trabalhar sem ela.
+ +
+
+
🗺
+
Plataforma de Referência Nacional
+

O maior diretório de terapeutas do Brasil, com pacientes encontrando profissionais por abordagem, especialidade e localização.

+
+
+
🎓
+
Educação Continuada Reconhecida
+

Certificações reconhecidas pelo mercado. Parcerias com conselhos profissionais e universidades.

+
+
+
🤖
+
IA Clínica de Suporte
+

Assistente que apoia na documentação, sugestão de intervenções e identificação de padrões de risco — com o profissional como protagonista.

+
+
+
🌍
+
Expansão Lusófona
+

Portugal, Angola e Moçambique como mercados naturais de expansão, aproveitando a base já construída.

+
+
+
💚
+
Impacto Social Mensurável
+

Contribuir para o aumento da qualidade e acessibilidade do cuidado em saúde mental no Brasil, com métricas públicas de impacto.

+
+
+ +
+

Este documento é vivo. Deve ser revisado e atualizado a cada fase concluída, incorporando os aprendizados do mercado e as decisões do time.

+
+
+ +
+ + + + + + + diff --git a/Pedra Angular/plataforma_saude_mental_estrategia.pdf b/Pedra Angular/plataforma_saude_mental_estrategia.pdf new file mode 100644 index 0000000..dc93825 --- /dev/null +++ b/Pedra Angular/plataforma_saude_mental_estrategia.pdf @@ -0,0 +1,429 @@ +%PDF-1.4 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 6 0 R /F4 23 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/BaseFont /Helvetica-BoldOblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +7 0 obj +<< +/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +8 0 obj +<< +/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +9 0 obj +<< +/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +10 0 obj +<< +/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +11 0 obj +<< +/Contents 34 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +12 0 obj +<< +/Contents 35 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +13 0 obj +<< +/Contents 36 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +14 0 obj +<< +/Contents 37 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +15 0 obj +<< +/Contents 38 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +16 0 obj +<< +/Contents 39 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +17 0 obj +<< +/Contents 40 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +18 0 obj +<< +/Contents 41 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +19 0 obj +<< +/Contents 42 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +20 0 obj +<< +/Contents 43 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +21 0 obj +<< +/Contents 44 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +22 0 obj +<< +/Contents 45 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +23 0 obj +<< +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font +>> +endobj +24 0 obj +<< +/Contents 46 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +25 0 obj +<< +/PageMode /UseNone /Pages 27 0 R /Type /Catalog +>> +endobj +26 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20260304134538+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260304134538+00'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False +>> +endobj +27 0 obj +<< +/Count 19 /Kids [ 4 0 R 5 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R + 15 0 R 16 0 R 17 0 R 18 0 R 19 0 R 20 0 R 21 0 R 22 0 R 24 0 R ] /Type /Pages +>> +endobj +28 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1167 +>> +stream +Gatn%9lJcG&;KZQ'm&.^C+a/oIIqEJ!_'EL2JQ$u#dP)QLo&UeZ2=-:8:Qh/4-^6S7+247jB`uNoVSC([i6Mi2\3`H4llJt2%Gr`o>@We7<$tO%f`[aNk;T3C02RHIGWa.K+H_2L-$RaJ(<*JG#MG\2Lh7@-%`DY.M0F2-S>@Bgq[503JsN;OD(0K`*fPU*PZ!WQ7SmR`9d8i0PL\,j?XF((iDb@YN>b&VmNH)Kl!GSP%e--%Q/('VVG61"7Z/q9g)cO^ST)8d/eR)EO.u(#:]R2%XBA%;Z7@X9oE`Za[Da\O8AT%Fj%#G#]#[`r0n"s@fY\c,;b]0]gChTp*tMP=<-llD/7:**T9RZFEBusU;6`\k#kIPSBKBSK`*YBL'cHg^IJXg;@od+;7`Tc:3\@;MO@^USq^P/O@l2.m$SO6pbC]>"XF_:^.4ftR]Br+abt"ANV0S>I4FZ.dYaPTaUANJ&J\D#E8+qO$;??tFjQ^_KI1SF%Vl`/W=a*a(<)/m[tg?7fhq%NAKZ/Gipr250Qi$ZX,)U@+^"@5Rp[#Tn?mlrR@mJ/TC_AfI_aZ$=^b;A9n`@-rId;Td0:pDOqF!;BSW"u:m$2o0(SYLh$cJ/B;aHJH#rq&esag8gH%Xk@]d1>ds-b/fuN-ZmHX3N$J^cH0%C>R\lDiTTofHi;FF<0[?#Nud`qjX(--HSJ:aNu5H5#shEc)[O>eI1q#;37&@@`KJ7cV\'b[r[R0a`n(PIo0T3WaK2+&6J\$"P1'rmMRcm)(0>1I.R"UPe1%6CrXMh'ZW':)7VTM4lQQn^TeZ$XV`U,MDT,V5mGYfZ[F0Ym4)^,edUG)([]IiGV2c5b;!lIn^9+>&]`bB4`hnJDG0d:H\\)aS(L$T(*oC@4l%6nS-^SF[~>endstream +endobj +29 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1306 +>> +stream +Gb!$H95iQE&BF8='QZqUPfrg]kfjJem)F2'S:&`,[%T`#2j0[7qWW4"8:DgYp9^]),c4:n1S"9'r0(_Xn\s5^kn`4'"oNAXJ8n\mJOq%o0nJZ0k\3F%APP$(bYmCZ&FPu?T\PJ5I+U2?PS=1`;hQ*dJQj=oQIs'lqXXtS`>>t#9lR=r"\3ZI!5E*901loF\78pW+DhXI/R6LHj2LfUDE])YeMZ-CQU5XB9Wu2a`fj3&G_F'!Y+oS3mrpFT--f&&gAP#BG?\G(!NrK79^8HNCm1[rQCJo(%)k83q'+(Hr5]Xn8mY,16GDB_>('dIu>BdRl6jmF!h0F"5[=2ZM#BJVBq(li%Crf8!8g(%1nJaXO?NG)OE!._K;C*mCK!W2%3kWVrfVc&YBOR+WQ3OAP'XL_3045?O!`FIEM$6R,8mAj]["78P.uDd"ToPM"$RjlB]EKNa+#[A]0'';C="2VgVp:ul8'J/%(=1"M_6!.m_a$Jo=mX9h1.nhMZ^pL:[2I2aqk\f=^$R8-Y8DTRlE7oBJer3BZQr/ES_ucXF)'-'#+6;ZWs;/BQ"mu?Y@.!IIHYD%2fTa3?0cq,2G^0?i@HL`D'8+p9\i$.pkDor&JPB,`'?mMT,i>Nkf1fXsYmG:kVQ2Ctj$NoO.sI$NXJmU0<#'_h3fbK5e_*ROll5jZKSA<1/`;W3]4-D.n'p>H-4*fOui"iX4/~>endstream +endobj +30 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2486 +>> +stream +Gau0D?#SK-&q/*0(bDmVDNXMTqkJ)?9j4np2qc.(IE;jAdgcaWF=>7:gg>QW:`l!oVFb`N@3h]qMOZ\J3+p5pH2.1&Io-gVB0lRsi0,80@RY_k,$J6uq;UVs8GJdK-U%i$o>3U4&`e=lm/![#bF'H03!r$1o2l-0S5><(0M3P5:pbOiKLPFICpuhu)=NZFACpm+rP=dZ&P4&)5%R#.g^8ssNit'1(T/?-YJ6UUff@lYhM,Hh_3acN(8huKU;]&p]GVJo&"[)kbVPBZ;7aO?R45^>eZGP:VR<_C%:ami$U_BhOOAJSYOk`CKpLh:09O:HQ@pOS,YG`Qh^Jd/Ll&SC,E.<=a([Yd!N:B1`.1l.4)7(df4=5oeuC>.4l?mc`Scm'/Q:(5a@GZ[HoM[\;H([aFR%dJ.#\$uVk\-F*cu.]T,iCpp<5/o#3+i3@-M$hU^e$i9_aju2kK9DWm^I#DX\S'!E9o1Xam7bXc46CoS)Jp&U&EX[(LO[ntLG2dD-+V+&4MsTZ2c$QJAGL'?4b"Z2gKRHuH0"ejju`AfqrEd4o+B@1R28$_[VIo.Bb`3I%PLg\=LH+"d*:38;IQS"WEMlN.U@7P2N8tL)f*/:pB#K)jWJ#3q]1a$9%h@9!R/'9[pTIG?a.Xk&9d1j[%=U.A3'E)o=-/7=-4t+dqDk*9Bik(qi6hT/!ku33X'!`lF!?D(tqp!-uf[U=r6a_e?UU/J!:G(9\mPr\R^b?U&O?G__S:483:mQiT!O?F&P`f@.m&*)UfD?'q=?ojeFH/482!&c]eRscC:$kM9,#g3R2s%Nf`oG(1-/#a1C*9a&JK143h5i>3Q&?C5=kW":BkpI7)Vf0ZZNl[I@R&/$baF#9D>L30;Jd`RH0AdqXd70d.QSUh1+Qp9TeXEI,HQKGlYUJQE@,hXW>Kqh4],TO7S&\>S>U7C\Q8B$3Z\Z4n,dMI2%k#;,NMmD5mt+5<$/C!#NEZVUL;Y3)o]qR\U_Ka1W49k(_p8jY8>,Ac>sSLjW+>*rV[3l(^YYrtY,R\1,nmt*6#F&E*Uq)Diobc'&Q.=Q[qLp"gTlm`t>W-ko*f:)OXV&?!9aV.:!4FmROV.4SA&rS[n?\P='IoK-r5`X8(c]*;PQ.Da.0Y#_MWIan;/BeH%P!PaOLaU1/48cJ8WLRjL6[,QBui>XY).f5m+%V+UrcTatGR3<@%\:JZYkDZAB:IPu/?B>i!#n((:`8'9pC2Hd@oaI/($3tSN(\kuKYc3c1,E,+Xd*+tInO&os*N(0p240HO?&:QOI>;s4]<["joWSkV2YN$@m0r`\2&kWi`s"l"Q896injiYC(@_2(HRJ/88E27GC@n?T(<8^$lVUGKu!;/Ze'd'!agtSHe_J3d4>au9h(%,UXrutRXVdW0r(0&R\9(rLN+fgh+OdUlL.nd$"/6lffOfao\dH1VXL8joCF5Zu#MQ3?*&"RV1c\R*Tb8"Y_LJt#WCJ#miXmNbmD,jrkJ_6?N8cuI.)ajjqfngmBMcJ7gg+1>eHAbrr.]c4#>9K_'lC5na$5qU2Y?b9\.*m5iJ+RPVF]g['/.p^?0E!"A:G*3@s]_,KFqiZ#Y](u$hpi.tFMo:PEXN0?8hUG1uBnuH[bHIP`hYK7S!r9d'Ni#PYWH]1%Y.RtKqpli3[&=:Rh\V_"?n/1eJAtfj6YUs8qdkoZr6u6^#C;?R@0+Mg_l;M8%89WKD+_,;4ns9R**STO%_DV[pJlJdkfR$,2$KB\aJ*1K/oVX\FgEgqUnP1VYTnu\/i@%W:mgitm%S"paSHs_7VCDIP>tgccGWF;DR'O0fb/D/)H1Mg2[HbhU8O2]!tQr):`\t]L:u2_CtX;bec7kZ(j$:n46KE<&0R'N:^k,4B7Np\$s5[=n4ofPiWr5#Z(9!:?NcDiZbAf23S7*;EU03W!m0.Qmg`PU]R(3qFD;s:$'9@5hiUEOmGjfUE/a6*4aIc3&._4.n+>b=>X+k2>P@>f%W3XKb0bR'MA@,)e@2]0\p)O"c&V[KrLr?(Fi2mdB/!%e&D\m,a#,`0FS%eb:pUW~>endstream +endobj +31 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2081 +>> +stream +Gatm;gN)%,&:O:Sm%`TE)jHZg\a_FK?qX,?-(-2mUBP#SenPDhq(b:C//Sp`X516Qo,8]WI2\K[_f?D)I_HJVmhHp2q>[2\IbE'%.IM[f9nB^'9Sr#Rhds:^ic"a1^h)BH/or[V"[?2ECC?S]tP^[?3[4bduo!Y(T-$#2[M'e?4E>g.uTAi3*EtTLWTjn[+PF>c4O=D\X]T@(bOj)mJbT6[^QuIY\Udlns)M8F;s/0YE>93$aUkhLe.T0lkqTq+\le]$l!rtHoZ7>$MAja>h0@V#qOW@G,4_t#&k&2?Ncj]N;Hl2O[=-6(ooPUN=WAN3"h\X'buREe#r!7as*f<,QDnf>9/dQ0k2D4?Rr,321u[,jZ^hU8F'BW1_"g39*+C0Qk%B\g%\"mtPF4]]!36C#g8g\k8"hHu13Vf?;M(.TJ:4Q?*fKdo','4/$Olau!\'LZ(EBG(L?%t26NuCPBVPi?nP7i'cF1-a2dnd0oM+WZMjph9P%r_`j1uNWfZdfoO([V034:&%)c1R$+RolfB_``<'?_alA$j@a38jZ[jKEdX(7B._+E$qRhDU('%kP0=gZjg`WCAeX!Jpq@9m\u/B<:OkDLp#t[^SOc2"H.5O#2-Rkl@H#CYm/](,@IdPfLq(@ad/W9#s$`(6A=[RGTq=a[IgK9GlKK2B@kVJh+m;.gapVnPDU?BTuL(Km&:peoPb5K6c>)IMHRP/G9DqBQYgWDicJg($S&K1O%X8jTXb]A\caB*AXEZo0IZh`c%MUf0\]X)=0CniTP8+IMK*;[tLf2bgV-*A'1P0'oN@)m2Q[^S&@0D;QqRg[Q%O@c661SF(o$VY9-Mq!L_a7%'3Q['MV0ZOSjMQq^eDnGoKTLL,@GC(jD9L)O@#6`%C%Vde]WcVTJDiYPRN1hcDh+1qfFB&.sFR#/n4@9g%>NjpTDG$^)\_N/28YZ9fcGO',jHJEHk]:5JNa-HP[bBp"2,SqmDnr*Tkm!_[:[IsJH6dD0JBZb&>@U)*q*.<6oH.6\r0sXc%+BS_.d14W6f!W]4LPfrcur>QpP.P3cN>N];F@p?s:(d^`g&aTFDW#V%"Kn2&\s*c&smB#-L-$IV2OQ(!fsP0:VB_kt*]01r'/Y=6[O`juclD4h$Li\hCP=dVWfE1$<$'YTK.SO[2&a$dM*Q/t'G1(8cq8#(u+h0?.CYDICXOPeAT%eU/W*$rtURV9Op`\hNAW`a!n6;SJeoK-1pn%Gpe@H\\TPI9F,/Kk\*i9M@]XU@cEXA+3.O$\[u0NT4\r<4`V%aREuEYhoX3NI!VT`;*gK^HNhK;@)Z`fQDiKCk@LmhqGOTCd&!Vm^SoMg!.>d`[3M+mD1J]]s/Y"*D_.4/s9>9ENU0""S6Ul&r;RPlQ1NT2^L[c[(E.\YQNUn+@a]p]BV;\\Ud+R:f!9jI~>endstream +endobj +32 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2052 +>> +stream +Gatm<>>s99'Ro4HSB#F^[$'P:^9JA5gO[d>]!Dp[#[9mNZ)uplFT;;gZ=CoU!jn05K\2T52c=t3\2aFD59Fk-IZO@<'J^KOs#]N8Lt2HLkkbbE$5FTn;T/ucaX7T?u6=>8r*a+K[,l;AiL]MNB^lf@lB:)dS^E!,t-Nf]b*pQcdAWr.G8r)eUH;*B8tL_XV*W67Lr331\c0qaAgJn7[&"o[_'=JsaM#R$AF?dqkbC;s-"Cc/T;'f/\/sI7BR]`72@!;GjNMSEWR1F)aV'.'\#ZJ*s=(D\ZR(AO:#!O11d?^.R5>[YDHWrf1/\V5QT[YUZJ`-k"A9iit8*R;5_tdi(c\JUlEk2kAZYs\P(>c5cNaEUh5J)VnVV5Rg20lm?WXQHcVC[m7*&SK<$8/o=:<^pIdJp5gm/L%'h1Ku`J*n>1IHD9:)""$DeIRQKf/?iCm*b:jQL=K2gB!hRf@OO`r_NZ$Cda`J8l@''4pGYdi2QPmDUs7n-A6Iaq5he_GKueW\hj%>ZBt84g's8\i-ubRtLbgZ/J.h+(pPFS-oFKQR'O>5,\eG]GX`W;Gf1mI"dj_rDkbCAQ="f4d4;@5$@%Z/"&>dQh\Qp-H\tetnp<96=K&RLZGNm=(XWsrff%SBld8U$NK'04(a+R"IM,G"cC0[33LX2c+*9.8)Kb2D\([)"Rlil2MCR;T/j"C#Rmiqrl!i82Ppq!N=ICSPERa`)d("/R$9Q&AJ)E=_mqk.Ocn@?Q+g0ua_5Oo6od9ORMchZQF^>BH''`/M+/;qpN"do7O*WY=@%0$iJb?A`94*TupS8ms7(AQHj51qpKZ9s^hH)F;_,L#hgP9kE7/pZ2EhI\4n>)>0GrmlZc/0o"'\CKPa"4.G,NcWD(c*71YfBD.f$fjT)+--ol"pn;IL:^LknRmS`36huJ]^LY-EeTG&^'et<alo7`7QV(WA`poc)GNF(51:ql@a[[;l-#*V'ALM9U$c87bA&F\S+FoBSX+K%E`TVdKIp-gJ>)q>HUO@[Skg4?j/>'Mj%,BWIeK'u*;s):emnuWF<^OSe"V>nD)\#(Fk3Fi+1Q[0QAm=tjQJc<31Fe]m^#co>O:4Foj'Y>a!J#oV=MZZ0cg0&SUB=-~>endstream +endobj +33 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2097 +>> +stream +Gatm;gN)%,&:O:Slq;!>N.V`C_2I383(_qGS;WXrjTVM("YE:o]JscC^'>c,;+d'd`ZEGs'Fb,UkFCA7j:pR6LX2\=H9S7a??+tVk"F\gMeRW70BZtTc5;UeSoA.$Me"GrM;7!]Ap%/7iJ^F,@OXn1MMY:Y,Vg_,,fU0YbGT"*'Rs4*NY%HN'69Ct#N.3V@o3P*EfkFcKOeMHGstNc?-hZ>=)U=9RF1+h\t+84;W-QZbFA=oqH'-c7J!G;7QuIbgWM]hGoEUXgL%OdY>!b4HJ*J$mQO4]=13)?8ViU;\9_j*VCnr&-YK\h-!;FKj[kt3$,0>Cn-p_l%La5]LoXL7?iV\%;`p.d/-0@g"`oVeAL_6["*h=`WD^7qnc*^^E/ft&t6m199gZ^mS_Z(Y-o6H(s%U>(fq#<>1'q_fFV8X;o8m=u@89u?.YKAJscS*(D=VNIm;AI4T$<'Q]%jNr\aCHaQeq:J(XqFRJ"?-ci)]U@SR&BBPBZcfh"FU==<]t2(Ag$N*.hiB22=L%gMo-hqZQk$kA1D]MB15HGio@0ZhR$9=\Rce&Bac(L2AOcFD)L/8]$i2-1t,XWm,umVPK>r/4`oRgOMolW^fE]fgf"t=TLpc$>c.'2Ct2!LS-N^42-kt?LgSNQK;n1_H>!mUA)4X08dU2OP_`\s^/s+E[bkj3a0H8u1ron.J>;C-ag47Rc+mb'gWice&8YiWRQR\jpm5KDJWe.VHp_"Yn&c]iV#tuNLW+tdZ\DXo38E&JCOi[nK??!+Y#9de6`\4BE!\d57W/TM8$>YEGnmK`-QFUd50g.Enp8q$iVM3nSLoIRGGo7+?+mOL^Tb*u:QYoaW),>$@hMXA\m=er&ePrPatuhS#Ei8+,WUE1n1Xp2.+A/X$+/Z*N"8mDJ3K.$6WDc#e1,-L2`bZpo7ptQ?O0j!oKnab?K+W4[ao`Zl!m/IYOb1imV;r?TS<''2`?m)'^2"WI>)dS0hWK#`.6<)hi2A:G:EN?B'Yb)hmpnuds:0d0s/hEYcnH^6,ETrM#B3$m!5@1H\s#3bZH?&BL`dee?'&j#VM?kZ.6e@]D>'N]``kX^PbeQ1BKYk1/g*P*^\JtDgZk!\!(#4a7#W:u3N)#.r_Rn3H%,_bHf)l&\bQ(!BXEHCRTTu\V7\:?9fEHD:bVs)+>Lj+2AVro+P=J32.bu_%,gt3H^"N'U\3dqg%BOKTens65&3\@;_**VBR.2H^"5E:kYC)#">BqMq"6R4qg2_WZ4Xf0Z5J4&c9Z+5Q;u#uO[skqJ`>\+Ng&_o!gQ5/D;_$,R#HtL.,HC&4MJWD1;s_UF@%[d339V9a*/dG]CTJ.:P"@qLPN*A4e2D1o:^r41fc&ht"#DJJYa?*@NWcoL&1RGDG27jh*PASIZ(F_Au&-FT)'ljh>'n]#9b#J1;ICeuc#l$`;EP(RWJf:^:;q_L#2Gc.N?hP?A$,JQUsd"!tc1?BE+*6EUmZu8/ULWAl>J"TK%VXaA$Vd&13s1g;cqotiQTA"XC)%KpFFV.;UqrS:"Q-<#QF7aE~>endstream +endobj +34 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1866 +>> +stream +Gatm;968iG&AJ$Clqu$K-lrL_oVNm/VO=5T*4[G(?kQA!+UOSJ2q)-k"\;7M/_P"g,?pn``BM"aHn,C8?S&j&-,Ah(r;M]Wj07A?Mb"!:O)V?f]/q6r1)2Sf/>Y[Q+C+^]ctq#X4Ie/mA0_:K,md@X$p5g[MBe?>Y^(AG9(@V`_$#PC>>QRd%H(MMqBBD^/$9_b>`bUe?,GUma45M/mdfYl$Jo+)f@>il*%sK]r3]jZr!h67@Jsq?j6hV%9N*rn!9:DVRti_p<$Y^<$7>68a\nfP-qPMm_c&j1+jr`%K%VDOT,IKWFYoYIq%uRF;j,.&=]tQBRE^[g#[-c%EU%u4+jgZ9'3JOg\c-YuZ+drh9"BW$=-mj/f9Z_K*;:[M$/PK@_ETI?iG(^AVAt6#H9^Q%KIBkH9i['X$M\pF5<;T4_!``Kak8q=P1KNl%PD"15dTV41olFGHIJ_Wr'd9rqlH`SsQ=P#11TJhf'`RK*[UVVJ!ONuuLpBRDA9r@r8*d2oY6H.*b1)EJpW/:j+?;1KF:XuI@#K(054=3XO5b6)4p^iHuQqq0:&SE5K?m63#WL`sd_i"8u`pT-t79H!.`T,?V16-"&il?h/EZlo1PWpjHr$W![lpnmZGeP-9'1@'q8I&?-CF8Zr98Slg'g?h%Yg=N,@2KC;`c6riF_D?ap=6UB_;&PohIC$4311Br7jLr0%a#Q*$EQ$dfh>2fcZ'A$l&!gCUO7-F%WO\kZ!`s_<4pjn%\NKL^TUW\Lr-S<4T/fs`YCs%Q*%JVB4FL9k9lfh@.E]8odT/EqfJ3jdFb<.'q"s9QdsYd\IL0soGq(W;`cR>@k1hr`\r'Ip-iW?CL*E=3Ot)rJ-7VW12WlrNQo+/"865M,@Zf0Ku8q+>%\WmlEA%q9r^Iu*2U#lQa^XZ><ZrM!Y9X'9m>`[dKU2O]&0eDdL*Wb^3rdR-mfoTkaEmZ`"b/8Md$"r7kh#htBS'D.L,"E?<)O#a<%]#6ZjLF]M:qh9O?Y$a:+#p"nb:3G+Q%MmtjrX$4?@]AQ^m"\9I)#0Y\-Z@DmMBeHkl=lYOAh8jrH>ScFSS&V77th[RFu-8@!%iae)3IH6b$a5,_=-(fa\@u[2%oL[&nfDAD"s[P!(T#YU/'I"MVR3]"PJrI4/UomoKS'93r00/5AZg)>oo,?B:Z0\5%8AhT^?Zg-&AO-`-t:_qph;6tgsaH,/?W==bV2XKM86]eDZif(sK$CdKF+.##ekkWAfRsE6Ih4K2u'fNtRA2sKVh!TKL!C"_H$DJ9km-ltEmL'"+MZ%i$8s^LCBZ:9^@7"bC*R]8@"DSVm#-Vlu:2ljJP^kFTIJRkVAMu`qR:fqKIKKdj*8Ki]R0>#o1Uf8l&Hkuq"2QuD1o6ecEM*4:4Q_JaLtif_Nj-@\4ha"F&U]/V@Ia+tj1HeS@;&3/Ri[(.qPSm("0cq.(Ps&GmVqtD!Ik@WSG`][]am"~>endstream +endobj +35 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2265 +>> +stream +Gau0DbAuCH'&Dk(HBY@c94eJ!SXlpYUn!TX72k?,["13(&TUHp+ushh?_>g*F!SpIk:3=KJ/f.!3dnT/JpdJ%pHL6dtbZ#nFobFup;!;O3DinK7gU`Loj=7=GuVfTF5]*SOq`Oka!en5fVe+ujD^D[Sk$_BOGaAOZa_W:4]T5;4pFk'VUDnAJ]2aJ^4-F\(AAO:%;gSRmL!VS"C$e;5YD+[6UFL'Db[aVbABQDC-htGK"jhBs&lWViM+Ik4`dUuPthLS4C\_=N>5Ys;W%O[53Y<;YUYo^>.b,Llb.^oj$L-DNB055lY:hWY?.n&ee`>>r)%Ia#S]VN]YmEu2&iBXHo%A6sGqC^'q/SrmckpG!kQ+R32eHN=2Be0s9C(2,_.]o'HoI84">Nj@3)bUMSW\>BGA+B[37$+(SkFiV-e^VgR$E#VScuPh4!?fTQAYN`AW\4//B0qX5r7:;53-k)gK6'22lnZ:JRmVO+,f+Ms8iE9JS=2(j2=oA.5=HhCh1.Patad:=4Br,9c#:lA!>?p)u93c)`56/nX;[VF(T%L+%fN@KA[#L),j<-c[7/SV7LPH#H0KkWEp\o"Q!4M%aYHV,2oE6rr^JHK9`?CHDa,:Bp;m+/PGZ'?duDd(,Q_LeDt/Noen^UIadFo$-i#A1nWjJ#j"%bl="_W)e+M`!kSS-1gA^F?)VIH/W0I*]W/YRW'Z[L;!4..?crDA=Ws_n%3@qa0#Cnj^gGm>Ke/[".(MX5TI1V=9Y+[=s=A5Qk>araAa$di>?TK*DVDA%]p`PkUQl^5YTp\+Ra$J$*iNrd&X*e8&oJNQ%fRR:c.<`6RCXmO(\-'ZFq[VUALS.[b]C[ct(7LM/:KD[7Ca@B6kN\E_"@;h2;7PK7\j6tTXl%UeAJW$g;C(38KsSlCn,kV^Xs?'A\-Yo.ZUM7XG>LTbIY#.qgYQ>`:WTk!f4B8s;oTm<[XbCRJpeH'pMrP#*)j)8#G&u#_,=L7j&[1P#r;&-jQpcZp6Z&:.%"a$N#+B%,0@JoQ'/4,"',rW69mXAg6?VE8)R3./-'U.>pCT`b^dDF#\D\B_:d0JSnM,XtG3KF7nt`q`4=NkidaU?L_!J&Z9(m];&&\T/QY?71Y^1Q9!E8$uc[%A)OM@p)9j/>&a-`ROk+/O8:*o>]8?2?`=u>XdX=s(o+rthl%=^+T-,#9HsqRj'd.FgCIoDD]8V.&H;eS(Ff%(R)A1-)0bmi%%6M90M>Ms-W')OmX\H_2[#Y@b_2!f#CJ9I@ksXOm*M-hpID_=l,c`8TM*QqIPuCg0:;V/?elt'lfi!(o+Yf5rdEE7j^OgSEe#%cjmf<)D/RDCendstream +endobj +36 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2121 +>> +stream +Gau0Da`?/p&A@B[qC&sdL&eaKVJ(ldP,1]Zq_AtT`/Ke>"O0AYjm2L.4:1Sp'*D?]a>8t9R'tg"GLRRnMlJ`-_:U%5%RV`S6@qlbVoL7@JI8*5oJO:JB?+Wp1"`[Tt,iV$PaXB82_Ba)l-tQ-NBpBj$Dgo$T3a,!fNR"gK0.1WBUQ`l*?8L'To6KlOU/#"$:2N!COG9*@%s#j)>@m"N`;+,#Dh0+L?S=T"djdk4'3$o-'M5]`QQ_pufYOKLXpe;luKd[pVrFWg;4qXSP88,#k%d1'>gLn#Xc7XaBtZ[rJ9LeIH]mXBm;)+-HVM=_Lo7jVB@1mnIf*nKfuffg2oN_h1GA13XK8p0F$U!.4$t<7@5#jO[_e=uEU]*57*1,KYd@lNY:WHZ-V*725NlTub^X1gqTXkG6cWp;uG^us%'ms&&3%jgAR5,kEj0l*"@MjldColJ&[1,n2W5;+"p7]c[T%da!]<=#KSe&EN9j19=9SEmUAXbKJ\!oRa0^m@+.$hXsVEj=@sD2YR*JjcV\14W8@WA0s6'&$`2UdoinQIol;A=4Ts#.aqDJkdHXL0c"QErY.sqIKD6!_AH",)oJ.o&U$=HWVt/&)cXPCIjM]f7^41d4U;jEmKB)jIU]L9nGViC&>,f-X!,O).[&cg/PANgefQi+f_qd,T4HuJi`*WB`F',Tp1GiRP+hTN1?'q;WV*AOl>O>WlS=)g_Ec:XC\7.Pmt,ag5:diZ3TJj26KqhPIlFL]%E\2Yem]V!W/d.A0Bl/W+OF"4'((TZI9O/CFf?P(IMhc$e!&?C$G@c%?#)@RXggNNHnK/TQFW6Qt[r0R#bO"QHq\77qqr_As*'GEQ37n(hSU3FfE`]B2eFc$p(B(+K[/"!lD+;\lK:bq]$;dT,92hD!,E7jqB;Aj)\?gn,PT-Q?unSP`kQ4XA=I^=0Og,15';,S.fU[)U%I\a_:+?^a[t>C-VsUaXi1e5>2]Im=&*#r<0Ok/MR,*mp,.I[:5]I=(=r=VenI/c-:d^`h?*.et?4B>NARs*0]1^l]bjh4X5(e04$_Zn5H/rKd$L1'+A14?lNXX0S-?p3.S(/"=?u>rTj@p8n@dJpM+5;1MU;PdAaN<50cI5)rfqkbRN6XgJi6XDi9fiO1E'g^,t_c:JP+rkPeaNWr>0613NjeYWG4F2+c\a(O!\HEFD%R8;^Q3!"@!/d'\TYb]fMX]h_5?%0)&eNGh?_uA[43)=!THiF-1r3\r~>endstream +endobj +37 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1330 +>> +stream +Gatm:9p;>1'YO$LH>j-6$SKEt[+g0u@Wt@71i.]WdK)uXOXOciKnT9?XcT)]rUG?+D=;q%I+Z='*hKeFhe.3)Yl?$(hS4^0:WVB'$X!\k?J\HkP/]dM?2dmdh+?+uK"YkUD\%:f9:7:i0G_Q$eONbu3g[Jtm7#GW8g5?J)?mA^iA&=cHI9[CMW"fYUL>,P@\?\b`qVl2[6uaE2_bR80aSkQs,i'l&r.X%=T!XSD%;$Q/"jiH5r4RUrRpX/JZN(LCP;bMT5<&F`hfJ[bebKV$bC,Y;J-5q,[\Ds#pn*!aV9ciQM*R548jn']"AlbJO^\+,`0lOq$Cu#QGu@!EN#q4o;-)NTG>qo$beUc&iZ\-%,4sn*7]jl71'&bQpI<5BkYI9(:oZAX@B?fE)c\Pa)=*#JJssJ$21Qt&!f]"j/5?aY]]6b:@eN?<0KsTF(lVCriC4X96#R$3ns$[crdBW"^O\E2Xo.cGV'A&H8gHW>FNC'-uj=qP^eh?_2nLp(]f?OASnVEMg-8-h9WPp;"'#ucl#s!`RWPFBiV/.;0"oj[:iNprR,Z0TiHe5LLBEfYVunZW6okG=fo)2ha\Y4Mn;&@1Fj$?H.u(Z;/C!`/,9^A"\QFeAU1%*XU/SO/qnol)hjW+F!9eYIBPQZ+rARW^_8c:WA(ER(N).@Te1P,7rVbVIN[7f[\g*/X"$"JRA!k&O-Eio-gY2j''"3l=3eIq&@4ZGPtH1Zd:hr9TeJ8Vqb(Gu8W;V'H`%G(,!(jI&dRBOb7)a>C!cU[rlaCSg8?mF8,8%5PB5RR7*_1L6VLb0c.9QF#,0ZJok`):G$KH1M)*>D?WdXf1%^GLq9fB@[j2n)rPCRK6)JGgj!PCO.tS;6Zh[T@JYuGLK,-h,"L-'VF3U)K73_MWYr;_n&IpqnOk"?nJ\Y\\G8p\c%$Vgs)!M\mY1hFrWO5U8oF~>endstream +endobj +38 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2364 +>> +stream +Gau0D9lo&K'#"0DoOY&8W<>l`S%3eX-^uP4nN*,lM_?ajbEsR^)C1N8q=XV'Ua4*C@ni4d:2Ef0dBC/h^MZkGJ04$*QOV?WEZ7IBlu`)X-XfooUPP'W&Rh!$']pTsdhjSlh7bkbVYIuBD^)!I/JKD"%H(/F@nBR\(1A@8%jU%W=>b2HH#^8RHh[@$iu,NnV_;]7;&&$Lj8#V'fu60ho`qU*bT!el:N%iK;J0gE,)nC'"KosV<]hZk`'Ft9r6UmW-VlF'M4ak&W%]kZQR6E$lTdXONXc%B^iqiN28aAYp9CjsU7L"1^$g)+$20Y#JBqD9T'm5*kaLGr;,0@%nH5(34uILjTq_8NkiG$o@eL,H`;Cf#puc?+fZ_o8\NF/Bn6dmE,@OlN=902Lu-B&R5!4cbbr?$[6A+:J;XJ%h&hfILN%[.N"u++3O@ea`3Gl7Sp&D0qOOXcZ%I23]=K(YDt%-mD/A#H(L_)O2*\djbjc1qR7IHPOMO,`:Fii+X'tXLe5Vi?m2u.Ao^:nB2\>-l1@tEDZU,Na3R>TJNTVgDmn2e+\g%-KkM[CGrhd@sU[nsEnN7sn<(Bo`"VYLY+:ESE`6alZ]AW\LQQ*Ur/Ih!Beu87^TX$K3NhjcJdnJ3W9q(\0oLFcicZ?fiNMRh`kQSJRWUsEEF@Z@Z61)cILuT^9U6)I,&Af\D@MEgEa,u<>Jr!.12IGET'#VM']T^'d2V`oHf!B?6)p.%,WC2tH1(eAHoeI,:=Mh>[Vs-eu+/k+EdnVRH?bN=dO#LE,44%-05astc4]UF#5oo1'['eWN;J"Ek!$NihJ;#aM;6A\cK:9,gCCQIE#"R<=\0@;l\f+6'^p6Ek(q$S+pA!R.#pBGCDb09erH(Q5T!7JGJY75P00Sj7d=!ea^/ro_IJ/15?llK`JuPWjRW3==IugMBir:)b'#kdBq6q-K96rK,kkV$K->5Y)Xr\A"DF#7Z(Yh7(m(UqU^cLm:JaY6BBBhR=X6,gXcou\*;\!k9Vg1H['q"Q>>pmHPWi(Mu+&s.RYdRQk)h]WV/<>5Mhl)E(]R[j=GaRTD(!pD(?q5AHpWmj)Dio*sl<-p#m-m,B<4^]K*GkA>4qEIIC@JSc:(M);4f2l6LmEqoP53<*d#X'!&:.;ki*dYjD>dK3YOpK=6]dX(9j3T`D?'J.^r?O:RQPnZ%rn%S(Ei8lfH?TGEIsRAJ7(fKoI0:PQ4J!`,cC+?Z"$7>lXd)u9CsiS*OT2-rW(mH'%au[L+CTial[$\^pVC2CumF-4<%%oo)\<&5NgHBLBh&&_a1.05StO]iJ1H6VWmB*rk;uD*]&%k2QWhRdbZt'mi+1nVI>&RlGHnf[64M9E,=_Smk#>cY,p7GYoYmb(oEZ3*=-&8I<.YiU\raE0-(aAS7ao):Bn2:1^623lZq@XiuhfHenU,Te$Z#2mk,-$SGj5>@['ioq.o+C[%V&&K*.u*Xm_4NIF[6.D[.CIH:`PE0b.)^qHc19DOQ1+"ENeB0p#E$PJ=)G;WH\F&-!,)*T\IRU\%+]Mf9Bb/GSH:VBFQh+4L;uLA&?sFc:9h7a2Hmki!aQA_K4_S=NYhl8LkhT]sb1f$.Ig]hj7:?8N@BQtlN;OEZ4"fs)QCkOe__SMCfB8V]HCd$AOf>R>DX=>0NEeJZ%6h"N2YQ8YF.q5F'G]gWLY0u*cJS3d**O0@+ZG,IgeM%2L)DQ^1$M.bi.Wp%oG>0)a0,Wf[2;GIK3]"CqO,p::m]ADU:@#DGq'K`d(\:1\FTS36'Ck#e=p[+N:Q&j\LrY1.sO_s(jj7q-'I7%oendstream +endobj +39 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1849 +>> +stream +Gau0C9lncS&A@7.o[-L"bIU%>_!ZF^QUZ-n?"gE9&OX_F*4<)'&t2*&k'j!78ao("%ur<1^d2e(^`hiSL?ShK([hN@o>KLkW"_H_]:A0c5S5P8Ju_8">A!\]OCgbA^\b-ncf'H.UU^B#AM6+jc*ZSePJ:>*Ve!:81*R@5k?1e`N]b%X,MF&q_ii-X-e3'elR&W1BHN]S"+ip5[$cOeOo@M3ME8<_9@&kK(%=_@c.LkJ(4I\)Z"NdHj]_>JCoLBU\hLRW/(M$!!?A3!>^\9[4]FYd'hr[B%Yq;a1n-:,]_IEd+d-Jeo]U$k+@N"VUY8OQlFtZ"Zt/B,*9W$N#=O/0^Kl#pCP=XgdQp!NgXlb29DC-RdG1FgiI"q"t'9eF-8\I.M.r#ZE+OhC,n'oi:!U$hZgib6lAg!/PWDHo`4LN"."M6*4=.l'o]FPfr=lSE[Ai]`:#bI16sc7C[i)YqW2#fg+BdH@8&1NK_jZIEnIDVDo/o*2$n-\H'moYi+pc'mCI3aV\n-Ri_>WgtiLn(62e#W*=(:*fq)1/=qCEk8IXIIj*Pel4CQ"Za\@dK+P(g]r9:7[BI&TL-91V2#p7r8A&_@E=';UEok]&\lZ)V+qhgtQQ)o,;t`G_KsFqrXL7`b6`KPj(WsSZ)'0urR23`MQ*pJ3f^Gqd!%UqGgoS`M6T7S*V0U;,.0=o$n3q9K#-J'n6.P9.?e17!%ddrqd2P2f*Q$(s-W6GgM@E&ZTt>bQo5&9o;H]s2j3nM^6A3ZZ`91J,fX.BU7sI_F[6cO_V/MP'@0;DG%kr`b/q.2_P@;`1AR?;cRF?0p>01]'e%dI`Z+d'c/hi!(2=H:QZ+nJVT^fPOWdR^)JSkXISkOG8^YCPsN%UA)TQPQ#H$)/2m/=FI[pgb!GK7$qnJ!8e0fef(Akh:-JG*BP6)r5ZE3?N'r"ngKWK/D55op"M1@7=XXCN,;))4/-0(Pi'Ys2%USe^95UPjR(VOg]AVrXS%)5ktbpaq]6ButoKmI9>3rOYWk`;Bc_h,WV`>8uN1eaU23j?5dERP-Fe0oIJhR=,m+_c<&aeMXY"ajFVk(*M$F4%Idd:EAf1M!LXmGU=C8\B^$,j2h?G':\nbGp&2$6BDC"/H\$/2k*'8DT4T(X>*UA?;f,)ZnZ`0_37UU#m*G0N&ql2)mMD1[\DC@bL3]C8uCII@(qnbimPUWU>iJn;e%SUUf9K8?#faRo/(<2eXJ,c[@Bdqg8:9aGu8Q6IcIhS3t+p=M:.u?ahianendstream +endobj +40 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2285 +>> +stream +Gau0D968iI%)1n+i95=s@5F5<.DrYT_@36K0j0n1AL]\/t0,4/"pk:@>p$/pd(ppB/fZsGKJFI)i>Ate)>+)+?]dnHLS!>SiO]*s4JZ#B*p2-@Ws'hd)OkSXE41Vq$o,WdioAS66F5b@l?^EcG56CaaHX["eai=h80#gn?a^g1g5jGaLt*^Xi#1^*3GTcM[gB[-/3nQs>rQ1f_=9i)"CZh5"N]-m+)sWfp-PN8esVXUoEgn5JlF28NG'l7s!Q:2rseu,5C1!=P+KU7(gns#?q8([?<_Erp8hVUPKq;$_%hdjhM@T(5Xdk\kkl,$!':u14&ts63HuQm+g_#'h("gM?E87;t?Q^@re6P4mL>cO#bM$uY\;jG,g\963jcI/a@E0:6KS0+GbIF6!Kal5mDC]K#'VKN\g,Hq*AWS?X.-gZJoEs(oe.5hlP:":d7>IUs`5Pig)jm9)bT$10MdZ)YX]gjjA]N\dO`3fB7D5!/phr"@V$mH(l[=j`.8iOMCJ8H+TL8#MF*D"GA0[tiS%S1Kp4o,jgVC=a;6CPJo1Wbc:B,ReY5ZclT'tEPEWa;OLkWUoDlR;cpdhuk)##pQ4L.Gc-$k)MG)[6D)K3;&r1]d,S3FUklmf[2Oi66.i'XPc[M1aZM@*"XbTsVQ.%Q%?na8eak%=Y?q7LQp($?Sk^(FMllWXbjpBUS'>W+#Ig'9!Q7U*d9YXfJ2J;Y"HBSk-BKOaYg_/:)ED[WMe_"Ve.$Be>Je>,9sQsdJ]ZMo`.SJOfmTfMN3f^'o^J09Vp2@kdbD1V]\6o.L!m]VV$L=C-YO13jQ'>@7MO*k%'s;;YBUgb]nl593V(#Vht!d@W,6Vg8WbVI9j1$oK\HDp9`\uUcj@#\JsK8G40'+=&WkCXBLWY/N8$0QE.<8U&h8*6P]0VL1i]G%I/ainWg^a:O3*P/JH8uB"3gs7/%9iSZWJ^sC_NOtl-8-:)^&`!rr7?j])r7Z7oM-*A^kQa%;mai>4%#S'mC1-R]E+UT;EUZlXjl.7onW-Q>/P/FI305$'j^im)3^PL;LuYnCbY2!7Ra1]Ig2sfm9[K,D`,Ku>EPE0+V)Dj+"0Ma\L\rtMG#']:MhR^ksD3Jdbu&[2U0nc"<*jpMF`9.bJNk)W@d7=X3+lhF$,q5du!)%Q>_$I$8_&39RQkn945Ul.XlX?$nSJ5T1>U_LMI"E">T.gZCOY8D<$du^'tWTthmm6+[7(kp)qd7_9mlX,dqana:S3bHendstream +endobj +41 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2319 +>> +stream +Gb!;dbAu>q']&X:m\Vd8CU4/ZVft`YVs2inA#o%;)jl4fB)%HRde37uHN93AV-$@=;;CDLClnjVHZ1(i*f,r:&$u;"SET@)IQhD_3)/SS4NF5]"\kjaO%0\$=WB,9=QG@,PJ^o%JFqj"^rm<'Npd9?IdB-YoE4*8,J:h0$q9b0O0C#rQu,Q:MN!8A#s&05auV)&hukcq)+@diG(jX*a&a(Pl0/'*p#V"L'=o(;$,LV$3VdMQ\p-TkcSYU*>KH/=p[TMgS2g/Xc/luWdc@F,9:AhfQM$g!26;1R.go)3hlYj.D0`fL5A?%\0l:`hLUd,\eb4dt46`nl3LflB9fRG?_p7rg_`ge<1/rHN>Z_;rq_%>6BEjP4b,(aJkjF+-:;_)Zt2!$Un64267%,sY43&3E`'8%?\)B%QlF+GT2<2i_.([_K;"i3Ci'(U?U3P-^M4@.PE_/ij;gJCGe`g"hk?(HZ"d4m/W>+go??D08k4g)1@**r?c"b<\/-e.622S>pP"@!C=ZJ==3n%@==6d&q[>%\&]Mc+t2-05`kmU@(HHE!a`QW5:/o)SuFZ+1YVo?U(WQ5Q4X^C%7?o&T_[3#Pe(,`SajEn/4EBiY\rs\Pn.h.ij2YWc#CL.f(2^4$I`JXi\7nOq*ue[DD69?P#nKY-Y9YI]$>h)WU*6to4V@.+s"B\P]Y-WkDInQ1,.=2)mnQMBKMEM4!\C_!P.+Eu[WCuc&KIr0\M/YAA[771EtVOZ-F6ShB,&AMe=%?=b99KV:/tJ4P2.Nq%K.&X*!q8S_%VtsFp?#1p7fGJ*\C4?Z=PP*c8&>c"_B0uf;_:KT2J8G1mb2p_&j2nY"$Ur?@^fm*5Hc+`6'b\=D'(H'M*es+FGpa'&"pbh:2#&T<]9*lQ-PnPqJ!+1!H`JeoS,FlRoDmDjWF?DJe&D>eIfO@23$h0QcJl+cAabnDCpq=@$Pi!Th"\L?V&sZDl%&%+T#/2V(so1X^>D8QkFV7/qe&F7K6E9\*&f(D1_]eEO7k(L#*_M9]3=kcK!^=ANU5Ag[Ek[)i`I4sBJQ3SPKuI_a*OX(q8lPrtGRDW8rH6Zu9sX)+lQ7qf"$&07uQcF$5k>2GsXh.N9gnhea*VPGim@W=l*6l.KlW2,-AT3r+#+a];IXQc&-mZIL+^X#_bi*RSN15aI2,7q-ET"YeDNdsC$C`Gi4KCsV.DN,"06_:bAK\P#gG;k8d],H:R)iW^X*roj_#&r0)GT-q$2#t@!T6i):\?X-raoIoAh&cCC6@?J/NGZ0EO57Xj_3>`Og?sVE@0@hck9AP-N#5N25N/)b&SKo]%`d_"&_N\9e0;pH9SDE?(In>7#buB#=5$:(FVB4:8HlLE?VM\hL,>XnV3"A5f24>Js$@mJ#Acqp.ZkU)EG7I2B&o%:q:HoJW88RTNj!p-F;gduqpG0q4JB7E%sMqC_^;52C*8q!X_Vjm5GZ:oE)r4?>'FZNic?HQ,OrL)pn%T>/ESuh5,,]b\3dQ[POAK]jef)*h'ak-WEa@k2m6OPZA7=$\&Tm"1+e'MC(B6N4pK4&#oT3KlF.o1QIeD[TI^GH+nMB%m4U>(81lBBHd*BR:':3Gpe%"E'&GUSWBLli&:`,uU*-HX)I4Ja14?U!li6I\q>H,ajM5[FK(4RZ1UemBpV5=94%.Ln,Ap]C!u(PhibIH_NS'6Ze;2=]:)*N9,pJ9EnS_<]/#goBREpi3J5#YsP2O+,LZ5r0_eWi[42'+C9%m5,GL;'b4LV?u@mb$J+::%S]c[ZogtWL76#MR>WXsP."]l4@cG4>-[7a#>!usC%.f~>endstream +endobj +42 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1722 +>> +stream +Gb!;c9lo&I&A@C2m*SB=Q2n[J;f*i@:7OA%g640D$-C8M"N(bJdpLu;O:k.*eg),-AidA3JkKP0/p?tP132p<9K5rP"u_hCrE/2HsY)j4!-2XZ).k$\u8D#oP`SG.iQm_\7*DlJ82QQXf/LM=Z^Z0>c0EQG+0Saf\Y`0'>L2?>\6cr-XPUX`c"GbC*&]Pa4MSRr`(gs@d>=F\X[RU&"`1)79t-W_FJ6m[b%-niO\^?b8J&n87ns]nOW3;0KF>67M*!'%o,X/$;(s6tWF@Eq(IFL3%#0urZIF-d.1eR+:_`pHfQ(r%pff^?Ae+E74GRG&ZJPPudg$T8rehAs$Ua&$8_dXG!+C&QGt\?^X`&Y:6oHPu;jk[%JKZd5(om28=nQFsh2YlN&at-B!_:>?FI928E_uWXC1R*/VUO_N#5AN?$,gnLDmeH`OtK-Tb=pD2BiS/s)oo_t-Z#^T\lIKP%`8:[;g$>J@Fc0'R-pk3!%pTR+m@F!rD\8iHY"&+5UF"`AkTV18dp]anKEp/.X)qc:h@.9d&,G"fo<;'7OE+C2`TS#lG87TD@Z`q[\a?9SVU4bch^>2W+km"NcB8%G%,JqOJ.$c@\YW82GnqaDZRl+WjZM!.GE&57O3[?Z5MN&(.R@N1PQJ8>uRllj^4S!A=*DlV6Wqqf2h3M7Rdmd$a(!;_n&(t*#MnafhigDp,e4b2;#$)6eBa;ob'5WBMK_F9+dO;/c.h[%Q`BDu8,F,:[&CmMT/F5]8WZ?^q:5$!^Vd8-BUS@%<]&`uqX`2DYUdC3@GrH!5o3+tkr4WD:W"`E8MRldKmT8Y>)kfllg[]tl0qu3"1>AMWVOCM,k&nZsFY@Hg[$J-cNZS0+>AG^rqY+5O$rg6`""n-?BA'.@oSf:qHKtcPf'Pl>UfFtC]>0T4[>@KiG7\-P8Y,MVdcLM&:S-_:M*0bb%'3SQd359X)'=60d-U/G.MiU.KuHV:>\NXLRIu!B~>endstream +endobj +43 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2151 +>> +stream +Gatm=>>s9;&:Vs/fZ1?4I!"oO0l;L+)-SGam[Y:bD:ccK&iOYU3%4c6n'V@AhfoDX[s!:?BoU)O/N7&<"eTe8*+9Gso]fo"R%IYjJ""91;n/5K3G8-Vl1FRVIdmUX5@F#i*luR`nF/R0Xh_9ViU@q2=Muu:24S9(.e4m0kZ3BUQAg+Y?7-]"Q:\#CeFR5TNpF:sXMD5,io4;U+(u<$dKP9X(86cOeA8ps;ltU!3'I#9OBRD_fS'gu]NR74QZC0W06nRV>Si0%335!1]\(Fak<(9"Dt^4+IN@Q`QjU"0qdEE0sEQb,IUY;nAfCl+Z?F$PKH7ct2l44NFpY(TA.h:\1[NPsC`qK8gHFNg6$(+p`FCIJN^0r-E7JEXAAP?,:R(OXN/d2p4,^0`0h]^-t(B529S=IjY[8bGfRM@HqiqfB7:ZA$Fe)o)6AE7KbXTa!YbSHqFI4f^SD!_s&qgRP1POS1jQZHn"o%eBIk$VO%jk3ml\]_ueN$SG>W)=NaAb[fYIR6n4jc+]kq%:rffV%s")>6,H6I5L!+R9s:tMB!=pn:=UGKXBW,pdM&dI<,P,/^Bt9ORE?@qMb'a:/eM)i[.ubK.b.@+"Yu;$@$$c-if?Q&^d%mT(r9e7o3&oe((]397'5@_,:.PMi%A_*4KDM]*FLt:@\$mB+)"X,LH)t2%W@[$dqbO8rK1s&De.)E.n6C2Yj\X/a:pj#IUqfSk9Eko\5T2lGHBU4RM>P4nSK0;^;tNR[@=!raTsgMTKCAuiu(=g;;Gf@MX0JdBd^e>O-sG.b]o+gf0\.7]QSa)YCb'm_#DD474t#k^V[=0@Mu\SXd/'neY6h"&'^.TF-NPB(O=8qW>4pU9C'f)_*gg`h[JE+MH02BlFNirLV.2Z)dnc?YQ2tj\9Q*a*-uL(YHQWdDOg1_dF+WG6EO$,.6:Rnp-s)PfWal=-=:RUD^@/a;!/`hNJ)UajL7V:GEL,B%0$4>R"^))PMMCfQ,P(a_DP^6j1P`;WKe9/f0>*Dpj`?!>?YarYR'E_S#@V52D;-Oa>4qHA-CR+:/U/iaA8<)f_FsQ-+K_Ddf4DPih*"9&2/XbfnVV*`@n?f0cbU7\s'1!Xa_BuU+N:`iPp3Z7_6comZFt.o:`-BHIk#-YC_gopC[?%443akfq9"L&%C=t@km@6D>6t=H"HsNh[C\;Ue8(DnsVoX*g&d1C(b7/ZQ:C_./:"-Dj@_1035qYb=B'%^\TY2[n?G>Gr4(;U8<(c-cQcJfBG(AYq)23TuK/s()\N5Usq3k\@OK-/0:A?I[.CG'Ri_,caPo'a1gpQ4==?!"lT6)@bG_P?L5YeD3en)X4[-#[P&"ZS`>=@)?McEd6JnXif#iHbeQX)]aCmgEeS6\U!VEga6%PNO`m_p+PM<9`>:3i[kr~>endstream +endobj +44 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2180 +>> +stream +GauHL968iG&AJ$Cm&b"M7L4P&j/=e0`gO>[CpbWr+;0jbOG![uEi6[d$]6UjZ>@(?N"V&q!nbK[\)/f/_8?e#r['Y@LDdc)7"I+\/V3c:LY5b=R%=18,LiK.5_7;'j"UYQ@eTuWeQIH],Kbl43=:`.R)Ka``_!MS/;M?lfBI,Jl2(*g$6LqZDE?sNiPX3b+1(4`?X`Xeb^O=dE[Q*\HQG[&%fL?jqi<)'F&c0D0$;>ZCIY5mY_&d[1`kt0tciso=^097U64a/._l_\69Jk\Ln*m^'3d3ZiQuKlH9P5eJDKVB>Blh!5H\*[2KnN;+jVtQ>-DH=E2QH>5kkMD6oWFAOJ`WHE-n.-tfgjH+>1H>lBa'."Peh#lKB0,Fe@msdWMH2?.eXJu2nJQAeI]F=WLkm;U/aeED$#/JtXjS_*l'4Z:$QH\C%G76F_ubl^*RZR#4ANHTR`X(YW-+r:g<*1b,d4OHT7IA8;ONsd,aPme%22ZJEN?L$+19=AeB/8pR.Gj+2(k)l::)H\7c:V[\@.g.i0Pd>^OSB^e2^&NPD^=uK![c&Ub6FN*OXV6K48&&lWcb&(Dpd5eJ^A?-,G$2Xg)QDHK`tLKgjp#c44(iKdo_%M9Oe-B:Tk?o>N.dH\jpifcb9AaPFsAY%6#YoY6X`!o!jK^C%S(4>O1uL8Q*-VpT]Bs$u=Y7\/NnWM'^3ohJ1dFBO&r4p)uqJjjpPJablZ6V-A!9&ZtE>a<,QkF:lB[1^;#;sbjM=NO$:q;j@Y$R5&>ZZK\hi/kfV)O)a61E>,hX.^9(Mg9q_T3s6nRCU]0tFoO?l^ih,17+gH(e7cdE[]1aprubMBi)k$lDtn1L8d#U;!JqL+:XG$^lC_n!ASrK-QT?J+DK2oY$:bh"a]A]0EZ]BuKJ.jX"FLXWi]?/^E5gfkE:p6Y#rPrUnR2>^fA:!LKBRIa*N,R.l!CHFF$FX>4tTbtg7N=o$]lLdk^i10q)*u);1WKMV(USaDVDpDJ#4;b`d[?C\=AC==SfE,YJk94&?3^H*E/f$q1<#7(*WWI3H@)n(J]>6AB#"ZC,mAOp!$=uqg/$F&(9r=^mFFN44kGQ_g8R/tUaW]Hj^J7b<2\>co[PT6LgFh,W^ZR^>g>\:-[G2lX.'4,tihYYis-k4&iYs8.b.Oj>KQ,4\)s`'E=a:MrCh&K`(j2dQ7KHH-KN9=4]S=srf_IeH5E7oQ#4aLs#AMo6`YYJWgR2s`sqg(E;J'=h1M,EWr=+r]p+E7g!M+%pR['u]W#4e2?(o3a]rhA4O7g^aTL:>b'Xann@Os42SUlp(=N\'G~>endstream +endobj +45 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1927 +>> +stream +Gatm<9lo&I&A@sBm*X(f/AZ8uVDrV9*P4p[V7l'oOcYNh,T)&R1AUTk!3%V!:6gdX@nS"3i4XR9G#r;:j+k1[78o74Ir[hhT=cPep9Z-\E(0Vg[h]c`:J$8O8nOpEQ@X^7Q,;uC%24!L?ASre<@GQ=nP&PJX=8KQrOZ68ep%eH.7hq^ugPH3Bmm=a=il$foD_Z4UmVgm#?2o!c<,L'ST)d*@uD]e,0Q`!],\G[@HoX/?n78qZLhCZU3X_"2>&HjOgWb*PMd1haFHY7VLS7G2^?CjQaTAVTG)n(T[^C*LJGs7/8GROdC`E8!&&fXIH1YDtR`1M@\A+5-?YeWol(iLu)ItT_<0FAOLcQ>CWILm%Y4r?_ol%Epg!.^^.e38kOUgY1[;,/'P40`gm%_e9X$Ra#_a$Q(4_K!WUU4(iZkrfYno3`2i4OQ4\Z37.,e3D6b]i0?"Z<;!pDnuTPl#WXO:%.7hPPHD5m-V2RQH>WL,4nh3L,5:Hjk]S.o%GG9li>c6_*sdTn%`N,)/K44gS",s;>Co_!6TM(8*+NDP2+6d4Mur3mU+`k;;L",LIurL4Gt2)hiXPYUp]uC.5Z?[/GFbBX-.V41[Z.nt3.OnDl\A8XCLqp:VA0Tk;q-:l&QW@d6X!I_%b_jm9A82;ibO[h(LT"5n_;)&("'EZKFuF`-d)nVT=gM(4QWA!;(/lfS^/JX"2D:>R[eM'3[^)rX8T6)NfJ+c6`33CIC2$LH"[,/GJ=_"^1i[&[gU-'XY)gp\Nr>/U#5abEESf0A2]RC'"Xau,Y"#a?!IWcd5P5bmeoG7[WJt#ig`@kh6/9%eMjeH8V?St:Y7i5"Y"L@hVcj7$@D-:3]>dU:<-F.f1-.fZBGTMC<.@#VqQ9'lh=1S`\@![f]U[jgZOY_[_6W!@sNmqcTV"2\)7GR_r81QM:etVo)S$@.%r.1nNoRK99`)S&os(.Mc*eV_96IkEjmi0rcR7'>;[RHceW[9q;FS[KO/($S8:7?TBfcn?4.lub0:>GL[m(F1u.Zh-u];WJ%lcciP>)DaW=!-I+\i"ZI"u.=c+A#'H2aBW]<]$(r.MLh@'c.W0Mcm1p>KO_AX!W#;2[.rqjJm1ML'\o84>nWRTtI5o(`Nf^_(A"aV#Pa+t=<0/H=OM]*r*o:U23;Imm?M%^pY1$"!Elj50H;G'nK0"`_Eb[d^JlhLnKL=^?@sI2Z9H;V=E)t#/a,g;1@(s"r+m2endstream +endobj +46 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2037 +>> +stream +Gatm;9lJcG&A@7.b[l?AO`EmJPc2:+D-F;;Rr%EX!fL;sRmpeBLcO?Ih5O9'/.58hj2CJqRaH/0oC@RQWZ^;>agG3^Spg-WI/a%_u",pnA*r<2/BJ7F%'MED*32(SgNajlQBK,C4?)+%Fk-0!KSIrbaj0pmoEsB`)Bp'I1-FHsUMfQ*NM1s!#snf(rsNo7#A\j`Ra$gsptYKdc22n2!#2^36A63/^e:cp^^2.V+q^pai].gRMgOAE8nb>a9%p##`EcM&2&:9-gjr-$/RjA1C%;KCj-!8u8bJrdV:\J"=bF&80\9Bor_0=oI.]Edh^a+5d.YF3AM>`YR"8'M;E5okk:@Q0=;arN/.JF2i:(O53CJngYZ0E7/)9de:XH$uqM(1L5N7Fo,[?*LrQqJh?gGZKRfL8=sqAki:t>f6NZh7\3ji^3auo=78ZZ?66ZII7L)Fg"m<8T<[S(ajpQk#*_c2Npto?)Gk<0deM8;LO8E$`M%^OdtNi$hQ1`^i*gR4`QBj>3?u\"q+?d%?Go7:VWq(:r)De?1Cr[KG`9n2Ie!;aY\@"1C3SlV-BVC:WW6ePL1e6%Gr2=9k^s8SobBCYQ.eK7^ihYDOeVm;PF\-.5?NPe.C6-biCBgj*(l8Rl8_9<&r?fE7rSR>O8s^(`VWgO*s@C9?M7*%RE7cpFfZe]C3H2tYE0ngD0q^i,6A'or\%+]+nr+E=5hcK/(<@!$VT;gd8S6'_rJ97MIpm%ehE1(<^>f;^+J)tN&oX/SdS>GWK)&]TlLcjbNX"@!EIdX2LRdKf8R@/ta!.#1sXe^EeE&b,R=AO3,)9aL.)G&rP03P9B#'l^S/H-XlaXmKjZ%5`._pc;PMloH>.[-Xp0=ga%'C8d1`F=r):^:/1MEifB!VEFteHZT'.%O]_./&4ns4ep"o1-\l6Kg7\SWq"S[Tg>2g\P##J.XO%*%EWo>??$BZ)7-@[M$#keeE7On(EfXr7kpeaG@tpcleqYj2R/C0Xtm!%f7$&jA^2ZhFr#B%lX_d(+Te8je\;7g\'l/FI4+_3:^`#EKG1/)#3QR-X&-lKl^2pn8S&/iM(^Ve4IL0q&9\kMd#";Z+q(`U9g,S'H9_@]ebs_DafND@DVO)^5?'&rRm1"R_fR-4Y2OjkoJn4,_!Z=Bi7.8r8jOqS\Vs.lL+PG6Y`/^U4SHR"I_t@"bH#`8hbl5CF):,(I7gCSCR6VtD4uFrXQ[<>W>PgBVgQU"(BRSZKtYogpVMRua&?ghpj^_hE+LkF&$]!&IPX0Ri#9#Jd!=`?e\^qooQ;O1O.%/o;S_E+U,%1qkk^dQ3]]-:\S37MIj5Gu`]_HjJNQ`39:B`jKou(#endstream +endobj +xref +0 47 +0000000000 65535 f +0000000061 00000 n +0000000123 00000 n +0000000230 00000 n +0000000342 00000 n +0000000547 00000 n +0000000752 00000 n +0000000871 00000 n +0000001076 00000 n +0000001281 00000 n +0000001486 00000 n +0000001692 00000 n +0000001898 00000 n +0000002104 00000 n +0000002310 00000 n +0000002516 00000 n +0000002722 00000 n +0000002928 00000 n +0000003134 00000 n +0000003340 00000 n +0000003546 00000 n +0000003752 00000 n +0000003958 00000 n +0000004164 00000 n +0000004280 00000 n +0000004486 00000 n +0000004556 00000 n +0000004837 00000 n +0000005023 00000 n +0000006282 00000 n +0000007680 00000 n +0000010258 00000 n +0000012431 00000 n +0000014575 00000 n +0000016764 00000 n +0000018722 00000 n +0000021079 00000 n +0000023292 00000 n +0000024714 00000 n +0000027170 00000 n +0000029111 00000 n +0000031488 00000 n +0000033899 00000 n +0000035713 00000 n +0000037956 00000 n +0000040228 00000 n +0000042247 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 26 0 R +/Root 25 0 R +/Size 47 +>> +startxref +44376 +%%EOF diff --git a/USER_ARCHETYPES.html b/USER_ARCHETYPES.html new file mode 100644 index 0000000..1b17849 --- /dev/null +++ b/USER_ARCHETYPES.html @@ -0,0 +1,566 @@ + + + + + + AgenciaPsi — Arquétipos de Usuário + + + + + + +
+ + + + + +
+
+
🛡️
+
+
SaaS Admin
+
profiles.role = 'saas_admin'
+
+ Fase 1 +
+ +
+
Usuário (saas@agenciapsi.com.br)
+
  └── profiles.role 'saas_admin'
+
      // sem memberships de tenant
+
      // acessa /saas/*
+
+ +
+ role: saas_admin + sem tenant +
+ +

Acesso total à plataforma. Gerencia planos, features, assinaturas e usuários. Nunca entra em contexto de tenant.

+
+ + +
+
+
✍️
+
+
Editor de Conteúdo
+
profiles.platform_roles[] = 'editor'
+
+ Fase 1 +
+ +
+
Usuário (editor@agenciapsi.com.br)
+
  └── profiles.platform_roles ['editor']
+
      // sem memberships de tenant
+
      // acessa /editor/*
+
+ +
+ platform_roles: editor + sem tenant +
+ +

Papel de plataforma (não de tenant). Gerencia conteúdo público, landing pages, textos. Verificado via platform_roles[].

+
+ + + + + +
+
+
🏥
+
+
Admin da Clínica
+
tenant.kind = 'clinic'
+
+ Fase 1 +
+ +
+
Usuário (admin@clinicax.com.br)
+
  ├── profiles.role 'tenant_member'
+
  └── Membership: Clínica X
+
      ├── tenant.kind 'clinic'
+
      ├── role 'clinic_admin'
+
      └── plano clinic_free | clinic_pro
+
          // acessa /admin/*
+
+ +
+ role: clinic_admin + tenant: clinic + clinic_free + clinic_pro +
+ +

Dono ou gestor de uma clínica. Gerencia profissionais, pacientes, agenda e módulos da clínica.

+
+ + +
+
+
🧑‍⚕️
+
+
Terapeuta da Clínica
+
tenant.kind = 'clinic' / role = 'therapist'
+
+ Fase 1 +
+ +
+
Usuário (terapeuta@clinicax.com.br)
+
  ├── profiles.role 'tenant_member'
+
  └── Membership: Clínica X
+
      ├── tenant.kind 'clinic'
+
      ├── role 'therapist'
+
      └── entitlements via plano da clínica
+
          // acessa /therapist/*
+
+ +
+ role: therapist + tenant: clinic + entitlements da clínica +
+ +

Terapeuta vinculado a uma clínica. Seus entitlements vêm do plano do tenant (clínica), não de assinatura pessoal.

+
+ + + + + +
+
+
🧑‍💼
+
+
Terapeuta Solo
+
tenant.kind = 'saas'
+
+ Fase 1 +
+ +
+
Usuário (terapeuta@gmail.com)
+
  ├── profiles.role 'tenant_member'
+
  └── Membership: Tenant Pessoal
+
      ├── tenant.kind 'saas'
+
      ├── role 'therapist'
+
      └── plano therapist_free | therapist_pro
+
          // entitlements via v_user_entitlements
+
          // acessa /therapist/*
+
+ +
+ role: therapist + tenant: saas (pessoal) + therapist_free + therapist_pro +
+ +

Terapeuta autônomo sem clínica. Assina diretamente. Entitlements vêm de v_user_entitlements (assinatura pessoal).

+
+ + +
+
+
🔀
+
+
Terapeuta Solo + Clínica
+
2 memberships / contexto switcher
+
+ Fase 1 +
+ +
+
Usuário (terapeuta@gmail.com)
+
  ├── Membership A: Tenant Pessoal
+
      ├── tenant.kind 'saas'
+
      ├── role 'therapist'
+
      └── plano therapist_pro
+
  └── Membership B: Clínica X
+
      ├── tenant.kind 'clinic'
+
      └── role 'therapist'
+
          // switcher de contexto no topbar
+
+ +
+ role: therapist + 2 tenants + therapist_pro + switcher de contexto +
+ +

Atua em dois contextos. No tenant pessoal usa PRO. Na clínica usa os entitlements da clínica. Precisa de switcher de tenant no topbar.

+
+ + + + + +
+
+
🎓
+
+
Supervisor Solo
+
tenant.kind = 'supervisor'
+
+ Fase 1 +
+ +
+
Usuário (supervisor@gmail.com)
+
  ├── profiles.role 'tenant_member'
+
  └── Membership: Tenant Supervisão (novo)
+
      ├── tenant.kind 'supervisor'
+
      ├── role 'supervisor'
+
      ├── plano supervisor_free | supervisor_pro
+
      └── max_supervisees 3 | 20
+
          // acessa /supervisor/*
+
+ +
+ role: supervisor + tenant: supervisor + supervisor_free + supervisor_pro +
+ +

Novo. Supervisor independente. Tem sua própria sala de supervisão. O plano define o limite de terapeutas supervisionados (plans.max_supervisees).

+
+ + +
+
+
🔀🎓
+
+
Terapeuta + Supervisor
+
2 tenants / 2 papéis
+
+ Fase 1 +
+ +
+
Usuário (terapeuta@gmail.com)
+
  ├── Membership A: Tenant Pessoal
+
      ├── tenant.kind 'saas'
+
      ├── role 'therapist'
+
      └── plano therapist_pro
+
  └── Membership B: Tenant Supervisão (novo)
+
      ├── tenant.kind 'supervisor'
+
      ├── role 'supervisor'
+
      └── plano supervisor_pro
+
          // switcher: "Meu consultório" / "Minha supervisão"
+
+ +
+ therapist + supervisor + 2 tenants + therapist_pro + supervisor_pro +
+ +

O caso mais comum. Atua como terapeuta no tenant pessoal e como supervisor no tenant de supervisão. Switcher de contexto no topbar.

+
+ + +
+
+
🏥🎓
+
+
Terapeuta (Clínica) + Supervisor
+
3 tenants possíveis
+
+ Fase 1 +
+ +
+
Usuário (terapeuta@clinicax.com.br)
+
  ├── Membership A: Clínica X
+
      ├── tenant.kind 'clinic'
+
      └── role 'therapist'
+
  └── Membership B: Tenant Supervisão (novo)
+
      ├── tenant.kind 'supervisor'
+
      ├── role 'supervisor'
+
      └── plano supervisor_free | supervisor_pro
+
          // supervisão é INDEPENDENTE da clínica
+
          // colegas da clínica podem ser supervisionados
+
          // via convite no tenant de supervisão
+
+ +
+ therapist (clínica) + supervisor (independente) + supervisor_pro +
+ +

Trabalha na clínica como terapeuta e supervisiona outros terapeutas (inclusive colegas da clínica) de forma independente. A clínica não interfere na supervisão.

+
+ + + + + +
+
+
🏥🤝🎓
+
+
Clínica com Supervisor Contratado
+
repasse financeiro AgenciaPsi
+
+ Fase 2 +
+ +
+
Clínica X
+
  ├── Ativa módulo supervisao (feature)
+
  ├── Associa supervisor externo via convite
+
  ├── Sessões registradas na plataforma
+
  └── Pagamento via AgenciaPsi
+
 
+
Fluxo financeiro
+
  └── Clínica paga R$ 200/sessão
+
      ├── Supervisor recebe R$ 180
+
      └── AgenciaPsi retém R$ 20 (10%)
+
+ +
+ split de pagamento + Stripe Connect / Iugu + Fase 2 +
+ +

Futuro. A clínica contrata supervisão via plataforma. AgenciaPsi faz o split automático. Requer gateway com marketplace split (Stripe Connect, Iugu, Pagar.me).

+
+ + + + + +
+
+
👤
+
+
Paciente
+
profiles.role = 'portal_user'
+
+ Fase 1 +
+ +
+
Usuário (paciente@gmail.com)
+
  ├── profiles.role 'portal_user'
+
  └── // sem memberships de tenant
+
      // acessa /portal/*
+
      // identidade global, não tenant
+
+ +
+ role: portal_user + sem tenant +
+ +

Acessa apenas o portal do paciente. Vê suas sessões, agenda e documentos. Nunca entra na área de tenant-app.

+
+ +
+ + +
+

Legenda

+
+
Novo — Fase 1 (supervisor)
+
Existente — Fase 1
+
Planejado — Fase 2
+
Plataforma (sem tenant)
+
profiles.role → identidade global
+
memberships.role → contexto de tenant
+
+
+ + +
+

Resumo Técnico — Como o Guard decide o menu

+
+
profiles.role = identidade global (saas_admin | tenant_member | portal_user)
+
memberships.role = papel dentro do tenant (clinic_admin | therapist | supervisor | editor)
+
tenant.kind = tipo do tenant (clinic | saas | supervisor) → define qual menu e contexto
+
plans.target = para quem é o plano (clinic | therapist | supervisor)
+
plans.max_supervisees = limite de supervisionados (novo — Fase 1)
+
+ Entitlements: v_tenant_entitlements (plano do tenant) UNION v_user_entitlements (assinatura pessoal) +
+
+
+ + + diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..1b99328 --- /dev/null +++ b/schema.sql @@ -0,0 +1,9444 @@ + + + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + + +CREATE EXTENSION IF NOT EXISTS "pg_net" WITH SCHEMA "extensions"; + + + + + + +COMMENT ON SCHEMA "public" IS 'standard public schema'; + + + +CREATE EXTENSION IF NOT EXISTS "btree_gist" WITH SCHEMA "public"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "citext" WITH SCHEMA "public"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "pg_trgm" WITH SCHEMA "public"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions"; + + + + + + +CREATE TYPE "public"."commitment_log_source" AS ENUM ( + 'manual', + 'auto' +); + + +ALTER TYPE "public"."commitment_log_source" OWNER TO "supabase_admin"; + + +CREATE TYPE "public"."determined_field_type" AS ENUM ( + 'text', + 'textarea', + 'number', + 'date', + 'select', + 'boolean' +); + + +ALTER TYPE "public"."determined_field_type" OWNER TO "supabase_admin"; + + +CREATE TYPE "public"."status_evento_agenda" AS ENUM ( + 'agendado', + 'realizado', + 'faltou', + 'cancelado' +); + + +ALTER TYPE "public"."status_evento_agenda" OWNER TO "supabase_admin"; + + +CREATE TYPE "public"."status_excecao_agenda" AS ENUM ( + 'pendente', + 'ativo', + 'arquivado' +); + + +ALTER TYPE "public"."status_excecao_agenda" OWNER TO "supabase_admin"; + + +CREATE TYPE "public"."tipo_evento_agenda" AS ENUM ( + 'sessao', + 'bloqueio' +); + + +ALTER TYPE "public"."tipo_evento_agenda" OWNER TO "supabase_admin"; + + +CREATE TYPE "public"."tipo_excecao_agenda" AS ENUM ( + 'bloqueio', + 'horario_extra' +); + + +ALTER TYPE "public"."tipo_excecao_agenda" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."__rls_ping"() RETURNS "text" + LANGUAGE "sql" STABLE + AS $$ + select 'ok'::text; +$$; + + +ALTER FUNCTION "public"."__rls_ping"() OWNER TO "supabase_admin"; + +SET default_tablespace = ''; + +SET default_table_access_method = "heap"; + + +CREATE TABLE IF NOT EXISTS "public"."subscriptions" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "user_id" "uuid", + "plan_id" "uuid" NOT NULL, + "status" "text" DEFAULT 'active'::"text" NOT NULL, + "current_period_start" timestamp with time zone, + "current_period_end" timestamp with time zone, + "cancel_at_period_end" boolean DEFAULT false NOT NULL, + "provider" "text" DEFAULT 'manual'::"text" NOT NULL, + "provider_customer_id" "text", + "provider_subscription_id" "text", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "tenant_id" "uuid", + "plan_key" "text", + "interval" "text", + "source" "text" DEFAULT 'manual'::"text" NOT NULL, + "started_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "canceled_at" timestamp with time zone, + "activated_at" timestamp with time zone, + "past_due_since" timestamp with time zone, + "suspended_at" timestamp with time zone, + "suspended_reason" "text", + "cancelled_at" timestamp with time zone, + "cancel_reason" "text", + "expired_at" timestamp with time zone, + CONSTRAINT "subscriptions_interval_check" CHECK (("interval" = ANY (ARRAY['month'::"text", 'year'::"text"]))), + CONSTRAINT "subscriptions_owner_xor" CHECK (((("tenant_id" IS NOT NULL) AND ("user_id" IS NULL)) OR (("tenant_id" IS NULL) AND ("user_id" IS NOT NULL)))), + CONSTRAINT "subscriptions_status_check" CHECK (("status" = ANY (ARRAY['pending'::"text", 'active'::"text", 'past_due'::"text", 'suspended'::"text", 'cancelled'::"text", 'expired'::"text"]))) +); + + +ALTER TABLE "public"."subscriptions" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."activate_subscription_from_intent"("p_intent_id" "uuid") RETURNS "public"."subscriptions" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +declare + v_intent record; + v_sub public.subscriptions; + v_days int; + v_user_id uuid; + v_plan_id uuid; + v_target text; +begin + -- lê pela VIEW unificada (agora ela tem plan_target e plan_id) + select * + into v_intent + from public.subscription_intents + where id = p_intent_id; + + if not found then + raise exception 'Intent não encontrada'; + end if; + + if v_intent.status <> 'paid' then + raise exception 'Intent precisa estar paid para ativar assinatura'; + end if; + + -- resolve target e plan_id via plans.key + select p.id, p.target + into v_plan_id, v_target + from public.plans p + where p.key = v_intent.plan_key + limit 1; + + if v_plan_id is null then + raise exception 'Plano não encontrado em plans.key = %', v_intent.plan_key; + end if; + + v_target := lower(coalesce(v_target, '')); + + if v_target not in ('clinic','therapist') then + raise exception 'Target inválido em plans.target: %', v_target; + end if; + + -- regra por target + if v_target = 'clinic' then + if v_intent.tenant_id is null then + raise exception 'Intent sem tenant_id'; + end if; + else + -- therapist + if v_intent.tenant_id is not null then + raise exception 'Intent therapist não deve ter tenant_id'; + end if; + end if; + + v_days := case when v_intent.interval = 'year' then 365 else 30 end; + + -- define user_id (compat com schema legado) + v_user_id := coalesce(v_intent.created_by_user_id, v_intent.user_id); + + -- fallback só faz sentido para therapist/personal (porque clinic NÃO usa user_id no XOR) + -- Se você quiser manter um "responsável" da clínica, faça isso em outra coluna/tabela/evento, + -- mas NÃO em subscriptions.user_id, pois quebra o XOR. + if v_target = 'therapist' and v_user_id is null then + raise exception 'Não foi possível determinar user_id para assinatura therapist.'; + end if; + + -- cancela assinatura ativa anterior + if v_target = 'clinic' then + -- para clinic: uma assinatura ativa por tenant + update public.subscriptions + set status = 'cancelled', + cancelled_at = now(), + updated_at = now() + where tenant_id = v_intent.tenant_id + and status = 'active'; + else + -- para therapist: uma assinatura ativa por user (escopo pessoal) + update public.subscriptions + set status = 'cancelled', + cancelled_at = now(), + updated_at = now() + where user_id = v_user_id + and tenant_id is null + and status = 'active'; + end if; + + -- cria nova assinatura ativa (✅ respeita XOR) + insert into public.subscriptions ( + user_id, + plan_id, + status, + current_period_start, + current_period_end, + cancel_at_period_end, + provider, + provider_customer_id, + provider_subscription_id, + created_at, + updated_at, + + tenant_id, + plan_key, + interval, + source, + started_at, + canceled_at, + cancelled_at, + activated_at + ) + values ( + case when v_target = 'clinic' then null else v_user_id end, -- ✅ XOR + v_plan_id, + 'active', + now(), + now() + make_interval(days => v_days), + false, + 'manual', + null, + null, + now(), + now(), + + case when v_target = 'clinic' then v_intent.tenant_id else null end, -- ✅ XOR + v_intent.plan_key, + v_intent.interval, + 'manual', + now(), + null, + null, + now() + ) + returning * into v_sub; + + -- ✅ grava o vínculo intent → subscription (para rastreio e UI) + if v_target = 'clinic' then + update public.subscription_intents_tenant + set subscription_id = v_sub.id + where id = p_intent_id; + else + update public.subscription_intents_personal + set subscription_id = v_sub.id + where id = p_intent_id; + end if; + + return v_sub; +end; +$$; + + +ALTER FUNCTION "public"."activate_subscription_from_intent"("p_intent_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."admin_fix_plan_target"("p_plan_key" "text", "p_new_target" "text") RETURNS "void" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +declare + v_plan_id uuid; +begin + -- (opcional) restringe targets válidos + if p_new_target not in ('clinic','therapist') then + raise exception 'Target inválido: %', p_new_target using errcode='P0001'; + end if; + + -- trava o plano + select id into v_plan_id + from public.plans + where key = p_plan_key + for update; + + if v_plan_id is null then + raise exception 'Plano não encontrado: %', p_plan_key using errcode='P0001'; + end if; + + -- segurança: não mexer se existe subscription + if exists (select 1 from public.subscriptions s where s.plan_id = v_plan_id) then + raise exception 'Plano % possui subscriptions. Migração bloqueada.', p_plan_key using errcode='P0001'; + end if; + + -- liga bypass SOMENTE nesta transação + perform set_config('app.plan_migration_bypass', '1', true); + + update public.plans + set target = p_new_target + where id = v_plan_id; + +end +$$; + + +ALTER FUNCTION "public"."admin_fix_plan_target"("p_plan_key" "text", "p_new_target" "text") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."agenda_cfg_sync"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +begin + if new.agenda_view_mode = 'custom' then + new.usar_horario_admin_custom := true; + new.admin_inicio_visualizacao := new.agenda_custom_start; + new.admin_fim_visualizacao := new.agenda_custom_end; + else + new.usar_horario_admin_custom := false; + end if; + + return new; +end; +$$; + + +ALTER FUNCTION "public"."agenda_cfg_sync"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."cancel_subscription"("p_subscription_id" "uuid") RETURNS "public"."subscriptions" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +declare + v_sub public.subscriptions; + v_owner_type text; + v_owner_ref uuid; +begin + + select * + into v_sub + from public.subscriptions + where id = p_subscription_id + for update; + + if not found then + raise exception 'Subscription não encontrada'; + end if; + + if v_sub.status = 'canceled' then + return v_sub; + end if; + + if v_sub.tenant_id is not null then + v_owner_type := 'clinic'; + v_owner_ref := v_sub.tenant_id; + elsif v_sub.user_id is not null then + v_owner_type := 'therapist'; + v_owner_ref := v_sub.user_id; + else + v_owner_type := null; + v_owner_ref := null; + end if; + + update public.subscriptions + set status = 'canceled', + cancel_at_period_end = false, + updated_at = now() + where id = p_subscription_id + returning * into v_sub; + + insert into public.subscription_events( + subscription_id, + owner_id, + owner_type, + owner_ref, + event_type, + old_plan_id, + new_plan_id, + created_by, + reason, + source, + metadata + ) + values ( + v_sub.id, + v_owner_ref, + v_owner_type, + v_owner_ref, + 'canceled', + v_sub.plan_id, + v_sub.plan_id, + auth.uid(), + 'Cancelamento manual via admin', + 'admin_panel', + jsonb_build_object('previous_status', 'active') + ); + + if v_owner_ref is not null then + insert into public.entitlements_invalidation(owner_id, changed_at) + values (v_owner_ref, now()) + on conflict (owner_id) + do update set changed_at = excluded.changed_at; + end if; + + return v_sub; + +end; +$$; + + +ALTER FUNCTION "public"."cancel_subscription"("p_subscription_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."change_subscription_plan"("p_subscription_id" "uuid", "p_new_plan_id" "uuid") RETURNS "public"."subscriptions" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +declare + v_sub public.subscriptions; + v_old_plan uuid; + v_new_key text; + + v_owner_type text; + v_owner_ref uuid; + + v_new_target text; + v_sub_target text; +begin + select * + into v_sub + from public.subscriptions + where id = p_subscription_id + for update; + + if not found then + raise exception 'Subscription não encontrada'; + end if; + + v_old_plan := v_sub.plan_id; + + if v_old_plan = p_new_plan_id then + return v_sub; + end if; + + select key, target + into v_new_key, v_new_target + from public.plans + where id = p_new_plan_id; + + if v_new_key is null then + raise exception 'Plano não encontrado'; + end if; + + v_new_target := lower(coalesce(v_new_target, '')); + + v_sub_target := case + when v_sub.tenant_id is not null then 'clinic' + else 'therapist' + end; + + if v_new_target <> v_sub_target then + raise exception 'Plano inválido para este tipo de assinatura. Assinatura é % e o plano é %.', + v_sub_target, v_new_target + using errcode = 'P0001'; + end if; + + if v_sub.tenant_id is not null then + v_owner_type := 'clinic'; + v_owner_ref := v_sub.tenant_id; + elsif v_sub.user_id is not null then + v_owner_type := 'therapist'; + v_owner_ref := v_sub.user_id; + else + v_owner_type := null; + v_owner_ref := null; + end if; + + update public.subscriptions + set plan_id = p_new_plan_id, + plan_key = v_new_key, + updated_at = now() + where id = p_subscription_id + returning * into v_sub; + + insert into public.subscription_events( + subscription_id, + owner_id, + owner_type, + owner_ref, + event_type, + old_plan_id, + new_plan_id, + created_by, + reason, + source, + metadata + ) + values ( + v_sub.id, + v_owner_ref, + v_owner_type, + v_owner_ref, + 'plan_changed', + v_old_plan, + p_new_plan_id, + auth.uid(), + 'Plan change via DEV menu', + 'dev_menu', + jsonb_build_object( + 'previous_plan', v_old_plan, + 'new_plan', p_new_plan_id, + 'new_plan_key', v_new_key, + 'new_plan_target', v_new_target + ) + ); + + if v_owner_ref is not null then + insert into public.entitlements_invalidation (owner_id, changed_at) + values (v_owner_ref, now()) + on conflict (owner_id) + do update set changed_at = excluded.changed_at; + end if; + + return v_sub; +end; +$$; + + +ALTER FUNCTION "public"."change_subscription_plan"("p_subscription_id" "uuid", "p_new_plan_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."create_clinic_tenant"("p_name" "text") RETURNS "uuid" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +declare + v_uid uuid; + v_tenant uuid; + v_name text; +begin + v_uid := auth.uid(); + if v_uid is null then + raise exception 'Not authenticated'; + end if; + + v_name := nullif(trim(coalesce(p_name, '')), ''); + if v_name is null then + v_name := 'Clínica'; + end if; + + insert into public.tenants (name, kind, created_at) + values (v_name, 'clinic', now()) + returning id into v_tenant; + + insert into public.tenant_members (tenant_id, user_id, role, status, created_at) + values (v_tenant, v_uid, 'tenant_admin', 'active', now()); + + return v_tenant; +end; +$$; + + +ALTER FUNCTION "public"."create_clinic_tenant"("p_name" "text") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."create_patient_intake_request"("p_token" "text", "p_name" "text", "p_email" "text" DEFAULT NULL::"text", "p_phone" "text" DEFAULT NULL::"text", "p_notes" "text" DEFAULT NULL::"text", "p_consent" boolean DEFAULT false) RETURNS "uuid" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public' + AS $$ +declare + v_owner uuid; + v_active boolean; + v_expires timestamptz; + v_max_uses int; + v_uses int; + v_id uuid; +begin + select owner_id, active, expires_at, max_uses, uses + into v_owner, v_active, v_expires, v_max_uses, v_uses + from public.patient_invites + where token = p_token + limit 1; + + if v_owner is null then + raise exception 'Token inválido'; + end if; + + if v_active is not true then + raise exception 'Link desativado'; + end if; + + if v_expires is not null and now() > v_expires then + raise exception 'Link expirado'; + end if; + + if v_max_uses is not null and v_uses >= v_max_uses then + raise exception 'Limite de uso atingido'; + end if; + + if p_name is null or length(trim(p_name)) = 0 then + raise exception 'Nome é obrigatório'; + end if; + + insert into public.patient_intake_requests + (owner_id, token, name, email, phone, notes, consent, status) + values + (v_owner, p_token, trim(p_name), + nullif(lower(trim(p_email)), ''), + nullif(trim(p_phone), ''), + nullif(trim(p_notes), ''), + coalesce(p_consent, false), + 'new') + returning id into v_id; + + update public.patient_invites + set uses = uses + 1 + where token = p_token; + + return v_id; +end; +$$; + + +ALTER FUNCTION "public"."create_patient_intake_request"("p_token" "text", "p_name" "text", "p_email" "text", "p_phone" "text", "p_notes" "text", "p_consent" boolean) OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."create_patient_intake_request_v2"("p_token" "text", "p_payload" "jsonb") RETURNS "uuid" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public' + AS $_$ +declare + v_owner_id uuid; + v_intake_id uuid; + v_birth_raw text; + v_birth date; +begin + select owner_id + into v_owner_id + from public.patient_invites + where token = p_token; + + if v_owner_id is null then + raise exception 'Token inválido ou expirado'; + end if; + + v_birth_raw := nullif(trim(coalesce( + p_payload->>'data_nascimento', + '' + )), ''); + + v_birth := case + when v_birth_raw is null then null + when v_birth_raw ~ '^\d{4}-\d{2}-\d{2}$' then v_birth_raw::date + when v_birth_raw ~ '^\d{2}-\d{2}-\d{4}$' then to_date(v_birth_raw, 'DD-MM-YYYY') + else null + end; + + insert into public.patient_intake_requests ( + owner_id, + token, + status, + consent, + + nome_completo, + email_principal, + telefone, + + avatar_url, -- 🔥 AQUI + + data_nascimento, + cpf, + rg, + genero, + estado_civil, + profissao, + escolaridade, + nacionalidade, + naturalidade, + + cep, + pais, + cidade, + estado, + endereco, + numero, + complemento, + bairro, + + observacoes, + notas_internas, + + encaminhado_por, + onde_nos_conheceu + ) + values ( + v_owner_id, + p_token, + 'new', + coalesce((p_payload->>'consent')::boolean, false), + + nullif(trim(p_payload->>'nome_completo'), ''), + nullif(trim(p_payload->>'email_principal'), ''), + nullif(regexp_replace(coalesce(p_payload->>'telefone',''), '\D', '', 'g'), ''), + + nullif(trim(p_payload->>'avatar_url'), ''), -- 🔥 AQUI + + v_birth, + nullif(regexp_replace(coalesce(p_payload->>'cpf',''), '\D', '', 'g'), ''), + nullif(trim(p_payload->>'rg'), ''), + nullif(trim(p_payload->>'genero'), ''), + nullif(trim(p_payload->>'estado_civil'), ''), + nullif(trim(p_payload->>'profissao'), ''), + nullif(trim(p_payload->>'escolaridade'), ''), + nullif(trim(p_payload->>'nacionalidade'), ''), + nullif(trim(p_payload->>'naturalidade'), ''), + + nullif(regexp_replace(coalesce(p_payload->>'cep',''), '\D', '', 'g'), ''), + nullif(trim(p_payload->>'pais'), ''), + nullif(trim(p_payload->>'cidade'), ''), + nullif(trim(p_payload->>'estado'), ''), + nullif(trim(p_payload->>'endereco'), ''), + nullif(trim(p_payload->>'numero'), ''), + nullif(trim(p_payload->>'complemento'), ''), + nullif(trim(p_payload->>'bairro'), ''), + + nullif(trim(p_payload->>'observacoes'), ''), + nullif(trim(p_payload->>'notas_internas'), ''), + + nullif(trim(p_payload->>'encaminhado_por'), ''), + nullif(trim(p_payload->>'onde_nos_conheceu'), '') + ) + returning id into v_intake_id; + + return v_intake_id; +end; +$_$; + + +ALTER FUNCTION "public"."create_patient_intake_request_v2"("p_token" "text", "p_payload" "jsonb") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."current_member_id"("p_tenant_id" "uuid") RETURNS "uuid" + LANGUAGE "sql" STABLE + AS $$ + select tm.id + from public.tenant_members tm + where tm.tenant_id = p_tenant_id + and tm.user_id = auth.uid() + limit 1 +$$; + + +ALTER FUNCTION "public"."current_member_id"("p_tenant_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."current_member_role"("p_tenant_id" "uuid") RETURNS "text" + LANGUAGE "sql" STABLE + AS $$ + select tm.role + from public.tenant_members tm + where tm.tenant_id = p_tenant_id + and tm.user_id = auth.uid() + limit 1 +$$; + + +ALTER FUNCTION "public"."current_member_role"("p_tenant_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."delete_commitment_full"("p_tenant_id" "uuid", "p_commitment_id" "uuid") RETURNS "jsonb" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public' + AS $$ +declare + v_is_native boolean; + v_fields int := 0; + v_logs int := 0; + v_parent int := 0; +begin + if auth.uid() is null then + raise exception 'Not authenticated'; + end if; + + if not exists ( + select 1 + from public.tenant_members tm + where tm.tenant_id = p_tenant_id + and tm.user_id = auth.uid() + and tm.status = 'active' + ) then + raise exception 'Not allowed'; + end if; + + select dc.is_native + into v_is_native + from public.determined_commitments dc + where dc.tenant_id = p_tenant_id + and dc.id = p_commitment_id; + + if v_is_native is null then + raise exception 'Commitment not found'; + end if; + + if v_is_native = true then + raise exception 'Cannot delete native commitment'; + end if; + + delete from public.determined_commitment_fields + where tenant_id = p_tenant_id + and commitment_id = p_commitment_id; + get diagnostics v_fields = row_count; + + delete from public.commitment_time_logs + where tenant_id = p_tenant_id + and commitment_id = p_commitment_id; + get diagnostics v_logs = row_count; + + delete from public.determined_commitments + where tenant_id = p_tenant_id + and id = p_commitment_id; + get diagnostics v_parent = row_count; + + if v_parent <> 1 then + raise exception 'Parent not deleted (RLS/owner issue).'; + end if; + + return jsonb_build_object( + 'ok', true, + 'deleted', jsonb_build_object( + 'fields', v_fields, + 'logs', v_logs, + 'commitment', v_parent + ) + ); +end; +$$; + + +ALTER FUNCTION "public"."delete_commitment_full"("p_tenant_id" "uuid", "p_commitment_id" "uuid") OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."delete_determined_commitment"("p_tenant_id" "uuid", "p_commitment_id" "uuid") RETURNS "jsonb" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public' + AS $$ +declare + v_is_native boolean; + v_fields_deleted int := 0; + v_logs_deleted int := 0; + v_commitment_deleted int := 0; +begin + if auth.uid() is null then + raise exception 'Not authenticated'; + end if; + + if not exists ( + select 1 + from public.tenant_members tm + where tm.tenant_id = p_tenant_id + and tm.user_id = auth.uid() + and tm.status = 'active' + ) then + raise exception 'Not allowed'; + end if; + + select dc.is_native + into v_is_native + from public.determined_commitments dc + where dc.tenant_id = p_tenant_id + and dc.id = p_commitment_id; + + if v_is_native is null then + raise exception 'Commitment not found for tenant'; + end if; + + if v_is_native = true then + raise exception 'Cannot delete native commitment'; + end if; + + delete from public.determined_commitment_fields f + where f.tenant_id = p_tenant_id + and f.commitment_id = p_commitment_id; + get diagnostics v_fields_deleted = row_count; + + delete from public.commitment_time_logs l + where l.tenant_id = p_tenant_id + and l.commitment_id = p_commitment_id; + get diagnostics v_logs_deleted = row_count; + + delete from public.determined_commitments dc + where dc.tenant_id = p_tenant_id + and dc.id = p_commitment_id; + get diagnostics v_commitment_deleted = row_count; + + if v_commitment_deleted <> 1 then + raise exception 'Delete did not remove the commitment (tenant mismatch?)'; + end if; + + return jsonb_build_object( + 'ok', true, + 'tenant_id', p_tenant_id, + 'commitment_id', p_commitment_id, + 'deleted', jsonb_build_object( + 'fields', v_fields_deleted, + 'logs', v_logs_deleted, + 'commitment', v_commitment_deleted + ) + ); +end; +$$; + + +ALTER FUNCTION "public"."delete_determined_commitment"("p_tenant_id" "uuid", "p_commitment_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."dev_list_auth_users"("p_limit" integer DEFAULT 50) RETURNS TABLE("id" "uuid", "email" "text", "created_at" timestamp with time zone) + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'auth' + AS $$ +begin + -- só saas_admin pode ver + if not exists ( + select 1 + from public.profiles p + where p.id = auth.uid() + and p.role = 'saas_admin' + ) then + return; + end if; + + return query + select + u.id, + u.email, + u.created_at + from auth.users u + order by u.created_at desc + limit greatest(1, least(coalesce(p_limit, 50), 500)); +end; +$$; + + +ALTER FUNCTION "public"."dev_list_auth_users"("p_limit" integer) OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."dev_list_custom_users"() RETURNS TABLE("user_id" "uuid", "email" "text", "created_at" timestamp with time zone, "global_role" "text", "tenant_role" "text", "tenant_id" "uuid", "password_dev" "text", "kind" "text") + LANGUAGE "sql" SECURITY DEFINER + SET "search_path" TO 'public' + AS $$ + with base as ( + select + u.id as user_id, + lower(u.email) as email, + u.created_at + from auth.users u + where lower(u.email) not in ( + 'clinic@agenciapsi.com.br', + 'therapist@agenciapsi.com.br', + 'patient@agenciapsi.com.br', + 'saas@agenciapsi.com.br' + ) + ), + prof as ( + select p.id, p.role as global_role + from public.profiles p + ), + last_membership as ( + select distinct on (tm.user_id) + tm.user_id, + tm.tenant_id, + tm.role as tenant_role, + tm.created_at + from public.tenant_members tm + where tm.status = 'active' + order by tm.user_id, tm.created_at desc + ) + select + b.user_id, + b.email, + b.created_at, + pr.global_role, + lm.tenant_role, + lm.tenant_id, + dc.password_dev, + dc.kind + from base b + left join prof pr on pr.id = b.user_id + left join last_membership lm on lm.user_id = b.user_id + left join public.dev_user_credentials dc on lower(dc.email) = b.email + order by b.created_at desc; +$$; + + +ALTER FUNCTION "public"."dev_list_custom_users"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."dev_list_intent_leads"() RETURNS TABLE("email" "text", "last_intent_at" timestamp with time zone, "plan_key" "text", "billing_interval" "text", "status" "text", "tenant_id" "uuid") + LANGUAGE "sql" SECURITY DEFINER + SET "search_path" TO 'public' + AS $$ + select + lower(si.email) as email, + max(si.created_at) as last_intent_at, + (array_agg(si.plan_key order by si.created_at desc))[1] as plan_key, + (array_agg(si.interval order by si.created_at desc))[1] as billing_interval, + (array_agg(si.status order by si.created_at desc))[1] as status, + (array_agg(si.tenant_id order by si.created_at desc))[1] as tenant_id + from public.subscription_intents si + where si.email is not null + and not exists ( + select 1 + from auth.users au + where lower(au.email) = lower(si.email) + ) + group by lower(si.email) + order by max(si.created_at) desc; +$$; + + +ALTER FUNCTION "public"."dev_list_intent_leads"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."dev_public_debug_snapshot"() RETURNS TABLE("users_total" integer, "tenants_total" integer, "intents_new_total" integer, "latest_intents" "jsonb") + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public' + AS $_$ +declare + v_latest jsonb; +begin + select jsonb_agg( + jsonb_build_object( + 'created_at', si.created_at, + 'email_masked', + regexp_replace(lower(si.email), '(^.).*(@.*$)', '\1***\2'), + 'plan_key', si.plan_key, + 'status', si.status + ) + order by si.created_at desc + ) + into v_latest + from ( + select si.* + from public.subscription_intents si + where si.email is not null + order by si.created_at desc + limit 5 + ) si; + + return query + select + (select count(*)::int from auth.users) as users_total, + (select count(*)::int from public.tenants) as tenants_total, + (select count(*)::int from public.subscription_intents where status = 'new') as intents_new_total, + coalesce(v_latest, '[]'::jsonb) as latest_intents; +end; +$_$; + + +ALTER FUNCTION "public"."dev_public_debug_snapshot"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."ensure_personal_tenant"() RETURNS "uuid" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +DECLARE + v_uid uuid; + v_existing uuid; +BEGIN + v_uid := auth.uid(); + IF v_uid IS NULL THEN + RAISE EXCEPTION 'Not authenticated'; + END IF; + + SELECT tm.tenant_id INTO v_existing + FROM public.tenant_members tm + JOIN public.tenants t ON t.id = tm.tenant_id + WHERE tm.user_id = v_uid + AND tm.status = 'active' + AND t.kind IN ('therapist', 'saas') + ORDER BY tm.created_at DESC + LIMIT 1; + + IF v_existing IS NOT NULL THEN + RETURN v_existing; + END IF; + + RETURN public.provision_account_tenant(v_uid, 'therapist'); +END; +$$; + + +ALTER FUNCTION "public"."ensure_personal_tenant"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."ensure_personal_tenant_for_user"("p_user_id" "uuid") RETURNS "uuid" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +declare + v_uid uuid; + v_existing uuid; + v_tenant uuid; + v_email text; + v_name text; +begin + v_uid := p_user_id; + if v_uid is null then + raise exception 'Missing user id'; + end if; + + -- só considera tenant pessoal (kind='saas') + select tm.tenant_id + into v_existing + from public.tenant_members tm + join public.tenants t on t.id = tm.tenant_id + where tm.user_id = v_uid + and tm.status = 'active' + and t.kind = 'saas' + order by tm.created_at desc + limit 1; + + if v_existing is not null then + return v_existing; + end if; + + select email into v_email + from auth.users + where id = v_uid; + + v_name := coalesce(split_part(v_email, '@', 1), 'Conta'); + + insert into public.tenants (name, kind, created_at) + values (v_name || ' (Pessoal)', 'saas', now()) + returning id into v_tenant; + + insert into public.tenant_members (tenant_id, user_id, role, status, created_at) + values (v_tenant, v_uid, 'tenant_admin', 'active', now()); + + return v_tenant; +end; +$$; + + +ALTER FUNCTION "public"."ensure_personal_tenant_for_user"("p_user_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."fix_all_subscription_mismatches"() RETURNS "void" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +declare + r record; +begin + for r in + select distinct s.user_id as owner_id + from public.subscriptions s + where s.status = 'active' + and s.user_id is not null + loop + perform public.rebuild_owner_entitlements(r.owner_id); + end loop; +end; +$$; + + +ALTER FUNCTION "public"."fix_all_subscription_mismatches"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."fn_agenda_regras_semanais_no_overlap"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +declare + v_count int; +begin + if new.ativo is false then + return new; + end if; + + select count(*) into v_count + from public.agenda_regras_semanais r + where r.owner_id = new.owner_id + and r.dia_semana = new.dia_semana + and r.ativo is true + and (tg_op = 'INSERT' or r.id <> new.id) + and (new.hora_inicio < r.hora_fim and new.hora_fim > r.hora_inicio); + + if v_count > 0 then + raise exception 'Janela sobreposta: já existe uma regra ativa nesse intervalo.'; + end if; + + return new; +end; +$$; + + +ALTER FUNCTION "public"."fn_agenda_regras_semanais_no_overlap"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."get_my_email"() RETURNS "text" + LANGUAGE "sql" SECURITY DEFINER + SET "search_path" TO 'public', 'auth' + AS $$ + select lower(email) + from auth.users + where id = auth.uid(); +$$; + + +ALTER FUNCTION "public"."get_my_email"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."guard_account_type_immutable"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +BEGIN + IF OLD.account_type <> 'free' AND NEW.account_type IS DISTINCT FROM OLD.account_type THEN + RAISE EXCEPTION 'account_type é imutável após escolha (atual: "%" para tentativa: "%"). Para mudar de perfil, crie uma nova conta.', OLD.account_type, NEW.account_type + USING ERRCODE = 'P0001'; + END IF; + RETURN NEW; +END; +$$; + + +ALTER FUNCTION "public"."guard_account_type_immutable"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."guard_locked_commitment"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +begin + if (old.is_locked = true) then + if (tg_op = 'DELETE') then + raise exception 'Compromisso bloqueado não pode ser excluído.'; + end if; + + if (tg_op = 'UPDATE') then + if (new.active = false) then + raise exception 'Compromisso bloqueado não pode ser desativado.'; + end if; + + -- trava renomear (mantém o "Sessão" sempre igual) + if (new.name is distinct from old.name) then + raise exception 'Compromisso bloqueado não pode ser renomeado.'; + end if; + + -- se quiser travar descrição também, descomente: + -- if (new.description is distinct from old.description) then + -- raise exception 'Compromisso bloqueado não pode alterar descrição.'; + -- end if; + end if; + end if; + + return new; +end; +$$; + + +ALTER FUNCTION "public"."guard_locked_commitment"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."guard_no_change_core_plan_key"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +begin + if old.key in ('clinic_free','clinic_pro','therapist_free','therapist_pro') + and new.key is distinct from old.key then + raise exception 'Não é permitido alterar a key do plano padrão (%).', old.key + using errcode = 'P0001'; + end if; + + return new; +end $$; + + +ALTER FUNCTION "public"."guard_no_change_core_plan_key"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."guard_no_change_plan_target"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +declare + v_bypass text; +begin + -- bypass controlado por sessão/transação: + -- só passa se app.plan_migration_bypass = '1' + v_bypass := current_setting('app.plan_migration_bypass', true); + + if v_bypass = '1' then + return new; + end if; + + -- comportamento original (bloqueia qualquer mudança) + if new.target is distinct from old.target then + raise exception 'Não é permitido alterar target do plano (%) de % para %.', + old.key, old.target, new.target + using errcode = 'P0001'; + end if; + + return new; +end +$$; + + +ALTER FUNCTION "public"."guard_no_change_plan_target"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."guard_no_delete_core_plans"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +begin + if old.key in ('clinic_free','clinic_pro','therapist_free','therapist_pro') then + raise exception 'Plano padrão (%) não pode ser removido.', old.key + using errcode = 'P0001'; + end if; + + return old; +end $$; + + +ALTER FUNCTION "public"."guard_no_delete_core_plans"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."guard_patient_cannot_own_tenant"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +DECLARE + v_account_type text; +BEGIN + SELECT account_type INTO v_account_type + FROM public.profiles + WHERE id = NEW.user_id; + + IF v_account_type = 'patient' AND NEW.role IN ('tenant_admin', 'therapist') THEN + RAISE EXCEPTION 'Usuário com perfil "patient" não pode ser proprietário ou terapeuta de um tenant. Se tornou profissional? Crie uma nova conta.' + USING ERRCODE = 'P0001'; + END IF; + + RETURN NEW; +END; +$$; + + +ALTER FUNCTION "public"."guard_patient_cannot_own_tenant"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."guard_tenant_kind_immutable"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +BEGIN + IF NEW.kind IS DISTINCT FROM OLD.kind THEN + RAISE EXCEPTION 'tenants.kind é imutável após criação. Tentativa de alterar "%" para "%".', OLD.kind, NEW.kind + USING ERRCODE = 'P0001'; + END IF; + RETURN NEW; +END; +$$; + + +ALTER FUNCTION "public"."guard_tenant_kind_immutable"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."handle_new_user"() RETURNS "trigger" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public' + AS $$ +BEGIN + INSERT INTO public.profiles (id, role, account_type) + VALUES (NEW.id, 'portal_user', 'free') + ON CONFLICT (id) DO NOTHING; + RETURN NEW; +END; +$$; + + +ALTER FUNCTION "public"."handle_new_user"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."handle_new_user_create_personal_tenant"() RETURNS "trigger" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +BEGIN + -- Desabilitado. Tenant criado no onboarding via provision_account_tenant(). + RETURN NEW; +END; +$$; + + +ALTER FUNCTION "public"."handle_new_user_create_personal_tenant"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."has_feature"("p_owner_id" "uuid", "p_feature_key" "text") RETURNS boolean + LANGUAGE "sql" STABLE + AS $$ + select exists ( + select 1 + from public.owner_feature_entitlements e + where e.owner_id = p_owner_id + and e.feature_key = p_feature_key + ); +$$; + + +ALTER FUNCTION "public"."has_feature"("p_owner_id" "uuid", "p_feature_key" "text") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."is_clinic_tenant"("_tenant_id" "uuid") RETURNS boolean + LANGUAGE "sql" STABLE + AS $$ + SELECT EXISTS ( + SELECT 1 FROM public.tenants t + WHERE t.id = _tenant_id + AND t.kind IN ('clinic', 'clinic_coworking', 'clinic_reception', 'clinic_full') + ); +$$; + + +ALTER FUNCTION "public"."is_clinic_tenant"("_tenant_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."is_saas_admin"() RETURNS boolean + LANGUAGE "sql" STABLE + AS $$ + select exists ( + select 1 from public.saas_admins sa + where sa.user_id = auth.uid() + ); +$$; + + +ALTER FUNCTION "public"."is_saas_admin"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."is_tenant_admin"("p_tenant_id" "uuid") RETURNS boolean + LANGUAGE "sql" STABLE SECURITY DEFINER + SET "search_path" TO 'public' + SET "row_security" TO 'off' + AS $$ + select exists ( + select 1 + from public.tenant_members tm + where tm.tenant_id = p_tenant_id + and tm.user_id = auth.uid() + and tm.role = 'tenant_admin' + and tm.status = 'active' + ); +$$; + + +ALTER FUNCTION "public"."is_tenant_admin"("p_tenant_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."is_tenant_member"("_tenant_id" "uuid") RETURNS boolean + LANGUAGE "sql" STABLE + AS $$ + select exists ( + select 1 + from public.tenant_members m + where m.tenant_id = _tenant_id + and m.user_id = auth.uid() + and m.status = 'active' + ); +$$; + + +ALTER FUNCTION "public"."is_tenant_member"("_tenant_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."is_therapist_tenant"("_tenant_id" "uuid") RETURNS boolean + LANGUAGE "sql" STABLE + AS $$ + SELECT EXISTS ( + SELECT 1 FROM public.tenants t + WHERE t.id = _tenant_id AND t.kind = 'therapist' + ); +$$; + + +ALTER FUNCTION "public"."is_therapist_tenant"("_tenant_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."jwt_email"() RETURNS "text" + LANGUAGE "sql" STABLE + AS $$ + select nullif(lower(current_setting('request.jwt.claim.email', true)), ''); +$$; + + +ALTER FUNCTION "public"."jwt_email"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."my_tenants"() RETURNS TABLE("tenant_id" "uuid", "role" "text", "status" "text", "kind" "text") + LANGUAGE "sql" STABLE + AS $$ + select + tm.tenant_id, + tm.role, + tm.status, + t.kind + from public.tenant_members tm + join public.tenants t on t.id = tm.tenant_id + where tm.user_id = auth.uid(); +$$; + + +ALTER FUNCTION "public"."my_tenants"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."patients_validate_member_consistency"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +DECLARE + v_tenant_responsible uuid; + v_tenant_therapist uuid; +BEGIN + -- responsible_member sempre deve existir e ser do tenant + SELECT tenant_id INTO v_tenant_responsible + FROM public.tenant_members + WHERE id = NEW.responsible_member_id; + + IF v_tenant_responsible IS NULL THEN + RAISE EXCEPTION 'Responsible member not found'; + END IF; + + IF NEW.tenant_id IS NULL THEN + RAISE EXCEPTION 'tenant_id is required'; + END IF; + + IF v_tenant_responsible <> NEW.tenant_id THEN + RAISE EXCEPTION 'Responsible member must belong to the same tenant'; + END IF; + + -- therapist scope: therapist_member_id deve existir e ser do mesmo tenant + IF NEW.patient_scope = 'therapist' THEN + IF NEW.therapist_member_id IS NULL THEN + RAISE EXCEPTION 'therapist_member_id is required when patient_scope=therapist'; + END IF; + + SELECT tenant_id INTO v_tenant_therapist + FROM public.tenant_members + WHERE id = NEW.therapist_member_id; + + IF v_tenant_therapist IS NULL THEN + RAISE EXCEPTION 'Therapist member not found'; + END IF; + + IF v_tenant_therapist <> NEW.tenant_id THEN + RAISE EXCEPTION 'Therapist member must belong to the same tenant'; + END IF; + END IF; + + RETURN NEW; +END; +$$; + + +ALTER FUNCTION "public"."patients_validate_member_consistency"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."patients_validate_responsible_member_tenant"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +declare + m_tenant uuid; +begin + select tenant_id into m_tenant + from public.tenant_members + where id = new.responsible_member_id; + + if m_tenant is null then + raise exception 'Responsible member not found'; + end if; + + if new.tenant_id is null then + raise exception 'tenant_id is required'; + end if; + + if m_tenant <> new.tenant_id then + raise exception 'Responsible member must belong to the same tenant'; + end if; + + return new; +end; +$$; + + +ALTER FUNCTION "public"."patients_validate_responsible_member_tenant"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."prevent_promoting_to_system"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +begin + if new.is_system = true and old.is_system is distinct from true then + raise exception 'Não é permitido transformar um grupo comum em grupo do sistema.'; + end if; + return new; +end; +$$; + + +ALTER FUNCTION "public"."prevent_promoting_to_system"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."prevent_saas_membership"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM public.profiles + WHERE id = NEW.user_id + AND role = 'saas_admin' + ) THEN + RAISE EXCEPTION 'SaaS admin cannot belong to tenant'; + END IF; + + RETURN NEW; +END; +$$; + + +ALTER FUNCTION "public"."prevent_saas_membership"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."prevent_system_group_changes"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +begin + -- Se for grupo do sistema, regras rígidas: + if old.is_system = true then + + -- nunca pode deletar + if tg_op = 'DELETE' then + raise exception 'Grupos padrão do sistema não podem ser alterados ou excluídos.'; + end if; + + if tg_op = 'UPDATE' then + -- permite SOMENTE mudar tenant_id e/ou updated_at + -- qualquer mudança de conteúdo permanece proibida + if + new.nome is distinct from old.nome or + new.descricao is distinct from old.descricao or + new.cor is distinct from old.cor or + new.is_active is distinct from old.is_active or + new.is_system is distinct from old.is_system or + new.owner_id is distinct from old.owner_id or + new.therapist_id is distinct from old.therapist_id or + new.created_at is distinct from old.created_at + then + raise exception 'Grupos padrão do sistema não podem ser alterados ou excluídos.'; + end if; + + -- chegou aqui: só tenant_id/updated_at mudaram -> ok + return new; + end if; + + end if; + + -- não-system: deixa passar + if tg_op = 'DELETE' then + return old; + end if; + + return new; +end; +$$; + + +ALTER FUNCTION "public"."prevent_system_group_changes"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."provision_account_tenant"("p_user_id" "uuid", "p_kind" "text", "p_name" "text" DEFAULT NULL::"text") RETURNS "uuid" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +DECLARE + v_tenant_id uuid; + v_account_type text; + v_name text; +BEGIN + IF p_kind NOT IN ('therapist', 'clinic_coworking', 'clinic_reception', 'clinic_full') THEN + RAISE EXCEPTION 'kind inválido: "%". Use: therapist, clinic_coworking, clinic_reception, clinic_full.', p_kind + USING ERRCODE = 'P0001'; + END IF; + + v_account_type := CASE WHEN p_kind = 'therapist' THEN 'therapist' ELSE 'clinic' END; + + IF EXISTS ( + SELECT 1 + FROM public.tenant_members tm + JOIN public.tenants t ON t.id = tm.tenant_id + WHERE tm.user_id = p_user_id + AND tm.role = 'tenant_admin' + AND tm.status = 'active' + AND t.kind = p_kind + ) THEN + RAISE EXCEPTION 'Usuário já possui um tenant do tipo "%".', p_kind + USING ERRCODE = 'P0001'; + END IF; + + v_name := COALESCE( + NULLIF(TRIM(p_name), ''), + ( + SELECT COALESCE(NULLIF(TRIM(pr.full_name), ''), SPLIT_PART(au.email, '@', 1)) + FROM public.profiles pr + JOIN auth.users au ON au.id = pr.id + WHERE pr.id = p_user_id + ), + 'Conta' + ); + + INSERT INTO public.tenants (name, kind, created_at) + VALUES (v_name, p_kind, now()) + RETURNING id INTO v_tenant_id; + + INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at) + VALUES (v_tenant_id, p_user_id, 'tenant_admin', 'active', now()); + + UPDATE public.profiles + SET account_type = v_account_type + WHERE id = p_user_id; + + PERFORM public.seed_determined_commitments(v_tenant_id); + + RETURN v_tenant_id; +END; +$$; + + +ALTER FUNCTION "public"."provision_account_tenant"("p_user_id" "uuid", "p_kind" "text", "p_name" "text") OWNER TO "supabase_admin"; + + +COMMENT ON FUNCTION "public"."provision_account_tenant"("p_user_id" "uuid", "p_kind" "text", "p_name" "text") IS 'Cria o tenant do tipo correto e atualiza account_type no profile. Chamar no onboarding após escolha/pagamento de plano therapist ou clinic. p_kind: therapist | clinic_coworking | clinic_reception | clinic_full'; + + + +CREATE OR REPLACE FUNCTION "public"."reactivate_subscription"("p_subscription_id" "uuid") RETURNS "public"."subscriptions" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +declare + v_sub public.subscriptions; + v_owner_type text; + v_owner_ref uuid; +begin + + select * + into v_sub + from public.subscriptions + where id = p_subscription_id + for update; + + if not found then + raise exception 'Subscription não encontrada'; + end if; + + if v_sub.status = 'active' then + return v_sub; + end if; + + if v_sub.tenant_id is not null then + v_owner_type := 'clinic'; + v_owner_ref := v_sub.tenant_id; + elsif v_sub.user_id is not null then + v_owner_type := 'therapist'; + v_owner_ref := v_sub.user_id; + else + v_owner_type := null; + v_owner_ref := null; + end if; + + update public.subscriptions + set status = 'active', + cancel_at_period_end = false, + updated_at = now() + where id = p_subscription_id + returning * into v_sub; + + insert into public.subscription_events( + subscription_id, + owner_id, + owner_type, + owner_ref, + event_type, + old_plan_id, + new_plan_id, + created_by, + reason, + source, + metadata + ) + values ( + v_sub.id, + v_owner_ref, + v_owner_type, + v_owner_ref, + 'reactivated', + v_sub.plan_id, + v_sub.plan_id, + auth.uid(), + 'Reativação manual via admin', + 'admin_panel', + jsonb_build_object('previous_status', 'canceled') + ); + + if v_owner_ref is not null then + insert into public.entitlements_invalidation(owner_id, changed_at) + values (v_owner_ref, now()) + on conflict (owner_id) + do update set changed_at = excluded.changed_at; + end if; + + return v_sub; + +end; +$$; + + +ALTER FUNCTION "public"."reactivate_subscription"("p_subscription_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."rebuild_owner_entitlements"("p_owner_id" "uuid") RETURNS "void" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +declare + v_plan_id uuid; +begin + -- Plano ativo do owner (owner = subscriptions.user_id) + select s.plan_id + into v_plan_id + from public.subscriptions s + where s.user_id = p_owner_id + and s.status = 'active' + order by s.created_at desc + limit 1; + + -- Sempre zera entitlements do owner (rebuild) + delete from public.owner_feature_entitlements e + where e.owner_id = p_owner_id; + + -- Se não tem assinatura ativa, acabou + if v_plan_id is null then + return; + end if; + + -- Recria entitlements esperados pelo plano + insert into public.owner_feature_entitlements (owner_id, feature_key, sources, limits_list) + select + p_owner_id as owner_id, + f.key as feature_key, + array['plan'::text] as sources, + '{}'::jsonb as limits_list + from public.plan_features pf + join public.features f on f.id = pf.feature_id + where pf.plan_id = v_plan_id; + +end; +$$; + + +ALTER FUNCTION "public"."rebuild_owner_entitlements"("p_owner_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."rotate_patient_invite_token"("p_new_token" "text") RETURNS "uuid" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public' + AS $$ +declare + v_uid uuid; + v_id uuid; +begin + -- pega o usuário logado + v_uid := auth.uid(); + if v_uid is null then + raise exception 'Usuário não autenticado'; + end if; + + -- desativa tokens antigos ativos do usuário + update public.patient_invites + set active = false + where owner_id = v_uid + and active = true; + + -- cria novo token + insert into public.patient_invites (owner_id, token, active) + values (v_uid, p_new_token, true) + returning id into v_id; + + return v_id; +end; +$$; + + +ALTER FUNCTION "public"."rotate_patient_invite_token"("p_new_token" "text") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."seed_determined_commitments"("p_tenant_id" "uuid") RETURNS "void" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +declare + v_id uuid; +begin + -- Sessão (locked + sempre ativa) + if not exists ( + select 1 from public.determined_commitments + where tenant_id = p_tenant_id and is_native = true and native_key = 'session' + ) then + insert into public.determined_commitments + (tenant_id, is_native, native_key, is_locked, active, name, description) + values + (p_tenant_id, true, 'session', true, true, 'Sessão', 'Sessão com paciente'); + end if; + + -- Leitura + if not exists ( + select 1 from public.determined_commitments + where tenant_id = p_tenant_id and is_native = true and native_key = 'reading' + ) then + insert into public.determined_commitments + (tenant_id, is_native, native_key, is_locked, active, name, description) + values + (p_tenant_id, true, 'reading', false, true, 'Leitura', 'Praticar leitura'); + end if; + + -- Supervisão + if not exists ( + select 1 from public.determined_commitments + where tenant_id = p_tenant_id and is_native = true and native_key = 'supervision' + ) then + insert into public.determined_commitments + (tenant_id, is_native, native_key, is_locked, active, name, description) + values + (p_tenant_id, true, 'supervision', false, true, 'Supervisão', 'Supervisão'); + end if; + + -- Aula ✅ (corrigido) + if not exists ( + select 1 from public.determined_commitments + where tenant_id = p_tenant_id and is_native = true and native_key = 'class' + ) then + insert into public.determined_commitments + (tenant_id, is_native, native_key, is_locked, active, name, description) + values + (p_tenant_id, true, 'class', false, false, 'Aula', 'Dar aula'); + end if; + + -- Análise pessoal + if not exists ( + select 1 from public.determined_commitments + where tenant_id = p_tenant_id and is_native = true and native_key = 'analysis' + ) then + insert into public.determined_commitments + (tenant_id, is_native, native_key, is_locked, active, name, description) + values + (p_tenant_id, true, 'analysis', false, true, 'Análise Pessoal', 'Minha análise pessoal'); + end if; + + -- ------------------------------------------------------- + -- Campos padrão (idempotentes por (commitment_id, key)) + -- ------------------------------------------------------- + + -- Leitura + select id into v_id + from public.determined_commitments + where tenant_id = p_tenant_id and is_native = true and native_key = 'reading' + limit 1; + + if v_id is not null then + if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'book') then + insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order) + values (p_tenant_id, v_id, 'book', 'Livro', 'text', false, 10); + end if; + + if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'author') then + insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order) + values (p_tenant_id, v_id, 'author', 'Autor', 'text', false, 20); + end if; + + if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'notes') then + insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order) + values (p_tenant_id, v_id, 'notes', 'Observação', 'textarea', false, 30); + end if; + end if; + + -- Supervisão + select id into v_id + from public.determined_commitments + where tenant_id = p_tenant_id and is_native = true and native_key = 'supervision' + limit 1; + + if v_id is not null then + if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'supervisor') then + insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order) + values (p_tenant_id, v_id, 'supervisor', 'Supervisor', 'text', false, 10); + end if; + + if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'topic') then + insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order) + values (p_tenant_id, v_id, 'topic', 'Assunto', 'text', false, 20); + end if; + + if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'notes') then + insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order) + values (p_tenant_id, v_id, 'notes', 'Observação', 'textarea', false, 30); + end if; + end if; + + -- Aula + select id into v_id + from public.determined_commitments + where tenant_id = p_tenant_id and is_native = true and native_key = 'class' + limit 1; + + if v_id is not null then + if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'theme') then + insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order) + values (p_tenant_id, v_id, 'theme', 'Tema', 'text', false, 10); + end if; + + if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'group') then + insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order) + values (p_tenant_id, v_id, 'group', 'Turma', 'text', false, 20); + end if; + + if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'notes') then + insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order) + values (p_tenant_id, v_id, 'notes', 'Observação', 'textarea', false, 30); + end if; + end if; + + -- Análise + select id into v_id + from public.determined_commitments + where tenant_id = p_tenant_id and is_native = true and native_key = 'analysis' + limit 1; + + if v_id is not null then + if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'analyst') then + insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order) + values (p_tenant_id, v_id, 'analyst', 'Analista', 'text', false, 10); + end if; + + if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'focus') then + insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order) + values (p_tenant_id, v_id, 'focus', 'Foco', 'text', false, 20); + end if; + + if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'notes') then + insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order) + values (p_tenant_id, v_id, 'notes', 'Observação', 'textarea', false, 30); + end if; + end if; +end; +$$; + + +ALTER FUNCTION "public"."seed_determined_commitments"("p_tenant_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."set_owner_id"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +begin + if new.owner_id is null then + new.owner_id := auth.uid(); + end if; + return new; +end; +$$; + + +ALTER FUNCTION "public"."set_owner_id"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."set_tenant_feature_exception"("p_tenant_id" "uuid", "p_feature_key" "text", "p_enabled" boolean, "p_reason" "text" DEFAULT NULL::"text") RETURNS "void" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +begin + -- ✅ Só owner ou admin do tenant podem alterar features + if not exists ( + select 1 from public.tenant_members + where tenant_id = p_tenant_id + and user_id = auth.uid() + and role in ('owner', 'admin') + and status = 'active' + ) then + raise exception 'Acesso negado: apenas owner/admin pode alterar features do tenant.'; + end if; + + insert into public.tenant_features (tenant_id, feature_key, enabled) + values (p_tenant_id, p_feature_key, p_enabled) + on conflict (tenant_id, feature_key) + do update set enabled = excluded.enabled; + + insert into public.tenant_feature_exceptions_log ( + tenant_id, feature_key, enabled, reason, created_by + ) values ( + p_tenant_id, p_feature_key, p_enabled, p_reason, auth.uid() + ); +end; +$$; + + +ALTER FUNCTION "public"."set_tenant_feature_exception"("p_tenant_id" "uuid", "p_feature_key" "text", "p_enabled" boolean, "p_reason" "text") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."set_updated_at"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +begin + new.updated_at = now(); + return new; +end; +$$; + + +ALTER FUNCTION "public"."set_updated_at"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."subscription_intents_view_insert"() RETURNS "trigger" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +declare + v_target text; + v_plan_id uuid; +begin + -- descobre o plano pelo plan_key + select p.id, p.target into v_plan_id, v_target + from public.plans p + where p.key = new.plan_key; + + if v_plan_id is null then + raise exception 'Plano inválido: plan_key=%', new.plan_key; + end if; + + -- clinic → tabela tenant (tenant_id obrigatório) + if lower(v_target) = 'clinic' then + if new.tenant_id is null then + raise exception 'Intenção clinic exige tenant_id.'; + end if; + + insert into public.subscription_intents_tenant ( + id, user_id, created_by_user_id, email, + plan_id, plan_key, interval, amount_cents, currency, + status, source, notes, created_at, paid_at, + tenant_id + ) values ( + coalesce(new.id, gen_random_uuid()), + new.user_id, + new.created_by_user_id, + new.email, + v_plan_id, + new.plan_key, + new.interval, + new.amount_cents, + new.currency, + coalesce(new.status, 'new'), + coalesce(new.source, 'manual'), + new.notes, + coalesce(new.created_at, now()), + new.paid_at, + new.tenant_id + ) + returning + id, user_id, created_by_user_id, email, + plan_id, plan_key, interval, amount_cents, currency, + status, source, notes, created_at, paid_at, + tenant_id, + 'clinic'::text + into + new.id, new.user_id, new.created_by_user_id, new.email, + new.plan_id, new.plan_key, new.interval, new.amount_cents, new.currency, + new.status, new.source, new.notes, new.created_at, new.paid_at, + new.tenant_id, + new.plan_target; + + return new; + end if; + + -- therapist → tabela personal (sem tenant_id) + if lower(v_target) = 'therapist' then + insert into public.subscription_intents_personal ( + id, user_id, created_by_user_id, email, + plan_id, plan_key, interval, amount_cents, currency, + status, source, notes, created_at, paid_at + ) values ( + coalesce(new.id, gen_random_uuid()), + new.user_id, + new.created_by_user_id, + new.email, + v_plan_id, + new.plan_key, + new.interval, + new.amount_cents, + new.currency, + coalesce(new.status, 'new'), + coalesce(new.source, 'manual'), + new.notes, + coalesce(new.created_at, now()), + new.paid_at + ) + returning + id, user_id, created_by_user_id, email, + plan_id, plan_key, interval, amount_cents, currency, + status, source, notes, created_at, paid_at + into + new.id, new.user_id, new.created_by_user_id, new.email, + new.plan_id, new.plan_key, new.interval, new.amount_cents, new.currency, + new.status, new.source, new.notes, new.created_at, new.paid_at; + + new.tenant_id := null; + new.plan_target := 'therapist'; + return new; + end if; + + raise exception 'Target de plano não suportado: %', v_target; +end; +$$; + + +ALTER FUNCTION "public"."subscription_intents_view_insert"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."subscriptions_validate_scope"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +DECLARE + v_target text; +BEGIN + SELECT lower(p.target) INTO v_target + FROM public.plans p + WHERE p.id = NEW.plan_id; + + IF v_target IS NULL THEN + RAISE EXCEPTION 'Plano inválido (target nulo).'; + END IF; + + IF v_target = 'clinic' THEN + IF NEW.tenant_id IS NULL THEN + RAISE EXCEPTION 'Assinatura clinic exige tenant_id.'; + END IF; + IF NEW.user_id IS NOT NULL THEN + RAISE EXCEPTION 'Assinatura clinic não pode ter user_id (XOR).'; + END IF; + + ELSIF v_target = 'therapist' THEN + IF NEW.tenant_id IS NOT NULL THEN + RAISE EXCEPTION 'Assinatura therapist não deve ter tenant_id.'; + END IF; + IF NEW.user_id IS NULL THEN + RAISE EXCEPTION 'Assinatura therapist exige user_id.'; + END IF; + + ELSIF v_target = 'patient' THEN + IF NEW.tenant_id IS NOT NULL THEN + RAISE EXCEPTION 'Assinatura patient não deve ter tenant_id.'; + END IF; + IF NEW.user_id IS NULL THEN + RAISE EXCEPTION 'Assinatura patient exige user_id.'; + END IF; + + ELSE + RAISE EXCEPTION 'Target de plano inválido: %', v_target; + END IF; + + RETURN NEW; +END; +$$; + + +ALTER FUNCTION "public"."subscriptions_validate_scope"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."sync_busy_mirror_agenda_eventos"() RETURNS "trigger" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public' + AS $$ +declare + clinic_tenant uuid; + is_personal boolean; + should_mirror boolean; +begin + -- Anti-recursão: espelho não espelha + if (tg_op <> 'DELETE') then + if new.mirror_of_event_id is not null then + return new; + end if; + else + if old.mirror_of_event_id is not null then + return old; + end if; + end if; + + -- Define se é pessoal e se deve espelhar + if (tg_op = 'DELETE') then + is_personal := (old.tenant_id = old.owner_id); + should_mirror := (old.visibility_scope in ('busy_only','private')); + else + is_personal := (new.tenant_id = new.owner_id); + should_mirror := (new.visibility_scope in ('busy_only','private')); + end if; + + -- Se não é pessoal, não faz nada + if not is_personal then + if (tg_op = 'DELETE') then + return old; + end if; + return new; + end if; + + -- DELETE: remove espelhos existentes + if (tg_op = 'DELETE') then + delete from public.agenda_eventos e + where e.mirror_of_event_id = old.id + and e.mirror_source = 'personal_busy_mirror'; + + return old; + end if; + + -- INSERT/UPDATE: + -- Se não deve espelhar, remove espelhos e sai + if not should_mirror then + delete from public.agenda_eventos e + where e.mirror_of_event_id = new.id + and e.mirror_source = 'personal_busy_mirror'; + + return new; + end if; + + -- Para cada clínica onde o usuário é therapist active, cria/atualiza o "Ocupado" + for clinic_tenant in + select tm.tenant_id + from public.tenant_members tm + where tm.user_id = new.owner_id + and tm.role = 'therapist' + and tm.status = 'active' + and tm.tenant_id <> new.owner_id + loop + insert into public.agenda_eventos ( + tenant_id, + owner_id, + terapeuta_id, + paciente_id, + tipo, + status, + titulo, + observacoes, + inicio_em, + fim_em, + mirror_of_event_id, + mirror_source, + visibility_scope, + created_at, + updated_at + ) values ( + clinic_tenant, + new.owner_id, + new.owner_id, + null, + 'bloqueio'::public.tipo_evento_agenda, + 'agendado'::public.status_evento_agenda, + 'Ocupado', + null, + new.inicio_em, + new.fim_em, + new.id, + 'personal_busy_mirror', + 'public', + now(), + now() + ) + on conflict (tenant_id, mirror_of_event_id) where mirror_of_event_id is not null + do update set + owner_id = excluded.owner_id, + terapeuta_id = excluded.terapeuta_id, + tipo = excluded.tipo, + status = excluded.status, + titulo = excluded.titulo, + observacoes = excluded.observacoes, + inicio_em = excluded.inicio_em, + fim_em = excluded.fim_em, + updated_at = now(); + end loop; + + -- Limpa espelhos de clínicas onde o vínculo therapist active não existe mais + delete from public.agenda_eventos e + where e.mirror_of_event_id = new.id + and e.mirror_source = 'personal_busy_mirror' + and not exists ( + select 1 + from public.tenant_members tm + where tm.user_id = new.owner_id + and tm.role = 'therapist' + and tm.status = 'active' + and tm.tenant_id = e.tenant_id + ); + + return new; +end; +$$; + + +ALTER FUNCTION "public"."sync_busy_mirror_agenda_eventos"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."tenant_accept_invite"("p_token" "uuid") RETURNS "jsonb" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'auth' + AS $$ +declare + v_uid uuid; + v_email text; + v_invite public.tenant_invites%rowtype; +begin + -- 1) precisa estar autenticado + v_uid := auth.uid(); + if v_uid is null then + raise exception 'not_authenticated' using errcode = 'P0001'; + end if; + + -- 2) pega email real do usuário logado sem depender do JWT claim + select u.email + into v_email + from auth.users u + where u.id = v_uid; + + if v_email is null or length(trim(v_email)) = 0 then + raise exception 'missing_user_email' using errcode = 'P0001'; + end if; + + -- 3) carrega o invite e trava linha (evita 2 aceites concorrentes) + select * + into v_invite + from public.tenant_invites i + where i.token = p_token + for update; + + if not found then + raise exception 'invite_not_found' using errcode = 'P0001'; + end if; + + -- 4) validações de estado + if v_invite.revoked_at is not null then + raise exception 'invite_revoked' using errcode = 'P0001'; + end if; + + if v_invite.accepted_at is not null then + raise exception 'invite_already_accepted' using errcode = 'P0001'; + end if; + + if v_invite.expires_at is not null and v_invite.expires_at <= now() then + raise exception 'invite_expired' using errcode = 'P0001'; + end if; + + -- 5) valida email (case-insensitive) + if lower(trim(v_invite.email)) <> lower(trim(v_email)) then + raise exception 'email_mismatch' using errcode = 'P0001'; + end if; + + -- 6) consome o invite + update public.tenant_invites + set accepted_at = now(), + accepted_by = v_uid + where id = v_invite.id; + + -- 7) cria ou reativa o membership + insert into public.tenant_members (tenant_id, user_id, role, status, created_at) + values (v_invite.tenant_id, v_uid, v_invite.role, 'active', now()) + on conflict (tenant_id, user_id) + do update set + role = excluded.role, + status = 'active'; + + -- 8) retorno útil pro front (você já tenta ler tenant_id no AcceptInvitePage) + return jsonb_build_object( + 'ok', true, + 'tenant_id', v_invite.tenant_id, + 'role', v_invite.role + ); +end; +$$; + + +ALTER FUNCTION "public"."tenant_accept_invite"("p_token" "uuid") OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."tenant_members" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "tenant_id" "uuid" NOT NULL, + "user_id" "uuid" NOT NULL, + "role" "text" NOT NULL, + "status" "text" DEFAULT 'active'::"text" NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."tenant_members" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."tenant_add_member_by_email"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text" DEFAULT 'therapist'::"text") RETURNS "public"."tenant_members" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'auth' + AS $$ +declare + v_target_uid uuid; + v_member public.tenant_members%rowtype; + v_is_admin boolean; + v_email text; +begin + if p_tenant_id is null then + raise exception 'tenant_id é obrigatório'; + end if; + + v_email := lower(trim(coalesce(p_email, ''))); + if v_email = '' then + raise exception 'email é obrigatório'; + end if; + + -- valida role permitida + if p_role not in ('tenant_admin','therapist','secretary','patient') then + raise exception 'role inválida: %', p_role; + end if; + + -- apenas admin do tenant (role real no banco) + select exists ( + select 1 + from public.tenant_members tm + where tm.tenant_id = p_tenant_id + and tm.user_id = auth.uid() + and tm.role = 'tenant_admin' + and coalesce(tm.status,'active') = 'active' + ) into v_is_admin; + + if not v_is_admin then + raise exception 'sem permissão: apenas admin da clínica pode adicionar membros'; + end if; + + -- acha usuário pelo e-mail no Supabase Auth + select u.id + into v_target_uid + from auth.users u + where lower(u.email) = v_email + limit 1; + + if v_target_uid is null then + raise exception 'nenhum usuário encontrado com este e-mail'; + end if; + + -- cria ou reativa membro + insert into public.tenant_members (tenant_id, user_id, role, status) + values (p_tenant_id, v_target_uid, p_role, 'active') + on conflict (tenant_id, user_id) + do update set + role = excluded.role, + status = 'active' + returning * into v_member; + + return v_member; +end; +$$; + + +ALTER FUNCTION "public"."tenant_add_member_by_email"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."tenant_feature_allowed"("p_tenant_id" "uuid", "p_feature_key" "text") RETURNS boolean + LANGUAGE "sql" STABLE + AS $$ + select exists ( + select 1 + from public.v_tenant_entitlements v + where v.tenant_id = p_tenant_id + and v.feature_key = p_feature_key + and coalesce(v.allowed, false) = true + ); +$$; + + +ALTER FUNCTION "public"."tenant_feature_allowed"("p_tenant_id" "uuid", "p_feature_key" "text") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."tenant_feature_enabled"("p_tenant_id" "uuid", "p_feature_key" "text") RETURNS boolean + LANGUAGE "sql" STABLE + AS $$ + select coalesce( + (select tf.enabled + from public.tenant_features tf + where tf.tenant_id = p_tenant_id and tf.feature_key = p_feature_key), + false + ); +$$; + + +ALTER FUNCTION "public"."tenant_feature_enabled"("p_tenant_id" "uuid", "p_feature_key" "text") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."tenant_features_guard_with_plan"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +declare + v_allowed boolean; +begin + -- só valida quando está habilitando + if new.enabled is distinct from true then + return new; + end if; + + -- permitido pelo plano do tenant? + select exists ( + select 1 + from public.v_tenant_entitlements_full v + where v.tenant_id = new.tenant_id + and v.feature_key = new.feature_key + and v.allowed = true + ) + into v_allowed; + + if not v_allowed then + raise exception 'Feature % não permitida pelo plano atual do tenant %.', + new.feature_key, new.tenant_id + using errcode = 'P0001'; + end if; + + return new; +end; +$$; + + +ALTER FUNCTION "public"."tenant_features_guard_with_plan"() OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."tenant_has_feature"("_tenant_id" "uuid", "_feature" "text") RETURNS boolean + LANGUAGE "sql" STABLE + AS $$ + select + exists ( + select 1 + from public.v_tenant_entitlements e + where e.tenant_id = _tenant_id + and e.feature_key = _feature + and e.allowed = true + ) + or exists ( + select 1 + from public.tenant_features tf + where tf.tenant_id = _tenant_id + and tf.feature_key = _feature + and tf.enabled = true + ); +$$; + + +ALTER FUNCTION "public"."tenant_has_feature"("_tenant_id" "uuid", "_feature" "text") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."tenant_invite_member_by_email"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") RETURNS "uuid" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'auth' + AS $$ +declare + v_email text; + v_my_email text; + v_token uuid; + v_updated int; +begin + -- validações básicas + if p_tenant_id is null then + raise exception 'tenant_id inválido' using errcode = 'P0001'; + end if; + + v_email := lower(trim(coalesce(p_email, ''))); + if v_email = '' then + raise exception 'Informe um email' using errcode = 'P0001'; + end if; + + -- role permitido (ajuste se quiser) + if p_role is null or p_role not in ('therapist', 'secretary') then + raise exception 'Role inválido (use therapist/secretary)' using errcode = 'P0001'; + end if; + + -- ✅ bloqueio: auto-convite + v_my_email := public.get_my_email(); + if v_my_email is not null and v_email = v_my_email then + raise exception 'Você não pode convidar o seu próprio email.' using errcode = 'P0001'; + end if; + + -- ✅ bloqueio: já é membro ativo do tenant + if exists ( + select 1 + from tenant_members tm + join auth.users au on au.id = tm.user_id + where tm.tenant_id = p_tenant_id + and tm.status = 'active' + and lower(au.email) = v_email + ) then + raise exception 'Este email já está vinculado a esta clínica.' using errcode = 'P0001'; + end if; + + -- ✅ permissão: só admin do tenant pode convidar + if not exists ( + select 1 + from tenant_members me + where me.tenant_id = p_tenant_id + and me.user_id = auth.uid() + and me.status = 'active' + and me.role in ('tenant_admin','clinic_admin') + ) then + raise exception 'Sem permissão para convidar membros.' using errcode = 'P0001'; + end if; + + -- Gera token (reenvio simples / regeneração) + v_token := gen_random_uuid(); + + -- 1) tenta "regerar" um convite pendente existente (mesmo email) + update tenant_invites + set token = v_token, + role = p_role, + created_at = now(), + expires_at = now() + interval '7 days', + accepted_at = null, + revoked_at = null + where tenant_id = p_tenant_id + and lower(email) = v_email + and accepted_at is null + and revoked_at is null; + + get diagnostics v_updated = row_count; + + -- 2) se não atualizou nada, cria convite novo + if v_updated = 0 then + insert into tenant_invites (tenant_id, email, role, token, created_at, expires_at) + values (p_tenant_id, v_email, p_role, v_token, now(), now() + interval '7 days'); + end if; + + return v_token; +end; +$$; + + +ALTER FUNCTION "public"."tenant_invite_member_by_email"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."tenant_reactivate_member"("p_tenant_id" "uuid", "p_member_user_id" "uuid") RETURNS "void" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public' + SET "row_security" TO 'off' + AS $$ +begin + if auth.uid() is null then + raise exception 'not_authenticated'; + end if; + + if not public.is_tenant_admin(p_tenant_id) then + raise exception 'not_allowed'; + end if; + + update public.tenant_members + set status = 'active' + where tenant_id = p_tenant_id + and user_id = p_member_user_id; + + if not found then + raise exception 'membership_not_found'; + end if; +end; +$$; + + +ALTER FUNCTION "public"."tenant_reactivate_member"("p_tenant_id" "uuid", "p_member_user_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."tenant_remove_member"("p_tenant_id" "uuid", "p_member_user_id" "uuid") RETURNS "void" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public' + SET "row_security" TO 'off' + AS $$ +declare + v_role text; +begin + if auth.uid() is null then + raise exception 'not_authenticated'; + end if; + + if not public.is_tenant_admin(p_tenant_id) then + raise exception 'not_allowed'; + end if; + + if p_member_user_id = auth.uid() then + raise exception 'cannot_remove_self'; + end if; + + -- pega role atual do membro (se não existir, erro) + select role into v_role + from public.tenant_members + where tenant_id = p_tenant_id + and user_id = p_member_user_id; + + if v_role is null then + raise exception 'membership_not_found'; + end if; + + -- trava: se for therapist, não pode remover com eventos futuros + if v_role = 'therapist' then + if exists ( + select 1 + from public.agenda_eventos e + where e.owner_id = p_tenant_id + and e.terapeuta_id = p_member_user_id + and e.inicio_em >= now() + and e.status::text not in ('cancelado','cancelled','canceled') + limit 1 + ) then + raise exception 'cannot_remove_therapist_with_future_events'; + end if; + end if; + + update public.tenant_members + set status = 'inactive' + where tenant_id = p_tenant_id + and user_id = p_member_user_id; + + if not found then + raise exception 'membership_not_found'; + end if; +end; +$$; + + +ALTER FUNCTION "public"."tenant_remove_member"("p_tenant_id" "uuid", "p_member_user_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."tenant_remove_member_soft"("p_tenant_id" "uuid", "p_member_user_id" "uuid") RETURNS "void" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public' + SET "row_security" TO 'off' + AS $$ +begin + if auth.uid() is null then + raise exception 'not_authenticated'; + end if; + + if not public.is_tenant_admin(p_tenant_id) then + raise exception 'not_allowed'; + end if; + + if p_member_user_id = auth.uid() then + raise exception 'cannot_remove_self'; + end if; + + update public.tenant_members + set status = 'inactive' + where tenant_id = p_tenant_id + and user_id = p_member_user_id; + + if not found then + raise exception 'membership_not_found'; + end if; +end; +$$; + + +ALTER FUNCTION "public"."tenant_remove_member_soft"("p_tenant_id" "uuid", "p_member_user_id" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."tenant_revoke_invite"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") RETURNS "void" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public' + SET "row_security" TO 'off' + AS $$ +declare + v_email text; +begin + if auth.uid() is null then + raise exception 'not_authenticated'; + end if; + + if not public.is_tenant_admin(p_tenant_id) then + raise exception 'not_allowed'; + end if; + + v_email := lower(trim(p_email)); + + update public.tenant_invites + set revoked_at = now(), + revoked_by = auth.uid() + where tenant_id = p_tenant_id + and lower(email) = v_email + and role = p_role + and accepted_at is null + and revoked_at is null; + + if not found then + raise exception 'invite_not_found'; + end if; +end; +$$; + + +ALTER FUNCTION "public"."tenant_revoke_invite"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."tenant_set_member_status"("p_tenant_id" "uuid", "p_member_user_id" "uuid", "p_new_status" "text") RETURNS "void" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public' + SET "row_security" TO 'off' + AS $$ +begin + if auth.uid() is null then + raise exception 'not_authenticated'; + end if; + + -- valida status (adapte aos seus valores reais) + if p_new_status not in ('active','inactive','suspended','invited') then + raise exception 'invalid_status: %', p_new_status; + end if; + + if not public.is_tenant_admin(p_tenant_id) then + raise exception 'not_allowed'; + end if; + + -- evita desativar a si mesmo (opcional) + if p_member_user_id = auth.uid() and p_new_status <> 'active' then + raise exception 'cannot_disable_self'; + end if; + + update public.tenant_members + set status = p_new_status + where tenant_id = p_tenant_id + and user_id = p_member_user_id; + + if not found then + raise exception 'membership_not_found'; + end if; +end; +$$; + + +ALTER FUNCTION "public"."tenant_set_member_status"("p_tenant_id" "uuid", "p_member_user_id" "uuid", "p_new_status" "text") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."tenant_update_member_role"("p_tenant_id" "uuid", "p_member_user_id" "uuid", "p_new_role" "text") RETURNS "void" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public' + SET "row_security" TO 'off' + AS $$ +begin + -- exige auth + if auth.uid() is null then + raise exception 'not_authenticated'; + end if; + + -- valida role + if p_new_role not in ('tenant_admin','therapist','secretary','patient') then + raise exception 'invalid_role: %', p_new_role; + end if; + + -- somente tenant_admin ativo pode alterar role + if not public.is_tenant_admin(p_tenant_id) then + raise exception 'not_allowed'; + end if; + + -- evita o admin remover o próprio admin sem querer (opcional mas recomendado) + if p_member_user_id = auth.uid() and p_new_role <> 'tenant_admin' then + raise exception 'cannot_demote_self'; + end if; + + update public.tenant_members + set role = p_new_role + where tenant_id = p_tenant_id + and user_id = p_member_user_id; + + if not found then + raise exception 'membership_not_found'; + end if; +end; +$$; + + +ALTER FUNCTION "public"."tenant_update_member_role"("p_tenant_id" "uuid", "p_member_user_id" "uuid", "p_new_role" "text") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."toggle_plan"("owner" "uuid") RETURNS "void" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +declare + current_key text; + new_key text; +begin + select p.key into current_key + from subscriptions s + join plans p on p.id = s.plan_id + where s.owner_id = owner + and s.status = 'active'; + + new_key := case + when current_key = 'pro' then 'free' + else 'pro' + end; + + update subscriptions s + set plan_id = p.id + from plans p + where p.key = new_key + and s.owner_id = owner + and s.status = 'active'; +end; +$$; + + +ALTER FUNCTION "public"."toggle_plan"("owner" "uuid") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."transition_subscription"("p_subscription_id" "uuid", "p_to_status" "text", "p_reason" "text" DEFAULT NULL::"text", "p_metadata" "jsonb" DEFAULT NULL::"jsonb") RETURNS "public"."subscriptions" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +declare + v_sub public.subscriptions; + v_uid uuid; + v_is_allowed boolean := false; +begin + v_uid := auth.uid(); + + select * + into v_sub + from public.subscriptions + where id = p_subscription_id; + + if not found then + raise exception 'Assinatura não encontrada'; + end if; + + -- ===================================================== + -- 🔐 BLOCO DE AUTORIZAÇÃO + -- ===================================================== + + -- 1) SaaS admin pode tudo + if is_saas_admin() then + v_is_allowed := true; + end if; + + -- 2) Assinatura pessoal (therapist) + if not v_is_allowed + and v_sub.tenant_id is null + and v_sub.user_id = v_uid then + v_is_allowed := true; + end if; + + -- 3) Assinatura de clinic (tenant) + if not v_is_allowed + and v_sub.tenant_id is not null then + + if exists ( + select 1 + from public.tenant_members tm + where tm.tenant_id = v_sub.tenant_id + and tm.user_id = v_uid + and tm.status = 'active' + and tm.role = 'tenant_admin' + ) then + v_is_allowed := true; + end if; + + end if; + + if not v_is_allowed then + raise exception 'Sem permissão para transicionar esta assinatura'; + end if; + + -- ===================================================== + -- 🧠 TRANSIÇÃO + -- ===================================================== + + update public.subscriptions + set status = p_to_status, + updated_at = now(), + cancelled_at = case when p_to_status = 'cancelled' then now() else cancelled_at end, + suspended_at = case when p_to_status = 'suspended' then now() else suspended_at end, + past_due_since = case when p_to_status = 'past_due' then now() else past_due_since end, + expired_at = case when p_to_status = 'expired' then now() else expired_at end, + activated_at = case when p_to_status = 'active' then now() else activated_at end + where id = p_subscription_id + returning * into v_sub; + + -- ===================================================== + -- 🧾 EVENT LOG + -- ===================================================== + + insert into public.subscription_events ( + subscription_id, + owner_id, + event_type, + created_at, + created_by, + source, + reason, + metadata, + owner_type, + owner_ref + ) + values ( + v_sub.id, + coalesce(v_sub.tenant_id, v_sub.user_id), + 'status_changed', + now(), + v_uid, + 'manual_transition', + p_reason, + p_metadata, + case when v_sub.tenant_id is not null then 'tenant' else 'personal' end, + coalesce(v_sub.tenant_id, v_sub.user_id) + ); + + return v_sub; +end; +$$; + + +ALTER FUNCTION "public"."transition_subscription"("p_subscription_id" "uuid", "p_to_status" "text", "p_reason" "text", "p_metadata" "jsonb") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."user_has_feature"("_user_id" "uuid", "_feature" "text") RETURNS boolean + LANGUAGE "sql" STABLE + AS $$ + select exists ( + select 1 + from public.v_user_entitlements e + where e.user_id = _user_id + and e.feature_key = _feature + and e.allowed = true + ); +$$; + + +ALTER FUNCTION "public"."user_has_feature"("_user_id" "uuid", "_feature" "text") OWNER TO "supabase_admin"; + + +CREATE OR REPLACE FUNCTION "public"."whoami"() RETURNS TABLE("uid" "uuid", "role" "text") + LANGUAGE "sql" STABLE + AS $$ + select auth.uid() as uid, auth.role() as role; +$$; + + +ALTER FUNCTION "public"."whoami"() OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."agenda_configuracoes" ( + "owner_id" "uuid" NOT NULL, + "duracao_padrao_minutos" integer DEFAULT 50 NOT NULL, + "intervalo_padrao_minutos" integer DEFAULT 0 NOT NULL, + "timezone" "text" DEFAULT 'America/Sao_Paulo'::"text" NOT NULL, + "usar_horario_admin_custom" boolean DEFAULT false NOT NULL, + "admin_inicio_visualizacao" time without time zone, + "admin_fim_visualizacao" time without time zone, + "admin_slot_visual_minutos" integer DEFAULT 30 NOT NULL, + "online_ativo" boolean DEFAULT false NOT NULL, + "online_min_antecedencia_horas" integer DEFAULT 24 NOT NULL, + "online_max_dias_futuro" integer DEFAULT 60 NOT NULL, + "online_cancelar_ate_horas" integer DEFAULT 12 NOT NULL, + "online_reagendar_ate_horas" integer DEFAULT 12 NOT NULL, + "online_limite_agendamentos_futuros" integer DEFAULT 1 NOT NULL, + "online_modo" "text" DEFAULT 'automatico'::"text" NOT NULL, + "online_buffer_antes_min" integer DEFAULT 0 NOT NULL, + "online_buffer_depois_min" integer DEFAULT 0 NOT NULL, + "online_modalidade" "text" DEFAULT 'ambos'::"text" NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "usar_granularidade_custom" boolean DEFAULT false NOT NULL, + "granularidade_min" integer, + "setup_concluido" boolean DEFAULT false NOT NULL, + "setup_concluido_em" timestamp with time zone, + "agenda_view_mode" "text" DEFAULT 'full_24h'::"text" NOT NULL, + "agenda_custom_start" time without time zone, + "agenda_custom_end" time without time zone, + "session_duration_min" integer DEFAULT 50 NOT NULL, + "session_break_min" integer DEFAULT 10 NOT NULL, + "session_start_offset_min" integer DEFAULT 0 NOT NULL, + "pausas_semanais" "jsonb" DEFAULT '[]'::"jsonb" NOT NULL, + "setup_clinica_concluido" boolean DEFAULT false NOT NULL, + "setup_clinica_concluido_em" timestamp with time zone, + "jornada_igual_todos" boolean DEFAULT true, + "tenant_id" "uuid", + CONSTRAINT "agenda_configuracoes_admin_slot_visual_minutos_check" CHECK (("admin_slot_visual_minutos" = ANY (ARRAY[5, 10, 15, 20, 30, 60]))), + CONSTRAINT "agenda_configuracoes_check" CHECK ((("usar_horario_admin_custom" = false) OR (("admin_inicio_visualizacao" IS NOT NULL) AND ("admin_fim_visualizacao" IS NOT NULL) AND ("admin_fim_visualizacao" > "admin_inicio_visualizacao")))), + CONSTRAINT "agenda_configuracoes_duracao_padrao_minutos_check" CHECK ((("duracao_padrao_minutos" >= 10) AND ("duracao_padrao_minutos" <= 240))), + CONSTRAINT "agenda_configuracoes_granularidade_min_check" CHECK ((("granularidade_min" IS NULL) OR ("granularidade_min" = ANY (ARRAY[5, 10, 15, 20, 30, 45, 50, 60])))), + CONSTRAINT "agenda_configuracoes_intervalo_padrao_minutos_check" CHECK ((("intervalo_padrao_minutos" >= 0) AND ("intervalo_padrao_minutos" <= 120))), + CONSTRAINT "agenda_configuracoes_online_buffer_antes_min_check" CHECK ((("online_buffer_antes_min" >= 0) AND ("online_buffer_antes_min" <= 120))), + CONSTRAINT "agenda_configuracoes_online_buffer_depois_min_check" CHECK ((("online_buffer_depois_min" >= 0) AND ("online_buffer_depois_min" <= 120))), + CONSTRAINT "agenda_configuracoes_online_cancelar_ate_horas_check" CHECK ((("online_cancelar_ate_horas" >= 0) AND ("online_cancelar_ate_horas" <= 720))), + CONSTRAINT "agenda_configuracoes_online_limite_agendamentos_futuros_check" CHECK ((("online_limite_agendamentos_futuros" >= 1) AND ("online_limite_agendamentos_futuros" <= 10))), + CONSTRAINT "agenda_configuracoes_online_max_dias_futuro_check" CHECK ((("online_max_dias_futuro" >= 1) AND ("online_max_dias_futuro" <= 365))), + CONSTRAINT "agenda_configuracoes_online_min_antecedencia_horas_check" CHECK ((("online_min_antecedencia_horas" >= 0) AND ("online_min_antecedencia_horas" <= 720))), + CONSTRAINT "agenda_configuracoes_online_modalidade_check" CHECK (("online_modalidade" = ANY (ARRAY['online'::"text", 'presencial'::"text", 'ambos'::"text"]))), + CONSTRAINT "agenda_configuracoes_online_modo_check" CHECK (("online_modo" = ANY (ARRAY['automatico'::"text", 'aprovacao'::"text"]))), + CONSTRAINT "agenda_configuracoes_online_reagendar_ate_horas_check" CHECK ((("online_reagendar_ate_horas" >= 0) AND ("online_reagendar_ate_horas" <= 720))), + CONSTRAINT "session_break_min_chk" CHECK ((("session_break_min" >= 0) AND ("session_break_min" <= 60))), + CONSTRAINT "session_duration_min_chk" CHECK ((("session_duration_min" >= 10) AND ("session_duration_min" <= 240))), + CONSTRAINT "session_start_offset_min_chk" CHECK (("session_start_offset_min" = ANY (ARRAY[0, 15, 30, 45]))) +); + + +ALTER TABLE "public"."agenda_configuracoes" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."agenda_eventos" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "owner_id" "uuid" NOT NULL, + "paciente_id" "uuid", + "tipo" "public"."tipo_evento_agenda" DEFAULT 'sessao'::"public"."tipo_evento_agenda" NOT NULL, + "status" "public"."status_evento_agenda" DEFAULT 'agendado'::"public"."status_evento_agenda" NOT NULL, + "titulo" "text", + "observacoes" "text", + "inicio_em" timestamp with time zone NOT NULL, + "fim_em" timestamp with time zone NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "terapeuta_id" "uuid", + "tenant_id" "uuid" NOT NULL, + "visibility_scope" "text" DEFAULT 'public'::"text" NOT NULL, + "mirror_of_event_id" "uuid", + "mirror_source" "text", + "patient_id" "uuid", + "determined_commitment_id" "uuid", + "link_online" "text", + CONSTRAINT "agenda_eventos_check" CHECK (("fim_em" > "inicio_em")) +); + + +ALTER TABLE "public"."agenda_eventos" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."agenda_excecoes" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "owner_id" "uuid" NOT NULL, + "data" "date" NOT NULL, + "hora_inicio" time without time zone, + "hora_fim" time without time zone, + "tipo" "public"."tipo_excecao_agenda" NOT NULL, + "motivo" "text", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "status" "public"."status_excecao_agenda" DEFAULT 'ativo'::"public"."status_excecao_agenda" NOT NULL, + "fonte" "text" DEFAULT 'manual'::"text" NOT NULL, + "aplicavel_online" boolean DEFAULT true NOT NULL, + "tenant_id" "uuid" NOT NULL, + CONSTRAINT "agenda_excecoes_check" CHECK (((("hora_inicio" IS NULL) AND ("hora_fim" IS NULL)) OR (("hora_inicio" IS NOT NULL) AND ("hora_fim" IS NOT NULL) AND ("hora_fim" > "hora_inicio")))), + CONSTRAINT "agenda_excecoes_fonte_check" CHECK (("fonte" = ANY (ARRAY['manual'::"text", 'feriado_google'::"text", 'sistema'::"text"]))) +); + + +ALTER TABLE "public"."agenda_excecoes" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."agenda_online_slots" ( + "id" bigint NOT NULL, + "owner_id" "uuid" NOT NULL, + "weekday" integer NOT NULL, + "time" time without time zone NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "tenant_id" "uuid" NOT NULL, + CONSTRAINT "agenda_online_slots_weekday_check" CHECK (("weekday" = ANY (ARRAY[0, 1, 2, 3, 4, 5, 6]))) +); + + +ALTER TABLE "public"."agenda_online_slots" OWNER TO "supabase_admin"; + + +CREATE SEQUENCE IF NOT EXISTS "public"."agenda_online_slots_id_seq" + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE "public"."agenda_online_slots_id_seq" OWNER TO "supabase_admin"; + + +ALTER SEQUENCE "public"."agenda_online_slots_id_seq" OWNED BY "public"."agenda_online_slots"."id"; + + + +CREATE TABLE IF NOT EXISTS "public"."agenda_regras_semanais" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "owner_id" "uuid" NOT NULL, + "dia_semana" smallint NOT NULL, + "hora_inicio" time without time zone NOT NULL, + "hora_fim" time without time zone NOT NULL, + "modalidade" "text" DEFAULT 'ambos'::"text" NOT NULL, + "ativo" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "tenant_id" "uuid" NOT NULL, + CONSTRAINT "agenda_regras_semanais_check" CHECK (("hora_fim" > "hora_inicio")), + CONSTRAINT "agenda_regras_semanais_dia_semana_check" CHECK ((("dia_semana" >= 0) AND ("dia_semana" <= 6))), + CONSTRAINT "agenda_regras_semanais_modalidade_check" CHECK ((("modalidade" = ANY (ARRAY['online'::"text", 'presencial'::"text", 'ambos'::"text"])) OR ("modalidade" IS NULL))) +); + + +ALTER TABLE "public"."agenda_regras_semanais" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."agenda_slots_bloqueados_semanais" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "owner_id" "uuid" NOT NULL, + "dia_semana" smallint NOT NULL, + "hora_inicio" time without time zone NOT NULL, + "motivo" "text", + "ativo" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "tenant_id" "uuid" NOT NULL, + CONSTRAINT "agenda_slots_bloqueados_semanais_dia_semana_check" CHECK ((("dia_semana" >= 0) AND ("dia_semana" <= 6))) +); + + +ALTER TABLE "public"."agenda_slots_bloqueados_semanais" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."agenda_slots_regras" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "owner_id" "uuid" NOT NULL, + "dia_semana" smallint NOT NULL, + "passo_minutos" integer NOT NULL, + "offset_minutos" integer DEFAULT 0 NOT NULL, + "buffer_antes_min" integer DEFAULT 0 NOT NULL, + "buffer_depois_min" integer DEFAULT 0 NOT NULL, + "min_antecedencia_horas" integer DEFAULT 0 NOT NULL, + "ativo" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "tenant_id" "uuid" NOT NULL, + CONSTRAINT "agenda_slots_regras_buffer_antes_min_check" CHECK ((("buffer_antes_min" >= 0) AND ("buffer_antes_min" <= 240))), + CONSTRAINT "agenda_slots_regras_buffer_depois_min_check" CHECK ((("buffer_depois_min" >= 0) AND ("buffer_depois_min" <= 240))), + CONSTRAINT "agenda_slots_regras_dia_semana_check" CHECK ((("dia_semana" >= 0) AND ("dia_semana" <= 6))), + CONSTRAINT "agenda_slots_regras_min_antecedencia_horas_check" CHECK ((("min_antecedencia_horas" >= 0) AND ("min_antecedencia_horas" <= 720))), + CONSTRAINT "agenda_slots_regras_offset_minutos_check" CHECK ((("offset_minutos" >= 0) AND ("offset_minutos" <= 55))), + CONSTRAINT "agenda_slots_regras_passo_minutos_check" CHECK ((("passo_minutos" >= 5) AND ("passo_minutos" <= 240))) +); + + +ALTER TABLE "public"."agenda_slots_regras" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."commitment_time_logs" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "tenant_id" "uuid" NOT NULL, + "commitment_id" "uuid" NOT NULL, + "calendar_event_id" "uuid", + "source" "public"."commitment_log_source" DEFAULT 'manual'::"public"."commitment_log_source" NOT NULL, + "started_at" timestamp with time zone NOT NULL, + "ended_at" timestamp with time zone NOT NULL, + "minutes" integer NOT NULL, + "created_by" "uuid", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."commitment_time_logs" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."current_tenant_id" AS + SELECT "current_setting"('request.jwt.claim.tenant_id'::"text", true) AS "current_setting"; + + +ALTER VIEW "public"."current_tenant_id" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."determined_commitment_fields" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "tenant_id" "uuid" NOT NULL, + "commitment_id" "uuid" NOT NULL, + "key" "text" NOT NULL, + "label" "text" NOT NULL, + "field_type" "public"."determined_field_type" DEFAULT 'text'::"public"."determined_field_type" NOT NULL, + "required" boolean DEFAULT false NOT NULL, + "sort_order" integer DEFAULT 0 NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."determined_commitment_fields" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."determined_commitments" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "tenant_id" "uuid" NOT NULL, + "created_by" "uuid", + "is_native" boolean DEFAULT false NOT NULL, + "native_key" "text", + "is_locked" boolean DEFAULT false NOT NULL, + "active" boolean DEFAULT true NOT NULL, + "name" "text" NOT NULL, + "description" "text", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."determined_commitments" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."dev_user_credentials" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "user_id" "uuid", + "email" "text" NOT NULL, + "password_dev" "text" NOT NULL, + "kind" "text" DEFAULT 'custom'::"text" NOT NULL, + "note" "text", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."dev_user_credentials" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."entitlements_invalidation" ( + "owner_id" "uuid" NOT NULL, + "changed_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."entitlements_invalidation" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."features" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "key" "text" NOT NULL, + "description" "text", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "descricao" "text" DEFAULT ''::"text" NOT NULL, + "name" "text" DEFAULT ''::"text" NOT NULL +); + + +ALTER TABLE "public"."features" OWNER TO "supabase_admin"; + + +COMMENT ON COLUMN "public"."features"."descricao" IS 'Descrição humana da feature (exibição no admin e documentação).'; + + + +CREATE TABLE IF NOT EXISTS "public"."module_features" ( + "module_id" "uuid" NOT NULL, + "feature_id" "uuid" NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "limits" "jsonb", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."module_features" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."modules" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "key" "text" NOT NULL, + "name" "text" NOT NULL, + "description" "text", + "is_active" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."modules" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."plan_features" ( + "plan_id" "uuid" NOT NULL, + "feature_id" "uuid" NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "limits" "jsonb", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."plan_features" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."tenant_modules" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "owner_id" "uuid" NOT NULL, + "module_id" "uuid" NOT NULL, + "status" "text" DEFAULT 'active'::"text" NOT NULL, + "settings" "jsonb", + "provider" "text" DEFAULT 'manual'::"text" NOT NULL, + "provider_item_id" "text", + "installed_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."tenant_modules" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."owner_feature_entitlements" AS + WITH "base" AS ( + SELECT "s"."user_id" AS "owner_id", + "f"."key" AS "feature_key", + "pf"."limits", + 'plan'::"text" AS "source" + FROM (("public"."subscriptions" "s" + JOIN "public"."plan_features" "pf" ON ((("pf"."plan_id" = "s"."plan_id") AND ("pf"."enabled" = true)))) + JOIN "public"."features" "f" ON (("f"."id" = "pf"."feature_id"))) + WHERE (("s"."status" = 'active'::"text") AND ("s"."user_id" IS NOT NULL)) + UNION ALL + SELECT "tm"."owner_id", + "f"."key" AS "feature_key", + "mf"."limits", + 'module'::"text" AS "source" + FROM ((("public"."tenant_modules" "tm" + JOIN "public"."modules" "m" ON ((("m"."id" = "tm"."module_id") AND ("m"."is_active" = true)))) + JOIN "public"."module_features" "mf" ON ((("mf"."module_id" = "m"."id") AND ("mf"."enabled" = true)))) + JOIN "public"."features" "f" ON (("f"."id" = "mf"."feature_id"))) + WHERE (("tm"."status" = 'active'::"text") AND ("tm"."owner_id" IS NOT NULL)) + ) + SELECT "owner_id", + "feature_key", + "array_agg"(DISTINCT "source") AS "sources", + "jsonb_agg"("limits") FILTER (WHERE ("limits" IS NOT NULL)) AS "limits_list" + FROM "base" + GROUP BY "owner_id", "feature_key"; + + +ALTER VIEW "public"."owner_feature_entitlements" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."owner_users" ( + "owner_id" "uuid" NOT NULL, + "user_id" "uuid" NOT NULL, + "role" "text" DEFAULT 'admin'::"text" NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."owner_users" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."patient_group_patient" ( + "patient_group_id" "uuid" NOT NULL, + "patient_id" "uuid" NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"(), + "tenant_id" "uuid" NOT NULL +); + + +ALTER TABLE "public"."patient_group_patient" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."patient_groups" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "nome" "text" NOT NULL, + "descricao" "text", + "cor" "text", + "is_active" boolean DEFAULT true NOT NULL, + "is_system" boolean DEFAULT false NOT NULL, + "owner_id" "uuid" DEFAULT "auth"."uid"() NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "therapist_id" "uuid", + "tenant_id" "uuid" NOT NULL +); + + +ALTER TABLE "public"."patient_groups" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."patient_intake_requests" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "owner_id" "uuid" NOT NULL, + "token" "text" NOT NULL, + "consent" boolean DEFAULT false NOT NULL, + "status" "text" DEFAULT 'new'::"text" NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "converted_patient_id" "uuid", + "rejected_reason" "text", + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "cpf" "text", + "rg" "text", + "cep" "text", + "nome_completo" "text", + "email_principal" "text", + "telefone" "text", + "pais" "text", + "cidade" "text", + "estado" "text", + "endereco" "text", + "numero" "text", + "bairro" "text", + "complemento" "text", + "data_nascimento" "date", + "naturalidade" "text", + "genero" "text", + "estado_civil" "text", + "onde_nos_conheceu" "text", + "encaminhado_por" "text", + "observacoes" "text", + "notas_internas" "text", + "email_alternativo" "text", + "telefone_alternativo" "text", + "profissao" "text", + "escolaridade" "text", + "nacionalidade" "text", + "avatar_url" "text", + "tenant_id" "uuid", + CONSTRAINT "chk_intakes_status" CHECK (("status" = ANY (ARRAY['new'::"text", 'converted'::"text", 'rejected'::"text"]))) +); + + +ALTER TABLE "public"."patient_intake_requests" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."patient_invites" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "owner_id" "uuid" NOT NULL, + "token" "text" NOT NULL, + "active" boolean DEFAULT true NOT NULL, + "expires_at" timestamp with time zone, + "max_uses" integer, + "uses" integer DEFAULT 0 NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "tenant_id" "uuid" +); + + +ALTER TABLE "public"."patient_invites" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."patient_patient_tag" ( + "owner_id" "uuid" NOT NULL, + "patient_id" "uuid" NOT NULL, + "tag_id" "uuid" NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "tenant_id" "uuid" NOT NULL +); + + +ALTER TABLE "public"."patient_patient_tag" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."patient_tags" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "owner_id" "uuid" NOT NULL, + "nome" "text" NOT NULL, + "cor" "text", + "is_padrao" boolean DEFAULT false NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone, + "tenant_id" "uuid" NOT NULL +); + + +ALTER TABLE "public"."patient_tags" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."patients" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "nome_completo" "text" NOT NULL, + "email_principal" "text", + "telefone" "text", + "created_at" timestamp with time zone DEFAULT "now"(), + "owner_id" "uuid", + "avatar_url" "text", + "status" "text" DEFAULT 'Ativo'::"text", + "last_attended_at" timestamp with time zone, + "is_native" boolean DEFAULT false, + "naturalidade" "text", + "data_nascimento" "date", + "rg" "text", + "cpf" "text", + "identification_color" "text", + "genero" "text", + "estado_civil" "text", + "email_alternativo" "text", + "pais" "text" DEFAULT 'Brasil'::"text", + "cep" "text", + "cidade" "text", + "estado" "text", + "endereco" "text", + "numero" "text", + "bairro" "text", + "complemento" "text", + "escolaridade" "text", + "profissao" "text", + "nome_parente" "text", + "grau_parentesco" "text", + "telefone_alternativo" "text", + "onde_nos_conheceu" "text", + "encaminhado_por" "text", + "nome_responsavel" "text", + "telefone_responsavel" "text", + "cpf_responsavel" "text", + "observacao_responsavel" "text", + "cobranca_no_responsavel" boolean DEFAULT false, + "observacoes" "text", + "notas_internas" "text", + "updated_at" timestamp with time zone DEFAULT "now"(), + "telefone_parente" "text", + "tenant_id" "uuid" NOT NULL, + "responsible_member_id" "uuid" NOT NULL, + "user_id" "uuid", + "patient_scope" "text" DEFAULT 'clinic'::"text" NOT NULL, + "therapist_member_id" "uuid", + CONSTRAINT "cpf_responsavel_format_check" CHECK ((("cpf_responsavel" IS NULL) OR ("cpf_responsavel" ~ '^\d{11}$'::"text"))), + CONSTRAINT "patients_cpf_format_check" CHECK ((("cpf" IS NULL) OR ("cpf" ~ '^\d{11}$'::"text"))), + CONSTRAINT "patients_patient_scope_check" CHECK (("patient_scope" = ANY (ARRAY['clinic'::"text", 'therapist'::"text"]))), + CONSTRAINT "patients_status_check" CHECK (("status" = ANY (ARRAY['Ativo'::"text", 'Inativo'::"text", 'Alta'::"text", 'Encaminhado'::"text"]))), + CONSTRAINT "patients_therapist_scope_consistency" CHECK (((("patient_scope" = 'clinic'::"text") AND ("therapist_member_id" IS NULL)) OR (("patient_scope" = 'therapist'::"text") AND ("therapist_member_id" IS NOT NULL)))) +); + + +ALTER TABLE "public"."patients" OWNER TO "supabase_admin"; + + +COMMENT ON COLUMN "public"."patients"."avatar_url" IS 'URL pública da imagem de avatar armazenada no Supabase Storage'; + + + +CREATE TABLE IF NOT EXISTS "public"."plan_prices" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "plan_id" "uuid" NOT NULL, + "currency" "text" DEFAULT 'BRL'::"text" NOT NULL, + "interval" "text" NOT NULL, + "amount_cents" integer NOT NULL, + "is_active" boolean DEFAULT true NOT NULL, + "active_from" timestamp with time zone DEFAULT "now"() NOT NULL, + "active_to" timestamp with time zone, + "source" "text" DEFAULT 'manual'::"text" NOT NULL, + "provider" "text", + "provider_price_id" "text", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + CONSTRAINT "plan_prices_amount_cents_check" CHECK (("amount_cents" >= 0)), + CONSTRAINT "plan_prices_interval_check" CHECK (("interval" = ANY (ARRAY['month'::"text", 'year'::"text"]))) +); + + +ALTER TABLE "public"."plan_prices" OWNER TO "supabase_admin"; + + +COMMENT ON TABLE "public"."plan_prices" IS 'Histórico de preços por plano (fonte: manual/gateway).'; + + + +CREATE TABLE IF NOT EXISTS "public"."plan_public" ( + "plan_id" "uuid" NOT NULL, + "public_name" "text" DEFAULT ''::"text" NOT NULL, + "public_description" "text" DEFAULT ''::"text" NOT NULL, + "badge" "text", + "is_featured" boolean DEFAULT false NOT NULL, + "is_visible" boolean DEFAULT true NOT NULL, + "sort_order" integer DEFAULT 0 NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."plan_public" OWNER TO "supabase_admin"; + + +COMMENT ON TABLE "public"."plan_public" IS 'Configuração de vitrine (página pública) dos planos.'; + + + +CREATE TABLE IF NOT EXISTS "public"."plan_public_bullets" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "plan_id" "uuid" NOT NULL, + "text" "text" NOT NULL, + "sort_order" integer DEFAULT 0 NOT NULL, + "highlight" boolean DEFAULT false NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."plan_public_bullets" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."plans" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "key" "text" NOT NULL, + "name" "text" NOT NULL, + "description" "text", + "is_active" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "price_cents" integer DEFAULT 0 NOT NULL, + "currency" "text" DEFAULT 'BRL'::"text" NOT NULL, + "billing_interval" "text" DEFAULT 'month'::"text" NOT NULL, + "target" "text", + CONSTRAINT "plans_target_check" CHECK (("target" = ANY (ARRAY['patient'::"text", 'therapist'::"text", 'clinic'::"text"]))) +); + + +ALTER TABLE "public"."plans" OWNER TO "supabase_admin"; + + +COMMENT ON COLUMN "public"."plans"."name" IS 'Nome interno do plano (admin). A key é técnica/imutável.'; + + + +COMMENT ON COLUMN "public"."plans"."target" IS 'Público-alvo do plano: patient, therapist ou clinic.'; + + + +CREATE TABLE IF NOT EXISTS "public"."profiles" ( + "id" "uuid" NOT NULL, + "role" "text" DEFAULT 'tenant_member'::"text" NOT NULL, + "full_name" "text", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "avatar_url" "text", + "phone" "text", + "bio" "text", + "language" "text" DEFAULT 'pt-BR'::"text" NOT NULL, + "timezone" "text" DEFAULT 'America/Sao_Paulo'::"text" NOT NULL, + "notify_system_email" boolean DEFAULT true NOT NULL, + "notify_reminders" boolean DEFAULT true NOT NULL, + "notify_news" boolean DEFAULT false NOT NULL, + "account_type" "text" DEFAULT 'free'::"text" NOT NULL, + CONSTRAINT "profiles_account_type_check" CHECK (("account_type" = ANY (ARRAY['free'::"text", 'patient'::"text", 'therapist'::"text", 'clinic'::"text"]))), + CONSTRAINT "profiles_role_check" CHECK (("role" = ANY (ARRAY['saas_admin'::"text", 'tenant_member'::"text", 'portal_user'::"text", 'patient'::"text"]))) +); + + +ALTER TABLE "public"."profiles" OWNER TO "supabase_admin"; + + +COMMENT ON COLUMN "public"."profiles"."account_type" IS 'Tipo de conta: free=sem perfil ainda, patient=paciente (imutável), therapist=terapeuta (imutável), clinic=clínica (imutável).'; + + + +CREATE TABLE IF NOT EXISTS "public"."saas_admins" ( + "user_id" "uuid" NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."saas_admins" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."subscription_events" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "subscription_id" "uuid" NOT NULL, + "owner_id" "uuid" NOT NULL, + "event_type" "text" NOT NULL, + "old_plan_id" "uuid", + "new_plan_id" "uuid", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "created_by" "uuid", + "source" "text" DEFAULT 'admin_ui'::"text", + "reason" "text", + "metadata" "jsonb", + "owner_type" "text" NOT NULL, + "owner_ref" "uuid" NOT NULL, + CONSTRAINT "subscription_events_owner_ref_consistency_chk" CHECK (("owner_id" = "owner_ref")), + CONSTRAINT "subscription_events_owner_type_chk" CHECK ((("owner_type" IS NULL) OR ("owner_type" = ANY (ARRAY['clinic'::"text", 'therapist'::"text"])))) +); + + +ALTER TABLE "public"."subscription_events" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."subscription_intents_personal" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "user_id" "uuid" NOT NULL, + "created_by_user_id" "uuid", + "email" "text" NOT NULL, + "plan_id" "uuid" NOT NULL, + "plan_key" "text", + "interval" "text", + "amount_cents" integer, + "currency" "text", + "status" "text" DEFAULT 'new'::"text" NOT NULL, + "source" "text" DEFAULT 'manual'::"text" NOT NULL, + "notes" "text", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "paid_at" timestamp with time zone, + "subscription_id" "uuid", + CONSTRAINT "sint_personal_interval_check" CHECK ((("interval" IS NULL) OR ("interval" = ANY (ARRAY['month'::"text", 'year'::"text"])))), + CONSTRAINT "sint_personal_status_check" CHECK (("status" = ANY (ARRAY['new'::"text", 'waiting_payment'::"text", 'paid'::"text", 'canceled'::"text"]))) +); + + +ALTER TABLE "public"."subscription_intents_personal" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."subscription_intents_tenant" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "user_id" "uuid" NOT NULL, + "created_by_user_id" "uuid", + "email" "text" NOT NULL, + "plan_id" "uuid" NOT NULL, + "plan_key" "text", + "interval" "text", + "amount_cents" integer, + "currency" "text", + "status" "text" DEFAULT 'new'::"text" NOT NULL, + "source" "text" DEFAULT 'manual'::"text" NOT NULL, + "notes" "text", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "paid_at" timestamp with time zone, + "tenant_id" "uuid" NOT NULL, + "subscription_id" "uuid", + CONSTRAINT "sint_tenant_interval_check" CHECK ((("interval" IS NULL) OR ("interval" = ANY (ARRAY['month'::"text", 'year'::"text"])))), + CONSTRAINT "sint_tenant_status_check" CHECK (("status" = ANY (ARRAY['new'::"text", 'waiting_payment'::"text", 'paid'::"text", 'canceled'::"text"]))) +); + + +ALTER TABLE "public"."subscription_intents_tenant" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."subscription_intents" AS + SELECT "t"."id", + "t"."user_id", + "t"."created_by_user_id", + "t"."email", + "t"."plan_id", + "t"."plan_key", + "t"."interval", + "t"."amount_cents", + "t"."currency", + "t"."status", + "t"."source", + "t"."notes", + "t"."created_at", + "t"."paid_at", + "t"."tenant_id", + "t"."subscription_id", + 'clinic'::"text" AS "plan_target" + FROM "public"."subscription_intents_tenant" "t" +UNION ALL + SELECT "p"."id", + "p"."user_id", + "p"."created_by_user_id", + "p"."email", + "p"."plan_id", + "p"."plan_key", + "p"."interval", + "p"."amount_cents", + "p"."currency", + "p"."status", + "p"."source", + "p"."notes", + "p"."created_at", + "p"."paid_at", + NULL::"uuid" AS "tenant_id", + "p"."subscription_id", + 'therapist'::"text" AS "plan_target" + FROM "public"."subscription_intents_personal" "p"; + + +ALTER VIEW "public"."subscription_intents" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."subscription_intents_legacy" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "user_id" "uuid", + "email" "text", + "plan_key" "text" NOT NULL, + "interval" "text" NOT NULL, + "amount_cents" integer NOT NULL, + "currency" "text" DEFAULT 'BRL'::"text" NOT NULL, + "status" "text" DEFAULT 'new'::"text" NOT NULL, + "source" "text" DEFAULT 'landing'::"text" NOT NULL, + "notes" "text", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "paid_at" timestamp with time zone, + "tenant_id" "uuid" NOT NULL, + "created_by_user_id" "uuid", + CONSTRAINT "subscription_intents_interval_check" CHECK (("interval" = ANY (ARRAY['month'::"text", 'year'::"text"]))), + CONSTRAINT "subscription_intents_status_check" CHECK (("status" = ANY (ARRAY['new'::"text", 'waiting_payment'::"text", 'paid'::"text", 'canceled'::"text"]))) +); + + +ALTER TABLE "public"."subscription_intents_legacy" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."tenant_feature_exceptions_log" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "tenant_id" "uuid" NOT NULL, + "feature_key" "text" NOT NULL, + "enabled" boolean NOT NULL, + "reason" "text", + "created_by" "uuid", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."tenant_feature_exceptions_log" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."tenant_features" ( + "tenant_id" "uuid" NOT NULL, + "feature_key" "text" NOT NULL, + "enabled" boolean DEFAULT false NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."tenant_features" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."tenant_invites" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "tenant_id" "uuid" NOT NULL, + "email" "text" NOT NULL, + "role" "text" NOT NULL, + "token" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "invited_by" "uuid", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "expires_at" timestamp with time zone DEFAULT ("now"() + '7 days'::interval) NOT NULL, + "accepted_at" timestamp with time zone, + "accepted_by" "uuid", + "revoked_at" timestamp with time zone, + "revoked_by" "uuid", + CONSTRAINT "tenant_invites_role_check" CHECK (("role" = ANY (ARRAY['therapist'::"text", 'secretary'::"text"]))) +); + + +ALTER TABLE "public"."tenant_invites" OWNER TO "supabase_admin"; + + +CREATE TABLE IF NOT EXISTS "public"."tenants" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "name" "text", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "kind" "text" DEFAULT 'saas'::"text" NOT NULL, + CONSTRAINT "tenants_kind_check" CHECK (("kind" = ANY (ARRAY['therapist'::"text", 'clinic_coworking'::"text", 'clinic_reception'::"text", 'clinic_full'::"text", 'clinic'::"text", 'saas'::"text", 'supervisor'::"text"]))) +); + + +ALTER TABLE "public"."tenants" OWNER TO "supabase_admin"; + + +COMMENT ON COLUMN "public"."tenants"."kind" IS 'Tipo do tenant. Imutável após criação. therapist=terapeuta solo. clinic_coworking/clinic_reception/clinic_full=clínicas. clinic e saas são legados.'; + + + +CREATE TABLE IF NOT EXISTS "public"."user_settings" ( + "user_id" "uuid" NOT NULL, + "theme_mode" "text" DEFAULT 'dark'::"text" NOT NULL, + "preset" "text" DEFAULT 'Aura'::"text" NOT NULL, + "primary_color" "text" DEFAULT 'noir'::"text" NOT NULL, + "surface_color" "text" DEFAULT 'slate'::"text" NOT NULL, + "menu_mode" "text" DEFAULT 'static'::"text" NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + + +ALTER TABLE "public"."user_settings" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_auth_users_public" AS + SELECT "id" AS "user_id", + "email", + "created_at", + "last_sign_in_at" + FROM "auth"."users" "u"; + + +ALTER VIEW "public"."v_auth_users_public" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_commitment_totals" AS + SELECT "c"."tenant_id", + "c"."id" AS "commitment_id", + (COALESCE("sum"("l"."minutes"), (0)::bigint))::integer AS "total_minutes" + FROM ("public"."determined_commitments" "c" + LEFT JOIN "public"."commitment_time_logs" "l" ON (("l"."commitment_id" = "c"."id"))) + GROUP BY "c"."tenant_id", "c"."id"; + + +ALTER VIEW "public"."v_commitment_totals" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_patient_groups_with_counts" AS + SELECT "pg"."id", + "pg"."nome", + "pg"."cor", + "pg"."owner_id", + "pg"."is_system", + "pg"."is_active", + "pg"."created_at", + "pg"."updated_at", + (COALESCE("count"("pgp"."patient_id"), (0)::bigint))::integer AS "patients_count" + FROM ("public"."patient_groups" "pg" + LEFT JOIN "public"."patient_group_patient" "pgp" ON (("pgp"."patient_group_id" = "pg"."id"))) + GROUP BY "pg"."id", "pg"."nome", "pg"."cor", "pg"."owner_id", "pg"."is_system", "pg"."is_active", "pg"."created_at", "pg"."updated_at"; + + +ALTER VIEW "public"."v_patient_groups_with_counts" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_plan_active_prices" AS + SELECT "plan_id", + "max"( + CASE + WHEN (("interval" = 'month'::"text") AND "is_active") THEN "amount_cents" + ELSE NULL::integer + END) AS "monthly_cents", + "max"( + CASE + WHEN (("interval" = 'year'::"text") AND "is_active") THEN "amount_cents" + ELSE NULL::integer + END) AS "yearly_cents", + "max"( + CASE + WHEN (("interval" = 'month'::"text") AND "is_active") THEN "currency" + ELSE NULL::"text" + END) AS "monthly_currency", + "max"( + CASE + WHEN (("interval" = 'year'::"text") AND "is_active") THEN "currency" + ELSE NULL::"text" + END) AS "yearly_currency" + FROM "public"."plan_prices" + GROUP BY "plan_id"; + + +ALTER VIEW "public"."v_plan_active_prices" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_public_pricing" AS + SELECT "p"."id" AS "plan_id", + "p"."key" AS "plan_key", + "p"."name" AS "plan_name", + COALESCE("pp"."public_name", ''::"text") AS "public_name", + COALESCE("pp"."public_description", ''::"text") AS "public_description", + "pp"."badge", + COALESCE("pp"."is_featured", false) AS "is_featured", + COALESCE("pp"."is_visible", true) AS "is_visible", + COALESCE("pp"."sort_order", 0) AS "sort_order", + "ap"."monthly_cents", + "ap"."yearly_cents", + "ap"."monthly_currency", + "ap"."yearly_currency", + COALESCE(( SELECT "jsonb_agg"("jsonb_build_object"('id', "b"."id", 'text', "b"."text", 'highlight', "b"."highlight", 'sort_order', "b"."sort_order") ORDER BY "b"."sort_order", "b"."created_at") AS "jsonb_agg" + FROM "public"."plan_public_bullets" "b" + WHERE ("b"."plan_id" = "p"."id")), '[]'::"jsonb") AS "bullets", + "p"."target" AS "plan_target" + FROM (("public"."plans" "p" + LEFT JOIN "public"."plan_public" "pp" ON (("pp"."plan_id" = "p"."id"))) + LEFT JOIN "public"."v_plan_active_prices" "ap" ON (("ap"."plan_id" = "p"."id"))) + ORDER BY COALESCE("pp"."sort_order", 0), "p"."key"; + + +ALTER VIEW "public"."v_public_pricing" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_subscription_feature_mismatch" AS + WITH "expected" AS ( + SELECT "s"."user_id" AS "owner_id", + "f"."key" AS "feature_key" + FROM (("public"."subscriptions" "s" + JOIN "public"."plan_features" "pf" ON (("pf"."plan_id" = "s"."plan_id") AND ("pf"."enabled" = true))) + JOIN "public"."features" "f" ON (("f"."id" = "pf"."feature_id"))) + WHERE (("s"."status" = 'active'::"text") AND ("s"."tenant_id" IS NULL) AND ("s"."user_id" IS NOT NULL)) + ), "actual" AS ( + SELECT "e"."owner_id", + "e"."feature_key" + FROM "public"."owner_feature_entitlements" "e" + ) + SELECT COALESCE("expected"."owner_id", "actual"."owner_id") AS "owner_id", + COALESCE("expected"."feature_key", "actual"."feature_key") AS "feature_key", + CASE + WHEN (("expected"."feature_key" IS NOT NULL) AND ("actual"."feature_key" IS NULL)) THEN 'missing_entitlement'::"text" + WHEN (("expected"."feature_key" IS NULL) AND ("actual"."feature_key" IS NOT NULL)) THEN 'unexpected_entitlement'::"text" + ELSE NULL::"text" + END AS "mismatch_type" + FROM ("expected" + FULL JOIN "actual" ON ((("expected"."owner_id" = "actual"."owner_id") AND ("expected"."feature_key" = "actual"."feature_key")))) + WHERE (("expected"."feature_key" IS NULL) OR ("actual"."feature_key" IS NULL)); + + +ALTER VIEW "public"."v_subscription_feature_mismatch" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_subscription_health" AS + SELECT "s"."id" AS "subscription_id", + "s"."user_id" AS "owner_id", + "s"."status", + "s"."plan_id", + "p"."key" AS "plan_key", + "s"."current_period_start", + "s"."current_period_end", + "s"."updated_at", + CASE + WHEN ("s"."plan_id" IS NULL) THEN 'missing_plan'::"text" + WHEN ("p"."id" IS NULL) THEN 'invalid_plan'::"text" + WHEN (("s"."status" = 'active'::"text") AND ("s"."current_period_end" IS NOT NULL) AND ("s"."current_period_end" < "now"())) THEN 'expired_but_active'::"text" + WHEN (("s"."status" = 'canceled'::"text") AND ("s"."current_period_end" > "now"())) THEN 'canceled_but_still_in_period'::"text" + ELSE 'ok'::"text" + END AS "health_status", + CASE + WHEN ("s"."tenant_id" IS NOT NULL) THEN 'clinic'::"text" + ELSE 'therapist'::"text" + END AS "owner_type", + COALESCE("s"."tenant_id", "s"."user_id") AS "owner_ref" + FROM ("public"."subscriptions" "s" + LEFT JOIN "public"."plans" "p" ON (("p"."id" = "s"."plan_id"))); + + +ALTER VIEW "public"."v_subscription_health" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_subscription_health_v2" AS + SELECT "s"."id" AS "subscription_id", + "s"."user_id" AS "owner_id", + CASE + WHEN ("s"."tenant_id" IS NOT NULL) THEN 'clinic'::"text" + ELSE 'therapist'::"text" + END AS "owner_type", + COALESCE("s"."tenant_id", "s"."user_id") AS "owner_ref", + "s"."status", + "s"."plan_id", + "p"."key" AS "plan_key", + "s"."current_period_start", + "s"."current_period_end", + "s"."updated_at", + CASE + WHEN ("s"."plan_id" IS NULL) THEN 'missing_plan'::"text" + WHEN ("p"."id" IS NULL) THEN 'invalid_plan'::"text" + WHEN (("s"."status" = 'active'::"text") AND ("s"."current_period_end" IS NOT NULL) AND ("s"."current_period_end" < "now"())) THEN 'expired_but_active'::"text" + WHEN (("s"."status" = 'canceled'::"text") AND ("s"."current_period_end" > "now"())) THEN 'canceled_but_still_in_period'::"text" + ELSE 'ok'::"text" + END AS "health_status" + FROM ("public"."subscriptions" "s" + LEFT JOIN "public"."plans" "p" ON (("p"."id" = "s"."plan_id"))); + + +ALTER VIEW "public"."v_subscription_health_v2" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_tag_patient_counts" AS + SELECT "t"."id", + "t"."owner_id", + "t"."nome", + "t"."cor", + "t"."is_padrao", + "t"."created_at", + "t"."updated_at", + (COALESCE("count"("ppt"."patient_id"), (0)::bigint))::integer AS "pacientes_count", + (COALESCE("count"("ppt"."patient_id"), (0)::bigint))::integer AS "patient_count" + FROM ("public"."patient_tags" "t" + LEFT JOIN "public"."patient_patient_tag" "ppt" ON ((("ppt"."tag_id" = "t"."id") AND ("ppt"."owner_id" = "t"."owner_id")))) + GROUP BY "t"."id", "t"."owner_id", "t"."nome", "t"."cor", "t"."is_padrao", "t"."created_at", "t"."updated_at"; + + +ALTER VIEW "public"."v_tag_patient_counts" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_tenant_active_subscription" AS + SELECT DISTINCT ON ("tenant_id") "tenant_id", + "plan_id", + "plan_key", + "interval", + "status", + "current_period_start", + "current_period_end", + "created_at" + FROM "public"."subscriptions" "s" + WHERE (("tenant_id" IS NOT NULL) AND ("status" = 'active'::"text") AND (("current_period_end" IS NULL) OR ("current_period_end" > "now"()))) + ORDER BY "tenant_id", "created_at" DESC; + + +ALTER VIEW "public"."v_tenant_active_subscription" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_tenant_entitlements" AS + SELECT "a"."tenant_id", + "f"."key" AS "feature_key", + true AS "allowed" + FROM (("public"."v_tenant_active_subscription" "a" + JOIN "public"."plan_features" "pf" ON ((("pf"."plan_id" = "a"."plan_id") AND ("pf"."enabled" = true)))) + JOIN "public"."features" "f" ON (("f"."id" = "pf"."feature_id"))); + + +ALTER VIEW "public"."v_tenant_entitlements" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_tenant_entitlements_full" AS + SELECT "a"."tenant_id", + "f"."key" AS "feature_key", + ("pf"."enabled" = true) AS "allowed", + "pf"."limits", + "a"."plan_id", + "p"."key" AS "plan_key" + FROM ((("public"."v_tenant_active_subscription" "a" + JOIN "public"."plan_features" "pf" ON (("pf"."plan_id" = "a"."plan_id"))) + JOIN "public"."features" "f" ON (("f"."id" = "pf"."feature_id"))) + JOIN "public"."plans" "p" ON (("p"."id" = "a"."plan_id"))); + + +ALTER VIEW "public"."v_tenant_entitlements_full" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_tenant_entitlements_json" AS + SELECT "tenant_id", + "max"("plan_key") AS "plan_key", + "jsonb_object_agg"("feature_key", "jsonb_build_object"('allowed', "allowed", 'limits', COALESCE("limits", '{}'::"jsonb")) ORDER BY "feature_key") AS "entitlements" + FROM "public"."v_tenant_entitlements_full" + GROUP BY "tenant_id"; + + +ALTER VIEW "public"."v_tenant_entitlements_json" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_tenant_feature_exceptions" AS + SELECT "tf"."tenant_id", + "a"."plan_key", + "tf"."feature_key", + 'commercial_exception'::"text" AS "exception_type" + FROM (("public"."tenant_features" "tf" + JOIN "public"."v_tenant_active_subscription" "a" ON (("a"."tenant_id" = "tf"."tenant_id"))) + LEFT JOIN "public"."v_tenant_entitlements_full" "v" ON ((("v"."tenant_id" = "tf"."tenant_id") AND ("v"."feature_key" = "tf"."feature_key")))) + WHERE (("tf"."enabled" = true) AND (COALESCE("v"."allowed", false) = false)); + + +ALTER VIEW "public"."v_tenant_feature_exceptions" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_tenant_feature_mismatch" AS + WITH "plan_allowed" AS ( + SELECT "v"."tenant_id", + "v"."feature_key", + "v"."allowed" + FROM "public"."v_tenant_entitlements_full" "v" + ), "overrides" AS ( + SELECT "tf"."tenant_id", + "tf"."feature_key", + "tf"."enabled" + FROM "public"."tenant_features" "tf" + ) + SELECT "o"."tenant_id", + "o"."feature_key", + CASE + WHEN (("o"."enabled" = true) AND (COALESCE("p"."allowed", false) = false)) THEN 'unexpected_override'::"text" + ELSE NULL::"text" + END AS "mismatch_type" + FROM ("overrides" "o" + LEFT JOIN "plan_allowed" "p" ON ((("p"."tenant_id" = "o"."tenant_id") AND ("p"."feature_key" = "o"."feature_key")))) + WHERE (("o"."enabled" = true) AND (COALESCE("p"."allowed", false) = false)); + + +ALTER VIEW "public"."v_tenant_feature_mismatch" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_tenant_members_with_profiles" AS + SELECT "tm"."id" AS "tenant_member_id", + "tm"."tenant_id", + "tm"."user_id", + "tm"."role", + "tm"."status", + "tm"."created_at", + "p"."full_name", + "au"."email" + FROM (("public"."tenant_members" "tm" + LEFT JOIN "public"."profiles" "p" ON (("p"."id" = "tm"."user_id"))) + LEFT JOIN "auth"."users" "au" ON (("au"."id" = "tm"."user_id"))); + + +ALTER VIEW "public"."v_tenant_members_with_profiles" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_tenant_people" AS + SELECT 'member'::"text" AS "type", + "m"."tenant_id", + "m"."user_id", + "u"."email", + "m"."role", + "m"."status", + NULL::"uuid" AS "invite_token", + NULL::timestamp with time zone AS "expires_at" + FROM ("public"."tenant_members" "m" + JOIN "auth"."users" "u" ON (("u"."id" = "m"."user_id"))) +UNION ALL + SELECT 'invite'::"text" AS "type", + "i"."tenant_id", + NULL::"uuid" AS "user_id", + "i"."email", + "i"."role", + 'invited'::"text" AS "status", + "i"."token" AS "invite_token", + "i"."expires_at" + FROM "public"."tenant_invites" "i" + WHERE (("i"."accepted_at" IS NULL) AND ("i"."revoked_at" IS NULL)); + + +ALTER VIEW "public"."v_tenant_people" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_tenant_staff" AS + SELECT ('m_'::"text" || ("tm"."id")::"text") AS "row_id", + "tm"."tenant_id", + "tm"."user_id", + "tm"."role", + "tm"."status", + "tm"."created_at", + "p"."full_name", + "au"."email", + NULL::"uuid" AS "invite_token" + FROM (("public"."tenant_members" "tm" + LEFT JOIN "public"."profiles" "p" ON (("p"."id" = "tm"."user_id"))) + LEFT JOIN "auth"."users" "au" ON (("au"."id" = "tm"."user_id"))) +UNION ALL + SELECT ('i_'::"text" || ("ti"."id")::"text") AS "row_id", + "ti"."tenant_id", + NULL::"uuid" AS "user_id", + "ti"."role", + 'invited'::"text" AS "status", + "ti"."created_at", + NULL::"text" AS "full_name", + "ti"."email", + "ti"."token" AS "invite_token" + FROM "public"."tenant_invites" "ti" + WHERE (("ti"."accepted_at" IS NULL) AND ("ti"."revoked_at" IS NULL) AND ("ti"."expires_at" > "now"())); + + +ALTER VIEW "public"."v_tenant_staff" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_user_active_subscription" AS + SELECT DISTINCT ON ("user_id") "user_id", + "plan_id", + "plan_key", + "interval", + "status", + "current_period_start", + "current_period_end", + "created_at" + FROM "public"."subscriptions" "s" + WHERE (("tenant_id" IS NULL) AND ("user_id" IS NOT NULL) AND ("status" = 'active'::"text") AND (("current_period_end" IS NULL) OR ("current_period_end" > "now"()))) + ORDER BY "user_id", "created_at" DESC; + + +ALTER VIEW "public"."v_user_active_subscription" OWNER TO "supabase_admin"; + + +CREATE OR REPLACE VIEW "public"."v_user_entitlements" AS + SELECT "a"."user_id", + "f"."key" AS "feature_key", + true AS "allowed" + FROM (("public"."v_user_active_subscription" "a" + JOIN "public"."plan_features" "pf" ON ((("pf"."plan_id" = "a"."plan_id") AND ("pf"."enabled" = true)))) + JOIN "public"."features" "f" ON (("f"."id" = "pf"."feature_id"))); + + +ALTER VIEW "public"."v_user_entitlements" OWNER TO "supabase_admin"; + + +ALTER TABLE ONLY "public"."agenda_online_slots" ALTER COLUMN "id" SET DEFAULT "nextval"('"public"."agenda_online_slots_id_seq"'::"regclass"); + + + +ALTER TABLE ONLY "public"."agenda_configuracoes" + ADD CONSTRAINT "agenda_configuracoes_pkey" PRIMARY KEY ("owner_id"); + + + +ALTER TABLE ONLY "public"."agenda_eventos" + ADD CONSTRAINT "agenda_eventos_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."agenda_eventos" + ADD CONSTRAINT "agenda_eventos_sem_sobreposicao" EXCLUDE USING "gist" ("owner_id" WITH =, "tstzrange"("inicio_em", "fim_em", '[)'::"text") WITH &&); + + + +ALTER TABLE ONLY "public"."agenda_excecoes" + ADD CONSTRAINT "agenda_excecoes_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."agenda_online_slots" + ADD CONSTRAINT "agenda_online_slots_owner_id_weekday_time_key" UNIQUE ("owner_id", "weekday", "time"); + + + +ALTER TABLE ONLY "public"."agenda_online_slots" + ADD CONSTRAINT "agenda_online_slots_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."agenda_regras_semanais" + ADD CONSTRAINT "agenda_regras_semanais_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."agenda_regras_semanais" + ADD CONSTRAINT "agenda_regras_semanais_unique" UNIQUE ("owner_id", "dia_semana", "hora_inicio", "hora_fim", "modalidade"); + + + +ALTER TABLE ONLY "public"."agenda_slots_bloqueados_semanais" + ADD CONSTRAINT "agenda_slots_bloqueados_seman_owner_id_dia_semana_hora_inic_key" UNIQUE ("owner_id", "dia_semana", "hora_inicio"); + + + +ALTER TABLE ONLY "public"."agenda_slots_bloqueados_semanais" + ADD CONSTRAINT "agenda_slots_bloqueados_semanais_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."agenda_slots_regras" + ADD CONSTRAINT "agenda_slots_regras_owner_id_dia_semana_key" UNIQUE ("owner_id", "dia_semana"); + + + +ALTER TABLE ONLY "public"."agenda_slots_regras" + ADD CONSTRAINT "agenda_slots_regras_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."commitment_time_logs" + ADD CONSTRAINT "commitment_time_logs_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."determined_commitment_fields" + ADD CONSTRAINT "determined_commitment_fields_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."determined_commitments" + ADD CONSTRAINT "determined_commitments_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."determined_commitments" + ADD CONSTRAINT "determined_commitments_tenant_native_key_uq" UNIQUE ("tenant_id", "native_key"); + + + +ALTER TABLE ONLY "public"."dev_user_credentials" + ADD CONSTRAINT "dev_user_credentials_email_key" UNIQUE ("email"); + + + +ALTER TABLE ONLY "public"."dev_user_credentials" + ADD CONSTRAINT "dev_user_credentials_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."entitlements_invalidation" + ADD CONSTRAINT "entitlements_invalidation_pkey" PRIMARY KEY ("owner_id"); + + + +ALTER TABLE ONLY "public"."features" + ADD CONSTRAINT "features_key_key" UNIQUE ("key"); + + + +ALTER TABLE ONLY "public"."features" + ADD CONSTRAINT "features_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."module_features" + ADD CONSTRAINT "module_features_pkey" PRIMARY KEY ("module_id", "feature_id"); + + + +ALTER TABLE ONLY "public"."modules" + ADD CONSTRAINT "modules_key_key" UNIQUE ("key"); + + + +ALTER TABLE ONLY "public"."modules" + ADD CONSTRAINT "modules_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."owner_users" + ADD CONSTRAINT "owner_users_pkey" PRIMARY KEY ("owner_id", "user_id"); + + + +ALTER TABLE ONLY "public"."patient_group_patient" + ADD CONSTRAINT "patient_group_patient_pkey" PRIMARY KEY ("patient_group_id", "patient_id"); + + + +ALTER TABLE ONLY "public"."patient_groups" + ADD CONSTRAINT "patient_groups_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."patient_intake_requests" + ADD CONSTRAINT "patient_intake_requests_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."patient_invites" + ADD CONSTRAINT "patient_invites_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."patient_invites" + ADD CONSTRAINT "patient_invites_token_key" UNIQUE ("token"); + + + +ALTER TABLE ONLY "public"."patient_patient_tag" + ADD CONSTRAINT "patient_patient_tag_pkey" PRIMARY KEY ("patient_id", "tag_id"); + + + +ALTER TABLE ONLY "public"."patient_tags" + ADD CONSTRAINT "patient_tags_owner_name_uniq" UNIQUE ("owner_id", "nome"); + + + +ALTER TABLE ONLY "public"."patient_tags" + ADD CONSTRAINT "patient_tags_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."patients" + ADD CONSTRAINT "patients_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."plan_features" + ADD CONSTRAINT "plan_features_pkey" PRIMARY KEY ("plan_id", "feature_id"); + + + +ALTER TABLE ONLY "public"."plan_prices" + ADD CONSTRAINT "plan_prices_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."plan_public_bullets" + ADD CONSTRAINT "plan_public_bullets_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."plan_public" + ADD CONSTRAINT "plan_public_pkey" PRIMARY KEY ("plan_id"); + + + +ALTER TABLE ONLY "public"."plans" + ADD CONSTRAINT "plans_key_key" UNIQUE ("key"); + + + +ALTER TABLE ONLY "public"."plans" + ADD CONSTRAINT "plans_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."profiles" + ADD CONSTRAINT "profiles_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."saas_admins" + ADD CONSTRAINT "saas_admins_pkey" PRIMARY KEY ("user_id"); + + + +ALTER TABLE ONLY "public"."subscription_events" + ADD CONSTRAINT "subscription_events_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."subscription_intents_personal" + ADD CONSTRAINT "subscription_intents_personal_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."subscription_intents_legacy" + ADD CONSTRAINT "subscription_intents_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."subscription_intents_tenant" + ADD CONSTRAINT "subscription_intents_tenant_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."subscriptions" + ADD CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."tenant_feature_exceptions_log" + ADD CONSTRAINT "tenant_feature_exceptions_log_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."tenant_features" + ADD CONSTRAINT "tenant_features_pkey" PRIMARY KEY ("tenant_id", "feature_key"); + + + +ALTER TABLE ONLY "public"."tenant_invites" + ADD CONSTRAINT "tenant_invites_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."tenant_members" + ADD CONSTRAINT "tenant_members_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."tenant_members" + ADD CONSTRAINT "tenant_members_tenant_id_user_id_key" UNIQUE ("tenant_id", "user_id"); + + + +ALTER TABLE ONLY "public"."tenant_modules" + ADD CONSTRAINT "tenant_modules_owner_id_module_id_key" UNIQUE ("owner_id", "module_id"); + + + +ALTER TABLE ONLY "public"."tenant_modules" + ADD CONSTRAINT "tenant_modules_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."tenants" + ADD CONSTRAINT "tenants_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."user_settings" + ADD CONSTRAINT "user_settings_pkey" PRIMARY KEY ("user_id"); + + + +CREATE INDEX "agenda_configuracoes_tenant_idx" ON "public"."agenda_configuracoes" USING "btree" ("tenant_id"); + + + +CREATE INDEX "agenda_configuracoes_tenant_owner_idx" ON "public"."agenda_configuracoes" USING "btree" ("tenant_id", "owner_id"); + + + +CREATE INDEX "agenda_eventos_owner_inicio_idx" ON "public"."agenda_eventos" USING "btree" ("owner_id", "inicio_em"); + + + +CREATE INDEX "agenda_eventos_owner_paciente_idx" ON "public"."agenda_eventos" USING "btree" ("owner_id", "paciente_id"); + + + +CREATE INDEX "agenda_eventos_owner_terapeuta_inicio_idx" ON "public"."agenda_eventos" USING "btree" ("owner_id", "terapeuta_id", "inicio_em"); + + + +CREATE INDEX "agenda_eventos_tenant_inicio_idx" ON "public"."agenda_eventos" USING "btree" ("tenant_id", "inicio_em"); + + + +CREATE INDEX "agenda_eventos_tenant_owner_inicio_idx" ON "public"."agenda_eventos" USING "btree" ("tenant_id", "owner_id", "inicio_em"); + + + +CREATE INDEX "agenda_excecoes_owner_data_idx" ON "public"."agenda_excecoes" USING "btree" ("owner_id", "data"); + + + +CREATE INDEX "agenda_excecoes_tenant_idx" ON "public"."agenda_excecoes" USING "btree" ("tenant_id"); + + + +CREATE INDEX "agenda_excecoes_tenant_owner_idx" ON "public"."agenda_excecoes" USING "btree" ("tenant_id", "owner_id"); + + + +CREATE INDEX "agenda_online_slots_owner_weekday_idx" ON "public"."agenda_online_slots" USING "btree" ("owner_id", "weekday"); + + + +CREATE INDEX "agenda_online_slots_tenant_idx" ON "public"."agenda_online_slots" USING "btree" ("tenant_id"); + + + +CREATE INDEX "agenda_online_slots_tenant_owner_idx" ON "public"."agenda_online_slots" USING "btree" ("tenant_id", "owner_id"); + + + +CREATE INDEX "agenda_regras_semanais_owner_dia_idx" ON "public"."agenda_regras_semanais" USING "btree" ("owner_id", "dia_semana"); + + + +CREATE INDEX "agenda_regras_semanais_tenant_idx" ON "public"."agenda_regras_semanais" USING "btree" ("tenant_id"); + + + +CREATE INDEX "agenda_regras_semanais_tenant_owner_idx" ON "public"."agenda_regras_semanais" USING "btree" ("tenant_id", "owner_id"); + + + +CREATE INDEX "agenda_slots_bloqueados_semanais_tenant_idx" ON "public"."agenda_slots_bloqueados_semanais" USING "btree" ("tenant_id"); + + + +CREATE INDEX "agenda_slots_bloqueados_semanais_tenant_owner_idx" ON "public"."agenda_slots_bloqueados_semanais" USING "btree" ("tenant_id", "owner_id"); + + + +CREATE INDEX "agenda_slots_regras_tenant_idx" ON "public"."agenda_slots_regras" USING "btree" ("tenant_id"); + + + +CREATE INDEX "agenda_slots_regras_tenant_owner_idx" ON "public"."agenda_slots_regras" USING "btree" ("tenant_id", "owner_id"); + + + +CREATE INDEX "commitment_time_logs_calendar_event_idx" ON "public"."commitment_time_logs" USING "btree" ("calendar_event_id"); + + + +CREATE INDEX "commitment_time_logs_commitment_idx" ON "public"."commitment_time_logs" USING "btree" ("commitment_id", "created_at" DESC); + + + +CREATE INDEX "commitment_time_logs_tenant_idx" ON "public"."commitment_time_logs" USING "btree" ("tenant_id", "created_at" DESC); + + + +CREATE INDEX "determined_commitment_fields_commitment_idx" ON "public"."determined_commitment_fields" USING "btree" ("commitment_id", "sort_order"); + + + +CREATE UNIQUE INDEX "determined_commitment_fields_key_uniq" ON "public"."determined_commitment_fields" USING "btree" ("commitment_id", "key"); + + + +CREATE INDEX "determined_commitment_fields_tenant_idx" ON "public"."determined_commitment_fields" USING "btree" ("tenant_id"); + + + +CREATE INDEX "determined_commitments_active_idx" ON "public"."determined_commitments" USING "btree" ("tenant_id", "active"); + + + +CREATE INDEX "determined_commitments_tenant_idx" ON "public"."determined_commitments" USING "btree" ("tenant_id"); + + + +CREATE UNIQUE INDEX "determined_commitments_tenant_name_uniq" ON "public"."determined_commitments" USING "btree" ("tenant_id", "lower"("name")); + + + +CREATE INDEX "idx_agenda_eventos_determined_commitment_id" ON "public"."agenda_eventos" USING "btree" ("determined_commitment_id"); + + + +CREATE INDEX "idx_agenda_excecoes_owner_data" ON "public"."agenda_excecoes" USING "btree" ("owner_id", "data"); + + + +CREATE INDEX "idx_agenda_slots_regras_owner_dia" ON "public"."agenda_slots_regras" USING "btree" ("owner_id", "dia_semana"); + + + +CREATE INDEX "idx_intakes_converted_patient_id" ON "public"."patient_intake_requests" USING "btree" ("converted_patient_id"); + + + +CREATE INDEX "idx_intakes_owner_cpf" ON "public"."patient_intake_requests" USING "btree" ("owner_id", "cpf"); + + + +CREATE INDEX "idx_intakes_owner_created" ON "public"."patient_intake_requests" USING "btree" ("owner_id", "created_at" DESC); + + + +CREATE INDEX "idx_intakes_owner_status_created" ON "public"."patient_intake_requests" USING "btree" ("owner_id", "status", "created_at" DESC); + + + +CREATE INDEX "idx_intakes_status_created" ON "public"."patient_intake_requests" USING "btree" ("status", "created_at" DESC); + + + +CREATE INDEX "idx_patient_group_patient_group_id" ON "public"."patient_group_patient" USING "btree" ("patient_group_id"); + + + +CREATE INDEX "idx_patient_groups_owner" ON "public"."patient_groups" USING "btree" ("owner_id"); + + + +CREATE INDEX "idx_patient_groups_owner_system_nome" ON "public"."patient_groups" USING "btree" ("owner_id", "is_system", "nome"); + + + +CREATE INDEX "idx_patient_tags_owner" ON "public"."patient_tags" USING "btree" ("owner_id"); + + + +CREATE INDEX "idx_patients_created_at" ON "public"."patients" USING "btree" ("created_at" DESC); + + + +CREATE INDEX "idx_patients_last_attended" ON "public"."patients" USING "btree" ("last_attended_at" DESC); + + + +CREATE INDEX "idx_patients_owner_email_principal" ON "public"."patients" USING "btree" ("owner_id", "email_principal"); + + + +CREATE INDEX "idx_patients_owner_id" ON "public"."patients" USING "btree" ("owner_id"); + + + +CREATE INDEX "idx_patients_owner_nome" ON "public"."patients" USING "btree" ("owner_id", "nome_completo"); + + + +CREATE INDEX "idx_patients_responsible_member" ON "public"."patients" USING "btree" ("responsible_member_id"); + + + +CREATE INDEX "idx_patients_status" ON "public"."patients" USING "btree" ("status"); + + + +CREATE INDEX "idx_patients_tenant" ON "public"."patients" USING "btree" ("tenant_id"); + + + +CREATE INDEX "idx_patients_tenant_email_norm" ON "public"."patients" USING "btree" ("tenant_id", "lower"(TRIM(BOTH FROM "email_principal"))); + + + +CREATE INDEX "idx_pgp_group" ON "public"."patient_group_patient" USING "btree" ("patient_group_id"); + + + +CREATE INDEX "idx_pgp_patient" ON "public"."patient_group_patient" USING "btree" ("patient_id"); + + + +CREATE INDEX "idx_ppt_patient" ON "public"."patient_patient_tag" USING "btree" ("patient_id"); + + + +CREATE INDEX "idx_ppt_tag" ON "public"."patient_patient_tag" USING "btree" ("tag_id"); + + + +CREATE INDEX "idx_slots_bloq_owner_dia" ON "public"."agenda_slots_bloqueados_semanais" USING "btree" ("owner_id", "dia_semana"); + + + +CREATE INDEX "idx_subscription_intents_plan_interval" ON "public"."subscription_intents_legacy" USING "btree" ("plan_key", "interval"); + + + +CREATE INDEX "idx_subscription_intents_status" ON "public"."subscription_intents_legacy" USING "btree" ("status"); + + + +CREATE INDEX "idx_subscription_intents_user_id" ON "public"."subscription_intents_legacy" USING "btree" ("user_id"); + + + +CREATE INDEX "idx_tenant_features_tenant" ON "public"."tenant_features" USING "btree" ("tenant_id"); + + + +CREATE INDEX "idx_tenant_invites_tenant" ON "public"."tenant_invites" USING "btree" ("tenant_id"); + + + +CREATE INDEX "idx_tenant_invites_token" ON "public"."tenant_invites" USING "btree" ("token"); + + + +CREATE INDEX "ix_plan_prices_plan" ON "public"."plan_prices" USING "btree" ("plan_id"); + + + +CREATE INDEX "ix_plan_public_bullets_plan" ON "public"."plan_public_bullets" USING "btree" ("plan_id"); + + + +CREATE INDEX "ix_plan_public_sort" ON "public"."plan_public" USING "btree" ("sort_order"); + + + +CREATE INDEX "patient_group_patient_tenant_idx" ON "public"."patient_group_patient" USING "btree" ("tenant_id"); + + + +CREATE UNIQUE INDEX "patient_groups_owner_nome_uniq" ON "public"."patient_groups" USING "btree" ("owner_id", "nome"); + + + +CREATE INDEX "patient_groups_tenant_idx" ON "public"."patient_groups" USING "btree" ("tenant_id"); + + + +CREATE INDEX "patient_intake_owner_id_idx" ON "public"."patient_intake_requests" USING "btree" ("owner_id"); + + + +CREATE INDEX "patient_intake_requests_tenant_idx" ON "public"."patient_intake_requests" USING "btree" ("tenant_id"); + + + +CREATE INDEX "patient_intake_status_idx" ON "public"."patient_intake_requests" USING "btree" ("status"); + + + +CREATE INDEX "patient_intake_token_idx" ON "public"."patient_intake_requests" USING "btree" ("token"); + + + +CREATE UNIQUE INDEX "patient_invites_one_active_per_owner" ON "public"."patient_invites" USING "btree" ("owner_id") WHERE ("active" = true); + + + +CREATE INDEX "patient_invites_owner_id_idx" ON "public"."patient_invites" USING "btree" ("owner_id"); + + + +CREATE INDEX "patient_invites_tenant_idx" ON "public"."patient_invites" USING "btree" ("tenant_id"); + + + +CREATE INDEX "patient_invites_token_idx" ON "public"."patient_invites" USING "btree" ("token"); + + + +CREATE INDEX "patient_patient_tag_tenant_idx" ON "public"."patient_patient_tag" USING "btree" ("tenant_id"); + + + +CREATE UNIQUE INDEX "patient_tags_owner_name_uq" ON "public"."patient_tags" USING "btree" ("owner_id", "lower"("nome")); + + + +CREATE INDEX "patient_tags_tenant_idx" ON "public"."patient_tags" USING "btree" ("tenant_id"); + + + +CREATE INDEX "ppt_owner_idx" ON "public"."patient_patient_tag" USING "btree" ("owner_id"); + + + +CREATE INDEX "ppt_patient_idx" ON "public"."patient_patient_tag" USING "btree" ("patient_id"); + + + +CREATE INDEX "ppt_tag_idx" ON "public"."patient_patient_tag" USING "btree" ("tag_id"); + + + +CREATE INDEX "sint_personal_created_idx" ON "public"."subscription_intents_personal" USING "btree" ("created_at" DESC); + + + +CREATE INDEX "sint_personal_status_idx" ON "public"."subscription_intents_personal" USING "btree" ("status"); + + + +CREATE INDEX "sint_tenant_created_idx" ON "public"."subscription_intents_tenant" USING "btree" ("created_at" DESC); + + + +CREATE INDEX "sint_tenant_status_idx" ON "public"."subscription_intents_tenant" USING "btree" ("status"); + + + +CREATE INDEX "sint_tenant_tenant_idx" ON "public"."subscription_intents_tenant" USING "btree" ("tenant_id"); + + + +CREATE INDEX "subscription_events_created_at_idx" ON "public"."subscription_events" USING "btree" ("created_at" DESC); + + + +CREATE INDEX "subscription_events_owner_ref_idx" ON "public"."subscription_events" USING "btree" ("owner_type", "owner_ref"); + + + +CREATE INDEX "subscription_events_sub_created_idx" ON "public"."subscription_events" USING "btree" ("subscription_id", "created_at" DESC); + + + +CREATE INDEX "subscription_events_subscription_id_idx" ON "public"."subscription_events" USING "btree" ("subscription_id"); + + + +CREATE UNIQUE INDEX "subscriptions_one_active_per_tenant" ON "public"."subscriptions" USING "btree" ("tenant_id") WHERE ("status" = 'active'::"text"); + + + +CREATE UNIQUE INDEX "subscriptions_one_active_per_user" ON "public"."subscriptions" USING "btree" ("user_id") WHERE ("status" = 'active'::"text"); + + + +CREATE UNIQUE INDEX "subscriptions_one_active_per_user_personal" ON "public"."subscriptions" USING "btree" ("user_id") WHERE (("tenant_id" IS NULL) AND ("status" = 'active'::"text")); + + + +CREATE INDEX "subscriptions_owner_idx" ON "public"."subscriptions" USING "btree" ("user_id"); + + + +CREATE INDEX "subscriptions_plan_key_idx" ON "public"."subscriptions" USING "btree" ("plan_key"); + + + +CREATE INDEX "subscriptions_status_idx" ON "public"."subscriptions" USING "btree" ("status"); + + + +CREATE INDEX "subscriptions_tenant_id_idx" ON "public"."subscriptions" USING "btree" ("tenant_id"); + + + +CREATE INDEX "subscriptions_tenant_period_end_idx" ON "public"."subscriptions" USING "btree" ("tenant_id", "current_period_end"); + + + +CREATE INDEX "subscriptions_tenant_status_idx" ON "public"."subscriptions" USING "btree" ("tenant_id", "status"); + + + +CREATE INDEX "subscriptions_user_status_idx" ON "public"."subscriptions" USING "btree" ("user_id", "status", "created_at" DESC); + + + +CREATE INDEX "tenant_members_tenant_idx" ON "public"."tenant_members" USING "btree" ("tenant_id"); + + + +CREATE INDEX "tenant_members_user_idx" ON "public"."tenant_members" USING "btree" ("user_id"); + + + +CREATE INDEX "tenant_modules_owner_idx" ON "public"."tenant_modules" USING "btree" ("owner_id"); + + + +CREATE UNIQUE INDEX "unique_member_per_tenant" ON "public"."tenant_members" USING "btree" ("tenant_id", "user_id"); + + + +CREATE UNIQUE INDEX "uq_patients_tenant_user" ON "public"."patients" USING "btree" ("tenant_id", "user_id") WHERE ("user_id" IS NOT NULL); + + + +CREATE UNIQUE INDEX "uq_plan_price_active" ON "public"."plan_prices" USING "btree" ("plan_id", "interval", "currency") WHERE (("is_active" = true) AND ("active_to" IS NULL)); + + + +CREATE UNIQUE INDEX "uq_plan_prices_active" ON "public"."plan_prices" USING "btree" ("plan_id", "interval") WHERE ("is_active" = true); + + + +CREATE UNIQUE INDEX "uq_subscriptions_active_by_tenant" ON "public"."subscriptions" USING "btree" ("tenant_id") WHERE (("tenant_id" IS NOT NULL) AND ("status" = 'active'::"text")); + + + +CREATE UNIQUE INDEX "uq_subscriptions_active_personal_by_user" ON "public"."subscriptions" USING "btree" ("user_id") WHERE (("tenant_id" IS NULL) AND ("status" = 'active'::"text")); + + + +CREATE UNIQUE INDEX "uq_tenant_invites_pending" ON "public"."tenant_invites" USING "btree" ("tenant_id", "lower"("email"), "role") WHERE (("accepted_at" IS NULL) AND ("revoked_at" IS NULL)); + + + +CREATE UNIQUE INDEX "uq_tenant_members_tenant_user" ON "public"."tenant_members" USING "btree" ("tenant_id", "user_id"); + + + +CREATE UNIQUE INDEX "ux_subscriptions_active_per_personal_user" ON "public"."subscriptions" USING "btree" ("user_id") WHERE (("status" = 'active'::"text") AND ("tenant_id" IS NULL)); + + + +CREATE UNIQUE INDEX "ux_subscriptions_active_per_tenant" ON "public"."subscriptions" USING "btree" ("tenant_id") WHERE (("status" = 'active'::"text") AND ("tenant_id" IS NOT NULL)); + + + +CREATE OR REPLACE TRIGGER "prevent_saas_membership_trigger" BEFORE INSERT ON "public"."tenant_members" FOR EACH ROW EXECUTE FUNCTION "public"."prevent_saas_membership"(); + + + +CREATE OR REPLACE TRIGGER "tg_agenda_configuracoes_updated_at" BEFORE UPDATE ON "public"."agenda_configuracoes" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); + + + +CREATE OR REPLACE TRIGGER "tg_agenda_eventos_updated_at" BEFORE UPDATE ON "public"."agenda_eventos" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); + + + +CREATE OR REPLACE TRIGGER "tg_agenda_excecoes_updated_at" BEFORE UPDATE ON "public"."agenda_excecoes" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); + + + +CREATE OR REPLACE TRIGGER "tg_agenda_regras_semanais_updated_at" BEFORE UPDATE ON "public"."agenda_regras_semanais" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); + + + +CREATE OR REPLACE TRIGGER "tr_plan_public_updated_at" BEFORE UPDATE ON "public"."plan_public" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); + + + +CREATE OR REPLACE TRIGGER "trg_account_type_immutable" BEFORE UPDATE OF "account_type" ON "public"."profiles" FOR EACH ROW EXECUTE FUNCTION "public"."guard_account_type_immutable"(); + + + +CREATE OR REPLACE TRIGGER "trg_agenda_cfg_sync" BEFORE INSERT OR UPDATE ON "public"."agenda_configuracoes" FOR EACH ROW EXECUTE FUNCTION "public"."agenda_cfg_sync"(); + + + +CREATE OR REPLACE TRIGGER "trg_agenda_eventos_busy_mirror_del" AFTER DELETE ON "public"."agenda_eventos" FOR EACH ROW WHEN ((("old"."mirror_of_event_id" IS NULL) AND ("old"."tenant_id" = "old"."owner_id"))) EXECUTE FUNCTION "public"."sync_busy_mirror_agenda_eventos"(); + + + +CREATE OR REPLACE TRIGGER "trg_agenda_eventos_busy_mirror_ins" AFTER INSERT ON "public"."agenda_eventos" FOR EACH ROW WHEN ((("new"."mirror_of_event_id" IS NULL) AND ("new"."tenant_id" = "new"."owner_id") AND ("new"."visibility_scope" = ANY (ARRAY['busy_only'::"text", 'private'::"text"])))) EXECUTE FUNCTION "public"."sync_busy_mirror_agenda_eventos"(); + + + +CREATE OR REPLACE TRIGGER "trg_agenda_eventos_busy_mirror_upd" AFTER UPDATE ON "public"."agenda_eventos" FOR EACH ROW WHEN ((("new"."mirror_of_event_id" IS NULL) AND ("new"."tenant_id" = "new"."owner_id") AND (("new"."visibility_scope" IS DISTINCT FROM "old"."visibility_scope") OR ("new"."inicio_em" IS DISTINCT FROM "old"."inicio_em") OR ("new"."fim_em" IS DISTINCT FROM "old"."fim_em") OR ("new"."owner_id" IS DISTINCT FROM "old"."owner_id") OR ("new"."tenant_id" IS DISTINCT FROM "old"."tenant_id")))) EXECUTE FUNCTION "public"."sync_busy_mirror_agenda_eventos"(); + + + +CREATE OR REPLACE TRIGGER "trg_agenda_regras_semanais_no_overlap" BEFORE INSERT OR UPDATE ON "public"."agenda_regras_semanais" FOR EACH ROW EXECUTE FUNCTION "public"."fn_agenda_regras_semanais_no_overlap"(); + + + +CREATE OR REPLACE TRIGGER "trg_determined_commitment_fields_updated_at" BEFORE UPDATE ON "public"."determined_commitment_fields" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); + + + +CREATE OR REPLACE TRIGGER "trg_determined_commitments_updated_at" BEFORE UPDATE ON "public"."determined_commitments" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); + + + +CREATE OR REPLACE TRIGGER "trg_no_change_core_plan_key" BEFORE UPDATE ON "public"."plans" FOR EACH ROW EXECUTE FUNCTION "public"."guard_no_change_core_plan_key"(); + + + +CREATE OR REPLACE TRIGGER "trg_no_change_plan_target" BEFORE UPDATE ON "public"."plans" FOR EACH ROW EXECUTE FUNCTION "public"."guard_no_change_plan_target"(); + + + +CREATE OR REPLACE TRIGGER "trg_no_delete_core_plans" BEFORE DELETE ON "public"."plans" FOR EACH ROW EXECUTE FUNCTION "public"."guard_no_delete_core_plans"(); + + + +CREATE OR REPLACE TRIGGER "trg_patient_cannot_own_tenant" BEFORE INSERT OR UPDATE ON "public"."tenant_members" FOR EACH ROW EXECUTE FUNCTION "public"."guard_patient_cannot_own_tenant"(); + + + +CREATE OR REPLACE TRIGGER "trg_patient_groups_set_updated_at" BEFORE UPDATE ON "public"."patient_groups" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); + + + +CREATE OR REPLACE TRIGGER "trg_patient_intake_requests_updated_at" BEFORE UPDATE ON "public"."patient_intake_requests" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); + + + +CREATE OR REPLACE TRIGGER "trg_patient_tags_set_updated_at" BEFORE UPDATE ON "public"."patient_tags" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); + + + +CREATE OR REPLACE TRIGGER "trg_patients_updated_at" BEFORE UPDATE ON "public"."patients" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); + + + +CREATE OR REPLACE TRIGGER "trg_patients_validate_members" BEFORE INSERT OR UPDATE OF "tenant_id", "responsible_member_id", "patient_scope", "therapist_member_id" ON "public"."patients" FOR EACH ROW EXECUTE FUNCTION "public"."patients_validate_member_consistency"(); + + + +CREATE OR REPLACE TRIGGER "trg_prevent_promoting_to_system" BEFORE UPDATE ON "public"."patient_groups" FOR EACH ROW EXECUTE FUNCTION "public"."prevent_promoting_to_system"(); + + + +CREATE OR REPLACE TRIGGER "trg_prevent_system_group_changes" BEFORE DELETE OR UPDATE ON "public"."patient_groups" FOR EACH ROW EXECUTE FUNCTION "public"."prevent_system_group_changes"(); + + + +CREATE OR REPLACE TRIGGER "trg_profiles_updated_at" BEFORE UPDATE ON "public"."profiles" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); + + + +CREATE OR REPLACE TRIGGER "trg_subscription_intents_view_insert" INSTEAD OF INSERT ON "public"."subscription_intents" FOR EACH ROW EXECUTE FUNCTION "public"."subscription_intents_view_insert"(); + + + +CREATE OR REPLACE TRIGGER "trg_subscriptions_validate_scope" BEFORE INSERT OR UPDATE ON "public"."subscriptions" FOR EACH ROW EXECUTE FUNCTION "public"."subscriptions_validate_scope"(); + + + +CREATE OR REPLACE TRIGGER "trg_tenant_features_guard_with_plan" BEFORE INSERT OR UPDATE ON "public"."tenant_features" FOR EACH ROW EXECUTE FUNCTION "public"."tenant_features_guard_with_plan"(); + + + +CREATE OR REPLACE TRIGGER "trg_tenant_features_updated_at" BEFORE UPDATE ON "public"."tenant_features" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); + + + +CREATE OR REPLACE TRIGGER "trg_tenant_kind_immutable" BEFORE UPDATE OF "kind" ON "public"."tenants" FOR EACH ROW EXECUTE FUNCTION "public"."guard_tenant_kind_immutable"(); + + + +CREATE OR REPLACE TRIGGER "trg_user_settings_updated_at" BEFORE UPDATE ON "public"."user_settings" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); + + + +ALTER TABLE ONLY "public"."agenda_configuracoes" + ADD CONSTRAINT "agenda_configuracoes_tenant_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."agenda_eventos" + ADD CONSTRAINT "agenda_eventos_determined_commitment_fk" FOREIGN KEY ("determined_commitment_id") REFERENCES "public"."determined_commitments"("id") ON DELETE SET NULL; + + + +ALTER TABLE ONLY "public"."agenda_eventos" + ADD CONSTRAINT "agenda_eventos_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "public"."patients"("id") ON DELETE SET NULL; + + + +ALTER TABLE ONLY "public"."agenda_eventos" + ADD CONSTRAINT "agenda_eventos_terapeuta_fk" FOREIGN KEY ("terapeuta_id") REFERENCES "auth"."users"("id") ON DELETE SET NULL; + + + +ALTER TABLE ONLY "public"."agenda_excecoes" + ADD CONSTRAINT "agenda_excecoes_tenant_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."agenda_online_slots" + ADD CONSTRAINT "agenda_online_slots_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."agenda_online_slots" + ADD CONSTRAINT "agenda_online_slots_tenant_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."agenda_regras_semanais" + ADD CONSTRAINT "agenda_regras_semanais_tenant_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."agenda_slots_bloqueados_semanais" + ADD CONSTRAINT "agenda_slots_bloqueados_semanais_tenant_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."agenda_slots_regras" + ADD CONSTRAINT "agenda_slots_regras_tenant_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."commitment_time_logs" + ADD CONSTRAINT "commitment_time_logs_calendar_event_id_fkey" FOREIGN KEY ("calendar_event_id") REFERENCES "public"."agenda_eventos"("id") ON DELETE SET NULL; + + + +ALTER TABLE ONLY "public"."commitment_time_logs" + ADD CONSTRAINT "commitment_time_logs_commitment_id_fkey" FOREIGN KEY ("commitment_id") REFERENCES "public"."determined_commitments"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."commitment_time_logs" + ADD CONSTRAINT "commitment_time_logs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."determined_commitment_fields" + ADD CONSTRAINT "determined_commitment_fields_commitment_id_fkey" FOREIGN KEY ("commitment_id") REFERENCES "public"."determined_commitments"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."determined_commitment_fields" + ADD CONSTRAINT "determined_commitment_fields_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."determined_commitments" + ADD CONSTRAINT "determined_commitments_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."module_features" + ADD CONSTRAINT "module_features_feature_id_fkey" FOREIGN KEY ("feature_id") REFERENCES "public"."features"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."module_features" + ADD CONSTRAINT "module_features_module_id_fkey" FOREIGN KEY ("module_id") REFERENCES "public"."modules"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."patient_group_patient" + ADD CONSTRAINT "patient_group_patient_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "public"."patients"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."patient_group_patient" + ADD CONSTRAINT "patient_group_patient_tenant_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."patient_groups" + ADD CONSTRAINT "patient_groups_tenant_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."patient_intake_requests" + ADD CONSTRAINT "patient_intake_requests_tenant_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."patient_invites" + ADD CONSTRAINT "patient_invites_tenant_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."patient_patient_tag" + ADD CONSTRAINT "patient_patient_tag_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "public"."patient_tags"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."patient_patient_tag" + ADD CONSTRAINT "patient_patient_tag_tenant_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."patient_tags" + ADD CONSTRAINT "patient_tags_tenant_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."patients" + ADD CONSTRAINT "patients_responsible_member_id_fkey" FOREIGN KEY ("responsible_member_id") REFERENCES "public"."tenant_members"("id") ON DELETE RESTRICT; + + + +ALTER TABLE ONLY "public"."patients" + ADD CONSTRAINT "patients_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."patients" + ADD CONSTRAINT "patients_therapist_member_id_fkey" FOREIGN KEY ("therapist_member_id") REFERENCES "public"."tenant_members"("id"); + + + +ALTER TABLE ONLY "public"."patients" + ADD CONSTRAINT "patients_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE SET NULL; + + + +ALTER TABLE ONLY "public"."plan_features" + ADD CONSTRAINT "plan_features_feature_id_fkey" FOREIGN KEY ("feature_id") REFERENCES "public"."features"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."plan_features" + ADD CONSTRAINT "plan_features_plan_id_fkey" FOREIGN KEY ("plan_id") REFERENCES "public"."plans"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."plan_prices" + ADD CONSTRAINT "plan_prices_plan_id_fkey" FOREIGN KEY ("plan_id") REFERENCES "public"."plans"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."plan_public_bullets" + ADD CONSTRAINT "plan_public_bullets_plan_id_fkey" FOREIGN KEY ("plan_id") REFERENCES "public"."plans"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."plan_public" + ADD CONSTRAINT "plan_public_plan_id_fkey" FOREIGN KEY ("plan_id") REFERENCES "public"."plans"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."patient_patient_tag" + ADD CONSTRAINT "ppt_patient_fk" FOREIGN KEY ("patient_id") REFERENCES "public"."patients"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."patient_patient_tag" + ADD CONSTRAINT "ppt_tag_fk" FOREIGN KEY ("tag_id") REFERENCES "public"."patient_tags"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."profiles" + ADD CONSTRAINT "profiles_id_fkey" FOREIGN KEY ("id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."saas_admins" + ADD CONSTRAINT "saas_admins_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."subscription_intents_personal" + ADD CONSTRAINT "sint_personal_subscription_id_fkey" FOREIGN KEY ("subscription_id") REFERENCES "public"."subscriptions"("id") ON DELETE SET NULL; + + + +ALTER TABLE ONLY "public"."subscription_intents_tenant" + ADD CONSTRAINT "sint_tenant_subscription_id_fkey" FOREIGN KEY ("subscription_id") REFERENCES "public"."subscriptions"("id") ON DELETE SET NULL; + + + +ALTER TABLE ONLY "public"."subscription_events" + ADD CONSTRAINT "subscription_events_subscription_id_fkey" FOREIGN KEY ("subscription_id") REFERENCES "public"."subscriptions"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."subscription_intents_personal" + ADD CONSTRAINT "subscription_intents_personal_plan_id_fkey" FOREIGN KEY ("plan_id") REFERENCES "public"."plans"("id") ON DELETE RESTRICT; + + + +ALTER TABLE ONLY "public"."subscription_intents_tenant" + ADD CONSTRAINT "subscription_intents_tenant_plan_id_fkey" FOREIGN KEY ("plan_id") REFERENCES "public"."plans"("id") ON DELETE RESTRICT; + + + +ALTER TABLE ONLY "public"."subscription_intents_legacy" + ADD CONSTRAINT "subscription_intents_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE SET NULL; + + + +ALTER TABLE ONLY "public"."subscriptions" + ADD CONSTRAINT "subscriptions_owner_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."subscriptions" + ADD CONSTRAINT "subscriptions_plan_id_fkey" FOREIGN KEY ("plan_id") REFERENCES "public"."plans"("id") ON DELETE RESTRICT; + + + +ALTER TABLE ONLY "public"."tenant_features" + ADD CONSTRAINT "tenant_features_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."tenant_invites" + ADD CONSTRAINT "tenant_invites_accepted_by_fkey" FOREIGN KEY ("accepted_by") REFERENCES "auth"."users"("id") ON DELETE SET NULL; + + + +ALTER TABLE ONLY "public"."tenant_invites" + ADD CONSTRAINT "tenant_invites_invited_by_fkey" FOREIGN KEY ("invited_by") REFERENCES "auth"."users"("id") ON DELETE SET NULL; + + + +ALTER TABLE ONLY "public"."tenant_invites" + ADD CONSTRAINT "tenant_invites_revoked_by_fkey" FOREIGN KEY ("revoked_by") REFERENCES "auth"."users"("id") ON DELETE SET NULL; + + + +ALTER TABLE ONLY "public"."tenant_invites" + ADD CONSTRAINT "tenant_invites_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."tenant_members" + ADD CONSTRAINT "tenant_members_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."tenant_members" + ADD CONSTRAINT "tenant_members_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."tenant_modules" + ADD CONSTRAINT "tenant_modules_module_id_fkey" FOREIGN KEY ("module_id") REFERENCES "public"."modules"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."tenant_modules" + ADD CONSTRAINT "tenant_modules_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."user_settings" + ADD CONSTRAINT "user_settings_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + + + +ALTER TABLE "public"."agenda_configuracoes" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "agenda_configuracoes_owner" ON "public"."agenda_configuracoes" FOR ALL USING (("owner_id" = "auth"."uid"())) WITH CHECK (("owner_id" = "auth"."uid"())); + +CREATE POLICY "agenda_configuracoes_clinic_read" ON "public"."agenda_configuracoes" FOR SELECT USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.view'::"text"))); + +CREATE POLICY "agenda_configuracoes_clinic_write" ON "public"."agenda_configuracoes" FOR ALL USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.edit'::"text"))) WITH CHECK (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.edit'::"text"))); + + + +ALTER TABLE "public"."agenda_eventos" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "agenda_eventos_delete" ON "public"."agenda_eventos" FOR DELETE USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.delete'::"text"))); + + + +CREATE POLICY "agenda_eventos_insert" ON "public"."agenda_eventos" FOR INSERT WITH CHECK (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.create'::"text"))); + + + +CREATE POLICY "agenda_eventos_select" ON "public"."agenda_eventos" FOR SELECT USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.view'::"text"))); + + + +CREATE POLICY "agenda_eventos_update" ON "public"."agenda_eventos" FOR UPDATE USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.edit'::"text"))) WITH CHECK (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.edit'::"text"))); + +-- Permite que o terapeuta/owner gerencie seus próprios eventos de agenda +CREATE POLICY "agenda_eventos_owner_all" ON "public"."agenda_eventos" + FOR ALL TO authenticated + USING ("owner_id" = "auth"."uid"()) + WITH CHECK ("owner_id" = "auth"."uid"()); + + + +ALTER TABLE "public"."agenda_excecoes" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "agenda_excecoes_select" ON "public"."agenda_excecoes" FOR SELECT USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.view'::"text"))); + + + +CREATE POLICY "agenda_excecoes_write" ON "public"."agenda_excecoes" USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.edit'::"text"))) WITH CHECK (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.edit'::"text"))); + + + +ALTER TABLE "public"."agenda_online_slots" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "agenda_online_slots_select" ON "public"."agenda_online_slots" FOR SELECT USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.view'::"text"))); + + + +CREATE POLICY "agenda_online_slots_write" ON "public"."agenda_online_slots" USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.edit'::"text"))) WITH CHECK (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.edit'::"text"))); + + + +ALTER TABLE "public"."agenda_regras_semanais" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "agenda_regras_semanais_select" ON "public"."agenda_regras_semanais" FOR SELECT USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.view'::"text"))); + + + +CREATE POLICY "agenda_regras_semanais_write" ON "public"."agenda_regras_semanais" USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.edit'::"text"))) WITH CHECK (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.edit'::"text"))); + + + +ALTER TABLE "public"."agenda_slots_bloqueados_semanais" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "agenda_slots_bloqueados_semanais_select" ON "public"."agenda_slots_bloqueados_semanais" FOR SELECT USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.view'::"text"))); + + + +CREATE POLICY "agenda_slots_bloqueados_semanais_write" ON "public"."agenda_slots_bloqueados_semanais" USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.edit'::"text"))) WITH CHECK (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.edit'::"text"))); + + + +ALTER TABLE "public"."agenda_slots_regras" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "agenda_slots_regras_select" ON "public"."agenda_slots_regras" FOR SELECT USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.view'::"text"))); + + + +CREATE POLICY "agenda_slots_regras_write" ON "public"."agenda_slots_regras" USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.edit'::"text"))) WITH CHECK (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'agenda.edit'::"text"))); + + + +ALTER TABLE "public"."commitment_time_logs" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "ctl_delete_for_active_member" ON "public"."commitment_time_logs" FOR DELETE TO "authenticated" USING ((EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "commitment_time_logs"."tenant_id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text"))))); + + + +CREATE POLICY "ctl_insert_for_active_member" ON "public"."commitment_time_logs" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "commitment_time_logs"."tenant_id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text"))))); + + + +CREATE POLICY "ctl_select_for_active_member" ON "public"."commitment_time_logs" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "commitment_time_logs"."tenant_id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text"))))); + + + +CREATE POLICY "ctl_update_for_active_member" ON "public"."commitment_time_logs" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "commitment_time_logs"."tenant_id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text"))))) WITH CHECK ((EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "commitment_time_logs"."tenant_id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text"))))); + + + +CREATE POLICY "dc_delete_custom_for_active_member" ON "public"."determined_commitments" FOR DELETE TO "authenticated" USING ((("is_native" = false) AND (EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "determined_commitments"."tenant_id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text")))))); + + + +CREATE POLICY "dc_insert_for_active_member" ON "public"."determined_commitments" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "determined_commitments"."tenant_id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text"))))); + + + +CREATE POLICY "dc_select_for_active_member" ON "public"."determined_commitments" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "determined_commitments"."tenant_id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text"))))); + + + +CREATE POLICY "dc_update_for_active_member" ON "public"."determined_commitments" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "determined_commitments"."tenant_id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text"))))) WITH CHECK ((EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "determined_commitments"."tenant_id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text"))))); + + + +CREATE POLICY "dcf_delete_for_active_member" ON "public"."determined_commitment_fields" FOR DELETE TO "authenticated" USING ((EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "determined_commitment_fields"."tenant_id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text"))))); + + + +CREATE POLICY "dcf_insert_for_active_member" ON "public"."determined_commitment_fields" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "determined_commitment_fields"."tenant_id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text"))))); + + + +CREATE POLICY "dcf_select_for_active_member" ON "public"."determined_commitment_fields" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "determined_commitment_fields"."tenant_id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text"))))); + + + +CREATE POLICY "dcf_update_for_active_member" ON "public"."determined_commitment_fields" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "determined_commitment_fields"."tenant_id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text"))))) WITH CHECK ((EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "determined_commitment_fields"."tenant_id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text"))))); + + + +ALTER TABLE "public"."determined_commitment_fields" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."determined_commitments" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "dev_creds_select_saas_admin" ON "public"."dev_user_credentials" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1 + FROM "public"."profiles" "p" + WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = 'saas_admin'::"text"))))); + + + +CREATE POLICY "dev_creds_write_saas_admin" ON "public"."dev_user_credentials" TO "authenticated" USING ((EXISTS ( SELECT 1 + FROM "public"."profiles" "p" + WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = 'saas_admin'::"text"))))) WITH CHECK ((EXISTS ( SELECT 1 + FROM "public"."profiles" "p" + WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = 'saas_admin'::"text"))))); + + + +ALTER TABLE "public"."dev_user_credentials" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "ent_inv_select_own" ON "public"."entitlements_invalidation" FOR SELECT USING ((("owner_id" = "auth"."uid"()) OR "public"."is_saas_admin"())); + + + +CREATE POLICY "ent_inv_update_saas" ON "public"."entitlements_invalidation" FOR UPDATE USING ("public"."is_saas_admin"()) WITH CHECK ("public"."is_saas_admin"()); + + + +CREATE POLICY "ent_inv_write_saas" ON "public"."entitlements_invalidation" FOR INSERT WITH CHECK ("public"."is_saas_admin"()); + + + +ALTER TABLE "public"."entitlements_invalidation" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."features" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "features_read_authenticated" ON "public"."features" FOR SELECT TO "authenticated" USING (true); + + + +CREATE POLICY "features_write_saas_admin" ON "public"."features" TO "authenticated" USING ("public"."is_saas_admin"()) WITH CHECK ("public"."is_saas_admin"()); + + + +ALTER TABLE "public"."module_features" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "module_features_read_authenticated" ON "public"."module_features" FOR SELECT TO "authenticated" USING (true); + + + +CREATE POLICY "module_features_write_saas_admin" ON "public"."module_features" TO "authenticated" USING ("public"."is_saas_admin"()) WITH CHECK ("public"."is_saas_admin"()); + + + +ALTER TABLE "public"."modules" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "modules_read_authenticated" ON "public"."modules" FOR SELECT TO "authenticated" USING (true); + + + +CREATE POLICY "modules_write_saas_admin" ON "public"."modules" TO "authenticated" USING ("public"."is_saas_admin"()) WITH CHECK ("public"."is_saas_admin"()); + + + +ALTER TABLE "public"."owner_users" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "owner_users: user can read own links" ON "public"."owner_users" FOR SELECT TO "authenticated" USING (("user_id" = "auth"."uid"())); + + + +ALTER TABLE "public"."patient_group_patient" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "patient_group_patient_select" ON "public"."patient_group_patient" FOR SELECT USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.view'::"text"))); + + + +CREATE POLICY "patient_group_patient_write" ON "public"."patient_group_patient" USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.edit'::"text"))) WITH CHECK (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.edit'::"text"))); + +-- Permite que o terapeuta/owner gerencie os grupos dos seus próprios pacientes +CREATE POLICY "patient_group_patient_owner_all" ON "public"."patient_group_patient" + FOR ALL TO authenticated + USING ( + EXISTS ( + SELECT 1 FROM "public"."patients" p + WHERE p.id = "patient_id" + AND p.owner_id = "auth"."uid"() + ) + ) + WITH CHECK ( + EXISTS ( + SELECT 1 FROM "public"."patients" p + WHERE p.id = "patient_id" + AND p.owner_id = "auth"."uid"() + ) + ); + + + +ALTER TABLE "public"."patient_groups" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "patient_groups_select" ON "public"."patient_groups" FOR SELECT USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.view'::"text"))); + + + +CREATE POLICY "patient_groups_write" ON "public"."patient_groups" USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.edit'::"text"))) WITH CHECK (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.edit'::"text"))); + +-- Permite que o terapeuta/owner acesse seus próprios grupos (incluindo via onboarding individual) +CREATE POLICY "patient_groups_owner_all" ON "public"."patient_groups" + FOR ALL TO authenticated + USING ("owner_id" = "auth"."uid"()) + WITH CHECK ("owner_id" = "auth"."uid"()); + + + +ALTER TABLE "public"."patient_intake_requests" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "patient_intake_requests_select" ON "public"."patient_intake_requests" FOR SELECT USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.view'::"text"))); + + + +CREATE POLICY "patient_intake_requests_write" ON "public"."patient_intake_requests" USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.edit'::"text"))) WITH CHECK (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.edit'::"text"))); + +-- Permite que o terapeuta/owner veja e gerencie os cadastros recebidos dos seus links +CREATE POLICY "patient_intake_requests_owner_all" ON "public"."patient_intake_requests" + FOR ALL TO authenticated + USING ("owner_id" = "auth"."uid"()) + WITH CHECK ("owner_id" = "auth"."uid"()); + + + +ALTER TABLE "public"."patient_invites" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "patient_invites_select" ON "public"."patient_invites" FOR SELECT USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.view'::"text"))); + + + +CREATE POLICY "patient_invites_write" ON "public"."patient_invites" USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.edit'::"text"))) WITH CHECK (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.edit'::"text"))); + +-- Permite que o terapeuta/owner gerencie seus próprios convites de cadastro externo +CREATE POLICY "patient_invites_owner_all" ON "public"."patient_invites" + FOR ALL TO authenticated + USING ("owner_id" = "auth"."uid"()) + WITH CHECK ("owner_id" = "auth"."uid"()); + + + +ALTER TABLE "public"."patient_patient_tag" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "patient_patient_tag_select" ON "public"."patient_patient_tag" FOR SELECT USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.view'::"text"))); + + + +CREATE POLICY "patient_patient_tag_write" ON "public"."patient_patient_tag" USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.edit'::"text"))) WITH CHECK (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.edit'::"text"))); + +-- Permite que o terapeuta/owner gerencie a pivot tag<->paciente pelos seus próprios registros +CREATE POLICY "patient_patient_tag_owner_all" ON "public"."patient_patient_tag" + FOR ALL TO authenticated + USING ("owner_id" = "auth"."uid"()) + WITH CHECK ("owner_id" = "auth"."uid"()); + + + +ALTER TABLE "public"."patient_tags" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "patient_tags_select" ON "public"."patient_tags" FOR SELECT USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.view'::"text"))); + + + +CREATE POLICY "patient_tags_write" ON "public"."patient_tags" USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.edit'::"text"))) WITH CHECK (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.edit'::"text"))); + +-- Permite que o terapeuta/owner acesse suas próprias tags +CREATE POLICY "patient_tags_owner_all" ON "public"."patient_tags" + FOR ALL TO authenticated + USING ("owner_id" = "auth"."uid"()) + WITH CHECK ("owner_id" = "auth"."uid"()); + + + +ALTER TABLE "public"."patients" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "patients_delete" ON "public"."patients" FOR DELETE USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.delete'::"text"))); + + + +CREATE POLICY "patients_insert" ON "public"."patients" FOR INSERT WITH CHECK (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.create'::"text"))); + + + +CREATE POLICY "patients_select" ON "public"."patients" FOR SELECT USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.view'::"text"))); + + + +CREATE POLICY "patients_update" ON "public"."patients" FOR UPDATE USING (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.edit'::"text"))) WITH CHECK (("public"."is_clinic_tenant"("tenant_id") AND "public"."is_tenant_member"("tenant_id") AND "public"."tenant_has_feature"("tenant_id", 'patients.edit'::"text"))); + + + +ALTER TABLE "public"."plan_features" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "plan_features_read_authenticated" ON "public"."plan_features" FOR SELECT TO "authenticated" USING (true); + + + +CREATE POLICY "plan_features_write_saas_admin" ON "public"."plan_features" TO "authenticated" USING ("public"."is_saas_admin"()) WITH CHECK ("public"."is_saas_admin"()); + + + +ALTER TABLE "public"."plans" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "plans_read_authenticated" ON "public"."plans" FOR SELECT TO "authenticated" USING (true); + + + +CREATE POLICY "plans_write_saas_admin" ON "public"."plans" TO "authenticated" USING ("public"."is_saas_admin"()) WITH CHECK ("public"."is_saas_admin"()); + + + +ALTER TABLE "public"."profiles" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "profiles_insert_own" ON "public"."profiles" FOR INSERT WITH CHECK (("id" = "auth"."uid"())); + + + +CREATE POLICY "profiles_read_saas_admin" ON "public"."profiles" FOR SELECT USING ("public"."is_saas_admin"()); + + + +CREATE POLICY "profiles_select_own" ON "public"."profiles" FOR SELECT USING (("id" = "auth"."uid"())); + + + +CREATE POLICY "profiles_update_own" ON "public"."profiles" FOR UPDATE USING (("id" = "auth"."uid"())) WITH CHECK (("id" = "auth"."uid"())); + + + +CREATE POLICY "read features (auth)" ON "public"."features" FOR SELECT TO "authenticated" USING (true); + + + +CREATE POLICY "read plan_features (auth)" ON "public"."plan_features" FOR SELECT TO "authenticated" USING (true); + + + +CREATE POLICY "read plans (auth)" ON "public"."plans" FOR SELECT TO "authenticated" USING (true); + + + +CREATE POLICY "saas_admin can read subscription_intents" ON "public"."subscription_intents_legacy" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1 + FROM "public"."saas_admins" "a" + WHERE ("a"."user_id" = "auth"."uid"())))); + + + +CREATE POLICY "saas_admin can update subscription_intents" ON "public"."subscription_intents_legacy" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1 + FROM "public"."saas_admins" "a" + WHERE ("a"."user_id" = "auth"."uid"())))) WITH CHECK ((EXISTS ( SELECT 1 + FROM "public"."saas_admins" "a" + WHERE ("a"."user_id" = "auth"."uid"())))); + + + +ALTER TABLE "public"."saas_admins" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "saas_admins_select_self" ON "public"."saas_admins" FOR SELECT TO "authenticated" USING (("user_id" = "auth"."uid"())); + + + +ALTER TABLE "public"."subscription_events" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "subscription_events_read_saas" ON "public"."subscription_events" FOR SELECT USING ("public"."is_saas_admin"()); + + + +CREATE POLICY "subscription_events_write_saas" ON "public"."subscription_events" FOR INSERT WITH CHECK ("public"."is_saas_admin"()); + + + +CREATE POLICY "subscription_intents_insert_own" ON "public"."subscription_intents_legacy" FOR INSERT TO "authenticated" WITH CHECK (("user_id" = "auth"."uid"())); + + + +ALTER TABLE "public"."subscription_intents_legacy" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "subscription_intents_select_own" ON "public"."subscription_intents_legacy" FOR SELECT TO "authenticated" USING (("user_id" = "auth"."uid"())); + + + +ALTER TABLE "public"."subscriptions" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "subscriptions read own" ON "public"."subscriptions" FOR SELECT TO "authenticated" USING (("user_id" = "auth"."uid"())); + + + +CREATE POLICY "subscriptions: read if linked owner_users" ON "public"."subscriptions" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1 + FROM "public"."owner_users" "ou" + WHERE (("ou"."owner_id" = "subscriptions"."user_id") AND ("ou"."user_id" = "auth"."uid"()))))); + + + +CREATE POLICY "subscriptions_insert_own_personal" ON "public"."subscriptions" FOR INSERT TO "authenticated" WITH CHECK ((("user_id" = "auth"."uid"()) AND ("tenant_id" IS NULL))); + + + +CREATE POLICY "subscriptions_no_direct_update" ON "public"."subscriptions" FOR UPDATE TO "authenticated" USING (false) WITH CHECK (false); + + + +CREATE POLICY "subscriptions_read_own" ON "public"."subscriptions" FOR SELECT TO "authenticated" USING ((("user_id" = "auth"."uid"()) OR "public"."is_saas_admin"())); + + + +CREATE POLICY "subscriptions_select_for_tenant_members" ON "public"."subscriptions" FOR SELECT TO "authenticated" USING ((("tenant_id" IS NOT NULL) AND (EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "subscriptions"."tenant_id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text")))))); + + + +CREATE POLICY "subscriptions_select_own_personal" ON "public"."subscriptions" FOR SELECT TO "authenticated" USING ((("user_id" = "auth"."uid"()) AND ("tenant_id" IS NULL))); + + + +CREATE POLICY "subscriptions_update_only_saas_admin" ON "public"."subscriptions" FOR UPDATE TO "authenticated" USING ("public"."is_saas_admin"()) WITH CHECK ("public"."is_saas_admin"()); + + + +ALTER TABLE "public"."tenant_members" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "tenant_members_write_saas" ON "public"."tenant_members" TO "authenticated" USING ("public"."is_saas_admin"()) WITH CHECK ("public"."is_saas_admin"()); + + + +ALTER TABLE "public"."tenant_modules" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "tenant_modules_read_own" ON "public"."tenant_modules" FOR SELECT TO "authenticated" USING ((("owner_id" = "auth"."uid"()) OR "public"."is_saas_admin"())); + + + +CREATE POLICY "tenant_modules_write_saas" ON "public"."tenant_modules" TO "authenticated" USING ("public"."is_saas_admin"()) WITH CHECK ("public"."is_saas_admin"()); + + + +ALTER TABLE "public"."tenants" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "tenants_read_members" ON "public"."tenants" FOR SELECT TO "authenticated" USING (("public"."is_saas_admin"() OR (EXISTS ( SELECT 1 + FROM "public"."tenant_members" "tm" + WHERE (("tm"."tenant_id" = "tenants"."id") AND ("tm"."user_id" = "auth"."uid"()) AND ("tm"."status" = 'active'::"text")))))); + + + +CREATE POLICY "tenants_write_saas" ON "public"."tenants" TO "authenticated" USING ("public"."is_saas_admin"()) WITH CHECK ("public"."is_saas_admin"()); + + + +CREATE POLICY "tm_select_admin_all_members" ON "public"."tenant_members" FOR SELECT TO "authenticated" USING ("public"."is_tenant_admin"("tenant_id")); + + + +CREATE POLICY "tm_select_own_membership" ON "public"."tenant_members" FOR SELECT TO "authenticated" USING (("user_id" = "auth"."uid"())); + + + +ALTER TABLE "public"."user_settings" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "user_settings_insert_own" ON "public"."user_settings" FOR INSERT WITH CHECK (("user_id" = "auth"."uid"())); + + + +CREATE POLICY "user_settings_select_own" ON "public"."user_settings" FOR SELECT USING (("user_id" = "auth"."uid"())); + + + +CREATE POLICY "user_settings_update_own" ON "public"."user_settings" FOR UPDATE USING (("user_id" = "auth"."uid"())) WITH CHECK (("user_id" = "auth"."uid"())); + + + + + +ALTER PUBLICATION "supabase_realtime" OWNER TO "postgres"; + + + + + +GRANT USAGE ON SCHEMA "public" TO "postgres"; +GRANT USAGE ON SCHEMA "public" TO "anon"; +GRANT USAGE ON SCHEMA "public" TO "authenticated"; +GRANT USAGE ON SCHEMA "public" TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citextin"("cstring") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citextin"("cstring") TO "anon"; +GRANT ALL ON FUNCTION "public"."citextin"("cstring") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citextin"("cstring") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citextout"("public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citextout"("public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citextout"("public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citextout"("public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citextrecv"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citextrecv"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."citextrecv"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citextrecv"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citextsend"("public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citextsend"("public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citextsend"("public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citextsend"("public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbtreekey16_in"("cstring") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbtreekey16_in"("cstring") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbtreekey16_in"("cstring") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbtreekey16_in"("cstring") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbtreekey16_out"("public"."gbtreekey16") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbtreekey16_out"("public"."gbtreekey16") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbtreekey16_out"("public"."gbtreekey16") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbtreekey16_out"("public"."gbtreekey16") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbtreekey2_in"("cstring") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbtreekey2_in"("cstring") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbtreekey2_in"("cstring") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbtreekey2_in"("cstring") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbtreekey2_out"("public"."gbtreekey2") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbtreekey2_out"("public"."gbtreekey2") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbtreekey2_out"("public"."gbtreekey2") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbtreekey2_out"("public"."gbtreekey2") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbtreekey32_in"("cstring") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbtreekey32_in"("cstring") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbtreekey32_in"("cstring") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbtreekey32_in"("cstring") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbtreekey32_out"("public"."gbtreekey32") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbtreekey32_out"("public"."gbtreekey32") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbtreekey32_out"("public"."gbtreekey32") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbtreekey32_out"("public"."gbtreekey32") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbtreekey4_in"("cstring") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbtreekey4_in"("cstring") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbtreekey4_in"("cstring") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbtreekey4_in"("cstring") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbtreekey4_out"("public"."gbtreekey4") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbtreekey4_out"("public"."gbtreekey4") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbtreekey4_out"("public"."gbtreekey4") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbtreekey4_out"("public"."gbtreekey4") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbtreekey8_in"("cstring") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbtreekey8_in"("cstring") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbtreekey8_in"("cstring") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbtreekey8_in"("cstring") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbtreekey8_out"("public"."gbtreekey8") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbtreekey8_out"("public"."gbtreekey8") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbtreekey8_out"("public"."gbtreekey8") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbtreekey8_out"("public"."gbtreekey8") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbtreekey_var_in"("cstring") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbtreekey_var_in"("cstring") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbtreekey_var_in"("cstring") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbtreekey_var_in"("cstring") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbtreekey_var_out"("public"."gbtreekey_var") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbtreekey_var_out"("public"."gbtreekey_var") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbtreekey_var_out"("public"."gbtreekey_var") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbtreekey_var_out"("public"."gbtreekey_var") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gtrgm_in"("cstring") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gtrgm_in"("cstring") TO "anon"; +GRANT ALL ON FUNCTION "public"."gtrgm_in"("cstring") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gtrgm_in"("cstring") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gtrgm_out"("public"."gtrgm") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gtrgm_out"("public"."gtrgm") TO "anon"; +GRANT ALL ON FUNCTION "public"."gtrgm_out"("public"."gtrgm") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gtrgm_out"("public"."gtrgm") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext"(boolean) TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext"(boolean) TO "anon"; +GRANT ALL ON FUNCTION "public"."citext"(boolean) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext"(boolean) TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext"(character) TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext"(character) TO "anon"; +GRANT ALL ON FUNCTION "public"."citext"(character) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext"(character) TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext"("inet") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext"("inet") TO "anon"; +GRANT ALL ON FUNCTION "public"."citext"("inet") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext"("inet") TO "service_role"; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +GRANT ALL ON FUNCTION "public"."__rls_ping"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."__rls_ping"() TO "anon"; +GRANT ALL ON FUNCTION "public"."__rls_ping"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."__rls_ping"() TO "service_role"; + + + +GRANT ALL ON TABLE "public"."subscriptions" TO "postgres"; +GRANT ALL ON TABLE "public"."subscriptions" TO "anon"; +GRANT ALL ON TABLE "public"."subscriptions" TO "authenticated"; +GRANT ALL ON TABLE "public"."subscriptions" TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."activate_subscription_from_intent"("p_intent_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."activate_subscription_from_intent"("p_intent_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."activate_subscription_from_intent"("p_intent_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."activate_subscription_from_intent"("p_intent_id" "uuid") TO "service_role"; + + + +REVOKE ALL ON FUNCTION "public"."admin_fix_plan_target"("p_plan_key" "text", "p_new_target" "text") FROM PUBLIC; +GRANT ALL ON FUNCTION "public"."admin_fix_plan_target"("p_plan_key" "text", "p_new_target" "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."admin_fix_plan_target"("p_plan_key" "text", "p_new_target" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."admin_fix_plan_target"("p_plan_key" "text", "p_new_target" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."admin_fix_plan_target"("p_plan_key" "text", "p_new_target" "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."agenda_cfg_sync"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."agenda_cfg_sync"() TO "anon"; +GRANT ALL ON FUNCTION "public"."agenda_cfg_sync"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."agenda_cfg_sync"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."cancel_subscription"("p_subscription_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."cancel_subscription"("p_subscription_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."cancel_subscription"("p_subscription_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."cancel_subscription"("p_subscription_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."cash_dist"("money", "money") TO "postgres"; +GRANT ALL ON FUNCTION "public"."cash_dist"("money", "money") TO "anon"; +GRANT ALL ON FUNCTION "public"."cash_dist"("money", "money") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."cash_dist"("money", "money") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."change_subscription_plan"("p_subscription_id" "uuid", "p_new_plan_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."change_subscription_plan"("p_subscription_id" "uuid", "p_new_plan_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."change_subscription_plan"("p_subscription_id" "uuid", "p_new_plan_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."change_subscription_plan"("p_subscription_id" "uuid", "p_new_plan_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext_cmp"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext_cmp"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citext_cmp"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext_cmp"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext_eq"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext_eq"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citext_eq"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext_eq"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext_ge"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext_ge"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citext_ge"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext_ge"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext_gt"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext_gt"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citext_gt"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext_gt"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext_hash"("public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext_hash"("public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citext_hash"("public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext_hash"("public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext_hash_extended"("public"."citext", bigint) TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext_hash_extended"("public"."citext", bigint) TO "anon"; +GRANT ALL ON FUNCTION "public"."citext_hash_extended"("public"."citext", bigint) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext_hash_extended"("public"."citext", bigint) TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext_larger"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext_larger"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citext_larger"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext_larger"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext_le"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext_le"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citext_le"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext_le"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext_lt"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext_lt"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citext_lt"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext_lt"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext_ne"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext_ne"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citext_ne"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext_ne"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext_pattern_cmp"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext_pattern_cmp"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citext_pattern_cmp"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext_pattern_cmp"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext_pattern_ge"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext_pattern_ge"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citext_pattern_ge"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext_pattern_ge"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext_pattern_gt"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext_pattern_gt"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citext_pattern_gt"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext_pattern_gt"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext_pattern_le"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext_pattern_le"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citext_pattern_le"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext_pattern_le"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext_pattern_lt"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext_pattern_lt"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citext_pattern_lt"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext_pattern_lt"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."citext_smaller"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."citext_smaller"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."citext_smaller"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."citext_smaller"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."create_clinic_tenant"("p_name" "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."create_clinic_tenant"("p_name" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."create_clinic_tenant"("p_name" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."create_clinic_tenant"("p_name" "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."create_patient_intake_request"("p_token" "text", "p_name" "text", "p_email" "text", "p_phone" "text", "p_notes" "text", "p_consent" boolean) TO "postgres"; +GRANT ALL ON FUNCTION "public"."create_patient_intake_request"("p_token" "text", "p_name" "text", "p_email" "text", "p_phone" "text", "p_notes" "text", "p_consent" boolean) TO "anon"; +GRANT ALL ON FUNCTION "public"."create_patient_intake_request"("p_token" "text", "p_name" "text", "p_email" "text", "p_phone" "text", "p_notes" "text", "p_consent" boolean) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."create_patient_intake_request"("p_token" "text", "p_name" "text", "p_email" "text", "p_phone" "text", "p_notes" "text", "p_consent" boolean) TO "service_role"; + + + +REVOKE ALL ON FUNCTION "public"."create_patient_intake_request_v2"("p_token" "text", "p_payload" "jsonb") FROM PUBLIC; +GRANT ALL ON FUNCTION "public"."create_patient_intake_request_v2"("p_token" "text", "p_payload" "jsonb") TO "postgres"; +GRANT ALL ON FUNCTION "public"."create_patient_intake_request_v2"("p_token" "text", "p_payload" "jsonb") TO "anon"; +GRANT ALL ON FUNCTION "public"."create_patient_intake_request_v2"("p_token" "text", "p_payload" "jsonb") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."create_patient_intake_request_v2"("p_token" "text", "p_payload" "jsonb") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."current_member_id"("p_tenant_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."current_member_id"("p_tenant_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."current_member_id"("p_tenant_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."current_member_id"("p_tenant_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."current_member_role"("p_tenant_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."current_member_role"("p_tenant_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."current_member_role"("p_tenant_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."current_member_role"("p_tenant_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."date_dist"("date", "date") TO "postgres"; +GRANT ALL ON FUNCTION "public"."date_dist"("date", "date") TO "anon"; +GRANT ALL ON FUNCTION "public"."date_dist"("date", "date") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."date_dist"("date", "date") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."delete_commitment_full"("p_tenant_id" "uuid", "p_commitment_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."delete_commitment_full"("p_tenant_id" "uuid", "p_commitment_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."delete_commitment_full"("p_tenant_id" "uuid", "p_commitment_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."delete_determined_commitment"("p_tenant_id" "uuid", "p_commitment_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."delete_determined_commitment"("p_tenant_id" "uuid", "p_commitment_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."delete_determined_commitment"("p_tenant_id" "uuid", "p_commitment_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."delete_determined_commitment"("p_tenant_id" "uuid", "p_commitment_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."dev_list_auth_users"("p_limit" integer) TO "postgres"; +GRANT ALL ON FUNCTION "public"."dev_list_auth_users"("p_limit" integer) TO "anon"; +GRANT ALL ON FUNCTION "public"."dev_list_auth_users"("p_limit" integer) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."dev_list_auth_users"("p_limit" integer) TO "service_role"; + + + +REVOKE ALL ON FUNCTION "public"."dev_list_custom_users"() FROM PUBLIC; +GRANT ALL ON FUNCTION "public"."dev_list_custom_users"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."dev_list_custom_users"() TO "anon"; +GRANT ALL ON FUNCTION "public"."dev_list_custom_users"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."dev_list_custom_users"() TO "service_role"; + + + +REVOKE ALL ON FUNCTION "public"."dev_list_intent_leads"() FROM PUBLIC; +GRANT ALL ON FUNCTION "public"."dev_list_intent_leads"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."dev_list_intent_leads"() TO "anon"; +GRANT ALL ON FUNCTION "public"."dev_list_intent_leads"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."dev_list_intent_leads"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."dev_public_debug_snapshot"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."dev_public_debug_snapshot"() TO "anon"; +GRANT ALL ON FUNCTION "public"."dev_public_debug_snapshot"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."dev_public_debug_snapshot"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."ensure_personal_tenant"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."ensure_personal_tenant"() TO "anon"; +GRANT ALL ON FUNCTION "public"."ensure_personal_tenant"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."ensure_personal_tenant"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."ensure_personal_tenant_for_user"("p_user_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."ensure_personal_tenant_for_user"("p_user_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."ensure_personal_tenant_for_user"("p_user_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."ensure_personal_tenant_for_user"("p_user_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."fix_all_subscription_mismatches"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."fix_all_subscription_mismatches"() TO "anon"; +GRANT ALL ON FUNCTION "public"."fix_all_subscription_mismatches"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."fix_all_subscription_mismatches"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."float4_dist"(real, real) TO "postgres"; +GRANT ALL ON FUNCTION "public"."float4_dist"(real, real) TO "anon"; +GRANT ALL ON FUNCTION "public"."float4_dist"(real, real) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."float4_dist"(real, real) TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."float8_dist"(double precision, double precision) TO "postgres"; +GRANT ALL ON FUNCTION "public"."float8_dist"(double precision, double precision) TO "anon"; +GRANT ALL ON FUNCTION "public"."float8_dist"(double precision, double precision) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."float8_dist"(double precision, double precision) TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."fn_agenda_regras_semanais_no_overlap"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."fn_agenda_regras_semanais_no_overlap"() TO "anon"; +GRANT ALL ON FUNCTION "public"."fn_agenda_regras_semanais_no_overlap"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."fn_agenda_regras_semanais_no_overlap"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bit_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bit_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bit_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bit_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bit_consistent"("internal", bit, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bit_consistent"("internal", bit, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bit_consistent"("internal", bit, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bit_consistent"("internal", bit, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bit_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bit_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bit_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bit_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bit_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bit_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bit_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bit_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bit_same"("public"."gbtreekey_var", "public"."gbtreekey_var", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bit_same"("public"."gbtreekey_var", "public"."gbtreekey_var", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bit_same"("public"."gbtreekey_var", "public"."gbtreekey_var", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bit_same"("public"."gbtreekey_var", "public"."gbtreekey_var", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bit_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bit_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bit_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bit_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bool_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bool_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bool_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bool_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bool_consistent"("internal", boolean, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bool_consistent"("internal", boolean, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bool_consistent"("internal", boolean, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bool_consistent"("internal", boolean, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bool_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bool_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bool_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bool_fetch"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bool_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bool_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bool_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bool_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bool_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bool_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bool_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bool_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bool_same"("public"."gbtreekey2", "public"."gbtreekey2", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bool_same"("public"."gbtreekey2", "public"."gbtreekey2", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bool_same"("public"."gbtreekey2", "public"."gbtreekey2", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bool_same"("public"."gbtreekey2", "public"."gbtreekey2", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bool_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bool_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bool_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bool_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bpchar_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bpchar_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bpchar_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bpchar_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bpchar_consistent"("internal", character, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bpchar_consistent"("internal", character, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bpchar_consistent"("internal", character, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bpchar_consistent"("internal", character, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bytea_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bytea_consistent"("internal", "bytea", smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_consistent"("internal", "bytea", smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_consistent"("internal", "bytea", smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_consistent"("internal", "bytea", smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bytea_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bytea_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bytea_same"("public"."gbtreekey_var", "public"."gbtreekey_var", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_same"("public"."gbtreekey_var", "public"."gbtreekey_var", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_same"("public"."gbtreekey_var", "public"."gbtreekey_var", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_same"("public"."gbtreekey_var", "public"."gbtreekey_var", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_bytea_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_bytea_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_cash_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_cash_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_cash_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_cash_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_cash_consistent"("internal", "money", smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_cash_consistent"("internal", "money", smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_cash_consistent"("internal", "money", smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_cash_consistent"("internal", "money", smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_cash_distance"("internal", "money", smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_cash_distance"("internal", "money", smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_cash_distance"("internal", "money", smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_cash_distance"("internal", "money", smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_cash_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_cash_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_cash_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_cash_fetch"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_cash_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_cash_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_cash_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_cash_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_cash_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_cash_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_cash_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_cash_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_cash_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_cash_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_cash_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_cash_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_cash_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_cash_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_cash_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_cash_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_date_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_date_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_date_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_date_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_date_consistent"("internal", "date", smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_date_consistent"("internal", "date", smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_date_consistent"("internal", "date", smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_date_consistent"("internal", "date", smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_date_distance"("internal", "date", smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_date_distance"("internal", "date", smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_date_distance"("internal", "date", smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_date_distance"("internal", "date", smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_date_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_date_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_date_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_date_fetch"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_date_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_date_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_date_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_date_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_date_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_date_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_date_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_date_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_date_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_date_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_date_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_date_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_date_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_date_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_date_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_date_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_decompress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_decompress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_decompress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_decompress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_enum_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_enum_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_enum_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_enum_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_enum_consistent"("internal", "anyenum", smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_enum_consistent"("internal", "anyenum", smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_enum_consistent"("internal", "anyenum", smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_enum_consistent"("internal", "anyenum", smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_enum_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_enum_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_enum_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_enum_fetch"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_enum_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_enum_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_enum_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_enum_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_enum_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_enum_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_enum_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_enum_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_enum_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_enum_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_enum_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_enum_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_enum_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_enum_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_enum_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_enum_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_float4_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_float4_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_float4_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_float4_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_float4_consistent"("internal", real, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_float4_consistent"("internal", real, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_float4_consistent"("internal", real, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_float4_consistent"("internal", real, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_float4_distance"("internal", real, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_float4_distance"("internal", real, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_float4_distance"("internal", real, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_float4_distance"("internal", real, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_float4_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_float4_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_float4_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_float4_fetch"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_float4_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_float4_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_float4_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_float4_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_float4_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_float4_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_float4_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_float4_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_float4_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_float4_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_float4_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_float4_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_float4_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_float4_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_float4_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_float4_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_float8_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_float8_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_float8_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_float8_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_float8_consistent"("internal", double precision, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_float8_consistent"("internal", double precision, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_float8_consistent"("internal", double precision, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_float8_consistent"("internal", double precision, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_float8_distance"("internal", double precision, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_float8_distance"("internal", double precision, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_float8_distance"("internal", double precision, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_float8_distance"("internal", double precision, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_float8_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_float8_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_float8_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_float8_fetch"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_float8_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_float8_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_float8_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_float8_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_float8_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_float8_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_float8_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_float8_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_float8_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_float8_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_float8_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_float8_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_float8_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_float8_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_float8_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_float8_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_inet_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_inet_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_inet_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_inet_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_inet_consistent"("internal", "inet", smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_inet_consistent"("internal", "inet", smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_inet_consistent"("internal", "inet", smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_inet_consistent"("internal", "inet", smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_inet_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_inet_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_inet_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_inet_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_inet_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_inet_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_inet_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_inet_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_inet_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_inet_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_inet_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_inet_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_inet_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_inet_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_inet_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_inet_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int2_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int2_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int2_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int2_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int2_consistent"("internal", smallint, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int2_consistent"("internal", smallint, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int2_consistent"("internal", smallint, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int2_consistent"("internal", smallint, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int2_distance"("internal", smallint, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int2_distance"("internal", smallint, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int2_distance"("internal", smallint, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int2_distance"("internal", smallint, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int2_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int2_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int2_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int2_fetch"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int2_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int2_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int2_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int2_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int2_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int2_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int2_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int2_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int2_same"("public"."gbtreekey4", "public"."gbtreekey4", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int2_same"("public"."gbtreekey4", "public"."gbtreekey4", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int2_same"("public"."gbtreekey4", "public"."gbtreekey4", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int2_same"("public"."gbtreekey4", "public"."gbtreekey4", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int2_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int2_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int2_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int2_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int4_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int4_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int4_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int4_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int4_consistent"("internal", integer, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int4_consistent"("internal", integer, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int4_consistent"("internal", integer, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int4_consistent"("internal", integer, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int4_distance"("internal", integer, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int4_distance"("internal", integer, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int4_distance"("internal", integer, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int4_distance"("internal", integer, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int4_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int4_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int4_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int4_fetch"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int4_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int4_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int4_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int4_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int4_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int4_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int4_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int4_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int4_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int4_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int4_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int4_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int4_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int4_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int4_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int4_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int8_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int8_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int8_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int8_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int8_consistent"("internal", bigint, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int8_consistent"("internal", bigint, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int8_consistent"("internal", bigint, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int8_consistent"("internal", bigint, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int8_distance"("internal", bigint, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int8_distance"("internal", bigint, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int8_distance"("internal", bigint, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int8_distance"("internal", bigint, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int8_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int8_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int8_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int8_fetch"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int8_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int8_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int8_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int8_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int8_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int8_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int8_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int8_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int8_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int8_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int8_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int8_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_int8_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_int8_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_int8_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_int8_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_intv_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_intv_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_intv_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_intv_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_intv_consistent"("internal", interval, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_intv_consistent"("internal", interval, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_intv_consistent"("internal", interval, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_intv_consistent"("internal", interval, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_intv_decompress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_intv_decompress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_intv_decompress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_intv_decompress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_intv_distance"("internal", interval, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_intv_distance"("internal", interval, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_intv_distance"("internal", interval, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_intv_distance"("internal", interval, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_intv_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_intv_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_intv_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_intv_fetch"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_intv_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_intv_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_intv_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_intv_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_intv_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_intv_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_intv_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_intv_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_intv_same"("public"."gbtreekey32", "public"."gbtreekey32", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_intv_same"("public"."gbtreekey32", "public"."gbtreekey32", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_intv_same"("public"."gbtreekey32", "public"."gbtreekey32", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_intv_same"("public"."gbtreekey32", "public"."gbtreekey32", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_intv_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_intv_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_intv_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_intv_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_macad8_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_macad8_consistent"("internal", "macaddr8", smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_consistent"("internal", "macaddr8", smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_consistent"("internal", "macaddr8", smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_consistent"("internal", "macaddr8", smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_macad8_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_fetch"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_macad8_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_macad8_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_macad8_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_macad8_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_macad8_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_macad_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_macad_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_macad_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_macad_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_macad_consistent"("internal", "macaddr", smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_macad_consistent"("internal", "macaddr", smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_macad_consistent"("internal", "macaddr", smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_macad_consistent"("internal", "macaddr", smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_macad_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_macad_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_macad_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_macad_fetch"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_macad_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_macad_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_macad_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_macad_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_macad_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_macad_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_macad_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_macad_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_macad_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_macad_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_macad_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_macad_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_macad_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_macad_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_macad_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_macad_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_numeric_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_numeric_consistent"("internal", numeric, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_consistent"("internal", numeric, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_consistent"("internal", numeric, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_consistent"("internal", numeric, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_numeric_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_numeric_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_numeric_same"("public"."gbtreekey_var", "public"."gbtreekey_var", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_same"("public"."gbtreekey_var", "public"."gbtreekey_var", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_same"("public"."gbtreekey_var", "public"."gbtreekey_var", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_same"("public"."gbtreekey_var", "public"."gbtreekey_var", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_numeric_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_numeric_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_oid_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_oid_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_oid_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_oid_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_oid_consistent"("internal", "oid", smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_oid_consistent"("internal", "oid", smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_oid_consistent"("internal", "oid", smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_oid_consistent"("internal", "oid", smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_oid_distance"("internal", "oid", smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_oid_distance"("internal", "oid", smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_oid_distance"("internal", "oid", smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_oid_distance"("internal", "oid", smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_oid_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_oid_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_oid_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_oid_fetch"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_oid_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_oid_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_oid_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_oid_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_oid_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_oid_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_oid_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_oid_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_oid_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_oid_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_oid_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_oid_same"("public"."gbtreekey8", "public"."gbtreekey8", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_oid_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_oid_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_oid_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_oid_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_text_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_text_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_text_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_text_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_text_consistent"("internal", "text", smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_text_consistent"("internal", "text", smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_text_consistent"("internal", "text", smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_text_consistent"("internal", "text", smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_text_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_text_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_text_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_text_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_text_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_text_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_text_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_text_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_text_same"("public"."gbtreekey_var", "public"."gbtreekey_var", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_text_same"("public"."gbtreekey_var", "public"."gbtreekey_var", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_text_same"("public"."gbtreekey_var", "public"."gbtreekey_var", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_text_same"("public"."gbtreekey_var", "public"."gbtreekey_var", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_text_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_text_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_text_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_text_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_time_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_time_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_time_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_time_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_time_consistent"("internal", time without time zone, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_time_consistent"("internal", time without time zone, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_time_consistent"("internal", time without time zone, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_time_consistent"("internal", time without time zone, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_time_distance"("internal", time without time zone, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_time_distance"("internal", time without time zone, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_time_distance"("internal", time without time zone, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_time_distance"("internal", time without time zone, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_time_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_time_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_time_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_time_fetch"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_time_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_time_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_time_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_time_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_time_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_time_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_time_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_time_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_time_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_time_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_time_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_time_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_time_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_time_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_time_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_time_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_timetz_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_timetz_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_timetz_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_timetz_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_timetz_consistent"("internal", time with time zone, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_timetz_consistent"("internal", time with time zone, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_timetz_consistent"("internal", time with time zone, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_timetz_consistent"("internal", time with time zone, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_ts_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_ts_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_ts_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_ts_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_ts_consistent"("internal", timestamp without time zone, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_ts_consistent"("internal", timestamp without time zone, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_ts_consistent"("internal", timestamp without time zone, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_ts_consistent"("internal", timestamp without time zone, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_ts_distance"("internal", timestamp without time zone, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_ts_distance"("internal", timestamp without time zone, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_ts_distance"("internal", timestamp without time zone, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_ts_distance"("internal", timestamp without time zone, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_ts_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_ts_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_ts_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_ts_fetch"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_ts_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_ts_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_ts_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_ts_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_ts_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_ts_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_ts_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_ts_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_ts_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_ts_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_ts_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_ts_same"("public"."gbtreekey16", "public"."gbtreekey16", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_ts_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_ts_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_ts_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_ts_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_tstz_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_tstz_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_tstz_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_tstz_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_tstz_consistent"("internal", timestamp with time zone, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_tstz_consistent"("internal", timestamp with time zone, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_tstz_consistent"("internal", timestamp with time zone, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_tstz_consistent"("internal", timestamp with time zone, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_tstz_distance"("internal", timestamp with time zone, smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_tstz_distance"("internal", timestamp with time zone, smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_tstz_distance"("internal", timestamp with time zone, smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_tstz_distance"("internal", timestamp with time zone, smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_uuid_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_uuid_consistent"("internal", "uuid", smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_consistent"("internal", "uuid", smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_consistent"("internal", "uuid", smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_consistent"("internal", "uuid", smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_uuid_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_fetch"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_uuid_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_uuid_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_uuid_same"("public"."gbtreekey32", "public"."gbtreekey32", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_same"("public"."gbtreekey32", "public"."gbtreekey32", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_same"("public"."gbtreekey32", "public"."gbtreekey32", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_same"("public"."gbtreekey32", "public"."gbtreekey32", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_uuid_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_uuid_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_var_decompress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_var_decompress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_var_decompress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_var_decompress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gbt_var_fetch"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gbt_var_fetch"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gbt_var_fetch"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gbt_var_fetch"("internal") TO "service_role"; + + + +REVOKE ALL ON FUNCTION "public"."get_my_email"() FROM PUBLIC; +GRANT ALL ON FUNCTION "public"."get_my_email"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."get_my_email"() TO "anon"; +GRANT ALL ON FUNCTION "public"."get_my_email"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."get_my_email"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gin_extract_query_trgm"("text", "internal", smallint, "internal", "internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gin_extract_query_trgm"("text", "internal", smallint, "internal", "internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gin_extract_query_trgm"("text", "internal", smallint, "internal", "internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gin_extract_query_trgm"("text", "internal", smallint, "internal", "internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gin_extract_value_trgm"("text", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gin_extract_value_trgm"("text", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gin_extract_value_trgm"("text", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gin_extract_value_trgm"("text", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gin_trgm_consistent"("internal", smallint, "text", integer, "internal", "internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gin_trgm_consistent"("internal", smallint, "text", integer, "internal", "internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gin_trgm_consistent"("internal", smallint, "text", integer, "internal", "internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gin_trgm_consistent"("internal", smallint, "text", integer, "internal", "internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gin_trgm_triconsistent"("internal", smallint, "text", integer, "internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gin_trgm_triconsistent"("internal", smallint, "text", integer, "internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gin_trgm_triconsistent"("internal", smallint, "text", integer, "internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gin_trgm_triconsistent"("internal", smallint, "text", integer, "internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gtrgm_compress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gtrgm_compress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gtrgm_compress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gtrgm_compress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gtrgm_consistent"("internal", "text", smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gtrgm_consistent"("internal", "text", smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gtrgm_consistent"("internal", "text", smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gtrgm_consistent"("internal", "text", smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gtrgm_decompress"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gtrgm_decompress"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gtrgm_decompress"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gtrgm_decompress"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gtrgm_distance"("internal", "text", smallint, "oid", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gtrgm_distance"("internal", "text", smallint, "oid", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gtrgm_distance"("internal", "text", smallint, "oid", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gtrgm_distance"("internal", "text", smallint, "oid", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gtrgm_options"("internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gtrgm_options"("internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gtrgm_options"("internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gtrgm_options"("internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gtrgm_penalty"("internal", "internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gtrgm_penalty"("internal", "internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gtrgm_penalty"("internal", "internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gtrgm_penalty"("internal", "internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gtrgm_picksplit"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gtrgm_picksplit"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gtrgm_picksplit"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gtrgm_picksplit"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gtrgm_same"("public"."gtrgm", "public"."gtrgm", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gtrgm_same"("public"."gtrgm", "public"."gtrgm", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gtrgm_same"("public"."gtrgm", "public"."gtrgm", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gtrgm_same"("public"."gtrgm", "public"."gtrgm", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."gtrgm_union"("internal", "internal") TO "postgres"; +GRANT ALL ON FUNCTION "public"."gtrgm_union"("internal", "internal") TO "anon"; +GRANT ALL ON FUNCTION "public"."gtrgm_union"("internal", "internal") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."gtrgm_union"("internal", "internal") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."guard_account_type_immutable"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."guard_account_type_immutable"() TO "anon"; +GRANT ALL ON FUNCTION "public"."guard_account_type_immutable"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."guard_account_type_immutable"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."guard_locked_commitment"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."guard_locked_commitment"() TO "anon"; +GRANT ALL ON FUNCTION "public"."guard_locked_commitment"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."guard_locked_commitment"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."guard_no_change_core_plan_key"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."guard_no_change_core_plan_key"() TO "anon"; +GRANT ALL ON FUNCTION "public"."guard_no_change_core_plan_key"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."guard_no_change_core_plan_key"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."guard_no_change_plan_target"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."guard_no_change_plan_target"() TO "anon"; +GRANT ALL ON FUNCTION "public"."guard_no_change_plan_target"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."guard_no_change_plan_target"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."guard_no_delete_core_plans"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."guard_no_delete_core_plans"() TO "anon"; +GRANT ALL ON FUNCTION "public"."guard_no_delete_core_plans"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."guard_no_delete_core_plans"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."guard_patient_cannot_own_tenant"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."guard_patient_cannot_own_tenant"() TO "anon"; +GRANT ALL ON FUNCTION "public"."guard_patient_cannot_own_tenant"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."guard_patient_cannot_own_tenant"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."guard_tenant_kind_immutable"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."guard_tenant_kind_immutable"() TO "anon"; +GRANT ALL ON FUNCTION "public"."guard_tenant_kind_immutable"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."guard_tenant_kind_immutable"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "anon"; +GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."handle_new_user_create_personal_tenant"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."handle_new_user_create_personal_tenant"() TO "anon"; +GRANT ALL ON FUNCTION "public"."handle_new_user_create_personal_tenant"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."handle_new_user_create_personal_tenant"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."has_feature"("p_owner_id" "uuid", "p_feature_key" "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."has_feature"("p_owner_id" "uuid", "p_feature_key" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."has_feature"("p_owner_id" "uuid", "p_feature_key" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."has_feature"("p_owner_id" "uuid", "p_feature_key" "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."int2_dist"(smallint, smallint) TO "postgres"; +GRANT ALL ON FUNCTION "public"."int2_dist"(smallint, smallint) TO "anon"; +GRANT ALL ON FUNCTION "public"."int2_dist"(smallint, smallint) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."int2_dist"(smallint, smallint) TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."int4_dist"(integer, integer) TO "postgres"; +GRANT ALL ON FUNCTION "public"."int4_dist"(integer, integer) TO "anon"; +GRANT ALL ON FUNCTION "public"."int4_dist"(integer, integer) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."int4_dist"(integer, integer) TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."int8_dist"(bigint, bigint) TO "postgres"; +GRANT ALL ON FUNCTION "public"."int8_dist"(bigint, bigint) TO "anon"; +GRANT ALL ON FUNCTION "public"."int8_dist"(bigint, bigint) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."int8_dist"(bigint, bigint) TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."interval_dist"(interval, interval) TO "postgres"; +GRANT ALL ON FUNCTION "public"."interval_dist"(interval, interval) TO "anon"; +GRANT ALL ON FUNCTION "public"."interval_dist"(interval, interval) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."interval_dist"(interval, interval) TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."is_clinic_tenant"("_tenant_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."is_clinic_tenant"("_tenant_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."is_clinic_tenant"("_tenant_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."is_clinic_tenant"("_tenant_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."is_saas_admin"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."is_saas_admin"() TO "anon"; +GRANT ALL ON FUNCTION "public"."is_saas_admin"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."is_saas_admin"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."is_tenant_admin"("p_tenant_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."is_tenant_admin"("p_tenant_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."is_tenant_admin"("p_tenant_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."is_tenant_admin"("p_tenant_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."is_tenant_member"("_tenant_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."is_tenant_member"("_tenant_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."is_tenant_member"("_tenant_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."is_tenant_member"("_tenant_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."is_therapist_tenant"("_tenant_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."is_therapist_tenant"("_tenant_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."is_therapist_tenant"("_tenant_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."is_therapist_tenant"("_tenant_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."jwt_email"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."jwt_email"() TO "anon"; +GRANT ALL ON FUNCTION "public"."jwt_email"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."jwt_email"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."my_tenants"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."my_tenants"() TO "anon"; +GRANT ALL ON FUNCTION "public"."my_tenants"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."my_tenants"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."oid_dist"("oid", "oid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."oid_dist"("oid", "oid") TO "anon"; +GRANT ALL ON FUNCTION "public"."oid_dist"("oid", "oid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."oid_dist"("oid", "oid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."patients_validate_member_consistency"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."patients_validate_member_consistency"() TO "anon"; +GRANT ALL ON FUNCTION "public"."patients_validate_member_consistency"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."patients_validate_member_consistency"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."patients_validate_responsible_member_tenant"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."patients_validate_responsible_member_tenant"() TO "anon"; +GRANT ALL ON FUNCTION "public"."patients_validate_responsible_member_tenant"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."patients_validate_responsible_member_tenant"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."prevent_promoting_to_system"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."prevent_promoting_to_system"() TO "anon"; +GRANT ALL ON FUNCTION "public"."prevent_promoting_to_system"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."prevent_promoting_to_system"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."prevent_saas_membership"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."prevent_saas_membership"() TO "anon"; +GRANT ALL ON FUNCTION "public"."prevent_saas_membership"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."prevent_saas_membership"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."prevent_system_group_changes"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."prevent_system_group_changes"() TO "anon"; +GRANT ALL ON FUNCTION "public"."prevent_system_group_changes"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."prevent_system_group_changes"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."provision_account_tenant"("p_user_id" "uuid", "p_kind" "text", "p_name" "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."provision_account_tenant"("p_user_id" "uuid", "p_kind" "text", "p_name" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."provision_account_tenant"("p_user_id" "uuid", "p_kind" "text", "p_name" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."provision_account_tenant"("p_user_id" "uuid", "p_kind" "text", "p_name" "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."reactivate_subscription"("p_subscription_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."reactivate_subscription"("p_subscription_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."reactivate_subscription"("p_subscription_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."reactivate_subscription"("p_subscription_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."rebuild_owner_entitlements"("p_owner_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."rebuild_owner_entitlements"("p_owner_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."rebuild_owner_entitlements"("p_owner_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."rebuild_owner_entitlements"("p_owner_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."regexp_match"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."regexp_match"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."regexp_match"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."regexp_match"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."regexp_match"("public"."citext", "public"."citext", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."regexp_match"("public"."citext", "public"."citext", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."regexp_match"("public"."citext", "public"."citext", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."regexp_match"("public"."citext", "public"."citext", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."regexp_matches"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."regexp_matches"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."regexp_matches"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."regexp_matches"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."regexp_matches"("public"."citext", "public"."citext", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."regexp_matches"("public"."citext", "public"."citext", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."regexp_matches"("public"."citext", "public"."citext", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."regexp_matches"("public"."citext", "public"."citext", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."regexp_replace"("public"."citext", "public"."citext", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."regexp_replace"("public"."citext", "public"."citext", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."regexp_replace"("public"."citext", "public"."citext", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."regexp_replace"("public"."citext", "public"."citext", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."regexp_replace"("public"."citext", "public"."citext", "text", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."regexp_replace"("public"."citext", "public"."citext", "text", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."regexp_replace"("public"."citext", "public"."citext", "text", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."regexp_replace"("public"."citext", "public"."citext", "text", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."regexp_split_to_array"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."regexp_split_to_array"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."regexp_split_to_array"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."regexp_split_to_array"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."regexp_split_to_array"("public"."citext", "public"."citext", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."regexp_split_to_array"("public"."citext", "public"."citext", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."regexp_split_to_array"("public"."citext", "public"."citext", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."regexp_split_to_array"("public"."citext", "public"."citext", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."regexp_split_to_table"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."regexp_split_to_table"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."regexp_split_to_table"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."regexp_split_to_table"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."regexp_split_to_table"("public"."citext", "public"."citext", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."regexp_split_to_table"("public"."citext", "public"."citext", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."regexp_split_to_table"("public"."citext", "public"."citext", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."regexp_split_to_table"("public"."citext", "public"."citext", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."replace"("public"."citext", "public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."replace"("public"."citext", "public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."replace"("public"."citext", "public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."replace"("public"."citext", "public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."rotate_patient_invite_token"("p_new_token" "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."rotate_patient_invite_token"("p_new_token" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."rotate_patient_invite_token"("p_new_token" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."rotate_patient_invite_token"("p_new_token" "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."seed_determined_commitments"("p_tenant_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."seed_determined_commitments"("p_tenant_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."seed_determined_commitments"("p_tenant_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."seed_determined_commitments"("p_tenant_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."set_limit"(real) TO "postgres"; +GRANT ALL ON FUNCTION "public"."set_limit"(real) TO "anon"; +GRANT ALL ON FUNCTION "public"."set_limit"(real) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."set_limit"(real) TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."set_owner_id"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."set_owner_id"() TO "anon"; +GRANT ALL ON FUNCTION "public"."set_owner_id"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."set_owner_id"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."set_tenant_feature_exception"("p_tenant_id" "uuid", "p_feature_key" "text", "p_enabled" boolean, "p_reason" "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."set_tenant_feature_exception"("p_tenant_id" "uuid", "p_feature_key" "text", "p_enabled" boolean, "p_reason" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."set_tenant_feature_exception"("p_tenant_id" "uuid", "p_feature_key" "text", "p_enabled" boolean, "p_reason" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."set_tenant_feature_exception"("p_tenant_id" "uuid", "p_feature_key" "text", "p_enabled" boolean, "p_reason" "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."set_updated_at"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."set_updated_at"() TO "anon"; +GRANT ALL ON FUNCTION "public"."set_updated_at"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."set_updated_at"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."show_limit"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."show_limit"() TO "anon"; +GRANT ALL ON FUNCTION "public"."show_limit"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."show_limit"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."show_trgm"("text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."show_trgm"("text") TO "anon"; +GRANT ALL ON FUNCTION "public"."show_trgm"("text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."show_trgm"("text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."similarity"("text", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."similarity"("text", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."similarity"("text", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."similarity"("text", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."similarity_dist"("text", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."similarity_dist"("text", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."similarity_dist"("text", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."similarity_dist"("text", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."similarity_op"("text", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."similarity_op"("text", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."similarity_op"("text", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."similarity_op"("text", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."split_part"("public"."citext", "public"."citext", integer) TO "postgres"; +GRANT ALL ON FUNCTION "public"."split_part"("public"."citext", "public"."citext", integer) TO "anon"; +GRANT ALL ON FUNCTION "public"."split_part"("public"."citext", "public"."citext", integer) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."split_part"("public"."citext", "public"."citext", integer) TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."strict_word_similarity"("text", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."strict_word_similarity"("text", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."strict_word_similarity"("text", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."strict_word_similarity"("text", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."strict_word_similarity_commutator_op"("text", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."strict_word_similarity_commutator_op"("text", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."strict_word_similarity_commutator_op"("text", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."strict_word_similarity_commutator_op"("text", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."strict_word_similarity_dist_commutator_op"("text", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."strict_word_similarity_dist_commutator_op"("text", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."strict_word_similarity_dist_commutator_op"("text", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."strict_word_similarity_dist_commutator_op"("text", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."strict_word_similarity_dist_op"("text", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."strict_word_similarity_dist_op"("text", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."strict_word_similarity_dist_op"("text", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."strict_word_similarity_dist_op"("text", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."strict_word_similarity_op"("text", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."strict_word_similarity_op"("text", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."strict_word_similarity_op"("text", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."strict_word_similarity_op"("text", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."strpos"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."strpos"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."strpos"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."strpos"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."subscription_intents_view_insert"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."subscription_intents_view_insert"() TO "anon"; +GRANT ALL ON FUNCTION "public"."subscription_intents_view_insert"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."subscription_intents_view_insert"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."subscriptions_validate_scope"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."subscriptions_validate_scope"() TO "anon"; +GRANT ALL ON FUNCTION "public"."subscriptions_validate_scope"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."subscriptions_validate_scope"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."sync_busy_mirror_agenda_eventos"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."sync_busy_mirror_agenda_eventos"() TO "anon"; +GRANT ALL ON FUNCTION "public"."sync_busy_mirror_agenda_eventos"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."sync_busy_mirror_agenda_eventos"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."tenant_accept_invite"("p_token" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."tenant_accept_invite"("p_token" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."tenant_accept_invite"("p_token" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."tenant_accept_invite"("p_token" "uuid") TO "service_role"; + + + +GRANT ALL ON TABLE "public"."tenant_members" TO "postgres"; +GRANT SELECT,REFERENCES,TRIGGER,TRUNCATE,MAINTAIN ON TABLE "public"."tenant_members" TO "authenticated"; +GRANT ALL ON TABLE "public"."tenant_members" TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."tenant_add_member_by_email"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."tenant_add_member_by_email"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."tenant_add_member_by_email"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."tenant_add_member_by_email"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."tenant_feature_allowed"("p_tenant_id" "uuid", "p_feature_key" "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."tenant_feature_allowed"("p_tenant_id" "uuid", "p_feature_key" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."tenant_feature_allowed"("p_tenant_id" "uuid", "p_feature_key" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."tenant_feature_allowed"("p_tenant_id" "uuid", "p_feature_key" "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."tenant_feature_enabled"("p_tenant_id" "uuid", "p_feature_key" "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."tenant_feature_enabled"("p_tenant_id" "uuid", "p_feature_key" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."tenant_feature_enabled"("p_tenant_id" "uuid", "p_feature_key" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."tenant_feature_enabled"("p_tenant_id" "uuid", "p_feature_key" "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."tenant_features_guard_with_plan"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."tenant_features_guard_with_plan"() TO "anon"; +GRANT ALL ON FUNCTION "public"."tenant_features_guard_with_plan"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."tenant_features_guard_with_plan"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."tenant_has_feature"("_tenant_id" "uuid", "_feature" "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."tenant_has_feature"("_tenant_id" "uuid", "_feature" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."tenant_has_feature"("_tenant_id" "uuid", "_feature" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."tenant_has_feature"("_tenant_id" "uuid", "_feature" "text") TO "service_role"; + + + +REVOKE ALL ON FUNCTION "public"."tenant_invite_member_by_email"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") FROM PUBLIC; +GRANT ALL ON FUNCTION "public"."tenant_invite_member_by_email"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."tenant_invite_member_by_email"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."tenant_invite_member_by_email"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."tenant_invite_member_by_email"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."tenant_reactivate_member"("p_tenant_id" "uuid", "p_member_user_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."tenant_reactivate_member"("p_tenant_id" "uuid", "p_member_user_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."tenant_reactivate_member"("p_tenant_id" "uuid", "p_member_user_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."tenant_reactivate_member"("p_tenant_id" "uuid", "p_member_user_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."tenant_remove_member"("p_tenant_id" "uuid", "p_member_user_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."tenant_remove_member"("p_tenant_id" "uuid", "p_member_user_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."tenant_remove_member"("p_tenant_id" "uuid", "p_member_user_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."tenant_remove_member"("p_tenant_id" "uuid", "p_member_user_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."tenant_remove_member_soft"("p_tenant_id" "uuid", "p_member_user_id" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."tenant_remove_member_soft"("p_tenant_id" "uuid", "p_member_user_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."tenant_remove_member_soft"("p_tenant_id" "uuid", "p_member_user_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."tenant_remove_member_soft"("p_tenant_id" "uuid", "p_member_user_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."tenant_revoke_invite"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."tenant_revoke_invite"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."tenant_revoke_invite"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."tenant_revoke_invite"("p_tenant_id" "uuid", "p_email" "text", "p_role" "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."tenant_set_member_status"("p_tenant_id" "uuid", "p_member_user_id" "uuid", "p_new_status" "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."tenant_set_member_status"("p_tenant_id" "uuid", "p_member_user_id" "uuid", "p_new_status" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."tenant_set_member_status"("p_tenant_id" "uuid", "p_member_user_id" "uuid", "p_new_status" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."tenant_set_member_status"("p_tenant_id" "uuid", "p_member_user_id" "uuid", "p_new_status" "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."tenant_update_member_role"("p_tenant_id" "uuid", "p_member_user_id" "uuid", "p_new_role" "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."tenant_update_member_role"("p_tenant_id" "uuid", "p_member_user_id" "uuid", "p_new_role" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."tenant_update_member_role"("p_tenant_id" "uuid", "p_member_user_id" "uuid", "p_new_role" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."tenant_update_member_role"("p_tenant_id" "uuid", "p_member_user_id" "uuid", "p_new_role" "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."texticlike"("public"."citext", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."texticlike"("public"."citext", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."texticlike"("public"."citext", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."texticlike"("public"."citext", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."texticlike"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."texticlike"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."texticlike"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."texticlike"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."texticnlike"("public"."citext", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."texticnlike"("public"."citext", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."texticnlike"("public"."citext", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."texticnlike"("public"."citext", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."texticnlike"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."texticnlike"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."texticnlike"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."texticnlike"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."texticregexeq"("public"."citext", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."texticregexeq"("public"."citext", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."texticregexeq"("public"."citext", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."texticregexeq"("public"."citext", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."texticregexeq"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."texticregexeq"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."texticregexeq"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."texticregexeq"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."texticregexne"("public"."citext", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."texticregexne"("public"."citext", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."texticregexne"("public"."citext", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."texticregexne"("public"."citext", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."texticregexne"("public"."citext", "public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."texticregexne"("public"."citext", "public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."texticregexne"("public"."citext", "public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."texticregexne"("public"."citext", "public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."time_dist"(time without time zone, time without time zone) TO "postgres"; +GRANT ALL ON FUNCTION "public"."time_dist"(time without time zone, time without time zone) TO "anon"; +GRANT ALL ON FUNCTION "public"."time_dist"(time without time zone, time without time zone) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."time_dist"(time without time zone, time without time zone) TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."toggle_plan"("owner" "uuid") TO "postgres"; +GRANT ALL ON FUNCTION "public"."toggle_plan"("owner" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."toggle_plan"("owner" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."toggle_plan"("owner" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."transition_subscription"("p_subscription_id" "uuid", "p_to_status" "text", "p_reason" "text", "p_metadata" "jsonb") TO "postgres"; +GRANT ALL ON FUNCTION "public"."transition_subscription"("p_subscription_id" "uuid", "p_to_status" "text", "p_reason" "text", "p_metadata" "jsonb") TO "anon"; +GRANT ALL ON FUNCTION "public"."transition_subscription"("p_subscription_id" "uuid", "p_to_status" "text", "p_reason" "text", "p_metadata" "jsonb") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."transition_subscription"("p_subscription_id" "uuid", "p_to_status" "text", "p_reason" "text", "p_metadata" "jsonb") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."translate"("public"."citext", "public"."citext", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."translate"("public"."citext", "public"."citext", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."translate"("public"."citext", "public"."citext", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."translate"("public"."citext", "public"."citext", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."ts_dist"(timestamp without time zone, timestamp without time zone) TO "postgres"; +GRANT ALL ON FUNCTION "public"."ts_dist"(timestamp without time zone, timestamp without time zone) TO "anon"; +GRANT ALL ON FUNCTION "public"."ts_dist"(timestamp without time zone, timestamp without time zone) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."ts_dist"(timestamp without time zone, timestamp without time zone) TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."tstz_dist"(timestamp with time zone, timestamp with time zone) TO "postgres"; +GRANT ALL ON FUNCTION "public"."tstz_dist"(timestamp with time zone, timestamp with time zone) TO "anon"; +GRANT ALL ON FUNCTION "public"."tstz_dist"(timestamp with time zone, timestamp with time zone) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."tstz_dist"(timestamp with time zone, timestamp with time zone) TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."user_has_feature"("_user_id" "uuid", "_feature" "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."user_has_feature"("_user_id" "uuid", "_feature" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."user_has_feature"("_user_id" "uuid", "_feature" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."user_has_feature"("_user_id" "uuid", "_feature" "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."whoami"() TO "postgres"; +GRANT ALL ON FUNCTION "public"."whoami"() TO "anon"; +GRANT ALL ON FUNCTION "public"."whoami"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."whoami"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."word_similarity"("text", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."word_similarity"("text", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."word_similarity"("text", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."word_similarity"("text", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."word_similarity_commutator_op"("text", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."word_similarity_commutator_op"("text", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."word_similarity_commutator_op"("text", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."word_similarity_commutator_op"("text", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."word_similarity_dist_commutator_op"("text", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."word_similarity_dist_commutator_op"("text", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."word_similarity_dist_commutator_op"("text", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."word_similarity_dist_commutator_op"("text", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."word_similarity_dist_op"("text", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."word_similarity_dist_op"("text", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."word_similarity_dist_op"("text", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."word_similarity_dist_op"("text", "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."word_similarity_op"("text", "text") TO "postgres"; +GRANT ALL ON FUNCTION "public"."word_similarity_op"("text", "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."word_similarity_op"("text", "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."word_similarity_op"("text", "text") TO "service_role"; + + + + + + + + + + + + +GRANT ALL ON FUNCTION "public"."max"("public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."max"("public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."max"("public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."max"("public"."citext") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."min"("public"."citext") TO "postgres"; +GRANT ALL ON FUNCTION "public"."min"("public"."citext") TO "anon"; +GRANT ALL ON FUNCTION "public"."min"("public"."citext") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."min"("public"."citext") TO "service_role"; + + + + + + + + + +GRANT ALL ON TABLE "public"."agenda_configuracoes" TO "postgres"; +GRANT ALL ON TABLE "public"."agenda_configuracoes" TO "anon"; +GRANT ALL ON TABLE "public"."agenda_configuracoes" TO "authenticated"; +GRANT ALL ON TABLE "public"."agenda_configuracoes" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."agenda_eventos" TO "postgres"; +GRANT ALL ON TABLE "public"."agenda_eventos" TO "anon"; +GRANT ALL ON TABLE "public"."agenda_eventos" TO "authenticated"; +GRANT ALL ON TABLE "public"."agenda_eventos" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."agenda_excecoes" TO "postgres"; +GRANT ALL ON TABLE "public"."agenda_excecoes" TO "anon"; +GRANT ALL ON TABLE "public"."agenda_excecoes" TO "authenticated"; +GRANT ALL ON TABLE "public"."agenda_excecoes" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."agenda_online_slots" TO "postgres"; +GRANT ALL ON TABLE "public"."agenda_online_slots" TO "anon"; +GRANT ALL ON TABLE "public"."agenda_online_slots" TO "authenticated"; +GRANT ALL ON TABLE "public"."agenda_online_slots" TO "service_role"; + + + +GRANT ALL ON SEQUENCE "public"."agenda_online_slots_id_seq" TO "postgres"; +GRANT ALL ON SEQUENCE "public"."agenda_online_slots_id_seq" TO "anon"; +GRANT ALL ON SEQUENCE "public"."agenda_online_slots_id_seq" TO "authenticated"; +GRANT ALL ON SEQUENCE "public"."agenda_online_slots_id_seq" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."agenda_regras_semanais" TO "postgres"; +GRANT ALL ON TABLE "public"."agenda_regras_semanais" TO "anon"; +GRANT ALL ON TABLE "public"."agenda_regras_semanais" TO "authenticated"; +GRANT ALL ON TABLE "public"."agenda_regras_semanais" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."agenda_slots_bloqueados_semanais" TO "postgres"; +GRANT ALL ON TABLE "public"."agenda_slots_bloqueados_semanais" TO "anon"; +GRANT ALL ON TABLE "public"."agenda_slots_bloqueados_semanais" TO "authenticated"; +GRANT ALL ON TABLE "public"."agenda_slots_bloqueados_semanais" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."agenda_slots_regras" TO "postgres"; +GRANT ALL ON TABLE "public"."agenda_slots_regras" TO "anon"; +GRANT ALL ON TABLE "public"."agenda_slots_regras" TO "authenticated"; +GRANT ALL ON TABLE "public"."agenda_slots_regras" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."commitment_time_logs" TO "postgres"; +GRANT ALL ON TABLE "public"."commitment_time_logs" TO "anon"; +GRANT ALL ON TABLE "public"."commitment_time_logs" TO "authenticated"; +GRANT ALL ON TABLE "public"."commitment_time_logs" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."current_tenant_id" TO "postgres"; +GRANT ALL ON TABLE "public"."current_tenant_id" TO "anon"; +GRANT ALL ON TABLE "public"."current_tenant_id" TO "authenticated"; +GRANT ALL ON TABLE "public"."current_tenant_id" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."determined_commitment_fields" TO "postgres"; +GRANT ALL ON TABLE "public"."determined_commitment_fields" TO "anon"; +GRANT ALL ON TABLE "public"."determined_commitment_fields" TO "authenticated"; +GRANT ALL ON TABLE "public"."determined_commitment_fields" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."determined_commitments" TO "postgres"; +GRANT ALL ON TABLE "public"."determined_commitments" TO "anon"; +GRANT ALL ON TABLE "public"."determined_commitments" TO "authenticated"; +GRANT ALL ON TABLE "public"."determined_commitments" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."dev_user_credentials" TO "postgres"; +GRANT ALL ON TABLE "public"."dev_user_credentials" TO "anon"; +GRANT ALL ON TABLE "public"."dev_user_credentials" TO "authenticated"; +GRANT ALL ON TABLE "public"."dev_user_credentials" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."entitlements_invalidation" TO "postgres"; +GRANT ALL ON TABLE "public"."entitlements_invalidation" TO "anon"; +GRANT ALL ON TABLE "public"."entitlements_invalidation" TO "authenticated"; +GRANT ALL ON TABLE "public"."entitlements_invalidation" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."features" TO "postgres"; +GRANT ALL ON TABLE "public"."features" TO "anon"; +GRANT ALL ON TABLE "public"."features" TO "authenticated"; +GRANT ALL ON TABLE "public"."features" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."module_features" TO "postgres"; +GRANT ALL ON TABLE "public"."module_features" TO "anon"; +GRANT ALL ON TABLE "public"."module_features" TO "authenticated"; +GRANT ALL ON TABLE "public"."module_features" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."modules" TO "postgres"; +GRANT ALL ON TABLE "public"."modules" TO "anon"; +GRANT ALL ON TABLE "public"."modules" TO "authenticated"; +GRANT ALL ON TABLE "public"."modules" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."plan_features" TO "postgres"; +GRANT ALL ON TABLE "public"."plan_features" TO "anon"; +GRANT ALL ON TABLE "public"."plan_features" TO "authenticated"; +GRANT ALL ON TABLE "public"."plan_features" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."tenant_modules" TO "postgres"; +GRANT ALL ON TABLE "public"."tenant_modules" TO "anon"; +GRANT ALL ON TABLE "public"."tenant_modules" TO "authenticated"; +GRANT ALL ON TABLE "public"."tenant_modules" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."owner_feature_entitlements" TO "postgres"; +GRANT ALL ON TABLE "public"."owner_feature_entitlements" TO "anon"; +GRANT ALL ON TABLE "public"."owner_feature_entitlements" TO "authenticated"; +GRANT ALL ON TABLE "public"."owner_feature_entitlements" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."owner_users" TO "postgres"; +GRANT ALL ON TABLE "public"."owner_users" TO "anon"; +GRANT ALL ON TABLE "public"."owner_users" TO "authenticated"; +GRANT ALL ON TABLE "public"."owner_users" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."patient_group_patient" TO "postgres"; +GRANT ALL ON TABLE "public"."patient_group_patient" TO "anon"; +GRANT ALL ON TABLE "public"."patient_group_patient" TO "authenticated"; +GRANT ALL ON TABLE "public"."patient_group_patient" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."patient_groups" TO "postgres"; +GRANT ALL ON TABLE "public"."patient_groups" TO "anon"; +GRANT ALL ON TABLE "public"."patient_groups" TO "authenticated"; +GRANT ALL ON TABLE "public"."patient_groups" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."patient_intake_requests" TO "postgres"; +GRANT ALL ON TABLE "public"."patient_intake_requests" TO "authenticated"; +GRANT ALL ON TABLE "public"."patient_intake_requests" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."patient_invites" TO "postgres"; +GRANT ALL ON TABLE "public"."patient_invites" TO "authenticated"; +GRANT ALL ON TABLE "public"."patient_invites" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."patient_patient_tag" TO "postgres"; +GRANT ALL ON TABLE "public"."patient_patient_tag" TO "anon"; +GRANT ALL ON TABLE "public"."patient_patient_tag" TO "authenticated"; +GRANT ALL ON TABLE "public"."patient_patient_tag" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."patient_tags" TO "postgres"; +GRANT ALL ON TABLE "public"."patient_tags" TO "anon"; +GRANT ALL ON TABLE "public"."patient_tags" TO "authenticated"; +GRANT ALL ON TABLE "public"."patient_tags" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."patients" TO "postgres"; +GRANT ALL ON TABLE "public"."patients" TO "anon"; +GRANT ALL ON TABLE "public"."patients" TO "authenticated"; +GRANT ALL ON TABLE "public"."patients" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."plan_prices" TO "postgres"; +GRANT ALL ON TABLE "public"."plan_prices" TO "anon"; +GRANT ALL ON TABLE "public"."plan_prices" TO "authenticated"; +GRANT ALL ON TABLE "public"."plan_prices" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."plan_public" TO "postgres"; +GRANT ALL ON TABLE "public"."plan_public" TO "anon"; +GRANT ALL ON TABLE "public"."plan_public" TO "authenticated"; +GRANT ALL ON TABLE "public"."plan_public" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."plan_public_bullets" TO "postgres"; +GRANT ALL ON TABLE "public"."plan_public_bullets" TO "anon"; +GRANT ALL ON TABLE "public"."plan_public_bullets" TO "authenticated"; +GRANT ALL ON TABLE "public"."plan_public_bullets" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."plans" TO "postgres"; +GRANT ALL ON TABLE "public"."plans" TO "anon"; +GRANT ALL ON TABLE "public"."plans" TO "authenticated"; +GRANT ALL ON TABLE "public"."plans" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."profiles" TO "postgres"; +GRANT ALL ON TABLE "public"."profiles" TO "anon"; +GRANT ALL ON TABLE "public"."profiles" TO "authenticated"; +GRANT ALL ON TABLE "public"."profiles" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."saas_admins" TO "postgres"; +GRANT ALL ON TABLE "public"."saas_admins" TO "anon"; +GRANT ALL ON TABLE "public"."saas_admins" TO "authenticated"; +GRANT ALL ON TABLE "public"."saas_admins" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."subscription_events" TO "postgres"; +GRANT ALL ON TABLE "public"."subscription_events" TO "anon"; +GRANT ALL ON TABLE "public"."subscription_events" TO "authenticated"; +GRANT ALL ON TABLE "public"."subscription_events" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."subscription_intents_personal" TO "postgres"; +GRANT ALL ON TABLE "public"."subscription_intents_personal" TO "anon"; +GRANT ALL ON TABLE "public"."subscription_intents_personal" TO "authenticated"; +GRANT ALL ON TABLE "public"."subscription_intents_personal" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."subscription_intents_tenant" TO "postgres"; +GRANT ALL ON TABLE "public"."subscription_intents_tenant" TO "anon"; +GRANT ALL ON TABLE "public"."subscription_intents_tenant" TO "authenticated"; +GRANT ALL ON TABLE "public"."subscription_intents_tenant" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."subscription_intents" TO "postgres"; +GRANT ALL ON TABLE "public"."subscription_intents" TO "anon"; +GRANT ALL ON TABLE "public"."subscription_intents" TO "authenticated"; +GRANT ALL ON TABLE "public"."subscription_intents" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."subscription_intents_legacy" TO "postgres"; +GRANT ALL ON TABLE "public"."subscription_intents_legacy" TO "anon"; +GRANT ALL ON TABLE "public"."subscription_intents_legacy" TO "authenticated"; +GRANT ALL ON TABLE "public"."subscription_intents_legacy" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."tenant_feature_exceptions_log" TO "postgres"; +GRANT ALL ON TABLE "public"."tenant_feature_exceptions_log" TO "anon"; +GRANT ALL ON TABLE "public"."tenant_feature_exceptions_log" TO "authenticated"; +GRANT ALL ON TABLE "public"."tenant_feature_exceptions_log" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."tenant_features" TO "postgres"; +GRANT ALL ON TABLE "public"."tenant_features" TO "anon"; +GRANT ALL ON TABLE "public"."tenant_features" TO "authenticated"; +GRANT ALL ON TABLE "public"."tenant_features" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."tenant_invites" TO "postgres"; +GRANT ALL ON TABLE "public"."tenant_invites" TO "authenticated"; +GRANT ALL ON TABLE "public"."tenant_invites" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."tenants" TO "postgres"; +GRANT ALL ON TABLE "public"."tenants" TO "anon"; +GRANT ALL ON TABLE "public"."tenants" TO "authenticated"; +GRANT ALL ON TABLE "public"."tenants" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."user_settings" TO "postgres"; +GRANT ALL ON TABLE "public"."user_settings" TO "anon"; +GRANT ALL ON TABLE "public"."user_settings" TO "authenticated"; +GRANT ALL ON TABLE "public"."user_settings" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_auth_users_public" TO "postgres"; +GRANT ALL ON TABLE "public"."v_auth_users_public" TO "anon"; +GRANT ALL ON TABLE "public"."v_auth_users_public" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_auth_users_public" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_commitment_totals" TO "postgres"; +GRANT ALL ON TABLE "public"."v_commitment_totals" TO "anon"; +GRANT ALL ON TABLE "public"."v_commitment_totals" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_commitment_totals" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_patient_groups_with_counts" TO "postgres"; +GRANT ALL ON TABLE "public"."v_patient_groups_with_counts" TO "anon"; +GRANT ALL ON TABLE "public"."v_patient_groups_with_counts" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_patient_groups_with_counts" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_plan_active_prices" TO "postgres"; +GRANT ALL ON TABLE "public"."v_plan_active_prices" TO "anon"; +GRANT ALL ON TABLE "public"."v_plan_active_prices" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_plan_active_prices" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_public_pricing" TO "postgres"; +GRANT ALL ON TABLE "public"."v_public_pricing" TO "anon"; +GRANT ALL ON TABLE "public"."v_public_pricing" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_public_pricing" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_subscription_feature_mismatch" TO "postgres"; +GRANT ALL ON TABLE "public"."v_subscription_feature_mismatch" TO "anon"; +GRANT ALL ON TABLE "public"."v_subscription_feature_mismatch" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_subscription_feature_mismatch" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_subscription_health" TO "postgres"; +GRANT ALL ON TABLE "public"."v_subscription_health" TO "anon"; +GRANT ALL ON TABLE "public"."v_subscription_health" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_subscription_health" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_subscription_health_v2" TO "postgres"; +GRANT ALL ON TABLE "public"."v_subscription_health_v2" TO "anon"; +GRANT ALL ON TABLE "public"."v_subscription_health_v2" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_subscription_health_v2" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_tag_patient_counts" TO "postgres"; +GRANT ALL ON TABLE "public"."v_tag_patient_counts" TO "anon"; +GRANT ALL ON TABLE "public"."v_tag_patient_counts" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_tag_patient_counts" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_tenant_active_subscription" TO "postgres"; +GRANT ALL ON TABLE "public"."v_tenant_active_subscription" TO "anon"; +GRANT ALL ON TABLE "public"."v_tenant_active_subscription" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_tenant_active_subscription" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_tenant_entitlements" TO "postgres"; +GRANT ALL ON TABLE "public"."v_tenant_entitlements" TO "anon"; +GRANT ALL ON TABLE "public"."v_tenant_entitlements" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_tenant_entitlements" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_tenant_entitlements_full" TO "postgres"; +GRANT ALL ON TABLE "public"."v_tenant_entitlements_full" TO "anon"; +GRANT ALL ON TABLE "public"."v_tenant_entitlements_full" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_tenant_entitlements_full" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_tenant_entitlements_json" TO "postgres"; +GRANT ALL ON TABLE "public"."v_tenant_entitlements_json" TO "anon"; +GRANT ALL ON TABLE "public"."v_tenant_entitlements_json" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_tenant_entitlements_json" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_tenant_feature_exceptions" TO "postgres"; +GRANT ALL ON TABLE "public"."v_tenant_feature_exceptions" TO "anon"; +GRANT ALL ON TABLE "public"."v_tenant_feature_exceptions" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_tenant_feature_exceptions" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_tenant_feature_mismatch" TO "postgres"; +GRANT ALL ON TABLE "public"."v_tenant_feature_mismatch" TO "anon"; +GRANT ALL ON TABLE "public"."v_tenant_feature_mismatch" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_tenant_feature_mismatch" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_tenant_members_with_profiles" TO "postgres"; +GRANT ALL ON TABLE "public"."v_tenant_members_with_profiles" TO "anon"; +GRANT ALL ON TABLE "public"."v_tenant_members_with_profiles" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_tenant_members_with_profiles" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_tenant_people" TO "postgres"; +GRANT ALL ON TABLE "public"."v_tenant_people" TO "anon"; +GRANT ALL ON TABLE "public"."v_tenant_people" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_tenant_people" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_tenant_staff" TO "postgres"; +GRANT ALL ON TABLE "public"."v_tenant_staff" TO "anon"; +GRANT ALL ON TABLE "public"."v_tenant_staff" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_tenant_staff" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_user_active_subscription" TO "postgres"; +GRANT ALL ON TABLE "public"."v_user_active_subscription" TO "anon"; +GRANT ALL ON TABLE "public"."v_user_active_subscription" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_user_active_subscription" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."v_user_entitlements" TO "postgres"; +GRANT ALL ON TABLE "public"."v_user_entitlements" TO "anon"; +GRANT ALL ON TABLE "public"."v_user_entitlements" TO "authenticated"; +GRANT ALL ON TABLE "public"."v_user_entitlements" TO "service_role"; + + +-- ============================================================= +-- MIGRATIONS APLICADAS MANUALMENTE (post-dump) +-- ============================================================= + +-- [2025-03] Coluna jornada_igual_todos em agenda_configuracoes +-- Já incluída na definição da tabela acima. +-- Para aplicar em ambiente existente: +-- ALTER TABLE public.agenda_configuracoes +-- ADD COLUMN IF NOT EXISTS jornada_igual_todos boolean DEFAULT true; + +-- [2025-03] RLS: terapeuta acessa seus próprios grupos de pacientes +-- Já incluída na seção de policies acima (patient_groups_owner_all). +-- Para aplicar em ambiente existente: +-- CREATE POLICY "patient_groups_owner_all" ON public.patient_groups +-- FOR ALL TO authenticated +-- USING (owner_id = auth.uid()) +-- WITH CHECK (owner_id = auth.uid()); + +-- [2025-03] RLS: terapeuta gerencia pivot grupo<->paciente dos seus pacientes +-- Já incluída na seção de policies acima (patient_group_patient_owner_all). +-- Para aplicar em ambiente existente: +-- CREATE POLICY "patient_group_patient_owner_all" ON public.patient_group_patient +-- FOR ALL TO authenticated +-- USING (EXISTS (SELECT 1 FROM public.patients p WHERE p.id = patient_id AND p.owner_id = auth.uid())) +-- WITH CHECK (EXISTS (SELECT 1 FROM public.patients p WHERE p.id = patient_id AND p.owner_id = auth.uid())); + + +-- ============================================================= +-- SEEDER: Grupos padrão do sistema por owner/tenant +-- ============================================================= +-- Grupos criados como is_system=true por owner+tenant. +-- Devem ser semeados no onboarding de cada novo usuário. +-- Execute este bloco para popular usuários já existentes. +-- ============================================================= + +CREATE OR REPLACE FUNCTION public.seed_system_groups_for_member( + p_owner_id uuid, + p_tenant_id uuid +) +RETURNS void +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +DECLARE + v_groups text[][] := ARRAY[ + ARRAY['Ativo', '#22c55e'], + ARRAY['Em Avaliação', '#3b82f6'], + ARRAY['Lista de Espera', '#f59e0b'], + ARRAY['Alta', '#8b5cf6'], + ARRAY['Inativo', '#6b7280'] + ]; + v_entry text[]; +BEGIN + FOREACH v_entry SLICE 1 IN ARRAY v_groups + LOOP + INSERT INTO public.patient_groups ( + owner_id, + tenant_id, + nome, + cor, + is_system, + is_active + ) + VALUES ( + p_owner_id, + p_tenant_id, + v_entry[1], + v_entry[2], + true, + true + ) + ON CONFLICT (owner_id, nome) DO NOTHING; + END LOOP; +END; +$$; + +-- Semeia para todos os membros ativos que ainda não têm grupos do sistema +DO $$ +DECLARE + r record; +BEGIN + FOR r IN + SELECT DISTINCT tm.user_id, tm.tenant_id + FROM public.tenant_members tm + WHERE tm.status = 'active' + AND NOT EXISTS ( + SELECT 1 + FROM public.patient_groups pg + WHERE pg.owner_id = tm.user_id + AND pg.is_system = true + ) + LOOP + PERFORM public.seed_system_groups_for_member(r.user_id, r.tenant_id); + END LOOP; +END; +$$; + + +-- ============================================================= +-- TRIGGER: semeia grupos padrão ao ativar novo membro de tenant +-- ============================================================= + +CREATE OR REPLACE FUNCTION public.trg_seed_system_groups_on_member_active() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +BEGIN + -- só age quando status muda para 'active' (insert ou update) + IF NEW.status = 'active' THEN + PERFORM public.seed_system_groups_for_member(NEW.user_id, NEW.tenant_id); + END IF; + RETURN NEW; +END; +$$; + +DROP TRIGGER IF EXISTS trg_seed_system_groups ON public.tenant_members; +CREATE TRIGGER trg_seed_system_groups + AFTER INSERT OR UPDATE OF status ON public.tenant_members + FOR EACH ROW + EXECUTE FUNCTION public.trg_seed_system_groups_on_member_active(); + + + + + + + + + +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "postgres"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "anon"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "authenticated"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "service_role"; + + + + + + +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "postgres"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "anon"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "authenticated"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "service_role"; + + + + + + +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "postgres"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "anon"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "authenticated"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "service_role"; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.vue b/src/App.vue index 43cb4d2..2fe97d3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,38 +1,128 @@ + + \ No newline at end of file diff --git a/src/features/agenda/components/AgendaEventDialog.vue b/src/features/agenda/components/AgendaEventDialog.vue index 041ab33..da49865 100644 --- a/src/features/agenda/components/AgendaEventDialog.vue +++ b/src/features/agenda/components/AgendaEventDialog.vue @@ -1,42 +1,530 @@ +