@fayz-ai/db 0.20.1 → 0.22.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.
- package/migrations/004_the_projections_get_a_successor.sql +23 -1
- package/migrations/162_o_walter_liga_a_lista_da_vez.sql +92 -0
- package/migrations/164_checkin_e_confirmacoes_vieram_com_a_agenda.sql +29 -0
- package/migrations/165_unit_address_geocoding.sql +258 -0
- 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/package.json +1 -1
|
@@ -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;
|