@flowselections/mailbox-orders 1.0.34 → 1.0.37

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.
@@ -156,6 +156,7 @@ export declare const approveProposal: import("@tanstack/start-client-core").Opti
156
156
  crates_per_layer: number;
157
157
  layers_per_cart: number;
158
158
  } | null | undefined;
159
+ needs_sticker?: boolean | undefined;
159
160
  }[];
160
161
  fieldValues?: {
161
162
  field_key: string;
@@ -164,12 +165,27 @@ export declare const approveProposal: import("@tanstack/start-client-core").Opti
164
165
  customerReference?: string | null | undefined;
165
166
  momentKey?: string | null | undefined;
166
167
  acceptMissingPrice?: boolean | undefined;
168
+ lateSignup?: {
169
+ reason: string;
170
+ } | null | undefined;
167
171
  }, Promise<{
172
+ ok: false;
173
+ reason: import("./verkoop-order.js").VerkoopRefusalKind;
174
+ message: string;
175
+ nextDeliveryDate: any;
176
+ orderId?: undefined;
177
+ orderNumber?: undefined;
178
+ alreadyApproved?: undefined;
179
+ shortages?: undefined;
180
+ } | {
168
181
  ok: true;
169
182
  orderId: string;
170
183
  orderNumber: string;
171
184
  alreadyApproved: boolean;
172
185
  shortages: any[];
186
+ reason?: undefined;
187
+ message?: undefined;
188
+ nextDeliveryDate?: undefined;
173
189
  }>>;
174
190
  export declare const rejectProposal: import("@tanstack/start-client-core").OptionalFetcher<readonly [import("@tanstack/start-client-core").FunctionMiddlewareAfterServer<{}, unknown, undefined, {
175
191
  supabase: import("../integrations/supabase/auth-middleware.js").AuthedSupabaseClient;
@@ -476,4 +492,18 @@ export declare const countOutdatedProposals: import("@tanstack/start-client-core
476
492
  }, undefined, undefined, undefined>], undefined, Promise<{
477
493
  count: number;
478
494
  }>>;
495
+ export declare const getStickerDefaults: import("@tanstack/start-client-core").OptionalFetcher<readonly [import("@tanstack/start-client-core").FunctionMiddlewareAfterServer<{}, unknown, undefined, {
496
+ supabase: import("../integrations/supabase/auth-middleware.js").AuthedSupabaseClient;
497
+ userId: string;
498
+ claims: import("@supabase/auth-js").User;
499
+ }, undefined, undefined, undefined>], (input: unknown) => {
500
+ customerId: string | null;
501
+ productIds: string[];
502
+ }, Promise<{
503
+ available: false;
504
+ defaults: Record<string, boolean>;
505
+ } | {
506
+ available: true;
507
+ defaults: Record<string, boolean>;
508
+ }>>;
479
509
  //# sourceMappingURL=mailbox.functions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mailbox.functions.d.ts","sourceRoot":"","sources":["../../src/lib/mailbox.functions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mailbox.functions.d.ts","sourceRoot":"","sources":["../../src/lib/mailbox.functions.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { classifyVerkoopRefusal, existingOrderOrFilter, defaultNeedsSticker } from "./verkoop-order.js";
1
2
  // Server functions voor de Mailbox-bestellingen module
2
3
  import { createServerFn } from "@tanstack/react-start";
3
4
  import { requireSupabaseAuth } from "../integrations/supabase/auth-middleware.js";
@@ -267,6 +268,7 @@ const LineEdit = z.object({
267
268
  })
268
269
  .nullable()
269
270
  .optional(),
271
+ needs_sticker: z.boolean().optional(),
270
272
  });
271
273
  const FieldValueInput = z.object({
272
274
  field_key: z.string().min(1).max(64),
@@ -285,6 +287,7 @@ export const approveProposal = createServerFn({ method: "POST" })
285
287
  customerReference: z.string().max(120).nullable().optional(),
286
288
  momentKey: z.string().max(200).nullable().optional(),
287
289
  acceptMissingPrice: z.boolean().optional(),
290
+ lateSignup: z.object({ reason: z.string().min(3).max(500) }).nullable().optional(),
288
291
  }).parse(input))
289
292
  .handler(async ({ data, context }) => {
290
293
  const { supabaseAdmin } = await import("../integrations/supabase/client.server.js");
@@ -305,42 +308,36 @@ export const approveProposal = createServerFn({ method: "POST" })
305
308
  throw new Error(`Ladingdrager "${loadCarrierLabel(lcList, loadCarrierType) ?? "onbekend"}" is uitgeschakeld in de Rule Engine — bestelling niet aangemaakt.`);
306
309
  }
307
310
  customFields["load_carrier"] = loadCarrierType;
308
- // ─── Leverdatum: haalbaarheid volgens de Logistics Engine ───────────────
309
- // De gevraagde datum is leidend; de Engine beslist of hij haalbaar is.
311
+ // ─── Prijs en referentie ────────────────────────────────────────────────
312
+ // Verkoop accepteert geen regel zonder prijs; dus hier al blokkeren.
313
+ if (data.lines.some((l) => l.unit_price === null || l.unit_price === undefined || l.unit_price <= 0)) {
314
+ throw new Error("Niet alle regels hebben een prijs. Vul bij elke regel een prijs per stuk in.");
315
+ }
316
+ const reference = (data.customerReference ?? "").trim();
317
+ if (reference)
318
+ customFields["customer_reference"] = reference;
319
+ else
320
+ delete customFields["customer_reference"];
310
321
  const carrierId = typeof customFields["carrier"] === "string" ? customFields["carrier"] : null;
311
322
  const deliveryLocationId = typeof customFields["delivery_location"] === "string" ? customFields["delivery_location"] : null;
312
323
  if (!carrierId)
313
324
  throw new Error("Kies een vervoerder.");
314
325
  if (!deliveryLocationId)
315
326
  throw new Error("Kies een afleverlocatie.");
316
- const { data: preview, error: previewErr } = await context.supabase.rpc("logistics_departure_preview", {
317
- p_carrier_id: carrierId,
318
- p_delivery_location: deliveryLocationId,
319
- p_delivery_date: data.deliveryDate,
320
- p_moment_key: data.momentKey ?? null,
321
- });
322
- if (previewErr)
323
- throw new Error(`Vertrekcontrole mislukt: ${previewErr.message}`);
324
- const pv = (preview ?? {});
325
- if (pv.ok !== true)
326
- throw new Error(pv.reason ? `Vertrekcontrole: ${pv.reason}` : "Vertrekcontrole gaf geen uitkomst.");
327
- if (pv.deliveryDateFeasible !== true) {
328
- throw new Error(`Leverdatum ${data.deliveryDate} is niet haalbaar: ${pv.reason ?? "geen reden opgegeven"}` +
329
- (pv.firstFeasibleDeliveryDate ? ` Eerst haalbaar: ${pv.firstFeasibleDeliveryDate}.` : ""));
327
+ if (!data.customerId)
328
+ throw new Error("Selecteer een bestaande klant.");
329
+ if (data.lines.some((line) => !line.product_id || !line.quantity || line.quantity <= 0)) {
330
+ throw new Error("Iedere bestelregel moet aan een bestaand product gekoppeld zijn.");
330
331
  }
331
- if (pv.chosen?.momentKey)
332
- customFields["moment_key"] = pv.chosen.momentKey;
333
- if (pv.chosen?.departureAt)
334
- customFields["departure_at"] = pv.chosen.departureAt;
335
- // ─── Prijs en referentie ────────────────────────────────────────────────
336
- if (data.lines.some((l) => l.unit_price === null || l.unit_price === undefined) && data.acceptMissingPrice !== true) {
337
- throw new Error("Niet alle regels hebben een prijs. Bevestig dat je zonder prijs wilt doorgaan.");
332
+ // ─── Dubbele orders voorkomen: zoek op BEIDE sleutels ───────────────────
333
+ const existing = await findExistingOrderForProposal(supabaseAdmin, data.proposalId);
334
+ if (existing) {
335
+ await markProposalApproved(supabaseAdmin, data.proposalId, context.userId, existing, data.notes, reference, data.fieldValues ?? []);
336
+ return {
337
+ ok: true, orderId: existing.id, orderNumber: existing.order_number,
338
+ alreadyApproved: true, shortages: [],
339
+ };
338
340
  }
339
- const reference = (data.customerReference ?? "").trim();
340
- if (reference)
341
- customFields["customer_reference"] = reference;
342
- else
343
- delete customFields["customer_reference"];
344
341
  // ─── Voorraadcontrole door de Logistics Engine (nooit zelf beslissen) ────
345
342
  const checkLines = data.lines
346
343
  .filter((l) => l.product_id)
@@ -361,79 +358,99 @@ export const approveProposal = createServerFn({ method: "POST" })
361
358
  .map((s) => `${s.product ?? s.productId}: besteld ${s.wanted}, beschikbaar ${s.available}, tekort ${s.missing}`)
362
359
  .join("\n");
363
360
  const blockText = `Geblokkeerd - onvoldoende voorraad\n${detail}`;
364
- await supabaseAdmin
365
- .from("mailbox_order_proposals")
366
- .update({ notes: blockText })
367
- .eq("id", data.proposalId);
361
+ await supabaseAdmin.from("mailbox_order_proposals").update({ notes: blockText }).eq("id", data.proposalId);
368
362
  const { data: blockedProp } = await supabaseAdmin
369
- .from("mailbox_order_proposals")
370
- .select("message_id").eq("id", data.proposalId).maybeSingle();
363
+ .from("mailbox_order_proposals").select("message_id").eq("id", data.proposalId).maybeSingle();
371
364
  if (blockedProp?.message_id) {
372
- await supabaseAdmin
373
- .from("mailbox_messages")
374
- .update({ status: "blocked_insufficient_stock" })
375
- .eq("id", blockedProp.message_id);
365
+ await supabaseAdmin.from("mailbox_messages").update({ status: "blocked_insufficient_stock" }).eq("id", blockedProp.message_id);
376
366
  }
377
367
  throw new Error(blockText);
378
368
  }
379
369
  }
380
- if (!data.customerId)
381
- throw new Error("Selecteer een bestaande klant.");
382
- if (data.lines.some((line) => !line.product_id || !line.quantity || line.quantity <= 0)) {
383
- throw new Error("Iedere bestelregel moet aan een bestaand product gekoppeld zijn.");
370
+ // ─── "Toch aanmaken" na verstreken aanmeldtijd ──────────────────────────
371
+ const lateReason = (data.lateSignup?.reason ?? "").trim();
372
+ const acceptLate = lateReason.length > 0;
373
+ const claims = context.claims ?? {};
374
+ const acceptedBy = acceptLate
375
+ ? String(claims.email ?? claims.user_metadata?.full_name ?? context.userId)
376
+ : null;
377
+ const notesParts = [data.notes ?? ""].filter((s) => s.trim());
378
+ if (acceptLate) {
379
+ notesParts.push(`Toch aangemaakt na verstreken aanmeldtijd door ${acceptedBy} op ${new Date().toISOString()} — reden: ${lateReason}`);
384
380
  }
385
- const { data: approval, error: approvalError } = await supabaseAdmin.rpc("approve_mailbox_order_proposal", {
386
- p_proposal_id: data.proposalId,
387
- p_reviewed_by: context.userId,
381
+ // ─── Order aanmaken via Verkoop (één centrale route) ────────────────────
382
+ const { data: vk, error: vkErr } = await context.supabase.rpc("verkoop_create_order", {
383
+ p_source: "mailbox",
384
+ p_source_id: data.proposalId,
388
385
  p_customer_id: data.customerId,
389
386
  p_delivery_date: data.deliveryDate,
390
- p_notes: data.notes ?? "",
391
- p_custom_fields: customFields,
387
+ p_delivery_location_id: deliveryLocationId,
388
+ p_carrier_id: carrierId,
392
389
  p_lines: data.lines.map((line) => ({
393
390
  product_id: line.product_id,
394
- quantity: line.quantity,
395
- unit: "planten",
396
- unit_price: line.unit_price ?? null,
397
- plants_per_crate: line.belading?.plants_per_crate ?? null,
398
- crates_per_layer: line.belading?.crates_per_layer ?? null,
399
- layers_per_cart: line.belading?.layers_per_cart ?? null,
391
+ quantity_plants: Math.round(line.quantity ?? 0),
392
+ unit_price: line.unit_price,
393
+ needs_sticker: line.needs_sticker === true,
400
394
  })),
401
- p_field_values: data.fieldValues ?? [],
395
+ p_customer_reference: reference || null,
396
+ p_moment_key: data.momentKey ?? null,
397
+ p_accept_late_signup: acceptLate,
398
+ p_accepted_by: acceptedBy,
399
+ p_own_transport: false,
400
+ p_notes: notesParts.join("\n") || null,
402
401
  });
403
- if (approvalError)
404
- throw new Error(approvalError.message);
405
- const result = approval;
406
- if (!result?.orderId || !result.orderNumber) {
407
- throw new Error("Bestelling kon niet veilig worden aangemaakt.");
402
+ if (vkErr)
403
+ throw new Error(`Bestelling aanmaken mislukt: ${vkErr.message}`);
404
+ const res = (vk ?? {});
405
+ if (res.ok !== true || !res.order_id) {
406
+ // Nooit stil mislukken: het voorstel blijft open met een duidelijke reden.
407
+ return {
408
+ ok: false,
409
+ reason: classifyVerkoopRefusal(res.reason),
410
+ message: String(res.reason ?? "Verkoop heeft de bestelling geweigerd zonder reden."),
411
+ nextDeliveryDate: typeof res.next_delivery_date === "string" ? res.next_delivery_date : null,
412
+ };
408
413
  }
409
- // ─── Gebeurtenis melden aan de Logistics Engine ─────────────────────────
410
- const deliveryLocationRef = typeof customFields["delivery_location"] === "string" ? customFields["delivery_location"] : null;
411
- await context.supabase.rpc("logistics_emit_event", {
412
- p_source_module: "mailbox",
413
- p_event_type: "order.received",
414
- p_order_ref: result.orderNumber,
415
- p_payload: {
416
- checkinAt: data.deliveryDate,
417
- pickupAt: pv.chosen?.departureAt ?? data.deliveryDate,
418
- momentKey: pv.chosen?.momentKey ?? null,
419
- customerRef: data.customerId,
420
- deliveryLocationRef,
421
- bufferRef: null,
422
- loadCarrierType,
423
- loadCarriers: [loadCarrierType],
424
- ruleVersion: null,
425
- },
426
- p_shadow: true,
427
- p_idempotency_key: `mailbox:${data.proposalId}:order.received:v1`,
428
- });
414
+ await markProposalApproved(supabaseAdmin, data.proposalId, context.userId, { id: res.order_id, order_number: res.order_number }, notesParts.join("\n") || null, reference, data.fieldValues ?? []);
429
415
  return {
430
416
  ok: true,
431
- orderId: result.orderId,
432
- orderNumber: result.orderNumber,
433
- alreadyApproved: result.alreadyApproved === true,
417
+ orderId: res.order_id,
418
+ orderNumber: res.order_number,
419
+ alreadyApproved: res.already_existed === true,
434
420
  shortages,
435
421
  };
436
422
  });
423
+ /** Zoekt een bestaande order voor dit voorstel op beide sleutels (oud en nieuw). */
424
+ async function findExistingOrderForProposal(admin, proposalId) {
425
+ const { data, error } = await admin
426
+ .from("orders")
427
+ .select("id, order_number")
428
+ .is("deleted_at", null)
429
+ .or(existingOrderOrFilter(proposalId))
430
+ .limit(1);
431
+ if (error)
432
+ throw new Error(`Controle op bestaande order mislukt: ${error.message}`);
433
+ const row = Array.isArray(data) ? data[0] : null;
434
+ return row?.id ? row : null;
435
+ }
436
+ async function markProposalApproved(admin, proposalId, userId, order, notes, reference, fieldValues) {
437
+ for (const fv of fieldValues) {
438
+ await admin.from("mailbox_proposal_field_values").upsert({ proposal_id: proposalId, field_key: fv.field_key, value: fv.value, ai_filled: false, needs_review: false }, { onConflict: "proposal_id,field_key" });
439
+ }
440
+ const { data: prop } = await admin
441
+ .from("mailbox_order_proposals")
442
+ .update({
443
+ status: "approved", reviewed_by: userId, reviewed_at: new Date().toISOString(),
444
+ created_order_id: order.id, order_number: order.order_number, notes,
445
+ customer_reference: reference || null,
446
+ })
447
+ .eq("id", proposalId)
448
+ .select("message_id")
449
+ .maybeSingle();
450
+ if (prop?.message_id) {
451
+ await admin.from("mailbox_messages").update({ status: "converted" }).eq("id", prop.message_id);
452
+ }
453
+ }
437
454
  export const rejectProposal = createServerFn({ method: "POST" })
438
455
  .middleware([requireSupabaseAuth])
439
456
  .inputValidator((input) => z.object({ proposalId: z.string().uuid(), reason: z.string().max(500).optional() }).parse(input))
@@ -1214,3 +1231,29 @@ export const countOutdatedProposals = createServerFn({ method: "GET" })
1214
1231
  const { countOutdatedAgentProposals } = await import("./mailbox-ingest.server.js");
1215
1232
  return { count: await countOutdatedAgentProposals(supabaseAdmin) };
1216
1233
  });
1234
+ // ─── Stickervinkje: beschikbaarheid + standaardwaarden ─────────────────────
1235
+ // Verborgen als de kolom order_items.needs_sticker (Verkoop) ontbreekt.
1236
+ export const getStickerDefaults = createServerFn({ method: "POST" })
1237
+ .middleware([requireSupabaseAuth])
1238
+ .inputValidator((input) => z.object({
1239
+ customerId: z.string().uuid().nullable(),
1240
+ productIds: z.array(z.string().uuid()).max(200),
1241
+ }).parse(input))
1242
+ .handler(async ({ data }) => {
1243
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server.js");
1244
+ const probe = await supabaseAdmin.from("order_items").select("needs_sticker").limit(1);
1245
+ if (probe.error)
1246
+ return { available: false, defaults: {} };
1247
+ const defaults = {};
1248
+ if (!data.customerId || data.productIds.length === 0)
1249
+ return { available: true, defaults };
1250
+ const { data: rows } = await supabaseAdmin
1251
+ .from("customer_product_stickers")
1252
+ .select("customer_key, trade_item_key")
1253
+ .eq("customer_key", data.customerId)
1254
+ .in("trade_item_key", data.productIds);
1255
+ const list = Array.isArray(rows) ? rows : [];
1256
+ for (const id of data.productIds)
1257
+ defaults[id] = defaultNeedsSticker(list, data.customerId, id);
1258
+ return { available: true, defaults };
1259
+ });
@@ -0,0 +1,14 @@
1
+ /** Hulpfuncties rond de Verkoop-route (verkoop_create_order). Puur, zonder IO. */
2
+ export type VerkoopRefusalKind = "departed" | "late_signup" | "other";
3
+ /** Herkent op basis van de Nederlandse reden van Verkoop welk soort weigering het is. */
4
+ export declare function classifyVerkoopRefusal(reason: unknown): VerkoopRefusalKind;
5
+ /** PostgREST or-filter: een order hoort bij dit voorstel via de oude óf de nieuwe sleutel. */
6
+ export declare function existingOrderOrFilter(proposalId: string): string;
7
+ /** "Toch aanmaken" mag alleen met een reden, en nooit als de vervoerder al vertrokken is. */
8
+ export declare function canOverrideLateSignup(kind: VerkoopRefusalKind, reason: string | null | undefined): boolean;
9
+ /** Standaardwaarde stickervinkje: aan als er een sticker bestaat voor deze klant + dit product. */
10
+ export declare function defaultNeedsSticker(stickers: {
11
+ customer_key: string | null;
12
+ trade_item_key: string | null;
13
+ }[], customerId: string | null, productId: string | null): boolean;
14
+ //# sourceMappingURL=verkoop-order.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verkoop-order.d.ts","sourceRoot":"","sources":["../../src/lib/verkoop-order.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAElF,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,aAAa,GAAG,OAAO,CAAC;AAEtE,yFAAyF;AACzF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,GAAG,kBAAkB,CAK1E;AAED,8FAA8F;AAC9F,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,6FAA6F;AAC7F,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE1G;AAED,mGAAmG;AACnG,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE;IAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAAE,EAC1E,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,OAAO,CAGT"}
@@ -0,0 +1,24 @@
1
+ /** Hulpfuncties rond de Verkoop-route (verkoop_create_order). Puur, zonder IO. */
2
+ /** Herkent op basis van de Nederlandse reden van Verkoop welk soort weigering het is. */
3
+ export function classifyVerkoopRefusal(reason) {
4
+ const r = String(reason ?? "").toLowerCase();
5
+ if (r.includes("vertrokken"))
6
+ return "departed";
7
+ if (r.includes("aanmeld") || r.includes("verstreken") || r.includes("niet meer"))
8
+ return "late_signup";
9
+ return "other";
10
+ }
11
+ /** PostgREST or-filter: een order hoort bij dit voorstel via de oude óf de nieuwe sleutel. */
12
+ export function existingOrderOrFilter(proposalId) {
13
+ return `custom_fields->>proposal_id.eq.${proposalId},custom_fields->>mailbox_proposal_id.eq.${proposalId}`;
14
+ }
15
+ /** "Toch aanmaken" mag alleen met een reden, en nooit als de vervoerder al vertrokken is. */
16
+ export function canOverrideLateSignup(kind, reason) {
17
+ return kind === "late_signup" && (reason ?? "").trim().length >= 3;
18
+ }
19
+ /** Standaardwaarde stickervinkje: aan als er een sticker bestaat voor deze klant + dit product. */
20
+ export function defaultNeedsSticker(stickers, customerId, productId) {
21
+ if (!customerId || !productId)
22
+ return false;
23
+ return stickers.some((s) => s.customer_key === customerId && s.trade_item_key === productId);
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowselections/mailbox-orders",
3
- "version": "1.0.34",
3
+ "version": "1.0.37",
4
4
  "private": false,
5
5
  "sideEffects": false,
6
6
  "type": "module",