@fayz-ai/db 0.14.0 → 0.16.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 (68) hide show
  1. package/migrations/011_leadcontrol_joins_the_family.sql +5 -1
  2. package/migrations/029_o_administrador_enxerga_o_que_existe.sql +167 -0
  3. package/migrations/031_ticketcontrol_joins_the_family.sql +109 -0
  4. package/migrations/032_a_marca_e_um_conjunto_de_tokens.sql +291 -0
  5. package/migrations/041_a_entrega_tem_onde_guardar_a_politica.sql +53 -0
  6. package/migrations/042_o_token_de_marca_fecha_a_porta_anon.sql +17 -0
  7. package/migrations/043_fiscalcontrol_joins_the_family.sql +75 -0
  8. package/migrations/044_o_teste_gratis_dura_trinta_dias.sql +15 -0
  9. package/migrations/045_o_espelho_manda_enquanto_o_v2_nao_escreveu.sql +519 -0
  10. package/migrations/046_the_tick_advances_the_flows.sql +77 -0
  11. package/migrations/047_the_tick_sweeps_the_offers.sql +74 -0
  12. package/migrations/052_fullcontrol_joins_the_family.sql +213 -0
  13. package/migrations/053_o_erp_para_de_exigir_o_que_nao_desenha.sql +37 -0
  14. package/migrations/054_o_arquivo_migrado_ganha_bytes.sql +439 -0
  15. package/migrations/061_a_linha_sem_data_pode_ser_estrutura.sql +253 -0
  16. package/migrations/063_o_anexo_ja_registrado_recebe_os_bytes.sql +303 -0
  17. package/migrations/064_um_arquivo_anexado_a_dezenove_contas.sql +178 -0
  18. package/migrations/065_apagar_o_sobrevivente_nao_apaga_o_tenant.sql +59 -0
  19. package/migrations/066_the_port_machinery_closes_its_doors.sql +51 -0
  20. package/migrations/067_the_erp_keeps_the_front_door.sql +56 -0
  21. package/migrations/068_o_ledger_encontra_a_linha_pelo_caminho_que_o_writer_usa.sql +55 -0
  22. package/migrations/069_a_sondagem_de_irma_usa_indice.sql +293 -0
  23. package/migrations/071_o_sdr_trabalha_o_funil_e_mais_nada.sql +110 -0
  24. package/migrations/072_o_porte_nao_avisa_a_plataforma_de_uma_venda_antiga.sql +91 -0
  25. package/migrations/073_a_exclusao_e_contrato_do_cliente_nao_do_cluster.sql +300 -0
  26. package/migrations/074_a_classificacao_quebrada_nao_leva_o_dinheiro_junto.sql +332 -0
  27. package/migrations/075_o_apelido_da_referencia_nao_disputa_com_a_variavel.sql +276 -0
  28. package/migrations/076_o_desligado_tambem_precisa_atravessar.sql +110 -0
  29. package/migrations/077_a_unidade_escolhida_chega_ao_banco.sql +128 -0
  30. package/migrations/078_onde_o_profissional_atende_atravessa.sql +71 -0
  31. package/migrations/079_quem_aparece_na_agenda_e_quem_atende.sql +210 -0
  32. package/migrations/080_o_contexto_da_requisicao_se_calcula_uma_vez.sql +240 -0
  33. package/migrations/081_a_permissao_por_unidade_se_resolve_uma_vez_por_unidade.sql +88 -0
  34. package/migrations/082_a_cerca_tambem_se_lembra.sql +110 -0
  35. package/migrations/083_quem_e_profissional_continua_agendavel.sql +32 -0
  36. package/migrations/084_quem_atende_em_varias_nao_cabe_numa_coluna.sql +60 -0
  37. package/migrations/085_a_unidade_do_profissional_vem_de_onde_ele_atende.sql +47 -0
  38. package/migrations/086_a_ponte_de_clientes_volta_a_existir.sql +89 -0
  39. package/migrations/087_a_liberacao_pergunta_ao_indice_antes_da_funcao.sql +79 -0
  40. package/migrations/088_quem_pode_executar_o_que_o_porte_criou.sql +50 -0
  41. package/migrations/089_a_memoria_de_transacao_nao_serve_para_isto.sql +86 -0
  42. package/migrations/090_a_marca_tambem_conta_como_mudanca_de_configuracao.sql +75 -0
  43. package/migrations/091_o_shell_tambem_e_configuracao_do_tenant.sql +71 -0
  44. package/migrations/092_a_marca_tem_uma_grafia_so_e_uma_porta_so.sql +159 -0
  45. package/migrations/093_a_marca_carrega_o_tema_inteiro.sql +179 -0
  46. package/migrations/094_a_porta_do_dominio_aprende_a_grafia_nova.sql +55 -0
  47. package/migrations/095_o_aplicativo_tem_um_preset_que_a_conta_estende.sql +98 -0
  48. package/migrations/096_o_sublinhado_tambem_e_nome_de_modulo.sql +100 -0
  49. package/migrations/097_o_oficio_diz_o_que_oferece_e_a_conta_o_que_usa.sql +137 -0
  50. package/migrations/098_a_arvore_do_oficio_nomeia_quem_ainda_desenha.sql +22 -0
  51. package/migrations/099_o_estoque_mora_dentro_de_produtos.sql +48 -0
  52. package/migrations/100_o_chefcontrol_desenha_abas_de_modulo.sql +24 -0
  53. package/migrations/101_o_oficio_agrupa_o_plugin_nomeia.sql +93 -0
  54. package/migrations/102_tres_marcas_ganham_fundo_e_contraste.sql +73 -0
  55. package/migrations/103_great_djs_e_iam_club_entram_na_frota.sql +75 -0
  56. package/migrations/104_o_curso_e_a_bilheteria_ganham_preset.sql +29 -0
  57. package/migrations/105_o_que_o_oficio_exige_ele_tambem_oferece.sql +78 -0
  58. package/migrations/106_a_comunidade_se_gere_como_escola.sql +84 -0
  59. package/migrations/107_o_logo_da_coluna_e_projecao_do_documento.sql +53 -0
  60. package/migrations/108_o_modulo_do_oficio_vizinho_tem_onde_ser_visto.sql +78 -0
  61. package/migrations/109_a_conta_diz_o_que_nao_usa_e_a_marca_pinta_a_casa.sql +67 -0
  62. package/migrations/110_a_tabela_de_ramos_aprende_o_vocabulario_fechado.sql +60 -0
  63. package/migrations/111_o_erp_generico_nao_vende_por_pedido.sql +50 -0
  64. package/migrations/112_o_cache_do_contexto_sabe_de_quem_ele_e.sql +327 -0
  65. package/migrations/113_o_ramo_volta_a_saber_que_papeis_a_conta_nasce_tendo.sql +124 -0
  66. package/migrations/114_a_resposta_inicial_volta_a_dizer_o_que_a_pessoa_pode.sql +93 -0
  67. package/migrations/115_o_rail_e_de_quem_tem_um.sql +108 -0
  68. package/package.json +1 -1
@@ -0,0 +1,77 @@
1
+ -- 046_the_tick_advances_the_flows.sql
2
+ --
3
+ -- A cadence needs a clock, and the database already has one: `fayz_event_runner`
4
+ -- is a pg_cron job calling `event_runner_tick(200)` every minute. What it does
5
+ -- not have is a call to the thing that advances a multi-step flow.
6
+ --
7
+ -- Fifth call, same shape as the other four: guarded by `to_regprocedure`, so a
8
+ -- pool without plugin-automations ticks exactly as it does today. The guard is
9
+ -- the whole reason this belongs in the spine rather than in the plugin — the
10
+ -- tick is one function, and a plugin cannot append to it.
11
+ --
12
+ -- ORDER MATTERS, AND IT IS DELIBERATE. The flows run AFTER `notifications_drain`
13
+ -- and not before. A step enqueues with `scheduled_for = now()`, so draining
14
+ -- first means a step enqueued on this tick waits for the next one — sixty
15
+ -- seconds, once, at the start of a cadence that spans days. Running the flows
16
+ -- first would instead let a step be enqueued and drained in the same tick,
17
+ -- which sounds better and is not: the drain would be doing work the advisory
18
+ -- lock is already holding the whole database's tick for, and a slow provider
19
+ -- would then delay every other plugin's due work behind it. Cheap latency,
20
+ -- bought with predictable tick length.
21
+ --
22
+ -- The limit is 50 rather than the caller's `p_limit`. A cadence step is a write
23
+ -- plus an enqueue; two hundred of them in one tick is a long transaction under
24
+ -- an advisory lock that stops everything else. `marketing_run_due(20)` made the
25
+ -- same trade for the same reason.
26
+
27
+ CREATE OR REPLACE FUNCTION public.event_runner_tick(p_limit integer DEFAULT 200) RETURNS jsonb
28
+ LANGUAGE plpgsql SECURITY DEFINER
29
+ SET search_path TO ''
30
+ AS $_$
31
+ DECLARE
32
+ v_lock boolean;
33
+ v_events record;
34
+ v_notif record;
35
+ v_out jsonb := '{}'::jsonb;
36
+ v_extra jsonb;
37
+ v_start timestamptz := clock_timestamp();
38
+ BEGIN
39
+ -- 1 tick at a time per database. A second caller returns immediately rather
40
+ -- than waiting, so a cron that fires while the previous one is still going
41
+ -- does not queue up behind it.
42
+ SELECT pg_try_advisory_xact_lock(hashtextextended('fayz.event_runner', 0)) INTO v_lock;
43
+ IF NOT v_lock THEN
44
+ RETURN jsonb_build_object('skipped', 'another tick is running');
45
+ END IF;
46
+
47
+ SELECT * INTO v_events FROM public.consume_event_log(p_limit);
48
+ v_out := v_out || jsonb_build_object('events', to_jsonb(v_events));
49
+
50
+ -- The notification queue drains in its OWN pass. A provider timing out costs
51
+ -- a retry there and must never hold up the event log — which is why these are
52
+ -- two functions and not one loop.
53
+ IF to_regprocedure('public.notifications_drain(integer)') IS NOT NULL THEN
54
+ EXECUTE 'SELECT * FROM public.notifications_drain($1)' INTO v_notif USING p_limit;
55
+ v_out := v_out || jsonb_build_object('notifications', to_jsonb(v_notif));
56
+ END IF;
57
+
58
+ -- Campaigns, scheduled reports and cadence flows ENQUEUE; they never send. So
59
+ -- they run on the same tick as the log and drain through the same
60
+ -- notification queue.
61
+ IF to_regprocedure('public.marketing_run_due(integer)') IS NOT NULL THEN
62
+ EXECUTE 'SELECT public.marketing_run_due(20)' INTO v_extra;
63
+ v_out := v_out || jsonb_build_object('marketing', v_extra);
64
+ END IF;
65
+
66
+ IF to_regprocedure('public.reports_deliver_due(integer)') IS NOT NULL THEN
67
+ EXECUTE 'SELECT public.reports_deliver_due(50)' INTO v_extra;
68
+ v_out := v_out || jsonb_build_object('reports', v_extra);
69
+ END IF;
70
+
71
+ IF to_regprocedure('public.automations_flows_run_due(integer)') IS NOT NULL THEN
72
+ EXECUTE 'SELECT public.automations_flows_run_due(50)' INTO v_extra;
73
+ v_out := v_out || jsonb_build_object('flows', v_extra);
74
+ END IF;
75
+
76
+ RETURN v_out || jsonb_build_object('took_ms', (extract(epoch FROM clock_timestamp() - v_start) * 1000)::integer);
77
+ END $_$;
@@ -0,0 +1,74 @@
1
+ -- 047_the_tick_sweeps_the_offers.sql
2
+ --
3
+ -- Sixth call on the minute tick: expiring a lead offer that nobody took.
4
+ --
5
+ -- A five-minute window means the sweep's resolution has to be finer than the
6
+ -- window, and one minute is what the runner already gives. A rep therefore gets
7
+ -- their full five minutes and loses the lead within sixty seconds of the window
8
+ -- closing — which is the behaviour the rota is sold on.
9
+ --
10
+ -- It runs LAST, after the flows. Both write, and the offer sweep can cascade —
11
+ -- one expiry creating the next offer — so it is the call most likely to be slow
12
+ -- on a busy tenant. Last means a slow sweep delays nothing but itself.
13
+ --
14
+ -- The whole function is restated because a plugin cannot append to it, and
15
+ -- `CREATE OR REPLACE` needs the entire body. Migration 046 added the flows call
16
+ -- immediately before this one; if this file is ever reordered ahead of it, the
17
+ -- flows call disappears.
18
+
19
+ CREATE OR REPLACE FUNCTION public.event_runner_tick(p_limit integer DEFAULT 200) RETURNS jsonb
20
+ LANGUAGE plpgsql SECURITY DEFINER
21
+ SET search_path TO ''
22
+ AS $_$
23
+ DECLARE
24
+ v_lock boolean;
25
+ v_events record;
26
+ v_notif record;
27
+ v_out jsonb := '{}'::jsonb;
28
+ v_extra jsonb;
29
+ v_start timestamptz := clock_timestamp();
30
+ BEGIN
31
+ -- 1 tick at a time per database. A second caller returns immediately rather
32
+ -- than waiting, so a cron that fires while the previous one is still going
33
+ -- does not queue up behind it.
34
+ SELECT pg_try_advisory_xact_lock(hashtextextended('fayz.event_runner', 0)) INTO v_lock;
35
+ IF NOT v_lock THEN
36
+ RETURN jsonb_build_object('skipped', 'another tick is running');
37
+ END IF;
38
+
39
+ SELECT * INTO v_events FROM public.consume_event_log(p_limit);
40
+ v_out := v_out || jsonb_build_object('events', to_jsonb(v_events));
41
+
42
+ -- The notification queue drains in its OWN pass. A provider timing out costs
43
+ -- a retry there and must never hold up the event log — which is why these are
44
+ -- two functions and not one loop.
45
+ IF to_regprocedure('public.notifications_drain(integer)') IS NOT NULL THEN
46
+ EXECUTE 'SELECT * FROM public.notifications_drain($1)' INTO v_notif USING p_limit;
47
+ v_out := v_out || jsonb_build_object('notifications', to_jsonb(v_notif));
48
+ END IF;
49
+
50
+ -- Campaigns, scheduled reports and cadence flows ENQUEUE; they never send. So
51
+ -- they run on the same tick as the log and drain through the same
52
+ -- notification queue.
53
+ IF to_regprocedure('public.marketing_run_due(integer)') IS NOT NULL THEN
54
+ EXECUTE 'SELECT public.marketing_run_due(20)' INTO v_extra;
55
+ v_out := v_out || jsonb_build_object('marketing', v_extra);
56
+ END IF;
57
+
58
+ IF to_regprocedure('public.reports_deliver_due(integer)') IS NOT NULL THEN
59
+ EXECUTE 'SELECT public.reports_deliver_due(50)' INTO v_extra;
60
+ v_out := v_out || jsonb_build_object('reports', v_extra);
61
+ END IF;
62
+
63
+ IF to_regprocedure('public.automations_flows_run_due(integer)') IS NOT NULL THEN
64
+ EXECUTE 'SELECT public.automations_flows_run_due(50)' INTO v_extra;
65
+ v_out := v_out || jsonb_build_object('flows', v_extra);
66
+ END IF;
67
+
68
+ IF to_regprocedure('public.crm_offers_sweep(integer)') IS NOT NULL THEN
69
+ EXECUTE 'SELECT public.crm_offers_sweep(100)' INTO v_extra;
70
+ v_out := v_out || jsonb_build_object('offers', v_extra);
71
+ END IF;
72
+
73
+ RETURN v_out || jsonb_build_object('took_ms', (extract(epoch FROM clock_timestamp() - v_start) * 1000)::integer);
74
+ END $_$;
@@ -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';