payload-reserve 2.3.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +218 -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 +12 -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 +27 -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 +27 -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 +4 -0
  42. package/dist/endpoints/resourceAvailability.js +105 -4
  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 +22 -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 +51 -5
  69. package/dist/services/AvailabilityService.js +315 -38
  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 +32 -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/reserveDebug.d.ts +26 -0
  97. package/dist/utilities/reserveDebug.js +35 -0
  98. package/dist/utilities/reserveDebug.js.map +1 -0
  99. package/dist/utilities/resolveReservationItems.d.ts +13 -2
  100. package/dist/utilities/resolveReservationItems.js +87 -2
  101. package/dist/utilities/resolveReservationItems.js.map +1 -1
  102. package/dist/utilities/retryOnWriteConflict.d.ts +66 -0
  103. package/dist/utilities/retryOnWriteConflict.js +135 -0
  104. package/dist/utilities/retryOnWriteConflict.js.map +1 -0
  105. package/dist/utilities/tenantTimezone.d.ts +77 -1
  106. package/dist/utilities/tenantTimezone.js +163 -2
  107. package/dist/utilities/tenantTimezone.js.map +1 -1
  108. package/dist/utilities/transactionSupport.d.ts +13 -0
  109. package/dist/utilities/transactionSupport.js +30 -0
  110. package/dist/utilities/transactionSupport.js.map +1 -0
  111. package/package.json +13 -13
@@ -1,3 +1,4 @@
1
+ import { createReserveDebug, NOOP_RESERVE_DEBUG } from '../utilities/reserveDebug.js';
1
2
  import { resolveScheduleForDate } from '../utilities/scheduleUtils.js';
2
3
  import { readCookie } from '../utilities/tenantFilter.js';
3
4
  import { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js';
@@ -5,7 +6,7 @@ import { addDaysToDayKey, combineDayKeyAndTime, getDayKeyInTimezone } from '../u
5
6
  import { isPrivilegedUser } from '../utilities/userRoles.js';
6
7
  const MAX_RANGE_MS = 90 * 86_400_000;
7
8
  /** Busy intervals (with capacity units) for one resource over [start, end). */ async function busyFor(args) {
8
- const { blockingStatuses, capacityMode, end, payload, reservationSlug, resourceId, start } = args;
9
+ const { blockingStatuses, capacityMode, end, holdsSlug, payload, reservationSlug, resourceId, start } = args;
9
10
  const where = {
10
11
  and: [
11
12
  {
@@ -48,14 +49,62 @@ const MAX_RANGE_MS = 90 * 86_400_000;
48
49
  limit: 0,
49
50
  where
50
51
  });
51
- return docs.filter((r)=>r.startTime && r.endTime).map((r)=>({
52
+ const busy = docs.filter((r)=>r.startTime && r.endTime).map((r)=>({
52
53
  end: new Date(r.endTime).toISOString(),
53
54
  start: new Date(r.startTime).toISOString(),
54
55
  units: capacityMode === 'per-guest' ? r.guestCount ?? 1 : 1
55
56
  }));
57
+ // An unexpired hold occupies the resource exactly as a blocking reservation
58
+ // does — the write path counts it — so the grid must show it busy. Expiry is a
59
+ // read-time predicate here for the same reason it is in `checkAvailability`:
60
+ // TTL indexes are MongoDB-only.
61
+ if (holdsSlug) {
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ const { docs: holdDocs } = await payload.find({
64
+ collection: holdsSlug,
65
+ depth: 0,
66
+ limit: 0,
67
+ where: {
68
+ and: [
69
+ {
70
+ resource: {
71
+ equals: resourceId
72
+ }
73
+ },
74
+ {
75
+ expiresAt: {
76
+ greater_than: new Date().toISOString()
77
+ }
78
+ },
79
+ {
80
+ startTime: {
81
+ less_than: end.toISOString()
82
+ }
83
+ },
84
+ {
85
+ endTime: {
86
+ greater_than: start.toISOString()
87
+ }
88
+ }
89
+ ]
90
+ }
91
+ });
92
+ for (const hold of holdDocs){
93
+ if (!hold.startTime || !hold.endTime) {
94
+ continue;
95
+ }
96
+ busy.push({
97
+ end: new Date(hold.endTime).toISOString(),
98
+ start: new Date(hold.startTime).toISOString(),
99
+ units: capacityMode === 'per-guest' ? hold.guestCount ?? 1 : 1
100
+ });
101
+ }
102
+ }
103
+ return busy;
56
104
  }
57
105
  export async function buildResourceAvailability(params) {
58
- const { blockingStatuses, end, getExternalBusy, payload, req, reservationSlug, resourceId, resourceSlug, scheduleSlug, start, timeZone } = params;
106
+ const { blockingStatuses, debug, end, getExternalBusy, holdsSlug, payload, req, reservationSlug, resourceId, resourceSlug, scheduleSlug, start, timeZone } = params;
107
+ const trace = debug ?? NOOP_RESERVE_DEBUG;
59
108
  // depth 1 so `services` are populated (their `requiredResources` come back as ids)
60
109
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
110
  const resource = await payload.findByID({
@@ -143,6 +192,7 @@ export async function buildResourceAvailability(params) {
143
192
  blockingStatuses,
144
193
  capacityMode,
145
194
  end,
195
+ holdsSlug,
146
196
  payload,
147
197
  reservationSlug,
148
198
  resourceId,
@@ -178,6 +228,7 @@ export async function buildResourceAvailability(params) {
178
228
  blockingStatuses,
179
229
  capacityMode: poolCapacityMode,
180
230
  end,
231
+ holdsSlug,
181
232
  payload,
182
233
  reservationSlug,
183
234
  resourceId: poolId,
@@ -197,7 +248,12 @@ export async function buildResourceAvailability(params) {
197
248
  resourceId,
198
249
  start
199
250
  })).filter((iv)=>!isNaN(Date.parse(iv.start)) && !isNaN(Date.parse(iv.end)));
200
- } catch {
251
+ } catch (err) {
252
+ trace.dbg('error', {
253
+ err,
254
+ resourceId,
255
+ where: 'getExternalBusy'
256
+ });
201
257
  external = [];
202
258
  }
203
259
  }
@@ -230,6 +286,7 @@ export function createResourceAvailabilityEndpoint(config) {
230
286
  status: 403
231
287
  });
232
288
  }
289
+ const dbg = createReserveDebug(req.payload.logger, config.debug);
233
290
  const url = new URL(req.url);
234
291
  const resource = url.searchParams.get('resource');
235
292
  const start = url.searchParams.get('start');
@@ -265,6 +322,31 @@ export function createResourceAvailabilityEndpoint(config) {
265
322
  status: 400
266
323
  });
267
324
  }
325
+ // Authorization boundary. Delegating to collection access here is what
326
+ // makes owner-mode ownership AND multi-tenant isolation apply — the four
327
+ // reads inside buildResourceAvailability deliberately stay privileged so
328
+ // the busy grid stays complete (see the spec, A4). depth:0 is required:
329
+ // at depth 1 an access-denied `services` relationship populates as null
330
+ // and the requiredResources loop throws on `typeof null === 'object'`.
331
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
332
+ const permitted = await req.payload.findByID({
333
+ id: resource,
334
+ collection: config.slugs.resources,
335
+ depth: 0,
336
+ overrideAccess: false,
337
+ req
338
+ }).catch(()=>null);
339
+ if (!permitted) {
340
+ dbg.dbg('response', {
341
+ endpoint: 'resource-availability',
342
+ reason: 'resource_not_found'
343
+ });
344
+ return Response.json({
345
+ error: 'Resource not found'
346
+ }, {
347
+ status: 404
348
+ });
349
+ }
268
350
  // In multiTenant mode, resolve day-boundaries in the SELECTED tenant's zone
269
351
  // (tenant timezone → global → UTC). Degrades to the global zone for plain
270
352
  // installs: no tenant relationship on reservations / no tenant cookie ⇒ no
@@ -273,15 +355,25 @@ export function createResourceAvailabilityEndpoint(config) {
273
355
  const timeZone = await getEffectiveTenantTimezone({
274
356
  globalTimezone: config.timezone,
275
357
  payload: req.payload,
358
+ req,
276
359
  scopedCollection: reservationsCollection,
277
360
  tenantField: config.multiTenant.tenantField,
278
361
  tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),
279
362
  timezoneField: config.multiTenant.timezoneField
280
363
  });
364
+ dbg.dbg('request', {
365
+ end,
366
+ endpoint: 'resource-availability',
367
+ resource,
368
+ start,
369
+ timeZone
370
+ });
281
371
  const result = await buildResourceAvailability({
282
372
  blockingStatuses: config.statusMachine.blockingStatuses,
373
+ debug: dbg,
283
374
  end: endDate,
284
375
  getExternalBusy: config.getExternalBusy,
376
+ holdsSlug: config.slotHolds.enabled ? config.slugs.holds : undefined,
285
377
  payload: req.payload,
286
378
  req,
287
379
  reservationSlug: config.slugs.reservations,
@@ -292,12 +384,21 @@ export function createResourceAvailabilityEndpoint(config) {
292
384
  timeZone
293
385
  });
294
386
  if (!result) {
387
+ dbg.dbg('response', {
388
+ endpoint: 'resource-availability',
389
+ reason: 'resource_not_found'
390
+ });
295
391
  return Response.json({
296
392
  error: 'Resource not found'
297
393
  }, {
298
394
  status: 404
299
395
  });
300
396
  }
397
+ dbg.dbg('response', {
398
+ dayCount: result.days.length,
399
+ endpoint: 'resource-availability',
400
+ externalCount: result.external.length
401
+ });
301
402
  return Response.json(result);
302
403
  },
303
404
  method: 'get',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/resourceAvailability.ts"],"sourcesContent":["import type { Endpoint, Payload, PayloadRequest, Where } from 'payload'\n\nimport type {\n ExternalBusyInterval,\n GetExternalBusy,\n ResolvedReservationPluginConfig,\n} from '../types.js'\n\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { readCookie } from '../utilities/tenantFilter.js'\nimport { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js'\nimport {\n addDaysToDayKey,\n combineDayKeyAndTime,\n getDayKeyInTimezone,\n} from '../utilities/timezoneUtils.js'\nimport { isPrivilegedUser } from '../utilities/userRoles.js'\n\nconst MAX_RANGE_MS = 90 * 86_400_000\n\ntype DayAvailability = {\n date: string\n shiftWindows: Array<{ end: string; start: string }>\n timeOff: Array<{ end: string; reason?: string; start: string; type?: string }>\n}\n\ntype Busy = Array<{ end: string; start: string; units: number }>\n\nexport type ResourceAvailability = {\n busy: Busy\n capacityMode: 'per-guest' | 'per-reservation'\n days: DayAvailability[]\n /** External busy intervals (calendar sync etc.) from getExternalBusy — display-only here; enforcement lives in checkAvailability. */\n external: ExternalBusyInterval[]\n quantity: number\n /** Capacity of resources this resource's services also require (e.g. a chair pool). */\n requiredPools: Array<{ busy: Busy; quantity: number }>\n /** IANA zone the day windows were resolved in (selected tenant's zone, else global). */\n timeZone: string\n}\n\n/** Busy intervals (with capacity units) for one resource over [start, end). */\nasync function busyFor(args: {\n blockingStatuses: string[]\n capacityMode: 'per-guest' | 'per-reservation'\n end: Date\n payload: Payload\n reservationSlug: string\n resourceId: number | string\n start: Date\n}): Promise<Busy> {\n const { blockingStatuses, capacityMode, end, payload, reservationSlug, resourceId, start } = args\n const where: Where = {\n and: [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: end.toISOString() } },\n { endTime: { greater_than: start.toISOString() } },\n { or: [{ resource: { equals: resourceId } }, { 'items.resource': { equals: resourceId } }] },\n ],\n }\n // limit:0 = all matching — bounded by the endpoint's 90-day range cap, so this\n // can't run away, and the grid no longer silently drops busy intervals (D9).\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 where,\n })\n return (docs as Array<Record<string, unknown>>)\n .filter((r) => r.startTime && r.endTime)\n .map((r) => ({\n end: new Date(r.endTime as string).toISOString(),\n start: new Date(r.startTime as string).toISOString(),\n units: capacityMode === 'per-guest' ? ((r.guestCount as number) ?? 1) : 1,\n }))\n}\n\nexport async function buildResourceAvailability(params: {\n blockingStatuses: string[]\n end: Date\n getExternalBusy?: GetExternalBusy\n payload: Payload\n req?: PayloadRequest\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n scheduleSlug: string\n start: Date\n timeZone: string\n}): Promise<null | ResourceAvailability> {\n const {\n blockingStatuses,\n end,\n getExternalBusy,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n scheduleSlug,\n start,\n timeZone,\n } = params\n\n // depth 1 so `services` are populated (their `requiredResources` come back as ids)\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: 1,\n }).catch(() => null)\n if (!resource) {\n return null\n }\n const quantity = (resource?.quantity as number) ?? 1\n const capacityMode =\n (resource?.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n\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 where: { and: [{ active: { equals: true } }, { resource: { equals: resourceId } }] },\n })\n\n type RawException = {\n date: string\n endDate?: string\n reason?: string\n type?: string\n }\n\n const days: DayAvailability[] = []\n const startKey = getDayKeyInTimezone(start, timeZone)\n const lastKey = getDayKeyInTimezone(new Date(end.getTime() - 1), timeZone)\n for (let date = startKey; date <= lastKey; date = addDaysToDayKey(date, 1)) {\n const shiftWindows: DayAvailability['shiftWindows'] = []\n const timeOff: DayAvailability['timeOff'] = []\n\n // A11: an exception on ANY of the resource's schedules makes the whole\n // resource unavailable that day. Find the first matching exception (if any)\n // — it suppresses all shift windows and marks the full day as time-off.\n let dayException: RawException | undefined\n for (const sched of schedules as Array<Record<string, unknown>>) {\n const exceptions = (sched.exceptions as RawException[] | undefined) ?? []\n for (const exc of exceptions) {\n const excStart = getDayKeyInTimezone(new Date(exc.date), timeZone)\n const excEnd = exc.endDate ? getDayKeyInTimezone(new Date(exc.endDate), timeZone) : excStart\n if (date >= excStart && date <= excEnd) {\n dayException = exc\n break\n }\n }\n if (dayException) {\n break\n }\n }\n\n if (dayException) {\n const dayStart = combineDayKeyAndTime(date, '00:00', timeZone)\n const dayEnd = new Date(combineDayKeyAndTime(date, '23:59', timeZone).getTime() + 59_999)\n timeOff.push({\n type: dayException.type,\n end: dayEnd.toISOString(),\n reason: dayException.reason,\n start: dayStart.toISOString(),\n })\n } else {\n for (const sched of schedules as Array<Record<string, unknown>>) {\n // resolveScheduleForDate accepts a Schedule-shaped object; cast through unknown\n const ranges = resolveScheduleForDate(\n sched as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n timeZone,\n )\n for (const r of ranges) {\n shiftWindows.push({ end: r.end.toISOString(), start: r.start.toISOString() })\n }\n }\n }\n\n days.push({ date, shiftWindows, timeOff })\n }\n\n const busy = await busyFor({\n blockingStatuses,\n capacityMode,\n end,\n payload,\n reservationSlug,\n resourceId,\n start,\n })\n\n // Resources this resource's services ALSO require (e.g. a shared chair pool).\n // A slot isn't truly bookable if any of these is at capacity, even when the\n // resource itself is free — so the calendar reflects real availability.\n const poolIds = new Set<string>()\n for (const svc of (resource?.services as Array<Record<string, unknown>>) ?? []) {\n const reqs = (typeof svc === 'object' ? (svc.requiredResources as unknown[]) : []) ?? []\n for (const rr of reqs) {\n const id: number | string | undefined =\n typeof rr === 'object' && rr !== null\n ? (rr as { id?: number | string }).id\n : (rr as number | string)\n if (id != null && String(id) !== String(resourceId)) {\n poolIds.add(String(id))\n }\n }\n }\n\n const requiredPools: ResourceAvailability['requiredPools'] = []\n for (const poolId of poolIds) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const pool = await (payload.findByID as any)({\n id: poolId,\n collection: resourceSlug,\n depth: 0,\n }).catch(() => null)\n if (!pool) {\n continue\n }\n const poolCapacityMode =\n (pool.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n requiredPools.push({\n busy: await busyFor({\n blockingStatuses,\n capacityMode: poolCapacityMode,\n end,\n payload,\n reservationSlug,\n resourceId: poolId,\n start,\n }),\n quantity: (pool.quantity as number) ?? 1,\n })\n }\n\n // External busy — display only (enforcement lives in checkAvailability).\n // Fail-open: a resolver error must never break the grid.\n let external: ExternalBusyInterval[] = []\n if (getExternalBusy && req) {\n try {\n external = (await getExternalBusy({ end, req, resourceId, start })).filter(\n (iv) => !isNaN(Date.parse(iv.start)) && !isNaN(Date.parse(iv.end)),\n )\n } catch {\n external = []\n }\n }\n\n return { busy, capacityMode, days, external, quantity, requiredPools, timeZone }\n}\n\nexport function createResourceAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n // The grid data (every reservation's busy window) is staff/admin-only —\n // same gate as customer search.\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n if (!isPrivilegedUser(req.user, config)) {\n return Response.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const url = new URL(req.url!)\n const resource = url.searchParams.get('resource')\n const start = url.searchParams.get('start')\n const end = url.searchParams.get('end')\n\n if (!resource || !start || !end) {\n return Response.json(\n { error: 'Missing required query params: resource, start, end' },\n { status: 400 },\n )\n }\n\n const startDate = new Date(start)\n const endDate = new Date(end)\n if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {\n return Response.json({ error: 'Invalid start/end date' }, { status: 400 })\n }\n if (endDate <= startDate) {\n return Response.json({ error: 'end must be after start' }, { status: 400 })\n }\n // Unbounded ranges turn the per-day resolution loop into a CPU sink\n if (endDate.getTime() - startDate.getTime() > MAX_RANGE_MS) {\n return Response.json({ error: 'Date range too large (max 90 days)' }, { status: 400 })\n }\n\n // In multiTenant mode, resolve day-boundaries in the SELECTED tenant's zone\n // (tenant timezone → global → UTC). Degrades to the global zone for plain\n // installs: no tenant relationship on reservations / no tenant cookie ⇒ no\n // DB read, same output as before.\n const reservationsCollection = req.payload.config.collections?.find(\n (c) => c.slug === config.slugs.reservations,\n )\n const timeZone = await getEffectiveTenantTimezone({\n globalTimezone: config.timezone,\n payload: req.payload,\n scopedCollection: reservationsCollection as { fields?: unknown[] } | undefined,\n tenantField: config.multiTenant.tenantField,\n tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),\n timezoneField: config.multiTenant.timezoneField,\n })\n\n const result = await buildResourceAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n end: endDate,\n getExternalBusy: config.getExternalBusy,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: resource,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n start: startDate,\n timeZone,\n })\n\n if (!result) {\n return Response.json({ error: 'Resource not found' }, { status: 404 })\n }\n\n return Response.json(result)\n },\n method: 'get',\n path: '/reserve/resource-availability',\n }\n}\n"],"names":["resolveScheduleForDate","readCookie","getEffectiveTenantTimezone","addDaysToDayKey","combineDayKeyAndTime","getDayKeyInTimezone","isPrivilegedUser","MAX_RANGE_MS","busyFor","args","blockingStatuses","capacityMode","end","payload","reservationSlug","resourceId","start","where","and","status","in","startTime","less_than","toISOString","endTime","greater_than","or","resource","equals","docs","find","collection","depth","limit","filter","r","map","Date","units","guestCount","buildResourceAvailability","params","getExternalBusy","req","resourceSlug","scheduleSlug","timeZone","findByID","id","catch","quantity","schedules","active","days","startKey","lastKey","getTime","date","shiftWindows","timeOff","dayException","sched","exceptions","exc","excStart","excEnd","endDate","dayStart","dayEnd","push","type","reason","ranges","busy","poolIds","Set","svc","services","reqs","requiredResources","rr","String","add","requiredPools","poolId","pool","poolCapacityMode","external","iv","isNaN","parse","createResourceAvailabilityEndpoint","config","handler","user","Response","json","error","url","URL","searchParams","get","startDate","reservationsCollection","collections","c","slug","slugs","reservations","globalTimezone","timezone","scopedCollection","tenantField","multiTenant","tenantId","headers","cookieName","timezoneField","result","statusMachine","resources","method","path"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/endpoints/resourceAvailability.ts"],"sourcesContent":["import type { Endpoint, Payload, PayloadRequest, Where } from 'payload'\n\nimport type {\n ExternalBusyInterval,\n GetExternalBusy,\n ResolvedReservationPluginConfig,\n} from '../types.js'\n\nimport {\n createReserveDebug,\n NOOP_RESERVE_DEBUG,\n type ReserveDebug,\n} from '../utilities/reserveDebug.js'\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { readCookie } from '../utilities/tenantFilter.js'\nimport { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js'\nimport {\n addDaysToDayKey,\n combineDayKeyAndTime,\n getDayKeyInTimezone,\n} from '../utilities/timezoneUtils.js'\nimport { isPrivilegedUser } from '../utilities/userRoles.js'\n\nconst MAX_RANGE_MS = 90 * 86_400_000\n\ntype DayAvailability = {\n date: string\n shiftWindows: Array<{ end: string; start: string }>\n timeOff: Array<{ end: string; reason?: string; start: string; type?: string }>\n}\n\ntype Busy = Array<{ end: string; start: string; units: number }>\n\nexport type ResourceAvailability = {\n busy: Busy\n capacityMode: 'per-guest' | 'per-reservation'\n days: DayAvailability[]\n /** External busy intervals (calendar sync etc.) from getExternalBusy — display-only here; enforcement lives in checkAvailability. */\n external: ExternalBusyInterval[]\n quantity: number\n /** Capacity of resources this resource's services also require (e.g. a chair pool). */\n requiredPools: Array<{ busy: Busy; quantity: number }>\n /** IANA zone the day windows were resolved in (selected tenant's zone, else global). */\n timeZone: string\n}\n\n/** Busy intervals (with capacity units) for one resource over [start, end). */\nasync function busyFor(args: {\n blockingStatuses: string[]\n capacityMode: 'per-guest' | 'per-reservation'\n end: Date\n /** Slot-holds slug; omit to ignore holds (i.e. when `slotHolds` is off). */\n holdsSlug?: string\n payload: Payload\n reservationSlug: string\n resourceId: number | string\n start: Date\n}): Promise<Busy> {\n const {\n blockingStatuses,\n capacityMode,\n end,\n holdsSlug,\n payload,\n reservationSlug,\n resourceId,\n start,\n } = args\n const where: Where = {\n and: [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: end.toISOString() } },\n { endTime: { greater_than: start.toISOString() } },\n { or: [{ resource: { equals: resourceId } }, { 'items.resource': { equals: resourceId } }] },\n ],\n }\n // limit:0 = all matching — bounded by the endpoint's 90-day range cap, so this\n // can't run away, and the grid no longer silently drops busy intervals (D9).\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 where,\n })\n const busy: Busy = (docs as Array<Record<string, unknown>>)\n .filter((r) => r.startTime && r.endTime)\n .map((r) => ({\n end: new Date(r.endTime as string).toISOString(),\n start: new Date(r.startTime as string).toISOString(),\n units: capacityMode === 'per-guest' ? ((r.guestCount as number) ?? 1) : 1,\n }))\n\n // An unexpired hold occupies the resource exactly as a blocking reservation\n // does — the write path counts it — so the grid must show it busy. Expiry is a\n // read-time predicate here for the same reason it is in `checkAvailability`:\n // TTL indexes are MongoDB-only.\n if (holdsSlug) {\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 where: {\n and: [\n { resource: { equals: resourceId } },\n { expiresAt: { greater_than: new Date().toISOString() } },\n { startTime: { less_than: end.toISOString() } },\n { endTime: { greater_than: start.toISOString() } },\n ],\n } as Where,\n })\n for (const hold of holdDocs as Array<Record<string, unknown>>) {\n if (!hold.startTime || !hold.endTime) {\n continue\n }\n busy.push({\n end: new Date(hold.endTime as string).toISOString(),\n start: new Date(hold.startTime as string).toISOString(),\n units: capacityMode === 'per-guest' ? ((hold.guestCount as number) ?? 1) : 1,\n })\n }\n }\n\n return busy\n}\n\nexport async function buildResourceAvailability(params: {\n blockingStatuses: string[]\n debug?: ReserveDebug\n end: Date\n getExternalBusy?: GetExternalBusy\n /** Slot-holds slug; omit to ignore holds (i.e. when `slotHolds` is off). */\n holdsSlug?: string\n payload: Payload\n req?: PayloadRequest\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n scheduleSlug: string\n start: Date\n timeZone: string\n}): Promise<null | ResourceAvailability> {\n const {\n blockingStatuses,\n debug,\n end,\n getExternalBusy,\n holdsSlug,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n scheduleSlug,\n start,\n timeZone,\n } = params\n const trace = debug ?? NOOP_RESERVE_DEBUG\n\n // depth 1 so `services` are populated (their `requiredResources` come back as ids)\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: 1,\n }).catch(() => null)\n if (!resource) {\n return null\n }\n const quantity = (resource?.quantity as number) ?? 1\n const capacityMode =\n (resource?.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n\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 where: { and: [{ active: { equals: true } }, { resource: { equals: resourceId } }] },\n })\n\n type RawException = {\n date: string\n endDate?: string\n reason?: string\n type?: string\n }\n\n const days: DayAvailability[] = []\n const startKey = getDayKeyInTimezone(start, timeZone)\n const lastKey = getDayKeyInTimezone(new Date(end.getTime() - 1), timeZone)\n for (let date = startKey; date <= lastKey; date = addDaysToDayKey(date, 1)) {\n const shiftWindows: DayAvailability['shiftWindows'] = []\n const timeOff: DayAvailability['timeOff'] = []\n\n // A11: an exception on ANY of the resource's schedules makes the whole\n // resource unavailable that day. Find the first matching exception (if any)\n // — it suppresses all shift windows and marks the full day as time-off.\n let dayException: RawException | undefined\n for (const sched of schedules as Array<Record<string, unknown>>) {\n const exceptions = (sched.exceptions as RawException[] | undefined) ?? []\n for (const exc of exceptions) {\n const excStart = getDayKeyInTimezone(new Date(exc.date), timeZone)\n const excEnd = exc.endDate ? getDayKeyInTimezone(new Date(exc.endDate), timeZone) : excStart\n if (date >= excStart && date <= excEnd) {\n dayException = exc\n break\n }\n }\n if (dayException) {\n break\n }\n }\n\n if (dayException) {\n const dayStart = combineDayKeyAndTime(date, '00:00', timeZone)\n const dayEnd = new Date(combineDayKeyAndTime(date, '23:59', timeZone).getTime() + 59_999)\n timeOff.push({\n type: dayException.type,\n end: dayEnd.toISOString(),\n reason: dayException.reason,\n start: dayStart.toISOString(),\n })\n } else {\n for (const sched of schedules as Array<Record<string, unknown>>) {\n // resolveScheduleForDate accepts a Schedule-shaped object; cast through unknown\n const ranges = resolveScheduleForDate(\n sched as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n timeZone,\n )\n for (const r of ranges) {\n shiftWindows.push({ end: r.end.toISOString(), start: r.start.toISOString() })\n }\n }\n }\n\n days.push({ date, shiftWindows, timeOff })\n }\n\n const busy = await busyFor({\n blockingStatuses,\n capacityMode,\n end,\n holdsSlug,\n payload,\n reservationSlug,\n resourceId,\n start,\n })\n\n // Resources this resource's services ALSO require (e.g. a shared chair pool).\n // A slot isn't truly bookable if any of these is at capacity, even when the\n // resource itself is free — so the calendar reflects real availability.\n const poolIds = new Set<string>()\n for (const svc of (resource?.services as Array<Record<string, unknown>>) ?? []) {\n const reqs = (typeof svc === 'object' ? (svc.requiredResources as unknown[]) : []) ?? []\n for (const rr of reqs) {\n const id: number | string | undefined =\n typeof rr === 'object' && rr !== null\n ? (rr as { id?: number | string }).id\n : (rr as number | string)\n if (id != null && String(id) !== String(resourceId)) {\n poolIds.add(String(id))\n }\n }\n }\n\n const requiredPools: ResourceAvailability['requiredPools'] = []\n for (const poolId of poolIds) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const pool = await (payload.findByID as any)({\n id: poolId,\n collection: resourceSlug,\n depth: 0,\n }).catch(() => null)\n if (!pool) {\n continue\n }\n const poolCapacityMode =\n (pool.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n requiredPools.push({\n busy: await busyFor({\n blockingStatuses,\n capacityMode: poolCapacityMode,\n end,\n holdsSlug,\n payload,\n reservationSlug,\n resourceId: poolId,\n start,\n }),\n quantity: (pool.quantity as number) ?? 1,\n })\n }\n\n // External busy — display only (enforcement lives in checkAvailability).\n // Fail-open: a resolver error must never break the grid.\n let external: ExternalBusyInterval[] = []\n if (getExternalBusy && req) {\n try {\n external = (await getExternalBusy({ end, req, resourceId, start })).filter(\n (iv) => !isNaN(Date.parse(iv.start)) && !isNaN(Date.parse(iv.end)),\n )\n } catch (err) {\n trace.dbg('error', { err, resourceId, where: 'getExternalBusy' })\n external = []\n }\n }\n\n return { busy, capacityMode, days, external, quantity, requiredPools, timeZone }\n}\n\nexport function createResourceAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n // The grid data (every reservation's busy window) is staff/admin-only —\n // same gate as customer search.\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n if (!isPrivilegedUser(req.user, config)) {\n return Response.json({ error: 'Forbidden' }, { status: 403 })\n }\n const dbg = createReserveDebug(req.payload.logger, config.debug)\n\n const url = new URL(req.url!)\n const resource = url.searchParams.get('resource')\n const start = url.searchParams.get('start')\n const end = url.searchParams.get('end')\n\n if (!resource || !start || !end) {\n return Response.json(\n { error: 'Missing required query params: resource, start, end' },\n { status: 400 },\n )\n }\n\n const startDate = new Date(start)\n const endDate = new Date(end)\n if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {\n return Response.json({ error: 'Invalid start/end date' }, { status: 400 })\n }\n if (endDate <= startDate) {\n return Response.json({ error: 'end must be after start' }, { status: 400 })\n }\n // Unbounded ranges turn the per-day resolution loop into a CPU sink\n if (endDate.getTime() - startDate.getTime() > MAX_RANGE_MS) {\n return Response.json({ error: 'Date range too large (max 90 days)' }, { status: 400 })\n }\n\n // Authorization boundary. Delegating to collection access here is what\n // makes owner-mode ownership AND multi-tenant isolation apply — the four\n // reads inside buildResourceAvailability deliberately stay privileged so\n // the busy grid stays complete (see the spec, A4). depth:0 is required:\n // at depth 1 an access-denied `services` relationship populates as null\n // and the requiredResources loop throws on `typeof null === 'object'`.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const permitted = await (req.payload.findByID as any)({\n id: resource,\n collection: config.slugs.resources,\n depth: 0,\n overrideAccess: false,\n req,\n }).catch(() => null)\n\n if (!permitted) {\n dbg.dbg('response', { endpoint: 'resource-availability', reason: 'resource_not_found' })\n return Response.json({ error: 'Resource not found' }, { status: 404 })\n }\n\n // In multiTenant mode, resolve day-boundaries in the SELECTED tenant's zone\n // (tenant timezone → global → UTC). Degrades to the global zone for plain\n // installs: no tenant relationship on reservations / no tenant cookie ⇒ no\n // DB read, same output as before.\n const reservationsCollection = req.payload.config.collections?.find(\n (c) => c.slug === config.slugs.reservations,\n )\n const timeZone = await getEffectiveTenantTimezone({\n globalTimezone: config.timezone,\n payload: req.payload,\n req,\n scopedCollection: reservationsCollection as { fields?: unknown[] } | undefined,\n tenantField: config.multiTenant.tenantField,\n tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),\n timezoneField: config.multiTenant.timezoneField,\n })\n\n dbg.dbg('request', { end, endpoint: 'resource-availability', resource, start, timeZone })\n\n const result = await buildResourceAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n debug: dbg,\n end: endDate,\n getExternalBusy: config.getExternalBusy,\n holdsSlug: config.slotHolds.enabled ? config.slugs.holds : undefined,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: resource,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n start: startDate,\n timeZone,\n })\n\n if (!result) {\n dbg.dbg('response', { endpoint: 'resource-availability', reason: 'resource_not_found' })\n return Response.json({ error: 'Resource not found' }, { status: 404 })\n }\n\n dbg.dbg('response', {\n dayCount: result.days.length,\n endpoint: 'resource-availability',\n externalCount: result.external.length,\n })\n return Response.json(result)\n },\n method: 'get',\n path: '/reserve/resource-availability',\n }\n}\n"],"names":["createReserveDebug","NOOP_RESERVE_DEBUG","resolveScheduleForDate","readCookie","getEffectiveTenantTimezone","addDaysToDayKey","combineDayKeyAndTime","getDayKeyInTimezone","isPrivilegedUser","MAX_RANGE_MS","busyFor","args","blockingStatuses","capacityMode","end","holdsSlug","payload","reservationSlug","resourceId","start","where","and","status","in","startTime","less_than","toISOString","endTime","greater_than","or","resource","equals","docs","find","collection","depth","limit","busy","filter","r","map","Date","units","guestCount","holdDocs","expiresAt","hold","push","buildResourceAvailability","params","debug","getExternalBusy","req","resourceSlug","scheduleSlug","timeZone","trace","findByID","id","catch","quantity","schedules","active","days","startKey","lastKey","getTime","date","shiftWindows","timeOff","dayException","sched","exceptions","exc","excStart","excEnd","endDate","dayStart","dayEnd","type","reason","ranges","poolIds","Set","svc","services","reqs","requiredResources","rr","String","add","requiredPools","poolId","pool","poolCapacityMode","external","iv","isNaN","parse","err","dbg","createResourceAvailabilityEndpoint","config","handler","user","Response","json","error","logger","url","URL","searchParams","get","startDate","permitted","slugs","resources","overrideAccess","endpoint","reservationsCollection","collections","c","slug","reservations","globalTimezone","timezone","scopedCollection","tenantField","multiTenant","tenantId","headers","cookieName","timezoneField","result","statusMachine","slotHolds","enabled","holds","undefined","dayCount","length","externalCount","method","path"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { CollectionBeforeChangeHook } from 'payload';
2
+ import type { ResolvedReservationPluginConfig } from '../../types.js';
3
+ /**
4
+ * Serialize and validate a slot hold, inside the hold's own transaction.
5
+ *
6
+ * This deliberately lives in a `beforeChange` hook rather than in a service
7
+ * function that calls the pieces in sequence. The lock only serializes anything
8
+ * if the lock write, the availability read, and the insert all share one
9
+ * transaction — and Payload opens that transaction around `create`, so a hook
10
+ * is inside it while an orchestrating caller is not. An earlier version of this
11
+ * ran the same three steps from a service function and granted 3 of 8
12
+ * simultaneous holds for one slot; moved here, it grants exactly 1.
13
+ */
14
+ export declare const validateHoldSlot: (config: ResolvedReservationPluginConfig) => CollectionBeforeChangeHook;
@@ -0,0 +1,90 @@
1
+ import { ValidationError } from 'payload';
2
+ import { checkAvailability } from '../../services/AvailabilityService.js';
3
+ import { extractId } from '../../utilities/resolveReservationItems.js';
4
+ /**
5
+ * Serialize and validate a slot hold, inside the hold's own transaction.
6
+ *
7
+ * This deliberately lives in a `beforeChange` hook rather than in a service
8
+ * function that calls the pieces in sequence. The lock only serializes anything
9
+ * if the lock write, the availability read, and the insert all share one
10
+ * transaction — and Payload opens that transaction around `create`, so a hook
11
+ * is inside it while an orchestrating caller is not. An earlier version of this
12
+ * ran the same three steps from a service function and granted 3 of 8
13
+ * simultaneous holds for one slot; moved here, it grants exactly 1.
14
+ */ export const validateHoldSlot = (config)=>async ({ context, data, operation, req })=>{
15
+ if (context?.skipReservationHooks || operation !== 'create') {
16
+ return data;
17
+ }
18
+ const resourceId = extractId(data.resource);
19
+ const serviceId = extractId(data.service);
20
+ if (resourceId === undefined || !data.startTime || !data.endTime) {
21
+ return data;
22
+ }
23
+ // Claim the resource first. Two simultaneous holds — or a hold racing a
24
+ // booking — now collide on this one document and the database serializes
25
+ // them. See acquireBookingLock for the full rationale.
26
+ await req.payload.db.updateOne({
27
+ id: resourceId,
28
+ collection: config.slugs.resources,
29
+ data: {
30
+ bookingLock: `hold:${String(data.startTime)}`
31
+ },
32
+ req,
33
+ returning: false
34
+ });
35
+ let bufferBefore = config.defaultBufferTime;
36
+ let bufferAfter = config.defaultBufferTime;
37
+ if (serviceId !== undefined) {
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ const service = await req.payload.findByID({
40
+ id: serviceId,
41
+ collection: config.slugs.services,
42
+ depth: 0,
43
+ joins: false,
44
+ req
45
+ });
46
+ if (service) {
47
+ if (config.enforceActive && service.active === false) {
48
+ throw new ValidationError({
49
+ errors: [
50
+ {
51
+ message: 'Service is not active',
52
+ path: 'service'
53
+ }
54
+ ]
55
+ });
56
+ }
57
+ bufferBefore = service.bufferTimeBefore ?? config.defaultBufferTime;
58
+ bufferAfter = service.bufferTimeAfter ?? config.defaultBufferTime;
59
+ }
60
+ }
61
+ const availability = await checkAvailability({
62
+ blockingStatuses: config.statusMachine.blockingStatuses,
63
+ bufferAfter,
64
+ bufferBefore,
65
+ endTime: new Date(data.endTime),
66
+ getExternalBusy: config.getExternalBusy,
67
+ guestCount: data.guestCount ?? 1,
68
+ holdsSlug: config.slugs.holds,
69
+ payload: req.payload,
70
+ req,
71
+ reservationSlug: config.slugs.reservations,
72
+ resourceId,
73
+ resourceSlug: config.slugs.resources,
74
+ servicesSlug: config.slugs.services,
75
+ startTime: new Date(data.startTime)
76
+ });
77
+ if (!availability.available) {
78
+ throw new ValidationError({
79
+ errors: [
80
+ {
81
+ message: availability.reason ?? 'Slot is not available',
82
+ path: 'startTime'
83
+ }
84
+ ]
85
+ });
86
+ }
87
+ return data;
88
+ };
89
+
90
+ //# sourceMappingURL=validateHoldSlot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/hooks/holds/validateHoldSlot.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { checkAvailability } from '../../services/AvailabilityService.js'\nimport { extractId } from '../../utilities/resolveReservationItems.js'\n\n/**\n * Serialize and validate a slot hold, inside the hold's own transaction.\n *\n * This deliberately lives in a `beforeChange` hook rather than in a service\n * function that calls the pieces in sequence. The lock only serializes anything\n * if the lock write, the availability read, and the insert all share one\n * transaction — and Payload opens that transaction around `create`, so a hook\n * is inside it while an orchestrating caller is not. An earlier version of this\n * ran the same three steps from a service function and granted 3 of 8\n * simultaneous holds for one slot; moved here, it grants exactly 1.\n */\nexport const validateHoldSlot =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, req }) => {\n if (context?.skipReservationHooks || operation !== 'create') {\n return data\n }\n\n const resourceId = extractId(data.resource)\n const serviceId = extractId(data.service)\n\n if (resourceId === undefined || !data.startTime || !data.endTime) {\n return data\n }\n\n // Claim the resource first. Two simultaneous holds — or a hold racing a\n // booking — now collide on this one document and the database serializes\n // them. See acquireBookingLock for the full rationale.\n await req.payload.db.updateOne({\n id: resourceId,\n collection: config.slugs.resources,\n data: { bookingLock: `hold:${String(data.startTime)}` },\n req,\n returning: false,\n })\n\n let bufferBefore = config.defaultBufferTime\n let bufferAfter = config.defaultBufferTime\n\n if (serviceId !== undefined) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: serviceId,\n collection: config.slugs.services,\n depth: 0,\n joins: false,\n req,\n })\n\n if (service) {\n if (config.enforceActive && service.active === false) {\n throw new ValidationError({\n errors: [{ message: 'Service is not active', path: 'service' }],\n })\n }\n bufferBefore = (service.bufferTimeBefore as number) ?? config.defaultBufferTime\n bufferAfter = (service.bufferTimeAfter as number) ?? config.defaultBufferTime\n }\n }\n\n const availability = await checkAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime: new Date(data.endTime as string),\n getExternalBusy: config.getExternalBusy,\n guestCount: (data.guestCount as number) ?? 1,\n holdsSlug: config.slugs.holds,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId,\n resourceSlug: config.slugs.resources,\n servicesSlug: config.slugs.services,\n startTime: new Date(data.startTime as string),\n })\n\n if (!availability.available) {\n throw new ValidationError({\n errors: [\n { message: availability.reason ?? 'Slot is not available', path: 'startTime' },\n ],\n })\n }\n\n return data\n }\n"],"names":["ValidationError","checkAvailability","extractId","validateHoldSlot","config","context","data","operation","req","skipReservationHooks","resourceId","resource","serviceId","service","undefined","startTime","endTime","payload","db","updateOne","id","collection","slugs","resources","bookingLock","String","returning","bufferBefore","defaultBufferTime","bufferAfter","findByID","services","depth","joins","enforceActive","active","errors","message","path","bufferTimeBefore","bufferTimeAfter","availability","blockingStatuses","statusMachine","Date","getExternalBusy","guestCount","holdsSlug","holds","reservationSlug","reservations","resourceSlug","servicesSlug","available","reason"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAIzC,SAASC,iBAAiB,QAAQ,wCAAuC;AACzE,SAASC,SAAS,QAAQ,6CAA4C;AAEtE;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,mBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,GAAG,EAAE;QACtC,IAAIH,SAASI,wBAAwBF,cAAc,UAAU;YAC3D,OAAOD;QACT;QAEA,MAAMI,aAAaR,UAAUI,KAAKK,QAAQ;QAC1C,MAAMC,YAAYV,UAAUI,KAAKO,OAAO;QAExC,IAAIH,eAAeI,aAAa,CAACR,KAAKS,SAAS,IAAI,CAACT,KAAKU,OAAO,EAAE;YAChE,OAAOV;QACT;QAEA,wEAAwE;QACxE,yEAAyE;QACzE,uDAAuD;QACvD,MAAME,IAAIS,OAAO,CAACC,EAAE,CAACC,SAAS,CAAC;YAC7BC,IAAIV;YACJW,YAAYjB,OAAOkB,KAAK,CAACC,SAAS;YAClCjB,MAAM;gBAAEkB,aAAa,CAAC,KAAK,EAAEC,OAAOnB,KAAKS,SAAS,GAAG;YAAC;YACtDP;YACAkB,WAAW;QACb;QAEA,IAAIC,eAAevB,OAAOwB,iBAAiB;QAC3C,IAAIC,cAAczB,OAAOwB,iBAAiB;QAE1C,IAAIhB,cAAcE,WAAW;YAC3B,8DAA8D;YAC9D,MAAMD,UAAU,MAAM,AAACL,IAAIS,OAAO,CAACa,QAAQ,CAAS;gBAClDV,IAAIR;gBACJS,YAAYjB,OAAOkB,KAAK,CAACS,QAAQ;gBACjCC,OAAO;gBACPC,OAAO;gBACPzB;YACF;YAEA,IAAIK,SAAS;gBACX,IAAIT,OAAO8B,aAAa,IAAIrB,QAAQsB,MAAM,KAAK,OAAO;oBACpD,MAAM,IAAInC,gBAAgB;wBACxBoC,QAAQ;4BAAC;gCAAEC,SAAS;gCAAyBC,MAAM;4BAAU;yBAAE;oBACjE;gBACF;gBACAX,eAAe,AAACd,QAAQ0B,gBAAgB,IAAenC,OAAOwB,iBAAiB;gBAC/EC,cAAc,AAAChB,QAAQ2B,eAAe,IAAepC,OAAOwB,iBAAiB;YAC/E;QACF;QAEA,MAAMa,eAAe,MAAMxC,kBAAkB;YAC3CyC,kBAAkBtC,OAAOuC,aAAa,CAACD,gBAAgB;YACvDb;YACAF;YACAX,SAAS,IAAI4B,KAAKtC,KAAKU,OAAO;YAC9B6B,iBAAiBzC,OAAOyC,eAAe;YACvCC,YAAY,AAACxC,KAAKwC,UAAU,IAAe;YAC3CC,WAAW3C,OAAOkB,KAAK,CAAC0B,KAAK;YAC7B/B,SAAST,IAAIS,OAAO;YACpBT;YACAyC,iBAAiB7C,OAAOkB,KAAK,CAAC4B,YAAY;YAC1CxC;YACAyC,cAAc/C,OAAOkB,KAAK,CAACC,SAAS;YACpC6B,cAAchD,OAAOkB,KAAK,CAACS,QAAQ;YACnChB,WAAW,IAAI6B,KAAKtC,KAAKS,SAAS;QACpC;QAEA,IAAI,CAAC0B,aAAaY,SAAS,EAAE;YAC3B,MAAM,IAAIrD,gBAAgB;gBACxBoC,QAAQ;oBACN;wBAAEC,SAASI,aAAaa,MAAM,IAAI;wBAAyBhB,MAAM;oBAAY;iBAC9E;YACH;QACF;QAEA,OAAOhC;IACT,EAAC"}
@@ -0,0 +1,30 @@
1
+ import type { CollectionBeforeChangeHook } from 'payload';
2
+ import type { ResolvedReservationPluginConfig } from '../../types.js';
3
+ /**
4
+ * Serialize concurrent bookings that claim the same resource.
5
+ *
6
+ * `validateConflicts` is a read-then-write: it queries for overlapping
7
+ * reservations, then Payload inserts. Payload runs `create` inside a
8
+ * transaction, but transactional isolation does not help here — snapshot
9
+ * isolation lets N transactions each read "no conflict" and then insert N
10
+ * DIFFERENT documents. A database raises a write conflict only when two
11
+ * transactions touch the SAME document, and reservation rows are distinct.
12
+ * Measured: 10 simultaneous bookings for one `quantity: 1` slot produced 10
13
+ * confirmed reservations.
14
+ *
15
+ * This hook manufactures the missing contention. Before the conflict check
16
+ * reads, it writes a throwaway value to each claimed resource's `bookingLock`
17
+ * field on the booking's own transaction. Two bookings for one resource now
18
+ * collide on that single document, and the database serializes them:
19
+ *
20
+ * - MongoDB fails the loser fast with a WriteConflict; its transaction aborts
21
+ * and nothing is inserted.
22
+ * - Postgres/SQLite block the loser until the winner commits, then let it
23
+ * proceed — where `validateConflicts` now SEES the committed booking and
24
+ * rejects it through the normal path.
25
+ *
26
+ * Either way the outcome is correct. The write goes through the database
27
+ * adapter rather than the Local API deliberately: this must not fire Resources
28
+ * hooks, run its access control, or touch its version history.
29
+ */
30
+ export declare const acquireBookingLock: (config: ResolvedReservationPluginConfig) => CollectionBeforeChangeHook;
@@ -0,0 +1,71 @@
1
+ import { mergeReservationData, schedulingFieldsChanged } from '../../utilities/reservationChanges.js';
2
+ import { resolveReservationItems } from '../../utilities/resolveReservationItems.js';
3
+ /**
4
+ * Serialize concurrent bookings that claim the same resource.
5
+ *
6
+ * `validateConflicts` is a read-then-write: it queries for overlapping
7
+ * reservations, then Payload inserts. Payload runs `create` inside a
8
+ * transaction, but transactional isolation does not help here — snapshot
9
+ * isolation lets N transactions each read "no conflict" and then insert N
10
+ * DIFFERENT documents. A database raises a write conflict only when two
11
+ * transactions touch the SAME document, and reservation rows are distinct.
12
+ * Measured: 10 simultaneous bookings for one `quantity: 1` slot produced 10
13
+ * confirmed reservations.
14
+ *
15
+ * This hook manufactures the missing contention. Before the conflict check
16
+ * reads, it writes a throwaway value to each claimed resource's `bookingLock`
17
+ * field on the booking's own transaction. Two bookings for one resource now
18
+ * collide on that single document, and the database serializes them:
19
+ *
20
+ * - MongoDB fails the loser fast with a WriteConflict; its transaction aborts
21
+ * and nothing is inserted.
22
+ * - Postgres/SQLite block the loser until the winner commits, then let it
23
+ * proceed — where `validateConflicts` now SEES the committed booking and
24
+ * rejects it through the normal path.
25
+ *
26
+ * Either way the outcome is correct. The write goes through the database
27
+ * adapter rather than the Local API deliberately: this must not fire Resources
28
+ * hooks, run its access control, or touch its version history.
29
+ */ export const acquireBookingLock = (config)=>async ({ context, data, operation, originalDoc, req })=>{
30
+ if (context?.skipReservationHooks) {
31
+ return data;
32
+ }
33
+ // Mirror validateConflicts' guard exactly. A benign edit claims no resource
34
+ // time, so it needs no lock — and taking one would resolve items on a row
35
+ // whose items[] may hold a duplicate (resource, startTime) pair that
36
+ // resolveReservationItems rejects, breaking edits that must stay possible.
37
+ const isUpdate = operation === 'update';
38
+ if (isUpdate && !schedulingFieldsChanged({
39
+ blockingStatuses: config.statusMachine.blockingStatuses,
40
+ data: data,
41
+ originalDoc: originalDoc
42
+ })) {
43
+ return data;
44
+ }
45
+ const source = isUpdate ? mergeReservationData(data, originalDoc) : data;
46
+ const items = resolveReservationItems(source);
47
+ if (items.length === 0) {
48
+ return data;
49
+ }
50
+ // Sorted so two bookings claiming the same pair of resources always take
51
+ // them in the same order — otherwise A(r1,r2) and B(r2,r1) could deadlock
52
+ // on a blocking database.
53
+ const resourceIds = [
54
+ ...new Set(items.map((item)=>String(item.resource)))
55
+ ].sort();
56
+ const token = `${Date.now()}:${resourceIds.length}`;
57
+ for (const id of resourceIds){
58
+ await req.payload.db.updateOne({
59
+ id,
60
+ collection: config.slugs.resources,
61
+ data: {
62
+ bookingLock: token
63
+ },
64
+ req,
65
+ returning: false
66
+ });
67
+ }
68
+ return data;
69
+ };
70
+
71
+ //# sourceMappingURL=acquireBookingLock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/hooks/reservations/acquireBookingLock.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport {\n mergeReservationData,\n schedulingFieldsChanged,\n} from '../../utilities/reservationChanges.js'\nimport { resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\n/**\n * Serialize concurrent bookings that claim the same resource.\n *\n * `validateConflicts` is a read-then-write: it queries for overlapping\n * reservations, then Payload inserts. Payload runs `create` inside a\n * transaction, but transactional isolation does not help here — snapshot\n * isolation lets N transactions each read \"no conflict\" and then insert N\n * DIFFERENT documents. A database raises a write conflict only when two\n * transactions touch the SAME document, and reservation rows are distinct.\n * Measured: 10 simultaneous bookings for one `quantity: 1` slot produced 10\n * confirmed reservations.\n *\n * This hook manufactures the missing contention. Before the conflict check\n * reads, it writes a throwaway value to each claimed resource's `bookingLock`\n * field on the booking's own transaction. Two bookings for one resource now\n * collide on that single document, and the database serializes them:\n *\n * - MongoDB fails the loser fast with a WriteConflict; its transaction aborts\n * and nothing is inserted.\n * - Postgres/SQLite block the loser until the winner commits, then let it\n * proceed — where `validateConflicts` now SEES the committed booking and\n * rejects it through the normal path.\n *\n * Either way the outcome is correct. The write goes through the database\n * adapter rather than the Local API deliberately: this must not fire Resources\n * hooks, run its access control, or touch its version history.\n */\nexport const acquireBookingLock =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {\n return data\n }\n\n // Mirror validateConflicts' guard exactly. A benign edit claims no resource\n // time, so it needs no lock — and taking one would resolve items on a row\n // whose items[] may hold a duplicate (resource, startTime) pair that\n // resolveReservationItems rejects, breaking edits that must stay possible.\n const isUpdate = operation === 'update'\n if (\n isUpdate &&\n !schedulingFieldsChanged({\n blockingStatuses: config.statusMachine.blockingStatuses,\n data: data as Record<string, unknown>,\n originalDoc: originalDoc as Record<string, unknown> | undefined,\n })\n ) {\n return data\n }\n\n const source =\n isUpdate\n ? mergeReservationData(\n data as Record<string, unknown>,\n originalDoc as Record<string, unknown> | undefined,\n )\n : (data as Record<string, unknown>)\n\n const items = resolveReservationItems(source)\n if (items.length === 0) {\n return data\n }\n\n // Sorted so two bookings claiming the same pair of resources always take\n // them in the same order — otherwise A(r1,r2) and B(r2,r1) could deadlock\n // on a blocking database.\n const resourceIds = [...new Set(items.map((item) => String(item.resource)))].sort()\n\n const token = `${Date.now()}:${resourceIds.length}`\n\n for (const id of resourceIds) {\n await req.payload.db.updateOne({\n id,\n collection: config.slugs.resources,\n data: { bookingLock: token },\n req,\n returning: false,\n })\n }\n\n return data\n }\n"],"names":["mergeReservationData","schedulingFieldsChanged","resolveReservationItems","acquireBookingLock","config","context","data","operation","originalDoc","req","skipReservationHooks","isUpdate","blockingStatuses","statusMachine","source","items","length","resourceIds","Set","map","item","String","resource","sort","token","Date","now","id","payload","db","updateOne","collection","slugs","resources","bookingLock","returning"],"mappings":"AAIA,SACEA,oBAAoB,EACpBC,uBAAuB,QAClB,wCAAuC;AAC9C,SAASC,uBAAuB,QAAQ,6CAA4C;AAEpF;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,MAAMC,qBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACnD,IAAIJ,SAASK,sBAAsB;YACjC,OAAOJ;QACT;QAEA,4EAA4E;QAC5E,0EAA0E;QAC1E,qEAAqE;QACrE,2EAA2E;QAC3E,MAAMK,WAAWJ,cAAc;QAC/B,IACEI,YACA,CAACV,wBAAwB;YACvBW,kBAAkBR,OAAOS,aAAa,CAACD,gBAAgB;YACvDN,MAAMA;YACNE,aAAaA;QACf,IACA;YACA,OAAOF;QACT;QAEA,MAAMQ,SACJH,WACIX,qBACEM,MACAE,eAEDF;QAEP,MAAMS,QAAQb,wBAAwBY;QACtC,IAAIC,MAAMC,MAAM,KAAK,GAAG;YACtB,OAAOV;QACT;QAEA,yEAAyE;QACzE,0EAA0E;QAC1E,0BAA0B;QAC1B,MAAMW,cAAc;eAAI,IAAIC,IAAIH,MAAMI,GAAG,CAAC,CAACC,OAASC,OAAOD,KAAKE,QAAQ;SAAI,CAACC,IAAI;QAEjF,MAAMC,QAAQ,GAAGC,KAAKC,GAAG,GAAG,CAAC,EAAET,YAAYD,MAAM,EAAE;QAEnD,KAAK,MAAMW,MAAMV,YAAa;YAC5B,MAAMR,IAAImB,OAAO,CAACC,EAAE,CAACC,SAAS,CAAC;gBAC7BH;gBACAI,YAAY3B,OAAO4B,KAAK,CAACC,SAAS;gBAClC3B,MAAM;oBAAE4B,aAAaV;gBAAM;gBAC3Bf;gBACA0B,WAAW;YACb;QACF;QAEA,OAAO7B;IACT,EAAC"}
@@ -22,13 +22,21 @@ export const calculateEndTime = (config)=>async ({ context, data, operation, ori
22
22
  return data;
23
23
  }
24
24
  const items = resolveReservationItems(merged);
25
- if (items.length <= 1) {
25
+ // Branch on REAL items only. A synthesised parent item (B1) must not flip a
26
+ // single-resource booking onto the multi-resource path — that would change
27
+ // which code computes endTime, which this fix deliberately does not touch.
28
+ const realItemCount = items.filter((i)=>!i.fromParent).length;
29
+ if (realItemCount <= 1) {
26
30
  // Single-resource: compute top-level endTime
27
31
  const serviceId = extractId(merged.service);
28
32
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
33
  const service = await req.payload.findByID({
30
34
  id: serviceId,
31
35
  collection: config.slugs.services,
36
+ depth: 0,
37
+ // Skip the resources join — internal logic never reads it, and without this
38
+ // every service read becomes an aggregation with a $lookup.
39
+ joins: false,
32
40
  req
33
41
  });
34
42
  if (!service?.duration && service?.durationType !== 'full-day') {
@@ -94,6 +102,10 @@ export const calculateEndTime = (config)=>async ({ context, data, operation, ori
94
102
  const service = await req.payload.findByID({
95
103
  id: itemServiceId,
96
104
  collection: config.slugs.services,
105
+ depth: 0,
106
+ // Skip the resources join — internal logic never reads it, and without this
107
+ // every service read becomes an aggregation with a $lookup.
108
+ joins: false,
97
109
  req
98
110
  });
99
111
  if (!service?.duration && service?.durationType !== 'full-day') {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/reservations/calculateEndTime.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { DurationType, ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { computeEndTime } from '../../services/AvailabilityService.js'\nimport {\n mergeReservationData,\n schedulingFieldsChanged,\n} from '../../utilities/reservationChanges.js'\nimport { extractId, resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const calculateEndTime =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {\n return data\n }\n\n const isUpdate = operation === 'update'\n\n // Skip when an update touches no scheduling-relevant field — a notes or\n // status edit must not recompute (or invalidate) the stored times.\n if (\n isUpdate &&\n !schedulingFieldsChanged({\n blockingStatuses: config.statusMachine.blockingStatuses,\n data: data as Record<string, unknown>,\n originalDoc: originalDoc as Record<string, unknown> | undefined,\n })\n ) {\n return data\n }\n\n // On update `data` is a partial patch — compute from the merged document.\n const merged = isUpdate\n ? mergeReservationData(\n data as Record<string, unknown>,\n originalDoc as Record<string, unknown> | undefined,\n )\n : (data as Record<string, unknown>)\n\n if (!merged?.startTime || !merged?.service) {\n return data\n }\n\n const items = resolveReservationItems(merged)\n\n if (items.length <= 1) {\n // Single-resource: compute top-level endTime\n const serviceId = extractId(merged.service)\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: serviceId,\n collection: config.slugs.services,\n req,\n })\n\n if (!service?.duration && service?.durationType !== 'full-day') {\n return data\n }\n\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n const startDate = new Date(merged.startTime as string)\n\n if (durationType === 'flexible') {\n if (!merged.endTime) {\n throw new ValidationError({\n errors: [\n { message: 'endTime is required for flexible duration services', path: 'endTime' },\n ],\n })\n }\n // An inverted window would be invisible to overlap queries — reject it\n // (computeEndTime performs no validation for flexible durations).\n if (new Date(merged.endTime as string) <= startDate) {\n throw new ValidationError({\n errors: [{ message: 'endTime must be after startTime', path: 'endTime' }],\n })\n }\n } else {\n const result = computeEndTime({\n durationType,\n serviceDuration: (service.duration as number) ?? 0,\n startTime: startDate,\n timeZone: config.timezone,\n })\n data.endTime = result.endTime.toISOString()\n }\n } else {\n // Multi-resource: recompute only when the patch carries items[]. In\n // practice Payload backfills items from originalDoc on API updates, so\n // this guard mainly protects direct programmatic invocation; the\n // schedulingFieldsChanged gate above is the real skip for benign edits.\n // Rewriting items from a partial patch is A4 territory and out of scope.\n if (isUpdate && !data.items) {\n return data\n }\n\n // Compute endTime per item, then set a top-level endTime that spans all\n // items so conflict detection (which queries top-level startTime/endTime)\n // can see this reservation.\n let earliestStart: Date | undefined\n let latestEnd: Date | undefined\n for (const item of data.items as Array<Record<string, unknown>>) {\n if (!item.startTime) {\n continue\n }\n\n const itemServiceId = extractId(item.service) ?? extractId(merged.service)\n\n if (!itemServiceId) {\n continue\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: itemServiceId,\n collection: config.slugs.services,\n req,\n })\n\n if (!service?.duration && service?.durationType !== 'full-day') {\n continue\n }\n\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n\n if (durationType === 'flexible' && !item.endTime) {\n continue\n }\n\n if (durationType !== 'flexible') {\n const result = computeEndTime({\n durationType,\n serviceDuration: (service.duration as number) ?? 0,\n startTime: new Date(item.startTime as string),\n timeZone: config.timezone,\n })\n item.endTime = result.endTime.toISOString()\n }\n\n const start = new Date(item.startTime as string)\n if (!earliestStart || start < earliestStart) {\n earliestStart = start\n }\n\n if (item.endTime) {\n const end = new Date(item.endTime as string)\n if (!latestEnd || end > latestEnd) {\n latestEnd = end\n }\n }\n }\n\n if (earliestStart) {\n data.startTime = earliestStart.toISOString()\n }\n\n if (latestEnd) {\n data.endTime = latestEnd.toISOString()\n }\n }\n\n return data\n }\n"],"names":["ValidationError","computeEndTime","mergeReservationData","schedulingFieldsChanged","extractId","resolveReservationItems","calculateEndTime","config","context","data","operation","originalDoc","req","skipReservationHooks","isUpdate","blockingStatuses","statusMachine","merged","startTime","service","items","length","serviceId","payload","findByID","id","collection","slugs","services","duration","durationType","startDate","Date","endTime","errors","message","path","result","serviceDuration","timeZone","timezone","toISOString","earliestStart","latestEnd","item","itemServiceId","start","end"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/hooks/reservations/calculateEndTime.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { DurationType, ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { computeEndTime } from '../../services/AvailabilityService.js'\nimport {\n mergeReservationData,\n schedulingFieldsChanged,\n} from '../../utilities/reservationChanges.js'\nimport { extractId, resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const calculateEndTime =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {\n return data\n }\n\n const isUpdate = operation === 'update'\n\n // Skip when an update touches no scheduling-relevant field — a notes or\n // status edit must not recompute (or invalidate) the stored times.\n if (\n isUpdate &&\n !schedulingFieldsChanged({\n blockingStatuses: config.statusMachine.blockingStatuses,\n data: data as Record<string, unknown>,\n originalDoc: originalDoc as Record<string, unknown> | undefined,\n })\n ) {\n return data\n }\n\n // On update `data` is a partial patch — compute from the merged document.\n const merged = isUpdate\n ? mergeReservationData(\n data as Record<string, unknown>,\n originalDoc as Record<string, unknown> | undefined,\n )\n : (data as Record<string, unknown>)\n\n if (!merged?.startTime || !merged?.service) {\n return data\n }\n\n const items = resolveReservationItems(merged)\n\n // Branch on REAL items only. A synthesised parent item (B1) must not flip a\n // single-resource booking onto the multi-resource path — that would change\n // which code computes endTime, which this fix deliberately does not touch.\n const realItemCount = items.filter((i) => !i.fromParent).length\n\n if (realItemCount <= 1) {\n // Single-resource: compute top-level endTime\n const serviceId = extractId(merged.service)\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: serviceId,\n collection: config.slugs.services,\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 (!service?.duration && service?.durationType !== 'full-day') {\n return data\n }\n\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n const startDate = new Date(merged.startTime as string)\n\n if (durationType === 'flexible') {\n if (!merged.endTime) {\n throw new ValidationError({\n errors: [\n { message: 'endTime is required for flexible duration services', path: 'endTime' },\n ],\n })\n }\n // An inverted window would be invisible to overlap queries — reject it\n // (computeEndTime performs no validation for flexible durations).\n if (new Date(merged.endTime as string) <= startDate) {\n throw new ValidationError({\n errors: [{ message: 'endTime must be after startTime', path: 'endTime' }],\n })\n }\n } else {\n const result = computeEndTime({\n durationType,\n serviceDuration: (service.duration as number) ?? 0,\n startTime: startDate,\n timeZone: config.timezone,\n })\n data.endTime = result.endTime.toISOString()\n }\n } else {\n // Multi-resource: recompute only when the patch carries items[]. In\n // practice Payload backfills items from originalDoc on API updates, so\n // this guard mainly protects direct programmatic invocation; the\n // schedulingFieldsChanged gate above is the real skip for benign edits.\n // Rewriting items from a partial patch is A4 territory and out of scope.\n if (isUpdate && !data.items) {\n return data\n }\n\n // Compute endTime per item, then set a top-level endTime that spans all\n // items so conflict detection (which queries top-level startTime/endTime)\n // can see this reservation.\n let earliestStart: Date | undefined\n let latestEnd: Date | undefined\n for (const item of data.items as Array<Record<string, unknown>>) {\n if (!item.startTime) {\n continue\n }\n\n const itemServiceId = extractId(item.service) ?? extractId(merged.service)\n\n if (!itemServiceId) {\n continue\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: itemServiceId,\n collection: config.slugs.services,\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 (!service?.duration && service?.durationType !== 'full-day') {\n continue\n }\n\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n\n if (durationType === 'flexible' && !item.endTime) {\n continue\n }\n\n if (durationType !== 'flexible') {\n const result = computeEndTime({\n durationType,\n serviceDuration: (service.duration as number) ?? 0,\n startTime: new Date(item.startTime as string),\n timeZone: config.timezone,\n })\n item.endTime = result.endTime.toISOString()\n }\n\n const start = new Date(item.startTime as string)\n if (!earliestStart || start < earliestStart) {\n earliestStart = start\n }\n\n if (item.endTime) {\n const end = new Date(item.endTime as string)\n if (!latestEnd || end > latestEnd) {\n latestEnd = end\n }\n }\n }\n\n if (earliestStart) {\n data.startTime = earliestStart.toISOString()\n }\n\n if (latestEnd) {\n data.endTime = latestEnd.toISOString()\n }\n }\n\n return data\n }\n"],"names":["ValidationError","computeEndTime","mergeReservationData","schedulingFieldsChanged","extractId","resolveReservationItems","calculateEndTime","config","context","data","operation","originalDoc","req","skipReservationHooks","isUpdate","blockingStatuses","statusMachine","merged","startTime","service","items","realItemCount","filter","i","fromParent","length","serviceId","payload","findByID","id","collection","slugs","services","depth","joins","duration","durationType","startDate","Date","endTime","errors","message","path","result","serviceDuration","timeZone","timezone","toISOString","earliestStart","latestEnd","item","itemServiceId","start","end"],"mappings":"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"}