@flowselections/floriday-authenticatie-module 1.0.13 → 1.0.14

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 (48) hide show
  1. package/dist-lib/components/floriday/FlorydaySyncHealthBanner.d.ts +20 -0
  2. package/dist-lib/components/floriday/FlorydaySyncHealthBanner.d.ts.map +1 -0
  3. package/dist-lib/components/floriday/FlorydaySyncHealthBanner.js +35 -0
  4. package/dist-lib/components/settings/FlorydaySettingsCard.d.ts.map +1 -1
  5. package/dist-lib/components/settings/FlorydaySettingsCard.js +75 -53
  6. package/dist-lib/integrations/supabase/auth-middleware.d.ts +6121 -2360
  7. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  8. package/dist-lib/integrations/supabase/client.d.ts +6121 -2360
  9. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  10. package/dist-lib/integrations/supabase/client.server.d.ts +6121 -2360
  11. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  12. package/dist-lib/integrations/supabase/types.d.ts +6259 -2382
  13. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  14. package/dist-lib/integrations/supabase/types.js +10 -1
  15. package/dist-lib/lib/floricode-product-features.d.ts +50 -0
  16. package/dist-lib/lib/floricode-product-features.d.ts.map +1 -0
  17. package/dist-lib/lib/floricode-product-features.js +314 -0
  18. package/dist-lib/lib/floricode-regulatory-features.d.ts +41 -0
  19. package/dist-lib/lib/floricode-regulatory-features.d.ts.map +1 -0
  20. package/dist-lib/lib/floricode-regulatory-features.js +226 -0
  21. package/dist-lib/lib/floriday-auction-letters.functions.d.ts +9307 -0
  22. package/dist-lib/lib/floriday-auction-letters.functions.d.ts.map +1 -0
  23. package/dist-lib/lib/floriday-auction-letters.functions.js +129 -0
  24. package/dist-lib/lib/floriday-gateway-hmac.d.ts +29 -0
  25. package/dist-lib/lib/floriday-gateway-hmac.d.ts.map +1 -0
  26. package/dist-lib/lib/floriday-gateway-hmac.js +119 -0
  27. package/dist-lib/lib/floriday-labels.functions.d.ts +9312 -0
  28. package/dist-lib/lib/floriday-labels.functions.d.ts.map +1 -0
  29. package/dist-lib/lib/floriday-labels.functions.js +141 -0
  30. package/dist-lib/lib/floriday-public.functions.d.ts +55883 -0
  31. package/dist-lib/lib/floriday-public.functions.d.ts.map +1 -0
  32. package/dist-lib/lib/floriday-public.functions.js +241 -0
  33. package/dist-lib/lib/floriday-scopes.d.ts +20 -0
  34. package/dist-lib/lib/floriday-scopes.d.ts.map +1 -0
  35. package/dist-lib/lib/floriday-scopes.js +89 -0
  36. package/dist-lib/lib/floriday-stickers.functions.d.ts +9304 -0
  37. package/dist-lib/lib/floriday-stickers.functions.d.ts.map +1 -0
  38. package/dist-lib/lib/floriday-stickers.functions.js +128 -0
  39. package/dist-lib/lib/floriday-sync.functions.d.ts +240368 -1916
  40. package/dist-lib/lib/floriday-sync.functions.d.ts.map +1 -1
  41. package/dist-lib/lib/floriday-sync.functions.js +836 -35
  42. package/dist-lib/lib/floriday-writes.functions.d.ts +55812 -0
  43. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -0
  44. package/dist-lib/lib/floriday-writes.functions.js +160 -0
  45. package/dist-lib/lib/floriday.functions.d.ts +125816 -46888
  46. package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
  47. package/dist-lib/lib/floriday.functions.js +451 -58
  48. package/package.json +2 -2
@@ -1,8 +1,17 @@
1
1
  export const Constants = {
2
2
  public: {
3
3
  Enums: {
4
- app_role: ["admin", "manager", "warehouse", "sales", "developer"],
4
+ app_role: [
5
+ "admin",
6
+ "manager",
7
+ "warehouse",
8
+ "sales",
9
+ "developer",
10
+ "receptie",
11
+ "monteur",
12
+ ],
5
13
  exact_env: ["staging", "live"],
14
+ exact_push_status: ["pending", "sent", "error"],
6
15
  quote_status: [
7
16
  "concept",
8
17
  "verzonden",
@@ -0,0 +1,50 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ import type { FlorydayEnvironment } from "./floriday.functions";
3
+ declare const PRODUCT_FEATURE_LIST_NAME = "vbn-product-characteristic";
4
+ export interface ProductCharacteristic {
5
+ code: string;
6
+ label: string | null;
7
+ productCode: string;
8
+ productGroupId: string | null;
9
+ productName: string | null;
10
+ characteristicCode: string;
11
+ characteristicLabel: string | null;
12
+ defaultValueCode: string | null;
13
+ defaultValueLabel: string | null;
14
+ featureTypeId: string;
15
+ featureValueCode: string | null;
16
+ required: boolean;
17
+ regulationTypeId: string | null;
18
+ regulationLabel: string | null;
19
+ presentationOrder: number | null;
20
+ allowedValues: Array<{
21
+ code: string;
22
+ label: string | null;
23
+ }>;
24
+ payload: Record<string, unknown>;
25
+ }
26
+ export declare function getProductCharacteristicsForVbnCode(supabase: SupabaseClient, environment: FlorydayEnvironment, productCodeInput: string | number, options?: {
27
+ forceRefresh?: boolean;
28
+ }): Promise<{
29
+ ok: true;
30
+ productCode: string;
31
+ productGroupId: string | null;
32
+ productName: string | null;
33
+ source: "cache" | "floricode-live";
34
+ codeListName: typeof PRODUCT_FEATURE_LIST_NAME;
35
+ characteristics: ProductCharacteristic[];
36
+ }>;
37
+ export declare function syncFloricodeProductFeatures(supabase: SupabaseClient, options?: {
38
+ batchSize?: number;
39
+ maxBatches?: number;
40
+ }): Promise<{
41
+ ok: true;
42
+ resource: string;
43
+ fetched: number;
44
+ upserted: number;
45
+ batches: number;
46
+ projected: any;
47
+ durationMs: number;
48
+ }>;
49
+ export {};
50
+ //# sourceMappingURL=floricode-product-features.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floricode-product-features.d.ts","sourceRoot":"","sources":["../../src/lib/floricode-product-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAShE,QAAA,MAAM,yBAAyB,+BAA+B,CAAC;AAY/D,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC7D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AA0OD,wBAAsB,mCAAmC,CACvD,QAAQ,EAAE,cAAc,EACxB,WAAW,EAAE,mBAAmB,EAChC,gBAAgB,EAAE,MAAM,GAAG,MAAM,EACjC,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,GACvC,OAAO,CAAC;IACT,EAAE,EAAE,IAAI,CAAC;IACT,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,OAAO,GAAG,gBAAgB,CAAC;IACnC,YAAY,EAAE,OAAO,yBAAyB,CAAC;IAC/C,eAAe,EAAE,qBAAqB,EAAE,CAAC;CAC1C,CAAC,CAuDD;AAED,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,cAAc,EACxB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO;;;;;;;;GAiF1D"}
@@ -0,0 +1,314 @@
1
+ import { fetchFloricodeRegulatoryFeaturesForProduct, upsertRegulatoryFeatureRecords, } from "./floricode-regulatory-features";
2
+ const FLORICODE_TOKEN_URL = "https://api.floricode.com/oauth/token";
3
+ const FLORICODE_PRODUCT_FEATURE_URL = "https://api.floricode.com/v2/VBN/ProductFeature";
4
+ const PRODUCT_FEATURE_SCOPE = "vbn_productfeature:read";
5
+ const PRODUCT_FEATURE_LIST_NAME = "vbn-product-characteristic";
6
+ const PRODUCT_FEATURE_RESOURCE = "ProductFeature";
7
+ function normalizeProductCode(input) {
8
+ const value = String(input).trim();
9
+ if (!/^\d+$/.test(value)) {
10
+ throw new Error("productCode moet een numerieke VBN-code zijn");
11
+ }
12
+ return value;
13
+ }
14
+ function chunk(items, size) {
15
+ const out = [];
16
+ for (let i = 0; i < items.length; i += size)
17
+ out.push(items.slice(i, i + size));
18
+ return out;
19
+ }
20
+ async function getFloricodeAccessToken() {
21
+ const clientId = process.env.FLORICODE_CLIENT_ID;
22
+ const clientSecret = process.env.FLORICODE_CLIENT_SECRET;
23
+ if (!clientId || !clientSecret) {
24
+ throw new Error("Floricode credentials ontbreken (FLORICODE_CLIENT_ID/FLORICODE_CLIENT_SECRET)");
25
+ }
26
+ const body = new URLSearchParams({
27
+ grant_type: "client_credentials",
28
+ client_id: clientId,
29
+ client_secret: clientSecret,
30
+ scope: PRODUCT_FEATURE_SCOPE,
31
+ });
32
+ const res = await fetch(FLORICODE_TOKEN_URL, {
33
+ method: "POST",
34
+ headers: { "content-type": "application/x-www-form-urlencoded" },
35
+ body,
36
+ });
37
+ const json = (await res.json().catch(() => ({})));
38
+ if (!res.ok) {
39
+ throw new Error(`Floricode token-aanvraag mislukt (${res.status}): ${json?.error_description ?? json?.error ?? res.statusText}`);
40
+ }
41
+ if (!json?.access_token)
42
+ throw new Error("Floricode token-response bevat geen access_token");
43
+ return String(json.access_token);
44
+ }
45
+ async function fetchFloricodeProductFeaturesForProduct(productCode) {
46
+ const token = await getFloricodeAccessToken();
47
+ const url = new URL(FLORICODE_PRODUCT_FEATURE_URL);
48
+ url.searchParams.set("$filter", `product_id eq ${productCode}`);
49
+ url.searchParams.set("$orderby", "feature_type_id");
50
+ url.searchParams.set("$top", "250");
51
+ const res = await fetch(url.toString(), {
52
+ headers: { authorization: `Bearer ${token}` },
53
+ });
54
+ const json = (await res.json().catch(() => ({})));
55
+ if (!res.ok) {
56
+ throw new Error(`Floricode ProductFeature mislukt (${res.status}): ${json?.error?.message ?? json?.error ?? res.statusText}`);
57
+ }
58
+ return Array.isArray(json?.value) ? json.value : [];
59
+ }
60
+ async function fetchFloricodeProductFeaturesPage(token, skip, top) {
61
+ const url = new URL(FLORICODE_PRODUCT_FEATURE_URL);
62
+ url.searchParams.set("$top", String(top));
63
+ url.searchParams.set("$skip", String(skip));
64
+ url.searchParams.set("$orderby", "product_id,feature_type_id");
65
+ const res = await fetch(url.toString(), {
66
+ headers: { authorization: `Bearer ${token}` },
67
+ });
68
+ const json = (await res.json().catch(() => ({})));
69
+ if (!res.ok) {
70
+ throw new Error(`Floricode ProductFeature batch mislukt (${res.status}): ${json?.error?.message ?? json?.error ?? res.statusText}`);
71
+ }
72
+ return Array.isArray(json?.value) ? json.value : [];
73
+ }
74
+ async function upsertFloricodeProductFeatureRecords(supabase, items) {
75
+ const rows = items
76
+ .filter((item) => item?.product_id != null && item?.feature_type_id)
77
+ .map((item) => {
78
+ const productCode = String(item.product_id);
79
+ const featureCode = String(item.feature_type_id);
80
+ const valueCode = item.feature_value_code == null ? "" : String(item.feature_value_code);
81
+ return {
82
+ resource: PRODUCT_FEATURE_RESOURCE,
83
+ record_id: `${productCode}:${featureCode}:${valueCode}`,
84
+ data: item,
85
+ change_date_time: item.change_date_time ?? null,
86
+ last_synced_at: new Date().toISOString(),
87
+ };
88
+ });
89
+ for (const part of chunk(rows, 1000)) {
90
+ const { error } = await supabase
91
+ .from("floricode_records")
92
+ .upsert(part, { onConflict: "resource,record_id" });
93
+ if (error)
94
+ throw new Error(`ProductFeature cache-upsert mislukt: ${error.message}`);
95
+ }
96
+ return rows.length;
97
+ }
98
+ async function loadAllowedValuesForFeatures(supabase, environment, featureCodes) {
99
+ const map = new Map();
100
+ const unique = Array.from(new Set(featureCodes.filter(Boolean)));
101
+ if (unique.length === 0)
102
+ return map;
103
+ for (const part of chunk(unique, 100)) {
104
+ const orFilters = part.map((c) => `code.like.${c}:%`).join(",");
105
+ const { data, error } = await supabase
106
+ .from("floriday_code_lists_cache")
107
+ .select("code,label,payload")
108
+ .eq("environment", environment)
109
+ .eq("list_name", "vbn-characteristic-value")
110
+ .or(orFilters)
111
+ .limit(50000);
112
+ if (error)
113
+ throw new Error(`Allowed values lezen mislukt: ${error.message}`);
114
+ for (const row of (data ?? [])) {
115
+ const payload = (row.payload ?? {});
116
+ const featureCode = String(payload?.characteristic_code ?? String(row.code).split(":")[0]);
117
+ const valueCode = String(payload?.value_code ?? String(row.code).split(":")[1] ?? "");
118
+ if (!featureCode || !valueCode)
119
+ continue;
120
+ const arr = map.get(featureCode) ?? [];
121
+ arr.push({ code: valueCode, label: row.label ?? null });
122
+ map.set(featureCode, arr);
123
+ }
124
+ }
125
+ return map;
126
+ }
127
+ async function loadProductMeta(supabase, environment, productCode) {
128
+ const { data } = await supabase
129
+ .from("floriday_code_lists_cache")
130
+ .select("label,payload")
131
+ .eq("environment", environment)
132
+ .eq("list_name", "vbn-product-code")
133
+ .eq("code", productCode)
134
+ .maybeSingle();
135
+ const row = data;
136
+ return {
137
+ productName: row?.label ?? null,
138
+ productGroupId: row?.payload?.product_group_id == null ? null : String(row.payload.product_group_id),
139
+ };
140
+ }
141
+ async function readCachedProductCharacteristics(supabase, environment, productCode) {
142
+ const { data, error } = await supabase
143
+ .from("floriday_code_lists_cache")
144
+ .select("code,label,payload")
145
+ .eq("environment", environment)
146
+ .eq("list_name", PRODUCT_FEATURE_LIST_NAME)
147
+ .like("code", `${productCode}:%`)
148
+ .limit(5000);
149
+ if (error)
150
+ throw new Error(`ProductFeature cache lezen mislukt: ${error.message}`);
151
+ const rows = (data ?? []).map((row) => {
152
+ const payload = (row.payload ?? {});
153
+ const featureCode = String(payload.characteristic_code ?? payload.feature_type_id ?? String(row.code).split(":")[1] ?? "");
154
+ const valueCode = payload.default_value_code == null ? null : String(payload.default_value_code);
155
+ const presentationOrder = payload.presentation_order == null || payload.presentation_order === ""
156
+ ? null
157
+ : Number(payload.presentation_order);
158
+ return {
159
+ code: String(row.code),
160
+ label: row.label ?? null,
161
+ productCode,
162
+ productGroupId: payload.product_group_id == null ? null : String(payload.product_group_id),
163
+ productName: payload.product_name == null ? null : String(payload.product_name),
164
+ characteristicCode: featureCode,
165
+ characteristicLabel: payload.characteristic_label ?? row.label ?? null,
166
+ defaultValueCode: valueCode,
167
+ defaultValueLabel: payload.default_value_label ?? payload.value_label ?? null,
168
+ featureTypeId: featureCode,
169
+ featureValueCode: payload.feature_value_code == null ? valueCode : String(payload.feature_value_code),
170
+ required: payload.required !== false,
171
+ regulationTypeId: payload.regulation_type_id == null ? null : String(payload.regulation_type_id),
172
+ regulationLabel: payload.regulation_label == null ? null : String(payload.regulation_label),
173
+ presentationOrder: Number.isFinite(presentationOrder)
174
+ ? presentationOrder
175
+ : null,
176
+ allowedValues: [],
177
+ payload,
178
+ };
179
+ });
180
+ // Allowed values per feature laden en sorteren op presentation_order.
181
+ const allowed = await loadAllowedValuesForFeatures(supabase, environment, rows.map((r) => r.characteristicCode));
182
+ for (const row of rows) {
183
+ row.allowedValues = allowed.get(row.characteristicCode) ?? [];
184
+ }
185
+ rows.sort((a, b) => {
186
+ const ao = a.presentationOrder ?? 9999;
187
+ const bo = b.presentationOrder ?? 9999;
188
+ if (ao !== bo)
189
+ return ao - bo;
190
+ return a.characteristicCode.localeCompare(b.characteristicCode);
191
+ });
192
+ return rows;
193
+ }
194
+ export async function getProductCharacteristicsForVbnCode(supabase, environment, productCodeInput, options = {}) {
195
+ const productCode = normalizeProductCode(productCodeInput);
196
+ if (!options.forceRefresh) {
197
+ const cached = await readCachedProductCharacteristics(supabase, environment, productCode);
198
+ if (cached.length > 0) {
199
+ return {
200
+ ok: true,
201
+ productCode,
202
+ productGroupId: cached[0]?.productGroupId ?? null,
203
+ productName: cached[0]?.productName ?? null,
204
+ source: "cache",
205
+ codeListName: PRODUCT_FEATURE_LIST_NAME,
206
+ characteristics: cached,
207
+ };
208
+ }
209
+ }
210
+ // Live-fetch: RegulatoryFeatureType (verplichte/optionele kenmerken) +
211
+ // ProductFeature (default-waardes) parallel, upsert in floricode_records
212
+ // en daarna RPC draaien zodat de derived code-list weer up-to-date is.
213
+ const [regItems, pfItems] = await Promise.all([
214
+ fetchFloricodeRegulatoryFeaturesForProduct(productCode).catch((e) => {
215
+ console.warn("[product-characteristics] RegulatoryFeatureType fetch failed:", String(e?.message ?? e));
216
+ return [];
217
+ }),
218
+ fetchFloricodeProductFeaturesForProduct(productCode).catch((e) => {
219
+ console.warn("[product-characteristics] ProductFeature fetch failed:", String(e?.message ?? e));
220
+ return [];
221
+ }),
222
+ ]);
223
+ if (regItems.length > 0)
224
+ await upsertRegulatoryFeatureRecords(supabase, regItems);
225
+ if (pfItems.length > 0)
226
+ await upsertFloricodeProductFeatureRecords(supabase, pfItems);
227
+ if (regItems.length > 0 || pfItems.length > 0) {
228
+ const { error: projError } = await supabase.rpc("refresh_floricode_derived_code_lists");
229
+ if (projError) {
230
+ console.warn("[product-characteristics] projection failed:", projError.message);
231
+ }
232
+ }
233
+ const fresh = await readCachedProductCharacteristics(supabase, environment, productCode);
234
+ const meta = await loadProductMeta(supabase, environment, productCode);
235
+ return {
236
+ ok: true,
237
+ productCode,
238
+ productGroupId: fresh[0]?.productGroupId ?? meta.productGroupId,
239
+ productName: fresh[0]?.productName ?? meta.productName,
240
+ source: "floricode-live",
241
+ codeListName: PRODUCT_FEATURE_LIST_NAME,
242
+ characteristics: fresh,
243
+ };
244
+ }
245
+ export async function syncFloricodeProductFeatures(supabase, options = {}) {
246
+ const startedAt = Date.now();
247
+ const batchSize = Math.min(Math.max(options.batchSize ?? 5000, 1), 5000);
248
+ const maxBatches = options.maxBatches && options.maxBatches > 0 ? options.maxBatches : Infinity;
249
+ let fetched = 0;
250
+ let upserted = 0;
251
+ let skip = 0;
252
+ let batches = 0;
253
+ let maxChangeDateTime = null;
254
+ await supabase.from("floricode_sync_state").upsert({
255
+ resource: PRODUCT_FEATURE_RESOURCE,
256
+ status: "running",
257
+ error_message: null,
258
+ last_sync_at: new Date().toISOString(),
259
+ updated_at: new Date().toISOString(),
260
+ }, { onConflict: "resource" });
261
+ try {
262
+ const token = await getFloricodeAccessToken();
263
+ while (batches < maxBatches) {
264
+ const items = await fetchFloricodeProductFeaturesPage(token, skip, batchSize);
265
+ if (items.length === 0)
266
+ break;
267
+ fetched += items.length;
268
+ upserted += await upsertFloricodeProductFeatureRecords(supabase, items);
269
+ for (const item of items) {
270
+ const changed = item.change_date_time ?? null;
271
+ if (changed && (!maxChangeDateTime || changed > maxChangeDateTime))
272
+ maxChangeDateTime = changed;
273
+ }
274
+ batches += 1;
275
+ skip += items.length;
276
+ if (items.length < batchSize)
277
+ break;
278
+ }
279
+ const { data: projection, error: projectionError } = await supabase.rpc("refresh_floricode_derived_code_lists");
280
+ if (projectionError)
281
+ throw new Error(`Projectie naar code-lists mislukt: ${projectionError.message}`);
282
+ await supabase.from("floricode_sync_state").upsert({
283
+ resource: PRODUCT_FEATURE_RESOURCE,
284
+ status: "success",
285
+ error_message: null,
286
+ last_sequence_number: fetched,
287
+ last_change_date_time: maxChangeDateTime,
288
+ last_sync_at: new Date().toISOString(),
289
+ updated_at: new Date().toISOString(),
290
+ }, { onConflict: "resource" });
291
+ return {
292
+ ok: true,
293
+ resource: PRODUCT_FEATURE_RESOURCE,
294
+ fetched,
295
+ upserted,
296
+ batches,
297
+ projected: projection ?? [],
298
+ durationMs: Date.now() - startedAt,
299
+ };
300
+ }
301
+ catch (e) {
302
+ const msg = String(e?.message ?? e);
303
+ await supabase.from("floricode_sync_state").upsert({
304
+ resource: PRODUCT_FEATURE_RESOURCE,
305
+ status: "error",
306
+ error_message: msg,
307
+ last_sequence_number: fetched,
308
+ last_change_date_time: maxChangeDateTime,
309
+ last_sync_at: new Date().toISOString(),
310
+ updated_at: new Date().toISOString(),
311
+ }, { onConflict: "resource" });
312
+ throw e;
313
+ }
314
+ }
@@ -0,0 +1,41 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ interface RegFeatureType {
3
+ expiry_date?: string | null;
4
+ entry_date?: string | null;
5
+ product_id: number | string;
6
+ feature_type_id: string;
7
+ regulation_type_id: number | string;
8
+ presentation_order?: number | string | null;
9
+ change_date_time?: string | null;
10
+ }
11
+ interface RegulationType {
12
+ expiry_date?: string | null;
13
+ entry_date?: string | null;
14
+ id: number | string;
15
+ description?: string | null;
16
+ change_date_time?: string | null;
17
+ }
18
+ export declare function fetchFloricodeRegulatoryFeaturesForProduct(productCode: string): Promise<RegFeatureType[]>;
19
+ export declare function upsertRegulatoryFeatureRecords(supabase: SupabaseClient, items: RegFeatureType[]): Promise<number>;
20
+ export declare function upsertRegulationTypeRecords(supabase: SupabaseClient, items: RegulationType[]): Promise<number>;
21
+ export declare function syncFloricodeRegulationTypes(supabase: SupabaseClient): Promise<{
22
+ ok: true;
23
+ resource: string;
24
+ fetched: number;
25
+ upserted: number;
26
+ durationMs: number;
27
+ }>;
28
+ export declare function syncFloricodeRegulatoryFeatures(supabase: SupabaseClient, options?: {
29
+ batchSize?: number;
30
+ maxBatches?: number;
31
+ }): Promise<{
32
+ ok: true;
33
+ resource: string;
34
+ fetched: number;
35
+ upserted: number;
36
+ batches: number;
37
+ projected: any;
38
+ durationMs: number;
39
+ }>;
40
+ export {};
41
+ //# sourceMappingURL=floricode-regulatory-features.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floricode-regulatory-features.d.ts","sourceRoot":"","sources":["../../src/lib/floricode-regulatory-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAY5D,UAAU,cAAc;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAAC;IACpC,kBAAkB,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,UAAU,cAAc;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAoCD,wBAAsB,0CAA0C,CAC9D,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,cAAc,EAAE,CAAC,CAgB3B;AAkCD,wBAAsB,8BAA8B,CAClD,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,cAAc,EAAE,GACtB,OAAO,CAAC,MAAM,CAAC,CAqBjB;AAED,wBAAsB,2BAA2B,CAC/C,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,cAAc,EAAE,GACtB,OAAO,CAAC,MAAM,CAAC,CAqBjB;AAiBD,wBAAsB,4BAA4B,CAAC,QAAQ,EAAE,cAAc;;;;;;GA2B1E;AAED,wBAAsB,+BAA+B,CACnD,QAAQ,EAAE,cAAc,EACxB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO;;;;;;;;GAyE1D"}
@@ -0,0 +1,226 @@
1
+ const FLORICODE_TOKEN_URL = "https://api.floricode.com/oauth/token";
2
+ const REG_FEATURE_TYPE_URL = "https://api.floricode.com/v2/VBN/RegulatoryFeatureType";
3
+ const REGULATION_TYPE_URL = "https://api.floricode.com/v2/VBN/RegulationType";
4
+ const REG_FEATURE_TYPE_RESOURCE = "RegulatoryFeatureType";
5
+ const REGULATION_TYPE_RESOURCE = "RegulationType";
6
+ const REG_FEATURE_TYPE_SCOPE = "vbn_regulatoryfeaturetype:read";
7
+ const REGULATION_TYPE_SCOPE = "vbn_regulationtype:read";
8
+ function chunk(items, size) {
9
+ const out = [];
10
+ for (let i = 0; i < items.length; i += size)
11
+ out.push(items.slice(i, i + size));
12
+ return out;
13
+ }
14
+ async function getFloricodeToken(scope) {
15
+ const clientId = process.env.FLORICODE_CLIENT_ID;
16
+ const clientSecret = process.env.FLORICODE_CLIENT_SECRET;
17
+ if (!clientId || !clientSecret) {
18
+ throw new Error("Floricode credentials ontbreken (FLORICODE_CLIENT_ID/FLORICODE_CLIENT_SECRET)");
19
+ }
20
+ const body = new URLSearchParams({
21
+ grant_type: "client_credentials",
22
+ client_id: clientId,
23
+ client_secret: clientSecret,
24
+ scope,
25
+ });
26
+ const res = await fetch(FLORICODE_TOKEN_URL, {
27
+ method: "POST",
28
+ headers: { "content-type": "application/x-www-form-urlencoded" },
29
+ body,
30
+ });
31
+ const json = (await res.json().catch(() => ({})));
32
+ if (!res.ok || !json?.access_token) {
33
+ throw new Error(`Floricode token (${scope}) mislukt (${res.status}): ${json?.error_description ?? json?.error ?? res.statusText}`);
34
+ }
35
+ return String(json.access_token);
36
+ }
37
+ export async function fetchFloricodeRegulatoryFeaturesForProduct(productCode) {
38
+ const token = await getFloricodeToken(REG_FEATURE_TYPE_SCOPE);
39
+ const url = new URL(REG_FEATURE_TYPE_URL);
40
+ url.searchParams.set("$filter", `product_id eq ${productCode}`);
41
+ url.searchParams.set("$orderby", "presentation_order");
42
+ url.searchParams.set("$top", "500");
43
+ const res = await fetch(url.toString(), { headers: { authorization: `Bearer ${token}` } });
44
+ const json = (await res.json().catch(() => ({})));
45
+ if (!res.ok) {
46
+ throw new Error(`Floricode RegulatoryFeatureType mislukt (${res.status}): ${json?.error?.message ?? json?.error ?? res.statusText}`);
47
+ }
48
+ return Array.isArray(json?.value) ? json.value : [];
49
+ }
50
+ async function fetchRegFeatureTypePage(token, skip, top) {
51
+ const url = new URL(REG_FEATURE_TYPE_URL);
52
+ url.searchParams.set("$top", String(top));
53
+ url.searchParams.set("$skip", String(skip));
54
+ url.searchParams.set("$orderby", "product_id,feature_type_id");
55
+ const res = await fetch(url.toString(), { headers: { authorization: `Bearer ${token}` } });
56
+ const json = (await res.json().catch(() => ({})));
57
+ if (!res.ok) {
58
+ throw new Error(`Floricode RegulatoryFeatureType batch mislukt (${res.status}): ${json?.error?.message ?? json?.error ?? res.statusText}`);
59
+ }
60
+ return Array.isArray(json?.value) ? json.value : [];
61
+ }
62
+ async function fetchRegulationTypeAll(token) {
63
+ const url = new URL(REGULATION_TYPE_URL);
64
+ url.searchParams.set("$top", "500");
65
+ const res = await fetch(url.toString(), { headers: { authorization: `Bearer ${token}` } });
66
+ const json = (await res.json().catch(() => ({})));
67
+ if (!res.ok) {
68
+ throw new Error(`Floricode RegulationType mislukt (${res.status}): ${json?.error?.message ?? json?.error ?? res.statusText}`);
69
+ }
70
+ return Array.isArray(json?.value) ? json.value : [];
71
+ }
72
+ export async function upsertRegulatoryFeatureRecords(supabase, items) {
73
+ const dedup = new Map();
74
+ for (const i of items) {
75
+ if (i?.product_id == null || !i?.feature_type_id || i?.regulation_type_id == null)
76
+ continue;
77
+ const record_id = `${i.product_id}:${i.feature_type_id}:${i.regulation_type_id}`;
78
+ dedup.set(record_id, {
79
+ resource: REG_FEATURE_TYPE_RESOURCE,
80
+ record_id,
81
+ data: i,
82
+ change_date_time: i.change_date_time ?? null,
83
+ last_synced_at: new Date().toISOString(),
84
+ });
85
+ }
86
+ const rows = Array.from(dedup.values());
87
+ for (const part of chunk(rows, 1000)) {
88
+ const { error } = await supabase
89
+ .from("floricode_records")
90
+ .upsert(part, { onConflict: "resource,record_id" });
91
+ if (error)
92
+ throw new Error(`RegulatoryFeatureType cache-upsert mislukt: ${error.message}`);
93
+ }
94
+ return rows.length;
95
+ }
96
+ export async function upsertRegulationTypeRecords(supabase, items) {
97
+ const dedup = new Map();
98
+ for (const i of items) {
99
+ if (i?.id == null)
100
+ continue;
101
+ const record_id = String(i.id);
102
+ dedup.set(record_id, {
103
+ resource: REGULATION_TYPE_RESOURCE,
104
+ record_id,
105
+ data: i,
106
+ change_date_time: i.change_date_time ?? null,
107
+ last_synced_at: new Date().toISOString(),
108
+ });
109
+ }
110
+ const rows = Array.from(dedup.values());
111
+ for (const part of chunk(rows, 500)) {
112
+ const { error } = await supabase
113
+ .from("floricode_records")
114
+ .upsert(part, { onConflict: "resource,record_id" });
115
+ if (error)
116
+ throw new Error(`RegulationType cache-upsert mislukt: ${error.message}`);
117
+ }
118
+ return rows.length;
119
+ }
120
+ async function setSyncState(supabase, resource, patch) {
121
+ await supabase.from("floricode_sync_state").upsert({
122
+ resource,
123
+ updated_at: new Date().toISOString(),
124
+ ...patch,
125
+ }, { onConflict: "resource" });
126
+ }
127
+ export async function syncFloricodeRegulationTypes(supabase) {
128
+ const startedAt = Date.now();
129
+ await setSyncState(supabase, REGULATION_TYPE_RESOURCE, {
130
+ status: "running",
131
+ error_message: null,
132
+ last_sync_at: new Date().toISOString(),
133
+ });
134
+ try {
135
+ const token = await getFloricodeToken(REGULATION_TYPE_SCOPE);
136
+ const items = await fetchRegulationTypeAll(token);
137
+ const upserted = await upsertRegulationTypeRecords(supabase, items);
138
+ await setSyncState(supabase, REGULATION_TYPE_RESOURCE, {
139
+ status: "success",
140
+ error_message: null,
141
+ last_sequence_number: upserted,
142
+ last_sync_at: new Date().toISOString(),
143
+ });
144
+ return { ok: true, resource: REGULATION_TYPE_RESOURCE, fetched: items.length, upserted, durationMs: Date.now() - startedAt };
145
+ }
146
+ catch (e) {
147
+ const msg = String(e?.message ?? e);
148
+ await setSyncState(supabase, REGULATION_TYPE_RESOURCE, {
149
+ status: "error",
150
+ error_message: msg,
151
+ last_sync_at: new Date().toISOString(),
152
+ });
153
+ throw e;
154
+ }
155
+ }
156
+ export async function syncFloricodeRegulatoryFeatures(supabase, options = {}) {
157
+ const startedAt = Date.now();
158
+ const batchSize = Math.min(Math.max(options.batchSize ?? 5000, 1), 5000);
159
+ const maxBatches = options.maxBatches && options.maxBatches > 0 ? options.maxBatches : Infinity;
160
+ let fetched = 0;
161
+ let upserted = 0;
162
+ let skip = 0;
163
+ let batches = 0;
164
+ let maxChangeDateTime = null;
165
+ await setSyncState(supabase, REG_FEATURE_TYPE_RESOURCE, {
166
+ status: "running",
167
+ error_message: null,
168
+ last_sync_at: new Date().toISOString(),
169
+ });
170
+ try {
171
+ const token = await getFloricodeToken(REG_FEATURE_TYPE_SCOPE);
172
+ while (batches < maxBatches) {
173
+ const items = await fetchRegFeatureTypePage(token, skip, batchSize);
174
+ if (items.length === 0)
175
+ break;
176
+ fetched += items.length;
177
+ upserted += await upsertRegulatoryFeatureRecords(supabase, items);
178
+ for (const item of items) {
179
+ const changed = item.change_date_time ?? null;
180
+ if (changed && (!maxChangeDateTime || changed > maxChangeDateTime))
181
+ maxChangeDateTime = changed;
182
+ }
183
+ batches += 1;
184
+ skip += items.length;
185
+ if (items.length < batchSize)
186
+ break;
187
+ }
188
+ // RegulationType is een kleine lijst — altijd meeladen voor labels.
189
+ try {
190
+ await syncFloricodeRegulationTypes(supabase);
191
+ }
192
+ catch (e) {
193
+ console.warn("[regulatory] RegulationType sync skipped:", String(e?.message ?? e));
194
+ }
195
+ const { data: projection, error: projectionError } = await supabase.rpc("refresh_floricode_derived_code_lists");
196
+ if (projectionError)
197
+ throw new Error(`Projectie naar code-lists mislukt: ${projectionError.message}`);
198
+ await setSyncState(supabase, REG_FEATURE_TYPE_RESOURCE, {
199
+ status: "success",
200
+ error_message: null,
201
+ last_sequence_number: fetched,
202
+ last_change_date_time: maxChangeDateTime,
203
+ last_sync_at: new Date().toISOString(),
204
+ });
205
+ return {
206
+ ok: true,
207
+ resource: REG_FEATURE_TYPE_RESOURCE,
208
+ fetched,
209
+ upserted,
210
+ batches,
211
+ projected: projection ?? [],
212
+ durationMs: Date.now() - startedAt,
213
+ };
214
+ }
215
+ catch (e) {
216
+ const msg = String(e?.message ?? e);
217
+ await setSyncState(supabase, REG_FEATURE_TYPE_RESOURCE, {
218
+ status: "error",
219
+ error_message: msg,
220
+ last_sequence_number: fetched,
221
+ last_change_date_time: maxChangeDateTime,
222
+ last_sync_at: new Date().toISOString(),
223
+ });
224
+ throw e;
225
+ }
226
+ }