payload-reserve 2.3.0 → 3.0.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/README.md +218 -4
- package/dist/collections/Holds.d.ts +18 -0
- package/dist/collections/Holds.js +113 -0
- package/dist/collections/Holds.js.map +1 -0
- package/dist/collections/Reservations.js +8 -0
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.js +35 -1
- package/dist/collections/Resources.js.map +1 -1
- package/dist/collections/Services.js +41 -0
- package/dist/collections/Services.js.map +1 -1
- package/dist/components/CalendarView/LaneTimelineView.js +4 -5
- package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
- package/dist/components/CalendarView/index.js +55 -71
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/components/DashboardWidget/DashboardWidgetServer.js +11 -74
- package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
- package/dist/components/DashboardWidget/fetchDashboardStats.d.ts +42 -0
- package/dist/components/DashboardWidget/fetchDashboardStats.js +113 -0
- package/dist/components/DashboardWidget/fetchDashboardStats.js.map +1 -0
- package/dist/defaults.d.ts +2 -0
- package/dist/defaults.js +12 -0
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.js +43 -14
- package/dist/endpoints/cancelBooking.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +27 -1
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/createBooking.js +108 -7
- package/dist/endpoints/createBooking.js.map +1 -1
- package/dist/endpoints/customerSearch.js +6 -0
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/effectiveTimezone.js +1 -0
- package/dist/endpoints/effectiveTimezone.js.map +1 -1
- package/dist/endpoints/getSlots.js +27 -1
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/holdSlot.d.ts +12 -0
- package/dist/endpoints/holdSlot.js +126 -0
- package/dist/endpoints/holdSlot.js.map +1 -0
- package/dist/endpoints/releaseSlot.d.ts +8 -0
- package/dist/endpoints/releaseSlot.js +34 -0
- package/dist/endpoints/releaseSlot.js.map +1 -0
- package/dist/endpoints/resourceAvailability.d.ts +4 -0
- package/dist/endpoints/resourceAvailability.js +105 -4
- package/dist/endpoints/resourceAvailability.js.map +1 -1
- package/dist/hooks/holds/validateHoldSlot.d.ts +14 -0
- package/dist/hooks/holds/validateHoldSlot.js +90 -0
- package/dist/hooks/holds/validateHoldSlot.js.map +1 -0
- package/dist/hooks/reservations/acquireBookingLock.d.ts +30 -0
- package/dist/hooks/reservations/acquireBookingLock.js +71 -0
- package/dist/hooks/reservations/acquireBookingLock.js.map +1 -0
- package/dist/hooks/reservations/calculateEndTime.js +13 -1
- package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
- package/dist/hooks/reservations/expandRequiredResources.js +3 -0
- package/dist/hooks/reservations/expandRequiredResources.js.map +1 -1
- package/dist/hooks/reservations/validateActive.d.ts +11 -0
- package/dist/hooks/reservations/validateActive.js +103 -0
- package/dist/hooks/reservations/validateActive.js.map +1 -0
- package/dist/hooks/reservations/validateConflicts.js +22 -1
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/hooks/reservations/validateGuestBooking.js +3 -0
- package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
- package/dist/hooks/shared/preventDeleteWhenReferenced.d.ts +42 -0
- package/dist/hooks/shared/preventDeleteWhenReferenced.js +87 -0
- package/dist/hooks/shared/preventDeleteWhenReferenced.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +179 -3
- package/dist/plugin.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +51 -5
- package/dist/services/AvailabilityService.js +315 -38
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/services/HoldService.d.ts +53 -0
- package/dist/services/HoldService.js +169 -0
- package/dist/services/HoldService.js.map +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/translations/ar.json +5 -1
- package/dist/translations/de.json +5 -1
- package/dist/translations/en.json +5 -1
- package/dist/translations/es.json +5 -1
- package/dist/translations/fa.json +5 -1
- package/dist/translations/fr.json +5 -1
- package/dist/translations/hi.json +5 -1
- package/dist/translations/id.json +5 -1
- package/dist/translations/pl.json +5 -1
- package/dist/translations/ru.json +5 -1
- package/dist/translations/tr.json +5 -1
- package/dist/translations/zh.json +5 -1
- package/dist/types.d.ts +32 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/calendarGrid.d.ts +44 -0
- package/dist/utilities/calendarGrid.js +67 -0
- package/dist/utilities/calendarGrid.js.map +1 -0
- package/dist/utilities/reservationChanges.d.ts +6 -1
- package/dist/utilities/reservationChanges.js +1 -1
- package/dist/utilities/reservationChanges.js.map +1 -1
- package/dist/utilities/reserveDebug.d.ts +26 -0
- package/dist/utilities/reserveDebug.js +35 -0
- package/dist/utilities/reserveDebug.js.map +1 -0
- package/dist/utilities/resolveReservationItems.d.ts +13 -2
- package/dist/utilities/resolveReservationItems.js +87 -2
- package/dist/utilities/resolveReservationItems.js.map +1 -1
- package/dist/utilities/retryOnWriteConflict.d.ts +66 -0
- package/dist/utilities/retryOnWriteConflict.js +135 -0
- package/dist/utilities/retryOnWriteConflict.js.map +1 -0
- package/dist/utilities/tenantTimezone.d.ts +77 -1
- package/dist/utilities/tenantTimezone.js +163 -2
- package/dist/utilities/tenantTimezone.js.map +1 -1
- package/dist/utilities/transactionSupport.d.ts +13 -0
- package/dist/utilities/transactionSupport.js +30 -0
- package/dist/utilities/transactionSupport.js.map +1 -0
- package/package.json +13 -13
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NOOP_RESERVE_DEBUG } from '../utilities/reserveDebug.js';
|
|
1
2
|
import { resolveReservationItems } from '../utilities/resolveReservationItems.js';
|
|
2
3
|
import { isExceptionDate, resolveScheduleForDate } from '../utilities/scheduleUtils.js';
|
|
3
4
|
import { addMinutes, computeBlockedWindow, doRangesOverlap, intersectIntervals } from '../utilities/slotUtils.js';
|
|
@@ -148,12 +149,23 @@ export function buildOverlapQuery(params) {
|
|
|
148
149
|
}
|
|
149
150
|
return occupancies;
|
|
150
151
|
}
|
|
151
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* Occupancy a single fetched reservation imposes on `resourceId`.
|
|
154
|
+
*
|
|
155
|
+
* Resolves `reservation` LENIENTLY: this is a stored document, which — unlike
|
|
156
|
+
* a write in progress — can carry an inverted top-level window from a
|
|
157
|
+
* `context.skipReservationHooks` write or data predating that check. Lenient
|
|
158
|
+
* mode skips parent synthesis instead of throwing, so a malformed top-level
|
|
159
|
+
* window never discards the real `items[]` occupancies also present on the
|
|
160
|
+
* same row (see the lenient-mode note in resolveReservationItems.ts).
|
|
161
|
+
*/ export async function reservationOccupancies(params) {
|
|
152
162
|
const { bufferFor, capacityMode, reservation, resourceId } = params;
|
|
153
163
|
return itemsToOccupancies({
|
|
154
164
|
bufferFor,
|
|
155
165
|
capacityMode,
|
|
156
|
-
items: resolveReservationItems(reservation
|
|
166
|
+
items: resolveReservationItems(reservation, {
|
|
167
|
+
lenient: true
|
|
168
|
+
}),
|
|
157
169
|
resourceId
|
|
158
170
|
});
|
|
159
171
|
}
|
|
@@ -180,7 +192,8 @@ export function validateTransition(fromStatus, toStatus, statusMachine) {
|
|
|
180
192
|
}
|
|
181
193
|
// --- DB functions (use Payload Local API only) ---
|
|
182
194
|
export async function checkAvailability(params) {
|
|
183
|
-
const { blockingStatuses, bufferAfter, bufferBefore, endTime, excludeReservationId, getExternalBusy, guestCount, payload, req, reservationSlug, resourceId, resourceSlug, servicesSlug, siblingItems, startTime } = params;
|
|
195
|
+
const { blockingStatuses, bufferAfter, bufferBefore, debug, endTime, excludeHoldToken, excludeReservationId, getExternalBusy, guestCount, holdsSlug, payload, req, reservationSlug, resourceId, resourceSlug, servicesSlug, siblingItems, startTime } = params;
|
|
196
|
+
const trace = debug ?? NOOP_RESERVE_DEBUG;
|
|
184
197
|
// Fetch resource for quantity and capacity mode
|
|
185
198
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
186
199
|
const resource = await payload.findByID({
|
|
@@ -194,19 +207,29 @@ export async function checkAvailability(params) {
|
|
|
194
207
|
// Candidate window expanded by its own buffers
|
|
195
208
|
const { effectiveEnd: candidateEnd, effectiveStart: candidateStart } = computeBlockedWindow(startTime, endTime, bufferBefore, bufferAfter);
|
|
196
209
|
// Coarse superset fetch — precise per-item overlap is computed in memory below
|
|
210
|
+
const coarseWhere = buildCoarseOverlapQuery({
|
|
211
|
+
blockingStatuses,
|
|
212
|
+
candidateEnd,
|
|
213
|
+
candidateStart,
|
|
214
|
+
excludeReservationId,
|
|
215
|
+
resourceId
|
|
216
|
+
});
|
|
197
217
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
198
218
|
const { docs } = await payload.find({
|
|
199
219
|
collection: reservationSlug,
|
|
200
220
|
depth: 0,
|
|
201
221
|
limit: 0,
|
|
202
222
|
req,
|
|
203
|
-
where:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
223
|
+
where: coarseWhere
|
|
224
|
+
});
|
|
225
|
+
trace.dbg('check', {
|
|
226
|
+
blockingReservations: docs.length,
|
|
227
|
+
candidateEnd: candidateEnd.toISOString(),
|
|
228
|
+
candidateStart: candidateStart.toISOString(),
|
|
229
|
+
capacityMode,
|
|
230
|
+
coarseWhere,
|
|
231
|
+
quantity,
|
|
232
|
+
resourceId
|
|
210
233
|
});
|
|
211
234
|
// Per-call cache: fetch each distinct service's buffers at most once
|
|
212
235
|
const bufferCache = new Map();
|
|
@@ -227,6 +250,9 @@ export async function checkAvailability(params) {
|
|
|
227
250
|
id: serviceId,
|
|
228
251
|
collection: servicesSlug,
|
|
229
252
|
depth: 0,
|
|
253
|
+
// Skip the resources join — internal logic never reads it, and without this
|
|
254
|
+
// every service read becomes an aggregation with a $lookup.
|
|
255
|
+
joins: false,
|
|
230
256
|
req
|
|
231
257
|
});
|
|
232
258
|
if (service) {
|
|
@@ -235,19 +261,41 @@ export async function checkAvailability(params) {
|
|
|
235
261
|
before: service.bufferTimeBefore ?? 0
|
|
236
262
|
};
|
|
237
263
|
}
|
|
238
|
-
} catch
|
|
239
|
-
|
|
264
|
+
} catch (err) {
|
|
265
|
+
trace.dbg('error', {
|
|
266
|
+
err,
|
|
267
|
+
serviceId,
|
|
268
|
+
where: 'bufferFor'
|
|
269
|
+
});
|
|
240
270
|
}
|
|
241
271
|
}
|
|
242
272
|
bufferCache.set(key, result);
|
|
243
273
|
return result;
|
|
244
274
|
};
|
|
245
|
-
|
|
275
|
+
// reservationOccupancies resolves each doc LENIENTLY (see its own doc
|
|
276
|
+
// comment) — an inverted top-level window on a stored row skips parent
|
|
277
|
+
// synthesis rather than throwing, so this never needs a try/catch: every
|
|
278
|
+
// real items[] occupancy on the row still comes back.
|
|
279
|
+
//
|
|
280
|
+
// SEQUENTIAL, not Promise.all, and that is load-bearing. Each iteration can
|
|
281
|
+
// reach `bufferFor` -> `payload.findByID` on the shared `req`, and this whole
|
|
282
|
+
// function runs inside the booking's own transaction on the write path. A
|
|
283
|
+
// MongoDB ClientSession cannot carry concurrent operations inside a
|
|
284
|
+
// transaction: when two do collide, the loser's `count`/`findByID` calls
|
|
285
|
+
// `killTransaction` from its own catch, which rolls back and clears the
|
|
286
|
+
// transaction the enclosing create/delete owns, and the survivor then reports
|
|
287
|
+
// `NoSuchTransaction` ("transaction number N does not match any in-progress
|
|
288
|
+
// transactions"). Buffers are per-service and cached, so the loop is at most
|
|
289
|
+
// one read per DISTINCT neighbouring service either way.
|
|
290
|
+
const fetchedOccupancies = [];
|
|
291
|
+
for (const doc of docs){
|
|
292
|
+
fetchedOccupancies.push(...await reservationOccupancies({
|
|
246
293
|
bufferFor,
|
|
247
294
|
capacityMode,
|
|
248
295
|
reservation: doc,
|
|
249
296
|
resourceId
|
|
250
|
-
}))
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
251
299
|
// Sibling items from the same booking (review A5) — expanded with the same
|
|
252
300
|
// per-service buffers and capacity mode.
|
|
253
301
|
const siblingOccupancies = siblingItems ? await itemsToOccupancies({
|
|
@@ -273,19 +321,102 @@ export async function checkAvailability(params) {
|
|
|
273
321
|
blockedStart: new Date(iv.start),
|
|
274
322
|
units: quantity
|
|
275
323
|
})).filter((o)=>!isNaN(o.blockedStart.getTime()) && !isNaN(o.blockedEnd.getTime()));
|
|
276
|
-
} catch
|
|
324
|
+
} catch (err) {
|
|
325
|
+
trace.dbg('error', {
|
|
326
|
+
err,
|
|
327
|
+
resourceId,
|
|
328
|
+
where: 'getExternalBusy'
|
|
329
|
+
});
|
|
277
330
|
externalOccupancies = [];
|
|
278
331
|
}
|
|
279
332
|
}
|
|
333
|
+
// Unexpired slot holds occupy the resource exactly as a blocking reservation
|
|
334
|
+
// does. Expiry is a read-time predicate rather than a TTL index so the
|
|
335
|
+
// behaviour is identical on Mongo, Postgres and SQLite. `excludeHoldToken`
|
|
336
|
+
// is the bearer converting their OWN hold into a booking — without it the
|
|
337
|
+
// hold would block the very booking it was taken to protect.
|
|
338
|
+
const holdOccupancies = [];
|
|
339
|
+
if (holdsSlug) {
|
|
340
|
+
const holdWhere = {
|
|
341
|
+
and: [
|
|
342
|
+
{
|
|
343
|
+
resource: {
|
|
344
|
+
equals: resourceId
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
expiresAt: {
|
|
349
|
+
greater_than: new Date().toISOString()
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
startTime: {
|
|
354
|
+
less_than: candidateEnd.toISOString()
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
endTime: {
|
|
359
|
+
greater_than: candidateStart.toISOString()
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
...excludeHoldToken ? [
|
|
363
|
+
{
|
|
364
|
+
token: {
|
|
365
|
+
not_equals: excludeHoldToken
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
] : []
|
|
369
|
+
]
|
|
370
|
+
};
|
|
371
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
372
|
+
const { docs: holdDocs } = await payload.find({
|
|
373
|
+
collection: holdsSlug,
|
|
374
|
+
depth: 0,
|
|
375
|
+
limit: 0,
|
|
376
|
+
req,
|
|
377
|
+
where: holdWhere
|
|
378
|
+
});
|
|
379
|
+
// Sequential for the same reason the reservation loop above is: `bufferFor`
|
|
380
|
+
// reads through the shared `req`, and concurrent operations on one MongoDB
|
|
381
|
+
// session inside a transaction abort it.
|
|
382
|
+
for (const hold of holdDocs){
|
|
383
|
+
// Expand by the held service's own buffers, so a hold reserves the same
|
|
384
|
+
// real-world window the resulting booking will.
|
|
385
|
+
const { after, before } = await bufferFor(hold.service);
|
|
386
|
+
const { effectiveEnd, effectiveStart } = computeBlockedWindow(new Date(hold.startTime), new Date(hold.endTime), before, after);
|
|
387
|
+
holdOccupancies.push({
|
|
388
|
+
blockedEnd: effectiveEnd,
|
|
389
|
+
blockedStart: effectiveStart,
|
|
390
|
+
units: capacityMode === 'per-guest' ? hold.guestCount ?? 1 : 1
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
trace.dbg('check_holds', {
|
|
394
|
+
activeHolds: holdOccupancies.length,
|
|
395
|
+
resourceId
|
|
396
|
+
});
|
|
397
|
+
}
|
|
280
398
|
const occupancies = [
|
|
281
399
|
...fetchedOccupancies,
|
|
282
400
|
...siblingOccupancies,
|
|
283
|
-
...externalOccupancies
|
|
401
|
+
...externalOccupancies,
|
|
402
|
+
...holdOccupancies
|
|
284
403
|
];
|
|
285
404
|
// Sum the units of every occupancy whose buffered window overlaps the candidate
|
|
286
405
|
const currentUnits = occupancies.reduce((sum, occ)=>doRangesOverlap(candidateStart, candidateEnd, occ.blockedStart, occ.blockedEnd) ? sum + occ.units : sum, 0);
|
|
287
406
|
const candidateUnits = capacityMode === 'per-guest' ? guestCount : 1;
|
|
288
407
|
const available = currentUnits + candidateUnits <= quantity;
|
|
408
|
+
trace.dbg('check_result', {
|
|
409
|
+
available,
|
|
410
|
+
candidateUnits,
|
|
411
|
+
currentUnits,
|
|
412
|
+
occupancySources: {
|
|
413
|
+
external: externalOccupancies.length,
|
|
414
|
+
fetched: fetchedOccupancies.length,
|
|
415
|
+
sibling: siblingOccupancies.length
|
|
416
|
+
},
|
|
417
|
+
quantity,
|
|
418
|
+
resourceId
|
|
419
|
+
});
|
|
289
420
|
return {
|
|
290
421
|
available,
|
|
291
422
|
currentCount: currentUnits,
|
|
@@ -294,14 +425,29 @@ export async function checkAvailability(params) {
|
|
|
294
425
|
};
|
|
295
426
|
}
|
|
296
427
|
export async function getAvailableSlots(params) {
|
|
297
|
-
const { blockingStatuses, date, getExternalBusy, guestCount, payload, req, reservationSlug, resourceId, resourceIds, resourceSlug, scheduleSlug, serviceId, serviceSlug, timeZone } = params;
|
|
428
|
+
const { blockingStatuses, date, debug, enforceActive, getExternalBusy, guestCount, holdsSlug, payload, req, reservationSlug, resourceId, resourceIds, resourceSlug, scheduleSlug, serviceId, serviceSlug, timeZone } = params;
|
|
298
429
|
const tz = timeZone ?? 'UTC';
|
|
299
430
|
// Resolve the set of resources to intersect (single-resource callers still work)
|
|
300
431
|
const ids = resourceIds && resourceIds.length > 0 ? resourceIds : resourceId !== undefined ? [
|
|
301
432
|
resourceId
|
|
302
433
|
] : [];
|
|
434
|
+
const trace = (debug ?? NOOP_RESERVE_DEBUG).child({
|
|
435
|
+
resourceIds: ids,
|
|
436
|
+
serviceId
|
|
437
|
+
});
|
|
438
|
+
trace.dbg('input', {
|
|
439
|
+
date: date instanceof Date ? date.toISOString() : date,
|
|
440
|
+
guestCount: guestCount ?? 1,
|
|
441
|
+
timeZone: tz
|
|
442
|
+
});
|
|
303
443
|
if (ids.length === 0) {
|
|
304
|
-
|
|
444
|
+
trace.dbg('empty', {
|
|
445
|
+
reason: 'no_resource_ids'
|
|
446
|
+
});
|
|
447
|
+
return {
|
|
448
|
+
reason: 'no_resource_ids',
|
|
449
|
+
slots: []
|
|
450
|
+
};
|
|
305
451
|
}
|
|
306
452
|
// 1. Service for duration + buffer times (from the primary service)
|
|
307
453
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -309,45 +455,119 @@ export async function getAvailableSlots(params) {
|
|
|
309
455
|
id: serviceId,
|
|
310
456
|
collection: serviceSlug,
|
|
311
457
|
depth: 0,
|
|
458
|
+
// Skip the resources join — internal logic never reads it, and without this
|
|
459
|
+
// every service read becomes an aggregation with a $lookup.
|
|
460
|
+
joins: false,
|
|
312
461
|
req
|
|
313
462
|
});
|
|
463
|
+
if (enforceActive !== false && service?.active === false) {
|
|
464
|
+
trace.dbg('empty', {
|
|
465
|
+
reason: 'service_inactive',
|
|
466
|
+
serviceId
|
|
467
|
+
});
|
|
468
|
+
return {
|
|
469
|
+
reason: 'service_inactive',
|
|
470
|
+
slots: []
|
|
471
|
+
};
|
|
472
|
+
}
|
|
314
473
|
const duration = service.duration ?? 60;
|
|
315
474
|
const bufferBefore = service.bufferTimeBefore ?? 0;
|
|
316
475
|
const bufferAfter = service.bufferTimeAfter ?? 0;
|
|
317
476
|
const durationType = service.durationType ?? 'fixed';
|
|
477
|
+
trace.dbg('service', {
|
|
478
|
+
bufferAfter,
|
|
479
|
+
bufferBefore,
|
|
480
|
+
duration,
|
|
481
|
+
durationType
|
|
482
|
+
});
|
|
318
483
|
// 2. Per resource: fetch schedules and resolve to windows. A resource with >=1
|
|
319
484
|
// schedule is "schedule-bearing" and constrains time; a resource with zero
|
|
320
485
|
// schedules is capacity-only and contributes no time windows.
|
|
321
486
|
const scheduleBearingWindowLists = [];
|
|
322
487
|
for (const rid of ids){
|
|
488
|
+
// This read exists solely to feed the active check below — skip it
|
|
489
|
+
// entirely when enforceActive is off so an opted-out consumer doesn't pay
|
|
490
|
+
// for a findByID whose result is never read.
|
|
491
|
+
if (enforceActive !== false) {
|
|
492
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
493
|
+
const resourceDoc = await payload.findByID({
|
|
494
|
+
id: rid,
|
|
495
|
+
collection: resourceSlug,
|
|
496
|
+
depth: 0,
|
|
497
|
+
// Skip joins — internal logic never reads them, and without this every
|
|
498
|
+
// resource read becomes an aggregation with a $lookup.
|
|
499
|
+
joins: false,
|
|
500
|
+
req
|
|
501
|
+
}).catch(()=>null);
|
|
502
|
+
if (resourceDoc?.active === false) {
|
|
503
|
+
trace.dbg('empty', {
|
|
504
|
+
reason: 'resource_inactive',
|
|
505
|
+
resourceId: rid
|
|
506
|
+
});
|
|
507
|
+
return {
|
|
508
|
+
reason: 'resource_inactive',
|
|
509
|
+
slots: []
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const scheduleWhere = {
|
|
514
|
+
and: [
|
|
515
|
+
{
|
|
516
|
+
resource: {
|
|
517
|
+
equals: rid
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
active: {
|
|
522
|
+
equals: true
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
]
|
|
526
|
+
};
|
|
323
527
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
324
528
|
const { docs: schedules } = await payload.find({
|
|
325
529
|
collection: scheduleSlug,
|
|
326
530
|
depth: 0,
|
|
327
531
|
limit: 100,
|
|
328
532
|
req,
|
|
329
|
-
where:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
{
|
|
337
|
-
active: {
|
|
338
|
-
equals: true
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
]
|
|
342
|
-
}
|
|
533
|
+
where: scheduleWhere
|
|
534
|
+
});
|
|
535
|
+
trace.dbg('schedules', {
|
|
536
|
+
activeFlags: schedules.map((s)=>s.active),
|
|
537
|
+
resourceId: rid,
|
|
538
|
+
rowsFound: schedules.length,
|
|
539
|
+
scheduleWhere
|
|
343
540
|
});
|
|
344
541
|
if (!schedules || schedules.length === 0) {
|
|
542
|
+
trace.dbg('skip', {
|
|
543
|
+
reason: 'no_active_schedules',
|
|
544
|
+
resourceId: rid,
|
|
545
|
+
rowsFound: 0
|
|
546
|
+
});
|
|
345
547
|
continue;
|
|
346
548
|
}
|
|
347
549
|
// A11: an exception on ANY of the resource's schedules makes the whole
|
|
348
550
|
// resource unavailable that day — not just the schedule it's recorded on.
|
|
349
|
-
|
|
350
|
-
|
|
551
|
+
let matched;
|
|
552
|
+
for (const s of schedules){
|
|
553
|
+
const exs = s.exceptions ?? [];
|
|
554
|
+
if (isExceptionDate(date, exs, tz)) {
|
|
555
|
+
matched = {
|
|
556
|
+
exception: exs.find((e)=>isExceptionDate(date, [
|
|
557
|
+
e
|
|
558
|
+
], tz)),
|
|
559
|
+
scheduleId: s.id
|
|
560
|
+
};
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (matched) {
|
|
565
|
+
trace.dbg('skip', {
|
|
566
|
+
matchedException: matched.exception,
|
|
567
|
+
reason: 'date_excepted',
|
|
568
|
+
resourceId: rid,
|
|
569
|
+
scheduleId: matched.scheduleId
|
|
570
|
+
});
|
|
351
571
|
scheduleBearingWindowLists.push([]);
|
|
352
572
|
continue;
|
|
353
573
|
}
|
|
@@ -355,11 +575,26 @@ export async function getAvailableSlots(params) {
|
|
|
355
575
|
for (const schedule of schedules){
|
|
356
576
|
windows.push(...resolveScheduleForDate(schedule, date, tz));
|
|
357
577
|
}
|
|
578
|
+
trace.dbg('windows', {
|
|
579
|
+
resourceId: rid,
|
|
580
|
+
windowCount: windows.length,
|
|
581
|
+
windows: windows.map((w)=>({
|
|
582
|
+
end: w.end.toISOString(),
|
|
583
|
+
start: w.start.toISOString()
|
|
584
|
+
})),
|
|
585
|
+
windowsEmpty: windows.length === 0
|
|
586
|
+
});
|
|
358
587
|
scheduleBearingWindowLists.push(windows);
|
|
359
588
|
}
|
|
360
589
|
// No resource constrains time → no basis for generating slots
|
|
361
590
|
if (scheduleBearingWindowLists.length === 0) {
|
|
362
|
-
|
|
591
|
+
trace.dbg('empty', {
|
|
592
|
+
reason: 'no_windows'
|
|
593
|
+
});
|
|
594
|
+
return {
|
|
595
|
+
reason: 'no_windows',
|
|
596
|
+
slots: []
|
|
597
|
+
};
|
|
363
598
|
}
|
|
364
599
|
// 3. Intersect all schedule-bearing window lists
|
|
365
600
|
let timeRanges = scheduleBearingWindowLists[0];
|
|
@@ -367,7 +602,17 @@ export async function getAvailableSlots(params) {
|
|
|
367
602
|
timeRanges = intersectIntervals(timeRanges, scheduleBearingWindowLists[i]);
|
|
368
603
|
}
|
|
369
604
|
if (timeRanges.length === 0) {
|
|
370
|
-
|
|
605
|
+
trace.dbg('empty', {
|
|
606
|
+
perResourceWindows: scheduleBearingWindowLists.map((list)=>list.map((w)=>({
|
|
607
|
+
end: w.end.toISOString(),
|
|
608
|
+
start: w.start.toISOString()
|
|
609
|
+
}))),
|
|
610
|
+
reason: 'empty_intersection'
|
|
611
|
+
});
|
|
612
|
+
return {
|
|
613
|
+
reason: 'empty_intersection',
|
|
614
|
+
slots: []
|
|
615
|
+
};
|
|
371
616
|
}
|
|
372
617
|
// 4. Candidate slot sizing
|
|
373
618
|
// NOTE: epoch-trick sizing is only meaningful for fixed/flexible durations.
|
|
@@ -381,6 +626,11 @@ export async function getAvailableSlots(params) {
|
|
|
381
626
|
});
|
|
382
627
|
const slotDuration = Math.round(slotEndOffset.getTime() / 60_000);
|
|
383
628
|
const effectiveDuration = durationType === 'fixed' ? duration : slotDuration;
|
|
629
|
+
trace.dbg('sizing', {
|
|
630
|
+
effectiveDuration,
|
|
631
|
+
slotDuration,
|
|
632
|
+
stepSize: Math.min(effectiveDuration, 15)
|
|
633
|
+
});
|
|
384
634
|
// Helper: a window is available only if EVERY required resource is free
|
|
385
635
|
const allAvailable = async (start, end, bBefore, bAfter)=>{
|
|
386
636
|
for (const rid of ids){
|
|
@@ -388,9 +638,11 @@ export async function getAvailableSlots(params) {
|
|
|
388
638
|
blockingStatuses,
|
|
389
639
|
bufferAfter: bAfter,
|
|
390
640
|
bufferBefore: bBefore,
|
|
641
|
+
debug: trace,
|
|
391
642
|
endTime: end,
|
|
392
643
|
getExternalBusy,
|
|
393
644
|
guestCount: guestCount ?? 1,
|
|
645
|
+
holdsSlug,
|
|
394
646
|
payload,
|
|
395
647
|
req,
|
|
396
648
|
reservationSlug,
|
|
@@ -408,7 +660,9 @@ export async function getAvailableSlots(params) {
|
|
|
408
660
|
const availableSlots = [];
|
|
409
661
|
// Full-day: offer each range as a single slot if all resources are free
|
|
410
662
|
if (durationType === 'full-day') {
|
|
663
|
+
let candidatesGenerated = 0;
|
|
411
664
|
for (const range of timeRanges){
|
|
665
|
+
candidatesGenerated++;
|
|
412
666
|
if (await allAvailable(range.start, range.end, 0, 0)) {
|
|
413
667
|
availableSlots.push({
|
|
414
668
|
end: range.end,
|
|
@@ -416,9 +670,21 @@ export async function getAvailableSlots(params) {
|
|
|
416
670
|
});
|
|
417
671
|
}
|
|
418
672
|
}
|
|
419
|
-
|
|
673
|
+
trace.dbg('result', {
|
|
674
|
+
candidatesAvailable: availableSlots.length,
|
|
675
|
+
candidatesGenerated,
|
|
676
|
+
durationType: 'full-day',
|
|
677
|
+
returnedCount: availableSlots.length
|
|
678
|
+
});
|
|
679
|
+
return {
|
|
680
|
+
...availableSlots.length === 0 ? {
|
|
681
|
+
reason: 'all_slots_taken'
|
|
682
|
+
} : {},
|
|
683
|
+
slots: availableSlots
|
|
684
|
+
};
|
|
420
685
|
}
|
|
421
686
|
const stepSize = Math.min(effectiveDuration, 15);
|
|
687
|
+
let candidatesGenerated = 0;
|
|
422
688
|
for (const range of timeRanges){
|
|
423
689
|
let candidateStart = new Date(range.start);
|
|
424
690
|
while(true){
|
|
@@ -426,6 +692,7 @@ export async function getAvailableSlots(params) {
|
|
|
426
692
|
if (candidateEnd > range.end) {
|
|
427
693
|
break;
|
|
428
694
|
}
|
|
695
|
+
candidatesGenerated++;
|
|
429
696
|
if (await allAvailable(candidateStart, candidateEnd, bufferBefore, bufferAfter)) {
|
|
430
697
|
availableSlots.push({
|
|
431
698
|
end: candidateEnd,
|
|
@@ -435,7 +702,17 @@ export async function getAvailableSlots(params) {
|
|
|
435
702
|
candidateStart = addMinutes(candidateStart, stepSize);
|
|
436
703
|
}
|
|
437
704
|
}
|
|
438
|
-
|
|
705
|
+
trace.dbg('result', {
|
|
706
|
+
candidatesAvailable: availableSlots.length,
|
|
707
|
+
candidatesGenerated,
|
|
708
|
+
returnedCount: availableSlots.length
|
|
709
|
+
});
|
|
710
|
+
return {
|
|
711
|
+
...availableSlots.length === 0 ? {
|
|
712
|
+
reason: candidatesGenerated === 0 ? 'window_too_short' : 'all_slots_taken'
|
|
713
|
+
} : {},
|
|
714
|
+
slots: availableSlots
|
|
715
|
+
};
|
|
439
716
|
}
|
|
440
717
|
|
|
441
718
|
//# sourceMappingURL=AvailabilityService.js.map
|