@flowselections/logistiek-engine 1.0.241 → 1.0.242

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.
@@ -44266,3 +44266,879 @@ SELECT public.logistics_pickup_moments_koppel();
44266
44266
  -- ===== 2026-11-10c: rechten hulponderdelen ophaalmoment =====
44267
44267
  REVOKE EXECUTE ON FUNCTION public.logistics_pickup_regel() FROM authenticated;
44268
44268
  REVOKE EXECUTE ON FUNCTION public.logistics_pickup_moment_kies(uuid, date, timestamptz, text, numeric) FROM authenticated;
44269
+
44270
+ -- ===== 2026-11-10d: karindeling snel en betrouwbaar bij productievolume =====
44271
+ -- Karindeling snel en betrouwbaar bij productievolume (schema 2026-11-10d)
44272
+ CREATE OR REPLACE FUNCTION public.logistics_product_maten(p_product_ids uuid[])
44273
+ RETURNS TABLE(product_id uuid, product_name text, plants_per_crate integer, fust_type_id uuid, fust_name text, fust_bron text,
44274
+ fust_length_mm numeric, fust_width_mm numeric, fust_height_mm numeric,
44275
+ lengte_bron text, breedte_bron text, hoogte_bron text,
44276
+ plant_height_cm numeric, height_source text, weight_kg numeric,
44277
+ pad_fust text, pad_artikel text, missing_reason text)
44278
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
44279
+ #variable_conflict use_column
44280
+ BEGIN
44281
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_product_maten');
44282
+ IF p_product_ids IS NOT NULL AND cardinality(p_product_ids) > 2000 THEN
44283
+ RAISE EXCEPTION 'Maximaal 2000 producten per aanvraag.' USING ERRCODE = '22023';
44284
+ END IF;
44285
+ RETURN QUERY
44286
+ WITH fm AS MATERIALIZED (SELECT * FROM public.logistics_fust_maten()),
44287
+ pr AS MATERIALIZED (
44288
+ SELECT p.id, p.product, p.plants_per_crate, p.fust_type_id AS hand_fid, p.plant_height_cm AS hand_h, p.weight,
44289
+ public.logistics_artikel_vbn(p.id) AS a
44290
+ FROM public.products p
44291
+ WHERE p_product_ids IS NULL OR p.id = ANY(p_product_ids)
44292
+ ),
44293
+ keuze AS (
44294
+ SELECT pr.*,
44295
+ (SELECT m.fust_type_id FROM fm m
44296
+ WHERE (pr.a->>'vbn_package_code') ~ '^[0-9]+$'
44297
+ AND (m.vbn_code = (pr.a->>'vbn_package_code')::int OR m.fust_key = 'vbn:' || (pr.a->>'vbn_package_code'))
44298
+ ORDER BY ((m.length_mm IS NOT NULL)::int + (m.width_mm IS NOT NULL)::int + (m.height_mm IS NOT NULL)::int) DESC, m.fust_type_id
44299
+ LIMIT 1) AS vbn_fid,
44300
+ (SELECT f.id FROM public.fust_types f
44301
+ WHERE nullif(pr.a->>'custom_package_id', '') IS NOT NULL AND f.custom_package_id = pr.a->>'custom_package_id'
44302
+ ORDER BY f.created_at, f.id LIMIT 1) AS cp_fid
44303
+ FROM pr
44304
+ ),
44305
+ k2 AS (
44306
+ SELECT k.*, coalesce(k.hand_fid, k.vbn_fid, k.cp_fid) AS fid,
44307
+ CASE WHEN k.hand_fid IS NOT NULL THEN 'handmatig' WHEN k.vbn_fid IS NOT NULL THEN 'vbn' WHEN k.cp_fid IS NOT NULL THEN 'floriday' END AS fbron
44308
+ FROM keuze k
44309
+ ),
44310
+ maat AS (
44311
+ SELECT k.*, m.name AS mname, m.pad AS mpad,
44312
+ coalesce(m.length_mm, lx.length_mm) AS l, CASE WHEN m.length_mm IS NOT NULL THEN m.lengte_bron ELSE coalesce(lx.lengte_bron, 'onbekend') END AS lb,
44313
+ coalesce(m.width_mm, wx.width_mm) AS w, CASE WHEN m.width_mm IS NOT NULL THEN m.breedte_bron ELSE coalesce(wx.breedte_bron, 'onbekend') END AS wb,
44314
+ coalesce(m.height_mm, hx.height_mm) AS h, CASE WHEN m.height_mm IS NOT NULL THEN m.hoogte_bron ELSE coalesce(hx.hoogte_bron, 'onbekend') END AS hb
44315
+ FROM k2 k
44316
+ LEFT JOIN fm m ON m.fust_type_id = k.fid
44317
+ LEFT JOIN LATERAL (SELECT m2.length_mm, m2.lengte_bron FROM fm m2 WHERE m.fust_key IS NOT NULL AND m2.fust_key = m.fust_key AND m2.length_mm IS NOT NULL ORDER BY m2.fust_type_id LIMIT 1) lx ON true
44318
+ LEFT JOIN LATERAL (SELECT m2.width_mm, m2.breedte_bron FROM fm m2 WHERE m.fust_key IS NOT NULL AND m2.fust_key = m.fust_key AND m2.width_mm IS NOT NULL ORDER BY m2.fust_type_id LIMIT 1) wx ON true
44319
+ LEFT JOIN LATERAL (SELECT m2.height_mm, m2.hoogte_bron FROM fm m2 WHERE m.fust_key IS NOT NULL AND m2.fust_key = m.fust_key AND m2.height_mm IS NOT NULL ORDER BY m2.fust_type_id LIMIT 1) hx ON true
44320
+ ),
44321
+ hoogte AS (
44322
+ SELECT x.*,
44323
+ public.logistics_pos_num(x.hand_h::text) AS hh,
44324
+ public.logistics_pos_num(x.a->>'s15_cm') AS s15,
44325
+ public.logistics_pos_num(x.a->>'s02_cm') AS s02,
44326
+ (SELECT b.bron FROM public.logistics_gegevens_bron b
44327
+ WHERE b.onderwerp = 'product' AND b.sleutel = x.id::text AND b.veld = 'plant_height_cm'
44328
+ AND public.logistics_pos_num(b.waarde) = public.logistics_pos_num(x.hand_h::text) LIMIT 1) AS hand_bron
44329
+ FROM maat x
44330
+ )
44331
+ SELECT h.id, h.product, h.plants_per_crate::int, h.fid, h.mname, h.fbron,
44332
+ h.l, h.w, h.h, h.lb, h.wb, h.hb,
44333
+ coalesce(h.hh, h.s15, h.s02),
44334
+ CASE WHEN h.hh IS NOT NULL THEN coalesce(h.hand_bron, 'handmatig')
44335
+ WHEN h.s15 IS NOT NULL THEN 'floricode_s15'
44336
+ WHEN h.s02 IS NOT NULL THEN 'floricode_s02' END,
44337
+ public.logistics_pos_num(regexp_replace(coalesce(h.weight::text, ''), '[^0-9.,]', '', 'g')),
44338
+ coalesce(h.mpad, (SELECT fm.pad FROM fm LIMIT 1)), h.a->>'pad',
44339
+ CASE
44340
+ WHEN h.fid IS NULL AND h.a->>'vbn_package_code' IS NULL AND h.a->>'custom_package_id' IS NULL
44341
+ THEN format('Bij "%s" is geen fustsoort bekend: kies de fustsoort bij het product in Voorraad.', h.product)
44342
+ WHEN h.fid IS NULL
44343
+ THEN format('Bij "%s" bestaat de fustsoort (VBN %s) niet in Voorraad → Fusten.', h.product, coalesce(h.a->>'vbn_package_code', h.a->>'custom_package_id'))
44344
+ WHEN h.l IS NULL OR h.w IS NULL OR h.h IS NULL
44345
+ THEN format('Fust %s heeft geen volledige maten in Floricode, Floriday of Voorraad → Fusten (ontbreekt: %s).', coalesce(h.mname, 'onbekend'),
44346
+ array_to_string(array_remove(ARRAY[CASE WHEN h.l IS NULL THEN 'lengte' END, CASE WHEN h.w IS NULL THEN 'breedte' END, CASE WHEN h.h IS NULL THEN 'hoogte' END], NULL), ', '))
44347
+ WHEN coalesce(h.hh, h.s15, h.s02) IS NULL
44348
+ THEN format('Bij "%s" is de planthoogte inclusief pot onbekend (geen S15/S02 in Floricode of Floriday, niet handmatig ingevuld).', h.product)
44349
+ END
44350
+ FROM hoogte h;
44351
+ END $$;
44352
+ GRANT EXECUTE ON FUNCTION public.logistics_product_maten(uuid[]) TO authenticated, service_role;
44353
+
44354
+ -- ===== Karindeling: tijdelijke fouten opnieuw proberen =====
44355
+ CREATE OR REPLACE FUNCTION public.logistics_plan_is_tijdelijk(p_text text)
44356
+ RETURNS boolean LANGUAGE sql IMMUTABLE SET search_path TO 'public'
44357
+ AS $function$
44358
+ SELECT coalesce(p_text, '') ~* '(statement timeout|canceling statement|niet afgesloten|claim niet afgerond|lock timeout|could not obtain lock|deadlock detected)'
44359
+ $function$;
44360
+ REVOKE EXECUTE ON FUNCTION public.logistics_plan_is_tijdelijk(text) FROM PUBLIC, anon;
44361
+ GRANT EXECUTE ON FUNCTION public.logistics_plan_is_tijdelijk(text) TO authenticated, service_role;
44362
+
44363
+ CREATE OR REPLACE FUNCTION public.logistics_plan_tijdelijk_wacht(p_attempts integer)
44364
+ RETURNS interval LANGUAGE sql IMMUTABLE SET search_path TO 'public'
44365
+ AS $function$
44366
+ SELECT make_interval(mins => (2 ^ (LEAST(5, GREATEST(1, coalesce(p_attempts, 1))) - 1))::int)
44367
+ $function$;
44368
+ REVOKE EXECUTE ON FUNCTION public.logistics_plan_tijdelijk_wacht(integer) FROM PUBLIC, anon;
44369
+ GRANT EXECUTE ON FUNCTION public.logistics_plan_tijdelijk_wacht(integer) TO authenticated, service_role;
44370
+
44371
+ CREATE OR REPLACE FUNCTION public.logistics_plan_tijdelijk_opnieuw(p_order_ids uuid[], p_reason text)
44372
+ RETURNS uuid[] LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
44373
+ AS $function$
44374
+ -- Zet wachtrijregels met een tijdelijke fout terug met oplopende wachttijd.
44375
+ -- Geeft de orders terug die al het maximum aantal pogingen hadden (die worden echt geblokkeerd).
44376
+ DECLARE
44377
+ v_max int := GREATEST(1, public.logistics_plan_setting('max_attempts', 5)::int);
44378
+ v_op uuid[];
44379
+ r record;
44380
+ BEGIN
44381
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_plan_tijdelijk_opnieuw');
44382
+ FOR r IN
44383
+ UPDATE public.logistics_plan_queue q
44384
+ SET status = 'pending', reason_code = 'tijdelijk', error = left(p_reason, 500),
44385
+ claimed_at = NULL, next_attempt_at = now() + public.logistics_plan_tijdelijk_wacht(q.attempts),
44386
+ updated_at = now()
44387
+ WHERE q.order_id = ANY(p_order_ids) AND q.status IN ('pending', 'running') AND q.attempts < v_max
44388
+ RETURNING q.order_id, q.attempts, q.next_attempt_at
44389
+ LOOP
44390
+ BEGIN
44391
+ PERFORM public.logistics_trace_order_step(
44392
+ r.order_id, 'stap:beladingindeling', 'karrenindeling opnieuw proberen', 'wachtend',
44393
+ 'tijdelijke fout, poging ' || r.attempts || ' van ' || v_max,
44394
+ 'De indeling liep tegen een tijdslimiet aan. Bestaande karren blijven staan. Volgende poging om '
44395
+ || to_char(r.next_attempt_at AT TIME ZONE public.logistics_tz(), 'HH24:MI') || ' uur.',
44396
+ 'Karrenindeling', left(p_reason, 500), 'tijdelijk');
44397
+ EXCEPTION WHEN OTHERS THEN
44398
+ RAISE WARNING 'logistiek-engine: verloop niet vastgelegd voor order %: %', r.order_id, SQLERRM;
44399
+ END;
44400
+ END LOOP;
44401
+ SELECT array_agg(q.order_id) INTO v_op FROM public.logistics_plan_queue q
44402
+ WHERE q.order_id = ANY(p_order_ids) AND q.status IN ('pending', 'running') AND q.attempts >= v_max;
44403
+ RETURN coalesce(v_op, ARRAY[]::uuid[]);
44404
+ END $function$;
44405
+ REVOKE EXECUTE ON FUNCTION public.logistics_plan_tijdelijk_opnieuw(uuid[], text) FROM PUBLIC, anon, authenticated;
44406
+ GRANT EXECUTE ON FUNCTION public.logistics_plan_tijdelijk_opnieuw(uuid[], text) TO service_role;
44407
+
44408
+ CREATE OR REPLACE FUNCTION public.logistics_plan_block(p_order_ids uuid[], p_reason text, p_details jsonb DEFAULT '{}'::jsonb)
44409
+ RETURNS integer
44410
+ LANGUAGE plpgsql
44411
+ SECURITY DEFINER
44412
+ SET search_path TO 'public'
44413
+ AS $function$
44414
+ DECLARE
44415
+ v_user uuid;
44416
+ o record;
44417
+ v_count integer := 0;
44418
+ v_code text;
44419
+ v_details jsonb := COALESCE(p_details, '{}'::jsonb);
44420
+ v_reason text := left(COALESCE(p_reason, 'belading onbekend'), 500);
44421
+ v_buffer text;
44422
+ v_subject_type text;
44423
+ v_subject_id text;
44424
+ v_result jsonb;
44425
+ v_last jsonb;
44426
+ v_members text[];
44427
+ BEGIN
44428
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_plan_block');
44429
+ IF p_order_ids IS NULL OR array_length(p_order_ids, 1) IS NULL THEN
44430
+ RETURN 0;
44431
+ END IF;
44432
+
44433
+ -- Tijdelijke fout (time-out): eerst opnieuw proberen, pas na het maximum blokkeren.
44434
+ IF public.logistics_plan_is_tijdelijk(p_reason) THEN
44435
+ v_count := cardinality(p_order_ids);
44436
+ p_order_ids := public.logistics_plan_tijdelijk_opnieuw(p_order_ids, v_reason);
44437
+ IF cardinality(p_order_ids) = 0 THEN
44438
+ RETURN v_count;
44439
+ END IF;
44440
+ v_reason := left(v_reason || ' (na ' || public.logistics_plan_setting('max_attempts', 5)::int || ' pogingen)', 500);
44441
+ v_count := 0;
44442
+ END IF;
44443
+
44444
+ v_user := COALESCE(
44445
+ auth.uid(),
44446
+ (SELECT user_id FROM public.logistics_strategies WHERE is_active ORDER BY updated_at DESC LIMIT 1),
44447
+ (SELECT user_id FROM public.logistics_strategies ORDER BY updated_at DESC LIMIT 1)
44448
+ );
44449
+
44450
+ v_code := public.logistics_block_reason_code(p_reason, v_details);
44451
+ v_buffer := NULLIF(v_details ->> 'bufferId', '');
44452
+
44453
+ UPDATE public.logistics_plan_queue
44454
+ SET status = 'geblokkeerd',
44455
+ reason = v_reason,
44456
+ reason_code = v_code,
44457
+ error = v_reason,
44458
+ claimed_at = NULL,
44459
+ updated_at = now()
44460
+ WHERE order_id = ANY(p_order_ids)
44461
+ AND status IN ('pending', 'running');
44462
+
44463
+ FOR o IN
44464
+ SELECT id, order_number FROM public.orders
44465
+ WHERE id = ANY(p_order_ids) AND deleted_at IS NULL
44466
+ LOOP
44467
+ v_count := v_count + 1;
44468
+
44469
+ IF v_user IS NOT NULL AND o.order_number IS NOT NULL THEN
44470
+ INSERT INTO public.logistics_events (
44471
+ user_id, source_module, event_type, order_ref, payload, shadow,
44472
+ status, processed_at, idempotency_key
44473
+ ) VALUES (
44474
+ v_user, 'logistiek-engine', 'plan.blocked', o.order_number,
44475
+ jsonb_build_object('reason', p_reason, 'reasonCode', v_code, 'orderId', o.id) || v_details,
44476
+ false, 'processed', now(),
44477
+ 'plan.blocked|' || o.id::text || '|' || v_code
44478
+ )
44479
+ ON CONFLICT DO NOTHING;
44480
+ END IF;
44481
+ END LOOP;
44482
+
44483
+ IF v_user IS NOT NULL THEN
44484
+ SELECT array_agg(COALESCE(ord.order_number, ord.id::text) ORDER BY COALESCE(ord.order_number, ord.id::text))
44485
+ INTO v_members
44486
+ FROM public.orders ord
44487
+ WHERE ord.id = ANY(p_order_ids) AND ord.deleted_at IS NULL;
44488
+
44489
+ IF v_buffer IS NOT NULL THEN
44490
+ v_subject_type := 'buffer';
44491
+ v_subject_id := v_buffer;
44492
+ ELSE
44493
+ v_subject_type := 'order';
44494
+ v_subject_id := p_order_ids[1]::text;
44495
+ END IF;
44496
+
44497
+ v_result := jsonb_build_object(
44498
+ 'blocked', true,
44499
+ 'reason_code', v_code,
44500
+ 'path', COALESCE(v_details ->> 'path', 'onbekend')
44501
+ );
44502
+
44503
+ SELECT d.result INTO v_last
44504
+ FROM public.logistics_decisions d
44505
+ WHERE d.user_id = v_user
44506
+ AND d.subject_type = v_subject_type
44507
+ AND d.subject_id = v_subject_id
44508
+ AND d.slot = 'beladingindeling'
44509
+ ORDER BY d.created_at DESC
44510
+ LIMIT 1;
44511
+
44512
+ IF v_last IS DISTINCT FROM v_result THEN
44513
+ INSERT INTO public.logistics_decisions (
44514
+ user_id, subject_type, subject_id, slot, result, explanation, input_snapshot
44515
+ ) VALUES (
44516
+ v_user, v_subject_type, v_subject_id, 'beladingindeling',
44517
+ v_result,
44518
+ left(COALESCE(p_reason, 'Belading onbekend'), 2000),
44519
+ v_details || jsonb_build_object(
44520
+ 'members', to_jsonb(COALESCE(v_members, ARRAY[]::text[])),
44521
+ 'memberCount', COALESCE(array_length(v_members, 1), 0)
44522
+ )
44523
+ );
44524
+ END IF;
44525
+ END IF;
44526
+
44527
+ RETURN v_count;
44528
+ END;
44529
+ $function$;
44530
+
44531
+ CREATE OR REPLACE FUNCTION public.logistics_plan_close_claim(p_id uuid, p_reason text, p_reason_code text DEFAULT NULL::text)
44532
+ RETURNS boolean
44533
+ LANGUAGE plpgsql
44534
+ SECURITY DEFINER
44535
+ SET search_path TO 'public'
44536
+ AS $function$
44537
+ DECLARE v_n int; v_order uuid;
44538
+ BEGIN
44539
+ PERFORM public.logistics_acc_guard('server', 'logistics_plan_close_claim');
44540
+ IF public.logistics_plan_is_tijdelijk(p_reason) THEN
44541
+ SELECT q.order_id INTO v_order FROM public.logistics_plan_queue q WHERE q.id = p_id AND q.status = 'running';
44542
+ IF v_order IS NOT NULL AND cardinality(public.logistics_plan_tijdelijk_opnieuw(ARRAY[v_order], p_reason)) = 0 THEN
44543
+ RETURN true;
44544
+ END IF;
44545
+ END IF;
44546
+ UPDATE public.logistics_plan_queue q
44547
+ SET status='geblokkeerd', reason=COALESCE(p_reason, q.reason), error=p_reason,
44548
+ reason_code=COALESCE(p_reason_code, public.logistics_block_reason_code(p_reason, '{}'::jsonb)),
44549
+ claimed_at=NULL, updated_at=now()
44550
+ WHERE q.id = p_id AND q.status = 'running';
44551
+ GET DIAGNOSTICS v_n = ROW_COUNT;
44552
+ RETURN v_n > 0;
44553
+ END $function$;
44554
+
44555
+ CREATE OR REPLACE FUNCTION public.logistics_plan_release_stale()
44556
+ RETURNS integer
44557
+ LANGUAGE plpgsql
44558
+ SECURITY DEFINER
44559
+ SET search_path TO 'public'
44560
+ AS $function$
44561
+ DECLARE
44562
+ v_n int := 0;
44563
+ v_min int := public.logistics_plan_setting('claim_timeout_minutes', 2)::int;
44564
+ v_max int := GREATEST(1, public.logistics_plan_setting('max_attempts', 5)::int);
44565
+ v_txt text;
44566
+ r record;
44567
+ BEGIN
44568
+ PERFORM public.logistics_acc_guard('server', 'logistics_plan_release_stale');
44569
+ v_txt := 'de ronde heeft deze indeling na ' || v_min || ' minuten niet afgesloten (claim niet afgerond)';
44570
+ FOR r IN
44571
+ UPDATE public.logistics_plan_queue q
44572
+ SET status = CASE WHEN q.attempts >= v_max THEN 'geblokkeerd' ELSE 'pending' END,
44573
+ error = v_txt,
44574
+ reason = CASE WHEN q.attempts >= v_max
44575
+ THEN v_txt || '; ' || q.attempts || ' keer achter elkaar, daarom geblokkeerd' ELSE q.reason END,
44576
+ reason_code = 'claim_niet_afgerond',
44577
+ claimed_at = NULL,
44578
+ next_attempt_at = now() + public.logistics_plan_tijdelijk_wacht(q.attempts),
44579
+ updated_at = now()
44580
+ WHERE q.status = 'running'
44581
+ AND q.claimed_at < now() - make_interval(mins => v_min)
44582
+ RETURNING q.order_id, q.attempts, q.status
44583
+ LOOP
44584
+ v_n := v_n + 1;
44585
+ BEGIN
44586
+ PERFORM public.logistics_trace_order_step(
44587
+ r.order_id, 'stap:beladingindeling', 'claim niet afgerond',
44588
+ CASE WHEN r.status = 'geblokkeerd' THEN 'mislukt' ELSE 'wacht' END,
44589
+ 'ronde sloot de indeling niet af',
44590
+ CASE WHEN r.status = 'geblokkeerd'
44591
+ THEN 'De ronde sloot de indeling ' || r.attempts || ' keer niet af. De order is geblokkeerd zodat dit niet blijft herhalen; bekijk de reden en bied hem opnieuw aan.'
44592
+ ELSE 'De ronde sloot de indeling niet af binnen ' || v_min || ' minuten. Er volgt automatisch een nieuwe poging (' || r.attempts || ' van ' || v_max || ').'
44593
+ END,
44594
+ 'Karrenindeling', v_txt, 'regel');
44595
+ EXCEPTION WHEN OTHERS THEN
44596
+ RAISE WARNING 'logistiek-engine: verloop niet vastgelegd voor order %: %', r.order_id, SQLERRM;
44597
+ END;
44598
+ END LOOP;
44599
+ RETURN v_n;
44600
+ END $function$;
44601
+
44602
+ -- ===== Gegevensblokkades automatisch opnieuw =====
44603
+ CREATE OR REPLACE FUNCTION public.logistics_plan_gegevens_codes()
44604
+ RETURNS text[] LANGUAGE sql IMMUTABLE SET search_path TO 'public'
44605
+ AS $function$
44606
+ SELECT ARRAY['belading_onbekend','belading_onvolledig','beladingsprofiel_ontbreekt','afmetingen_onbekend',
44607
+ 'afmetingen_plan_ongeldig','ladingdrager_onbekend','artikel_ontbreekt','belading_onwaarschijnlijk',
44608
+ 'karindeling_geblokkeerd']
44609
+ $function$;
44610
+ REVOKE EXECUTE ON FUNCTION public.logistics_plan_gegevens_codes() FROM PUBLIC, anon;
44611
+ GRANT EXECUTE ON FUNCTION public.logistics_plan_gegevens_codes() TO authenticated, service_role;
44612
+
44613
+ CREATE OR REPLACE FUNCTION public.logistics_plan_gegevens_opnieuw(p_order_ids uuid[], p_reden text)
44614
+ RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
44615
+ AS $function$
44616
+ -- Biedt alleen de geblokkeerde orders met een gegevensblokkade opnieuw aan. Per order een eigen poging.
44617
+ DECLARE v_id uuid; v_n int := 0;
44618
+ BEGIN
44619
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_plan_gegevens_opnieuw');
44620
+ FOR v_id IN
44621
+ SELECT DISTINCT q.order_id FROM public.logistics_plan_queue q
44622
+ WHERE q.order_id = ANY(p_order_ids) AND q.status = 'geblokkeerd'
44623
+ AND q.reason_code = ANY(public.logistics_plan_gegevens_codes())
44624
+ LOOP
44625
+ BEGIN
44626
+ PERFORM public.logistics_plan_enqueue(v_id, p_reden);
44627
+ v_n := v_n + 1;
44628
+ EXCEPTION WHEN OTHERS THEN
44629
+ PERFORM public.logistics_plan_fail_log('logistics_plan_gegevens_opnieuw', v_id, SQLSTATE, SQLERRM);
44630
+ END;
44631
+ END LOOP;
44632
+ RETURN v_n;
44633
+ END $function$;
44634
+ REVOKE EXECUTE ON FUNCTION public.logistics_plan_gegevens_opnieuw(uuid[], text) FROM PUBLIC, anon, authenticated;
44635
+ GRANT EXECUTE ON FUNCTION public.logistics_plan_gegevens_opnieuw(uuid[], text) TO service_role;
44636
+
44637
+ CREATE OR REPLACE FUNCTION public.logistics_fust_maten_gewijzigd_trg()
44638
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
44639
+ AS $function$
44640
+ DECLARE v_orders uuid[];
44641
+ BEGIN
44642
+ BEGIN
44643
+ IF NEW.length_mm IS NOT DISTINCT FROM OLD.length_mm AND NEW.width_mm IS NOT DISTINCT FROM OLD.width_mm
44644
+ AND NEW.height_mm IS NOT DISTINCT FROM OLD.height_mm AND NEW.vbn_code IS NOT DISTINCT FROM OLD.vbn_code
44645
+ AND NEW.custom_package_id IS NOT DISTINCT FROM OLD.custom_package_id AND NEW.fust_key IS NOT DISTINCT FROM OLD.fust_key THEN
44646
+ RETURN NEW;
44647
+ END IF;
44648
+ WITH geblokkeerd AS MATERIALIZED (
44649
+ SELECT DISTINCT q.order_id FROM public.logistics_plan_queue q
44650
+ WHERE q.status = 'geblokkeerd' AND q.reason_code = ANY(public.logistics_plan_gegevens_codes())
44651
+ ), prod AS MATERIALIZED (
44652
+ SELECT DISTINCT oi.order_id, oi.product_id FROM public.order_items oi JOIN geblokkeerd g ON g.order_id = oi.order_id
44653
+ WHERE oi.product_id IS NOT NULL
44654
+ ), maten AS MATERIALIZED (
44655
+ SELECT m.product_id, m.fust_type_id FROM public.logistics_product_maten((SELECT array_agg(DISTINCT product_id) FROM prod)) m
44656
+ WHERE EXISTS (SELECT 1 FROM prod)
44657
+ )
44658
+ SELECT array_agg(DISTINCT p.order_id) INTO v_orders
44659
+ FROM prod p LEFT JOIN maten m ON m.product_id = p.product_id
44660
+ LEFT JOIN public.products pr ON pr.id = p.product_id
44661
+ WHERE m.fust_type_id = NEW.id OR pr.fust_type_id = NEW.id;
44662
+ IF v_orders IS NOT NULL THEN
44663
+ PERFORM public.logistics_plan_gegevens_opnieuw(v_orders, 'fustmaten aangepast');
44664
+ END IF;
44665
+ EXCEPTION WHEN OTHERS THEN
44666
+ RAISE WARNING 'logistiek-engine: opnieuw aanbieden na fustwijziging mislukt: %', SQLERRM;
44667
+ BEGIN
44668
+ PERFORM public.logistics_plan_fail_log('trg_logistics_fust_maten_gewijzigd', NULL, SQLSTATE, SQLERRM);
44669
+ EXCEPTION WHEN OTHERS THEN
44670
+ RAISE WARNING 'logistiek-engine: fout niet vastgelegd: %', SQLERRM;
44671
+ END;
44672
+ END;
44673
+ RETURN NEW;
44674
+ END $function$;
44675
+ REVOKE EXECUTE ON FUNCTION public.logistics_fust_maten_gewijzigd_trg() FROM PUBLIC, anon, authenticated;
44676
+ DO $fs$ BEGIN
44677
+ IF to_regclass('public.fust_types') IS NOT NULL THEN
44678
+ EXECUTE 'DROP TRIGGER IF EXISTS trg_logistics_fust_maten_gewijzigd ON public.fust_types';
44679
+ EXECUTE 'CREATE TRIGGER trg_logistics_fust_maten_gewijzigd AFTER UPDATE OF length_mm, width_mm, height_mm, vbn_code, custom_package_id, fust_key ON public.fust_types FOR EACH ROW EXECUTE FUNCTION public.logistics_fust_maten_gewijzigd_trg()';
44680
+ ELSE
44681
+ RAISE NOTICE 'logistiek-engine: trigger fustmaten overgeslagen (tabel fust_types van Voorraad ontbreekt)';
44682
+ END IF;
44683
+ EXCEPTION WHEN undefined_column THEN
44684
+ RAISE NOTICE 'logistiek-engine: trigger fustmaten overgeslagen: %', SQLERRM;
44685
+ END $fs$;
44686
+
44687
+ CREATE OR REPLACE FUNCTION public.logistics_plan_products_trg()
44688
+ RETURNS trigger
44689
+ LANGUAGE plpgsql
44690
+ SECURITY DEFINER
44691
+ SET search_path TO 'public'
44692
+ AS $function$
44693
+ DECLARE r record;
44694
+ BEGIN
44695
+ IF NEW.plant_height_cm IS NOT DISTINCT FROM OLD.plant_height_cm
44696
+ AND NEW.fust_type_id IS NOT DISTINCT FROM OLD.fust_type_id
44697
+ AND NEW.plants_per_crate IS NOT DISTINCT FROM OLD.plants_per_crate
44698
+ AND NEW.crates_per_layer IS NOT DISTINCT FROM OLD.crates_per_layer
44699
+ AND NEW.layers_per_cart IS NOT DISTINCT FROM OLD.layers_per_cart
44700
+ AND NEW.is_layer IS NOT DISTINCT FROM OLD.is_layer THEN
44701
+ RETURN NEW;
44702
+ END IF;
44703
+
44704
+ BEGIN
44705
+ FOR r IN
44706
+ SELECT DISTINCT o.id
44707
+ FROM logistics_active.order_items oi
44708
+ JOIN public.orders o ON o.id = oi.order_id
44709
+ WHERE oi.product_id = NEW.id
44710
+ AND o.deleted_at IS NULL
44711
+ AND COALESCE(o.is_reservation, false) = false
44712
+ AND o.plan_locked_at IS NULL
44713
+ AND (o.delivery_date IS NULL OR public.logistics_order_date(o.delivery_date::text) >= (now() AT TIME ZONE coalesce(public.logistics_tz(), 'Europe/Amsterdam'))::date)
44714
+ LOOP
44715
+ BEGIN
44716
+ PERFORM public.logistics_plan_enqueue(r.id, 'product');
44717
+ EXCEPTION WHEN OTHERS THEN
44718
+ PERFORM public.logistics_plan_fail_log('trg_logistics_plan_products', r.id, SQLSTATE, SQLERRM);
44719
+ END;
44720
+ END LOOP;
44721
+ EXCEPTION WHEN OTHERS THEN
44722
+ RAISE WARNING 'logistiek-engine: opnieuw aanbieden na productwijziging mislukt: %', SQLERRM;
44723
+ END;
44724
+
44725
+ RETURN NEW;
44726
+ END;
44727
+ $function$;
44728
+
44729
+ -- ===== Onderhoud in de minuutronde =====
44730
+ CREATE OR REPLACE FUNCTION public.logistics_plan_herstel_tijdelijk()
44731
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
44732
+ AS $function$
44733
+ -- Eenmalig: orders die vóór deze versie geblokkeerd raakten door een time-out of een niet afgesloten ronde
44734
+ -- worden opnieuw aangeboden. Een tweede keer doet niets.
44735
+ DECLARE v_key text := 'herstel_tijdelijk_2026_11_10'; v_val text; v_n int := 0; v_id uuid;
44736
+ BEGIN
44737
+ PERFORM public.logistics_acc_guard('server', 'logistics_plan_herstel_tijdelijk');
44738
+ SELECT value INTO v_val FROM public.logistics_plan_settings WHERE key = v_key;
44739
+ IF v_val IS NOT NULL THEN
44740
+ RETURN jsonb_build_object('gedaan', true, 'aantal', coalesce(nullif(v_val, '')::int, 0), 'nu', 0);
44741
+ END IF;
44742
+ FOR v_id IN
44743
+ SELECT DISTINCT q.order_id FROM public.logistics_plan_queue q JOIN public.orders o ON o.id = q.order_id
44744
+ WHERE q.status IN ('geblokkeerd', 'failed')
44745
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
44746
+ AND (q.reason_code = 'claim_niet_afgerond'
44747
+ OR public.logistics_plan_is_tijdelijk(coalesce(q.error, '') || ' ' || coalesce(q.reason, '')))
44748
+ LOOP
44749
+ BEGIN
44750
+ UPDATE public.logistics_plan_queue q
44751
+ SET status = 'pending', attempts = 0, reason = 'automatisch opnieuw aangeboden na een time-out',
44752
+ reason_code = NULL, error = NULL, claimed_at = NULL, next_attempt_at = now(), updated_at = now()
44753
+ WHERE q.order_id = v_id AND q.status IN ('geblokkeerd', 'failed');
44754
+ v_n := v_n + 1;
44755
+ EXCEPTION WHEN OTHERS THEN
44756
+ PERFORM public.logistics_plan_fail_log('logistics_plan_herstel_tijdelijk', v_id, SQLSTATE, SQLERRM);
44757
+ END;
44758
+ END LOOP;
44759
+ INSERT INTO public.logistics_plan_settings(key, value, updated_at) VALUES (v_key, v_n::text, now())
44760
+ ON CONFLICT (key) DO NOTHING;
44761
+ RETURN jsonb_build_object('gedaan', true, 'aantal', v_n, 'nu', v_n);
44762
+ END $function$;
44763
+ REVOKE EXECUTE ON FUNCTION public.logistics_plan_herstel_tijdelijk() FROM PUBLIC, anon, authenticated;
44764
+ GRANT EXECUTE ON FUNCTION public.logistics_plan_herstel_tijdelijk() TO service_role;
44765
+
44766
+ CREATE OR REPLACE FUNCTION public.logistics_plan_gegevens_vangnet(p_force boolean DEFAULT false)
44767
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
44768
+ AS $function$
44769
+ -- Hooguit elke 15 minuten: gegevensblokkade waarvan de gegevens na de blokkade zijn aangepast -> opnieuw.
44770
+ -- Meet meteen hoe lang het lezen van alle productmaten duurt.
44771
+ DECLARE v_last timestamptz; v_orders uuid[]; v_n int := 0; v_t timestamptz; v_ms int; v_cnt int;
44772
+ BEGIN
44773
+ PERFORM public.logistics_acc_guard('server', 'logistics_plan_gegevens_vangnet');
44774
+ SELECT nullif(value, '')::timestamptz INTO v_last FROM public.logistics_plan_settings WHERE key = 'gegevens_vangnet_at';
44775
+ IF NOT p_force AND v_last IS NOT NULL AND v_last > now() - interval '15 minutes' THEN
44776
+ RETURN jsonb_build_object('overgeslagen', true, 'laatst', v_last);
44777
+ END IF;
44778
+ INSERT INTO public.logistics_plan_settings(key, value, updated_at) VALUES ('gegevens_vangnet_at', now()::text, now())
44779
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
44780
+
44781
+ SELECT array_agg(DISTINCT q.order_id) INTO v_orders
44782
+ FROM public.logistics_plan_queue q
44783
+ WHERE q.status = 'geblokkeerd' AND q.reason_code = ANY(public.logistics_plan_gegevens_codes())
44784
+ AND (EXISTS (SELECT 1 FROM public.order_items oi JOIN public.products p ON p.id = oi.product_id
44785
+ WHERE oi.order_id = q.order_id AND p.updated_at > q.updated_at)
44786
+ OR EXISTS (SELECT 1 FROM public.order_items oi JOIN public.products p ON p.id = oi.product_id
44787
+ JOIN public.fust_types f ON f.id = p.fust_type_id
44788
+ WHERE oi.order_id = q.order_id AND f.updated_at > q.updated_at)
44789
+ OR EXISTS (SELECT 1 FROM public.logistics_belading_bevestigingen b
44790
+ WHERE b.order_id = q.order_id AND b.created_at > q.updated_at)
44791
+ OR (q.reason_code = 'artikel_ontbreekt'
44792
+ AND NOT EXISTS (SELECT 1 FROM public.order_items oi WHERE oi.order_id = q.order_id AND oi.product_id IS NULL)));
44793
+ IF v_orders IS NOT NULL THEN
44794
+ v_n := public.logistics_plan_gegevens_opnieuw(v_orders, 'gegevens aangepast (controle minuutronde)');
44795
+ END IF;
44796
+
44797
+ v_t := clock_timestamp();
44798
+ BEGIN
44799
+ SELECT count(*) INTO v_cnt FROM public.logistics_product_maten(NULL);
44800
+ v_ms := (extract(epoch FROM clock_timestamp() - v_t) * 1000)::int;
44801
+ EXCEPTION WHEN OTHERS THEN
44802
+ v_ms := (extract(epoch FROM clock_timestamp() - v_t) * 1000)::int;
44803
+ RAISE WARNING 'logistiek-engine: meting productmaten mislukt: %', SQLERRM;
44804
+ END;
44805
+ INSERT INTO public.logistics_plan_settings(key, value, updated_at) VALUES ('productmaten_ms', v_ms::text, now())
44806
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
44807
+ RETURN jsonb_build_object('opnieuw', v_n, 'productmatenMs', v_ms);
44808
+ END $function$;
44809
+ REVOKE EXECUTE ON FUNCTION public.logistics_plan_gegevens_vangnet(boolean) FROM PUBLIC, anon, authenticated;
44810
+ GRANT EXECUTE ON FUNCTION public.logistics_plan_gegevens_vangnet(boolean) TO service_role;
44811
+
44812
+ CREATE OR REPLACE FUNCTION public.logistics_karindeling_bewaking()
44813
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
44814
+ AS $function$
44815
+ DECLARE v jsonb;
44816
+ BEGIN
44817
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_karindeling_bewaking');
44818
+ SELECT jsonb_build_object(
44819
+ 'achterstand', (SELECT count(DISTINCT q.order_id) FROM public.logistics_plan_queue q JOIN public.orders o ON o.id = q.order_id
44820
+ WHERE q.status IN ('pending', 'running') AND q.enqueued_at < now() - interval '10 minutes'
44821
+ AND o.deleted_at IS NULL),
44822
+ 'productmatenMs', (SELECT nullif(value, '')::int FROM public.logistics_plan_settings WHERE key = 'productmaten_ms'),
44823
+ 'productmatenOp', (SELECT updated_at FROM public.logistics_plan_settings WHERE key = 'productmaten_ms'),
44824
+ 'tijdelijkOpnieuw', (SELECT count(*) FROM public.logistics_plan_queue q WHERE q.status = 'pending' AND q.reason_code IN ('tijdelijk', 'claim_niet_afgerond')),
44825
+ 'herstelTijdelijk', (SELECT nullif(value, '')::int FROM public.logistics_plan_settings WHERE key = 'herstel_tijdelijk_2026_11_10')
44826
+ ) INTO v;
44827
+ RETURN v;
44828
+ END $function$;
44829
+ REVOKE EXECUTE ON FUNCTION public.logistics_karindeling_bewaking() FROM PUBLIC, anon;
44830
+ GRANT EXECUTE ON FUNCTION public.logistics_karindeling_bewaking() TO authenticated, service_role;
44831
+
44832
+ CREATE OR REPLACE FUNCTION public.logistics_karindeling_onderhoud()
44833
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
44834
+ AS $function$
44835
+ DECLARE v jsonb := '{}'::jsonb;
44836
+ BEGIN
44837
+ PERFORM public.logistics_acc_guard('server', 'logistics_karindeling_onderhoud');
44838
+ BEGIN
44839
+ v := v || jsonb_build_object('herstel', public.logistics_plan_herstel_tijdelijk());
44840
+ EXCEPTION WHEN OTHERS THEN
44841
+ v := v || jsonb_build_object('herstel', left(SQLERRM, 200));
44842
+ END;
44843
+ BEGIN
44844
+ v := v || jsonb_build_object('vangnet', public.logistics_plan_gegevens_vangnet(false));
44845
+ EXCEPTION WHEN OTHERS THEN
44846
+ v := v || jsonb_build_object('vangnet', left(SQLERRM, 200));
44847
+ END;
44848
+ RETURN v;
44849
+ END $function$;
44850
+ REVOKE EXECUTE ON FUNCTION public.logistics_karindeling_onderhoud() FROM PUBLIC, anon, authenticated;
44851
+ GRANT EXECUTE ON FUNCTION public.logistics_karindeling_onderhoud() TO service_role;
44852
+
44853
+ CREATE OR REPLACE FUNCTION public.logistics_plan_finish(p_id uuid, p_error text DEFAULT NULL::text)
44854
+ RETURNS void
44855
+ LANGUAGE plpgsql
44856
+ SECURITY DEFINER
44857
+ SET search_path TO 'public'
44858
+ AS $function$
44859
+ DECLARE
44860
+ v_max int := public.logistics_plan_setting('max_attempts', 5)::int;
44861
+ v_attempts int;
44862
+ v_order uuid;
44863
+ v_text text;
44864
+ v_class text;
44865
+ v_next timestamptz;
44866
+ v_min numeric;
44867
+ BEGIN
44868
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_plan_finish');
44869
+ SELECT order_id, attempts INTO v_order, v_attempts
44870
+ FROM public.logistics_plan_queue WHERE id = p_id;
44871
+
44872
+ IF p_error IS NULL THEN
44873
+ UPDATE public.logistics_plan_queue SET status='done', error=NULL, claimed_at=NULL, updated_at=now() WHERE id = p_id;
44874
+ PERFORM public.logistics_trace_order_step(
44875
+ v_order, 'stap:beladingindeling', 'karrenindeling afgerond', 'geslaagd',
44876
+ 'indeling klaar', NULL, 'Order picken', NULL, NULL);
44877
+ BEGIN
44878
+ PERFORM public.logistics_trace_progress_order(v_order::text);
44879
+ EXCEPTION WHEN OTHERS THEN
44880
+ NULL;
44881
+ END;
44882
+ RETURN;
44883
+ END IF;
44884
+
44885
+ IF p_error = 'deadline' THEN
44886
+ PERFORM public.logistics_plan_defer(p_id);
44887
+ RETURN;
44888
+ END IF;
44889
+
44890
+ v_text := public.logistics_plan_error_text(p_error);
44891
+ v_class := public.logistics_plan_error_class(p_error);
44892
+
44893
+ IF COALESCE(v_attempts,0) >= v_max THEN
44894
+ UPDATE public.logistics_plan_queue SET status='failed', error=p_error, claimed_at=NULL, updated_at=now() WHERE id = p_id;
44895
+ PERFORM public.logistics_trace_order_step(
44896
+ v_order, 'stap:beladingindeling', 'karrenindeling mislukt', 'mislukt',
44897
+ 'gestopt na ' || COALESCE(v_attempts,0) || ' van ' || v_max || ' pogingen',
44898
+ v_text || ' Dit ging ' || COALESCE(v_attempts,0) || ' keer achter elkaar mis; de engine probeert het niet meer vanzelf. Bestaande karren blijven staan. Los de oorzaak op en start de order opnieuw via de wachtrij.',
44899
+ 'Oorzaak oplossen en opnieuw aanmelden in de wachtrij', v_text, v_class);
44900
+ ELSE
44901
+ -- Tijdelijke fout (time-out, ronde niet afgesloten): oplopend 1, 2, 4, 8, 16 minuten.
44902
+ v_min := CASE
44903
+ WHEN public.logistics_plan_is_tijdelijk(p_error) THEN EXTRACT(epoch FROM public.logistics_plan_tijdelijk_wacht(v_attempts)) / 60
44904
+ WHEN COALESCE(v_attempts,1) <= 1 THEN public.logistics_plan_setting('retry_1_min', 5)
44905
+ WHEN v_attempts = 2 THEN public.logistics_plan_setting('retry_2_min', 15)
44906
+ ELSE public.logistics_plan_setting('retry_3_min', 60) END;
44907
+ v_next := now() + make_interval(mins => GREATEST(1, v_min)::int);
44908
+ UPDATE public.logistics_plan_queue
44909
+ SET status='pending', error=p_error, claimed_at=NULL, updated_at=now(),
44910
+ next_attempt_at = v_next
44911
+ WHERE id = p_id;
44912
+ PERFORM public.logistics_trace_order_step(
44913
+ v_order, 'stap:beladingindeling', 'karrenindeling opnieuw proberen', 'wachtend',
44914
+ 'nieuwe poging ingepland (poging ' || (COALESCE(v_attempts,0) + 1) || ' van ' || v_max || ')',
44915
+ v_text || ' Bestaande karren blijven staan. Volgende poging om ' || to_char(v_next AT TIME ZONE public.logistics_tz(), 'HH24:MI') || ' uur.',
44916
+ 'Karrenindeling', v_text, v_class);
44917
+ END IF;
44918
+ END;
44919
+ $function$;
44920
+
44921
+ CREATE OR REPLACE FUNCTION public.logistics_tick()
44922
+ RETURNS jsonb
44923
+ LANGUAGE plpgsql
44924
+ SECURITY DEFINER
44925
+ SET search_path TO 'public'
44926
+ AS $function$
44927
+ DECLARE v_result jsonb := '{}'::jsonb; v_start timestamptz := clock_timestamp(); v_t timestamptz; v_err boolean := false;
44928
+ BEGIN
44929
+ PERFORM public.logistics_acc_guard('server', 'logistics_tick');
44930
+ v_t := clock_timestamp();
44931
+ BEGIN
44932
+ PERFORM set_config('statement_timeout', '10000', true);
44933
+ v_result := v_result || jsonb_build_object('vervoerder', public.logistics_carrier_recalc_run(200));
44934
+ EXCEPTION WHEN OTHERS THEN
44935
+ v_err := true; v_result := v_result || jsonb_build_object('vervoerder', left(SQLERRM, 200));
44936
+ END;
44937
+ v_result := v_result || jsonb_build_object('vervoerder_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
44938
+
44939
+ v_t := clock_timestamp();
44940
+ BEGIN
44941
+ PERFORM set_config('statement_timeout', '15000', true);
44942
+ PERFORM public.logistics_rebuffer_pending();
44943
+ v_result := v_result || jsonb_build_object('rebuffer', 'ok');
44944
+ EXCEPTION WHEN OTHERS THEN
44945
+ v_err := true;
44946
+ IF SQLERRM ILIKE '%tijdzone%' THEN
44947
+ v_result := v_result || jsonb_build_object('rebuffer', 'overgeslagen: tijdzone ontbreekt in Werktijden');
44948
+ BEGIN
44949
+ UPDATE public.logistics_rebuffer_state SET last_error = 'Herindelen overgeslagen: de tijdzone ontbreekt in Werktijden.', updated_at = now() WHERE id;
44950
+ EXCEPTION WHEN OTHERS THEN NULL; END;
44951
+ ELSE
44952
+ v_result := v_result || jsonb_build_object('rebuffer', left(SQLERRM, 200));
44953
+ END IF;
44954
+ END;
44955
+ v_result := v_result || jsonb_build_object('rebuffer_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
44956
+
44957
+ v_t := clock_timestamp();
44958
+ BEGIN
44959
+ PERFORM set_config('statement_timeout', '20000', true);
44960
+ PERFORM public.run_logistics_tasks_dispatch();
44961
+ v_result := v_result || jsonb_build_object('taken', 'ok');
44962
+ EXCEPTION WHEN OTHERS THEN
44963
+ v_err := true; v_result := v_result || jsonb_build_object('taken', left(SQLERRM, 200));
44964
+ END;
44965
+ v_result := v_result || jsonb_build_object('taken_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
44966
+
44967
+ v_t := clock_timestamp();
44968
+ BEGIN
44969
+ PERFORM set_config('statement_timeout', '20000', true);
44970
+ v_result := v_result || jsonb_build_object('afgerond', public.logistics_order_finalize_run(25, 120));
44971
+ EXCEPTION WHEN OTHERS THEN
44972
+ v_err := true; v_result := v_result || jsonb_build_object('afgerond', left(SQLERRM, 200));
44973
+ END;
44974
+ v_result := v_result || jsonb_build_object('afgerond_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
44975
+
44976
+ v_t := clock_timestamp();
44977
+ BEGIN
44978
+ PERFORM set_config('statement_timeout', '15000', true);
44979
+ v_result := v_result || jsonb_build_object('pickwachtrij', public.logistics_sync_pick_queue());
44980
+ EXCEPTION WHEN OTHERS THEN
44981
+ v_err := true; v_result := v_result || jsonb_build_object('pickwachtrij', left(SQLERRM, 200));
44982
+ END;
44983
+ v_result := v_result || jsonb_build_object('pickwachtrij_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
44984
+
44985
+ v_t := clock_timestamp();
44986
+ BEGIN
44987
+ PERFORM set_config('statement_timeout', '10000', true);
44988
+ v_result := v_result || jsonb_build_object('laadtoestand', public.logistics_refresh_plan_state_als_nodig());
44989
+ EXCEPTION WHEN OTHERS THEN
44990
+ v_err := true; v_result := v_result || jsonb_build_object('laadtoestand', left(SQLERRM, 200));
44991
+ END;
44992
+ v_result := v_result || jsonb_build_object('laadtoestand_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
44993
+
44994
+ v_t := clock_timestamp();
44995
+ BEGIN
44996
+ PERFORM set_config('statement_timeout', '10000', true);
44997
+ v_result := v_result || jsonb_build_object('instellingen', public.logistics_ensure_periodiek());
44998
+ EXCEPTION WHEN OTHERS THEN
44999
+ v_err := true; v_result := v_result || jsonb_build_object('instellingen', left(SQLERRM, 200));
45000
+ END;
45001
+ v_result := v_result || jsonb_build_object('instellingen_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
45002
+
45003
+ -- Karindeling: eenmalig herstel na time-outs en (hooguit elke 15 min) vangnet voor gegevensblokkades.
45004
+ v_t := clock_timestamp();
45005
+ BEGIN
45006
+ PERFORM set_config('statement_timeout', '10000', true);
45007
+ v_result := v_result || jsonb_build_object('karindeling', public.logistics_karindeling_onderhoud());
45008
+ EXCEPTION WHEN OTHERS THEN
45009
+ v_err := true; v_result := v_result || jsonb_build_object('karindeling', left(SQLERRM, 200));
45010
+ END;
45011
+ v_result := v_result || jsonb_build_object('karindeling_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
45012
+
45013
+ BEGIN
45014
+ INSERT INTO public.logistics_tick_runs(started_at, finished_at, duration_ms, has_error, steps)
45015
+ VALUES (v_start, clock_timestamp(), (extract(epoch from clock_timestamp()-v_start)*1000)::int, v_err, v_result);
45016
+ EXCEPTION WHEN OTHERS THEN NULL;
45017
+ END;
45018
+ PERFORM set_config('statement_timeout', '30000', true);
45019
+ RETURN v_result;
45020
+ END $function$;
45021
+
45022
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_samenvoegen()
45023
+ RETURNS TABLE(onderdeel text, ok boolean, toelichting text)
45024
+ LANGUAGE plpgsql
45025
+ STABLE SECURITY DEFINER
45026
+ SET search_path TO 'public'
45027
+ AS $function$
45028
+ DECLARE f text;
45029
+ BEGIN
45030
+ FOREACH f IN ARRAY ARRAY['logistics_order_is_backfill(uuid)', 'logistics_order_merge_key_v2(uuid)',
45031
+ 'logistics_order_mergeable(uuid)', 'logistics_merge_floriday_lines(uuid)', 'logistics_order_buffer_ref(uuid)',
45032
+ 'logistics_merge_herstel_voorbeeld()', 'logistics_merge_herstel_uitvoeren(text,integer)',
45033
+ 'logistics_merge_status()', 'logistics_trace_event(uuid,text,text,text)', 'logistics_schema_versie()',
45034
+ 'logistics_pickup_regel()', 'logistics_pickup_moment_kies(uuid,date,timestamp with time zone,text,numeric)',
45035
+ 'logistics_pickup_vul_voorbeeld()', 'logistics_pickup_vul_uitvoeren(integer)', 'logistics_pickup_status()',
45036
+ 'logistics_pickup_herwijs(uuid[],integer)', 'logistics_refresh_pickup_moments(integer)',
45037
+ 'logistics_plan_is_tijdelijk(text)', 'logistics_plan_tijdelijk_wacht(integer)',
45038
+ 'logistics_plan_tijdelijk_opnieuw(uuid[],text)', 'logistics_plan_gegevens_codes()',
45039
+ 'logistics_plan_gegevens_opnieuw(uuid[],text)', 'logistics_plan_herstel_tijdelijk()',
45040
+ 'logistics_plan_gegevens_vangnet(boolean)', 'logistics_karindeling_bewaking()',
45041
+ 'logistics_karindeling_onderhoud()', 'logistics_fust_maten_gewijzigd_trg()'] LOOP
45042
+ onderdeel := 'functie ' || f;
45043
+ ok := to_regprocedure('public.' || f) IS NOT NULL;
45044
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: werk de Engine bij via Supabase bijwerken' END;
45045
+ RETURN NEXT;
45046
+ END LOOP;
45047
+ onderdeel := 'tabel logistics_merge_herstel_log';
45048
+ ok := to_regclass('public.logistics_merge_herstel_log') IS NOT NULL;
45049
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: werk de Engine bij via Supabase bijwerken' END;
45050
+ RETURN NEXT;
45051
+ onderdeel := 'kolom logistics_pickup_moments.carrier_id';
45052
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public'
45053
+ AND table_name = 'logistics_pickup_moments' AND column_name = 'carrier_id');
45054
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: werk de Engine bij via Supabase bijwerken' END;
45055
+ RETURN NEXT;
45056
+ onderdeel := 'trigger ophaalmoment op orders';
45057
+ ok := EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_logistics_orders_zpickup_moment');
45058
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: werk de Engine bij via Supabase bijwerken' END;
45059
+ RETURN NEXT;
45060
+ onderdeel := 'trigger fustmaten gewijzigd';
45061
+ ok := EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_logistics_fust_maten_gewijzigd');
45062
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: werk de Engine bij via Supabase bijwerken' END;
45063
+ RETURN NEXT;
45064
+ END $function$;
45065
+
45066
+ CREATE OR REPLACE FUNCTION public.logistics_schema_versie()
45067
+ RETURNS text LANGUAGE sql IMMUTABLE SET search_path TO 'public'
45068
+ AS $function$ SELECT '2026-11-10d'::text $function$;
45069
+ -- ===== 2026-11-10d: tijdbudget planstatus pickscherm =====
45070
+ CREATE OR REPLACE FUNCTION public.logistics_refresh_plan_state(p_limit integer DEFAULT 500)
45071
+ RETURNS jsonb
45072
+ LANGUAGE plpgsql
45073
+ SECURITY DEFINER
45074
+ SET search_path TO 'public'
45075
+ AS $function$
45076
+ -- Per pickwachtrijregel wordt de planstatus opnieuw bepaald. Dat kost per regel enkele honderden ms,
45077
+ -- daarom geldt een tijdbudget van 2 s per ronde; de volgende ronde gaat verder waar deze stopte.
45078
+ declare rec record; v jsonb; v_ref text; v_updated integer := 0; v_checked integer := 0;
45079
+ v_start timestamptz := clock_timestamp(); v_cursor timestamptz; v_last timestamptz; v_klaar boolean := true;
45080
+ begin
45081
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_refresh_plan_state');
45082
+ SELECT nullif(value->>'cursor', '')::timestamptz INTO v_cursor FROM public.logistics_tick_state WHERE key = 'refresh_plan_state_cursor';
45083
+ for rec in
45084
+ select q.id, q.payload, q.order_number, q.document_number, q.created_at
45085
+ from public.pick_queue q
45086
+ where q.cancelled_at is null and q.completed_at is null
45087
+ and (v_cursor is null or q.created_at < v_cursor)
45088
+ order by q.created_at desc
45089
+ limit greatest(coalesce(p_limit, 500), 1)
45090
+ loop
45091
+ if clock_timestamp() - v_start > interval '2 seconds' then
45092
+ v_klaar := false;
45093
+ exit;
45094
+ end if;
45095
+ v_last := rec.created_at;
45096
+ v_checked := v_checked + 1;
45097
+ v_ref := coalesce(nullif(rec.payload->>'orderRef', ''), rec.order_number, rec.document_number);
45098
+ if v_ref is null then continue; end if;
45099
+ v := public.logistics_plan_state_for(v_ref);
45100
+ update public.pick_queue q
45101
+ set payload = coalesce(q.payload, '{}'::jsonb) || jsonb_build_object(
45102
+ 'planState', v->>'planState',
45103
+ 'planProgress', v->'planProgress',
45104
+ 'planStateSince', v->>'planStateSince',
45105
+ 'cartPlanReady', coalesce((v->>'cartPlanReady')::boolean, false),
45106
+ 'cartPlanReleased', coalesce((v->>'cartPlanReleased')::boolean, false),
45107
+ 'cartPlanReason', v->>'cartPlanReason')
45108
+ where q.id = rec.id
45109
+ and coalesce(q.payload->>'planState', '') is distinct from coalesce(v->>'planState', '');
45110
+ if found then v_updated := v_updated + 1; end if;
45111
+ end loop;
45112
+ begin
45113
+ INSERT INTO public.logistics_tick_state(key, value, updated_at)
45114
+ VALUES ('refresh_plan_state_cursor', jsonb_build_object('cursor', CASE WHEN v_klaar THEN NULL ELSE v_last END), now())
45115
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
45116
+ exception when others then
45117
+ raise warning 'logistiek-engine: voortgang planstatus niet opgeslagen: %', SQLERRM;
45118
+ end;
45119
+ return jsonb_build_object('updated', v_updated, 'gecontroleerd', v_checked, 'klaar', v_klaar);
45120
+ end;
45121
+ $function$;
45122
+ CREATE OR REPLACE FUNCTION public.logistics_refresh_plan_state_als_nodig()
45123
+ RETURNS jsonb
45124
+ LANGUAGE plpgsql
45125
+ SECURITY DEFINER
45126
+ SET search_path TO 'public'
45127
+ AS $function$
45128
+ DECLARE v_wm timestamptz; v_prev timestamptz; v_at timestamptz; v_res jsonb; v_bezig boolean;
45129
+ BEGIN
45130
+ PERFORM public.logistics_acc_guard('server', 'logistics_refresh_plan_state_als_nodig');
45131
+ SELECT greatest((SELECT max(updated_at) FROM public.logistics_plan_queue),
45132
+ (SELECT max(updated_at) FROM public.logistics_tasks)) INTO v_wm;
45133
+ SELECT (value->>'wm')::timestamptz, (value->>'at')::timestamptz INTO v_prev, v_at
45134
+ FROM public.logistics_tick_state WHERE key = 'refresh_plan_state';
45135
+ SELECT (value->>'cursor') IS NOT NULL INTO v_bezig FROM public.logistics_tick_state WHERE key = 'refresh_plan_state_cursor';
45136
+ IF NOT coalesce(v_bezig, false) AND v_at IS NOT NULL AND v_wm IS NOT DISTINCT FROM v_prev AND v_at > now() - interval '5 minutes' THEN
45137
+ RETURN jsonb_build_object('overgeslagen', 'geen wijziging');
45138
+ END IF;
45139
+ v_res := public.logistics_refresh_plan_state(p_limit => 500);
45140
+ INSERT INTO public.logistics_tick_state(key, value, updated_at)
45141
+ VALUES ('refresh_plan_state', jsonb_build_object('wm', v_wm, 'at', now()), now())
45142
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
45143
+ RETURN v_res;
45144
+ END $function$;