@flowselections/floriday-voorraad 1.0.66 → 1.0.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/dist-lib/hooks-entry.d.ts +11 -0
  2. package/dist-lib/hooks-entry.d.ts.map +1 -0
  3. package/dist-lib/hooks-entry.js +10 -0
  4. package/dist-lib/integrations/supabase/types.d.ts +122 -0
  5. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  6. package/dist-lib/lib/inventory-settings.functions.d.ts +1 -1
  7. package/dist-lib/server-hooks/floriday-base-supply.d.ts +3 -0
  8. package/dist-lib/server-hooks/floriday-base-supply.d.ts.map +1 -0
  9. package/dist-lib/server-hooks/floriday-base-supply.js +73 -0
  10. package/dist-lib/server-hooks/floriday-continuous-stock-pull.d.ts +3 -0
  11. package/dist-lib/server-hooks/floriday-continuous-stock-pull.d.ts.map +1 -0
  12. package/dist-lib/server-hooks/floriday-continuous-stock-pull.js +168 -0
  13. package/dist-lib/server-hooks/floriday-continuous-stock.d.ts +3 -0
  14. package/dist-lib/server-hooks/floriday-continuous-stock.d.ts.map +1 -0
  15. package/dist-lib/server-hooks/floriday-continuous-stock.js +111 -0
  16. package/dist-lib/server-hooks/floriday-stock-reconcile.d.ts +3 -0
  17. package/dist-lib/server-hooks/floriday-stock-reconcile.d.ts.map +1 -0
  18. package/dist-lib/server-hooks/floriday-stock-reconcile.js +235 -0
  19. package/dist-lib/server-hooks/guard.d.ts +3 -0
  20. package/dist-lib/server-hooks/guard.d.ts.map +1 -0
  21. package/dist-lib/server-hooks/guard.js +60 -0
  22. package/dist-lib/server-hooks/logistics-tasks-run.d.ts +3 -0
  23. package/dist-lib/server-hooks/logistics-tasks-run.d.ts.map +1 -0
  24. package/dist-lib/server-hooks/logistics-tasks-run.js +37 -0
  25. package/package.json +53 -2
@@ -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?: "FOLLOW_ERP" | "MAXIMUM" | "FIXED_REFILL" | "MANUAL" | "DISABLED" | undefined;
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,3 @@
1
+ /** Shell-handler voor /api/public/hooks/floriday-base-supply. Schakelaar: voorraad_hook_enabled.floriday_base_supply. */
2
+ export declare function handleFloridayBaseSupply(request: Request): Promise<Response>;
3
+ //# sourceMappingURL=floriday-base-supply.d.ts.map
@@ -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,3 @@
1
+ /** Shell-handler voor /api/public/hooks/floriday-continuous-stock. Schakelaar: voorraad_hook_enabled.continuous_stock. */
2
+ export declare function handleFloridayContinuousStock(request: Request): Promise<Response>;
3
+ //# sourceMappingURL=floriday-continuous-stock.d.ts.map
@@ -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,3 @@
1
+ /** Shell-handler voor /api/public/hooks/floriday-stock-reconcile. Schakelaar: voorraad_hook_enabled.stock_reconcile. */
2
+ export declare function handleFloridayStockReconcile(request: Request): Promise<Response>;
3
+ //# sourceMappingURL=floriday-stock-reconcile.d.ts.map
@@ -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,3 @@
1
+ export declare function hookEnabled(name: string): Promise<boolean>;
2
+ export declare function runGuardedHook(request: Request, name: string, handle: (request: Request) => Promise<Response>, label: string): Promise<Response>;
3
+ //# sourceMappingURL=guard.d.ts.map
@@ -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"}