@fayz-ai/db 0.18.2 → 0.20.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 (23) hide show
  1. package/migrations/136_a_baixa_do_espelho_e_do_porte_nao_do_v2.sql +204 -0
  2. package/migrations/137_a_retirada_e_um_canal_de_venda.sql +43 -0
  3. package/migrations/138_o_produto_tem_cor_e_a_conta_so_a_veste_se_pagar.sql +606 -0
  4. package/migrations/139_a_equipe_e_um_agrupamento_declarado.sql +50 -0
  5. package/migrations/140_a_unidade_se_escreve_pela_mesma_porta_que_se_le.sql +130 -0
  6. package/migrations/141_o_endereco_da_unidade_mora_na_tabela_de_enderecos.sql +203 -0
  7. package/migrations/142_a_coordenada_e_do_endereco_nao_da_unidade.sql +157 -0
  8. package/migrations/143_o_preset_para_de_nomear_telas_que_nao_existem.sql +29 -0
  9. package/migrations/144_marketing_e_um_botao_nao_uma_gaveta.sql +46 -0
  10. package/migrations/145_campanhas_e_templates_somem_em_toda_conta.sql +36 -0
  11. package/migrations/146_a_clinica_ve_as_proprias_analises.sql +31 -0
  12. package/migrations/147_tres_saloes_desligam_o_rh.sql +30 -0
  13. package/migrations/148_servicos_ganham_a_permissao_que_nunca_tiveram.sql +37 -0
  14. package/migrations/149_a_conta_do_fornecedor_nao_e_conta_da_casa.sql +131 -0
  15. package/migrations/150_todo_cadastro_tem_observacao_e_etiqueta.sql +62 -0
  16. package/migrations/151_a_parceria_e_uma_especie_de_pessoa.sql +35 -0
  17. package/migrations/152_a_profissao_e_a_especie_de_pet_sao_catalogos.sql +31 -0
  18. package/migrations/153_a_sala_nao_e_a_filial.sql +94 -0
  19. package/migrations/154_as_duas_tabelas_novas_dizem_de_quem_sao.sql +33 -0
  20. package/migrations/155_o_pacote_tem_observacao_e_etiqueta_e_diz_de_quem_e.sql +28 -0
  21. package/migrations/156_restaurante_nao_vende_servico.sql +44 -0
  22. package/migrations/157_o_que_a_porta_da_unidade_criou_tambem_se_revoga.sql +45 -0
  23. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ -- ============================================================================
2
+ -- 147 — três salões desligam o RH
3
+ --
4
+ -- Espaço Facial, Walter's Coiffeur e Além do Olhar não têm departamento de
5
+ -- pessoal: quem trabalha lá é profissional que atende, e o cadastro disso já
6
+ -- mora em Cadastros → Funcionários. A linha "RH" no rail abria um módulo
7
+ -- inteiro (ponto, escalas, folha) que nenhuma das três usa — e um item de menu
8
+ -- que ninguém abre ainda custa: ele empurra para baixo os que abrem todo dia.
9
+ --
10
+ -- Por conta e não por preset: as três dividem o StudioControl com salões que
11
+ -- talvez queiram RH. `app.tenant_plugins` é onde isso se decide — um plugin
12
+ -- desligado não contribui rota, menu, aba de configuração nem widget
13
+ -- (`resolvePluginRuntime`), então a tela deixa de existir para elas em vez de
14
+ -- ficar escondida e alcançável por link.
15
+ --
16
+ -- `source = 'manual'` porque foi decisão da casa, não do plano nem do padrão do
17
+ -- ofício — e é o que impede um `tenant_apply_vertical_defaults` de reativá-lo
18
+ -- sem querer.
19
+ -- ============================================================================
20
+
21
+ INSERT INTO app.tenant_plugins (tenant_id, plugin_id, facet, status, source)
22
+ SELECT t.id, 'workforce', '', 'disabled', 'manual'
23
+ FROM public.tenants t
24
+ WHERE t.id IN (
25
+ 'af800ef8-853d-543f-9940-1e9a5663c061', -- Espaço Facial
26
+ 'c3a8f61d-742b-4e05-9d16-8b47e2f0a5c3', -- Walter's Coiffeur
27
+ 'b1d7c4e2-9a3f-4c18-8f52-6e0a7d3b4c91' -- Além do Olhar
28
+ )
29
+ ON CONFLICT (tenant_id, plugin_id, facet)
30
+ DO UPDATE SET status = 'disabled', source = 'manual', updated_at = now();
@@ -0,0 +1,37 @@
1
+ -- ============================================================================
2
+ -- 148 — Serviços ganham a permissão que nunca tiveram
3
+ --
4
+ -- O `plugin-registry` tranca quatro telas — Serviços, Categorias de Serviço,
5
+ -- Locais Operacionais e Grupos de Locais — atrás de `feature: 'services'`. A
6
+ -- chave `services.*` NUNCA foi cunhada em `app.permissions`: ela não está no
7
+ -- catálogo, então papel nenhum a tem, e nenhuma tela ligada a ela desenha.
8
+ --
9
+ -- Só o OWNER via as quatro, porque owner lê o catálogo em vez da lista
10
+ -- congelada do papel. Daí o sintoma confuso: as mesmas telas apareciam numa
11
+ -- conta e sumiam na outra conforme o papel de quem entrou, sem erro e sem
12
+ -- explicação. São 30 contas com admin ou gerente nesse estado.
13
+ --
14
+ -- A permissão vai onde o `catalog.*` já vai, e por isso: as duas respondem à
15
+ -- mesma pergunta — quem mantém o que a casa OFERECE. Quem pode ler o catálogo
16
+ -- pode ler os serviços; quem edita, edita. Sem inventar audiência nova, e sem
17
+ -- dar a ninguém algo que já não tivesse.
18
+ --
19
+ -- Idempotente.
20
+ -- ============================================================================
21
+
22
+ INSERT INTO app.permissions (key, plugin, description)
23
+ VALUES
24
+ ('services.read', 'registry', 'Ver serviços, categorias de serviço, locais operacionais e grupos de locais'),
25
+ ('services.create', 'registry', 'Cadastrar serviço, categoria, local operacional ou grupo de locais'),
26
+ ('services.edit', 'registry', 'Alterar serviço, categoria, local operacional ou grupo de locais'),
27
+ ('services.delete', 'registry', 'Excluir serviço, categoria, local operacional ou grupo de locais'),
28
+ ('services.manage', 'registry', 'Coringa de serviços — vale por ler, criar, alterar e excluir')
29
+ ON CONFLICT (key) DO NOTHING;
30
+
31
+ -- Espelha o que cada papel já pode no catálogo. `manage` é coringa e viaja
32
+ -- junto: um papel que administra o catálogo administra o que a casa oferece.
33
+ INSERT INTO app.role_permissions (role_id, permission, tenant_id)
34
+ SELECT p.role_id, replace(p.permission, 'catalog.', 'services.'), p.tenant_id
35
+ FROM app.role_permissions p
36
+ WHERE p.permission IN ('catalog.read', 'catalog.create', 'catalog.edit', 'catalog.delete', 'catalog.manage')
37
+ ON CONFLICT DO NOTHING;
@@ -0,0 +1,131 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 149_a_conta_do_fornecedor_nao_e_conta_da_casa.sql
3
+ --
4
+ -- `public.payee_accounts` — para onde vai o dinheiro de QUEM A CASA PAGA.
5
+ --
6
+ -- ── por que uma tabela, e não oito chaves em custom_fields ──────────────
7
+ --
8
+ -- É o mesmo argumento que a 142 fez para endereço, e a 117 para o Instagram:
9
+ -- dado de contato guardado numa sacola jsonb não é buscável, não é exportável,
10
+ -- não é conferível, e nenhuma consulta o alcança. A remessa CNAB 240 precisa
11
+ -- varrer «todos os fornecedores com conta cadastrada» — em `custom_fields`
12
+ -- isso é um scan de jsonb sobre a tabela inteira de pessoas.
13
+ --
14
+ -- E há o motivo de forma: uma pessoa pode ter MAIS DE UMA conta (a do CNPJ e a
15
+ -- do sócio; a corrente e a poupança do INSS). Oito colunas numa linha de pessoa
16
+ -- decidem por ela que só existe uma.
17
+ --
18
+ -- ── e por que uma tabela NOVA, e não `plg_financial_accounts` ───────────
19
+ --
20
+ -- Esta é a pergunta que o cadastro de Contas responde errado se a resposta for
21
+ -- «reaproveita». `plg_financial_accounts` é o RAZÃO da casa: cada linha dela é
22
+ -- dinheiro que o tenant tem, concilia e fecha. A conta do fornecedor não é
23
+ -- dinheiro da casa — é uma INSTRUÇÃO DE PAGAMENTO, um endereço para onde o
24
+ -- dinheiro sai.
25
+ --
26
+ -- Misturar as duas põe a conta do fornecedor na lista de Contas, no seletor de
27
+ -- «Receber em», no extrato e no saldo consolidado. Não existe filtro que
28
+ -- conserte isso com segurança: basta uma tela nova esquecer o recorte e a conta
29
+ -- de terceiro vira caixa da empresa. Tabelas separadas tornam o vazamento
30
+ -- impossível por construção, que é a única garantia que não depende de ninguém
31
+ -- lembrar.
32
+ --
33
+ -- (`plg_financial_bank_accounts` é a tabela LEGADA, encaminhada por gatilho
34
+ -- para `plg_financial_accounts`. Não é candidata a nada.)
35
+ --
36
+ -- ── titular ─────────────────────────────────────────────────────────────
37
+ --
38
+ -- `holder_name` e `holder_document` existem porque a conta raramente está no
39
+ -- nome de quem presta o serviço: está no do MEI, da esposa, da holding. Quem
40
+ -- faz a transferência precisa bater o documento antes de mandar, ou paga o
41
+ -- titular errado com o nome certo.
42
+ -- ---------------------------------------------------------------------------
43
+
44
+ CREATE TABLE IF NOT EXISTS public.payee_accounts (
45
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
46
+ tenant_id uuid NOT NULL,
47
+
48
+ -- Polimórfico como `addresses`, e pelas mesmas razões: fornecedor, parceiro,
49
+ -- profissional e funcionário são todos `people`, e a unidade também recebe.
50
+ owner_type text DEFAULT 'person'::text NOT NULL,
51
+ owner_id uuid NOT NULL,
52
+
53
+ -- «Conta do MEI», «Poupança». Nulo é aceitável: a maioria tem uma só.
54
+ label text,
55
+ is_default boolean DEFAULT false NOT NULL,
56
+
57
+ -- O NÚMERO do banco antes do nome: «077» é como o boleto, o CNAB e o PIX
58
+ -- chamam o Inter; «Inter» é como a pessoa chama. O cadastro precisa dos dois,
59
+ -- e o porte do V1 mostrou o custo de não separar — lá as duas coisas dividem
60
+ -- a mesma coluna e há conta guardando o código onde deveria estar o nome.
61
+ bank_code text,
62
+ bank_name text,
63
+
64
+ -- Dígito em coluna própria: a remessa CNAB 240 lê agência e dígito em campos
65
+ -- separados, e partir «1344-7» na hora de gerar o arquivo é pagamento
66
+ -- devolvido quando alguém digitou «1344/7».
67
+ agency_number text,
68
+ agency_digit text,
69
+ account_number text,
70
+ account_digit text,
71
+ account_type text,
72
+
73
+ -- O tipo da chave PIX não se deduz com segurança do formato: um telefone sem
74
+ -- o +55 e um CPF são ambos onze dígitos, e mandar um como o outro paga a
75
+ -- pessoa errada. Quem escolhe é quem cadastra.
76
+ pix_key text,
77
+ pix_key_type text,
78
+
79
+ holder_name text,
80
+ holder_document text,
81
+
82
+ notes text,
83
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
84
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
85
+
86
+ CONSTRAINT payee_accounts_pkey PRIMARY KEY (id),
87
+ CONSTRAINT payee_accounts_owner_type_check
88
+ CHECK (owner_type = ANY (ARRAY['person'::text, 'unit'::text, 'tenant'::text])),
89
+ CONSTRAINT payee_accounts_account_type_check
90
+ CHECK (account_type IS NULL OR account_type = ANY (ARRAY['corrente'::text, 'poupanca'::text, 'pagamento'::text])),
91
+ CONSTRAINT payee_accounts_pix_key_type_check
92
+ CHECK (pix_key_type IS NULL OR pix_key_type = ANY (ARRAY['cpf'::text, 'cnpj'::text, 'email'::text, 'telefone'::text, 'aleatoria'::text])),
93
+
94
+ -- Uma linha SEM conta e SEM PIX não é um dado, é um formulário aberto por
95
+ -- engano. A tela já recusa antes; isto é a rede.
96
+ CONSTRAINT payee_accounts_has_something
97
+ CHECK (account_number IS NOT NULL OR pix_key IS NOT NULL)
98
+ );
99
+
100
+ COMMENT ON TABLE public.payee_accounts IS
101
+ 'Para onde o dinheiro SAI: conta e PIX de quem a casa paga (fornecedor, parceiro, profissional, funcionário). NÃO é conta do razão — o dinheiro da casa vive em plg_financial_accounts, e misturar as duas põe a conta de terceiro no extrato e no saldo da empresa.';
102
+
103
+ COMMENT ON COLUMN public.payee_accounts.holder_document IS
104
+ 'A conta raramente está no nome de quem presta o serviço. Quem transfere bate o documento antes de mandar.';
105
+
106
+ -- O caminho que toda tela faz: «as contas desta pessoa».
107
+ CREATE INDEX IF NOT EXISTS payee_accounts_owner_idx
108
+ ON public.payee_accounts (tenant_id, owner_type, owner_id);
109
+
110
+ -- Uma principal por dono. Sem isto, duas linhas marcadas como padrão fazem a
111
+ -- tela mostrar uma e a remessa pagar a outra.
112
+ CREATE UNIQUE INDEX IF NOT EXISTS payee_accounts_one_default_uidx
113
+ ON public.payee_accounts (tenant_id, owner_type, owner_id)
114
+ WHERE is_default;
115
+
116
+ ALTER TABLE public.payee_accounts ENABLE ROW LEVEL SECURITY;
117
+ ALTER TABLE public.payee_accounts FORCE ROW LEVEL SECURITY;
118
+
119
+ -- Mesmo recorte de `addresses`: quem é da conta vê o que é da conta. Sem a
120
+ -- política de leitura pública que `addresses` tem para o comprador da loja —
121
+ -- dado bancário de terceiro não tem leitor anônimo.
122
+ DROP POLICY IF EXISTS payee_accounts_member_all ON public.payee_accounts;
123
+ CREATE POLICY payee_accounts_member_all ON public.payee_accounts
124
+ TO authenticated
125
+ USING (tenant_id IN (SELECT public.user_tenant_ids()))
126
+ WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
127
+
128
+ DROP TRIGGER IF EXISTS payee_accounts_set_updated_at ON public.payee_accounts;
129
+ CREATE TRIGGER payee_accounts_set_updated_at
130
+ BEFORE UPDATE ON public.payee_accounts
131
+ FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
@@ -0,0 +1,62 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 150_todo_cadastro_tem_observacao_e_etiqueta.sql
3
+ --
4
+ -- `notes` e `tags` nas tabelas de cadastro do núcleo.
5
+ --
6
+ -- ── por que TODO cadastro, e não «quem pedir» ──────────────────────────
7
+ --
8
+ -- O rail direito de uma ficha responde sempre às mesmas quatro perguntas: de
9
+ -- que unidade é, se vale, o que alguém anotou, e como a casa recorta este
10
+ -- registro. Hoje só `people` consegue responder às duas últimas — e o
11
+ -- resultado é que a mesma tela muda de forma conforme o cadastro: o operador
12
+ -- que aprendeu a etiquetar um cliente não acha a etiqueta no serviço, e
13
+ -- conclui que não existe.
14
+ --
15
+ -- Um lugar fixo só é fixo se estiver em todos.
16
+ --
17
+ -- ── etiqueta é recorte, não classificação ──────────────────────────────
18
+ --
19
+ -- `categories` já existe para classificar, e não é substituta: a categoria diz
20
+ -- O QUE a coisa é (uma taxonomia que alguém manteve), a etiqueta diz como ESTA
21
+ -- casa a recorta, hoje, para um fim que o produto não previu — «sem estoque no
22
+ -- fornecedor», «promoção de julho», «serviço que a Ana faz». Sem ela isso vai
23
+ -- para as observações, onde não filtra e não conta.
24
+ --
25
+ -- `text[]` e não uma tabela de junção: etiqueta é escrita e lida junto com a
26
+ -- linha, sempre, e um índice GIN responde «quem tem esta» sem uma segunda
27
+ -- tabela para manter. É a mesma escolha que `people.tags` já tinha feito.
28
+ -- ---------------------------------------------------------------------------
29
+
30
+ ALTER TABLE public.categories
31
+ ADD COLUMN IF NOT EXISTS notes text,
32
+ ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[];
33
+
34
+ ALTER TABLE public.products
35
+ ADD COLUMN IF NOT EXISTS notes text;
36
+
37
+ -- `public.services` SÓ SE ELA EXISTIR.
38
+ --
39
+ -- O espelho foi aposentado pela 191 do spine — um serviço é um produto de
40
+ -- `kind='service'`. Num pool antigo a tabela continua lá e precisa da coluna;
41
+ -- num banco levantado do zero ela não nasce, e um `ALTER TABLE` direto derruba
42
+ -- a cadeia inteira em `relation "public.services" does not exist`. Foi o que o
43
+ -- db-from-zero pegou.
44
+ DO $servicos$
45
+ BEGIN
46
+ IF to_regclass('public.services') IS NOT NULL THEN
47
+ EXECUTE 'ALTER TABLE public.services ADD COLUMN IF NOT EXISTS notes text';
48
+ EXECUTE 'CREATE INDEX IF NOT EXISTS services_tags_gin ON public.services USING gin (tags)';
49
+ END IF;
50
+ END $servicos$;
51
+
52
+ -- `products.tags` e `services.tags` já existem — quem chegou primeiro estava
53
+ -- certo, e isto só empata o resto.
54
+
55
+ COMMENT ON COLUMN public.categories.tags IS
56
+ 'Como ESTA casa recorta o registro, para um fim que o produto não previu. Diferente de uma categoria, que diz o que a coisa É.';
57
+
58
+ -- «Quem tem esta etiqueta» sem varrer a tabela. A coluna nasce para filtrar;
59
+ -- sem índice ela seria um campo bonito que trava a lista no primeiro uso.
60
+ CREATE INDEX IF NOT EXISTS categories_tags_gin ON public.categories USING gin (tags);
61
+ CREATE INDEX IF NOT EXISTS products_tags_gin ON public.products USING gin (tags);
62
+ CREATE INDEX IF NOT EXISTS people_tags_gin ON public.people USING gin (tags);
@@ -0,0 +1,35 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 151_a_parceria_e_uma_especie_de_pessoa.sql
3
+ --
4
+ -- `partner` entra no censo de espécies de pessoa.
5
+ --
6
+ -- ── o sintoma ──────────────────────────────────────────────────────────
7
+ --
8
+ -- Salvar uma parceria devolvia:
9
+ --
10
+ -- insert or update on table "people" violates foreign key constraint
11
+ -- "people_kind_registered"
12
+ --
13
+ -- O cadastro de Parcerias sempre gravou `people.kind = 'partner'` — está no
14
+ -- `archetypeKind` da entidade desde que ela existe —, e `person_kinds` nunca
15
+ -- teve a linha. Ou seja: o cadastro NUNCA conseguiu criar um registro. Não é
16
+ -- regressão; é uma tela que nasceu sem porta e ninguém tinha tentado abrir.
17
+ --
18
+ -- ── e por que o censo não pegou ────────────────────────────────────────
19
+ --
20
+ -- `check-kind-registry.mjs` existe exatamente para isto, e passava limpo: ele
21
+ -- varre os arquivos SQL de cada plugin procurando `INSERT INTO people (kind)
22
+ -- VALUES ('…')`. Só que NENHUM cadastro do produto escreve assim — quem grava
23
+ -- é o CRUD genérico, a partir do `archetypeKind` declarado em TypeScript. O
24
+ -- verificador cobria o caminho que quase ninguém usa e deixava de fora o
25
+ -- principal. O script foi estendido junto com esta migration.
26
+ --
27
+ -- `plugin-registry` não tem migrations próprias, e por isso a declaração mora
28
+ -- aqui, ao lado das outras espécies de pessoa do núcleo (contact, supplier,
29
+ -- staff), que também são dele.
30
+ -- ---------------------------------------------------------------------------
31
+
32
+ SELECT public.register_kind(
33
+ 'person', 'partner', 'base', NULL, NULL,
34
+ 'Quem indica cliente ou divide receita com a casa — a academia ao lado, o hotel, o dentista que encaminha. Pessoa como as outras: tem contato, endereço e conta para receber.'
35
+ );
@@ -0,0 +1,31 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 152 — Profissão e espécie de pet entram no censo de categorias.
3
+ --
4
+ -- Dois cadastros que o V1 tem e o V2 não tinha:
5
+ --
6
+ -- `profession` — `professions` do V1. O que o profissional FAZ, escolhido de
7
+ -- uma lista que a casa mantém. Hoje o V2 pergunta em texto
8
+ -- livre, e «Esteticista», «esteticista» e «Esteticista(a)»
9
+ -- viram três profissões na hora de montar uma escala ou
10
+ -- filtrar quem pode executar um serviço.
11
+ --
12
+ -- `pet_species` — `pet_species` do V1. Cachorro, gato, ave. Só aparece em
13
+ -- casa que atende pet, e é por isso que é catálogo por conta
14
+ -- e não lista fixa: um hotel para aves não quer «cachorro»
15
+ -- na primeira posição.
16
+ --
17
+ -- As duas são CATEGORIAS: nome, ordem, ativo. Não merecem tabela própria —
18
+ -- `categories` existe exatamente para as taxonomias que cada casa mantém, e
19
+ -- foi por não usá-la que o V1 acabou com uma tabela de cinco colunas por
20
+ -- vocabulário.
21
+ -- ---------------------------------------------------------------------------
22
+
23
+ SELECT public.register_kind(
24
+ 'category', 'profession', 'base', NULL, NULL,
25
+ 'O que o profissional faz — a lista que a casa mantém, para a escala e o filtro de quem executa cada serviço não dependerem de como alguém digitou.'
26
+ );
27
+
28
+ SELECT public.register_kind(
29
+ 'category', 'pet_species', 'base', NULL, NULL,
30
+ 'Espécie de animal atendido (cachorro, gato, ave). Catálogo por conta: um hotel para aves não quer cachorro na primeira posição.'
31
+ );
@@ -0,0 +1,94 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 153_a_sala_nao_e_a_filial.sql
3
+ --
4
+ -- `public.rooms` — a SALA onde se atende, dentro de uma unidade.
5
+ --
6
+ -- ── o conflito que esta migration resolve ──────────────────────────────
7
+ --
8
+ -- O V2 chamava de «Local de Atendimento» a FILIAL (`v_units`). No V1, Local
9
+ -- Operacional é outra coisa: a sala, a cadeira, o box — tem `unit_id`, um
10
+ -- grupo e uma LOTAÇÃO (`max_simultaneous_clients`), e é o que o cadastro de
11
+ -- Equipamentos referencia («este aparelho está na Sala 2»).
12
+ --
13
+ -- Dois cadastros com o mesmo nome e granularidades diferentes: quem procurava
14
+ -- a sala achava a filial, e a agenda não tinha como dizer que duas clientes
15
+ -- não cabem na mesma cadeira ao mesmo tempo.
16
+ --
17
+ -- ── por que tabela, e não uma unidade «filha» ──────────────────────────
18
+ --
19
+ -- `v_units` já tem hierarquia (região → unidade), e a tentação é pendurar a
20
+ -- sala como um terceiro nível. Não serve: uma unidade é onde a RLS corta, onde
21
+ -- o caixa fecha e onde o estoque mora. Uma sala não corta nada disso — ela não
22
+ -- tem caixa, não tem estoque e não recorta permissão. Torná-la unidade faria
23
+ -- cada sala multiplicar o escopo de tudo que hoje é por unidade.
24
+ --
25
+ -- ── lotação ────────────────────────────────────────────────────────────
26
+ --
27
+ -- `capacity` é o que o V1 chama de «máximo de clientes simultâneos»: uma sala
28
+ -- de banho tem uma cadeira, uma sala de bronzeamento tem quatro. É o número
29
+ -- que impede a agenda de marcar cinco pessoas às 14h onde cabem quatro — e
30
+ -- sem ele a sala é só um rótulo.
31
+ -- ---------------------------------------------------------------------------
32
+
33
+ CREATE TABLE IF NOT EXISTS public.rooms (
34
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
35
+ tenant_id uuid NOT NULL,
36
+
37
+ -- A unidade em que a sala fica. NOT NULL: uma sala sem prédio não existe, e
38
+ -- é por `unit_id` que a agenda e o equipamento a alcançam.
39
+ unit_id uuid NOT NULL,
40
+
41
+ name text NOT NULL,
42
+ code text,
43
+
44
+ -- O grupo é como a casa junta salas parecidas («Estética», «Banho e tosa»)
45
+ -- para escalar e para relatório. `categories` com kind `location_group` já
46
+ -- existe e serve — não vale uma tabela para uma palavra.
47
+ group_id uuid,
48
+
49
+ -- Quantas pessoas cabem ao mesmo tempo. 1 é o caso comum (uma cadeira).
50
+ capacity integer DEFAULT 1 NOT NULL,
51
+
52
+ notes text,
53
+ tags text[] DEFAULT '{}'::text[],
54
+ is_active boolean DEFAULT true NOT NULL,
55
+ custom_fields jsonb DEFAULT '{}'::jsonb NOT NULL,
56
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
57
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
58
+ created_by uuid,
59
+
60
+ CONSTRAINT rooms_pkey PRIMARY KEY (id),
61
+ CONSTRAINT rooms_capacity_positive CHECK (capacity >= 1),
62
+ CONSTRAINT rooms_group_fk FOREIGN KEY (group_id) REFERENCES public.categories(id) ON DELETE SET NULL
63
+ );
64
+
65
+ COMMENT ON TABLE public.rooms IS
66
+ 'A sala, cadeira ou box onde se atende, DENTRO de uma unidade. Não é unidade: não tem caixa, não tem estoque e não recorta permissão — o que ela tem é lotação.';
67
+
68
+ COMMENT ON COLUMN public.rooms.capacity IS
69
+ 'Quantas pessoas cabem ao mesmo tempo. É o que impede a agenda de marcar cinco às 14h onde cabem quatro.';
70
+
71
+ CREATE INDEX IF NOT EXISTS rooms_unit_idx ON public.rooms (tenant_id, unit_id) WHERE is_active;
72
+ CREATE INDEX IF NOT EXISTS rooms_tags_gin ON public.rooms USING gin (tags);
73
+
74
+ ALTER TABLE public.rooms ENABLE ROW LEVEL SECURITY;
75
+ ALTER TABLE public.rooms FORCE ROW LEVEL SECURITY;
76
+
77
+ DROP POLICY IF EXISTS rooms_member_all ON public.rooms;
78
+ CREATE POLICY rooms_member_all ON public.rooms
79
+ TO authenticated
80
+ USING (tenant_id IN (SELECT public.user_tenant_ids()))
81
+ WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
82
+
83
+ DROP TRIGGER IF EXISTS rooms_set_updated_at ON public.rooms;
84
+ CREATE TRIGGER rooms_set_updated_at
85
+ BEFORE UPDATE ON public.rooms
86
+ FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
87
+
88
+ -- O EQUIPAMENTO passa a poder dizer em que sala está. Coluna nova e nula: os
89
+ -- aparelhos já cadastrados continuam sem sala, que é a verdade sobre eles.
90
+ ALTER TABLE public.products
91
+ ADD COLUMN IF NOT EXISTS room_id uuid REFERENCES public.rooms(id) ON DELETE SET NULL;
92
+
93
+ COMMENT ON COLUMN public.products.room_id IS
94
+ 'Para `kind = asset`: em que sala o equipamento está. Ver a 153.';
@@ -0,0 +1,33 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 154 — `payee_accounts` e `rooms` declaram que são da BASE.
3
+ --
4
+ -- As duas nasceram sem prefixo, que é o certo: a regra (#285, ADR 0011) diz
5
+ -- que `plg_x_*` marca plugin e um nome NU em `public` é da base. E as duas
6
+ -- foram criadas em `packages/db/migrations`, que é a base.
7
+ --
8
+ -- O que ficou faltando foi a outra metade da regra. `app.base_owned_tables` é
9
+ -- a lista que o verificador consulta para RECUSAR um plugin que tente criar um
10
+ -- nome da base — foi ela que nasceu da vez em que plugin-forms criou
11
+ -- `public.documents`, o financeiro criou `public.sequences` e a loja criou
12
+ -- `public.shipping_zones`: três tabelas que leem como base e não são.
13
+ --
14
+ -- Sem a linha aqui, `public.rooms` está protegido por convenção outra vez, que
15
+ -- é exatamente o que essa lista existe para substituir.
16
+ --
17
+ -- ── e por que as duas são da base, e não de um plugin ──────────────────
18
+ --
19
+ -- `rooms`: `public.products.room_id` aponta para ela — uma coluna de tabela da
20
+ -- base. Se a sala fosse de um plugin, a base dependeria dele para existir. E
21
+ -- `appointments`, `schedules` e `services` já são base: atendimento presencial
22
+ -- não é um módulo opcional da espinha, é parte dela.
23
+ --
24
+ -- `payee_accounts`: é alcançada por `people` (base) e usada por três plugins
25
+ -- diferentes — registry, workforce e financial. Uma tabela que três plugins
26
+ -- leem e nenhum é dono é, por definição, da base.
27
+ -- ---------------------------------------------------------------------------
28
+
29
+ INSERT INTO app.base_owned_tables (table_name, note)
30
+ VALUES
31
+ ('payee_accounts', 'Para onde o dinheiro SAI: conta e PIX de quem a casa paga. Não é conta do razão.'),
32
+ ('rooms', 'A sala, cadeira ou box onde se atende, dentro de uma unidade. Não é unidade: não tem caixa, estoque nem permissão própria.')
33
+ ON CONFLICT (table_name) DO NOTHING;
@@ -0,0 +1,28 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 155 — o pacote de serviços ganha o rail e entra na lista de nomes da base.
3
+ --
4
+ -- `service_packages` e `service_package_items` JÁ EXISTEM: vieram inteiras do
5
+ -- porte do V1, com preço, sessões e validade. O que nunca existiu foi a TELA —
6
+ -- e por isso elas estavam no quadro de paridade como «não existe», quando o
7
+ -- que faltava era só a porta.
8
+ --
9
+ -- Esta migration faz duas coisas pequenas e nenhuma grande:
10
+ --
11
+ -- 1. `notes` e `tags`, para o pacote ter o mesmo rail de todo cadastro.
12
+ -- 2. a linha em `app.base_owned_tables` — os dois nomes são nus em `public`
13
+ -- e nada declarava que são da base, o mesmo furo que a 154 fechou para
14
+ -- `rooms` e `payee_accounts`.
15
+ -- ---------------------------------------------------------------------------
16
+
17
+ ALTER TABLE public.service_packages
18
+ ADD COLUMN IF NOT EXISTS notes text,
19
+ ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[];
20
+
21
+ CREATE INDEX IF NOT EXISTS service_packages_tags_gin
22
+ ON public.service_packages USING gin (tags);
23
+
24
+ INSERT INTO app.base_owned_tables (table_name, note)
25
+ VALUES
26
+ ('service_packages', 'O conjunto de serviços vendido junto, com preço, número de sessões e validade.'),
27
+ ('service_package_items', 'Que serviços entram num pacote, e quantas vezes cada um.')
28
+ ON CONFLICT (table_name) DO NOTHING;
@@ -0,0 +1,44 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 156_restaurante_nao_vende_servico.sql
3
+ -- ---------------------------------------------------------------------------
4
+ -- Um restaurante não tem "Serviços".
5
+ --
6
+ -- Cadastros abre com onze telas, e duas delas — Serviços e Categorias de
7
+ -- Serviço — são do ofício de quem MARCA HORA: o salão, a clínica, o estúdio.
8
+ -- Numa cozinha elas são duas linhas que nunca recebem um registro, e o preço
9
+ -- não é o espaço: é que quem abre o menu procurando "onde cadastro o prato"
10
+ -- lê "Serviços" antes de ler "Cardápio".
11
+ --
12
+ -- `preset.hide` é o `hideNav()` em forma de dado (083): rota que este ofício
13
+ -- NÃO põe no rail. A tela continua existindo — o dia em que a casa abrir uma
14
+ -- consultoria de cardápio, a rota está lá —, ela só deixa de ser oferecida a
15
+ -- quem nunca a pediu.
16
+ --
17
+ -- ── O que isto NÃO resolve, e onde ──────────────────────────────────────────
18
+ -- O tipo de linha "Serviço" do Financeiro não sai daqui, e não devia: acoplar
19
+ -- o documento a uma rota do menu seria fazer o Financeiro conhecer o desenho
20
+ -- do rail. Ele se resolve sozinho no aplicativo — um catálogo com zero linhas
21
+ -- deixa de virar escolha (ver `useStockedItemTypes`), o que também acerta a
22
+ -- loja e qualquer casa que receba o catálogo do pacote e não o use.
23
+ --
24
+ -- Só o `resto`: o `full` é o aplicativo central, que carrega o ofício de quem
25
+ -- entrar e por isso não pode decidir isto por ninguém.
26
+ --
27
+ -- Idempotente: recalcula o `hide` a partir do que está lá.
28
+ -- ---------------------------------------------------------------------------
29
+
30
+ UPDATE app.apps
31
+ SET preset = jsonb_set(
32
+ preset,
33
+ '{hide}',
34
+ (
35
+ SELECT to_jsonb(array_agg(rota ORDER BY rota))
36
+ FROM (
37
+ SELECT jsonb_array_elements_text(coalesce(preset -> 'hide', '[]'::jsonb)) AS rota
38
+ UNION
39
+ SELECT unnest(ARRAY['/registry/services', '/registry/categories'])
40
+ ) AS rotas
41
+ ),
42
+ true
43
+ )
44
+ WHERE id = 'resto';
@@ -0,0 +1,45 @@
1
+ -- ---------------------------------------------------------------------------
2
+ -- 157_o_que_a_porta_da_unidade_criou_tambem_se_revoga.sql
3
+ --
4
+ -- ── a postura que voltou a ficar vermelha ───────────────────────────────
5
+ --
6
+ -- N15c: anon can execute nothing callable in schema app
7
+ -- (trigger functions cannot be called directly)
8
+ --
9
+ -- A regra está escrita na 088, e é a mesma de sempre: **função nova nasce com
10
+ -- `EXECUTE` para PUBLIC, e `anon` herda dali**. Não é preciso conceder nada
11
+ -- para abrir a porta — é preciso REVOGAR para fechá-la, e quem esquece não vê
12
+ -- erro nenhum.
13
+ --
14
+ -- Três migrations desta onda criaram função em `app` e nenhuma revogou:
15
+ --
16
+ -- 140 app.trg_v_units_write a porta de ESCRITA da vista de unidades
17
+ -- 141 app.parse_legacy_address o endereço antigo virando estruturado
18
+ -- 142 app.trg_v_units_write (redefinida)
19
+ --
20
+ -- `trg_v_units_write` é função de GATILHO, e é exatamente o caso que o nome da
21
+ -- postura cita: gatilho não se chama direto. Ele roda como dono da vista, com
22
+ -- `SECURITY DEFINER` — deixá-lo executável por `anon` é oferecer a porta de
23
+ -- escrita da unidade a quem nem entrou na casa.
24
+ --
25
+ -- Por `proname` e não por assinatura: a 142 redefine a mesma função, e listar
26
+ -- a assinatura deixaria a versão anterior para trás caso ela sobreviva em
27
+ -- algum pool. O laço pega todas as sobrecargas que existirem.
28
+ --
29
+ -- Idempotente e replay-safe: revogar duas vezes é no-op, e um pool onde a
30
+ -- função ainda não existe simplesmente não entra no laço.
31
+ -- ---------------------------------------------------------------------------
32
+
33
+ DO $$
34
+ DECLARE v_fn text;
35
+ BEGIN
36
+ FOR v_fn IN
37
+ SELECT p.oid::regprocedure::text
38
+ FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
39
+ WHERE n.nspname = 'app'
40
+ AND p.proname IN ('trg_v_units_write', 'parse_legacy_address')
41
+ LOOP
42
+ EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC, anon, authenticated', v_fn);
43
+ EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO service_role', v_fn);
44
+ END LOOP;
45
+ END $$;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "fayz": {
4
4
  "status": "beta"
5
5
  },
6
- "version": "0.18.2",
6
+ "version": "0.20.0",
7
7
  "description": "Fayz SDK database layer — Drizzle schema primitives, spine references, and migration helpers shared across plugins.",
8
8
  "type": "module",
9
9
  "sideEffects": false,