@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
|
@@ -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.';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
-- The Beauty app offers the turn queue as an independent module. Walter's is
|
|
2
|
+
-- the first tenant to turn it on; every other salon keeps today's behavior
|
|
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.)
|
|
19
|
+
|
|
20
|
+
-- Walter's is a salon account. Keep Beauty active so app_config selects the
|
|
21
|
+
-- StudioControl rail even if the tenant also owns a back-office application.
|
|
22
|
+
INSERT INTO app.tenant_apps (tenant_id, app_id, status, source)
|
|
23
|
+
SELECT t.id, 'beauty', 'active', 'manual'
|
|
24
|
+
FROM public.tenants t
|
|
25
|
+
WHERE t.id = 'c3a8f61d-742b-4e05-9d16-8b47e2f0a5c3'
|
|
26
|
+
ON CONFLICT (tenant_id, app_id)
|
|
27
|
+
DO UPDATE SET status = 'active', source = 'manual', updated_at = now();
|
|
28
|
+
|
|
29
|
+
UPDATE public.tenants
|
|
30
|
+
SET vertical_id = 'beauty', updated_at = now()
|
|
31
|
+
WHERE id = 'c3a8f61d-742b-4e05-9d16-8b47e2f0a5c3'
|
|
32
|
+
AND vertical_id IS DISTINCT FROM 'beauty';
|
|
33
|
+
|
|
34
|
+
UPDATE app.apps
|
|
35
|
+
SET preset = jsonb_set(
|
|
36
|
+
preset,
|
|
37
|
+
'{modules}',
|
|
38
|
+
coalesce(preset -> 'modules', '[]'::jsonb) || '["agenda-turn-queue"]'::jsonb
|
|
39
|
+
)
|
|
40
|
+
WHERE id = 'beauty'
|
|
41
|
+
AND NOT (coalesce(preset -> 'modules', '[]'::jsonb) ? 'agenda-turn-queue');
|
|
42
|
+
|
|
43
|
+
SELECT app.seed_permissions($$[
|
|
44
|
+
{"key":"agenda.turn_queue.read","plugin":"agenda-turn-queue","description":"See the live turn queue and its audit trail"},
|
|
45
|
+
{"key":"agenda.turn_queue.create","plugin":"agenda-turn-queue","description":"Enter the turn queue"},
|
|
46
|
+
{"key":"agenda.turn_queue.edit","plugin":"agenda-turn-queue","description":"Enter or leave your own turn"},
|
|
47
|
+
{"key":"agenda.turn_queue.manage","plugin":"agenda-turn-queue","description":"Record front-desk exceptions in the turn queue"}
|
|
48
|
+
]$$::jsonb);
|
|
49
|
+
|
|
50
|
+
SELECT app.seed_role_template_rows('salon', 'manager', 'Gerente', 1,
|
|
51
|
+
ARRAY['agenda.turn_queue.read','agenda.turn_queue.edit','agenda.turn_queue.manage']);
|
|
52
|
+
SELECT app.seed_role_template_rows('salon', 'reception', 'Recepção', 2,
|
|
53
|
+
ARRAY['agenda.turn_queue.read','agenda.turn_queue.manage']);
|
|
54
|
+
SELECT app.seed_role_template_rows('salon', 'professional', 'Profissional', 3,
|
|
55
|
+
ARRAY['agenda.turn_queue.read','agenda.turn_queue.edit']);
|
|
56
|
+
SELECT app.seed_role_template_rows('clinic', 'manager', 'Coordenação', 1,
|
|
57
|
+
ARRAY['agenda.turn_queue.read','agenda.turn_queue.edit','agenda.turn_queue.manage']);
|
|
58
|
+
SELECT app.seed_role_template_rows('clinic', 'reception', 'Recepção', 2,
|
|
59
|
+
ARRAY['agenda.turn_queue.read','agenda.turn_queue.manage']);
|
|
60
|
+
SELECT app.seed_role_template_rows('clinic', 'professional', 'Profissional', 3,
|
|
61
|
+
ARRAY['agenda.turn_queue.read','agenda.turn_queue.edit']);
|
|
62
|
+
|
|
63
|
+
INSERT INTO app.role_permissions (tenant_id, role_id, permission)
|
|
64
|
+
SELECT r.tenant_id, r.id, p.permission
|
|
65
|
+
FROM app.roles r
|
|
66
|
+
CROSS JOIN LATERAL (
|
|
67
|
+
SELECT unnest(CASE r.key
|
|
68
|
+
WHEN 'owner' THEN ARRAY['agenda.turn_queue.read','agenda.turn_queue.create','agenda.turn_queue.edit','agenda.turn_queue.manage']
|
|
69
|
+
WHEN 'manager' THEN ARRAY['agenda.turn_queue.read','agenda.turn_queue.edit','agenda.turn_queue.manage']
|
|
70
|
+
WHEN 'reception' THEN ARRAY['agenda.turn_queue.read','agenda.turn_queue.manage']
|
|
71
|
+
WHEN 'professional' THEN ARRAY['agenda.turn_queue.read','agenda.turn_queue.edit']
|
|
72
|
+
ELSE ARRAY[]::text[] END) AS permission
|
|
73
|
+
) p
|
|
74
|
+
WHERE r.tenant_id = 'c3a8f61d-742b-4e05-9d16-8b47e2f0a5c3'
|
|
75
|
+
AND r.key IN ('owner','manager','reception','professional')
|
|
76
|
+
ON CONFLICT DO NOTHING;
|
|
77
|
+
|
|
78
|
+
INSERT INTO app.tenant_plugins (tenant_id, plugin_id, facet, status, source)
|
|
79
|
+
SELECT t.id, 'agenda-turn-queue', '', 'active', 'manual'
|
|
80
|
+
FROM public.tenants t
|
|
81
|
+
WHERE t.id = 'c3a8f61d-742b-4e05-9d16-8b47e2f0a5c3'
|
|
82
|
+
ON CONFLICT (tenant_id, plugin_id, facet)
|
|
83
|
+
DO UPDATE SET status = 'active', source = 'manual', updated_at = now();
|
|
84
|
+
|
|
85
|
+
-- Walter's must see the existing check-in entry even if the tenant previously
|
|
86
|
+
-- hid it while shaping its menu. The Beauty preset already groups it in Agenda.
|
|
87
|
+
INSERT INTO app.tenant_nav (tenant_id, entry_key, hidden)
|
|
88
|
+
SELECT t.id, '/checkin', false
|
|
89
|
+
FROM public.tenants t
|
|
90
|
+
WHERE t.id = 'c3a8f61d-742b-4e05-9d16-8b47e2f0a5c3'
|
|
91
|
+
ON CONFLICT (tenant_id, entry_key)
|
|
92
|
+
DO UPDATE SET hidden = false, updated_at = now();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- 164: check-in e confirmações eram opção de BUILD, nunca um item de plano —
|
|
2
|
+
-- toda conta com Agenda os tinha, de graça. Virar facet (rail-preset, mesma
|
|
3
|
+
-- onda que criou `requiresFacet`) sem seguir esse fato para `app.plan_grants`
|
|
4
|
+
-- quebrou o teto: `plan_permits('agenda','checkin',tenant)` nega em qualquer
|
|
5
|
+
-- plano que já restrinja QUALQUER OUTRA coisa (a regra é "silêncio total
|
|
6
|
+
-- permite", não "silêncio por facet") — então `tenant_plugin_set` rejeitava o
|
|
7
|
+
-- toggle em todo plano com ao menos uma linha em `plan_grants`, e ligar
|
|
8
|
+
-- Check-in em Módulos não fazia nada.
|
|
9
|
+
--
|
|
10
|
+
-- A correção segue o mesmo plano que já concede a Agenda: onde a Agenda em si
|
|
11
|
+
-- está `included` ou `optional`, as duas facets entram `included` — elas
|
|
12
|
+
-- nunca foram um upsell, só eram invisíveis para o catálogo de planos.
|
|
13
|
+
|
|
14
|
+
DO $$
|
|
15
|
+
DECLARE
|
|
16
|
+
v_plan record;
|
|
17
|
+
BEGIN
|
|
18
|
+
IF to_regprocedure('public.register_plan_grant(text,text,text,text,text)') IS NULL THEN
|
|
19
|
+
RAISE NOTICE '164: register_plan_grant ausente — pulado (pool sem app.plan_grants ainda)';
|
|
20
|
+
RETURN;
|
|
21
|
+
END IF;
|
|
22
|
+
|
|
23
|
+
FOR v_plan IN SELECT DISTINCT plan_id FROM app.plan_grants WHERE plugin_id = 'agenda' AND facet = '' LOOP
|
|
24
|
+
PERFORM public.register_plan_grant(v_plan.plan_id, 'agenda', 'checkin', 'included',
|
|
25
|
+
'Vem com a Agenda desde sempre — nunca foi upsell, só nunca tinha linha própria.');
|
|
26
|
+
PERFORM public.register_plan_grant(v_plan.plan_id, 'agenda', 'confirmations', 'included',
|
|
27
|
+
'Vem com a Agenda desde sempre — nunca foi upsell, só nunca tinha linha própria.');
|
|
28
|
+
END LOOP;
|
|
29
|
+
END $$;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
-- Unit coordinates must follow the address they describe. Queue an asynchronous
|
|
2
|
+
-- geocode only when the normalized, location-bearing fields actually change.
|
|
3
|
+
-- Saving remains available if the provider or pg_net is temporarily offline.
|
|
4
|
+
|
|
5
|
+
CREATE OR REPLACE FUNCTION app.unit_address_geocode_fingerprint(
|
|
6
|
+
p_street text,
|
|
7
|
+
p_number text,
|
|
8
|
+
p_district text,
|
|
9
|
+
p_city text,
|
|
10
|
+
p_state text,
|
|
11
|
+
p_postal_code text,
|
|
12
|
+
p_country text
|
|
13
|
+
) RETURNS text
|
|
14
|
+
LANGUAGE sql
|
|
15
|
+
IMMUTABLE
|
|
16
|
+
SET search_path = ''
|
|
17
|
+
AS $$
|
|
18
|
+
SELECT md5(concat_ws('|',
|
|
19
|
+
lower(regexp_replace(btrim(coalesce(p_street, '')), '\s+', ' ', 'g')),
|
|
20
|
+
lower(regexp_replace(btrim(coalesce(p_number, '')), '\s+', ' ', 'g')),
|
|
21
|
+
lower(regexp_replace(btrim(coalesce(p_district, '')), '\s+', ' ', 'g')),
|
|
22
|
+
lower(regexp_replace(btrim(coalesce(p_city, '')), '\s+', ' ', 'g')),
|
|
23
|
+
upper(regexp_replace(btrim(coalesce(p_state, '')), '\s+', ' ', 'g')),
|
|
24
|
+
regexp_replace(coalesce(p_postal_code, ''), '\D', '', 'g'),
|
|
25
|
+
upper(btrim(coalesce(p_country, 'BR')))
|
|
26
|
+
));
|
|
27
|
+
$$;
|
|
28
|
+
|
|
29
|
+
REVOKE ALL ON FUNCTION app.unit_address_geocode_fingerprint(text, text, text, text, text, text, text) FROM PUBLIC;
|
|
30
|
+
|
|
31
|
+
CREATE OR REPLACE FUNCTION app.trg_queue_unit_address_geocode() RETURNS trigger
|
|
32
|
+
LANGUAGE plpgsql
|
|
33
|
+
SECURITY DEFINER
|
|
34
|
+
SET search_path = ''
|
|
35
|
+
AS $$
|
|
36
|
+
DECLARE
|
|
37
|
+
v_fingerprint text;
|
|
38
|
+
v_old_fingerprint text;
|
|
39
|
+
v_request_id bigint;
|
|
40
|
+
v_coordinates_supplied boolean;
|
|
41
|
+
BEGIN
|
|
42
|
+
IF NEW.owner_type <> 'unit' THEN
|
|
43
|
+
RETURN NEW;
|
|
44
|
+
END IF;
|
|
45
|
+
|
|
46
|
+
v_fingerprint := app.unit_address_geocode_fingerprint(
|
|
47
|
+
NEW.street, NEW.number, NEW.district, NEW.city, NEW.state,
|
|
48
|
+
NEW.postal_code, NEW.country
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
IF TG_OP = 'UPDATE' THEN
|
|
52
|
+
v_old_fingerprint := app.unit_address_geocode_fingerprint(
|
|
53
|
+
OLD.street, OLD.number, OLD.district, OLD.city, OLD.state,
|
|
54
|
+
OLD.postal_code, OLD.country
|
|
55
|
+
);
|
|
56
|
+
IF v_fingerprint = v_old_fingerprint THEN
|
|
57
|
+
RETURN NEW;
|
|
58
|
+
END IF;
|
|
59
|
+
END IF;
|
|
60
|
+
|
|
61
|
+
-- A coordinate pair edited alongside the address is an explicit answer and
|
|
62
|
+
-- wins over automatic geocoding. An unchanged old pair does not: it points
|
|
63
|
+
-- to the previous address and must be invalidated before the async lookup.
|
|
64
|
+
v_coordinates_supplied := NEW.latitude IS NOT NULL AND NEW.longitude IS NOT NULL
|
|
65
|
+
AND (TG_OP = 'INSERT'
|
|
66
|
+
OR NEW.latitude IS DISTINCT FROM OLD.latitude
|
|
67
|
+
OR NEW.longitude IS DISTINCT FROM OLD.longitude);
|
|
68
|
+
|
|
69
|
+
IF v_coordinates_supplied THEN
|
|
70
|
+
NEW.metadata := jsonb_set(
|
|
71
|
+
coalesce(NEW.metadata, '{}'::jsonb),
|
|
72
|
+
'{geocode}',
|
|
73
|
+
jsonb_build_object(
|
|
74
|
+
'status', 'manual',
|
|
75
|
+
'fingerprint', v_fingerprint,
|
|
76
|
+
'updated_at', now()
|
|
77
|
+
),
|
|
78
|
+
true
|
|
79
|
+
);
|
|
80
|
+
RETURN NEW;
|
|
81
|
+
END IF;
|
|
82
|
+
|
|
83
|
+
NEW.latitude := NULL;
|
|
84
|
+
NEW.longitude := NULL;
|
|
85
|
+
IF to_regprocedure('net.http_get(text,jsonb,jsonb,integer)') IS NULL THEN
|
|
86
|
+
NEW.metadata := jsonb_set(
|
|
87
|
+
coalesce(NEW.metadata, '{}'::jsonb), '{geocode}',
|
|
88
|
+
jsonb_build_object('status', 'unconfigured', 'fingerprint', v_fingerprint, 'updated_at', now()), true
|
|
89
|
+
);
|
|
90
|
+
RETURN NEW;
|
|
91
|
+
END IF;
|
|
92
|
+
|
|
93
|
+
BEGIN
|
|
94
|
+
v_request_id := net.http_get(
|
|
95
|
+
url := 'https://nominatim.openstreetmap.org/search',
|
|
96
|
+
params := jsonb_strip_nulls(jsonb_build_object(
|
|
97
|
+
'format', 'jsonv2',
|
|
98
|
+
'limit', '1',
|
|
99
|
+
'street', concat_ws(' ', NEW.number, NEW.street),
|
|
100
|
+
'city', NEW.city,
|
|
101
|
+
'state', NEW.state,
|
|
102
|
+
'postalcode', NEW.postal_code,
|
|
103
|
+
'country', coalesce(nullif(NEW.country, ''), 'BR'),
|
|
104
|
+
'countrycodes', CASE WHEN coalesce(NEW.country, 'BR') ~* '^[a-z]{2}$'
|
|
105
|
+
THEN lower(coalesce(NEW.country, 'BR')) END,
|
|
106
|
+
'email', 'support@fayz.ai'
|
|
107
|
+
)),
|
|
108
|
+
headers := jsonb_build_object(
|
|
109
|
+
'Accept', 'application/json',
|
|
110
|
+
'User-Agent', 'Fayz/1.0 (support@fayz.ai)',
|
|
111
|
+
'Referer', 'https://fayz.ai/'
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
EXCEPTION WHEN OTHERS THEN
|
|
115
|
+
v_request_id := NULL;
|
|
116
|
+
END;
|
|
117
|
+
|
|
118
|
+
NEW.metadata := jsonb_set(
|
|
119
|
+
coalesce(NEW.metadata, '{}'::jsonb),
|
|
120
|
+
'{geocode}',
|
|
121
|
+
jsonb_build_object(
|
|
122
|
+
'status', CASE WHEN v_request_id IS NULL THEN 'delivery_failed' ELSE 'pending' END,
|
|
123
|
+
'fingerprint', v_fingerprint,
|
|
124
|
+
'request_id', v_request_id,
|
|
125
|
+
'requested_at', now()
|
|
126
|
+
),
|
|
127
|
+
true
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
RETURN NEW;
|
|
131
|
+
END;
|
|
132
|
+
$$;
|
|
133
|
+
|
|
134
|
+
REVOKE ALL ON FUNCTION app.trg_queue_unit_address_geocode() FROM PUBLIC;
|
|
135
|
+
|
|
136
|
+
DROP TRIGGER IF EXISTS queue_unit_address_geocode ON public.addresses;
|
|
137
|
+
CREATE TRIGGER queue_unit_address_geocode
|
|
138
|
+
BEFORE INSERT OR UPDATE OF street, number, district, city, state, postal_code,
|
|
139
|
+
country, latitude, longitude
|
|
140
|
+
ON public.addresses
|
|
141
|
+
FOR EACH ROW
|
|
142
|
+
EXECUTE FUNCTION app.trg_queue_unit_address_geocode();
|
|
143
|
+
|
|
144
|
+
COMMENT ON FUNCTION app.trg_queue_unit_address_geocode() IS
|
|
145
|
+
'Invalidates stale unit coordinates and asynchronously requests geocoding when the normalized address changes.';
|
|
146
|
+
|
|
147
|
+
CREATE OR REPLACE FUNCTION app.process_unit_address_geocodes(p_limit integer DEFAULT 100)
|
|
148
|
+
RETURNS integer
|
|
149
|
+
LANGUAGE plpgsql
|
|
150
|
+
SECURITY DEFINER
|
|
151
|
+
SET search_path = ''
|
|
152
|
+
AS $$
|
|
153
|
+
DECLARE
|
|
154
|
+
v_row record;
|
|
155
|
+
v_body jsonb;
|
|
156
|
+
v_hit jsonb;
|
|
157
|
+
v_lat numeric;
|
|
158
|
+
v_lng numeric;
|
|
159
|
+
v_processed integer := 0;
|
|
160
|
+
v_status text;
|
|
161
|
+
BEGIN
|
|
162
|
+
FOR v_row IN
|
|
163
|
+
SELECT a.id,
|
|
164
|
+
a.metadata,
|
|
165
|
+
a.metadata #>> '{geocode,fingerprint}' AS fingerprint,
|
|
166
|
+
r.status_code,
|
|
167
|
+
r.error_msg,
|
|
168
|
+
r.content
|
|
169
|
+
FROM public.addresses a
|
|
170
|
+
JOIN net._http_response r
|
|
171
|
+
ON r.id = CASE
|
|
172
|
+
WHEN a.metadata #>> '{geocode,request_id}' ~ '^[0-9]+$'
|
|
173
|
+
THEN (a.metadata #>> '{geocode,request_id}')::bigint
|
|
174
|
+
END
|
|
175
|
+
WHERE a.owner_type = 'unit'
|
|
176
|
+
AND a.metadata #>> '{geocode,status}' = 'pending'
|
|
177
|
+
AND a.metadata #>> '{geocode,request_id}' ~ '^[0-9]+$'
|
|
178
|
+
ORDER BY r.created
|
|
179
|
+
LIMIT greatest(1, least(coalesce(p_limit, 100), 500))
|
|
180
|
+
FOR UPDATE OF a SKIP LOCKED
|
|
181
|
+
LOOP
|
|
182
|
+
v_status := 'failed';
|
|
183
|
+
v_lat := NULL;
|
|
184
|
+
v_lng := NULL;
|
|
185
|
+
|
|
186
|
+
IF v_row.status_code = 200 THEN
|
|
187
|
+
BEGIN
|
|
188
|
+
v_body := v_row.content::jsonb;
|
|
189
|
+
IF jsonb_typeof(v_body) = 'array' AND jsonb_array_length(v_body) > 0 THEN
|
|
190
|
+
v_hit := v_body -> 0;
|
|
191
|
+
IF v_hit ->> 'lat' ~ '^-?[0-9]+(\.[0-9]+)?$'
|
|
192
|
+
AND v_hit ->> 'lon' ~ '^-?[0-9]+(\.[0-9]+)?$' THEN
|
|
193
|
+
v_lat := (v_hit ->> 'lat')::numeric;
|
|
194
|
+
v_lng := (v_hit ->> 'lon')::numeric;
|
|
195
|
+
IF v_lat BETWEEN -90 AND 90 AND v_lng BETWEEN -180 AND 180 THEN
|
|
196
|
+
v_status := 'resolved';
|
|
197
|
+
END IF;
|
|
198
|
+
ELSE
|
|
199
|
+
v_status := 'not_found';
|
|
200
|
+
END IF;
|
|
201
|
+
ELSE
|
|
202
|
+
v_status := 'not_found';
|
|
203
|
+
END IF;
|
|
204
|
+
EXCEPTION WHEN OTHERS THEN
|
|
205
|
+
v_status := 'failed';
|
|
206
|
+
END;
|
|
207
|
+
END IF;
|
|
208
|
+
|
|
209
|
+
UPDATE public.addresses a
|
|
210
|
+
SET latitude = CASE WHEN v_status = 'resolved' THEN v_lat ELSE NULL END,
|
|
211
|
+
longitude = CASE WHEN v_status = 'resolved' THEN v_lng ELSE NULL END,
|
|
212
|
+
metadata = jsonb_set(
|
|
213
|
+
coalesce(a.metadata, '{}'::jsonb),
|
|
214
|
+
'{geocode}',
|
|
215
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
216
|
+
'status', v_status,
|
|
217
|
+
'fingerprint', v_row.fingerprint,
|
|
218
|
+
'source', CASE WHEN v_status = 'resolved' THEN 'nominatim_openstreetmap' END,
|
|
219
|
+
'precision', CASE WHEN v_status = 'resolved'
|
|
220
|
+
THEN coalesce(v_hit ->> 'addresstype', v_hit ->> 'type') END,
|
|
221
|
+
'updated_at', now()
|
|
222
|
+
)),
|
|
223
|
+
true
|
|
224
|
+
)
|
|
225
|
+
WHERE a.id = v_row.id
|
|
226
|
+
AND a.metadata #>> '{geocode,fingerprint}' = v_row.fingerprint;
|
|
227
|
+
|
|
228
|
+
v_processed := v_processed + 1;
|
|
229
|
+
END LOOP;
|
|
230
|
+
|
|
231
|
+
RETURN v_processed;
|
|
232
|
+
END;
|
|
233
|
+
$$;
|
|
234
|
+
|
|
235
|
+
REVOKE ALL ON FUNCTION app.process_unit_address_geocodes(integer) FROM PUBLIC;
|
|
236
|
+
|
|
237
|
+
COMMENT ON FUNCTION app.process_unit_address_geocodes(integer) IS
|
|
238
|
+
'Consumes completed pg_net geocoding responses and writes coordinates only while the address fingerprint still matches.';
|
|
239
|
+
|
|
240
|
+
DO $$
|
|
241
|
+
DECLARE
|
|
242
|
+
v_job_id bigint;
|
|
243
|
+
BEGIN
|
|
244
|
+
IF to_regnamespace('cron') IS NULL THEN RETURN; END IF;
|
|
245
|
+
|
|
246
|
+
FOR v_job_id IN SELECT jobid FROM cron.job WHERE jobname = 'fayz-unit-address-geocode' LOOP
|
|
247
|
+
PERFORM cron.unschedule(v_job_id);
|
|
248
|
+
END LOOP;
|
|
249
|
+
|
|
250
|
+
PERFORM cron.schedule(
|
|
251
|
+
'fayz-unit-address-geocode',
|
|
252
|
+
'10 seconds',
|
|
253
|
+
'SELECT app.process_unit_address_geocodes()'
|
|
254
|
+
);
|
|
255
|
+
EXCEPTION WHEN OTHERS THEN
|
|
256
|
+
RAISE WARNING 'unit address geocoding scheduler was not installed: %', SQLERRM;
|
|
257
|
+
END;
|
|
258
|
+
$$;
|