@fayz-ai/db 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/canonical-schema.json +106 -23
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/schema/spine.d.ts +42 -37
- package/dist/schema/spine.d.ts.map +1 -1
- package/migrations/000_baseline.sql +29667 -0
- package/migrations/001_the_tenant_has_apps.sql +314 -0
- package/migrations/002_the_spine_drops_the_vendor_and_the_dead.sql +129 -0
- package/migrations/003_the_role_rpcs_finish_and_the_mirrors_go.sql +135 -0
- package/migrations/004_the_projections_get_a_successor.sql +312 -0
- package/migrations/005_the_archetypes_anchor_to_the_unit.sql +90 -0
- package/migrations/006_the_helpers_read_the_source.sql +153 -0
- package/migrations/007_the_compat_machinery_leaves_with_the_tables.sql +77 -0
- package/migrations/008_the_switchers_see_across_accounts.sql +150 -0
- package/migrations/009_the_compat_aliases_nobody_adopted.sql +57 -0
- package/migrations/010_the_new_tenant_is_born_with_its_trade.sql +201 -0
- package/migrations/011_leadcontrol_joins_the_family.sql +120 -0
- package/migrations/012_chefcontrol_wears_its_own_mark.sql +28 -0
- package/migrations/013_the_workspace_switch_reaches_the_other_half.sql +156 -0
- package/migrations/014_the_chooser_can_read_what_it_offers.sql +91 -0
- package/migrations/015_the_unit_bindings_belong_to_one_workspace.sql +86 -0
- package/migrations/016_the_rule_stops_asking_every_row.sql +543 -0
- package/migrations/017_the_pin_is_checked_against_the_right_workspace.sql +139 -0
- package/migrations/018_the_bootstrap_says_what_you_reach.sql +114 -0
- package/migrations/019_distributing_one_row_stops_costing_the_table.sql +245 -0
- package/migrations/020_the_agenda_asks_where_someone_works.sql +81 -0
- package/migrations/022_the_line_says_which_document_it_belongs_to.sql +109 -0
- package/migrations/029_o_administrador_enxerga_o_que_existe.sql +167 -0
- package/migrations/031_ticketcontrol_joins_the_family.sql +109 -0
- package/migrations/032_a_marca_e_um_conjunto_de_tokens.sql +291 -0
- package/migrations/041_a_entrega_tem_onde_guardar_a_politica.sql +53 -0
- package/migrations/042_o_token_de_marca_fecha_a_porta_anon.sql +17 -0
- package/migrations/043_fiscalcontrol_joins_the_family.sql +75 -0
- package/migrations/044_o_teste_gratis_dura_trinta_dias.sql +15 -0
- package/migrations/046_the_tick_advances_the_flows.sql +77 -0
- package/migrations/047_the_tick_sweeps_the_offers.sql +74 -0
- package/migrations/052_fullcontrol_joins_the_family.sql +213 -0
- package/migrations/053_o_erp_para_de_exigir_o_que_nao_desenha.sql +37 -0
- package/package.json +1 -1
- package/migrations/0000_legacy_quarantine.sql +0 -86
- package/migrations/0000b_straggler_sweep.sql +0 -37
- package/migrations/000_core_v1_convert.sql +0 -343
- package/migrations/001_core.sql +0 -372
- package/migrations/002_rls_user_tenant_ids.sql +0 -64
- package/migrations/003_invitations.sql +0 -17
- package/migrations/004_archetypes.sql +0 -343
- package/migrations/005_locations_archetype.sql +0 -18
- package/migrations/006_archetype_rls.sql +0 -74
- package/migrations/007_archetype_grants.sql +0 -4
- package/migrations/008_grants.sql +0 -5
- package/migrations/009_anon_hardening.sql +0 -19
- package/migrations/010_migration_ledger.sql +0 -46
- package/migrations/011_anon_write_revoke.sql +0 -12
- package/migrations/012_rename_migration_ledger.sql +0 -61
- package/migrations/013_sequences_rls.sql +0 -22
- package/migrations/014_plan_entitlements.sql +0 -28
- package/migrations/015_agent_guard.sql +0 -108
- package/migrations/016_agent_guard_actor.sql +0 -102
- package/migrations/017_core_addresses.sql +0 -115
- package/migrations/018_global_search.sql +0 -482
- package/migrations/019_tenant_members_person_id.sql +0 -20
- package/migrations/020_fayz_projects.sql +0 -63
- package/migrations/021_tenant_roles.sql +0 -74
- package/migrations/022_invite_acceptance.sql +0 -91
- package/migrations/023_team_visibility.sql +0 -75
- package/migrations/024_analytics_engine.sql +0 -317
- package/migrations/025_created_by.sql +0 -75
- package/migrations/026_audit_trail.sql +0 -73
- package/migrations/027_domain_events.sql +0 -266
- package/migrations/028_tenant_scoped_token.sql +0 -127
- package/migrations/029_connections.sql +0 -186
- package/migrations/030_effect_idempotency.sql +0 -159
- package/migrations/031_sync_run_message.sql +0 -39
- package/migrations/032_connection_secrets.sql +0 -227
- package/migrations/033_sync_schedule.sql +0 -651
- package/migrations/034_custom_fields.sql +0 -55
- package/migrations/035_field_registry.sql +0 -148
- package/migrations/036_analytics_run_batch.sql +0 -84
- package/migrations/037_sync_tick_one_at_a_time.sql +0 -256
- package/migrations/038_onboarding_responses.sql +0 -103
- package/migrations/039_unit_tree.sql +0 -270
- package/migrations/040_resource_grants.sql +0 -474
- package/migrations/041_scoped_columns.sql +0 -192
- package/migrations/042_unit_scope_policies.sql +0 -145
- package/migrations/043_view_invoker.sql +0 -90
- package/migrations/044_unit_member_facts.sql +0 -47
- package/migrations/045_unit_entry.sql +0 -236
- package/migrations/046_membership_visible_to_members.sql +0 -85
- package/migrations/047_tasks.sql +0 -266
- package/migrations/048_every_login_is_a_person.sql +0 -190
- package/migrations/049_bookable_people.sql +0 -126
- package/migrations/050_authz_core.sql +0 -230
- package/migrations/051_authz_helpers.sql +0 -330
- package/migrations/052_authz_fanout.sql +0 -253
- package/migrations/053_authz_catalog_templates.sql +0 -402
- package/migrations/060_tenancy_seam.sql +0 -132
- package/migrations/061_create_tenant.sql +0 -125
- package/migrations/062_invitations.sql +0 -329
- package/migrations/063_legacy_tenancy_retired.sql +0 -568
- package/migrations/064_agent_guard_authz.sql +0 -118
- package/migrations/064b_supersede_unit_scope.sql +0 -205
- package/migrations/065_scaffold_sweep.sql +0 -139
- package/migrations/066_shadow_report.sql +0 -122
- package/migrations/070_permission_catalog.sql +0 -205
- package/migrations/071_v1_permission_map.sql +0 -266
- package/migrations/072_role_rpcs.sql +0 -126
- package/migrations/073_legacy_rbac_retire.sql +0 -471
- package/migrations/075_company_legal_entities.sql +0 -169
- package/migrations/076_tenant_settings.sql +0 -251
- package/migrations/080_people_roles.sql +0 -269
- package/migrations/081_people_identity.sql +0 -271
- package/migrations/082_people_membership_link.sql +0 -142
- package/migrations/083_people_data_migration.sql +0 -75
- package/migrations/084_people_registrations_tokens.sql +0 -282
- package/migrations/085_catalog_products.sql +0 -333
- package/migrations/086_catalog_pricing.sql +0 -194
- package/migrations/087_catalog_packages.sql +0 -134
- package/migrations/090_orders_venda.sql +0 -166
- package/migrations/091_orders_scaffold.sql +0 -92
- package/migrations/095_config_field_rules.sql +0 -261
- package/migrations/096_audit_append_only.sql +0 -256
- package/migrations/097_sequences_authorized.sql +0 -111
- package/migrations/098_holidays.sql +0 -80
- package/migrations/099_access_grants.sql +0 -293
- package/migrations/100_migration_schema.sql +0 -217
- package/migrations/101_migration_allowlist.sql +0 -202
- package/migrations/102_migration_rpcs.sql +0 -933
- package/migrations/103_migration_reconcile_ponr.sql +0 -132
- package/migrations/105_policies_to_authenticated.sql +0 -71
- package/migrations/106_function_search_path.sql +0 -55
- package/migrations/107_core_table_prefix.sql +0 -361
- package/migrations/108_authz_write_guards.sql +0 -149
- package/migrations/109_catalog_migration_promoted_columns.sql +0 -126
- package/migrations/110_people_kind_vocabulary.sql +0 -120
- package/migrations/111_migration_excluded_is_a_decision.sql +0 -259
- package/migrations/111_replacing_a_view_keeps_its_dependents.sql +0 -132
- package/migrations/112_money_precision_standard.sql +0 -132
- package/migrations/113_addresses_units_companies.sql +0 -114
- package/migrations/114_migrations_ledger_immutable.sql +0 -65
- package/migrations/115_registration_honours_field_rules.sql +0 -172
- package/migrations/116_field_rules_check_needs_a_tenant.sql +0 -72
- package/migrations/117_authz_reconciliation_schedule.sql +0 -82
- package/migrations/118_migration_catalog_writers.sql +0 -450
- package/migrations/119_migration_config_writers.sql +0 -193
- package/migrations/120_migration_facet_writer.sql +0 -139
- package/migrations/121_migration_financial_writers.sql +0 -294
- package/migrations/122_migration_inventory_writers.sql +0 -245
- package/migrations/123_migration_order_item_writer.sql +0 -144
- package/migrations/124_migration_ledger_writers.sql +0 -170
- package/migrations/125_migration_authz_people_writers.sql +0 -205
- package/migrations/126_owner_scoping_is_configuration.sql +0 -247
- package/migrations/127_scaffold_audit_hook.sql +0 -98
- package/migrations/128_shadow_tables_enforced.sql +0 -93
- package/migrations/129_order_kind_terminal_states.sql +0 -57
- package/migrations/130_every_login_is_a_person.sql +0 -196
- package/migrations/131_migration_invoice_split.sql +0 -127
- package/migrations/132_audit_prune_scheduled.sql +0 -28
- package/migrations/133_unit_is_a_session_property.sql +0 -155
- package/migrations/134_field_rules_on_read.sql +0 -341
- package/migrations/135_distribution_polarity.sql +0 -158
- package/migrations/136_unit_id_means_locality.sql +0 -358
- package/migrations/137_vendor_named_spine_objects.sql +0 -126
- package/migrations/138_grants_inherit_down_the_tree.sql +0 -178
- package/migrations/139_distributable_is_a_scaffold_option.sql +0 -109
- package/migrations/140_masking_is_a_scaffold_option.sql +0 -85
- package/migrations/141_renamed_objects_keep_a_compat_name.sql +0 -192
- package/migrations/142_a_person_has_addresses.sql +0 -212
- package/migrations/143_a_professional_has_a_rate.sql +0 -75
- package/migrations/144_a_product_can_be_sold_in_sizes.sql +0 -57
- package/migrations/145_the_app_says_which_tenant_it_is_in.sql +0 -123
- package/migrations/146_products_stock_has_one_writer.sql +0 -67
- package/migrations/147_every_archetype_has_a_kind_registry.sql +0 -388
- package/migrations/148_the_legacy_archetype_stops_being_writable.sql +0 -154
- package/migrations/149_the_event_log_gets_a_consumer.sql +0 -331
- package/migrations/150_the_app_is_configured_in_the_database.sql +0 -285
- package/migrations/151_stock_balance_is_an_archetype.sql +0 -144
- package/migrations/152_a_file_is_an_archetype.sql +0 -222
- package/migrations/153_a_plugin_calls_a_service_through_one_door.sql +0 -199
- package/migrations/154_the_plan_is_a_ceiling.sql +0 -258
- package/migrations/155_a_tenant_can_leave.sql +0 -271
- package/migrations/156_the_service_door_has_a_server_side.sql +0 -335
- package/migrations/157_nobody_creates_objects_in_public.sql +0 -74
- package/migrations/158_the_registry_owns_the_status_vocabulary.sql +0 -153
- package/migrations/159_the_plan_catalogue_gets_its_first_rows.sql +0 -132
- package/migrations/160_the_vertical_decides_what_starts_on.sql +0 -246
- package/migrations/161_a_tenant_leaves_with_its_data.sql +0 -363
- package/migrations/162_every_name_has_one_creator_for_real.sql +0 -126
- package/migrations/163_a_species_nobody_declared_goes_to_quarantine.sql +0 -184
- package/migrations/164_the_sequence_table_joins_the_register.sql +0 -26
- package/migrations/165_a_view_is_not_a_way_in.sql +0 -90
- package/migrations/166_the_platform_billing_leftovers_go.sql +0 -72
- package/migrations/167_a_view_gets_back_what_its_table_grants.sql +0 -112
- package/migrations/168_the_grant_census_stops_crying_wolf.sql +0 -33
- package/migrations/169_the_census_asks_postgres_instead_of_guessing.sql +0 -45
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 052_fullcontrol_joins_the_family.sql — o funil vira a casa inteira.
|
|
3
|
+
--
|
|
4
|
+
-- A 011 deu uma linha ao LeadControl porque o funil tinha transbordado de uma
|
|
5
|
+
-- aba dentro do app do ofício. O que transbordou agora é o app inteiro: a
|
|
6
|
+
-- mesma base que guarda o lead guarda o título, o saldo de estoque e a nota, e
|
|
7
|
+
-- o produto que olha tudo isso de uma vez não é um CRM — é um ERP. Este
|
|
8
|
+
-- arquivo é o registro admitindo isso.
|
|
9
|
+
--
|
|
10
|
+
-- O recorte também mudou de público. Os outros Controls são OFÍCIOS (cozinha,
|
|
11
|
+
-- loja, estúdio, escola, bilheteria, escritório de contabilidade). Este é para
|
|
12
|
+
-- a empresa cujo RAMO não é o assunto do software — a distribuidora, a
|
|
13
|
+
-- confecção, a prestadora, a indústria pequena. Elas não precisam de um app
|
|
14
|
+
-- que entenda de comida; precisam de um que entenda de conta a pagar.
|
|
15
|
+
--
|
|
16
|
+
-- ── Por que um ID NOVO e não um UPDATE no 'crm' ────────────────────────────
|
|
17
|
+
--
|
|
18
|
+
-- `app.apps.id` é chave, e `app.tenant_apps.app_id` é a única FK que aponta
|
|
19
|
+
-- para ela (001 §2, ON DELETE RESTRICT) — então trocar o id é ADITIVO e
|
|
20
|
+
-- reversível, e custa as poucas linhas do §2. O que não é reversível é o que
|
|
21
|
+
-- sobra se não trocarmos: um app cujo id é o nome de UM dos seus módulos,
|
|
22
|
+
-- numa linha que passaria a ler `id = 'crm' … requires ARRAY['crm', …]` — um
|
|
23
|
+
-- app que exige a si mesmo. `tenant_app_set('crm')`, `tenant_plugin_set('crm')`
|
|
24
|
+
-- e `plan_permits('crm')` continuariam querendo dizer três coisas diferentes.
|
|
25
|
+
--
|
|
26
|
+
-- Hoje é o dia mais barato para fazer isso: `url` ainda é `localhost`, nenhum
|
|
27
|
+
-- domínio de cliente aponta para cá, e não existe união de ids em TypeScript
|
|
28
|
+
-- (packages/admin/src/directory/app-id.ts é explícito: "a rename is a row, not
|
|
29
|
+
-- a release"). O que o app lê é `VITE_APP_ID`, e ele acompanha este arquivo.
|
|
30
|
+
--
|
|
31
|
+
-- A linha antiga NÃO é apagada — é APOSENTADA (§3). A FK a protege, as linhas
|
|
32
|
+
-- de `app.tenant_apps` com app_id='crm' são o histórico de quem teve o produto,
|
|
33
|
+
-- e enquanto elas existirem a volta atrás é um UPDATE e não um INSERT.
|
|
34
|
+
--
|
|
35
|
+
-- ── `serves_verticals` fica VAZIO, e agora o argumento é o OPOSTO ──────────
|
|
36
|
+
--
|
|
37
|
+
-- Nas 011/031/043 o array ficou vazio porque aquele app servia a um recorte.
|
|
38
|
+
-- Aqui ele fica vazio porque o app serve a TODOS — e é exatamente por isso que
|
|
39
|
+
-- semear por vertical (010) seria o pior erro possível: todo restaurante criado
|
|
40
|
+
-- amanhã acordaria com um segundo produto no switcher, com financeiro e estoque
|
|
41
|
+
-- acesos que ninguém configurou. Um app que chega sem ser convidado não é
|
|
42
|
+
-- funcionalidade, é chamado de suporte. Opt-in via `tenant_app_set('full', …)`.
|
|
43
|
+
--
|
|
44
|
+
-- ── `requires` cresce em DEGRAUS, um marco por vez ─────────────────────────
|
|
45
|
+
--
|
|
46
|
+
-- Regra da 031/043: o array ganha um plugin no arquivo que traz as telas dele.
|
|
47
|
+
-- Aqui ele nomeia o que o bundle do app JÁ monta hoje (src/config/app.tsx):
|
|
48
|
+
-- crm, conversations, marketing, automations, notifications, reports,
|
|
49
|
+
-- financial e inventory.
|
|
50
|
+
--
|
|
51
|
+
-- E há um motivo mais duro para não listar o resto de uma vez:
|
|
52
|
+
-- **app.tenant_plugins não tem app_id**. Um plugin exigido aqui acende para o
|
|
53
|
+
-- TENANT, e aparece em qualquer outro Control que aquele tenant rode e que
|
|
54
|
+
-- empacote o mesmo plugin. Agenda, tasks, forms e orders entram quando este app
|
|
55
|
+
-- desenhar as telas deles.
|
|
56
|
+
--
|
|
57
|
+
-- Os addons ficam de fora por definição: `fiscal-br` e `banking-br` são opt-in
|
|
58
|
+
-- contratados, e o que entra em `requires` é ligado para todo mundo.
|
|
59
|
+
--
|
|
60
|
+
-- ── O que a 011 dizia sobre `financial`, e por que caiu ────────────────────
|
|
61
|
+
--
|
|
62
|
+
-- A 011 recusou `financial` com o argumento certo para o produto de então:
|
|
63
|
+
-- "um negócio ganho neste produto passa a bola para o app que é dono do
|
|
64
|
+
-- dinheiro; duplicar o razão aqui daria duas portas de entrada para a mesma
|
|
65
|
+
-- fatura." O argumento não mudou — mudou de lado. **O FullControl É o app que
|
|
66
|
+
-- é dono do dinheiro.** O orçamento aprovado no CRM vira título e aterrissa em
|
|
67
|
+
-- /financial/receivables/detail/:id sem sair do app, e é a mesma linha de
|
|
68
|
+
-- `plg_financial_invoices` que o ChefControl do mesmo tenant leria.
|
|
69
|
+
-- ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
-- ── §1 a linha nova ───────────────────────────────────────────────────────
|
|
72
|
+
--
|
|
73
|
+
-- #1E3A8A (blue-900) e não o teal que este arquivo carregou por uma tarde: o
|
|
74
|
+
-- CourseControl está com #0D9488 no cluster — a linha divergiu do âmbar que a
|
|
75
|
+
-- 001 deu a ele —, e dois teais no launcher leem como um produto em dois tons.
|
|
76
|
+
-- Também não é o #2563EB do LeadControl por acaso: é ele um degrau mais grave.
|
|
77
|
+
-- Para as nove contas que já tinham o ladrilho azul, o produto CRESCEU; a cor
|
|
78
|
+
-- diz produção onde a antiga dizia vendas. Branco sobre ele dá 10,4:1.
|
|
79
|
+
--
|
|
80
|
+
-- 'Layers' existe no registro CURADO de ícones (packages/ui/src/icons.ts) —
|
|
81
|
+
-- conferido, e não é zelo: 'Scissors', que a 001 deu ao StudioControl, NÃO
|
|
82
|
+
-- existe lá, e aquele ladrilho desenha iniciais desde então, sem erro nenhum.
|
|
83
|
+
-- Camadas sobre uma base é a figura literal de um ERP: vários módulos sobre um
|
|
84
|
+
-- tenant só. Um FullControlMark próprio entra quando existir marca em livro —
|
|
85
|
+
-- inventar um SVG que todos os irmãos passam a desenhar é pior que um glifo
|
|
86
|
+
-- bem escolhido (foi o que a 012 pagou para o ChefControl, e só depois do
|
|
87
|
+
-- brand book).
|
|
88
|
+
--
|
|
89
|
+
-- `url` é a origem de dev; a coluna é a allow-list do handoff, e o coalesce
|
|
90
|
+
-- impede que uma reaplicação devolva uma frota publicada para localhost
|
|
91
|
+
-- (regra da 031/043).
|
|
92
|
+
INSERT INTO app.apps (id, name, icon, accent_color, url, requires, serves_verticals, active) VALUES
|
|
93
|
+
('full', 'FullControl', 'Layers', '#1E3A8A', 'http://localhost:5307',
|
|
94
|
+
ARRAY['crm', 'conversations', 'marketing', 'automations', 'notifications',
|
|
95
|
+
'reports', 'financial', 'inventory'],
|
|
96
|
+
ARRAY[]::text[], true)
|
|
97
|
+
ON CONFLICT (id) DO UPDATE
|
|
98
|
+
SET name = EXCLUDED.name, icon = EXCLUDED.icon, accent_color = EXCLUDED.accent_color,
|
|
99
|
+
active = true,
|
|
100
|
+
-- `requires` só CRESCE numa reaplicação: um marco posterior pode ter
|
|
101
|
+
-- somado um plugin depois deste arquivo, e sobrescrever aqui desfaria
|
|
102
|
+
-- aquele marco em silêncio.
|
|
103
|
+
requires = (SELECT array_agg(DISTINCT p ORDER BY p)
|
|
104
|
+
FROM unnest(app.apps.requires || EXCLUDED.requires) AS p),
|
|
105
|
+
url = coalesce(app.apps.url, EXCLUDED.url);
|
|
106
|
+
|
|
107
|
+
COMMENT ON TABLE app.apps IS
|
|
108
|
+
'Which applications exist on this cluster (001, 011, 043, 052). The registry the app switcher reads. An app is a LENS over a tenant''s rows — it never scopes them; two apps on one tenant share public.people by design. `requires` names the plugins the app cannot work without, and it is a SEEDING list, not a dependency check — app.tenant_plugins has no app_id, so a plugin required here is lit for the whole tenant. `url` doubles as the allow-list for the session handoff; NULL takes the app off every switcher. `active = false` retires a row without breaking the FK from app.tenant_apps.';
|
|
109
|
+
|
|
110
|
+
-- ── §2 quem recebe: exatamente quem tinha o LeadControl ───────────────────
|
|
111
|
+
--
|
|
112
|
+
-- Carrega `status`, `source` e `added_by` da linha antiga: uma conta que tinha
|
|
113
|
+
-- DESLIGADO o LeadControl não pode acordar com o FullControl aceso, e uma linha
|
|
114
|
+
-- semeada pela plataforma ('default', 011 §3) não pode virar escolha de gente.
|
|
115
|
+
INSERT INTO app.tenant_apps (tenant_id, app_id, status, source, added_by)
|
|
116
|
+
SELECT ta.tenant_id, 'full', ta.status, ta.source, ta.added_by
|
|
117
|
+
FROM app.tenant_apps ta
|
|
118
|
+
WHERE ta.app_id = 'crm'
|
|
119
|
+
ON CONFLICT (tenant_id, app_id) DO NOTHING;
|
|
120
|
+
|
|
121
|
+
-- ── §3 a linha antiga é aposentada, não apagada ───────────────────────────
|
|
122
|
+
--
|
|
123
|
+
-- `active = false` já a tira do `app_config()` (008 filtra `a.active`), e
|
|
124
|
+
-- `url = NULL` é o segundo cinto: o cliente também descarta app sem url
|
|
125
|
+
-- (packages/admin/src/directory/directory.store.ts, normalize()). O nome fica,
|
|
126
|
+
-- para que uma consulta de suporte sobre uma linha antiga ainda leia.
|
|
127
|
+
UPDATE app.apps
|
|
128
|
+
SET active = false,
|
|
129
|
+
url = NULL
|
|
130
|
+
WHERE id = 'crm';
|
|
131
|
+
|
|
132
|
+
-- ── §4 o teto do plano passa a ter a linha do PLUGIN, não só das facets ───
|
|
133
|
+
--
|
|
134
|
+
-- Este bloco conserta um defeito que existia ANTES deste app e que só não
|
|
135
|
+
-- apareceu porque as contas do dogfood não têm plano.
|
|
136
|
+
--
|
|
137
|
+
-- `plan_permits(plugin, facet, tenant)` casa a facet EXATAMENTE
|
|
138
|
+
-- (000_baseline.sql), e "o plugin é a facet de nome vazio"
|
|
139
|
+
-- (packages/core/src/types/plugins.ts). Todo caminho de semeadura chama com
|
|
140
|
+
-- `facet = ''`: tenant_app_set (001), tenant_apply_vertical_defaults (010) e os
|
|
141
|
+
-- backfills das migrations de app. Mas o seed de `app.plan_grants` só escreveu
|
|
142
|
+
-- linhas de FACET (`financial.ledger`, `crm.contacts`, `inventory.stock`,
|
|
143
|
+
-- `agenda.scheduling`) — com `facet = ''` existem apenas `tasks`, `forms` e
|
|
144
|
+
-- `scale/marketing`.
|
|
145
|
+
--
|
|
146
|
+
-- Consequência: qualquer tenant COM plano é recusado no próprio plugin cujas
|
|
147
|
+
-- facets o plano dele inclui, e `tenant_apply_plan` acende facets órfãs que a
|
|
148
|
+
-- casca corretamente não lê como plugin (packages/core/src/app/app-config.ts).
|
|
149
|
+
-- Sem este §4, o FullControl acende hoje (contas sem plano) e para de acender
|
|
150
|
+
-- no dia em que alguém vender o primeiro plano — que é a pior hora possível
|
|
151
|
+
-- para descobrir.
|
|
152
|
+
--
|
|
153
|
+
-- `included` só onde o plano JÁ inclui uma facet daquele plugin: ali a linha
|
|
154
|
+
-- que falta é um defeito, não uma decisão de produto (é o "defeito 2" que a
|
|
155
|
+
-- 010 §2 nomeia). Todo o resto entra como `optional`, e isso é deliberado:
|
|
156
|
+
-- `plan_permits` NÃO lê o `mode`, então `optional` levanta o teto sem que
|
|
157
|
+
-- `tenant_apply_plan` imponha o módulo a quem não pediu.
|
|
158
|
+
DO $$
|
|
159
|
+
DECLARE p text;
|
|
160
|
+
BEGIN
|
|
161
|
+
IF to_regprocedure('public.register_plan_grant(text,text,text,text,text)') IS NULL THEN
|
|
162
|
+
RAISE NOTICE '052: register_plan_grant ausente — §4 pulado';
|
|
163
|
+
RETURN;
|
|
164
|
+
END IF;
|
|
165
|
+
|
|
166
|
+
-- 4.1 a linha do plugin para cada plugin cujas facets os planos já incluem.
|
|
167
|
+
FOREACH p IN ARRAY ARRAY['financial', 'inventory', 'crm', 'agenda'] LOOP
|
|
168
|
+
PERFORM public.register_plan_grant('essential', p, '', 'included',
|
|
169
|
+
'O plugin é a facet de nome vazio: sem esta linha o teto recusa o plugin cujas facets este plano inclui (052 §4).');
|
|
170
|
+
PERFORM public.register_plan_grant('pro', p, '', 'included', NULL);
|
|
171
|
+
PERFORM public.register_plan_grant('scale', p, '', 'included', NULL);
|
|
172
|
+
END LOOP;
|
|
173
|
+
|
|
174
|
+
-- 4.2 o que o ERP soma, por plano. `optional` = permitido, não imposto.
|
|
175
|
+
-- No essential entram só os dois que atendem e cobram — um ERP inteiro não é
|
|
176
|
+
-- produto de plano básico.
|
|
177
|
+
PERFORM public.register_plan_grant('essential', 'conversations', '', 'optional', 'Onde o cliente e o fornecedor falam.');
|
|
178
|
+
PERFORM public.register_plan_grant('essential', 'notifications', '', 'optional', 'Toda cobrança e toda confirmação saem por aqui.');
|
|
179
|
+
|
|
180
|
+
FOREACH p IN ARRAY ARRAY['conversations', 'notifications', 'marketing', 'automations',
|
|
181
|
+
'reports', 'tasks', 'forms'] LOOP
|
|
182
|
+
PERFORM public.register_plan_grant('pro', p, '', 'optional', NULL);
|
|
183
|
+
PERFORM public.register_plan_grant('scale', p, '', 'optional', NULL);
|
|
184
|
+
END LOOP;
|
|
185
|
+
|
|
186
|
+
-- 4.3 os addons: permitidos só no topo, e nunca `included`.
|
|
187
|
+
PERFORM public.register_plan_grant('scale', 'fiscal-br', '', 'optional', 'Addon: a emissão fiscal é contratada, não incluída.');
|
|
188
|
+
PERFORM public.register_plan_grant('scale', 'banking-br', '', 'optional', 'Addon: a conciliação bancária é contratada.');
|
|
189
|
+
END $$;
|
|
190
|
+
|
|
191
|
+
-- ── §5 os plugins que o app exige, com o teto do plano respeitado ─────────
|
|
192
|
+
--
|
|
193
|
+
-- DISTINCT porque um tenant que chega por dois caminhos faria o ON CONFLICT
|
|
194
|
+
-- recusar tocar a linha duas vezes; `plan_permits` porque o teto do plano vence
|
|
195
|
+
-- sempre sobre um padrão — uma conta cujo plano não inclui o plugin não o
|
|
196
|
+
-- ganha aceso, e a semeadura não falha por isso (011 §3, mesma regra).
|
|
197
|
+
INSERT INTO app.tenant_plugins (tenant_id, plugin_id, facet, status, source)
|
|
198
|
+
SELECT DISTINCT ta.tenant_id, r.plugin_id, '', 'active', 'default'
|
|
199
|
+
FROM app.tenant_apps ta
|
|
200
|
+
JOIN app.apps a ON a.id = ta.app_id
|
|
201
|
+
CROSS JOIN LATERAL unnest(a.requires) AS r(plugin_id)
|
|
202
|
+
WHERE ta.app_id = 'full'
|
|
203
|
+
AND ta.status = 'active'
|
|
204
|
+
AND public.plan_permits(r.plugin_id, '', ta.tenant_id)
|
|
205
|
+
ON CONFLICT (tenant_id, plugin_id, facet) DO NOTHING;
|
|
206
|
+
|
|
207
|
+
-- ── §6 (comentado) o ramo órfão ───────────────────────────────────────────
|
|
208
|
+
--
|
|
209
|
+
-- 'agency' está em app.vertical_defaults desde o baseline e NENHUM app o serve:
|
|
210
|
+
-- um tenant criado com vertical_id='agency' ganha plugins e zero apps. Se
|
|
211
|
+
-- alguém decidir que o ERP genérico é a resposta honesta para esse ramo, é UMA
|
|
212
|
+
-- linha — e ela merece uma migration própria, para poder ser revista sozinha:
|
|
213
|
+
-- UPDATE app.apps SET serves_verticals = ARRAY['agency'] WHERE id = 'full';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 053_o_erp_para_de_exigir_o_que_nao_desenha.sql — o FullControl encolhe o
|
|
3
|
+
-- `requires` para o que ele realmente monta.
|
|
4
|
+
--
|
|
5
|
+
-- A 052 registrou o app com oito plugins, e naquele dia ele montava os oito. No
|
|
6
|
+
-- mesmo dia o recorte apertou: o FullControl é para a empresa que vende SERVIÇO
|
|
7
|
+
-- e SOFTWARE — agência, software house, consultoria —, e duas coisas saíram do
|
|
8
|
+
-- bundle por não existirem nessa casa:
|
|
9
|
+
--
|
|
10
|
+
-- `inventory` prateleira. Quem tem estoque é atendido pelo StoreControl e
|
|
11
|
+
-- pelo ChefControl, e um ERP que oferece contagem de inventário
|
|
12
|
+
-- a quem não tem depósito é um formulário a mais.
|
|
13
|
+
-- `marketing` campanha e canal de aquisição. O funil fica (o orçamento é o
|
|
14
|
+
-- começo do título), mas a máquina de aquisição é produto do
|
|
15
|
+
-- LeadControl, que é justamente de onde este app veio.
|
|
16
|
+
--
|
|
17
|
+
-- ── Por que isso é uma migration, e não uma edição na 052 ──────────────────
|
|
18
|
+
--
|
|
19
|
+
-- A 052 já foi aplicada e o ledger guarda o sha256 dela; arquivo aplicado é
|
|
20
|
+
-- congelado. E a 052 tem, de propósito, um `ON CONFLICT` que só faz `requires`
|
|
21
|
+
-- CRESCER numa reaplicação — então nem reaplicá-la desfaria isto. Encolher é um
|
|
22
|
+
-- ato explícito, e é este arquivo.
|
|
23
|
+
--
|
|
24
|
+
-- ── O que este arquivo NÃO faz: desligar plugin de ninguém ─────────────────
|
|
25
|
+
--
|
|
26
|
+
-- `requires` é lista de SEMEADURA, não de dependência: ele decide o que um
|
|
27
|
+
-- tenant ganha ao ATIVAR o app daqui para a frente. As contas que já receberam
|
|
28
|
+
-- `inventory` e `marketing` ficam com eles acesos, e isso está certo — a
|
|
29
|
+
-- 052 §5 os acendeu para o TENANT (app.tenant_plugins não tem app_id), e o
|
|
30
|
+
-- ChefControl e o StoreControl das mesmas contas estão de pé sobre `inventory`
|
|
31
|
+
-- neste exato momento. Apagar a linha aqui apagaria o estoque do restaurante.
|
|
32
|
+
-- ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
UPDATE app.apps
|
|
35
|
+
SET requires = ARRAY['crm', 'conversations', 'automations', 'notifications',
|
|
36
|
+
'reports', 'financial']
|
|
37
|
+
WHERE id = 'full';
|
package/package.json
CHANGED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
-- ============================================================================
|
|
2
|
-
-- 0000_legacy_quarantine.sql — pre-convert collision guard (ALL pools)
|
|
3
|
-
--
|
|
4
|
-
-- MUST run BEFORE 000_core_v1_convert.sql. The filename starts `0000_` so it
|
|
5
|
-
-- lexically sorts ahead of `000_core_v1_convert.sql` (the runner orders files
|
|
6
|
-
-- with a plain string sort — verify: ['0000_legacy_quarantine.sql',
|
|
7
|
-
-- '000_core_v1_convert.sql'].sort()).
|
|
8
|
-
--
|
|
9
|
-
-- WHY: the in-place converter moves saas_core.<source> -> public.<target> only
|
|
10
|
-
-- when public.<target> is FREE (guarded on target absence). If a pool already
|
|
11
|
-
-- holds a legacy public.<target> (a bespoke pre-pool table sharing the name),
|
|
12
|
-
-- the move/rename silently no-ops and the live saas_core.<source> is left
|
|
13
|
-
-- behind — then `DROP SCHEMA saas_core CASCADE` would DESTROY it. This file
|
|
14
|
-
-- gets the colliding legacy table out of the way first, into a quarantine
|
|
15
|
-
-- schema, so every source table has a free target to land on.
|
|
16
|
-
--
|
|
17
|
-
-- For EVERY (source -> target) pair the converter touches, if BOTH
|
|
18
|
-
-- saas_core.<source> AND public.<target> exist, move public.<target> into
|
|
19
|
-
-- legacy_pre_pools. The presence of saas_core.<source> proves the convert has
|
|
20
|
-
-- not run yet, so this public.<target> must be a legacy collision (not the
|
|
21
|
-
-- converted core table).
|
|
22
|
-
--
|
|
23
|
-
-- Known real collisions from the 2026-07-14 backups (all covered by the loop):
|
|
24
|
-
-- * creators — public.subscriptions (4 rows, direct-move pair)
|
|
25
|
-
-- * salon/gphx — public.appointments (0 rows, bookings->appointments rename)
|
|
26
|
-
-- * restaurant — public.orders + public.order_items (0 rows, direct-move pairs)
|
|
27
|
-
--
|
|
28
|
-
-- Idempotent: guarded on saas_core existing; each move guarded on both source
|
|
29
|
-
-- present AND target present; a no-op on fresh pools and on already-converted
|
|
30
|
-
-- pools (saas_core dropped -> early return).
|
|
31
|
-
-- ============================================================================
|
|
32
|
-
|
|
33
|
-
DO $quarantine$
|
|
34
|
-
DECLARE
|
|
35
|
-
r record;
|
|
36
|
-
BEGIN
|
|
37
|
-
-- Only act on pools that still carry the legacy schema (pre-convert).
|
|
38
|
-
IF NOT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'saas_core') THEN
|
|
39
|
-
RETURN;
|
|
40
|
-
END IF;
|
|
41
|
-
|
|
42
|
-
-- (source in saas_core) -> (target in public) for every table the converter
|
|
43
|
-
-- moves. Direct moves keep the name; the three changed entities are renamed
|
|
44
|
-
-- (persons->people, bookings->appointments, booking_items->appointment_items).
|
|
45
|
-
-- Mirrors the move list in 000_core_v1_convert.sql exactly.
|
|
46
|
-
FOR r IN
|
|
47
|
-
SELECT * FROM (VALUES
|
|
48
|
-
('tenants', 'tenants'),
|
|
49
|
-
('profiles', 'profiles'),
|
|
50
|
-
('tenant_members', 'tenant_members'),
|
|
51
|
-
('plans', 'plans'),
|
|
52
|
-
('permissions', 'permissions'),
|
|
53
|
-
('role_permissions', 'role_permissions'),
|
|
54
|
-
('tenant_role_overrides', 'tenant_role_overrides'),
|
|
55
|
-
('invitations', 'invitations'),
|
|
56
|
-
('payment_events', 'payment_events'),
|
|
57
|
-
('locations', 'locations'),
|
|
58
|
-
('location_members', 'location_members'),
|
|
59
|
-
('audit_logs', 'audit_logs'),
|
|
60
|
-
('subscriptions', 'subscriptions'),
|
|
61
|
-
('invoices', 'invoices'),
|
|
62
|
-
('notifications', 'notifications'),
|
|
63
|
-
('persons', 'people'),
|
|
64
|
-
('categories', 'categories'),
|
|
65
|
-
('products', 'products'),
|
|
66
|
-
('services', 'services'),
|
|
67
|
-
('orders', 'orders'),
|
|
68
|
-
('order_items', 'order_items'),
|
|
69
|
-
('transactions', 'transactions'),
|
|
70
|
-
('bookings', 'appointments'),
|
|
71
|
-
('booking_items', 'appointment_items'),
|
|
72
|
-
('schedules', 'schedules'),
|
|
73
|
-
('sequences', 'sequences'),
|
|
74
|
-
('documents', 'documents')
|
|
75
|
-
) AS m(source, target)
|
|
76
|
-
LOOP
|
|
77
|
-
IF to_regclass('saas_core.' || quote_ident(r.source)) IS NOT NULL
|
|
78
|
-
AND to_regclass('public.' || quote_ident(r.target)) IS NOT NULL THEN
|
|
79
|
-
CREATE SCHEMA IF NOT EXISTS legacy_pre_pools;
|
|
80
|
-
EXECUTE format('ALTER TABLE public.%I SET SCHEMA legacy_pre_pools', r.target);
|
|
81
|
-
RAISE NOTICE 'legacy_pre_pools: quarantined public.% (collides with the saas_core.% -> public.% move)',
|
|
82
|
-
r.target, r.source, r.target;
|
|
83
|
-
END IF;
|
|
84
|
-
END LOOP;
|
|
85
|
-
END;
|
|
86
|
-
$quarantine$;
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
-- Move any saas_core table the converter does not know about into public,
|
|
2
|
-
-- so the converter's emptiness gate can pass and drop the schema safely.
|
|
3
|
-
-- Found live: salon carries saas_core.tenant_roles (RBAC iteration that only
|
|
4
|
-
-- ever existed there). Runs between 0000_legacy_quarantine and
|
|
5
|
-
-- 000_core_v1_convert ('0000b' < '000_' lexically). Idempotent: no-ops when
|
|
6
|
-
-- saas_core is gone or has no stragglers. A public-name collision aborts —
|
|
7
|
-
-- that case must be quarantined explicitly, never guessed.
|
|
8
|
-
|
|
9
|
-
DO $sweep$
|
|
10
|
-
DECLARE
|
|
11
|
-
r record;
|
|
12
|
-
known text[] := ARRAY[
|
|
13
|
-
'tenants', 'profiles', 'tenant_members', 'plans', 'permissions',
|
|
14
|
-
'role_permissions', 'tenant_role_overrides', 'invitations',
|
|
15
|
-
'payment_events', 'locations', 'location_members', 'audit_logs',
|
|
16
|
-
'subscriptions', 'invoices', 'notifications',
|
|
17
|
-
'persons', 'categories', 'products', 'services', 'orders', 'order_items',
|
|
18
|
-
'transactions', 'bookings', 'booking_items', 'schedules',
|
|
19
|
-
'sequences', 'documents'
|
|
20
|
-
];
|
|
21
|
-
BEGIN
|
|
22
|
-
IF NOT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'saas_core') THEN
|
|
23
|
-
RETURN;
|
|
24
|
-
END IF;
|
|
25
|
-
|
|
26
|
-
FOR r IN
|
|
27
|
-
SELECT table_name FROM information_schema.tables
|
|
28
|
-
WHERE table_schema = 'saas_core' AND table_type = 'BASE TABLE'
|
|
29
|
-
AND table_name <> ALL (known)
|
|
30
|
-
LOOP
|
|
31
|
-
IF to_regclass('public.' || quote_ident(r.table_name)) IS NOT NULL THEN
|
|
32
|
-
RAISE EXCEPTION 'straggler saas_core.% collides with an existing public table — quarantine it explicitly', r.table_name;
|
|
33
|
-
END IF;
|
|
34
|
-
EXECUTE format('ALTER TABLE saas_core.%I SET SCHEMA public', r.table_name);
|
|
35
|
-
RAISE NOTICE 'straggler moved: saas_core.% -> public', r.table_name;
|
|
36
|
-
END LOOP;
|
|
37
|
-
END $sweep$;
|