@final-commerce/common 2.2.3-staging3.2 → 2.3.0-staging.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.mjs CHANGED
@@ -420,6 +420,7 @@ var StripeCurrencyCode = /* @__PURE__ */ ((StripeCurrencyCode2) => {
420
420
  var ProductType = /* @__PURE__ */ ((ProductType2) => {
421
421
  ProductType2["SIMPLE"] = "simple";
422
422
  ProductType2["VARIABLE"] = "variable";
423
+ ProductType2["BOOKING"] = "booking";
423
424
  return ProductType2;
424
425
  })(ProductType || {});
425
426
  var ProductStatus = /* @__PURE__ */ ((ProductStatus2) => {
@@ -437,6 +438,50 @@ var PaymentProcessor = /* @__PURE__ */ ((PaymentProcessor2) => {
437
438
  return PaymentProcessor2;
438
439
  })(PaymentProcessor || {});
439
440
 
441
+ // enums/booking.enum.ts
442
+ var BookingType = /* @__PURE__ */ ((BookingType3) => {
443
+ BookingType3["RENTAL"] = "rental";
444
+ BookingType3["APPOINTMENT"] = "appointment";
445
+ BookingType3["OVERNIGHT"] = "overnight";
446
+ BookingType3["GROUP"] = "group";
447
+ return BookingType3;
448
+ })(BookingType || {});
449
+ var BookingRatePeriod = /* @__PURE__ */ ((BookingRatePeriod2) => {
450
+ BookingRatePeriod2["SLOT"] = "slot";
451
+ BookingRatePeriod2["DAY"] = "day";
452
+ BookingRatePeriod2["NIGHT"] = "night";
453
+ BookingRatePeriod2["WEEK"] = "week";
454
+ BookingRatePeriod2["MONTH"] = "month";
455
+ return BookingRatePeriod2;
456
+ })(BookingRatePeriod || {});
457
+ var BookingResourceKind = /* @__PURE__ */ ((BookingResourceKind2) => {
458
+ BookingResourceKind2["ASSET"] = "asset";
459
+ BookingResourceKind2["ROOM"] = "room";
460
+ BookingResourceKind2["PROPERTY"] = "property";
461
+ BookingResourceKind2["SPACE"] = "space";
462
+ BookingResourceKind2["STAFF"] = "staff";
463
+ BookingResourceKind2["TABLE"] = "table";
464
+ return BookingResourceKind2;
465
+ })(BookingResourceKind || {});
466
+ var ReservationStatus = /* @__PURE__ */ ((ReservationStatus2) => {
467
+ ReservationStatus2["HELD"] = "held";
468
+ ReservationStatus2["CONFIRMED"] = "confirmed";
469
+ ReservationStatus2["CANCELLED"] = "cancelled";
470
+ ReservationStatus2["EXPIRED"] = "expired";
471
+ return ReservationStatus2;
472
+ })(ReservationStatus || {});
473
+ var BookingResourceStatus = /* @__PURE__ */ ((BookingResourceStatus2) => {
474
+ BookingResourceStatus2["ACTIVE"] = "active";
475
+ BookingResourceStatus2["INACTIVE"] = "inactive";
476
+ return BookingResourceStatus2;
477
+ })(BookingResourceStatus || {});
478
+ var ResourceAvailabilityState = /* @__PURE__ */ ((ResourceAvailabilityState2) => {
479
+ ResourceAvailabilityState2["AVAILABLE"] = "available";
480
+ ResourceAvailabilityState2["FULLY_BOOKED"] = "fullyBooked";
481
+ ResourceAvailabilityState2["NO_HOURS_IN_RANGE"] = "noHoursInRange";
482
+ return ResourceAvailabilityState2;
483
+ })(ResourceAvailabilityState || {});
484
+
440
485
  // utils/currency.util.ts
441
486
  var CURRENCY_CONFIG = {
442
487
  // Major currencies with 2 decimal places
@@ -1646,6 +1691,15 @@ function sumRefundQuantities(refunds) {
1646
1691
  }
1647
1692
  return t;
1648
1693
  }
1694
+ var asCount = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
1695
+ function countsOf(row) {
1696
+ const quantity = asCount(row.quantity);
1697
+ return {
1698
+ quantity,
1699
+ fulfilled: Math.min(asCount(row.fulfilledQuantity), quantity),
1700
+ returned: Math.min(asCount(row.returnedQuantity), quantity)
1701
+ };
1702
+ }
1649
1703
  function buildOrderContext(order, extras) {
1650
1704
  const o = order ?? {};
1651
1705
  const total = typeof o.total === "number" && Number.isFinite(o.total) ? o.total : 0;
@@ -1653,6 +1707,7 @@ function buildOrderContext(order, extras) {
1653
1707
  const paymentMethods = o.paymentMethods ?? [];
1654
1708
  const refunds = o.refunds ?? o.refund ?? [];
1655
1709
  const lineItems = o.lineItems ?? [];
1710
+ const reservations = o.reservations ?? [];
1656
1711
  const totalPaid = sumNumbers(paymentMethods);
1657
1712
  const totalRefunded = sumRefundQuantities(refunds);
1658
1713
  const isFullyPaid = total > 0 ? balance <= 0 : totalPaid > 0 && balance <= 0;
@@ -1660,13 +1715,11 @@ function buildOrderContext(order, extras) {
1660
1715
  let itemCount = 0;
1661
1716
  let fulfilledItemCount = 0;
1662
1717
  let returnedItemCount = 0;
1663
- for (const li of lineItems) {
1664
- const q = typeof li.quantity === "number" && Number.isFinite(li.quantity) ? Math.max(0, li.quantity) : 0;
1665
- itemCount += q;
1666
- const fq = typeof li.fulfilledQuantity === "number" && Number.isFinite(li.fulfilledQuantity) ? Math.max(0, li.fulfilledQuantity) : 0;
1667
- fulfilledItemCount += Math.min(fq, q);
1668
- const rq = typeof li.returnedQuantity === "number" && Number.isFinite(li.returnedQuantity) ? Math.max(0, li.returnedQuantity) : 0;
1669
- returnedItemCount += Math.min(rq, q);
1718
+ for (const row of [...lineItems, ...reservations]) {
1719
+ const { quantity, fulfilled, returned } = countsOf(row);
1720
+ itemCount += quantity;
1721
+ fulfilledItemCount += fulfilled;
1722
+ returnedItemCount += returned;
1670
1723
  }
1671
1724
  const isFullyReturned = itemCount > 0 ? returnedItemCount >= itemCount : returnedItemCount > 0;
1672
1725
  const hasPartialReturns = returnedItemCount > 0 && !isFullyReturned;
@@ -1676,7 +1729,8 @@ function buildOrderContext(order, extras) {
1676
1729
  balance,
1677
1730
  paymentMethods,
1678
1731
  refunds,
1679
- lineItems
1732
+ lineItems,
1733
+ reservations
1680
1734
  },
1681
1735
  computed: {
1682
1736
  balance,
@@ -2756,14 +2810,364 @@ var ORDER_STATE_EVENTS = {
2756
2810
  configUpdated: "config-updated"
2757
2811
  };
2758
2812
 
2759
- // constants/adyen.constants.ts
2760
- var ADYEN_ROLLOUT_COUNTRY_CODES = /* @__PURE__ */ new Set(["CA"]);
2761
- function isAdyenRolloutCountry(alpha2) {
2762
- return ADYEN_ROLLOUT_COUNTRY_CODES.has(alpha2);
2813
+ // booking/types.ts
2814
+ var BookingRulesError = class extends Error {
2815
+ constructor(message) {
2816
+ super(message);
2817
+ this.name = "BookingRulesError";
2818
+ }
2819
+ };
2820
+
2821
+ // booking/zoned-time.ts
2822
+ var MINUTE = 6e4;
2823
+ var formatters = /* @__PURE__ */ new Map();
2824
+ var formatterFor = (timeZone) => {
2825
+ const existing = formatters.get(timeZone);
2826
+ if (existing) return existing;
2827
+ let created;
2828
+ try {
2829
+ created = new Intl.DateTimeFormat("en-US", {
2830
+ timeZone,
2831
+ hour12: false,
2832
+ year: "numeric",
2833
+ month: "2-digit",
2834
+ day: "2-digit",
2835
+ hour: "2-digit",
2836
+ minute: "2-digit",
2837
+ second: "2-digit",
2838
+ weekday: "short"
2839
+ });
2840
+ } catch {
2841
+ throw new BookingRulesError(`Booking rules: "${timeZone}" is not a known time zone`);
2842
+ }
2843
+ formatters.set(timeZone, created);
2844
+ return created;
2845
+ };
2846
+ var WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
2847
+ function zonedParts(instant, timeZone) {
2848
+ const parts = {};
2849
+ for (const { type, value } of formatterFor(timeZone).formatToParts(instant)) parts[type] = value;
2850
+ const hour = Number(parts.hour) % 24;
2851
+ return {
2852
+ year: Number(parts.year),
2853
+ month: Number(parts.month),
2854
+ day: Number(parts.day),
2855
+ hour,
2856
+ minute: Number(parts.minute),
2857
+ second: Number(parts.second),
2858
+ weekday: WEEKDAYS.indexOf(parts.weekday)
2859
+ };
2860
+ }
2861
+ function offsetAt(instant, timeZone) {
2862
+ const { year, month, day, hour, minute, second } = zonedParts(instant, timeZone);
2863
+ return Date.UTC(year, month - 1, day, hour, minute, second) - Math.floor(instant.getTime() / 1e3) * 1e3;
2864
+ }
2865
+ function instantFromLocal(year, month, day, minutes, timeZone) {
2866
+ const naive = Date.UTC(year, month - 1, day) + minutes * MINUTE;
2867
+ const firstGuess = naive - offsetAt(new Date(naive), timeZone);
2868
+ return new Date(naive - offsetAt(new Date(firstGuess), timeZone));
2869
+ }
2870
+ function startOfZonedDay(instant, timeZone) {
2871
+ const { year, month, day } = zonedParts(instant, timeZone);
2872
+ return instantFromLocal(year, month, day, 0, timeZone);
2873
+ }
2874
+ function nextZonedDay(dayStart, timeZone) {
2875
+ const { year, month, day } = zonedParts(dayStart, timeZone);
2876
+ return instantFromLocal(year, month, day + 1, 0, timeZone);
2877
+ }
2878
+ function atZonedMinute(dayStart, minutes, timeZone) {
2879
+ const { year, month, day } = zonedParts(dayStart, timeZone);
2880
+ return instantFromLocal(year, month, day, minutes, timeZone);
2881
+ }
2882
+
2883
+ // booking/grid.ts
2884
+ var MINUTE2 = 6e4;
2885
+ var MAX_RANGE_DAYS = 366;
2886
+ var asTime = (value, field) => {
2887
+ if (value == null) return void 0;
2888
+ const time = new Date(value).getTime();
2889
+ if (Number.isNaN(time)) throw new BookingRulesError(`Booking rules: ${field} is not a date, got "${String(value)}"`);
2890
+ return time;
2891
+ };
2892
+ function parseHhMm(value, field) {
2893
+ const [hours, minutes] = value.split(":").map(Number);
2894
+ if (!Number.isInteger(hours) || !Number.isInteger(minutes)) {
2895
+ throw new BookingRulesError(`Booking rules: ${field} must be "HH:mm", got "${value}"`);
2896
+ }
2897
+ return hours * 60 + minutes;
2898
+ }
2899
+ function windowEndsAt(rule, now) {
2900
+ const { maxAdvanceDays, bookableTo } = rule.window;
2901
+ const ends = Math.min(
2902
+ maxAdvanceDays ? now.getTime() + maxAdvanceDays * 1440 * MINUTE2 : Number.POSITIVE_INFINITY,
2903
+ asTime(bookableTo, "window.bookableTo") ?? Number.POSITIVE_INFINITY
2904
+ );
2905
+ return Number.isFinite(ends) ? new Date(ends) : null;
2906
+ }
2907
+ function periodSlot(dayStart, timeZone, rule, openMinute, closeMinute) {
2908
+ const { ratePeriod, slot, checkInTime, checkOutTime } = rule;
2909
+ const at = (minutes) => atZonedMinute(dayStart, minutes, timeZone);
2910
+ if (ratePeriod === "night" /* NIGHT */) {
2911
+ if (!checkInTime || !checkOutTime) {
2912
+ throw new BookingRulesError("Overnight booking rules need both checkInTime and checkOutTime");
2913
+ }
2914
+ const checkIn = parseHhMm(checkInTime, "checkInTime");
2915
+ const checkOut = parseHhMm(checkOutTime, "checkOutTime");
2916
+ return {
2917
+ startAt: at(checkIn),
2918
+ endAt: at(1440 + checkOut),
2919
+ // Housekeeping sits between check-out and the next check-in: it costs no night.
2920
+ bufferEndAt: at(1440 + checkIn),
2921
+ canStart: true
2922
+ };
2923
+ }
2924
+ const from = ratePeriod === "day" /* DAY */ ? openMinute ?? 0 : 0;
2925
+ const to = ratePeriod === "day" /* DAY */ ? closeMinute ?? 1440 : 1440;
2926
+ return {
2927
+ startAt: at(from),
2928
+ endAt: at(to),
2929
+ bufferEndAt: at(to + (slot.bufferMinutes ?? 0)),
2930
+ canStart: true
2931
+ };
2932
+ }
2933
+ function freeGaps(openAt, closeAt, busy) {
2934
+ const inside = busy.map(({ startAt, bufferEndAt }) => ({ from: +startAt, to: +bufferEndAt })).filter(({ from, to }) => to > openAt && from < closeAt).sort((first, second) => first.from - second.from);
2935
+ const gaps = [];
2936
+ let cursor = openAt;
2937
+ for (const { from, to } of inside) {
2938
+ if (from > cursor) gaps.push({ from: cursor, to: from, endsAtBooking: true });
2939
+ cursor = Math.max(cursor, to);
2940
+ if (cursor >= closeAt) break;
2941
+ }
2942
+ if (cursor < closeAt) gaps.push({ from: cursor, to: closeAt, endsAtBooking: false });
2943
+ return gaps;
2944
+ }
2945
+ function buildGrid(rule, timeZone, from, to, now, busy = []) {
2946
+ if (to.getTime() - from.getTime() > MAX_RANGE_DAYS * 1440 * MINUTE2) {
2947
+ throw new BookingRulesError(`A booking range may span at most ${MAX_RANGE_DAYS} days`);
2948
+ }
2949
+ const { availability, slot, window, ratePeriod } = rule;
2950
+ const periodMode = ratePeriod !== "slot" /* SLOT */;
2951
+ const weeklyHours = availability.weeklyHours ?? [];
2952
+ const notBefore = Math.max(
2953
+ now.getTime() + (window.minNoticeMinutes ?? 0) * MINUTE2,
2954
+ asTime(window.bookableFrom, "window.bookableFrom") ?? 0,
2955
+ asTime(availability.availableFrom, "availability.availableFrom") ?? 0
2956
+ );
2957
+ const notAfter = windowEndsAt(rule, now)?.getTime() ?? Number.POSITIVE_INFINITY;
2958
+ const blocked = (availability.offMarket ?? []).map(({ from: blockFrom, to: blockTo }) => ({
2959
+ from: asTime(blockFrom, "availability.offMarket.from"),
2960
+ to: asTime(blockTo, "availability.offMarket.to")
2961
+ }));
2962
+ const arrivalDays = availability.arrivalDays;
2963
+ const step = slot.lengthMinutes + (slot.bufferMinutes ?? 0);
2964
+ const slots = [];
2965
+ const lastDay = startOfZonedDay(to, timeZone).getTime();
2966
+ let dayStart = startOfZonedDay(from, timeZone);
2967
+ while (dayStart.getTime() <= lastDay) {
2968
+ const weekday = zonedParts(dayStart, timeZone).weekday;
2969
+ const rows = weeklyHours.filter((entry) => entry.weekday === weekday);
2970
+ if (periodMode) {
2971
+ const openToday = weeklyHours.length === 0 || rows.length > 0;
2972
+ if (openToday) {
2973
+ const built = periodSlot(dayStart, timeZone, rule, rows[0]?.openMinute, rows[0]?.closeMinute);
2974
+ slots.push({ ...built, canStart: !arrivalDays?.length || arrivalDays.includes(weekday) });
2975
+ }
2976
+ } else {
2977
+ for (const row of rows) {
2978
+ const openAt = +atZonedMinute(dayStart, row.openMinute, timeZone);
2979
+ const closeAt = +atZonedMinute(dayStart, row.closeMinute, timeZone);
2980
+ for (const gap of freeGaps(openAt, closeAt, busy)) {
2981
+ const mustFit = (gap.endsAtBooking ? step : slot.lengthMinutes) * MINUTE2;
2982
+ for (let start = gap.from; start + mustFit <= gap.to; start += step * MINUTE2) {
2983
+ slots.push({
2984
+ startAt: new Date(start),
2985
+ endAt: new Date(start + slot.lengthMinutes * MINUTE2),
2986
+ bufferEndAt: new Date(start + step * MINUTE2),
2987
+ canStart: true
2988
+ });
2989
+ }
2990
+ }
2991
+ }
2992
+ }
2993
+ dayStart = nextZonedDay(dayStart, timeZone);
2994
+ }
2995
+ const askedFrom = from.getTime();
2996
+ const askedTo = to.getTime();
2997
+ return slots.filter(({ startAt, bufferEndAt }) => {
2998
+ const start = startAt.getTime();
2999
+ if (start < askedFrom || start >= askedTo) return false;
3000
+ if (start < notBefore || start > notAfter) return false;
3001
+ return !blocked.some(({ from: blockFrom, to: blockTo }) => start < blockTo && bufferEndAt.getTime() > blockFrom);
3002
+ }).sort((first, second) => +first.startAt - +second.startAt);
3003
+ }
3004
+ function slotsForWindow(grid, startAt, endAt) {
3005
+ const startIndex = grid.findIndex((entry) => entry.startAt.getTime() === startAt.getTime());
3006
+ if (startIndex < 0) {
3007
+ throw new BookingRulesError(`${startAt.toISOString()} is not a bookable slot start for these rules`);
3008
+ }
3009
+ if (!grid[startIndex].canStart) {
3010
+ throw new BookingRulesError(`${startAt.toISOString()} is not an arrival day for these rules`);
3011
+ }
3012
+ const covered = [];
3013
+ for (let index = startIndex; index < grid.length; index++) {
3014
+ const entry = grid[index];
3015
+ const previous = covered[covered.length - 1];
3016
+ if (previous && entry.startAt.getTime() !== previous.bufferEndAt.getTime()) break;
3017
+ covered.push(entry);
3018
+ if (entry.endAt.getTime() === endAt.getTime()) return covered;
3019
+ if (entry.endAt.getTime() > endAt.getTime()) break;
3020
+ }
3021
+ throw new BookingRulesError(
3022
+ `${startAt.toISOString()} \u2192 ${endAt.toISOString()} does not line up with the bookable grid`
3023
+ );
3024
+ }
3025
+
3026
+ // booking/availability.ts
3027
+ var withinPresence = (slot, timeZone, presence) => {
3028
+ if (!presence?.length) return true;
3029
+ const dayStart = startOfZonedDay(slot.startAt, timeZone);
3030
+ const weekday = zonedParts(dayStart, timeZone).weekday;
3031
+ return presence.some(
3032
+ (shift) => shift.weekday === weekday && slot.startAt.getTime() >= atZonedMinute(dayStart, shift.openMinute, timeZone).getTime() && slot.endAt.getTime() <= atZonedMinute(dayStart, shift.closeMinute, timeZone).getTime()
3033
+ );
3034
+ };
3035
+ var overlaps = (entry, startAt, bufferEndAt) => entry.startAt.getTime() < bufferEndAt.getTime() && entry.bufferEndAt.getTime() > startAt.getTime();
3036
+ function occupancyIn(occupied, productId, startAt, bufferEndAt, shared) {
3037
+ const clashing = occupied.filter((entry) => overlaps(entry, startAt, bufferEndAt));
3038
+ const foreign = shared ? void 0 : clashing.find((entry) => entry.productId !== productId);
3039
+ return {
3040
+ taken: clashing.filter((entry) => shared || entry.productId === productId).reduce((sum, entry) => sum + entry.qty, 0),
3041
+ foreignProductId: foreign?.productId
3042
+ };
3043
+ }
3044
+ var stateFor = (hasShift, offerable, free) => {
3045
+ if (offerable === 0) {
3046
+ return hasShift() ? "fullyBooked" /* FULLY_BOOKED */ : "noHoursInRange" /* NO_HOURS_IN_RANGE */;
3047
+ }
3048
+ return free === 0 ? "fullyBooked" /* FULLY_BOOKED */ : "available" /* AVAILABLE */;
3049
+ };
3050
+ function buildAvailability({
3051
+ productId,
3052
+ bookingRulesId,
3053
+ bookingType,
3054
+ rule,
3055
+ resources,
3056
+ occupied,
3057
+ from,
3058
+ to,
3059
+ now
3060
+ }) {
3061
+ const shared = rule.slot.sharedAcrossProducts ?? false;
3062
+ const bookableUntil = windowEndsAt(rule, now);
3063
+ const header = {
3064
+ productId,
3065
+ bookingRulesId,
3066
+ bookingType,
3067
+ ratePeriod: rule.ratePeriod,
3068
+ // Passed through for the UI to label: a group slot under minimumToRun may not run.
3069
+ minimumToRun: rule.slot.minimumToRun,
3070
+ minStay: rule.slot.minStay,
3071
+ maxStay: rule.slot.maxStay,
3072
+ // Why a day can come back empty. Past this instant the rule refuses everything, so the UI
3073
+ // can say "beyond the booking window" where it would otherwise claim "nothing free".
3074
+ bookableUntil,
3075
+ // The comparison itself, not just the boundary: deciding whether a day lies past the
3076
+ // window is a rule, and a UI that re-derives it owns a copy of that rule — which is how
3077
+ // the same empty grid ends up explained one way on the till and another in a flow.
3078
+ beyondWindow: !!bookableUntil && from.getTime() > bookableUntil.getTime()
3079
+ };
3080
+ const byResource = /* @__PURE__ */ new Map();
3081
+ for (const entry of occupied) {
3082
+ byResource.set(entry.resourceId, [...byResource.get(entry.resourceId) ?? [], entry]);
3083
+ }
3084
+ const grids = resources.map((resource) => {
3085
+ const mine = byResource.get(resource.id) ?? [];
3086
+ const busy = (resource.seats ?? 1) === 1 ? mine : [];
3087
+ const slots = buildGrid(rule, resource.timeZone, from, to, now, busy).filter(
3088
+ (slot) => withinPresence(slot, resource.timeZone, resource.presence)
3089
+ );
3090
+ return { resource, slots };
3091
+ });
3092
+ const byInstant = /* @__PURE__ */ new Map();
3093
+ const freeSlotsPerResource = /* @__PURE__ */ new Map();
3094
+ for (const { resource, slots } of grids) {
3095
+ const capacity = resource.seats ?? 1;
3096
+ const mine = byResource.get(resource.id) ?? [];
3097
+ for (const slot of slots) {
3098
+ const { taken, foreignProductId } = occupancyIn(mine, productId, slot.startAt, slot.bufferEndAt, shared);
3099
+ const free = foreignProductId ? 0 : Math.max(capacity - taken, 0);
3100
+ const entry = byInstant.get(+slot.startAt) ?? { ...slot, capacity: 0, booked: 0, free: 0, resources: [] };
3101
+ entry.capacity += capacity;
3102
+ entry.booked += foreignProductId ? capacity : taken;
3103
+ entry.free += free;
3104
+ entry.canStart = entry.canStart || slot.canStart;
3105
+ entry.resources.push({ resourceId: resource.id, name: resource.name, capacity, booked: taken, free });
3106
+ if (free > 0) freeSlotsPerResource.set(resource.id, (freeSlotsPerResource.get(resource.id) ?? 0) + 1);
3107
+ byInstant.set(+slot.startAt, entry);
3108
+ }
3109
+ }
3110
+ const inWindow = grids.map(({ resource, slots }) => {
3111
+ const free = freeSlotsPerResource.get(resource.id) ?? 0;
3112
+ return {
3113
+ resourceId: resource.id,
3114
+ name: resource.name,
3115
+ slots: slots.length,
3116
+ free,
3117
+ // Lazily: the shift grid is a second walk, and only an empty answer needs it.
3118
+ state: stateFor(
3119
+ () => buildGrid(rule, resource.timeZone, from, to, now).filter(
3120
+ (slot) => withinPresence(slot, resource.timeZone, resource.presence)
3121
+ ).length > 0,
3122
+ slots.length,
3123
+ free
3124
+ )
3125
+ };
3126
+ });
3127
+ return {
3128
+ ...header,
3129
+ resources: inWindow,
3130
+ slots: [...byInstant.values()].sort((first, second) => +first.startAt - +second.startAt)
3131
+ };
3132
+ }
3133
+ function presentAt(resources, rows, outletId) {
3134
+ const byResource = /* @__PURE__ */ new Map();
3135
+ for (const row of rows) {
3136
+ byResource.set(row.resourceId, [...byResource.get(row.resourceId) ?? [], row]);
3137
+ }
3138
+ return resources.flatMap((resource) => {
3139
+ const own = byResource.get(resource.id) ?? [];
3140
+ if (own.length === 0 || !outletId) return [{ resource }];
3141
+ const here = own.find((row) => row.outletId === outletId);
3142
+ return here ? [{ resource, presence: here.weeklyHours }] : [];
3143
+ });
3144
+ }
3145
+
3146
+ // booking/plan.ts
3147
+ var DEFAULT_HOLD_MINUTES = 10;
3148
+ function planWindow({ rule, timeZone, presence, startAt, endAt, now, busy = [] }) {
3149
+ const grid = buildGrid(rule, timeZone, startAt, endAt, now, busy).filter(
3150
+ (slot) => withinPresence(slot, timeZone, presence)
3151
+ );
3152
+ const slots = slotsForWindow(grid, startAt, endAt);
3153
+ const { minStay, maxStay } = rule.slot;
3154
+ if (minStay && slots.length < minStay) {
3155
+ throw new BookingRulesError(`These rules ask for at least ${minStay} period(s), got ${slots.length}`);
3156
+ }
3157
+ if (maxStay && slots.length > maxStay) {
3158
+ throw new BookingRulesError(`These rules allow at most ${maxStay} period(s), got ${slots.length}`);
3159
+ }
3160
+ const last = slots[slots.length - 1];
3161
+ return { slots, endAt: last.endAt, bufferEndAt: last.bufferEndAt };
2763
3162
  }
3163
+ var holdExpiresAt = (rule, now) => new Date(now.getTime() + (rule.window.holdMinutes ?? DEFAULT_HOLD_MINUTES) * 6e4);
2764
3164
  export {
2765
- ADYEN_ROLLOUT_COUNTRY_CODES,
2766
3165
  AttributeType,
3166
+ BookingRatePeriod,
3167
+ BookingResourceKind,
3168
+ BookingResourceStatus,
3169
+ BookingRulesError,
3170
+ BookingType,
2767
3171
  CONDITION_OPERATORS,
2768
3172
  COUNTRY_CURRENCIES,
2769
3173
  CURRENCY_CONFIG,
@@ -2774,6 +3178,7 @@ export {
2774
3178
  DEFAULT_CONFIG,
2775
3179
  DEFAULT_CROSS_AXIS_RULES,
2776
3180
  DEFAULT_DISPLAY_STATE_MAP,
3181
+ DEFAULT_HOLD_MINUTES,
2777
3182
  DEFAULT_RESOLVED_CONFIG,
2778
3183
  DEFAULT_UNIT_ID,
2779
3184
  DualWriteStrategy,
@@ -2817,8 +3222,10 @@ export {
2817
3222
  REPORT_NEGATE_PATHS,
2818
3223
  ReceiptSendChannel,
2819
3224
  ReceiptType,
3225
+ ReservationStatus,
2820
3226
  ResidualPaymentMethod,
2821
3227
  ResidualStatus,
3228
+ ResourceAvailabilityState,
2822
3229
  STATE_SCHEMA,
2823
3230
  STRIPE_SUPPORTED_COUNTRIES,
2824
3231
  SYSTEM_FAMILIES,
@@ -2839,6 +3246,8 @@ export {
2839
3246
  baseFulfillmentDefinitions,
2840
3247
  baseOf,
2841
3248
  basePaymentDefinitions,
3249
+ buildAvailability,
3250
+ buildGrid,
2842
3251
  buildOrderContext,
2843
3252
  buildPosPresetConfig,
2844
3253
  buildResolvedConfig,
@@ -2878,8 +3287,8 @@ export {
2878
3287
  getPricingRegionFromCountry,
2879
3288
  getSettlementTotal,
2880
3289
  getTotalCollected,
3290
+ holdExpiresAt,
2881
3291
  inferStatesFromLegacyStatus,
2882
- isAdyenRolloutCountry,
2883
3292
  isConditionOperator,
2884
3293
  isFulfillmentState,
2885
3294
  isPaymentState,
@@ -2892,9 +3301,12 @@ export {
2892
3301
  mergeFragment,
2893
3302
  multiplyAmount,
2894
3303
  normalizeMongoIdString,
3304
+ occupancyIn,
2895
3305
  parseAmountToMinorUnits,
2896
3306
  parseCurrencyCode,
2897
3307
  parseStripeCurrencyCode,
3308
+ planWindow,
3309
+ presentAt,
2898
3310
  removeFragment,
2899
3311
  resolveFamily,
2900
3312
  resolveTemplate,
@@ -2903,12 +3315,15 @@ export {
2903
3315
  sameFamily,
2904
3316
  setOrderState,
2905
3317
  signReportForDisplay,
3318
+ slotsForWindow,
2906
3319
  stringifyMinorUnits,
2907
3320
  subtractAmounts,
2908
3321
  toBase,
2909
3322
  toMinorUnits,
2910
3323
  validateConfig,
2911
3324
  validateFragmentCompatibility,
2912
- violatesFinancialInvariant
3325
+ violatesFinancialInvariant,
3326
+ windowEndsAt,
3327
+ withinPresence
2913
3328
  };
2914
3329
  //# sourceMappingURL=index.mjs.map