@fayz-ai/db 0.21.0 → 0.22.2
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/004_the_projections_get_a_successor.sql +23 -1
- package/migrations/141_o_endereco_da_unidade_mora_na_tabela_de_enderecos.sql +29 -5
- package/migrations/142_a_coordenada_e_do_endereco_nao_da_unidade.sql +28 -4
- package/migrations/162_o_walter_liga_a_lista_da_vez.sql +24 -3
- package/migrations/166_o_pacote_diz_quanto_vale_cada_servico_dentro_dele.sql +336 -0
- package/migrations/167_o_banco_e_do_pais_nao_e_da_casa.sql +590 -0
- package/migrations/168_a_empresa_do_v1_cabe_na_unidade.sql +366 -0
- package/migrations/169_a_configuracao_da_filial_se_le_pela_porta_da_sessao.sql +65 -0
- package/migrations/171_a_validade_informada_nao_se_apaga_sozinha.sql +55 -0
- package/migrations/172_o_pacote_pode_repetir_o_servico.sql +23 -0
- package/package.json +1 -1
|
@@ -42,7 +42,27 @@ CREATE OR REPLACE VIEW public.v_team_members WITH (security_invoker='true') AS
|
|
|
42
42
|
COMMENT ON VIEW public.v_team_members IS
|
|
43
43
|
'Active memberships with the role KEY (#214). Drop-in successor to public.tenant_members: same columns, security_invoker, so app.memberships RLS decides the rows.';
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
-- ── replay: quem alargou a view depois é quem manda na forma dela ──────────
|
|
46
|
+
--
|
|
47
|
+
-- `CREATE OR REPLACE VIEW` sabe ACRESCENTAR coluna no fim e não sabe tirar. As
|
|
48
|
+
-- migrations 141, 142, 168 e 169 alargaram `v_units` depois desta; quando a
|
|
49
|
+
-- cadeia é aplicada do zero e depois REPLAYADA, este statement voltaria a
|
|
50
|
+
-- definir a versão ESTREITA e o Postgres recusa com «cannot drop columns from
|
|
51
|
+
-- view» — derrubando o replay inteiro aqui, no arquivo 4 de 141.
|
|
52
|
+
--
|
|
53
|
+
-- A view NASCE aqui e evolui adiante. Então aqui ela se cria uma vez só: se já
|
|
54
|
+
-- existe, a forma atual é de quem a alargou, e o replay não a desfaz. É o mesmo
|
|
55
|
+
-- raciocínio do backfill fora de ordem, que já custou view estreitada antes.
|
|
56
|
+
--
|
|
57
|
+
-- Em cluster que já aplicou este arquivo nada muda — o ledger não o roda de
|
|
58
|
+
-- novo. O que muda é que a cadeia passa a sobreviver ao próprio replay.
|
|
59
|
+
DO $v_units$
|
|
60
|
+
BEGIN
|
|
61
|
+
IF to_regclass('public.v_units') IS NOT NULL THEN
|
|
62
|
+
RETURN;
|
|
63
|
+
END IF;
|
|
64
|
+
EXECUTE $ddl$
|
|
65
|
+
CREATE VIEW public.v_units WITH (security_invoker='true') AS
|
|
46
66
|
SELECT u.id,
|
|
47
67
|
u.tenant_id,
|
|
48
68
|
coalesce(u.metadata ->> 'legacy_kind', u.kind) AS kind,
|
|
@@ -79,6 +99,8 @@ CREATE OR REPLACE VIEW public.v_units WITH (security_invoker='true') AS
|
|
|
79
99
|
CASE WHEN u.metadata ->> 'longitude' ~ '^-?[0-9]+(\.[0-9]+)?$'
|
|
80
100
|
THEN (u.metadata ->> 'longitude')::numeric END AS longitude
|
|
81
101
|
FROM app.units u;
|
|
102
|
+
$ddl$;
|
|
103
|
+
END $v_units$;
|
|
82
104
|
|
|
83
105
|
COMMENT ON VIEW public.v_units IS
|
|
84
106
|
'Units in the shape the units screen reads (#214). Drop-in successor to public.locations, deriving the same columns from app.units + metadata as app.compat_sync_unit did.';
|
|
@@ -69,6 +69,22 @@ SELECT u.tenant_id, 'unit', u.id, 'main',
|
|
|
69
69
|
AND (p.street <> '' OR p.city <> '');
|
|
70
70
|
|
|
71
71
|
-- ── 2. a view lê o endereço de onde ele mora ────────────────────────────────
|
|
72
|
+
-- Replay guard. 168 widened v_units with the company columns and rewrote its
|
|
73
|
+
-- writer. Re-running this file over that shape fails (CREATE OR REPLACE VIEW
|
|
74
|
+
-- cannot drop columns) and, if it did not, would put this file's older writer
|
|
75
|
+
-- back over 168's. So once the view carries `legal_entity_id`, the view and
|
|
76
|
+
-- the writer below are history and are skipped; what sits above this block is
|
|
77
|
+
-- idempotent and still runs.
|
|
78
|
+
DO $guard$
|
|
79
|
+
BEGIN
|
|
80
|
+
IF EXISTS (SELECT 1 FROM pg_attribute
|
|
81
|
+
WHERE attrelid = 'public.v_units'::regclass
|
|
82
|
+
AND attname = 'legal_entity_id' AND NOT attisdropped) THEN
|
|
83
|
+
RAISE NOTICE '141: v_units already has the 168 shape; view and writer left as they are';
|
|
84
|
+
RETURN;
|
|
85
|
+
END IF;
|
|
86
|
+
|
|
87
|
+
EXECUTE $s1$
|
|
72
88
|
CREATE OR REPLACE VIEW public.v_units WITH (security_invoker='true') AS
|
|
73
89
|
SELECT u.id,
|
|
74
90
|
u.tenant_id,
|
|
@@ -112,12 +128,16 @@ CREATE OR REPLACE VIEW public.v_units WITH (security_invoker='true') AS
|
|
|
112
128
|
WHERE x.owner_type = 'unit' AND x.owner_id = u.id
|
|
113
129
|
ORDER BY x.is_default DESC, x.created_at
|
|
114
130
|
LIMIT 1
|
|
115
|
-
) a ON true
|
|
131
|
+
) a ON true
|
|
132
|
+
$s1$;
|
|
116
133
|
|
|
134
|
+
EXECUTE $s2$
|
|
117
135
|
COMMENT ON VIEW public.v_units IS
|
|
118
|
-
'Units in the shape the units screen reads (#214), com o endereço vindo de public.addresses (141).'
|
|
136
|
+
'Units in the shape the units screen reads (#214), com o endereço vindo de public.addresses (141).'
|
|
137
|
+
$s2$;
|
|
119
138
|
|
|
120
|
-
-- ── 3. quem escreve o endereço é a tabela de endereços ──────────────────────
|
|
139
|
+
-- ── 3. quem escreve o endereço é a tabela de endereços ──────────────────────
|
|
140
|
+
EXECUTE $s3$
|
|
121
141
|
CREATE OR REPLACE FUNCTION app.trg_v_units_write() RETURNS trigger
|
|
122
142
|
LANGUAGE plpgsql
|
|
123
143
|
SET search_path TO ''
|
|
@@ -197,7 +217,11 @@ BEGIN
|
|
|
197
217
|
|
|
198
218
|
SELECT * INTO NEW FROM public.v_units WHERE id = v_id;
|
|
199
219
|
RETURN NEW;
|
|
200
|
-
END
|
|
220
|
+
END $$
|
|
221
|
+
$s3$;
|
|
201
222
|
|
|
223
|
+
EXECUTE $s4$
|
|
202
224
|
COMMENT ON FUNCTION app.trg_v_units_write() IS
|
|
203
|
-
'Escreve v_units em app.units; o endereço é de public.addresses e não passa por aqui (141).'
|
|
225
|
+
'Escreve v_units em app.units; o endereço é de public.addresses e não passa por aqui (141).'
|
|
226
|
+
$s4$;
|
|
227
|
+
END $guard$;
|
|
@@ -35,6 +35,22 @@ UPDATE app.units u
|
|
|
35
35
|
SET metadata = u.metadata - 'latitude' - 'longitude'
|
|
36
36
|
WHERE u.metadata ? 'latitude' OR u.metadata ? 'longitude';
|
|
37
37
|
|
|
38
|
+
-- Replay guard. 168 widened v_units with the company columns and rewrote its
|
|
39
|
+
-- writer. Re-running this file over that shape fails (CREATE OR REPLACE VIEW
|
|
40
|
+
-- cannot drop columns) and, if it did not, would put this file's older writer
|
|
41
|
+
-- back over 168's. So once the view carries `legal_entity_id`, the view and
|
|
42
|
+
-- the writer below are history and are skipped; what sits above this block is
|
|
43
|
+
-- idempotent and still runs.
|
|
44
|
+
DO $guard$
|
|
45
|
+
BEGIN
|
|
46
|
+
IF EXISTS (SELECT 1 FROM pg_attribute
|
|
47
|
+
WHERE attrelid = 'public.v_units'::regclass
|
|
48
|
+
AND attname = 'legal_entity_id' AND NOT attisdropped) THEN
|
|
49
|
+
RAISE NOTICE '142: v_units already has the 168 shape; view and writer left as they are';
|
|
50
|
+
RETURN;
|
|
51
|
+
END IF;
|
|
52
|
+
|
|
53
|
+
EXECUTE $s1$
|
|
38
54
|
CREATE OR REPLACE VIEW public.v_units WITH (security_invoker='true') AS
|
|
39
55
|
SELECT u.id,
|
|
40
56
|
u.tenant_id,
|
|
@@ -72,11 +88,15 @@ CREATE OR REPLACE VIEW public.v_units WITH (security_invoker='true') AS
|
|
|
72
88
|
WHERE x.owner_type = 'unit' AND x.owner_id = u.id
|
|
73
89
|
ORDER BY x.is_default DESC, x.created_at
|
|
74
90
|
LIMIT 1
|
|
75
|
-
) a ON true
|
|
91
|
+
) a ON true
|
|
92
|
+
$s1$;
|
|
76
93
|
|
|
94
|
+
EXECUTE $s2$
|
|
77
95
|
COMMENT ON VIEW public.v_units IS
|
|
78
|
-
'Units in the shape the units screen reads (#214); endereço e coordenada vêm de public.addresses (141, 142).'
|
|
96
|
+
'Units in the shape the units screen reads (#214); endereço e coordenada vêm de public.addresses (141, 142).'
|
|
97
|
+
$s2$;
|
|
79
98
|
|
|
99
|
+
EXECUTE $s3$
|
|
80
100
|
CREATE OR REPLACE FUNCTION app.trg_v_units_write() RETURNS trigger
|
|
81
101
|
LANGUAGE plpgsql
|
|
82
102
|
SET search_path TO ''
|
|
@@ -151,7 +171,11 @@ BEGIN
|
|
|
151
171
|
|
|
152
172
|
SELECT * INTO NEW FROM public.v_units WHERE id = v_id;
|
|
153
173
|
RETURN NEW;
|
|
154
|
-
END
|
|
174
|
+
END $$
|
|
175
|
+
$s3$;
|
|
155
176
|
|
|
177
|
+
EXECUTE $s4$
|
|
156
178
|
COMMENT ON FUNCTION app.trg_v_units_write() IS
|
|
157
|
-
'Escreve v_units em app.units; endereço e coordenada são de public.addresses (141, 142).'
|
|
179
|
+
'Escreve v_units em app.units; endereço e coordenada são de public.addresses (141, 142).'
|
|
180
|
+
$s4$;
|
|
181
|
+
END $guard$;
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
-- The Beauty app offers the turn queue as an independent module. Walter's is
|
|
2
2
|
-- the first tenant to turn it on; every other salon keeps today's behavior
|
|
3
3
|
-- until an operator enables it from Settings > Modules.
|
|
4
|
+
--
|
|
5
|
+
-- ── por que as escritas do TENANT são condicionais ─────────────────────────
|
|
6
|
+
--
|
|
7
|
+
-- Três linhas aqui apontam para um tenant por uuid, e `tenant_apps`,
|
|
8
|
+
-- `tenant_plugins` e `tenant_nav` têm FK para `public.tenants`. Num banco que
|
|
9
|
+
-- nasce do zero — a cadeia do `db-from-zero`, o banco do bench — esse tenant
|
|
10
|
+
-- não existe, e a FK derrubava a CADEIA INTEIRA no arquivo 162: nada depois
|
|
11
|
+
-- dele chegava a rodar.
|
|
12
|
+
--
|
|
13
|
+
-- `WHERE EXISTS` não muda nada onde a conta existe: lá a linha entra igual. O
|
|
14
|
+
-- que ele faz é não exigir que todo banco do mundo contenha a conta do Walter
|
|
15
|
+
-- para que o resto do produto possa ser aplicado.
|
|
16
|
+
--
|
|
17
|
+
-- (O que é do PRODUTO — o preset do app, as permissões, os papéis-modelo —
|
|
18
|
+
-- segue incondicional, porque é verdade em qualquer banco.)
|
|
4
19
|
|
|
5
20
|
-- Walter's is a salon account. Keep Beauty active so app_config selects the
|
|
6
21
|
-- StudioControl rail even if the tenant also owns a back-office application.
|
|
7
22
|
INSERT INTO app.tenant_apps (tenant_id, app_id, status, source)
|
|
8
|
-
|
|
23
|
+
SELECT t.id, 'beauty', 'active', 'manual'
|
|
24
|
+
FROM public.tenants t
|
|
25
|
+
WHERE t.id = 'c3a8f61d-742b-4e05-9d16-8b47e2f0a5c3'
|
|
9
26
|
ON CONFLICT (tenant_id, app_id)
|
|
10
27
|
DO UPDATE SET status = 'active', source = 'manual', updated_at = now();
|
|
11
28
|
|
|
@@ -59,13 +76,17 @@ SELECT r.tenant_id, r.id, p.permission
|
|
|
59
76
|
ON CONFLICT DO NOTHING;
|
|
60
77
|
|
|
61
78
|
INSERT INTO app.tenant_plugins (tenant_id, plugin_id, facet, status, source)
|
|
62
|
-
|
|
79
|
+
SELECT t.id, 'agenda-turn-queue', '', 'active', 'manual'
|
|
80
|
+
FROM public.tenants t
|
|
81
|
+
WHERE t.id = 'c3a8f61d-742b-4e05-9d16-8b47e2f0a5c3'
|
|
63
82
|
ON CONFLICT (tenant_id, plugin_id, facet)
|
|
64
83
|
DO UPDATE SET status = 'active', source = 'manual', updated_at = now();
|
|
65
84
|
|
|
66
85
|
-- Walter's must see the existing check-in entry even if the tenant previously
|
|
67
86
|
-- hid it while shaping its menu. The Beauty preset already groups it in Agenda.
|
|
68
87
|
INSERT INTO app.tenant_nav (tenant_id, entry_key, hidden)
|
|
69
|
-
|
|
88
|
+
SELECT t.id, '/checkin', false
|
|
89
|
+
FROM public.tenants t
|
|
90
|
+
WHERE t.id = 'c3a8f61d-742b-4e05-9d16-8b47e2f0a5c3'
|
|
70
91
|
ON CONFLICT (tenant_id, entry_key)
|
|
71
92
|
DO UPDATE SET hidden = false, updated_at = now();
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 166 — o pacote diz quanto vale cada serviço DENTRO dele.
|
|
3
|
+
--
|
|
4
|
+
-- A 155 abriu a porta do pacote; o porte trouxe as duas tabelas com preço,
|
|
5
|
+
-- sessões e validade, e parou aí. O que ficou de fora é justamente o que faz um
|
|
6
|
+
-- pacote ser um pacote: cada linha dele tem DOIS preços.
|
|
7
|
+
--
|
|
8
|
+
-- `unit_price` — o valor de tabela do serviço. É a base da COMISSÃO do
|
|
9
|
+
-- profissional, e é o lado esquerdo da conta da economia.
|
|
10
|
+
-- `package_price` — quanto aquele serviço vale DENTRO deste pacote. É o que
|
|
11
|
+
-- o cliente paga por ele, e é o que a nota registra.
|
|
12
|
+
--
|
|
13
|
+
-- Sem os dois, o preço do pacote era um número solto: a tela de venda tinha de
|
|
14
|
+
-- repartir o total entre os serviços por regra de três sobre o preço de tabela
|
|
15
|
+
-- — um palpite razoável, e um palpite. Com eles, a casa decide qual serviço
|
|
16
|
+
-- absorve o desconto, e a comissão sai do valor certo.
|
|
17
|
+
--
|
|
18
|
+
-- ── O PREÇO DO PACOTE PASSA A SER A SOMA, e não um campo digitado ──────────
|
|
19
|
+
--
|
|
20
|
+
-- É o que o V1 faz (`total_price = Σ package_price × quantity`, ver
|
|
21
|
+
-- ServicePackageForm) e é a única forma de os dois nunca divergirem. Aqui os
|
|
22
|
+
-- itens gravam por fora do formulário do pacote — cada linha é uma linha de
|
|
23
|
+
-- outra tabela —, então quem mantém a soma é o BANCO, não a tela.
|
|
24
|
+
--
|
|
25
|
+
-- O pacote SEM itens continua com o preço digitado: ele é um bloco solto de
|
|
26
|
+
-- sessões («10 sessões, escolhe na hora»), e zerá-lo seria apagar o que a casa
|
|
27
|
+
-- cadastrou.
|
|
28
|
+
--
|
|
29
|
+
-- ── A validade em meses, com os dias atrás dela ────────────────────────────
|
|
30
|
+
--
|
|
31
|
+
-- O V1 pergunta MESES e grava os dois (`validity_days = months × 30`). Os dias
|
|
32
|
+
-- já existem aqui e são o que o vencimento vai ler; os meses são a pergunta.
|
|
33
|
+
-- Um trigger mantém o espelho, pela mesma razão da soma: dois campos para a
|
|
34
|
+
-- mesma coisa só sobrevivem se ninguém puder escrever um sem o outro.
|
|
35
|
+
-- ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
-- ── As linhas ──────────────────────────────────────────────────────────────
|
|
38
|
+
ALTER TABLE public.service_package_items
|
|
39
|
+
-- Serviço ou produto. `products` guarda os dois desde a 191, e é o `kind`
|
|
40
|
+
-- dela que diz qual — mas a LINHA do pacote precisa dizer o que foi escolhido
|
|
41
|
+
-- para que a tela ofereça o catálogo certo em cada linha.
|
|
42
|
+
ADD COLUMN IF NOT EXISTS item_type text NOT NULL DEFAULT 'service',
|
|
43
|
+
ADD COLUMN IF NOT EXISTS unit_price numeric(14,2) NOT NULL DEFAULT 0,
|
|
44
|
+
ADD COLUMN IF NOT EXISTS package_price numeric(14,2) NOT NULL DEFAULT 0,
|
|
45
|
+
-- O valor que sai na NOTA quando a sessão é consumida. Vazio = usa o do
|
|
46
|
+
-- pacote; só a casa que emite nota por consumo precisa separá-los.
|
|
47
|
+
ADD COLUMN IF NOT EXISTS fiscal_unit_price numeric(14,2);
|
|
48
|
+
|
|
49
|
+
DO $$
|
|
50
|
+
BEGIN
|
|
51
|
+
ALTER TABLE public.service_package_items
|
|
52
|
+
ADD CONSTRAINT service_package_items_item_type_check
|
|
53
|
+
CHECK (item_type IN ('service', 'product'));
|
|
54
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
55
|
+
END $$;
|
|
56
|
+
|
|
57
|
+
DO $$
|
|
58
|
+
BEGIN
|
|
59
|
+
ALTER TABLE public.service_package_items
|
|
60
|
+
ADD CONSTRAINT service_package_items_prices_check
|
|
61
|
+
CHECK (unit_price >= 0 AND package_price >= 0
|
|
62
|
+
AND (fiscal_unit_price IS NULL OR fiscal_unit_price >= 0));
|
|
63
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
64
|
+
END $$;
|
|
65
|
+
|
|
66
|
+
COMMENT ON COLUMN public.service_package_items.unit_price IS
|
|
67
|
+
'O valor de tabela do serviço — base da comissão do profissional, e o lado esquerdo da economia. Item com 0 não gera comissão (RN do V1).';
|
|
68
|
+
COMMENT ON COLUMN public.service_package_items.package_price IS
|
|
69
|
+
'Quanto este serviço vale DENTRO do pacote. A soma destes × quantity é o preço do pacote.';
|
|
70
|
+
COMMENT ON COLUMN public.service_package_items.fiscal_unit_price IS
|
|
71
|
+
'O valor da nota na hora do consumo. NULL = o mesmo do pacote.';
|
|
72
|
+
|
|
73
|
+
-- ── O que já existia ──────────────────────────────────────────────────────
|
|
74
|
+
--
|
|
75
|
+
-- As linhas antigas nasceram sem preço nenhum, e o preço do pacote delas é um
|
|
76
|
+
-- número real que alguém digitou — em conta de cliente de verdade. Copiar o
|
|
77
|
+
-- catálogo nos dois lados zeraria o desconto e, com o trigger abaixo, REESCREVERIA
|
|
78
|
+
-- o preço do pacote para a soma da tabela: um pacote de R$ 400 viraria R$ 500 na
|
|
79
|
+
-- primeira vez que alguém tocasse numa linha dele.
|
|
80
|
+
--
|
|
81
|
+
-- Então: `unit_price` recebe o catálogo (é o que ele é), e `package_price` recebe
|
|
82
|
+
-- o preço do pacote REPARTIDO entre as linhas, proporcional ao peso de cada uma
|
|
83
|
+
-- na tabela. O total continua sendo exatamente o que era, e o desconto aparece
|
|
84
|
+
-- sozinho — ele sempre esteve lá, só não tinha onde ser escrito.
|
|
85
|
+
WITH alvo AS (
|
|
86
|
+
SELECT i.id,
|
|
87
|
+
i.quantity,
|
|
88
|
+
COALESCE(p.price, 0) AS catalogo,
|
|
89
|
+
sp.price AS preco_pacote,
|
|
90
|
+
sum(COALESCE(p.price, 0) * i.quantity) OVER (PARTITION BY i.package_id, i.tenant_id) AS tabela,
|
|
91
|
+
sum(i.quantity) OVER (PARTITION BY i.package_id, i.tenant_id) AS qtd_total
|
|
92
|
+
FROM public.service_package_items i
|
|
93
|
+
JOIN public.service_packages sp
|
|
94
|
+
ON sp.id = i.package_id AND sp.tenant_id = i.tenant_id
|
|
95
|
+
LEFT JOIN public.products p ON p.id = i.product_id
|
|
96
|
+
WHERE i.unit_price = 0
|
|
97
|
+
AND i.package_price = 0
|
|
98
|
+
)
|
|
99
|
+
UPDATE public.service_package_items i
|
|
100
|
+
SET unit_price = a.catalogo,
|
|
101
|
+
-- Catálogo todo zerado (ou ausente) divide por quantidade: é a única
|
|
102
|
+
-- outra resposta possível, e é melhor que jogar tudo na primeira linha.
|
|
103
|
+
package_price = round(
|
|
104
|
+
(a.preco_pacote * CASE
|
|
105
|
+
WHEN a.tabela > 0 THEN (a.catalogo * a.quantity) / a.tabela
|
|
106
|
+
WHEN a.qtd_total > 0 THEN a.quantity::numeric / a.qtd_total
|
|
107
|
+
ELSE 0
|
|
108
|
+
END) / NULLIF(a.quantity, 0), 2)
|
|
109
|
+
FROM alvo a
|
|
110
|
+
WHERE i.id = a.id;
|
|
111
|
+
|
|
112
|
+
-- ── O cabeçalho ────────────────────────────────────────────────────────────
|
|
113
|
+
ALTER TABLE public.service_packages
|
|
114
|
+
-- A validade é uma ESCOLHA, não um campo vazio: «sem validade» e «ainda não
|
|
115
|
+
-- preenchi» são respostas diferentes, e só o interruptor as separa.
|
|
116
|
+
ADD COLUMN IF NOT EXISTS validity_enabled boolean NOT NULL DEFAULT false,
|
|
117
|
+
ADD COLUMN IF NOT EXISTS validity_months integer,
|
|
118
|
+
ADD COLUMN IF NOT EXISTS commission_percent numeric(5,2) NOT NULL DEFAULT 0,
|
|
119
|
+
-- Vendedor pode mexer no preço na hora da venda. Sem isto, o balcão não tem
|
|
120
|
+
-- como fechar um pacote com o desconto que o dono autorizou de boca.
|
|
121
|
+
ADD COLUMN IF NOT EXISTS allow_price_edit_on_sale boolean NOT NULL DEFAULT false,
|
|
122
|
+
-- O recibo sai sem o valor: pacote de cortesia, ou cliente que não quer o
|
|
123
|
+
-- preço impresso no papel que leva para casa.
|
|
124
|
+
ADD COLUMN IF NOT EXISTS hide_value_on_receipt boolean NOT NULL DEFAULT false,
|
|
125
|
+
-- DERIVADO, e guardado assim mesmo: é o que o V1 faz, e é o número que a
|
|
126
|
+
-- lista de pacotes mostra sem abrir cada um para somar as linhas dele.
|
|
127
|
+
ADD COLUMN IF NOT EXISTS discount_percentage numeric(5,2);
|
|
128
|
+
|
|
129
|
+
DO $$
|
|
130
|
+
BEGIN
|
|
131
|
+
ALTER TABLE public.service_packages
|
|
132
|
+
ADD CONSTRAINT service_packages_validity_months_check
|
|
133
|
+
CHECK (validity_months IS NULL OR validity_months > 0);
|
|
134
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
135
|
+
END $$;
|
|
136
|
+
|
|
137
|
+
DO $$
|
|
138
|
+
BEGIN
|
|
139
|
+
ALTER TABLE public.service_packages
|
|
140
|
+
ADD CONSTRAINT service_packages_commission_percent_check
|
|
141
|
+
CHECK (commission_percent >= 0 AND commission_percent <= 100);
|
|
142
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
143
|
+
END $$;
|
|
144
|
+
|
|
145
|
+
-- O que já tinha dias vira meses, e nasce com a validade ligada: ela estava
|
|
146
|
+
-- preenchida, então alguém a quis.
|
|
147
|
+
UPDATE public.service_packages
|
|
148
|
+
SET validity_enabled = true,
|
|
149
|
+
validity_months = GREATEST(1, round(validity_days / 30.0)::integer)
|
|
150
|
+
WHERE validity_days IS NOT NULL
|
|
151
|
+
AND validity_months IS NULL;
|
|
152
|
+
|
|
153
|
+
-- ---------------------------------------------------------------------------
|
|
154
|
+
-- Os dois espelhos, mantidos pelo banco.
|
|
155
|
+
-- ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
-- `search_path` explícito e qualificado: função sem isso compila no DDL e
|
|
158
|
+
-- estoura na primeira chamada quando o chamador tem outro caminho.
|
|
159
|
+
CREATE OR REPLACE FUNCTION public.service_packages_sync_validity()
|
|
160
|
+
RETURNS trigger
|
|
161
|
+
LANGUAGE plpgsql
|
|
162
|
+
SECURITY INVOKER
|
|
163
|
+
SET search_path = ''
|
|
164
|
+
AS $$
|
|
165
|
+
BEGIN
|
|
166
|
+
IF NEW.validity_enabled AND NEW.validity_months IS NOT NULL THEN
|
|
167
|
+
NEW.validity_days := NEW.validity_months * 30;
|
|
168
|
+
ELSIF NOT NEW.validity_enabled THEN
|
|
169
|
+
NEW.validity_days := NULL;
|
|
170
|
+
NEW.validity_months := NULL;
|
|
171
|
+
END IF;
|
|
172
|
+
RETURN NEW;
|
|
173
|
+
END $$;
|
|
174
|
+
|
|
175
|
+
DROP TRIGGER IF EXISTS service_packages_sync_validity ON public.service_packages;
|
|
176
|
+
CREATE TRIGGER service_packages_sync_validity
|
|
177
|
+
BEFORE INSERT OR UPDATE OF validity_enabled, validity_months
|
|
178
|
+
ON public.service_packages
|
|
179
|
+
FOR EACH ROW EXECUTE FUNCTION public.service_packages_sync_validity();
|
|
180
|
+
|
|
181
|
+
/*
|
|
182
|
+
* A LINHA NOVA NASCE COM O PREÇO DE TABELA nos dois lados.
|
|
183
|
+
*
|
|
184
|
+
* Quem monta um pacote escolhe o serviço e só então decide o desconto: começar
|
|
185
|
+
* com os dois campos em zero obriga a digitar o preço que o catálogo já sabe, e
|
|
186
|
+
* uma linha esquecida em zero não cobra nada e não paga comissão nenhuma.
|
|
187
|
+
*
|
|
188
|
+
* No banco, e não na tela, porque são três superfícies inserindo aqui — a grade
|
|
189
|
+
* do pacote, o importador e o porte — e um padrão que vale só numa delas não é
|
|
190
|
+
* padrão.
|
|
191
|
+
*/
|
|
192
|
+
CREATE OR REPLACE FUNCTION public.service_package_items_default_prices()
|
|
193
|
+
RETURNS trigger
|
|
194
|
+
LANGUAGE plpgsql
|
|
195
|
+
SECURITY INVOKER
|
|
196
|
+
SET search_path = ''
|
|
197
|
+
AS $$
|
|
198
|
+
DECLARE
|
|
199
|
+
v_catalogo numeric;
|
|
200
|
+
BEGIN
|
|
201
|
+
IF NEW.unit_price > 0 OR NEW.package_price > 0 THEN
|
|
202
|
+
RETURN NEW;
|
|
203
|
+
END IF;
|
|
204
|
+
SELECT p.price INTO v_catalogo
|
|
205
|
+
FROM public.products p
|
|
206
|
+
WHERE p.id = NEW.product_id AND p.tenant_id = NEW.tenant_id;
|
|
207
|
+
IF v_catalogo IS NULL OR v_catalogo <= 0 THEN
|
|
208
|
+
RETURN NEW;
|
|
209
|
+
END IF;
|
|
210
|
+
NEW.unit_price := v_catalogo;
|
|
211
|
+
NEW.package_price := v_catalogo;
|
|
212
|
+
RETURN NEW;
|
|
213
|
+
END $$;
|
|
214
|
+
|
|
215
|
+
DROP TRIGGER IF EXISTS service_package_items_default_prices ON public.service_package_items;
|
|
216
|
+
CREATE TRIGGER service_package_items_default_prices
|
|
217
|
+
BEFORE INSERT ON public.service_package_items
|
|
218
|
+
FOR EACH ROW EXECUTE FUNCTION public.service_package_items_default_prices();
|
|
219
|
+
|
|
220
|
+
/*
|
|
221
|
+
* O preço do pacote é a soma das linhas dele.
|
|
222
|
+
*
|
|
223
|
+
* SECURITY DEFINER porque ele escreve no PAI a partir de um trigger no FILHO:
|
|
224
|
+
* quem tem permissão de mexer no item nem sempre tem a de editar o pacote, e
|
|
225
|
+
* uma atualização recusada pela RLS deixaria o preço mentindo. O recorte é o
|
|
226
|
+
* tenant da própria linha — não há parâmetro vindo de fora.
|
|
227
|
+
*/
|
|
228
|
+
CREATE OR REPLACE FUNCTION public.service_packages_resum()
|
|
229
|
+
RETURNS trigger
|
|
230
|
+
LANGUAGE plpgsql
|
|
231
|
+
SECURITY DEFINER
|
|
232
|
+
SET search_path = ''
|
|
233
|
+
AS $$
|
|
234
|
+
DECLARE
|
|
235
|
+
v_package uuid := COALESCE(NEW.package_id, OLD.package_id);
|
|
236
|
+
v_tenant uuid := COALESCE(NEW.tenant_id, OLD.tenant_id);
|
|
237
|
+
v_pacote numeric;
|
|
238
|
+
v_tabela numeric;
|
|
239
|
+
v_linhas integer;
|
|
240
|
+
BEGIN
|
|
241
|
+
SELECT COALESCE(sum(i.package_price * i.quantity), 0),
|
|
242
|
+
COALESCE(sum(i.unit_price * i.quantity), 0),
|
|
243
|
+
count(*)
|
|
244
|
+
INTO v_pacote, v_tabela, v_linhas
|
|
245
|
+
FROM public.service_package_items i
|
|
246
|
+
WHERE i.package_id = v_package
|
|
247
|
+
AND i.tenant_id = v_tenant;
|
|
248
|
+
|
|
249
|
+
-- Pacote sem linha nenhuma mantém o preço digitado: ele é um bloco solto de
|
|
250
|
+
-- sessões, e zerá-lo apagaria o que a casa cadastrou.
|
|
251
|
+
IF v_linhas = 0 THEN
|
|
252
|
+
RETURN NULL;
|
|
253
|
+
END IF;
|
|
254
|
+
|
|
255
|
+
UPDATE public.service_packages
|
|
256
|
+
SET price = v_pacote,
|
|
257
|
+
discount_percentage = CASE
|
|
258
|
+
WHEN v_tabela > 0 AND v_tabela > v_pacote
|
|
259
|
+
THEN round(((v_tabela - v_pacote) / v_tabela) * 100, 2)
|
|
260
|
+
ELSE NULL
|
|
261
|
+
END,
|
|
262
|
+
updated_at = now()
|
|
263
|
+
WHERE id = v_package
|
|
264
|
+
AND tenant_id = v_tenant;
|
|
265
|
+
|
|
266
|
+
RETURN NULL;
|
|
267
|
+
END $$;
|
|
268
|
+
|
|
269
|
+
REVOKE ALL ON FUNCTION public.service_packages_resum() FROM PUBLIC, anon, authenticated;
|
|
270
|
+
|
|
271
|
+
DROP TRIGGER IF EXISTS service_package_items_resum ON public.service_package_items;
|
|
272
|
+
CREATE TRIGGER service_package_items_resum
|
|
273
|
+
AFTER INSERT OR DELETE OR UPDATE OF package_price, unit_price, quantity
|
|
274
|
+
ON public.service_package_items
|
|
275
|
+
FOR EACH ROW EXECUTE FUNCTION public.service_packages_resum();
|
|
276
|
+
|
|
277
|
+
/*
|
|
278
|
+
* O PREÇO DIGITADO NÃO GANHA DA SOMA.
|
|
279
|
+
*
|
|
280
|
+
* O formulário do pacote tem um campo de preço e, abaixo dele, a grade das
|
|
281
|
+
* linhas — que grava por fora. Editar uma linha mudava o preço no banco e o
|
|
282
|
+
* campo na tela continuava com o número velho; salvar o formulário então
|
|
283
|
+
* regravava o velho por cima, e a soma que o cliente vai pagar perdia para um
|
|
284
|
+
* campo que ninguém tocou.
|
|
285
|
+
*
|
|
286
|
+
* Aqui, e não na tela, porque o mesmo vale para o importador, para a API e para
|
|
287
|
+
* qualquer outra porta: com linhas, o preço É a soma delas.
|
|
288
|
+
*/
|
|
289
|
+
CREATE OR REPLACE FUNCTION public.service_packages_price_from_items()
|
|
290
|
+
RETURNS trigger
|
|
291
|
+
LANGUAGE plpgsql
|
|
292
|
+
SECURITY INVOKER
|
|
293
|
+
SET search_path = ''
|
|
294
|
+
AS $$
|
|
295
|
+
DECLARE
|
|
296
|
+
v_pacote numeric;
|
|
297
|
+
v_linhas integer;
|
|
298
|
+
BEGIN
|
|
299
|
+
SELECT COALESCE(sum(i.package_price * i.quantity), 0), count(*)
|
|
300
|
+
INTO v_pacote, v_linhas
|
|
301
|
+
FROM public.service_package_items i
|
|
302
|
+
WHERE i.package_id = NEW.id AND i.tenant_id = NEW.tenant_id;
|
|
303
|
+
|
|
304
|
+
-- Sem linhas o preço é dele: o pacote é um bloco solto de sessões.
|
|
305
|
+
IF v_linhas > 0 THEN
|
|
306
|
+
NEW.price := v_pacote;
|
|
307
|
+
END IF;
|
|
308
|
+
RETURN NEW;
|
|
309
|
+
END $$;
|
|
310
|
+
|
|
311
|
+
DROP TRIGGER IF EXISTS service_packages_price_from_items ON public.service_packages;
|
|
312
|
+
CREATE TRIGGER service_packages_price_from_items
|
|
313
|
+
BEFORE UPDATE OF price ON public.service_packages
|
|
314
|
+
FOR EACH ROW EXECUTE FUNCTION public.service_packages_price_from_items();
|
|
315
|
+
|
|
316
|
+
-- O DESCONTO dos pacotes que já existiam, sem mexer no preço deles.
|
|
317
|
+
--
|
|
318
|
+
-- Recalcular o preço aqui seria trocar um número que alguém digitou pela soma
|
|
319
|
+
-- de um rateio arredondado — um centavo de diferença numa conta de cliente. A
|
|
320
|
+
-- soma passa a valer a partir da próxima vez que alguém editar uma linha, que é
|
|
321
|
+
-- quando ela deixa de ser um palpite.
|
|
322
|
+
UPDATE public.service_packages sp
|
|
323
|
+
SET discount_percentage = CASE
|
|
324
|
+
WHEN t.tabela > 0 AND t.tabela > t.pacote
|
|
325
|
+
THEN round(((t.tabela - t.pacote) / t.tabela) * 100, 2)
|
|
326
|
+
ELSE NULL
|
|
327
|
+
END
|
|
328
|
+
FROM (
|
|
329
|
+
SELECT i.package_id, i.tenant_id,
|
|
330
|
+
sum(i.package_price * i.quantity) AS pacote,
|
|
331
|
+
sum(i.unit_price * i.quantity) AS tabela
|
|
332
|
+
FROM public.service_package_items i
|
|
333
|
+
GROUP BY i.package_id, i.tenant_id
|
|
334
|
+
) t
|
|
335
|
+
WHERE t.package_id = sp.id
|
|
336
|
+
AND t.tenant_id = sp.tenant_id;
|