@flowselections/floriday-klanten-module 1.0.0
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/_core-safelist.d.ts +2 -0
- package/dist-lib/_core-safelist.d.ts.map +1 -0
- package/dist-lib/_core-safelist.js +15 -0
- package/dist-lib/components/CustomerAvatar.d.ts +7 -0
- package/dist-lib/components/CustomerAvatar.d.ts.map +1 -0
- package/dist-lib/components/CustomerAvatar.js +18 -0
- package/dist-lib/components/TemplatePage.d.ts +2 -0
- package/dist-lib/components/TemplatePage.d.ts.map +1 -0
- package/dist-lib/components/TemplatePage.js +4 -0
- package/dist-lib/components/settings/CustomerFieldsCard.d.ts +2 -0
- package/dist-lib/components/settings/CustomerFieldsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/CustomerFieldsCard.js +59 -0
- package/dist-lib/index.d.ts +5 -0
- package/dist-lib/index.d.ts.map +1 -0
- package/dist-lib/index.js +32 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts +2 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/auth-attacher.js +12 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +1560 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/auth-middleware.js +52 -0
- package/dist-lib/integrations/supabase/client.d.ts +1556 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/client.js +28 -0
- package/dist-lib/integrations/supabase/client.server.d.ts +1556 -0
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/client.server.js +50 -0
- package/dist-lib/integrations/supabase/types.d.ts +1656 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/types.js +8 -0
- package/dist-lib/lib/accounting/exact/client.server.d.ts +37 -0
- package/dist-lib/lib/accounting/exact/client.server.d.ts.map +1 -0
- package/dist-lib/lib/accounting/exact/client.server.js +160 -0
- package/dist-lib/lib/accounting/exact/mapping.d.ts +39 -0
- package/dist-lib/lib/accounting/exact/mapping.d.ts.map +1 -0
- package/dist-lib/lib/accounting/exact/mapping.js +45 -0
- package/dist-lib/lib/accounting/exact/sync.server.d.ts +4 -0
- package/dist-lib/lib/accounting/exact/sync.server.d.ts.map +1 -0
- package/dist-lib/lib/accounting/exact/sync.server.js +181 -0
- package/dist-lib/lib/accounting/floriday/sync.server.d.ts +3 -0
- package/dist-lib/lib/accounting/floriday/sync.server.d.ts.map +1 -0
- package/dist-lib/lib/accounting/floriday/sync.server.js +17 -0
- package/dist-lib/lib/accounting/registry.server.d.ts +4 -0
- package/dist-lib/lib/accounting/registry.server.d.ts.map +1 -0
- package/dist-lib/lib/accounting/registry.server.js +9 -0
- package/dist-lib/lib/accounting/types.d.ts +24 -0
- package/dist-lib/lib/accounting/types.d.ts.map +1 -0
- package/dist-lib/lib/accounting/types.js +4 -0
- package/dist-lib/lib/accounting.functions.d.ts +4682 -0
- package/dist-lib/lib/accounting.functions.d.ts.map +1 -0
- package/dist-lib/lib/accounting.functions.js +50 -0
- package/dist-lib/lib/crypto.server.d.ts +10 -0
- package/dist-lib/lib/crypto.server.d.ts.map +1 -0
- package/dist-lib/lib/crypto.server.js +61 -0
- package/dist-lib/lib/customer-schemas.d.ts +53 -0
- package/dist-lib/lib/customer-schemas.d.ts.map +1 -0
- package/dist-lib/lib/customer-schemas.js +31 -0
- package/dist-lib/lib/customers.functions.d.ts +15904 -0
- package/dist-lib/lib/customers.functions.d.ts.map +1 -0
- package/dist-lib/lib/customers.functions.js +251 -0
- package/dist-lib/lib/floriday.server.d.ts +55 -0
- package/dist-lib/lib/floriday.server.d.ts.map +1 -0
- package/dist-lib/lib/floriday.server.js +765 -0
- package/dist-lib/lib/utils.d.ts +3 -0
- package/dist-lib/lib/utils.d.ts.map +1 -0
- package/dist-lib/lib/utils.js +5 -0
- package/dist-lib/lib/validationSchemas.d.ts +15 -0
- package/dist-lib/lib/validationSchemas.d.ts.map +1 -0
- package/dist-lib/lib/validationSchemas.js +25 -0
- package/dist-lib/styles.css +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
// Server-only Floriday API helper. NEVER import from client-side code.
|
|
2
|
+
import { supabaseAdmin } from "@/integrations/supabase/client.server";
|
|
3
|
+
const FLORIDAY_BASE_URLS = {
|
|
4
|
+
staging: "https://api.staging.floriday.io/suppliers-api-2026v1",
|
|
5
|
+
production: "https://api.floriday.io/suppliers-api-2026v1",
|
|
6
|
+
acceptance: "https://api.acceptance.floriday.io/suppliers-api-2026v1",
|
|
7
|
+
};
|
|
8
|
+
const FLORIDAY_TOKEN_URLS = {
|
|
9
|
+
staging: "https://idm.staging.floriday.io/oauth2/ausmw6b47z1BnlHkw0h7/v1/token",
|
|
10
|
+
production: "https://idm.floriday.io/oauth2/aus3testdcf2vyfs70i7/v1/token",
|
|
11
|
+
acceptance: "https://idm.staging.floriday.io/oauth2/ausmw6b47z1BnlHkw0h7/v1/token",
|
|
12
|
+
};
|
|
13
|
+
const FLORIDAY_SCOPE = "role:app organization:read network:read catalog:read supply:read sales-order:read delivery-conditions:read fulfillment:read";
|
|
14
|
+
function baseUrlFor(environment) {
|
|
15
|
+
return FLORIDAY_BASE_URLS[environment] ?? FLORIDAY_BASE_URLS.staging;
|
|
16
|
+
}
|
|
17
|
+
async function refreshAccessToken(conn) {
|
|
18
|
+
const params = new URLSearchParams({
|
|
19
|
+
grant_type: "client_credentials",
|
|
20
|
+
client_id: conn.client_id,
|
|
21
|
+
client_secret: conn.client_secret,
|
|
22
|
+
scope: FLORIDAY_SCOPE,
|
|
23
|
+
});
|
|
24
|
+
const res = await fetch(FLORIDAY_TOKEN_URLS[conn.environment] ?? FLORIDAY_TOKEN_URLS.staging, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
Accept: "application/json",
|
|
28
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
29
|
+
},
|
|
30
|
+
body: params.toString(),
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
throw new Error(`Token refresh failed (${res.status}): ${await res.text()}`);
|
|
34
|
+
}
|
|
35
|
+
const data = (await res.json());
|
|
36
|
+
const expiresAt = new Date(Date.now() + (data.expires_in - 60) * 1000);
|
|
37
|
+
await supabaseAdmin
|
|
38
|
+
.from("floriday_connections")
|
|
39
|
+
.update({
|
|
40
|
+
access_token: data.access_token,
|
|
41
|
+
token_expires_at: expiresAt.toISOString(),
|
|
42
|
+
})
|
|
43
|
+
.eq("id", conn.id);
|
|
44
|
+
return data.access_token;
|
|
45
|
+
}
|
|
46
|
+
async function ensureValidToken(conn) {
|
|
47
|
+
const expiresAt = conn.token_expires_at ? new Date(conn.token_expires_at) : null;
|
|
48
|
+
const validForOneMin = expiresAt && expiresAt.getTime() > Date.now() + 60000;
|
|
49
|
+
if (conn.access_token && validForOneMin) {
|
|
50
|
+
return conn.access_token;
|
|
51
|
+
}
|
|
52
|
+
return refreshAccessToken(conn);
|
|
53
|
+
}
|
|
54
|
+
async function fetchWithRetry(url, init, attempts = 3) {
|
|
55
|
+
let lastErr = null;
|
|
56
|
+
for (let i = 0; i < attempts; i++) {
|
|
57
|
+
try {
|
|
58
|
+
const res = await fetch(url, init);
|
|
59
|
+
if (res.ok || (res.status >= 400 && res.status < 500 && res.status !== 429)) {
|
|
60
|
+
return res;
|
|
61
|
+
}
|
|
62
|
+
lastErr = new Error(`HTTP ${res.status}`);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
lastErr = err;
|
|
66
|
+
}
|
|
67
|
+
await new Promise((r) => setTimeout(r, [1000, 3000, 9000][i] ?? 9000));
|
|
68
|
+
}
|
|
69
|
+
throw lastErr instanceof Error ? lastErr : new Error("Request failed");
|
|
70
|
+
}
|
|
71
|
+
function mapFloridayPartner(raw, connectionId) {
|
|
72
|
+
const orgId = raw?.organizationId ?? raw?.id ?? raw?.tradePartnerId;
|
|
73
|
+
if (!orgId)
|
|
74
|
+
return null;
|
|
75
|
+
const addr = raw?.physicalAddress ??
|
|
76
|
+
raw?.mailingAddress ??
|
|
77
|
+
raw?.address ??
|
|
78
|
+
raw?.addresses?.[0] ??
|
|
79
|
+
{};
|
|
80
|
+
const contact = raw?.contactPerson ?? raw?.contactPersons?.[0] ?? {};
|
|
81
|
+
const contactInfo = raw?.contactInformation ??
|
|
82
|
+
raw?.contactInfo ??
|
|
83
|
+
raw?.contact ??
|
|
84
|
+
{};
|
|
85
|
+
const primaryContact = raw?.primaryContact ??
|
|
86
|
+
raw?.generalContact ??
|
|
87
|
+
raw?.mainContact ??
|
|
88
|
+
{};
|
|
89
|
+
const name = raw?.organizationName ??
|
|
90
|
+
raw?.name ??
|
|
91
|
+
raw?.commercialName ??
|
|
92
|
+
(raw?.companyGln ? `Floriday-organisatie ${raw.companyGln}` : null) ??
|
|
93
|
+
`Floriday-organisatie ${String(orgId).slice(0, 8)}`;
|
|
94
|
+
const toArr = (v) => Array.isArray(v) ? v.map((x) => String(x)).filter(Boolean) : [];
|
|
95
|
+
const pick = (...vals) => {
|
|
96
|
+
for (const v of vals) {
|
|
97
|
+
if (typeof v === "string" && v.trim().length > 0)
|
|
98
|
+
return v.trim();
|
|
99
|
+
if (typeof v === "number")
|
|
100
|
+
return String(v);
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
const customFields = {};
|
|
105
|
+
const rfhRelationId = pick(raw?.rfhRelationId, raw?.rfhRelationNumber, raw?.rfhCompanyAccountId);
|
|
106
|
+
if (rfhRelationId)
|
|
107
|
+
customFields.rfh_relation_id = rfhRelationId;
|
|
108
|
+
if (raw?.plantionRegistrationNumber)
|
|
109
|
+
customFields.plantion_registration_number = raw.plantionRegistrationNumber;
|
|
110
|
+
return {
|
|
111
|
+
organization_id: String(orgId),
|
|
112
|
+
connection_id: connectionId,
|
|
113
|
+
company_name: name,
|
|
114
|
+
vat_number: pick(raw?.vatNumber, raw?.taxNumber, raw?.chamberOfCommerceNumber, raw?.kvkNumber),
|
|
115
|
+
email: pick(raw?.email, raw?.emailAddress, contactInfo?.email, contactInfo?.emailAddress, primaryContact?.email, primaryContact?.emailAddress, contact?.email, contact?.emailAddress),
|
|
116
|
+
phone: pick(raw?.phoneNumber, raw?.phone, raw?.telephoneNumber, contactInfo?.phoneNumber, contactInfo?.phone, primaryContact?.phoneNumber, primaryContact?.phone, contact?.phoneNumber, contact?.phone),
|
|
117
|
+
contact_person: contact?.fullName ??
|
|
118
|
+
([contact?.firstName, contact?.lastName].filter(Boolean).join(" ") ||
|
|
119
|
+
null),
|
|
120
|
+
street: addr?.streetName ?? addr?.street ?? addr?.addressLine ?? null,
|
|
121
|
+
house_number: addr?.houseNumber ?? null,
|
|
122
|
+
postal_code: addr?.postalCode ?? addr?.zipCode ?? null,
|
|
123
|
+
city: addr?.city ?? null,
|
|
124
|
+
country: addr?.countryCode ?? addr?.country ?? null,
|
|
125
|
+
gln: raw?.companyGln ?? raw?.gln ?? null,
|
|
126
|
+
commercial_name: raw?.commercialName ?? null,
|
|
127
|
+
bio: raw?.bio ?? raw?.description ?? null,
|
|
128
|
+
segments: toArr(raw?.segments ?? raw?.buyerSegments),
|
|
129
|
+
trade_forms: toArr(raw?.tradeForms ?? raw?.tradeForm),
|
|
130
|
+
markets: toArr(raw?.markets ?? raw?.salesMarkets),
|
|
131
|
+
product_groups: toArr(raw?.productGroups),
|
|
132
|
+
payment_methods: toArr(raw?.paymentProviders ?? raw?.paymentMethods),
|
|
133
|
+
organization_type: raw?.organizationType ?? null,
|
|
134
|
+
website: pick(raw?.website, raw?.websiteUrl, raw?.url, contactInfo?.website, contactInfo?.websiteUrl),
|
|
135
|
+
logo_url: raw?.logoUrl ?? null,
|
|
136
|
+
custom_fields: customFields,
|
|
137
|
+
raw,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Realtime sync — alleen handelsrelaties (trade-partners) van elke actieve
|
|
142
|
+
* connectie. Dit is wat de "Synchroniseer Floriday"-knop aanroept en wat de
|
|
143
|
+
* uurlijkse token-cron triggert. Het volledige Floriday-netwerk wordt apart
|
|
144
|
+
* 's nachts gesynchroniseerd via syncFloridayNetwork().
|
|
145
|
+
*/
|
|
146
|
+
export async function syncAllFloridayCustomers() {
|
|
147
|
+
const { data: settings } = await supabaseAdmin
|
|
148
|
+
.from("floriday_settings")
|
|
149
|
+
.select("active_environment")
|
|
150
|
+
.eq("id", 1)
|
|
151
|
+
.maybeSingle();
|
|
152
|
+
const activeEnv = settings?.active_environment ?? "staging";
|
|
153
|
+
const { data: connections, error } = await supabaseAdmin
|
|
154
|
+
.from("floriday_connections")
|
|
155
|
+
.select("*")
|
|
156
|
+
.eq("is_active", true)
|
|
157
|
+
.eq("environment", activeEnv);
|
|
158
|
+
if (error) {
|
|
159
|
+
const hint = error.message?.toLowerCase().includes("invalid api key")
|
|
160
|
+
? ' (Het secret SB_SERVICE_ROLE_KEY lijkt ongeldig — controleer of de service_role key uit Supabase → Project Settings → API is ingeplakt.)'
|
|
161
|
+
: "";
|
|
162
|
+
throw new Error(`Supabase-fout bij ophalen Floriday-verbindingen: ${error.message}${hint}`);
|
|
163
|
+
}
|
|
164
|
+
const errors = [];
|
|
165
|
+
let totalSynced = 0;
|
|
166
|
+
for (const conn of connections ?? []) {
|
|
167
|
+
try {
|
|
168
|
+
const token = await ensureValidToken(conn);
|
|
169
|
+
const base = baseUrlFor(conn.environment);
|
|
170
|
+
// Reset connection_status voor deze connectie. De upsert hieronder
|
|
171
|
+
// zet alleen de daadwerkelijke trade-partners weer op 'connected',
|
|
172
|
+
// zodat klanten die uit Floriday zijn verwijderd automatisch uit
|
|
173
|
+
// de klantenlijst verdwijnen (history blijft behouden).
|
|
174
|
+
await supabaseAdmin
|
|
175
|
+
.from("customers")
|
|
176
|
+
.update({ connection_status: "none" })
|
|
177
|
+
.eq("floriday_connection_id", conn.id)
|
|
178
|
+
.eq("source", "floriday");
|
|
179
|
+
// In de Suppliers API (2026v1) bestaan géén /customers of
|
|
180
|
+
// /trade-partners endpoints. Handelsrelaties worden uitgedrukt als
|
|
181
|
+
// Connections: GET /connections/sync/{seq} geeft alle actieve
|
|
182
|
+
// connecties van deze supplier-organisatie, waarbij elke connectie
|
|
183
|
+
// verwijst naar een customerOrganizationId. Voor de volledige
|
|
184
|
+
// klantgegevens halen we vervolgens GET /organizations/{id} op.
|
|
185
|
+
const headers = {
|
|
186
|
+
Authorization: `Bearer ${token}`,
|
|
187
|
+
"X-Api-Key": conn.api_key,
|
|
188
|
+
Accept: "application/json",
|
|
189
|
+
};
|
|
190
|
+
// 1) Haal alle connecties op (sequence-paged, 1000 per pagina).
|
|
191
|
+
const allConnections = [];
|
|
192
|
+
let seq = 0;
|
|
193
|
+
// veiligheidslimiet — voorkomt oneindige loop bij API-bugs.
|
|
194
|
+
for (let page = 0; page < 50; page++) {
|
|
195
|
+
const url = `${base}/connections/sync/${seq}?limitResult=1000`;
|
|
196
|
+
const res = await fetchWithRetry(url, { headers });
|
|
197
|
+
if (!res.ok) {
|
|
198
|
+
const body = await res.text().catch(() => "");
|
|
199
|
+
throw new Error(`GET /connections/sync/${seq} → HTTP ${res.status}${body ? `: ${body.slice(0, 300)}` : ""}`);
|
|
200
|
+
}
|
|
201
|
+
const json = await res.json();
|
|
202
|
+
const items = Array.isArray(json)
|
|
203
|
+
? json
|
|
204
|
+
: Array.isArray(json?.results)
|
|
205
|
+
? json.results
|
|
206
|
+
: Array.isArray(json?.items)
|
|
207
|
+
? json.items
|
|
208
|
+
: [];
|
|
209
|
+
if (items.length === 0)
|
|
210
|
+
break;
|
|
211
|
+
allConnections.push(...items);
|
|
212
|
+
const lastSeq = items
|
|
213
|
+
.map((c) => Number(c?.sequenceNumber ?? c?.sequence ?? 0))
|
|
214
|
+
.filter((n) => Number.isFinite(n) && n > 0)
|
|
215
|
+
.reduce((a, b) => Math.max(a, b), 0);
|
|
216
|
+
if (!lastSeq || lastSeq <= seq)
|
|
217
|
+
break;
|
|
218
|
+
seq = lastSeq;
|
|
219
|
+
}
|
|
220
|
+
// 2) Filter op actieve connecties + unieke customer organization IDs.
|
|
221
|
+
const customerOrgIds = Array.from(new Set(allConnections
|
|
222
|
+
.filter((c) => {
|
|
223
|
+
// Het Suppliers-API 2026v1 /connections/sync endpoint geeft
|
|
224
|
+
// alleen { customerOrganizationId, lastModifiedDateTime,
|
|
225
|
+
// isDeleted, sequenceNumber } terug — geen status-veld.
|
|
226
|
+
// Een connectie is actief zolang isDeleted=false.
|
|
227
|
+
if (c?.isDeleted === true)
|
|
228
|
+
return false;
|
|
229
|
+
// endDate is optioneel; als die in het verleden ligt is de
|
|
230
|
+
// connectie beëindigd.
|
|
231
|
+
const endDate = c?.endDate ? new Date(c.endDate) : null;
|
|
232
|
+
if (endDate && Number.isFinite(endDate.getTime()) && endDate.getTime() < Date.now()) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
})
|
|
237
|
+
.map((c) => c?.customerOrganizationId ??
|
|
238
|
+
c?.organizationId ??
|
|
239
|
+
c?.partnerOrganizationId ??
|
|
240
|
+
null)
|
|
241
|
+
.filter((id) => typeof id === "string" && id.length > 0)));
|
|
242
|
+
// 3) Haal per organisatie de details op (parallel, throttled).
|
|
243
|
+
const partners = [];
|
|
244
|
+
const concurrency = 5;
|
|
245
|
+
let cursor = 0;
|
|
246
|
+
async function worker() {
|
|
247
|
+
while (cursor < customerOrgIds.length) {
|
|
248
|
+
const idx = cursor++;
|
|
249
|
+
const orgId = customerOrgIds[idx];
|
|
250
|
+
try {
|
|
251
|
+
const r = await fetch(`${base}/organizations/${encodeURIComponent(orgId)}`, { headers });
|
|
252
|
+
if (!r.ok) {
|
|
253
|
+
// Fallback: alléén ID — beter dan helemaal niets in de lijst.
|
|
254
|
+
partners.push({ organizationId: orgId });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const detail = await r.json();
|
|
258
|
+
partners.push({ organizationId: orgId, ...detail });
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
partners.push({ organizationId: orgId });
|
|
262
|
+
}
|
|
263
|
+
await new Promise((res2) => setTimeout(res2, 50));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, Math.max(customerOrgIds.length, 1)) }, () => worker()));
|
|
267
|
+
const rows = partners
|
|
268
|
+
.map((p) => mapFloridayPartner(p, conn.id))
|
|
269
|
+
.filter((p) => {
|
|
270
|
+
if (p === null)
|
|
271
|
+
return false;
|
|
272
|
+
// Skip organisaties zonder echte naam én zonder GLN — dat zijn
|
|
273
|
+
// geanonimiseerde / beëindigde organisaties die Floriday niet meer
|
|
274
|
+
// als handelsrelatie toont.
|
|
275
|
+
const hasRealName = p.company_name &&
|
|
276
|
+
!p.company_name.startsWith("Floriday-organisatie ");
|
|
277
|
+
if (!hasRealName && !p.gln)
|
|
278
|
+
return false;
|
|
279
|
+
// Skip beëindigde organisaties (endDate in het verleden).
|
|
280
|
+
const rawAny = p.raw;
|
|
281
|
+
const endDate = rawAny?.endDate ? new Date(rawAny.endDate) : null;
|
|
282
|
+
if (endDate && Number.isFinite(endDate.getTime()) && endDate.getTime() < Date.now()) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
return true;
|
|
286
|
+
});
|
|
287
|
+
if (rows.length > 0) {
|
|
288
|
+
const payload = rows.map((r) => ({
|
|
289
|
+
organization_id: r.organization_id,
|
|
290
|
+
connection_id: r.connection_id,
|
|
291
|
+
data: JSON.parse(JSON.stringify(r)),
|
|
292
|
+
fetched_at: new Date().toISOString(),
|
|
293
|
+
}));
|
|
294
|
+
const { error: upsertError } = await supabaseAdmin
|
|
295
|
+
.from("floriday_customers_cache")
|
|
296
|
+
.upsert(payload);
|
|
297
|
+
if (upsertError)
|
|
298
|
+
throw new Error(upsertError.message);
|
|
299
|
+
// Upsert ook naar de zichtbare customers-tabel zodat Floriday-klanten
|
|
300
|
+
// verschijnen in /klanten.
|
|
301
|
+
const nowIso = new Date().toISOString();
|
|
302
|
+
const customerRows = rows.map((r) => ({
|
|
303
|
+
company_name: r.company_name,
|
|
304
|
+
contact_person: r.contact_person,
|
|
305
|
+
email: r.email,
|
|
306
|
+
phone: r.phone,
|
|
307
|
+
vat_number: r.vat_number,
|
|
308
|
+
street: r.street,
|
|
309
|
+
house_number: r.house_number,
|
|
310
|
+
postal_code: r.postal_code,
|
|
311
|
+
city: r.city,
|
|
312
|
+
country: r.country,
|
|
313
|
+
floriday_organization_id: r.organization_id,
|
|
314
|
+
floriday_connection_id: r.connection_id,
|
|
315
|
+
source: "floriday",
|
|
316
|
+
connection_status: "connected",
|
|
317
|
+
sync_status: "synced",
|
|
318
|
+
sync_error: null,
|
|
319
|
+
last_synced_at: nowIso,
|
|
320
|
+
is_active: true,
|
|
321
|
+
gln: r.gln,
|
|
322
|
+
commercial_name: r.commercial_name,
|
|
323
|
+
bio: r.bio,
|
|
324
|
+
segments: r.segments,
|
|
325
|
+
trade_forms: r.trade_forms,
|
|
326
|
+
markets: r.markets,
|
|
327
|
+
product_groups: r.product_groups,
|
|
328
|
+
payment_methods: r.payment_methods,
|
|
329
|
+
organization_type: r.organization_type,
|
|
330
|
+
website: r.website,
|
|
331
|
+
logo_url: r.logo_url,
|
|
332
|
+
custom_fields: JSON.parse(JSON.stringify(r.custom_fields ?? {})),
|
|
333
|
+
floriday_raw: JSON.parse(JSON.stringify(r.raw ?? {})),
|
|
334
|
+
}));
|
|
335
|
+
// Batchen om payload-limieten te ontwijken.
|
|
336
|
+
const batchSize = 250;
|
|
337
|
+
for (let i = 0; i < customerRows.length; i += batchSize) {
|
|
338
|
+
const batch = customerRows.slice(i, i + batchSize);
|
|
339
|
+
const { error: custErr } = await supabaseAdmin
|
|
340
|
+
.from("customers")
|
|
341
|
+
.upsert(batch, {
|
|
342
|
+
onConflict: "floriday_connection_id,floriday_organization_id",
|
|
343
|
+
});
|
|
344
|
+
if (custErr)
|
|
345
|
+
throw new Error(custErr.message);
|
|
346
|
+
}
|
|
347
|
+
// Haal customer-ids op om child-tabellen te kunnen vullen.
|
|
348
|
+
const orgIds = rows.map((r) => r.organization_id);
|
|
349
|
+
const { data: custIdRows } = await supabaseAdmin
|
|
350
|
+
.from("customers")
|
|
351
|
+
.select("id, floriday_organization_id")
|
|
352
|
+
.eq("floriday_connection_id", conn.id)
|
|
353
|
+
.in("floriday_organization_id", orgIds);
|
|
354
|
+
const orgToCustomerId = new Map((custIdRows ?? []).map((r) => [
|
|
355
|
+
r.floriday_organization_id,
|
|
356
|
+
r.id,
|
|
357
|
+
]));
|
|
358
|
+
const { matchWarnings } = await syncChildEntities({
|
|
359
|
+
token,
|
|
360
|
+
base,
|
|
361
|
+
apiKey: conn.api_key,
|
|
362
|
+
orgToCustomerId,
|
|
363
|
+
});
|
|
364
|
+
for (const w of matchWarnings) {
|
|
365
|
+
errors.push({
|
|
366
|
+
connectionId: conn.id,
|
|
367
|
+
message: `Waarschuwing child-sync (connection ${conn.id}): ${w}`,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
totalSynced += rows.length;
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
errors.push({
|
|
375
|
+
connectionId: conn.id,
|
|
376
|
+
message: `Floriday API-fout (connection ${conn.id}): ${err instanceof Error ? err.message : String(err)}`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return { synced: totalSynced, errors };
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Best-effort sync van child-entiteiten (contactpersonen, afleverlocaties,
|
|
384
|
+
* handelsinstellingen, geselecteerd assortiment). Floriday's Suppliers API
|
|
385
|
+
* heeft niet altijd 1-op-1 dezelfde endpoints als de FHC-koper-UI; mislukte
|
|
386
|
+
* endpoints worden stil overgeslagen zodat de hoofdsync niet faalt.
|
|
387
|
+
*/
|
|
388
|
+
async function syncChildEntities(params) {
|
|
389
|
+
const { token, base, apiKey, orgToCustomerId } = params;
|
|
390
|
+
const headers = {
|
|
391
|
+
Authorization: `Bearer ${token}`,
|
|
392
|
+
"X-Api-Key": apiKey,
|
|
393
|
+
Accept: "application/json",
|
|
394
|
+
};
|
|
395
|
+
const matchWarnings = [];
|
|
396
|
+
const trackMatch = (label, total, matched) => {
|
|
397
|
+
if (total === 0)
|
|
398
|
+
return;
|
|
399
|
+
if (matched / total < 0.5) {
|
|
400
|
+
matchWarnings.push(`${label}: ${matched}/${total} gekoppeld aan klant — controleer id-veld`);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
async function safeFetchList(url) {
|
|
404
|
+
try {
|
|
405
|
+
const res = await fetch(url, { headers });
|
|
406
|
+
if (!res.ok)
|
|
407
|
+
return [];
|
|
408
|
+
const json = await res.json();
|
|
409
|
+
if (Array.isArray(json))
|
|
410
|
+
return json;
|
|
411
|
+
if (Array.isArray(json?.results))
|
|
412
|
+
return json.results;
|
|
413
|
+
if (Array.isArray(json?.items))
|
|
414
|
+
return json.items;
|
|
415
|
+
return [];
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// CONTACTPERSONEN — probeer endpoint-varianten
|
|
422
|
+
// Let op: de Suppliers API 2026v1 exposeert géén contact-personen van
|
|
423
|
+
// klant-organisaties (alleen /organizations en /connections bestaan).
|
|
424
|
+
// We proberen toch de bulk sync-endpoints — voor het geval Floriday er
|
|
425
|
+
// ooit één publiceert — maar slaan de per-organisatie probe over,
|
|
426
|
+
// omdat die endpoints niet bestaan en alleen onnodige HTTP-calls
|
|
427
|
+
// genereren bij elke sync. Contactgegevens (e-mail, telefoon) moeten
|
|
428
|
+
// handmatig op de klant worden ingevuld.
|
|
429
|
+
const contactEndpoints = [
|
|
430
|
+
`${base}/contact-persons/sync/0?limitResult=1000`,
|
|
431
|
+
`${base}/contacts/sync/0?limitResult=1000`,
|
|
432
|
+
];
|
|
433
|
+
let contacts = [];
|
|
434
|
+
for (const url of contactEndpoints) {
|
|
435
|
+
contacts = await safeFetchList(url);
|
|
436
|
+
if (contacts.length > 0)
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
if (contacts.length > 0) {
|
|
440
|
+
let matched = 0;
|
|
441
|
+
const rows = contacts
|
|
442
|
+
.map((c) => {
|
|
443
|
+
const orgId = c?.organizationId ?? c?.companyId ?? c?.tradePartnerId;
|
|
444
|
+
const customerId = orgToCustomerId.get(String(orgId));
|
|
445
|
+
if (!customerId)
|
|
446
|
+
return null;
|
|
447
|
+
matched += 1;
|
|
448
|
+
const fullName = c?.fullName ??
|
|
449
|
+
[c?.firstName, c?.lastName].filter(Boolean).join(" ") ??
|
|
450
|
+
c?.name ??
|
|
451
|
+
null;
|
|
452
|
+
if (!fullName)
|
|
453
|
+
return null;
|
|
454
|
+
return {
|
|
455
|
+
customer_id: customerId,
|
|
456
|
+
floriday_contact_id: c?.contactPersonId ?? c?.id ?? c?.contactId ?? null,
|
|
457
|
+
full_name: fullName,
|
|
458
|
+
job_title: c?.jobTitle ?? c?.role ?? c?.function ?? null,
|
|
459
|
+
email: c?.email ?? c?.emailAddress ?? null,
|
|
460
|
+
phone: c?.phoneNumber ?? c?.phone ?? null,
|
|
461
|
+
photo_url: c?.photoUrl ?? c?.avatarUrl ?? null,
|
|
462
|
+
is_active: c?.isActive ?? true,
|
|
463
|
+
raw: c,
|
|
464
|
+
};
|
|
465
|
+
})
|
|
466
|
+
.filter((r) => r !== null);
|
|
467
|
+
trackMatch("contactpersonen", contacts.length, matched);
|
|
468
|
+
for (let i = 0; i < rows.length; i += 250) {
|
|
469
|
+
await supabaseAdmin
|
|
470
|
+
.from("customer_contacts")
|
|
471
|
+
.upsert(rows.slice(i, i + 250), {
|
|
472
|
+
onConflict: "customer_id,floriday_contact_id",
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// AFLEVERLOCATIES
|
|
477
|
+
const deliveryEndpoints = [
|
|
478
|
+
`${base}/delivery-locations/sync/0?limitResult=1000`,
|
|
479
|
+
`${base}/customer-delivery-locations/sync/0?limitResult=1000`,
|
|
480
|
+
];
|
|
481
|
+
let locations = [];
|
|
482
|
+
for (const url of deliveryEndpoints) {
|
|
483
|
+
locations = await safeFetchList(url);
|
|
484
|
+
if (locations.length > 0)
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
if (locations.length > 0) {
|
|
488
|
+
let matched = 0;
|
|
489
|
+
const rows = locations
|
|
490
|
+
.map((l) => {
|
|
491
|
+
const orgId = l?.organizationId ?? l?.companyId ?? l?.customerId;
|
|
492
|
+
const customerId = orgToCustomerId.get(String(orgId));
|
|
493
|
+
if (!customerId)
|
|
494
|
+
return null;
|
|
495
|
+
matched += 1;
|
|
496
|
+
const addr = l?.address ?? l?.physicalAddress ?? {};
|
|
497
|
+
return {
|
|
498
|
+
customer_id: customerId,
|
|
499
|
+
floriday_location_id: l?.deliveryLocationId ?? l?.locationId ?? l?.id ?? null,
|
|
500
|
+
name: l?.name ?? l?.locationName ?? null,
|
|
501
|
+
address_line: addr?.addressLine ??
|
|
502
|
+
([addr?.streetName, addr?.houseNumber].filter(Boolean).join(" ") ||
|
|
503
|
+
null),
|
|
504
|
+
postal_code: addr?.postalCode ?? null,
|
|
505
|
+
city: addr?.city ?? null,
|
|
506
|
+
country: addr?.countryCode ?? addr?.country ?? null,
|
|
507
|
+
gln: l?.gln ?? l?.locationGln ?? null,
|
|
508
|
+
is_primary: l?.isPrimary ?? l?.isFixed ?? false,
|
|
509
|
+
raw: l,
|
|
510
|
+
};
|
|
511
|
+
})
|
|
512
|
+
.filter((r) => r !== null);
|
|
513
|
+
trackMatch("afleverlocaties", locations.length, matched);
|
|
514
|
+
for (let i = 0; i < rows.length; i += 250) {
|
|
515
|
+
await supabaseAdmin
|
|
516
|
+
.from("customer_delivery_locations")
|
|
517
|
+
.upsert(rows.slice(i, i + 250), {
|
|
518
|
+
onConflict: "customer_id,floriday_location_id",
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
// GESELECTEERD ASSORTIMENT
|
|
523
|
+
const assortmentEndpoints = [
|
|
524
|
+
`${base}/customer-specific-offerings/sync/0?limitResult=1000`,
|
|
525
|
+
`${base}/selected-assortment/sync/0?limitResult=1000`,
|
|
526
|
+
`${base}/customer-assortments/sync/0?limitResult=1000`,
|
|
527
|
+
];
|
|
528
|
+
let assortment = [];
|
|
529
|
+
for (const url of assortmentEndpoints) {
|
|
530
|
+
assortment = await safeFetchList(url);
|
|
531
|
+
if (assortment.length > 0)
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
if (assortment.length > 0) {
|
|
535
|
+
let matched = 0;
|
|
536
|
+
const rows = assortment
|
|
537
|
+
.map((a) => {
|
|
538
|
+
const orgId = a?.organizationId ?? a?.customerId ?? a?.tradePartnerId;
|
|
539
|
+
const customerId = orgToCustomerId.get(String(orgId));
|
|
540
|
+
if (!customerId)
|
|
541
|
+
return null;
|
|
542
|
+
matched += 1;
|
|
543
|
+
const name = a?.productName ?? a?.tradeItemName ?? a?.name ?? null;
|
|
544
|
+
if (!name)
|
|
545
|
+
return null;
|
|
546
|
+
return {
|
|
547
|
+
customer_id: customerId,
|
|
548
|
+
floriday_item_id: a?.tradeItemId ?? a?.offeringId ?? a?.itemId ?? a?.id ?? null,
|
|
549
|
+
article_number: a?.articleNumber ?? a?.gtin ?? null,
|
|
550
|
+
product_name: name,
|
|
551
|
+
product_group: a?.productGroup ?? a?.category ?? null,
|
|
552
|
+
price: typeof a?.price === "number"
|
|
553
|
+
? a.price
|
|
554
|
+
: typeof a?.unitPrice === "number"
|
|
555
|
+
? a.unitPrice
|
|
556
|
+
: null,
|
|
557
|
+
unit: a?.unit ?? a?.priceUnit ?? null,
|
|
558
|
+
raw: a,
|
|
559
|
+
};
|
|
560
|
+
})
|
|
561
|
+
.filter((r) => r !== null);
|
|
562
|
+
trackMatch("assortiment", assortment.length, matched);
|
|
563
|
+
for (let i = 0; i < rows.length; i += 250) {
|
|
564
|
+
await supabaseAdmin
|
|
565
|
+
.from("customer_selected_assortment")
|
|
566
|
+
.upsert(rows.slice(i, i + 250), {
|
|
567
|
+
onConflict: "customer_id,floriday_item_id",
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return { matchWarnings };
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Nachtelijke sync van het volledige Floriday-netwerk (alle organisaties
|
|
575
|
+
* waar de connectie zicht op heeft). Wordt incrementeel opgehaald via een
|
|
576
|
+
* per-connectie sequence number en in `floriday_network_cache` opgeslagen.
|
|
577
|
+
* Raakt de `customers`-tabel níet aan — die wordt door
|
|
578
|
+
* syncAllFloridayCustomers() (= trade-partners) gevuld.
|
|
579
|
+
*/
|
|
580
|
+
export async function syncFloridayNetwork() {
|
|
581
|
+
const { data: settings } = await supabaseAdmin
|
|
582
|
+
.from("floriday_settings")
|
|
583
|
+
.select("active_environment")
|
|
584
|
+
.eq("id", 1)
|
|
585
|
+
.maybeSingle();
|
|
586
|
+
const activeEnv = settings?.active_environment ?? "staging";
|
|
587
|
+
const { data: connections, error } = await supabaseAdmin
|
|
588
|
+
.from("floriday_connections")
|
|
589
|
+
.select("*")
|
|
590
|
+
.eq("is_active", true)
|
|
591
|
+
.eq("environment", activeEnv);
|
|
592
|
+
if (error)
|
|
593
|
+
throw new Error(error.message);
|
|
594
|
+
const errors = [];
|
|
595
|
+
let totalUpserted = 0;
|
|
596
|
+
for (const conn of connections ?? []) {
|
|
597
|
+
try {
|
|
598
|
+
const token = await ensureValidToken(conn);
|
|
599
|
+
const base = baseUrlFor(conn.environment);
|
|
600
|
+
const { data: state } = await supabaseAdmin
|
|
601
|
+
.from("floriday_network_sync_state")
|
|
602
|
+
.select("last_sequence_number")
|
|
603
|
+
.eq("connection_id", conn.id)
|
|
604
|
+
.maybeSingle();
|
|
605
|
+
let seq = state?.last_sequence_number ?? 0;
|
|
606
|
+
let pages = 0;
|
|
607
|
+
const maxPages = 50; // bovengrens, beschermt tegen runaway loops
|
|
608
|
+
while (pages < maxPages) {
|
|
609
|
+
const url = `${base}/organizations/sync/${seq}?limitResult=1000`;
|
|
610
|
+
const res = await fetchWithRetry(url, {
|
|
611
|
+
headers: {
|
|
612
|
+
Authorization: `Bearer ${token}`,
|
|
613
|
+
"X-Api-Key": conn.api_key,
|
|
614
|
+
Accept: "application/json",
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
if (!res.ok) {
|
|
618
|
+
const body = await res.text().catch(() => "");
|
|
619
|
+
throw new Error(`${url} → HTTP ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`);
|
|
620
|
+
}
|
|
621
|
+
const json = (await res.json());
|
|
622
|
+
const items = Array.isArray(json)
|
|
623
|
+
? json
|
|
624
|
+
: Array.isArray(json?.results)
|
|
625
|
+
? json.results
|
|
626
|
+
: Array.isArray(json?.items)
|
|
627
|
+
? json.items
|
|
628
|
+
: [];
|
|
629
|
+
if (items.length === 0)
|
|
630
|
+
break;
|
|
631
|
+
const rows = items
|
|
632
|
+
.map((o) => {
|
|
633
|
+
const orgId = o?.organizationId ?? o?.id;
|
|
634
|
+
if (!orgId)
|
|
635
|
+
return null;
|
|
636
|
+
const addr = o?.physicalAddress ?? o?.mailingAddress ?? o?.address ?? {};
|
|
637
|
+
return {
|
|
638
|
+
connection_id: conn.id,
|
|
639
|
+
organization_id: String(orgId),
|
|
640
|
+
organization_name: o?.organizationName ?? o?.name ?? o?.commercialName ?? null,
|
|
641
|
+
gln: o?.companyGln ?? o?.gln ?? null,
|
|
642
|
+
country: addr?.countryCode ?? addr?.country ?? null,
|
|
643
|
+
city: addr?.city ?? null,
|
|
644
|
+
data: o,
|
|
645
|
+
fetched_at: new Date().toISOString(),
|
|
646
|
+
};
|
|
647
|
+
})
|
|
648
|
+
.filter((r) => r !== null);
|
|
649
|
+
if (rows.length > 0) {
|
|
650
|
+
const { error: upErr } = await supabaseAdmin
|
|
651
|
+
.from("floriday_network_cache")
|
|
652
|
+
.upsert(rows, { onConflict: "connection_id,organization_id" });
|
|
653
|
+
if (upErr)
|
|
654
|
+
throw new Error(upErr.message);
|
|
655
|
+
totalUpserted += rows.length;
|
|
656
|
+
}
|
|
657
|
+
// Volgende sequence: hoogste sequenceNumber in de batch + 1.
|
|
658
|
+
const maxSeq = items.reduce((m, it) => {
|
|
659
|
+
const s = Number(it?.sequenceNumber ?? it?.seq ?? 0);
|
|
660
|
+
return Number.isFinite(s) && s > m ? s : m;
|
|
661
|
+
}, seq);
|
|
662
|
+
if (maxSeq <= seq)
|
|
663
|
+
break;
|
|
664
|
+
seq = maxSeq;
|
|
665
|
+
pages += 1;
|
|
666
|
+
if (items.length < 1000)
|
|
667
|
+
break; // laatste pagina
|
|
668
|
+
}
|
|
669
|
+
await supabaseAdmin
|
|
670
|
+
.from("floriday_network_sync_state")
|
|
671
|
+
.upsert({
|
|
672
|
+
connection_id: conn.id,
|
|
673
|
+
last_sequence_number: seq,
|
|
674
|
+
last_synced_at: new Date().toISOString(),
|
|
675
|
+
last_error: null,
|
|
676
|
+
updated_at: new Date().toISOString(),
|
|
677
|
+
});
|
|
678
|
+
// Verrijk organisaties met lege naam via /organizations/{id} — Floriday
|
|
679
|
+
// anonimiseert namen in /organizations/sync; het detail-endpoint geeft
|
|
680
|
+
// ze wel terug voor organisaties waar deze connectie rechten op heeft.
|
|
681
|
+
await enrichNetworkOrganizations({
|
|
682
|
+
connectionId: conn.id,
|
|
683
|
+
token,
|
|
684
|
+
base,
|
|
685
|
+
apiKey: conn.api_key,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
catch (err) {
|
|
689
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
690
|
+
errors.push({ connectionId: conn.id, message });
|
|
691
|
+
await supabaseAdmin
|
|
692
|
+
.from("floriday_network_sync_state")
|
|
693
|
+
.upsert({
|
|
694
|
+
connection_id: conn.id,
|
|
695
|
+
last_sequence_number: 0,
|
|
696
|
+
last_error: message,
|
|
697
|
+
updated_at: new Date().toISOString(),
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return { upserted: totalUpserted, errors };
|
|
702
|
+
}
|
|
703
|
+
async function enrichNetworkOrganizations(params) {
|
|
704
|
+
const { connectionId, token, base, apiKey } = params;
|
|
705
|
+
const headers = {
|
|
706
|
+
Authorization: `Bearer ${token}`,
|
|
707
|
+
"X-Api-Key": apiKey,
|
|
708
|
+
Accept: "application/json",
|
|
709
|
+
};
|
|
710
|
+
// Pak max 200 organisaties per run zonder naam, om Floriday-rate-limits te
|
|
711
|
+
// respecteren. Volgende runs werken de rest af.
|
|
712
|
+
const { data: emptyRows } = await supabaseAdmin
|
|
713
|
+
.from("floriday_network_cache")
|
|
714
|
+
.select("organization_id")
|
|
715
|
+
.eq("connection_id", connectionId)
|
|
716
|
+
.or("organization_name.is.null,organization_name.eq.")
|
|
717
|
+
.limit(200);
|
|
718
|
+
if (!emptyRows || emptyRows.length === 0)
|
|
719
|
+
return;
|
|
720
|
+
const rows = emptyRows;
|
|
721
|
+
const concurrency = 5;
|
|
722
|
+
let cursor = 0;
|
|
723
|
+
async function worker() {
|
|
724
|
+
while (cursor < rows.length) {
|
|
725
|
+
const idx = cursor++;
|
|
726
|
+
const row = rows[idx];
|
|
727
|
+
const url = `${base}/organizations/${encodeURIComponent(row.organization_id)}`;
|
|
728
|
+
try {
|
|
729
|
+
const res = await fetch(url, { headers });
|
|
730
|
+
if (!res.ok)
|
|
731
|
+
continue;
|
|
732
|
+
const detail = await res.json();
|
|
733
|
+
const addr = detail?.physicalAddress ??
|
|
734
|
+
detail?.mailingAddress ??
|
|
735
|
+
detail?.address ??
|
|
736
|
+
{};
|
|
737
|
+
const name = detail?.organizationName ??
|
|
738
|
+
detail?.name ??
|
|
739
|
+
detail?.commercialName ??
|
|
740
|
+
null;
|
|
741
|
+
const gln = detail?.companyGln ?? detail?.gln ?? null;
|
|
742
|
+
if (!name && !gln)
|
|
743
|
+
continue; // niets bruikbaars terug
|
|
744
|
+
await supabaseAdmin
|
|
745
|
+
.from("floriday_network_cache")
|
|
746
|
+
.update({
|
|
747
|
+
organization_name: name || null,
|
|
748
|
+
gln: gln || null,
|
|
749
|
+
city: addr?.city ?? null,
|
|
750
|
+
country: addr?.countryCode ?? addr?.country ?? null,
|
|
751
|
+
data: detail,
|
|
752
|
+
fetched_at: new Date().toISOString(),
|
|
753
|
+
})
|
|
754
|
+
.eq("connection_id", connectionId)
|
|
755
|
+
.eq("organization_id", row.organization_id);
|
|
756
|
+
}
|
|
757
|
+
catch {
|
|
758
|
+
// ignore — best effort
|
|
759
|
+
}
|
|
760
|
+
// throttle een beetje
|
|
761
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, rows.length) }, () => worker()));
|
|
765
|
+
}
|