payload-reserve 2.3.0 → 2.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/README.md +1 -0
- package/dist/defaults.js +1 -0
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +15 -0
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/getSlots.js +15 -0
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/resourceAvailability.d.ts +2 -0
- package/dist/endpoints/resourceAvailability.js +27 -2
- package/dist/endpoints/resourceAvailability.js.map +1 -1
- package/dist/hooks/reservations/validateConflicts.js +15 -0
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +5 -0
- package/dist/services/AvailabilityService.js +149 -28
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.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/package.json +1 -1
|
@@ -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';
|
|
@@ -180,7 +181,8 @@ export function validateTransition(fromStatus, toStatus, statusMachine) {
|
|
|
180
181
|
}
|
|
181
182
|
// --- DB functions (use Payload Local API only) ---
|
|
182
183
|
export async function checkAvailability(params) {
|
|
183
|
-
const { blockingStatuses, bufferAfter, bufferBefore, endTime, excludeReservationId, getExternalBusy, guestCount, payload, req, reservationSlug, resourceId, resourceSlug, servicesSlug, siblingItems, startTime } = params;
|
|
184
|
+
const { blockingStatuses, bufferAfter, bufferBefore, debug, endTime, excludeReservationId, getExternalBusy, guestCount, payload, req, reservationSlug, resourceId, resourceSlug, servicesSlug, siblingItems, startTime } = params;
|
|
185
|
+
const trace = debug ?? NOOP_RESERVE_DEBUG;
|
|
184
186
|
// Fetch resource for quantity and capacity mode
|
|
185
187
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
186
188
|
const resource = await payload.findByID({
|
|
@@ -194,19 +196,29 @@ export async function checkAvailability(params) {
|
|
|
194
196
|
// Candidate window expanded by its own buffers
|
|
195
197
|
const { effectiveEnd: candidateEnd, effectiveStart: candidateStart } = computeBlockedWindow(startTime, endTime, bufferBefore, bufferAfter);
|
|
196
198
|
// Coarse superset fetch — precise per-item overlap is computed in memory below
|
|
199
|
+
const coarseWhere = buildCoarseOverlapQuery({
|
|
200
|
+
blockingStatuses,
|
|
201
|
+
candidateEnd,
|
|
202
|
+
candidateStart,
|
|
203
|
+
excludeReservationId,
|
|
204
|
+
resourceId
|
|
205
|
+
});
|
|
197
206
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
198
207
|
const { docs } = await payload.find({
|
|
199
208
|
collection: reservationSlug,
|
|
200
209
|
depth: 0,
|
|
201
210
|
limit: 0,
|
|
202
211
|
req,
|
|
203
|
-
where:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
212
|
+
where: coarseWhere
|
|
213
|
+
});
|
|
214
|
+
trace.dbg('check', {
|
|
215
|
+
blockingReservations: docs.length,
|
|
216
|
+
candidateEnd: candidateEnd.toISOString(),
|
|
217
|
+
candidateStart: candidateStart.toISOString(),
|
|
218
|
+
capacityMode,
|
|
219
|
+
coarseWhere,
|
|
220
|
+
quantity,
|
|
221
|
+
resourceId
|
|
210
222
|
});
|
|
211
223
|
// Per-call cache: fetch each distinct service's buffers at most once
|
|
212
224
|
const bufferCache = new Map();
|
|
@@ -235,8 +247,12 @@ export async function checkAvailability(params) {
|
|
|
235
247
|
before: service.bufferTimeBefore ?? 0
|
|
236
248
|
};
|
|
237
249
|
}
|
|
238
|
-
} catch
|
|
239
|
-
|
|
250
|
+
} catch (err) {
|
|
251
|
+
trace.dbg('error', {
|
|
252
|
+
err,
|
|
253
|
+
serviceId,
|
|
254
|
+
where: 'bufferFor'
|
|
255
|
+
});
|
|
240
256
|
}
|
|
241
257
|
}
|
|
242
258
|
bufferCache.set(key, result);
|
|
@@ -273,7 +289,12 @@ export async function checkAvailability(params) {
|
|
|
273
289
|
blockedStart: new Date(iv.start),
|
|
274
290
|
units: quantity
|
|
275
291
|
})).filter((o)=>!isNaN(o.blockedStart.getTime()) && !isNaN(o.blockedEnd.getTime()));
|
|
276
|
-
} catch
|
|
292
|
+
} catch (err) {
|
|
293
|
+
trace.dbg('error', {
|
|
294
|
+
err,
|
|
295
|
+
resourceId,
|
|
296
|
+
where: 'getExternalBusy'
|
|
297
|
+
});
|
|
277
298
|
externalOccupancies = [];
|
|
278
299
|
}
|
|
279
300
|
}
|
|
@@ -286,6 +307,18 @@ export async function checkAvailability(params) {
|
|
|
286
307
|
const currentUnits = occupancies.reduce((sum, occ)=>doRangesOverlap(candidateStart, candidateEnd, occ.blockedStart, occ.blockedEnd) ? sum + occ.units : sum, 0);
|
|
287
308
|
const candidateUnits = capacityMode === 'per-guest' ? guestCount : 1;
|
|
288
309
|
const available = currentUnits + candidateUnits <= quantity;
|
|
310
|
+
trace.dbg('check_result', {
|
|
311
|
+
available,
|
|
312
|
+
candidateUnits,
|
|
313
|
+
currentUnits,
|
|
314
|
+
occupancySources: {
|
|
315
|
+
external: externalOccupancies.length,
|
|
316
|
+
fetched: fetchedOccupancies.length,
|
|
317
|
+
sibling: siblingOccupancies.length
|
|
318
|
+
},
|
|
319
|
+
quantity,
|
|
320
|
+
resourceId
|
|
321
|
+
});
|
|
289
322
|
return {
|
|
290
323
|
available,
|
|
291
324
|
currentCount: currentUnits,
|
|
@@ -294,13 +327,25 @@ export async function checkAvailability(params) {
|
|
|
294
327
|
};
|
|
295
328
|
}
|
|
296
329
|
export async function getAvailableSlots(params) {
|
|
297
|
-
const { blockingStatuses, date, getExternalBusy, guestCount, payload, req, reservationSlug, resourceId, resourceIds, resourceSlug, scheduleSlug, serviceId, serviceSlug, timeZone } = params;
|
|
330
|
+
const { blockingStatuses, date, debug, getExternalBusy, guestCount, payload, req, reservationSlug, resourceId, resourceIds, resourceSlug, scheduleSlug, serviceId, serviceSlug, timeZone } = params;
|
|
298
331
|
const tz = timeZone ?? 'UTC';
|
|
299
332
|
// Resolve the set of resources to intersect (single-resource callers still work)
|
|
300
333
|
const ids = resourceIds && resourceIds.length > 0 ? resourceIds : resourceId !== undefined ? [
|
|
301
334
|
resourceId
|
|
302
335
|
] : [];
|
|
336
|
+
const trace = (debug ?? NOOP_RESERVE_DEBUG).child({
|
|
337
|
+
resourceIds: ids,
|
|
338
|
+
serviceId
|
|
339
|
+
});
|
|
340
|
+
trace.dbg('input', {
|
|
341
|
+
date: date instanceof Date ? date.toISOString() : date,
|
|
342
|
+
guestCount: guestCount ?? 1,
|
|
343
|
+
timeZone: tz
|
|
344
|
+
});
|
|
303
345
|
if (ids.length === 0) {
|
|
346
|
+
trace.dbg('empty', {
|
|
347
|
+
reason: 'no_resource_ids'
|
|
348
|
+
});
|
|
304
349
|
return [];
|
|
305
350
|
}
|
|
306
351
|
// 1. Service for duration + buffer times (from the primary service)
|
|
@@ -315,39 +360,75 @@ export async function getAvailableSlots(params) {
|
|
|
315
360
|
const bufferBefore = service.bufferTimeBefore ?? 0;
|
|
316
361
|
const bufferAfter = service.bufferTimeAfter ?? 0;
|
|
317
362
|
const durationType = service.durationType ?? 'fixed';
|
|
363
|
+
trace.dbg('service', {
|
|
364
|
+
bufferAfter,
|
|
365
|
+
bufferBefore,
|
|
366
|
+
duration,
|
|
367
|
+
durationType
|
|
368
|
+
});
|
|
318
369
|
// 2. Per resource: fetch schedules and resolve to windows. A resource with >=1
|
|
319
370
|
// schedule is "schedule-bearing" and constrains time; a resource with zero
|
|
320
371
|
// schedules is capacity-only and contributes no time windows.
|
|
321
372
|
const scheduleBearingWindowLists = [];
|
|
322
373
|
for (const rid of ids){
|
|
374
|
+
const scheduleWhere = {
|
|
375
|
+
and: [
|
|
376
|
+
{
|
|
377
|
+
resource: {
|
|
378
|
+
equals: rid
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
active: {
|
|
383
|
+
equals: true
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
]
|
|
387
|
+
};
|
|
323
388
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
324
389
|
const { docs: schedules } = await payload.find({
|
|
325
390
|
collection: scheduleSlug,
|
|
326
391
|
depth: 0,
|
|
327
392
|
limit: 100,
|
|
328
393
|
req,
|
|
329
|
-
where:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
{
|
|
337
|
-
active: {
|
|
338
|
-
equals: true
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
]
|
|
342
|
-
}
|
|
394
|
+
where: scheduleWhere
|
|
395
|
+
});
|
|
396
|
+
trace.dbg('schedules', {
|
|
397
|
+
activeFlags: schedules.map((s)=>s.active),
|
|
398
|
+
resourceId: rid,
|
|
399
|
+
rowsFound: schedules.length,
|
|
400
|
+
scheduleWhere
|
|
343
401
|
});
|
|
344
402
|
if (!schedules || schedules.length === 0) {
|
|
403
|
+
trace.dbg('skip', {
|
|
404
|
+
reason: 'no_active_schedules',
|
|
405
|
+
resourceId: rid,
|
|
406
|
+
rowsFound: 0
|
|
407
|
+
});
|
|
345
408
|
continue;
|
|
346
409
|
}
|
|
347
410
|
// A11: an exception on ANY of the resource's schedules makes the whole
|
|
348
411
|
// resource unavailable that day — not just the schedule it's recorded on.
|
|
349
|
-
|
|
350
|
-
|
|
412
|
+
let matched;
|
|
413
|
+
for (const s of schedules){
|
|
414
|
+
const exs = s.exceptions ?? [];
|
|
415
|
+
if (isExceptionDate(date, exs, tz)) {
|
|
416
|
+
matched = {
|
|
417
|
+
exception: exs.find((e)=>isExceptionDate(date, [
|
|
418
|
+
e
|
|
419
|
+
], tz)),
|
|
420
|
+
scheduleId: s.id
|
|
421
|
+
};
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (matched) {
|
|
426
|
+
trace.dbg('skip', {
|
|
427
|
+
matchedException: matched.exception,
|
|
428
|
+
reason: 'date_excepted',
|
|
429
|
+
resourceId: rid,
|
|
430
|
+
scheduleId: matched.scheduleId
|
|
431
|
+
});
|
|
351
432
|
scheduleBearingWindowLists.push([]);
|
|
352
433
|
continue;
|
|
353
434
|
}
|
|
@@ -355,10 +436,22 @@ export async function getAvailableSlots(params) {
|
|
|
355
436
|
for (const schedule of schedules){
|
|
356
437
|
windows.push(...resolveScheduleForDate(schedule, date, tz));
|
|
357
438
|
}
|
|
439
|
+
trace.dbg('windows', {
|
|
440
|
+
resourceId: rid,
|
|
441
|
+
windowCount: windows.length,
|
|
442
|
+
windows: windows.map((w)=>({
|
|
443
|
+
end: w.end.toISOString(),
|
|
444
|
+
start: w.start.toISOString()
|
|
445
|
+
})),
|
|
446
|
+
windowsEmpty: windows.length === 0
|
|
447
|
+
});
|
|
358
448
|
scheduleBearingWindowLists.push(windows);
|
|
359
449
|
}
|
|
360
450
|
// No resource constrains time → no basis for generating slots
|
|
361
451
|
if (scheduleBearingWindowLists.length === 0) {
|
|
452
|
+
trace.dbg('empty', {
|
|
453
|
+
reason: 'no_windows'
|
|
454
|
+
});
|
|
362
455
|
return [];
|
|
363
456
|
}
|
|
364
457
|
// 3. Intersect all schedule-bearing window lists
|
|
@@ -367,6 +460,13 @@ export async function getAvailableSlots(params) {
|
|
|
367
460
|
timeRanges = intersectIntervals(timeRanges, scheduleBearingWindowLists[i]);
|
|
368
461
|
}
|
|
369
462
|
if (timeRanges.length === 0) {
|
|
463
|
+
trace.dbg('empty', {
|
|
464
|
+
perResourceWindows: scheduleBearingWindowLists.map((list)=>list.map((w)=>({
|
|
465
|
+
end: w.end.toISOString(),
|
|
466
|
+
start: w.start.toISOString()
|
|
467
|
+
}))),
|
|
468
|
+
reason: 'empty_intersection'
|
|
469
|
+
});
|
|
370
470
|
return [];
|
|
371
471
|
}
|
|
372
472
|
// 4. Candidate slot sizing
|
|
@@ -381,6 +481,11 @@ export async function getAvailableSlots(params) {
|
|
|
381
481
|
});
|
|
382
482
|
const slotDuration = Math.round(slotEndOffset.getTime() / 60_000);
|
|
383
483
|
const effectiveDuration = durationType === 'fixed' ? duration : slotDuration;
|
|
484
|
+
trace.dbg('sizing', {
|
|
485
|
+
effectiveDuration,
|
|
486
|
+
slotDuration,
|
|
487
|
+
stepSize: Math.min(effectiveDuration, 15)
|
|
488
|
+
});
|
|
384
489
|
// Helper: a window is available only if EVERY required resource is free
|
|
385
490
|
const allAvailable = async (start, end, bBefore, bAfter)=>{
|
|
386
491
|
for (const rid of ids){
|
|
@@ -388,6 +493,7 @@ export async function getAvailableSlots(params) {
|
|
|
388
493
|
blockingStatuses,
|
|
389
494
|
bufferAfter: bAfter,
|
|
390
495
|
bufferBefore: bBefore,
|
|
496
|
+
debug: trace,
|
|
391
497
|
endTime: end,
|
|
392
498
|
getExternalBusy,
|
|
393
499
|
guestCount: guestCount ?? 1,
|
|
@@ -408,7 +514,9 @@ export async function getAvailableSlots(params) {
|
|
|
408
514
|
const availableSlots = [];
|
|
409
515
|
// Full-day: offer each range as a single slot if all resources are free
|
|
410
516
|
if (durationType === 'full-day') {
|
|
517
|
+
let candidatesGenerated = 0;
|
|
411
518
|
for (const range of timeRanges){
|
|
519
|
+
candidatesGenerated++;
|
|
412
520
|
if (await allAvailable(range.start, range.end, 0, 0)) {
|
|
413
521
|
availableSlots.push({
|
|
414
522
|
end: range.end,
|
|
@@ -416,9 +524,16 @@ export async function getAvailableSlots(params) {
|
|
|
416
524
|
});
|
|
417
525
|
}
|
|
418
526
|
}
|
|
527
|
+
trace.dbg('result', {
|
|
528
|
+
candidatesAvailable: availableSlots.length,
|
|
529
|
+
candidatesGenerated,
|
|
530
|
+
durationType: 'full-day',
|
|
531
|
+
returnedCount: availableSlots.length
|
|
532
|
+
});
|
|
419
533
|
return availableSlots;
|
|
420
534
|
}
|
|
421
535
|
const stepSize = Math.min(effectiveDuration, 15);
|
|
536
|
+
let candidatesGenerated = 0;
|
|
422
537
|
for (const range of timeRanges){
|
|
423
538
|
let candidateStart = new Date(range.start);
|
|
424
539
|
while(true){
|
|
@@ -426,6 +541,7 @@ export async function getAvailableSlots(params) {
|
|
|
426
541
|
if (candidateEnd > range.end) {
|
|
427
542
|
break;
|
|
428
543
|
}
|
|
544
|
+
candidatesGenerated++;
|
|
429
545
|
if (await allAvailable(candidateStart, candidateEnd, bufferBefore, bufferAfter)) {
|
|
430
546
|
availableSlots.push({
|
|
431
547
|
end: candidateEnd,
|
|
@@ -435,6 +551,11 @@ export async function getAvailableSlots(params) {
|
|
|
435
551
|
candidateStart = addMinutes(candidateStart, stepSize);
|
|
436
552
|
}
|
|
437
553
|
}
|
|
554
|
+
trace.dbg('result', {
|
|
555
|
+
candidatesAvailable: availableSlots.length,
|
|
556
|
+
candidatesGenerated,
|
|
557
|
+
returnedCount: availableSlots.length
|
|
558
|
+
});
|
|
438
559
|
return availableSlots;
|
|
439
560
|
}
|
|
440
561
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/services/AvailabilityService.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport type { CapacityMode, DurationType, GetExternalBusy, StatusMachineConfig } from '../types.js'\nimport type { ResolvedItem } from '../utilities/resolveReservationItems.js'\n\nimport { resolveReservationItems } from '../utilities/resolveReservationItems.js'\nimport { isExceptionDate, resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport {\n addMinutes,\n computeBlockedWindow,\n doRangesOverlap,\n intersectIntervals,\n} from '../utilities/slotUtils.js'\nimport { endOfDayInTimezone } from '../utilities/timezoneUtils.js'\n\n/** A window during which a resource is occupied. Reservation/sibling\n * occupancies are expanded by buffer times; external intervals (from\n * `getExternalBusy`) are used as-is — only the candidate window itself\n * carries buffers when checked against them. */\nexport type Occupancy = { blockedEnd: Date; blockedStart: Date; units: number }\n\n/** Coarse pre-filter widen: covers any realistic neighbor buffer (buffers are\n * minutes). The precise per-item overlap check runs in memory afterwards. */\nconst COARSE_MARGIN_MS = 24 * 60 * 60 * 1000\n\n// --- Pure functions (no DB) ---\n\nexport function computeEndTime(params: {\n durationType: DurationType\n endTime?: Date\n serviceDuration: number\n startTime: Date\n timeZone?: string\n}): { durationMinutes: number; endTime: Date } {\n const { durationType, serviceDuration, startTime } = params\n\n if (durationType === 'full-day') {\n const end = endOfDayInTimezone(startTime, params.timeZone ?? 'UTC')\n const durationMinutes = Math.round((end.getTime() - startTime.getTime()) / 60_000)\n return { durationMinutes, endTime: end }\n }\n\n if (durationType === 'flexible' && params.endTime) {\n const durationMinutes = Math.round(\n (params.endTime.getTime() - startTime.getTime()) / 60_000,\n )\n return { durationMinutes, endTime: params.endTime }\n }\n\n // fixed duration (default)\n const endTime = addMinutes(startTime, serviceDuration)\n return { durationMinutes: serviceDuration, endTime }\n}\n\nexport function buildOverlapQuery(params: {\n blockingStatuses: string[]\n effectiveEnd: Date\n effectiveStart: Date\n excludeReservationId?: number | string\n resourceId: number | string\n}): Where {\n const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } =\n params\n\n const conditions: Where[] = [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\n {\n or: [\n { resource: { equals: resourceId } },\n { 'items.resource': { equals: resourceId } },\n ],\n },\n ]\n\n if (excludeReservationId) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\n/**\n * Coarse superset query: blocking reservations whose top-level (span) window\n * comes within COARSE_MARGIN_MS of the candidate window and reference the\n * resource at top level or in items[]. The precise per-item overlap is computed\n * in memory afterwards — the top-level span is a superset of every item's\n * window, so this never misses a real conflict (margin covers neighbor buffers).\n */\nexport function buildCoarseOverlapQuery(params: {\n blockingStatuses: string[]\n candidateEnd: Date\n candidateStart: Date\n excludeReservationId?: number | string\n resourceId: number | string\n}): Where {\n const { blockingStatuses, candidateEnd, candidateStart, excludeReservationId, resourceId } =\n params\n const windowStart = new Date(candidateStart.getTime() - COARSE_MARGIN_MS)\n const windowEnd = new Date(candidateEnd.getTime() + COARSE_MARGIN_MS)\n\n const conditions: Where[] = [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: windowEnd.toISOString() } },\n { endTime: { greater_than: windowStart.toISOString() } },\n {\n or: [{ resource: { equals: resourceId } }, { 'items.resource': { equals: resourceId } }],\n },\n ]\n\n if (excludeReservationId !== undefined) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\n/**\n * The occupancy windows a set of resolved items imposes on `resourceId`. Each\n * matching item's [startTime, endTime) is expanded by that item's own service\n * buffers (so neighbor buffers are enforced — review A3), and only the items\n * that actually reference `resourceId` count (so a multi-resource booking blocks\n * each resource only for its own item's window — review A4).\n */\nexport async function itemsToOccupancies(params: {\n bufferFor: (serviceId: number | string | undefined) => Promise<{ after: number; before: number }>\n capacityMode: CapacityMode\n items: ResolvedItem[]\n resourceId: number | string\n}): Promise<Occupancy[]> {\n const { bufferFor, capacityMode, items, resourceId } = params\n const occupancies: Occupancy[] = []\n\n for (const item of items) {\n if (String(item.resource) !== String(resourceId) || !item.endTime) {\n continue\n }\n const { after, before } = await bufferFor(item.service)\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n new Date(item.startTime),\n new Date(item.endTime),\n before,\n after,\n )\n occupancies.push({\n blockedEnd: effectiveEnd,\n blockedStart: effectiveStart,\n units: capacityMode === 'per-guest' ? item.guestCount : 1,\n })\n }\n\n return occupancies\n}\n\n/** Occupancy a single fetched reservation imposes on `resourceId`. */\nexport async function reservationOccupancies(params: {\n bufferFor: (serviceId: number | string | undefined) => Promise<{ after: number; before: number }>\n capacityMode: CapacityMode\n reservation: Record<string, unknown>\n resourceId: number | string\n}): Promise<Occupancy[]> {\n const { bufferFor, capacityMode, reservation, resourceId } = params\n return itemsToOccupancies({\n bufferFor,\n capacityMode,\n items: resolveReservationItems(reservation),\n resourceId,\n })\n}\n\nexport function isBlockingStatus(\n status: string,\n statusMachine: StatusMachineConfig,\n): boolean {\n return statusMachine.blockingStatuses.includes(status)\n}\n\nexport function validateTransition(\n fromStatus: string,\n toStatus: string,\n statusMachine: StatusMachineConfig,\n): { reason?: string; valid: boolean } {\n const allowed = statusMachine.transitions[fromStatus]\n if (!allowed) {\n return { reason: `Unknown status: ${fromStatus}`, valid: false }\n }\n if (!allowed.includes(toStatus)) {\n return {\n reason: `Cannot transition from \"${fromStatus}\" to \"${toStatus}\"`,\n valid: false,\n }\n }\n return { valid: true }\n}\n\n// --- DB functions (use Payload Local API only) ---\n\nexport async function checkAvailability(params: {\n blockingStatuses: string[]\n bufferAfter: number\n bufferBefore: number\n endTime: Date\n excludeReservationId?: number | string\n /** External busy resolver (calendar sync etc.) — intervals block the whole resource. */\n getExternalBusy?: GetExternalBusy\n guestCount: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n servicesSlug: string\n /** Other items from the same booking — counted as occupancy (review A5). */\n siblingItems?: ResolvedItem[]\n startTime: Date\n}): Promise<{\n available: boolean\n currentCount: number\n reason?: string\n totalCapacity: number\n}> {\n const {\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime,\n excludeReservationId,\n getExternalBusy,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n servicesSlug,\n siblingItems,\n startTime,\n } = params\n\n // Fetch resource for quantity and capacity mode\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: resourceSlug,\n depth: 0,\n req,\n })\n const quantity = (resource.quantity as number) ?? 1\n const capacityMode = ((resource.capacityMode as string) ?? 'per-reservation') as CapacityMode\n\n // Candidate window expanded by its own buffers\n const { effectiveEnd: candidateEnd, effectiveStart: candidateStart } = computeBlockedWindow(\n startTime,\n endTime,\n bufferBefore,\n bufferAfter,\n )\n\n // Coarse superset fetch — precise per-item overlap is computed in memory below\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({\n collection: reservationSlug,\n depth: 0,\n limit: 0,\n req,\n where: buildCoarseOverlapQuery({\n blockingStatuses,\n candidateEnd,\n candidateStart,\n excludeReservationId,\n resourceId,\n }),\n })\n\n // Per-call cache: fetch each distinct service's buffers at most once\n const bufferCache = new Map<string, { after: number; before: number }>()\n const bufferFor = async (\n serviceId: number | string | undefined,\n ): Promise<{ after: number; before: number }> => {\n const key = serviceId === undefined ? '' : String(serviceId)\n const cached = bufferCache.get(key)\n if (cached) {\n return cached\n }\n let result = { after: 0, before: 0 }\n if (serviceId !== undefined) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: servicesSlug,\n depth: 0,\n req,\n })\n if (service) {\n result = {\n after: (service.bufferTimeAfter as number) ?? 0,\n before: (service.bufferTimeBefore as number) ?? 0,\n }\n }\n } catch {\n // service missing — no buffers\n }\n }\n bufferCache.set(key, result)\n return result\n }\n\n const fetchedOccupancies = (\n await Promise.all(\n (docs as Array<Record<string, unknown>>).map((doc) =>\n reservationOccupancies({ bufferFor, capacityMode, reservation: doc, resourceId }),\n ),\n )\n ).flat()\n\n // Sibling items from the same booking (review A5) — expanded with the same\n // per-service buffers and capacity mode.\n const siblingOccupancies = siblingItems\n ? await itemsToOccupancies({ bufferFor, capacityMode, items: siblingItems, resourceId })\n : []\n\n // External busy (calendar sync etc.) — a synced calendar isn't unit-aware, so\n // an external event blocks the WHOLE resource (units = quantity), not one\n // unit. Fail-open: a resolver error must never block a real booking.\n let externalOccupancies: Occupancy[] = []\n if (getExternalBusy) {\n try {\n const intervals = await getExternalBusy({\n end: candidateEnd,\n req,\n resourceId,\n start: candidateStart,\n })\n externalOccupancies = intervals\n .map((iv) => ({\n blockedEnd: new Date(iv.end),\n blockedStart: new Date(iv.start),\n units: quantity,\n }))\n .filter((o) => !isNaN(o.blockedStart.getTime()) && !isNaN(o.blockedEnd.getTime()))\n } catch {\n externalOccupancies = []\n }\n }\n\n const occupancies = [...fetchedOccupancies, ...siblingOccupancies, ...externalOccupancies]\n\n // Sum the units of every occupancy whose buffered window overlaps the candidate\n const currentUnits = occupancies.reduce(\n (sum, occ) =>\n doRangesOverlap(candidateStart, candidateEnd, occ.blockedStart, occ.blockedEnd)\n ? sum + occ.units\n : sum,\n 0,\n )\n\n const candidateUnits = capacityMode === 'per-guest' ? guestCount : 1\n const available = currentUnits + candidateUnits <= quantity\n\n return {\n available,\n currentCount: currentUnits,\n reason: available\n ? undefined\n : capacityMode === 'per-guest'\n ? 'Guest capacity exceeded'\n : 'All units are booked for this time',\n totalCapacity: quantity,\n }\n}\n\nexport async function getAvailableSlots(params: {\n blockingStatuses: string[]\n date: Date | string\n /** External busy resolver (calendar sync etc.) — intervals block the whole resource. */\n getExternalBusy?: GetExternalBusy\n guestCount?: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId?: number | string\n resourceIds?: Array<number | string>\n resourceSlug: string\n scheduleSlug: string\n serviceId: number | string\n serviceSlug: string\n timeZone?: string\n}): Promise<Array<{ end: Date; start: Date }>> {\n const {\n blockingStatuses,\n date,\n getExternalBusy,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceIds,\n resourceSlug,\n scheduleSlug,\n serviceId,\n serviceSlug,\n timeZone,\n } = params\n\n const tz = timeZone ?? 'UTC'\n\n // Resolve the set of resources to intersect (single-resource callers still work)\n const ids =\n resourceIds && resourceIds.length > 0\n ? resourceIds\n : resourceId !== undefined\n ? [resourceId]\n : []\n if (ids.length === 0) {\n return []\n }\n\n // 1. Service for duration + buffer times (from the primary service)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: serviceSlug,\n depth: 0,\n req,\n })\n const duration = (service.duration as number) ?? 60\n const bufferBefore = (service.bufferTimeBefore as number) ?? 0\n const bufferAfter = (service.bufferTimeAfter as number) ?? 0\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n\n // 2. Per resource: fetch schedules and resolve to windows. A resource with >=1\n // schedule is \"schedule-bearing\" and constrains time; a resource with zero\n // schedules is capacity-only and contributes no time windows.\n const scheduleBearingWindowLists: Array<Array<{ end: Date; start: Date }>> = []\n for (const rid of ids) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: schedules } = await (payload.find as any)({\n collection: scheduleSlug,\n depth: 0,\n limit: 100,\n req,\n where: {\n and: [{ resource: { equals: rid } }, { active: { equals: true } }],\n },\n })\n if (!schedules || schedules.length === 0) {\n continue\n }\n // A11: an exception on ANY of the resource's schedules makes the whole\n // resource unavailable that day — not just the schedule it's recorded on.\n const exceptedToday = (schedules as Array<Record<string, unknown>>).some((s) =>\n isExceptionDate(\n date,\n (s.exceptions as Array<{ date: string; endDate?: string }> | undefined) ?? [],\n tz,\n ),\n )\n if (exceptedToday) {\n scheduleBearingWindowLists.push([])\n continue\n }\n const windows: Array<{ end: Date; start: Date }> = []\n for (const schedule of schedules) {\n windows.push(\n ...resolveScheduleForDate(\n schedule as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n tz,\n ),\n )\n }\n scheduleBearingWindowLists.push(windows)\n }\n\n // No resource constrains time → no basis for generating slots\n if (scheduleBearingWindowLists.length === 0) {\n return []\n }\n\n // 3. Intersect all schedule-bearing window lists\n let timeRanges = scheduleBearingWindowLists[0]\n for (let i = 1; i < scheduleBearingWindowLists.length; i++) {\n timeRanges = intersectIntervals(timeRanges, scheduleBearingWindowLists[i])\n }\n if (timeRanges.length === 0) {\n return []\n }\n\n // 4. Candidate slot sizing\n // NOTE: epoch-trick sizing is only meaningful for fixed/flexible durations.\n // full-day services return early via the range-as-slot branch below and never\n // consume slotDuration — keep it that way if reordering this function.\n const { endTime: slotEndOffset } = computeEndTime({\n durationType,\n serviceDuration: duration,\n startTime: new Date(0),\n timeZone: tz,\n })\n const slotDuration = Math.round(slotEndOffset.getTime() / 60_000)\n const effectiveDuration = durationType === 'fixed' ? duration : slotDuration\n\n // Helper: a window is available only if EVERY required resource is free\n const allAvailable = async (\n start: Date,\n end: Date,\n bBefore: number,\n bAfter: number,\n ): Promise<boolean> => {\n for (const rid of ids) {\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter: bAfter,\n bufferBefore: bBefore,\n endTime: end,\n getExternalBusy,\n guestCount: guestCount ?? 1,\n payload,\n req,\n reservationSlug,\n resourceId: rid,\n resourceSlug,\n servicesSlug: serviceSlug,\n startTime: start,\n })\n if (!result.available) {\n return false\n }\n }\n return true\n }\n\n const availableSlots: Array<{ end: Date; start: Date }> = []\n\n // Full-day: offer each range as a single slot if all resources are free\n if (durationType === 'full-day') {\n for (const range of timeRanges) {\n if (await allAvailable(range.start, range.end, 0, 0)) {\n availableSlots.push({ end: range.end, start: range.start })\n }\n }\n return availableSlots\n }\n\n const stepSize = Math.min(effectiveDuration, 15)\n\n for (const range of timeRanges) {\n let candidateStart = new Date(range.start)\n\n while (true) {\n const candidateEnd = addMinutes(candidateStart, effectiveDuration)\n if (candidateEnd > range.end) {\n break\n }\n\n if (await allAvailable(candidateStart, candidateEnd, bufferBefore, bufferAfter)) {\n availableSlots.push({ end: candidateEnd, start: new Date(candidateStart) })\n }\n\n candidateStart = addMinutes(candidateStart, stepSize)\n }\n }\n\n return availableSlots\n}\n"],"names":["resolveReservationItems","isExceptionDate","resolveScheduleForDate","addMinutes","computeBlockedWindow","doRangesOverlap","intersectIntervals","endOfDayInTimezone","COARSE_MARGIN_MS","computeEndTime","params","durationType","serviceDuration","startTime","end","timeZone","durationMinutes","Math","round","getTime","endTime","buildOverlapQuery","blockingStatuses","effectiveEnd","effectiveStart","excludeReservationId","resourceId","conditions","status","in","less_than","toISOString","greater_than","or","resource","equals","push","id","not_equals","and","buildCoarseOverlapQuery","candidateEnd","candidateStart","windowStart","Date","windowEnd","undefined","itemsToOccupancies","bufferFor","capacityMode","items","occupancies","item","String","after","before","service","blockedEnd","blockedStart","units","guestCount","reservationOccupancies","reservation","isBlockingStatus","statusMachine","includes","validateTransition","fromStatus","toStatus","allowed","transitions","reason","valid","checkAvailability","bufferAfter","bufferBefore","getExternalBusy","payload","req","reservationSlug","resourceSlug","servicesSlug","siblingItems","findByID","collection","depth","quantity","docs","find","limit","where","bufferCache","Map","serviceId","key","cached","get","result","bufferTimeAfter","bufferTimeBefore","set","fetchedOccupancies","Promise","all","map","doc","flat","siblingOccupancies","externalOccupancies","intervals","start","iv","filter","o","isNaN","currentUnits","reduce","sum","occ","candidateUnits","available","currentCount","totalCapacity","getAvailableSlots","date","resourceIds","scheduleSlug","serviceSlug","tz","ids","length","duration","scheduleBearingWindowLists","rid","schedules","active","exceptedToday","some","s","exceptions","windows","schedule","timeRanges","i","slotEndOffset","slotDuration","effectiveDuration","allAvailable","bBefore","bAfter","availableSlots","range","stepSize","min"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/services/AvailabilityService.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport type { CapacityMode, DurationType, GetExternalBusy, StatusMachineConfig } from '../types.js'\nimport type { ResolvedItem } from '../utilities/resolveReservationItems.js'\n\nimport { NOOP_RESERVE_DEBUG, type ReserveDebug } from '../utilities/reserveDebug.js'\nimport { resolveReservationItems } from '../utilities/resolveReservationItems.js'\nimport { isExceptionDate, resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport {\n addMinutes,\n computeBlockedWindow,\n doRangesOverlap,\n intersectIntervals,\n} from '../utilities/slotUtils.js'\nimport { endOfDayInTimezone } from '../utilities/timezoneUtils.js'\n\n/** A window during which a resource is occupied. Reservation/sibling\n * occupancies are expanded by buffer times; external intervals (from\n * `getExternalBusy`) are used as-is — only the candidate window itself\n * carries buffers when checked against them. */\nexport type Occupancy = { blockedEnd: Date; blockedStart: Date; units: number }\n\n/** Coarse pre-filter widen: covers any realistic neighbor buffer (buffers are\n * minutes). The precise per-item overlap check runs in memory afterwards. */\nconst COARSE_MARGIN_MS = 24 * 60 * 60 * 1000\n\n// --- Pure functions (no DB) ---\n\nexport function computeEndTime(params: {\n durationType: DurationType\n endTime?: Date\n serviceDuration: number\n startTime: Date\n timeZone?: string\n}): { durationMinutes: number; endTime: Date } {\n const { durationType, serviceDuration, startTime } = params\n\n if (durationType === 'full-day') {\n const end = endOfDayInTimezone(startTime, params.timeZone ?? 'UTC')\n const durationMinutes = Math.round((end.getTime() - startTime.getTime()) / 60_000)\n return { durationMinutes, endTime: end }\n }\n\n if (durationType === 'flexible' && params.endTime) {\n const durationMinutes = Math.round(\n (params.endTime.getTime() - startTime.getTime()) / 60_000,\n )\n return { durationMinutes, endTime: params.endTime }\n }\n\n // fixed duration (default)\n const endTime = addMinutes(startTime, serviceDuration)\n return { durationMinutes: serviceDuration, endTime }\n}\n\nexport function buildOverlapQuery(params: {\n blockingStatuses: string[]\n effectiveEnd: Date\n effectiveStart: Date\n excludeReservationId?: number | string\n resourceId: number | string\n}): Where {\n const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } =\n params\n\n const conditions: Where[] = [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\n {\n or: [\n { resource: { equals: resourceId } },\n { 'items.resource': { equals: resourceId } },\n ],\n },\n ]\n\n if (excludeReservationId) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\n/**\n * Coarse superset query: blocking reservations whose top-level (span) window\n * comes within COARSE_MARGIN_MS of the candidate window and reference the\n * resource at top level or in items[]. The precise per-item overlap is computed\n * in memory afterwards — the top-level span is a superset of every item's\n * window, so this never misses a real conflict (margin covers neighbor buffers).\n */\nexport function buildCoarseOverlapQuery(params: {\n blockingStatuses: string[]\n candidateEnd: Date\n candidateStart: Date\n excludeReservationId?: number | string\n resourceId: number | string\n}): Where {\n const { blockingStatuses, candidateEnd, candidateStart, excludeReservationId, resourceId } =\n params\n const windowStart = new Date(candidateStart.getTime() - COARSE_MARGIN_MS)\n const windowEnd = new Date(candidateEnd.getTime() + COARSE_MARGIN_MS)\n\n const conditions: Where[] = [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: windowEnd.toISOString() } },\n { endTime: { greater_than: windowStart.toISOString() } },\n {\n or: [{ resource: { equals: resourceId } }, { 'items.resource': { equals: resourceId } }],\n },\n ]\n\n if (excludeReservationId !== undefined) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\n/**\n * The occupancy windows a set of resolved items imposes on `resourceId`. Each\n * matching item's [startTime, endTime) is expanded by that item's own service\n * buffers (so neighbor buffers are enforced — review A3), and only the items\n * that actually reference `resourceId` count (so a multi-resource booking blocks\n * each resource only for its own item's window — review A4).\n */\nexport async function itemsToOccupancies(params: {\n bufferFor: (serviceId: number | string | undefined) => Promise<{ after: number; before: number }>\n capacityMode: CapacityMode\n items: ResolvedItem[]\n resourceId: number | string\n}): Promise<Occupancy[]> {\n const { bufferFor, capacityMode, items, resourceId } = params\n const occupancies: Occupancy[] = []\n\n for (const item of items) {\n if (String(item.resource) !== String(resourceId) || !item.endTime) {\n continue\n }\n const { after, before } = await bufferFor(item.service)\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n new Date(item.startTime),\n new Date(item.endTime),\n before,\n after,\n )\n occupancies.push({\n blockedEnd: effectiveEnd,\n blockedStart: effectiveStart,\n units: capacityMode === 'per-guest' ? item.guestCount : 1,\n })\n }\n\n return occupancies\n}\n\n/** Occupancy a single fetched reservation imposes on `resourceId`. */\nexport async function reservationOccupancies(params: {\n bufferFor: (serviceId: number | string | undefined) => Promise<{ after: number; before: number }>\n capacityMode: CapacityMode\n reservation: Record<string, unknown>\n resourceId: number | string\n}): Promise<Occupancy[]> {\n const { bufferFor, capacityMode, reservation, resourceId } = params\n return itemsToOccupancies({\n bufferFor,\n capacityMode,\n items: resolveReservationItems(reservation),\n resourceId,\n })\n}\n\nexport function isBlockingStatus(\n status: string,\n statusMachine: StatusMachineConfig,\n): boolean {\n return statusMachine.blockingStatuses.includes(status)\n}\n\nexport function validateTransition(\n fromStatus: string,\n toStatus: string,\n statusMachine: StatusMachineConfig,\n): { reason?: string; valid: boolean } {\n const allowed = statusMachine.transitions[fromStatus]\n if (!allowed) {\n return { reason: `Unknown status: ${fromStatus}`, valid: false }\n }\n if (!allowed.includes(toStatus)) {\n return {\n reason: `Cannot transition from \"${fromStatus}\" to \"${toStatus}\"`,\n valid: false,\n }\n }\n return { valid: true }\n}\n\n// --- DB functions (use Payload Local API only) ---\n\nexport async function checkAvailability(params: {\n blockingStatuses: string[]\n bufferAfter: number\n bufferBefore: number\n /** Optional tracer — emits check/check_result/error lines when enabled. */\n debug?: ReserveDebug\n endTime: Date\n excludeReservationId?: number | string\n /** External busy resolver (calendar sync etc.) — intervals block the whole resource. */\n getExternalBusy?: GetExternalBusy\n guestCount: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n servicesSlug: string\n /** Other items from the same booking — counted as occupancy (review A5). */\n siblingItems?: ResolvedItem[]\n startTime: Date\n}): Promise<{\n available: boolean\n currentCount: number\n reason?: string\n totalCapacity: number\n}> {\n const {\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n debug,\n endTime,\n excludeReservationId,\n getExternalBusy,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n servicesSlug,\n siblingItems,\n startTime,\n } = params\n\n const trace = debug ?? NOOP_RESERVE_DEBUG\n\n // Fetch resource for quantity and capacity mode\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: resourceSlug,\n depth: 0,\n req,\n })\n const quantity = (resource.quantity as number) ?? 1\n const capacityMode = ((resource.capacityMode as string) ?? 'per-reservation') as CapacityMode\n\n // Candidate window expanded by its own buffers\n const { effectiveEnd: candidateEnd, effectiveStart: candidateStart } = computeBlockedWindow(\n startTime,\n endTime,\n bufferBefore,\n bufferAfter,\n )\n\n // Coarse superset fetch — precise per-item overlap is computed in memory below\n const coarseWhere = buildCoarseOverlapQuery({\n blockingStatuses,\n candidateEnd,\n candidateStart,\n excludeReservationId,\n resourceId,\n })\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({\n collection: reservationSlug,\n depth: 0,\n limit: 0,\n req,\n where: coarseWhere,\n })\n\n trace.dbg('check', {\n blockingReservations: (docs as unknown[]).length,\n candidateEnd: candidateEnd.toISOString(),\n candidateStart: candidateStart.toISOString(),\n capacityMode,\n coarseWhere,\n quantity,\n resourceId,\n })\n\n // Per-call cache: fetch each distinct service's buffers at most once\n const bufferCache = new Map<string, { after: number; before: number }>()\n const bufferFor = async (\n serviceId: number | string | undefined,\n ): Promise<{ after: number; before: number }> => {\n const key = serviceId === undefined ? '' : String(serviceId)\n const cached = bufferCache.get(key)\n if (cached) {\n return cached\n }\n let result = { after: 0, before: 0 }\n if (serviceId !== undefined) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: servicesSlug,\n depth: 0,\n req,\n })\n if (service) {\n result = {\n after: (service.bufferTimeAfter as number) ?? 0,\n before: (service.bufferTimeBefore as number) ?? 0,\n }\n }\n } catch (err) {\n trace.dbg('error', { err, serviceId, where: 'bufferFor' })\n }\n }\n bufferCache.set(key, result)\n return result\n }\n\n const fetchedOccupancies = (\n await Promise.all(\n (docs as Array<Record<string, unknown>>).map((doc) =>\n reservationOccupancies({ bufferFor, capacityMode, reservation: doc, resourceId }),\n ),\n )\n ).flat()\n\n // Sibling items from the same booking (review A5) — expanded with the same\n // per-service buffers and capacity mode.\n const siblingOccupancies = siblingItems\n ? await itemsToOccupancies({ bufferFor, capacityMode, items: siblingItems, resourceId })\n : []\n\n // External busy (calendar sync etc.) — a synced calendar isn't unit-aware, so\n // an external event blocks the WHOLE resource (units = quantity), not one\n // unit. Fail-open: a resolver error must never block a real booking.\n let externalOccupancies: Occupancy[] = []\n if (getExternalBusy) {\n try {\n const intervals = await getExternalBusy({\n end: candidateEnd,\n req,\n resourceId,\n start: candidateStart,\n })\n externalOccupancies = intervals\n .map((iv) => ({\n blockedEnd: new Date(iv.end),\n blockedStart: new Date(iv.start),\n units: quantity,\n }))\n .filter((o) => !isNaN(o.blockedStart.getTime()) && !isNaN(o.blockedEnd.getTime()))\n } catch (err) {\n trace.dbg('error', { err, resourceId, where: 'getExternalBusy' })\n externalOccupancies = []\n }\n }\n\n const occupancies = [...fetchedOccupancies, ...siblingOccupancies, ...externalOccupancies]\n\n // Sum the units of every occupancy whose buffered window overlaps the candidate\n const currentUnits = occupancies.reduce(\n (sum, occ) =>\n doRangesOverlap(candidateStart, candidateEnd, occ.blockedStart, occ.blockedEnd)\n ? sum + occ.units\n : sum,\n 0,\n )\n\n const candidateUnits = capacityMode === 'per-guest' ? guestCount : 1\n const available = currentUnits + candidateUnits <= quantity\n\n trace.dbg('check_result', {\n available,\n candidateUnits,\n currentUnits,\n occupancySources: {\n external: externalOccupancies.length,\n fetched: fetchedOccupancies.length,\n sibling: siblingOccupancies.length,\n },\n quantity,\n resourceId,\n })\n\n return {\n available,\n currentCount: currentUnits,\n reason: available\n ? undefined\n : capacityMode === 'per-guest'\n ? 'Guest capacity exceeded'\n : 'All units are booked for this time',\n totalCapacity: quantity,\n }\n}\n\nexport async function getAvailableSlots(params: {\n blockingStatuses: string[]\n date: Date | string\n /** Optional tracer — emits per-stage slot-generation lines when enabled. */\n debug?: ReserveDebug\n /** External busy resolver (calendar sync etc.) — intervals block the whole resource. */\n getExternalBusy?: GetExternalBusy\n guestCount?: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId?: number | string\n resourceIds?: Array<number | string>\n resourceSlug: string\n scheduleSlug: string\n serviceId: number | string\n serviceSlug: string\n timeZone?: string\n}): Promise<Array<{ end: Date; start: Date }>> {\n const {\n blockingStatuses,\n date,\n debug,\n getExternalBusy,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceIds,\n resourceSlug,\n scheduleSlug,\n serviceId,\n serviceSlug,\n timeZone,\n } = params\n\n const tz = timeZone ?? 'UTC'\n\n // Resolve the set of resources to intersect (single-resource callers still work)\n const ids =\n resourceIds && resourceIds.length > 0\n ? resourceIds\n : resourceId !== undefined\n ? [resourceId]\n : []\n\n const trace = (debug ?? NOOP_RESERVE_DEBUG).child({ resourceIds: ids, serviceId })\n trace.dbg('input', {\n date: date instanceof Date ? date.toISOString() : date,\n guestCount: guestCount ?? 1,\n timeZone: tz,\n })\n\n if (ids.length === 0) {\n trace.dbg('empty', { reason: 'no_resource_ids' })\n return []\n }\n\n // 1. Service for duration + buffer times (from the primary service)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: serviceSlug,\n depth: 0,\n req,\n })\n const duration = (service.duration as number) ?? 60\n const bufferBefore = (service.bufferTimeBefore as number) ?? 0\n const bufferAfter = (service.bufferTimeAfter as number) ?? 0\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n trace.dbg('service', { bufferAfter, bufferBefore, duration, durationType })\n\n // 2. Per resource: fetch schedules and resolve to windows. A resource with >=1\n // schedule is \"schedule-bearing\" and constrains time; a resource with zero\n // schedules is capacity-only and contributes no time windows.\n const scheduleBearingWindowLists: Array<Array<{ end: Date; start: Date }>> = []\n for (const rid of ids) {\n const scheduleWhere = { and: [{ resource: { equals: rid } }, { active: { equals: true } }] }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: schedules } = await (payload.find as any)({\n collection: scheduleSlug,\n depth: 0,\n limit: 100,\n req,\n where: scheduleWhere,\n })\n trace.dbg('schedules', {\n activeFlags: (schedules as Array<Record<string, unknown>>).map((s) => s.active),\n resourceId: rid,\n rowsFound: (schedules as unknown[]).length,\n scheduleWhere,\n })\n if (!schedules || schedules.length === 0) {\n trace.dbg('skip', { reason: 'no_active_schedules', resourceId: rid, rowsFound: 0 })\n continue\n }\n // A11: an exception on ANY of the resource's schedules makes the whole\n // resource unavailable that day — not just the schedule it's recorded on.\n let matched: { exception: unknown; scheduleId: unknown } | undefined\n for (const s of schedules as Array<Record<string, unknown>>) {\n const exs = (s.exceptions as Array<{ date: string; endDate?: string }> | undefined) ?? []\n if (isExceptionDate(date, exs, tz)) {\n matched = { exception: exs.find((e) => isExceptionDate(date, [e], tz)), scheduleId: s.id }\n break\n }\n }\n if (matched) {\n trace.dbg('skip', {\n matchedException: matched.exception,\n reason: 'date_excepted',\n resourceId: rid,\n scheduleId: matched.scheduleId,\n })\n scheduleBearingWindowLists.push([])\n continue\n }\n const windows: Array<{ end: Date; start: Date }> = []\n for (const schedule of schedules) {\n windows.push(\n ...resolveScheduleForDate(\n schedule as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n tz,\n ),\n )\n }\n trace.dbg('windows', {\n resourceId: rid,\n windowCount: windows.length,\n windows: windows.map((w) => ({ end: w.end.toISOString(), start: w.start.toISOString() })),\n windowsEmpty: windows.length === 0,\n })\n scheduleBearingWindowLists.push(windows)\n }\n\n // No resource constrains time → no basis for generating slots\n if (scheduleBearingWindowLists.length === 0) {\n trace.dbg('empty', { reason: 'no_windows' })\n return []\n }\n\n // 3. Intersect all schedule-bearing window lists\n let timeRanges = scheduleBearingWindowLists[0]\n for (let i = 1; i < scheduleBearingWindowLists.length; i++) {\n timeRanges = intersectIntervals(timeRanges, scheduleBearingWindowLists[i])\n }\n if (timeRanges.length === 0) {\n trace.dbg('empty', {\n perResourceWindows: scheduleBearingWindowLists.map((list) =>\n list.map((w) => ({ end: w.end.toISOString(), start: w.start.toISOString() })),\n ),\n reason: 'empty_intersection',\n })\n return []\n }\n\n // 4. Candidate slot sizing\n // NOTE: epoch-trick sizing is only meaningful for fixed/flexible durations.\n // full-day services return early via the range-as-slot branch below and never\n // consume slotDuration — keep it that way if reordering this function.\n const { endTime: slotEndOffset } = computeEndTime({\n durationType,\n serviceDuration: duration,\n startTime: new Date(0),\n timeZone: tz,\n })\n const slotDuration = Math.round(slotEndOffset.getTime() / 60_000)\n const effectiveDuration = durationType === 'fixed' ? duration : slotDuration\n trace.dbg('sizing', {\n effectiveDuration,\n slotDuration,\n stepSize: Math.min(effectiveDuration, 15),\n })\n\n // Helper: a window is available only if EVERY required resource is free\n const allAvailable = async (\n start: Date,\n end: Date,\n bBefore: number,\n bAfter: number,\n ): Promise<boolean> => {\n for (const rid of ids) {\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter: bAfter,\n bufferBefore: bBefore,\n debug: trace,\n endTime: end,\n getExternalBusy,\n guestCount: guestCount ?? 1,\n payload,\n req,\n reservationSlug,\n resourceId: rid,\n resourceSlug,\n servicesSlug: serviceSlug,\n startTime: start,\n })\n if (!result.available) {\n return false\n }\n }\n return true\n }\n\n const availableSlots: Array<{ end: Date; start: Date }> = []\n\n // Full-day: offer each range as a single slot if all resources are free\n if (durationType === 'full-day') {\n let candidatesGenerated = 0\n for (const range of timeRanges) {\n candidatesGenerated++\n if (await allAvailable(range.start, range.end, 0, 0)) {\n availableSlots.push({ end: range.end, start: range.start })\n }\n }\n trace.dbg('result', {\n candidatesAvailable: availableSlots.length,\n candidatesGenerated,\n durationType: 'full-day',\n returnedCount: availableSlots.length,\n })\n return availableSlots\n }\n\n const stepSize = Math.min(effectiveDuration, 15)\n let candidatesGenerated = 0\n\n for (const range of timeRanges) {\n let candidateStart = new Date(range.start)\n\n while (true) {\n const candidateEnd = addMinutes(candidateStart, effectiveDuration)\n if (candidateEnd > range.end) {\n break\n }\n\n candidatesGenerated++\n if (await allAvailable(candidateStart, candidateEnd, bufferBefore, bufferAfter)) {\n availableSlots.push({ end: candidateEnd, start: new Date(candidateStart) })\n }\n\n candidateStart = addMinutes(candidateStart, stepSize)\n }\n }\n\n trace.dbg('result', {\n candidatesAvailable: availableSlots.length,\n candidatesGenerated,\n returnedCount: availableSlots.length,\n })\n return availableSlots\n}\n"],"names":["NOOP_RESERVE_DEBUG","resolveReservationItems","isExceptionDate","resolveScheduleForDate","addMinutes","computeBlockedWindow","doRangesOverlap","intersectIntervals","endOfDayInTimezone","COARSE_MARGIN_MS","computeEndTime","params","durationType","serviceDuration","startTime","end","timeZone","durationMinutes","Math","round","getTime","endTime","buildOverlapQuery","blockingStatuses","effectiveEnd","effectiveStart","excludeReservationId","resourceId","conditions","status","in","less_than","toISOString","greater_than","or","resource","equals","push","id","not_equals","and","buildCoarseOverlapQuery","candidateEnd","candidateStart","windowStart","Date","windowEnd","undefined","itemsToOccupancies","bufferFor","capacityMode","items","occupancies","item","String","after","before","service","blockedEnd","blockedStart","units","guestCount","reservationOccupancies","reservation","isBlockingStatus","statusMachine","includes","validateTransition","fromStatus","toStatus","allowed","transitions","reason","valid","checkAvailability","bufferAfter","bufferBefore","debug","getExternalBusy","payload","req","reservationSlug","resourceSlug","servicesSlug","siblingItems","trace","findByID","collection","depth","quantity","coarseWhere","docs","find","limit","where","dbg","blockingReservations","length","bufferCache","Map","serviceId","key","cached","get","result","bufferTimeAfter","bufferTimeBefore","err","set","fetchedOccupancies","Promise","all","map","doc","flat","siblingOccupancies","externalOccupancies","intervals","start","iv","filter","o","isNaN","currentUnits","reduce","sum","occ","candidateUnits","available","occupancySources","external","fetched","sibling","currentCount","totalCapacity","getAvailableSlots","date","resourceIds","scheduleSlug","serviceSlug","tz","ids","child","duration","scheduleBearingWindowLists","rid","scheduleWhere","active","schedules","activeFlags","s","rowsFound","matched","exs","exceptions","exception","e","scheduleId","matchedException","windows","schedule","windowCount","w","windowsEmpty","timeRanges","i","perResourceWindows","list","slotEndOffset","slotDuration","effectiveDuration","stepSize","min","allAvailable","bBefore","bAfter","availableSlots","candidatesGenerated","range","candidatesAvailable","returnedCount"],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -162,6 +162,8 @@ export type ReservationPluginConfig = {
|
|
|
162
162
|
schedules?: CollectionOverride;
|
|
163
163
|
services?: CollectionOverride;
|
|
164
164
|
};
|
|
165
|
+
/** Emit info-level `reserve_debug` traces for slot generation and conflict detection (default false) */
|
|
166
|
+
debug?: boolean;
|
|
165
167
|
/** Default buffer time in minutes between reservations */
|
|
166
168
|
defaultBufferTime?: number;
|
|
167
169
|
/** Disable the plugin entirely */
|
|
@@ -227,6 +229,7 @@ export type ResolvedReservationPluginConfig = {
|
|
|
227
229
|
schedules?: CollectionOverride;
|
|
228
230
|
services?: CollectionOverride;
|
|
229
231
|
};
|
|
232
|
+
debug: boolean;
|
|
230
233
|
defaultBufferTime: number;
|
|
231
234
|
disabled: boolean;
|
|
232
235
|
extraReservationFields: Field[];
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionConfig, Field, PayloadRequest } from 'payload'\n\n// --- Duration & Capacity models ---\n\nexport type DurationType = 'fixed' | 'flexible' | 'full-day'\n\nexport type CapacityMode = 'per-guest' | 'per-reservation'\n\n// --- Configurable status machine ---\n\nexport type StatusMachineConfig = {\n blockingStatuses: string[]\n /** Status treated as \"cancelled\" — fires beforeBookingCancel/afterBookingCancel, the cancellation notice period, and the cancellationReason field. */\n cancelStatus: string\n /** Status treated as \"confirmed\" — fires beforeBookingConfirm/afterBookingConfirm. */\n confirmStatus: string\n defaultStatus: string\n statuses: string[]\n terminalStatuses: string[]\n transitions: Record<string, string[]>\n}\n\nexport const DEFAULT_STATUS_MACHINE: StatusMachineConfig = {\n blockingStatuses: ['pending', 'confirmed'],\n cancelStatus: 'cancelled',\n confirmStatus: 'confirmed',\n defaultStatus: 'pending',\n statuses: ['pending', 'confirmed', 'completed', 'cancelled', 'no-show'],\n terminalStatuses: ['completed', 'cancelled', 'no-show'],\n transitions: {\n cancelled: [],\n completed: [],\n confirmed: ['completed', 'cancelled', 'no-show'],\n 'no-show': [],\n pending: ['confirmed', 'cancelled'],\n },\n}\n\n// --- Reservation item (for multi-resource bookings, Phase 3) ---\n\nexport type ReservationItemConfig = {\n endTime?: string\n guestCount?: number\n resource: string\n service?: string\n startTime?: string\n}\n\n// --- Plugin hooks for external integrations ---\n\nexport type ReservationPluginHooks = {\n afterBookingCancel?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingConfirm?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingCreate?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterStatusChange?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n previousStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCancel?: Array<\n (args: {\n doc: Record<string, unknown>\n reason?: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingConfirm?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCreate?: Array<\n (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n >\n}\n\n// --- Resource owner mode ---\n\nexport type ResourceOwnerModeConfig = {\n /** Roles that can see all records (default: check req.user.collection === adminCollection) */\n adminRoles?: string[]\n /** Whether Services also get an owner field (default: false — Services are platform-managed) */\n ownedServices?: boolean\n /**\n * Collection the owner field relates to (where owners/staff live). Defaults to\n * `staffProvisioning.userCollection` when set, otherwise `slugs.customers`. Set\n * this when owners live in a different collection than your customers (e.g.\n * separate `users` and `customers` collections).\n */\n ownerCollection?: string\n /** Field name for the owner relationship on Resources (default: 'owner') */\n ownerField?: string\n /** User field holding the role for admin detection (default: staffProvisioning.roleField or 'role') */\n roleField?: string\n}\n\nexport type ResolvedResourceOwnerModeConfig = {\n adminRoles: string[]\n ownedServices: boolean\n ownerCollection?: string\n ownerField: string\n roleField: string\n}\n\n// --- Staff provisioning ---\n\nexport type StaffProvisioningConfig = {\n /** Stamp tenant / custom fields onto the provisioned Resource before create. */\n beforeCreate?: (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n user: Record<string, unknown>\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n /** User field copied into Resource `name` (default 'name', falls back to email). */\n nameFrom?: string\n /** resourceType to stamp (default 'staff'). Must be a valid resourceType. */\n resourceType?: string\n /** Field on the user holding the role (default 'role'). */\n roleField?: string\n /** Role value(s) marking a user as staff. Required, non-empty. */\n staffRoles: string[]\n /** Auth collection holding staff users. Defaults to top-level `userCollection`. */\n userCollection?: string\n}\n\nexport type ResolvedStaffProvisioningConfig = {\n beforeCreate?: StaffProvisioningConfig['beforeCreate']\n nameFrom: string\n resourceType: string\n roleField: string\n staffRoles: string[]\n userCollection: string\n}\n\n// --- Plugin configuration ---\n\n/**\n * Per-collection override applied to a generated collection. `fields` is a\n * function receiving the plugin's default fields so you can append/reorder/\n * replace them; supplied `hooks` are merged with (not replacing) the plugin's;\n * `access` composes per operation; `slug` is ignored (use the `slugs` option).\n */\nexport type CollectionOverride = {\n fields?: (args: { defaultFields: Field[] }) => Field[]\n} & Omit<Partial<CollectionConfig>, 'fields' | 'slug'>\n\n/** One external busy interval returned by `getExternalBusy` (ISO date strings). */\nexport type ExternalBusyInterval = { end: string; label?: string; start: string }\n\n/**\n * App-supplied resolver mapping an external source (e.g. Google/Outlook sync\n * tables) to busy intervals for one resource over [start, end). Wired into\n * checkAvailability (bookings overlapping an interval are unavailable — the\n * interval blocks the WHOLE resource, all units) and into the\n * resource-availability endpoint (returned as `external` for distinct\n * rendering). The plugin calls it inside try/catch and treats an error as []\n * (fail-open): a calendar/sync failure must never block a real booking.\n * Called once per candidate window during slot computation (N calls per\n * request) — keep it cheap: read a local sync table or a per-request cache,\n * never a remote API directly.\n */\nexport type GetExternalBusy = (args: {\n end: Date\n req: PayloadRequest\n resourceId: number | string\n start: Date\n}) => Promise<ExternalBusyInterval[]>\n\nexport type ReservationPluginConfig = {\n /** Override access control per collection */\n access?: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n /** Admin group name for all reservation collections */\n adminGroup?: string\n /** Allow bookings without a customer account by default (per-service override available) */\n allowGuestBooking?: boolean\n /** Hours of notice required before cancellation */\n cancellationNoticePeriod?: number\n /** Per-collection overrides applied to the generated collections (issue #4) */\n collectionOverrides?: {\n customers?: CollectionOverride\n reservations?: CollectionOverride\n resources?: CollectionOverride\n schedules?: CollectionOverride\n services?: CollectionOverride\n }\n /** Default buffer time in minutes between reservations */\n defaultBufferTime?: number\n /** Disable the plugin entirely */\n disabled?: boolean\n /** @deprecated Use `collectionOverrides.reservations.fields` instead. Extra fields appended to Reservations. */\n extraReservationFields?: Field[]\n /** Resolve external busy intervals (calendar sync etc.) folded into availability + calendar display. */\n getExternalBusy?: GetExternalBusy\n /** Plugin hooks for external integrations */\n hooks?: ReservationPluginHooks\n /** Configurable leave/exception type vocabulary (default: vacation/sick/personal/closure/other) */\n leaveTypes?: string[]\n /** Tenant scoping for the custom admin views (calendar, availability, dashboard). Applied only when the scoped collection has the tenant field AND the tenant cookie is set. */\n multiTenant?: {\n /** Cookie written by the tenant-selector (default 'payload-tenant'). */\n cookieName?: string\n /** Tenant field name on scoped collections (default 'tenant'). */\n tenantField?: string\n /**\n * Field on the tenant document holding its IANA timezone (default 'timezone').\n * When set and the selected tenant has a valid value, the admin views resolve\n * day-boundaries in that tenant's zone instead of the global `timezone`.\n */\n timezoneField?: string\n }\n /** Enable resource-owner multi-tenancy (opt-in) */\n resourceOwnerMode?: ResourceOwnerModeConfig\n /** Configurable resourceType vocabulary (default: staff/equipment/room) */\n resourceTypes?: string[]\n /** Override collection slugs */\n slugs?: {\n customers?: string\n media?: string\n reservations?: string\n resources?: string\n schedules?: string\n services?: string\n }\n /** Auto-provision a Resource from staff-role users (opt-in; requires resourceOwnerMode) */\n staffProvisioning?: StaffProvisioningConfig\n /** Configurable status machine (defaults to current behavior) */\n statusMachine?: Partial<StatusMachineConfig>\n /** IANA business timezone governing schedules and day boundaries (default 'UTC') */\n timezone?: string\n /** Which existing auth collection to extend with customer fields */\n userCollection?: string\n}\n\nexport type ResolvedReservationPluginConfig = {\n access: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n adminGroup: string\n allowGuestBooking: boolean\n cancellationNoticePeriod: number\n collectionOverrides: {\n customers?: CollectionOverride\n reservations?: CollectionOverride\n resources?: CollectionOverride\n schedules?: CollectionOverride\n services?: CollectionOverride\n }\n defaultBufferTime: number\n disabled: boolean\n extraReservationFields: Field[]\n getExternalBusy: GetExternalBusy | undefined\n /** Whether the media collection (`slugs.media`) exists — set by the plugin; gates the image upload fields. */\n hasMediaCollection: boolean\n hooks: ReservationPluginHooks\n leaveTypes: string[]\n localized: boolean\n multiTenant: {\n cookieName: string\n tenantField: string\n timezoneField: string\n }\n resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined\n resourceTypes: string[]\n slugs: {\n customers: string\n media: string\n reservations: string\n resources: string\n schedules: string\n services: string\n }\n staffProvisioning: ResolvedStaffProvisioningConfig | undefined\n statusMachine: StatusMachineConfig\n timezone: string\n userCollection: string | undefined\n}\n\nexport type ReservationStatus = 'cancelled' | 'completed' | 'confirmed' | 'no-show' | 'pending'\n\nexport type DayOfWeek = 'fri' | 'mon' | 'sat' | 'sun' | 'thu' | 'tue' | 'wed'\n\nexport type ScheduleType = 'manual' | 'recurring'\n\n/** @deprecated Use DEFAULT_STATUS_MACHINE.transitions instead */\nexport const VALID_STATUS_TRANSITIONS: Record<ReservationStatus, ReservationStatus[]> =\n DEFAULT_STATUS_MACHINE.transitions as Record<ReservationStatus, ReservationStatus[]>\n"],"names":["DEFAULT_STATUS_MACHINE","blockingStatuses","cancelStatus","confirmStatus","defaultStatus","statuses","terminalStatuses","transitions","cancelled","completed","confirmed","pending","VALID_STATUS_TRANSITIONS"],"mappings":"AAsBA,OAAO,MAAMA,yBAA8C;IACzDC,kBAAkB;QAAC;QAAW;KAAY;IAC1CC,cAAc;IACdC,eAAe;IACfC,eAAe;IACfC,UAAU;QAAC;QAAW;QAAa;QAAa;QAAa;KAAU;IACvEC,kBAAkB;QAAC;QAAa;QAAa;KAAU;IACvDC,aAAa;QACXC,WAAW,EAAE;QACbC,WAAW,EAAE;QACbC,WAAW;YAAC;YAAa;YAAa;SAAU;QAChD,WAAW,EAAE;QACbC,SAAS;YAAC;YAAa;SAAY;IACrC;AACF,EAAC;AA+QD,+DAA+D,GAC/D,OAAO,MAAMC,2BACXZ,uBAAuBO,WAAW,CAAkD"}
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionConfig, Field, PayloadRequest } from 'payload'\n\n// --- Duration & Capacity models ---\n\nexport type DurationType = 'fixed' | 'flexible' | 'full-day'\n\nexport type CapacityMode = 'per-guest' | 'per-reservation'\n\n// --- Configurable status machine ---\n\nexport type StatusMachineConfig = {\n blockingStatuses: string[]\n /** Status treated as \"cancelled\" — fires beforeBookingCancel/afterBookingCancel, the cancellation notice period, and the cancellationReason field. */\n cancelStatus: string\n /** Status treated as \"confirmed\" — fires beforeBookingConfirm/afterBookingConfirm. */\n confirmStatus: string\n defaultStatus: string\n statuses: string[]\n terminalStatuses: string[]\n transitions: Record<string, string[]>\n}\n\nexport const DEFAULT_STATUS_MACHINE: StatusMachineConfig = {\n blockingStatuses: ['pending', 'confirmed'],\n cancelStatus: 'cancelled',\n confirmStatus: 'confirmed',\n defaultStatus: 'pending',\n statuses: ['pending', 'confirmed', 'completed', 'cancelled', 'no-show'],\n terminalStatuses: ['completed', 'cancelled', 'no-show'],\n transitions: {\n cancelled: [],\n completed: [],\n confirmed: ['completed', 'cancelled', 'no-show'],\n 'no-show': [],\n pending: ['confirmed', 'cancelled'],\n },\n}\n\n// --- Reservation item (for multi-resource bookings, Phase 3) ---\n\nexport type ReservationItemConfig = {\n endTime?: string\n guestCount?: number\n resource: string\n service?: string\n startTime?: string\n}\n\n// --- Plugin hooks for external integrations ---\n\nexport type ReservationPluginHooks = {\n afterBookingCancel?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingConfirm?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingCreate?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterStatusChange?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n previousStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCancel?: Array<\n (args: {\n doc: Record<string, unknown>\n reason?: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingConfirm?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCreate?: Array<\n (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n >\n}\n\n// --- Resource owner mode ---\n\nexport type ResourceOwnerModeConfig = {\n /** Roles that can see all records (default: check req.user.collection === adminCollection) */\n adminRoles?: string[]\n /** Whether Services also get an owner field (default: false — Services are platform-managed) */\n ownedServices?: boolean\n /**\n * Collection the owner field relates to (where owners/staff live). Defaults to\n * `staffProvisioning.userCollection` when set, otherwise `slugs.customers`. Set\n * this when owners live in a different collection than your customers (e.g.\n * separate `users` and `customers` collections).\n */\n ownerCollection?: string\n /** Field name for the owner relationship on Resources (default: 'owner') */\n ownerField?: string\n /** User field holding the role for admin detection (default: staffProvisioning.roleField or 'role') */\n roleField?: string\n}\n\nexport type ResolvedResourceOwnerModeConfig = {\n adminRoles: string[]\n ownedServices: boolean\n ownerCollection?: string\n ownerField: string\n roleField: string\n}\n\n// --- Staff provisioning ---\n\nexport type StaffProvisioningConfig = {\n /** Stamp tenant / custom fields onto the provisioned Resource before create. */\n beforeCreate?: (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n user: Record<string, unknown>\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n /** User field copied into Resource `name` (default 'name', falls back to email). */\n nameFrom?: string\n /** resourceType to stamp (default 'staff'). Must be a valid resourceType. */\n resourceType?: string\n /** Field on the user holding the role (default 'role'). */\n roleField?: string\n /** Role value(s) marking a user as staff. Required, non-empty. */\n staffRoles: string[]\n /** Auth collection holding staff users. Defaults to top-level `userCollection`. */\n userCollection?: string\n}\n\nexport type ResolvedStaffProvisioningConfig = {\n beforeCreate?: StaffProvisioningConfig['beforeCreate']\n nameFrom: string\n resourceType: string\n roleField: string\n staffRoles: string[]\n userCollection: string\n}\n\n// --- Plugin configuration ---\n\n/**\n * Per-collection override applied to a generated collection. `fields` is a\n * function receiving the plugin's default fields so you can append/reorder/\n * replace them; supplied `hooks` are merged with (not replacing) the plugin's;\n * `access` composes per operation; `slug` is ignored (use the `slugs` option).\n */\nexport type CollectionOverride = {\n fields?: (args: { defaultFields: Field[] }) => Field[]\n} & Omit<Partial<CollectionConfig>, 'fields' | 'slug'>\n\n/** One external busy interval returned by `getExternalBusy` (ISO date strings). */\nexport type ExternalBusyInterval = { end: string; label?: string; start: string }\n\n/**\n * App-supplied resolver mapping an external source (e.g. Google/Outlook sync\n * tables) to busy intervals for one resource over [start, end). Wired into\n * checkAvailability (bookings overlapping an interval are unavailable — the\n * interval blocks the WHOLE resource, all units) and into the\n * resource-availability endpoint (returned as `external` for distinct\n * rendering). The plugin calls it inside try/catch and treats an error as []\n * (fail-open): a calendar/sync failure must never block a real booking.\n * Called once per candidate window during slot computation (N calls per\n * request) — keep it cheap: read a local sync table or a per-request cache,\n * never a remote API directly.\n */\nexport type GetExternalBusy = (args: {\n end: Date\n req: PayloadRequest\n resourceId: number | string\n start: Date\n}) => Promise<ExternalBusyInterval[]>\n\nexport type ReservationPluginConfig = {\n /** Override access control per collection */\n access?: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n /** Admin group name for all reservation collections */\n adminGroup?: string\n /** Allow bookings without a customer account by default (per-service override available) */\n allowGuestBooking?: boolean\n /** Hours of notice required before cancellation */\n cancellationNoticePeriod?: number\n /** Per-collection overrides applied to the generated collections (issue #4) */\n collectionOverrides?: {\n customers?: CollectionOverride\n reservations?: CollectionOverride\n resources?: CollectionOverride\n schedules?: CollectionOverride\n services?: CollectionOverride\n }\n /** Emit info-level `reserve_debug` traces for slot generation and conflict detection (default false) */\n debug?: boolean\n /** Default buffer time in minutes between reservations */\n defaultBufferTime?: number\n /** Disable the plugin entirely */\n disabled?: boolean\n /** @deprecated Use `collectionOverrides.reservations.fields` instead. Extra fields appended to Reservations. */\n extraReservationFields?: Field[]\n /** Resolve external busy intervals (calendar sync etc.) folded into availability + calendar display. */\n getExternalBusy?: GetExternalBusy\n /** Plugin hooks for external integrations */\n hooks?: ReservationPluginHooks\n /** Configurable leave/exception type vocabulary (default: vacation/sick/personal/closure/other) */\n leaveTypes?: string[]\n /** Tenant scoping for the custom admin views (calendar, availability, dashboard). Applied only when the scoped collection has the tenant field AND the tenant cookie is set. */\n multiTenant?: {\n /** Cookie written by the tenant-selector (default 'payload-tenant'). */\n cookieName?: string\n /** Tenant field name on scoped collections (default 'tenant'). */\n tenantField?: string\n /**\n * Field on the tenant document holding its IANA timezone (default 'timezone').\n * When set and the selected tenant has a valid value, the admin views resolve\n * day-boundaries in that tenant's zone instead of the global `timezone`.\n */\n timezoneField?: string\n }\n /** Enable resource-owner multi-tenancy (opt-in) */\n resourceOwnerMode?: ResourceOwnerModeConfig\n /** Configurable resourceType vocabulary (default: staff/equipment/room) */\n resourceTypes?: string[]\n /** Override collection slugs */\n slugs?: {\n customers?: string\n media?: string\n reservations?: string\n resources?: string\n schedules?: string\n services?: string\n }\n /** Auto-provision a Resource from staff-role users (opt-in; requires resourceOwnerMode) */\n staffProvisioning?: StaffProvisioningConfig\n /** Configurable status machine (defaults to current behavior) */\n statusMachine?: Partial<StatusMachineConfig>\n /** IANA business timezone governing schedules and day boundaries (default 'UTC') */\n timezone?: string\n /** Which existing auth collection to extend with customer fields */\n userCollection?: string\n}\n\nexport type ResolvedReservationPluginConfig = {\n access: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n adminGroup: string\n allowGuestBooking: boolean\n cancellationNoticePeriod: number\n collectionOverrides: {\n customers?: CollectionOverride\n reservations?: CollectionOverride\n resources?: CollectionOverride\n schedules?: CollectionOverride\n services?: CollectionOverride\n }\n debug: boolean\n defaultBufferTime: number\n disabled: boolean\n extraReservationFields: Field[]\n getExternalBusy: GetExternalBusy | undefined\n /** Whether the media collection (`slugs.media`) exists — set by the plugin; gates the image upload fields. */\n hasMediaCollection: boolean\n hooks: ReservationPluginHooks\n leaveTypes: string[]\n localized: boolean\n multiTenant: {\n cookieName: string\n tenantField: string\n timezoneField: string\n }\n resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined\n resourceTypes: string[]\n slugs: {\n customers: string\n media: string\n reservations: string\n resources: string\n schedules: string\n services: string\n }\n staffProvisioning: ResolvedStaffProvisioningConfig | undefined\n statusMachine: StatusMachineConfig\n timezone: string\n userCollection: string | undefined\n}\n\nexport type ReservationStatus = 'cancelled' | 'completed' | 'confirmed' | 'no-show' | 'pending'\n\nexport type DayOfWeek = 'fri' | 'mon' | 'sat' | 'sun' | 'thu' | 'tue' | 'wed'\n\nexport type ScheduleType = 'manual' | 'recurring'\n\n/** @deprecated Use DEFAULT_STATUS_MACHINE.transitions instead */\nexport const VALID_STATUS_TRANSITIONS: Record<ReservationStatus, ReservationStatus[]> =\n DEFAULT_STATUS_MACHINE.transitions as Record<ReservationStatus, ReservationStatus[]>\n"],"names":["DEFAULT_STATUS_MACHINE","blockingStatuses","cancelStatus","confirmStatus","defaultStatus","statuses","terminalStatuses","transitions","cancelled","completed","confirmed","pending","VALID_STATUS_TRANSITIONS"],"mappings":"AAsBA,OAAO,MAAMA,yBAA8C;IACzDC,kBAAkB;QAAC;QAAW;KAAY;IAC1CC,cAAc;IACdC,eAAe;IACfC,eAAe;IACfC,UAAU;QAAC;QAAW;QAAa;QAAa;QAAa;KAAU;IACvEC,kBAAkB;QAAC;QAAa;QAAa;KAAU;IACvDC,aAAa;QACXC,WAAW,EAAE;QACbC,WAAW,EAAE;QACbC,WAAW;YAAC;YAAa;YAAa;SAAU;QAChD,WAAW,EAAE;QACbC,SAAS;YAAC;YAAa;SAAY;IACrC;AACF,EAAC;AAkRD,+DAA+D,GAC/D,OAAO,MAAMC,2BACXZ,uBAAuBO,WAAW,CAAkD"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Structured fields attached to one debug line. */
|
|
2
|
+
export type ReserveDebugFields = Record<string, unknown>;
|
|
3
|
+
/** Minimal logger surface — Payload's Pino `logger.info(obj, msg?)`. */
|
|
4
|
+
type DebugLogger = {
|
|
5
|
+
info: (obj: unknown, msg?: string) => void;
|
|
6
|
+
};
|
|
7
|
+
export type ReserveDebug = {
|
|
8
|
+
/** New instance, same traceId, merging baseFields into every subsequent line. */
|
|
9
|
+
child: (baseFields: ReserveDebugFields) => ReserveDebug;
|
|
10
|
+
/** Emit one trace line (no-op when disabled). */
|
|
11
|
+
dbg: (stage: string, fields?: ReserveDebugFields) => void;
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
traceId: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Create a debug tracer. When `enabled`, every `dbg(stage, fields)` emits a
|
|
17
|
+
* single Pino line:
|
|
18
|
+
* `logger.info({ event: 'reserve_debug', traceId, stage, ...base, ...fields }, 'reserve_debug')`.
|
|
19
|
+
* When disabled, `dbg` is an immediate no-op. Emitting at INFO is deliberate:
|
|
20
|
+
* the opt-in flag is the gate, so traces survive Pino's default info level in
|
|
21
|
+
* production (a `.debug()` line would be silently dropped there).
|
|
22
|
+
*/
|
|
23
|
+
export declare function createReserveDebug(logger: DebugLogger, enabled: boolean, traceId?: string, baseFields?: ReserveDebugFields): ReserveDebug;
|
|
24
|
+
/** Shared disabled tracer — the default for functions whose `debug` param is omitted. */
|
|
25
|
+
export declare const NOOP_RESERVE_DEBUG: ReserveDebug;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Structured fields attached to one debug line. */ /**
|
|
2
|
+
* Create a debug tracer. When `enabled`, every `dbg(stage, fields)` emits a
|
|
3
|
+
* single Pino line:
|
|
4
|
+
* `logger.info({ event: 'reserve_debug', traceId, stage, ...base, ...fields }, 'reserve_debug')`.
|
|
5
|
+
* When disabled, `dbg` is an immediate no-op. Emitting at INFO is deliberate:
|
|
6
|
+
* the opt-in flag is the gate, so traces survive Pino's default info level in
|
|
7
|
+
* production (a `.debug()` line would be silently dropped there).
|
|
8
|
+
*/ export function createReserveDebug(logger, enabled, traceId, baseFields = {}) {
|
|
9
|
+
const id = traceId ?? Math.random().toString(36).slice(2, 10);
|
|
10
|
+
return {
|
|
11
|
+
child: (extra)=>createReserveDebug(logger, enabled, id, {
|
|
12
|
+
...baseFields,
|
|
13
|
+
...extra
|
|
14
|
+
}),
|
|
15
|
+
dbg: (stage, fields = {})=>{
|
|
16
|
+
if (!enabled) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
logger.info({
|
|
20
|
+
event: 'reserve_debug',
|
|
21
|
+
stage,
|
|
22
|
+
traceId: id,
|
|
23
|
+
...baseFields,
|
|
24
|
+
...fields
|
|
25
|
+
}, 'reserve_debug');
|
|
26
|
+
},
|
|
27
|
+
enabled,
|
|
28
|
+
traceId: id
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Shared disabled tracer — the default for functions whose `debug` param is omitted. */ export const NOOP_RESERVE_DEBUG = createReserveDebug({
|
|
32
|
+
info: ()=>{}
|
|
33
|
+
}, false);
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=reserveDebug.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/reserveDebug.ts"],"sourcesContent":["/** Structured fields attached to one debug line. */\nexport type ReserveDebugFields = Record<string, unknown>\n\n/** Minimal logger surface — Payload's Pino `logger.info(obj, msg?)`. */\ntype DebugLogger = { info: (obj: unknown, msg?: string) => void }\n\nexport type ReserveDebug = {\n /** New instance, same traceId, merging baseFields into every subsequent line. */\n child: (baseFields: ReserveDebugFields) => ReserveDebug\n /** Emit one trace line (no-op when disabled). */\n dbg: (stage: string, fields?: ReserveDebugFields) => void\n enabled: boolean\n traceId: string\n}\n\n/**\n * Create a debug tracer. When `enabled`, every `dbg(stage, fields)` emits a\n * single Pino line:\n * `logger.info({ event: 'reserve_debug', traceId, stage, ...base, ...fields }, 'reserve_debug')`.\n * When disabled, `dbg` is an immediate no-op. Emitting at INFO is deliberate:\n * the opt-in flag is the gate, so traces survive Pino's default info level in\n * production (a `.debug()` line would be silently dropped there).\n */\nexport function createReserveDebug(\n logger: DebugLogger,\n enabled: boolean,\n traceId?: string,\n baseFields: ReserveDebugFields = {},\n): ReserveDebug {\n const id = traceId ?? Math.random().toString(36).slice(2, 10)\n return {\n child: (extra) => createReserveDebug(logger, enabled, id, { ...baseFields, ...extra }),\n dbg: (stage, fields = {}) => {\n if (!enabled) {\n return\n }\n logger.info(\n { event: 'reserve_debug', stage, traceId: id, ...baseFields, ...fields },\n 'reserve_debug',\n )\n },\n enabled,\n traceId: id,\n }\n}\n\n/** Shared disabled tracer — the default for functions whose `debug` param is omitted. */\nexport const NOOP_RESERVE_DEBUG: ReserveDebug = createReserveDebug({ info: () => {} }, false)\n"],"names":["createReserveDebug","logger","enabled","traceId","baseFields","id","Math","random","toString","slice","child","extra","dbg","stage","fields","info","event","NOOP_RESERVE_DEBUG"],"mappings":"AAAA,kDAAkD,GAelD;;;;;;;CAOC,GACD,OAAO,SAASA,mBACdC,MAAmB,EACnBC,OAAgB,EAChBC,OAAgB,EAChBC,aAAiC,CAAC,CAAC;IAEnC,MAAMC,KAAKF,WAAWG,KAAKC,MAAM,GAAGC,QAAQ,CAAC,IAAIC,KAAK,CAAC,GAAG;IAC1D,OAAO;QACLC,OAAO,CAACC,QAAUX,mBAAmBC,QAAQC,SAASG,IAAI;gBAAE,GAAGD,UAAU;gBAAE,GAAGO,KAAK;YAAC;QACpFC,KAAK,CAACC,OAAOC,SAAS,CAAC,CAAC;YACtB,IAAI,CAACZ,SAAS;gBACZ;YACF;YACAD,OAAOc,IAAI,CACT;gBAAEC,OAAO;gBAAiBH;gBAAOV,SAASE;gBAAI,GAAGD,UAAU;gBAAE,GAAGU,MAAM;YAAC,GACvE;QAEJ;QACAZ;QACAC,SAASE;IACX;AACF;AAEA,uFAAuF,GACvF,OAAO,MAAMY,qBAAmCjB,mBAAmB;IAAEe,MAAM,KAAO;AAAE,GAAG,OAAM"}
|
package/package.json
CHANGED