@lofder/dsers-mcp-product 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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/dist/dsers/account.d.ts +17 -0
  4. package/dist/dsers/account.d.ts.map +1 -0
  5. package/dist/dsers/account.js +37 -0
  6. package/dist/dsers/account.js.map +1 -0
  7. package/dist/dsers/auth.d.ts +14 -0
  8. package/dist/dsers/auth.d.ts.map +1 -0
  9. package/dist/dsers/auth.js +87 -0
  10. package/dist/dsers/auth.js.map +1 -0
  11. package/dist/dsers/client.d.ts +21 -0
  12. package/dist/dsers/client.d.ts.map +1 -0
  13. package/dist/dsers/client.js +78 -0
  14. package/dist/dsers/client.js.map +1 -0
  15. package/dist/dsers/config.d.ts +9 -0
  16. package/dist/dsers/config.d.ts.map +1 -0
  17. package/dist/dsers/config.js +28 -0
  18. package/dist/dsers/config.js.map +1 -0
  19. package/dist/dsers/product.d.ts +42 -0
  20. package/dist/dsers/product.d.ts.map +1 -0
  21. package/dist/dsers/product.js +289 -0
  22. package/dist/dsers/product.js.map +1 -0
  23. package/dist/dsers/settings.d.ts +30 -0
  24. package/dist/dsers/settings.d.ts.map +1 -0
  25. package/dist/dsers/settings.js +63 -0
  26. package/dist/dsers/settings.js.map +1 -0
  27. package/dist/error-map.d.ts +7 -0
  28. package/dist/error-map.d.ts.map +1 -0
  29. package/dist/error-map.js +231 -0
  30. package/dist/error-map.js.map +1 -0
  31. package/dist/index.d.ts +17 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +43 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/job-store-memory.d.ts +8 -0
  36. package/dist/job-store-memory.d.ts.map +1 -0
  37. package/dist/job-store-memory.js +60 -0
  38. package/dist/job-store-memory.js.map +1 -0
  39. package/dist/job-store.d.ts +14 -0
  40. package/dist/job-store.d.ts.map +1 -0
  41. package/dist/job-store.js +31 -0
  42. package/dist/job-store.js.map +1 -0
  43. package/dist/provider.d.ts +53 -0
  44. package/dist/provider.d.ts.map +1 -0
  45. package/dist/provider.js +1298 -0
  46. package/dist/provider.js.map +1 -0
  47. package/dist/push-options.d.ts +7 -0
  48. package/dist/push-options.d.ts.map +1 -0
  49. package/dist/push-options.js +158 -0
  50. package/dist/push-options.js.map +1 -0
  51. package/dist/resolver.d.ts +8 -0
  52. package/dist/resolver.d.ts.map +1 -0
  53. package/dist/resolver.js +81 -0
  54. package/dist/resolver.js.map +1 -0
  55. package/dist/rules.d.ts +14 -0
  56. package/dist/rules.d.ts.map +1 -0
  57. package/dist/rules.js +332 -0
  58. package/dist/rules.js.map +1 -0
  59. package/dist/service.d.ts +22 -0
  60. package/dist/service.d.ts.map +1 -0
  61. package/dist/service.js +461 -0
  62. package/dist/service.js.map +1 -0
  63. package/dist/tools.d.ts +4 -0
  64. package/dist/tools.d.ts.map +1 -0
  65. package/dist/tools.js +402 -0
  66. package/dist/tools.js.map +1 -0
  67. package/package.json +54 -0
@@ -0,0 +1,1298 @@
1
+ import { DSersClient, DSersAPIError } from "./dsers/client.js";
2
+ import { configFromEnv } from "./dsers/config.js";
3
+ import * as account from "./dsers/account.js";
4
+ import * as product from "./dsers/product.js";
5
+ import * as settings from "./dsers/settings.js";
6
+ async function safeCall(fn) {
7
+ try {
8
+ return await fn();
9
+ }
10
+ catch (err) {
11
+ if (err instanceof DSersAPIError) {
12
+ const result = { error: err.message };
13
+ result.status = err.status;
14
+ result.detail = err.body;
15
+ return result;
16
+ }
17
+ throw err;
18
+ }
19
+ }
20
+ const DEFAULT_ALIEXPRESS_APP_ID = "159831080";
21
+ const DEFAULT_ALIBABA_APP_ID = "1902659021782450176";
22
+ const DEFAULT_PUSH_CHANNELS = [
23
+ "online_store",
24
+ "shop_app",
25
+ "google_youtube",
26
+ "tiktok",
27
+ "facebook_instagram",
28
+ "amazon",
29
+ ];
30
+ const ALIEXPRESS_ID_PATTERN = /\/item\/(\d+)\.html/i;
31
+ const ALIBABA_ID_PATTERN = /\/product-detail\/[^_]+_(\d+)\.html/i;
32
+ const ALI1688_ID_PATTERN = /1688\.com\/(?:offer|product-detail)\/(\d+)\.html/i;
33
+ export class PrivateDsersProvider {
34
+ name = "private-dsers";
35
+ client;
36
+ aliexpressAppId;
37
+ alibabaAppId;
38
+ constructor(config) {
39
+ const cfg = config ?? configFromEnv();
40
+ this.client = new DSersClient(cfg);
41
+ this.aliexpressAppId = String(process.env.PRIVATE_DSERS_ALIEXPRESS_APP_ID || DEFAULT_ALIEXPRESS_APP_ID);
42
+ this.alibabaAppId = String(process.env.PRIVATE_DSERS_ALIBABA_APP_ID || DEFAULT_ALIBABA_APP_ID);
43
+ }
44
+ // ── ImportProvider interface ──
45
+ async getRuleCapabilities(targetStore) {
46
+ let stores = await this.listStores();
47
+ stores = await this.enrichShopifyProfiles(stores);
48
+ const visibilityModes = ["backend_only", "sell_immediately"];
49
+ const notes = [
50
+ "Provider-native pricing and auto-sync capabilities are available through the private adapter.",
51
+ "Advanced image transformations are not auto-applied in this MVP.",
52
+ ];
53
+ if (targetStore) {
54
+ notes.push(`Target store hint received: ${targetStore}`);
55
+ }
56
+ return {
57
+ provider_label: "Private DSers Adapter",
58
+ source_support: ["aliexpress", "alibaba", "1688"],
59
+ stores,
60
+ rule_families: {
61
+ pricing: {
62
+ supported: true,
63
+ modes: ["provider_default", "multiplier", "fixed_markup"],
64
+ native_snapshot_available: true,
65
+ },
66
+ content: {
67
+ supported: [
68
+ "title_override",
69
+ "title_prefix",
70
+ "title_suffix",
71
+ "description_override_html",
72
+ "description_append_html",
73
+ ],
74
+ unsupported: ["tags_add"],
75
+ },
76
+ images: {
77
+ supported: ["keep_first_n", "drop_indexes"],
78
+ unsupported: ["translate_image_text", "remove_logo"],
79
+ },
80
+ visibility: { supported_modes: visibilityModes },
81
+ },
82
+ push_options: {
83
+ supported: [
84
+ "publish_to_online_store",
85
+ "only_push_specifications",
86
+ "image_strategy",
87
+ "pricing_rule_behavior",
88
+ "auto_inventory_update",
89
+ "auto_price_update",
90
+ "sales_channels",
91
+ "store_shipping_profile",
92
+ "shipping_profile_name",
93
+ ],
94
+ image_strategy_modes: ["selected_only", "all_available"],
95
+ pricing_rule_behavior_modes: [
96
+ "keep_manual",
97
+ "apply_store_pricing_rule",
98
+ ],
99
+ sales_channels: DEFAULT_PUSH_CHANNELS,
100
+ shipping_profile_name_hint: "For Shopify stores, specify a delivery profile name " +
101
+ "(e.g. 'DSers Shipping Profile') to override the default. " +
102
+ "If omitted, the profile marked as default (isChecked) is used automatically.",
103
+ },
104
+ notes,
105
+ };
106
+ }
107
+ async prepareCandidate(sourceUrl, sourceHint, country) {
108
+ let [sourceKind, appId, supplyProductId] = this.resolveSourceIdentifier(sourceUrl);
109
+ const cleanUrl = this.cleanProductUrl(sourceUrl);
110
+ const parsePayload = await safeCall(() => product.parseProductUrl(this.client, cleanUrl, appId));
111
+ const canonicalId = this.extractSupplyProductId(parsePayload ?? {});
112
+ const afTraceId = this.extractAfTraceId(sourceUrl);
113
+ if (!supplyProductId) {
114
+ this.raiseIfError(parsePayload, "Could not resolve the supplier product URL. Verify the URL is a valid AliExpress, Alibaba, or 1688 product link.");
115
+ supplyProductId = canonicalId || afTraceId;
116
+ }
117
+ if (!supplyProductId) {
118
+ throw new Error(`Could not extract a supplier product ID from the URL: ${sourceUrl}. Ensure it contains a numeric product ID.`);
119
+ }
120
+ const importPayload = await safeCall(() => product.importByProductId(this.client, {
121
+ supplyProductId: supplyProductId,
122
+ supplyAppId: appId,
123
+ country,
124
+ }));
125
+ if (this.hasReason(importPayload, "ALIBABA_NOT_AVAILABLE")) {
126
+ throw new Error("The selected Alibaba or 1688 product is recognized, but DSers reports it is currently not available for import.");
127
+ }
128
+ if (this.hasReason(importPayload, "PRODUCT_STATUS_NOT_ONSELLING")) {
129
+ throw new Error("The selected supplier product is recognized, but DSers reports it is not currently importable under the chosen source app.");
130
+ }
131
+ const alreadyExists = this.hasReason(importPayload, "IMPORT_LIST_PRODUCT_ALREADY_EXISTS");
132
+ if (!alreadyExists) {
133
+ this.raiseIfError(importPayload, `Could not import this ${sourceKind} product. The DSers account may not have the required source app enabled, or the product is unavailable.`);
134
+ }
135
+ let importItemId = this.extractImportItemId(importPayload);
136
+ if (!importItemId) {
137
+ const searchIds = new Set([supplyProductId, canonicalId, afTraceId].filter(Boolean));
138
+ importItemId = await this.recoverImportItemId(searchIds);
139
+ }
140
+ if (!importItemId) {
141
+ throw new Error("Could not locate the imported product draft in the import list. The import may have failed silently.");
142
+ }
143
+ const itemPayload = await safeCall(() => product.getImportListItem(this.client, importItemId));
144
+ this.raiseIfError(itemPayload, "Could not fetch the imported product draft. The item may have been deleted from the import list.");
145
+ const [draft, fieldMap, warnings] = this.normalizeImportItem(itemPayload);
146
+ const providerState = {
147
+ import_item_id: importItemId,
148
+ source_hint: sourceHint,
149
+ source_kind: sourceKind,
150
+ source_app_id: appId,
151
+ country,
152
+ field_map: fieldMap,
153
+ supply_product_id: supplyProductId,
154
+ };
155
+ return {
156
+ provider_label: "Private DSers Adapter",
157
+ provider_state: providerState,
158
+ draft,
159
+ warnings,
160
+ resolved_source: sourceUrl,
161
+ };
162
+ }
163
+ async commitCandidate(providerState, draft, targetStore, visibilityMode, pushOptions) {
164
+ const fieldMap = providerState.field_map ?? {};
165
+ const warnings = [];
166
+ const pricingRuleBehavior = String(pushOptions.pricing_rule_behavior ?? "keep_manual");
167
+ const updateArgs = {
168
+ id: providerState.import_item_id,
169
+ };
170
+ const titleKey = fieldMap.title_key ?? "title";
171
+ const descriptionKey = fieldMap.description_key ?? "description";
172
+ const tagsKey = fieldMap.tags_key;
173
+ updateArgs[titleKey] = draft.title;
174
+ updateArgs[descriptionKey] = draft.description_html;
175
+ if (tagsKey) {
176
+ updateArgs[tagsKey] = draft.tags ?? [];
177
+ }
178
+ else if (draft.tags?.length) {
179
+ warnings.push("Tag edits were skipped because the DSers import list API does not support direct tag writes for this item type.");
180
+ }
181
+ const variantsKey = fieldMap.variants_key;
182
+ if (variantsKey && fieldMap.raw_variants?.length) {
183
+ updateArgs[variantsKey] = this.denormalizeVariants(draft.variants ?? [], fieldMap);
184
+ const priceEditFlagKey = fieldMap.price_edit_flag_key;
185
+ if (priceEditFlagKey) {
186
+ updateArgs[priceEditFlagKey] =
187
+ pricingRuleBehavior !== "apply_store_pricing_rule";
188
+ }
189
+ const supplyKey = fieldMap.supply_key;
190
+ if (supplyKey && fieldMap.raw_supply) {
191
+ updateArgs[supplyKey] = this.denormalizeSupply(draft.variants ?? [], fieldMap);
192
+ }
193
+ const priceBounds = this.computePriceBounds(draft.variants ?? []);
194
+ if (fieldMap.min_price_key && priceBounds[0] != null) {
195
+ updateArgs[fieldMap.min_price_key] = priceBounds[0];
196
+ }
197
+ if (fieldMap.max_price_key && priceBounds[1] != null) {
198
+ updateArgs[fieldMap.max_price_key] = priceBounds[1];
199
+ }
200
+ }
201
+ const imagesKey = fieldMap.images_key;
202
+ if (imagesKey &&
203
+ (fieldMap.images_mode === "string_list" ||
204
+ fieldMap.images_mode === "dict_list")) {
205
+ updateArgs[imagesKey] = this.denormalizeImages(draft.images ?? [], fieldMap);
206
+ const mainImageKey = fieldMap.main_image_key;
207
+ if (mainImageKey) {
208
+ updateArgs[mainImageKey] = (draft.images ?? [null])[0];
209
+ }
210
+ }
211
+ else if (imagesKey && draft.images != null) {
212
+ warnings.push("Image edits were skipped because the detected image structure could not be safely written back.");
213
+ }
214
+ const updatePayload = await safeCall(() => product.updateImportListItem(this.client, providerState.import_item_id, updateArgs));
215
+ this.raiseIfError(updatePayload, "Could not save the updated product draft. The import list item may have been modified or deleted.");
216
+ const store = await this.resolveStore(targetStore);
217
+ const pushArgs = this.buildPushArguments(providerState.import_item_id, store.store_ref, visibilityMode, pushOptions);
218
+ warnings.push(...(await this.refreshProductShippingInfo(providerState)));
219
+ warnings.push(...(await this.attachShippingTemplateLogistics(providerState, store.store_ref, pushArgs)));
220
+ warnings.push(...(await this.attachStoreShippingProfile(store, pushArgs, pushOptions, providerState.import_item_id)));
221
+ const pushPayload = await safeCall(() => product.pushToStore(this.client, pushArgs));
222
+ this.raiseIfError(pushPayload, "Could not push the product to the store. Check that the store is connected and the product draft is valid.");
223
+ const eventId = this.findFirstValueByKeys(pushPayload, [
224
+ "event_id",
225
+ "eventId",
226
+ "id",
227
+ ]);
228
+ let pushState = "requested";
229
+ let statusPayload = null;
230
+ if (eventId) {
231
+ try {
232
+ for (let attempt = 0; attempt < 4; attempt++) {
233
+ statusPayload = await product.getPushStatus(this.client, String(eventId));
234
+ pushState = this.extractPushState(statusPayload);
235
+ if (pushState === "failed" || pushState === "completed")
236
+ break;
237
+ if (attempt < 3)
238
+ await sleep(10000);
239
+ }
240
+ }
241
+ catch {
242
+ warnings.push("Push status polling could not complete. The push may still be processing — call dsers.job.status later to check.");
243
+ }
244
+ }
245
+ if (pushState === "failed") {
246
+ const pushError = this.extractPushError(statusPayload ?? {});
247
+ if (pushError)
248
+ warnings.push(`Provider push failed: ${pushError}`);
249
+ }
250
+ const visibilityApplied = pushOptions.publish_to_online_store
251
+ ? "sell_immediately"
252
+ : "backend_only";
253
+ if (visibilityMode === "sell_immediately" &&
254
+ !pushOptions.publish_to_online_store) {
255
+ warnings.push("sell_immediately was requested, but publish_to_online_store resolved to false in push_options.");
256
+ }
257
+ return {
258
+ provider_label: "Private DSers Adapter",
259
+ job_status: pushState,
260
+ event_id: eventId,
261
+ visibility_requested: visibilityMode,
262
+ visibility_applied: visibilityApplied,
263
+ push_options_applied: pushOptions,
264
+ target_store: store.display_name ?? store.store_ref,
265
+ warnings,
266
+ summary: {
267
+ title: draft.title,
268
+ image_count: (draft.images ?? []).length,
269
+ variant_count: (draft.variants ?? []).length,
270
+ },
271
+ };
272
+ }
273
+ // ── Internal helpers ──
274
+ raiseIfError(payload, genericMessage) {
275
+ if (payload?.error) {
276
+ const detail = String(payload.detail ?? payload.error);
277
+ if (detail.includes("PERMISSION_DENIED"))
278
+ throw new Error(genericMessage);
279
+ throw new Error(`${genericMessage} Provider detail: ${payload.error}`);
280
+ }
281
+ }
282
+ buildPushArguments(importItemId, storeRef, visibilityMode, pushOptions) {
283
+ const importListId = coerceNumericId(importItemId);
284
+ const storeId = coerceNumericId(storeRef);
285
+ const visible = Boolean(pushOptions.publish_to_online_store);
286
+ const pushAllImages = String(pushOptions.image_strategy ?? "selected_only") === "all_available";
287
+ const withPriceRule = String(pushOptions.pricing_rule_behavior ?? "keep_manual") ===
288
+ "apply_store_pricing_rule";
289
+ const autoInventoryUpdate = Boolean(pushOptions.auto_inventory_update);
290
+ const autoPriceUpdate = Boolean(pushOptions.auto_price_update);
291
+ const salesChannels = Array.isArray(pushOptions.sales_channels)
292
+ ? pushOptions.sales_channels
293
+ : [];
294
+ const request = {
295
+ importListIds: [importListId],
296
+ storeIds: [storeId],
297
+ visible,
298
+ pushStatus: visibilityMode === "sell_immediately" ? "ACTIVE" : "DRAFT",
299
+ inventoryPolicy: false,
300
+ onlyPushSpecifications: Boolean(pushOptions.only_push_specifications),
301
+ isPushAllImage: pushAllImages,
302
+ storeLanguageList: [{ storeId, language: "EN" }],
303
+ pushProducts: [{ importListId, pushLanguageCode: "EN" }],
304
+ skus: [],
305
+ stores: [],
306
+ saleChannels: salesChannels,
307
+ logistics: [
308
+ {
309
+ storeId,
310
+ importListId,
311
+ shipCost: "",
312
+ logisticId: "",
313
+ switch: true,
314
+ },
315
+ ],
316
+ pricingRuleImportListIds: [{ importListId, storeId }],
317
+ };
318
+ if (withPriceRule)
319
+ request.withPriceRule = true;
320
+ request.myProductSyncSetting = {
321
+ autoUpdateStock: autoInventoryUpdate,
322
+ autoUpdatePrice: autoPriceUpdate,
323
+ handleUpdatePrice: autoPriceUpdate,
324
+ };
325
+ return request;
326
+ }
327
+ async refreshProductShippingInfo(providerState) {
328
+ const sourceAppId = providerState.source_app_id;
329
+ if (sourceAppId == null || sourceAppId === "")
330
+ return [];
331
+ const payload = await safeCall(() => settings.getProductShippingInfo(this.client, coerceNumericId(sourceAppId)));
332
+ if (payload?.error)
333
+ return [
334
+ "DSers product shipping config could not be loaded before refresh.",
335
+ ];
336
+ const data = payload.data;
337
+ if (!data || typeof data !== "object")
338
+ return [
339
+ "DSers product shipping config payload was empty before refresh.",
340
+ ];
341
+ const shippingInfo = data.shippingInfo;
342
+ if (!shippingInfo ||
343
+ typeof shippingInfo !== "object" ||
344
+ !Object.keys(shippingInfo).length)
345
+ return [
346
+ "DSers product shipping config did not include a reusable shippingInfo object.",
347
+ ];
348
+ const warns = [];
349
+ let enabled = data.status;
350
+ if (enabled == null) {
351
+ enabled = Boolean(shippingInfo);
352
+ }
353
+ else if (!enabled) {
354
+ enabled = true;
355
+ warns.push("Enabled DSers product shipping config because a shipping template already exists.");
356
+ }
357
+ const updatePayload = await safeCall(() => settings.updateProductShippingInfo(this.client, {
358
+ status: Boolean(enabled),
359
+ shippingInfo,
360
+ }));
361
+ if (updatePayload?.error) {
362
+ warns.push("DSers product shipping config refresh failed before push.");
363
+ return warns;
364
+ }
365
+ warns.push("Refreshed DSers product shipping config before push.");
366
+ return warns;
367
+ }
368
+ async attachShippingTemplateLogistics(providerState, storeRef, pushArgs) {
369
+ if (pushArgs.logistics?.length)
370
+ return [];
371
+ const sourceAppId = providerState.source_app_id;
372
+ if (sourceAppId == null || sourceAppId === "")
373
+ return [];
374
+ const importListId = coerceNumericId(providerState.import_item_id);
375
+ const storeId = coerceNumericId(storeRef);
376
+ const country = String(providerState.country ?? "").trim().toUpperCase();
377
+ const supplyProductId = String(providerState.supply_product_id ?? "").trim();
378
+ const [serviceIds, sourceLabel, serviceWarnings] = await this.getTemplateServiceIds(sourceAppId, supplyProductId, country);
379
+ const warnings = [...serviceWarnings];
380
+ if (!serviceIds.length)
381
+ return warnings;
382
+ const availableIds = await this.getPushLogisticsIds(importListId, storeId);
383
+ let selectedId = "";
384
+ if (availableIds.length) {
385
+ selectedId = serviceIds.find((id) => availableIds.includes(id)) ?? "";
386
+ if (!selectedId) {
387
+ warnings.push("DSers returned push-logistics options for the selected store, but none matched the current shipping template.");
388
+ return warnings;
389
+ }
390
+ }
391
+ else {
392
+ selectedId = serviceIds[0];
393
+ }
394
+ pushArgs.logistics = [
395
+ { importListId, storeId, logisticId: selectedId },
396
+ ];
397
+ const appliedFrom = sourceLabel ? ` from ${sourceLabel}` : "";
398
+ warnings.push(`Applied DSers shipping template logistic '${selectedId}'${appliedFrom} to the push request.`);
399
+ return warnings;
400
+ }
401
+ async attachStoreShippingProfile(store, pushArgs, pushOptions, _importItemId) {
402
+ if (pushArgs.storeShippingProfile)
403
+ return [];
404
+ const storeRef = store.store_ref ?? "";
405
+ const storeDomain = store.domain ?? "";
406
+ const storeName = store.display_name ?? storeRef;
407
+ const isShopify = storeDomain.includes(".myshopify.com") ||
408
+ String(store.platform ?? "").toLowerCase() === "shopify";
409
+ if (!isShopify)
410
+ return [];
411
+ const warnings = [];
412
+ const storeId = coerceNumericId(storeRef);
413
+ let profileItems = null;
414
+ const desiredName = String(pushOptions.shipping_profile_name ?? "").trim();
415
+ try {
416
+ const profilesByStore = await this.fetchShopifyProfiles();
417
+ const targetKey = String(storeId);
418
+ const rawProfiles = profilesByStore[targetKey] ?? [];
419
+ if (desiredName) {
420
+ for (const profile of rawProfiles) {
421
+ if ((profile.name ?? "").trim().toLowerCase() ===
422
+ desiredName.toLowerCase()) {
423
+ profileItems = extractProfileGids(profile, targetKey);
424
+ if (profileItems)
425
+ warnings.push(`Matched shipping profile by name: '${desiredName}'.`);
426
+ break;
427
+ }
428
+ }
429
+ if (!profileItems) {
430
+ const available = rawProfiles.map((p) => p.name ?? "");
431
+ warnings.push(`shipping_profile_name '${desiredName}' not found. Available profiles: ${JSON.stringify(available)}. Falling back to default.`);
432
+ }
433
+ }
434
+ if (!profileItems) {
435
+ for (const profile of rawProfiles) {
436
+ if (profile.isChecked) {
437
+ profileItems = extractProfileGids(profile, targetKey);
438
+ break;
439
+ }
440
+ }
441
+ }
442
+ }
443
+ catch {
444
+ warnings.push("Could not query Shopify delivery profiles.");
445
+ }
446
+ if (!profileItems) {
447
+ const fallback = pushOptions.store_shipping_profile;
448
+ if (Array.isArray(fallback) && fallback.length) {
449
+ profileItems = fallback;
450
+ warnings.push("Using store_shipping_profile from push_options (API returned empty).");
451
+ }
452
+ }
453
+ if (profileItems) {
454
+ pushArgs.storeShippingProfile = profileItems;
455
+ warnings.push("Attached Shopify delivery profile to the push request.");
456
+ }
457
+ else {
458
+ warnings.push(`Shopify store '${storeName}' (${storeDomain}) has no Delivery Profile ` +
459
+ `configured in DSers. The push will likely fail with 'shipping profile ` +
460
+ `not found'. To fix: open DSers web UI -> Settings -> Shipping -> configure ` +
461
+ `a Delivery Profile for this store, or provide store_shipping_profile ` +
462
+ `in push_options.`);
463
+ }
464
+ return warnings;
465
+ }
466
+ async getTemplateServiceIds(sourceAppId, supplyProductId, country) {
467
+ const warnings = [];
468
+ const productPayload = await safeCall(() => settings.getProductShipSettings(this.client, {
469
+ supplierProductId: supplyProductId ? [supplyProductId] : undefined,
470
+ supplierAppId: [coerceNumericId(sourceAppId)],
471
+ }));
472
+ const [productIds, productScope] = this.extractProductShipServiceIds(productPayload, country);
473
+ if (productIds.length) {
474
+ return [
475
+ productIds,
476
+ productScope || "product shipping settings",
477
+ warnings,
478
+ ];
479
+ }
480
+ if (productPayload?.error) {
481
+ warnings.push("DSers product-level shipping settings were unavailable; falling back to the user shipping template.");
482
+ }
483
+ const shippingPayload = await safeCall(() => settings.getProductShippingInfo(this.client, coerceNumericId(sourceAppId)));
484
+ const [shippingIds, shippingScope] = this.extractShippingTemplateServiceIds(shippingPayload, country);
485
+ if (shippingIds.length) {
486
+ return [shippingIds, shippingScope || "shipping template", warnings];
487
+ }
488
+ if (shippingPayload?.error) {
489
+ warnings.push("DSers user shipping template could not be loaded before push.");
490
+ }
491
+ else {
492
+ warnings.push("No DSers shipping template service was found for the selected source app and country.");
493
+ }
494
+ return [[], "", warnings];
495
+ }
496
+ async getPushLogisticsIds(importListId, storeId) {
497
+ const payload = await safeCall(() => product.getPushLogistics(this.client, {
498
+ importListIds: [importListId],
499
+ storeIds: [storeId],
500
+ }));
501
+ if (payload?.error)
502
+ return [];
503
+ const data = payload.data;
504
+ if (!data || typeof data !== "object")
505
+ return [];
506
+ const targetStore = String(storeId);
507
+ const ids = [];
508
+ for (const importPayload of Object.values(data)) {
509
+ if (!importPayload || typeof importPayload !== "object")
510
+ continue;
511
+ for (const storePayload of importPayload.storeLogistics ?? []) {
512
+ if (!storePayload || typeof storePayload !== "object")
513
+ continue;
514
+ const currentStoreId = String(storePayload.storeId ?? "");
515
+ if (currentStoreId && currentStoreId !== targetStore)
516
+ continue;
517
+ for (const item of storePayload.logistics ?? []) {
518
+ if (!item || typeof item !== "object")
519
+ continue;
520
+ const serviceId = firstPresent(item, [
521
+ "logisticId",
522
+ "serviceId",
523
+ "id",
524
+ ]);
525
+ if (serviceId && !ids.includes(String(serviceId))) {
526
+ ids.push(String(serviceId));
527
+ }
528
+ }
529
+ }
530
+ }
531
+ return ids;
532
+ }
533
+ extractProductShipServiceIds(payload, country) {
534
+ const data = payload.data;
535
+ if (!Array.isArray(data))
536
+ return [[], ""];
537
+ const candidates = [];
538
+ for (const item of data) {
539
+ if (!item || typeof item !== "object")
540
+ continue;
541
+ const freightInfo = item.freightInfo;
542
+ if (!Array.isArray(freightInfo))
543
+ continue;
544
+ const countryIds = this.pickFreightServiceIds(freightInfo, country);
545
+ if (countryIds.length) {
546
+ candidates.push([
547
+ 2,
548
+ countryIds,
549
+ country || "product shipping settings",
550
+ ]);
551
+ continue;
552
+ }
553
+ const globalIds = this.pickFreightServiceIds(freightInfo, "GLOBAL");
554
+ if (globalIds.length) {
555
+ candidates.push([
556
+ 1,
557
+ globalIds,
558
+ "Global product shipping settings",
559
+ ]);
560
+ }
561
+ }
562
+ if (!candidates.length)
563
+ return [[], ""];
564
+ candidates.sort((a, b) => b[0] - a[0]);
565
+ return [candidates[0][1], candidates[0][2]];
566
+ }
567
+ extractShippingTemplateServiceIds(payload, country) {
568
+ const data = payload.data;
569
+ if (!data || typeof data !== "object")
570
+ return [[], ""];
571
+ const shippingInfo = data.shippingInfo;
572
+ if (!shippingInfo || typeof shippingInfo !== "object")
573
+ return [[], ""];
574
+ const candidates = [];
575
+ for (const entry of shippingInfo.shippingCountryList ?? []) {
576
+ if (!entry || typeof entry !== "object")
577
+ continue;
578
+ const entryCountry = String(entry.country ?? "").trim();
579
+ let score = 0;
580
+ if (entryCountry.toUpperCase() === country && country)
581
+ score = 2;
582
+ else if (entryCountry.toUpperCase() === "GLOBAL")
583
+ score = 1;
584
+ if (!score)
585
+ continue;
586
+ const serviceIds = this.extractServiceIdsFromCountryEntry(entry);
587
+ if (serviceIds.length) {
588
+ candidates.push([
589
+ score,
590
+ serviceIds,
591
+ entryCountry || "shipping template",
592
+ ]);
593
+ }
594
+ }
595
+ if (!candidates.length)
596
+ return [[], ""];
597
+ candidates.sort((a, b) => b[0] - a[0]);
598
+ return [candidates[0][1], candidates[0][2]];
599
+ }
600
+ pickFreightServiceIds(freightInfo, country) {
601
+ const picked = [];
602
+ for (const item of freightInfo) {
603
+ if (!item || typeof item !== "object")
604
+ continue;
605
+ const shipTo = String(item.shipTo ?? "").trim().toUpperCase();
606
+ if (shipTo !== country)
607
+ continue;
608
+ const serviceId = firstPresent(item, ["serviceId", "logisticId", "id"]);
609
+ if (serviceId && !picked.includes(String(serviceId))) {
610
+ picked.push(String(serviceId));
611
+ }
612
+ }
613
+ return picked;
614
+ }
615
+ extractServiceIdsFromCountryEntry(entry) {
616
+ const serviceIds = [];
617
+ for (const item of entry.list ?? []) {
618
+ const text = String(item ?? "").trim();
619
+ if (text && !serviceIds.includes(text))
620
+ serviceIds.push(text);
621
+ }
622
+ for (const item of entry.logisticsInfo ?? []) {
623
+ if (!item || typeof item !== "object")
624
+ continue;
625
+ const serviceId = firstPresent(item, ["serviceId", "logisticId", "id"]);
626
+ if (serviceId && !serviceIds.includes(String(serviceId))) {
627
+ serviceIds.push(String(serviceId));
628
+ }
629
+ }
630
+ return serviceIds;
631
+ }
632
+ hasReason(payload, reason) {
633
+ if (!payload || typeof payload !== "object")
634
+ return false;
635
+ const detail = String(payload.detail ?? payload.error ?? "");
636
+ return detail.includes(reason);
637
+ }
638
+ async listStores() {
639
+ const payload = await safeCall(() => account.listStores(this.client));
640
+ const storeDicts = extractStoreDicts(payload);
641
+ const stores = [];
642
+ for (const item of storeDicts) {
643
+ const storeRef = firstPresent(item, ["storeId", "id", "sellerStoreId"]);
644
+ if (!storeRef)
645
+ continue;
646
+ const domain = String(firstPresent(item, ["domain"]) ?? "");
647
+ let platform = firstPresent(item, ["platform", "storeType"]);
648
+ if (!platform && domain.includes(".myshopify.com"))
649
+ platform = "shopify";
650
+ stores.push({
651
+ store_ref: String(storeRef),
652
+ display_name: String(firstPresent(item, [
653
+ "sellerName",
654
+ "storeName",
655
+ "name",
656
+ "nickname",
657
+ ]) ?? storeRef),
658
+ platform,
659
+ domain,
660
+ });
661
+ }
662
+ return stores;
663
+ }
664
+ async fetchShopifyProfiles() {
665
+ try {
666
+ const payload = await product.getShopifyShippingProfiles(this.client);
667
+ const data = payload.data;
668
+ if (!Array.isArray(data))
669
+ return {};
670
+ const result = {};
671
+ for (const entry of data) {
672
+ if (entry?.storeId) {
673
+ result[String(entry.storeId)] = entry.profiles ?? [];
674
+ }
675
+ }
676
+ return result;
677
+ }
678
+ catch {
679
+ return {};
680
+ }
681
+ }
682
+ async enrichShopifyProfiles(stores) {
683
+ const hasShopify = stores.some((s) => (s.domain ?? "").includes(".myshopify.com") ||
684
+ String(s.platform ?? "").toLowerCase() === "shopify");
685
+ if (!hasShopify)
686
+ return stores;
687
+ const profilesByStore = await this.fetchShopifyProfiles();
688
+ if (!Object.keys(profilesByStore).length)
689
+ return stores;
690
+ return stores.map((s) => {
691
+ const isShopify = (s.domain ?? "").includes(".myshopify.com") ||
692
+ String(s.platform ?? "").toLowerCase() === "shopify";
693
+ if (!isShopify)
694
+ return s;
695
+ const rawProfiles = profilesByStore[s.store_ref] ?? [];
696
+ const readable = rawProfiles.map((p) => {
697
+ const groups = p.profileGroups ?? [];
698
+ const firstGroup = groups[0] ?? {};
699
+ return {
700
+ name: p.name ?? "",
701
+ is_default: Boolean(p.isChecked),
702
+ countries: Number(firstGroup.countryCount ?? 0),
703
+ rate: firstGroup.rate ?? "",
704
+ currency: firstGroup.currency ?? "",
705
+ };
706
+ });
707
+ return { ...s, shipping_profiles: readable };
708
+ });
709
+ }
710
+ async resolveStore(targetStore) {
711
+ const stores = await this.listStores();
712
+ if (!stores.length)
713
+ throw new Error("No linked stores found. Connect a Shopify store in DSers before pushing products.");
714
+ const storeNames = stores
715
+ .map((s) => `${s.display_name} (${s.store_ref})`)
716
+ .join(", ");
717
+ if (!targetStore) {
718
+ if (stores.length === 1)
719
+ return stores[0];
720
+ throw new Error(`Multiple stores are available: ${storeNames}. Provide target_store with the store_ref or display_name from dsers.store.discover.`);
721
+ }
722
+ const target = targetStore.trim().toLowerCase();
723
+ for (const store of stores) {
724
+ if (store.store_ref.toLowerCase() === target)
725
+ return store;
726
+ if (String(store.display_name ?? "").trim().toLowerCase() === target)
727
+ return store;
728
+ }
729
+ throw new Error(`Unknown target_store '${targetStore}'. Available stores: ${storeNames}. Use the store_ref or display_name from dsers.store.discover.`);
730
+ }
731
+ extractSupplyProductId(payload) {
732
+ return String(this.findFirstValueByKeys(payload, [
733
+ "supplyProductId",
734
+ "productId",
735
+ "itemId",
736
+ "id",
737
+ ]) ?? "").trim();
738
+ }
739
+ // ── URL & ID parsing ──
740
+ extractAfTraceId(sourceUrl) {
741
+ try {
742
+ const url = new URL(sourceUrl);
743
+ const trace = url.searchParams.get("afTraceInfo") ?? "";
744
+ const m = /^(\d{10,})/.exec(trace);
745
+ return m ? m[1] : "";
746
+ }
747
+ catch {
748
+ return "";
749
+ }
750
+ }
751
+ cleanProductUrl(sourceUrl) {
752
+ try {
753
+ const url = new URL(sourceUrl);
754
+ return `${url.origin}${url.pathname}`;
755
+ }
756
+ catch {
757
+ return sourceUrl;
758
+ }
759
+ }
760
+ resolveSourceIdentifier(sourceUrl) {
761
+ sourceUrl = sourceUrl ?? "";
762
+ let match = ALIEXPRESS_ID_PATTERN.exec(sourceUrl);
763
+ if (match)
764
+ return ["aliexpress", this.aliexpressAppId, match[1]];
765
+ match = ALIBABA_ID_PATTERN.exec(sourceUrl);
766
+ if (match)
767
+ return ["alibaba", this.alibabaAppId, match[1]];
768
+ match = ALI1688_ID_PATTERN.exec(sourceUrl);
769
+ if (match)
770
+ return ["1688", this.alibabaAppId, match[1]];
771
+ return ["unknown", this.aliexpressAppId, ""];
772
+ }
773
+ extractImportItemId(payload) {
774
+ const candidates = findAllValuesByKeys(payload, [
775
+ "importListId",
776
+ "id",
777
+ ]);
778
+ for (const value of candidates) {
779
+ const text = String(value).trim();
780
+ if (text)
781
+ return text;
782
+ }
783
+ return "";
784
+ }
785
+ async recoverImportItemId(searchIds) {
786
+ if (searchIds.size === 0)
787
+ return "";
788
+ for (let page = 1; page <= 5; page++) {
789
+ const listing = await product.getImportList(this.client, {
790
+ page,
791
+ pageSize: 100,
792
+ });
793
+ const items = extractImportItems(listing);
794
+ for (const item of items) {
795
+ const itemId = firstPresent(item, ["id", "importListId"]);
796
+ if (!itemId)
797
+ continue;
798
+ const haystack = JSON.stringify(item);
799
+ for (const needle of searchIds) {
800
+ if (haystack.includes(needle))
801
+ return String(itemId);
802
+ }
803
+ }
804
+ if (items.length < 100)
805
+ break;
806
+ }
807
+ return "";
808
+ }
809
+ // ── Draft normalisation & de-normalisation ──
810
+ normalizeImportItem(payload) {
811
+ const item = extractImportItem(payload);
812
+ const warnings = [];
813
+ const titleKey = firstMatchingKey(item, [
814
+ "title",
815
+ "productTitle",
816
+ "name",
817
+ ]);
818
+ const descriptionKey = firstMatchingKey(item, [
819
+ "description",
820
+ "descriptionHtml",
821
+ "desc",
822
+ ]);
823
+ const rawTagsKey = firstMatchingKey(item, ["tags", "tagList"]);
824
+ const tagsKey = null;
825
+ const [imagesKey, images, imagesMode] = extractImages(item);
826
+ const [variantsKey, variants] = extractVariants(item);
827
+ const mainImageKey = firstMatchingKey(item, [
828
+ "mainImgUrl",
829
+ "mainImageUrl",
830
+ ]);
831
+ const priceEditFlagKey = firstMatchingKey(item, ["isPriceEdited"]);
832
+ const minPriceKey = firstMatchingKey(item, ["minPrice"]);
833
+ const maxPriceKey = firstMatchingKey(item, ["maxPrice"]);
834
+ const supplyKey = firstMatchingKey(item, ["supply"]);
835
+ if (!titleKey)
836
+ warnings.push("Could not detect a title field in the imported product. Using an empty title fallback.");
837
+ if (!imagesKey)
838
+ warnings.push("Could not detect a top-level images field. Image edits may be limited.");
839
+ if (!variantsKey)
840
+ warnings.push("Could not detect a variants field. Pricing rule edits may be limited.");
841
+ if (rawTagsKey)
842
+ warnings.push("Tag edits are preview-only because the DSers import list API does not support direct tag writes for this item type.");
843
+ const draft = {
844
+ title: String(item[titleKey] ?? ""),
845
+ description_html: String(item[descriptionKey] ?? ""),
846
+ images,
847
+ tags: rawTagsKey ? [...(item[rawTagsKey] ?? [])] : [],
848
+ variants,
849
+ };
850
+ const fieldMap = {
851
+ title_key: titleKey,
852
+ description_key: descriptionKey,
853
+ tags_key: tagsKey,
854
+ images_key: imagesKey,
855
+ images_mode: imagesMode,
856
+ main_image_key: mainImageKey,
857
+ raw_images: imagesKey ? structuredClone(item[imagesKey] ?? []) : [],
858
+ variants_key: variantsKey,
859
+ raw_variants: variantsKey
860
+ ? structuredClone(item[variantsKey] ?? [])
861
+ : [],
862
+ price_edit_flag_key: priceEditFlagKey,
863
+ min_price_key: minPriceKey,
864
+ max_price_key: maxPriceKey,
865
+ supply_key: supplyKey,
866
+ raw_supply: supplyKey ? structuredClone(item[supplyKey] ?? {}) : {},
867
+ variant_ref_key: "variant_ref",
868
+ };
869
+ return [draft, fieldMap, warnings];
870
+ }
871
+ denormalizeVariants(normalizedVariants, fieldMap) {
872
+ const rawVariants = structuredClone(fieldMap.raw_variants ?? []);
873
+ if (!rawVariants.length)
874
+ return normalizedVariants;
875
+ for (let idx = 0; idx < normalizedVariants.length; idx++) {
876
+ if (idx >= rawVariants.length)
877
+ break;
878
+ const normalized = normalizedVariants[idx];
879
+ const rawVariant = rawVariants[idx];
880
+ const offerKey = firstMatchingKey(rawVariant, [
881
+ "sellPrice",
882
+ "salePrice",
883
+ "price",
884
+ ]);
885
+ const supplierKey = firstMatchingKey(rawVariant, [
886
+ "supplierPrice",
887
+ "buyPrice",
888
+ "cost",
889
+ ]);
890
+ const ttlKey = firstMatchingKey(rawVariant, [
891
+ "title",
892
+ "name",
893
+ "skuTitle",
894
+ ]);
895
+ const skuKey = firstMatchingKey(rawVariant, [
896
+ "sku",
897
+ "sellerSku",
898
+ "itemSku",
899
+ "skuCode",
900
+ ]);
901
+ const imageKey = firstMatchingKey(rawVariant, [
902
+ "imageUrl",
903
+ "image",
904
+ "imgUrl",
905
+ ]);
906
+ if (offerKey)
907
+ rawVariant[offerKey] = coerceLike(rawVariant[offerKey], normalized.offer_price);
908
+ if (supplierKey && normalized.supplier_price != null)
909
+ rawVariant[supplierKey] = coerceLike(rawVariant[supplierKey], normalized.supplier_price);
910
+ if (ttlKey)
911
+ rawVariant[ttlKey] = normalized.title;
912
+ if (skuKey)
913
+ rawVariant[skuKey] = normalized.sku;
914
+ if (imageKey && normalized.image_url)
915
+ rawVariant[imageKey] = normalized.image_url;
916
+ }
917
+ return rawVariants;
918
+ }
919
+ denormalizeSupply(normalizedVariants, fieldMap) {
920
+ const rawSupply = structuredClone(fieldMap.raw_supply ?? {});
921
+ if (typeof rawSupply !== "object")
922
+ return {};
923
+ const variantsByRef = {};
924
+ for (const item of normalizedVariants) {
925
+ const ref = String(item[fieldMap.variant_ref_key ?? "variant_ref"] ?? "");
926
+ if (ref)
927
+ variantsByRef[ref] = item;
928
+ }
929
+ for (const [supplyRef, rawEntry] of Object.entries(rawSupply)) {
930
+ if (!rawEntry || typeof rawEntry !== "object")
931
+ continue;
932
+ const normalized = variantsByRef[supplyRef];
933
+ if (!normalized)
934
+ continue;
935
+ const offerKey = firstMatchingKey(rawEntry, [
936
+ "sellPrice",
937
+ "salePrice",
938
+ "price",
939
+ ]);
940
+ const supplierKey = firstMatchingKey(rawEntry, [
941
+ "supplierPrice",
942
+ "buyPrice",
943
+ "cost",
944
+ ]);
945
+ const compareKey = firstMatchingKey(rawEntry, [
946
+ "compareAtPrice",
947
+ ]);
948
+ if (offerKey)
949
+ rawEntry[offerKey] = coerceLike(rawEntry[offerKey], normalized.offer_price);
950
+ if (supplierKey && normalized.supplier_price != null)
951
+ rawEntry[supplierKey] = coerceLike(rawEntry[supplierKey], normalized.supplier_price);
952
+ if (compareKey && normalized.offer_price != null)
953
+ rawEntry[compareKey] = coerceLike(rawEntry[compareKey], normalized.offer_price);
954
+ }
955
+ return rawSupply;
956
+ }
957
+ denormalizeImages(normalizedImages, fieldMap) {
958
+ const rawImages = structuredClone(fieldMap.raw_images ?? []);
959
+ if (!rawImages.length)
960
+ return normalizedImages;
961
+ if (fieldMap.images_mode === "string_list") {
962
+ return normalizedImages.filter((item) => item);
963
+ }
964
+ if (fieldMap.images_mode !== "dict_list")
965
+ return rawImages;
966
+ const result = [];
967
+ for (let idx = 0; idx < normalizedImages.length; idx++) {
968
+ const url = normalizedImages[idx];
969
+ if (!url)
970
+ continue;
971
+ const template = idx < rawImages.length && typeof rawImages[idx] === "object"
972
+ ? rawImages[idx]
973
+ : {};
974
+ const entry = { ...template };
975
+ const imgKey = firstMatchingKey(entry, [
976
+ "url",
977
+ "imageUrl",
978
+ "src",
979
+ "originUrl",
980
+ "imgUrl",
981
+ ]);
982
+ if (imgKey) {
983
+ entry[imgKey] = url;
984
+ }
985
+ else if (Object.keys(entry).length) {
986
+ const firstKey = Object.keys(entry)[0];
987
+ entry[firstKey] = url;
988
+ }
989
+ else {
990
+ result.push({ url });
991
+ continue;
992
+ }
993
+ result.push(entry);
994
+ }
995
+ return result;
996
+ }
997
+ computePriceBounds(normalizedVariants) {
998
+ const prices = normalizedVariants
999
+ .map((v) => asFloat(v.offer_price))
1000
+ .filter((p) => p != null);
1001
+ if (!prices.length)
1002
+ return [null, null];
1003
+ return [formatScalar(Math.min(...prices)), formatScalar(Math.max(...prices))];
1004
+ }
1005
+ // ── Push status parsing ──
1006
+ extractPushState(payload) {
1007
+ const state = this.findFirstValueByKeys(payload, [
1008
+ "status",
1009
+ "state",
1010
+ "result",
1011
+ ]);
1012
+ const mapped = {
1013
+ "0": "requested",
1014
+ "1": "requested",
1015
+ "4": "failed",
1016
+ "5": "completed",
1017
+ };
1018
+ return mapped[String(state)] ?? String(state ?? "requested");
1019
+ }
1020
+ extractPushError(payload) {
1021
+ const message = this.findFirstValueByKeys(payload, [
1022
+ "errmsg",
1023
+ "message",
1024
+ "detail",
1025
+ "error",
1026
+ ]);
1027
+ const reason = this.findFirstValueByKeys(payload, ["reason"]);
1028
+ const pieces = [message, reason]
1029
+ .filter((p) => p != null && p !== "")
1030
+ .map((p) => String(p).trim());
1031
+ return [...new Set(pieces)].join(" | ");
1032
+ }
1033
+ // ── Generic JSON traversal helpers ──
1034
+ findFirstValueByKeys(node, keys) {
1035
+ return findFirstValueByKeys(node, keys);
1036
+ }
1037
+ async fetchImportItem(importItemId) {
1038
+ const payload = await safeCall(() => product.getImportListItem(this.client, importItemId));
1039
+ this.raiseIfError(payload, "Could not re-fetch the import list item for job recovery. The item may have been deleted.");
1040
+ return payload;
1041
+ }
1042
+ normalizeForRecovery(itemPayload) {
1043
+ const [draft, fieldMap] = this.normalizeImportItem(itemPayload);
1044
+ return [draft, fieldMap];
1045
+ }
1046
+ }
1047
+ // ── Standalone utility functions ──
1048
+ function coerceNumericId(value) {
1049
+ const text = String(value ?? "").trim();
1050
+ if (!/^-?\d+$/.test(text))
1051
+ return value;
1052
+ const num = Number(text);
1053
+ return Number.isSafeInteger(num) ? num : text;
1054
+ }
1055
+ function firstPresent(node, keys) {
1056
+ for (const key of keys) {
1057
+ const value = node[key];
1058
+ if (value != null && value !== "")
1059
+ return value;
1060
+ }
1061
+ return null;
1062
+ }
1063
+ function firstMatchingKey(node, keys) {
1064
+ for (const key of keys) {
1065
+ if (key in node)
1066
+ return key;
1067
+ }
1068
+ return null;
1069
+ }
1070
+ function findFirstValueByKeys(node, keys) {
1071
+ if (node && typeof node === "object" && !Array.isArray(node)) {
1072
+ for (const key of keys) {
1073
+ if (key in node && node[key] != null && node[key] !== "")
1074
+ return node[key];
1075
+ }
1076
+ for (const value of Object.values(node)) {
1077
+ const found = findFirstValueByKeys(value, keys);
1078
+ if (found != null && found !== "")
1079
+ return found;
1080
+ }
1081
+ }
1082
+ else if (Array.isArray(node)) {
1083
+ for (const value of node) {
1084
+ const found = findFirstValueByKeys(value, keys);
1085
+ if (found != null && found !== "")
1086
+ return found;
1087
+ }
1088
+ }
1089
+ return null;
1090
+ }
1091
+ function findAllValuesByKeys(node, keys) {
1092
+ const found = [];
1093
+ if (node && typeof node === "object" && !Array.isArray(node)) {
1094
+ for (const key of keys) {
1095
+ if (key in node && node[key] != null && node[key] !== "")
1096
+ found.push(node[key]);
1097
+ }
1098
+ for (const value of Object.values(node)) {
1099
+ found.push(...findAllValuesByKeys(value, keys));
1100
+ }
1101
+ }
1102
+ else if (Array.isArray(node)) {
1103
+ for (const value of node) {
1104
+ found.push(...findAllValuesByKeys(value, keys));
1105
+ }
1106
+ }
1107
+ return found;
1108
+ }
1109
+ function findListCandidates(node, prefix = "root") {
1110
+ const results = [];
1111
+ if (node && typeof node === "object" && !Array.isArray(node)) {
1112
+ for (const [key, value] of Object.entries(node)) {
1113
+ const childPrefix = `${prefix}.${key}`;
1114
+ if (Array.isArray(value) &&
1115
+ value.length &&
1116
+ value.every((item) => item && typeof item === "object")) {
1117
+ results.push([childPrefix, value]);
1118
+ }
1119
+ results.push(...findListCandidates(value, childPrefix));
1120
+ }
1121
+ }
1122
+ else if (Array.isArray(node)) {
1123
+ for (let idx = 0; idx < node.length; idx++) {
1124
+ results.push(...findListCandidates(node[idx], `${prefix}[${idx}]`));
1125
+ }
1126
+ }
1127
+ return results;
1128
+ }
1129
+ function extractStoreDicts(payload) {
1130
+ const candidates = findListCandidates(payload);
1131
+ const scored = [];
1132
+ for (const [key, items] of candidates) {
1133
+ if (!items.length)
1134
+ continue;
1135
+ let score = 0;
1136
+ if (key.toLowerCase().includes("store"))
1137
+ score += 2;
1138
+ if (firstPresent(items[0], ["storeId", "id"]))
1139
+ score += 1;
1140
+ scored.push([score, items]);
1141
+ }
1142
+ if (!scored.length)
1143
+ return [];
1144
+ scored.sort((a, b) => b[0] - a[0]);
1145
+ return scored[0][1];
1146
+ }
1147
+ function extractImportItems(payload) {
1148
+ const candidates = findListCandidates(payload);
1149
+ const scored = [];
1150
+ for (const [key, items] of candidates) {
1151
+ if (!items.length)
1152
+ continue;
1153
+ let score = 0;
1154
+ if (key.toLowerCase().includes("import"))
1155
+ score += 2;
1156
+ if (firstPresent(items[0], ["id", "importListId"]))
1157
+ score += 1;
1158
+ scored.push([score, items]);
1159
+ }
1160
+ if (!scored.length)
1161
+ return [];
1162
+ scored.sort((a, b) => b[0] - a[0]);
1163
+ return scored[0][1];
1164
+ }
1165
+ function extractImportItem(payload) {
1166
+ const data = payload.data ?? payload;
1167
+ if (data && typeof data === "object" && !Array.isArray(data))
1168
+ return data;
1169
+ if (Array.isArray(data) && data.length && typeof data[0] === "object")
1170
+ return data[0];
1171
+ if (payload && typeof payload === "object")
1172
+ return payload;
1173
+ throw new Error("Could not parse the imported product data. The API response format may have changed.");
1174
+ }
1175
+ function extractImages(item) {
1176
+ for (const key of [
1177
+ "medias",
1178
+ "images",
1179
+ "productImages",
1180
+ "imageList",
1181
+ "mainImages",
1182
+ ]) {
1183
+ const value = item[key];
1184
+ if (!Array.isArray(value))
1185
+ continue;
1186
+ if (!value.length)
1187
+ return [key, [], "string_list"];
1188
+ if (value.every((entry) => typeof entry === "string")) {
1189
+ return [key, value.filter((e) => e), "string_list"];
1190
+ }
1191
+ const urls = [];
1192
+ for (const entry of value) {
1193
+ if (entry && typeof entry === "object") {
1194
+ const url = firstPresent(entry, [
1195
+ "url",
1196
+ "imageUrl",
1197
+ "src",
1198
+ "originUrl",
1199
+ "imgUrl",
1200
+ ]);
1201
+ if (url)
1202
+ urls.push(String(url));
1203
+ }
1204
+ }
1205
+ if (urls.length)
1206
+ return [key, urls, "dict_list"];
1207
+ }
1208
+ return [null, variantImages(item), "unknown"];
1209
+ }
1210
+ function extractVariants(item) {
1211
+ for (const key of [
1212
+ "variants",
1213
+ "skuList",
1214
+ "variantList",
1215
+ "productSkuList",
1216
+ ]) {
1217
+ const value = item[key];
1218
+ if (!Array.isArray(value))
1219
+ continue;
1220
+ const normalized = [];
1221
+ for (let idx = 0; idx < value.length; idx++) {
1222
+ const raw = value[idx];
1223
+ if (!raw || typeof raw !== "object")
1224
+ continue;
1225
+ const variantRef = firstPresent(raw, ["id", "variantId", "skuId", "sellerSku"]) ??
1226
+ `variant-${idx}`;
1227
+ normalized.push({
1228
+ variant_ref: String(variantRef),
1229
+ title: String(firstPresent(raw, ["title", "name", "skuTitle", "skuAttr"]) ??
1230
+ `Variant ${idx + 1}`),
1231
+ supplier_price: asFloat(firstPresent(raw, [
1232
+ "supplierPrice",
1233
+ "buyPrice",
1234
+ "cost",
1235
+ "price",
1236
+ ])),
1237
+ offer_price: asFloat(firstPresent(raw, ["sellPrice", "salePrice", "price"])),
1238
+ sku: String(firstPresent(raw, [
1239
+ "sku",
1240
+ "sellerSku",
1241
+ "itemSku",
1242
+ "skuCode",
1243
+ ]) ?? ""),
1244
+ image_url: String(firstPresent(raw, ["imageUrl", "image", "imgUrl"]) ?? ""),
1245
+ });
1246
+ }
1247
+ return [key, normalized];
1248
+ }
1249
+ return [null, []];
1250
+ }
1251
+ function variantImages(item) {
1252
+ const [, variants] = extractVariants(item);
1253
+ const seen = [];
1254
+ for (const variant of variants) {
1255
+ const url = variant.image_url;
1256
+ if (url && !seen.includes(url))
1257
+ seen.push(url);
1258
+ }
1259
+ return seen;
1260
+ }
1261
+ function extractProfileGids(profile, storeId) {
1262
+ const profileId = profile.id ?? "";
1263
+ const groups = profile.profileGroups ?? [];
1264
+ const locationId = groups.length ? groups[0].id ?? "" : "";
1265
+ if (profileId && locationId)
1266
+ return [{ storeId, locationId, profileId }];
1267
+ return null;
1268
+ }
1269
+ function asFloat(value) {
1270
+ if (value == null || value === "")
1271
+ return null;
1272
+ const n = Number(value);
1273
+ return isNaN(n) ? null : n;
1274
+ }
1275
+ function coerceLike(original, value) {
1276
+ if (value == null)
1277
+ return value;
1278
+ if (typeof original === "string")
1279
+ return formatScalar(value);
1280
+ return value;
1281
+ }
1282
+ function formatScalar(value) {
1283
+ const n = Number(value);
1284
+ if (isNaN(n))
1285
+ return String(value);
1286
+ if (Number.isInteger(n))
1287
+ return String(n);
1288
+ let text = n.toFixed(2);
1289
+ text = text.replace(/0+$/, "").replace(/\.$/, "");
1290
+ return text;
1291
+ }
1292
+ function sleep(ms) {
1293
+ return new Promise((resolve) => setTimeout(resolve, ms));
1294
+ }
1295
+ export function buildProvider(config) {
1296
+ return new PrivateDsersProvider(config);
1297
+ }
1298
+ //# sourceMappingURL=provider.js.map