@final-commerce/common 2.4.1-staging2.1 → 2.5.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
@@ -19,14 +19,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  // index.ts
20
20
  var index_exports = {};
21
21
  __export(index_exports, {
22
- ADYEN_JOB_TITLE_ASSOCIATION_TYPES: () => ADYEN_JOB_TITLE_ASSOCIATION_TYPES,
23
- ADYEN_LEGAL_ENTITY_STATE_PROVINCE_COUNTRIES: () => ADYEN_LEGAL_ENTITY_STATE_PROVINCE_COUNTRIES,
24
- ADYEN_NO_TAX_NUMBER_ABSENT_COUNTRIES: () => ADYEN_NO_TAX_NUMBER_ABSENT_COUNTRIES,
25
22
  ADYEN_ROLLOUT_COUNTRY_CODES: () => ADYEN_ROLLOUT_COUNTRY_CODES,
26
- ADYEN_STORE_STATE_PROVINCE_COUNTRIES: () => ADYEN_STORE_STATE_PROVINCE_COUNTRIES,
27
- ADYEN_TAX_NUMBER_FORMATS: () => ADYEN_TAX_NUMBER_FORMATS,
28
- ADYEN_TAX_NUMBER_MIN_LENGTH: () => ADYEN_TAX_NUMBER_MIN_LENGTH,
29
23
  AttributeType: () => AttributeType,
24
+ BookingRatePeriod: () => BookingRatePeriod,
25
+ BookingResourceKind: () => BookingResourceKind,
26
+ BookingResourceStatus: () => BookingResourceStatus,
27
+ BookingRulesError: () => BookingRulesError,
28
+ BookingType: () => BookingType,
30
29
  CONDITION_OPERATORS: () => CONDITION_OPERATORS,
31
30
  COUNTRY_CURRENCIES: () => COUNTRY_CURRENCIES,
32
31
  CURRENCY_CONFIG: () => CURRENCY_CONFIG,
@@ -37,6 +36,7 @@ __export(index_exports, {
37
36
  DEFAULT_CONFIG: () => DEFAULT_CONFIG,
38
37
  DEFAULT_CROSS_AXIS_RULES: () => DEFAULT_CROSS_AXIS_RULES,
39
38
  DEFAULT_DISPLAY_STATE_MAP: () => DEFAULT_DISPLAY_STATE_MAP,
39
+ DEFAULT_HOLD_MINUTES: () => DEFAULT_HOLD_MINUTES,
40
40
  DEFAULT_RESOLVED_CONFIG: () => DEFAULT_RESOLVED_CONFIG,
41
41
  DEFAULT_UNIT_ID: () => DEFAULT_UNIT_ID,
42
42
  DualWriteStrategy: () => DualWriteStrategy,
@@ -80,8 +80,10 @@ __export(index_exports, {
80
80
  REPORT_NEGATE_PATHS: () => REPORT_NEGATE_PATHS,
81
81
  ReceiptSendChannel: () => ReceiptSendChannel,
82
82
  ReceiptType: () => ReceiptType,
83
+ ReservationStatus: () => ReservationStatus,
83
84
  ResidualPaymentMethod: () => ResidualPaymentMethod,
84
85
  ResidualStatus: () => ResidualStatus,
86
+ ResourceAvailabilityState: () => ResourceAvailabilityState,
85
87
  STATE_SCHEMA: () => STATE_SCHEMA,
86
88
  STRIPE_SUPPORTED_COUNTRIES: () => STRIPE_SUPPORTED_COUNTRIES,
87
89
  SYSTEM_FAMILIES: () => SYSTEM_FAMILIES,
@@ -95,11 +97,6 @@ __export(index_exports, {
95
97
  TopMetericsSort: () => TopMetericsSort,
96
98
  UserTypes: () => UserTypes,
97
99
  addAmounts: () => addAmounts,
98
- adyenLegalEntityRequiresStateOrProvince: () => adyenLegalEntityRequiresStateOrProvince,
99
- adyenRequiresJobTitle: () => adyenRequiresJobTitle,
100
- adyenStoreRequiresStateOrProvince: () => adyenStoreRequiresStateOrProvince,
101
- adyenSupportsTaxNumberAbsent: () => adyenSupportsTaxNumberAbsent,
102
- adyenTaxNumberFormat: () => adyenTaxNumberFormat,
103
100
  amountToString: () => amountToString,
104
101
  assertUnitSane: () => assertUnitSane,
105
102
  availableIn: () => availableIn,
@@ -107,6 +104,8 @@ __export(index_exports, {
107
104
  baseFulfillmentDefinitions: () => baseFulfillmentDefinitions,
108
105
  baseOf: () => baseOf,
109
106
  basePaymentDefinitions: () => basePaymentDefinitions,
107
+ buildAvailability: () => buildAvailability,
108
+ buildGrid: () => buildGrid,
110
109
  buildOrderContext: () => buildOrderContext,
111
110
  buildPosPresetConfig: () => buildPosPresetConfig,
112
111
  buildResolvedConfig: () => buildResolvedConfig,
@@ -146,6 +145,7 @@ __export(index_exports, {
146
145
  getPricingRegionFromCountry: () => getPricingRegionFromCountry,
147
146
  getSettlementTotal: () => getSettlementTotal,
148
147
  getTotalCollected: () => getTotalCollected,
148
+ holdExpiresAt: () => holdExpiresAt,
149
149
  inferStatesFromLegacyStatus: () => inferStatesFromLegacyStatus,
150
150
  isAdyenRolloutCountry: () => isAdyenRolloutCountry,
151
151
  isConditionOperator: () => isConditionOperator,
@@ -160,9 +160,12 @@ __export(index_exports, {
160
160
  mergeFragment: () => mergeFragment,
161
161
  multiplyAmount: () => multiplyAmount,
162
162
  normalizeMongoIdString: () => normalizeMongoIdString,
163
+ occupancyIn: () => occupancyIn,
163
164
  parseAmountToMinorUnits: () => parseAmountToMinorUnits,
164
165
  parseCurrencyCode: () => parseCurrencyCode,
165
166
  parseStripeCurrencyCode: () => parseStripeCurrencyCode,
167
+ planWindow: () => planWindow,
168
+ presentAt: () => presentAt,
166
169
  removeFragment: () => removeFragment,
167
170
  resolveFamily: () => resolveFamily,
168
171
  resolveTemplate: () => resolveTemplate,
@@ -170,14 +173,19 @@ __export(index_exports, {
170
173
  roundToPrecision: () => roundToPrecision,
171
174
  sameFamily: () => sameFamily,
172
175
  setOrderState: () => setOrderState,
176
+ shopDayKey: () => shopDayKey,
177
+ shopDayRange: () => shopDayRange,
173
178
  signReportForDisplay: () => signReportForDisplay,
179
+ slotsForWindow: () => slotsForWindow,
174
180
  stringifyMinorUnits: () => stringifyMinorUnits,
175
181
  subtractAmounts: () => subtractAmounts,
176
182
  toBase: () => toBase,
177
183
  toMinorUnits: () => toMinorUnits,
178
184
  validateConfig: () => validateConfig,
179
185
  validateFragmentCompatibility: () => validateFragmentCompatibility,
180
- violatesFinancialInvariant: () => violatesFinancialInvariant
186
+ violatesFinancialInvariant: () => violatesFinancialInvariant,
187
+ windowEndsAt: () => windowEndsAt,
188
+ withinPresence: () => withinPresence
181
189
  });
182
190
  module.exports = __toCommonJS(index_exports);
183
191
 
@@ -420,6 +428,7 @@ var InventorySpecificActionType = /* @__PURE__ */ ((InventorySpecificActionType2
420
428
  InventorySpecificActionType2["PO_RECEIVED"] = "PO_RECEIVED";
421
429
  InventorySpecificActionType2["EXTERNAL_SALE"] = "EXTERNAL_SALE";
422
430
  InventorySpecificActionType2["EXTERNAL_RESTOCK"] = "EXTERNAL_RESTOCK";
431
+ InventorySpecificActionType2["CUSTOM"] = "CUSTOM";
423
432
  return InventorySpecificActionType2;
424
433
  })(InventorySpecificActionType || {});
425
434
  var ExtensionCategory = /* @__PURE__ */ ((ExtensionCategory2) => {
@@ -605,6 +614,7 @@ var StripeCurrencyCode = /* @__PURE__ */ ((StripeCurrencyCode2) => {
605
614
  var ProductType = /* @__PURE__ */ ((ProductType2) => {
606
615
  ProductType2["SIMPLE"] = "simple";
607
616
  ProductType2["VARIABLE"] = "variable";
617
+ ProductType2["BOOKING"] = "booking";
608
618
  return ProductType2;
609
619
  })(ProductType || {});
610
620
  var ProductStatus = /* @__PURE__ */ ((ProductStatus2) => {
@@ -622,6 +632,50 @@ var PaymentProcessor = /* @__PURE__ */ ((PaymentProcessor2) => {
622
632
  return PaymentProcessor2;
623
633
  })(PaymentProcessor || {});
624
634
 
635
+ // enums/booking.enum.ts
636
+ var BookingType = /* @__PURE__ */ ((BookingType3) => {
637
+ BookingType3["RENTAL"] = "rental";
638
+ BookingType3["APPOINTMENT"] = "appointment";
639
+ BookingType3["OVERNIGHT"] = "overnight";
640
+ BookingType3["GROUP"] = "group";
641
+ return BookingType3;
642
+ })(BookingType || {});
643
+ var BookingRatePeriod = /* @__PURE__ */ ((BookingRatePeriod2) => {
644
+ BookingRatePeriod2["SLOT"] = "slot";
645
+ BookingRatePeriod2["DAY"] = "day";
646
+ BookingRatePeriod2["NIGHT"] = "night";
647
+ BookingRatePeriod2["WEEK"] = "week";
648
+ BookingRatePeriod2["MONTH"] = "month";
649
+ return BookingRatePeriod2;
650
+ })(BookingRatePeriod || {});
651
+ var BookingResourceKind = /* @__PURE__ */ ((BookingResourceKind2) => {
652
+ BookingResourceKind2["ASSET"] = "asset";
653
+ BookingResourceKind2["ROOM"] = "room";
654
+ BookingResourceKind2["PROPERTY"] = "property";
655
+ BookingResourceKind2["SPACE"] = "space";
656
+ BookingResourceKind2["STAFF"] = "staff";
657
+ BookingResourceKind2["TABLE"] = "table";
658
+ return BookingResourceKind2;
659
+ })(BookingResourceKind || {});
660
+ var ReservationStatus = /* @__PURE__ */ ((ReservationStatus2) => {
661
+ ReservationStatus2["HELD"] = "held";
662
+ ReservationStatus2["CONFIRMED"] = "confirmed";
663
+ ReservationStatus2["CANCELLED"] = "cancelled";
664
+ ReservationStatus2["EXPIRED"] = "expired";
665
+ return ReservationStatus2;
666
+ })(ReservationStatus || {});
667
+ var BookingResourceStatus = /* @__PURE__ */ ((BookingResourceStatus2) => {
668
+ BookingResourceStatus2["ACTIVE"] = "active";
669
+ BookingResourceStatus2["INACTIVE"] = "inactive";
670
+ return BookingResourceStatus2;
671
+ })(BookingResourceStatus || {});
672
+ var ResourceAvailabilityState = /* @__PURE__ */ ((ResourceAvailabilityState2) => {
673
+ ResourceAvailabilityState2["AVAILABLE"] = "available";
674
+ ResourceAvailabilityState2["FULLY_BOOKED"] = "fullyBooked";
675
+ ResourceAvailabilityState2["NO_HOURS_IN_RANGE"] = "noHoursInRange";
676
+ return ResourceAvailabilityState2;
677
+ })(ResourceAvailabilityState || {});
678
+
625
679
  // utils/currency.util.ts
626
680
  var CURRENCY_CONFIG = {
627
681
  // Major currencies with 2 decimal places
@@ -1833,6 +1887,15 @@ function sumRefundQuantities(refunds) {
1833
1887
  }
1834
1888
  return t;
1835
1889
  }
1890
+ var asCount = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
1891
+ function countsOf(row) {
1892
+ const quantity = asCount(row.quantity);
1893
+ return {
1894
+ quantity,
1895
+ fulfilled: Math.min(asCount(row.fulfilledQuantity), quantity),
1896
+ returned: Math.min(asCount(row.returnedQuantity), quantity)
1897
+ };
1898
+ }
1836
1899
  function buildOrderContext(order, extras) {
1837
1900
  const o = order ?? {};
1838
1901
  const total = typeof o.total === "number" && Number.isFinite(o.total) ? o.total : 0;
@@ -1840,6 +1903,7 @@ function buildOrderContext(order, extras) {
1840
1903
  const paymentMethods = o.paymentMethods ?? [];
1841
1904
  const refunds = o.refunds ?? o.refund ?? [];
1842
1905
  const lineItems = o.lineItems ?? [];
1906
+ const reservations = o.reservations ?? [];
1843
1907
  const totalPaid = sumNumbers(paymentMethods);
1844
1908
  const totalRefunded = sumRefundQuantities(refunds);
1845
1909
  const isFullyPaid = total > 0 ? balance <= 0 : totalPaid > 0 && balance <= 0;
@@ -1847,13 +1911,11 @@ function buildOrderContext(order, extras) {
1847
1911
  let itemCount = 0;
1848
1912
  let fulfilledItemCount = 0;
1849
1913
  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);
1914
+ for (const row of [...lineItems, ...reservations]) {
1915
+ const { quantity, fulfilled, returned } = countsOf(row);
1916
+ itemCount += quantity;
1917
+ fulfilledItemCount += fulfilled;
1918
+ returnedItemCount += returned;
1857
1919
  }
1858
1920
  const isFullyReturned = itemCount > 0 ? returnedItemCount >= itemCount : returnedItemCount > 0;
1859
1921
  const hasPartialReturns = returnedItemCount > 0 && !isFullyReturned;
@@ -1863,7 +1925,8 @@ function buildOrderContext(order, extras) {
1863
1925
  balance,
1864
1926
  paymentMethods,
1865
1927
  refunds,
1866
- lineItems
1928
+ lineItems,
1929
+ reservations
1867
1930
  },
1868
1931
  computed: {
1869
1932
  balance,
@@ -2943,44 +3006,400 @@ var ORDER_STATE_EVENTS = {
2943
3006
  configUpdated: "config-updated"
2944
3007
  };
2945
3008
 
2946
- // constants/adyen.constants.ts
2947
- var ADYEN_ROLLOUT_COUNTRY_CODES = /* @__PURE__ */ new Set(["CA"]);
2948
- function isAdyenRolloutCountry(alpha2) {
2949
- return ADYEN_ROLLOUT_COUNTRY_CODES.has(alpha2);
3009
+ // booking/types.ts
3010
+ var BookingRulesError = class extends Error {
3011
+ constructor(message) {
3012
+ super(message);
3013
+ this.name = "BookingRulesError";
3014
+ }
3015
+ };
3016
+
3017
+ // booking/zoned-time.ts
3018
+ var MINUTE = 6e4;
3019
+ var formatters = /* @__PURE__ */ new Map();
3020
+ var formatterFor = (timeZone) => {
3021
+ const existing = formatters.get(timeZone);
3022
+ if (existing) return existing;
3023
+ let created;
3024
+ try {
3025
+ created = new Intl.DateTimeFormat("en-US", {
3026
+ timeZone,
3027
+ hour12: false,
3028
+ year: "numeric",
3029
+ month: "2-digit",
3030
+ day: "2-digit",
3031
+ hour: "2-digit",
3032
+ minute: "2-digit",
3033
+ second: "2-digit",
3034
+ weekday: "short"
3035
+ });
3036
+ } catch {
3037
+ throw new BookingRulesError(`Booking rules: "${timeZone}" is not a known time zone`);
3038
+ }
3039
+ formatters.set(timeZone, created);
3040
+ return created;
3041
+ };
3042
+ var WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
3043
+ function zonedParts(instant, timeZone) {
3044
+ const parts = {};
3045
+ for (const { type, value } of formatterFor(timeZone).formatToParts(instant)) parts[type] = value;
3046
+ const hour = Number(parts.hour) % 24;
3047
+ return {
3048
+ year: Number(parts.year),
3049
+ month: Number(parts.month),
3050
+ day: Number(parts.day),
3051
+ hour,
3052
+ minute: Number(parts.minute),
3053
+ second: Number(parts.second),
3054
+ weekday: WEEKDAYS.indexOf(parts.weekday)
3055
+ };
2950
3056
  }
2951
- var ADYEN_STORE_STATE_PROVINCE_COUNTRIES = /* @__PURE__ */ new Set(["AU", "BR", "CA", "IN", "MX", "NZ", "US"]);
2952
- function adyenStoreRequiresStateOrProvince(alpha2) {
2953
- return !!alpha2 && ADYEN_STORE_STATE_PROVINCE_COUNTRIES.has(alpha2);
3057
+ function offsetAt(instant, timeZone) {
3058
+ const { year, month, day, hour, minute, second } = zonedParts(instant, timeZone);
3059
+ return Date.UTC(year, month - 1, day, hour, minute, second) - Math.floor(instant.getTime() / 1e3) * 1e3;
2954
3060
  }
2955
- var ADYEN_LEGAL_ENTITY_STATE_PROVINCE_COUNTRIES = /* @__PURE__ */ new Set(["AU", "BR", "CA", "IN", "MX", "NZ", "US"]);
2956
- function adyenLegalEntityRequiresStateOrProvince(alpha2) {
2957
- return !!alpha2 && ADYEN_LEGAL_ENTITY_STATE_PROVINCE_COUNTRIES.has(alpha2);
3061
+ function instantFromLocal(year, month, day, minutes, timeZone) {
3062
+ const naive = Date.UTC(year, month - 1, day) + minutes * MINUTE;
3063
+ const firstGuess = naive - offsetAt(new Date(naive), timeZone);
3064
+ return new Date(naive - offsetAt(new Date(firstGuess), timeZone));
3065
+ }
3066
+ function startOfZonedDay(instant, timeZone) {
3067
+ const { year, month, day } = zonedParts(instant, timeZone);
3068
+ return instantFromLocal(year, month, day, 0, timeZone);
3069
+ }
3070
+ function nextZonedDay(dayStart, timeZone) {
3071
+ const { year, month, day } = zonedParts(dayStart, timeZone);
3072
+ return instantFromLocal(year, month, day + 1, 0, timeZone);
3073
+ }
3074
+ function atZonedMinute(dayStart, minutes, timeZone) {
3075
+ const { year, month, day } = zonedParts(dayStart, timeZone);
3076
+ return instantFromLocal(year, month, day, minutes, timeZone);
3077
+ }
3078
+
3079
+ // booking/grid.ts
3080
+ var MINUTE2 = 6e4;
3081
+ var MAX_RANGE_DAYS = 366;
3082
+ var asTime = (value, field) => {
3083
+ if (value == null) return void 0;
3084
+ const time = new Date(value).getTime();
3085
+ if (Number.isNaN(time)) throw new BookingRulesError(`Booking rules: ${field} is not a date, got "${String(value)}"`);
3086
+ return time;
3087
+ };
3088
+ function parseHhMm(value, field) {
3089
+ const [hours, minutes] = value.split(":").map(Number);
3090
+ if (!Number.isInteger(hours) || !Number.isInteger(minutes)) {
3091
+ throw new BookingRulesError(`Booking rules: ${field} must be "HH:mm", got "${value}"`);
3092
+ }
3093
+ return hours * 60 + minutes;
3094
+ }
3095
+ function windowEndsAt(rule, now) {
3096
+ const { maxAdvanceDays, bookableTo } = rule.window;
3097
+ const ends = Math.min(
3098
+ maxAdvanceDays ? now.getTime() + maxAdvanceDays * 1440 * MINUTE2 : Number.POSITIVE_INFINITY,
3099
+ asTime(bookableTo, "window.bookableTo") ?? Number.POSITIVE_INFINITY
3100
+ );
3101
+ return Number.isFinite(ends) ? new Date(ends) : null;
3102
+ }
3103
+ function periodSlot(dayStart, timeZone, rule, openMinute, closeMinute) {
3104
+ const { ratePeriod, slot, checkInTime, checkOutTime } = rule;
3105
+ const at = (minutes) => atZonedMinute(dayStart, minutes, timeZone);
3106
+ if (ratePeriod === "night" /* NIGHT */) {
3107
+ if (!checkInTime || !checkOutTime) {
3108
+ throw new BookingRulesError("Overnight booking rules need both checkInTime and checkOutTime");
3109
+ }
3110
+ const checkIn = parseHhMm(checkInTime, "checkInTime");
3111
+ const checkOut = parseHhMm(checkOutTime, "checkOutTime");
3112
+ return {
3113
+ startAt: at(checkIn),
3114
+ endAt: at(1440 + checkOut),
3115
+ // Housekeeping sits between check-out and the next check-in: it costs no night.
3116
+ bufferEndAt: at(1440 + checkIn),
3117
+ canStart: true
3118
+ };
3119
+ }
3120
+ const from = ratePeriod === "day" /* DAY */ ? openMinute ?? 0 : 0;
3121
+ const to = ratePeriod === "day" /* DAY */ ? closeMinute ?? 1440 : 1440;
3122
+ return {
3123
+ startAt: at(from),
3124
+ endAt: at(to),
3125
+ bufferEndAt: at(to + (slot.bufferMinutes ?? 0)),
3126
+ canStart: true
3127
+ };
3128
+ }
3129
+ function freeGaps(openAt, closeAt, busy) {
3130
+ const inside = busy.map(({ startAt, bufferEndAt }) => ({ from: +startAt, to: +bufferEndAt })).filter(({ from, to }) => to > openAt && from < closeAt).sort((first, second) => first.from - second.from);
3131
+ const gaps = [];
3132
+ let cursor = openAt;
3133
+ for (const { from, to } of inside) {
3134
+ if (from > cursor) gaps.push({ from: cursor, to: from, endsAtBooking: true });
3135
+ cursor = Math.max(cursor, to);
3136
+ if (cursor >= closeAt) break;
3137
+ }
3138
+ if (cursor < closeAt) gaps.push({ from: cursor, to: closeAt, endsAtBooking: false });
3139
+ return gaps;
3140
+ }
3141
+ function buildGrid(rule, timeZone, from, to, now, busy = []) {
3142
+ if (to.getTime() - from.getTime() > MAX_RANGE_DAYS * 1440 * MINUTE2) {
3143
+ throw new BookingRulesError(`A booking range may span at most ${MAX_RANGE_DAYS} days`);
3144
+ }
3145
+ const { availability, slot, window, ratePeriod } = rule;
3146
+ const periodMode = ratePeriod !== "slot" /* SLOT */;
3147
+ const weeklyHours = availability.weeklyHours ?? [];
3148
+ const notBefore = Math.max(
3149
+ now.getTime() + (window.minNoticeMinutes ?? 0) * MINUTE2,
3150
+ asTime(window.bookableFrom, "window.bookableFrom") ?? 0,
3151
+ asTime(availability.availableFrom, "availability.availableFrom") ?? 0
3152
+ );
3153
+ const notAfter = windowEndsAt(rule, now)?.getTime() ?? Number.POSITIVE_INFINITY;
3154
+ const blocked = (availability.offMarket ?? []).map(({ from: blockFrom, to: blockTo }) => ({
3155
+ from: asTime(blockFrom, "availability.offMarket.from"),
3156
+ to: asTime(blockTo, "availability.offMarket.to")
3157
+ }));
3158
+ const arrivalDays = availability.arrivalDays;
3159
+ const step = slot.lengthMinutes + (slot.bufferMinutes ?? 0);
3160
+ const slots = [];
3161
+ const lastDay = startOfZonedDay(to, timeZone).getTime();
3162
+ let dayStart = startOfZonedDay(from, timeZone);
3163
+ while (dayStart.getTime() <= lastDay) {
3164
+ const weekday = zonedParts(dayStart, timeZone).weekday;
3165
+ const rows = weeklyHours.filter((entry) => entry.weekday === weekday);
3166
+ if (periodMode) {
3167
+ const openToday = weeklyHours.length === 0 || rows.length > 0;
3168
+ if (openToday) {
3169
+ const built = periodSlot(dayStart, timeZone, rule, rows[0]?.openMinute, rows[0]?.closeMinute);
3170
+ slots.push({ ...built, canStart: !arrivalDays?.length || arrivalDays.includes(weekday) });
3171
+ }
3172
+ } else {
3173
+ for (const row of rows) {
3174
+ const openAt = +atZonedMinute(dayStart, row.openMinute, timeZone);
3175
+ const closeAt = +atZonedMinute(dayStart, row.closeMinute, timeZone);
3176
+ for (const gap of freeGaps(openAt, closeAt, busy)) {
3177
+ const mustFit = (gap.endsAtBooking ? step : slot.lengthMinutes) * MINUTE2;
3178
+ for (let start = gap.from; start + mustFit <= gap.to; start += step * MINUTE2) {
3179
+ slots.push({
3180
+ startAt: new Date(start),
3181
+ endAt: new Date(start + slot.lengthMinutes * MINUTE2),
3182
+ bufferEndAt: new Date(start + step * MINUTE2),
3183
+ canStart: true
3184
+ });
3185
+ }
3186
+ }
3187
+ }
3188
+ }
3189
+ dayStart = nextZonedDay(dayStart, timeZone);
3190
+ }
3191
+ const askedFrom = from.getTime();
3192
+ const askedTo = to.getTime();
3193
+ return slots.filter(({ startAt, bufferEndAt }) => {
3194
+ const start = startAt.getTime();
3195
+ if (start < askedFrom || start >= askedTo) return false;
3196
+ if (start < notBefore || start > notAfter) return false;
3197
+ return !blocked.some(({ from: blockFrom, to: blockTo }) => start < blockTo && bufferEndAt.getTime() > blockFrom);
3198
+ }).sort((first, second) => +first.startAt - +second.startAt);
3199
+ }
3200
+ function slotsForWindow(grid, startAt, endAt) {
3201
+ const startIndex = grid.findIndex((entry) => entry.startAt.getTime() === startAt.getTime());
3202
+ if (startIndex < 0) {
3203
+ throw new BookingRulesError(`${startAt.toISOString()} is not a bookable slot start for these rules`);
3204
+ }
3205
+ if (!grid[startIndex].canStart) {
3206
+ throw new BookingRulesError(`${startAt.toISOString()} is not an arrival day for these rules`);
3207
+ }
3208
+ const covered = [];
3209
+ for (let index = startIndex; index < grid.length; index++) {
3210
+ const entry = grid[index];
3211
+ const previous = covered[covered.length - 1];
3212
+ if (previous && entry.startAt.getTime() !== previous.bufferEndAt.getTime()) break;
3213
+ covered.push(entry);
3214
+ if (entry.endAt.getTime() === endAt.getTime()) return covered;
3215
+ if (entry.endAt.getTime() > endAt.getTime()) break;
3216
+ }
3217
+ throw new BookingRulesError(
3218
+ `${startAt.toISOString()} \u2192 ${endAt.toISOString()} does not line up with the bookable grid`
3219
+ );
2958
3220
  }
2959
- var ADYEN_NO_TAX_NUMBER_ABSENT_COUNTRIES = /* @__PURE__ */ new Set(["CA"]);
2960
- function adyenSupportsTaxNumberAbsent(alpha2) {
2961
- return !!alpha2 && !ADYEN_NO_TAX_NUMBER_ABSENT_COUNTRIES.has(alpha2);
3221
+
3222
+ // booking/availability.ts
3223
+ var withinPresence = (slot, timeZone, presence) => {
3224
+ if (!presence?.length) return true;
3225
+ const dayStart = startOfZonedDay(slot.startAt, timeZone);
3226
+ const weekday = zonedParts(dayStart, timeZone).weekday;
3227
+ return presence.some(
3228
+ (shift) => shift.weekday === weekday && slot.startAt.getTime() >= atZonedMinute(dayStart, shift.openMinute, timeZone).getTime() && slot.endAt.getTime() <= atZonedMinute(dayStart, shift.closeMinute, timeZone).getTime()
3229
+ );
3230
+ };
3231
+ var overlaps = (entry, startAt, bufferEndAt) => entry.startAt.getTime() < bufferEndAt.getTime() && entry.bufferEndAt.getTime() > startAt.getTime();
3232
+ function occupancyIn(occupied, productId, startAt, bufferEndAt, shared) {
3233
+ const clashing = occupied.filter((entry) => overlaps(entry, startAt, bufferEndAt));
3234
+ const foreign = shared ? void 0 : clashing.find((entry) => entry.productId !== productId);
3235
+ return {
3236
+ taken: clashing.filter((entry) => shared || entry.productId === productId).reduce((sum, entry) => sum + entry.qty, 0),
3237
+ foreignProductId: foreign?.productId
3238
+ };
2962
3239
  }
2963
- var ADYEN_TAX_NUMBER_FORMATS = {
2964
- CA: { pattern: /^\d{9}$/, length: 9 }
3240
+ var stateFor = (hasShift, offerable, free) => {
3241
+ if (offerable === 0) {
3242
+ return hasShift() ? "fullyBooked" /* FULLY_BOOKED */ : "noHoursInRange" /* NO_HOURS_IN_RANGE */;
3243
+ }
3244
+ return free === 0 ? "fullyBooked" /* FULLY_BOOKED */ : "available" /* AVAILABLE */;
2965
3245
  };
2966
- function adyenTaxNumberFormat(alpha2) {
2967
- return alpha2 ? ADYEN_TAX_NUMBER_FORMATS[alpha2] : void 0;
3246
+ function shopDayKey(instant, timeZone) {
3247
+ const { year, month, day } = zonedParts(instant, timeZone);
3248
+ const pad = (value) => String(value).padStart(2, "0");
3249
+ return `${year}-${pad(month)}-${pad(day)}`;
3250
+ }
3251
+ function shopDayRange(fromDayKey, count, timeZone) {
3252
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(fromDayKey ?? "");
3253
+ if (!match) {
3254
+ throw new BookingRulesError(`Booking rules: "${fromDayKey}" is not a shop day (YYYY-MM-DD)`);
3255
+ }
3256
+ if (!Number.isInteger(count) || count < 1) {
3257
+ throw new BookingRulesError(`Booking rules: "${count}" is not a number of days`);
3258
+ }
3259
+ const [, year, month, day] = match.map(Number);
3260
+ const from = instantFromLocal(year, month, day, 0, timeZone);
3261
+ if (shopDayKey(from, timeZone) !== fromDayKey) {
3262
+ throw new BookingRulesError(`Booking rules: "${fromDayKey}" is not a real day`);
3263
+ }
3264
+ return { from, to: instantFromLocal(year, month, day + count, 0, timeZone) };
3265
+ }
3266
+ function buildAvailability({
3267
+ productId,
3268
+ bookingRulesId,
3269
+ bookingType,
3270
+ rule,
3271
+ resources,
3272
+ occupied,
3273
+ from,
3274
+ to,
3275
+ now
3276
+ }) {
3277
+ const shared = rule.slot.sharedAcrossProducts ?? false;
3278
+ const bookableUntil = windowEndsAt(rule, now);
3279
+ const header = {
3280
+ productId,
3281
+ bookingRulesId,
3282
+ bookingType,
3283
+ ratePeriod: rule.ratePeriod,
3284
+ // Passed through for the UI to label: a group slot under minimumToRun may not run.
3285
+ minimumToRun: rule.slot.minimumToRun,
3286
+ minStay: rule.slot.minStay,
3287
+ maxStay: rule.slot.maxStay,
3288
+ // Why a day can come back empty. Past this instant the rule refuses everything, so the UI
3289
+ // can say "beyond the booking window" where it would otherwise claim "nothing free".
3290
+ bookableUntil,
3291
+ // The comparison itself, not just the boundary: deciding whether a day lies past the
3292
+ // window is a rule, and a UI that re-derives it owns a copy of that rule — which is how
3293
+ // the same empty grid ends up explained one way on the till and another in a flow.
3294
+ beyondWindow: !!bookableUntil && from.getTime() > bookableUntil.getTime()
3295
+ };
3296
+ const byResource = /* @__PURE__ */ new Map();
3297
+ for (const entry of occupied) {
3298
+ byResource.set(entry.resourceId, [...byResource.get(entry.resourceId) ?? [], entry]);
3299
+ }
3300
+ const grids = resources.map((resource) => {
3301
+ const mine = byResource.get(resource.id) ?? [];
3302
+ const busy = (resource.seats ?? 1) === 1 ? mine : [];
3303
+ const slots2 = buildGrid(rule, resource.timeZone, from, to, now, busy).filter(
3304
+ (slot) => withinPresence(slot, resource.timeZone, resource.presence)
3305
+ );
3306
+ return { resource, slots: slots2 };
3307
+ });
3308
+ const byInstant = /* @__PURE__ */ new Map();
3309
+ const freeSlotsPerResource = /* @__PURE__ */ new Map();
3310
+ for (const { resource, slots: slots2 } of grids) {
3311
+ const capacity = resource.seats ?? 1;
3312
+ const mine = byResource.get(resource.id) ?? [];
3313
+ for (const slot of slots2) {
3314
+ const { taken, foreignProductId } = occupancyIn(mine, productId, slot.startAt, slot.bufferEndAt, shared);
3315
+ const free = foreignProductId ? 0 : Math.max(capacity - taken, 0);
3316
+ const entry = byInstant.get(+slot.startAt) ?? { ...slot, capacity: 0, booked: 0, free: 0, resources: [], dayKey: shopDayKey(slot.startAt, resource.timeZone) };
3317
+ entry.capacity += capacity;
3318
+ entry.booked += foreignProductId ? capacity : taken;
3319
+ entry.free += free;
3320
+ entry.canStart = entry.canStart || slot.canStart;
3321
+ entry.resources.push({ resourceId: resource.id, name: resource.name, capacity, booked: taken, free });
3322
+ if (free > 0) freeSlotsPerResource.set(resource.id, (freeSlotsPerResource.get(resource.id) ?? 0) + 1);
3323
+ byInstant.set(+slot.startAt, entry);
3324
+ }
3325
+ }
3326
+ const inWindow = grids.map(({ resource, slots: slots2 }) => {
3327
+ const free = freeSlotsPerResource.get(resource.id) ?? 0;
3328
+ return {
3329
+ resourceId: resource.id,
3330
+ name: resource.name,
3331
+ slots: slots2.length,
3332
+ free,
3333
+ // Lazily: the shift grid is a second walk, and only an empty answer needs it.
3334
+ state: stateFor(
3335
+ () => buildGrid(rule, resource.timeZone, from, to, now).filter(
3336
+ (slot) => withinPresence(slot, resource.timeZone, resource.presence)
3337
+ ).length > 0,
3338
+ slots2.length,
3339
+ free
3340
+ )
3341
+ };
3342
+ });
3343
+ const slots = [...byInstant.values()].sort((first, second) => +first.startAt - +second.startAt);
3344
+ return {
3345
+ ...header,
3346
+ resources: inWindow,
3347
+ slots,
3348
+ /**
3349
+ * The clock every instant above reads on. Returned rather than assumed: without it a caller
3350
+ * has to fetch the zone from somewhere else and hope it is the same one the grid used.
3351
+ */
3352
+ timeZone: resources[0]?.timeZone ?? null,
3353
+ /** The shop days this answer covers, in order — what a day picker lists. */
3354
+ days: [...new Set(slots.map((slot) => slot.dayKey))]
3355
+ };
2968
3356
  }
2969
- var ADYEN_TAX_NUMBER_MIN_LENGTH = 5;
2970
- var ADYEN_JOB_TITLE_ASSOCIATION_TYPES = ["signatory", "uboThroughControl"];
2971
- function adyenRequiresJobTitle(associationType) {
2972
- return ADYEN_JOB_TITLE_ASSOCIATION_TYPES.includes(associationType);
3357
+ function presentAt(resources, rows, outletId) {
3358
+ const byResource = /* @__PURE__ */ new Map();
3359
+ for (const row of rows) {
3360
+ byResource.set(row.resourceId, [...byResource.get(row.resourceId) ?? [], row]);
3361
+ }
3362
+ return resources.flatMap((resource) => {
3363
+ const own = byResource.get(resource.id) ?? [];
3364
+ if (own.length === 0 || !outletId) return [{ resource }];
3365
+ const here = own.find((row) => row.outletId === outletId);
3366
+ return here ? [{ resource, presence: here.weeklyHours }] : [];
3367
+ });
3368
+ }
3369
+
3370
+ // booking/plan.ts
3371
+ var DEFAULT_HOLD_MINUTES = 10;
3372
+ function planWindow({ rule, timeZone, presence, startAt, endAt, now, busy = [] }) {
3373
+ const grid = buildGrid(rule, timeZone, startAt, endAt, now, busy).filter(
3374
+ (slot) => withinPresence(slot, timeZone, presence)
3375
+ );
3376
+ const slots = slotsForWindow(grid, startAt, endAt);
3377
+ const { minStay, maxStay } = rule.slot;
3378
+ if (minStay && slots.length < minStay) {
3379
+ throw new BookingRulesError(`These rules ask for at least ${minStay} period(s), got ${slots.length}`);
3380
+ }
3381
+ if (maxStay && slots.length > maxStay) {
3382
+ throw new BookingRulesError(`These rules allow at most ${maxStay} period(s), got ${slots.length}`);
3383
+ }
3384
+ const last = slots[slots.length - 1];
3385
+ return { slots, endAt: last.endAt, bufferEndAt: last.bufferEndAt };
3386
+ }
3387
+ var holdExpiresAt = (rule, now) => new Date(now.getTime() + (rule.window.holdMinutes ?? DEFAULT_HOLD_MINUTES) * 6e4);
3388
+
3389
+ // constants/adyen.constants.ts
3390
+ var ADYEN_ROLLOUT_COUNTRY_CODES = /* @__PURE__ */ new Set(["CA"]);
3391
+ function isAdyenRolloutCountry(alpha2) {
3392
+ return ADYEN_ROLLOUT_COUNTRY_CODES.has(alpha2);
2973
3393
  }
2974
3394
  // Annotate the CommonJS export names for ESM import in node:
2975
3395
  0 && (module.exports = {
2976
- ADYEN_JOB_TITLE_ASSOCIATION_TYPES,
2977
- ADYEN_LEGAL_ENTITY_STATE_PROVINCE_COUNTRIES,
2978
- ADYEN_NO_TAX_NUMBER_ABSENT_COUNTRIES,
2979
3396
  ADYEN_ROLLOUT_COUNTRY_CODES,
2980
- ADYEN_STORE_STATE_PROVINCE_COUNTRIES,
2981
- ADYEN_TAX_NUMBER_FORMATS,
2982
- ADYEN_TAX_NUMBER_MIN_LENGTH,
2983
3397
  AttributeType,
3398
+ BookingRatePeriod,
3399
+ BookingResourceKind,
3400
+ BookingResourceStatus,
3401
+ BookingRulesError,
3402
+ BookingType,
2984
3403
  CONDITION_OPERATORS,
2985
3404
  COUNTRY_CURRENCIES,
2986
3405
  CURRENCY_CONFIG,
@@ -2991,6 +3410,7 @@ function adyenRequiresJobTitle(associationType) {
2991
3410
  DEFAULT_CONFIG,
2992
3411
  DEFAULT_CROSS_AXIS_RULES,
2993
3412
  DEFAULT_DISPLAY_STATE_MAP,
3413
+ DEFAULT_HOLD_MINUTES,
2994
3414
  DEFAULT_RESOLVED_CONFIG,
2995
3415
  DEFAULT_UNIT_ID,
2996
3416
  DualWriteStrategy,
@@ -3034,8 +3454,10 @@ function adyenRequiresJobTitle(associationType) {
3034
3454
  REPORT_NEGATE_PATHS,
3035
3455
  ReceiptSendChannel,
3036
3456
  ReceiptType,
3457
+ ReservationStatus,
3037
3458
  ResidualPaymentMethod,
3038
3459
  ResidualStatus,
3460
+ ResourceAvailabilityState,
3039
3461
  STATE_SCHEMA,
3040
3462
  STRIPE_SUPPORTED_COUNTRIES,
3041
3463
  SYSTEM_FAMILIES,
@@ -3049,11 +3471,6 @@ function adyenRequiresJobTitle(associationType) {
3049
3471
  TopMetericsSort,
3050
3472
  UserTypes,
3051
3473
  addAmounts,
3052
- adyenLegalEntityRequiresStateOrProvince,
3053
- adyenRequiresJobTitle,
3054
- adyenStoreRequiresStateOrProvince,
3055
- adyenSupportsTaxNumberAbsent,
3056
- adyenTaxNumberFormat,
3057
3474
  amountToString,
3058
3475
  assertUnitSane,
3059
3476
  availableIn,
@@ -3061,6 +3478,8 @@ function adyenRequiresJobTitle(associationType) {
3061
3478
  baseFulfillmentDefinitions,
3062
3479
  baseOf,
3063
3480
  basePaymentDefinitions,
3481
+ buildAvailability,
3482
+ buildGrid,
3064
3483
  buildOrderContext,
3065
3484
  buildPosPresetConfig,
3066
3485
  buildResolvedConfig,
@@ -3100,6 +3519,7 @@ function adyenRequiresJobTitle(associationType) {
3100
3519
  getPricingRegionFromCountry,
3101
3520
  getSettlementTotal,
3102
3521
  getTotalCollected,
3522
+ holdExpiresAt,
3103
3523
  inferStatesFromLegacyStatus,
3104
3524
  isAdyenRolloutCountry,
3105
3525
  isConditionOperator,
@@ -3114,9 +3534,12 @@ function adyenRequiresJobTitle(associationType) {
3114
3534
  mergeFragment,
3115
3535
  multiplyAmount,
3116
3536
  normalizeMongoIdString,
3537
+ occupancyIn,
3117
3538
  parseAmountToMinorUnits,
3118
3539
  parseCurrencyCode,
3119
3540
  parseStripeCurrencyCode,
3541
+ planWindow,
3542
+ presentAt,
3120
3543
  removeFragment,
3121
3544
  resolveFamily,
3122
3545
  resolveTemplate,
@@ -3124,13 +3547,18 @@ function adyenRequiresJobTitle(associationType) {
3124
3547
  roundToPrecision,
3125
3548
  sameFamily,
3126
3549
  setOrderState,
3550
+ shopDayKey,
3551
+ shopDayRange,
3127
3552
  signReportForDisplay,
3553
+ slotsForWindow,
3128
3554
  stringifyMinorUnits,
3129
3555
  subtractAmounts,
3130
3556
  toBase,
3131
3557
  toMinorUnits,
3132
3558
  validateConfig,
3133
3559
  validateFragmentCompatibility,
3134
- violatesFinancialInvariant
3560
+ violatesFinancialInvariant,
3561
+ windowEndsAt,
3562
+ withinPresence
3135
3563
  });
3136
3564
  //# sourceMappingURL=index.js.map