@final-commerce/common 2.3.0 → 2.4.0-preprod.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.d.mts +201 -2
- package/dist/index.d.ts +201 -2
- package/dist/index.js +474 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +454 -9
- package/dist/index.mjs.map +1 -1
- package/dist/pos-types/index.d.mts +147 -18
- package/dist/pos-types/index.d.ts +147 -18
- package/package.json +1 -1
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
|
|
1668
|
-
const
|
|
1669
|
-
itemCount +=
|
|
1670
|
-
|
|
1671
|
-
|
|
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,378 @@ 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 shopDayKey(instant, timeZone) {
|
|
3055
|
+
const { year, month, day } = zonedParts(instant, timeZone);
|
|
3056
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
3057
|
+
return `${year}-${pad(month)}-${pad(day)}`;
|
|
3058
|
+
}
|
|
3059
|
+
function shopDayRange(fromDayKey, count, timeZone) {
|
|
3060
|
+
const [year, month, day] = fromDayKey.split("-").map(Number);
|
|
3061
|
+
return {
|
|
3062
|
+
from: instantFromLocal(year, month, day, 0, timeZone),
|
|
3063
|
+
to: instantFromLocal(year, month, day + Math.max(1, count), 0, timeZone)
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
function buildAvailability({
|
|
3067
|
+
productId,
|
|
3068
|
+
bookingRulesId,
|
|
3069
|
+
bookingType,
|
|
3070
|
+
rule,
|
|
3071
|
+
resources,
|
|
3072
|
+
occupied,
|
|
3073
|
+
from,
|
|
3074
|
+
to,
|
|
3075
|
+
now
|
|
3076
|
+
}) {
|
|
3077
|
+
const shared = rule.slot.sharedAcrossProducts ?? false;
|
|
3078
|
+
const bookableUntil = windowEndsAt(rule, now);
|
|
3079
|
+
const header = {
|
|
3080
|
+
productId,
|
|
3081
|
+
bookingRulesId,
|
|
3082
|
+
bookingType,
|
|
3083
|
+
ratePeriod: rule.ratePeriod,
|
|
3084
|
+
// Passed through for the UI to label: a group slot under minimumToRun may not run.
|
|
3085
|
+
minimumToRun: rule.slot.minimumToRun,
|
|
3086
|
+
minStay: rule.slot.minStay,
|
|
3087
|
+
maxStay: rule.slot.maxStay,
|
|
3088
|
+
// Why a day can come back empty. Past this instant the rule refuses everything, so the UI
|
|
3089
|
+
// can say "beyond the booking window" where it would otherwise claim "nothing free".
|
|
3090
|
+
bookableUntil,
|
|
3091
|
+
// The comparison itself, not just the boundary: deciding whether a day lies past the
|
|
3092
|
+
// window is a rule, and a UI that re-derives it owns a copy of that rule — which is how
|
|
3093
|
+
// the same empty grid ends up explained one way on the till and another in a flow.
|
|
3094
|
+
beyondWindow: !!bookableUntil && from.getTime() > bookableUntil.getTime()
|
|
3095
|
+
};
|
|
3096
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
3097
|
+
for (const entry of occupied) {
|
|
3098
|
+
byResource.set(entry.resourceId, [...byResource.get(entry.resourceId) ?? [], entry]);
|
|
3099
|
+
}
|
|
3100
|
+
const grids = resources.map((resource) => {
|
|
3101
|
+
const mine = byResource.get(resource.id) ?? [];
|
|
3102
|
+
const busy = (resource.seats ?? 1) === 1 ? mine : [];
|
|
3103
|
+
const slots2 = buildGrid(rule, resource.timeZone, from, to, now, busy).filter(
|
|
3104
|
+
(slot) => withinPresence(slot, resource.timeZone, resource.presence)
|
|
3105
|
+
);
|
|
3106
|
+
return { resource, slots: slots2 };
|
|
3107
|
+
});
|
|
3108
|
+
const byInstant = /* @__PURE__ */ new Map();
|
|
3109
|
+
const freeSlotsPerResource = /* @__PURE__ */ new Map();
|
|
3110
|
+
for (const { resource, slots: slots2 } of grids) {
|
|
3111
|
+
const capacity = resource.seats ?? 1;
|
|
3112
|
+
const mine = byResource.get(resource.id) ?? [];
|
|
3113
|
+
for (const slot of slots2) {
|
|
3114
|
+
const { taken, foreignProductId } = occupancyIn(mine, productId, slot.startAt, slot.bufferEndAt, shared);
|
|
3115
|
+
const free = foreignProductId ? 0 : Math.max(capacity - taken, 0);
|
|
3116
|
+
const entry = byInstant.get(+slot.startAt) ?? { ...slot, capacity: 0, booked: 0, free: 0, resources: [], dayKey: shopDayKey(slot.startAt, resource.timeZone) };
|
|
3117
|
+
entry.capacity += capacity;
|
|
3118
|
+
entry.booked += foreignProductId ? capacity : taken;
|
|
3119
|
+
entry.free += free;
|
|
3120
|
+
entry.canStart = entry.canStart || slot.canStart;
|
|
3121
|
+
entry.resources.push({ resourceId: resource.id, name: resource.name, capacity, booked: taken, free });
|
|
3122
|
+
if (free > 0) freeSlotsPerResource.set(resource.id, (freeSlotsPerResource.get(resource.id) ?? 0) + 1);
|
|
3123
|
+
byInstant.set(+slot.startAt, entry);
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
const inWindow = grids.map(({ resource, slots: slots2 }) => {
|
|
3127
|
+
const free = freeSlotsPerResource.get(resource.id) ?? 0;
|
|
3128
|
+
return {
|
|
3129
|
+
resourceId: resource.id,
|
|
3130
|
+
name: resource.name,
|
|
3131
|
+
slots: slots2.length,
|
|
3132
|
+
free,
|
|
3133
|
+
// Lazily: the shift grid is a second walk, and only an empty answer needs it.
|
|
3134
|
+
state: stateFor(
|
|
3135
|
+
() => buildGrid(rule, resource.timeZone, from, to, now).filter(
|
|
3136
|
+
(slot) => withinPresence(slot, resource.timeZone, resource.presence)
|
|
3137
|
+
).length > 0,
|
|
3138
|
+
slots2.length,
|
|
3139
|
+
free
|
|
3140
|
+
)
|
|
3141
|
+
};
|
|
3142
|
+
});
|
|
3143
|
+
const slots = [...byInstant.values()].sort((first, second) => +first.startAt - +second.startAt);
|
|
3144
|
+
return {
|
|
3145
|
+
...header,
|
|
3146
|
+
resources: inWindow,
|
|
3147
|
+
slots,
|
|
3148
|
+
/**
|
|
3149
|
+
* The clock every instant above reads on. Returned rather than assumed: without it a caller
|
|
3150
|
+
* has to fetch the zone from somewhere else and hope it is the same one the grid used.
|
|
3151
|
+
*/
|
|
3152
|
+
timeZone: resources[0]?.timeZone ?? null,
|
|
3153
|
+
/** The shop days this answer covers, in order — what a day picker lists. */
|
|
3154
|
+
days: [...new Set(slots.map((slot) => slot.dayKey))]
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
3157
|
+
function presentAt(resources, rows, outletId) {
|
|
3158
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
3159
|
+
for (const row of rows) {
|
|
3160
|
+
byResource.set(row.resourceId, [...byResource.get(row.resourceId) ?? [], row]);
|
|
3161
|
+
}
|
|
3162
|
+
return resources.flatMap((resource) => {
|
|
3163
|
+
const own = byResource.get(resource.id) ?? [];
|
|
3164
|
+
if (own.length === 0 || !outletId) return [{ resource }];
|
|
3165
|
+
const here = own.find((row) => row.outletId === outletId);
|
|
3166
|
+
return here ? [{ resource, presence: here.weeklyHours }] : [];
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
// booking/plan.ts
|
|
3171
|
+
var DEFAULT_HOLD_MINUTES = 10;
|
|
3172
|
+
function planWindow({ rule, timeZone, presence, startAt, endAt, now, busy = [] }) {
|
|
3173
|
+
const grid = buildGrid(rule, timeZone, startAt, endAt, now, busy).filter(
|
|
3174
|
+
(slot) => withinPresence(slot, timeZone, presence)
|
|
3175
|
+
);
|
|
3176
|
+
const slots = slotsForWindow(grid, startAt, endAt);
|
|
3177
|
+
const { minStay, maxStay } = rule.slot;
|
|
3178
|
+
if (minStay && slots.length < minStay) {
|
|
3179
|
+
throw new BookingRulesError(`These rules ask for at least ${minStay} period(s), got ${slots.length}`);
|
|
3180
|
+
}
|
|
3181
|
+
if (maxStay && slots.length > maxStay) {
|
|
3182
|
+
throw new BookingRulesError(`These rules allow at most ${maxStay} period(s), got ${slots.length}`);
|
|
3183
|
+
}
|
|
3184
|
+
const last = slots[slots.length - 1];
|
|
3185
|
+
return { slots, endAt: last.endAt, bufferEndAt: last.bufferEndAt };
|
|
3186
|
+
}
|
|
3187
|
+
var holdExpiresAt = (rule, now) => new Date(now.getTime() + (rule.window.holdMinutes ?? DEFAULT_HOLD_MINUTES) * 6e4);
|
|
3188
|
+
|
|
2763
3189
|
// constants/adyen.constants.ts
|
|
2764
3190
|
var ADYEN_ROLLOUT_COUNTRY_CODES = /* @__PURE__ */ new Set(["CA"]);
|
|
2765
3191
|
function isAdyenRolloutCountry(alpha2) {
|
|
@@ -2768,6 +3194,11 @@ function isAdyenRolloutCountry(alpha2) {
|
|
|
2768
3194
|
export {
|
|
2769
3195
|
ADYEN_ROLLOUT_COUNTRY_CODES,
|
|
2770
3196
|
AttributeType,
|
|
3197
|
+
BookingRatePeriod,
|
|
3198
|
+
BookingResourceKind,
|
|
3199
|
+
BookingResourceStatus,
|
|
3200
|
+
BookingRulesError,
|
|
3201
|
+
BookingType,
|
|
2771
3202
|
CONDITION_OPERATORS,
|
|
2772
3203
|
COUNTRY_CURRENCIES,
|
|
2773
3204
|
CURRENCY_CONFIG,
|
|
@@ -2778,6 +3209,7 @@ export {
|
|
|
2778
3209
|
DEFAULT_CONFIG,
|
|
2779
3210
|
DEFAULT_CROSS_AXIS_RULES,
|
|
2780
3211
|
DEFAULT_DISPLAY_STATE_MAP,
|
|
3212
|
+
DEFAULT_HOLD_MINUTES,
|
|
2781
3213
|
DEFAULT_RESOLVED_CONFIG,
|
|
2782
3214
|
DEFAULT_UNIT_ID,
|
|
2783
3215
|
DualWriteStrategy,
|
|
@@ -2821,8 +3253,10 @@ export {
|
|
|
2821
3253
|
REPORT_NEGATE_PATHS,
|
|
2822
3254
|
ReceiptSendChannel,
|
|
2823
3255
|
ReceiptType,
|
|
3256
|
+
ReservationStatus,
|
|
2824
3257
|
ResidualPaymentMethod,
|
|
2825
3258
|
ResidualStatus,
|
|
3259
|
+
ResourceAvailabilityState,
|
|
2826
3260
|
STATE_SCHEMA,
|
|
2827
3261
|
STRIPE_SUPPORTED_COUNTRIES,
|
|
2828
3262
|
SYSTEM_FAMILIES,
|
|
@@ -2843,6 +3277,8 @@ export {
|
|
|
2843
3277
|
baseFulfillmentDefinitions,
|
|
2844
3278
|
baseOf,
|
|
2845
3279
|
basePaymentDefinitions,
|
|
3280
|
+
buildAvailability,
|
|
3281
|
+
buildGrid,
|
|
2846
3282
|
buildOrderContext,
|
|
2847
3283
|
buildPosPresetConfig,
|
|
2848
3284
|
buildResolvedConfig,
|
|
@@ -2882,6 +3318,7 @@ export {
|
|
|
2882
3318
|
getPricingRegionFromCountry,
|
|
2883
3319
|
getSettlementTotal,
|
|
2884
3320
|
getTotalCollected,
|
|
3321
|
+
holdExpiresAt,
|
|
2885
3322
|
inferStatesFromLegacyStatus,
|
|
2886
3323
|
isAdyenRolloutCountry,
|
|
2887
3324
|
isConditionOperator,
|
|
@@ -2896,9 +3333,12 @@ export {
|
|
|
2896
3333
|
mergeFragment,
|
|
2897
3334
|
multiplyAmount,
|
|
2898
3335
|
normalizeMongoIdString,
|
|
3336
|
+
occupancyIn,
|
|
2899
3337
|
parseAmountToMinorUnits,
|
|
2900
3338
|
parseCurrencyCode,
|
|
2901
3339
|
parseStripeCurrencyCode,
|
|
3340
|
+
planWindow,
|
|
3341
|
+
presentAt,
|
|
2902
3342
|
removeFragment,
|
|
2903
3343
|
resolveFamily,
|
|
2904
3344
|
resolveTemplate,
|
|
@@ -2906,13 +3346,18 @@ export {
|
|
|
2906
3346
|
roundToPrecision,
|
|
2907
3347
|
sameFamily,
|
|
2908
3348
|
setOrderState,
|
|
3349
|
+
shopDayKey,
|
|
3350
|
+
shopDayRange,
|
|
2909
3351
|
signReportForDisplay,
|
|
3352
|
+
slotsForWindow,
|
|
2910
3353
|
stringifyMinorUnits,
|
|
2911
3354
|
subtractAmounts,
|
|
2912
3355
|
toBase,
|
|
2913
3356
|
toMinorUnits,
|
|
2914
3357
|
validateConfig,
|
|
2915
3358
|
validateFragmentCompatibility,
|
|
2916
|
-
violatesFinancialInvariant
|
|
3359
|
+
violatesFinancialInvariant,
|
|
3360
|
+
windowEndsAt,
|
|
3361
|
+
withinPresence
|
|
2917
3362
|
};
|
|
2918
3363
|
//# sourceMappingURL=index.mjs.map
|