@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.
Files changed (198) hide show
  1. package/canonical-schema.json +106 -23
  2. package/dist/index.cjs +4 -4
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +4 -4
  7. package/dist/index.js.map +1 -1
  8. package/dist/schema/spine.d.ts +42 -37
  9. package/dist/schema/spine.d.ts.map +1 -1
  10. package/migrations/000_baseline.sql +29667 -0
  11. package/migrations/001_the_tenant_has_apps.sql +314 -0
  12. package/migrations/002_the_spine_drops_the_vendor_and_the_dead.sql +129 -0
  13. package/migrations/003_the_role_rpcs_finish_and_the_mirrors_go.sql +135 -0
  14. package/migrations/004_the_projections_get_a_successor.sql +312 -0
  15. package/migrations/005_the_archetypes_anchor_to_the_unit.sql +90 -0
  16. package/migrations/006_the_helpers_read_the_source.sql +153 -0
  17. package/migrations/007_the_compat_machinery_leaves_with_the_tables.sql +77 -0
  18. package/migrations/008_the_switchers_see_across_accounts.sql +150 -0
  19. package/migrations/009_the_compat_aliases_nobody_adopted.sql +57 -0
  20. package/migrations/010_the_new_tenant_is_born_with_its_trade.sql +201 -0
  21. package/migrations/011_leadcontrol_joins_the_family.sql +120 -0
  22. package/migrations/012_chefcontrol_wears_its_own_mark.sql +28 -0
  23. package/migrations/013_the_workspace_switch_reaches_the_other_half.sql +156 -0
  24. package/migrations/014_the_chooser_can_read_what_it_offers.sql +91 -0
  25. package/migrations/015_the_unit_bindings_belong_to_one_workspace.sql +86 -0
  26. package/migrations/016_the_rule_stops_asking_every_row.sql +543 -0
  27. package/migrations/017_the_pin_is_checked_against_the_right_workspace.sql +139 -0
  28. package/migrations/018_the_bootstrap_says_what_you_reach.sql +114 -0
  29. package/migrations/019_distributing_one_row_stops_costing_the_table.sql +245 -0
  30. package/migrations/020_the_agenda_asks_where_someone_works.sql +81 -0
  31. package/migrations/022_the_line_says_which_document_it_belongs_to.sql +109 -0
  32. package/migrations/029_o_administrador_enxerga_o_que_existe.sql +167 -0
  33. package/migrations/031_ticketcontrol_joins_the_family.sql +109 -0
  34. package/migrations/032_a_marca_e_um_conjunto_de_tokens.sql +291 -0
  35. package/migrations/041_a_entrega_tem_onde_guardar_a_politica.sql +53 -0
  36. package/migrations/042_o_token_de_marca_fecha_a_porta_anon.sql +17 -0
  37. package/migrations/043_fiscalcontrol_joins_the_family.sql +75 -0
  38. package/migrations/044_o_teste_gratis_dura_trinta_dias.sql +15 -0
  39. package/migrations/046_the_tick_advances_the_flows.sql +77 -0
  40. package/migrations/047_the_tick_sweeps_the_offers.sql +74 -0
  41. package/migrations/052_fullcontrol_joins_the_family.sql +213 -0
  42. package/migrations/053_o_erp_para_de_exigir_o_que_nao_desenha.sql +37 -0
  43. package/package.json +1 -1
  44. package/migrations/0000_legacy_quarantine.sql +0 -86
  45. package/migrations/0000b_straggler_sweep.sql +0 -37
  46. package/migrations/000_core_v1_convert.sql +0 -343
  47. package/migrations/001_core.sql +0 -372
  48. package/migrations/002_rls_user_tenant_ids.sql +0 -64
  49. package/migrations/003_invitations.sql +0 -17
  50. package/migrations/004_archetypes.sql +0 -343
  51. package/migrations/005_locations_archetype.sql +0 -18
  52. package/migrations/006_archetype_rls.sql +0 -74
  53. package/migrations/007_archetype_grants.sql +0 -4
  54. package/migrations/008_grants.sql +0 -5
  55. package/migrations/009_anon_hardening.sql +0 -19
  56. package/migrations/010_migration_ledger.sql +0 -46
  57. package/migrations/011_anon_write_revoke.sql +0 -12
  58. package/migrations/012_rename_migration_ledger.sql +0 -61
  59. package/migrations/013_sequences_rls.sql +0 -22
  60. package/migrations/014_plan_entitlements.sql +0 -28
  61. package/migrations/015_agent_guard.sql +0 -108
  62. package/migrations/016_agent_guard_actor.sql +0 -102
  63. package/migrations/017_core_addresses.sql +0 -115
  64. package/migrations/018_global_search.sql +0 -482
  65. package/migrations/019_tenant_members_person_id.sql +0 -20
  66. package/migrations/020_fayz_projects.sql +0 -63
  67. package/migrations/021_tenant_roles.sql +0 -74
  68. package/migrations/022_invite_acceptance.sql +0 -91
  69. package/migrations/023_team_visibility.sql +0 -75
  70. package/migrations/024_analytics_engine.sql +0 -317
  71. package/migrations/025_created_by.sql +0 -75
  72. package/migrations/026_audit_trail.sql +0 -73
  73. package/migrations/027_domain_events.sql +0 -266
  74. package/migrations/028_tenant_scoped_token.sql +0 -127
  75. package/migrations/029_connections.sql +0 -186
  76. package/migrations/030_effect_idempotency.sql +0 -159
  77. package/migrations/031_sync_run_message.sql +0 -39
  78. package/migrations/032_connection_secrets.sql +0 -227
  79. package/migrations/033_sync_schedule.sql +0 -651
  80. package/migrations/034_custom_fields.sql +0 -55
  81. package/migrations/035_field_registry.sql +0 -148
  82. package/migrations/036_analytics_run_batch.sql +0 -84
  83. package/migrations/037_sync_tick_one_at_a_time.sql +0 -256
  84. package/migrations/038_onboarding_responses.sql +0 -103
  85. package/migrations/039_unit_tree.sql +0 -270
  86. package/migrations/040_resource_grants.sql +0 -474
  87. package/migrations/041_scoped_columns.sql +0 -192
  88. package/migrations/042_unit_scope_policies.sql +0 -145
  89. package/migrations/043_view_invoker.sql +0 -90
  90. package/migrations/044_unit_member_facts.sql +0 -47
  91. package/migrations/045_unit_entry.sql +0 -236
  92. package/migrations/046_membership_visible_to_members.sql +0 -85
  93. package/migrations/047_tasks.sql +0 -266
  94. package/migrations/048_every_login_is_a_person.sql +0 -190
  95. package/migrations/049_bookable_people.sql +0 -126
  96. package/migrations/050_authz_core.sql +0 -230
  97. package/migrations/051_authz_helpers.sql +0 -330
  98. package/migrations/052_authz_fanout.sql +0 -253
  99. package/migrations/053_authz_catalog_templates.sql +0 -402
  100. package/migrations/060_tenancy_seam.sql +0 -132
  101. package/migrations/061_create_tenant.sql +0 -125
  102. package/migrations/062_invitations.sql +0 -329
  103. package/migrations/063_legacy_tenancy_retired.sql +0 -568
  104. package/migrations/064_agent_guard_authz.sql +0 -118
  105. package/migrations/064b_supersede_unit_scope.sql +0 -205
  106. package/migrations/065_scaffold_sweep.sql +0 -139
  107. package/migrations/066_shadow_report.sql +0 -122
  108. package/migrations/070_permission_catalog.sql +0 -205
  109. package/migrations/071_v1_permission_map.sql +0 -266
  110. package/migrations/072_role_rpcs.sql +0 -126
  111. package/migrations/073_legacy_rbac_retire.sql +0 -471
  112. package/migrations/075_company_legal_entities.sql +0 -169
  113. package/migrations/076_tenant_settings.sql +0 -251
  114. package/migrations/080_people_roles.sql +0 -269
  115. package/migrations/081_people_identity.sql +0 -271
  116. package/migrations/082_people_membership_link.sql +0 -142
  117. package/migrations/083_people_data_migration.sql +0 -75
  118. package/migrations/084_people_registrations_tokens.sql +0 -282
  119. package/migrations/085_catalog_products.sql +0 -333
  120. package/migrations/086_catalog_pricing.sql +0 -194
  121. package/migrations/087_catalog_packages.sql +0 -134
  122. package/migrations/090_orders_venda.sql +0 -166
  123. package/migrations/091_orders_scaffold.sql +0 -92
  124. package/migrations/095_config_field_rules.sql +0 -261
  125. package/migrations/096_audit_append_only.sql +0 -256
  126. package/migrations/097_sequences_authorized.sql +0 -111
  127. package/migrations/098_holidays.sql +0 -80
  128. package/migrations/099_access_grants.sql +0 -293
  129. package/migrations/100_migration_schema.sql +0 -217
  130. package/migrations/101_migration_allowlist.sql +0 -202
  131. package/migrations/102_migration_rpcs.sql +0 -933
  132. package/migrations/103_migration_reconcile_ponr.sql +0 -132
  133. package/migrations/105_policies_to_authenticated.sql +0 -71
  134. package/migrations/106_function_search_path.sql +0 -55
  135. package/migrations/107_core_table_prefix.sql +0 -361
  136. package/migrations/108_authz_write_guards.sql +0 -149
  137. package/migrations/109_catalog_migration_promoted_columns.sql +0 -126
  138. package/migrations/110_people_kind_vocabulary.sql +0 -120
  139. package/migrations/111_migration_excluded_is_a_decision.sql +0 -259
  140. package/migrations/111_replacing_a_view_keeps_its_dependents.sql +0 -132
  141. package/migrations/112_money_precision_standard.sql +0 -132
  142. package/migrations/113_addresses_units_companies.sql +0 -114
  143. package/migrations/114_migrations_ledger_immutable.sql +0 -65
  144. package/migrations/115_registration_honours_field_rules.sql +0 -172
  145. package/migrations/116_field_rules_check_needs_a_tenant.sql +0 -72
  146. package/migrations/117_authz_reconciliation_schedule.sql +0 -82
  147. package/migrations/118_migration_catalog_writers.sql +0 -450
  148. package/migrations/119_migration_config_writers.sql +0 -193
  149. package/migrations/120_migration_facet_writer.sql +0 -139
  150. package/migrations/121_migration_financial_writers.sql +0 -294
  151. package/migrations/122_migration_inventory_writers.sql +0 -245
  152. package/migrations/123_migration_order_item_writer.sql +0 -144
  153. package/migrations/124_migration_ledger_writers.sql +0 -170
  154. package/migrations/125_migration_authz_people_writers.sql +0 -205
  155. package/migrations/126_owner_scoping_is_configuration.sql +0 -247
  156. package/migrations/127_scaffold_audit_hook.sql +0 -98
  157. package/migrations/128_shadow_tables_enforced.sql +0 -93
  158. package/migrations/129_order_kind_terminal_states.sql +0 -57
  159. package/migrations/130_every_login_is_a_person.sql +0 -196
  160. package/migrations/131_migration_invoice_split.sql +0 -127
  161. package/migrations/132_audit_prune_scheduled.sql +0 -28
  162. package/migrations/133_unit_is_a_session_property.sql +0 -155
  163. package/migrations/134_field_rules_on_read.sql +0 -341
  164. package/migrations/135_distribution_polarity.sql +0 -158
  165. package/migrations/136_unit_id_means_locality.sql +0 -358
  166. package/migrations/137_vendor_named_spine_objects.sql +0 -126
  167. package/migrations/138_grants_inherit_down_the_tree.sql +0 -178
  168. package/migrations/139_distributable_is_a_scaffold_option.sql +0 -109
  169. package/migrations/140_masking_is_a_scaffold_option.sql +0 -85
  170. package/migrations/141_renamed_objects_keep_a_compat_name.sql +0 -192
  171. package/migrations/142_a_person_has_addresses.sql +0 -212
  172. package/migrations/143_a_professional_has_a_rate.sql +0 -75
  173. package/migrations/144_a_product_can_be_sold_in_sizes.sql +0 -57
  174. package/migrations/145_the_app_says_which_tenant_it_is_in.sql +0 -123
  175. package/migrations/146_products_stock_has_one_writer.sql +0 -67
  176. package/migrations/147_every_archetype_has_a_kind_registry.sql +0 -388
  177. package/migrations/148_the_legacy_archetype_stops_being_writable.sql +0 -154
  178. package/migrations/149_the_event_log_gets_a_consumer.sql +0 -331
  179. package/migrations/150_the_app_is_configured_in_the_database.sql +0 -285
  180. package/migrations/151_stock_balance_is_an_archetype.sql +0 -144
  181. package/migrations/152_a_file_is_an_archetype.sql +0 -222
  182. package/migrations/153_a_plugin_calls_a_service_through_one_door.sql +0 -199
  183. package/migrations/154_the_plan_is_a_ceiling.sql +0 -258
  184. package/migrations/155_a_tenant_can_leave.sql +0 -271
  185. package/migrations/156_the_service_door_has_a_server_side.sql +0 -335
  186. package/migrations/157_nobody_creates_objects_in_public.sql +0 -74
  187. package/migrations/158_the_registry_owns_the_status_vocabulary.sql +0 -153
  188. package/migrations/159_the_plan_catalogue_gets_its_first_rows.sql +0 -132
  189. package/migrations/160_the_vertical_decides_what_starts_on.sql +0 -246
  190. package/migrations/161_a_tenant_leaves_with_its_data.sql +0 -363
  191. package/migrations/162_every_name_has_one_creator_for_real.sql +0 -126
  192. package/migrations/163_a_species_nobody_declared_goes_to_quarantine.sql +0 -184
  193. package/migrations/164_the_sequence_table_joins_the_register.sql +0 -26
  194. package/migrations/165_a_view_is_not_a_way_in.sql +0 -90
  195. package/migrations/166_the_platform_billing_leftovers_go.sql +0 -72
  196. package/migrations/167_a_view_gets_back_what_its_table_grants.sql +0 -112
  197. package/migrations/168_the_grant_census_stops_crying_wolf.sql +0 -33
  198. 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
@@ -3,7 +3,7 @@
3
3
  "fayz": {
4
4
  "status": "beta"
5
5
  },
6
- "version": "0.13.0",
6
+ "version": "0.15.0",
7
7
  "description": "Fayz SDK database layer — Drizzle schema primitives, spine references, and migration helpers shared across plugins.",
8
8
  "type": "module",
9
9
  "sideEffects": false,
@@ -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$;