@final-commerce/common 2.4.1-staging2.1 → 2.5.0-staging.1

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.js CHANGED
@@ -19,14 +19,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  // index.ts
20
20
  var index_exports = {};
21
21
  __export(index_exports, {
22
- ADYEN_JOB_TITLE_ASSOCIATION_TYPES: () => ADYEN_JOB_TITLE_ASSOCIATION_TYPES,
23
- ADYEN_LEGAL_ENTITY_STATE_PROVINCE_COUNTRIES: () => ADYEN_LEGAL_ENTITY_STATE_PROVINCE_COUNTRIES,
24
- ADYEN_NO_TAX_NUMBER_ABSENT_COUNTRIES: () => ADYEN_NO_TAX_NUMBER_ABSENT_COUNTRIES,
25
22
  ADYEN_ROLLOUT_COUNTRY_CODES: () => ADYEN_ROLLOUT_COUNTRY_CODES,
26
- ADYEN_STORE_STATE_PROVINCE_COUNTRIES: () => ADYEN_STORE_STATE_PROVINCE_COUNTRIES,
27
- ADYEN_TAX_NUMBER_FORMATS: () => ADYEN_TAX_NUMBER_FORMATS,
28
- ADYEN_TAX_NUMBER_MIN_LENGTH: () => ADYEN_TAX_NUMBER_MIN_LENGTH,
29
23
  AttributeType: () => AttributeType,
24
+ BookingRatePeriod: () => BookingRatePeriod,
25
+ BookingResourceKind: () => BookingResourceKind,
26
+ BookingResourceStatus: () => BookingResourceStatus,
27
+ BookingRulesError: () => BookingRulesError,
28
+ BookingType: () => BookingType,
30
29
  CONDITION_OPERATORS: () => CONDITION_OPERATORS,
31
30
  COUNTRY_CURRENCIES: () => COUNTRY_CURRENCIES,
32
31
  CURRENCY_CONFIG: () => CURRENCY_CONFIG,
@@ -37,6 +36,7 @@ __export(index_exports, {
37
36
  DEFAULT_CONFIG: () => DEFAULT_CONFIG,
38
37
  DEFAULT_CROSS_AXIS_RULES: () => DEFAULT_CROSS_AXIS_RULES,
39
38
  DEFAULT_DISPLAY_STATE_MAP: () => DEFAULT_DISPLAY_STATE_MAP,
39
+ DEFAULT_HOLD_MINUTES: () => DEFAULT_HOLD_MINUTES,
40
40
  DEFAULT_RESOLVED_CONFIG: () => DEFAULT_RESOLVED_CONFIG,
41
41
  DEFAULT_UNIT_ID: () => DEFAULT_UNIT_ID,
42
42
  DualWriteStrategy: () => DualWriteStrategy,
@@ -80,8 +80,10 @@ __export(index_exports, {
80
80
  REPORT_NEGATE_PATHS: () => REPORT_NEGATE_PATHS,
81
81
  ReceiptSendChannel: () => ReceiptSendChannel,
82
82
  ReceiptType: () => ReceiptType,
83
+ ReservationStatus: () => ReservationStatus,
83
84
  ResidualPaymentMethod: () => ResidualPaymentMethod,
84
85
  ResidualStatus: () => ResidualStatus,
86
+ ResourceAvailabilityState: () => ResourceAvailabilityState,
85
87
  STATE_SCHEMA: () => STATE_SCHEMA,
86
88
  STRIPE_SUPPORTED_COUNTRIES: () => STRIPE_SUPPORTED_COUNTRIES,
87
89
  SYSTEM_FAMILIES: () => SYSTEM_FAMILIES,
@@ -95,23 +97,29 @@ __export(index_exports, {
95
97
  TopMetericsSort: () => TopMetericsSort,
96
98
  UserTypes: () => UserTypes,
97
99
  addAmounts: () => addAmounts,
98
- adyenLegalEntityRequiresStateOrProvince: () => adyenLegalEntityRequiresStateOrProvince,
99
- adyenRequiresJobTitle: () => adyenRequiresJobTitle,
100
- adyenStoreRequiresStateOrProvince: () => adyenStoreRequiresStateOrProvince,
101
- adyenSupportsTaxNumberAbsent: () => adyenSupportsTaxNumberAbsent,
102
- adyenTaxNumberFormat: () => adyenTaxNumberFormat,
103
100
  amountToString: () => amountToString,
101
+ ancestorChain: () => ancestorChain,
104
102
  assertUnitSane: () => assertUnitSane,
105
103
  availableIn: () => availableIn,
106
104
  baseFor: () => baseFor,
107
105
  baseFulfillmentDefinitions: () => baseFulfillmentDefinitions,
108
106
  baseOf: () => baseOf,
109
107
  basePaymentDefinitions: () => basePaymentDefinitions,
108
+ buildAvailability: () => buildAvailability,
109
+ buildGrid: () => buildGrid,
110
110
  buildOrderContext: () => buildOrderContext,
111
111
  buildPosPresetConfig: () => buildPosPresetConfig,
112
112
  buildResolvedConfig: () => buildResolvedConfig,
113
113
  calculatePercentage: () => calculatePercentage,
114
114
  canTransition: () => canTransition,
115
+ cheapestCompositeItem: () => cheapestCompositeItem,
116
+ cheapestCompositePrice: () => cheapestCompositePrice,
117
+ compositeItemCost: () => compositeItemCost,
118
+ compositeItemUnavailable: () => compositeItemUnavailable,
119
+ compositeMakeable: () => compositeMakeable,
120
+ compositeModifierMismatches: () => compositeModifierMismatches,
121
+ compositeReservationMismatches: () => compositeReservationMismatches,
122
+ compositeVariantInStock: () => compositeVariantInStock,
115
123
  conditionSummary: () => conditionSummary,
116
124
  convertFixedFeeToLocalCurrency: () => convertFixedFeeToLocalCurrency,
117
125
  createAmount: () => createAmount,
@@ -146,8 +154,11 @@ __export(index_exports, {
146
154
  getPricingRegionFromCountry: () => getPricingRegionFromCountry,
147
155
  getSettlementTotal: () => getSettlementTotal,
148
156
  getTotalCollected: () => getTotalCollected,
157
+ hasSellableChoice: () => hasSellableChoice,
158
+ holdExpiresAt: () => holdExpiresAt,
149
159
  inferStatesFromLegacyStatus: () => inferStatesFromLegacyStatus,
150
160
  isAdyenRolloutCountry: () => isAdyenRolloutCountry,
161
+ isChoiceAvailableAtOutlet: () => isChoiceAvailableAtOutlet,
151
162
  isConditionOperator: () => isConditionOperator,
152
163
  isFulfillmentState: () => isFulfillmentState,
153
164
  isPaymentState: () => isPaymentState,
@@ -158,26 +169,40 @@ __export(index_exports, {
158
169
  mapToLegacyStatus: () => mapToLegacyStatus,
159
170
  maxPrecisionFor: () => maxPrecisionFor,
160
171
  mergeFragment: () => mergeFragment,
172
+ modifiersSellable: () => modifiersSellable,
161
173
  multiplyAmount: () => multiplyAmount,
162
174
  normalizeMongoIdString: () => normalizeMongoIdString,
175
+ occupancyIn: () => occupancyIn,
163
176
  parseAmountToMinorUnits: () => parseAmountToMinorUnits,
164
177
  parseCurrencyCode: () => parseCurrencyCode,
165
178
  parseStripeCurrencyCode: () => parseStripeCurrencyCode,
179
+ planWindow: () => planWindow,
180
+ poolDemand: () => poolDemand,
181
+ poolOf: () => poolOf,
182
+ poolShortfall: () => poolShortfall,
183
+ poolStockAt: () => poolStockAt,
184
+ presentAt: () => presentAt,
166
185
  removeFragment: () => removeFragment,
167
186
  resolveFamily: () => resolveFamily,
187
+ resolveModifiersForProduct: () => resolveModifiersForProduct,
168
188
  resolveTemplate: () => resolveTemplate,
169
189
  resolveUnit: () => resolveUnit,
170
190
  roundToPrecision: () => roundToPrecision,
171
191
  sameFamily: () => sameFamily,
172
192
  setOrderState: () => setOrderState,
193
+ shopDayKey: () => shopDayKey,
194
+ shopDayRange: () => shopDayRange,
173
195
  signReportForDisplay: () => signReportForDisplay,
196
+ slotsForWindow: () => slotsForWindow,
174
197
  stringifyMinorUnits: () => stringifyMinorUnits,
175
198
  subtractAmounts: () => subtractAmounts,
176
199
  toBase: () => toBase,
177
200
  toMinorUnits: () => toMinorUnits,
178
201
  validateConfig: () => validateConfig,
179
202
  validateFragmentCompatibility: () => validateFragmentCompatibility,
180
- violatesFinancialInvariant: () => violatesFinancialInvariant
203
+ violatesFinancialInvariant: () => violatesFinancialInvariant,
204
+ windowEndsAt: () => windowEndsAt,
205
+ withinPresence: () => withinPresence
181
206
  });
182
207
  module.exports = __toCommonJS(index_exports);
183
208
 
@@ -605,6 +630,8 @@ var StripeCurrencyCode = /* @__PURE__ */ ((StripeCurrencyCode2) => {
605
630
  var ProductType = /* @__PURE__ */ ((ProductType2) => {
606
631
  ProductType2["SIMPLE"] = "simple";
607
632
  ProductType2["VARIABLE"] = "variable";
633
+ ProductType2["BOOKING"] = "booking";
634
+ ProductType2["COMPOSITE"] = "composite";
608
635
  return ProductType2;
609
636
  })(ProductType || {});
610
637
  var ProductStatus = /* @__PURE__ */ ((ProductStatus2) => {
@@ -622,6 +649,50 @@ var PaymentProcessor = /* @__PURE__ */ ((PaymentProcessor2) => {
622
649
  return PaymentProcessor2;
623
650
  })(PaymentProcessor || {});
624
651
 
652
+ // enums/booking.enum.ts
653
+ var BookingType = /* @__PURE__ */ ((BookingType3) => {
654
+ BookingType3["RENTAL"] = "rental";
655
+ BookingType3["APPOINTMENT"] = "appointment";
656
+ BookingType3["OVERNIGHT"] = "overnight";
657
+ BookingType3["GROUP"] = "group";
658
+ return BookingType3;
659
+ })(BookingType || {});
660
+ var BookingRatePeriod = /* @__PURE__ */ ((BookingRatePeriod2) => {
661
+ BookingRatePeriod2["SLOT"] = "slot";
662
+ BookingRatePeriod2["DAY"] = "day";
663
+ BookingRatePeriod2["NIGHT"] = "night";
664
+ BookingRatePeriod2["WEEK"] = "week";
665
+ BookingRatePeriod2["MONTH"] = "month";
666
+ return BookingRatePeriod2;
667
+ })(BookingRatePeriod || {});
668
+ var BookingResourceKind = /* @__PURE__ */ ((BookingResourceKind2) => {
669
+ BookingResourceKind2["ASSET"] = "asset";
670
+ BookingResourceKind2["ROOM"] = "room";
671
+ BookingResourceKind2["PROPERTY"] = "property";
672
+ BookingResourceKind2["SPACE"] = "space";
673
+ BookingResourceKind2["STAFF"] = "staff";
674
+ BookingResourceKind2["TABLE"] = "table";
675
+ return BookingResourceKind2;
676
+ })(BookingResourceKind || {});
677
+ var ReservationStatus = /* @__PURE__ */ ((ReservationStatus2) => {
678
+ ReservationStatus2["HELD"] = "held";
679
+ ReservationStatus2["CONFIRMED"] = "confirmed";
680
+ ReservationStatus2["CANCELLED"] = "cancelled";
681
+ ReservationStatus2["EXPIRED"] = "expired";
682
+ return ReservationStatus2;
683
+ })(ReservationStatus || {});
684
+ var BookingResourceStatus = /* @__PURE__ */ ((BookingResourceStatus2) => {
685
+ BookingResourceStatus2["ACTIVE"] = "active";
686
+ BookingResourceStatus2["INACTIVE"] = "inactive";
687
+ return BookingResourceStatus2;
688
+ })(BookingResourceStatus || {});
689
+ var ResourceAvailabilityState = /* @__PURE__ */ ((ResourceAvailabilityState2) => {
690
+ ResourceAvailabilityState2["AVAILABLE"] = "available";
691
+ ResourceAvailabilityState2["FULLY_BOOKED"] = "fullyBooked";
692
+ ResourceAvailabilityState2["NO_HOURS_IN_RANGE"] = "noHoursInRange";
693
+ return ResourceAvailabilityState2;
694
+ })(ResourceAvailabilityState || {});
695
+
625
696
  // utils/currency.util.ts
626
697
  var CURRENCY_CONFIG = {
627
698
  // Major currencies with 2 decimal places
@@ -1491,6 +1562,309 @@ function normalizeMongoIdString(value) {
1491
1562
  return null;
1492
1563
  }
1493
1564
 
1565
+ // utils/composite-price.util.ts
1566
+ var compositeItemCost = (item) => item.extraCharge * item.quantity;
1567
+ function cheapestCompositeItem(items) {
1568
+ let cheapest;
1569
+ items.forEach((item, index) => {
1570
+ if (!item.available) return;
1571
+ if (cheapest === void 0 || compositeItemCost(item) < compositeItemCost(items[cheapest])) cheapest = index;
1572
+ });
1573
+ return cheapest;
1574
+ }
1575
+ function cheapestCompositePrice(parts) {
1576
+ let total = 0;
1577
+ for (const part of parts) {
1578
+ if (part.min === 0) continue;
1579
+ const index = cheapestCompositeItem(part.items) ?? cheapestCompositeItem(part.items.map((item) => ({ ...item, available: true })));
1580
+ if (index === void 0) return void 0;
1581
+ total += part.min * compositeItemCost(part.items[index]);
1582
+ }
1583
+ return total;
1584
+ }
1585
+
1586
+ // utils/modifier-resolution.util.ts
1587
+ function ancestorChain(categoryId, categoriesById) {
1588
+ const chain = [];
1589
+ const seen = /* @__PURE__ */ new Set([categoryId]);
1590
+ let current = categoriesById.get(categoryId)?.parentId ?? void 0;
1591
+ while (current && !seen.has(current)) {
1592
+ chain.push(current);
1593
+ seen.add(current);
1594
+ current = categoriesById.get(current)?.parentId ?? void 0;
1595
+ }
1596
+ return chain;
1597
+ }
1598
+ function clampRule(rule) {
1599
+ if (rule.min != null && rule.max != null && rule.min > rule.max) {
1600
+ return { ...rule, min: rule.max };
1601
+ }
1602
+ return rule;
1603
+ }
1604
+ function mergeRules(a, b) {
1605
+ const required = a.required || b.required;
1606
+ const min = a.min == null ? b.min : b.min == null ? a.min : Math.max(a.min, b.min);
1607
+ const max = a.max == null ? b.max : b.max == null ? a.max : Math.min(a.max, b.max);
1608
+ return clampRule({ required, min, max, sortOrder: Math.min(a.sortOrder, b.sortOrder) });
1609
+ }
1610
+ function toRule(row) {
1611
+ const required = row.required ?? false;
1612
+ return clampRule({
1613
+ required,
1614
+ // `required` is the gate: a stale min left on a placement that went optional is
1615
+ // ignored. A cleared min/max stays the explicit null it arrived as — nothing
1616
+ // downstream distinguishes null from absent.
1617
+ min: required ? row.min : void 0,
1618
+ max: row.max,
1619
+ sortOrder: row.sortOrder ?? 0
1620
+ });
1621
+ }
1622
+ function toResolvedChoices(modifier) {
1623
+ return (modifier.choices ?? []).map((choice, index) => ({
1624
+ _id: String(choice._id),
1625
+ name: choice.name,
1626
+ price: choice.price ?? 0,
1627
+ isDefault: choice.isDefault,
1628
+ defaultQuantity: choice.defaultQuantity,
1629
+ unavailableOutletIds: choice.unavailableOutletIds?.map(String),
1630
+ sortOrder: choice.sortOrder ?? index
1631
+ })).sort((a, b) => a.sortOrder - b.sortOrder);
1632
+ }
1633
+ function resolveModifiersForProduct(inputs) {
1634
+ const categoriesById = new Map(
1635
+ inputs.categories.filter((category) => !category.isDeleted).map((category) => [String(category._id), category])
1636
+ );
1637
+ const modifiersById = new Map(
1638
+ inputs.modifiers.filter((modifier) => !modifier.isDeleted).map((modifier) => [String(modifier._id), modifier])
1639
+ );
1640
+ const directCategories = new Set(inputs.categoryIds.map(String));
1641
+ const inheritedCategories = /* @__PURE__ */ new Set();
1642
+ for (const categoryId of directCategories) {
1643
+ for (const ancestor of ancestorChain(categoryId, categoriesById)) {
1644
+ if (!directCategories.has(ancestor)) inheritedCategories.add(ancestor);
1645
+ }
1646
+ }
1647
+ const rules = /* @__PURE__ */ new Map();
1648
+ for (const placement of inputs.categoryModifiers) {
1649
+ if (placement.isDeleted) continue;
1650
+ const categoryId = String(placement.categoryId);
1651
+ const precedence = directCategories.has(categoryId) ? 0 : inheritedCategories.has(categoryId) ? 1 : -1;
1652
+ if (precedence === -1) continue;
1653
+ const modifierId = String(placement.modifierId);
1654
+ if (!modifiersById.has(modifierId)) continue;
1655
+ const incoming = toRule(placement);
1656
+ const existing = rules.get(modifierId);
1657
+ if (!existing || precedence < existing.precedence) {
1658
+ rules.set(modifierId, { precedence, rule: incoming });
1659
+ } else if (precedence === existing.precedence) {
1660
+ rules.set(modifierId, { precedence, rule: mergeRules(existing.rule, incoming) });
1661
+ }
1662
+ }
1663
+ const resolved = [];
1664
+ for (const [modifierId, { rule }] of rules) {
1665
+ const modifier = modifiersById.get(modifierId);
1666
+ if (!modifier) continue;
1667
+ const required = rule.required;
1668
+ resolved.push({
1669
+ _id: modifierId,
1670
+ name: modifier.name,
1671
+ selectionType: modifier.selectionType,
1672
+ allowQuantity: modifier.allowQuantity,
1673
+ required,
1674
+ // single-selection is implicitly capped at 1 — min/max are never carried.
1675
+ min: modifier.selectionType === "single" ? void 0 : rule.min,
1676
+ max: modifier.selectionType === "single" ? void 0 : rule.max,
1677
+ sortOrder: rule.sortOrder,
1678
+ choices: toResolvedChoices(modifier)
1679
+ });
1680
+ }
1681
+ return resolved.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name));
1682
+ }
1683
+ function isChoiceAvailableAtOutlet(choice, outletId) {
1684
+ return !(choice.unavailableOutletIds ?? []).includes(outletId);
1685
+ }
1686
+ function hasSellableChoice(modifier, outletId) {
1687
+ return modifier.choices.some((choice) => isChoiceAvailableAtOutlet(choice, outletId));
1688
+ }
1689
+ var modifiersSellable = (modifiers, outletId) => modifiers.every((modifier) => !modifier.required || hasSellableChoice(modifier, outletId));
1690
+
1691
+ // utils/composite-availability.util.ts
1692
+ function poolStockAt(rows, outletId) {
1693
+ if (!rows.some((row) => row.manageStock === true)) return null;
1694
+ return Math.max(0, rows.find((row) => String(row.outletId) === outletId)?.quantity ?? 0);
1695
+ }
1696
+ function compositeVariantInStock(variantId, quantity, outlet) {
1697
+ const shelf = outlet.shelves.get(variantId);
1698
+ if (!shelf || shelf.unresolved) return false;
1699
+ const onHand = outlet.poolStock.get(shelf.pool);
1700
+ if (onHand === void 0) return false;
1701
+ if (onHand === null || shelf.backorder) return true;
1702
+ return onHand >= (shelf.unit ? toBase(quantity, shelf.unit) : quantity);
1703
+ }
1704
+ function compositeItemUnavailable(item, rows) {
1705
+ const { outlet } = rows;
1706
+ const sellableHere = (productId2) => !outlet || !outlet.hiddenProductIds.has(productId2) && !outlet.unsellableProductIds.has(productId2);
1707
+ if (item.categoryId) {
1708
+ const variants = rows.categoryVariants.get(String(item.categoryId));
1709
+ if (!variants) return "deleted";
1710
+ const offered = variants.filter((variantId2) => sellableHere(rows.variantProducts.get(variantId2) ?? ""));
1711
+ if (!offered.length) return "empty";
1712
+ if (!outlet) return null;
1713
+ return offered.some((variantId2) => compositeVariantInStock(variantId2, item.quantity, outlet)) ? null : "out_of_stock";
1714
+ }
1715
+ const variantId = String(item.variantId);
1716
+ const productId = rows.variantProducts.get(variantId);
1717
+ if (!productId || !rows.productStatuses.has(productId)) return "deleted";
1718
+ if (rows.productStatuses.get(productId) !== "active" /* ACTIVE */) return "inactive";
1719
+ if (!outlet) return null;
1720
+ if (outlet.hiddenProductIds.has(productId)) return "hidden";
1721
+ if (outlet.unsellableProductIds.has(productId)) return "empty";
1722
+ if (rows.bookableProductIds.has(productId)) {
1723
+ if (!outlet.bookableSeats) return "by_date";
1724
+ return (outlet.bookableSeats.get(productId) ?? 0) >= item.quantity ? null : "fully_booked";
1725
+ }
1726
+ return compositeVariantInStock(variantId, item.quantity, outlet) ? null : "out_of_stock";
1727
+ }
1728
+ function compositeMakeable(parts, rows) {
1729
+ const { outlet } = rows;
1730
+ if (!outlet) throw new Error("compositeMakeable counts at one outlet: rows.outlet is required");
1731
+ const required = parts.map((part, partIndex) => ({ ...part, partIndex })).filter((part) => part.min > 0);
1732
+ const isBookable = (variantId) => rows.bookableProductIds.has(rows.variantProducts.get(String(variantId)) ?? "");
1733
+ if (required.some((part) => part.items.some((item) => item.variantId && isBookable(item.variantId))))
1734
+ return { byDate: true };
1735
+ const optionsOf = (item) => (item.categoryId ? rows.categoryVariants.get(String(item.categoryId)) ?? [] : [String(item.variantId)]).flatMap(
1736
+ (variantId) => {
1737
+ const reason = compositeItemUnavailable({ variantId, quantity: item.quantity }, rows);
1738
+ const shelf = outlet.shelves.get(variantId);
1739
+ if (reason && reason !== "out_of_stock" || !shelf) return [];
1740
+ const stock = outlet.poolStock.get(shelf.pool);
1741
+ const onHand = shelf.unresolved || stock === void 0 ? 0 : stock === null || shelf.backorder ? null : stock;
1742
+ const perPick = shelf.unit ? toBase(item.quantity, shelf.unit) : item.quantity;
1743
+ return [{ itemId: item.id, pool: shelf.pool, perPick, onHand }];
1744
+ }
1745
+ );
1746
+ const counted = required.map((part) => ({ ...part, options: part.items.flatMap(optionsOf) }));
1747
+ const order = [...counted].sort((a, b) => Number(a.options.length !== 1) - Number(b.options.length !== 1));
1748
+ const picksLeft = (option, reserved) => option.onHand === null ? Infinity : Math.max(0, Math.floor((option.onHand - (reserved.get(option.pool) ?? 0)) / option.perPick));
1749
+ const shortAt = (n) => {
1750
+ const reserved = /* @__PURE__ */ new Map();
1751
+ for (const part of order) {
1752
+ let wanted = part.min * n;
1753
+ const ranked = [...part.options].sort((a, b) => picksLeft(b, reserved) - picksLeft(a, reserved));
1754
+ for (const option of ranked) {
1755
+ if (wanted <= 0) break;
1756
+ const taken = Math.min(wanted, picksLeft(option, reserved));
1757
+ if (option.onHand !== null)
1758
+ reserved.set(option.pool, (reserved.get(option.pool) ?? 0) + taken * option.perPick);
1759
+ wanted -= taken;
1760
+ }
1761
+ if (wanted > 0) return part.partIndex;
1762
+ }
1763
+ return void 0;
1764
+ };
1765
+ const whyRows = [];
1766
+ const rowPools = /* @__PURE__ */ new Map();
1767
+ const makes = (has, uses) => has === null ? null : Math.floor(has / uses);
1768
+ for (const part of counted) {
1769
+ const [option] = part.options;
1770
+ if (part.options.length === 1) {
1771
+ const uses = option.perPick * part.min;
1772
+ const row = whyRows.find((candidate) => rowPools.get(candidate) === option.pool);
1773
+ if (!row) {
1774
+ const created = { itemIds: [option.itemId], uses, has: option.onHand, makes: makes(option.onHand, uses) };
1775
+ rowPools.set(created, option.pool);
1776
+ whyRows.push(created);
1777
+ continue;
1778
+ }
1779
+ row.itemIds.push(option.itemId);
1780
+ row.uses += uses;
1781
+ if (option.onHand === null) row.has = null;
1782
+ row.makes = makes(row.has, row.uses);
1783
+ continue;
1784
+ }
1785
+ const perPool = /* @__PURE__ */ new Map();
1786
+ for (const candidate of part.options)
1787
+ perPool.set(candidate.pool, Math.max(perPool.get(candidate.pool) ?? 0, picksLeft(candidate, /* @__PURE__ */ new Map())));
1788
+ const total = [...perPool.values()].reduce((sum, picks) => sum + picks, 0);
1789
+ const has = total === Infinity ? null : total;
1790
+ whyRows.push({
1791
+ itemIds: part.items.map((item) => item.id),
1792
+ partIndex: part.partIndex,
1793
+ uses: part.min,
1794
+ has,
1795
+ makes: makes(has, part.min)
1796
+ });
1797
+ }
1798
+ if (counted.every((part) => part.options.some((option) => option.onHand === null)))
1799
+ return { count: null, rows: whyRows };
1800
+ const ceiling = Math.min(
1801
+ ...counted.filter((part) => !part.options.some((option) => option.onHand === null)).map(
1802
+ (part) => Math.floor(part.options.reduce((sum, option) => sum + picksLeft(option, /* @__PURE__ */ new Map()), 0) / part.min)
1803
+ )
1804
+ );
1805
+ let [low, high] = [0, ceiling];
1806
+ while (low < high) {
1807
+ const mid = Math.ceil((low + high) / 2);
1808
+ if (shortAt(mid) === void 0) low = mid;
1809
+ else high = mid - 1;
1810
+ }
1811
+ const short = shortAt(low + 1);
1812
+ const shortPart = counted.find((part) => part.partIndex === short);
1813
+ const limitedBy = whyRows.findIndex(
1814
+ (row) => shortPart?.options.length === 1 ? row.partIndex === void 0 && row.itemIds.includes(shortPart.options[0].itemId) : row.partIndex === short
1815
+ );
1816
+ return { count: low, rows: whyRows, ...limitedBy >= 0 ? { limitedBy } : {} };
1817
+ }
1818
+ var poolOf = (line) => line.stockVariantId || line.variantId;
1819
+ function poolDemand(lines) {
1820
+ const demand = /* @__PURE__ */ new Map();
1821
+ const add = (line, times) => demand.set(
1822
+ poolOf(line),
1823
+ (demand.get(poolOf(line)) ?? 0) + (line.unit ? toBase(line.quantity * times, line.unit) : line.quantity * times)
1824
+ );
1825
+ for (const line of lines) {
1826
+ if (line.components) for (const component of line.components) add(component, line.quantity);
1827
+ else add(line, 1);
1828
+ }
1829
+ return demand;
1830
+ }
1831
+ function poolShortfall(demand, poolStock, backordered = /* @__PURE__ */ new Set()) {
1832
+ for (const [poolId, wanted] of demand) {
1833
+ const available = poolStock.get(poolId);
1834
+ if (available === null || available === void 0 || backordered.has(poolId)) continue;
1835
+ if (wanted > available) return { poolId, available, wanted };
1836
+ }
1837
+ return null;
1838
+ }
1839
+
1840
+ // utils/composite-reservation.util.ts
1841
+ function compositeModifierMismatches(lineItems) {
1842
+ return lineItems.flatMap(
1843
+ (line) => line.components?.length ? (line.modifiers ?? []).flatMap(
1844
+ (modifier) => modifier.componentIndex !== void 0 && line.components[modifier.componentIndex] !== void 0 ? [] : [
1845
+ `Modifier ${modifier.modifierName ?? "?"} on composite line ${line.internalId ?? "?"} names no component (componentIndex ${modifier.componentIndex}) \u2014 a composite has no modifiers of its own`
1846
+ ]
1847
+ ) : []
1848
+ );
1849
+ }
1850
+ function compositeReservationMismatches(order) {
1851
+ return (order.reservations ?? []).flatMap((reservation) => {
1852
+ if (reservation.lineItemInternalId === void 0) return [];
1853
+ const name = `Booking ${String(reservation.bookingId ?? "?")}`;
1854
+ const line = order.lineItems?.find((candidate) => candidate.internalId === reservation.lineItemInternalId);
1855
+ if (!line) return [`${name} names line ${reservation.lineItemInternalId}, which is not on the order`];
1856
+ const component = reservation.componentIndex === void 0 ? void 0 : line.components?.[reservation.componentIndex];
1857
+ if (!component) return [`${name} names no component ${reservation.componentIndex} of line ${line.internalId}`];
1858
+ if (String(component.productId) !== String(reservation.productId))
1859
+ return [
1860
+ `${name} is for ${String(reservation.productId)}, but that component sold ${String(component.productId)}`
1861
+ ];
1862
+ if (reservation.price !== 0 || reservation.total !== 0)
1863
+ return [`${name} carries money (${reservation.price}/${reservation.total}); its component already does`];
1864
+ return [];
1865
+ });
1866
+ }
1867
+
1494
1868
  // units/family-code.ts
1495
1869
  var MAX_SLUG_LENGTH = 32;
1496
1870
  var FALLBACK_SLUG = "family";
@@ -1833,6 +2207,15 @@ function sumRefundQuantities(refunds) {
1833
2207
  }
1834
2208
  return t;
1835
2209
  }
2210
+ var asCount = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
2211
+ function countsOf(row) {
2212
+ const quantity = asCount(row.quantity);
2213
+ return {
2214
+ quantity,
2215
+ fulfilled: Math.min(asCount(row.fulfilledQuantity), quantity),
2216
+ returned: Math.min(asCount(row.returnedQuantity), quantity)
2217
+ };
2218
+ }
1836
2219
  function buildOrderContext(order, extras) {
1837
2220
  const o = order ?? {};
1838
2221
  const total = typeof o.total === "number" && Number.isFinite(o.total) ? o.total : 0;
@@ -1840,6 +2223,7 @@ function buildOrderContext(order, extras) {
1840
2223
  const paymentMethods = o.paymentMethods ?? [];
1841
2224
  const refunds = o.refunds ?? o.refund ?? [];
1842
2225
  const lineItems = o.lineItems ?? [];
2226
+ const reservations = o.reservations ?? [];
1843
2227
  const totalPaid = sumNumbers(paymentMethods);
1844
2228
  const totalRefunded = sumRefundQuantities(refunds);
1845
2229
  const isFullyPaid = total > 0 ? balance <= 0 : totalPaid > 0 && balance <= 0;
@@ -1847,13 +2231,11 @@ function buildOrderContext(order, extras) {
1847
2231
  let itemCount = 0;
1848
2232
  let fulfilledItemCount = 0;
1849
2233
  let returnedItemCount = 0;
1850
- for (const li of lineItems) {
1851
- const q = typeof li.quantity === "number" && Number.isFinite(li.quantity) ? Math.max(0, li.quantity) : 0;
1852
- itemCount += q;
1853
- const fq = typeof li.fulfilledQuantity === "number" && Number.isFinite(li.fulfilledQuantity) ? Math.max(0, li.fulfilledQuantity) : 0;
1854
- fulfilledItemCount += Math.min(fq, q);
1855
- const rq = typeof li.returnedQuantity === "number" && Number.isFinite(li.returnedQuantity) ? Math.max(0, li.returnedQuantity) : 0;
1856
- returnedItemCount += Math.min(rq, q);
2234
+ for (const row of [...lineItems, ...reservations]) {
2235
+ const { quantity, fulfilled, returned } = countsOf(row);
2236
+ itemCount += quantity;
2237
+ fulfilledItemCount += fulfilled;
2238
+ returnedItemCount += returned;
1857
2239
  }
1858
2240
  const isFullyReturned = itemCount > 0 ? returnedItemCount >= itemCount : returnedItemCount > 0;
1859
2241
  const hasPartialReturns = returnedItemCount > 0 && !isFullyReturned;
@@ -1863,7 +2245,8 @@ function buildOrderContext(order, extras) {
1863
2245
  balance,
1864
2246
  paymentMethods,
1865
2247
  refunds,
1866
- lineItems
2248
+ lineItems,
2249
+ reservations
1867
2250
  },
1868
2251
  computed: {
1869
2252
  balance,
@@ -2943,44 +3326,409 @@ var ORDER_STATE_EVENTS = {
2943
3326
  configUpdated: "config-updated"
2944
3327
  };
2945
3328
 
2946
- // constants/adyen.constants.ts
2947
- var ADYEN_ROLLOUT_COUNTRY_CODES = /* @__PURE__ */ new Set(["CA"]);
2948
- function isAdyenRolloutCountry(alpha2) {
2949
- return ADYEN_ROLLOUT_COUNTRY_CODES.has(alpha2);
3329
+ // booking/types.ts
3330
+ var BookingRulesError = class extends Error {
3331
+ constructor(message) {
3332
+ super(message);
3333
+ this.name = "BookingRulesError";
3334
+ }
3335
+ };
3336
+
3337
+ // booking/zoned-time.ts
3338
+ var MINUTE = 6e4;
3339
+ var formatters = /* @__PURE__ */ new Map();
3340
+ var formatterFor = (timeZone) => {
3341
+ const existing = formatters.get(timeZone);
3342
+ if (existing) return existing;
3343
+ let created;
3344
+ try {
3345
+ created = new Intl.DateTimeFormat("en-US", {
3346
+ timeZone,
3347
+ hour12: false,
3348
+ year: "numeric",
3349
+ month: "2-digit",
3350
+ day: "2-digit",
3351
+ hour: "2-digit",
3352
+ minute: "2-digit",
3353
+ second: "2-digit",
3354
+ weekday: "short"
3355
+ });
3356
+ } catch {
3357
+ throw new BookingRulesError(`Booking rules: "${timeZone}" is not a known time zone`);
3358
+ }
3359
+ formatters.set(timeZone, created);
3360
+ return created;
3361
+ };
3362
+ var WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
3363
+ function zonedParts(instant, timeZone) {
3364
+ const parts = {};
3365
+ for (const { type, value } of formatterFor(timeZone).formatToParts(instant)) parts[type] = value;
3366
+ const hour = Number(parts.hour) % 24;
3367
+ return {
3368
+ year: Number(parts.year),
3369
+ month: Number(parts.month),
3370
+ day: Number(parts.day),
3371
+ hour,
3372
+ minute: Number(parts.minute),
3373
+ second: Number(parts.second),
3374
+ weekday: WEEKDAYS.indexOf(parts.weekday)
3375
+ };
3376
+ }
3377
+ function offsetAt(instant, timeZone) {
3378
+ const { year, month, day, hour, minute, second } = zonedParts(instant, timeZone);
3379
+ return Date.UTC(year, month - 1, day, hour, minute, second) - Math.floor(instant.getTime() / 1e3) * 1e3;
3380
+ }
3381
+ function instantFromLocal(year, month, day, minutes, timeZone) {
3382
+ const naive = Date.UTC(year, month - 1, day) + minutes * MINUTE;
3383
+ const firstGuess = naive - offsetAt(new Date(naive), timeZone);
3384
+ return new Date(naive - offsetAt(new Date(firstGuess), timeZone));
3385
+ }
3386
+ function startOfZonedDay(instant, timeZone) {
3387
+ const { year, month, day } = zonedParts(instant, timeZone);
3388
+ return instantFromLocal(year, month, day, 0, timeZone);
3389
+ }
3390
+ function nextZonedDay(dayStart, timeZone) {
3391
+ const { year, month, day } = zonedParts(dayStart, timeZone);
3392
+ return instantFromLocal(year, month, day + 1, 0, timeZone);
2950
3393
  }
2951
- var ADYEN_STORE_STATE_PROVINCE_COUNTRIES = /* @__PURE__ */ new Set(["AU", "BR", "CA", "IN", "MX", "NZ", "US"]);
2952
- function adyenStoreRequiresStateOrProvince(alpha2) {
2953
- return !!alpha2 && ADYEN_STORE_STATE_PROVINCE_COUNTRIES.has(alpha2);
3394
+ function atZonedMinute(dayStart, minutes, timeZone) {
3395
+ const { year, month, day } = zonedParts(dayStart, timeZone);
3396
+ return instantFromLocal(year, month, day, minutes, timeZone);
3397
+ }
3398
+
3399
+ // booking/grid.ts
3400
+ var MINUTE2 = 6e4;
3401
+ var MAX_RANGE_DAYS = 366;
3402
+ var asTime = (value, field) => {
3403
+ if (value == null) return void 0;
3404
+ const time = new Date(value).getTime();
3405
+ if (Number.isNaN(time)) throw new BookingRulesError(`Booking rules: ${field} is not a date, got "${String(value)}"`);
3406
+ return time;
3407
+ };
3408
+ function parseHhMm(value, field) {
3409
+ const [hours, minutes] = value.split(":").map(Number);
3410
+ if (!Number.isInteger(hours) || !Number.isInteger(minutes)) {
3411
+ throw new BookingRulesError(`Booking rules: ${field} must be "HH:mm", got "${value}"`);
3412
+ }
3413
+ return hours * 60 + minutes;
3414
+ }
3415
+ function windowEndsAt(rule, now) {
3416
+ const { maxAdvanceDays, bookableTo } = rule.window;
3417
+ const ends = Math.min(
3418
+ maxAdvanceDays ? now.getTime() + maxAdvanceDays * 1440 * MINUTE2 : Number.POSITIVE_INFINITY,
3419
+ asTime(bookableTo, "window.bookableTo") ?? Number.POSITIVE_INFINITY
3420
+ );
3421
+ return Number.isFinite(ends) ? new Date(ends) : null;
3422
+ }
3423
+ function periodSlot(dayStart, timeZone, rule, openMinute, closeMinute) {
3424
+ const { ratePeriod, slot, checkInTime, checkOutTime } = rule;
3425
+ const at = (minutes) => atZonedMinute(dayStart, minutes, timeZone);
3426
+ if (ratePeriod === "night" /* NIGHT */) {
3427
+ if (!checkInTime || !checkOutTime) {
3428
+ throw new BookingRulesError("Overnight booking rules need both checkInTime and checkOutTime");
3429
+ }
3430
+ const checkIn = parseHhMm(checkInTime, "checkInTime");
3431
+ const checkOut = parseHhMm(checkOutTime, "checkOutTime");
3432
+ return {
3433
+ startAt: at(checkIn),
3434
+ endAt: at(1440 + checkOut),
3435
+ // Housekeeping sits between check-out and the next check-in: it costs no night.
3436
+ bufferEndAt: at(1440 + checkIn),
3437
+ canStart: true
3438
+ };
3439
+ }
3440
+ const from = ratePeriod === "day" /* DAY */ ? openMinute ?? 0 : 0;
3441
+ const to = ratePeriod === "day" /* DAY */ ? closeMinute ?? 1440 : 1440;
3442
+ return {
3443
+ startAt: at(from),
3444
+ endAt: at(to),
3445
+ bufferEndAt: at(to + (slot.bufferMinutes ?? 0)),
3446
+ canStart: true
3447
+ };
2954
3448
  }
2955
- var ADYEN_LEGAL_ENTITY_STATE_PROVINCE_COUNTRIES = /* @__PURE__ */ new Set(["AU", "BR", "CA", "IN", "MX", "NZ", "US"]);
2956
- function adyenLegalEntityRequiresStateOrProvince(alpha2) {
2957
- return !!alpha2 && ADYEN_LEGAL_ENTITY_STATE_PROVINCE_COUNTRIES.has(alpha2);
3449
+ function freeGaps(openAt, closeAt, busy) {
3450
+ const inside = busy.map(({ startAt, bufferEndAt }) => ({ from: +startAt, to: +bufferEndAt })).filter(({ from, to }) => to > openAt && from < closeAt).sort((first, second) => first.from - second.from);
3451
+ const gaps = [];
3452
+ let cursor = openAt;
3453
+ for (const { from, to } of inside) {
3454
+ if (from > cursor) gaps.push({ from: cursor, to: from, endsAtBooking: true });
3455
+ cursor = Math.max(cursor, to);
3456
+ if (cursor >= closeAt) break;
3457
+ }
3458
+ if (cursor < closeAt) gaps.push({ from: cursor, to: closeAt, endsAtBooking: false });
3459
+ return gaps;
3460
+ }
3461
+ function buildGrid(rule, timeZone, from, to, now, busy = []) {
3462
+ if (to.getTime() - from.getTime() > MAX_RANGE_DAYS * 1440 * MINUTE2) {
3463
+ throw new BookingRulesError(`A booking range may span at most ${MAX_RANGE_DAYS} days`);
3464
+ }
3465
+ const { availability, slot, window, ratePeriod } = rule;
3466
+ const periodMode = ratePeriod !== "slot" /* SLOT */;
3467
+ const weeklyHours = availability.weeklyHours ?? [];
3468
+ const notBefore = Math.max(
3469
+ now.getTime() + (window.minNoticeMinutes ?? 0) * MINUTE2,
3470
+ asTime(window.bookableFrom, "window.bookableFrom") ?? 0,
3471
+ asTime(availability.availableFrom, "availability.availableFrom") ?? 0
3472
+ );
3473
+ const notAfter = windowEndsAt(rule, now)?.getTime() ?? Number.POSITIVE_INFINITY;
3474
+ const blocked = (availability.offMarket ?? []).map(({ from: blockFrom, to: blockTo }) => ({
3475
+ from: asTime(blockFrom, "availability.offMarket.from"),
3476
+ to: asTime(blockTo, "availability.offMarket.to")
3477
+ }));
3478
+ const arrivalDays = availability.arrivalDays;
3479
+ const step = slot.lengthMinutes + (slot.bufferMinutes ?? 0);
3480
+ const slots = [];
3481
+ const lastDay = startOfZonedDay(to, timeZone).getTime();
3482
+ let dayStart = startOfZonedDay(from, timeZone);
3483
+ while (dayStart.getTime() <= lastDay) {
3484
+ const weekday = zonedParts(dayStart, timeZone).weekday;
3485
+ const rows = weeklyHours.filter((entry) => entry.weekday === weekday);
3486
+ if (periodMode) {
3487
+ const openToday = weeklyHours.length === 0 || rows.length > 0;
3488
+ if (openToday) {
3489
+ const built = periodSlot(dayStart, timeZone, rule, rows[0]?.openMinute, rows[0]?.closeMinute);
3490
+ slots.push({ ...built, canStart: !arrivalDays?.length || arrivalDays.includes(weekday) });
3491
+ }
3492
+ } else {
3493
+ for (const row of rows) {
3494
+ const openAt = +atZonedMinute(dayStart, row.openMinute, timeZone);
3495
+ const closeAt = +atZonedMinute(dayStart, row.closeMinute, timeZone);
3496
+ for (const gap of freeGaps(openAt, closeAt, busy)) {
3497
+ const mustFit = (gap.endsAtBooking ? step : slot.lengthMinutes) * MINUTE2;
3498
+ for (let start = gap.from; start + mustFit <= gap.to; start += step * MINUTE2) {
3499
+ slots.push({
3500
+ startAt: new Date(start),
3501
+ endAt: new Date(start + slot.lengthMinutes * MINUTE2),
3502
+ bufferEndAt: new Date(start + step * MINUTE2),
3503
+ canStart: true
3504
+ });
3505
+ }
3506
+ }
3507
+ }
3508
+ }
3509
+ dayStart = nextZonedDay(dayStart, timeZone);
3510
+ }
3511
+ const askedFrom = from.getTime();
3512
+ const askedTo = to.getTime();
3513
+ return slots.filter(({ startAt, bufferEndAt }) => {
3514
+ const start = startAt.getTime();
3515
+ if (start < askedFrom || start >= askedTo) return false;
3516
+ if (start < notBefore || start > notAfter) return false;
3517
+ return !blocked.some(
3518
+ ({ from: blockFrom, to: blockTo }) => start < blockTo && bufferEndAt.getTime() > blockFrom
3519
+ );
3520
+ }).sort((first, second) => +first.startAt - +second.startAt);
3521
+ }
3522
+ function slotsForWindow(grid, startAt, endAt) {
3523
+ const startIndex = grid.findIndex((entry) => entry.startAt.getTime() === startAt.getTime());
3524
+ if (startIndex < 0) {
3525
+ throw new BookingRulesError(`${startAt.toISOString()} is not a bookable slot start for these rules`);
3526
+ }
3527
+ if (!grid[startIndex].canStart) {
3528
+ throw new BookingRulesError(`${startAt.toISOString()} is not an arrival day for these rules`);
3529
+ }
3530
+ const covered = [];
3531
+ for (let index = startIndex; index < grid.length; index++) {
3532
+ const entry = grid[index];
3533
+ const previous = covered[covered.length - 1];
3534
+ if (previous && entry.startAt.getTime() !== previous.bufferEndAt.getTime()) break;
3535
+ covered.push(entry);
3536
+ if (entry.endAt.getTime() === endAt.getTime()) return covered;
3537
+ if (entry.endAt.getTime() > endAt.getTime()) break;
3538
+ }
3539
+ throw new BookingRulesError(
3540
+ `${startAt.toISOString()} \u2192 ${endAt.toISOString()} does not line up with the bookable grid`
3541
+ );
2958
3542
  }
2959
- var ADYEN_NO_TAX_NUMBER_ABSENT_COUNTRIES = /* @__PURE__ */ new Set(["CA"]);
2960
- function adyenSupportsTaxNumberAbsent(alpha2) {
2961
- return !!alpha2 && !ADYEN_NO_TAX_NUMBER_ABSENT_COUNTRIES.has(alpha2);
3543
+
3544
+ // booking/availability.ts
3545
+ var withinPresence = (slot, timeZone, presence) => {
3546
+ if (!presence?.length) return true;
3547
+ const dayStart = startOfZonedDay(slot.startAt, timeZone);
3548
+ const weekday = zonedParts(dayStart, timeZone).weekday;
3549
+ return presence.some(
3550
+ (shift) => shift.weekday === weekday && slot.startAt.getTime() >= atZonedMinute(dayStart, shift.openMinute, timeZone).getTime() && slot.endAt.getTime() <= atZonedMinute(dayStart, shift.closeMinute, timeZone).getTime()
3551
+ );
3552
+ };
3553
+ var overlaps = (entry, startAt, bufferEndAt) => entry.startAt.getTime() < bufferEndAt.getTime() && entry.bufferEndAt.getTime() > startAt.getTime();
3554
+ function occupancyIn(occupied, productId, startAt, bufferEndAt, shared) {
3555
+ const clashing = occupied.filter((entry) => overlaps(entry, startAt, bufferEndAt));
3556
+ const foreign = shared ? void 0 : clashing.find((entry) => entry.productId !== productId);
3557
+ return {
3558
+ taken: clashing.filter((entry) => shared || entry.productId === productId).reduce((sum, entry) => sum + entry.qty, 0),
3559
+ foreignProductId: foreign?.productId
3560
+ };
2962
3561
  }
2963
- var ADYEN_TAX_NUMBER_FORMATS = {
2964
- CA: { pattern: /^\d{9}$/, length: 9 }
3562
+ var stateFor = (hasShift, offerable, free) => {
3563
+ if (offerable === 0) {
3564
+ return hasShift() ? "fullyBooked" /* FULLY_BOOKED */ : "noHoursInRange" /* NO_HOURS_IN_RANGE */;
3565
+ }
3566
+ return free === 0 ? "fullyBooked" /* FULLY_BOOKED */ : "available" /* AVAILABLE */;
2965
3567
  };
2966
- function adyenTaxNumberFormat(alpha2) {
2967
- return alpha2 ? ADYEN_TAX_NUMBER_FORMATS[alpha2] : void 0;
3568
+ function shopDayKey(instant, timeZone) {
3569
+ const { year, month, day } = zonedParts(instant, timeZone);
3570
+ const pad = (value) => String(value).padStart(2, "0");
3571
+ return `${year}-${pad(month)}-${pad(day)}`;
3572
+ }
3573
+ function shopDayRange(fromDayKey, count, timeZone) {
3574
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(fromDayKey ?? "");
3575
+ if (!match) {
3576
+ throw new BookingRulesError(`Booking rules: "${fromDayKey}" is not a shop day (YYYY-MM-DD)`);
3577
+ }
3578
+ if (!Number.isInteger(count) || count < 1) {
3579
+ throw new BookingRulesError(`Booking rules: "${count}" is not a number of days`);
3580
+ }
3581
+ const [, year, month, day] = match.map(Number);
3582
+ const from = instantFromLocal(year, month, day, 0, timeZone);
3583
+ if (shopDayKey(from, timeZone) !== fromDayKey) {
3584
+ throw new BookingRulesError(`Booking rules: "${fromDayKey}" is not a real day`);
3585
+ }
3586
+ return { from, to: instantFromLocal(year, month, day + count, 0, timeZone) };
3587
+ }
3588
+ function buildAvailability({
3589
+ productId,
3590
+ bookingRulesId,
3591
+ bookingType,
3592
+ rule,
3593
+ resources,
3594
+ occupied,
3595
+ from,
3596
+ to,
3597
+ now
3598
+ }) {
3599
+ const shared = rule.slot.sharedAcrossProducts ?? false;
3600
+ const bookableUntil = windowEndsAt(rule, now);
3601
+ const header = {
3602
+ productId,
3603
+ bookingRulesId,
3604
+ bookingType,
3605
+ ratePeriod: rule.ratePeriod,
3606
+ // Passed through for the UI to label: a group slot under minimumToRun may not run.
3607
+ minimumToRun: rule.slot.minimumToRun,
3608
+ minStay: rule.slot.minStay,
3609
+ maxStay: rule.slot.maxStay,
3610
+ // Why a day can come back empty. Past this instant the rule refuses everything, so the UI
3611
+ // can say "beyond the booking window" where it would otherwise claim "nothing free".
3612
+ bookableUntil,
3613
+ // The comparison itself, not just the boundary: deciding whether a day lies past the
3614
+ // window is a rule, and a UI that re-derives it owns a copy of that rule — which is how
3615
+ // the same empty grid ends up explained one way on the till and another in a flow.
3616
+ beyondWindow: !!bookableUntil && from.getTime() > bookableUntil.getTime()
3617
+ };
3618
+ const byResource = /* @__PURE__ */ new Map();
3619
+ for (const entry of occupied) {
3620
+ byResource.set(entry.resourceId, [...byResource.get(entry.resourceId) ?? [], entry]);
3621
+ }
3622
+ const grids = resources.map((resource) => {
3623
+ const mine = byResource.get(resource.id) ?? [];
3624
+ const busy = (resource.seats ?? 1) === 1 ? mine : [];
3625
+ const slots2 = buildGrid(rule, resource.timeZone, from, to, now, busy).filter(
3626
+ (slot) => withinPresence(slot, resource.timeZone, resource.presence)
3627
+ );
3628
+ return { resource, slots: slots2 };
3629
+ });
3630
+ const byInstant = /* @__PURE__ */ new Map();
3631
+ const freeSlotsPerResource = /* @__PURE__ */ new Map();
3632
+ for (const { resource, slots: slots2 } of grids) {
3633
+ const capacity = resource.seats ?? 1;
3634
+ const mine = byResource.get(resource.id) ?? [];
3635
+ for (const slot of slots2) {
3636
+ const { taken, foreignProductId } = occupancyIn(mine, productId, slot.startAt, slot.bufferEndAt, shared);
3637
+ const free = foreignProductId ? 0 : Math.max(capacity - taken, 0);
3638
+ const entry = byInstant.get(+slot.startAt) ?? {
3639
+ ...slot,
3640
+ capacity: 0,
3641
+ booked: 0,
3642
+ free: 0,
3643
+ resources: [],
3644
+ dayKey: shopDayKey(slot.startAt, resource.timeZone)
3645
+ };
3646
+ entry.capacity += capacity;
3647
+ entry.booked += foreignProductId ? capacity : taken;
3648
+ entry.free += free;
3649
+ entry.canStart = entry.canStart || slot.canStart;
3650
+ entry.resources.push({ resourceId: resource.id, name: resource.name, capacity, booked: taken, free });
3651
+ if (free > 0) freeSlotsPerResource.set(resource.id, (freeSlotsPerResource.get(resource.id) ?? 0) + 1);
3652
+ byInstant.set(+slot.startAt, entry);
3653
+ }
3654
+ }
3655
+ const inWindow = grids.map(({ resource, slots: slots2 }) => {
3656
+ const free = freeSlotsPerResource.get(resource.id) ?? 0;
3657
+ return {
3658
+ resourceId: resource.id,
3659
+ name: resource.name,
3660
+ slots: slots2.length,
3661
+ free,
3662
+ // Lazily: the shift grid is a second walk, and only an empty answer needs it.
3663
+ state: stateFor(
3664
+ () => buildGrid(rule, resource.timeZone, from, to, now).filter(
3665
+ (slot) => withinPresence(slot, resource.timeZone, resource.presence)
3666
+ ).length > 0,
3667
+ slots2.length,
3668
+ free
3669
+ )
3670
+ };
3671
+ });
3672
+ const slots = [...byInstant.values()].sort((first, second) => +first.startAt - +second.startAt);
3673
+ return {
3674
+ ...header,
3675
+ resources: inWindow,
3676
+ slots,
3677
+ /**
3678
+ * The clock every instant above reads on. Returned rather than assumed: without it a caller
3679
+ * has to fetch the zone from somewhere else and hope it is the same one the grid used.
3680
+ */
3681
+ timeZone: resources[0]?.timeZone ?? null,
3682
+ /** The shop days this answer covers, in order — what a day picker lists. */
3683
+ days: [...new Set(slots.map((slot) => slot.dayKey))]
3684
+ };
3685
+ }
3686
+ function presentAt(resources, rows, outletId) {
3687
+ const byResource = /* @__PURE__ */ new Map();
3688
+ for (const row of rows) {
3689
+ byResource.set(row.resourceId, [...byResource.get(row.resourceId) ?? [], row]);
3690
+ }
3691
+ return resources.flatMap((resource) => {
3692
+ const own = byResource.get(resource.id) ?? [];
3693
+ if (own.length === 0 || !outletId) return [{ resource }];
3694
+ const here = own.find((row) => row.outletId === outletId);
3695
+ return here ? [{ resource, presence: here.weeklyHours }] : [];
3696
+ });
3697
+ }
3698
+
3699
+ // booking/plan.ts
3700
+ var DEFAULT_HOLD_MINUTES = 10;
3701
+ function planWindow({ rule, timeZone, presence, startAt, endAt, now, busy = [] }) {
3702
+ const grid = buildGrid(rule, timeZone, startAt, endAt, now, busy).filter(
3703
+ (slot) => withinPresence(slot, timeZone, presence)
3704
+ );
3705
+ const slots = slotsForWindow(grid, startAt, endAt);
3706
+ const { minStay, maxStay } = rule.slot;
3707
+ if (minStay && slots.length < minStay) {
3708
+ throw new BookingRulesError(`These rules ask for at least ${minStay} period(s), got ${slots.length}`);
3709
+ }
3710
+ if (maxStay && slots.length > maxStay) {
3711
+ throw new BookingRulesError(`These rules allow at most ${maxStay} period(s), got ${slots.length}`);
3712
+ }
3713
+ const last = slots[slots.length - 1];
3714
+ return { slots, endAt: last.endAt, bufferEndAt: last.bufferEndAt };
2968
3715
  }
2969
- var ADYEN_TAX_NUMBER_MIN_LENGTH = 5;
2970
- var ADYEN_JOB_TITLE_ASSOCIATION_TYPES = ["signatory", "uboThroughControl"];
2971
- function adyenRequiresJobTitle(associationType) {
2972
- return ADYEN_JOB_TITLE_ASSOCIATION_TYPES.includes(associationType);
3716
+ var holdExpiresAt = (rule, now) => new Date(now.getTime() + (rule.window.holdMinutes ?? DEFAULT_HOLD_MINUTES) * 6e4);
3717
+
3718
+ // constants/adyen.constants.ts
3719
+ var ADYEN_ROLLOUT_COUNTRY_CODES = /* @__PURE__ */ new Set(["CA"]);
3720
+ function isAdyenRolloutCountry(alpha2) {
3721
+ return ADYEN_ROLLOUT_COUNTRY_CODES.has(alpha2);
2973
3722
  }
2974
3723
  // Annotate the CommonJS export names for ESM import in node:
2975
3724
  0 && (module.exports = {
2976
- ADYEN_JOB_TITLE_ASSOCIATION_TYPES,
2977
- ADYEN_LEGAL_ENTITY_STATE_PROVINCE_COUNTRIES,
2978
- ADYEN_NO_TAX_NUMBER_ABSENT_COUNTRIES,
2979
3725
  ADYEN_ROLLOUT_COUNTRY_CODES,
2980
- ADYEN_STORE_STATE_PROVINCE_COUNTRIES,
2981
- ADYEN_TAX_NUMBER_FORMATS,
2982
- ADYEN_TAX_NUMBER_MIN_LENGTH,
2983
3726
  AttributeType,
3727
+ BookingRatePeriod,
3728
+ BookingResourceKind,
3729
+ BookingResourceStatus,
3730
+ BookingRulesError,
3731
+ BookingType,
2984
3732
  CONDITION_OPERATORS,
2985
3733
  COUNTRY_CURRENCIES,
2986
3734
  CURRENCY_CONFIG,
@@ -2991,6 +3739,7 @@ function adyenRequiresJobTitle(associationType) {
2991
3739
  DEFAULT_CONFIG,
2992
3740
  DEFAULT_CROSS_AXIS_RULES,
2993
3741
  DEFAULT_DISPLAY_STATE_MAP,
3742
+ DEFAULT_HOLD_MINUTES,
2994
3743
  DEFAULT_RESOLVED_CONFIG,
2995
3744
  DEFAULT_UNIT_ID,
2996
3745
  DualWriteStrategy,
@@ -3034,8 +3783,10 @@ function adyenRequiresJobTitle(associationType) {
3034
3783
  REPORT_NEGATE_PATHS,
3035
3784
  ReceiptSendChannel,
3036
3785
  ReceiptType,
3786
+ ReservationStatus,
3037
3787
  ResidualPaymentMethod,
3038
3788
  ResidualStatus,
3789
+ ResourceAvailabilityState,
3039
3790
  STATE_SCHEMA,
3040
3791
  STRIPE_SUPPORTED_COUNTRIES,
3041
3792
  SYSTEM_FAMILIES,
@@ -3049,23 +3800,29 @@ function adyenRequiresJobTitle(associationType) {
3049
3800
  TopMetericsSort,
3050
3801
  UserTypes,
3051
3802
  addAmounts,
3052
- adyenLegalEntityRequiresStateOrProvince,
3053
- adyenRequiresJobTitle,
3054
- adyenStoreRequiresStateOrProvince,
3055
- adyenSupportsTaxNumberAbsent,
3056
- adyenTaxNumberFormat,
3057
3803
  amountToString,
3804
+ ancestorChain,
3058
3805
  assertUnitSane,
3059
3806
  availableIn,
3060
3807
  baseFor,
3061
3808
  baseFulfillmentDefinitions,
3062
3809
  baseOf,
3063
3810
  basePaymentDefinitions,
3811
+ buildAvailability,
3812
+ buildGrid,
3064
3813
  buildOrderContext,
3065
3814
  buildPosPresetConfig,
3066
3815
  buildResolvedConfig,
3067
3816
  calculatePercentage,
3068
3817
  canTransition,
3818
+ cheapestCompositeItem,
3819
+ cheapestCompositePrice,
3820
+ compositeItemCost,
3821
+ compositeItemUnavailable,
3822
+ compositeMakeable,
3823
+ compositeModifierMismatches,
3824
+ compositeReservationMismatches,
3825
+ compositeVariantInStock,
3069
3826
  conditionSummary,
3070
3827
  convertFixedFeeToLocalCurrency,
3071
3828
  createAmount,
@@ -3100,8 +3857,11 @@ function adyenRequiresJobTitle(associationType) {
3100
3857
  getPricingRegionFromCountry,
3101
3858
  getSettlementTotal,
3102
3859
  getTotalCollected,
3860
+ hasSellableChoice,
3861
+ holdExpiresAt,
3103
3862
  inferStatesFromLegacyStatus,
3104
3863
  isAdyenRolloutCountry,
3864
+ isChoiceAvailableAtOutlet,
3105
3865
  isConditionOperator,
3106
3866
  isFulfillmentState,
3107
3867
  isPaymentState,
@@ -3112,25 +3872,39 @@ function adyenRequiresJobTitle(associationType) {
3112
3872
  mapToLegacyStatus,
3113
3873
  maxPrecisionFor,
3114
3874
  mergeFragment,
3875
+ modifiersSellable,
3115
3876
  multiplyAmount,
3116
3877
  normalizeMongoIdString,
3878
+ occupancyIn,
3117
3879
  parseAmountToMinorUnits,
3118
3880
  parseCurrencyCode,
3119
3881
  parseStripeCurrencyCode,
3882
+ planWindow,
3883
+ poolDemand,
3884
+ poolOf,
3885
+ poolShortfall,
3886
+ poolStockAt,
3887
+ presentAt,
3120
3888
  removeFragment,
3121
3889
  resolveFamily,
3890
+ resolveModifiersForProduct,
3122
3891
  resolveTemplate,
3123
3892
  resolveUnit,
3124
3893
  roundToPrecision,
3125
3894
  sameFamily,
3126
3895
  setOrderState,
3896
+ shopDayKey,
3897
+ shopDayRange,
3127
3898
  signReportForDisplay,
3899
+ slotsForWindow,
3128
3900
  stringifyMinorUnits,
3129
3901
  subtractAmounts,
3130
3902
  toBase,
3131
3903
  toMinorUnits,
3132
3904
  validateConfig,
3133
3905
  validateFragmentCompatibility,
3134
- violatesFinancialInvariant
3906
+ violatesFinancialInvariant,
3907
+ windowEndsAt,
3908
+ withinPresence
3135
3909
  });
3136
3910
  //# sourceMappingURL=index.js.map