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