@fayz-ai/db 0.18.2 → 0.20.1
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.
- package/migrations/136_a_baixa_do_espelho_e_do_porte_nao_do_v2.sql +204 -0
- package/migrations/137_a_retirada_e_um_canal_de_venda.sql +43 -0
- package/migrations/138_o_produto_tem_cor_e_a_conta_so_a_veste_se_pagar.sql +606 -0
- package/migrations/139_a_equipe_e_um_agrupamento_declarado.sql +50 -0
- package/migrations/140_a_unidade_se_escreve_pela_mesma_porta_que_se_le.sql +130 -0
- package/migrations/141_o_endereco_da_unidade_mora_na_tabela_de_enderecos.sql +203 -0
- package/migrations/142_a_coordenada_e_do_endereco_nao_da_unidade.sql +157 -0
- package/migrations/143_o_preset_para_de_nomear_telas_que_nao_existem.sql +29 -0
- package/migrations/144_marketing_e_um_botao_nao_uma_gaveta.sql +46 -0
- package/migrations/145_campanhas_e_templates_somem_em_toda_conta.sql +36 -0
- package/migrations/146_a_clinica_ve_as_proprias_analises.sql +31 -0
- package/migrations/147_tres_saloes_desligam_o_rh.sql +30 -0
- package/migrations/148_servicos_ganham_a_permissao_que_nunca_tiveram.sql +37 -0
- package/migrations/149_a_conta_do_fornecedor_nao_e_conta_da_casa.sql +131 -0
- package/migrations/150_todo_cadastro_tem_observacao_e_etiqueta.sql +62 -0
- package/migrations/151_a_parceria_e_uma_especie_de_pessoa.sql +35 -0
- package/migrations/152_a_profissao_e_a_especie_de_pet_sao_catalogos.sql +31 -0
- package/migrations/153_a_sala_nao_e_a_filial.sql +94 -0
- package/migrations/154_as_duas_tabelas_novas_dizem_de_quem_sao.sql +33 -0
- package/migrations/155_o_pacote_tem_observacao_e_etiqueta_e_diz_de_quem_e.sql +28 -0
- package/migrations/156_restaurante_nao_vende_servico.sql +44 -0
- package/migrations/157_o_que_a_porta_da_unidade_criou_tambem_se_revoga.sql +45 -0
- package/migrations/158_a_producao_e_do_restaurante_e_mora_no_estoque.sql +129 -0
- package/migrations/159_a_loja_tambem_confundia_o_que_vende_com_o_que_guarda.sql +98 -0
- package/migrations/161_vender_e_falar_com_quem_ainda_nao_comprou_sao_o_mesmo_assunto.sql +73 -0
- 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 $$;
|