@flowselections/floriday-verkoop-module 1.0.93 → 1.0.94
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.
- package/dist-lib/components/floriday/FlorydaySyncHealthBanner.d.ts +9 -0
- package/dist-lib/components/floriday/FlorydaySyncHealthBanner.d.ts.map +1 -0
- package/dist-lib/components/floriday/FlorydaySyncHealthBanner.js +26 -0
- package/dist-lib/components/karindeling/AfmetingenTab.d.ts +6 -0
- package/dist-lib/components/karindeling/AfmetingenTab.d.ts.map +1 -0
- package/dist-lib/components/karindeling/AfmetingenTab.js +128 -0
- package/dist-lib/components/karindeling/KarindelingPage.d.ts +2 -0
- package/dist-lib/components/karindeling/KarindelingPage.d.ts.map +1 -0
- package/dist-lib/components/karindeling/KarindelingPage.js +102 -0
- package/dist-lib/components/ui/dropdown-menu.d.ts +9 -0
- package/dist-lib/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist-lib/components/ui/dropdown-menu.js +14 -0
- package/dist-lib/components/verkoop/AnalyticsCards.d.ts +1 -1
- package/dist-lib/components/verkoop/AnalyticsCards.d.ts.map +1 -1
- package/dist-lib/components/verkoop/AutoFulfillmentStatusCard.d.ts +4 -0
- package/dist-lib/components/verkoop/AutoFulfillmentStatusCard.d.ts.map +1 -0
- package/dist-lib/components/verkoop/AutoFulfillmentStatusCard.js +95 -0
- package/dist-lib/components/verkoop/CartAssignmentWizard.d.ts +1 -1
- package/dist-lib/components/verkoop/CartAssignmentWizard.d.ts.map +1 -1
- package/dist-lib/components/verkoop/CartLayersDialog.d.ts +1 -1
- package/dist-lib/components/verkoop/CartLayersDialog.d.ts.map +1 -1
- package/dist-lib/components/verkoop/CreateOrderDialog.d.ts +5 -0
- package/dist-lib/components/verkoop/CreateOrderDialog.d.ts.map +1 -0
- package/dist-lib/components/verkoop/CreateOrderDialog.js +33 -0
- package/dist-lib/components/verkoop/CreateOrderPage.d.ts +1 -1
- package/dist-lib/components/verkoop/CreateOrderPage.d.ts.map +1 -1
- package/dist-lib/components/verkoop/DiagnoseConnectionDialog.d.ts +1 -1
- package/dist-lib/components/verkoop/DiagnoseConnectionDialog.d.ts.map +1 -1
- package/dist-lib/components/verkoop/EditLocalOrderDialog.d.ts +1 -1
- package/dist-lib/components/verkoop/EditLocalOrderDialog.d.ts.map +1 -1
- package/dist-lib/components/verkoop/EngineBufferCard.d.ts +1 -1
- package/dist-lib/components/verkoop/EngineBufferCard.d.ts.map +1 -1
- package/dist-lib/components/verkoop/FloridayStageLine.d.ts +2 -2
- package/dist-lib/components/verkoop/FloridayStageLine.d.ts.map +1 -1
- package/dist-lib/components/verkoop/InvoiceTimingSettings.d.ts +1 -1
- package/dist-lib/components/verkoop/InvoiceTimingSettings.d.ts.map +1 -1
- package/dist-lib/components/verkoop/OrderAfleverbrievenCard.d.ts +1 -1
- package/dist-lib/components/verkoop/OrderAfleverbrievenCard.d.ts.map +1 -1
- package/dist-lib/components/verkoop/OrderCarrierPicker.d.ts +1 -1
- package/dist-lib/components/verkoop/OrderCarrierPicker.d.ts.map +1 -1
- package/dist-lib/components/verkoop/OrderControlStatusCard.d.ts +1 -1
- package/dist-lib/components/verkoop/OrderControlStatusCard.d.ts.map +1 -1
- package/dist-lib/components/verkoop/OrderKarindelingCard.d.ts +1 -1
- package/dist-lib/components/verkoop/OrderKarindelingCard.d.ts.map +1 -1
- package/dist-lib/components/verkoop/OrderLinePackingCard.d.ts +1 -1
- package/dist-lib/components/verkoop/OrderLinePackingCard.d.ts.map +1 -1
- package/dist-lib/components/verkoop/OrderPickupMomentPicker.d.ts +1 -1
- package/dist-lib/components/verkoop/OrderPickupMomentPicker.d.ts.map +1 -1
- package/dist-lib/components/verkoop/OrderShipmentsCard.d.ts +1 -1
- package/dist-lib/components/verkoop/OrderShipmentsCard.d.ts.map +1 -1
- package/dist-lib/components/verkoop/PlanCoverageBadge.d.ts +1 -1
- package/dist-lib/components/verkoop/PlanCoverageBadge.d.ts.map +1 -1
- package/dist-lib/components/verkoop/ProductDimensionsSettings.d.ts +1 -1
- package/dist-lib/components/verkoop/ProductDimensionsSettings.d.ts.map +1 -1
- package/dist-lib/components/verkoop/SalesOrderDetailPage.d.ts +1 -1
- package/dist-lib/components/verkoop/SalesOrderDetailPage.d.ts.map +1 -1
- package/dist-lib/components/verkoop/SalesOrderDetailPage.js +6 -6
- package/dist-lib/components/verkoop/SalesOrderDetailView.d.ts +1 -1
- package/dist-lib/components/verkoop/SalesOrderDetailView.d.ts.map +1 -1
- package/dist-lib/components/verkoop/SalesOrdersFilterPanel.d.ts +1 -1
- package/dist-lib/components/verkoop/SalesOrdersFilterPanel.d.ts.map +1 -1
- package/dist-lib/components/verkoop/SalesOrdersFilters.d.ts +1 -1
- package/dist-lib/components/verkoop/SalesOrdersFilters.d.ts.map +1 -1
- package/dist-lib/components/verkoop/SalesOrdersTable.d.ts +1 -2
- package/dist-lib/components/verkoop/SalesOrdersTable.d.ts.map +1 -1
- package/dist-lib/components/verkoop/SearchableSelect.d.ts +1 -1
- package/dist-lib/components/verkoop/SearchableSelect.d.ts.map +1 -1
- package/dist-lib/components/verkoop/StockConsumptionAlertsBanner.d.ts +1 -1
- package/dist-lib/components/verkoop/StockConsumptionAlertsBanner.d.ts.map +1 -1
- package/dist-lib/components/verkoop/VerkoopOrdersPage.d.ts +1 -1
- package/dist-lib/components/verkoop/VerkoopOrdersPage.d.ts.map +1 -1
- package/dist-lib/components/verkoop/WeekCalendar.d.ts +1 -1
- package/dist-lib/components/verkoop/WeekCalendar.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.js +1 -1
- package/dist-lib/integrations/supabase/client.server.js +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +74 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/cron/mirror-floriday-orders.d.ts +4 -0
- package/dist-lib/lib/cron/mirror-floriday-orders.d.ts.map +1 -0
- package/dist-lib/lib/cron/mirror-floriday-orders.js +23 -0
- package/dist-lib/lib/cron/push-order-statuses.d.ts +4 -0
- package/dist-lib/lib/cron/push-order-statuses.d.ts.map +1 -0
- package/dist-lib/lib/cron/push-order-statuses.js +23 -0
- package/dist-lib/lib/floriday/floriday.server.d.ts +14 -0
- package/dist-lib/lib/floriday/floriday.server.d.ts.map +1 -0
- package/dist-lib/lib/floriday/floriday.server.js +92 -0
- package/dist-lib/lib/floriday/sales-orders.functions.d.ts +4736 -0
- package/dist-lib/lib/floriday/sales-orders.functions.d.ts.map +1 -0
- package/dist-lib/lib/floriday/sales-orders.functions.js +193 -0
- package/dist-lib/lib/floriday-auto-fulfillment.functions.d.ts +77581 -0
- package/dist-lib/lib/floriday-auto-fulfillment.functions.d.ts.map +1 -0
- package/dist-lib/lib/floriday-auto-fulfillment.functions.js +65 -0
- package/dist-lib/lib/floriday-auto-fulfillment.server.d.ts +21 -0
- package/dist-lib/lib/floriday-auto-fulfillment.server.d.ts.map +1 -0
- package/dist-lib/lib/floriday-auto-fulfillment.server.js +352 -0
- package/dist-lib/lib/floriday-status-sync.server.d.ts +14 -0
- package/dist-lib/lib/floriday-status-sync.server.d.ts.map +1 -0
- package/dist-lib/lib/floriday-status-sync.server.js +130 -0
- package/dist-lib/lib/tradeitems.functions.d.ts +12 -1117
- package/dist-lib/lib/tradeitems.functions.d.ts.map +1 -1
- package/dist-lib/lib/useBufferEvents.d.ts +2 -2
- package/dist-lib/lib/useBufferEvents.d.ts.map +1 -1
- package/dist-lib/lib/useLocalSalesOrderDetailQuery.d.ts +2 -2
- package/dist-lib/lib/useLocalSalesOrderDetailQuery.d.ts.map +1 -1
- package/dist-lib/lib/useSalesOrderDetailQuery.d.ts +1 -1
- package/dist-lib/lib/useSalesOrderDetailQuery.d.ts.map +1 -1
- package/dist-lib/lib/useSalesOrdersQuery.d.ts +1 -1
- package/dist-lib/lib/useSalesOrdersQuery.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist-lib/lib/order-cart-count.server.d.ts +0 -8
- package/dist-lib/lib/order-cart-count.server.d.ts.map +0 -1
- package/dist-lib/lib/order-cart-count.server.js +0 -69
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Client-callable helpers around the auto-fulfillment log.
|
|
2
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { requireSupabaseAuth } from "../integrations/supabase/auth-middleware.js";
|
|
5
|
+
export const getPreferredWarehouseStatus = createServerFn({ method: "POST" })
|
|
6
|
+
.middleware([requireSupabaseAuth])
|
|
7
|
+
.handler(async ({ context }) => {
|
|
8
|
+
const { supabase } = context;
|
|
9
|
+
const { data } = await supabase
|
|
10
|
+
.from("floriday_connections")
|
|
11
|
+
.select("id, preferred_warehouse_id")
|
|
12
|
+
.eq("is_active", true)
|
|
13
|
+
.limit(1)
|
|
14
|
+
.maybeSingle();
|
|
15
|
+
return {
|
|
16
|
+
hasPreferredWarehouse: Boolean(data?.preferred_warehouse_id),
|
|
17
|
+
connectionId: data?.id ?? null,
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
const SalesOrderSchema = z.object({ salesOrderId: z.string().min(1) });
|
|
21
|
+
export const getAutoFulfillmentStatus = createServerFn({ method: "POST" })
|
|
22
|
+
.middleware([requireSupabaseAuth])
|
|
23
|
+
.inputValidator((d) => SalesOrderSchema.parse(d))
|
|
24
|
+
.handler(async ({ data, context }) => {
|
|
25
|
+
const { supabase } = context;
|
|
26
|
+
const { data: row } = await supabase
|
|
27
|
+
.from("floriday_auto_fulfillment_log")
|
|
28
|
+
.select("status, reason, error, fulfillment_order_id, attempts, last_attempt_at, updated_at")
|
|
29
|
+
.eq("sales_order_id", data.salesOrderId)
|
|
30
|
+
.order("updated_at", { ascending: false })
|
|
31
|
+
.limit(1)
|
|
32
|
+
.maybeSingle();
|
|
33
|
+
return { row: row ?? null };
|
|
34
|
+
});
|
|
35
|
+
export const retryAutoFulfillment = createServerFn({ method: "POST" })
|
|
36
|
+
.middleware([requireSupabaseAuth])
|
|
37
|
+
.inputValidator((d) => SalesOrderSchema.parse(d))
|
|
38
|
+
.handler(async ({ data, context }) => {
|
|
39
|
+
const { supabase, userId } = context;
|
|
40
|
+
const { data: isAdmin } = await supabase.rpc("has_role", {
|
|
41
|
+
_user_id: userId,
|
|
42
|
+
_role: "admin",
|
|
43
|
+
});
|
|
44
|
+
if (!isAdmin)
|
|
45
|
+
throw new Error("Alleen admins mogen fulfillment orders opnieuw aanmaken");
|
|
46
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server.js");
|
|
47
|
+
const { processOrderForAutoFulfillment } = await import("./floriday-auto-fulfillment.server.js");
|
|
48
|
+
const { data: order, error } = await supabaseAdmin
|
|
49
|
+
.from("floriday_salesorders_cache")
|
|
50
|
+
.select("connection_id, floriday_id, status, is_deleted, data")
|
|
51
|
+
.eq("floriday_id", data.salesOrderId)
|
|
52
|
+
.limit(1)
|
|
53
|
+
.maybeSingle();
|
|
54
|
+
if (error)
|
|
55
|
+
throw new Error(`Salesorder ophalen faalde: ${error.message}`);
|
|
56
|
+
if (!order)
|
|
57
|
+
throw new Error("Salesorder niet gevonden in cache");
|
|
58
|
+
// Force a fresh attempt: clear a prior 'created' guard so we re-run.
|
|
59
|
+
await supabaseAdmin.from("floriday_auto_fulfillment_log")
|
|
60
|
+
.update({ status: "pending", error: null })
|
|
61
|
+
.eq("connection_id", order.connection_id)
|
|
62
|
+
.eq("sales_order_id", order.floriday_id);
|
|
63
|
+
const result = await processOrderForAutoFulfillment(supabaseAdmin, order);
|
|
64
|
+
return result;
|
|
65
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
type SalesOrderRow = {
|
|
3
|
+
connection_id: string;
|
|
4
|
+
floriday_id: string;
|
|
5
|
+
status: string | null;
|
|
6
|
+
is_deleted: boolean | null;
|
|
7
|
+
data: any;
|
|
8
|
+
};
|
|
9
|
+
export declare function processOrderForAutoFulfillment(supabase: SupabaseClient, order: SalesOrderRow): Promise<{
|
|
10
|
+
status: "created" | "skipped" | "failed";
|
|
11
|
+
reason?: string;
|
|
12
|
+
fulfillmentOrderId?: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function processRecentSalesOrders(supabase: SupabaseClient, connectionId: string, sinceIso: string): Promise<{
|
|
15
|
+
processed: number;
|
|
16
|
+
created: number;
|
|
17
|
+
failed: number;
|
|
18
|
+
skipped: number;
|
|
19
|
+
}>;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=floriday-auto-fulfillment.server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floriday-auto-fulfillment.server.d.ts","sourceRoot":"","sources":["../../src/lib/floriday-auto-fulfillment.server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAI5D,KAAK,aAAa,GAAG;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3B,IAAI,EAAE,GAAG,CAAC;CACX,CAAC;AA4NF,wBAAsB,8BAA8B,CAClD,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,aAAa,GACnB,OAAO,CAAC;IAAE,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA+IrG;AAED,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,cAAc,EACxB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CA2ElF"}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { florydayWrite } from "./floriday-gateway.server.js";
|
|
2
|
+
async function loadConnectionMeta(supabase, connectionId) {
|
|
3
|
+
const { data } = await supabase
|
|
4
|
+
.from("floriday_connections")
|
|
5
|
+
.select("id, environment, api_key, is_active, gln_code, preferred_warehouse_id")
|
|
6
|
+
.eq("id", connectionId)
|
|
7
|
+
.maybeSingle();
|
|
8
|
+
return data ?? null;
|
|
9
|
+
}
|
|
10
|
+
async function loadProductPacking(supabase, tradeItemId) {
|
|
11
|
+
if (!tradeItemId)
|
|
12
|
+
return null;
|
|
13
|
+
const { data } = await supabase
|
|
14
|
+
.from("products")
|
|
15
|
+
.select("plants_per_crate, crates_per_layer, layers_per_cart")
|
|
16
|
+
.eq("trade_item_key", tradeItemId)
|
|
17
|
+
.limit(1)
|
|
18
|
+
.maybeSingle();
|
|
19
|
+
if (!data)
|
|
20
|
+
return null;
|
|
21
|
+
return {
|
|
22
|
+
plants_per_crate: Number(data.plants_per_crate) || 0,
|
|
23
|
+
crates_per_layer: Number(data.crates_per_layer) || 0,
|
|
24
|
+
layers_per_cart: Number(data.layers_per_cart) || 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function upsertLog(supabase, connectionId, salesOrderId, patch) {
|
|
28
|
+
return supabase.from("floriday_auto_fulfillment_log").upsert({
|
|
29
|
+
connection_id: connectionId,
|
|
30
|
+
sales_order_id: salesOrderId,
|
|
31
|
+
last_attempt_at: new Date().toISOString(),
|
|
32
|
+
...patch,
|
|
33
|
+
}, { onConflict: "connection_id,sales_order_id" });
|
|
34
|
+
}
|
|
35
|
+
async function existingFulfillmentForOrder(supabase, connectionId, salesOrderId) {
|
|
36
|
+
// Scan the most recent fulfillment orders for this connection and look for
|
|
37
|
+
// a nested loadCarrierItems entry referencing this sales order.
|
|
38
|
+
const { data } = await supabase
|
|
39
|
+
.from("floriday_fulfillment_orders_cache")
|
|
40
|
+
.select("data")
|
|
41
|
+
.eq("connection_id", connectionId)
|
|
42
|
+
.order("fetched_at", { ascending: false })
|
|
43
|
+
.limit(500);
|
|
44
|
+
const rows = data ?? [];
|
|
45
|
+
for (const row of rows) {
|
|
46
|
+
const carriers = row?.data?.loadCarriers;
|
|
47
|
+
if (!Array.isArray(carriers))
|
|
48
|
+
continue;
|
|
49
|
+
for (const c of carriers) {
|
|
50
|
+
const items = c?.loadCarrierItems;
|
|
51
|
+
if (!Array.isArray(items))
|
|
52
|
+
continue;
|
|
53
|
+
for (const it of items) {
|
|
54
|
+
if (String(it?.salesOrderId ?? "") === salesOrderId)
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
async function alreadyProcessed(supabase, connectionId, salesOrderId) {
|
|
62
|
+
const { data } = await supabase
|
|
63
|
+
.from("floriday_auto_fulfillment_log")
|
|
64
|
+
.select("status")
|
|
65
|
+
.eq("connection_id", connectionId)
|
|
66
|
+
.eq("sales_order_id", salesOrderId)
|
|
67
|
+
.maybeSingle();
|
|
68
|
+
return data?.status === "created";
|
|
69
|
+
}
|
|
70
|
+
async function loadDeliveryContext(supabase, connectionId, salesOrderId) {
|
|
71
|
+
const { data } = await supabase
|
|
72
|
+
.from("floriday_delivery_orders_cache")
|
|
73
|
+
.select("data")
|
|
74
|
+
.eq("connection_id", connectionId)
|
|
75
|
+
.eq("is_deleted", false)
|
|
76
|
+
.order("fetched_at", { ascending: false })
|
|
77
|
+
.limit(500);
|
|
78
|
+
const rows = data ?? [];
|
|
79
|
+
for (const row of rows) {
|
|
80
|
+
const d = row?.data;
|
|
81
|
+
const reqs = Array.isArray(d?.fulfillmentRequests) ? d.fulfillmentRequests : [];
|
|
82
|
+
const matching = [];
|
|
83
|
+
for (const r of reqs) {
|
|
84
|
+
if (String(r?.salesOrderId ?? "") !== salesOrderId)
|
|
85
|
+
continue;
|
|
86
|
+
matching.push({
|
|
87
|
+
fulfillmentRequestId: String(r?.fulfillmentRequestId ?? ""),
|
|
88
|
+
salesOrderId: String(r?.salesOrderId ?? ""),
|
|
89
|
+
loadCarrier: r?.loadCarrier ?? null,
|
|
90
|
+
numberOfPackages: Number(r?.numberOfPackages ?? 0) || 0,
|
|
91
|
+
deliveryRemarks: r?.deliveryRemarks ?? null,
|
|
92
|
+
tradeItemId: r?.tradeItemId ?? null,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (matching.length > 0) {
|
|
96
|
+
return {
|
|
97
|
+
deliveryOrderId: String(d?.deliveryOrderId ?? ""),
|
|
98
|
+
despatchWarehouseId: d?.despatchWarehouseId ?? null,
|
|
99
|
+
destination: d?.destination ?? null,
|
|
100
|
+
latestDeliveryDateTime: d?.deliveryDateTime ?? null,
|
|
101
|
+
requests: matching,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function buildFulfillmentPayload(order, warehouseId, ctx) {
|
|
108
|
+
const warnings = [];
|
|
109
|
+
const supplierOrganizationId = String(order?.supplierOrganizationId ?? "");
|
|
110
|
+
const customerOrganizationId = String(order?.customerOrganizationId ?? "");
|
|
111
|
+
// Group requests by load-carrier type to build one loadCarrier per group.
|
|
112
|
+
const byCarrier = new Map();
|
|
113
|
+
for (const r of ctx.requests) {
|
|
114
|
+
const key = r.loadCarrier ?? "DANISH_TROLLEY";
|
|
115
|
+
if (!byCarrier.has(key))
|
|
116
|
+
byCarrier.set(key, []);
|
|
117
|
+
byCarrier.get(key).push(r);
|
|
118
|
+
}
|
|
119
|
+
const loadCarriers = [];
|
|
120
|
+
let sortIndex = 1;
|
|
121
|
+
for (const [carrier, requests] of byCarrier) {
|
|
122
|
+
loadCarriers.push({
|
|
123
|
+
sortIndex: sortIndex++,
|
|
124
|
+
loadCarrierType: carrier === "NONE" ? "DANISH_TROLLEY" : carrier,
|
|
125
|
+
numberOfAdditionalLayers: 0,
|
|
126
|
+
loadCarrierItems: requests.map((r, idx) => {
|
|
127
|
+
const item = {
|
|
128
|
+
sortIndex: idx + 1,
|
|
129
|
+
fulfillmentRequestId: r.fulfillmentRequestId,
|
|
130
|
+
tradeItemId: r.tradeItemId ?? String(order?.tradeItemId ?? ""),
|
|
131
|
+
salesOrderId: r.salesOrderId,
|
|
132
|
+
numberOfPackages: r.numberOfPackages,
|
|
133
|
+
};
|
|
134
|
+
const remarks = (r.deliveryRemarks ?? "").toString().trim();
|
|
135
|
+
if (remarks)
|
|
136
|
+
item.deliveryRemarks = remarks;
|
|
137
|
+
return item;
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const payload = {
|
|
142
|
+
fulfillmentOrderId: crypto.randomUUID(),
|
|
143
|
+
type: order?.tradeInstrument ?? "DIRECT_SALES",
|
|
144
|
+
despatchWarehouseId: warehouseId,
|
|
145
|
+
latestDeliveryDateTime: ctx.latestDeliveryDateTime ?? order?.delivery?.latestDeliveryDateTime ?? null,
|
|
146
|
+
supplierOrganizationId,
|
|
147
|
+
fulfillingOrganizationId: supplierOrganizationId,
|
|
148
|
+
destination: ctx.destination ?? {
|
|
149
|
+
organizationId: customerOrganizationId,
|
|
150
|
+
location: order?.delivery?.location ?? null,
|
|
151
|
+
warehouseId: null,
|
|
152
|
+
},
|
|
153
|
+
loadCarriers,
|
|
154
|
+
oneLabelOnly: false,
|
|
155
|
+
};
|
|
156
|
+
return { payload, warnings };
|
|
157
|
+
}
|
|
158
|
+
const SKIPPABLE_STATUSES = new Set(["REJECTED", "CANCELLED", "CANCELED", "EXPIRED"]);
|
|
159
|
+
export async function processOrderForAutoFulfillment(supabase, order) {
|
|
160
|
+
const salesOrderId = order.floriday_id;
|
|
161
|
+
const connectionId = order.connection_id;
|
|
162
|
+
if (order.is_deleted) {
|
|
163
|
+
await upsertLog(supabase, connectionId, salesOrderId, {
|
|
164
|
+
status: "skipped",
|
|
165
|
+
reason: "sales-order-deleted",
|
|
166
|
+
});
|
|
167
|
+
return { status: "skipped", reason: "sales-order-deleted" };
|
|
168
|
+
}
|
|
169
|
+
if (order.status && SKIPPABLE_STATUSES.has(String(order.status).toUpperCase())) {
|
|
170
|
+
await upsertLog(supabase, connectionId, salesOrderId, {
|
|
171
|
+
status: "skipped",
|
|
172
|
+
reason: `status-${order.status}`,
|
|
173
|
+
});
|
|
174
|
+
return { status: "skipped", reason: `status-${order.status}` };
|
|
175
|
+
}
|
|
176
|
+
if (await alreadyProcessed(supabase, connectionId, salesOrderId)) {
|
|
177
|
+
return { status: "skipped", reason: "already-created" };
|
|
178
|
+
}
|
|
179
|
+
if (await existingFulfillmentForOrder(supabase, connectionId, salesOrderId)) {
|
|
180
|
+
await upsertLog(supabase, connectionId, salesOrderId, {
|
|
181
|
+
status: "skipped",
|
|
182
|
+
reason: "existing-fulfillment",
|
|
183
|
+
});
|
|
184
|
+
return { status: "skipped", reason: "existing-fulfillment" };
|
|
185
|
+
}
|
|
186
|
+
const conn = await loadConnectionMeta(supabase, connectionId);
|
|
187
|
+
if (!conn) {
|
|
188
|
+
await upsertLog(supabase, connectionId, salesOrderId, {
|
|
189
|
+
status: "failed",
|
|
190
|
+
error: "connection-not-found",
|
|
191
|
+
});
|
|
192
|
+
return { status: "failed", reason: "connection-not-found" };
|
|
193
|
+
}
|
|
194
|
+
// Warehouse fallback chain:
|
|
195
|
+
// 1) connection.preferred_warehouse_id
|
|
196
|
+
// 2) salesorder.data.despatchWarehouseId
|
|
197
|
+
// 3) first non-deleted warehouse from floriday_warehouses_cache
|
|
198
|
+
let warehouseId = conn.preferred_warehouse_id ?? null;
|
|
199
|
+
let warehouseFallback = null;
|
|
200
|
+
if (!warehouseId) {
|
|
201
|
+
const orderWarehouse = String(order.data?.despatchWarehouseId ?? "").trim();
|
|
202
|
+
if (orderWarehouse) {
|
|
203
|
+
warehouseId = orderWarehouse;
|
|
204
|
+
warehouseFallback = "warehouse-fallback:order";
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!warehouseId) {
|
|
208
|
+
const { data: wh } = await supabase
|
|
209
|
+
.from("floriday_warehouses_cache")
|
|
210
|
+
.select("floriday_id, is_default")
|
|
211
|
+
.eq("connection_id", connectionId)
|
|
212
|
+
.eq("is_deleted", false)
|
|
213
|
+
.order("is_default", { ascending: false })
|
|
214
|
+
.limit(1)
|
|
215
|
+
.maybeSingle();
|
|
216
|
+
const cached = String(wh?.floriday_id ?? "").trim();
|
|
217
|
+
if (cached) {
|
|
218
|
+
warehouseId = cached;
|
|
219
|
+
warehouseFallback = "warehouse-fallback:cache";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!warehouseId) {
|
|
223
|
+
await upsertLog(supabase, connectionId, salesOrderId, {
|
|
224
|
+
status: "skipped",
|
|
225
|
+
reason: "no-warehouse",
|
|
226
|
+
});
|
|
227
|
+
return { status: "skipped", reason: "no-warehouse" };
|
|
228
|
+
}
|
|
229
|
+
// Load Floriday-issued fulfillment requests from the matching delivery order.
|
|
230
|
+
const deliveryCtx = await loadDeliveryContext(supabase, connectionId, salesOrderId);
|
|
231
|
+
if (!deliveryCtx || deliveryCtx.requests.length === 0) {
|
|
232
|
+
await upsertLog(supabase, connectionId, salesOrderId, {
|
|
233
|
+
status: "skipped",
|
|
234
|
+
reason: "no-delivery-order",
|
|
235
|
+
});
|
|
236
|
+
return { status: "skipped", reason: "no-delivery-order" };
|
|
237
|
+
}
|
|
238
|
+
// Prefer the delivery order's despatchWarehouseId when Floriday supplies it.
|
|
239
|
+
const effectiveWarehouseId = deliveryCtx.despatchWarehouseId || warehouseId;
|
|
240
|
+
const { payload, warnings } = buildFulfillmentPayload(order.data, effectiveWarehouseId, deliveryCtx);
|
|
241
|
+
if (warehouseFallback && !deliveryCtx.despatchWarehouseId)
|
|
242
|
+
warnings.push(warehouseFallback);
|
|
243
|
+
// Increment attempts.
|
|
244
|
+
const { data: existing } = await supabase
|
|
245
|
+
.from("floriday_auto_fulfillment_log")
|
|
246
|
+
.select("attempts")
|
|
247
|
+
.eq("connection_id", connectionId)
|
|
248
|
+
.eq("sales_order_id", salesOrderId)
|
|
249
|
+
.maybeSingle();
|
|
250
|
+
const attempts = Number(existing?.attempts ?? 0) + 1;
|
|
251
|
+
const env = (conn.environment === "production" || conn.environment === "live" ? "live" : "staging");
|
|
252
|
+
const result = await florydayWrite(supabase, env, {
|
|
253
|
+
module: "warehouse",
|
|
254
|
+
action: "add-fulfillment-order",
|
|
255
|
+
method: "POST",
|
|
256
|
+
path: "/fulfillment-orders",
|
|
257
|
+
referenceId: salesOrderId,
|
|
258
|
+
body: payload,
|
|
259
|
+
}, null);
|
|
260
|
+
if (!result.ok) {
|
|
261
|
+
await upsertLog(supabase, connectionId, salesOrderId, {
|
|
262
|
+
status: "failed",
|
|
263
|
+
attempts,
|
|
264
|
+
error: (result.error ?? `HTTP ${result.status}`).slice(0, 500),
|
|
265
|
+
reason: warnings.join(",") || null,
|
|
266
|
+
});
|
|
267
|
+
return { status: "failed", reason: result.error };
|
|
268
|
+
}
|
|
269
|
+
const fulfillmentOrderId = String(result.data?.fulfillmentOrderId ?? result.data?.id ?? payload.fulfillmentOrderId ?? "");
|
|
270
|
+
await upsertLog(supabase, connectionId, salesOrderId, {
|
|
271
|
+
status: "created",
|
|
272
|
+
attempts,
|
|
273
|
+
fulfillment_order_id: fulfillmentOrderId || null,
|
|
274
|
+
error: null,
|
|
275
|
+
reason: warnings.join(",") || null,
|
|
276
|
+
});
|
|
277
|
+
return { status: "created", fulfillmentOrderId };
|
|
278
|
+
}
|
|
279
|
+
export async function processRecentSalesOrders(supabase, connectionId, sinceIso) {
|
|
280
|
+
// 1) Recently synced / updated sales orders in this cycle.
|
|
281
|
+
const { data: recent } = await supabase
|
|
282
|
+
.from("floriday_salesorders_cache")
|
|
283
|
+
.select("connection_id, floriday_id, status, is_deleted, data, updated_at")
|
|
284
|
+
.eq("connection_id", connectionId)
|
|
285
|
+
.gte("updated_at", sinceIso)
|
|
286
|
+
.limit(200);
|
|
287
|
+
const rowMap = new Map();
|
|
288
|
+
for (const r of (recent ?? [])) {
|
|
289
|
+
rowMap.set(r.floriday_id, r);
|
|
290
|
+
}
|
|
291
|
+
// 2) Backlog: sales orders that never got a log row, or were previously
|
|
292
|
+
// skipped/failed because the preferred warehouse wasn't set yet.
|
|
293
|
+
const { data: allOrders } = await supabase
|
|
294
|
+
.from("floriday_salesorders_cache")
|
|
295
|
+
.select("connection_id, floriday_id, status, is_deleted, data")
|
|
296
|
+
.eq("connection_id", connectionId)
|
|
297
|
+
.eq("is_deleted", false)
|
|
298
|
+
.limit(500);
|
|
299
|
+
const orderList = allOrders ?? [];
|
|
300
|
+
if (orderList.length > 0) {
|
|
301
|
+
const ids = orderList.map((o) => o.floriday_id);
|
|
302
|
+
const { data: logs } = await supabase
|
|
303
|
+
.from("floriday_auto_fulfillment_log")
|
|
304
|
+
.select("sales_order_id, status, reason")
|
|
305
|
+
.eq("connection_id", connectionId)
|
|
306
|
+
.in("sales_order_id", ids);
|
|
307
|
+
const logMap = new Map();
|
|
308
|
+
for (const l of (logs ?? [])) {
|
|
309
|
+
logMap.set(l.sales_order_id, { status: l.status, reason: l.reason });
|
|
310
|
+
}
|
|
311
|
+
for (const o of orderList) {
|
|
312
|
+
const existing = logMap.get(o.floriday_id);
|
|
313
|
+
if (!existing) {
|
|
314
|
+
rowMap.set(o.floriday_id, o);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (existing.status === "failed") {
|
|
318
|
+
rowMap.set(o.floriday_id, o);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (existing.status === "skipped" &&
|
|
322
|
+
(existing.reason === "no-preferred-warehouse" ||
|
|
323
|
+
existing.reason === "no-warehouse" ||
|
|
324
|
+
existing.reason === "no-delivery-order")) {
|
|
325
|
+
rowMap.set(o.floriday_id, o);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const rows = Array.from(rowMap.values());
|
|
330
|
+
let created = 0;
|
|
331
|
+
let failed = 0;
|
|
332
|
+
let skipped = 0;
|
|
333
|
+
for (const row of rows) {
|
|
334
|
+
try {
|
|
335
|
+
const r = await processOrderForAutoFulfillment(supabase, row);
|
|
336
|
+
if (r.status === "created")
|
|
337
|
+
created++;
|
|
338
|
+
else if (r.status === "failed")
|
|
339
|
+
failed++;
|
|
340
|
+
else
|
|
341
|
+
skipped++;
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
failed++;
|
|
345
|
+
await upsertLog(supabase, row.connection_id, row.floriday_id, {
|
|
346
|
+
status: "failed",
|
|
347
|
+
error: err.message?.slice(0, 500) ?? "unknown error",
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return { processed: rows.length, created, failed, skipped };
|
|
352
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type SyncAction = "commit" | "cancel" | null;
|
|
2
|
+
/** Welke Floriday-actie hoort bij onze opgeslagen status? */
|
|
3
|
+
export declare function actionForStatus(status: string): SyncAction;
|
|
4
|
+
export interface StatusPushResult {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
processed: number;
|
|
7
|
+
pushed: number;
|
|
8
|
+
skipped: number;
|
|
9
|
+
failed: number;
|
|
10
|
+
errors: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function pushOrderStatusesToFloriday(limit?: number): Promise<StatusPushResult>;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=floriday-status-sync.server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floriday-status-sync.server.d.ts","sourceRoot":"","sources":["../../src/lib/floriday-status-sync.server.ts"],"names":[],"mappings":"AAWA,KAAK,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAAC;AAW7C,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAM1D;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,wBAAsB,2BAA2B,CAC/C,KAAK,SAAM,GACV,OAAO,CAAC,gBAAgB,CAAC,CAuH3B"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Statuswijzigingen uit ons ERP doorzetten naar Floriday.
|
|
2
|
+
//
|
|
3
|
+
// Floriday kent voor een verkooporder maar twee schrijfacties op de status:
|
|
4
|
+
// PATCH /sales-orders/{id}/commit (ACCEPTED -> COMMITTED)
|
|
5
|
+
// PATCH /sales-orders/{id}/cancel (ACCEPTED -> CANCELLED)
|
|
6
|
+
// Verzending/levering gaat in Floriday niet via de verkooporder maar via
|
|
7
|
+
// fulfillment-orders (afleverbrieven); dat blijft de bestaande route.
|
|
8
|
+
//
|
|
9
|
+
// Geen omgevingswaarden in deze module: environment en sleutels komen uit
|
|
10
|
+
// floriday_connections / de centrale auth-module.
|
|
11
|
+
const CANCEL_STATUSES = new Set([
|
|
12
|
+
"geannuleerd",
|
|
13
|
+
"cancelled",
|
|
14
|
+
"canceled",
|
|
15
|
+
"vervallen",
|
|
16
|
+
"afgewezen",
|
|
17
|
+
"rejected",
|
|
18
|
+
]);
|
|
19
|
+
/** Welke Floriday-actie hoort bij onze opgeslagen status? */
|
|
20
|
+
export function actionForStatus(status) {
|
|
21
|
+
const s = String(status ?? "").trim().toLowerCase();
|
|
22
|
+
if (CANCEL_STATUSES.has(s))
|
|
23
|
+
return "cancel";
|
|
24
|
+
// Alle overige werkstatussen betekenen: wij gaan hem leveren.
|
|
25
|
+
if (s.length === 0)
|
|
26
|
+
return null;
|
|
27
|
+
return "commit";
|
|
28
|
+
}
|
|
29
|
+
export async function pushOrderStatusesToFloriday(limit = 100) {
|
|
30
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server.js");
|
|
31
|
+
const { florydayWrite } = await import("./floriday-gateway.server.js");
|
|
32
|
+
const result = {
|
|
33
|
+
ok: true,
|
|
34
|
+
processed: 0,
|
|
35
|
+
pushed: 0,
|
|
36
|
+
skipped: 0,
|
|
37
|
+
failed: 0,
|
|
38
|
+
errors: [],
|
|
39
|
+
};
|
|
40
|
+
const { data: queue, error: qErr } = await supabaseAdmin
|
|
41
|
+
.from("floriday_status_sync_queue")
|
|
42
|
+
.select("order_id, floriday_sales_order_id, target_status, attempts")
|
|
43
|
+
.eq("state", "pending")
|
|
44
|
+
.order("requested_at", { ascending: true })
|
|
45
|
+
.limit(Math.min(500, Math.max(1, limit)));
|
|
46
|
+
if (qErr)
|
|
47
|
+
throw new Error(`statuswachtrij lezen faalde: ${qErr.message}`);
|
|
48
|
+
if (!queue || queue.length === 0)
|
|
49
|
+
return result;
|
|
50
|
+
// Actieve Floriday-connectie bepaalt de omgeving. Ontbreekt die, dan stoppen
|
|
51
|
+
// we bewust met een duidelijke fout in plaats van een aanname.
|
|
52
|
+
const { data: conn } = await supabaseAdmin
|
|
53
|
+
.from("floriday_connections")
|
|
54
|
+
.select("environment")
|
|
55
|
+
.eq("is_active", true)
|
|
56
|
+
.limit(1)
|
|
57
|
+
.maybeSingle();
|
|
58
|
+
const environment = conn?.environment
|
|
59
|
+
? String(conn.environment).toLowerCase() === "production" ||
|
|
60
|
+
String(conn.environment).toLowerCase() === "live"
|
|
61
|
+
? "live"
|
|
62
|
+
: "staging"
|
|
63
|
+
: null;
|
|
64
|
+
if (!environment) {
|
|
65
|
+
throw new Error("Geen actieve Floriday-connectie gevonden; statuswijzigingen worden niet verstuurd.");
|
|
66
|
+
}
|
|
67
|
+
const salesOrderIds = queue.map((q) => String(q.floriday_sales_order_id));
|
|
68
|
+
const { data: cached } = await supabaseAdmin
|
|
69
|
+
.from("floriday_salesorders_cache")
|
|
70
|
+
.select("sales_order_id, data")
|
|
71
|
+
.in("sales_order_id", salesOrderIds);
|
|
72
|
+
const floridayStatusById = new Map();
|
|
73
|
+
for (const row of cached ?? []) {
|
|
74
|
+
const status = String((row.data ?? {})?.status ?? "").toUpperCase();
|
|
75
|
+
floridayStatusById.set(String(row.sales_order_id), status);
|
|
76
|
+
}
|
|
77
|
+
for (const item of queue) {
|
|
78
|
+
result.processed += 1;
|
|
79
|
+
const orderId = String(item.order_id);
|
|
80
|
+
const salesOrderId = String(item.floriday_sales_order_id);
|
|
81
|
+
const action = actionForStatus(String(item.target_status ?? ""));
|
|
82
|
+
const currentFloriday = floridayStatusById.get(salesOrderId) ?? "";
|
|
83
|
+
const markDone = async (state, error) => {
|
|
84
|
+
await supabaseAdmin.from("floriday_status_sync_queue")
|
|
85
|
+
.update({
|
|
86
|
+
state,
|
|
87
|
+
error,
|
|
88
|
+
attempts: Number(item.attempts ?? 0) + 1,
|
|
89
|
+
processed_at: new Date().toISOString(),
|
|
90
|
+
})
|
|
91
|
+
.eq("order_id", orderId);
|
|
92
|
+
};
|
|
93
|
+
if (!action) {
|
|
94
|
+
result.skipped += 1;
|
|
95
|
+
await markDone("skipped", "Geen Floriday-actie voor deze status");
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// Floriday accepteert commit/cancel alleen vanuit ACCEPTED. Staat de order
|
|
99
|
+
// daar al verder, dan is er niets te doen (en is dat geen fout).
|
|
100
|
+
if (currentFloriday && currentFloriday !== "ACCEPTED") {
|
|
101
|
+
const alreadyThere = (action === "cancel" && currentFloriday === "CANCELLED") ||
|
|
102
|
+
(action === "commit" && currentFloriday === "COMMITTED");
|
|
103
|
+
result.skipped += 1;
|
|
104
|
+
await markDone(alreadyThere ? "done" : "skipped", alreadyThere
|
|
105
|
+
? null
|
|
106
|
+
: `Floriday-status is ${currentFloriday}; ${action} is daar niet meer toegestaan`);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const res = await florydayWrite(supabaseAdmin, environment, {
|
|
110
|
+
module: "sales",
|
|
111
|
+
action: `sales-order-${action}`,
|
|
112
|
+
method: "PATCH",
|
|
113
|
+
path: `/sales-orders/${encodeURIComponent(salesOrderId)}/${action}`,
|
|
114
|
+
referenceId: salesOrderId,
|
|
115
|
+
}, null);
|
|
116
|
+
if (res.ok) {
|
|
117
|
+
result.pushed += 1;
|
|
118
|
+
await markDone("done", null);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
result.failed += 1;
|
|
122
|
+
const msg = `${salesOrderId}: ${res.error ?? `HTTP ${res.status}`}`;
|
|
123
|
+
result.errors.push(msg);
|
|
124
|
+
await markDone("failed", msg.slice(0, 500));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (result.failed > 0)
|
|
128
|
+
result.ok = false;
|
|
129
|
+
return result;
|
|
130
|
+
}
|