@thorprovider/adapters 5.0.10 → 5.0.12
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/AGENTS.md +1 -1
- package/dist/index.d.mts +376 -16
- package/dist/index.d.ts +376 -16
- package/dist/index.js +1687 -528
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1680 -530
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- 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/auth.test.ts +148 -25
- package/src/providers/medusa/auth.ts +129 -16
- 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/customer-orders.test.ts +142 -0
- package/src/providers/medusa/index.test.ts +480 -0
- package/src/providers/medusa/index.ts +1592 -538
- package/src/providers/medusa/live-smoke.test.ts +68 -0
- package/src/providers/medusa/transforms.test.ts +222 -0
- package/src/providers/medusa/transforms.ts +152 -66
- package/src/providers/mock/catalog-query.test.ts +491 -0
- package/src/providers/mock/index.test.ts +66 -0
- package/src/providers/mock/index.ts +43 -1
- package/src/providers/mock/mock.ts +513 -3
- package/src/types/completion.ts +57 -0
- package/src/types/index.ts +7 -0
- package/.turbo/turbo-build.log +0 -18
- package/.turbo/turbo-test.log +0 -372
- package/.turbo/turbo-type-check.log +0 -0
- package/dist/.tsbuildinfo +0 -1
- package/publish.log +0 -93
package/dist/index.js
CHANGED
|
@@ -45,11 +45,13 @@ __export(src_exports, {
|
|
|
45
45
|
MAGENTO_CAPABILITIES: () => MAGENTO_CAPABILITIES,
|
|
46
46
|
MEDUSA_CAPABILITIES: () => MEDUSA_CAPABILITIES,
|
|
47
47
|
MOCK_CAPABILITIES: () => MOCK_CAPABILITIES,
|
|
48
|
+
MOCK_SALES_CHANNEL_ID: () => MOCK_SALES_CHANNEL_ID,
|
|
48
49
|
MagentoProvider: () => MagentoProvider,
|
|
49
50
|
MedusaMetadataAdapter: () => MedusaMetadataAdapter,
|
|
50
51
|
MedusaProvider: () => MedusaProvider,
|
|
51
52
|
MetricsService: () => MetricsService,
|
|
52
53
|
MockProvider: () => MockProvider,
|
|
54
|
+
PROVIDER_METADATA: () => PROVIDER_METADATA,
|
|
53
55
|
ProviderAPIError: () => ProviderAPIError,
|
|
54
56
|
ProviderNotFoundError: () => ProviderNotFoundError,
|
|
55
57
|
ResourceNotFoundError: () => ResourceNotFoundError,
|
|
@@ -59,7 +61,8 @@ __export(src_exports, {
|
|
|
59
61
|
ShopifyProvider: () => ShopifyProvider,
|
|
60
62
|
SpreeProvider: () => SpreeProvider,
|
|
61
63
|
StelOrderProvider: () => StelOrderProvider,
|
|
62
|
-
StorefrontConfigError: () =>
|
|
64
|
+
StorefrontConfigError: () => StorefrontConfigError,
|
|
65
|
+
SupportedProviderType: () => SupportedProviderType,
|
|
63
66
|
WOOCOMMERCE_CAPABILITIES: () => WOOCOMMERCE_CAPABILITIES,
|
|
64
67
|
WooCommerceProvider: () => WooCommerceProvider,
|
|
65
68
|
assertEntityBelongsToChannel: () => assertEntityBelongsToChannel,
|
|
@@ -78,10 +81,12 @@ __export(src_exports, {
|
|
|
78
81
|
getBackendProviderId: () => getBackendProviderId,
|
|
79
82
|
getCapabilityDescription: () => getCapabilityDescription,
|
|
80
83
|
getChannelScope: () => getChannelScope,
|
|
84
|
+
getProviderMetadata: () => getProviderMetadata,
|
|
81
85
|
isBackendCapabilities: () => isBackendCapabilities,
|
|
82
86
|
isGlobalEntity: () => isGlobalEntity,
|
|
83
87
|
isProviderType: () => isProviderType,
|
|
84
88
|
isScopedToChannel: () => isScopedToChannel,
|
|
89
|
+
isSupportedProviderType: () => isSupportedProviderType,
|
|
85
90
|
moneyToNumber: () => moneyToNumber,
|
|
86
91
|
moneyToNumberSafe: () => moneyToNumberSafe,
|
|
87
92
|
numberToMoney: () => numberToMoney,
|
|
@@ -98,7 +103,6 @@ __export(src_exports, {
|
|
|
98
103
|
validatePaymentId: () => validatePaymentId
|
|
99
104
|
});
|
|
100
105
|
module.exports = __toCommonJS(src_exports);
|
|
101
|
-
var import_types2 = require("@thorprovider/types");
|
|
102
106
|
|
|
103
107
|
// src/types/capabilities.types.ts
|
|
104
108
|
var MOCK_CAPABILITIES = {
|
|
@@ -463,6 +467,16 @@ var ConfigurationError = class extends CommerceError {
|
|
|
463
467
|
this.name = "ConfigurationError";
|
|
464
468
|
}
|
|
465
469
|
};
|
|
470
|
+
var StorefrontConfigError = class extends Error {
|
|
471
|
+
platformType;
|
|
472
|
+
requiredEnvVar;
|
|
473
|
+
constructor(message, platformType, requiredEnvVar) {
|
|
474
|
+
super(message);
|
|
475
|
+
this.name = "StorefrontConfigError";
|
|
476
|
+
this.platformType = platformType;
|
|
477
|
+
this.requiredEnvVar = requiredEnvVar;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
466
480
|
var ResourceNotFoundError = class extends CommerceError {
|
|
467
481
|
constructor(resource, identifier) {
|
|
468
482
|
super(`${resource} not found: ${identifier}`, "RESOURCE_NOT_FOUND", 404);
|
|
@@ -997,6 +1011,16 @@ function transformMoney(amount, currencyCode = "USD") {
|
|
|
997
1011
|
currencyCode: currencyCode.toUpperCase()
|
|
998
1012
|
};
|
|
999
1013
|
}
|
|
1014
|
+
function transformAmount(amount, currencyCode) {
|
|
1015
|
+
const currencyCodeUpper = (currencyCode ?? "").toUpperCase();
|
|
1016
|
+
if (typeof amount !== "number" || !Number.isFinite(amount)) {
|
|
1017
|
+
return { amount: "", currencyCode: currencyCodeUpper };
|
|
1018
|
+
}
|
|
1019
|
+
return { amount: amount.toFixed(2), currencyCode: currencyCodeUpper };
|
|
1020
|
+
}
|
|
1021
|
+
function transformProductPrice(amount, currencyCode) {
|
|
1022
|
+
return transformAmount(amount, currencyCode);
|
|
1023
|
+
}
|
|
1000
1024
|
function transformImage(url, alt = "") {
|
|
1001
1025
|
return {
|
|
1002
1026
|
url,
|
|
@@ -1048,39 +1072,42 @@ function transformProductOption(option) {
|
|
|
1048
1072
|
values: option.values?.map((v) => v.value ?? "") ?? []
|
|
1049
1073
|
};
|
|
1050
1074
|
}
|
|
1051
|
-
function transformProductVariant(variant, currencyCode
|
|
1075
|
+
function transformProductVariant(variant, currencyCode) {
|
|
1052
1076
|
let priceAmount;
|
|
1053
1077
|
let priceCurrency = currencyCode;
|
|
1054
1078
|
if (variant.calculated_price) {
|
|
1055
1079
|
priceAmount = variant.calculated_price.calculated_amount;
|
|
1056
|
-
priceCurrency = variant.calculated_price.currency_code
|
|
1080
|
+
priceCurrency = variant.calculated_price.currency_code ?? currencyCode;
|
|
1057
1081
|
} else if (variant.prices && variant.prices.length > 0) {
|
|
1058
|
-
const
|
|
1082
|
+
const target = currencyCode?.toLowerCase();
|
|
1083
|
+
const price = (target ? variant.prices.find((p) => p.currency_code === target) : void 0) ?? variant.prices[0];
|
|
1059
1084
|
priceAmount = price?.amount;
|
|
1060
|
-
priceCurrency = price?.currency_code
|
|
1085
|
+
priceCurrency = price?.currency_code ?? currencyCode;
|
|
1061
1086
|
}
|
|
1062
1087
|
const isAvailable = variant.manage_inventory === false ? true : (variant.inventory_quantity ?? 0) > 0 || variant.allow_backorder === true;
|
|
1063
1088
|
return {
|
|
1064
1089
|
id: variant.id,
|
|
1065
1090
|
title: variant.title ?? "Default",
|
|
1091
|
+
// SKU is optional descriptive data only — never API/purchase identity.
|
|
1066
1092
|
sku: variant.sku ?? void 0,
|
|
1067
1093
|
availableForSale: isAvailable,
|
|
1068
1094
|
selectedOptions: variant.options?.map((opt) => ({
|
|
1069
1095
|
name: opt.option?.title ?? "",
|
|
1070
1096
|
value: opt.value ?? ""
|
|
1071
1097
|
})) ?? [],
|
|
1072
|
-
price:
|
|
1098
|
+
price: transformProductPrice(priceAmount, priceCurrency),
|
|
1073
1099
|
compareAtPrice: void 0,
|
|
1074
1100
|
// Medusa doesn't have compare-at pricing
|
|
1075
1101
|
quantityAvailable: variant.manage_inventory ? variant.inventory_quantity : void 0,
|
|
1076
1102
|
image: variant.thumbnail ? transformImage(variant.thumbnail, variant.title ?? "") : void 0
|
|
1077
1103
|
};
|
|
1078
1104
|
}
|
|
1079
|
-
function transformProduct(product, currencyCode
|
|
1105
|
+
function transformProduct(product, currencyCode) {
|
|
1080
1106
|
const variants = product.variants?.map((v) => transformProductVariant(v, currencyCode)) ?? [];
|
|
1081
|
-
const
|
|
1082
|
-
const
|
|
1083
|
-
const
|
|
1107
|
+
const validPrices = variants.map((v) => v.price).filter((money) => money.amount !== "" && Number.isFinite(Number(money.amount))).map((money) => Number(money.amount));
|
|
1108
|
+
const rangeCurrency = variants.find((v) => v.price.currencyCode)?.price.currencyCode ?? (currencyCode ?? "").toUpperCase();
|
|
1109
|
+
const minPrice = validPrices.length > 0 ? Math.min(...validPrices) : void 0;
|
|
1110
|
+
const maxPrice = validPrices.length > 0 ? Math.max(...validPrices) : void 0;
|
|
1084
1111
|
return {
|
|
1085
1112
|
id: product.id,
|
|
1086
1113
|
handle: product.handle ?? "",
|
|
@@ -1096,16 +1123,28 @@ function transformProduct(product, currencyCode = "USD") {
|
|
|
1096
1123
|
images: product.images?.map((img) => transformImage(img.url ?? "", product.title ?? "")) ?? [],
|
|
1097
1124
|
priceRange: {
|
|
1098
1125
|
// Build Money directly from already-resolved major-unit prices.
|
|
1099
|
-
// Do NOT
|
|
1100
|
-
// and do NOT
|
|
1101
|
-
minVariantPrice: {
|
|
1102
|
-
|
|
1126
|
+
// Do NOT multiply by 100 (that was a compensatory hack for the old /100
|
|
1127
|
+
// bug) and do NOT fabricate a zero for an unresolved price.
|
|
1128
|
+
minVariantPrice: {
|
|
1129
|
+
amount: minPrice !== void 0 ? minPrice.toFixed(2) : "",
|
|
1130
|
+
currencyCode: rangeCurrency
|
|
1131
|
+
},
|
|
1132
|
+
maxVariantPrice: {
|
|
1133
|
+
amount: maxPrice !== void 0 ? maxPrice.toFixed(2) : "",
|
|
1134
|
+
currencyCode: rangeCurrency
|
|
1135
|
+
}
|
|
1103
1136
|
},
|
|
1104
1137
|
collections: product.collection ? [{
|
|
1105
1138
|
id: product.collection.id,
|
|
1106
1139
|
title: product.collection.title ?? "",
|
|
1107
1140
|
handle: product.collection.handle ?? ""
|
|
1108
1141
|
}] : void 0,
|
|
1142
|
+
categories: product.categories?.map((category) => ({
|
|
1143
|
+
id: category.id,
|
|
1144
|
+
name: category.name ?? "",
|
|
1145
|
+
handle: category.handle ?? "",
|
|
1146
|
+
parentCategoryId: category.parent_category_id ?? void 0
|
|
1147
|
+
})),
|
|
1109
1148
|
productType: product.type?.value ?? void 0,
|
|
1110
1149
|
vendor: void 0,
|
|
1111
1150
|
// Medusa doesn't have vendor field
|
|
@@ -1122,38 +1161,19 @@ function transformProduct(product, currencyCode = "USD") {
|
|
|
1122
1161
|
function resolveMedusaCartItemCurrencyCode(item, cartCurrencyCode) {
|
|
1123
1162
|
return item?.cart?.currency_code ?? cartCurrencyCode ?? "usd";
|
|
1124
1163
|
}
|
|
1125
|
-
function
|
|
1126
|
-
const
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
item?.total,
|
|
1130
|
-
item?.item_total,
|
|
1131
|
-
item?.original_total,
|
|
1132
|
-
item?.subtotal,
|
|
1133
|
-
item?.item_subtotal,
|
|
1134
|
-
item?.original_subtotal
|
|
1135
|
-
];
|
|
1136
|
-
for (const candidate of totalCandidates) {
|
|
1137
|
-
if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0) {
|
|
1164
|
+
function resolveAuthoritativeLineTotal(item) {
|
|
1165
|
+
const candidates = [item?.total, item?.item_total, item?.subtotal];
|
|
1166
|
+
for (const candidate of candidates) {
|
|
1167
|
+
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
1138
1168
|
return candidate;
|
|
1139
1169
|
}
|
|
1140
1170
|
}
|
|
1141
|
-
|
|
1142
|
-
item?.unit_price,
|
|
1143
|
-
item?.original_unit_price
|
|
1144
|
-
];
|
|
1145
|
-
for (const candidate of unitCandidates) {
|
|
1146
|
-
if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0) {
|
|
1147
|
-
return candidate * safeQuantity;
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
return 0;
|
|
1171
|
+
return void 0;
|
|
1151
1172
|
}
|
|
1152
1173
|
function transformCartItem(item, cartCurrencyCode) {
|
|
1153
1174
|
const variant = item.variant;
|
|
1154
1175
|
const product = variant?.product;
|
|
1155
1176
|
const currencyCode = resolveMedusaCartItemCurrencyCode(item, cartCurrencyCode);
|
|
1156
|
-
const totalCents = resolveMedusaCartItemTotal(item);
|
|
1157
1177
|
const merchandiseId = variant?.id ?? item.variant_id ?? "";
|
|
1158
1178
|
const merchandiseTitle = variant?.title ?? item.variant_title ?? "";
|
|
1159
1179
|
const productId = product?.id ?? item.product_id ?? "";
|
|
@@ -1164,7 +1184,9 @@ function transformCartItem(item, cartCurrencyCode) {
|
|
|
1164
1184
|
id: item.id,
|
|
1165
1185
|
quantity: item.quantity ?? 1,
|
|
1166
1186
|
cost: {
|
|
1167
|
-
totalAmount:
|
|
1187
|
+
totalAmount: transformAmount(resolveAuthoritativeLineTotal(item), currencyCode),
|
|
1188
|
+
// Backend unit price, carried so the display never derives it locally.
|
|
1189
|
+
unitPrice: transformAmount(item?.unit_price, currencyCode)
|
|
1168
1190
|
},
|
|
1169
1191
|
merchandise: {
|
|
1170
1192
|
id: merchandiseId,
|
|
@@ -1183,19 +1205,30 @@ function transformCartItem(item, cartCurrencyCode) {
|
|
|
1183
1205
|
};
|
|
1184
1206
|
}
|
|
1185
1207
|
function transformCart(cart) {
|
|
1186
|
-
const currencyCode = cart.currency_code
|
|
1208
|
+
const currencyCode = cart.currency_code;
|
|
1187
1209
|
return {
|
|
1188
1210
|
id: cart.id,
|
|
1189
1211
|
checkoutUrl: `/checkout/${cart.id}`,
|
|
1190
1212
|
// Medusa doesn't have a direct checkout URL
|
|
1191
1213
|
cost: {
|
|
1192
|
-
subtotalAmount:
|
|
1193
|
-
|
|
1194
|
-
|
|
1214
|
+
subtotalAmount: transformAmount(
|
|
1215
|
+
typeof cart.item_subtotal === "number" ? cart.item_subtotal : cart.subtotal,
|
|
1216
|
+
currencyCode
|
|
1217
|
+
),
|
|
1218
|
+
discountAmount: transformAmount(cart.discount_total, currencyCode),
|
|
1219
|
+
totalTaxAmount: transformAmount(cart.tax_total, currencyCode),
|
|
1220
|
+
shippingAmount: transformAmount(cart.shipping_total, currencyCode),
|
|
1221
|
+
totalAmount: transformAmount(cart.total, currencyCode)
|
|
1195
1222
|
},
|
|
1196
1223
|
lines: cart.items?.map((item) => transformCartItem(item, currencyCode)) ?? [],
|
|
1197
1224
|
totalQuantity: cart.items?.reduce((sum, item) => sum + (item.quantity ?? 0), 0) ?? 0,
|
|
1198
|
-
|
|
1225
|
+
salesChannelId: cart.sales_channel_id ?? void 0,
|
|
1226
|
+
regionId: cart.region_id ?? void 0,
|
|
1227
|
+
currencyCode: typeof currencyCode === "string" ? currencyCode.toUpperCase() : void 0,
|
|
1228
|
+
selectedShippingMethodId: cart.shipping_methods?.[0]?.id ?? void 0,
|
|
1229
|
+
selectedShippingOptionId: cart.shipping_methods?.[0]?.shipping_option_id ?? void 0,
|
|
1230
|
+
completedAt: cart.completed_at ? typeof cart.completed_at === "string" ? cart.completed_at : cart.completed_at?.toISOString() : void 0,
|
|
1231
|
+
metadata: cart.metadata ?? void 0
|
|
1199
1232
|
};
|
|
1200
1233
|
}
|
|
1201
1234
|
function transformCollection(collection) {
|
|
@@ -1284,8 +1317,8 @@ function transformOrderItem(item, currencyCode) {
|
|
|
1284
1317
|
title: item.product_title ?? item.title ?? "",
|
|
1285
1318
|
variantTitle: item.variant_title ?? variant?.title ?? void 0,
|
|
1286
1319
|
quantity: item.quantity ?? 1,
|
|
1287
|
-
price:
|
|
1288
|
-
total:
|
|
1320
|
+
price: transformAmount(item.unit_price, currencyCode),
|
|
1321
|
+
total: transformAmount(item.total, currencyCode),
|
|
1289
1322
|
sku: variant?.sku ?? void 0,
|
|
1290
1323
|
image: product?.thumbnail || item.thumbnail ? transformImage(product?.thumbnail || item.thumbnail, item.product_title ?? "") : void 0,
|
|
1291
1324
|
productId: item.product_id ?? product?.id ?? void 0,
|
|
@@ -1336,12 +1369,19 @@ function transformOrder(order) {
|
|
|
1336
1369
|
customer: order.customer ? transformCustomer(order.customer, order.email) : void 0,
|
|
1337
1370
|
// Items
|
|
1338
1371
|
items: (order.items ?? []).filter((item) => item != null).map((item) => transformOrderItem(item, currencyCode)) ?? [],
|
|
1339
|
-
// Pricing
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1372
|
+
// Pricing — backend-authoritative; a legitimate zero is preserved and no
|
|
1373
|
+
// amount is derived locally. `subtotal` prefers the backend's
|
|
1374
|
+
// `item_subtotal` (lines only, excluding shipping/tax); Medusa's native
|
|
1375
|
+
// `subtotal` folds in `shipping_subtotal`, so falling back to it is only
|
|
1376
|
+
// for installs that do not supply `item_subtotal`.
|
|
1377
|
+
subtotal: transformAmount(
|
|
1378
|
+
typeof order.item_subtotal === "number" ? order.item_subtotal : order.subtotal,
|
|
1379
|
+
currencyCode
|
|
1380
|
+
),
|
|
1381
|
+
total: transformAmount(order.total, currencyCode),
|
|
1382
|
+
tax: transformAmount(order.tax_total, currencyCode),
|
|
1383
|
+
shipping: transformAmount(order.shipping_total, currencyCode),
|
|
1384
|
+
discount: transformAmount(order.discount_total, currencyCode),
|
|
1345
1385
|
// Addresses (transformed to camelCase)
|
|
1346
1386
|
shippingAddress: order.shipping_address ? transformAddress(order.shipping_address) : void 0,
|
|
1347
1387
|
billingAddress: order.billing_address ? transformAddress(order.billing_address) : void 0,
|
|
@@ -1760,7 +1800,6 @@ function assertEntityBelongsToChannel(entity, expectedChannelId) {
|
|
|
1760
1800
|
|
|
1761
1801
|
// src/providers/medusa/index.ts
|
|
1762
1802
|
var import_js_sdk2 = __toESM(require("@medusajs/js-sdk"));
|
|
1763
|
-
var import_types = require("@thorprovider/types");
|
|
1764
1803
|
|
|
1765
1804
|
// src/providers/medusa/auth.ts
|
|
1766
1805
|
var AuthStorageManager = class {
|
|
@@ -1878,6 +1917,7 @@ var MedusaAuthProvider = class {
|
|
|
1878
1917
|
if (this.authConfig.method === "jwt") {
|
|
1879
1918
|
await this.storageManager.setToken(token);
|
|
1880
1919
|
}
|
|
1920
|
+
await this.linkCustomerChannel();
|
|
1881
1921
|
const customer = await this.getCurrentCustomer();
|
|
1882
1922
|
this.logger.info("Login successful", { customerId: customer?.id });
|
|
1883
1923
|
return {
|
|
@@ -1885,6 +1925,7 @@ var MedusaAuthProvider = class {
|
|
|
1885
1925
|
customer: customer ?? void 0
|
|
1886
1926
|
};
|
|
1887
1927
|
} catch (error) {
|
|
1928
|
+
if (error instanceof ProviderAPIError) throw error;
|
|
1888
1929
|
this.logger.error("Login failed", error);
|
|
1889
1930
|
throw new ProviderAPIError(
|
|
1890
1931
|
`Authentication failed: ${error.message}`,
|
|
@@ -1896,16 +1937,14 @@ var MedusaAuthProvider = class {
|
|
|
1896
1937
|
async register(data) {
|
|
1897
1938
|
this.logger.debug("register", { email: data.email });
|
|
1898
1939
|
try {
|
|
1899
|
-
await this.sdk.auth.register("customer", "emailpass", {
|
|
1940
|
+
const registrationToken = await this.sdk.auth.register("customer", "emailpass", {
|
|
1900
1941
|
email: data.email,
|
|
1901
1942
|
password: data.password
|
|
1902
1943
|
});
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
phone: data.phone
|
|
1908
|
-
});
|
|
1944
|
+
if (this.authConfig.method === "jwt" && typeof registrationToken === "string") {
|
|
1945
|
+
await this.storageManager.setToken(registrationToken);
|
|
1946
|
+
}
|
|
1947
|
+
const linked = await this.createChannelLinkedCustomer(data);
|
|
1909
1948
|
const loginResult = await this.sdk.auth.login("customer", "emailpass", {
|
|
1910
1949
|
email: data.email,
|
|
1911
1950
|
password: data.password
|
|
@@ -1931,12 +1970,20 @@ var MedusaAuthProvider = class {
|
|
|
1931
1970
|
if (this.authConfig.method === "jwt") {
|
|
1932
1971
|
await this.storageManager.setToken(token);
|
|
1933
1972
|
}
|
|
1973
|
+
const customer = await this.getCurrentCustomer();
|
|
1974
|
+
if (!customer || customer.id !== linked.id) {
|
|
1975
|
+
throw new ProviderAPIError(
|
|
1976
|
+
"Registration created an auth identity without a linked customer.",
|
|
1977
|
+
"CUSTOMER_LINK_INCOMPLETE"
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1934
1980
|
this.logger.info("Registration successful", { customerId: customer.id });
|
|
1935
1981
|
return {
|
|
1936
1982
|
token: this.authConfig.method === "jwt" ? token : void 0,
|
|
1937
|
-
customer
|
|
1983
|
+
customer
|
|
1938
1984
|
};
|
|
1939
1985
|
} catch (error) {
|
|
1986
|
+
if (error instanceof ProviderAPIError) throw error;
|
|
1940
1987
|
this.logger.error("Registration failed", error);
|
|
1941
1988
|
throw new ProviderAPIError(
|
|
1942
1989
|
`Registration failed: ${error.message}`,
|
|
@@ -1945,6 +1992,79 @@ var MedusaAuthProvider = class {
|
|
|
1945
1992
|
);
|
|
1946
1993
|
}
|
|
1947
1994
|
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Create and channel-link a customer through `POST /store/thor/customers`.
|
|
1997
|
+
*
|
|
1998
|
+
* The request carries the registration token (read by the SDK from the
|
|
1999
|
+
* request-scoped custom storage) and the publishable key; the backend links
|
|
2000
|
+
* the customer to the publishable-key sales channel. A route that is
|
|
2001
|
+
* unavailable fails closed with a typed error — there is no silent native
|
|
2002
|
+
* fallback that would leave the customer unlinked.
|
|
2003
|
+
*/
|
|
2004
|
+
async createChannelLinkedCustomer(data) {
|
|
2005
|
+
const response = await this.fetchChannelAccountRoute(
|
|
2006
|
+
"/store/thor/customers",
|
|
2007
|
+
{
|
|
2008
|
+
method: "POST",
|
|
2009
|
+
body: {
|
|
2010
|
+
email: data.email,
|
|
2011
|
+
first_name: data.first_name,
|
|
2012
|
+
last_name: data.last_name,
|
|
2013
|
+
phone: data.phone
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
);
|
|
2017
|
+
const id = response?.customer?.id;
|
|
2018
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
2019
|
+
throw new ProviderAPIError(
|
|
2020
|
+
"Channel-linked customer creation returned no customer id.",
|
|
2021
|
+
"CUSTOMER_LINK_INCOMPLETE"
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
return { id };
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Link the authenticated customer to the publishable-key sales channel
|
|
2028
|
+
* through `POST /store/thor/customers/me/channels` (idempotent). Called after
|
|
2029
|
+
* login so history is channel-scoped for the current storefront.
|
|
2030
|
+
*/
|
|
2031
|
+
async linkCustomerChannel() {
|
|
2032
|
+
const response = await this.fetchChannelAccountRoute("/store/thor/customers/me/channels", { method: "POST" });
|
|
2033
|
+
if (response?.linked === false && response?.reason !== "already_linked") {
|
|
2034
|
+
throw new ProviderAPIError(
|
|
2035
|
+
"Customer channel link did not complete.",
|
|
2036
|
+
"CUSTOMER_LINK_INCOMPLETE"
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
/**
|
|
2041
|
+
* Call a Thor channel account route through the SDK client, which attaches
|
|
2042
|
+
* the request-scoped customer/registration token and the publishable key. A
|
|
2043
|
+
* missing client or an unavailable route (404/405/501) fails closed with a
|
|
2044
|
+
* typed `CUSTOMER_LINK_UNAVAILABLE` error.
|
|
2045
|
+
*/
|
|
2046
|
+
async fetchChannelAccountRoute(path, init) {
|
|
2047
|
+
const client = this.sdk.client;
|
|
2048
|
+
if (!client || typeof client.fetch !== "function") {
|
|
2049
|
+
throw new ProviderAPIError(
|
|
2050
|
+
`The channel account route ${path} is unavailable in this SDK.`,
|
|
2051
|
+
"CUSTOMER_LINK_UNAVAILABLE"
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
try {
|
|
2055
|
+
return await client.fetch(path, init);
|
|
2056
|
+
} catch (error) {
|
|
2057
|
+
const status = error?.status || error?.statusCode;
|
|
2058
|
+
if (status === 404 || status === 405 || status === 501) {
|
|
2059
|
+
throw new ProviderAPIError(
|
|
2060
|
+
`The channel account route ${path} is unavailable (status ${status}).`,
|
|
2061
|
+
"CUSTOMER_LINK_UNAVAILABLE",
|
|
2062
|
+
status
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
throw error;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
1948
2068
|
async logout() {
|
|
1949
2069
|
this.logger.debug("logout");
|
|
1950
2070
|
try {
|
|
@@ -2290,10 +2410,164 @@ async function getPaymentMethods(sdk, options) {
|
|
|
2290
2410
|
}
|
|
2291
2411
|
|
|
2292
2412
|
// src/providers/medusa/index.ts
|
|
2293
|
-
var MEDUSA_CART_RETRIEVE_FIELDS = "+items,+items.variant,+items.variant.product,+items.variant.product.thumbnail,+items.variant.options,+items.variant.options.option";
|
|
2413
|
+
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";
|
|
2414
|
+
var CUSTOMER_ORDERS_DEFAULT_PAGE_SIZE = 10;
|
|
2415
|
+
var CUSTOMER_ORDERS_MAX_PAGE_SIZE = 100;
|
|
2416
|
+
function normalizeCustomerOrdersPage(options) {
|
|
2417
|
+
const page = Number.isInteger(options.page) && options.page > 0 ? options.page : 1;
|
|
2418
|
+
const requested = Number.isInteger(options.pageSize) && options.pageSize > 0 ? options.pageSize : CUSTOMER_ORDERS_DEFAULT_PAGE_SIZE;
|
|
2419
|
+
const pageSize = Math.min(requested, CUSTOMER_ORDERS_MAX_PAGE_SIZE);
|
|
2420
|
+
return { page, pageSize, limit: pageSize, offset: (page - 1) * pageSize };
|
|
2421
|
+
}
|
|
2422
|
+
var MEDUSA_ORDER_COMPLETION_FIELDS = "*items";
|
|
2423
|
+
var DEFAULT_CATALOG_PAGE_SIZE = 50;
|
|
2424
|
+
function resolveCatalogPageSize(configured) {
|
|
2425
|
+
const fromEnv = process.env.MEDUSA_CATALOG_PAGE_SIZE !== void 0 ? Number(process.env.MEDUSA_CATALOG_PAGE_SIZE) : void 0;
|
|
2426
|
+
const raw = configured ?? fromEnv;
|
|
2427
|
+
if (raw === void 0 || !Number.isFinite(raw) || raw < 1) {
|
|
2428
|
+
return DEFAULT_CATALOG_PAGE_SIZE;
|
|
2429
|
+
}
|
|
2430
|
+
return Math.floor(raw);
|
|
2431
|
+
}
|
|
2432
|
+
function resolveConfiguredSalesChannelId() {
|
|
2433
|
+
return process.env.MEDUSA_SALES_CHANNEL_ID ?? process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
|
|
2434
|
+
}
|
|
2435
|
+
var MEDUSA_SORT_KEY_MAP = {
|
|
2436
|
+
featured: null,
|
|
2437
|
+
name: "title",
|
|
2438
|
+
newest: "-created_at",
|
|
2439
|
+
"price-asc": null,
|
|
2440
|
+
"price-desc": null,
|
|
2441
|
+
rating: null
|
|
2442
|
+
};
|
|
2443
|
+
function resolveEffectiveCategoryHandles(query) {
|
|
2444
|
+
const { categoryScope, categories } = query;
|
|
2445
|
+
if (categoryScope === void 0) return categories;
|
|
2446
|
+
if (categories === void 0) return [categoryScope];
|
|
2447
|
+
return categories.filter((handle) => handle === categoryScope);
|
|
2448
|
+
}
|
|
2449
|
+
function buildAppliedCatalogFilters(query, effectiveCategoryHandles, includeAdvanced = false) {
|
|
2450
|
+
return {
|
|
2451
|
+
...query.text ? { text: query.text } : {},
|
|
2452
|
+
...effectiveCategoryHandles !== void 0 ? { categories: effectiveCategoryHandles } : {},
|
|
2453
|
+
...query.collections !== void 0 ? { collections: query.collections } : {},
|
|
2454
|
+
...includeAdvanced && query.priceMin !== void 0 ? { priceMin: query.priceMin } : {},
|
|
2455
|
+
...includeAdvanced && query.priceMax !== void 0 ? { priceMax: query.priceMax } : {},
|
|
2456
|
+
...includeAdvanced && query.inStock !== void 0 ? { inStock: query.inStock } : {},
|
|
2457
|
+
...includeAdvanced && query.attributes !== void 0 ? { attributes: query.attributes } : {}
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
function buildBasicDemonstratedQuery(query) {
|
|
2461
|
+
return {
|
|
2462
|
+
...query.text !== void 0 ? { text: query.text } : {},
|
|
2463
|
+
...query.categoryScope !== void 0 ? { categoryScope: query.categoryScope } : {},
|
|
2464
|
+
...query.categories !== void 0 ? { categories: query.categories } : {},
|
|
2465
|
+
...query.collections !== void 0 ? { collections: query.collections } : {}
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
var ADVANCED_SEARCH_ENDPOINT = "/store/search";
|
|
2469
|
+
var BASIC_LISTING_ENDPOINT = "/store/products";
|
|
2470
|
+
function collectRequestedAdvancedFields(query) {
|
|
2471
|
+
const fields = [];
|
|
2472
|
+
if (query.priceMin !== void 0) fields.push("priceMin");
|
|
2473
|
+
if (query.priceMax !== void 0) fields.push("priceMax");
|
|
2474
|
+
if (query.inStock !== void 0) fields.push("inStock");
|
|
2475
|
+
for (const name of Object.keys(query.attributes ?? {})) {
|
|
2476
|
+
fields.push(`attr.${name}`);
|
|
2477
|
+
}
|
|
2478
|
+
return fields;
|
|
2479
|
+
}
|
|
2480
|
+
function advancedFieldToFacet(field) {
|
|
2481
|
+
if (field === "priceMin" || field === "priceMax") return "price";
|
|
2482
|
+
return field;
|
|
2483
|
+
}
|
|
2484
|
+
function collectAdvancedSearchRequestFields(options) {
|
|
2485
|
+
const fields = [];
|
|
2486
|
+
if (options?.priceMin !== void 0) fields.push("priceMin");
|
|
2487
|
+
if (options?.priceMax !== void 0) fields.push("priceMax");
|
|
2488
|
+
if (options?.inStockOnly === true) fields.push("inStock");
|
|
2489
|
+
if (options?.attributes && options.attributes.length > 0) {
|
|
2490
|
+
fields.push("attributes");
|
|
2491
|
+
}
|
|
2492
|
+
return fields;
|
|
2493
|
+
}
|
|
2494
|
+
function toFiniteNumber(value) {
|
|
2495
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2496
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
2497
|
+
const parsed = Number(value);
|
|
2498
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
2499
|
+
}
|
|
2500
|
+
return void 0;
|
|
2501
|
+
}
|
|
2502
|
+
function readAppliedPriceRange(applied) {
|
|
2503
|
+
const range = applied.price_range;
|
|
2504
|
+
if (!range || typeof range !== "object" || Array.isArray(range)) return {};
|
|
2505
|
+
const record = range;
|
|
2506
|
+
return { min: toFiniteNumber(record.min), max: toFiniteNumber(record.max) };
|
|
2507
|
+
}
|
|
2508
|
+
function mapAppliedFiltersToFacets(applied) {
|
|
2509
|
+
const facets = /* @__PURE__ */ new Set();
|
|
2510
|
+
const priceRange = readAppliedPriceRange(applied);
|
|
2511
|
+
const hasAppliedPriceRange = priceRange.min !== void 0 && priceRange.max !== void 0;
|
|
2512
|
+
if (hasAppliedPriceRange || "price" in applied || "price_min" in applied || "price_max" in applied) {
|
|
2513
|
+
facets.add("price");
|
|
2514
|
+
}
|
|
2515
|
+
if (typeof applied.is_available === "boolean" || "inStock" in applied || "isAvailable" in applied || "available" in applied) {
|
|
2516
|
+
facets.add("inStock");
|
|
2517
|
+
}
|
|
2518
|
+
const attributes = applied.attributes;
|
|
2519
|
+
if (attributes && typeof attributes === "object" && !Array.isArray(attributes)) {
|
|
2520
|
+
for (const name of Object.keys(attributes)) {
|
|
2521
|
+
facets.add(`attr.${name}`);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
return facets;
|
|
2525
|
+
}
|
|
2526
|
+
function collectUnappliedAdvancedFields(applied, query) {
|
|
2527
|
+
const unapplied = [];
|
|
2528
|
+
const priceRange = readAppliedPriceRange(applied);
|
|
2529
|
+
if (query.priceMin !== void 0) {
|
|
2530
|
+
const echoed = toFiniteNumber(
|
|
2531
|
+
priceRange.min ?? applied.price_min ?? applied.priceMin
|
|
2532
|
+
);
|
|
2533
|
+
if (echoed !== query.priceMin) unapplied.push("priceMin");
|
|
2534
|
+
}
|
|
2535
|
+
if (query.priceMax !== void 0) {
|
|
2536
|
+
const echoed = toFiniteNumber(
|
|
2537
|
+
priceRange.max ?? applied.price_max ?? applied.priceMax
|
|
2538
|
+
);
|
|
2539
|
+
if (echoed !== query.priceMax) unapplied.push("priceMax");
|
|
2540
|
+
}
|
|
2541
|
+
if (query.inStock === true) {
|
|
2542
|
+
const echoed = applied.is_available ?? applied.isAvailable ?? applied.inStock ?? applied.available;
|
|
2543
|
+
if (echoed !== true && echoed !== "true") unapplied.push("inStock");
|
|
2544
|
+
}
|
|
2545
|
+
const requestedAttributes = query.attributes ?? {};
|
|
2546
|
+
const appliedAttributes = applied.attributes;
|
|
2547
|
+
for (const name of Object.keys(requestedAttributes)) {
|
|
2548
|
+
const hasName = !!appliedAttributes && typeof appliedAttributes === "object" && !Array.isArray(appliedAttributes) && name in appliedAttributes;
|
|
2549
|
+
if (!hasName) unapplied.push(`attr.${name}`);
|
|
2550
|
+
}
|
|
2551
|
+
return unapplied;
|
|
2552
|
+
}
|
|
2553
|
+
function readFiniteFacetNumber(value) {
|
|
2554
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2555
|
+
}
|
|
2294
2556
|
var MedusaProvider = class {
|
|
2295
2557
|
name = "medusa";
|
|
2296
2558
|
version = "2.0.0";
|
|
2559
|
+
/**
|
|
2560
|
+
* Canonical catalog sort keys this provider applies at the query level.
|
|
2561
|
+
*
|
|
2562
|
+
* Advertises only the backend-verified orderings (E2 Decision 6). `price-asc`,
|
|
2563
|
+
* `price-desc`, and `rating` are intentionally absent: they are not applied at
|
|
2564
|
+
* the query level, so the listing never offers them as global sorts.
|
|
2565
|
+
*
|
|
2566
|
+
* The literal tuple is assignable to `CommerceProvider['supportedCatalogSortKeys']`
|
|
2567
|
+
* (`readonly CatalogSortKey[]`); it is written structurally here so the adapter
|
|
2568
|
+
* compiles against the published `@thorprovider/types` declaration.
|
|
2569
|
+
*/
|
|
2570
|
+
supportedCatalogSortKeys = ["featured", "name", "newest"];
|
|
2297
2571
|
/**
|
|
2298
2572
|
* Medusa backend capabilities.
|
|
2299
2573
|
* Starts with static defaults, replaced by dynamic values from
|
|
@@ -2315,6 +2589,14 @@ var MedusaProvider = class {
|
|
|
2315
2589
|
baseUrl;
|
|
2316
2590
|
publishableKey;
|
|
2317
2591
|
regionId;
|
|
2592
|
+
/** Configured per-request catalog listing bound (never an unbounded scan). */
|
|
2593
|
+
catalogPageSize;
|
|
2594
|
+
/**
|
|
2595
|
+
* Per-scope cache of the truthful facet-support descriptor. The optional
|
|
2596
|
+
* advanced endpoint is probed once per demonstrated scope/query rather than on
|
|
2597
|
+
* every listing request (E2 task 2.4).
|
|
2598
|
+
*/
|
|
2599
|
+
facetSupportCache = /* @__PURE__ */ new Map();
|
|
2318
2600
|
/** Secret API key for admin operations (if configured). */
|
|
2319
2601
|
adminApiKey;
|
|
2320
2602
|
/**
|
|
@@ -2359,6 +2641,7 @@ var MedusaProvider = class {
|
|
|
2359
2641
|
this.baseUrl = config.baseUrl;
|
|
2360
2642
|
this.publishableKey = config.publishableKey;
|
|
2361
2643
|
this.regionId = config.regionId;
|
|
2644
|
+
this.catalogPageSize = resolveCatalogPageSize(config.catalogPageSize);
|
|
2362
2645
|
this.retryConfig = {
|
|
2363
2646
|
maxRetries: config.retry?.maxRetries ?? 3,
|
|
2364
2647
|
timeoutMs: config.retry?.timeoutMs ?? 1e4,
|
|
@@ -2380,7 +2663,16 @@ var MedusaProvider = class {
|
|
|
2380
2663
|
debug: config.debug ?? false,
|
|
2381
2664
|
// Always use JWT mode for auth - SDK needs to send Bearer tokens
|
|
2382
2665
|
// Configure storage method for JWT tokens (browser only)
|
|
2383
|
-
auth: config.auth ? {
|
|
2666
|
+
auth: config.auth ? config.auth.customStorage ? {
|
|
2667
|
+
type: "jwt",
|
|
2668
|
+
// Request-scoped custom storage: the caller supplies a per-request
|
|
2669
|
+
// token holder (for example a server request's customer JWT). The
|
|
2670
|
+
// SDK reads the token on every request and writes the login /
|
|
2671
|
+
// registration token back into the same holder, so a fresh
|
|
2672
|
+
// provider per request is authenticated without shared state.
|
|
2673
|
+
jwtTokenStorageMethod: "custom",
|
|
2674
|
+
storage: config.auth.customStorage
|
|
2675
|
+
} : {
|
|
2384
2676
|
type: "jwt",
|
|
2385
2677
|
// Use localStorage in browser, memory storage on server
|
|
2386
2678
|
jwtTokenStorageMethod: typeof window !== "undefined" ? "local" : "memory"
|
|
@@ -2488,35 +2780,52 @@ var MedusaProvider = class {
|
|
|
2488
2780
|
}
|
|
2489
2781
|
throw lastError;
|
|
2490
2782
|
}
|
|
2783
|
+
/** Fields needed to price a product variant for the current region. */
|
|
2784
|
+
buildProductFields(regionId) {
|
|
2785
|
+
const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+tags,+categories.*";
|
|
2786
|
+
const pricingFields = ",+variants.calculated_price,+variants.prices";
|
|
2787
|
+
return regionId ? baseFields + pricingFields : baseFields;
|
|
2788
|
+
}
|
|
2789
|
+
/** Scope a store query to the configured storefront sales channel, if any. */
|
|
2790
|
+
applySalesChannelScope(queryParams) {
|
|
2791
|
+
const salesChannelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
|
|
2792
|
+
if (salesChannelId) {
|
|
2793
|
+
queryParams.sales_channel_id = [salesChannelId];
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
/**
|
|
2797
|
+
* Directed single-product lookup by a supported native filter (`handle` or
|
|
2798
|
+
* `id`). This is independent of catalog listing pagination, so a known
|
|
2799
|
+
* product beyond the first listing page still resolves without scanning.
|
|
2800
|
+
*
|
|
2801
|
+
* Medusa's store API supports `handle` and `id` on `/store/products`; it
|
|
2802
|
+
* does not expose a variant filter there, so a variant is resolved from the
|
|
2803
|
+
* product once the product is known (see `getProductDetailByHandle` in the
|
|
2804
|
+
* runtime). No `limit` beyond 1 is issued.
|
|
2805
|
+
*/
|
|
2806
|
+
async fetchSingleProduct(filter, options) {
|
|
2807
|
+
const regionId = options?.regionId ?? this.regionId;
|
|
2808
|
+
const queryParams = {
|
|
2809
|
+
...filter,
|
|
2810
|
+
limit: 1,
|
|
2811
|
+
fields: this.buildProductFields(regionId)
|
|
2812
|
+
};
|
|
2813
|
+
if (regionId) {
|
|
2814
|
+
queryParams.region_id = regionId;
|
|
2815
|
+
}
|
|
2816
|
+
this.applySalesChannelScope(queryParams);
|
|
2817
|
+
const { products } = await this.sdk.store.product.list(queryParams);
|
|
2818
|
+
if (!products || products.length === 0) {
|
|
2819
|
+
this.logger.debug("Product not found", filter);
|
|
2820
|
+
return void 0;
|
|
2821
|
+
}
|
|
2822
|
+
return transformProduct(products[0], this.currencyCode);
|
|
2823
|
+
}
|
|
2491
2824
|
async getProduct(handle, options) {
|
|
2492
2825
|
this.logger.debug("getProduct", { handle, regionId: options?.regionId });
|
|
2493
2826
|
try {
|
|
2494
2827
|
return await this.withRetry(
|
|
2495
|
-
|
|
2496
|
-
const regionId = options?.regionId ?? this.regionId;
|
|
2497
|
-
const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+tags";
|
|
2498
|
-
const pricingFields = ",+variants.calculated_price,+variants.prices";
|
|
2499
|
-
const fields = regionId ? baseFields + pricingFields : baseFields;
|
|
2500
|
-
const queryParams = {
|
|
2501
|
-
handle,
|
|
2502
|
-
limit: 1,
|
|
2503
|
-
fields
|
|
2504
|
-
};
|
|
2505
|
-
if (regionId) {
|
|
2506
|
-
queryParams.region_id = regionId;
|
|
2507
|
-
}
|
|
2508
|
-
const salesChannelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
|
|
2509
|
-
if (salesChannelId) {
|
|
2510
|
-
queryParams.sales_channel_id = [salesChannelId];
|
|
2511
|
-
}
|
|
2512
|
-
const { products } = await this.sdk.store.product.list(queryParams);
|
|
2513
|
-
if (!products || products.length === 0) {
|
|
2514
|
-
this.logger.debug("Product not found", { handle });
|
|
2515
|
-
return void 0;
|
|
2516
|
-
}
|
|
2517
|
-
const transformedProduct = transformProduct(products[0], this.currencyCode);
|
|
2518
|
-
return transformedProduct;
|
|
2519
|
-
},
|
|
2828
|
+
() => this.fetchSingleProduct({ handle }, options),
|
|
2520
2829
|
"getProduct",
|
|
2521
2830
|
{ handle, regionId: options?.regionId }
|
|
2522
2831
|
);
|
|
@@ -2528,6 +2837,26 @@ var MedusaProvider = class {
|
|
|
2528
2837
|
);
|
|
2529
2838
|
}
|
|
2530
2839
|
}
|
|
2840
|
+
/**
|
|
2841
|
+
* Directed product lookup by backend product ID. Independent of the first
|
|
2842
|
+
* listing page, mirroring `getProduct` for handle-based resolution.
|
|
2843
|
+
*/
|
|
2844
|
+
async getProductById(id, options) {
|
|
2845
|
+
this.logger.debug("getProductById", { id, regionId: options?.regionId });
|
|
2846
|
+
try {
|
|
2847
|
+
return await this.withRetry(
|
|
2848
|
+
() => this.fetchSingleProduct({ id }, options),
|
|
2849
|
+
"getProductById",
|
|
2850
|
+
{ id, regionId: options?.regionId }
|
|
2851
|
+
);
|
|
2852
|
+
} catch (error) {
|
|
2853
|
+
throw new ProviderAPIError(
|
|
2854
|
+
`Failed to fetch product by id: ${error.message}`,
|
|
2855
|
+
"medusa",
|
|
2856
|
+
error.status
|
|
2857
|
+
);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2531
2860
|
async getProducts(options = {}) {
|
|
2532
2861
|
this.logger.debug("getProducts", options);
|
|
2533
2862
|
try {
|
|
@@ -2537,28 +2866,14 @@ var MedusaProvider = class {
|
|
|
2537
2866
|
const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+tags";
|
|
2538
2867
|
const pricingFields = ",+variants.calculated_price,+variants.prices";
|
|
2539
2868
|
const fields = regionId ? baseFields + pricingFields : baseFields;
|
|
2540
|
-
const SORT_KEY_MAP = {
|
|
2541
|
-
featured: null,
|
|
2542
|
-
// server default order
|
|
2543
|
-
name: "title",
|
|
2544
|
-
// A→Z by product title
|
|
2545
|
-
newest: "-created_at",
|
|
2546
|
-
// newest products first
|
|
2547
|
-
"price-asc": null,
|
|
2548
|
-
// not supported by native endpoint → client-side
|
|
2549
|
-
"price-desc": null,
|
|
2550
|
-
// not supported by native endpoint → client-side
|
|
2551
|
-
rating: null
|
|
2552
|
-
// not supported → server default
|
|
2553
|
-
};
|
|
2554
2869
|
const queryParams = {
|
|
2555
2870
|
q: options.query,
|
|
2556
|
-
limit: options.first ??
|
|
2557
|
-
offset: options.after ? parseInt(String(options.after), 10) :
|
|
2871
|
+
limit: options.first ?? this.catalogPageSize,
|
|
2872
|
+
offset: options.after !== void 0 ? parseInt(String(options.after), 10) : options.skip,
|
|
2558
2873
|
fields
|
|
2559
2874
|
};
|
|
2560
2875
|
if (options.sortKey) {
|
|
2561
|
-
const mappedOrder =
|
|
2876
|
+
const mappedOrder = MEDUSA_SORT_KEY_MAP[options.sortKey] ?? null;
|
|
2562
2877
|
if (mappedOrder !== null) {
|
|
2563
2878
|
queryParams.order = mappedOrder;
|
|
2564
2879
|
}
|
|
@@ -2598,6 +2913,468 @@ var MedusaProvider = class {
|
|
|
2598
2913
|
);
|
|
2599
2914
|
}
|
|
2600
2915
|
}
|
|
2916
|
+
/**
|
|
2917
|
+
* Canonical catalog query path (E2 task 2.2).
|
|
2918
|
+
*
|
|
2919
|
+
* Accepts a validated `CatalogQuery`, translates it into Medusa Store
|
|
2920
|
+
* `/store/products` parameters, and returns a truthful `CatalogQueryOutcome`.
|
|
2921
|
+
* The route category scope is always retained and intersected with the
|
|
2922
|
+
* `categories` filter; `categories`/`collections` handles are resolved to
|
|
2923
|
+
* backend IDs through the existing directed lookups (`getCategory`/
|
|
2924
|
+
* `getCollection`). Any requested filter this provider cannot apply
|
|
2925
|
+
* (price/`inStock`/attributes) is rejected as a typed `unsupported-query`
|
|
2926
|
+
* outcome naming the fields — never a partial success and never a silent
|
|
2927
|
+
* drop. Pagination is `offset = (page - 1) * pageSize`, defaulting to the
|
|
2928
|
+
* canonical page size.
|
|
2929
|
+
*
|
|
2930
|
+
* Legacy `getProducts`/`getCategoryProducts` are unchanged; this additive
|
|
2931
|
+
* method is the typed seam the runtime query bridge (E2 task 3.1) consumes.
|
|
2932
|
+
*/
|
|
2933
|
+
async getCatalogProducts(query) {
|
|
2934
|
+
this.logger.debug("getCatalogProducts", query);
|
|
2935
|
+
if (query.sort !== void 0 && !this.supportedCatalogSortKeys.includes(query.sort)) {
|
|
2936
|
+
return this.unsupportedCatalogOutcome(["sort"]);
|
|
2937
|
+
}
|
|
2938
|
+
const effectiveCategoryHandles = resolveEffectiveCategoryHandles(query);
|
|
2939
|
+
try {
|
|
2940
|
+
const categoryIds = effectiveCategoryHandles !== void 0 ? await this.resolveCategoryIds(effectiveCategoryHandles) : void 0;
|
|
2941
|
+
const collectionIds = query.collections ? await this.resolveCollectionIds(query.collections) : void 0;
|
|
2942
|
+
const facetSupport = await this.getCatalogFacetSupport({
|
|
2943
|
+
query,
|
|
2944
|
+
categoryIds,
|
|
2945
|
+
collectionIds,
|
|
2946
|
+
scope: query.categoryScope
|
|
2947
|
+
});
|
|
2948
|
+
const requestedAdvanced = collectRequestedAdvancedFields(query);
|
|
2949
|
+
const heldAdvanced = requestedAdvanced.filter(
|
|
2950
|
+
(field) => !facetSupport.offered.includes(advancedFieldToFacet(field))
|
|
2951
|
+
);
|
|
2952
|
+
if (heldAdvanced.length > 0) {
|
|
2953
|
+
return this.unsupportedCatalogOutcome(heldAdvanced);
|
|
2954
|
+
}
|
|
2955
|
+
const emptyIntersection = effectiveCategoryHandles !== void 0 && effectiveCategoryHandles.length === 0;
|
|
2956
|
+
const categoryFilterEmpty = effectiveCategoryHandles !== void 0 && (categoryIds?.length ?? 0) === 0;
|
|
2957
|
+
const collectionFilterEmpty = query.collections !== void 0 && (collectionIds?.length ?? 0) === 0;
|
|
2958
|
+
if (emptyIntersection || categoryFilterEmpty || collectionFilterEmpty) {
|
|
2959
|
+
return {
|
|
2960
|
+
ok: true,
|
|
2961
|
+
results: {
|
|
2962
|
+
products: [],
|
|
2963
|
+
appliedFilters: buildAppliedCatalogFilters(
|
|
2964
|
+
query,
|
|
2965
|
+
effectiveCategoryHandles,
|
|
2966
|
+
requestedAdvanced.length > 0
|
|
2967
|
+
),
|
|
2968
|
+
facetSupport
|
|
2969
|
+
}
|
|
2970
|
+
};
|
|
2971
|
+
}
|
|
2972
|
+
if (requestedAdvanced.length > 0) {
|
|
2973
|
+
return this.queryAdvancedCatalog(
|
|
2974
|
+
query,
|
|
2975
|
+
categoryIds,
|
|
2976
|
+
collectionIds,
|
|
2977
|
+
facetSupport
|
|
2978
|
+
);
|
|
2979
|
+
}
|
|
2980
|
+
return await this.queryBasicCatalog(
|
|
2981
|
+
query,
|
|
2982
|
+
effectiveCategoryHandles,
|
|
2983
|
+
categoryIds,
|
|
2984
|
+
collectionIds,
|
|
2985
|
+
facetSupport
|
|
2986
|
+
);
|
|
2987
|
+
} catch (error) {
|
|
2988
|
+
this.logger.error("getCatalogProducts error details", {
|
|
2989
|
+
message: error.message,
|
|
2990
|
+
status: error.status
|
|
2991
|
+
});
|
|
2992
|
+
throw new ProviderAPIError(
|
|
2993
|
+
`Failed to run catalog query: ${error.message || "Unknown error"}`,
|
|
2994
|
+
"medusa",
|
|
2995
|
+
error.status
|
|
2996
|
+
);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
/**
|
|
3000
|
+
* Execute a basic-only canonical query against the equivalent basic endpoint
|
|
3001
|
+
* `/store/products`, reporting the filters it actually applied.
|
|
3002
|
+
*/
|
|
3003
|
+
async queryBasicCatalog(query, effectiveCategoryHandles, categoryIds, collectionIds, facetSupport) {
|
|
3004
|
+
const pageSize = query.pageSize ?? this.catalogPageSize;
|
|
3005
|
+
const page = query.page ?? 1;
|
|
3006
|
+
const regionId = this.regionId;
|
|
3007
|
+
const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+thumbnail,+tags";
|
|
3008
|
+
const pricingFields = ",+variants.calculated_price,+variants.prices";
|
|
3009
|
+
const queryParams = {
|
|
3010
|
+
limit: pageSize,
|
|
3011
|
+
offset: (page - 1) * pageSize,
|
|
3012
|
+
fields: regionId ? baseFields + pricingFields : baseFields
|
|
3013
|
+
};
|
|
3014
|
+
if (query.text) {
|
|
3015
|
+
queryParams.q = query.text;
|
|
3016
|
+
}
|
|
3017
|
+
if (categoryIds && categoryIds.length > 0) {
|
|
3018
|
+
queryParams.category_id = categoryIds;
|
|
3019
|
+
}
|
|
3020
|
+
if (collectionIds && collectionIds.length > 0) {
|
|
3021
|
+
queryParams.collection_id = collectionIds;
|
|
3022
|
+
}
|
|
3023
|
+
if (query.sort) {
|
|
3024
|
+
const mappedOrder = MEDUSA_SORT_KEY_MAP[query.sort] ?? null;
|
|
3025
|
+
if (mappedOrder !== null) {
|
|
3026
|
+
queryParams.order = mappedOrder;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
if (regionId) {
|
|
3030
|
+
queryParams.region_id = regionId;
|
|
3031
|
+
}
|
|
3032
|
+
const salesChannelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
|
|
3033
|
+
if (salesChannelId) {
|
|
3034
|
+
queryParams.sales_channel_id = [salesChannelId];
|
|
3035
|
+
}
|
|
3036
|
+
const response = await this.sdk.store.product.list(queryParams);
|
|
3037
|
+
const products = (response.products ?? []).map(
|
|
3038
|
+
(p) => transformProduct(p, this.currencyCode)
|
|
3039
|
+
);
|
|
3040
|
+
const count = response.count;
|
|
3041
|
+
return {
|
|
3042
|
+
ok: true,
|
|
3043
|
+
results: {
|
|
3044
|
+
products,
|
|
3045
|
+
// Total is only the backend's actual count; never manufactured.
|
|
3046
|
+
...typeof count === "number" ? { total: count } : {},
|
|
3047
|
+
appliedFilters: buildAppliedCatalogFilters(
|
|
3048
|
+
query,
|
|
3049
|
+
effectiveCategoryHandles
|
|
3050
|
+
),
|
|
3051
|
+
facetSupport
|
|
3052
|
+
}
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
/**
|
|
3056
|
+
* Execute an advanced canonical query against the real optional endpoint
|
|
3057
|
+
* `/store/search`, then re-validate that the response applied every requested
|
|
3058
|
+
* filter. A response that does not confirm a requested field, or an endpoint
|
|
3059
|
+
* failure, yields a typed `unsupported-query` error — never a partial result
|
|
3060
|
+
* labelled success and never a silent fallback to the basic endpoint.
|
|
3061
|
+
*/
|
|
3062
|
+
async queryAdvancedCatalog(query, categoryIds, collectionIds, facetSupport) {
|
|
3063
|
+
const pageSize = query.pageSize ?? this.catalogPageSize;
|
|
3064
|
+
const page = query.page ?? 1;
|
|
3065
|
+
const advancedQuery = this.buildAdvancedSearchQuery(
|
|
3066
|
+
query,
|
|
3067
|
+
categoryIds ?? [],
|
|
3068
|
+
collectionIds ?? []
|
|
3069
|
+
);
|
|
3070
|
+
advancedQuery.limit = pageSize;
|
|
3071
|
+
advancedQuery.offset = (page - 1) * pageSize;
|
|
3072
|
+
let data;
|
|
3073
|
+
try {
|
|
3074
|
+
data = await this.sdk.client.fetch(ADVANCED_SEARCH_ENDPOINT, {
|
|
3075
|
+
query: advancedQuery
|
|
3076
|
+
});
|
|
3077
|
+
} catch (error) {
|
|
3078
|
+
this.logger.error("Advanced catalog listing failed", {
|
|
3079
|
+
message: error?.message,
|
|
3080
|
+
status: error?.status
|
|
3081
|
+
});
|
|
3082
|
+
return this.unsupportedCatalogOutcome(
|
|
3083
|
+
collectRequestedAdvancedFields(query)
|
|
3084
|
+
);
|
|
3085
|
+
}
|
|
3086
|
+
const appliedRaw = data?.applied_filters ?? data?.appliedFilters;
|
|
3087
|
+
if (!appliedRaw || typeof appliedRaw !== "object") {
|
|
3088
|
+
this.logger.warn("Advanced endpoint did not echo applied filters", {
|
|
3089
|
+
endpoint: ADVANCED_SEARCH_ENDPOINT
|
|
3090
|
+
});
|
|
3091
|
+
return this.unsupportedCatalogOutcome(
|
|
3092
|
+
collectRequestedAdvancedFields(query)
|
|
3093
|
+
);
|
|
3094
|
+
}
|
|
3095
|
+
const unapplied = collectUnappliedAdvancedFields(
|
|
3096
|
+
appliedRaw,
|
|
3097
|
+
query
|
|
3098
|
+
);
|
|
3099
|
+
if (unapplied.length > 0) {
|
|
3100
|
+
this.logger.warn("Advanced endpoint did not apply requested filters", {
|
|
3101
|
+
endpoint: ADVANCED_SEARCH_ENDPOINT,
|
|
3102
|
+
fields: unapplied
|
|
3103
|
+
});
|
|
3104
|
+
return this.unsupportedCatalogOutcome(unapplied);
|
|
3105
|
+
}
|
|
3106
|
+
const products = (data.hits ?? []).map(
|
|
3107
|
+
(hit) => transformProduct(hit, this.currencyCode)
|
|
3108
|
+
);
|
|
3109
|
+
const total = typeof data.estimatedTotalHits === "number" ? data.estimatedTotalHits : typeof data.count === "number" ? data.count : void 0;
|
|
3110
|
+
return {
|
|
3111
|
+
ok: true,
|
|
3112
|
+
results: {
|
|
3113
|
+
products,
|
|
3114
|
+
...total !== void 0 ? { total } : {},
|
|
3115
|
+
appliedFilters: buildAppliedCatalogFilters(
|
|
3116
|
+
query,
|
|
3117
|
+
resolveEffectiveCategoryHandles(query),
|
|
3118
|
+
true
|
|
3119
|
+
),
|
|
3120
|
+
facetSupport
|
|
3121
|
+
}
|
|
3122
|
+
};
|
|
3123
|
+
}
|
|
3124
|
+
/** Resolve category handles to Medusa category IDs via directed lookups. */
|
|
3125
|
+
async resolveCategoryIds(handles) {
|
|
3126
|
+
const ids = [];
|
|
3127
|
+
for (const handle of handles) {
|
|
3128
|
+
const category = await this.getCategory(handle);
|
|
3129
|
+
if (category) ids.push(category.id);
|
|
3130
|
+
}
|
|
3131
|
+
return ids;
|
|
3132
|
+
}
|
|
3133
|
+
/** Resolve collection handles to Medusa collection IDs via directed lookups. */
|
|
3134
|
+
async resolveCollectionIds(handles) {
|
|
3135
|
+
const ids = [];
|
|
3136
|
+
for (const handle of handles) {
|
|
3137
|
+
const collection = await this.getCollection(handle);
|
|
3138
|
+
if (collection) ids.push(collection.id);
|
|
3139
|
+
}
|
|
3140
|
+
return ids;
|
|
3141
|
+
}
|
|
3142
|
+
/**
|
|
3143
|
+
* Truthful facet-support descriptor for a scope (E2 task 2.4).
|
|
3144
|
+
*
|
|
3145
|
+
* Basic facets (`categories`, `collections`) are always offered: they are
|
|
3146
|
+
* applied by the canonical basic endpoint `/store/products`, and the
|
|
3147
|
+
* descriptor records that endpoint with the applied query and scope as their
|
|
3148
|
+
* demonstration source.
|
|
3149
|
+
*
|
|
3150
|
+
* Optional advanced facets (`price`, `inStock`, and advertised `attr.<name>`)
|
|
3151
|
+
* are offered only when the real optional `/store/search` endpoint has been
|
|
3152
|
+
* demonstrated with the applied query for the current scope AND the response
|
|
3153
|
+
* echoes the filter as applied. Otherwise they are held with the real probe
|
|
3154
|
+
* status/reason — a capability boolean or returned facet metadata is never
|
|
3155
|
+
* accepted as proof (E2 Decisions 4–5; spec "Backend support is proven against
|
|
3156
|
+
* the real endpoint, not assumed").
|
|
3157
|
+
*
|
|
3158
|
+
* The descriptor carries no counts or ranges and no invented timestamp; every
|
|
3159
|
+
* offered facet has an actual `{ endpoint, appliedQuery, scope }` source.
|
|
3160
|
+
*/
|
|
3161
|
+
async getCatalogFacetSupport(options = {}) {
|
|
3162
|
+
const query = options.query ?? {};
|
|
3163
|
+
const scope = options.scope ?? query.categoryScope;
|
|
3164
|
+
const key = JSON.stringify({
|
|
3165
|
+
scope: scope ?? null,
|
|
3166
|
+
categoryIds: options.categoryIds ?? null,
|
|
3167
|
+
collectionIds: options.collectionIds ?? null,
|
|
3168
|
+
categories: query.categories ?? null,
|
|
3169
|
+
text: query.text ?? null,
|
|
3170
|
+
collections: query.collections ?? null,
|
|
3171
|
+
priceMin: query.priceMin ?? null,
|
|
3172
|
+
priceMax: query.priceMax ?? null,
|
|
3173
|
+
inStock: query.inStock ?? null,
|
|
3174
|
+
attributes: query.attributes ?? null
|
|
3175
|
+
});
|
|
3176
|
+
const cached = this.facetSupportCache.get(key);
|
|
3177
|
+
if (cached) return cached;
|
|
3178
|
+
const pending = this.resolveCatalogFacetSupport(
|
|
3179
|
+
query,
|
|
3180
|
+
options.categoryIds,
|
|
3181
|
+
options.collectionIds,
|
|
3182
|
+
scope
|
|
3183
|
+
);
|
|
3184
|
+
this.facetSupportCache.set(key, pending);
|
|
3185
|
+
return pending;
|
|
3186
|
+
}
|
|
3187
|
+
async resolveCatalogFacetSupport(query, providedCategoryIds, providedCollectionIds, scope) {
|
|
3188
|
+
const advancedDemonstratedQuery = {
|
|
3189
|
+
...query.text !== void 0 ? { text: query.text } : {},
|
|
3190
|
+
...query.categoryScope !== void 0 ? { categoryScope: query.categoryScope } : {},
|
|
3191
|
+
...query.categories !== void 0 ? { categories: query.categories } : {},
|
|
3192
|
+
...query.collections !== void 0 ? { collections: query.collections } : {},
|
|
3193
|
+
...query.priceMin !== void 0 ? { priceMin: query.priceMin } : {},
|
|
3194
|
+
...query.priceMax !== void 0 ? { priceMax: query.priceMax } : {},
|
|
3195
|
+
...query.inStock !== void 0 ? { inStock: query.inStock } : {},
|
|
3196
|
+
...query.attributes !== void 0 ? { attributes: query.attributes } : {}
|
|
3197
|
+
};
|
|
3198
|
+
const basicDemonstratedQuery = buildBasicDemonstratedQuery(query);
|
|
3199
|
+
const scopeField = scope !== void 0 ? { scope } : {};
|
|
3200
|
+
const offered = ["categories", "collections"];
|
|
3201
|
+
const demonstrations = {
|
|
3202
|
+
categories: {
|
|
3203
|
+
endpoint: BASIC_LISTING_ENDPOINT,
|
|
3204
|
+
appliedQuery: basicDemonstratedQuery,
|
|
3205
|
+
...scopeField
|
|
3206
|
+
},
|
|
3207
|
+
collections: {
|
|
3208
|
+
endpoint: BASIC_LISTING_ENDPOINT,
|
|
3209
|
+
appliedQuery: basicDemonstratedQuery,
|
|
3210
|
+
...scopeField
|
|
3211
|
+
}
|
|
3212
|
+
};
|
|
3213
|
+
const held = [];
|
|
3214
|
+
const heldReasons = {};
|
|
3215
|
+
const categoryIds = providedCategoryIds ?? await this.resolveScopeCategoryIds(query);
|
|
3216
|
+
const collectionIds = providedCollectionIds ?? (query.collections ? await this.resolveCollectionIds(query.collections) : []);
|
|
3217
|
+
const candidates = ["price", "inStock"];
|
|
3218
|
+
for (const handle of await this.resolveFilterableAttributeHandles(
|
|
3219
|
+
categoryIds
|
|
3220
|
+
)) {
|
|
3221
|
+
const facet = `attr.${handle}`;
|
|
3222
|
+
if (!candidates.includes(facet)) candidates.push(facet);
|
|
3223
|
+
}
|
|
3224
|
+
const probe = await this.probeAdvancedSearchSupport(
|
|
3225
|
+
query,
|
|
3226
|
+
categoryIds,
|
|
3227
|
+
collectionIds
|
|
3228
|
+
);
|
|
3229
|
+
if (probe.available) {
|
|
3230
|
+
for (const facet of candidates) {
|
|
3231
|
+
if (probe.applied.has(facet)) {
|
|
3232
|
+
offered.push(facet);
|
|
3233
|
+
demonstrations[facet] = {
|
|
3234
|
+
endpoint: ADVANCED_SEARCH_ENDPOINT,
|
|
3235
|
+
appliedQuery: advancedDemonstratedQuery,
|
|
3236
|
+
...scopeField
|
|
3237
|
+
};
|
|
3238
|
+
} else {
|
|
3239
|
+
held.push(facet);
|
|
3240
|
+
heldReasons[facet] = `not echoed as applied by ${ADVANCED_SEARCH_ENDPOINT}`;
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
} else {
|
|
3244
|
+
for (const facet of candidates) {
|
|
3245
|
+
held.push(facet);
|
|
3246
|
+
heldReasons[facet] = probe.reason;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
return {
|
|
3250
|
+
offered,
|
|
3251
|
+
held,
|
|
3252
|
+
demonstrations,
|
|
3253
|
+
...Object.keys(heldReasons).length > 0 ? { heldReasons } : {}
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
/**
|
|
3257
|
+
* Demonstrate the optional advanced endpoint with the applied query for the
|
|
3258
|
+
* current scope. A non-2xx response (including the HTTP 404/500 an absent
|
|
3259
|
+
* plugin returns), an unparseable body, or a response that does not echo the
|
|
3260
|
+
* applied filters all mean the facet is not demonstrated — never a silent
|
|
3261
|
+
* assumption of support. Uses a single SDK request (no retry): a support probe
|
|
3262
|
+
* must not amplify a broken optional endpoint into repeated requests.
|
|
3263
|
+
*/
|
|
3264
|
+
async probeAdvancedSearchSupport(query, categoryIds, collectionIds) {
|
|
3265
|
+
const advancedQuery = this.buildAdvancedSearchQuery(
|
|
3266
|
+
query,
|
|
3267
|
+
categoryIds,
|
|
3268
|
+
collectionIds
|
|
3269
|
+
);
|
|
3270
|
+
advancedQuery.limit = 1;
|
|
3271
|
+
try {
|
|
3272
|
+
const data = await this.sdk.client.fetch(ADVANCED_SEARCH_ENDPOINT, {
|
|
3273
|
+
query: advancedQuery
|
|
3274
|
+
});
|
|
3275
|
+
const appliedRaw = data?.applied_filters ?? data?.appliedFilters;
|
|
3276
|
+
if (!appliedRaw || typeof appliedRaw !== "object") {
|
|
3277
|
+
return {
|
|
3278
|
+
available: false,
|
|
3279
|
+
applied: /* @__PURE__ */ new Set(),
|
|
3280
|
+
reason: `${ADVANCED_SEARCH_ENDPOINT} did not echo applied filters; support unverified`
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
return {
|
|
3284
|
+
available: true,
|
|
3285
|
+
applied: mapAppliedFiltersToFacets(
|
|
3286
|
+
appliedRaw
|
|
3287
|
+
),
|
|
3288
|
+
reason: `${ADVANCED_SEARCH_ENDPOINT} demonstrated with the applied query`
|
|
3289
|
+
};
|
|
3290
|
+
} catch (error) {
|
|
3291
|
+
const status = typeof error?.status === "number" ? error.status : void 0;
|
|
3292
|
+
this.logger.warn("Optional advanced search endpoint not demonstrated", {
|
|
3293
|
+
endpoint: ADVANCED_SEARCH_ENDPOINT,
|
|
3294
|
+
status,
|
|
3295
|
+
message: error?.message
|
|
3296
|
+
});
|
|
3297
|
+
return {
|
|
3298
|
+
available: false,
|
|
3299
|
+
applied: /* @__PURE__ */ new Set(),
|
|
3300
|
+
reason: status !== void 0 ? `${ADVANCED_SEARCH_ENDPOINT} returned ${status}; optional advanced endpoint not demonstrated` : `${ADVANCED_SEARCH_ENDPOINT} unavailable: ${extractErrorMessage(error)}`,
|
|
3301
|
+
...status !== void 0 ? { status } : {}
|
|
3302
|
+
};
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
/**
|
|
3306
|
+
* Build the advanced-endpoint query parameters from the canonical query. The
|
|
3307
|
+
* SDK serializes the plain object, so no manual stringification is needed.
|
|
3308
|
+
*/
|
|
3309
|
+
buildAdvancedSearchQuery(query, categoryIds, collectionIds) {
|
|
3310
|
+
const advancedQuery = {};
|
|
3311
|
+
if (query.text) advancedQuery.q = query.text;
|
|
3312
|
+
if (categoryIds.length > 0) advancedQuery.categories = categoryIds.join(",");
|
|
3313
|
+
if (collectionIds.length > 0) {
|
|
3314
|
+
advancedQuery.collections = collectionIds.join(",");
|
|
3315
|
+
}
|
|
3316
|
+
if (query.priceMin !== void 0) advancedQuery.price_min = query.priceMin;
|
|
3317
|
+
if (query.priceMax !== void 0) advancedQuery.price_max = query.priceMax;
|
|
3318
|
+
if (query.inStock === true) advancedQuery.isAvailable = true;
|
|
3319
|
+
if (query.attributes) {
|
|
3320
|
+
const attributes = Object.entries(query.attributes).map(([id, values]) => ({
|
|
3321
|
+
id,
|
|
3322
|
+
values
|
|
3323
|
+
}));
|
|
3324
|
+
advancedQuery.attributes = JSON.stringify(attributes);
|
|
3325
|
+
}
|
|
3326
|
+
if (query.sort) {
|
|
3327
|
+
const mappedOrder = MEDUSA_SORT_KEY_MAP[query.sort] ?? null;
|
|
3328
|
+
if (mappedOrder !== null) advancedQuery.order = mappedOrder;
|
|
3329
|
+
}
|
|
3330
|
+
if (this.regionId) advancedQuery.region_id = this.regionId;
|
|
3331
|
+
if (this.currencyCode) {
|
|
3332
|
+
advancedQuery.currency_code = this.currencyCode.toLowerCase();
|
|
3333
|
+
}
|
|
3334
|
+
return advancedQuery;
|
|
3335
|
+
}
|
|
3336
|
+
/** Resolve the effective category scope of a query to backend IDs. */
|
|
3337
|
+
async resolveScopeCategoryIds(query) {
|
|
3338
|
+
const handles = resolveEffectiveCategoryHandles(query);
|
|
3339
|
+
if (handles === void 0 || handles.length === 0) return [];
|
|
3340
|
+
return this.resolveCategoryIds(handles);
|
|
3341
|
+
}
|
|
3342
|
+
/**
|
|
3343
|
+
* Advertised filterable attribute handles for the scope. The custom-attributes
|
|
3344
|
+
* endpoint is facet *metadata* (which attributes exist), not proof that a
|
|
3345
|
+
* filter can be applied; these handles are candidates only, and each still
|
|
3346
|
+
* requires the advanced endpoint to echo it as applied before it is offered.
|
|
3347
|
+
*/
|
|
3348
|
+
async resolveFilterableAttributeHandles(categoryIds) {
|
|
3349
|
+
const handles = [];
|
|
3350
|
+
for (const categoryId of categoryIds) {
|
|
3351
|
+
try {
|
|
3352
|
+
const data = await this.sdk.client.fetch(
|
|
3353
|
+
`/store/custom-attributes/category/${categoryId}`
|
|
3354
|
+
);
|
|
3355
|
+
for (const item of data?.attributes ?? []) {
|
|
3356
|
+
const attr = item.attribute ?? item;
|
|
3357
|
+
if (attr?.is_filterable && attr?.is_active && typeof attr.handle === "string" && attr.handle.length > 0) {
|
|
3358
|
+
handles.push(attr.handle);
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
} catch {
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
return handles;
|
|
3365
|
+
}
|
|
3366
|
+
/** Build a typed `unsupported-query` outcome naming the offending fields. */
|
|
3367
|
+
unsupportedCatalogOutcome(fields) {
|
|
3368
|
+
return {
|
|
3369
|
+
ok: false,
|
|
3370
|
+
error: {
|
|
3371
|
+
code: "unsupported-query",
|
|
3372
|
+
fields,
|
|
3373
|
+
message: `Unsupported catalog query: ${fields.join(", ")}`,
|
|
3374
|
+
repair: { remove: fields }
|
|
3375
|
+
}
|
|
3376
|
+
};
|
|
3377
|
+
}
|
|
2601
3378
|
async getProductRecommendations(productId) {
|
|
2602
3379
|
this.logger.debug("getProductRecommendations", { productId });
|
|
2603
3380
|
this.logger.warn("Product recommendations not yet implemented for Medusa");
|
|
@@ -2619,16 +3396,8 @@ var MedusaProvider = class {
|
|
|
2619
3396
|
async searchProductsAdvanced(options) {
|
|
2620
3397
|
this.logger.debug("searchProductsAdvanced", options);
|
|
2621
3398
|
if (!this.capabilities.advancedSearch) {
|
|
2622
|
-
this.logger.warn("Advanced search
|
|
2623
|
-
|
|
2624
|
-
return {
|
|
2625
|
-
products,
|
|
2626
|
-
meta: {
|
|
2627
|
-
limit: options?.first ?? 20,
|
|
2628
|
-
offset: Number(options?.after ?? 0),
|
|
2629
|
-
hybridSearchUsed: false
|
|
2630
|
-
}
|
|
2631
|
-
};
|
|
3399
|
+
this.logger.warn("Advanced search capability disabled, using faithful fallback");
|
|
3400
|
+
return this.fallbackAdvancedSearch(options);
|
|
2632
3401
|
}
|
|
2633
3402
|
try {
|
|
2634
3403
|
const queryParams = {};
|
|
@@ -2728,20 +3497,39 @@ var MedusaProvider = class {
|
|
|
2728
3497
|
});
|
|
2729
3498
|
return { products, meta };
|
|
2730
3499
|
} catch (error) {
|
|
2731
|
-
this.logger.error("Advanced search failed,
|
|
3500
|
+
this.logger.error("Advanced search failed, using faithful fallback", {
|
|
2732
3501
|
error: error.message,
|
|
2733
3502
|
stack: error.stack
|
|
2734
3503
|
});
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
3504
|
+
return this.fallbackAdvancedSearch(options);
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
/**
|
|
3508
|
+
* Faithful fallback for `searchProductsAdvanced`. A basic-only request is
|
|
3509
|
+
* served by `getProducts`; a request that depends on the optional advanced
|
|
3510
|
+
* endpoint is rejected as a typed `ProviderAPIError` naming the unappliable
|
|
3511
|
+
* fields, so a failed advanced search never returns an unfiltered result
|
|
3512
|
+
* labelled success (E2 task 2.4).
|
|
3513
|
+
*/
|
|
3514
|
+
async fallbackAdvancedSearch(options) {
|
|
3515
|
+
const requested = collectAdvancedSearchRequestFields(options);
|
|
3516
|
+
if (requested.length > 0) {
|
|
3517
|
+
throw new ProviderAPIError(
|
|
3518
|
+
`Advanced search unavailable for requested fields: ${requested.join(", ")}`,
|
|
3519
|
+
"medusa",
|
|
3520
|
+
void 0,
|
|
3521
|
+
{ fields: requested, repair: { remove: requested } }
|
|
3522
|
+
);
|
|
2744
3523
|
}
|
|
3524
|
+
const products = await this.getProducts(options);
|
|
3525
|
+
return {
|
|
3526
|
+
products,
|
|
3527
|
+
meta: {
|
|
3528
|
+
limit: options?.first ?? 20,
|
|
3529
|
+
offset: Number(options?.after ?? 0),
|
|
3530
|
+
hybridSearchUsed: false
|
|
3531
|
+
}
|
|
3532
|
+
};
|
|
2745
3533
|
}
|
|
2746
3534
|
/**
|
|
2747
3535
|
* Get available filter options for product filtering
|
|
@@ -2796,23 +3584,20 @@ var MedusaProvider = class {
|
|
|
2796
3584
|
});
|
|
2797
3585
|
}
|
|
2798
3586
|
}
|
|
2799
|
-
if (options.useAdvancedSearch
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
type: "range",
|
|
2804
|
-
min: 0,
|
|
2805
|
-
max: 1e4,
|
|
2806
|
-
isActive: false,
|
|
2807
|
-
ranking: 0
|
|
2808
|
-
});
|
|
2809
|
-
filters.push({
|
|
2810
|
-
id: "isAvailable",
|
|
2811
|
-
label: "In Stock",
|
|
2812
|
-
type: "boolean",
|
|
2813
|
-
isActive: false,
|
|
2814
|
-
ranking: 5
|
|
3587
|
+
if (options.useAdvancedSearch) {
|
|
3588
|
+
const support = await this.getCatalogFacetSupport({
|
|
3589
|
+
categoryIds: options.categoryId ? [options.categoryId] : [],
|
|
3590
|
+
scope: options.categoryId
|
|
2815
3591
|
});
|
|
3592
|
+
if (support.offered.includes("inStock")) {
|
|
3593
|
+
filters.push({
|
|
3594
|
+
id: "isAvailable",
|
|
3595
|
+
label: "In Stock",
|
|
3596
|
+
type: "boolean",
|
|
3597
|
+
isActive: false,
|
|
3598
|
+
ranking: 5
|
|
3599
|
+
});
|
|
3600
|
+
}
|
|
2816
3601
|
}
|
|
2817
3602
|
if (options.categoryId && this.capabilities.customAttributes) {
|
|
2818
3603
|
try {
|
|
@@ -2847,19 +3632,27 @@ var MedusaProvider = class {
|
|
|
2847
3632
|
// Custom attributes lower priority
|
|
2848
3633
|
};
|
|
2849
3634
|
if ((filterType === "single" || filterType === "multi") && attr.values) {
|
|
2850
|
-
filter.options = attr.values.map((val) =>
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
3635
|
+
filter.options = attr.values.map((val) => {
|
|
3636
|
+
const count = options.includeFacetCounts ? readFiniteFacetNumber(val.count) : void 0;
|
|
3637
|
+
return {
|
|
3638
|
+
value: this.toSlug(val.value),
|
|
3639
|
+
// URL slug: ?ram=8gb
|
|
3640
|
+
label: val.value,
|
|
3641
|
+
// Display: "8GB"
|
|
3642
|
+
id: val.id,
|
|
3643
|
+
// Backend ID: 01KN03N67V13K5S2E6B6695KRN
|
|
3644
|
+
...count !== void 0 ? { count } : {}
|
|
3645
|
+
};
|
|
3646
|
+
});
|
|
2859
3647
|
}
|
|
2860
|
-
if (filterType === "range"
|
|
2861
|
-
|
|
2862
|
-
|
|
3648
|
+
if (filterType === "range") {
|
|
3649
|
+
const min = readFiniteFacetNumber(attr.metadata?.min);
|
|
3650
|
+
const max = readFiniteFacetNumber(attr.metadata?.max);
|
|
3651
|
+
if (min === void 0 || max === void 0) {
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
3654
|
+
filter.min = min;
|
|
3655
|
+
filter.max = max;
|
|
2863
3656
|
}
|
|
2864
3657
|
filters.push(filter);
|
|
2865
3658
|
});
|
|
@@ -3221,7 +4014,7 @@ var MedusaProvider = class {
|
|
|
3221
4014
|
return customFetcher(options);
|
|
3222
4015
|
}
|
|
3223
4016
|
try {
|
|
3224
|
-
const limit = options.
|
|
4017
|
+
const limit = options.limit ?? this.catalogPageSize;
|
|
3225
4018
|
const queryParams = {
|
|
3226
4019
|
limit,
|
|
3227
4020
|
include_descendants_tree: options.includeDescendants ?? false
|
|
@@ -3261,11 +4054,15 @@ var MedusaProvider = class {
|
|
|
3261
4054
|
}
|
|
3262
4055
|
const categoryQueryParams = {
|
|
3263
4056
|
category_id: [category.id],
|
|
3264
|
-
|
|
3265
|
-
limit: options.first ??
|
|
4057
|
+
// Bounded, paginated listing (change B, decision 5).
|
|
4058
|
+
limit: options.first ?? this.catalogPageSize,
|
|
3266
4059
|
offset: options.after ? parseInt(options.after, 10) : void 0,
|
|
3267
|
-
fields: "+variants,+variants.prices,+variants.
|
|
4060
|
+
fields: "+variants,+variants.prices,+variants.calculated_price,+variants.inventory_quantity,+options,+images,+tags"
|
|
3268
4061
|
};
|
|
4062
|
+
const mappedOrder = options.sortKey ? MEDUSA_SORT_KEY_MAP[options.sortKey] ?? null : null;
|
|
4063
|
+
if (mappedOrder !== null) {
|
|
4064
|
+
categoryQueryParams.order = mappedOrder;
|
|
4065
|
+
}
|
|
3269
4066
|
if (this.regionId) {
|
|
3270
4067
|
categoryQueryParams.region_id = this.regionId;
|
|
3271
4068
|
}
|
|
@@ -3300,7 +4097,7 @@ var MedusaProvider = class {
|
|
|
3300
4097
|
} else {
|
|
3301
4098
|
createPayload.currency_code = this.currencyCode.toLowerCase();
|
|
3302
4099
|
}
|
|
3303
|
-
const salesChannelId = options?.salesChannelId ??
|
|
4100
|
+
const salesChannelId = options?.salesChannelId ?? resolveConfiguredSalesChannelId();
|
|
3304
4101
|
if (salesChannelId) {
|
|
3305
4102
|
createPayload.sales_channel_id = salesChannelId;
|
|
3306
4103
|
}
|
|
@@ -3371,7 +4168,7 @@ var MedusaProvider = class {
|
|
|
3371
4168
|
fields: "+variants,+variants.calculated_price,+variants.prices,+variants.money_amounts",
|
|
3372
4169
|
region_id: backendCart.region_id
|
|
3373
4170
|
};
|
|
3374
|
-
const salesChannelId =
|
|
4171
|
+
const salesChannelId = resolveConfiguredSalesChannelId();
|
|
3375
4172
|
if (salesChannelId) {
|
|
3376
4173
|
variantCheckParams.sales_channel_id = [salesChannelId];
|
|
3377
4174
|
}
|
|
@@ -3543,13 +4340,17 @@ var MedusaProvider = class {
|
|
|
3543
4340
|
// Order Operations
|
|
3544
4341
|
// ============================================
|
|
3545
4342
|
/**
|
|
3546
|
-
* Get all orders for a customer (storefront).
|
|
4343
|
+
* Get all orders for a customer (storefront) as an array (legacy surface).
|
|
3547
4344
|
*
|
|
3548
|
-
*
|
|
3549
|
-
*
|
|
4345
|
+
* Delegates to the channel-scoped paginated contract
|
|
4346
|
+
* ({@link customerOrdersPage}) and returns that window's orders. The backend
|
|
4347
|
+
* (`GET /store/thor/orders`) scopes by BOTH the authenticated customer and
|
|
4348
|
+
* the sales channel resolved from the publishable key, so cross-channel
|
|
4349
|
+
* orders are excluded by the backend before pagination and **no client-side
|
|
4350
|
+
* channel filter runs**.
|
|
3550
4351
|
*
|
|
3551
|
-
* @param options - Order query options (supports
|
|
3552
|
-
* @returns Array of orders for the authenticated customer
|
|
4352
|
+
* @param options - Order query options (supports a custom fetcher)
|
|
4353
|
+
* @returns Array of orders for the authenticated customer on the store channel
|
|
3553
4354
|
*
|
|
3554
4355
|
* @example
|
|
3555
4356
|
* ```typescript
|
|
@@ -3563,32 +4364,68 @@ var MedusaProvider = class {
|
|
|
3563
4364
|
this.logger.debug("Using custom fetcher for orders");
|
|
3564
4365
|
return customFetcher(options);
|
|
3565
4366
|
}
|
|
3566
|
-
|
|
4367
|
+
const page = await this.customerOrdersPage({
|
|
4368
|
+
page: 1,
|
|
4369
|
+
pageSize: options.limit ?? CUSTOMER_ORDERS_MAX_PAGE_SIZE,
|
|
4370
|
+
salesChannelId
|
|
4371
|
+
});
|
|
4372
|
+
return page.orders;
|
|
4373
|
+
}
|
|
4374
|
+
/**
|
|
4375
|
+
* Channel-scoped, paginated order history for the authenticated customer.
|
|
4376
|
+
*
|
|
4377
|
+
* Calls the backend route `GET /store/thor/orders`, which requires the
|
|
4378
|
+
* customer session and scopes `order` by `customer_id` **and** the
|
|
4379
|
+
* `sales_channel_id` resolved from the publishable key, returning
|
|
4380
|
+
* `{ orders, count, limit, offset }` with the backend's real total computed
|
|
4381
|
+
* before pagination.
|
|
4382
|
+
*
|
|
4383
|
+
* The `salesChannelId` option is the store's explicitly configured channel.
|
|
4384
|
+
* When supplied, every returned order is asserted to belong to it; a
|
|
4385
|
+
* foreign-channel order (for example from a multi-channel publishable key that
|
|
4386
|
+
* silently resolved an arbitrary first id) fails closed with a typed error
|
|
4387
|
+
* rather than being presented as the customer's history.
|
|
4388
|
+
*
|
|
4389
|
+
* @param options - Bounded pagination window and the store's sales channel
|
|
4390
|
+
* @returns The backend's `{ orders, count, limit, offset }`
|
|
4391
|
+
*/
|
|
4392
|
+
async customerOrdersPage(options = {}) {
|
|
4393
|
+
const { page, pageSize, limit, offset } = normalizeCustomerOrdersPage(options);
|
|
4394
|
+
const { salesChannelId } = options;
|
|
4395
|
+
this.logger.info("Getting channel-scoped customer orders", { page, pageSize });
|
|
3567
4396
|
try {
|
|
3568
|
-
const
|
|
3569
|
-
async () => this.sdk.
|
|
3570
|
-
|
|
3571
|
-
|
|
4397
|
+
const response = await this.withRetry(
|
|
4398
|
+
async () => this.sdk.client.fetch("/store/thor/orders", {
|
|
4399
|
+
method: "GET",
|
|
4400
|
+
query: { limit, offset }
|
|
3572
4401
|
}),
|
|
3573
|
-
"
|
|
4402
|
+
"customerOrdersPage",
|
|
3574
4403
|
{}
|
|
3575
4404
|
);
|
|
3576
|
-
const
|
|
3577
|
-
this.logger.debug(`Found ${
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
const scoped = filterByChannel(transformed, channelId);
|
|
3582
|
-
this.logger.debug(
|
|
3583
|
-
`Channel scoping: ${scoped.length}/${transformed.length} orders match channel ${channelId}`
|
|
4405
|
+
const rawOrders = response.orders ?? [];
|
|
4406
|
+
this.logger.debug(`Found ${rawOrders.length} channel-scoped orders for customer`);
|
|
4407
|
+
if (salesChannelId) {
|
|
4408
|
+
const foreign = rawOrders.find(
|
|
4409
|
+
(order) => typeof order?.sales_channel_id === "string" && order.sales_channel_id !== salesChannelId
|
|
3584
4410
|
);
|
|
3585
|
-
|
|
4411
|
+
if (foreign) {
|
|
4412
|
+
throw new ProviderAPIError(
|
|
4413
|
+
`Channel-scoped order history returned an order outside the store's sales channel (${salesChannelId}).`,
|
|
4414
|
+
"medusa"
|
|
4415
|
+
);
|
|
4416
|
+
}
|
|
3586
4417
|
}
|
|
3587
|
-
return
|
|
4418
|
+
return {
|
|
4419
|
+
orders: rawOrders.map((order) => transformOrder(order)),
|
|
4420
|
+
count: typeof response.count === "number" ? response.count : rawOrders.length,
|
|
4421
|
+
limit: typeof response.limit === "number" ? response.limit : limit,
|
|
4422
|
+
offset: typeof response.offset === "number" ? response.offset : offset
|
|
4423
|
+
};
|
|
3588
4424
|
} catch (error) {
|
|
4425
|
+
if (error instanceof ProviderAPIError) throw error;
|
|
3589
4426
|
const status = error?.status || error?.statusCode;
|
|
3590
4427
|
const errorMessage = extractErrorMessage(error);
|
|
3591
|
-
this.logger.error("Error getting orders", { error: errorMessage });
|
|
4428
|
+
this.logger.error("Error getting channel-scoped orders", { error: errorMessage });
|
|
3592
4429
|
throw new ProviderAPIError(
|
|
3593
4430
|
`Failed to fetch orders: ${errorMessage}`,
|
|
3594
4431
|
"medusa",
|
|
@@ -4487,32 +5324,85 @@ var MedusaProvider = class {
|
|
|
4487
5324
|
* @throws {ProviderAPIError} If checkout fails
|
|
4488
5325
|
*/
|
|
4489
5326
|
async completeCheckout(cartId) {
|
|
4490
|
-
this.
|
|
4491
|
-
|
|
4492
|
-
return
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
orderId: order.id,
|
|
4500
|
-
displayId: order.display_id
|
|
4501
|
-
});
|
|
4502
|
-
return transformOrder(order);
|
|
4503
|
-
},
|
|
4504
|
-
"completeCheckout",
|
|
4505
|
-
{ cartId }
|
|
5327
|
+
const result = await this.completeCart(cartId);
|
|
5328
|
+
if (result.type === "order") {
|
|
5329
|
+
return result.order;
|
|
5330
|
+
}
|
|
5331
|
+
if (result.type === "cart") {
|
|
5332
|
+
throw new ProviderAPIError(
|
|
5333
|
+
`Failed to complete checkout: ${result.error.message}`,
|
|
5334
|
+
"medusa",
|
|
5335
|
+
result.error.status
|
|
4506
5336
|
);
|
|
5337
|
+
}
|
|
5338
|
+
throw new ProviderAPIError(
|
|
5339
|
+
`Checkout outcome is unresolved: ${result.error.message}`,
|
|
5340
|
+
"medusa",
|
|
5341
|
+
result.error.status
|
|
5342
|
+
);
|
|
5343
|
+
}
|
|
5344
|
+
/**
|
|
5345
|
+
* Complete a cart exactly once and preserve the native discriminator
|
|
5346
|
+
* (change B, decision 11).
|
|
5347
|
+
*
|
|
5348
|
+
* Medusa's `POST /store/carts/:id/complete` returns HTTP 200 whose body is
|
|
5349
|
+
* either `{ type: 'order', order }` or `{ type: 'cart', cart, error }`. This
|
|
5350
|
+
* method never retries: a thrown transport/timeout/5xx failure becomes an
|
|
5351
|
+
* `unresolved` result so the caller reconciles the committed cart before any
|
|
5352
|
+
* further attempt. The Medusa JS SDK performs no implicit retry of its own,
|
|
5353
|
+
* so there is no blind retry beneath this call either.
|
|
5354
|
+
*/
|
|
5355
|
+
async completeCart(cartId) {
|
|
5356
|
+
this.logger.debug("completeCart", { cartId });
|
|
5357
|
+
try {
|
|
5358
|
+
const response = await this.sdk.store.cart.complete(cartId, {
|
|
5359
|
+
fields: MEDUSA_ORDER_COMPLETION_FIELDS
|
|
5360
|
+
});
|
|
5361
|
+
if (response?.type === "cart" || response?.cart && response?.error) {
|
|
5362
|
+
const error = this.toCompletionError(response.error);
|
|
5363
|
+
this.logger.warn("Cart completion returned a cart with an error", {
|
|
5364
|
+
cartId,
|
|
5365
|
+
code: error.code,
|
|
5366
|
+
message: error.message
|
|
5367
|
+
});
|
|
5368
|
+
return { type: "cart", cart: transformCart(response.cart), error };
|
|
5369
|
+
}
|
|
5370
|
+
const order = response?.order;
|
|
5371
|
+
if (!order) {
|
|
5372
|
+
return {
|
|
5373
|
+
type: "unresolved",
|
|
5374
|
+
error: { message: "Checkout returned neither an order nor a cart error" }
|
|
5375
|
+
};
|
|
5376
|
+
}
|
|
5377
|
+
this.logger.info("Checkout completed successfully", {
|
|
5378
|
+
orderId: order.id,
|
|
5379
|
+
displayId: order.display_id
|
|
5380
|
+
});
|
|
5381
|
+
return { type: "order", order: transformOrder(order) };
|
|
4507
5382
|
} catch (error) {
|
|
4508
|
-
this.
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
);
|
|
5383
|
+
const completionError = this.toCompletionError(error);
|
|
5384
|
+
this.logger.error("Cart completion failed (unresolved, not retried)", {
|
|
5385
|
+
cartId,
|
|
5386
|
+
status: completionError.status,
|
|
5387
|
+
message: completionError.message
|
|
5388
|
+
});
|
|
5389
|
+
return { type: "unresolved", error: completionError };
|
|
4514
5390
|
}
|
|
4515
5391
|
}
|
|
5392
|
+
/** Normalize any thrown/returned completion failure into a structured error. */
|
|
5393
|
+
toCompletionError(error) {
|
|
5394
|
+
if (error && typeof error === "object") {
|
|
5395
|
+
const err = error;
|
|
5396
|
+
const message = typeof err.message === "string" && err.message.length > 0 ? err.message : extractErrorMessage(error);
|
|
5397
|
+
return {
|
|
5398
|
+
message,
|
|
5399
|
+
code: typeof err.code === "string" ? err.code : void 0,
|
|
5400
|
+
status: err.status ?? err.statusCode,
|
|
5401
|
+
raw: err
|
|
5402
|
+
};
|
|
5403
|
+
}
|
|
5404
|
+
return { message: extractErrorMessage(error), raw: error };
|
|
5405
|
+
}
|
|
4516
5406
|
/**
|
|
4517
5407
|
* Get available shipping methods for cart
|
|
4518
5408
|
*
|
|
@@ -4523,130 +5413,66 @@ var MedusaProvider = class {
|
|
|
4523
5413
|
* @throws {ProviderAPIError} If cart not found or region not configured
|
|
4524
5414
|
*/
|
|
4525
5415
|
async getShippingMethods(cartId) {
|
|
4526
|
-
this.logger.
|
|
5416
|
+
this.logger.debug("getShippingMethods", { cartId });
|
|
4527
5417
|
try {
|
|
4528
5418
|
return await this.withRetry(
|
|
4529
5419
|
async () => {
|
|
4530
|
-
this.logger.info("\u{1F69A} [DEBUG] Step 1: Retrieving cart from Medusa...", { cartId });
|
|
4531
5420
|
const cartResponse = await this.sdk.store.cart.retrieve(cartId);
|
|
4532
5421
|
const { cart } = cartResponse;
|
|
4533
|
-
this.logger.info("\u{1F69A} [DEBUG] Step 2: Cart retrieved from backend", {
|
|
4534
|
-
cartId,
|
|
4535
|
-
hasCart: !!cart,
|
|
4536
|
-
cartKeys: cart ? Object.keys(cart) : [],
|
|
4537
|
-
regionId: cart?.region_id,
|
|
4538
|
-
currencyCode: cart?.currency_code,
|
|
4539
|
-
shippingAddress: cart?.shipping_address ? {
|
|
4540
|
-
country: cart.shipping_address.country_code,
|
|
4541
|
-
postal: cart.shipping_address.postal_code
|
|
4542
|
-
} : null
|
|
4543
|
-
});
|
|
4544
5422
|
if (!cart) {
|
|
4545
|
-
this.logger.error("\u{1F69A} [DEBUG] ERROR: Cart not found in response", {
|
|
4546
|
-
cartId,
|
|
4547
|
-
cartResponse
|
|
4548
|
-
});
|
|
4549
5423
|
throw new ResourceNotFoundError("Cart", cartId);
|
|
4550
5424
|
}
|
|
4551
5425
|
if (!cart.region_id) {
|
|
4552
|
-
this.logger.error("\u{1F69A} [DEBUG] ERROR: Cart has no region_id", {
|
|
4553
|
-
cartId,
|
|
4554
|
-
cart: {
|
|
4555
|
-
id: cart.id,
|
|
4556
|
-
region_id: cart.region_id,
|
|
4557
|
-
region: cart.region,
|
|
4558
|
-
allKeys: Object.keys(cart)
|
|
4559
|
-
}
|
|
4560
|
-
});
|
|
4561
5426
|
throw new ProviderAPIError("Cart has no region assigned", "medusa", 400);
|
|
4562
5427
|
}
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
const
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
});
|
|
4577
|
-
} else {
|
|
4578
|
-
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
4579
|
-
const url = new URL("store/shipping-options", baseUrl);
|
|
4580
|
-
url.searchParams.set("cart_id", cartId);
|
|
4581
|
-
const response = await fetchWithRetry(
|
|
4582
|
-
url.toString(),
|
|
4583
|
-
{
|
|
4584
|
-
method: "GET",
|
|
4585
|
-
headers: {
|
|
4586
|
-
"x-publishable-api-key": this.publishableKey,
|
|
4587
|
-
"content-type": "application/json"
|
|
4588
|
-
}
|
|
4589
|
-
},
|
|
4590
|
-
this.retryConfig
|
|
4591
|
-
);
|
|
4592
|
-
shippingResponse = await response.json();
|
|
4593
|
-
}
|
|
4594
|
-
shipping_options = shippingResponse.shipping_options;
|
|
4595
|
-
} catch (sdkError) {
|
|
4596
|
-
this.logger.error(
|
|
4597
|
-
"\u{1F69A} [DEBUG] ERROR: Medusa SDK threw error when fetching shipping options",
|
|
5428
|
+
let shippingOptions = [];
|
|
5429
|
+
const storeAny = this.sdk.store;
|
|
5430
|
+
if (storeAny?.fulfillment?.listCartOptions) {
|
|
5431
|
+
const response = await storeAny.fulfillment.listCartOptions({
|
|
5432
|
+
cart_id: cartId
|
|
5433
|
+
});
|
|
5434
|
+
shippingOptions = response?.shipping_options ?? [];
|
|
5435
|
+
} else {
|
|
5436
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
5437
|
+
const url = new URL("store/shipping-options", baseUrl);
|
|
5438
|
+
url.searchParams.set("cart_id", cartId);
|
|
5439
|
+
const response = await fetchWithRetry(
|
|
5440
|
+
url.toString(),
|
|
4598
5441
|
{
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
errorBody: sdkError?.body ? JSON.stringify(sdkError.body, null, 2) : "N/A",
|
|
4607
|
-
fullError: JSON.stringify(sdkError, Object.getOwnPropertyNames(sdkError), 2)
|
|
4608
|
-
}
|
|
5442
|
+
method: "GET",
|
|
5443
|
+
headers: {
|
|
5444
|
+
"x-publishable-api-key": this.publishableKey,
|
|
5445
|
+
"content-type": "application/json"
|
|
5446
|
+
}
|
|
5447
|
+
},
|
|
5448
|
+
this.retryConfig
|
|
4609
5449
|
);
|
|
4610
|
-
|
|
5450
|
+
shippingOptions = (await response.json())?.shipping_options ?? [];
|
|
4611
5451
|
}
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
id:
|
|
4618
|
-
name:
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
name: option.name,
|
|
4628
|
-
price: {
|
|
4629
|
-
amount: option.price_incl_tax?.toString() || option.amount?.toString() || "0",
|
|
4630
|
-
currencyCode: cart.currency_code?.toUpperCase() || this.currencyCode
|
|
4631
|
-
},
|
|
4632
|
-
description: option.data?.description,
|
|
4633
|
-
estimatedDays: option.data?.estimated_days || "N/A"
|
|
4634
|
-
}));
|
|
4635
|
-
this.logger.info("\u{1F69A} [DEBUG] Step 5: Transformed methods for Thor types", {
|
|
4636
|
-
cartId,
|
|
4637
|
-
count: methods.length,
|
|
4638
|
-
methods: JSON.stringify(methods, null, 2)
|
|
5452
|
+
const currencyCode = cart.currency_code?.toUpperCase() ?? "";
|
|
5453
|
+
return shippingOptions.map((option) => {
|
|
5454
|
+
const amount = option?.amount ?? option?.price_incl_tax;
|
|
5455
|
+
const estimatedDays = option?.data?.estimated_days != null ? String(option.data.estimated_days) : void 0;
|
|
5456
|
+
return {
|
|
5457
|
+
id: option.id,
|
|
5458
|
+
name: option.name,
|
|
5459
|
+
price: {
|
|
5460
|
+
amount: typeof amount === "number" && Number.isFinite(amount) ? amount.toFixed(2) : "",
|
|
5461
|
+
currencyCode
|
|
5462
|
+
},
|
|
5463
|
+
description: option?.data?.description,
|
|
5464
|
+
estimatedDays,
|
|
5465
|
+
fulfillmentSetType: option?.service_zone?.fulfillment_set?.type ?? null
|
|
5466
|
+
};
|
|
4639
5467
|
});
|
|
4640
|
-
return methods;
|
|
4641
5468
|
},
|
|
4642
5469
|
"getShippingMethods",
|
|
4643
5470
|
{ cartId }
|
|
4644
5471
|
);
|
|
4645
5472
|
} catch (error) {
|
|
4646
|
-
this.logger.error("
|
|
5473
|
+
this.logger.error("Failed to fetch shipping methods", {
|
|
4647
5474
|
cartId,
|
|
4648
5475
|
errorMessage: error.message,
|
|
4649
|
-
errorStack: error.stack,
|
|
4650
5476
|
errorStatus: error.status
|
|
4651
5477
|
});
|
|
4652
5478
|
throw new ProviderAPIError(
|
|
@@ -5007,19 +5833,12 @@ var MedusaProvider = class {
|
|
|
5007
5833
|
async addShippingMethod(cartId, methodId) {
|
|
5008
5834
|
this.logger.debug("addShippingMethod", { cartId, methodId });
|
|
5009
5835
|
try {
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
void cart;
|
|
5017
|
-
const updatedCart = await this.retrieveCartWithLineDetails(cartId);
|
|
5018
|
-
return transformCart(updatedCart);
|
|
5019
|
-
},
|
|
5020
|
-
"addShippingMethod",
|
|
5021
|
-
{ cartId, methodId }
|
|
5022
|
-
);
|
|
5836
|
+
await this.sdk.store.cart.addShippingMethod(cartId, {
|
|
5837
|
+
option_id: methodId
|
|
5838
|
+
});
|
|
5839
|
+
this.logger.info("Shipping method added successfully", { cartId, methodId });
|
|
5840
|
+
const updatedCart = await this.retrieveCartWithLineDetails(cartId);
|
|
5841
|
+
return transformCart(updatedCart);
|
|
5023
5842
|
} catch (error) {
|
|
5024
5843
|
this.logger.error("Failed to add shipping method", {
|
|
5025
5844
|
cartId,
|
|
@@ -5043,166 +5862,75 @@ var MedusaProvider = class {
|
|
|
5043
5862
|
* @throws {ProviderAPIError} If provider invalid or cart not ready
|
|
5044
5863
|
*/
|
|
5045
5864
|
async initiatePaymentSession(cartId, providerId) {
|
|
5046
|
-
this.logger.
|
|
5865
|
+
this.logger.debug("initiatePaymentSession", { cartId, providerId });
|
|
5047
5866
|
try {
|
|
5048
|
-
await this.
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
"\u{1F4B3} [DEBUG] WARNING: Billing address not set, payment provider may require it",
|
|
5111
|
-
{
|
|
5112
|
-
cartId: rawCart.id,
|
|
5113
|
-
shippingAddress: rawCart.shipping_address
|
|
5114
|
-
}
|
|
5115
|
-
);
|
|
5116
|
-
}
|
|
5117
|
-
const existingSessions = rawCart.payment_collection?.payment_sessions || [];
|
|
5118
|
-
if (existingSessions.length > 0) {
|
|
5119
|
-
this.logger.info("\u{1F4B3} [DEBUG] Payment session already exists, skipping creation", {
|
|
5120
|
-
cartId: rawCart.id,
|
|
5121
|
-
sessionCount: existingSessions.length,
|
|
5122
|
-
sessionIds: existingSessions.map((s) => s.id)
|
|
5123
|
-
});
|
|
5124
|
-
return;
|
|
5125
|
-
}
|
|
5126
|
-
this.logger.info("\u{1F4B3} [DEBUG] Step 2.5: No existing payment sessions, will create new one", {
|
|
5127
|
-
cartId: rawCart.id,
|
|
5128
|
-
hasPaymentCollection: !!rawCart.payment_collection,
|
|
5129
|
-
providerId
|
|
5130
|
-
});
|
|
5131
|
-
this.logger.info("\u{1F4B3} [DEBUG] Step 3: Calling Medusa SDK initiatePaymentSession...", {
|
|
5132
|
-
cartId,
|
|
5133
|
-
providerId,
|
|
5134
|
-
endpoint: `${this.baseUrl}store/payment-collections`,
|
|
5135
|
-
note: "About to call SDK method"
|
|
5136
|
-
});
|
|
5137
|
-
this.logger.info("\u{1F4B3} [DEBUG] Step 2.5: Validating provider ID...", {
|
|
5138
|
-
providerId
|
|
5139
|
-
});
|
|
5140
|
-
try {
|
|
5141
|
-
validatePaymentId(providerId);
|
|
5142
|
-
this.logger.info("\u{1F4B3} [DEBUG] Provider ID validation passed", { providerId });
|
|
5143
|
-
} catch (validationError) {
|
|
5144
|
-
this.logger.error("\u{1F4B3} [DEBUG] ERROR: Invalid provider ID detected", {
|
|
5145
|
-
providerId,
|
|
5146
|
-
error: validationError.message,
|
|
5147
|
-
hint: "Use getBackendProviderId(paymentMethod) to extract correct ID"
|
|
5148
|
-
});
|
|
5149
|
-
throw new ProviderAPIError(
|
|
5150
|
-
validationError.message,
|
|
5151
|
-
"medusa",
|
|
5152
|
-
400
|
|
5153
|
-
);
|
|
5154
|
-
}
|
|
5155
|
-
let paymentResponse;
|
|
5156
|
-
try {
|
|
5157
|
-
paymentResponse = await this.sdk.store.payment.initiatePaymentSession(rawCart, {
|
|
5158
|
-
provider_id: providerId
|
|
5159
|
-
});
|
|
5160
|
-
this.logger.info("\u{1F4B3} [DEBUG] Step 4: Payment session response received", {
|
|
5161
|
-
cartId,
|
|
5162
|
-
providerId,
|
|
5163
|
-
responseKeys: paymentResponse ? Object.keys(paymentResponse) : [],
|
|
5164
|
-
response: JSON.stringify(paymentResponse, null, 2)
|
|
5165
|
-
});
|
|
5166
|
-
} catch (sdkError) {
|
|
5167
|
-
this.logger.error(
|
|
5168
|
-
"\u{1F4B3} [DEBUG] ERROR: Medusa SDK threw error when initiating payment session",
|
|
5169
|
-
{
|
|
5170
|
-
cartId,
|
|
5171
|
-
providerId,
|
|
5172
|
-
errorType: sdkError?.constructor?.name,
|
|
5173
|
-
errorMessage: sdkError?.message,
|
|
5174
|
-
errorStatus: sdkError?.status,
|
|
5175
|
-
errorCode: sdkError?.code,
|
|
5176
|
-
errorResponse: sdkError?.response ? JSON.stringify(sdkError.response, null, 2) : "N/A",
|
|
5177
|
-
errorBody: sdkError?.body ? JSON.stringify(sdkError.body, null, 2) : "N/A",
|
|
5178
|
-
fullError: JSON.stringify(sdkError, Object.getOwnPropertyNames(sdkError), 2),
|
|
5179
|
-
// Include cart state in error for debugging
|
|
5180
|
-
cartState: {
|
|
5181
|
-
hasShippingMethod: !!rawCart.shipping_methods?.length,
|
|
5182
|
-
hasShippingAddress: !!rawCart.shipping_address,
|
|
5183
|
-
hasBillingAddress: !!rawCart.billing_address,
|
|
5184
|
-
hasEmail: !!rawCart.email,
|
|
5185
|
-
total: rawCart.total,
|
|
5186
|
-
itemCount: rawCart.items?.length || 0
|
|
5187
|
-
}
|
|
5188
|
-
}
|
|
5189
|
-
);
|
|
5190
|
-
throw sdkError;
|
|
5191
|
-
}
|
|
5192
|
-
this.logger.info("\u{1F4B3} [DEBUG] Payment session initiated successfully", {
|
|
5193
|
-
cartId,
|
|
5194
|
-
providerId
|
|
5195
|
-
});
|
|
5196
|
-
},
|
|
5197
|
-
"initiatePaymentSession",
|
|
5198
|
-
{ cartId, providerId }
|
|
5199
|
-
);
|
|
5867
|
+
const { cart: rawCart } = await this.sdk.store.cart.retrieve(cartId, {
|
|
5868
|
+
fields: "+shipping_methods,+shipping_address,+email,+region_id,+total,+items,+payment_collection,+payment_collection.payment_sessions,+billing_address"
|
|
5869
|
+
});
|
|
5870
|
+
if (!rawCart.items || rawCart.items.length === 0) {
|
|
5871
|
+
throw new ProviderAPIError(
|
|
5872
|
+
"Cannot create payment session: cart is empty. Please add items to your cart before checking out.",
|
|
5873
|
+
"medusa",
|
|
5874
|
+
400
|
|
5875
|
+
);
|
|
5876
|
+
}
|
|
5877
|
+
if (!rawCart.shipping_address) {
|
|
5878
|
+
throw new ProviderAPIError(
|
|
5879
|
+
"Shipping address is required before initiating payment session",
|
|
5880
|
+
"medusa",
|
|
5881
|
+
400
|
|
5882
|
+
);
|
|
5883
|
+
}
|
|
5884
|
+
if (!rawCart.shipping_methods?.length) {
|
|
5885
|
+
throw new ProviderAPIError(
|
|
5886
|
+
"Shipping method is required before initiating payment session",
|
|
5887
|
+
"medusa",
|
|
5888
|
+
400
|
|
5889
|
+
);
|
|
5890
|
+
}
|
|
5891
|
+
if (!rawCart.email) {
|
|
5892
|
+
throw new ProviderAPIError(
|
|
5893
|
+
"Email is required before initiating payment session",
|
|
5894
|
+
"medusa",
|
|
5895
|
+
400
|
|
5896
|
+
);
|
|
5897
|
+
}
|
|
5898
|
+
validatePaymentId(providerId);
|
|
5899
|
+
const existingSessions = rawCart.payment_collection?.payment_sessions ?? [];
|
|
5900
|
+
const compatible = existingSessions.find((session) => {
|
|
5901
|
+
const statusOk = ["pending", "requires_more", "authorized"].includes(session?.status);
|
|
5902
|
+
return session?.provider_id === providerId && session?.amount === rawCart.total && statusOk;
|
|
5903
|
+
});
|
|
5904
|
+
if (compatible) {
|
|
5905
|
+
this.logger.info("Reusing compatible payment session", {
|
|
5906
|
+
cartId,
|
|
5907
|
+
providerId,
|
|
5908
|
+
sessionId: compatible.id,
|
|
5909
|
+
amount: compatible.amount
|
|
5910
|
+
});
|
|
5911
|
+
return;
|
|
5912
|
+
}
|
|
5913
|
+
if (existingSessions.length > 0) {
|
|
5914
|
+
this.logger.info("Existing payment session incompatible; replacing it", {
|
|
5915
|
+
cartId,
|
|
5916
|
+
providerId,
|
|
5917
|
+
existing: existingSessions.map((session) => ({
|
|
5918
|
+
id: session.id,
|
|
5919
|
+
providerId: session.provider_id,
|
|
5920
|
+
amount: session.amount,
|
|
5921
|
+
status: session.status
|
|
5922
|
+
}))
|
|
5923
|
+
});
|
|
5924
|
+
}
|
|
5925
|
+
await this.sdk.store.payment.initiatePaymentSession(rawCart, {
|
|
5926
|
+
provider_id: providerId
|
|
5927
|
+
});
|
|
5928
|
+
this.logger.info("Payment session initiated", { cartId, providerId });
|
|
5200
5929
|
} catch (error) {
|
|
5201
|
-
this.logger.error("
|
|
5930
|
+
this.logger.error("Failed to initiate payment session", {
|
|
5202
5931
|
cartId,
|
|
5203
5932
|
providerId,
|
|
5204
5933
|
errorMessage: error.message,
|
|
5205
|
-
errorStack: error.stack,
|
|
5206
5934
|
errorStatus: error.status
|
|
5207
5935
|
});
|
|
5208
5936
|
throw new ProviderAPIError(
|
|
@@ -5225,31 +5953,23 @@ var MedusaProvider = class {
|
|
|
5225
5953
|
async applyDiscountCode(cartId, code) {
|
|
5226
5954
|
this.logger.debug("applyDiscountCode", { cartId, code });
|
|
5227
5955
|
try {
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
"Content-Type": "application/json",
|
|
5234
|
-
"x-publishable-api-key": this.publishableKey || ""
|
|
5235
|
-
},
|
|
5236
|
-
body: JSON.stringify({
|
|
5237
|
-
promo_codes: [code]
|
|
5238
|
-
})
|
|
5239
|
-
});
|
|
5240
|
-
if (!response.ok) {
|
|
5241
|
-
const error = await response.json();
|
|
5242
|
-
throw new Error(error.message || "Failed to apply discount code");
|
|
5243
|
-
}
|
|
5244
|
-
const { cart: medusaCart } = await response.json();
|
|
5245
|
-
this.logger.info("Discount code applied successfully", { cartId, code });
|
|
5246
|
-
void medusaCart;
|
|
5247
|
-
const updatedCart = await this.retrieveCartWithLineDetails(cartId);
|
|
5248
|
-
return transformCart(updatedCart);
|
|
5956
|
+
const response = await fetch(`${this.baseUrl}/store/carts/${cartId}/promotions`, {
|
|
5957
|
+
method: "POST",
|
|
5958
|
+
headers: {
|
|
5959
|
+
"Content-Type": "application/json",
|
|
5960
|
+
"x-publishable-api-key": this.publishableKey || ""
|
|
5249
5961
|
},
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5962
|
+
body: JSON.stringify({
|
|
5963
|
+
promo_codes: [code]
|
|
5964
|
+
})
|
|
5965
|
+
});
|
|
5966
|
+
if (!response.ok) {
|
|
5967
|
+
const error = await response.json();
|
|
5968
|
+
throw new Error(error.message || "Failed to apply discount code");
|
|
5969
|
+
}
|
|
5970
|
+
this.logger.info("Discount code applied successfully", { cartId, code });
|
|
5971
|
+
const updatedCart = await this.retrieveCartWithLineDetails(cartId);
|
|
5972
|
+
return transformCart(updatedCart);
|
|
5253
5973
|
} catch (error) {
|
|
5254
5974
|
this.logger.error("Failed to apply discount code", { cartId, code, error: error.message });
|
|
5255
5975
|
throw new ProviderAPIError(
|
|
@@ -5272,31 +5992,23 @@ var MedusaProvider = class {
|
|
|
5272
5992
|
async removeDiscountCode(cartId, code) {
|
|
5273
5993
|
this.logger.debug("removeDiscountCode", { cartId, code });
|
|
5274
5994
|
try {
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
"Content-Type": "application/json",
|
|
5281
|
-
"x-publishable-api-key": this.publishableKey || ""
|
|
5282
|
-
},
|
|
5283
|
-
body: JSON.stringify({
|
|
5284
|
-
promo_codes: [code]
|
|
5285
|
-
})
|
|
5286
|
-
});
|
|
5287
|
-
if (!response.ok) {
|
|
5288
|
-
const error = await response.json();
|
|
5289
|
-
throw new Error(error.message || "Failed to remove discount code");
|
|
5290
|
-
}
|
|
5291
|
-
const { cart: medusaCart } = await response.json();
|
|
5292
|
-
this.logger.info("Discount code removed successfully", { cartId, code });
|
|
5293
|
-
void medusaCart;
|
|
5294
|
-
const updatedCart = await this.retrieveCartWithLineDetails(cartId);
|
|
5295
|
-
return transformCart(updatedCart);
|
|
5995
|
+
const response = await fetch(`${this.baseUrl}/store/carts/${cartId}/promotions`, {
|
|
5996
|
+
method: "DELETE",
|
|
5997
|
+
headers: {
|
|
5998
|
+
"Content-Type": "application/json",
|
|
5999
|
+
"x-publishable-api-key": this.publishableKey || ""
|
|
5296
6000
|
},
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
6001
|
+
body: JSON.stringify({
|
|
6002
|
+
promo_codes: [code]
|
|
6003
|
+
})
|
|
6004
|
+
});
|
|
6005
|
+
if (!response.ok) {
|
|
6006
|
+
const error = await response.json();
|
|
6007
|
+
throw new Error(error.message || "Failed to remove discount code");
|
|
6008
|
+
}
|
|
6009
|
+
this.logger.info("Discount code removed successfully", { cartId, code });
|
|
6010
|
+
const updatedCart = await this.retrieveCartWithLineDetails(cartId);
|
|
6011
|
+
return transformCart(updatedCart);
|
|
5300
6012
|
} catch (error) {
|
|
5301
6013
|
this.logger.error("Failed to remove discount code", { cartId, code, error: error.message });
|
|
5302
6014
|
throw new ProviderAPIError(
|
|
@@ -5434,7 +6146,7 @@ var MedusaProvider = class {
|
|
|
5434
6146
|
}
|
|
5435
6147
|
const channelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
|
|
5436
6148
|
if (!channelId) {
|
|
5437
|
-
throw new
|
|
6149
|
+
throw new StorefrontConfigError(
|
|
5438
6150
|
"Medusa requires NEXT_PUBLIC_SALES_CHANNEL_ID to be configured. Each storefront must be linked to a sales channel. Set this in your .env file or environment variables.",
|
|
5439
6151
|
"medusa",
|
|
5440
6152
|
"NEXT_PUBLIC_SALES_CHANNEL_ID"
|
|
@@ -5463,7 +6175,7 @@ var MedusaProvider = class {
|
|
|
5463
6175
|
});
|
|
5464
6176
|
return context;
|
|
5465
6177
|
} catch (error) {
|
|
5466
|
-
if (error instanceof
|
|
6178
|
+
if (error instanceof StorefrontConfigError) throw error;
|
|
5467
6179
|
const errorMessage = extractErrorMessage(error);
|
|
5468
6180
|
throw new ProviderAPIError(
|
|
5469
6181
|
`Failed to initialize storefront context: ${errorMessage}`,
|
|
@@ -9291,6 +10003,7 @@ var StelOrderProvider = class {
|
|
|
9291
10003
|
// src/providers/mock/mock.ts
|
|
9292
10004
|
var FIXTURE_TIMESTAMP = "2026-01-01T00:00:00.000Z";
|
|
9293
10005
|
var DEFAULT_SITE_URL = "https://mock.thorcommerce.local";
|
|
10006
|
+
var MOCK_SALES_CHANNEL_ID = "mock-sales-channel";
|
|
9294
10007
|
var regionFixtures = [
|
|
9295
10008
|
{
|
|
9296
10009
|
id: "mock-region-us",
|
|
@@ -9359,10 +10072,11 @@ var categoryFixtures = [
|
|
|
9359
10072
|
description: "Complementary products for cart and bundle flows."
|
|
9360
10073
|
}
|
|
9361
10074
|
];
|
|
9362
|
-
var
|
|
10075
|
+
var heroProductFixtures = [
|
|
9363
10076
|
{
|
|
9364
10077
|
id: "mock-product-aurora-jacket",
|
|
9365
10078
|
handle: "aurora-jacket",
|
|
10079
|
+
createdAt: "2026-01-05T00:00:00.000Z",
|
|
9366
10080
|
title: "Aurora Quilted Jacket",
|
|
9367
10081
|
description: "Lightweight quilted outerwear with tonal hardware and a soft structured collar.",
|
|
9368
10082
|
imageLabel: "Aurora Jacket",
|
|
@@ -9404,6 +10118,7 @@ var productFixtures = [
|
|
|
9404
10118
|
{
|
|
9405
10119
|
id: "mock-product-solis-dress",
|
|
9406
10120
|
handle: "solis-dress",
|
|
10121
|
+
createdAt: "2026-01-03T00:00:00.000Z",
|
|
9407
10122
|
title: "Solis Pleated Dress",
|
|
9408
10123
|
description: "A fluid pleated dress with a clean neckline designed for hero storytelling.",
|
|
9409
10124
|
imageLabel: "Solis Dress",
|
|
@@ -9443,6 +10158,7 @@ var productFixtures = [
|
|
|
9443
10158
|
{
|
|
9444
10159
|
id: "mock-product-helios-lamp",
|
|
9445
10160
|
handle: "helios-lamp",
|
|
10161
|
+
createdAt: "2026-01-04T00:00:00.000Z",
|
|
9446
10162
|
title: "Helios Table Lamp",
|
|
9447
10163
|
description: "Warm ambient lamp with sculptural proportions for lifestyle and home grids.",
|
|
9448
10164
|
imageLabel: "Helios Lamp",
|
|
@@ -9466,6 +10182,7 @@ var productFixtures = [
|
|
|
9466
10182
|
{
|
|
9467
10183
|
id: "mock-product-luna-bag",
|
|
9468
10184
|
handle: "luna-bag",
|
|
10185
|
+
createdAt: "2026-01-02T00:00:00.000Z",
|
|
9469
10186
|
title: "Luna Crossbody Bag",
|
|
9470
10187
|
description: "Compact everyday bag for accessory rails, checkout add-ons, and bundle modules.",
|
|
9471
10188
|
imageLabel: "Luna Bag",
|
|
@@ -9487,6 +10204,119 @@ var productFixtures = [
|
|
|
9487
10204
|
]
|
|
9488
10205
|
}
|
|
9489
10206
|
];
|
|
10207
|
+
var generatedBlueprints = [
|
|
10208
|
+
{
|
|
10209
|
+
categoryHandle: "jackets",
|
|
10210
|
+
noun: "Jacket",
|
|
10211
|
+
productType: "outerwear",
|
|
10212
|
+
vendor: "Thor Atelier",
|
|
10213
|
+
tags: ["outerwear", "layer"],
|
|
10214
|
+
variantName: "Size",
|
|
10215
|
+
variantValues: ["S", "M"],
|
|
10216
|
+
names: ["Alpine", "Boreal", "Cinder", "Drift", "Ember", "Fjord", "Harbor", "Meridian", "Nomad", "Tundra", "Vantage"],
|
|
10217
|
+
description: "A considered layering piece in a brushed weave.",
|
|
10218
|
+
basePrice: 118,
|
|
10219
|
+
baseHue: 210
|
|
10220
|
+
},
|
|
10221
|
+
{
|
|
10222
|
+
categoryHandle: "dresses",
|
|
10223
|
+
noun: "Dress",
|
|
10224
|
+
productType: "dress",
|
|
10225
|
+
vendor: "Thor Atelier",
|
|
10226
|
+
tags: ["dress", "editorial"],
|
|
10227
|
+
variantName: "Size",
|
|
10228
|
+
variantValues: ["S", "M"],
|
|
10229
|
+
names: ["Amber", "Camille", "Dahlia", "Elara", "Iris", "Lumen", "Nova", "Rosalind", "Vera"],
|
|
10230
|
+
description: "Cut from a fluid cloth for editorial styling.",
|
|
10231
|
+
basePrice: 142,
|
|
10232
|
+
baseHue: 330
|
|
10233
|
+
},
|
|
10234
|
+
{
|
|
10235
|
+
categoryHandle: "home",
|
|
10236
|
+
noun: "Lamp",
|
|
10237
|
+
productType: "decor",
|
|
10238
|
+
vendor: "Thor Home",
|
|
10239
|
+
tags: ["home", "decor", "interior"],
|
|
10240
|
+
variantName: "Finish",
|
|
10241
|
+
variantValues: ["Brass"],
|
|
10242
|
+
names: ["Atlas", "Copper", "Dune", "Onyx", "Quill", "Slate", "Terra", "Verdant"],
|
|
10243
|
+
description: "A calm sculptural accent for modern interiors.",
|
|
10244
|
+
basePrice: 74,
|
|
10245
|
+
baseHue: 40
|
|
10246
|
+
},
|
|
10247
|
+
{
|
|
10248
|
+
categoryHandle: "accessories",
|
|
10249
|
+
noun: "Bag",
|
|
10250
|
+
productType: "bag",
|
|
10251
|
+
vendor: "Thor Atelier",
|
|
10252
|
+
tags: ["accessory", "everyday"],
|
|
10253
|
+
variantName: "Color",
|
|
10254
|
+
variantValues: ["Juniper"],
|
|
10255
|
+
names: ["Cove", "Fable", "Juniper", "Kestrel", "Marlowe", "Piper", "Rune"],
|
|
10256
|
+
description: "An everyday carry with a textured finish.",
|
|
10257
|
+
basePrice: 68,
|
|
10258
|
+
baseHue: 280
|
|
10259
|
+
}
|
|
10260
|
+
];
|
|
10261
|
+
var GENERATED_CREATED_AT_BASE = Date.parse("2025-12-30T00:00:00.000Z");
|
|
10262
|
+
var GENERATED_COLLECTION_POOL = ["new-arrivals", "best-sellers", "studio-essentials"];
|
|
10263
|
+
function buildGeneratedProductFixtures() {
|
|
10264
|
+
const fixtures = [];
|
|
10265
|
+
let globalIndex = 0;
|
|
10266
|
+
for (const blueprint of generatedBlueprints) {
|
|
10267
|
+
const noun = blueprint.noun.toLowerCase();
|
|
10268
|
+
blueprint.names.forEach((name, localIndex) => {
|
|
10269
|
+
const handle = `${name.toLowerCase()}-${noun}`;
|
|
10270
|
+
const title = `${name} ${blueprint.noun}`;
|
|
10271
|
+
const price = blueprint.basePrice + localIndex * 7 + localIndex % 3 * 0.5;
|
|
10272
|
+
const quantityAvailable = globalIndex * 5 % 17;
|
|
10273
|
+
const primaryCollection = GENERATED_COLLECTION_POOL[globalIndex % GENERATED_COLLECTION_POOL.length];
|
|
10274
|
+
const collectionHandles = [primaryCollection];
|
|
10275
|
+
if (globalIndex % 5 === 0) {
|
|
10276
|
+
const secondary = GENERATED_COLLECTION_POOL[(globalIndex + 1) % GENERATED_COLLECTION_POOL.length];
|
|
10277
|
+
if (secondary !== primaryCollection) {
|
|
10278
|
+
collectionHandles.push(secondary);
|
|
10279
|
+
}
|
|
10280
|
+
}
|
|
10281
|
+
const createdAt = new Date(GENERATED_CREATED_AT_BASE - globalIndex * 864e5).toISOString();
|
|
10282
|
+
const variants = blueprint.variantValues.map((value, variantIndex) => ({
|
|
10283
|
+
id: `mock-variant-${handle}-${value.toLowerCase()}`,
|
|
10284
|
+
title: value,
|
|
10285
|
+
price,
|
|
10286
|
+
quantityAvailable: Math.max(0, quantityAvailable - variantIndex),
|
|
10287
|
+
selectedOptions: [{ name: blueprint.variantName, value }]
|
|
10288
|
+
}));
|
|
10289
|
+
fixtures.push({
|
|
10290
|
+
id: `mock-product-${handle}`,
|
|
10291
|
+
handle,
|
|
10292
|
+
title,
|
|
10293
|
+
description: `${title} - ${blueprint.description}`,
|
|
10294
|
+
imageLabel: title,
|
|
10295
|
+
imageHue: (blueprint.baseHue + localIndex * 13) % 360,
|
|
10296
|
+
collectionHandles,
|
|
10297
|
+
categoryHandle: blueprint.categoryHandle,
|
|
10298
|
+
productType: blueprint.productType,
|
|
10299
|
+
vendor: blueprint.vendor,
|
|
10300
|
+
tags: [...blueprint.tags],
|
|
10301
|
+
options: [
|
|
10302
|
+
{
|
|
10303
|
+
id: `${handle}-${blueprint.variantName.toLowerCase()}`,
|
|
10304
|
+
name: blueprint.variantName,
|
|
10305
|
+
values: [...blueprint.variantValues]
|
|
10306
|
+
}
|
|
10307
|
+
],
|
|
10308
|
+
variants,
|
|
10309
|
+
createdAt
|
|
10310
|
+
});
|
|
10311
|
+
globalIndex += 1;
|
|
10312
|
+
});
|
|
10313
|
+
}
|
|
10314
|
+
return fixtures;
|
|
10315
|
+
}
|
|
10316
|
+
var productFixtures = [
|
|
10317
|
+
...heroProductFixtures,
|
|
10318
|
+
...buildGeneratedProductFixtures()
|
|
10319
|
+
];
|
|
9490
10320
|
var stockLocationFixture = {
|
|
9491
10321
|
id: "mock-stock-primary",
|
|
9492
10322
|
name: "Mock Main Warehouse",
|
|
@@ -9603,10 +10433,24 @@ function buildProducts(currencyCode) {
|
|
|
9603
10433
|
title: collection.title,
|
|
9604
10434
|
handle: collection.handle
|
|
9605
10435
|
})),
|
|
10436
|
+
categories: (() => {
|
|
10437
|
+
const category = categoryFixtures.find(
|
|
10438
|
+
(item) => item.handle === product.categoryHandle
|
|
10439
|
+
);
|
|
10440
|
+
if (!category) return void 0;
|
|
10441
|
+
return [
|
|
10442
|
+
{
|
|
10443
|
+
id: category.id,
|
|
10444
|
+
name: category.name,
|
|
10445
|
+
handle: category.handle,
|
|
10446
|
+
parentCategoryId: category.parentCategoryId
|
|
10447
|
+
}
|
|
10448
|
+
];
|
|
10449
|
+
})(),
|
|
9606
10450
|
productType: product.productType,
|
|
9607
10451
|
vendor: product.vendor,
|
|
9608
10452
|
tags: product.tags,
|
|
9609
|
-
createdAt:
|
|
10453
|
+
createdAt: product.createdAt,
|
|
9610
10454
|
updatedAt: FIXTURE_TIMESTAMP,
|
|
9611
10455
|
metadata: {
|
|
9612
10456
|
categoryHandle: product.categoryHandle,
|
|
@@ -9675,6 +10519,92 @@ function paginateProducts(products, skip, first) {
|
|
|
9675
10519
|
const end = first ? start + first : void 0;
|
|
9676
10520
|
return products.slice(start, end);
|
|
9677
10521
|
}
|
|
10522
|
+
var MOCK_CATALOG_SORT_KEYS = [
|
|
10523
|
+
"featured",
|
|
10524
|
+
"name",
|
|
10525
|
+
"newest"
|
|
10526
|
+
];
|
|
10527
|
+
var MOCK_CATALOG_ENDPOINT = "mock://catalog/products";
|
|
10528
|
+
var MOCK_CATALOG_PAGE_SIZE = 50;
|
|
10529
|
+
function resolveEffectiveCategoryHandles2(query) {
|
|
10530
|
+
const { categoryScope, categories } = query;
|
|
10531
|
+
if (categoryScope === void 0) return categories;
|
|
10532
|
+
if (categories === void 0) return [categoryScope];
|
|
10533
|
+
return categories.filter((handle) => handle === categoryScope);
|
|
10534
|
+
}
|
|
10535
|
+
function collectRequestedAdvancedFields2(query) {
|
|
10536
|
+
const fields = [];
|
|
10537
|
+
if (query.priceMin !== void 0) fields.push("priceMin");
|
|
10538
|
+
if (query.priceMax !== void 0) fields.push("priceMax");
|
|
10539
|
+
if (query.inStock !== void 0) fields.push("inStock");
|
|
10540
|
+
for (const name of Object.keys(query.attributes ?? {})) {
|
|
10541
|
+
fields.push(`attr.${name}`);
|
|
10542
|
+
}
|
|
10543
|
+
return fields;
|
|
10544
|
+
}
|
|
10545
|
+
function advancedFieldToFacet2(field) {
|
|
10546
|
+
if (field === "priceMin" || field === "priceMax") return "price";
|
|
10547
|
+
return field;
|
|
10548
|
+
}
|
|
10549
|
+
function buildAppliedCatalogFilters2(query, effectiveCategoryHandles) {
|
|
10550
|
+
return {
|
|
10551
|
+
...query.text ? { text: query.text } : {},
|
|
10552
|
+
...effectiveCategoryHandles !== void 0 ? { categories: effectiveCategoryHandles } : {},
|
|
10553
|
+
...query.collections !== void 0 ? { collections: query.collections } : {}
|
|
10554
|
+
};
|
|
10555
|
+
}
|
|
10556
|
+
function buildBasicDemonstratedQuery2(query) {
|
|
10557
|
+
return {
|
|
10558
|
+
...query.text !== void 0 ? { text: query.text } : {},
|
|
10559
|
+
...query.categoryScope !== void 0 ? { categoryScope: query.categoryScope } : {},
|
|
10560
|
+
...query.categories !== void 0 ? { categories: query.categories } : {},
|
|
10561
|
+
...query.collections !== void 0 ? { collections: query.collections } : {}
|
|
10562
|
+
};
|
|
10563
|
+
}
|
|
10564
|
+
function buildMockFacetSupport(query) {
|
|
10565
|
+
const appliedQuery = buildBasicDemonstratedQuery2(query);
|
|
10566
|
+
const scopeField = query.categoryScope !== void 0 ? { scope: query.categoryScope } : {};
|
|
10567
|
+
return {
|
|
10568
|
+
offered: ["categories", "collections"],
|
|
10569
|
+
held: ["price", "inStock"],
|
|
10570
|
+
demonstrations: {
|
|
10571
|
+
categories: {
|
|
10572
|
+
endpoint: MOCK_CATALOG_ENDPOINT,
|
|
10573
|
+
appliedQuery,
|
|
10574
|
+
...scopeField
|
|
10575
|
+
},
|
|
10576
|
+
collections: {
|
|
10577
|
+
endpoint: MOCK_CATALOG_ENDPOINT,
|
|
10578
|
+
appliedQuery,
|
|
10579
|
+
...scopeField
|
|
10580
|
+
}
|
|
10581
|
+
},
|
|
10582
|
+
heldReasons: {
|
|
10583
|
+
price: `${MOCK_CATALOG_ENDPOINT} is the mock basic listing; price filtering is not implemented in mock mode`,
|
|
10584
|
+
inStock: `${MOCK_CATALOG_ENDPOINT} is the mock basic listing; stock filtering is not implemented in mock mode`
|
|
10585
|
+
}
|
|
10586
|
+
};
|
|
10587
|
+
}
|
|
10588
|
+
function unsupportedMockCatalogOutcome(fields) {
|
|
10589
|
+
return {
|
|
10590
|
+
ok: false,
|
|
10591
|
+
error: {
|
|
10592
|
+
code: "unsupported-query",
|
|
10593
|
+
fields,
|
|
10594
|
+
message: `Unsupported catalog query in mock mode: ${fields.join(", ")}`,
|
|
10595
|
+
repair: { remove: fields }
|
|
10596
|
+
}
|
|
10597
|
+
};
|
|
10598
|
+
}
|
|
10599
|
+
function sortMockCatalogProducts(products, sortKey) {
|
|
10600
|
+
const sorted = [...products];
|
|
10601
|
+
if (sortKey === "name") {
|
|
10602
|
+
sorted.sort((left, right) => left.title.localeCompare(right.title));
|
|
10603
|
+
} else if (sortKey === "newest") {
|
|
10604
|
+
sorted.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
10605
|
+
}
|
|
10606
|
+
return sorted;
|
|
10607
|
+
}
|
|
9678
10608
|
function resolveVariant(currencyCode, merchandiseId) {
|
|
9679
10609
|
const products = buildProducts(currencyCode);
|
|
9680
10610
|
for (const product of products) {
|
|
@@ -9739,11 +10669,22 @@ function buildExternalCart(state, currencyCode, siteUrl) {
|
|
|
9739
10669
|
checkoutUrl: `${siteUrl}/checkout?cart=${state.id}`,
|
|
9740
10670
|
cost: {
|
|
9741
10671
|
subtotalAmount: createMoney(subtotal, regionCurrency),
|
|
9742
|
-
|
|
9743
|
-
totalTaxAmount: createMoney(totalTax, regionCurrency)
|
|
10672
|
+
discountAmount: createMoney(discount, regionCurrency),
|
|
10673
|
+
totalTaxAmount: createMoney(totalTax, regionCurrency),
|
|
10674
|
+
shippingAmount: createMoney(shipping, regionCurrency),
|
|
10675
|
+
totalAmount: createMoney(total, regionCurrency)
|
|
9744
10676
|
},
|
|
9745
10677
|
lines,
|
|
9746
10678
|
totalQuantity: lines.reduce((sum, line) => sum + line.quantity, 0),
|
|
10679
|
+
salesChannelId: MOCK_SALES_CHANNEL_ID,
|
|
10680
|
+
regionId: state.regionId,
|
|
10681
|
+
currencyCode: regionCurrency.toUpperCase(),
|
|
10682
|
+
selectedShippingMethodId: state.shippingMethodId,
|
|
10683
|
+
// The mock method id IS the shipping option id (the value returned by
|
|
10684
|
+
// `getMockShippingMethods` and accepted by `addShippingMethod`), so the
|
|
10685
|
+
// canonical option identity mirrors the real adapter's `so_` field and the
|
|
10686
|
+
// checkout UI can highlight the selected option.
|
|
10687
|
+
selectedShippingOptionId: state.shippingMethodId,
|
|
9747
10688
|
completedAt: state.completedAt
|
|
9748
10689
|
};
|
|
9749
10690
|
}
|
|
@@ -9799,6 +10740,69 @@ function getMockProducts(options) {
|
|
|
9799
10740
|
const sorted = sortProducts(filtered, options?.sortKey, options?.reverse);
|
|
9800
10741
|
return paginateProducts(sorted, options?.skip, options?.first);
|
|
9801
10742
|
}
|
|
10743
|
+
function getMockCatalogProducts(query, options = {}) {
|
|
10744
|
+
if (query.sort !== void 0 && !MOCK_CATALOG_SORT_KEYS.includes(query.sort)) {
|
|
10745
|
+
return unsupportedMockCatalogOutcome(["sort"]);
|
|
10746
|
+
}
|
|
10747
|
+
const region = getRegionFixture(options.regionId);
|
|
10748
|
+
const currencyCode = options.currencyCode ?? region.currencyCode;
|
|
10749
|
+
const allProducts = buildProducts(currencyCode);
|
|
10750
|
+
const facetSupport = buildMockFacetSupport(query);
|
|
10751
|
+
const requestedAdvanced = collectRequestedAdvancedFields2(query);
|
|
10752
|
+
const heldAdvanced = requestedAdvanced.filter(
|
|
10753
|
+
(field) => !facetSupport.offered.includes(advancedFieldToFacet2(field))
|
|
10754
|
+
);
|
|
10755
|
+
if (heldAdvanced.length > 0) {
|
|
10756
|
+
return unsupportedMockCatalogOutcome(heldAdvanced);
|
|
10757
|
+
}
|
|
10758
|
+
const effectiveCategoryHandles = resolveEffectiveCategoryHandles2(query);
|
|
10759
|
+
const emptyIntersection = effectiveCategoryHandles !== void 0 && effectiveCategoryHandles.length === 0;
|
|
10760
|
+
const categoryFilterEmpty = effectiveCategoryHandles !== void 0 && !effectiveCategoryHandles.some(
|
|
10761
|
+
(handle) => categoryFixtures.some(
|
|
10762
|
+
(category) => category.handle === handle || category.id === handle
|
|
10763
|
+
)
|
|
10764
|
+
);
|
|
10765
|
+
const collectionFilterEmpty = query.collections !== void 0 && !query.collections.some(
|
|
10766
|
+
(handle) => collectionFixtures.some(
|
|
10767
|
+
(collection) => collection.handle === handle || collection.id === handle
|
|
10768
|
+
)
|
|
10769
|
+
);
|
|
10770
|
+
if (emptyIntersection || categoryFilterEmpty || collectionFilterEmpty) {
|
|
10771
|
+
return {
|
|
10772
|
+
ok: true,
|
|
10773
|
+
results: {
|
|
10774
|
+
products: [],
|
|
10775
|
+
appliedFilters: buildAppliedCatalogFilters2(
|
|
10776
|
+
query,
|
|
10777
|
+
effectiveCategoryHandles
|
|
10778
|
+
),
|
|
10779
|
+
facetSupport
|
|
10780
|
+
}
|
|
10781
|
+
};
|
|
10782
|
+
}
|
|
10783
|
+
const filtered = filterProducts(allProducts, {
|
|
10784
|
+
query: query.text,
|
|
10785
|
+
categoryIds: effectiveCategoryHandles,
|
|
10786
|
+
collectionIds: query.collections
|
|
10787
|
+
});
|
|
10788
|
+
const sorted = sortMockCatalogProducts(filtered, query.sort);
|
|
10789
|
+
const pageSize = query.pageSize ?? MOCK_CATALOG_PAGE_SIZE;
|
|
10790
|
+
const page = query.page ?? 1;
|
|
10791
|
+
const offset = (page - 1) * pageSize;
|
|
10792
|
+
return {
|
|
10793
|
+
ok: true,
|
|
10794
|
+
results: {
|
|
10795
|
+
products: sorted.slice(offset, offset + pageSize),
|
|
10796
|
+
// The full matching fixture count, never the page length.
|
|
10797
|
+
total: filtered.length,
|
|
10798
|
+
appliedFilters: buildAppliedCatalogFilters2(
|
|
10799
|
+
query,
|
|
10800
|
+
effectiveCategoryHandles
|
|
10801
|
+
),
|
|
10802
|
+
facetSupport
|
|
10803
|
+
}
|
|
10804
|
+
};
|
|
10805
|
+
}
|
|
9802
10806
|
function getMockProduct(handle, regionId) {
|
|
9803
10807
|
const region = getRegionFixture(regionId);
|
|
9804
10808
|
return buildProducts(region.currencyCode).find(
|
|
@@ -9911,6 +10915,17 @@ function getMockPaymentMethods(currencyCode) {
|
|
|
9911
10915
|
isEnabled: true,
|
|
9912
10916
|
requiresSetup: false,
|
|
9913
10917
|
description: "Mock cash payment option for checkout prototypes."
|
|
10918
|
+
},
|
|
10919
|
+
{
|
|
10920
|
+
// The single approved manual TEST provider used by the real Medusa
|
|
10921
|
+
// backend, mirrored in mock mode so the guest TEST purchase path is
|
|
10922
|
+
// exercisable without a backend.
|
|
10923
|
+
id: "pp_system_default",
|
|
10924
|
+
name: "Manual TEST payment",
|
|
10925
|
+
type: "cash_on_delivery",
|
|
10926
|
+
isEnabled: true,
|
|
10927
|
+
requiresSetup: false,
|
|
10928
|
+
description: "Manual system TEST provider (bookkeeping only; not external funds)."
|
|
9914
10929
|
}
|
|
9915
10930
|
];
|
|
9916
10931
|
}
|
|
@@ -10004,6 +11019,7 @@ var MockProvider = class {
|
|
|
10004
11019
|
version = "1.0.0";
|
|
10005
11020
|
capabilities = MOCK_CAPABILITIES;
|
|
10006
11021
|
isMock = true;
|
|
11022
|
+
supportedCatalogSortKeys = MOCK_CATALOG_SORT_KEYS;
|
|
10007
11023
|
currencyCode;
|
|
10008
11024
|
siteUrl;
|
|
10009
11025
|
constructor(config = {}) {
|
|
@@ -10034,6 +11050,18 @@ var MockProvider = class {
|
|
|
10034
11050
|
regionId: options?.regionId
|
|
10035
11051
|
});
|
|
10036
11052
|
}
|
|
11053
|
+
/**
|
|
11054
|
+
* Canonical catalog query path (E2 task 2.5).
|
|
11055
|
+
*
|
|
11056
|
+
* Delegates to the fixture-backed canonical implementation so mock mode is a
|
|
11057
|
+
* faithful stand-in for the same contract the Medusa adapter honors: scope ∩
|
|
11058
|
+
* categories, collection resolution, global ordering before pagination, the
|
|
11059
|
+
* real full match count, and a typed `unsupported-query` for advanced filters
|
|
11060
|
+
* the mock does not implement.
|
|
11061
|
+
*/
|
|
11062
|
+
async getCatalogProducts(query) {
|
|
11063
|
+
return getMockCatalogProducts(query, { currencyCode: this.currencyCode });
|
|
11064
|
+
}
|
|
10037
11065
|
async getProductRecommendations(productId) {
|
|
10038
11066
|
return getMockProductRecommendations(productId);
|
|
10039
11067
|
}
|
|
@@ -10169,6 +11197,25 @@ var MockProvider = class {
|
|
|
10169
11197
|
const order = completeMockCheckout(cart, this.currencyCode, this.siteUrl);
|
|
10170
11198
|
return order;
|
|
10171
11199
|
}
|
|
11200
|
+
/**
|
|
11201
|
+
* Discriminated completion for the fixture provider.
|
|
11202
|
+
*
|
|
11203
|
+
* Completion is idempotent by construction (the fixture order id derives from
|
|
11204
|
+
* the cart id), so completing an already-completed cart returns the same
|
|
11205
|
+
* persisted order — mirroring the native reconcile path without a second
|
|
11206
|
+
* order.
|
|
11207
|
+
*/
|
|
11208
|
+
async completeCart(cartId) {
|
|
11209
|
+
const cart = getMockCartState(cartId);
|
|
11210
|
+
if (!cart) {
|
|
11211
|
+
return {
|
|
11212
|
+
type: "unresolved",
|
|
11213
|
+
error: { message: `Cart not found: ${cartId}`, code: "CART_NOT_FOUND", status: 404 }
|
|
11214
|
+
};
|
|
11215
|
+
}
|
|
11216
|
+
const order = completeMockCheckout(cart, this.currencyCode, this.siteUrl);
|
|
11217
|
+
return { type: "order", order };
|
|
11218
|
+
}
|
|
10172
11219
|
async getShippingMethods(cartId) {
|
|
10173
11220
|
const cart = getMockCartState(cartId);
|
|
10174
11221
|
return getMockShippingMethods(cart?.regionId);
|
|
@@ -10293,6 +11340,113 @@ var MockProvider = class {
|
|
|
10293
11340
|
}
|
|
10294
11341
|
};
|
|
10295
11342
|
|
|
11343
|
+
// src/factory/provider-types.ts
|
|
11344
|
+
var SupportedProviderType = /* @__PURE__ */ ((SupportedProviderType2) => {
|
|
11345
|
+
SupportedProviderType2["Mock"] = "mock";
|
|
11346
|
+
SupportedProviderType2["Medusa"] = "medusa";
|
|
11347
|
+
SupportedProviderType2["StelOrder"] = "stelorder";
|
|
11348
|
+
SupportedProviderType2["Shopify"] = "shopify";
|
|
11349
|
+
SupportedProviderType2["BigCommerce"] = "bigcommerce";
|
|
11350
|
+
SupportedProviderType2["WooCommerce"] = "woocommerce";
|
|
11351
|
+
SupportedProviderType2["Spree"] = "spree";
|
|
11352
|
+
SupportedProviderType2["Magento"] = "magento";
|
|
11353
|
+
return SupportedProviderType2;
|
|
11354
|
+
})(SupportedProviderType || {});
|
|
11355
|
+
var PROVIDER_METADATA = {
|
|
11356
|
+
mock: {
|
|
11357
|
+
name: "Mock",
|
|
11358
|
+
description: "Fixture-backed runtime provider for layout/design mode without a real backend",
|
|
11359
|
+
requiresStorefront: false,
|
|
11360
|
+
requiredEnvVars: [],
|
|
11361
|
+
maxRetries: 0
|
|
11362
|
+
},
|
|
11363
|
+
medusa: {
|
|
11364
|
+
name: "Medusa JS",
|
|
11365
|
+
description: "Medusa v2 commerce engine",
|
|
11366
|
+
requiresStorefront: true,
|
|
11367
|
+
requiredEnvVars: [
|
|
11368
|
+
"NEXT_PUBLIC_COMMERCE_API_URL",
|
|
11369
|
+
"NEXT_PUBLIC_COMMERCE_API_KEY",
|
|
11370
|
+
"NEXT_PUBLIC_SALES_CHANNEL_ID"
|
|
11371
|
+
],
|
|
11372
|
+
maxRetries: 3
|
|
11373
|
+
},
|
|
11374
|
+
stelorder: {
|
|
11375
|
+
name: "Stel Order",
|
|
11376
|
+
description: "Stel Order ERP and sales document API",
|
|
11377
|
+
requiresStorefront: false,
|
|
11378
|
+
requiredEnvVars: [
|
|
11379
|
+
"NEXT_PUBLIC_COMMERCE_API_URL",
|
|
11380
|
+
"NEXT_PUBLIC_COMMERCE_API_KEY"
|
|
11381
|
+
],
|
|
11382
|
+
maxRetries: 2
|
|
11383
|
+
},
|
|
11384
|
+
shopify: {
|
|
11385
|
+
name: "Shopify",
|
|
11386
|
+
description: "Shopify Storefront API (GraphQL)",
|
|
11387
|
+
requiresStorefront: false,
|
|
11388
|
+
requiredEnvVars: [
|
|
11389
|
+
"NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN",
|
|
11390
|
+
"NEXT_PUBLIC_SHOPIFY_STOREFRONT_TOKEN"
|
|
11391
|
+
],
|
|
11392
|
+
maxRetries: 3
|
|
11393
|
+
},
|
|
11394
|
+
bigcommerce: {
|
|
11395
|
+
name: "BigCommerce",
|
|
11396
|
+
description: "BigCommerce REST Storefront API",
|
|
11397
|
+
requiresStorefront: false,
|
|
11398
|
+
requiredEnvVars: [
|
|
11399
|
+
"BIGCOMMERCE_STORE_HASH",
|
|
11400
|
+
"BIGCOMMERCE_STOREFRONT_API_TOKEN",
|
|
11401
|
+
"BIGCOMMERCE_CHANNEL_ID"
|
|
11402
|
+
],
|
|
11403
|
+
maxRetries: 3
|
|
11404
|
+
},
|
|
11405
|
+
woocommerce: {
|
|
11406
|
+
name: "WooCommerce",
|
|
11407
|
+
description: "WooCommerce Store API + REST API v3",
|
|
11408
|
+
requiresStorefront: false,
|
|
11409
|
+
requiredEnvVars: [
|
|
11410
|
+
"WOOCOMMERCE_URL",
|
|
11411
|
+
"WOOCOMMERCE_CONSUMER_KEY",
|
|
11412
|
+
"WOOCOMMERCE_CONSUMER_SECRET"
|
|
11413
|
+
],
|
|
11414
|
+
maxRetries: 3
|
|
11415
|
+
},
|
|
11416
|
+
spree: {
|
|
11417
|
+
name: "Spree Commerce",
|
|
11418
|
+
description: "Spree API v2 Storefront (JSON:API)",
|
|
11419
|
+
requiresStorefront: false,
|
|
11420
|
+
requiredEnvVars: [
|
|
11421
|
+
"SPREE_API_URL"
|
|
11422
|
+
],
|
|
11423
|
+
maxRetries: 3
|
|
11424
|
+
},
|
|
11425
|
+
magento: {
|
|
11426
|
+
name: "Adobe Commerce (Magento)",
|
|
11427
|
+
description: "Adobe Commerce GraphQL + REST API",
|
|
11428
|
+
requiresStorefront: true,
|
|
11429
|
+
requiredEnvVars: [
|
|
11430
|
+
"MAGENTO_URL",
|
|
11431
|
+
"MAGENTO_STORE_CODE"
|
|
11432
|
+
],
|
|
11433
|
+
maxRetries: 3
|
|
11434
|
+
}
|
|
11435
|
+
};
|
|
11436
|
+
function isSupportedProviderType(value) {
|
|
11437
|
+
return typeof value === "string" && Object.values(SupportedProviderType).includes(value);
|
|
11438
|
+
}
|
|
11439
|
+
function getProviderMetadata(type) {
|
|
11440
|
+
if (!isSupportedProviderType(type)) {
|
|
11441
|
+
throw new Error(
|
|
11442
|
+
`Unsupported provider: ${type}. Supported: ${Object.values(
|
|
11443
|
+
SupportedProviderType
|
|
11444
|
+
).join(", ")}`
|
|
11445
|
+
);
|
|
11446
|
+
}
|
|
11447
|
+
return PROVIDER_METADATA[type];
|
|
11448
|
+
}
|
|
11449
|
+
|
|
10296
11450
|
// src/factory/index.ts
|
|
10297
11451
|
function createProvider(options) {
|
|
10298
11452
|
if (!options || !options.provider) {
|
|
@@ -10343,11 +11497,13 @@ function isProviderType(provider, type) {
|
|
|
10343
11497
|
MAGENTO_CAPABILITIES,
|
|
10344
11498
|
MEDUSA_CAPABILITIES,
|
|
10345
11499
|
MOCK_CAPABILITIES,
|
|
11500
|
+
MOCK_SALES_CHANNEL_ID,
|
|
10346
11501
|
MagentoProvider,
|
|
10347
11502
|
MedusaMetadataAdapter,
|
|
10348
11503
|
MedusaProvider,
|
|
10349
11504
|
MetricsService,
|
|
10350
11505
|
MockProvider,
|
|
11506
|
+
PROVIDER_METADATA,
|
|
10351
11507
|
ProviderAPIError,
|
|
10352
11508
|
ProviderNotFoundError,
|
|
10353
11509
|
ResourceNotFoundError,
|
|
@@ -10358,6 +11514,7 @@ function isProviderType(provider, type) {
|
|
|
10358
11514
|
SpreeProvider,
|
|
10359
11515
|
StelOrderProvider,
|
|
10360
11516
|
StorefrontConfigError,
|
|
11517
|
+
SupportedProviderType,
|
|
10361
11518
|
WOOCOMMERCE_CAPABILITIES,
|
|
10362
11519
|
WooCommerceProvider,
|
|
10363
11520
|
assertEntityBelongsToChannel,
|
|
@@ -10376,10 +11533,12 @@ function isProviderType(provider, type) {
|
|
|
10376
11533
|
getBackendProviderId,
|
|
10377
11534
|
getCapabilityDescription,
|
|
10378
11535
|
getChannelScope,
|
|
11536
|
+
getProviderMetadata,
|
|
10379
11537
|
isBackendCapabilities,
|
|
10380
11538
|
isGlobalEntity,
|
|
10381
11539
|
isProviderType,
|
|
10382
11540
|
isScopedToChannel,
|
|
11541
|
+
isSupportedProviderType,
|
|
10383
11542
|
moneyToNumber,
|
|
10384
11543
|
moneyToNumberSafe,
|
|
10385
11544
|
numberToMoney,
|