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