@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,178 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 064_um_arquivo_anexado_a_dezenove_contas.sql — a identidade do ledger de bytes
3
+ -- era o objeto de origem, e ela não basta.
4
+ --
5
+ -- Medido em 19/09/2026 no Pertinho do Céu, logo depois da 063:
6
+ --
7
+ -- 1.192 registros de anexo apontam para 681 arquivos distintos
8
+ -- 41 arquivos estão anexados a mais de uma conta
9
+ -- `payroll/8/1781889562635_RECIBO_DE_PAGAMENTO_05.2026.pdf` está em 19
10
+ --
11
+ -- E faz sentido: um recibo de folha cobre dezenove funcionários, então o V1
12
+ -- anexou o MESMO arquivo às dezenove contas. Não é sujeira — é como a casa
13
+ -- trabalha.
14
+ --
15
+ -- ── O que quebrava ────────────────────────────────────────────────────────
16
+ --
17
+ -- A 054 chaveia o ledger de bytes por `(projeto, 'storage:<bucket>', md5 do
18
+ -- caminho de origem)`. Para o avatar e a foto de ponto isso é exato: um objeto,
19
+ -- um registro. Para o anexo não, e o efeito é silencioso — a primeira das
20
+ -- dezenove sobe e fica `migrated`; as outras dezoito batem no mesmo ledger,
21
+ -- voltam `skipped` por "bytes iguais", e ficam `v1-pending` **para sempre**,
22
+ -- apontando para um V1 que vai sair do ar.
23
+ --
24
+ -- São **511 dos 1.192** registros nessa situação. E o pior: a rodada termina
25
+ -- verde. `skipped` é a resposta certa para "já atravessou", e nada distingue
26
+ -- "já atravessou" de "atravessou para OUTRO registro".
27
+ --
28
+ -- ── A identidade certa ────────────────────────────────────────────────────
29
+ --
30
+ -- Um byte que atravessa tem DUAS pontas, e o ledger precisa das duas: de onde
31
+ -- veio, e para qual registro foi. Quando o backlog nomeia um `document_id`, ele
32
+ -- entra na chave. Quando não nomeia — avatar, foto de ponto —, a chave é a de
33
+ -- antes, porque ali o registro é derivado do objeto e a segunda ponta não
34
+ -- acrescenta nada.
35
+ --
36
+ -- Cada um dos dezenove ganha a sua linha de ledger, o seu objeto no bucket e o
37
+ -- seu checksum. Dezenove cópias do mesmo PDF é o preço, e é o preço certo:
38
+ -- registros independentes, um apagado não cega os outros dezoito, e a conta de
39
+ -- "quantos arquivos eu tenho desta pessoa" continua fechando.
40
+ --
41
+ -- ── As cinco linhas já gravadas ───────────────────────────────────────────
42
+ --
43
+ -- O ensaio da 063 gravou cinco linhas com a chave antiga. Elas não são
44
+ -- apagadas: a chave nova não as encontra, o backlog reoferece os registros, e o
45
+ -- upload vai com `x-upsert`. O que sobra é cinco linhas de ledger órfãs
46
+ -- apontando para objetos que existem — inofensivas, e preferíveis a um DELETE
47
+ -- num ledger de produção para arrumar aparência.
48
+ --
49
+ -- Idempotente e replay-safe.
50
+ -- ---------------------------------------------------------------------------
51
+
52
+ CREATE OR REPLACE FUNCTION migration._attach_asset_impl(
53
+ p_batch uuid, p_kind text, p_source_path text, p_row jsonb)
54
+ RETURNS jsonb
55
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
56
+ AS $$
57
+ DECLARE
58
+ b migration.batches%ROWTYPE;
59
+ k migration.asset_kinds%ROWTYPE;
60
+ l migration.ledger%ROWTYPE;
61
+ v_source_table text;
62
+ v_source_uuid uuid;
63
+ v_checksum text := p_row ->> 'checksum';
64
+ v_doc uuid;
65
+ v_person uuid := nullif(p_row ->> 'person_id', '')::uuid;
66
+ v_adotado uuid := nullif(p_row ->> 'document_id', '')::uuid;
67
+ BEGIN
68
+ SELECT * INTO b FROM migration.batches WHERE id = p_batch;
69
+ IF NOT FOUND THEN RAISE EXCEPTION 'attach_asset: lote % desconhecido', p_batch USING ERRCODE = 'P0002'; END IF;
70
+ IF b.status <> 'running' THEN
71
+ RAISE EXCEPTION 'attach_asset: o lote % está % (só um lote em execução aceita arquivo)', p_batch, b.status USING ERRCODE = '55000';
72
+ END IF;
73
+ PERFORM migration._assert_fence(b.tenant_id);
74
+
75
+ SELECT * INTO k FROM migration.asset_kinds WHERE kind = p_kind;
76
+ IF NOT FOUND THEN
77
+ RAISE EXCEPTION 'attach_asset: espécie % não está em migration.asset_kinds', p_kind USING ERRCODE = '22023';
78
+ END IF;
79
+ IF v_checksum IS NULL OR p_row ->> 'target_path' IS NULL THEN
80
+ RAISE EXCEPTION 'attach_asset: preciso de checksum e target_path' USING ERRCODE = '22023';
81
+ END IF;
82
+
83
+ v_source_table := 'storage:' || k.source_bucket;
84
+ -- As duas pontas do byte. O `document_id` só entra quando existe: sem ele a
85
+ -- chave é a da 054, e um avatar já migrado continua sendo reconhecido.
86
+ v_source_uuid := md5(v_source_table || ':' || p_source_path
87
+ || coalesce('::' || v_adotado::text, ''))::uuid;
88
+
89
+ SELECT * INTO l FROM migration.ledger
90
+ WHERE source_project = b.source_project AND source_table = v_source_table
91
+ AND source_uuid = v_source_uuid AND coalesce(target_table, '') = 'public.documents'
92
+ FOR UPDATE;
93
+ IF NOT FOUND THEN
94
+ INSERT INTO migration.ledger (source_project, source_table, source_uuid, tenant_id,
95
+ target_table, checksum, cursor, batch_id, status)
96
+ VALUES (b.source_project, v_source_table, v_source_uuid, b.tenant_id,
97
+ 'public.documents', v_checksum, p_source_path, p_batch, 'pending')
98
+ RETURNING * INTO l;
99
+ ELSIF l.tenant_id <> b.tenant_id THEN
100
+ RETURN migration._quarantine(l.id, p_batch, 'tenant',
101
+ format('o objeto %s já é do tenant %s; este lote migra o %s', p_source_path, l.tenant_id, b.tenant_id), p_row);
102
+ ELSIF l.status = 'migrated' AND l.checksum = v_checksum THEN
103
+ PERFORM migration._count(p_batch, 'skipped');
104
+ RETURN jsonb_build_object('status', 'skipped', 'reason', 'bytes iguais',
105
+ 'target_table', 'public.documents', 'target_id', l.target_id, 'ledger_id', l.id);
106
+ END IF;
107
+
108
+ -- O registro ADOTADO vem primeiro: quando o backlog nomeia um documento,
109
+ -- aquela linha é o anexo, e cunhar outra deixaria o mesmo arquivo duas vezes
110
+ -- na conta (063).
111
+ v_doc := coalesce(v_adotado, l.target_id, md5('document:' || p_kind || ':' || (p_row ->> 'target_path'))::uuid);
112
+
113
+ IF v_adotado IS NOT NULL AND NOT EXISTS (
114
+ SELECT 1 FROM public.documents d WHERE d.id = v_adotado AND d.tenant_id = b.tenant_id) THEN
115
+ RETURN migration._quarantine(l.id, p_batch, 'fk',
116
+ format('o registro %s não existe neste tenant — o backlog está apontando para um documento que sumiu', v_adotado), p_row);
117
+ END IF;
118
+
119
+ INSERT INTO public.documents (
120
+ id, tenant_id, kind, person_id, subject_type, subject_id, title, status,
121
+ file_url, file_name, file_size, mime_type,
122
+ storage_provider, storage_bucket, storage_path, checksum, metadata)
123
+ VALUES (
124
+ v_doc, b.tenant_id, p_kind, v_person,
125
+ p_row ->> 'subject_type', nullif(p_row ->> 'subject_id', '')::uuid,
126
+ p_row ->> 'title', 'active',
127
+ p_row ->> 'public_url', p_row ->> 'file_name',
128
+ nullif(p_row ->> 'file_size', '')::integer, p_row ->> 'mime_type',
129
+ 'supabase', k.target_bucket, p_row ->> 'target_path', v_checksum,
130
+ jsonb_build_object('migration', jsonb_build_object(
131
+ 'source_project', b.source_project, 'source_bucket', k.source_bucket,
132
+ 'source_path', p_source_path, 'batch_id', p_batch)))
133
+ ON CONFLICT (id) DO UPDATE SET
134
+ person_id = coalesce(EXCLUDED.person_id, public.documents.person_id),
135
+ -- Num registro adotado, quem sabe o sujeito e o nome é a linha que já está
136
+ -- lá: ela veio do de-para. O que o transporte tem a dizer é onde os bytes
137
+ -- foram parar.
138
+ subject_type = coalesce(public.documents.subject_type, EXCLUDED.subject_type),
139
+ subject_id = coalesce(public.documents.subject_id, EXCLUDED.subject_id),
140
+ title = coalesce(public.documents.title, EXCLUDED.title),
141
+ file_url = EXCLUDED.file_url,
142
+ file_name = coalesce(public.documents.file_name, EXCLUDED.file_name),
143
+ file_size = EXCLUDED.file_size,
144
+ mime_type = coalesce(EXCLUDED.mime_type, public.documents.mime_type),
145
+ storage_provider = EXCLUDED.storage_provider,
146
+ storage_bucket = EXCLUDED.storage_bucket,
147
+ storage_path = EXCLUDED.storage_path,
148
+ checksum = EXCLUDED.checksum,
149
+ is_active = true,
150
+ metadata = migration._merge_meta(public.documents.metadata, EXCLUDED.metadata),
151
+ updated_at = now();
152
+
153
+ IF k.pointer_fn IS NOT NULL THEN
154
+ EXECUTE format('SELECT %s($1, $2, $3)', k.pointer_fn)
155
+ USING b.tenant_id, v_doc, p_row || jsonb_build_object('bucket', k.target_bucket);
156
+ END IF;
157
+
158
+ UPDATE migration.ledger
159
+ SET status = 'migrated', target_id = v_doc, checksum = v_checksum,
160
+ cursor = p_source_path, batch_id = p_batch, reason = NULL
161
+ WHERE id = l.id;
162
+ PERFORM migration._count(p_batch, 'migrated');
163
+
164
+ RETURN jsonb_build_object('status', 'migrated', 'target_table', 'public.documents',
165
+ 'target_id', v_doc, 'ledger_id', l.id);
166
+ END $$;
167
+
168
+ COMMENT ON FUNCTION migration._attach_asset_impl(uuid, text, text, jsonb) IS
169
+ 'Um arquivo que atravessou. A identidade no ledger é o OBJETO DE ORIGEM mais o REGISTRO de destino quando há um (064): sem a segunda ponta, um arquivo anexado a dezenove contas dava bytes a uma e deixava dezoito apontando para o V1, com a rodada terminando verde.';
170
+
171
+
172
+ -- ── quem pode executar ────────────────────────────────────────────────────
173
+ --
174
+ -- Função nova nasce executável por PUBLIC, e `anon`/`authenticated` herdam
175
+ -- dali. Estas são de PORTE: SECURITY DEFINER, escrevem dados de qualquer
176
+ -- inquilino. Só o service_role, que é quem roda a onda.
177
+ REVOKE ALL ON FUNCTION migration._attach_asset_impl(uuid, text, text, jsonb) FROM PUBLIC, anon, authenticated;
178
+ GRANT EXECUTE ON FUNCTION migration._attach_asset_impl(uuid, text, text, jsonb) TO service_role;
@@ -0,0 +1,59 @@
1
+ -- 065_apagar_o_sobrevivente_nao_apaga_o_tenant.sql — quem foi fundido volta a
2
+ -- poder ser apagado.
3
+ --
4
+ -- `person_erase()` é o RPC de apagamento da LGPD. Ele faz `DELETE FROM
5
+ -- public.people` e confia nas chaves estrangeiras, como diz o próprio
6
+ -- comentário dele: "What references it with ON DELETE SET NULL keeps its shape
7
+ -- (a sale still happened) and loses the person".
8
+ --
9
+ -- Uma dessas chaves é a de `people` para ela mesma:
10
+ --
11
+ -- people_merged_into_fk
12
+ -- FOREIGN KEY (tenant_id, merged_into_id)
13
+ -- REFERENCES people(tenant_id, id) ON DELETE SET NULL
14
+ --
15
+ -- SET NULL composto SEM lista de colunas anula TODAS as colunas da chave. E
16
+ -- `tenant_id` é NOT NULL. Então apagar alguém que é SOBREVIVENTE de um merge
17
+ -- tenta anular o `tenant_id` da lápide e morre:
18
+ --
19
+ -- 23502: null value in column "tenant_id" of relation "people"
20
+ -- violates not-null constraint
21
+ --
22
+ -- O efeito é preciso e ruim: **um pedido de exclusão de titular falha
23
+ -- exatamente para quem já apareceu duas vezes no cadastro** — que é, por
24
+ -- construção, a pessoa mais provável de ter duas fichas e de pedir para sumir.
25
+ --
26
+ -- Não aparecia porque outra coisa barrava o DELETE antes: quatro chaves do CRM
27
+ -- estavam em NO ACTION e faziam `person_erase` falhar mais cedo, com outro
28
+ -- erro. Corrigidas aquelas (plugin-crm/017), este defeito ficou na frente.
29
+ --
30
+ -- É o MESMO erro que plugin-crm/013 já tinha documentado ao criar
31
+ -- `plg_crm_deals.company_id`, e lá a lista de colunas foi escrita desde o
32
+ -- começo justamente por causa disto. Aqui faltou.
33
+ --
34
+ -- A correção é a lista de colunas — `SET NULL (merged_into_id)`, suportada
35
+ -- desde o Postgres 15. A lápide perde o ponteiro para quem foi apagado e
36
+ -- continua sendo uma linha do seu tenant, que é o que ela sempre deveria ter
37
+ -- sido: registro de que aquelas duas fichas eram a mesma pessoa.
38
+
39
+ DO $$
40
+ BEGIN
41
+ IF EXISTS (
42
+ SELECT 1 FROM pg_constraint
43
+ WHERE conname = 'people_merged_into_fk'
44
+ AND conrelid = 'public.people'::regclass
45
+ -- Só recria se ainda estiver na forma antiga. `confdelsetcols` vazio é
46
+ -- "anula a chave inteira"; preenchido é a lista que queremos.
47
+ AND coalesce(array_length(confdelsetcols, 1), 0) = 0
48
+ ) THEN
49
+ ALTER TABLE public.people DROP CONSTRAINT people_merged_into_fk;
50
+ ALTER TABLE public.people
51
+ ADD CONSTRAINT people_merged_into_fk
52
+ FOREIGN KEY (tenant_id, merged_into_id)
53
+ REFERENCES public.people (tenant_id, id)
54
+ ON DELETE SET NULL (merged_into_id);
55
+ END IF;
56
+ END $$;
57
+
58
+ COMMENT ON CONSTRAINT people_merged_into_fk ON public.people IS
59
+ 'A lápide aponta para o sobrevivente. SET NULL com LISTA DE COLUNAS: sem ela o Postgres anula tambem o tenant_id, que é NOT NULL, e apagar um sobrevivente de merge passa a ser impossível (23502). Ver o cabeçalho de 065.';
@@ -0,0 +1,51 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 066_the_port_machinery_closes_its_doors.sql — the V1 port's new pieces get
3
+ -- the posture the rest of schema `migration` already has.
4
+ --
5
+ -- 045 (the mirror) and 054 (migrated files get their bytes) added two tables
6
+ -- and a set of functions to schema `migration` and gave them none of the
7
+ -- contract 090_migration_ledger/001 grades:
8
+ --
9
+ -- · `migration.asset_kinds` and `migration.retire_policy` had row security
10
+ -- off and no grant to service_role — the one role that is supposed to
11
+ -- write them, and the only one that should.
12
+ -- · every function the two files created kept PostgreSQL's default
13
+ -- `EXECUTE TO PUBLIC`, which on Supabase means `anon`. Among them
14
+ -- `retire_row`, `attach_asset` and `register_asset_kind`: writers of the
15
+ -- port's own ledger, callable by anyone holding the anon key.
16
+ --
17
+ -- 045 and 054 are applied on the cluster and their checksums are in the
18
+ -- ledger, so the fix is this file and not an edit to them.
19
+ --
20
+ -- The function sweep is by schema, not by name: whatever `migration` holds
21
+ -- when this runs loses PUBLIC/anon/authenticated and keeps service_role. The
22
+ -- plugins that add functions to this schema after the spine (plugin-financial)
23
+ -- run the same sweep at the end of their own chain.
24
+ -- ---------------------------------------------------------------------------
25
+
26
+ DO $$
27
+ DECLARE t text;
28
+ BEGIN
29
+ FOREACH t IN ARRAY ARRAY['asset_kinds', 'retire_policy'] LOOP
30
+ IF to_regclass(format('migration.%I', t)) IS NOT NULL THEN
31
+ EXECUTE format('ALTER TABLE migration.%I ENABLE ROW LEVEL SECURITY', t);
32
+ EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON migration.%I TO service_role', t);
33
+ END IF;
34
+ END LOOP;
35
+ END $$;
36
+
37
+ DO $$
38
+ DECLARE f regprocedure;
39
+ BEGIN
40
+ FOR f IN
41
+ SELECT p.oid::regprocedure
42
+ FROM pg_proc p
43
+ JOIN pg_namespace n ON n.oid = p.pronamespace
44
+ WHERE n.nspname = 'migration'
45
+ AND (has_function_privilege('anon', p.oid, 'EXECUTE')
46
+ OR has_function_privilege('authenticated', p.oid, 'EXECUTE'))
47
+ LOOP
48
+ EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC, anon, authenticated', f);
49
+ EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO service_role', f);
50
+ END LOOP;
51
+ END $$;
@@ -0,0 +1,56 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 067_the_erp_keeps_the_front_door.sql — FullControl requires `marketing`
3
+ -- again.
4
+ --
5
+ -- 053 took `marketing` out of the app's `requires` on the reading that "the
6
+ -- acquisition machine is LeadControl's product". But 052 §3 retired the
7
+ -- LeadControl row (`active = false`, `url = NULL`): there is no LeadControl
8
+ -- left to own it. Read together, the two files left campaigns, the funnel and
9
+ -- the landing page with its capture form — the only public door a lead comes
10
+ -- through — without an app.
11
+ --
12
+ -- FullControl is where that work lives now. The ERP is the wider house; the
13
+ -- sales engine built as LeadControl (company model, identity, economic group,
14
+ -- more than one funnel, cadences, the cascading offer) is its commercial
15
+ -- module, and a commercial module whose leads cannot arrive is a list someone
16
+ -- has to type into by hand. So the app mounts plugin-marketing again, and
17
+ -- `requires` has to say so: it is the list a tenant is seeded with when it
18
+ -- ACTIVATES the app, and a plugin the bundle mounts but the tenant never got
19
+ -- renders as a dead rail entry.
20
+ --
21
+ -- `inventory` stays out, as 053 decided.
22
+ --
23
+ -- ── Why a new file ──────────────────────────────────────────────────────────
24
+ --
25
+ -- 052 and 053 are applied and the ledger holds their sha256. And 052's
26
+ -- ON CONFLICT only ever GROWS `requires` on a reapply, so it cannot be the
27
+ -- vehicle either. This is the explicit step, the same way 053 was.
28
+ --
29
+ -- ── Who gets the plugin now ─────────────────────────────────────────────────
30
+ --
31
+ -- Every tenant that carried LeadControl already has `marketing` lit: 011 put
32
+ -- it in `requires` and 011 §3 seeded it, and `app.tenant_plugins` has no
33
+ -- app_id, so 052's rename did not touch it. §2 below only reaches a tenant
34
+ -- that activated `full` between 053 and this file — and, as in 052 §5, the
35
+ -- plan ceiling wins over the default: a plan that does not permit `marketing`
36
+ -- does not get it lit, and the seeding does not fail because of it.
37
+ -- ---------------------------------------------------------------------------
38
+
39
+ -- ── §1 the seeding list ──────────────────────────────────────────────────
40
+ --
41
+ -- Additive on purpose: a later milestone may have appended a plugin after
42
+ -- this file was written, and overwriting the array here would undo it
43
+ -- silently.
44
+ UPDATE app.apps
45
+ SET requires = (SELECT array_agg(DISTINCT p ORDER BY p)
46
+ FROM unnest(requires || ARRAY['marketing']) AS p)
47
+ WHERE id = 'full';
48
+
49
+ -- ── §2 the tenants that activated FullControl without it ───────────────────
50
+ INSERT INTO app.tenant_plugins (tenant_id, plugin_id, facet, status, source)
51
+ SELECT DISTINCT ta.tenant_id, 'marketing', '', 'active', 'default'
52
+ FROM app.tenant_apps ta
53
+ WHERE ta.app_id = 'full'
54
+ AND ta.status = 'active'
55
+ AND public.plan_permits('marketing', '', ta.tenant_id)
56
+ ON CONFLICT (tenant_id, plugin_id, facet) DO NOTHING;
@@ -0,0 +1,55 @@
1
+ -- O ledger encontra a linha pelo caminho que o writer usa.
2
+ --
3
+ -- Todo writer que resolve uma referência pergunta ao ledger a mesma coisa:
4
+ --
5
+ -- SELECT target_id FROM migration.ledger
6
+ -- WHERE tenant_id = … AND source_table = … AND source_numeric_id = …
7
+ -- AND target_table = … AND status = 'migrated'
8
+ --
9
+ -- Nenhum índice cobre essa forma. O que existe —
10
+ -- `ledger_source_numeric_key (source_project, source_table, source_numeric_id, …)`
11
+ -- — começa por `source_project`, que a consulta não informa. Sem a coluna da
12
+ -- frente, o Postgres faz varredura.
13
+ --
14
+ -- Medido em 19/09/2026 no tenant Espaço Facial, UMA busca:
15
+ --
16
+ -- Index Scan using ledger_source_numeric_key (actual time=5416.611..5416.634 rows=0)
17
+ -- Buffers: shared hit=11 read=6891
18
+ -- Execution Time: 5423.387 ms
19
+ --
20
+ -- **5,4 segundos e 55 MB lidos, por linha.** E o custo cresce com a ledger: o
21
+ -- porte do Espaço Facial a levou de ~60 mil para 298 mil linhas numa noite, e
22
+ -- `account_central` sozinha paga essa conta 125.945 vezes. É O(n) por registro
23
+ -- num porte de 1,4 milhão — a razão de a carga não terminar.
24
+ --
25
+ -- Ninguém escreveu essa consulta errada; ela é a pergunta certa. O que faltava
26
+ -- era o índice que a responde.
27
+ --
28
+ -- ── Por que estas colunas, nesta ordem ──────────────────────────────────────
29
+ --
30
+ -- `tenant_id` primeiro porque é o recorte que sempre existe e o que mais
31
+ -- elimina. Depois `source_table` e `source_numeric_id`, que juntos identificam
32
+ -- a linha da origem. `target_table` por último, porque a mesma linha do V1 tem
33
+ -- mais de um destino — uma `invoice` vira pedido E título — e é ela que separa
34
+ -- os dois.
35
+ --
36
+ -- `status` fica FORA: ele muda (`pending` → `migrated` → `retired`), e uma
37
+ -- coluna que muda dentro do índice faz cada transição reescrever a entrada. O
38
+ -- filtro por status sobre as duas ou três linhas que sobram é grátis.
39
+ --
40
+ -- O índice é PARCIAL em `source_numeric_id IS NOT NULL` porque a origem que se
41
+ -- identifica por uuid não usa este caminho — e um índice parcial que cobre o
42
+ -- caso real é menor e mais rápido que um completo que carrega o resto.
43
+ --
44
+ -- Idempotente.
45
+
46
+ CREATE INDEX IF NOT EXISTS ledger_tenant_table_numeric_idx
47
+ ON migration.ledger (tenant_id, source_table, source_numeric_id, target_table)
48
+ WHERE source_numeric_id IS NOT NULL;
49
+
50
+ COMMENT ON INDEX migration.ledger_tenant_table_numeric_idx IS
51
+ 'A forma exata em que os writers consultam o ledger para resolver uma '
52
+ 'referência. Sem ele a busca é varredura: 5,4 s e 55 MB por linha, medido '
53
+ 'com 298 mil linhas de ledger — o que torna impossível um porte de milhão.';
54
+
55
+ ANALYZE migration.ledger;