payload-reserve 1.6.0 → 2.1.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 (95) hide show
  1. package/README.md +55 -3
  2. package/dist/collections/Reservations.js +19 -7
  3. package/dist/collections/Reservations.js.map +1 -1
  4. package/dist/collections/Resources.js +11 -8
  5. package/dist/collections/Resources.js.map +1 -1
  6. package/dist/collections/Schedules.js +12 -6
  7. package/dist/collections/Schedules.js.map +1 -1
  8. package/dist/collections/Services.js +19 -10
  9. package/dist/collections/Services.js.map +1 -1
  10. package/dist/components/AvailabilityOverview/index.js +76 -18
  11. package/dist/components/AvailabilityOverview/index.js.map +1 -1
  12. package/dist/components/CalendarView/CalendarView.module.css +9 -0
  13. package/dist/components/CalendarView/LaneTimelineView.d.ts +4 -1
  14. package/dist/components/CalendarView/LaneTimelineView.js +17 -12
  15. package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
  16. package/dist/components/CalendarView/index.js +166 -44
  17. package/dist/components/CalendarView/index.js.map +1 -1
  18. package/dist/components/CustomerField/index.js +8 -3
  19. package/dist/components/CustomerField/index.js.map +1 -1
  20. package/dist/components/DashboardWidget/DashboardWidgetServer.js +91 -18
  21. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  22. package/dist/defaults.js +44 -9
  23. package/dist/defaults.js.map +1 -1
  24. package/dist/endpoints/cancelBooking.js +1 -1
  25. package/dist/endpoints/cancelBooking.js.map +1 -1
  26. package/dist/endpoints/checkAvailability.js +56 -7
  27. package/dist/endpoints/checkAvailability.js.map +1 -1
  28. package/dist/endpoints/createBooking.js +19 -10
  29. package/dist/endpoints/createBooking.js.map +1 -1
  30. package/dist/endpoints/customerSearch.js +5 -2
  31. package/dist/endpoints/customerSearch.js.map +1 -1
  32. package/dist/endpoints/effectiveTimezone.d.ts +13 -0
  33. package/dist/endpoints/effectiveTimezone.js +41 -0
  34. package/dist/endpoints/effectiveTimezone.js.map +1 -0
  35. package/dist/endpoints/getSlots.js +56 -7
  36. package/dist/endpoints/getSlots.js.map +1 -1
  37. package/dist/endpoints/resourceAvailability.d.ts +4 -1
  38. package/dist/endpoints/resourceAvailability.js +102 -26
  39. package/dist/endpoints/resourceAvailability.js.map +1 -1
  40. package/dist/hooks/reservations/calculateEndTime.js +48 -20
  41. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  42. package/dist/hooks/reservations/enforceCustomerOwnership.d.ts +11 -0
  43. package/dist/hooks/reservations/enforceCustomerOwnership.js +30 -0
  44. package/dist/hooks/reservations/enforceCustomerOwnership.js.map +1 -0
  45. package/dist/hooks/reservations/onStatusChange.js +10 -4
  46. package/dist/hooks/reservations/onStatusChange.js.map +1 -1
  47. package/dist/hooks/reservations/validateCancellation.js +3 -2
  48. package/dist/hooks/reservations/validateCancellation.js.map +1 -1
  49. package/dist/hooks/reservations/validateConflicts.js +23 -4
  50. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  51. package/dist/hooks/reservations/validateGuestBooking.js +3 -4
  52. package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
  53. package/dist/hooks/reservations/validateStatusTransition.js +2 -2
  54. package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
  55. package/dist/hooks/users/provisionStaffResource.js +5 -8
  56. package/dist/hooks/users/provisionStaffResource.js.map +1 -1
  57. package/dist/plugin.js +83 -14
  58. package/dist/plugin.js.map +1 -1
  59. package/dist/services/AvailabilityService.d.ts +54 -2
  60. package/dist/services/AvailabilityService.js +180 -46
  61. package/dist/services/AvailabilityService.js.map +1 -1
  62. package/dist/translations/ar.json +1 -0
  63. package/dist/translations/de.json +1 -0
  64. package/dist/translations/en.json +1 -0
  65. package/dist/translations/es.json +1 -0
  66. package/dist/translations/fa.json +1 -0
  67. package/dist/translations/fr.json +1 -0
  68. package/dist/translations/hi.json +1 -0
  69. package/dist/translations/id.json +1 -0
  70. package/dist/translations/pl.json +1 -0
  71. package/dist/translations/ru.json +1 -0
  72. package/dist/translations/tr.json +1 -0
  73. package/dist/translations/zh.json +1 -0
  74. package/dist/types.d.ts +46 -1
  75. package/dist/types.js +2 -0
  76. package/dist/types.js.map +1 -1
  77. package/dist/utilities/collectionOverrides.d.ts +14 -0
  78. package/dist/utilities/collectionOverrides.js +47 -0
  79. package/dist/utilities/collectionOverrides.js.map +1 -0
  80. package/dist/utilities/ownerAccess.d.ts +6 -0
  81. package/dist/utilities/ownerAccess.js +25 -12
  82. package/dist/utilities/ownerAccess.js.map +1 -1
  83. package/dist/utilities/reservationChanges.d.ts +17 -0
  84. package/dist/utilities/reservationChanges.js +88 -0
  85. package/dist/utilities/reservationChanges.js.map +1 -0
  86. package/dist/utilities/scheduleUtils.d.ts +14 -8
  87. package/dist/utilities/scheduleUtils.js +26 -19
  88. package/dist/utilities/scheduleUtils.js.map +1 -1
  89. package/dist/utilities/tenantTimezone.d.ts +41 -0
  90. package/dist/utilities/tenantTimezone.js +77 -0
  91. package/dist/utilities/tenantTimezone.js.map +1 -0
  92. package/dist/utilities/timezoneUtils.d.ts +44 -0
  93. package/dist/utilities/timezoneUtils.js +146 -0
  94. package/dist/utilities/timezoneUtils.js.map +1 -0
  95. package/package.json +1 -1
@@ -1,17 +1,30 @@
1
1
  import { ValidationError } from 'payload';
2
2
  import { computeEndTime } from '../../services/AvailabilityService.js';
3
- import { resolveReservationItems } from '../../utilities/resolveReservationItems.js';
4
- export const calculateEndTime = (config)=>async ({ context, data, req })=>{
3
+ import { mergeReservationData, schedulingFieldsChanged } from '../../utilities/reservationChanges.js';
4
+ import { extractId, resolveReservationItems } from '../../utilities/resolveReservationItems.js';
5
+ export const calculateEndTime = (config)=>async ({ context, data, operation, originalDoc, req })=>{
5
6
  if (context?.skipReservationHooks) {
6
7
  return data;
7
8
  }
8
- if (!data?.startTime || !data?.service) {
9
+ const isUpdate = operation === 'update';
10
+ // Skip when an update touches no scheduling-relevant field — a notes or
11
+ // status edit must not recompute (or invalidate) the stored times.
12
+ if (isUpdate && !schedulingFieldsChanged({
13
+ blockingStatuses: config.statusMachine.blockingStatuses,
14
+ data: data,
15
+ originalDoc: originalDoc
16
+ })) {
9
17
  return data;
10
18
  }
11
- const items = resolveReservationItems(data);
19
+ // On update `data` is a partial patch — compute from the merged document.
20
+ const merged = isUpdate ? mergeReservationData(data, originalDoc) : data;
21
+ if (!merged?.startTime || !merged?.service) {
22
+ return data;
23
+ }
24
+ const items = resolveReservationItems(merged);
12
25
  if (items.length <= 1) {
13
26
  // Single-resource: compute top-level endTime
14
- const serviceId = typeof data.service === 'object' ? data.service.id : data.service;
27
+ const serviceId = extractId(merged.service);
15
28
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
29
  const service = await req.payload.findByID({
17
30
  id: serviceId,
@@ -22,9 +35,9 @@ export const calculateEndTime = (config)=>async ({ context, data, req })=>{
22
35
  return data;
23
36
  }
24
37
  const durationType = service.durationType ?? 'fixed';
25
- const startDate = new Date(data.startTime);
38
+ const startDate = new Date(merged.startTime);
26
39
  if (durationType === 'flexible') {
27
- if (!data.endTime) {
40
+ if (!merged.endTime) {
28
41
  throw new ValidationError({
29
42
  errors: [
30
43
  {
@@ -34,32 +47,46 @@ export const calculateEndTime = (config)=>async ({ context, data, req })=>{
34
47
  ]
35
48
  });
36
49
  }
37
- // Validate customer-provided endTime (computeEndTime returns it back)
38
- computeEndTime({
39
- durationType: 'flexible',
40
- endTime: new Date(data.endTime),
41
- serviceDuration: service.duration,
42
- startTime: startDate
43
- });
50
+ // An inverted window would be invisible to overlap queries — reject it
51
+ // (computeEndTime performs no validation for flexible durations).
52
+ if (new Date(merged.endTime) <= startDate) {
53
+ throw new ValidationError({
54
+ errors: [
55
+ {
56
+ message: 'endTime must be after startTime',
57
+ path: 'endTime'
58
+ }
59
+ ]
60
+ });
61
+ }
44
62
  } else {
45
63
  const result = computeEndTime({
46
64
  durationType,
47
65
  serviceDuration: service.duration ?? 0,
48
- startTime: startDate
66
+ startTime: startDate,
67
+ timeZone: config.timezone
49
68
  });
50
69
  data.endTime = result.endTime.toISOString();
51
70
  }
52
71
  } else {
53
- // Multi-resource: compute endTime per item, then set a top-level endTime
54
- // that spans all items so conflict detection (which queries top-level
55
- // startTime/endTime) can see this reservation.
72
+ // Multi-resource: recompute only when the patch carries items[]. In
73
+ // practice Payload backfills items from originalDoc on API updates, so
74
+ // this guard mainly protects direct programmatic invocation; the
75
+ // schedulingFieldsChanged gate above is the real skip for benign edits.
76
+ // Rewriting items from a partial patch is A4 territory and out of scope.
77
+ if (isUpdate && !data.items) {
78
+ return data;
79
+ }
80
+ // Compute endTime per item, then set a top-level endTime that spans all
81
+ // items so conflict detection (which queries top-level startTime/endTime)
82
+ // can see this reservation.
56
83
  let earliestStart;
57
84
  let latestEnd;
58
85
  for (const item of data.items){
59
86
  if (!item.startTime) {
60
87
  continue;
61
88
  }
62
- const itemServiceId = typeof item.service === 'object' ? item.service.id : item.service ?? (typeof data.service === 'object' ? data.service.id : data.service);
89
+ const itemServiceId = extractId(item.service) ?? extractId(merged.service);
63
90
  if (!itemServiceId) {
64
91
  continue;
65
92
  }
@@ -80,7 +107,8 @@ export const calculateEndTime = (config)=>async ({ context, data, req })=>{
80
107
  const result = computeEndTime({
81
108
  durationType,
82
109
  serviceDuration: service.duration ?? 0,
83
- startTime: new Date(item.startTime)
110
+ startTime: new Date(item.startTime),
111
+ timeZone: config.timezone
84
112
  });
85
113
  item.endTime = result.endTime.toISOString();
86
114
  }
@@ -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 { resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const calculateEndTime =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n if (!data?.startTime || !data?.service) {return data}\n\n const items = resolveReservationItems(data)\n\n if (items.length <= 1) {\n // Single-resource: compute top-level endTime\n const serviceId = typeof data.service === 'object' ? data.service.id : data.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') {return data}\n\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n const startDate = new Date(data.startTime)\n\n if (durationType === 'flexible') {\n if (!data.endTime) {\n throw new ValidationError({\n errors: [{ message: 'endTime is required for flexible duration services', path: 'endTime' }],\n })\n }\n // Validate customer-provided endTime (computeEndTime returns it back)\n computeEndTime({\n durationType: 'flexible',\n endTime: new Date(data.endTime),\n serviceDuration: service.duration as number,\n startTime: startDate,\n })\n } else {\n const result = computeEndTime({\n durationType,\n serviceDuration: (service.duration as number) ?? 0,\n startTime: startDate,\n })\n data.endTime = result.endTime.toISOString()\n }\n } else {\n // Multi-resource: compute endTime per item, then set a top-level endTime\n // that spans all items so conflict detection (which queries top-level\n // startTime/endTime) 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) {continue}\n\n const itemServiceId = typeof item.service === 'object'\n ? (item.service as { id: string }).id\n : (item.service as string) ?? (typeof data.service === 'object' ? data.service.id : data.service)\n\n if (!itemServiceId) {continue}\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') {continue}\n\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n\n if (durationType === 'flexible' && !item.endTime) {continue}\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 })\n item.endTime = result.endTime.toISOString()\n }\n\n const start = new Date(item.startTime as string)\n if (!earliestStart || start < earliestStart) {earliestStart = start}\n\n if (item.endTime) {\n const end = new Date(item.endTime as string)\n if (!latestEnd || end > latestEnd) {latestEnd = end}\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","resolveReservationItems","calculateEndTime","config","context","data","req","skipReservationHooks","startTime","service","items","length","serviceId","id","payload","findByID","collection","slugs","services","duration","durationType","startDate","Date","endTime","errors","message","path","serviceDuration","result","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 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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"}
@@ -0,0 +1,11 @@
1
+ import type { CollectionBeforeChangeHook } from 'payload';
2
+ import type { ResolvedReservationPluginConfig } from '../../types.js';
3
+ /**
4
+ * Prevents a non-privileged authenticated user from creating a reservation on
5
+ * behalf of another customer (mass assignment). The `/api/reserve/book` endpoint
6
+ * already enforces this, but a logged-in customer could reach the same write
7
+ * through Payload's default collection REST API — this guard closes that route
8
+ * (review B3 parallel path). Staff/admin may still book for anyone (walk-ins);
9
+ * guest bookings (no `customer`) are untouched.
10
+ */
11
+ export declare const enforceCustomerOwnership: (config: ResolvedReservationPluginConfig) => CollectionBeforeChangeHook;
@@ -0,0 +1,30 @@
1
+ import { extractId } from '../../utilities/resolveReservationItems.js';
2
+ import { isPrivilegedUser } from '../../utilities/userRoles.js';
3
+ /**
4
+ * Prevents a non-privileged authenticated user from creating a reservation on
5
+ * behalf of another customer (mass assignment). The `/api/reserve/book` endpoint
6
+ * already enforces this, but a logged-in customer could reach the same write
7
+ * through Payload's default collection REST API — this guard closes that route
8
+ * (review B3 parallel path). Staff/admin may still book for anyone (walk-ins);
9
+ * guest bookings (no `customer`) are untouched.
10
+ */ export const enforceCustomerOwnership = (config)=>({ context, data, operation, req })=>{
11
+ if (context?.skipReservationHooks) {
12
+ return data;
13
+ }
14
+ if (operation !== 'create' || !req.user) {
15
+ return data;
16
+ }
17
+ if (isPrivilegedUser(req.user, config)) {
18
+ return data;
19
+ }
20
+ const customer = data?.customer;
21
+ if (customer != null && customer !== '') {
22
+ const customerId = extractId(customer);
23
+ if (String(customerId) !== String(req.user.id)) {
24
+ data.customer = req.user.id;
25
+ }
26
+ }
27
+ return data;
28
+ };
29
+
30
+ //# sourceMappingURL=enforceCustomerOwnership.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/hooks/reservations/enforceCustomerOwnership.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { extractId } from '../../utilities/resolveReservationItems.js'\nimport { isPrivilegedUser } from '../../utilities/userRoles.js'\n\n/**\n * Prevents a non-privileged authenticated user from creating a reservation on\n * behalf of another customer (mass assignment). The `/api/reserve/book` endpoint\n * already enforces this, but a logged-in customer could reach the same write\n * through Payload's default collection REST API — this guard closes that route\n * (review B3 parallel path). Staff/admin may still book for anyone (walk-ins);\n * guest bookings (no `customer`) are untouched.\n */\nexport const enforceCustomerOwnership =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n ({ context, data, operation, req }) => {\n if (context?.skipReservationHooks) {\n return data\n }\n if (operation !== 'create' || !req.user) {\n return data\n }\n if (isPrivilegedUser(req.user, config)) {\n return data\n }\n\n const customer = data?.customer\n if (customer != null && customer !== '') {\n const customerId = extractId(customer)\n if (String(customerId) !== String(req.user.id)) {\n data.customer = req.user.id\n }\n }\n\n return data\n }\n"],"names":["extractId","isPrivilegedUser","enforceCustomerOwnership","config","context","data","operation","req","skipReservationHooks","user","customer","customerId","String","id"],"mappings":"AAIA,SAASA,SAAS,QAAQ,6CAA4C;AACtE,SAASC,gBAAgB,QAAQ,+BAA8B;AAE/D;;;;;;;CAOC,GACD,OAAO,MAAMC,2BACX,CAACC,SACD,CAAC,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,GAAG,EAAE;QAChC,IAAIH,SAASI,sBAAsB;YACjC,OAAOH;QACT;QACA,IAAIC,cAAc,YAAY,CAACC,IAAIE,IAAI,EAAE;YACvC,OAAOJ;QACT;QACA,IAAIJ,iBAAiBM,IAAIE,IAAI,EAAEN,SAAS;YACtC,OAAOE;QACT;QAEA,MAAMK,WAAWL,MAAMK;QACvB,IAAIA,YAAY,QAAQA,aAAa,IAAI;YACvC,MAAMC,aAAaX,UAAUU;YAC7B,IAAIE,OAAOD,gBAAgBC,OAAOL,IAAIE,IAAI,CAACI,EAAE,GAAG;gBAC9CR,KAAKK,QAAQ,GAAGH,IAAIE,IAAI,CAACI,EAAE;YAC7B;QACF;QAEA,OAAOR;IACT,EAAC"}
@@ -1,8 +1,14 @@
1
- export const onStatusChange = (config)=>async ({ context, doc, previousDoc, req })=>{
1
+ export const onStatusChange = (config)=>async ({ context, doc, operation, previousDoc, req })=>{
2
2
  if (context?.skipReservationHooks) {
3
3
  return doc;
4
4
  }
5
- if (!previousDoc || previousDoc.status === doc.status) {
5
+ // On create Payload passes previousDoc: {} (not undefined) — there is no
6
+ // previous status, so status-change hooks must not fire (afterBookingCreate
7
+ // covers creation).
8
+ if (operation !== 'update') {
9
+ return doc;
10
+ }
11
+ if (!previousDoc?.status || previousDoc.status === doc.status) {
6
12
  return doc;
7
13
  }
8
14
  const prev = previousDoc.status;
@@ -24,7 +30,7 @@ export const onStatusChange = (config)=>async ({ context, doc, previousDoc, req
24
30
  }
25
31
  }
26
32
  }
27
- if (next === 'confirmed' && config.hooks?.afterBookingConfirm) {
33
+ if (next === config.statusMachine.confirmStatus && config.hooks?.afterBookingConfirm) {
28
34
  for (const hook of config.hooks.afterBookingConfirm){
29
35
  try {
30
36
  await hook({
@@ -39,7 +45,7 @@ export const onStatusChange = (config)=>async ({ context, doc, previousDoc, req
39
45
  }
40
46
  }
41
47
  }
42
- if (next === 'cancelled' && config.hooks?.afterBookingCancel) {
48
+ if (next === config.statusMachine.cancelStatus && config.hooks?.afterBookingCancel) {
43
49
  for (const hook of config.hooks.afterBookingCancel){
44
50
  try {
45
51
  await hook({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/reservations/onStatusChange.ts"],"sourcesContent":["import type { CollectionAfterChangeHook } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nexport const onStatusChange =\n (config: ResolvedReservationPluginConfig): CollectionAfterChangeHook =>\n async ({ context, doc, previousDoc, req }) => {\n if (context?.skipReservationHooks) {return doc}\n if (!previousDoc || previousDoc.status === doc.status) {return doc}\n\n const prev = previousDoc.status as string\n const next = doc.status as string\n\n if (config.hooks?.afterStatusChange) {\n for (const hook of config.hooks.afterStatusChange) {\n try {\n await hook({ doc: doc as Record<string, unknown>, newStatus: next, previousStatus: prev, req })\n } catch (err) {\n req.payload.logger.error({ err, msg: `afterStatusChange hook failed for reservation ${doc.id}` })\n }\n }\n }\n\n if (next === 'confirmed' && config.hooks?.afterBookingConfirm) {\n for (const hook of config.hooks.afterBookingConfirm) {\n try {\n await hook({ doc: doc as Record<string, unknown>, req })\n } catch (err) {\n req.payload.logger.error({ err, msg: `afterBookingConfirm hook failed for reservation ${doc.id}` })\n }\n }\n }\n if (next === 'cancelled' && config.hooks?.afterBookingCancel) {\n for (const hook of config.hooks.afterBookingCancel) {\n try {\n await hook({ doc: doc as Record<string, unknown>, req })\n } catch (err) {\n req.payload.logger.error({ err, msg: `afterBookingCancel hook failed for reservation ${doc.id}` })\n }\n }\n }\n\n return doc\n }\n"],"names":["onStatusChange","config","context","doc","previousDoc","req","skipReservationHooks","status","prev","next","hooks","afterStatusChange","hook","newStatus","previousStatus","err","payload","logger","error","msg","id","afterBookingConfirm","afterBookingCancel"],"mappings":"AAIA,OAAO,MAAMA,iBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,GAAG,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACvC,IAAIH,SAASI,sBAAsB;YAAC,OAAOH;QAAG;QAC9C,IAAI,CAACC,eAAeA,YAAYG,MAAM,KAAKJ,IAAII,MAAM,EAAE;YAAC,OAAOJ;QAAG;QAElE,MAAMK,OAAOJ,YAAYG,MAAM;QAC/B,MAAME,OAAON,IAAII,MAAM;QAEvB,IAAIN,OAAOS,KAAK,EAAEC,mBAAmB;YACnC,KAAK,MAAMC,QAAQX,OAAOS,KAAK,CAACC,iBAAiB,CAAE;gBACjD,IAAI;oBACF,MAAMC,KAAK;wBAAET,KAAKA;wBAAgCU,WAAWJ;wBAAMK,gBAAgBN;wBAAMH;oBAAI;gBAC/F,EAAE,OAAOU,KAAK;oBACZV,IAAIW,OAAO,CAACC,MAAM,CAACC,KAAK,CAAC;wBAAEH;wBAAKI,KAAK,CAAC,8CAA8C,EAAEhB,IAAIiB,EAAE,EAAE;oBAAC;gBACjG;YACF;QACF;QAEA,IAAIX,SAAS,eAAeR,OAAOS,KAAK,EAAEW,qBAAqB;YAC7D,KAAK,MAAMT,QAAQX,OAAOS,KAAK,CAACW,mBAAmB,CAAE;gBACnD,IAAI;oBACF,MAAMT,KAAK;wBAAET,KAAKA;wBAAgCE;oBAAI;gBACxD,EAAE,OAAOU,KAAK;oBACZV,IAAIW,OAAO,CAACC,MAAM,CAACC,KAAK,CAAC;wBAAEH;wBAAKI,KAAK,CAAC,gDAAgD,EAAEhB,IAAIiB,EAAE,EAAE;oBAAC;gBACnG;YACF;QACF;QACA,IAAIX,SAAS,eAAeR,OAAOS,KAAK,EAAEY,oBAAoB;YAC5D,KAAK,MAAMV,QAAQX,OAAOS,KAAK,CAACY,kBAAkB,CAAE;gBAClD,IAAI;oBACF,MAAMV,KAAK;wBAAET,KAAKA;wBAAgCE;oBAAI;gBACxD,EAAE,OAAOU,KAAK;oBACZV,IAAIW,OAAO,CAACC,MAAM,CAACC,KAAK,CAAC;wBAAEH;wBAAKI,KAAK,CAAC,+CAA+C,EAAEhB,IAAIiB,EAAE,EAAE;oBAAC;gBAClG;YACF;QACF;QAEA,OAAOjB;IACT,EAAC"}
1
+ {"version":3,"sources":["../../../src/hooks/reservations/onStatusChange.ts"],"sourcesContent":["import type { CollectionAfterChangeHook } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nexport const onStatusChange =\n (config: ResolvedReservationPluginConfig): CollectionAfterChangeHook =>\n async ({ context, doc, operation, previousDoc, req }) => {\n if (context?.skipReservationHooks) {return doc}\n // On create Payload passes previousDoc: {} (not undefined) — there is no\n // previous status, so status-change hooks must not fire (afterBookingCreate\n // covers creation).\n if (operation !== 'update') {return doc}\n if (!previousDoc?.status || previousDoc.status === doc.status) {return doc}\n\n const prev = previousDoc.status as string\n const next = doc.status as string\n\n if (config.hooks?.afterStatusChange) {\n for (const hook of config.hooks.afterStatusChange) {\n try {\n await hook({ doc: doc as Record<string, unknown>, newStatus: next, previousStatus: prev, req })\n } catch (err) {\n req.payload.logger.error({ err, msg: `afterStatusChange hook failed for reservation ${doc.id}` })\n }\n }\n }\n\n if (next === config.statusMachine.confirmStatus && config.hooks?.afterBookingConfirm) {\n for (const hook of config.hooks.afterBookingConfirm) {\n try {\n await hook({ doc: doc as Record<string, unknown>, req })\n } catch (err) {\n req.payload.logger.error({ err, msg: `afterBookingConfirm hook failed for reservation ${doc.id}` })\n }\n }\n }\n if (next === config.statusMachine.cancelStatus && config.hooks?.afterBookingCancel) {\n for (const hook of config.hooks.afterBookingCancel) {\n try {\n await hook({ doc: doc as Record<string, unknown>, req })\n } catch (err) {\n req.payload.logger.error({ err, msg: `afterBookingCancel hook failed for reservation ${doc.id}` })\n }\n }\n }\n\n return doc\n }\n"],"names":["onStatusChange","config","context","doc","operation","previousDoc","req","skipReservationHooks","status","prev","next","hooks","afterStatusChange","hook","newStatus","previousStatus","err","payload","logger","error","msg","id","statusMachine","confirmStatus","afterBookingConfirm","cancelStatus","afterBookingCancel"],"mappings":"AAIA,OAAO,MAAMA,iBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,GAAG,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QAClD,IAAIJ,SAASK,sBAAsB;YAAC,OAAOJ;QAAG;QAC9C,yEAAyE;QACzE,4EAA4E;QAC5E,oBAAoB;QACpB,IAAIC,cAAc,UAAU;YAAC,OAAOD;QAAG;QACvC,IAAI,CAACE,aAAaG,UAAUH,YAAYG,MAAM,KAAKL,IAAIK,MAAM,EAAE;YAAC,OAAOL;QAAG;QAE1E,MAAMM,OAAOJ,YAAYG,MAAM;QAC/B,MAAME,OAAOP,IAAIK,MAAM;QAEvB,IAAIP,OAAOU,KAAK,EAAEC,mBAAmB;YACnC,KAAK,MAAMC,QAAQZ,OAAOU,KAAK,CAACC,iBAAiB,CAAE;gBACjD,IAAI;oBACF,MAAMC,KAAK;wBAAEV,KAAKA;wBAAgCW,WAAWJ;wBAAMK,gBAAgBN;wBAAMH;oBAAI;gBAC/F,EAAE,OAAOU,KAAK;oBACZV,IAAIW,OAAO,CAACC,MAAM,CAACC,KAAK,CAAC;wBAAEH;wBAAKI,KAAK,CAAC,8CAA8C,EAAEjB,IAAIkB,EAAE,EAAE;oBAAC;gBACjG;YACF;QACF;QAEA,IAAIX,SAAST,OAAOqB,aAAa,CAACC,aAAa,IAAItB,OAAOU,KAAK,EAAEa,qBAAqB;YACpF,KAAK,MAAMX,QAAQZ,OAAOU,KAAK,CAACa,mBAAmB,CAAE;gBACnD,IAAI;oBACF,MAAMX,KAAK;wBAAEV,KAAKA;wBAAgCG;oBAAI;gBACxD,EAAE,OAAOU,KAAK;oBACZV,IAAIW,OAAO,CAACC,MAAM,CAACC,KAAK,CAAC;wBAAEH;wBAAKI,KAAK,CAAC,gDAAgD,EAAEjB,IAAIkB,EAAE,EAAE;oBAAC;gBACnG;YACF;QACF;QACA,IAAIX,SAAST,OAAOqB,aAAa,CAACG,YAAY,IAAIxB,OAAOU,KAAK,EAAEe,oBAAoB;YAClF,KAAK,MAAMb,QAAQZ,OAAOU,KAAK,CAACe,kBAAkB,CAAE;gBAClD,IAAI;oBACF,MAAMb,KAAK;wBAAEV,KAAKA;wBAAgCG;oBAAI;gBACxD,EAAE,OAAOU,KAAK;oBACZV,IAAIW,OAAO,CAACC,MAAM,CAACC,KAAK,CAAC;wBAAEH;wBAAKI,KAAK,CAAC,+CAA+C,EAAEjB,IAAIkB,EAAE,EAAE;oBAAC;gBAClG;YACF;QACF;QAEA,OAAOlB;IACT,EAAC"}
@@ -9,8 +9,9 @@ export const validateCancellation = (config)=>({ context, data, operation, origi
9
9
  }
10
10
  const newStatus = data?.status;
11
11
  const previousStatus = originalDoc?.status;
12
- // Only check when transitioning to cancelled
13
- if (newStatus !== 'cancelled' || previousStatus === 'cancelled') {
12
+ // Only check when transitioning to the cancel status
13
+ const cancelStatus = config.statusMachine.cancelStatus;
14
+ if (newStatus !== cancelStatus || previousStatus === cancelStatus) {
14
15
  return data;
15
16
  }
16
17
  const startTime = data?.startTime ?? originalDoc?.startTime;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/reservations/validateCancellation.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { hoursUntil } from '../../utilities/slotUtils.js'\n\nexport const validateCancellation =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n if (operation !== 'update') {return data}\n\n const newStatus = data?.status\n const previousStatus = originalDoc?.status\n\n // Only check when transitioning to cancelled\n if (newStatus !== 'cancelled' || previousStatus === 'cancelled') {return data}\n\n const startTime = data?.startTime ?? originalDoc?.startTime\n if (!startTime) {return data}\n\n const startDate = new Date(startTime)\n const hours = hoursUntil(startDate)\n\n if (hours > 0 && hours < config.cancellationNoticePeriod) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorCancellationNotice', {\n hours: String(Math.round(hours)),\n period: String(config.cancellationNoticePeriod),\n }),\n path: 'status',\n },\n ],\n })\n }\n\n return data\n }\n"],"names":["ValidationError","hoursUntil","validateCancellation","config","context","data","operation","originalDoc","req","skipReservationHooks","newStatus","status","previousStatus","startTime","startDate","Date","hours","cancellationNoticePeriod","errors","message","t","String","Math","round","period","path"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAKzC,SAASC,UAAU,QAAQ,+BAA8B;AAEzD,OAAO,MAAMC,uBACX,CAACC,SACD,CAAC,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QAC7C,IAAIJ,SAASK,sBAAsB;YAAC,OAAOJ;QAAI;QAE/C,IAAIC,cAAc,UAAU;YAAC,OAAOD;QAAI;QAExC,MAAMK,YAAYL,MAAMM;QACxB,MAAMC,iBAAiBL,aAAaI;QAEpC,6CAA6C;QAC7C,IAAID,cAAc,eAAeE,mBAAmB,aAAa;YAAC,OAAOP;QAAI;QAE7E,MAAMQ,YAAYR,MAAMQ,aAAaN,aAAaM;QAClD,IAAI,CAACA,WAAW;YAAC,OAAOR;QAAI;QAE5B,MAAMS,YAAY,IAAIC,KAAKF;QAC3B,MAAMG,QAAQf,WAAWa;QAEzB,IAAIE,QAAQ,KAAKA,QAAQb,OAAOc,wBAAwB,EAAE;YACxD,MAAM,IAAIjB,gBAAgB;gBACxBkB,QAAQ;oBACN;wBACEC,SAAS,AAACX,IAAIY,CAAC,CAAa,uCAAuC;4BACjEJ,OAAOK,OAAOC,KAAKC,KAAK,CAACP;4BACzBQ,QAAQH,OAAOlB,OAAOc,wBAAwB;wBAChD;wBACAQ,MAAM;oBACR;iBACD;YACH;QACF;QAEA,OAAOpB;IACT,EAAC"}
1
+ {"version":3,"sources":["../../../src/hooks/reservations/validateCancellation.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { hoursUntil } from '../../utilities/slotUtils.js'\n\nexport const validateCancellation =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n if (operation !== 'update') {return data}\n\n const newStatus = data?.status\n const previousStatus = originalDoc?.status\n\n // Only check when transitioning to the cancel status\n const cancelStatus = config.statusMachine.cancelStatus\n if (newStatus !== cancelStatus || previousStatus === cancelStatus) {return data}\n\n const startTime = data?.startTime ?? originalDoc?.startTime\n if (!startTime) {return data}\n\n const startDate = new Date(startTime)\n const hours = hoursUntil(startDate)\n\n if (hours > 0 && hours < config.cancellationNoticePeriod) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorCancellationNotice', {\n hours: String(Math.round(hours)),\n period: String(config.cancellationNoticePeriod),\n }),\n path: 'status',\n },\n ],\n })\n }\n\n return data\n }\n"],"names":["ValidationError","hoursUntil","validateCancellation","config","context","data","operation","originalDoc","req","skipReservationHooks","newStatus","status","previousStatus","cancelStatus","statusMachine","startTime","startDate","Date","hours","cancellationNoticePeriod","errors","message","t","String","Math","round","period","path"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAKzC,SAASC,UAAU,QAAQ,+BAA8B;AAEzD,OAAO,MAAMC,uBACX,CAACC,SACD,CAAC,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QAC7C,IAAIJ,SAASK,sBAAsB;YAAC,OAAOJ;QAAI;QAE/C,IAAIC,cAAc,UAAU;YAAC,OAAOD;QAAI;QAExC,MAAMK,YAAYL,MAAMM;QACxB,MAAMC,iBAAiBL,aAAaI;QAEpC,qDAAqD;QACrD,MAAME,eAAeV,OAAOW,aAAa,CAACD,YAAY;QACtD,IAAIH,cAAcG,gBAAgBD,mBAAmBC,cAAc;YAAC,OAAOR;QAAI;QAE/E,MAAMU,YAAYV,MAAMU,aAAaR,aAAaQ;QAClD,IAAI,CAACA,WAAW;YAAC,OAAOV;QAAI;QAE5B,MAAMW,YAAY,IAAIC,KAAKF;QAC3B,MAAMG,QAAQjB,WAAWe;QAEzB,IAAIE,QAAQ,KAAKA,QAAQf,OAAOgB,wBAAwB,EAAE;YACxD,MAAM,IAAInB,gBAAgB;gBACxBoB,QAAQ;oBACN;wBACEC,SAAS,AAACb,IAAIc,CAAC,CAAa,uCAAuC;4BACjEJ,OAAOK,OAAOC,KAAKC,KAAK,CAACP;4BACzBQ,QAAQH,OAAOpB,OAAOgB,wBAAwB;wBAChD;wBACAQ,MAAM;oBACR;iBACD;YACH;QACF;QAEA,OAAOtB;IACT,EAAC"}
@@ -1,11 +1,25 @@
1
1
  import { ValidationError } from 'payload';
2
2
  import { checkAvailability } from '../../services/AvailabilityService.js';
3
- import { resolveReservationItems } from '../../utilities/resolveReservationItems.js';
3
+ import { mergeReservationData, schedulingFieldsChanged } from '../../utilities/reservationChanges.js';
4
+ import { extractId, resolveReservationItems } from '../../utilities/resolveReservationItems.js';
4
5
  export const validateConflicts = (config)=>async ({ context, data, operation, originalDoc, req })=>{
5
6
  if (context?.skipReservationHooks) {
6
7
  return data;
7
8
  }
8
- const items = resolveReservationItems(data);
9
+ const isUpdate = operation === 'update';
10
+ // Skip when an update touches no scheduling-relevant field, so reservations
11
+ // booked under older buffers/schedules can still take benign edits.
12
+ if (isUpdate && !schedulingFieldsChanged({
13
+ blockingStatuses: config.statusMachine.blockingStatuses,
14
+ data: data,
15
+ originalDoc: originalDoc
16
+ })) {
17
+ return data;
18
+ }
19
+ // Validate the merged document — Payload usually backfills update patches
20
+ // from originalDoc before beforeChange, but the hook must not rely on it.
21
+ const source = isUpdate ? mergeReservationData(data, originalDoc) : data;
22
+ const items = resolveReservationItems(source);
9
23
  if (items.length === 0) {
10
24
  return data;
11
25
  }
@@ -15,7 +29,7 @@ export const validateConflicts = (config)=>async ({ context, data, operation, or
15
29
  continue;
16
30
  }
17
31
  // Fetch buffer times from the item's own service (not just the primary)
18
- const itemServiceId = item.service ?? (typeof data?.service === 'object' ? data.service.id : data?.service);
32
+ const itemServiceId = item.service ?? extractId(source.service);
19
33
  let bufferBefore = config.defaultBufferTime;
20
34
  let bufferAfter = config.defaultBufferTime;
21
35
  if (itemServiceId) {
@@ -34,18 +48,23 @@ export const validateConflicts = (config)=>async ({ context, data, operation, or
34
48
  // Use defaults if service lookup fails
35
49
  }
36
50
  }
51
+ // Other items of this same booking on the same resource count as occupancy
52
+ // so two items can't double-book one resource within one create (review A5).
53
+ const siblingItems = items.filter((other, j)=>j !== i && other.resource === item.resource);
37
54
  const result = await checkAvailability({
38
55
  blockingStatuses: config.statusMachine.blockingStatuses,
39
56
  bufferAfter,
40
57
  bufferBefore,
41
58
  endTime: new Date(item.endTime),
42
- excludeReservationId: operation === 'update' ? originalDoc?.id : undefined,
59
+ excludeReservationId: isUpdate ? originalDoc?.id : undefined,
43
60
  guestCount: item.guestCount,
44
61
  payload: req.payload,
45
62
  req,
46
63
  reservationSlug: config.slugs.reservations,
47
64
  resourceId: item.resource,
48
65
  resourceSlug: config.slugs.resources,
66
+ servicesSlug: config.slugs.services,
67
+ siblingItems,
49
68
  startTime: new Date(item.startTime)
50
69
  });
51
70
  if (!result.available) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/reservations/validateConflicts.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { checkAvailability } from '../../services/AvailabilityService.js'\nimport { resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const validateConflicts =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n const items = resolveReservationItems(data as Record<string, unknown>)\n\n if (items.length === 0) {return data}\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n if (!item.endTime) {continue}\n\n // Fetch buffer times from the item's own service (not just the primary)\n const itemServiceId = item.service\n ?? (typeof data?.service === 'object' ? data.service.id : data?.service)\n let bufferBefore = config.defaultBufferTime\n let bufferAfter = config.defaultBufferTime\n\n if (itemServiceId) {\n try {\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 if (service) {\n bufferBefore = (service.bufferTimeBefore as number) ?? config.defaultBufferTime\n bufferAfter = (service.bufferTimeAfter as number) ?? config.defaultBufferTime\n }\n } catch {\n // Use defaults if service lookup fails\n }\n }\n\n const result = await checkAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime: new Date(item.endTime),\n excludeReservationId: operation === 'update' ? originalDoc?.id : undefined,\n guestCount: item.guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: item.resource,\n resourceSlug: config.slugs.resources,\n startTime: new Date(item.startTime),\n })\n\n if (!result.available) {\n throw new ValidationError({\n errors: [\n {\n message: result.reason ?? (req.t as PluginT)('reservation:errorConflict'),\n path: items.length > 1 ? `items.${i}.startTime` : 'startTime',\n },\n ],\n })\n }\n }\n\n return data\n }\n"],"names":["ValidationError","checkAvailability","resolveReservationItems","validateConflicts","config","context","data","operation","originalDoc","req","skipReservationHooks","items","length","i","item","endTime","itemServiceId","service","id","bufferBefore","defaultBufferTime","bufferAfter","payload","findByID","collection","slugs","services","bufferTimeBefore","bufferTimeAfter","result","blockingStatuses","statusMachine","Date","excludeReservationId","undefined","guestCount","reservationSlug","reservations","resourceId","resource","resourceSlug","resources","startTime","available","errors","message","reason","t","path"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAKzC,SAASC,iBAAiB,QAAQ,wCAAuC;AACzE,SAASC,uBAAuB,QAAQ,6CAA4C;AAEpF,OAAO,MAAMC,oBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACnD,IAAIJ,SAASK,sBAAsB;YAAC,OAAOJ;QAAI;QAE/C,MAAMK,QAAQT,wBAAwBI;QAEtC,IAAIK,MAAMC,MAAM,KAAK,GAAG;YAAC,OAAON;QAAI;QAEpC,IAAK,IAAIO,IAAI,GAAGA,IAAIF,MAAMC,MAAM,EAAEC,IAAK;YACrC,MAAMC,OAAOH,KAAK,CAACE,EAAE;YACrB,IAAI,CAACC,KAAKC,OAAO,EAAE;gBAAC;YAAQ;YAE5B,wEAAwE;YACxE,MAAMC,gBAAgBF,KAAKG,OAAO,IAC5B,CAAA,OAAOX,MAAMW,YAAY,WAAWX,KAAKW,OAAO,CAACC,EAAE,GAAGZ,MAAMW,OAAM;YACxE,IAAIE,eAAef,OAAOgB,iBAAiB;YAC3C,IAAIC,cAAcjB,OAAOgB,iBAAiB;YAE1C,IAAIJ,eAAe;gBACjB,IAAI;oBACF,8DAA8D;oBAC9D,MAAMC,UAAU,MAAM,AAACR,IAAIa,OAAO,CAACC,QAAQ,CAAS;wBAClDL,IAAIF;wBACJQ,YAAYpB,OAAOqB,KAAK,CAACC,QAAQ;wBACjCjB;oBACF;oBACA,IAAIQ,SAAS;wBACXE,eAAe,AAACF,QAAQU,gBAAgB,IAAevB,OAAOgB,iBAAiB;wBAC/EC,cAAc,AAACJ,QAAQW,eAAe,IAAexB,OAAOgB,iBAAiB;oBAC/E;gBACF,EAAE,OAAM;gBACN,uCAAuC;gBACzC;YACF;YAEA,MAAMS,SAAS,MAAM5B,kBAAkB;gBACrC6B,kBAAkB1B,OAAO2B,aAAa,CAACD,gBAAgB;gBACvDT;gBACAF;gBACAJ,SAAS,IAAIiB,KAAKlB,KAAKC,OAAO;gBAC9BkB,sBAAsB1B,cAAc,WAAWC,aAAaU,KAAKgB;gBACjEC,YAAYrB,KAAKqB,UAAU;gBAC3Bb,SAASb,IAAIa,OAAO;gBACpBb;gBACA2B,iBAAiBhC,OAAOqB,KAAK,CAACY,YAAY;gBAC1CC,YAAYxB,KAAKyB,QAAQ;gBACzBC,cAAcpC,OAAOqB,KAAK,CAACgB,SAAS;gBACpCC,WAAW,IAAIV,KAAKlB,KAAK4B,SAAS;YACpC;YAEA,IAAI,CAACb,OAAOc,SAAS,EAAE;gBACrB,MAAM,IAAI3C,gBAAgB;oBACxB4C,QAAQ;wBACN;4BACEC,SAAShB,OAAOiB,MAAM,IAAI,AAACrC,IAAIsC,CAAC,CAAa;4BAC7CC,MAAMrC,MAAMC,MAAM,GAAG,IAAI,CAAC,MAAM,EAAEC,EAAE,UAAU,CAAC,GAAG;wBACpD;qBACD;gBACH;YACF;QACF;QAEA,OAAOP;IACT,EAAC"}
1
+ {"version":3,"sources":["../../../src/hooks/reservations/validateConflicts.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { checkAvailability } from '../../services/AvailabilityService.js'\nimport {\n mergeReservationData,\n schedulingFieldsChanged,\n} from '../../utilities/reservationChanges.js'\nimport { extractId, resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const validateConflicts =\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, so reservations\n // booked under older buffers/schedules can still take benign edits.\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 // Validate the merged document — Payload usually backfills update patches\n // from originalDoc before beforeChange, but the hook must not rely on it.\n const source = 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\n if (items.length === 0) {\n return data\n }\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n if (!item.endTime) {\n continue\n }\n\n // Fetch buffer times from the item's own service (not just the primary)\n const itemServiceId = item.service ?? extractId(source.service)\n let bufferBefore = config.defaultBufferTime\n let bufferAfter = config.defaultBufferTime\n\n if (itemServiceId) {\n try {\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 if (service) {\n bufferBefore = (service.bufferTimeBefore as number) ?? config.defaultBufferTime\n bufferAfter = (service.bufferTimeAfter as number) ?? config.defaultBufferTime\n }\n } catch {\n // Use defaults if service lookup fails\n }\n }\n\n // Other items of this same booking on the same resource count as occupancy\n // so two items can't double-book one resource within one create (review A5).\n const siblingItems = items.filter((other, j) => j !== i && other.resource === item.resource)\n\n const result = await checkAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime: new Date(item.endTime),\n excludeReservationId: isUpdate ? originalDoc?.id : undefined,\n guestCount: item.guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: item.resource,\n resourceSlug: config.slugs.resources,\n servicesSlug: config.slugs.services,\n siblingItems,\n startTime: new Date(item.startTime),\n })\n\n if (!result.available) {\n throw new ValidationError({\n errors: [\n {\n message: result.reason ?? (req.t as PluginT)('reservation:errorConflict'),\n path: items.length > 1 ? `items.${i}.startTime` : 'startTime',\n },\n ],\n })\n }\n }\n\n return data\n }\n"],"names":["ValidationError","checkAvailability","mergeReservationData","schedulingFieldsChanged","extractId","resolveReservationItems","validateConflicts","config","context","data","operation","originalDoc","req","skipReservationHooks","isUpdate","blockingStatuses","statusMachine","source","items","length","i","item","endTime","itemServiceId","service","bufferBefore","defaultBufferTime","bufferAfter","payload","findByID","id","collection","slugs","services","bufferTimeBefore","bufferTimeAfter","siblingItems","filter","other","j","resource","result","Date","excludeReservationId","undefined","guestCount","reservationSlug","reservations","resourceId","resourceSlug","resources","servicesSlug","startTime","available","errors","message","reason","t","path"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAKzC,SAASC,iBAAiB,QAAQ,wCAAuC;AACzE,SACEC,oBAAoB,EACpBC,uBAAuB,QAClB,wCAAuC;AAC9C,SAASC,SAAS,EAAEC,uBAAuB,QAAQ,6CAA4C;AAE/F,OAAO,MAAMC,oBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACnD,IAAIJ,SAASK,sBAAsB;YACjC,OAAOJ;QACT;QAEA,MAAMK,WAAWJ,cAAc;QAE/B,4EAA4E;QAC5E,oEAAoE;QACpE,IACEI,YACA,CAACX,wBAAwB;YACvBY,kBAAkBR,OAAOS,aAAa,CAACD,gBAAgB;YACvDN,MAAMA;YACNE,aAAaA;QACf,IACA;YACA,OAAOF;QACT;QAEA,0EAA0E;QAC1E,0EAA0E;QAC1E,MAAMQ,SAASH,WACXZ,qBACEO,MACAE,eAEDF;QAEL,MAAMS,QAAQb,wBAAwBY;QAEtC,IAAIC,MAAMC,MAAM,KAAK,GAAG;YACtB,OAAOV;QACT;QAEA,IAAK,IAAIW,IAAI,GAAGA,IAAIF,MAAMC,MAAM,EAAEC,IAAK;YACrC,MAAMC,OAAOH,KAAK,CAACE,EAAE;YACrB,IAAI,CAACC,KAAKC,OAAO,EAAE;gBACjB;YACF;YAEA,wEAAwE;YACxE,MAAMC,gBAAgBF,KAAKG,OAAO,IAAIpB,UAAUa,OAAOO,OAAO;YAC9D,IAAIC,eAAelB,OAAOmB,iBAAiB;YAC3C,IAAIC,cAAcpB,OAAOmB,iBAAiB;YAE1C,IAAIH,eAAe;gBACjB,IAAI;oBACF,8DAA8D;oBAC9D,MAAMC,UAAU,MAAM,AAACZ,IAAIgB,OAAO,CAACC,QAAQ,CAAS;wBAClDC,IAAIP;wBACJQ,YAAYxB,OAAOyB,KAAK,CAACC,QAAQ;wBACjCrB;oBACF;oBACA,IAAIY,SAAS;wBACXC,eAAe,AAACD,QAAQU,gBAAgB,IAAe3B,OAAOmB,iBAAiB;wBAC/EC,cAAc,AAACH,QAAQW,eAAe,IAAe5B,OAAOmB,iBAAiB;oBAC/E;gBACF,EAAE,OAAM;gBACN,uCAAuC;gBACzC;YACF;YAEA,2EAA2E;YAC3E,6EAA6E;YAC7E,MAAMU,eAAelB,MAAMmB,MAAM,CAAC,CAACC,OAAOC,IAAMA,MAAMnB,KAAKkB,MAAME,QAAQ,KAAKnB,KAAKmB,QAAQ;YAE3F,MAAMC,SAAS,MAAMxC,kBAAkB;gBACrCc,kBAAkBR,OAAOS,aAAa,CAACD,gBAAgB;gBACvDY;gBACAF;gBACAH,SAAS,IAAIoB,KAAKrB,KAAKC,OAAO;gBAC9BqB,sBAAsB7B,WAAWH,aAAamB,KAAKc;gBACnDC,YAAYxB,KAAKwB,UAAU;gBAC3BjB,SAAShB,IAAIgB,OAAO;gBACpBhB;gBACAkC,iBAAiBvC,OAAOyB,KAAK,CAACe,YAAY;gBAC1CC,YAAY3B,KAAKmB,QAAQ;gBACzBS,cAAc1C,OAAOyB,KAAK,CAACkB,SAAS;gBACpCC,cAAc5C,OAAOyB,KAAK,CAACC,QAAQ;gBACnCG;gBACAgB,WAAW,IAAIV,KAAKrB,KAAK+B,SAAS;YACpC;YAEA,IAAI,CAACX,OAAOY,SAAS,EAAE;gBACrB,MAAM,IAAIrD,gBAAgB;oBACxBsD,QAAQ;wBACN;4BACEC,SAASd,OAAOe,MAAM,IAAI,AAAC5C,IAAI6C,CAAC,CAAa;4BAC7CC,MAAMxC,MAAMC,MAAM,GAAG,IAAI,CAAC,MAAM,EAAEC,EAAE,UAAU,CAAC,GAAG;wBACpD;qBACD;gBACH;YACF;QACF;QAEA,OAAOX;IACT,EAAC"}
@@ -83,10 +83,9 @@ export const validateGuestBooking = (config)=>async ({ context, data, operation,
83
83
  }
84
84
  }
85
85
  }
86
- // Generate a cancellation token the host project can deliver to the guest.
87
- if (!data.cancellationToken) {
88
- data.cancellationToken = randomUUID();
89
- }
86
+ // Always server-generate the cancellation token the host project delivers
87
+ // to the guest — never honor a caller-supplied value (it's a secret).
88
+ data.cancellationToken = randomUUID();
90
89
  return data;
91
90
  };
92
91
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/reservations/validateGuestBooking.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { randomUUID } from 'node:crypto'\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { resolveGuestBookingAllowed } from '../../utilities/guestBooking.js'\n\ntype GuestData = { email?: string; name?: string; phone?: string }\n\nexport const validateGuestBooking =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, req }) => {\n if (context?.skipReservationHooks) {\n return data\n }\n if (operation !== 'create') {\n return data\n }\n\n const customer = data?.customer\n const guest = data?.guest as GuestData | undefined\n const hasCustomer = customer != null && customer !== ''\n const hasGuest =\n guest != null && (Boolean(guest.name) || Boolean(guest.email) || Boolean(guest.phone))\n\n if (!hasCustomer && !hasGuest) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestOrCustomerRequired'),\n path: 'customer',\n },\n ],\n })\n }\n\n if (hasCustomer && hasGuest) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestAndCustomer'),\n path: 'guest',\n },\n ],\n })\n }\n\n if (hasCustomer) {\n return data\n }\n\n // Guest path\n if (!guest?.name) {\n throw new ValidationError({\n errors: [\n { message: (req.t as PluginT)('reservation:errorGuestNameRequired'), path: 'guest.name' },\n ],\n })\n }\n if (!guest.email && !guest.phone) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestContactRequired'),\n path: 'guest.email',\n },\n ],\n })\n }\n\n // Gate by service — admins (non-customer collection users) bypass.\n const isAdmin = req.user != null && req.user.collection !== config.slugs.customers\n if (!isAdmin) {\n const serviceId =\n typeof data.service === 'object'\n ? (data.service as { id?: string } | null)?.id\n : data.service\n // `service` is a required field on the collection, so Payload's field\n // validation (which runs before this beforeChange hook) guarantees it is\n // present for any booking that reaches here. The guard is purely defensive.\n if (serviceId) {\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 req,\n })\n if (!resolveGuestBookingAllowed(service, config.allowGuestBooking)) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestNotAllowed'),\n path: 'guest',\n },\n ],\n })\n }\n }\n }\n\n // Generate a cancellation token the host project can deliver to the guest.\n if (!data.cancellationToken) {\n data.cancellationToken = randomUUID()\n }\n\n return data\n }\n"],"names":["randomUUID","ValidationError","resolveGuestBookingAllowed","validateGuestBooking","config","context","data","operation","req","skipReservationHooks","customer","guest","hasCustomer","hasGuest","Boolean","name","email","phone","errors","message","t","path","isAdmin","user","collection","slugs","customers","serviceId","service","id","payload","findByID","services","depth","allowGuestBooking","cancellationToken"],"mappings":"AAEA,SAASA,UAAU,QAAQ,cAAa;AACxC,SAASC,eAAe,QAAQ,UAAS;AAKzC,SAASC,0BAA0B,QAAQ,kCAAiC;AAI5E,OAAO,MAAMC,uBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,GAAG,EAAE;QACtC,IAAIH,SAASI,sBAAsB;YACjC,OAAOH;QACT;QACA,IAAIC,cAAc,UAAU;YAC1B,OAAOD;QACT;QAEA,MAAMI,WAAWJ,MAAMI;QACvB,MAAMC,QAAQL,MAAMK;QACpB,MAAMC,cAAcF,YAAY,QAAQA,aAAa;QACrD,MAAMG,WACJF,SAAS,QAASG,CAAAA,QAAQH,MAAMI,IAAI,KAAKD,QAAQH,MAAMK,KAAK,KAAKF,QAAQH,MAAMM,KAAK,CAAA;QAEtF,IAAI,CAACL,eAAe,CAACC,UAAU;YAC7B,MAAM,IAAIZ,gBAAgB;gBACxBiB,QAAQ;oBACN;wBACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,IAAIT,eAAeC,UAAU;YAC3B,MAAM,IAAIZ,gBAAgB;gBACxBiB,QAAQ;oBACN;wBACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,IAAIT,aAAa;YACf,OAAON;QACT;QAEA,aAAa;QACb,IAAI,CAACK,OAAOI,MAAM;YAChB,MAAM,IAAId,gBAAgB;gBACxBiB,QAAQ;oBACN;wBAAEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAAuCC,MAAM;oBAAa;iBACzF;YACH;QACF;QACA,IAAI,CAACV,MAAMK,KAAK,IAAI,CAACL,MAAMM,KAAK,EAAE;YAChC,MAAM,IAAIhB,gBAAgB;gBACxBiB,QAAQ;oBACN;wBACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,mEAAmE;QACnE,MAAMC,UAAUd,IAAIe,IAAI,IAAI,QAAQf,IAAIe,IAAI,CAACC,UAAU,KAAKpB,OAAOqB,KAAK,CAACC,SAAS;QAClF,IAAI,CAACJ,SAAS;YACZ,MAAMK,YACJ,OAAOrB,KAAKsB,OAAO,KAAK,WACnBtB,KAAKsB,OAAO,EAA6BC,KAC1CvB,KAAKsB,OAAO;YAClB,sEAAsE;YACtE,yEAAyE;YACzE,4EAA4E;YAC5E,IAAID,WAAW;gBACb,8DAA8D;gBAC9D,MAAMC,UAAU,MAAM,AAACpB,IAAIsB,OAAO,CAACC,QAAQ,CAAS;oBAClDF,IAAIF;oBACJH,YAAYpB,OAAOqB,KAAK,CAACO,QAAQ;oBACjCC,OAAO;oBACPzB;gBACF;gBACA,IAAI,CAACN,2BAA2B0B,SAASxB,OAAO8B,iBAAiB,GAAG;oBAClE,MAAM,IAAIjC,gBAAgB;wBACxBiB,QAAQ;4BACN;gCACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;gCAC5BC,MAAM;4BACR;yBACD;oBACH;gBACF;YACF;QACF;QAEA,2EAA2E;QAC3E,IAAI,CAACf,KAAK6B,iBAAiB,EAAE;YAC3B7B,KAAK6B,iBAAiB,GAAGnC;QAC3B;QAEA,OAAOM;IACT,EAAC"}
1
+ {"version":3,"sources":["../../../src/hooks/reservations/validateGuestBooking.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { randomUUID } from 'node:crypto'\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { resolveGuestBookingAllowed } from '../../utilities/guestBooking.js'\n\ntype GuestData = { email?: string; name?: string; phone?: string }\n\nexport const validateGuestBooking =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, req }) => {\n if (context?.skipReservationHooks) {\n return data\n }\n if (operation !== 'create') {\n return data\n }\n\n const customer = data?.customer\n const guest = data?.guest as GuestData | undefined\n const hasCustomer = customer != null && customer !== ''\n const hasGuest =\n guest != null && (Boolean(guest.name) || Boolean(guest.email) || Boolean(guest.phone))\n\n if (!hasCustomer && !hasGuest) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestOrCustomerRequired'),\n path: 'customer',\n },\n ],\n })\n }\n\n if (hasCustomer && hasGuest) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestAndCustomer'),\n path: 'guest',\n },\n ],\n })\n }\n\n if (hasCustomer) {\n return data\n }\n\n // Guest path\n if (!guest?.name) {\n throw new ValidationError({\n errors: [\n { message: (req.t as PluginT)('reservation:errorGuestNameRequired'), path: 'guest.name' },\n ],\n })\n }\n if (!guest.email && !guest.phone) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestContactRequired'),\n path: 'guest.email',\n },\n ],\n })\n }\n\n // Gate by service — admins (non-customer collection users) bypass.\n const isAdmin = req.user != null && req.user.collection !== config.slugs.customers\n if (!isAdmin) {\n const serviceId =\n typeof data.service === 'object'\n ? (data.service as { id?: string } | null)?.id\n : data.service\n // `service` is a required field on the collection, so Payload's field\n // validation (which runs before this beforeChange hook) guarantees it is\n // present for any booking that reaches here. The guard is purely defensive.\n if (serviceId) {\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 req,\n })\n if (!resolveGuestBookingAllowed(service, config.allowGuestBooking)) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestNotAllowed'),\n path: 'guest',\n },\n ],\n })\n }\n }\n }\n\n // Always server-generate the cancellation token the host project delivers\n // to the guest — never honor a caller-supplied value (it's a secret).\n data.cancellationToken = randomUUID()\n\n return data\n }\n"],"names":["randomUUID","ValidationError","resolveGuestBookingAllowed","validateGuestBooking","config","context","data","operation","req","skipReservationHooks","customer","guest","hasCustomer","hasGuest","Boolean","name","email","phone","errors","message","t","path","isAdmin","user","collection","slugs","customers","serviceId","service","id","payload","findByID","services","depth","allowGuestBooking","cancellationToken"],"mappings":"AAEA,SAASA,UAAU,QAAQ,cAAa;AACxC,SAASC,eAAe,QAAQ,UAAS;AAKzC,SAASC,0BAA0B,QAAQ,kCAAiC;AAI5E,OAAO,MAAMC,uBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,GAAG,EAAE;QACtC,IAAIH,SAASI,sBAAsB;YACjC,OAAOH;QACT;QACA,IAAIC,cAAc,UAAU;YAC1B,OAAOD;QACT;QAEA,MAAMI,WAAWJ,MAAMI;QACvB,MAAMC,QAAQL,MAAMK;QACpB,MAAMC,cAAcF,YAAY,QAAQA,aAAa;QACrD,MAAMG,WACJF,SAAS,QAASG,CAAAA,QAAQH,MAAMI,IAAI,KAAKD,QAAQH,MAAMK,KAAK,KAAKF,QAAQH,MAAMM,KAAK,CAAA;QAEtF,IAAI,CAACL,eAAe,CAACC,UAAU;YAC7B,MAAM,IAAIZ,gBAAgB;gBACxBiB,QAAQ;oBACN;wBACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,IAAIT,eAAeC,UAAU;YAC3B,MAAM,IAAIZ,gBAAgB;gBACxBiB,QAAQ;oBACN;wBACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,IAAIT,aAAa;YACf,OAAON;QACT;QAEA,aAAa;QACb,IAAI,CAACK,OAAOI,MAAM;YAChB,MAAM,IAAId,gBAAgB;gBACxBiB,QAAQ;oBACN;wBAAEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAAuCC,MAAM;oBAAa;iBACzF;YACH;QACF;QACA,IAAI,CAACV,MAAMK,KAAK,IAAI,CAACL,MAAMM,KAAK,EAAE;YAChC,MAAM,IAAIhB,gBAAgB;gBACxBiB,QAAQ;oBACN;wBACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,mEAAmE;QACnE,MAAMC,UAAUd,IAAIe,IAAI,IAAI,QAAQf,IAAIe,IAAI,CAACC,UAAU,KAAKpB,OAAOqB,KAAK,CAACC,SAAS;QAClF,IAAI,CAACJ,SAAS;YACZ,MAAMK,YACJ,OAAOrB,KAAKsB,OAAO,KAAK,WACnBtB,KAAKsB,OAAO,EAA6BC,KAC1CvB,KAAKsB,OAAO;YAClB,sEAAsE;YACtE,yEAAyE;YACzE,4EAA4E;YAC5E,IAAID,WAAW;gBACb,8DAA8D;gBAC9D,MAAMC,UAAU,MAAM,AAACpB,IAAIsB,OAAO,CAACC,QAAQ,CAAS;oBAClDF,IAAIF;oBACJH,YAAYpB,OAAOqB,KAAK,CAACO,QAAQ;oBACjCC,OAAO;oBACPzB;gBACF;gBACA,IAAI,CAACN,2BAA2B0B,SAASxB,OAAO8B,iBAAiB,GAAG;oBAClE,MAAM,IAAIjC,gBAAgB;wBACxBiB,QAAQ;4BACN;gCACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;gCAC5BC,MAAM;4BACR;yBACD;oBACH;gBACF;YACF;QACF;QAEA,0EAA0E;QAC1E,sEAAsE;QACtEf,KAAK6B,iBAAiB,GAAGnC;QAEzB,OAAOM;IACT,EAAC"}
@@ -56,7 +56,7 @@ export const validateStatusTransition = (config)=>async ({ context, data, operat
56
56
  });
57
57
  }
58
58
  // Call beforeBookingConfirm plugin hooks
59
- if (newStatus === 'confirmed' && config.hooks?.beforeBookingConfirm) {
59
+ if (newStatus === config.statusMachine.confirmStatus && config.hooks?.beforeBookingConfirm) {
60
60
  for (const hook of config.hooks.beforeBookingConfirm){
61
61
  await hook({
62
62
  doc: {
@@ -69,7 +69,7 @@ export const validateStatusTransition = (config)=>async ({ context, data, operat
69
69
  }
70
70
  }
71
71
  // Call beforeBookingCancel plugin hooks
72
- if (newStatus === 'cancelled' && config.hooks?.beforeBookingCancel) {
72
+ if (newStatus === config.statusMachine.cancelStatus && config.hooks?.beforeBookingCancel) {
73
73
  for (const hook of config.hooks.beforeBookingCancel){
74
74
  await hook({
75
75
  doc: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/reservations/validateStatusTransition.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { validateTransition } from '../../services/AvailabilityService.js'\nimport { isPrivilegedUser } from '../../utilities/userRoles.js'\n\nexport const validateStatusTransition =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n const newStatus = data?.status as string | undefined\n const { statusMachine } = config\n\n if (operation === 'create') {\n // context.allowConfirmedOnCreate is the escape hatch for payment hooks\n // that need to create confirmed reservations programmatically\n const hasContextBypass = Boolean(context?.allowConfirmedOnCreate)\n // Staff/admin detection: collection-based, with a role-based fallback for\n // single-collection deployments (userCollection set). See isPrivilegedUser.\n const isAdmin = isPrivilegedUser(req.user, config)\n const defaultStatus = statusMachine.defaultStatus\n const nonDefaultStatuses = statusMachine.transitions[defaultStatus] ?? []\n const allowedOnCreate: string[] = (isAdmin || hasContextBypass)\n ? [defaultStatus, ...nonDefaultStatuses]\n : [defaultStatus]\n\n if (newStatus && !allowedOnCreate.includes(newStatus)) {\n const allowed = allowedOnCreate.map((s) => `\"${s}\"`).join(' or ')\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorInvalidCreateStatus', { allowed }),\n path: 'status',\n },\n ],\n })\n }\n\n return data\n }\n\n // On update\n if (operation === 'update' && newStatus) {\n const previousStatus = originalDoc?.status as string | undefined\n\n if (previousStatus && previousStatus !== newStatus) {\n const result = validateTransition(previousStatus, newStatus, statusMachine)\n\n if (!result.valid) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorInvalidTransition', {\n from: previousStatus,\n to: newStatus,\n }),\n path: 'status',\n },\n ],\n })\n }\n\n // Call beforeBookingConfirm plugin hooks\n if (newStatus === 'confirmed' && config.hooks?.beforeBookingConfirm) {\n for (const hook of config.hooks.beforeBookingConfirm) {\n await hook({\n doc: { ...(originalDoc as Record<string, unknown>), ...(data as Record<string, unknown>) },\n newStatus,\n req,\n })\n }\n }\n\n // Call beforeBookingCancel plugin hooks\n if (newStatus === 'cancelled' && config.hooks?.beforeBookingCancel) {\n for (const hook of config.hooks.beforeBookingCancel) {\n await hook({\n doc: { ...(originalDoc as Record<string, unknown>), ...(data as Record<string, unknown>) },\n reason: data?.cancellationReason as string | undefined,\n req,\n })\n }\n }\n }\n }\n\n return data\n }\n"],"names":["ValidationError","validateTransition","isPrivilegedUser","validateStatusTransition","config","context","data","operation","originalDoc","req","skipReservationHooks","newStatus","status","statusMachine","hasContextBypass","Boolean","allowConfirmedOnCreate","isAdmin","user","defaultStatus","nonDefaultStatuses","transitions","allowedOnCreate","includes","allowed","map","s","join","errors","message","t","path","previousStatus","result","valid","from","to","hooks","beforeBookingConfirm","hook","doc","beforeBookingCancel","reason","cancellationReason"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAKzC,SAASC,kBAAkB,QAAQ,wCAAuC;AAC1E,SAASC,gBAAgB,QAAQ,+BAA8B;AAE/D,OAAO,MAAMC,2BACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACnD,IAAIJ,SAASK,sBAAsB;YAAC,OAAOJ;QAAI;QAE/C,MAAMK,YAAYL,MAAMM;QACxB,MAAM,EAAEC,aAAa,EAAE,GAAGT;QAE1B,IAAIG,cAAc,UAAU;YAC1B,uEAAuE;YACvE,8DAA8D;YAC9D,MAAMO,mBAAmBC,QAAQV,SAASW;YAC1C,0EAA0E;YAC1E,4EAA4E;YAC5E,MAAMC,UAAUf,iBAAiBO,IAAIS,IAAI,EAAEd;YAC3C,MAAMe,gBAAgBN,cAAcM,aAAa;YACjD,MAAMC,qBAAqBP,cAAcQ,WAAW,CAACF,cAAc,IAAI,EAAE;YACzE,MAAMG,kBAA4B,AAACL,WAAWH,mBAC1C;gBAACK;mBAAkBC;aAAmB,GACtC;gBAACD;aAAc;YAEnB,IAAIR,aAAa,CAACW,gBAAgBC,QAAQ,CAACZ,YAAY;gBACrD,MAAMa,UAAUF,gBAAgBG,GAAG,CAAC,CAACC,IAAM,CAAC,CAAC,EAAEA,EAAE,CAAC,CAAC,EAAEC,IAAI,CAAC;gBAC1D,MAAM,IAAI3B,gBAAgB;oBACxB4B,QAAQ;wBACN;4BACEC,SAAS,AAACpB,IAAIqB,CAAC,CAAa,wCAAwC;gCAAEN;4BAAQ;4BAC9EO,MAAM;wBACR;qBACD;gBACH;YACF;YAEA,OAAOzB;QACT;QAEA,YAAY;QACZ,IAAIC,cAAc,YAAYI,WAAW;YACvC,MAAMqB,iBAAiBxB,aAAaI;YAEpC,IAAIoB,kBAAkBA,mBAAmBrB,WAAW;gBAClD,MAAMsB,SAAShC,mBAAmB+B,gBAAgBrB,WAAWE;gBAE7D,IAAI,CAACoB,OAAOC,KAAK,EAAE;oBACjB,MAAM,IAAIlC,gBAAgB;wBACxB4B,QAAQ;4BACN;gCACEC,SAAS,AAACpB,IAAIqB,CAAC,CAAa,sCAAsC;oCAChEK,MAAMH;oCACNI,IAAIzB;gCACN;gCACAoB,MAAM;4BACR;yBACD;oBACH;gBACF;gBAEA,yCAAyC;gBACzC,IAAIpB,cAAc,eAAeP,OAAOiC,KAAK,EAAEC,sBAAsB;oBACnE,KAAK,MAAMC,QAAQnC,OAAOiC,KAAK,CAACC,oBAAoB,CAAE;wBACpD,MAAMC,KAAK;4BACTC,KAAK;gCAAE,GAAIhC,WAAW;gCAA8B,GAAIF,IAAI;4BAA6B;4BACzFK;4BACAF;wBACF;oBACF;gBACF;gBAEA,wCAAwC;gBACxC,IAAIE,cAAc,eAAeP,OAAOiC,KAAK,EAAEI,qBAAqB;oBAClE,KAAK,MAAMF,QAAQnC,OAAOiC,KAAK,CAACI,mBAAmB,CAAE;wBACnD,MAAMF,KAAK;4BACTC,KAAK;gCAAE,GAAIhC,WAAW;gCAA8B,GAAIF,IAAI;4BAA6B;4BACzFoC,QAAQpC,MAAMqC;4BACdlC;wBACF;oBACF;gBACF;YACF;QACF;QAEA,OAAOH;IACT,EAAC"}
1
+ {"version":3,"sources":["../../../src/hooks/reservations/validateStatusTransition.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { validateTransition } from '../../services/AvailabilityService.js'\nimport { isPrivilegedUser } from '../../utilities/userRoles.js'\n\nexport const validateStatusTransition =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n const newStatus = data?.status as string | undefined\n const { statusMachine } = config\n\n if (operation === 'create') {\n // context.allowConfirmedOnCreate is the escape hatch for payment hooks\n // that need to create confirmed reservations programmatically\n const hasContextBypass = Boolean(context?.allowConfirmedOnCreate)\n // Staff/admin detection: collection-based, with a role-based fallback for\n // single-collection deployments (userCollection set). See isPrivilegedUser.\n const isAdmin = isPrivilegedUser(req.user, config)\n const defaultStatus = statusMachine.defaultStatus\n const nonDefaultStatuses = statusMachine.transitions[defaultStatus] ?? []\n const allowedOnCreate: string[] = (isAdmin || hasContextBypass)\n ? [defaultStatus, ...nonDefaultStatuses]\n : [defaultStatus]\n\n if (newStatus && !allowedOnCreate.includes(newStatus)) {\n const allowed = allowedOnCreate.map((s) => `\"${s}\"`).join(' or ')\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorInvalidCreateStatus', { allowed }),\n path: 'status',\n },\n ],\n })\n }\n\n return data\n }\n\n // On update\n if (operation === 'update' && newStatus) {\n const previousStatus = originalDoc?.status as string | undefined\n\n if (previousStatus && previousStatus !== newStatus) {\n const result = validateTransition(previousStatus, newStatus, statusMachine)\n\n if (!result.valid) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorInvalidTransition', {\n from: previousStatus,\n to: newStatus,\n }),\n path: 'status',\n },\n ],\n })\n }\n\n // Call beforeBookingConfirm plugin hooks\n if (newStatus === config.statusMachine.confirmStatus && config.hooks?.beforeBookingConfirm) {\n for (const hook of config.hooks.beforeBookingConfirm) {\n await hook({\n doc: { ...(originalDoc as Record<string, unknown>), ...(data as Record<string, unknown>) },\n newStatus,\n req,\n })\n }\n }\n\n // Call beforeBookingCancel plugin hooks\n if (newStatus === config.statusMachine.cancelStatus && config.hooks?.beforeBookingCancel) {\n for (const hook of config.hooks.beforeBookingCancel) {\n await hook({\n doc: { ...(originalDoc as Record<string, unknown>), ...(data as Record<string, unknown>) },\n reason: data?.cancellationReason as string | undefined,\n req,\n })\n }\n }\n }\n }\n\n return data\n }\n"],"names":["ValidationError","validateTransition","isPrivilegedUser","validateStatusTransition","config","context","data","operation","originalDoc","req","skipReservationHooks","newStatus","status","statusMachine","hasContextBypass","Boolean","allowConfirmedOnCreate","isAdmin","user","defaultStatus","nonDefaultStatuses","transitions","allowedOnCreate","includes","allowed","map","s","join","errors","message","t","path","previousStatus","result","valid","from","to","confirmStatus","hooks","beforeBookingConfirm","hook","doc","cancelStatus","beforeBookingCancel","reason","cancellationReason"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAKzC,SAASC,kBAAkB,QAAQ,wCAAuC;AAC1E,SAASC,gBAAgB,QAAQ,+BAA8B;AAE/D,OAAO,MAAMC,2BACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACnD,IAAIJ,SAASK,sBAAsB;YAAC,OAAOJ;QAAI;QAE/C,MAAMK,YAAYL,MAAMM;QACxB,MAAM,EAAEC,aAAa,EAAE,GAAGT;QAE1B,IAAIG,cAAc,UAAU;YAC1B,uEAAuE;YACvE,8DAA8D;YAC9D,MAAMO,mBAAmBC,QAAQV,SAASW;YAC1C,0EAA0E;YAC1E,4EAA4E;YAC5E,MAAMC,UAAUf,iBAAiBO,IAAIS,IAAI,EAAEd;YAC3C,MAAMe,gBAAgBN,cAAcM,aAAa;YACjD,MAAMC,qBAAqBP,cAAcQ,WAAW,CAACF,cAAc,IAAI,EAAE;YACzE,MAAMG,kBAA4B,AAACL,WAAWH,mBAC1C;gBAACK;mBAAkBC;aAAmB,GACtC;gBAACD;aAAc;YAEnB,IAAIR,aAAa,CAACW,gBAAgBC,QAAQ,CAACZ,YAAY;gBACrD,MAAMa,UAAUF,gBAAgBG,GAAG,CAAC,CAACC,IAAM,CAAC,CAAC,EAAEA,EAAE,CAAC,CAAC,EAAEC,IAAI,CAAC;gBAC1D,MAAM,IAAI3B,gBAAgB;oBACxB4B,QAAQ;wBACN;4BACEC,SAAS,AAACpB,IAAIqB,CAAC,CAAa,wCAAwC;gCAAEN;4BAAQ;4BAC9EO,MAAM;wBACR;qBACD;gBACH;YACF;YAEA,OAAOzB;QACT;QAEA,YAAY;QACZ,IAAIC,cAAc,YAAYI,WAAW;YACvC,MAAMqB,iBAAiBxB,aAAaI;YAEpC,IAAIoB,kBAAkBA,mBAAmBrB,WAAW;gBAClD,MAAMsB,SAAShC,mBAAmB+B,gBAAgBrB,WAAWE;gBAE7D,IAAI,CAACoB,OAAOC,KAAK,EAAE;oBACjB,MAAM,IAAIlC,gBAAgB;wBACxB4B,QAAQ;4BACN;gCACEC,SAAS,AAACpB,IAAIqB,CAAC,CAAa,sCAAsC;oCAChEK,MAAMH;oCACNI,IAAIzB;gCACN;gCACAoB,MAAM;4BACR;yBACD;oBACH;gBACF;gBAEA,yCAAyC;gBACzC,IAAIpB,cAAcP,OAAOS,aAAa,CAACwB,aAAa,IAAIjC,OAAOkC,KAAK,EAAEC,sBAAsB;oBAC1F,KAAK,MAAMC,QAAQpC,OAAOkC,KAAK,CAACC,oBAAoB,CAAE;wBACpD,MAAMC,KAAK;4BACTC,KAAK;gCAAE,GAAIjC,WAAW;gCAA8B,GAAIF,IAAI;4BAA6B;4BACzFK;4BACAF;wBACF;oBACF;gBACF;gBAEA,wCAAwC;gBACxC,IAAIE,cAAcP,OAAOS,aAAa,CAAC6B,YAAY,IAAItC,OAAOkC,KAAK,EAAEK,qBAAqB;oBACxF,KAAK,MAAMH,QAAQpC,OAAOkC,KAAK,CAACK,mBAAmB,CAAE;wBACnD,MAAMH,KAAK;4BACTC,KAAK;gCAAE,GAAIjC,WAAW;gCAA8B,GAAIF,IAAI;4BAA6B;4BACzFsC,QAAQtC,MAAMuC;4BACdpC;wBACF;oBACF;gBACF;YACF;QACF;QAEA,OAAOH;IACT,EAAC"}
@@ -13,7 +13,7 @@
13
13
  * whose `user` is the new staff user (spreading the original req preserves
14
14
  * `transactionID` for atomicity). The owner field's force-owner-to-req.user
15
15
  * rule then assigns the correct owner with no bypass flag — nothing to exploit.
16
- */ export const provisionStaffResource = (config)=>async ({ context, doc, operation, previousDoc, req })=>{
16
+ */ export const provisionStaffResource = (config)=>async ({ context, doc, req })=>{
17
17
  if (context?.skipReservationHooks) {
18
18
  return doc;
19
19
  }
@@ -26,13 +26,10 @@
26
26
  if (!isStaffNow) {
27
27
  return doc;
28
28
  }
29
- if (operation === 'update') {
30
- // Skip if the user was already staff provisioned on a prior save.
31
- const wasStaff = roleMatches(previousDoc?.[sp.roleField], sp.staffRoles);
32
- if (wasStaff) {
33
- return doc;
34
- }
35
- }
29
+ // Idempotency is enforced by the dedup-by-owner query below — NOT by an
30
+ // early "was already staff" return. Relying on the query means pre-existing
31
+ // staff (enabled after the fact) and users whose Resource was deleted get
32
+ // (re)provisioned on their next save (review C5).
36
33
  const ownerField = config.resourceOwnerMode?.ownerField ?? 'owner';
37
34
  try {
38
35
  // Idempotency: skip if a resource already owns this user.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/users/provisionStaffResource.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionSlug } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\n/** True if the user's role value (string or string[]) intersects staffRoles. */\nexport function roleMatches(role: unknown, staffRoles: string[]): boolean {\n if (Array.isArray(role)) {\n return role.some((r) => staffRoles.includes(r as string))\n }\n return typeof role === 'string' && staffRoles.includes(role)\n}\n\n/**\n * afterChange hook for the staff user collection. On create — or on an update\n * that promotes a user into a staff role — provisions a paired Resource owned\n * by that user, unless one already exists.\n *\n * Ownership is assigned via IMPERSONATION: the Resource is created with a `req`\n * whose `user` is the new staff user (spreading the original req preserves\n * `transactionID` for atomicity). The owner field's force-owner-to-req.user\n * rule then assigns the correct owner with no bypass flag — nothing to exploit.\n */\nexport const provisionStaffResource =\n (config: ResolvedReservationPluginConfig): CollectionAfterChangeHook =>\n async ({ context, doc, operation, previousDoc, req }) => {\n if (context?.skipReservationHooks) {\n return doc\n }\n\n const sp = config.staffProvisioning\n if (!sp) {\n return doc\n }\n\n const d = doc as Record<string, unknown>\n const isStaffNow = roleMatches(d[sp.roleField], sp.staffRoles)\n if (!isStaffNow) {\n return doc\n }\n\n if (operation === 'update') {\n // Skip if the user was already staff provisioned on a prior save.\n const wasStaff = roleMatches(\n (previousDoc as Record<string, unknown> | undefined)?.[sp.roleField],\n sp.staffRoles,\n )\n if (wasStaff) {\n return doc\n }\n }\n\n const ownerField = config.resourceOwnerMode?.ownerField ?? 'owner'\n\n try {\n // Idempotency: skip if a resource already owns this user.\n const existing = await req.payload.find({\n collection: config.slugs.resources as unknown as CollectionSlug,\n depth: 0,\n limit: 1,\n req,\n where: { [ownerField]: { equals: d.id } },\n })\n if (existing.docs.length > 0) {\n return doc\n }\n\n let data: Record<string, unknown> = {\n name: (d[sp.nameFrom] as string) ?? (d.email as string),\n [ownerField]: d.id,\n quantity: 1,\n resourceType: sp.resourceType,\n }\n\n if (sp.beforeCreate) {\n data = await sp.beforeCreate({ data, req, user: d })\n }\n\n // Impersonate the new staff user so the owner field resolves to them, not\n // the admin who triggered the create. Spread preserves the transaction.\n await req.payload.create({\n collection: config.slugs.resources as unknown as CollectionSlug,\n data,\n req: { ...req, user: { ...d, collection: sp.userCollection } } as typeof req,\n })\n } catch (err) {\n req.payload.logger.error({\n err,\n msg: `provisionStaffResource: failed to provision a resource for user ${String(d.id)}`,\n })\n }\n\n return doc\n }\n"],"names":["roleMatches","role","staffRoles","Array","isArray","some","r","includes","provisionStaffResource","config","context","doc","operation","previousDoc","req","skipReservationHooks","sp","staffProvisioning","d","isStaffNow","roleField","wasStaff","ownerField","resourceOwnerMode","existing","payload","find","collection","slugs","resources","depth","limit","where","equals","id","docs","length","data","name","nameFrom","email","quantity","resourceType","beforeCreate","user","create","userCollection","err","logger","error","msg","String"],"mappings":"AAIA,8EAA8E,GAC9E,OAAO,SAASA,YAAYC,IAAa,EAAEC,UAAoB;IAC7D,IAAIC,MAAMC,OAAO,CAACH,OAAO;QACvB,OAAOA,KAAKI,IAAI,CAAC,CAACC,IAAMJ,WAAWK,QAAQ,CAACD;IAC9C;IACA,OAAO,OAAOL,SAAS,YAAYC,WAAWK,QAAQ,CAACN;AACzD;AAEA;;;;;;;;;CASC,GACD,OAAO,MAAMO,yBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,GAAG,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QAClD,IAAIJ,SAASK,sBAAsB;YACjC,OAAOJ;QACT;QAEA,MAAMK,KAAKP,OAAOQ,iBAAiB;QACnC,IAAI,CAACD,IAAI;YACP,OAAOL;QACT;QAEA,MAAMO,IAAIP;QACV,MAAMQ,aAAanB,YAAYkB,CAAC,CAACF,GAAGI,SAAS,CAAC,EAAEJ,GAAGd,UAAU;QAC7D,IAAI,CAACiB,YAAY;YACf,OAAOR;QACT;QAEA,IAAIC,cAAc,UAAU;YAC1B,oEAAoE;YACpE,MAAMS,WAAWrB,YACda,aAAqD,CAACG,GAAGI,SAAS,CAAC,EACpEJ,GAAGd,UAAU;YAEf,IAAImB,UAAU;gBACZ,OAAOV;YACT;QACF;QAEA,MAAMW,aAAab,OAAOc,iBAAiB,EAAED,cAAc;QAE3D,IAAI;YACF,0DAA0D;YAC1D,MAAME,WAAW,MAAMV,IAAIW,OAAO,CAACC,IAAI,CAAC;gBACtCC,YAAYlB,OAAOmB,KAAK,CAACC,SAAS;gBAClCC,OAAO;gBACPC,OAAO;gBACPjB;gBACAkB,OAAO;oBAAE,CAACV,WAAW,EAAE;wBAAEW,QAAQf,EAAEgB,EAAE;oBAAC;gBAAE;YAC1C;YACA,IAAIV,SAASW,IAAI,CAACC,MAAM,GAAG,GAAG;gBAC5B,OAAOzB;YACT;YAEA,IAAI0B,OAAgC;gBAClCC,MAAM,AAACpB,CAAC,CAACF,GAAGuB,QAAQ,CAAC,IAAgBrB,EAAEsB,KAAK;gBAC5C,CAAClB,WAAW,EAAEJ,EAAEgB,EAAE;gBAClBO,UAAU;gBACVC,cAAc1B,GAAG0B,YAAY;YAC/B;YAEA,IAAI1B,GAAG2B,YAAY,EAAE;gBACnBN,OAAO,MAAMrB,GAAG2B,YAAY,CAAC;oBAAEN;oBAAMvB;oBAAK8B,MAAM1B;gBAAE;YACpD;YAEA,0EAA0E;YAC1E,wEAAwE;YACxE,MAAMJ,IAAIW,OAAO,CAACoB,MAAM,CAAC;gBACvBlB,YAAYlB,OAAOmB,KAAK,CAACC,SAAS;gBAClCQ;gBACAvB,KAAK;oBAAE,GAAGA,GAAG;oBAAE8B,MAAM;wBAAE,GAAG1B,CAAC;wBAAES,YAAYX,GAAG8B,cAAc;oBAAC;gBAAE;YAC/D;QACF,EAAE,OAAOC,KAAK;YACZjC,IAAIW,OAAO,CAACuB,MAAM,CAACC,KAAK,CAAC;gBACvBF;gBACAG,KAAK,CAAC,gEAAgE,EAAEC,OAAOjC,EAAEgB,EAAE,GAAG;YACxF;QACF;QAEA,OAAOvB;IACT,EAAC"}
1
+ {"version":3,"sources":["../../../src/hooks/users/provisionStaffResource.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionSlug } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\n/** True if the user's role value (string or string[]) intersects staffRoles. */\nexport function roleMatches(role: unknown, staffRoles: string[]): boolean {\n if (Array.isArray(role)) {\n return role.some((r) => staffRoles.includes(r as string))\n }\n return typeof role === 'string' && staffRoles.includes(role)\n}\n\n/**\n * afterChange hook for the staff user collection. On create — or on an update\n * that promotes a user into a staff role — provisions a paired Resource owned\n * by that user, unless one already exists.\n *\n * Ownership is assigned via IMPERSONATION: the Resource is created with a `req`\n * whose `user` is the new staff user (spreading the original req preserves\n * `transactionID` for atomicity). The owner field's force-owner-to-req.user\n * rule then assigns the correct owner with no bypass flag — nothing to exploit.\n */\nexport const provisionStaffResource =\n (config: ResolvedReservationPluginConfig): CollectionAfterChangeHook =>\n async ({ context, doc, req }) => {\n if (context?.skipReservationHooks) {\n return doc\n }\n\n const sp = config.staffProvisioning\n if (!sp) {\n return doc\n }\n\n const d = doc as Record<string, unknown>\n const isStaffNow = roleMatches(d[sp.roleField], sp.staffRoles)\n if (!isStaffNow) {\n return doc\n }\n\n // Idempotency is enforced by the dedup-by-owner query below — NOT by an\n // early \"was already staff\" return. Relying on the query means pre-existing\n // staff (enabled after the fact) and users whose Resource was deleted get\n // (re)provisioned on their next save (review C5).\n const ownerField = config.resourceOwnerMode?.ownerField ?? 'owner'\n\n try {\n // Idempotency: skip if a resource already owns this user.\n const existing = await req.payload.find({\n collection: config.slugs.resources as unknown as CollectionSlug,\n depth: 0,\n limit: 1,\n req,\n where: { [ownerField]: { equals: d.id } },\n })\n if (existing.docs.length > 0) {\n return doc\n }\n\n let data: Record<string, unknown> = {\n name: (d[sp.nameFrom] as string) ?? (d.email as string),\n [ownerField]: d.id,\n quantity: 1,\n resourceType: sp.resourceType,\n }\n\n if (sp.beforeCreate) {\n data = await sp.beforeCreate({ data, req, user: d })\n }\n\n // Impersonate the new staff user so the owner field resolves to them, not\n // the admin who triggered the create. Spread preserves the transaction.\n await req.payload.create({\n collection: config.slugs.resources as unknown as CollectionSlug,\n data,\n req: { ...req, user: { ...d, collection: sp.userCollection } } as typeof req,\n })\n } catch (err) {\n req.payload.logger.error({\n err,\n msg: `provisionStaffResource: failed to provision a resource for user ${String(d.id)}`,\n })\n }\n\n return doc\n }\n"],"names":["roleMatches","role","staffRoles","Array","isArray","some","r","includes","provisionStaffResource","config","context","doc","req","skipReservationHooks","sp","staffProvisioning","d","isStaffNow","roleField","ownerField","resourceOwnerMode","existing","payload","find","collection","slugs","resources","depth","limit","where","equals","id","docs","length","data","name","nameFrom","email","quantity","resourceType","beforeCreate","user","create","userCollection","err","logger","error","msg","String"],"mappings":"AAIA,8EAA8E,GAC9E,OAAO,SAASA,YAAYC,IAAa,EAAEC,UAAoB;IAC7D,IAAIC,MAAMC,OAAO,CAACH,OAAO;QACvB,OAAOA,KAAKI,IAAI,CAAC,CAACC,IAAMJ,WAAWK,QAAQ,CAACD;IAC9C;IACA,OAAO,OAAOL,SAAS,YAAYC,WAAWK,QAAQ,CAACN;AACzD;AAEA;;;;;;;;;CASC,GACD,OAAO,MAAMO,yBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,GAAG,EAAEC,GAAG,EAAE;QAC1B,IAAIF,SAASG,sBAAsB;YACjC,OAAOF;QACT;QAEA,MAAMG,KAAKL,OAAOM,iBAAiB;QACnC,IAAI,CAACD,IAAI;YACP,OAAOH;QACT;QAEA,MAAMK,IAAIL;QACV,MAAMM,aAAajB,YAAYgB,CAAC,CAACF,GAAGI,SAAS,CAAC,EAAEJ,GAAGZ,UAAU;QAC7D,IAAI,CAACe,YAAY;YACf,OAAON;QACT;QAEA,wEAAwE;QACxE,4EAA4E;QAC5E,0EAA0E;QAC1E,kDAAkD;QAClD,MAAMQ,aAAaV,OAAOW,iBAAiB,EAAED,cAAc;QAE3D,IAAI;YACF,0DAA0D;YAC1D,MAAME,WAAW,MAAMT,IAAIU,OAAO,CAACC,IAAI,CAAC;gBACtCC,YAAYf,OAAOgB,KAAK,CAACC,SAAS;gBAClCC,OAAO;gBACPC,OAAO;gBACPhB;gBACAiB,OAAO;oBAAE,CAACV,WAAW,EAAE;wBAAEW,QAAQd,EAAEe,EAAE;oBAAC;gBAAE;YAC1C;YACA,IAAIV,SAASW,IAAI,CAACC,MAAM,GAAG,GAAG;gBAC5B,OAAOtB;YACT;YAEA,IAAIuB,OAAgC;gBAClCC,MAAM,AAACnB,CAAC,CAACF,GAAGsB,QAAQ,CAAC,IAAgBpB,EAAEqB,KAAK;gBAC5C,CAAClB,WAAW,EAAEH,EAAEe,EAAE;gBAClBO,UAAU;gBACVC,cAAczB,GAAGyB,YAAY;YAC/B;YAEA,IAAIzB,GAAG0B,YAAY,EAAE;gBACnBN,OAAO,MAAMpB,GAAG0B,YAAY,CAAC;oBAAEN;oBAAMtB;oBAAK6B,MAAMzB;gBAAE;YACpD;YAEA,0EAA0E;YAC1E,wEAAwE;YACxE,MAAMJ,IAAIU,OAAO,CAACoB,MAAM,CAAC;gBACvBlB,YAAYf,OAAOgB,KAAK,CAACC,SAAS;gBAClCQ;gBACAtB,KAAK;oBAAE,GAAGA,GAAG;oBAAE6B,MAAM;wBAAE,GAAGzB,CAAC;wBAAEQ,YAAYV,GAAG6B,cAAc;oBAAC;gBAAE;YAC/D;QACF,EAAE,OAAOC,KAAK;YACZhC,IAAIU,OAAO,CAACuB,MAAM,CAACC,KAAK,CAAC;gBACvBF;gBACAG,KAAK,CAAC,gEAAgE,EAAEC,OAAOhC,EAAEe,EAAE,GAAG;YACxF;QACF;QAEA,OAAOpB;IACT,EAAC"}