@moonbase.sh/storefront-api 3.0.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/dist/index.cjs +185 -5
- package/dist/index.d.cts +91 -6
- package/dist/index.d.ts +91 -6
- package/dist/index.js +185 -5
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -266,6 +266,10 @@ var productSummarySchema = import_zod2.z.object({
|
|
|
266
266
|
releaseDescription: import_zod2.z.string().optional(),
|
|
267
267
|
downloadsNeedsUser: import_zod2.z.boolean(),
|
|
268
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(),
|
|
269
273
|
downloads: import_zod2.z.array(downloadSchema).optional()
|
|
270
274
|
});
|
|
271
275
|
|
|
@@ -1430,6 +1434,9 @@ var OrderEndpoints = class {
|
|
|
1430
1434
|
if (options.checkout.returnUrl) {
|
|
1431
1435
|
query.append("returnUrl", options.checkout.returnUrl);
|
|
1432
1436
|
}
|
|
1437
|
+
if (options.checkout.embedded !== void 0) {
|
|
1438
|
+
query.append("embedded", options.checkout.embedded ? "true" : "false");
|
|
1439
|
+
}
|
|
1433
1440
|
}
|
|
1434
1441
|
const response = await this.api.fetch(`/api/customer/orders/${orderId}?${query.toString()}`, orderSchema, {
|
|
1435
1442
|
abort: options == null ? void 0 : options.abort
|
|
@@ -1444,6 +1451,8 @@ var OrderEndpoints = class {
|
|
|
1444
1451
|
query.checkout = "true";
|
|
1445
1452
|
if (checkout.returnUrl)
|
|
1446
1453
|
query.returnUrl = checkout.returnUrl;
|
|
1454
|
+
if (checkout.embedded !== void 0)
|
|
1455
|
+
query.embedded = checkout.embedded ? "true" : "false";
|
|
1447
1456
|
}
|
|
1448
1457
|
const response = await this.api.fetch(
|
|
1449
1458
|
`/api/customer/orders/${order.id}?${new URLSearchParams(query).toString()}`,
|
|
@@ -1878,10 +1887,29 @@ var OfferUtils = class _OfferUtils {
|
|
|
1878
1887
|
static relevantTargets(offer, cartItems) {
|
|
1879
1888
|
if (offer.targets.length > 1) {
|
|
1880
1889
|
const allOwned = offer.targets.every((t) => t.item.owned);
|
|
1881
|
-
return offer.targets.filter((t) => (allOwned || !t.item.owned) && !
|
|
1890
|
+
return offer.targets.filter((t) => (allOwned || !t.item.owned) && !_OfferUtils.targetInCart(t, cartItems));
|
|
1882
1891
|
}
|
|
1883
1892
|
return offer.targets;
|
|
1884
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
|
+
}
|
|
1885
1913
|
// The pricing variations a target restricts the offer to. An empty (or missing) variation list
|
|
1886
1914
|
// means the offer is not narrowed to specific variations, in which case we keep the established
|
|
1887
1915
|
// single default-variation upsell.
|
|
@@ -1912,7 +1940,7 @@ var OfferUtils = class _OfferUtils {
|
|
|
1912
1940
|
};
|
|
1913
1941
|
|
|
1914
1942
|
// src/utils/cart.ts
|
|
1915
|
-
var
|
|
1943
|
+
var _CartUtils = class _CartUtils {
|
|
1916
1944
|
// The pricing variation a line item points at, falling back to the item's default variation when
|
|
1917
1945
|
// the variation id doesn't resolve, which is what the cart has always displayed.
|
|
1918
1946
|
static resolveVariation(catalog, item) {
|
|
@@ -1970,8 +1998,13 @@ var CartUtils = class _CartUtils {
|
|
|
1970
1998
|
// Which lines a cart offer discounts. No targets means the whole cart, a reading that only holds
|
|
1971
1999
|
// for cart-scope offers, where an empty target list is the API's way of saying "every line".
|
|
1972
2000
|
static cartOfferAppliesToLine(offer, item) {
|
|
1973
|
-
|
|
1974
|
-
|
|
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) {
|
|
1975
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)));
|
|
1976
2009
|
}
|
|
1977
2010
|
// The cart offer actually discounting this cart, as opposed to the one attached to the order. An
|
|
@@ -2048,6 +2081,148 @@ var CartUtils = class _CartUtils {
|
|
|
2048
2081
|
return aggregate + DiscountUtils.apply(offer.discount, { [currency]: unitPrice })[currency] * item.quantity;
|
|
2049
2082
|
}, 0);
|
|
2050
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
|
+
}
|
|
2051
2226
|
// A line's unit price once its own offer has come off, which is what a cart offer is applied to:
|
|
2052
2227
|
// the API discounts in the order product discount -> line offer -> cart offer -> coupons. An
|
|
2053
2228
|
// unresolvable price counts as zero, matching `total`'s "still render something" stance rather
|
|
@@ -2058,9 +2233,14 @@ var CartUtils = class _CartUtils {
|
|
|
2058
2233
|
const itemOffer = item.offerId ? (_b = catalog.offers) == null ? void 0 : _b.find((o) => o.id === item.offerId) : void 0;
|
|
2059
2234
|
if (!itemOffer || !OfferUtils.eligible(itemOffer, context))
|
|
2060
2235
|
return unitPrice;
|
|
2061
|
-
return unitPrice -
|
|
2236
|
+
return unitPrice - _CartUtils.itemOfferUnitDiscount(itemOffer, unitPrice, currency);
|
|
2062
2237
|
}
|
|
2063
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;
|
|
2064
2244
|
|
|
2065
2245
|
// src/utils/image.ts
|
|
2066
2246
|
var imageWidths = [16, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048];
|
package/dist/index.d.cts
CHANGED
|
@@ -1682,6 +1682,8 @@ declare const licenseSchema: z.ZodObject<{
|
|
|
1682
1682
|
releaseDescription: z.ZodOptional<z.ZodString>;
|
|
1683
1683
|
downloadsNeedsUser: z.ZodBoolean;
|
|
1684
1684
|
downloadsNeedsOwnership: z.ZodBoolean;
|
|
1685
|
+
downloadsNeedsGroupMembership: z.ZodBoolean;
|
|
1686
|
+
downloadsAllowed: z.ZodBoolean;
|
|
1685
1687
|
downloads: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
1686
1688
|
name: z.ZodString;
|
|
1687
1689
|
key: z.ZodString;
|
|
@@ -1749,6 +1751,8 @@ declare const licenseSchema: z.ZodObject<{
|
|
|
1749
1751
|
currentVersion: string | null;
|
|
1750
1752
|
downloadsNeedsUser: boolean;
|
|
1751
1753
|
downloadsNeedsOwnership: boolean;
|
|
1754
|
+
downloadsNeedsGroupMembership: boolean;
|
|
1755
|
+
downloadsAllowed: boolean;
|
|
1752
1756
|
description?: string | null | undefined;
|
|
1753
1757
|
tagline?: string | null | undefined;
|
|
1754
1758
|
website?: string | undefined;
|
|
@@ -1783,6 +1787,8 @@ declare const licenseSchema: z.ZodObject<{
|
|
|
1783
1787
|
currentVersion: string | null;
|
|
1784
1788
|
downloadsNeedsUser: boolean;
|
|
1785
1789
|
downloadsNeedsOwnership: boolean;
|
|
1790
|
+
downloadsNeedsGroupMembership: boolean;
|
|
1791
|
+
downloadsAllowed: boolean;
|
|
1786
1792
|
description?: string | null | undefined;
|
|
1787
1793
|
tagline?: string | null | undefined;
|
|
1788
1794
|
website?: string | undefined;
|
|
@@ -1850,6 +1856,8 @@ declare const licenseSchema: z.ZodObject<{
|
|
|
1850
1856
|
currentVersion: string | null;
|
|
1851
1857
|
downloadsNeedsUser: boolean;
|
|
1852
1858
|
downloadsNeedsOwnership: boolean;
|
|
1859
|
+
downloadsNeedsGroupMembership: boolean;
|
|
1860
|
+
downloadsAllowed: boolean;
|
|
1853
1861
|
description?: string | null | undefined;
|
|
1854
1862
|
tagline?: string | null | undefined;
|
|
1855
1863
|
website?: string | undefined;
|
|
@@ -1903,6 +1911,8 @@ declare const licenseSchema: z.ZodObject<{
|
|
|
1903
1911
|
currentVersion: string | null;
|
|
1904
1912
|
downloadsNeedsUser: boolean;
|
|
1905
1913
|
downloadsNeedsOwnership: boolean;
|
|
1914
|
+
downloadsNeedsGroupMembership: boolean;
|
|
1915
|
+
downloadsAllowed: boolean;
|
|
1906
1916
|
description?: string | null | undefined;
|
|
1907
1917
|
tagline?: string | null | undefined;
|
|
1908
1918
|
website?: string | undefined;
|
|
@@ -2450,6 +2460,8 @@ declare const productSummarySchema: z.ZodObject<{
|
|
|
2450
2460
|
releaseDescription: z.ZodOptional<z.ZodString>;
|
|
2451
2461
|
downloadsNeedsUser: z.ZodBoolean;
|
|
2452
2462
|
downloadsNeedsOwnership: z.ZodBoolean;
|
|
2463
|
+
downloadsNeedsGroupMembership: z.ZodBoolean;
|
|
2464
|
+
downloadsAllowed: z.ZodBoolean;
|
|
2453
2465
|
downloads: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
2454
2466
|
name: z.ZodString;
|
|
2455
2467
|
key: z.ZodString;
|
|
@@ -2517,6 +2529,8 @@ declare const productSummarySchema: z.ZodObject<{
|
|
|
2517
2529
|
currentVersion: string | null;
|
|
2518
2530
|
downloadsNeedsUser: boolean;
|
|
2519
2531
|
downloadsNeedsOwnership: boolean;
|
|
2532
|
+
downloadsNeedsGroupMembership: boolean;
|
|
2533
|
+
downloadsAllowed: boolean;
|
|
2520
2534
|
description?: string | null | undefined;
|
|
2521
2535
|
tagline?: string | null | undefined;
|
|
2522
2536
|
website?: string | undefined;
|
|
@@ -2551,6 +2565,8 @@ declare const productSummarySchema: z.ZodObject<{
|
|
|
2551
2565
|
currentVersion: string | null;
|
|
2552
2566
|
downloadsNeedsUser: boolean;
|
|
2553
2567
|
downloadsNeedsOwnership: boolean;
|
|
2568
|
+
downloadsNeedsGroupMembership: boolean;
|
|
2569
|
+
downloadsAllowed: boolean;
|
|
2554
2570
|
description?: string | null | undefined;
|
|
2555
2571
|
tagline?: string | null | undefined;
|
|
2556
2572
|
website?: string | undefined;
|
|
@@ -22703,15 +22719,25 @@ type LineItem = z.infer<typeof openOrderLineItem>;
|
|
|
22703
22719
|
type ProductLineItem = z.infer<typeof openProductLineItem>;
|
|
22704
22720
|
type BundleLineItem = z.infer<typeof openBundleLineItem>;
|
|
22705
22721
|
|
|
22722
|
+
interface CheckoutUrlOptions {
|
|
22723
|
+
/** Where to send the buyer once the checkout is done with them. */
|
|
22724
|
+
returnUrl?: string;
|
|
22725
|
+
/**
|
|
22726
|
+
* Whether checkout will be rendered in the overlay iframe rather than as a
|
|
22727
|
+
* full-page redirect. Tells the backend whether a checkout login token is
|
|
22728
|
+
* worth minting: the returned `hostedCheckoutUrl` only carries one when this
|
|
22729
|
+
* is not `false`. Say `false` whenever you intend to redirect, so a day-long
|
|
22730
|
+
* credential is never minted for a URL that will not use it.
|
|
22731
|
+
*/
|
|
22732
|
+
embedded?: boolean;
|
|
22733
|
+
}
|
|
22706
22734
|
declare class OrderEndpoints {
|
|
22707
22735
|
private api;
|
|
22708
22736
|
constructor(api: MoonbaseApi);
|
|
22709
22737
|
get(orderId: string, options?: {
|
|
22710
22738
|
abort?: AbortSignal;
|
|
22711
22739
|
finalize?: boolean;
|
|
22712
|
-
checkout?:
|
|
22713
|
-
returnUrl?: string;
|
|
22714
|
-
};
|
|
22740
|
+
checkout?: CheckoutUrlOptions;
|
|
22715
22741
|
}): Promise<{
|
|
22716
22742
|
id: string;
|
|
22717
22743
|
status: OrderStatus.Completed;
|
|
@@ -24102,9 +24128,7 @@ declare class OrderEndpoints {
|
|
|
24102
24128
|
offerId?: string;
|
|
24103
24129
|
replaced?: string[];
|
|
24104
24130
|
})[];
|
|
24105
|
-
}, checkout?:
|
|
24106
|
-
returnUrl?: string;
|
|
24107
|
-
}, utm?: UrchinTrackingModule): Promise<OpenOrder>;
|
|
24131
|
+
}, checkout?: CheckoutUrlOptions, utm?: UrchinTrackingModule): Promise<OpenOrder>;
|
|
24108
24132
|
addBillingDetails(orderId: string, details: {
|
|
24109
24133
|
name: string;
|
|
24110
24134
|
email: string;
|
|
@@ -46733,6 +46757,18 @@ declare class OfferUtils {
|
|
|
46733
46757
|
static progress(offer: StorefrontOffer, context: Order | OfferConditionContext): OfferProgress | null;
|
|
46734
46758
|
private static evaluate;
|
|
46735
46759
|
static relevantTargets(offer: StorefrontOffer, cartItems: LineItem[]): StorefrontOfferTarget[];
|
|
46760
|
+
/**
|
|
46761
|
+
* Whether every item an offer targets is already in the cart, so the offer has no upsell left to
|
|
46762
|
+
* show.
|
|
46763
|
+
*
|
|
46764
|
+
* This is the set of offers the cart hides: a card inviting the buyer to add what they have already
|
|
46765
|
+
* added says nothing, so it is dropped. Which makes it exactly the set whose discount nothing can
|
|
46766
|
+
* currently claim: an offer is only good for one line, and there is no card left to spend it from.
|
|
46767
|
+
*
|
|
46768
|
+
* An offer with no targets names nothing and so is never in this set.
|
|
46769
|
+
*/
|
|
46770
|
+
static allTargetsInCart(offer: StorefrontOffer, cartItems: LineItem[]): boolean;
|
|
46771
|
+
private static targetInCart;
|
|
46736
46772
|
static targetedVariations(target: StorefrontOfferTarget): PricingVariation[];
|
|
46737
46773
|
static relevantTargetVariations(offer: StorefrontOffer, cartItems: LineItem[]): {
|
|
46738
46774
|
target: StorefrontProduct | StorefrontBundle;
|
|
@@ -46764,6 +46800,7 @@ declare class CartUtils {
|
|
|
46764
46800
|
static measure(catalog: CartCatalog, items: LineItem[], currency: string): Money | undefined;
|
|
46765
46801
|
static conditionContext(catalog: CartCatalog, order: CartOrder, currency: string): OfferConditionContext;
|
|
46766
46802
|
static cartOfferAppliesToLine(offer: StorefrontOffer, item: LineItem): boolean;
|
|
46803
|
+
static offerTargetsLine(offer: StorefrontOffer, item: LineItem): boolean;
|
|
46767
46804
|
static appliedCartOffer(catalog: CartCatalog, order: CartOrder, context: OfferConditionContext): StorefrontOffer | undefined;
|
|
46768
46805
|
static total(catalog: CartCatalog, order: CartOrder, currency: string): CartTotal;
|
|
46769
46806
|
static bestCartOffer(catalog: CartCatalog, order: CartOrder, currency: string): StorefrontOffer | undefined;
|
|
@@ -46776,6 +46813,54 @@ declare class CartUtils {
|
|
|
46776
46813
|
* line discounted to a tenth of its price is worth less than 10% of a full-price one.
|
|
46777
46814
|
*/
|
|
46778
46815
|
static cartOfferValue(catalog: CartCatalog, order: CartOrder, offer: StorefrontOffer, currency: string, context?: OfferConditionContext): number;
|
|
46816
|
+
/**
|
|
46817
|
+
* The item-scope offer worth attaching to a single line, or `undefined` when none is.
|
|
46818
|
+
*
|
|
46819
|
+
* Ranked by what it would actually take off this line rather than by percentage, the same way
|
|
46820
|
+
* `bestCartOffer` ranks a cart: a flat amount off can beat a bigger-looking percentage on a cheap
|
|
46821
|
+
* line, and the other way round on an expensive one.
|
|
46822
|
+
*
|
|
46823
|
+
* `unavailableOfferIds` are offers the rest of the cart has already claimed. An offer is only ever
|
|
46824
|
+
* good for one line, since the cart treats an offer held by any line as in use. So a caller walking
|
|
46825
|
+
* the cart must reserve each one as it goes, or two lines would both count on the same offer and
|
|
46826
|
+
* the API would honour it once. `assignItemOffers` does that bookkeeping for a whole cart.
|
|
46827
|
+
*/
|
|
46828
|
+
static bestItemOffer(catalog: CartCatalog, item: LineItem, currency: string, context: OfferConditionContext, unavailableOfferIds?: ReadonlySet<string>): StorefrontOffer | undefined;
|
|
46829
|
+
/**
|
|
46830
|
+
* Which item-scope offer to attach to which line, for the lines not already carrying one.
|
|
46831
|
+
*
|
|
46832
|
+
* Chosen for what the pairings are worth together rather than by cart order. An offer is only good
|
|
46833
|
+
* for one line, so walking the cart and taking the best offer for each line in turn hands a
|
|
46834
|
+
* multi-target offer to whichever line the buyer happened to add first, which is arbitrary and can
|
|
46835
|
+
* be the cheapest one. A buyer claiming these from the upsell cards would have placed them where
|
|
46836
|
+
* they are worth most, so that is what this does.
|
|
46837
|
+
*
|
|
46838
|
+
* Lines already holding an offer are left alone and their offers reserved: an offer on a line is
|
|
46839
|
+
* intent, whether the buyer's from the upsell or the API's from a refreshed order, and this is not
|
|
46840
|
+
* the place to second-guess it.
|
|
46841
|
+
*/
|
|
46842
|
+
static assignItemOffers(catalog: CartCatalog, order: CartOrder, currency: string, context?: OfferConditionContext): {
|
|
46843
|
+
itemId: string;
|
|
46844
|
+
offerId: string;
|
|
46845
|
+
}[];
|
|
46846
|
+
/**
|
|
46847
|
+
* The pairings worth the most together, given each offer can only be spent once.
|
|
46848
|
+
*
|
|
46849
|
+
* Taking the most valuable pairing first is not enough: with one offer worth 100 on line A and 99 on
|
|
46850
|
+
* line B, and a second offer worth 98 on line A alone, claiming the 100 strands the second offer and
|
|
46851
|
+
* saves 100, where putting the first offer on B and the second on A saves 197. The pairings have to
|
|
46852
|
+
* be chosen as a set.
|
|
46853
|
+
*
|
|
46854
|
+
* Walked line by line, memoised on which offers are still free. The candidates have already been
|
|
46855
|
+
* through targeting and eligibility, so in a real cart there are a handful; past `searchableOffers`
|
|
46856
|
+
* distinct offers it takes the most valuable pairing first instead, which is never worse than
|
|
46857
|
+
* applying nothing.
|
|
46858
|
+
*/
|
|
46859
|
+
private static maximalItemOfferAssignment;
|
|
46860
|
+
private static mostValuableItemOfferPairings;
|
|
46861
|
+
private static readonly searchableOffers;
|
|
46862
|
+
private static itemOfferCandidates;
|
|
46863
|
+
private static itemOfferUnitDiscount;
|
|
46779
46864
|
private static unitPriceAfterItemOffer;
|
|
46780
46865
|
}
|
|
46781
46866
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1682,6 +1682,8 @@ declare const licenseSchema: z.ZodObject<{
|
|
|
1682
1682
|
releaseDescription: z.ZodOptional<z.ZodString>;
|
|
1683
1683
|
downloadsNeedsUser: z.ZodBoolean;
|
|
1684
1684
|
downloadsNeedsOwnership: z.ZodBoolean;
|
|
1685
|
+
downloadsNeedsGroupMembership: z.ZodBoolean;
|
|
1686
|
+
downloadsAllowed: z.ZodBoolean;
|
|
1685
1687
|
downloads: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
1686
1688
|
name: z.ZodString;
|
|
1687
1689
|
key: z.ZodString;
|
|
@@ -1749,6 +1751,8 @@ declare const licenseSchema: z.ZodObject<{
|
|
|
1749
1751
|
currentVersion: string | null;
|
|
1750
1752
|
downloadsNeedsUser: boolean;
|
|
1751
1753
|
downloadsNeedsOwnership: boolean;
|
|
1754
|
+
downloadsNeedsGroupMembership: boolean;
|
|
1755
|
+
downloadsAllowed: boolean;
|
|
1752
1756
|
description?: string | null | undefined;
|
|
1753
1757
|
tagline?: string | null | undefined;
|
|
1754
1758
|
website?: string | undefined;
|
|
@@ -1783,6 +1787,8 @@ declare const licenseSchema: z.ZodObject<{
|
|
|
1783
1787
|
currentVersion: string | null;
|
|
1784
1788
|
downloadsNeedsUser: boolean;
|
|
1785
1789
|
downloadsNeedsOwnership: boolean;
|
|
1790
|
+
downloadsNeedsGroupMembership: boolean;
|
|
1791
|
+
downloadsAllowed: boolean;
|
|
1786
1792
|
description?: string | null | undefined;
|
|
1787
1793
|
tagline?: string | null | undefined;
|
|
1788
1794
|
website?: string | undefined;
|
|
@@ -1850,6 +1856,8 @@ declare const licenseSchema: z.ZodObject<{
|
|
|
1850
1856
|
currentVersion: string | null;
|
|
1851
1857
|
downloadsNeedsUser: boolean;
|
|
1852
1858
|
downloadsNeedsOwnership: boolean;
|
|
1859
|
+
downloadsNeedsGroupMembership: boolean;
|
|
1860
|
+
downloadsAllowed: boolean;
|
|
1853
1861
|
description?: string | null | undefined;
|
|
1854
1862
|
tagline?: string | null | undefined;
|
|
1855
1863
|
website?: string | undefined;
|
|
@@ -1903,6 +1911,8 @@ declare const licenseSchema: z.ZodObject<{
|
|
|
1903
1911
|
currentVersion: string | null;
|
|
1904
1912
|
downloadsNeedsUser: boolean;
|
|
1905
1913
|
downloadsNeedsOwnership: boolean;
|
|
1914
|
+
downloadsNeedsGroupMembership: boolean;
|
|
1915
|
+
downloadsAllowed: boolean;
|
|
1906
1916
|
description?: string | null | undefined;
|
|
1907
1917
|
tagline?: string | null | undefined;
|
|
1908
1918
|
website?: string | undefined;
|
|
@@ -2450,6 +2460,8 @@ declare const productSummarySchema: z.ZodObject<{
|
|
|
2450
2460
|
releaseDescription: z.ZodOptional<z.ZodString>;
|
|
2451
2461
|
downloadsNeedsUser: z.ZodBoolean;
|
|
2452
2462
|
downloadsNeedsOwnership: z.ZodBoolean;
|
|
2463
|
+
downloadsNeedsGroupMembership: z.ZodBoolean;
|
|
2464
|
+
downloadsAllowed: z.ZodBoolean;
|
|
2453
2465
|
downloads: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
2454
2466
|
name: z.ZodString;
|
|
2455
2467
|
key: z.ZodString;
|
|
@@ -2517,6 +2529,8 @@ declare const productSummarySchema: z.ZodObject<{
|
|
|
2517
2529
|
currentVersion: string | null;
|
|
2518
2530
|
downloadsNeedsUser: boolean;
|
|
2519
2531
|
downloadsNeedsOwnership: boolean;
|
|
2532
|
+
downloadsNeedsGroupMembership: boolean;
|
|
2533
|
+
downloadsAllowed: boolean;
|
|
2520
2534
|
description?: string | null | undefined;
|
|
2521
2535
|
tagline?: string | null | undefined;
|
|
2522
2536
|
website?: string | undefined;
|
|
@@ -2551,6 +2565,8 @@ declare const productSummarySchema: z.ZodObject<{
|
|
|
2551
2565
|
currentVersion: string | null;
|
|
2552
2566
|
downloadsNeedsUser: boolean;
|
|
2553
2567
|
downloadsNeedsOwnership: boolean;
|
|
2568
|
+
downloadsNeedsGroupMembership: boolean;
|
|
2569
|
+
downloadsAllowed: boolean;
|
|
2554
2570
|
description?: string | null | undefined;
|
|
2555
2571
|
tagline?: string | null | undefined;
|
|
2556
2572
|
website?: string | undefined;
|
|
@@ -22703,15 +22719,25 @@ type LineItem = z.infer<typeof openOrderLineItem>;
|
|
|
22703
22719
|
type ProductLineItem = z.infer<typeof openProductLineItem>;
|
|
22704
22720
|
type BundleLineItem = z.infer<typeof openBundleLineItem>;
|
|
22705
22721
|
|
|
22722
|
+
interface CheckoutUrlOptions {
|
|
22723
|
+
/** Where to send the buyer once the checkout is done with them. */
|
|
22724
|
+
returnUrl?: string;
|
|
22725
|
+
/**
|
|
22726
|
+
* Whether checkout will be rendered in the overlay iframe rather than as a
|
|
22727
|
+
* full-page redirect. Tells the backend whether a checkout login token is
|
|
22728
|
+
* worth minting: the returned `hostedCheckoutUrl` only carries one when this
|
|
22729
|
+
* is not `false`. Say `false` whenever you intend to redirect, so a day-long
|
|
22730
|
+
* credential is never minted for a URL that will not use it.
|
|
22731
|
+
*/
|
|
22732
|
+
embedded?: boolean;
|
|
22733
|
+
}
|
|
22706
22734
|
declare class OrderEndpoints {
|
|
22707
22735
|
private api;
|
|
22708
22736
|
constructor(api: MoonbaseApi);
|
|
22709
22737
|
get(orderId: string, options?: {
|
|
22710
22738
|
abort?: AbortSignal;
|
|
22711
22739
|
finalize?: boolean;
|
|
22712
|
-
checkout?:
|
|
22713
|
-
returnUrl?: string;
|
|
22714
|
-
};
|
|
22740
|
+
checkout?: CheckoutUrlOptions;
|
|
22715
22741
|
}): Promise<{
|
|
22716
22742
|
id: string;
|
|
22717
22743
|
status: OrderStatus.Completed;
|
|
@@ -24102,9 +24128,7 @@ declare class OrderEndpoints {
|
|
|
24102
24128
|
offerId?: string;
|
|
24103
24129
|
replaced?: string[];
|
|
24104
24130
|
})[];
|
|
24105
|
-
}, checkout?:
|
|
24106
|
-
returnUrl?: string;
|
|
24107
|
-
}, utm?: UrchinTrackingModule): Promise<OpenOrder>;
|
|
24131
|
+
}, checkout?: CheckoutUrlOptions, utm?: UrchinTrackingModule): Promise<OpenOrder>;
|
|
24108
24132
|
addBillingDetails(orderId: string, details: {
|
|
24109
24133
|
name: string;
|
|
24110
24134
|
email: string;
|
|
@@ -46733,6 +46757,18 @@ declare class OfferUtils {
|
|
|
46733
46757
|
static progress(offer: StorefrontOffer, context: Order | OfferConditionContext): OfferProgress | null;
|
|
46734
46758
|
private static evaluate;
|
|
46735
46759
|
static relevantTargets(offer: StorefrontOffer, cartItems: LineItem[]): StorefrontOfferTarget[];
|
|
46760
|
+
/**
|
|
46761
|
+
* Whether every item an offer targets is already in the cart, so the offer has no upsell left to
|
|
46762
|
+
* show.
|
|
46763
|
+
*
|
|
46764
|
+
* This is the set of offers the cart hides: a card inviting the buyer to add what they have already
|
|
46765
|
+
* added says nothing, so it is dropped. Which makes it exactly the set whose discount nothing can
|
|
46766
|
+
* currently claim: an offer is only good for one line, and there is no card left to spend it from.
|
|
46767
|
+
*
|
|
46768
|
+
* An offer with no targets names nothing and so is never in this set.
|
|
46769
|
+
*/
|
|
46770
|
+
static allTargetsInCart(offer: StorefrontOffer, cartItems: LineItem[]): boolean;
|
|
46771
|
+
private static targetInCart;
|
|
46736
46772
|
static targetedVariations(target: StorefrontOfferTarget): PricingVariation[];
|
|
46737
46773
|
static relevantTargetVariations(offer: StorefrontOffer, cartItems: LineItem[]): {
|
|
46738
46774
|
target: StorefrontProduct | StorefrontBundle;
|
|
@@ -46764,6 +46800,7 @@ declare class CartUtils {
|
|
|
46764
46800
|
static measure(catalog: CartCatalog, items: LineItem[], currency: string): Money | undefined;
|
|
46765
46801
|
static conditionContext(catalog: CartCatalog, order: CartOrder, currency: string): OfferConditionContext;
|
|
46766
46802
|
static cartOfferAppliesToLine(offer: StorefrontOffer, item: LineItem): boolean;
|
|
46803
|
+
static offerTargetsLine(offer: StorefrontOffer, item: LineItem): boolean;
|
|
46767
46804
|
static appliedCartOffer(catalog: CartCatalog, order: CartOrder, context: OfferConditionContext): StorefrontOffer | undefined;
|
|
46768
46805
|
static total(catalog: CartCatalog, order: CartOrder, currency: string): CartTotal;
|
|
46769
46806
|
static bestCartOffer(catalog: CartCatalog, order: CartOrder, currency: string): StorefrontOffer | undefined;
|
|
@@ -46776,6 +46813,54 @@ declare class CartUtils {
|
|
|
46776
46813
|
* line discounted to a tenth of its price is worth less than 10% of a full-price one.
|
|
46777
46814
|
*/
|
|
46778
46815
|
static cartOfferValue(catalog: CartCatalog, order: CartOrder, offer: StorefrontOffer, currency: string, context?: OfferConditionContext): number;
|
|
46816
|
+
/**
|
|
46817
|
+
* The item-scope offer worth attaching to a single line, or `undefined` when none is.
|
|
46818
|
+
*
|
|
46819
|
+
* Ranked by what it would actually take off this line rather than by percentage, the same way
|
|
46820
|
+
* `bestCartOffer` ranks a cart: a flat amount off can beat a bigger-looking percentage on a cheap
|
|
46821
|
+
* line, and the other way round on an expensive one.
|
|
46822
|
+
*
|
|
46823
|
+
* `unavailableOfferIds` are offers the rest of the cart has already claimed. An offer is only ever
|
|
46824
|
+
* good for one line, since the cart treats an offer held by any line as in use. So a caller walking
|
|
46825
|
+
* the cart must reserve each one as it goes, or two lines would both count on the same offer and
|
|
46826
|
+
* the API would honour it once. `assignItemOffers` does that bookkeeping for a whole cart.
|
|
46827
|
+
*/
|
|
46828
|
+
static bestItemOffer(catalog: CartCatalog, item: LineItem, currency: string, context: OfferConditionContext, unavailableOfferIds?: ReadonlySet<string>): StorefrontOffer | undefined;
|
|
46829
|
+
/**
|
|
46830
|
+
* Which item-scope offer to attach to which line, for the lines not already carrying one.
|
|
46831
|
+
*
|
|
46832
|
+
* Chosen for what the pairings are worth together rather than by cart order. An offer is only good
|
|
46833
|
+
* for one line, so walking the cart and taking the best offer for each line in turn hands a
|
|
46834
|
+
* multi-target offer to whichever line the buyer happened to add first, which is arbitrary and can
|
|
46835
|
+
* be the cheapest one. A buyer claiming these from the upsell cards would have placed them where
|
|
46836
|
+
* they are worth most, so that is what this does.
|
|
46837
|
+
*
|
|
46838
|
+
* Lines already holding an offer are left alone and their offers reserved: an offer on a line is
|
|
46839
|
+
* intent, whether the buyer's from the upsell or the API's from a refreshed order, and this is not
|
|
46840
|
+
* the place to second-guess it.
|
|
46841
|
+
*/
|
|
46842
|
+
static assignItemOffers(catalog: CartCatalog, order: CartOrder, currency: string, context?: OfferConditionContext): {
|
|
46843
|
+
itemId: string;
|
|
46844
|
+
offerId: string;
|
|
46845
|
+
}[];
|
|
46846
|
+
/**
|
|
46847
|
+
* The pairings worth the most together, given each offer can only be spent once.
|
|
46848
|
+
*
|
|
46849
|
+
* Taking the most valuable pairing first is not enough: with one offer worth 100 on line A and 99 on
|
|
46850
|
+
* line B, and a second offer worth 98 on line A alone, claiming the 100 strands the second offer and
|
|
46851
|
+
* saves 100, where putting the first offer on B and the second on A saves 197. The pairings have to
|
|
46852
|
+
* be chosen as a set.
|
|
46853
|
+
*
|
|
46854
|
+
* Walked line by line, memoised on which offers are still free. The candidates have already been
|
|
46855
|
+
* through targeting and eligibility, so in a real cart there are a handful; past `searchableOffers`
|
|
46856
|
+
* distinct offers it takes the most valuable pairing first instead, which is never worse than
|
|
46857
|
+
* applying nothing.
|
|
46858
|
+
*/
|
|
46859
|
+
private static maximalItemOfferAssignment;
|
|
46860
|
+
private static mostValuableItemOfferPairings;
|
|
46861
|
+
private static readonly searchableOffers;
|
|
46862
|
+
private static itemOfferCandidates;
|
|
46863
|
+
private static itemOfferUnitDiscount;
|
|
46779
46864
|
private static unitPriceAfterItemOffer;
|
|
46780
46865
|
}
|
|
46781
46866
|
|
package/dist/index.js
CHANGED
|
@@ -208,6 +208,10 @@ var productSummarySchema = z2.object({
|
|
|
208
208
|
releaseDescription: z2.string().optional(),
|
|
209
209
|
downloadsNeedsUser: z2.boolean(),
|
|
210
210
|
downloadsNeedsOwnership: z2.boolean(),
|
|
211
|
+
downloadsNeedsGroupMembership: z2.boolean(),
|
|
212
|
+
// Whether this caller clears every requirement above, including the group membership the client
|
|
213
|
+
// can't see for itself.
|
|
214
|
+
downloadsAllowed: z2.boolean(),
|
|
211
215
|
downloads: z2.array(downloadSchema).optional()
|
|
212
216
|
});
|
|
213
217
|
|
|
@@ -1372,6 +1376,9 @@ var OrderEndpoints = class {
|
|
|
1372
1376
|
if (options.checkout.returnUrl) {
|
|
1373
1377
|
query.append("returnUrl", options.checkout.returnUrl);
|
|
1374
1378
|
}
|
|
1379
|
+
if (options.checkout.embedded !== void 0) {
|
|
1380
|
+
query.append("embedded", options.checkout.embedded ? "true" : "false");
|
|
1381
|
+
}
|
|
1375
1382
|
}
|
|
1376
1383
|
const response = await this.api.fetch(`/api/customer/orders/${orderId}?${query.toString()}`, orderSchema, {
|
|
1377
1384
|
abort: options == null ? void 0 : options.abort
|
|
@@ -1386,6 +1393,8 @@ var OrderEndpoints = class {
|
|
|
1386
1393
|
query.checkout = "true";
|
|
1387
1394
|
if (checkout.returnUrl)
|
|
1388
1395
|
query.returnUrl = checkout.returnUrl;
|
|
1396
|
+
if (checkout.embedded !== void 0)
|
|
1397
|
+
query.embedded = checkout.embedded ? "true" : "false";
|
|
1389
1398
|
}
|
|
1390
1399
|
const response = await this.api.fetch(
|
|
1391
1400
|
`/api/customer/orders/${order.id}?${new URLSearchParams(query).toString()}`,
|
|
@@ -1820,10 +1829,29 @@ var OfferUtils = class _OfferUtils {
|
|
|
1820
1829
|
static relevantTargets(offer, cartItems) {
|
|
1821
1830
|
if (offer.targets.length > 1) {
|
|
1822
1831
|
const allOwned = offer.targets.every((t) => t.item.owned);
|
|
1823
|
-
return offer.targets.filter((t) => (allOwned || !t.item.owned) && !
|
|
1832
|
+
return offer.targets.filter((t) => (allOwned || !t.item.owned) && !_OfferUtils.targetInCart(t, cartItems));
|
|
1824
1833
|
}
|
|
1825
1834
|
return offer.targets;
|
|
1826
1835
|
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Whether every item an offer targets is already in the cart, so the offer has no upsell left to
|
|
1838
|
+
* show.
|
|
1839
|
+
*
|
|
1840
|
+
* This is the set of offers the cart hides: a card inviting the buyer to add what they have already
|
|
1841
|
+
* added says nothing, so it is dropped. Which makes it exactly the set whose discount nothing can
|
|
1842
|
+
* currently claim: an offer is only good for one line, and there is no card left to spend it from.
|
|
1843
|
+
*
|
|
1844
|
+
* An offer with no targets names nothing and so is never in this set.
|
|
1845
|
+
*/
|
|
1846
|
+
static allTargetsInCart(offer, cartItems) {
|
|
1847
|
+
return offer.targets.length > 0 && offer.targets.every((target) => _OfferUtils.targetInCart(target, cartItems));
|
|
1848
|
+
}
|
|
1849
|
+
// Matched on the item alone, not the variation: a target narrowed to one variation still counts as
|
|
1850
|
+
// in the cart when another variation of it is. That is the reading the cart's own upsell has always
|
|
1851
|
+
// taken, and the two have to agree or an offer could be hidden and auto-applied at once.
|
|
1852
|
+
static targetInCart(target, cartItems) {
|
|
1853
|
+
return cartItems.some((l) => l.type === target.item.type && target.item.id === (l.type === "Product" ? l.productId : l.bundleId));
|
|
1854
|
+
}
|
|
1827
1855
|
// The pricing variations a target restricts the offer to. An empty (or missing) variation list
|
|
1828
1856
|
// means the offer is not narrowed to specific variations, in which case we keep the established
|
|
1829
1857
|
// single default-variation upsell.
|
|
@@ -1854,7 +1882,7 @@ var OfferUtils = class _OfferUtils {
|
|
|
1854
1882
|
};
|
|
1855
1883
|
|
|
1856
1884
|
// src/utils/cart.ts
|
|
1857
|
-
var
|
|
1885
|
+
var _CartUtils = class _CartUtils {
|
|
1858
1886
|
// The pricing variation a line item points at, falling back to the item's default variation when
|
|
1859
1887
|
// the variation id doesn't resolve, which is what the cart has always displayed.
|
|
1860
1888
|
static resolveVariation(catalog, item) {
|
|
@@ -1912,8 +1940,13 @@ var CartUtils = class _CartUtils {
|
|
|
1912
1940
|
// Which lines a cart offer discounts. No targets means the whole cart, a reading that only holds
|
|
1913
1941
|
// for cart-scope offers, where an empty target list is the API's way of saying "every line".
|
|
1914
1942
|
static cartOfferAppliesToLine(offer, item) {
|
|
1915
|
-
|
|
1916
|
-
|
|
1943
|
+
return offer.targets.length === 0 || _CartUtils.offerTargetsLine(offer, item);
|
|
1944
|
+
}
|
|
1945
|
+
// Whether an offer names this line among its targets. An empty variation list on a target leaves
|
|
1946
|
+
// the offer open to every variation of that item. An offer with no targets at all names nothing
|
|
1947
|
+
// here, which is why the "every line" reading stays in `cartOfferAppliesToLine`: it is only true
|
|
1948
|
+
// of a cart offer.
|
|
1949
|
+
static offerTargetsLine(offer, item) {
|
|
1917
1950
|
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)));
|
|
1918
1951
|
}
|
|
1919
1952
|
// The cart offer actually discounting this cart, as opposed to the one attached to the order. An
|
|
@@ -1990,6 +2023,148 @@ var CartUtils = class _CartUtils {
|
|
|
1990
2023
|
return aggregate + DiscountUtils.apply(offer.discount, { [currency]: unitPrice })[currency] * item.quantity;
|
|
1991
2024
|
}, 0);
|
|
1992
2025
|
}
|
|
2026
|
+
/**
|
|
2027
|
+
* The item-scope offer worth attaching to a single line, or `undefined` when none is.
|
|
2028
|
+
*
|
|
2029
|
+
* Ranked by what it would actually take off this line rather than by percentage, the same way
|
|
2030
|
+
* `bestCartOffer` ranks a cart: a flat amount off can beat a bigger-looking percentage on a cheap
|
|
2031
|
+
* line, and the other way round on an expensive one.
|
|
2032
|
+
*
|
|
2033
|
+
* `unavailableOfferIds` are offers the rest of the cart has already claimed. An offer is only ever
|
|
2034
|
+
* good for one line, since the cart treats an offer held by any line as in use. So a caller walking
|
|
2035
|
+
* the cart must reserve each one as it goes, or two lines would both count on the same offer and
|
|
2036
|
+
* the API would honour it once. `assignItemOffers` does that bookkeeping for a whole cart.
|
|
2037
|
+
*/
|
|
2038
|
+
static bestItemOffer(catalog, item, currency, context, unavailableOfferIds) {
|
|
2039
|
+
let best;
|
|
2040
|
+
for (const candidate of _CartUtils.itemOfferCandidates(catalog, [item], currency, context, unavailableOfferIds)) {
|
|
2041
|
+
if (!best || candidate.value > best.value)
|
|
2042
|
+
best = candidate;
|
|
2043
|
+
}
|
|
2044
|
+
return best == null ? void 0 : best.offer;
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Which item-scope offer to attach to which line, for the lines not already carrying one.
|
|
2048
|
+
*
|
|
2049
|
+
* Chosen for what the pairings are worth together rather than by cart order. An offer is only good
|
|
2050
|
+
* for one line, so walking the cart and taking the best offer for each line in turn hands a
|
|
2051
|
+
* multi-target offer to whichever line the buyer happened to add first, which is arbitrary and can
|
|
2052
|
+
* be the cheapest one. A buyer claiming these from the upsell cards would have placed them where
|
|
2053
|
+
* they are worth most, so that is what this does.
|
|
2054
|
+
*
|
|
2055
|
+
* Lines already holding an offer are left alone and their offers reserved: an offer on a line is
|
|
2056
|
+
* intent, whether the buyer's from the upsell or the API's from a refreshed order, and this is not
|
|
2057
|
+
* the place to second-guess it.
|
|
2058
|
+
*/
|
|
2059
|
+
static assignItemOffers(catalog, order, currency, context) {
|
|
2060
|
+
const conditionContext = context != null ? context : _CartUtils.conditionContext(catalog, order, currency);
|
|
2061
|
+
const claimed = new Set(order.items.map((i) => i.offerId).filter((id) => !!id));
|
|
2062
|
+
return _CartUtils.maximalItemOfferAssignment(
|
|
2063
|
+
_CartUtils.itemOfferCandidates(catalog, order.items.filter((i) => !i.offerId), currency, conditionContext, claimed)
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
2067
|
+
* The pairings worth the most together, given each offer can only be spent once.
|
|
2068
|
+
*
|
|
2069
|
+
* Taking the most valuable pairing first is not enough: with one offer worth 100 on line A and 99 on
|
|
2070
|
+
* line B, and a second offer worth 98 on line A alone, claiming the 100 strands the second offer and
|
|
2071
|
+
* saves 100, where putting the first offer on B and the second on A saves 197. The pairings have to
|
|
2072
|
+
* be chosen as a set.
|
|
2073
|
+
*
|
|
2074
|
+
* Walked line by line, memoised on which offers are still free. The candidates have already been
|
|
2075
|
+
* through targeting and eligibility, so in a real cart there are a handful; past `searchableOffers`
|
|
2076
|
+
* distinct offers it takes the most valuable pairing first instead, which is never worse than
|
|
2077
|
+
* applying nothing.
|
|
2078
|
+
*/
|
|
2079
|
+
static maximalItemOfferAssignment(candidates) {
|
|
2080
|
+
var _a;
|
|
2081
|
+
const optionsByItem = /* @__PURE__ */ new Map();
|
|
2082
|
+
const offerBits = /* @__PURE__ */ new Map();
|
|
2083
|
+
for (const { item, offer, value } of candidates) {
|
|
2084
|
+
let bit = offerBits.get(offer.id);
|
|
2085
|
+
if (bit === void 0) {
|
|
2086
|
+
bit = offerBits.size;
|
|
2087
|
+
offerBits.set(offer.id, bit);
|
|
2088
|
+
}
|
|
2089
|
+
const options = (_a = optionsByItem.get(item.id)) != null ? _a : [];
|
|
2090
|
+
options.push({ offerId: offer.id, bit, value });
|
|
2091
|
+
optionsByItem.set(item.id, options);
|
|
2092
|
+
}
|
|
2093
|
+
if (offerBits.size > _CartUtils.searchableOffers)
|
|
2094
|
+
return _CartUtils.mostValuableItemOfferPairings(candidates);
|
|
2095
|
+
const itemIds = [...optionsByItem.keys()];
|
|
2096
|
+
const memo = /* @__PURE__ */ new Map();
|
|
2097
|
+
const from = (index, spent) => {
|
|
2098
|
+
var _a2;
|
|
2099
|
+
if (index >= itemIds.length)
|
|
2100
|
+
return { value: 0, assignments: [] };
|
|
2101
|
+
const key = `${index}:${spent}`;
|
|
2102
|
+
const memoised = memo.get(key);
|
|
2103
|
+
if (memoised)
|
|
2104
|
+
return memoised;
|
|
2105
|
+
let best = from(index + 1, spent);
|
|
2106
|
+
const itemId = itemIds[index];
|
|
2107
|
+
for (const option of (_a2 = optionsByItem.get(itemId)) != null ? _a2 : []) {
|
|
2108
|
+
if (spent & 1 << option.bit)
|
|
2109
|
+
continue;
|
|
2110
|
+
const rest = from(index + 1, spent | 1 << option.bit);
|
|
2111
|
+
const value = option.value + rest.value;
|
|
2112
|
+
if (value > best.value)
|
|
2113
|
+
best = { value, assignments: [{ itemId, offerId: option.offerId }, ...rest.assignments] };
|
|
2114
|
+
}
|
|
2115
|
+
memo.set(key, best);
|
|
2116
|
+
return best;
|
|
2117
|
+
};
|
|
2118
|
+
return from(0, 0).assignments;
|
|
2119
|
+
}
|
|
2120
|
+
// The fallback for a candidate set too large to search: repeatedly take the most valuable pairing
|
|
2121
|
+
// still available. Can strand a better combination, which is the whole reason it is only a fallback.
|
|
2122
|
+
static mostValuableItemOfferPairings(candidates) {
|
|
2123
|
+
const assignments = [];
|
|
2124
|
+
const assignedItemIds = /* @__PURE__ */ new Set();
|
|
2125
|
+
const spentOfferIds = /* @__PURE__ */ new Set();
|
|
2126
|
+
for (const { item, offer } of [...candidates].sort((a, b) => b.value - a.value)) {
|
|
2127
|
+
if (assignedItemIds.has(item.id) || spentOfferIds.has(offer.id))
|
|
2128
|
+
continue;
|
|
2129
|
+
assignedItemIds.add(item.id);
|
|
2130
|
+
spentOfferIds.add(offer.id);
|
|
2131
|
+
assignments.push({ itemId: item.id, offerId: offer.id });
|
|
2132
|
+
}
|
|
2133
|
+
return assignments;
|
|
2134
|
+
}
|
|
2135
|
+
// Every line-and-offer pairing the cart could make, and what each would save. Enumerated in cart
|
|
2136
|
+
// order and then catalog order, which `sort` preserves for equal savings, so an unchanged cart
|
|
2137
|
+
// always resolves the same way.
|
|
2138
|
+
static itemOfferCandidates(catalog, items, currency, context, unavailableOfferIds) {
|
|
2139
|
+
var _a;
|
|
2140
|
+
const candidates = [];
|
|
2141
|
+
for (const item of items) {
|
|
2142
|
+
const unitPrice = _CartUtils.unitPrice(catalog, item, currency);
|
|
2143
|
+
if (unitPrice === void 0)
|
|
2144
|
+
continue;
|
|
2145
|
+
for (const offer of (_a = catalog.offers) != null ? _a : []) {
|
|
2146
|
+
if (offer.scope === "Cart" || (unavailableOfferIds == null ? void 0 : unavailableOfferIds.has(offer.id)))
|
|
2147
|
+
continue;
|
|
2148
|
+
if (!_CartUtils.offerTargetsLine(offer, item))
|
|
2149
|
+
continue;
|
|
2150
|
+
if (!OfferUtils.allTargetsInCart(offer, context.order.items))
|
|
2151
|
+
continue;
|
|
2152
|
+
if (!OfferUtils.eligible(offer, context))
|
|
2153
|
+
continue;
|
|
2154
|
+
const value = _CartUtils.itemOfferUnitDiscount(offer, unitPrice, currency) * item.quantity;
|
|
2155
|
+
if (value <= 0)
|
|
2156
|
+
continue;
|
|
2157
|
+
candidates.push({ item, offer, value });
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
return candidates;
|
|
2161
|
+
}
|
|
2162
|
+
// What an item offer takes off one unit of a line. Measured off the post-product-discount price,
|
|
2163
|
+
// which is what an item offer comes off in the API's order: product discount -> line offer -> cart
|
|
2164
|
+
// offer -> coupons.
|
|
2165
|
+
static itemOfferUnitDiscount(offer, unitPrice, currency) {
|
|
2166
|
+
return DiscountUtils.apply(offer.discount, { [currency]: unitPrice })[currency];
|
|
2167
|
+
}
|
|
1993
2168
|
// A line's unit price once its own offer has come off, which is what a cart offer is applied to:
|
|
1994
2169
|
// the API discounts in the order product discount -> line offer -> cart offer -> coupons. An
|
|
1995
2170
|
// unresolvable price counts as zero, matching `total`'s "still render something" stance rather
|
|
@@ -2000,9 +2175,14 @@ var CartUtils = class _CartUtils {
|
|
|
2000
2175
|
const itemOffer = item.offerId ? (_b = catalog.offers) == null ? void 0 : _b.find((o) => o.id === item.offerId) : void 0;
|
|
2001
2176
|
if (!itemOffer || !OfferUtils.eligible(itemOffer, context))
|
|
2002
2177
|
return unitPrice;
|
|
2003
|
-
return unitPrice -
|
|
2178
|
+
return unitPrice - _CartUtils.itemOfferUnitDiscount(itemOffer, unitPrice, currency);
|
|
2004
2179
|
}
|
|
2005
2180
|
};
|
|
2181
|
+
// How many distinct offers can compete for the same cart before the exact search is given up on.
|
|
2182
|
+
// 12 leaves 4096 states per line, which is nothing, and no real storefront has a dozen item offers
|
|
2183
|
+
// eligible against one cart at once.
|
|
2184
|
+
_CartUtils.searchableOffers = 12;
|
|
2185
|
+
var CartUtils = _CartUtils;
|
|
2006
2186
|
|
|
2007
2187
|
// src/utils/image.ts
|
|
2008
2188
|
var imageWidths = [16, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonbase.sh/storefront-api",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.1.0",
|
|
5
5
|
"description": "Package to let you build storefronts with Moonbase.sh as payment and delivery provider",
|
|
6
6
|
"author": "Tobias Lønnerød Madsen <m@dsen.tv>",
|
|
7
7
|
"license": "MIT",
|