@thorprovider/adapters 5.0.9 → 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.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // src/index.ts
2
- import {
3
- StorefrontConfigError as StorefrontConfigError2
4
- } from "@thorprovider/types";
5
-
6
1
  // src/types/capabilities.types.ts
7
2
  var MOCK_CAPABILITIES = {
8
3
  updateProfile: false,
@@ -366,6 +361,16 @@ var ConfigurationError = class extends CommerceError {
366
361
  this.name = "ConfigurationError";
367
362
  }
368
363
  };
364
+ var StorefrontConfigError = class extends Error {
365
+ platformType;
366
+ requiredEnvVar;
367
+ constructor(message, platformType, requiredEnvVar) {
368
+ super(message);
369
+ this.name = "StorefrontConfigError";
370
+ this.platformType = platformType;
371
+ this.requiredEnvVar = requiredEnvVar;
372
+ }
373
+ };
369
374
  var ResourceNotFoundError = class extends CommerceError {
370
375
  constructor(resource, identifier) {
371
376
  super(`${resource} not found: ${identifier}`, "RESOURCE_NOT_FOUND", 404);
@@ -900,6 +905,16 @@ function transformMoney(amount, currencyCode = "USD") {
900
905
  currencyCode: currencyCode.toUpperCase()
901
906
  };
902
907
  }
908
+ function transformAmount(amount, currencyCode) {
909
+ const currencyCodeUpper = (currencyCode ?? "").toUpperCase();
910
+ if (typeof amount !== "number" || !Number.isFinite(amount)) {
911
+ return { amount: "", currencyCode: currencyCodeUpper };
912
+ }
913
+ return { amount: amount.toFixed(2), currencyCode: currencyCodeUpper };
914
+ }
915
+ function transformProductPrice(amount, currencyCode) {
916
+ return transformAmount(amount, currencyCode);
917
+ }
903
918
  function transformImage(url, alt = "") {
904
919
  return {
905
920
  url,
@@ -951,39 +966,42 @@ function transformProductOption(option) {
951
966
  values: option.values?.map((v) => v.value ?? "") ?? []
952
967
  };
953
968
  }
954
- function transformProductVariant(variant, currencyCode = "USD") {
969
+ function transformProductVariant(variant, currencyCode) {
955
970
  let priceAmount;
956
971
  let priceCurrency = currencyCode;
957
972
  if (variant.calculated_price) {
958
973
  priceAmount = variant.calculated_price.calculated_amount;
959
- priceCurrency = variant.calculated_price.currency_code || currencyCode;
974
+ priceCurrency = variant.calculated_price.currency_code ?? currencyCode;
960
975
  } else if (variant.prices && variant.prices.length > 0) {
961
- const price = variant.prices.find((p) => p.currency_code === currencyCode.toLowerCase());
976
+ const target = currencyCode?.toLowerCase();
977
+ const price = (target ? variant.prices.find((p) => p.currency_code === target) : void 0) ?? variant.prices[0];
962
978
  priceAmount = price?.amount;
963
- priceCurrency = price?.currency_code || currencyCode;
979
+ priceCurrency = price?.currency_code ?? currencyCode;
964
980
  }
965
981
  const isAvailable = variant.manage_inventory === false ? true : (variant.inventory_quantity ?? 0) > 0 || variant.allow_backorder === true;
966
982
  return {
967
983
  id: variant.id,
968
984
  title: variant.title ?? "Default",
985
+ // SKU is optional descriptive data only — never API/purchase identity.
969
986
  sku: variant.sku ?? void 0,
970
987
  availableForSale: isAvailable,
971
988
  selectedOptions: variant.options?.map((opt) => ({
972
989
  name: opt.option?.title ?? "",
973
990
  value: opt.value ?? ""
974
991
  })) ?? [],
975
- price: transformMoney(priceAmount, priceCurrency),
992
+ price: transformProductPrice(priceAmount, priceCurrency),
976
993
  compareAtPrice: void 0,
977
994
  // Medusa doesn't have compare-at pricing
978
995
  quantityAvailable: variant.manage_inventory ? variant.inventory_quantity : void 0,
979
996
  image: variant.thumbnail ? transformImage(variant.thumbnail, variant.title ?? "") : void 0
980
997
  };
981
998
  }
982
- function transformProduct(product, currencyCode = "USD") {
999
+ function transformProduct(product, currencyCode) {
983
1000
  const variants = product.variants?.map((v) => transformProductVariant(v, currencyCode)) ?? [];
984
- const prices = variants.map((v) => parseFloat(v.price.amount));
985
- const minPrice = Math.min(...prices);
986
- const maxPrice = Math.max(...prices);
1001
+ const validPrices = variants.map((v) => v.price).filter((money) => money.amount !== "" && Number.isFinite(Number(money.amount))).map((money) => Number(money.amount));
1002
+ const rangeCurrency = variants.find((v) => v.price.currencyCode)?.price.currencyCode ?? (currencyCode ?? "").toUpperCase();
1003
+ const minPrice = validPrices.length > 0 ? Math.min(...validPrices) : void 0;
1004
+ const maxPrice = validPrices.length > 0 ? Math.max(...validPrices) : void 0;
987
1005
  return {
988
1006
  id: product.id,
989
1007
  handle: product.handle ?? "",
@@ -999,10 +1017,16 @@ function transformProduct(product, currencyCode = "USD") {
999
1017
  images: product.images?.map((img) => transformImage(img.url ?? "", product.title ?? "")) ?? [],
1000
1018
  priceRange: {
1001
1019
  // Build Money directly from already-resolved major-unit prices.
1002
- // Do NOT pass through transformMoney (which would call toFixed on already-correct values)
1003
- // and do NOT multiply by 100 (that was a compensatory hack for the old /100 bug).
1004
- minVariantPrice: { amount: (Number.isFinite(minPrice) ? minPrice : 0).toFixed(2), currencyCode: currencyCode.toUpperCase() },
1005
- maxVariantPrice: { amount: (Number.isFinite(maxPrice) ? maxPrice : 0).toFixed(2), currencyCode: currencyCode.toUpperCase() }
1020
+ // Do NOT multiply by 100 (that was a compensatory hack for the old /100
1021
+ // bug) and do NOT fabricate a zero for an unresolved price.
1022
+ minVariantPrice: {
1023
+ amount: minPrice !== void 0 ? minPrice.toFixed(2) : "",
1024
+ currencyCode: rangeCurrency
1025
+ },
1026
+ maxVariantPrice: {
1027
+ amount: maxPrice !== void 0 ? maxPrice.toFixed(2) : "",
1028
+ currencyCode: rangeCurrency
1029
+ }
1006
1030
  },
1007
1031
  collections: product.collection ? [{
1008
1032
  id: product.collection.id,
@@ -1025,38 +1049,19 @@ function transformProduct(product, currencyCode = "USD") {
1025
1049
  function resolveMedusaCartItemCurrencyCode(item, cartCurrencyCode) {
1026
1050
  return item?.cart?.currency_code ?? cartCurrencyCode ?? "usd";
1027
1051
  }
1028
- function resolveMedusaCartItemTotal(item) {
1029
- const quantity = item?.quantity ?? 1;
1030
- const safeQuantity = typeof quantity === "number" && quantity > 0 ? quantity : 1;
1031
- const totalCandidates = [
1032
- item?.total,
1033
- item?.item_total,
1034
- item?.original_total,
1035
- item?.subtotal,
1036
- item?.item_subtotal,
1037
- item?.original_subtotal
1038
- ];
1039
- for (const candidate of totalCandidates) {
1040
- if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0) {
1052
+ function resolveAuthoritativeLineTotal(item) {
1053
+ const candidates = [item?.total, item?.item_total, item?.subtotal];
1054
+ for (const candidate of candidates) {
1055
+ if (typeof candidate === "number" && Number.isFinite(candidate)) {
1041
1056
  return candidate;
1042
1057
  }
1043
1058
  }
1044
- const unitCandidates = [
1045
- item?.unit_price,
1046
- item?.original_unit_price
1047
- ];
1048
- for (const candidate of unitCandidates) {
1049
- if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0) {
1050
- return candidate * safeQuantity;
1051
- }
1052
- }
1053
- return 0;
1059
+ return void 0;
1054
1060
  }
1055
1061
  function transformCartItem(item, cartCurrencyCode) {
1056
1062
  const variant = item.variant;
1057
1063
  const product = variant?.product;
1058
1064
  const currencyCode = resolveMedusaCartItemCurrencyCode(item, cartCurrencyCode);
1059
- const totalCents = resolveMedusaCartItemTotal(item);
1060
1065
  const merchandiseId = variant?.id ?? item.variant_id ?? "";
1061
1066
  const merchandiseTitle = variant?.title ?? item.variant_title ?? "";
1062
1067
  const productId = product?.id ?? item.product_id ?? "";
@@ -1067,7 +1072,9 @@ function transformCartItem(item, cartCurrencyCode) {
1067
1072
  id: item.id,
1068
1073
  quantity: item.quantity ?? 1,
1069
1074
  cost: {
1070
- totalAmount: transformMoney(totalCents, currencyCode)
1075
+ totalAmount: transformAmount(resolveAuthoritativeLineTotal(item), currencyCode),
1076
+ // Backend unit price, carried so the display never derives it locally.
1077
+ unitPrice: transformAmount(item?.unit_price, currencyCode)
1071
1078
  },
1072
1079
  merchandise: {
1073
1080
  id: merchandiseId,
@@ -1086,19 +1093,30 @@ function transformCartItem(item, cartCurrencyCode) {
1086
1093
  };
1087
1094
  }
1088
1095
  function transformCart(cart) {
1089
- const currencyCode = cart.currency_code ?? "usd";
1096
+ const currencyCode = cart.currency_code;
1090
1097
  return {
1091
1098
  id: cart.id,
1092
1099
  checkoutUrl: `/checkout/${cart.id}`,
1093
1100
  // Medusa doesn't have a direct checkout URL
1094
1101
  cost: {
1095
- subtotalAmount: transformMoney(cart.subtotal, currencyCode),
1096
- totalAmount: transformMoney(cart.total, currencyCode),
1097
- totalTaxAmount: transformMoney(cart.tax_total, currencyCode)
1102
+ subtotalAmount: transformAmount(
1103
+ typeof cart.item_subtotal === "number" ? cart.item_subtotal : cart.subtotal,
1104
+ currencyCode
1105
+ ),
1106
+ discountAmount: transformAmount(cart.discount_total, currencyCode),
1107
+ totalTaxAmount: transformAmount(cart.tax_total, currencyCode),
1108
+ shippingAmount: transformAmount(cart.shipping_total, currencyCode),
1109
+ totalAmount: transformAmount(cart.total, currencyCode)
1098
1110
  },
1099
1111
  lines: cart.items?.map((item) => transformCartItem(item, currencyCode)) ?? [],
1100
1112
  totalQuantity: cart.items?.reduce((sum, item) => sum + (item.quantity ?? 0), 0) ?? 0,
1101
- completedAt: cart.completed_at ? typeof cart.completed_at === "string" ? cart.completed_at : cart.completed_at?.toISOString() : void 0
1113
+ salesChannelId: cart.sales_channel_id ?? void 0,
1114
+ regionId: cart.region_id ?? void 0,
1115
+ currencyCode: typeof currencyCode === "string" ? currencyCode.toUpperCase() : void 0,
1116
+ selectedShippingMethodId: cart.shipping_methods?.[0]?.id ?? void 0,
1117
+ selectedShippingOptionId: cart.shipping_methods?.[0]?.shipping_option_id ?? void 0,
1118
+ completedAt: cart.completed_at ? typeof cart.completed_at === "string" ? cart.completed_at : cart.completed_at?.toISOString() : void 0,
1119
+ metadata: cart.metadata ?? void 0
1102
1120
  };
1103
1121
  }
1104
1122
  function transformCollection(collection) {
@@ -1187,8 +1205,8 @@ function transformOrderItem(item, currencyCode) {
1187
1205
  title: item.product_title ?? item.title ?? "",
1188
1206
  variantTitle: item.variant_title ?? variant?.title ?? void 0,
1189
1207
  quantity: item.quantity ?? 1,
1190
- price: transformMoney(item.unit_price, currencyCode),
1191
- total: transformMoney(item.total, currencyCode),
1208
+ price: transformAmount(item.unit_price, currencyCode),
1209
+ total: transformAmount(item.total, currencyCode),
1192
1210
  sku: variant?.sku ?? void 0,
1193
1211
  image: product?.thumbnail || item.thumbnail ? transformImage(product?.thumbnail || item.thumbnail, item.product_title ?? "") : void 0,
1194
1212
  productId: item.product_id ?? product?.id ?? void 0,
@@ -1239,12 +1257,19 @@ function transformOrder(order) {
1239
1257
  customer: order.customer ? transformCustomer(order.customer, order.email) : void 0,
1240
1258
  // Items
1241
1259
  items: (order.items ?? []).filter((item) => item != null).map((item) => transformOrderItem(item, currencyCode)) ?? [],
1242
- // Pricing
1243
- subtotal: transformMoney(order.subtotal, currencyCode),
1244
- total: transformMoney(order.total, currencyCode),
1245
- tax: transformMoney(order.tax_total, currencyCode),
1246
- shipping: transformMoney(order.shipping_total, currencyCode),
1247
- discount: transformMoney(order.discount_total, currencyCode),
1260
+ // Pricing — backend-authoritative; a legitimate zero is preserved and no
1261
+ // amount is derived locally. `subtotal` prefers the backend's
1262
+ // `item_subtotal` (lines only, excluding shipping/tax); Medusa's native
1263
+ // `subtotal` folds in `shipping_subtotal`, so falling back to it is only
1264
+ // for installs that do not supply `item_subtotal`.
1265
+ subtotal: transformAmount(
1266
+ typeof order.item_subtotal === "number" ? order.item_subtotal : order.subtotal,
1267
+ currencyCode
1268
+ ),
1269
+ total: transformAmount(order.total, currencyCode),
1270
+ tax: transformAmount(order.tax_total, currencyCode),
1271
+ shipping: transformAmount(order.shipping_total, currencyCode),
1272
+ discount: transformAmount(order.discount_total, currencyCode),
1248
1273
  // Addresses (transformed to camelCase)
1249
1274
  shippingAddress: order.shipping_address ? transformAddress(order.shipping_address) : void 0,
1250
1275
  billingAddress: order.billing_address ? transformAddress(order.billing_address) : void 0,
@@ -1663,7 +1688,6 @@ function assertEntityBelongsToChannel(entity, expectedChannelId) {
1663
1688
 
1664
1689
  // src/providers/medusa/index.ts
1665
1690
  import Medusa2 from "@medusajs/js-sdk";
1666
- import { StorefrontConfigError } from "@thorprovider/types";
1667
1691
 
1668
1692
  // src/providers/medusa/auth.ts
1669
1693
  var AuthStorageManager = class {
@@ -2193,10 +2217,156 @@ async function getPaymentMethods(sdk, options) {
2193
2217
  }
2194
2218
 
2195
2219
  // src/providers/medusa/index.ts
2196
- var MEDUSA_CART_RETRIEVE_FIELDS = "+items,+items.variant,+items.variant.product,+items.variant.product.thumbnail,+items.variant.options,+items.variant.options.option";
2220
+ 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";
2221
+ var MEDUSA_ORDER_COMPLETION_FIELDS = "*items";
2222
+ var DEFAULT_CATALOG_PAGE_SIZE = 50;
2223
+ function resolveCatalogPageSize(configured) {
2224
+ const fromEnv = process.env.MEDUSA_CATALOG_PAGE_SIZE !== void 0 ? Number(process.env.MEDUSA_CATALOG_PAGE_SIZE) : void 0;
2225
+ const raw = configured ?? fromEnv;
2226
+ if (raw === void 0 || !Number.isFinite(raw) || raw < 1) {
2227
+ return DEFAULT_CATALOG_PAGE_SIZE;
2228
+ }
2229
+ return Math.floor(raw);
2230
+ }
2231
+ function resolveConfiguredSalesChannelId() {
2232
+ return process.env.MEDUSA_SALES_CHANNEL_ID ?? process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
2233
+ }
2234
+ var MEDUSA_SORT_KEY_MAP = {
2235
+ featured: null,
2236
+ name: "title",
2237
+ newest: "-created_at",
2238
+ "price-asc": null,
2239
+ "price-desc": null,
2240
+ rating: null
2241
+ };
2242
+ function resolveEffectiveCategoryHandles(query) {
2243
+ const { categoryScope, categories } = query;
2244
+ if (categoryScope === void 0) return categories;
2245
+ if (categories === void 0) return [categoryScope];
2246
+ return categories.filter((handle) => handle === categoryScope);
2247
+ }
2248
+ function buildAppliedCatalogFilters(query, effectiveCategoryHandles, includeAdvanced = false) {
2249
+ return {
2250
+ ...query.text ? { text: query.text } : {},
2251
+ ...effectiveCategoryHandles !== void 0 ? { categories: effectiveCategoryHandles } : {},
2252
+ ...query.collections !== void 0 ? { collections: query.collections } : {},
2253
+ ...includeAdvanced && query.priceMin !== void 0 ? { priceMin: query.priceMin } : {},
2254
+ ...includeAdvanced && query.priceMax !== void 0 ? { priceMax: query.priceMax } : {},
2255
+ ...includeAdvanced && query.inStock !== void 0 ? { inStock: query.inStock } : {},
2256
+ ...includeAdvanced && query.attributes !== void 0 ? { attributes: query.attributes } : {}
2257
+ };
2258
+ }
2259
+ function buildBasicDemonstratedQuery(query) {
2260
+ return {
2261
+ ...query.text !== void 0 ? { text: query.text } : {},
2262
+ ...query.categoryScope !== void 0 ? { categoryScope: query.categoryScope } : {},
2263
+ ...query.categories !== void 0 ? { categories: query.categories } : {},
2264
+ ...query.collections !== void 0 ? { collections: query.collections } : {}
2265
+ };
2266
+ }
2267
+ var ADVANCED_SEARCH_ENDPOINT = "/store/search";
2268
+ var BASIC_LISTING_ENDPOINT = "/store/products";
2269
+ function collectRequestedAdvancedFields(query) {
2270
+ const fields = [];
2271
+ if (query.priceMin !== void 0) fields.push("priceMin");
2272
+ if (query.priceMax !== void 0) fields.push("priceMax");
2273
+ if (query.inStock !== void 0) fields.push("inStock");
2274
+ for (const name of Object.keys(query.attributes ?? {})) {
2275
+ fields.push(`attr.${name}`);
2276
+ }
2277
+ return fields;
2278
+ }
2279
+ function advancedFieldToFacet(field) {
2280
+ if (field === "priceMin" || field === "priceMax") return "price";
2281
+ return field;
2282
+ }
2283
+ function collectAdvancedSearchRequestFields(options) {
2284
+ const fields = [];
2285
+ if (options?.priceMin !== void 0) fields.push("priceMin");
2286
+ if (options?.priceMax !== void 0) fields.push("priceMax");
2287
+ if (options?.inStockOnly === true) fields.push("inStock");
2288
+ if (options?.attributes && options.attributes.length > 0) {
2289
+ fields.push("attributes");
2290
+ }
2291
+ return fields;
2292
+ }
2293
+ function toFiniteNumber(value) {
2294
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2295
+ if (typeof value === "string" && value.trim() !== "") {
2296
+ const parsed = Number(value);
2297
+ return Number.isFinite(parsed) ? parsed : void 0;
2298
+ }
2299
+ return void 0;
2300
+ }
2301
+ function readAppliedPriceRange(applied) {
2302
+ const range = applied.price_range;
2303
+ if (!range || typeof range !== "object" || Array.isArray(range)) return {};
2304
+ const record = range;
2305
+ return { min: toFiniteNumber(record.min), max: toFiniteNumber(record.max) };
2306
+ }
2307
+ function mapAppliedFiltersToFacets(applied) {
2308
+ const facets = /* @__PURE__ */ new Set();
2309
+ const priceRange = readAppliedPriceRange(applied);
2310
+ const hasAppliedPriceRange = priceRange.min !== void 0 && priceRange.max !== void 0;
2311
+ if (hasAppliedPriceRange || "price" in applied || "price_min" in applied || "price_max" in applied) {
2312
+ facets.add("price");
2313
+ }
2314
+ if (typeof applied.is_available === "boolean" || "inStock" in applied || "isAvailable" in applied || "available" in applied) {
2315
+ facets.add("inStock");
2316
+ }
2317
+ const attributes = applied.attributes;
2318
+ if (attributes && typeof attributes === "object" && !Array.isArray(attributes)) {
2319
+ for (const name of Object.keys(attributes)) {
2320
+ facets.add(`attr.${name}`);
2321
+ }
2322
+ }
2323
+ return facets;
2324
+ }
2325
+ function collectUnappliedAdvancedFields(applied, query) {
2326
+ const unapplied = [];
2327
+ const priceRange = readAppliedPriceRange(applied);
2328
+ if (query.priceMin !== void 0) {
2329
+ const echoed = toFiniteNumber(
2330
+ priceRange.min ?? applied.price_min ?? applied.priceMin
2331
+ );
2332
+ if (echoed !== query.priceMin) unapplied.push("priceMin");
2333
+ }
2334
+ if (query.priceMax !== void 0) {
2335
+ const echoed = toFiniteNumber(
2336
+ priceRange.max ?? applied.price_max ?? applied.priceMax
2337
+ );
2338
+ if (echoed !== query.priceMax) unapplied.push("priceMax");
2339
+ }
2340
+ if (query.inStock === true) {
2341
+ const echoed = applied.is_available ?? applied.isAvailable ?? applied.inStock ?? applied.available;
2342
+ if (echoed !== true && echoed !== "true") unapplied.push("inStock");
2343
+ }
2344
+ const requestedAttributes = query.attributes ?? {};
2345
+ const appliedAttributes = applied.attributes;
2346
+ for (const name of Object.keys(requestedAttributes)) {
2347
+ const hasName = !!appliedAttributes && typeof appliedAttributes === "object" && !Array.isArray(appliedAttributes) && name in appliedAttributes;
2348
+ if (!hasName) unapplied.push(`attr.${name}`);
2349
+ }
2350
+ return unapplied;
2351
+ }
2352
+ function readFiniteFacetNumber(value) {
2353
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2354
+ }
2197
2355
  var MedusaProvider = class {
2198
2356
  name = "medusa";
2199
2357
  version = "2.0.0";
2358
+ /**
2359
+ * Canonical catalog sort keys this provider applies at the query level.
2360
+ *
2361
+ * Advertises only the backend-verified orderings (E2 Decision 6). `price-asc`,
2362
+ * `price-desc`, and `rating` are intentionally absent: they are not applied at
2363
+ * the query level, so the listing never offers them as global sorts.
2364
+ *
2365
+ * The literal tuple is assignable to `CommerceProvider['supportedCatalogSortKeys']`
2366
+ * (`readonly CatalogSortKey[]`); it is written structurally here so the adapter
2367
+ * compiles against the published `@thorprovider/types` declaration.
2368
+ */
2369
+ supportedCatalogSortKeys = ["featured", "name", "newest"];
2200
2370
  /**
2201
2371
  * Medusa backend capabilities.
2202
2372
  * Starts with static defaults, replaced by dynamic values from
@@ -2218,6 +2388,14 @@ var MedusaProvider = class {
2218
2388
  baseUrl;
2219
2389
  publishableKey;
2220
2390
  regionId;
2391
+ /** Configured per-request catalog listing bound (never an unbounded scan). */
2392
+ catalogPageSize;
2393
+ /**
2394
+ * Per-scope cache of the truthful facet-support descriptor. The optional
2395
+ * advanced endpoint is probed once per demonstrated scope/query rather than on
2396
+ * every listing request (E2 task 2.4).
2397
+ */
2398
+ facetSupportCache = /* @__PURE__ */ new Map();
2221
2399
  /** Secret API key for admin operations (if configured). */
2222
2400
  adminApiKey;
2223
2401
  /**
@@ -2262,6 +2440,7 @@ var MedusaProvider = class {
2262
2440
  this.baseUrl = config.baseUrl;
2263
2441
  this.publishableKey = config.publishableKey;
2264
2442
  this.regionId = config.regionId;
2443
+ this.catalogPageSize = resolveCatalogPageSize(config.catalogPageSize);
2265
2444
  this.retryConfig = {
2266
2445
  maxRetries: config.retry?.maxRetries ?? 3,
2267
2446
  timeoutMs: config.retry?.timeoutMs ?? 1e4,
@@ -2283,7 +2462,16 @@ var MedusaProvider = class {
2283
2462
  debug: config.debug ?? false,
2284
2463
  // Always use JWT mode for auth - SDK needs to send Bearer tokens
2285
2464
  // Configure storage method for JWT tokens (browser only)
2286
- auth: config.auth ? {
2465
+ auth: config.auth ? config.auth.customStorage ? {
2466
+ type: "jwt",
2467
+ // Request-scoped custom storage: the caller supplies a per-request
2468
+ // token holder (for example a server request's customer JWT). The
2469
+ // SDK reads the token on every request and writes the login /
2470
+ // registration token back into the same holder, so a fresh
2471
+ // provider per request is authenticated without shared state.
2472
+ jwtTokenStorageMethod: "custom",
2473
+ storage: config.auth.customStorage
2474
+ } : {
2287
2475
  type: "jwt",
2288
2476
  // Use localStorage in browser, memory storage on server
2289
2477
  jwtTokenStorageMethod: typeof window !== "undefined" ? "local" : "memory"
@@ -2391,35 +2579,52 @@ var MedusaProvider = class {
2391
2579
  }
2392
2580
  throw lastError;
2393
2581
  }
2582
+ /** Fields needed to price a product variant for the current region. */
2583
+ buildProductFields(regionId) {
2584
+ const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+tags";
2585
+ const pricingFields = ",+variants.calculated_price,+variants.prices";
2586
+ return regionId ? baseFields + pricingFields : baseFields;
2587
+ }
2588
+ /** Scope a store query to the configured storefront sales channel, if any. */
2589
+ applySalesChannelScope(queryParams) {
2590
+ const salesChannelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
2591
+ if (salesChannelId) {
2592
+ queryParams.sales_channel_id = [salesChannelId];
2593
+ }
2594
+ }
2595
+ /**
2596
+ * Directed single-product lookup by a supported native filter (`handle` or
2597
+ * `id`). This is independent of catalog listing pagination, so a known
2598
+ * product beyond the first listing page still resolves without scanning.
2599
+ *
2600
+ * Medusa's store API supports `handle` and `id` on `/store/products`; it
2601
+ * does not expose a variant filter there, so a variant is resolved from the
2602
+ * product once the product is known (see `getProductDetailByHandle` in the
2603
+ * runtime). No `limit` beyond 1 is issued.
2604
+ */
2605
+ async fetchSingleProduct(filter, options) {
2606
+ const regionId = options?.regionId ?? this.regionId;
2607
+ const queryParams = {
2608
+ ...filter,
2609
+ limit: 1,
2610
+ fields: this.buildProductFields(regionId)
2611
+ };
2612
+ if (regionId) {
2613
+ queryParams.region_id = regionId;
2614
+ }
2615
+ this.applySalesChannelScope(queryParams);
2616
+ const { products } = await this.sdk.store.product.list(queryParams);
2617
+ if (!products || products.length === 0) {
2618
+ this.logger.debug("Product not found", filter);
2619
+ return void 0;
2620
+ }
2621
+ return transformProduct(products[0], this.currencyCode);
2622
+ }
2394
2623
  async getProduct(handle, options) {
2395
2624
  this.logger.debug("getProduct", { handle, regionId: options?.regionId });
2396
2625
  try {
2397
2626
  return await this.withRetry(
2398
- async () => {
2399
- const regionId = options?.regionId ?? this.regionId;
2400
- const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+tags";
2401
- const pricingFields = ",+variants.calculated_price,+variants.prices";
2402
- const fields = regionId ? baseFields + pricingFields : baseFields;
2403
- const queryParams = {
2404
- handle,
2405
- limit: 1,
2406
- fields
2407
- };
2408
- if (regionId) {
2409
- queryParams.region_id = regionId;
2410
- }
2411
- const salesChannelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
2412
- if (salesChannelId) {
2413
- queryParams.sales_channel_id = [salesChannelId];
2414
- }
2415
- const { products } = await this.sdk.store.product.list(queryParams);
2416
- if (!products || products.length === 0) {
2417
- this.logger.debug("Product not found", { handle });
2418
- return void 0;
2419
- }
2420
- const transformedProduct = transformProduct(products[0], this.currencyCode);
2421
- return transformedProduct;
2422
- },
2627
+ () => this.fetchSingleProduct({ handle }, options),
2423
2628
  "getProduct",
2424
2629
  { handle, regionId: options?.regionId }
2425
2630
  );
@@ -2431,6 +2636,26 @@ var MedusaProvider = class {
2431
2636
  );
2432
2637
  }
2433
2638
  }
2639
+ /**
2640
+ * Directed product lookup by backend product ID. Independent of the first
2641
+ * listing page, mirroring `getProduct` for handle-based resolution.
2642
+ */
2643
+ async getProductById(id, options) {
2644
+ this.logger.debug("getProductById", { id, regionId: options?.regionId });
2645
+ try {
2646
+ return await this.withRetry(
2647
+ () => this.fetchSingleProduct({ id }, options),
2648
+ "getProductById",
2649
+ { id, regionId: options?.regionId }
2650
+ );
2651
+ } catch (error) {
2652
+ throw new ProviderAPIError(
2653
+ `Failed to fetch product by id: ${error.message}`,
2654
+ "medusa",
2655
+ error.status
2656
+ );
2657
+ }
2658
+ }
2434
2659
  async getProducts(options = {}) {
2435
2660
  this.logger.debug("getProducts", options);
2436
2661
  try {
@@ -2440,28 +2665,14 @@ var MedusaProvider = class {
2440
2665
  const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+tags";
2441
2666
  const pricingFields = ",+variants.calculated_price,+variants.prices";
2442
2667
  const fields = regionId ? baseFields + pricingFields : baseFields;
2443
- const SORT_KEY_MAP = {
2444
- featured: null,
2445
- // server default order
2446
- name: "title",
2447
- // A→Z by product title
2448
- newest: "-created_at",
2449
- // newest products first
2450
- "price-asc": null,
2451
- // not supported by native endpoint → client-side
2452
- "price-desc": null,
2453
- // not supported by native endpoint → client-side
2454
- rating: null
2455
- // not supported → server default
2456
- };
2457
2668
  const queryParams = {
2458
2669
  q: options.query,
2459
- limit: options.first ?? 100,
2460
- offset: options.after ? parseInt(String(options.after), 10) : void 0,
2670
+ limit: options.first ?? this.catalogPageSize,
2671
+ offset: options.after !== void 0 ? parseInt(String(options.after), 10) : options.skip,
2461
2672
  fields
2462
2673
  };
2463
2674
  if (options.sortKey) {
2464
- const mappedOrder = SORT_KEY_MAP[options.sortKey] ?? null;
2675
+ const mappedOrder = MEDUSA_SORT_KEY_MAP[options.sortKey] ?? null;
2465
2676
  if (mappedOrder !== null) {
2466
2677
  queryParams.order = mappedOrder;
2467
2678
  }
@@ -2501,6 +2712,468 @@ var MedusaProvider = class {
2501
2712
  );
2502
2713
  }
2503
2714
  }
2715
+ /**
2716
+ * Canonical catalog query path (E2 task 2.2).
2717
+ *
2718
+ * Accepts a validated `CatalogQuery`, translates it into Medusa Store
2719
+ * `/store/products` parameters, and returns a truthful `CatalogQueryOutcome`.
2720
+ * The route category scope is always retained and intersected with the
2721
+ * `categories` filter; `categories`/`collections` handles are resolved to
2722
+ * backend IDs through the existing directed lookups (`getCategory`/
2723
+ * `getCollection`). Any requested filter this provider cannot apply
2724
+ * (price/`inStock`/attributes) is rejected as a typed `unsupported-query`
2725
+ * outcome naming the fields — never a partial success and never a silent
2726
+ * drop. Pagination is `offset = (page - 1) * pageSize`, defaulting to the
2727
+ * canonical page size.
2728
+ *
2729
+ * Legacy `getProducts`/`getCategoryProducts` are unchanged; this additive
2730
+ * method is the typed seam the runtime query bridge (E2 task 3.1) consumes.
2731
+ */
2732
+ async getCatalogProducts(query) {
2733
+ this.logger.debug("getCatalogProducts", query);
2734
+ if (query.sort !== void 0 && !this.supportedCatalogSortKeys.includes(query.sort)) {
2735
+ return this.unsupportedCatalogOutcome(["sort"]);
2736
+ }
2737
+ const effectiveCategoryHandles = resolveEffectiveCategoryHandles(query);
2738
+ try {
2739
+ const categoryIds = effectiveCategoryHandles !== void 0 ? await this.resolveCategoryIds(effectiveCategoryHandles) : void 0;
2740
+ const collectionIds = query.collections ? await this.resolveCollectionIds(query.collections) : void 0;
2741
+ const facetSupport = await this.getCatalogFacetSupport({
2742
+ query,
2743
+ categoryIds,
2744
+ collectionIds,
2745
+ scope: query.categoryScope
2746
+ });
2747
+ const requestedAdvanced = collectRequestedAdvancedFields(query);
2748
+ const heldAdvanced = requestedAdvanced.filter(
2749
+ (field) => !facetSupport.offered.includes(advancedFieldToFacet(field))
2750
+ );
2751
+ if (heldAdvanced.length > 0) {
2752
+ return this.unsupportedCatalogOutcome(heldAdvanced);
2753
+ }
2754
+ const emptyIntersection = effectiveCategoryHandles !== void 0 && effectiveCategoryHandles.length === 0;
2755
+ const categoryFilterEmpty = effectiveCategoryHandles !== void 0 && (categoryIds?.length ?? 0) === 0;
2756
+ const collectionFilterEmpty = query.collections !== void 0 && (collectionIds?.length ?? 0) === 0;
2757
+ if (emptyIntersection || categoryFilterEmpty || collectionFilterEmpty) {
2758
+ return {
2759
+ ok: true,
2760
+ results: {
2761
+ products: [],
2762
+ appliedFilters: buildAppliedCatalogFilters(
2763
+ query,
2764
+ effectiveCategoryHandles,
2765
+ requestedAdvanced.length > 0
2766
+ ),
2767
+ facetSupport
2768
+ }
2769
+ };
2770
+ }
2771
+ if (requestedAdvanced.length > 0) {
2772
+ return this.queryAdvancedCatalog(
2773
+ query,
2774
+ categoryIds,
2775
+ collectionIds,
2776
+ facetSupport
2777
+ );
2778
+ }
2779
+ return await this.queryBasicCatalog(
2780
+ query,
2781
+ effectiveCategoryHandles,
2782
+ categoryIds,
2783
+ collectionIds,
2784
+ facetSupport
2785
+ );
2786
+ } catch (error) {
2787
+ this.logger.error("getCatalogProducts error details", {
2788
+ message: error.message,
2789
+ status: error.status
2790
+ });
2791
+ throw new ProviderAPIError(
2792
+ `Failed to run catalog query: ${error.message || "Unknown error"}`,
2793
+ "medusa",
2794
+ error.status
2795
+ );
2796
+ }
2797
+ }
2798
+ /**
2799
+ * Execute a basic-only canonical query against the equivalent basic endpoint
2800
+ * `/store/products`, reporting the filters it actually applied.
2801
+ */
2802
+ async queryBasicCatalog(query, effectiveCategoryHandles, categoryIds, collectionIds, facetSupport) {
2803
+ const pageSize = query.pageSize ?? this.catalogPageSize;
2804
+ const page = query.page ?? 1;
2805
+ const regionId = this.regionId;
2806
+ const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+thumbnail,+tags";
2807
+ const pricingFields = ",+variants.calculated_price,+variants.prices";
2808
+ const queryParams = {
2809
+ limit: pageSize,
2810
+ offset: (page - 1) * pageSize,
2811
+ fields: regionId ? baseFields + pricingFields : baseFields
2812
+ };
2813
+ if (query.text) {
2814
+ queryParams.q = query.text;
2815
+ }
2816
+ if (categoryIds && categoryIds.length > 0) {
2817
+ queryParams.category_id = categoryIds;
2818
+ }
2819
+ if (collectionIds && collectionIds.length > 0) {
2820
+ queryParams.collection_id = collectionIds;
2821
+ }
2822
+ if (query.sort) {
2823
+ const mappedOrder = MEDUSA_SORT_KEY_MAP[query.sort] ?? null;
2824
+ if (mappedOrder !== null) {
2825
+ queryParams.order = mappedOrder;
2826
+ }
2827
+ }
2828
+ if (regionId) {
2829
+ queryParams.region_id = regionId;
2830
+ }
2831
+ const salesChannelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
2832
+ if (salesChannelId) {
2833
+ queryParams.sales_channel_id = [salesChannelId];
2834
+ }
2835
+ const response = await this.sdk.store.product.list(queryParams);
2836
+ const products = (response.products ?? []).map(
2837
+ (p) => transformProduct(p, this.currencyCode)
2838
+ );
2839
+ const count = response.count;
2840
+ return {
2841
+ ok: true,
2842
+ results: {
2843
+ products,
2844
+ // Total is only the backend's actual count; never manufactured.
2845
+ ...typeof count === "number" ? { total: count } : {},
2846
+ appliedFilters: buildAppliedCatalogFilters(
2847
+ query,
2848
+ effectiveCategoryHandles
2849
+ ),
2850
+ facetSupport
2851
+ }
2852
+ };
2853
+ }
2854
+ /**
2855
+ * Execute an advanced canonical query against the real optional endpoint
2856
+ * `/store/search`, then re-validate that the response applied every requested
2857
+ * filter. A response that does not confirm a requested field, or an endpoint
2858
+ * failure, yields a typed `unsupported-query` error — never a partial result
2859
+ * labelled success and never a silent fallback to the basic endpoint.
2860
+ */
2861
+ async queryAdvancedCatalog(query, categoryIds, collectionIds, facetSupport) {
2862
+ const pageSize = query.pageSize ?? this.catalogPageSize;
2863
+ const page = query.page ?? 1;
2864
+ const advancedQuery = this.buildAdvancedSearchQuery(
2865
+ query,
2866
+ categoryIds ?? [],
2867
+ collectionIds ?? []
2868
+ );
2869
+ advancedQuery.limit = pageSize;
2870
+ advancedQuery.offset = (page - 1) * pageSize;
2871
+ let data;
2872
+ try {
2873
+ data = await this.sdk.client.fetch(ADVANCED_SEARCH_ENDPOINT, {
2874
+ query: advancedQuery
2875
+ });
2876
+ } catch (error) {
2877
+ this.logger.error("Advanced catalog listing failed", {
2878
+ message: error?.message,
2879
+ status: error?.status
2880
+ });
2881
+ return this.unsupportedCatalogOutcome(
2882
+ collectRequestedAdvancedFields(query)
2883
+ );
2884
+ }
2885
+ const appliedRaw = data?.applied_filters ?? data?.appliedFilters;
2886
+ if (!appliedRaw || typeof appliedRaw !== "object") {
2887
+ this.logger.warn("Advanced endpoint did not echo applied filters", {
2888
+ endpoint: ADVANCED_SEARCH_ENDPOINT
2889
+ });
2890
+ return this.unsupportedCatalogOutcome(
2891
+ collectRequestedAdvancedFields(query)
2892
+ );
2893
+ }
2894
+ const unapplied = collectUnappliedAdvancedFields(
2895
+ appliedRaw,
2896
+ query
2897
+ );
2898
+ if (unapplied.length > 0) {
2899
+ this.logger.warn("Advanced endpoint did not apply requested filters", {
2900
+ endpoint: ADVANCED_SEARCH_ENDPOINT,
2901
+ fields: unapplied
2902
+ });
2903
+ return this.unsupportedCatalogOutcome(unapplied);
2904
+ }
2905
+ const products = (data.hits ?? []).map(
2906
+ (hit) => transformProduct(hit, this.currencyCode)
2907
+ );
2908
+ const total = typeof data.estimatedTotalHits === "number" ? data.estimatedTotalHits : typeof data.count === "number" ? data.count : void 0;
2909
+ return {
2910
+ ok: true,
2911
+ results: {
2912
+ products,
2913
+ ...total !== void 0 ? { total } : {},
2914
+ appliedFilters: buildAppliedCatalogFilters(
2915
+ query,
2916
+ resolveEffectiveCategoryHandles(query),
2917
+ true
2918
+ ),
2919
+ facetSupport
2920
+ }
2921
+ };
2922
+ }
2923
+ /** Resolve category handles to Medusa category IDs via directed lookups. */
2924
+ async resolveCategoryIds(handles) {
2925
+ const ids = [];
2926
+ for (const handle of handles) {
2927
+ const category = await this.getCategory(handle);
2928
+ if (category) ids.push(category.id);
2929
+ }
2930
+ return ids;
2931
+ }
2932
+ /** Resolve collection handles to Medusa collection IDs via directed lookups. */
2933
+ async resolveCollectionIds(handles) {
2934
+ const ids = [];
2935
+ for (const handle of handles) {
2936
+ const collection = await this.getCollection(handle);
2937
+ if (collection) ids.push(collection.id);
2938
+ }
2939
+ return ids;
2940
+ }
2941
+ /**
2942
+ * Truthful facet-support descriptor for a scope (E2 task 2.4).
2943
+ *
2944
+ * Basic facets (`categories`, `collections`) are always offered: they are
2945
+ * applied by the canonical basic endpoint `/store/products`, and the
2946
+ * descriptor records that endpoint with the applied query and scope as their
2947
+ * demonstration source.
2948
+ *
2949
+ * Optional advanced facets (`price`, `inStock`, and advertised `attr.<name>`)
2950
+ * are offered only when the real optional `/store/search` endpoint has been
2951
+ * demonstrated with the applied query for the current scope AND the response
2952
+ * echoes the filter as applied. Otherwise they are held with the real probe
2953
+ * status/reason — a capability boolean or returned facet metadata is never
2954
+ * accepted as proof (E2 Decisions 4–5; spec "Backend support is proven against
2955
+ * the real endpoint, not assumed").
2956
+ *
2957
+ * The descriptor carries no counts or ranges and no invented timestamp; every
2958
+ * offered facet has an actual `{ endpoint, appliedQuery, scope }` source.
2959
+ */
2960
+ async getCatalogFacetSupport(options = {}) {
2961
+ const query = options.query ?? {};
2962
+ const scope = options.scope ?? query.categoryScope;
2963
+ const key = JSON.stringify({
2964
+ scope: scope ?? null,
2965
+ categoryIds: options.categoryIds ?? null,
2966
+ collectionIds: options.collectionIds ?? null,
2967
+ categories: query.categories ?? null,
2968
+ text: query.text ?? null,
2969
+ collections: query.collections ?? null,
2970
+ priceMin: query.priceMin ?? null,
2971
+ priceMax: query.priceMax ?? null,
2972
+ inStock: query.inStock ?? null,
2973
+ attributes: query.attributes ?? null
2974
+ });
2975
+ const cached = this.facetSupportCache.get(key);
2976
+ if (cached) return cached;
2977
+ const pending = this.resolveCatalogFacetSupport(
2978
+ query,
2979
+ options.categoryIds,
2980
+ options.collectionIds,
2981
+ scope
2982
+ );
2983
+ this.facetSupportCache.set(key, pending);
2984
+ return pending;
2985
+ }
2986
+ async resolveCatalogFacetSupport(query, providedCategoryIds, providedCollectionIds, scope) {
2987
+ const advancedDemonstratedQuery = {
2988
+ ...query.text !== void 0 ? { text: query.text } : {},
2989
+ ...query.categoryScope !== void 0 ? { categoryScope: query.categoryScope } : {},
2990
+ ...query.categories !== void 0 ? { categories: query.categories } : {},
2991
+ ...query.collections !== void 0 ? { collections: query.collections } : {},
2992
+ ...query.priceMin !== void 0 ? { priceMin: query.priceMin } : {},
2993
+ ...query.priceMax !== void 0 ? { priceMax: query.priceMax } : {},
2994
+ ...query.inStock !== void 0 ? { inStock: query.inStock } : {},
2995
+ ...query.attributes !== void 0 ? { attributes: query.attributes } : {}
2996
+ };
2997
+ const basicDemonstratedQuery = buildBasicDemonstratedQuery(query);
2998
+ const scopeField = scope !== void 0 ? { scope } : {};
2999
+ const offered = ["categories", "collections"];
3000
+ const demonstrations = {
3001
+ categories: {
3002
+ endpoint: BASIC_LISTING_ENDPOINT,
3003
+ appliedQuery: basicDemonstratedQuery,
3004
+ ...scopeField
3005
+ },
3006
+ collections: {
3007
+ endpoint: BASIC_LISTING_ENDPOINT,
3008
+ appliedQuery: basicDemonstratedQuery,
3009
+ ...scopeField
3010
+ }
3011
+ };
3012
+ const held = [];
3013
+ const heldReasons = {};
3014
+ const categoryIds = providedCategoryIds ?? await this.resolveScopeCategoryIds(query);
3015
+ const collectionIds = providedCollectionIds ?? (query.collections ? await this.resolveCollectionIds(query.collections) : []);
3016
+ const candidates = ["price", "inStock"];
3017
+ for (const handle of await this.resolveFilterableAttributeHandles(
3018
+ categoryIds
3019
+ )) {
3020
+ const facet = `attr.${handle}`;
3021
+ if (!candidates.includes(facet)) candidates.push(facet);
3022
+ }
3023
+ const probe = await this.probeAdvancedSearchSupport(
3024
+ query,
3025
+ categoryIds,
3026
+ collectionIds
3027
+ );
3028
+ if (probe.available) {
3029
+ for (const facet of candidates) {
3030
+ if (probe.applied.has(facet)) {
3031
+ offered.push(facet);
3032
+ demonstrations[facet] = {
3033
+ endpoint: ADVANCED_SEARCH_ENDPOINT,
3034
+ appliedQuery: advancedDemonstratedQuery,
3035
+ ...scopeField
3036
+ };
3037
+ } else {
3038
+ held.push(facet);
3039
+ heldReasons[facet] = `not echoed as applied by ${ADVANCED_SEARCH_ENDPOINT}`;
3040
+ }
3041
+ }
3042
+ } else {
3043
+ for (const facet of candidates) {
3044
+ held.push(facet);
3045
+ heldReasons[facet] = probe.reason;
3046
+ }
3047
+ }
3048
+ return {
3049
+ offered,
3050
+ held,
3051
+ demonstrations,
3052
+ ...Object.keys(heldReasons).length > 0 ? { heldReasons } : {}
3053
+ };
3054
+ }
3055
+ /**
3056
+ * Demonstrate the optional advanced endpoint with the applied query for the
3057
+ * current scope. A non-2xx response (including the HTTP 404/500 an absent
3058
+ * plugin returns), an unparseable body, or a response that does not echo the
3059
+ * applied filters all mean the facet is not demonstrated — never a silent
3060
+ * assumption of support. Uses a single SDK request (no retry): a support probe
3061
+ * must not amplify a broken optional endpoint into repeated requests.
3062
+ */
3063
+ async probeAdvancedSearchSupport(query, categoryIds, collectionIds) {
3064
+ const advancedQuery = this.buildAdvancedSearchQuery(
3065
+ query,
3066
+ categoryIds,
3067
+ collectionIds
3068
+ );
3069
+ advancedQuery.limit = 1;
3070
+ try {
3071
+ const data = await this.sdk.client.fetch(ADVANCED_SEARCH_ENDPOINT, {
3072
+ query: advancedQuery
3073
+ });
3074
+ const appliedRaw = data?.applied_filters ?? data?.appliedFilters;
3075
+ if (!appliedRaw || typeof appliedRaw !== "object") {
3076
+ return {
3077
+ available: false,
3078
+ applied: /* @__PURE__ */ new Set(),
3079
+ reason: `${ADVANCED_SEARCH_ENDPOINT} did not echo applied filters; support unverified`
3080
+ };
3081
+ }
3082
+ return {
3083
+ available: true,
3084
+ applied: mapAppliedFiltersToFacets(
3085
+ appliedRaw
3086
+ ),
3087
+ reason: `${ADVANCED_SEARCH_ENDPOINT} demonstrated with the applied query`
3088
+ };
3089
+ } catch (error) {
3090
+ const status = typeof error?.status === "number" ? error.status : void 0;
3091
+ this.logger.warn("Optional advanced search endpoint not demonstrated", {
3092
+ endpoint: ADVANCED_SEARCH_ENDPOINT,
3093
+ status,
3094
+ message: error?.message
3095
+ });
3096
+ return {
3097
+ available: false,
3098
+ applied: /* @__PURE__ */ new Set(),
3099
+ reason: status !== void 0 ? `${ADVANCED_SEARCH_ENDPOINT} returned ${status}; optional advanced endpoint not demonstrated` : `${ADVANCED_SEARCH_ENDPOINT} unavailable: ${extractErrorMessage(error)}`,
3100
+ ...status !== void 0 ? { status } : {}
3101
+ };
3102
+ }
3103
+ }
3104
+ /**
3105
+ * Build the advanced-endpoint query parameters from the canonical query. The
3106
+ * SDK serializes the plain object, so no manual stringification is needed.
3107
+ */
3108
+ buildAdvancedSearchQuery(query, categoryIds, collectionIds) {
3109
+ const advancedQuery = {};
3110
+ if (query.text) advancedQuery.q = query.text;
3111
+ if (categoryIds.length > 0) advancedQuery.categories = categoryIds.join(",");
3112
+ if (collectionIds.length > 0) {
3113
+ advancedQuery.collections = collectionIds.join(",");
3114
+ }
3115
+ if (query.priceMin !== void 0) advancedQuery.price_min = query.priceMin;
3116
+ if (query.priceMax !== void 0) advancedQuery.price_max = query.priceMax;
3117
+ if (query.inStock === true) advancedQuery.isAvailable = true;
3118
+ if (query.attributes) {
3119
+ const attributes = Object.entries(query.attributes).map(([id, values]) => ({
3120
+ id,
3121
+ values
3122
+ }));
3123
+ advancedQuery.attributes = JSON.stringify(attributes);
3124
+ }
3125
+ if (query.sort) {
3126
+ const mappedOrder = MEDUSA_SORT_KEY_MAP[query.sort] ?? null;
3127
+ if (mappedOrder !== null) advancedQuery.order = mappedOrder;
3128
+ }
3129
+ if (this.regionId) advancedQuery.region_id = this.regionId;
3130
+ if (this.currencyCode) {
3131
+ advancedQuery.currency_code = this.currencyCode.toLowerCase();
3132
+ }
3133
+ return advancedQuery;
3134
+ }
3135
+ /** Resolve the effective category scope of a query to backend IDs. */
3136
+ async resolveScopeCategoryIds(query) {
3137
+ const handles = resolveEffectiveCategoryHandles(query);
3138
+ if (handles === void 0 || handles.length === 0) return [];
3139
+ return this.resolveCategoryIds(handles);
3140
+ }
3141
+ /**
3142
+ * Advertised filterable attribute handles for the scope. The custom-attributes
3143
+ * endpoint is facet *metadata* (which attributes exist), not proof that a
3144
+ * filter can be applied; these handles are candidates only, and each still
3145
+ * requires the advanced endpoint to echo it as applied before it is offered.
3146
+ */
3147
+ async resolveFilterableAttributeHandles(categoryIds) {
3148
+ const handles = [];
3149
+ for (const categoryId of categoryIds) {
3150
+ try {
3151
+ const data = await this.sdk.client.fetch(
3152
+ `/store/custom-attributes/category/${categoryId}`
3153
+ );
3154
+ for (const item of data?.attributes ?? []) {
3155
+ const attr = item.attribute ?? item;
3156
+ if (attr?.is_filterable && attr?.is_active && typeof attr.handle === "string" && attr.handle.length > 0) {
3157
+ handles.push(attr.handle);
3158
+ }
3159
+ }
3160
+ } catch {
3161
+ }
3162
+ }
3163
+ return handles;
3164
+ }
3165
+ /** Build a typed `unsupported-query` outcome naming the offending fields. */
3166
+ unsupportedCatalogOutcome(fields) {
3167
+ return {
3168
+ ok: false,
3169
+ error: {
3170
+ code: "unsupported-query",
3171
+ fields,
3172
+ message: `Unsupported catalog query: ${fields.join(", ")}`,
3173
+ repair: { remove: fields }
3174
+ }
3175
+ };
3176
+ }
2504
3177
  async getProductRecommendations(productId) {
2505
3178
  this.logger.debug("getProductRecommendations", { productId });
2506
3179
  this.logger.warn("Product recommendations not yet implemented for Medusa");
@@ -2522,16 +3195,8 @@ var MedusaProvider = class {
2522
3195
  async searchProductsAdvanced(options) {
2523
3196
  this.logger.debug("searchProductsAdvanced", options);
2524
3197
  if (!this.capabilities.advancedSearch) {
2525
- this.logger.warn("Advanced search not available, falling back to getProducts()");
2526
- const products = await this.getProducts(options);
2527
- return {
2528
- products,
2529
- meta: {
2530
- limit: options?.first ?? 20,
2531
- offset: Number(options?.after ?? 0),
2532
- hybridSearchUsed: false
2533
- }
2534
- };
3198
+ this.logger.warn("Advanced search capability disabled, using faithful fallback");
3199
+ return this.fallbackAdvancedSearch(options);
2535
3200
  }
2536
3201
  try {
2537
3202
  const queryParams = {};
@@ -2631,20 +3296,39 @@ var MedusaProvider = class {
2631
3296
  });
2632
3297
  return { products, meta };
2633
3298
  } catch (error) {
2634
- this.logger.error("Advanced search failed, falling back to getProducts()", {
3299
+ this.logger.error("Advanced search failed, using faithful fallback", {
2635
3300
  error: error.message,
2636
3301
  stack: error.stack
2637
3302
  });
2638
- const products = await this.getProducts(options);
2639
- return {
2640
- products,
2641
- meta: {
2642
- limit: options?.first ?? 20,
2643
- offset: Number(options?.after ?? 0),
2644
- hybridSearchUsed: false
2645
- }
2646
- };
3303
+ return this.fallbackAdvancedSearch(options);
3304
+ }
3305
+ }
3306
+ /**
3307
+ * Faithful fallback for `searchProductsAdvanced`. A basic-only request is
3308
+ * served by `getProducts`; a request that depends on the optional advanced
3309
+ * endpoint is rejected as a typed `ProviderAPIError` naming the unappliable
3310
+ * fields, so a failed advanced search never returns an unfiltered result
3311
+ * labelled success (E2 task 2.4).
3312
+ */
3313
+ async fallbackAdvancedSearch(options) {
3314
+ const requested = collectAdvancedSearchRequestFields(options);
3315
+ if (requested.length > 0) {
3316
+ throw new ProviderAPIError(
3317
+ `Advanced search unavailable for requested fields: ${requested.join(", ")}`,
3318
+ "medusa",
3319
+ void 0,
3320
+ { fields: requested, repair: { remove: requested } }
3321
+ );
2647
3322
  }
3323
+ const products = await this.getProducts(options);
3324
+ return {
3325
+ products,
3326
+ meta: {
3327
+ limit: options?.first ?? 20,
3328
+ offset: Number(options?.after ?? 0),
3329
+ hybridSearchUsed: false
3330
+ }
3331
+ };
2648
3332
  }
2649
3333
  /**
2650
3334
  * Get available filter options for product filtering
@@ -2699,23 +3383,20 @@ var MedusaProvider = class {
2699
3383
  });
2700
3384
  }
2701
3385
  }
2702
- if (options.useAdvancedSearch && this.capabilities.advancedSearch) {
2703
- filters.push({
2704
- id: "price",
2705
- label: "Price",
2706
- type: "range",
2707
- min: 0,
2708
- max: 1e4,
2709
- isActive: false,
2710
- ranking: 0
2711
- });
2712
- filters.push({
2713
- id: "isAvailable",
2714
- label: "In Stock",
2715
- type: "boolean",
2716
- isActive: false,
2717
- ranking: 5
3386
+ if (options.useAdvancedSearch) {
3387
+ const support = await this.getCatalogFacetSupport({
3388
+ categoryIds: options.categoryId ? [options.categoryId] : [],
3389
+ scope: options.categoryId
2718
3390
  });
3391
+ if (support.offered.includes("inStock")) {
3392
+ filters.push({
3393
+ id: "isAvailable",
3394
+ label: "In Stock",
3395
+ type: "boolean",
3396
+ isActive: false,
3397
+ ranking: 5
3398
+ });
3399
+ }
2719
3400
  }
2720
3401
  if (options.categoryId && this.capabilities.customAttributes) {
2721
3402
  try {
@@ -2750,19 +3431,27 @@ var MedusaProvider = class {
2750
3431
  // Custom attributes lower priority
2751
3432
  };
2752
3433
  if ((filterType === "single" || filterType === "multi") && attr.values) {
2753
- filter.options = attr.values.map((val) => ({
2754
- value: this.toSlug(val.value),
2755
- // URL slug: ?ram=8gb
2756
- label: val.value,
2757
- // Display: "8GB"
2758
- id: val.id,
2759
- // Backend ID: 01KN03N67V13K5S2E6B6695KRN
2760
- count: options.includeFacetCounts ? val.count : void 0
2761
- }));
3434
+ filter.options = attr.values.map((val) => {
3435
+ const count = options.includeFacetCounts ? readFiniteFacetNumber(val.count) : void 0;
3436
+ return {
3437
+ value: this.toSlug(val.value),
3438
+ // URL slug: ?ram=8gb
3439
+ label: val.value,
3440
+ // Display: "8GB"
3441
+ id: val.id,
3442
+ // Backend ID: 01KN03N67V13K5S2E6B6695KRN
3443
+ ...count !== void 0 ? { count } : {}
3444
+ };
3445
+ });
2762
3446
  }
2763
- if (filterType === "range" && attr.metadata) {
2764
- filter.min = attr.metadata.min || 0;
2765
- filter.max = attr.metadata.max || 100;
3447
+ if (filterType === "range") {
3448
+ const min = readFiniteFacetNumber(attr.metadata?.min);
3449
+ const max = readFiniteFacetNumber(attr.metadata?.max);
3450
+ if (min === void 0 || max === void 0) {
3451
+ return;
3452
+ }
3453
+ filter.min = min;
3454
+ filter.max = max;
2766
3455
  }
2767
3456
  filters.push(filter);
2768
3457
  });
@@ -3124,7 +3813,7 @@ var MedusaProvider = class {
3124
3813
  return customFetcher(options);
3125
3814
  }
3126
3815
  try {
3127
- const limit = options.query ? 1e3 : options.limit ?? 100;
3816
+ const limit = options.limit ?? this.catalogPageSize;
3128
3817
  const queryParams = {
3129
3818
  limit,
3130
3819
  include_descendants_tree: options.includeDescendants ?? false
@@ -3164,11 +3853,15 @@ var MedusaProvider = class {
3164
3853
  }
3165
3854
  const categoryQueryParams = {
3166
3855
  category_id: [category.id],
3167
- order: options.sortKey,
3168
- limit: options.first ?? 100,
3856
+ // Bounded, paginated listing (change B, decision 5).
3857
+ limit: options.first ?? this.catalogPageSize,
3169
3858
  offset: options.after ? parseInt(options.after, 10) : void 0,
3170
- fields: "+variants,+variants.prices,+variants.calculated_prices,+variants.inventory_quantity,+options,+images,+tags"
3859
+ fields: "+variants,+variants.prices,+variants.calculated_price,+variants.inventory_quantity,+options,+images,+tags"
3171
3860
  };
3861
+ const mappedOrder = options.sortKey ? MEDUSA_SORT_KEY_MAP[options.sortKey] ?? null : null;
3862
+ if (mappedOrder !== null) {
3863
+ categoryQueryParams.order = mappedOrder;
3864
+ }
3172
3865
  if (this.regionId) {
3173
3866
  categoryQueryParams.region_id = this.regionId;
3174
3867
  }
@@ -3203,7 +3896,7 @@ var MedusaProvider = class {
3203
3896
  } else {
3204
3897
  createPayload.currency_code = this.currencyCode.toLowerCase();
3205
3898
  }
3206
- const salesChannelId = options?.salesChannelId ?? process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
3899
+ const salesChannelId = options?.salesChannelId ?? resolveConfiguredSalesChannelId();
3207
3900
  if (salesChannelId) {
3208
3901
  createPayload.sales_channel_id = salesChannelId;
3209
3902
  }
@@ -3274,7 +3967,7 @@ var MedusaProvider = class {
3274
3967
  fields: "+variants,+variants.calculated_price,+variants.prices,+variants.money_amounts",
3275
3968
  region_id: backendCart.region_id
3276
3969
  };
3277
- const salesChannelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
3970
+ const salesChannelId = resolveConfiguredSalesChannelId();
3278
3971
  if (salesChannelId) {
3279
3972
  variantCheckParams.sales_channel_id = [salesChannelId];
3280
3973
  }
@@ -4390,31 +5083,84 @@ var MedusaProvider = class {
4390
5083
  * @throws {ProviderAPIError} If checkout fails
4391
5084
  */
4392
5085
  async completeCheckout(cartId) {
4393
- this.logger.debug("completeCheckout", { cartId });
4394
- try {
4395
- return await this.withRetry(
4396
- async () => {
4397
- const { order } = await this.sdk.store.cart.complete(cartId);
4398
- if (!order) {
4399
- throw new ProviderAPIError("Checkout completed but no order returned", "medusa", 500);
4400
- }
4401
- this.logger.info("Checkout completed successfully", {
4402
- orderId: order.id,
4403
- displayId: order.display_id
4404
- });
4405
- return transformOrder(order);
4406
- },
4407
- "completeCheckout",
4408
- { cartId }
4409
- );
4410
- } catch (error) {
4411
- this.logger.error("Failed to complete checkout", { cartId, error: error.message });
5086
+ const result = await this.completeCart(cartId);
5087
+ if (result.type === "order") {
5088
+ return result.order;
5089
+ }
5090
+ if (result.type === "cart") {
4412
5091
  throw new ProviderAPIError(
4413
- `Failed to complete checkout: ${error.message}`,
5092
+ `Failed to complete checkout: ${result.error.message}`,
4414
5093
  "medusa",
4415
- error.status
5094
+ result.error.status
4416
5095
  );
4417
5096
  }
5097
+ throw new ProviderAPIError(
5098
+ `Checkout outcome is unresolved: ${result.error.message}`,
5099
+ "medusa",
5100
+ result.error.status
5101
+ );
5102
+ }
5103
+ /**
5104
+ * Complete a cart exactly once and preserve the native discriminator
5105
+ * (change B, decision 11).
5106
+ *
5107
+ * Medusa's `POST /store/carts/:id/complete` returns HTTP 200 whose body is
5108
+ * either `{ type: 'order', order }` or `{ type: 'cart', cart, error }`. This
5109
+ * method never retries: a thrown transport/timeout/5xx failure becomes an
5110
+ * `unresolved` result so the caller reconciles the committed cart before any
5111
+ * further attempt. The Medusa JS SDK performs no implicit retry of its own,
5112
+ * so there is no blind retry beneath this call either.
5113
+ */
5114
+ async completeCart(cartId) {
5115
+ this.logger.debug("completeCart", { cartId });
5116
+ try {
5117
+ const response = await this.sdk.store.cart.complete(cartId, {
5118
+ fields: MEDUSA_ORDER_COMPLETION_FIELDS
5119
+ });
5120
+ if (response?.type === "cart" || response?.cart && response?.error) {
5121
+ const error = this.toCompletionError(response.error);
5122
+ this.logger.warn("Cart completion returned a cart with an error", {
5123
+ cartId,
5124
+ code: error.code,
5125
+ message: error.message
5126
+ });
5127
+ return { type: "cart", cart: transformCart(response.cart), error };
5128
+ }
5129
+ const order = response?.order;
5130
+ if (!order) {
5131
+ return {
5132
+ type: "unresolved",
5133
+ error: { message: "Checkout returned neither an order nor a cart error" }
5134
+ };
5135
+ }
5136
+ this.logger.info("Checkout completed successfully", {
5137
+ orderId: order.id,
5138
+ displayId: order.display_id
5139
+ });
5140
+ return { type: "order", order: transformOrder(order) };
5141
+ } catch (error) {
5142
+ const completionError = this.toCompletionError(error);
5143
+ this.logger.error("Cart completion failed (unresolved, not retried)", {
5144
+ cartId,
5145
+ status: completionError.status,
5146
+ message: completionError.message
5147
+ });
5148
+ return { type: "unresolved", error: completionError };
5149
+ }
5150
+ }
5151
+ /** Normalize any thrown/returned completion failure into a structured error. */
5152
+ toCompletionError(error) {
5153
+ if (error && typeof error === "object") {
5154
+ const err = error;
5155
+ const message = typeof err.message === "string" && err.message.length > 0 ? err.message : extractErrorMessage(error);
5156
+ return {
5157
+ message,
5158
+ code: typeof err.code === "string" ? err.code : void 0,
5159
+ status: err.status ?? err.statusCode,
5160
+ raw: err
5161
+ };
5162
+ }
5163
+ return { message: extractErrorMessage(error), raw: error };
4418
5164
  }
4419
5165
  /**
4420
5166
  * Get available shipping methods for cart
@@ -4426,130 +5172,66 @@ var MedusaProvider = class {
4426
5172
  * @throws {ProviderAPIError} If cart not found or region not configured
4427
5173
  */
4428
5174
  async getShippingMethods(cartId) {
4429
- this.logger.info("\u{1F69A} [DEBUG] getShippingMethods called", { cartId });
5175
+ this.logger.debug("getShippingMethods", { cartId });
4430
5176
  try {
4431
5177
  return await this.withRetry(
4432
5178
  async () => {
4433
- this.logger.info("\u{1F69A} [DEBUG] Step 1: Retrieving cart from Medusa...", { cartId });
4434
5179
  const cartResponse = await this.sdk.store.cart.retrieve(cartId);
4435
5180
  const { cart } = cartResponse;
4436
- this.logger.info("\u{1F69A} [DEBUG] Step 2: Cart retrieved from backend", {
4437
- cartId,
4438
- hasCart: !!cart,
4439
- cartKeys: cart ? Object.keys(cart) : [],
4440
- regionId: cart?.region_id,
4441
- currencyCode: cart?.currency_code,
4442
- shippingAddress: cart?.shipping_address ? {
4443
- country: cart.shipping_address.country_code,
4444
- postal: cart.shipping_address.postal_code
4445
- } : null
4446
- });
4447
5181
  if (!cart) {
4448
- this.logger.error("\u{1F69A} [DEBUG] ERROR: Cart not found in response", {
4449
- cartId,
4450
- cartResponse
4451
- });
4452
5182
  throw new ResourceNotFoundError("Cart", cartId);
4453
5183
  }
4454
5184
  if (!cart.region_id) {
4455
- this.logger.error("\u{1F69A} [DEBUG] ERROR: Cart has no region_id", {
4456
- cartId,
4457
- cart: {
4458
- id: cart.id,
4459
- region_id: cart.region_id,
4460
- region: cart.region,
4461
- allKeys: Object.keys(cart)
4462
- }
4463
- });
4464
5185
  throw new ProviderAPIError("Cart has no region assigned", "medusa", 400);
4465
5186
  }
4466
- this.logger.info("\u{1F69A} [DEBUG] Step 3: Fetching shipping options from Medusa...", {
4467
- cartId,
4468
- regionId: cart.region_id,
4469
- endpoint: `${this.baseUrl}store/shipping-options?cart_id=${cartId}`,
4470
- note: "About to fetch shipping options (SDK when available, otherwise HTTP)"
4471
- });
4472
- let shippingResponse;
4473
- let shipping_options;
4474
- try {
4475
- const storeAny = this.sdk.store;
4476
- if (storeAny?.shippingOption?.list) {
4477
- shippingResponse = await storeAny.shippingOption.list({
4478
- cart_id: cartId
4479
- });
4480
- } else {
4481
- const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
4482
- const url = new URL("store/shipping-options", baseUrl);
4483
- url.searchParams.set("cart_id", cartId);
4484
- const response = await fetchWithRetry(
4485
- url.toString(),
4486
- {
4487
- method: "GET",
4488
- headers: {
4489
- "x-publishable-api-key": this.publishableKey,
4490
- "content-type": "application/json"
4491
- }
4492
- },
4493
- this.retryConfig
4494
- );
4495
- shippingResponse = await response.json();
4496
- }
4497
- shipping_options = shippingResponse.shipping_options;
4498
- } catch (sdkError) {
4499
- this.logger.error(
4500
- "\u{1F69A} [DEBUG] ERROR: Medusa SDK threw error when fetching shipping options",
5187
+ let shippingOptions = [];
5188
+ const storeAny = this.sdk.store;
5189
+ if (storeAny?.fulfillment?.listCartOptions) {
5190
+ const response = await storeAny.fulfillment.listCartOptions({
5191
+ cart_id: cartId
5192
+ });
5193
+ shippingOptions = response?.shipping_options ?? [];
5194
+ } else {
5195
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
5196
+ const url = new URL("store/shipping-options", baseUrl);
5197
+ url.searchParams.set("cart_id", cartId);
5198
+ const response = await fetchWithRetry(
5199
+ url.toString(),
4501
5200
  {
4502
- cartId,
4503
- regionId: cart.region_id,
4504
- errorType: sdkError?.constructor?.name,
4505
- errorMessage: sdkError?.message,
4506
- errorStatus: sdkError?.status,
4507
- errorCode: sdkError?.code,
4508
- errorResponse: sdkError?.response ? JSON.stringify(sdkError.response, null, 2) : "N/A",
4509
- errorBody: sdkError?.body ? JSON.stringify(sdkError.body, null, 2) : "N/A",
4510
- fullError: JSON.stringify(sdkError, Object.getOwnPropertyNames(sdkError), 2)
4511
- }
5201
+ method: "GET",
5202
+ headers: {
5203
+ "x-publishable-api-key": this.publishableKey,
5204
+ "content-type": "application/json"
5205
+ }
5206
+ },
5207
+ this.retryConfig
4512
5208
  );
4513
- throw sdkError;
5209
+ shippingOptions = (await response.json())?.shipping_options ?? [];
4514
5210
  }
4515
- this.logger.info("\u{1F69A} [DEBUG] Step 4: Shipping options received from backend", {
4516
- cartId,
4517
- totalOptions: shipping_options?.length || 0,
4518
- rawResponse: JSON.stringify(shippingResponse, null, 2),
4519
- firstOption: shipping_options?.[0] ? {
4520
- id: shipping_options[0].id,
4521
- name: shipping_options[0].name,
4522
- allKeys: Object.keys(shipping_options[0]),
4523
- price_incl_tax: shipping_options[0].price_incl_tax,
4524
- amount: shipping_options[0].amount,
4525
- data: shipping_options[0].data
4526
- } : null
4527
- });
4528
- const methods = shipping_options.map((option) => ({
4529
- id: option.id,
4530
- name: option.name,
4531
- price: {
4532
- amount: option.price_incl_tax?.toString() || option.amount?.toString() || "0",
4533
- currencyCode: cart.currency_code?.toUpperCase() || this.currencyCode
4534
- },
4535
- description: option.data?.description,
4536
- estimatedDays: option.data?.estimated_days || "N/A"
4537
- }));
4538
- this.logger.info("\u{1F69A} [DEBUG] Step 5: Transformed methods for Thor types", {
4539
- cartId,
4540
- count: methods.length,
4541
- methods: JSON.stringify(methods, null, 2)
5211
+ const currencyCode = cart.currency_code?.toUpperCase() ?? "";
5212
+ return shippingOptions.map((option) => {
5213
+ const amount = option?.amount ?? option?.price_incl_tax;
5214
+ const estimatedDays = option?.data?.estimated_days != null ? String(option.data.estimated_days) : void 0;
5215
+ return {
5216
+ id: option.id,
5217
+ name: option.name,
5218
+ price: {
5219
+ amount: typeof amount === "number" && Number.isFinite(amount) ? amount.toFixed(2) : "",
5220
+ currencyCode
5221
+ },
5222
+ description: option?.data?.description,
5223
+ estimatedDays,
5224
+ fulfillmentSetType: option?.service_zone?.fulfillment_set?.type ?? null
5225
+ };
4542
5226
  });
4543
- return methods;
4544
5227
  },
4545
5228
  "getShippingMethods",
4546
5229
  { cartId }
4547
5230
  );
4548
5231
  } catch (error) {
4549
- this.logger.error("\u{1F69A} [DEBUG] ERROR: Failed to fetch shipping methods", {
5232
+ this.logger.error("Failed to fetch shipping methods", {
4550
5233
  cartId,
4551
5234
  errorMessage: error.message,
4552
- errorStack: error.stack,
4553
5235
  errorStatus: error.status
4554
5236
  });
4555
5237
  throw new ProviderAPIError(
@@ -4910,19 +5592,12 @@ var MedusaProvider = class {
4910
5592
  async addShippingMethod(cartId, methodId) {
4911
5593
  this.logger.debug("addShippingMethod", { cartId, methodId });
4912
5594
  try {
4913
- return await this.withRetry(
4914
- async () => {
4915
- const { cart } = await this.sdk.store.cart.addShippingMethod(cartId, {
4916
- option_id: methodId
4917
- });
4918
- this.logger.info("Shipping method added successfully", { cartId, methodId });
4919
- void cart;
4920
- const updatedCart = await this.retrieveCartWithLineDetails(cartId);
4921
- return transformCart(updatedCart);
4922
- },
4923
- "addShippingMethod",
4924
- { cartId, methodId }
4925
- );
5595
+ await this.sdk.store.cart.addShippingMethod(cartId, {
5596
+ option_id: methodId
5597
+ });
5598
+ this.logger.info("Shipping method added successfully", { cartId, methodId });
5599
+ const updatedCart = await this.retrieveCartWithLineDetails(cartId);
5600
+ return transformCart(updatedCart);
4926
5601
  } catch (error) {
4927
5602
  this.logger.error("Failed to add shipping method", {
4928
5603
  cartId,
@@ -4946,166 +5621,75 @@ var MedusaProvider = class {
4946
5621
  * @throws {ProviderAPIError} If provider invalid or cart not ready
4947
5622
  */
4948
5623
  async initiatePaymentSession(cartId, providerId) {
4949
- this.logger.info("\u{1F4B3} [DEBUG] initiatePaymentSession called", { cartId, providerId });
5624
+ this.logger.debug("initiatePaymentSession", { cartId, providerId });
4950
5625
  try {
4951
- await this.withRetry(
4952
- async () => {
4953
- this.logger.info(
4954
- "\u{1F4B3} [DEBUG] Step 1: Retrieving RAW cart from Medusa for payment session...",
4955
- { cartId }
4956
- );
4957
- const { cart: rawCart } = await this.sdk.store.cart.retrieve(cartId, {
4958
- fields: "+shipping_methods,+shipping_address,+email,+region_id,+total,+items,+payment_collection,+payment_collection.payment_sessions,+billing_address"
4959
- });
4960
- this.logger.info("\u{1F4B3} [DEBUG] Step 2: RAW cart retrieved, checking state...", {
4961
- cartId: rawCart.id,
4962
- hasShippingMethod: !!rawCart.shipping_methods?.length,
4963
- shippingMethodIds: rawCart.shipping_methods?.map((sm) => sm.id) || [],
4964
- hasShippingAddress: !!rawCart.shipping_address,
4965
- shippingAddressCity: rawCart.shipping_address?.city || "N/A",
4966
- hasEmail: !!rawCart.email,
4967
- email: rawCart.email || "N/A",
4968
- hasBillingAddress: !!rawCart.billing_address,
4969
- hasPaymentCollection: !!rawCart.payment_collection,
4970
- existingSessionsCount: rawCart.payment_collection?.payment_sessions?.length || 0,
4971
- regionId: rawCart.region_id,
4972
- total: rawCart.total,
4973
- itemCount: rawCart.items?.length || 0
4974
- });
4975
- if (!rawCart.items || rawCart.items.length === 0) {
4976
- this.logger.error("\u{1F4B3} [DEBUG] ERROR: Cannot create payment session for empty cart", {
4977
- cartId,
4978
- itemCount: 0,
4979
- reason: "Cart has no items"
4980
- });
4981
- throw new ProviderAPIError(
4982
- "Cannot create payment session: cart is empty. Please add items to your cart before checking out.",
4983
- "medusa",
4984
- 400
4985
- );
4986
- }
4987
- if (!rawCart.shipping_address) {
4988
- this.logger.error("\u{1F4B3} [DEBUG] ERROR: Shipping address required", { cartId });
4989
- throw new ProviderAPIError(
4990
- "Shipping address is required before initiating payment session",
4991
- "medusa",
4992
- 400
4993
- );
4994
- }
4995
- if (!rawCart.shipping_methods?.length) {
4996
- this.logger.error("\u{1F4B3} [DEBUG] ERROR: Shipping method required", { cartId });
4997
- throw new ProviderAPIError(
4998
- "Shipping method is required before initiating payment session",
4999
- "medusa",
5000
- 400
5001
- );
5002
- }
5003
- if (!rawCart.email) {
5004
- this.logger.error("\u{1F4B3} [DEBUG] ERROR: Email required", { cartId });
5005
- throw new ProviderAPIError(
5006
- "Email is required before initiating payment session",
5007
- "medusa",
5008
- 400
5009
- );
5010
- }
5011
- if (!rawCart.billing_address) {
5012
- this.logger.warn(
5013
- "\u{1F4B3} [DEBUG] WARNING: Billing address not set, payment provider may require it",
5014
- {
5015
- cartId: rawCart.id,
5016
- shippingAddress: rawCart.shipping_address
5017
- }
5018
- );
5019
- }
5020
- const existingSessions = rawCart.payment_collection?.payment_sessions || [];
5021
- if (existingSessions.length > 0) {
5022
- this.logger.info("\u{1F4B3} [DEBUG] Payment session already exists, skipping creation", {
5023
- cartId: rawCart.id,
5024
- sessionCount: existingSessions.length,
5025
- sessionIds: existingSessions.map((s) => s.id)
5026
- });
5027
- return;
5028
- }
5029
- this.logger.info("\u{1F4B3} [DEBUG] Step 2.5: No existing payment sessions, will create new one", {
5030
- cartId: rawCart.id,
5031
- hasPaymentCollection: !!rawCart.payment_collection,
5032
- providerId
5033
- });
5034
- this.logger.info("\u{1F4B3} [DEBUG] Step 3: Calling Medusa SDK initiatePaymentSession...", {
5035
- cartId,
5036
- providerId,
5037
- endpoint: `${this.baseUrl}store/payment-collections`,
5038
- note: "About to call SDK method"
5039
- });
5040
- this.logger.info("\u{1F4B3} [DEBUG] Step 2.5: Validating provider ID...", {
5041
- providerId
5042
- });
5043
- try {
5044
- validatePaymentId(providerId);
5045
- this.logger.info("\u{1F4B3} [DEBUG] Provider ID validation passed", { providerId });
5046
- } catch (validationError) {
5047
- this.logger.error("\u{1F4B3} [DEBUG] ERROR: Invalid provider ID detected", {
5048
- providerId,
5049
- error: validationError.message,
5050
- hint: "Use getBackendProviderId(paymentMethod) to extract correct ID"
5051
- });
5052
- throw new ProviderAPIError(
5053
- validationError.message,
5054
- "medusa",
5055
- 400
5056
- );
5057
- }
5058
- let paymentResponse;
5059
- try {
5060
- paymentResponse = await this.sdk.store.payment.initiatePaymentSession(rawCart, {
5061
- provider_id: providerId
5062
- });
5063
- this.logger.info("\u{1F4B3} [DEBUG] Step 4: Payment session response received", {
5064
- cartId,
5065
- providerId,
5066
- responseKeys: paymentResponse ? Object.keys(paymentResponse) : [],
5067
- response: JSON.stringify(paymentResponse, null, 2)
5068
- });
5069
- } catch (sdkError) {
5070
- this.logger.error(
5071
- "\u{1F4B3} [DEBUG] ERROR: Medusa SDK threw error when initiating payment session",
5072
- {
5073
- cartId,
5074
- providerId,
5075
- errorType: sdkError?.constructor?.name,
5076
- errorMessage: sdkError?.message,
5077
- errorStatus: sdkError?.status,
5078
- errorCode: sdkError?.code,
5079
- errorResponse: sdkError?.response ? JSON.stringify(sdkError.response, null, 2) : "N/A",
5080
- errorBody: sdkError?.body ? JSON.stringify(sdkError.body, null, 2) : "N/A",
5081
- fullError: JSON.stringify(sdkError, Object.getOwnPropertyNames(sdkError), 2),
5082
- // Include cart state in error for debugging
5083
- cartState: {
5084
- hasShippingMethod: !!rawCart.shipping_methods?.length,
5085
- hasShippingAddress: !!rawCart.shipping_address,
5086
- hasBillingAddress: !!rawCart.billing_address,
5087
- hasEmail: !!rawCart.email,
5088
- total: rawCart.total,
5089
- itemCount: rawCart.items?.length || 0
5090
- }
5091
- }
5092
- );
5093
- throw sdkError;
5094
- }
5095
- this.logger.info("\u{1F4B3} [DEBUG] Payment session initiated successfully", {
5096
- cartId,
5097
- providerId
5098
- });
5099
- },
5100
- "initiatePaymentSession",
5101
- { cartId, providerId }
5102
- );
5626
+ const { cart: rawCart } = await this.sdk.store.cart.retrieve(cartId, {
5627
+ fields: "+shipping_methods,+shipping_address,+email,+region_id,+total,+items,+payment_collection,+payment_collection.payment_sessions,+billing_address"
5628
+ });
5629
+ if (!rawCart.items || rawCart.items.length === 0) {
5630
+ throw new ProviderAPIError(
5631
+ "Cannot create payment session: cart is empty. Please add items to your cart before checking out.",
5632
+ "medusa",
5633
+ 400
5634
+ );
5635
+ }
5636
+ if (!rawCart.shipping_address) {
5637
+ throw new ProviderAPIError(
5638
+ "Shipping address is required before initiating payment session",
5639
+ "medusa",
5640
+ 400
5641
+ );
5642
+ }
5643
+ if (!rawCart.shipping_methods?.length) {
5644
+ throw new ProviderAPIError(
5645
+ "Shipping method is required before initiating payment session",
5646
+ "medusa",
5647
+ 400
5648
+ );
5649
+ }
5650
+ if (!rawCart.email) {
5651
+ throw new ProviderAPIError(
5652
+ "Email is required before initiating payment session",
5653
+ "medusa",
5654
+ 400
5655
+ );
5656
+ }
5657
+ validatePaymentId(providerId);
5658
+ const existingSessions = rawCart.payment_collection?.payment_sessions ?? [];
5659
+ const compatible = existingSessions.find((session) => {
5660
+ const statusOk = ["pending", "requires_more", "authorized"].includes(session?.status);
5661
+ return session?.provider_id === providerId && session?.amount === rawCart.total && statusOk;
5662
+ });
5663
+ if (compatible) {
5664
+ this.logger.info("Reusing compatible payment session", {
5665
+ cartId,
5666
+ providerId,
5667
+ sessionId: compatible.id,
5668
+ amount: compatible.amount
5669
+ });
5670
+ return;
5671
+ }
5672
+ if (existingSessions.length > 0) {
5673
+ this.logger.info("Existing payment session incompatible; replacing it", {
5674
+ cartId,
5675
+ providerId,
5676
+ existing: existingSessions.map((session) => ({
5677
+ id: session.id,
5678
+ providerId: session.provider_id,
5679
+ amount: session.amount,
5680
+ status: session.status
5681
+ }))
5682
+ });
5683
+ }
5684
+ await this.sdk.store.payment.initiatePaymentSession(rawCart, {
5685
+ provider_id: providerId
5686
+ });
5687
+ this.logger.info("Payment session initiated", { cartId, providerId });
5103
5688
  } catch (error) {
5104
- this.logger.error("\u{1F4B3} [DEBUG] ERROR: Failed to initiate payment session", {
5689
+ this.logger.error("Failed to initiate payment session", {
5105
5690
  cartId,
5106
5691
  providerId,
5107
5692
  errorMessage: error.message,
5108
- errorStack: error.stack,
5109
5693
  errorStatus: error.status
5110
5694
  });
5111
5695
  throw new ProviderAPIError(
@@ -5128,31 +5712,23 @@ var MedusaProvider = class {
5128
5712
  async applyDiscountCode(cartId, code) {
5129
5713
  this.logger.debug("applyDiscountCode", { cartId, code });
5130
5714
  try {
5131
- return await this.withRetry(
5132
- async () => {
5133
- const response = await fetch(`${this.baseUrl}/store/carts/${cartId}/promotions`, {
5134
- method: "POST",
5135
- headers: {
5136
- "Content-Type": "application/json",
5137
- "x-publishable-api-key": this.publishableKey || ""
5138
- },
5139
- body: JSON.stringify({
5140
- promo_codes: [code]
5141
- })
5142
- });
5143
- if (!response.ok) {
5144
- const error = await response.json();
5145
- throw new Error(error.message || "Failed to apply discount code");
5146
- }
5147
- const { cart: medusaCart } = await response.json();
5148
- this.logger.info("Discount code applied successfully", { cartId, code });
5149
- void medusaCart;
5150
- const updatedCart = await this.retrieveCartWithLineDetails(cartId);
5151
- return transformCart(updatedCart);
5715
+ const response = await fetch(`${this.baseUrl}/store/carts/${cartId}/promotions`, {
5716
+ method: "POST",
5717
+ headers: {
5718
+ "Content-Type": "application/json",
5719
+ "x-publishable-api-key": this.publishableKey || ""
5152
5720
  },
5153
- "applyDiscountCode",
5154
- { cartId, code }
5155
- );
5721
+ body: JSON.stringify({
5722
+ promo_codes: [code]
5723
+ })
5724
+ });
5725
+ if (!response.ok) {
5726
+ const error = await response.json();
5727
+ throw new Error(error.message || "Failed to apply discount code");
5728
+ }
5729
+ this.logger.info("Discount code applied successfully", { cartId, code });
5730
+ const updatedCart = await this.retrieveCartWithLineDetails(cartId);
5731
+ return transformCart(updatedCart);
5156
5732
  } catch (error) {
5157
5733
  this.logger.error("Failed to apply discount code", { cartId, code, error: error.message });
5158
5734
  throw new ProviderAPIError(
@@ -5175,31 +5751,23 @@ var MedusaProvider = class {
5175
5751
  async removeDiscountCode(cartId, code) {
5176
5752
  this.logger.debug("removeDiscountCode", { cartId, code });
5177
5753
  try {
5178
- return await this.withRetry(
5179
- async () => {
5180
- const response = await fetch(`${this.baseUrl}/store/carts/${cartId}/promotions`, {
5181
- method: "DELETE",
5182
- headers: {
5183
- "Content-Type": "application/json",
5184
- "x-publishable-api-key": this.publishableKey || ""
5185
- },
5186
- body: JSON.stringify({
5187
- promo_codes: [code]
5188
- })
5189
- });
5190
- if (!response.ok) {
5191
- const error = await response.json();
5192
- throw new Error(error.message || "Failed to remove discount code");
5193
- }
5194
- const { cart: medusaCart } = await response.json();
5195
- this.logger.info("Discount code removed successfully", { cartId, code });
5196
- void medusaCart;
5197
- const updatedCart = await this.retrieveCartWithLineDetails(cartId);
5198
- return transformCart(updatedCart);
5754
+ const response = await fetch(`${this.baseUrl}/store/carts/${cartId}/promotions`, {
5755
+ method: "DELETE",
5756
+ headers: {
5757
+ "Content-Type": "application/json",
5758
+ "x-publishable-api-key": this.publishableKey || ""
5199
5759
  },
5200
- "removeDiscountCode",
5201
- { cartId, code }
5202
- );
5760
+ body: JSON.stringify({
5761
+ promo_codes: [code]
5762
+ })
5763
+ });
5764
+ if (!response.ok) {
5765
+ const error = await response.json();
5766
+ throw new Error(error.message || "Failed to remove discount code");
5767
+ }
5768
+ this.logger.info("Discount code removed successfully", { cartId, code });
5769
+ const updatedCart = await this.retrieveCartWithLineDetails(cartId);
5770
+ return transformCart(updatedCart);
5203
5771
  } catch (error) {
5204
5772
  this.logger.error("Failed to remove discount code", { cartId, code, error: error.message });
5205
5773
  throw new ProviderAPIError(
@@ -9194,6 +9762,7 @@ var StelOrderProvider = class {
9194
9762
  // src/providers/mock/mock.ts
9195
9763
  var FIXTURE_TIMESTAMP = "2026-01-01T00:00:00.000Z";
9196
9764
  var DEFAULT_SITE_URL = "https://mock.thorcommerce.local";
9765
+ var MOCK_SALES_CHANNEL_ID = "mock-sales-channel";
9197
9766
  var regionFixtures = [
9198
9767
  {
9199
9768
  id: "mock-region-us",
@@ -9266,6 +9835,7 @@ var productFixtures = [
9266
9835
  {
9267
9836
  id: "mock-product-aurora-jacket",
9268
9837
  handle: "aurora-jacket",
9838
+ createdAt: "2026-01-05T00:00:00.000Z",
9269
9839
  title: "Aurora Quilted Jacket",
9270
9840
  description: "Lightweight quilted outerwear with tonal hardware and a soft structured collar.",
9271
9841
  imageLabel: "Aurora Jacket",
@@ -9307,6 +9877,7 @@ var productFixtures = [
9307
9877
  {
9308
9878
  id: "mock-product-solis-dress",
9309
9879
  handle: "solis-dress",
9880
+ createdAt: "2026-01-03T00:00:00.000Z",
9310
9881
  title: "Solis Pleated Dress",
9311
9882
  description: "A fluid pleated dress with a clean neckline designed for hero storytelling.",
9312
9883
  imageLabel: "Solis Dress",
@@ -9346,6 +9917,7 @@ var productFixtures = [
9346
9917
  {
9347
9918
  id: "mock-product-helios-lamp",
9348
9919
  handle: "helios-lamp",
9920
+ createdAt: "2026-01-04T00:00:00.000Z",
9349
9921
  title: "Helios Table Lamp",
9350
9922
  description: "Warm ambient lamp with sculptural proportions for lifestyle and home grids.",
9351
9923
  imageLabel: "Helios Lamp",
@@ -9369,6 +9941,7 @@ var productFixtures = [
9369
9941
  {
9370
9942
  id: "mock-product-luna-bag",
9371
9943
  handle: "luna-bag",
9944
+ createdAt: "2026-01-02T00:00:00.000Z",
9372
9945
  title: "Luna Crossbody Bag",
9373
9946
  description: "Compact everyday bag for accessory rails, checkout add-ons, and bundle modules.",
9374
9947
  imageLabel: "Luna Bag",
@@ -9509,7 +10082,7 @@ function buildProducts(currencyCode) {
9509
10082
  productType: product.productType,
9510
10083
  vendor: product.vendor,
9511
10084
  tags: product.tags,
9512
- createdAt: FIXTURE_TIMESTAMP,
10085
+ createdAt: product.createdAt,
9513
10086
  updatedAt: FIXTURE_TIMESTAMP,
9514
10087
  metadata: {
9515
10088
  categoryHandle: product.categoryHandle,
@@ -9578,6 +10151,92 @@ function paginateProducts(products, skip, first) {
9578
10151
  const end = first ? start + first : void 0;
9579
10152
  return products.slice(start, end);
9580
10153
  }
10154
+ var MOCK_CATALOG_SORT_KEYS = [
10155
+ "featured",
10156
+ "name",
10157
+ "newest"
10158
+ ];
10159
+ var MOCK_CATALOG_ENDPOINT = "mock://catalog/products";
10160
+ var MOCK_CATALOG_PAGE_SIZE = 50;
10161
+ function resolveEffectiveCategoryHandles2(query) {
10162
+ const { categoryScope, categories } = query;
10163
+ if (categoryScope === void 0) return categories;
10164
+ if (categories === void 0) return [categoryScope];
10165
+ return categories.filter((handle) => handle === categoryScope);
10166
+ }
10167
+ function collectRequestedAdvancedFields2(query) {
10168
+ const fields = [];
10169
+ if (query.priceMin !== void 0) fields.push("priceMin");
10170
+ if (query.priceMax !== void 0) fields.push("priceMax");
10171
+ if (query.inStock !== void 0) fields.push("inStock");
10172
+ for (const name of Object.keys(query.attributes ?? {})) {
10173
+ fields.push(`attr.${name}`);
10174
+ }
10175
+ return fields;
10176
+ }
10177
+ function advancedFieldToFacet2(field) {
10178
+ if (field === "priceMin" || field === "priceMax") return "price";
10179
+ return field;
10180
+ }
10181
+ function buildAppliedCatalogFilters2(query, effectiveCategoryHandles) {
10182
+ return {
10183
+ ...query.text ? { text: query.text } : {},
10184
+ ...effectiveCategoryHandles !== void 0 ? { categories: effectiveCategoryHandles } : {},
10185
+ ...query.collections !== void 0 ? { collections: query.collections } : {}
10186
+ };
10187
+ }
10188
+ function buildBasicDemonstratedQuery2(query) {
10189
+ return {
10190
+ ...query.text !== void 0 ? { text: query.text } : {},
10191
+ ...query.categoryScope !== void 0 ? { categoryScope: query.categoryScope } : {},
10192
+ ...query.categories !== void 0 ? { categories: query.categories } : {},
10193
+ ...query.collections !== void 0 ? { collections: query.collections } : {}
10194
+ };
10195
+ }
10196
+ function buildMockFacetSupport(query) {
10197
+ const appliedQuery = buildBasicDemonstratedQuery2(query);
10198
+ const scopeField = query.categoryScope !== void 0 ? { scope: query.categoryScope } : {};
10199
+ return {
10200
+ offered: ["categories", "collections"],
10201
+ held: ["price", "inStock"],
10202
+ demonstrations: {
10203
+ categories: {
10204
+ endpoint: MOCK_CATALOG_ENDPOINT,
10205
+ appliedQuery,
10206
+ ...scopeField
10207
+ },
10208
+ collections: {
10209
+ endpoint: MOCK_CATALOG_ENDPOINT,
10210
+ appliedQuery,
10211
+ ...scopeField
10212
+ }
10213
+ },
10214
+ heldReasons: {
10215
+ price: `${MOCK_CATALOG_ENDPOINT} is the mock basic listing; price filtering is not implemented in mock mode`,
10216
+ inStock: `${MOCK_CATALOG_ENDPOINT} is the mock basic listing; stock filtering is not implemented in mock mode`
10217
+ }
10218
+ };
10219
+ }
10220
+ function unsupportedMockCatalogOutcome(fields) {
10221
+ return {
10222
+ ok: false,
10223
+ error: {
10224
+ code: "unsupported-query",
10225
+ fields,
10226
+ message: `Unsupported catalog query in mock mode: ${fields.join(", ")}`,
10227
+ repair: { remove: fields }
10228
+ }
10229
+ };
10230
+ }
10231
+ function sortMockCatalogProducts(products, sortKey) {
10232
+ const sorted = [...products];
10233
+ if (sortKey === "name") {
10234
+ sorted.sort((left, right) => left.title.localeCompare(right.title));
10235
+ } else if (sortKey === "newest") {
10236
+ sorted.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
10237
+ }
10238
+ return sorted;
10239
+ }
9581
10240
  function resolveVariant(currencyCode, merchandiseId) {
9582
10241
  const products = buildProducts(currencyCode);
9583
10242
  for (const product of products) {
@@ -9642,11 +10301,22 @@ function buildExternalCart(state, currencyCode, siteUrl) {
9642
10301
  checkoutUrl: `${siteUrl}/checkout?cart=${state.id}`,
9643
10302
  cost: {
9644
10303
  subtotalAmount: createMoney(subtotal, regionCurrency),
9645
- totalAmount: createMoney(total, regionCurrency),
9646
- totalTaxAmount: createMoney(totalTax, regionCurrency)
10304
+ discountAmount: createMoney(discount, regionCurrency),
10305
+ totalTaxAmount: createMoney(totalTax, regionCurrency),
10306
+ shippingAmount: createMoney(shipping, regionCurrency),
10307
+ totalAmount: createMoney(total, regionCurrency)
9647
10308
  },
9648
10309
  lines,
9649
10310
  totalQuantity: lines.reduce((sum, line) => sum + line.quantity, 0),
10311
+ salesChannelId: MOCK_SALES_CHANNEL_ID,
10312
+ regionId: state.regionId,
10313
+ currencyCode: regionCurrency.toUpperCase(),
10314
+ selectedShippingMethodId: state.shippingMethodId,
10315
+ // The mock method id IS the shipping option id (the value returned by
10316
+ // `getMockShippingMethods` and accepted by `addShippingMethod`), so the
10317
+ // canonical option identity mirrors the real adapter's `so_` field and the
10318
+ // checkout UI can highlight the selected option.
10319
+ selectedShippingOptionId: state.shippingMethodId,
9650
10320
  completedAt: state.completedAt
9651
10321
  };
9652
10322
  }
@@ -9702,6 +10372,69 @@ function getMockProducts(options) {
9702
10372
  const sorted = sortProducts(filtered, options?.sortKey, options?.reverse);
9703
10373
  return paginateProducts(sorted, options?.skip, options?.first);
9704
10374
  }
10375
+ function getMockCatalogProducts(query, options = {}) {
10376
+ if (query.sort !== void 0 && !MOCK_CATALOG_SORT_KEYS.includes(query.sort)) {
10377
+ return unsupportedMockCatalogOutcome(["sort"]);
10378
+ }
10379
+ const region = getRegionFixture(options.regionId);
10380
+ const currencyCode = options.currencyCode ?? region.currencyCode;
10381
+ const allProducts = buildProducts(currencyCode);
10382
+ const facetSupport = buildMockFacetSupport(query);
10383
+ const requestedAdvanced = collectRequestedAdvancedFields2(query);
10384
+ const heldAdvanced = requestedAdvanced.filter(
10385
+ (field) => !facetSupport.offered.includes(advancedFieldToFacet2(field))
10386
+ );
10387
+ if (heldAdvanced.length > 0) {
10388
+ return unsupportedMockCatalogOutcome(heldAdvanced);
10389
+ }
10390
+ const effectiveCategoryHandles = resolveEffectiveCategoryHandles2(query);
10391
+ const emptyIntersection = effectiveCategoryHandles !== void 0 && effectiveCategoryHandles.length === 0;
10392
+ const categoryFilterEmpty = effectiveCategoryHandles !== void 0 && !effectiveCategoryHandles.some(
10393
+ (handle) => categoryFixtures.some(
10394
+ (category) => category.handle === handle || category.id === handle
10395
+ )
10396
+ );
10397
+ const collectionFilterEmpty = query.collections !== void 0 && !query.collections.some(
10398
+ (handle) => collectionFixtures.some(
10399
+ (collection) => collection.handle === handle || collection.id === handle
10400
+ )
10401
+ );
10402
+ if (emptyIntersection || categoryFilterEmpty || collectionFilterEmpty) {
10403
+ return {
10404
+ ok: true,
10405
+ results: {
10406
+ products: [],
10407
+ appliedFilters: buildAppliedCatalogFilters2(
10408
+ query,
10409
+ effectiveCategoryHandles
10410
+ ),
10411
+ facetSupport
10412
+ }
10413
+ };
10414
+ }
10415
+ const filtered = filterProducts(allProducts, {
10416
+ query: query.text,
10417
+ categoryIds: effectiveCategoryHandles,
10418
+ collectionIds: query.collections
10419
+ });
10420
+ const sorted = sortMockCatalogProducts(filtered, query.sort);
10421
+ const pageSize = query.pageSize ?? MOCK_CATALOG_PAGE_SIZE;
10422
+ const page = query.page ?? 1;
10423
+ const offset = (page - 1) * pageSize;
10424
+ return {
10425
+ ok: true,
10426
+ results: {
10427
+ products: sorted.slice(offset, offset + pageSize),
10428
+ // The full matching fixture count, never the page length.
10429
+ total: filtered.length,
10430
+ appliedFilters: buildAppliedCatalogFilters2(
10431
+ query,
10432
+ effectiveCategoryHandles
10433
+ ),
10434
+ facetSupport
10435
+ }
10436
+ };
10437
+ }
9705
10438
  function getMockProduct(handle, regionId) {
9706
10439
  const region = getRegionFixture(regionId);
9707
10440
  return buildProducts(region.currencyCode).find(
@@ -9814,6 +10547,17 @@ function getMockPaymentMethods(currencyCode) {
9814
10547
  isEnabled: true,
9815
10548
  requiresSetup: false,
9816
10549
  description: "Mock cash payment option for checkout prototypes."
10550
+ },
10551
+ {
10552
+ // The single approved manual TEST provider used by the real Medusa
10553
+ // backend, mirrored in mock mode so the guest TEST purchase path is
10554
+ // exercisable without a backend.
10555
+ id: "pp_system_default",
10556
+ name: "Manual TEST payment",
10557
+ type: "cash_on_delivery",
10558
+ isEnabled: true,
10559
+ requiresSetup: false,
10560
+ description: "Manual system TEST provider (bookkeeping only; not external funds)."
9817
10561
  }
9818
10562
  ];
9819
10563
  }
@@ -9907,6 +10651,7 @@ var MockProvider = class {
9907
10651
  version = "1.0.0";
9908
10652
  capabilities = MOCK_CAPABILITIES;
9909
10653
  isMock = true;
10654
+ supportedCatalogSortKeys = MOCK_CATALOG_SORT_KEYS;
9910
10655
  currencyCode;
9911
10656
  siteUrl;
9912
10657
  constructor(config = {}) {
@@ -9937,6 +10682,18 @@ var MockProvider = class {
9937
10682
  regionId: options?.regionId
9938
10683
  });
9939
10684
  }
10685
+ /**
10686
+ * Canonical catalog query path (E2 task 2.5).
10687
+ *
10688
+ * Delegates to the fixture-backed canonical implementation so mock mode is a
10689
+ * faithful stand-in for the same contract the Medusa adapter honors: scope ∩
10690
+ * categories, collection resolution, global ordering before pagination, the
10691
+ * real full match count, and a typed `unsupported-query` for advanced filters
10692
+ * the mock does not implement.
10693
+ */
10694
+ async getCatalogProducts(query) {
10695
+ return getMockCatalogProducts(query, { currencyCode: this.currencyCode });
10696
+ }
9940
10697
  async getProductRecommendations(productId) {
9941
10698
  return getMockProductRecommendations(productId);
9942
10699
  }
@@ -10072,6 +10829,25 @@ var MockProvider = class {
10072
10829
  const order = completeMockCheckout(cart, this.currencyCode, this.siteUrl);
10073
10830
  return order;
10074
10831
  }
10832
+ /**
10833
+ * Discriminated completion for the fixture provider.
10834
+ *
10835
+ * Completion is idempotent by construction (the fixture order id derives from
10836
+ * the cart id), so completing an already-completed cart returns the same
10837
+ * persisted order — mirroring the native reconcile path without a second
10838
+ * order.
10839
+ */
10840
+ async completeCart(cartId) {
10841
+ const cart = getMockCartState(cartId);
10842
+ if (!cart) {
10843
+ return {
10844
+ type: "unresolved",
10845
+ error: { message: `Cart not found: ${cartId}`, code: "CART_NOT_FOUND", status: 404 }
10846
+ };
10847
+ }
10848
+ const order = completeMockCheckout(cart, this.currencyCode, this.siteUrl);
10849
+ return { type: "order", order };
10850
+ }
10075
10851
  async getShippingMethods(cartId) {
10076
10852
  const cart = getMockCartState(cartId);
10077
10853
  return getMockShippingMethods(cart?.regionId);
@@ -10196,6 +10972,113 @@ var MockProvider = class {
10196
10972
  }
10197
10973
  };
10198
10974
 
10975
+ // src/factory/provider-types.ts
10976
+ var SupportedProviderType = /* @__PURE__ */ ((SupportedProviderType2) => {
10977
+ SupportedProviderType2["Mock"] = "mock";
10978
+ SupportedProviderType2["Medusa"] = "medusa";
10979
+ SupportedProviderType2["StelOrder"] = "stelorder";
10980
+ SupportedProviderType2["Shopify"] = "shopify";
10981
+ SupportedProviderType2["BigCommerce"] = "bigcommerce";
10982
+ SupportedProviderType2["WooCommerce"] = "woocommerce";
10983
+ SupportedProviderType2["Spree"] = "spree";
10984
+ SupportedProviderType2["Magento"] = "magento";
10985
+ return SupportedProviderType2;
10986
+ })(SupportedProviderType || {});
10987
+ var PROVIDER_METADATA = {
10988
+ mock: {
10989
+ name: "Mock",
10990
+ description: "Fixture-backed runtime provider for layout/design mode without a real backend",
10991
+ requiresStorefront: false,
10992
+ requiredEnvVars: [],
10993
+ maxRetries: 0
10994
+ },
10995
+ medusa: {
10996
+ name: "Medusa JS",
10997
+ description: "Medusa v2 commerce engine",
10998
+ requiresStorefront: true,
10999
+ requiredEnvVars: [
11000
+ "NEXT_PUBLIC_COMMERCE_API_URL",
11001
+ "NEXT_PUBLIC_COMMERCE_API_KEY",
11002
+ "NEXT_PUBLIC_SALES_CHANNEL_ID"
11003
+ ],
11004
+ maxRetries: 3
11005
+ },
11006
+ stelorder: {
11007
+ name: "Stel Order",
11008
+ description: "Stel Order ERP and sales document API",
11009
+ requiresStorefront: false,
11010
+ requiredEnvVars: [
11011
+ "NEXT_PUBLIC_COMMERCE_API_URL",
11012
+ "NEXT_PUBLIC_COMMERCE_API_KEY"
11013
+ ],
11014
+ maxRetries: 2
11015
+ },
11016
+ shopify: {
11017
+ name: "Shopify",
11018
+ description: "Shopify Storefront API (GraphQL)",
11019
+ requiresStorefront: false,
11020
+ requiredEnvVars: [
11021
+ "NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN",
11022
+ "NEXT_PUBLIC_SHOPIFY_STOREFRONT_TOKEN"
11023
+ ],
11024
+ maxRetries: 3
11025
+ },
11026
+ bigcommerce: {
11027
+ name: "BigCommerce",
11028
+ description: "BigCommerce REST Storefront API",
11029
+ requiresStorefront: false,
11030
+ requiredEnvVars: [
11031
+ "BIGCOMMERCE_STORE_HASH",
11032
+ "BIGCOMMERCE_STOREFRONT_API_TOKEN",
11033
+ "BIGCOMMERCE_CHANNEL_ID"
11034
+ ],
11035
+ maxRetries: 3
11036
+ },
11037
+ woocommerce: {
11038
+ name: "WooCommerce",
11039
+ description: "WooCommerce Store API + REST API v3",
11040
+ requiresStorefront: false,
11041
+ requiredEnvVars: [
11042
+ "WOOCOMMERCE_URL",
11043
+ "WOOCOMMERCE_CONSUMER_KEY",
11044
+ "WOOCOMMERCE_CONSUMER_SECRET"
11045
+ ],
11046
+ maxRetries: 3
11047
+ },
11048
+ spree: {
11049
+ name: "Spree Commerce",
11050
+ description: "Spree API v2 Storefront (JSON:API)",
11051
+ requiresStorefront: false,
11052
+ requiredEnvVars: [
11053
+ "SPREE_API_URL"
11054
+ ],
11055
+ maxRetries: 3
11056
+ },
11057
+ magento: {
11058
+ name: "Adobe Commerce (Magento)",
11059
+ description: "Adobe Commerce GraphQL + REST API",
11060
+ requiresStorefront: true,
11061
+ requiredEnvVars: [
11062
+ "MAGENTO_URL",
11063
+ "MAGENTO_STORE_CODE"
11064
+ ],
11065
+ maxRetries: 3
11066
+ }
11067
+ };
11068
+ function isSupportedProviderType(value) {
11069
+ return typeof value === "string" && Object.values(SupportedProviderType).includes(value);
11070
+ }
11071
+ function getProviderMetadata(type) {
11072
+ if (!isSupportedProviderType(type)) {
11073
+ throw new Error(
11074
+ `Unsupported provider: ${type}. Supported: ${Object.values(
11075
+ SupportedProviderType
11076
+ ).join(", ")}`
11077
+ );
11078
+ }
11079
+ return PROVIDER_METADATA[type];
11080
+ }
11081
+
10199
11082
  // src/factory/index.ts
10200
11083
  function createProvider(options) {
10201
11084
  if (!options || !options.provider) {
@@ -10245,11 +11128,13 @@ export {
10245
11128
  MAGENTO_CAPABILITIES,
10246
11129
  MEDUSA_CAPABILITIES,
10247
11130
  MOCK_CAPABILITIES,
11131
+ MOCK_SALES_CHANNEL_ID,
10248
11132
  MagentoProvider,
10249
11133
  MedusaMetadataAdapter,
10250
11134
  MedusaProvider,
10251
11135
  MetricsService,
10252
11136
  MockProvider,
11137
+ PROVIDER_METADATA,
10253
11138
  ProviderAPIError,
10254
11139
  ProviderNotFoundError,
10255
11140
  ResourceNotFoundError,
@@ -10259,7 +11144,8 @@ export {
10259
11144
  ShopifyProvider,
10260
11145
  SpreeProvider,
10261
11146
  StelOrderProvider,
10262
- StorefrontConfigError2 as StorefrontConfigError,
11147
+ StorefrontConfigError,
11148
+ SupportedProviderType,
10263
11149
  WOOCOMMERCE_CAPABILITIES,
10264
11150
  WooCommerceProvider,
10265
11151
  assertEntityBelongsToChannel,
@@ -10278,10 +11164,12 @@ export {
10278
11164
  getBackendProviderId,
10279
11165
  getCapabilityDescription,
10280
11166
  getChannelScope,
11167
+ getProviderMetadata,
10281
11168
  isBackendCapabilities,
10282
11169
  isGlobalEntity,
10283
11170
  isProviderType,
10284
11171
  isScopedToChannel,
11172
+ isSupportedProviderType,
10285
11173
  moneyToNumber,
10286
11174
  moneyToNumberSafe,
10287
11175
  numberToMoney,