payload-reserve 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +217 -4
  2. package/dist/collections/Holds.d.ts +18 -0
  3. package/dist/collections/Holds.js +113 -0
  4. package/dist/collections/Holds.js.map +1 -0
  5. package/dist/collections/Reservations.js +8 -0
  6. package/dist/collections/Reservations.js.map +1 -1
  7. package/dist/collections/Resources.js +35 -1
  8. package/dist/collections/Resources.js.map +1 -1
  9. package/dist/collections/Services.js +41 -0
  10. package/dist/collections/Services.js.map +1 -1
  11. package/dist/components/CalendarView/LaneTimelineView.js +4 -5
  12. package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
  13. package/dist/components/CalendarView/index.js +55 -71
  14. package/dist/components/CalendarView/index.js.map +1 -1
  15. package/dist/components/DashboardWidget/DashboardWidgetServer.js +11 -74
  16. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  17. package/dist/components/DashboardWidget/fetchDashboardStats.d.ts +42 -0
  18. package/dist/components/DashboardWidget/fetchDashboardStats.js +113 -0
  19. package/dist/components/DashboardWidget/fetchDashboardStats.js.map +1 -0
  20. package/dist/defaults.d.ts +2 -0
  21. package/dist/defaults.js +11 -0
  22. package/dist/defaults.js.map +1 -1
  23. package/dist/endpoints/cancelBooking.js +43 -14
  24. package/dist/endpoints/cancelBooking.js.map +1 -1
  25. package/dist/endpoints/checkAvailability.js +12 -1
  26. package/dist/endpoints/checkAvailability.js.map +1 -1
  27. package/dist/endpoints/createBooking.js +108 -7
  28. package/dist/endpoints/createBooking.js.map +1 -1
  29. package/dist/endpoints/customerSearch.js +6 -0
  30. package/dist/endpoints/customerSearch.js.map +1 -1
  31. package/dist/endpoints/effectiveTimezone.js +1 -0
  32. package/dist/endpoints/effectiveTimezone.js.map +1 -1
  33. package/dist/endpoints/getSlots.js +12 -1
  34. package/dist/endpoints/getSlots.js.map +1 -1
  35. package/dist/endpoints/holdSlot.d.ts +12 -0
  36. package/dist/endpoints/holdSlot.js +126 -0
  37. package/dist/endpoints/holdSlot.js.map +1 -0
  38. package/dist/endpoints/releaseSlot.d.ts +8 -0
  39. package/dist/endpoints/releaseSlot.js +34 -0
  40. package/dist/endpoints/releaseSlot.js.map +1 -0
  41. package/dist/endpoints/resourceAvailability.d.ts +2 -0
  42. package/dist/endpoints/resourceAvailability.js +79 -3
  43. package/dist/endpoints/resourceAvailability.js.map +1 -1
  44. package/dist/hooks/holds/validateHoldSlot.d.ts +14 -0
  45. package/dist/hooks/holds/validateHoldSlot.js +90 -0
  46. package/dist/hooks/holds/validateHoldSlot.js.map +1 -0
  47. package/dist/hooks/reservations/acquireBookingLock.d.ts +30 -0
  48. package/dist/hooks/reservations/acquireBookingLock.js +71 -0
  49. package/dist/hooks/reservations/acquireBookingLock.js.map +1 -0
  50. package/dist/hooks/reservations/calculateEndTime.js +13 -1
  51. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  52. package/dist/hooks/reservations/expandRequiredResources.js +3 -0
  53. package/dist/hooks/reservations/expandRequiredResources.js.map +1 -1
  54. package/dist/hooks/reservations/validateActive.d.ts +11 -0
  55. package/dist/hooks/reservations/validateActive.js +103 -0
  56. package/dist/hooks/reservations/validateActive.js.map +1 -0
  57. package/dist/hooks/reservations/validateConflicts.js +7 -1
  58. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  59. package/dist/hooks/reservations/validateGuestBooking.js +3 -0
  60. package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
  61. package/dist/hooks/shared/preventDeleteWhenReferenced.d.ts +42 -0
  62. package/dist/hooks/shared/preventDeleteWhenReferenced.js +87 -0
  63. package/dist/hooks/shared/preventDeleteWhenReferenced.js.map +1 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/plugin.js +179 -3
  67. package/dist/plugin.js.map +1 -1
  68. package/dist/services/AvailabilityService.d.ts +46 -5
  69. package/dist/services/AvailabilityService.js +168 -12
  70. package/dist/services/AvailabilityService.js.map +1 -1
  71. package/dist/services/HoldService.d.ts +53 -0
  72. package/dist/services/HoldService.js +169 -0
  73. package/dist/services/HoldService.js.map +1 -0
  74. package/dist/services/index.d.ts +1 -0
  75. package/dist/services/index.js.map +1 -1
  76. package/dist/translations/ar.json +5 -1
  77. package/dist/translations/de.json +5 -1
  78. package/dist/translations/en.json +5 -1
  79. package/dist/translations/es.json +5 -1
  80. package/dist/translations/fa.json +5 -1
  81. package/dist/translations/fr.json +5 -1
  82. package/dist/translations/hi.json +5 -1
  83. package/dist/translations/id.json +5 -1
  84. package/dist/translations/pl.json +5 -1
  85. package/dist/translations/ru.json +5 -1
  86. package/dist/translations/tr.json +5 -1
  87. package/dist/translations/zh.json +5 -1
  88. package/dist/types.d.ts +29 -0
  89. package/dist/types.js.map +1 -1
  90. package/dist/utilities/calendarGrid.d.ts +44 -0
  91. package/dist/utilities/calendarGrid.js +67 -0
  92. package/dist/utilities/calendarGrid.js.map +1 -0
  93. package/dist/utilities/reservationChanges.d.ts +6 -1
  94. package/dist/utilities/reservationChanges.js +1 -1
  95. package/dist/utilities/reservationChanges.js.map +1 -1
  96. package/dist/utilities/resolveReservationItems.d.ts +13 -2
  97. package/dist/utilities/resolveReservationItems.js +87 -2
  98. package/dist/utilities/resolveReservationItems.js.map +1 -1
  99. package/dist/utilities/retryOnWriteConflict.d.ts +66 -0
  100. package/dist/utilities/retryOnWriteConflict.js +135 -0
  101. package/dist/utilities/retryOnWriteConflict.js.map +1 -0
  102. package/dist/utilities/tenantTimezone.d.ts +77 -1
  103. package/dist/utilities/tenantTimezone.js +163 -2
  104. package/dist/utilities/tenantTimezone.js.map +1 -1
  105. package/dist/utilities/transactionSupport.d.ts +13 -0
  106. package/dist/utilities/transactionSupport.js +30 -0
  107. package/dist/utilities/transactionSupport.js.map +1 -0
  108. package/package.json +13 -13
@@ -149,12 +149,23 @@ export function buildOverlapQuery(params) {
149
149
  }
150
150
  return occupancies;
151
151
  }
152
- /** Occupancy a single fetched reservation imposes on `resourceId`. */ export async function reservationOccupancies(params) {
152
+ /**
153
+ * Occupancy a single fetched reservation imposes on `resourceId`.
154
+ *
155
+ * Resolves `reservation` LENIENTLY: this is a stored document, which — unlike
156
+ * a write in progress — can carry an inverted top-level window from a
157
+ * `context.skipReservationHooks` write or data predating that check. Lenient
158
+ * mode skips parent synthesis instead of throwing, so a malformed top-level
159
+ * window never discards the real `items[]` occupancies also present on the
160
+ * same row (see the lenient-mode note in resolveReservationItems.ts).
161
+ */ export async function reservationOccupancies(params) {
153
162
  const { bufferFor, capacityMode, reservation, resourceId } = params;
154
163
  return itemsToOccupancies({
155
164
  bufferFor,
156
165
  capacityMode,
157
- items: resolveReservationItems(reservation),
166
+ items: resolveReservationItems(reservation, {
167
+ lenient: true
168
+ }),
158
169
  resourceId
159
170
  });
160
171
  }
@@ -181,7 +192,7 @@ export function validateTransition(fromStatus, toStatus, statusMachine) {
181
192
  }
182
193
  // --- DB functions (use Payload Local API only) ---
183
194
  export async function checkAvailability(params) {
184
- const { blockingStatuses, bufferAfter, bufferBefore, debug, endTime, excludeReservationId, getExternalBusy, guestCount, payload, req, reservationSlug, resourceId, resourceSlug, servicesSlug, siblingItems, startTime } = params;
195
+ const { blockingStatuses, bufferAfter, bufferBefore, debug, endTime, excludeHoldToken, excludeReservationId, getExternalBusy, guestCount, holdsSlug, payload, req, reservationSlug, resourceId, resourceSlug, servicesSlug, siblingItems, startTime } = params;
185
196
  const trace = debug ?? NOOP_RESERVE_DEBUG;
186
197
  // Fetch resource for quantity and capacity mode
187
198
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -239,6 +250,9 @@ export async function checkAvailability(params) {
239
250
  id: serviceId,
240
251
  collection: servicesSlug,
241
252
  depth: 0,
253
+ // Skip the resources join — internal logic never reads it, and without this
254
+ // every service read becomes an aggregation with a $lookup.
255
+ joins: false,
242
256
  req
243
257
  });
244
258
  if (service) {
@@ -258,12 +272,30 @@ export async function checkAvailability(params) {
258
272
  bufferCache.set(key, result);
259
273
  return result;
260
274
  };
261
- const fetchedOccupancies = (await Promise.all(docs.map((doc)=>reservationOccupancies({
275
+ // reservationOccupancies resolves each doc LENIENTLY (see its own doc
276
+ // comment) — an inverted top-level window on a stored row skips parent
277
+ // synthesis rather than throwing, so this never needs a try/catch: every
278
+ // real items[] occupancy on the row still comes back.
279
+ //
280
+ // SEQUENTIAL, not Promise.all, and that is load-bearing. Each iteration can
281
+ // reach `bufferFor` -> `payload.findByID` on the shared `req`, and this whole
282
+ // function runs inside the booking's own transaction on the write path. A
283
+ // MongoDB ClientSession cannot carry concurrent operations inside a
284
+ // transaction: when two do collide, the loser's `count`/`findByID` calls
285
+ // `killTransaction` from its own catch, which rolls back and clears the
286
+ // transaction the enclosing create/delete owns, and the survivor then reports
287
+ // `NoSuchTransaction` ("transaction number N does not match any in-progress
288
+ // transactions"). Buffers are per-service and cached, so the loop is at most
289
+ // one read per DISTINCT neighbouring service either way.
290
+ const fetchedOccupancies = [];
291
+ for (const doc of docs){
292
+ fetchedOccupancies.push(...await reservationOccupancies({
262
293
  bufferFor,
263
294
  capacityMode,
264
295
  reservation: doc,
265
296
  resourceId
266
- })))).flat();
297
+ }));
298
+ }
267
299
  // Sibling items from the same booking (review A5) — expanded with the same
268
300
  // per-service buffers and capacity mode.
269
301
  const siblingOccupancies = siblingItems ? await itemsToOccupancies({
@@ -298,10 +330,76 @@ export async function checkAvailability(params) {
298
330
  externalOccupancies = [];
299
331
  }
300
332
  }
333
+ // Unexpired slot holds occupy the resource exactly as a blocking reservation
334
+ // does. Expiry is a read-time predicate rather than a TTL index so the
335
+ // behaviour is identical on Mongo, Postgres and SQLite. `excludeHoldToken`
336
+ // is the bearer converting their OWN hold into a booking — without it the
337
+ // hold would block the very booking it was taken to protect.
338
+ const holdOccupancies = [];
339
+ if (holdsSlug) {
340
+ const holdWhere = {
341
+ and: [
342
+ {
343
+ resource: {
344
+ equals: resourceId
345
+ }
346
+ },
347
+ {
348
+ expiresAt: {
349
+ greater_than: new Date().toISOString()
350
+ }
351
+ },
352
+ {
353
+ startTime: {
354
+ less_than: candidateEnd.toISOString()
355
+ }
356
+ },
357
+ {
358
+ endTime: {
359
+ greater_than: candidateStart.toISOString()
360
+ }
361
+ },
362
+ ...excludeHoldToken ? [
363
+ {
364
+ token: {
365
+ not_equals: excludeHoldToken
366
+ }
367
+ }
368
+ ] : []
369
+ ]
370
+ };
371
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
372
+ const { docs: holdDocs } = await payload.find({
373
+ collection: holdsSlug,
374
+ depth: 0,
375
+ limit: 0,
376
+ req,
377
+ where: holdWhere
378
+ });
379
+ // Sequential for the same reason the reservation loop above is: `bufferFor`
380
+ // reads through the shared `req`, and concurrent operations on one MongoDB
381
+ // session inside a transaction abort it.
382
+ for (const hold of holdDocs){
383
+ // Expand by the held service's own buffers, so a hold reserves the same
384
+ // real-world window the resulting booking will.
385
+ const { after, before } = await bufferFor(hold.service);
386
+ const { effectiveEnd, effectiveStart } = computeBlockedWindow(new Date(hold.startTime), new Date(hold.endTime), before, after);
387
+ holdOccupancies.push({
388
+ blockedEnd: effectiveEnd,
389
+ blockedStart: effectiveStart,
390
+ units: capacityMode === 'per-guest' ? hold.guestCount ?? 1 : 1
391
+ });
392
+ }
393
+ trace.dbg('check_holds', {
394
+ activeHolds: holdOccupancies.length,
395
+ resourceId
396
+ });
397
+ }
301
398
  const occupancies = [
302
399
  ...fetchedOccupancies,
303
400
  ...siblingOccupancies,
304
- ...externalOccupancies
401
+ ...externalOccupancies,
402
+ ...holdOccupancies
305
403
  ];
306
404
  // Sum the units of every occupancy whose buffered window overlaps the candidate
307
405
  const currentUnits = occupancies.reduce((sum, occ)=>doRangesOverlap(candidateStart, candidateEnd, occ.blockedStart, occ.blockedEnd) ? sum + occ.units : sum, 0);
@@ -327,7 +425,7 @@ export async function checkAvailability(params) {
327
425
  };
328
426
  }
329
427
  export async function getAvailableSlots(params) {
330
- const { blockingStatuses, date, debug, getExternalBusy, guestCount, payload, req, reservationSlug, resourceId, resourceIds, resourceSlug, scheduleSlug, serviceId, serviceSlug, timeZone } = params;
428
+ const { blockingStatuses, date, debug, enforceActive, getExternalBusy, guestCount, holdsSlug, payload, req, reservationSlug, resourceId, resourceIds, resourceSlug, scheduleSlug, serviceId, serviceSlug, timeZone } = params;
331
429
  const tz = timeZone ?? 'UTC';
332
430
  // Resolve the set of resources to intersect (single-resource callers still work)
333
431
  const ids = resourceIds && resourceIds.length > 0 ? resourceIds : resourceId !== undefined ? [
@@ -346,7 +444,10 @@ export async function getAvailableSlots(params) {
346
444
  trace.dbg('empty', {
347
445
  reason: 'no_resource_ids'
348
446
  });
349
- return [];
447
+ return {
448
+ reason: 'no_resource_ids',
449
+ slots: []
450
+ };
350
451
  }
351
452
  // 1. Service for duration + buffer times (from the primary service)
352
453
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -354,8 +455,21 @@ export async function getAvailableSlots(params) {
354
455
  id: serviceId,
355
456
  collection: serviceSlug,
356
457
  depth: 0,
458
+ // Skip the resources join — internal logic never reads it, and without this
459
+ // every service read becomes an aggregation with a $lookup.
460
+ joins: false,
357
461
  req
358
462
  });
463
+ if (enforceActive !== false && service?.active === false) {
464
+ trace.dbg('empty', {
465
+ reason: 'service_inactive',
466
+ serviceId
467
+ });
468
+ return {
469
+ reason: 'service_inactive',
470
+ slots: []
471
+ };
472
+ }
359
473
  const duration = service.duration ?? 60;
360
474
  const bufferBefore = service.bufferTimeBefore ?? 0;
361
475
  const bufferAfter = service.bufferTimeAfter ?? 0;
@@ -371,6 +485,31 @@ export async function getAvailableSlots(params) {
371
485
  // schedules is capacity-only and contributes no time windows.
372
486
  const scheduleBearingWindowLists = [];
373
487
  for (const rid of ids){
488
+ // This read exists solely to feed the active check below — skip it
489
+ // entirely when enforceActive is off so an opted-out consumer doesn't pay
490
+ // for a findByID whose result is never read.
491
+ if (enforceActive !== false) {
492
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
493
+ const resourceDoc = await payload.findByID({
494
+ id: rid,
495
+ collection: resourceSlug,
496
+ depth: 0,
497
+ // Skip joins — internal logic never reads them, and without this every
498
+ // resource read becomes an aggregation with a $lookup.
499
+ joins: false,
500
+ req
501
+ }).catch(()=>null);
502
+ if (resourceDoc?.active === false) {
503
+ trace.dbg('empty', {
504
+ reason: 'resource_inactive',
505
+ resourceId: rid
506
+ });
507
+ return {
508
+ reason: 'resource_inactive',
509
+ slots: []
510
+ };
511
+ }
512
+ }
374
513
  const scheduleWhere = {
375
514
  and: [
376
515
  {
@@ -452,7 +591,10 @@ export async function getAvailableSlots(params) {
452
591
  trace.dbg('empty', {
453
592
  reason: 'no_windows'
454
593
  });
455
- return [];
594
+ return {
595
+ reason: 'no_windows',
596
+ slots: []
597
+ };
456
598
  }
457
599
  // 3. Intersect all schedule-bearing window lists
458
600
  let timeRanges = scheduleBearingWindowLists[0];
@@ -467,7 +609,10 @@ export async function getAvailableSlots(params) {
467
609
  }))),
468
610
  reason: 'empty_intersection'
469
611
  });
470
- return [];
612
+ return {
613
+ reason: 'empty_intersection',
614
+ slots: []
615
+ };
471
616
  }
472
617
  // 4. Candidate slot sizing
473
618
  // NOTE: epoch-trick sizing is only meaningful for fixed/flexible durations.
@@ -497,6 +642,7 @@ export async function getAvailableSlots(params) {
497
642
  endTime: end,
498
643
  getExternalBusy,
499
644
  guestCount: guestCount ?? 1,
645
+ holdsSlug,
500
646
  payload,
501
647
  req,
502
648
  reservationSlug,
@@ -530,7 +676,12 @@ export async function getAvailableSlots(params) {
530
676
  durationType: 'full-day',
531
677
  returnedCount: availableSlots.length
532
678
  });
533
- return availableSlots;
679
+ return {
680
+ ...availableSlots.length === 0 ? {
681
+ reason: 'all_slots_taken'
682
+ } : {},
683
+ slots: availableSlots
684
+ };
534
685
  }
535
686
  const stepSize = Math.min(effectiveDuration, 15);
536
687
  let candidatesGenerated = 0;
@@ -556,7 +707,12 @@ export async function getAvailableSlots(params) {
556
707
  candidatesGenerated,
557
708
  returnedCount: availableSlots.length
558
709
  });
559
- return availableSlots;
710
+ return {
711
+ ...availableSlots.length === 0 ? {
712
+ reason: candidatesGenerated === 0 ? 'window_too_short' : 'all_slots_taken'
713
+ } : {},
714
+ slots: availableSlots
715
+ };
560
716
  }
561
717
 
562
718
  //# sourceMappingURL=AvailabilityService.js.map
@@ -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 { 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":"AAKA,SAASA,kBAAkB,QAA2B,+BAA8B;AACpF,SAASC,uBAAuB,QAAQ,0CAAyC;AACjF,SAASC,eAAe,EAAEC,sBAAsB,QAAQ,gCAA+B;AACvF,SACEC,UAAU,EACVC,oBAAoB,EACpBC,eAAe,EACfC,kBAAkB,QACb,4BAA2B;AAClC,SAASC,kBAAkB,QAAQ,gCAA+B;AAQlE;2EAC2E,GAC3E,MAAMC,mBAAmB,KAAK,KAAK,KAAK;AAExC,iCAAiC;AAEjC,OAAO,SAASC,eAAeC,MAM9B;IACC,MAAM,EAAEC,YAAY,EAAEC,eAAe,EAAEC,SAAS,EAAE,GAAGH;IAErD,IAAIC,iBAAiB,YAAY;QAC/B,MAAMG,MAAMP,mBAAmBM,WAAWH,OAAOK,QAAQ,IAAI;QAC7D,MAAMC,kBAAkBC,KAAKC,KAAK,CAAC,AAACJ,CAAAA,IAAIK,OAAO,KAAKN,UAAUM,OAAO,EAAC,IAAK;QAC3E,OAAO;YAAEH;YAAiBI,SAASN;QAAI;IACzC;IAEA,IAAIH,iBAAiB,cAAcD,OAAOU,OAAO,EAAE;QACjD,MAAMJ,kBAAkBC,KAAKC,KAAK,CAChC,AAACR,CAAAA,OAAOU,OAAO,CAACD,OAAO,KAAKN,UAAUM,OAAO,EAAC,IAAK;QAErD,OAAO;YAAEH;YAAiBI,SAASV,OAAOU,OAAO;QAAC;IACpD;IAEA,2BAA2B;IAC3B,MAAMA,UAAUjB,WAAWU,WAAWD;IACtC,OAAO;QAAEI,iBAAiBJ;QAAiBQ;IAAQ;AACrD;AAEA,OAAO,SAASC,kBAAkBX,MAMjC;IACC,MAAM,EAAEY,gBAAgB,EAAEC,YAAY,EAAEC,cAAc,EAAEC,oBAAoB,EAAEC,UAAU,EAAE,GACxFhB;IAEF,MAAMiB,aAAsB;QAC1B;YAAEC,QAAQ;gBAAEC,IAAIP;YAAiB;QAAE;QACnC;YAAET,WAAW;gBAAEiB,WAAWP,aAAaQ,WAAW;YAAG;QAAE;QACvD;YAAEX,SAAS;gBAAEY,cAAcR,eAAeO,WAAW;YAAG;QAAE;QAC1D;YACEE,IAAI;gBACF;oBAAEC,UAAU;wBAAEC,QAAQT;oBAAW;gBAAE;gBACnC;oBAAE,kBAAkB;wBAAES,QAAQT;oBAAW;gBAAE;aAC5C;QACH;KACD;IAED,IAAID,sBAAsB;QACxBE,WAAWS,IAAI,CAAC;YAAEC,IAAI;gBAAEC,YAAYb;YAAqB;QAAE;IAC7D;IAEA,OAAO;QAAEc,KAAKZ;IAAW;AAC3B;AAEA;;;;;;CAMC,GACD,OAAO,SAASa,wBAAwB9B,MAMvC;IACC,MAAM,EAAEY,gBAAgB,EAAEmB,YAAY,EAAEC,cAAc,EAAEjB,oBAAoB,EAAEC,UAAU,EAAE,GACxFhB;IACF,MAAMiC,cAAc,IAAIC,KAAKF,eAAevB,OAAO,KAAKX;IACxD,MAAMqC,YAAY,IAAID,KAAKH,aAAatB,OAAO,KAAKX;IAEpD,MAAMmB,aAAsB;QAC1B;YAAEC,QAAQ;gBAAEC,IAAIP;YAAiB;QAAE;QACnC;YAAET,WAAW;gBAAEiB,WAAWe,UAAUd,WAAW;YAAG;QAAE;QACpD;YAAEX,SAAS;gBAAEY,cAAcW,YAAYZ,WAAW;YAAG;QAAE;QACvD;YACEE,IAAI;gBAAC;oBAAEC,UAAU;wBAAEC,QAAQT;oBAAW;gBAAE;gBAAG;oBAAE,kBAAkB;wBAAES,QAAQT;oBAAW;gBAAE;aAAE;QAC1F;KACD;IAED,IAAID,yBAAyBqB,WAAW;QACtCnB,WAAWS,IAAI,CAAC;YAAEC,IAAI;gBAAEC,YAAYb;YAAqB;QAAE;IAC7D;IAEA,OAAO;QAAEc,KAAKZ;IAAW;AAC3B;AAEA;;;;;;CAMC,GACD,OAAO,eAAeoB,mBAAmBrC,MAKxC;IACC,MAAM,EAAEsC,SAAS,EAAEC,YAAY,EAAEC,KAAK,EAAExB,UAAU,EAAE,GAAGhB;IACvD,MAAMyC,cAA2B,EAAE;IAEnC,KAAK,MAAMC,QAAQF,MAAO;QACxB,IAAIG,OAAOD,KAAKlB,QAAQ,MAAMmB,OAAO3B,eAAe,CAAC0B,KAAKhC,OAAO,EAAE;YACjE;QACF;QACA,MAAM,EAAEkC,KAAK,EAAEC,MAAM,EAAE,GAAG,MAAMP,UAAUI,KAAKI,OAAO;QACtD,MAAM,EAAEjC,YAAY,EAAEC,cAAc,EAAE,GAAGpB,qBACvC,IAAIwC,KAAKQ,KAAKvC,SAAS,GACvB,IAAI+B,KAAKQ,KAAKhC,OAAO,GACrBmC,QACAD;QAEFH,YAAYf,IAAI,CAAC;YACfqB,YAAYlC;YACZmC,cAAclC;YACdmC,OAAOV,iBAAiB,cAAcG,KAAKQ,UAAU,GAAG;QAC1D;IACF;IAEA,OAAOT;AACT;AAEA,oEAAoE,GACpE,OAAO,eAAeU,uBAAuBnD,MAK5C;IACC,MAAM,EAAEsC,SAAS,EAAEC,YAAY,EAAEa,WAAW,EAAEpC,UAAU,EAAE,GAAGhB;IAC7D,OAAOqC,mBAAmB;QACxBC;QACAC;QACAC,OAAOlD,wBAAwB8D;QAC/BpC;IACF;AACF;AAEA,OAAO,SAASqC,iBACdnC,MAAc,EACdoC,aAAkC;IAElC,OAAOA,cAAc1C,gBAAgB,CAAC2C,QAAQ,CAACrC;AACjD;AAEA,OAAO,SAASsC,mBACdC,UAAkB,EAClBC,QAAgB,EAChBJ,aAAkC;IAElC,MAAMK,UAAUL,cAAcM,WAAW,CAACH,WAAW;IACrD,IAAI,CAACE,SAAS;QACZ,OAAO;YAAEE,QAAQ,CAAC,gBAAgB,EAAEJ,YAAY;YAAEK,OAAO;QAAM;IACjE;IACA,IAAI,CAACH,QAAQJ,QAAQ,CAACG,WAAW;QAC/B,OAAO;YACLG,QAAQ,CAAC,wBAAwB,EAAEJ,WAAW,MAAM,EAAEC,SAAS,CAAC,CAAC;YACjEI,OAAO;QACT;IACF;IACA,OAAO;QAAEA,OAAO;IAAK;AACvB;AAEA,oDAAoD;AAEpD,OAAO,eAAeC,kBAAkB/D,MAoBvC;IAMC,MAAM,EACJY,gBAAgB,EAChBoD,WAAW,EACXC,YAAY,EACZC,KAAK,EACLxD,OAAO,EACPK,oBAAoB,EACpBoD,eAAe,EACfjB,UAAU,EACVkB,OAAO,EACPC,GAAG,EACHC,eAAe,EACftD,UAAU,EACVuD,YAAY,EACZC,YAAY,EACZC,YAAY,EACZtE,SAAS,EACV,GAAGH;IAEJ,MAAM0E,QAAQR,SAAS7E;IAEvB,gDAAgD;IAChD,8DAA8D;IAC9D,MAAMmC,WAAW,MAAM,AAAC4C,QAAQO,QAAQ,CAAS;QAC/ChD,IAAIX;QACJ4D,YAAYL;QACZM,OAAO;QACPR;IACF;IACA,MAAMS,WAAW,AAACtD,SAASsD,QAAQ,IAAe;IAClD,MAAMvC,eAAgB,AAACf,SAASe,YAAY,IAAe;IAE3D,+CAA+C;IAC/C,MAAM,EAAE1B,cAAckB,YAAY,EAAEjB,gBAAgBkB,cAAc,EAAE,GAAGtC,qBACrES,WACAO,SACAuD,cACAD;IAGF,+EAA+E;IAC/E,MAAMe,cAAcjD,wBAAwB;QAC1ClB;QACAmB;QACAC;QACAjB;QACAC;IACF;IACA,8DAA8D;IAC9D,MAAM,EAAEgE,IAAI,EAAE,GAAG,MAAM,AAACZ,QAAQa,IAAI,CAAS;QAC3CL,YAAYN;QACZO,OAAO;QACPK,OAAO;QACPb;QACAc,OAAOJ;IACT;IAEAL,MAAMU,GAAG,CAAC,SAAS;QACjBC,sBAAsB,AAACL,KAAmBM,MAAM;QAChDvD,cAAcA,aAAaV,WAAW;QACtCW,gBAAgBA,eAAeX,WAAW;QAC1CkB;QACAwC;QACAD;QACA9D;IACF;IAEA,qEAAqE;IACrE,MAAMuE,cAAc,IAAIC;IACxB,MAAMlD,YAAY,OAChBmD;QAEA,MAAMC,MAAMD,cAAcrD,YAAY,KAAKO,OAAO8C;QAClD,MAAME,SAASJ,YAAYK,GAAG,CAACF;QAC/B,IAAIC,QAAQ;YACV,OAAOA;QACT;QACA,IAAIE,SAAS;YAAEjD,OAAO;YAAGC,QAAQ;QAAE;QACnC,IAAI4C,cAAcrD,WAAW;YAC3B,IAAI;gBACF,8DAA8D;gBAC9D,MAAMU,UAAU,MAAM,AAACsB,QAAQO,QAAQ,CAAS;oBAC9ChD,IAAI8D;oBACJb,YAAYJ;oBACZK,OAAO;oBACPR;gBACF;gBACA,IAAIvB,SAAS;oBACX+C,SAAS;wBACPjD,OAAO,AAACE,QAAQgD,eAAe,IAAe;wBAC9CjD,QAAQ,AAACC,QAAQiD,gBAAgB,IAAe;oBAClD;gBACF;YACF,EAAE,OAAOC,KAAK;gBACZtB,MAAMU,GAAG,CAAC,SAAS;oBAAEY;oBAAKP;oBAAWN,OAAO;gBAAY;YAC1D;QACF;QACAI,YAAYU,GAAG,CAACP,KAAKG;QACrB,OAAOA;IACT;IAEA,MAAMK,qBAAqB,AACzB,CAAA,MAAMC,QAAQC,GAAG,CACf,AAACpB,KAAwCqB,GAAG,CAAC,CAACC,MAC5CnD,uBAAuB;YAAEb;YAAWC;YAAca,aAAakD;YAAKtF;QAAW,IAEnF,EACAuF,IAAI;IAEN,2EAA2E;IAC3E,yCAAyC;IACzC,MAAMC,qBAAqB/B,eACvB,MAAMpC,mBAAmB;QAAEC;QAAWC;QAAcC,OAAOiC;QAAczD;IAAW,KACpF,EAAE;IAEN,8EAA8E;IAC9E,0EAA0E;IAC1E,qEAAqE;IACrE,IAAIyF,sBAAmC,EAAE;IACzC,IAAItC,iBAAiB;QACnB,IAAI;YACF,MAAMuC,YAAY,MAAMvC,gBAAgB;gBACtC/D,KAAK2B;gBACLsC;gBACArD;gBACA2F,OAAO3E;YACT;YACAyE,sBAAsBC,UACnBL,GAAG,CAAC,CAACO,KAAQ,CAAA;oBACZ7D,YAAY,IAAIb,KAAK0E,GAAGxG,GAAG;oBAC3B4C,cAAc,IAAId,KAAK0E,GAAGD,KAAK;oBAC/B1D,OAAO6B;gBACT,CAAA,GACC+B,MAAM,CAAC,CAACC,IAAM,CAACC,MAAMD,EAAE9D,YAAY,CAACvC,OAAO,OAAO,CAACsG,MAAMD,EAAE/D,UAAU,CAACtC,OAAO;QAClF,EAAE,OAAOuF,KAAK;YACZtB,MAAMU,GAAG,CAAC,SAAS;gBAAEY;gBAAKhF;gBAAYmE,OAAO;YAAkB;YAC/DsB,sBAAsB,EAAE;QAC1B;IACF;IAEA,MAAMhE,cAAc;WAAIyD;WAAuBM;WAAuBC;KAAoB;IAE1F,gFAAgF;IAChF,MAAMO,eAAevE,YAAYwE,MAAM,CACrC,CAACC,KAAKC,MACJxH,gBAAgBqC,gBAAgBD,cAAcoF,IAAInE,YAAY,EAAEmE,IAAIpE,UAAU,IAC1EmE,MAAMC,IAAIlE,KAAK,GACfiE,KACN;IAGF,MAAME,iBAAiB7E,iBAAiB,cAAcW,aAAa;IACnE,MAAMmE,YAAYL,eAAeI,kBAAkBtC;IAEnDJ,MAAMU,GAAG,CAAC,gBAAgB;QACxBiC;QACAD;QACAJ;QACAM,kBAAkB;YAChBC,UAAUd,oBAAoBnB,MAAM;YACpCkC,SAAStB,mBAAmBZ,MAAM;YAClCmC,SAASjB,mBAAmBlB,MAAM;QACpC;QACAR;QACA9D;IACF;IAEA,OAAO;QACLqG;QACAK,cAAcV;QACdnD,QAAQwD,YACJjF,YACAG,iBAAiB,cACf,4BACA;QACNoF,eAAe7C;IACjB;AACF;AAEA,OAAO,eAAe8C,kBAAkB5H,MAkBvC;IACC,MAAM,EACJY,gBAAgB,EAChBiH,IAAI,EACJ3D,KAAK,EACLC,eAAe,EACfjB,UAAU,EACVkB,OAAO,EACPC,GAAG,EACHC,eAAe,EACftD,UAAU,EACV8G,WAAW,EACXvD,YAAY,EACZwD,YAAY,EACZtC,SAAS,EACTuC,WAAW,EACX3H,QAAQ,EACT,GAAGL;IAEJ,MAAMiI,KAAK5H,YAAY;IAEvB,iFAAiF;IACjF,MAAM6H,MACJJ,eAAeA,YAAYxC,MAAM,GAAG,IAChCwC,cACA9G,eAAeoB,YACb;QAACpB;KAAW,GACZ,EAAE;IAEV,MAAM0D,QAAQ,AAACR,CAAAA,SAAS7E,kBAAiB,EAAG8I,KAAK,CAAC;QAAEL,aAAaI;QAAKzC;IAAU;IAChFf,MAAMU,GAAG,CAAC,SAAS;QACjByC,MAAMA,gBAAgB3F,OAAO2F,KAAKxG,WAAW,KAAKwG;QAClD3E,YAAYA,cAAc;QAC1B7C,UAAU4H;IACZ;IAEA,IAAIC,IAAI5C,MAAM,KAAK,GAAG;QACpBZ,MAAMU,GAAG,CAAC,SAAS;YAAEvB,QAAQ;QAAkB;QAC/C,OAAO,EAAE;IACX;IAEA,oEAAoE;IACpE,8DAA8D;IAC9D,MAAMf,UAAU,MAAM,AAACsB,QAAQO,QAAQ,CAAS;QAC9ChD,IAAI8D;QACJb,YAAYoD;QACZnD,OAAO;QACPR;IACF;IACA,MAAM+D,WAAW,AAACtF,QAAQsF,QAAQ,IAAe;IACjD,MAAMnE,eAAe,AAACnB,QAAQiD,gBAAgB,IAAe;IAC7D,MAAM/B,cAAc,AAAClB,QAAQgD,eAAe,IAAe;IAC3D,MAAM7F,eAAgB,AAAC6C,QAAQ7C,YAAY,IAAe;IAC1DyE,MAAMU,GAAG,CAAC,WAAW;QAAEpB;QAAaC;QAAcmE;QAAUnI;IAAa;IAEzE,+EAA+E;IAC/E,8EAA8E;IAC9E,iEAAiE;IACjE,MAAMoI,6BAAuE,EAAE;IAC/E,KAAK,MAAMC,OAAOJ,IAAK;QACrB,MAAMK,gBAAgB;YAAE1G,KAAK;gBAAC;oBAAEL,UAAU;wBAAEC,QAAQ6G;oBAAI;gBAAE;gBAAG;oBAAEE,QAAQ;wBAAE/G,QAAQ;oBAAK;gBAAE;aAAE;QAAC;QAC3F,8DAA8D;QAC9D,MAAM,EAAEuD,MAAMyD,SAAS,EAAE,GAAG,MAAM,AAACrE,QAAQa,IAAI,CAAS;YACtDL,YAAYmD;YACZlD,OAAO;YACPK,OAAO;YACPb;YACAc,OAAOoD;QACT;QACA7D,MAAMU,GAAG,CAAC,aAAa;YACrBsD,aAAa,AAACD,UAA6CpC,GAAG,CAAC,CAACsC,IAAMA,EAAEH,MAAM;YAC9ExH,YAAYsH;YACZM,WAAW,AAACH,UAAwBnD,MAAM;YAC1CiD;QACF;QACA,IAAI,CAACE,aAAaA,UAAUnD,MAAM,KAAK,GAAG;YACxCZ,MAAMU,GAAG,CAAC,QAAQ;gBAAEvB,QAAQ;gBAAuB7C,YAAYsH;gBAAKM,WAAW;YAAE;YACjF;QACF;QACA,uEAAuE;QACvE,0EAA0E;QAC1E,IAAIC;QACJ,KAAK,MAAMF,KAAKF,UAA6C;YAC3D,MAAMK,MAAM,AAACH,EAAEI,UAAU,IAA8D,EAAE;YACzF,IAAIxJ,gBAAgBsI,MAAMiB,KAAKb,KAAK;gBAClCY,UAAU;oBAAEG,WAAWF,IAAI7D,IAAI,CAAC,CAACgE,IAAM1J,gBAAgBsI,MAAM;4BAACoB;yBAAE,EAAEhB;oBAAMiB,YAAYP,EAAEhH,EAAE;gBAAC;gBACzF;YACF;QACF;QACA,IAAIkH,SAAS;YACXnE,MAAMU,GAAG,CAAC,QAAQ;gBAChB+D,kBAAkBN,QAAQG,SAAS;gBACnCnF,QAAQ;gBACR7C,YAAYsH;gBACZY,YAAYL,QAAQK,UAAU;YAChC;YACAb,2BAA2B3G,IAAI,CAAC,EAAE;YAClC;QACF;QACA,MAAM0H,UAA6C,EAAE;QACrD,KAAK,MAAMC,YAAYZ,UAAW;YAChCW,QAAQ1H,IAAI,IACPlC,uBACD6J,UACAxB,MACAI;QAGN;QACAvD,MAAMU,GAAG,CAAC,WAAW;YACnBpE,YAAYsH;YACZgB,aAAaF,QAAQ9D,MAAM;YAC3B8D,SAASA,QAAQ/C,GAAG,CAAC,CAACkD,IAAO,CAAA;oBAAEnJ,KAAKmJ,EAAEnJ,GAAG,CAACiB,WAAW;oBAAIsF,OAAO4C,EAAE5C,KAAK,CAACtF,WAAW;gBAAG,CAAA;YACtFmI,cAAcJ,QAAQ9D,MAAM,KAAK;QACnC;QACA+C,2BAA2B3G,IAAI,CAAC0H;IAClC;IAEA,8DAA8D;IAC9D,IAAIf,2BAA2B/C,MAAM,KAAK,GAAG;QAC3CZ,MAAMU,GAAG,CAAC,SAAS;YAAEvB,QAAQ;QAAa;QAC1C,OAAO,EAAE;IACX;IAEA,iDAAiD;IACjD,IAAI4F,aAAapB,0BAA0B,CAAC,EAAE;IAC9C,IAAK,IAAIqB,IAAI,GAAGA,IAAIrB,2BAA2B/C,MAAM,EAAEoE,IAAK;QAC1DD,aAAa7J,mBAAmB6J,YAAYpB,0BAA0B,CAACqB,EAAE;IAC3E;IACA,IAAID,WAAWnE,MAAM,KAAK,GAAG;QAC3BZ,MAAMU,GAAG,CAAC,SAAS;YACjBuE,oBAAoBtB,2BAA2BhC,GAAG,CAAC,CAACuD,OAClDA,KAAKvD,GAAG,CAAC,CAACkD,IAAO,CAAA;wBAAEnJ,KAAKmJ,EAAEnJ,GAAG,CAACiB,WAAW;wBAAIsF,OAAO4C,EAAE5C,KAAK,CAACtF,WAAW;oBAAG,CAAA;YAE5EwC,QAAQ;QACV;QACA,OAAO,EAAE;IACX;IAEA,2BAA2B;IAC3B,4EAA4E;IAC5E,8EAA8E;IAC9E,uEAAuE;IACvE,MAAM,EAAEnD,SAASmJ,aAAa,EAAE,GAAG9J,eAAe;QAChDE;QACAC,iBAAiBkI;QACjBjI,WAAW,IAAI+B,KAAK;QACpB7B,UAAU4H;IACZ;IACA,MAAM6B,eAAevJ,KAAKC,KAAK,CAACqJ,cAAcpJ,OAAO,KAAK;IAC1D,MAAMsJ,oBAAoB9J,iBAAiB,UAAUmI,WAAW0B;IAChEpF,MAAMU,GAAG,CAAC,UAAU;QAClB2E;QACAD;QACAE,UAAUzJ,KAAK0J,GAAG,CAACF,mBAAmB;IACxC;IAEA,wEAAwE;IACxE,MAAMG,eAAe,OACnBvD,OACAvG,KACA+J,SACAC;QAEA,KAAK,MAAM9B,OAAOJ,IAAK;YACrB,MAAMrC,SAAS,MAAM9B,kBAAkB;gBACrCnD;gBACAoD,aAAaoG;gBACbnG,cAAckG;gBACdjG,OAAOQ;gBACPhE,SAASN;gBACT+D;gBACAjB,YAAYA,cAAc;gBAC1BkB;gBACAC;gBACAC;gBACAtD,YAAYsH;gBACZ/D;gBACAC,cAAcwD;gBACd7H,WAAWwG;YACb;YACA,IAAI,CAACd,OAAOwB,SAAS,EAAE;gBACrB,OAAO;YACT;QACF;QACA,OAAO;IACT;IAEA,MAAMgD,iBAAoD,EAAE;IAE5D,wEAAwE;IACxE,IAAIpK,iBAAiB,YAAY;QAC/B,IAAIqK,sBAAsB;QAC1B,KAAK,MAAMC,SAASd,WAAY;YAC9Ba;YACA,IAAI,MAAMJ,aAAaK,MAAM5D,KAAK,EAAE4D,MAAMnK,GAAG,EAAE,GAAG,IAAI;gBACpDiK,eAAe3I,IAAI,CAAC;oBAAEtB,KAAKmK,MAAMnK,GAAG;oBAAEuG,OAAO4D,MAAM5D,KAAK;gBAAC;YAC3D;QACF;QACAjC,MAAMU,GAAG,CAAC,UAAU;YAClBoF,qBAAqBH,eAAe/E,MAAM;YAC1CgF;YACArK,cAAc;YACdwK,eAAeJ,eAAe/E,MAAM;QACtC;QACA,OAAO+E;IACT;IAEA,MAAML,WAAWzJ,KAAK0J,GAAG,CAACF,mBAAmB;IAC7C,IAAIO,sBAAsB;IAE1B,KAAK,MAAMC,SAASd,WAAY;QAC9B,IAAIzH,iBAAiB,IAAIE,KAAKqI,MAAM5D,KAAK;QAEzC,MAAO,KAAM;YACX,MAAM5E,eAAetC,WAAWuC,gBAAgB+H;YAChD,IAAIhI,eAAewI,MAAMnK,GAAG,EAAE;gBAC5B;YACF;YAEAkK;YACA,IAAI,MAAMJ,aAAalI,gBAAgBD,cAAckC,cAAcD,cAAc;gBAC/EqG,eAAe3I,IAAI,CAAC;oBAAEtB,KAAK2B;oBAAc4E,OAAO,IAAIzE,KAAKF;gBAAgB;YAC3E;YAEAA,iBAAiBvC,WAAWuC,gBAAgBgI;QAC9C;IACF;IAEAtF,MAAMU,GAAG,CAAC,UAAU;QAClBoF,qBAAqBH,eAAe/E,MAAM;QAC1CgF;QACAG,eAAeJ,eAAe/E,MAAM;IACtC;IACA,OAAO+E;AACT"}
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/**\n * Occupancy a single fetched reservation imposes on `resourceId`.\n *\n * Resolves `reservation` LENIENTLY: this is a stored document, which — unlike\n * a write in progress — can carry an inverted top-level window from a\n * `context.skipReservationHooks` write or data predating that check. Lenient\n * mode skips parent synthesis instead of throwing, so a malformed top-level\n * window never discards the real `items[]` occupancies also present on the\n * same row (see the lenient-mode note in resolveReservationItems.ts).\n */\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, { lenient: true }),\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 /** Token of the hold being converted into this booking — never self-blocks. */\n excludeHoldToken?: string\n excludeReservationId?: number | string\n /** External busy resolver (calendar sync etc.) — intervals block the whole resource. */\n getExternalBusy?: GetExternalBusy\n guestCount: number\n /** Slug of the slot-holds collection; omit to ignore holds entirely. */\n holdsSlug?: string\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 excludeHoldToken,\n excludeReservationId,\n getExternalBusy,\n guestCount,\n holdsSlug,\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 // Skip the resources join — internal logic never reads it, and without this\n // every service read becomes an aggregation with a $lookup.\n joins: false,\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 // reservationOccupancies resolves each doc LENIENTLY (see its own doc\n // comment) — an inverted top-level window on a stored row skips parent\n // synthesis rather than throwing, so this never needs a try/catch: every\n // real items[] occupancy on the row still comes back.\n //\n // SEQUENTIAL, not Promise.all, and that is load-bearing. Each iteration can\n // reach `bufferFor` -> `payload.findByID` on the shared `req`, and this whole\n // function runs inside the booking's own transaction on the write path. A\n // MongoDB ClientSession cannot carry concurrent operations inside a\n // transaction: when two do collide, the loser's `count`/`findByID` calls\n // `killTransaction` from its own catch, which rolls back and clears the\n // transaction the enclosing create/delete owns, and the survivor then reports\n // `NoSuchTransaction` (\"transaction number N does not match any in-progress\n // transactions\"). Buffers are per-service and cached, so the loop is at most\n // one read per DISTINCT neighbouring service either way.\n const fetchedOccupancies: Occupancy[] = []\n for (const doc of docs as Array<Record<string, unknown>>) {\n fetchedOccupancies.push(\n ...(await reservationOccupancies({ bufferFor, capacityMode, reservation: doc, resourceId })),\n )\n }\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 // Unexpired slot holds occupy the resource exactly as a blocking reservation\n // does. Expiry is a read-time predicate rather than a TTL index so the\n // behaviour is identical on Mongo, Postgres and SQLite. `excludeHoldToken`\n // is the bearer converting their OWN hold into a booking — without it the\n // hold would block the very booking it was taken to protect.\n const holdOccupancies: Occupancy[] = []\n if (holdsSlug) {\n const holdWhere: Where = {\n and: [\n { resource: { equals: resourceId } },\n { expiresAt: { greater_than: new Date().toISOString() } },\n { startTime: { less_than: candidateEnd.toISOString() } },\n { endTime: { greater_than: candidateStart.toISOString() } },\n ...(excludeHoldToken ? [{ token: { not_equals: excludeHoldToken } }] : []),\n ],\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: holdDocs } = await (payload.find as any)({\n collection: holdsSlug,\n depth: 0,\n limit: 0,\n req,\n where: holdWhere,\n })\n\n // Sequential for the same reason the reservation loop above is: `bufferFor`\n // reads through the shared `req`, and concurrent operations on one MongoDB\n // session inside a transaction abort it.\n for (const hold of holdDocs as Array<Record<string, unknown>>) {\n // Expand by the held service's own buffers, so a hold reserves the same\n // real-world window the resulting booking will.\n const { after, before } = await bufferFor(hold.service as number | string | undefined)\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n new Date(hold.startTime as string),\n new Date(hold.endTime as string),\n before,\n after,\n )\n holdOccupancies.push({\n blockedEnd: effectiveEnd,\n blockedStart: effectiveStart,\n units: capacityMode === 'per-guest' ? ((hold.guestCount as number) ?? 1) : 1,\n })\n }\n\n trace.dbg('check_holds', { activeHolds: holdOccupancies.length, resourceId })\n }\n\n const occupancies = [\n ...fetchedOccupancies,\n ...siblingOccupancies,\n ...externalOccupancies,\n ...holdOccupancies,\n ]\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\n/**\n * Why availability came back empty. Returned alongside the slots so an empty\n * result is diagnosable without turning on `debug`.\n *\n * `no_active_schedules` and `date_excepted` are deliberately absent: they are\n * per-resource `skip` traces inside a loop, not return points — they funnel\n * into `no_windows` — and stay debug-only.\n *\n * `window_too_short` is reachable only from the general (non-full-day) branch:\n * the full-day branch returns `no_windows` before it, so it always has at least\n * one candidate.\n */\nexport type EmptyReason =\n | 'all_slots_taken'\n | 'empty_intersection'\n | 'no_resource_ids'\n | 'no_windows'\n | 'resource_inactive'\n | 'service_inactive'\n | 'window_too_short'\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 /** Skip the service/resource `active` short-circuits when explicitly `false`. */\n enforceActive?: boolean\n /** External busy resolver (calendar sync etc.) — intervals block the whole resource. */\n getExternalBusy?: GetExternalBusy\n guestCount?: number\n /**\n * Slug of the slot-holds collection; omit to ignore holds entirely.\n *\n * Load-bearing, not optional polish: without it every read-path caller\n * (`/reserve/availability`, `/reserve/slots`, the reservation form's slot\n * picker) advertises a held slot as FREE, and the customer who clicks it is\n * rejected by the write path — which DOES count holds — as a 409. Pass it\n * whenever `slotHolds.enabled`.\n */\n holdsSlug?: string\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<{ reason?: EmptyReason; slots: Array<{ end: Date; start: Date }> }> {\n const {\n blockingStatuses,\n date,\n debug,\n enforceActive,\n getExternalBusy,\n guestCount,\n holdsSlug,\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 { reason: 'no_resource_ids', slots: [] }\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 // Skip the resources join — internal logic never reads it, and without this\n // every service read becomes an aggregation with a $lookup.\n joins: false,\n req,\n })\n\n if (enforceActive !== false && service?.active === false) {\n trace.dbg('empty', { reason: 'service_inactive', serviceId })\n return { reason: 'service_inactive', slots: [] }\n }\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 // This read exists solely to feed the active check below — skip it\n // entirely when enforceActive is off so an opted-out consumer doesn't pay\n // for a findByID whose result is never read.\n if (enforceActive !== false) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resourceDoc = await (payload.findByID as any)({\n id: rid,\n collection: resourceSlug,\n depth: 0,\n // Skip joins — internal logic never reads them, and without this every\n // resource read becomes an aggregation with a $lookup.\n joins: false,\n req,\n }).catch(() => null)\n if (resourceDoc?.active === false) {\n trace.dbg('empty', { reason: 'resource_inactive', resourceId: rid })\n return { reason: 'resource_inactive', slots: [] }\n }\n }\n\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 { reason: 'no_windows', slots: [] }\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 { reason: 'empty_intersection', slots: [] }\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 holdsSlug,\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 {\n ...(availableSlots.length === 0 ? { reason: 'all_slots_taken' as const } : {}),\n slots: availableSlots,\n }\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 {\n ...(availableSlots.length === 0\n ? {\n reason: (candidatesGenerated === 0\n ? 'window_too_short'\n : 'all_slots_taken') as EmptyReason,\n }\n : {}),\n slots: availableSlots,\n }\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","lenient","isBlockingStatus","statusMachine","includes","validateTransition","fromStatus","toStatus","allowed","transitions","reason","valid","checkAvailability","bufferAfter","bufferBefore","debug","excludeHoldToken","getExternalBusy","holdsSlug","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","joins","bufferTimeAfter","bufferTimeBefore","err","set","fetchedOccupancies","doc","siblingOccupancies","externalOccupancies","intervals","start","map","iv","filter","o","isNaN","holdOccupancies","holdWhere","expiresAt","token","holdDocs","hold","activeHolds","currentUnits","reduce","sum","occ","candidateUnits","available","occupancySources","external","fetched","sibling","currentCount","totalCapacity","getAvailableSlots","date","enforceActive","resourceIds","scheduleSlug","serviceSlug","tz","ids","child","slots","active","duration","scheduleBearingWindowLists","rid","resourceDoc","catch","scheduleWhere","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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"}
@@ -0,0 +1,53 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { ResolvedReservationPluginConfig } from '../types.js';
3
+ /**
4
+ * Why a hold was refused — a CLOSED set, deliberately.
5
+ *
6
+ * `/reserve/hold` is reachable without authentication, so the refusal reason is
7
+ * attacker-visible output. An earlier version echoed `error.message` from any
8
+ * failed write, which leaked internal detail (DB errors, `Forbidden`, genuine
9
+ * bugs) and reported all of it as `409 slot_taken` — a status that tells a
10
+ * well-behaved client the slot is gone and it should stop, for conditions that
11
+ * are nothing of the sort. Everything not enumerated here now propagates as a
12
+ * thrown error, so it surfaces as a 500 the operator can see rather than a
13
+ * plausible-looking 409 the caller cannot act on.
14
+ */
15
+ export type HoldRefusalReason = 'resource_not_found' | 'service_inactive' | 'service_not_found' | 'slot_taken';
16
+ export type TakeHoldResult = {
17
+ hold: {
18
+ expiresAt: string;
19
+ id: number | string;
20
+ token: string;
21
+ };
22
+ ok: true;
23
+ } | {
24
+ ok: false;
25
+ reason: HoldRefusalReason;
26
+ };
27
+ /**
28
+ * Claim a slot for `config.slotHolds.ttlMinutes` while the caller completes an
29
+ * external step (typically payment).
30
+ *
31
+ * This function only assembles the row. The parts that make a hold trustworthy
32
+ * — claiming the resource lock and running the same `checkAvailability` a
33
+ * booking runs — live in the `validateHoldSlot` beforeChange hook, because they
34
+ * must share the transaction Payload opens around `create`. Doing them here
35
+ * instead granted 3 of 8 simultaneous holds for one slot.
36
+ */
37
+ export declare function takeHold(params: {
38
+ config: ResolvedReservationPluginConfig;
39
+ endTime?: Date;
40
+ guestCount?: number;
41
+ req: PayloadRequest;
42
+ resourceId: number | string;
43
+ serviceId: number | string;
44
+ startTime: Date;
45
+ }): Promise<TakeHoldResult>;
46
+ /** Release a hold early. Idempotent: releasing an unknown token is not an error. */
47
+ export declare function releaseHold(params: {
48
+ config: ResolvedReservationPluginConfig;
49
+ req: PayloadRequest;
50
+ token: string;
51
+ }): Promise<{
52
+ released: number;
53
+ }>;