@flowselections/logistiek-engine 1.0.183 → 1.0.185

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.
@@ -30674,3 +30674,1429 @@ AS $function$
30674
30674
  $function$;
30675
30675
  REVOKE ALL ON FUNCTION public.logistics_sticker_etiket_status() FROM PUBLIC, anon;
30676
30676
  GRANT EXECUTE ON FUNCTION public.logistics_sticker_etiket_status() TO authenticated, service_role;
30677
+
30678
+
30679
+ -- ============================================================================
30680
+ -- 1.0.184 — Vervoerder per order (gedeeld contract; de Engine is eigenaar)
30681
+ -- ============================================================================
30682
+ -- * Kolommen op orders: carrier_id, carrier_source, carrier_reason,
30683
+ -- carrier_resolved_at, carrier_set_by, carrier_set_at.
30684
+ -- * Eén regel: logistics_resolve_carrier_core (vaste volgorde, op id).
30685
+ -- * BEFORE-trigger vult NEW.carrier_* direct (geen extra UPDATE, geen lus).
30686
+ -- * Herberekenlijst wordt alleen gevuld door triggers op carriers en
30687
+ -- customer_delivery_locations; nooit door de trigger op orders.
30688
+ -- * Bestaande orders (vóór installatie) pas na de schakelaar in Beheer.
30689
+ -- * Opslaan of wijzigen van een vervoerder verstuurt nooit een mail.
30690
+ -- Velden van Afhandeling/Klanten worden via to_jsonb gelezen, zodat dit ook
30691
+ -- werkt vóór hun koppelprompts (de stap wordt dan overgeslagen met reden).
30692
+ -- ============================================================================
30693
+
30694
+ ALTER TABLE public.orders ADD COLUMN IF NOT EXISTS carrier_id uuid;
30695
+ ALTER TABLE public.orders ADD COLUMN IF NOT EXISTS carrier_source text;
30696
+ ALTER TABLE public.orders ADD COLUMN IF NOT EXISTS carrier_reason text;
30697
+ ALTER TABLE public.orders ADD COLUMN IF NOT EXISTS carrier_resolved_at timestamptz;
30698
+ ALTER TABLE public.orders ADD COLUMN IF NOT EXISTS carrier_set_by uuid;
30699
+ ALTER TABLE public.orders ADD COLUMN IF NOT EXISTS carrier_set_at timestamptz;
30700
+
30701
+ DO $fs$ BEGIN
30702
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_carrier_source_check') THEN
30703
+ ALTER TABLE public.orders ADD CONSTRAINT orders_carrier_source_check CHECK (carrier_source IS NULL OR carrier_source IN
30704
+ ('handmatig','afleverlocatie','afleverlocatie_gln','klant','werkgebied','favoriet','geen','oud'));
30705
+ END IF;
30706
+ END $fs$;
30707
+
30708
+ DO $fs$ BEGIN
30709
+ IF to_regclass('public.carriers') IS NOT NULL
30710
+ AND NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_carrier_id_fkey') THEN
30711
+ ALTER TABLE public.orders ADD CONSTRAINT orders_carrier_id_fkey FOREIGN KEY (carrier_id)
30712
+ REFERENCES public.carriers(id) ON DELETE SET NULL NOT VALID;
30713
+ END IF;
30714
+ END $fs$;
30715
+
30716
+ CREATE INDEX IF NOT EXISTS orders_carrier_source_idx ON public.orders (carrier_source);
30717
+
30718
+ CREATE TABLE IF NOT EXISTS public.logistics_carrier_settings (
30719
+ id boolean PRIMARY KEY DEFAULT true CHECK (id),
30720
+ installed_at timestamptz NOT NULL DEFAULT now(),
30721
+ backfill_existing boolean NOT NULL DEFAULT false,
30722
+ backfill_set_by uuid,
30723
+ backfill_set_at timestamptz,
30724
+ updated_at timestamptz NOT NULL DEFAULT now()
30725
+ );
30726
+ GRANT SELECT ON public.logistics_carrier_settings TO authenticated;
30727
+ GRANT ALL ON public.logistics_carrier_settings TO service_role;
30728
+ ALTER TABLE public.logistics_carrier_settings ENABLE ROW LEVEL SECURITY;
30729
+ DO $fs$ BEGIN
30730
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='logistics_carrier_settings' AND policyname='logistics_carrier_settings_read') THEN
30731
+ CREATE POLICY logistics_carrier_settings_read ON public.logistics_carrier_settings FOR SELECT TO authenticated USING (true);
30732
+ END IF;
30733
+ END $fs$;
30734
+ -- Eén rij per Shell; gegevens gaan niet mee met kloon of schemasync, dus de rij wordt
30735
+ -- bij gebruik aangemaakt (standaard "uit"). installed_at = moment van eerste gebruik in deze Shell.
30736
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_settings_ensure()
30737
+ RETURNS public.logistics_carrier_settings LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path TO 'public'
30738
+ AS $function$
30739
+ DECLARE r public.logistics_carrier_settings;
30740
+ BEGIN
30741
+ SELECT * INTO r FROM public.logistics_carrier_settings WHERE id;
30742
+ IF NOT FOUND THEN
30743
+ INSERT INTO public.logistics_carrier_settings (id, backfill_existing) VALUES (true, false)
30744
+ ON CONFLICT (id) DO NOTHING;
30745
+ SELECT * INTO r FROM public.logistics_carrier_settings WHERE id;
30746
+ END IF;
30747
+ RETURN r;
30748
+ END $function$;
30749
+ REVOKE ALL ON FUNCTION public.logistics_carrier_settings_ensure() FROM PUBLIC, anon, authenticated;
30750
+ GRANT EXECUTE ON FUNCTION public.logistics_carrier_settings_ensure() TO service_role;
30751
+
30752
+ -- Zichtbare vastlegging van vervoerderwijzigingen en mislukte bufferherberekening.
30753
+ CREATE TABLE IF NOT EXISTS public.logistics_carrier_events (
30754
+ id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
30755
+ order_id uuid NOT NULL,
30756
+ soort text NOT NULL DEFAULT 'info',
30757
+ bericht text NOT NULL,
30758
+ created_at timestamptz NOT NULL DEFAULT now()
30759
+ );
30760
+ GRANT SELECT ON public.logistics_carrier_events TO authenticated;
30761
+ GRANT ALL ON public.logistics_carrier_events TO service_role;
30762
+ ALTER TABLE public.logistics_carrier_events ENABLE ROW LEVEL SECURITY;
30763
+ DO $fe$ BEGIN
30764
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='logistics_carrier_events' AND policyname='logistics_carrier_events_read') THEN
30765
+ CREATE POLICY logistics_carrier_events_read ON public.logistics_carrier_events FOR SELECT TO authenticated USING (true);
30766
+ END IF;
30767
+ END $fe$;
30768
+ CREATE INDEX IF NOT EXISTS logistics_carrier_events_order_idx ON public.logistics_carrier_events (order_id, created_at DESC);
30769
+
30770
+ CREATE TABLE IF NOT EXISTS public.logistics_carrier_recalc_queue (
30771
+ order_id uuid PRIMARY KEY,
30772
+ reden text NOT NULL,
30773
+ queued_at timestamptz NOT NULL DEFAULT now(),
30774
+ processed_at timestamptz,
30775
+ last_checked_at timestamptz
30776
+ );
30777
+ GRANT ALL ON public.logistics_carrier_recalc_queue TO service_role;
30778
+ ALTER TABLE public.logistics_carrier_recalc_queue ENABLE ROW LEVEL SECURITY;
30779
+ CREATE INDEX IF NOT EXISTS logistics_carrier_recalc_queue_open_idx
30780
+ ON public.logistics_carrier_recalc_queue (queued_at) WHERE processed_at IS NULL;
30781
+
30782
+ -- Waarom een vervoerder niet bruikbaar is (NULL = bruikbaar).
30783
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_skip_reason(p_carrier_id uuid)
30784
+ RETURNS text LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
30785
+ AS $function$
30786
+ DECLARE c jsonb;
30787
+ BEGIN
30788
+ IF p_carrier_id IS NULL THEN RETURN 'is leeg'; END IF;
30789
+ SELECT to_jsonb(ca) INTO c FROM public.carriers ca WHERE ca.id = p_carrier_id;
30790
+ IF c IS NULL THEN RETURN 'bestaat niet (meer)'; END IF;
30791
+ IF nullif(c->>'archived_at', '') IS NOT NULL THEN RETURN coalesce(c->>'name', '?') || ' is gearchiveerd'; END IF;
30792
+ IF NOT (c ? 'carrier_kind') THEN
30793
+ RETURN coalesce(c->>'name', '?') || ' heeft nog geen soort (veld carrier_kind ontbreekt nog; Afhandeling)';
30794
+ END IF;
30795
+ IF coalesce(c->>'carrier_kind', '') NOT IN ('extern', 'eigen_vervoer', 'klant_haalt_op') THEN
30796
+ RETURN coalesce(c->>'name', '?') || ' heeft geen soort ingesteld';
30797
+ END IF;
30798
+ IF c->>'carrier_kind' = 'extern' AND nullif(btrim(coalesce(c->>'email', '')), '') IS NULL THEN
30799
+ RETURN coalesce(c->>'name', '?') || ' heeft geen e-mailadres';
30800
+ END IF;
30801
+ RETURN NULL;
30802
+ END $function$;
30803
+
30804
+ -- Heeft de vervoerder een ophaalmoment voor deze leverdag?
30805
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_picks_up_for(p_carrier_id uuid, p_delivery_date date)
30806
+ RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
30807
+ AS $function$
30808
+ SELECT EXISTS (
30809
+ SELECT 1 FROM public.carriers ca
30810
+ CROSS JOIN LATERAL jsonb_array_elements(CASE WHEN jsonb_typeof(ca.pickup_moments) = 'array' THEN ca.pickup_moments ELSE '[]'::jsonb END) m
30811
+ WHERE ca.id = p_carrier_id AND nullif(m->>'pickup_time', '') IS NOT NULL
30812
+ AND (p_delivery_date IS NULL OR ca.pickup_weekdays IS NULL OR cardinality(ca.pickup_weekdays) = 0
30813
+ OR extract(isodow FROM p_delivery_date - coalesce(nullif(m->>'pickup_days', '')::int, 0))::int = ANY (ca.pickup_weekdays)))
30814
+ $function$;
30815
+
30816
+ -- Opgemaakt resultaat volgens het contract.
30817
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_result(p_carrier_id uuid, p_bron text, p_reden text, p_skips text[])
30818
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
30819
+ AS $function$
30820
+ DECLARE c jsonb; v_kind text; v_name text; v_reden text := p_reden;
30821
+ BEGIN
30822
+ IF p_carrier_id IS NOT NULL THEN
30823
+ SELECT to_jsonb(ca) INTO c FROM public.carriers ca WHERE ca.id = p_carrier_id;
30824
+ v_kind := c->>'carrier_kind';
30825
+ v_name := CASE v_kind WHEN 'eigen_vervoer' THEN 'Eigen vervoer' WHEN 'klant_haalt_op' THEN 'Afhalen' ELSE c->>'name' END;
30826
+ END IF;
30827
+ IF coalesce(cardinality(p_skips), 0) > 0 THEN
30828
+ v_reden := v_reden || ' Overgeslagen: ' || array_to_string(p_skips, '; ') || '.';
30829
+ END IF;
30830
+ RETURN jsonb_build_object('carrier_id', p_carrier_id, 'carrier_name', v_name, 'carrier_kind', v_kind,
30831
+ 'bron', p_bron, 'reden', v_reden, 'waarschuwing', NULL, 'overgeslagen', to_jsonb(coalesce(p_skips, '{}'::text[])));
30832
+ END $function$;
30833
+
30834
+ -- Favoriet van Afhandeling op id (afh_favorite_carrier() geeft een rij met carrier_id).
30835
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_favorite_id()
30836
+ RETURNS uuid LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
30837
+ AS $function$
30838
+ DECLARE j jsonb;
30839
+ BEGIN
30840
+ IF to_regprocedure('public.afh_favorite_carrier()') IS NULL THEN RETURN NULL; END IF;
30841
+ EXECUTE 'SELECT to_jsonb(f) FROM public.afh_favorite_carrier() f LIMIT 1' INTO j;
30842
+ RETURN public.logistics_try_uuid(CASE jsonb_typeof(j) WHEN 'object' THEN coalesce(j->>'carrier_id', j->>'id')
30843
+ WHEN 'string' THEN j#>>'{}' END);
30844
+ END $function$;
30845
+
30846
+ -- DE regel. p_order = to_jsonb(orders-rij) (of een deel daarvan).
30847
+ -- Volgorde: 1 handmatig (en 'oud' uit custom_fields.carrier) -> 2 afleverlocatie
30848
+ -- -> 3 GLN-lijst per vervoerder -> 4 standaard van de klant -> 5 werkgebied
30849
+ -- (plaats > provincie > land, ophaalmoment op de leverdag) -> 6 favoriet.
30850
+ CREATE OR REPLACE FUNCTION public.logistics_resolve_carrier_core(p_order jsonb, p_skip_manual boolean DEFAULT false)
30851
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
30852
+ AS $function$
30853
+ DECLARE
30854
+ cf jsonb := CASE WHEN jsonb_typeof(p_order->'custom_fields') = 'object' THEN p_order->'custom_fields' ELSE '{}'::jsonb END;
30855
+ v_cust uuid := public.logistics_try_uuid(p_order->>'customer_id');
30856
+ v_zonder_datum boolean := coalesce((p_order->>'_zonder_datum')::boolean, false);
30857
+ v_date date; v_loc_raw text; v_loc_id uuid; v_gln text;
30858
+ l jsonb; cu jsonb; v_id uuid; v_skip text; v_skips text[] := '{}'::text[];
30859
+ v_country text; v_prov text; v_city text; v_lvl int; v_any boolean; r record; v_fav text;
30860
+ BEGIN
30861
+ IF coalesce(p_order->>'delivery_date', '') ~ '^\d{4}-\d{2}-\d{2}' THEN
30862
+ v_date := left(p_order->>'delivery_date', 10)::date;
30863
+ END IF;
30864
+
30865
+ -- 1. Handmatig gekozen op de order (alleen via logistics_set_order_carrier).
30866
+ IF NOT p_skip_manual AND p_order->>'carrier_source' = 'handmatig' THEN
30867
+ v_id := public.logistics_try_uuid(p_order->>'carrier_id');
30868
+ v_skip := public.logistics_carrier_skip_reason(v_id);
30869
+ IF v_skip IS NULL THEN
30870
+ RETURN public.logistics_carrier_result(v_id, 'handmatig', 'Handmatig gekozen op de order.', v_skips);
30871
+ END IF;
30872
+ v_skips := v_skips || ('handmatige keuze: ' || v_skip);
30873
+ END IF;
30874
+ -- 1b. Overgenomen uit een bestaande custom_fields.carrier (bron 'oud').
30875
+ v_id := public.logistics_try_uuid(cf->>'carrier');
30876
+ IF v_id IS NOT NULL THEN
30877
+ v_skip := public.logistics_carrier_skip_reason(v_id);
30878
+ IF v_skip IS NULL THEN
30879
+ RETURN public.logistics_carrier_result(v_id, 'oud', 'Overgenomen uit de bestaande vervoerder op de order.', v_skips);
30880
+ END IF;
30881
+ v_skips := v_skips || ('bestaande vervoerder op de order: ' || v_skip);
30882
+ END IF;
30883
+
30884
+ -- 2. Afleverlocatie (op id, anders op GLN).
30885
+ v_loc_raw := coalesce(nullif(btrim(cf->>'delivery_location_id'), ''), nullif(btrim(cf->>'delivery_location'), ''));
30886
+ v_loc_id := public.logistics_try_uuid(v_loc_raw);
30887
+ v_gln := coalesce(nullif(btrim(cf->>'delivery_location_gln'), ''), CASE WHEN v_loc_id IS NULL THEN v_loc_raw END);
30888
+ IF v_loc_id IS NOT NULL THEN
30889
+ SELECT to_jsonb(x) INTO l FROM public.customer_delivery_locations x WHERE x.id = v_loc_id;
30890
+ END IF;
30891
+ IF l IS NULL AND v_gln IS NOT NULL THEN
30892
+ SELECT to_jsonb(x) INTO l FROM public.customer_delivery_locations x
30893
+ WHERE x.gln = v_gln
30894
+ ORDER BY (x.customer_id IS NOT DISTINCT FROM v_cust) DESC, x.is_primary DESC NULLS LAST, x.created_at, x.id
30895
+ LIMIT 1;
30896
+ END IF;
30897
+ IF l IS NOT NULL THEN
30898
+ v_gln := coalesce(nullif(btrim(l->>'gln'), ''), v_gln);
30899
+ IF coalesce((l->>'self_pickup')::boolean, false) THEN
30900
+ RETURN jsonb_build_object('carrier_id', NULL, 'carrier_name', 'Afhalen', 'carrier_kind', 'klant_haalt_op',
30901
+ 'bron', 'afleverlocatie', 'reden', 'Klant haalt zelf op (ingesteld bij de afleverlocatie).'
30902
+ || CASE WHEN cardinality(v_skips) > 0 THEN ' Overgeslagen: ' || array_to_string(v_skips, '; ') || '.' ELSE '' END,
30903
+ 'waarschuwing', NULL, 'overgeslagen', to_jsonb(v_skips));
30904
+ END IF;
30905
+ v_id := public.logistics_try_uuid(l->>'carrier_id');
30906
+ IF v_id IS NOT NULL THEN
30907
+ IF coalesce(l->>'carrier_assignment_source', '') <> 'manual' THEN
30908
+ v_skips := v_skips || 'vervoerder bij de afleverlocatie is geen handmatige keuze'::text;
30909
+ ELSE
30910
+ v_skip := public.logistics_carrier_skip_reason(v_id);
30911
+ IF v_skip IS NULL THEN
30912
+ RETURN public.logistics_carrier_result(v_id, 'afleverlocatie', 'Vervoerder van de afleverlocatie.', v_skips);
30913
+ END IF;
30914
+ v_skips := v_skips || ('afleverlocatie: ' || v_skip);
30915
+ END IF;
30916
+ END IF;
30917
+ END IF;
30918
+
30919
+ -- 3. Vervoerder die deze afleverlocatie-GLN bedient (lijst van Afhandeling).
30920
+ IF v_gln IS NOT NULL THEN
30921
+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'carriers' AND column_name = 'delivery_location_glns') THEN
30922
+ FOR r IN SELECT ca.id FROM public.carriers ca
30923
+ WHERE coalesce(to_jsonb(ca)->'delivery_location_glns', '[]'::jsonb) ? v_gln
30924
+ ORDER BY ca.sort_order NULLS LAST, ca.id LOOP
30925
+ v_skip := public.logistics_carrier_skip_reason(r.id);
30926
+ IF v_skip IS NULL THEN
30927
+ RETURN public.logistics_carrier_result(r.id, 'afleverlocatie_gln', 'Vervoerder bedient afleverlocatie-GLN ' || v_gln || '.', v_skips);
30928
+ END IF;
30929
+ v_skips := v_skips || ('GLN-lijst: ' || v_skip);
30930
+ END LOOP;
30931
+ ELSE
30932
+ v_skips := v_skips || 'GLN-lijst per vervoerder bestaat nog niet (Afhandeling)'::text;
30933
+ END IF;
30934
+ END IF;
30935
+
30936
+ -- 4. Standaardvervoerder van de klant (veld van Klanten).
30937
+ IF v_cust IS NOT NULL THEN
30938
+ SELECT to_jsonb(x) INTO cu FROM public.customers x WHERE x.id = v_cust;
30939
+ IF cu IS NOT NULL AND NOT (cu ? 'default_carrier_id') THEN
30940
+ v_skips := v_skips || 'standaardvervoerder per klant bestaat nog niet (Klanten)'::text;
30941
+ ELSE
30942
+ v_id := public.logistics_try_uuid(cu->>'default_carrier_id');
30943
+ IF v_id IS NOT NULL THEN
30944
+ v_skip := public.logistics_carrier_skip_reason(v_id);
30945
+ IF v_skip IS NULL THEN
30946
+ RETURN public.logistics_carrier_result(v_id, 'klant', 'Standaardvervoerder van de klant.', v_skips);
30947
+ END IF;
30948
+ v_skips := v_skips || ('standaard van de klant: ' || v_skip);
30949
+ END IF;
30950
+ END IF;
30951
+ END IF;
30952
+
30953
+ -- 5. Werkgebied: plaats > provincie > land, met een ophaalmoment op de leverdag.
30954
+ IF v_date IS NULL AND NOT v_zonder_datum THEN
30955
+ v_skips := v_skips || 'werkgebied: leverdatum onbekend'::text;
30956
+ ELSE
30957
+ BEGIN
30958
+ v_country := public.afh_normalize_country(coalesce(l->>'country', cu->>'country'));
30959
+ v_city := public.afh_city_code(v_country, coalesce(l->>'city', cu->>'city'));
30960
+ v_prov := public.afh_normalize_nl_province(l->>'province');
30961
+ EXCEPTION WHEN undefined_function THEN
30962
+ v_country := upper(nullif(btrim(coalesce(l->>'country', cu->>'country')), ''));
30963
+ v_city := nullif(btrim(coalesce(l->>'city', cu->>'city')), '');
30964
+ v_prov := nullif(btrim(l->>'province'), '');
30965
+ END;
30966
+ FOR v_lvl IN 1..3 LOOP
30967
+ v_any := false;
30968
+ FOR r IN SELECT ca.id FROM public.carriers ca
30969
+ WHERE CASE v_lvl WHEN 1 THEN v_city IS NOT NULL AND v_city = ANY (ca.service_cities)
30970
+ WHEN 2 THEN v_prov IS NOT NULL AND v_prov = ANY (ca.service_provinces)
30971
+ ELSE v_country IS NOT NULL AND v_country = ANY (ca.service_countries) END
30972
+ ORDER BY ca.sort_order NULLS LAST, ca.id LOOP
30973
+ v_skip := public.logistics_carrier_skip_reason(r.id);
30974
+ IF v_skip IS NULL AND NOT public.logistics_carrier_picks_up_for(r.id, v_date) THEN
30975
+ v_skip := (SELECT name FROM public.carriers WHERE id = r.id) || ' heeft geen ophaalmoment op de leverdag';
30976
+ END IF;
30977
+ IF v_skip IS NULL THEN
30978
+ RETURN public.logistics_carrier_result(r.id, 'werkgebied',
30979
+ 'Werkgebied (' || CASE v_lvl WHEN 1 THEN 'plaats' WHEN 2 THEN 'provincie' ELSE 'land' END || ').', v_skips);
30980
+ END IF;
30981
+ v_skips := v_skips || ('werkgebied: ' || v_skip);
30982
+ END LOOP;
30983
+ END LOOP;
30984
+ END IF;
30985
+
30986
+ -- 6. Favoriete vervoerder van de Shell (Afhandeling).
30987
+ IF to_regprocedure('public.afh_favorite_carrier()') IS NULL THEN
30988
+ v_skips := v_skips || 'favoriete vervoerder bestaat nog niet (Afhandeling)'::text;
30989
+ ELSE
30990
+ BEGIN
30991
+ v_fav := public.logistics_carrier_favorite_id()::text;
30992
+ EXCEPTION WHEN OTHERS THEN
30993
+ v_fav := NULL; v_skips := v_skips || ('favoriet niet te lezen: ' || SQLERRM);
30994
+ END;
30995
+ v_id := public.logistics_try_uuid(v_fav);
30996
+ IF v_id IS NULL THEN
30997
+ v_skips := v_skips || 'er is geen favoriete vervoerder ingesteld'::text;
30998
+ ELSE
30999
+ v_skip := public.logistics_carrier_skip_reason(v_id);
31000
+ IF v_skip IS NULL THEN
31001
+ RETURN public.logistics_carrier_result(v_id, 'favoriet', 'Favoriete vervoerder van de Shell.', v_skips);
31002
+ END IF;
31003
+ v_skips := v_skips || ('favoriet: ' || v_skip);
31004
+ END IF;
31005
+ END IF;
31006
+
31007
+ RETURN jsonb_build_object('carrier_id', NULL, 'carrier_name', NULL, 'carrier_kind', NULL, 'bron', 'geen',
31008
+ 'reden', 'Geen vervoerder: stel een favoriete vervoerder in (Afhandeling → Vervoerders).'
31009
+ || CASE WHEN cardinality(v_skips) > 0 THEN ' Overgeslagen: ' || array_to_string(v_skips, '; ') || '.' ELSE '' END,
31010
+ 'waarschuwing', NULL, 'overgeslagen', to_jsonb(v_skips));
31011
+ END $function$;
31012
+
31013
+ -- Waarom de vervoerder van deze order niet meer automatisch wijzigt (NULL = mag).
31014
+ -- "Aangemeld" = vastlegging van Afhandeling in carrier_signup_dispatches (sent_at gevuld,
31015
+ -- order_ids bevat de order), gelezen via to_jsonb.
31016
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_frozen_reason(p_order_id uuid)
31017
+ RETURNS text LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
31018
+ AS $function$
31019
+ DECLARE v_locked timestamptz; v_hit boolean;
31020
+ BEGIN
31021
+ SELECT plan_locked_at INTO v_locked FROM public.orders WHERE id = p_order_id;
31022
+ IF v_locked IS NOT NULL THEN RETURN 'de planning is vastgezet'; END IF;
31023
+ BEGIN
31024
+ IF public.logistics_order_is_frozen(p_order_id) THEN RETURN 'het picken is begonnen of de order staat op een vastgezette kar'; END IF;
31025
+ EXCEPTION WHEN undefined_function OR undefined_table THEN NULL; END;
31026
+ BEGIN
31027
+ IF public.logistics_order_frozen(p_order_id) THEN RETURN 'de order is gepickt, gecontroleerd of afgemeld'; END IF;
31028
+ EXCEPTION WHEN undefined_function OR undefined_table THEN NULL; END;
31029
+ IF to_regclass('public.carrier_signup_dispatches') IS NOT NULL THEN
31030
+ BEGIN
31031
+ EXECUTE 'SELECT EXISTS (SELECT 1 FROM public.carrier_signup_dispatches d
31032
+ WHERE nullif(to_jsonb(d)->>''sent_at'', '''') IS NOT NULL
31033
+ AND coalesce(to_jsonb(d)->''order_ids'', ''[]''::jsonb) ? $1)'
31034
+ INTO v_hit USING p_order_id::text;
31035
+ IF v_hit THEN RETURN 'de order is al bij de vervoerder aangemeld'; END IF;
31036
+ EXCEPTION WHEN OTHERS THEN NULL; END;
31037
+ END IF;
31038
+ RETURN NULL;
31039
+ END $function$;
31040
+
31041
+ -- Vervoerder van een order volgens het contract (alleen lezen).
31042
+ CREATE OR REPLACE FUNCTION public.logistics_resolve_carrier(p_order_id uuid)
31043
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
31044
+ AS $function$
31045
+ DECLARE o jsonb; r jsonb; v_fr text;
31046
+ BEGIN
31047
+ SELECT to_jsonb(x) INTO o FROM public.orders x WHERE x.id = p_order_id;
31048
+ IF o IS NULL THEN
31049
+ RETURN jsonb_build_object('carrier_id', NULL, 'carrier_name', NULL, 'carrier_kind', NULL, 'bron', 'geen',
31050
+ 'reden', 'Order niet gevonden.', 'waarschuwing', NULL);
31051
+ END IF;
31052
+ r := public.logistics_resolve_carrier_core(o, false);
31053
+ v_fr := public.logistics_carrier_frozen_reason(p_order_id);
31054
+ IF v_fr IS NOT NULL AND o->>'carrier_source' IS NOT NULL
31055
+ AND ((r->>'carrier_id') IS DISTINCT FROM (o->>'carrier_id') OR (r->>'bron') IS DISTINCT FROM (o->>'carrier_source')) THEN
31056
+ r := r || jsonb_build_object('waarschuwing',
31057
+ 'Vervoerder zou nu ' || coalesce(r->>'carrier_name', 'geen') || ' zijn; niet aangepast omdat ' || v_fr || '.');
31058
+ END IF;
31059
+ RETURN r || jsonb_build_object('bevroren', v_fr);
31060
+ END $function$;
31061
+
31062
+ CREATE OR REPLACE FUNCTION public.logistics_resolve_carrier_many(p_order_ids uuid[])
31063
+ RETURNS TABLE(order_id uuid, resultaat jsonb) LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
31064
+ AS $function$
31065
+ SELECT x.id, public.logistics_resolve_carrier(x.id) FROM unnest(coalesce(p_order_ids, '{}'::uuid[])) AS x(id)
31066
+ $function$;
31067
+
31068
+ -- Voor Klanten: dezelfde volgorde zonder stap 1 (geen order, dus geen leverdag).
31069
+ CREATE OR REPLACE FUNCTION public.logistics_resolve_carrier_for_location(p_location_id uuid, p_customer_id uuid)
31070
+ RETURNS jsonb LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
31071
+ AS $function$
31072
+ SELECT public.logistics_resolve_carrier_core(jsonb_build_object(
31073
+ 'customer_id', coalesce(p_customer_id, (SELECT l.customer_id FROM public.customer_delivery_locations l WHERE l.id = p_location_id)),
31074
+ 'custom_fields', jsonb_build_object('delivery_location_id', p_location_id),
31075
+ '_zonder_datum', true), true)
31076
+ $function$;
31077
+
31078
+ CREATE OR REPLACE FUNCTION public.logistics_resolve_carrier_for_location_many(p_location_ids uuid[], p_customer_id uuid)
31079
+ RETURNS TABLE(location_id uuid, resultaat jsonb) LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
31080
+ AS $function$
31081
+ SELECT x.id, public.logistics_resolve_carrier_for_location(x.id, p_customer_id)
31082
+ FROM unnest(coalesce(p_location_ids, '{}'::uuid[])) AS x(id)
31083
+ $function$;
31084
+
31085
+ -- Variant voor een hele klantenlijst: [{location_id, customer_id}, ...] (max. 1000 regels).
31086
+ CREATE OR REPLACE FUNCTION public.logistics_resolve_carrier_for_location_many(p_items jsonb)
31087
+ RETURNS TABLE(location_id uuid, customer_id uuid, carrier_id uuid, carrier_name text, carrier_kind text,
31088
+ bron text, reden text, waarschuwing text)
31089
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
31090
+ AS $function$
31091
+ DECLARE it jsonb; r jsonb; v_loc uuid; v_cust uuid;
31092
+ BEGIN
31093
+ IF auth.uid() IS NULL AND coalesce(auth.role(), '') <> 'service_role' THEN RAISE EXCEPTION 'Niet ingelogd'; END IF;
31094
+ IF p_items IS NULL OR jsonb_typeof(p_items) <> 'array' THEN RAISE EXCEPTION 'p_items moet een lijst zijn: [{location_id, customer_id}, ...]'; END IF;
31095
+ IF jsonb_array_length(p_items) > 1000 THEN RAISE EXCEPTION 'Te veel regels (%); maximaal 1000 per aanroep.', jsonb_array_length(p_items); END IF;
31096
+ FOR it IN SELECT e FROM jsonb_array_elements(p_items) WITH ORDINALITY AS t(e, n) ORDER BY n LOOP
31097
+ v_loc := public.logistics_try_uuid(it->>'location_id');
31098
+ v_cust := public.logistics_try_uuid(it->>'customer_id');
31099
+ IF v_loc IS NULL AND v_cust IS NULL THEN
31100
+ location_id := NULL; customer_id := NULL; carrier_id := NULL; carrier_name := NULL; carrier_kind := NULL;
31101
+ bron := 'geen'; reden := 'Ongeldige regel: geen geldig location_id of customer_id.'; waarschuwing := NULL;
31102
+ RETURN NEXT; CONTINUE;
31103
+ END IF;
31104
+ r := public.logistics_resolve_carrier_for_location(v_loc, v_cust);
31105
+ location_id := v_loc; customer_id := v_cust;
31106
+ carrier_id := public.logistics_try_uuid(r->>'carrier_id'); carrier_name := r->>'carrier_name';
31107
+ carrier_kind := r->>'carrier_kind'; bron := r->>'bron'; reden := r->>'reden'; waarschuwing := r->>'waarschuwing';
31108
+ RETURN NEXT;
31109
+ END LOOP;
31110
+ END $function$;
31111
+
31112
+ -- Opgeslagen vervoerder (of, zolang nog niet opgeslagen, de regel live; niets schrijven).
31113
+ CREATE OR REPLACE FUNCTION public.logistics_order_carrier_info(p_order_id uuid)
31114
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
31115
+ AS $function$
31116
+ DECLARE o jsonb;
31117
+ BEGIN
31118
+ SELECT to_jsonb(x) INTO o FROM public.orders x WHERE x.id = p_order_id;
31119
+ IF o IS NULL THEN RETURN jsonb_build_object('carrier_id', NULL, 'bron', 'geen', 'reden', 'Order niet gevonden.'); END IF;
31120
+ IF o->>'carrier_source' IS NOT NULL THEN
31121
+ IF o->>'carrier_source' = 'afleverlocatie' AND o->>'carrier_id' IS NULL THEN
31122
+ RETURN jsonb_build_object('carrier_id', NULL, 'carrier_name', 'Afhalen', 'carrier_kind', 'klant_haalt_op',
31123
+ 'bron', o->>'carrier_source', 'reden', o->>'carrier_reason', 'opgeslagen', true);
31124
+ END IF;
31125
+ IF o->>'carrier_id' IS NULL THEN
31126
+ RETURN jsonb_build_object('carrier_id', NULL, 'carrier_name', NULL, 'carrier_kind', NULL,
31127
+ 'bron', o->>'carrier_source', 'reden', o->>'carrier_reason', 'opgeslagen', true);
31128
+ END IF;
31129
+ RETURN public.logistics_carrier_result((o->>'carrier_id')::uuid, o->>'carrier_source', o->>'carrier_reason', '{}'::text[])
31130
+ || jsonb_build_object('reden', o->>'carrier_reason', 'opgeslagen', true);
31131
+ END IF;
31132
+ RETURN public.logistics_resolve_carrier_core(o, false) || jsonb_build_object('opgeslagen', false);
31133
+ END $function$;
31134
+
31135
+ CREATE OR REPLACE FUNCTION public.logistics_order_carrier_label(p_order_id uuid)
31136
+ RETURNS text LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
31137
+ AS $function$
31138
+ SELECT public.logistics_order_carrier_info(p_order_id)->>'carrier_name'
31139
+ $function$;
31140
+
31141
+ -- Bestaande functie: nu volgens dezelfde regel.
31142
+ CREATE OR REPLACE FUNCTION public.logistics_order_carrier_id(p_order_id uuid)
31143
+ RETURNS uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
31144
+ AS $function$
31145
+ SELECT public.logistics_try_uuid(public.logistics_order_carrier_info(p_order_id)->>'carrier_id')
31146
+ $function$;
31147
+
31148
+ -- Bestaande functie blijft; dezelfde regel (locatie op id of GLN).
31149
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_for_location(p_location text, p_customer_id uuid)
31150
+ RETURNS uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
31151
+ AS $function$
31152
+ SELECT public.logistics_try_uuid(public.logistics_resolve_carrier_core(jsonb_build_object(
31153
+ 'customer_id', p_customer_id,
31154
+ 'custom_fields', jsonb_build_object('delivery_location', nullif(btrim(coalesce(p_location, '')), '')),
31155
+ '_zonder_datum', true), true)->>'carrier_id')
31156
+ $function$;
31157
+
31158
+ -- BEFORE INSERT/UPDATE op orders: vult NEW.carrier_* direct in.
31159
+ -- Geen lus: deze trigger zet nooit iets op de herberekenlijst en schrijft
31160
+ -- niets anders weg. Engine-schrijvers zetten logistics.carrier_writer = 'engine'.
31161
+ CREATE OR REPLACE FUNCTION public.logistics_orders_carrier_fill()
31162
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
31163
+ AS $function$
31164
+ DECLARE r jsonb; s record;
31165
+ BEGIN
31166
+ IF coalesce(current_setting('logistics.carrier_writer', true), '') = 'engine' THEN RETURN NEW; END IF;
31167
+ IF TG_OP = 'INSERT' THEN
31168
+ IF NEW.carrier_source = 'handmatig' THEN RETURN NEW; END IF;
31169
+ ELSE
31170
+ IF OLD.carrier_source = 'handmatig' THEN RETURN NEW; END IF;
31171
+ IF NEW.customer_id IS NOT DISTINCT FROM OLD.customer_id
31172
+ AND NEW.delivery_date IS NOT DISTINCT FROM OLD.delivery_date
31173
+ AND (NEW.custom_fields->>'delivery_location') IS NOT DISTINCT FROM (OLD.custom_fields->>'delivery_location')
31174
+ AND (NEW.custom_fields->>'delivery_location_id') IS NOT DISTINCT FROM (OLD.custom_fields->>'delivery_location_id')
31175
+ AND (NEW.custom_fields->>'delivery_location_gln') IS NOT DISTINCT FROM (OLD.custom_fields->>'delivery_location_gln')
31176
+ AND (NEW.custom_fields->>'carrier') IS NOT DISTINCT FROM (OLD.custom_fields->>'carrier') THEN
31177
+ RETURN NEW;
31178
+ END IF;
31179
+ SELECT * INTO s FROM public.logistics_carrier_settings_ensure();
31180
+ IF OLD.carrier_source IS NULL AND NOT coalesce(s.backfill_existing, false)
31181
+ AND OLD.created_at < coalesce(s.installed_at, now()) THEN
31182
+ RETURN NEW; -- bestaande order: pas na akkoord (schakelaar in Beheer)
31183
+ END IF;
31184
+ IF NEW.plan_locked_at IS NOT NULL OR public.logistics_carrier_frozen_reason(NEW.id) IS NOT NULL THEN
31185
+ RETURN NEW; -- bevroren: alleen waarschuwing bij het lezen
31186
+ END IF;
31187
+ END IF;
31188
+ BEGIN
31189
+ r := public.logistics_resolve_carrier_core(to_jsonb(NEW), false);
31190
+ EXCEPTION WHEN OTHERS THEN
31191
+ r := jsonb_build_object('carrier_id', NULL, 'bron', 'geen',
31192
+ 'reden', 'Geen vervoerder: de vervoerder kon niet worden bepaald (' || left(SQLERRM, 200) || ').');
31193
+ END;
31194
+ IF TG_OP = 'INSERT'
31195
+ OR public.logistics_try_uuid(r->>'carrier_id') IS DISTINCT FROM NEW.carrier_id
31196
+ OR (r->>'bron') IS DISTINCT FROM NEW.carrier_source
31197
+ OR (r->>'reden') IS DISTINCT FROM NEW.carrier_reason THEN
31198
+ NEW.carrier_id := public.logistics_try_uuid(r->>'carrier_id');
31199
+ NEW.carrier_source := r->>'bron';
31200
+ NEW.carrier_reason := r->>'reden';
31201
+ NEW.carrier_resolved_at := now();
31202
+ END IF;
31203
+ RETURN NEW;
31204
+ END $function$;
31205
+
31206
+ DROP TRIGGER IF EXISTS trg_logistics_orders_carrier_fill ON public.orders;
31207
+ CREATE TRIGGER trg_logistics_orders_carrier_fill BEFORE INSERT OR UPDATE ON public.orders
31208
+ FOR EACH ROW EXECUTE FUNCTION public.logistics_orders_carrier_fill();
31209
+
31210
+ -- AFTER UPDATE: alleen als carrier_id echt verandert en de order niet bevroren is:
31211
+ -- buffer opnieuw bepalen (bestaande regels) en de kar opnieuw laten indelen.
31212
+ CREATE OR REPLACE FUNCTION public.logistics_orders_carrier_changed()
31213
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
31214
+ AS $function$
31215
+ DECLARE v_fr text;
31216
+ BEGIN
31217
+ IF NEW.deleted_at IS NOT NULL THEN RETURN NULL; END IF;
31218
+ v_fr := public.logistics_carrier_frozen_reason(NEW.id);
31219
+ IF v_fr IS NOT NULL THEN
31220
+ BEGIN
31221
+ INSERT INTO public.logistics_carrier_events(order_id, soort, bericht)
31222
+ VALUES (NEW.id, 'bevroren', 'Vervoerder gewijzigd, maar buffer en kar blijven: ' || v_fr || '.');
31223
+ EXCEPTION WHEN undefined_table THEN NULL; END;
31224
+ RETURN NULL;
31225
+ END IF;
31226
+ BEGIN
31227
+ PERFORM public.logistics_apply_order_buffer(NEW.id);
31228
+ EXCEPTION WHEN OTHERS THEN
31229
+ BEGIN
31230
+ INSERT INTO public.logistics_carrier_events(order_id, soort, bericht)
31231
+ VALUES (NEW.id, 'fout', 'Vervoerder gewijzigd; buffer opnieuw bepalen mislukte: ' || left(SQLERRM, 200));
31232
+ EXCEPTION WHEN undefined_table THEN NULL; END;
31233
+ END;
31234
+ BEGIN
31235
+ PERFORM public.enqueue_cart_replan_if_unlocked(NEW.id);
31236
+ EXCEPTION WHEN OTHERS THEN
31237
+ INSERT INTO public.logistics_carrier_events(order_id, soort, bericht)
31238
+ VALUES (NEW.id, 'fout', 'Vervoerder gewijzigd; opnieuw indelen aanvragen mislukte: ' || left(SQLERRM, 200));
31239
+ END;
31240
+ BEGIN
31241
+ INSERT INTO public.logistics_carrier_events(order_id, soort, bericht)
31242
+ VALUES (NEW.id, 'info', 'Vervoerder gewijzigd (' || coalesce(NEW.carrier_source, '?') || '); buffer en kar opnieuw bepaald.');
31243
+ EXCEPTION WHEN undefined_table THEN NULL; END;
31244
+ RETURN NULL;
31245
+ END $function$;
31246
+
31247
+ DROP TRIGGER IF EXISTS trg_logistics_orders_carrier_changed ON public.orders;
31248
+ CREATE TRIGGER trg_logistics_orders_carrier_changed AFTER UPDATE OF carrier_id ON public.orders
31249
+ FOR EACH ROW WHEN (OLD.carrier_id IS DISTINCT FROM NEW.carrier_id)
31250
+ EXECUTE FUNCTION public.logistics_orders_carrier_changed();
31251
+
31252
+ -- Herberekenlijst vullen: alleen open orders, nooit handmatig, bestaande orders pas na de schakelaar.
31253
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_enqueue(p_order_ids uuid[], p_reden text)
31254
+ RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
31255
+ AS $function$
31256
+ DECLARE n int;
31257
+ BEGIN
31258
+ PERFORM public.logistics_carrier_settings_ensure();
31259
+ INSERT INTO public.logistics_carrier_recalc_queue AS q (order_id, reden)
31260
+ SELECT o.id, p_reden FROM public.orders o
31261
+ JOIN public.logistics_carrier_settings s ON s.id
31262
+ WHERE o.id = ANY (p_order_ids)
31263
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
31264
+ AND o.carrier_source IS DISTINCT FROM 'handmatig'
31265
+ AND coalesce(o.delivery_date, '') ~ '^\d{4}-\d{2}-\d{2}' AND left(o.delivery_date, 10)::date >= current_date
31266
+ AND (s.backfill_existing OR o.created_at >= s.installed_at)
31267
+ ON CONFLICT (order_id) DO UPDATE SET processed_at = NULL, queued_at = now(), reden = EXCLUDED.reden;
31268
+ GET DIAGNOSTICS n = ROW_COUNT;
31269
+ RETURN n;
31270
+ END $function$;
31271
+
31272
+ CREATE OR REPLACE FUNCTION public.logistics_carriers_enqueue_orders()
31273
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
31274
+ AS $function$
31275
+ BEGIN
31276
+ BEGIN
31277
+ PERFORM public.logistics_carrier_enqueue(ARRAY(
31278
+ SELECT o.id FROM public.orders o
31279
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
31280
+ AND coalesce(o.delivery_date, '') ~ '^\d{4}-\d{2}-\d{2}' AND left(o.delivery_date, 10)::date >= current_date),
31281
+ 'Vervoerdersinstellingen gewijzigd');
31282
+ EXCEPTION WHEN OTHERS THEN NULL; END;
31283
+ RETURN NULL;
31284
+ END $function$;
31285
+
31286
+ CREATE OR REPLACE FUNCTION public.logistics_locations_enqueue_orders()
31287
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
31288
+ AS $function$
31289
+ DECLARE v_old_gln text;
31290
+ BEGIN
31291
+ IF TG_OP = 'UPDATE' THEN v_old_gln := OLD.gln; END IF;
31292
+ BEGIN
31293
+ PERFORM public.logistics_carrier_enqueue(ARRAY(
31294
+ SELECT o.id FROM public.orders o
31295
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
31296
+ AND ((o.custom_fields->>'delivery_location_id') = NEW.id::text
31297
+ OR (o.custom_fields->>'delivery_location') = NEW.id::text
31298
+ OR (NEW.gln IS NOT NULL AND ((o.custom_fields->>'delivery_location_gln') = NEW.gln OR (o.custom_fields->>'delivery_location') = NEW.gln))
31299
+ OR (v_old_gln IS NOT NULL AND (o.custom_fields->>'delivery_location_gln') = v_old_gln))),
31300
+ 'Afleverlocatie gewijzigd');
31301
+ EXCEPTION WHEN OTHERS THEN NULL; END;
31302
+ RETURN NULL;
31303
+ END $function$;
31304
+
31305
+ DO $fs$ BEGIN
31306
+ IF to_regclass('public.carriers') IS NOT NULL THEN
31307
+ DROP TRIGGER IF EXISTS trg_logistics_carriers_enqueue ON public.carriers;
31308
+ CREATE TRIGGER trg_logistics_carriers_enqueue AFTER INSERT OR UPDATE ON public.carriers
31309
+ FOR EACH STATEMENT EXECUTE FUNCTION public.logistics_carriers_enqueue_orders();
31310
+ ELSE
31311
+ RAISE NOTICE 'logistiek-engine: trigger trg_logistics_carriers_enqueue overgeslagen: tabel carriers ontbreekt';
31312
+ END IF;
31313
+ IF to_regclass('public.customer_delivery_locations') IS NOT NULL THEN
31314
+ DROP TRIGGER IF EXISTS trg_logistics_locations_enqueue ON public.customer_delivery_locations;
31315
+ CREATE TRIGGER trg_logistics_locations_enqueue
31316
+ AFTER INSERT OR UPDATE OF carrier_id, carrier_assignment_source, self_pickup, gln ON public.customer_delivery_locations
31317
+ FOR EACH ROW EXECUTE FUNCTION public.logistics_locations_enqueue_orders();
31318
+ ELSE
31319
+ RAISE NOTICE 'logistiek-engine: trigger trg_logistics_locations_enqueue overgeslagen: tabel customer_delivery_locations ontbreekt';
31320
+ END IF;
31321
+ END $fs$;
31322
+
31323
+ -- Minuutronde: eerst de herberekenlijst, dan het vangnet (open orders die
31324
+ -- langer dan 24 uur niet gecontroleerd zijn). Hooguit p_limit (200) orders per
31325
+ -- ronde. Schrijft ALLEEN als carrier_id, bron of reden verschilt.
31326
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_recalc_run(p_limit integer DEFAULT 200)
31327
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
31328
+ AS $function$
31329
+ DECLARE s record; r record; o jsonb; v jsonb; v_fr text;
31330
+ n_chk int := 0; n_upd int := 0; n_frozen int := 0; n_hand int := 0; n_err int := 0;
31331
+ BEGIN
31332
+ SELECT * INTO s FROM public.logistics_carrier_settings_ensure();
31333
+ IF NOT FOUND THEN RETURN jsonb_build_object('overgeslagen', 'instellingen vervoerder ontbreken'); END IF;
31334
+ FOR r IN
31335
+ WITH kand_open AS (
31336
+ SELECT o.id FROM public.orders o
31337
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
31338
+ AND coalesce(o.delivery_date, '') ~ '^\d{4}-\d{2}-\d{2}' AND left(o.delivery_date, 10)::date >= current_date
31339
+ AND (s.backfill_existing OR o.created_at >= s.installed_at)
31340
+ ), cand AS (
31341
+ SELECT q.order_id AS id, 1 AS prio, q.queued_at AS t
31342
+ FROM public.logistics_carrier_recalc_queue q JOIN kand_open ON kand_open.id = q.order_id
31343
+ WHERE q.processed_at IS NULL
31344
+ UNION ALL
31345
+ SELECT kand_open.id, 2, coalesce(q.last_checked_at, '-infinity'::timestamptz)
31346
+ FROM kand_open LEFT JOIN public.logistics_carrier_recalc_queue q ON q.order_id = kand_open.id
31347
+ WHERE q.order_id IS NULL OR (q.processed_at IS NOT NULL
31348
+ AND (q.last_checked_at IS NULL OR q.last_checked_at < now() - interval '24 hours'))
31349
+ )
31350
+ SELECT c.id FROM (SELECT DISTINCT ON (cand.id) cand.id, cand.prio, cand.t FROM cand ORDER BY cand.id, cand.prio) c
31351
+ ORDER BY c.prio, c.t LIMIT greatest(1, least(coalesce(p_limit, 200), 1000))
31352
+ LOOP
31353
+ BEGIN
31354
+ SELECT to_jsonb(x) INTO o FROM public.orders x WHERE x.id = r.id FOR UPDATE SKIP LOCKED;
31355
+ IF o IS NULL THEN CONTINUE; END IF;
31356
+ n_chk := n_chk + 1;
31357
+ IF o->>'carrier_source' = 'handmatig' THEN
31358
+ n_hand := n_hand + 1;
31359
+ ELSE
31360
+ v_fr := public.logistics_carrier_frozen_reason(r.id);
31361
+ IF v_fr IS NOT NULL THEN
31362
+ n_frozen := n_frozen + 1;
31363
+ ELSE
31364
+ v := public.logistics_resolve_carrier_core(o, false);
31365
+ IF (v->>'carrier_id') IS DISTINCT FROM (o->>'carrier_id')
31366
+ OR (v->>'bron') IS DISTINCT FROM (o->>'carrier_source')
31367
+ OR (v->>'reden') IS DISTINCT FROM (o->>'carrier_reason') THEN
31368
+ PERFORM set_config('logistics.carrier_writer', 'engine', true);
31369
+ UPDATE public.orders
31370
+ SET carrier_id = public.logistics_try_uuid(v->>'carrier_id'), carrier_source = v->>'bron',
31371
+ carrier_reason = v->>'reden', carrier_resolved_at = now()
31372
+ WHERE id = r.id;
31373
+ PERFORM set_config('logistics.carrier_writer', '', true);
31374
+ n_upd := n_upd + 1;
31375
+ END IF;
31376
+ END IF;
31377
+ END IF;
31378
+ INSERT INTO public.logistics_carrier_recalc_queue AS q (order_id, reden, processed_at, last_checked_at)
31379
+ VALUES (r.id, 'Vangnet', now(), now())
31380
+ ON CONFLICT (order_id) DO UPDATE SET processed_at = now(), last_checked_at = now();
31381
+ EXCEPTION WHEN OTHERS THEN
31382
+ PERFORM set_config('logistics.carrier_writer', '', true);
31383
+ n_err := n_err + 1;
31384
+ END;
31385
+ END LOOP;
31386
+ RETURN jsonb_build_object('gecontroleerd', n_chk, 'gewijzigd', n_upd, 'bevroren', n_frozen,
31387
+ 'handmatig', n_hand, 'fouten', n_err, 'bestaandeOrders', s.backfill_existing);
31388
+ END $function$;
31389
+
31390
+ -- Handmatig kiezen: de enige manier. NULL = terug naar automatisch. Verstuurt nooit een mail.
31391
+ CREATE OR REPLACE FUNCTION public.logistics_set_order_carrier(p_order_id uuid, p_carrier_id uuid)
31392
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
31393
+ AS $function$
31394
+ DECLARE v_uid uuid := auth.uid(); o jsonb; v_skip text; r jsonb;
31395
+ BEGIN
31396
+ IF v_uid IS NULL AND coalesce(auth.role(), '') <> 'service_role' THEN RAISE EXCEPTION 'Niet ingelogd'; END IF;
31397
+ SELECT to_jsonb(x) INTO o FROM public.orders x WHERE x.id = p_order_id AND x.deleted_at IS NULL FOR UPDATE;
31398
+ IF o IS NULL THEN RAISE EXCEPTION 'Order niet gevonden.'; END IF;
31399
+ IF p_carrier_id IS NOT NULL THEN
31400
+ v_skip := public.logistics_carrier_skip_reason(p_carrier_id);
31401
+ IF v_skip IS NOT NULL THEN RAISE EXCEPTION 'Deze vervoerder kan niet gekozen worden: %.', v_skip; END IF;
31402
+ r := public.logistics_carrier_result(p_carrier_id, 'handmatig', 'Handmatig gekozen op de order.', '{}'::text[]);
31403
+ ELSE
31404
+ r := public.logistics_resolve_carrier_core(o, true);
31405
+ END IF;
31406
+ PERFORM set_config('logistics.carrier_writer', 'engine', true);
31407
+ UPDATE public.orders
31408
+ SET carrier_id = public.logistics_try_uuid(r->>'carrier_id'), carrier_source = r->>'bron',
31409
+ carrier_reason = r->>'reden', carrier_resolved_at = now(), carrier_set_by = v_uid, carrier_set_at = now()
31410
+ WHERE id = p_order_id;
31411
+ PERFORM set_config('logistics.carrier_writer', '', true);
31412
+ RETURN r || jsonb_build_object('bevroren', public.logistics_carrier_frozen_reason(p_order_id));
31413
+ END $function$;
31414
+
31415
+ -- Lijst F: wat de regel per bestaande open order zou opslaan (alleen lezen).
31416
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_preview_existing(p_limit integer DEFAULT 1000)
31417
+ RETURNS TABLE(order_id uuid, order_number text, order_source text, delivery_date text, huidig_custom_carrier text,
31418
+ carrier_id uuid, carrier_name text, carrier_kind text, bron text, reden text, bevroren text)
31419
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
31420
+ AS $function$
31421
+ BEGIN
31422
+ IF auth.uid() IS NOT NULL AND NOT public.logistics_is_beheer() THEN
31423
+ RAISE EXCEPTION 'Alleen beheer mag deze lijst bekijken.';
31424
+ END IF;
31425
+ RETURN QUERY
31426
+ SELECT o.id, o.order_number, o.order_source, o.delivery_date, o.custom_fields->>'carrier',
31427
+ public.logistics_try_uuid(v.r->>'carrier_id'), v.r->>'carrier_name', v.r->>'carrier_kind', v.r->>'bron', v.r->>'reden',
31428
+ public.logistics_carrier_frozen_reason(o.id)
31429
+ FROM public.orders o
31430
+ CROSS JOIN LATERAL (SELECT public.logistics_resolve_carrier_core(to_jsonb(o), false) AS r) v
31431
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL AND o.carrier_source IS NULL
31432
+ AND coalesce(o.delivery_date, '') ~ '^\d{4}-\d{2}-\d{2}' AND left(o.delivery_date, 10)::date >= current_date
31433
+ ORDER BY o.delivery_date, o.order_number
31434
+ LIMIT greatest(1, coalesce(p_limit, 1000));
31435
+ END $function$;
31436
+
31437
+ -- Schakelaar "Vervoerder berekenen voor bestaande orders" (alleen beheer).
31438
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_set_backfill(p_on boolean)
31439
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
31440
+ AS $function$
31441
+ BEGIN
31442
+ IF auth.uid() IS NULL OR NOT public.logistics_is_beheer() THEN
31443
+ RAISE EXCEPTION 'Alleen beheer mag deze schakelaar wijzigen.';
31444
+ END IF;
31445
+ PERFORM public.logistics_carrier_settings_ensure();
31446
+ UPDATE public.logistics_carrier_settings
31447
+ SET backfill_existing = coalesce(p_on, false), backfill_set_by = auth.uid(), backfill_set_at = now(), updated_at = now()
31448
+ WHERE id;
31449
+ RETURN (SELECT to_jsonb(s) FROM public.logistics_carrier_settings s WHERE s.id);
31450
+ END $function$;
31451
+
31452
+ -- 'oud' -> 'handmatig', alleen na akkoord en met exact aantal (alleen beheer).
31453
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_promote_oud(p_order_ids uuid[], p_confirm_count integer)
31454
+ RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
31455
+ AS $function$
31456
+ DECLARE n int;
31457
+ BEGIN
31458
+ IF auth.uid() IS NULL OR NOT public.logistics_is_beheer() THEN
31459
+ RAISE EXCEPTION 'Alleen beheer mag dit doen.';
31460
+ END IF;
31461
+ SELECT count(*) INTO n FROM public.orders WHERE id = ANY (p_order_ids) AND carrier_source = 'oud';
31462
+ IF n IS DISTINCT FROM p_confirm_count THEN
31463
+ RAISE EXCEPTION 'Aantal klopt niet: % orders met bron oud, bevestigd %.', n, p_confirm_count;
31464
+ END IF;
31465
+ PERFORM set_config('logistics.carrier_writer', 'engine', true);
31466
+ UPDATE public.orders SET carrier_source = 'handmatig', carrier_reason = 'Handmatig (overgenomen uit de bestaande vervoerder).',
31467
+ carrier_set_by = auth.uid(), carrier_set_at = now()
31468
+ WHERE id = ANY (p_order_ids) AND carrier_source = 'oud';
31469
+ PERFORM set_config('logistics.carrier_writer', '', true);
31470
+ RETURN n;
31471
+ END $function$;
31472
+
31473
+ -- Bewaking in Beheer (alleen lezen).
31474
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_status()
31475
+ RETURNS jsonb LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path TO 'public'
31476
+ AS $function$
31477
+ DECLARE s record; v jsonb;
31478
+ BEGIN
31479
+ IF auth.uid() IS NULL AND coalesce(auth.role(), '') <> 'service_role' THEN RAISE EXCEPTION 'Niet ingelogd'; END IF;
31480
+ SELECT * INTO s FROM public.logistics_carrier_settings_ensure();
31481
+ WITH open_orders AS (
31482
+ SELECT o.id, o.order_number, o.delivery_date, o.carrier_source, o.carrier_reason, o.created_at
31483
+ FROM public.orders o
31484
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
31485
+ AND coalesce(o.delivery_date, '') ~ '^\d{4}-\d{2}-\d{2}' AND left(o.delivery_date, 10)::date >= current_date
31486
+ )
31487
+ SELECT jsonb_build_object(
31488
+ 'zonderVervoerder', count(*) FILTER (WHERE carrier_source = 'geen' OR (carrier_source IS NULL AND created_at >= s.installed_at)),
31489
+ 'viaFavoriet', count(*) FILTER (WHERE carrier_source = 'favoriet'),
31490
+ 'nogNietBerekend', count(*) FILTER (WHERE carrier_source IS NULL AND created_at < s.installed_at),
31491
+ 'bronOud', count(*) FILTER (WHERE carrier_source = 'oud'),
31492
+ 'lijstZonder', coalesce(jsonb_agg(jsonb_build_object('orderId', id, 'orderNumber', order_number, 'deliveryDate', delivery_date,
31493
+ 'reden', coalesce(carrier_reason, 'Nog niet berekend.')) ORDER BY delivery_date, order_number)
31494
+ FILTER (WHERE carrier_source = 'geen' OR (carrier_source IS NULL AND created_at >= s.installed_at)), '[]'::jsonb),
31495
+ 'lijstFavoriet', coalesce(jsonb_agg(jsonb_build_object('orderId', id, 'orderNumber', order_number, 'deliveryDate', delivery_date,
31496
+ 'reden', carrier_reason) ORDER BY delivery_date, order_number) FILTER (WHERE carrier_source = 'favoriet'), '[]'::jsonb))
31497
+ INTO v FROM open_orders;
31498
+ RETURN v || jsonb_build_object('bufferFouten', (SELECT count(*) FROM public.logistics_carrier_events e
31499
+ WHERE e.soort = 'fout' AND e.created_at > now() - interval '7 days'), 'bestaandeOrders', coalesce(s.backfill_existing, false),
31500
+ 'installedAt', s.installed_at, 'schakelaarGezetDoor', s.backfill_set_by, 'schakelaarGezetOp', s.backfill_set_at);
31501
+ END $function$;
31502
+
31503
+ -- Installatiecontrole voor de vervoerder-onderdelen (alleen lezen).
31504
+ CREATE OR REPLACE FUNCTION public.logistics_install_check()
31505
+ RETURNS TABLE(onderdeel text, ok boolean, toelichting text)
31506
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path TO 'public'
31507
+ AS $function$
31508
+ DECLARE c text; f text; t text; v_fav text; n bigint; s record;
31509
+ BEGIN
31510
+ -- Engine-onderdelen waar de vervoerder van afhangt + 1.0.179/1.0.182 (functie of tabel op naam).
31511
+ FOREACH f IN ARRAY ARRAY[
31512
+ 'f:logistics_try_uuid','f:logistics_tz','f:logistics_is_beheer','f:logistics_buffer_payload','f:logistics_apply_order_buffer',
31513
+ 'f:logistics_apply_buffer_decision','f:logistics_rebuffer_pending','f:logistics_departure_calc','f:logistics_own_transport_departure',
31514
+ 'f:logistics_order_departure','f:logistics_brief_transport','f:logistics_order_frozen','f:logistics_order_is_frozen',
31515
+ 'f:logistics_order_finalize_run','f:run_logistics_tasks_dispatch','f:enqueue_cart_replan_if_unlocked','f:logistics_tick',
31516
+ 't:logistics_tick_runs',
31517
+ 'f:logistics_belading_bevestig','f:logistics_cart_seq_dubbel','f:logistics_plan_retry_later','f:logistics_own_traysticker_sync',
31518
+ 't:logistics_belading_bevestigingen',
31519
+ 'f:logistics_sticker_copies','f:logistics_sticker_etiket_status','f:logistics_sticker_order_counts','f:logistics_sticker_sync',
31520
+ 'c:logistics_sticker_prints.kopieen_bron',
31521
+ 'f:logistics_carrier_settings_ensure','t:logistics_carrier_settings','t:logistics_carrier_recalc_queue','t:logistics_carrier_events',
31522
+ 'f:logistics_carrier_status','f:logistics_carrier_set_backfill','f:logistics_carrier_preview_existing','c:control_get_todays_carts.carrier_name'] LOOP
31523
+ onderdeel := 'Engine: ' || substr(f, 3);
31524
+ ok := CASE left(f, 1)
31525
+ WHEN 'f' THEN EXISTS (SELECT 1 FROM pg_proc p WHERE p.pronamespace = 'public'::regnamespace AND p.proname = substr(f, 3))
31526
+ WHEN 't' THEN to_regclass('public.' || substr(f, 3)) IS NOT NULL
31527
+ ELSE CASE WHEN split_part(substr(f, 3), '.', 1) = 'control_get_todays_carts'
31528
+ THEN coalesce(pg_get_function_result(to_regprocedure('public.control_get_todays_carts()')), '') LIKE '%carrier_name%'
31529
+ ELSE EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public'
31530
+ AND table_name = split_part(substr(f, 3), '.', 1) AND column_name = split_part(substr(f, 3), '.', 2)) END
31531
+ END;
31532
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie (1.0.182 t/m 1.0.184) in dev uitvoeren en via schemasync verspreiden' END;
31533
+ RETURN NEXT;
31534
+ END LOOP;
31535
+ FOREACH c IN ARRAY ARRAY['carrier_id','carrier_source','carrier_reason','carrier_resolved_at','carrier_set_by','carrier_set_at'] LOOP
31536
+ onderdeel := 'orders.' || c;
31537
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='orders' AND column_name=c);
31538
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie' END; RETURN NEXT;
31539
+ END LOOP;
31540
+ FOREACH f IN ARRAY ARRAY['logistics_resolve_carrier(uuid)','logistics_resolve_carrier_many(uuid[])',
31541
+ 'logistics_resolve_carrier_for_location(uuid,uuid)','logistics_resolve_carrier_for_location_many(uuid[],uuid)',
31542
+ 'logistics_resolve_carrier_for_location_many(jsonb)',
31543
+ 'logistics_set_order_carrier(uuid,uuid)','logistics_carrier_recalc_run(integer)'] LOOP
31544
+ onderdeel := 'functie ' || f; ok := to_regprocedure('public.' || f) IS NOT NULL;
31545
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie' END; RETURN NEXT;
31546
+ END LOOP;
31547
+ FOREACH t IN ARRAY ARRAY['orders:trg_logistics_orders_carrier_fill','orders:trg_logistics_orders_carrier_changed',
31548
+ 'carriers:trg_logistics_carriers_enqueue','customer_delivery_locations:trg_logistics_locations_enqueue'] LOOP
31549
+ onderdeel := 'trigger ' || split_part(t, ':', 2);
31550
+ ok := EXISTS (SELECT 1 FROM pg_trigger g WHERE g.tgname = split_part(t, ':', 2)
31551
+ AND g.tgrelid = to_regclass('public.' || split_part(t, ':', 1)));
31552
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie' END; RETURN NEXT;
31553
+ END LOOP;
31554
+ onderdeel := 'minuutronde rekent vervoerders';
31555
+ ok := EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'logistics_tick' AND prosrc LIKE '%logistics_carrier_recalc_run%');
31556
+ toelichting := CASE WHEN ok THEN 'ja' ELSE 'nee' END; RETURN NEXT;
31557
+ FOREACH c IN ARRAY ARRAY['carrier_kind','is_favorite','archived_at','delivery_location_glns'] LOOP
31558
+ onderdeel := 'Afhandeling: carriers.' || c;
31559
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='carriers' AND column_name=c);
31560
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Afhandeling' END; RETURN NEXT;
31561
+ END LOOP;
31562
+ FOREACH f IN ARRAY ARRAY['afh_favorite_carrier','afh_city_code','afh_normalize_country','afh_normalize_nl_province'] LOOP
31563
+ onderdeel := 'Afhandeling: ' || f || '()';
31564
+ ok := EXISTS (SELECT 1 FROM pg_proc p WHERE p.pronamespace = 'public'::regnamespace AND p.proname = f);
31565
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Afhandeling' END; RETURN NEXT;
31566
+ END LOOP;
31567
+ onderdeel := 'Favoriete vervoerder ingesteld en bruikbaar';
31568
+ IF to_regprocedure('public.afh_favorite_carrier()') IS NULL THEN ok := false; toelichting := 'functie ontbreekt';
31569
+ ELSE
31570
+ BEGIN v_fav := public.logistics_carrier_favorite_id()::text; EXCEPTION WHEN OTHERS THEN v_fav := NULL; END;
31571
+ ok := public.logistics_try_uuid(v_fav) IS NOT NULL AND public.logistics_carrier_skip_reason(public.logistics_try_uuid(v_fav)) IS NULL;
31572
+ toelichting := CASE WHEN ok THEN 'ja' WHEN v_fav IS NULL THEN 'geen favoriet ingesteld (Afhandeling → Vervoerders)'
31573
+ ELSE public.logistics_carrier_skip_reason(public.logistics_try_uuid(v_fav)) END;
31574
+ END IF;
31575
+ RETURN NEXT;
31576
+ onderdeel := 'Klanten: customers.default_carrier_id';
31577
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='customers' AND column_name='default_carrier_id');
31578
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Klanten' END; RETURN NEXT;
31579
+ IF to_regclass('public.logistics_carrier_settings') IS NOT NULL AND to_regclass('public.orders') IS NOT NULL
31580
+ AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='orders' AND column_name='carrier_source') THEN
31581
+ SELECT * INTO s FROM public.logistics_carrier_settings_ensure();
31582
+ EXECUTE $q$SELECT count(*) FROM public.orders o
31583
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
31584
+ AND coalesce(o.delivery_date, '') ~ '^\d{4}-\d{2}-\d{2}' AND left(o.delivery_date, 10)::date >= current_date
31585
+ AND (o.carrier_source = 'geen' OR (o.carrier_source IS NULL AND o.created_at >= $1))$q$ INTO n USING s.installed_at;
31586
+ onderdeel := 'Open orders zonder vervoerder'; ok := n = 0; toelichting := n || ' order(s)'; RETURN NEXT;
31587
+ onderdeel := 'Schakelaar bestaande orders'; ok := true;
31588
+ toelichting := CASE WHEN coalesce(s.backfill_existing, false) THEN 'aan' ELSE 'uit (bestaande orders wachten op akkoord)' END; RETURN NEXT;
31589
+ END IF;
31590
+ END $function$;
31591
+
31592
+ -- Buffergegevens: vervoerder van de order (kolom) gaat voor custom_fields.
31593
+ CREATE OR REPLACE FUNCTION public.logistics_buffer_payload(p_order_id uuid)
31594
+ RETURNS jsonb
31595
+ LANGUAGE sql
31596
+ STABLE SECURITY DEFINER
31597
+ SET search_path TO 'public'
31598
+ AS $function$
31599
+ SELECT jsonb_build_object(
31600
+ 'orderRef', COALESCE(o.order_number, o.id::text),
31601
+ 'checkinAt', COALESCE(cf ->> 'checkin_at', cf ->> 'checkin_moment', cf ->> 'pickup_moment', cf ->> 'pickup_datetime'),
31602
+ 'pickupAt', COALESCE(cf ->> 'pickup_datetime', cf ->> 'pickup_moment', cf ->> 'checkin_at'),
31603
+ 'customerRef', o.customer_id::text,
31604
+ 'floridayCustomerRef', COALESCE(cf ->> 'floriday_customer_id', cf ->> 'floriday_customer_ref'),
31605
+ 'floridaySalesOrderRef', COALESCE(o.floriday_sales_order_id::text, cf ->> 'floriday_sales_order_id'),
31606
+ 'bvoCustomerRef', COALESCE(cf ->> 'bvo_customer_ref', cf ->> 'bvo_customer_id'),
31607
+ 'deliveryLocationRef', COALESCE(cf ->> 'delivery_location_gln', cf ->> 'delivery_location_id', cf ->> 'delivery_location'),
31608
+ 'deliveryDate', o.delivery_date::text,
31609
+ 'carrierRef', CASE WHEN o.carrier_id IS NOT NULL THEN o.carrier_id::text
31610
+ WHEN o.carrier_source IS NOT NULL AND o.carrier_source <> 'geen' THEN 'klant_haalt_op'
31611
+ ELSE COALESCE(cf ->> 'carrier_id', cf ->> 'carrier') END,
31612
+ 'loadCarrierType', COALESCE(cf ->> 'load_carrier_type', 'trolley'),
31613
+ 'orderType', COALESCE(cf ->> 'order_type', o.order_source, 'standaard'),
31614
+ 'isReservation', COALESCE(o.is_reservation, false)
31615
+ )
31616
+ FROM public.orders o
31617
+ CROSS JOIN LATERAL (SELECT COALESCE(o.custom_fields, '{}'::jsonb) AS cf) x
31618
+ WHERE o.id = p_order_id;
31619
+ $function$;
31620
+
31621
+ -- Vertrek per order: eigen vervoer en afhalen op soort; geen vervoerder = luide reden.
31622
+ CREATE OR REPLACE FUNCTION public.logistics_order_departure(p_order_id uuid)
31623
+ RETURNS jsonb
31624
+ LANGUAGE plpgsql
31625
+ STABLE SECURITY DEFINER
31626
+ SET search_path TO 'public'
31627
+ AS $function$
31628
+ DECLARE o record; v jsonb; v_own jsonb; v_txt text; v_at timestamptz; v_info jsonb;
31629
+ BEGIN
31630
+ IF public.logistics_tz() IS NULL THEN
31631
+ RETURN jsonb_build_object('ok', false, 'reason', 'De tijdzone van de vestiging ontbreekt. Stel die in bij Werktijden.');
31632
+ END IF;
31633
+ SELECT id, created_at, delivery_date, custom_fields INTO o FROM public.orders WHERE id = p_order_id;
31634
+ IF NOT FOUND OR coalesce(o.delivery_date,'') !~ '^\d{4}-\d{2}-\d{2}' THEN
31635
+ RETURN jsonb_build_object('ok', false, 'reason', 'De leverdatum van de order is onbekend.');
31636
+ END IF;
31637
+
31638
+ v_own := CASE WHEN jsonb_typeof(o.custom_fields->'own_transport') = 'object' THEN o.custom_fields->'own_transport' END;
31639
+ IF v_own IS NOT NULL THEN
31640
+ v_txt := nullif(btrim(coalesce(v_own->>'pickup_at', '')), '');
31641
+ IF v_txt IS NOT NULL THEN
31642
+ IF v_txt !~ '(Z|[+-]\d{2}(:?\d{2})?)$' THEN
31643
+ RETURN jsonb_build_object('ok', false, 'ownTransport', true,
31644
+ 'reason', 'De ophaaltijd bij eigen vervoer mist een tijdzone.');
31645
+ END IF;
31646
+ BEGIN
31647
+ v_at := v_txt::timestamptz;
31648
+ EXCEPTION WHEN OTHERS THEN
31649
+ RETURN jsonb_build_object('ok', false, 'ownTransport', true,
31650
+ 'reason', 'De ophaaltijd bij eigen vervoer is geen geldige tijd.');
31651
+ END;
31652
+ END IF;
31653
+ RETURN public.logistics_own_transport_departure(left(o.delivery_date,10)::date, v_at);
31654
+ END IF;
31655
+
31656
+ v_info := public.logistics_order_carrier_info(p_order_id);
31657
+ IF v_info->>'carrier_kind' IN ('eigen_vervoer', 'klant_haalt_op') THEN
31658
+ RETURN public.logistics_own_transport_departure(left(o.delivery_date,10)::date, NULL)
31659
+ || jsonb_build_object('carrierKind', v_info->>'carrier_kind', 'carrierLabel', v_info->>'carrier_name',
31660
+ 'carrierSource', v_info->>'bron');
31661
+ END IF;
31662
+ IF public.logistics_try_uuid(v_info->>'carrier_id') IS NULL THEN
31663
+ RETURN jsonb_build_object('ok', false, 'carrierSource', coalesce(v_info->>'bron', 'geen'),
31664
+ 'reason', coalesce(v_info->>'reden', 'Geen vervoerder: stel een favoriete vervoerder in (Afhandeling → Vervoerders).'));
31665
+ END IF;
31666
+
31667
+ v := public.logistics_departure_calc((v_info->>'carrier_id')::uuid, left(o.delivery_date,10)::date, o.created_at, NULL);
31668
+ RETURN (v - 'moments') || jsonb_build_object('carrierSource', v_info->>'bron', 'carrierLabel', v_info->>'carrier_name');
31669
+ END $function$;
31670
+
31671
+ -- Vervoerder op brieven: zelfde regel; eigen vervoer/afhalen als label, nooit "onbekend".
31672
+ CREATE OR REPLACE FUNCTION public.logistics_brief_transport(p_order_id uuid, p_cart_id uuid DEFAULT NULL::uuid)
31673
+ RETURNS jsonb
31674
+ LANGUAGE plpgsql
31675
+ STABLE SECURITY DEFINER
31676
+ SET search_path TO 'public'
31677
+ AS $function$
31678
+ DECLARE
31679
+ o record; v_carrier_id uuid; v_bron text; ca public.carriers%ROWTYPE; v_info jsonb; v_own jsonb;
31680
+ v_naam text; v_code text; v_date date; v_time time; n int;
31681
+ v_wens text; onbekend text[] := '{}'::text[]; uit jsonb := '{}'::jsonb;
31682
+ BEGIN
31683
+ SELECT id, custom_fields, floriday_sales_order_id, delivery_date INTO o FROM public.orders WHERE id = p_order_id AND deleted_at IS NULL;
31684
+ IF o.id IS NULL THEN
31685
+ RETURN jsonb_build_object('onbekend', jsonb_build_array('order'));
31686
+ END IF;
31687
+
31688
+ v_info := public.logistics_order_carrier_info(p_order_id);
31689
+ IF v_info->>'carrier_kind' IN ('eigen_vervoer', 'klant_haalt_op') THEN
31690
+ uit := jsonb_build_object('vervoerder', jsonb_build_object('naam', v_info->>'carrier_name'),
31691
+ 'vervoerder_bron', v_info->>'bron', 'carrier_label', v_info->>'carrier_name', 'carrier_kind', v_info->>'carrier_kind');
31692
+ IF coalesce(o.delivery_date, '') ~ '^\d{4}-\d{2}-\d{2}' THEN
31693
+ v_own := public.logistics_own_transport_departure(left(o.delivery_date, 10)::date, NULL);
31694
+ IF coalesce((v_own->>'ok')::boolean, false) AND v_own->>'departureAt' IS NOT NULL THEN
31695
+ uit := uit || jsonb_build_object('ophaalmoment', v_own->>'departureAt');
31696
+ ELSE
31697
+ onbekend := array_append(onbekend, 'ophaalmoment');
31698
+ END IF;
31699
+ ELSE
31700
+ onbekend := array_append(onbekend, 'ophaalmoment');
31701
+ END IF;
31702
+ RETURN uit || jsonb_build_object('onbekend', to_jsonb(onbekend));
31703
+ END IF;
31704
+
31705
+ v_carrier_id := public.logistics_try_uuid(v_info->>'carrier_id');
31706
+ v_bron := v_info->>'bron';
31707
+ IF v_carrier_id IS NOT NULL THEN
31708
+ SELECT * INTO ca FROM public.carriers WHERE id = v_carrier_id;
31709
+ END IF;
31710
+
31711
+ IF ca.id IS NOT NULL AND nullif(trim(ca.name),'') IS NOT NULL THEN
31712
+ uit := uit || jsonb_build_object('vervoerder', jsonb_build_object('naam', ca.name), 'vervoerder_bron', v_bron,
31713
+ 'carrier_label', ca.name, 'carrier_kind', v_info->>'carrier_kind');
31714
+ ELSE
31715
+ SELECT count(*), min(fc.name), min(nullif(fc.gln_code,'')) INTO n, v_naam, v_code
31716
+ FROM public.floriday_delivery_orders_cache d
31717
+ JOIN public.floriday_carriers_cache fc ON fc.organization_id::text = d.data->>'carrierOrganizationId'
31718
+ WHERE o.floriday_sales_order_id IS NOT NULL
31719
+ AND d.data::text LIKE '%' || o.floriday_sales_order_id::text || '%'
31720
+ AND coalesce(d.is_deleted,false) = false;
31721
+ IF n = 1 AND nullif(trim(v_naam),'') IS NOT NULL THEN
31722
+ uit := uit || jsonb_build_object('vervoerder', jsonb_build_object('naam', v_naam) ||
31723
+ CASE WHEN v_code IS NOT NULL THEN jsonb_build_object('code', v_code) ELSE '{}'::jsonb END,
31724
+ 'vervoerder_bron', 'floriday', 'carrier_label', v_naam);
31725
+ ELSE
31726
+ onbekend := array_append(onbekend, CASE WHEN n > 1 THEN 'vervoerder_niet_eenduidig' ELSE 'vervoerder' END);
31727
+ END IF;
31728
+ END IF;
31729
+
31730
+ SELECT pick_date INTO v_date FROM public.logistics_order_pick_day WHERE order_id = p_order_id;
31731
+ IF v_date IS NULL OR ca.id IS NULL OR jsonb_typeof(ca.pickup_moments) IS DISTINCT FROM 'array' THEN
31732
+ onbekend := array_append(onbekend, 'ophaalmoment');
31733
+ ELSIF ca.pickup_weekdays IS NOT NULL AND array_length(ca.pickup_weekdays,1) > 0
31734
+ AND NOT (extract(isodow FROM v_date)::int = ANY (ca.pickup_weekdays)) THEN
31735
+ onbekend := array_append(onbekend, 'ophaalmoment');
31736
+ ELSE
31737
+ v_wens := nullif(o.custom_fields->>'pickup_moment','');
31738
+ SELECT count(*), min(nullif(m->>'pickup_time','')::time) INTO n, v_time
31739
+ FROM jsonb_array_elements(ca.pickup_moments) m
31740
+ WHERE nullif(m->>'pickup_time','') IS NOT NULL
31741
+ AND coalesce(nullif(m->>'pickup_days',''),'0') = '0'
31742
+ AND (v_wens IS NULL OR left(m->>'pickup_time',5) = left(v_wens,5));
31743
+ IF n = 1 THEN
31744
+ uit := uit || jsonb_build_object('ophaalmoment',
31745
+ to_char(((v_date + v_time) AT TIME ZONE public.logistics_tz()) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"'));
31746
+ ELSE
31747
+ onbekend := array_append(onbekend, CASE WHEN n > 1 THEN 'ophaalmoment_niet_eenduidig' ELSE 'ophaalmoment' END);
31748
+ END IF;
31749
+ END IF;
31750
+
31751
+ RETURN uit || jsonb_build_object('onbekend', to_jsonb(onbekend));
31752
+ END;
31753
+ $function$;
31754
+
31755
+ -- Minuutronde met stap 'vervoerder' (hooguit 200 orders per ronde).
31756
+ CREATE OR REPLACE FUNCTION public.logistics_tick()
31757
+ RETURNS jsonb
31758
+ LANGUAGE plpgsql
31759
+ SECURITY DEFINER
31760
+ SET search_path TO 'public'
31761
+ AS $function$
31762
+ DECLARE v_result jsonb := '{}'::jsonb; v_start timestamptz := clock_timestamp(); v_t timestamptz; v_err boolean := false;
31763
+ BEGIN
31764
+ v_t := clock_timestamp();
31765
+ BEGIN
31766
+ PERFORM set_config('statement_timeout', '10000', true);
31767
+ v_result := v_result || jsonb_build_object('vervoerder', public.logistics_carrier_recalc_run(200));
31768
+ EXCEPTION WHEN OTHERS THEN
31769
+ v_err := true; v_result := v_result || jsonb_build_object('vervoerder', left(SQLERRM, 200));
31770
+ END;
31771
+ v_result := v_result || jsonb_build_object('vervoerder_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
31772
+
31773
+ v_t := clock_timestamp();
31774
+ BEGIN
31775
+ PERFORM set_config('statement_timeout', '15000', true);
31776
+ PERFORM public.logistics_rebuffer_pending();
31777
+ v_result := v_result || jsonb_build_object('rebuffer', 'ok');
31778
+ EXCEPTION WHEN OTHERS THEN
31779
+ v_err := true;
31780
+ IF SQLERRM ILIKE '%tijdzone%' THEN
31781
+ v_result := v_result || jsonb_build_object('rebuffer', 'overgeslagen: tijdzone ontbreekt in Werktijden');
31782
+ BEGIN
31783
+ UPDATE public.logistics_rebuffer_state SET last_error = 'Herindelen overgeslagen: de tijdzone ontbreekt in Werktijden.', updated_at = now() WHERE id;
31784
+ EXCEPTION WHEN OTHERS THEN NULL; END;
31785
+ ELSE
31786
+ v_result := v_result || jsonb_build_object('rebuffer', left(SQLERRM, 200));
31787
+ END IF;
31788
+ END;
31789
+ v_result := v_result || jsonb_build_object('rebuffer_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
31790
+
31791
+ v_t := clock_timestamp();
31792
+ BEGIN
31793
+ PERFORM set_config('statement_timeout', '20000', true);
31794
+ PERFORM public.run_logistics_tasks_dispatch();
31795
+ v_result := v_result || jsonb_build_object('taken', 'ok');
31796
+ EXCEPTION WHEN OTHERS THEN
31797
+ v_err := true; v_result := v_result || jsonb_build_object('taken', left(SQLERRM, 200));
31798
+ END;
31799
+ v_result := v_result || jsonb_build_object('taken_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
31800
+
31801
+ v_t := clock_timestamp();
31802
+ BEGIN
31803
+ PERFORM set_config('statement_timeout', '20000', true);
31804
+ v_result := v_result || jsonb_build_object('afgerond', public.logistics_order_finalize_run(25, 120));
31805
+ EXCEPTION WHEN OTHERS THEN
31806
+ v_err := true; v_result := v_result || jsonb_build_object('afgerond', left(SQLERRM, 200));
31807
+ END;
31808
+ v_result := v_result || jsonb_build_object('afgerond_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
31809
+
31810
+ BEGIN
31811
+ INSERT INTO public.logistics_tick_runs(started_at, finished_at, duration_ms, has_error, steps)
31812
+ VALUES (v_start, clock_timestamp(), (extract(epoch from clock_timestamp()-v_start)*1000)::int, v_err, v_result);
31813
+ EXCEPTION WHEN OTHERS THEN NULL;
31814
+ END;
31815
+ PERFORM set_config('statement_timeout', '30000', true);
31816
+ RETURN v_result;
31817
+ END $function$;
31818
+
31819
+ -- Rechten: API-functies voor ingelogde gebruikers; interne functies alleen service_role.
31820
+ DO $fs$ DECLARE f text; BEGIN
31821
+ FOREACH f IN ARRAY ARRAY['logistics_resolve_carrier(uuid)','logistics_resolve_carrier_many(uuid[])',
31822
+ 'logistics_resolve_carrier_for_location(uuid,uuid)','logistics_resolve_carrier_for_location_many(uuid[],uuid)',
31823
+ 'logistics_set_order_carrier(uuid,uuid)','logistics_carrier_status()','logistics_carrier_set_backfill(boolean)',
31824
+ 'logistics_carrier_preview_existing(integer)','logistics_carrier_promote_oud(uuid[],integer)',
31825
+ 'logistics_order_carrier_label(uuid)','logistics_install_check()','logistics_order_departure(uuid)','logistics_resolve_carrier_for_location_many(jsonb)'] LOOP
31826
+ EXECUTE format('REVOKE ALL ON FUNCTION public.%s FROM PUBLIC, anon', f);
31827
+ EXECUTE format('GRANT EXECUTE ON FUNCTION public.%s TO authenticated, service_role', f);
31828
+ END LOOP;
31829
+ FOREACH f IN ARRAY ARRAY['logistics_carrier_skip_reason(uuid)','logistics_carrier_picks_up_for(uuid,date)','logistics_carrier_favorite_id()',
31830
+ 'logistics_carrier_result(uuid,text,text,text[])','logistics_resolve_carrier_core(jsonb,boolean)',
31831
+ 'logistics_carrier_frozen_reason(uuid)','logistics_order_carrier_info(uuid)','logistics_carrier_enqueue(uuid[],text)',
31832
+ 'logistics_carrier_recalc_run(integer)','logistics_orders_carrier_fill()','logistics_orders_carrier_changed()',
31833
+ 'logistics_carriers_enqueue_orders()','logistics_locations_enqueue_orders()',
31834
+ 'logistics_order_carrier_id(uuid)','logistics_carrier_for_location(text,uuid)','logistics_buffer_payload(uuid)',
31835
+ 'logistics_brief_transport(uuid,uuid)','logistics_tick()'] LOOP
31836
+ EXECUTE format('REVOKE ALL ON FUNCTION public.%s FROM PUBLIC, anon, authenticated', f);
31837
+ EXECUTE format('GRANT EXECUTE ON FUNCTION public.%s TO service_role', f);
31838
+ END LOOP;
31839
+ END $fs$;
31840
+
31841
+ -- 1.0.184: karrenlijst toont de vervoerder uit orders.carrier_id (achteraan toegevoegd).
31842
+ -- "Eigen vervoer" en "Afhalen" op carrier_kind / self_pickup, nooit op naam.
31843
+ DO $m$ BEGIN
31844
+ IF to_regprocedure('public.control_get_todays_carts()') IS NOT NULL
31845
+ AND pg_get_function_result('public.control_get_todays_carts()'::regprocedure) NOT LIKE '%carrier_name%' THEN
31846
+ DROP FUNCTION public.control_get_todays_carts();
31847
+ END IF;
31848
+ END $m$;
31849
+
31850
+ CREATE OR REPLACE FUNCTION public.control_get_todays_carts()
31851
+ RETURNS TABLE(cart_number_id uuid, cart_number text, order_source text, order_id text, cart_index integer, order_number text, pick_date date, status text, printed_at timestamp with time zone, printed_today boolean, crates_on_cart integer, country_code text, carrier_name text)
31852
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
31853
+ AS $function$
31854
+ WITH vandaag AS (
31855
+ SELECT (now() AT TIME ZONE public.logistics_tz())::date AS d
31856
+ ),
31857
+ idx AS (
31858
+ SELECT DISTINCT ON (i.cart_id) i.*
31859
+ FROM public.logistics_cart_index((SELECT d FROM vandaag), (SELECT d FROM vandaag)) i
31860
+ ORDER BY i.cart_id, i.order_number
31861
+ ),
31862
+ gecontroleerd AS (
31863
+ SELECT cci.cart_number_id, cci.order_source, cci.order_id, cci.cart_index
31864
+ FROM public.cart_control_items cci
31865
+ JOIN public.cart_control_sessions ccs ON ccs.id = cci.session_id
31866
+ WHERE ccs.control_date = (SELECT d FROM vandaag)
31867
+ AND cci.action = 'checked'
31868
+ )
31869
+ SELECT
31870
+ i.cart_id,
31871
+ COALESCE(cn.cart_number, i.cart_label),
31872
+ i.order_source,
31873
+ i.order_uuid::text,
31874
+ i.cart_index,
31875
+ i.order_number,
31876
+ i.pick_date,
31877
+ COALESCE(cn.status, 'pending'),
31878
+ i.printed_at,
31879
+ (i.printed_at >= (SELECT d FROM vandaag)::timestamptz),
31880
+ (SELECT sum(ci.crates)::int FROM public.order_cart_items ci WHERE ci.cart_id = i.cart_id),
31881
+ (SELECT CASE WHEN upper(btrim(l.country)) ~ '^[A-Z]{2}$' THEN upper(btrim(l.country)) END
31882
+ FROM public.orders o
31883
+ JOIN public.customer_delivery_locations l
31884
+ ON l.id = public.logistics_try_uuid(o.custom_fields->>'delivery_location')
31885
+ WHERE o.id = i.order_uuid
31886
+ LIMIT 1),
31887
+ (SELECT CASE WHEN c.carrier_kind = 'eigen_vervoer' THEN 'Eigen vervoer'
31888
+ WHEN c.carrier_kind = 'klant_haalt_op' THEN 'Afhalen'
31889
+ WHEN o.carrier_id IS NULL AND o.carrier_source = 'afleverlocatie' THEN 'Afhalen'
31890
+ ELSE c.name END
31891
+ FROM public.orders o
31892
+ LEFT JOIN public.carriers c ON c.id = o.carrier_id
31893
+ WHERE o.id = i.order_uuid
31894
+ LIMIT 1)
31895
+ FROM idx i
31896
+ LEFT JOIN public.cart_numbers cn
31897
+ ON cn.order_source = i.order_source
31898
+ AND cn.order_id = i.order_uuid::text
31899
+ AND cn.cart_index = i.cart_index
31900
+ LEFT JOIN gecontroleerd g1 ON g1.cart_number_id = i.cart_id
31901
+ LEFT JOIN gecontroleerd g2
31902
+ ON g2.order_source = i.order_source
31903
+ AND g2.order_id = i.order_uuid::text
31904
+ AND g2.cart_index = i.cart_index
31905
+ WHERE COALESCE(cn.status, 'pending') <> 'delivered'
31906
+ AND g1.cart_number_id IS NULL
31907
+ AND g2.order_id IS NULL
31908
+ ORDER BY i.order_number, i.cart_index;
31909
+ $function$;
31910
+ REVOKE ALL ON FUNCTION public.control_get_todays_carts() FROM PUBLIC, anon;
31911
+ GRANT EXECUTE ON FUNCTION public.control_get_todays_carts() TO authenticated, service_role;
31912
+
31913
+ -- ===== 1.0.184 toegangscontrole in functies (overleeft schemasync) =====
31914
+ CREATE OR REPLACE FUNCTION public.logistics_acc_is_server()
31915
+ RETURNS boolean LANGUAGE sql STABLE SET search_path TO 'public'
31916
+ AS $function$
31917
+ SELECT coalesce(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role', '') = 'service_role'
31918
+ OR session_user::text <> 'authenticator'
31919
+ $function$;
31920
+
31921
+ CREATE OR REPLACE FUNCTION public.logistics_acc_direct(p_fn text)
31922
+ RETURNS boolean LANGUAGE sql STABLE SET search_path TO 'public'
31923
+ AS $function$
31924
+ SELECT coalesce(current_query(), '') ~* ('(^|[^a-z0-9_])' || p_fn || '([^a-z0-9_]|$)')
31925
+ $function$;
31926
+
31927
+ CREATE OR REPLACE FUNCTION public.logistics_acc_guard(p_level text, p_fn text)
31928
+ RETURNS void LANGUAGE plpgsql STABLE SET search_path TO 'public'
31929
+ AS $function$
31930
+ BEGIN
31931
+ IF public.logistics_acc_is_server() THEN RETURN; END IF;
31932
+ IF pg_trigger_depth() > 0 THEN RETURN; END IF;
31933
+ IF NOT public.logistics_acc_direct(p_fn) THEN RETURN; END IF;
31934
+ IF p_level = 'ingelogd' AND auth.uid() IS NOT NULL THEN RETURN; END IF;
31935
+ IF p_level = 'beheer' AND auth.uid() IS NOT NULL AND public.logistics_is_beheer() THEN RETURN; END IF;
31936
+ RAISE EXCEPTION '%', CASE p_level
31937
+ WHEN 'server' THEN 'Geen toegang: ' || p_fn || ' is alleen voor de server.'
31938
+ WHEN 'beheer' THEN 'Geen toegang: ' || p_fn || ' is alleen voor beheer.'
31939
+ ELSE 'Geen toegang: log in om ' || p_fn || ' te gebruiken.' END
31940
+ USING ERRCODE = '42501';
31941
+ END $function$;
31942
+ REVOKE ALL ON FUNCTION public.logistics_acc_guard(text, text) FROM PUBLIC, anon;
31943
+ GRANT EXECUTE ON FUNCTION public.logistics_acc_guard(text, text) TO authenticated, service_role;
31944
+ REVOKE ALL ON FUNCTION public.logistics_acc_direct(text) FROM PUBLIC, anon;
31945
+ GRANT EXECUTE ON FUNCTION public.logistics_acc_direct(text) TO authenticated, service_role;
31946
+ REVOKE ALL ON FUNCTION public.logistics_acc_is_server() FROM PUBLIC, anon;
31947
+ GRANT EXECUTE ON FUNCTION public.logistics_acc_is_server() TO authenticated, service_role;
31948
+
31949
+ CREATE OR REPLACE FUNCTION public.logistics_acc_levels()
31950
+ RETURNS TABLE(functie text, niveau text) LANGUAGE sql IMMUTABLE SET search_path TO 'public'
31951
+ AS $function$
31952
+ SELECT * FROM (
31953
+ SELECT unnest(ARRAY['claim_logistics_tasks','control_get_todays_carts','control_mark_carts_delivered','control_mark_carts_delivered_detail','logistics_allowed_load_carriers','logistics_apply_buffer_decision','logistics_apply_document_layouts','logistics_apply_order_buffer','logistics_approve_delay_reschedule','logistics_archived_orders','logistics_backorder_allowed','logistics_belading_bevestig','logistics_belading_verstuurder','logistics_blocked_orders','logistics_blocked_overview','logistics_buffer_decision','logistics_buffer_detach_late_orders','logistics_buffer_frozen_order','logistics_buffer_handled','logistics_buffer_notify','logistics_buffer_open_key','logistics_buffer_payload','logistics_buffer_simulate','logistics_cancel_order','logistics_cancel_orphan_tasks','logistics_capacity_day_check','logistics_carrier_preview_existing','logistics_carrier_status','logistics_cart_archive','logistics_cart_archive_preview','logistics_cart_checkin_at','logistics_cart_fixed_numbering','logistics_cart_handled','logistics_cart_index','logistics_cart_is_archived','logistics_cart_is_full','logistics_cart_json_fixed','logistics_cart_layout_method','logistics_cart_order_ids','logistics_cart_order_refs','logistics_cart_plan_overview','logistics_cart_plan_status_core','logistics_cart_release_reason','logistics_cart_seq','logistics_cart_unarchive','logistics_cart_units','logistics_cart_wait_until','logistics_check_order_allowed','logistics_check_order_stock','logistics_claim_next_task','logistics_cleanup_duplicate_order_lines','logistics_cleanup_history','logistics_cleanup_tasks','logistics_consistency_check','logistics_control_complete_cart','logistics_control_settings','logistics_cron_health','logistics_day_windows','logistics_delivery_agenda','logistics_departure_preview','logistics_document_layout','logistics_document_layout_v2','logistics_emit_event','logistics_external_fulfillment_key_report','logistics_floriday_cart_plan','logistics_floriday_claim_pushes','logistics_floriday_finish_push','logistics_floriday_finish_push_v2','logistics_floriday_finish_push_v3','logistics_floriday_is_read_only','logistics_floriday_log_refusal','logistics_floriday_log_send','logistics_floriday_order_approved','logistics_floriday_prune_pushes','logistics_floriday_queue_push','logistics_full_cart_min_layers','logistics_full_cart_min_percentage','logistics_install_check','logistics_is_beheer','logistics_item_external_fulfilled','logistics_layout_method_status','logistics_link_order_items','logistics_local_hhmi','logistics_local_today','logistics_merge_decision','logistics_merge_floriday_lines','logistics_merge_lines_enabled','logistics_merge_partial_carts','logistics_opening_timezone','logistics_order_backlog_info','logistics_order_cart_approval','logistics_order_cart_ids','logistics_order_changed','logistics_order_definitively_blocked','logistics_order_departure','logistics_order_departure_list','logistics_order_documents_ready','logistics_order_documents_ready_bool','logistics_order_external_state','logistics_order_finalize_enqueue','logistics_order_finalize_overview','logistics_order_finalize_prune','logistics_order_finalize_reconcile','logistics_order_finalize_retry','logistics_order_finalize_run','logistics_order_frozen','logistics_order_handled','logistics_order_is_archived','logistics_order_is_backlog','logistics_order_is_frozen','logistics_order_line_duplicates','logistics_order_mergeable','logistics_order_overview','logistics_order_plan_coverage','logistics_order_plan_coverage_list','logistics_orphan_carts_list','logistics_orphan_carts_summary','logistics_own_transport_departure','logistics_packing_autofix','logistics_packing_block_reason','logistics_packing_dismiss','logistics_packing_overview','logistics_packing_reconcile','logistics_packing_set_manual','logistics_partial_wait_minutes','logistics_payload_is_floriday','logistics_plan_block','logistics_plan_defer','logistics_plan_enqueue','logistics_plan_finish','logistics_plan_floriday_order','logistics_plan_lock_acquire','logistics_plan_lock_release','logistics_plan_progress','logistics_plan_progress_pct','logistics_plan_queue_list','logistics_plan_reset_queue','logistics_plan_retry','logistics_plan_state_for','logistics_plan_timeout_running','logistics_plan_unarchive','logistics_prune_buffer_member_queue','logistics_queue_capacity_email','logistics_queue_stock_email','logistics_reapply_buffer_decisions','logistics_rebuffer_open_orders','logistics_rebuffer_orders','logistics_rebuffer_pending','logistics_rebuffer_report','logistics_refresh_plan_state','logistics_replan_all','logistics_replan_all_force','logistics_replan_order_journey','logistics_replan_preview','logistics_replan_selected','logistics_report_delay','logistics_reschedule_order','logistics_reschedule_preview','logistics_resolve_carrier','logistics_resolve_carrier_for_location','logistics_resolve_carrier_for_location_many','logistics_resolve_carrier_many','logistics_resolve_settings','logistics_sales_order_external_fulfilled','logistics_schedule_tasks','logistics_set_order_priority','logistics_subtract_working_minutes','logistics_supersede_stale_tasks','logistics_sync_task_carriers','logistics_task_open_counts','logistics_tasks_prune','logistics_tick_health','logistics_tick_ping','logistics_timezone_options','logistics_trace','logistics_trace_all_orders','logistics_trace_archive_order','logistics_trace_batch','logistics_trace_derive','logistics_trace_order_journey_start','logistics_trace_order_origin','logistics_trace_order_overview','logistics_trace_order_state','logistics_trace_order_step','logistics_trace_orders','logistics_trace_progress','logistics_trace_progress_order','logistics_trace_step_once','logistics_trace_stock_check','logistics_trace_timeline','logistics_trace_version_events','logistics_trace_versions','logistics_tz','logistics_unit_order_ids','logistics_upsert_task','logistics_valid_timezone','logistics_write_buffer_layer_positions','logistics_write_cart_layer_positions']), 'ingelogd'
31954
+ UNION ALL
31955
+ SELECT unnest(ARRAY['logistics_active_layout_method','logistics_address_key','logistics_block_reason_code','logistics_buffer_criteria','logistics_buffer_decision_for_settings','logistics_buffer_value','logistics_capacity_plan_day','logistics_carrier_minutes','logistics_delay_customer_email','logistics_document_selection_status','logistics_document_type_map','logistics_floriday_delivery_note_create_enabled','logistics_floriday_gate_status','logistics_floriday_shipping_document','logistics_floriday_status_updates_enabled','logistics_floriday_stock_updates_enabled','logistics_floriday_write_allowed','logistics_is_engine_response','logistics_live_carriers','logistics_live_cart_count','logistics_next_workday_date','logistics_next_workday_not_past','logistics_nl_date','logistics_nl_moment','logistics_not_past','logistics_order_cart_count','logistics_order_is_shipped_status','logistics_order_merge_key','logistics_plan_error_class','logistics_plan_error_text','logistics_resolve_product_id','logistics_resolve_product_match','logistics_retention_days','logistics_rule_bool','logistics_rule_num','logistics_rule_text','logistics_rule_value','logistics_shift_to_workday','logistics_status_after_shipping','logistics_task_owner','logistics_task_retention_days','logistics_trace_node','logistics_trace_redact','logistics_trace_source_node','logistics_tray_plants','logistics_try_uuid']), 'open (rekenhulp/rechten aanroeper)'
31956
+ UNION ALL
31957
+ SELECT unnest(ARRAY['logistics_active_pick_workdays','logistics_auto_send_status','logistics_barcode_lookup','logistics_cancel_stale_cart_pick_rows','logistics_carrier_picks_up_for','logistics_cart_barcode_opschonen','logistics_cart_barcode_value','logistics_cart_has_packing_slip','logistics_cart_pick_starts_at','logistics_cart_plan_status','logistics_cart_released','logistics_cart_scanned','logistics_cart_slip_plan','logistics_cart_slip_queued','logistics_cart_sticker_state','logistics_carts_per_print','logistics_claim_task','logistics_cleanup_orphan_print_jobs','logistics_complete_task','logistics_control_scan_approve','logistics_control_scan_claim','logistics_control_scan_finish','logistics_control_scan_lookup','logistics_control_scan_overview','logistics_control_scan_print_own','logistics_control_scan_record_afmelding','logistics_control_scan_record_afmelding_cart','logistics_control_scan_retry','logistics_document_for_order','logistics_document_station','logistics_enqueue_shipping_note_with_slip','logistics_enqueue_sticker_with_slip','logistics_ensure_cart_barcode','logistics_ensure_pick_data','logistics_ensure_shipping_notes','logistics_fail_task','logistics_fix_wrong_shipping_notes','logistics_fulfill_barcode_requests','logistics_is_group_label','logistics_is_pick_workday','logistics_normalize_product_label','logistics_order_all_carts_scanned','logistics_order_carrier_label','logistics_order_is_topick','logistics_order_pick_day','logistics_order_released','logistics_order_sticker_meldingen','logistics_own_sticker_count','logistics_own_sticker_sync','logistics_own_traysticker_payload','logistics_own_traysticker_sync','logistics_packing_scan','logistics_pakbon_old_order_row','logistics_pakbon_oude_orderregels','logistics_parse_pickup','logistics_pick_agenda','logistics_pick_backlog_max_days','logistics_pick_barcode_enabled','logistics_pick_email_purpose','logistics_pick_mode','logistics_pick_partial_mode','logistics_pick_queue_close','logistics_pick_start_estimate','logistics_pick_units','logistics_pickup_moments_today','logistics_prepare_pick_documents','logistics_print_job_state','logistics_print_via_layout','logistics_prune_handled_pick_queue','logistics_refresh_pick_queue_status','logistics_refresh_pickup_moments','logistics_rescan_orders','logistics_resolve_pick_document','logistics_scan_delays','logistics_scan_order_stock','logistics_scan_resolve','logistics_set_pick_day','logistics_shipping_note_diagnose','logistics_shipping_note_ensure_accepted','logistics_shipping_note_follows_slip','logistics_shipping_note_from_barcode','logistics_shipping_note_job_for_slip','logistics_shipping_note_max_attempts','logistics_shipping_note_release','logistics_shipping_note_status','logistics_shipping_note_wait_reasons','logistics_shipping_note_wake','logistics_shipping_notes_wake_all','logistics_slip_is_floriday','logistics_slip_is_herprint','logistics_slip_note_claim','logistics_slip_note_finish','logistics_slip_note_hold','logistics_slip_note_hold_belading','logistics_slip_note_hold_kenmerk','logistics_slip_note_hold_layout','logistics_slip_note_hold_read_only','logistics_slip_note_keep_waiting','logistics_slip_note_release_layout','logistics_slip_note_retry','logistics_slip_note_skip_herprint','logistics_sticker_blocked_list','logistics_sticker_cart_plek','logistics_sticker_copies','logistics_sticker_distribution','logistics_sticker_etiket_status','logistics_sticker_external_fulfilled','logistics_sticker_follows_slip','logistics_sticker_moment','logistics_sticker_order_counts','logistics_sticker_parse_size','logistics_sticker_sync','logistics_sticker_upsert','logistics_subtract_pick_workdays','logistics_sync_pick_queue','logistics_sync_pick_queue_backlog','logistics_sync_pick_queue_core','logistics_task_pick_minutes','logistics_trace_pick_state','logistics_unit_all_carts_scanned','logistics_working_hours_status','pick_cart_breakdown','pick_enqueue_print_job','pick_queue_cart_plan']), 'wacht op pickschermlijst'
31958
+ UNION ALL
31959
+ SELECT unnest(ARRAY['logistics_afhandeling_fulfillment_order','logistics_allocation_check_changes','logistics_allocation_fp','logistics_allocation_frozen','logistics_apply_cart_swap','logistics_apply_external_fulfillment','logistics_auto_send_claim','logistics_auto_send_enqueue_for_order','logistics_auto_send_finish','logistics_auto_send_route_missing','logistics_auto_send_sent_elsewhere','logistics_auto_send_status_core','logistics_base_url_check_status','logistics_base_url_checks_resolve','logistics_brief_transport','logistics_buffer_closed_for_join','logistics_buffer_frozen','logistics_buffer_target','logistics_carrier_enqueue','logistics_carrier_favorite_id','logistics_carrier_for_location','logistics_carrier_frozen_reason','logistics_carrier_recalc_run','logistics_carrier_result','logistics_carrier_settings_ensure','logistics_carrier_skip_reason','logistics_cart_archive_classify','logistics_cart_coverage_check','logistics_cart_extra_fields','logistics_cart_ids_frozen','logistics_cart_is_frozen','logistics_cart_plan_status_core_v152','logistics_cart_seq_dubbel','logistics_departure_calc','logistics_double_group_lines','logistics_ensure_runtime_defaults','logistics_expand_line','logistics_fill_cart_departure','logistics_floriday_layout_id','logistics_floriday_push_hold_read_only','logistics_floriday_release_read_only','logistics_frozen_allocation_scaled','logistics_key_product','logistics_mark_cart_delivered_core','logistics_order_afgemeld','logistics_order_buffer_excluded','logistics_order_carrier_id','logistics_order_carrier_info','logistics_order_current_warnings','logistics_order_fp','logistics_order_line_allocation','logistics_order_on_frozen_shared_cart','logistics_order_plan_coverage_list_v152','logistics_order_plan_coverage_v165','logistics_plan_all','logistics_plan_buffer_sql','logistics_plan_claim','logistics_plan_close_claim','logistics_plan_coverage_check','logistics_plan_due_count','logistics_plan_ensure_event','logistics_plan_housekeeping','logistics_plan_impact_orders','logistics_plan_kick','logistics_plan_prune','logistics_plan_queue_dubbel','logistics_plan_release_stale','logistics_plan_retry_later','logistics_plan_setting','logistics_plan_wachttijd','logistics_plan_wachttijd_melden','logistics_planner_cart_snapshot','logistics_planner_diff_mode','logistics_planner_diff_prune','logistics_planner_lines','logistics_prune_cron_history','logistics_queue_row_has_open_peer','logistics_rebuffer_candidates','logistics_rebuffer_simulate_open','logistics_recalc_tick_kick','logistics_recompute_reserved_targeted','logistics_reconcile_missing_plan_queue','logistics_register_tick_apikey','logistics_replan_candidates','logistics_replan_clear_foreign_cart_items','logistics_replan_selected_orders','logistics_report_frozen_shortage','logistics_reserved_fast_skip','logistics_reserved_key','logistics_reserved_order_excluded','logistics_reserved_stock_nightly','logistics_resolve_carrier_core','logistics_set_components_scaled','logistics_shared_cart_peers','logistics_shell_base_url','logistics_sync_stock_alert_email_status','logistics_sync_task_status','logistics_tick','logistics_tick_base_url_check','logistics_tick_calls_resolve','logistics_tick_runs_prune','logistics_today_cart_ids','logistics_verkoop_order_link']), 'server'
31960
+ UNION ALL
31961
+ SELECT unnest(ARRAY['logistics_carrier_promote_oud','logistics_carrier_set_backfill']), 'beheer'
31962
+ UNION ALL
31963
+ SELECT unnest(ARRAY['logistics_set_order_carrier']), 'eigen (p_actor)'
31964
+ UNION ALL
31965
+ SELECT unnest(ARRAY['recompute_all_reserved_stock','recompute_reserved_stock']), 'eigenaar Voorraad'
31966
+ ) v(functie, niveau)
31967
+ $function$;
31968
+
31969
+ DO $acc$
31970
+ DECLARE r record; v_def text; v_new text; v_pos int; v_fail text[] := '{}'::text[]; n int := 0;
31971
+ BEGIN
31972
+ FOR r IN
31973
+ SELECT p.oid, p.proname, l.lanname, p.prosrc, x.lvl
31974
+ FROM public.logistics_acc_levels() x(fn, lvl)
31975
+ JOIN pg_proc p ON p.proname = x.fn AND p.pronamespace = 'public'::regnamespace
31976
+ JOIN pg_language l ON l.oid = p.prolang
31977
+ WHERE x.lvl IN ('server', 'ingelogd', 'beheer')
31978
+ AND p.prorettype <> 'trigger'::regtype AND coalesce(p.prosrc, '') <> ''
31979
+ AND p.prosrc NOT LIKE '%logistics_acc_guard(%'
31980
+ LOOP
31981
+ BEGIN
31982
+ v_def := pg_get_functiondef(r.oid);
31983
+ IF r.lanname = 'plpgsql' THEN
31984
+ v_pos := regexp_instr(r.prosrc, '\mBEGIN\M', 1, 1, 1, 'i');
31985
+ IF v_pos = 0 THEN RAISE EXCEPTION 'geen BEGIN gevonden'; END IF;
31986
+ v_new := left(r.prosrc, v_pos - 1) || E'\n PERFORM public.logistics_acc_guard(' || quote_literal(r.lvl) || ', '
31987
+ || quote_literal(r.proname) || ');' || substr(r.prosrc, v_pos);
31988
+ ELSIF r.lanname = 'sql' THEN
31989
+ v_new := E'\n SELECT public.logistics_acc_guard(' || quote_literal(r.lvl) || ', ' || quote_literal(r.proname) || ');' || r.prosrc;
31990
+ ELSE
31991
+ RAISE EXCEPTION 'taal % niet ondersteund', r.lanname;
31992
+ END IF;
31993
+ v_pos := position(r.prosrc IN v_def);
31994
+ IF v_pos = 0 THEN RAISE EXCEPTION 'definitie niet te herschrijven'; END IF;
31995
+ v_def := overlay(v_def placing v_new from v_pos for length(r.prosrc));
31996
+ EXECUTE v_def;
31997
+ n := n + 1;
31998
+ EXCEPTION WHEN OTHERS THEN
31999
+ v_fail := v_fail || (r.proname || ': ' || SQLERRM);
32000
+ END;
32001
+ END LOOP;
32002
+ RAISE NOTICE 'logistiek-engine toegangscontrole: % functies voorzien; mislukt: %', n, array_to_string(v_fail, ' | ');
32003
+ END $acc$;
32004
+
32005
+ CREATE OR REPLACE FUNCTION public.logistics_set_order_carrier(p_order_id uuid, p_carrier_id uuid, p_actor uuid)
32006
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
32007
+ AS $function$
32008
+ DECLARE v_uid uuid; o jsonb; v_skip text; r jsonb;
32009
+ BEGIN
32010
+ IF public.logistics_acc_is_server() THEN
32011
+ v_uid := coalesce(p_actor, auth.uid());
32012
+ ELSE
32013
+ v_uid := auth.uid();
32014
+ IF v_uid IS NULL THEN
32015
+ RAISE EXCEPTION 'Geen toegang: log in om een vervoerder te kiezen.' USING ERRCODE = '42501';
32016
+ END IF;
32017
+ END IF;
32018
+ SELECT to_jsonb(x) INTO o FROM public.orders x WHERE x.id = p_order_id AND x.deleted_at IS NULL FOR UPDATE;
32019
+ IF o IS NULL THEN RAISE EXCEPTION 'Order niet gevonden.'; END IF;
32020
+ IF p_carrier_id IS NOT NULL THEN
32021
+ v_skip := public.logistics_carrier_skip_reason(p_carrier_id);
32022
+ IF v_skip IS NOT NULL THEN RAISE EXCEPTION 'Deze vervoerder kan niet gekozen worden: %.', v_skip; END IF;
32023
+ r := public.logistics_carrier_result(p_carrier_id, 'handmatig', 'Handmatig gekozen op de order.', '{}'::text[]);
32024
+ ELSE
32025
+ r := public.logistics_resolve_carrier_core(o, true);
32026
+ END IF;
32027
+ PERFORM set_config('logistics.carrier_writer', 'engine', true);
32028
+ UPDATE public.orders
32029
+ SET carrier_id = public.logistics_try_uuid(r->>'carrier_id'), carrier_source = r->>'bron',
32030
+ carrier_reason = r->>'reden', carrier_resolved_at = now(), carrier_set_by = v_uid, carrier_set_at = now()
32031
+ WHERE id = p_order_id;
32032
+ PERFORM set_config('logistics.carrier_writer', '', true);
32033
+ RETURN r || jsonb_build_object('bevroren', public.logistics_carrier_frozen_reason(p_order_id));
32034
+ END $function$;
32035
+
32036
+ CREATE OR REPLACE FUNCTION public.logistics_set_order_carrier(p_order_id uuid, p_carrier_id uuid)
32037
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
32038
+ AS $function$
32039
+ BEGIN
32040
+ RETURN public.logistics_set_order_carrier(p_order_id, p_carrier_id, NULL::uuid);
32041
+ END $function$;
32042
+ REVOKE ALL ON FUNCTION public.logistics_set_order_carrier(uuid, uuid, uuid) FROM PUBLIC, anon;
32043
+ GRANT EXECUTE ON FUNCTION public.logistics_set_order_carrier(uuid, uuid, uuid) TO authenticated, service_role;
32044
+ REVOKE ALL ON FUNCTION public.logistics_set_order_carrier(uuid, uuid) FROM PUBLIC, anon;
32045
+ GRANT EXECUTE ON FUNCTION public.logistics_set_order_carrier(uuid, uuid) TO authenticated, service_role;
32046
+
32047
+ CREATE OR REPLACE FUNCTION public.logistics_access_check()
32048
+ RETURNS TABLE(onderdeel text, ok boolean, toelichting text)
32049
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
32050
+ AS $function$
32051
+ DECLARE r record;
32052
+ BEGIN
32053
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_access_check');
32054
+ FOR r IN
32055
+ SELECT lv.functie, lv.niveau, p.oid, p.prosrc,
32056
+ has_function_privilege('anon', p.oid, 'EXECUTE') AS an,
32057
+ has_function_privilege('authenticated', p.oid, 'EXECUTE') AS au,
32058
+ pg_get_function_identity_arguments(p.oid) AS args
32059
+ FROM public.logistics_acc_levels() lv
32060
+ LEFT JOIN pg_proc p ON p.proname = lv.functie AND p.pronamespace = 'public'::regnamespace
32061
+ ORDER BY lv.functie, args
32062
+ LOOP
32063
+ onderdeel := 'Toegang ' || r.functie || CASE WHEN r.args IS NOT NULL AND r.args <> '' THEN '(' || r.args || ')' ELSE '' END;
32064
+ IF r.oid IS NULL THEN
32065
+ ok := r.niveau IN ('niet van Engine', 'eigenaar Voorraad'); toelichting := r.niveau || '; functie ontbreekt';
32066
+ ELSIF r.niveau IN ('server', 'ingelogd', 'beheer') THEN
32067
+ ok := r.prosrc LIKE '%logistics_acc_guard(%';
32068
+ toelichting := 'niveau ' || r.niveau || '; controle ' || CASE WHEN ok THEN 'aanwezig' ELSE 'ONTBREEKT' END
32069
+ || '; uitvoerrecht anon ' || CASE WHEN r.an THEN 'ja' ELSE 'nee' END || ', authenticated ' || CASE WHEN r.au THEN 'ja' ELSE 'nee' END;
32070
+ ELSE
32071
+ ok := true;
32072
+ toelichting := CASE r.niveau WHEN 'wacht op pickschermlijst' THEN 'bewust open: wacht op pickschermlijst (pickscherm/scanner zonder inlog)'
32073
+ ELSE r.niveau END
32074
+ || '; uitvoerrecht anon ' || CASE WHEN r.an THEN 'ja' ELSE 'nee' END || ', authenticated ' || CASE WHEN r.au THEN 'ja' ELSE 'nee' END;
32075
+ END IF;
32076
+ RETURN NEXT;
32077
+ END LOOP;
32078
+ FOR r IN SELECT t.n, t.tbl FROM unnest(ARRAY['logistics_capacity_alert_notify_trg','logistics_formulas_touch','logistics_packing_deviations_touch','logistics_strategies_touch','logistics_strategy_schedules_touch','logistics_variables_touch','set_logistics_delay_alerts_updated_at','set_logistics_stock_alerts_updated_at','touch_capacity_alerts','touch_pickup_moments','touch_staff_planning','trg_cart_items_layer_positions','trg_cart_number_prunes_pick_queue','trg_control_item_prunes_pick_queue','trg_floriday_gate_status_queue','trg_floriday_gate_stock_queue','trg_fust_types_requeue_blocked','trg_guard_archived_cart','trg_guard_archived_cart','trg_guard_archived_cart','trg_guard_archived_cart','trg_guard_archived_cart','trg_guard_archived_cart','trg_guard_archived_cart','trg_guard_archived_cart','trg_guard_archived_cart_row','trg_logistics_auto_send_on_scan','trg_logistics_block_bvo_note_for_floriday','trg_logistics_block_merged_shipping_note','trg_logistics_carriers_enqueue','trg_logistics_cart_barcode_autofill','trg_logistics_clear_layer_positions','trg_logistics_close_pick_rows_on_shipped','trg_logistics_event_buffer','trg_logistics_event_insert','trg_logistics_keep_archived_cart','trg_logistics_locations_enqueue','trg_logistics_order_finalize_queue_updated','trg_logistics_order_item_autolink','trg_logistics_order_items_stock','trg_logistics_order_sticker_warnings_touch','trg_logistics_orders_carrier_changed','trg_logistics_orders_carrier_fill','trg_logistics_pick_queue_enrich','trg_logistics_pick_queue_layout','trg_logistics_pick_queue_sync','trg_logistics_pin_pick_day','trg_logistics_plan_order_items','trg_logistics_plan_orders','trg_logistics_plan_products','trg_logistics_require_cart_on_packing_slip','trg_logistics_require_pakbon_barcode','trg_logistics_reschedules_touch','trg_logistics_rule_config_layout_sync','trg_logistics_rule_config_method_changed','trg_logistics_rule_config_rebuffer','trg_logistics_rule_config_single_active','trg_logistics_rule_config_snapshot','trg_logistics_rule_config_snapshot_write','trg_logistics_schedule_after_event','trg_logistics_shipping_note_after_slip','trg_logistics_shipping_note_follow_printer','trg_logistics_shipping_note_release_on_scan','trg_logistics_slip_note_block_herprint','trg_logistics_sticker_prints_touch','trg_logistics_stock_alert_email','trg_logistics_stock_alert_skip_closed','trg_logistics_task_status_note','trg_logistics_task_status_scan','trg_logistics_time_settings_validate','trg_logistics_trace_pick_queue','trg_orders_guard_buffer_id','trg_pick_queue_cart_breakdown','trg_strategies_requeue_blocked','trg_trace_control_scan','trg_trace_order_cart_items_del','trg_trace_order_cart_items_ins','trg_trace_order_carts','trg_trace_order_carts_del','trg_trace_order_carts_ins','trg_trace_pick_queue_insert','trg_trace_plan_queue','trg_trace_print_jobs','trg_trace_slip_shipping_note','update_logistics_document_prints_updated_at','update_logistics_events_updated_at','update_logistics_floriday_pushes_updated_at','update_logistics_plan_runs_updated_at','update_logistics_recalc_jobs_updated_at','update_logistics_reservation_allocations_updated_at','update_logistics_rule_configs_updated_at','update_logistics_tasks_updated_at'],
32079
+ ARRAY['logistics_capacity_alerts','logistics_formulas','logistics_packing_deviations','logistics_strategies','logistics_strategy_schedules','logistics_variables','logistics_delay_alerts','logistics_stock_alerts','logistics_capacity_alerts','logistics_pickup_moments','logistics_staff_planning','order_cart_items','cart_numbers','cart_control_items','floriday_status_sync_queue','floriday_continuous_stock_queue','fust_types','logistics_cart_barcode','logistics_control_scan','logistics_slip_shipping_note','logistics_sticker_prints','order_cart_items','pakbon_barcode_verzoeken','pick_queue','print_jobs','order_carts','logistics_control_scan','print_jobs','pick_queue','carriers','order_carts','logistics_rule_configs','orders','logistics_events','logistics_events','order_carts','customer_delivery_locations','logistics_order_finalize_queue','order_items','order_items','logistics_order_sticker_warnings','orders','orders','pick_queue','pick_queue','logistics_tasks','pick_queue','order_items','orders','products','print_jobs','print_jobs','logistics_reschedules','logistics_rule_configs','logistics_rule_configs','logistics_rule_configs','logistics_rule_configs','logistics_rule_configs','logistics_rule_configs','logistics_events','print_jobs','print_jobs','logistics_control_scan','logistics_slip_shipping_note','logistics_sticker_prints','logistics_stock_alerts','logistics_stock_alerts','print_jobs','logistics_control_scan','logistics_time_settings','pick_queue','orders','pick_queue','logistics_strategies','logistics_control_scan','order_cart_items','order_cart_items','order_carts','order_carts','order_carts','pick_queue','logistics_plan_queue','print_jobs','logistics_slip_shipping_note','logistics_document_prints','logistics_events','logistics_floriday_pushes','logistics_plan_runs','logistics_recalc_jobs','logistics_reservation_allocations','logistics_rule_configs','logistics_tasks']) t(n, tbl) LOOP
32080
+ onderdeel := 'Trigger ' || r.n || ' op ' || r.tbl;
32081
+ ok := EXISTS (SELECT 1 FROM pg_trigger g WHERE g.tgname = r.n AND g.tgrelid = to_regclass('public.' || r.tbl));
32082
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt (schemasync controleren)' END; RETURN NEXT;
32083
+ END LOOP;
32084
+ FOR r IN SELECT t.n, t.tbl FROM unnest(ARRAY['Authenticated can read document prints','Beheer leest externe afhandeling','Beheer leest kar-archieflog','Beheer leest minuutronde-log','Beheer leest reserveringstoewijzing','Beheer leest vertrekcontrole','Beheerders lezen verdeel-vingerafdrukken','Eigen vertragingslogboek','Eigen vertragingsmeldingen','Eigen voorraadmeldingen','Ingelogd lezen stickermeldingen','Ingelogd lezen stickerprints','Ingelogd lezen vastgezette orders','Ingelogde gebruikers kunnen herberekeningen bekijken','Ingelogde gebruikers lezen de hartslag','Ingelogde gebruikers lezen de herberekenstatus','Ingelogde gebruikers lezen dekkingspogingen','Ingelogde gebruikers lezen pickdagen','Ingelogde gebruikers lezen planner-vergelijkingen','Ingelogde gebruikers lezen verzendbriefwachtrij','Ingelogde gebruikers zien beladingsafwijkingen','Ingelogde gebruikers zien de pushstatus','Users manage their own logistics events','Users manage their own logistics tasks','Users manage their own plan runs','Users manage their own rule configs','Users manage their own rule versions','beheer leest bevestigingen','finalize_queue_read_authenticated','logistics_carrier_events_read','logistics_carrier_settings_read','logistics_cart_barcode_read','logistics_control_scan_authenticated','logistics_floriday_carrier_map_read','logistics_opening_exceptions_rw','logistics_opening_hours_rw','logistics_order_merge_lines_read','logistics_order_merges_read','logistics_pick_data_requests_rw','logistics_shipping_note_scan_read','logistics_time_settings_rw','naamkoppeling logboek lezen door beheerders','override log readable','own capacity alerts','own capacity events','own capacity settings','own decisions','own formulas','own pickup moments','own schedules','own staff planning','own strategies','own variables','plan queue readable','plan settings readable','plan_locks_read_authenticated','refusals_read_authenticated','reschedules own insert','reschedules own read','service role manages runtime config','status_sends_read_authenticated','trace lezen door ingelogde gebruikers','trace_archive_read_authenticated'],
32085
+ ARRAY['logistics_document_prints','logistics_external_fulfillment','logistics_cart_archive_log','logistics_tick_runs','logistics_reservation_allocations','logistics_departure_check','logistics_allocation_fingerprint','logistics_delay_log','logistics_delay_alerts','logistics_stock_alerts','logistics_order_sticker_warnings','logistics_sticker_prints','logistics_order_plan_exclusion','logistics_recalc_jobs','logistics_tick_heartbeat','logistics_rebuffer_state','logistics_plan_coverage_retries','logistics_order_pick_day','logistics_planner_diff_runs','logistics_slip_shipping_note','logistics_packing_deviations','logistics_floriday_pushes','logistics_events','logistics_tasks','logistics_plan_runs','logistics_rule_configs','logistics_rule_versions','logistics_belading_bevestigingen','logistics_order_finalize_queue','logistics_carrier_events','logistics_carrier_settings','logistics_cart_barcode','logistics_control_scan','logistics_floriday_carrier_map','logistics_opening_exceptions','logistics_opening_hours','logistics_order_merge_lines','logistics_order_merges','logistics_pick_data_requests','logistics_shipping_note_scan','logistics_time_settings','logistics_autolink_audit','logistics_layer_override_log','logistics_capacity_alerts','logistics_capacity_events','logistics_capacity_settings','logistics_decisions','logistics_formulas','logistics_pickup_moments','logistics_strategy_schedules','logistics_staff_planning','logistics_strategies','logistics_variables','logistics_plan_queue','logistics_plan_settings','logistics_plan_locks','logistics_floriday_push_refusals','logistics_reschedules','logistics_reschedules','module_runtime_config','logistics_floriday_status_sends','logistics_trace_events','logistics_trace_events_archive']) t(n, tbl) LOOP
32086
+ onderdeel := 'Policy "' || r.n || '" op ' || r.tbl;
32087
+ ok := EXISTS (SELECT 1 FROM pg_policies x WHERE x.schemaname = 'public' AND x.tablename = r.tbl AND x.policyname = r.n);
32088
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt (schemasync controleren)' END; RETURN NEXT;
32089
+ END LOOP;
32090
+ END $function$;
32091
+ REVOKE ALL ON FUNCTION public.logistics_access_check() FROM PUBLIC, anon;
32092
+ GRANT EXECUTE ON FUNCTION public.logistics_access_check() TO authenticated, service_role;
32093
+
32094
+ DO $ic$
32095
+ DECLARE v_def text;
32096
+ BEGIN
32097
+ v_def := pg_get_functiondef('public.logistics_install_check()'::regprocedure);
32098
+ IF v_def NOT LIKE '%logistics_access_check()%' THEN
32099
+ v_def := regexp_replace(v_def, 'END \$function\$\s*$', E' RETURN QUERY SELECT * FROM public.logistics_access_check();\nEND $function$');
32100
+ EXECUTE v_def;
32101
+ END IF;
32102
+ END $ic$;