@final-commerce/common 2.4.1-staging2.1 → 2.5.0-preprod.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +203 -20
- package/dist/index.d.ts +203 -20
- package/dist/index.js +481 -53
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +461 -41
- package/dist/index.mjs.map +1 -1
- package/dist/pos-types/index.d.mts +164 -21
- package/dist/pos-types/index.d.ts +164 -21
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -237,6 +237,7 @@ var InventorySpecificActionType = /* @__PURE__ */ ((InventorySpecificActionType2
|
|
|
237
237
|
InventorySpecificActionType2["PO_RECEIVED"] = "PO_RECEIVED";
|
|
238
238
|
InventorySpecificActionType2["EXTERNAL_SALE"] = "EXTERNAL_SALE";
|
|
239
239
|
InventorySpecificActionType2["EXTERNAL_RESTOCK"] = "EXTERNAL_RESTOCK";
|
|
240
|
+
InventorySpecificActionType2["CUSTOM"] = "CUSTOM";
|
|
240
241
|
return InventorySpecificActionType2;
|
|
241
242
|
})(InventorySpecificActionType || {});
|
|
242
243
|
var ExtensionCategory = /* @__PURE__ */ ((ExtensionCategory2) => {
|
|
@@ -422,6 +423,7 @@ var StripeCurrencyCode = /* @__PURE__ */ ((StripeCurrencyCode2) => {
|
|
|
422
423
|
var ProductType = /* @__PURE__ */ ((ProductType2) => {
|
|
423
424
|
ProductType2["SIMPLE"] = "simple";
|
|
424
425
|
ProductType2["VARIABLE"] = "variable";
|
|
426
|
+
ProductType2["BOOKING"] = "booking";
|
|
425
427
|
return ProductType2;
|
|
426
428
|
})(ProductType || {});
|
|
427
429
|
var ProductStatus = /* @__PURE__ */ ((ProductStatus2) => {
|
|
@@ -439,6 +441,50 @@ var PaymentProcessor = /* @__PURE__ */ ((PaymentProcessor2) => {
|
|
|
439
441
|
return PaymentProcessor2;
|
|
440
442
|
})(PaymentProcessor || {});
|
|
441
443
|
|
|
444
|
+
// enums/booking.enum.ts
|
|
445
|
+
var BookingType = /* @__PURE__ */ ((BookingType3) => {
|
|
446
|
+
BookingType3["RENTAL"] = "rental";
|
|
447
|
+
BookingType3["APPOINTMENT"] = "appointment";
|
|
448
|
+
BookingType3["OVERNIGHT"] = "overnight";
|
|
449
|
+
BookingType3["GROUP"] = "group";
|
|
450
|
+
return BookingType3;
|
|
451
|
+
})(BookingType || {});
|
|
452
|
+
var BookingRatePeriod = /* @__PURE__ */ ((BookingRatePeriod2) => {
|
|
453
|
+
BookingRatePeriod2["SLOT"] = "slot";
|
|
454
|
+
BookingRatePeriod2["DAY"] = "day";
|
|
455
|
+
BookingRatePeriod2["NIGHT"] = "night";
|
|
456
|
+
BookingRatePeriod2["WEEK"] = "week";
|
|
457
|
+
BookingRatePeriod2["MONTH"] = "month";
|
|
458
|
+
return BookingRatePeriod2;
|
|
459
|
+
})(BookingRatePeriod || {});
|
|
460
|
+
var BookingResourceKind = /* @__PURE__ */ ((BookingResourceKind2) => {
|
|
461
|
+
BookingResourceKind2["ASSET"] = "asset";
|
|
462
|
+
BookingResourceKind2["ROOM"] = "room";
|
|
463
|
+
BookingResourceKind2["PROPERTY"] = "property";
|
|
464
|
+
BookingResourceKind2["SPACE"] = "space";
|
|
465
|
+
BookingResourceKind2["STAFF"] = "staff";
|
|
466
|
+
BookingResourceKind2["TABLE"] = "table";
|
|
467
|
+
return BookingResourceKind2;
|
|
468
|
+
})(BookingResourceKind || {});
|
|
469
|
+
var ReservationStatus = /* @__PURE__ */ ((ReservationStatus2) => {
|
|
470
|
+
ReservationStatus2["HELD"] = "held";
|
|
471
|
+
ReservationStatus2["CONFIRMED"] = "confirmed";
|
|
472
|
+
ReservationStatus2["CANCELLED"] = "cancelled";
|
|
473
|
+
ReservationStatus2["EXPIRED"] = "expired";
|
|
474
|
+
return ReservationStatus2;
|
|
475
|
+
})(ReservationStatus || {});
|
|
476
|
+
var BookingResourceStatus = /* @__PURE__ */ ((BookingResourceStatus2) => {
|
|
477
|
+
BookingResourceStatus2["ACTIVE"] = "active";
|
|
478
|
+
BookingResourceStatus2["INACTIVE"] = "inactive";
|
|
479
|
+
return BookingResourceStatus2;
|
|
480
|
+
})(BookingResourceStatus || {});
|
|
481
|
+
var ResourceAvailabilityState = /* @__PURE__ */ ((ResourceAvailabilityState2) => {
|
|
482
|
+
ResourceAvailabilityState2["AVAILABLE"] = "available";
|
|
483
|
+
ResourceAvailabilityState2["FULLY_BOOKED"] = "fullyBooked";
|
|
484
|
+
ResourceAvailabilityState2["NO_HOURS_IN_RANGE"] = "noHoursInRange";
|
|
485
|
+
return ResourceAvailabilityState2;
|
|
486
|
+
})(ResourceAvailabilityState || {});
|
|
487
|
+
|
|
442
488
|
// utils/currency.util.ts
|
|
443
489
|
var CURRENCY_CONFIG = {
|
|
444
490
|
// Major currencies with 2 decimal places
|
|
@@ -1650,6 +1696,15 @@ function sumRefundQuantities(refunds) {
|
|
|
1650
1696
|
}
|
|
1651
1697
|
return t;
|
|
1652
1698
|
}
|
|
1699
|
+
var asCount = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
1700
|
+
function countsOf(row) {
|
|
1701
|
+
const quantity = asCount(row.quantity);
|
|
1702
|
+
return {
|
|
1703
|
+
quantity,
|
|
1704
|
+
fulfilled: Math.min(asCount(row.fulfilledQuantity), quantity),
|
|
1705
|
+
returned: Math.min(asCount(row.returnedQuantity), quantity)
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1653
1708
|
function buildOrderContext(order, extras) {
|
|
1654
1709
|
const o = order ?? {};
|
|
1655
1710
|
const total = typeof o.total === "number" && Number.isFinite(o.total) ? o.total : 0;
|
|
@@ -1657,6 +1712,7 @@ function buildOrderContext(order, extras) {
|
|
|
1657
1712
|
const paymentMethods = o.paymentMethods ?? [];
|
|
1658
1713
|
const refunds = o.refunds ?? o.refund ?? [];
|
|
1659
1714
|
const lineItems = o.lineItems ?? [];
|
|
1715
|
+
const reservations = o.reservations ?? [];
|
|
1660
1716
|
const totalPaid = sumNumbers(paymentMethods);
|
|
1661
1717
|
const totalRefunded = sumRefundQuantities(refunds);
|
|
1662
1718
|
const isFullyPaid = total > 0 ? balance <= 0 : totalPaid > 0 && balance <= 0;
|
|
@@ -1664,13 +1720,11 @@ function buildOrderContext(order, extras) {
|
|
|
1664
1720
|
let itemCount = 0;
|
|
1665
1721
|
let fulfilledItemCount = 0;
|
|
1666
1722
|
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);
|
|
1723
|
+
for (const row of [...lineItems, ...reservations]) {
|
|
1724
|
+
const { quantity, fulfilled, returned } = countsOf(row);
|
|
1725
|
+
itemCount += quantity;
|
|
1726
|
+
fulfilledItemCount += fulfilled;
|
|
1727
|
+
returnedItemCount += returned;
|
|
1674
1728
|
}
|
|
1675
1729
|
const isFullyReturned = itemCount > 0 ? returnedItemCount >= itemCount : returnedItemCount > 0;
|
|
1676
1730
|
const hasPartialReturns = returnedItemCount > 0 && !isFullyReturned;
|
|
@@ -1680,7 +1734,8 @@ function buildOrderContext(order, extras) {
|
|
|
1680
1734
|
balance,
|
|
1681
1735
|
paymentMethods,
|
|
1682
1736
|
refunds,
|
|
1683
|
-
lineItems
|
|
1737
|
+
lineItems,
|
|
1738
|
+
reservations
|
|
1684
1739
|
},
|
|
1685
1740
|
computed: {
|
|
1686
1741
|
balance,
|
|
@@ -2760,43 +2815,399 @@ var ORDER_STATE_EVENTS = {
|
|
|
2760
2815
|
configUpdated: "config-updated"
|
|
2761
2816
|
};
|
|
2762
2817
|
|
|
2763
|
-
//
|
|
2764
|
-
var
|
|
2765
|
-
|
|
2766
|
-
|
|
2818
|
+
// booking/types.ts
|
|
2819
|
+
var BookingRulesError = class extends Error {
|
|
2820
|
+
constructor(message) {
|
|
2821
|
+
super(message);
|
|
2822
|
+
this.name = "BookingRulesError";
|
|
2823
|
+
}
|
|
2824
|
+
};
|
|
2825
|
+
|
|
2826
|
+
// booking/zoned-time.ts
|
|
2827
|
+
var MINUTE = 6e4;
|
|
2828
|
+
var formatters = /* @__PURE__ */ new Map();
|
|
2829
|
+
var formatterFor = (timeZone) => {
|
|
2830
|
+
const existing = formatters.get(timeZone);
|
|
2831
|
+
if (existing) return existing;
|
|
2832
|
+
let created;
|
|
2833
|
+
try {
|
|
2834
|
+
created = new Intl.DateTimeFormat("en-US", {
|
|
2835
|
+
timeZone,
|
|
2836
|
+
hour12: false,
|
|
2837
|
+
year: "numeric",
|
|
2838
|
+
month: "2-digit",
|
|
2839
|
+
day: "2-digit",
|
|
2840
|
+
hour: "2-digit",
|
|
2841
|
+
minute: "2-digit",
|
|
2842
|
+
second: "2-digit",
|
|
2843
|
+
weekday: "short"
|
|
2844
|
+
});
|
|
2845
|
+
} catch {
|
|
2846
|
+
throw new BookingRulesError(`Booking rules: "${timeZone}" is not a known time zone`);
|
|
2847
|
+
}
|
|
2848
|
+
formatters.set(timeZone, created);
|
|
2849
|
+
return created;
|
|
2850
|
+
};
|
|
2851
|
+
var WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
2852
|
+
function zonedParts(instant, timeZone) {
|
|
2853
|
+
const parts = {};
|
|
2854
|
+
for (const { type, value } of formatterFor(timeZone).formatToParts(instant)) parts[type] = value;
|
|
2855
|
+
const hour = Number(parts.hour) % 24;
|
|
2856
|
+
return {
|
|
2857
|
+
year: Number(parts.year),
|
|
2858
|
+
month: Number(parts.month),
|
|
2859
|
+
day: Number(parts.day),
|
|
2860
|
+
hour,
|
|
2861
|
+
minute: Number(parts.minute),
|
|
2862
|
+
second: Number(parts.second),
|
|
2863
|
+
weekday: WEEKDAYS.indexOf(parts.weekday)
|
|
2864
|
+
};
|
|
2767
2865
|
}
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
return
|
|
2866
|
+
function offsetAt(instant, timeZone) {
|
|
2867
|
+
const { year, month, day, hour, minute, second } = zonedParts(instant, timeZone);
|
|
2868
|
+
return Date.UTC(year, month - 1, day, hour, minute, second) - Math.floor(instant.getTime() / 1e3) * 1e3;
|
|
2771
2869
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2870
|
+
function instantFromLocal(year, month, day, minutes, timeZone) {
|
|
2871
|
+
const naive = Date.UTC(year, month - 1, day) + minutes * MINUTE;
|
|
2872
|
+
const firstGuess = naive - offsetAt(new Date(naive), timeZone);
|
|
2873
|
+
return new Date(naive - offsetAt(new Date(firstGuess), timeZone));
|
|
2874
|
+
}
|
|
2875
|
+
function startOfZonedDay(instant, timeZone) {
|
|
2876
|
+
const { year, month, day } = zonedParts(instant, timeZone);
|
|
2877
|
+
return instantFromLocal(year, month, day, 0, timeZone);
|
|
2878
|
+
}
|
|
2879
|
+
function nextZonedDay(dayStart, timeZone) {
|
|
2880
|
+
const { year, month, day } = zonedParts(dayStart, timeZone);
|
|
2881
|
+
return instantFromLocal(year, month, day + 1, 0, timeZone);
|
|
2882
|
+
}
|
|
2883
|
+
function atZonedMinute(dayStart, minutes, timeZone) {
|
|
2884
|
+
const { year, month, day } = zonedParts(dayStart, timeZone);
|
|
2885
|
+
return instantFromLocal(year, month, day, minutes, timeZone);
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// booking/grid.ts
|
|
2889
|
+
var MINUTE2 = 6e4;
|
|
2890
|
+
var MAX_RANGE_DAYS = 366;
|
|
2891
|
+
var asTime = (value, field) => {
|
|
2892
|
+
if (value == null) return void 0;
|
|
2893
|
+
const time = new Date(value).getTime();
|
|
2894
|
+
if (Number.isNaN(time)) throw new BookingRulesError(`Booking rules: ${field} is not a date, got "${String(value)}"`);
|
|
2895
|
+
return time;
|
|
2896
|
+
};
|
|
2897
|
+
function parseHhMm(value, field) {
|
|
2898
|
+
const [hours, minutes] = value.split(":").map(Number);
|
|
2899
|
+
if (!Number.isInteger(hours) || !Number.isInteger(minutes)) {
|
|
2900
|
+
throw new BookingRulesError(`Booking rules: ${field} must be "HH:mm", got "${value}"`);
|
|
2901
|
+
}
|
|
2902
|
+
return hours * 60 + minutes;
|
|
2903
|
+
}
|
|
2904
|
+
function windowEndsAt(rule, now) {
|
|
2905
|
+
const { maxAdvanceDays, bookableTo } = rule.window;
|
|
2906
|
+
const ends = Math.min(
|
|
2907
|
+
maxAdvanceDays ? now.getTime() + maxAdvanceDays * 1440 * MINUTE2 : Number.POSITIVE_INFINITY,
|
|
2908
|
+
asTime(bookableTo, "window.bookableTo") ?? Number.POSITIVE_INFINITY
|
|
2909
|
+
);
|
|
2910
|
+
return Number.isFinite(ends) ? new Date(ends) : null;
|
|
2911
|
+
}
|
|
2912
|
+
function periodSlot(dayStart, timeZone, rule, openMinute, closeMinute) {
|
|
2913
|
+
const { ratePeriod, slot, checkInTime, checkOutTime } = rule;
|
|
2914
|
+
const at = (minutes) => atZonedMinute(dayStart, minutes, timeZone);
|
|
2915
|
+
if (ratePeriod === "night" /* NIGHT */) {
|
|
2916
|
+
if (!checkInTime || !checkOutTime) {
|
|
2917
|
+
throw new BookingRulesError("Overnight booking rules need both checkInTime and checkOutTime");
|
|
2918
|
+
}
|
|
2919
|
+
const checkIn = parseHhMm(checkInTime, "checkInTime");
|
|
2920
|
+
const checkOut = parseHhMm(checkOutTime, "checkOutTime");
|
|
2921
|
+
return {
|
|
2922
|
+
startAt: at(checkIn),
|
|
2923
|
+
endAt: at(1440 + checkOut),
|
|
2924
|
+
// Housekeeping sits between check-out and the next check-in: it costs no night.
|
|
2925
|
+
bufferEndAt: at(1440 + checkIn),
|
|
2926
|
+
canStart: true
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
const from = ratePeriod === "day" /* DAY */ ? openMinute ?? 0 : 0;
|
|
2930
|
+
const to = ratePeriod === "day" /* DAY */ ? closeMinute ?? 1440 : 1440;
|
|
2931
|
+
return {
|
|
2932
|
+
startAt: at(from),
|
|
2933
|
+
endAt: at(to),
|
|
2934
|
+
bufferEndAt: at(to + (slot.bufferMinutes ?? 0)),
|
|
2935
|
+
canStart: true
|
|
2936
|
+
};
|
|
2937
|
+
}
|
|
2938
|
+
function freeGaps(openAt, closeAt, busy) {
|
|
2939
|
+
const inside = busy.map(({ startAt, bufferEndAt }) => ({ from: +startAt, to: +bufferEndAt })).filter(({ from, to }) => to > openAt && from < closeAt).sort((first, second) => first.from - second.from);
|
|
2940
|
+
const gaps = [];
|
|
2941
|
+
let cursor = openAt;
|
|
2942
|
+
for (const { from, to } of inside) {
|
|
2943
|
+
if (from > cursor) gaps.push({ from: cursor, to: from, endsAtBooking: true });
|
|
2944
|
+
cursor = Math.max(cursor, to);
|
|
2945
|
+
if (cursor >= closeAt) break;
|
|
2946
|
+
}
|
|
2947
|
+
if (cursor < closeAt) gaps.push({ from: cursor, to: closeAt, endsAtBooking: false });
|
|
2948
|
+
return gaps;
|
|
2949
|
+
}
|
|
2950
|
+
function buildGrid(rule, timeZone, from, to, now, busy = []) {
|
|
2951
|
+
if (to.getTime() - from.getTime() > MAX_RANGE_DAYS * 1440 * MINUTE2) {
|
|
2952
|
+
throw new BookingRulesError(`A booking range may span at most ${MAX_RANGE_DAYS} days`);
|
|
2953
|
+
}
|
|
2954
|
+
const { availability, slot, window, ratePeriod } = rule;
|
|
2955
|
+
const periodMode = ratePeriod !== "slot" /* SLOT */;
|
|
2956
|
+
const weeklyHours = availability.weeklyHours ?? [];
|
|
2957
|
+
const notBefore = Math.max(
|
|
2958
|
+
now.getTime() + (window.minNoticeMinutes ?? 0) * MINUTE2,
|
|
2959
|
+
asTime(window.bookableFrom, "window.bookableFrom") ?? 0,
|
|
2960
|
+
asTime(availability.availableFrom, "availability.availableFrom") ?? 0
|
|
2961
|
+
);
|
|
2962
|
+
const notAfter = windowEndsAt(rule, now)?.getTime() ?? Number.POSITIVE_INFINITY;
|
|
2963
|
+
const blocked = (availability.offMarket ?? []).map(({ from: blockFrom, to: blockTo }) => ({
|
|
2964
|
+
from: asTime(blockFrom, "availability.offMarket.from"),
|
|
2965
|
+
to: asTime(blockTo, "availability.offMarket.to")
|
|
2966
|
+
}));
|
|
2967
|
+
const arrivalDays = availability.arrivalDays;
|
|
2968
|
+
const step = slot.lengthMinutes + (slot.bufferMinutes ?? 0);
|
|
2969
|
+
const slots = [];
|
|
2970
|
+
const lastDay = startOfZonedDay(to, timeZone).getTime();
|
|
2971
|
+
let dayStart = startOfZonedDay(from, timeZone);
|
|
2972
|
+
while (dayStart.getTime() <= lastDay) {
|
|
2973
|
+
const weekday = zonedParts(dayStart, timeZone).weekday;
|
|
2974
|
+
const rows = weeklyHours.filter((entry) => entry.weekday === weekday);
|
|
2975
|
+
if (periodMode) {
|
|
2976
|
+
const openToday = weeklyHours.length === 0 || rows.length > 0;
|
|
2977
|
+
if (openToday) {
|
|
2978
|
+
const built = periodSlot(dayStart, timeZone, rule, rows[0]?.openMinute, rows[0]?.closeMinute);
|
|
2979
|
+
slots.push({ ...built, canStart: !arrivalDays?.length || arrivalDays.includes(weekday) });
|
|
2980
|
+
}
|
|
2981
|
+
} else {
|
|
2982
|
+
for (const row of rows) {
|
|
2983
|
+
const openAt = +atZonedMinute(dayStart, row.openMinute, timeZone);
|
|
2984
|
+
const closeAt = +atZonedMinute(dayStart, row.closeMinute, timeZone);
|
|
2985
|
+
for (const gap of freeGaps(openAt, closeAt, busy)) {
|
|
2986
|
+
const mustFit = (gap.endsAtBooking ? step : slot.lengthMinutes) * MINUTE2;
|
|
2987
|
+
for (let start = gap.from; start + mustFit <= gap.to; start += step * MINUTE2) {
|
|
2988
|
+
slots.push({
|
|
2989
|
+
startAt: new Date(start),
|
|
2990
|
+
endAt: new Date(start + slot.lengthMinutes * MINUTE2),
|
|
2991
|
+
bufferEndAt: new Date(start + step * MINUTE2),
|
|
2992
|
+
canStart: true
|
|
2993
|
+
});
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
dayStart = nextZonedDay(dayStart, timeZone);
|
|
2999
|
+
}
|
|
3000
|
+
const askedFrom = from.getTime();
|
|
3001
|
+
const askedTo = to.getTime();
|
|
3002
|
+
return slots.filter(({ startAt, bufferEndAt }) => {
|
|
3003
|
+
const start = startAt.getTime();
|
|
3004
|
+
if (start < askedFrom || start >= askedTo) return false;
|
|
3005
|
+
if (start < notBefore || start > notAfter) return false;
|
|
3006
|
+
return !blocked.some(({ from: blockFrom, to: blockTo }) => start < blockTo && bufferEndAt.getTime() > blockFrom);
|
|
3007
|
+
}).sort((first, second) => +first.startAt - +second.startAt);
|
|
3008
|
+
}
|
|
3009
|
+
function slotsForWindow(grid, startAt, endAt) {
|
|
3010
|
+
const startIndex = grid.findIndex((entry) => entry.startAt.getTime() === startAt.getTime());
|
|
3011
|
+
if (startIndex < 0) {
|
|
3012
|
+
throw new BookingRulesError(`${startAt.toISOString()} is not a bookable slot start for these rules`);
|
|
3013
|
+
}
|
|
3014
|
+
if (!grid[startIndex].canStart) {
|
|
3015
|
+
throw new BookingRulesError(`${startAt.toISOString()} is not an arrival day for these rules`);
|
|
3016
|
+
}
|
|
3017
|
+
const covered = [];
|
|
3018
|
+
for (let index = startIndex; index < grid.length; index++) {
|
|
3019
|
+
const entry = grid[index];
|
|
3020
|
+
const previous = covered[covered.length - 1];
|
|
3021
|
+
if (previous && entry.startAt.getTime() !== previous.bufferEndAt.getTime()) break;
|
|
3022
|
+
covered.push(entry);
|
|
3023
|
+
if (entry.endAt.getTime() === endAt.getTime()) return covered;
|
|
3024
|
+
if (entry.endAt.getTime() > endAt.getTime()) break;
|
|
3025
|
+
}
|
|
3026
|
+
throw new BookingRulesError(
|
|
3027
|
+
`${startAt.toISOString()} \u2192 ${endAt.toISOString()} does not line up with the bookable grid`
|
|
3028
|
+
);
|
|
2775
3029
|
}
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
3030
|
+
|
|
3031
|
+
// booking/availability.ts
|
|
3032
|
+
var withinPresence = (slot, timeZone, presence) => {
|
|
3033
|
+
if (!presence?.length) return true;
|
|
3034
|
+
const dayStart = startOfZonedDay(slot.startAt, timeZone);
|
|
3035
|
+
const weekday = zonedParts(dayStart, timeZone).weekday;
|
|
3036
|
+
return presence.some(
|
|
3037
|
+
(shift) => shift.weekday === weekday && slot.startAt.getTime() >= atZonedMinute(dayStart, shift.openMinute, timeZone).getTime() && slot.endAt.getTime() <= atZonedMinute(dayStart, shift.closeMinute, timeZone).getTime()
|
|
3038
|
+
);
|
|
3039
|
+
};
|
|
3040
|
+
var overlaps = (entry, startAt, bufferEndAt) => entry.startAt.getTime() < bufferEndAt.getTime() && entry.bufferEndAt.getTime() > startAt.getTime();
|
|
3041
|
+
function occupancyIn(occupied, productId, startAt, bufferEndAt, shared) {
|
|
3042
|
+
const clashing = occupied.filter((entry) => overlaps(entry, startAt, bufferEndAt));
|
|
3043
|
+
const foreign = shared ? void 0 : clashing.find((entry) => entry.productId !== productId);
|
|
3044
|
+
return {
|
|
3045
|
+
taken: clashing.filter((entry) => shared || entry.productId === productId).reduce((sum, entry) => sum + entry.qty, 0),
|
|
3046
|
+
foreignProductId: foreign?.productId
|
|
3047
|
+
};
|
|
2779
3048
|
}
|
|
2780
|
-
var
|
|
2781
|
-
|
|
3049
|
+
var stateFor = (hasShift, offerable, free) => {
|
|
3050
|
+
if (offerable === 0) {
|
|
3051
|
+
return hasShift() ? "fullyBooked" /* FULLY_BOOKED */ : "noHoursInRange" /* NO_HOURS_IN_RANGE */;
|
|
3052
|
+
}
|
|
3053
|
+
return free === 0 ? "fullyBooked" /* FULLY_BOOKED */ : "available" /* AVAILABLE */;
|
|
2782
3054
|
};
|
|
2783
|
-
function
|
|
2784
|
-
|
|
3055
|
+
function shopDayKey(instant, timeZone) {
|
|
3056
|
+
const { year, month, day } = zonedParts(instant, timeZone);
|
|
3057
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
3058
|
+
return `${year}-${pad(month)}-${pad(day)}`;
|
|
3059
|
+
}
|
|
3060
|
+
function shopDayRange(fromDayKey, count, timeZone) {
|
|
3061
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(fromDayKey ?? "");
|
|
3062
|
+
if (!match) {
|
|
3063
|
+
throw new BookingRulesError(`Booking rules: "${fromDayKey}" is not a shop day (YYYY-MM-DD)`);
|
|
3064
|
+
}
|
|
3065
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
3066
|
+
throw new BookingRulesError(`Booking rules: "${count}" is not a number of days`);
|
|
3067
|
+
}
|
|
3068
|
+
const [, year, month, day] = match.map(Number);
|
|
3069
|
+
const from = instantFromLocal(year, month, day, 0, timeZone);
|
|
3070
|
+
if (shopDayKey(from, timeZone) !== fromDayKey) {
|
|
3071
|
+
throw new BookingRulesError(`Booking rules: "${fromDayKey}" is not a real day`);
|
|
3072
|
+
}
|
|
3073
|
+
return { from, to: instantFromLocal(year, month, day + count, 0, timeZone) };
|
|
3074
|
+
}
|
|
3075
|
+
function buildAvailability({
|
|
3076
|
+
productId,
|
|
3077
|
+
bookingRulesId,
|
|
3078
|
+
bookingType,
|
|
3079
|
+
rule,
|
|
3080
|
+
resources,
|
|
3081
|
+
occupied,
|
|
3082
|
+
from,
|
|
3083
|
+
to,
|
|
3084
|
+
now
|
|
3085
|
+
}) {
|
|
3086
|
+
const shared = rule.slot.sharedAcrossProducts ?? false;
|
|
3087
|
+
const bookableUntil = windowEndsAt(rule, now);
|
|
3088
|
+
const header = {
|
|
3089
|
+
productId,
|
|
3090
|
+
bookingRulesId,
|
|
3091
|
+
bookingType,
|
|
3092
|
+
ratePeriod: rule.ratePeriod,
|
|
3093
|
+
// Passed through for the UI to label: a group slot under minimumToRun may not run.
|
|
3094
|
+
minimumToRun: rule.slot.minimumToRun,
|
|
3095
|
+
minStay: rule.slot.minStay,
|
|
3096
|
+
maxStay: rule.slot.maxStay,
|
|
3097
|
+
// Why a day can come back empty. Past this instant the rule refuses everything, so the UI
|
|
3098
|
+
// can say "beyond the booking window" where it would otherwise claim "nothing free".
|
|
3099
|
+
bookableUntil,
|
|
3100
|
+
// The comparison itself, not just the boundary: deciding whether a day lies past the
|
|
3101
|
+
// window is a rule, and a UI that re-derives it owns a copy of that rule — which is how
|
|
3102
|
+
// the same empty grid ends up explained one way on the till and another in a flow.
|
|
3103
|
+
beyondWindow: !!bookableUntil && from.getTime() > bookableUntil.getTime()
|
|
3104
|
+
};
|
|
3105
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
3106
|
+
for (const entry of occupied) {
|
|
3107
|
+
byResource.set(entry.resourceId, [...byResource.get(entry.resourceId) ?? [], entry]);
|
|
3108
|
+
}
|
|
3109
|
+
const grids = resources.map((resource) => {
|
|
3110
|
+
const mine = byResource.get(resource.id) ?? [];
|
|
3111
|
+
const busy = (resource.seats ?? 1) === 1 ? mine : [];
|
|
3112
|
+
const slots2 = buildGrid(rule, resource.timeZone, from, to, now, busy).filter(
|
|
3113
|
+
(slot) => withinPresence(slot, resource.timeZone, resource.presence)
|
|
3114
|
+
);
|
|
3115
|
+
return { resource, slots: slots2 };
|
|
3116
|
+
});
|
|
3117
|
+
const byInstant = /* @__PURE__ */ new Map();
|
|
3118
|
+
const freeSlotsPerResource = /* @__PURE__ */ new Map();
|
|
3119
|
+
for (const { resource, slots: slots2 } of grids) {
|
|
3120
|
+
const capacity = resource.seats ?? 1;
|
|
3121
|
+
const mine = byResource.get(resource.id) ?? [];
|
|
3122
|
+
for (const slot of slots2) {
|
|
3123
|
+
const { taken, foreignProductId } = occupancyIn(mine, productId, slot.startAt, slot.bufferEndAt, shared);
|
|
3124
|
+
const free = foreignProductId ? 0 : Math.max(capacity - taken, 0);
|
|
3125
|
+
const entry = byInstant.get(+slot.startAt) ?? { ...slot, capacity: 0, booked: 0, free: 0, resources: [], dayKey: shopDayKey(slot.startAt, resource.timeZone) };
|
|
3126
|
+
entry.capacity += capacity;
|
|
3127
|
+
entry.booked += foreignProductId ? capacity : taken;
|
|
3128
|
+
entry.free += free;
|
|
3129
|
+
entry.canStart = entry.canStart || slot.canStart;
|
|
3130
|
+
entry.resources.push({ resourceId: resource.id, name: resource.name, capacity, booked: taken, free });
|
|
3131
|
+
if (free > 0) freeSlotsPerResource.set(resource.id, (freeSlotsPerResource.get(resource.id) ?? 0) + 1);
|
|
3132
|
+
byInstant.set(+slot.startAt, entry);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
const inWindow = grids.map(({ resource, slots: slots2 }) => {
|
|
3136
|
+
const free = freeSlotsPerResource.get(resource.id) ?? 0;
|
|
3137
|
+
return {
|
|
3138
|
+
resourceId: resource.id,
|
|
3139
|
+
name: resource.name,
|
|
3140
|
+
slots: slots2.length,
|
|
3141
|
+
free,
|
|
3142
|
+
// Lazily: the shift grid is a second walk, and only an empty answer needs it.
|
|
3143
|
+
state: stateFor(
|
|
3144
|
+
() => buildGrid(rule, resource.timeZone, from, to, now).filter(
|
|
3145
|
+
(slot) => withinPresence(slot, resource.timeZone, resource.presence)
|
|
3146
|
+
).length > 0,
|
|
3147
|
+
slots2.length,
|
|
3148
|
+
free
|
|
3149
|
+
)
|
|
3150
|
+
};
|
|
3151
|
+
});
|
|
3152
|
+
const slots = [...byInstant.values()].sort((first, second) => +first.startAt - +second.startAt);
|
|
3153
|
+
return {
|
|
3154
|
+
...header,
|
|
3155
|
+
resources: inWindow,
|
|
3156
|
+
slots,
|
|
3157
|
+
/**
|
|
3158
|
+
* The clock every instant above reads on. Returned rather than assumed: without it a caller
|
|
3159
|
+
* has to fetch the zone from somewhere else and hope it is the same one the grid used.
|
|
3160
|
+
*/
|
|
3161
|
+
timeZone: resources[0]?.timeZone ?? null,
|
|
3162
|
+
/** The shop days this answer covers, in order — what a day picker lists. */
|
|
3163
|
+
days: [...new Set(slots.map((slot) => slot.dayKey))]
|
|
3164
|
+
};
|
|
2785
3165
|
}
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
3166
|
+
function presentAt(resources, rows, outletId) {
|
|
3167
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
3168
|
+
for (const row of rows) {
|
|
3169
|
+
byResource.set(row.resourceId, [...byResource.get(row.resourceId) ?? [], row]);
|
|
3170
|
+
}
|
|
3171
|
+
return resources.flatMap((resource) => {
|
|
3172
|
+
const own = byResource.get(resource.id) ?? [];
|
|
3173
|
+
if (own.length === 0 || !outletId) return [{ resource }];
|
|
3174
|
+
const here = own.find((row) => row.outletId === outletId);
|
|
3175
|
+
return here ? [{ resource, presence: here.weeklyHours }] : [];
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
// booking/plan.ts
|
|
3180
|
+
var DEFAULT_HOLD_MINUTES = 10;
|
|
3181
|
+
function planWindow({ rule, timeZone, presence, startAt, endAt, now, busy = [] }) {
|
|
3182
|
+
const grid = buildGrid(rule, timeZone, startAt, endAt, now, busy).filter(
|
|
3183
|
+
(slot) => withinPresence(slot, timeZone, presence)
|
|
3184
|
+
);
|
|
3185
|
+
const slots = slotsForWindow(grid, startAt, endAt);
|
|
3186
|
+
const { minStay, maxStay } = rule.slot;
|
|
3187
|
+
if (minStay && slots.length < minStay) {
|
|
3188
|
+
throw new BookingRulesError(`These rules ask for at least ${minStay} period(s), got ${slots.length}`);
|
|
3189
|
+
}
|
|
3190
|
+
if (maxStay && slots.length > maxStay) {
|
|
3191
|
+
throw new BookingRulesError(`These rules allow at most ${maxStay} period(s), got ${slots.length}`);
|
|
3192
|
+
}
|
|
3193
|
+
const last = slots[slots.length - 1];
|
|
3194
|
+
return { slots, endAt: last.endAt, bufferEndAt: last.bufferEndAt };
|
|
3195
|
+
}
|
|
3196
|
+
var holdExpiresAt = (rule, now) => new Date(now.getTime() + (rule.window.holdMinutes ?? DEFAULT_HOLD_MINUTES) * 6e4);
|
|
3197
|
+
|
|
3198
|
+
// constants/adyen.constants.ts
|
|
3199
|
+
var ADYEN_ROLLOUT_COUNTRY_CODES = /* @__PURE__ */ new Set(["CA"]);
|
|
3200
|
+
function isAdyenRolloutCountry(alpha2) {
|
|
3201
|
+
return ADYEN_ROLLOUT_COUNTRY_CODES.has(alpha2);
|
|
2790
3202
|
}
|
|
2791
3203
|
export {
|
|
2792
|
-
ADYEN_JOB_TITLE_ASSOCIATION_TYPES,
|
|
2793
|
-
ADYEN_LEGAL_ENTITY_STATE_PROVINCE_COUNTRIES,
|
|
2794
|
-
ADYEN_NO_TAX_NUMBER_ABSENT_COUNTRIES,
|
|
2795
3204
|
ADYEN_ROLLOUT_COUNTRY_CODES,
|
|
2796
|
-
ADYEN_STORE_STATE_PROVINCE_COUNTRIES,
|
|
2797
|
-
ADYEN_TAX_NUMBER_FORMATS,
|
|
2798
|
-
ADYEN_TAX_NUMBER_MIN_LENGTH,
|
|
2799
3205
|
AttributeType,
|
|
3206
|
+
BookingRatePeriod,
|
|
3207
|
+
BookingResourceKind,
|
|
3208
|
+
BookingResourceStatus,
|
|
3209
|
+
BookingRulesError,
|
|
3210
|
+
BookingType,
|
|
2800
3211
|
CONDITION_OPERATORS,
|
|
2801
3212
|
COUNTRY_CURRENCIES,
|
|
2802
3213
|
CURRENCY_CONFIG,
|
|
@@ -2807,6 +3218,7 @@ export {
|
|
|
2807
3218
|
DEFAULT_CONFIG,
|
|
2808
3219
|
DEFAULT_CROSS_AXIS_RULES,
|
|
2809
3220
|
DEFAULT_DISPLAY_STATE_MAP,
|
|
3221
|
+
DEFAULT_HOLD_MINUTES,
|
|
2810
3222
|
DEFAULT_RESOLVED_CONFIG,
|
|
2811
3223
|
DEFAULT_UNIT_ID,
|
|
2812
3224
|
DualWriteStrategy,
|
|
@@ -2850,8 +3262,10 @@ export {
|
|
|
2850
3262
|
REPORT_NEGATE_PATHS,
|
|
2851
3263
|
ReceiptSendChannel,
|
|
2852
3264
|
ReceiptType,
|
|
3265
|
+
ReservationStatus,
|
|
2853
3266
|
ResidualPaymentMethod,
|
|
2854
3267
|
ResidualStatus,
|
|
3268
|
+
ResourceAvailabilityState,
|
|
2855
3269
|
STATE_SCHEMA,
|
|
2856
3270
|
STRIPE_SUPPORTED_COUNTRIES,
|
|
2857
3271
|
SYSTEM_FAMILIES,
|
|
@@ -2865,11 +3279,6 @@ export {
|
|
|
2865
3279
|
TopMetericsSort,
|
|
2866
3280
|
UserTypes,
|
|
2867
3281
|
addAmounts,
|
|
2868
|
-
adyenLegalEntityRequiresStateOrProvince,
|
|
2869
|
-
adyenRequiresJobTitle,
|
|
2870
|
-
adyenStoreRequiresStateOrProvince,
|
|
2871
|
-
adyenSupportsTaxNumberAbsent,
|
|
2872
|
-
adyenTaxNumberFormat,
|
|
2873
3282
|
amountToString,
|
|
2874
3283
|
assertUnitSane,
|
|
2875
3284
|
availableIn,
|
|
@@ -2877,6 +3286,8 @@ export {
|
|
|
2877
3286
|
baseFulfillmentDefinitions,
|
|
2878
3287
|
baseOf,
|
|
2879
3288
|
basePaymentDefinitions,
|
|
3289
|
+
buildAvailability,
|
|
3290
|
+
buildGrid,
|
|
2880
3291
|
buildOrderContext,
|
|
2881
3292
|
buildPosPresetConfig,
|
|
2882
3293
|
buildResolvedConfig,
|
|
@@ -2916,6 +3327,7 @@ export {
|
|
|
2916
3327
|
getPricingRegionFromCountry,
|
|
2917
3328
|
getSettlementTotal,
|
|
2918
3329
|
getTotalCollected,
|
|
3330
|
+
holdExpiresAt,
|
|
2919
3331
|
inferStatesFromLegacyStatus,
|
|
2920
3332
|
isAdyenRolloutCountry,
|
|
2921
3333
|
isConditionOperator,
|
|
@@ -2930,9 +3342,12 @@ export {
|
|
|
2930
3342
|
mergeFragment,
|
|
2931
3343
|
multiplyAmount,
|
|
2932
3344
|
normalizeMongoIdString,
|
|
3345
|
+
occupancyIn,
|
|
2933
3346
|
parseAmountToMinorUnits,
|
|
2934
3347
|
parseCurrencyCode,
|
|
2935
3348
|
parseStripeCurrencyCode,
|
|
3349
|
+
planWindow,
|
|
3350
|
+
presentAt,
|
|
2936
3351
|
removeFragment,
|
|
2937
3352
|
resolveFamily,
|
|
2938
3353
|
resolveTemplate,
|
|
@@ -2940,13 +3355,18 @@ export {
|
|
|
2940
3355
|
roundToPrecision,
|
|
2941
3356
|
sameFamily,
|
|
2942
3357
|
setOrderState,
|
|
3358
|
+
shopDayKey,
|
|
3359
|
+
shopDayRange,
|
|
2943
3360
|
signReportForDisplay,
|
|
3361
|
+
slotsForWindow,
|
|
2944
3362
|
stringifyMinorUnits,
|
|
2945
3363
|
subtractAmounts,
|
|
2946
3364
|
toBase,
|
|
2947
3365
|
toMinorUnits,
|
|
2948
3366
|
validateConfig,
|
|
2949
3367
|
validateFragmentCompatibility,
|
|
2950
|
-
violatesFinancialInvariant
|
|
3368
|
+
violatesFinancialInvariant,
|
|
3369
|
+
windowEndsAt,
|
|
3370
|
+
withinPresence
|
|
2951
3371
|
};
|
|
2952
3372
|
//# sourceMappingURL=index.mjs.map
|