@flowselections/floriday-voorraad 1.0.66 → 1.0.68
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/hooks-entry.d.ts +11 -0
- package/dist-lib/hooks-entry.d.ts.map +1 -0
- package/dist-lib/hooks-entry.js +10 -0
- package/dist-lib/integrations/supabase/types.d.ts +122 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/inventory-settings.functions.d.ts +1 -1
- package/dist-lib/server-hooks/floriday-base-supply.d.ts +3 -0
- package/dist-lib/server-hooks/floriday-base-supply.d.ts.map +1 -0
- package/dist-lib/server-hooks/floriday-base-supply.js +73 -0
- package/dist-lib/server-hooks/floriday-continuous-stock-pull.d.ts +3 -0
- package/dist-lib/server-hooks/floriday-continuous-stock-pull.d.ts.map +1 -0
- package/dist-lib/server-hooks/floriday-continuous-stock-pull.js +168 -0
- package/dist-lib/server-hooks/floriday-continuous-stock.d.ts +3 -0
- package/dist-lib/server-hooks/floriday-continuous-stock.d.ts.map +1 -0
- package/dist-lib/server-hooks/floriday-continuous-stock.js +111 -0
- package/dist-lib/server-hooks/floriday-stock-reconcile.d.ts +3 -0
- package/dist-lib/server-hooks/floriday-stock-reconcile.d.ts.map +1 -0
- package/dist-lib/server-hooks/floriday-stock-reconcile.js +235 -0
- package/dist-lib/server-hooks/guard.d.ts +3 -0
- package/dist-lib/server-hooks/guard.d.ts.map +1 -0
- package/dist-lib/server-hooks/guard.js +60 -0
- package/dist-lib/server-hooks/logistics-tasks-run.d.ts +3 -0
- package/dist-lib/server-hooks/logistics-tasks-run.d.ts.map +1 -0
- package/dist-lib/server-hooks/logistics-tasks-run.js +37 -0
- package/package.json +54 -3
|
@@ -5,7 +5,7 @@ export declare const saveInventorySettings: import("@tanstack/start-client-core"
|
|
|
5
5
|
}, undefined, undefined, undefined>], (data: unknown) => {
|
|
6
6
|
connectionId: string;
|
|
7
7
|
productIds: string[];
|
|
8
|
-
stockMode?: "
|
|
8
|
+
stockMode?: "DISABLED" | "FOLLOW_ERP" | "MAXIMUM" | "FIXED_REFILL" | "MANUAL" | undefined;
|
|
9
9
|
bufferQuantity?: number | undefined;
|
|
10
10
|
maximumQuantity?: number | null | undefined;
|
|
11
11
|
fixedQuantity?: number | null | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floriday-base-supply.d.ts","sourceRoot":"","sources":["../../src/server-hooks/floriday-base-supply.ts"],"names":[],"mappings":"AA2EA,yHAAyH;AACzH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAElF"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verwerker "weekaanbod aanmaken" (M15b). Gewekt door de database zodra een
|
|
3
|
+
* wachtrijregel needs_base_supply wordt (en door een herhaaltaak die alleen
|
|
4
|
+
* bestaat zolang er regels op een nieuwe poging wachten).
|
|
5
|
+
*
|
|
6
|
+
* Authenticatie: apikey-header, vergeleken met de publieke databasesleutel
|
|
7
|
+
* (zelfde als de andere Voorraad-hooks). Open punt: naar een gedeeld geheim.
|
|
8
|
+
* De route verwerkt alleen bestaande regels en geeft geen gegevens terug.
|
|
9
|
+
*/
|
|
10
|
+
import { runGuardedHook } from "./guard";
|
|
11
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
12
|
+
async function handle(request) {
|
|
13
|
+
const json = (b, status = 200) => new Response(JSON.stringify(b), { status, headers: { "Content-Type": "application/json" } });
|
|
14
|
+
const apikey = request.headers.get("apikey") ??
|
|
15
|
+
request.headers.get("x-api-key") ??
|
|
16
|
+
request.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
|
|
17
|
+
const expected = process.env.SUPABASE_PUBLISHABLE_KEY ?? process.env.SUPABASE_ANON_KEY ?? "";
|
|
18
|
+
if (!expected || !apikey || apikey !== expected)
|
|
19
|
+
return json({ error: "Unauthorized" }, 401);
|
|
20
|
+
const { stockPublishAllowed } = await import("../lib/floriday-write-gate.server.js");
|
|
21
|
+
const permission = await stockPublishAllowed();
|
|
22
|
+
if (!permission.allowed)
|
|
23
|
+
return json({ processed: 0, waiting: true, reason: permission.reason });
|
|
24
|
+
const { getAdminClient } = await import("../lib/supabase-admin.server.js");
|
|
25
|
+
const admin = getAdminClient();
|
|
26
|
+
const { resolveActiveFloridayConnection } = await import("../lib/floriday-env.server.js");
|
|
27
|
+
const { gatewaySupplyLineWeek } = await import("../lib/floriday-gateway.server.js");
|
|
28
|
+
const { priceForRow, buildRequest, decide } = await import("../lib/floriday-base-supply.js");
|
|
29
|
+
const conn = await resolveActiveFloridayConnection(admin);
|
|
30
|
+
const { data: rows, error } = await admin.rpc("claim_floriday_base_supply", {
|
|
31
|
+
p_worker: "voorraad-base-supply",
|
|
32
|
+
});
|
|
33
|
+
if (error)
|
|
34
|
+
return json({ error: `claim: ${error.message}` }, 500);
|
|
35
|
+
const summary = { processed: 0, done: 0, retry: 0, stop: 0, week_past: 0, no_price: 0 };
|
|
36
|
+
let first = true;
|
|
37
|
+
for (const row of (rows ?? [])) {
|
|
38
|
+
summary.processed++;
|
|
39
|
+
const price = priceForRow(row);
|
|
40
|
+
if (!price.ok) {
|
|
41
|
+
await admin.rpc("complete_floriday_base_supply", { p_id: row.id, p_kind: "no_price", p_label: price.label });
|
|
42
|
+
summary.no_price++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!first)
|
|
46
|
+
await sleep(1000); // max 1 aanvraag per seconde
|
|
47
|
+
first = false;
|
|
48
|
+
let d;
|
|
49
|
+
try {
|
|
50
|
+
const { httpStatus, body } = await gatewaySupplyLineWeek(buildRequest(row, price.price, conn.environment));
|
|
51
|
+
d = decide(httpStatus, body, row.target_week);
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
d = { kind: "retry", label: "Floriday tijdelijk niet bereikbaar", error: String(e?.message ?? e) };
|
|
55
|
+
}
|
|
56
|
+
await admin.rpc("complete_floriday_base_supply", {
|
|
57
|
+
p_id: row.id,
|
|
58
|
+
p_kind: d.kind,
|
|
59
|
+
p_label: d.label,
|
|
60
|
+
p_error: d.error ?? null,
|
|
61
|
+
p_attempt_id: d.attemptId ?? null,
|
|
62
|
+
p_outcome: d.outcome ?? null,
|
|
63
|
+
p_price_status: d.priceStatus ?? null,
|
|
64
|
+
p_price: price.price,
|
|
65
|
+
});
|
|
66
|
+
summary[d.kind]++;
|
|
67
|
+
}
|
|
68
|
+
return json({ ok: true, ...summary });
|
|
69
|
+
}
|
|
70
|
+
/** Shell-handler voor /api/public/hooks/floriday-base-supply. Schakelaar: voorraad_hook_enabled.floriday_base_supply. */
|
|
71
|
+
export async function handleFloridayBaseSupply(request) {
|
|
72
|
+
return runGuardedHook(request, "floriday_base_supply", handle, "floriday-base-supply");
|
|
73
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Shell-handler voor /api/public/hooks/floriday-continuous-stock-pull. Schakelaar: voorraad_hook_enabled.continuous_stock_pull. */
|
|
2
|
+
export declare function handleFloridayContinuousStockPull(request: Request): Promise<Response>;
|
|
3
|
+
//# sourceMappingURL=floriday-continuous-stock-pull.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floriday-continuous-stock-pull.d.ts","sourceRoot":"","sources":["../../src/server-hooks/floriday-continuous-stock-pull.ts"],"names":[],"mappings":"AAoNA,oIAAoI;AACpI,wBAAsB,iCAAiC,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAE3F"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cron-endpoint (elke minuut vanuit pg_cron): haalt per actieve Floriday-
|
|
3
|
+
* connectie voor elk gepubliceerd trade-item de actuele voorraad op via
|
|
4
|
+
* `GET /trade-items/{tradeItemId}/continuous-stock` en cachet het aantal
|
|
5
|
+
* in `floriday_continuous_stock_cache`.
|
|
6
|
+
*
|
|
7
|
+
* Auth: apikey-header met de Supabase publishable key (identiek aan de
|
|
8
|
+
* bestaande push-hook).
|
|
9
|
+
*
|
|
10
|
+
* Rate limits en CPU-tijd op Cloudflare Workers: max 200 items per run,
|
|
11
|
+
* oudste `fetched_at` eerst; parallel 5 tegelijk per connectie.
|
|
12
|
+
*/
|
|
13
|
+
import { runGuardedHook } from "./guard";
|
|
14
|
+
const MAX_ITEMS_PER_RUN = 200;
|
|
15
|
+
const PARALLEL = 5;
|
|
16
|
+
async function pMapLimited(items, limit, worker) {
|
|
17
|
+
const results = new Array(items.length);
|
|
18
|
+
let i = 0;
|
|
19
|
+
const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
20
|
+
while (i < items.length) {
|
|
21
|
+
const idx = i++;
|
|
22
|
+
results[idx] = await worker(items[idx]);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
await Promise.all(runners);
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
28
|
+
async function handle(request) {
|
|
29
|
+
{
|
|
30
|
+
void 0;
|
|
31
|
+
const apikey = request.headers.get("apikey") ??
|
|
32
|
+
request.headers.get("x-api-key") ??
|
|
33
|
+
request.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
|
|
34
|
+
const expected = process.env.SUPABASE_PUBLISHABLE_KEY ??
|
|
35
|
+
process.env.SUPABASE_ANON_KEY ??
|
|
36
|
+
"";
|
|
37
|
+
if (!expected || !apikey || apikey !== expected) {
|
|
38
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
39
|
+
}
|
|
40
|
+
const { getAdminClient } = await import("../lib/supabase-admin.server.js");
|
|
41
|
+
const supabaseAdmin = getAdminClient();
|
|
42
|
+
const { gatewayRead } = await import("../lib/floriday-gateway.server.js");
|
|
43
|
+
const { resolveActiveFloridayConnection } = await import("../lib/floriday-env.server.js");
|
|
44
|
+
const activeConnection = await resolveActiveFloridayConnection(supabaseAdmin);
|
|
45
|
+
const connections = [activeConnection];
|
|
46
|
+
let totalOk = 0;
|
|
47
|
+
let totalFail = 0;
|
|
48
|
+
const perConnection = [];
|
|
49
|
+
for (const conn of connections ?? []) {
|
|
50
|
+
const environment = conn.environment;
|
|
51
|
+
// Kandidaat-trade-items voor deze connectie (gepubliceerd, niet
|
|
52
|
+
// verwijderd), oudste cache-entry eerst zodat we alles cyclisch
|
|
53
|
+
// verversen binnen een aantal runs.
|
|
54
|
+
const { data: items, error: itemsErr } = await supabaseAdmin
|
|
55
|
+
.from("floriday_trade_items_cache")
|
|
56
|
+
.select("floriday_id")
|
|
57
|
+
.eq("connection_id", conn.id)
|
|
58
|
+
.eq("is_deleted", false)
|
|
59
|
+
.limit(2000);
|
|
60
|
+
if (itemsErr) {
|
|
61
|
+
console.error(`[continuous-stock-pull] items ${conn.id}:`, itemsErr.message);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const ids = (items ?? [])
|
|
65
|
+
.map((r) => r.floriday_id)
|
|
66
|
+
.filter((v) => !!v);
|
|
67
|
+
if (!ids.length)
|
|
68
|
+
continue;
|
|
69
|
+
// Sorteer op fetched_at (oudste eerst); ontbrekende cache-entries
|
|
70
|
+
// hebben voorrang.
|
|
71
|
+
const { data: existing } = await supabaseAdmin
|
|
72
|
+
.from("floriday_continuous_stock_cache")
|
|
73
|
+
.select("trade_item_id, fetched_at")
|
|
74
|
+
.eq("connection_id", conn.id)
|
|
75
|
+
.in("trade_item_id", ids);
|
|
76
|
+
const fetchedMap = new Map();
|
|
77
|
+
for (const r of existing ?? []) {
|
|
78
|
+
fetchedMap.set(r.trade_item_id, r.fetched_at);
|
|
79
|
+
}
|
|
80
|
+
const sorted = ids.slice().sort((a, b) => {
|
|
81
|
+
const ta = fetchedMap.get(a);
|
|
82
|
+
const tb = fetchedMap.get(b);
|
|
83
|
+
if (!ta && !tb)
|
|
84
|
+
return 0;
|
|
85
|
+
if (!ta)
|
|
86
|
+
return -1;
|
|
87
|
+
if (!tb)
|
|
88
|
+
return 1;
|
|
89
|
+
return ta.localeCompare(tb);
|
|
90
|
+
});
|
|
91
|
+
const batch = sorted.slice(0, MAX_ITEMS_PER_RUN);
|
|
92
|
+
let ok = 0;
|
|
93
|
+
let fail = 0;
|
|
94
|
+
const upserts = [];
|
|
95
|
+
await pMapLimited(batch, PARALLEL, async (tradeItemId) => {
|
|
96
|
+
try {
|
|
97
|
+
const res = await gatewayRead({
|
|
98
|
+
environment,
|
|
99
|
+
path: `/suppliers-api-2026v1/trade-items/${tradeItemId}/continuous-stock`,
|
|
100
|
+
});
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
// 404 = geen beschikbaarheid ingesteld → niet beschikbaar.
|
|
103
|
+
if (res.status === 404) {
|
|
104
|
+
upserts.push({
|
|
105
|
+
connection_id: conn.id,
|
|
106
|
+
trade_item_id: tradeItemId,
|
|
107
|
+
number_of_pieces: 0,
|
|
108
|
+
fetched_at: new Date().toISOString(),
|
|
109
|
+
});
|
|
110
|
+
ok++;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
console.error(`[continuous-stock-pull] ${tradeItemId}: HTTP ${res.status}`);
|
|
114
|
+
fail++;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Floriday `ContinuousStock` is een beschikbaarheids-toggle:
|
|
118
|
+
// { available: boolean }. We cachen dat als 1 (beschikbaar) of
|
|
119
|
+
// 0 (niet beschikbaar).
|
|
120
|
+
const data = res.data ?? {};
|
|
121
|
+
const pieces = typeof data.numberOfPieces === "number"
|
|
122
|
+
? data.numberOfPieces
|
|
123
|
+
: data.available === true
|
|
124
|
+
? 1
|
|
125
|
+
: 0;
|
|
126
|
+
upserts.push({
|
|
127
|
+
connection_id: conn.id,
|
|
128
|
+
trade_item_id: tradeItemId,
|
|
129
|
+
number_of_pieces: pieces,
|
|
130
|
+
fetched_at: new Date().toISOString(),
|
|
131
|
+
});
|
|
132
|
+
ok++;
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
console.error(`[continuous-stock-pull] ${tradeItemId}:`, e?.message ?? e);
|
|
136
|
+
fail++;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
if (upserts.length) {
|
|
140
|
+
const { error: upErr } = await supabaseAdmin
|
|
141
|
+
.from("floriday_continuous_stock_cache")
|
|
142
|
+
.upsert(upserts, {
|
|
143
|
+
onConflict: "connection_id,trade_item_id",
|
|
144
|
+
});
|
|
145
|
+
if (upErr) {
|
|
146
|
+
console.error(`[continuous-stock-pull] upsert ${conn.id}:`, upErr.message);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
totalOk += ok;
|
|
150
|
+
totalFail += fail;
|
|
151
|
+
perConnection.push({
|
|
152
|
+
connection_id: conn.id,
|
|
153
|
+
items: batch.length,
|
|
154
|
+
ok,
|
|
155
|
+
fail,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return new Response(JSON.stringify({
|
|
159
|
+
ok: totalOk,
|
|
160
|
+
failed: totalFail,
|
|
161
|
+
connections: perConnection,
|
|
162
|
+
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Shell-handler voor /api/public/hooks/floriday-continuous-stock-pull. Schakelaar: voorraad_hook_enabled.continuous_stock_pull. */
|
|
166
|
+
export async function handleFloridayContinuousStockPull(request) {
|
|
167
|
+
return runGuardedHook(request, "continuous_stock_pull", handle, "floriday-continuous-stock-pull");
|
|
168
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floriday-continuous-stock.d.ts","sourceRoot":"","sources":["../../src/server-hooks/floriday-continuous-stock.ts"],"names":[],"mappings":"AA8IA,0HAA0H;AAC1H,wBAAsB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEvF"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verwerker voor de voorraad-wachtrij (`floriday_continuous_stock_queue`).
|
|
3
|
+
*
|
|
4
|
+
* Dit endpoint is de *terugvalweg*: de vaste verwerker is de Floriday-
|
|
5
|
+
* Authenticatie-module, die dezelfde database deelt en dezelfde twee
|
|
6
|
+
* database-functies gebruikt (`claim_floriday_stock_queue` en
|
|
7
|
+
* `complete_floriday_stock_queue`). Doordat claimen atomair gebeurt, kunnen
|
|
8
|
+
* beide verwerkers naast elkaar draaien zonder dubbele pushes.
|
|
9
|
+
*
|
|
10
|
+
* Authenticatie: apikey-header met de anon key (dit endpoint zit onder
|
|
11
|
+
* `/api/public/*`, dus platform-auth wordt overgeslagen; wij verifiëren
|
|
12
|
+
* de apikey zelf om misbruik te voorkomen).
|
|
13
|
+
*/
|
|
14
|
+
import { runGuardedHook } from "./guard";
|
|
15
|
+
/** Vaste maximale batch per ronde: beschermt de Floriday-snelheidslimiet. */
|
|
16
|
+
const BATCH_SIZE = 25;
|
|
17
|
+
/** Wachttijd voordat de terugvalweg een blijven liggen regel zelf oppakt. */
|
|
18
|
+
const STALE_AFTER_MS = 5 * 60000;
|
|
19
|
+
async function handle(request) {
|
|
20
|
+
const apikey = request.headers.get("apikey") ??
|
|
21
|
+
request.headers.get("x-api-key") ??
|
|
22
|
+
request.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
|
|
23
|
+
const expected = process.env.SUPABASE_PUBLISHABLE_KEY ?? process.env.SUPABASE_ANON_KEY ?? "";
|
|
24
|
+
if (!expected || !apikey || apikey !== expected) {
|
|
25
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
26
|
+
status: 401,
|
|
27
|
+
headers: { "Content-Type": "application/json" },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// Fail-closed poort: elke ronde opnieuw toestemming vragen. Zonder
|
|
31
|
+
// toestemming blijft de wachtrij simpelweg wachten; er wordt niets geclaimd,
|
|
32
|
+
// niets weggegooid en niets stiekem opnieuw geprobeerd.
|
|
33
|
+
const { stockPublishAllowed } = await import("../lib/floriday-write-gate.server.js");
|
|
34
|
+
const permission = await stockPublishAllowed();
|
|
35
|
+
if (!permission.allowed) {
|
|
36
|
+
return new Response(JSON.stringify({ processed: 0, ok: 0, failed: 0, waiting: true, reason: permission.reason }), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
37
|
+
}
|
|
38
|
+
const { getAdminClient } = await import("../lib/supabase-admin.server.js");
|
|
39
|
+
const supabaseAdmin = getAdminClient();
|
|
40
|
+
const { resolveActiveFloridayConnection } = await import("../lib/floriday-env.server.js");
|
|
41
|
+
const { pushStockRow, isRateLimited } = await import("../lib/floriday-stock-supply.server.js");
|
|
42
|
+
const activeConnection = await resolveActiveFloridayConnection(supabaseAdmin);
|
|
43
|
+
const admin = supabaseAdmin;
|
|
44
|
+
// Zelfherstel: regels van een oude koppeling worden omgezet naar de
|
|
45
|
+
// actieve koppeling in plaats van definitief afgekeurd.
|
|
46
|
+
await admin
|
|
47
|
+
.from("floriday_continuous_stock_queue")
|
|
48
|
+
.update({ connection_id: activeConnection.id })
|
|
49
|
+
.eq("status", "pending")
|
|
50
|
+
.neq("connection_id", activeConnection.id);
|
|
51
|
+
// De authenticatiemodule is de vaste verwerker. Deze terugvalweg pakt alleen
|
|
52
|
+
// regels op die daar al enkele minuten zijn blijven liggen, zodat beide
|
|
53
|
+
// verwerkers elkaar niet voor de voeten lopen.
|
|
54
|
+
const graceCutoff = new Date(Date.now() - STALE_AFTER_MS).toISOString();
|
|
55
|
+
const { count: staleCount } = await admin
|
|
56
|
+
.from("floriday_continuous_stock_queue")
|
|
57
|
+
.select("id", { count: "exact", head: true })
|
|
58
|
+
.eq("status", "pending")
|
|
59
|
+
.lt("created_at", graceCutoff);
|
|
60
|
+
if (!staleCount) {
|
|
61
|
+
return new Response(JSON.stringify({ processed: 0, ok: 0, failed: 0, idle: true, owner: "auth-module" }), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
62
|
+
}
|
|
63
|
+
const { data: claimed, error: claimError } = await admin.rpc("claim_floriday_stock_queue", { p_limit: BATCH_SIZE, p_worker: "voorraadmodule-fallback" });
|
|
64
|
+
if (claimError) {
|
|
65
|
+
return new Response(JSON.stringify({ error: `queue claim: ${claimError.message}` }), { status: 500, headers: { "Content-Type": "application/json" } });
|
|
66
|
+
}
|
|
67
|
+
const rows = (claimed ?? []);
|
|
68
|
+
let ok = 0;
|
|
69
|
+
let failed = 0;
|
|
70
|
+
let rateLimited = false;
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
if (rateLimited) {
|
|
73
|
+
// Ronde afbreken: de rest blijft geclaimd en valt na de lock-time-out
|
|
74
|
+
// vanzelf terug in de wachtrij.
|
|
75
|
+
await admin.rpc("complete_floriday_stock_queue", {
|
|
76
|
+
p_id: row.id,
|
|
77
|
+
p_ok: false,
|
|
78
|
+
p_error: "Snelheidslimiet van Floriday bereikt; automatisch opnieuw geprobeerd.",
|
|
79
|
+
p_permanent: false,
|
|
80
|
+
p_attempt_id: null,
|
|
81
|
+
});
|
|
82
|
+
failed++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const outcome = await pushStockRow({
|
|
86
|
+
supabaseAdmin,
|
|
87
|
+
environment: activeConnection.environment,
|
|
88
|
+
row,
|
|
89
|
+
});
|
|
90
|
+
if (outcome.ok) {
|
|
91
|
+
ok++;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
failed++;
|
|
95
|
+
if (outcome.error && isRateLimited(outcome.error))
|
|
96
|
+
rateLimited = true;
|
|
97
|
+
}
|
|
98
|
+
await admin.rpc("complete_floriday_stock_queue", {
|
|
99
|
+
p_id: row.id,
|
|
100
|
+
p_ok: outcome.ok,
|
|
101
|
+
p_error: outcome.ok ? outcome.quantitySkipped ?? null : outcome.error ?? null,
|
|
102
|
+
p_permanent: outcome.permanent ?? false,
|
|
103
|
+
p_attempt_id: outcome.attemptId ?? null,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return new Response(JSON.stringify({ processed: rows.length, ok, failed, rateLimited }), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
107
|
+
}
|
|
108
|
+
/** Shell-handler voor /api/public/hooks/floriday-continuous-stock. Schakelaar: voorraad_hook_enabled.continuous_stock. */
|
|
109
|
+
export async function handleFloridayContinuousStock(request) {
|
|
110
|
+
return runGuardedHook(request, "continuous_stock", handle, "floriday-continuous-stock");
|
|
111
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floriday-stock-reconcile.d.ts","sourceRoot":"","sources":["../../src/server-hooks/floriday-stock-reconcile.ts"],"names":[],"mappings":"AAwPA,wHAAwH;AACxH,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEtF"}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconciliatie: leest de werkelijke standen bij Floriday terug en vergelijkt
|
|
3
|
+
* ze met wat wij gewenst hebben.
|
|
4
|
+
*
|
|
5
|
+
* Bewust langzaam: lezen van `base-supply` is bij Floriday begrensd op één
|
|
6
|
+
* call per seconde. Per ronde wordt daarom een beperkt aantal weken gelezen
|
|
7
|
+
* (de lopende week plus de horizon), nooit per artikel.
|
|
8
|
+
*
|
|
9
|
+
* Bij een verschil zonder openstaande wachtrijregel wordt het artikel als
|
|
10
|
+
* DRIFT gemarkeerd en één correctie ingepland.
|
|
11
|
+
*/
|
|
12
|
+
import { runGuardedHook } from "./guard";
|
|
13
|
+
const MAX_WEEKS_PER_RUN = 5;
|
|
14
|
+
async function handle(request) {
|
|
15
|
+
const apikey = request.headers.get("apikey") ??
|
|
16
|
+
request.headers.get("x-api-key") ??
|
|
17
|
+
request.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
|
|
18
|
+
const expected = process.env.SUPABASE_PUBLISHABLE_KEY ?? process.env.SUPABASE_ANON_KEY ?? "";
|
|
19
|
+
if (!expected || !apikey || apikey !== expected) {
|
|
20
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
21
|
+
status: 401,
|
|
22
|
+
headers: { "Content-Type": "application/json" },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const { getAdminClient } = await import("../lib/supabase-admin.server.js");
|
|
26
|
+
const admin = getAdminClient();
|
|
27
|
+
const { resolveActiveFloridayConnection } = await import("../lib/floriday-env.server.js");
|
|
28
|
+
const { isoWeekOf } = await import("../lib/floriday-2026v1.server.js");
|
|
29
|
+
const connection = await resolveActiveFloridayConnection(admin);
|
|
30
|
+
const remote = new Map();
|
|
31
|
+
const errors = [];
|
|
32
|
+
let reads = 0;
|
|
33
|
+
let readErrors = 0;
|
|
34
|
+
let rateLimited = 0;
|
|
35
|
+
let lastHttpStatus = null;
|
|
36
|
+
let currentWeekRead = false;
|
|
37
|
+
const { data: runRow, error: runErr } = await admin
|
|
38
|
+
.from("floriday_reconcile_runs")
|
|
39
|
+
.insert({ connection_id: connection.id })
|
|
40
|
+
.select("id")
|
|
41
|
+
.single();
|
|
42
|
+
if (runErr)
|
|
43
|
+
throw new Error(`Run-log kon niet worden aangemaakt: ${runErr.message}`);
|
|
44
|
+
const runId = runRow.id;
|
|
45
|
+
// Lezen via /api/public/gateway/floriday-get van de authenticatiemodule;
|
|
46
|
+
// per week één verzoek, minimaal 1 s ertussen.
|
|
47
|
+
const { readBaseSupplyWeeks } = await import("../lib/base-supply-reader.server.js");
|
|
48
|
+
const weeks = [];
|
|
49
|
+
for (let offset = 0; offset < MAX_WEEKS_PER_RUN; offset++) {
|
|
50
|
+
const date = new Date();
|
|
51
|
+
date.setUTCDate(date.getUTCDate() + offset * 7);
|
|
52
|
+
weeks.push(isoWeekOf(date));
|
|
53
|
+
}
|
|
54
|
+
let endpoint = "";
|
|
55
|
+
let environment = null;
|
|
56
|
+
let missingScope = false;
|
|
57
|
+
const weekLog = [];
|
|
58
|
+
try {
|
|
59
|
+
const out = await readBaseSupplyWeeks(weeks);
|
|
60
|
+
endpoint = out.endpoint;
|
|
61
|
+
environment = out.environment;
|
|
62
|
+
lastHttpStatus = out.httpStatus;
|
|
63
|
+
missingScope = out.missingScope;
|
|
64
|
+
out.results.forEach((r, i) => {
|
|
65
|
+
reads++;
|
|
66
|
+
if (r.rateLimited)
|
|
67
|
+
rateLimited++;
|
|
68
|
+
weekLog.push({ year: r.year, week: r.week, ok: r.ok, status: r.httpStatus, rows: r.rows.length,
|
|
69
|
+
error: r.error, retryAfterMs: r.retryAfterMs, rateLimited: r.rateLimited, missingScope: r.missingScope });
|
|
70
|
+
if (!r.ok) {
|
|
71
|
+
readErrors++;
|
|
72
|
+
errors.push(`week ${r.year}-${r.week}: ${r.error ?? "onbekende fout"} (status ${r.httpStatus ?? "?"})`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (i === 0) {
|
|
76
|
+
currentWeekRead = true;
|
|
77
|
+
for (const row of r.rows) {
|
|
78
|
+
remote.set(row.tradeItemId, { pieces: row.numberOfPieces, available: row.isAvailable });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
const unread = weeks.length - out.results.length;
|
|
83
|
+
if (unread > 0) {
|
|
84
|
+
readErrors += unread;
|
|
85
|
+
if (out.error && !errors.includes(out.error))
|
|
86
|
+
errors.unshift(out.error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
readErrors = weeks.length;
|
|
91
|
+
errors.push(`leesroute niet bereikbaar: ${e?.message ?? e}`);
|
|
92
|
+
}
|
|
93
|
+
if (missingScope) {
|
|
94
|
+
errors.unshift("MISSING_SCOPE_SUPPLY_READ: Floriday heeft het leesrecht supply:read nog niet vrijgegeven");
|
|
95
|
+
}
|
|
96
|
+
// Beschikbaarheid uit Floriday voor artikelen zonder weekaanbod.
|
|
97
|
+
let availability = null;
|
|
98
|
+
let availMap = new Map();
|
|
99
|
+
if (currentWeekRead && !missingScope) {
|
|
100
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
101
|
+
try {
|
|
102
|
+
const { readAvailabilities } = await import("../lib/base-supply-reader.server.js");
|
|
103
|
+
const av = await readAvailabilities();
|
|
104
|
+
reads++;
|
|
105
|
+
if (av.rateLimited)
|
|
106
|
+
rateLimited++;
|
|
107
|
+
availMap = av.map;
|
|
108
|
+
availability = { path: "/trade-items/availabilities", ok: av.ok, status: av.httpStatus, rows: av.rows,
|
|
109
|
+
matched: av.map.size, error: av.error, retryAfterMs: av.retryAfterMs, rateLimited: av.rateLimited };
|
|
110
|
+
if (!av.ok)
|
|
111
|
+
errors.push(`availabilities: ${av.error} (status ${av.httpStatus ?? "?"})`);
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
availability = { path: "/trade-items/availabilities", ok: false, error: String(e?.message ?? e) };
|
|
115
|
+
errors.push(`availabilities niet bereikbaar: ${e?.message ?? e}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const { data: settings, error: setErr } = await admin
|
|
119
|
+
.from("floriday_inventory_settings")
|
|
120
|
+
.select("id, connection_id, product_id, floriday_trade_item_id, desired_quantity, desired_availability, stock_mode, sync_status")
|
|
121
|
+
.eq("connection_id", connection.id)
|
|
122
|
+
.not("floriday_trade_item_id", "is", null);
|
|
123
|
+
if (setErr)
|
|
124
|
+
throw new Error(setErr.message);
|
|
125
|
+
const { data: pendingRows } = await admin
|
|
126
|
+
.from("floriday_continuous_stock_queue")
|
|
127
|
+
.select("trade_item_id")
|
|
128
|
+
.eq("connection_id", connection.id)
|
|
129
|
+
.in("status", ["pending", "processing"]);
|
|
130
|
+
const pending = new Set((pendingRows ?? []).map((r) => String(r.trade_item_id)));
|
|
131
|
+
let drift = 0;
|
|
132
|
+
let inSync = 0;
|
|
133
|
+
let noSupply = 0;
|
|
134
|
+
const noSupplyItems = [];
|
|
135
|
+
const nowIso = new Date().toISOString();
|
|
136
|
+
for (const s of settings ?? []) {
|
|
137
|
+
const key = String(s.floriday_trade_item_id);
|
|
138
|
+
const actual = remote.get(key);
|
|
139
|
+
if (missingScope && !actual)
|
|
140
|
+
continue;
|
|
141
|
+
if (!actual) {
|
|
142
|
+
// Alleen als de lopende week echt gelezen is mogen we "geen aanbod" concluderen.
|
|
143
|
+
if (!currentWeekRead || s.stock_mode === "DISABLED" || s.sync_status === "ACTION_REQUIRED")
|
|
144
|
+
continue;
|
|
145
|
+
noSupply++;
|
|
146
|
+
noSupplyItems.push(key);
|
|
147
|
+
await admin
|
|
148
|
+
.from("floriday_inventory_settings")
|
|
149
|
+
.update({
|
|
150
|
+
remote_quantity: null,
|
|
151
|
+
remote_available: availMap.has(key) ? availMap.get(key) : null,
|
|
152
|
+
remote_checked_at: nowIso,
|
|
153
|
+
sync_status: "NO_REMOTE_SUPPLY",
|
|
154
|
+
last_sync_error: "geen aanbod in Floriday, prijs ontbreekt",
|
|
155
|
+
updated_at: nowIso,
|
|
156
|
+
})
|
|
157
|
+
.eq("id", s.id);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const matches = actual.pieces === Number(s.desired_quantity ?? 0) &&
|
|
161
|
+
actual.available === Boolean(s.desired_availability);
|
|
162
|
+
const hasPending = pending.has(key);
|
|
163
|
+
const status = s.stock_mode === "DISABLED"
|
|
164
|
+
? "DISABLED"
|
|
165
|
+
: matches
|
|
166
|
+
? "IN_SYNC"
|
|
167
|
+
: hasPending
|
|
168
|
+
? "PENDING"
|
|
169
|
+
: "DRIFT";
|
|
170
|
+
await admin
|
|
171
|
+
.from("floriday_inventory_settings")
|
|
172
|
+
.update({
|
|
173
|
+
remote_quantity: actual.pieces,
|
|
174
|
+
remote_available: actual.available,
|
|
175
|
+
remote_checked_at: nowIso,
|
|
176
|
+
sync_status: status,
|
|
177
|
+
last_confirmed_at: matches ? nowIso : undefined,
|
|
178
|
+
updated_at: nowIso,
|
|
179
|
+
})
|
|
180
|
+
.eq("id", s.id);
|
|
181
|
+
if (status === "DRIFT") {
|
|
182
|
+
drift++;
|
|
183
|
+
await admin.rpc("floriday_recompute_inventory", {
|
|
184
|
+
p_connection_id: s.connection_id,
|
|
185
|
+
p_product_id: s.product_id,
|
|
186
|
+
p_force: true,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
else if (status === "IN_SYNC") {
|
|
190
|
+
inSync++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
await admin
|
|
194
|
+
.from("floriday_sync_cursors")
|
|
195
|
+
.upsert({
|
|
196
|
+
connection_id: connection.id,
|
|
197
|
+
resource_type: "base-supply-reconcile",
|
|
198
|
+
sequence_number: 0,
|
|
199
|
+
last_sync_at: nowIso,
|
|
200
|
+
last_error: errors.length ? errors.join("; ") : null,
|
|
201
|
+
updated_at: nowIso,
|
|
202
|
+
}, { onConflict: "connection_id,resource_type" });
|
|
203
|
+
const runStatus = readErrors === 0 ? "ok" : currentWeekRead ? "partial" : "failed";
|
|
204
|
+
const response = {
|
|
205
|
+
runId, status: runStatus, endpoint, environment, reason: missingScope ? "MISSING_SCOPE_SUPPLY_READ" : null,
|
|
206
|
+
weeks: weekLog, availability, reads, readErrors, rateLimited,
|
|
207
|
+
checked: remote.size, inSync, drift, noSupply, noSupplyItems, errors,
|
|
208
|
+
};
|
|
209
|
+
const { error: finErr } = await admin
|
|
210
|
+
.from("floriday_reconcile_runs")
|
|
211
|
+
.update({
|
|
212
|
+
finished_at: new Date().toISOString(),
|
|
213
|
+
status: runStatus,
|
|
214
|
+
reads,
|
|
215
|
+
read_errors: readErrors,
|
|
216
|
+
rate_limited: rateLimited,
|
|
217
|
+
last_http_status: lastHttpStatus,
|
|
218
|
+
first_error: errors[0] ?? null,
|
|
219
|
+
items_checked: remote.size,
|
|
220
|
+
items_no_supply: noSupply,
|
|
221
|
+
items_drift: drift,
|
|
222
|
+
response,
|
|
223
|
+
})
|
|
224
|
+
.eq("id", runId);
|
|
225
|
+
if (finErr)
|
|
226
|
+
console.error("[stock-reconcile] run-log afronden mislukt:", finErr.message);
|
|
227
|
+
return new Response(JSON.stringify(response), {
|
|
228
|
+
status: runStatus === "failed" ? 502 : 200,
|
|
229
|
+
headers: { "Content-Type": "application/json" },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
/** Shell-handler voor /api/public/hooks/floriday-stock-reconcile. Schakelaar: voorraad_hook_enabled.stock_reconcile. */
|
|
233
|
+
export async function handleFloridayStockReconcile(request) {
|
|
234
|
+
return runGuardedHook(request, "stock_reconcile", handle, "floriday-stock-reconcile");
|
|
235
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/server-hooks/guard.ts"],"names":[],"mappings":"AAuCA,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUhE;AAED,wBAAsB,cAAc,CAClC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAC/C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,QAAQ,CAAC,CAUnB"}
|