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.
@@ -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: buildCoarseOverlapQuery({
204
- blockingStatuses,
205
- candidateEnd,
206
- candidateStart,
207
- excludeReservationId,
208
- resourceId
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
- // service missing — no buffers
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
- and: [
331
- {
332
- resource: {
333
- equals: rid
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
- const exceptedToday = schedules.some((s)=>isExceptionDate(date, s.exceptions ?? [], tz));
350
- if (exceptedToday) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-reserve",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "A Payload CMS 3.x plugin for reservation and booking management with conflict detection, status workflows, and calendar UI",
5
5
  "keywords": [
6
6
  "payload",