@thorprovider/adapters 5.0.10 → 5.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -45,11 +45,13 @@ __export(src_exports, {
45
45
  MAGENTO_CAPABILITIES: () => MAGENTO_CAPABILITIES,
46
46
  MEDUSA_CAPABILITIES: () => MEDUSA_CAPABILITIES,
47
47
  MOCK_CAPABILITIES: () => MOCK_CAPABILITIES,
48
+ MOCK_SALES_CHANNEL_ID: () => MOCK_SALES_CHANNEL_ID,
48
49
  MagentoProvider: () => MagentoProvider,
49
50
  MedusaMetadataAdapter: () => MedusaMetadataAdapter,
50
51
  MedusaProvider: () => MedusaProvider,
51
52
  MetricsService: () => MetricsService,
52
53
  MockProvider: () => MockProvider,
54
+ PROVIDER_METADATA: () => PROVIDER_METADATA,
53
55
  ProviderAPIError: () => ProviderAPIError,
54
56
  ProviderNotFoundError: () => ProviderNotFoundError,
55
57
  ResourceNotFoundError: () => ResourceNotFoundError,
@@ -59,7 +61,8 @@ __export(src_exports, {
59
61
  ShopifyProvider: () => ShopifyProvider,
60
62
  SpreeProvider: () => SpreeProvider,
61
63
  StelOrderProvider: () => StelOrderProvider,
62
- StorefrontConfigError: () => import_types2.StorefrontConfigError,
64
+ StorefrontConfigError: () => StorefrontConfigError,
65
+ SupportedProviderType: () => SupportedProviderType,
63
66
  WOOCOMMERCE_CAPABILITIES: () => WOOCOMMERCE_CAPABILITIES,
64
67
  WooCommerceProvider: () => WooCommerceProvider,
65
68
  assertEntityBelongsToChannel: () => assertEntityBelongsToChannel,
@@ -78,10 +81,12 @@ __export(src_exports, {
78
81
  getBackendProviderId: () => getBackendProviderId,
79
82
  getCapabilityDescription: () => getCapabilityDescription,
80
83
  getChannelScope: () => getChannelScope,
84
+ getProviderMetadata: () => getProviderMetadata,
81
85
  isBackendCapabilities: () => isBackendCapabilities,
82
86
  isGlobalEntity: () => isGlobalEntity,
83
87
  isProviderType: () => isProviderType,
84
88
  isScopedToChannel: () => isScopedToChannel,
89
+ isSupportedProviderType: () => isSupportedProviderType,
85
90
  moneyToNumber: () => moneyToNumber,
86
91
  moneyToNumberSafe: () => moneyToNumberSafe,
87
92
  numberToMoney: () => numberToMoney,
@@ -98,7 +103,6 @@ __export(src_exports, {
98
103
  validatePaymentId: () => validatePaymentId
99
104
  });
100
105
  module.exports = __toCommonJS(src_exports);
101
- var import_types2 = require("@thorprovider/types");
102
106
 
103
107
  // src/types/capabilities.types.ts
104
108
  var MOCK_CAPABILITIES = {
@@ -463,6 +467,16 @@ var ConfigurationError = class extends CommerceError {
463
467
  this.name = "ConfigurationError";
464
468
  }
465
469
  };
470
+ var StorefrontConfigError = class extends Error {
471
+ platformType;
472
+ requiredEnvVar;
473
+ constructor(message, platformType, requiredEnvVar) {
474
+ super(message);
475
+ this.name = "StorefrontConfigError";
476
+ this.platformType = platformType;
477
+ this.requiredEnvVar = requiredEnvVar;
478
+ }
479
+ };
466
480
  var ResourceNotFoundError = class extends CommerceError {
467
481
  constructor(resource, identifier) {
468
482
  super(`${resource} not found: ${identifier}`, "RESOURCE_NOT_FOUND", 404);
@@ -997,6 +1011,16 @@ function transformMoney(amount, currencyCode = "USD") {
997
1011
  currencyCode: currencyCode.toUpperCase()
998
1012
  };
999
1013
  }
1014
+ function transformAmount(amount, currencyCode) {
1015
+ const currencyCodeUpper = (currencyCode ?? "").toUpperCase();
1016
+ if (typeof amount !== "number" || !Number.isFinite(amount)) {
1017
+ return { amount: "", currencyCode: currencyCodeUpper };
1018
+ }
1019
+ return { amount: amount.toFixed(2), currencyCode: currencyCodeUpper };
1020
+ }
1021
+ function transformProductPrice(amount, currencyCode) {
1022
+ return transformAmount(amount, currencyCode);
1023
+ }
1000
1024
  function transformImage(url, alt = "") {
1001
1025
  return {
1002
1026
  url,
@@ -1048,39 +1072,42 @@ function transformProductOption(option) {
1048
1072
  values: option.values?.map((v) => v.value ?? "") ?? []
1049
1073
  };
1050
1074
  }
1051
- function transformProductVariant(variant, currencyCode = "USD") {
1075
+ function transformProductVariant(variant, currencyCode) {
1052
1076
  let priceAmount;
1053
1077
  let priceCurrency = currencyCode;
1054
1078
  if (variant.calculated_price) {
1055
1079
  priceAmount = variant.calculated_price.calculated_amount;
1056
- priceCurrency = variant.calculated_price.currency_code || currencyCode;
1080
+ priceCurrency = variant.calculated_price.currency_code ?? currencyCode;
1057
1081
  } else if (variant.prices && variant.prices.length > 0) {
1058
- const price = variant.prices.find((p) => p.currency_code === currencyCode.toLowerCase());
1082
+ const target = currencyCode?.toLowerCase();
1083
+ const price = (target ? variant.prices.find((p) => p.currency_code === target) : void 0) ?? variant.prices[0];
1059
1084
  priceAmount = price?.amount;
1060
- priceCurrency = price?.currency_code || currencyCode;
1085
+ priceCurrency = price?.currency_code ?? currencyCode;
1061
1086
  }
1062
1087
  const isAvailable = variant.manage_inventory === false ? true : (variant.inventory_quantity ?? 0) > 0 || variant.allow_backorder === true;
1063
1088
  return {
1064
1089
  id: variant.id,
1065
1090
  title: variant.title ?? "Default",
1091
+ // SKU is optional descriptive data only — never API/purchase identity.
1066
1092
  sku: variant.sku ?? void 0,
1067
1093
  availableForSale: isAvailable,
1068
1094
  selectedOptions: variant.options?.map((opt) => ({
1069
1095
  name: opt.option?.title ?? "",
1070
1096
  value: opt.value ?? ""
1071
1097
  })) ?? [],
1072
- price: transformMoney(priceAmount, priceCurrency),
1098
+ price: transformProductPrice(priceAmount, priceCurrency),
1073
1099
  compareAtPrice: void 0,
1074
1100
  // Medusa doesn't have compare-at pricing
1075
1101
  quantityAvailable: variant.manage_inventory ? variant.inventory_quantity : void 0,
1076
1102
  image: variant.thumbnail ? transformImage(variant.thumbnail, variant.title ?? "") : void 0
1077
1103
  };
1078
1104
  }
1079
- function transformProduct(product, currencyCode = "USD") {
1105
+ function transformProduct(product, currencyCode) {
1080
1106
  const variants = product.variants?.map((v) => transformProductVariant(v, currencyCode)) ?? [];
1081
- const prices = variants.map((v) => parseFloat(v.price.amount));
1082
- const minPrice = Math.min(...prices);
1083
- const maxPrice = Math.max(...prices);
1107
+ const validPrices = variants.map((v) => v.price).filter((money) => money.amount !== "" && Number.isFinite(Number(money.amount))).map((money) => Number(money.amount));
1108
+ const rangeCurrency = variants.find((v) => v.price.currencyCode)?.price.currencyCode ?? (currencyCode ?? "").toUpperCase();
1109
+ const minPrice = validPrices.length > 0 ? Math.min(...validPrices) : void 0;
1110
+ const maxPrice = validPrices.length > 0 ? Math.max(...validPrices) : void 0;
1084
1111
  return {
1085
1112
  id: product.id,
1086
1113
  handle: product.handle ?? "",
@@ -1096,10 +1123,16 @@ function transformProduct(product, currencyCode = "USD") {
1096
1123
  images: product.images?.map((img) => transformImage(img.url ?? "", product.title ?? "")) ?? [],
1097
1124
  priceRange: {
1098
1125
  // Build Money directly from already-resolved major-unit prices.
1099
- // Do NOT pass through transformMoney (which would call toFixed on already-correct values)
1100
- // and do NOT multiply by 100 (that was a compensatory hack for the old /100 bug).
1101
- minVariantPrice: { amount: (Number.isFinite(minPrice) ? minPrice : 0).toFixed(2), currencyCode: currencyCode.toUpperCase() },
1102
- maxVariantPrice: { amount: (Number.isFinite(maxPrice) ? maxPrice : 0).toFixed(2), currencyCode: currencyCode.toUpperCase() }
1126
+ // Do NOT multiply by 100 (that was a compensatory hack for the old /100
1127
+ // bug) and do NOT fabricate a zero for an unresolved price.
1128
+ minVariantPrice: {
1129
+ amount: minPrice !== void 0 ? minPrice.toFixed(2) : "",
1130
+ currencyCode: rangeCurrency
1131
+ },
1132
+ maxVariantPrice: {
1133
+ amount: maxPrice !== void 0 ? maxPrice.toFixed(2) : "",
1134
+ currencyCode: rangeCurrency
1135
+ }
1103
1136
  },
1104
1137
  collections: product.collection ? [{
1105
1138
  id: product.collection.id,
@@ -1122,38 +1155,19 @@ function transformProduct(product, currencyCode = "USD") {
1122
1155
  function resolveMedusaCartItemCurrencyCode(item, cartCurrencyCode) {
1123
1156
  return item?.cart?.currency_code ?? cartCurrencyCode ?? "usd";
1124
1157
  }
1125
- function resolveMedusaCartItemTotal(item) {
1126
- const quantity = item?.quantity ?? 1;
1127
- const safeQuantity = typeof quantity === "number" && quantity > 0 ? quantity : 1;
1128
- const totalCandidates = [
1129
- item?.total,
1130
- item?.item_total,
1131
- item?.original_total,
1132
- item?.subtotal,
1133
- item?.item_subtotal,
1134
- item?.original_subtotal
1135
- ];
1136
- for (const candidate of totalCandidates) {
1137
- if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0) {
1158
+ function resolveAuthoritativeLineTotal(item) {
1159
+ const candidates = [item?.total, item?.item_total, item?.subtotal];
1160
+ for (const candidate of candidates) {
1161
+ if (typeof candidate === "number" && Number.isFinite(candidate)) {
1138
1162
  return candidate;
1139
1163
  }
1140
1164
  }
1141
- const unitCandidates = [
1142
- item?.unit_price,
1143
- item?.original_unit_price
1144
- ];
1145
- for (const candidate of unitCandidates) {
1146
- if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0) {
1147
- return candidate * safeQuantity;
1148
- }
1149
- }
1150
- return 0;
1165
+ return void 0;
1151
1166
  }
1152
1167
  function transformCartItem(item, cartCurrencyCode) {
1153
1168
  const variant = item.variant;
1154
1169
  const product = variant?.product;
1155
1170
  const currencyCode = resolveMedusaCartItemCurrencyCode(item, cartCurrencyCode);
1156
- const totalCents = resolveMedusaCartItemTotal(item);
1157
1171
  const merchandiseId = variant?.id ?? item.variant_id ?? "";
1158
1172
  const merchandiseTitle = variant?.title ?? item.variant_title ?? "";
1159
1173
  const productId = product?.id ?? item.product_id ?? "";
@@ -1164,7 +1178,9 @@ function transformCartItem(item, cartCurrencyCode) {
1164
1178
  id: item.id,
1165
1179
  quantity: item.quantity ?? 1,
1166
1180
  cost: {
1167
- totalAmount: transformMoney(totalCents, currencyCode)
1181
+ totalAmount: transformAmount(resolveAuthoritativeLineTotal(item), currencyCode),
1182
+ // Backend unit price, carried so the display never derives it locally.
1183
+ unitPrice: transformAmount(item?.unit_price, currencyCode)
1168
1184
  },
1169
1185
  merchandise: {
1170
1186
  id: merchandiseId,
@@ -1183,19 +1199,30 @@ function transformCartItem(item, cartCurrencyCode) {
1183
1199
  };
1184
1200
  }
1185
1201
  function transformCart(cart) {
1186
- const currencyCode = cart.currency_code ?? "usd";
1202
+ const currencyCode = cart.currency_code;
1187
1203
  return {
1188
1204
  id: cart.id,
1189
1205
  checkoutUrl: `/checkout/${cart.id}`,
1190
1206
  // Medusa doesn't have a direct checkout URL
1191
1207
  cost: {
1192
- subtotalAmount: transformMoney(cart.subtotal, currencyCode),
1193
- totalAmount: transformMoney(cart.total, currencyCode),
1194
- totalTaxAmount: transformMoney(cart.tax_total, currencyCode)
1208
+ subtotalAmount: transformAmount(
1209
+ typeof cart.item_subtotal === "number" ? cart.item_subtotal : cart.subtotal,
1210
+ currencyCode
1211
+ ),
1212
+ discountAmount: transformAmount(cart.discount_total, currencyCode),
1213
+ totalTaxAmount: transformAmount(cart.tax_total, currencyCode),
1214
+ shippingAmount: transformAmount(cart.shipping_total, currencyCode),
1215
+ totalAmount: transformAmount(cart.total, currencyCode)
1195
1216
  },
1196
1217
  lines: cart.items?.map((item) => transformCartItem(item, currencyCode)) ?? [],
1197
1218
  totalQuantity: cart.items?.reduce((sum, item) => sum + (item.quantity ?? 0), 0) ?? 0,
1198
- completedAt: cart.completed_at ? typeof cart.completed_at === "string" ? cart.completed_at : cart.completed_at?.toISOString() : void 0
1219
+ salesChannelId: cart.sales_channel_id ?? void 0,
1220
+ regionId: cart.region_id ?? void 0,
1221
+ currencyCode: typeof currencyCode === "string" ? currencyCode.toUpperCase() : void 0,
1222
+ selectedShippingMethodId: cart.shipping_methods?.[0]?.id ?? void 0,
1223
+ selectedShippingOptionId: cart.shipping_methods?.[0]?.shipping_option_id ?? void 0,
1224
+ completedAt: cart.completed_at ? typeof cart.completed_at === "string" ? cart.completed_at : cart.completed_at?.toISOString() : void 0,
1225
+ metadata: cart.metadata ?? void 0
1199
1226
  };
1200
1227
  }
1201
1228
  function transformCollection(collection) {
@@ -1284,8 +1311,8 @@ function transformOrderItem(item, currencyCode) {
1284
1311
  title: item.product_title ?? item.title ?? "",
1285
1312
  variantTitle: item.variant_title ?? variant?.title ?? void 0,
1286
1313
  quantity: item.quantity ?? 1,
1287
- price: transformMoney(item.unit_price, currencyCode),
1288
- total: transformMoney(item.total, currencyCode),
1314
+ price: transformAmount(item.unit_price, currencyCode),
1315
+ total: transformAmount(item.total, currencyCode),
1289
1316
  sku: variant?.sku ?? void 0,
1290
1317
  image: product?.thumbnail || item.thumbnail ? transformImage(product?.thumbnail || item.thumbnail, item.product_title ?? "") : void 0,
1291
1318
  productId: item.product_id ?? product?.id ?? void 0,
@@ -1336,12 +1363,19 @@ function transformOrder(order) {
1336
1363
  customer: order.customer ? transformCustomer(order.customer, order.email) : void 0,
1337
1364
  // Items
1338
1365
  items: (order.items ?? []).filter((item) => item != null).map((item) => transformOrderItem(item, currencyCode)) ?? [],
1339
- // Pricing
1340
- subtotal: transformMoney(order.subtotal, currencyCode),
1341
- total: transformMoney(order.total, currencyCode),
1342
- tax: transformMoney(order.tax_total, currencyCode),
1343
- shipping: transformMoney(order.shipping_total, currencyCode),
1344
- discount: transformMoney(order.discount_total, currencyCode),
1366
+ // Pricing — backend-authoritative; a legitimate zero is preserved and no
1367
+ // amount is derived locally. `subtotal` prefers the backend's
1368
+ // `item_subtotal` (lines only, excluding shipping/tax); Medusa's native
1369
+ // `subtotal` folds in `shipping_subtotal`, so falling back to it is only
1370
+ // for installs that do not supply `item_subtotal`.
1371
+ subtotal: transformAmount(
1372
+ typeof order.item_subtotal === "number" ? order.item_subtotal : order.subtotal,
1373
+ currencyCode
1374
+ ),
1375
+ total: transformAmount(order.total, currencyCode),
1376
+ tax: transformAmount(order.tax_total, currencyCode),
1377
+ shipping: transformAmount(order.shipping_total, currencyCode),
1378
+ discount: transformAmount(order.discount_total, currencyCode),
1345
1379
  // Addresses (transformed to camelCase)
1346
1380
  shippingAddress: order.shipping_address ? transformAddress(order.shipping_address) : void 0,
1347
1381
  billingAddress: order.billing_address ? transformAddress(order.billing_address) : void 0,
@@ -1760,7 +1794,6 @@ function assertEntityBelongsToChannel(entity, expectedChannelId) {
1760
1794
 
1761
1795
  // src/providers/medusa/index.ts
1762
1796
  var import_js_sdk2 = __toESM(require("@medusajs/js-sdk"));
1763
- var import_types = require("@thorprovider/types");
1764
1797
 
1765
1798
  // src/providers/medusa/auth.ts
1766
1799
  var AuthStorageManager = class {
@@ -2290,10 +2323,156 @@ async function getPaymentMethods(sdk, options) {
2290
2323
  }
2291
2324
 
2292
2325
  // src/providers/medusa/index.ts
2293
- var MEDUSA_CART_RETRIEVE_FIELDS = "+items,+items.variant,+items.variant.product,+items.variant.product.thumbnail,+items.variant.options,+items.variant.options.option";
2326
+ var MEDUSA_CART_RETRIEVE_FIELDS = "+items,+items.variant,+items.variant.product,+items.variant.product.thumbnail,+items.variant.options,+items.variant.options.option,+items.total,+items.unit_price,+items.item_subtotal,+items.item_total,+shipping_methods,+shipping_methods.id,+shipping_methods.shipping_option_id";
2327
+ var MEDUSA_ORDER_COMPLETION_FIELDS = "*items";
2328
+ var DEFAULT_CATALOG_PAGE_SIZE = 50;
2329
+ function resolveCatalogPageSize(configured) {
2330
+ const fromEnv = process.env.MEDUSA_CATALOG_PAGE_SIZE !== void 0 ? Number(process.env.MEDUSA_CATALOG_PAGE_SIZE) : void 0;
2331
+ const raw = configured ?? fromEnv;
2332
+ if (raw === void 0 || !Number.isFinite(raw) || raw < 1) {
2333
+ return DEFAULT_CATALOG_PAGE_SIZE;
2334
+ }
2335
+ return Math.floor(raw);
2336
+ }
2337
+ function resolveConfiguredSalesChannelId() {
2338
+ return process.env.MEDUSA_SALES_CHANNEL_ID ?? process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
2339
+ }
2340
+ var MEDUSA_SORT_KEY_MAP = {
2341
+ featured: null,
2342
+ name: "title",
2343
+ newest: "-created_at",
2344
+ "price-asc": null,
2345
+ "price-desc": null,
2346
+ rating: null
2347
+ };
2348
+ function resolveEffectiveCategoryHandles(query) {
2349
+ const { categoryScope, categories } = query;
2350
+ if (categoryScope === void 0) return categories;
2351
+ if (categories === void 0) return [categoryScope];
2352
+ return categories.filter((handle) => handle === categoryScope);
2353
+ }
2354
+ function buildAppliedCatalogFilters(query, effectiveCategoryHandles, includeAdvanced = false) {
2355
+ return {
2356
+ ...query.text ? { text: query.text } : {},
2357
+ ...effectiveCategoryHandles !== void 0 ? { categories: effectiveCategoryHandles } : {},
2358
+ ...query.collections !== void 0 ? { collections: query.collections } : {},
2359
+ ...includeAdvanced && query.priceMin !== void 0 ? { priceMin: query.priceMin } : {},
2360
+ ...includeAdvanced && query.priceMax !== void 0 ? { priceMax: query.priceMax } : {},
2361
+ ...includeAdvanced && query.inStock !== void 0 ? { inStock: query.inStock } : {},
2362
+ ...includeAdvanced && query.attributes !== void 0 ? { attributes: query.attributes } : {}
2363
+ };
2364
+ }
2365
+ function buildBasicDemonstratedQuery(query) {
2366
+ return {
2367
+ ...query.text !== void 0 ? { text: query.text } : {},
2368
+ ...query.categoryScope !== void 0 ? { categoryScope: query.categoryScope } : {},
2369
+ ...query.categories !== void 0 ? { categories: query.categories } : {},
2370
+ ...query.collections !== void 0 ? { collections: query.collections } : {}
2371
+ };
2372
+ }
2373
+ var ADVANCED_SEARCH_ENDPOINT = "/store/search";
2374
+ var BASIC_LISTING_ENDPOINT = "/store/products";
2375
+ function collectRequestedAdvancedFields(query) {
2376
+ const fields = [];
2377
+ if (query.priceMin !== void 0) fields.push("priceMin");
2378
+ if (query.priceMax !== void 0) fields.push("priceMax");
2379
+ if (query.inStock !== void 0) fields.push("inStock");
2380
+ for (const name of Object.keys(query.attributes ?? {})) {
2381
+ fields.push(`attr.${name}`);
2382
+ }
2383
+ return fields;
2384
+ }
2385
+ function advancedFieldToFacet(field) {
2386
+ if (field === "priceMin" || field === "priceMax") return "price";
2387
+ return field;
2388
+ }
2389
+ function collectAdvancedSearchRequestFields(options) {
2390
+ const fields = [];
2391
+ if (options?.priceMin !== void 0) fields.push("priceMin");
2392
+ if (options?.priceMax !== void 0) fields.push("priceMax");
2393
+ if (options?.inStockOnly === true) fields.push("inStock");
2394
+ if (options?.attributes && options.attributes.length > 0) {
2395
+ fields.push("attributes");
2396
+ }
2397
+ return fields;
2398
+ }
2399
+ function toFiniteNumber(value) {
2400
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2401
+ if (typeof value === "string" && value.trim() !== "") {
2402
+ const parsed = Number(value);
2403
+ return Number.isFinite(parsed) ? parsed : void 0;
2404
+ }
2405
+ return void 0;
2406
+ }
2407
+ function readAppliedPriceRange(applied) {
2408
+ const range = applied.price_range;
2409
+ if (!range || typeof range !== "object" || Array.isArray(range)) return {};
2410
+ const record = range;
2411
+ return { min: toFiniteNumber(record.min), max: toFiniteNumber(record.max) };
2412
+ }
2413
+ function mapAppliedFiltersToFacets(applied) {
2414
+ const facets = /* @__PURE__ */ new Set();
2415
+ const priceRange = readAppliedPriceRange(applied);
2416
+ const hasAppliedPriceRange = priceRange.min !== void 0 && priceRange.max !== void 0;
2417
+ if (hasAppliedPriceRange || "price" in applied || "price_min" in applied || "price_max" in applied) {
2418
+ facets.add("price");
2419
+ }
2420
+ if (typeof applied.is_available === "boolean" || "inStock" in applied || "isAvailable" in applied || "available" in applied) {
2421
+ facets.add("inStock");
2422
+ }
2423
+ const attributes = applied.attributes;
2424
+ if (attributes && typeof attributes === "object" && !Array.isArray(attributes)) {
2425
+ for (const name of Object.keys(attributes)) {
2426
+ facets.add(`attr.${name}`);
2427
+ }
2428
+ }
2429
+ return facets;
2430
+ }
2431
+ function collectUnappliedAdvancedFields(applied, query) {
2432
+ const unapplied = [];
2433
+ const priceRange = readAppliedPriceRange(applied);
2434
+ if (query.priceMin !== void 0) {
2435
+ const echoed = toFiniteNumber(
2436
+ priceRange.min ?? applied.price_min ?? applied.priceMin
2437
+ );
2438
+ if (echoed !== query.priceMin) unapplied.push("priceMin");
2439
+ }
2440
+ if (query.priceMax !== void 0) {
2441
+ const echoed = toFiniteNumber(
2442
+ priceRange.max ?? applied.price_max ?? applied.priceMax
2443
+ );
2444
+ if (echoed !== query.priceMax) unapplied.push("priceMax");
2445
+ }
2446
+ if (query.inStock === true) {
2447
+ const echoed = applied.is_available ?? applied.isAvailable ?? applied.inStock ?? applied.available;
2448
+ if (echoed !== true && echoed !== "true") unapplied.push("inStock");
2449
+ }
2450
+ const requestedAttributes = query.attributes ?? {};
2451
+ const appliedAttributes = applied.attributes;
2452
+ for (const name of Object.keys(requestedAttributes)) {
2453
+ const hasName = !!appliedAttributes && typeof appliedAttributes === "object" && !Array.isArray(appliedAttributes) && name in appliedAttributes;
2454
+ if (!hasName) unapplied.push(`attr.${name}`);
2455
+ }
2456
+ return unapplied;
2457
+ }
2458
+ function readFiniteFacetNumber(value) {
2459
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2460
+ }
2294
2461
  var MedusaProvider = class {
2295
2462
  name = "medusa";
2296
2463
  version = "2.0.0";
2464
+ /**
2465
+ * Canonical catalog sort keys this provider applies at the query level.
2466
+ *
2467
+ * Advertises only the backend-verified orderings (E2 Decision 6). `price-asc`,
2468
+ * `price-desc`, and `rating` are intentionally absent: they are not applied at
2469
+ * the query level, so the listing never offers them as global sorts.
2470
+ *
2471
+ * The literal tuple is assignable to `CommerceProvider['supportedCatalogSortKeys']`
2472
+ * (`readonly CatalogSortKey[]`); it is written structurally here so the adapter
2473
+ * compiles against the published `@thorprovider/types` declaration.
2474
+ */
2475
+ supportedCatalogSortKeys = ["featured", "name", "newest"];
2297
2476
  /**
2298
2477
  * Medusa backend capabilities.
2299
2478
  * Starts with static defaults, replaced by dynamic values from
@@ -2315,6 +2494,14 @@ var MedusaProvider = class {
2315
2494
  baseUrl;
2316
2495
  publishableKey;
2317
2496
  regionId;
2497
+ /** Configured per-request catalog listing bound (never an unbounded scan). */
2498
+ catalogPageSize;
2499
+ /**
2500
+ * Per-scope cache of the truthful facet-support descriptor. The optional
2501
+ * advanced endpoint is probed once per demonstrated scope/query rather than on
2502
+ * every listing request (E2 task 2.4).
2503
+ */
2504
+ facetSupportCache = /* @__PURE__ */ new Map();
2318
2505
  /** Secret API key for admin operations (if configured). */
2319
2506
  adminApiKey;
2320
2507
  /**
@@ -2359,6 +2546,7 @@ var MedusaProvider = class {
2359
2546
  this.baseUrl = config.baseUrl;
2360
2547
  this.publishableKey = config.publishableKey;
2361
2548
  this.regionId = config.regionId;
2549
+ this.catalogPageSize = resolveCatalogPageSize(config.catalogPageSize);
2362
2550
  this.retryConfig = {
2363
2551
  maxRetries: config.retry?.maxRetries ?? 3,
2364
2552
  timeoutMs: config.retry?.timeoutMs ?? 1e4,
@@ -2380,7 +2568,16 @@ var MedusaProvider = class {
2380
2568
  debug: config.debug ?? false,
2381
2569
  // Always use JWT mode for auth - SDK needs to send Bearer tokens
2382
2570
  // Configure storage method for JWT tokens (browser only)
2383
- auth: config.auth ? {
2571
+ auth: config.auth ? config.auth.customStorage ? {
2572
+ type: "jwt",
2573
+ // Request-scoped custom storage: the caller supplies a per-request
2574
+ // token holder (for example a server request's customer JWT). The
2575
+ // SDK reads the token on every request and writes the login /
2576
+ // registration token back into the same holder, so a fresh
2577
+ // provider per request is authenticated without shared state.
2578
+ jwtTokenStorageMethod: "custom",
2579
+ storage: config.auth.customStorage
2580
+ } : {
2384
2581
  type: "jwt",
2385
2582
  // Use localStorage in browser, memory storage on server
2386
2583
  jwtTokenStorageMethod: typeof window !== "undefined" ? "local" : "memory"
@@ -2488,35 +2685,52 @@ var MedusaProvider = class {
2488
2685
  }
2489
2686
  throw lastError;
2490
2687
  }
2688
+ /** Fields needed to price a product variant for the current region. */
2689
+ buildProductFields(regionId) {
2690
+ const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+tags";
2691
+ const pricingFields = ",+variants.calculated_price,+variants.prices";
2692
+ return regionId ? baseFields + pricingFields : baseFields;
2693
+ }
2694
+ /** Scope a store query to the configured storefront sales channel, if any. */
2695
+ applySalesChannelScope(queryParams) {
2696
+ const salesChannelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
2697
+ if (salesChannelId) {
2698
+ queryParams.sales_channel_id = [salesChannelId];
2699
+ }
2700
+ }
2701
+ /**
2702
+ * Directed single-product lookup by a supported native filter (`handle` or
2703
+ * `id`). This is independent of catalog listing pagination, so a known
2704
+ * product beyond the first listing page still resolves without scanning.
2705
+ *
2706
+ * Medusa's store API supports `handle` and `id` on `/store/products`; it
2707
+ * does not expose a variant filter there, so a variant is resolved from the
2708
+ * product once the product is known (see `getProductDetailByHandle` in the
2709
+ * runtime). No `limit` beyond 1 is issued.
2710
+ */
2711
+ async fetchSingleProduct(filter, options) {
2712
+ const regionId = options?.regionId ?? this.regionId;
2713
+ const queryParams = {
2714
+ ...filter,
2715
+ limit: 1,
2716
+ fields: this.buildProductFields(regionId)
2717
+ };
2718
+ if (regionId) {
2719
+ queryParams.region_id = regionId;
2720
+ }
2721
+ this.applySalesChannelScope(queryParams);
2722
+ const { products } = await this.sdk.store.product.list(queryParams);
2723
+ if (!products || products.length === 0) {
2724
+ this.logger.debug("Product not found", filter);
2725
+ return void 0;
2726
+ }
2727
+ return transformProduct(products[0], this.currencyCode);
2728
+ }
2491
2729
  async getProduct(handle, options) {
2492
2730
  this.logger.debug("getProduct", { handle, regionId: options?.regionId });
2493
2731
  try {
2494
2732
  return await this.withRetry(
2495
- async () => {
2496
- const regionId = options?.regionId ?? this.regionId;
2497
- const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+tags";
2498
- const pricingFields = ",+variants.calculated_price,+variants.prices";
2499
- const fields = regionId ? baseFields + pricingFields : baseFields;
2500
- const queryParams = {
2501
- handle,
2502
- limit: 1,
2503
- fields
2504
- };
2505
- if (regionId) {
2506
- queryParams.region_id = regionId;
2507
- }
2508
- const salesChannelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
2509
- if (salesChannelId) {
2510
- queryParams.sales_channel_id = [salesChannelId];
2511
- }
2512
- const { products } = await this.sdk.store.product.list(queryParams);
2513
- if (!products || products.length === 0) {
2514
- this.logger.debug("Product not found", { handle });
2515
- return void 0;
2516
- }
2517
- const transformedProduct = transformProduct(products[0], this.currencyCode);
2518
- return transformedProduct;
2519
- },
2733
+ () => this.fetchSingleProduct({ handle }, options),
2520
2734
  "getProduct",
2521
2735
  { handle, regionId: options?.regionId }
2522
2736
  );
@@ -2528,6 +2742,26 @@ var MedusaProvider = class {
2528
2742
  );
2529
2743
  }
2530
2744
  }
2745
+ /**
2746
+ * Directed product lookup by backend product ID. Independent of the first
2747
+ * listing page, mirroring `getProduct` for handle-based resolution.
2748
+ */
2749
+ async getProductById(id, options) {
2750
+ this.logger.debug("getProductById", { id, regionId: options?.regionId });
2751
+ try {
2752
+ return await this.withRetry(
2753
+ () => this.fetchSingleProduct({ id }, options),
2754
+ "getProductById",
2755
+ { id, regionId: options?.regionId }
2756
+ );
2757
+ } catch (error) {
2758
+ throw new ProviderAPIError(
2759
+ `Failed to fetch product by id: ${error.message}`,
2760
+ "medusa",
2761
+ error.status
2762
+ );
2763
+ }
2764
+ }
2531
2765
  async getProducts(options = {}) {
2532
2766
  this.logger.debug("getProducts", options);
2533
2767
  try {
@@ -2537,28 +2771,14 @@ var MedusaProvider = class {
2537
2771
  const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+tags";
2538
2772
  const pricingFields = ",+variants.calculated_price,+variants.prices";
2539
2773
  const fields = regionId ? baseFields + pricingFields : baseFields;
2540
- const SORT_KEY_MAP = {
2541
- featured: null,
2542
- // server default order
2543
- name: "title",
2544
- // A→Z by product title
2545
- newest: "-created_at",
2546
- // newest products first
2547
- "price-asc": null,
2548
- // not supported by native endpoint → client-side
2549
- "price-desc": null,
2550
- // not supported by native endpoint → client-side
2551
- rating: null
2552
- // not supported → server default
2553
- };
2554
2774
  const queryParams = {
2555
2775
  q: options.query,
2556
- limit: options.first ?? 100,
2557
- offset: options.after ? parseInt(String(options.after), 10) : void 0,
2776
+ limit: options.first ?? this.catalogPageSize,
2777
+ offset: options.after !== void 0 ? parseInt(String(options.after), 10) : options.skip,
2558
2778
  fields
2559
2779
  };
2560
2780
  if (options.sortKey) {
2561
- const mappedOrder = SORT_KEY_MAP[options.sortKey] ?? null;
2781
+ const mappedOrder = MEDUSA_SORT_KEY_MAP[options.sortKey] ?? null;
2562
2782
  if (mappedOrder !== null) {
2563
2783
  queryParams.order = mappedOrder;
2564
2784
  }
@@ -2598,6 +2818,468 @@ var MedusaProvider = class {
2598
2818
  );
2599
2819
  }
2600
2820
  }
2821
+ /**
2822
+ * Canonical catalog query path (E2 task 2.2).
2823
+ *
2824
+ * Accepts a validated `CatalogQuery`, translates it into Medusa Store
2825
+ * `/store/products` parameters, and returns a truthful `CatalogQueryOutcome`.
2826
+ * The route category scope is always retained and intersected with the
2827
+ * `categories` filter; `categories`/`collections` handles are resolved to
2828
+ * backend IDs through the existing directed lookups (`getCategory`/
2829
+ * `getCollection`). Any requested filter this provider cannot apply
2830
+ * (price/`inStock`/attributes) is rejected as a typed `unsupported-query`
2831
+ * outcome naming the fields — never a partial success and never a silent
2832
+ * drop. Pagination is `offset = (page - 1) * pageSize`, defaulting to the
2833
+ * canonical page size.
2834
+ *
2835
+ * Legacy `getProducts`/`getCategoryProducts` are unchanged; this additive
2836
+ * method is the typed seam the runtime query bridge (E2 task 3.1) consumes.
2837
+ */
2838
+ async getCatalogProducts(query) {
2839
+ this.logger.debug("getCatalogProducts", query);
2840
+ if (query.sort !== void 0 && !this.supportedCatalogSortKeys.includes(query.sort)) {
2841
+ return this.unsupportedCatalogOutcome(["sort"]);
2842
+ }
2843
+ const effectiveCategoryHandles = resolveEffectiveCategoryHandles(query);
2844
+ try {
2845
+ const categoryIds = effectiveCategoryHandles !== void 0 ? await this.resolveCategoryIds(effectiveCategoryHandles) : void 0;
2846
+ const collectionIds = query.collections ? await this.resolveCollectionIds(query.collections) : void 0;
2847
+ const facetSupport = await this.getCatalogFacetSupport({
2848
+ query,
2849
+ categoryIds,
2850
+ collectionIds,
2851
+ scope: query.categoryScope
2852
+ });
2853
+ const requestedAdvanced = collectRequestedAdvancedFields(query);
2854
+ const heldAdvanced = requestedAdvanced.filter(
2855
+ (field) => !facetSupport.offered.includes(advancedFieldToFacet(field))
2856
+ );
2857
+ if (heldAdvanced.length > 0) {
2858
+ return this.unsupportedCatalogOutcome(heldAdvanced);
2859
+ }
2860
+ const emptyIntersection = effectiveCategoryHandles !== void 0 && effectiveCategoryHandles.length === 0;
2861
+ const categoryFilterEmpty = effectiveCategoryHandles !== void 0 && (categoryIds?.length ?? 0) === 0;
2862
+ const collectionFilterEmpty = query.collections !== void 0 && (collectionIds?.length ?? 0) === 0;
2863
+ if (emptyIntersection || categoryFilterEmpty || collectionFilterEmpty) {
2864
+ return {
2865
+ ok: true,
2866
+ results: {
2867
+ products: [],
2868
+ appliedFilters: buildAppliedCatalogFilters(
2869
+ query,
2870
+ effectiveCategoryHandles,
2871
+ requestedAdvanced.length > 0
2872
+ ),
2873
+ facetSupport
2874
+ }
2875
+ };
2876
+ }
2877
+ if (requestedAdvanced.length > 0) {
2878
+ return this.queryAdvancedCatalog(
2879
+ query,
2880
+ categoryIds,
2881
+ collectionIds,
2882
+ facetSupport
2883
+ );
2884
+ }
2885
+ return await this.queryBasicCatalog(
2886
+ query,
2887
+ effectiveCategoryHandles,
2888
+ categoryIds,
2889
+ collectionIds,
2890
+ facetSupport
2891
+ );
2892
+ } catch (error) {
2893
+ this.logger.error("getCatalogProducts error details", {
2894
+ message: error.message,
2895
+ status: error.status
2896
+ });
2897
+ throw new ProviderAPIError(
2898
+ `Failed to run catalog query: ${error.message || "Unknown error"}`,
2899
+ "medusa",
2900
+ error.status
2901
+ );
2902
+ }
2903
+ }
2904
+ /**
2905
+ * Execute a basic-only canonical query against the equivalent basic endpoint
2906
+ * `/store/products`, reporting the filters it actually applied.
2907
+ */
2908
+ async queryBasicCatalog(query, effectiveCategoryHandles, categoryIds, collectionIds, facetSupport) {
2909
+ const pageSize = query.pageSize ?? this.catalogPageSize;
2910
+ const page = query.page ?? 1;
2911
+ const regionId = this.regionId;
2912
+ const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+thumbnail,+tags";
2913
+ const pricingFields = ",+variants.calculated_price,+variants.prices";
2914
+ const queryParams = {
2915
+ limit: pageSize,
2916
+ offset: (page - 1) * pageSize,
2917
+ fields: regionId ? baseFields + pricingFields : baseFields
2918
+ };
2919
+ if (query.text) {
2920
+ queryParams.q = query.text;
2921
+ }
2922
+ if (categoryIds && categoryIds.length > 0) {
2923
+ queryParams.category_id = categoryIds;
2924
+ }
2925
+ if (collectionIds && collectionIds.length > 0) {
2926
+ queryParams.collection_id = collectionIds;
2927
+ }
2928
+ if (query.sort) {
2929
+ const mappedOrder = MEDUSA_SORT_KEY_MAP[query.sort] ?? null;
2930
+ if (mappedOrder !== null) {
2931
+ queryParams.order = mappedOrder;
2932
+ }
2933
+ }
2934
+ if (regionId) {
2935
+ queryParams.region_id = regionId;
2936
+ }
2937
+ const salesChannelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
2938
+ if (salesChannelId) {
2939
+ queryParams.sales_channel_id = [salesChannelId];
2940
+ }
2941
+ const response = await this.sdk.store.product.list(queryParams);
2942
+ const products = (response.products ?? []).map(
2943
+ (p) => transformProduct(p, this.currencyCode)
2944
+ );
2945
+ const count = response.count;
2946
+ return {
2947
+ ok: true,
2948
+ results: {
2949
+ products,
2950
+ // Total is only the backend's actual count; never manufactured.
2951
+ ...typeof count === "number" ? { total: count } : {},
2952
+ appliedFilters: buildAppliedCatalogFilters(
2953
+ query,
2954
+ effectiveCategoryHandles
2955
+ ),
2956
+ facetSupport
2957
+ }
2958
+ };
2959
+ }
2960
+ /**
2961
+ * Execute an advanced canonical query against the real optional endpoint
2962
+ * `/store/search`, then re-validate that the response applied every requested
2963
+ * filter. A response that does not confirm a requested field, or an endpoint
2964
+ * failure, yields a typed `unsupported-query` error — never a partial result
2965
+ * labelled success and never a silent fallback to the basic endpoint.
2966
+ */
2967
+ async queryAdvancedCatalog(query, categoryIds, collectionIds, facetSupport) {
2968
+ const pageSize = query.pageSize ?? this.catalogPageSize;
2969
+ const page = query.page ?? 1;
2970
+ const advancedQuery = this.buildAdvancedSearchQuery(
2971
+ query,
2972
+ categoryIds ?? [],
2973
+ collectionIds ?? []
2974
+ );
2975
+ advancedQuery.limit = pageSize;
2976
+ advancedQuery.offset = (page - 1) * pageSize;
2977
+ let data;
2978
+ try {
2979
+ data = await this.sdk.client.fetch(ADVANCED_SEARCH_ENDPOINT, {
2980
+ query: advancedQuery
2981
+ });
2982
+ } catch (error) {
2983
+ this.logger.error("Advanced catalog listing failed", {
2984
+ message: error?.message,
2985
+ status: error?.status
2986
+ });
2987
+ return this.unsupportedCatalogOutcome(
2988
+ collectRequestedAdvancedFields(query)
2989
+ );
2990
+ }
2991
+ const appliedRaw = data?.applied_filters ?? data?.appliedFilters;
2992
+ if (!appliedRaw || typeof appliedRaw !== "object") {
2993
+ this.logger.warn("Advanced endpoint did not echo applied filters", {
2994
+ endpoint: ADVANCED_SEARCH_ENDPOINT
2995
+ });
2996
+ return this.unsupportedCatalogOutcome(
2997
+ collectRequestedAdvancedFields(query)
2998
+ );
2999
+ }
3000
+ const unapplied = collectUnappliedAdvancedFields(
3001
+ appliedRaw,
3002
+ query
3003
+ );
3004
+ if (unapplied.length > 0) {
3005
+ this.logger.warn("Advanced endpoint did not apply requested filters", {
3006
+ endpoint: ADVANCED_SEARCH_ENDPOINT,
3007
+ fields: unapplied
3008
+ });
3009
+ return this.unsupportedCatalogOutcome(unapplied);
3010
+ }
3011
+ const products = (data.hits ?? []).map(
3012
+ (hit) => transformProduct(hit, this.currencyCode)
3013
+ );
3014
+ const total = typeof data.estimatedTotalHits === "number" ? data.estimatedTotalHits : typeof data.count === "number" ? data.count : void 0;
3015
+ return {
3016
+ ok: true,
3017
+ results: {
3018
+ products,
3019
+ ...total !== void 0 ? { total } : {},
3020
+ appliedFilters: buildAppliedCatalogFilters(
3021
+ query,
3022
+ resolveEffectiveCategoryHandles(query),
3023
+ true
3024
+ ),
3025
+ facetSupport
3026
+ }
3027
+ };
3028
+ }
3029
+ /** Resolve category handles to Medusa category IDs via directed lookups. */
3030
+ async resolveCategoryIds(handles) {
3031
+ const ids = [];
3032
+ for (const handle of handles) {
3033
+ const category = await this.getCategory(handle);
3034
+ if (category) ids.push(category.id);
3035
+ }
3036
+ return ids;
3037
+ }
3038
+ /** Resolve collection handles to Medusa collection IDs via directed lookups. */
3039
+ async resolveCollectionIds(handles) {
3040
+ const ids = [];
3041
+ for (const handle of handles) {
3042
+ const collection = await this.getCollection(handle);
3043
+ if (collection) ids.push(collection.id);
3044
+ }
3045
+ return ids;
3046
+ }
3047
+ /**
3048
+ * Truthful facet-support descriptor for a scope (E2 task 2.4).
3049
+ *
3050
+ * Basic facets (`categories`, `collections`) are always offered: they are
3051
+ * applied by the canonical basic endpoint `/store/products`, and the
3052
+ * descriptor records that endpoint with the applied query and scope as their
3053
+ * demonstration source.
3054
+ *
3055
+ * Optional advanced facets (`price`, `inStock`, and advertised `attr.<name>`)
3056
+ * are offered only when the real optional `/store/search` endpoint has been
3057
+ * demonstrated with the applied query for the current scope AND the response
3058
+ * echoes the filter as applied. Otherwise they are held with the real probe
3059
+ * status/reason — a capability boolean or returned facet metadata is never
3060
+ * accepted as proof (E2 Decisions 4–5; spec "Backend support is proven against
3061
+ * the real endpoint, not assumed").
3062
+ *
3063
+ * The descriptor carries no counts or ranges and no invented timestamp; every
3064
+ * offered facet has an actual `{ endpoint, appliedQuery, scope }` source.
3065
+ */
3066
+ async getCatalogFacetSupport(options = {}) {
3067
+ const query = options.query ?? {};
3068
+ const scope = options.scope ?? query.categoryScope;
3069
+ const key = JSON.stringify({
3070
+ scope: scope ?? null,
3071
+ categoryIds: options.categoryIds ?? null,
3072
+ collectionIds: options.collectionIds ?? null,
3073
+ categories: query.categories ?? null,
3074
+ text: query.text ?? null,
3075
+ collections: query.collections ?? null,
3076
+ priceMin: query.priceMin ?? null,
3077
+ priceMax: query.priceMax ?? null,
3078
+ inStock: query.inStock ?? null,
3079
+ attributes: query.attributes ?? null
3080
+ });
3081
+ const cached = this.facetSupportCache.get(key);
3082
+ if (cached) return cached;
3083
+ const pending = this.resolveCatalogFacetSupport(
3084
+ query,
3085
+ options.categoryIds,
3086
+ options.collectionIds,
3087
+ scope
3088
+ );
3089
+ this.facetSupportCache.set(key, pending);
3090
+ return pending;
3091
+ }
3092
+ async resolveCatalogFacetSupport(query, providedCategoryIds, providedCollectionIds, scope) {
3093
+ const advancedDemonstratedQuery = {
3094
+ ...query.text !== void 0 ? { text: query.text } : {},
3095
+ ...query.categoryScope !== void 0 ? { categoryScope: query.categoryScope } : {},
3096
+ ...query.categories !== void 0 ? { categories: query.categories } : {},
3097
+ ...query.collections !== void 0 ? { collections: query.collections } : {},
3098
+ ...query.priceMin !== void 0 ? { priceMin: query.priceMin } : {},
3099
+ ...query.priceMax !== void 0 ? { priceMax: query.priceMax } : {},
3100
+ ...query.inStock !== void 0 ? { inStock: query.inStock } : {},
3101
+ ...query.attributes !== void 0 ? { attributes: query.attributes } : {}
3102
+ };
3103
+ const basicDemonstratedQuery = buildBasicDemonstratedQuery(query);
3104
+ const scopeField = scope !== void 0 ? { scope } : {};
3105
+ const offered = ["categories", "collections"];
3106
+ const demonstrations = {
3107
+ categories: {
3108
+ endpoint: BASIC_LISTING_ENDPOINT,
3109
+ appliedQuery: basicDemonstratedQuery,
3110
+ ...scopeField
3111
+ },
3112
+ collections: {
3113
+ endpoint: BASIC_LISTING_ENDPOINT,
3114
+ appliedQuery: basicDemonstratedQuery,
3115
+ ...scopeField
3116
+ }
3117
+ };
3118
+ const held = [];
3119
+ const heldReasons = {};
3120
+ const categoryIds = providedCategoryIds ?? await this.resolveScopeCategoryIds(query);
3121
+ const collectionIds = providedCollectionIds ?? (query.collections ? await this.resolveCollectionIds(query.collections) : []);
3122
+ const candidates = ["price", "inStock"];
3123
+ for (const handle of await this.resolveFilterableAttributeHandles(
3124
+ categoryIds
3125
+ )) {
3126
+ const facet = `attr.${handle}`;
3127
+ if (!candidates.includes(facet)) candidates.push(facet);
3128
+ }
3129
+ const probe = await this.probeAdvancedSearchSupport(
3130
+ query,
3131
+ categoryIds,
3132
+ collectionIds
3133
+ );
3134
+ if (probe.available) {
3135
+ for (const facet of candidates) {
3136
+ if (probe.applied.has(facet)) {
3137
+ offered.push(facet);
3138
+ demonstrations[facet] = {
3139
+ endpoint: ADVANCED_SEARCH_ENDPOINT,
3140
+ appliedQuery: advancedDemonstratedQuery,
3141
+ ...scopeField
3142
+ };
3143
+ } else {
3144
+ held.push(facet);
3145
+ heldReasons[facet] = `not echoed as applied by ${ADVANCED_SEARCH_ENDPOINT}`;
3146
+ }
3147
+ }
3148
+ } else {
3149
+ for (const facet of candidates) {
3150
+ held.push(facet);
3151
+ heldReasons[facet] = probe.reason;
3152
+ }
3153
+ }
3154
+ return {
3155
+ offered,
3156
+ held,
3157
+ demonstrations,
3158
+ ...Object.keys(heldReasons).length > 0 ? { heldReasons } : {}
3159
+ };
3160
+ }
3161
+ /**
3162
+ * Demonstrate the optional advanced endpoint with the applied query for the
3163
+ * current scope. A non-2xx response (including the HTTP 404/500 an absent
3164
+ * plugin returns), an unparseable body, or a response that does not echo the
3165
+ * applied filters all mean the facet is not demonstrated — never a silent
3166
+ * assumption of support. Uses a single SDK request (no retry): a support probe
3167
+ * must not amplify a broken optional endpoint into repeated requests.
3168
+ */
3169
+ async probeAdvancedSearchSupport(query, categoryIds, collectionIds) {
3170
+ const advancedQuery = this.buildAdvancedSearchQuery(
3171
+ query,
3172
+ categoryIds,
3173
+ collectionIds
3174
+ );
3175
+ advancedQuery.limit = 1;
3176
+ try {
3177
+ const data = await this.sdk.client.fetch(ADVANCED_SEARCH_ENDPOINT, {
3178
+ query: advancedQuery
3179
+ });
3180
+ const appliedRaw = data?.applied_filters ?? data?.appliedFilters;
3181
+ if (!appliedRaw || typeof appliedRaw !== "object") {
3182
+ return {
3183
+ available: false,
3184
+ applied: /* @__PURE__ */ new Set(),
3185
+ reason: `${ADVANCED_SEARCH_ENDPOINT} did not echo applied filters; support unverified`
3186
+ };
3187
+ }
3188
+ return {
3189
+ available: true,
3190
+ applied: mapAppliedFiltersToFacets(
3191
+ appliedRaw
3192
+ ),
3193
+ reason: `${ADVANCED_SEARCH_ENDPOINT} demonstrated with the applied query`
3194
+ };
3195
+ } catch (error) {
3196
+ const status = typeof error?.status === "number" ? error.status : void 0;
3197
+ this.logger.warn("Optional advanced search endpoint not demonstrated", {
3198
+ endpoint: ADVANCED_SEARCH_ENDPOINT,
3199
+ status,
3200
+ message: error?.message
3201
+ });
3202
+ return {
3203
+ available: false,
3204
+ applied: /* @__PURE__ */ new Set(),
3205
+ reason: status !== void 0 ? `${ADVANCED_SEARCH_ENDPOINT} returned ${status}; optional advanced endpoint not demonstrated` : `${ADVANCED_SEARCH_ENDPOINT} unavailable: ${extractErrorMessage(error)}`,
3206
+ ...status !== void 0 ? { status } : {}
3207
+ };
3208
+ }
3209
+ }
3210
+ /**
3211
+ * Build the advanced-endpoint query parameters from the canonical query. The
3212
+ * SDK serializes the plain object, so no manual stringification is needed.
3213
+ */
3214
+ buildAdvancedSearchQuery(query, categoryIds, collectionIds) {
3215
+ const advancedQuery = {};
3216
+ if (query.text) advancedQuery.q = query.text;
3217
+ if (categoryIds.length > 0) advancedQuery.categories = categoryIds.join(",");
3218
+ if (collectionIds.length > 0) {
3219
+ advancedQuery.collections = collectionIds.join(",");
3220
+ }
3221
+ if (query.priceMin !== void 0) advancedQuery.price_min = query.priceMin;
3222
+ if (query.priceMax !== void 0) advancedQuery.price_max = query.priceMax;
3223
+ if (query.inStock === true) advancedQuery.isAvailable = true;
3224
+ if (query.attributes) {
3225
+ const attributes = Object.entries(query.attributes).map(([id, values]) => ({
3226
+ id,
3227
+ values
3228
+ }));
3229
+ advancedQuery.attributes = JSON.stringify(attributes);
3230
+ }
3231
+ if (query.sort) {
3232
+ const mappedOrder = MEDUSA_SORT_KEY_MAP[query.sort] ?? null;
3233
+ if (mappedOrder !== null) advancedQuery.order = mappedOrder;
3234
+ }
3235
+ if (this.regionId) advancedQuery.region_id = this.regionId;
3236
+ if (this.currencyCode) {
3237
+ advancedQuery.currency_code = this.currencyCode.toLowerCase();
3238
+ }
3239
+ return advancedQuery;
3240
+ }
3241
+ /** Resolve the effective category scope of a query to backend IDs. */
3242
+ async resolveScopeCategoryIds(query) {
3243
+ const handles = resolveEffectiveCategoryHandles(query);
3244
+ if (handles === void 0 || handles.length === 0) return [];
3245
+ return this.resolveCategoryIds(handles);
3246
+ }
3247
+ /**
3248
+ * Advertised filterable attribute handles for the scope. The custom-attributes
3249
+ * endpoint is facet *metadata* (which attributes exist), not proof that a
3250
+ * filter can be applied; these handles are candidates only, and each still
3251
+ * requires the advanced endpoint to echo it as applied before it is offered.
3252
+ */
3253
+ async resolveFilterableAttributeHandles(categoryIds) {
3254
+ const handles = [];
3255
+ for (const categoryId of categoryIds) {
3256
+ try {
3257
+ const data = await this.sdk.client.fetch(
3258
+ `/store/custom-attributes/category/${categoryId}`
3259
+ );
3260
+ for (const item of data?.attributes ?? []) {
3261
+ const attr = item.attribute ?? item;
3262
+ if (attr?.is_filterable && attr?.is_active && typeof attr.handle === "string" && attr.handle.length > 0) {
3263
+ handles.push(attr.handle);
3264
+ }
3265
+ }
3266
+ } catch {
3267
+ }
3268
+ }
3269
+ return handles;
3270
+ }
3271
+ /** Build a typed `unsupported-query` outcome naming the offending fields. */
3272
+ unsupportedCatalogOutcome(fields) {
3273
+ return {
3274
+ ok: false,
3275
+ error: {
3276
+ code: "unsupported-query",
3277
+ fields,
3278
+ message: `Unsupported catalog query: ${fields.join(", ")}`,
3279
+ repair: { remove: fields }
3280
+ }
3281
+ };
3282
+ }
2601
3283
  async getProductRecommendations(productId) {
2602
3284
  this.logger.debug("getProductRecommendations", { productId });
2603
3285
  this.logger.warn("Product recommendations not yet implemented for Medusa");
@@ -2619,16 +3301,8 @@ var MedusaProvider = class {
2619
3301
  async searchProductsAdvanced(options) {
2620
3302
  this.logger.debug("searchProductsAdvanced", options);
2621
3303
  if (!this.capabilities.advancedSearch) {
2622
- this.logger.warn("Advanced search not available, falling back to getProducts()");
2623
- const products = await this.getProducts(options);
2624
- return {
2625
- products,
2626
- meta: {
2627
- limit: options?.first ?? 20,
2628
- offset: Number(options?.after ?? 0),
2629
- hybridSearchUsed: false
2630
- }
2631
- };
3304
+ this.logger.warn("Advanced search capability disabled, using faithful fallback");
3305
+ return this.fallbackAdvancedSearch(options);
2632
3306
  }
2633
3307
  try {
2634
3308
  const queryParams = {};
@@ -2728,20 +3402,39 @@ var MedusaProvider = class {
2728
3402
  });
2729
3403
  return { products, meta };
2730
3404
  } catch (error) {
2731
- this.logger.error("Advanced search failed, falling back to getProducts()", {
3405
+ this.logger.error("Advanced search failed, using faithful fallback", {
2732
3406
  error: error.message,
2733
3407
  stack: error.stack
2734
3408
  });
2735
- const products = await this.getProducts(options);
2736
- return {
2737
- products,
2738
- meta: {
2739
- limit: options?.first ?? 20,
2740
- offset: Number(options?.after ?? 0),
2741
- hybridSearchUsed: false
2742
- }
2743
- };
3409
+ return this.fallbackAdvancedSearch(options);
3410
+ }
3411
+ }
3412
+ /**
3413
+ * Faithful fallback for `searchProductsAdvanced`. A basic-only request is
3414
+ * served by `getProducts`; a request that depends on the optional advanced
3415
+ * endpoint is rejected as a typed `ProviderAPIError` naming the unappliable
3416
+ * fields, so a failed advanced search never returns an unfiltered result
3417
+ * labelled success (E2 task 2.4).
3418
+ */
3419
+ async fallbackAdvancedSearch(options) {
3420
+ const requested = collectAdvancedSearchRequestFields(options);
3421
+ if (requested.length > 0) {
3422
+ throw new ProviderAPIError(
3423
+ `Advanced search unavailable for requested fields: ${requested.join(", ")}`,
3424
+ "medusa",
3425
+ void 0,
3426
+ { fields: requested, repair: { remove: requested } }
3427
+ );
2744
3428
  }
3429
+ const products = await this.getProducts(options);
3430
+ return {
3431
+ products,
3432
+ meta: {
3433
+ limit: options?.first ?? 20,
3434
+ offset: Number(options?.after ?? 0),
3435
+ hybridSearchUsed: false
3436
+ }
3437
+ };
2745
3438
  }
2746
3439
  /**
2747
3440
  * Get available filter options for product filtering
@@ -2796,23 +3489,20 @@ var MedusaProvider = class {
2796
3489
  });
2797
3490
  }
2798
3491
  }
2799
- if (options.useAdvancedSearch && this.capabilities.advancedSearch) {
2800
- filters.push({
2801
- id: "price",
2802
- label: "Price",
2803
- type: "range",
2804
- min: 0,
2805
- max: 1e4,
2806
- isActive: false,
2807
- ranking: 0
2808
- });
2809
- filters.push({
2810
- id: "isAvailable",
2811
- label: "In Stock",
2812
- type: "boolean",
2813
- isActive: false,
2814
- ranking: 5
3492
+ if (options.useAdvancedSearch) {
3493
+ const support = await this.getCatalogFacetSupport({
3494
+ categoryIds: options.categoryId ? [options.categoryId] : [],
3495
+ scope: options.categoryId
2815
3496
  });
3497
+ if (support.offered.includes("inStock")) {
3498
+ filters.push({
3499
+ id: "isAvailable",
3500
+ label: "In Stock",
3501
+ type: "boolean",
3502
+ isActive: false,
3503
+ ranking: 5
3504
+ });
3505
+ }
2816
3506
  }
2817
3507
  if (options.categoryId && this.capabilities.customAttributes) {
2818
3508
  try {
@@ -2847,19 +3537,27 @@ var MedusaProvider = class {
2847
3537
  // Custom attributes lower priority
2848
3538
  };
2849
3539
  if ((filterType === "single" || filterType === "multi") && attr.values) {
2850
- filter.options = attr.values.map((val) => ({
2851
- value: this.toSlug(val.value),
2852
- // URL slug: ?ram=8gb
2853
- label: val.value,
2854
- // Display: "8GB"
2855
- id: val.id,
2856
- // Backend ID: 01KN03N67V13K5S2E6B6695KRN
2857
- count: options.includeFacetCounts ? val.count : void 0
2858
- }));
3540
+ filter.options = attr.values.map((val) => {
3541
+ const count = options.includeFacetCounts ? readFiniteFacetNumber(val.count) : void 0;
3542
+ return {
3543
+ value: this.toSlug(val.value),
3544
+ // URL slug: ?ram=8gb
3545
+ label: val.value,
3546
+ // Display: "8GB"
3547
+ id: val.id,
3548
+ // Backend ID: 01KN03N67V13K5S2E6B6695KRN
3549
+ ...count !== void 0 ? { count } : {}
3550
+ };
3551
+ });
2859
3552
  }
2860
- if (filterType === "range" && attr.metadata) {
2861
- filter.min = attr.metadata.min || 0;
2862
- filter.max = attr.metadata.max || 100;
3553
+ if (filterType === "range") {
3554
+ const min = readFiniteFacetNumber(attr.metadata?.min);
3555
+ const max = readFiniteFacetNumber(attr.metadata?.max);
3556
+ if (min === void 0 || max === void 0) {
3557
+ return;
3558
+ }
3559
+ filter.min = min;
3560
+ filter.max = max;
2863
3561
  }
2864
3562
  filters.push(filter);
2865
3563
  });
@@ -3221,7 +3919,7 @@ var MedusaProvider = class {
3221
3919
  return customFetcher(options);
3222
3920
  }
3223
3921
  try {
3224
- const limit = options.query ? 1e3 : options.limit ?? 100;
3922
+ const limit = options.limit ?? this.catalogPageSize;
3225
3923
  const queryParams = {
3226
3924
  limit,
3227
3925
  include_descendants_tree: options.includeDescendants ?? false
@@ -3261,11 +3959,15 @@ var MedusaProvider = class {
3261
3959
  }
3262
3960
  const categoryQueryParams = {
3263
3961
  category_id: [category.id],
3264
- order: options.sortKey,
3265
- limit: options.first ?? 100,
3962
+ // Bounded, paginated listing (change B, decision 5).
3963
+ limit: options.first ?? this.catalogPageSize,
3266
3964
  offset: options.after ? parseInt(options.after, 10) : void 0,
3267
- fields: "+variants,+variants.prices,+variants.calculated_prices,+variants.inventory_quantity,+options,+images,+tags"
3965
+ fields: "+variants,+variants.prices,+variants.calculated_price,+variants.inventory_quantity,+options,+images,+tags"
3268
3966
  };
3967
+ const mappedOrder = options.sortKey ? MEDUSA_SORT_KEY_MAP[options.sortKey] ?? null : null;
3968
+ if (mappedOrder !== null) {
3969
+ categoryQueryParams.order = mappedOrder;
3970
+ }
3269
3971
  if (this.regionId) {
3270
3972
  categoryQueryParams.region_id = this.regionId;
3271
3973
  }
@@ -3300,7 +4002,7 @@ var MedusaProvider = class {
3300
4002
  } else {
3301
4003
  createPayload.currency_code = this.currencyCode.toLowerCase();
3302
4004
  }
3303
- const salesChannelId = options?.salesChannelId ?? process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
4005
+ const salesChannelId = options?.salesChannelId ?? resolveConfiguredSalesChannelId();
3304
4006
  if (salesChannelId) {
3305
4007
  createPayload.sales_channel_id = salesChannelId;
3306
4008
  }
@@ -3371,7 +4073,7 @@ var MedusaProvider = class {
3371
4073
  fields: "+variants,+variants.calculated_price,+variants.prices,+variants.money_amounts",
3372
4074
  region_id: backendCart.region_id
3373
4075
  };
3374
- const salesChannelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
4076
+ const salesChannelId = resolveConfiguredSalesChannelId();
3375
4077
  if (salesChannelId) {
3376
4078
  variantCheckParams.sales_channel_id = [salesChannelId];
3377
4079
  }
@@ -4487,31 +5189,84 @@ var MedusaProvider = class {
4487
5189
  * @throws {ProviderAPIError} If checkout fails
4488
5190
  */
4489
5191
  async completeCheckout(cartId) {
4490
- this.logger.debug("completeCheckout", { cartId });
4491
- try {
4492
- return await this.withRetry(
4493
- async () => {
4494
- const { order } = await this.sdk.store.cart.complete(cartId);
4495
- if (!order) {
4496
- throw new ProviderAPIError("Checkout completed but no order returned", "medusa", 500);
4497
- }
4498
- this.logger.info("Checkout completed successfully", {
4499
- orderId: order.id,
4500
- displayId: order.display_id
4501
- });
4502
- return transformOrder(order);
4503
- },
4504
- "completeCheckout",
4505
- { cartId }
4506
- );
4507
- } catch (error) {
4508
- this.logger.error("Failed to complete checkout", { cartId, error: error.message });
5192
+ const result = await this.completeCart(cartId);
5193
+ if (result.type === "order") {
5194
+ return result.order;
5195
+ }
5196
+ if (result.type === "cart") {
4509
5197
  throw new ProviderAPIError(
4510
- `Failed to complete checkout: ${error.message}`,
5198
+ `Failed to complete checkout: ${result.error.message}`,
4511
5199
  "medusa",
4512
- error.status
5200
+ result.error.status
4513
5201
  );
4514
5202
  }
5203
+ throw new ProviderAPIError(
5204
+ `Checkout outcome is unresolved: ${result.error.message}`,
5205
+ "medusa",
5206
+ result.error.status
5207
+ );
5208
+ }
5209
+ /**
5210
+ * Complete a cart exactly once and preserve the native discriminator
5211
+ * (change B, decision 11).
5212
+ *
5213
+ * Medusa's `POST /store/carts/:id/complete` returns HTTP 200 whose body is
5214
+ * either `{ type: 'order', order }` or `{ type: 'cart', cart, error }`. This
5215
+ * method never retries: a thrown transport/timeout/5xx failure becomes an
5216
+ * `unresolved` result so the caller reconciles the committed cart before any
5217
+ * further attempt. The Medusa JS SDK performs no implicit retry of its own,
5218
+ * so there is no blind retry beneath this call either.
5219
+ */
5220
+ async completeCart(cartId) {
5221
+ this.logger.debug("completeCart", { cartId });
5222
+ try {
5223
+ const response = await this.sdk.store.cart.complete(cartId, {
5224
+ fields: MEDUSA_ORDER_COMPLETION_FIELDS
5225
+ });
5226
+ if (response?.type === "cart" || response?.cart && response?.error) {
5227
+ const error = this.toCompletionError(response.error);
5228
+ this.logger.warn("Cart completion returned a cart with an error", {
5229
+ cartId,
5230
+ code: error.code,
5231
+ message: error.message
5232
+ });
5233
+ return { type: "cart", cart: transformCart(response.cart), error };
5234
+ }
5235
+ const order = response?.order;
5236
+ if (!order) {
5237
+ return {
5238
+ type: "unresolved",
5239
+ error: { message: "Checkout returned neither an order nor a cart error" }
5240
+ };
5241
+ }
5242
+ this.logger.info("Checkout completed successfully", {
5243
+ orderId: order.id,
5244
+ displayId: order.display_id
5245
+ });
5246
+ return { type: "order", order: transformOrder(order) };
5247
+ } catch (error) {
5248
+ const completionError = this.toCompletionError(error);
5249
+ this.logger.error("Cart completion failed (unresolved, not retried)", {
5250
+ cartId,
5251
+ status: completionError.status,
5252
+ message: completionError.message
5253
+ });
5254
+ return { type: "unresolved", error: completionError };
5255
+ }
5256
+ }
5257
+ /** Normalize any thrown/returned completion failure into a structured error. */
5258
+ toCompletionError(error) {
5259
+ if (error && typeof error === "object") {
5260
+ const err = error;
5261
+ const message = typeof err.message === "string" && err.message.length > 0 ? err.message : extractErrorMessage(error);
5262
+ return {
5263
+ message,
5264
+ code: typeof err.code === "string" ? err.code : void 0,
5265
+ status: err.status ?? err.statusCode,
5266
+ raw: err
5267
+ };
5268
+ }
5269
+ return { message: extractErrorMessage(error), raw: error };
4515
5270
  }
4516
5271
  /**
4517
5272
  * Get available shipping methods for cart
@@ -4523,130 +5278,66 @@ var MedusaProvider = class {
4523
5278
  * @throws {ProviderAPIError} If cart not found or region not configured
4524
5279
  */
4525
5280
  async getShippingMethods(cartId) {
4526
- this.logger.info("\u{1F69A} [DEBUG] getShippingMethods called", { cartId });
5281
+ this.logger.debug("getShippingMethods", { cartId });
4527
5282
  try {
4528
5283
  return await this.withRetry(
4529
5284
  async () => {
4530
- this.logger.info("\u{1F69A} [DEBUG] Step 1: Retrieving cart from Medusa...", { cartId });
4531
5285
  const cartResponse = await this.sdk.store.cart.retrieve(cartId);
4532
5286
  const { cart } = cartResponse;
4533
- this.logger.info("\u{1F69A} [DEBUG] Step 2: Cart retrieved from backend", {
4534
- cartId,
4535
- hasCart: !!cart,
4536
- cartKeys: cart ? Object.keys(cart) : [],
4537
- regionId: cart?.region_id,
4538
- currencyCode: cart?.currency_code,
4539
- shippingAddress: cart?.shipping_address ? {
4540
- country: cart.shipping_address.country_code,
4541
- postal: cart.shipping_address.postal_code
4542
- } : null
4543
- });
4544
5287
  if (!cart) {
4545
- this.logger.error("\u{1F69A} [DEBUG] ERROR: Cart not found in response", {
4546
- cartId,
4547
- cartResponse
4548
- });
4549
5288
  throw new ResourceNotFoundError("Cart", cartId);
4550
5289
  }
4551
5290
  if (!cart.region_id) {
4552
- this.logger.error("\u{1F69A} [DEBUG] ERROR: Cart has no region_id", {
4553
- cartId,
4554
- cart: {
4555
- id: cart.id,
4556
- region_id: cart.region_id,
4557
- region: cart.region,
4558
- allKeys: Object.keys(cart)
4559
- }
4560
- });
4561
5291
  throw new ProviderAPIError("Cart has no region assigned", "medusa", 400);
4562
5292
  }
4563
- this.logger.info("\u{1F69A} [DEBUG] Step 3: Fetching shipping options from Medusa...", {
4564
- cartId,
4565
- regionId: cart.region_id,
4566
- endpoint: `${this.baseUrl}store/shipping-options?cart_id=${cartId}`,
4567
- note: "About to fetch shipping options (SDK when available, otherwise HTTP)"
4568
- });
4569
- let shippingResponse;
4570
- let shipping_options;
4571
- try {
4572
- const storeAny = this.sdk.store;
4573
- if (storeAny?.shippingOption?.list) {
4574
- shippingResponse = await storeAny.shippingOption.list({
4575
- cart_id: cartId
4576
- });
4577
- } else {
4578
- const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
4579
- const url = new URL("store/shipping-options", baseUrl);
4580
- url.searchParams.set("cart_id", cartId);
4581
- const response = await fetchWithRetry(
4582
- url.toString(),
4583
- {
4584
- method: "GET",
4585
- headers: {
4586
- "x-publishable-api-key": this.publishableKey,
4587
- "content-type": "application/json"
4588
- }
4589
- },
4590
- this.retryConfig
4591
- );
4592
- shippingResponse = await response.json();
4593
- }
4594
- shipping_options = shippingResponse.shipping_options;
4595
- } catch (sdkError) {
4596
- this.logger.error(
4597
- "\u{1F69A} [DEBUG] ERROR: Medusa SDK threw error when fetching shipping options",
5293
+ let shippingOptions = [];
5294
+ const storeAny = this.sdk.store;
5295
+ if (storeAny?.fulfillment?.listCartOptions) {
5296
+ const response = await storeAny.fulfillment.listCartOptions({
5297
+ cart_id: cartId
5298
+ });
5299
+ shippingOptions = response?.shipping_options ?? [];
5300
+ } else {
5301
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
5302
+ const url = new URL("store/shipping-options", baseUrl);
5303
+ url.searchParams.set("cart_id", cartId);
5304
+ const response = await fetchWithRetry(
5305
+ url.toString(),
4598
5306
  {
4599
- cartId,
4600
- regionId: cart.region_id,
4601
- errorType: sdkError?.constructor?.name,
4602
- errorMessage: sdkError?.message,
4603
- errorStatus: sdkError?.status,
4604
- errorCode: sdkError?.code,
4605
- errorResponse: sdkError?.response ? JSON.stringify(sdkError.response, null, 2) : "N/A",
4606
- errorBody: sdkError?.body ? JSON.stringify(sdkError.body, null, 2) : "N/A",
4607
- fullError: JSON.stringify(sdkError, Object.getOwnPropertyNames(sdkError), 2)
4608
- }
5307
+ method: "GET",
5308
+ headers: {
5309
+ "x-publishable-api-key": this.publishableKey,
5310
+ "content-type": "application/json"
5311
+ }
5312
+ },
5313
+ this.retryConfig
4609
5314
  );
4610
- throw sdkError;
5315
+ shippingOptions = (await response.json())?.shipping_options ?? [];
4611
5316
  }
4612
- this.logger.info("\u{1F69A} [DEBUG] Step 4: Shipping options received from backend", {
4613
- cartId,
4614
- totalOptions: shipping_options?.length || 0,
4615
- rawResponse: JSON.stringify(shippingResponse, null, 2),
4616
- firstOption: shipping_options?.[0] ? {
4617
- id: shipping_options[0].id,
4618
- name: shipping_options[0].name,
4619
- allKeys: Object.keys(shipping_options[0]),
4620
- price_incl_tax: shipping_options[0].price_incl_tax,
4621
- amount: shipping_options[0].amount,
4622
- data: shipping_options[0].data
4623
- } : null
4624
- });
4625
- const methods = shipping_options.map((option) => ({
4626
- id: option.id,
4627
- name: option.name,
4628
- price: {
4629
- amount: option.price_incl_tax?.toString() || option.amount?.toString() || "0",
4630
- currencyCode: cart.currency_code?.toUpperCase() || this.currencyCode
4631
- },
4632
- description: option.data?.description,
4633
- estimatedDays: option.data?.estimated_days || "N/A"
4634
- }));
4635
- this.logger.info("\u{1F69A} [DEBUG] Step 5: Transformed methods for Thor types", {
4636
- cartId,
4637
- count: methods.length,
4638
- methods: JSON.stringify(methods, null, 2)
5317
+ const currencyCode = cart.currency_code?.toUpperCase() ?? "";
5318
+ return shippingOptions.map((option) => {
5319
+ const amount = option?.amount ?? option?.price_incl_tax;
5320
+ const estimatedDays = option?.data?.estimated_days != null ? String(option.data.estimated_days) : void 0;
5321
+ return {
5322
+ id: option.id,
5323
+ name: option.name,
5324
+ price: {
5325
+ amount: typeof amount === "number" && Number.isFinite(amount) ? amount.toFixed(2) : "",
5326
+ currencyCode
5327
+ },
5328
+ description: option?.data?.description,
5329
+ estimatedDays,
5330
+ fulfillmentSetType: option?.service_zone?.fulfillment_set?.type ?? null
5331
+ };
4639
5332
  });
4640
- return methods;
4641
5333
  },
4642
5334
  "getShippingMethods",
4643
5335
  { cartId }
4644
5336
  );
4645
5337
  } catch (error) {
4646
- this.logger.error("\u{1F69A} [DEBUG] ERROR: Failed to fetch shipping methods", {
5338
+ this.logger.error("Failed to fetch shipping methods", {
4647
5339
  cartId,
4648
5340
  errorMessage: error.message,
4649
- errorStack: error.stack,
4650
5341
  errorStatus: error.status
4651
5342
  });
4652
5343
  throw new ProviderAPIError(
@@ -5007,19 +5698,12 @@ var MedusaProvider = class {
5007
5698
  async addShippingMethod(cartId, methodId) {
5008
5699
  this.logger.debug("addShippingMethod", { cartId, methodId });
5009
5700
  try {
5010
- return await this.withRetry(
5011
- async () => {
5012
- const { cart } = await this.sdk.store.cart.addShippingMethod(cartId, {
5013
- option_id: methodId
5014
- });
5015
- this.logger.info("Shipping method added successfully", { cartId, methodId });
5016
- void cart;
5017
- const updatedCart = await this.retrieveCartWithLineDetails(cartId);
5018
- return transformCart(updatedCart);
5019
- },
5020
- "addShippingMethod",
5021
- { cartId, methodId }
5022
- );
5701
+ await this.sdk.store.cart.addShippingMethod(cartId, {
5702
+ option_id: methodId
5703
+ });
5704
+ this.logger.info("Shipping method added successfully", { cartId, methodId });
5705
+ const updatedCart = await this.retrieveCartWithLineDetails(cartId);
5706
+ return transformCart(updatedCart);
5023
5707
  } catch (error) {
5024
5708
  this.logger.error("Failed to add shipping method", {
5025
5709
  cartId,
@@ -5043,166 +5727,75 @@ var MedusaProvider = class {
5043
5727
  * @throws {ProviderAPIError} If provider invalid or cart not ready
5044
5728
  */
5045
5729
  async initiatePaymentSession(cartId, providerId) {
5046
- this.logger.info("\u{1F4B3} [DEBUG] initiatePaymentSession called", { cartId, providerId });
5730
+ this.logger.debug("initiatePaymentSession", { cartId, providerId });
5047
5731
  try {
5048
- await this.withRetry(
5049
- async () => {
5050
- this.logger.info(
5051
- "\u{1F4B3} [DEBUG] Step 1: Retrieving RAW cart from Medusa for payment session...",
5052
- { cartId }
5053
- );
5054
- const { cart: rawCart } = await this.sdk.store.cart.retrieve(cartId, {
5055
- fields: "+shipping_methods,+shipping_address,+email,+region_id,+total,+items,+payment_collection,+payment_collection.payment_sessions,+billing_address"
5056
- });
5057
- this.logger.info("\u{1F4B3} [DEBUG] Step 2: RAW cart retrieved, checking state...", {
5058
- cartId: rawCart.id,
5059
- hasShippingMethod: !!rawCart.shipping_methods?.length,
5060
- shippingMethodIds: rawCart.shipping_methods?.map((sm) => sm.id) || [],
5061
- hasShippingAddress: !!rawCart.shipping_address,
5062
- shippingAddressCity: rawCart.shipping_address?.city || "N/A",
5063
- hasEmail: !!rawCart.email,
5064
- email: rawCart.email || "N/A",
5065
- hasBillingAddress: !!rawCart.billing_address,
5066
- hasPaymentCollection: !!rawCart.payment_collection,
5067
- existingSessionsCount: rawCart.payment_collection?.payment_sessions?.length || 0,
5068
- regionId: rawCart.region_id,
5069
- total: rawCart.total,
5070
- itemCount: rawCart.items?.length || 0
5071
- });
5072
- if (!rawCart.items || rawCart.items.length === 0) {
5073
- this.logger.error("\u{1F4B3} [DEBUG] ERROR: Cannot create payment session for empty cart", {
5074
- cartId,
5075
- itemCount: 0,
5076
- reason: "Cart has no items"
5077
- });
5078
- throw new ProviderAPIError(
5079
- "Cannot create payment session: cart is empty. Please add items to your cart before checking out.",
5080
- "medusa",
5081
- 400
5082
- );
5083
- }
5084
- if (!rawCart.shipping_address) {
5085
- this.logger.error("\u{1F4B3} [DEBUG] ERROR: Shipping address required", { cartId });
5086
- throw new ProviderAPIError(
5087
- "Shipping address is required before initiating payment session",
5088
- "medusa",
5089
- 400
5090
- );
5091
- }
5092
- if (!rawCart.shipping_methods?.length) {
5093
- this.logger.error("\u{1F4B3} [DEBUG] ERROR: Shipping method required", { cartId });
5094
- throw new ProviderAPIError(
5095
- "Shipping method is required before initiating payment session",
5096
- "medusa",
5097
- 400
5098
- );
5099
- }
5100
- if (!rawCart.email) {
5101
- this.logger.error("\u{1F4B3} [DEBUG] ERROR: Email required", { cartId });
5102
- throw new ProviderAPIError(
5103
- "Email is required before initiating payment session",
5104
- "medusa",
5105
- 400
5106
- );
5107
- }
5108
- if (!rawCart.billing_address) {
5109
- this.logger.warn(
5110
- "\u{1F4B3} [DEBUG] WARNING: Billing address not set, payment provider may require it",
5111
- {
5112
- cartId: rawCart.id,
5113
- shippingAddress: rawCart.shipping_address
5114
- }
5115
- );
5116
- }
5117
- const existingSessions = rawCart.payment_collection?.payment_sessions || [];
5118
- if (existingSessions.length > 0) {
5119
- this.logger.info("\u{1F4B3} [DEBUG] Payment session already exists, skipping creation", {
5120
- cartId: rawCart.id,
5121
- sessionCount: existingSessions.length,
5122
- sessionIds: existingSessions.map((s) => s.id)
5123
- });
5124
- return;
5125
- }
5126
- this.logger.info("\u{1F4B3} [DEBUG] Step 2.5: No existing payment sessions, will create new one", {
5127
- cartId: rawCart.id,
5128
- hasPaymentCollection: !!rawCart.payment_collection,
5129
- providerId
5130
- });
5131
- this.logger.info("\u{1F4B3} [DEBUG] Step 3: Calling Medusa SDK initiatePaymentSession...", {
5132
- cartId,
5133
- providerId,
5134
- endpoint: `${this.baseUrl}store/payment-collections`,
5135
- note: "About to call SDK method"
5136
- });
5137
- this.logger.info("\u{1F4B3} [DEBUG] Step 2.5: Validating provider ID...", {
5138
- providerId
5139
- });
5140
- try {
5141
- validatePaymentId(providerId);
5142
- this.logger.info("\u{1F4B3} [DEBUG] Provider ID validation passed", { providerId });
5143
- } catch (validationError) {
5144
- this.logger.error("\u{1F4B3} [DEBUG] ERROR: Invalid provider ID detected", {
5145
- providerId,
5146
- error: validationError.message,
5147
- hint: "Use getBackendProviderId(paymentMethod) to extract correct ID"
5148
- });
5149
- throw new ProviderAPIError(
5150
- validationError.message,
5151
- "medusa",
5152
- 400
5153
- );
5154
- }
5155
- let paymentResponse;
5156
- try {
5157
- paymentResponse = await this.sdk.store.payment.initiatePaymentSession(rawCart, {
5158
- provider_id: providerId
5159
- });
5160
- this.logger.info("\u{1F4B3} [DEBUG] Step 4: Payment session response received", {
5161
- cartId,
5162
- providerId,
5163
- responseKeys: paymentResponse ? Object.keys(paymentResponse) : [],
5164
- response: JSON.stringify(paymentResponse, null, 2)
5165
- });
5166
- } catch (sdkError) {
5167
- this.logger.error(
5168
- "\u{1F4B3} [DEBUG] ERROR: Medusa SDK threw error when initiating payment session",
5169
- {
5170
- cartId,
5171
- providerId,
5172
- errorType: sdkError?.constructor?.name,
5173
- errorMessage: sdkError?.message,
5174
- errorStatus: sdkError?.status,
5175
- errorCode: sdkError?.code,
5176
- errorResponse: sdkError?.response ? JSON.stringify(sdkError.response, null, 2) : "N/A",
5177
- errorBody: sdkError?.body ? JSON.stringify(sdkError.body, null, 2) : "N/A",
5178
- fullError: JSON.stringify(sdkError, Object.getOwnPropertyNames(sdkError), 2),
5179
- // Include cart state in error for debugging
5180
- cartState: {
5181
- hasShippingMethod: !!rawCart.shipping_methods?.length,
5182
- hasShippingAddress: !!rawCart.shipping_address,
5183
- hasBillingAddress: !!rawCart.billing_address,
5184
- hasEmail: !!rawCart.email,
5185
- total: rawCart.total,
5186
- itemCount: rawCart.items?.length || 0
5187
- }
5188
- }
5189
- );
5190
- throw sdkError;
5191
- }
5192
- this.logger.info("\u{1F4B3} [DEBUG] Payment session initiated successfully", {
5193
- cartId,
5194
- providerId
5195
- });
5196
- },
5197
- "initiatePaymentSession",
5198
- { cartId, providerId }
5199
- );
5732
+ const { cart: rawCart } = await this.sdk.store.cart.retrieve(cartId, {
5733
+ fields: "+shipping_methods,+shipping_address,+email,+region_id,+total,+items,+payment_collection,+payment_collection.payment_sessions,+billing_address"
5734
+ });
5735
+ if (!rawCart.items || rawCart.items.length === 0) {
5736
+ throw new ProviderAPIError(
5737
+ "Cannot create payment session: cart is empty. Please add items to your cart before checking out.",
5738
+ "medusa",
5739
+ 400
5740
+ );
5741
+ }
5742
+ if (!rawCart.shipping_address) {
5743
+ throw new ProviderAPIError(
5744
+ "Shipping address is required before initiating payment session",
5745
+ "medusa",
5746
+ 400
5747
+ );
5748
+ }
5749
+ if (!rawCart.shipping_methods?.length) {
5750
+ throw new ProviderAPIError(
5751
+ "Shipping method is required before initiating payment session",
5752
+ "medusa",
5753
+ 400
5754
+ );
5755
+ }
5756
+ if (!rawCart.email) {
5757
+ throw new ProviderAPIError(
5758
+ "Email is required before initiating payment session",
5759
+ "medusa",
5760
+ 400
5761
+ );
5762
+ }
5763
+ validatePaymentId(providerId);
5764
+ const existingSessions = rawCart.payment_collection?.payment_sessions ?? [];
5765
+ const compatible = existingSessions.find((session) => {
5766
+ const statusOk = ["pending", "requires_more", "authorized"].includes(session?.status);
5767
+ return session?.provider_id === providerId && session?.amount === rawCart.total && statusOk;
5768
+ });
5769
+ if (compatible) {
5770
+ this.logger.info("Reusing compatible payment session", {
5771
+ cartId,
5772
+ providerId,
5773
+ sessionId: compatible.id,
5774
+ amount: compatible.amount
5775
+ });
5776
+ return;
5777
+ }
5778
+ if (existingSessions.length > 0) {
5779
+ this.logger.info("Existing payment session incompatible; replacing it", {
5780
+ cartId,
5781
+ providerId,
5782
+ existing: existingSessions.map((session) => ({
5783
+ id: session.id,
5784
+ providerId: session.provider_id,
5785
+ amount: session.amount,
5786
+ status: session.status
5787
+ }))
5788
+ });
5789
+ }
5790
+ await this.sdk.store.payment.initiatePaymentSession(rawCart, {
5791
+ provider_id: providerId
5792
+ });
5793
+ this.logger.info("Payment session initiated", { cartId, providerId });
5200
5794
  } catch (error) {
5201
- this.logger.error("\u{1F4B3} [DEBUG] ERROR: Failed to initiate payment session", {
5795
+ this.logger.error("Failed to initiate payment session", {
5202
5796
  cartId,
5203
5797
  providerId,
5204
5798
  errorMessage: error.message,
5205
- errorStack: error.stack,
5206
5799
  errorStatus: error.status
5207
5800
  });
5208
5801
  throw new ProviderAPIError(
@@ -5225,31 +5818,23 @@ var MedusaProvider = class {
5225
5818
  async applyDiscountCode(cartId, code) {
5226
5819
  this.logger.debug("applyDiscountCode", { cartId, code });
5227
5820
  try {
5228
- return await this.withRetry(
5229
- async () => {
5230
- const response = await fetch(`${this.baseUrl}/store/carts/${cartId}/promotions`, {
5231
- method: "POST",
5232
- headers: {
5233
- "Content-Type": "application/json",
5234
- "x-publishable-api-key": this.publishableKey || ""
5235
- },
5236
- body: JSON.stringify({
5237
- promo_codes: [code]
5238
- })
5239
- });
5240
- if (!response.ok) {
5241
- const error = await response.json();
5242
- throw new Error(error.message || "Failed to apply discount code");
5243
- }
5244
- const { cart: medusaCart } = await response.json();
5245
- this.logger.info("Discount code applied successfully", { cartId, code });
5246
- void medusaCart;
5247
- const updatedCart = await this.retrieveCartWithLineDetails(cartId);
5248
- return transformCart(updatedCart);
5821
+ const response = await fetch(`${this.baseUrl}/store/carts/${cartId}/promotions`, {
5822
+ method: "POST",
5823
+ headers: {
5824
+ "Content-Type": "application/json",
5825
+ "x-publishable-api-key": this.publishableKey || ""
5249
5826
  },
5250
- "applyDiscountCode",
5251
- { cartId, code }
5252
- );
5827
+ body: JSON.stringify({
5828
+ promo_codes: [code]
5829
+ })
5830
+ });
5831
+ if (!response.ok) {
5832
+ const error = await response.json();
5833
+ throw new Error(error.message || "Failed to apply discount code");
5834
+ }
5835
+ this.logger.info("Discount code applied successfully", { cartId, code });
5836
+ const updatedCart = await this.retrieveCartWithLineDetails(cartId);
5837
+ return transformCart(updatedCart);
5253
5838
  } catch (error) {
5254
5839
  this.logger.error("Failed to apply discount code", { cartId, code, error: error.message });
5255
5840
  throw new ProviderAPIError(
@@ -5272,31 +5857,23 @@ var MedusaProvider = class {
5272
5857
  async removeDiscountCode(cartId, code) {
5273
5858
  this.logger.debug("removeDiscountCode", { cartId, code });
5274
5859
  try {
5275
- return await this.withRetry(
5276
- async () => {
5277
- const response = await fetch(`${this.baseUrl}/store/carts/${cartId}/promotions`, {
5278
- method: "DELETE",
5279
- headers: {
5280
- "Content-Type": "application/json",
5281
- "x-publishable-api-key": this.publishableKey || ""
5282
- },
5283
- body: JSON.stringify({
5284
- promo_codes: [code]
5285
- })
5286
- });
5287
- if (!response.ok) {
5288
- const error = await response.json();
5289
- throw new Error(error.message || "Failed to remove discount code");
5290
- }
5291
- const { cart: medusaCart } = await response.json();
5292
- this.logger.info("Discount code removed successfully", { cartId, code });
5293
- void medusaCart;
5294
- const updatedCart = await this.retrieveCartWithLineDetails(cartId);
5295
- return transformCart(updatedCart);
5860
+ const response = await fetch(`${this.baseUrl}/store/carts/${cartId}/promotions`, {
5861
+ method: "DELETE",
5862
+ headers: {
5863
+ "Content-Type": "application/json",
5864
+ "x-publishable-api-key": this.publishableKey || ""
5296
5865
  },
5297
- "removeDiscountCode",
5298
- { cartId, code }
5299
- );
5866
+ body: JSON.stringify({
5867
+ promo_codes: [code]
5868
+ })
5869
+ });
5870
+ if (!response.ok) {
5871
+ const error = await response.json();
5872
+ throw new Error(error.message || "Failed to remove discount code");
5873
+ }
5874
+ this.logger.info("Discount code removed successfully", { cartId, code });
5875
+ const updatedCart = await this.retrieveCartWithLineDetails(cartId);
5876
+ return transformCart(updatedCart);
5300
5877
  } catch (error) {
5301
5878
  this.logger.error("Failed to remove discount code", { cartId, code, error: error.message });
5302
5879
  throw new ProviderAPIError(
@@ -5434,7 +6011,7 @@ var MedusaProvider = class {
5434
6011
  }
5435
6012
  const channelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
5436
6013
  if (!channelId) {
5437
- throw new import_types.StorefrontConfigError(
6014
+ throw new StorefrontConfigError(
5438
6015
  "Medusa requires NEXT_PUBLIC_SALES_CHANNEL_ID to be configured. Each storefront must be linked to a sales channel. Set this in your .env file or environment variables.",
5439
6016
  "medusa",
5440
6017
  "NEXT_PUBLIC_SALES_CHANNEL_ID"
@@ -5463,7 +6040,7 @@ var MedusaProvider = class {
5463
6040
  });
5464
6041
  return context;
5465
6042
  } catch (error) {
5466
- if (error instanceof import_types.StorefrontConfigError) throw error;
6043
+ if (error instanceof StorefrontConfigError) throw error;
5467
6044
  const errorMessage = extractErrorMessage(error);
5468
6045
  throw new ProviderAPIError(
5469
6046
  `Failed to initialize storefront context: ${errorMessage}`,
@@ -9291,6 +9868,7 @@ var StelOrderProvider = class {
9291
9868
  // src/providers/mock/mock.ts
9292
9869
  var FIXTURE_TIMESTAMP = "2026-01-01T00:00:00.000Z";
9293
9870
  var DEFAULT_SITE_URL = "https://mock.thorcommerce.local";
9871
+ var MOCK_SALES_CHANNEL_ID = "mock-sales-channel";
9294
9872
  var regionFixtures = [
9295
9873
  {
9296
9874
  id: "mock-region-us",
@@ -9363,6 +9941,7 @@ var productFixtures = [
9363
9941
  {
9364
9942
  id: "mock-product-aurora-jacket",
9365
9943
  handle: "aurora-jacket",
9944
+ createdAt: "2026-01-05T00:00:00.000Z",
9366
9945
  title: "Aurora Quilted Jacket",
9367
9946
  description: "Lightweight quilted outerwear with tonal hardware and a soft structured collar.",
9368
9947
  imageLabel: "Aurora Jacket",
@@ -9404,6 +9983,7 @@ var productFixtures = [
9404
9983
  {
9405
9984
  id: "mock-product-solis-dress",
9406
9985
  handle: "solis-dress",
9986
+ createdAt: "2026-01-03T00:00:00.000Z",
9407
9987
  title: "Solis Pleated Dress",
9408
9988
  description: "A fluid pleated dress with a clean neckline designed for hero storytelling.",
9409
9989
  imageLabel: "Solis Dress",
@@ -9443,6 +10023,7 @@ var productFixtures = [
9443
10023
  {
9444
10024
  id: "mock-product-helios-lamp",
9445
10025
  handle: "helios-lamp",
10026
+ createdAt: "2026-01-04T00:00:00.000Z",
9446
10027
  title: "Helios Table Lamp",
9447
10028
  description: "Warm ambient lamp with sculptural proportions for lifestyle and home grids.",
9448
10029
  imageLabel: "Helios Lamp",
@@ -9466,6 +10047,7 @@ var productFixtures = [
9466
10047
  {
9467
10048
  id: "mock-product-luna-bag",
9468
10049
  handle: "luna-bag",
10050
+ createdAt: "2026-01-02T00:00:00.000Z",
9469
10051
  title: "Luna Crossbody Bag",
9470
10052
  description: "Compact everyday bag for accessory rails, checkout add-ons, and bundle modules.",
9471
10053
  imageLabel: "Luna Bag",
@@ -9606,7 +10188,7 @@ function buildProducts(currencyCode) {
9606
10188
  productType: product.productType,
9607
10189
  vendor: product.vendor,
9608
10190
  tags: product.tags,
9609
- createdAt: FIXTURE_TIMESTAMP,
10191
+ createdAt: product.createdAt,
9610
10192
  updatedAt: FIXTURE_TIMESTAMP,
9611
10193
  metadata: {
9612
10194
  categoryHandle: product.categoryHandle,
@@ -9675,6 +10257,92 @@ function paginateProducts(products, skip, first) {
9675
10257
  const end = first ? start + first : void 0;
9676
10258
  return products.slice(start, end);
9677
10259
  }
10260
+ var MOCK_CATALOG_SORT_KEYS = [
10261
+ "featured",
10262
+ "name",
10263
+ "newest"
10264
+ ];
10265
+ var MOCK_CATALOG_ENDPOINT = "mock://catalog/products";
10266
+ var MOCK_CATALOG_PAGE_SIZE = 50;
10267
+ function resolveEffectiveCategoryHandles2(query) {
10268
+ const { categoryScope, categories } = query;
10269
+ if (categoryScope === void 0) return categories;
10270
+ if (categories === void 0) return [categoryScope];
10271
+ return categories.filter((handle) => handle === categoryScope);
10272
+ }
10273
+ function collectRequestedAdvancedFields2(query) {
10274
+ const fields = [];
10275
+ if (query.priceMin !== void 0) fields.push("priceMin");
10276
+ if (query.priceMax !== void 0) fields.push("priceMax");
10277
+ if (query.inStock !== void 0) fields.push("inStock");
10278
+ for (const name of Object.keys(query.attributes ?? {})) {
10279
+ fields.push(`attr.${name}`);
10280
+ }
10281
+ return fields;
10282
+ }
10283
+ function advancedFieldToFacet2(field) {
10284
+ if (field === "priceMin" || field === "priceMax") return "price";
10285
+ return field;
10286
+ }
10287
+ function buildAppliedCatalogFilters2(query, effectiveCategoryHandles) {
10288
+ return {
10289
+ ...query.text ? { text: query.text } : {},
10290
+ ...effectiveCategoryHandles !== void 0 ? { categories: effectiveCategoryHandles } : {},
10291
+ ...query.collections !== void 0 ? { collections: query.collections } : {}
10292
+ };
10293
+ }
10294
+ function buildBasicDemonstratedQuery2(query) {
10295
+ return {
10296
+ ...query.text !== void 0 ? { text: query.text } : {},
10297
+ ...query.categoryScope !== void 0 ? { categoryScope: query.categoryScope } : {},
10298
+ ...query.categories !== void 0 ? { categories: query.categories } : {},
10299
+ ...query.collections !== void 0 ? { collections: query.collections } : {}
10300
+ };
10301
+ }
10302
+ function buildMockFacetSupport(query) {
10303
+ const appliedQuery = buildBasicDemonstratedQuery2(query);
10304
+ const scopeField = query.categoryScope !== void 0 ? { scope: query.categoryScope } : {};
10305
+ return {
10306
+ offered: ["categories", "collections"],
10307
+ held: ["price", "inStock"],
10308
+ demonstrations: {
10309
+ categories: {
10310
+ endpoint: MOCK_CATALOG_ENDPOINT,
10311
+ appliedQuery,
10312
+ ...scopeField
10313
+ },
10314
+ collections: {
10315
+ endpoint: MOCK_CATALOG_ENDPOINT,
10316
+ appliedQuery,
10317
+ ...scopeField
10318
+ }
10319
+ },
10320
+ heldReasons: {
10321
+ price: `${MOCK_CATALOG_ENDPOINT} is the mock basic listing; price filtering is not implemented in mock mode`,
10322
+ inStock: `${MOCK_CATALOG_ENDPOINT} is the mock basic listing; stock filtering is not implemented in mock mode`
10323
+ }
10324
+ };
10325
+ }
10326
+ function unsupportedMockCatalogOutcome(fields) {
10327
+ return {
10328
+ ok: false,
10329
+ error: {
10330
+ code: "unsupported-query",
10331
+ fields,
10332
+ message: `Unsupported catalog query in mock mode: ${fields.join(", ")}`,
10333
+ repair: { remove: fields }
10334
+ }
10335
+ };
10336
+ }
10337
+ function sortMockCatalogProducts(products, sortKey) {
10338
+ const sorted = [...products];
10339
+ if (sortKey === "name") {
10340
+ sorted.sort((left, right) => left.title.localeCompare(right.title));
10341
+ } else if (sortKey === "newest") {
10342
+ sorted.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
10343
+ }
10344
+ return sorted;
10345
+ }
9678
10346
  function resolveVariant(currencyCode, merchandiseId) {
9679
10347
  const products = buildProducts(currencyCode);
9680
10348
  for (const product of products) {
@@ -9739,11 +10407,22 @@ function buildExternalCart(state, currencyCode, siteUrl) {
9739
10407
  checkoutUrl: `${siteUrl}/checkout?cart=${state.id}`,
9740
10408
  cost: {
9741
10409
  subtotalAmount: createMoney(subtotal, regionCurrency),
9742
- totalAmount: createMoney(total, regionCurrency),
9743
- totalTaxAmount: createMoney(totalTax, regionCurrency)
10410
+ discountAmount: createMoney(discount, regionCurrency),
10411
+ totalTaxAmount: createMoney(totalTax, regionCurrency),
10412
+ shippingAmount: createMoney(shipping, regionCurrency),
10413
+ totalAmount: createMoney(total, regionCurrency)
9744
10414
  },
9745
10415
  lines,
9746
10416
  totalQuantity: lines.reduce((sum, line) => sum + line.quantity, 0),
10417
+ salesChannelId: MOCK_SALES_CHANNEL_ID,
10418
+ regionId: state.regionId,
10419
+ currencyCode: regionCurrency.toUpperCase(),
10420
+ selectedShippingMethodId: state.shippingMethodId,
10421
+ // The mock method id IS the shipping option id (the value returned by
10422
+ // `getMockShippingMethods` and accepted by `addShippingMethod`), so the
10423
+ // canonical option identity mirrors the real adapter's `so_` field and the
10424
+ // checkout UI can highlight the selected option.
10425
+ selectedShippingOptionId: state.shippingMethodId,
9747
10426
  completedAt: state.completedAt
9748
10427
  };
9749
10428
  }
@@ -9799,6 +10478,69 @@ function getMockProducts(options) {
9799
10478
  const sorted = sortProducts(filtered, options?.sortKey, options?.reverse);
9800
10479
  return paginateProducts(sorted, options?.skip, options?.first);
9801
10480
  }
10481
+ function getMockCatalogProducts(query, options = {}) {
10482
+ if (query.sort !== void 0 && !MOCK_CATALOG_SORT_KEYS.includes(query.sort)) {
10483
+ return unsupportedMockCatalogOutcome(["sort"]);
10484
+ }
10485
+ const region = getRegionFixture(options.regionId);
10486
+ const currencyCode = options.currencyCode ?? region.currencyCode;
10487
+ const allProducts = buildProducts(currencyCode);
10488
+ const facetSupport = buildMockFacetSupport(query);
10489
+ const requestedAdvanced = collectRequestedAdvancedFields2(query);
10490
+ const heldAdvanced = requestedAdvanced.filter(
10491
+ (field) => !facetSupport.offered.includes(advancedFieldToFacet2(field))
10492
+ );
10493
+ if (heldAdvanced.length > 0) {
10494
+ return unsupportedMockCatalogOutcome(heldAdvanced);
10495
+ }
10496
+ const effectiveCategoryHandles = resolveEffectiveCategoryHandles2(query);
10497
+ const emptyIntersection = effectiveCategoryHandles !== void 0 && effectiveCategoryHandles.length === 0;
10498
+ const categoryFilterEmpty = effectiveCategoryHandles !== void 0 && !effectiveCategoryHandles.some(
10499
+ (handle) => categoryFixtures.some(
10500
+ (category) => category.handle === handle || category.id === handle
10501
+ )
10502
+ );
10503
+ const collectionFilterEmpty = query.collections !== void 0 && !query.collections.some(
10504
+ (handle) => collectionFixtures.some(
10505
+ (collection) => collection.handle === handle || collection.id === handle
10506
+ )
10507
+ );
10508
+ if (emptyIntersection || categoryFilterEmpty || collectionFilterEmpty) {
10509
+ return {
10510
+ ok: true,
10511
+ results: {
10512
+ products: [],
10513
+ appliedFilters: buildAppliedCatalogFilters2(
10514
+ query,
10515
+ effectiveCategoryHandles
10516
+ ),
10517
+ facetSupport
10518
+ }
10519
+ };
10520
+ }
10521
+ const filtered = filterProducts(allProducts, {
10522
+ query: query.text,
10523
+ categoryIds: effectiveCategoryHandles,
10524
+ collectionIds: query.collections
10525
+ });
10526
+ const sorted = sortMockCatalogProducts(filtered, query.sort);
10527
+ const pageSize = query.pageSize ?? MOCK_CATALOG_PAGE_SIZE;
10528
+ const page = query.page ?? 1;
10529
+ const offset = (page - 1) * pageSize;
10530
+ return {
10531
+ ok: true,
10532
+ results: {
10533
+ products: sorted.slice(offset, offset + pageSize),
10534
+ // The full matching fixture count, never the page length.
10535
+ total: filtered.length,
10536
+ appliedFilters: buildAppliedCatalogFilters2(
10537
+ query,
10538
+ effectiveCategoryHandles
10539
+ ),
10540
+ facetSupport
10541
+ }
10542
+ };
10543
+ }
9802
10544
  function getMockProduct(handle, regionId) {
9803
10545
  const region = getRegionFixture(regionId);
9804
10546
  return buildProducts(region.currencyCode).find(
@@ -9911,6 +10653,17 @@ function getMockPaymentMethods(currencyCode) {
9911
10653
  isEnabled: true,
9912
10654
  requiresSetup: false,
9913
10655
  description: "Mock cash payment option for checkout prototypes."
10656
+ },
10657
+ {
10658
+ // The single approved manual TEST provider used by the real Medusa
10659
+ // backend, mirrored in mock mode so the guest TEST purchase path is
10660
+ // exercisable without a backend.
10661
+ id: "pp_system_default",
10662
+ name: "Manual TEST payment",
10663
+ type: "cash_on_delivery",
10664
+ isEnabled: true,
10665
+ requiresSetup: false,
10666
+ description: "Manual system TEST provider (bookkeeping only; not external funds)."
9914
10667
  }
9915
10668
  ];
9916
10669
  }
@@ -10004,6 +10757,7 @@ var MockProvider = class {
10004
10757
  version = "1.0.0";
10005
10758
  capabilities = MOCK_CAPABILITIES;
10006
10759
  isMock = true;
10760
+ supportedCatalogSortKeys = MOCK_CATALOG_SORT_KEYS;
10007
10761
  currencyCode;
10008
10762
  siteUrl;
10009
10763
  constructor(config = {}) {
@@ -10034,6 +10788,18 @@ var MockProvider = class {
10034
10788
  regionId: options?.regionId
10035
10789
  });
10036
10790
  }
10791
+ /**
10792
+ * Canonical catalog query path (E2 task 2.5).
10793
+ *
10794
+ * Delegates to the fixture-backed canonical implementation so mock mode is a
10795
+ * faithful stand-in for the same contract the Medusa adapter honors: scope ∩
10796
+ * categories, collection resolution, global ordering before pagination, the
10797
+ * real full match count, and a typed `unsupported-query` for advanced filters
10798
+ * the mock does not implement.
10799
+ */
10800
+ async getCatalogProducts(query) {
10801
+ return getMockCatalogProducts(query, { currencyCode: this.currencyCode });
10802
+ }
10037
10803
  async getProductRecommendations(productId) {
10038
10804
  return getMockProductRecommendations(productId);
10039
10805
  }
@@ -10169,6 +10935,25 @@ var MockProvider = class {
10169
10935
  const order = completeMockCheckout(cart, this.currencyCode, this.siteUrl);
10170
10936
  return order;
10171
10937
  }
10938
+ /**
10939
+ * Discriminated completion for the fixture provider.
10940
+ *
10941
+ * Completion is idempotent by construction (the fixture order id derives from
10942
+ * the cart id), so completing an already-completed cart returns the same
10943
+ * persisted order — mirroring the native reconcile path without a second
10944
+ * order.
10945
+ */
10946
+ async completeCart(cartId) {
10947
+ const cart = getMockCartState(cartId);
10948
+ if (!cart) {
10949
+ return {
10950
+ type: "unresolved",
10951
+ error: { message: `Cart not found: ${cartId}`, code: "CART_NOT_FOUND", status: 404 }
10952
+ };
10953
+ }
10954
+ const order = completeMockCheckout(cart, this.currencyCode, this.siteUrl);
10955
+ return { type: "order", order };
10956
+ }
10172
10957
  async getShippingMethods(cartId) {
10173
10958
  const cart = getMockCartState(cartId);
10174
10959
  return getMockShippingMethods(cart?.regionId);
@@ -10293,6 +11078,113 @@ var MockProvider = class {
10293
11078
  }
10294
11079
  };
10295
11080
 
11081
+ // src/factory/provider-types.ts
11082
+ var SupportedProviderType = /* @__PURE__ */ ((SupportedProviderType2) => {
11083
+ SupportedProviderType2["Mock"] = "mock";
11084
+ SupportedProviderType2["Medusa"] = "medusa";
11085
+ SupportedProviderType2["StelOrder"] = "stelorder";
11086
+ SupportedProviderType2["Shopify"] = "shopify";
11087
+ SupportedProviderType2["BigCommerce"] = "bigcommerce";
11088
+ SupportedProviderType2["WooCommerce"] = "woocommerce";
11089
+ SupportedProviderType2["Spree"] = "spree";
11090
+ SupportedProviderType2["Magento"] = "magento";
11091
+ return SupportedProviderType2;
11092
+ })(SupportedProviderType || {});
11093
+ var PROVIDER_METADATA = {
11094
+ mock: {
11095
+ name: "Mock",
11096
+ description: "Fixture-backed runtime provider for layout/design mode without a real backend",
11097
+ requiresStorefront: false,
11098
+ requiredEnvVars: [],
11099
+ maxRetries: 0
11100
+ },
11101
+ medusa: {
11102
+ name: "Medusa JS",
11103
+ description: "Medusa v2 commerce engine",
11104
+ requiresStorefront: true,
11105
+ requiredEnvVars: [
11106
+ "NEXT_PUBLIC_COMMERCE_API_URL",
11107
+ "NEXT_PUBLIC_COMMERCE_API_KEY",
11108
+ "NEXT_PUBLIC_SALES_CHANNEL_ID"
11109
+ ],
11110
+ maxRetries: 3
11111
+ },
11112
+ stelorder: {
11113
+ name: "Stel Order",
11114
+ description: "Stel Order ERP and sales document API",
11115
+ requiresStorefront: false,
11116
+ requiredEnvVars: [
11117
+ "NEXT_PUBLIC_COMMERCE_API_URL",
11118
+ "NEXT_PUBLIC_COMMERCE_API_KEY"
11119
+ ],
11120
+ maxRetries: 2
11121
+ },
11122
+ shopify: {
11123
+ name: "Shopify",
11124
+ description: "Shopify Storefront API (GraphQL)",
11125
+ requiresStorefront: false,
11126
+ requiredEnvVars: [
11127
+ "NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN",
11128
+ "NEXT_PUBLIC_SHOPIFY_STOREFRONT_TOKEN"
11129
+ ],
11130
+ maxRetries: 3
11131
+ },
11132
+ bigcommerce: {
11133
+ name: "BigCommerce",
11134
+ description: "BigCommerce REST Storefront API",
11135
+ requiresStorefront: false,
11136
+ requiredEnvVars: [
11137
+ "BIGCOMMERCE_STORE_HASH",
11138
+ "BIGCOMMERCE_STOREFRONT_API_TOKEN",
11139
+ "BIGCOMMERCE_CHANNEL_ID"
11140
+ ],
11141
+ maxRetries: 3
11142
+ },
11143
+ woocommerce: {
11144
+ name: "WooCommerce",
11145
+ description: "WooCommerce Store API + REST API v3",
11146
+ requiresStorefront: false,
11147
+ requiredEnvVars: [
11148
+ "WOOCOMMERCE_URL",
11149
+ "WOOCOMMERCE_CONSUMER_KEY",
11150
+ "WOOCOMMERCE_CONSUMER_SECRET"
11151
+ ],
11152
+ maxRetries: 3
11153
+ },
11154
+ spree: {
11155
+ name: "Spree Commerce",
11156
+ description: "Spree API v2 Storefront (JSON:API)",
11157
+ requiresStorefront: false,
11158
+ requiredEnvVars: [
11159
+ "SPREE_API_URL"
11160
+ ],
11161
+ maxRetries: 3
11162
+ },
11163
+ magento: {
11164
+ name: "Adobe Commerce (Magento)",
11165
+ description: "Adobe Commerce GraphQL + REST API",
11166
+ requiresStorefront: true,
11167
+ requiredEnvVars: [
11168
+ "MAGENTO_URL",
11169
+ "MAGENTO_STORE_CODE"
11170
+ ],
11171
+ maxRetries: 3
11172
+ }
11173
+ };
11174
+ function isSupportedProviderType(value) {
11175
+ return typeof value === "string" && Object.values(SupportedProviderType).includes(value);
11176
+ }
11177
+ function getProviderMetadata(type) {
11178
+ if (!isSupportedProviderType(type)) {
11179
+ throw new Error(
11180
+ `Unsupported provider: ${type}. Supported: ${Object.values(
11181
+ SupportedProviderType
11182
+ ).join(", ")}`
11183
+ );
11184
+ }
11185
+ return PROVIDER_METADATA[type];
11186
+ }
11187
+
10296
11188
  // src/factory/index.ts
10297
11189
  function createProvider(options) {
10298
11190
  if (!options || !options.provider) {
@@ -10343,11 +11235,13 @@ function isProviderType(provider, type) {
10343
11235
  MAGENTO_CAPABILITIES,
10344
11236
  MEDUSA_CAPABILITIES,
10345
11237
  MOCK_CAPABILITIES,
11238
+ MOCK_SALES_CHANNEL_ID,
10346
11239
  MagentoProvider,
10347
11240
  MedusaMetadataAdapter,
10348
11241
  MedusaProvider,
10349
11242
  MetricsService,
10350
11243
  MockProvider,
11244
+ PROVIDER_METADATA,
10351
11245
  ProviderAPIError,
10352
11246
  ProviderNotFoundError,
10353
11247
  ResourceNotFoundError,
@@ -10358,6 +11252,7 @@ function isProviderType(provider, type) {
10358
11252
  SpreeProvider,
10359
11253
  StelOrderProvider,
10360
11254
  StorefrontConfigError,
11255
+ SupportedProviderType,
10361
11256
  WOOCOMMERCE_CAPABILITIES,
10362
11257
  WooCommerceProvider,
10363
11258
  assertEntityBelongsToChannel,
@@ -10376,10 +11271,12 @@ function isProviderType(provider, type) {
10376
11271
  getBackendProviderId,
10377
11272
  getCapabilityDescription,
10378
11273
  getChannelScope,
11274
+ getProviderMetadata,
10379
11275
  isBackendCapabilities,
10380
11276
  isGlobalEntity,
10381
11277
  isProviderType,
10382
11278
  isScopedToChannel,
11279
+ isSupportedProviderType,
10383
11280
  moneyToNumber,
10384
11281
  moneyToNumberSafe,
10385
11282
  numberToMoney,