@happychef/algorithm 1.4.0 → 1.4.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/BRANCH_PROTECTION_SETUP.md +167 -167
- package/README.md +144 -144
- package/__tests__/filters.test.js +276 -276
- package/__tests__/isDateAvailable.test.js +175 -179
- package/__tests__/isTimeAvailable.test.js +168 -174
- package/__tests__/restaurantData.test.js +422 -422
- package/__tests__/tableHelpers.test.js +247 -247
- package/assignTables.js +0 -62
- package/bundle_entry.js +100 -0
- package/getAvailableTimeblocks.js +12 -0
- package/index.js +1 -7
- package/jest.config.js +23 -23
- package/moment-timezone-shim.js +179 -0
- package/nul +0 -0
- package/package.json +1 -1
- package/processing/timeblocksAvailable.js +26 -183
- package/reservation_data/counter.js +60 -67
- package/restaurant_data/openinghours.js +23 -0
- package/simulateTableAssignment.js +2 -108
- package/tableHelpers.js +5 -2
- package/test-detailed-filter.js +100 -100
- package/test-lunch-debug.js +110 -110
- package/test-max-arrivals-filter.js +79 -79
- package/test-timezone-debug.js +47 -47
- package/.claude/settings.local.json +0 -16
- package/__tests__/crossMidnight.test.js +0 -63
- package/__tests__/crossMidnightTimeblocks.test.js +0 -312
- package/__tests__/edgeCases.test.js +0 -271
- package/changes/2026/February/PR16_add__.md +0 -20
- package/changes/2026/February/PR16_add_getDateClosingReasons.md +0 -31
- package/dateHelpers.js +0 -31
- package/getDateClosingReasons.js +0 -193
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
// __tests__/crossMidnightTimeblocks.test.js
|
|
2
|
-
// Tests for timeblocksAvailable cross-midnight slot generation
|
|
3
|
-
|
|
4
|
-
jest.mock('../restaurant_data/exceptions', () => ({
|
|
5
|
-
getDataByDateAndMealWithExceptions: jest.fn(),
|
|
6
|
-
getDataByDateAndTimeWithExceptions: jest.fn(),
|
|
7
|
-
}));
|
|
8
|
-
|
|
9
|
-
jest.mock('../restaurant_data/openinghours', () => ({
|
|
10
|
-
getMealTypeByTime: jest.fn(),
|
|
11
|
-
parseTime: jest.fn((timeStr) => {
|
|
12
|
-
const [h, m] = timeStr.split(':').map(Number);
|
|
13
|
-
return h * 60 + m;
|
|
14
|
-
}),
|
|
15
|
-
daysOfWeekEnglish: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
jest.mock('../processing/dailyGuestCounts', () => ({
|
|
19
|
-
getDailyGuestCounts: jest.fn(),
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
jest.mock('../processing/mealTypeCount', () => ({
|
|
23
|
-
getMealTypesWithShifts: jest.fn(),
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
jest.mock('../reservation_data/counter', () => ({
|
|
27
|
-
getGuestCountAtHour: jest.fn(),
|
|
28
|
-
}));
|
|
29
|
-
|
|
30
|
-
const { timeblocksAvailable } = require('../processing/timeblocksAvailable');
|
|
31
|
-
const { getDataByDateAndMealWithExceptions } = require('../restaurant_data/exceptions');
|
|
32
|
-
const { getDailyGuestCounts } = require('../processing/dailyGuestCounts');
|
|
33
|
-
const { getMealTypesWithShifts } = require('../processing/mealTypeCount');
|
|
34
|
-
const { getGuestCountAtHour: mockGetGuestCountAtHour } = require('../reservation_data/counter');
|
|
35
|
-
const { getMealTypeByTime } = require('../restaurant_data/openinghours');
|
|
36
|
-
|
|
37
|
-
describe('timeblocksAvailable - cross-midnight', () => {
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
jest.clearAllMocks();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// Per-day format: hoursOpenedPastMidnight in openinghours-dinner.schemeSettings
|
|
43
|
-
function setupBowlingData(hoursOpenedPastMidnight = 3) {
|
|
44
|
-
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
|
45
|
-
const schemeSettings = {};
|
|
46
|
-
for (const day of days) {
|
|
47
|
-
schemeSettings[day] = { hoursOpenedPastMidnight };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const data = {
|
|
51
|
-
'general-settings': {
|
|
52
|
-
duurReservatie: '60',
|
|
53
|
-
intervalReservatie: '30',
|
|
54
|
-
},
|
|
55
|
-
'openinghours-dinner': {
|
|
56
|
-
schemeSettings,
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// Bowling restaurant: dinner 16:00-24:00
|
|
61
|
-
getDataByDateAndMealWithExceptions.mockImplementation((_data, dateStr, mealType) => {
|
|
62
|
-
if (mealType === 'dinner') {
|
|
63
|
-
return {
|
|
64
|
-
enabled: true,
|
|
65
|
-
startTime: '16:00',
|
|
66
|
-
endTime: '24:00',
|
|
67
|
-
maxCapacity: 40,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
return null;
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
getMealTypesWithShifts.mockReturnValue([]);
|
|
74
|
-
|
|
75
|
-
return data;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Per-day format with different values per day
|
|
79
|
-
function setupPerDayData(perDayValues) {
|
|
80
|
-
const schemeSettings = {};
|
|
81
|
-
for (const [day, val] of Object.entries(perDayValues)) {
|
|
82
|
-
schemeSettings[day] = { hoursOpenedPastMidnight: val };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const data = {
|
|
86
|
-
'general-settings': {
|
|
87
|
-
duurReservatie: '60',
|
|
88
|
-
intervalReservatie: '30',
|
|
89
|
-
},
|
|
90
|
-
'openinghours-dinner': {
|
|
91
|
-
schemeSettings,
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
getDataByDateAndMealWithExceptions.mockImplementation((_data, dateStr, mealType) => {
|
|
96
|
-
if (mealType === 'dinner') {
|
|
97
|
-
return {
|
|
98
|
-
enabled: true,
|
|
99
|
-
startTime: '16:00',
|
|
100
|
-
endTime: '24:00',
|
|
101
|
-
maxCapacity: 40,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
return null;
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
getMealTypesWithShifts.mockReturnValue([]);
|
|
108
|
-
|
|
109
|
-
return data;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function makeDinnerGuestCounts() {
|
|
113
|
-
const guestCounts = {};
|
|
114
|
-
for (let t = 960; t <= 1410; t += 30) {
|
|
115
|
-
const h = Math.floor(t / 60);
|
|
116
|
-
const m = t % 60;
|
|
117
|
-
const key = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
|
118
|
-
guestCounts[key] = 38; // 40 - 2 = 38 seats available
|
|
119
|
-
}
|
|
120
|
-
return guestCounts;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
test('generates late-evening slots when hoursOpenedPastMidnight is set', () => {
|
|
124
|
-
const data = setupBowlingData(3);
|
|
125
|
-
|
|
126
|
-
getDailyGuestCounts.mockReturnValue({
|
|
127
|
-
guestCounts: makeDinnerGuestCounts(),
|
|
128
|
-
shiftsInfo: [],
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
mockGetGuestCountAtHour.mockReturnValue(0);
|
|
132
|
-
|
|
133
|
-
// 2025-02-20 is Thursday
|
|
134
|
-
const result = timeblocksAvailable(data, '2025-02-20', [], 2, [], null, false, 180);
|
|
135
|
-
|
|
136
|
-
// 23:00 + 180min = 02:00, within 3h limit
|
|
137
|
-
expect(result['23:00']).toBeDefined();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test('rejects late slot when next-day spillover guests exceed capacity', () => {
|
|
141
|
-
const data = setupBowlingData(3);
|
|
142
|
-
|
|
143
|
-
getDailyGuestCounts.mockReturnValue({
|
|
144
|
-
guestCounts: makeDinnerGuestCounts(),
|
|
145
|
-
shiftsInfo: [],
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// Next day has 40 guests already (full capacity)
|
|
149
|
-
mockGetGuestCountAtHour.mockImplementation((_data, _res, timeStr, dateStr) => {
|
|
150
|
-
if (dateStr === '2025-02-21') return 40;
|
|
151
|
-
return 0;
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const result = timeblocksAvailable(data, '2025-02-20', [], 2, [], null, false, 180);
|
|
155
|
-
|
|
156
|
-
expect(result['23:00']).toBeUndefined();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test('rejects cross-midnight when hoursOpenedPastMidnight is 0', () => {
|
|
160
|
-
const data = setupBowlingData(0);
|
|
161
|
-
|
|
162
|
-
getDailyGuestCounts.mockReturnValue({
|
|
163
|
-
guestCounts: makeDinnerGuestCounts(),
|
|
164
|
-
shiftsInfo: [],
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
mockGetGuestCountAtHour.mockReturnValue(0);
|
|
168
|
-
|
|
169
|
-
const result = timeblocksAvailable(data, '2025-02-20', [], 2, [], null, false, 180);
|
|
170
|
-
|
|
171
|
-
// 23:00 + 180min crosses midnight, but hoursOpenedPastMidnight=0 disallows it
|
|
172
|
-
expect(result['23:00']).toBeUndefined();
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test('rejects slot when spillover exceeds hoursOpenedPastMidnight limit', () => {
|
|
176
|
-
const data = setupBowlingData(1); // Only 1 hour past midnight
|
|
177
|
-
|
|
178
|
-
getDailyGuestCounts.mockReturnValue({
|
|
179
|
-
guestCounts: makeDinnerGuestCounts(),
|
|
180
|
-
shiftsInfo: [],
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
mockGetGuestCountAtHour.mockReturnValue(0);
|
|
184
|
-
|
|
185
|
-
const result = timeblocksAvailable(data, '2025-02-20', [], 2, [], null, false, 180);
|
|
186
|
-
|
|
187
|
-
// 23:00 + 180min = 02:00, which is 2h past midnight > 1h limit
|
|
188
|
-
expect(result['23:00']).toBeUndefined();
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test('per-day: Friday allows cross-midnight, Monday does not', () => {
|
|
192
|
-
// Mon-Thu: 0, Fri-Sun: 2
|
|
193
|
-
const data = setupPerDayData({
|
|
194
|
-
Monday: 0, Tuesday: 0, Wednesday: 0, Thursday: 0,
|
|
195
|
-
Friday: 2, Saturday: 2, Sunday: 2,
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
getDailyGuestCounts.mockReturnValue({
|
|
199
|
-
guestCounts: makeDinnerGuestCounts(),
|
|
200
|
-
shiftsInfo: [],
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
mockGetGuestCountAtHour.mockReturnValue(0);
|
|
204
|
-
|
|
205
|
-
// 2025-02-21 is Friday
|
|
206
|
-
const fridayResult = timeblocksAvailable(data, '2025-02-21', [], 2, [], null, false, 180);
|
|
207
|
-
// 23:00 + 180min = 02:00, within 2h limit on Friday
|
|
208
|
-
expect(fridayResult['23:00']).toBeDefined();
|
|
209
|
-
|
|
210
|
-
// 2025-02-17 is Monday
|
|
211
|
-
const mondayResult = timeblocksAvailable(data, '2025-02-17', [], 2, [], null, false, 180);
|
|
212
|
-
// 23:00 + 180min = 02:00, but hoursOpenedPastMidnight=0 on Monday
|
|
213
|
-
expect(mondayResult['23:00']).toBeUndefined();
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test('backward compat: reads from general-settings when dinner schemeSettings missing', () => {
|
|
217
|
-
// Old format: hoursOpenedPastMidnight in general-settings only
|
|
218
|
-
const data = {
|
|
219
|
-
'general-settings': {
|
|
220
|
-
duurReservatie: '60',
|
|
221
|
-
intervalReservatie: '30',
|
|
222
|
-
hoursOpenedPastMidnight: '3',
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
getDataByDateAndMealWithExceptions.mockImplementation((_data, dateStr, mealType) => {
|
|
227
|
-
if (mealType === 'dinner') {
|
|
228
|
-
return {
|
|
229
|
-
enabled: true,
|
|
230
|
-
startTime: '16:00',
|
|
231
|
-
endTime: '24:00',
|
|
232
|
-
maxCapacity: 40,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
return null;
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
getMealTypesWithShifts.mockReturnValue([]);
|
|
239
|
-
|
|
240
|
-
getDailyGuestCounts.mockReturnValue({
|
|
241
|
-
guestCounts: makeDinnerGuestCounts(),
|
|
242
|
-
shiftsInfo: [],
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
mockGetGuestCountAtHour.mockReturnValue(0);
|
|
246
|
-
|
|
247
|
-
const result = timeblocksAvailable(data, '2025-02-20', [], 2, [], null, false, 180);
|
|
248
|
-
|
|
249
|
-
// Should still work via general-settings fallback
|
|
250
|
-
expect(result['23:00']).toBeDefined();
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
test('non-bowling: 22:30 is last slot, 23:00 not bookable', () => {
|
|
254
|
-
const data = {
|
|
255
|
-
'general-settings': {
|
|
256
|
-
duurReservatie: '120',
|
|
257
|
-
intervalReservatie: '30',
|
|
258
|
-
},
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
getDataByDateAndMealWithExceptions.mockImplementation((_data, dateStr, mealType) => {
|
|
262
|
-
if (mealType === 'dinner') {
|
|
263
|
-
return {
|
|
264
|
-
enabled: true,
|
|
265
|
-
startTime: '16:00',
|
|
266
|
-
endTime: '25:00', // 23:00 + 120min default (via addDuurReservatieToEndTime)
|
|
267
|
-
maxCapacity: 40,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
return null;
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
getMealTypesWithShifts.mockReturnValue([]);
|
|
274
|
-
|
|
275
|
-
// Mimic original production shift boundaries (strict < for end)
|
|
276
|
-
getMealTypeByTime.mockImplementation((timeStr) => {
|
|
277
|
-
const [h, m] = timeStr.split(':').map(Number);
|
|
278
|
-
const time = h * 60 + m;
|
|
279
|
-
if (time >= 420 && time < 660) return 'breakfast';
|
|
280
|
-
if (time >= 660 && time < 960) return 'lunch';
|
|
281
|
-
if (time >= 960 && time < 1380) return 'dinner';
|
|
282
|
-
return null;
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
// guestCounts covers the full extended meal period
|
|
286
|
-
const guestCounts = {};
|
|
287
|
-
for (let t = 960; t <= 1500; t += 30) {
|
|
288
|
-
const h = Math.floor(t / 60);
|
|
289
|
-
const m = t % 60;
|
|
290
|
-
const key = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
|
291
|
-
guestCounts[key] = 38;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
getDailyGuestCounts.mockReturnValue({
|
|
295
|
-
guestCounts,
|
|
296
|
-
shiftsInfo: [],
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
mockGetGuestCountAtHour.mockReturnValue(0);
|
|
300
|
-
|
|
301
|
-
const result = timeblocksAvailable(data, '2025-02-20', [], 2, [], null, false, null);
|
|
302
|
-
|
|
303
|
-
// Standard restaurant: 18:00 should be available
|
|
304
|
-
expect(result['18:00']).toBeDefined();
|
|
305
|
-
// 22:30 is last valid slot (getMealTypeByTime returns 'dinner' for < 23:00)
|
|
306
|
-
expect(result['22:30']).toBeDefined();
|
|
307
|
-
// 23:00: getMealTypeByTime returns null (strict < 23:00) → not bookable
|
|
308
|
-
expect(result['23:00']).toBeUndefined();
|
|
309
|
-
expect(result['23:30']).toBeUndefined();
|
|
310
|
-
expect(result['24:00']).toBeUndefined();
|
|
311
|
-
});
|
|
312
|
-
});
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
// __tests__/edgeCases.test.js
|
|
2
|
-
// Edge case tests for bowling vs non-bowling and per-day hoursOpenedPastMidnight
|
|
3
|
-
|
|
4
|
-
jest.mock('../restaurant_data/exceptions', () => ({
|
|
5
|
-
getDataByDateAndMealWithExceptions: jest.fn(),
|
|
6
|
-
getDataByDateAndTimeWithExceptions: jest.fn(),
|
|
7
|
-
}));
|
|
8
|
-
|
|
9
|
-
jest.mock('../restaurant_data/openinghours', () => ({
|
|
10
|
-
getMealTypeByTime: jest.fn(),
|
|
11
|
-
parseTime: jest.fn((timeStr) => {
|
|
12
|
-
const [h, m] = timeStr.split(':').map(Number);
|
|
13
|
-
return h * 60 + m;
|
|
14
|
-
}),
|
|
15
|
-
daysOfWeekEnglish: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
jest.mock('../processing/dailyGuestCounts', () => ({
|
|
19
|
-
getDailyGuestCounts: jest.fn(),
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
jest.mock('../processing/mealTypeCount', () => ({
|
|
23
|
-
getMealTypesWithShifts: jest.fn(),
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
jest.mock('../reservation_data/counter', () => ({
|
|
27
|
-
getGuestCountAtHour: jest.fn(),
|
|
28
|
-
}));
|
|
29
|
-
|
|
30
|
-
const { timeblocksAvailable } = require('../processing/timeblocksAvailable');
|
|
31
|
-
const { getDataByDateAndMealWithExceptions } = require('../restaurant_data/exceptions');
|
|
32
|
-
const { getDailyGuestCounts } = require('../processing/dailyGuestCounts');
|
|
33
|
-
const { getMealTypesWithShifts } = require('../processing/mealTypeCount');
|
|
34
|
-
const { getGuestCountAtHour: mockGetGuestCountAtHour } = require('../reservation_data/counter');
|
|
35
|
-
const { getMealTypeByTime } = require('../restaurant_data/openinghours');
|
|
36
|
-
|
|
37
|
-
// Helper: generate guestCounts from startMin to endMin (inclusive) at given interval
|
|
38
|
-
function makeGuestCounts(startMin, endMin, interval, available = 38) {
|
|
39
|
-
const gc = {};
|
|
40
|
-
for (let t = startMin; t <= endMin; t += interval) {
|
|
41
|
-
const h = Math.floor(t / 60);
|
|
42
|
-
const m = t % 60;
|
|
43
|
-
gc[`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`] = available;
|
|
44
|
-
}
|
|
45
|
-
return gc;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
describe('Edge cases: non-bowling restaurants', () => {
|
|
49
|
-
beforeEach(() => {
|
|
50
|
-
jest.clearAllMocks();
|
|
51
|
-
getMealTypesWithShifts.mockReturnValue([]);
|
|
52
|
-
mockGetGuestCountAtHour.mockReturnValue(0);
|
|
53
|
-
// Mimic original production shift boundaries (strict < for end)
|
|
54
|
-
getMealTypeByTime.mockImplementation((timeStr) => {
|
|
55
|
-
const [h, m] = timeStr.split(':').map(Number);
|
|
56
|
-
const time = h * 60 + m;
|
|
57
|
-
if (time >= 420 && time < 660) return 'breakfast';
|
|
58
|
-
if (time >= 660 && time < 960) return 'lunch';
|
|
59
|
-
if (time >= 960 && time < 1380) return 'dinner';
|
|
60
|
-
return null;
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('regular restaurant (no midnight setting): 22:45 is last slot, 23:00 not bookable', () => {
|
|
65
|
-
const data = {
|
|
66
|
-
'general-settings': {
|
|
67
|
-
duurReservatie: '120',
|
|
68
|
-
intervalReservatie: '15',
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
getDataByDateAndMealWithExceptions.mockImplementation((_data, _date, mealType) => {
|
|
73
|
-
if (mealType === 'dinner') {
|
|
74
|
-
return { enabled: true, startTime: '16:00', endTime: '25:00', maxCapacity: 40 };
|
|
75
|
-
}
|
|
76
|
-
return null;
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// guestCounts 16:00 to 25:00 at 15min interval
|
|
80
|
-
getDailyGuestCounts.mockReturnValue({
|
|
81
|
-
guestCounts: makeGuestCounts(960, 1500, 15),
|
|
82
|
-
shiftsInfo: [],
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const result = timeblocksAvailable(data, '2025-02-20', [], 2, [], null, false, null);
|
|
86
|
-
|
|
87
|
-
expect(result['22:00']).toBeDefined();
|
|
88
|
-
expect(result['22:45']).toBeDefined(); // last valid slot (getMealTypeByTime returns 'dinner' for < 23:00)
|
|
89
|
-
expect(result['23:00']).toBeUndefined(); // getMealTypeByTime returns null for 23:00 (strict <)
|
|
90
|
-
expect(result['23:15']).toBeUndefined();
|
|
91
|
-
expect(result['23:30']).toBeUndefined();
|
|
92
|
-
expect(result['24:00']).toBeUndefined();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test('regular restaurant with 15min interval and 60min duration: 22:45 is last slot', () => {
|
|
96
|
-
const data = {
|
|
97
|
-
'general-settings': {
|
|
98
|
-
duurReservatie: '60',
|
|
99
|
-
intervalReservatie: '15',
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
getDataByDateAndMealWithExceptions.mockImplementation((_data, _date, mealType) => {
|
|
104
|
-
if (mealType === 'dinner') {
|
|
105
|
-
// 23:00 + 60min extension = 24:00
|
|
106
|
-
return { enabled: true, startTime: '16:00', endTime: '24:00', maxCapacity: 40 };
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
getDailyGuestCounts.mockReturnValue({
|
|
112
|
-
guestCounts: makeGuestCounts(960, 1440, 15),
|
|
113
|
-
shiftsInfo: [],
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const result = timeblocksAvailable(data, '2025-02-20', [], 2, [], null, false, null);
|
|
117
|
-
|
|
118
|
-
expect(result['22:45']).toBeDefined(); // last valid (getMealTypeByTime returns 'dinner')
|
|
119
|
-
expect(result['23:00']).toBeUndefined(); // getMealTypeByTime returns null for 23:00
|
|
120
|
-
expect(result['23:15']).toBeUndefined();
|
|
121
|
-
expect(result['23:30']).toBeUndefined();
|
|
122
|
-
expect(result['24:00']).toBeUndefined();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test('regular restaurant lunch: no slots after lunch closing time', () => {
|
|
126
|
-
const data = {
|
|
127
|
-
'general-settings': {
|
|
128
|
-
duurReservatie: '120',
|
|
129
|
-
intervalReservatie: '15',
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
getDataByDateAndMealWithExceptions.mockImplementation((_data, _date, mealType) => {
|
|
134
|
-
if (mealType === 'lunch') {
|
|
135
|
-
// lunch 11:00-14:00, extended by 120min to 16:00
|
|
136
|
-
return { enabled: true, startTime: '11:00', endTime: '16:00', maxCapacity: 40 };
|
|
137
|
-
}
|
|
138
|
-
return null;
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
getDailyGuestCounts.mockReturnValue({
|
|
142
|
-
guestCounts: makeGuestCounts(660, 960, 15),
|
|
143
|
-
shiftsInfo: [],
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const result = timeblocksAvailable(data, '2025-02-20', [], 2, [], null, false, null);
|
|
147
|
-
|
|
148
|
-
expect(result['13:45']).toBeDefined();
|
|
149
|
-
expect(result['14:00']).toBeDefined(); // original closing
|
|
150
|
-
expect(result['14:15']).toBeUndefined(); // past closing
|
|
151
|
-
expect(result['15:00']).toBeUndefined();
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
describe('Edge cases: bowling per-day', () => {
|
|
156
|
-
beforeEach(() => {
|
|
157
|
-
jest.clearAllMocks();
|
|
158
|
-
getMealTypesWithShifts.mockReturnValue([]);
|
|
159
|
-
mockGetGuestCountAtHour.mockReturnValue(0);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test('Friday=1hr, Saturday=2hr: Saturday allows more spillover than Friday', () => {
|
|
163
|
-
const data = {
|
|
164
|
-
'general-settings': { duurReservatie: '60', intervalReservatie: '15' },
|
|
165
|
-
'openinghours-dinner': {
|
|
166
|
-
schemeSettings: {
|
|
167
|
-
Monday: { hoursOpenedPastMidnight: 0 },
|
|
168
|
-
Tuesday: { hoursOpenedPastMidnight: 0 },
|
|
169
|
-
Wednesday: { hoursOpenedPastMidnight: 0 },
|
|
170
|
-
Thursday: { hoursOpenedPastMidnight: 0 },
|
|
171
|
-
Friday: { hoursOpenedPastMidnight: 1 },
|
|
172
|
-
Saturday: { hoursOpenedPastMidnight: 2 },
|
|
173
|
-
Sunday: { hoursOpenedPastMidnight: 1 },
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
getDataByDateAndMealWithExceptions.mockImplementation((_data, _date, mealType) => {
|
|
179
|
-
if (mealType === 'dinner') {
|
|
180
|
-
return { enabled: true, startTime: '16:00', endTime: '24:00', maxCapacity: 40 };
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
getDailyGuestCounts.mockReturnValue({
|
|
186
|
-
guestCounts: makeGuestCounts(960, 1440, 15),
|
|
187
|
-
shiftsInfo: [],
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// 2025-02-21 is Friday
|
|
191
|
-
const fri = timeblocksAvailable(data, '2025-02-21', [], 2, [], null, false, 90);
|
|
192
|
-
// 23:30 + 90min = 01:00, within 1hr limit on Friday
|
|
193
|
-
expect(fri['23:30']).toBeDefined();
|
|
194
|
-
// 23:45 + 90min = 01:15, exceeds 1hr limit on Friday
|
|
195
|
-
expect(fri['23:45']).toBeUndefined();
|
|
196
|
-
|
|
197
|
-
// 2025-02-22 is Saturday
|
|
198
|
-
const sat = timeblocksAvailable(data, '2025-02-22', [], 2, [], null, false, 90);
|
|
199
|
-
// 23:45 + 90min = 01:15, within 2hr limit on Saturday
|
|
200
|
-
expect(sat['23:45']).toBeDefined();
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test('bowling Monday (hoursOpenedPastMidnight=0): 23:00 with short duration OK, 23:15 not', () => {
|
|
204
|
-
const data = {
|
|
205
|
-
'general-settings': { duurReservatie: '60', intervalReservatie: '15' },
|
|
206
|
-
'openinghours-dinner': {
|
|
207
|
-
schemeSettings: {
|
|
208
|
-
Monday: { hoursOpenedPastMidnight: 0 },
|
|
209
|
-
Friday: { hoursOpenedPastMidnight: 1 },
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
getDataByDateAndMealWithExceptions.mockImplementation((_data, _date, mealType) => {
|
|
215
|
-
if (mealType === 'dinner') {
|
|
216
|
-
return { enabled: true, startTime: '16:00', endTime: '24:00', maxCapacity: 40 };
|
|
217
|
-
}
|
|
218
|
-
return null;
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
getDailyGuestCounts.mockReturnValue({
|
|
222
|
-
guestCounts: makeGuestCounts(960, 1440, 15),
|
|
223
|
-
shiftsInfo: [],
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
// 2025-02-17 is Monday, 60min duration
|
|
227
|
-
const result = timeblocksAvailable(data, '2025-02-17', [], 2, [], null, false, 60);
|
|
228
|
-
|
|
229
|
-
// 23:00 + 60min = 24:00 (midnight exactly), hoursOpenedPastMidnight=0 allows up to midnight
|
|
230
|
-
expect(result['23:00']).toBeDefined();
|
|
231
|
-
// 23:15 + 60min = 24:15, past midnight, not allowed
|
|
232
|
-
expect(result['23:15']).toBeUndefined();
|
|
233
|
-
// 24:00 + 60min = 25:00, past midnight, not allowed
|
|
234
|
-
expect(result['24:00']).toBeUndefined();
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
test('bowling Friday (hoursOpenedPastMidnight=1): 23:30, 24:00 show', () => {
|
|
238
|
-
const data = {
|
|
239
|
-
'general-settings': { duurReservatie: '60', intervalReservatie: '15' },
|
|
240
|
-
'openinghours-dinner': {
|
|
241
|
-
schemeSettings: {
|
|
242
|
-
Monday: { hoursOpenedPastMidnight: 0 },
|
|
243
|
-
Friday: { hoursOpenedPastMidnight: 1 },
|
|
244
|
-
},
|
|
245
|
-
},
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
getDataByDateAndMealWithExceptions.mockImplementation((_data, _date, mealType) => {
|
|
249
|
-
if (mealType === 'dinner') {
|
|
250
|
-
return { enabled: true, startTime: '16:00', endTime: '24:00', maxCapacity: 40 };
|
|
251
|
-
}
|
|
252
|
-
return null;
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
getDailyGuestCounts.mockReturnValue({
|
|
256
|
-
guestCounts: makeGuestCounts(960, 1440, 15),
|
|
257
|
-
shiftsInfo: [],
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// 2025-02-21 is Friday, 60min duration
|
|
261
|
-
const result = timeblocksAvailable(data, '2025-02-21', [], 2, [], null, false, 60);
|
|
262
|
-
|
|
263
|
-
expect(result['23:00']).toBeDefined();
|
|
264
|
-
expect(result['23:15']).toBeDefined();
|
|
265
|
-
expect(result['23:30']).toBeDefined();
|
|
266
|
-
expect(result['23:45']).toBeDefined();
|
|
267
|
-
expect(result['24:00']).toBeDefined(); // 24:00 + 60 = 25:00, within 1hr? No...
|
|
268
|
-
|
|
269
|
-
// Wait: 24:00 + 60min = 25:00 → endMinutes=1500, limit=1440+60=1500 → 1500>1500 false → OK
|
|
270
|
-
});
|
|
271
|
-
});
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# PR 16 - Add getDateClosingReasons diagnostic function
|
|
2
|
-
|
|
3
|
-
**Actions:**
|
|
4
|
-
|
|
5
|
-
## Changes Summary
|
|
6
|
-
ADDED:
|
|
7
|
-
- New file `getDateClosingReasons.js` containing the diagnostic function `getDateClosingReasons(data, dateStr, reservations, guests, blockedSlots, giftcard, duration, zitplaats)`.
|
|
8
|
-
- New exported function `getDateClosingReasons` added to the test exports in `index.js`.
|
|
9
|
-
- New diagnostic logic that runs through 10 sequential stages of availability filtering to identify closure causes.
|
|
10
|
-
- New supported closure reason constants: `DATE_OUT_OF_RANGE`, `OPENING_HOURS_DISABLED`, `EXCEPTION_CLOSURE`, `CAPACITY_INSUFFICIENT`, `CAPACITY_REACHED`, `GIFTCARD_MISMATCH`, `BLOCKED_SLOTS`, `TODAY_TIME_RESTRICTION`, `MAX_GROUPS`, `MAX_ARRIVALS`, and `TABLE_PLAN`.
|
|
11
|
-
|
|
12
|
-
NO_REMOVALS
|
|
13
|
-
|
|
14
|
-
NO_CHANGES
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
**Author:** thibaultvandesompele2
|
|
19
|
-
**Date:** 2026-02-19
|
|
20
|
-
**PR Link:** https://github.com/thibaultvandesompele2/15-happy-algorithm/pull/16
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# PR 16 - Add getDateClosingReasons diagnostic function
|
|
2
|
-
|
|
3
|
-
**Actions:**
|
|
4
|
-
|
|
5
|
-
## Changes Summary
|
|
6
|
-
ADDED:
|
|
7
|
-
- New function `getDateClosingReasons(data, dateStr, reservations, guests, blockedSlots, giftcard, duration, zitplaats)` that diagnoses why a date is closed by running the availability pipeline stage-by-stage and identifying the key bottleneck reason.
|
|
8
|
-
- New file `getDateClosingReasons.js` containing the diagnostic logic.
|
|
9
|
-
- Exported `getDateClosingReasons` from the package index.
|
|
10
|
-
|
|
11
|
-
Supported reason types:
|
|
12
|
-
- `DATE_OUT_OF_RANGE` - Date outside booking window
|
|
13
|
-
- `OPENING_HOURS_DISABLED` - No meals enabled for this day of the week
|
|
14
|
-
- `EXCEPTION_CLOSURE` - Exception rule (e.g. holiday) closes the date
|
|
15
|
-
- `CAPACITY_INSUFFICIENT` - Base capacity too small for group size
|
|
16
|
-
- `CAPACITY_REACHED` - All time slots fully booked
|
|
17
|
-
- `GIFTCARD_MISMATCH` - Selected giftcard not valid on this day
|
|
18
|
-
- `BLOCKED_SLOTS` - Manually blocked time slots close remaining availability
|
|
19
|
-
- `TODAY_TIME_RESTRICTION` - Advance booking cutoff or stop times passed
|
|
20
|
-
- `MAX_GROUPS` - Group size limits reached
|
|
21
|
-
- `MAX_ARRIVALS` - Arrival count limits reached
|
|
22
|
-
- `TABLE_PLAN` - No table combination available for group size
|
|
23
|
-
|
|
24
|
-
NO_REMOVALS
|
|
25
|
-
|
|
26
|
-
CHANGED:
|
|
27
|
-
- Version bumped from 1.2.31 to 1.3.0.
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
**Date:** 2026-02-19
|
package/dateHelpers.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// dateHelpers.js — Simple date string arithmetic for cross-midnight support
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Returns the next day's date string in YYYY-MM-DD format.
|
|
5
|
-
* @param {string} dateStr - Date in "YYYY-MM-DD" format.
|
|
6
|
-
* @returns {string} Next day in "YYYY-MM-DD" format.
|
|
7
|
-
*/
|
|
8
|
-
function getNextDateStr(dateStr) {
|
|
9
|
-
const [y, m, d] = dateStr.split('-').map(Number);
|
|
10
|
-
const next = new Date(y, m - 1, d + 1);
|
|
11
|
-
const ny = next.getFullYear();
|
|
12
|
-
const nm = String(next.getMonth() + 1).padStart(2, '0');
|
|
13
|
-
const nd = String(next.getDate()).padStart(2, '0');
|
|
14
|
-
return `${ny}-${nm}-${nd}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Returns the previous day's date string in YYYY-MM-DD format.
|
|
19
|
-
* @param {string} dateStr - Date in "YYYY-MM-DD" format.
|
|
20
|
-
* @returns {string} Previous day in "YYYY-MM-DD" format.
|
|
21
|
-
*/
|
|
22
|
-
function getPrevDateStr(dateStr) {
|
|
23
|
-
const [y, m, d] = dateStr.split('-').map(Number);
|
|
24
|
-
const prev = new Date(y, m - 1, d - 1);
|
|
25
|
-
const py = prev.getFullYear();
|
|
26
|
-
const pm = String(prev.getMonth() + 1).padStart(2, '0');
|
|
27
|
-
const pd = String(prev.getDate()).padStart(2, '0');
|
|
28
|
-
return `${py}-${pm}-${pd}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
module.exports = { getNextDateStr, getPrevDateStr };
|