@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.js
CHANGED
|
@@ -20,6 +20,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
var index_exports = {};
|
|
21
21
|
__export(index_exports, {
|
|
22
22
|
AttributeType: () => AttributeType,
|
|
23
|
+
BookingRatePeriod: () => BookingRatePeriod,
|
|
24
|
+
BookingResourceKind: () => BookingResourceKind,
|
|
25
|
+
BookingResourceStatus: () => BookingResourceStatus,
|
|
26
|
+
BookingRulesError: () => BookingRulesError,
|
|
27
|
+
BookingType: () => BookingType,
|
|
23
28
|
CONDITION_OPERATORS: () => CONDITION_OPERATORS,
|
|
24
29
|
COUNTRY_CURRENCIES: () => COUNTRY_CURRENCIES,
|
|
25
30
|
CURRENCY_CONFIG: () => CURRENCY_CONFIG,
|
|
@@ -30,6 +35,7 @@ __export(index_exports, {
|
|
|
30
35
|
DEFAULT_CONFIG: () => DEFAULT_CONFIG,
|
|
31
36
|
DEFAULT_CROSS_AXIS_RULES: () => DEFAULT_CROSS_AXIS_RULES,
|
|
32
37
|
DEFAULT_DISPLAY_STATE_MAP: () => DEFAULT_DISPLAY_STATE_MAP,
|
|
38
|
+
DEFAULT_HOLD_MINUTES: () => DEFAULT_HOLD_MINUTES,
|
|
33
39
|
DEFAULT_RESOLVED_CONFIG: () => DEFAULT_RESOLVED_CONFIG,
|
|
34
40
|
DEFAULT_UNIT_ID: () => DEFAULT_UNIT_ID,
|
|
35
41
|
DualWriteStrategy: () => DualWriteStrategy,
|
|
@@ -73,8 +79,10 @@ __export(index_exports, {
|
|
|
73
79
|
REPORT_NEGATE_PATHS: () => REPORT_NEGATE_PATHS,
|
|
74
80
|
ReceiptSendChannel: () => ReceiptSendChannel,
|
|
75
81
|
ReceiptType: () => ReceiptType,
|
|
82
|
+
ReservationStatus: () => ReservationStatus,
|
|
76
83
|
ResidualPaymentMethod: () => ResidualPaymentMethod,
|
|
77
84
|
ResidualStatus: () => ResidualStatus,
|
|
85
|
+
ResourceAvailabilityState: () => ResourceAvailabilityState,
|
|
78
86
|
STATE_SCHEMA: () => STATE_SCHEMA,
|
|
79
87
|
STRIPE_SUPPORTED_COUNTRIES: () => STRIPE_SUPPORTED_COUNTRIES,
|
|
80
88
|
SYSTEM_FAMILIES: () => SYSTEM_FAMILIES,
|
|
@@ -95,6 +103,8 @@ __export(index_exports, {
|
|
|
95
103
|
baseFulfillmentDefinitions: () => baseFulfillmentDefinitions,
|
|
96
104
|
baseOf: () => baseOf,
|
|
97
105
|
basePaymentDefinitions: () => basePaymentDefinitions,
|
|
106
|
+
buildAvailability: () => buildAvailability,
|
|
107
|
+
buildGrid: () => buildGrid,
|
|
98
108
|
buildOrderContext: () => buildOrderContext,
|
|
99
109
|
buildPosPresetConfig: () => buildPosPresetConfig,
|
|
100
110
|
buildResolvedConfig: () => buildResolvedConfig,
|
|
@@ -134,6 +144,7 @@ __export(index_exports, {
|
|
|
134
144
|
getPricingRegionFromCountry: () => getPricingRegionFromCountry,
|
|
135
145
|
getSettlementTotal: () => getSettlementTotal,
|
|
136
146
|
getTotalCollected: () => getTotalCollected,
|
|
147
|
+
holdExpiresAt: () => holdExpiresAt,
|
|
137
148
|
inferStatesFromLegacyStatus: () => inferStatesFromLegacyStatus,
|
|
138
149
|
isConditionOperator: () => isConditionOperator,
|
|
139
150
|
isFulfillmentState: () => isFulfillmentState,
|
|
@@ -147,9 +158,12 @@ __export(index_exports, {
|
|
|
147
158
|
mergeFragment: () => mergeFragment,
|
|
148
159
|
multiplyAmount: () => multiplyAmount,
|
|
149
160
|
normalizeMongoIdString: () => normalizeMongoIdString,
|
|
161
|
+
occupancyIn: () => occupancyIn,
|
|
150
162
|
parseAmountToMinorUnits: () => parseAmountToMinorUnits,
|
|
151
163
|
parseCurrencyCode: () => parseCurrencyCode,
|
|
152
164
|
parseStripeCurrencyCode: () => parseStripeCurrencyCode,
|
|
165
|
+
planWindow: () => planWindow,
|
|
166
|
+
presentAt: () => presentAt,
|
|
153
167
|
removeFragment: () => removeFragment,
|
|
154
168
|
resolveFamily: () => resolveFamily,
|
|
155
169
|
resolveTemplate: () => resolveTemplate,
|
|
@@ -158,13 +172,16 @@ __export(index_exports, {
|
|
|
158
172
|
sameFamily: () => sameFamily,
|
|
159
173
|
setOrderState: () => setOrderState,
|
|
160
174
|
signReportForDisplay: () => signReportForDisplay,
|
|
175
|
+
slotsForWindow: () => slotsForWindow,
|
|
161
176
|
stringifyMinorUnits: () => stringifyMinorUnits,
|
|
162
177
|
subtractAmounts: () => subtractAmounts,
|
|
163
178
|
toBase: () => toBase,
|
|
164
179
|
toMinorUnits: () => toMinorUnits,
|
|
165
180
|
validateConfig: () => validateConfig,
|
|
166
181
|
validateFragmentCompatibility: () => validateFragmentCompatibility,
|
|
167
|
-
violatesFinancialInvariant: () => violatesFinancialInvariant
|
|
182
|
+
violatesFinancialInvariant: () => violatesFinancialInvariant,
|
|
183
|
+
windowEndsAt: () => windowEndsAt,
|
|
184
|
+
withinPresence: () => withinPresence
|
|
168
185
|
});
|
|
169
186
|
module.exports = __toCommonJS(index_exports);
|
|
170
187
|
|
|
@@ -590,6 +607,7 @@ var StripeCurrencyCode = /* @__PURE__ */ ((StripeCurrencyCode2) => {
|
|
|
590
607
|
var ProductType = /* @__PURE__ */ ((ProductType2) => {
|
|
591
608
|
ProductType2["SIMPLE"] = "simple";
|
|
592
609
|
ProductType2["VARIABLE"] = "variable";
|
|
610
|
+
ProductType2["BOOKING"] = "booking";
|
|
593
611
|
return ProductType2;
|
|
594
612
|
})(ProductType || {});
|
|
595
613
|
var ProductStatus = /* @__PURE__ */ ((ProductStatus2) => {
|
|
@@ -607,6 +625,50 @@ var PaymentProcessor = /* @__PURE__ */ ((PaymentProcessor2) => {
|
|
|
607
625
|
return PaymentProcessor2;
|
|
608
626
|
})(PaymentProcessor || {});
|
|
609
627
|
|
|
628
|
+
// enums/booking.enum.ts
|
|
629
|
+
var BookingType = /* @__PURE__ */ ((BookingType3) => {
|
|
630
|
+
BookingType3["RENTAL"] = "rental";
|
|
631
|
+
BookingType3["APPOINTMENT"] = "appointment";
|
|
632
|
+
BookingType3["OVERNIGHT"] = "overnight";
|
|
633
|
+
BookingType3["GROUP"] = "group";
|
|
634
|
+
return BookingType3;
|
|
635
|
+
})(BookingType || {});
|
|
636
|
+
var BookingRatePeriod = /* @__PURE__ */ ((BookingRatePeriod2) => {
|
|
637
|
+
BookingRatePeriod2["SLOT"] = "slot";
|
|
638
|
+
BookingRatePeriod2["DAY"] = "day";
|
|
639
|
+
BookingRatePeriod2["NIGHT"] = "night";
|
|
640
|
+
BookingRatePeriod2["WEEK"] = "week";
|
|
641
|
+
BookingRatePeriod2["MONTH"] = "month";
|
|
642
|
+
return BookingRatePeriod2;
|
|
643
|
+
})(BookingRatePeriod || {});
|
|
644
|
+
var BookingResourceKind = /* @__PURE__ */ ((BookingResourceKind2) => {
|
|
645
|
+
BookingResourceKind2["ASSET"] = "asset";
|
|
646
|
+
BookingResourceKind2["ROOM"] = "room";
|
|
647
|
+
BookingResourceKind2["PROPERTY"] = "property";
|
|
648
|
+
BookingResourceKind2["SPACE"] = "space";
|
|
649
|
+
BookingResourceKind2["STAFF"] = "staff";
|
|
650
|
+
BookingResourceKind2["TABLE"] = "table";
|
|
651
|
+
return BookingResourceKind2;
|
|
652
|
+
})(BookingResourceKind || {});
|
|
653
|
+
var ReservationStatus = /* @__PURE__ */ ((ReservationStatus2) => {
|
|
654
|
+
ReservationStatus2["HELD"] = "held";
|
|
655
|
+
ReservationStatus2["CONFIRMED"] = "confirmed";
|
|
656
|
+
ReservationStatus2["CANCELLED"] = "cancelled";
|
|
657
|
+
ReservationStatus2["EXPIRED"] = "expired";
|
|
658
|
+
return ReservationStatus2;
|
|
659
|
+
})(ReservationStatus || {});
|
|
660
|
+
var BookingResourceStatus = /* @__PURE__ */ ((BookingResourceStatus2) => {
|
|
661
|
+
BookingResourceStatus2["ACTIVE"] = "active";
|
|
662
|
+
BookingResourceStatus2["INACTIVE"] = "inactive";
|
|
663
|
+
return BookingResourceStatus2;
|
|
664
|
+
})(BookingResourceStatus || {});
|
|
665
|
+
var ResourceAvailabilityState = /* @__PURE__ */ ((ResourceAvailabilityState2) => {
|
|
666
|
+
ResourceAvailabilityState2["AVAILABLE"] = "available";
|
|
667
|
+
ResourceAvailabilityState2["FULLY_BOOKED"] = "fullyBooked";
|
|
668
|
+
ResourceAvailabilityState2["NO_HOURS_IN_RANGE"] = "noHoursInRange";
|
|
669
|
+
return ResourceAvailabilityState2;
|
|
670
|
+
})(ResourceAvailabilityState || {});
|
|
671
|
+
|
|
610
672
|
// utils/currency.util.ts
|
|
611
673
|
var CURRENCY_CONFIG = {
|
|
612
674
|
// Major currencies with 2 decimal places
|
|
@@ -1816,6 +1878,15 @@ function sumRefundQuantities(refunds) {
|
|
|
1816
1878
|
}
|
|
1817
1879
|
return t;
|
|
1818
1880
|
}
|
|
1881
|
+
var asCount = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
1882
|
+
function countsOf(row) {
|
|
1883
|
+
const quantity = asCount(row.quantity);
|
|
1884
|
+
return {
|
|
1885
|
+
quantity,
|
|
1886
|
+
fulfilled: Math.min(asCount(row.fulfilledQuantity), quantity),
|
|
1887
|
+
returned: Math.min(asCount(row.returnedQuantity), quantity)
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1819
1890
|
function buildOrderContext(order, extras) {
|
|
1820
1891
|
const o = order ?? {};
|
|
1821
1892
|
const total = typeof o.total === "number" && Number.isFinite(o.total) ? o.total : 0;
|
|
@@ -1823,6 +1894,7 @@ function buildOrderContext(order, extras) {
|
|
|
1823
1894
|
const paymentMethods = o.paymentMethods ?? [];
|
|
1824
1895
|
const refunds = o.refunds ?? o.refund ?? [];
|
|
1825
1896
|
const lineItems = o.lineItems ?? [];
|
|
1897
|
+
const reservations = o.reservations ?? [];
|
|
1826
1898
|
const totalPaid = sumNumbers(paymentMethods);
|
|
1827
1899
|
const totalRefunded = sumRefundQuantities(refunds);
|
|
1828
1900
|
const isFullyPaid = total > 0 ? balance <= 0 : totalPaid > 0 && balance <= 0;
|
|
@@ -1830,13 +1902,11 @@ function buildOrderContext(order, extras) {
|
|
|
1830
1902
|
let itemCount = 0;
|
|
1831
1903
|
let fulfilledItemCount = 0;
|
|
1832
1904
|
let returnedItemCount = 0;
|
|
1833
|
-
for (const
|
|
1834
|
-
const
|
|
1835
|
-
itemCount +=
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
const rq = typeof li.returnedQuantity === "number" && Number.isFinite(li.returnedQuantity) ? Math.max(0, li.returnedQuantity) : 0;
|
|
1839
|
-
returnedItemCount += Math.min(rq, q);
|
|
1905
|
+
for (const row of [...lineItems, ...reservations]) {
|
|
1906
|
+
const { quantity, fulfilled, returned } = countsOf(row);
|
|
1907
|
+
itemCount += quantity;
|
|
1908
|
+
fulfilledItemCount += fulfilled;
|
|
1909
|
+
returnedItemCount += returned;
|
|
1840
1910
|
}
|
|
1841
1911
|
const isFullyReturned = itemCount > 0 ? returnedItemCount >= itemCount : returnedItemCount > 0;
|
|
1842
1912
|
const hasPartialReturns = returnedItemCount > 0 && !isFullyReturned;
|
|
@@ -1846,7 +1916,8 @@ function buildOrderContext(order, extras) {
|
|
|
1846
1916
|
balance,
|
|
1847
1917
|
paymentMethods,
|
|
1848
1918
|
refunds,
|
|
1849
|
-
lineItems
|
|
1919
|
+
lineItems,
|
|
1920
|
+
reservations
|
|
1850
1921
|
},
|
|
1851
1922
|
computed: {
|
|
1852
1923
|
balance,
|
|
@@ -2925,9 +2996,366 @@ var ORDER_STATE_EVENTS = {
|
|
|
2925
2996
|
stateTransitionBlocked: "state-transition-blocked",
|
|
2926
2997
|
configUpdated: "config-updated"
|
|
2927
2998
|
};
|
|
2999
|
+
|
|
3000
|
+
// booking/types.ts
|
|
3001
|
+
var BookingRulesError = class extends Error {
|
|
3002
|
+
constructor(message) {
|
|
3003
|
+
super(message);
|
|
3004
|
+
this.name = "BookingRulesError";
|
|
3005
|
+
}
|
|
3006
|
+
};
|
|
3007
|
+
|
|
3008
|
+
// booking/zoned-time.ts
|
|
3009
|
+
var MINUTE = 6e4;
|
|
3010
|
+
var formatters = /* @__PURE__ */ new Map();
|
|
3011
|
+
var formatterFor = (timeZone) => {
|
|
3012
|
+
const existing = formatters.get(timeZone);
|
|
3013
|
+
if (existing) return existing;
|
|
3014
|
+
let created;
|
|
3015
|
+
try {
|
|
3016
|
+
created = new Intl.DateTimeFormat("en-US", {
|
|
3017
|
+
timeZone,
|
|
3018
|
+
hour12: false,
|
|
3019
|
+
year: "numeric",
|
|
3020
|
+
month: "2-digit",
|
|
3021
|
+
day: "2-digit",
|
|
3022
|
+
hour: "2-digit",
|
|
3023
|
+
minute: "2-digit",
|
|
3024
|
+
second: "2-digit",
|
|
3025
|
+
weekday: "short"
|
|
3026
|
+
});
|
|
3027
|
+
} catch {
|
|
3028
|
+
throw new BookingRulesError(`Booking rules: "${timeZone}" is not a known time zone`);
|
|
3029
|
+
}
|
|
3030
|
+
formatters.set(timeZone, created);
|
|
3031
|
+
return created;
|
|
3032
|
+
};
|
|
3033
|
+
var WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
3034
|
+
function zonedParts(instant, timeZone) {
|
|
3035
|
+
const parts = {};
|
|
3036
|
+
for (const { type, value } of formatterFor(timeZone).formatToParts(instant)) parts[type] = value;
|
|
3037
|
+
const hour = Number(parts.hour) % 24;
|
|
3038
|
+
return {
|
|
3039
|
+
year: Number(parts.year),
|
|
3040
|
+
month: Number(parts.month),
|
|
3041
|
+
day: Number(parts.day),
|
|
3042
|
+
hour,
|
|
3043
|
+
minute: Number(parts.minute),
|
|
3044
|
+
second: Number(parts.second),
|
|
3045
|
+
weekday: WEEKDAYS.indexOf(parts.weekday)
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
3048
|
+
function offsetAt(instant, timeZone) {
|
|
3049
|
+
const { year, month, day, hour, minute, second } = zonedParts(instant, timeZone);
|
|
3050
|
+
return Date.UTC(year, month - 1, day, hour, minute, second) - Math.floor(instant.getTime() / 1e3) * 1e3;
|
|
3051
|
+
}
|
|
3052
|
+
function instantFromLocal(year, month, day, minutes, timeZone) {
|
|
3053
|
+
const naive = Date.UTC(year, month - 1, day) + minutes * MINUTE;
|
|
3054
|
+
const firstGuess = naive - offsetAt(new Date(naive), timeZone);
|
|
3055
|
+
return new Date(naive - offsetAt(new Date(firstGuess), timeZone));
|
|
3056
|
+
}
|
|
3057
|
+
function startOfZonedDay(instant, timeZone) {
|
|
3058
|
+
const { year, month, day } = zonedParts(instant, timeZone);
|
|
3059
|
+
return instantFromLocal(year, month, day, 0, timeZone);
|
|
3060
|
+
}
|
|
3061
|
+
function nextZonedDay(dayStart, timeZone) {
|
|
3062
|
+
const { year, month, day } = zonedParts(dayStart, timeZone);
|
|
3063
|
+
return instantFromLocal(year, month, day + 1, 0, timeZone);
|
|
3064
|
+
}
|
|
3065
|
+
function atZonedMinute(dayStart, minutes, timeZone) {
|
|
3066
|
+
const { year, month, day } = zonedParts(dayStart, timeZone);
|
|
3067
|
+
return instantFromLocal(year, month, day, minutes, timeZone);
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
// booking/grid.ts
|
|
3071
|
+
var MINUTE2 = 6e4;
|
|
3072
|
+
var MAX_RANGE_DAYS = 366;
|
|
3073
|
+
var asTime = (value, field) => {
|
|
3074
|
+
if (value == null) return void 0;
|
|
3075
|
+
const time = new Date(value).getTime();
|
|
3076
|
+
if (Number.isNaN(time)) throw new BookingRulesError(`Booking rules: ${field} is not a date, got "${String(value)}"`);
|
|
3077
|
+
return time;
|
|
3078
|
+
};
|
|
3079
|
+
function parseHhMm(value, field) {
|
|
3080
|
+
const [hours, minutes] = value.split(":").map(Number);
|
|
3081
|
+
if (!Number.isInteger(hours) || !Number.isInteger(minutes)) {
|
|
3082
|
+
throw new BookingRulesError(`Booking rules: ${field} must be "HH:mm", got "${value}"`);
|
|
3083
|
+
}
|
|
3084
|
+
return hours * 60 + minutes;
|
|
3085
|
+
}
|
|
3086
|
+
function windowEndsAt(rule, now) {
|
|
3087
|
+
const { maxAdvanceDays, bookableTo } = rule.window;
|
|
3088
|
+
const ends = Math.min(
|
|
3089
|
+
maxAdvanceDays ? now.getTime() + maxAdvanceDays * 1440 * MINUTE2 : Number.POSITIVE_INFINITY,
|
|
3090
|
+
asTime(bookableTo, "window.bookableTo") ?? Number.POSITIVE_INFINITY
|
|
3091
|
+
);
|
|
3092
|
+
return Number.isFinite(ends) ? new Date(ends) : null;
|
|
3093
|
+
}
|
|
3094
|
+
function periodSlot(dayStart, timeZone, rule, openMinute, closeMinute) {
|
|
3095
|
+
const { ratePeriod, slot, checkInTime, checkOutTime } = rule;
|
|
3096
|
+
const at = (minutes) => atZonedMinute(dayStart, minutes, timeZone);
|
|
3097
|
+
if (ratePeriod === "night" /* NIGHT */) {
|
|
3098
|
+
if (!checkInTime || !checkOutTime) {
|
|
3099
|
+
throw new BookingRulesError("Overnight booking rules need both checkInTime and checkOutTime");
|
|
3100
|
+
}
|
|
3101
|
+
const checkIn = parseHhMm(checkInTime, "checkInTime");
|
|
3102
|
+
const checkOut = parseHhMm(checkOutTime, "checkOutTime");
|
|
3103
|
+
return {
|
|
3104
|
+
startAt: at(checkIn),
|
|
3105
|
+
endAt: at(1440 + checkOut),
|
|
3106
|
+
// Housekeeping sits between check-out and the next check-in: it costs no night.
|
|
3107
|
+
bufferEndAt: at(1440 + checkIn),
|
|
3108
|
+
canStart: true
|
|
3109
|
+
};
|
|
3110
|
+
}
|
|
3111
|
+
const from = ratePeriod === "day" /* DAY */ ? openMinute ?? 0 : 0;
|
|
3112
|
+
const to = ratePeriod === "day" /* DAY */ ? closeMinute ?? 1440 : 1440;
|
|
3113
|
+
return {
|
|
3114
|
+
startAt: at(from),
|
|
3115
|
+
endAt: at(to),
|
|
3116
|
+
bufferEndAt: at(to + (slot.bufferMinutes ?? 0)),
|
|
3117
|
+
canStart: true
|
|
3118
|
+
};
|
|
3119
|
+
}
|
|
3120
|
+
function freeGaps(openAt, closeAt, busy) {
|
|
3121
|
+
const inside = busy.map(({ startAt, bufferEndAt }) => ({ from: +startAt, to: +bufferEndAt })).filter(({ from, to }) => to > openAt && from < closeAt).sort((first, second) => first.from - second.from);
|
|
3122
|
+
const gaps = [];
|
|
3123
|
+
let cursor = openAt;
|
|
3124
|
+
for (const { from, to } of inside) {
|
|
3125
|
+
if (from > cursor) gaps.push({ from: cursor, to: from, endsAtBooking: true });
|
|
3126
|
+
cursor = Math.max(cursor, to);
|
|
3127
|
+
if (cursor >= closeAt) break;
|
|
3128
|
+
}
|
|
3129
|
+
if (cursor < closeAt) gaps.push({ from: cursor, to: closeAt, endsAtBooking: false });
|
|
3130
|
+
return gaps;
|
|
3131
|
+
}
|
|
3132
|
+
function buildGrid(rule, timeZone, from, to, now, busy = []) {
|
|
3133
|
+
if (to.getTime() - from.getTime() > MAX_RANGE_DAYS * 1440 * MINUTE2) {
|
|
3134
|
+
throw new BookingRulesError(`A booking range may span at most ${MAX_RANGE_DAYS} days`);
|
|
3135
|
+
}
|
|
3136
|
+
const { availability, slot, window, ratePeriod } = rule;
|
|
3137
|
+
const periodMode = ratePeriod !== "slot" /* SLOT */;
|
|
3138
|
+
const weeklyHours = availability.weeklyHours ?? [];
|
|
3139
|
+
const notBefore = Math.max(
|
|
3140
|
+
now.getTime() + (window.minNoticeMinutes ?? 0) * MINUTE2,
|
|
3141
|
+
asTime(window.bookableFrom, "window.bookableFrom") ?? 0,
|
|
3142
|
+
asTime(availability.availableFrom, "availability.availableFrom") ?? 0
|
|
3143
|
+
);
|
|
3144
|
+
const notAfter = windowEndsAt(rule, now)?.getTime() ?? Number.POSITIVE_INFINITY;
|
|
3145
|
+
const blocked = (availability.offMarket ?? []).map(({ from: blockFrom, to: blockTo }) => ({
|
|
3146
|
+
from: asTime(blockFrom, "availability.offMarket.from"),
|
|
3147
|
+
to: asTime(blockTo, "availability.offMarket.to")
|
|
3148
|
+
}));
|
|
3149
|
+
const arrivalDays = availability.arrivalDays;
|
|
3150
|
+
const step = slot.lengthMinutes + (slot.bufferMinutes ?? 0);
|
|
3151
|
+
const slots = [];
|
|
3152
|
+
const lastDay = startOfZonedDay(to, timeZone).getTime();
|
|
3153
|
+
let dayStart = startOfZonedDay(from, timeZone);
|
|
3154
|
+
while (dayStart.getTime() <= lastDay) {
|
|
3155
|
+
const weekday = zonedParts(dayStart, timeZone).weekday;
|
|
3156
|
+
const rows = weeklyHours.filter((entry) => entry.weekday === weekday);
|
|
3157
|
+
if (periodMode) {
|
|
3158
|
+
const openToday = weeklyHours.length === 0 || rows.length > 0;
|
|
3159
|
+
if (openToday) {
|
|
3160
|
+
const built = periodSlot(dayStart, timeZone, rule, rows[0]?.openMinute, rows[0]?.closeMinute);
|
|
3161
|
+
slots.push({ ...built, canStart: !arrivalDays?.length || arrivalDays.includes(weekday) });
|
|
3162
|
+
}
|
|
3163
|
+
} else {
|
|
3164
|
+
for (const row of rows) {
|
|
3165
|
+
const openAt = +atZonedMinute(dayStart, row.openMinute, timeZone);
|
|
3166
|
+
const closeAt = +atZonedMinute(dayStart, row.closeMinute, timeZone);
|
|
3167
|
+
for (const gap of freeGaps(openAt, closeAt, busy)) {
|
|
3168
|
+
const mustFit = (gap.endsAtBooking ? step : slot.lengthMinutes) * MINUTE2;
|
|
3169
|
+
for (let start = gap.from; start + mustFit <= gap.to; start += step * MINUTE2) {
|
|
3170
|
+
slots.push({
|
|
3171
|
+
startAt: new Date(start),
|
|
3172
|
+
endAt: new Date(start + slot.lengthMinutes * MINUTE2),
|
|
3173
|
+
bufferEndAt: new Date(start + step * MINUTE2),
|
|
3174
|
+
canStart: true
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
dayStart = nextZonedDay(dayStart, timeZone);
|
|
3181
|
+
}
|
|
3182
|
+
const askedFrom = from.getTime();
|
|
3183
|
+
const askedTo = to.getTime();
|
|
3184
|
+
return slots.filter(({ startAt, bufferEndAt }) => {
|
|
3185
|
+
const start = startAt.getTime();
|
|
3186
|
+
if (start < askedFrom || start >= askedTo) return false;
|
|
3187
|
+
if (start < notBefore || start > notAfter) return false;
|
|
3188
|
+
return !blocked.some(({ from: blockFrom, to: blockTo }) => start < blockTo && bufferEndAt.getTime() > blockFrom);
|
|
3189
|
+
}).sort((first, second) => +first.startAt - +second.startAt);
|
|
3190
|
+
}
|
|
3191
|
+
function slotsForWindow(grid, startAt, endAt) {
|
|
3192
|
+
const startIndex = grid.findIndex((entry) => entry.startAt.getTime() === startAt.getTime());
|
|
3193
|
+
if (startIndex < 0) {
|
|
3194
|
+
throw new BookingRulesError(`${startAt.toISOString()} is not a bookable slot start for these rules`);
|
|
3195
|
+
}
|
|
3196
|
+
if (!grid[startIndex].canStart) {
|
|
3197
|
+
throw new BookingRulesError(`${startAt.toISOString()} is not an arrival day for these rules`);
|
|
3198
|
+
}
|
|
3199
|
+
const covered = [];
|
|
3200
|
+
for (let index = startIndex; index < grid.length; index++) {
|
|
3201
|
+
const entry = grid[index];
|
|
3202
|
+
const previous = covered[covered.length - 1];
|
|
3203
|
+
if (previous && entry.startAt.getTime() !== previous.bufferEndAt.getTime()) break;
|
|
3204
|
+
covered.push(entry);
|
|
3205
|
+
if (entry.endAt.getTime() === endAt.getTime()) return covered;
|
|
3206
|
+
if (entry.endAt.getTime() > endAt.getTime()) break;
|
|
3207
|
+
}
|
|
3208
|
+
throw new BookingRulesError(
|
|
3209
|
+
`${startAt.toISOString()} \u2192 ${endAt.toISOString()} does not line up with the bookable grid`
|
|
3210
|
+
);
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
// booking/availability.ts
|
|
3214
|
+
var withinPresence = (slot, timeZone, presence) => {
|
|
3215
|
+
if (!presence?.length) return true;
|
|
3216
|
+
const dayStart = startOfZonedDay(slot.startAt, timeZone);
|
|
3217
|
+
const weekday = zonedParts(dayStart, timeZone).weekday;
|
|
3218
|
+
return presence.some(
|
|
3219
|
+
(shift) => shift.weekday === weekday && slot.startAt.getTime() >= atZonedMinute(dayStart, shift.openMinute, timeZone).getTime() && slot.endAt.getTime() <= atZonedMinute(dayStart, shift.closeMinute, timeZone).getTime()
|
|
3220
|
+
);
|
|
3221
|
+
};
|
|
3222
|
+
var overlaps = (entry, startAt, bufferEndAt) => entry.startAt.getTime() < bufferEndAt.getTime() && entry.bufferEndAt.getTime() > startAt.getTime();
|
|
3223
|
+
function occupancyIn(occupied, productId, startAt, bufferEndAt, shared) {
|
|
3224
|
+
const clashing = occupied.filter((entry) => overlaps(entry, startAt, bufferEndAt));
|
|
3225
|
+
const foreign = shared ? void 0 : clashing.find((entry) => entry.productId !== productId);
|
|
3226
|
+
return {
|
|
3227
|
+
taken: clashing.filter((entry) => shared || entry.productId === productId).reduce((sum, entry) => sum + entry.qty, 0),
|
|
3228
|
+
foreignProductId: foreign?.productId
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
var stateFor = (hasShift, offerable, free) => {
|
|
3232
|
+
if (offerable === 0) {
|
|
3233
|
+
return hasShift() ? "fullyBooked" /* FULLY_BOOKED */ : "noHoursInRange" /* NO_HOURS_IN_RANGE */;
|
|
3234
|
+
}
|
|
3235
|
+
return free === 0 ? "fullyBooked" /* FULLY_BOOKED */ : "available" /* AVAILABLE */;
|
|
3236
|
+
};
|
|
3237
|
+
function buildAvailability({
|
|
3238
|
+
productId,
|
|
3239
|
+
bookingRulesId,
|
|
3240
|
+
bookingType,
|
|
3241
|
+
rule,
|
|
3242
|
+
resources,
|
|
3243
|
+
occupied,
|
|
3244
|
+
from,
|
|
3245
|
+
to,
|
|
3246
|
+
now
|
|
3247
|
+
}) {
|
|
3248
|
+
const shared = rule.slot.sharedAcrossProducts ?? false;
|
|
3249
|
+
const bookableUntil = windowEndsAt(rule, now);
|
|
3250
|
+
const header = {
|
|
3251
|
+
productId,
|
|
3252
|
+
bookingRulesId,
|
|
3253
|
+
bookingType,
|
|
3254
|
+
ratePeriod: rule.ratePeriod,
|
|
3255
|
+
// Passed through for the UI to label: a group slot under minimumToRun may not run.
|
|
3256
|
+
minimumToRun: rule.slot.minimumToRun,
|
|
3257
|
+
minStay: rule.slot.minStay,
|
|
3258
|
+
maxStay: rule.slot.maxStay,
|
|
3259
|
+
// Why a day can come back empty. Past this instant the rule refuses everything, so the UI
|
|
3260
|
+
// can say "beyond the booking window" where it would otherwise claim "nothing free".
|
|
3261
|
+
bookableUntil,
|
|
3262
|
+
// The comparison itself, not just the boundary: deciding whether a day lies past the
|
|
3263
|
+
// window is a rule, and a UI that re-derives it owns a copy of that rule — which is how
|
|
3264
|
+
// the same empty grid ends up explained one way on the till and another in a flow.
|
|
3265
|
+
beyondWindow: !!bookableUntil && from.getTime() > bookableUntil.getTime()
|
|
3266
|
+
};
|
|
3267
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
3268
|
+
for (const entry of occupied) {
|
|
3269
|
+
byResource.set(entry.resourceId, [...byResource.get(entry.resourceId) ?? [], entry]);
|
|
3270
|
+
}
|
|
3271
|
+
const grids = resources.map((resource) => {
|
|
3272
|
+
const mine = byResource.get(resource.id) ?? [];
|
|
3273
|
+
const busy = (resource.seats ?? 1) === 1 ? mine : [];
|
|
3274
|
+
const slots = buildGrid(rule, resource.timeZone, from, to, now, busy).filter(
|
|
3275
|
+
(slot) => withinPresence(slot, resource.timeZone, resource.presence)
|
|
3276
|
+
);
|
|
3277
|
+
return { resource, slots };
|
|
3278
|
+
});
|
|
3279
|
+
const byInstant = /* @__PURE__ */ new Map();
|
|
3280
|
+
const freeSlotsPerResource = /* @__PURE__ */ new Map();
|
|
3281
|
+
for (const { resource, slots } of grids) {
|
|
3282
|
+
const capacity = resource.seats ?? 1;
|
|
3283
|
+
const mine = byResource.get(resource.id) ?? [];
|
|
3284
|
+
for (const slot of slots) {
|
|
3285
|
+
const { taken, foreignProductId } = occupancyIn(mine, productId, slot.startAt, slot.bufferEndAt, shared);
|
|
3286
|
+
const free = foreignProductId ? 0 : Math.max(capacity - taken, 0);
|
|
3287
|
+
const entry = byInstant.get(+slot.startAt) ?? { ...slot, capacity: 0, booked: 0, free: 0, resources: [] };
|
|
3288
|
+
entry.capacity += capacity;
|
|
3289
|
+
entry.booked += foreignProductId ? capacity : taken;
|
|
3290
|
+
entry.free += free;
|
|
3291
|
+
entry.canStart = entry.canStart || slot.canStart;
|
|
3292
|
+
entry.resources.push({ resourceId: resource.id, name: resource.name, capacity, booked: taken, free });
|
|
3293
|
+
if (free > 0) freeSlotsPerResource.set(resource.id, (freeSlotsPerResource.get(resource.id) ?? 0) + 1);
|
|
3294
|
+
byInstant.set(+slot.startAt, entry);
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
const inWindow = grids.map(({ resource, slots }) => {
|
|
3298
|
+
const free = freeSlotsPerResource.get(resource.id) ?? 0;
|
|
3299
|
+
return {
|
|
3300
|
+
resourceId: resource.id,
|
|
3301
|
+
name: resource.name,
|
|
3302
|
+
slots: slots.length,
|
|
3303
|
+
free,
|
|
3304
|
+
// Lazily: the shift grid is a second walk, and only an empty answer needs it.
|
|
3305
|
+
state: stateFor(
|
|
3306
|
+
() => buildGrid(rule, resource.timeZone, from, to, now).filter(
|
|
3307
|
+
(slot) => withinPresence(slot, resource.timeZone, resource.presence)
|
|
3308
|
+
).length > 0,
|
|
3309
|
+
slots.length,
|
|
3310
|
+
free
|
|
3311
|
+
)
|
|
3312
|
+
};
|
|
3313
|
+
});
|
|
3314
|
+
return {
|
|
3315
|
+
...header,
|
|
3316
|
+
resources: inWindow,
|
|
3317
|
+
slots: [...byInstant.values()].sort((first, second) => +first.startAt - +second.startAt)
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
function presentAt(resources, rows, outletId) {
|
|
3321
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
3322
|
+
for (const row of rows) {
|
|
3323
|
+
byResource.set(row.resourceId, [...byResource.get(row.resourceId) ?? [], row]);
|
|
3324
|
+
}
|
|
3325
|
+
return resources.flatMap((resource) => {
|
|
3326
|
+
const own = byResource.get(resource.id) ?? [];
|
|
3327
|
+
if (own.length === 0 || !outletId) return [{ resource }];
|
|
3328
|
+
const here = own.find((row) => row.outletId === outletId);
|
|
3329
|
+
return here ? [{ resource, presence: here.weeklyHours }] : [];
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
// booking/plan.ts
|
|
3334
|
+
var DEFAULT_HOLD_MINUTES = 10;
|
|
3335
|
+
function planWindow({ rule, timeZone, presence, startAt, endAt, now, busy = [] }) {
|
|
3336
|
+
const grid = buildGrid(rule, timeZone, startAt, endAt, now, busy).filter(
|
|
3337
|
+
(slot) => withinPresence(slot, timeZone, presence)
|
|
3338
|
+
);
|
|
3339
|
+
const slots = slotsForWindow(grid, startAt, endAt);
|
|
3340
|
+
const { minStay, maxStay } = rule.slot;
|
|
3341
|
+
if (minStay && slots.length < minStay) {
|
|
3342
|
+
throw new BookingRulesError(`These rules ask for at least ${minStay} period(s), got ${slots.length}`);
|
|
3343
|
+
}
|
|
3344
|
+
if (maxStay && slots.length > maxStay) {
|
|
3345
|
+
throw new BookingRulesError(`These rules allow at most ${maxStay} period(s), got ${slots.length}`);
|
|
3346
|
+
}
|
|
3347
|
+
const last = slots[slots.length - 1];
|
|
3348
|
+
return { slots, endAt: last.endAt, bufferEndAt: last.bufferEndAt };
|
|
3349
|
+
}
|
|
3350
|
+
var holdExpiresAt = (rule, now) => new Date(now.getTime() + (rule.window.holdMinutes ?? DEFAULT_HOLD_MINUTES) * 6e4);
|
|
2928
3351
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2929
3352
|
0 && (module.exports = {
|
|
2930
3353
|
AttributeType,
|
|
3354
|
+
BookingRatePeriod,
|
|
3355
|
+
BookingResourceKind,
|
|
3356
|
+
BookingResourceStatus,
|
|
3357
|
+
BookingRulesError,
|
|
3358
|
+
BookingType,
|
|
2931
3359
|
CONDITION_OPERATORS,
|
|
2932
3360
|
COUNTRY_CURRENCIES,
|
|
2933
3361
|
CURRENCY_CONFIG,
|
|
@@ -2938,6 +3366,7 @@ var ORDER_STATE_EVENTS = {
|
|
|
2938
3366
|
DEFAULT_CONFIG,
|
|
2939
3367
|
DEFAULT_CROSS_AXIS_RULES,
|
|
2940
3368
|
DEFAULT_DISPLAY_STATE_MAP,
|
|
3369
|
+
DEFAULT_HOLD_MINUTES,
|
|
2941
3370
|
DEFAULT_RESOLVED_CONFIG,
|
|
2942
3371
|
DEFAULT_UNIT_ID,
|
|
2943
3372
|
DualWriteStrategy,
|
|
@@ -2981,8 +3410,10 @@ var ORDER_STATE_EVENTS = {
|
|
|
2981
3410
|
REPORT_NEGATE_PATHS,
|
|
2982
3411
|
ReceiptSendChannel,
|
|
2983
3412
|
ReceiptType,
|
|
3413
|
+
ReservationStatus,
|
|
2984
3414
|
ResidualPaymentMethod,
|
|
2985
3415
|
ResidualStatus,
|
|
3416
|
+
ResourceAvailabilityState,
|
|
2986
3417
|
STATE_SCHEMA,
|
|
2987
3418
|
STRIPE_SUPPORTED_COUNTRIES,
|
|
2988
3419
|
SYSTEM_FAMILIES,
|
|
@@ -3003,6 +3434,8 @@ var ORDER_STATE_EVENTS = {
|
|
|
3003
3434
|
baseFulfillmentDefinitions,
|
|
3004
3435
|
baseOf,
|
|
3005
3436
|
basePaymentDefinitions,
|
|
3437
|
+
buildAvailability,
|
|
3438
|
+
buildGrid,
|
|
3006
3439
|
buildOrderContext,
|
|
3007
3440
|
buildPosPresetConfig,
|
|
3008
3441
|
buildResolvedConfig,
|
|
@@ -3042,6 +3475,7 @@ var ORDER_STATE_EVENTS = {
|
|
|
3042
3475
|
getPricingRegionFromCountry,
|
|
3043
3476
|
getSettlementTotal,
|
|
3044
3477
|
getTotalCollected,
|
|
3478
|
+
holdExpiresAt,
|
|
3045
3479
|
inferStatesFromLegacyStatus,
|
|
3046
3480
|
isConditionOperator,
|
|
3047
3481
|
isFulfillmentState,
|
|
@@ -3055,9 +3489,12 @@ var ORDER_STATE_EVENTS = {
|
|
|
3055
3489
|
mergeFragment,
|
|
3056
3490
|
multiplyAmount,
|
|
3057
3491
|
normalizeMongoIdString,
|
|
3492
|
+
occupancyIn,
|
|
3058
3493
|
parseAmountToMinorUnits,
|
|
3059
3494
|
parseCurrencyCode,
|
|
3060
3495
|
parseStripeCurrencyCode,
|
|
3496
|
+
planWindow,
|
|
3497
|
+
presentAt,
|
|
3061
3498
|
removeFragment,
|
|
3062
3499
|
resolveFamily,
|
|
3063
3500
|
resolveTemplate,
|
|
@@ -3066,12 +3503,15 @@ var ORDER_STATE_EVENTS = {
|
|
|
3066
3503
|
sameFamily,
|
|
3067
3504
|
setOrderState,
|
|
3068
3505
|
signReportForDisplay,
|
|
3506
|
+
slotsForWindow,
|
|
3069
3507
|
stringifyMinorUnits,
|
|
3070
3508
|
subtractAmounts,
|
|
3071
3509
|
toBase,
|
|
3072
3510
|
toMinorUnits,
|
|
3073
3511
|
validateConfig,
|
|
3074
3512
|
validateFragmentCompatibility,
|
|
3075
|
-
violatesFinancialInvariant
|
|
3513
|
+
violatesFinancialInvariant,
|
|
3514
|
+
windowEndsAt,
|
|
3515
|
+
withinPresence
|
|
3076
3516
|
});
|
|
3077
3517
|
//# sourceMappingURL=index.js.map
|