@moonbase.sh/storefront-api 2.5.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/index.cjs +634 -18
- package/dist/index.d.cts +8055 -8021
- package/dist/index.d.ts +8055 -8021
- package/dist/index.js +627 -18
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -25,10 +25,12 @@ __export(index_exports, {
|
|
|
25
25
|
ActivationRequestStatus: () => ActivationRequestStatus,
|
|
26
26
|
ActivationStatus: () => ActivationStatus,
|
|
27
27
|
Architecture: () => Architecture,
|
|
28
|
+
CartUtils: () => CartUtils,
|
|
28
29
|
ConnectableAccountProvider: () => ConnectableAccountProvider,
|
|
29
30
|
ConsoleLogger: () => ConsoleLogger,
|
|
30
31
|
CycleLength: () => CycleLength,
|
|
31
32
|
DiscountUtils: () => DiscountUtils,
|
|
33
|
+
ImageUtils: () => ImageUtils,
|
|
32
34
|
InMemoryStore: () => InMemoryStore,
|
|
33
35
|
LicenseStatus: () => LicenseStatus,
|
|
34
36
|
LocalStorageStore: () => LocalStorageStore,
|
|
@@ -46,9 +48,14 @@ __export(index_exports, {
|
|
|
46
48
|
Platform: () => Platform,
|
|
47
49
|
SubscriptionStatus: () => SubscriptionStatus,
|
|
48
50
|
TokenStore: () => TokenStore,
|
|
51
|
+
imageWidths: () => imageWidths,
|
|
52
|
+
isPromotionDisplay: () => isPromotionDisplay,
|
|
53
|
+
isTransformableImageUrl: () => isTransformableImageUrl,
|
|
54
|
+
maxImageDimension: () => maxImageDimension,
|
|
49
55
|
objectToQuery: () => objectToQuery,
|
|
50
56
|
parseOffSiteReferrer: () => parseOffSiteReferrer,
|
|
51
57
|
problemDetailsSchema: () => problemDetailsSchema,
|
|
58
|
+
promotionDisplays: () => promotionDisplays,
|
|
52
59
|
resolveUtm: () => resolveUtm,
|
|
53
60
|
schemas: () => schemas_exports12,
|
|
54
61
|
utmToObject: () => utmToObject
|
|
@@ -69,10 +76,14 @@ var import_zod4 = require("zod");
|
|
|
69
76
|
var schemas_exports2 = {};
|
|
70
77
|
__export(schemas_exports2, {
|
|
71
78
|
offerConditionSchema: () => offerConditionSchema,
|
|
79
|
+
offerScopeSchema: () => offerScopeSchema,
|
|
72
80
|
storefrontBundleSchema: () => storefrontBundleSchema,
|
|
73
81
|
storefrontOfferSchema: () => storefrontOfferSchema,
|
|
74
82
|
storefrontOfferTargetSchema: () => storefrontOfferTargetSchema,
|
|
75
83
|
storefrontProductSchema: () => storefrontProductSchema,
|
|
84
|
+
storefrontPromotionCtaSchema: () => storefrontPromotionCtaSchema,
|
|
85
|
+
storefrontPromotionSchema: () => storefrontPromotionSchema,
|
|
86
|
+
storefrontPromotionTargetSchema: () => storefrontPromotionTargetSchema,
|
|
76
87
|
storefrontSchema: () => storefrontSchema,
|
|
77
88
|
taxEstimateSchema: () => taxEstimateSchema
|
|
78
89
|
});
|
|
@@ -112,6 +123,11 @@ function utmToObject(utm) {
|
|
|
112
123
|
|
|
113
124
|
// src/globalSchemas.ts
|
|
114
125
|
var priceCollectionSchema = import_zod.z.record(import_zod.z.number());
|
|
126
|
+
var discountPromotionFields = {
|
|
127
|
+
promotionId: import_zod.z.string().optional(),
|
|
128
|
+
validFrom: import_zod.z.string().optional(),
|
|
129
|
+
validUntil: import_zod.z.string().optional()
|
|
130
|
+
};
|
|
115
131
|
var percentageOffDiscountSchema = import_zod.z.object({
|
|
116
132
|
type: import_zod.z.literal("PercentageOffDiscount"),
|
|
117
133
|
name: import_zod.z.string(),
|
|
@@ -119,7 +135,8 @@ var percentageOffDiscountSchema = import_zod.z.object({
|
|
|
119
135
|
percentage: import_zod.z.number(),
|
|
120
136
|
total: priceCollectionSchema.optional(),
|
|
121
137
|
isExclusive: import_zod.z.boolean(),
|
|
122
|
-
recurringPaymentUseCount: import_zod.z.number().optional()
|
|
138
|
+
recurringPaymentUseCount: import_zod.z.number().optional(),
|
|
139
|
+
...discountPromotionFields
|
|
123
140
|
});
|
|
124
141
|
var flatAmountOffDiscountSchema = import_zod.z.object({
|
|
125
142
|
type: import_zod.z.literal("FlatAmountOffDiscount"),
|
|
@@ -127,7 +144,8 @@ var flatAmountOffDiscountSchema = import_zod.z.object({
|
|
|
127
144
|
description: import_zod.z.string().optional(),
|
|
128
145
|
total: priceCollectionSchema.optional(),
|
|
129
146
|
isExclusive: import_zod.z.boolean(),
|
|
130
|
-
recurringPaymentUseCount: import_zod.z.number().optional()
|
|
147
|
+
recurringPaymentUseCount: import_zod.z.number().optional(),
|
|
148
|
+
...discountPromotionFields
|
|
131
149
|
});
|
|
132
150
|
var moneySchema = import_zod.z.object({
|
|
133
151
|
currency: import_zod.z.string(),
|
|
@@ -161,6 +179,7 @@ var rawPropertyValueSchema = import_zod.z.lazy(
|
|
|
161
179
|
import_zod.z.string(),
|
|
162
180
|
import_zod.z.number(),
|
|
163
181
|
import_zod.z.boolean(),
|
|
182
|
+
import_zod.z.null(),
|
|
164
183
|
import_zod.z.record(rawPropertyValueSchema),
|
|
165
184
|
import_zod.z.array(rawPropertyValueSchema)
|
|
166
185
|
])
|
|
@@ -247,6 +266,10 @@ var productSummarySchema = import_zod2.z.object({
|
|
|
247
266
|
releaseDescription: import_zod2.z.string().optional(),
|
|
248
267
|
downloadsNeedsUser: import_zod2.z.boolean(),
|
|
249
268
|
downloadsNeedsOwnership: import_zod2.z.boolean(),
|
|
269
|
+
downloadsNeedsGroupMembership: import_zod2.z.boolean(),
|
|
270
|
+
// Whether this caller clears every requirement above, including the group membership the client
|
|
271
|
+
// can't see for itself.
|
|
272
|
+
downloadsAllowed: import_zod2.z.boolean(),
|
|
250
273
|
downloads: import_zod2.z.array(downloadSchema).optional()
|
|
251
274
|
});
|
|
252
275
|
|
|
@@ -287,7 +310,22 @@ var cartContainsItemsConditionSchema = import_zod3.z.object({
|
|
|
287
310
|
minimumItems: import_zod3.z.number(),
|
|
288
311
|
relevantItemVariations: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.string().array())
|
|
289
312
|
});
|
|
290
|
-
var
|
|
313
|
+
var cartTotalConditionSchema = import_zod3.z.object({
|
|
314
|
+
type: import_zod3.z.literal("CartTotal"),
|
|
315
|
+
// Both bounds are optional, but never both absent. They serialize as an explicit `null` when
|
|
316
|
+
// unset, hence `.nullish()` rather than `.optional()`. The API converts them into every enabled
|
|
317
|
+
// currency up front, precisely so we can tell the buyer how much further they have to go without
|
|
318
|
+
// doing any currency conversion ourselves.
|
|
319
|
+
minimumTotal: priceCollectionSchema.nullish(),
|
|
320
|
+
maximumTotal: priceCollectionSchema.nullish()
|
|
321
|
+
});
|
|
322
|
+
var knownOfferConditionSchema = import_zod3.z.discriminatedUnion("type", [
|
|
323
|
+
cartContainsItemsConditionSchema,
|
|
324
|
+
cartTotalConditionSchema
|
|
325
|
+
]);
|
|
326
|
+
var unsupportedOfferConditionSchema = import_zod3.z.object({ type: import_zod3.z.string() }).passthrough().transform(({ type }) => ({ type: "Unsupported", unsupportedType: type }));
|
|
327
|
+
var offerConditionSchema = import_zod3.z.union([knownOfferConditionSchema, unsupportedOfferConditionSchema]);
|
|
328
|
+
var offerScopeSchema = import_zod3.z.enum(["Item", "Cart"]).catch("Item").optional().default("Item");
|
|
291
329
|
var offerTargetItemSchema = import_zod3.z.union([storefrontProductSchema, storefrontBundleSchema]);
|
|
292
330
|
var storefrontOfferTargetSchema = import_zod3.z.object({
|
|
293
331
|
// The product or bundle the offer applies to.
|
|
@@ -303,8 +341,10 @@ function legacyOfferTargets(target, targetVariations) {
|
|
|
303
341
|
}
|
|
304
342
|
var storefrontOfferSchema = import_zod3.z.object({
|
|
305
343
|
id: import_zod3.z.string(),
|
|
344
|
+
scope: offerScopeSchema,
|
|
306
345
|
// Each target pairs a product/bundle with the pricing variations the offer applies to (an empty
|
|
307
|
-
// list means all variations). This is the model to consume.
|
|
346
|
+
// list means all variations). This is the model to consume. A cart offer may have no targets at
|
|
347
|
+
// all, which means it discounts every line item.
|
|
308
348
|
targets: import_zod3.z.array(storefrontOfferTargetSchema).optional(),
|
|
309
349
|
// Legacy fields, kept optional only so old cached storefront responses still parse. The API now
|
|
310
350
|
// exposes the unified `targets` model; we derive `targets` from these when a response predates it.
|
|
@@ -318,6 +358,57 @@ var storefrontOfferSchema = import_zod3.z.object({
|
|
|
318
358
|
...rest,
|
|
319
359
|
targets: targets != null ? targets : legacyOfferTargets(target, targetVariations)
|
|
320
360
|
}));
|
|
361
|
+
var storefrontPromotionTargetSchema = import_zod3.z.object({
|
|
362
|
+
// A prefixed reference to the item the sale covers, `Product/<id>` or `Bundle/<id>`. The item
|
|
363
|
+
// itself is already serialized in `products`/`bundles`; embedding it again would duplicate the
|
|
364
|
+
// bulk of the payload for a sale spanning dozens of products.
|
|
365
|
+
referenceId: import_zod3.z.string(),
|
|
366
|
+
// The pricing variation IDs the sale is restricted to; an empty list means all variations.
|
|
367
|
+
variations: import_zod3.z.string().array()
|
|
368
|
+
});
|
|
369
|
+
var storefrontPromotionCtaSchema = import_zod3.z.object({
|
|
370
|
+
url: import_zod3.z.string(),
|
|
371
|
+
label: import_zod3.z.string()
|
|
372
|
+
});
|
|
373
|
+
var storefrontPromotionSchema = import_zod3.z.object({
|
|
374
|
+
id: import_zod3.z.string(),
|
|
375
|
+
name: import_zod3.z.string(),
|
|
376
|
+
description: import_zod3.z.string().nullish(),
|
|
377
|
+
imageUrl: import_zod3.z.string().nullish(),
|
|
378
|
+
// Absent when the merchant has not set one, in which case the storefront decides whether the
|
|
379
|
+
// sale leads anywhere.
|
|
380
|
+
cta: storefrontPromotionCtaSchema.nullish(),
|
|
381
|
+
// How the merchant wants the sale surfaced, e.g. `['Banner', 'Popup']`; an empty list means no
|
|
382
|
+
// preference. Kept as free-form strings and narrowed with `isPromotionDisplay`, so a display
|
|
383
|
+
// mode added server-side later is ignored rather than failing the whole storefront.
|
|
384
|
+
display: import_zod3.z.string().array(),
|
|
385
|
+
// The promotion's *intended* scope, not what is actually showing the sale price: each variation
|
|
386
|
+
// displays only its single best discount, so a targeted item may be showing a better one. The
|
|
387
|
+
// authoritative signal is `promotionId` on a variation's own discount.
|
|
388
|
+
targets: storefrontPromotionTargetSchema.array(),
|
|
389
|
+
// The promotion's own discount, for banner copy ("20% off"). Per-item prices are already
|
|
390
|
+
// discounted on each variation.
|
|
391
|
+
discount: discountSchema,
|
|
392
|
+
validFrom: import_zod3.z.string().nullish(),
|
|
393
|
+
validUntil: import_zod3.z.string().nullish(),
|
|
394
|
+
properties: import_zod3.z.record(rawPropertyValueSchema).nullish()
|
|
395
|
+
});
|
|
396
|
+
var storefrontOffersSchema = import_zod3.z.array(import_zod3.z.unknown()).transform((offers) => offers.flatMap((offer) => {
|
|
397
|
+
const parsed = storefrontOfferSchema.safeParse(offer);
|
|
398
|
+
if (!parsed.success) {
|
|
399
|
+
console.warn("Ignoring storefront offer that could not be parsed:", parsed.error.issues);
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
402
|
+
return [parsed.data];
|
|
403
|
+
}));
|
|
404
|
+
var storefrontPromotionsSchema = import_zod3.z.array(import_zod3.z.unknown()).transform((promotions) => promotions.flatMap((promotion) => {
|
|
405
|
+
const parsed = storefrontPromotionSchema.safeParse(promotion);
|
|
406
|
+
if (!parsed.success) {
|
|
407
|
+
console.warn("Ignoring storefront promotion that could not be parsed:", parsed.error.issues);
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
return [parsed.data];
|
|
411
|
+
}));
|
|
321
412
|
var taxEstimateSchema = import_zod3.z.object({
|
|
322
413
|
rate: import_zod3.z.number(),
|
|
323
414
|
mode: import_zod3.z.enum(["Exclusive", "Inclusive"]),
|
|
@@ -331,7 +422,9 @@ var storefrontSchema = import_zod3.z.object({
|
|
|
331
422
|
products: storefrontProductSchema.array(),
|
|
332
423
|
bundles: storefrontBundleSchema.array(),
|
|
333
424
|
// Offers need to be optional since we may still have old, cached representations in browsers
|
|
334
|
-
offers:
|
|
425
|
+
offers: storefrontOffersSchema.optional(),
|
|
426
|
+
// Promotions need to be optional since we may still have old, cached representations in browsers
|
|
427
|
+
promotions: storefrontPromotionsSchema.optional(),
|
|
335
428
|
// Depending on circumstances, we may have a tax estimate available
|
|
336
429
|
estimatedTax: taxEstimateSchema.optional()
|
|
337
430
|
});
|
|
@@ -1077,6 +1170,7 @@ var import_zod13 = require("zod");
|
|
|
1077
1170
|
var schemas_exports7 = {};
|
|
1078
1171
|
__export(schemas_exports7, {
|
|
1079
1172
|
completedOrderSchema: () => completedOrderSchema,
|
|
1173
|
+
lineItemTotalSchema: () => lineItemTotalSchema,
|
|
1080
1174
|
openBundleLineItem: () => openBundleLineItem,
|
|
1081
1175
|
openOrderLineItem: () => openOrderLineItem,
|
|
1082
1176
|
openOrderSchema: () => openOrderSchema,
|
|
@@ -1112,7 +1206,10 @@ var lineItemTotalSchema = import_zod12.z.object({
|
|
|
1112
1206
|
original: moneySchema,
|
|
1113
1207
|
discount: moneySchema,
|
|
1114
1208
|
subtotal: moneySchema,
|
|
1115
|
-
due: moneySchema
|
|
1209
|
+
due: moneySchema,
|
|
1210
|
+
// This line's per-unit share of the order's cart offer, kept apart from the offer the buyer picked
|
|
1211
|
+
// for this item. Both are part of `discount`. Absent on orders priced before cart offers existed.
|
|
1212
|
+
cartOfferDiscount: moneySchema.optional()
|
|
1116
1213
|
});
|
|
1117
1214
|
var openProductLineItem = import_zod12.z.object({
|
|
1118
1215
|
id: import_zod12.z.string(),
|
|
@@ -1164,7 +1261,10 @@ var orderTotalSchema = import_zod12.z.object({
|
|
|
1164
1261
|
discount: moneySchema,
|
|
1165
1262
|
subtotal: moneySchema,
|
|
1166
1263
|
taxes: moneySchema,
|
|
1167
|
-
due: moneySchema
|
|
1264
|
+
due: moneySchema,
|
|
1265
|
+
// How much of `discount` came from the cart offer, so it can be shown as one order-wide saving
|
|
1266
|
+
// rather than a share on every line. Absent when no cart offer applied.
|
|
1267
|
+
cartOfferDiscount: moneySchema.optional()
|
|
1168
1268
|
});
|
|
1169
1269
|
var openOrderSchema = import_zod12.z.object({
|
|
1170
1270
|
id: import_zod12.z.string(),
|
|
@@ -1172,6 +1272,12 @@ var openOrderSchema = import_zod12.z.object({
|
|
|
1172
1272
|
total: orderTotalSchema.nullish(),
|
|
1173
1273
|
items: openOrderLineItem.array(),
|
|
1174
1274
|
couponsApplied: couponSchema.array(),
|
|
1275
|
+
// The cart offer on the order as a whole: the buyer's intent. It is part of the cart payload, so
|
|
1276
|
+
// a content push that leaves it out clears it. The API echoes it back even while the cart sits
|
|
1277
|
+
// below the offer's threshold.
|
|
1278
|
+
offerId: import_zod12.z.string().optional(),
|
|
1279
|
+
// The cart offer as actually applied: the result. Absent while the offer's condition doesn't hold.
|
|
1280
|
+
appliedOffer: discountSchema.optional(),
|
|
1175
1281
|
checkoutUrl: import_zod12.z.string().optional(),
|
|
1176
1282
|
hostedCheckoutUrl: import_zod12.z.string().optional(),
|
|
1177
1283
|
embeddedCheckoutUrl: import_zod12.z.string().optional()
|
|
@@ -1191,7 +1297,8 @@ var completedOrderSchema = import_zod12.z.object({
|
|
|
1191
1297
|
customer: customerSnapshotSchema,
|
|
1192
1298
|
total: orderTotalSchema,
|
|
1193
1299
|
items: openOrderLineItem.array(),
|
|
1194
|
-
couponsApplied: couponSchema.array()
|
|
1300
|
+
couponsApplied: couponSchema.array(),
|
|
1301
|
+
appliedOffer: discountSchema.optional()
|
|
1195
1302
|
});
|
|
1196
1303
|
var orderSchema = import_zod12.z.discriminatedUnion("status", [
|
|
1197
1304
|
openOrderSchema.extend({
|
|
@@ -1327,6 +1434,9 @@ var OrderEndpoints = class {
|
|
|
1327
1434
|
if (options.checkout.returnUrl) {
|
|
1328
1435
|
query.append("returnUrl", options.checkout.returnUrl);
|
|
1329
1436
|
}
|
|
1437
|
+
if (options.checkout.embedded !== void 0) {
|
|
1438
|
+
query.append("embedded", options.checkout.embedded ? "true" : "false");
|
|
1439
|
+
}
|
|
1330
1440
|
}
|
|
1331
1441
|
const response = await this.api.fetch(`/api/customer/orders/${orderId}?${query.toString()}`, orderSchema, {
|
|
1332
1442
|
abort: options == null ? void 0 : options.abort
|
|
@@ -1341,6 +1451,8 @@ var OrderEndpoints = class {
|
|
|
1341
1451
|
query.checkout = "true";
|
|
1342
1452
|
if (checkout.returnUrl)
|
|
1343
1453
|
query.returnUrl = checkout.returnUrl;
|
|
1454
|
+
if (checkout.embedded !== void 0)
|
|
1455
|
+
query.embedded = checkout.embedded ? "true" : "false";
|
|
1344
1456
|
}
|
|
1345
1457
|
const response = await this.api.fetch(
|
|
1346
1458
|
`/api/customer/orders/${order.id}?${new URLSearchParams(query).toString()}`,
|
|
@@ -1349,6 +1461,7 @@ var OrderEndpoints = class {
|
|
|
1349
1461
|
method: "PATCH",
|
|
1350
1462
|
body: {
|
|
1351
1463
|
currency: order.currency || void 0,
|
|
1464
|
+
offerId: order.offerId,
|
|
1352
1465
|
items: order.items
|
|
1353
1466
|
}
|
|
1354
1467
|
}
|
|
@@ -1363,6 +1476,12 @@ var OrderEndpoints = class {
|
|
|
1363
1476
|
const response = await this.api.fetch(`/api/customer/orders/${orderId}/billing-details`, openOrderSchema, { method: "DELETE" });
|
|
1364
1477
|
return response.data;
|
|
1365
1478
|
}
|
|
1479
|
+
// Takes the cart offer off the order without re-sending the cart. Offers picked for a single item
|
|
1480
|
+
// come off by pushing that item without its `offerId` instead.
|
|
1481
|
+
async removeOffer(orderId) {
|
|
1482
|
+
const response = await this.api.fetch(`/api/customer/orders/${orderId}/offer`, openOrderSchema, { method: "DELETE" });
|
|
1483
|
+
return response.data;
|
|
1484
|
+
}
|
|
1366
1485
|
async addCouponCode(orderId, code) {
|
|
1367
1486
|
const response = await this.api.fetch(`/api/customer/orders/${orderId}/coupon?code=${encodeURIComponent(code)}`, openOrderSchema, { method: "PATCH" });
|
|
1368
1487
|
return response.data;
|
|
@@ -1667,6 +1786,12 @@ __export(schemas_exports11, {
|
|
|
1667
1786
|
subscriptions: () => schemas_exports8
|
|
1668
1787
|
});
|
|
1669
1788
|
|
|
1789
|
+
// src/storefront/models.ts
|
|
1790
|
+
var promotionDisplays = ["Banner", "Popup"];
|
|
1791
|
+
function isPromotionDisplay(value) {
|
|
1792
|
+
return promotionDisplays.includes(value);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1670
1795
|
// src/utils/discount.ts
|
|
1671
1796
|
var DiscountUtils = class {
|
|
1672
1797
|
static apply(discount, price) {
|
|
@@ -1693,25 +1818,68 @@ var MoneyCollectionUtils = class {
|
|
|
1693
1818
|
};
|
|
1694
1819
|
|
|
1695
1820
|
// src/utils/offer.ts
|
|
1821
|
+
function evaluated(unit, currency, current, target, eligible) {
|
|
1822
|
+
return {
|
|
1823
|
+
eligible,
|
|
1824
|
+
progress: {
|
|
1825
|
+
unit,
|
|
1826
|
+
currency,
|
|
1827
|
+
current,
|
|
1828
|
+
target,
|
|
1829
|
+
remaining: Math.max(0, target - current),
|
|
1830
|
+
fraction: target <= 0 ? 1 : Math.min(1, Math.max(0, current / target)),
|
|
1831
|
+
eligible
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1696
1835
|
var OfferUtils = class _OfferUtils {
|
|
1697
|
-
|
|
1698
|
-
|
|
1836
|
+
// Callers that only have an order keep working and simply have no cart total, so a monetary
|
|
1837
|
+
// condition fails closed for them. Pass the full context to have `CartTotal` actually evaluate.
|
|
1838
|
+
static eligible(offer, context) {
|
|
1839
|
+
return _OfferUtils.evaluate(offer, context).eligible;
|
|
1840
|
+
}
|
|
1841
|
+
// How close the cart is to satisfying the offer, or `null` when there is nothing to show progress
|
|
1842
|
+
// toward: an unsupported condition, an unknown cart total, or a condition with only an upper
|
|
1843
|
+
// bound (which the buyer starts out satisfying rather than working toward).
|
|
1844
|
+
static progress(offer, context) {
|
|
1845
|
+
return _OfferUtils.evaluate(offer, context).progress;
|
|
1846
|
+
}
|
|
1847
|
+
// The single evaluator behind both faces, so the "spend €12 more" nudge and the gate that unlocks
|
|
1848
|
+
// the discount can never disagree about the same cart.
|
|
1849
|
+
static evaluate(offer, context) {
|
|
1850
|
+
var _a, _b;
|
|
1851
|
+
const { order, cartTotal } = "items" in context ? { order: context, cartTotal: void 0 } : context;
|
|
1852
|
+
const condition = offer.condition;
|
|
1853
|
+
switch (condition.type) {
|
|
1699
1854
|
case "CartContainsItems": {
|
|
1700
1855
|
const relevantItems = order.items.filter(
|
|
1701
|
-
(i) => i.type === "Product" &&
|
|
1856
|
+
(i) => i.type === "Product" && condition.relevantItemVariations[`Product/${i.productId}`] && condition.relevantItemVariations[`Product/${i.productId}`].includes(i.variationId) || i.type === "Bundle" && condition.relevantItemVariations[`Bundle/${i.bundleId}`] && condition.relevantItemVariations[`Bundle/${i.bundleId}`].includes(i.variationId)
|
|
1702
1857
|
);
|
|
1703
1858
|
const relevantReplacedItems = order.items.flatMap((i) => {
|
|
1704
|
-
var
|
|
1705
|
-
return offer.id === i.offerId && i.type === "Bundle" && _OfferUtils.targetContainsBundle(offer, i.id) ? (
|
|
1859
|
+
var _a2;
|
|
1860
|
+
return offer.id === i.offerId && i.type === "Bundle" && _OfferUtils.targetContainsBundle(offer, i.id) ? (_a2 = i.replaced) != null ? _a2 : [] : [];
|
|
1706
1861
|
}).filter((ref) => {
|
|
1707
1862
|
const [productId, variationId] = ref.split("/");
|
|
1708
|
-
return
|
|
1863
|
+
return condition.relevantItemVariations[`Product/${productId}`] && condition.relevantItemVariations[`Product/${productId}`].includes(variationId);
|
|
1709
1864
|
});
|
|
1710
|
-
|
|
1865
|
+
const current = relevantItems.length + relevantReplacedItems.length;
|
|
1866
|
+
return evaluated("Items", void 0, current, condition.minimumItems, current >= condition.minimumItems);
|
|
1867
|
+
}
|
|
1868
|
+
case "CartTotal": {
|
|
1869
|
+
if (!cartTotal)
|
|
1870
|
+
return { eligible: false, progress: null };
|
|
1871
|
+
const minimum = (_a = condition.minimumTotal) == null ? void 0 : _a[cartTotal.currency];
|
|
1872
|
+
const maximum = (_b = condition.maximumTotal) == null ? void 0 : _b[cartTotal.currency];
|
|
1873
|
+
if (minimum === void 0 && maximum === void 0)
|
|
1874
|
+
return { eligible: false, progress: null };
|
|
1875
|
+
const eligible = (minimum === void 0 || cartTotal.amount >= minimum) && (maximum === void 0 || cartTotal.amount <= maximum);
|
|
1876
|
+
if (minimum === void 0)
|
|
1877
|
+
return { eligible, progress: null };
|
|
1878
|
+
return evaluated("Money", cartTotal.currency, cartTotal.amount, minimum, eligible);
|
|
1711
1879
|
}
|
|
1712
1880
|
}
|
|
1713
|
-
console.warn("Unsupported offer condition found:",
|
|
1714
|
-
return false;
|
|
1881
|
+
console.warn("Unsupported offer condition found:", condition);
|
|
1882
|
+
return { eligible: false, progress: null };
|
|
1715
1883
|
}
|
|
1716
1884
|
// The relevant targets of an offer with more than a single target needs to:
|
|
1717
1885
|
// a. Not be owned, or all targets need to be owned
|
|
@@ -1719,10 +1887,29 @@ var OfferUtils = class _OfferUtils {
|
|
|
1719
1887
|
static relevantTargets(offer, cartItems) {
|
|
1720
1888
|
if (offer.targets.length > 1) {
|
|
1721
1889
|
const allOwned = offer.targets.every((t) => t.item.owned);
|
|
1722
|
-
return offer.targets.filter((t) => (allOwned || !t.item.owned) && !
|
|
1890
|
+
return offer.targets.filter((t) => (allOwned || !t.item.owned) && !_OfferUtils.targetInCart(t, cartItems));
|
|
1723
1891
|
}
|
|
1724
1892
|
return offer.targets;
|
|
1725
1893
|
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Whether every item an offer targets is already in the cart, so the offer has no upsell left to
|
|
1896
|
+
* show.
|
|
1897
|
+
*
|
|
1898
|
+
* This is the set of offers the cart hides: a card inviting the buyer to add what they have already
|
|
1899
|
+
* added says nothing, so it is dropped. Which makes it exactly the set whose discount nothing can
|
|
1900
|
+
* currently claim: an offer is only good for one line, and there is no card left to spend it from.
|
|
1901
|
+
*
|
|
1902
|
+
* An offer with no targets names nothing and so is never in this set.
|
|
1903
|
+
*/
|
|
1904
|
+
static allTargetsInCart(offer, cartItems) {
|
|
1905
|
+
return offer.targets.length > 0 && offer.targets.every((target) => _OfferUtils.targetInCart(target, cartItems));
|
|
1906
|
+
}
|
|
1907
|
+
// Matched on the item alone, not the variation: a target narrowed to one variation still counts as
|
|
1908
|
+
// in the cart when another variation of it is. That is the reading the cart's own upsell has always
|
|
1909
|
+
// taken, and the two have to agree or an offer could be hidden and auto-applied at once.
|
|
1910
|
+
static targetInCart(target, cartItems) {
|
|
1911
|
+
return cartItems.some((l) => l.type === target.item.type && target.item.id === (l.type === "Product" ? l.productId : l.bundleId));
|
|
1912
|
+
}
|
|
1726
1913
|
// The pricing variations a target restricts the offer to. An empty (or missing) variation list
|
|
1727
1914
|
// means the offer is not narrowed to specific variations, in which case we keep the established
|
|
1728
1915
|
// single default-variation upsell.
|
|
@@ -1752,6 +1939,428 @@ var OfferUtils = class _OfferUtils {
|
|
|
1752
1939
|
}
|
|
1753
1940
|
};
|
|
1754
1941
|
|
|
1942
|
+
// src/utils/cart.ts
|
|
1943
|
+
var _CartUtils = class _CartUtils {
|
|
1944
|
+
// The pricing variation a line item points at, falling back to the item's default variation when
|
|
1945
|
+
// the variation id doesn't resolve, which is what the cart has always displayed.
|
|
1946
|
+
static resolveVariation(catalog, item) {
|
|
1947
|
+
var _a, _b;
|
|
1948
|
+
const source = item.type === "Product" ? catalog.products.find((p) => p.id === item.productId) : catalog.bundles.find((b) => b.id === item.bundleId);
|
|
1949
|
+
return (_b = (_a = source == null ? void 0 : source.variations) == null ? void 0 : _a.find((v) => v.id === item.variationId)) != null ? _b : source == null ? void 0 : source.defaultVariation;
|
|
1950
|
+
}
|
|
1951
|
+
// A line's unit price after its product discount and any pricing tier, and *before* any offer.
|
|
1952
|
+
// This is the basis the API measures cart total conditions on, which is why it reads the catalog
|
|
1953
|
+
// rather than the line item's own `price`: `enrichLineItems` rewrites that to the offer-discounted
|
|
1954
|
+
// price, and reading it here would take the offer off twice.
|
|
1955
|
+
static unitPrice(catalog, item, currency) {
|
|
1956
|
+
var _a;
|
|
1957
|
+
return (_a = _CartUtils.unitPricing(catalog, item, currency)) == null ? void 0 : _a.price;
|
|
1958
|
+
}
|
|
1959
|
+
// A line's list and post-product-discount unit prices, from whichever pricing tier applies. Both
|
|
1960
|
+
// come from the same tier so they can be subtracted from one another: taking the list price off one
|
|
1961
|
+
// tier and the paid price off another would invent a discount neither of them offers.
|
|
1962
|
+
static unitPricing(catalog, item, currency) {
|
|
1963
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1964
|
+
const variation = _CartUtils.resolveVariation(catalog, item);
|
|
1965
|
+
const price = variation == null ? void 0 : variation.price[currency];
|
|
1966
|
+
if (price === void 0 || variation === void 0)
|
|
1967
|
+
return void 0;
|
|
1968
|
+
const eligiblePricingTiers = (_b = (_a = variation.pricingTiers) == null ? void 0 : _a.filter((tier) => item.quantity >= tier.minQuantity)) != null ? _b : [];
|
|
1969
|
+
const matchingPricingTier = eligiblePricingTiers.length > 0 ? eligiblePricingTiers[eligiblePricingTiers.length - 1] : null;
|
|
1970
|
+
const tierPrice = matchingPricingTier == null ? void 0 : matchingPricingTier.price[currency];
|
|
1971
|
+
if (tierPrice !== void 0 && tierPrice < price) {
|
|
1972
|
+
return {
|
|
1973
|
+
price: tierPrice,
|
|
1974
|
+
originalPrice: (_d = (_c = matchingPricingTier == null ? void 0 : matchingPricingTier.originalPrice) == null ? void 0 : _c[currency]) != null ? _d : tierPrice
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
return { price, originalPrice: (_f = (_e = variation.originalPrice) == null ? void 0 : _e[currency]) != null ? _f : price };
|
|
1978
|
+
}
|
|
1979
|
+
// The number the API measures cart conditions against: each line's price after its product
|
|
1980
|
+
// discount, times quantity, before offers and coupons. `undefined` when any line's price can't be
|
|
1981
|
+
// resolved in `currency`. One unreadable line makes the whole measurement a guess, and a guess
|
|
1982
|
+
// must not unlock an offer.
|
|
1983
|
+
static measure(catalog, items, currency) {
|
|
1984
|
+
if (!currency)
|
|
1985
|
+
return void 0;
|
|
1986
|
+
let amount = 0;
|
|
1987
|
+
for (const item of items) {
|
|
1988
|
+
const unitPrice = _CartUtils.unitPrice(catalog, item, currency);
|
|
1989
|
+
if (unitPrice === void 0)
|
|
1990
|
+
return void 0;
|
|
1991
|
+
amount += unitPrice * item.quantity;
|
|
1992
|
+
}
|
|
1993
|
+
return { currency, amount };
|
|
1994
|
+
}
|
|
1995
|
+
static conditionContext(catalog, order, currency) {
|
|
1996
|
+
return { order, cartTotal: _CartUtils.measure(catalog, order.items, currency) };
|
|
1997
|
+
}
|
|
1998
|
+
// Which lines a cart offer discounts. No targets means the whole cart, a reading that only holds
|
|
1999
|
+
// for cart-scope offers, where an empty target list is the API's way of saying "every line".
|
|
2000
|
+
static cartOfferAppliesToLine(offer, item) {
|
|
2001
|
+
return offer.targets.length === 0 || _CartUtils.offerTargetsLine(offer, item);
|
|
2002
|
+
}
|
|
2003
|
+
// Whether an offer names this line among its targets. An empty variation list on a target leaves
|
|
2004
|
+
// the offer open to every variation of that item. An offer with no targets at all names nothing
|
|
2005
|
+
// here, which is why the "every line" reading stays in `cartOfferAppliesToLine`: it is only true
|
|
2006
|
+
// of a cart offer.
|
|
2007
|
+
static offerTargetsLine(offer, item) {
|
|
2008
|
+
return offer.targets.some(({ item: target, variations }) => target.type === item.type && target.id === (item.type === "Product" ? item.productId : item.bundleId) && (variations.length === 0 || variations.includes(item.variationId)));
|
|
2009
|
+
}
|
|
2010
|
+
// The cart offer actually discounting this cart, as opposed to the one attached to the order. An
|
|
2011
|
+
// offer whose targets name nothing in the cart discounts nothing however healthy the total looks,
|
|
2012
|
+
// so it has not really applied.
|
|
2013
|
+
static appliedCartOffer(catalog, order, context) {
|
|
2014
|
+
var _a;
|
|
2015
|
+
if (!order.offerId)
|
|
2016
|
+
return void 0;
|
|
2017
|
+
const offer = (_a = catalog.offers) == null ? void 0 : _a.find((o) => o.id === order.offerId);
|
|
2018
|
+
if (!offer || offer.scope !== "Cart" || !OfferUtils.eligible(offer, context))
|
|
2019
|
+
return void 0;
|
|
2020
|
+
return order.items.some((item) => _CartUtils.cartOfferAppliesToLine(offer, item)) ? offer : void 0;
|
|
2021
|
+
}
|
|
2022
|
+
// What the cart is worth to the buyer, broken down so the cart can show the saving on its own row.
|
|
2023
|
+
// Unlike `measure`, an unresolvable price counts as zero here: this number fills a label, so a
|
|
2024
|
+
// half-loaded catalog should still render a total rather than nothing.
|
|
2025
|
+
static total(catalog, order, currency) {
|
|
2026
|
+
var _a, _b;
|
|
2027
|
+
const context = _CartUtils.conditionContext(catalog, order, currency);
|
|
2028
|
+
const cartOffer = _CartUtils.appliedCartOffer(catalog, order, context);
|
|
2029
|
+
let original = 0;
|
|
2030
|
+
let subtotal = 0;
|
|
2031
|
+
let cartOfferDiscount = 0;
|
|
2032
|
+
for (const item of order.items) {
|
|
2033
|
+
const unitPrice = _CartUtils.unitPriceAfterItemOffer(catalog, item, currency, context);
|
|
2034
|
+
original += ((_b = (_a = _CartUtils.unitPricing(catalog, item, currency)) == null ? void 0 : _a.originalPrice) != null ? _b : unitPrice) * item.quantity;
|
|
2035
|
+
subtotal += unitPrice * item.quantity;
|
|
2036
|
+
if (cartOffer && _CartUtils.cartOfferAppliesToLine(cartOffer, item))
|
|
2037
|
+
cartOfferDiscount += DiscountUtils.apply(cartOffer.discount, { [currency]: unitPrice })[currency] * item.quantity;
|
|
2038
|
+
}
|
|
2039
|
+
const amount = subtotal - cartOfferDiscount;
|
|
2040
|
+
return {
|
|
2041
|
+
currency,
|
|
2042
|
+
original,
|
|
2043
|
+
subtotal,
|
|
2044
|
+
cartOffer: cartOffer ? { offer: cartOffer, discount: cartOfferDiscount } : null,
|
|
2045
|
+
discount: original - amount,
|
|
2046
|
+
amount
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
// The cart offer worth attaching to the order. Cart offers are always percentages, but 10% off
|
|
2050
|
+
// everything can still beat 50% off one cheap line, so they are ranked by what they would actually
|
|
2051
|
+
// take off this cart rather than by percentage.
|
|
2052
|
+
static bestCartOffer(catalog, order, currency) {
|
|
2053
|
+
var _a;
|
|
2054
|
+
const context = _CartUtils.conditionContext(catalog, order, currency);
|
|
2055
|
+
let best;
|
|
2056
|
+
for (const offer of (_a = catalog.offers) != null ? _a : []) {
|
|
2057
|
+
if (offer.scope !== "Cart" || !OfferUtils.eligible(offer, context))
|
|
2058
|
+
continue;
|
|
2059
|
+
const value = _CartUtils.cartOfferValue(catalog, order, offer, currency, context);
|
|
2060
|
+
if (value <= 0)
|
|
2061
|
+
continue;
|
|
2062
|
+
if (!best || value > best.value || value === best.value && offer.id === order.offerId)
|
|
2063
|
+
best = { offer, value };
|
|
2064
|
+
}
|
|
2065
|
+
return best == null ? void 0 : best.offer;
|
|
2066
|
+
}
|
|
2067
|
+
/**
|
|
2068
|
+
* What a cart offer would take off this cart, whether or not its condition is met yet.
|
|
2069
|
+
*
|
|
2070
|
+
* Computed on the same post-line-offer price `total` discounts, so "which offer do we attach" and
|
|
2071
|
+
* "what does the cart actually save" can never disagree. Ranking on the list price instead would
|
|
2072
|
+
* over-value an offer aimed at a line that already carries a steep offer of its own: 50% of a
|
|
2073
|
+
* line discounted to a tenth of its price is worth less than 10% of a full-price one.
|
|
2074
|
+
*/
|
|
2075
|
+
static cartOfferValue(catalog, order, offer, currency, context) {
|
|
2076
|
+
const conditionContext = context != null ? context : _CartUtils.conditionContext(catalog, order, currency);
|
|
2077
|
+
return order.items.reduce((aggregate, item) => {
|
|
2078
|
+
if (!_CartUtils.cartOfferAppliesToLine(offer, item))
|
|
2079
|
+
return aggregate;
|
|
2080
|
+
const unitPrice = _CartUtils.unitPriceAfterItemOffer(catalog, item, currency, conditionContext);
|
|
2081
|
+
return aggregate + DiscountUtils.apply(offer.discount, { [currency]: unitPrice })[currency] * item.quantity;
|
|
2082
|
+
}, 0);
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* The item-scope offer worth attaching to a single line, or `undefined` when none is.
|
|
2086
|
+
*
|
|
2087
|
+
* Ranked by what it would actually take off this line rather than by percentage, the same way
|
|
2088
|
+
* `bestCartOffer` ranks a cart: a flat amount off can beat a bigger-looking percentage on a cheap
|
|
2089
|
+
* line, and the other way round on an expensive one.
|
|
2090
|
+
*
|
|
2091
|
+
* `unavailableOfferIds` are offers the rest of the cart has already claimed. An offer is only ever
|
|
2092
|
+
* good for one line, since the cart treats an offer held by any line as in use. So a caller walking
|
|
2093
|
+
* the cart must reserve each one as it goes, or two lines would both count on the same offer and
|
|
2094
|
+
* the API would honour it once. `assignItemOffers` does that bookkeeping for a whole cart.
|
|
2095
|
+
*/
|
|
2096
|
+
static bestItemOffer(catalog, item, currency, context, unavailableOfferIds) {
|
|
2097
|
+
let best;
|
|
2098
|
+
for (const candidate of _CartUtils.itemOfferCandidates(catalog, [item], currency, context, unavailableOfferIds)) {
|
|
2099
|
+
if (!best || candidate.value > best.value)
|
|
2100
|
+
best = candidate;
|
|
2101
|
+
}
|
|
2102
|
+
return best == null ? void 0 : best.offer;
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Which item-scope offer to attach to which line, for the lines not already carrying one.
|
|
2106
|
+
*
|
|
2107
|
+
* Chosen for what the pairings are worth together rather than by cart order. An offer is only good
|
|
2108
|
+
* for one line, so walking the cart and taking the best offer for each line in turn hands a
|
|
2109
|
+
* multi-target offer to whichever line the buyer happened to add first, which is arbitrary and can
|
|
2110
|
+
* be the cheapest one. A buyer claiming these from the upsell cards would have placed them where
|
|
2111
|
+
* they are worth most, so that is what this does.
|
|
2112
|
+
*
|
|
2113
|
+
* Lines already holding an offer are left alone and their offers reserved: an offer on a line is
|
|
2114
|
+
* intent, whether the buyer's from the upsell or the API's from a refreshed order, and this is not
|
|
2115
|
+
* the place to second-guess it.
|
|
2116
|
+
*/
|
|
2117
|
+
static assignItemOffers(catalog, order, currency, context) {
|
|
2118
|
+
const conditionContext = context != null ? context : _CartUtils.conditionContext(catalog, order, currency);
|
|
2119
|
+
const claimed = new Set(order.items.map((i) => i.offerId).filter((id) => !!id));
|
|
2120
|
+
return _CartUtils.maximalItemOfferAssignment(
|
|
2121
|
+
_CartUtils.itemOfferCandidates(catalog, order.items.filter((i) => !i.offerId), currency, conditionContext, claimed)
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* The pairings worth the most together, given each offer can only be spent once.
|
|
2126
|
+
*
|
|
2127
|
+
* Taking the most valuable pairing first is not enough: with one offer worth 100 on line A and 99 on
|
|
2128
|
+
* line B, and a second offer worth 98 on line A alone, claiming the 100 strands the second offer and
|
|
2129
|
+
* saves 100, where putting the first offer on B and the second on A saves 197. The pairings have to
|
|
2130
|
+
* be chosen as a set.
|
|
2131
|
+
*
|
|
2132
|
+
* Walked line by line, memoised on which offers are still free. The candidates have already been
|
|
2133
|
+
* through targeting and eligibility, so in a real cart there are a handful; past `searchableOffers`
|
|
2134
|
+
* distinct offers it takes the most valuable pairing first instead, which is never worse than
|
|
2135
|
+
* applying nothing.
|
|
2136
|
+
*/
|
|
2137
|
+
static maximalItemOfferAssignment(candidates) {
|
|
2138
|
+
var _a;
|
|
2139
|
+
const optionsByItem = /* @__PURE__ */ new Map();
|
|
2140
|
+
const offerBits = /* @__PURE__ */ new Map();
|
|
2141
|
+
for (const { item, offer, value } of candidates) {
|
|
2142
|
+
let bit = offerBits.get(offer.id);
|
|
2143
|
+
if (bit === void 0) {
|
|
2144
|
+
bit = offerBits.size;
|
|
2145
|
+
offerBits.set(offer.id, bit);
|
|
2146
|
+
}
|
|
2147
|
+
const options = (_a = optionsByItem.get(item.id)) != null ? _a : [];
|
|
2148
|
+
options.push({ offerId: offer.id, bit, value });
|
|
2149
|
+
optionsByItem.set(item.id, options);
|
|
2150
|
+
}
|
|
2151
|
+
if (offerBits.size > _CartUtils.searchableOffers)
|
|
2152
|
+
return _CartUtils.mostValuableItemOfferPairings(candidates);
|
|
2153
|
+
const itemIds = [...optionsByItem.keys()];
|
|
2154
|
+
const memo = /* @__PURE__ */ new Map();
|
|
2155
|
+
const from = (index, spent) => {
|
|
2156
|
+
var _a2;
|
|
2157
|
+
if (index >= itemIds.length)
|
|
2158
|
+
return { value: 0, assignments: [] };
|
|
2159
|
+
const key = `${index}:${spent}`;
|
|
2160
|
+
const memoised = memo.get(key);
|
|
2161
|
+
if (memoised)
|
|
2162
|
+
return memoised;
|
|
2163
|
+
let best = from(index + 1, spent);
|
|
2164
|
+
const itemId = itemIds[index];
|
|
2165
|
+
for (const option of (_a2 = optionsByItem.get(itemId)) != null ? _a2 : []) {
|
|
2166
|
+
if (spent & 1 << option.bit)
|
|
2167
|
+
continue;
|
|
2168
|
+
const rest = from(index + 1, spent | 1 << option.bit);
|
|
2169
|
+
const value = option.value + rest.value;
|
|
2170
|
+
if (value > best.value)
|
|
2171
|
+
best = { value, assignments: [{ itemId, offerId: option.offerId }, ...rest.assignments] };
|
|
2172
|
+
}
|
|
2173
|
+
memo.set(key, best);
|
|
2174
|
+
return best;
|
|
2175
|
+
};
|
|
2176
|
+
return from(0, 0).assignments;
|
|
2177
|
+
}
|
|
2178
|
+
// The fallback for a candidate set too large to search: repeatedly take the most valuable pairing
|
|
2179
|
+
// still available. Can strand a better combination, which is the whole reason it is only a fallback.
|
|
2180
|
+
static mostValuableItemOfferPairings(candidates) {
|
|
2181
|
+
const assignments = [];
|
|
2182
|
+
const assignedItemIds = /* @__PURE__ */ new Set();
|
|
2183
|
+
const spentOfferIds = /* @__PURE__ */ new Set();
|
|
2184
|
+
for (const { item, offer } of [...candidates].sort((a, b) => b.value - a.value)) {
|
|
2185
|
+
if (assignedItemIds.has(item.id) || spentOfferIds.has(offer.id))
|
|
2186
|
+
continue;
|
|
2187
|
+
assignedItemIds.add(item.id);
|
|
2188
|
+
spentOfferIds.add(offer.id);
|
|
2189
|
+
assignments.push({ itemId: item.id, offerId: offer.id });
|
|
2190
|
+
}
|
|
2191
|
+
return assignments;
|
|
2192
|
+
}
|
|
2193
|
+
// Every line-and-offer pairing the cart could make, and what each would save. Enumerated in cart
|
|
2194
|
+
// order and then catalog order, which `sort` preserves for equal savings, so an unchanged cart
|
|
2195
|
+
// always resolves the same way.
|
|
2196
|
+
static itemOfferCandidates(catalog, items, currency, context, unavailableOfferIds) {
|
|
2197
|
+
var _a;
|
|
2198
|
+
const candidates = [];
|
|
2199
|
+
for (const item of items) {
|
|
2200
|
+
const unitPrice = _CartUtils.unitPrice(catalog, item, currency);
|
|
2201
|
+
if (unitPrice === void 0)
|
|
2202
|
+
continue;
|
|
2203
|
+
for (const offer of (_a = catalog.offers) != null ? _a : []) {
|
|
2204
|
+
if (offer.scope === "Cart" || (unavailableOfferIds == null ? void 0 : unavailableOfferIds.has(offer.id)))
|
|
2205
|
+
continue;
|
|
2206
|
+
if (!_CartUtils.offerTargetsLine(offer, item))
|
|
2207
|
+
continue;
|
|
2208
|
+
if (!OfferUtils.allTargetsInCart(offer, context.order.items))
|
|
2209
|
+
continue;
|
|
2210
|
+
if (!OfferUtils.eligible(offer, context))
|
|
2211
|
+
continue;
|
|
2212
|
+
const value = _CartUtils.itemOfferUnitDiscount(offer, unitPrice, currency) * item.quantity;
|
|
2213
|
+
if (value <= 0)
|
|
2214
|
+
continue;
|
|
2215
|
+
candidates.push({ item, offer, value });
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
return candidates;
|
|
2219
|
+
}
|
|
2220
|
+
// What an item offer takes off one unit of a line. Measured off the post-product-discount price,
|
|
2221
|
+
// which is what an item offer comes off in the API's order: product discount -> line offer -> cart
|
|
2222
|
+
// offer -> coupons.
|
|
2223
|
+
static itemOfferUnitDiscount(offer, unitPrice, currency) {
|
|
2224
|
+
return DiscountUtils.apply(offer.discount, { [currency]: unitPrice })[currency];
|
|
2225
|
+
}
|
|
2226
|
+
// A line's unit price once its own offer has come off, which is what a cart offer is applied to:
|
|
2227
|
+
// the API discounts in the order product discount -> line offer -> cart offer -> coupons. An
|
|
2228
|
+
// unresolvable price counts as zero, matching `total`'s "still render something" stance rather
|
|
2229
|
+
// than `measure`'s "refuse to guess".
|
|
2230
|
+
static unitPriceAfterItemOffer(catalog, item, currency, context) {
|
|
2231
|
+
var _a, _b;
|
|
2232
|
+
const unitPrice = (_a = _CartUtils.unitPrice(catalog, item, currency)) != null ? _a : 0;
|
|
2233
|
+
const itemOffer = item.offerId ? (_b = catalog.offers) == null ? void 0 : _b.find((o) => o.id === item.offerId) : void 0;
|
|
2234
|
+
if (!itemOffer || !OfferUtils.eligible(itemOffer, context))
|
|
2235
|
+
return unitPrice;
|
|
2236
|
+
return unitPrice - _CartUtils.itemOfferUnitDiscount(itemOffer, unitPrice, currency);
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
// How many distinct offers can compete for the same cart before the exact search is given up on.
|
|
2240
|
+
// 12 leaves 4096 states per line, which is nothing, and no real storefront has a dozen item offers
|
|
2241
|
+
// eligible against one cart at once.
|
|
2242
|
+
_CartUtils.searchableOffers = 12;
|
|
2243
|
+
var CartUtils = _CartUtils;
|
|
2244
|
+
|
|
2245
|
+
// src/utils/image.ts
|
|
2246
|
+
var imageWidths = [16, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048];
|
|
2247
|
+
var maxImageDimension = imageWidths[imageWidths.length - 1];
|
|
2248
|
+
var transformPathSegment = "_i";
|
|
2249
|
+
var maxCoverAspectRatio = 4;
|
|
2250
|
+
function isAssetUrl(url) {
|
|
2251
|
+
let parsed;
|
|
2252
|
+
try {
|
|
2253
|
+
parsed = new URL(url);
|
|
2254
|
+
} catch (e) {
|
|
2255
|
+
return false;
|
|
2256
|
+
}
|
|
2257
|
+
return parsed.hostname === "assets.moonbase.sh" || parsed.hostname.startsWith("assets.") && parsed.hostname.endsWith(".moonbase.sh");
|
|
2258
|
+
}
|
|
2259
|
+
function isRendition(url) {
|
|
2260
|
+
try {
|
|
2261
|
+
return new URL(url).pathname.startsWith(`/${transformPathSegment}/`);
|
|
2262
|
+
} catch (e) {
|
|
2263
|
+
return false;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
function isTransformableImageUrl(url) {
|
|
2267
|
+
return isAssetUrl(url) && !isRendition(url);
|
|
2268
|
+
}
|
|
2269
|
+
var warned = /* @__PURE__ */ new Set();
|
|
2270
|
+
function warnOnce(reason, url, message) {
|
|
2271
|
+
const key = `${reason}:${url}`;
|
|
2272
|
+
if (warned.has(key))
|
|
2273
|
+
return;
|
|
2274
|
+
warned.add(key);
|
|
2275
|
+
console.warn(message, url);
|
|
2276
|
+
}
|
|
2277
|
+
function snap(value) {
|
|
2278
|
+
var _a;
|
|
2279
|
+
if (value === void 0 || !Number.isFinite(value) || value <= 0)
|
|
2280
|
+
return void 0;
|
|
2281
|
+
return (_a = imageWidths.find((width) => value <= width)) != null ? _a : maxImageDimension;
|
|
2282
|
+
}
|
|
2283
|
+
var ImageUtils = class _ImageUtils {
|
|
2284
|
+
/**
|
|
2285
|
+
* Rewrites an asset URL to ask the CDN for a rendition at the given size.
|
|
2286
|
+
*
|
|
2287
|
+
* Returns the URL untouched — never a broken one — when there is nothing to do: a URL that
|
|
2288
|
+
* isn't a Moonbase asset, one that is already a rendition, an empty transform, or a `cover`
|
|
2289
|
+
* crop too extreme for the CDN to serve. Callers can apply it unconditionally.
|
|
2290
|
+
*/
|
|
2291
|
+
static transform(url, transform) {
|
|
2292
|
+
if (!url)
|
|
2293
|
+
return void 0;
|
|
2294
|
+
if (!isAssetUrl(url)) {
|
|
2295
|
+
warnOnce(
|
|
2296
|
+
"host",
|
|
2297
|
+
url,
|
|
2298
|
+
"Only images hosted by Moonbase can be resized on the fly. Serving this one at its original size; host it in Moonbase, or size it yourself before uploading:"
|
|
2299
|
+
);
|
|
2300
|
+
return url;
|
|
2301
|
+
}
|
|
2302
|
+
if (isRendition(url))
|
|
2303
|
+
return url;
|
|
2304
|
+
const query = _ImageUtils.toQuery(transform);
|
|
2305
|
+
if (query === null) {
|
|
2306
|
+
warnOnce(
|
|
2307
|
+
"aspect-ratio",
|
|
2308
|
+
url,
|
|
2309
|
+
`A 'cover' crop more lopsided than ${maxCoverAspectRatio}:1 is not served. Serving this image at its original size instead:`
|
|
2310
|
+
);
|
|
2311
|
+
return url;
|
|
2312
|
+
}
|
|
2313
|
+
if (query.length === 0)
|
|
2314
|
+
return url;
|
|
2315
|
+
const parsed = new URL(url);
|
|
2316
|
+
const separator = parsed.search ? "&" : "?";
|
|
2317
|
+
return `${parsed.origin}/${transformPathSegment}${parsed.pathname}${parsed.search}${separator}${query.map(([key, value]) => `${key}=${value}`).join("&")}`;
|
|
2318
|
+
}
|
|
2319
|
+
/**
|
|
2320
|
+
* A `srcset` covering the given pixel densities, so a retina screen gets the sharper rendition
|
|
2321
|
+
* without every screen paying for it. Pair it with a `src` built from the same transform.
|
|
2322
|
+
*/
|
|
2323
|
+
static srcSet(url, transform, ...scales) {
|
|
2324
|
+
if (!url || scales.length === 0)
|
|
2325
|
+
return void 0;
|
|
2326
|
+
return scales.map((scale) => `${_ImageUtils.transform(url, _ImageUtils.atScale(transform, scale))} ${scale}x`).join(", ");
|
|
2327
|
+
}
|
|
2328
|
+
/** The same transform at a different pixel density. */
|
|
2329
|
+
static atScale(transform, scale) {
|
|
2330
|
+
return {
|
|
2331
|
+
...transform,
|
|
2332
|
+
width: transform.width === void 0 ? void 0 : transform.width * scale,
|
|
2333
|
+
height: transform.height === void 0 ? void 0 : transform.height * scale
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
/**
|
|
2337
|
+
* The query parameters for a transform, in the CDN's own order, with defaults left out so two
|
|
2338
|
+
* equivalent transforms always produce the same URL — and therefore the same cache entry.
|
|
2339
|
+
* `null` when the CDN would refuse the transform outright.
|
|
2340
|
+
*/
|
|
2341
|
+
static toQuery(transform) {
|
|
2342
|
+
var _a, _b;
|
|
2343
|
+
const fit = (_a = transform.fit) != null ? _a : "contain";
|
|
2344
|
+
const width = snap(transform.width);
|
|
2345
|
+
const height = fit === "cover" ? (_b = snap(transform.height)) != null ? _b : width : snap(transform.height);
|
|
2346
|
+
if (fit === "cover" && width && height) {
|
|
2347
|
+
const ratio = width > height ? width / height : height / width;
|
|
2348
|
+
if (ratio > maxCoverAspectRatio)
|
|
2349
|
+
return null;
|
|
2350
|
+
}
|
|
2351
|
+
const query = [];
|
|
2352
|
+
if (width)
|
|
2353
|
+
query.push(["w", String(width)]);
|
|
2354
|
+
if (height && (fit === "cover" || transform.height !== void 0))
|
|
2355
|
+
query.push(["h", String(height)]);
|
|
2356
|
+
if (fit !== "contain")
|
|
2357
|
+
query.push(["fit", fit]);
|
|
2358
|
+
if (transform.format)
|
|
2359
|
+
query.push(["f", transform.format]);
|
|
2360
|
+
return query;
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
|
|
1755
2364
|
// src/utils/utm.ts
|
|
1756
2365
|
function parseOffSiteReferrer(rawReferrer, currentOrigin) {
|
|
1757
2366
|
if (!rawReferrer)
|
|
@@ -1799,10 +2408,12 @@ var MoonbaseClient = class {
|
|
|
1799
2408
|
ActivationRequestStatus,
|
|
1800
2409
|
ActivationStatus,
|
|
1801
2410
|
Architecture,
|
|
2411
|
+
CartUtils,
|
|
1802
2412
|
ConnectableAccountProvider,
|
|
1803
2413
|
ConsoleLogger,
|
|
1804
2414
|
CycleLength,
|
|
1805
2415
|
DiscountUtils,
|
|
2416
|
+
ImageUtils,
|
|
1806
2417
|
InMemoryStore,
|
|
1807
2418
|
LicenseStatus,
|
|
1808
2419
|
LocalStorageStore,
|
|
@@ -1820,9 +2431,14 @@ var MoonbaseClient = class {
|
|
|
1820
2431
|
Platform,
|
|
1821
2432
|
SubscriptionStatus,
|
|
1822
2433
|
TokenStore,
|
|
2434
|
+
imageWidths,
|
|
2435
|
+
isPromotionDisplay,
|
|
2436
|
+
isTransformableImageUrl,
|
|
2437
|
+
maxImageDimension,
|
|
1823
2438
|
objectToQuery,
|
|
1824
2439
|
parseOffSiteReferrer,
|
|
1825
2440
|
problemDetailsSchema,
|
|
2441
|
+
promotionDisplays,
|
|
1826
2442
|
resolveUtm,
|
|
1827
2443
|
schemas,
|
|
1828
2444
|
utmToObject
|