@flowselections/logistiek-engine 1.0.238 → 1.0.240

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.
@@ -26892,7 +26892,8 @@ BEGIN
26892
26892
  AND coalesce(o.order_source, '') <> 'floriday'
26893
26893
  AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
26894
26894
  AND coalesce(o.status, '') <> 'geannuleerd'
26895
- AND public.logistics_order_date(o.delivery_date) >= current_date - 1
26895
+ AND public.logistics_order_date(o.delivery_date::text)::date
26896
+ >= (now() AT TIME ZONE 'Europe/Amsterdam')::date - 1 -- type-vast: werkt bij text, date en timestamptz
26896
26897
  GROUP BY o.id, o.order_number, o.customer_id, oi.product_id
26897
26898
  LIMIT greatest(1, p_limit)
26898
26899
  LOOP
@@ -28390,7 +28391,8 @@ BEGIN
28390
28391
  AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
28391
28392
  AND coalesce(o.status, '') <> 'geannuleerd'
28392
28393
  AND oi.quarantined_at IS NULL
28393
- AND public.logistics_order_date(o.delivery_date) >= current_date - 1
28394
+ AND public.logistics_order_date(o.delivery_date::text)::date
28395
+ >= (now() AT TIME ZONE 'Europe/Amsterdam')::date - 1 -- type-vast: werkt bij text, date en timestamptz
28394
28396
  )
28395
28397
  SELECT g.*,
28396
28398
  (SELECT min(x.regelnaam) FROM regels x WHERE x.order_id = g.order_id AND coalesce(x.crates, 0) <= 0) AS onbekende_regel
@@ -28439,7 +28441,7 @@ BEGIN
28439
28441
  SET status = 'niet_printen', reden = 'Fust hoort niet meer bij deze kar of order vervallen.', updated_at = now()
28440
28442
  WHERE p.soort = 'eigen_traysticker' AND p.status IN ('wacht_op_moment', 'geblokkeerd', 'klaar')
28441
28443
  AND NOT (p.bron_id = ANY (v_keep))
28442
- AND p.order_id IN (SELECT o.id FROM public.orders o WHERE public.logistics_order_date(o.delivery_date) >= current_date - 1);
28444
+ AND p.order_id IN (SELECT o.id FROM public.orders o WHERE public.logistics_order_date(o.delivery_date::text)::date >= (now() AT TIME ZONE 'Europe/Amsterdam')::date - 1);
28443
28445
  GET DIAGNOSTICS n_uit = ROW_COUNT;
28444
28446
 
28445
28447
  UPDATE public.logistics_sticker_prints p
@@ -28673,7 +28675,7 @@ BEGIN
28673
28675
  FROM public.orders o
28674
28676
  JOIN public.floriday_salesorders_cache s ON s.floriday_id = o.floriday_sales_order_id AND NOT coalesce(s.is_deleted,false)
28675
28677
  CROSS JOIN LATERAL jsonb_array_elements(CASE WHEN jsonb_typeof(s.data->'additionalServices') = 'array' THEN s.data->'additionalServices' ELSE '[]'::jsonb END) e
28676
- WHERE o.deleted_at IS NULL AND public.logistics_order_date(o.delivery_date) >= current_date - 2
28678
+ WHERE o.deleted_at IS NULL AND public.logistics_order_date(o.delivery_date::text)::date >= (now() AT TIME ZONE 'Europe/Amsterdam')::date - 2
28677
28679
  AND upper(coalesce(e->>'unit','')) = 'LABEL' AND (e->>'numberOfUnits') ~ '^\d+$'
28678
28680
  AND nullif(btrim(coalesce(e->>'stickerId','')),'') IS NOT NULL
28679
28681
  AND NOT EXISTS (SELECT 1 FROM public.floriday_customer_stickers_cache c WHERE c.floriday_id = e->>'stickerId')
@@ -28707,7 +28709,7 @@ BEGIN
28707
28709
  SELECT o.id, o.order_number, nullif(btrim(o.floriday_sales_order_id),'') AS sales
28708
28710
  FROM public.orders o
28709
28711
  WHERE o.deleted_at IS NULL AND nullif(btrim(o.floriday_sales_order_id),'') IS NOT NULL
28710
- AND public.logistics_order_date(o.delivery_date) >= current_date - 2
28712
+ AND public.logistics_order_date(o.delivery_date::text)::date >= (now() AT TIME ZONE 'Europe/Amsterdam')::date - 2
28711
28713
  AND EXISTS (SELECT 1 FROM logistics_active.order_carts oc WHERE oc.order_id = o.id)
28712
28714
  ORDER BY o.delivery_date LIMIT greatest(p_limit,1)
28713
28715
  LOOP
@@ -32233,7 +32235,8 @@ BEGIN
32233
32235
  AND coalesce(o.order_source, '') <> 'floriday'
32234
32236
  AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
32235
32237
  AND coalesce(o.status, '') <> 'geannuleerd'
32236
- AND public.logistics_order_date(o.delivery_date) >= current_date - 1
32238
+ AND public.logistics_order_date(o.delivery_date::text)::date
32239
+ >= (now() AT TIME ZONE 'Europe/Amsterdam')::date - 1 -- type-vast: werkt bij text, date en timestamptz
32237
32240
  GROUP BY o.id, o.order_number, o.customer_id, oi.product_id
32238
32241
  LIMIT greatest(1, p_limit)
32239
32242
  LOOP
@@ -32352,7 +32355,8 @@ BEGIN
32352
32355
  AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
32353
32356
  AND coalesce(o.status, '') <> 'geannuleerd'
32354
32357
  AND oi.quarantined_at IS NULL
32355
- AND public.logistics_order_date(o.delivery_date) >= current_date - 1
32358
+ AND public.logistics_order_date(o.delivery_date::text)::date
32359
+ >= (now() AT TIME ZONE 'Europe/Amsterdam')::date - 1 -- type-vast: werkt bij text, date en timestamptz
32356
32360
  )
32357
32361
  SELECT g.*,
32358
32362
  (SELECT min(x.regelnaam) FROM regels x WHERE x.order_id = g.order_id AND coalesce(x.crates, 0) <= 0) AS onbekende_regel
@@ -32401,7 +32405,7 @@ BEGIN
32401
32405
  SET status = 'niet_printen', reden = 'Fust hoort niet meer bij deze kar of order vervallen.', updated_at = now()
32402
32406
  WHERE p.soort = 'eigen_traysticker' AND p.status IN ('wacht_op_moment', 'geblokkeerd', 'klaar')
32403
32407
  AND NOT (p.bron_id = ANY (v_keep))
32404
- AND p.order_id IN (SELECT o.id FROM public.orders o WHERE public.logistics_order_date(o.delivery_date) >= current_date - 1);
32408
+ AND p.order_id IN (SELECT o.id FROM public.orders o WHERE public.logistics_order_date(o.delivery_date::text)::date >= (now() AT TIME ZONE 'Europe/Amsterdam')::date - 1);
32405
32409
  GET DIAGNOSTICS n_uit = ROW_COUNT;
32406
32410
 
32407
32411
  UPDATE public.logistics_sticker_prints p
@@ -32569,7 +32573,7 @@ BEGIN
32569
32573
  FROM public.orders o
32570
32574
  JOIN public.floriday_salesorders_cache s ON s.floriday_id = o.floriday_sales_order_id AND NOT coalesce(s.is_deleted,false)
32571
32575
  CROSS JOIN LATERAL jsonb_array_elements(CASE WHEN jsonb_typeof(s.data->'additionalServices') = 'array' THEN s.data->'additionalServices' ELSE '[]'::jsonb END) e
32572
- WHERE o.deleted_at IS NULL AND public.logistics_order_date(o.delivery_date) >= current_date - 2
32576
+ WHERE o.deleted_at IS NULL AND public.logistics_order_date(o.delivery_date::text)::date >= (now() AT TIME ZONE 'Europe/Amsterdam')::date - 2
32573
32577
  AND upper(coalesce(e->>'unit','')) = 'LABEL' AND (e->>'numberOfUnits') ~ '^\d+$'
32574
32578
  AND nullif(btrim(coalesce(e->>'stickerId','')),'') IS NOT NULL
32575
32579
  AND NOT EXISTS (SELECT 1 FROM public.floriday_customer_stickers_cache c WHERE c.floriday_id = e->>'stickerId')
@@ -32603,7 +32607,7 @@ BEGIN
32603
32607
  SELECT o.id, o.order_number, nullif(btrim(o.floriday_sales_order_id),'') AS sales
32604
32608
  FROM public.orders o
32605
32609
  WHERE o.deleted_at IS NULL AND nullif(btrim(o.floriday_sales_order_id),'') IS NOT NULL
32606
- AND public.logistics_order_date(o.delivery_date) >= current_date - 2
32610
+ AND public.logistics_order_date(o.delivery_date::text)::date >= (now() AT TIME ZONE 'Europe/Amsterdam')::date - 2
32607
32611
  AND EXISTS (SELECT 1 FROM logistics_active.order_carts oc WHERE oc.order_id = o.id)
32608
32612
  ORDER BY o.delivery_date LIMIT greatest(p_limit,1)
32609
32613
  LOOP
@@ -41085,3 +41089,2424 @@ BEGIN
41085
41089
  END IF;
41086
41090
  RETURN v_res;
41087
41091
  END $function$;
41092
+
41093
+
41094
+ -- ============================================================================
41095
+ -- Functies die alleen in de installatiecontrole of in andere functies voorkwamen,
41096
+ -- maar hier nergens werden aangemaakt. Volgorde: pickup_at vóór pickup_ts.
41097
+ -- ============================================================================
41098
+
41099
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_at(p_value text, p_delivery_date date)
41100
+ RETURNS jsonb
41101
+ LANGUAGE plpgsql
41102
+ STABLE
41103
+ SET search_path TO 'public'
41104
+ AS $function$
41105
+ DECLARE
41106
+ v text := btrim(coalesce(p_value, ''));
41107
+ v_tz text;
41108
+ m text[];
41109
+ v_parts text[];
41110
+ v_date date;
41111
+ v_time time;
41112
+ v_pick timestamptz;
41113
+ v_sign timestamptz;
41114
+ BEGIN
41115
+ IF v = '' THEN
41116
+ RETURN jsonb_build_object('ok', false, 'bron', 'leeg', 'reden', 'Het ophaalmoment ontbreekt.');
41117
+ END IF;
41118
+ v_tz := public.logistics_tz();
41119
+ IF v_tz IS NULL THEN
41120
+ RETURN jsonb_build_object('ok', false, 'bron', 'onbekend',
41121
+ 'reden', 'De tijdzone van de vestiging ontbreekt. Stel die in bij Werktijden.');
41122
+ END IF;
41123
+
41124
+ -- Momentsleutel van de vervoerder: ophalen n@HH:MM, optioneel aanmelden m@HH:MM.
41125
+ m := regexp_match(v, '^(\d{1,2})@(\d{1,2}:\d{2})(?::\d{2})?(?:\|(\d{1,2})@(\d{1,2}:\d{2})(?::\d{2})?)?$');
41126
+ IF m IS NOT NULL THEN
41127
+ IF p_delivery_date IS NULL THEN
41128
+ RETURN jsonb_build_object('ok', false, 'bron', 'momentsleutel',
41129
+ 'reden', 'De leverdatum ontbreekt, daardoor is het ophaalmoment niet te bepalen.');
41130
+ END IF;
41131
+ BEGIN
41132
+ v_pick := (public.logistics_subtract_pick_workdays(p_delivery_date, m[1]::int)::timestamp + m[2]::time) AT TIME ZONE v_tz;
41133
+ IF m[3] IS NOT NULL THEN
41134
+ v_sign := (public.logistics_subtract_pick_workdays(p_delivery_date, m[3]::int)::timestamp + m[4]::time) AT TIME ZONE v_tz;
41135
+ END IF;
41136
+ EXCEPTION WHEN invalid_datetime_format OR datetime_field_overflow THEN
41137
+ RETURN jsonb_build_object('ok', false, 'bron', 'momentsleutel',
41138
+ 'reden', 'De tijd in het ophaalmoment is ongeldig.');
41139
+ END;
41140
+ RETURN jsonb_build_object('ok', true, 'bron', 'momentsleutel', 'pickupAt', v_pick, 'signupAt', v_sign);
41141
+ END IF;
41142
+
41143
+ -- "YYYY-MM-DD|HH:MM[|HH:MM]"
41144
+ IF v ~ '^\d{4}-\d{2}-\d{2}\|' THEN
41145
+ v_parts := string_to_array(v, '|');
41146
+ BEGIN
41147
+ v_date := v_parts[1]::date;
41148
+ v_time := NULLIF(btrim(v_parts[2]), '')::time;
41149
+ EXCEPTION WHEN invalid_datetime_format OR datetime_field_overflow THEN
41150
+ RETURN jsonb_build_object('ok', false, 'bron', 'datum_tijd', 'reden', 'De datum of tijd in het ophaalmoment is ongeldig.');
41151
+ END;
41152
+ IF v_time IS NULL THEN
41153
+ RETURN jsonb_build_object('ok', false, 'bron', 'datum_tijd', 'pickupDate', v_date,
41154
+ 'reden', 'De ophaaltijd ontbreekt.');
41155
+ END IF;
41156
+ RETURN jsonb_build_object('ok', true, 'bron', 'datum_tijd',
41157
+ 'pickupAt', (v_date + v_time) AT TIME ZONE v_tz, 'signupAt', NULL);
41158
+ END IF;
41159
+
41160
+ -- Alleen een datum: geen tijd, dus geen ophaalmoment.
41161
+ IF v ~ '^\d{4}-\d{2}-\d{2}$' THEN
41162
+ RETURN jsonb_build_object('ok', false, 'bron', 'datum', 'pickupDate', v,
41163
+ 'reden', 'De ophaaltijd ontbreekt.');
41164
+ END IF;
41165
+
41166
+ -- Volledige tijdstempel.
41167
+ BEGIN
41168
+ v_pick := v::timestamptz;
41169
+ RETURN jsonb_build_object('ok', true, 'bron', 'tijdstempel', 'pickupAt', v_pick, 'signupAt', NULL);
41170
+ EXCEPTION WHEN others THEN
41171
+ RETURN jsonb_build_object('ok', false, 'bron', 'onbekend',
41172
+ 'reden', 'Het ophaalmoment heeft een onbekende vorm.');
41173
+ END;
41174
+ END;
41175
+ $function$;
41176
+ REVOKE EXECUTE ON FUNCTION public.logistics_pickup_at(text, date) FROM PUBLIC, anon;
41177
+ GRANT EXECUTE ON FUNCTION public.logistics_pickup_at(text, date) TO authenticated, service_role;
41178
+
41179
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_ts(p_value text, p_delivery_date date)
41180
+ RETURNS timestamp with time zone
41181
+ LANGUAGE sql
41182
+ STABLE
41183
+ SET search_path TO 'public'
41184
+ AS $function$
41185
+ SELECT CASE WHEN coalesce((r->>'ok')::boolean, false) THEN (r->>'pickupAt')::timestamptz END
41186
+ FROM (SELECT public.logistics_pickup_at(p_value, p_delivery_date) AS r) x
41187
+ $function$;
41188
+ REVOKE EXECUTE ON FUNCTION public.logistics_pickup_ts(text, date) FROM PUBLIC, anon;
41189
+ GRANT EXECUTE ON FUNCTION public.logistics_pickup_ts(text, date) TO authenticated, service_role;
41190
+
41191
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_herbereken()
41192
+ RETURNS jsonb
41193
+ LANGUAGE plpgsql
41194
+ SECURITY DEFINER
41195
+ SET search_path TO 'public'
41196
+ AS $function$
41197
+ DECLARE
41198
+ r record;
41199
+ v_tz text := public.logistics_tz();
41200
+ v_today date;
41201
+ v_total int := 0; v_key int := 0; v_dt int := 0; v_fout int := 0; v_err int := 0;
41202
+ v_pk jsonb;
41203
+ BEGIN
41204
+ PERFORM public.logistics_acc_guard('beheer', 'logistics_pickup_herbereken');
41205
+ IF v_tz IS NULL THEN
41206
+ RETURN jsonb_build_object('ok', false, 'reden', 'De tijdzone van de vestiging ontbreekt. Stel die in bij Werktijden.');
41207
+ END IF;
41208
+ v_today := (now() AT TIME ZONE v_tz)::date;
41209
+ FOR r IN
41210
+ SELECT o.id, o.order_number, o.delivery_date, o.custom_fields->>'pickup_moment' AS pm
41211
+ FROM public.orders o
41212
+ WHERE o.deleted_at IS NULL
41213
+ AND o.plan_locked_at IS NULL
41214
+ AND nullif(btrim(o.custom_fields->>'pickup_moment'), '') IS NOT NULL
41215
+ AND coalesce(o.delivery_date, '') ~ '^\d{4}-\d{2}-\d{2}'
41216
+ AND left(o.delivery_date, 10)::date >= v_today
41217
+ AND NOT public.logistics_order_geannuleerd(o.id)
41218
+ LOOP
41219
+ v_total := v_total + 1;
41220
+ v_pk := public.logistics_pickup_at(r.pm, left(r.delivery_date, 10)::date);
41221
+ IF NOT coalesce((v_pk->>'ok')::boolean, false) THEN v_fout := v_fout + 1;
41222
+ ELSIF v_pk->>'bron' = 'momentsleutel' THEN v_key := v_key + 1;
41223
+ ELSE v_dt := v_dt + 1; END IF;
41224
+ BEGIN
41225
+ PERFORM public.logistics_plan_enqueue(r.id, 'ophaalmoment herberekend');
41226
+ EXCEPTION WHEN others THEN
41227
+ v_err := v_err + 1;
41228
+ INSERT INTO public.logistics_planner_trace(order_id, message)
41229
+ VALUES (r.id, 'herberekenen ophaalmoment mislukt: ' || SQLERRM);
41230
+ END;
41231
+ END LOOP;
41232
+ RETURN jsonb_build_object('ok', v_err = 0, 'orders', v_total, 'momentsleutel', v_key,
41233
+ 'datumTijd', v_dt, 'zonderLeesbaarMoment', v_fout, 'fouten', v_err);
41234
+ END;
41235
+ $function$;
41236
+ REVOKE EXECUTE ON FUNCTION public.logistics_pickup_herbereken() FROM PUBLIC, anon;
41237
+ GRANT EXECUTE ON FUNCTION public.logistics_pickup_herbereken() TO authenticated, service_role;
41238
+
41239
+ CREATE OR REPLACE FUNCTION public.process_logistics_event(p_event_id uuid)
41240
+ RETURNS uuid
41241
+ LANGUAGE plpgsql
41242
+ SECURITY DEFINER
41243
+ SET search_path TO 'public'
41244
+ AS $function$
41245
+ DECLARE
41246
+ ev public.logistics_events%ROWTYPE;
41247
+ cfg public.logistics_rule_configs%ROWTYPE;
41248
+ s jsonb; pl jsonb; carriers jsonb; c jsonb; base jsonb; child jsonb;
41249
+ snap_version integer; snap_hash text; warn_list jsonb := '[]'::jsonb;
41250
+ prio integer;
41251
+ checkin_at timestamptz; pickup_at timestamptz;
41252
+ buffer_on boolean; buffer_close timestamptz; buffer_release text;
41253
+ staff integer; total_minutes numeric := 0; pick_minutes integer;
41254
+ start_mode text; reserve_minutes integer := 0; lead_minutes numeric;
41255
+ pick_start timestamptz; pick_end timestamptz; pick_start_raw timestamptz;
41256
+ fill_mode text; scanner text;
41257
+ control_mode text; control_notice timestamptz; control_owner text;
41258
+ shipping_choice text; shipping_at timestamptz;
41259
+ run_id uuid; seq integer := 0; created integer := 0; revised integer := 0;
41260
+ outcome text; default_minutes numeric; keep text[] := ARRAY[]::text[]; stale integer := 0;
41261
+ cancelled_count integer := 0; child_count integer := 0; child_ref text;
41262
+ v_pk_dd date;
41263
+ BEGIN
41264
+ SELECT * INTO ev FROM public.logistics_events WHERE id = p_event_id FOR UPDATE;
41265
+ IF NOT FOUND THEN RETURN NULL; END IF;
41266
+
41267
+ pl := COALESCE(ev.payload, '{}'::jsonb);
41268
+ prio := COALESCE(NULLIF(pl ->> 'priority', '')::integer, NULLIF(ev.priority, 0), 0);
41269
+ IF ev.event_type = 'order.changed' AND lower(btrim(coalesce(pl->>'reason', ''))) = 'afgehandeld in floriday' THEN
41270
+ RETURN public.logistics_apply_external_fulfillment(p_event_id);
41271
+ END IF;
41272
+
41273
+ IF ev.event_type IN ('order.cancelled', 'order.canceled') THEN
41274
+ INSERT INTO public.logistics_plan_runs (
41275
+ user_id, order_ref, source_module, strategy_id, trigger, input, status
41276
+ ) VALUES (
41277
+ ev.user_id, ev.order_ref, ev.source_module, ev.strategy_id, ev.event_type, pl, 'ok'
41278
+ ) RETURNING id INTO run_id;
41279
+
41280
+ cancelled_count := public.logistics_cancel_order(
41281
+ ev.order_ref, COALESCE(pl ->> 'reason', 'order geannuleerd'), ev.user_id);
41282
+
41283
+ UPDATE public.logistics_plan_runs
41284
+ SET input = input || jsonb_build_object('tasksCancelled', cancelled_count), updated_at = now()
41285
+ WHERE id = run_id;
41286
+
41287
+ UPDATE public.logistics_events
41288
+ SET status = 'processed', plan_run_id = run_id, error = NULL,
41289
+ processed_at = now(), updated_at = now()
41290
+ WHERE id = p_event_id;
41291
+ RETURN run_id;
41292
+ END IF;
41293
+
41294
+ IF ev.event_type = 'order.split' THEN
41295
+ INSERT INTO public.logistics_plan_runs (
41296
+ user_id, order_ref, source_module, strategy_id, trigger, input, status
41297
+ ) VALUES (
41298
+ ev.user_id, ev.order_ref, ev.source_module, ev.strategy_id, ev.event_type, pl, 'ok'
41299
+ ) RETURNING id INTO run_id;
41300
+
41301
+ cancelled_count := public.logistics_cancel_order(ev.order_ref, 'order gesplitst', ev.user_id);
41302
+
41303
+ FOR child IN SELECT * FROM jsonb_array_elements(COALESCE(pl -> 'children', '[]'::jsonb)) LOOP
41304
+ child_ref := child ->> 'orderRef';
41305
+ CONTINUE WHEN child_ref IS NULL OR child_ref = '';
41306
+ INSERT INTO public.logistics_events (
41307
+ user_id, source_module, event_type, order_ref, strategy_id, shadow, priority,
41308
+ idempotency_key, payload
41309
+ ) VALUES (
41310
+ ev.user_id, ev.source_module, 'order.received', child_ref, ev.strategy_id, ev.shadow, prio,
41311
+ 'split:' || ev.id::text || ':' || child_ref,
41312
+ (pl - 'children') || child || jsonb_build_object('splitFrom', ev.order_ref, 'priority', prio)
41313
+ )
41314
+ ON CONFLICT (user_id, idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING;
41315
+ child_count := child_count + 1;
41316
+ END LOOP;
41317
+
41318
+ UPDATE public.logistics_plan_runs
41319
+ SET input = input || jsonb_build_object(
41320
+ 'tasksCancelled', cancelled_count, 'childOrders', child_count), updated_at = now()
41321
+ WHERE id = run_id;
41322
+
41323
+ UPDATE public.logistics_events
41324
+ SET status = 'processed', plan_run_id = run_id, error = NULL,
41325
+ processed_at = now(), updated_at = now()
41326
+ WHERE id = p_event_id;
41327
+ RETURN run_id;
41328
+ END IF;
41329
+
41330
+ -- Ophaalmoment: een gewone tijdstempel blijft werken zoals altijd; anders (momentsleutel
41331
+ -- "n@HH:MM|m@HH:MM" of "datum|tijd") lezen via logistics_pickup_ts met de leverdatum.
41332
+ SELECT CASE WHEN coalesce(x.delivery_date, '') ~ '^\d{4}-\d{2}-\d{2}' THEN left(x.delivery_date, 10)::date END
41333
+ INTO v_pk_dd
41334
+ FROM public.orders x
41335
+ WHERE (x.id = public.logistics_try_uuid(pl ->> 'orderId') OR x.order_number = ev.order_ref)
41336
+ AND x.deleted_at IS NULL
41337
+ ORDER BY (x.id = public.logistics_try_uuid(pl ->> 'orderId')) DESC NULLS LAST, x.created_at DESC
41338
+ LIMIT 1;
41339
+ BEGIN
41340
+ checkin_at := NULLIF(pl ->> 'checkinAt', '')::timestamptz;
41341
+ EXCEPTION WHEN others THEN
41342
+ checkin_at := public.logistics_pickup_ts(pl ->> 'checkinAt', v_pk_dd);
41343
+ END;
41344
+ checkin_at := COALESCE(checkin_at, now());
41345
+ BEGIN
41346
+ pickup_at := NULLIF(pl ->> 'pickupAt', '')::timestamptz;
41347
+ EXCEPTION WHEN others THEN
41348
+ pickup_at := public.logistics_pickup_ts(pl ->> 'pickupAt', v_pk_dd);
41349
+ END;
41350
+ pickup_at := COALESCE(pickup_at, checkin_at);
41351
+ pl := pl || jsonb_build_object('checkinAt', checkin_at, 'pickupAt', pickup_at);
41352
+ carriers := COALESCE(pl -> 'loadCarriers', '[]'::jsonb);
41353
+
41354
+ IF ev.strategy_id IS NOT NULL THEN
41355
+ SELECT * INTO cfg FROM public.logistics_rule_configs
41356
+ WHERE strategy_id = ev.strategy_id AND user_id = ev.user_id
41357
+ ORDER BY is_active DESC, updated_at DESC LIMIT 1;
41358
+ END IF;
41359
+ IF cfg.id IS NULL THEN
41360
+ SELECT * INTO cfg FROM public.logistics_rule_configs
41361
+ WHERE user_id = ev.user_id ORDER BY is_active DESC, updated_at DESC LIMIT 1;
41362
+ END IF;
41363
+
41364
+ BEGIN
41365
+ snap_version := NULLIF(pl ->> 'ruleVersion', '')::integer;
41366
+ EXCEPTION WHEN others THEN snap_version := NULL; END;
41367
+
41368
+ IF snap_version IS NOT NULL AND cfg.id IS NOT NULL THEN
41369
+ s := public.logistics_resolve_settings(ev.user_id, cfg.id, snap_version);
41370
+ IF NOT EXISTS (SELECT 1 FROM public.logistics_rule_versions
41371
+ WHERE config_id = cfg.id AND user_id = ev.user_id AND version = snap_version) THEN
41372
+ warn_list := warn_list || jsonb_build_array(jsonb_build_object(
41373
+ 'code', 'rule_version_not_found', 'requestedVersion', snap_version));
41374
+ snap_version := cfg.current_version;
41375
+ END IF;
41376
+ ELSE
41377
+ s := COALESCE(cfg.settings, '{}'::jsonb);
41378
+ snap_version := cfg.current_version;
41379
+ END IF;
41380
+ s := COALESCE(s, '{}'::jsonb);
41381
+ snap_hash := md5(s::text);
41382
+
41383
+ IF cfg.id IS NULL THEN
41384
+ warn_list := warn_list || jsonb_build_array(jsonb_build_object('code', 'no_rule_config'));
41385
+ END IF;
41386
+
41387
+ INSERT INTO public.logistics_plan_runs (
41388
+ user_id, order_ref, source_module, strategy_id, rule_config_id, rule_version, trigger, input,
41389
+ settings_snapshot, settings_hash
41390
+ ) VALUES (
41391
+ ev.user_id, ev.order_ref, ev.source_module, ev.strategy_id, cfg.id, snap_version, ev.event_type, pl,
41392
+ s, snap_hash
41393
+ ) RETURNING id INTO run_id;
41394
+
41395
+ base := jsonb_build_object(
41396
+ 'orderRef', ev.order_ref,
41397
+ 'customerRef', pl -> 'customerRef',
41398
+ 'checkinAt', checkin_at,
41399
+ 'pickupAt', pickup_at,
41400
+ 'shadow', ev.shadow,
41401
+ 'strategyId', ev.strategy_id,
41402
+ 'ruleVersion', snap_version,
41403
+ 'settingsHash', snap_hash,
41404
+ 'priority', prio,
41405
+ 'splitFrom', pl -> 'splitFrom'
41406
+ );
41407
+
41408
+ buffer_on := public.logistics_rule_bool(s, 'order_verwerking', 'buffer_enabled', true);
41409
+ buffer_release := public.logistics_rule_text(s, 'order_verwerking', 'buffer_release', 'auto');
41410
+ IF buffer_on THEN
41411
+ buffer_close := CASE
41412
+ WHEN public.logistics_rule_text(s, 'order_verwerking', 'buffer_close_reference', 'checkin') = 'pick_start'
41413
+ AND public.logistics_pick_start_estimate(s, pl) IS NOT NULL
41414
+ THEN public.logistics_pick_start_estimate(s, pl)
41415
+ ELSE checkin_at
41416
+ END - make_interval(mins =>
41417
+ public.logistics_rule_num(s, 'order_verwerking', 'buffer_close_before_minutes', 60)::int);
41418
+ END IF;
41419
+
41420
+ staff := GREATEST(1, ROUND(public.logistics_rule_num(s, 'pickstations', 'avg_staff', 5))::int);
41421
+ FOR c IN SELECT * FROM jsonb_array_elements(carriers) LOOP
41422
+ default_minutes := CASE c ->> 'type'
41423
+ WHEN 'trolley' THEN 15 WHEN 'danish_container' THEN 15 WHEN 'dense_container' THEN 12
41424
+ WHEN 'pallet' THEN 20 WHEN 'box' THEN 3 WHEN 'loose' THEN 5 ELSE 10 END;
41425
+ total_minutes := total_minutes + GREATEST(0, COALESCE((c ->> 'count')::numeric, 0)) *
41426
+ public.logistics_rule_num(s, 'pickstations', 'pick_time_' || (c ->> 'type'), default_minutes);
41427
+ END LOOP;
41428
+ pick_minutes := CEIL(total_minutes / staff)::int;
41429
+
41430
+ start_mode := public.logistics_rule_text(s, 'order_picken', 'pick_start_mode', 'as_late_as_possible');
41431
+ IF start_mode = 'as_late_as_possible' THEN
41432
+ IF public.logistics_rule_text(s, 'order_picken', 'pick_reserve_type', 'fixed') = 'percentage' THEN
41433
+ reserve_minutes := CEIL(pick_minutes *
41434
+ public.logistics_rule_num(s, 'order_picken', 'pick_reserve_percentage', 20) / 100)::int;
41435
+ ELSE
41436
+ reserve_minutes := public.logistics_rule_num(s, 'order_picken', 'pick_reserve_minutes', 30)::int;
41437
+ END IF;
41438
+ END IF;
41439
+
41440
+ IF start_mode = 'window_before_checkin' THEN
41441
+ IF public.logistics_rule_text(s, 'order_picken', 'pick_window_unit', 'hours') = 'days' THEN
41442
+ lead_minutes := public.logistics_rule_num(s, 'order_picken', 'pick_window_days', 1) * 24 * 60;
41443
+ ELSE
41444
+ lead_minutes := public.logistics_rule_num(s, 'order_picken', 'pick_window_hours', 4) * 60;
41445
+ END IF;
41446
+ pick_start := checkin_at - make_interval(mins => lead_minutes::int);
41447
+ pick_end := checkin_at;
41448
+ ELSE
41449
+ pick_end := pickup_at - make_interval(mins => reserve_minutes);
41450
+ pick_start := pick_end - make_interval(mins => pick_minutes);
41451
+ END IF;
41452
+
41453
+ pick_start_raw := pick_start;
41454
+ pick_start := public.logistics_not_past(pick_start);
41455
+ IF pick_start_raw < now() THEN
41456
+ warn_list := warn_list || jsonb_build_array(jsonb_build_object(
41457
+ 'code', 'pick_start_in_past', 'plannedStart', pick_start_raw, 'shiftedTo', pick_start));
41458
+ END IF;
41459
+ IF pick_end IS NOT NULL AND pick_end < pick_start THEN
41460
+ pick_end := pick_start + make_interval(mins => GREATEST(pick_minutes, 1));
41461
+ warn_list := warn_list || jsonb_build_array(jsonb_build_object('code', 'pick_window_too_short'));
41462
+ END IF;
41463
+ IF pickup_at < now() THEN
41464
+ warn_list := warn_list || jsonb_build_array(jsonb_build_object('code', 'pickup_in_past', 'pickupAt', pickup_at));
41465
+ END IF;
41466
+ IF buffer_close IS NOT NULL AND buffer_close < now() THEN
41467
+ warn_list := warn_list || jsonb_build_array(jsonb_build_object('code', 'buffer_close_in_past', 'closeAt', buffer_close));
41468
+ buffer_close := public.logistics_not_past(buffer_close);
41469
+ END IF;
41470
+
41471
+ fill_mode := public.logistics_rule_text(s, 'kar_vullen', 'fill_mode', 'generated');
41472
+ scanner := public.logistics_rule_text(s, 'order_picken', 'scanner', 'required');
41473
+
41474
+ IF buffer_on AND buffer_close IS NOT NULL THEN
41475
+ outcome := public.logistics_upsert_task(ev.user_id, run_id, ev.order_ref, pl ->> 'bufferRef', ev.strategy_id,
41476
+ cfg.id, snap_version, 'buffer.close', 'verkoop', seq, buffer_close, buffer_close, ev.shadow,
41477
+ base || jsonb_build_object('bufferRef', pl -> 'bufferRef', 'release', buffer_release,
41478
+ 'matchCriteria', COALESCE(public.logistics_rule_value(s, 'order_verwerking', 'buffer_match'), '[]'::jsonb)));
41479
+ keep := array_append(keep, 'buffer.close'); seq := seq + 1;
41480
+ IF outcome = 'created' THEN created := created + 1; ELSIF outcome = 'revised' THEN revised := revised + 1; END IF;
41481
+ END IF;
41482
+
41483
+ outcome := public.logistics_upsert_task(ev.user_id, run_id, ev.order_ref, pl ->> 'bufferRef', ev.strategy_id,
41484
+ cfg.id, snap_version, 'stock.reserve', 'voorraad', seq,
41485
+ public.logistics_not_past(COALESCE(buffer_close, pick_start)),
41486
+ public.logistics_not_past(COALESCE(buffer_close, pick_start)), ev.shadow,
41487
+ base || jsonb_build_object('loadCarriers', carriers));
41488
+ keep := array_append(keep, 'stock.reserve'); seq := seq + 1;
41489
+ IF outcome = 'created' THEN created := created + 1; ELSIF outcome = 'revised' THEN revised := revised + 1; END IF;
41490
+
41491
+ outcome := public.logistics_upsert_task(ev.user_id, run_id, ev.order_ref, pl ->> 'bufferRef', ev.strategy_id,
41492
+ cfg.id, snap_version, 'load.compose', 'afhandeling', seq,
41493
+ public.logistics_not_past(pick_start - interval '10 minutes'), pick_start, ev.shadow,
41494
+ base || jsonb_build_object('loadCarriers', carriers, 'fillMode', fill_mode));
41495
+ keep := array_append(keep, 'load.compose'); seq := seq + 1;
41496
+ IF outcome = 'created' THEN created := created + 1; ELSIF outcome = 'revised' THEN revised := revised + 1; END IF;
41497
+
41498
+ outcome := public.logistics_upsert_task(ev.user_id, run_id, ev.order_ref, pl ->> 'bufferRef', ev.strategy_id,
41499
+ cfg.id, snap_version, 'pick.execute', 'pickschermen', seq, pick_start, pick_end, ev.shadow,
41500
+ base || jsonb_build_object(
41501
+ 'pickMode', public.logistics_rule_text(s, 'order_picken', 'pick_mode', 'cart'),
41502
+ 'pickStartMode', start_mode, 'pickMinutes', pick_minutes, 'staff', staff,
41503
+ 'reserveMinutes', reserve_minutes, 'scanner', scanner, 'loadCarriers', carriers));
41504
+ keep := array_append(keep, 'pick.execute'); seq := seq + 1;
41505
+ IF outcome = 'created' THEN created := created + 1; ELSIF outcome = 'revised' THEN revised := revised + 1; END IF;
41506
+
41507
+ IF fill_mode = 'manual_scan' THEN
41508
+ outcome := public.logistics_upsert_task(ev.user_id, run_id, ev.order_ref, pl ->> 'bufferRef', ev.strategy_id,
41509
+ cfg.id, snap_version, 'scan.verify', 'scanners', seq, pick_end, pick_end, ev.shadow,
41510
+ base || jsonb_build_object('reason', 'kar_vullen_manual_scan'));
41511
+ keep := array_append(keep, 'scan.verify'); seq := seq + 1;
41512
+ IF outcome = 'created' THEN created := created + 1; ELSIF outcome = 'revised' THEN revised := revised + 1; END IF;
41513
+ ELSIF scanner = 'required' THEN
41514
+ outcome := public.logistics_upsert_task(ev.user_id, run_id, ev.order_ref, pl ->> 'bufferRef', ev.strategy_id,
41515
+ cfg.id, snap_version, 'scan.verify', 'scanners', seq, pick_start, pick_end, ev.shadow,
41516
+ base || jsonb_build_object('reason', 'pick_scanner_required'));
41517
+ keep := array_append(keep, 'scan.verify'); seq := seq + 1;
41518
+ IF outcome = 'created' THEN created := created + 1; ELSIF outcome = 'revised' THEN revised := revised + 1; END IF;
41519
+ END IF;
41520
+
41521
+ control_mode := public.logistics_rule_text(s, 'controle', 'control_mode', 'scanner');
41522
+ IF control_mode <> 'skip' THEN
41523
+ control_notice := public.logistics_not_past(pickup_at - make_interval(mins =>
41524
+ public.logistics_rule_num(s, 'controle', 'control_notice_minutes', 60)::int));
41525
+ control_owner := CASE WHEN control_mode = 'scanner' THEN 'scanners' ELSE 'afhandeling' END;
41526
+
41527
+ outcome := public.logistics_upsert_task(ev.user_id, run_id, ev.order_ref, pl ->> 'bufferRef', ev.strategy_id,
41528
+ cfg.id, snap_version, 'control.execute', control_owner, seq, control_notice,
41529
+ GREATEST(pickup_at, control_notice), ev.shadow,
41530
+ base || jsonb_build_object('controlMode', control_mode, 'noticeAt', control_notice));
41531
+ keep := array_append(keep, 'control.execute'); seq := seq + 1;
41532
+ IF outcome = 'created' THEN created := created + 1; ELSIF outcome = 'revised' THEN revised := revised + 1; END IF;
41533
+
41534
+ IF control_mode = 'paper' THEN
41535
+ outcome := public.logistics_upsert_task(ev.user_id, run_id, ev.order_ref, pl ->> 'bufferRef', ev.strategy_id,
41536
+ cfg.id, snap_version, 'print.control_note', 'afhandeling', seq, control_notice, control_notice, ev.shadow,
41537
+ base || jsonb_build_object('printItems',
41538
+ public.logistics_rule_text(s, 'controle', 'control_print_items', 'control_note')));
41539
+ keep := array_append(keep, 'print.control_note'); seq := seq + 1;
41540
+ IF outcome = 'created' THEN created := created + 1; ELSIF outcome = 'revised' THEN revised := revised + 1; END IF;
41541
+ END IF;
41542
+ END IF;
41543
+
41544
+ IF fill_mode = 'generated' THEN
41545
+ shipping_choice := public.logistics_rule_text(s, 'kar_vullen', 'shipping_note_print_generated', 'with_packing_slip');
41546
+ ELSE
41547
+ shipping_choice := public.logistics_rule_text(s, 'kar_vullen', 'shipping_note_print_manual', 'after_control');
41548
+ END IF;
41549
+ shipping_at := CASE shipping_choice
41550
+ WHEN 'with_packing_slip' THEN pick_start
41551
+ WHEN 'with_control' THEN COALESCE(control_notice, pick_end)
41552
+ WHEN 'after_control' THEN COALESCE(control_notice, pick_end)
41553
+ ELSE pick_end END;
41554
+ shipping_at := public.logistics_not_past(shipping_at);
41555
+
41556
+ outcome := public.logistics_upsert_task(ev.user_id, run_id, ev.order_ref, pl ->> 'bufferRef', ev.strategy_id,
41557
+ cfg.id, snap_version, 'print.shipping_note', 'afhandeling', seq, shipping_at, shipping_at, ev.shadow,
41558
+ base || jsonb_build_object('moment', shipping_choice));
41559
+ keep := array_append(keep, 'print.shipping_note'); seq := seq + 1;
41560
+ IF outcome = 'created' THEN created := created + 1; ELSIF outcome = 'revised' THEN revised := revised + 1; END IF;
41561
+
41562
+ outcome := public.logistics_upsert_task(ev.user_id, run_id, ev.order_ref, pl ->> 'bufferRef', ev.strategy_id,
41563
+ cfg.id, snap_version, 'transport.dispatch', 'afhandeling', seq,
41564
+ public.logistics_not_past(pickup_at), public.logistics_not_past(pickup_at), ev.shadow, base);
41565
+ keep := array_append(keep, 'transport.dispatch');
41566
+ IF outcome = 'created' THEN created := created + 1; ELSIF outcome = 'revised' THEN revised := revised + 1; END IF;
41567
+
41568
+ stale := public.logistics_supersede_stale_tasks(ev.user_id, ev.order_ref, run_id, keep);
41569
+
41570
+ UPDATE public.logistics_tasks
41571
+ SET priority = prio, updated_at = now()
41572
+ WHERE user_id = ev.user_id AND order_ref = ev.order_ref
41573
+ AND status NOT IN ('cancelled','superseded','done')
41574
+ AND priority IS DISTINCT FROM prio;
41575
+
41576
+ UPDATE public.logistics_plan_runs
41577
+ SET tasks_created = created, tasks_revised = revised, status = 'ok',
41578
+ warnings = warn_list,
41579
+ input = input || jsonb_build_object('tasksSuperseded', stale, 'priority', prio),
41580
+ updated_at = now()
41581
+ WHERE id = run_id;
41582
+
41583
+ UPDATE public.logistics_events
41584
+ SET status = 'processed', plan_run_id = run_id, error = NULL, processed_at = now(), updated_at = now()
41585
+ WHERE id = p_event_id;
41586
+
41587
+ RETURN run_id;
41588
+ END; $function$;
41589
+ REVOKE EXECUTE ON FUNCTION public.process_logistics_event(uuid) FROM PUBLIC, anon;
41590
+ GRANT EXECUTE ON FUNCTION public.process_logistics_event(uuid) TO authenticated, service_role;
41591
+
41592
+ -- Laatste uitkomst van de stickerstappen in de Engine-ronde (voor de Engine-status).
41593
+ CREATE OR REPLACE FUNCTION public.logistics_sticker_sync_status()
41594
+ RETURNS jsonb
41595
+ LANGUAGE plpgsql
41596
+
41597
+ SECURITY DEFINER
41598
+ SET search_path TO 'public'
41599
+ AS $function$
41600
+ DECLARE v jsonb;
41601
+ BEGIN
41602
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_sticker_sync_status');
41603
+ SELECT value INTO v FROM public.logistics_tick_state WHERE key = 'sticker_sync';
41604
+ IF v IS NULL THEN
41605
+ RETURN jsonb_build_object('bekend', false, 'fouten', '[]'::jsonb);
41606
+ END IF;
41607
+ RETURN jsonb_build_object('bekend', true, 'op', v->'at',
41608
+ 'fouten', CASE WHEN jsonb_typeof(v->'fouten') = 'array' THEN v->'fouten' ELSE '[]'::jsonb END);
41609
+ END $function$;
41610
+ REVOKE EXECUTE ON FUNCTION public.logistics_sticker_sync_status() FROM PUBLIC, anon;
41611
+ GRANT EXECUTE ON FUNCTION public.logistics_sticker_sync_status() TO authenticated, service_role;
41612
+
41613
+ -- 1.0.238: logistics_trace_event, veilige beladingsovername, leverdatum altijd als datum (Nederlandse tijd), labels installatiecontrole.
41614
+ -- Orderverloopregel; mag nooit falen.
41615
+ CREATE OR REPLACE FUNCTION public.logistics_trace_event(p_order_id uuid, p_step text, p_status text, p_reason text)
41616
+ RETURNS void
41617
+ LANGUAGE plpgsql
41618
+ SECURITY DEFINER
41619
+ SET search_path TO 'public'
41620
+ AS $function$
41621
+ BEGIN
41622
+ BEGIN
41623
+ PERFORM public.logistics_trace_order_step(p_order_id, p_step, p_step, p_status, NULL, p_reason);
41624
+ EXCEPTION WHEN OTHERS THEN
41625
+ RAISE WARNING 'logistiek-engine: orderverloop schrijven mislukt (order %, stap %): %', p_order_id, p_step, SQLERRM;
41626
+ END;
41627
+ END;
41628
+ $function$;
41629
+ REVOKE ALL ON FUNCTION public.logistics_trace_event(uuid, text, text, text) FROM PUBLIC, anon;
41630
+ REVOKE EXECUTE ON FUNCTION public.logistics_trace_event(uuid, text, text, text) FROM authenticated;
41631
+ GRANT EXECUTE ON FUNCTION public.logistics_trace_event(uuid, text, text, text) TO service_role;
41632
+
41633
+ -- Automatische beladingsovername: alleen volledige Floriday-verpakking, nooit op handmatig vastgezette regels,
41634
+ -- en elke wijziging met oude en nieuwe belading in het orderverloop.
41635
+ CREATE OR REPLACE FUNCTION public.logistics_packing_autofix(p_limit integer DEFAULT 100)
41636
+ RETURNS integer
41637
+ LANGUAGE plpgsql
41638
+ SECURITY DEFINER
41639
+ SET search_path TO 'public'
41640
+ AS $function$
41641
+ DECLARE r record; v_reden text; v_aantal integer := 0; v_ppc int; v_cpl int; v_lpc int;
41642
+ v_oud record; v_tekst text;
41643
+ BEGIN
41644
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_packing_autofix');
41645
+ FOR r IN
41646
+ SELECT d.* FROM public.logistics_packing_deviations d
41647
+ WHERE d.resolved_at IS NULL AND d.scope = 'order_line' AND d.needs_human = false
41648
+ AND d.order_id IS NOT NULL AND d.order_item_id IS NOT NULL
41649
+ ORDER BY d.first_seen_at
41650
+ LIMIT GREATEST(1, LEAST(p_limit, 500))
41651
+ LOOP
41652
+ v_reden := public.logistics_packing_block_reason(r.order_id, r.order_item_id);
41653
+ IF v_reden IS NOT NULL THEN
41654
+ UPDATE public.logistics_packing_deviations SET needs_human = true, human_reason = v_reden WHERE id = r.id;
41655
+ CONTINUE;
41656
+ END IF;
41657
+ v_ppc := nullif(greatest(coalesce((r.floriday_packing->>'pieces_per_package')::numeric, 0), 0), 0)::int;
41658
+ v_cpl := nullif(greatest(coalesce((r.floriday_packing->>'packages_per_layer')::numeric, 0), 0), 0)::int;
41659
+ v_lpc := nullif(greatest(coalesce((r.floriday_packing->>'layers_per_load_carrier')::numeric, 0), 0), 0)::int;
41660
+ IF v_ppc IS NULL OR v_cpl IS NULL OR v_lpc IS NULL THEN
41661
+ UPDATE public.logistics_packing_deviations
41662
+ SET needs_human = true,
41663
+ human_reason = 'Floriday-verpakking onvolledig (0 of leeg = onbekend); niet automatisch overgenomen.'
41664
+ WHERE id = r.id;
41665
+ CONTINUE;
41666
+ END IF;
41667
+ SELECT oi.plants_per_crate, oi.crates_per_layer, oi.layers_per_cart, oi.packing_locked_at
41668
+ INTO v_oud FROM public.order_items oi WHERE oi.id = r.order_item_id FOR UPDATE;
41669
+ IF NOT FOUND THEN CONTINUE; END IF;
41670
+ IF v_oud.packing_locked_at IS NOT NULL THEN
41671
+ -- Handmatig vastgezet: nooit aanpassen; één keer ter beoordeling, niet elke ronde opnieuw.
41672
+ UPDATE public.logistics_packing_deviations
41673
+ SET needs_human = true, human_reason = 'handmatig vastgezet, niet automatisch aangepast'
41674
+ WHERE id = r.id;
41675
+ CONTINUE;
41676
+ END IF;
41677
+ UPDATE public.order_items oi
41678
+ SET plants_per_crate = v_ppc, crates_per_layer = v_cpl, layers_per_cart = v_lpc
41679
+ WHERE oi.id = r.order_item_id AND oi.packing_locked_at IS NULL;
41680
+ IF FOUND THEN
41681
+ v_aantal := v_aantal + 1;
41682
+ v_tekst := format('belading %s/%s/%s → %s/%s/%s (stuks per fust / fusten per laag / lagen per kar), automatisch naar de Floriday-stand',
41683
+ coalesce(v_oud.plants_per_crate::text, '?'), coalesce(v_oud.crates_per_layer::text, '?'), coalesce(v_oud.layers_per_cart::text, '?'),
41684
+ v_ppc, v_cpl, v_lpc);
41685
+ UPDATE public.logistics_packing_deviations
41686
+ SET auto_fixed_at = now(), resolved_at = now(), human_reason = v_tekst
41687
+ WHERE id = r.id;
41688
+ PERFORM public.logistics_trace_event(r.order_id, 'stap:belading_synchronisatie', 'ok', v_tekst);
41689
+ END IF;
41690
+ END LOOP;
41691
+ RETURN v_aantal;
41692
+ END;
41693
+ $function$;
41694
+ CREATE OR REPLACE FUNCTION public.logistics_shipping_note_ensure_accepted(p_order_id uuid DEFAULT NULL::uuid, p_limit integer DEFAULT 100)
41695
+ RETURNS integer
41696
+ LANGUAGE plpgsql
41697
+ SECURITY DEFINER
41698
+ SET search_path TO 'public'
41699
+ AS $function$
41700
+ DECLARE r record; v_cart uuid; v_n int := 0;
41701
+ BEGIN
41702
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_shipping_note_ensure_accepted');
41703
+ FOR r IN
41704
+ SELECT o.id, o.order_number FROM public.orders o
41705
+ WHERE o.deleted_at IS NULL
41706
+ AND (p_order_id IS NULL OR o.id = p_order_id OR o.buffer_id = (SELECT buffer_id FROM public.orders WHERE id = p_order_id))
41707
+ AND (nullif(o.floriday_sales_order_id,'') IS NOT NULL OR lower(coalesce(o.order_source,'')) LIKE '%floriday%')
41708
+ AND coalesce(o.status,'') <> 'geannuleerd'
41709
+ AND 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 - 7
41710
+ AND EXISTS (SELECT 1 FROM unnest(public.logistics_order_cart_ids(o.id)) c
41711
+ WHERE NOT EXISTS (SELECT 1 FROM public.logistics_slip_shipping_note s WHERE s.cart_id = c))
41712
+ ORDER BY o.updated_at DESC
41713
+ LIMIT greatest(coalesce(p_limit,100),1)
41714
+ LOOP
41715
+ IF NOT coalesce((public.logistics_afhandeling_fulfillment_order(r.id)->>'verstuurd')::boolean, false) THEN CONTINUE; END IF;
41716
+ FOREACH v_cart IN ARRAY public.logistics_order_cart_ids(r.id) LOOP
41717
+ IF EXISTS (SELECT 1 FROM public.logistics_slip_shipping_note s WHERE s.cart_id = v_cart) THEN CONTINUE; END IF;
41718
+ INSERT INTO public.logistics_slip_shipping_note(slip_job_id, cart_id, order_id, order_number, status, batch_key, next_attempt_at)
41719
+ VALUES (v_cart, v_cart, r.id, r.order_number, 'wachtend', 'na-acceptatie:' || r.id::text, now())
41720
+ ON CONFLICT (slip_job_id) DO NOTHING;
41721
+ IF FOUND THEN
41722
+ v_n := v_n + 1;
41723
+ BEGIN
41724
+ PERFORM public.logistics_trace_order_step(r.id, 'stap:verzendbrief', 'verzendbrief klaargezet', 'ok',
41725
+ 'na acceptatie belading', 'Floriday accepteerde de belading; verzendbrief voor kar ' || v_cart::text || ' klaargezet.', 'Verzendbrief');
41726
+ EXCEPTION WHEN others THEN NULL; END;
41727
+ END IF;
41728
+ END LOOP;
41729
+ END LOOP;
41730
+ RETURN v_n;
41731
+ END $function$;
41732
+
41733
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_status()
41734
+ RETURNS jsonb
41735
+ LANGUAGE plpgsql
41736
+ SECURITY DEFINER
41737
+ SET search_path TO 'public'
41738
+ AS $function$
41739
+ DECLARE s record; v jsonb;
41740
+ BEGIN
41741
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_carrier_status');
41742
+ IF auth.uid() IS NULL AND coalesce(auth.role(), '') <> 'service_role' THEN RAISE EXCEPTION 'Niet ingelogd'; END IF;
41743
+ SELECT * INTO s FROM public.logistics_carrier_settings_ensure();
41744
+ WITH open_orders AS (
41745
+ SELECT o.id, o.order_number, o.delivery_date, o.carrier_source, o.carrier_reason, o.created_at
41746
+ FROM public.orders o
41747
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
41748
+ 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
41749
+ )
41750
+ SELECT jsonb_build_object(
41751
+ 'zonderVervoerder', count(*) FILTER (WHERE carrier_source = 'geen' OR (carrier_source IS NULL AND created_at >= s.installed_at)),
41752
+ 'viaFavoriet', count(*) FILTER (WHERE carrier_source = 'favoriet'),
41753
+ 'nogNietBerekend', count(*) FILTER (WHERE carrier_source IS NULL AND created_at < s.installed_at),
41754
+ 'bronOud', count(*) FILTER (WHERE carrier_source = 'oud'),
41755
+ 'lijstZonder', coalesce(jsonb_agg(jsonb_build_object('orderId', id, 'orderNumber', order_number, 'deliveryDate', delivery_date,
41756
+ 'reden', coalesce(carrier_reason, 'Nog niet berekend.')) ORDER BY delivery_date, order_number)
41757
+ FILTER (WHERE carrier_source = 'geen' OR (carrier_source IS NULL AND created_at >= s.installed_at)), '[]'::jsonb),
41758
+ 'lijstFavoriet', coalesce(jsonb_agg(jsonb_build_object('orderId', id, 'orderNumber', order_number, 'deliveryDate', delivery_date,
41759
+ 'reden', carrier_reason) ORDER BY delivery_date, order_number) FILTER (WHERE carrier_source = 'favoriet'), '[]'::jsonb))
41760
+ INTO v FROM open_orders;
41761
+ RETURN v || jsonb_build_object('bufferFouten', (SELECT count(*) FROM public.logistics_carrier_events e
41762
+ WHERE e.soort = 'fout' AND e.created_at > now() - interval '7 days'), 'bestaandeOrders', coalesce(s.backfill_existing, false),
41763
+ 'installedAt', s.installed_at, 'schakelaarGezetDoor', s.backfill_set_by, 'schakelaarGezetOp', s.backfill_set_at);
41764
+ END $function$;
41765
+
41766
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_preview_existing(p_limit integer DEFAULT 1000)
41767
+ RETURNS TABLE(order_id uuid, order_number text, order_source text, delivery_date text, huidig_custom_carrier text, carrier_id uuid, carrier_name text, carrier_kind text, bron text, reden text, bevroren text)
41768
+ LANGUAGE plpgsql
41769
+ STABLE SECURITY DEFINER
41770
+ SET search_path TO 'public'
41771
+ AS $function$
41772
+ BEGIN
41773
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_carrier_preview_existing');
41774
+ IF auth.uid() IS NOT NULL AND NOT public.logistics_is_beheer() THEN
41775
+ RAISE EXCEPTION 'Alleen beheer mag deze lijst bekijken.';
41776
+ END IF;
41777
+ RETURN QUERY
41778
+ SELECT o.id, o.order_number, o.order_source, o.delivery_date, o.custom_fields->>'carrier',
41779
+ public.logistics_try_uuid(v.r->>'carrier_id'), v.r->>'carrier_name', v.r->>'carrier_kind', v.r->>'bron', v.r->>'reden',
41780
+ public.logistics_carrier_frozen_reason(o.id)
41781
+ FROM public.orders o
41782
+ CROSS JOIN LATERAL (SELECT public.logistics_resolve_carrier_core(to_jsonb(o), false) AS r) v
41783
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL AND o.carrier_source IS NULL
41784
+ 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
41785
+ ORDER BY o.delivery_date, o.order_number
41786
+ LIMIT greatest(1, coalesce(p_limit, 1000));
41787
+ END $function$;
41788
+
41789
+ CREATE OR REPLACE FUNCTION public.logistics_install_check()
41790
+ RETURNS TABLE(onderdeel text, ok boolean, toelichting text)
41791
+ LANGUAGE plpgsql
41792
+ SECURITY DEFINER
41793
+ SET search_path TO 'public'
41794
+ AS $function$
41795
+ DECLARE c text; f text; t text; v_fav text; n bigint; s record;
41796
+ BEGIN
41797
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_install_check');
41798
+ -- Alleen bestaan controleren: herberekenen nooit tijdens installatie uitvoeren.
41799
+ FOREACH f IN ARRAY ARRAY['logistics_pickup_at(text,date)',
41800
+ 'logistics_pickup_ts(text,date)', 'logistics_pickup_herbereken()'] LOOP
41801
+ onderdeel := 'functie ' || f;
41802
+ ok := to_regprocedure('public.' || f) IS NOT NULL;
41803
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: werk de Engine bij via Supabase bijwerken' END;
41804
+ RETURN NEXT;
41805
+ END LOOP;
41806
+ FOREACH f IN ARRAY ARRAY[
41807
+ 'f:logistics_try_uuid','f:logistics_tz','f:logistics_is_beheer','f:logistics_buffer_payload','f:logistics_apply_order_buffer',
41808
+ 'f:logistics_apply_buffer_decision','f:logistics_rebuffer_pending','f:logistics_departure_calc','f:logistics_own_transport_departure',
41809
+ 'f:logistics_order_departure','f:logistics_brief_transport','f:logistics_order_frozen','f:logistics_order_is_frozen',
41810
+ 'f:logistics_order_finalize_run','f:run_logistics_tasks_dispatch','f:enqueue_cart_replan_if_unlocked','f:logistics_tick',
41811
+ 't:logistics_tick_runs',
41812
+ 'f:logistics_belading_bevestig','f:logistics_cart_seq_dubbel','f:logistics_plan_retry_later','f:logistics_own_traysticker_sync',
41813
+ 't:logistics_belading_bevestigingen',
41814
+ 'f:logistics_sticker_copies','f:logistics_sticker_etiket_status','f:logistics_sticker_order_counts','f:logistics_sticker_sync',
41815
+ 'c:logistics_sticker_prints.kopieen_bron',
41816
+ 'f:logistics_carrier_settings_ensure','t:logistics_carrier_settings','t:logistics_carrier_recalc_queue','t:logistics_carrier_events',
41817
+ 'f:logistics_carrier_status','f:logistics_carrier_set_backfill','f:logistics_carrier_preview_existing','c:control_get_todays_carts.carrier_name',
41818
+ 'f:logistics_plan_fail_log'] LOOP
41819
+ 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);
41820
+ ok := CASE left(f, 1)
41821
+ WHEN 'f' THEN EXISTS (SELECT 1 FROM pg_proc p WHERE p.pronamespace = 'public'::regnamespace AND p.proname = substr(f, 3))
41822
+ WHEN 't' THEN to_regclass('public.' || substr(f, 3)) IS NOT NULL
41823
+ ELSE CASE WHEN split_part(substr(f, 3), '.', 1) = 'control_get_todays_carts'
41824
+ THEN coalesce(pg_get_function_result(to_regprocedure('public.control_get_todays_carts()')), '') LIKE '%carrier_name%'
41825
+ ELSE EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public'
41826
+ AND table_name = split_part(substr(f, 3), '.', 1) AND column_name = split_part(substr(f, 3), '.', 2)) END
41827
+ END;
41828
+ 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;
41829
+ RETURN NEXT;
41830
+ END LOOP;
41831
+ FOREACH c IN ARRAY ARRAY['carrier_id','carrier_source','carrier_reason','carrier_resolved_at','carrier_set_by','carrier_set_at'] LOOP
41832
+ onderdeel := 'orders.' || c;
41833
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='orders' AND column_name=c);
41834
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie' END; RETURN NEXT;
41835
+ END LOOP;
41836
+ FOREACH f IN ARRAY ARRAY['logistics_resolve_carrier(uuid)','logistics_resolve_carrier_many(uuid[])',
41837
+ 'logistics_resolve_carrier_for_location(uuid,uuid)','logistics_resolve_carrier_for_location_many(uuid[],uuid)',
41838
+ 'logistics_resolve_carrier_for_location_many(jsonb)',
41839
+ 'logistics_set_order_carrier(uuid,uuid)','logistics_carrier_recalc_run(integer)'] LOOP
41840
+ onderdeel := 'functie ' || f; ok := to_regprocedure('public.' || f) IS NOT NULL;
41841
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie' END; RETURN NEXT;
41842
+ END LOOP;
41843
+ FOREACH t IN ARRAY ARRAY['orders:trg_logistics_orders_carrier_fill','orders:trg_logistics_orders_carrier_changed',
41844
+ 'carriers:trg_logistics_carriers_enqueue','customer_delivery_locations:trg_logistics_locations_enqueue'] LOOP
41845
+ onderdeel := 'trigger ' || split_part(t, ':', 2);
41846
+ ok := EXISTS (SELECT 1 FROM pg_trigger g WHERE g.tgname = split_part(t, ':', 2)
41847
+ AND g.tgrelid = to_regclass('public.' || split_part(t, ':', 1)));
41848
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie' END; RETURN NEXT;
41849
+ END LOOP;
41850
+ onderdeel := 'minuutronde rekent vervoerders';
41851
+ ok := EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'logistics_tick' AND prosrc LIKE '%logistics_carrier_recalc_run%');
41852
+ toelichting := CASE WHEN ok THEN 'ja' ELSE 'nee' END; RETURN NEXT;
41853
+ FOREACH c IN ARRAY ARRAY['carrier_kind','is_favorite','archived_at','delivery_location_glns'] LOOP
41854
+ onderdeel := 'Afhandeling: carriers.' || c;
41855
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='carriers' AND column_name=c);
41856
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Afhandeling' END; RETURN NEXT;
41857
+ END LOOP;
41858
+ FOREACH f IN ARRAY ARRAY['afh_favorite_carrier','afh_city_code','afh_normalize_country','afh_normalize_nl_province'] LOOP
41859
+ onderdeel := 'Afhandeling: ' || f || '()';
41860
+ ok := EXISTS (SELECT 1 FROM pg_proc p WHERE p.pronamespace = 'public'::regnamespace AND p.proname = f);
41861
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Afhandeling' END; RETURN NEXT;
41862
+ END LOOP;
41863
+ onderdeel := 'Favoriete vervoerder ingesteld en bruikbaar';
41864
+ IF to_regprocedure('public.afh_favorite_carrier()') IS NULL THEN ok := false; toelichting := 'functie ontbreekt';
41865
+ ELSE
41866
+ BEGIN v_fav := public.logistics_carrier_favorite_id()::text; EXCEPTION WHEN OTHERS THEN v_fav := NULL; END;
41867
+ ok := public.logistics_try_uuid(v_fav) IS NOT NULL AND public.logistics_carrier_skip_reason(public.logistics_try_uuid(v_fav)) IS NULL;
41868
+ toelichting := CASE WHEN ok THEN 'ja' WHEN v_fav IS NULL THEN 'geen favoriet ingesteld (Afhandeling → Vervoerders)'
41869
+ ELSE public.logistics_carrier_skip_reason(public.logistics_try_uuid(v_fav)) END;
41870
+ END IF;
41871
+ RETURN NEXT;
41872
+ onderdeel := 'Klanten: customers.default_carrier_id';
41873
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='customers' AND column_name='default_carrier_id');
41874
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Klanten' END; RETURN NEXT;
41875
+ IF to_regclass('public.logistics_carrier_settings') IS NOT NULL AND to_regclass('public.orders') IS NOT NULL
41876
+ AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='orders' AND column_name='carrier_source') THEN
41877
+ SELECT * INTO s FROM public.logistics_carrier_settings_ensure();
41878
+ EXECUTE $q$SELECT count(*) FROM public.orders o
41879
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
41880
+ 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
41881
+ AND (o.carrier_source = 'geen' OR (o.carrier_source IS NULL AND o.created_at >= $1))$q$ INTO n USING s.installed_at;
41882
+ onderdeel := 'Open orders zonder vervoerder'; ok := n = 0; toelichting := n || ' order(s)'; RETURN NEXT;
41883
+ onderdeel := 'Schakelaar bestaande orders'; ok := true;
41884
+ toelichting := CASE WHEN coalesce(s.backfill_existing, false) THEN 'aan' ELSE 'uit (bestaande orders wachten op akkoord)' END; RETURN NEXT;
41885
+ END IF;
41886
+ SELECT count(*) INTO n FROM public.logistics_trace_events e
41887
+ WHERE e.actor IN ('trg_logistics_plan_order_items', 'trg_logistics_plan_orders')
41888
+ AND e.status = 'mislukt' AND e.created_at > now() - interval '24 hours';
41889
+ onderdeel := 'Mislukte herplanning (laatste 24 uur)'; ok := n = 0;
41890
+ 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
41891
+ WHERE e.actor IN ('trg_logistics_plan_order_items', 'trg_logistics_plan_orders') AND e.status = 'mislukt'
41892
+ ORDER BY e.created_at DESC LIMIT 1), '') END;
41893
+ RETURN NEXT;
41894
+ RETURN QUERY SELECT * FROM public.logistics_install_check_194();
41895
+ RETURN QUERY SELECT * FROM public.logistics_install_check_196();
41896
+ RETURN QUERY SELECT * FROM public.logistics_install_check_197();
41897
+ RETURN QUERY SELECT * FROM public.logistics_install_check_198();
41898
+ RETURN QUERY SELECT * FROM public.logistics_access_check();
41899
+ RETURN QUERY SELECT * FROM public.logistics_overload_check();
41900
+ RETURN QUERY SELECT * FROM public.logistics_check_190();
41901
+ RETURN QUERY SELECT * FROM public.logistics_install_check_productiebon();
41902
+ END $function$;
41903
+
41904
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_enqueue(p_order_ids uuid[], p_reden text)
41905
+ RETURNS integer
41906
+ LANGUAGE plpgsql
41907
+ SECURITY DEFINER
41908
+ SET search_path TO 'public'
41909
+ AS $function$
41910
+ DECLARE n int;
41911
+ BEGIN
41912
+ PERFORM public.logistics_acc_guard('server', 'logistics_carrier_enqueue');
41913
+ PERFORM public.logistics_carrier_settings_ensure();
41914
+ INSERT INTO public.logistics_carrier_recalc_queue AS q (order_id, reden)
41915
+ SELECT o.id, p_reden FROM public.orders o
41916
+ JOIN public.logistics_carrier_settings s ON s.id
41917
+ WHERE o.id = ANY (p_order_ids)
41918
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
41919
+ AND o.carrier_source IS DISTINCT FROM 'handmatig'
41920
+ 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
41921
+ AND (s.backfill_existing OR o.created_at >= s.installed_at)
41922
+ ON CONFLICT (order_id) DO UPDATE SET processed_at = NULL, queued_at = now(), reden = EXCLUDED.reden;
41923
+ GET DIAGNOSTICS n = ROW_COUNT;
41924
+ RETURN n;
41925
+ END $function$;
41926
+
41927
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_recalc_run(p_limit integer DEFAULT 200)
41928
+ RETURNS jsonb
41929
+ LANGUAGE plpgsql
41930
+ SECURITY DEFINER
41931
+ SET search_path TO 'public'
41932
+ AS $function$
41933
+ DECLARE s record; r record; o jsonb; v jsonb; v_fr text;
41934
+ n_chk int := 0; n_upd int := 0; n_frozen int := 0; n_hand int := 0; n_err int := 0;
41935
+ BEGIN
41936
+ PERFORM public.logistics_acc_guard('server', 'logistics_carrier_recalc_run');
41937
+ SELECT * INTO s FROM public.logistics_carrier_settings_ensure();
41938
+ IF NOT FOUND THEN RETURN jsonb_build_object('overgeslagen', 'instellingen vervoerder ontbreken'); END IF;
41939
+ FOR r IN
41940
+ WITH kand_open AS (
41941
+ SELECT o.id FROM public.orders o
41942
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
41943
+ 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
41944
+ AND (s.backfill_existing OR o.created_at >= s.installed_at)
41945
+ ), cand AS (
41946
+ SELECT q.order_id AS id, 1 AS prio, q.queued_at AS t
41947
+ FROM public.logistics_carrier_recalc_queue q JOIN kand_open ON kand_open.id = q.order_id
41948
+ WHERE q.processed_at IS NULL
41949
+ UNION ALL
41950
+ SELECT kand_open.id, 2, coalesce(q.last_checked_at, '-infinity'::timestamptz)
41951
+ FROM kand_open LEFT JOIN public.logistics_carrier_recalc_queue q ON q.order_id = kand_open.id
41952
+ WHERE q.order_id IS NULL OR (q.processed_at IS NOT NULL
41953
+ AND (q.last_checked_at IS NULL OR q.last_checked_at < now() - interval '24 hours'))
41954
+ )
41955
+ SELECT c.id FROM (SELECT DISTINCT ON (cand.id) cand.id, cand.prio, cand.t FROM cand ORDER BY cand.id, cand.prio) c
41956
+ ORDER BY c.prio, c.t LIMIT greatest(1, least(coalesce(p_limit, 200), 1000))
41957
+ LOOP
41958
+ BEGIN
41959
+ SELECT to_jsonb(x) INTO o FROM public.orders x WHERE x.id = r.id FOR UPDATE SKIP LOCKED;
41960
+ IF o IS NULL THEN CONTINUE; END IF;
41961
+ n_chk := n_chk + 1;
41962
+ IF o->>'carrier_source' = 'handmatig' THEN
41963
+ n_hand := n_hand + 1;
41964
+ ELSE
41965
+ v_fr := public.logistics_carrier_frozen_reason(r.id);
41966
+ IF v_fr IS NOT NULL THEN
41967
+ n_frozen := n_frozen + 1;
41968
+ ELSE
41969
+ v := public.logistics_resolve_carrier_core(o, false);
41970
+ IF (v->>'carrier_id') IS DISTINCT FROM (o->>'carrier_id')
41971
+ OR (v->>'bron') IS DISTINCT FROM (o->>'carrier_source')
41972
+ OR (v->>'reden') IS DISTINCT FROM (o->>'carrier_reason') THEN
41973
+ PERFORM set_config('logistics.carrier_writer', 'engine', true);
41974
+ UPDATE public.orders
41975
+ SET carrier_id = public.logistics_try_uuid(v->>'carrier_id'), carrier_source = v->>'bron',
41976
+ carrier_reason = v->>'reden', carrier_resolved_at = now()
41977
+ WHERE id = r.id;
41978
+ PERFORM set_config('logistics.carrier_writer', '', true);
41979
+ n_upd := n_upd + 1;
41980
+ END IF;
41981
+ END IF;
41982
+ END IF;
41983
+ INSERT INTO public.logistics_carrier_recalc_queue AS q (order_id, reden, processed_at, last_checked_at)
41984
+ VALUES (r.id, 'Vangnet', now(), now())
41985
+ ON CONFLICT (order_id) DO UPDATE SET processed_at = now(), last_checked_at = now();
41986
+ EXCEPTION WHEN OTHERS THEN
41987
+ PERFORM set_config('logistics.carrier_writer', '', true);
41988
+ n_err := n_err + 1;
41989
+ END;
41990
+ END LOOP;
41991
+ RETURN jsonb_build_object('gecontroleerd', n_chk, 'gewijzigd', n_upd, 'bevroren', n_frozen,
41992
+ 'handmatig', n_hand, 'fouten', n_err, 'bestaandeOrders', s.backfill_existing);
41993
+ END $function$;
41994
+
41995
+ CREATE OR REPLACE FUNCTION public.logistics_carriers_enqueue_orders()
41996
+ RETURNS trigger
41997
+ LANGUAGE plpgsql
41998
+ SECURITY DEFINER
41999
+ SET search_path TO 'public'
42000
+ AS $function$
42001
+ BEGIN
42002
+ BEGIN
42003
+ PERFORM public.logistics_carrier_enqueue(ARRAY(
42004
+ SELECT o.id FROM public.orders o
42005
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
42006
+ 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),
42007
+ 'Vervoerdersinstellingen gewijzigd');
42008
+ EXCEPTION WHEN OTHERS THEN NULL; END;
42009
+ RETURN NULL;
42010
+ END $function$;
42011
+
42012
+ CREATE OR REPLACE FUNCTION public.logistics_scan_delays()
42013
+ RETURNS integer
42014
+ LANGUAGE plpgsql
42015
+ SECURITY DEFINER
42016
+ SET search_path TO 'public'
42017
+ AS $function$
42018
+ DECLARE
42019
+ rec record;
42020
+ cnt integer := 0;
42021
+ BEGIN
42022
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_scan_delays');
42023
+ FOR rec IN
42024
+ SELECT o.id
42025
+ FROM public.orders o
42026
+ WHERE o.deleted_at IS NULL
42027
+ AND o.delivery_date IS NOT NULL
42028
+ AND public.logistics_order_date(o.delivery_date::text) < (now() AT TIME ZONE coalesce(public.logistics_tz(), 'Europe/Amsterdam'))::date
42029
+ AND COALESCE(o.status::text, '') NOT IN ('delivered', 'cancelled', 'Geleverd', 'Geannuleerd')
42030
+ AND NOT EXISTS (
42031
+ SELECT 1 FROM public.logistics_delay_alerts d
42032
+ WHERE d.order_id = o.id AND d.status = 'open')
42033
+ LIMIT 200
42034
+ LOOP
42035
+ PERFORM public.logistics_report_delay(p_order_id => rec.id, p_reason => 'Geplande leverdatum is verstreken', p_expected_date => NULL::date, p_force_customer_email => false);
42036
+ cnt := cnt + 1;
42037
+ END LOOP;
42038
+ RETURN cnt;
42039
+ END;
42040
+ $function$;
42041
+
42042
+ CREATE OR REPLACE FUNCTION public.logistics_plan_housekeeping()
42043
+ RETURNS jsonb
42044
+ LANGUAGE plpgsql
42045
+ SECURITY DEFINER
42046
+ SET search_path TO 'public'
42047
+ AS $function$
42048
+ DECLARE v_resolved int := 0; v_archived int := 0; v_nieuw int := 0; v_alerts int := 0; v_gone int := 0; v_refs text;
42049
+ BEGIN
42050
+ PERFORM public.logistics_acc_guard('server', 'logistics_plan_housekeeping');
42051
+ UPDATE public.logistics_plan_queue q SET status='opgelost', error='opgelost: order is al verzonden',
42052
+ claimed_at=NULL, updated_at=now()
42053
+ FROM public.orders o
42054
+ WHERE o.id = q.order_id AND q.status IN ('geblokkeerd','failed')
42055
+ AND o.status IN ('Verzonden','Gereed voor facturatie');
42056
+ GET DIAGNOSTICS v_resolved = ROW_COUNT;
42057
+ UPDATE public.logistics_plan_queue q SET status='opgelost', error='order bestaat niet meer', claimed_at=NULL, updated_at=now()
42058
+ WHERE q.status IN ('geblokkeerd','failed')
42059
+ AND NOT EXISTS (SELECT 1 FROM public.orders o WHERE o.id = q.order_id AND o.deleted_at IS NULL);
42060
+
42061
+ WITH kand AS (
42062
+ SELECT q.id, q.order_id, o.order_number, public.logistics_plan_inhoud_fp(o.id) AS fp
42063
+ FROM public.logistics_plan_queue q JOIN public.orders o ON o.id = q.order_id
42064
+ WHERE q.status IN ('geblokkeerd','failed')
42065
+ AND o.deleted_at IS NULL
42066
+ AND COALESCE(o.status,'') NOT IN ('Verzonden','Gereed voor facturatie','geannuleerd')
42067
+ AND o.delivery_date IS NOT NULL AND public.logistics_order_date(o.delivery_date::text) < (now() AT TIME ZONE coalesce(public.logistics_tz(), 'Europe/Amsterdam'))::date - 2
42068
+ ), a AS (
42069
+ UPDATE public.logistics_plan_queue q SET status='gearchiveerd', error=COALESCE(q.error, q.reason),
42070
+ order_fp = k.fp, claimed_at=NULL, updated_at=now()
42071
+ FROM kand k WHERE q.id = k.id
42072
+ RETURNING q.id, k.order_id, k.order_number, k.fp
42073
+ ), nieuw AS (
42074
+ SELECT DISTINCT a.order_number FROM a
42075
+ WHERE NOT EXISTS (SELECT 1 FROM public.logistics_plan_queue e
42076
+ WHERE e.order_id = a.order_id AND e.status = 'gearchiveerd' AND e.id <> a.id
42077
+ AND (e.order_fp IS NULL OR e.order_fp = a.fp))
42078
+ )
42079
+ SELECT (SELECT count(*) FROM a), (SELECT count(*) FROM nieuw), (SELECT string_agg(order_number, ', ' ORDER BY order_number) FROM nieuw)
42080
+ INTO v_archived, v_nieuw, v_refs;
42081
+
42082
+ IF v_nieuw > 0 THEN
42083
+ INSERT INTO public.notifications(user_id, title, message, read)
42084
+ SELECT DISTINCT ur.user_id, 'Niet geleverd en gearchiveerd',
42085
+ v_nieuw || ' geblokkeerde order(s) gearchiveerd (leverdatum voorbij): ' || v_refs ||
42086
+ '. Zie Engine › Wachtrij; "Opnieuw aanbieden" zet ze terug.', false
42087
+ FROM public.user_roles ur WHERE ur.role IN ('planner','admin');
42088
+ END IF;
42089
+
42090
+ UPDATE public.logistics_stock_alerts a SET status='resolved', resolved_at=now(),
42091
+ resolution_reason='automatisch opgelost: order verzonden/afgerond', updated_at=now()
42092
+ FROM public.orders o
42093
+ WHERE o.id = a.order_id AND a.status='open'
42094
+ AND o.status IN ('Verzonden','Gereed voor facturatie','geannuleerd');
42095
+ GET DIAGNOSTICS v_alerts = ROW_COUNT;
42096
+
42097
+ UPDATE public.logistics_stock_alerts a SET status='resolved', resolved_at=now(),
42098
+ resolution_reason='order bestaat niet meer', updated_at=now()
42099
+ WHERE a.status='open' AND a.order_id IS NOT NULL
42100
+ AND NOT EXISTS (SELECT 1 FROM public.orders o WHERE o.id = a.order_id AND o.deleted_at IS NULL);
42101
+ GET DIAGNOSTICS v_gone = ROW_COUNT;
42102
+
42103
+ RETURN jsonb_build_object('blokkadesOpgelost', v_resolved, 'gearchiveerd', v_archived, 'gemeld', v_nieuw,
42104
+ 'meldingenOpgelost', v_alerts, 'meldingenZonderOrder', v_gone);
42105
+ END $function$;
42106
+
42107
+ CREATE OR REPLACE FUNCTION public.logistics_cart_plan_overview(p_days integer DEFAULT 2)
42108
+ RETURNS TABLE(order_ref text, delivery_date date, plan_ready boolean, cart_total integer, released boolean, reason text)
42109
+ LANGUAGE plpgsql
42110
+ STABLE SECURITY DEFINER
42111
+ SET search_path TO 'public'
42112
+ AS $function$
42113
+ DECLARE
42114
+ v_days integer := GREATEST(COALESCE(p_days, 2), 1);
42115
+ r record;
42116
+ v_status jsonb;
42117
+ BEGIN
42118
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_cart_plan_overview');
42119
+ FOR r IN
42120
+ SELECT o.order_number, public.logistics_order_date(o.delivery_date::text) AS ddate
42121
+ FROM public.orders o
42122
+ WHERE o.deleted_at IS NULL
42123
+ AND public.logistics_order_date(o.delivery_date::text) >= (now() AT TIME ZONE coalesce(public.logistics_tz(), 'Europe/Amsterdam'))::date
42124
+ AND public.logistics_order_date(o.delivery_date::text) < (now() AT TIME ZONE coalesce(public.logistics_tz(), 'Europe/Amsterdam'))::date + v_days
42125
+ ORDER BY 2, 1
42126
+ LOOP
42127
+ v_status := public.logistics_cart_plan_status(r.order_number);
42128
+ order_ref := r.order_number;
42129
+ delivery_date := r.ddate;
42130
+ plan_ready := COALESCE((v_status->>'ready')::boolean, false);
42131
+ cart_total := COALESCE((v_status->>'cartTotal')::integer, 0);
42132
+ released := COALESCE((v_status->>'released')::boolean, false);
42133
+ reason := v_status->>'reason';
42134
+ RETURN NEXT;
42135
+ END LOOP;
42136
+ END;
42137
+ $function$;
42138
+
42139
+ CREATE OR REPLACE FUNCTION public.logistics_plan_products_trg()
42140
+ RETURNS trigger
42141
+ LANGUAGE plpgsql
42142
+ SECURITY DEFINER
42143
+ SET search_path TO 'public'
42144
+ AS $function$
42145
+ DECLARE r record;
42146
+ BEGIN
42147
+ IF NEW.plant_height_cm IS NOT DISTINCT FROM OLD.plant_height_cm
42148
+ AND NEW.fust_type_id IS NOT DISTINCT FROM OLD.fust_type_id
42149
+ AND NEW.plants_per_crate IS NOT DISTINCT FROM OLD.plants_per_crate
42150
+ AND NEW.crates_per_layer IS NOT DISTINCT FROM OLD.crates_per_layer
42151
+ AND NEW.layers_per_cart IS NOT DISTINCT FROM OLD.layers_per_cart
42152
+ AND NEW.is_layer IS NOT DISTINCT FROM OLD.is_layer THEN
42153
+ RETURN NEW;
42154
+ END IF;
42155
+
42156
+ FOR r IN
42157
+ SELECT DISTINCT o.id
42158
+ FROM logistics_active.order_items oi
42159
+ JOIN public.orders o ON o.id = oi.order_id
42160
+ WHERE oi.product_id = NEW.id
42161
+ AND o.deleted_at IS NULL
42162
+ AND COALESCE(o.is_reservation, false) = false
42163
+ AND o.plan_locked_at IS NULL
42164
+ AND (o.delivery_date IS NULL OR public.logistics_order_date(o.delivery_date::text) >= (now() AT TIME ZONE coalesce(public.logistics_tz(), 'Europe/Amsterdam'))::date)
42165
+ LOOP
42166
+ PERFORM public.logistics_plan_enqueue(r.id, 'product');
42167
+ END LOOP;
42168
+
42169
+ RETURN NEW;
42170
+ END;
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
+