@happychef/algorithm 1.3.0 → 1.4.0
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/.claude/settings.local.json +16 -0
- package/.github/workflows/ci-cd.yml +80 -80
- package/BRANCH_PROTECTION_SETUP.md +167 -167
- package/CHANGELOG.md +8 -8
- package/README.md +144 -144
- package/RESERVERINGEN_GIDS.md +986 -986
- package/__tests__/crossMidnight.test.js +63 -0
- package/__tests__/crossMidnightTimeblocks.test.js +312 -0
- package/__tests__/edgeCases.test.js +271 -0
- package/__tests__/filters.test.js +276 -276
- package/__tests__/isDateAvailable.test.js +179 -175
- package/__tests__/isTimeAvailable.test.js +174 -168
- package/__tests__/restaurantData.test.js +422 -422
- package/__tests__/tableHelpers.test.js +247 -247
- package/assignTables.js +506 -444
- package/changes/2025/December/PR2___change.md +14 -14
- package/changes/2025/December/PR3_add__change.md +20 -20
- package/changes/2025/December/PR4___.md +15 -15
- package/changes/2025/December/PR5___.md +15 -15
- package/changes/2025/December/PR6__del_.md +17 -17
- package/changes/2025/December/PR7_add__change.md +21 -21
- package/changes/2026/February/PR15_add__change.md +21 -21
- package/changes/2026/February/PR16_add__.md +20 -0
- package/changes/2026/February/PR16_add_getDateClosingReasons.md +31 -31
- package/changes/2026/January/PR10_add__change.md +21 -21
- package/changes/2026/January/PR11_add__change.md +19 -19
- package/changes/2026/January/PR12_add__.md +21 -21
- package/changes/2026/January/PR13_add__change.md +20 -20
- package/changes/2026/January/PR14_add__change.md +19 -19
- package/changes/2026/January/PR8_add__change.md +38 -38
- package/changes/2026/January/PR9_add__change.md +19 -19
- package/dateHelpers.js +31 -0
- package/filters/maxArrivalsFilter.js +114 -114
- package/filters/maxGroupsFilter.js +221 -221
- package/filters/timeFilter.js +89 -89
- package/getAvailableTimeblocks.js +158 -158
- package/getDateClosingReasons.js +193 -193
- package/grouping.js +162 -162
- package/index.js +48 -43
- package/isDateAvailable.js +80 -80
- package/isDateAvailableWithTableCheck.js +172 -172
- package/isTimeAvailable.js +26 -26
- package/jest.config.js +23 -23
- package/package.json +27 -27
- package/processing/dailyGuestCounts.js +73 -73
- package/processing/mealTypeCount.js +133 -133
- package/processing/timeblocksAvailable.js +344 -182
- package/reservation_data/counter.js +82 -75
- package/restaurant_data/exceptions.js +150 -150
- package/restaurant_data/openinghours.js +142 -142
- package/simulateTableAssignment.js +833 -726
- package/tableHelpers.js +209 -209
- package/tables/time/parseTime.js +19 -19
- package/tables/time/shifts.js +7 -7
- package/tables/utils/calculateDistance.js +13 -13
- package/tables/utils/isTableFreeForAllSlots.js +14 -14
- package/tables/utils/isTemporaryTableValid.js +39 -39
- package/test/test_counter.js +194 -194
- package/test/test_dailyCount.js +81 -81
- package/test/test_datesAvailable.js +106 -106
- package/test/test_exceptions.js +172 -172
- package/test/test_isDateAvailable.js +330 -330
- package/test/test_mealTypeCount.js +54 -54
- package/test/test_timesAvailable.js +88 -88
- package/test-detailed-filter.js +100 -100
- package/test-lunch-debug.js +110 -110
- package/test-max-arrivals-filter.js +79 -79
- package/test-meal-stop-fix.js +147 -147
- package/test-meal-stop-simple.js +93 -93
- package/test-timezone-debug.js +47 -47
- package/test.js +336 -336
package/getDateClosingReasons.js
CHANGED
|
@@ -1,193 +1,193 @@
|
|
|
1
|
-
// getDateClosingReasons.js
|
|
2
|
-
//
|
|
3
|
-
// Diagnostic function that identifies the KEY reason a date is closed.
|
|
4
|
-
// Runs the same pipeline as Calendar.js's computeFinalTimeblocksCount,
|
|
5
|
-
// but sequentially tracks which stage eliminates all remaining slots.
|
|
6
|
-
//
|
|
7
|
-
// IMPORTANT: This pipeline must stay in sync with Calendar.js's
|
|
8
|
-
// computeFinalTimeblocksCount. If that pipeline changes, update this too.
|
|
9
|
-
|
|
10
|
-
const { timeblocksAvailable } = require('./processing/timeblocksAvailable');
|
|
11
|
-
const { getAvailableTimeblocks } = require('./getAvailableTimeblocks');
|
|
12
|
-
const { getDataByDateAndMealWithExceptions } = require('./restaurant_data/exceptions');
|
|
13
|
-
const { getDataByDateAndMeal } = require('./restaurant_data/openinghours');
|
|
14
|
-
const { filterTimeblocksByMaxGroups } = require('./filters/maxGroupsFilter');
|
|
15
|
-
const { filterTimeblocksByMaxArrivals } = require('./filters/maxArrivalsFilter');
|
|
16
|
-
const { filterTimeblocksByStopTimes } = require('./filters/timeFilter');
|
|
17
|
-
const { getAvailableTimeblocksWithTableCheck } = require('./simulateTableAssignment');
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Checks if a date is within the allowed future range defined by dagenInToekomst.
|
|
21
|
-
* Copied from isDateAvailable.js since it is not exported.
|
|
22
|
-
*/
|
|
23
|
-
function isDateWithinAllowedRange(data, dateStr) {
|
|
24
|
-
let dagenInToekomst = 90;
|
|
25
|
-
if (
|
|
26
|
-
data['general-settings'] &&
|
|
27
|
-
data['general-settings'].dagenInToekomst &&
|
|
28
|
-
parseInt(data['general-settings'].dagenInToekomst, 10) > 0
|
|
29
|
-
) {
|
|
30
|
-
dagenInToekomst = parseInt(data['general-settings'].dagenInToekomst, 10);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const timeZone = 'Europe/Amsterdam';
|
|
34
|
-
|
|
35
|
-
const now = new Date();
|
|
36
|
-
const currentTimeInTimeZone = new Date(
|
|
37
|
-
now.toLocaleString('en-US', { timeZone: timeZone })
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const maxAllowedDate = new Date(currentTimeInTimeZone.getTime());
|
|
41
|
-
maxAllowedDate.setDate(maxAllowedDate.getDate() + dagenInToekomst);
|
|
42
|
-
maxAllowedDate.setHours(23, 59, 59, 999);
|
|
43
|
-
|
|
44
|
-
const [year, month, day] = dateStr.split('-').map(Number);
|
|
45
|
-
const targetDate = new Date(Date.UTC(year, month - 1, day));
|
|
46
|
-
const targetDateInTimeZone = new Date(
|
|
47
|
-
targetDate.toLocaleString('en-US', { timeZone: timeZone })
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
return targetDateInTimeZone <= maxAllowedDate;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Diagnoses why a date has no available timeblocks.
|
|
55
|
-
* Runs the same pipeline as Calendar.js's computeFinalTimeblocksCount,
|
|
56
|
-
* but tracks at which stage slots go to zero to identify the key closing reason.
|
|
57
|
-
*
|
|
58
|
-
* @param {Object} data - Restaurant data object
|
|
59
|
-
* @param {string} dateStr - Date in "YYYY-MM-DD" format
|
|
60
|
-
* @param {Array} reservations - Existing reservations
|
|
61
|
-
* @param {number} guests - Number of guests for the reservation
|
|
62
|
-
* @param {Array} blockedSlots - Blocked time slots
|
|
63
|
-
* @param {string|null} giftcard - Selected giftcard or null
|
|
64
|
-
* @param {number|null} duration - Custom duration or null
|
|
65
|
-
* @param {string|null} zitplaats - Selected seating area or null
|
|
66
|
-
* @returns {Array<{type: string}>} Array of reason objects (typically single-element)
|
|
67
|
-
*/
|
|
68
|
-
function getDateClosingReasons(data, dateStr, reservations, guests, blockedSlots = [], giftcard = null, duration = null, zitplaats = null) {
|
|
69
|
-
const reasons = [];
|
|
70
|
-
|
|
71
|
-
// --- Stage 0: Date out of range ---
|
|
72
|
-
if (!isDateWithinAllowedRange(data, dateStr)) {
|
|
73
|
-
reasons.push({ type: 'DATE_OUT_OF_RANGE' });
|
|
74
|
-
return reasons;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// --- Stage 1: Opening hours & exceptions ---
|
|
78
|
-
// Check if any meal is open for this day (with exceptions applied)
|
|
79
|
-
const mealTypes = ['breakfast', 'lunch', 'dinner'];
|
|
80
|
-
let anyMealOpenWithExceptions = false;
|
|
81
|
-
let anyMealOpenWithoutExceptions = false;
|
|
82
|
-
|
|
83
|
-
for (const mealType of mealTypes) {
|
|
84
|
-
const withExceptions = getDataByDateAndMealWithExceptions(data, dateStr, mealType);
|
|
85
|
-
if (withExceptions) {
|
|
86
|
-
anyMealOpenWithExceptions = true;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const withoutExceptions = getDataByDateAndMeal(data, dateStr, mealType);
|
|
90
|
-
if (withoutExceptions) {
|
|
91
|
-
anyMealOpenWithoutExceptions = true;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (!anyMealOpenWithExceptions) {
|
|
96
|
-
if (anyMealOpenWithoutExceptions) {
|
|
97
|
-
// Meals are enabled in base opening hours but closed by exception rule
|
|
98
|
-
reasons.push({ type: 'EXCEPTION_CLOSURE' });
|
|
99
|
-
} else {
|
|
100
|
-
// No meals enabled at all for this day of the week
|
|
101
|
-
reasons.push({ type: 'OPENING_HOURS_DISABLED' });
|
|
102
|
-
}
|
|
103
|
-
return reasons;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// --- Stage 2: Base capacity (0 reservations, no blocked slots, no giftcard) ---
|
|
107
|
-
// Tests if the restaurant has enough base capacity for the group size
|
|
108
|
-
const baseTbs = timeblocksAvailable(data, dateStr, [], guests, [], null, false, duration);
|
|
109
|
-
if (Object.keys(baseTbs).length === 0) {
|
|
110
|
-
reasons.push({ type: 'CAPACITY_INSUFFICIENT' });
|
|
111
|
-
return reasons;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// --- Stage 3: With real reservations (no blocked slots, no giftcard) ---
|
|
115
|
-
// Tests if existing bookings have filled all capacity
|
|
116
|
-
const withResTbs = timeblocksAvailable(data, dateStr, reservations, guests, [], null, false, duration);
|
|
117
|
-
if (Object.keys(withResTbs).length === 0) {
|
|
118
|
-
reasons.push({ type: 'CAPACITY_REACHED' });
|
|
119
|
-
return reasons;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// --- Stage 4: With giftcard filter (if applicable) ---
|
|
123
|
-
if (giftcard) {
|
|
124
|
-
const withGcTbs = timeblocksAvailable(data, dateStr, reservations, guests, [], giftcard, false, duration);
|
|
125
|
-
if (Object.keys(withGcTbs).length === 0) {
|
|
126
|
-
reasons.push({ type: 'GIFTCARD_MISMATCH' });
|
|
127
|
-
return reasons;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// --- Stage 5: With blocked slots ---
|
|
132
|
-
// Full timeblocksAvailable call (capacity + giftcard + blocked slots)
|
|
133
|
-
const fullTbs = timeblocksAvailable(data, dateStr, reservations, guests, blockedSlots, giftcard, false, duration);
|
|
134
|
-
if (Object.keys(fullTbs).length === 0) {
|
|
135
|
-
reasons.push({ type: 'BLOCKED_SLOTS' });
|
|
136
|
-
return reasons;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// --- Stage 6: getAvailableTimeblocks (adds today-only filters: uurOpVoorhand + stop times) ---
|
|
140
|
-
let tbs = getAvailableTimeblocks(data, dateStr, reservations, guests, blockedSlots, giftcard, false, duration);
|
|
141
|
-
if (Object.keys(tbs).length === 0) {
|
|
142
|
-
reasons.push({ type: 'TODAY_TIME_RESTRICTION' });
|
|
143
|
-
return reasons;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// --- Stage 7: Max groups filter ---
|
|
147
|
-
tbs = filterTimeblocksByMaxGroups(data, dateStr, tbs, reservations, guests);
|
|
148
|
-
if (Object.keys(tbs).length === 0) {
|
|
149
|
-
reasons.push({ type: 'MAX_GROUPS' });
|
|
150
|
-
return reasons;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// --- Stage 8: Max arrivals filter ---
|
|
154
|
-
tbs = filterTimeblocksByMaxArrivals(data, dateStr, tbs, reservations, guests);
|
|
155
|
-
if (Object.keys(tbs).length === 0) {
|
|
156
|
-
reasons.push({ type: 'MAX_ARRIVALS' });
|
|
157
|
-
return reasons;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// --- Stage 9: Table assignment (if enabled) ---
|
|
161
|
-
const tableSettings = data?.['table-settings'] || {};
|
|
162
|
-
const isTableAssignmentEnabled =
|
|
163
|
-
tableSettings.isInstalled === true && tableSettings.assignmentMode === 'automatic';
|
|
164
|
-
|
|
165
|
-
if (isTableAssignmentEnabled) {
|
|
166
|
-
const tableTbs = getAvailableTimeblocksWithTableCheck(data, dateStr, tbs, guests, reservations, zitplaats, duration);
|
|
167
|
-
if (Object.keys(tableTbs).length === 0) {
|
|
168
|
-
reasons.push({ type: 'TABLE_PLAN' });
|
|
169
|
-
return reasons;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Re-apply max arrivals after table check (matching Calendar pipeline)
|
|
173
|
-
tbs = filterTimeblocksByMaxArrivals(data, dateStr, tableTbs, reservations, guests);
|
|
174
|
-
if (Object.keys(tbs).length === 0) {
|
|
175
|
-
reasons.push({ type: 'MAX_ARRIVALS' });
|
|
176
|
-
return reasons;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// --- Stage 10: Stop times filter ---
|
|
181
|
-
tbs = filterTimeblocksByStopTimes(tbs, data, dateStr);
|
|
182
|
-
if (Object.keys(tbs).length === 0) {
|
|
183
|
-
reasons.push({ type: 'TODAY_TIME_RESTRICTION' });
|
|
184
|
-
return reasons;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// If we get here, the date is actually available (reasons array is empty)
|
|
188
|
-
return reasons;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
module.exports = {
|
|
192
|
-
getDateClosingReasons,
|
|
193
|
-
};
|
|
1
|
+
// getDateClosingReasons.js
|
|
2
|
+
//
|
|
3
|
+
// Diagnostic function that identifies the KEY reason a date is closed.
|
|
4
|
+
// Runs the same pipeline as Calendar.js's computeFinalTimeblocksCount,
|
|
5
|
+
// but sequentially tracks which stage eliminates all remaining slots.
|
|
6
|
+
//
|
|
7
|
+
// IMPORTANT: This pipeline must stay in sync with Calendar.js's
|
|
8
|
+
// computeFinalTimeblocksCount. If that pipeline changes, update this too.
|
|
9
|
+
|
|
10
|
+
const { timeblocksAvailable } = require('./processing/timeblocksAvailable');
|
|
11
|
+
const { getAvailableTimeblocks } = require('./getAvailableTimeblocks');
|
|
12
|
+
const { getDataByDateAndMealWithExceptions } = require('./restaurant_data/exceptions');
|
|
13
|
+
const { getDataByDateAndMeal } = require('./restaurant_data/openinghours');
|
|
14
|
+
const { filterTimeblocksByMaxGroups } = require('./filters/maxGroupsFilter');
|
|
15
|
+
const { filterTimeblocksByMaxArrivals } = require('./filters/maxArrivalsFilter');
|
|
16
|
+
const { filterTimeblocksByStopTimes } = require('./filters/timeFilter');
|
|
17
|
+
const { getAvailableTimeblocksWithTableCheck } = require('./simulateTableAssignment');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks if a date is within the allowed future range defined by dagenInToekomst.
|
|
21
|
+
* Copied from isDateAvailable.js since it is not exported.
|
|
22
|
+
*/
|
|
23
|
+
function isDateWithinAllowedRange(data, dateStr) {
|
|
24
|
+
let dagenInToekomst = 90;
|
|
25
|
+
if (
|
|
26
|
+
data['general-settings'] &&
|
|
27
|
+
data['general-settings'].dagenInToekomst &&
|
|
28
|
+
parseInt(data['general-settings'].dagenInToekomst, 10) > 0
|
|
29
|
+
) {
|
|
30
|
+
dagenInToekomst = parseInt(data['general-settings'].dagenInToekomst, 10);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const timeZone = 'Europe/Amsterdam';
|
|
34
|
+
|
|
35
|
+
const now = new Date();
|
|
36
|
+
const currentTimeInTimeZone = new Date(
|
|
37
|
+
now.toLocaleString('en-US', { timeZone: timeZone })
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const maxAllowedDate = new Date(currentTimeInTimeZone.getTime());
|
|
41
|
+
maxAllowedDate.setDate(maxAllowedDate.getDate() + dagenInToekomst);
|
|
42
|
+
maxAllowedDate.setHours(23, 59, 59, 999);
|
|
43
|
+
|
|
44
|
+
const [year, month, day] = dateStr.split('-').map(Number);
|
|
45
|
+
const targetDate = new Date(Date.UTC(year, month - 1, day));
|
|
46
|
+
const targetDateInTimeZone = new Date(
|
|
47
|
+
targetDate.toLocaleString('en-US', { timeZone: timeZone })
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return targetDateInTimeZone <= maxAllowedDate;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Diagnoses why a date has no available timeblocks.
|
|
55
|
+
* Runs the same pipeline as Calendar.js's computeFinalTimeblocksCount,
|
|
56
|
+
* but tracks at which stage slots go to zero to identify the key closing reason.
|
|
57
|
+
*
|
|
58
|
+
* @param {Object} data - Restaurant data object
|
|
59
|
+
* @param {string} dateStr - Date in "YYYY-MM-DD" format
|
|
60
|
+
* @param {Array} reservations - Existing reservations
|
|
61
|
+
* @param {number} guests - Number of guests for the reservation
|
|
62
|
+
* @param {Array} blockedSlots - Blocked time slots
|
|
63
|
+
* @param {string|null} giftcard - Selected giftcard or null
|
|
64
|
+
* @param {number|null} duration - Custom duration or null
|
|
65
|
+
* @param {string|null} zitplaats - Selected seating area or null
|
|
66
|
+
* @returns {Array<{type: string}>} Array of reason objects (typically single-element)
|
|
67
|
+
*/
|
|
68
|
+
function getDateClosingReasons(data, dateStr, reservations, guests, blockedSlots = [], giftcard = null, duration = null, zitplaats = null) {
|
|
69
|
+
const reasons = [];
|
|
70
|
+
|
|
71
|
+
// --- Stage 0: Date out of range ---
|
|
72
|
+
if (!isDateWithinAllowedRange(data, dateStr)) {
|
|
73
|
+
reasons.push({ type: 'DATE_OUT_OF_RANGE' });
|
|
74
|
+
return reasons;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- Stage 1: Opening hours & exceptions ---
|
|
78
|
+
// Check if any meal is open for this day (with exceptions applied)
|
|
79
|
+
const mealTypes = ['breakfast', 'lunch', 'dinner'];
|
|
80
|
+
let anyMealOpenWithExceptions = false;
|
|
81
|
+
let anyMealOpenWithoutExceptions = false;
|
|
82
|
+
|
|
83
|
+
for (const mealType of mealTypes) {
|
|
84
|
+
const withExceptions = getDataByDateAndMealWithExceptions(data, dateStr, mealType);
|
|
85
|
+
if (withExceptions) {
|
|
86
|
+
anyMealOpenWithExceptions = true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const withoutExceptions = getDataByDateAndMeal(data, dateStr, mealType);
|
|
90
|
+
if (withoutExceptions) {
|
|
91
|
+
anyMealOpenWithoutExceptions = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!anyMealOpenWithExceptions) {
|
|
96
|
+
if (anyMealOpenWithoutExceptions) {
|
|
97
|
+
// Meals are enabled in base opening hours but closed by exception rule
|
|
98
|
+
reasons.push({ type: 'EXCEPTION_CLOSURE' });
|
|
99
|
+
} else {
|
|
100
|
+
// No meals enabled at all for this day of the week
|
|
101
|
+
reasons.push({ type: 'OPENING_HOURS_DISABLED' });
|
|
102
|
+
}
|
|
103
|
+
return reasons;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Stage 2: Base capacity (0 reservations, no blocked slots, no giftcard) ---
|
|
107
|
+
// Tests if the restaurant has enough base capacity for the group size
|
|
108
|
+
const baseTbs = timeblocksAvailable(data, dateStr, [], guests, [], null, false, duration);
|
|
109
|
+
if (Object.keys(baseTbs).length === 0) {
|
|
110
|
+
reasons.push({ type: 'CAPACITY_INSUFFICIENT' });
|
|
111
|
+
return reasons;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- Stage 3: With real reservations (no blocked slots, no giftcard) ---
|
|
115
|
+
// Tests if existing bookings have filled all capacity
|
|
116
|
+
const withResTbs = timeblocksAvailable(data, dateStr, reservations, guests, [], null, false, duration);
|
|
117
|
+
if (Object.keys(withResTbs).length === 0) {
|
|
118
|
+
reasons.push({ type: 'CAPACITY_REACHED' });
|
|
119
|
+
return reasons;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- Stage 4: With giftcard filter (if applicable) ---
|
|
123
|
+
if (giftcard) {
|
|
124
|
+
const withGcTbs = timeblocksAvailable(data, dateStr, reservations, guests, [], giftcard, false, duration);
|
|
125
|
+
if (Object.keys(withGcTbs).length === 0) {
|
|
126
|
+
reasons.push({ type: 'GIFTCARD_MISMATCH' });
|
|
127
|
+
return reasons;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- Stage 5: With blocked slots ---
|
|
132
|
+
// Full timeblocksAvailable call (capacity + giftcard + blocked slots)
|
|
133
|
+
const fullTbs = timeblocksAvailable(data, dateStr, reservations, guests, blockedSlots, giftcard, false, duration);
|
|
134
|
+
if (Object.keys(fullTbs).length === 0) {
|
|
135
|
+
reasons.push({ type: 'BLOCKED_SLOTS' });
|
|
136
|
+
return reasons;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- Stage 6: getAvailableTimeblocks (adds today-only filters: uurOpVoorhand + stop times) ---
|
|
140
|
+
let tbs = getAvailableTimeblocks(data, dateStr, reservations, guests, blockedSlots, giftcard, false, duration);
|
|
141
|
+
if (Object.keys(tbs).length === 0) {
|
|
142
|
+
reasons.push({ type: 'TODAY_TIME_RESTRICTION' });
|
|
143
|
+
return reasons;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- Stage 7: Max groups filter ---
|
|
147
|
+
tbs = filterTimeblocksByMaxGroups(data, dateStr, tbs, reservations, guests);
|
|
148
|
+
if (Object.keys(tbs).length === 0) {
|
|
149
|
+
reasons.push({ type: 'MAX_GROUPS' });
|
|
150
|
+
return reasons;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- Stage 8: Max arrivals filter ---
|
|
154
|
+
tbs = filterTimeblocksByMaxArrivals(data, dateStr, tbs, reservations, guests);
|
|
155
|
+
if (Object.keys(tbs).length === 0) {
|
|
156
|
+
reasons.push({ type: 'MAX_ARRIVALS' });
|
|
157
|
+
return reasons;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- Stage 9: Table assignment (if enabled) ---
|
|
161
|
+
const tableSettings = data?.['table-settings'] || {};
|
|
162
|
+
const isTableAssignmentEnabled =
|
|
163
|
+
tableSettings.isInstalled === true && tableSettings.assignmentMode === 'automatic';
|
|
164
|
+
|
|
165
|
+
if (isTableAssignmentEnabled) {
|
|
166
|
+
const tableTbs = getAvailableTimeblocksWithTableCheck(data, dateStr, tbs, guests, reservations, zitplaats, duration);
|
|
167
|
+
if (Object.keys(tableTbs).length === 0) {
|
|
168
|
+
reasons.push({ type: 'TABLE_PLAN' });
|
|
169
|
+
return reasons;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Re-apply max arrivals after table check (matching Calendar pipeline)
|
|
173
|
+
tbs = filterTimeblocksByMaxArrivals(data, dateStr, tableTbs, reservations, guests);
|
|
174
|
+
if (Object.keys(tbs).length === 0) {
|
|
175
|
+
reasons.push({ type: 'MAX_ARRIVALS' });
|
|
176
|
+
return reasons;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- Stage 10: Stop times filter ---
|
|
181
|
+
tbs = filterTimeblocksByStopTimes(tbs, data, dateStr);
|
|
182
|
+
if (Object.keys(tbs).length === 0) {
|
|
183
|
+
reasons.push({ type: 'TODAY_TIME_RESTRICTION' });
|
|
184
|
+
return reasons;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// If we get here, the date is actually available (reasons array is empty)
|
|
188
|
+
return reasons;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
getDateClosingReasons,
|
|
193
|
+
};
|