@flowselections/logistiek-engine 1.0.239 → 1.0.241

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/db/schema/logistiek-engine.sql +2097 -0
  2. package/dist-lib/components/GeblokkeerdeOrdersCard.d.ts.map +1 -1
  3. package/dist-lib/components/GeblokkeerdeOrdersCard.js +2 -2
  4. package/dist-lib/components/KarplanStatusCard.d.ts.map +1 -1
  5. package/dist-lib/components/KarplanStatusCard.js +7 -2
  6. package/dist-lib/components/StandaardgegevensCard.d.ts.map +1 -1
  7. package/dist-lib/components/StandaardgegevensCard.js +67 -4
  8. package/dist-lib/components/TaakGezondheidCard.d.ts.map +1 -1
  9. package/dist-lib/components/TaakGezondheidCard.js +5 -3
  10. package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts +2 -0
  11. package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts.map +1 -1
  12. package/dist-lib/hooks/useGeblokkeerdeOrders.js +13 -0
  13. package/dist-lib/integrations/supabase/types.d.ts +750 -0
  14. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  15. package/dist-lib/lib/engineStatusTeksten.d.ts +7 -0
  16. package/dist-lib/lib/engineStatusTeksten.d.ts.map +1 -0
  17. package/dist-lib/lib/engineStatusTeksten.js +22 -0
  18. package/dist-lib/lib/flowTree.js +3 -3
  19. package/dist-lib/lib/modulePrompts.d.ts.map +1 -1
  20. package/dist-lib/lib/modulePrompts.js +44 -0
  21. package/dist-lib/lib/moduleVersion.d.ts +1 -1
  22. package/dist-lib/lib/moduleVersion.js +1 -1
  23. package/dist-lib/lib/ophaalmomentTeksten.d.ts +9 -0
  24. package/dist-lib/lib/ophaalmomentTeksten.d.ts.map +1 -0
  25. package/dist-lib/lib/ophaalmomentTeksten.js +29 -0
  26. package/dist-lib/lib/ruleEngineSchema.d.ts.map +1 -1
  27. package/dist-lib/lib/ruleEngineSchema.js +27 -2
  28. package/dist-lib/lib/taakTeLaat.d.ts +7 -0
  29. package/dist-lib/lib/taakTeLaat.d.ts.map +1 -0
  30. package/dist-lib/lib/taakTeLaat.js +62 -0
  31. package/package.json +1 -1
@@ -42169,3 +42169,2100 @@ BEGIN
42169
42169
  RETURN NEW;
42170
42170
  END;
42171
42171
  $function$;
42172
+
42173
+ -- Productiecheck 2026-11-09: versiemarkering, samenvoeggroepen met vaste karren
42174
+ CREATE OR REPLACE FUNCTION public.logistics_schema_versie()
42175
+ RETURNS text LANGUAGE sql IMMUTABLE SET search_path TO 'public' AS $$ SELECT '2026-11-09'::text $$;
42176
+ REVOKE ALL ON FUNCTION public.logistics_schema_versie() FROM PUBLIC, anon;
42177
+ GRANT EXECUTE ON FUNCTION public.logistics_schema_versie() TO authenticated, service_role;
42178
+
42179
+ CREATE OR REPLACE FUNCTION public._logistics_order_karren_vast(p_order_id uuid)
42180
+ RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
42181
+ SELECT EXISTS (SELECT 1 FROM logistics_active.order_carts oc
42182
+ WHERE oc.order_id = p_order_id
42183
+ AND (public.logistics_cart_is_frozen(oc.id)
42184
+ OR EXISTS (SELECT 1 FROM public.logistics_control_scan s WHERE s.cart_id = oc.id AND s.status <> 'geannuleerd')
42185
+ OR EXISTS (SELECT 1 FROM public.logistics_slip_shipping_note n
42186
+ WHERE n.cart_id = oc.id AND (n.status IN ('in_printwachtrij','geprint') OR n.fulfillment_order_id IS NOT NULL))))
42187
+ $$;
42188
+ REVOKE ALL ON FUNCTION public._logistics_order_karren_vast(uuid) FROM PUBLIC, anon, authenticated;
42189
+ GRANT EXECUTE ON FUNCTION public._logistics_order_karren_vast(uuid) TO service_role;
42190
+
42191
+ CREATE OR REPLACE FUNCTION public._logistics_buffers_vervallen_herstel(p_order_id uuid)
42192
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
42193
+ DECLARE r record; v_groepen uuid[] := '{}'; v_orders int := 0; v_vast int := 0; v_oud text; v_stand jsonb;
42194
+ BEGIN
42195
+ v_oud := current_setting('logistics.buffer_writer', true);
42196
+ FOR r IN
42197
+ SELECT o.id, o.order_number, o.buffer_id,
42198
+ CASE WHEN o.deleted_at IS NOT NULL THEN 'verwijderd' ELSE 'geannuleerd' END AS reden
42199
+ FROM public.orders o
42200
+ WHERE o.buffer_id IS NOT NULL AND (p_order_id IS NULL OR o.id = p_order_id)
42201
+ AND public.logistics_order_vervallen(o.id)
42202
+ ORDER BY o.order_number
42203
+ FOR UPDATE OF o
42204
+ LOOP
42205
+ IF public._logistics_order_karren_vast(r.id) THEN
42206
+ v_vast := v_vast + 1;
42207
+ CONTINUE;
42208
+ END IF;
42209
+ PERFORM set_config('logistics.buffer_writer', 'engine', true);
42210
+ UPDATE public.orders SET buffer_id = NULL WHERE id = r.id AND buffer_id = r.buffer_id;
42211
+ INSERT INTO public.logistics_correctie_log (correctie, object_id, omschrijving, oud, nieuw)
42212
+ VALUES ('buffers_geannuleerd', r.id, coalesce(r.order_number, r.id::text) || ' (' || r.reden || ') losgemaakt uit samenvoeggroep',
42213
+ jsonb_build_object('buffer_id', r.buffer_id), jsonb_build_object('buffer_id', NULL));
42214
+ v_orders := v_orders + 1;
42215
+ IF NOT r.buffer_id = ANY (v_groepen) THEN v_groepen := v_groepen || r.buffer_id; END IF;
42216
+ END LOOP;
42217
+ PERFORM set_config('logistics.buffer_writer', coalesce(v_oud, ''), true);
42218
+
42219
+ IF cardinality(v_groepen) > 0 THEN
42220
+ FOR r IN SELECT x.id, x.order_number FROM public.orders x
42221
+ WHERE x.buffer_id = ANY (v_groepen) AND x.deleted_at IS NULL AND x.cancelled_at IS NULL LOOP
42222
+ BEGIN
42223
+ PERFORM public.logistics_plan_enqueue(r.id, 'correctie_buffers_geannuleerd');
42224
+ EXCEPTION WHEN others THEN
42225
+ INSERT INTO public.logistics_correctie_log (correctie, object_id, omschrijving)
42226
+ VALUES ('buffers_geannuleerd_fout', r.id, 'Opnieuw plannen van ' || coalesce(r.order_number, r.id::text) || ' mislukt: ' || SQLERRM);
42227
+ END;
42228
+ END LOOP;
42229
+ END IF;
42230
+ IF p_order_id IS NULL OR cardinality(v_groepen) > 0 THEN
42231
+ SELECT value::jsonb INTO v_stand FROM public.logistics_plan_settings WHERE key = 'buffers_geannuleerd_herstel';
42232
+ v_stand := jsonb_build_object(
42233
+ 'totaalGroepen', coalesce((v_stand->>'totaalGroepen')::int, 0) + cardinality(v_groepen),
42234
+ 'totaalOrders', coalesce((v_stand->>'totaalOrders')::int, 0) + v_orders,
42235
+ 'laatsteGroepen', cardinality(v_groepen), 'laatsteOrders', v_orders,
42236
+ 'laatsteOp', CASE WHEN cardinality(v_groepen) > 0 THEN to_jsonb(now()) ELSE coalesce(v_stand->'laatsteOp', 'null'::jsonb) END,
42237
+ 'gecontroleerdOp', now());
42238
+ INSERT INTO public.logistics_plan_settings (key, value) VALUES ('buffers_geannuleerd_herstel', v_stand::text)
42239
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
42240
+ END IF;
42241
+ RETURN jsonb_build_object('groepen', cardinality(v_groepen), 'orders', v_orders, 'blijvenStaan', v_vast);
42242
+ END $$;
42243
+ REVOKE ALL ON FUNCTION public._logistics_buffers_vervallen_herstel(uuid) FROM PUBLIC, anon, authenticated;
42244
+ GRANT EXECUTE ON FUNCTION public._logistics_buffers_vervallen_herstel(uuid) TO service_role;
42245
+
42246
+ CREATE OR REPLACE FUNCTION public.logistics_buffers_herstel_status()
42247
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
42248
+ DECLARE v_nog int; v_vast int;
42249
+ BEGIN
42250
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_buffers_herstel_status');
42251
+ SELECT count(*), count(*) FILTER (WHERE public._logistics_order_karren_vast(o.id))
42252
+ INTO v_nog, v_vast
42253
+ FROM public.orders o WHERE o.buffer_id IS NOT NULL AND public.logistics_order_vervallen(o.id);
42254
+ RETURN jsonb_build_object(
42255
+ 'stand', (SELECT value::jsonb FROM public.logistics_plan_settings WHERE key = 'buffers_geannuleerd_herstel'),
42256
+ 'nogInGroep', v_nog,
42257
+ 'blijvenStaan', v_vast,
42258
+ 'wachten', v_nog - v_vast,
42259
+ 'fouten', coalesce((SELECT jsonb_agg(jsonb_build_object('op', l.created_at, 'melding', l.omschrijving) ORDER BY l.created_at DESC)
42260
+ FROM (SELECT * FROM public.logistics_correctie_log WHERE correctie = 'buffers_geannuleerd_fout'
42261
+ AND created_at > now() - interval '7 days' ORDER BY created_at DESC LIMIT 5) l), '[]'::jsonb));
42262
+ END $$;
42263
+ REVOKE ALL ON FUNCTION public.logistics_buffers_herstel_status() FROM PUBLIC, anon;
42264
+ GRANT EXECUTE ON FUNCTION public.logistics_buffers_herstel_status() TO authenticated, service_role;
42265
+ -- Ingehaalde orders (backfill) 2026-11-10
42266
+ -- Ingehaalde orders (custom_fields.backfill): nooit kar, reservering, wachtrij of melding.
42267
+ CREATE OR REPLACE FUNCTION public.logistics_order_is_backfill(p_order_id uuid)
42268
+ RETURNS boolean
42269
+ LANGUAGE sql
42270
+ STABLE
42271
+ SET search_path TO 'public'
42272
+ AS $function$
42273
+ SELECT COALESCE((
42274
+ SELECT CASE
42275
+ WHEN NOT (o.custom_fields ? 'backfill') THEN false
42276
+ WHEN jsonb_typeof(o.custom_fields->'backfill') = 'null' THEN false
42277
+ WHEN jsonb_typeof(o.custom_fields->'backfill') = 'boolean' THEN (o.custom_fields->>'backfill')::boolean
42278
+ ELSE lower(btrim(o.custom_fields->>'backfill')) NOT IN ('', 'false', '0', 'nee', 'no')
42279
+ END
42280
+ FROM public.orders o
42281
+ WHERE o.id = p_order_id AND jsonb_typeof(o.custom_fields) = 'object'), false);
42282
+ $function$;
42283
+ REVOKE ALL ON FUNCTION public.logistics_order_is_backfill(uuid) FROM PUBLIC;
42284
+ REVOKE ALL ON FUNCTION public.logistics_order_is_backfill(uuid) FROM anon;
42285
+ GRANT EXECUTE ON FUNCTION public.logistics_order_is_backfill(uuid) TO authenticated, service_role;
42286
+
42287
+
42288
+ CREATE OR REPLACE FUNCTION public.logistics_plan_enqueue(p_order_id uuid, p_reason text DEFAULT 'change'::text)
42289
+ RETURNS void
42290
+ LANGUAGE plpgsql
42291
+ SECURITY DEFINER
42292
+ SET search_path TO 'public'
42293
+ AS $function$
42294
+ DECLARE
42295
+ v_ok boolean;
42296
+ v_id uuid;
42297
+ v_status text;
42298
+ v_decision text;
42299
+ v_target uuid := p_order_id;
42300
+ v_wacht jsonb;
42301
+ BEGIN
42302
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_plan_enqueue');
42303
+ IF COALESCE(current_setting('logistics.merging', true), '') <> '1' THEN
42304
+ BEGIN
42305
+ PERFORM set_config('logistics.merging', '1', true);
42306
+ v_target := COALESCE(public.logistics_merge_floriday_lines(p_order_id), p_order_id);
42307
+ PERFORM set_config('logistics.merging', '0', true);
42308
+ EXCEPTION WHEN OTHERS THEN
42309
+ v_target := p_order_id;
42310
+ END;
42311
+ END IF;
42312
+
42313
+ IF public.logistics_order_is_backfill(v_target) THEN
42314
+ BEGIN
42315
+ PERFORM public.logistics_trace_order_step(
42316
+ v_target, 'stap:orders_combineren', 'order aanmelden voor indeling', 'overgeslagen',
42317
+ 'niet aangemeld', 'Ingehaalde order: krijgt geen kar, reservering of documenten', NULL);
42318
+ EXCEPTION WHEN OTHERS THEN
42319
+ RAISE WARNING 'logistiek-engine: trace ingehaalde order mislukt: %', SQLERRM;
42320
+ END;
42321
+ RETURN;
42322
+ END IF;
42323
+ -- A2: wachttijd na een mislukte indeling respecteren bij automatische meldingen.
42324
+ v_wacht := public.logistics_plan_wachttijd(v_target, p_reason);
42325
+ IF COALESCE((v_wacht->>'wacht')::boolean, false) THEN
42326
+ PERFORM public.logistics_plan_wachttijd_melden(v_target, p_reason, v_wacht);
42327
+ RETURN;
42328
+ END IF;
42329
+
42330
+ SELECT (o.deleted_at IS NULL AND COALESCE(o.is_reservation, false) = false AND o.plan_locked_at IS NULL)
42331
+ INTO v_ok FROM public.orders o WHERE o.id = v_target;
42332
+ -- 1.0.194: geannuleerde orders niet opnieuw indelen (alleen als de kolommen bestaan).
42333
+ IF COALESCE(v_ok, false) AND public.logistics_order_geannuleerd(v_target) THEN
42334
+ PERFORM public.logistics_trace_order_step(
42335
+ v_target, 'stap:orders_combineren', 'order aanmelden voor indeling', 'overgeslagen',
42336
+ 'niet aangemeld', 'Order is geannuleerd', NULL);
42337
+ RETURN;
42338
+ END IF;
42339
+ IF NOT COALESCE(v_ok, false) THEN
42340
+ PERFORM public.logistics_trace_order_step(
42341
+ v_target, 'stap:orders_combineren', 'order aanmelden voor indeling', 'overgeslagen',
42342
+ 'niet aangemeld', 'Order is verwijderd, samengevoegd, een reservering of de indeling staat vast', NULL);
42343
+ RETURN;
42344
+ END IF;
42345
+
42346
+ SELECT q.id, q.status INTO v_id, v_status
42347
+ FROM public.logistics_plan_queue q
42348
+ WHERE q.order_id = v_target
42349
+ AND q.status IN ('pending', 'running', 'geblokkeerd')
42350
+ ORDER BY q.updated_at DESC
42351
+ LIMIT 1
42352
+ FOR UPDATE;
42353
+
42354
+ IF v_id IS NULL THEN
42355
+ BEGIN
42356
+ INSERT INTO public.logistics_plan_queue(order_id, reason) VALUES (v_target, p_reason);
42357
+ v_decision := 'nieuw aangemeld voor indeling';
42358
+ EXCEPTION WHEN unique_violation THEN
42359
+ v_decision := 'stond al in de wachtrij';
42360
+ END;
42361
+ ELSIF v_status = 'geblokkeerd' THEN
42362
+ UPDATE public.logistics_plan_queue
42363
+ SET status = 'pending', attempts = 0, reason = p_reason, reason_code = NULL,
42364
+ error = NULL, claimed_at = NULL, next_attempt_at = now(), updated_at = now()
42365
+ WHERE id = v_id;
42366
+ v_decision := 'blokkade opgeheven, opnieuw aangemeld';
42367
+ ELSE
42368
+ UPDATE public.logistics_plan_queue
42369
+ SET reason = p_reason, next_attempt_at = LEAST(next_attempt_at, now()), updated_at = now()
42370
+ WHERE id = v_id;
42371
+ v_decision := 'opnieuw aangemeld';
42372
+ END IF;
42373
+
42374
+ PERFORM public.logistics_trace_order_step(
42375
+ v_target, 'stap:orders_combineren', 'order aanmelden voor indeling', 'geslaagd',
42376
+ v_decision, p_reason, 'Karrenindeling');
42377
+
42378
+ PERFORM public.logistics_plan_kick();
42379
+ END;
42380
+ $function$;
42381
+
42382
+ CREATE OR REPLACE FUNCTION public.logistics_order_changed(p_order_id uuid, p_reason text DEFAULT 'wijziging'::text)
42383
+ RETURNS void
42384
+ LANGUAGE plpgsql
42385
+ SECURITY DEFINER
42386
+ SET search_path TO 'public'
42387
+ AS $function$
42388
+ DECLARE v_number text; v_ok boolean; v_wacht jsonb; v_fp text;
42389
+ v_reason text := COALESCE(NULLIF(btrim(p_reason), ''), 'wijziging');
42390
+ BEGIN
42391
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_changed');
42392
+ IF p_order_id IS NULL THEN RETURN; END IF;
42393
+ IF COALESCE(current_setting('logistics.replanning', true), '') = '1' THEN RETURN; END IF;
42394
+ IF public.logistics_order_is_backfill(p_order_id) THEN
42395
+ BEGIN
42396
+ PERFORM public.logistics_trace_order_step(
42397
+ p_order_id, 'stap:orders_combineren', 'order aanmelden voor indeling', 'overgeslagen',
42398
+ 'niet aangemeld', 'Ingehaalde order: krijgt geen kar, reservering of documenten', NULL);
42399
+ EXCEPTION WHEN OTHERS THEN
42400
+ RAISE WARNING 'logistiek-engine: trace ingehaalde order mislukt: %', SQLERRM;
42401
+ END;
42402
+ RETURN;
42403
+ END IF;
42404
+ v_fp := public.logistics_order_fp(p_order_id);
42405
+ IF v_reason = 'order_gewijzigd' THEN
42406
+ IF EXISTS (SELECT 1 FROM public.logistics_order_change_fp f WHERE f.order_id = p_order_id AND f.fp = v_fp) THEN RETURN; END IF;
42407
+ INSERT INTO public.logistics_order_change_fp(order_id, fp, updated_at) VALUES (p_order_id, v_fp, now())
42408
+ ON CONFLICT (order_id) DO UPDATE SET fp = EXCLUDED.fp, updated_at = now();
42409
+ END IF;
42410
+ IF public.logistics_order_afgemeld(p_order_id)
42411
+ OR EXISTS (SELECT 1 FROM public.logistics_shared_cart_peers(p_order_id) p
42412
+ WHERE public.logistics_order_afgemeld(p.peer_order_id)) THEN
42413
+ IF EXISTS (SELECT 1 FROM public.logistics_order_change_fp f WHERE f.order_id = p_order_id AND f.frozen_fp = v_fp) THEN RETURN; END IF;
42414
+ INSERT INTO public.logistics_order_change_fp(order_id, frozen_fp, updated_at) VALUES (p_order_id, v_fp, now())
42415
+ ON CONFLICT (order_id) DO UPDATE SET frozen_fp = EXCLUDED.frozen_fp, updated_at = now();
42416
+ BEGIN
42417
+ PERFORM public.logistics_trace_order_step(p_order_id, 'stap:orders_combineren', 'wijziging na afmelding', 'overgeslagen',
42418
+ 'niet opnieuw ingedeeld', 'order al afgemeld; kar bevroren (' || v_reason || ')', 'Karrenindeling');
42419
+ EXCEPTION WHEN OTHERS THEN NULL; END;
42420
+ RETURN;
42421
+ END IF;
42422
+ v_wacht := public.logistics_plan_wachttijd(p_order_id, v_reason);
42423
+ IF COALESCE((v_wacht->>'wacht')::boolean, false) THEN
42424
+ PERFORM public.logistics_plan_wachttijd_melden(p_order_id, v_reason, v_wacht);
42425
+ RETURN;
42426
+ END IF;
42427
+ SELECT o.order_number, (o.deleted_at IS NULL AND COALESCE(o.is_reservation, false) = false AND o.plan_locked_at IS NULL)
42428
+ INTO v_number, v_ok FROM public.orders o WHERE o.id = p_order_id;
42429
+ IF v_number IS NULL OR NOT COALESCE(v_ok, false) THEN
42430
+ PERFORM public.logistics_plan_enqueue(p_order_id, v_reason); RETURN;
42431
+ END IF;
42432
+ IF public.logistics_order_frozen(p_order_id) THEN
42433
+ BEGIN
42434
+ PERFORM public.logistics_trace_order_step(p_order_id, 'stap:orders_combineren', 'wijziging uit een andere module', 'waarschuwing',
42435
+ 'niet opnieuw ingedeeld',
42436
+ 'De order is gewijzigd (' || v_reason || '), maar er zijn al geprinte, gescande of gecontroleerde karren. Die blijven staan.',
42437
+ 'Karrenindeling');
42438
+ EXCEPTION WHEN OTHERS THEN NULL; END;
42439
+ PERFORM public.logistics_plan_enqueue(p_order_id, v_reason); RETURN;
42440
+ END IF;
42441
+ BEGIN
42442
+ PERFORM set_config('logistics.replanning', '1', true);
42443
+ PERFORM public.logistics_replan_order_journey(v_number, v_reason);
42444
+ PERFORM set_config('logistics.replanning', '0', true);
42445
+ EXCEPTION WHEN OTHERS THEN
42446
+ PERFORM set_config('logistics.replanning', '0', true);
42447
+ PERFORM public.logistics_plan_enqueue(p_order_id, v_reason);
42448
+ END;
42449
+ END;
42450
+ $function$;
42451
+
42452
+ CREATE OR REPLACE FUNCTION public.logistics_reserved_order_excluded(p_order_id uuid)
42453
+ RETURNS boolean
42454
+ LANGUAGE plpgsql
42455
+ STABLE SECURITY DEFINER
42456
+ SET search_path TO 'public'
42457
+ AS $function$
42458
+ DECLARE r record; a jsonb;
42459
+ BEGIN
42460
+ PERFORM public.logistics_acc_guard('server', 'logistics_reserved_order_excluded');
42461
+ SELECT o.deleted_at, o.cancelled_at, o.status INTO r FROM public.orders o WHERE o.id = p_order_id;
42462
+ IF NOT FOUND THEN RETURN false; END IF;
42463
+ IF public.logistics_order_is_backfill(p_order_id) THEN RETURN true; END IF;
42464
+ IF r.deleted_at IS NOT NULL OR r.cancelled_at IS NOT NULL
42465
+ OR lower(coalesce(r.status, '')) IN ('geannuleerd', 'cancelled') THEN
42466
+ RETURN true;
42467
+ END IF;
42468
+ BEGIN
42469
+ a := public.logistics_order_is_archived(p_order_id);
42470
+ RETURN coalesce((a->>'gearchiveerd')::boolean, false);
42471
+ EXCEPTION WHEN OTHERS THEN
42472
+ RETURN false; -- leesfout: order blijft meetellen (fail closed)
42473
+ END;
42474
+ END $function$;
42475
+
42476
+ CREATE OR REPLACE FUNCTION public.logistics_order_stock_trigger()
42477
+ RETURNS trigger
42478
+ LANGUAGE plpgsql
42479
+ SECURITY DEFINER
42480
+ SET search_path TO 'public'
42481
+ AS $function$
42482
+ DECLARE
42483
+ oid uuid := COALESCE(NEW.order_id, OLD.order_id);
42484
+ BEGIN
42485
+ IF public.logistics_order_is_backfill(oid) THEN RETURN NULL; END IF;
42486
+ BEGIN
42487
+ PERFORM public.logistics_scan_order_stock(oid);
42488
+ EXCEPTION WHEN OTHERS THEN
42489
+ NULL;
42490
+ END;
42491
+ RETURN NULL;
42492
+ END;
42493
+ $function$;
42494
+
42495
+ CREATE OR REPLACE FUNCTION public.logistics_open_orders_zonder_kar()
42496
+ RETURNS TABLE(order_id uuid, order_number text, delivery_date text, laatste_reden text)
42497
+ LANGUAGE sql
42498
+ STABLE SECURITY DEFINER
42499
+ SET search_path TO 'public'
42500
+ AS $function$
42501
+ SELECT public.logistics_acc_guard('ingelogd', 'logistics_open_orders_zonder_kar');
42502
+ WITH lq AS (
42503
+ SELECT DISTINCT ON (q.order_id) q.order_id, q.status, q.reason_code, q.error
42504
+ FROM public.logistics_plan_queue q ORDER BY q.order_id, q.updated_at DESC
42505
+ )
42506
+ SELECT o.id, o.order_number, o.delivery_date, lq.error
42507
+ FROM public.orders o
42508
+ LEFT JOIN lq ON lq.order_id = o.id
42509
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
42510
+ AND lower(coalesce(o.status,'')) NOT IN ('cancelled','geannuleerd','deleted','verwijderd','afgehandeld','completed','delivered','geleverd')
42511
+ AND o.delivery_date ~ '^\d{4}-\d{2}-\d{2}'
42512
+ AND (left(o.delivery_date,10)::date >= public.logistics_local_today()
42513
+ OR (left(o.delivery_date,10)::date >= public.logistics_local_today() - 7 AND o.updated_at > now() - interval '24 hours'))
42514
+ AND EXISTS (SELECT 1 FROM public.order_items oi WHERE oi.order_id = o.id)
42515
+ AND NOT public.logistics_order_is_backfill(o.id)
42516
+ AND NOT coalesce(public.logistics_order_buffer_excluded(o.id), false)
42517
+ AND NOT (coalesce(lq.status,'') IN ('geblokkeerd','pending','running'))
42518
+ AND NOT (coalesce(lq.status,'') = 'gearchiveerd'
42519
+ AND coalesce(lq.reason_code,'') IN ('belading_onwaarschijnlijk','belading_onvolledig','belading_onbekend','artikel_kiezen','ladingdrager_onbekend'))
42520
+ AND NOT EXISTS (SELECT 1 FROM logistics_active.order_carts c WHERE c.order_id = o.id)
42521
+ AND NOT EXISTS (SELECT 1 FROM public.order_items oi
42522
+ JOIN logistics_active.order_cart_items ci ON ci.order_item_id = oi.id
42523
+ JOIN logistics_active.order_carts c ON c.id = ci.cart_id
42524
+ WHERE oi.order_id = o.id);
42525
+ $function$;
42526
+
42527
+ CREATE OR REPLACE FUNCTION public.logistics_cart_plan_overview(p_days integer DEFAULT 2)
42528
+ RETURNS TABLE(order_ref text, delivery_date date, plan_ready boolean, cart_total integer, released boolean, reason text)
42529
+ LANGUAGE plpgsql
42530
+ STABLE SECURITY DEFINER
42531
+ SET search_path TO 'public'
42532
+ AS $function$
42533
+ DECLARE
42534
+ v_days integer := GREATEST(COALESCE(p_days, 2), 1);
42535
+ r record;
42536
+ v_status jsonb;
42537
+ BEGIN
42538
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_cart_plan_overview');
42539
+ FOR r IN
42540
+ SELECT o.order_number, public.logistics_order_date(o.delivery_date::text) AS ddate
42541
+ FROM public.orders o
42542
+ WHERE o.deleted_at IS NULL
42543
+ AND NOT public.logistics_order_is_backfill(o.id)
42544
+ AND public.logistics_order_date(o.delivery_date::text) >= (now() AT TIME ZONE coalesce(public.logistics_tz(), 'Europe/Amsterdam'))::date
42545
+ AND public.logistics_order_date(o.delivery_date::text) < (now() AT TIME ZONE coalesce(public.logistics_tz(), 'Europe/Amsterdam'))::date + v_days
42546
+ ORDER BY 2, 1
42547
+ LOOP
42548
+ v_status := public.logistics_cart_plan_status(r.order_number);
42549
+ order_ref := r.order_number;
42550
+ delivery_date := r.ddate;
42551
+ plan_ready := COALESCE((v_status->>'ready')::boolean, false);
42552
+ cart_total := COALESCE((v_status->>'cartTotal')::integer, 0);
42553
+ released := COALESCE((v_status->>'released')::boolean, false);
42554
+ reason := v_status->>'reason';
42555
+ RETURN NEXT;
42556
+ END LOOP;
42557
+ END;
42558
+ $function$;
42559
+
42560
+ CREATE OR REPLACE FUNCTION public.logistics_schema_versie()
42561
+ RETURNS text LANGUAGE sql IMMUTABLE SET search_path TO 'public'
42562
+ AS $function$ SELECT '2026-11-10'::text $function$;
42563
+ -- ==== migratie 20261110090236
42564
+
42565
+ CREATE TABLE IF NOT EXISTS public.logistics_merge_meldingen (
42566
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
42567
+ soort text NOT NULL,
42568
+ order_id uuid,
42569
+ group_key text,
42570
+ aantal integer,
42571
+ fout text,
42572
+ created_at timestamptz NOT NULL DEFAULT now()
42573
+ );
42574
+ CREATE INDEX IF NOT EXISTS logistics_merge_meldingen_created_idx ON public.logistics_merge_meldingen (created_at DESC);
42575
+ GRANT ALL ON public.logistics_merge_meldingen TO service_role;
42576
+ ALTER TABLE public.logistics_merge_meldingen ENABLE ROW LEVEL SECURITY;
42577
+
42578
+ CREATE TABLE IF NOT EXISTS public.logistics_merge_herstel_log (
42579
+ merged_order_id uuid PRIMARY KEY,
42580
+ kept_order_id uuid,
42581
+ categorie text NOT NULL,
42582
+ regels_terug integer NOT NULL DEFAULT 0,
42583
+ hersteld_at timestamptz NOT NULL DEFAULT now(),
42584
+ details jsonb
42585
+ );
42586
+ GRANT ALL ON public.logistics_merge_herstel_log TO service_role;
42587
+ ALTER TABLE public.logistics_merge_herstel_log ENABLE ROW LEVEL SECURITY;
42588
+
42589
+ CREATE OR REPLACE FUNCTION public._logistics_merge_melding(p_soort text, p_order_id uuid, p_key text, p_aantal integer, p_fout text)
42590
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
42591
+ BEGIN
42592
+ INSERT INTO public.logistics_merge_meldingen(soort, order_id, group_key, aantal, fout)
42593
+ VALUES (p_soort, p_order_id, p_key, p_aantal, left(p_fout, 2000));
42594
+ DELETE FROM public.logistics_merge_meldingen WHERE created_at < now() - interval '90 days';
42595
+ EXCEPTION WHEN OTHERS THEN
42596
+ RAISE WARNING 'logistiek-engine: samenvoegmelding vastleggen mislukt (%): %', p_soort, SQLERRM;
42597
+ END $$;
42598
+ REVOKE ALL ON FUNCTION public._logistics_merge_melding(text, uuid, text, integer, text) FROM PUBLIC, anon, authenticated;
42599
+ GRANT EXECUTE ON FUNCTION public._logistics_merge_melding(text, uuid, text, integer, text) TO service_role;
42600
+
42601
+ CREATE OR REPLACE FUNCTION public.logistics_order_merge_key_v2(p_order_id uuid)
42602
+ RETURNS text LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
42603
+ DECLARE o record; v_dag date; v_loc text; v_pick text; v_ts timestamptz;
42604
+ BEGIN
42605
+ SELECT id, customer_id, delivery_date, custom_fields INTO o FROM public.orders WHERE id = p_order_id;
42606
+ IF NOT FOUND OR o.customer_id IS NULL OR o.delivery_date IS NULL THEN RETURN NULL; END IF;
42607
+ BEGIN
42608
+ v_dag := public.logistics_order_date(o.delivery_date::text);
42609
+ EXCEPTION WHEN OTHERS THEN
42610
+ v_dag := NULL;
42611
+ END;
42612
+ IF v_dag IS NULL THEN RETURN NULL; END IF;
42613
+ IF jsonb_typeof(o.custom_fields) IS DISTINCT FROM 'object' THEN RETURN NULL; END IF;
42614
+ v_loc := nullif(btrim(coalesce(o.custom_fields->>'delivery_location_gln', o.custom_fields->>'delivery_location_id',
42615
+ o.custom_fields->>'delivery_location', '')), '');
42616
+ v_pick := nullif(btrim(coalesce(o.custom_fields->>'pickup_moment', o.custom_fields->>'pickup_datetime', '')), '');
42617
+ IF v_loc IS NULL OR v_pick IS NULL THEN RETURN NULL; END IF;
42618
+ BEGIN
42619
+ v_ts := public.logistics_pickup_ts(v_pick, v_dag);
42620
+ EXCEPTION WHEN OTHERS THEN
42621
+ v_ts := NULL;
42622
+ END;
42623
+ RETURN concat_ws('|', 'v2', o.customer_id::text, v_dag::text, lower(v_loc), coalesce(v_ts::text, lower(v_pick)));
42624
+ END $$;
42625
+ REVOKE ALL ON FUNCTION public.logistics_order_merge_key_v2(uuid) FROM PUBLIC, anon;
42626
+ GRANT EXECUTE ON FUNCTION public.logistics_order_merge_key_v2(uuid) TO authenticated, service_role;
42627
+
42628
+ CREATE OR REPLACE FUNCTION public.logistics_order_mergeable(p_order_id uuid)
42629
+ RETURNS boolean LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
42630
+ DECLARE v_ok boolean; v_dag date; v_dd text;
42631
+ BEGIN
42632
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_mergeable');
42633
+ SELECT o.delivery_date INTO v_dd FROM public.orders o WHERE o.id = p_order_id;
42634
+ IF NOT FOUND OR v_dd IS NULL THEN RETURN false; END IF;
42635
+ BEGIN
42636
+ v_dag := public.logistics_order_date(v_dd);
42637
+ EXCEPTION WHEN OTHERS THEN
42638
+ RETURN false;
42639
+ END;
42640
+ IF v_dag IS NULL OR v_dag < (now() AT TIME ZONE 'Europe/Amsterdam')::date THEN RETURN false; END IF;
42641
+ SELECT (o.deleted_at IS NULL
42642
+ AND o.cancelled_at IS NULL
42643
+ AND o.plan_locked_at IS NULL
42644
+ AND COALESCE(o.is_reservation, false) = false
42645
+ AND o.order_source = 'floriday'
42646
+ AND lower(btrim(COALESCE(o.status, ''))) NOT IN
42647
+ ('verzonden', 'afgehandeld', 'gereed voor facturatie', 'gepickt', 'geannuleerd',
42648
+ 'cancelled', 'canceled', 'annulering', 'gereserveerd')
42649
+ AND NOT public.logistics_order_is_backfill(o.id)
42650
+ AND NOT EXISTS (SELECT 1 FROM logistics_active.order_carts oc WHERE oc.order_id = o.id)
42651
+ AND NOT EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.order_number = o.order_number)
42652
+ AND NOT public.logistics_order_print_actief(o.order_number))
42653
+ INTO v_ok
42654
+ FROM public.orders o
42655
+ WHERE o.id = p_order_id;
42656
+ RETURN COALESCE(v_ok, false);
42657
+ EXCEPTION WHEN OTHERS THEN
42658
+ PERFORM public._logistics_merge_melding('fout', p_order_id, NULL, NULL, 'controle samenvoegen: ' || SQLERRM);
42659
+ RETURN false;
42660
+ END $$;
42661
+
42662
+ CREATE OR REPLACE FUNCTION public.logistics_merge_floriday_lines(p_order_id uuid)
42663
+ RETURNS uuid LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
42664
+ DECLARE
42665
+ v_key text; v_keep uuid; v_keepnr text; v_cust uuid; r record;
42666
+ v_moved int; v_logged int; v_total int := 0; v_count int := 0; v_groep int;
42667
+ v_lines text[] := ARRAY[]::text[];
42668
+ BEGIN
42669
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_merge_floriday_lines');
42670
+ IF p_order_id IS NULL THEN RETURN p_order_id; END IF;
42671
+ IF COALESCE(current_setting('logistics.merge_uit', true), '') = '1' THEN RETURN p_order_id; END IF;
42672
+ IF NOT public.logistics_merge_lines_enabled() THEN RETURN p_order_id; END IF;
42673
+ IF NOT public.logistics_order_mergeable(p_order_id) THEN RETURN p_order_id; END IF;
42674
+ IF NOT EXISTS (SELECT 1 FROM logistics_active.order_items i WHERE i.order_id = p_order_id) THEN RETURN p_order_id; END IF;
42675
+
42676
+ v_key := public.logistics_order_merge_key_v2(p_order_id);
42677
+ IF v_key IS NULL THEN RETURN p_order_id; END IF;
42678
+ IF NOT pg_try_advisory_xact_lock(hashtext('logistics_merge:' || v_key)) THEN RETURN p_order_id; END IF;
42679
+ SELECT customer_id INTO v_cust FROM public.orders WHERE id = p_order_id;
42680
+
42681
+ CREATE TEMP TABLE IF NOT EXISTS _logistics_merge_groep (id uuid, order_number text, sales text, created_at timestamptz) ON COMMIT DROP;
42682
+ TRUNCATE _logistics_merge_groep;
42683
+ INSERT INTO _logistics_merge_groep
42684
+ SELECT o.id, o.order_number, o.floriday_sales_order_id, o.created_at
42685
+ FROM public.orders o
42686
+ WHERE o.customer_id = v_cust AND o.deleted_at IS NULL AND o.order_source = 'floriday'
42687
+ AND EXISTS (SELECT 1 FROM logistics_active.order_items i WHERE i.order_id = o.id)
42688
+ AND public.logistics_order_merge_key_v2(o.id) = v_key
42689
+ AND public.logistics_order_mergeable(o.id);
42690
+ SELECT count(*) INTO v_groep FROM _logistics_merge_groep;
42691
+ IF v_groep < 2 THEN RETURN p_order_id; END IF;
42692
+ IF v_groep > 20 THEN
42693
+ IF NOT EXISTS (SELECT 1 FROM public.logistics_merge_meldingen m
42694
+ WHERE m.soort = 'groep_te_groot' AND m.group_key = v_key AND m.created_at > now() - interval '1 day') THEN
42695
+ PERFORM public._logistics_merge_melding('groep_te_groot', p_order_id, v_key, v_groep,
42696
+ v_groep || ' orders met dezelfde klant, leverdag, afleverlocatie en ophaalmoment; niet samengevoegd.');
42697
+ END IF;
42698
+ RETURN p_order_id;
42699
+ END IF;
42700
+
42701
+ SELECT g.id, g.order_number INTO v_keep, v_keepnr FROM _logistics_merge_groep g
42702
+ ORDER BY g.order_number ASC NULLS LAST, g.created_at ASC LIMIT 1;
42703
+
42704
+ FOR r IN SELECT * FROM _logistics_merge_groep g WHERE g.id <> v_keep ORDER BY g.order_number ASC NULLS LAST LOOP
42705
+ BEGIN
42706
+ INSERT INTO public.logistics_order_merge_lines(
42707
+ group_key, from_order_id, from_order_number, to_order_id, to_order_number,
42708
+ order_item_id, product_id, product_name, quantity, source_line_key,
42709
+ sales_order_id, ordered_at, reason)
42710
+ SELECT v_key, r.id, r.order_number, v_keep, v_keepnr,
42711
+ i.id, i.product_id, i.product_name, i.quantity, i.source_line_key,
42712
+ r.sales, r.created_at, 'Zelfde klant, leverdag, afleverlocatie en ophaalmoment'
42713
+ FROM logistics_active.order_items i WHERE i.order_id = r.id;
42714
+ GET DIAGNOSTICS v_logged = ROW_COUNT;
42715
+ UPDATE public.order_items SET order_id = v_keep WHERE order_id = r.id AND quarantined_at IS NULL;
42716
+ GET DIAGNOSTICS v_moved = ROW_COUNT;
42717
+ IF v_moved <> v_logged THEN RAISE EXCEPTION 'aantal verplaatste regels (%) wijkt af van het logboek (%)', v_moved, v_logged; END IF;
42718
+ IF v_moved = 0 THEN RAISE EXCEPTION 'geen regels verplaatst; order blijft staan'; END IF;
42719
+ INSERT INTO public.logistics_order_merges(group_key, kept_order_id, kept_order_number,
42720
+ merged_order_id, merged_order_number, sales_order_id, items_moved)
42721
+ VALUES (v_key, v_keep, v_keepnr, r.id, r.order_number, r.sales, v_moved)
42722
+ ON CONFLICT (merged_order_id) DO NOTHING;
42723
+ UPDATE public.orders SET deleted_at = now(),
42724
+ notes = COALESCE(NULLIF(notes, ''), '')
42725
+ || CASE WHEN COALESCE(notes, '') = '' THEN '' ELSE E'\n' END
42726
+ || 'Samengevoegd met ' || COALESCE(v_keepnr, '') || '.',
42727
+ updated_at = now()
42728
+ WHERE id = r.id;
42729
+ v_total := v_total + v_moved; v_count := v_count + 1;
42730
+ v_lines := v_lines || (COALESCE(r.order_number, '') || ': ' || v_moved::text || ' regel(s)');
42731
+ EXCEPTION WHEN OTHERS THEN
42732
+ PERFORM public._logistics_merge_melding('fout', r.id, v_key, NULL,
42733
+ 'samenvoegen ' || COALESCE(r.order_number, '?') || ' met ' || COALESCE(v_keepnr, '?') || ': ' || SQLERRM);
42734
+ END;
42735
+ END LOOP;
42736
+
42737
+ IF v_count > 0 THEN
42738
+ UPDATE public.orders o SET total = to_char(COALESCE((
42739
+ SELECT SUM(COALESCE(i.quantity, 0) * COALESCE(i.unit_price, 0))
42740
+ FROM logistics_active.order_items i WHERE i.order_id = o.id), 0), 'FM9999999990.00'),
42741
+ updated_at = now()
42742
+ WHERE o.id = v_keep;
42743
+ PERFORM public.logistics_trace_order_step(
42744
+ v_keep, 'stap:bestelregels_samenvoegen', 'bestelregels samenvoegen', 'geslaagd',
42745
+ 'samengevoegd tot ' || COALESCE(v_keepnr, 'één bestelling'),
42746
+ v_count::text || ' order(s), ' || v_total::text || ' regel(s) verplaatst: ' || array_to_string(v_lines, '; '),
42747
+ 'Orders combineren');
42748
+ END IF;
42749
+ IF EXISTS (SELECT 1 FROM public.orders WHERE id = p_order_id AND deleted_at IS NOT NULL) THEN RETURN v_keep; END IF;
42750
+ RETURN p_order_id;
42751
+ EXCEPTION WHEN OTHERS THEN
42752
+ PERFORM public._logistics_merge_melding('fout', p_order_id, v_key, NULL, 'samenvoegen: ' || SQLERRM);
42753
+ RETURN p_order_id;
42754
+ END $$;
42755
+
42756
+ CREATE OR REPLACE FUNCTION public.logistics_plan_enqueue(p_order_id uuid, p_reason text DEFAULT 'change'::text)
42757
+ RETURNS void
42758
+ LANGUAGE plpgsql
42759
+ SECURITY DEFINER
42760
+ SET search_path TO 'public'
42761
+ AS $function$
42762
+ DECLARE
42763
+ v_ok boolean;
42764
+ v_id uuid;
42765
+ v_status text;
42766
+ v_decision text;
42767
+ v_target uuid := p_order_id;
42768
+ v_wacht jsonb;
42769
+ BEGIN
42770
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_plan_enqueue');
42771
+ IF COALESCE(current_setting('logistics.plan_uit', true), '') = '1' THEN RETURN; END IF;
42772
+ IF COALESCE(current_setting('logistics.merging', true), '') <> '1' THEN
42773
+ BEGIN
42774
+ PERFORM set_config('logistics.merging', '1', true);
42775
+ v_target := COALESCE(public.logistics_merge_floriday_lines(p_order_id), p_order_id);
42776
+ PERFORM set_config('logistics.merging', '0', true);
42777
+ EXCEPTION WHEN OTHERS THEN
42778
+ PERFORM public._logistics_merge_melding('fout', p_order_id, NULL, NULL, 'samenvoegen bij aanmelden: ' || SQLERRM);
42779
+ v_target := p_order_id;
42780
+ END;
42781
+ END IF;
42782
+
42783
+ IF public.logistics_order_is_backfill(v_target) THEN
42784
+ BEGIN
42785
+ PERFORM public.logistics_trace_order_step(
42786
+ v_target, 'stap:orders_combineren', 'order aanmelden voor indeling', 'overgeslagen',
42787
+ 'niet aangemeld', 'Ingehaalde order: krijgt geen kar, reservering of documenten', NULL);
42788
+ EXCEPTION WHEN OTHERS THEN
42789
+ RAISE WARNING 'logistiek-engine: trace ingehaalde order mislukt: %', SQLERRM;
42790
+ END;
42791
+ RETURN;
42792
+ END IF;
42793
+ v_wacht := public.logistics_plan_wachttijd(v_target, p_reason);
42794
+ IF COALESCE((v_wacht->>'wacht')::boolean, false) THEN
42795
+ PERFORM public.logistics_plan_wachttijd_melden(v_target, p_reason, v_wacht);
42796
+ RETURN;
42797
+ END IF;
42798
+
42799
+ SELECT (o.deleted_at IS NULL AND COALESCE(o.is_reservation, false) = false AND o.plan_locked_at IS NULL)
42800
+ INTO v_ok FROM public.orders o WHERE o.id = v_target;
42801
+ IF COALESCE(v_ok, false) AND public.logistics_order_geannuleerd(v_target) THEN
42802
+ PERFORM public.logistics_trace_order_step(
42803
+ v_target, 'stap:orders_combineren', 'order aanmelden voor indeling', 'overgeslagen',
42804
+ 'niet aangemeld', 'Order is geannuleerd', NULL);
42805
+ RETURN;
42806
+ END IF;
42807
+ IF NOT COALESCE(v_ok, false) THEN
42808
+ PERFORM public.logistics_trace_order_step(
42809
+ v_target, 'stap:orders_combineren', 'order aanmelden voor indeling', 'overgeslagen',
42810
+ 'niet aangemeld', 'Order is verwijderd, samengevoegd, een reservering of de indeling staat vast', NULL);
42811
+ RETURN;
42812
+ END IF;
42813
+
42814
+ SELECT q.id, q.status INTO v_id, v_status
42815
+ FROM public.logistics_plan_queue q
42816
+ WHERE q.order_id = v_target
42817
+ AND q.status IN ('pending', 'running', 'geblokkeerd')
42818
+ ORDER BY q.updated_at DESC
42819
+ LIMIT 1
42820
+ FOR UPDATE;
42821
+
42822
+ IF v_id IS NULL THEN
42823
+ BEGIN
42824
+ INSERT INTO public.logistics_plan_queue(order_id, reason) VALUES (v_target, p_reason);
42825
+ v_decision := 'nieuw aangemeld voor indeling';
42826
+ EXCEPTION WHEN unique_violation THEN
42827
+ v_decision := 'stond al in de wachtrij';
42828
+ END;
42829
+ ELSIF v_status = 'geblokkeerd' THEN
42830
+ UPDATE public.logistics_plan_queue
42831
+ SET status = 'pending', attempts = 0, reason = p_reason, reason_code = NULL,
42832
+ error = NULL, claimed_at = NULL, next_attempt_at = now(), updated_at = now()
42833
+ WHERE id = v_id;
42834
+ v_decision := 'blokkade opgeheven, opnieuw aangemeld';
42835
+ ELSE
42836
+ UPDATE public.logistics_plan_queue
42837
+ SET reason = p_reason, next_attempt_at = LEAST(next_attempt_at, now()), updated_at = now()
42838
+ WHERE id = v_id;
42839
+ v_decision := 'opnieuw aangemeld';
42840
+ END IF;
42841
+
42842
+ PERFORM public.logistics_trace_order_step(
42843
+ v_target, 'stap:orders_combineren', 'order aanmelden voor indeling', 'geslaagd',
42844
+ v_decision, p_reason, 'Karrenindeling');
42845
+
42846
+ PERFORM public.logistics_plan_kick();
42847
+ END;
42848
+ $function$;
42849
+
42850
+ CREATE OR REPLACE FUNCTION public._logistics_merge_herstel_kandidaten()
42851
+ RETURNS TABLE(merged_order_id uuid, merged_order_number text, kept_order_id uuid, kept_order_number text,
42852
+ categorie text, regels integer, status text, leverdag date, reden text)
42853
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
42854
+ DECLARE r record; v_dag date; v_onzeker text; v_vandaag date := (now() AT TIME ZONE 'Europe/Amsterdam')::date;
42855
+ BEGIN
42856
+ FOR r IN
42857
+ SELECT m.merged_order_id, m.merged_order_number, m.kept_order_id, m.kept_order_number, m.items_moved,
42858
+ o.status, o.delivery_date
42859
+ FROM public.logistics_order_merges m
42860
+ JOIN public.orders o ON o.id = m.merged_order_id
42861
+ WHERE o.deleted_at IS NOT NULL
42862
+ AND NOT EXISTS (SELECT 1 FROM public.logistics_merge_herstel_log h WHERE h.merged_order_id = m.merged_order_id)
42863
+ LOOP
42864
+ v_onzeker := NULL;
42865
+ BEGIN
42866
+ v_dag := public.logistics_order_date(r.delivery_date::text);
42867
+ EXCEPTION WHEN OTHERS THEN
42868
+ v_dag := NULL;
42869
+ END;
42870
+ IF v_dag IS NULL THEN v_onzeker := 'leverdag onleesbaar'; END IF;
42871
+ IF v_onzeker IS NULL AND (SELECT count(*) FROM public.logistics_order_merge_lines l WHERE l.from_order_id = r.merged_order_id)
42872
+ <> COALESCE(r.items_moved, 0) THEN
42873
+ v_onzeker := 'logboek en aantal verplaatste regels verschillen';
42874
+ END IF;
42875
+ IF v_onzeker IS NULL AND EXISTS (
42876
+ SELECT 1 FROM public.logistics_order_merge_lines l
42877
+ LEFT JOIN public.order_items i ON i.id = l.order_item_id
42878
+ WHERE l.from_order_id = r.merged_order_id
42879
+ AND (i.id IS NULL OR i.order_id IS DISTINCT FROM r.kept_order_id)) THEN
42880
+ v_onzeker := 'regel staat niet meer bij de verzamelorder';
42881
+ END IF;
42882
+ IF v_onzeker IS NULL AND EXISTS (
42883
+ SELECT 1 FROM public.logistics_order_merge_lines l
42884
+ WHERE l.from_order_id = r.merged_order_id
42885
+ AND EXISTS (SELECT 1 FROM public.logistics_order_merge_lines l2
42886
+ WHERE l2.order_item_id = l.order_item_id AND l2.from_order_id <> l.from_order_id)) THEN
42887
+ v_onzeker := 'regel komt bij meer dan één order vandaan';
42888
+ END IF;
42889
+ merged_order_id := r.merged_order_id; merged_order_number := r.merged_order_number;
42890
+ kept_order_id := r.kept_order_id; kept_order_number := r.kept_order_number;
42891
+ regels := COALESCE(r.items_moved, 0); status := r.status; leverdag := v_dag; reden := v_onzeker;
42892
+ categorie := CASE
42893
+ WHEN v_onzeker IS NOT NULL THEN 'onzeker'
42894
+ WHEN v_dag < v_vandaag THEN 'verleden'
42895
+ WHEN lower(btrim(COALESCE(r.status, ''))) = 'nog te picken' THEN 'urgent'
42896
+ ELSE 'toekomst_overig' END;
42897
+ RETURN NEXT;
42898
+ END LOOP;
42899
+ END $$;
42900
+ REVOKE ALL ON FUNCTION public._logistics_merge_herstel_kandidaten() FROM PUBLIC, anon, authenticated;
42901
+ GRANT EXECUTE ON FUNCTION public._logistics_merge_herstel_kandidaten() TO service_role;
42902
+
42903
+ CREATE OR REPLACE FUNCTION public.logistics_merge_herstel_voorbeeld()
42904
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
42905
+ DECLARE v jsonb;
42906
+ BEGIN
42907
+ PERFORM public.logistics_acc_guard('beheer', 'logistics_merge_herstel_voorbeeld');
42908
+ SELECT jsonb_object_agg(c.categorie, jsonb_build_object('orders', c.n, 'regelsTerug', c.regels,
42909
+ 'voorbeelden', to_jsonb(c.vb[1:10]))) INTO v
42910
+ FROM (SELECT k.categorie, count(*) n, COALESCE(sum(k.regels), 0) regels,
42911
+ array_agg(jsonb_build_object('order', k.merged_order_number, 'verzamelorder', k.kept_order_number,
42912
+ 'status', k.status, 'leverdag', k.leverdag, 'regels', k.regels, 'reden', k.reden)
42913
+ ORDER BY k.leverdag DESC NULLS LAST, k.merged_order_number) vb
42914
+ FROM public._logistics_merge_herstel_kandidaten() k GROUP BY k.categorie) c;
42915
+ RETURN jsonb_build_object(
42916
+ 'categorieen', COALESCE(v, '{}'::jsonb),
42917
+ 'alHersteld', (SELECT COALESCE(jsonb_object_agg(categorie, n), '{}'::jsonb)
42918
+ FROM (SELECT categorie, count(*) n FROM public.logistics_merge_herstel_log GROUP BY 1) h),
42919
+ 'volgorde', jsonb_build_array('urgent', 'toekomst_overig', 'verleden'),
42920
+ 'toelichting', 'onzeker wordt nooit automatisch hersteld');
42921
+ END $$;
42922
+ REVOKE ALL ON FUNCTION public.logistics_merge_herstel_voorbeeld() FROM PUBLIC, anon, authenticated;
42923
+ GRANT EXECUTE ON FUNCTION public.logistics_merge_herstel_voorbeeld() TO service_role;
42924
+
42925
+ CREATE OR REPLACE FUNCTION public.logistics_merge_herstel_uitvoeren(p_categorie text, p_max integer DEFAULT 200)
42926
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
42927
+ DECLARE
42928
+ k record; v_terug int; v_ok int := 0; v_regels int := 0; v_fouten jsonb := '[]'::jsonb;
42929
+ v_kept uuid[] := ARRAY[]::uuid[]; v_opgeruimd int := 0; v_rest int;
42930
+ v_notes_re text := E'\\n?Samengevoegd met [^\\n]*\\.';
42931
+ BEGIN
42932
+ PERFORM public.logistics_acc_guard('beheer', 'logistics_merge_herstel_uitvoeren');
42933
+ IF p_categorie IS NULL OR p_categorie NOT IN ('urgent', 'toekomst_overig', 'verleden') THEN
42934
+ RAISE EXCEPTION 'Onbekende categorie %; kies urgent, toekomst_overig of verleden.', p_categorie;
42935
+ END IF;
42936
+ PERFORM set_config('logistics.merge_uit', '1', true);
42937
+ PERFORM set_config('logistics.merging', '1', true);
42938
+
42939
+ FOR k IN SELECT * FROM public._logistics_merge_herstel_kandidaten() c
42940
+ WHERE c.categorie = p_categorie
42941
+ ORDER BY c.leverdag, c.merged_order_number
42942
+ LIMIT greatest(least(COALESCE(p_max, 200), 2000), 1)
42943
+ LOOP
42944
+ BEGIN
42945
+ PERFORM set_config('logistics.plan_uit', CASE WHEN p_categorie = 'urgent' THEN '0' ELSE '1' END, true);
42946
+ UPDATE public.orders o
42947
+ SET deleted_at = NULL,
42948
+ notes = NULLIF(btrim(regexp_replace(COALESCE(o.notes, ''), v_notes_re, '', 'g')), ''),
42949
+ custom_fields = CASE WHEN p_categorie = 'verleden'
42950
+ THEN (CASE WHEN jsonb_typeof(o.custom_fields) = 'object' THEN o.custom_fields ELSE '{}'::jsonb END)
42951
+ || jsonb_build_object('backfill', jsonb_build_object('categorie', 'herstel_samenvoegen', 'at', now()))
42952
+ ELSE o.custom_fields END,
42953
+ updated_at = now()
42954
+ WHERE o.id = k.merged_order_id AND o.deleted_at IS NOT NULL;
42955
+ IF NOT FOUND THEN RAISE EXCEPTION 'order is al actief'; END IF;
42956
+
42957
+ UPDATE public.order_items i SET order_id = k.merged_order_id
42958
+ FROM public.logistics_order_merge_lines l
42959
+ WHERE l.from_order_id = k.merged_order_id AND l.order_item_id = i.id AND i.order_id = k.kept_order_id;
42960
+ GET DIAGNOSTICS v_terug = ROW_COUNT;
42961
+ IF v_terug <> k.regels THEN
42962
+ RAISE EXCEPTION '% van % regels teruggezet; niets gewijzigd', v_terug, k.regels;
42963
+ END IF;
42964
+
42965
+ UPDATE public.orders o SET total = to_char(COALESCE((
42966
+ SELECT SUM(COALESCE(i.quantity, 0) * COALESCE(i.unit_price, 0))
42967
+ FROM logistics_active.order_items i WHERE i.order_id = o.id), 0), 'FM9999999990.00'),
42968
+ updated_at = now()
42969
+ WHERE o.id IN (k.merged_order_id, k.kept_order_id);
42970
+
42971
+ INSERT INTO public.logistics_merge_herstel_log(merged_order_id, kept_order_id, categorie, regels_terug, details)
42972
+ VALUES (k.merged_order_id, k.kept_order_id, p_categorie, v_terug,
42973
+ jsonb_build_object('order', k.merged_order_number, 'verzamelorder', k.kept_order_number, 'status', k.status));
42974
+
42975
+ IF p_categorie = 'urgent' THEN
42976
+ PERFORM public.logistics_plan_enqueue(k.merged_order_id, 'herstel_samenvoegen');
42977
+ END IF;
42978
+ PERFORM set_config('logistics.plan_uit', '0', true);
42979
+ v_ok := v_ok + 1; v_regels := v_regels + v_terug;
42980
+ IF NOT (k.kept_order_id = ANY (v_kept)) THEN v_kept := v_kept || k.kept_order_id; END IF;
42981
+ EXCEPTION WHEN OTHERS THEN
42982
+ PERFORM set_config('logistics.plan_uit', '0', true);
42983
+ v_fouten := v_fouten || jsonb_build_object('order', k.merged_order_number, 'fout', SQLERRM);
42984
+ PERFORM public._logistics_merge_melding('herstel_fout', k.merged_order_id, NULL, NULL,
42985
+ 'herstel ' || COALESCE(k.merged_order_number, '?') || ': ' || SQLERRM);
42986
+ END;
42987
+ END LOOP;
42988
+
42989
+ WITH weg AS (
42990
+ DELETE FROM public.logistics_plan_queue q
42991
+ USING public.orders o
42992
+ WHERE q.order_id = o.id AND q.order_id = ANY (v_kept)
42993
+ AND q.status IN ('pending', 'geblokkeerd')
42994
+ AND (lower(btrim(COALESCE(o.status, ''))) <> 'nog te picken'
42995
+ OR public.logistics_order_date(o.delivery_date::text) < (now() AT TIME ZONE 'Europe/Amsterdam')::date)
42996
+ RETURNING q.id)
42997
+ SELECT count(*) INTO v_opgeruimd FROM weg;
42998
+
42999
+ PERFORM set_config('logistics.merge_uit', '0', true);
43000
+ PERFORM set_config('logistics.merging', '0', true);
43001
+ SELECT count(*) INTO v_rest FROM public._logistics_merge_herstel_kandidaten() c WHERE c.categorie = p_categorie;
43002
+ RETURN jsonb_build_object('categorie', p_categorie, 'hersteld', v_ok, 'regelsTerug', v_regels,
43003
+ 'wachtrijOpgeruimd', v_opgeruimd, 'nogTeDoen', v_rest, 'fouten', v_fouten);
43004
+ END $$;
43005
+ REVOKE ALL ON FUNCTION public.logistics_merge_herstel_uitvoeren(text, integer) FROM PUBLIC, anon, authenticated;
43006
+ GRANT EXECUTE ON FUNCTION public.logistics_merge_herstel_uitvoeren(text, integer) TO service_role;
43007
+
43008
+ CREATE OR REPLACE FUNCTION public.logistics_merge_status()
43009
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
43010
+ BEGIN
43011
+ PERFORM public.logistics_acc_guard('beheer', 'logistics_merge_status');
43012
+ RETURN jsonb_build_object(
43013
+ 'samenvoegenAan', public.logistics_merge_lines_enabled(),
43014
+ 'groepTeGroot', (SELECT COALESCE(sum(x.aantal), 0) FROM (
43015
+ SELECT DISTINCT ON (m.group_key) m.aantal FROM public.logistics_merge_meldingen m
43016
+ WHERE m.soort = 'groep_te_groot' AND m.created_at > now() - interval '7 days'
43017
+ ORDER BY m.group_key, m.created_at DESC) x),
43018
+ 'nieuwVerwijderd', (SELECT count(*) FROM public.orders o
43019
+ WHERE o.deleted_at IS NOT NULL AND o.created_at > now() - interval '7 days'
43020
+ AND o.deleted_at - o.created_at < interval '2 minutes'
43021
+ AND NOT EXISTS (SELECT 1 FROM public.logistics_merge_herstel_log h WHERE h.merged_order_id = o.id)),
43022
+ 'fouten', (SELECT count(*) FROM public.logistics_merge_meldingen m
43023
+ WHERE m.soort IN ('fout', 'herstel_fout') AND m.created_at > now() - interval '7 days'),
43024
+ 'laatsteFout', (SELECT jsonb_build_object('op', m.created_at, 'melding', m.fout) FROM public.logistics_merge_meldingen m
43025
+ WHERE m.soort IN ('fout', 'herstel_fout') ORDER BY m.created_at DESC LIMIT 1),
43026
+ 'hersteld', (SELECT COALESCE(jsonb_object_agg(categorie, n), '{}'::jsonb)
43027
+ FROM (SELECT categorie, count(*) n FROM public.logistics_merge_herstel_log GROUP BY 1) h));
43028
+ END $$;
43029
+ REVOKE ALL ON FUNCTION public.logistics_merge_status() FROM PUBLIC, anon;
43030
+ GRANT EXECUTE ON FUNCTION public.logistics_merge_status() TO authenticated, service_role;
43031
+
43032
+ CREATE OR REPLACE FUNCTION public.logistics_cart_coverage_check()
43033
+ RETURNS jsonb LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $function$
43034
+ SELECT public.logistics_acc_guard('server', 'logistics_cart_coverage_check');
43035
+ WITH o AS (
43036
+ SELECT id, order_number, order_source, status, buffer_id,
43037
+ (status IN ('Verzonden','Gereed voor facturatie')) afgehandeld
43038
+ FROM orders WHERE deleted_at IS NULL AND cancelled_at IS NULL AND buffer_id IS NOT NULL
43039
+ AND (status IN ('Verzonden','Gereed voor facturatie') OR NOT public.logistics_order_is_backfill(id))
43040
+ ), r AS (
43041
+ SELECT o.*, oi.id item, oi.quantity q, COALESCE(NULLIF(oi.plants_per_crate,0),1) ppc,
43042
+ (SELECT count(*) FROM logistics_active.order_items d WHERE d.order_id=o.id AND d.product_id IS NOT DISTINCT FROM oi.product_id) > 1 dubbel,
43043
+ (SELECT sum(ci.crates) FROM logistics_active.order_cart_items ci JOIN logistics_active.order_carts c ON c.id=ci.cart_id
43044
+ WHERE ci.order_item_id=oi.id AND (c.order_id=o.id OR (c.order_id IS DISTINCT FROM o.id AND c.buffer_id=o.buffer_id))) c_goed,
43045
+ (SELECT sum(ci.crates*COALESCE(ci.plants_per_crate,1)) FROM logistics_active.order_cart_items ci JOIN logistics_active.order_carts c ON c.id=ci.cart_id
43046
+ WHERE ci.order_item_id=oi.id AND (c.order_id=o.id OR (c.order_id IS DISTINCT FROM o.id AND c.buffer_id=o.buffer_id))) p_goed,
43047
+ EXISTS (SELECT 1 FROM logistics_active.order_cart_items ci JOIN logistics_active.order_carts c ON c.id=ci.cart_id
43048
+ WHERE ci.order_item_id=oi.id AND c.order_id IS DISTINCT FROM o.id AND c.buffer_id IS DISTINCT FROM o.buffer_id) verkeerd,
43049
+ (SELECT q.reason_code FROM logistics_plan_queue q WHERE q.order_id=o.id AND q.status='geblokkeerd' ORDER BY q.enqueued_at DESC LIMIT 1) blok
43050
+ FROM o JOIN logistics_active.order_items oi ON oi.order_id=o.id
43051
+ ), k AS (
43052
+ SELECT r.*, CASE
43053
+ WHEN verkeerd THEN 'verkeerde_karinhoud'
43054
+ WHEN blok IS NOT NULL AND c_goed IS NULL THEN 'geblokkeerd'
43055
+ WHEN c_goed IS NULL THEN 'geen_kar'
43056
+ WHEN q IN (c_goed, p_goed) THEN 'gelijk'
43057
+ WHEN abs(p_goed - q) < ppc THEN 'gelijk_afgerond'
43058
+ ELSE 'echt_verschil' END klasse
43059
+ FROM r
43060
+ ), po AS (
43061
+ SELECT id, order_number, order_source, status, buffer_id, afgehandeld,
43062
+ (array_agg(klasse ORDER BY CASE klasse WHEN 'verkeerde_karinhoud' THEN 1 WHEN 'geblokkeerd' THEN 2 WHEN 'geen_kar' THEN 3
43063
+ WHEN 'echt_verschil' THEN 4 WHEN 'gelijk_afgerond' THEN 5 ELSE 6 END))[1] klasse,
43064
+ sum(q) q, sum(c_goed) c_goed, sum(p_goed) p_goed, max(blok) blok, bool_or(dubbel) dubbel, count(*) regels
43065
+ FROM k GROUP BY id, order_number, order_source, status, buffer_id, afgehandeld
43066
+ )
43067
+ SELECT jsonb_build_object(
43068
+ 'gemetenOp', now(),
43069
+ 'perOrder', true,
43070
+ 'totaal', (SELECT jsonb_object_agg(klasse, n) FROM (SELECT klasse, count(*) n FROM po GROUP BY 1) s),
43071
+ 'lopend', (SELECT COALESCE(jsonb_object_agg(klasse, n),'{}') FROM (SELECT klasse, count(*) n FROM po WHERE NOT afgehandeld GROUP BY 1) s),
43072
+ 'afgehandeld', (SELECT COALESCE(jsonb_object_agg(klasse, n),'{}') FROM (SELECT klasse, count(*) n FROM po WHERE afgehandeld GROUP BY 1) s),
43073
+ 'dubbeleRegels', (SELECT count(*) FROM k WHERE dubbel),
43074
+ 'afwijkingen', (SELECT COALESCE(jsonb_agg(jsonb_build_object('order',order_number,'bron',order_source,'status',status,
43075
+ 'buffer',buffer_id,'klasse',klasse,'besteld',q,'fusten',c_goed,'planten',p_goed,'blokkade',blok,'dubbel',dubbel,
43076
+ 'afgehandeld',afgehandeld,'regels',regels)
43077
+ ORDER BY afgehandeld, order_number),'[]')
43078
+ FROM po WHERE klasse NOT IN ('gelijk','gelijk_afgerond'))
43079
+ );
43080
+ $function$;
43081
+
43082
+ CREATE OR REPLACE FUNCTION public.logistics_tick_http_log(p_steps jsonb)
43083
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
43084
+ DECLARE v_fouten jsonb := coalesce(p_steps->'stickerSyncFouten', '[]'::jsonb);
43085
+ BEGIN
43086
+ PERFORM public.logistics_acc_guard('server', 'logistics_tick_http_log');
43087
+ INSERT INTO public.logistics_tick_runs(started_at, finished_at, duration_ms, has_error, steps)
43088
+ VALUES (coalesce((p_steps->>'gestart')::timestamptz, now()), now(),
43089
+ nullif(p_steps->>'duurMs', '')::int,
43090
+ false,
43091
+ jsonb_build_object('bron', 'http') || p_steps);
43092
+ INSERT INTO public.logistics_tick_state(key, value, updated_at)
43093
+ VALUES ('sticker_sync', jsonb_build_object('at', now(), 'fouten', v_fouten), now())
43094
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
43095
+ END $$;
43096
+
43097
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_samenvoegen()
43098
+ RETURNS TABLE(onderdeel text, ok boolean, toelichting text)
43099
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
43100
+ DECLARE f text;
43101
+ BEGIN
43102
+ FOREACH f IN ARRAY ARRAY['logistics_order_is_backfill(uuid)', 'logistics_order_merge_key_v2(uuid)',
43103
+ 'logistics_order_mergeable(uuid)', 'logistics_merge_floriday_lines(uuid)',
43104
+ 'logistics_merge_herstel_voorbeeld()', 'logistics_merge_herstel_uitvoeren(text,integer)',
43105
+ 'logistics_merge_status()', 'logistics_trace_event(uuid,text,text,text)', 'logistics_schema_versie()'] LOOP
43106
+ onderdeel := 'functie ' || f;
43107
+ ok := to_regprocedure('public.' || f) IS NOT NULL;
43108
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: werk de Engine bij via Supabase bijwerken' END;
43109
+ RETURN NEXT;
43110
+ END LOOP;
43111
+ onderdeel := 'tabel logistics_merge_herstel_log';
43112
+ ok := to_regclass('public.logistics_merge_herstel_log') IS NOT NULL;
43113
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: werk de Engine bij via Supabase bijwerken' END;
43114
+ RETURN NEXT;
43115
+ END $$;
43116
+ REVOKE ALL ON FUNCTION public.logistics_install_check_samenvoegen() FROM PUBLIC, anon;
43117
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_samenvoegen() TO authenticated, service_role;
43118
+
43119
+ CREATE OR REPLACE FUNCTION public.logistics_install_check()
43120
+ RETURNS TABLE(onderdeel text, ok boolean, toelichting text)
43121
+ LANGUAGE plpgsql
43122
+ SECURITY DEFINER
43123
+ SET search_path TO 'public'
43124
+ AS $function$
43125
+ DECLARE c text; f text; t text; v_fav text; n bigint; s record;
43126
+ BEGIN
43127
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_install_check');
43128
+ FOREACH f IN ARRAY ARRAY['logistics_pickup_at(text,date)',
43129
+ 'logistics_pickup_ts(text,date)', 'logistics_pickup_herbereken()'] LOOP
43130
+ onderdeel := 'functie ' || f;
43131
+ ok := to_regprocedure('public.' || f) IS NOT NULL;
43132
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: werk de Engine bij via Supabase bijwerken' END;
43133
+ RETURN NEXT;
43134
+ END LOOP;
43135
+ FOREACH f IN ARRAY ARRAY[
43136
+ 'f:logistics_try_uuid','f:logistics_tz','f:logistics_is_beheer','f:logistics_buffer_payload','f:logistics_apply_order_buffer',
43137
+ 'f:logistics_apply_buffer_decision','f:logistics_rebuffer_pending','f:logistics_departure_calc','f:logistics_own_transport_departure',
43138
+ 'f:logistics_order_departure','f:logistics_brief_transport','f:logistics_order_frozen','f:logistics_order_is_frozen',
43139
+ 'f:logistics_order_finalize_run','f:run_logistics_tasks_dispatch','f:enqueue_cart_replan_if_unlocked','f:logistics_tick',
43140
+ 't:logistics_tick_runs',
43141
+ 'f:logistics_belading_bevestig','f:logistics_cart_seq_dubbel','f:logistics_plan_retry_later','f:logistics_own_traysticker_sync',
43142
+ 't:logistics_belading_bevestigingen',
43143
+ 'f:logistics_sticker_copies','f:logistics_sticker_etiket_status','f:logistics_sticker_order_counts','f:logistics_sticker_sync',
43144
+ 'c:logistics_sticker_prints.kopieen_bron',
43145
+ 'f:logistics_carrier_settings_ensure','t:logistics_carrier_settings','t:logistics_carrier_recalc_queue','t:logistics_carrier_events',
43146
+ 'f:logistics_carrier_status','f:logistics_carrier_set_backfill','f:logistics_carrier_preview_existing','c:control_get_todays_carts.carrier_name',
43147
+ 'f:logistics_plan_fail_log'] LOOP
43148
+ onderdeel := CASE substr(f, 3) WHEN 'run_logistics_tasks_dispatch' THEN 'Voorraad: ' WHEN 'enqueue_cart_replan_if_unlocked' THEN 'Verkoop: ' ELSE 'Engine: ' END || substr(f, 3);
43149
+ ok := CASE left(f, 1)
43150
+ WHEN 'f' THEN EXISTS (SELECT 1 FROM pg_proc p WHERE p.pronamespace = 'public'::regnamespace AND p.proname = substr(f, 3))
43151
+ WHEN 't' THEN to_regclass('public.' || substr(f, 3)) IS NOT NULL
43152
+ ELSE CASE WHEN split_part(substr(f, 3), '.', 1) = 'control_get_todays_carts'
43153
+ THEN coalesce(pg_get_function_result(to_regprocedure('public.control_get_todays_carts()')), '') LIKE '%carrier_name%'
43154
+ ELSE EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public'
43155
+ AND table_name = split_part(substr(f, 3), '.', 1) AND column_name = split_part(substr(f, 3), '.', 2)) END
43156
+ END;
43157
+ toelichting := CASE WHEN ok THEN 'aanwezig' WHEN substr(f, 3) = 'run_logistics_tasks_dispatch' THEN 'ontbreekt: hoort bij Voorraad; werk Voorraad bij' WHEN substr(f, 3) = 'enqueue_cart_replan_if_unlocked' THEN 'ontbreekt: hoort bij Verkoop; werk Verkoop bij' ELSE 'ontbreekt: Engine-migratie (1.0.182 t/m 1.0.188) in dev uitvoeren en via schemasync verspreiden' END;
43158
+ RETURN NEXT;
43159
+ END LOOP;
43160
+ FOREACH c IN ARRAY ARRAY['carrier_id','carrier_source','carrier_reason','carrier_resolved_at','carrier_set_by','carrier_set_at'] LOOP
43161
+ onderdeel := 'orders.' || c;
43162
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='orders' AND column_name=c);
43163
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie' END; RETURN NEXT;
43164
+ END LOOP;
43165
+ FOREACH f IN ARRAY ARRAY['logistics_resolve_carrier(uuid)','logistics_resolve_carrier_many(uuid[])',
43166
+ 'logistics_resolve_carrier_for_location(uuid,uuid)','logistics_resolve_carrier_for_location_many(uuid[],uuid)',
43167
+ 'logistics_resolve_carrier_for_location_many(jsonb)',
43168
+ 'logistics_set_order_carrier(uuid,uuid)','logistics_carrier_recalc_run(integer)'] LOOP
43169
+ onderdeel := 'functie ' || f; ok := to_regprocedure('public.' || f) IS NOT NULL;
43170
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie' END; RETURN NEXT;
43171
+ END LOOP;
43172
+ FOREACH t IN ARRAY ARRAY['orders:trg_logistics_orders_carrier_fill','orders:trg_logistics_orders_carrier_changed',
43173
+ 'carriers:trg_logistics_carriers_enqueue','customer_delivery_locations:trg_logistics_locations_enqueue'] LOOP
43174
+ onderdeel := 'trigger ' || split_part(t, ':', 2);
43175
+ ok := EXISTS (SELECT 1 FROM pg_trigger g WHERE g.tgname = split_part(t, ':', 2)
43176
+ AND g.tgrelid = to_regclass('public.' || split_part(t, ':', 1)));
43177
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie' END; RETURN NEXT;
43178
+ END LOOP;
43179
+ onderdeel := 'minuutronde rekent vervoerders';
43180
+ ok := EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'logistics_tick' AND prosrc LIKE '%logistics_carrier_recalc_run%');
43181
+ toelichting := CASE WHEN ok THEN 'ja' ELSE 'nee' END; RETURN NEXT;
43182
+ FOREACH c IN ARRAY ARRAY['carrier_kind','is_favorite','archived_at','delivery_location_glns'] LOOP
43183
+ onderdeel := 'Afhandeling: carriers.' || c;
43184
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='carriers' AND column_name=c);
43185
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Afhandeling' END; RETURN NEXT;
43186
+ END LOOP;
43187
+ FOREACH f IN ARRAY ARRAY['afh_favorite_carrier','afh_city_code','afh_normalize_country','afh_normalize_nl_province'] LOOP
43188
+ onderdeel := 'Afhandeling: ' || f || '()';
43189
+ ok := EXISTS (SELECT 1 FROM pg_proc p WHERE p.pronamespace = 'public'::regnamespace AND p.proname = f);
43190
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Afhandeling' END; RETURN NEXT;
43191
+ END LOOP;
43192
+ onderdeel := 'Favoriete vervoerder ingesteld en bruikbaar';
43193
+ IF to_regprocedure('public.afh_favorite_carrier()') IS NULL THEN ok := false; toelichting := 'functie ontbreekt';
43194
+ ELSE
43195
+ BEGIN v_fav := public.logistics_carrier_favorite_id()::text; EXCEPTION WHEN OTHERS THEN v_fav := NULL; END;
43196
+ ok := public.logistics_try_uuid(v_fav) IS NOT NULL AND public.logistics_carrier_skip_reason(public.logistics_try_uuid(v_fav)) IS NULL;
43197
+ toelichting := CASE WHEN ok THEN 'ja' WHEN v_fav IS NULL THEN 'geen favoriet ingesteld (Afhandeling → Vervoerders)'
43198
+ ELSE public.logistics_carrier_skip_reason(public.logistics_try_uuid(v_fav)) END;
43199
+ END IF;
43200
+ RETURN NEXT;
43201
+ onderdeel := 'Klanten: customers.default_carrier_id';
43202
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='customers' AND column_name='default_carrier_id');
43203
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Klanten' END; RETURN NEXT;
43204
+ IF to_regclass('public.logistics_carrier_settings') IS NOT NULL AND to_regclass('public.orders') IS NOT NULL
43205
+ AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='orders' AND column_name='carrier_source') THEN
43206
+ SELECT * INTO s FROM public.logistics_carrier_settings_ensure();
43207
+ EXECUTE $q$SELECT count(*) FROM public.orders o
43208
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
43209
+ AND coalesce(o.delivery_date, '') ~ '^\d{4}-\d{2}-\d{2}' AND public.logistics_order_date(o.delivery_date::text) >= (now() AT TIME ZONE coalesce(public.logistics_tz(), 'Europe/Amsterdam'))::date
43210
+ AND (o.carrier_source = 'geen' OR (o.carrier_source IS NULL AND o.created_at >= $1))$q$ INTO n USING s.installed_at;
43211
+ onderdeel := 'Open orders zonder vervoerder'; ok := n = 0; toelichting := n || ' order(s)'; RETURN NEXT;
43212
+ onderdeel := 'Schakelaar bestaande orders'; ok := true;
43213
+ toelichting := CASE WHEN coalesce(s.backfill_existing, false) THEN 'aan' ELSE 'uit (bestaande orders wachten op akkoord)' END; RETURN NEXT;
43214
+ END IF;
43215
+ SELECT count(*) INTO n FROM public.logistics_trace_events e
43216
+ WHERE e.actor IN ('trg_logistics_plan_order_items', 'trg_logistics_plan_orders')
43217
+ AND e.status = 'mislukt' AND e.created_at > now() - interval '24 hours';
43218
+ onderdeel := 'Mislukte herplanning (laatste 24 uur)'; ok := n = 0;
43219
+ toelichting := CASE WHEN n = 0 THEN 'geen' ELSE n || ' keer; laatste: ' || coalesce((SELECT coalesce(e.order_ref, '?') || ': ' || coalesce(e.error, '') FROM public.logistics_trace_events e
43220
+ WHERE e.actor IN ('trg_logistics_plan_order_items', 'trg_logistics_plan_orders') AND e.status = 'mislukt'
43221
+ ORDER BY e.created_at DESC LIMIT 1), '') END;
43222
+ RETURN NEXT;
43223
+ RETURN QUERY SELECT * FROM public.logistics_install_check_194();
43224
+ RETURN QUERY SELECT * FROM public.logistics_install_check_196();
43225
+ RETURN QUERY SELECT * FROM public.logistics_install_check_197();
43226
+ RETURN QUERY SELECT * FROM public.logistics_install_check_198();
43227
+ RETURN QUERY SELECT * FROM public.logistics_access_check();
43228
+ RETURN QUERY SELECT * FROM public.logistics_overload_check();
43229
+ RETURN QUERY SELECT * FROM public.logistics_check_190();
43230
+ RETURN QUERY SELECT * FROM public.logistics_install_check_productiebon();
43231
+ RETURN QUERY SELECT * FROM public.logistics_install_check_samenvoegen();
43232
+ END $function$;
43233
+
43234
+ CREATE OR REPLACE FUNCTION public.logistics_schema_versie()
43235
+ RETURNS text LANGUAGE sql IMMUTABLE SET search_path TO 'public' AS $function$ SELECT '2026-11-10b'::text $function$;
43236
+
43237
+ -- ==== migratie 20261110090544
43238
+ CREATE OR REPLACE FUNCTION public.logistics_merge_floriday_lines(p_order_id uuid)
43239
+ RETURNS uuid LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
43240
+ DECLARE
43241
+ v_key text; v_keep uuid; v_keepnr text; v_cust uuid; r record; v_ids uuid[];
43242
+ v_moved int; v_logged int; v_total int := 0; v_count int := 0; v_groep int;
43243
+ v_lines text[] := ARRAY[]::text[];
43244
+ BEGIN
43245
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_merge_floriday_lines');
43246
+ IF p_order_id IS NULL THEN RETURN p_order_id; END IF;
43247
+ IF COALESCE(current_setting('logistics.merge_uit', true), '') = '1' THEN RETURN p_order_id; END IF;
43248
+ IF NOT public.logistics_merge_lines_enabled() THEN RETURN p_order_id; END IF;
43249
+ IF NOT public.logistics_order_mergeable(p_order_id) THEN RETURN p_order_id; END IF;
43250
+ IF NOT EXISTS (SELECT 1 FROM logistics_active.order_items i WHERE i.order_id = p_order_id) THEN RETURN p_order_id; END IF;
43251
+
43252
+ v_key := public.logistics_order_merge_key_v2(p_order_id);
43253
+ IF v_key IS NULL THEN RETURN p_order_id; END IF;
43254
+ IF NOT pg_try_advisory_xact_lock(hashtext('logistics_merge:' || v_key)) THEN RETURN p_order_id; END IF;
43255
+ SELECT customer_id INTO v_cust FROM public.orders WHERE id = p_order_id;
43256
+
43257
+ SELECT array_agg(o.id ORDER BY o.order_number ASC NULLS LAST, o.created_at ASC) INTO v_ids
43258
+ FROM public.orders o
43259
+ WHERE o.customer_id = v_cust AND o.deleted_at IS NULL AND o.order_source = 'floriday'
43260
+ AND EXISTS (SELECT 1 FROM logistics_active.order_items i WHERE i.order_id = o.id)
43261
+ AND public.logistics_order_merge_key_v2(o.id) = v_key
43262
+ AND public.logistics_order_mergeable(o.id);
43263
+ v_groep := COALESCE(cardinality(v_ids), 0);
43264
+ IF v_groep < 2 THEN RETURN p_order_id; END IF;
43265
+ IF v_groep > 20 THEN
43266
+ IF NOT EXISTS (SELECT 1 FROM public.logistics_merge_meldingen m
43267
+ WHERE m.soort = 'groep_te_groot' AND m.group_key = v_key AND m.created_at > now() - interval '1 day') THEN
43268
+ PERFORM public._logistics_merge_melding('groep_te_groot', p_order_id, v_key, v_groep,
43269
+ v_groep || ' orders met dezelfde klant, leverdag, afleverlocatie en ophaalmoment; niet samengevoegd.');
43270
+ END IF;
43271
+ RETURN p_order_id;
43272
+ END IF;
43273
+
43274
+ v_keep := v_ids[1];
43275
+ SELECT order_number INTO v_keepnr FROM public.orders WHERE id = v_keep;
43276
+ -- Geen geneste samenvoegronde tijdens het verplaatsen van regels.
43277
+ PERFORM set_config('logistics.merge_uit', '1', true);
43278
+
43279
+ FOR r IN SELECT o.id, o.order_number, o.floriday_sales_order_id AS sales, o.created_at
43280
+ FROM public.orders o WHERE o.id = ANY (v_ids) AND o.id <> v_keep
43281
+ ORDER BY o.order_number ASC NULLS LAST LOOP
43282
+ BEGIN
43283
+ INSERT INTO public.logistics_order_merge_lines(
43284
+ group_key, from_order_id, from_order_number, to_order_id, to_order_number,
43285
+ order_item_id, product_id, product_name, quantity, source_line_key,
43286
+ sales_order_id, ordered_at, reason)
43287
+ SELECT v_key, r.id, r.order_number, v_keep, v_keepnr,
43288
+ i.id, i.product_id, i.product_name, i.quantity, i.source_line_key,
43289
+ r.sales, r.created_at, 'Zelfde klant, leverdag, afleverlocatie en ophaalmoment'
43290
+ FROM logistics_active.order_items i WHERE i.order_id = r.id;
43291
+ GET DIAGNOSTICS v_logged = ROW_COUNT;
43292
+ UPDATE public.order_items SET order_id = v_keep WHERE order_id = r.id AND quarantined_at IS NULL;
43293
+ GET DIAGNOSTICS v_moved = ROW_COUNT;
43294
+ IF v_moved <> v_logged THEN RAISE EXCEPTION 'aantal verplaatste regels (%) wijkt af van het logboek (%)', v_moved, v_logged; END IF;
43295
+ IF v_moved = 0 THEN RAISE EXCEPTION 'geen regels verplaatst; order blijft staan'; END IF;
43296
+ INSERT INTO public.logistics_order_merges(group_key, kept_order_id, kept_order_number,
43297
+ merged_order_id, merged_order_number, sales_order_id, items_moved)
43298
+ VALUES (v_key, v_keep, v_keepnr, r.id, r.order_number, r.sales, v_moved)
43299
+ ON CONFLICT (merged_order_id) DO NOTHING;
43300
+ UPDATE public.orders SET deleted_at = now(),
43301
+ notes = COALESCE(NULLIF(notes, ''), '')
43302
+ || CASE WHEN COALESCE(notes, '') = '' THEN '' ELSE E'\n' END
43303
+ || 'Samengevoegd met ' || COALESCE(v_keepnr, '') || '.',
43304
+ updated_at = now()
43305
+ WHERE id = r.id;
43306
+ v_total := v_total + v_moved; v_count := v_count + 1;
43307
+ v_lines := v_lines || (COALESCE(r.order_number, '') || ': ' || v_moved::text || ' regel(s)');
43308
+ EXCEPTION WHEN OTHERS THEN
43309
+ PERFORM public._logistics_merge_melding('fout', r.id, v_key, NULL,
43310
+ 'samenvoegen ' || COALESCE(r.order_number, '?') || ' met ' || COALESCE(v_keepnr, '?') || ': ' || SQLERRM);
43311
+ END;
43312
+ END LOOP;
43313
+ PERFORM set_config('logistics.merge_uit', '0', true);
43314
+
43315
+ IF v_count > 0 THEN
43316
+ UPDATE public.orders o SET total = to_char(COALESCE((
43317
+ SELECT SUM(COALESCE(i.quantity, 0) * COALESCE(i.unit_price, 0))
43318
+ FROM logistics_active.order_items i WHERE i.order_id = o.id), 0), 'FM9999999990.00'),
43319
+ updated_at = now()
43320
+ WHERE o.id = v_keep;
43321
+ PERFORM public.logistics_trace_order_step(
43322
+ v_keep, 'stap:bestelregels_samenvoegen', 'bestelregels samenvoegen', 'geslaagd',
43323
+ 'samengevoegd tot ' || COALESCE(v_keepnr, 'één bestelling'),
43324
+ v_count::text || ' order(s), ' || v_total::text || ' regel(s) verplaatst: ' || array_to_string(v_lines, '; '),
43325
+ 'Orders combineren');
43326
+ END IF;
43327
+ IF EXISTS (SELECT 1 FROM public.orders WHERE id = p_order_id AND deleted_at IS NOT NULL) THEN RETURN v_keep; END IF;
43328
+ RETURN p_order_id;
43329
+ EXCEPTION WHEN OTHERS THEN
43330
+ PERFORM set_config('logistics.merge_uit', '0', true);
43331
+ PERFORM public._logistics_merge_melding('fout', p_order_id, v_key, NULL, 'samenvoegen: ' || SQLERRM);
43332
+ RETURN p_order_id;
43333
+ END $$;
43334
+
43335
+ CREATE OR REPLACE FUNCTION public._logistics_merge_herstel_kandidaten()
43336
+ RETURNS TABLE(merged_order_id uuid, merged_order_number text, kept_order_id uuid, kept_order_number text,
43337
+ categorie text, regels integer, status text, leverdag date, reden text)
43338
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
43339
+ DECLARE r record; v_dag date; v_onzeker text; v_vandaag date := (now() AT TIME ZONE 'Europe/Amsterdam')::date;
43340
+ BEGIN
43341
+ FOR r IN
43342
+ SELECT m.merged_order_id, m.merged_order_number, m.kept_order_id, m.kept_order_number, m.items_moved,
43343
+ o.status, o.delivery_date
43344
+ FROM public.logistics_order_merges m
43345
+ JOIN public.orders o ON o.id = m.merged_order_id
43346
+ WHERE o.deleted_at IS NOT NULL
43347
+ AND COALESCE(m.group_key, '') NOT LIKE 'v2|%'
43348
+ AND NOT EXISTS (SELECT 1 FROM public.logistics_merge_herstel_log h WHERE h.merged_order_id = m.merged_order_id)
43349
+ LOOP
43350
+ v_onzeker := NULL;
43351
+ BEGIN
43352
+ v_dag := public.logistics_order_date(r.delivery_date::text);
43353
+ EXCEPTION WHEN OTHERS THEN
43354
+ v_dag := NULL;
43355
+ END;
43356
+ IF v_dag IS NULL THEN v_onzeker := 'leverdag onleesbaar'; END IF;
43357
+ IF v_onzeker IS NULL AND (SELECT count(*) FROM public.logistics_order_merge_lines l WHERE l.from_order_id = r.merged_order_id)
43358
+ <> COALESCE(r.items_moved, 0) THEN
43359
+ v_onzeker := 'logboek en aantal verplaatste regels verschillen';
43360
+ END IF;
43361
+ IF v_onzeker IS NULL AND EXISTS (
43362
+ SELECT 1 FROM public.logistics_order_merge_lines l
43363
+ LEFT JOIN public.order_items i ON i.id = l.order_item_id
43364
+ WHERE l.from_order_id = r.merged_order_id
43365
+ AND (i.id IS NULL OR i.order_id IS DISTINCT FROM r.kept_order_id)) THEN
43366
+ v_onzeker := 'regel staat niet meer bij de verzamelorder';
43367
+ END IF;
43368
+ IF v_onzeker IS NULL AND EXISTS (
43369
+ SELECT 1 FROM public.logistics_order_merge_lines l
43370
+ WHERE l.from_order_id = r.merged_order_id
43371
+ AND EXISTS (SELECT 1 FROM public.logistics_order_merge_lines l2
43372
+ WHERE l2.order_item_id = l.order_item_id AND l2.from_order_id <> l.from_order_id)) THEN
43373
+ v_onzeker := 'regel komt bij meer dan één order vandaan';
43374
+ END IF;
43375
+ merged_order_id := r.merged_order_id; merged_order_number := r.merged_order_number;
43376
+ kept_order_id := r.kept_order_id; kept_order_number := r.kept_order_number;
43377
+ regels := COALESCE(r.items_moved, 0); status := r.status; leverdag := v_dag; reden := v_onzeker;
43378
+ categorie := CASE
43379
+ WHEN v_onzeker IS NOT NULL THEN 'onzeker'
43380
+ WHEN v_dag < v_vandaag THEN 'verleden'
43381
+ WHEN lower(btrim(COALESCE(r.status, ''))) = 'nog te picken' THEN 'urgent'
43382
+ ELSE 'toekomst_overig' END;
43383
+ RETURN NEXT;
43384
+ END LOOP;
43385
+ END $$;
43386
+
43387
+ REVOKE EXECUTE ON FUNCTION public.logistics_order_merge_key_v2(uuid) FROM authenticated;
43388
+
43389
+ -- ==== migratie 20261110090709
43390
+ CREATE OR REPLACE FUNCTION public.logistics_merge_floriday_lines(p_order_id uuid)
43391
+ RETURNS uuid LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
43392
+ DECLARE
43393
+ v_key text; v_keep uuid; v_keepnr text; v_cust uuid; r record; v_ids uuid[];
43394
+ v_moved int; v_logged int; v_total int := 0; v_count int := 0; v_groep int;
43395
+ v_lines text[] := ARRAY[]::text[];
43396
+ BEGIN
43397
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_merge_floriday_lines');
43398
+ IF p_order_id IS NULL THEN RETURN p_order_id; END IF;
43399
+ IF COALESCE(current_setting('logistics.merge_uit', true), '') = '1' THEN RETURN p_order_id; END IF;
43400
+ IF NOT public.logistics_merge_lines_enabled() THEN RETURN p_order_id; END IF;
43401
+ IF NOT public.logistics_order_mergeable(p_order_id) THEN RETURN p_order_id; END IF;
43402
+ IF NOT EXISTS (SELECT 1 FROM logistics_active.order_items i WHERE i.order_id = p_order_id) THEN RETURN p_order_id; END IF;
43403
+
43404
+ v_key := public.logistics_order_merge_key_v2(p_order_id);
43405
+ IF v_key IS NULL THEN RETURN p_order_id; END IF;
43406
+ IF NOT pg_try_advisory_xact_lock(hashtext('logistics_merge:' || v_key)) THEN RETURN p_order_id; END IF;
43407
+ SELECT customer_id INTO v_cust FROM public.orders WHERE id = p_order_id;
43408
+
43409
+ SELECT array_agg(o.id ORDER BY o.order_number ASC NULLS LAST, o.created_at ASC) INTO v_ids
43410
+ FROM public.orders o
43411
+ WHERE o.customer_id = v_cust AND o.deleted_at IS NULL AND o.order_source = 'floriday'
43412
+ AND EXISTS (SELECT 1 FROM logistics_active.order_items i WHERE i.order_id = o.id)
43413
+ AND public.logistics_order_merge_key_v2(o.id) = v_key
43414
+ AND public.logistics_order_mergeable(o.id);
43415
+ v_groep := COALESCE(cardinality(v_ids), 0);
43416
+ IF v_groep < 2 THEN RETURN p_order_id; END IF;
43417
+ v_keep := v_ids[1];
43418
+ -- Grens: eerder samengevoegde orders bij dezelfde bewaarorder tellen mee.
43419
+ v_groep := v_groep + (SELECT count(*) FROM public.logistics_order_merges m WHERE m.kept_order_id = v_keep AND m.group_key = v_key);
43420
+ IF v_groep > 20 THEN
43421
+ IF NOT EXISTS (SELECT 1 FROM public.logistics_merge_meldingen m
43422
+ WHERE m.soort = 'groep_te_groot' AND m.group_key = v_key AND m.created_at > now() - interval '1 day') THEN
43423
+ PERFORM public._logistics_merge_melding('groep_te_groot', p_order_id, v_key, v_groep,
43424
+ v_groep || ' orders met dezelfde klant, leverdag, afleverlocatie en ophaalmoment; niet samengevoegd.');
43425
+ END IF;
43426
+ RETURN p_order_id;
43427
+ END IF;
43428
+
43429
+ SELECT order_number INTO v_keepnr FROM public.orders WHERE id = v_keep;
43430
+ PERFORM set_config('logistics.merge_uit', '1', true);
43431
+
43432
+ FOR r IN SELECT o.id, o.order_number, o.floriday_sales_order_id AS sales, o.created_at
43433
+ FROM public.orders o WHERE o.id = ANY (v_ids) AND o.id <> v_keep
43434
+ ORDER BY o.order_number ASC NULLS LAST LOOP
43435
+ BEGIN
43436
+ INSERT INTO public.logistics_order_merge_lines(
43437
+ group_key, from_order_id, from_order_number, to_order_id, to_order_number,
43438
+ order_item_id, product_id, product_name, quantity, source_line_key,
43439
+ sales_order_id, ordered_at, reason)
43440
+ SELECT v_key, r.id, r.order_number, v_keep, v_keepnr,
43441
+ i.id, i.product_id, i.product_name, i.quantity, i.source_line_key,
43442
+ r.sales, r.created_at, 'Zelfde klant, leverdag, afleverlocatie en ophaalmoment'
43443
+ FROM logistics_active.order_items i WHERE i.order_id = r.id;
43444
+ GET DIAGNOSTICS v_logged = ROW_COUNT;
43445
+ UPDATE public.order_items SET order_id = v_keep WHERE order_id = r.id AND quarantined_at IS NULL;
43446
+ GET DIAGNOSTICS v_moved = ROW_COUNT;
43447
+ IF v_moved <> v_logged THEN RAISE EXCEPTION 'aantal verplaatste regels (%) wijkt af van het logboek (%)', v_moved, v_logged; END IF;
43448
+ IF v_moved = 0 THEN RAISE EXCEPTION 'geen regels verplaatst; order blijft staan'; END IF;
43449
+ INSERT INTO public.logistics_order_merges(group_key, kept_order_id, kept_order_number,
43450
+ merged_order_id, merged_order_number, sales_order_id, items_moved)
43451
+ VALUES (v_key, v_keep, v_keepnr, r.id, r.order_number, r.sales, v_moved)
43452
+ ON CONFLICT (merged_order_id) DO NOTHING;
43453
+ UPDATE public.orders SET deleted_at = now(),
43454
+ notes = COALESCE(NULLIF(notes, ''), '')
43455
+ || CASE WHEN COALESCE(notes, '') = '' THEN '' ELSE E'\n' END
43456
+ || 'Samengevoegd met ' || COALESCE(v_keepnr, '') || '.',
43457
+ updated_at = now()
43458
+ WHERE id = r.id;
43459
+ v_total := v_total + v_moved; v_count := v_count + 1;
43460
+ v_lines := v_lines || (COALESCE(r.order_number, '') || ': ' || v_moved::text || ' regel(s)');
43461
+ EXCEPTION WHEN OTHERS THEN
43462
+ PERFORM public._logistics_merge_melding('fout', r.id, v_key, NULL,
43463
+ 'samenvoegen ' || COALESCE(r.order_number, '?') || ' met ' || COALESCE(v_keepnr, '?') || ': ' || SQLERRM);
43464
+ END;
43465
+ END LOOP;
43466
+ PERFORM set_config('logistics.merge_uit', '0', true);
43467
+
43468
+ IF v_count > 0 THEN
43469
+ UPDATE public.orders o SET total = to_char(COALESCE((
43470
+ SELECT SUM(COALESCE(i.quantity, 0) * COALESCE(i.unit_price, 0))
43471
+ FROM logistics_active.order_items i WHERE i.order_id = o.id), 0), 'FM9999999990.00'),
43472
+ updated_at = now()
43473
+ WHERE o.id = v_keep;
43474
+ PERFORM public.logistics_trace_order_step(
43475
+ v_keep, 'stap:bestelregels_samenvoegen', 'bestelregels samenvoegen', 'geslaagd',
43476
+ 'samengevoegd tot ' || COALESCE(v_keepnr, 'één bestelling'),
43477
+ v_count::text || ' order(s), ' || v_total::text || ' regel(s) verplaatst: ' || array_to_string(v_lines, '; '),
43478
+ 'Orders combineren');
43479
+ END IF;
43480
+ IF EXISTS (SELECT 1 FROM public.orders WHERE id = p_order_id AND deleted_at IS NOT NULL) THEN RETURN v_keep; END IF;
43481
+ RETURN p_order_id;
43482
+ EXCEPTION WHEN OTHERS THEN
43483
+ PERFORM set_config('logistics.merge_uit', '0', true);
43484
+ PERFORM public._logistics_merge_melding('fout', p_order_id, v_key, NULL, 'samenvoegen: ' || SQLERRM);
43485
+ RETURN p_order_id;
43486
+ END $$;
43487
+
43488
+ -- ==== migratie 20261110090737
43489
+ CREATE OR REPLACE FUNCTION public.logistics_merge_status()
43490
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
43491
+ BEGIN
43492
+ PERFORM public.logistics_acc_guard('beheer', 'logistics_merge_status');
43493
+ RETURN jsonb_build_object(
43494
+ 'samenvoegenAan', public.logistics_merge_lines_enabled(),
43495
+ 'groepTeGroot', (SELECT COALESCE(sum(x.aantal), 0) FROM (
43496
+ SELECT DISTINCT ON (m.group_key) m.aantal FROM public.logistics_merge_meldingen m
43497
+ WHERE m.soort = 'groep_te_groot' AND m.created_at > now() - interval '7 days'
43498
+ ORDER BY m.group_key, m.created_at DESC) x),
43499
+ 'nieuwVerwijderd', (SELECT count(*) FROM public.orders o
43500
+ WHERE o.deleted_at IS NOT NULL AND o.created_at > now() - interval '7 days'
43501
+ AND o.deleted_at - o.created_at < interval '2 minutes'
43502
+ AND NOT EXISTS (SELECT 1 FROM public.logistics_order_merges m
43503
+ WHERE m.merged_order_id = o.id AND m.items_moved > 0 AND m.group_key LIKE 'v2|%')
43504
+ AND NOT EXISTS (SELECT 1 FROM public.logistics_merge_herstel_log h WHERE h.merged_order_id = o.id)),
43505
+ 'fouten', (SELECT count(*) FROM public.logistics_merge_meldingen m
43506
+ WHERE m.soort IN ('fout', 'herstel_fout') AND m.created_at > now() - interval '7 days'),
43507
+ 'laatsteFout', (SELECT jsonb_build_object('op', m.created_at, 'melding', m.fout) FROM public.logistics_merge_meldingen m
43508
+ WHERE m.soort IN ('fout', 'herstel_fout') ORDER BY m.created_at DESC LIMIT 1),
43509
+ 'hersteld', (SELECT COALESCE(jsonb_object_agg(categorie, n), '{}'::jsonb)
43510
+ FROM (SELECT categorie, count(*) n FROM public.logistics_merge_herstel_log GROUP BY 1) h));
43511
+ END $$;
43512
+
43513
+ -- ===== 2026-11-10c: ophaalmoment, samenvoegen volgens buffergroep =====
43514
+ -- 1.0.239 (schema 2026-11-10c): ophaalmoment per order uit de werkwijze, samenvoegen volgens de buffergroep.
43515
+
43516
+ ALTER TABLE public.logistics_pickup_moments ADD COLUMN IF NOT EXISTS carrier_id uuid;
43517
+ ALTER TABLE public.logistics_pickup_moments ADD COLUMN IF NOT EXISTS pickup_days integer;
43518
+ ALTER TABLE public.logistics_pickup_moments ADD COLUMN IF NOT EXISTS signup_days integer;
43519
+ ALTER TABLE public.logistics_pickup_moments ADD COLUMN IF NOT EXISTS signup_time time without time zone;
43520
+ CREATE INDEX IF NOT EXISTS logistics_pickup_moments_carrier_dag_idx ON public.logistics_pickup_moments (carrier_id, moment_date);
43521
+
43522
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_carrier_uit_naam(p_naam text)
43523
+ RETURNS uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
43524
+ SELECT CASE WHEN count(*) = 1 THEN (array_agg(c.id))[1] END
43525
+ FROM public.carriers c
43526
+ WHERE c.archived_at IS NULL
43527
+ AND nullif(btrim(p_naam), '') IS NOT NULL
43528
+ AND lower(btrim(c.name)) = lower(btrim(p_naam));
43529
+ $$;
43530
+ REVOKE ALL ON FUNCTION public.logistics_pickup_carrier_uit_naam(text) FROM PUBLIC, anon, authenticated;
43531
+ GRANT EXECUTE ON FUNCTION public.logistics_pickup_carrier_uit_naam(text) TO service_role;
43532
+
43533
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_moments_koppel_trg()
43534
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
43535
+ BEGIN
43536
+ IF NEW.carrier_id IS NULL AND NEW.carrier_ref IS NOT NULL THEN
43537
+ NEW.carrier_id := public.logistics_pickup_carrier_uit_naam(NEW.carrier_ref);
43538
+ END IF;
43539
+ RETURN NEW;
43540
+ END $$;
43541
+ REVOKE ALL ON FUNCTION public.logistics_pickup_moments_koppel_trg() FROM PUBLIC, anon, authenticated;
43542
+ DROP TRIGGER IF EXISTS trg_logistics_pickup_moments_koppel ON public.logistics_pickup_moments;
43543
+ CREATE TRIGGER trg_logistics_pickup_moments_koppel
43544
+ BEFORE INSERT OR UPDATE OF carrier_ref, carrier_id ON public.logistics_pickup_moments
43545
+ FOR EACH ROW EXECUTE FUNCTION public.logistics_pickup_moments_koppel_trg();
43546
+
43547
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_moments_koppel()
43548
+ RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
43549
+ DECLARE n int;
43550
+ BEGIN
43551
+ UPDATE public.logistics_pickup_moments p
43552
+ SET carrier_id = public.logistics_pickup_carrier_uit_naam(p.carrier_ref)
43553
+ WHERE p.carrier_id IS NULL AND p.carrier_ref IS NOT NULL
43554
+ AND public.logistics_pickup_carrier_uit_naam(p.carrier_ref) IS NOT NULL;
43555
+ GET DIAGNOSTICS n = ROW_COUNT;
43556
+ RETURN n;
43557
+ END $$;
43558
+ REVOKE ALL ON FUNCTION public.logistics_pickup_moments_koppel() FROM PUBLIC, anon, authenticated;
43559
+ GRANT EXECUTE ON FUNCTION public.logistics_pickup_moments_koppel() TO service_role;
43560
+
43561
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_regel()
43562
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
43563
+ DECLARE s jsonb; v_regel text; v_marge numeric;
43564
+ BEGIN
43565
+ SELECT c.settings INTO s FROM public.logistics_rule_configs c WHERE c.is_active ORDER BY c.updated_at DESC LIMIT 1;
43566
+ v_regel := public.logistics_rule_text(COALESCE(s, '{}'::jsonb), 'order_verwerking', 'pickup_moment_rule', 'eerste');
43567
+ v_marge := public.logistics_rule_num(COALESCE(s, '{}'::jsonb), 'order_verwerking', 'pickup_moment_marge_uren', 2);
43568
+ IF v_regel NOT IN ('eerste', 'laatste_voor_levertijd') THEN v_regel := 'eerste'; END IF;
43569
+ RETURN jsonb_build_object('regel', v_regel, 'margeUren', COALESCE(v_marge, 2), 'werkwijzeGevonden', s IS NOT NULL);
43570
+ END $$;
43571
+ REVOKE ALL ON FUNCTION public.logistics_pickup_regel() FROM PUBLIC, anon;
43572
+ GRANT EXECUTE ON FUNCTION public.logistics_pickup_regel() TO authenticated, service_role;
43573
+
43574
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_moment_kies(
43575
+ p_carrier_id uuid, p_leverdag date, p_levertijd timestamptz, p_regel text, p_marge_uren numeric)
43576
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
43577
+ DECLARE v_tz text; r record; n int;
43578
+ BEGIN
43579
+ IF p_carrier_id IS NULL THEN
43580
+ RETURN jsonb_build_object('ok', false, 'redenCode', 'geen_vervoerder', 'reden', 'De order heeft geen vervoerder.');
43581
+ END IF;
43582
+ IF p_leverdag IS NULL THEN
43583
+ RETURN jsonb_build_object('ok', false, 'redenCode', 'geen_leverdag', 'reden', 'De order heeft geen leverdag.');
43584
+ END IF;
43585
+ v_tz := public.logistics_tz();
43586
+ IF v_tz IS NULL THEN
43587
+ RETURN jsonb_build_object('ok', false, 'redenCode', 'geen_tijdzone', 'reden', 'De tijdzone van de vestiging ontbreekt. Stel die in bij Werktijden.');
43588
+ END IF;
43589
+
43590
+ SELECT count(*) INTO n
43591
+ FROM public.logistics_pickup_moments p
43592
+ WHERE p.carrier_id = p_carrier_id AND p.moment_date = p_leverdag AND p.pickup_time IS NOT NULL
43593
+ AND public.logistics_subtract_pick_workdays(p_leverdag, COALESCE(p.pickup_days, 0)) IS NOT NULL;
43594
+ IF n = 0 THEN
43595
+ RETURN jsonb_build_object('ok', false, 'redenCode', 'geen_moment_op_dag',
43596
+ 'reden', 'De vervoerder heeft geen ophaalmoment op ' || to_char(p_leverdag, 'DD-MM-YYYY') || '.');
43597
+ END IF;
43598
+
43599
+ IF p_regel = 'laatste_voor_levertijd' AND p_levertijd IS NOT NULL THEN
43600
+ SELECT k2.* INTO r FROM (
43601
+ SELECT p.pickup_time, COALESCE(p.pickup_days, 0) AS pd, p.signup_days AS sd, p.signup_time AS st,
43602
+ public.logistics_subtract_pick_workdays(p_leverdag, COALESCE(p.pickup_days, 0)) AS pdag,
43603
+ ((public.logistics_subtract_pick_workdays(p_leverdag, COALESCE(p.pickup_days, 0))::timestamp + p.pickup_time) AT TIME ZONE v_tz) AS pickup_at
43604
+ FROM public.logistics_pickup_moments p
43605
+ WHERE p.carrier_id = p_carrier_id AND p.moment_date = p_leverdag AND p.pickup_time IS NOT NULL) k2
43606
+ WHERE k2.pdag IS NOT NULL
43607
+ AND k2.pickup_at <= p_levertijd - make_interval(secs => (COALESCE(p_marge_uren, 2) * 3600)::double precision)
43608
+ ORDER BY k2.pickup_at DESC LIMIT 1;
43609
+ IF NOT FOUND THEN
43610
+ RETURN jsonb_build_object('ok', false, 'redenCode', 'geen_moment_voor_levertijd',
43611
+ 'reden', 'Er is geen ophaalmoment minstens ' || COALESCE(p_marge_uren, 2) || ' uur vóór de levertijd van de klant ('
43612
+ || to_char(p_levertijd AT TIME ZONE v_tz, 'DD-MM-YYYY HH24:MI') || ').');
43613
+ END IF;
43614
+ ELSE
43615
+ SELECT k2.* INTO r FROM (
43616
+ SELECT p.pickup_time, COALESCE(p.pickup_days, 0) AS pd, p.signup_days AS sd, p.signup_time AS st,
43617
+ public.logistics_subtract_pick_workdays(p_leverdag, COALESCE(p.pickup_days, 0)) AS pdag,
43618
+ ((public.logistics_subtract_pick_workdays(p_leverdag, COALESCE(p.pickup_days, 0))::timestamp + p.pickup_time) AT TIME ZONE v_tz) AS pickup_at
43619
+ FROM public.logistics_pickup_moments p
43620
+ WHERE p.carrier_id = p_carrier_id AND p.moment_date = p_leverdag AND p.pickup_time IS NOT NULL) k2
43621
+ WHERE k2.pdag IS NOT NULL
43622
+ ORDER BY k2.pickup_at ASC LIMIT 1;
43623
+ END IF;
43624
+
43625
+ RETURN jsonb_build_object('ok', true,
43626
+ 'pickupMoment', to_char(r.pdag, 'YYYY-MM-DD') || '|' || to_char(r.pickup_time, 'HH24:MI'),
43627
+ 'momentKey', CASE WHEN r.st IS NOT NULL
43628
+ THEN r.pd::text || '@' || to_char(r.pickup_time, 'HH24:MI') || '|' || COALESCE(r.sd, 0)::text || '@' || to_char(r.st, 'HH24:MI') END,
43629
+ 'pickupAt', r.pickup_at,
43630
+ 'regel', CASE WHEN p_regel = 'laatste_voor_levertijd' AND p_levertijd IS NOT NULL THEN 'laatste_voor_levertijd' ELSE 'eerste' END);
43631
+ END $$;
43632
+ REVOKE ALL ON FUNCTION public.logistics_pickup_moment_kies(uuid, date, timestamptz, text, numeric) FROM PUBLIC, anon;
43633
+ GRANT EXECUTE ON FUNCTION public.logistics_pickup_moment_kies(uuid, date, timestamptz, text, numeric) TO authenticated, service_role;
43634
+
43635
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_cf_nieuw(
43636
+ p_cf jsonb, p_carrier_id uuid, p_delivery_date text, p_deleted_at timestamptz, p_cancelled_at timestamptz,
43637
+ p_plan_locked_at timestamptz, p_regel jsonb DEFAULT NULL)
43638
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
43639
+ DECLARE cf jsonb := COALESCE(p_cf, '{}'::jsonb); v_dag date; v_lt timestamptz; v_regel jsonb; k jsonb; n jsonb;
43640
+ BEGIN
43641
+ IF jsonb_typeof(cf) <> 'object' THEN RETURN NULL; END IF;
43642
+ IF p_deleted_at IS NOT NULL OR p_cancelled_at IS NOT NULL OR p_plan_locked_at IS NOT NULL THEN RETURN NULL; END IF;
43643
+ IF (cf->'backfill') IS NOT NULL AND jsonb_typeof(cf->'backfill') <> 'null'
43644
+ AND (CASE WHEN jsonb_typeof(cf->'backfill') = 'boolean' THEN (cf->>'backfill')::boolean
43645
+ ELSE lower(btrim(cf->>'backfill')) NOT IN ('', 'false', '0', 'nee', 'no') END) THEN
43646
+ RETURN NULL;
43647
+ END IF;
43648
+ IF nullif(btrim(COALESCE(cf->>'pickup_moment', '')), '') IS NOT NULL
43649
+ AND COALESCE(cf->>'pickup_moment_bron', '') <> 'auto' THEN
43650
+ RETURN NULL;
43651
+ END IF;
43652
+ BEGIN v_dag := public.logistics_order_date(p_delivery_date); EXCEPTION WHEN OTHERS THEN v_dag := NULL; END;
43653
+ IF v_dag IS NOT NULL AND v_dag < (now() AT TIME ZONE 'Europe/Amsterdam')::date THEN RETURN NULL; END IF;
43654
+ BEGIN v_lt := nullif(btrim(COALESCE(cf->>'floriday_latest_delivery_at', '')), '')::timestamptz;
43655
+ EXCEPTION WHEN OTHERS THEN v_lt := NULL; END;
43656
+ v_regel := COALESCE(p_regel, public.logistics_pickup_regel());
43657
+ k := public.logistics_pickup_moment_kies(p_carrier_id, v_dag, v_lt, v_regel->>'regel', (v_regel->>'margeUren')::numeric);
43658
+ IF COALESCE((k->>'ok')::boolean, false) THEN
43659
+ n := (cf - 'pickup_moment_reden') || jsonb_build_object('pickup_moment', k->>'pickupMoment', 'pickup_moment_bron', 'auto');
43660
+ n := CASE WHEN k->>'momentKey' IS NOT NULL THEN n || jsonb_build_object('pickup_moment_key', k->>'momentKey')
43661
+ ELSE n - 'pickup_moment_key' END;
43662
+ ELSE
43663
+ n := (cf - 'pickup_moment' - 'pickup_moment_key')
43664
+ || jsonb_build_object('pickup_moment_bron', 'auto', 'pickup_moment_reden', k->>'reden');
43665
+ END IF;
43666
+ IF n = cf THEN RETURN NULL; END IF;
43667
+ RETURN n;
43668
+ END;
43669
+ $$;
43670
+ REVOKE ALL ON FUNCTION public.logistics_pickup_cf_nieuw(jsonb, uuid, text, timestamptz, timestamptz, timestamptz, jsonb) FROM PUBLIC, anon, authenticated;
43671
+ GRANT EXECUTE ON FUNCTION public.logistics_pickup_cf_nieuw(jsonb, uuid, text, timestamptz, timestamptz, timestamptz, jsonb) TO service_role;
43672
+
43673
+ CREATE OR REPLACE FUNCTION public.logistics_orders_pickup_moment()
43674
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
43675
+ DECLARE n jsonb;
43676
+ BEGIN
43677
+ IF COALESCE(current_setting('logistics.pickup_writer', true), '') = 'engine' THEN RETURN NEW; END IF;
43678
+ IF TG_OP = 'UPDATE'
43679
+ AND NEW.carrier_id IS NOT DISTINCT FROM OLD.carrier_id
43680
+ AND NEW.delivery_date IS NOT DISTINCT FROM OLD.delivery_date
43681
+ AND (NEW.custom_fields->>'floriday_latest_delivery_at') IS NOT DISTINCT FROM (OLD.custom_fields->>'floriday_latest_delivery_at')
43682
+ AND NEW.deleted_at IS NOT DISTINCT FROM OLD.deleted_at
43683
+ AND (nullif(btrim(COALESCE(NEW.custom_fields->>'pickup_moment', '')), '') IS NOT NULL
43684
+ OR (NEW.custom_fields->'pickup_moment_reden') IS NOT NULL) THEN
43685
+ RETURN NEW;
43686
+ END IF;
43687
+ BEGIN
43688
+ n := public.logistics_pickup_cf_nieuw(NEW.custom_fields, NEW.carrier_id, NEW.delivery_date::text,
43689
+ NEW.deleted_at, NEW.cancelled_at, NEW.plan_locked_at, NULL);
43690
+ IF n IS NOT NULL THEN NEW.custom_fields := n; END IF;
43691
+ EXCEPTION WHEN OTHERS THEN
43692
+ RAISE WARNING 'logistiek-engine: ophaalmoment bepalen mislukt voor order %: %', NEW.id, SQLERRM;
43693
+ BEGIN
43694
+ PERFORM public._logistics_merge_melding('ophaalmoment_fout', NEW.id, NULL, NULL, 'ophaalmoment bepalen: ' || SQLERRM);
43695
+ EXCEPTION WHEN OTHERS THEN RAISE WARNING 'logistiek-engine: melding ophaalmoment mislukt: %', SQLERRM; END;
43696
+ END;
43697
+ RETURN NEW;
43698
+ END $$;
43699
+ REVOKE ALL ON FUNCTION public.logistics_orders_pickup_moment() FROM PUBLIC, anon, authenticated;
43700
+ DROP TRIGGER IF EXISTS trg_logistics_orders_zpickup_moment ON public.orders;
43701
+ CREATE TRIGGER trg_logistics_orders_zpickup_moment
43702
+ BEFORE INSERT OR UPDATE ON public.orders
43703
+ FOR EACH ROW EXECUTE FUNCTION public.logistics_orders_pickup_moment();
43704
+
43705
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_herwijs(p_carrier_ids uuid[], p_max integer DEFAULT 5000)
43706
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
43707
+ DECLARE r record; n jsonb; v_regel jsonb; v_gewijzigd int := 0; v_fouten jsonb := '[]'::jsonb; v_vorig text;
43708
+ BEGIN
43709
+ IF p_carrier_ids IS NULL OR cardinality(p_carrier_ids) = 0 THEN RETURN jsonb_build_object('gewijzigd', 0); END IF;
43710
+ v_regel := public.logistics_pickup_regel();
43711
+ v_vorig := current_setting('logistics.pickup_writer', true);
43712
+ FOR r IN
43713
+ SELECT o.id, o.order_number, o.custom_fields, o.carrier_id, o.delivery_date::text AS dd, o.deleted_at, o.cancelled_at, o.plan_locked_at
43714
+ FROM public.orders o
43715
+ WHERE o.carrier_id = ANY (p_carrier_ids) AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
43716
+ AND public.logistics_order_date(o.delivery_date::text) >= (now() AT TIME ZONE 'Europe/Amsterdam')::date
43717
+ AND (nullif(btrim(COALESCE(o.custom_fields->>'pickup_moment', '')), '') IS NULL
43718
+ OR COALESCE(o.custom_fields->>'pickup_moment_bron', '') = 'auto')
43719
+ ORDER BY o.delivery_date LIMIT greatest(COALESCE(p_max, 5000), 1)
43720
+ LOOP
43721
+ BEGIN
43722
+ n := public.logistics_pickup_cf_nieuw(r.custom_fields, r.carrier_id, r.dd, r.deleted_at, r.cancelled_at, r.plan_locked_at, v_regel);
43723
+ IF n IS NOT NULL THEN
43724
+ PERFORM set_config('logistics.pickup_writer', 'engine', true);
43725
+ UPDATE public.orders SET custom_fields = n, updated_at = now() WHERE id = r.id;
43726
+ PERFORM set_config('logistics.pickup_writer', COALESCE(v_vorig, ''), true);
43727
+ v_gewijzigd := v_gewijzigd + 1;
43728
+ END IF;
43729
+ EXCEPTION WHEN OTHERS THEN
43730
+ PERFORM set_config('logistics.pickup_writer', COALESCE(v_vorig, ''), true);
43731
+ v_fouten := v_fouten || jsonb_build_array(jsonb_build_object('order', r.order_number, 'fout', SQLERRM));
43732
+ PERFORM public._logistics_merge_melding('ophaalmoment_fout', r.id, NULL, NULL, 'ophaalmoment herzien: ' || SQLERRM);
43733
+ END;
43734
+ END LOOP;
43735
+ RETURN jsonb_build_object('gewijzigd', v_gewijzigd, 'fouten', v_fouten);
43736
+ END $$;
43737
+ REVOKE ALL ON FUNCTION public.logistics_pickup_herwijs(uuid[], integer) FROM PUBLIC, anon, authenticated;
43738
+ GRANT EXECUTE ON FUNCTION public.logistics_pickup_herwijs(uuid[], integer) TO service_role;
43739
+
43740
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_moments_gewijzigd_trg()
43741
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
43742
+ DECLARE v_ids uuid[];
43743
+ BEGIN
43744
+ IF TG_OP = 'DELETE' THEN
43745
+ SELECT array_agg(DISTINCT carrier_id) INTO v_ids FROM oud WHERE carrier_id IS NOT NULL;
43746
+ ELSIF TG_OP = 'INSERT' THEN
43747
+ SELECT array_agg(DISTINCT carrier_id) INTO v_ids FROM nieuw WHERE carrier_id IS NOT NULL;
43748
+ ELSE
43749
+ SELECT array_agg(DISTINCT x) INTO v_ids FROM (
43750
+ SELECT carrier_id x FROM nieuw UNION SELECT carrier_id FROM oud) t WHERE x IS NOT NULL;
43751
+ END IF;
43752
+ BEGIN
43753
+ PERFORM public.logistics_pickup_herwijs(v_ids, 5000);
43754
+ EXCEPTION WHEN OTHERS THEN
43755
+ RAISE WARNING 'logistiek-engine: ophaalmomenten herzien mislukt: %', SQLERRM;
43756
+ PERFORM public._logistics_merge_melding('ophaalmoment_fout', NULL, NULL, NULL, 'ophaalmomenten herzien: ' || SQLERRM);
43757
+ END;
43758
+ RETURN NULL;
43759
+ END $$;
43760
+ REVOKE ALL ON FUNCTION public.logistics_pickup_moments_gewijzigd_trg() FROM PUBLIC, anon, authenticated;
43761
+ DROP TRIGGER IF EXISTS trg_logistics_pickup_moments_ins ON public.logistics_pickup_moments;
43762
+ DROP TRIGGER IF EXISTS trg_logistics_pickup_moments_upd ON public.logistics_pickup_moments;
43763
+ DROP TRIGGER IF EXISTS trg_logistics_pickup_moments_del ON public.logistics_pickup_moments;
43764
+ CREATE TRIGGER trg_logistics_pickup_moments_ins AFTER INSERT ON public.logistics_pickup_moments
43765
+ REFERENCING NEW TABLE AS nieuw FOR EACH STATEMENT EXECUTE FUNCTION public.logistics_pickup_moments_gewijzigd_trg();
43766
+ CREATE TRIGGER trg_logistics_pickup_moments_upd AFTER UPDATE ON public.logistics_pickup_moments
43767
+ REFERENCING NEW TABLE AS nieuw OLD TABLE AS oud FOR EACH STATEMENT EXECUTE FUNCTION public.logistics_pickup_moments_gewijzigd_trg();
43768
+ CREATE TRIGGER trg_logistics_pickup_moments_del AFTER DELETE ON public.logistics_pickup_moments
43769
+ REFERENCING OLD TABLE AS oud FOR EACH STATEMENT EXECUTE FUNCTION public.logistics_pickup_moments_gewijzigd_trg();
43770
+
43771
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_vul_voorbeeld()
43772
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
43773
+ DECLARE v_regel jsonb := public.logistics_pickup_regel(); v jsonb;
43774
+ BEGIN
43775
+ WITH o AS (
43776
+ SELECT o.id, o.order_number, o.carrier_id, public.logistics_order_date(o.delivery_date::text) AS dag,
43777
+ public.logistics_pickup_cf_nieuw(o.custom_fields, o.carrier_id, o.delivery_date::text, o.deleted_at, o.cancelled_at, o.plan_locked_at, v_regel) AS n
43778
+ FROM public.orders o
43779
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
43780
+ AND public.logistics_order_date(o.delivery_date::text) >= (now() AT TIME ZONE 'Europe/Amsterdam')::date
43781
+ AND NOT public.logistics_order_is_backfill(o.id)
43782
+ AND (nullif(btrim(COALESCE(o.custom_fields->>'pickup_moment', '')), '') IS NULL
43783
+ OR COALESCE(o.custom_fields->>'pickup_moment_bron', '') = 'auto')
43784
+ ), u AS (
43785
+ SELECT o.*, COALESCE(c.name, '(geen vervoerder)') AS vervoerder,
43786
+ CASE WHEN o.n IS NULL THEN 'ongewijzigd'
43787
+ WHEN (o.n->'pickup_moment') IS NOT NULL THEN 'krijgt moment'
43788
+ ELSE 'geen moment: ' || COALESCE(o.n->>'pickup_moment_reden', '?') END AS uitkomst
43789
+ FROM o LEFT JOIN public.carriers c ON c.id = o.carrier_id
43790
+ )
43791
+ SELECT jsonb_build_object('regel', v_regel,
43792
+ 'totaal', (SELECT count(*) FROM u),
43793
+ 'krijgtMoment', (SELECT count(*) FROM u WHERE uitkomst = 'krijgt moment'),
43794
+ 'ongewijzigd', (SELECT count(*) FROM u WHERE uitkomst = 'ongewijzigd'),
43795
+ 'zonderMoment', (SELECT count(*) FROM u WHERE uitkomst LIKE 'geen moment%'),
43796
+ 'perVervoerderEnDag', COALESCE((SELECT jsonb_agg(jsonb_build_object('vervoerder', vervoerder, 'leverdag', dag,
43797
+ 'uitkomst', regexp_replace(uitkomst, ' op \d{2}-\d{2}-\d{4}\.$', ''), 'aantal', n, 'voorbeelden', vb)
43798
+ ORDER BY dag, vervoerder)
43799
+ FROM (SELECT vervoerder, dag, uitkomst, count(*) n, (array_agg(order_number ORDER BY order_number))[1:3] vb
43800
+ FROM u WHERE uitkomst <> 'ongewijzigd' GROUP BY 1, 2, 3) g), '[]'::jsonb))
43801
+ INTO v;
43802
+ RETURN v;
43803
+ END $$;
43804
+ REVOKE ALL ON FUNCTION public.logistics_pickup_vul_voorbeeld() FROM PUBLIC, anon, authenticated;
43805
+ GRANT EXECUTE ON FUNCTION public.logistics_pickup_vul_voorbeeld() TO service_role;
43806
+
43807
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_vul_uitvoeren(p_max integer DEFAULT 500)
43808
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
43809
+ DECLARE r record; n jsonb; v_regel jsonb; v_gevuld int := 0; v_zonder int := 0; v_bekeken int := 0;
43810
+ v_fouten jsonb := '[]'::jsonb; v_vorig text; v_rest int;
43811
+ BEGIN
43812
+ PERFORM public.logistics_pickup_moments_koppel();
43813
+ v_regel := public.logistics_pickup_regel();
43814
+ v_vorig := current_setting('logistics.pickup_writer', true);
43815
+ FOR r IN
43816
+ SELECT o.id, o.order_number, o.custom_fields, o.carrier_id, o.delivery_date::text AS dd, o.deleted_at, o.cancelled_at, o.plan_locked_at
43817
+ FROM public.orders o
43818
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
43819
+ AND public.logistics_order_date(o.delivery_date::text) >= (now() AT TIME ZONE 'Europe/Amsterdam')::date
43820
+ AND NOT public.logistics_order_is_backfill(o.id)
43821
+ AND (nullif(btrim(COALESCE(o.custom_fields->>'pickup_moment', '')), '') IS NULL
43822
+ OR COALESCE(o.custom_fields->>'pickup_moment_bron', '') = 'auto')
43823
+ ORDER BY o.delivery_date, o.order_number
43824
+ LOOP
43825
+ EXIT WHEN v_gevuld + v_zonder >= greatest(COALESCE(p_max, 500), 1);
43826
+ v_bekeken := v_bekeken + 1;
43827
+ BEGIN
43828
+ n := public.logistics_pickup_cf_nieuw(r.custom_fields, r.carrier_id, r.dd, r.deleted_at, r.cancelled_at, r.plan_locked_at, v_regel);
43829
+ IF n IS NOT NULL THEN
43830
+ PERFORM set_config('logistics.pickup_writer', 'engine', true);
43831
+ UPDATE public.orders SET custom_fields = n, updated_at = now() WHERE id = r.id;
43832
+ PERFORM set_config('logistics.pickup_writer', COALESCE(v_vorig, ''), true);
43833
+ IF (n->'pickup_moment') IS NOT NULL THEN v_gevuld := v_gevuld + 1; ELSE v_zonder := v_zonder + 1; END IF;
43834
+ END IF;
43835
+ EXCEPTION WHEN OTHERS THEN
43836
+ PERFORM set_config('logistics.pickup_writer', COALESCE(v_vorig, ''), true);
43837
+ v_fouten := v_fouten || jsonb_build_array(jsonb_build_object('order', r.order_number, 'fout', SQLERRM));
43838
+ END;
43839
+ END LOOP;
43840
+ v_rest := COALESCE((public.logistics_pickup_vul_voorbeeld()->>'krijgtMoment')::int, 0);
43841
+ RETURN jsonb_build_object('gevuld', v_gevuld, 'zonderMomentGemarkeerd', v_zonder, 'bekeken', v_bekeken,
43842
+ 'fouten', v_fouten, 'nogTeDoen', v_rest);
43843
+ END $$;
43844
+ REVOKE ALL ON FUNCTION public.logistics_pickup_vul_uitvoeren(integer) FROM PUBLIC, anon, authenticated;
43845
+ GRANT EXECUTE ON FUNCTION public.logistics_pickup_vul_uitvoeren(integer) TO service_role;
43846
+
43847
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_status()
43848
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
43849
+ DECLARE v_vandaag date := (now() AT TIME ZONE 'Europe/Amsterdam')::date; v_tot date; v_heeft boolean; v jsonb; v_regel jsonb;
43850
+ BEGIN
43851
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_pickup_status');
43852
+ v_regel := public.logistics_pickup_regel();
43853
+ SELECT max(moment_date) INTO v_tot FROM public.logistics_pickup_moments;
43854
+ SELECT EXISTS (SELECT 1 FROM public.carriers c WHERE c.archived_at IS NULL
43855
+ AND jsonb_typeof(c.pickup_moments) = 'array' AND jsonb_array_length(c.pickup_moments) > 0) INTO v_heeft;
43856
+ WITH o AS (
43857
+ SELECT o.order_number, o.carrier_id, public.logistics_order_date(o.delivery_date::text) AS dag, o.custom_fields cf
43858
+ FROM public.orders o
43859
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
43860
+ AND public.logistics_order_date(o.delivery_date::text) BETWEEN v_vandaag AND v_vandaag + 6
43861
+ AND NOT public.logistics_order_is_backfill(o.id)
43862
+ AND lower(btrim(COALESCE(o.status, ''))) NOT IN ('verzonden', 'afgehandeld', 'geannuleerd', 'cancelled', 'canceled')
43863
+ AND nullif(btrim(COALESCE(o.custom_fields->>'pickup_moment', '')), '') IS NULL
43864
+ ), r AS (
43865
+ SELECT o.*, public.logistics_pickup_moment_kies(o.carrier_id, o.dag,
43866
+ CASE WHEN o.cf->>'floriday_latest_delivery_at' ~ '^\d{4}-' THEN (o.cf->>'floriday_latest_delivery_at')::timestamptz END,
43867
+ v_regel->>'regel', (v_regel->>'margeUren')::numeric) AS k
43868
+ FROM o
43869
+ )
43870
+ SELECT jsonb_build_object(
43871
+ 'zonderMoment', (SELECT count(*) FROM r),
43872
+ 'redenen', COALESCE((SELECT jsonb_object_agg(code, n) FROM (
43873
+ SELECT COALESCE(CASE WHEN (k->>'ok')::boolean THEN 'nog_niet_gevuld' END, k->>'redenCode', 'onbekend') code, count(*) n
43874
+ FROM r GROUP BY 1) g), '{}'::jsonb),
43875
+ 'voorbeelden', COALESCE((SELECT jsonb_agg(order_number) FROM (SELECT order_number FROM r ORDER BY dag, order_number LIMIT 5) x), '[]'::jsonb),
43876
+ 'momentenTot', v_tot,
43877
+ 'momentenTeKort', v_heeft AND (v_tot IS NULL OR v_tot < v_vandaag + 14),
43878
+ 'ongekoppeld', (SELECT count(*) FROM public.logistics_pickup_moments p WHERE p.moment_date >= v_vandaag AND p.carrier_id IS NULL),
43879
+ 'regel', v_regel)
43880
+ INTO v;
43881
+ RETURN v;
43882
+ END $$;
43883
+ REVOKE ALL ON FUNCTION public.logistics_pickup_status() FROM PUBLIC, anon;
43884
+ GRANT EXECUTE ON FUNCTION public.logistics_pickup_status() TO authenticated, service_role;
43885
+
43886
+ CREATE OR REPLACE FUNCTION public.logistics_refresh_pickup_moments(p_weeks integer DEFAULT 4)
43887
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
43888
+ declare
43889
+ v_weeks integer := greatest(1, least(coalesce(p_weeks, 4), 26));
43890
+ v_from date := current_date;
43891
+ v_to date := current_date + (v_weeks * 7);
43892
+ v_inserted integer := 0;
43893
+ v_updated integer := 0;
43894
+ v_removed integer := 0;
43895
+ begin
43896
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_refresh_pickup_moments');
43897
+ create temp table _wanted on commit drop as
43898
+ with dagen as (
43899
+ select d::date as moment_date from generate_series(v_from, v_to, interval '1 day') d
43900
+ ),
43901
+ momenten as (
43902
+ select c.user_id, c.id as carrier_id, c.name as carrier_ref, c.pickup_weekdays,
43903
+ nullif(m->>'pickup_time','')::time as pickup_time,
43904
+ greatest(coalesce(nullif(m->>'pickup_days','')::int, 0), 0) as pickup_days,
43905
+ greatest(coalesce(nullif(m->>'signup_days','')::int, 0), 0) as signup_days,
43906
+ nullif(m->>'signup_time','')::time as signup_time,
43907
+ nullif(m->>'label','') as label
43908
+ from public.carriers c
43909
+ cross join lateral jsonb_array_elements(
43910
+ case when jsonb_typeof(c.pickup_moments) = 'array' then c.pickup_moments else '[]'::jsonb end) m
43911
+ where c.archived_at is null
43912
+ ),
43913
+ geldig as (
43914
+ select d.moment_date, mo.user_id, mo.carrier_id, mo.carrier_ref, mo.pickup_time, mo.pickup_days,
43915
+ mo.signup_days, mo.signup_time, mo.label
43916
+ from dagen d
43917
+ join momenten mo
43918
+ on mo.pickup_time is not null
43919
+ and (mo.pickup_weekdays is null or array_length(mo.pickup_weekdays, 1) is null
43920
+ or extract(isodow from d.moment_date)::int = any(mo.pickup_weekdays))
43921
+ ),
43922
+ uniek as (
43923
+ select distinct on (user_id, moment_date, carrier_id, pickup_time)
43924
+ user_id, moment_date, carrier_id, carrier_ref, pickup_time, pickup_days, signup_days, signup_time, label
43925
+ from geldig
43926
+ order by user_id, moment_date, carrier_id, pickup_time, pickup_days, signup_time
43927
+ )
43928
+ select u.*,
43929
+ row_number() over (partition by user_id, moment_date order by pickup_time, carrier_ref, carrier_id)::int as sequence
43930
+ from uniek u;
43931
+
43932
+ if not exists (select 1 from _wanted) then
43933
+ return jsonb_build_object('ok', true, 'reason', 'geen_ophaalmomenten_ingesteld', 'inserted', 0, 'updated', 0, 'removed', 0);
43934
+ end if;
43935
+
43936
+ with weg as (
43937
+ delete from public.logistics_pickup_moments p
43938
+ where p.moment_date between v_from and v_to
43939
+ and not exists (select 1 from _wanted w
43940
+ where w.user_id = p.user_id and w.moment_date = p.moment_date and w.sequence = p.sequence)
43941
+ returning 1
43942
+ )
43943
+ select count(*) into v_removed from weg;
43944
+
43945
+ with upd as (
43946
+ update public.logistics_pickup_moments p
43947
+ set pickup_time = w.pickup_time, carrier_ref = w.carrier_ref, carrier_id = w.carrier_id,
43948
+ pickup_days = w.pickup_days, signup_days = w.signup_days, signup_time = w.signup_time,
43949
+ label = w.label, updated_at = now()
43950
+ from _wanted w
43951
+ where p.user_id = w.user_id and p.moment_date = w.moment_date and p.sequence = w.sequence
43952
+ and (p.pickup_time is distinct from w.pickup_time or p.carrier_ref is distinct from w.carrier_ref
43953
+ or p.carrier_id is distinct from w.carrier_id or p.pickup_days is distinct from w.pickup_days
43954
+ or p.signup_days is distinct from w.signup_days or p.signup_time is distinct from w.signup_time
43955
+ or p.label is distinct from w.label)
43956
+ returning 1
43957
+ )
43958
+ select count(*) into v_updated from upd;
43959
+
43960
+ with ins as (
43961
+ insert into public.logistics_pickup_moments
43962
+ (user_id, moment_date, sequence, pickup_time, carrier_ref, carrier_id, pickup_days, signup_days, signup_time, label)
43963
+ select w.user_id, w.moment_date, w.sequence, w.pickup_time, w.carrier_ref, w.carrier_id, w.pickup_days, w.signup_days, w.signup_time, w.label
43964
+ from _wanted w
43965
+ where not exists (select 1 from public.logistics_pickup_moments p
43966
+ where p.user_id = w.user_id and p.moment_date = w.moment_date and p.sequence = w.sequence)
43967
+ returning 1
43968
+ )
43969
+ select count(*) into v_inserted from ins;
43970
+
43971
+ return jsonb_build_object('ok', true, 'inserted', v_inserted, 'updated', v_updated, 'removed', v_removed,
43972
+ 'from', v_from, 'to', v_to);
43973
+ end $$;
43974
+
43975
+ CREATE OR REPLACE FUNCTION public.logistics_order_buffer_ref(p_order_id uuid)
43976
+ RETURNS text LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
43977
+ DECLARE d jsonb;
43978
+ BEGIN
43979
+ d := public.logistics_buffer_decision(public.logistics_buffer_payload(p_order_id), NULL);
43980
+ IF COALESCE((d->>'buffer')::boolean, false) IS NOT TRUE OR NOT COALESCE((d->>'complete')::boolean, false) THEN RETURN NULL; END IF;
43981
+ RETURN nullif(d->>'bufferRef', '');
43982
+ END $$;
43983
+ REVOKE ALL ON FUNCTION public.logistics_order_buffer_ref(uuid) FROM PUBLIC, anon, authenticated;
43984
+ GRANT EXECUTE ON FUNCTION public.logistics_order_buffer_ref(uuid) TO service_role;
43985
+
43986
+ CREATE OR REPLACE FUNCTION public.logistics_merge_floriday_lines(p_order_id uuid)
43987
+ RETURNS uuid LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
43988
+ DECLARE
43989
+ v_ref text; v_key text; v_keep uuid; v_keepnr text; v_cust uuid; v_dag date; r record; v_ids uuid[];
43990
+ v_moved int; v_logged int; v_total int := 0; v_count int := 0; v_groep int; s jsonb;
43991
+ v_lines text[] := ARRAY[]::text[];
43992
+ BEGIN
43993
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_merge_floriday_lines');
43994
+ IF p_order_id IS NULL THEN RETURN p_order_id; END IF;
43995
+ IF COALESCE(current_setting('logistics.merge_uit', true), '') = '1' THEN RETURN p_order_id; END IF;
43996
+ IF NOT public.logistics_merge_lines_enabled() THEN RETURN p_order_id; END IF;
43997
+ SELECT c.settings INTO s FROM public.logistics_rule_configs c WHERE c.is_active ORDER BY c.updated_at DESC LIMIT 1;
43998
+ IF NOT public.logistics_rule_bool(COALESCE(s, '{}'::jsonb), 'order_verwerking', 'buffer_enabled', true) THEN RETURN p_order_id; END IF;
43999
+ IF NOT public.logistics_order_mergeable(p_order_id) THEN RETURN p_order_id; END IF;
44000
+ IF NOT EXISTS (SELECT 1 FROM logistics_active.order_items i WHERE i.order_id = p_order_id) THEN RETURN p_order_id; END IF;
44001
+
44002
+ v_ref := public.logistics_order_buffer_ref(p_order_id);
44003
+ IF v_ref IS NULL THEN RETURN p_order_id; END IF;
44004
+ SELECT customer_id, public.logistics_order_date(delivery_date::text) INTO v_cust, v_dag FROM public.orders WHERE id = p_order_id;
44005
+ IF v_cust IS NULL OR v_dag IS NULL THEN RETURN p_order_id; END IF;
44006
+ v_key := 'buf|' || v_ref || '|' || v_cust::text || '|' || v_dag::text;
44007
+ IF NOT pg_try_advisory_xact_lock(hashtext('logistics_merge:' || v_key)) THEN RETURN p_order_id; END IF;
44008
+
44009
+ SELECT array_agg(o.id ORDER BY o.order_number ASC NULLS LAST, o.created_at ASC) INTO v_ids
44010
+ FROM public.orders o
44011
+ WHERE o.customer_id = v_cust AND o.deleted_at IS NULL AND o.order_source = 'floriday'
44012
+ AND public.logistics_order_date(o.delivery_date::text) = v_dag
44013
+ AND EXISTS (SELECT 1 FROM logistics_active.order_items i WHERE i.order_id = o.id)
44014
+ AND public.logistics_order_mergeable(o.id)
44015
+ AND public.logistics_order_buffer_ref(o.id) = v_ref;
44016
+ v_groep := COALESCE(cardinality(v_ids), 0);
44017
+ IF v_groep < 2 THEN RETURN p_order_id; END IF;
44018
+ v_keep := v_ids[1];
44019
+ v_groep := v_groep + (SELECT count(*) FROM public.logistics_order_merges m WHERE m.kept_order_id = v_keep AND m.group_key = v_key);
44020
+ IF v_groep > 20 THEN
44021
+ IF NOT EXISTS (SELECT 1 FROM public.logistics_merge_meldingen m
44022
+ WHERE m.soort = 'groep_te_groot' AND m.group_key = v_key AND m.created_at > now() - interval '1 day') THEN
44023
+ PERFORM public._logistics_merge_melding('groep_te_groot', p_order_id, v_key, v_groep,
44024
+ v_groep || ' orders in dezelfde buffergroep; niet samengevoegd.');
44025
+ END IF;
44026
+ RETURN p_order_id;
44027
+ END IF;
44028
+
44029
+ SELECT order_number INTO v_keepnr FROM public.orders WHERE id = v_keep;
44030
+ PERFORM set_config('logistics.merge_uit', '1', true);
44031
+
44032
+ FOR r IN SELECT o.id, o.order_number, o.floriday_sales_order_id AS sales, o.created_at
44033
+ FROM public.orders o WHERE o.id = ANY (v_ids) AND o.id <> v_keep
44034
+ ORDER BY o.order_number ASC NULLS LAST LOOP
44035
+ BEGIN
44036
+ INSERT INTO public.logistics_order_merge_lines(
44037
+ group_key, from_order_id, from_order_number, to_order_id, to_order_number,
44038
+ order_item_id, product_id, product_name, quantity, source_line_key,
44039
+ sales_order_id, ordered_at, reason)
44040
+ SELECT v_key, r.id, r.order_number, v_keep, v_keepnr,
44041
+ i.id, i.product_id, i.product_name, i.quantity, i.source_line_key,
44042
+ r.sales, r.created_at, 'Zelfde buffergroep volgens de werkwijze'
44043
+ FROM logistics_active.order_items i WHERE i.order_id = r.id;
44044
+ GET DIAGNOSTICS v_logged = ROW_COUNT;
44045
+ UPDATE public.order_items SET order_id = v_keep WHERE order_id = r.id AND quarantined_at IS NULL;
44046
+ GET DIAGNOSTICS v_moved = ROW_COUNT;
44047
+ IF v_moved <> v_logged THEN RAISE EXCEPTION 'aantal verplaatste regels (%) wijkt af van het logboek (%)', v_moved, v_logged; END IF;
44048
+ IF v_moved = 0 THEN RAISE EXCEPTION 'geen regels verplaatst; order blijft staan'; END IF;
44049
+ INSERT INTO public.logistics_order_merges(group_key, kept_order_id, kept_order_number,
44050
+ merged_order_id, merged_order_number, sales_order_id, items_moved)
44051
+ VALUES (v_key, v_keep, v_keepnr, r.id, r.order_number, r.sales, v_moved)
44052
+ ON CONFLICT (merged_order_id) DO NOTHING;
44053
+ UPDATE public.orders SET deleted_at = now(),
44054
+ notes = COALESCE(NULLIF(notes, ''), '')
44055
+ || CASE WHEN COALESCE(notes, '') = '' THEN '' ELSE E'\n' END
44056
+ || 'Samengevoegd met ' || COALESCE(v_keepnr, '') || '.',
44057
+ updated_at = now()
44058
+ WHERE id = r.id;
44059
+ v_total := v_total + v_moved; v_count := v_count + 1;
44060
+ v_lines := v_lines || (COALESCE(r.order_number, '') || ': ' || v_moved::text || ' regel(s)');
44061
+ EXCEPTION WHEN OTHERS THEN
44062
+ PERFORM public._logistics_merge_melding('fout', r.id, v_key, NULL,
44063
+ 'samenvoegen ' || COALESCE(r.order_number, '?') || ' met ' || COALESCE(v_keepnr, '?') || ': ' || SQLERRM);
44064
+ END;
44065
+ END LOOP;
44066
+ PERFORM set_config('logistics.merge_uit', '0', true);
44067
+
44068
+ IF v_count > 0 THEN
44069
+ UPDATE public.orders o SET total = to_char(COALESCE((
44070
+ SELECT SUM(COALESCE(i.quantity, 0) * COALESCE(i.unit_price, 0))
44071
+ FROM logistics_active.order_items i WHERE i.order_id = o.id), 0), 'FM9999999990.00'),
44072
+ updated_at = now()
44073
+ WHERE o.id = v_keep;
44074
+ PERFORM public.logistics_trace_order_step(
44075
+ v_keep, 'stap:bestelregels_samenvoegen', 'bestelregels samenvoegen', 'geslaagd',
44076
+ 'samengevoegd tot ' || COALESCE(v_keepnr, 'één bestelling'),
44077
+ v_count::text || ' order(s), ' || v_total::text || ' regel(s) verplaatst: ' || array_to_string(v_lines, '; '),
44078
+ 'Orders combineren');
44079
+ END IF;
44080
+ IF EXISTS (SELECT 1 FROM public.orders WHERE id = p_order_id AND deleted_at IS NOT NULL) THEN RETURN v_keep; END IF;
44081
+ RETURN p_order_id;
44082
+ EXCEPTION WHEN OTHERS THEN
44083
+ PERFORM set_config('logistics.merge_uit', '0', true);
44084
+ PERFORM public._logistics_merge_melding('fout', p_order_id, v_key, NULL, 'samenvoegen: ' || SQLERRM);
44085
+ RETURN p_order_id;
44086
+ END $$;
44087
+
44088
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_samenvoegen()
44089
+ RETURNS TABLE(onderdeel text, ok boolean, toelichting text)
44090
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
44091
+ AS $function$
44092
+ DECLARE f text;
44093
+ BEGIN
44094
+ FOREACH f IN ARRAY ARRAY['logistics_order_is_backfill(uuid)', 'logistics_order_merge_key_v2(uuid)',
44095
+ 'logistics_order_mergeable(uuid)', 'logistics_merge_floriday_lines(uuid)', 'logistics_order_buffer_ref(uuid)',
44096
+ 'logistics_merge_herstel_voorbeeld()', 'logistics_merge_herstel_uitvoeren(text,integer)',
44097
+ 'logistics_merge_status()', 'logistics_trace_event(uuid,text,text,text)', 'logistics_schema_versie()',
44098
+ 'logistics_pickup_regel()', 'logistics_pickup_moment_kies(uuid,date,timestamp with time zone,text,numeric)',
44099
+ 'logistics_pickup_vul_voorbeeld()', 'logistics_pickup_vul_uitvoeren(integer)', 'logistics_pickup_status()',
44100
+ 'logistics_pickup_herwijs(uuid[],integer)', 'logistics_refresh_pickup_moments(integer)'] LOOP
44101
+ onderdeel := 'functie ' || f;
44102
+ ok := to_regprocedure('public.' || f) IS NOT NULL;
44103
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: werk de Engine bij via Supabase bijwerken' END;
44104
+ RETURN NEXT;
44105
+ END LOOP;
44106
+ onderdeel := 'tabel logistics_merge_herstel_log';
44107
+ ok := to_regclass('public.logistics_merge_herstel_log') IS NOT NULL;
44108
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: werk de Engine bij via Supabase bijwerken' END;
44109
+ RETURN NEXT;
44110
+ onderdeel := 'kolom logistics_pickup_moments.carrier_id';
44111
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public'
44112
+ AND table_name = 'logistics_pickup_moments' AND column_name = 'carrier_id');
44113
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: werk de Engine bij via Supabase bijwerken' END;
44114
+ RETURN NEXT;
44115
+ onderdeel := 'trigger ophaalmoment op orders';
44116
+ ok := EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_logistics_orders_zpickup_moment');
44117
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: werk de Engine bij via Supabase bijwerken' END;
44118
+ RETURN NEXT;
44119
+ END $function$;
44120
+
44121
+ CREATE OR REPLACE FUNCTION public.logistics_schema_versie()
44122
+ RETURNS text LANGUAGE sql IMMUTABLE SET search_path TO 'public'
44123
+ AS $function$ SELECT '2026-11-10c'::text $function$;
44124
+
44125
+ CREATE OR REPLACE FUNCTION public.logistics_buffer_decision_for_settings(p_order jsonb, p_settings jsonb, p_user_id uuid DEFAULT NULL::uuid)
44126
+ RETURNS jsonb
44127
+ LANGUAGE plpgsql
44128
+ STABLE
44129
+ SET search_path TO 'public'
44130
+ AS $function$
44131
+ DECLARE
44132
+ v_user uuid := p_user_id;
44133
+ v_settings jsonb := COALESCE(p_settings, '{}'::jsonb);
44134
+ v_enabled boolean; v_include_res boolean; v_is_res boolean;
44135
+ v_minutes int; v_reference text; v_release text; v_criteria text[];
44136
+ v_checkin timestamptz; v_pick_start timestamptz; v_close timestamptz;
44137
+ v_key text; v_parts text[] := ARRAY[]::text[]; v_trace jsonb := '[]'::jsonb;
44138
+ v_missing boolean := false; v_missing_list text[] := ARRAY[]::text[];
44139
+ v_day text; v_ref text; c text; eff text; val text;
44140
+ v_is_fd boolean; v_skip text[]; v_skip_raw jsonb;
44141
+ BEGIN
44142
+ p_order := COALESCE(p_order, '{}'::jsonb);
44143
+ v_enabled := public.logistics_rule_bool(v_settings, 'order_verwerking', 'buffer_enabled', true);
44144
+ v_include_res := public.logistics_rule_bool(v_settings, 'order_verwerking', 'buffer_include_reservations', false);
44145
+ v_minutes := public.logistics_rule_num(v_settings, 'order_verwerking', 'buffer_close_before_minutes', 60)::int;
44146
+ v_reference := public.logistics_rule_text(v_settings, 'order_verwerking', 'buffer_close_reference', 'checkin');
44147
+ v_release := public.logistics_rule_text(v_settings, 'order_verwerking', 'buffer_release', 'auto');
44148
+ v_criteria := public.logistics_buffer_criteria(v_settings);
44149
+
44150
+ v_skip_raw := public.logistics_rule_value(v_settings, 'order_verwerking', 'buffer_skip_for_floriday');
44151
+ IF v_skip_raw IS NOT NULL AND jsonb_typeof(v_skip_raw) = 'array' THEN
44152
+ v_skip := ARRAY(SELECT jsonb_array_elements_text(v_skip_raw));
44153
+ ELSE
44154
+ -- 1.0.239: geen verborgen standaard meer; alleen overslaan wat de werkwijze zelf aangeeft.
44155
+ v_skip := ARRAY[]::text[];
44156
+ END IF;
44157
+
44158
+ v_is_fd := NULLIF(TRIM(COALESCE(p_order ->> 'floridaySalesOrderRef', p_order ->> 'floriday_sales_order_id', '')), '') IS NOT NULL;
44159
+ v_is_res := COALESCE((p_order ->> 'isReservation')::boolean, (p_order ->> 'is_reservation')::boolean, false);
44160
+ v_ref := COALESCE(NULLIF(p_order ->> 'orderRef', ''), NULLIF(p_order ->> 'order_ref', ''), md5(p_order::text));
44161
+
44162
+ IF NOT v_enabled THEN
44163
+ RETURN jsonb_build_object('buffer', false, 'reason', 'buffering_disabled', 'bufferRef', NULL, 'bufferKey', NULL,
44164
+ 'matchCriteria', to_jsonb(v_criteria), 'parts', '[]'::jsonb, 'complete', true, 'missingCriteria', '[]'::jsonb,
44165
+ 'isReservation', v_is_res, 'isFloriday', v_is_fd, 'closeBeforeMinutes', NULL, 'closeReference', v_reference,
44166
+ 'bufferCloseAt', NULL, 'release', 'none',
44167
+ 'trace', jsonb_build_array(jsonb_build_object('rule', 'buffer_enabled', 'result', false,
44168
+ 'message', 'Orderbuffer staat uit in de Rule Engine; elke order wordt los verwerkt.')));
44169
+ END IF;
44170
+
44171
+ IF v_is_res AND NOT v_include_res THEN
44172
+ RETURN jsonb_build_object('buffer', false, 'reason', 'reservation_excluded', 'bufferRef', NULL, 'bufferKey', NULL,
44173
+ 'matchCriteria', to_jsonb(v_criteria), 'parts', '[]'::jsonb, 'complete', true, 'missingCriteria', '[]'::jsonb,
44174
+ 'isReservation', true, 'isFloriday', v_is_fd, 'closeBeforeMinutes', NULL, 'closeReference', v_reference,
44175
+ 'bufferCloseAt', NULL, 'release', 'none',
44176
+ 'trace', jsonb_build_array(jsonb_build_object('rule', 'buffer_include_reservations', 'result', false,
44177
+ 'message', 'Gereserveerde bestellingen worden volgens de instelling buiten de buffers gehouden.')));
44178
+ END IF;
44179
+
44180
+ BEGIN v_checkin := (p_order ->> 'checkinAt')::timestamptz; EXCEPTION WHEN others THEN v_checkin := NULL; END;
44181
+
44182
+ IF v_reference = 'pick_start' THEN
44183
+ v_pick_start := public.logistics_pick_start_estimate(v_settings, p_order);
44184
+ IF v_pick_start IS NOT NULL THEN
44185
+ v_close := v_pick_start - make_interval(mins => v_minutes);
44186
+ v_trace := v_trace || jsonb_build_array(jsonb_build_object('rule', 'buffer_close_reference', 'value', 'pick_start', 'result', true,
44187
+ 'message', 'De buffer sluit ' || v_minutes || ' minuten vóór de start van het picken.'));
44188
+ ELSIF v_checkin IS NOT NULL THEN
44189
+ v_close := v_checkin - make_interval(mins => v_minutes);
44190
+ v_trace := v_trace || jsonb_build_array(jsonb_build_object('rule', 'buffer_close_reference', 'value', 'checkin_fallback', 'result', true,
44191
+ 'message', 'De pickstart is nog niet te berekenen; er wordt teruggerekend vanaf de aanmeldtijd.'));
44192
+ END IF;
44193
+ ELSIF v_checkin IS NOT NULL THEN
44194
+ v_close := v_checkin - make_interval(mins => v_minutes);
44195
+ END IF;
44196
+
44197
+ v_day := LEFT(COALESCE(p_order ->> 'deliveryDate', p_order ->> 'delivery_date', p_order ->> 'checkinAt', ''), 10);
44198
+ IF v_day <> '' THEN
44199
+ v_parts := v_parts || ('day=' || v_day);
44200
+ v_trace := v_trace || jsonb_build_array(jsonb_build_object('rule', 'day_bucket', 'value', v_day, 'result', true,
44201
+ 'message', 'Orders worden alleen binnen dezelfde dag gecombineerd.'));
44202
+ END IF;
44203
+
44204
+ IF array_length(v_criteria, 1) IS NULL THEN
44205
+ val := NULLIF(TRIM(COALESCE(p_order ->> 'customerRef', '')), '');
44206
+ IF val IS NULL THEN
44207
+ v_missing := true; v_missing_list := v_missing_list || 'customer'::text;
44208
+ v_parts := v_parts || ('missing=customer:' || v_ref);
44209
+ v_trace := v_trace || jsonb_build_array(jsonb_build_object('rule', 'no_criteria_selected', 'value', NULL, 'result', false,
44210
+ 'message', 'Geen combineercriteria aangevinkt en de klant ontbreekt; deze order wordt niet gecombineerd.'));
44211
+ ELSE
44212
+ v_parts := v_parts || ('customer=' || val);
44213
+ v_trace := v_trace || jsonb_build_array(jsonb_build_object('rule', 'no_criteria_selected', 'result', false,
44214
+ 'message', 'Er zijn geen combineercriteria aangevinkt; er wordt teruggevallen op dezelfde klant.'));
44215
+ END IF;
44216
+ ELSE
44217
+ FOREACH c IN ARRAY v_criteria LOOP
44218
+ eff := c;
44219
+ IF v_is_fd AND c = ANY(v_skip) THEN
44220
+ v_trace := v_trace || jsonb_build_array(jsonb_build_object('rule', c, 'value', NULL, 'result', true,
44221
+ 'message', 'Niet van toepassing bij Floriday-orders; Floriday levert deze waarde niet.'));
44222
+ CONTINUE;
44223
+ END IF;
44224
+ IF v_is_fd AND c = 'customer_bvo' THEN
44225
+ eff := 'customer_floriday';
44226
+ v_trace := v_trace || jsonb_build_array(jsonb_build_object('rule', c, 'value', NULL, 'result', true,
44227
+ 'message', 'Bij Floriday-orders wordt de Floriday-koper gebruikt in plaats van de BVO-klant.'));
44228
+ END IF;
44229
+ val := public.logistics_buffer_value(p_order, eff);
44230
+ IF val IS NULL THEN
44231
+ v_missing := true; v_missing_list := v_missing_list || eff;
44232
+ v_parts := v_parts || ('missing=' || eff || ':' || v_ref);
44233
+ v_trace := v_trace || jsonb_build_array(jsonb_build_object('rule', eff, 'value', NULL, 'result', false,
44234
+ 'message', 'Waarde ontbreekt; deze order wordt niet gecombineerd.'));
44235
+ ELSE
44236
+ v_parts := v_parts || (eff || '=' || lower(val));
44237
+ v_trace := v_trace || jsonb_build_array(jsonb_build_object('rule', eff, 'value', val, 'result', true,
44238
+ 'message', 'Criterium meegenomen in de buffersleutel.'));
44239
+ END IF;
44240
+ END LOOP;
44241
+ END IF;
44242
+
44243
+ IF coalesce(p_order ->> 'transportGroup', 'extern') IN ('eigen_vervoer', 'klant_haalt_op') THEN
44244
+ v_parts := v_parts || ('vervoer=' || (p_order ->> 'transportGroup'));
44245
+ v_trace := v_trace || jsonb_build_array(jsonb_build_object('rule', 'transport_group', 'value', p_order ->> 'transportGroup', 'result', true,
44246
+ 'message', CASE p_order ->> 'transportGroup' WHEN 'eigen_vervoer' THEN 'Eigen vervoer wordt alleen samen gebufferd met andere orders met eigen vervoer.'
44247
+ ELSE 'Klant haalt op wordt alleen samen gebufferd met andere orders die de klant ophaalt.' END));
44248
+ END IF;
44249
+ v_parts := ARRAY(SELECT DISTINCT ON (p) p FROM unnest(v_parts) WITH ORDINALITY AS t(p, n) ORDER BY p, n);
44250
+ v_key := array_to_string(v_parts, '|');
44251
+
44252
+ RETURN jsonb_build_object(
44253
+ 'buffer', true,
44254
+ 'reason', CASE WHEN v_missing THEN 'buffering_enabled_missing_data' ELSE 'buffering_enabled' END,
44255
+ 'bufferRef', md5(COALESCE(v_user::text, 'anon') || '|' || v_key),
44256
+ 'bufferKey', v_key, 'parts', to_jsonb(v_parts), 'complete', NOT v_missing,
44257
+ 'missingCriteria', to_jsonb(v_missing_list), 'isReservation', v_is_res, 'isFloriday', v_is_fd,
44258
+ 'skippedForFloriday', CASE WHEN v_is_fd THEN to_jsonb(v_skip) ELSE '[]'::jsonb END,
44259
+ 'matchCriteria', to_jsonb(v_criteria), 'closeBeforeMinutes', v_minutes, 'closeReference', v_reference,
44260
+ 'bufferCloseAt', v_close, 'release', v_release, 'trace', v_trace);
44261
+ END;
44262
+ $function$;
44263
+
44264
+ SELECT public.logistics_pickup_moments_koppel();
44265
+
44266
+ -- ===== 2026-11-10c: rechten hulponderdelen ophaalmoment =====
44267
+ REVOKE EXECUTE ON FUNCTION public.logistics_pickup_regel() FROM authenticated;
44268
+ REVOKE EXECUTE ON FUNCTION public.logistics_pickup_moment_kies(uuid, date, timestamptz, text, numeric) FROM authenticated;