@final-commerce/common 2.2.3-staging3.2 → 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
@@ -19,8 +19,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  // index.ts
20
20
  var index_exports = {};
21
21
  __export(index_exports, {
22
- ADYEN_ROLLOUT_COUNTRY_CODES: () => ADYEN_ROLLOUT_COUNTRY_CODES,
23
22
  AttributeType: () => AttributeType,
23
+ BookingRatePeriod: () => BookingRatePeriod,
24
+ BookingResourceKind: () => BookingResourceKind,
25
+ BookingResourceStatus: () => BookingResourceStatus,
26
+ BookingRulesError: () => BookingRulesError,
27
+ BookingType: () => BookingType,
24
28
  CONDITION_OPERATORS: () => CONDITION_OPERATORS,
25
29
  COUNTRY_CURRENCIES: () => COUNTRY_CURRENCIES,
26
30
  CURRENCY_CONFIG: () => CURRENCY_CONFIG,
@@ -31,6 +35,7 @@ __export(index_exports, {
31
35
  DEFAULT_CONFIG: () => DEFAULT_CONFIG,
32
36
  DEFAULT_CROSS_AXIS_RULES: () => DEFAULT_CROSS_AXIS_RULES,
33
37
  DEFAULT_DISPLAY_STATE_MAP: () => DEFAULT_DISPLAY_STATE_MAP,
38
+ DEFAULT_HOLD_MINUTES: () => DEFAULT_HOLD_MINUTES,
34
39
  DEFAULT_RESOLVED_CONFIG: () => DEFAULT_RESOLVED_CONFIG,
35
40
  DEFAULT_UNIT_ID: () => DEFAULT_UNIT_ID,
36
41
  DualWriteStrategy: () => DualWriteStrategy,
@@ -74,8 +79,10 @@ __export(index_exports, {
74
79
  REPORT_NEGATE_PATHS: () => REPORT_NEGATE_PATHS,
75
80
  ReceiptSendChannel: () => ReceiptSendChannel,
76
81
  ReceiptType: () => ReceiptType,
82
+ ReservationStatus: () => ReservationStatus,
77
83
  ResidualPaymentMethod: () => ResidualPaymentMethod,
78
84
  ResidualStatus: () => ResidualStatus,
85
+ ResourceAvailabilityState: () => ResourceAvailabilityState,
79
86
  STATE_SCHEMA: () => STATE_SCHEMA,
80
87
  STRIPE_SUPPORTED_COUNTRIES: () => STRIPE_SUPPORTED_COUNTRIES,
81
88
  SYSTEM_FAMILIES: () => SYSTEM_FAMILIES,
@@ -96,6 +103,8 @@ __export(index_exports, {
96
103
  baseFulfillmentDefinitions: () => baseFulfillmentDefinitions,
97
104
  baseOf: () => baseOf,
98
105
  basePaymentDefinitions: () => basePaymentDefinitions,
106
+ buildAvailability: () => buildAvailability,
107
+ buildGrid: () => buildGrid,
99
108
  buildOrderContext: () => buildOrderContext,
100
109
  buildPosPresetConfig: () => buildPosPresetConfig,
101
110
  buildResolvedConfig: () => buildResolvedConfig,
@@ -135,8 +144,8 @@ __export(index_exports, {
135
144
  getPricingRegionFromCountry: () => getPricingRegionFromCountry,
136
145
  getSettlementTotal: () => getSettlementTotal,
137
146
  getTotalCollected: () => getTotalCollected,
147
+ holdExpiresAt: () => holdExpiresAt,
138
148
  inferStatesFromLegacyStatus: () => inferStatesFromLegacyStatus,
139
- isAdyenRolloutCountry: () => isAdyenRolloutCountry,
140
149
  isConditionOperator: () => isConditionOperator,
141
150
  isFulfillmentState: () => isFulfillmentState,
142
151
  isPaymentState: () => isPaymentState,
@@ -149,9 +158,12 @@ __export(index_exports, {
149
158
  mergeFragment: () => mergeFragment,
150
159
  multiplyAmount: () => multiplyAmount,
151
160
  normalizeMongoIdString: () => normalizeMongoIdString,
161
+ occupancyIn: () => occupancyIn,
152
162
  parseAmountToMinorUnits: () => parseAmountToMinorUnits,
153
163
  parseCurrencyCode: () => parseCurrencyCode,
154
164
  parseStripeCurrencyCode: () => parseStripeCurrencyCode,
165
+ planWindow: () => planWindow,
166
+ presentAt: () => presentAt,
155
167
  removeFragment: () => removeFragment,
156
168
  resolveFamily: () => resolveFamily,
157
169
  resolveTemplate: () => resolveTemplate,
@@ -160,13 +172,16 @@ __export(index_exports, {
160
172
  sameFamily: () => sameFamily,
161
173
  setOrderState: () => setOrderState,
162
174
  signReportForDisplay: () => signReportForDisplay,
175
+ slotsForWindow: () => slotsForWindow,
163
176
  stringifyMinorUnits: () => stringifyMinorUnits,
164
177
  subtractAmounts: () => subtractAmounts,
165
178
  toBase: () => toBase,
166
179
  toMinorUnits: () => toMinorUnits,
167
180
  validateConfig: () => validateConfig,
168
181
  validateFragmentCompatibility: () => validateFragmentCompatibility,
169
- violatesFinancialInvariant: () => violatesFinancialInvariant
182
+ violatesFinancialInvariant: () => violatesFinancialInvariant,
183
+ windowEndsAt: () => windowEndsAt,
184
+ withinPresence: () => withinPresence
170
185
  });
171
186
  module.exports = __toCommonJS(index_exports);
172
187
 
@@ -592,6 +607,7 @@ var StripeCurrencyCode = /* @__PURE__ */ ((StripeCurrencyCode2) => {
592
607
  var ProductType = /* @__PURE__ */ ((ProductType2) => {
593
608
  ProductType2["SIMPLE"] = "simple";
594
609
  ProductType2["VARIABLE"] = "variable";
610
+ ProductType2["BOOKING"] = "booking";
595
611
  return ProductType2;
596
612
  })(ProductType || {});
597
613
  var ProductStatus = /* @__PURE__ */ ((ProductStatus2) => {
@@ -609,6 +625,50 @@ var PaymentProcessor = /* @__PURE__ */ ((PaymentProcessor2) => {
609
625
  return PaymentProcessor2;
610
626
  })(PaymentProcessor || {});
611
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
+
612
672
  // utils/currency.util.ts
613
673
  var CURRENCY_CONFIG = {
614
674
  // Major currencies with 2 decimal places
@@ -1818,6 +1878,15 @@ function sumRefundQuantities(refunds) {
1818
1878
  }
1819
1879
  return t;
1820
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
+ }
1821
1890
  function buildOrderContext(order, extras) {
1822
1891
  const o = order ?? {};
1823
1892
  const total = typeof o.total === "number" && Number.isFinite(o.total) ? o.total : 0;
@@ -1825,6 +1894,7 @@ function buildOrderContext(order, extras) {
1825
1894
  const paymentMethods = o.paymentMethods ?? [];
1826
1895
  const refunds = o.refunds ?? o.refund ?? [];
1827
1896
  const lineItems = o.lineItems ?? [];
1897
+ const reservations = o.reservations ?? [];
1828
1898
  const totalPaid = sumNumbers(paymentMethods);
1829
1899
  const totalRefunded = sumRefundQuantities(refunds);
1830
1900
  const isFullyPaid = total > 0 ? balance <= 0 : totalPaid > 0 && balance <= 0;
@@ -1832,13 +1902,11 @@ function buildOrderContext(order, extras) {
1832
1902
  let itemCount = 0;
1833
1903
  let fulfilledItemCount = 0;
1834
1904
  let returnedItemCount = 0;
1835
- for (const li of lineItems) {
1836
- const q = typeof li.quantity === "number" && Number.isFinite(li.quantity) ? Math.max(0, li.quantity) : 0;
1837
- itemCount += q;
1838
- const fq = typeof li.fulfilledQuantity === "number" && Number.isFinite(li.fulfilledQuantity) ? Math.max(0, li.fulfilledQuantity) : 0;
1839
- fulfilledItemCount += Math.min(fq, q);
1840
- const rq = typeof li.returnedQuantity === "number" && Number.isFinite(li.returnedQuantity) ? Math.max(0, li.returnedQuantity) : 0;
1841
- 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;
1842
1910
  }
1843
1911
  const isFullyReturned = itemCount > 0 ? returnedItemCount >= itemCount : returnedItemCount > 0;
1844
1912
  const hasPartialReturns = returnedItemCount > 0 && !isFullyReturned;
@@ -1848,7 +1916,8 @@ function buildOrderContext(order, extras) {
1848
1916
  balance,
1849
1917
  paymentMethods,
1850
1918
  refunds,
1851
- lineItems
1919
+ lineItems,
1920
+ reservations
1852
1921
  },
1853
1922
  computed: {
1854
1923
  balance,
@@ -2928,15 +2997,365 @@ var ORDER_STATE_EVENTS = {
2928
2997
  configUpdated: "config-updated"
2929
2998
  };
2930
2999
 
2931
- // constants/adyen.constants.ts
2932
- var ADYEN_ROLLOUT_COUNTRY_CODES = /* @__PURE__ */ new Set(["CA"]);
2933
- function isAdyenRolloutCountry(alpha2) {
2934
- return ADYEN_ROLLOUT_COUNTRY_CODES.has(alpha2);
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 };
2935
3349
  }
3350
+ var holdExpiresAt = (rule, now) => new Date(now.getTime() + (rule.window.holdMinutes ?? DEFAULT_HOLD_MINUTES) * 6e4);
2936
3351
  // Annotate the CommonJS export names for ESM import in node:
2937
3352
  0 && (module.exports = {
2938
- ADYEN_ROLLOUT_COUNTRY_CODES,
2939
3353
  AttributeType,
3354
+ BookingRatePeriod,
3355
+ BookingResourceKind,
3356
+ BookingResourceStatus,
3357
+ BookingRulesError,
3358
+ BookingType,
2940
3359
  CONDITION_OPERATORS,
2941
3360
  COUNTRY_CURRENCIES,
2942
3361
  CURRENCY_CONFIG,
@@ -2947,6 +3366,7 @@ function isAdyenRolloutCountry(alpha2) {
2947
3366
  DEFAULT_CONFIG,
2948
3367
  DEFAULT_CROSS_AXIS_RULES,
2949
3368
  DEFAULT_DISPLAY_STATE_MAP,
3369
+ DEFAULT_HOLD_MINUTES,
2950
3370
  DEFAULT_RESOLVED_CONFIG,
2951
3371
  DEFAULT_UNIT_ID,
2952
3372
  DualWriteStrategy,
@@ -2990,8 +3410,10 @@ function isAdyenRolloutCountry(alpha2) {
2990
3410
  REPORT_NEGATE_PATHS,
2991
3411
  ReceiptSendChannel,
2992
3412
  ReceiptType,
3413
+ ReservationStatus,
2993
3414
  ResidualPaymentMethod,
2994
3415
  ResidualStatus,
3416
+ ResourceAvailabilityState,
2995
3417
  STATE_SCHEMA,
2996
3418
  STRIPE_SUPPORTED_COUNTRIES,
2997
3419
  SYSTEM_FAMILIES,
@@ -3012,6 +3434,8 @@ function isAdyenRolloutCountry(alpha2) {
3012
3434
  baseFulfillmentDefinitions,
3013
3435
  baseOf,
3014
3436
  basePaymentDefinitions,
3437
+ buildAvailability,
3438
+ buildGrid,
3015
3439
  buildOrderContext,
3016
3440
  buildPosPresetConfig,
3017
3441
  buildResolvedConfig,
@@ -3051,8 +3475,8 @@ function isAdyenRolloutCountry(alpha2) {
3051
3475
  getPricingRegionFromCountry,
3052
3476
  getSettlementTotal,
3053
3477
  getTotalCollected,
3478
+ holdExpiresAt,
3054
3479
  inferStatesFromLegacyStatus,
3055
- isAdyenRolloutCountry,
3056
3480
  isConditionOperator,
3057
3481
  isFulfillmentState,
3058
3482
  isPaymentState,
@@ -3065,9 +3489,12 @@ function isAdyenRolloutCountry(alpha2) {
3065
3489
  mergeFragment,
3066
3490
  multiplyAmount,
3067
3491
  normalizeMongoIdString,
3492
+ occupancyIn,
3068
3493
  parseAmountToMinorUnits,
3069
3494
  parseCurrencyCode,
3070
3495
  parseStripeCurrencyCode,
3496
+ planWindow,
3497
+ presentAt,
3071
3498
  removeFragment,
3072
3499
  resolveFamily,
3073
3500
  resolveTemplate,
@@ -3076,12 +3503,15 @@ function isAdyenRolloutCountry(alpha2) {
3076
3503
  sameFamily,
3077
3504
  setOrderState,
3078
3505
  signReportForDisplay,
3506
+ slotsForWindow,
3079
3507
  stringifyMinorUnits,
3080
3508
  subtractAmounts,
3081
3509
  toBase,
3082
3510
  toMinorUnits,
3083
3511
  validateConfig,
3084
3512
  validateFragmentCompatibility,
3085
- violatesFinancialInvariant
3513
+ violatesFinancialInvariant,
3514
+ windowEndsAt,
3515
+ withinPresence
3086
3516
  });
3087
3517
  //# sourceMappingURL=index.js.map