@fayz-ai/db 0.18.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/migrations/135_a_pessoa_diz_em_quais_unidades_esta_disponivel.sql +140 -0
- 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/package.json +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
-- A supplier, customer or other non-login Person may be shared with selected
|
|
2
|
+
-- units. The grants live in app.*, so the browser gets a narrow public door
|
|
3
|
+
-- instead of direct access to the authorization schema.
|
|
4
|
+
|
|
5
|
+
-- The importer already preserved these V1 fields in metadata. Lift them into
|
|
6
|
+
-- the declared V2 fields without overwriting anything edited in V2.
|
|
7
|
+
UPDATE public.people p
|
|
8
|
+
SET custom_fields = jsonb_strip_nulls(jsonb_build_object(
|
|
9
|
+
'serviceProviderType', nullif(p.metadata #>> '{contact,service_provider_type}', ''),
|
|
10
|
+
'registrationNumber', nullif(p.metadata #>> '{legal,state_registration}', '')
|
|
11
|
+
)) || p.custom_fields
|
|
12
|
+
WHERE p.kind = 'supplier'
|
|
13
|
+
AND (
|
|
14
|
+
(NOT (p.custom_fields ? 'serviceProviderType') AND nullif(p.metadata #>> '{contact,service_provider_type}', '') IS NOT NULL)
|
|
15
|
+
OR
|
|
16
|
+
(NOT (p.custom_fields ? 'registrationNumber') AND nullif(p.metadata #>> '{legal,state_registration}', '') IS NOT NULL)
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
-- Suppliers imported before the normalized address book kept one legacy main
|
|
20
|
+
-- address on people. Copy complete addresses once; partial addresses remain on
|
|
21
|
+
-- the Person instead of inventing required postal/city/state values.
|
|
22
|
+
INSERT INTO public.addresses (
|
|
23
|
+
tenant_id, owner_type, owner_id, kind, label, recipient, phone, postal_code,
|
|
24
|
+
street, number, complement, district, city, state, country, is_default, metadata
|
|
25
|
+
)
|
|
26
|
+
SELECT p.tenant_id, 'person', p.id, 'both', 'Principal', p.name, p.phone,
|
|
27
|
+
p.postal_code, p.address,
|
|
28
|
+
nullif(p.metadata #>> '{address,number}', ''),
|
|
29
|
+
nullif(p.metadata #>> '{address,complement}', ''),
|
|
30
|
+
nullif(p.metadata #>> '{address,neighborhood}', ''),
|
|
31
|
+
p.city, p.state, coalesce(nullif(p.country, ''), 'BR'), true,
|
|
32
|
+
jsonb_build_object('source', 'people_legacy_address')
|
|
33
|
+
FROM public.people p
|
|
34
|
+
WHERE p.kind = 'supplier'
|
|
35
|
+
AND nullif(p.postal_code, '') IS NOT NULL
|
|
36
|
+
AND nullif(p.address, '') IS NOT NULL
|
|
37
|
+
AND nullif(p.city, '') IS NOT NULL
|
|
38
|
+
AND nullif(p.state, '') IS NOT NULL
|
|
39
|
+
AND NOT EXISTS (
|
|
40
|
+
SELECT 1 FROM public.addresses a
|
|
41
|
+
WHERE a.owner_type = 'person' AND a.owner_id = p.id
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE OR REPLACE FUNCTION public.people_available_unit_ids(p_person uuid)
|
|
45
|
+
RETURNS uuid[]
|
|
46
|
+
LANGUAGE plpgsql
|
|
47
|
+
STABLE
|
|
48
|
+
SECURITY DEFINER
|
|
49
|
+
SET search_path TO ''
|
|
50
|
+
AS $$
|
|
51
|
+
DECLARE
|
|
52
|
+
v_tenant uuid := public.people_assert_scope('people.manage');
|
|
53
|
+
v_units uuid[];
|
|
54
|
+
BEGIN
|
|
55
|
+
IF NOT EXISTS (
|
|
56
|
+
SELECT 1 FROM public.people p
|
|
57
|
+
WHERE p.tenant_id = v_tenant AND p.id = p_person AND p.merged_into_id IS NULL
|
|
58
|
+
) THEN
|
|
59
|
+
RAISE EXCEPTION 'people_available_unit_ids: person not found in this tenant'
|
|
60
|
+
USING ERRCODE = 'P0002';
|
|
61
|
+
END IF;
|
|
62
|
+
|
|
63
|
+
SELECT coalesce(array_agg(g.subject_id ORDER BY g.subject_id), '{}'::uuid[])
|
|
64
|
+
INTO v_units
|
|
65
|
+
FROM app.resource_grants g
|
|
66
|
+
WHERE g.tenant_id = v_tenant
|
|
67
|
+
AND g.resource_type = 'people.person'
|
|
68
|
+
AND g.resource_id = p_person
|
|
69
|
+
AND g.relation = 'available_in'
|
|
70
|
+
AND g.subject_type = 'unit';
|
|
71
|
+
|
|
72
|
+
RETURN v_units;
|
|
73
|
+
END $$;
|
|
74
|
+
|
|
75
|
+
CREATE OR REPLACE FUNCTION public.people_set_available_units(
|
|
76
|
+
p_person uuid,
|
|
77
|
+
p_unit_ids uuid[] DEFAULT '{}'::uuid[]
|
|
78
|
+
)
|
|
79
|
+
RETURNS uuid[]
|
|
80
|
+
LANGUAGE plpgsql
|
|
81
|
+
SECURITY DEFINER
|
|
82
|
+
SET search_path TO ''
|
|
83
|
+
AS $$
|
|
84
|
+
DECLARE
|
|
85
|
+
v_tenant uuid := public.people_assert_scope('people.manage');
|
|
86
|
+
v_unit_ids uuid[];
|
|
87
|
+
BEGIN
|
|
88
|
+
IF NOT EXISTS (
|
|
89
|
+
SELECT 1 FROM public.people p
|
|
90
|
+
WHERE p.tenant_id = v_tenant AND p.id = p_person AND p.merged_into_id IS NULL
|
|
91
|
+
) THEN
|
|
92
|
+
RAISE EXCEPTION 'people_set_available_units: person not found in this tenant'
|
|
93
|
+
USING ERRCODE = 'P0002';
|
|
94
|
+
END IF;
|
|
95
|
+
|
|
96
|
+
SELECT coalesce(array_agg(DISTINCT requested.unit_id ORDER BY requested.unit_id), '{}'::uuid[])
|
|
97
|
+
INTO v_unit_ids
|
|
98
|
+
FROM unnest(coalesce(p_unit_ids, '{}'::uuid[])) AS requested(unit_id);
|
|
99
|
+
|
|
100
|
+
IF EXISTS (
|
|
101
|
+
SELECT 1
|
|
102
|
+
FROM unnest(v_unit_ids) AS requested(unit_id)
|
|
103
|
+
LEFT JOIN app.units u
|
|
104
|
+
ON u.id = requested.unit_id AND u.tenant_id = v_tenant
|
|
105
|
+
WHERE u.id IS NULL OR NOT app.has_unit(requested.unit_id)
|
|
106
|
+
) THEN
|
|
107
|
+
RAISE EXCEPTION 'people_set_available_units: unit not found in this tenant'
|
|
108
|
+
USING ERRCODE = '42501';
|
|
109
|
+
END IF;
|
|
110
|
+
|
|
111
|
+
DELETE FROM app.resource_grants g
|
|
112
|
+
WHERE g.tenant_id = v_tenant
|
|
113
|
+
AND g.resource_type = 'people.person'
|
|
114
|
+
AND g.resource_id = p_person
|
|
115
|
+
AND g.relation = 'available_in'
|
|
116
|
+
AND g.subject_type = 'unit';
|
|
117
|
+
|
|
118
|
+
INSERT INTO app.resource_grants (
|
|
119
|
+
tenant_id, resource_type, resource_id, relation, subject_type, subject_id, created_by
|
|
120
|
+
)
|
|
121
|
+
SELECT v_tenant, 'people.person', p_person, 'available_in', 'unit', unit_id, auth.uid()
|
|
122
|
+
FROM unnest(v_unit_ids) AS requested(unit_id);
|
|
123
|
+
|
|
124
|
+
RETURN v_unit_ids;
|
|
125
|
+
END $$;
|
|
126
|
+
|
|
127
|
+
REVOKE ALL ON FUNCTION public.people_available_unit_ids(uuid) FROM PUBLIC;
|
|
128
|
+
REVOKE ALL ON FUNCTION public.people_set_available_units(uuid, uuid[]) FROM PUBLIC;
|
|
129
|
+
GRANT EXECUTE ON FUNCTION public.people_available_unit_ids(uuid) TO authenticated, service_role;
|
|
130
|
+
GRANT EXECUTE ON FUNCTION public.people_set_available_units(uuid, uuid[]) TO authenticated, service_role;
|
|
131
|
+
|
|
132
|
+
COMMENT ON FUNCTION public.people_available_unit_ids(uuid) IS
|
|
133
|
+
'Units where a Person record is available. Empty means organization-wide.';
|
|
134
|
+
COMMENT ON FUNCTION public.people_set_available_units(uuid, uuid[]) IS
|
|
135
|
+
'Replaces the unit availability of a Person. Empty means organization-wide.';
|
|
136
|
+
|
|
137
|
+
-- PostgREST may keep serving the previous function catalog after DDL. The
|
|
138
|
+
-- explicit notification makes the new RPCs available as soon as this migration
|
|
139
|
+
-- commits instead of waiting for a later cache refresh.
|
|
140
|
+
NOTIFY pgrst, 'reload schema';
|
|
@@ -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
|
+
);
|