@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
@@ -52,7 +52,11 @@
52
52
  -- publishes these apps elsewhere updates the row; nothing in code hardcodes a
53
53
  -- port.
54
54
  INSERT INTO app.apps (id, name, icon, accent_color, url, requires, serves_verticals) VALUES
55
- ('crm', 'LeadControl', 'Target', '#2563EB', 'http://localhost:5307',
55
+ -- Megafone e não alvo: o alvo diz PONTARIA — a casa mira e acerta uma pessoa.
56
+ -- O megafone diz ALCANCE: a casa fala e muita gente ouve. É o que este app
57
+ -- faz — origem, segmento, campanha e porta pública existem para a casa ser
58
+ -- OUVIDA, e o lead é a resposta de quem ouviu.
59
+ ('crm', 'LeadControl', 'Megaphone', '#2563EB', 'http://localhost:5307',
56
60
  ARRAY['crm', 'conversations', 'marketing', 'automations'],
57
61
  ARRAY[]::text[])
58
62
  ON CONFLICT (id) DO UPDATE
@@ -0,0 +1,167 @@
1
+ -- 029_o_administrador_enxerga_o_que_existe.sql
2
+ --
3
+ -- O rail do LeadControl abria com duas linhas — Painel e Análises — enquanto o
4
+ -- painel logo abaixo mostrava "Total de Leads: 20". Os dados estavam lá, os
5
+ -- plugins montados, o console limpo. Só o MENU estava vazio.
6
+ --
7
+ -- A causa não é do app. É esta assimetria, no `app.seed_role_template`:
8
+ --
9
+ -- -- o Owner recebe o CATÁLOGO, e o catálogo cresce sozinho
10
+ -- INSERT INTO app.role_permissions
11
+ -- SELECT p_tenant, v_owner, p.key FROM app.permissions p;
12
+ --
13
+ -- -- todos os outros recebem uma LISTA, escrita à mão, que não cresce
14
+ -- INSERT INTO app.role_permissions
15
+ -- SELECT p_tenant, r.id, t.permission FROM app.role_templates t ...
16
+ --
17
+ -- Um plugin publicado depois que aquela lista foi escrita registra as suas
18
+ -- permissões em `app.permissions` e, a partir daí, chega ao Owner de graça e
19
+ -- **a mais ninguém, nunca**. Não há aviso: `AdminShell` esconde a linha do rail
20
+ -- quando a negação é por PAPEL (negação por PLANO manteria a linha com a coroa),
21
+ -- então o sintoma é um menu curto, que parece escolha de produto.
22
+ --
23
+ -- Medido no cluster antes desta migration, em 10 tenants:
24
+ --
25
+ -- Administrator/Manager que enxergam crm.* 0 de 10
26
+ -- Administrator/Manager que enxergam conversations.* 0 de 10
27
+ --
28
+ -- E não são só esses dois. Comparando Owner com Administrator num tenant real,
29
+ -- faltavam ao Administrator DEZENOVE features: appointments, automations, blog,
30
+ -- config, conversations, courses, crm, custom_forms, dashboard, marketing,
31
+ -- menu, reputation, sales, scribe, shop, sites, tables e mais. Ou seja: quase
32
+ -- todo plugin que o SDK ganhou desde que a lista foi escrita.
33
+ --
34
+ -- Por isso a correção NÃO é acrescentar quatro linhas à lista. Acrescentar
35
+ -- quatro linhas conserta a queixa de hoje e reprograma a mesma queixa para o
36
+ -- próximo plugin. A correção é tirar o Administrador da lista e colocá-lo no
37
+ -- CATÁLOGO, como o Owner — menos o que é do dono.
38
+ --
39
+ -- O que continua sendo lista, de propósito: manager, staff e viewer. Esses são
40
+ -- recortes deliberados — um "Viewer" que enxerga tudo o que existe deixou de
41
+ -- ser viewer. Eles vão continuar precisando de decisão a cada plugin novo; a
42
+ -- diferença é que agora ALGUÉM abaixo do dono enxerga o plugin no dia em que
43
+ -- ele registra as permissões, e o buraco aparece na hora em vez de meses depois.
44
+
45
+ -- ── o que é do dono ────────────────────────────────────────────────────────
46
+ --
47
+ -- Precisa ser uma marca no catálogo, não uma lista no meio de uma função: quem
48
+ -- registra uma permissão nova é o plugin, e é ele que sabe se ela é do dono.
49
+ ALTER TABLE app.permissions
50
+ ADD COLUMN IF NOT EXISTS owner_only boolean DEFAULT false NOT NULL;
51
+
52
+ COMMENT ON COLUMN app.permissions.owner_only IS
53
+ 'Permissão que só o Owner recebe. O Administrador recebe todo o resto do catálogo automaticamente (029).';
54
+
55
+ -- Assinatura, cartão e cancelamento são do dono. E `admin.*` é o painel da
56
+ -- PLATAFORMA — não é papel de tenant nenhum, nem do dono da loja.
57
+ --
58
+ -- `owner_only` governa o que se CONCEDE daqui para frente; não revoga o que já
59
+ -- foi concedido. Um Administrador que hoje tem `billing.read` porque a lista
60
+ -- antiga o deu continua tendo: tirar permissão de quem já trabalha com ela é
61
+ -- uma quebra, e esta migration veio consertar um menu vazio, não estreitar
62
+ -- ninguém.
63
+ UPDATE app.permissions SET owner_only = true
64
+ WHERE key ~ '^(billing|admin)\.' AND owner_only = false;
65
+
66
+ -- ── o administrador passa a ler o catálogo ─────────────────────────────────
67
+ CREATE OR REPLACE FUNCTION app.seed_role_template(p_tenant uuid, p_template text DEFAULT 'generic'::text) RETURNS uuid
68
+ LANGUAGE plpgsql SECURITY DEFINER
69
+ SET search_path TO ''
70
+ AS $$
71
+ DECLARE v_owner uuid;
72
+ BEGIN
73
+ IF NOT EXISTS (SELECT 1 FROM app.role_templates WHERE template = p_template) THEN
74
+ RAISE EXCEPTION 'seed_role_template: unknown template %', p_template;
75
+ END IF;
76
+
77
+ INSERT INTO app.roles (tenant_id, key, name, is_system)
78
+ VALUES (p_tenant, 'owner', 'Owner', true)
79
+ ON CONFLICT (tenant_id, key) DO NOTHING;
80
+ SELECT id INTO v_owner FROM app.roles WHERE tenant_id = p_tenant AND key = 'owner';
81
+
82
+ INSERT INTO app.role_permissions (tenant_id, role_id, permission)
83
+ SELECT p_tenant, v_owner, p.key FROM app.permissions p
84
+ ON CONFLICT DO NOTHING;
85
+
86
+ INSERT INTO app.roles (tenant_id, key, name, is_system)
87
+ SELECT DISTINCT ON (t.role_key) p_tenant, t.role_key, t.role_name, true
88
+ FROM app.role_templates t WHERE t.template = p_template
89
+ ORDER BY t.role_key
90
+ ON CONFLICT (tenant_id, key) DO NOTHING;
91
+
92
+ INSERT INTO app.role_permissions (tenant_id, role_id, permission)
93
+ SELECT p_tenant, r.id, t.permission
94
+ FROM app.role_templates t
95
+ JOIN app.roles r ON r.tenant_id = p_tenant AND r.key = t.role_key
96
+ WHERE t.template = p_template
97
+ ON CONFLICT DO NOTHING;
98
+
99
+ -- A linha que faltava. O papel `admin` ganha o catálogo menos o que é do
100
+ -- dono, DEPOIS da lista — o que a lista já deu continua dado, e o que ela
101
+ -- esqueceu deixa de importar. Só age se o template realmente tiver um papel
102
+ -- `admin`: um template sem administrador não passa a ter um por isto.
103
+ INSERT INTO app.role_permissions (tenant_id, role_id, permission)
104
+ SELECT p_tenant, r.id, p.key
105
+ FROM app.roles r
106
+ CROSS JOIN app.permissions p
107
+ WHERE r.tenant_id = p_tenant AND r.key = 'admin' AND p.owner_only = false
108
+ ON CONFLICT DO NOTHING;
109
+
110
+ RETURN v_owner;
111
+ END $$;
112
+
113
+ COMMENT ON FUNCTION app.seed_role_template(uuid, text) IS
114
+ 'Semeia os papéis do template. Owner e Administrador leem o CATÁLOGO (o admin sem o que é do dono); manager/staff/viewer seguem sendo recortes deliberados (029).';
115
+
116
+ -- ── as casas que já nasceram cegas ─────────────────────────────────────────
117
+ --
118
+ -- A função acima conserta quem nascer daqui para frente. Os dez tenants que já
119
+ -- existem precisam da mesma conta, aplicada agora.
120
+ DO $$
121
+ DECLARE v_admin integer; v_mgr integer;
122
+ BEGIN
123
+ INSERT INTO app.role_permissions (tenant_id, role_id, permission)
124
+ SELECT r.tenant_id, r.id, p.key
125
+ FROM app.roles r
126
+ CROSS JOIN app.permissions p
127
+ WHERE r.key = 'admin' AND p.owner_only = false
128
+ ON CONFLICT DO NOTHING;
129
+ GET DIAGNOSTICS v_admin = ROW_COUNT;
130
+
131
+ -- O gerente é recorte, não catálogo: ele opera a casa, não a administra.
132
+ -- Recebe ler/criar/editar do que o LeadControl põe no rail, e não recebe
133
+ -- apagar nem gerir — apagar conversa e gerir integração continuam acima
134
+ -- dele. `regexp_replace` pega o último segmento porque as chaves têm dois
135
+ -- formatos no catálogo: `sales.read` e `crm.leads.read`.
136
+ INSERT INTO app.role_permissions (tenant_id, role_id, permission)
137
+ SELECT r.tenant_id, r.id, p.key
138
+ FROM app.roles r
139
+ CROSS JOIN app.permissions p
140
+ WHERE r.key = 'manager'
141
+ AND p.owner_only = false
142
+ AND p.key ~ '^(crm|sales|conversations|marketing|automations)\.'
143
+ AND regexp_replace(p.key, '^.*\.', '') IN ('read', 'create', 'edit')
144
+ ON CONFLICT DO NOTHING;
145
+ GET DIAGNOSTICS v_mgr = ROW_COUNT;
146
+
147
+ RAISE NOTICE '029: % permissões devolvidas ao Administrador, % ao Gerente', v_admin, v_mgr;
148
+ END $$;
149
+
150
+ -- E o mesmo recorte entra no TEMPLATE do gerente, para o próximo tenant nascer
151
+ -- com ele. O administrador não precisa de linha aqui: ele passou a ler o
152
+ -- catálogo, e uma linha de template para ele seria a lista voltando pela porta
153
+ -- dos fundos.
154
+ INSERT INTO app.role_templates (template, role_key, role_name, sort_order, permission)
155
+ SELECT m.template, 'manager', m.role_name, m.sort_order, p.key
156
+ FROM (SELECT DISTINCT ON (template) template, role_name, sort_order
157
+ FROM app.role_templates WHERE role_key = 'manager'
158
+ ORDER BY template) m
159
+ CROSS JOIN app.permissions p
160
+ WHERE p.owner_only = false
161
+ AND p.key ~ '^(crm|sales|conversations|marketing|automations)\.'
162
+ AND regexp_replace(p.key, '^.*\.', '') IN ('read', 'create', 'edit')
163
+ -- NOT EXISTS em vez de ON CONFLICT: esta tabela é um catálogo sem chave
164
+ -- única declarada, e ali ON CONFLICT DO NOTHING não protege de nada.
165
+ AND NOT EXISTS (
166
+ SELECT 1 FROM app.role_templates x
167
+ WHERE x.template = m.template AND x.role_key = 'manager' AND x.permission = p.key);
@@ -0,0 +1,109 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 031_ticketcontrol_joins_the_family.sql — a casa de shows ganha uma linha.
3
+ --
4
+ -- O registro tem seis produtos: quatro TRADES (cozinha, loja, estúdio, escola),
5
+ -- uma FUNÇÃO que transbordou de aba para app (LeadControl, 011) e o painel da
6
+ -- própria casa (control-admin, 028). Este é o sétimo, e é um trade — mas um
7
+ -- trade que o ChefControl já toca pela metade.
8
+ --
9
+ -- ── Por que não é uma aba do ChefControl ───────────────────────────────────
10
+ -- Um bar que faz evento já cadastra o cardápio, o estoque e o caixa no Chef. O
11
+ -- que ele NÃO tem lá é o evento: a noite com data, capacidade por setor, lote
12
+ -- que vira lote, ingresso que precisa ser recusado na porta quando já entrou.
13
+ -- Isso não é uma tela a mais num app de restaurante — é outro turno, outra
14
+ -- equipe (guichê, portaria, promoter) e outro fechamento.
15
+ --
16
+ -- E é justamente por serem o MESMO TENANT que os dois convivem: `people`,
17
+ -- `orders`, `items` e o financeiro são os mesmos dos dois lados do switcher. A
18
+ -- pessoa que comprou o ingresso é a que abre comanda no bar, na mesma linha.
19
+ -- Nenhum concorrente brasileiro entrega isso porque nenhum é dono dos dois
20
+ -- lados: a bilheteria (Sympla, Ingresse) larga na porta, e o cashless (Zig)
21
+ -- começa nela.
22
+ --
23
+ -- ── `serves_verticals` fica VAZIO, de propósito ────────────────────────────
24
+ -- A 010 fez dessa coluna o gatilho de semeadura automática: um tenant que
25
+ -- declara um ramo ganha todo app que serve aquele ramo. Listar 'food' e
26
+ -- 'restaurant' aqui seria quase verdade — muito bar faz evento — e significaria
27
+ -- que TODO restaurante criado a partir de amanhã cresceria um segundo produto
28
+ -- no switcher que ninguém pediu e ninguém configurou. Um app que chega sem ser
29
+ -- convidado não é uma funcionalidade, é um chamado de suporte sobre uma tela
30
+ -- vazia. Este é opt-in: `tenant_app_set('ticket', ...)`.
31
+ --
32
+ -- ── `requires` nomeia cinco plugins ────────────────────────────────────────
33
+ -- `menu` e `inventory` são o bar; `orders` é o pedido (do ingresso E da
34
+ -- comanda, na mesma tabela); `financial` é o caixa onde a noite fecha;
35
+ -- `conversations` é por onde o público pergunta a hora de abrir. Sem esses
36
+ -- cinco o app abre num rail com Painel e placeholders.
37
+ --
38
+ -- O que NÃO está aqui: um plugin `events`. Ele nasce em M1 (ver
39
+ -- fayz-app/ticket-control/docs/ROADMAP.md) e é quando esta linha ganha o sexto
40
+ -- nome — a migration que criar as tabelas de evento é a mesma que atualiza este
41
+ -- array.
42
+ -- ---------------------------------------------------------------------------
43
+
44
+ -- ── §1 a linha do registro ────────────────────────────────────────────────
45
+ --
46
+ -- O nome é o nome de FORA (ADR: "Control" é a família, não o app). O rail
47
+ -- dentro diz só "Ticket"; a lista do switcher é de fora, então carrega inteiro.
48
+ --
49
+ -- #820AD1 e não outro roxo: a ficha É o produto num lançador, e o violeta claro
50
+ -- (#7C3AED) já é do StudioControl. Este é 18° adiante na roda e 23 pontos mais
51
+ -- escuro — lado a lado eles leem como duas cores, não como dois degraus de uma.
52
+ -- O dividendo prático de escolher o degrau ESCURO: branco sobre ele dá 7,2:1, e
53
+ -- a marca pode simplesmente ser o botão (o ouro do Chef não pode).
54
+ --
55
+ -- `url` é a origem de dev enquanto o app não é publicado, e é deliberadamente
56
+ -- visível: a coluna É a allow-list do handoff de sessão, então trocá-la é o
57
+ -- passo de deploy, não um detalhe.
58
+ INSERT INTO app.apps (id, name, icon, accent_color, url, requires, serves_verticals, active) VALUES
59
+ ('ticket', 'TicketControl', 'Ticket', '#820AD1', 'http://localhost:5308',
60
+ ARRAY['menu', 'inventory', 'orders', 'financial', 'conversations'],
61
+ ARRAY[]::text[], true)
62
+ ON CONFLICT (id) DO UPDATE
63
+ SET name = EXCLUDED.name, icon = EXCLUDED.icon, accent_color = EXCLUDED.accent_color,
64
+ requires = EXCLUDED.requires, active = true,
65
+ -- A URL NÃO é sobrescrita numa reaplicação: quem publicar o app aponta a
66
+ -- coluna para o endereço real, e uma migration que reaplica não pode
67
+ -- devolver a frota para localhost. (Mesma regra da 028.)
68
+ url = coalesce(app.apps.url, EXCLUDED.url);
69
+
70
+ -- ── §2 quem recebe agora ──────────────────────────────────────────────────
71
+ --
72
+ -- Só as contas de creators@fayalabs.com que JÁ têm cardápio ativo — ou seja, as
73
+ -- casas que já operam um bar nesta plataforma, que é a evidência honesta de que
74
+ -- "o bar que também faz evento" é a história delas.
75
+ --
76
+ -- Restrito ao dono do dogfood de propósito, e isto é o que difere da 011: a
77
+ -- LeadControl estava pronta quando ganhou a linha. Este app está em M0 — o rail
78
+ -- é real, metade das telas ainda é placeholder que diz de qual marco é. Ligá-lo
79
+ -- para todo restaurante do cluster seria distribuir um produto pela metade.
80
+ -- Quando M2 fechar (o ingresso vira dinheiro), a regra geral entra numa
81
+ -- migration própria, que é onde ela pode ser revista sozinha.
82
+ INSERT INTO app.tenant_apps (tenant_id, app_id, status, source)
83
+ SELECT DISTINCT m.tenant_id, 'ticket', 'active', 'manual'
84
+ FROM app.memberships m
85
+ JOIN auth.users u ON u.id = m.user_id
86
+ WHERE u.email = 'creators@fayalabs.com'
87
+ AND m.active
88
+ AND EXISTS (
89
+ SELECT 1 FROM app.tenant_plugins tp
90
+ WHERE tp.tenant_id = m.tenant_id AND tp.plugin_id = 'menu'
91
+ AND tp.facet = '' AND tp.status = 'active'
92
+ )
93
+ ON CONFLICT (tenant_id, app_id) DO NOTHING;
94
+
95
+ -- E os plugins que ele precisa, para exatamente essas contas — senão o app abre
96
+ -- num rail com Painel e nada mais, porque a shell monta o rail com o que
97
+ -- app_config() devolve em `plugins`.
98
+ --
99
+ -- O teto do plano ganha do padrão, sempre: uma conta cujo plano não inclui o
100
+ -- plugin não o acende, e a semeadura não falha por isso.
101
+ INSERT INTO app.tenant_plugins (tenant_id, plugin_id, facet, status, source)
102
+ SELECT DISTINCT ta.tenant_id, r.plugin_id, '', 'active', 'default'
103
+ FROM app.tenant_apps ta
104
+ JOIN app.apps a ON a.id = ta.app_id
105
+ CROSS JOIN LATERAL unnest(a.requires) AS r(plugin_id)
106
+ WHERE ta.app_id = 'ticket'
107
+ AND ta.status = 'active'
108
+ AND public.plan_permits(r.plugin_id, '', ta.tenant_id)
109
+ ON CONFLICT (tenant_id, plugin_id, facet) DO NOTHING;
@@ -0,0 +1,291 @@
1
+ -- 032_a_marca_e_um_conjunto_de_tokens.sql
2
+ --
3
+ -- A marca de um tenant tinha DUAS fontes de verdade e TRÊS grafias, e nenhuma
4
+ -- das três conversava com as outras.
5
+ --
6
+ -- `ConnectedBrandingSettings` GRAVA settings.branding.primaryColor (camelo)
7
+ -- `admin-app.tsx` LÊ settings.branding.primaryColor (camelo)
8
+ -- `app.branding_of()` LÊ settings.branding.primary_color (cobra)
9
+ --
10
+ -- Medido antes desta migration: **nenhum** tenant tinha a chave em camelo, e um
11
+ -- tinha a em cobra (posta por script). Ou seja: a cor que o lojista escolhe em
12
+ -- Configurações nunca chegou ao switcher nem à página pública, e a que existia
13
+ -- nunca chegou ao tema. A tela funcionava; o dado ia para uma gaveta que
14
+ -- ninguém abria.
15
+ --
16
+ -- E, por cima disso, o porte de CRM criou uma SEGUNDA marca — `plg_marketing_brand`
17
+ -- — com nome, tom de voz e palavras proibidas, dentro de um plugin. Duas marcas
18
+ -- para uma casa é pior que nenhuma: mudar a cor num lugar não muda no outro, e
19
+ -- ninguém sabe qual das duas o site está usando.
20
+ --
21
+ -- ── O QUE ESTA MIGRATION ESTABELECE ───────────────────────────────────────
22
+ --
23
+ -- Uma marca é UM CONJUNTO DE TOKENS com três camadas, e a terceira é a que
24
+ -- costuma ficar de fora:
25
+ --
26
+ -- identity quem a marca é nome, assinatura, posicionamento, logo
27
+ -- tokens como ela se parece cor e tipografia
28
+ -- voice como ela FALA tom, léxico, chamadas, palavras proibidas
29
+ --
30
+ -- Voz não é "uma funcionalidade de marketing" — é a dimensão VERBAL do mesmo
31
+ -- conjunto. Um sistema de design que descreve a cor do botão e não descreve o
32
+ -- que o produto pode dizer está descrevendo metade da marca. É por isso que ela
33
+ -- sai do plugin e entra aqui, ao lado da cor.
34
+ --
35
+ -- RETROCOMPATIBILIDADE É OBRIGATÓRIA, e não por educação: `app.branding_of` é
36
+ -- lida por `tenant_branding_public(slug, domain)`, que serve loja pública e
37
+ -- domínio próprio a visitante ANÔNIMO. Uma troca de forma que derrube isso tira
38
+ -- o site do ar de quem já vendeu hoje. Por isso o leitor passa a aceitar as
39
+ -- TRÊS grafias e a saída antiga continua idêntica — o documento em camadas vem
40
+ -- ao lado, não no lugar.
41
+
42
+ -- ── o leitor tolerante ─────────────────────────────────────────────────────
43
+ --
44
+ -- Uma chave, três lugares onde ela pode estar. A ordem é deliberada: o formato
45
+ -- NOVO primeiro, porque é para onde tudo caminha; depois cobra, que é o que o
46
+ -- resolvedor sempre leu; por último camelo, que é o que a tela sempre gravou.
47
+ CREATE OR REPLACE FUNCTION app.brand_token(p_branding jsonb, p_path text[], p_snake text, p_camel text)
48
+ RETURNS text
49
+ LANGUAGE sql
50
+ IMMUTABLE
51
+ SET search_path TO ''
52
+ AS $$
53
+ SELECT coalesce(
54
+ nullif(p_branding #>> p_path, ''),
55
+ nullif(p_branding ->> p_snake, ''),
56
+ nullif(p_branding ->> p_camel, '')
57
+ )
58
+ $$;
59
+
60
+ COMMENT ON FUNCTION app.brand_token(jsonb, text[], text, text) IS
61
+ 'Lê um token de marca aceitando as três grafias que existiram: o caminho em camadas, cobra e camelo. A tolerância é o que permite mudar a forma sem derrubar a loja pública (032).';
62
+
63
+ -- ── a marca inteira, normalizada ───────────────────────────────────────────
64
+ CREATE OR REPLACE FUNCTION app.brand_of(p_tenant uuid)
65
+ RETURNS jsonb
66
+ LANGUAGE plpgsql
67
+ STABLE
68
+ SECURITY DEFINER
69
+ SET search_path TO ''
70
+ AS $$
71
+ DECLARE t record; b jsonb;
72
+ BEGIN
73
+ SELECT x.id, x.name, x.slug, x.logo_url, x.settings INTO t
74
+ FROM public.tenants x WHERE x.id = p_tenant;
75
+ IF t.id IS NULL THEN RETURN NULL; END IF;
76
+
77
+ b := coalesce(t.settings -> 'branding', '{}'::jsonb);
78
+
79
+ RETURN jsonb_strip_nulls(jsonb_build_object(
80
+ 'identity', jsonb_build_object(
81
+ 'name', coalesce(app.brand_token(b, '{identity,name}', 'app_name', 'appName'), t.name),
82
+ 'tagline', app.brand_token(b, '{identity,tagline}', 'tagline', 'tagline'),
83
+ 'mission', app.brand_token(b, '{identity,mission}', 'mission', 'mission'),
84
+ 'logoUrl', coalesce(app.brand_token(b, '{identity,logoUrl}', 'logo_url', 'logoUrl'), t.logo_url),
85
+ 'faviconUrl',app.brand_token(b, '{identity,faviconUrl}', 'favicon_url', 'faviconUrl'),
86
+ 'customDomain', app.brand_token(b, '{identity,customDomain}', 'custom_domain', 'customDomain')
87
+ ),
88
+ 'tokens', jsonb_build_object(
89
+ 'color', jsonb_strip_nulls(jsonb_build_object(
90
+ 'primary', app.brand_token(b, '{tokens,color,primary}', 'primary_color', 'primaryColor'),
91
+ 'accent', app.brand_token(b, '{tokens,color,accent}', 'accent_color', 'accentColor')
92
+ )),
93
+ 'font', jsonb_strip_nulls(jsonb_build_object(
94
+ 'heading', app.brand_token(b, '{tokens,font,heading}', 'heading_font', 'headingFont'),
95
+ 'body', app.brand_token(b, '{tokens,font,body}', 'body_font', 'bodyFont')
96
+ ))
97
+ ),
98
+ -- A camada verbal. Arrays e não texto livre: o agente compara termo a
99
+ -- termo, e "evite gírias" não é comparável.
100
+ 'voice', jsonb_strip_nulls(jsonb_build_object(
101
+ 'tone', app.brand_token(b, '{voice,tone}', 'tone_of_voice', 'toneOfVoice'),
102
+ 'lexicon', b #> '{voice,lexicon}',
103
+ 'avoid', b #> '{voice,avoid}',
104
+ 'ctas', b #> '{voice,ctas}',
105
+ 'hashtags', b #> '{voice,hashtags}'
106
+ ))
107
+ ));
108
+ END $$;
109
+
110
+ COMMENT ON FUNCTION app.brand_of(uuid) IS
111
+ 'A marca como conjunto de tokens em três camadas: identity, tokens (cor/tipografia) e voice. Voz é a dimensão VERBAL do mesmo conjunto, não uma funcionalidade de marketing (032).';
112
+
113
+ REVOKE ALL ON FUNCTION app.brand_of(uuid) FROM PUBLIC, anon;
114
+
115
+ -- ── o resolvedor antigo continua idêntico por fora ─────────────────────────
116
+ --
117
+ -- Mesmas chaves, mesma forma. O que muda é que ele passa a ENXERGAR o que a
118
+ -- tela sempre gravou: antes lia só `primary_color`, e a tela gravava
119
+ -- `primaryColor`. É esta linha que faz a cor escolhida em Configurações chegar
120
+ -- ao switcher e à loja pública pela primeira vez.
121
+ CREATE OR REPLACE FUNCTION app.branding_of(p_tenant uuid) RETURNS jsonb
122
+ LANGUAGE plpgsql STABLE SECURITY DEFINER
123
+ SET search_path TO ''
124
+ AS $$
125
+ DECLARE t record; b jsonb;
126
+ BEGIN
127
+ SELECT x.id, x.name, x.slug, x.logo_url, x.settings INTO t FROM public.tenants x WHERE x.id = p_tenant;
128
+ IF t.id IS NULL THEN
129
+ RETURN jsonb_build_object('tenant_id', NULL, 'name', NULL, 'slug', NULL, 'logo_url', NULL, 'primary_color', NULL, 'custom_domain', NULL);
130
+ END IF;
131
+ b := coalesce(t.settings -> 'branding', '{}'::jsonb);
132
+ RETURN jsonb_build_object(
133
+ 'tenant_id', t.id,
134
+ 'name', coalesce(app.brand_token(b, '{identity,name}', 'app_name', 'appName'), t.name),
135
+ 'slug', t.slug,
136
+ 'logo_url', coalesce(app.brand_token(b, '{identity,logoUrl}', 'logo_url', 'logoUrl'), t.logo_url),
137
+ 'primary_color', app.brand_token(b, '{tokens,color,primary}', 'primary_color', 'primaryColor'),
138
+ 'custom_domain', app.brand_token(b, '{identity,customDomain}', 'custom_domain', 'customDomain')
139
+ );
140
+ EXCEPTION WHEN OTHERS THEN
141
+ RETURN jsonb_build_object('tenant_id', p_tenant, 'name', NULL, 'slug', NULL, 'logo_url', NULL, 'primary_color', NULL, 'custom_domain', NULL,
142
+ 'error', 'branding unavailable');
143
+ END $$;
144
+
145
+ -- ── a marca, para quem está dentro ─────────────────────────────────────────
146
+ CREATE OR REPLACE FUNCTION public.tenant_brand() RETURNS jsonb
147
+ LANGUAGE plpgsql STABLE SECURITY DEFINER
148
+ SET search_path TO ''
149
+ AS $$
150
+ BEGIN
151
+ RETURN coalesce(app.brand_of(app.current_tenant_id()), '{}'::jsonb);
152
+ EXCEPTION WHEN OTHERS THEN
153
+ RETURN '{}'::jsonb;
154
+ END $$;
155
+
156
+ COMMENT ON FUNCTION public.tenant_brand() IS
157
+ 'A marca em camadas do tenant da sessão. Consumidores: tema da shell (tokens.color), agente (voice), páginas públicas (tudo) (032).';
158
+
159
+ REVOKE ALL ON FUNCTION public.tenant_brand() FROM PUBLIC, anon;
160
+ GRANT EXECUTE ON FUNCTION public.tenant_brand() TO authenticated;
161
+ GRANT EXECUTE ON FUNCTION public.tenant_brand() TO service_role;
162
+
163
+ -- ── escrever uma camada sem apagar as outras ───────────────────────────────
164
+ --
165
+ -- A tela salva por SEÇÃO — cores, ou voz, ou identidade. Um `update` que troca
166
+ -- `settings.branding` inteiro faria salvar a aba de cores apagar o tom de voz
167
+ -- que outra pessoa acabou de escrever. Fundir por caminho é o que impede isso.
168
+ CREATE OR REPLACE FUNCTION public.tenant_brand_patch(p_patch jsonb)
169
+ RETURNS jsonb
170
+ LANGUAGE plpgsql
171
+ SECURITY DEFINER
172
+ SET search_path TO ''
173
+ AS $$
174
+ DECLARE v_tenant uuid := app.current_tenant_id();
175
+ BEGIN
176
+ IF v_tenant IS NULL THEN
177
+ RAISE EXCEPTION 'tenant_brand_patch: sem tenant na sessão' USING ERRCODE = '42501';
178
+ END IF;
179
+ -- `settings.edit` e não `tenant.edit`: a marca é configuração da casa, e quem
180
+ -- mexe nela é quem mexe nas configurações.
181
+ IF NOT app.has_permission('settings.edit', NULL::uuid) THEN
182
+ RAISE EXCEPTION 'tenant_brand_patch: exige settings.edit' USING ERRCODE = '42501';
183
+ END IF;
184
+
185
+ UPDATE public.tenants t
186
+ SET settings = jsonb_set(
187
+ coalesce(t.settings, '{}'::jsonb),
188
+ '{branding}',
189
+ -- `||` funde raso, e é o que se quer POR CAMADA: mandar
190
+ -- {"voice":{...}} troca a voz inteira e não toca em tokens. Mandar
191
+ -- {"tokens":{"color":{...}}} trocaria `tokens` inteiro, então a tela
192
+ -- manda a camada `tokens` completa quando mexe em qualquer cor.
193
+ coalesce(t.settings -> 'branding', '{}'::jsonb) || coalesce(p_patch, '{}'::jsonb)
194
+ ),
195
+ updated_at = now()
196
+ WHERE t.id = v_tenant;
197
+
198
+ RETURN coalesce(app.brand_of(v_tenant), '{}'::jsonb);
199
+ END $$;
200
+
201
+ COMMENT ON FUNCTION public.tenant_brand_patch(jsonb) IS
202
+ 'Funde uma camada da marca sem apagar as outras — salvar a aba de cores não pode apagar o tom de voz de quem escreveu antes (032).';
203
+
204
+ REVOKE ALL ON FUNCTION public.tenant_brand_patch(jsonb) FROM PUBLIC, anon;
205
+ GRANT EXECUTE ON FUNCTION public.tenant_brand_patch(jsonb) TO authenticated;
206
+ GRANT EXECUTE ON FUNCTION public.tenant_brand_patch(jsonb) TO service_role;
207
+
208
+ -- ── a segunda marca vem para casa ──────────────────────────────────────────
209
+ --
210
+ -- `plg_marketing_brand` durou uma onda. Ela nasceu com o kit de marca do porte
211
+ -- do V1, num plugin, sem saber que o núcleo já tinha uma marca com resolvedor e
212
+ -- três consumidores. As linhas dela entram na camada certa e a tabela sai — não
213
+ -- fica como view: uma segunda porta para a mesma coisa é justamente o que esta
214
+ -- migration existe para desfazer.
215
+ --
216
+ -- A fusão preserva o que já estivesse em `settings.branding`: o kit do plugin
217
+ -- perde para a configuração do núcleo em caso de conflito, porque é a do núcleo
218
+ -- que o tema e a loja pública leem hoje.
219
+ DO $$
220
+ DECLARE r record; v_patch jsonb; v_n integer := 0;
221
+ BEGIN
222
+ IF to_regclass('public.plg_marketing_brand') IS NULL THEN RETURN; END IF;
223
+
224
+ FOR r IN SELECT * FROM public.plg_marketing_brand LOOP
225
+ v_patch := jsonb_strip_nulls(jsonb_build_object(
226
+ 'identity', jsonb_strip_nulls(jsonb_build_object(
227
+ 'name', nullif(btrim(coalesce(r.name, '')), ''),
228
+ 'tagline', nullif(btrim(coalesce(r.tagline, '')), ''),
229
+ 'mission', nullif(btrim(coalesce(r.mission, '')), ''),
230
+ 'logoUrl', nullif(btrim(coalesce(r.logo_url, '')), '')
231
+ )),
232
+ -- A COR DO PLUGIN SÓ ENTRA SE O NÚCLEO NÃO TIVER UMA.
233
+ --
234
+ -- `||` funde raso e não sobrescreve caminho diferente: com a cor do
235
+ -- núcleo em `branding.primaryColor` (plana) e a do plugin em
236
+ -- `branding.tokens.color.primary` (em camadas), AS DUAS sobrevivem — e o
237
+ -- leitor tolerante prefere a forma nova, então a do plugin venceria.
238
+ -- Isto foi pego no container: `#FF0000` do plugin passou na frente do
239
+ -- `#2563EB` do núcleo. Quem manda é o núcleo, porque é o que o tema e a
240
+ -- loja pública leem hoje.
241
+ 'tokens', jsonb_strip_nulls(jsonb_build_object(
242
+ 'color', CASE
243
+ WHEN app.brand_token((SELECT x.settings -> 'branding' FROM public.tenants x WHERE x.id = r.tenant_id),
244
+ '{tokens,color,primary}', 'primary_color', 'primaryColor') IS NULL
245
+ THEN nullif(coalesce(r.colors, '{}'::jsonb), '{}'::jsonb)
246
+ END,
247
+ 'font', nullif(coalesce(r.fonts, '{}'::jsonb), '{}'::jsonb)
248
+ )),
249
+ 'voice', jsonb_strip_nulls(jsonb_build_object(
250
+ 'tone', nullif(btrim(coalesce(r.tone_of_voice, '')), ''),
251
+ 'lexicon', to_jsonb(coalesce(r.keywords, ARRAY[]::text[])),
252
+ 'avoid', to_jsonb(coalesce(r.avoid_words, ARRAY[]::text[])),
253
+ 'ctas', to_jsonb(coalesce(r.ctas, ARRAY[]::text[])),
254
+ 'hashtags', to_jsonb(coalesce(r.hashtags, ARRAY[]::text[]))
255
+ ))
256
+ ));
257
+
258
+ UPDATE public.tenants t
259
+ SET settings = jsonb_set(
260
+ coalesce(t.settings, '{}'::jsonb), '{branding}',
261
+ -- `patch || existente` deixa o que já estava vencer NO MESMO
262
+ -- caminho. Para caminhos diferentes (plana × camadas) a defesa é
263
+ -- o CASE acima, na montagem do patch.
264
+ v_patch || coalesce(t.settings -> 'branding', '{}'::jsonb))
265
+ WHERE t.id = r.tenant_id;
266
+ v_n := v_n + 1;
267
+ END LOOP;
268
+
269
+ RAISE NOTICE '032: % kit(s) de marca trazidos do plugin para o núcleo', v_n;
270
+ END $$;
271
+
272
+ DROP TABLE IF EXISTS public.plg_marketing_brand;
273
+ DELETE FROM app.scaffold_registry WHERE table_name = 'plg_marketing_brand';
274
+
275
+ -- ── a grafia velha, normalizada de uma vez ─────────────────────────────────
276
+ --
277
+ -- O leitor tolera as três grafias para sempre — é o que protege quem tiver dado
278
+ -- antigo. Mas os tenants que existem HOJE ganham a forma nova agora, para que a
279
+ -- tela edite e leia o mesmo lugar desde o primeiro clique.
280
+ UPDATE public.tenants t
281
+ SET settings = jsonb_set(
282
+ coalesce(t.settings, '{}'::jsonb), '{branding}',
283
+ jsonb_strip_nulls(jsonb_build_object(
284
+ 'tokens', jsonb_build_object('color', jsonb_strip_nulls(jsonb_build_object(
285
+ 'primary', app.brand_token(t.settings -> 'branding', '{tokens,color,primary}', 'primary_color', 'primaryColor'),
286
+ 'accent', app.brand_token(t.settings -> 'branding', '{tokens,color,accent}', 'accent_color', 'accentColor')
287
+ )))
288
+ )) || coalesce(t.settings -> 'branding', '{}'::jsonb))
289
+ WHERE t.settings -> 'branding' IS NOT NULL
290
+ AND app.brand_token(t.settings -> 'branding', '{tokens,color,primary}', 'primary_color', 'primaryColor') IS NOT NULL
291
+ AND t.settings #>> '{branding,tokens,color,primary}' IS NULL;
@@ -0,0 +1,53 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 041_a_entrega_tem_onde_guardar_a_politica.sql — o quadro de Delivery existe
3
+ -- desde sempre e a política dele não tinha onde morar.
4
+ --
5
+ -- ── O defeito ──────────────────────────────────────────────────────────────
6
+ -- Taxa de entrega, pedido mínimo, tempo estimado e raio são decisões da CASA:
7
+ -- mudam com a estação, com o combustível e com o bairro. Não estavam em lugar
8
+ -- nenhum — o app desenhava um interruptor de Delivery em `useState`, que volta
9
+ -- ao padrão a cada F5, e o resto era combinado por telefone.
10
+ --
11
+ -- ── A escolha ──────────────────────────────────────────────────────────────
12
+ -- `tenants.settings` (150/ADR 0023) já é onde a conta guarda o que muda sem
13
+ -- deploy, com `tenant_setting_set` cobrando `settings.manage` e conferindo o
14
+ -- tipo. Só faltava a chave existir: a função recusa chave não registrada, de
15
+ -- propósito, para que um erro de digitação no cliente não vire uma
16
+ -- configuração fantasma que ninguém lê.
17
+ --
18
+ -- Registradas com `reader = 'plugin:orders'`: quem as lê é o plugin de
19
+ -- pedidos, e é ele quem some junto se um dia o app não montar delivery.
20
+ -- ---------------------------------------------------------------------------
21
+
22
+ SELECT app.seed_setting_keys($$[
23
+ {
24
+ "key": "delivery.enabled", "type": "boolean", "default": true,
25
+ "reader": "plugin:orders",
26
+ "description": "Se a casa entrega. Desligado, o quadro de despacho segue existindo para o histórico e nenhum canal novo nasce como entrega."
27
+ },
28
+ {
29
+ "key": "delivery.fee", "type": "number", "default": 0,
30
+ "reader": "plugin:orders",
31
+ "description": "Taxa de entrega padrão, na moeda da casa. Zero = entrega sem taxa."
32
+ },
33
+ {
34
+ "key": "delivery.free_over", "type": "number", "default": null,
35
+ "reader": "plugin:orders",
36
+ "description": "Acima deste valor de pedido a taxa é zerada. Nulo = a taxa vale sempre."
37
+ },
38
+ {
39
+ "key": "delivery.min_order", "type": "number", "default": 0,
40
+ "reader": "plugin:orders",
41
+ "description": "Pedido mínimo para entregar. Zero = sem mínimo."
42
+ },
43
+ {
44
+ "key": "delivery.eta_minutes", "type": "integer", "default": 45,
45
+ "reader": "plugin:orders",
46
+ "description": "Tempo estimado de entrega, em minutos — o que o cliente ouve quando pergunta."
47
+ },
48
+ {
49
+ "key": "delivery.radius_km", "type": "number", "default": 0,
50
+ "reader": "plugin:orders",
51
+ "description": "Raio de entrega em quilômetros. Zero = sem limite declarado."
52
+ }
53
+ ]$$::jsonb);
@@ -0,0 +1,17 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 042_o_token_de_marca_fecha_a_porta_anon.sql
3
+ --
4
+ -- A 032 criou app.brand_token como helper interno, mas funções novas do
5
+ -- PostgreSQL nascem executáveis por PUBLIC. Isso deixou `anon` chamar uma
6
+ -- função do schema privado `app` e fez a postura N15c ficar vermelha.
7
+ --
8
+ -- A correção não pode morar retroativamente na 032: os clusters ChefControl
9
+ -- já registraram o checksum original dela em public._migrations. Editar um
10
+ -- arquivo aplicado produz drift e faz `fayz db apply` parar antes de alcançar
11
+ -- qualquer correção posterior. Por isso a 032 mantém seus bytes históricos e
12
+ -- esta migration nova fecha a permissão nos bancos existentes.
13
+ -- ---------------------------------------------------------------------------
14
+
15
+ REVOKE ALL
16
+ ON FUNCTION app.brand_token(jsonb, text[], text, text)
17
+ FROM PUBLIC, anon;