@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/.turbo/turbo-build.log +18 -0
- package/.turbo/turbo-test.log +897 -0
- package/.turbo/turbo-type-check.log +0 -0
- package/AGENTS.md +1 -1
- package/dist/.tsbuildinfo +1 -0
- package/dist/index.d.mts +348 -11
- package/dist/index.d.ts +348 -11
- package/dist/index.js +1393 -496
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1386 -498
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/publish.log +93 -0
- package/src/core/errors.ts +31 -0
- package/src/factory/index.ts +16 -6
- package/src/factory/provider-types.test.ts +32 -0
- package/src/factory/provider-types.ts +176 -0
- package/src/index.ts +6 -4
- package/src/providers/index.ts +1 -1
- package/src/providers/medusa/available-filters.test.ts +280 -0
- package/src/providers/medusa/catalog-facet-support.test.ts +715 -0
- package/src/providers/medusa/catalog-query.test.ts +422 -0
- package/src/providers/medusa/catalog-sort.test.ts +175 -0
- package/src/providers/medusa/index.test.ts +480 -0
- package/src/providers/medusa/index.ts +1490 -518
- package/src/providers/medusa/live-smoke.test.ts +68 -0
- package/src/providers/medusa/transforms.test.ts +177 -0
- package/src/providers/medusa/transforms.ts +145 -66
- package/src/providers/mock/catalog-query.test.ts +355 -0
- package/src/providers/mock/index.test.ts +47 -0
- package/src/providers/mock/index.ts +43 -1
- package/src/providers/mock/mock.ts +324 -2
- package/src/types/completion.ts +57 -0
- package/src/types/index.ts +7 -0
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
|
|
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
|
|
974
|
+
priceCurrency = variant.calculated_price.currency_code ?? currencyCode;
|
|
960
975
|
} else if (variant.prices && variant.prices.length > 0) {
|
|
961
|
-
const
|
|
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
|
|
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:
|
|
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
|
|
999
|
+
function transformProduct(product, currencyCode) {
|
|
983
1000
|
const variants = product.variants?.map((v) => transformProductVariant(v, currencyCode)) ?? [];
|
|
984
|
-
const
|
|
985
|
-
const
|
|
986
|
-
const
|
|
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
|
|
1003
|
-
// and do NOT
|
|
1004
|
-
minVariantPrice: {
|
|
1005
|
-
|
|
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
|
|
1029
|
-
const
|
|
1030
|
-
const
|
|
1031
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
1096
|
-
|
|
1097
|
-
|
|
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
|
-
|
|
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:
|
|
1191
|
-
total:
|
|
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
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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
|
-
|
|
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 ??
|
|
2460
|
-
offset: options.after ? parseInt(String(options.after), 10) :
|
|
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 =
|
|
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
|
|
2526
|
-
|
|
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,
|
|
3299
|
+
this.logger.error("Advanced search failed, using faithful fallback", {
|
|
2635
3300
|
error: error.message,
|
|
2636
3301
|
stack: error.stack
|
|
2637
3302
|
});
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
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
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
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
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
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"
|
|
2764
|
-
|
|
2765
|
-
|
|
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.
|
|
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
|
-
|
|
3168
|
-
limit: options.first ??
|
|
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.
|
|
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 ??
|
|
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 =
|
|
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.
|
|
4394
|
-
|
|
4395
|
-
return
|
|
4396
|
-
|
|
4397
|
-
|
|
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.
|
|
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
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
const
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
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
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
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
|
-
|
|
5209
|
+
shippingOptions = (await response.json())?.shipping_options ?? [];
|
|
4514
5210
|
}
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
id:
|
|
4521
|
-
name:
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
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("
|
|
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
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
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.
|
|
5624
|
+
this.logger.debug("initiatePaymentSession", { cartId, providerId });
|
|
4950
5625
|
try {
|
|
4951
|
-
await this.
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
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("
|
|
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
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
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
|
-
|
|
5154
|
-
|
|
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
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
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
|
-
|
|
5201
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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,
|