@flowselections/logistiek-engine 1.0.191 → 1.0.195

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.
Files changed (29) hide show
  1. package/db/schema/logistiek-engine.sql +1936 -1
  2. package/dist-lib/components/InplanVoortgangCard.d.ts +16 -0
  3. package/dist-lib/components/InplanVoortgangCard.d.ts.map +1 -0
  4. package/dist-lib/components/InplanVoortgangCard.js +34 -0
  5. package/dist-lib/components/RegiePage.d.ts.map +1 -1
  6. package/dist-lib/components/RegiePage.js +2 -1
  7. package/dist-lib/index.d.ts +1 -0
  8. package/dist-lib/index.d.ts.map +1 -1
  9. package/dist-lib/index.js +1 -0
  10. package/dist-lib/integrations/supabase/types.d.ts +513 -0
  11. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  12. package/dist-lib/lib/handlers/logistics-tick.server.d.ts.map +1 -1
  13. package/dist-lib/lib/handlers/logistics-tick.server.js +58 -26
  14. package/dist-lib/lib/logisticsRecalcJob.server.d.ts.map +1 -1
  15. package/dist-lib/lib/logisticsRecalcJob.server.js +2 -1
  16. package/dist-lib/lib/moduleVersion.d.ts +1 -1
  17. package/dist-lib/lib/moduleVersion.js +1 -1
  18. package/dist-lib/lib/planQueue.server.d.ts.map +1 -1
  19. package/dist-lib/lib/planQueue.server.js +2 -1
  20. package/dist-lib/lib/scheduleTasks.server.d.ts +18 -12
  21. package/dist-lib/lib/scheduleTasks.server.d.ts.map +1 -1
  22. package/dist-lib/lib/scheduleTasks.server.js +99 -51
  23. package/dist-lib/lib/stickerQueue.server.d.ts +6 -0
  24. package/dist-lib/lib/stickerQueue.server.d.ts.map +1 -1
  25. package/dist-lib/lib/stickerQueue.server.js +23 -18
  26. package/dist-lib/lib/voorraadFouten.d.ts +14 -0
  27. package/dist-lib/lib/voorraadFouten.d.ts.map +1 -0
  28. package/dist-lib/lib/voorraadFouten.js +18 -0
  29. package/package.json +1 -1
@@ -31636,4 +31636,1939 @@ BEGIN
31636
31636
  SELECT b.d, b.print_plek_id, b.breedte_mm, b.hoogte_mm, sum(greatest(coalesce(b.kopieen, 1), 1))::integer
31637
31637
  FROM b WHERE b.d BETWEEN p_van AND p_tot
31638
31638
  GROUP BY 1, 2, 3, 4 ORDER BY 1, 2, 3, 4;
31639
- END $function$;
31639
+ END $function$;
31640
+ -- ===== 1.0.192: inplannen in porties (db/migrations/20261023190000) =====
31641
+ -- Logistiek Engine 1.0.192: taken opnieuw inplannen in porties met vingerafdruk per order.
31642
+ -- Herhaalbaar. Geen datacorrecties, geen deletes. Bestaande functies (o.a. logistics_schedule_tasks)
31643
+ -- blijven ongewijzigd, zodat gepubliceerde 1.0.190 blijft werken.
31644
+
31645
+ CREATE TABLE IF NOT EXISTS public.logistics_schedule_fp (
31646
+ order_ref text PRIMARY KEY,
31647
+ fp text NOT NULL,
31648
+ ingepland_at timestamptz NOT NULL DEFAULT now()
31649
+ );
31650
+ GRANT SELECT ON public.logistics_schedule_fp TO authenticated;
31651
+ GRANT ALL ON public.logistics_schedule_fp TO service_role;
31652
+ ALTER TABLE public.logistics_schedule_fp ENABLE ROW LEVEL SECURITY;
31653
+ DROP POLICY IF EXISTS "Ingelogd mag inplan-vingerafdruk lezen" ON public.logistics_schedule_fp;
31654
+ CREATE POLICY "Ingelogd mag inplan-vingerafdruk lezen" ON public.logistics_schedule_fp
31655
+ FOR SELECT TO authenticated USING (true);
31656
+
31657
+ CREATE TABLE IF NOT EXISTS public.logistics_schedule_werklijst (
31658
+ run_id uuid NOT NULL,
31659
+ order_ref text NOT NULL,
31660
+ verwerkt_at timestamptz,
31661
+ uitkomst text,
31662
+ PRIMARY KEY (run_id, order_ref)
31663
+ );
31664
+ GRANT SELECT ON public.logistics_schedule_werklijst TO authenticated;
31665
+ GRANT ALL ON public.logistics_schedule_werklijst TO service_role;
31666
+ ALTER TABLE public.logistics_schedule_werklijst ENABLE ROW LEVEL SECURITY;
31667
+ DROP POLICY IF EXISTS "Ingelogd mag inplan-werklijst lezen" ON public.logistics_schedule_werklijst;
31668
+ CREATE POLICY "Ingelogd mag inplan-werklijst lezen" ON public.logistics_schedule_werklijst
31669
+ FOR SELECT TO authenticated USING (true);
31670
+
31671
+ CREATE TABLE IF NOT EXISTS public.logistics_schedule_voortgang (
31672
+ id boolean PRIMARY KEY DEFAULT true CHECK (id),
31673
+ run_id uuid,
31674
+ gestart_at timestamptz,
31675
+ bijgewerkt_at timestamptz,
31676
+ totaal integer NOT NULL DEFAULT 0,
31677
+ verwerkt integer NOT NULL DEFAULT 0,
31678
+ ingepland integer NOT NULL DEFAULT 0,
31679
+ overgeslagen integer NOT NULL DEFAULT 0,
31680
+ fouten integer NOT NULL DEFAULT 0,
31681
+ laatste_fout text,
31682
+ klaar boolean NOT NULL DEFAULT true,
31683
+ duur_ms integer
31684
+ );
31685
+ GRANT SELECT ON public.logistics_schedule_voortgang TO authenticated;
31686
+ GRANT ALL ON public.logistics_schedule_voortgang TO service_role;
31687
+ ALTER TABLE public.logistics_schedule_voortgang ENABLE ROW LEVEL SECURITY;
31688
+ DROP POLICY IF EXISTS "Ingelogd mag inplan-voortgang lezen" ON public.logistics_schedule_voortgang;
31689
+ CREATE POLICY "Ingelogd mag inplan-voortgang lezen" ON public.logistics_schedule_voortgang
31690
+ FOR SELECT TO authenticated USING (true);
31691
+
31692
+ CREATE TABLE IF NOT EXISTS public.logistics_index_log (
31693
+ index_naam text PRIMARY KEY,
31694
+ tabel text NOT NULL,
31695
+ rijen bigint,
31696
+ duur_ms integer,
31697
+ aangemaakt_at timestamptz NOT NULL DEFAULT now()
31698
+ );
31699
+ GRANT SELECT ON public.logistics_index_log TO authenticated;
31700
+ GRANT ALL ON public.logistics_index_log TO service_role;
31701
+ ALTER TABLE public.logistics_index_log ENABLE ROW LEVEL SECURITY;
31702
+ DROP POLICY IF EXISTS "Ingelogd mag indexlog lezen" ON public.logistics_index_log;
31703
+ CREATE POLICY "Ingelogd mag indexlog lezen" ON public.logistics_index_log
31704
+ FOR SELECT TO authenticated USING (true);
31705
+
31706
+ -- Index voor "open taken per order", met gemeten duur (schrijfblokkade op logistics_tasks zolang hij loopt).
31707
+ DO $idx$
31708
+ DECLARE t0 timestamptz; n bigint;
31709
+ BEGIN
31710
+ IF to_regclass('public.idx_logistics_tasks_open_order') IS NULL THEN
31711
+ SELECT count(*) INTO n FROM public.logistics_tasks;
31712
+ t0 := clock_timestamp();
31713
+ CREATE INDEX idx_logistics_tasks_open_order ON public.logistics_tasks (order_ref)
31714
+ WHERE status IN ('planned', 'ready') AND order_ref IS NOT NULL;
31715
+ INSERT INTO public.logistics_index_log (index_naam, tabel, rijen, duur_ms)
31716
+ VALUES ('idx_logistics_tasks_open_order', 'logistics_tasks', n,
31717
+ (extract(epoch FROM clock_timestamp() - t0) * 1000)::int)
31718
+ ON CONFLICT (index_naam) DO NOTHING;
31719
+ END IF;
31720
+ END $idx$;
31721
+
31722
+ -- Algemene invoer van het inplannen: werkwijze, werktijden, afwijkende dagen, capaciteit,
31723
+ -- ladingdragers, ophaalmomenten (vervoerders) en het lopende uur (taken mogen niet in het verleden starten).
31724
+ CREATE OR REPLACE FUNCTION public.logistics_schedule_global_fp()
31725
+ RETURNS text LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
31726
+ DECLARE
31727
+ v_tab text; v_h text; v_all text := '';
31728
+ BEGIN
31729
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_schedule_global_fp');
31730
+ FOREACH v_tab IN ARRAY ARRAY['logistics_time_settings','logistics_opening_hours','logistics_opening_exceptions',
31731
+ 'logistics_rule_configs','cart_planning_settings','cart_planning_pick_times','cart_types','carriers'] LOOP
31732
+ IF to_regclass('public.' || v_tab) IS NOT NULL THEN
31733
+ EXECUTE format('SELECT md5(coalesce(string_agg(x, ''|'' ORDER BY x), '''')) FROM (SELECT to_jsonb(t)::text AS x FROM public.%I t) s', v_tab)
31734
+ INTO v_h;
31735
+ v_all := v_all || v_tab || ':' || coalesce(v_h, '') || ';';
31736
+ END IF;
31737
+ END LOOP;
31738
+ v_all := v_all || 'uur:' || to_char(now() AT TIME ZONE coalesce(public.logistics_tz(), 'UTC'), 'YYYY-MM-DD HH24');
31739
+ RETURN md5(v_all);
31740
+ END $$;
31741
+ REVOKE ALL ON FUNCTION public.logistics_schedule_global_fp() FROM PUBLIC, anon;
31742
+ GRANT EXECUTE ON FUNCTION public.logistics_schedule_global_fp() TO authenticated, service_role;
31743
+
31744
+ CREATE OR REPLACE FUNCTION public.logistics_schedule_order_fp(p_order_ref text, p_global text)
31745
+ RETURNS text LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
31746
+ DECLARE v_o text; v_t text; v_c text;
31747
+ BEGIN
31748
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_schedule_order_fp');
31749
+ SELECT (to_jsonb(o) - 'updated_at')::text, array_to_string(public.logistics_order_cart_ids(o.id), ',')
31750
+ INTO v_o, v_c FROM public.orders o WHERE o.order_number = p_order_ref;
31751
+ SELECT string_agg(t.id::text || t.task_type || t.status, ',' ORDER BY t.id) INTO v_t
31752
+ FROM public.logistics_tasks t WHERE t.order_ref = p_order_ref AND t.status IN ('planned', 'ready');
31753
+ RETURN md5(coalesce(p_global, '') || '#' || coalesce(v_o, '') || '#' || coalesce(v_t, '') || '#' || coalesce(v_c, ''));
31754
+ END $$;
31755
+ REVOKE ALL ON FUNCTION public.logistics_schedule_order_fp(text, text) FROM PUBLIC, anon;
31756
+ GRANT EXECUTE ON FUNCTION public.logistics_schedule_order_fp(text, text) TO authenticated, service_role;
31757
+
31758
+ -- Portie-inplanner. Opties (jsonb): start (bool), run_id (uuid), max_orders (int, 25), budget_ms (int, 20000), force (bool).
31759
+ -- Per order een eigen subtransactie: lukt het niet, dan blijft de oude planning staan.
31760
+ CREATE OR REPLACE FUNCTION public.logistics_schedule_tasks_batch(p_opties jsonb)
31761
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
31762
+ DECLARE
31763
+ v_start timestamptz := clock_timestamp();
31764
+ v_run uuid := nullif(p_opties->>'run_id', '')::uuid;
31765
+ v_max int := greatest(1, least(coalesce((p_opties->>'max_orders')::int, 25), 500));
31766
+ v_budget int := greatest(1000, least(coalesce((p_opties->>'budget_ms')::int, 20000), 120000));
31767
+ v_force boolean := coalesce((p_opties->>'force')::boolean, false);
31768
+ v_since timestamptz; v_160 timestamptz;
31769
+ v_global text; v_fp text; v_old text; r record;
31770
+ v_done int := 0; v_plan int := 0; v_skip int := 0; v_err int := 0; v_fouten jsonb := '[]'::jsonb;
31771
+ v_totaal int; v_rest int;
31772
+ BEGIN
31773
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_schedule_tasks_batch');
31774
+ IF v_run IS NULL OR coalesce((p_opties->>'start')::boolean, false) THEN
31775
+ v_run := gen_random_uuid();
31776
+ SELECT departure_rule_since, rule_160_since INTO v_since, v_160
31777
+ FROM public.logistics_time_settings ORDER BY updated_at DESC NULLS LAST LIMIT 1;
31778
+ INSERT INTO public.logistics_schedule_werklijst (run_id, order_ref)
31779
+ SELECT v_run, o.order_number FROM public.orders o
31780
+ WHERE o.deleted_at IS NULL AND o.order_number IS NOT NULL
31781
+ AND EXISTS (SELECT 1 FROM public.logistics_tasks t WHERE t.order_ref = o.order_number AND t.status IN ('planned','ready'))
31782
+ AND NOT (v_since IS NOT NULL AND v_160 IS NOT NULL AND o.created_at >= v_since AND o.created_at < v_160)
31783
+ ON CONFLICT DO NOTHING;
31784
+ GET DIAGNOSTICS v_totaal = ROW_COUNT;
31785
+ INSERT INTO public.logistics_schedule_voortgang (id, run_id, gestart_at, bijgewerkt_at, totaal, verwerkt, ingepland, overgeslagen, fouten, laatste_fout, klaar, duur_ms)
31786
+ VALUES (true, v_run, now(), now(), v_totaal, 0, 0, 0, 0, NULL, v_totaal = 0, NULL)
31787
+ ON CONFLICT (id) DO UPDATE SET run_id = EXCLUDED.run_id, gestart_at = EXCLUDED.gestart_at, bijgewerkt_at = EXCLUDED.bijgewerkt_at,
31788
+ totaal = EXCLUDED.totaal, verwerkt = 0, ingepland = 0, overgeslagen = 0, fouten = 0, laatste_fout = NULL,
31789
+ klaar = EXCLUDED.klaar, duur_ms = NULL;
31790
+ END IF;
31791
+
31792
+ v_global := public.logistics_schedule_global_fp();
31793
+ FOR r IN SELECT w.order_ref FROM public.logistics_schedule_werklijst w
31794
+ WHERE w.run_id = v_run AND w.verwerkt_at IS NULL ORDER BY w.order_ref LIMIT v_max LOOP
31795
+ EXIT WHEN (extract(epoch FROM clock_timestamp() - v_start) * 1000) > v_budget;
31796
+ v_fp := public.logistics_schedule_order_fp(r.order_ref, v_global);
31797
+ SELECT f.fp INTO v_old FROM public.logistics_schedule_fp f WHERE f.order_ref = r.order_ref;
31798
+ IF NOT v_force AND v_old IS NOT DISTINCT FROM v_fp THEN
31799
+ v_skip := v_skip + 1;
31800
+ UPDATE public.logistics_schedule_werklijst SET verwerkt_at = now(), uitkomst = 'ongewijzigd'
31801
+ WHERE run_id = v_run AND order_ref = r.order_ref;
31802
+ ELSE
31803
+ BEGIN
31804
+ PERFORM public.logistics_schedule_tasks(p_order_ref => r.order_ref);
31805
+ INSERT INTO public.logistics_schedule_fp (order_ref, fp, ingepland_at) VALUES (r.order_ref, v_fp, now())
31806
+ ON CONFLICT (order_ref) DO UPDATE SET fp = EXCLUDED.fp, ingepland_at = EXCLUDED.ingepland_at;
31807
+ v_plan := v_plan + 1;
31808
+ UPDATE public.logistics_schedule_werklijst SET verwerkt_at = now(), uitkomst = 'ingepland'
31809
+ WHERE run_id = v_run AND order_ref = r.order_ref;
31810
+ EXCEPTION WHEN OTHERS THEN
31811
+ v_err := v_err + 1;
31812
+ v_fouten := v_fouten || jsonb_build_object('order', r.order_ref, 'fout', left(SQLERRM, 300));
31813
+ UPDATE public.logistics_schedule_werklijst SET verwerkt_at = now(), uitkomst = 'fout: ' || left(SQLERRM, 300)
31814
+ WHERE run_id = v_run AND order_ref = r.order_ref;
31815
+ END;
31816
+ END IF;
31817
+ v_done := v_done + 1;
31818
+ END LOOP;
31819
+
31820
+ SELECT count(*) FILTER (WHERE verwerkt_at IS NULL), count(*) INTO v_rest, v_totaal
31821
+ FROM public.logistics_schedule_werklijst WHERE run_id = v_run;
31822
+ UPDATE public.logistics_schedule_voortgang
31823
+ SET bijgewerkt_at = now(), verwerkt = v_totaal - v_rest, ingepland = ingepland + v_plan,
31824
+ overgeslagen = overgeslagen + v_skip, fouten = fouten + v_err,
31825
+ laatste_fout = CASE WHEN v_err > 0 THEN (v_fouten->0->>'order') || ': ' || (v_fouten->0->>'fout') ELSE laatste_fout END,
31826
+ klaar = (v_rest = 0),
31827
+ duur_ms = CASE WHEN v_rest = 0 THEN (extract(epoch FROM now() - gestart_at) * 1000)::int ELSE duur_ms END
31828
+ WHERE id AND run_id = v_run;
31829
+ RETURN jsonb_build_object('runId', v_run, 'totaal', v_totaal, 'verwerkt', v_totaal - v_rest, 'resterend', v_rest,
31830
+ 'ingepland', v_plan, 'overgeslagen', v_skip, 'fouten', v_fouten,
31831
+ 'durationMs', (extract(epoch FROM clock_timestamp() - v_start) * 1000)::int);
31832
+ END $$;
31833
+ REVOKE ALL ON FUNCTION public.logistics_schedule_tasks_batch(jsonb) FROM PUBLIC, anon;
31834
+ GRANT EXECUTE ON FUNCTION public.logistics_schedule_tasks_batch(jsonb) TO authenticated, service_role;
31835
+
31836
+ -- ===== 1.0.194: stickers, direct indelen, lichte ronde, printstatus (db/migrations/20261026110000) =====
31837
+ -- Logistiek Engine 1.0.194: stickers werken weer, direct indelen, lichte minuutronde, printstatus volgt print_jobs.
31838
+ -- Additief: CREATE OR REPLACE / IF NOT EXISTS; triggers via DO-blok met bestaan-check; geen REVOKE/DROP/cron.schedule.
31839
+ SET check_function_bodies = off;
31840
+
31841
+ -- A1: leverdatum (tekst) veilig als datum lezen. NULL als het geen geldige YYYY-MM-DD is.
31842
+ CREATE OR REPLACE FUNCTION public.logistics_order_date(p text)
31843
+ RETURNS date LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE SET search_path TO 'public' AS $$
31844
+ BEGIN
31845
+ IF p IS NULL OR p !~ '^\d{4}-\d{2}-\d{2}' THEN RETURN NULL; END IF;
31846
+ RETURN left(p, 10)::date;
31847
+ EXCEPTION WHEN OTHERS THEN
31848
+ RETURN NULL;
31849
+ END $$;
31850
+ CREATE OR REPLACE FUNCTION public.logistics_order_date(p date)
31851
+ RETURNS date LANGUAGE sql IMMUTABLE PARALLEL SAFE SET search_path TO 'public' AS $$ SELECT p $$;
31852
+
31853
+ -- B7: is de order geannuleerd? Werkt ook als cancelled_at/status in deze shell ontbreken.
31854
+ CREATE OR REPLACE FUNCTION public.logistics_order_geannuleerd(p_order_id uuid)
31855
+ RETURNS boolean LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
31856
+ DECLARE j jsonb;
31857
+ BEGIN
31858
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_geannuleerd');
31859
+ SELECT to_jsonb(o) INTO j FROM public.orders o WHERE o.id = p_order_id;
31860
+ IF j IS NULL THEN RETURN false; END IF;
31861
+ RETURN (j->>'cancelled_at') IS NOT NULL
31862
+ OR lower(coalesce(j->>'status', '')) IN ('cancelled', 'canceled', 'geannuleerd');
31863
+ END $$;
31864
+
31865
+ -- Toestand per minuutronde (watermerken) en vastlegging per wek-aanroep.
31866
+ CREATE TABLE IF NOT EXISTS public.logistics_tick_state (
31867
+ key text PRIMARY KEY,
31868
+ value jsonb NOT NULL DEFAULT '{}'::jsonb,
31869
+ updated_at timestamptz NOT NULL DEFAULT now()
31870
+ );
31871
+ GRANT SELECT ON public.logistics_tick_state TO authenticated;
31872
+ GRANT ALL ON public.logistics_tick_state TO service_role;
31873
+ ALTER TABLE public.logistics_tick_state ENABLE ROW LEVEL SECURITY;
31874
+ DO $p$ BEGIN
31875
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='logistics_tick_state' AND policyname='Beheer leest rondetoestand') THEN
31876
+ CREATE POLICY "Beheer leest rondetoestand" ON public.logistics_tick_state FOR SELECT TO authenticated USING (public.logistics_is_beheer());
31877
+ END IF;
31878
+ END $p$;
31879
+
31880
+ CREATE TABLE IF NOT EXISTS public.logistics_kick_log (
31881
+ id bigserial PRIMARY KEY,
31882
+ kicked_at timestamptz NOT NULL DEFAULT clock_timestamp(),
31883
+ request_id bigint,
31884
+ skipped_reason text,
31885
+ round_started_at timestamptz
31886
+ );
31887
+ CREATE INDEX IF NOT EXISTS logistics_kick_log_kicked_at_idx ON public.logistics_kick_log (kicked_at DESC);
31888
+ GRANT SELECT ON public.logistics_kick_log TO authenticated;
31889
+ GRANT ALL ON public.logistics_kick_log TO service_role;
31890
+ GRANT USAGE, SELECT ON SEQUENCE public.logistics_kick_log_id_seq TO service_role;
31891
+ ALTER TABLE public.logistics_kick_log ENABLE ROW LEVEL SECURITY;
31892
+ DO $p$ BEGIN
31893
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='logistics_kick_log' AND policyname='Beheer leest wek-aanroepen') THEN
31894
+ CREATE POLICY "Beheer leest wek-aanroepen" ON public.logistics_kick_log FOR SELECT TO authenticated USING (public.logistics_is_beheer());
31895
+ END IF;
31896
+ END $p$;
31897
+
31898
+ -- B2: time-out van de aanroep naar de Shell. Standaard 30000 ms; ongeldige waarde = luide fout.
31899
+ CREATE OR REPLACE FUNCTION public.logistics_tick_timeout_ms()
31900
+ RETURNS integer LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
31901
+ DECLARE v text; n int;
31902
+ BEGIN
31903
+ PERFORM public.logistics_acc_guard('server', 'logistics_tick_timeout_ms');
31904
+ SELECT NULLIF(btrim(value), '') INTO v FROM public.module_runtime_config WHERE key = 'logistics_tick_timeout_ms';
31905
+ IF v IS NULL THEN RETURN 30000; END IF;
31906
+ IF v !~ '^\d{3,6}$' THEN
31907
+ RAISE EXCEPTION 'logistics_tick_timeout_ms in module_runtime_config is ongeldig (%); verwacht een getal in milliseconden', v;
31908
+ END IF;
31909
+ n := v::int;
31910
+ IF n < 1000 OR n > 120000 THEN
31911
+ RAISE EXCEPTION 'logistics_tick_timeout_ms (%) valt buiten 1000–120000 ms', n;
31912
+ END IF;
31913
+ RETURN n;
31914
+ END $$;
31915
+
31916
+ -- Instellingen aanvullen (idempotent). Bestaande waarden blijven, behalve de oude claim-timeout 10.
31917
+ CREATE OR REPLACE FUNCTION public.logistics_ensure_tick_settings()
31918
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
31919
+ DECLARE v_t int := 0; v_c int := 0;
31920
+ BEGIN
31921
+ PERFORM public.logistics_acc_guard('server', 'logistics_ensure_tick_settings');
31922
+ INSERT INTO public.module_runtime_config (key, value, updated_at)
31923
+ VALUES ('logistics_tick_timeout_ms', '30000', now())
31924
+ ON CONFLICT (key) DO NOTHING;
31925
+ GET DIAGNOSTICS v_t = ROW_COUNT;
31926
+ INSERT INTO public.logistics_plan_settings (key, value, updated_at)
31927
+ VALUES ('claim_timeout_minutes', '2', now())
31928
+ ON CONFLICT (key) DO UPDATE SET value = '2', updated_at = now()
31929
+ WHERE NULLIF(btrim(public.logistics_plan_settings.value), '') IS NULL
31930
+ OR btrim(public.logistics_plan_settings.value) = '10';
31931
+ GET DIAGNOSTICS v_c = ROW_COUNT;
31932
+ RETURN jsonb_build_object('timeoutIngesteld', v_t > 0, 'claimTimeoutBijgewerkt', v_c > 0);
31933
+ END $$;
31934
+
31935
+ -- B5: periodieke ensure-stappen hooguit 1× per uur.
31936
+ CREATE OR REPLACE FUNCTION public.logistics_ensure_periodiek()
31937
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
31938
+ DECLARE v_last timestamptz; v_r jsonb := '{}'::jsonb;
31939
+ BEGIN
31940
+ PERFORM public.logistics_acc_guard('server', 'logistics_ensure_periodiek');
31941
+ SELECT (value->>'at')::timestamptz INTO v_last FROM public.logistics_tick_state WHERE key = 'ensure_periodiek';
31942
+ IF v_last IS NOT NULL AND v_last > now() - interval '1 hour' THEN
31943
+ RETURN jsonb_build_object('overgeslagen', 'binnen het uur', 'laatst', v_last);
31944
+ END IF;
31945
+ v_r := v_r || jsonb_build_object('rebuffer', public.logistics_ensure_rebuffer_setting());
31946
+ v_r := v_r || jsonb_build_object('tick', public.logistics_ensure_tick_settings());
31947
+ INSERT INTO public.logistics_tick_state(key, value, updated_at)
31948
+ VALUES ('ensure_periodiek', jsonb_build_object('at', now()) || v_r, now())
31949
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
31950
+ RETURN v_r;
31951
+ END $$;
31952
+
31953
+ -- B3: laadtoestand alleen bij wijziging in planwachtrij/taken, anders hooguit elke 5 minuten.
31954
+ CREATE OR REPLACE FUNCTION public.logistics_refresh_plan_state_als_nodig()
31955
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
31956
+ DECLARE v_wm timestamptz; v_prev timestamptz; v_at timestamptz; v_res jsonb;
31957
+ BEGIN
31958
+ PERFORM public.logistics_acc_guard('server', 'logistics_refresh_plan_state_als_nodig');
31959
+ SELECT greatest((SELECT max(updated_at) FROM public.logistics_plan_queue),
31960
+ (SELECT max(updated_at) FROM public.logistics_tasks)) INTO v_wm;
31961
+ SELECT (value->>'wm')::timestamptz, (value->>'at')::timestamptz INTO v_prev, v_at
31962
+ FROM public.logistics_tick_state WHERE key = 'refresh_plan_state';
31963
+ IF v_at IS NOT NULL AND v_wm IS NOT DISTINCT FROM v_prev AND v_at > now() - interval '5 minutes' THEN
31964
+ RETURN jsonb_build_object('overgeslagen', 'geen wijziging');
31965
+ END IF;
31966
+ v_res := public.logistics_refresh_plan_state(p_limit => 500);
31967
+ INSERT INTO public.logistics_tick_state(key, value, updated_at)
31968
+ VALUES ('refresh_plan_state', jsonb_build_object('wm', v_wm, 'at', now()), now())
31969
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
31970
+ RETURN v_res;
31971
+ END $$;
31972
+
31973
+ -- B1: een wek-aanroep tijdens een lopende ronde oppakken vóór het slot vrijgaat.
31974
+ CREATE OR REPLACE FUNCTION public.logistics_kick_pending_take(p_since timestamptz)
31975
+ RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
31976
+ DECLARE v_val timestamptz;
31977
+ BEGIN
31978
+ PERFORM public.logistics_acc_guard('server', 'logistics_kick_pending_take');
31979
+ SELECT NULLIF(s.value, '')::timestamptz INTO v_val FROM public.logistics_plan_settings s
31980
+ WHERE s.key = 'kick_pending' FOR UPDATE;
31981
+ IF v_val IS NULL THEN RETURN false; END IF;
31982
+ UPDATE public.logistics_plan_settings s SET value = '', updated_at = now() WHERE s.key = 'kick_pending';
31983
+ RETURN v_val IS NOT NULL AND v_val >= p_since;
31984
+ END $$;
31985
+
31986
+ -- B0: start van de ronde koppelen aan de wek-aanroepen die erop wachtten.
31987
+ CREATE OR REPLACE FUNCTION public.logistics_kick_round_start()
31988
+ RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
31989
+ DECLARE n int;
31990
+ BEGIN
31991
+ PERFORM public.logistics_acc_guard('server', 'logistics_kick_round_start');
31992
+ UPDATE public.logistics_kick_log SET round_started_at = clock_timestamp()
31993
+ WHERE round_started_at IS NULL AND kicked_at > now() - interval '2 minutes';
31994
+ GET DIAGNOSTICS n = ROW_COUNT;
31995
+ RETURN n;
31996
+ END $$;
31997
+
31998
+ -- B0: meting per wek-aanroep: tijd wek, verzoek-id, antwoord van pg_net, start ronde.
31999
+ CREATE OR REPLACE FUNCTION public.logistics_kick_metingen(p_limit integer)
32000
+ RETURNS TABLE(kicked_at timestamptz, request_id bigint, skipped_reason text, antwoord_at timestamptz,
32001
+ status_code integer, time_out boolean, fout text, round_started_at timestamptz,
32002
+ wacht_tot_antwoord_ms integer, wacht_tot_ronde_ms integer)
32003
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
32004
+ BEGIN
32005
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_kick_metingen');
32006
+ IF to_regclass('net._http_response') IS NULL THEN
32007
+ RETURN QUERY SELECT k.kicked_at, k.request_id, k.skipped_reason, NULL::timestamptz, NULL::int, NULL::boolean,
32008
+ NULL::text, k.round_started_at, NULL::int,
32009
+ (extract(epoch FROM k.round_started_at - k.kicked_at) * 1000)::int
32010
+ FROM public.logistics_kick_log k ORDER BY k.kicked_at DESC LIMIT least(greatest(coalesce(p_limit, 50), 1), 500);
32011
+ RETURN;
32012
+ END IF;
32013
+ RETURN QUERY EXECUTE $q$
32014
+ SELECT k.kicked_at, k.request_id, k.skipped_reason, r.created, r.status_code, r.timed_out,
32015
+ left(r.error_msg, 200), k.round_started_at,
32016
+ (extract(epoch FROM r.created - k.kicked_at) * 1000)::int,
32017
+ (extract(epoch FROM k.round_started_at - k.kicked_at) * 1000)::int
32018
+ FROM public.logistics_kick_log k
32019
+ LEFT JOIN net._http_response r ON r.id = k.request_id
32020
+ ORDER BY k.kicked_at DESC LIMIT $1$q$ USING least(greatest(coalesce(p_limit, 50), 1), 500);
32021
+ END $$;
32022
+
32023
+ -- A3: uitkomst van de HTTP-ronde (stickers) vastleggen in logistics_tick_runs.steps en rondetoestand.
32024
+ CREATE OR REPLACE FUNCTION public.logistics_tick_http_log(p_steps jsonb)
32025
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
32026
+ DECLARE v_fouten jsonb := coalesce(p_steps->'stickerSyncFouten', '[]'::jsonb);
32027
+ BEGIN
32028
+ PERFORM public.logistics_acc_guard('server', 'logistics_tick_http_log');
32029
+ INSERT INTO public.logistics_tick_runs(started_at, finished_at, duration_ms, has_error, steps)
32030
+ VALUES (coalesce((p_steps->>'gestart')::timestamptz, now()), now(),
32031
+ nullif(p_steps->>'duurMs', '')::int,
32032
+ jsonb_typeof(v_fouten) = 'array' AND jsonb_array_length(v_fouten) > 0,
32033
+ jsonb_build_object('bron', 'http') || p_steps);
32034
+ INSERT INTO public.logistics_tick_state(key, value, updated_at)
32035
+ VALUES ('sticker_sync', jsonb_build_object('at', now(), 'fouten', v_fouten), now())
32036
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
32037
+ END $$;
32038
+
32039
+ -- C3: loopt er een (niet verlopen) pakbonopdracht voor deze order? Zelfde filter als logistics_cart_is_frozen.
32040
+ CREATE OR REPLACE FUNCTION public.logistics_order_print_actief(p_order_number text)
32041
+ RETURNS boolean LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
32042
+ DECLARE v boolean;
32043
+ BEGIN
32044
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_print_actief');
32045
+ IF p_order_number IS NULL OR to_regclass('public.print_jobs') IS NULL THEN RETURN false; END IF;
32046
+ EXECUTE $q$
32047
+ SELECT EXISTS (
32048
+ SELECT 1 FROM public.print_jobs pj
32049
+ WHERE pj.documenttype::text = 'pakbon'
32050
+ AND COALESCE(pj.is_herprint, false) = false
32051
+ AND pj.status::text IN ('wachtend', 'bezig', 'voltooid')
32052
+ AND COALESCE(pj.foutcode, '') NOT IN ('verlopen', 'expired')
32053
+ AND COALESCE(pj.verwerkingsfase, '') NOT IN ('verlopen', 'expired')
32054
+ AND (EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.print_job_id = pj.id AND q.order_number = $1 AND q.cancelled_at IS NULL)
32055
+ OR pj.payload->>'cart_id' IN (SELECT oc.id::text FROM logistics_active.order_carts oc
32056
+ JOIN public.orders o ON o.id = oc.order_id WHERE o.order_number = $1)))$q$
32057
+ INTO v USING p_order_number;
32058
+ RETURN coalesce(v, false);
32059
+ END $$;
32060
+
32061
+ -- C4: printstatus in batch (max. 500 ids, één rij per id).
32062
+ CREATE OR REPLACE FUNCTION public.logistics_print_status(p_queue_item_ids uuid[])
32063
+ RETURNS TABLE(queue_item_id uuid, status text, printnode_status text, fase text, fase_gestart_op timestamptz, melding text)
32064
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
32065
+ DECLARE v_fgo text; v_log text;
32066
+ BEGIN
32067
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_print_status');
32068
+ IF coalesce(cardinality(p_queue_item_ids), 0) > 500 THEN
32069
+ RAISE EXCEPTION 'logistics_print_status: maximaal 500 ids per aanroep (gekregen: %)', cardinality(p_queue_item_ids)
32070
+ USING ERRCODE = '22023';
32071
+ END IF;
32072
+ IF coalesce(cardinality(p_queue_item_ids), 0) = 0 THEN RETURN; END IF;
32073
+ IF to_regclass('public.print_jobs') IS NULL THEN
32074
+ RETURN QUERY SELECT u.id, 'geen_printmodule'::text, NULL::text, NULL::text, NULL::timestamptz,
32075
+ 'De printmodule is in deze omgeving niet geïnstalleerd.'::text
32076
+ FROM (SELECT DISTINCT x AS id FROM unnest(p_queue_item_ids) x) u;
32077
+ RETURN;
32078
+ END IF;
32079
+ -- fase_gestart_op komt van Layouts; alleen lezen als de kolom bestaat.
32080
+ v_fgo := CASE WHEN EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public'
32081
+ AND table_name='print_jobs' AND column_name='fase_gestart_op')
32082
+ THEN 'pj.fase_gestart_op' ELSE 'NULL::timestamptz' END;
32083
+ -- 'fase' = print_jobs.verwerkingsfase; ontbreekt die, dan de laatste print_job_logs-gebeurtenis.
32084
+ v_log := CASE WHEN to_regclass('public.print_job_logs') IS NOT NULL
32085
+ THEN 'SELECT l.gebeurtenis FROM public.print_job_logs l WHERE l.job_id = pj.id ORDER BY l.created_at DESC LIMIT 1'
32086
+ ELSE 'SELECT NULL::text AS gebeurtenis' END;
32087
+ RETURN QUERY EXECUTE format($q$
32088
+ SELECT u.id,
32089
+ CASE WHEN q.id IS NULL THEN 'onbekende_regel'
32090
+ WHEN pj.id IS NULL THEN 'geen_opdracht'
32091
+ ELSE pj.status::text END,
32092
+ pj.printnode_status,
32093
+ COALESCE(NULLIF(pj.verwerkingsfase, ''), lg.gebeurtenis),
32094
+ %s,
32095
+ COALESCE(NULLIF(pj.foutmelding, ''), NULLIF(pj.printnode_bericht, ''))
32096
+ FROM (SELECT DISTINCT x AS id FROM unnest($1) x) u
32097
+ LEFT JOIN public.pick_queue q ON q.id = u.id
32098
+ LEFT JOIN public.print_jobs pj ON pj.id = q.print_job_id
32099
+ LEFT JOIN LATERAL (%s) lg ON true$q$, v_fgo, v_log)
32100
+ USING p_queue_item_ids;
32101
+ END $$;
32102
+
32103
+ -- C2: printed_at/print_count volgen de echte uitkomst van de printopdracht.
32104
+ CREATE OR REPLACE FUNCTION public.logistics_print_job_voltooid_trg()
32105
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
32106
+ BEGIN
32107
+ IF NEW.status::text = 'voltooid' AND (TG_OP = 'INSERT' OR OLD.status IS DISTINCT FROM NEW.status) THEN
32108
+ UPDATE public.pick_queue q
32109
+ SET printed_at = COALESCE(NEW.voltooid_op, now()),
32110
+ print_count = CASE WHEN q.printed_at IS NULL OR q.printed_at < NEW.created_at
32111
+ THEN COALESCE(q.print_count, 0) + 1 ELSE q.print_count END,
32112
+ updated_at = now()
32113
+ WHERE q.print_job_id = NEW.id
32114
+ AND q.printed_at IS DISTINCT FROM COALESCE(NEW.voltooid_op, now());
32115
+ END IF;
32116
+ RETURN NEW;
32117
+ EXCEPTION WHEN OTHERS THEN
32118
+ -- Een printopdracht mag nooit mislukken door deze bijwerking; wel zichtbaar maken.
32119
+ BEGIN
32120
+ INSERT INTO public.logistics_trace_events(phase, step, status, reason)
32121
+ VALUES ('printen', 'printed_at bijwerken na voltooid', 'mislukt', left(SQLERRM, 300));
32122
+ EXCEPTION WHEN OTHERS THEN NULL; END;
32123
+ RETURN NEW;
32124
+ END $$;
32125
+
32126
+ DO $t$ BEGIN
32127
+ IF to_regclass('public.print_jobs') IS NULL THEN
32128
+ RAISE NOTICE 'logistiek-engine: print_jobs ontbreekt; printstatus-trigger wordt aangemaakt zodra de printmodule er is (opnieuw draaien)';
32129
+ ELSIF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_logistics_print_job_voltooid' AND tgrelid = 'public.print_jobs'::regclass) THEN
32130
+ CREATE TRIGGER trg_logistics_print_job_voltooid
32131
+ AFTER INSERT OR UPDATE OF status ON public.print_jobs
32132
+ FOR EACH ROW EXECUTE FUNCTION public.logistics_print_job_voltooid_trg();
32133
+ END IF;
32134
+ END $t$;
32135
+
32136
+ -- Installatiecontrole 1.0.194.
32137
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_194()
32138
+ RETURNS TABLE(onderdeel text, ok boolean, toelichting text)
32139
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
32140
+ DECLARE v jsonb; v_at timestamptz; n int; v_pc boolean;
32141
+ BEGIN
32142
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_install_check_194');
32143
+ SELECT value INTO v FROM public.logistics_tick_state WHERE key = 'sticker_sync';
32144
+ v_at := (v->>'at')::timestamptz;
32145
+ onderdeel := 'Engine: sticker-sync laatste run zonder fout';
32146
+ ok := v IS NOT NULL AND jsonb_array_length(coalesce(v->'fouten', '[]'::jsonb)) = 0 AND v_at > now() - interval '15 minutes';
32147
+ toelichting := CASE WHEN v IS NULL THEN 'nog geen ronde vastgelegd (1.0.194 of nieuwer moet draaien)'
32148
+ WHEN v_at <= now() - interval '15 minutes' THEN 'laatste ronde ' || to_char(v_at, 'DD-MM HH24:MI') || ' (ouder dan 15 min)'
32149
+ WHEN ok THEN 'geen fouten (' || to_char(v_at, 'HH24:MI:SS') || ')'
32150
+ ELSE 'fout: ' || left((v->'fouten')::text, 300) END;
32151
+ RETURN NEXT;
32152
+
32153
+ onderdeel := 'Engine: wek-aanroep gaat niet verloren (kick_pending-mechanisme aanwezig)';
32154
+ ok := EXISTS (SELECT 1 FROM pg_proc WHERE pronamespace='public'::regnamespace AND proname='logistics_plan_kick' AND prosrc LIKE '%kick_pending%')
32155
+ AND to_regprocedure('public.logistics_kick_pending_take(timestamptz)') IS NOT NULL;
32156
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine 1.0.194 via schemasync' END; RETURN NEXT;
32157
+
32158
+ onderdeel := 'Engine: tick-aanroep heeft eigen time-out';
32159
+ SELECT count(*) INTO n FROM pg_proc WHERE pronamespace='public'::regnamespace
32160
+ AND proname IN ('logistics_plan_kick', 'logistics_recalc_tick_kick') AND prosrc LIKE '%timeout_milliseconds%';
32161
+ ok := n = 2;
32162
+ BEGIN
32163
+ toelichting := CASE WHEN ok THEN public.logistics_tick_timeout_ms() || ' ms' ELSE n || ' van 2 aanroepen met time-out' END;
32164
+ EXCEPTION WHEN OTHERS THEN ok := false; toelichting := SQLERRM; END;
32165
+ RETURN NEXT;
32166
+
32167
+ onderdeel := 'Engine: claim-timeout ≤ 3 min';
32168
+ BEGIN
32169
+ n := public.logistics_plan_setting('claim_timeout_minutes', 2)::int;
32170
+ ok := n <= 3; toelichting := n || ' min';
32171
+ EXCEPTION WHEN OTHERS THEN ok := false; toelichting := SQLERRM; END;
32172
+ RETURN NEXT;
32173
+
32174
+ onderdeel := 'Engine: printstatus-trigger aanwezig';
32175
+ ok := to_regclass('public.print_jobs') IS NOT NULL
32176
+ AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_logistics_print_job_voltooid' AND tgrelid = 'public.print_jobs'::regclass);
32177
+ toelichting := CASE WHEN to_regclass('public.print_jobs') IS NULL THEN 'print_jobs ontbreekt (printmodule niet geïnstalleerd)'
32178
+ WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: schemabestand opnieuw draaien' END;
32179
+ RETURN NEXT;
32180
+
32181
+ -- printed_at volgt print_jobs pas echt als geen enkele functie hem nog bij het aanmaken zet.
32182
+ SELECT EXISTS (SELECT 1 FROM pg_proc WHERE pronamespace='public'::regnamespace
32183
+ AND proname IN ('pick_complete_print', 'pick_complete_print_cart', 'pick_complete_print_carts')
32184
+ AND prosrc ~* 'printed_at\s*=\s*(now|clock_timestamp)') INTO v_pc;
32185
+ onderdeel := 'Engine: printed_at volgt print_jobs';
32186
+ ok := to_regprocedure('public.logistics_print_job_voltooid_trg()') IS NOT NULL AND NOT v_pc;
32187
+ toelichting := CASE WHEN ok THEN 'printed_at wordt alleen gezet bij status voltooid'
32188
+ WHEN v_pc THEN 'Engine-trigger aanwezig, maar Pickschermen (pick_complete_print*) zet printed_at nog bij het aanmaken'
32189
+ ELSE 'ontbreekt: Engine 1.0.194' END;
32190
+ RETURN NEXT;
32191
+
32192
+ onderdeel := 'Engine: leverdatum veilig als datum (logistics_order_date)';
32193
+ ok := to_regprocedure('public.logistics_order_date(text)') IS NOT NULL
32194
+ AND NOT EXISTS (SELECT 1 FROM pg_proc WHERE pronamespace='public'::regnamespace
32195
+ AND proname IN ('logistics_sticker_sync','logistics_own_sticker_sync','logistics_own_traysticker_sync')
32196
+ AND prosrc ~ '\mo\.delivery_date\s*>=');
32197
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'sticker-sync vergelijkt leverdatum nog als tekst' END; RETURN NEXT;
32198
+ END $$;
32199
+
32200
+ CREATE OR REPLACE FUNCTION public.logistics_own_sticker_sync(p_limit integer DEFAULT 200)
32201
+ RETURNS jsonb
32202
+ LANGUAGE plpgsql
32203
+ SECURITY DEFINER
32204
+ SET search_path TO 'public'
32205
+ AS $function$
32206
+ DECLARE
32207
+ r record;
32208
+ v_bron text;
32209
+ v_n int;
32210
+ v_sticker uuid;
32211
+ v_status text;
32212
+ v_reden text;
32213
+ v_bijgewerkt int := 0;
32214
+ v_vrij int := 0;
32215
+ v_uit int := 0;
32216
+ BEGIN
32217
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_own_sticker_sync');
32218
+ FOR r IN
32219
+ SELECT o.id AS order_id, o.order_number, o.customer_id, oi.product_id,
32220
+ sum(public.logistics_own_sticker_count(oi.unit, oi.quantity, oi.plants_per_crate, prd.plants_per_crate))::int AS planten,
32221
+ bool_or(public.logistics_own_sticker_count(oi.unit, oi.quantity, oi.plants_per_crate, prd.plants_per_crate) IS NULL) AS onbekend
32222
+ FROM public.orders o
32223
+ JOIN public.order_items oi ON oi.order_id = o.id
32224
+ LEFT JOIN public.products prd ON prd.id = oi.product_id
32225
+ WHERE oi.needs_sticker IS TRUE
32226
+ AND oi.quarantined_at IS NULL
32227
+ AND oi.product_id IS NOT NULL
32228
+ AND coalesce(o.order_source, '') <> 'floriday'
32229
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
32230
+ AND coalesce(o.status, '') <> 'geannuleerd'
32231
+ AND public.logistics_order_date(o.delivery_date) >= current_date - 1
32232
+ GROUP BY o.id, o.order_number, o.customer_id, oi.product_id
32233
+ LIMIT greatest(1, p_limit)
32234
+ LOOP
32235
+ v_bron := r.order_id::text || ':' || r.product_id::text;
32236
+ SELECT count(*), (array_agg(s.id))[1] INTO v_n, v_sticker
32237
+ FROM public.customer_product_stickers s
32238
+ JOIN public.customers c ON c.id = r.customer_id
32239
+ WHERE s.trade_item_key = r.product_id::text
32240
+ AND s.customer_key IN (c.id::text, 'tf:' || coalesce(c.twinfield_code, '-'), 'cn:' || coalesce(c.customer_number, '-'));
32241
+ v_status := 'wacht_op_moment'; v_reden := NULL;
32242
+ IF r.onbekend THEN
32243
+ v_status := 'geblokkeerd'; v_reden := 'Aantal stickers onbekend: regel in fust zonder planten per fust (niet op de regel en niet bij het product). Vul planten per fust in.';
32244
+ ELSIF coalesce(r.planten, 0) < 1 THEN
32245
+ v_status := 'geblokkeerd'; v_reden := 'Aantal planten is 0; geen sticker te printen.';
32246
+ ELSIF v_n = 0 THEN
32247
+ v_status := 'geblokkeerd'; v_reden := 'Geen eigen sticker gevonden voor deze klant en dit product. Voeg hem toe bij de klant.';
32248
+ ELSIF v_n > 1 THEN
32249
+ v_status := 'geblokkeerd'; v_reden := 'Meer dan één eigen sticker voor deze klant en dit product; er mag er maar één zijn.';
32250
+ END IF;
32251
+
32252
+ UPDATE public.logistics_sticker_prints
32253
+ SET kopieen = greatest(coalesce(r.planten, 0), 1),
32254
+ eigen_sticker_id = CASE WHEN v_n = 1 THEN v_sticker ELSE NULL END,
32255
+ status = CASE WHEN status = 'geblokkeerd' AND v_status = 'wacht_op_moment' THEN 'wacht_op_moment'
32256
+ WHEN status = 'niet_printen' THEN v_status
32257
+ WHEN status = 'wacht_op_moment' THEN v_status ELSE status END,
32258
+ reden = CASE WHEN status IN ('wacht_op_moment', 'geblokkeerd', 'niet_printen') THEN v_reden ELSE reden END,
32259
+ updated_at = now()
32260
+ WHERE soort = 'eigen_sticker' AND bron_id = v_bron
32261
+ AND status IN ('wacht_op_moment', 'geblokkeerd', 'niet_printen');
32262
+ IF NOT FOUND AND NOT EXISTS (SELECT 1 FROM public.logistics_sticker_prints WHERE soort = 'eigen_sticker' AND bron_id = v_bron) THEN
32263
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, kopieen, status, reden, eigen_sticker_id)
32264
+ VALUES ('eigen_sticker', v_bron, r.order_id, r.order_number, greatest(coalesce(r.planten, 0), 1), v_status, v_reden,
32265
+ CASE WHEN v_n = 1 THEN v_sticker ELSE NULL END);
32266
+ END IF;
32267
+ v_bijgewerkt := v_bijgewerkt + 1;
32268
+ END LOOP;
32269
+
32270
+ UPDATE public.logistics_sticker_prints p
32271
+ SET status = 'niet_printen', reden = 'Sticker uitgezet op de orderregel of order vervallen.', updated_at = now()
32272
+ WHERE p.soort = 'eigen_sticker' AND p.status IN ('wacht_op_moment', 'geblokkeerd', 'klaar')
32273
+ AND NOT EXISTS (
32274
+ SELECT 1 FROM public.order_items oi JOIN public.orders o ON o.id = oi.order_id
32275
+ WHERE oi.order_id = p.order_id AND p.bron_id = oi.order_id::text || ':' || oi.product_id::text
32276
+ AND oi.needs_sticker IS TRUE AND oi.quarantined_at IS NULL
32277
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL AND coalesce(o.status, '') <> 'geannuleerd');
32278
+ GET DIAGNOSTICS v_uit = ROW_COUNT;
32279
+
32280
+ WITH vrij AS (
32281
+ SELECT p.id,
32282
+ (SELECT public.logistics_try_uuid(q.payload->>'cartId')
32283
+ FROM public.pick_queue q
32284
+ WHERE q.cancelled_at IS NULL AND q.print_job_id IS NOT NULL
32285
+ AND (q.payload->>'orderId' = p.order_id::text OR q.payload->'orderIds' ? p.order_id::text)
32286
+ ORDER BY q.created_at LIMIT 1) AS kar,
32287
+ (SELECT k.print_plek_id::text
32288
+ FROM public.pick_queue q
32289
+ JOIN public.print_plek_koppelingen k ON k.bron IN ('pickstation', 'pick_station') AND k.bron_id = q.station_id::text
32290
+ WHERE q.cancelled_at IS NULL AND q.print_job_id IS NOT NULL
32291
+ AND (q.payload->>'orderId' = p.order_id::text OR q.payload->'orderIds' ? p.order_id::text)
32292
+ ORDER BY q.created_at, k.created_at LIMIT 1) AS plek,
32293
+ s.width_mm, s.height_mm,
32294
+ public.logistics_document_for_order(p.order_id, 'eigen_sticker') AS lay
32295
+ FROM public.logistics_sticker_prints p
32296
+ LEFT JOIN public.customer_product_stickers s ON s.id = p.eigen_sticker_id
32297
+ WHERE p.soort = 'eigen_sticker' AND p.status = 'wacht_op_moment'
32298
+ AND EXISTS (SELECT 1 FROM public.pick_queue q
32299
+ WHERE q.cancelled_at IS NULL AND q.print_job_id IS NOT NULL
32300
+ AND (q.payload->>'orderId' = p.order_id::text OR q.payload->'orderIds' ? p.order_id::text))
32301
+ )
32302
+ UPDATE public.logistics_sticker_prints p
32303
+ SET status = 'klaar', cart_id = vrij.kar, print_plek_id = vrij.plek,
32304
+ breedte_mm = coalesce(vrij.width_mm, (vrij.lay->>'breedte_mm')::numeric),
32305
+ hoogte_mm = coalesce(vrij.height_mm, (vrij.lay->>'hoogte_mm')::numeric),
32306
+ updated_at = now()
32307
+ FROM vrij
32308
+ WHERE p.id = vrij.id;
32309
+ GET DIAGNOSTICS v_vrij = ROW_COUNT;
32310
+
32311
+ RETURN jsonb_build_object('regels', v_bijgewerkt, 'vrijgegeven', v_vrij, 'niet_printen', v_uit);
32312
+ END;
32313
+ $function$
32314
+ ;
32315
+
32316
+ CREATE OR REPLACE FUNCTION public.logistics_own_traysticker_sync(p_limit integer DEFAULT 200)
32317
+ RETURNS jsonb
32318
+ LANGUAGE plpgsql
32319
+ SECURITY DEFINER
32320
+ SET search_path TO 'public'
32321
+ AS $function$
32322
+ DECLARE
32323
+ s jsonb; v_tray text; v_rel text; v_station text;
32324
+ r record; v_bron text; v_keep text[] := '{}'; v_wacht_orders uuid[] := '{}';
32325
+ n int := 0; n_geblokkeerd int := 0; n_vrij int := 0; n_uit int := 0;
32326
+ BEGIN
32327
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_own_traysticker_sync');
32328
+ SELECT settings INTO s FROM public.logistics_rule_configs
32329
+ ORDER BY is_active DESC, (status = 'actief') DESC, updated_at DESC LIMIT 1;
32330
+ v_tray := public.logistics_rule_text(s, 'pickstations', 'traysticker_release', 'uit');
32331
+ IF v_tray = 'uit' THEN
32332
+ RETURN jsonb_build_object('trayRelease', v_tray, 'regels', 0);
32333
+ END IF;
32334
+ v_rel := public.logistics_rule_text(s, 'pickstations', 'sticker_release', 'with_pick_release');
32335
+ v_station := CASE WHEN v_rel = 'with_packing_slip'
32336
+ THEN public.logistics_rule_text(s, 'pickstations', 'packing_slip_station', '')
32337
+ ELSE public.logistics_rule_text(s, 'pickstations', 'sticker_station', '') END;
32338
+
32339
+ FOR r IN
32340
+ WITH regels AS (
32341
+ SELECT o.id AS order_id, o.order_number, ci.cart_id, ci.id AS ci_id, ci.crates, ci.created_at,
32342
+ coalesce(ci.product_name, oi.product_name, 'onbekend') AS regelnaam
32343
+ FROM public.orders o
32344
+ JOIN public.order_items oi ON oi.order_id = o.id
32345
+ JOIN public.order_cart_items ci ON ci.order_item_id = oi.id
32346
+ WHERE coalesce(o.order_source, '') <> 'floriday'
32347
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
32348
+ AND coalesce(o.status, '') <> 'geannuleerd'
32349
+ AND oi.quarantined_at IS NULL
32350
+ AND public.logistics_order_date(o.delivery_date) >= current_date - 1
32351
+ )
32352
+ SELECT g.*,
32353
+ (SELECT min(x.regelnaam) FROM regels x WHERE x.order_id = g.order_id AND coalesce(x.crates, 0) <= 0) AS onbekende_regel
32354
+ FROM regels g
32355
+ ORDER BY g.order_number, g.cart_id, g.created_at, g.ci_id
32356
+ LIMIT greatest(1, p_limit)
32357
+ LOOP
32358
+ IF r.onbekende_regel IS NOT NULL THEN
32359
+ v_wacht_orders := v_wacht_orders || r.order_id;
32360
+ v_bron := r.order_id::text || ':fustaantal-onbekend';
32361
+ v_keep := v_keep || v_bron;
32362
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, cart_id, kopieen, status, reden)
32363
+ VALUES ('eigen_traysticker', v_bron, r.order_id, r.order_number, NULL, 1, 'geblokkeerd',
32364
+ 'Fustaantal van regel ' || r.onbekende_regel || ' onbekend. Alle traystickers van deze order wachten, zodat de nummering klopt.')
32365
+ ON CONFLICT DO NOTHING;
32366
+ n_geblokkeerd := n_geblokkeerd + 1;
32367
+ CONTINUE;
32368
+ END IF;
32369
+ FOR k IN 1 .. r.crates LOOP
32370
+ v_bron := r.order_id::text || ':' || r.cart_id::text || ':' || r.ci_id::text || ':' || k::text;
32371
+ v_keep := v_keep || v_bron;
32372
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, cart_id, kopieen, print_plek_id, status, reden)
32373
+ VALUES ('eigen_traysticker', v_bron, r.order_id, r.order_number, r.cart_id, 1,
32374
+ public.logistics_sticker_cart_plek(r.cart_id, v_station), 'wacht_op_moment', 'Wacht op de pakbon van deze kar.')
32375
+ ON CONFLICT DO NOTHING;
32376
+ UPDATE public.logistics_sticker_prints
32377
+ SET status = 'wacht_op_moment', reden = 'Wacht op de pakbon van deze kar.', updated_at = now()
32378
+ WHERE soort = 'eigen_traysticker' AND bron_id = v_bron AND status = 'geblokkeerd'
32379
+ AND reden LIKE 'Fustaantal van regel %';
32380
+ n := n + 1;
32381
+ END LOOP;
32382
+ END LOOP;
32383
+
32384
+ UPDATE public.logistics_sticker_prints p
32385
+ SET status = 'geblokkeerd',
32386
+ reden = (SELECT b.reden FROM public.logistics_sticker_prints b
32387
+ WHERE b.soort = 'eigen_traysticker' AND b.bron_id = p.order_id::text || ':fustaantal-onbekend' LIMIT 1),
32388
+ updated_at = now()
32389
+ WHERE p.soort = 'eigen_traysticker' AND p.status IN ('wacht_op_moment', 'klaar')
32390
+ AND p.order_id = ANY (v_wacht_orders);
32391
+ v_keep := v_keep || ARRAY(SELECT p.bron_id FROM public.logistics_sticker_prints p
32392
+ WHERE p.soort = 'eigen_traysticker' AND p.order_id = ANY (v_wacht_orders)
32393
+ AND p.reden LIKE 'Fustaantal van regel %');
32394
+
32395
+ UPDATE public.logistics_sticker_prints p
32396
+ SET status = 'niet_printen', reden = 'Fust hoort niet meer bij deze kar of order vervallen.', updated_at = now()
32397
+ WHERE p.soort = 'eigen_traysticker' AND p.status IN ('wacht_op_moment', 'geblokkeerd', 'klaar')
32398
+ AND NOT (p.bron_id = ANY (v_keep))
32399
+ AND p.order_id IN (SELECT o.id FROM public.orders o WHERE public.logistics_order_date(o.delivery_date) >= current_date - 1);
32400
+ GET DIAGNOSTICS n_uit = ROW_COUNT;
32401
+
32402
+ UPDATE public.logistics_sticker_prints p
32403
+ SET status = 'klaar', reden = NULL,
32404
+ print_plek_id = coalesce(p.print_plek_id, public.logistics_sticker_cart_plek(p.cart_id, v_station)),
32405
+ updated_at = now()
32406
+ WHERE p.soort = 'eigen_traysticker' AND p.status = 'wacht_op_moment'
32407
+ AND p.cart_id IS NOT NULL AND public.logistics_cart_slip_queued(p.cart_id);
32408
+ GET DIAGNOSTICS n_vrij = ROW_COUNT;
32409
+
32410
+ RETURN jsonb_build_object('trayRelease', v_tray, 'fusten', n, 'geblokkeerd', n_geblokkeerd, 'vrijgegeven', n_vrij, 'niet_printen', n_uit);
32411
+ END; $function$
32412
+ ;
32413
+
32414
+ CREATE OR REPLACE FUNCTION public.logistics_sticker_sync(p_limit integer DEFAULT 200)
32415
+ RETURNS jsonb
32416
+ LANGUAGE plpgsql
32417
+ SECURITY DEFINER
32418
+ SET search_path TO 'public'
32419
+ AS $function$
32420
+ DECLARE
32421
+ s jsonb; v_rel text; v_station text; v_tray text;
32422
+ st record; r_ord record; d jsonb; fi jsonb; v_n int; v_order uuid; v_onr text;
32423
+ v_copies int; v_size numeric[]; v_type text; v_should text; v_keep uuid[]; v_del text; v_fo text; v_cart uuid;
32424
+ v_crates_tot int; v_pieces_tot int; r record; v_rows int; v_cp jsonb; v_bron text; v_ncarts int; sv record;
32425
+ n_st int := 0; n_tray int := 0;
32426
+ BEGIN
32427
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_sticker_sync');
32428
+ SELECT settings INTO s FROM public.logistics_rule_configs
32429
+ ORDER BY is_active DESC, (status = 'actief') DESC, updated_at DESC LIMIT 1;
32430
+ v_rel := public.logistics_rule_text(s, 'pickstations', 'sticker_release', 'with_pick_release');
32431
+ v_station := CASE WHEN v_rel = 'with_packing_slip'
32432
+ THEN public.logistics_rule_text(s, 'pickstations', 'packing_slip_station', '')
32433
+ ELSE public.logistics_rule_text(s, 'pickstations', 'sticker_station', '') END;
32434
+ v_tray := public.logistics_rule_text(s, 'pickstations', 'traysticker_release', 'uit');
32435
+
32436
+ FOR st IN SELECT * FROM public.floriday_customer_stickers_cache ORDER BY updated_at DESC LIMIT greatest(p_limit,1) LOOP
32437
+ n_st := n_st + 1;
32438
+ d := coalesce(st.data, '{}'::jsonb);
32439
+ fi := coalesce(d->'stickerFileInformation', '{}'::jsonb);
32440
+ v_size := CASE WHEN st.sticker_width_mm IS NOT NULL AND st.sticker_height_mm IS NOT NULL
32441
+ THEN ARRAY[st.sticker_width_mm::numeric, st.sticker_height_mm::numeric]
32442
+ ELSE public.logistics_sticker_parse_size(d->'stickerSize') END;
32443
+ v_type := upper(nullif(btrim(coalesce(st.sticker_type, d->>'stickerType', '')), ''));
32444
+ v_should := lower(coalesce(st.should_be_printed::text, fi->>'shouldBePrinted', ''));
32445
+
32446
+ -- Koppeling volgens Suppliers API 2026v1: alleen via salesOrderId.
32447
+ SELECT count(*), (array_agg(id))[1], (array_agg(order_number))[1] INTO v_n, v_order, v_onr
32448
+ FROM public.orders WHERE deleted_at IS NULL
32449
+ AND nullif(btrim(coalesce(st.sales_order_id, d->>'salesOrderId', '')), '') IS NOT NULL
32450
+ AND floriday_sales_order_id = coalesce(st.sales_order_id, d->>'salesOrderId');
32451
+
32452
+ IF coalesce(st.is_deleted,false) OR lower(coalesce(d->>'isDeleted','')) = 'true' THEN
32453
+ UPDATE public.logistics_sticker_prints SET status = 'niet_printen', reden = 'De koper heeft deze sticker verwijderd.'
32454
+ WHERE soort = 'sticker' AND bron_id = st.floriday_id AND status NOT IN ('in_printwachtrij','afgemeld');
32455
+ CONTINUE;
32456
+ END IF;
32457
+ IF lower(coalesce(d->>'isHandled','')) = 'true' THEN
32458
+ UPDATE public.logistics_sticker_prints SET status = 'afgemeld', reden = 'Al afgemeld in Floriday.', handled_at = coalesce(handled_at, now())
32459
+ WHERE soort = 'sticker' AND bron_id = st.floriday_id AND status <> 'afgemeld';
32460
+ CONTINUE;
32461
+ END IF;
32462
+ IF v_n = 1 AND public.logistics_sales_order_external_fulfilled(v_order, coalesce(st.sales_order_id, d->>'salesOrderId')) THEN
32463
+ UPDATE public.logistics_sticker_prints SET status = 'niet_printen', reden = 'Afgehandeld in Floriday; door ons niet geprint en niet afgemeld.'
32464
+ WHERE soort = 'sticker' AND bron_id = st.floriday_id AND status NOT IN ('in_printwachtrij','afgemeld');
32465
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, NULL, 0, v_size, NULL, NULL, 'niet_printen', 'Afgehandeld in Floriday; door ons niet geprint en niet afgemeld.');
32466
+ CONTINUE;
32467
+ END IF;
32468
+ IF v_n <> 1 THEN
32469
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, NULL, NULL, NULL, 0, v_size, NULL, NULL, 'geblokkeerd',
32470
+ format('Geen eenduidige order bij Floriday-verkooporder %s (%s gevonden).', coalesce(st.sales_order_id, d->>'salesOrderId', 'onbekend'), v_n));
32471
+ UPDATE public.logistics_sticker_prints SET sticker_type = v_type WHERE soort='sticker' AND bron_id = st.floriday_id;
32472
+ CONTINUE;
32473
+ END IF;
32474
+ IF v_should = 'false' THEN
32475
+ -- Geen printopdracht; zichtbaar als 'niet_printen'. Wordt ook niet afgemeld in Floriday.
32476
+ UPDATE public.logistics_sticker_prints SET status = 'niet_printen',
32477
+ reden = 'Floriday: deze sticker hoeft niet geprint te worden (shouldBePrinted = false).'
32478
+ WHERE soort = 'sticker' AND bron_id = st.floriday_id AND cart_id IS NOT NULL AND status NOT IN ('in_printwachtrij','afgemeld');
32479
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, NULL, 0, v_size, NULL, NULL, 'niet_printen',
32480
+ 'Floriday: deze sticker hoeft niet geprint te worden (shouldBePrinted = false).');
32481
+ UPDATE public.logistics_sticker_prints SET sticker_type = v_type WHERE soort='sticker' AND bron_id = st.floriday_id;
32482
+ CONTINUE;
32483
+ END IF;
32484
+ IF lower(coalesce(fi->>'deliverSeparately','')) = 'true' THEN
32485
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, NULL, 0, v_size, NULL, NULL, 'geblokkeerd',
32486
+ 'Sticker moet apart worden geleverd (deliverSeparately); kies handmatig hoe die mee gaat.');
32487
+ UPDATE public.logistics_sticker_prints SET sticker_type = v_type WHERE soort='sticker' AND bron_id = st.floriday_id;
32488
+ CONTINUE;
32489
+ END IF;
32490
+ IF nullif(btrim(coalesce(d->>'salesOrderCorrectionRequestId','')),'') IS NOT NULL THEN
32491
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, NULL, 0, v_size, NULL, NULL, 'wacht_op_moment',
32492
+ 'Wacht tot de correctie op de verkooporder is verwerkt.');
32493
+ CONTINUE;
32494
+ END IF;
32495
+ -- 1.0.182: aantal uit numberOfCopies -> additionalServices.numberOfUnits (LABEL) -> afgeleid uit stickersoort.
32496
+ v_cp := public.logistics_sticker_copies(v_order, st.floriday_id, v_type, fi, nullif(btrim(coalesce(st.trade_item_id,'')),''));
32497
+ v_copies := nullif(v_cp->>'aantal','')::int; v_bron := v_cp->>'bron';
32498
+ IF coalesce(v_copies,0) <= 0 THEN
32499
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, NULL, 0, v_size, NULL, NULL, 'geblokkeerd',
32500
+ 'Aantal stickers onbekend: geen numberOfCopies, geen numberOfUnits (LABEL) en niet af te leiden uit de stickersoort; niet geprint.');
32501
+ UPDATE public.logistics_sticker_prints SET sticker_type = v_type, kopieen_bron = NULL WHERE soort='sticker' AND bron_id = st.floriday_id;
32502
+ CONTINUE;
32503
+ END IF;
32504
+ IF v_type = 'DOCUMENT' THEN
32505
+ SELECT oc.id INTO v_cart FROM logistics_active.order_carts oc WHERE oc.order_id = v_order ORDER BY oc.cart_number, oc.created_at LIMIT 1;
32506
+ IF v_cart IS NULL THEN
32507
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, NULL, v_copies, v_size, NULL, NULL, 'wacht_op_moment',
32508
+ 'Wacht tot de order op een kar staat.');
32509
+ ELSE
32510
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, v_cart, v_copies, v_size,
32511
+ public.logistics_sticker_cart_plek(v_cart, v_station), NULL,
32512
+ CASE WHEN public.logistics_cart_slip_queued(v_cart) THEN 'klaar' ELSE 'wacht_op_moment' END,
32513
+ CASE WHEN public.logistics_cart_slip_queued(v_cart) THEN NULL ELSE 'Wacht op de pakbon van deze kar.' END);
32514
+ UPDATE public.logistics_sticker_prints SET status = 'niet_printen', reden = 'Vervangen door regels per kar.'
32515
+ WHERE soort = 'sticker' AND bron_id = st.floriday_id AND (cart_id IS NULL OR cart_id <> v_cart)
32516
+ AND status NOT IN ('in_printwachtrij','afgemeld','niet_printen');
32517
+ END IF;
32518
+ UPDATE public.logistics_sticker_prints SET sticker_type = v_type, kopieen_bron = v_bron WHERE soort='sticker' AND bron_id = st.floriday_id;
32519
+ CONTINUE;
32520
+ END IF;
32521
+ IF v_type IS NULL OR v_type NOT IN ('POT','PRODUCT','SLEEVE','PACKAGE','LAYER','LOAD_CARRIER') THEN
32522
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, NULL, v_copies, v_size, NULL, NULL, 'geblokkeerd',
32523
+ format('Onbekende of niet-ondersteunde stickersoort (%s); kies handmatig hoe deze sticker mee gaat.', coalesce(v_type,'leeg')));
32524
+ UPDATE public.logistics_sticker_prints SET sticker_type = v_type WHERE soort='sticker' AND bron_id = st.floriday_id;
32525
+ CONTINUE;
32526
+ END IF;
32527
+
32528
+ v_keep := ARRAY[]::uuid[]; v_rows := 0; v_crates_tot := 0; v_pieces_tot := 0;
32529
+ FOR r IN SELECT * FROM public.logistics_sticker_distribution(v_order, v_type, v_copies, nullif(btrim(coalesce(st.trade_item_id,'')),'')) LOOP
32530
+ v_rows := v_rows + 1; v_keep := v_keep || r.cart_id;
32531
+ v_crates_tot := v_crates_tot + r.crates; v_pieces_tot := v_pieces_tot + r.pieces;
32532
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, r.cart_id, r.kopieen, v_size,
32533
+ public.logistics_sticker_cart_plek(r.cart_id, v_station), NULL,
32534
+ CASE WHEN public.logistics_cart_slip_queued(r.cart_id) THEN 'klaar' ELSE 'wacht_op_moment' END,
32535
+ CASE WHEN public.logistics_cart_slip_queued(r.cart_id) THEN NULL ELSE 'Wacht op de pakbon van deze kar.' END);
32536
+ END LOOP;
32537
+ IF v_rows = 0 THEN
32538
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, NULL, v_copies, v_size, NULL, NULL, 'geblokkeerd',
32539
+ 'De verdeling over karren sluit niet: de order staat (nog) niet op een kar, of de karren hebben geen fusten/stuks van dit artikel.');
32540
+ UPDATE public.logistics_sticker_prints SET sticker_type = v_type WHERE soort='sticker' AND bron_id = st.floriday_id;
32541
+ CONTINUE;
32542
+ END IF;
32543
+ UPDATE public.logistics_sticker_prints SET sticker_type = v_type, kopieen_bron = v_bron WHERE soort='sticker' AND bron_id = st.floriday_id;
32544
+ IF (v_type IN ('PACKAGE','LAYER') AND v_crates_tot <> v_copies) OR (v_type IN ('POT','PRODUCT','SLEEVE') AND v_pieces_tot <> v_copies) THEN
32545
+ BEGIN
32546
+ INSERT INTO public.logistics_trace_events(phase, step, status, reason, order_ref)
32547
+ SELECT 'stickers', 'Aantal klantstickers wijkt af', 'waarschuwing',
32548
+ format('Floriday vraagt %s (%s); op de karren: %s fusten / %s stuks. Het aantal van Floriday wordt geprint.',
32549
+ v_copies, v_type, v_crates_tot, v_pieces_tot), v_onr
32550
+ WHERE NOT EXISTS (SELECT 1 FROM public.logistics_trace_events e WHERE e.order_ref = v_onr AND e.step = 'Aantal klantstickers wijkt af'
32551
+ AND e.created_at > now() - interval '1 day');
32552
+ EXCEPTION WHEN OTHERS THEN NULL; END;
32553
+ END IF;
32554
+ UPDATE public.logistics_sticker_prints SET status = 'niet_printen',
32555
+ reden = CASE WHEN cart_id IS NULL THEN 'Vervangen door regels per kar.' ELSE 'Kar hoort niet meer bij deze sticker.' END
32556
+ WHERE soort = 'sticker' AND bron_id = st.floriday_id
32557
+ AND (cart_id IS NULL OR NOT (cart_id = ANY (v_keep)))
32558
+ AND status NOT IN ('in_printwachtrij','afgemeld','niet_printen');
32559
+ END LOOP;
32560
+
32561
+ -- 1.0.182: stickers die alleen in additionalServices van de verkooporder staan (stickercache leeg of nog niet bijgewerkt).
32562
+ FOR sv IN
32563
+ SELECT o.id AS order_id, o.order_number, e->>'stickerId' AS sticker_id, sum((e->>'numberOfUnits')::int)::int AS units
32564
+ FROM public.orders o
32565
+ JOIN public.floriday_salesorders_cache s ON s.floriday_id = o.floriday_sales_order_id AND NOT coalesce(s.is_deleted,false)
32566
+ CROSS JOIN LATERAL jsonb_array_elements(CASE WHEN jsonb_typeof(s.data->'additionalServices') = 'array' THEN s.data->'additionalServices' ELSE '[]'::jsonb END) e
32567
+ WHERE o.deleted_at IS NULL AND public.logistics_order_date(o.delivery_date) >= current_date - 2
32568
+ AND upper(coalesce(e->>'unit','')) = 'LABEL' AND (e->>'numberOfUnits') ~ '^\d+$'
32569
+ AND nullif(btrim(coalesce(e->>'stickerId','')),'') IS NOT NULL
32570
+ AND NOT EXISTS (SELECT 1 FROM public.floriday_customer_stickers_cache c WHERE c.floriday_id = e->>'stickerId')
32571
+ AND NOT public.logistics_sales_order_external_fulfilled(o.id, o.floriday_sales_order_id)
32572
+ GROUP BY o.id, o.order_number, e->>'stickerId'
32573
+ LIMIT greatest(p_limit,1)
32574
+ LOOP
32575
+ IF coalesce(sv.units,0) <= 0 THEN CONTINUE; END IF;
32576
+ SELECT count(*), (array_agg(oc.id ORDER BY oc.cart_number, oc.created_at))[1] INTO v_ncarts, v_cart
32577
+ FROM logistics_active.order_carts oc WHERE oc.order_id = sv.order_id;
32578
+ IF v_ncarts = 1 THEN
32579
+ PERFORM public.logistics_sticker_upsert('sticker', sv.sticker_id, sv.order_id, sv.order_number, v_cart, sv.units, NULL,
32580
+ public.logistics_sticker_cart_plek(v_cart, v_station), NULL,
32581
+ CASE WHEN public.logistics_cart_slip_queued(v_cart) THEN 'klaar' ELSE 'wacht_op_moment' END,
32582
+ CASE WHEN public.logistics_cart_slip_queued(v_cart) THEN NULL ELSE 'Wacht op de pakbon van deze kar.' END);
32583
+ UPDATE public.logistics_sticker_prints SET status = 'niet_printen', reden = 'Vervangen door regels per kar.'
32584
+ WHERE soort = 'sticker' AND bron_id = sv.sticker_id AND (cart_id IS NULL OR cart_id <> v_cart)
32585
+ AND status NOT IN ('in_printwachtrij','afgemeld','niet_printen');
32586
+ ELSE
32587
+ PERFORM public.logistics_sticker_upsert('sticker', sv.sticker_id, sv.order_id, sv.order_number, NULL, sv.units, NULL, NULL, NULL, 'wacht_op_moment',
32588
+ CASE WHEN v_ncarts = 0 THEN 'Wacht tot de order op een kar staat.'
32589
+ ELSE format('Stickersoort nog onbekend; wacht op de stickergegevens van Floriday om %s stickers over %s karren te verdelen.', sv.units, v_ncarts) END);
32590
+ END IF;
32591
+ UPDATE public.logistics_sticker_prints SET kopieen_bron = 'numberOfUnits' WHERE soort='sticker' AND bron_id = sv.sticker_id
32592
+ AND status NOT IN ('in_printwachtrij','afgemeld');
32593
+ n_st := n_st + 1;
32594
+ END LOOP;
32595
+
32596
+ IF v_tray <> 'uit' THEN
32597
+ FOR r_ord IN
32598
+ SELECT o.id, o.order_number, nullif(btrim(o.floriday_sales_order_id),'') AS sales
32599
+ FROM public.orders o
32600
+ WHERE o.deleted_at IS NULL AND nullif(btrim(o.floriday_sales_order_id),'') IS NOT NULL
32601
+ AND public.logistics_order_date(o.delivery_date) >= current_date - 2
32602
+ AND EXISTS (SELECT 1 FROM logistics_active.order_carts oc WHERE oc.order_id = o.id)
32603
+ ORDER BY o.delivery_date LIMIT greatest(p_limit,1)
32604
+ LOOP
32605
+ SELECT d2.floriday_id INTO v_del FROM public.floriday_delivery_orders_cache d2
32606
+ WHERE d2.data @> jsonb_build_object('fulfillmentRequests', jsonb_build_array(jsonb_build_object('salesOrderId', r_ord.sales)))
32607
+ ORDER BY d2.fetched_at DESC LIMIT 1;
32608
+ IF v_del IS NULL THEN CONTINUE; END IF;
32609
+ IF EXISTS (SELECT 1 FROM public.logistics_sticker_prints WHERE soort = 'traysticker' AND bron_id = v_del) THEN
32610
+ v_fo := public.logistics_afhandeling_fulfillment_order(r_ord.id)->>'fulfillmentOrderId';
32611
+ IF v_fo IS NOT NULL THEN
32612
+ UPDATE public.logistics_sticker_prints SET fulfillment_order_id = v_fo
32613
+ WHERE soort = 'traysticker' AND bron_id = v_del AND fulfillment_order_id IS DISTINCT FROM v_fo
32614
+ AND status NOT IN ('in_printwachtrij','afgemeld');
32615
+ END IF;
32616
+ CONTINUE;
32617
+ END IF;
32618
+ SELECT oc.id INTO v_cart FROM logistics_active.order_carts oc WHERE oc.order_id = r_ord.id ORDER BY oc.cart_number, oc.created_at LIMIT 1;
32619
+ v_fo := public.logistics_afhandeling_fulfillment_order(r_ord.id)->>'fulfillmentOrderId';
32620
+ PERFORM public.logistics_sticker_upsert('traysticker', v_del, r_ord.id, r_ord.order_number, v_cart, 1, NULL,
32621
+ public.logistics_sticker_cart_plek(v_cart, v_station), v_fo, 'wacht_op_moment',
32622
+ 'Wacht op geaccepteerde belading in Floriday.');
32623
+ n_tray := n_tray + 1;
32624
+ END LOOP;
32625
+ END IF;
32626
+
32627
+ RETURN jsonb_build_object('stickers', n_st, 'trayNieuw', n_tray, 'release', v_rel, 'trayRelease', v_tray,
32628
+ 'klantstickerMoment', 'with_packing_slip');
32629
+ END; $function$
32630
+ ;
32631
+
32632
+ CREATE OR REPLACE FUNCTION public.logistics_plan_kick()
32633
+ RETURNS void
32634
+ LANGUAGE plpgsql
32635
+ SECURITY DEFINER
32636
+ SET search_path TO 'public'
32637
+ AS $function$
32638
+ DECLARE
32639
+ v_base text; v_key text; v_path text; v_last timestamptz;
32640
+ v_debounce int := public.logistics_plan_setting('kick_debounce_seconds', 5)::int;
32641
+ v_running boolean; v_req bigint; v_timeout int := public.logistics_tick_timeout_ms();
32642
+ BEGIN
32643
+ PERFORM public.logistics_acc_guard('server', 'logistics_plan_kick');
32644
+ -- Loopt er al een tick-ronde (slot 'tick:ronde')? Dan geen tweede ronde wekken.
32645
+ -- Fail-closed: kan dit niet worden vastgesteld, dan valt de EXCEPTION-tak
32646
+ -- hieronder en wordt de wek-aanroep overgeslagen; de minuut-cron is het vangnet.
32647
+ SELECT EXISTS (
32648
+ SELECT 1 FROM public.logistics_plan_locks
32649
+ WHERE lock_key = 'tick:ronde' AND locked_until > now()
32650
+ ) INTO v_running;
32651
+ IF v_running IS DISTINCT FROM false THEN
32652
+ -- 1.0.194: niet laten vallen; de lopende ronde pakt de vlag vóór het vrijgeven op.
32653
+ INSERT INTO public.logistics_plan_settings(key, value, updated_at)
32654
+ VALUES ('kick_pending', clock_timestamp()::text, now())
32655
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
32656
+ BEGIN
32657
+ INSERT INTO public.logistics_kick_log(skipped_reason) VALUES ('ronde_loopt_vlag_gezet');
32658
+ EXCEPTION WHEN OTHERS THEN NULL; END;
32659
+ RETURN;
32660
+ END IF;
32661
+
32662
+ SELECT value INTO v_base FROM public.module_runtime_config WHERE key = 'logistics_tick_base_url';
32663
+ SELECT value INTO v_key FROM public.module_runtime_config WHERE key = 'logistics_tick_apikey';
32664
+ SELECT value INTO v_path FROM public.module_runtime_config WHERE key = 'logistics_tick_path';
32665
+ IF v_base IS NULL OR v_key IS NULL THEN RETURN; END IF;
32666
+ v_path := COALESCE(v_path, '/api/public/hooks/logistics-recalc-tick');
32667
+
32668
+ SELECT NULLIF(value,'')::timestamptz INTO v_last
32669
+ FROM public.logistics_plan_settings WHERE key = 'last_kick_at';
32670
+ IF v_last IS NOT NULL AND v_last > now() - make_interval(secs => v_debounce) THEN
32671
+ BEGIN INSERT INTO public.logistics_kick_log(skipped_reason) VALUES ('debounce'); EXCEPTION WHEN OTHERS THEN NULL; END;
32672
+ RETURN;
32673
+ END IF;
32674
+
32675
+ INSERT INTO public.logistics_plan_settings(key, value, updated_at)
32676
+ VALUES ('last_kick_at', now()::text, now())
32677
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
32678
+
32679
+ v_req := net.http_post(
32680
+ url := rtrim(v_base,'/') || v_path,
32681
+ headers := jsonb_build_object('Content-Type','application/json','apikey', v_key),
32682
+ body := '{}'::jsonb,
32683
+ timeout_milliseconds := v_timeout
32684
+ );
32685
+ BEGIN INSERT INTO public.logistics_kick_log(request_id) VALUES (v_req); EXCEPTION WHEN OTHERS THEN NULL; END;
32686
+ EXCEPTION WHEN OTHERS THEN
32687
+ NULL;
32688
+ END;
32689
+ $function$
32690
+ ;
32691
+
32692
+ CREATE OR REPLACE FUNCTION public.logistics_recalc_tick_kick()
32693
+ RETURNS bigint
32694
+ LANGUAGE plpgsql
32695
+ SECURITY DEFINER
32696
+ SET search_path TO 'public'
32697
+ AS $function$
32698
+ DECLARE
32699
+ v_base text; v_key text; v_path text; v_req bigint; v_host text;
32700
+ BEGIN
32701
+ PERFORM public.logistics_acc_guard('server', 'logistics_recalc_tick_kick');
32702
+ BEGIN PERFORM public.logistics_tick_calls_resolve(); EXCEPTION WHEN others THEN NULL; END;
32703
+
32704
+ SELECT NULLIF(btrim(value),'') INTO v_base FROM public.module_runtime_config WHERE key = 'logistics_tick_base_url';
32705
+ SELECT NULLIF(btrim(value),'') INTO v_key FROM public.module_runtime_config WHERE key = 'logistics_tick_apikey';
32706
+ SELECT NULLIF(btrim(value),'') INTO v_path FROM public.module_runtime_config WHERE key = 'logistics_tick_path';
32707
+ v_host := substring(v_base from '^[a-zA-Z]+://([^/?#]+)');
32708
+
32709
+ IF v_base IS NULL OR v_key IS NULL THEN
32710
+ INSERT INTO public.logistics_tick_calls (target_host, skipped_reason, engine_ok, error, resolved_at)
32711
+ VALUES (v_host,
32712
+ CASE WHEN v_base IS NULL THEN 'geen_adres' ELSE 'geen_sleutel' END,
32713
+ false,
32714
+ CASE WHEN v_base IS NULL THEN 'geen adres ingesteld' ELSE 'geen sleutel ingesteld' END,
32715
+ now());
32716
+ RAISE WARNING 'logistics_recalc_tick_kick: geen aanroep — logistics_tick_base_url en/of logistics_tick_apikey ontbreekt in module_runtime_config. Vul deze voor deze omgeving in.';
32717
+ RETURN NULL;
32718
+ END IF;
32719
+ v_path := COALESCE(v_path, '/api/public/hooks/logistics-recalc-tick');
32720
+ v_req := net.http_post(
32721
+ url := rtrim(v_base,'/') || v_path,
32722
+ headers := jsonb_build_object('Content-Type','application/json','apikey', v_key),
32723
+ body := '{}'::jsonb,
32724
+ timeout_milliseconds := public.logistics_tick_timeout_ms()
32725
+ );
32726
+ INSERT INTO public.logistics_tick_calls (request_id, target_host) VALUES (v_req, v_host);
32727
+ RETURN v_req;
32728
+ END;
32729
+ $function$
32730
+ ;
32731
+
32732
+ CREATE OR REPLACE FUNCTION public.logistics_plan_release_stale()
32733
+ RETURNS integer
32734
+ LANGUAGE plpgsql
32735
+ SECURITY DEFINER
32736
+ SET search_path TO 'public'
32737
+ AS $function$
32738
+ DECLARE
32739
+ v_n int := 0;
32740
+ v_min int := public.logistics_plan_setting('claim_timeout_minutes', 2)::int;
32741
+ v_max int := GREATEST(1, public.logistics_plan_setting('max_attempts', 3)::int);
32742
+ v_txt text;
32743
+ r record;
32744
+ BEGIN
32745
+ PERFORM public.logistics_acc_guard('server', 'logistics_plan_release_stale');
32746
+ v_txt := 'de ronde heeft deze indeling na ' || v_min || ' minuten niet afgesloten (claim niet afgerond)';
32747
+ FOR r IN
32748
+ UPDATE public.logistics_plan_queue q
32749
+ SET status = CASE WHEN q.attempts >= v_max OR q.reason_code = 'claim_niet_afgerond' THEN 'geblokkeerd' ELSE 'pending' END,
32750
+ error = v_txt,
32751
+ reason = CASE WHEN q.attempts >= v_max OR q.reason_code = 'claim_niet_afgerond'
32752
+ THEN v_txt || '; herhaald, daarom geblokkeerd' ELSE q.reason END,
32753
+ reason_code = 'claim_niet_afgerond',
32754
+ claimed_at = NULL,
32755
+ next_attempt_at = now() + make_interval(mins => LEAST(30, GREATEST(1, q.attempts))),
32756
+ updated_at = now()
32757
+ WHERE q.status = 'running'
32758
+ AND q.claimed_at < now() - make_interval(mins => v_min)
32759
+ RETURNING q.order_id, q.attempts, q.status
32760
+ LOOP
32761
+ v_n := v_n + 1;
32762
+ BEGIN
32763
+ PERFORM public.logistics_trace_order_step(
32764
+ r.order_id, 'stap:beladingindeling', 'claim niet afgerond',
32765
+ CASE WHEN r.status = 'geblokkeerd' THEN 'mislukt' ELSE 'wacht' END,
32766
+ 'ronde sloot de indeling niet af',
32767
+ CASE WHEN r.status = 'geblokkeerd'
32768
+ THEN 'De ronde sloot de indeling voor de tweede keer niet af. De order is geblokkeerd zodat dit niet blijft herhalen; bekijk de reden en bied hem opnieuw aan.'
32769
+ ELSE 'De ronde sloot de indeling niet af binnen ' || v_min || ' minuten. Eén nieuwe poging volgt.'
32770
+ END,
32771
+ 'Karrenindeling', v_txt, 'regel');
32772
+ EXCEPTION WHEN OTHERS THEN NULL;
32773
+ END;
32774
+ END LOOP;
32775
+ RETURN v_n;
32776
+ END $function$
32777
+ ;
32778
+
32779
+ CREATE OR REPLACE FUNCTION public.logistics_plan_enqueue(p_order_id uuid, p_reason text DEFAULT 'change'::text)
32780
+ RETURNS void
32781
+ LANGUAGE plpgsql
32782
+ SECURITY DEFINER
32783
+ SET search_path TO 'public'
32784
+ AS $function$
32785
+ DECLARE
32786
+ v_ok boolean;
32787
+ v_id uuid;
32788
+ v_status text;
32789
+ v_decision text;
32790
+ v_target uuid := p_order_id;
32791
+ v_wacht jsonb;
32792
+ BEGIN
32793
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_plan_enqueue');
32794
+ IF COALESCE(current_setting('logistics.merging', true), '') <> '1' THEN
32795
+ BEGIN
32796
+ PERFORM set_config('logistics.merging', '1', true);
32797
+ v_target := COALESCE(public.logistics_merge_floriday_lines(p_order_id), p_order_id);
32798
+ PERFORM set_config('logistics.merging', '0', true);
32799
+ EXCEPTION WHEN OTHERS THEN
32800
+ v_target := p_order_id;
32801
+ END;
32802
+ END IF;
32803
+
32804
+ -- A2: wachttijd na een mislukte indeling respecteren bij automatische meldingen.
32805
+ v_wacht := public.logistics_plan_wachttijd(v_target, p_reason);
32806
+ IF COALESCE((v_wacht->>'wacht')::boolean, false) THEN
32807
+ PERFORM public.logistics_plan_wachttijd_melden(v_target, p_reason, v_wacht);
32808
+ RETURN;
32809
+ END IF;
32810
+
32811
+ SELECT (o.deleted_at IS NULL AND COALESCE(o.is_reservation, false) = false AND o.plan_locked_at IS NULL)
32812
+ INTO v_ok FROM public.orders o WHERE o.id = v_target;
32813
+ -- 1.0.194: geannuleerde orders niet opnieuw indelen (alleen als de kolommen bestaan).
32814
+ IF COALESCE(v_ok, false) AND public.logistics_order_geannuleerd(v_target) THEN
32815
+ PERFORM public.logistics_trace_order_step(
32816
+ v_target, 'stap:orders_combineren', 'order aanmelden voor indeling', 'overgeslagen',
32817
+ 'niet aangemeld', 'Order is geannuleerd', NULL);
32818
+ RETURN;
32819
+ END IF;
32820
+ IF NOT COALESCE(v_ok, false) THEN
32821
+ PERFORM public.logistics_trace_order_step(
32822
+ v_target, 'stap:orders_combineren', 'order aanmelden voor indeling', 'overgeslagen',
32823
+ 'niet aangemeld', 'Order is verwijderd, samengevoegd, een reservering of de indeling staat vast', NULL);
32824
+ RETURN;
32825
+ END IF;
32826
+
32827
+ SELECT q.id, q.status INTO v_id, v_status
32828
+ FROM public.logistics_plan_queue q
32829
+ WHERE q.order_id = v_target
32830
+ AND q.status IN ('pending', 'running', 'geblokkeerd')
32831
+ ORDER BY q.updated_at DESC
32832
+ LIMIT 1
32833
+ FOR UPDATE;
32834
+
32835
+ IF v_id IS NULL THEN
32836
+ BEGIN
32837
+ INSERT INTO public.logistics_plan_queue(order_id, reason) VALUES (v_target, p_reason);
32838
+ v_decision := 'nieuw aangemeld voor indeling';
32839
+ EXCEPTION WHEN unique_violation THEN
32840
+ v_decision := 'stond al in de wachtrij';
32841
+ END;
32842
+ ELSIF v_status = 'geblokkeerd' THEN
32843
+ UPDATE public.logistics_plan_queue
32844
+ SET status = 'pending', attempts = 0, reason = p_reason, reason_code = NULL,
32845
+ error = NULL, claimed_at = NULL, next_attempt_at = now(), updated_at = now()
32846
+ WHERE id = v_id;
32847
+ v_decision := 'blokkade opgeheven, opnieuw aangemeld';
32848
+ ELSE
32849
+ UPDATE public.logistics_plan_queue
32850
+ SET reason = p_reason, next_attempt_at = LEAST(next_attempt_at, now()), updated_at = now()
32851
+ WHERE id = v_id;
32852
+ v_decision := 'opnieuw aangemeld';
32853
+ END IF;
32854
+
32855
+ PERFORM public.logistics_trace_order_step(
32856
+ v_target, 'stap:orders_combineren', 'order aanmelden voor indeling', 'geslaagd',
32857
+ v_decision, p_reason, 'Karrenindeling');
32858
+
32859
+ PERFORM public.logistics_plan_kick();
32860
+ END;
32861
+ $function$
32862
+ ;
32863
+
32864
+ CREATE OR REPLACE FUNCTION public.logistics_sync_pick_queue_core()
32865
+ RETURNS jsonb
32866
+ LANGUAGE plpgsql
32867
+ SECURITY DEFINER
32868
+ SET search_path TO 'public'
32869
+ AS $function$
32870
+ DECLARE
32871
+ v_type_id uuid; v_station uuid; v_screen uuid; v_status uuid;
32872
+ v_inserted integer := 0; v_updated integer := 0; v_closed integer := 0; v_candidates integer := 0;
32873
+ v_today date; v_forced date; v_pickday date; v_target date; r_unit record; v_start timestamptz; v_due timestamptz;
32874
+ v_carts integer; v_percart boolean := public.logistics_pick_barcode_enabled();
32875
+ v_lines integer; v_breakdown jsonb; v_pickmode text; r_cart record; v_barcode text;
32876
+ v_cart_lines integer; v_fix jsonb; v_cart_payload jsonb; v_cart_rows integer;
32877
+ v_wait timestamptz; v_reason text; v_ok boolean;
32878
+ BEGIN
32879
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_sync_pick_queue_core');
32880
+ v_today := public.logistics_local_today();
32881
+ v_forced := NULLIF(current_setting('logistics.forced_pick_day', true), '')::date;
32882
+ SELECT id INTO v_type_id FROM public.pick_document_types WHERE code = 'pakbon' LIMIT 1;
32883
+ IF v_type_id IS NULL THEN RETURN jsonb_build_object('error', 'documenttype pakbon ontbreekt'); END IF;
32884
+ SELECT id INTO v_status FROM public.pick_document_statuses WHERE code = 'queued' LIMIT 1;
32885
+ v_pickmode := COALESCE(public.logistics_pick_mode()->>'pickMode', '');
32886
+
32887
+ v_station := public.logistics_document_station('pakbon');
32888
+ IF v_station IS NULL THEN
32889
+ SELECT s.station_id INTO v_station FROM public.pick_station_screens s WHERE s.name ILIKE '%pakbon%' ORDER BY s.created_at LIMIT 1;
32890
+ END IF;
32891
+ IF v_station IS NULL THEN SELECT id INTO v_station FROM public.pick_stations ORDER BY created_at LIMIT 1; END IF;
32892
+ SELECT s.id INTO v_screen FROM public.pick_station_screens s
32893
+ WHERE s.station_id = v_station
32894
+ AND (s.name ILIKE '%pakbon%' OR EXISTS (SELECT 1 FROM public.pick_screen_document_types d WHERE d.screen_id = s.id AND d.document_type_id = v_type_id))
32895
+ ORDER BY s.created_at LIMIT 1;
32896
+ IF v_screen IS NULL THEN
32897
+ SELECT s.id INTO v_screen FROM public.pick_station_screens s WHERE s.station_id = v_station ORDER BY created_at LIMIT 1;
32898
+ END IF;
32899
+
32900
+ -- Afsluiten in plaats van verwijderen.
32901
+ v_closed := public.logistics_prune_handled_pick_queue();
32902
+ -- Order geannuleerd of verwijderd: annuleren.
32903
+ v_closed := v_closed + public.logistics_pick_queue_close(ARRAY(
32904
+ SELECT q.id FROM public.pick_queue q
32905
+ WHERE q.cancelled_at IS NULL AND q.completed_at IS NULL
32906
+ AND NOT EXISTS (SELECT 1 FROM public.orders o WHERE o.order_number = q.order_number
32907
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL AND coalesce(o.status, '') <> 'geannuleerd')),
32908
+ 'cancel', 'Order geannuleerd of verwijderd');
32909
+ -- Kar bestaat niet meer: vervallen door herindeling.
32910
+ v_closed := v_closed + public.logistics_pick_queue_close(ARRAY(
32911
+ SELECT q.id FROM public.pick_queue q
32912
+ WHERE q.cancelled_at IS NULL AND q.completed_at IS NULL AND q.printed_at IS NULL
32913
+ AND (q.payload->>'cartRowFor') IS NOT NULL
32914
+ AND NOT EXISTS (SELECT 1 FROM logistics_active.order_carts c WHERE c.id::text = q.reference)),
32915
+ 'cancel', 'Kar vervallen door herindeling');
32916
+ -- Pickmethode niet meer "per kar": karregels vervallen.
32917
+ IF v_pickmode <> 'cart' THEN
32918
+ v_closed := v_closed + public.logistics_pick_queue_close(ARRAY(
32919
+ SELECT q.id FROM public.pick_queue q
32920
+ WHERE q.cancelled_at IS NULL AND q.completed_at IS NULL AND q.printed_at IS NULL
32921
+ AND (q.payload->>'cartRowFor') IS NOT NULL),
32922
+ 'cancel', 'Kar vervallen: pickmethode is niet meer per kar');
32923
+ END IF;
32924
+
32925
+ SELECT min(d) INTO v_pickday FROM (
32926
+ SELECT public.logistics_order_pick_day(ord.order_number) AS d FROM public.orders ord
32927
+ WHERE ord.order_number IS NOT NULL
32928
+ AND public.logistics_order_is_topick(ord.id, ord.order_number)
32929
+ AND NOT public.logistics_order_handled(ord.id)
32930
+ AND public.logistics_order_released(ord.order_number)
32931
+ AND EXISTS (SELECT 1 FROM public.logistics_tasks lt WHERE lt.order_ref = ord.order_number AND lt.task_type = 'pick.execute'
32932
+ AND lt.status NOT IN ('done','cancelled','superseded','skipped'))
32933
+ ) s WHERE d >= v_today;
32934
+ IF v_forced IS NOT NULL THEN v_pickday := v_forced; END IF;
32935
+ v_target := v_pickday;
32936
+
32937
+ IF v_pickday IS NOT NULL THEN
32938
+ SELECT count(*) INTO v_candidates FROM public.orders ord
32939
+ WHERE ord.order_number IS NOT NULL
32940
+ AND public.logistics_order_is_topick(ord.id, ord.order_number)
32941
+ AND NOT public.logistics_order_handled(ord.id)
32942
+ AND (v_forced IS NOT NULL OR public.logistics_order_released(ord.order_number))
32943
+ AND public.logistics_order_pick_day(ord.order_number) = v_target
32944
+ AND EXISTS (SELECT 1 FROM public.logistics_tasks lt WHERE lt.order_ref = ord.order_number AND lt.task_type = 'pick.execute'
32945
+ AND lt.status NOT IN ('done','cancelled','superseded','skipped'));
32946
+ END IF;
32947
+
32948
+ IF v_pickday IS NULL OR COALESCE(v_candidates, 0) = 0 THEN
32949
+ RETURN jsonb_build_object('station', v_station, 'screen', v_screen, 'pickdag', v_pickday, 'toegevoegd', 0,
32950
+ 'afgesloten', v_closed, 'kandidaten', COALESCE(v_candidates, 0),
32951
+ 'per_kar', v_pickmode = 'cart', 'kleinsteTelt', v_percart, 'pickmethode', v_pickmode,
32952
+ 'guard', 'geen kandidaten');
32953
+ END IF;
32954
+
32955
+ -- 1.0.152: een geprinte regel blijft open tot scan, annulering of vervallen kar.
32956
+
32957
+ FOR r_unit IN
32958
+ WITH kandidaat AS (
32959
+ SELECT ord.id, ord.order_number, ord.customer_name, ord.delivery_date, ord.buffer_id, ord.custom_fields
32960
+ FROM public.orders ord
32961
+ WHERE ord.order_number IS NOT NULL
32962
+ AND public.logistics_order_is_topick(ord.id, ord.order_number)
32963
+ AND NOT public.logistics_order_handled(ord.id)
32964
+ AND (v_forced IS NOT NULL OR public.logistics_order_released(ord.order_number))
32965
+ AND public.logistics_order_pick_day(ord.order_number) = v_target
32966
+ AND EXISTS (SELECT 1 FROM public.logistics_tasks lt WHERE lt.order_ref = ord.order_number AND lt.task_type = 'pick.execute'
32967
+ AND lt.status NOT IN ('done','cancelled','superseded','skipped'))
32968
+ )
32969
+ SELECT min(k.order_number) AS order_number,
32970
+ (array_agg(k.id ORDER BY k.order_number))[1] AS lead_id,
32971
+ array_agg(k.order_number ORDER BY k.order_number) AS refs,
32972
+ array_agg(k.id ORDER BY k.order_number) AS ids,
32973
+ (array_agg(k.buffer_id ORDER BY k.order_number) FILTER (WHERE k.buffer_id IS NOT NULL))[1] AS buffer_id,
32974
+ count(*)::int AS leden,
32975
+ (array_agg(k.customer_name ORDER BY k.order_number))[1] AS customer_name,
32976
+ (array_agg(k.delivery_date ORDER BY k.order_number))[1] AS delivery_date,
32977
+ (array_agg(k.custom_fields ORDER BY k.order_number))[1] AS custom_fields
32978
+ FROM kandidaat k GROUP BY COALESCE(k.buffer_id::text, k.id::text) ORDER BY 1
32979
+ LOOP
32980
+ IF v_pickmode = 'cart' AND public.logistics_pakbon_old_order_row(r_unit.ids) THEN CONTINUE; END IF;
32981
+ SELECT min(lt.planned_start), max(COALESCE(lt.due_at, lt.planned_end)) INTO v_start, v_due
32982
+ FROM public.logistics_tasks lt WHERE lt.order_ref = ANY(r_unit.refs) AND lt.task_type IN ('print.packing_slip','pick.execute');
32983
+ SELECT count(DISTINCT c.id)::integer INTO v_carts FROM logistics_active.order_carts c
32984
+ WHERE c.order_id = ANY(r_unit.ids) OR (r_unit.buffer_id IS NOT NULL AND r_unit.leden > 1 AND c.buffer_id = r_unit.buffer_id AND c.order_id IS NULL);
32985
+ SELECT count(*) INTO v_lines FROM logistics_active.order_items oi WHERE oi.order_id = ANY(r_unit.ids);
32986
+ v_breakdown := public.pick_cart_breakdown(r_unit.lead_id);
32987
+
32988
+ IF v_pickmode = 'cart' THEN
32989
+ v_cart_rows := 0;
32990
+ FOR r_cart IN
32991
+ SELECT DISTINCT c.id AS cart_id, c.cart_number FROM logistics_active.order_carts c
32992
+ WHERE c.order_id = ANY(r_unit.ids)
32993
+ OR (r_unit.buffer_id IS NOT NULL AND r_unit.leden > 1 AND c.buffer_id = r_unit.buffer_id AND c.order_id IS NULL)
32994
+ OR EXISTS (SELECT 1 FROM logistics_active.order_cart_items ci JOIN logistics_active.order_items oi ON oi.id = ci.order_item_id
32995
+ WHERE ci.cart_id = c.id AND oi.order_id = ANY(r_unit.ids))
32996
+ ORDER BY c.cart_number
32997
+ LOOP
32998
+ -- Kar al gescand: niets nieuws aanmaken.
32999
+ IF public.logistics_cart_scanned(r_cart.cart_id) THEN CONTINUE; END IF;
33000
+ v_ok := v_forced IS NOT NULL OR public.logistics_cart_released(r_cart.cart_id);
33001
+ v_reason := CASE WHEN v_ok THEN NULL ELSE public.logistics_cart_release_reason(r_cart.cart_id) END;
33002
+ v_wait := CASE WHEN v_ok THEN NULL ELSE public.logistics_cart_wait_until(r_cart.cart_id) END;
33003
+
33004
+ v_barcode := public.logistics_ensure_cart_barcode(r_cart.cart_id);
33005
+ v_fix := public.logistics_cart_fixed_numbering(r_cart.cart_id);
33006
+ SELECT count(*) INTO v_cart_lines FROM logistics_active.order_cart_items ci WHERE ci.cart_id = r_cart.cart_id;
33007
+
33008
+ v_cart_payload := jsonb_build_object(
33009
+ 'source','logistiek-engine','orderId', r_unit.lead_id, 'documentType','pakbon',
33010
+ 'pickDate', v_target, 'perCart', true, 'pickMode', 'cart',
33011
+ 'cartRowFor', r_unit.order_number, 'cartId', r_cart.cart_id,
33012
+ 'isFullCart', public.logistics_cart_is_full(r_cart.cart_id),
33013
+ 'barcode', v_barcode, 'cartCount', v_fix->'cartTotal', 'totalCarts', v_fix->'cartTotal',
33014
+ 'bufferId', r_unit.buffer_id, 'isBuffer', r_unit.leden > 1,
33015
+ 'bufferOrders', to_jsonb(r_unit.refs), 'orderIds', to_jsonb(r_unit.ids),
33016
+ 'printbaar', v_ok, 'wachtTot', v_wait, 'wachtReden', v_reason)
33017
+ || COALESCE(v_breakdown, '{}'::jsonb) || v_fix;
33018
+
33019
+ IF EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.reference = r_cart.cart_id::text
33020
+ AND q.document_type_id = v_type_id AND q.cancelled_at IS NULL) THEN
33021
+ UPDATE public.pick_queue q
33022
+ SET screen_id = COALESCE(v_screen, q.screen_id), station_id = COALESCE(v_station, q.station_id),
33023
+ customer_name = r_unit.customer_name, order_number = r_unit.order_number,
33024
+ document_number = 'PB-' || r_unit.order_number || '-K' || (v_fix->>'cartSeq'),
33025
+ line_count = v_cart_lines, cart_count = 1,
33026
+ due_at = COALESCE(v_wait, v_due, q.due_at),
33027
+ payload = COALESCE(q.payload, '{}'::jsonb) || v_cart_payload, updated_at = now()
33028
+ WHERE q.reference = r_cart.cart_id::text AND q.document_type_id = v_type_id AND q.cancelled_at IS NULL
33029
+ AND (q.screen_id, q.station_id, q.customer_name, q.order_number, q.document_number, q.line_count, q.cart_count, q.due_at, q.payload)
33030
+ IS DISTINCT FROM
33031
+ (COALESCE(v_screen, q.screen_id), COALESCE(v_station, q.station_id), r_unit.customer_name, r_unit.order_number,
33032
+ 'PB-' || r_unit.order_number || '-K' || (v_fix->>'cartSeq'), v_cart_lines, 1,
33033
+ COALESCE(v_wait, v_due, q.due_at), COALESCE(q.payload, '{}'::jsonb) || v_cart_payload);
33034
+ IF FOUND THEN v_updated := v_updated + 1; END IF;
33035
+ ELSE
33036
+ INSERT INTO public.pick_queue (station_id, screen_id, document_type_id, status_id, document_number, order_number, reference,
33037
+ customer_name, delivery_address, line_count, cart_count, priority, announced_at, due_at, payload)
33038
+ VALUES (v_station, v_screen, v_type_id, v_status,
33039
+ 'PB-' || r_unit.order_number || '-K' || (v_fix->>'cartSeq'), r_unit.order_number, r_cart.cart_id::text,
33040
+ r_unit.customer_name, NULLIF(r_unit.custom_fields ->> 'delivery_address', ''),
33041
+ v_cart_lines, 1, 30, now(), COALESCE(v_wait, v_due, (r_unit.delivery_date)::timestamptz), v_cart_payload)
33042
+ ON CONFLICT DO NOTHING;
33043
+ v_inserted := v_inserted + 1;
33044
+ END IF;
33045
+ v_cart_rows := v_cart_rows + 1;
33046
+ END LOOP;
33047
+
33048
+ IF v_cart_rows > 0 THEN
33049
+ v_closed := v_closed + public.logistics_pick_queue_close(ARRAY(
33050
+ SELECT q.id FROM public.pick_queue q
33051
+ WHERE q.order_number = ANY(r_unit.refs) AND q.document_type_id = v_type_id
33052
+ AND (q.payload->>'cartRowFor') IS NULL AND q.printed_at IS NULL
33053
+ AND q.cancelled_at IS NULL AND q.completed_at IS NULL),
33054
+ 'cancel', 'Orderregel vervangen door één regel per kar');
33055
+ END IF;
33056
+ -- Bij picken per kar nooit terugvallen op een orderregel.
33057
+ CONTINUE;
33058
+ END IF;
33059
+
33060
+ IF EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.order_number = r_unit.order_number AND q.document_type_id = v_type_id
33061
+ AND (q.payload ->> 'cartRowFor') IS NULL AND q.cancelled_at IS NULL) THEN
33062
+ UPDATE public.pick_queue q
33063
+ SET screen_id = COALESCE(v_screen, q.screen_id), station_id = COALESCE(v_station, q.station_id),
33064
+ customer_name = r_unit.customer_name, document_number = 'PB-' || r_unit.order_number,
33065
+ line_count = v_lines, cart_count = COALESCE(v_carts, 0), due_at = COALESCE(v_due, q.due_at),
33066
+ payload = COALESCE(q.payload, '{}'::jsonb)
33067
+ || jsonb_build_object('source','logistiek-engine','orderId', r_unit.lead_id, 'documentType','pakbon',
33068
+ 'pickDate', v_target, 'perCart', v_percart, 'cartCount', COALESCE(v_carts, 0),
33069
+ 'bufferId', r_unit.buffer_id, 'isBuffer', r_unit.leden > 1,
33070
+ 'bufferOrders', to_jsonb(r_unit.refs), 'orderIds', to_jsonb(r_unit.ids))
33071
+ || COALESCE(v_breakdown, '{}'::jsonb),
33072
+ updated_at = now()
33073
+ WHERE q.order_number = r_unit.order_number AND q.document_type_id = v_type_id
33074
+ AND (q.payload ->> 'cartRowFor') IS NULL AND q.cancelled_at IS NULL
33075
+ AND (q.screen_id, q.station_id, q.customer_name, q.document_number, q.line_count, q.cart_count, q.due_at, q.payload)
33076
+ IS DISTINCT FROM
33077
+ (COALESCE(v_screen, q.screen_id), COALESCE(v_station, q.station_id), r_unit.customer_name, 'PB-' || r_unit.order_number,
33078
+ v_lines, COALESCE(v_carts, 0), COALESCE(v_due, q.due_at),
33079
+ COALESCE(q.payload, '{}'::jsonb)
33080
+ || jsonb_build_object('source','logistiek-engine','orderId', r_unit.lead_id, 'documentType','pakbon',
33081
+ 'pickDate', v_target, 'perCart', v_percart, 'cartCount', COALESCE(v_carts, 0),
33082
+ 'bufferId', r_unit.buffer_id, 'isBuffer', r_unit.leden > 1,
33083
+ 'bufferOrders', to_jsonb(r_unit.refs), 'orderIds', to_jsonb(r_unit.ids))
33084
+ || COALESCE(v_breakdown, '{}'::jsonb));
33085
+ IF FOUND THEN v_updated := v_updated + 1; END IF;
33086
+ CONTINUE;
33087
+ END IF;
33088
+
33089
+ INSERT INTO public.pick_queue (station_id, screen_id, document_type_id, status_id, document_number, order_number, reference,
33090
+ customer_name, delivery_address, line_count, cart_count, priority, announced_at, due_at, payload)
33091
+ VALUES (v_station, v_screen, v_type_id, v_status, 'PB-' || r_unit.order_number, r_unit.order_number, r_unit.order_number,
33092
+ r_unit.customer_name, NULLIF(r_unit.custom_fields ->> 'delivery_address', ''), v_lines, COALESCE(v_carts, 0), 30, now(),
33093
+ COALESCE(v_due, (r_unit.delivery_date)::timestamptz),
33094
+ jsonb_build_object('source','logistiek-engine','orderId', r_unit.lead_id, 'documentType','pakbon', 'pickDate', v_target,
33095
+ 'perCart', v_percart, 'cartCount', COALESCE(v_carts, 0), 'bufferId', r_unit.buffer_id,
33096
+ 'isBuffer', r_unit.leden > 1, 'bufferOrders', to_jsonb(r_unit.refs), 'orderIds', to_jsonb(r_unit.ids))
33097
+ || COALESCE(v_breakdown, '{}'::jsonb))
33098
+ ON CONFLICT DO NOTHING;
33099
+ v_inserted := v_inserted + 1;
33100
+ END LOOP;
33101
+
33102
+ RETURN jsonb_build_object('station', v_station, 'screen', v_screen, 'pickdag', v_pickday,
33103
+ 'kandidaten', v_candidates, 'per_kar', v_pickmode = 'cart', 'kleinsteTelt', v_percart, 'pickmethode', v_pickmode,
33104
+ 'toegevoegd', v_inserted, 'bijgewerkt', v_updated, 'afgesloten', v_closed);
33105
+ END;
33106
+ $function$
33107
+ ;
33108
+
33109
+ CREATE OR REPLACE FUNCTION public.logistics_sync_pick_queue_backlog()
33110
+ RETURNS jsonb
33111
+ LANGUAGE plpgsql
33112
+ SECURITY DEFINER
33113
+ SET search_path TO 'public'
33114
+ AS $function$
33115
+ DECLARE
33116
+ v_max integer := public.logistics_pick_backlog_max_days();
33117
+ v_type_id uuid; v_status uuid; v_station uuid; v_screen uuid; v_pickmode text;
33118
+ r_unit record; r_cart record; v_barcode text; v_seq integer; v_total integer;
33119
+ v_lines integer; v_carts integer; v_payload jsonb; v_breakdown jsonb;
33120
+ v_ins integer := 0; v_upd integer := 0; v_orders integer := 0;
33121
+ BEGIN
33122
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_sync_pick_queue_backlog');
33123
+ IF v_max IS NULL THEN RETURN jsonb_build_object('achterstand', 'uit'); END IF;
33124
+ v_pickmode := COALESCE(public.logistics_pick_mode()->>'pickMode', '');
33125
+ IF v_pickmode <> 'cart' THEN
33126
+ RETURN jsonb_build_object('achterstand', 'overgeslagen', 'reden', 'alleen bij picken per kar');
33127
+ END IF;
33128
+ SELECT id INTO v_type_id FROM public.pick_document_types WHERE code = 'pakbon' LIMIT 1;
33129
+ SELECT id INTO v_status FROM public.pick_document_statuses WHERE code = 'queued' LIMIT 1;
33130
+ IF v_type_id IS NULL THEN RETURN jsonb_build_object('achterstand', 'fout', 'reden', 'documenttype pakbon ontbreekt'); END IF;
33131
+ v_station := public.logistics_document_station('pakbon');
33132
+ IF v_station IS NULL THEN
33133
+ SELECT s.station_id INTO v_station FROM public.pick_station_screens s WHERE s.name ILIKE '%pakbon%' ORDER BY s.created_at LIMIT 1;
33134
+ END IF;
33135
+ IF v_station IS NULL THEN SELECT id INTO v_station FROM public.pick_stations ORDER BY created_at LIMIT 1; END IF;
33136
+ SELECT s.id INTO v_screen FROM public.pick_station_screens s
33137
+ WHERE s.station_id = v_station
33138
+ AND (s.name ILIKE '%pakbon%' OR EXISTS (SELECT 1 FROM public.pick_screen_document_types d WHERE d.screen_id = s.id AND d.document_type_id = v_type_id))
33139
+ ORDER BY s.created_at LIMIT 1;
33140
+
33141
+ FOR r_unit IN
33142
+ WITH k AS (
33143
+ SELECT o.id, o.order_number, o.customer_name, o.delivery_date, o.buffer_id, o.custom_fields
33144
+ FROM public.orders o WHERE o.order_number IS NOT NULL AND public.logistics_order_is_backlog(o.id))
33145
+ SELECT min(k.order_number) AS order_number,
33146
+ (array_agg(k.id ORDER BY k.order_number))[1] AS lead_id,
33147
+ array_agg(k.order_number ORDER BY k.order_number) AS refs,
33148
+ array_agg(k.id ORDER BY k.order_number) AS ids,
33149
+ (array_agg(k.buffer_id ORDER BY k.order_number) FILTER (WHERE k.buffer_id IS NOT NULL))[1] AS buffer_id,
33150
+ count(*)::int AS leden,
33151
+ (array_agg(k.customer_name ORDER BY k.order_number))[1] AS customer_name,
33152
+ min((k.delivery_date::text)::date) AS delivery_date,
33153
+ (array_agg(k.custom_fields ORDER BY k.order_number))[1] AS custom_fields
33154
+ FROM k GROUP BY COALESCE(k.buffer_id::text, k.id::text) ORDER BY 1
33155
+ LOOP
33156
+ IF public.logistics_pakbon_old_order_row(r_unit.ids) THEN CONTINUE; END IF;
33157
+ v_orders := v_orders + 1;
33158
+ SELECT count(DISTINCT c.id)::int INTO v_carts FROM logistics_active.order_carts c
33159
+ WHERE c.order_id = ANY(r_unit.ids) OR (r_unit.buffer_id IS NOT NULL AND c.buffer_id = r_unit.buffer_id);
33160
+ v_breakdown := public.pick_cart_breakdown(r_unit.lead_id);
33161
+ FOR r_cart IN
33162
+ SELECT DISTINCT c.id AS cart_id, c.cart_number, c.total_carts FROM logistics_active.order_carts c
33163
+ WHERE c.order_id = ANY(r_unit.ids) OR (r_unit.buffer_id IS NOT NULL AND c.buffer_id = r_unit.buffer_id)
33164
+ ORDER BY c.cart_number
33165
+ LOOP
33166
+ IF NOT public.logistics_cart_released(r_cart.cart_id) THEN CONTINUE; END IF;
33167
+ v_barcode := public.logistics_ensure_cart_barcode(r_cart.cart_id);
33168
+ v_seq := NULL; v_total := NULL;
33169
+ SELECT s.cart_seq, s.cart_total INTO v_seq, v_total FROM public.logistics_cart_seq(r_unit.lead_id) s WHERE s.cart_id = r_cart.cart_id;
33170
+ v_seq := COALESCE(v_seq, r_cart.cart_number);
33171
+ v_total := COALESCE(v_total, r_cart.total_carts, v_carts);
33172
+ SELECT count(*) INTO v_lines FROM logistics_active.order_cart_items ci WHERE ci.cart_id = r_cart.cart_id;
33173
+ v_payload := jsonb_build_object(
33174
+ 'source','logistiek-engine','orderId', r_unit.lead_id, 'documentType','pakbon',
33175
+ 'pickDate', r_unit.delivery_date, 'perCart', true, 'pickMode', 'cart',
33176
+ 'cartRowFor', r_unit.order_number, 'cartId', r_cart.cart_id,
33177
+ 'cartSeq', v_seq, 'cartIndex', r_cart.cart_number, 'cartTotal', v_total,
33178
+ 'cartLabel', 'Kar ' || v_seq || ' van ' || COALESCE(v_total, v_seq),
33179
+ 'isFullCart', public.logistics_cart_is_full(r_cart.cart_id),
33180
+ 'barcode', v_barcode, 'cartCount', COALESCE(v_carts, 0),
33181
+ 'bufferId', r_unit.buffer_id, 'isBuffer', r_unit.leden > 1,
33182
+ 'bufferOrders', to_jsonb(r_unit.refs), 'orderIds', to_jsonb(r_unit.ids),
33183
+ 'backlog', true,
33184
+ 'backlogDays', (now() AT TIME ZONE public.logistics_tz())::date - r_unit.delivery_date,
33185
+ 'backlogLabel', 'Achterstand, leverdatum ' || to_char(r_unit.delivery_date, 'DD-MM'))
33186
+ || COALESCE(v_breakdown, '{}'::jsonb);
33187
+ IF EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.reference = r_cart.cart_id::text AND q.document_type_id = v_type_id AND q.cancelled_at IS NULL) THEN
33188
+ UPDATE public.pick_queue q
33189
+ SET customer_name = r_unit.customer_name, order_number = r_unit.order_number,
33190
+ document_number = 'PB-' || r_unit.order_number || '-K' || v_seq,
33191
+ line_count = v_lines, cart_count = 1, is_urgent = true,
33192
+ payload = COALESCE(q.payload, '{}'::jsonb) || v_payload, updated_at = now()
33193
+ WHERE q.reference = r_cart.cart_id::text AND q.document_type_id = v_type_id AND q.cancelled_at IS NULL
33194
+ AND q.printed_at IS NULL AND q.completed_at IS NULL
33195
+ AND (q.customer_name, q.order_number, q.document_number, q.line_count, q.cart_count, q.is_urgent, q.payload)
33196
+ IS DISTINCT FROM
33197
+ (r_unit.customer_name, r_unit.order_number, 'PB-' || r_unit.order_number || '-K' || v_seq, v_lines, 1, true,
33198
+ COALESCE(q.payload, '{}'::jsonb) || v_payload);
33199
+ IF FOUND THEN v_upd := v_upd + 1; END IF;
33200
+ ELSE
33201
+ INSERT INTO public.pick_queue (
33202
+ station_id, screen_id, document_type_id, status_id, document_number, order_number, reference,
33203
+ customer_name, delivery_address, line_count, cart_count, priority, is_urgent, announced_at, due_at, payload
33204
+ ) VALUES (
33205
+ v_station, v_screen, v_type_id, v_status,
33206
+ 'PB-' || r_unit.order_number || '-K' || v_seq, r_unit.order_number, r_cart.cart_id::text,
33207
+ r_unit.customer_name, NULLIF(r_unit.custom_fields ->> 'delivery_address', ''),
33208
+ v_lines, 1, 30, true, now(), (r_unit.delivery_date)::timestamptz, v_payload
33209
+ ) ON CONFLICT DO NOTHING;
33210
+ v_ins := v_ins + 1;
33211
+ END IF;
33212
+ END LOOP;
33213
+ END LOOP;
33214
+ RETURN jsonb_build_object('achterstand', 'aan', 'maxDagen', v_max, 'eenheden', v_orders, 'toegevoegd', v_ins, 'bijgewerkt', v_upd);
33215
+ END $function$
33216
+ ;
33217
+
33218
+ CREATE OR REPLACE FUNCTION public.logistics_order_frozen(p_order_id uuid)
33219
+ RETURNS boolean
33220
+ LANGUAGE plpgsql
33221
+ STABLE SECURITY DEFINER
33222
+ SET search_path TO 'public'
33223
+ AS $function$
33224
+ DECLARE v_number text;
33225
+ BEGIN
33226
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_frozen');
33227
+ SELECT o.order_number INTO v_number FROM public.orders o WHERE o.id = p_order_id;
33228
+ IF v_number IS NULL THEN RETURN false; END IF;
33229
+ IF EXISTS (SELECT 1 FROM public.logistics_order_plan_exclusion x WHERE x.order_id = p_order_id) THEN RETURN true; END IF;
33230
+ IF public.logistics_order_afgemeld(p_order_id) THEN RETURN true; END IF;
33231
+ IF EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.order_number = v_number AND q.cancelled_at IS NULL
33232
+ AND (q.printed_at IS NOT NULL OR q.completed_at IS NOT NULL)) THEN RETURN true; END IF;
33233
+ IF EXISTS (SELECT 1 FROM public.cart_control_items ci WHERE ci.order_number = v_number) THEN RETURN true; END IF;
33234
+ IF public.logistics_order_print_actief(v_number) THEN RETURN true; END IF;
33235
+ RETURN false;
33236
+ END;
33237
+ $function$
33238
+ ;
33239
+
33240
+ CREATE OR REPLACE FUNCTION public.logistics_order_mergeable(p_order_id uuid)
33241
+ RETURNS boolean
33242
+ LANGUAGE plpgsql
33243
+ STABLE SECURITY DEFINER
33244
+ SET search_path TO 'public'
33245
+ AS $function$
33246
+ DECLARE
33247
+ v_ok boolean;
33248
+ BEGIN
33249
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_mergeable');
33250
+ SELECT (o.deleted_at IS NULL
33251
+ AND o.cancelled_at IS NULL
33252
+ AND o.plan_locked_at IS NULL
33253
+ AND COALESCE(o.is_reservation, false) = false
33254
+ AND o.order_source = 'floriday'
33255
+ AND o.floriday_order_group_key IS NOT NULL
33256
+ AND NOT EXISTS (
33257
+ SELECT 1
33258
+ FROM public.pick_queue q
33259
+ WHERE q.order_number = o.order_number
33260
+ AND (q.printed_at IS NOT NULL OR q.completed_at IS NOT NULL)
33261
+ )
33262
+ AND NOT public.logistics_order_print_actief(o.order_number))
33263
+ INTO v_ok
33264
+ FROM public.orders o
33265
+ WHERE o.id = p_order_id;
33266
+
33267
+ RETURN COALESCE(v_ok, false);
33268
+ EXCEPTION WHEN OTHERS THEN
33269
+ RETURN false;
33270
+ END $function$
33271
+ ;
33272
+
33273
+ CREATE OR REPLACE FUNCTION public.logistics_merge_partial_carts(p_limit integer DEFAULT 50)
33274
+ RETURNS jsonb
33275
+ LANGUAGE plpgsql
33276
+ SECURITY DEFINER
33277
+ SET search_path TO 'public'
33278
+ AS $function$
33279
+ declare r record; v_count integer := 0;
33280
+ begin
33281
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_merge_partial_carts');
33282
+ if coalesce(public.logistics_pick_mode()->>'pickMode','') <> 'cart' then
33283
+ return jsonb_build_object('overgeslagen', 'pickmethode is niet per kar');
33284
+ end if;
33285
+ if public.logistics_pick_partial_mode() <> 'after_checkin' then
33286
+ return jsonb_build_object('overgeslagen', 'deelkarren mogen ook voor het aanmeldmoment');
33287
+ end if;
33288
+
33289
+ for r in
33290
+ select distinct o.id as order_id
33291
+ from logistics_active.order_carts c
33292
+ join lateral unnest(public.logistics_cart_order_refs(c.id)) ref on true
33293
+ join public.orders o on o.order_number = ref
33294
+ where not public.logistics_cart_is_full(c.id)
33295
+ and not exists (select 1 from public.pick_queue q
33296
+ where q.reference = c.id::text and q.printed_at is not null)
33297
+ and not public.logistics_order_print_actief(o.order_number)
33298
+ and public.logistics_cart_checkin_at(c.id) is not null
33299
+ limit greatest(coalesce(p_limit, 50), 1)
33300
+ loop
33301
+ begin
33302
+ perform public.enqueue_cart_replan_if_unlocked(r.order_id);
33303
+ v_count := v_count + 1;
33304
+ exception when others then null;
33305
+ end;
33306
+ end loop;
33307
+
33308
+ return jsonb_build_object('opnieuw_ingedeeld', v_count);
33309
+ end; $function$
33310
+ ;
33311
+
33312
+ CREATE OR REPLACE FUNCTION public.logistics_order_is_frozen(p_order_id uuid)
33313
+ RETURNS boolean
33314
+ LANGUAGE sql
33315
+ STABLE SECURITY DEFINER
33316
+ SET search_path TO 'public'
33317
+ AS $function$
33318
+ SELECT public.logistics_acc_guard('ingelogd', 'logistics_order_is_frozen');
33319
+ SELECT EXISTS (
33320
+ SELECT 1 FROM public.pick_queue p
33321
+ WHERE p.cancelled_at IS NULL
33322
+ AND (p.printed_at IS NOT NULL OR p.completed_at IS NOT NULL
33323
+ OR jsonb_array_length(CASE WHEN jsonb_typeof(p.printed_cart_ids) = 'array' THEN p.printed_cart_ids ELSE '[]'::jsonb END) > 0)
33324
+ AND (p.payload->>'orderId' = p_order_id::text
33325
+ OR COALESCE(p.payload->'orderIds', '[]'::jsonb) ? p_order_id::text)
33326
+ )
33327
+ OR EXISTS (
33328
+ SELECT 1 FROM logistics_active.order_carts oc
33329
+ WHERE oc.order_id = p_order_id AND public.logistics_cart_is_frozen(oc.id)
33330
+ )
33331
+ OR public.logistics_order_print_actief((SELECT o.order_number FROM public.orders o WHERE o.id = p_order_id))
33332
+ OR EXISTS (
33333
+ SELECT 1 FROM logistics_active.order_carts oc
33334
+ JOIN logistics_active.order_cart_items oci ON oci.cart_id = oc.id
33335
+ JOIN public.order_items oi ON oi.id = oci.order_item_id
33336
+ WHERE oc.order_id IS NULL AND oi.order_id = p_order_id
33337
+ AND public.logistics_cart_is_frozen(oc.id)
33338
+ )
33339
+ $function$
33340
+ ;
33341
+
33342
+ CREATE OR REPLACE FUNCTION public.logistics_order_on_frozen_shared_cart(p_order_id uuid)
33343
+ RETURNS boolean
33344
+ LANGUAGE sql
33345
+ STABLE SECURITY DEFINER
33346
+ SET search_path TO 'public'
33347
+ AS $function$
33348
+ SELECT public.logistics_acc_guard('server', 'logistics_order_on_frozen_shared_cart');
33349
+ SELECT EXISTS (
33350
+ SELECT 1 FROM public.logistics_shared_cart_peers(p_order_id) p
33351
+ JOIN logistics_active.order_carts c ON c.id = p.cart_id
33352
+ LEFT JOIN public.orders po ON po.id = p.peer_order_id
33353
+ WHERE c.pickup_locked IS TRUE OR c.is_complete IS TRUE
33354
+ OR public.logistics_cart_handled(COALESCE(po.order_number, p.peer_order_id::text), p.cart_number)
33355
+ OR public.logistics_order_afgemeld(p.peer_order_id)
33356
+ OR public.logistics_order_print_actief(po.order_number)
33357
+ OR public.logistics_cart_is_frozen(p.cart_id)
33358
+ OR EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.cancelled_at IS NULL
33359
+ AND q.payload->>'cartId' = p.cart_id::text
33360
+ AND (q.printed_at IS NOT NULL OR q.completed_at IS NOT NULL)))
33361
+ $function$
33362
+ ;
33363
+
33364
+ CREATE OR REPLACE FUNCTION public.logistics_tick()
33365
+ RETURNS jsonb
33366
+ LANGUAGE plpgsql
33367
+ SECURITY DEFINER
33368
+ SET search_path TO 'public'
33369
+ AS $function$
33370
+ DECLARE v_result jsonb := '{}'::jsonb; v_start timestamptz := clock_timestamp(); v_t timestamptz; v_err boolean := false;
33371
+ BEGIN
33372
+ PERFORM public.logistics_acc_guard('server', 'logistics_tick');
33373
+ v_t := clock_timestamp();
33374
+ BEGIN
33375
+ PERFORM set_config('statement_timeout', '10000', true);
33376
+ v_result := v_result || jsonb_build_object('vervoerder', public.logistics_carrier_recalc_run(200));
33377
+ EXCEPTION WHEN OTHERS THEN
33378
+ v_err := true; v_result := v_result || jsonb_build_object('vervoerder', left(SQLERRM, 200));
33379
+ END;
33380
+ v_result := v_result || jsonb_build_object('vervoerder_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33381
+
33382
+ v_t := clock_timestamp();
33383
+ BEGIN
33384
+ PERFORM set_config('statement_timeout', '15000', true);
33385
+ PERFORM public.logistics_rebuffer_pending();
33386
+ v_result := v_result || jsonb_build_object('rebuffer', 'ok');
33387
+ EXCEPTION WHEN OTHERS THEN
33388
+ v_err := true;
33389
+ IF SQLERRM ILIKE '%tijdzone%' THEN
33390
+ v_result := v_result || jsonb_build_object('rebuffer', 'overgeslagen: tijdzone ontbreekt in Werktijden');
33391
+ BEGIN
33392
+ UPDATE public.logistics_rebuffer_state SET last_error = 'Herindelen overgeslagen: de tijdzone ontbreekt in Werktijden.', updated_at = now() WHERE id;
33393
+ EXCEPTION WHEN OTHERS THEN NULL; END;
33394
+ ELSE
33395
+ v_result := v_result || jsonb_build_object('rebuffer', left(SQLERRM, 200));
33396
+ END IF;
33397
+ END;
33398
+ v_result := v_result || jsonb_build_object('rebuffer_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33399
+
33400
+ v_t := clock_timestamp();
33401
+ BEGIN
33402
+ PERFORM set_config('statement_timeout', '20000', true);
33403
+ PERFORM public.run_logistics_tasks_dispatch();
33404
+ v_result := v_result || jsonb_build_object('taken', 'ok');
33405
+ EXCEPTION WHEN OTHERS THEN
33406
+ v_err := true; v_result := v_result || jsonb_build_object('taken', left(SQLERRM, 200));
33407
+ END;
33408
+ v_result := v_result || jsonb_build_object('taken_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33409
+
33410
+ v_t := clock_timestamp();
33411
+ BEGIN
33412
+ PERFORM set_config('statement_timeout', '20000', true);
33413
+ v_result := v_result || jsonb_build_object('afgerond', public.logistics_order_finalize_run(25, 120));
33414
+ EXCEPTION WHEN OTHERS THEN
33415
+ v_err := true; v_result := v_result || jsonb_build_object('afgerond', left(SQLERRM, 200));
33416
+ END;
33417
+ v_result := v_result || jsonb_build_object('afgerond_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33418
+
33419
+ -- 1.0.194: lichte ronde — pickwachtrij en laadtoestand in SQL, niet meer via HTTP.
33420
+ v_t := clock_timestamp();
33421
+ BEGIN
33422
+ PERFORM set_config('statement_timeout', '15000', true);
33423
+ v_result := v_result || jsonb_build_object('pickwachtrij', public.logistics_sync_pick_queue());
33424
+ EXCEPTION WHEN OTHERS THEN
33425
+ v_err := true; v_result := v_result || jsonb_build_object('pickwachtrij', left(SQLERRM, 200));
33426
+ END;
33427
+ v_result := v_result || jsonb_build_object('pickwachtrij_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33428
+
33429
+ v_t := clock_timestamp();
33430
+ BEGIN
33431
+ PERFORM set_config('statement_timeout', '10000', true);
33432
+ v_result := v_result || jsonb_build_object('laadtoestand', public.logistics_refresh_plan_state_als_nodig());
33433
+ EXCEPTION WHEN OTHERS THEN
33434
+ v_err := true; v_result := v_result || jsonb_build_object('laadtoestand', left(SQLERRM, 200));
33435
+ END;
33436
+ v_result := v_result || jsonb_build_object('laadtoestand_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33437
+
33438
+ v_t := clock_timestamp();
33439
+ BEGIN
33440
+ PERFORM set_config('statement_timeout', '10000', true);
33441
+ v_result := v_result || jsonb_build_object('instellingen', public.logistics_ensure_periodiek());
33442
+ EXCEPTION WHEN OTHERS THEN
33443
+ v_err := true; v_result := v_result || jsonb_build_object('instellingen', left(SQLERRM, 200));
33444
+ END;
33445
+ v_result := v_result || jsonb_build_object('instellingen_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33446
+
33447
+ BEGIN
33448
+ INSERT INTO public.logistics_tick_runs(started_at, finished_at, duration_ms, has_error, steps)
33449
+ VALUES (v_start, clock_timestamp(), (extract(epoch from clock_timestamp()-v_start)*1000)::int, v_err, v_result);
33450
+ EXCEPTION WHEN OTHERS THEN NULL;
33451
+ END;
33452
+ PERFORM set_config('statement_timeout', '30000', true);
33453
+ RETURN v_result;
33454
+ END $function$
33455
+ ;
33456
+
33457
+ CREATE OR REPLACE FUNCTION public.logistics_install_check()
33458
+ RETURNS TABLE(onderdeel text, ok boolean, toelichting text)
33459
+ LANGUAGE plpgsql
33460
+ SECURITY DEFINER
33461
+ SET search_path TO 'public'
33462
+ AS $function$
33463
+ DECLARE c text; f text; t text; v_fav text; n bigint; s record;
33464
+ BEGIN
33465
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_install_check');
33466
+ FOREACH f IN ARRAY ARRAY[
33467
+ 'f:logistics_try_uuid','f:logistics_tz','f:logistics_is_beheer','f:logistics_buffer_payload','f:logistics_apply_order_buffer',
33468
+ 'f:logistics_apply_buffer_decision','f:logistics_rebuffer_pending','f:logistics_departure_calc','f:logistics_own_transport_departure',
33469
+ 'f:logistics_order_departure','f:logistics_brief_transport','f:logistics_order_frozen','f:logistics_order_is_frozen',
33470
+ 'f:logistics_order_finalize_run','f:run_logistics_tasks_dispatch','f:enqueue_cart_replan_if_unlocked','f:logistics_tick',
33471
+ 't:logistics_tick_runs',
33472
+ 'f:logistics_belading_bevestig','f:logistics_cart_seq_dubbel','f:logistics_plan_retry_later','f:logistics_own_traysticker_sync',
33473
+ 't:logistics_belading_bevestigingen',
33474
+ 'f:logistics_sticker_copies','f:logistics_sticker_etiket_status','f:logistics_sticker_order_counts','f:logistics_sticker_sync',
33475
+ 'c:logistics_sticker_prints.kopieen_bron',
33476
+ 'f:logistics_carrier_settings_ensure','t:logistics_carrier_settings','t:logistics_carrier_recalc_queue','t:logistics_carrier_events',
33477
+ 'f:logistics_carrier_status','f:logistics_carrier_set_backfill','f:logistics_carrier_preview_existing','c:control_get_todays_carts.carrier_name',
33478
+ 'f:logistics_plan_fail_log'] LOOP
33479
+ onderdeel := 'Engine: ' || substr(f, 3);
33480
+ ok := CASE left(f, 1)
33481
+ WHEN 'f' THEN EXISTS (SELECT 1 FROM pg_proc p WHERE p.pronamespace = 'public'::regnamespace AND p.proname = substr(f, 3))
33482
+ WHEN 't' THEN to_regclass('public.' || substr(f, 3)) IS NOT NULL
33483
+ ELSE CASE WHEN split_part(substr(f, 3), '.', 1) = 'control_get_todays_carts'
33484
+ THEN coalesce(pg_get_function_result(to_regprocedure('public.control_get_todays_carts()')), '') LIKE '%carrier_name%'
33485
+ ELSE EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public'
33486
+ AND table_name = split_part(substr(f, 3), '.', 1) AND column_name = split_part(substr(f, 3), '.', 2)) END
33487
+ END;
33488
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie (1.0.182 t/m 1.0.188) in dev uitvoeren en via schemasync verspreiden' END;
33489
+ RETURN NEXT;
33490
+ END LOOP;
33491
+ FOREACH c IN ARRAY ARRAY['carrier_id','carrier_source','carrier_reason','carrier_resolved_at','carrier_set_by','carrier_set_at'] LOOP
33492
+ onderdeel := 'orders.' || c;
33493
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='orders' AND column_name=c);
33494
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie' END; RETURN NEXT;
33495
+ END LOOP;
33496
+ FOREACH f IN ARRAY ARRAY['logistics_resolve_carrier(uuid)','logistics_resolve_carrier_many(uuid[])',
33497
+ 'logistics_resolve_carrier_for_location(uuid,uuid)','logistics_resolve_carrier_for_location_many(uuid[],uuid)',
33498
+ 'logistics_resolve_carrier_for_location_many(jsonb)',
33499
+ 'logistics_set_order_carrier(uuid,uuid)','logistics_carrier_recalc_run(integer)'] LOOP
33500
+ onderdeel := 'functie ' || f; ok := to_regprocedure('public.' || f) IS NOT NULL;
33501
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie' END; RETURN NEXT;
33502
+ END LOOP;
33503
+ FOREACH t IN ARRAY ARRAY['orders:trg_logistics_orders_carrier_fill','orders:trg_logistics_orders_carrier_changed',
33504
+ 'carriers:trg_logistics_carriers_enqueue','customer_delivery_locations:trg_logistics_locations_enqueue'] LOOP
33505
+ onderdeel := 'trigger ' || split_part(t, ':', 2);
33506
+ ok := EXISTS (SELECT 1 FROM pg_trigger g WHERE g.tgname = split_part(t, ':', 2)
33507
+ AND g.tgrelid = to_regclass('public.' || split_part(t, ':', 1)));
33508
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie' END; RETURN NEXT;
33509
+ END LOOP;
33510
+ onderdeel := 'minuutronde rekent vervoerders';
33511
+ ok := EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'logistics_tick' AND prosrc LIKE '%logistics_carrier_recalc_run%');
33512
+ toelichting := CASE WHEN ok THEN 'ja' ELSE 'nee' END; RETURN NEXT;
33513
+ FOREACH c IN ARRAY ARRAY['carrier_kind','is_favorite','archived_at','delivery_location_glns'] LOOP
33514
+ onderdeel := 'Afhandeling: carriers.' || c;
33515
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='carriers' AND column_name=c);
33516
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Afhandeling' END; RETURN NEXT;
33517
+ END LOOP;
33518
+ FOREACH f IN ARRAY ARRAY['afh_favorite_carrier','afh_city_code','afh_normalize_country','afh_normalize_nl_province'] LOOP
33519
+ onderdeel := 'Afhandeling: ' || f || '()';
33520
+ ok := EXISTS (SELECT 1 FROM pg_proc p WHERE p.pronamespace = 'public'::regnamespace AND p.proname = f);
33521
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Afhandeling' END; RETURN NEXT;
33522
+ END LOOP;
33523
+ onderdeel := 'Favoriete vervoerder ingesteld en bruikbaar';
33524
+ IF to_regprocedure('public.afh_favorite_carrier()') IS NULL THEN ok := false; toelichting := 'functie ontbreekt';
33525
+ ELSE
33526
+ BEGIN v_fav := public.logistics_carrier_favorite_id()::text; EXCEPTION WHEN OTHERS THEN v_fav := NULL; END;
33527
+ ok := public.logistics_try_uuid(v_fav) IS NOT NULL AND public.logistics_carrier_skip_reason(public.logistics_try_uuid(v_fav)) IS NULL;
33528
+ toelichting := CASE WHEN ok THEN 'ja' WHEN v_fav IS NULL THEN 'geen favoriet ingesteld (Afhandeling → Vervoerders)'
33529
+ ELSE public.logistics_carrier_skip_reason(public.logistics_try_uuid(v_fav)) END;
33530
+ END IF;
33531
+ RETURN NEXT;
33532
+ onderdeel := 'Klanten: customers.default_carrier_id';
33533
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='customers' AND column_name='default_carrier_id');
33534
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Klanten' END; RETURN NEXT;
33535
+ IF to_regclass('public.logistics_carrier_settings') IS NOT NULL AND to_regclass('public.orders') IS NOT NULL
33536
+ AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='orders' AND column_name='carrier_source') THEN
33537
+ SELECT * INTO s FROM public.logistics_carrier_settings_ensure();
33538
+ EXECUTE $q$SELECT count(*) FROM public.orders o
33539
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
33540
+ AND coalesce(o.delivery_date, '') ~ '^\d{4}-\d{2}-\d{2}' AND left(o.delivery_date, 10)::date >= current_date
33541
+ AND (o.carrier_source = 'geen' OR (o.carrier_source IS NULL AND o.created_at >= $1))$q$ INTO n USING s.installed_at;
33542
+ onderdeel := 'Open orders zonder vervoerder'; ok := n = 0; toelichting := n || ' order(s)'; RETURN NEXT;
33543
+ onderdeel := 'Schakelaar bestaande orders'; ok := true;
33544
+ toelichting := CASE WHEN coalesce(s.backfill_existing, false) THEN 'aan' ELSE 'uit (bestaande orders wachten op akkoord)' END; RETURN NEXT;
33545
+ END IF;
33546
+ -- 1.0.188: mislukte aanmelding voor (her)planning vanuit de ordertriggers.
33547
+ SELECT count(*) INTO n FROM public.logistics_trace_events e
33548
+ WHERE e.actor IN ('trg_logistics_plan_order_items', 'trg_logistics_plan_orders')
33549
+ AND e.status = 'mislukt' AND e.created_at > now() - interval '24 hours';
33550
+ onderdeel := 'Mislukte herplanning (laatste 24 uur)'; ok := n = 0;
33551
+ toelichting := CASE WHEN n = 0 THEN 'geen' ELSE n || ' keer; laatste: ' || coalesce((SELECT coalesce(e.order_ref, '?') || ': ' || coalesce(e.error, '') FROM public.logistics_trace_events e
33552
+ WHERE e.actor IN ('trg_logistics_plan_order_items', 'trg_logistics_plan_orders') AND e.status = 'mislukt'
33553
+ ORDER BY e.created_at DESC LIMIT 1), '') END;
33554
+ RETURN NEXT;
33555
+ RETURN QUERY SELECT * FROM public.logistics_install_check_194();
33556
+ RETURN QUERY SELECT * FROM public.logistics_access_check();
33557
+ RETURN QUERY SELECT * FROM public.logistics_overload_check();
33558
+ RETURN QUERY SELECT * FROM public.logistics_check_190();
33559
+ END $function$
33560
+ ;
33561
+
33562
+ -- Bij installeren: instellingen aanvullen (idempotent).
33563
+ DO $e$ BEGIN
33564
+ PERFORM public.logistics_ensure_tick_settings();
33565
+ PERFORM public.logistics_ensure_rebuffer_setting();
33566
+ EXCEPTION WHEN OTHERS THEN
33567
+ RAISE NOTICE 'logistiek-engine 1.0.194: instellingen aanvullen overgeslagen (%); de minuutronde doet dit alsnog', SQLERRM;
33568
+ END $e$;
33569
+
33570
+ GRANT EXECUTE ON FUNCTION public.logistics_order_date(text) TO authenticated, service_role;
33571
+ GRANT EXECUTE ON FUNCTION public.logistics_order_date(date) TO authenticated, service_role;
33572
+ GRANT EXECUTE ON FUNCTION public.logistics_print_status(uuid[]) TO authenticated, service_role;
33573
+ GRANT EXECUTE ON FUNCTION public.logistics_kick_metingen(integer) TO authenticated, service_role;
33574
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_194() TO authenticated, service_role;