@final-commerce/common 2.2.3-staging3.1 → 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.d.mts +193 -2
- package/dist/index.d.ts +193 -2
- package/dist/index.js +450 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +432 -9
- package/dist/index.mjs.map +1 -1
- package/dist/pos-types/index.d.mts +120 -87
- package/dist/pos-types/index.d.ts +120 -87
- package/dist/pos-types/index.js.map +1 -1
- package/dist/pos-types/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
1664
|
-
const
|
|
1665
|
-
itemCount +=
|
|
1666
|
-
|
|
1667
|
-
|
|
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,
|
|
@@ -2755,8 +2809,365 @@ var ORDER_STATE_EVENTS = {
|
|
|
2755
2809
|
stateTransitionBlocked: "state-transition-blocked",
|
|
2756
2810
|
configUpdated: "config-updated"
|
|
2757
2811
|
};
|
|
2812
|
+
|
|
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 };
|
|
3162
|
+
}
|
|
3163
|
+
var holdExpiresAt = (rule, now) => new Date(now.getTime() + (rule.window.holdMinutes ?? DEFAULT_HOLD_MINUTES) * 6e4);
|
|
2758
3164
|
export {
|
|
2759
3165
|
AttributeType,
|
|
3166
|
+
BookingRatePeriod,
|
|
3167
|
+
BookingResourceKind,
|
|
3168
|
+
BookingResourceStatus,
|
|
3169
|
+
BookingRulesError,
|
|
3170
|
+
BookingType,
|
|
2760
3171
|
CONDITION_OPERATORS,
|
|
2761
3172
|
COUNTRY_CURRENCIES,
|
|
2762
3173
|
CURRENCY_CONFIG,
|
|
@@ -2767,6 +3178,7 @@ export {
|
|
|
2767
3178
|
DEFAULT_CONFIG,
|
|
2768
3179
|
DEFAULT_CROSS_AXIS_RULES,
|
|
2769
3180
|
DEFAULT_DISPLAY_STATE_MAP,
|
|
3181
|
+
DEFAULT_HOLD_MINUTES,
|
|
2770
3182
|
DEFAULT_RESOLVED_CONFIG,
|
|
2771
3183
|
DEFAULT_UNIT_ID,
|
|
2772
3184
|
DualWriteStrategy,
|
|
@@ -2810,8 +3222,10 @@ export {
|
|
|
2810
3222
|
REPORT_NEGATE_PATHS,
|
|
2811
3223
|
ReceiptSendChannel,
|
|
2812
3224
|
ReceiptType,
|
|
3225
|
+
ReservationStatus,
|
|
2813
3226
|
ResidualPaymentMethod,
|
|
2814
3227
|
ResidualStatus,
|
|
3228
|
+
ResourceAvailabilityState,
|
|
2815
3229
|
STATE_SCHEMA,
|
|
2816
3230
|
STRIPE_SUPPORTED_COUNTRIES,
|
|
2817
3231
|
SYSTEM_FAMILIES,
|
|
@@ -2832,6 +3246,8 @@ export {
|
|
|
2832
3246
|
baseFulfillmentDefinitions,
|
|
2833
3247
|
baseOf,
|
|
2834
3248
|
basePaymentDefinitions,
|
|
3249
|
+
buildAvailability,
|
|
3250
|
+
buildGrid,
|
|
2835
3251
|
buildOrderContext,
|
|
2836
3252
|
buildPosPresetConfig,
|
|
2837
3253
|
buildResolvedConfig,
|
|
@@ -2871,6 +3287,7 @@ export {
|
|
|
2871
3287
|
getPricingRegionFromCountry,
|
|
2872
3288
|
getSettlementTotal,
|
|
2873
3289
|
getTotalCollected,
|
|
3290
|
+
holdExpiresAt,
|
|
2874
3291
|
inferStatesFromLegacyStatus,
|
|
2875
3292
|
isConditionOperator,
|
|
2876
3293
|
isFulfillmentState,
|
|
@@ -2884,9 +3301,12 @@ export {
|
|
|
2884
3301
|
mergeFragment,
|
|
2885
3302
|
multiplyAmount,
|
|
2886
3303
|
normalizeMongoIdString,
|
|
3304
|
+
occupancyIn,
|
|
2887
3305
|
parseAmountToMinorUnits,
|
|
2888
3306
|
parseCurrencyCode,
|
|
2889
3307
|
parseStripeCurrencyCode,
|
|
3308
|
+
planWindow,
|
|
3309
|
+
presentAt,
|
|
2890
3310
|
removeFragment,
|
|
2891
3311
|
resolveFamily,
|
|
2892
3312
|
resolveTemplate,
|
|
@@ -2895,12 +3315,15 @@ export {
|
|
|
2895
3315
|
sameFamily,
|
|
2896
3316
|
setOrderState,
|
|
2897
3317
|
signReportForDisplay,
|
|
3318
|
+
slotsForWindow,
|
|
2898
3319
|
stringifyMinorUnits,
|
|
2899
3320
|
subtractAmounts,
|
|
2900
3321
|
toBase,
|
|
2901
3322
|
toMinorUnits,
|
|
2902
3323
|
validateConfig,
|
|
2903
3324
|
validateFragmentCompatibility,
|
|
2904
|
-
violatesFinancialInvariant
|
|
3325
|
+
violatesFinancialInvariant,
|
|
3326
|
+
windowEndsAt,
|
|
3327
|
+
withinPresence
|
|
2905
3328
|
};
|
|
2906
3329
|
//# sourceMappingURL=index.mjs.map
|