@final-commerce/common 2.2.3-staging3.1 → 2.3.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
@@ -20,6 +20,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  var index_exports = {};
21
21
  __export(index_exports, {
22
22
  AttributeType: () => AttributeType,
23
+ BookingRatePeriod: () => BookingRatePeriod,
24
+ BookingResourceKind: () => BookingResourceKind,
25
+ BookingResourceStatus: () => BookingResourceStatus,
26
+ BookingRulesError: () => BookingRulesError,
27
+ BookingType: () => BookingType,
23
28
  CONDITION_OPERATORS: () => CONDITION_OPERATORS,
24
29
  COUNTRY_CURRENCIES: () => COUNTRY_CURRENCIES,
25
30
  CURRENCY_CONFIG: () => CURRENCY_CONFIG,
@@ -30,6 +35,7 @@ __export(index_exports, {
30
35
  DEFAULT_CONFIG: () => DEFAULT_CONFIG,
31
36
  DEFAULT_CROSS_AXIS_RULES: () => DEFAULT_CROSS_AXIS_RULES,
32
37
  DEFAULT_DISPLAY_STATE_MAP: () => DEFAULT_DISPLAY_STATE_MAP,
38
+ DEFAULT_HOLD_MINUTES: () => DEFAULT_HOLD_MINUTES,
33
39
  DEFAULT_RESOLVED_CONFIG: () => DEFAULT_RESOLVED_CONFIG,
34
40
  DEFAULT_UNIT_ID: () => DEFAULT_UNIT_ID,
35
41
  DualWriteStrategy: () => DualWriteStrategy,
@@ -73,8 +79,10 @@ __export(index_exports, {
73
79
  REPORT_NEGATE_PATHS: () => REPORT_NEGATE_PATHS,
74
80
  ReceiptSendChannel: () => ReceiptSendChannel,
75
81
  ReceiptType: () => ReceiptType,
82
+ ReservationStatus: () => ReservationStatus,
76
83
  ResidualPaymentMethod: () => ResidualPaymentMethod,
77
84
  ResidualStatus: () => ResidualStatus,
85
+ ResourceAvailabilityState: () => ResourceAvailabilityState,
78
86
  STATE_SCHEMA: () => STATE_SCHEMA,
79
87
  STRIPE_SUPPORTED_COUNTRIES: () => STRIPE_SUPPORTED_COUNTRIES,
80
88
  SYSTEM_FAMILIES: () => SYSTEM_FAMILIES,
@@ -95,6 +103,8 @@ __export(index_exports, {
95
103
  baseFulfillmentDefinitions: () => baseFulfillmentDefinitions,
96
104
  baseOf: () => baseOf,
97
105
  basePaymentDefinitions: () => basePaymentDefinitions,
106
+ buildAvailability: () => buildAvailability,
107
+ buildGrid: () => buildGrid,
98
108
  buildOrderContext: () => buildOrderContext,
99
109
  buildPosPresetConfig: () => buildPosPresetConfig,
100
110
  buildResolvedConfig: () => buildResolvedConfig,
@@ -134,6 +144,7 @@ __export(index_exports, {
134
144
  getPricingRegionFromCountry: () => getPricingRegionFromCountry,
135
145
  getSettlementTotal: () => getSettlementTotal,
136
146
  getTotalCollected: () => getTotalCollected,
147
+ holdExpiresAt: () => holdExpiresAt,
137
148
  inferStatesFromLegacyStatus: () => inferStatesFromLegacyStatus,
138
149
  isConditionOperator: () => isConditionOperator,
139
150
  isFulfillmentState: () => isFulfillmentState,
@@ -147,9 +158,12 @@ __export(index_exports, {
147
158
  mergeFragment: () => mergeFragment,
148
159
  multiplyAmount: () => multiplyAmount,
149
160
  normalizeMongoIdString: () => normalizeMongoIdString,
161
+ occupancyIn: () => occupancyIn,
150
162
  parseAmountToMinorUnits: () => parseAmountToMinorUnits,
151
163
  parseCurrencyCode: () => parseCurrencyCode,
152
164
  parseStripeCurrencyCode: () => parseStripeCurrencyCode,
165
+ planWindow: () => planWindow,
166
+ presentAt: () => presentAt,
153
167
  removeFragment: () => removeFragment,
154
168
  resolveFamily: () => resolveFamily,
155
169
  resolveTemplate: () => resolveTemplate,
@@ -158,13 +172,16 @@ __export(index_exports, {
158
172
  sameFamily: () => sameFamily,
159
173
  setOrderState: () => setOrderState,
160
174
  signReportForDisplay: () => signReportForDisplay,
175
+ slotsForWindow: () => slotsForWindow,
161
176
  stringifyMinorUnits: () => stringifyMinorUnits,
162
177
  subtractAmounts: () => subtractAmounts,
163
178
  toBase: () => toBase,
164
179
  toMinorUnits: () => toMinorUnits,
165
180
  validateConfig: () => validateConfig,
166
181
  validateFragmentCompatibility: () => validateFragmentCompatibility,
167
- violatesFinancialInvariant: () => violatesFinancialInvariant
182
+ violatesFinancialInvariant: () => violatesFinancialInvariant,
183
+ windowEndsAt: () => windowEndsAt,
184
+ withinPresence: () => withinPresence
168
185
  });
169
186
  module.exports = __toCommonJS(index_exports);
170
187
 
@@ -590,6 +607,7 @@ var StripeCurrencyCode = /* @__PURE__ */ ((StripeCurrencyCode2) => {
590
607
  var ProductType = /* @__PURE__ */ ((ProductType2) => {
591
608
  ProductType2["SIMPLE"] = "simple";
592
609
  ProductType2["VARIABLE"] = "variable";
610
+ ProductType2["BOOKING"] = "booking";
593
611
  return ProductType2;
594
612
  })(ProductType || {});
595
613
  var ProductStatus = /* @__PURE__ */ ((ProductStatus2) => {
@@ -607,6 +625,50 @@ var PaymentProcessor = /* @__PURE__ */ ((PaymentProcessor2) => {
607
625
  return PaymentProcessor2;
608
626
  })(PaymentProcessor || {});
609
627
 
628
+ // enums/booking.enum.ts
629
+ var BookingType = /* @__PURE__ */ ((BookingType3) => {
630
+ BookingType3["RENTAL"] = "rental";
631
+ BookingType3["APPOINTMENT"] = "appointment";
632
+ BookingType3["OVERNIGHT"] = "overnight";
633
+ BookingType3["GROUP"] = "group";
634
+ return BookingType3;
635
+ })(BookingType || {});
636
+ var BookingRatePeriod = /* @__PURE__ */ ((BookingRatePeriod2) => {
637
+ BookingRatePeriod2["SLOT"] = "slot";
638
+ BookingRatePeriod2["DAY"] = "day";
639
+ BookingRatePeriod2["NIGHT"] = "night";
640
+ BookingRatePeriod2["WEEK"] = "week";
641
+ BookingRatePeriod2["MONTH"] = "month";
642
+ return BookingRatePeriod2;
643
+ })(BookingRatePeriod || {});
644
+ var BookingResourceKind = /* @__PURE__ */ ((BookingResourceKind2) => {
645
+ BookingResourceKind2["ASSET"] = "asset";
646
+ BookingResourceKind2["ROOM"] = "room";
647
+ BookingResourceKind2["PROPERTY"] = "property";
648
+ BookingResourceKind2["SPACE"] = "space";
649
+ BookingResourceKind2["STAFF"] = "staff";
650
+ BookingResourceKind2["TABLE"] = "table";
651
+ return BookingResourceKind2;
652
+ })(BookingResourceKind || {});
653
+ var ReservationStatus = /* @__PURE__ */ ((ReservationStatus2) => {
654
+ ReservationStatus2["HELD"] = "held";
655
+ ReservationStatus2["CONFIRMED"] = "confirmed";
656
+ ReservationStatus2["CANCELLED"] = "cancelled";
657
+ ReservationStatus2["EXPIRED"] = "expired";
658
+ return ReservationStatus2;
659
+ })(ReservationStatus || {});
660
+ var BookingResourceStatus = /* @__PURE__ */ ((BookingResourceStatus2) => {
661
+ BookingResourceStatus2["ACTIVE"] = "active";
662
+ BookingResourceStatus2["INACTIVE"] = "inactive";
663
+ return BookingResourceStatus2;
664
+ })(BookingResourceStatus || {});
665
+ var ResourceAvailabilityState = /* @__PURE__ */ ((ResourceAvailabilityState2) => {
666
+ ResourceAvailabilityState2["AVAILABLE"] = "available";
667
+ ResourceAvailabilityState2["FULLY_BOOKED"] = "fullyBooked";
668
+ ResourceAvailabilityState2["NO_HOURS_IN_RANGE"] = "noHoursInRange";
669
+ return ResourceAvailabilityState2;
670
+ })(ResourceAvailabilityState || {});
671
+
610
672
  // utils/currency.util.ts
611
673
  var CURRENCY_CONFIG = {
612
674
  // Major currencies with 2 decimal places
@@ -1816,6 +1878,15 @@ function sumRefundQuantities(refunds) {
1816
1878
  }
1817
1879
  return t;
1818
1880
  }
1881
+ var asCount = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
1882
+ function countsOf(row) {
1883
+ const quantity = asCount(row.quantity);
1884
+ return {
1885
+ quantity,
1886
+ fulfilled: Math.min(asCount(row.fulfilledQuantity), quantity),
1887
+ returned: Math.min(asCount(row.returnedQuantity), quantity)
1888
+ };
1889
+ }
1819
1890
  function buildOrderContext(order, extras) {
1820
1891
  const o = order ?? {};
1821
1892
  const total = typeof o.total === "number" && Number.isFinite(o.total) ? o.total : 0;
@@ -1823,6 +1894,7 @@ function buildOrderContext(order, extras) {
1823
1894
  const paymentMethods = o.paymentMethods ?? [];
1824
1895
  const refunds = o.refunds ?? o.refund ?? [];
1825
1896
  const lineItems = o.lineItems ?? [];
1897
+ const reservations = o.reservations ?? [];
1826
1898
  const totalPaid = sumNumbers(paymentMethods);
1827
1899
  const totalRefunded = sumRefundQuantities(refunds);
1828
1900
  const isFullyPaid = total > 0 ? balance <= 0 : totalPaid > 0 && balance <= 0;
@@ -1830,13 +1902,11 @@ function buildOrderContext(order, extras) {
1830
1902
  let itemCount = 0;
1831
1903
  let fulfilledItemCount = 0;
1832
1904
  let returnedItemCount = 0;
1833
- for (const li of lineItems) {
1834
- const q = typeof li.quantity === "number" && Number.isFinite(li.quantity) ? Math.max(0, li.quantity) : 0;
1835
- itemCount += q;
1836
- const fq = typeof li.fulfilledQuantity === "number" && Number.isFinite(li.fulfilledQuantity) ? Math.max(0, li.fulfilledQuantity) : 0;
1837
- fulfilledItemCount += Math.min(fq, q);
1838
- const rq = typeof li.returnedQuantity === "number" && Number.isFinite(li.returnedQuantity) ? Math.max(0, li.returnedQuantity) : 0;
1839
- returnedItemCount += Math.min(rq, q);
1905
+ for (const row of [...lineItems, ...reservations]) {
1906
+ const { quantity, fulfilled, returned } = countsOf(row);
1907
+ itemCount += quantity;
1908
+ fulfilledItemCount += fulfilled;
1909
+ returnedItemCount += returned;
1840
1910
  }
1841
1911
  const isFullyReturned = itemCount > 0 ? returnedItemCount >= itemCount : returnedItemCount > 0;
1842
1912
  const hasPartialReturns = returnedItemCount > 0 && !isFullyReturned;
@@ -1846,7 +1916,8 @@ function buildOrderContext(order, extras) {
1846
1916
  balance,
1847
1917
  paymentMethods,
1848
1918
  refunds,
1849
- lineItems
1919
+ lineItems,
1920
+ reservations
1850
1921
  },
1851
1922
  computed: {
1852
1923
  balance,
@@ -2925,9 +2996,366 @@ var ORDER_STATE_EVENTS = {
2925
2996
  stateTransitionBlocked: "state-transition-blocked",
2926
2997
  configUpdated: "config-updated"
2927
2998
  };
2999
+
3000
+ // booking/types.ts
3001
+ var BookingRulesError = class extends Error {
3002
+ constructor(message) {
3003
+ super(message);
3004
+ this.name = "BookingRulesError";
3005
+ }
3006
+ };
3007
+
3008
+ // booking/zoned-time.ts
3009
+ var MINUTE = 6e4;
3010
+ var formatters = /* @__PURE__ */ new Map();
3011
+ var formatterFor = (timeZone) => {
3012
+ const existing = formatters.get(timeZone);
3013
+ if (existing) return existing;
3014
+ let created;
3015
+ try {
3016
+ created = new Intl.DateTimeFormat("en-US", {
3017
+ timeZone,
3018
+ hour12: false,
3019
+ year: "numeric",
3020
+ month: "2-digit",
3021
+ day: "2-digit",
3022
+ hour: "2-digit",
3023
+ minute: "2-digit",
3024
+ second: "2-digit",
3025
+ weekday: "short"
3026
+ });
3027
+ } catch {
3028
+ throw new BookingRulesError(`Booking rules: "${timeZone}" is not a known time zone`);
3029
+ }
3030
+ formatters.set(timeZone, created);
3031
+ return created;
3032
+ };
3033
+ var WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
3034
+ function zonedParts(instant, timeZone) {
3035
+ const parts = {};
3036
+ for (const { type, value } of formatterFor(timeZone).formatToParts(instant)) parts[type] = value;
3037
+ const hour = Number(parts.hour) % 24;
3038
+ return {
3039
+ year: Number(parts.year),
3040
+ month: Number(parts.month),
3041
+ day: Number(parts.day),
3042
+ hour,
3043
+ minute: Number(parts.minute),
3044
+ second: Number(parts.second),
3045
+ weekday: WEEKDAYS.indexOf(parts.weekday)
3046
+ };
3047
+ }
3048
+ function offsetAt(instant, timeZone) {
3049
+ const { year, month, day, hour, minute, second } = zonedParts(instant, timeZone);
3050
+ return Date.UTC(year, month - 1, day, hour, minute, second) - Math.floor(instant.getTime() / 1e3) * 1e3;
3051
+ }
3052
+ function instantFromLocal(year, month, day, minutes, timeZone) {
3053
+ const naive = Date.UTC(year, month - 1, day) + minutes * MINUTE;
3054
+ const firstGuess = naive - offsetAt(new Date(naive), timeZone);
3055
+ return new Date(naive - offsetAt(new Date(firstGuess), timeZone));
3056
+ }
3057
+ function startOfZonedDay(instant, timeZone) {
3058
+ const { year, month, day } = zonedParts(instant, timeZone);
3059
+ return instantFromLocal(year, month, day, 0, timeZone);
3060
+ }
3061
+ function nextZonedDay(dayStart, timeZone) {
3062
+ const { year, month, day } = zonedParts(dayStart, timeZone);
3063
+ return instantFromLocal(year, month, day + 1, 0, timeZone);
3064
+ }
3065
+ function atZonedMinute(dayStart, minutes, timeZone) {
3066
+ const { year, month, day } = zonedParts(dayStart, timeZone);
3067
+ return instantFromLocal(year, month, day, minutes, timeZone);
3068
+ }
3069
+
3070
+ // booking/grid.ts
3071
+ var MINUTE2 = 6e4;
3072
+ var MAX_RANGE_DAYS = 366;
3073
+ var asTime = (value, field) => {
3074
+ if (value == null) return void 0;
3075
+ const time = new Date(value).getTime();
3076
+ if (Number.isNaN(time)) throw new BookingRulesError(`Booking rules: ${field} is not a date, got "${String(value)}"`);
3077
+ return time;
3078
+ };
3079
+ function parseHhMm(value, field) {
3080
+ const [hours, minutes] = value.split(":").map(Number);
3081
+ if (!Number.isInteger(hours) || !Number.isInteger(minutes)) {
3082
+ throw new BookingRulesError(`Booking rules: ${field} must be "HH:mm", got "${value}"`);
3083
+ }
3084
+ return hours * 60 + minutes;
3085
+ }
3086
+ function windowEndsAt(rule, now) {
3087
+ const { maxAdvanceDays, bookableTo } = rule.window;
3088
+ const ends = Math.min(
3089
+ maxAdvanceDays ? now.getTime() + maxAdvanceDays * 1440 * MINUTE2 : Number.POSITIVE_INFINITY,
3090
+ asTime(bookableTo, "window.bookableTo") ?? Number.POSITIVE_INFINITY
3091
+ );
3092
+ return Number.isFinite(ends) ? new Date(ends) : null;
3093
+ }
3094
+ function periodSlot(dayStart, timeZone, rule, openMinute, closeMinute) {
3095
+ const { ratePeriod, slot, checkInTime, checkOutTime } = rule;
3096
+ const at = (minutes) => atZonedMinute(dayStart, minutes, timeZone);
3097
+ if (ratePeriod === "night" /* NIGHT */) {
3098
+ if (!checkInTime || !checkOutTime) {
3099
+ throw new BookingRulesError("Overnight booking rules need both checkInTime and checkOutTime");
3100
+ }
3101
+ const checkIn = parseHhMm(checkInTime, "checkInTime");
3102
+ const checkOut = parseHhMm(checkOutTime, "checkOutTime");
3103
+ return {
3104
+ startAt: at(checkIn),
3105
+ endAt: at(1440 + checkOut),
3106
+ // Housekeeping sits between check-out and the next check-in: it costs no night.
3107
+ bufferEndAt: at(1440 + checkIn),
3108
+ canStart: true
3109
+ };
3110
+ }
3111
+ const from = ratePeriod === "day" /* DAY */ ? openMinute ?? 0 : 0;
3112
+ const to = ratePeriod === "day" /* DAY */ ? closeMinute ?? 1440 : 1440;
3113
+ return {
3114
+ startAt: at(from),
3115
+ endAt: at(to),
3116
+ bufferEndAt: at(to + (slot.bufferMinutes ?? 0)),
3117
+ canStart: true
3118
+ };
3119
+ }
3120
+ function freeGaps(openAt, closeAt, busy) {
3121
+ const inside = busy.map(({ startAt, bufferEndAt }) => ({ from: +startAt, to: +bufferEndAt })).filter(({ from, to }) => to > openAt && from < closeAt).sort((first, second) => first.from - second.from);
3122
+ const gaps = [];
3123
+ let cursor = openAt;
3124
+ for (const { from, to } of inside) {
3125
+ if (from > cursor) gaps.push({ from: cursor, to: from, endsAtBooking: true });
3126
+ cursor = Math.max(cursor, to);
3127
+ if (cursor >= closeAt) break;
3128
+ }
3129
+ if (cursor < closeAt) gaps.push({ from: cursor, to: closeAt, endsAtBooking: false });
3130
+ return gaps;
3131
+ }
3132
+ function buildGrid(rule, timeZone, from, to, now, busy = []) {
3133
+ if (to.getTime() - from.getTime() > MAX_RANGE_DAYS * 1440 * MINUTE2) {
3134
+ throw new BookingRulesError(`A booking range may span at most ${MAX_RANGE_DAYS} days`);
3135
+ }
3136
+ const { availability, slot, window, ratePeriod } = rule;
3137
+ const periodMode = ratePeriod !== "slot" /* SLOT */;
3138
+ const weeklyHours = availability.weeklyHours ?? [];
3139
+ const notBefore = Math.max(
3140
+ now.getTime() + (window.minNoticeMinutes ?? 0) * MINUTE2,
3141
+ asTime(window.bookableFrom, "window.bookableFrom") ?? 0,
3142
+ asTime(availability.availableFrom, "availability.availableFrom") ?? 0
3143
+ );
3144
+ const notAfter = windowEndsAt(rule, now)?.getTime() ?? Number.POSITIVE_INFINITY;
3145
+ const blocked = (availability.offMarket ?? []).map(({ from: blockFrom, to: blockTo }) => ({
3146
+ from: asTime(blockFrom, "availability.offMarket.from"),
3147
+ to: asTime(blockTo, "availability.offMarket.to")
3148
+ }));
3149
+ const arrivalDays = availability.arrivalDays;
3150
+ const step = slot.lengthMinutes + (slot.bufferMinutes ?? 0);
3151
+ const slots = [];
3152
+ const lastDay = startOfZonedDay(to, timeZone).getTime();
3153
+ let dayStart = startOfZonedDay(from, timeZone);
3154
+ while (dayStart.getTime() <= lastDay) {
3155
+ const weekday = zonedParts(dayStart, timeZone).weekday;
3156
+ const rows = weeklyHours.filter((entry) => entry.weekday === weekday);
3157
+ if (periodMode) {
3158
+ const openToday = weeklyHours.length === 0 || rows.length > 0;
3159
+ if (openToday) {
3160
+ const built = periodSlot(dayStart, timeZone, rule, rows[0]?.openMinute, rows[0]?.closeMinute);
3161
+ slots.push({ ...built, canStart: !arrivalDays?.length || arrivalDays.includes(weekday) });
3162
+ }
3163
+ } else {
3164
+ for (const row of rows) {
3165
+ const openAt = +atZonedMinute(dayStart, row.openMinute, timeZone);
3166
+ const closeAt = +atZonedMinute(dayStart, row.closeMinute, timeZone);
3167
+ for (const gap of freeGaps(openAt, closeAt, busy)) {
3168
+ const mustFit = (gap.endsAtBooking ? step : slot.lengthMinutes) * MINUTE2;
3169
+ for (let start = gap.from; start + mustFit <= gap.to; start += step * MINUTE2) {
3170
+ slots.push({
3171
+ startAt: new Date(start),
3172
+ endAt: new Date(start + slot.lengthMinutes * MINUTE2),
3173
+ bufferEndAt: new Date(start + step * MINUTE2),
3174
+ canStart: true
3175
+ });
3176
+ }
3177
+ }
3178
+ }
3179
+ }
3180
+ dayStart = nextZonedDay(dayStart, timeZone);
3181
+ }
3182
+ const askedFrom = from.getTime();
3183
+ const askedTo = to.getTime();
3184
+ return slots.filter(({ startAt, bufferEndAt }) => {
3185
+ const start = startAt.getTime();
3186
+ if (start < askedFrom || start >= askedTo) return false;
3187
+ if (start < notBefore || start > notAfter) return false;
3188
+ return !blocked.some(({ from: blockFrom, to: blockTo }) => start < blockTo && bufferEndAt.getTime() > blockFrom);
3189
+ }).sort((first, second) => +first.startAt - +second.startAt);
3190
+ }
3191
+ function slotsForWindow(grid, startAt, endAt) {
3192
+ const startIndex = grid.findIndex((entry) => entry.startAt.getTime() === startAt.getTime());
3193
+ if (startIndex < 0) {
3194
+ throw new BookingRulesError(`${startAt.toISOString()} is not a bookable slot start for these rules`);
3195
+ }
3196
+ if (!grid[startIndex].canStart) {
3197
+ throw new BookingRulesError(`${startAt.toISOString()} is not an arrival day for these rules`);
3198
+ }
3199
+ const covered = [];
3200
+ for (let index = startIndex; index < grid.length; index++) {
3201
+ const entry = grid[index];
3202
+ const previous = covered[covered.length - 1];
3203
+ if (previous && entry.startAt.getTime() !== previous.bufferEndAt.getTime()) break;
3204
+ covered.push(entry);
3205
+ if (entry.endAt.getTime() === endAt.getTime()) return covered;
3206
+ if (entry.endAt.getTime() > endAt.getTime()) break;
3207
+ }
3208
+ throw new BookingRulesError(
3209
+ `${startAt.toISOString()} \u2192 ${endAt.toISOString()} does not line up with the bookable grid`
3210
+ );
3211
+ }
3212
+
3213
+ // booking/availability.ts
3214
+ var withinPresence = (slot, timeZone, presence) => {
3215
+ if (!presence?.length) return true;
3216
+ const dayStart = startOfZonedDay(slot.startAt, timeZone);
3217
+ const weekday = zonedParts(dayStart, timeZone).weekday;
3218
+ return presence.some(
3219
+ (shift) => shift.weekday === weekday && slot.startAt.getTime() >= atZonedMinute(dayStart, shift.openMinute, timeZone).getTime() && slot.endAt.getTime() <= atZonedMinute(dayStart, shift.closeMinute, timeZone).getTime()
3220
+ );
3221
+ };
3222
+ var overlaps = (entry, startAt, bufferEndAt) => entry.startAt.getTime() < bufferEndAt.getTime() && entry.bufferEndAt.getTime() > startAt.getTime();
3223
+ function occupancyIn(occupied, productId, startAt, bufferEndAt, shared) {
3224
+ const clashing = occupied.filter((entry) => overlaps(entry, startAt, bufferEndAt));
3225
+ const foreign = shared ? void 0 : clashing.find((entry) => entry.productId !== productId);
3226
+ return {
3227
+ taken: clashing.filter((entry) => shared || entry.productId === productId).reduce((sum, entry) => sum + entry.qty, 0),
3228
+ foreignProductId: foreign?.productId
3229
+ };
3230
+ }
3231
+ var stateFor = (hasShift, offerable, free) => {
3232
+ if (offerable === 0) {
3233
+ return hasShift() ? "fullyBooked" /* FULLY_BOOKED */ : "noHoursInRange" /* NO_HOURS_IN_RANGE */;
3234
+ }
3235
+ return free === 0 ? "fullyBooked" /* FULLY_BOOKED */ : "available" /* AVAILABLE */;
3236
+ };
3237
+ function buildAvailability({
3238
+ productId,
3239
+ bookingRulesId,
3240
+ bookingType,
3241
+ rule,
3242
+ resources,
3243
+ occupied,
3244
+ from,
3245
+ to,
3246
+ now
3247
+ }) {
3248
+ const shared = rule.slot.sharedAcrossProducts ?? false;
3249
+ const bookableUntil = windowEndsAt(rule, now);
3250
+ const header = {
3251
+ productId,
3252
+ bookingRulesId,
3253
+ bookingType,
3254
+ ratePeriod: rule.ratePeriod,
3255
+ // Passed through for the UI to label: a group slot under minimumToRun may not run.
3256
+ minimumToRun: rule.slot.minimumToRun,
3257
+ minStay: rule.slot.minStay,
3258
+ maxStay: rule.slot.maxStay,
3259
+ // Why a day can come back empty. Past this instant the rule refuses everything, so the UI
3260
+ // can say "beyond the booking window" where it would otherwise claim "nothing free".
3261
+ bookableUntil,
3262
+ // The comparison itself, not just the boundary: deciding whether a day lies past the
3263
+ // window is a rule, and a UI that re-derives it owns a copy of that rule — which is how
3264
+ // the same empty grid ends up explained one way on the till and another in a flow.
3265
+ beyondWindow: !!bookableUntil && from.getTime() > bookableUntil.getTime()
3266
+ };
3267
+ const byResource = /* @__PURE__ */ new Map();
3268
+ for (const entry of occupied) {
3269
+ byResource.set(entry.resourceId, [...byResource.get(entry.resourceId) ?? [], entry]);
3270
+ }
3271
+ const grids = resources.map((resource) => {
3272
+ const mine = byResource.get(resource.id) ?? [];
3273
+ const busy = (resource.seats ?? 1) === 1 ? mine : [];
3274
+ const slots = buildGrid(rule, resource.timeZone, from, to, now, busy).filter(
3275
+ (slot) => withinPresence(slot, resource.timeZone, resource.presence)
3276
+ );
3277
+ return { resource, slots };
3278
+ });
3279
+ const byInstant = /* @__PURE__ */ new Map();
3280
+ const freeSlotsPerResource = /* @__PURE__ */ new Map();
3281
+ for (const { resource, slots } of grids) {
3282
+ const capacity = resource.seats ?? 1;
3283
+ const mine = byResource.get(resource.id) ?? [];
3284
+ for (const slot of slots) {
3285
+ const { taken, foreignProductId } = occupancyIn(mine, productId, slot.startAt, slot.bufferEndAt, shared);
3286
+ const free = foreignProductId ? 0 : Math.max(capacity - taken, 0);
3287
+ const entry = byInstant.get(+slot.startAt) ?? { ...slot, capacity: 0, booked: 0, free: 0, resources: [] };
3288
+ entry.capacity += capacity;
3289
+ entry.booked += foreignProductId ? capacity : taken;
3290
+ entry.free += free;
3291
+ entry.canStart = entry.canStart || slot.canStart;
3292
+ entry.resources.push({ resourceId: resource.id, name: resource.name, capacity, booked: taken, free });
3293
+ if (free > 0) freeSlotsPerResource.set(resource.id, (freeSlotsPerResource.get(resource.id) ?? 0) + 1);
3294
+ byInstant.set(+slot.startAt, entry);
3295
+ }
3296
+ }
3297
+ const inWindow = grids.map(({ resource, slots }) => {
3298
+ const free = freeSlotsPerResource.get(resource.id) ?? 0;
3299
+ return {
3300
+ resourceId: resource.id,
3301
+ name: resource.name,
3302
+ slots: slots.length,
3303
+ free,
3304
+ // Lazily: the shift grid is a second walk, and only an empty answer needs it.
3305
+ state: stateFor(
3306
+ () => buildGrid(rule, resource.timeZone, from, to, now).filter(
3307
+ (slot) => withinPresence(slot, resource.timeZone, resource.presence)
3308
+ ).length > 0,
3309
+ slots.length,
3310
+ free
3311
+ )
3312
+ };
3313
+ });
3314
+ return {
3315
+ ...header,
3316
+ resources: inWindow,
3317
+ slots: [...byInstant.values()].sort((first, second) => +first.startAt - +second.startAt)
3318
+ };
3319
+ }
3320
+ function presentAt(resources, rows, outletId) {
3321
+ const byResource = /* @__PURE__ */ new Map();
3322
+ for (const row of rows) {
3323
+ byResource.set(row.resourceId, [...byResource.get(row.resourceId) ?? [], row]);
3324
+ }
3325
+ return resources.flatMap((resource) => {
3326
+ const own = byResource.get(resource.id) ?? [];
3327
+ if (own.length === 0 || !outletId) return [{ resource }];
3328
+ const here = own.find((row) => row.outletId === outletId);
3329
+ return here ? [{ resource, presence: here.weeklyHours }] : [];
3330
+ });
3331
+ }
3332
+
3333
+ // booking/plan.ts
3334
+ var DEFAULT_HOLD_MINUTES = 10;
3335
+ function planWindow({ rule, timeZone, presence, startAt, endAt, now, busy = [] }) {
3336
+ const grid = buildGrid(rule, timeZone, startAt, endAt, now, busy).filter(
3337
+ (slot) => withinPresence(slot, timeZone, presence)
3338
+ );
3339
+ const slots = slotsForWindow(grid, startAt, endAt);
3340
+ const { minStay, maxStay } = rule.slot;
3341
+ if (minStay && slots.length < minStay) {
3342
+ throw new BookingRulesError(`These rules ask for at least ${minStay} period(s), got ${slots.length}`);
3343
+ }
3344
+ if (maxStay && slots.length > maxStay) {
3345
+ throw new BookingRulesError(`These rules allow at most ${maxStay} period(s), got ${slots.length}`);
3346
+ }
3347
+ const last = slots[slots.length - 1];
3348
+ return { slots, endAt: last.endAt, bufferEndAt: last.bufferEndAt };
3349
+ }
3350
+ var holdExpiresAt = (rule, now) => new Date(now.getTime() + (rule.window.holdMinutes ?? DEFAULT_HOLD_MINUTES) * 6e4);
2928
3351
  // Annotate the CommonJS export names for ESM import in node:
2929
3352
  0 && (module.exports = {
2930
3353
  AttributeType,
3354
+ BookingRatePeriod,
3355
+ BookingResourceKind,
3356
+ BookingResourceStatus,
3357
+ BookingRulesError,
3358
+ BookingType,
2931
3359
  CONDITION_OPERATORS,
2932
3360
  COUNTRY_CURRENCIES,
2933
3361
  CURRENCY_CONFIG,
@@ -2938,6 +3366,7 @@ var ORDER_STATE_EVENTS = {
2938
3366
  DEFAULT_CONFIG,
2939
3367
  DEFAULT_CROSS_AXIS_RULES,
2940
3368
  DEFAULT_DISPLAY_STATE_MAP,
3369
+ DEFAULT_HOLD_MINUTES,
2941
3370
  DEFAULT_RESOLVED_CONFIG,
2942
3371
  DEFAULT_UNIT_ID,
2943
3372
  DualWriteStrategy,
@@ -2981,8 +3410,10 @@ var ORDER_STATE_EVENTS = {
2981
3410
  REPORT_NEGATE_PATHS,
2982
3411
  ReceiptSendChannel,
2983
3412
  ReceiptType,
3413
+ ReservationStatus,
2984
3414
  ResidualPaymentMethod,
2985
3415
  ResidualStatus,
3416
+ ResourceAvailabilityState,
2986
3417
  STATE_SCHEMA,
2987
3418
  STRIPE_SUPPORTED_COUNTRIES,
2988
3419
  SYSTEM_FAMILIES,
@@ -3003,6 +3434,8 @@ var ORDER_STATE_EVENTS = {
3003
3434
  baseFulfillmentDefinitions,
3004
3435
  baseOf,
3005
3436
  basePaymentDefinitions,
3437
+ buildAvailability,
3438
+ buildGrid,
3006
3439
  buildOrderContext,
3007
3440
  buildPosPresetConfig,
3008
3441
  buildResolvedConfig,
@@ -3042,6 +3475,7 @@ var ORDER_STATE_EVENTS = {
3042
3475
  getPricingRegionFromCountry,
3043
3476
  getSettlementTotal,
3044
3477
  getTotalCollected,
3478
+ holdExpiresAt,
3045
3479
  inferStatesFromLegacyStatus,
3046
3480
  isConditionOperator,
3047
3481
  isFulfillmentState,
@@ -3055,9 +3489,12 @@ var ORDER_STATE_EVENTS = {
3055
3489
  mergeFragment,
3056
3490
  multiplyAmount,
3057
3491
  normalizeMongoIdString,
3492
+ occupancyIn,
3058
3493
  parseAmountToMinorUnits,
3059
3494
  parseCurrencyCode,
3060
3495
  parseStripeCurrencyCode,
3496
+ planWindow,
3497
+ presentAt,
3061
3498
  removeFragment,
3062
3499
  resolveFamily,
3063
3500
  resolveTemplate,
@@ -3066,12 +3503,15 @@ var ORDER_STATE_EVENTS = {
3066
3503
  sameFamily,
3067
3504
  setOrderState,
3068
3505
  signReportForDisplay,
3506
+ slotsForWindow,
3069
3507
  stringifyMinorUnits,
3070
3508
  subtractAmounts,
3071
3509
  toBase,
3072
3510
  toMinorUnits,
3073
3511
  validateConfig,
3074
3512
  validateFragmentCompatibility,
3075
- violatesFinancialInvariant
3513
+ violatesFinancialInvariant,
3514
+ windowEndsAt,
3515
+ withinPresence
3076
3516
  });
3077
3517
  //# sourceMappingURL=index.js.map