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