@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,204 @@
|
|
|
1
|
+
-- A baixa do espelho é do porte, não do V2.
|
|
2
|
+
--
|
|
3
|
+
-- O espelho D-1 desliga o próprio reap na primeira vez que ele funciona, e
|
|
4
|
+
-- depois disso nunca mais propaga uma exclusão. Medido em 28/09/2026, os dois
|
|
5
|
+
-- tenants em regime de espelho estão travados:
|
|
6
|
+
--
|
|
7
|
+
-- mirror_since first_v2_write_at is_mirroring
|
|
8
|
+
-- Espaço Facial 2026-09-19 03:04:20 2026-09-19 22:17:18 false
|
|
9
|
+
-- Pertinho do Céu 2026-09-18 15:15:30 2026-09-20 14:21:58 false
|
|
10
|
+
--
|
|
11
|
+
-- O carimbo do Espaço Facial cai DOIS SEGUNDOS antes da própria rodada do
|
|
12
|
+
-- espelho gravar o cursor do reap (`fayz_etl_meta.mirror_reap_cursor`,
|
|
13
|
+
-- 22:17:20). Não foi ninguém usando o V2: foi o reap.
|
|
14
|
+
--
|
|
15
|
+
-- ── O mecanismo ────────────────────────────────────────────────────────────
|
|
16
|
+
--
|
|
17
|
+
-- `migration.trg_ponr_stamp` existe em `public.orders` e `public.people`, e a
|
|
18
|
+
-- primeira linha dele é a escotilha que todo writer do porte usa:
|
|
19
|
+
--
|
|
20
|
+
-- IF coalesce(current_setting('migration.writer', true), '') = 'on'
|
|
21
|
+
-- THEN RETURN NULL; END IF;
|
|
22
|
+
--
|
|
23
|
+
-- Os 30 e tantos `migration.upsert_*` declaram essa flag. `migration.retire_row`
|
|
24
|
+
-- não declara — e ele escreve: a escada de baixa faz `UPDATE ... SET
|
|
25
|
+
-- cancelled_at` / `deleted_at` / `is_active = false` e, quando a tabela não tem
|
|
26
|
+
-- nenhuma dessas colunas, `DELETE`. Numa venda ou numa pessoa, qualquer um dos
|
|
27
|
+
-- dois acorda o gatilho.
|
|
28
|
+
--
|
|
29
|
+
-- A partir daí o laço fecha sozinho:
|
|
30
|
+
--
|
|
31
|
+
-- reap baixa uma venda → ponr carimba first_v2_write_at = now()
|
|
32
|
+
-- → is_mirroring(tenant) = false
|
|
33
|
+
-- → a rodada seguinte recusa TODA baixa
|
|
34
|
+
-- ('tenant is not in mirror mode')
|
|
35
|
+
--
|
|
36
|
+
-- E o custo aparece longe da causa, como saldo que não fecha. Medido em
|
|
37
|
+
-- 28/09/2026, exclusões do V1 pendentes desde que cada tenant travou:
|
|
38
|
+
--
|
|
39
|
+
-- Espaço Facial 324 financial_movements, 16 invoice_items,
|
|
40
|
+
-- 13 payment_allocations, 26 recurring_bill_occurrences
|
|
41
|
+
-- Pertinho do Céu 244 financial_movements
|
|
42
|
+
--
|
|
43
|
+
-- Numa semana do Pertinho do Céu (unidade 58, 2026-W38) isso são 14 contas a
|
|
44
|
+
-- pagar que o V1 já não tem e o V2 ainda mostra — R$ 18.181,92 de fantasma numa
|
|
45
|
+
-- semana só, e a conferência A × B acusa o V2 com MAIS dinheiro que a origem.
|
|
46
|
+
--
|
|
47
|
+
-- ── O conserto ─────────────────────────────────────────────────────────────
|
|
48
|
+
--
|
|
49
|
+
-- Uma linha: `retire_row` declara o que ele é. A baixa é o porte agindo sobre
|
|
50
|
+
-- uma linha que o porte trouxe, e o ponto-sem-retorno é sobre o V2 assumir a
|
|
51
|
+
-- escrita — duas coisas diferentes que hoje se confundem.
|
|
52
|
+
--
|
|
53
|
+
-- O que este arquivo NÃO faz: rearmar os dois tenants que já travaram. Empurrar
|
|
54
|
+
-- `mirror_since` para depois de um carimbo é decidir que nada escrito naquela
|
|
55
|
+
-- janela vale mais que a origem, e isso é decisão de quem é dono do tenant, não
|
|
56
|
+
-- de uma migration. O operador a toma com o relatório na mão (fayz-etl:
|
|
57
|
+
-- `mirror-rearm`, que lista o que nasceu no V2 antes de mexer na cerca).
|
|
58
|
+
--
|
|
59
|
+
-- Idempotente.
|
|
60
|
+
|
|
61
|
+
CREATE OR REPLACE FUNCTION migration.retire_row(p_batch uuid, p_source_table text, p_source_numeric_id bigint, p_source_uuid uuid, p_reason text DEFAULT 'apagada na origem'::text)
|
|
62
|
+
RETURNS jsonb
|
|
63
|
+
LANGUAGE plpgsql
|
|
64
|
+
SECURITY DEFINER
|
|
65
|
+
SET search_path TO ''
|
|
66
|
+
AS $function$
|
|
67
|
+
DECLARE
|
|
68
|
+
b migration.batches%ROWTYPE;
|
|
69
|
+
l migration.ledger%ROWTYPE;
|
|
70
|
+
pol migration.retire_policy%ROWTYPE;
|
|
71
|
+
v_mirror boolean;
|
|
72
|
+
v_col text; v_val text; v_type text;
|
|
73
|
+
v_n integer := 0;
|
|
74
|
+
v_done integer := 0;
|
|
75
|
+
v_first jsonb;
|
|
76
|
+
BEGIN
|
|
77
|
+
SELECT * INTO b FROM migration.batches WHERE id = p_batch;
|
|
78
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'retire_row: unknown batch %', p_batch USING ERRCODE = 'P0002'; END IF;
|
|
79
|
+
IF b.status <> 'running' THEN
|
|
80
|
+
RAISE EXCEPTION 'retire_row: batch % is % (only a running batch accepts rows)', p_batch, b.status
|
|
81
|
+
USING ERRCODE = '55000';
|
|
82
|
+
END IF;
|
|
83
|
+
PERFORM migration._assert_fence(b.tenant_id);
|
|
84
|
+
|
|
85
|
+
-- A baixa É o porte agindo, e o gatilho do ponto-sem-retorno precisa saber
|
|
86
|
+
-- disso antes de qualquer escrita. Sem esta linha, o primeiro DELETE numa
|
|
87
|
+
-- `public.orders` (ou o `deleted_at` numa `public.people`) carimba
|
|
88
|
+
-- `first_v2_write_at = now()`, `is_mirroring` passa a devolver false, e a
|
|
89
|
+
-- rodada SEGUINTE recusa baixar qualquer coisa — o espelho desliga o próprio
|
|
90
|
+
-- reap na primeira vez que ele funciona. Transaction-local: não vaza.
|
|
91
|
+
PERFORM set_config('migration.writer', 'on', true);
|
|
92
|
+
|
|
93
|
+
v_mirror := coalesce(migration.is_mirroring(b.tenant_id), false);
|
|
94
|
+
IF NOT v_mirror THEN
|
|
95
|
+
-- Baixar fora do regime de espelho seria apagar do V2 um dado que o V2 pode
|
|
96
|
+
-- já ter adotado como seu. A recusa é a trava, não um detalhe de permissão.
|
|
97
|
+
RETURN jsonb_build_object('status', 'refused', 'reason',
|
|
98
|
+
'tenant is not in mirror mode; a retirement outside the mirror regime would '
|
|
99
|
+
'delete data the V2 may already own');
|
|
100
|
+
END IF;
|
|
101
|
+
|
|
102
|
+
-- Uma linha da origem pode ter mais de um destino (a Venda e o seu título).
|
|
103
|
+
-- Baixar um e deixar o outro deixaria o financeiro apontando para nada.
|
|
104
|
+
FOR l IN
|
|
105
|
+
SELECT * FROM migration.ledger
|
|
106
|
+
WHERE source_project = b.source_project
|
|
107
|
+
AND source_table = p_source_table
|
|
108
|
+
AND tenant_id = b.tenant_id
|
|
109
|
+
AND ((p_source_numeric_id IS NOT NULL AND source_numeric_id = p_source_numeric_id)
|
|
110
|
+
OR (p_source_numeric_id IS NULL AND source_uuid = p_source_uuid))
|
|
111
|
+
AND status = 'migrated'
|
|
112
|
+
AND target_table IS NOT NULL
|
|
113
|
+
AND target_id IS NOT NULL
|
|
114
|
+
FOR UPDATE
|
|
115
|
+
LOOP
|
|
116
|
+
v_n := v_n + 1;
|
|
117
|
+
SELECT * INTO pol FROM migration.retire_policy WHERE target_table = l.target_table;
|
|
118
|
+
|
|
119
|
+
IF pol.mode = 'keep' THEN
|
|
120
|
+
UPDATE migration.ledger
|
|
121
|
+
SET status = 'retired', reason = 'retire_policy=keep: ' || pol.reason,
|
|
122
|
+
batch_id = p_batch, updated_at = now()
|
|
123
|
+
WHERE id = l.id;
|
|
124
|
+
v_done := v_done + 1;
|
|
125
|
+
CONTINUE;
|
|
126
|
+
END IF;
|
|
127
|
+
|
|
128
|
+
-- A escada. `deleted_at` e `cancelled_at` primeiro porque preservam a linha
|
|
129
|
+
-- e a história; `is_active` depois; o DELETE só quando não há nada que
|
|
130
|
+
-- signifique "não vale mais" naquela tabela.
|
|
131
|
+
v_col := NULL;
|
|
132
|
+
IF pol.mode = 'soft' THEN
|
|
133
|
+
v_col := pol.column_name; v_val := pol.value;
|
|
134
|
+
ELSIF pol.mode IS NULL THEN
|
|
135
|
+
IF migration._has_column(l.target_table, 'deleted_at') THEN v_col := 'deleted_at'; v_val := NULL;
|
|
136
|
+
ELSIF migration._has_column(l.target_table, 'cancelled_at') THEN v_col := 'cancelled_at'; v_val := NULL;
|
|
137
|
+
ELSIF migration._has_column(l.target_table, 'is_active') THEN v_col := 'is_active'; v_val := 'false';
|
|
138
|
+
ELSIF migration._has_column(l.target_table, 'active') THEN v_col := 'active'; v_val := 'false';
|
|
139
|
+
END IF;
|
|
140
|
+
END IF;
|
|
141
|
+
|
|
142
|
+
BEGIN
|
|
143
|
+
IF v_col IS NOT NULL THEN
|
|
144
|
+
IF v_val IS NULL THEN
|
|
145
|
+
EXECUTE format('UPDATE %s SET %I = now() WHERE id = $1', l.target_table, v_col)
|
|
146
|
+
USING l.target_id;
|
|
147
|
+
ELSE
|
|
148
|
+
SELECT a.atttypid::regtype::text INTO v_type
|
|
149
|
+
FROM pg_attribute a
|
|
150
|
+
WHERE a.attrelid = to_regclass(l.target_table) AND a.attname = v_col;
|
|
151
|
+
EXECUTE format('UPDATE %s SET %I = $1::%s WHERE id = $2', l.target_table, v_col, v_type)
|
|
152
|
+
USING v_val, l.target_id;
|
|
153
|
+
END IF;
|
|
154
|
+
ELSE
|
|
155
|
+
EXECUTE format('DELETE FROM %s WHERE id = $1', l.target_table) USING l.target_id;
|
|
156
|
+
END IF;
|
|
157
|
+
|
|
158
|
+
-- O carimbo é o que faz a baixa auditável depois que o ledger for lido por
|
|
159
|
+
-- outra pessoa: sem ele, uma linha desativada é indistinguível de uma que o
|
|
160
|
+
-- usuário desativou à mão.
|
|
161
|
+
IF v_col IS NOT NULL AND migration._has_column(l.target_table, 'metadata') THEN
|
|
162
|
+
EXECUTE format(
|
|
163
|
+
'UPDATE %s SET metadata = coalesce(metadata, ''{}''::jsonb) || $1 WHERE id = $2',
|
|
164
|
+
l.target_table)
|
|
165
|
+
USING jsonb_build_object('migration', jsonb_build_object(
|
|
166
|
+
'retired_at', now(), 'reason', p_reason, 'batch_id', p_batch)),
|
|
167
|
+
l.target_id;
|
|
168
|
+
END IF;
|
|
169
|
+
|
|
170
|
+
UPDATE migration.ledger
|
|
171
|
+
SET status = 'retired',
|
|
172
|
+
reason = p_reason || coalesce(' (' || v_col || ')', ' (delete)'),
|
|
173
|
+
batch_id = p_batch, updated_at = now()
|
|
174
|
+
WHERE id = l.id;
|
|
175
|
+
PERFORM migration._audit(b.tenant_id, l.target_table, l.target_id,
|
|
176
|
+
jsonb_build_object('action', 'migration.retire', 'reason', p_reason,
|
|
177
|
+
'source_table', p_source_table, 'batch_id', p_batch));
|
|
178
|
+
v_done := v_done + 1;
|
|
179
|
+
EXCEPTION WHEN OTHERS THEN
|
|
180
|
+
-- O caso concreto: uma venda apagada no V1 cujo movimento financeiro no V2
|
|
181
|
+
-- aponta para ela com FK NO ACTION. Cascatear seria apagar o razão junto
|
|
182
|
+
-- por causa de uma exclusão na origem — o motor recusa e escreve o porquê.
|
|
183
|
+
v_first := coalesce(v_first, migration._quarantine(l.id, p_batch, 'state',
|
|
184
|
+
format('não foi possível dar baixa em %s: %s: %s', l.target_table, SQLSTATE, SQLERRM),
|
|
185
|
+
jsonb_build_object('source_table', p_source_table,
|
|
186
|
+
'source_numeric_id', p_source_numeric_id,
|
|
187
|
+
'source_uuid', p_source_uuid, 'reason', p_reason),
|
|
188
|
+
l.target_table));
|
|
189
|
+
END;
|
|
190
|
+
END LOOP;
|
|
191
|
+
|
|
192
|
+
IF v_n = 0 THEN
|
|
193
|
+
-- Exclusão de linha que nunca foi migrada. É o caso NORMAL — tudo que foi
|
|
194
|
+
-- criado e apagado entre duas rodadas, e toda tabela cujo domínio o V2 ainda
|
|
195
|
+
-- não tem. Silencioso de propósito: contá-lo como erro afogaria o relatório.
|
|
196
|
+
RETURN jsonb_build_object('status', 'absent', 'source_table', p_source_table);
|
|
197
|
+
END IF;
|
|
198
|
+
IF v_done = 0 AND v_first IS NOT NULL THEN
|
|
199
|
+
RETURN v_first;
|
|
200
|
+
END IF;
|
|
201
|
+
PERFORM migration._count(p_batch, 'retired');
|
|
202
|
+
RETURN jsonb_build_object('status', 'retired', 'source_table', p_source_table,
|
|
203
|
+
'rows', v_done, 'refused', v_n - v_done);
|
|
204
|
+
END $function$;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- A retirada é um canal de venda.
|
|
2
|
+
--
|
|
3
|
+
-- O PDV do V1 lista quatro canais na trilha, e o operador escolhe entre eles a
|
|
4
|
+
-- cada venda: Salão · Balcão · Retirada · Delivery. Três deles já existem em
|
|
5
|
+
-- `orders.channel`; o quarto, não.
|
|
6
|
+
--
|
|
7
|
+
-- CHECK (channel IS NULL OR channel = ANY (ARRAY[
|
|
8
|
+
-- 'balcao', 'shop', 'dine_in', 'delivery', 'ticketing']))
|
|
9
|
+
--
|
|
10
|
+
-- Sem `retirada` a trilha do V2 pode até desenhar a linha, mas ela nunca terá
|
|
11
|
+
-- conteúdo: toda tentativa de abrir uma comanda nesse canal morre no CHECK —
|
|
12
|
+
-- e morre como violação de constraint, que na tela vira uma comanda que
|
|
13
|
+
-- simplesmente não nasce. É o mesmo formato de falha que a chave estrangeira
|
|
14
|
+
-- de `assignee_id` produziu no porte do PDV: a tela marcava a posição ocupada
|
|
15
|
+
-- e o banco não tinha linha nenhuma.
|
|
16
|
+
--
|
|
17
|
+
-- ── Por que `retirada` e não `takeaway`/`pickup` ───────────────────────────
|
|
18
|
+
--
|
|
19
|
+
-- Os valores que já estão lá são o vocabulário que a casa usa em português
|
|
20
|
+
-- (`balcao`, `dine_in` herdado, `delivery`), e a tela do V1 diz "Retirada".
|
|
21
|
+
-- Introduzir um quinto idioma no mesmo enum faria o filtro do painel de turno
|
|
22
|
+
-- casar com nada — é a armadilha do vocabulário duplo (`cash_register` ×
|
|
23
|
+
-- `cash_physical`) que já custou tempo neste repositório.
|
|
24
|
+
--
|
|
25
|
+
-- ── O que isto NÃO faz ─────────────────────────────────────────────────────
|
|
26
|
+
--
|
|
27
|
+
-- Não converte nenhuma linha existente. Venda antiga não tinha como ser
|
|
28
|
+
-- retirada, e adivinhar a partir de "delivery sem endereço" inventaria fato
|
|
29
|
+
-- que ninguém registrou.
|
|
30
|
+
--
|
|
31
|
+
-- Idempotente: derruba o CHECK pelo nome e recria com a lista inteira, que é a
|
|
32
|
+
-- única forma de ALTER em CHECK no Postgres. Rodar duas vezes deixa o mesmo
|
|
33
|
+
-- constraint de pé.
|
|
34
|
+
|
|
35
|
+
alter table public.orders
|
|
36
|
+
drop constraint if exists orders_channel_check;
|
|
37
|
+
|
|
38
|
+
alter table public.orders
|
|
39
|
+
add constraint orders_channel_check
|
|
40
|
+
check (
|
|
41
|
+
channel is null
|
|
42
|
+
or channel = any (array['balcao', 'shop', 'dine_in', 'delivery', 'ticketing', 'retirada'])
|
|
43
|
+
);
|