@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 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) && !cartItems.some((l) => l.type === t.item.type && t.item.id === (l.type === "Product" ? l.productId : l.bundleId)));
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 CartUtils = class _CartUtils {
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
- if (offer.targets.length === 0)
1974
- return true;
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 - DiscountUtils.apply(itemOffer.discount, { [currency]: unitPrice })[currency];
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) && !cartItems.some((l) => l.type === t.item.type && t.item.id === (l.type === "Product" ? l.productId : l.bundleId)));
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 CartUtils = class _CartUtils {
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
- if (offer.targets.length === 0)
1916
- return true;
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 - DiscountUtils.apply(itemOffer.discount, { [currency]: unitPrice })[currency];
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.0.0",
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",