@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,84 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 088_a_comunidade_se_gere_como_escola.sql
3
+ -- ---------------------------------------------------------------------------
4
+ -- Duas coisas, e a segunda é consequência da primeira.
5
+ --
6
+ -- ── `hide`: o contrário de `under` ─────────────────────────────────────────
7
+ -- Com o preset agrupando em vez de redesenhar (083), quem não fosse nomeado ia
8
+ -- para o FIM em vez de sumir — regra que existe para um plugin novo não
9
+ -- desaparecer em silêncio. Só que no ChefControl isso devolveu cinco linhas
10
+ -- soltas embaixo de sete rubricas curadas: Agenda, CRM, Marketing, Automações,
11
+ -- Comunicação.
12
+ --
13
+ -- Não era ruído: no vocabulário do ChefControl a Agenda É "Reservas" e o CRM É
14
+ -- "Clientes". Mostrar os dois nomes é mostrar o mesmo módulo duas vezes. O
15
+ -- resto-saas resolve isso com `hideNav()` em código; faltava a forma em dado.
16
+ --
17
+ -- `hide` são prefixos de rota que este ofício NÃO põe no rail. Aplicado ANTES
18
+ -- das rubricas: uma rota calada não pode ser reivindicada nem sobrar no fim.
19
+ --
20
+ -- ── A escola passa a servir COMUNIDADE ─────────────────────────────────────
21
+ -- IAM Club estava na bilheteria e as cinco contas dela — IAM, Great DJs, Crema
22
+ -- Club, Airfryer Collective, Aldeia Zen — são a mesma forma: programa,
23
+ -- encontro, membro, ingresso. Um clube se gere como comunidade, não como
24
+ -- portaria de evento avulso.
25
+ --
26
+ -- `orders` entra na oferta porque O INGRESSO É UM PEDIDO. Não há plugin de
27
+ -- `ticketing`, nem de `events`, nem de `community` — o que existe é `agenda`
28
+ -- (o encontro), `orders` (o ingresso), `conversations` + `crm` (a comunidade).
29
+ -- As rubricas usam as palavras do ramo; os módulos são os que existem.
30
+ --
31
+ -- O que isto NÃO decide: se "CommunityControl" merece ser um aplicativo
32
+ -- próprio. Hoje cinco contas de comunidade rodam a escola porque a forma serve;
33
+ -- o dia em que o vocabulário "aluno/turma" atrapalhar mais do que ajudar é o
34
+ -- dia de separar, e aí é uma linha em `app.apps`.
35
+ -- ---------------------------------------------------------------------------
36
+
37
+ UPDATE app.apps SET preset = preset
38
+ || '{"hide":["/agenda","/sales","/marketing","/automations","/communication"]}'::jsonb
39
+ WHERE id = 'resto';
40
+
41
+ UPDATE app.apps SET preset = preset || $$
42
+ {
43
+ "modules": ["dashboard","admin","notifications","conversations","courses","agenda","orders",
44
+ "crm","financial","marketing","automations","reports","custom_forms"],
45
+ "rail": ["dashboard", "conversations",
46
+ { "label": "Programa", "icon": "GraduationCap", "under": ["/courses"] },
47
+ { "label": "Encontros", "icon": "CalendarDays", "under": ["/agenda", "/orders"] },
48
+ "crm", "marketing", "financial", "reports"],
49
+ "hide": ["/automations", "/communication"]
50
+ }
51
+ $$::jsonb WHERE id = 'school';
52
+
53
+ -- IAM Club sai da bilheteria.
54
+ UPDATE app.tenant_apps SET app_id = 'school', updated_at = now()
55
+ WHERE tenant_id = (SELECT id FROM public.tenants WHERE slug = 'iam-club') AND app_id = 'ticket';
56
+ UPDATE public.tenants SET vertical_id = 'education', updated_at = now() WHERE slug = 'iam-club';
57
+ DELETE FROM app.tenant_plugins
58
+ WHERE tenant_id = (SELECT id FROM public.tenants WHERE slug = 'iam-club')
59
+ AND plugin_id IN ('menu','inventory');
60
+
61
+ -- O que uma comunidade liga.
62
+ INSERT INTO app.tenant_plugins (tenant_id, plugin_id, facet, status, source)
63
+ SELECT t.id, p.plugin_id, '', 'active', 'manual'
64
+ FROM public.tenants t
65
+ CROSS JOIN (VALUES ('courses'),('agenda'),('orders'),('crm'),('conversations'),
66
+ ('marketing'),('financial'),('reports')) AS p(plugin_id)
67
+ WHERE t.slug IN ('iam-club','crema-club','airfryer-collective','aldeia-zen','great-djs')
68
+ ON CONFLICT (tenant_id, plugin_id, facet) DO UPDATE SET status = 'active', updated_at = now();
69
+
70
+ -- A invariante da 087, de novo: oferecer mais do que se exige é o normal; o
71
+ -- contrário nunca é.
72
+ DO $$
73
+ DECLARE v_bad text;
74
+ BEGIN
75
+ SELECT string_agg(a.id || ' exige ' || r.m || ' e não oferece', '; ') INTO v_bad
76
+ FROM app.apps a CROSS JOIN LATERAL unnest(a.requires) AS r(m)
77
+ WHERE a.active
78
+ -- Ver a nota na 101: a verificação nomeia a divergência, não derruba a
79
+ -- cadeia por causa de uma forma que ela não esperava.
80
+ AND jsonb_typeof(a.preset->'modules') = 'array'
81
+ AND jsonb_array_length(a.preset->'modules') > 0
82
+ AND NOT (a.preset->'modules' ? r.m);
83
+ IF v_bad IS NOT NULL THEN RAISE EXCEPTION 'requires fora da oferta: %', v_bad; END IF;
84
+ END $$;
@@ -0,0 +1,53 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 089_o_logo_da_coluna_e_projecao_do_documento.sql
3
+ -- ---------------------------------------------------------------------------
4
+ -- Great DJs e IAM Club tinham marca e apareciam SEM marca no seletor de contas.
5
+ -- O motivo é velho conhecido nesta base: duas grafias do mesmo fato.
6
+ --
7
+ -- o seletor lê v_team_members → tenants.logo_url (coluna, de 2024)
8
+ -- a marca mora em settings.branding.identity.logoUrl (documento, 075)
9
+ --
10
+ -- Quem escreve a marca pela porta nova não toca na coluna velha, e a lista
11
+ -- mostra iniciais. Sincronizar à mão criaria a terceira oportunidade de
12
+ -- divergir — é o mesmo erro que a 074 já corrigiu uma vez para `primary_color`.
13
+ --
14
+ -- A coluna vira DERIVADA: um escritor (o documento), um gatilho, e todo leitor
15
+ -- antigo segue funcionando sem saber que mudou de fonte.
16
+ --
17
+ -- `update OF settings` e não `update`: o gatilho existe para acompanhar a
18
+ -- marca, e disparar em toda escrita da tabela o faria reescrever a coluna em
19
+ -- cada troca de plano ou de nome.
20
+ --
21
+ -- ── E por que `markUrl` vem ANTES de `logoUrl` ─────────────────────────────
22
+ -- Tudo que lê esta coluna desenha num SLOT QUADRADO: o seletor do rail, o
23
+ -- escolhedor pós-login, a tela de troca — todos `OrgMark`, de h-5 a h-9. O
24
+ -- Espaço Facial tem as duas artes, e a coluna pegou o wordmark horizontal: no
25
+ -- quadrado ele virou "…aço|", um pedaço do meio da palavra. `markUrl` existe
26
+ -- exatamente para esse lugar.
27
+ -- ---------------------------------------------------------------------------
28
+
29
+ CREATE OR REPLACE FUNCTION app.sync_tenant_logo_url() RETURNS trigger
30
+ LANGUAGE plpgsql AS $$
31
+ BEGIN
32
+ NEW.logo_url := coalesce(
33
+ NEW.settings #>> '{branding,identity,markUrl}', -- a arte QUADRADA
34
+ NEW.settings #>> '{branding,identity,logoUrl}', -- o wordmark, se for só o que há
35
+ NEW.settings #>> '{branding,logo_url}', -- a grafia anterior à 074
36
+ NEW.logo_url -- ninguém disse nada: mantém
37
+ );
38
+ RETURN NEW;
39
+ END $$;
40
+
41
+ DROP TRIGGER IF EXISTS tenants_sync_logo_url ON public.tenants;
42
+ CREATE TRIGGER tenants_sync_logo_url
43
+ BEFORE INSERT OR UPDATE OF settings ON public.tenants
44
+ FOR EACH ROW EXECUTE FUNCTION app.sync_tenant_logo_url();
45
+
46
+ COMMENT ON COLUMN public.tenants.logo_url IS
47
+ 'DERIVADA de settings.branding.identity.logoUrl pelo gatilho tenants_sync_logo_url (089). Leitores antigos — v_team_members, o seletor de contas — continuam lendo daqui; quem ESCREVE escreve no documento da marca.';
48
+
49
+ -- Backfill. `set settings = settings` e não `set updated_at = now()`: o gatilho
50
+ -- escuta a COLUNA settings, e tocar outra não o acorda.
51
+ UPDATE public.tenants SET settings = settings
52
+ WHERE settings #>> '{branding,identity,markUrl}' IS NOT NULL
53
+ OR settings #>> '{branding,identity,logoUrl}' IS NOT NULL;
@@ -0,0 +1,78 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 090_o_modulo_do_oficio_vizinho_tem_onde_ser_visto.sql
3
+ -- ---------------------------------------------------------------------------
4
+ -- A 079 deu ao ofício uma OFERTA (`app.apps.preset.modules`), e a tela de
5
+ -- Módulos passou a mostrar só o que o ofício DESTA conta oferece. Isso está
6
+ -- certo como padrão e errado como limite: um salão que resolveu vender online
7
+ -- não tem, em lugar nenhum do produto, onde descobrir que `shop` existe. O
8
+ -- catálogo acabou onde o ofício começou.
9
+ --
10
+ -- O que falta não é dado novo — é uma PORTA para o dado que já está lá. Sete
11
+ -- linhas de `app.apps` dizem, cada uma, um ofício e o que ele oferece, e
12
+ -- nenhum cliente consegue lê-las senão para a própria conta: `app_config()`
13
+ -- só devolve os aplicativos que estão em `app.tenant_apps`.
14
+ --
15
+ -- ── Por que uma função nova, e não mais uma chave no app_config() ──────────
16
+ -- `app_config()` é a chamada do BOOT — ela existe para a shell conseguir
17
+ -- desenhar a primeira tela. O catálogo não participa disso: quem o lê é uma
18
+ -- aba de Configurações, aberta por alguém que já entrou. Pendurá-lo ali é
19
+ -- pagar o payload em todo login de todo usuário por uma tela que a maioria
20
+ -- nunca abre.
21
+ --
22
+ -- E há o motivo prosaico: `normalizeAppConfig`, no @fayz-ai/core publicado,
23
+ -- reconstrói a resposta chave a chave e descarta o que não conhece. Uma chave
24
+ -- nova no `app_config()` só chegaria na tela depois de uma release do pacote —
25
+ -- a tela ficaria esperando o trem errado.
26
+ --
27
+ -- ── Por que `mine` vem do servidor ─────────────────────────────────────────
28
+ -- Quem lê precisa saber qual das rubricas é a DELE: é a que abre expandida, e
29
+ -- é a diferença entre "o seu ofício, e o que mais existe" e uma lista de sete
30
+ -- ofícios anônimos. A resposta depende da conta, e o servidor já sabe quem
31
+ -- está perguntando. Devolver o catálogo cru e mandar o cliente cruzá-lo com
32
+ -- `app_config().apps` seria criar a segunda grafia do mesmo fato — que é
33
+ -- exatamente o que a 079 passou a migration inteira desfazendo.
34
+ --
35
+ -- Nada de permissão nova: o catálogo é o que o PRODUTO oferece, igual para
36
+ -- todo mundo. O que a conta tem continua em `app.tenant_plugins`, e quem liga
37
+ -- ou desliga continua sendo `tenant_plugin_set`, que já exige `settings.manage`
38
+ -- e já impõe o teto do plano.
39
+ -- ---------------------------------------------------------------------------
40
+
41
+ CREATE OR REPLACE FUNCTION public.app_catalog()
42
+ RETURNS jsonb
43
+ LANGUAGE sql
44
+ STABLE SECURITY DEFINER
45
+ SET search_path TO ''
46
+ AS $function$
47
+ SELECT coalesce(jsonb_agg(x ORDER BY x ->> 'name'), '[]'::jsonb)
48
+ FROM (
49
+ SELECT jsonb_build_object(
50
+ 'id', a.id,
51
+ 'name', a.name,
52
+ 'vertical_id', a.vertical_id,
53
+ 'modules', coalesce(a.preset -> 'modules', '[]'::jsonb),
54
+ 'mine', EXISTS (
55
+ SELECT 1 FROM app.tenant_apps ta
56
+ WHERE ta.app_id = a.id
57
+ AND ta.status = 'active'
58
+ AND ta.tenant_id = app.current_tenant_id())
59
+ ) AS x
60
+ FROM app.apps a
61
+ -- Sem ofício não é ofício: o control-admin é a casa, não uma rubrica.
62
+ -- Sem oferta não há o que listar — a linha existiria como categoria
63
+ -- vazia, que é uma pergunta sem resposta.
64
+ WHERE a.active
65
+ AND a.vertical_id IS NOT NULL
66
+ -- `jsonb_typeof` antes: um ofício cujo `modules` não for array ainda
67
+ -- não tem oferta, e uma função STABLE não é lugar de levantar exceção.
68
+ AND jsonb_typeof(a.preset -> 'modules') = 'array'
69
+ AND jsonb_array_length(a.preset -> 'modules') > 0
70
+ ) c;
71
+ $function$;
72
+
73
+ REVOKE ALL ON FUNCTION public.app_catalog() FROM public;
74
+ GRANT EXECUTE ON FUNCTION public.app_catalog() TO authenticated;
75
+ GRANT EXECUTE ON FUNCTION public.app_catalog() TO service_role;
76
+
77
+ COMMENT ON FUNCTION public.app_catalog() IS
78
+ 'O catálogo por OFÍCIO (090): cada aplicativo ativo com id, nome, vertical_id, a oferta dele (preset.modules) e `mine` — se esta conta o roda. Lida pelo marketplace de Módulos para agrupar o que existe por categoria de ofício; o que a conta TEM continua em app_config().plugins.';
@@ -0,0 +1,67 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 091_a_conta_diz_o_que_nao_usa_e_a_marca_pinta_a_casa.sql
3
+ -- ---------------------------------------------------------------------------
4
+ -- Eu semeei as cinco contas de comunidade com os mesmos oito módulos — exatamente
5
+ -- o que esta arquitetura existe para não fazer. O founder corrigiu olhando a
6
+ -- tela: Crema Club não vende curso; Airfryer não tem curso nem conversas.
7
+ --
8
+ -- Ditas, não omitidas. Uma linha `disabled` é a conta AFIRMANDO que não usa o
9
+ -- módulo; omitir cairia no padrão do manifesto, que é ligado. E a rubrica some
10
+ -- sozinha: "Programa" não desenha numa casa sem curso, porque uma rubrica sem
11
+ -- filho ativo não é desenhada (ver rail-preset).
12
+ -- ---------------------------------------------------------------------------
13
+
14
+ INSERT INTO app.tenant_plugins (tenant_id, plugin_id, facet, status, source)
15
+ SELECT t.id, p.plugin_id, '', 'disabled', 'manual'
16
+ FROM public.tenants t
17
+ JOIN (VALUES
18
+ ('crema-club', 'courses'),
19
+ ('airfryer-collective', 'courses'),
20
+ ('airfryer-collective', 'conversations')
21
+ ) AS p(slug, plugin_id) ON p.slug = t.slug
22
+ ON CONFLICT (tenant_id, plugin_id, facet) DO UPDATE SET status = 'disabled', updated_at = now();
23
+
24
+ -- ── Espaço Renova: canvas marrom, conteúdo claro ───────────────────────────
25
+ -- Quatro tentativas, e as três primeiras erraram no MESMO ponto: eu escurecia
26
+ -- `surface.background` esperando ver o papel de fundo mudar, e ele não mudava.
27
+ --
28
+ -- O motivo está no shell, não na marca. Com a moldura ligada, o que aparece
29
+ -- ATRÁS dela é `bg-sidebar` — `AppShell` pinta o vão com a cor do rail
30
+ -- (`frame && 'md:bg-sidebar'`). Então, para uma marca:
31
+ --
32
+ -- rail.background é o rail E o canvas atrás da moldura
33
+ -- surface.background é a superfície DENTRO da moldura, onde mora o texto
34
+ --
35
+ -- Com isso o pedido é direto: o marrom vai no rail, o conteúdo volta a ser
36
+ -- claro com tinta escura, e nenhum texto fica sobre um fundo da própria cor.
37
+ -- A primária é o marrom AMOSTRADO da arte oficial do cliente (#935427), não uma
38
+ -- interpretação minha do logo. O creme da arte (#FFFAEA) ficou só no ladrilho:
39
+ -- ele é a cor do desenho, não a da casa.
40
+ --
41
+ -- E `surface` SAI do documento em vez de receber um branco escolhido por mim.
42
+ -- Sem a chave, a marca não opina sobre a superfície e o app usa o tema dele —
43
+ -- que é diferente de eu escrever outro hex claro, porque qualquer hex seria
44
+ -- mais uma opinião, e o pedido era não ter opinião nenhuma ali.
45
+ --
46
+ -- ── Armadilha que me custou três recargas ─────────────────────────────────
47
+ -- O tema lê a marca do SNAPSHOT DA ORGANIZAÇÃO (`fayz:org-snapshot`), não do
48
+ -- snapshot de configuração (`fayz:app-config:<tenant>`). Limpar um não limpa o
49
+ -- outro, e eu fiquei vendo o creme antigo depois de já o ter removido do banco.
50
+ -- Mesma família do TTL de 5 min que já escondeu mudança de trial antes.
51
+ UPDATE public.tenants SET settings = jsonb_set(settings, '{branding,tokens}', $$
52
+ {
53
+ "color": { "primary": "#935427", "accent": "#B0743F" },
54
+ "rail": { "background": "#2A201A", "foreground": "#F7F2EC", "border": "#3E3125",
55
+ "accent": "#3E3125", "accentForeground": "#D9A87C", "muted": "#B3A192" },
56
+ "radius": "0.75rem"
57
+ }
58
+ $$::jsonb), updated_at = now()
59
+ WHERE name = 'Espaco Renova Rio';
60
+
61
+ -- O ladrilho quadrado, agora sobre o marrom da marca em vez de preto.
62
+ UPDATE public.tenants t SET settings = jsonb_set(t.settings, '{branding,identity}',
63
+ coalesce(t.settings #> '{branding,identity}', '{}'::jsonb) || jsonb_build_object(
64
+ 'logoUrl', 'https://klvfxzreepavcpyjiwla.supabase.co/storage/v1/object/public/avatars/947b0ed0-82ad-4b99-88d0-b503a7ad09c2/branding/tile.png',
65
+ 'markUrl', 'https://klvfxzreepavcpyjiwla.supabase.co/storage/v1/object/public/avatars/947b0ed0-82ad-4b99-88d0-b503a7ad09c2/branding/tile.png')
66
+ ), updated_at = now()
67
+ WHERE t.name = 'Espaco Renova Rio';
@@ -0,0 +1,60 @@
1
+ -- A tabela de ramos aprende o vocabulário que a conta passou a falar
2
+ --
3
+ -- A 093 fechou `tenants.vertical_id` em sete nomes e deixou órfão um caminho
4
+ -- que ninguém olhou: o gatilho `tenants_apply_vertical_defaults`, que roda a
5
+ -- cada INSERT em `public.tenants` e cruza `app.vertical_defaults` POR ESSE
6
+ -- MESMO NOME. A tabela de ramos fala o vocabulário antigo:
7
+ --
8
+ -- app.vertical_defaults agency, ecommerce, restaurant, salon, school
9
+ -- tenants.vertical_id food, beauty, commerce, education, events,
10
+ -- accounting, generic
11
+ --
12
+ -- A interseção é VAZIA. Desde a 093, toda conta nova nasce sem módulo nenhum
13
+ -- semeado — e em silêncio, porque o gatilho engole a falha de propósito (uma
14
+ -- conta sem módulos se conserta num clique; uma conta que não foi criada é um
15
+ -- cadastro perdido). O sintoma some no warning e a conta chega vazia.
16
+ --
17
+ -- Isto não reabre a decisão da 093. `preset.modules` cruzado com
18
+ -- `tenant_plugins` continua sendo quem decide o que aparece; esta tabela é só
19
+ -- o ponto de partida de quem acabou de nascer. O conserto é traduzir as
20
+ -- dezesseis linhas para o vocabulário fechado, não criar uma oitava grafia.
21
+ --
22
+ -- restaurant -> food salon -> beauty school -> education
23
+ -- ecommerce -> commerce agency -> generic
24
+ --
25
+ -- `events` e `accounting` ficam sem linha, e isso é a resposta honesta: a
26
+ -- bilheteria e a contabilidade nunca tiveram default nenhum aqui, e inventar
27
+ -- um agora seria adivinhar.
28
+ --
29
+ -- Idempotente: insere o traduzido e só então apaga o original.
30
+
31
+ INSERT INTO app.vertical_defaults (vertical_id, plugin_id, facet, note)
32
+ SELECT v.novo, d.plugin_id, d.facet, d.note
33
+ FROM app.vertical_defaults d
34
+ JOIN (VALUES
35
+ ('restaurant', 'food'),
36
+ ('salon', 'beauty'),
37
+ ('ecommerce', 'commerce'),
38
+ ('school', 'education'),
39
+ ('agency', 'generic')
40
+ ) AS v(velho, novo) ON v.velho = d.vertical_id
41
+ ON CONFLICT (vertical_id, plugin_id, facet) DO NOTHING;
42
+
43
+ DELETE FROM app.vertical_defaults
44
+ WHERE vertical_id IN ('restaurant','salon','ecommerce','school','agency');
45
+
46
+ -- O que sobrou fala o vocabulário fechado, e nada mais.
47
+ DO $$
48
+ DECLARE v_fora text[];
49
+ BEGIN
50
+ SELECT array_agg(DISTINCT vertical_id) INTO v_fora
51
+ FROM app.vertical_defaults
52
+ WHERE vertical_id NOT IN
53
+ ('food','beauty','commerce','education','events','accounting','generic');
54
+ IF v_fora IS NOT NULL THEN
55
+ RAISE EXCEPTION 'app.vertical_defaults ainda fala fora do vocabulario: %', v_fora;
56
+ END IF;
57
+ END $$;
58
+
59
+ COMMENT ON TABLE app.vertical_defaults IS
60
+ 'O que uma conta NOVA daquele ramo já nasce com. Mesmo vocabulário de tenants.vertical_id (093). Ponto de partida, não portão: quem decide o que aparece é app.apps.preset.modules cruzado com app.tenant_plugins.';
@@ -0,0 +1,50 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 108_o_erp_generico_nao_vende_por_pedido.sql
3
+ -- ---------------------------------------------------------------------------
4
+ -- O ControlGroup é o ERP genérico: ele roda o `full` e o `fiscal`, e a empresa
5
+ -- atendida por ele não vende por PEDIDO. A venda dela é orçamento, contrato e
6
+ -- título — o funil do CRM e o razão do financeiro. "Pedidos" no rail é uma
7
+ -- tela que ninguém daquela casa abre, e que ainda promete um fluxo de balcão
8
+ -- que não existe ali.
9
+ --
10
+ -- ── Por que a linha está lá, se o ofício não a oferece ─────────────────────
11
+ -- `app.apps.preset.modules` do `full` e do `fiscal` não citam `orders`. O
12
+ -- portão da oferta (resolvePluginRuntime) pularia o plugin por isso — MENOS
13
+ -- quando existe uma linha `active` explícita, que é o caso do módulo vendido à
14
+ -- parte e vence a oferta. É uma dessas linhas, resíduo de quando a conta
15
+ -- recebeu aplicativo que não era o dela, que está desenhando "Pedidos".
16
+ --
17
+ -- ── Por que aqui, e não no preset do ofício ────────────────────────────────
18
+ -- Tirar `orders` de `app.apps.preset` tiraria Pedidos de TODA conta que roda o
19
+ -- mesmo aplicativo. O que muda é o que ESTA conta usa, e isso mora na camada
20
+ -- do meio: o ofício propõe, a conta dispõe.
21
+ --
22
+ -- ── Dita, não omitida ──────────────────────────────────────────────────────
23
+ -- Apagar a linha devolveria o plugin ao padrão do manifesto, que é
24
+ -- `defaultEnabled: true`. O runtime é aditivo: silêncio é "sim". Só um
25
+ -- `disabled` explícito é a conta AFIRMANDO que não usa o módulo.
26
+ --
27
+ -- ── É `orders`, não `shop` ─────────────────────────────────────────────────
28
+ -- Os dois plugins desenham `/orders` e os dois chamam a linha de "Pedidos", e
29
+ -- desligar o errado não mudaria nada na tela. Quem desenha aqui é `orders`:
30
+ -- `shop` contribui QUATRO rubricas juntas — Pedidos, Produtos, Conteúdo e
31
+ -- Descontos —, e é assim que ele aparece na PULSE. O rail do ControlGroup não
32
+ -- tem vitrine nenhuma; `orders` contribui uma linha só, e é a linha que sobra.
33
+ --
34
+ -- O Cardápio vai junto, e de propósito: `orders` declara `dependencies:
35
+ -- ['menu']` e o runtime acende a dependência por arrasto. Sem quem o puxasse,
36
+ -- `menu` volta a ficar fora da oferta e sai do rail sozinho.
37
+ --
38
+ -- ── Escrito na tabela, e não por `tenant_plugin_set` ───────────────────────
39
+ -- A porta é a certa para o produto: ela exige `settings.manage` e impõe o teto
40
+ -- do plano. Uma migration roda sem usuário — não há a quem pedir permissão —,
41
+ -- então ela escreve a mesma linha que a porta escreveria, como fez a 105.
42
+ -- ---------------------------------------------------------------------------
43
+
44
+ -- Por `slug`: num banco vazio nenhuma linha casa e a migration não faz nada,
45
+ -- em vez de estourar procurando uma conta que ainda não nasceu.
46
+ INSERT INTO app.tenant_plugins (tenant_id, plugin_id, facet, status, source)
47
+ SELECT t.id, 'orders', '', 'disabled', 'manual'
48
+ FROM public.tenants t
49
+ WHERE t.slug = 'controlgroup'
50
+ ON CONFLICT (tenant_id, plugin_id, facet) DO UPDATE SET status = 'disabled', updated_at = now();