@thorprovider/adapters 4.2.0 → 4.3.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 (56) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/PROVIDER_IMPLEMENTATION_ANALYSIS.md +659 -0
  3. package/README.md +19 -0
  4. package/dist/index.d.mts +249 -2
  5. package/dist/index.d.ts +249 -2
  6. package/dist/index.js +1450 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +1448 -2
  9. package/dist/index.mjs.map +1 -1
  10. package/docs/RETRY_SOLUTION_SUMMARY.md +26 -11
  11. package/docs/authentication.md +115 -62
  12. package/docs/capabilities-system.md +34 -2
  13. package/docs/category-customization.md +37 -29
  14. package/docs/fetch-reliability-solution.md +127 -278
  15. package/docs/getting-started.md +43 -29
  16. package/docs/profile-management.md +68 -38
  17. package/docs/providers-overview.md +14 -0
  18. package/docs/services-overview.md +20 -2
  19. package/package.json +10 -6
  20. package/quick-reference.md +34 -0
  21. package/src/factory/index.ts +4 -0
  22. package/src/index.ts +3 -0
  23. package/src/providers/index.ts +3 -0
  24. package/src/providers/medusa/admin.test.ts +404 -0
  25. package/src/providers/medusa/admin.ts +442 -0
  26. package/src/providers/medusa/auth.test.ts +137 -0
  27. package/src/providers/medusa/auth.ts +96 -1
  28. package/src/providers/medusa/index.ts +21 -1
  29. package/src/providers/shopify/auth.ts +6 -0
  30. package/src/providers/stelorder/client.test-helpers.ts +3 -0
  31. package/src/providers/stelorder/client.test.ts +58 -0
  32. package/src/providers/stelorder/client.ts +326 -0
  33. package/src/providers/stelorder/index.test.ts +48 -0
  34. package/src/providers/stelorder/index.ts +685 -0
  35. package/src/providers/stelorder/live-smoke.test.ts +54 -0
  36. package/src/providers/stelorder/transforms.test.ts +103 -0
  37. package/src/providers/stelorder/transforms.ts +631 -0
  38. package/src/resilience/README.md +70 -48
  39. package/src/types/capabilities.types.ts +51 -0
  40. package/vitest.config.ts +13 -0
  41. package/vitest.setup.ts +6 -0
  42. package/.turbo/turbo-build.log +0 -18
  43. package/dist/.tsbuildinfo +0 -1
  44. package/src/core/errors.js +0 -161
  45. package/src/core/fetch-utils.js +0 -240
  46. package/src/core/logger.js +0 -63
  47. package/src/metadata/channel-scope-validation.js +0 -136
  48. package/src/metadata/channel-scope.js +0 -174
  49. package/src/metadata/index.js +0 -31
  50. package/src/providers/medusa/auth.js +0 -601
  51. package/src/providers/medusa/index.js +0 -2485
  52. package/src/providers/medusa/payment.js +0 -265
  53. package/src/providers/medusa/transforms.js +0 -505
  54. package/src/types/capabilities.types.js +0 -141
  55. package/src/types/provider.js +0 -9
  56. package/src/utils/payment-utils.js +0 -152
package/dist/index.mjs CHANGED
@@ -71,6 +71,41 @@ var MEDUSA_CAPABILITIES = {
71
71
  loyaltyProgram: false
72
72
  // Requires custom implementation
73
73
  };
74
+ var STELORDER_CAPABILITIES = {
75
+ // Profile Management
76
+ updateProfile: false,
77
+ updateEmail: false,
78
+ changePasswordDirect: false,
79
+ changePasswordViaEmail: false,
80
+ deleteAccount: false,
81
+ // Avatar
82
+ uploadAvatar: false,
83
+ avatarViaMetadata: false,
84
+ // Custom Attributes
85
+ customAttributes: false,
86
+ // Orders
87
+ fetchOrders: true,
88
+ cancelOrder: false,
89
+ // Addresses
90
+ manageAddresses: true,
91
+ defaultAddress: false,
92
+ // Cart/Checkout
93
+ applyDiscounts: false,
94
+ shippingCalculation: false,
95
+ guestCheckout: false,
96
+ savedPaymentMethods: false,
97
+ listPaymentProviders: true,
98
+ regionSpecificPayments: false,
99
+ // Wishlist
100
+ wishlist: false,
101
+ // Search
102
+ advancedSearch: false,
103
+ facetedFilters: false,
104
+ // Reviews
105
+ productReviews: false,
106
+ // Loyalty
107
+ loyaltyProgram: false
108
+ };
74
109
  var SHOPIFY_CAPABILITIES = {
75
110
  // Profile Management
76
111
  updateProfile: true,
@@ -1694,11 +1729,17 @@ var MedusaAuthProvider = class {
1694
1729
  logger;
1695
1730
  authConfig;
1696
1731
  storageManager;
1697
- constructor(sdk, authConfig, debug = false) {
1732
+ /** Medusa backend base URL — needed for raw fetch calls */
1733
+ baseUrl;
1734
+ /** Publishable API key — sent as x-publishable-api-key on store calls */
1735
+ publishableKey;
1736
+ constructor(sdk, authConfig, debug = false, baseUrl = "", publishableKey = "") {
1698
1737
  this.sdk = sdk;
1699
1738
  this.authConfig = authConfig;
1700
1739
  this.logger = createLogger("MedusaAuthProvider", debug);
1701
1740
  this.storageManager = new AuthStorageManager(authConfig);
1741
+ this.baseUrl = baseUrl;
1742
+ this.publishableKey = publishableKey;
1702
1743
  this.logger.info("Medusa Auth Provider initialized", {
1703
1744
  method: authConfig.method,
1704
1745
  storage: authConfig.storage
@@ -1974,6 +2015,81 @@ var MedusaAuthProvider = class {
1974
2015
  void 0
1975
2016
  );
1976
2017
  }
2018
+ // ========================================
2019
+ // Multi-Tenant — Channel Link
2020
+ // ========================================
2021
+ /**
2022
+ * Link the currently authenticated customer to the storefront's sales channel.
2023
+ *
2024
+ * Must be called immediately after a successful `login()` so the customer
2025
+ * appears in the admin panel of the correct storefront channel.
2026
+ *
2027
+ * Implements `POST /store/thor/customers/me/channels`.
2028
+ * Idempotent — returns `{ linked: false, reason: 'already_linked' }` when
2029
+ * the link already exists.
2030
+ *
2031
+ * @throws {ProviderAPIError} When not authenticated (401) or channel cannot
2032
+ * be resolved from the publishable key (400).
2033
+ */
2034
+ async linkCustomerToChannel() {
2035
+ this.logger.debug("linkCustomerToChannel");
2036
+ if (!this.baseUrl || !this.publishableKey) {
2037
+ throw new ProviderAPIError(
2038
+ "linkCustomerToChannel requires baseUrl and publishableKey to be set on the auth provider.",
2039
+ "CONFIGURATION_ERROR",
2040
+ void 0
2041
+ );
2042
+ }
2043
+ const token = await this.storageManager.getToken();
2044
+ const headers = {
2045
+ "x-publishable-api-key": this.publishableKey,
2046
+ "Content-Type": "application/json"
2047
+ };
2048
+ if (token) {
2049
+ headers["Authorization"] = `Bearer ${token}`;
2050
+ }
2051
+ try {
2052
+ const url = `${this.baseUrl}/store/thor/customers/me/channels`;
2053
+ const response = await fetch(url, {
2054
+ method: "POST",
2055
+ headers
2056
+ });
2057
+ if (response.status === 401) {
2058
+ throw new ProviderAPIError(
2059
+ "linkCustomerToChannel: not authenticated. Call login() first.",
2060
+ "NOT_AUTHENTICATED",
2061
+ 401
2062
+ );
2063
+ }
2064
+ if (response.status === 400) {
2065
+ const body = await response.json().catch(() => ({}));
2066
+ throw new ProviderAPIError(
2067
+ `linkCustomerToChannel: could not resolve sales channel from publishable key. ${body?.message ?? ""}`.trim(),
2068
+ "CHANNEL_RESOLUTION_FAILED",
2069
+ 400
2070
+ );
2071
+ }
2072
+ if (!response.ok) {
2073
+ const body = await response.json().catch(() => ({}));
2074
+ throw new ProviderAPIError(
2075
+ `linkCustomerToChannel failed with status ${response.status}: ${body?.message ?? "Unknown error"}`,
2076
+ "LINK_CHANNEL_FAILED",
2077
+ response.status
2078
+ );
2079
+ }
2080
+ const data = await response.json();
2081
+ this.logger.info("Customer linked to channel", { linked: data.linked, reason: data.reason });
2082
+ return data;
2083
+ } catch (error) {
2084
+ if (error instanceof ProviderAPIError) throw error;
2085
+ this.logger.error("linkCustomerToChannel failed", error);
2086
+ throw new ProviderAPIError(
2087
+ `linkCustomerToChannel failed: ${error.message}`,
2088
+ "LINK_CHANNEL_FAILED",
2089
+ error
2090
+ );
2091
+ }
2092
+ }
1977
2093
  };
1978
2094
 
1979
2095
  // src/providers/medusa/payment.ts
@@ -2116,6 +2232,290 @@ async function getPaymentMethods(sdk, options) {
2116
2232
  }
2117
2233
  }
2118
2234
 
2235
+ // src/providers/medusa/admin.ts
2236
+ var MedusaAdminClient = class {
2237
+ baseUrl;
2238
+ apiKey;
2239
+ logger;
2240
+ constructor(config) {
2241
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
2242
+ this.apiKey = config.apiKey;
2243
+ this.logger = createLogger("MedusaAdminClient", config.debug);
2244
+ }
2245
+ // -----------------------------------------------------------------------
2246
+ // Private — HTTP helper
2247
+ // -----------------------------------------------------------------------
2248
+ async request(method, path, body) {
2249
+ const url = `${this.baseUrl}${path}`;
2250
+ this.logger.debug(`[admin] ${method} ${path}`);
2251
+ const headers = {
2252
+ "x-medusa-access-token": this.apiKey,
2253
+ "Content-Type": "application/json"
2254
+ };
2255
+ const init = {
2256
+ method,
2257
+ headers,
2258
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
2259
+ };
2260
+ let response;
2261
+ try {
2262
+ response = await fetch(url, init);
2263
+ } catch (networkError) {
2264
+ throw new ProviderAPIError(
2265
+ `[admin] Network error on ${method} ${path}: ${networkError.message}`,
2266
+ "NETWORK_ERROR",
2267
+ networkError
2268
+ );
2269
+ }
2270
+ if (!response.ok) {
2271
+ let message = `HTTP ${response.status}`;
2272
+ try {
2273
+ const json = await response.json();
2274
+ message = json?.message ?? message;
2275
+ } catch {
2276
+ }
2277
+ throw new ProviderAPIError(
2278
+ `[admin] ${method} ${path} failed: ${message}`,
2279
+ `ADMIN_API_${response.status}`,
2280
+ response.status
2281
+ );
2282
+ }
2283
+ return response.json();
2284
+ }
2285
+ // -----------------------------------------------------------------------
2286
+ // 3.1 — Sales Channel: Customers
2287
+ // -----------------------------------------------------------------------
2288
+ /**
2289
+ * Paginated list of customers linked to a sales channel.
2290
+ *
2291
+ * `GET /admin/thor/sales-channels/:id/customers`
2292
+ */
2293
+ async getChannelCustomers(salesChannelId, options = {}) {
2294
+ const { limit = 20, offset = 0 } = options;
2295
+ return this.request(
2296
+ "GET",
2297
+ `/admin/thor/sales-channels/${salesChannelId}/customers?limit=${limit}&offset=${offset}`
2298
+ );
2299
+ }
2300
+ // -----------------------------------------------------------------------
2301
+ // 3.2 — Sales Channel: API Keys
2302
+ // -----------------------------------------------------------------------
2303
+ /**
2304
+ * Publishable API keys associated with a sales channel.
2305
+ *
2306
+ * `GET /admin/thor/sales-channels/:id/api-keys`
2307
+ */
2308
+ async getChannelApiKeys(salesChannelId) {
2309
+ return this.request(
2310
+ "GET",
2311
+ `/admin/thor/sales-channels/${salesChannelId}/api-keys`
2312
+ );
2313
+ }
2314
+ // -----------------------------------------------------------------------
2315
+ // 3.3 — Sales Channel: Categories
2316
+ // -----------------------------------------------------------------------
2317
+ /**
2318
+ * Product categories assigned to a sales channel.
2319
+ *
2320
+ * `GET /admin/thor/sales-channels/:id/categories`
2321
+ */
2322
+ async getChannelCategories(salesChannelId, options = {}) {
2323
+ const { limit = 20, offset = 0, include_descendants = false } = options;
2324
+ const qs = new URLSearchParams({
2325
+ limit: String(limit),
2326
+ offset: String(offset),
2327
+ include_descendants: String(include_descendants)
2328
+ });
2329
+ return this.request(
2330
+ "GET",
2331
+ `/admin/thor/sales-channels/${salesChannelId}/categories?${qs}`
2332
+ );
2333
+ }
2334
+ // -----------------------------------------------------------------------
2335
+ // 3.4 — Category: Sales Channels
2336
+ // -----------------------------------------------------------------------
2337
+ /**
2338
+ * Sales channels to which a category is assigned.
2339
+ *
2340
+ * `GET /admin/thor/categories/:id/sales-channels`
2341
+ */
2342
+ async getCategorySalesChannels(categoryId) {
2343
+ return this.request(
2344
+ "GET",
2345
+ `/admin/thor/categories/${categoryId}/sales-channels`
2346
+ );
2347
+ }
2348
+ // -----------------------------------------------------------------------
2349
+ // 3.5 — Category: Assign to Sales Channels
2350
+ // -----------------------------------------------------------------------
2351
+ /**
2352
+ * Assign a category to one or more sales channels (idempotent).
2353
+ *
2354
+ * `POST /admin/thor/categories/:id/sales-channels`
2355
+ */
2356
+ async assignCategoryToChannels(categoryId, body) {
2357
+ return this.request(
2358
+ "POST",
2359
+ `/admin/thor/categories/${categoryId}/sales-channels`,
2360
+ body
2361
+ );
2362
+ }
2363
+ // -----------------------------------------------------------------------
2364
+ // 3.6 — Category: Unassign from Sales Channels
2365
+ // -----------------------------------------------------------------------
2366
+ /**
2367
+ * Remove category assignment from one or more sales channels (idempotent).
2368
+ *
2369
+ * `DELETE /admin/thor/categories/:id/sales-channels`
2370
+ */
2371
+ async unassignCategoryFromChannels(categoryId, body) {
2372
+ return this.request(
2373
+ "DELETE",
2374
+ `/admin/thor/categories/${categoryId}/sales-channels`,
2375
+ body
2376
+ );
2377
+ }
2378
+ // -----------------------------------------------------------------------
2379
+ // 3.7 — Collection: Sales Channels
2380
+ // -----------------------------------------------------------------------
2381
+ /**
2382
+ * Sales channels to which a collection is assigned.
2383
+ *
2384
+ * `GET /admin/thor/collections/:id/sales-channels`
2385
+ */
2386
+ async getCollectionSalesChannels(collectionId) {
2387
+ return this.request(
2388
+ "GET",
2389
+ `/admin/thor/collections/${collectionId}/sales-channels`
2390
+ );
2391
+ }
2392
+ // -----------------------------------------------------------------------
2393
+ // 3.8 — Collection: Assign to Sales Channels
2394
+ // -----------------------------------------------------------------------
2395
+ /**
2396
+ * Assign a collection to one or more sales channels (idempotent).
2397
+ *
2398
+ * `POST /admin/thor/collections/:id/sales-channels`
2399
+ */
2400
+ async assignCollectionToChannels(collectionId, body) {
2401
+ return this.request(
2402
+ "POST",
2403
+ `/admin/thor/collections/${collectionId}/sales-channels`,
2404
+ body
2405
+ );
2406
+ }
2407
+ // -----------------------------------------------------------------------
2408
+ // 3.9 — Collection: Unassign from Sales Channels
2409
+ // -----------------------------------------------------------------------
2410
+ /**
2411
+ * Remove collection assignment from one or more sales channels (idempotent).
2412
+ *
2413
+ * `DELETE /admin/thor/collections/:id/sales-channels`
2414
+ */
2415
+ async unassignCollectionFromChannels(collectionId, body) {
2416
+ return this.request(
2417
+ "DELETE",
2418
+ `/admin/thor/collections/${collectionId}/sales-channels`,
2419
+ body
2420
+ );
2421
+ }
2422
+ // -----------------------------------------------------------------------
2423
+ // 3.10 — Storefront Config: Read
2424
+ // -----------------------------------------------------------------------
2425
+ /**
2426
+ * Read the visual storefront configuration for a channel.
2427
+ * `:id` is the `sales_channel_id`.
2428
+ *
2429
+ * `GET /admin/thor/storefront-config/:id`
2430
+ */
2431
+ async getStorefrontConfig(salesChannelId) {
2432
+ return this.request(
2433
+ "GET",
2434
+ `/admin/thor/storefront-config/${salesChannelId}`
2435
+ );
2436
+ }
2437
+ // -----------------------------------------------------------------------
2438
+ // 3.11 — Storefront Config: Update
2439
+ // -----------------------------------------------------------------------
2440
+ /**
2441
+ * Update the visual storefront configuration for a channel.
2442
+ * Triggers the `storefront_config.updated` event → ISR webhook.
2443
+ *
2444
+ * `PUT /admin/thor/storefront-config/:id`
2445
+ */
2446
+ async updateStorefrontConfig(salesChannelId, body) {
2447
+ return this.request(
2448
+ "PUT",
2449
+ `/admin/thor/storefront-config/${salesChannelId}`,
2450
+ body
2451
+ );
2452
+ }
2453
+ // -----------------------------------------------------------------------
2454
+ // 3.12 — Site Designer Config: Read
2455
+ // -----------------------------------------------------------------------
2456
+ /**
2457
+ * Read the active Site Designer configuration for a channel, including the
2458
+ * last 10 history entries.
2459
+ *
2460
+ * `GET /admin/thor/site-config/:sales_channel_id`
2461
+ */
2462
+ async getSiteConfig(salesChannelId) {
2463
+ return this.request(
2464
+ "GET",
2465
+ `/admin/thor/site-config/${salesChannelId}`
2466
+ );
2467
+ }
2468
+ // -----------------------------------------------------------------------
2469
+ // 3.13 — Site Designer Config: Update
2470
+ // -----------------------------------------------------------------------
2471
+ /**
2472
+ * Update the Site Designer configuration for a channel.
2473
+ * Creates a new history entry on every PUT.
2474
+ *
2475
+ * `PUT /admin/thor/site-config/:sales_channel_id`
2476
+ */
2477
+ async updateSiteConfig(salesChannelId, body) {
2478
+ return this.request(
2479
+ "PUT",
2480
+ `/admin/thor/site-config/${salesChannelId}`,
2481
+ body
2482
+ );
2483
+ }
2484
+ // -----------------------------------------------------------------------
2485
+ // 3.14 — Site Designer Config: History
2486
+ // -----------------------------------------------------------------------
2487
+ /**
2488
+ * Full paginated history of Site Designer versions for a channel.
2489
+ * Entries are immutable — they are never deleted.
2490
+ *
2491
+ * `GET /admin/thor/site-config/:sales_channel_id/history`
2492
+ */
2493
+ async getSiteConfigHistory(salesChannelId, options = {}) {
2494
+ const { limit = 20, offset = 0 } = options;
2495
+ return this.request(
2496
+ "GET",
2497
+ `/admin/thor/site-config/${salesChannelId}/history?limit=${limit}&offset=${offset}`
2498
+ );
2499
+ }
2500
+ // -----------------------------------------------------------------------
2501
+ // 3.15 — Site Designer Config: Restore
2502
+ // -----------------------------------------------------------------------
2503
+ /**
2504
+ * Restore a previous Site Designer version.
2505
+ * Internally runs the update workflow with the stored config of the target
2506
+ * version — this creates a **new** history entry and does NOT mutate past
2507
+ * entries.
2508
+ *
2509
+ * `POST /admin/thor/site-config/:sales_channel_id/restore/:version`
2510
+ */
2511
+ async restoreSiteConfig(salesChannelId, version) {
2512
+ return this.request(
2513
+ "POST",
2514
+ `/admin/thor/site-config/${salesChannelId}/restore/${encodeURIComponent(version)}`
2515
+ );
2516
+ }
2517
+ };
2518
+
2119
2519
  // src/providers/medusa/index.ts
2120
2520
  var MEDUSA_CART_RETRIEVE_FIELDS = "+items,+items.variant,+items.variant.product,+items.variant.product.thumbnail,+items.variant.options,+items.variant.options.option";
2121
2521
  var MedusaProvider = class {
@@ -2150,6 +2550,18 @@ var MedusaProvider = class {
2150
2550
  * Only created when adminConfig.apiKey is provided.
2151
2551
  */
2152
2552
  sdkAdmin;
2553
+ /**
2554
+ * Admin client for Thor multi-tenant endpoints (`/admin/thor/`).
2555
+ * Available when `adminConfig.apiKey` is provided.
2556
+ *
2557
+ * @example
2558
+ * ```typescript
2559
+ * const { customers } = await commerce.admin.getChannelCustomers('sc_01JXXXXX');
2560
+ * const { api_keys } = await commerce.admin.getChannelApiKeys('sc_01JXXXXX');
2561
+ * await commerce.admin.updateStorefrontConfig('sc_01JXXXXX', { theme_accent_color: '#EF4444', ... });
2562
+ * ```
2563
+ */
2564
+ admin;
2153
2565
  /** Cached distinct product status values — rarely changes, cached for process lifetime */
2154
2566
  _cachedProductStatuses = null;
2155
2567
  /**
@@ -2220,10 +2632,15 @@ var MedusaProvider = class {
2220
2632
  debug: config.debug ?? false
2221
2633
  // No publishableKey or auth config: this instance is admin-only, no JWT needed
2222
2634
  });
2635
+ this.admin = new MedusaAdminClient({
2636
+ baseUrl: config.baseUrl,
2637
+ apiKey: config.adminConfig.apiKey,
2638
+ debug: config.debug
2639
+ });
2223
2640
  this.logger.info("[Admin Auth] Dedicated admin SDK instance created with API key");
2224
2641
  }
2225
2642
  if (config.auth) {
2226
- this.auth = new MedusaAuthProvider(this.sdk, config.auth, config.debug);
2643
+ this.auth = new MedusaAuthProvider(this.sdk, config.auth, config.debug, config.baseUrl, config.publishableKey);
2227
2644
  this.logger.info("Auth provider initialized");
2228
2645
  }
2229
2646
  this._capabilitiesPromise = this._fetchCapabilities();
@@ -7977,6 +8394,1031 @@ var MagentoProvider = class {
7977
8394
  }
7978
8395
  };
7979
8396
 
8397
+ // src/providers/stelorder/transforms.ts
8398
+ function normalizeStelCollection(payload) {
8399
+ if (!payload) {
8400
+ return [];
8401
+ }
8402
+ return Array.isArray(payload) ? payload : [payload];
8403
+ }
8404
+ function normalizeStelEntity(payload) {
8405
+ if (!payload) {
8406
+ return void 0;
8407
+ }
8408
+ return Array.isArray(payload) ? payload[0] : payload;
8409
+ }
8410
+ function transformStelMoney(amount, currencyCode = "USD") {
8411
+ const normalizedAmount = typeof amount === "string" ? Number.parseFloat(amount) : amount ?? 0;
8412
+ return {
8413
+ amount: Number.isFinite(normalizedAmount) ? normalizedAmount.toFixed(2) : "0.00",
8414
+ currencyCode: currencyCode.toUpperCase()
8415
+ };
8416
+ }
8417
+ function transformStelImage(url, altText = "") {
8418
+ const normalizedUrl = url?.trim();
8419
+ if (!normalizedUrl) {
8420
+ return void 0;
8421
+ }
8422
+ return {
8423
+ url: normalizedUrl,
8424
+ altText,
8425
+ width: 800,
8426
+ height: 800
8427
+ };
8428
+ }
8429
+ function normalizeStelDate(value) {
8430
+ if (!value) {
8431
+ return (/* @__PURE__ */ new Date()).toISOString();
8432
+ }
8433
+ const parsed = new Date(value);
8434
+ return Number.isNaN(parsed.getTime()) ? (/* @__PURE__ */ new Date()).toISOString() : parsed.toISOString();
8435
+ }
8436
+ function toStelStringId(value) {
8437
+ if (value === null || value === void 0) {
8438
+ return "";
8439
+ }
8440
+ return String(value);
8441
+ }
8442
+ function slugify(value) {
8443
+ return value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
8444
+ }
8445
+ function buildStelHandle(...candidates) {
8446
+ for (const candidate of candidates) {
8447
+ if (candidate === null || candidate === void 0) {
8448
+ continue;
8449
+ }
8450
+ const normalized = slugify(String(candidate).trim());
8451
+ if (normalized) {
8452
+ return normalized;
8453
+ }
8454
+ }
8455
+ return "stel-item";
8456
+ }
8457
+ function transformStelProductImages(product) {
8458
+ const images = [];
8459
+ if (!product["item-images"]) {
8460
+ return images;
8461
+ }
8462
+ const sorted = [...product["item-images"]].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
8463
+ for (const img of sorted) {
8464
+ const transformed = transformStelImage(img["item-image-path"], product.name ?? "");
8465
+ if (transformed) {
8466
+ images.push(transformed);
8467
+ }
8468
+ }
8469
+ return images;
8470
+ }
8471
+ function deriveStelStockStatus(product) {
8472
+ if (!product["stock-enabled"]) {
8473
+ return "in_stock";
8474
+ }
8475
+ const realStock = product["real-stock"] ?? 0;
8476
+ const minimumStock = product["minimum-stock"] ?? 0;
8477
+ if (realStock <= 0) {
8478
+ return "out_of_stock";
8479
+ }
8480
+ if (realStock <= minimumStock) {
8481
+ return "low_stock";
8482
+ }
8483
+ return "in_stock";
8484
+ }
8485
+ function buildSyntheticVariant(product, currencyCode) {
8486
+ const price = transformStelMoney(product["sales-price"], currencyCode);
8487
+ const stockStatus = deriveStelStockStatus(product);
8488
+ const quantityAvailable = product["stock-enabled"] ? product["real-stock"] ?? 0 : void 0;
8489
+ const image = product["item-images"]?.[0] ? transformStelImage(product["item-images"][0]["item-image-path"], product.name ?? "") : void 0;
8490
+ return {
8491
+ id: toStelStringId(product.id),
8492
+ title: "Default",
8493
+ sku: product.barcode ?? product["full-reference"] ?? void 0,
8494
+ availableForSale: stockStatus !== "out_of_stock",
8495
+ selectedOptions: [],
8496
+ price,
8497
+ compareAtPrice: void 0,
8498
+ quantityAvailable,
8499
+ stockStatus,
8500
+ image
8501
+ };
8502
+ }
8503
+ function transformStelProduct(raw, currencyCode) {
8504
+ const id = toStelStringId(raw.id);
8505
+ const handle = buildStelHandle(raw["full-reference"], raw.reference, raw.name, raw.id);
8506
+ const price = transformStelMoney(raw["sales-price"], currencyCode);
8507
+ const images = transformStelProductImages(raw);
8508
+ const variant = buildSyntheticVariant(raw, currencyCode);
8509
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8510
+ return {
8511
+ id,
8512
+ handle,
8513
+ title: raw.name ?? "",
8514
+ description: raw.description ?? "",
8515
+ availableForSale: variant.availableForSale,
8516
+ options: [],
8517
+ variants: [variant],
8518
+ featuredImage: images[0],
8519
+ images,
8520
+ priceRange: {
8521
+ minVariantPrice: price,
8522
+ maxVariantPrice: price
8523
+ },
8524
+ productType: void 0,
8525
+ vendor: void 0,
8526
+ tags: raw.promotional ? ["promotional"] : [],
8527
+ createdAt: normalizeStelDate(raw["creation-date"]),
8528
+ updatedAt: normalizeStelDate(raw["utc-last-modification-date"]) || now,
8529
+ metadata: {
8530
+ stelFullReference: raw["full-reference"],
8531
+ stelReference: raw.reference,
8532
+ stelBarcode: raw.barcode,
8533
+ stelCategoryId: raw["product-category-id"],
8534
+ stelInactive: raw.inactive,
8535
+ stelPromotional: raw.promotional,
8536
+ stelPurchasePrice: raw["purchase-price"]
8537
+ }
8538
+ };
8539
+ }
8540
+ function transformStelCategory(raw) {
8541
+ const id = toStelStringId(raw.id);
8542
+ const handle = buildStelHandle(raw.slug, raw.name, raw.id);
8543
+ return {
8544
+ id,
8545
+ name: raw.name ?? "",
8546
+ handle,
8547
+ description: raw.description ?? void 0,
8548
+ parentCategoryId: raw["parent-category-id"] ? toStelStringId(raw["parent-category-id"]) : void 0,
8549
+ metadata: {
8550
+ stelSlug: raw.slug
8551
+ }
8552
+ };
8553
+ }
8554
+ function transformStelCategoryToCollection(raw) {
8555
+ const id = toStelStringId(raw.id);
8556
+ const handle = buildStelHandle(raw.slug, raw.name, raw.id);
8557
+ return {
8558
+ id,
8559
+ handle,
8560
+ title: raw.name ?? "",
8561
+ description: raw.description ?? "",
8562
+ path: `/collections/${handle}`,
8563
+ updatedAt: normalizeStelDate(raw["utc-last-modification-date"])
8564
+ };
8565
+ }
8566
+ function transformStelWarehouseToStockLocation(raw) {
8567
+ return {
8568
+ id: toStelStringId(raw.id),
8569
+ name: raw.name ?? "Warehouse"
8570
+ };
8571
+ }
8572
+ function transformStelProductWarehouseToInventoryLevel(pw) {
8573
+ return {
8574
+ locationId: toStelStringId(pw["warehouse-id"]),
8575
+ locationName: pw["warehouse-path"] ?? `Warehouse ${pw["warehouse-id"] ?? "unknown"}`,
8576
+ availableQuantity: pw["real-stock"] ?? 0,
8577
+ reservedQuantity: Math.max(0, (pw["virtual-stock"] ?? 0) - (pw["real-stock"] ?? 0)),
8578
+ stockedQuantity: pw["virtual-stock"] ?? pw["real-stock"] ?? 0
8579
+ };
8580
+ }
8581
+ function splitStelName(fullName) {
8582
+ if (!fullName?.trim()) {
8583
+ return { firstName: "", lastName: "" };
8584
+ }
8585
+ const parts = fullName.trim().split(/\s+/);
8586
+ const firstName = parts[0] ?? "";
8587
+ const lastName = parts.length > 1 ? parts.slice(1).join(" ") : "";
8588
+ return { firstName, lastName };
8589
+ }
8590
+ function transformStelClient(raw) {
8591
+ const { firstName, lastName } = splitStelName(raw.name ?? raw["business-name"]);
8592
+ return {
8593
+ id: toStelStringId(raw.id),
8594
+ email: raw.email ?? "",
8595
+ firstName: firstName || null,
8596
+ lastName: lastName || null,
8597
+ phone: raw.phone ?? raw.mobile ?? null,
8598
+ hasAccount: true,
8599
+ createdAt: normalizeStelDate(raw["utc-last-modification-date"]),
8600
+ updatedAt: normalizeStelDate(raw["utc-last-modification-date"]),
8601
+ metadata: {
8602
+ stelBusinessName: raw["business-name"],
8603
+ stelRateId: raw["rate-id"],
8604
+ stelPaymentOptionId: raw["payment-option-id"],
8605
+ stelPaymentTermId: raw["payment-term-id"],
8606
+ stelDeliveryOptionId: raw["delivery-option-id"]
8607
+ }
8608
+ };
8609
+ }
8610
+ function deriveStelOrderStatus(raw) {
8611
+ if (raw.deleted) {
8612
+ return "cancelled";
8613
+ }
8614
+ const status = (raw.status ?? "").toLowerCase();
8615
+ if (status.includes("cancel") || status.includes("anulad")) {
8616
+ return "cancelled";
8617
+ }
8618
+ if (status.includes("complet") || status.includes("cerrad") || status.includes("settled")) {
8619
+ return "completed";
8620
+ }
8621
+ if (status.includes("process") || status.includes("confirm")) {
8622
+ return "processing";
8623
+ }
8624
+ return "pending";
8625
+ }
8626
+ function deriveStelPaymentStatus(raw) {
8627
+ const orderStatus = deriveStelOrderStatus(raw);
8628
+ if (orderStatus === "completed") {
8629
+ return "paid";
8630
+ }
8631
+ if (orderStatus === "cancelled") {
8632
+ return "pending";
8633
+ }
8634
+ return "pending";
8635
+ }
8636
+ function deriveStelFulfillmentStatus(raw) {
8637
+ const orderStatus = deriveStelOrderStatus(raw);
8638
+ if (orderStatus === "completed") {
8639
+ return "fulfilled";
8640
+ }
8641
+ return "unfulfilled";
8642
+ }
8643
+ function transformStelDocumentLine(line, index, currencyCode) {
8644
+ const unitPrice = line["item-base-price"] ?? 0;
8645
+ const quantity = line.units ?? 1;
8646
+ const lineTotal = unitPrice * quantity;
8647
+ return {
8648
+ id: toStelStringId(line["item-id"] ?? index),
8649
+ title: line["item-name"] ?? "",
8650
+ variantTitle: void 0,
8651
+ quantity,
8652
+ price: transformStelMoney(unitPrice, currencyCode),
8653
+ total: transformStelMoney(lineTotal, currencyCode),
8654
+ sku: void 0,
8655
+ productId: line["item-id"] ? toStelStringId(line["item-id"]) : void 0
8656
+ };
8657
+ }
8658
+ function transformStelSalesOrder(raw, currencyCode) {
8659
+ const id = toStelStringId(raw.id);
8660
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8661
+ const items = (raw["document-lines"] ?? []).filter((line) => line["line-type"] === "ITEM" || !line["line-type"]).map((line, i) => transformStelDocumentLine(line, i, currencyCode));
8662
+ const status = deriveStelOrderStatus(raw);
8663
+ return {
8664
+ id,
8665
+ orderNumber: raw.reference ?? id,
8666
+ email: raw.email ?? "",
8667
+ status,
8668
+ paymentStatus: deriveStelPaymentStatus(raw),
8669
+ fulfillmentStatus: deriveStelFulfillmentStatus(raw),
8670
+ customerId: raw["client-id"] ? toStelStringId(raw["client-id"]) : void 0,
8671
+ items,
8672
+ subtotal: transformStelMoney(raw["total-base"], currencyCode),
8673
+ total: transformStelMoney(raw.total, currencyCode),
8674
+ tax: raw["total-tax"] != null ? transformStelMoney(raw["total-tax"], currencyCode) : void 0,
8675
+ createdAt: normalizeStelDate(raw["creation-date"]),
8676
+ updatedAt: normalizeStelDate(raw["utc-last-modification-date"]) || now,
8677
+ cancelledAt: status === "cancelled" ? normalizeStelDate(raw["utc-last-modification-date"]) : void 0,
8678
+ completedAt: status === "completed" ? normalizeStelDate(raw["utc-last-modification-date"]) : void 0,
8679
+ metadata: {
8680
+ stelReference: raw.reference,
8681
+ stelStatus: raw.status,
8682
+ stelPaymentOptionId: raw["payment-option-id"],
8683
+ stelPaymentTermId: raw["payment-term-id"],
8684
+ stelDeliveryOptionId: raw["delivery-option-id"]
8685
+ }
8686
+ };
8687
+ }
8688
+ function inferPaymentMethodType(name) {
8689
+ if (!name) {
8690
+ return "other";
8691
+ }
8692
+ const lower = name.toLowerCase();
8693
+ if (lower.includes("transfer") || lower.includes("bank") || lower.includes("sepa") || lower.includes("iban")) {
8694
+ return "bank_transfer";
8695
+ }
8696
+ if (lower.includes("card") || lower.includes("tarjeta") || lower.includes("visa") || lower.includes("master")) {
8697
+ return "card";
8698
+ }
8699
+ if (lower.includes("paypal") || lower.includes("wallet") || lower.includes("apple") || lower.includes("google")) {
8700
+ return "wallet";
8701
+ }
8702
+ if (lower.includes("cash") || lower.includes("contrar") || lower.includes("efectivo") || lower.includes("cod")) {
8703
+ return "cash_on_delivery";
8704
+ }
8705
+ if (lower.includes("crypto") || lower.includes("bitcoin")) {
8706
+ return "crypto";
8707
+ }
8708
+ return "other";
8709
+ }
8710
+ function transformStelPaymentOption(raw) {
8711
+ return {
8712
+ id: toStelStringId(raw.id),
8713
+ name: raw.name ?? "Payment Option",
8714
+ type: inferPaymentMethodType(raw.name),
8715
+ isEnabled: !raw.deleted,
8716
+ requiresSetup: false,
8717
+ description: raw.description ?? void 0,
8718
+ metadata: {
8719
+ stelPath: raw.path,
8720
+ source: "paymentOption"
8721
+ }
8722
+ };
8723
+ }
8724
+ function transformStelDeliveryOption(raw, currencyCode) {
8725
+ return {
8726
+ id: toStelStringId(raw.id),
8727
+ name: raw.name ?? "Delivery Option",
8728
+ price: transformStelMoney(0, currencyCode),
8729
+ description: raw.description ?? void 0
8730
+ };
8731
+ }
8732
+
8733
+ // src/providers/stelorder/client.ts
8734
+ var DEFAULT_RETRY_CONFIG2 = {
8735
+ maxRetries: 2,
8736
+ initialDelayMs: 300,
8737
+ backoffMultiplier: 2,
8738
+ maxDelayMs: 5e3,
8739
+ timeoutMs: 1e4,
8740
+ retryableStatusCodes: [408, 429, 500, 502, 503, 504]
8741
+ };
8742
+ function sleep2(ms) {
8743
+ return new Promise((resolve) => setTimeout(resolve, ms));
8744
+ }
8745
+ function calculateDelay2(attempt, retryConfig) {
8746
+ const delay = retryConfig.initialDelayMs * Math.pow(retryConfig.backoffMultiplier, attempt);
8747
+ return Math.min(delay, retryConfig.maxDelayMs);
8748
+ }
8749
+ function isRetryableError2(error, retryConfig) {
8750
+ if (error instanceof ProviderAPIError) {
8751
+ return typeof error.status === "number" && retryConfig.retryableStatusCodes.includes(error.status);
8752
+ }
8753
+ if (!(error instanceof Error)) {
8754
+ return false;
8755
+ }
8756
+ const message = error.message.toLowerCase();
8757
+ return error.name === "AbortError" || error.name === "TimeoutError" || message.includes("fetch failed") || message.includes("timeout") || message.includes("econnrefused") || message.includes("etimedout");
8758
+ }
8759
+ function createRetryingFetch(fetchImplementation, retryConfig, logger2) {
8760
+ return async (url, init) => {
8761
+ let lastError = new Error("Unknown STEL Order request error");
8762
+ for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt += 1) {
8763
+ const controller = new AbortController();
8764
+ const timeoutId = setTimeout(() => controller.abort(), retryConfig.timeoutMs);
8765
+ try {
8766
+ const response = await fetchImplementation(url, {
8767
+ ...init,
8768
+ signal: controller.signal
8769
+ });
8770
+ clearTimeout(timeoutId);
8771
+ if (!response.ok && retryConfig.retryableStatusCodes.includes(response.status)) {
8772
+ throw new ProviderAPIError(
8773
+ `STEL Order API error: ${response.status} ${response.statusText}`,
8774
+ "stelorder",
8775
+ response.status
8776
+ );
8777
+ }
8778
+ return response;
8779
+ } catch (error) {
8780
+ clearTimeout(timeoutId);
8781
+ lastError = error;
8782
+ if (attempt >= retryConfig.maxRetries || !isRetryableError2(error, retryConfig)) {
8783
+ throw error;
8784
+ }
8785
+ const delay = calculateDelay2(attempt, retryConfig);
8786
+ logger2.warn("STEL request failed, retrying.", {
8787
+ url,
8788
+ attempt: attempt + 1,
8789
+ maxAttempts: retryConfig.maxRetries + 1,
8790
+ nextRetryIn: delay,
8791
+ error: extractErrorMessage(error)
8792
+ });
8793
+ await sleep2(delay);
8794
+ }
8795
+ }
8796
+ throw lastError;
8797
+ };
8798
+ }
8799
+ async function parseErrorResponse(response) {
8800
+ const contentType = response.headers.get("content-type") ?? "";
8801
+ try {
8802
+ if (contentType.includes("application/json")) {
8803
+ const body = await response.json();
8804
+ const message = extractErrorMessage(body);
8805
+ if (message && message !== "An unknown error occurred") {
8806
+ return message;
8807
+ }
8808
+ }
8809
+ } catch {
8810
+ }
8811
+ try {
8812
+ const text = await response.text();
8813
+ if (text.trim()) {
8814
+ return text.trim();
8815
+ }
8816
+ } catch {
8817
+ }
8818
+ return `STEL Order API error: ${response.status} ${response.statusText}`;
8819
+ }
8820
+ function normalizeStelApiBaseUrl(baseUrl) {
8821
+ const trimmed = baseUrl.trim();
8822
+ if (!trimmed) {
8823
+ throw new ConfigurationError("Stel Order baseUrl is required.");
8824
+ }
8825
+ const normalized = trimmed.replace(/\/api\/openapi\.json$/i, "").replace(/\/+$/, "");
8826
+ return normalized.endsWith("/app") ? normalized : `${normalized}/app`;
8827
+ }
8828
+ var StelOrderClient = class {
8829
+ apiBaseUrl;
8830
+ apiKey;
8831
+ logger;
8832
+ retryConfig;
8833
+ fetchImplementation;
8834
+ constructor(config) {
8835
+ const apiKey = config.apiKey ?? config.publishableKey;
8836
+ if (!apiKey) {
8837
+ throw new ConfigurationError(
8838
+ "Stel Order API key is required. Pass `apiKey` or `publishableKey` to createProvider()."
8839
+ );
8840
+ }
8841
+ this.apiBaseUrl = normalizeStelApiBaseUrl(config.baseUrl);
8842
+ this.apiKey = apiKey;
8843
+ this.logger = createLogger("StelOrderClient", config.debug);
8844
+ this.retryConfig = {
8845
+ ...DEFAULT_RETRY_CONFIG2,
8846
+ ...config.retry
8847
+ };
8848
+ this.fetchImplementation = config.customFetch ? createRetryingFetch(config.customFetch, this.retryConfig, this.logger) : void 0;
8849
+ this.logger.info("Initializing Stel Order client", {
8850
+ apiBaseUrl: this.apiBaseUrl,
8851
+ retryConfig: this.retryConfig
8852
+ });
8853
+ }
8854
+ async list(resource, query) {
8855
+ const payload = await this.request({
8856
+ method: "GET",
8857
+ path: resource,
8858
+ query
8859
+ });
8860
+ return normalizeStelCollection(payload);
8861
+ }
8862
+ async getOne(resource, id, query) {
8863
+ const payload = await this.request({
8864
+ method: "GET",
8865
+ path: `${resource}/${id}`,
8866
+ query
8867
+ });
8868
+ return normalizeStelEntity(payload);
8869
+ }
8870
+ async create(resource, body) {
8871
+ return this.request({
8872
+ method: "POST",
8873
+ path: resource,
8874
+ body
8875
+ });
8876
+ }
8877
+ async update(resource, id, body) {
8878
+ return this.request({
8879
+ method: "PUT",
8880
+ path: `${resource}/${id}`,
8881
+ body
8882
+ });
8883
+ }
8884
+ async remove(resource, id) {
8885
+ await this.request({
8886
+ method: "DELETE",
8887
+ path: `${resource}/${id}`
8888
+ });
8889
+ }
8890
+ buildUrl(path, query) {
8891
+ const normalizedPath = path.replace(/^\/+/, "");
8892
+ const url = new URL(`${this.apiBaseUrl}/${normalizedPath}`);
8893
+ if (!query) {
8894
+ return url.toString();
8895
+ }
8896
+ for (const [key, value] of Object.entries(query)) {
8897
+ if (value === void 0 || value === null || value === "") {
8898
+ continue;
8899
+ }
8900
+ if (Array.isArray(value)) {
8901
+ url.searchParams.set(key, value.map(String).join(","));
8902
+ continue;
8903
+ }
8904
+ url.searchParams.set(key, String(value));
8905
+ }
8906
+ return url.toString();
8907
+ }
8908
+ buildHeaders(headers, hasBody = false) {
8909
+ const normalizedHeaders = new Headers(headers);
8910
+ normalizedHeaders.set("Accept", "application/json");
8911
+ normalizedHeaders.set("APIKEY", this.apiKey);
8912
+ if (hasBody && !normalizedHeaders.has("Content-Type")) {
8913
+ normalizedHeaders.set("Content-Type", "application/json");
8914
+ }
8915
+ return normalizedHeaders;
8916
+ }
8917
+ async executeFetch(url, init) {
8918
+ if (this.fetchImplementation) {
8919
+ return this.fetchImplementation(url, init);
8920
+ }
8921
+ return fetchWithRetry(url, init, this.retryConfig);
8922
+ }
8923
+ async request(options) {
8924
+ const url = this.buildUrl(options.path, options.query);
8925
+ const hasBody = options.body !== void 0;
8926
+ this.logger.debug("STEL request", {
8927
+ method: options.method,
8928
+ url
8929
+ });
8930
+ const response = await this.executeFetch(url, {
8931
+ method: options.method,
8932
+ headers: this.buildHeaders(options.headers, hasBody),
8933
+ body: hasBody ? JSON.stringify(options.body) : void 0
8934
+ });
8935
+ if (!response.ok) {
8936
+ const message = await parseErrorResponse(response);
8937
+ throw new ProviderAPIError(message, "stelorder", response.status);
8938
+ }
8939
+ if (response.status === 204) {
8940
+ return void 0;
8941
+ }
8942
+ const contentType = response.headers.get("content-type") ?? "";
8943
+ if (contentType.includes("application/json")) {
8944
+ return await response.json();
8945
+ }
8946
+ return await response.text();
8947
+ }
8948
+ };
8949
+
8950
+ // src/providers/stelorder/index.ts
8951
+ var StelOrderProvider = class {
8952
+ name = "stelorder";
8953
+ version = "1.0.0";
8954
+ capabilities = STELORDER_CAPABILITIES;
8955
+ client;
8956
+ logger;
8957
+ currencyCode;
8958
+ storefrontId;
8959
+ companyName;
8960
+ constructor(config) {
8961
+ this.client = new StelOrderClient(config);
8962
+ this.logger = createLogger("StelOrderProvider", config.debug);
8963
+ this.currencyCode = config.currencyCode ?? "USD";
8964
+ this.storefrontId = config.storefrontId ?? "stelorder-primary";
8965
+ this.companyName = config.companyName ?? "Stel Order";
8966
+ this.logger.info("Initializing Stel Order provider", {
8967
+ storefrontId: this.storefrontId,
8968
+ companyName: this.companyName,
8969
+ currencyCode: this.currencyCode
8970
+ });
8971
+ }
8972
+ // ============================================
8973
+ // Product Operations
8974
+ // ============================================
8975
+ async getProduct(handle, _options) {
8976
+ this.logger.debug("getProduct", { handle });
8977
+ try {
8978
+ if (/^\d+$/.test(handle)) {
8979
+ const raw = await this.client.getOne("products", handle);
8980
+ return raw ? transformStelProduct(raw, this.currencyCode) : void 0;
8981
+ }
8982
+ const rawProducts = await this.client.list("products", { limit: 100 });
8983
+ for (const raw of rawProducts) {
8984
+ const productHandle = buildStelHandle(raw["full-reference"], raw.reference, raw.name, raw.id);
8985
+ if (productHandle === handle) {
8986
+ return transformStelProduct(raw, this.currencyCode);
8987
+ }
8988
+ }
8989
+ return void 0;
8990
+ } catch (error) {
8991
+ this.logger.error("getProduct failed", { handle, error });
8992
+ throw this.wrapError("getProduct", error);
8993
+ }
8994
+ }
8995
+ async getProducts(options) {
8996
+ this.logger.debug("getProducts", { options });
8997
+ try {
8998
+ const query = {};
8999
+ if (options?.first) {
9000
+ query.limit = options.first;
9001
+ }
9002
+ if (options?.skip) {
9003
+ query.start = options.skip;
9004
+ }
9005
+ if (typeof options?.after === "number") {
9006
+ query.start = options.after;
9007
+ }
9008
+ if (options?.sortKey) {
9009
+ query.sort = options.reverse ? `-${options.sortKey}` : options.sortKey;
9010
+ }
9011
+ if (options?.categoryIds?.length) {
9012
+ query["product-category-id"] = `in:${options.categoryIds.join(",")}`;
9013
+ }
9014
+ const rawProducts = await this.client.list("products", query);
9015
+ let products = rawProducts.filter((p) => !p.deleted && !p.inactive).map((p) => transformStelProduct(p, this.currencyCode));
9016
+ if (options?.query) {
9017
+ const needle = options.query.toLowerCase();
9018
+ products = products.filter(
9019
+ (p) => p.title.toLowerCase().includes(needle) || p.description.toLowerCase().includes(needle) || p.handle.includes(needle)
9020
+ );
9021
+ }
9022
+ return products;
9023
+ } catch (error) {
9024
+ this.logger.error("getProducts failed", { options, error });
9025
+ throw this.wrapError("getProducts", error);
9026
+ }
9027
+ }
9028
+ async getProductRecommendations(_productId) {
9029
+ return [];
9030
+ }
9031
+ // ============================================
9032
+ // Category Operations
9033
+ // ============================================
9034
+ async getCategory(idOrHandle) {
9035
+ this.logger.debug("getCategory", { idOrHandle });
9036
+ try {
9037
+ if (/^\d+$/.test(idOrHandle)) {
9038
+ const raw = await this.client.getOne("productCategories", idOrHandle);
9039
+ return raw ? transformStelCategory(raw) : void 0;
9040
+ }
9041
+ const rawCategories = await this.client.list("productCategories");
9042
+ for (const raw of rawCategories) {
9043
+ const catHandle = buildStelHandle(raw.slug, raw.name, raw.id);
9044
+ if (catHandle === idOrHandle) {
9045
+ return transformStelCategory(raw);
9046
+ }
9047
+ }
9048
+ return void 0;
9049
+ } catch (error) {
9050
+ this.logger.error("getCategory failed", { idOrHandle, error });
9051
+ throw this.wrapError("getCategory", error);
9052
+ }
9053
+ }
9054
+ async getCategories(options) {
9055
+ this.logger.debug("getCategories", { options });
9056
+ try {
9057
+ if (options?.customFetcher) {
9058
+ return options.customFetcher(options);
9059
+ }
9060
+ const query = {};
9061
+ if (options?.limit) {
9062
+ query.limit = options.limit;
9063
+ }
9064
+ if (options?.parentCategoryId) {
9065
+ query["parent-category-id"] = options.parentCategoryId;
9066
+ }
9067
+ const rawCategories = await this.client.list("productCategories", query);
9068
+ let categories = rawCategories.filter((c) => !c.deleted).map(transformStelCategory);
9069
+ if (options?.query) {
9070
+ const needle = options.query.toLowerCase();
9071
+ categories = categories.filter(
9072
+ (c) => c.name.toLowerCase().includes(needle) || c.handle.includes(needle)
9073
+ );
9074
+ }
9075
+ return categories;
9076
+ } catch (error) {
9077
+ this.logger.error("getCategories failed", { options, error });
9078
+ throw this.wrapError("getCategories", error);
9079
+ }
9080
+ }
9081
+ async getCategoryProducts(handle, options) {
9082
+ this.logger.debug("getCategoryProducts", { handle, options });
9083
+ try {
9084
+ const category = await this.getCategory(handle);
9085
+ if (!category) {
9086
+ return [];
9087
+ }
9088
+ return this.getProducts({
9089
+ categoryIds: [category.id],
9090
+ first: options?.first,
9091
+ sortKey: options?.sortKey,
9092
+ reverse: options?.reverse,
9093
+ after: options?.after
9094
+ });
9095
+ } catch (error) {
9096
+ this.logger.error("getCategoryProducts failed", { handle, error });
9097
+ throw this.wrapError("getCategoryProducts", error);
9098
+ }
9099
+ }
9100
+ // ============================================
9101
+ // Collection Operations (alias categories)
9102
+ // ============================================
9103
+ async getCollection(handle) {
9104
+ this.logger.debug("getCollection", { handle });
9105
+ try {
9106
+ if (/^\d+$/.test(handle)) {
9107
+ const raw = await this.client.getOne("productCategories", handle);
9108
+ return raw ? transformStelCategoryToCollection(raw) : void 0;
9109
+ }
9110
+ const rawCategories = await this.client.list("productCategories");
9111
+ for (const raw of rawCategories) {
9112
+ const catHandle = buildStelHandle(raw.slug, raw.name, raw.id);
9113
+ if (catHandle === handle) {
9114
+ return transformStelCategoryToCollection(raw);
9115
+ }
9116
+ }
9117
+ return void 0;
9118
+ } catch (error) {
9119
+ this.logger.error("getCollection failed", { handle, error });
9120
+ throw this.wrapError("getCollection", error);
9121
+ }
9122
+ }
9123
+ async getCollections(options) {
9124
+ this.logger.debug("getCollections", { options });
9125
+ try {
9126
+ const query = {};
9127
+ if (options?.limit) {
9128
+ query.limit = options.limit;
9129
+ }
9130
+ const rawCategories = await this.client.list("productCategories", query);
9131
+ let collections = rawCategories.filter((c) => !c.deleted).map(transformStelCategoryToCollection);
9132
+ if (options?.query) {
9133
+ const needle = options.query.toLowerCase();
9134
+ collections = collections.filter(
9135
+ (c) => c.title.toLowerCase().includes(needle) || c.handle.includes(needle)
9136
+ );
9137
+ }
9138
+ return collections;
9139
+ } catch (error) {
9140
+ this.logger.error("getCollections failed", { options, error });
9141
+ throw this.wrapError("getCollections", error);
9142
+ }
9143
+ }
9144
+ async getCollectionProducts(handle, options) {
9145
+ return this.getCategoryProducts(handle, options);
9146
+ }
9147
+ // ============================================
9148
+ // Inventory Operations
9149
+ // ============================================
9150
+ async getStockLocations(_params) {
9151
+ this.logger.debug("getStockLocations");
9152
+ try {
9153
+ const rawWarehouses = await this.client.list("warehouses");
9154
+ return rawWarehouses.filter((w) => !w.deleted).map(transformStelWarehouseToStockLocation);
9155
+ } catch (error) {
9156
+ this.logger.error("getStockLocations failed", { error });
9157
+ throw this.wrapError("getStockLocations", error);
9158
+ }
9159
+ }
9160
+ async checkInventoryLevels(variantId, _salesChannelId) {
9161
+ this.logger.debug("checkInventoryLevels", { variantId });
9162
+ try {
9163
+ const raw = await this.client.getOne("products", variantId);
9164
+ if (!raw?.["product-warehouses"]?.length) {
9165
+ return [];
9166
+ }
9167
+ return raw["product-warehouses"].map(transformStelProductWarehouseToInventoryLevel);
9168
+ } catch (error) {
9169
+ this.logger.error("checkInventoryLevels failed", { variantId, error });
9170
+ throw this.wrapError("checkInventoryLevels", error);
9171
+ }
9172
+ }
9173
+ validateCartStock(_cartId) {
9174
+ return this.notSupported("validateCartStock");
9175
+ }
9176
+ getFulfillmentOptions(_cartId, _salesChannelId) {
9177
+ return this.notSupported("getFulfillmentOptions");
9178
+ }
9179
+ // ============================================
9180
+ // Cart Operations (unsupported — STEL is ERP, not storefront)
9181
+ // ============================================
9182
+ createCart(_options) {
9183
+ return this.notSupported("createCart");
9184
+ }
9185
+ getCart(_cartId) {
9186
+ return this.notSupported("getCart");
9187
+ }
9188
+ addToCart(_cartId, _lines) {
9189
+ return this.notSupported("addToCart");
9190
+ }
9191
+ removeFromCart(_cartId, _lineIds) {
9192
+ return this.notSupported("removeFromCart");
9193
+ }
9194
+ updateCart(_cartId, _lines) {
9195
+ return this.notSupported("updateCart");
9196
+ }
9197
+ // ============================================
9198
+ // Checkout Operations (unsupported)
9199
+ // ============================================
9200
+ completeCheckout(_cartId) {
9201
+ return this.notSupported("completeCheckout");
9202
+ }
9203
+ getRegionCountries(_regionId) {
9204
+ return this.notSupported("getRegionCountries");
9205
+ }
9206
+ addShippingMethod(_cartId, _methodId) {
9207
+ return this.notSupported("addShippingMethod");
9208
+ }
9209
+ initiatePaymentSession(_cartId, _providerId) {
9210
+ return this.notSupported("initiatePaymentSession");
9211
+ }
9212
+ applyDiscountCode(_cartId, _code) {
9213
+ return this.notSupported("applyDiscountCode");
9214
+ }
9215
+ removeDiscountCode(_cartId, _code) {
9216
+ return this.notSupported("removeDiscountCode");
9217
+ }
9218
+ // ============================================
9219
+ // Payment Methods
9220
+ // ============================================
9221
+ async getPaymentMethods(_options) {
9222
+ this.logger.debug("getPaymentMethods");
9223
+ try {
9224
+ const rawOptions = await this.client.list("paymentOptions");
9225
+ return rawOptions.filter((o) => !o.deleted).map(transformStelPaymentOption);
9226
+ } catch (error) {
9227
+ this.logger.error("getPaymentMethods failed", { error });
9228
+ throw this.wrapError("getPaymentMethods", error);
9229
+ }
9230
+ }
9231
+ // ============================================
9232
+ // Shipping / Delivery Methods
9233
+ // ============================================
9234
+ async getShippingMethods(_cartId) {
9235
+ this.logger.debug("getShippingMethods (delivery options listing)");
9236
+ try {
9237
+ const rawOptions = await this.client.list("deliveryOptions");
9238
+ return rawOptions.filter((o) => !o.deleted).map((o) => transformStelDeliveryOption(o, this.currencyCode));
9239
+ } catch (error) {
9240
+ this.logger.error("getShippingMethods failed", { error });
9241
+ throw this.wrapError("getShippingMethods", error);
9242
+ }
9243
+ }
9244
+ // ============================================
9245
+ // Customer Operations (optional)
9246
+ // ============================================
9247
+ async getCustomer(customerId) {
9248
+ this.logger.debug("getCustomer", { customerId });
9249
+ try {
9250
+ const raw = await this.client.getOne("clients", customerId);
9251
+ return raw ? transformStelClient(raw) : void 0;
9252
+ } catch (error) {
9253
+ this.logger.error("getCustomer failed", { customerId, error });
9254
+ throw this.wrapError("getCustomer", error);
9255
+ }
9256
+ }
9257
+ async getCustomers(options) {
9258
+ this.logger.debug("getCustomers", { options });
9259
+ try {
9260
+ if (options?.customFetcher) {
9261
+ return options.customFetcher(options);
9262
+ }
9263
+ const query = {};
9264
+ if (options?.limit) {
9265
+ query.limit = options.limit;
9266
+ }
9267
+ if (options?.offset) {
9268
+ query.start = options.offset;
9269
+ }
9270
+ const rawClients = await this.client.list("clients", query);
9271
+ let customers = rawClients.filter((c) => !c.deleted).map(transformStelClient);
9272
+ if (options?.search) {
9273
+ const needle = options.search.toLowerCase();
9274
+ customers = customers.filter(
9275
+ (c) => c.email.toLowerCase().includes(needle) || (c.firstName ?? "").toLowerCase().includes(needle) || (c.lastName ?? "").toLowerCase().includes(needle)
9276
+ );
9277
+ }
9278
+ return customers;
9279
+ } catch (error) {
9280
+ this.logger.error("getCustomers failed", { options, error });
9281
+ throw this.wrapError("getCustomers", error);
9282
+ }
9283
+ }
9284
+ async updateCustomer(customerId, data) {
9285
+ this.logger.debug("updateCustomer", { customerId, data });
9286
+ try {
9287
+ const stelPayload = {};
9288
+ if (data.firstName || data.lastName) {
9289
+ stelPayload.name = [data.firstName, data.lastName].filter(Boolean).join(" ");
9290
+ }
9291
+ if (data.email) {
9292
+ stelPayload.email = data.email;
9293
+ }
9294
+ if (data.phone) {
9295
+ stelPayload.phone = data.phone;
9296
+ }
9297
+ const raw = await this.client.update("clients", customerId, stelPayload);
9298
+ return transformStelClient(raw);
9299
+ } catch (error) {
9300
+ this.logger.error("updateCustomer failed", { customerId, error });
9301
+ throw this.wrapError("updateCustomer", error);
9302
+ }
9303
+ }
9304
+ // ============================================
9305
+ // Order Operations (optional)
9306
+ // ============================================
9307
+ async getOrder(orderId) {
9308
+ this.logger.debug("getOrder", { orderId });
9309
+ try {
9310
+ const raw = await this.client.getOne("salesOrders", orderId);
9311
+ return raw ? transformStelSalesOrder(raw, this.currencyCode) : void 0;
9312
+ } catch (error) {
9313
+ this.logger.error("getOrder failed", { orderId, error });
9314
+ throw this.wrapError("getOrder", error);
9315
+ }
9316
+ }
9317
+ async getOrders(options) {
9318
+ this.logger.debug("getOrders", { options });
9319
+ try {
9320
+ if (options?.customFetcher) {
9321
+ return options.customFetcher(options);
9322
+ }
9323
+ const query = {};
9324
+ if (options?.limit) {
9325
+ query.limit = options.limit;
9326
+ }
9327
+ if (options?.offset) {
9328
+ query.start = options.offset;
9329
+ }
9330
+ if (options?.customerId) {
9331
+ query["client-id"] = options.customerId;
9332
+ }
9333
+ const rawOrders = await this.client.list("salesOrders", query);
9334
+ return rawOrders.filter((o) => !o.deleted).map((o) => transformStelSalesOrder(o, this.currencyCode));
9335
+ } catch (error) {
9336
+ this.logger.error("getOrders failed", { options, error });
9337
+ throw this.wrapError("getOrders", error);
9338
+ }
9339
+ }
9340
+ async getAdminOrders(options) {
9341
+ this.logger.debug("getAdminOrders", { options });
9342
+ try {
9343
+ const query = {};
9344
+ if (options?.limit) {
9345
+ query.limit = options.limit;
9346
+ }
9347
+ if (options?.offset) {
9348
+ query.start = options.offset;
9349
+ }
9350
+ const rawOrders = await this.client.list("salesOrders", query);
9351
+ let orders = rawOrders.filter((o) => !o.deleted).map((o) => transformStelSalesOrder(o, this.currencyCode));
9352
+ if (options?.search) {
9353
+ const needle = options.search.toLowerCase();
9354
+ orders = orders.filter(
9355
+ (o) => o.orderNumber.toLowerCase().includes(needle) || o.email.toLowerCase().includes(needle)
9356
+ );
9357
+ }
9358
+ if (options?.status) {
9359
+ const statuses = Array.isArray(options.status) ? options.status : [options.status];
9360
+ orders = orders.filter((o) => statuses.includes(o.status));
9361
+ }
9362
+ return { orders, count: orders.length };
9363
+ } catch (error) {
9364
+ this.logger.error("getAdminOrders failed", { options, error });
9365
+ throw this.wrapError("getAdminOrders", error);
9366
+ }
9367
+ }
9368
+ cancelOrder(_orderId) {
9369
+ return this.notSupported("cancelOrder");
9370
+ }
9371
+ createFulfillment(_orderId, _items) {
9372
+ return this.notSupported("createFulfillment");
9373
+ }
9374
+ createRefund(_orderId, _amount, _reason) {
9375
+ return this.notSupported("createRefund");
9376
+ }
9377
+ // ============================================
9378
+ // Storefront Context
9379
+ // ============================================
9380
+ async getStorefrontContext() {
9381
+ return {
9382
+ id: this.storefrontId,
9383
+ name: this.companyName,
9384
+ platformType: "stelorder",
9385
+ requiresProductScoping: false,
9386
+ currencyCode: this.currencyCode,
9387
+ metadata: {
9388
+ apiBaseUrl: this.client.apiBaseUrl,
9389
+ singleCompany: true,
9390
+ providerVersion: this.version
9391
+ }
9392
+ };
9393
+ }
9394
+ isStorefrontRequired() {
9395
+ return false;
9396
+ }
9397
+ // ============================================
9398
+ // Internal helpers
9399
+ // ============================================
9400
+ notSupported(operation) {
9401
+ return Promise.reject(
9402
+ new ProviderAPIError(
9403
+ `Stel Order does not support \`${operation}\`. STEL is an ERP adapter without cart/checkout/auth capabilities.`,
9404
+ "stelorder",
9405
+ 501
9406
+ )
9407
+ );
9408
+ }
9409
+ wrapError(operation, error) {
9410
+ if (error instanceof ProviderAPIError) {
9411
+ return error;
9412
+ }
9413
+ const message = error instanceof Error ? error.message : String(error);
9414
+ return new ProviderAPIError(
9415
+ `Stel Order \`${operation}\` failed: ${message}`,
9416
+ "stelorder",
9417
+ 500
9418
+ );
9419
+ }
9420
+ };
9421
+
7980
9422
  // src/providers/mock/mock.ts
7981
9423
  var FIXTURE_TIMESTAMP = "2026-01-01T00:00:00.000Z";
7982
9424
  var DEFAULT_SITE_URL = "https://mock.thorcommerce.local";
@@ -8996,6 +10438,8 @@ function createProvider(options) {
8996
10438
  return new MockProvider(config);
8997
10439
  case "medusa":
8998
10440
  return new MedusaProvider(config);
10441
+ case "stelorder":
10442
+ return new StelOrderProvider(config);
8999
10443
  case "shopify":
9000
10444
  return new ShopifyProvider(config);
9001
10445
  case "bigcommerce":
@@ -9039,8 +10483,10 @@ export {
9039
10483
  ResourceNotFoundError,
9040
10484
  SHOPIFY_CAPABILITIES,
9041
10485
  SPREE_CAPABILITIES,
10486
+ STELORDER_CAPABILITIES,
9042
10487
  ShopifyProvider,
9043
10488
  SpreeProvider,
10489
+ StelOrderProvider,
9044
10490
  StorefrontConfigError2 as StorefrontConfigError,
9045
10491
  WOOCOMMERCE_CAPABILITIES,
9046
10492
  WooCommerceProvider,