@final-commerce/common 2.4.1-staging2.1 → 2.4.1-staging2.2

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
@@ -27,6 +27,11 @@ __export(index_exports, {
27
27
  ADYEN_TAX_NUMBER_FORMATS: () => ADYEN_TAX_NUMBER_FORMATS,
28
28
  ADYEN_TAX_NUMBER_MIN_LENGTH: () => ADYEN_TAX_NUMBER_MIN_LENGTH,
29
29
  AttributeType: () => AttributeType,
30
+ BookingRatePeriod: () => BookingRatePeriod,
31
+ BookingResourceKind: () => BookingResourceKind,
32
+ BookingResourceStatus: () => BookingResourceStatus,
33
+ BookingRulesError: () => BookingRulesError,
34
+ BookingType: () => BookingType,
30
35
  CONDITION_OPERATORS: () => CONDITION_OPERATORS,
31
36
  COUNTRY_CURRENCIES: () => COUNTRY_CURRENCIES,
32
37
  CURRENCY_CONFIG: () => CURRENCY_CONFIG,
@@ -37,6 +42,7 @@ __export(index_exports, {
37
42
  DEFAULT_CONFIG: () => DEFAULT_CONFIG,
38
43
  DEFAULT_CROSS_AXIS_RULES: () => DEFAULT_CROSS_AXIS_RULES,
39
44
  DEFAULT_DISPLAY_STATE_MAP: () => DEFAULT_DISPLAY_STATE_MAP,
45
+ DEFAULT_HOLD_MINUTES: () => DEFAULT_HOLD_MINUTES,
40
46
  DEFAULT_RESOLVED_CONFIG: () => DEFAULT_RESOLVED_CONFIG,
41
47
  DEFAULT_UNIT_ID: () => DEFAULT_UNIT_ID,
42
48
  DualWriteStrategy: () => DualWriteStrategy,
@@ -80,8 +86,10 @@ __export(index_exports, {
80
86
  REPORT_NEGATE_PATHS: () => REPORT_NEGATE_PATHS,
81
87
  ReceiptSendChannel: () => ReceiptSendChannel,
82
88
  ReceiptType: () => ReceiptType,
89
+ ReservationStatus: () => ReservationStatus,
83
90
  ResidualPaymentMethod: () => ResidualPaymentMethod,
84
91
  ResidualStatus: () => ResidualStatus,
92
+ ResourceAvailabilityState: () => ResourceAvailabilityState,
85
93
  STATE_SCHEMA: () => STATE_SCHEMA,
86
94
  STRIPE_SUPPORTED_COUNTRIES: () => STRIPE_SUPPORTED_COUNTRIES,
87
95
  SYSTEM_FAMILIES: () => SYSTEM_FAMILIES,
@@ -107,6 +115,8 @@ __export(index_exports, {
107
115
  baseFulfillmentDefinitions: () => baseFulfillmentDefinitions,
108
116
  baseOf: () => baseOf,
109
117
  basePaymentDefinitions: () => basePaymentDefinitions,
118
+ buildAvailability: () => buildAvailability,
119
+ buildGrid: () => buildGrid,
110
120
  buildOrderContext: () => buildOrderContext,
111
121
  buildPosPresetConfig: () => buildPosPresetConfig,
112
122
  buildResolvedConfig: () => buildResolvedConfig,
@@ -146,6 +156,7 @@ __export(index_exports, {
146
156
  getPricingRegionFromCountry: () => getPricingRegionFromCountry,
147
157
  getSettlementTotal: () => getSettlementTotal,
148
158
  getTotalCollected: () => getTotalCollected,
159
+ holdExpiresAt: () => holdExpiresAt,
149
160
  inferStatesFromLegacyStatus: () => inferStatesFromLegacyStatus,
150
161
  isAdyenRolloutCountry: () => isAdyenRolloutCountry,
151
162
  isConditionOperator: () => isConditionOperator,
@@ -160,9 +171,12 @@ __export(index_exports, {
160
171
  mergeFragment: () => mergeFragment,
161
172
  multiplyAmount: () => multiplyAmount,
162
173
  normalizeMongoIdString: () => normalizeMongoIdString,
174
+ occupancyIn: () => occupancyIn,
163
175
  parseAmountToMinorUnits: () => parseAmountToMinorUnits,
164
176
  parseCurrencyCode: () => parseCurrencyCode,
165
177
  parseStripeCurrencyCode: () => parseStripeCurrencyCode,
178
+ planWindow: () => planWindow,
179
+ presentAt: () => presentAt,
166
180
  removeFragment: () => removeFragment,
167
181
  resolveFamily: () => resolveFamily,
168
182
  resolveTemplate: () => resolveTemplate,
@@ -170,14 +184,19 @@ __export(index_exports, {
170
184
  roundToPrecision: () => roundToPrecision,
171
185
  sameFamily: () => sameFamily,
172
186
  setOrderState: () => setOrderState,
187
+ shopDayKey: () => shopDayKey,
188
+ shopDayRange: () => shopDayRange,
173
189
  signReportForDisplay: () => signReportForDisplay,
190
+ slotsForWindow: () => slotsForWindow,
174
191
  stringifyMinorUnits: () => stringifyMinorUnits,
175
192
  subtractAmounts: () => subtractAmounts,
176
193
  toBase: () => toBase,
177
194
  toMinorUnits: () => toMinorUnits,
178
195
  validateConfig: () => validateConfig,
179
196
  validateFragmentCompatibility: () => validateFragmentCompatibility,
180
- violatesFinancialInvariant: () => violatesFinancialInvariant
197
+ violatesFinancialInvariant: () => violatesFinancialInvariant,
198
+ windowEndsAt: () => windowEndsAt,
199
+ withinPresence: () => withinPresence
181
200
  });
182
201
  module.exports = __toCommonJS(index_exports);
183
202
 
@@ -605,6 +624,7 @@ var StripeCurrencyCode = /* @__PURE__ */ ((StripeCurrencyCode2) => {
605
624
  var ProductType = /* @__PURE__ */ ((ProductType2) => {
606
625
  ProductType2["SIMPLE"] = "simple";
607
626
  ProductType2["VARIABLE"] = "variable";
627
+ ProductType2["BOOKING"] = "booking";
608
628
  return ProductType2;
609
629
  })(ProductType || {});
610
630
  var ProductStatus = /* @__PURE__ */ ((ProductStatus2) => {
@@ -622,6 +642,50 @@ var PaymentProcessor = /* @__PURE__ */ ((PaymentProcessor2) => {
622
642
  return PaymentProcessor2;
623
643
  })(PaymentProcessor || {});
624
644
 
645
+ // enums/booking.enum.ts
646
+ var BookingType = /* @__PURE__ */ ((BookingType3) => {
647
+ BookingType3["RENTAL"] = "rental";
648
+ BookingType3["APPOINTMENT"] = "appointment";
649
+ BookingType3["OVERNIGHT"] = "overnight";
650
+ BookingType3["GROUP"] = "group";
651
+ return BookingType3;
652
+ })(BookingType || {});
653
+ var BookingRatePeriod = /* @__PURE__ */ ((BookingRatePeriod2) => {
654
+ BookingRatePeriod2["SLOT"] = "slot";
655
+ BookingRatePeriod2["DAY"] = "day";
656
+ BookingRatePeriod2["NIGHT"] = "night";
657
+ BookingRatePeriod2["WEEK"] = "week";
658
+ BookingRatePeriod2["MONTH"] = "month";
659
+ return BookingRatePeriod2;
660
+ })(BookingRatePeriod || {});
661
+ var BookingResourceKind = /* @__PURE__ */ ((BookingResourceKind2) => {
662
+ BookingResourceKind2["ASSET"] = "asset";
663
+ BookingResourceKind2["ROOM"] = "room";
664
+ BookingResourceKind2["PROPERTY"] = "property";
665
+ BookingResourceKind2["SPACE"] = "space";
666
+ BookingResourceKind2["STAFF"] = "staff";
667
+ BookingResourceKind2["TABLE"] = "table";
668
+ return BookingResourceKind2;
669
+ })(BookingResourceKind || {});
670
+ var ReservationStatus = /* @__PURE__ */ ((ReservationStatus2) => {
671
+ ReservationStatus2["HELD"] = "held";
672
+ ReservationStatus2["CONFIRMED"] = "confirmed";
673
+ ReservationStatus2["CANCELLED"] = "cancelled";
674
+ ReservationStatus2["EXPIRED"] = "expired";
675
+ return ReservationStatus2;
676
+ })(ReservationStatus || {});
677
+ var BookingResourceStatus = /* @__PURE__ */ ((BookingResourceStatus2) => {
678
+ BookingResourceStatus2["ACTIVE"] = "active";
679
+ BookingResourceStatus2["INACTIVE"] = "inactive";
680
+ return BookingResourceStatus2;
681
+ })(BookingResourceStatus || {});
682
+ var ResourceAvailabilityState = /* @__PURE__ */ ((ResourceAvailabilityState2) => {
683
+ ResourceAvailabilityState2["AVAILABLE"] = "available";
684
+ ResourceAvailabilityState2["FULLY_BOOKED"] = "fullyBooked";
685
+ ResourceAvailabilityState2["NO_HOURS_IN_RANGE"] = "noHoursInRange";
686
+ return ResourceAvailabilityState2;
687
+ })(ResourceAvailabilityState || {});
688
+
625
689
  // utils/currency.util.ts
626
690
  var CURRENCY_CONFIG = {
627
691
  // Major currencies with 2 decimal places
@@ -1833,6 +1897,15 @@ function sumRefundQuantities(refunds) {
1833
1897
  }
1834
1898
  return t;
1835
1899
  }
1900
+ var asCount = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
1901
+ function countsOf(row) {
1902
+ const quantity = asCount(row.quantity);
1903
+ return {
1904
+ quantity,
1905
+ fulfilled: Math.min(asCount(row.fulfilledQuantity), quantity),
1906
+ returned: Math.min(asCount(row.returnedQuantity), quantity)
1907
+ };
1908
+ }
1836
1909
  function buildOrderContext(order, extras) {
1837
1910
  const o = order ?? {};
1838
1911
  const total = typeof o.total === "number" && Number.isFinite(o.total) ? o.total : 0;
@@ -1840,6 +1913,7 @@ function buildOrderContext(order, extras) {
1840
1913
  const paymentMethods = o.paymentMethods ?? [];
1841
1914
  const refunds = o.refunds ?? o.refund ?? [];
1842
1915
  const lineItems = o.lineItems ?? [];
1916
+ const reservations = o.reservations ?? [];
1843
1917
  const totalPaid = sumNumbers(paymentMethods);
1844
1918
  const totalRefunded = sumRefundQuantities(refunds);
1845
1919
  const isFullyPaid = total > 0 ? balance <= 0 : totalPaid > 0 && balance <= 0;
@@ -1847,13 +1921,11 @@ function buildOrderContext(order, extras) {
1847
1921
  let itemCount = 0;
1848
1922
  let fulfilledItemCount = 0;
1849
1923
  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);
1924
+ for (const row of [...lineItems, ...reservations]) {
1925
+ const { quantity, fulfilled, returned } = countsOf(row);
1926
+ itemCount += quantity;
1927
+ fulfilledItemCount += fulfilled;
1928
+ returnedItemCount += returned;
1857
1929
  }
1858
1930
  const isFullyReturned = itemCount > 0 ? returnedItemCount >= itemCount : returnedItemCount > 0;
1859
1931
  const hasPartialReturns = returnedItemCount > 0 && !isFullyReturned;
@@ -1863,7 +1935,8 @@ function buildOrderContext(order, extras) {
1863
1935
  balance,
1864
1936
  paymentMethods,
1865
1937
  refunds,
1866
- lineItems
1938
+ lineItems,
1939
+ reservations
1867
1940
  },
1868
1941
  computed: {
1869
1942
  balance,
@@ -2943,6 +3016,395 @@ var ORDER_STATE_EVENTS = {
2943
3016
  configUpdated: "config-updated"
2944
3017
  };
2945
3018
 
3019
+ // booking/types.ts
3020
+ var BookingRulesError = class extends Error {
3021
+ constructor(message) {
3022
+ super(message);
3023
+ this.name = "BookingRulesError";
3024
+ }
3025
+ };
3026
+
3027
+ // booking/zoned-time.ts
3028
+ var MINUTE = 6e4;
3029
+ var formatters = /* @__PURE__ */ new Map();
3030
+ var formatterFor = (timeZone) => {
3031
+ const existing = formatters.get(timeZone);
3032
+ if (existing) return existing;
3033
+ let created;
3034
+ try {
3035
+ created = new Intl.DateTimeFormat("en-US", {
3036
+ timeZone,
3037
+ hour12: false,
3038
+ year: "numeric",
3039
+ month: "2-digit",
3040
+ day: "2-digit",
3041
+ hour: "2-digit",
3042
+ minute: "2-digit",
3043
+ second: "2-digit",
3044
+ weekday: "short"
3045
+ });
3046
+ } catch {
3047
+ throw new BookingRulesError(`Booking rules: "${timeZone}" is not a known time zone`);
3048
+ }
3049
+ formatters.set(timeZone, created);
3050
+ return created;
3051
+ };
3052
+ var WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
3053
+ function zonedParts(instant, timeZone) {
3054
+ const parts = {};
3055
+ for (const { type, value } of formatterFor(timeZone).formatToParts(instant)) parts[type] = value;
3056
+ const hour = Number(parts.hour) % 24;
3057
+ return {
3058
+ year: Number(parts.year),
3059
+ month: Number(parts.month),
3060
+ day: Number(parts.day),
3061
+ hour,
3062
+ minute: Number(parts.minute),
3063
+ second: Number(parts.second),
3064
+ weekday: WEEKDAYS.indexOf(parts.weekday)
3065
+ };
3066
+ }
3067
+ function offsetAt(instant, timeZone) {
3068
+ const { year, month, day, hour, minute, second } = zonedParts(instant, timeZone);
3069
+ return Date.UTC(year, month - 1, day, hour, minute, second) - Math.floor(instant.getTime() / 1e3) * 1e3;
3070
+ }
3071
+ function instantFromLocal(year, month, day, minutes, timeZone) {
3072
+ const naive = Date.UTC(year, month - 1, day) + minutes * MINUTE;
3073
+ const firstGuess = naive - offsetAt(new Date(naive), timeZone);
3074
+ return new Date(naive - offsetAt(new Date(firstGuess), timeZone));
3075
+ }
3076
+ function startOfZonedDay(instant, timeZone) {
3077
+ const { year, month, day } = zonedParts(instant, timeZone);
3078
+ return instantFromLocal(year, month, day, 0, timeZone);
3079
+ }
3080
+ function nextZonedDay(dayStart, timeZone) {
3081
+ const { year, month, day } = zonedParts(dayStart, timeZone);
3082
+ return instantFromLocal(year, month, day + 1, 0, timeZone);
3083
+ }
3084
+ function atZonedMinute(dayStart, minutes, timeZone) {
3085
+ const { year, month, day } = zonedParts(dayStart, timeZone);
3086
+ return instantFromLocal(year, month, day, minutes, timeZone);
3087
+ }
3088
+
3089
+ // booking/grid.ts
3090
+ var MINUTE2 = 6e4;
3091
+ var MAX_RANGE_DAYS = 366;
3092
+ var asTime = (value, field) => {
3093
+ if (value == null) return void 0;
3094
+ const time = new Date(value).getTime();
3095
+ if (Number.isNaN(time)) throw new BookingRulesError(`Booking rules: ${field} is not a date, got "${String(value)}"`);
3096
+ return time;
3097
+ };
3098
+ function parseHhMm(value, field) {
3099
+ const [hours, minutes] = value.split(":").map(Number);
3100
+ if (!Number.isInteger(hours) || !Number.isInteger(minutes)) {
3101
+ throw new BookingRulesError(`Booking rules: ${field} must be "HH:mm", got "${value}"`);
3102
+ }
3103
+ return hours * 60 + minutes;
3104
+ }
3105
+ function windowEndsAt(rule, now) {
3106
+ const { maxAdvanceDays, bookableTo } = rule.window;
3107
+ const ends = Math.min(
3108
+ maxAdvanceDays ? now.getTime() + maxAdvanceDays * 1440 * MINUTE2 : Number.POSITIVE_INFINITY,
3109
+ asTime(bookableTo, "window.bookableTo") ?? Number.POSITIVE_INFINITY
3110
+ );
3111
+ return Number.isFinite(ends) ? new Date(ends) : null;
3112
+ }
3113
+ function periodSlot(dayStart, timeZone, rule, openMinute, closeMinute) {
3114
+ const { ratePeriod, slot, checkInTime, checkOutTime } = rule;
3115
+ const at = (minutes) => atZonedMinute(dayStart, minutes, timeZone);
3116
+ if (ratePeriod === "night" /* NIGHT */) {
3117
+ if (!checkInTime || !checkOutTime) {
3118
+ throw new BookingRulesError("Overnight booking rules need both checkInTime and checkOutTime");
3119
+ }
3120
+ const checkIn = parseHhMm(checkInTime, "checkInTime");
3121
+ const checkOut = parseHhMm(checkOutTime, "checkOutTime");
3122
+ return {
3123
+ startAt: at(checkIn),
3124
+ endAt: at(1440 + checkOut),
3125
+ // Housekeeping sits between check-out and the next check-in: it costs no night.
3126
+ bufferEndAt: at(1440 + checkIn),
3127
+ canStart: true
3128
+ };
3129
+ }
3130
+ const from = ratePeriod === "day" /* DAY */ ? openMinute ?? 0 : 0;
3131
+ const to = ratePeriod === "day" /* DAY */ ? closeMinute ?? 1440 : 1440;
3132
+ return {
3133
+ startAt: at(from),
3134
+ endAt: at(to),
3135
+ bufferEndAt: at(to + (slot.bufferMinutes ?? 0)),
3136
+ canStart: true
3137
+ };
3138
+ }
3139
+ function freeGaps(openAt, closeAt, busy) {
3140
+ const inside = busy.map(({ startAt, bufferEndAt }) => ({ from: +startAt, to: +bufferEndAt })).filter(({ from, to }) => to > openAt && from < closeAt).sort((first, second) => first.from - second.from);
3141
+ const gaps = [];
3142
+ let cursor = openAt;
3143
+ for (const { from, to } of inside) {
3144
+ if (from > cursor) gaps.push({ from: cursor, to: from, endsAtBooking: true });
3145
+ cursor = Math.max(cursor, to);
3146
+ if (cursor >= closeAt) break;
3147
+ }
3148
+ if (cursor < closeAt) gaps.push({ from: cursor, to: closeAt, endsAtBooking: false });
3149
+ return gaps;
3150
+ }
3151
+ function buildGrid(rule, timeZone, from, to, now, busy = []) {
3152
+ if (to.getTime() - from.getTime() > MAX_RANGE_DAYS * 1440 * MINUTE2) {
3153
+ throw new BookingRulesError(`A booking range may span at most ${MAX_RANGE_DAYS} days`);
3154
+ }
3155
+ const { availability, slot, window, ratePeriod } = rule;
3156
+ const periodMode = ratePeriod !== "slot" /* SLOT */;
3157
+ const weeklyHours = availability.weeklyHours ?? [];
3158
+ const notBefore = Math.max(
3159
+ now.getTime() + (window.minNoticeMinutes ?? 0) * MINUTE2,
3160
+ asTime(window.bookableFrom, "window.bookableFrom") ?? 0,
3161
+ asTime(availability.availableFrom, "availability.availableFrom") ?? 0
3162
+ );
3163
+ const notAfter = windowEndsAt(rule, now)?.getTime() ?? Number.POSITIVE_INFINITY;
3164
+ const blocked = (availability.offMarket ?? []).map(({ from: blockFrom, to: blockTo }) => ({
3165
+ from: asTime(blockFrom, "availability.offMarket.from"),
3166
+ to: asTime(blockTo, "availability.offMarket.to")
3167
+ }));
3168
+ const arrivalDays = availability.arrivalDays;
3169
+ const step = slot.lengthMinutes + (slot.bufferMinutes ?? 0);
3170
+ const slots = [];
3171
+ const lastDay = startOfZonedDay(to, timeZone).getTime();
3172
+ let dayStart = startOfZonedDay(from, timeZone);
3173
+ while (dayStart.getTime() <= lastDay) {
3174
+ const weekday = zonedParts(dayStart, timeZone).weekday;
3175
+ const rows = weeklyHours.filter((entry) => entry.weekday === weekday);
3176
+ if (periodMode) {
3177
+ const openToday = weeklyHours.length === 0 || rows.length > 0;
3178
+ if (openToday) {
3179
+ const built = periodSlot(dayStart, timeZone, rule, rows[0]?.openMinute, rows[0]?.closeMinute);
3180
+ slots.push({ ...built, canStart: !arrivalDays?.length || arrivalDays.includes(weekday) });
3181
+ }
3182
+ } else {
3183
+ for (const row of rows) {
3184
+ const openAt = +atZonedMinute(dayStart, row.openMinute, timeZone);
3185
+ const closeAt = +atZonedMinute(dayStart, row.closeMinute, timeZone);
3186
+ for (const gap of freeGaps(openAt, closeAt, busy)) {
3187
+ const mustFit = (gap.endsAtBooking ? step : slot.lengthMinutes) * MINUTE2;
3188
+ for (let start = gap.from; start + mustFit <= gap.to; start += step * MINUTE2) {
3189
+ slots.push({
3190
+ startAt: new Date(start),
3191
+ endAt: new Date(start + slot.lengthMinutes * MINUTE2),
3192
+ bufferEndAt: new Date(start + step * MINUTE2),
3193
+ canStart: true
3194
+ });
3195
+ }
3196
+ }
3197
+ }
3198
+ }
3199
+ dayStart = nextZonedDay(dayStart, timeZone);
3200
+ }
3201
+ const askedFrom = from.getTime();
3202
+ const askedTo = to.getTime();
3203
+ return slots.filter(({ startAt, bufferEndAt }) => {
3204
+ const start = startAt.getTime();
3205
+ if (start < askedFrom || start >= askedTo) return false;
3206
+ if (start < notBefore || start > notAfter) return false;
3207
+ return !blocked.some(
3208
+ ({ from: blockFrom, to: blockTo }) => start < blockTo && bufferEndAt.getTime() > blockFrom
3209
+ );
3210
+ }).sort((first, second) => +first.startAt - +second.startAt);
3211
+ }
3212
+ function slotsForWindow(grid, startAt, endAt) {
3213
+ const startIndex = grid.findIndex((entry) => entry.startAt.getTime() === startAt.getTime());
3214
+ if (startIndex < 0) {
3215
+ throw new BookingRulesError(`${startAt.toISOString()} is not a bookable slot start for these rules`);
3216
+ }
3217
+ if (!grid[startIndex].canStart) {
3218
+ throw new BookingRulesError(`${startAt.toISOString()} is not an arrival day for these rules`);
3219
+ }
3220
+ const covered = [];
3221
+ for (let index = startIndex; index < grid.length; index++) {
3222
+ const entry = grid[index];
3223
+ const previous = covered[covered.length - 1];
3224
+ if (previous && entry.startAt.getTime() !== previous.bufferEndAt.getTime()) break;
3225
+ covered.push(entry);
3226
+ if (entry.endAt.getTime() === endAt.getTime()) return covered;
3227
+ if (entry.endAt.getTime() > endAt.getTime()) break;
3228
+ }
3229
+ throw new BookingRulesError(
3230
+ `${startAt.toISOString()} \u2192 ${endAt.toISOString()} does not line up with the bookable grid`
3231
+ );
3232
+ }
3233
+
3234
+ // booking/availability.ts
3235
+ var withinPresence = (slot, timeZone, presence) => {
3236
+ if (!presence?.length) return true;
3237
+ const dayStart = startOfZonedDay(slot.startAt, timeZone);
3238
+ const weekday = zonedParts(dayStart, timeZone).weekday;
3239
+ return presence.some(
3240
+ (shift) => shift.weekday === weekday && slot.startAt.getTime() >= atZonedMinute(dayStart, shift.openMinute, timeZone).getTime() && slot.endAt.getTime() <= atZonedMinute(dayStart, shift.closeMinute, timeZone).getTime()
3241
+ );
3242
+ };
3243
+ var overlaps = (entry, startAt, bufferEndAt) => entry.startAt.getTime() < bufferEndAt.getTime() && entry.bufferEndAt.getTime() > startAt.getTime();
3244
+ function occupancyIn(occupied, productId, startAt, bufferEndAt, shared) {
3245
+ const clashing = occupied.filter((entry) => overlaps(entry, startAt, bufferEndAt));
3246
+ const foreign = shared ? void 0 : clashing.find((entry) => entry.productId !== productId);
3247
+ return {
3248
+ taken: clashing.filter((entry) => shared || entry.productId === productId).reduce((sum, entry) => sum + entry.qty, 0),
3249
+ foreignProductId: foreign?.productId
3250
+ };
3251
+ }
3252
+ var stateFor = (hasShift, offerable, free) => {
3253
+ if (offerable === 0) {
3254
+ return hasShift() ? "fullyBooked" /* FULLY_BOOKED */ : "noHoursInRange" /* NO_HOURS_IN_RANGE */;
3255
+ }
3256
+ return free === 0 ? "fullyBooked" /* FULLY_BOOKED */ : "available" /* AVAILABLE */;
3257
+ };
3258
+ function shopDayKey(instant, timeZone) {
3259
+ const { year, month, day } = zonedParts(instant, timeZone);
3260
+ const pad = (value) => String(value).padStart(2, "0");
3261
+ return `${year}-${pad(month)}-${pad(day)}`;
3262
+ }
3263
+ function shopDayRange(fromDayKey, count, timeZone) {
3264
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(fromDayKey ?? "");
3265
+ if (!match) {
3266
+ throw new BookingRulesError(`Booking rules: "${fromDayKey}" is not a shop day (YYYY-MM-DD)`);
3267
+ }
3268
+ if (!Number.isInteger(count) || count < 1) {
3269
+ throw new BookingRulesError(`Booking rules: "${count}" is not a number of days`);
3270
+ }
3271
+ const [, year, month, day] = match.map(Number);
3272
+ const from = instantFromLocal(year, month, day, 0, timeZone);
3273
+ if (shopDayKey(from, timeZone) !== fromDayKey) {
3274
+ throw new BookingRulesError(`Booking rules: "${fromDayKey}" is not a real day`);
3275
+ }
3276
+ return { from, to: instantFromLocal(year, month, day + count, 0, timeZone) };
3277
+ }
3278
+ function buildAvailability({
3279
+ productId,
3280
+ bookingRulesId,
3281
+ bookingType,
3282
+ rule,
3283
+ resources,
3284
+ occupied,
3285
+ from,
3286
+ to,
3287
+ now
3288
+ }) {
3289
+ const shared = rule.slot.sharedAcrossProducts ?? false;
3290
+ const bookableUntil = windowEndsAt(rule, now);
3291
+ const header = {
3292
+ productId,
3293
+ bookingRulesId,
3294
+ bookingType,
3295
+ ratePeriod: rule.ratePeriod,
3296
+ // Passed through for the UI to label: a group slot under minimumToRun may not run.
3297
+ minimumToRun: rule.slot.minimumToRun,
3298
+ minStay: rule.slot.minStay,
3299
+ maxStay: rule.slot.maxStay,
3300
+ // Why a day can come back empty. Past this instant the rule refuses everything, so the UI
3301
+ // can say "beyond the booking window" where it would otherwise claim "nothing free".
3302
+ bookableUntil,
3303
+ // The comparison itself, not just the boundary: deciding whether a day lies past the
3304
+ // window is a rule, and a UI that re-derives it owns a copy of that rule — which is how
3305
+ // the same empty grid ends up explained one way on the till and another in a flow.
3306
+ beyondWindow: !!bookableUntil && from.getTime() > bookableUntil.getTime()
3307
+ };
3308
+ const byResource = /* @__PURE__ */ new Map();
3309
+ for (const entry of occupied) {
3310
+ byResource.set(entry.resourceId, [...byResource.get(entry.resourceId) ?? [], entry]);
3311
+ }
3312
+ const grids = resources.map((resource) => {
3313
+ const mine = byResource.get(resource.id) ?? [];
3314
+ const busy = (resource.seats ?? 1) === 1 ? mine : [];
3315
+ const slots2 = buildGrid(rule, resource.timeZone, from, to, now, busy).filter(
3316
+ (slot) => withinPresence(slot, resource.timeZone, resource.presence)
3317
+ );
3318
+ return { resource, slots: slots2 };
3319
+ });
3320
+ const byInstant = /* @__PURE__ */ new Map();
3321
+ const freeSlotsPerResource = /* @__PURE__ */ new Map();
3322
+ for (const { resource, slots: slots2 } of grids) {
3323
+ const capacity = resource.seats ?? 1;
3324
+ const mine = byResource.get(resource.id) ?? [];
3325
+ for (const slot of slots2) {
3326
+ const { taken, foreignProductId } = occupancyIn(mine, productId, slot.startAt, slot.bufferEndAt, shared);
3327
+ const free = foreignProductId ? 0 : Math.max(capacity - taken, 0);
3328
+ const entry = byInstant.get(+slot.startAt) ?? {
3329
+ ...slot,
3330
+ capacity: 0,
3331
+ booked: 0,
3332
+ free: 0,
3333
+ resources: [],
3334
+ dayKey: shopDayKey(slot.startAt, resource.timeZone)
3335
+ };
3336
+ entry.capacity += capacity;
3337
+ entry.booked += foreignProductId ? capacity : taken;
3338
+ entry.free += free;
3339
+ entry.canStart = entry.canStart || slot.canStart;
3340
+ entry.resources.push({ resourceId: resource.id, name: resource.name, capacity, booked: taken, free });
3341
+ if (free > 0) freeSlotsPerResource.set(resource.id, (freeSlotsPerResource.get(resource.id) ?? 0) + 1);
3342
+ byInstant.set(+slot.startAt, entry);
3343
+ }
3344
+ }
3345
+ const inWindow = grids.map(({ resource, slots: slots2 }) => {
3346
+ const free = freeSlotsPerResource.get(resource.id) ?? 0;
3347
+ return {
3348
+ resourceId: resource.id,
3349
+ name: resource.name,
3350
+ slots: slots2.length,
3351
+ free,
3352
+ // Lazily: the shift grid is a second walk, and only an empty answer needs it.
3353
+ state: stateFor(
3354
+ () => buildGrid(rule, resource.timeZone, from, to, now).filter(
3355
+ (slot) => withinPresence(slot, resource.timeZone, resource.presence)
3356
+ ).length > 0,
3357
+ slots2.length,
3358
+ free
3359
+ )
3360
+ };
3361
+ });
3362
+ const slots = [...byInstant.values()].sort((first, second) => +first.startAt - +second.startAt);
3363
+ return {
3364
+ ...header,
3365
+ resources: inWindow,
3366
+ slots,
3367
+ /**
3368
+ * The clock every instant above reads on. Returned rather than assumed: without it a caller
3369
+ * has to fetch the zone from somewhere else and hope it is the same one the grid used.
3370
+ */
3371
+ timeZone: resources[0]?.timeZone ?? null,
3372
+ /** The shop days this answer covers, in order — what a day picker lists. */
3373
+ days: [...new Set(slots.map((slot) => slot.dayKey))]
3374
+ };
3375
+ }
3376
+ function presentAt(resources, rows, outletId) {
3377
+ const byResource = /* @__PURE__ */ new Map();
3378
+ for (const row of rows) {
3379
+ byResource.set(row.resourceId, [...byResource.get(row.resourceId) ?? [], row]);
3380
+ }
3381
+ return resources.flatMap((resource) => {
3382
+ const own = byResource.get(resource.id) ?? [];
3383
+ if (own.length === 0 || !outletId) return [{ resource }];
3384
+ const here = own.find((row) => row.outletId === outletId);
3385
+ return here ? [{ resource, presence: here.weeklyHours }] : [];
3386
+ });
3387
+ }
3388
+
3389
+ // booking/plan.ts
3390
+ var DEFAULT_HOLD_MINUTES = 10;
3391
+ function planWindow({ rule, timeZone, presence, startAt, endAt, now, busy = [] }) {
3392
+ const grid = buildGrid(rule, timeZone, startAt, endAt, now, busy).filter(
3393
+ (slot) => withinPresence(slot, timeZone, presence)
3394
+ );
3395
+ const slots = slotsForWindow(grid, startAt, endAt);
3396
+ const { minStay, maxStay } = rule.slot;
3397
+ if (minStay && slots.length < minStay) {
3398
+ throw new BookingRulesError(`These rules ask for at least ${minStay} period(s), got ${slots.length}`);
3399
+ }
3400
+ if (maxStay && slots.length > maxStay) {
3401
+ throw new BookingRulesError(`These rules allow at most ${maxStay} period(s), got ${slots.length}`);
3402
+ }
3403
+ const last = slots[slots.length - 1];
3404
+ return { slots, endAt: last.endAt, bufferEndAt: last.bufferEndAt };
3405
+ }
3406
+ var holdExpiresAt = (rule, now) => new Date(now.getTime() + (rule.window.holdMinutes ?? DEFAULT_HOLD_MINUTES) * 6e4);
3407
+
2946
3408
  // constants/adyen.constants.ts
2947
3409
  var ADYEN_ROLLOUT_COUNTRY_CODES = /* @__PURE__ */ new Set(["CA"]);
2948
3410
  function isAdyenRolloutCountry(alpha2) {
@@ -2981,6 +3443,11 @@ function adyenRequiresJobTitle(associationType) {
2981
3443
  ADYEN_TAX_NUMBER_FORMATS,
2982
3444
  ADYEN_TAX_NUMBER_MIN_LENGTH,
2983
3445
  AttributeType,
3446
+ BookingRatePeriod,
3447
+ BookingResourceKind,
3448
+ BookingResourceStatus,
3449
+ BookingRulesError,
3450
+ BookingType,
2984
3451
  CONDITION_OPERATORS,
2985
3452
  COUNTRY_CURRENCIES,
2986
3453
  CURRENCY_CONFIG,
@@ -2991,6 +3458,7 @@ function adyenRequiresJobTitle(associationType) {
2991
3458
  DEFAULT_CONFIG,
2992
3459
  DEFAULT_CROSS_AXIS_RULES,
2993
3460
  DEFAULT_DISPLAY_STATE_MAP,
3461
+ DEFAULT_HOLD_MINUTES,
2994
3462
  DEFAULT_RESOLVED_CONFIG,
2995
3463
  DEFAULT_UNIT_ID,
2996
3464
  DualWriteStrategy,
@@ -3034,8 +3502,10 @@ function adyenRequiresJobTitle(associationType) {
3034
3502
  REPORT_NEGATE_PATHS,
3035
3503
  ReceiptSendChannel,
3036
3504
  ReceiptType,
3505
+ ReservationStatus,
3037
3506
  ResidualPaymentMethod,
3038
3507
  ResidualStatus,
3508
+ ResourceAvailabilityState,
3039
3509
  STATE_SCHEMA,
3040
3510
  STRIPE_SUPPORTED_COUNTRIES,
3041
3511
  SYSTEM_FAMILIES,
@@ -3061,6 +3531,8 @@ function adyenRequiresJobTitle(associationType) {
3061
3531
  baseFulfillmentDefinitions,
3062
3532
  baseOf,
3063
3533
  basePaymentDefinitions,
3534
+ buildAvailability,
3535
+ buildGrid,
3064
3536
  buildOrderContext,
3065
3537
  buildPosPresetConfig,
3066
3538
  buildResolvedConfig,
@@ -3100,6 +3572,7 @@ function adyenRequiresJobTitle(associationType) {
3100
3572
  getPricingRegionFromCountry,
3101
3573
  getSettlementTotal,
3102
3574
  getTotalCollected,
3575
+ holdExpiresAt,
3103
3576
  inferStatesFromLegacyStatus,
3104
3577
  isAdyenRolloutCountry,
3105
3578
  isConditionOperator,
@@ -3114,9 +3587,12 @@ function adyenRequiresJobTitle(associationType) {
3114
3587
  mergeFragment,
3115
3588
  multiplyAmount,
3116
3589
  normalizeMongoIdString,
3590
+ occupancyIn,
3117
3591
  parseAmountToMinorUnits,
3118
3592
  parseCurrencyCode,
3119
3593
  parseStripeCurrencyCode,
3594
+ planWindow,
3595
+ presentAt,
3120
3596
  removeFragment,
3121
3597
  resolveFamily,
3122
3598
  resolveTemplate,
@@ -3124,13 +3600,18 @@ function adyenRequiresJobTitle(associationType) {
3124
3600
  roundToPrecision,
3125
3601
  sameFamily,
3126
3602
  setOrderState,
3603
+ shopDayKey,
3604
+ shopDayRange,
3127
3605
  signReportForDisplay,
3606
+ slotsForWindow,
3128
3607
  stringifyMinorUnits,
3129
3608
  subtractAmounts,
3130
3609
  toBase,
3131
3610
  toMinorUnits,
3132
3611
  validateConfig,
3133
3612
  validateFragmentCompatibility,
3134
- violatesFinancialInvariant
3613
+ violatesFinancialInvariant,
3614
+ windowEndsAt,
3615
+ withinPresence
3135
3616
  });
3136
3617
  //# sourceMappingURL=index.js.map