payload-reserve 1.5.0 → 2.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 (95) hide show
  1. package/README.md +40 -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 +70 -26
  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 +154 -53
  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 +97 -21
  21. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  22. package/dist/defaults.js +46 -8
  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/getSlots.js +56 -7
  33. package/dist/endpoints/getSlots.js.map +1 -1
  34. package/dist/endpoints/resourceAvailability.d.ts +2 -1
  35. package/dist/endpoints/resourceAvailability.js +85 -25
  36. package/dist/endpoints/resourceAvailability.js.map +1 -1
  37. package/dist/hooks/reservations/calculateEndTime.js +48 -20
  38. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  39. package/dist/hooks/reservations/enforceCustomerOwnership.d.ts +11 -0
  40. package/dist/hooks/reservations/enforceCustomerOwnership.js +30 -0
  41. package/dist/hooks/reservations/enforceCustomerOwnership.js.map +1 -0
  42. package/dist/hooks/reservations/onStatusChange.js +10 -4
  43. package/dist/hooks/reservations/onStatusChange.js.map +1 -1
  44. package/dist/hooks/reservations/validateCancellation.js +3 -2
  45. package/dist/hooks/reservations/validateCancellation.js.map +1 -1
  46. package/dist/hooks/reservations/validateConflicts.js +23 -4
  47. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  48. package/dist/hooks/reservations/validateGuestBooking.js +3 -4
  49. package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
  50. package/dist/hooks/reservations/validateStatusTransition.js +2 -2
  51. package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
  52. package/dist/hooks/users/provisionStaffResource.js +5 -8
  53. package/dist/hooks/users/provisionStaffResource.js.map +1 -1
  54. package/dist/plugin.js +82 -13
  55. package/dist/plugin.js.map +1 -1
  56. package/dist/services/AvailabilityService.d.ts +54 -2
  57. package/dist/services/AvailabilityService.js +180 -46
  58. package/dist/services/AvailabilityService.js.map +1 -1
  59. package/dist/translations/ar.json +1 -0
  60. package/dist/translations/de.json +1 -0
  61. package/dist/translations/en.json +1 -0
  62. package/dist/translations/es.json +1 -0
  63. package/dist/translations/fa.json +1 -0
  64. package/dist/translations/fr.json +1 -0
  65. package/dist/translations/hi.json +1 -0
  66. package/dist/translations/id.json +1 -0
  67. package/dist/translations/pl.json +1 -0
  68. package/dist/translations/ru.json +1 -0
  69. package/dist/translations/tr.json +1 -0
  70. package/dist/translations/zh.json +1 -0
  71. package/dist/types.d.ts +50 -1
  72. package/dist/types.js +2 -0
  73. package/dist/types.js.map +1 -1
  74. package/dist/utilities/collectionOverrides.d.ts +14 -0
  75. package/dist/utilities/collectionOverrides.js +47 -0
  76. package/dist/utilities/collectionOverrides.js.map +1 -0
  77. package/dist/utilities/ownerAccess.d.ts +6 -0
  78. package/dist/utilities/ownerAccess.js +25 -12
  79. package/dist/utilities/ownerAccess.js.map +1 -1
  80. package/dist/utilities/reservationChanges.d.ts +17 -0
  81. package/dist/utilities/reservationChanges.js +88 -0
  82. package/dist/utilities/reservationChanges.js.map +1 -0
  83. package/dist/utilities/scheduleUtils.d.ts +14 -8
  84. package/dist/utilities/scheduleUtils.js +26 -19
  85. package/dist/utilities/scheduleUtils.js.map +1 -1
  86. package/dist/utilities/tenantFilter.d.ts +25 -0
  87. package/dist/utilities/tenantFilter.js +56 -0
  88. package/dist/utilities/tenantFilter.js.map +1 -0
  89. package/dist/utilities/timezoneUtils.d.ts +39 -0
  90. package/dist/utilities/timezoneUtils.js +134 -0
  91. package/dist/utilities/timezoneUtils.js.map +1 -0
  92. package/dist/utilities/useTenantFilter.d.ts +6 -0
  93. package/dist/utilities/useTenantFilter.js +28 -0
  94. package/dist/utilities/useTenantFilter.js.map +1 -0
  95. package/package.json +2 -1
@@ -1,4 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { collectionHasTenantField, readCookie, tenantWhereClause } from '../../utilities/tenantFilter.js';
3
+ import { addDaysToDayKey, combineDayKeyAndTime, getDayKeyInTimezone } from '../../utilities/timezoneUtils.js';
2
4
  import styles from './DashboardWidget.module.css';
3
5
  export const DashboardWidgetServer = async (props)=>{
4
6
  const { req } = props;
@@ -8,35 +10,108 @@ export const DashboardWidgetServer = async (props)=>{
8
10
  if (!slugs) {
9
11
  return null;
10
12
  }
13
+ const tenantConfig = payload.config.admin?.custom?.reservationTenant ?? {};
14
+ const cookieName = tenantConfig.cookieName ?? 'payload-tenant';
15
+ const tenantField = tenantConfig.tenantField ?? 'tenant';
16
+ const reservationsCollection = payload.config.collections?.find((c)=>c.slug === slugs.reservations);
17
+ const tenantWhere = tenantWhereClause({
18
+ hasField: collectionHasTenantField(reservationsCollection, tenantField),
19
+ tenantField,
20
+ tenantId: readCookie(req.headers.get('cookie'), cookieName)
21
+ });
11
22
  // Read status machine from config — never hardcode status values
12
23
  const statusMachine = payload.config.admin?.custom?.reservationStatusMachine;
13
24
  const blockingStatuses = statusMachine?.blockingStatuses ?? [];
14
25
  const terminalStatuses = statusMachine?.terminalStatuses ?? [];
15
26
  const blockingSet = new Set(blockingStatuses);
16
27
  const terminalSet = new Set(terminalStatuses);
28
+ // "Today" is the business timezone's calendar day, not the server's
29
+ const reservationTimezone = payload.config.admin?.custom?.reservationTimezone ?? 'UTC';
17
30
  const now = new Date();
18
- const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
19
- const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
20
- const { docs: todayReservations } = await payload.find({
21
- collection: slugs.reservations,
22
- limit: 100,
23
- sort: 'startTime',
24
- where: {
25
- startTime: {
26
- greater_than_equal: startOfDay.toISOString(),
27
- less_than: endOfDay.toISOString()
28
- }
31
+ const todayKey = getDayKeyInTimezone(now, reservationTimezone);
32
+ const startOfDay = combineDayKeyAndTime(todayKey, '00:00', reservationTimezone);
33
+ const endOfDay = combineDayKeyAndTime(addDaysToDayKey(todayKey, 1), '00:00', reservationTimezone);
34
+ const where = {
35
+ startTime: {
36
+ greater_than_equal: startOfDay.toISOString(),
37
+ less_than: endOfDay.toISOString()
29
38
  }
30
- });
31
- const total = todayReservations.length;
32
- // Active = reservations in blockingStatuses (they hold a slot, past or future)
33
- const active = todayReservations.filter((r)=>blockingSet.has(r.status)).length;
34
- // Upcoming = active (blocking) reservations that haven't started yet
35
- const upcoming = todayReservations.filter((r)=>blockingSet.has(r.status) && new Date(r.startTime) > now).length;
36
- // Terminal = reservations in terminalStatuses (completed, cancelled, no-show, etc.)
37
- const terminal = todayReservations.filter((r)=>terminalSet.has(r.status)).length;
39
+ };
40
+ if (tenantWhere) {
41
+ Object.assign(where, tenantWhere);
42
+ }
43
+ // Stats are computed with count queries rather than a capped fetch+filter,
44
+ // so they stay accurate past 100 reservations/day (review D7).
45
+ const blocking = Array.from(blockingSet);
46
+ const terminalArr = Array.from(terminalSet);
47
+ const countWhere = (extra)=>extra ? {
48
+ and: [
49
+ where,
50
+ extra
51
+ ]
52
+ } : where;
53
+ const [total, active, terminal, upcoming, nextResult] = await Promise.all([
54
+ payload.count({
55
+ collection: slugs.reservations,
56
+ where
57
+ }).then((r)=>r.totalDocs),
58
+ blocking.length ? payload.count({
59
+ collection: slugs.reservations,
60
+ where: countWhere({
61
+ status: {
62
+ in: blocking
63
+ }
64
+ })
65
+ }).then((r)=>r.totalDocs) : Promise.resolve(0),
66
+ terminalArr.length ? payload.count({
67
+ collection: slugs.reservations,
68
+ where: countWhere({
69
+ status: {
70
+ in: terminalArr
71
+ }
72
+ })
73
+ }).then((r)=>r.totalDocs) : Promise.resolve(0),
74
+ blocking.length ? payload.count({
75
+ collection: slugs.reservations,
76
+ where: countWhere({
77
+ and: [
78
+ {
79
+ status: {
80
+ in: blocking
81
+ }
82
+ },
83
+ {
84
+ startTime: {
85
+ greater_than: now.toISOString()
86
+ }
87
+ }
88
+ ]
89
+ })
90
+ }).then((r)=>r.totalDocs) : Promise.resolve(0),
91
+ blocking.length ? payload.find({
92
+ collection: slugs.reservations,
93
+ limit: 1,
94
+ sort: 'startTime',
95
+ where: countWhere({
96
+ and: [
97
+ {
98
+ status: {
99
+ in: blocking
100
+ }
101
+ },
102
+ {
103
+ startTime: {
104
+ greater_than: now.toISOString()
105
+ }
106
+ }
107
+ ]
108
+ })
109
+ }) : Promise.resolve({
110
+ docs: []
111
+ })
112
+ ]);
38
113
  // Next appointment = the earliest upcoming blocking reservation
39
- const nextAppointment = todayReservations.find((r)=>blockingSet.has(r.status) && new Date(r.startTime) > now);
114
+ const nextAppointment = nextResult.docs[0];
40
115
  return /*#__PURE__*/ _jsxs("div", {
41
116
  className: styles.wrapper,
42
117
  children: [
@@ -113,7 +188,8 @@ export const DashboardWidgetServer = async (props)=>{
113
188
  ' ',
114
189
  new Date(nextAppointment.startTime).toLocaleTimeString([], {
115
190
  hour: '2-digit',
116
- minute: '2-digit'
191
+ minute: '2-digit',
192
+ timeZone: reservationTimezone
117
193
  })
118
194
  ]
119
195
  }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/DashboardWidget/DashboardWidgetServer.tsx"],"sourcesContent":["import type { WidgetServerProps } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { StatusMachineConfig } from '../../types.js'\n\nimport styles from './DashboardWidget.module.css'\n\nexport const DashboardWidgetServer = async (props: WidgetServerProps) => {\n const { req } = props\n const { i18n, payload } = req\n const t = i18n.t as PluginT\n\n const slugs = payload.config.admin?.custom?.reservationSlugs\n if (!slugs) {\n return null\n }\n\n // Read status machine from config — never hardcode status values\n const statusMachine: StatusMachineConfig | undefined =\n payload.config.admin?.custom?.reservationStatusMachine\n const blockingStatuses: string[] = statusMachine?.blockingStatuses ?? []\n const terminalStatuses: string[] = statusMachine?.terminalStatuses ?? []\n const blockingSet = new Set(blockingStatuses)\n const terminalSet = new Set(terminalStatuses)\n\n const now = new Date()\n const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1)\n\n const { docs: todayReservations } = await payload.find({\n collection: slugs.reservations,\n limit: 100,\n sort: 'startTime',\n where: {\n startTime: {\n greater_than_equal: startOfDay.toISOString(),\n less_than: endOfDay.toISOString(),\n },\n },\n })\n\n const total = todayReservations.length\n\n // Active = reservations in blockingStatuses (they hold a slot, past or future)\n const active = todayReservations.filter((r: Record<string, unknown>) =>\n blockingSet.has(r.status as string),\n ).length\n\n // Upcoming = active (blocking) reservations that haven't started yet\n const upcoming = todayReservations.filter(\n (r: Record<string, unknown>) =>\n blockingSet.has(r.status as string) && new Date(r.startTime as string) > now,\n ).length\n\n // Terminal = reservations in terminalStatuses (completed, cancelled, no-show, etc.)\n const terminal = todayReservations.filter((r: Record<string, unknown>) =>\n terminalSet.has(r.status as string),\n ).length\n\n // Next appointment = the earliest upcoming blocking reservation\n const nextAppointment = todayReservations.find(\n (r: Record<string, unknown>) =>\n blockingSet.has(r.status as string) && new Date(r.startTime as string) > now,\n )\n\n return (\n <div className={styles.wrapper}>\n <h3 className={styles.title}>{t('reservation:dashboardTitle')}</h3>\n <div className={styles.statsGrid}>\n <div className={styles.statCard}>\n <span className={styles.statValue}>{total}</span>\n <span className={styles.statLabel}>{t('reservation:dashboardTotal')}</span>\n </div>\n <div className={styles.statCard}>\n <span className={styles.statValue}>{active}</span>\n <span className={styles.statLabel}>{t('reservation:dashboardActive')}</span>\n </div>\n <div className={styles.statCard}>\n <span className={styles.statValue}>{upcoming}</span>\n <span className={styles.statLabel}>{t('reservation:dashboardUpcoming')}</span>\n </div>\n <div className={styles.statCard}>\n <span className={styles.statValue}>{terminal}</span>\n <span className={styles.statLabel}>{t('reservation:dashboardTerminal')}</span>\n </div>\n </div>\n {nextAppointment ? (\n <div className={styles.nextAppointment}>\n <strong>{t('reservation:dashboardNextAppointment')}</strong>\n <p>\n {t('reservation:dashboardTime')}{' '}\n {new Date(nextAppointment.startTime as string).toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n })}\n </p>\n <p>\n {t('reservation:dashboardStatus')} {nextAppointment.status as string}\n </p>\n </div>\n ) : (\n <p className={styles.noData}>{t('reservation:dashboardNoUpcoming')}</p>\n )}\n </div>\n )\n}\n"],"names":["styles","DashboardWidgetServer","props","req","i18n","payload","t","slugs","config","admin","custom","reservationSlugs","statusMachine","reservationStatusMachine","blockingStatuses","terminalStatuses","blockingSet","Set","terminalSet","now","Date","startOfDay","getFullYear","getMonth","getDate","endOfDay","docs","todayReservations","find","collection","reservations","limit","sort","where","startTime","greater_than_equal","toISOString","less_than","total","length","active","filter","r","has","status","upcoming","terminal","nextAppointment","div","className","wrapper","h3","title","statsGrid","statCard","span","statValue","statLabel","strong","p","toLocaleTimeString","hour","minute","noData"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/components/DashboardWidget/DashboardWidgetServer.tsx"],"sourcesContent":["import type { Where, WidgetServerProps } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { StatusMachineConfig } from '../../types.js'\n\nimport { collectionHasTenantField, readCookie, tenantWhereClause } from '../../utilities/tenantFilter.js'\nimport {\n addDaysToDayKey,\n combineDayKeyAndTime,\n getDayKeyInTimezone,\n} from '../../utilities/timezoneUtils.js'\nimport styles from './DashboardWidget.module.css'\n\nexport const DashboardWidgetServer = async (props: WidgetServerProps) => {\n const { req } = props\n const { i18n, payload } = req\n const t = i18n.t as PluginT\n\n const slugs = payload.config.admin?.custom?.reservationSlugs\n if (!slugs) {\n return null\n }\n\n const tenantConfig =\n (payload.config.admin?.custom?.reservationTenant as\n | { cookieName?: string; tenantField?: string }\n | undefined) ?? {}\n const cookieName = tenantConfig.cookieName ?? 'payload-tenant'\n const tenantField = tenantConfig.tenantField ?? 'tenant'\n const reservationsCollection = payload.config.collections?.find((c) => c.slug === slugs.reservations)\n const tenantWhere = tenantWhereClause({\n hasField: collectionHasTenantField(reservationsCollection as { fields?: unknown[] } | undefined, tenantField),\n tenantField,\n tenantId: readCookie(req.headers.get('cookie'), cookieName),\n })\n\n // Read status machine from config — never hardcode status values\n const statusMachine: StatusMachineConfig | undefined =\n payload.config.admin?.custom?.reservationStatusMachine\n const blockingStatuses: string[] = statusMachine?.blockingStatuses ?? []\n const terminalStatuses: string[] = statusMachine?.terminalStatuses ?? []\n const blockingSet = new Set(blockingStatuses)\n const terminalSet = new Set(terminalStatuses)\n\n // \"Today\" is the business timezone's calendar day, not the server's\n const reservationTimezone: string =\n payload.config.admin?.custom?.reservationTimezone ?? 'UTC'\n const now = new Date()\n const todayKey = getDayKeyInTimezone(now, reservationTimezone)\n const startOfDay = combineDayKeyAndTime(todayKey, '00:00', reservationTimezone)\n const endOfDay = combineDayKeyAndTime(addDaysToDayKey(todayKey, 1), '00:00', reservationTimezone)\n\n const where: Where = {\n startTime: {\n greater_than_equal: startOfDay.toISOString(),\n less_than: endOfDay.toISOString(),\n },\n }\n if (tenantWhere) {\n Object.assign(where, tenantWhere)\n }\n\n // Stats are computed with count queries rather than a capped fetch+filter,\n // so they stay accurate past 100 reservations/day (review D7).\n const blocking = Array.from(blockingSet)\n const terminalArr = Array.from(terminalSet)\n const countWhere = (extra?: Where): Where => (extra ? { and: [where, extra] } : where)\n\n const [total, active, terminal, upcoming, nextResult] = await Promise.all([\n payload.count({ collection: slugs.reservations, where }).then((r) => r.totalDocs),\n blocking.length\n ? payload\n .count({ collection: slugs.reservations, where: countWhere({ status: { in: blocking } }) })\n .then((r) => r.totalDocs)\n : Promise.resolve(0),\n terminalArr.length\n ? payload\n .count({\n collection: slugs.reservations,\n where: countWhere({ status: { in: terminalArr } }),\n })\n .then((r) => r.totalDocs)\n : Promise.resolve(0),\n blocking.length\n ? payload\n .count({\n collection: slugs.reservations,\n where: countWhere({\n and: [{ status: { in: blocking } }, { startTime: { greater_than: now.toISOString() } }],\n }),\n })\n .then((r) => r.totalDocs)\n : Promise.resolve(0),\n blocking.length\n ? payload.find({\n collection: slugs.reservations,\n limit: 1,\n sort: 'startTime',\n where: countWhere({\n and: [{ status: { in: blocking } }, { startTime: { greater_than: now.toISOString() } }],\n }),\n })\n : Promise.resolve({ docs: [] as Record<string, unknown>[] }),\n ])\n\n // Next appointment = the earliest upcoming blocking reservation\n const nextAppointment = nextResult.docs[0] as Record<string, unknown> | undefined\n\n return (\n <div className={styles.wrapper}>\n <h3 className={styles.title}>{t('reservation:dashboardTitle')}</h3>\n <div className={styles.statsGrid}>\n <div className={styles.statCard}>\n <span className={styles.statValue}>{total}</span>\n <span className={styles.statLabel}>{t('reservation:dashboardTotal')}</span>\n </div>\n <div className={styles.statCard}>\n <span className={styles.statValue}>{active}</span>\n <span className={styles.statLabel}>{t('reservation:dashboardActive')}</span>\n </div>\n <div className={styles.statCard}>\n <span className={styles.statValue}>{upcoming}</span>\n <span className={styles.statLabel}>{t('reservation:dashboardUpcoming')}</span>\n </div>\n <div className={styles.statCard}>\n <span className={styles.statValue}>{terminal}</span>\n <span className={styles.statLabel}>{t('reservation:dashboardTerminal')}</span>\n </div>\n </div>\n {nextAppointment ? (\n <div className={styles.nextAppointment}>\n <strong>{t('reservation:dashboardNextAppointment')}</strong>\n <p>\n {t('reservation:dashboardTime')}{' '}\n {new Date(nextAppointment.startTime as string).toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n timeZone: reservationTimezone,\n })}\n </p>\n <p>\n {t('reservation:dashboardStatus')} {nextAppointment.status as string}\n </p>\n </div>\n ) : (\n <p className={styles.noData}>{t('reservation:dashboardNoUpcoming')}</p>\n )}\n </div>\n )\n}\n"],"names":["collectionHasTenantField","readCookie","tenantWhereClause","addDaysToDayKey","combineDayKeyAndTime","getDayKeyInTimezone","styles","DashboardWidgetServer","props","req","i18n","payload","t","slugs","config","admin","custom","reservationSlugs","tenantConfig","reservationTenant","cookieName","tenantField","reservationsCollection","collections","find","c","slug","reservations","tenantWhere","hasField","tenantId","headers","get","statusMachine","reservationStatusMachine","blockingStatuses","terminalStatuses","blockingSet","Set","terminalSet","reservationTimezone","now","Date","todayKey","startOfDay","endOfDay","where","startTime","greater_than_equal","toISOString","less_than","Object","assign","blocking","Array","from","terminalArr","countWhere","extra","and","total","active","terminal","upcoming","nextResult","Promise","all","count","collection","then","r","totalDocs","length","status","in","resolve","greater_than","limit","sort","docs","nextAppointment","div","className","wrapper","h3","title","statsGrid","statCard","span","statValue","statLabel","strong","p","toLocaleTimeString","hour","minute","timeZone","noData"],"mappings":"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"}
package/dist/defaults.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_STATUS_MACHINE } from './types.js';
2
+ import { validateTimezone } from './utilities/timezoneUtils.js';
2
3
  function validateStatusMachine(sm) {
3
4
  if (!sm.statuses.includes(sm.defaultStatus)) {
4
5
  throw new Error(`statusMachine.defaultStatus "${sm.defaultStatus}" is not in statuses array`);
@@ -23,6 +24,23 @@ function validateStatusMachine(sm) {
23
24
  }
24
25
  }
25
26
  }
27
+ // A terminal status must have no outgoing transitions — otherwise the config
28
+ // contradicts itself (the status is declared terminal but can be left).
29
+ for (const s of sm.terminalStatuses){
30
+ if ((sm.transitions[s]?.length ?? 0) > 0) {
31
+ throw new Error(`statusMachine.terminalStatuses contains "${s}" but transitions["${s}"] is non-empty — a terminal status cannot have outgoing transitions`);
32
+ }
33
+ }
34
+ // A reservation is born in defaultStatus, so it must not be terminal.
35
+ if (sm.terminalStatuses.includes(sm.defaultStatus)) {
36
+ throw new Error(`statusMachine.defaultStatus "${sm.defaultStatus}" cannot be a terminal status`);
37
+ }
38
+ if (!sm.statuses.includes(sm.confirmStatus)) {
39
+ throw new Error(`statusMachine.confirmStatus "${sm.confirmStatus}" is not in statuses array`);
40
+ }
41
+ if (!sm.statuses.includes(sm.cancelStatus)) {
42
+ throw new Error(`statusMachine.cancelStatus "${sm.cancelStatus}" is not in statuses array`);
43
+ }
26
44
  }
27
45
  export const DEFAULT_RESOURCE_TYPES = [
28
46
  'staff',
@@ -77,11 +95,17 @@ export const DEFAULT_ALLOW_GUEST_BOOKING = false;
77
95
  export const DEFAULT_BUFFER_TIME = 0;
78
96
  export const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24;
79
97
  export function resolveConfig(pluginOptions) {
80
- if (pluginOptions.resourceTypes !== undefined && pluginOptions.resourceTypes.length === 0) {
81
- throw new Error('resourceTypes must be a non-empty array');
82
- }
83
- if (pluginOptions.leaveTypes !== undefined && pluginOptions.leaveTypes.length === 0) {
84
- throw new Error('leaveTypes must be a non-empty array');
98
+ // A disabled plugin still resolves (so collections register with the right
99
+ // slugs for schema stability) but skips all config validation — temporarily
100
+ // disabling a misconfigured plugin should not throw at boot (review C3).
101
+ const disabled = pluginOptions.disabled ?? false;
102
+ if (!disabled) {
103
+ if (pluginOptions.resourceTypes !== undefined && pluginOptions.resourceTypes.length === 0) {
104
+ throw new Error('resourceTypes must be a non-empty array');
105
+ }
106
+ if (pluginOptions.leaveTypes !== undefined && pluginOptions.leaveTypes.length === 0) {
107
+ throw new Error('leaveTypes must be a non-empty array');
108
+ }
85
109
  }
86
110
  const resourceTypes = pluginOptions.resourceTypes ?? DEFAULT_RESOURCE_TYPES;
87
111
  const userStatusMachine = pluginOptions.statusMachine;
@@ -91,17 +115,25 @@ export function resolveConfig(pluginOptions) {
91
115
  adminGroup: pluginOptions.adminGroup ?? DEFAULT_ADMIN_GROUP,
92
116
  allowGuestBooking: pluginOptions.allowGuestBooking ?? DEFAULT_ALLOW_GUEST_BOOKING,
93
117
  cancellationNoticePeriod: pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,
118
+ collectionOverrides: pluginOptions.collectionOverrides ?? {},
94
119
  defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,
95
120
  disabled: pluginOptions.disabled ?? false,
96
121
  extraReservationFields: pluginOptions.extraReservationFields ?? [],
122
+ // Real value is set by the plugin once config.collections is known (C8)
123
+ hasMediaCollection: false,
97
124
  hooks: pluginOptions.hooks ?? {},
98
125
  leaveTypes: pluginOptions.leaveTypes ?? DEFAULT_LEAVE_TYPES,
99
126
  localized: false,
127
+ multiTenant: {
128
+ cookieName: pluginOptions.multiTenant?.cookieName ?? 'payload-tenant',
129
+ tenantField: pluginOptions.multiTenant?.tenantField ?? 'tenant'
130
+ },
100
131
  resourceOwnerMode: rom ? {
101
132
  adminRoles: rom.adminRoles ?? [],
102
133
  ownedServices: rom.ownedServices ?? false,
103
134
  ownerCollection: rom.ownerCollection,
104
- ownerField: rom.ownerField ?? 'owner'
135
+ ownerField: rom.ownerField ?? 'owner',
136
+ roleField: rom.roleField ?? pluginOptions.staffProvisioning?.roleField ?? 'role'
105
137
  } : undefined,
106
138
  resourceTypes,
107
139
  slugs: {
@@ -112,9 +144,11 @@ export function resolveConfig(pluginOptions) {
112
144
  schedules: pluginOptions.slugs?.schedules ?? DEFAULT_SLUGS.schedules,
113
145
  services: pluginOptions.slugs?.services ?? DEFAULT_SLUGS.services
114
146
  },
115
- staffProvisioning: resolveStaffProvisioning(pluginOptions, resourceTypes),
147
+ staffProvisioning: disabled ? undefined : resolveStaffProvisioning(pluginOptions, resourceTypes),
116
148
  statusMachine: userStatusMachine ? {
117
149
  blockingStatuses: userStatusMachine.blockingStatuses ?? DEFAULT_STATUS_MACHINE.blockingStatuses,
150
+ cancelStatus: userStatusMachine.cancelStatus ?? DEFAULT_STATUS_MACHINE.cancelStatus,
151
+ confirmStatus: userStatusMachine.confirmStatus ?? DEFAULT_STATUS_MACHINE.confirmStatus,
118
152
  defaultStatus: userStatusMachine.defaultStatus ?? DEFAULT_STATUS_MACHINE.defaultStatus,
119
153
  statuses: userStatusMachine.statuses ?? DEFAULT_STATUS_MACHINE.statuses,
120
154
  terminalStatuses: userStatusMachine.terminalStatuses ?? DEFAULT_STATUS_MACHINE.terminalStatuses,
@@ -122,9 +156,13 @@ export function resolveConfig(pluginOptions) {
122
156
  } : {
123
157
  ...DEFAULT_STATUS_MACHINE
124
158
  },
159
+ timezone: pluginOptions.timezone ?? 'UTC',
125
160
  userCollection: pluginOptions.userCollection ?? undefined
126
161
  };
127
- validateStatusMachine(resolved.statusMachine);
162
+ if (!disabled) {
163
+ validateStatusMachine(resolved.statusMachine);
164
+ validateTimezone(resolved.timezone);
165
+ }
128
166
  return resolved;
129
167
  }
130
168
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type {\n ReservationPluginConfig,\n ResolvedReservationPluginConfig,\n ResolvedStaffProvisioningConfig,\n StatusMachineConfig,\n} from './types.js'\n\nimport { DEFAULT_STATUS_MACHINE } from './types.js'\n\nfunction validateStatusMachine(sm: StatusMachineConfig): void {\n if (!sm.statuses.includes(sm.defaultStatus)) {\n throw new Error(`statusMachine.defaultStatus \"${sm.defaultStatus}\" is not in statuses array`)\n }\n for (const s of sm.blockingStatuses) {\n if (!sm.statuses.includes(s)) {\n throw new Error(`statusMachine.blockingStatuses contains \"${s}\" which is not in statuses array`)\n }\n }\n for (const s of sm.terminalStatuses) {\n if (!sm.statuses.includes(s)) {\n throw new Error(`statusMachine.terminalStatuses contains \"${s}\" which is not in statuses array`)\n }\n }\n for (const [from, targets] of Object.entries(sm.transitions)) {\n if (!sm.statuses.includes(from)) {\n throw new Error(`statusMachine.transitions has key \"${from}\" which is not in statuses array`)\n }\n for (const to of targets) {\n if (!sm.statuses.includes(to)) {\n throw new Error(`statusMachine.transitions[\"${from}\"] targets \"${to}\" which is not in statuses array`)\n }\n }\n }\n}\n\nexport const DEFAULT_RESOURCE_TYPES = ['staff', 'equipment', 'room']\nexport const DEFAULT_LEAVE_TYPES = ['vacation', 'sick', 'personal', 'closure', 'other']\n\nfunction resolveStaffProvisioning(\n pluginOptions: ReservationPluginConfig,\n resourceTypes: string[],\n): ResolvedStaffProvisioningConfig | undefined {\n const sp = pluginOptions.staffProvisioning\n if (!sp) {\n return undefined\n }\n\n if (!pluginOptions.resourceOwnerMode) {\n throw new Error('staffProvisioning requires resourceOwnerMode to be enabled')\n }\n if (sp.staffRoles.length === 0) {\n throw new Error('staffProvisioning.staffRoles must be a non-empty array')\n }\n const resourceType = sp.resourceType ?? 'staff'\n if (!resourceTypes.includes(resourceType)) {\n throw new Error(\n `staffProvisioning.resourceType \"${resourceType}\" is not in resourceTypes [${resourceTypes.join(', ')}]`,\n )\n }\n const userCollection = sp.userCollection ?? pluginOptions.userCollection\n if (!userCollection) {\n throw new Error(\n 'staffProvisioning.userCollection is required when top-level userCollection is unset',\n )\n }\n\n return {\n beforeCreate: sp.beforeCreate,\n nameFrom: sp.nameFrom ?? 'name',\n resourceType,\n roleField: sp.roleField ?? 'role',\n staffRoles: sp.staffRoles,\n userCollection,\n }\n}\n\nexport const DEFAULT_SLUGS = {\n customers: 'customers',\n media: 'media',\n reservations: 'reservations',\n resources: 'resources',\n schedules: 'schedules',\n services: 'services',\n} as const\n\nexport const DEFAULT_ADMIN_GROUP = 'Reservations'\nexport const DEFAULT_ALLOW_GUEST_BOOKING = false\nexport const DEFAULT_BUFFER_TIME = 0\nexport const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24\n\nexport function resolveConfig(\n pluginOptions: ReservationPluginConfig,\n): ResolvedReservationPluginConfig {\n if (pluginOptions.resourceTypes !== undefined && pluginOptions.resourceTypes.length === 0) {\n throw new Error('resourceTypes must be a non-empty array')\n }\n if (pluginOptions.leaveTypes !== undefined && pluginOptions.leaveTypes.length === 0) {\n throw new Error('leaveTypes must be a non-empty array')\n }\n\n const resourceTypes = pluginOptions.resourceTypes ?? DEFAULT_RESOURCE_TYPES\n const userStatusMachine = pluginOptions.statusMachine\n const rom = pluginOptions.resourceOwnerMode\n const resolved: ResolvedReservationPluginConfig = {\n access: pluginOptions.access ?? {},\n adminGroup: pluginOptions.adminGroup ?? DEFAULT_ADMIN_GROUP,\n allowGuestBooking: pluginOptions.allowGuestBooking ?? DEFAULT_ALLOW_GUEST_BOOKING,\n cancellationNoticePeriod:\n pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,\n defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,\n disabled: pluginOptions.disabled ?? false,\n extraReservationFields: pluginOptions.extraReservationFields ?? [],\n hooks: pluginOptions.hooks ?? {},\n leaveTypes: pluginOptions.leaveTypes ?? DEFAULT_LEAVE_TYPES,\n localized: false,\n resourceOwnerMode: rom\n ? {\n adminRoles: rom.adminRoles ?? [],\n ownedServices: rom.ownedServices ?? false,\n ownerCollection: rom.ownerCollection,\n ownerField: rom.ownerField ?? 'owner',\n }\n : undefined,\n resourceTypes,\n slugs: {\n customers: pluginOptions.slugs?.customers ?? DEFAULT_SLUGS.customers,\n media: pluginOptions.slugs?.media ?? DEFAULT_SLUGS.media,\n reservations: pluginOptions.slugs?.reservations ?? DEFAULT_SLUGS.reservations,\n resources: pluginOptions.slugs?.resources ?? DEFAULT_SLUGS.resources,\n schedules: pluginOptions.slugs?.schedules ?? DEFAULT_SLUGS.schedules,\n services: pluginOptions.slugs?.services ?? DEFAULT_SLUGS.services,\n },\n staffProvisioning: resolveStaffProvisioning(pluginOptions, resourceTypes),\n statusMachine: userStatusMachine\n ? {\n blockingStatuses:\n userStatusMachine.blockingStatuses ?? DEFAULT_STATUS_MACHINE.blockingStatuses,\n defaultStatus: userStatusMachine.defaultStatus ?? DEFAULT_STATUS_MACHINE.defaultStatus,\n statuses: userStatusMachine.statuses ?? DEFAULT_STATUS_MACHINE.statuses,\n terminalStatuses:\n userStatusMachine.terminalStatuses ?? DEFAULT_STATUS_MACHINE.terminalStatuses,\n transitions: userStatusMachine.transitions ?? DEFAULT_STATUS_MACHINE.transitions,\n }\n : { ...DEFAULT_STATUS_MACHINE },\n userCollection: pluginOptions.userCollection ?? undefined,\n }\n\n validateStatusMachine(resolved.statusMachine)\n\n return resolved\n}\n"],"names":["DEFAULT_STATUS_MACHINE","validateStatusMachine","sm","statuses","includes","defaultStatus","Error","s","blockingStatuses","terminalStatuses","from","targets","Object","entries","transitions","to","DEFAULT_RESOURCE_TYPES","DEFAULT_LEAVE_TYPES","resolveStaffProvisioning","pluginOptions","resourceTypes","sp","staffProvisioning","undefined","resourceOwnerMode","staffRoles","length","resourceType","join","userCollection","beforeCreate","nameFrom","roleField","DEFAULT_SLUGS","customers","media","reservations","resources","schedules","services","DEFAULT_ADMIN_GROUP","DEFAULT_ALLOW_GUEST_BOOKING","DEFAULT_BUFFER_TIME","DEFAULT_CANCELLATION_NOTICE_PERIOD","resolveConfig","leaveTypes","userStatusMachine","statusMachine","rom","resolved","access","adminGroup","allowGuestBooking","cancellationNoticePeriod","defaultBufferTime","disabled","extraReservationFields","hooks","localized","adminRoles","ownedServices","ownerCollection","ownerField","slugs"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type {\n ReservationPluginConfig,\n ResolvedReservationPluginConfig,\n ResolvedStaffProvisioningConfig,\n StatusMachineConfig,\n} from './types.js'\n\nimport { DEFAULT_STATUS_MACHINE } from './types.js'\nimport { validateTimezone } from './utilities/timezoneUtils.js'\n\nfunction validateStatusMachine(sm: StatusMachineConfig): void {\n if (!sm.statuses.includes(sm.defaultStatus)) {\n throw new Error(`statusMachine.defaultStatus \"${sm.defaultStatus}\" is not in statuses array`)\n }\n for (const s of sm.blockingStatuses) {\n if (!sm.statuses.includes(s)) {\n throw new Error(`statusMachine.blockingStatuses contains \"${s}\" which is not in statuses array`)\n }\n }\n for (const s of sm.terminalStatuses) {\n if (!sm.statuses.includes(s)) {\n throw new Error(`statusMachine.terminalStatuses contains \"${s}\" which is not in statuses array`)\n }\n }\n for (const [from, targets] of Object.entries(sm.transitions)) {\n if (!sm.statuses.includes(from)) {\n throw new Error(`statusMachine.transitions has key \"${from}\" which is not in statuses array`)\n }\n for (const to of targets) {\n if (!sm.statuses.includes(to)) {\n throw new Error(`statusMachine.transitions[\"${from}\"] targets \"${to}\" which is not in statuses array`)\n }\n }\n }\n // A terminal status must have no outgoing transitions — otherwise the config\n // contradicts itself (the status is declared terminal but can be left).\n for (const s of sm.terminalStatuses) {\n if ((sm.transitions[s]?.length ?? 0) > 0) {\n throw new Error(\n `statusMachine.terminalStatuses contains \"${s}\" but transitions[\"${s}\"] is non-empty — a terminal status cannot have outgoing transitions`,\n )\n }\n }\n // A reservation is born in defaultStatus, so it must not be terminal.\n if (sm.terminalStatuses.includes(sm.defaultStatus)) {\n throw new Error(`statusMachine.defaultStatus \"${sm.defaultStatus}\" cannot be a terminal status`)\n }\n if (!sm.statuses.includes(sm.confirmStatus)) {\n throw new Error(`statusMachine.confirmStatus \"${sm.confirmStatus}\" is not in statuses array`)\n }\n if (!sm.statuses.includes(sm.cancelStatus)) {\n throw new Error(`statusMachine.cancelStatus \"${sm.cancelStatus}\" is not in statuses array`)\n }\n}\n\nexport const DEFAULT_RESOURCE_TYPES = ['staff', 'equipment', 'room']\nexport const DEFAULT_LEAVE_TYPES = ['vacation', 'sick', 'personal', 'closure', 'other']\n\nfunction resolveStaffProvisioning(\n pluginOptions: ReservationPluginConfig,\n resourceTypes: string[],\n): ResolvedStaffProvisioningConfig | undefined {\n const sp = pluginOptions.staffProvisioning\n if (!sp) {\n return undefined\n }\n\n if (!pluginOptions.resourceOwnerMode) {\n throw new Error('staffProvisioning requires resourceOwnerMode to be enabled')\n }\n if (sp.staffRoles.length === 0) {\n throw new Error('staffProvisioning.staffRoles must be a non-empty array')\n }\n const resourceType = sp.resourceType ?? 'staff'\n if (!resourceTypes.includes(resourceType)) {\n throw new Error(\n `staffProvisioning.resourceType \"${resourceType}\" is not in resourceTypes [${resourceTypes.join(', ')}]`,\n )\n }\n const userCollection = sp.userCollection ?? pluginOptions.userCollection\n if (!userCollection) {\n throw new Error(\n 'staffProvisioning.userCollection is required when top-level userCollection is unset',\n )\n }\n\n return {\n beforeCreate: sp.beforeCreate,\n nameFrom: sp.nameFrom ?? 'name',\n resourceType,\n roleField: sp.roleField ?? 'role',\n staffRoles: sp.staffRoles,\n userCollection,\n }\n}\n\nexport const DEFAULT_SLUGS = {\n customers: 'customers',\n media: 'media',\n reservations: 'reservations',\n resources: 'resources',\n schedules: 'schedules',\n services: 'services',\n} as const\n\nexport const DEFAULT_ADMIN_GROUP = 'Reservations'\nexport const DEFAULT_ALLOW_GUEST_BOOKING = false\nexport const DEFAULT_BUFFER_TIME = 0\nexport const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24\n\nexport function resolveConfig(\n pluginOptions: ReservationPluginConfig,\n): ResolvedReservationPluginConfig {\n // A disabled plugin still resolves (so collections register with the right\n // slugs for schema stability) but skips all config validation — temporarily\n // disabling a misconfigured plugin should not throw at boot (review C3).\n const disabled = pluginOptions.disabled ?? false\n\n if (!disabled) {\n if (pluginOptions.resourceTypes !== undefined && pluginOptions.resourceTypes.length === 0) {\n throw new Error('resourceTypes must be a non-empty array')\n }\n if (pluginOptions.leaveTypes !== undefined && pluginOptions.leaveTypes.length === 0) {\n throw new Error('leaveTypes must be a non-empty array')\n }\n }\n\n const resourceTypes = pluginOptions.resourceTypes ?? DEFAULT_RESOURCE_TYPES\n const userStatusMachine = pluginOptions.statusMachine\n const rom = pluginOptions.resourceOwnerMode\n const resolved: ResolvedReservationPluginConfig = {\n access: pluginOptions.access ?? {},\n adminGroup: pluginOptions.adminGroup ?? DEFAULT_ADMIN_GROUP,\n allowGuestBooking: pluginOptions.allowGuestBooking ?? DEFAULT_ALLOW_GUEST_BOOKING,\n cancellationNoticePeriod:\n pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,\n collectionOverrides: pluginOptions.collectionOverrides ?? {},\n defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,\n disabled: pluginOptions.disabled ?? false,\n extraReservationFields: pluginOptions.extraReservationFields ?? [],\n // Real value is set by the plugin once config.collections is known (C8)\n hasMediaCollection: false,\n hooks: pluginOptions.hooks ?? {},\n leaveTypes: pluginOptions.leaveTypes ?? DEFAULT_LEAVE_TYPES,\n localized: false,\n multiTenant: {\n cookieName: pluginOptions.multiTenant?.cookieName ?? 'payload-tenant',\n tenantField: pluginOptions.multiTenant?.tenantField ?? 'tenant',\n },\n resourceOwnerMode: rom\n ? {\n adminRoles: rom.adminRoles ?? [],\n ownedServices: rom.ownedServices ?? false,\n ownerCollection: rom.ownerCollection,\n ownerField: rom.ownerField ?? 'owner',\n roleField: rom.roleField ?? pluginOptions.staffProvisioning?.roleField ?? 'role',\n }\n : undefined,\n resourceTypes,\n slugs: {\n customers: pluginOptions.slugs?.customers ?? DEFAULT_SLUGS.customers,\n media: pluginOptions.slugs?.media ?? DEFAULT_SLUGS.media,\n reservations: pluginOptions.slugs?.reservations ?? DEFAULT_SLUGS.reservations,\n resources: pluginOptions.slugs?.resources ?? DEFAULT_SLUGS.resources,\n schedules: pluginOptions.slugs?.schedules ?? DEFAULT_SLUGS.schedules,\n services: pluginOptions.slugs?.services ?? DEFAULT_SLUGS.services,\n },\n staffProvisioning: disabled\n ? undefined\n : resolveStaffProvisioning(pluginOptions, resourceTypes),\n statusMachine: userStatusMachine\n ? {\n blockingStatuses:\n userStatusMachine.blockingStatuses ?? DEFAULT_STATUS_MACHINE.blockingStatuses,\n cancelStatus: userStatusMachine.cancelStatus ?? DEFAULT_STATUS_MACHINE.cancelStatus,\n confirmStatus: userStatusMachine.confirmStatus ?? DEFAULT_STATUS_MACHINE.confirmStatus,\n defaultStatus: userStatusMachine.defaultStatus ?? DEFAULT_STATUS_MACHINE.defaultStatus,\n statuses: userStatusMachine.statuses ?? DEFAULT_STATUS_MACHINE.statuses,\n terminalStatuses:\n userStatusMachine.terminalStatuses ?? DEFAULT_STATUS_MACHINE.terminalStatuses,\n transitions: userStatusMachine.transitions ?? DEFAULT_STATUS_MACHINE.transitions,\n }\n : { ...DEFAULT_STATUS_MACHINE },\n timezone: pluginOptions.timezone ?? 'UTC',\n userCollection: pluginOptions.userCollection ?? undefined,\n }\n\n if (!disabled) {\n validateStatusMachine(resolved.statusMachine)\n validateTimezone(resolved.timezone)\n }\n\n return resolved\n}\n"],"names":["DEFAULT_STATUS_MACHINE","validateTimezone","validateStatusMachine","sm","statuses","includes","defaultStatus","Error","s","blockingStatuses","terminalStatuses","from","targets","Object","entries","transitions","to","length","confirmStatus","cancelStatus","DEFAULT_RESOURCE_TYPES","DEFAULT_LEAVE_TYPES","resolveStaffProvisioning","pluginOptions","resourceTypes","sp","staffProvisioning","undefined","resourceOwnerMode","staffRoles","resourceType","join","userCollection","beforeCreate","nameFrom","roleField","DEFAULT_SLUGS","customers","media","reservations","resources","schedules","services","DEFAULT_ADMIN_GROUP","DEFAULT_ALLOW_GUEST_BOOKING","DEFAULT_BUFFER_TIME","DEFAULT_CANCELLATION_NOTICE_PERIOD","resolveConfig","disabled","leaveTypes","userStatusMachine","statusMachine","rom","resolved","access","adminGroup","allowGuestBooking","cancellationNoticePeriod","collectionOverrides","defaultBufferTime","extraReservationFields","hasMediaCollection","hooks","localized","multiTenant","cookieName","tenantField","adminRoles","ownedServices","ownerCollection","ownerField","slugs","timezone"],"mappings":"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"}
@@ -49,7 +49,7 @@ export function createCancelBookingEndpoint(config) {
49
49
  collection: config.slugs.reservations,
50
50
  data: {
51
51
  cancellationReason: reason,
52
- status: 'cancelled'
52
+ status: config.statusMachine.cancelStatus
53
53
  },
54
54
  // Authorization (owner / admin / matching token) is already enforced above.
55
55
  // overrideAccess lets the write proceed for anonymous guest cancellations,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/cancelBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { isPrivilegedUser } from '../utilities/userRoles.js'\n\nexport function createCancelBookingEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const body = await req.json?.()\n const { reason, reservationId, token } = (body ?? {}) as {\n reason?: string\n reservationId?: string\n token?: string\n }\n\n if (!reservationId) {\n return Response.json({ message: 'reservationId is required' }, { status: 400 })\n }\n\n // Fetch the reservation to check ownership / token.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const existing = await (req.payload.findByID as any)({\n id: reservationId,\n collection: config.slugs.reservations,\n depth: 0,\n overrideAccess: true,\n req,\n })\n\n if (req.user) {\n // Authenticated path: owner (customer === req.user) or admin/staff.\n const customerId =\n typeof existing.customer === 'object' ? existing.customer?.id : existing.customer\n const isOwner = customerId === req.user.id\n // Staff/admin detection (role-aware for single-collection deployments)\n const isAdmin = isPrivilegedUser(req.user, config)\n if (!isOwner && !isAdmin) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n } else {\n // Guest path: match the cancellation token.\n if (!token || !existing.cancellationToken || token !== existing.cancellationToken) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const reservation = await (req.payload.update as any)({\n id: reservationId,\n collection: config.slugs.reservations,\n data: {\n cancellationReason: reason,\n status: 'cancelled',\n },\n // Authorization (owner / admin / matching token) is already enforced above.\n // overrideAccess lets the write proceed for anonymous guest cancellations,\n // and avoids a spurious 500 when resourceOwnerMode restricts update access.\n overrideAccess: true,\n req,\n })\n\n // Strip the cancellation token from the response, consistent with the book\n // endpoint — it must never be echoed back over HTTP.\n const { cancellationToken: _cancellationToken, ...safeReservation } =\n reservation as Record<string, unknown>\n\n return Response.json(safeReservation)\n },\n method: 'post',\n path: '/reserve/cancel',\n }\n}\n"],"names":["isPrivilegedUser","createCancelBookingEndpoint","config","handler","req","body","json","reason","reservationId","token","Response","message","status","existing","payload","findByID","id","collection","slugs","reservations","depth","overrideAccess","user","customerId","customer","isOwner","isAdmin","cancellationToken","reservation","update","data","cancellationReason","_cancellationToken","safeReservation","method","path"],"mappings":"AAIA,SAASA,gBAAgB,QAAQ,4BAA2B;AAE5D,OAAO,SAASC,4BAA4BC,MAAuC;IACjF,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,OAAO,MAAMD,IAAIE,IAAI;YAC3B,MAAM,EAAEC,MAAM,EAAEC,aAAa,EAAEC,KAAK,EAAE,GAAIJ,QAAQ,CAAC;YAMnD,IAAI,CAACG,eAAe;gBAClB,OAAOE,SAASJ,IAAI,CAAC;oBAAEK,SAAS;gBAA4B,GAAG;oBAAEC,QAAQ;gBAAI;YAC/E;YAEA,oDAAoD;YACpD,8DAA8D;YAC9D,MAAMC,WAAW,MAAM,AAACT,IAAIU,OAAO,CAACC,QAAQ,CAAS;gBACnDC,IAAIR;gBACJS,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCC,OAAO;gBACPC,gBAAgB;gBAChBjB;YACF;YAEA,IAAIA,IAAIkB,IAAI,EAAE;gBACZ,oEAAoE;gBACpE,MAAMC,aACJ,OAAOV,SAASW,QAAQ,KAAK,WAAWX,SAASW,QAAQ,EAAER,KAAKH,SAASW,QAAQ;gBACnF,MAAMC,UAAUF,eAAenB,IAAIkB,IAAI,CAACN,EAAE;gBAC1C,uEAAuE;gBACvE,MAAMU,UAAU1B,iBAAiBI,IAAIkB,IAAI,EAAEpB;gBAC3C,IAAI,CAACuB,WAAW,CAACC,SAAS;oBACxB,OAAOhB,SAASJ,IAAI,CAAC;wBAAEK,SAAS;oBAAY,GAAG;wBAAEC,QAAQ;oBAAI;gBAC/D;YACF,OAAO;gBACL,4CAA4C;gBAC5C,IAAI,CAACH,SAAS,CAACI,SAASc,iBAAiB,IAAIlB,UAAUI,SAASc,iBAAiB,EAAE;oBACjF,OAAOjB,SAASJ,IAAI,CAAC;wBAAEK,SAAS;oBAAY,GAAG;wBAAEC,QAAQ;oBAAI;gBAC/D;YACF;YAEA,8DAA8D;YAC9D,MAAMgB,cAAc,MAAM,AAACxB,IAAIU,OAAO,CAACe,MAAM,CAAS;gBACpDb,IAAIR;gBACJS,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCW,MAAM;oBACJC,oBAAoBxB;oBACpBK,QAAQ;gBACV;gBACA,4EAA4E;gBAC5E,2EAA2E;gBAC3E,4EAA4E;gBAC5ES,gBAAgB;gBAChBjB;YACF;YAEA,2EAA2E;YAC3E,qDAAqD;YACrD,MAAM,EAAEuB,mBAAmBK,kBAAkB,EAAE,GAAGC,iBAAiB,GACjEL;YAEF,OAAOlB,SAASJ,IAAI,CAAC2B;QACvB;QACAC,QAAQ;QACRC,MAAM;IACR;AACF"}
1
+ {"version":3,"sources":["../../src/endpoints/cancelBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { isPrivilegedUser } from '../utilities/userRoles.js'\n\nexport function createCancelBookingEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const body = await req.json?.()\n const { reason, reservationId, token } = (body ?? {}) as {\n reason?: string\n reservationId?: string\n token?: string\n }\n\n if (!reservationId) {\n return Response.json({ message: 'reservationId is required' }, { status: 400 })\n }\n\n // Fetch the reservation to check ownership / token.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const existing = await (req.payload.findByID as any)({\n id: reservationId,\n collection: config.slugs.reservations,\n depth: 0,\n overrideAccess: true,\n req,\n })\n\n if (req.user) {\n // Authenticated path: owner (customer === req.user) or admin/staff.\n const customerId =\n typeof existing.customer === 'object' ? existing.customer?.id : existing.customer\n const isOwner = customerId === req.user.id\n // Staff/admin detection (role-aware for single-collection deployments)\n const isAdmin = isPrivilegedUser(req.user, config)\n if (!isOwner && !isAdmin) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n } else {\n // Guest path: match the cancellation token.\n if (!token || !existing.cancellationToken || token !== existing.cancellationToken) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const reservation = await (req.payload.update as any)({\n id: reservationId,\n collection: config.slugs.reservations,\n data: {\n cancellationReason: reason,\n status: config.statusMachine.cancelStatus,\n },\n // Authorization (owner / admin / matching token) is already enforced above.\n // overrideAccess lets the write proceed for anonymous guest cancellations,\n // and avoids a spurious 500 when resourceOwnerMode restricts update access.\n overrideAccess: true,\n req,\n })\n\n // Strip the cancellation token from the response, consistent with the book\n // endpoint — it must never be echoed back over HTTP.\n const { cancellationToken: _cancellationToken, ...safeReservation } =\n reservation as Record<string, unknown>\n\n return Response.json(safeReservation)\n },\n method: 'post',\n path: '/reserve/cancel',\n }\n}\n"],"names":["isPrivilegedUser","createCancelBookingEndpoint","config","handler","req","body","json","reason","reservationId","token","Response","message","status","existing","payload","findByID","id","collection","slugs","reservations","depth","overrideAccess","user","customerId","customer","isOwner","isAdmin","cancellationToken","reservation","update","data","cancellationReason","statusMachine","cancelStatus","_cancellationToken","safeReservation","method","path"],"mappings":"AAIA,SAASA,gBAAgB,QAAQ,4BAA2B;AAE5D,OAAO,SAASC,4BAA4BC,MAAuC;IACjF,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,OAAO,MAAMD,IAAIE,IAAI;YAC3B,MAAM,EAAEC,MAAM,EAAEC,aAAa,EAAEC,KAAK,EAAE,GAAIJ,QAAQ,CAAC;YAMnD,IAAI,CAACG,eAAe;gBAClB,OAAOE,SAASJ,IAAI,CAAC;oBAAEK,SAAS;gBAA4B,GAAG;oBAAEC,QAAQ;gBAAI;YAC/E;YAEA,oDAAoD;YACpD,8DAA8D;YAC9D,MAAMC,WAAW,MAAM,AAACT,IAAIU,OAAO,CAACC,QAAQ,CAAS;gBACnDC,IAAIR;gBACJS,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCC,OAAO;gBACPC,gBAAgB;gBAChBjB;YACF;YAEA,IAAIA,IAAIkB,IAAI,EAAE;gBACZ,oEAAoE;gBACpE,MAAMC,aACJ,OAAOV,SAASW,QAAQ,KAAK,WAAWX,SAASW,QAAQ,EAAER,KAAKH,SAASW,QAAQ;gBACnF,MAAMC,UAAUF,eAAenB,IAAIkB,IAAI,CAACN,EAAE;gBAC1C,uEAAuE;gBACvE,MAAMU,UAAU1B,iBAAiBI,IAAIkB,IAAI,EAAEpB;gBAC3C,IAAI,CAACuB,WAAW,CAACC,SAAS;oBACxB,OAAOhB,SAASJ,IAAI,CAAC;wBAAEK,SAAS;oBAAY,GAAG;wBAAEC,QAAQ;oBAAI;gBAC/D;YACF,OAAO;gBACL,4CAA4C;gBAC5C,IAAI,CAACH,SAAS,CAACI,SAASc,iBAAiB,IAAIlB,UAAUI,SAASc,iBAAiB,EAAE;oBACjF,OAAOjB,SAASJ,IAAI,CAAC;wBAAEK,SAAS;oBAAY,GAAG;wBAAEC,QAAQ;oBAAI;gBAC/D;YACF;YAEA,8DAA8D;YAC9D,MAAMgB,cAAc,MAAM,AAACxB,IAAIU,OAAO,CAACe,MAAM,CAAS;gBACpDb,IAAIR;gBACJS,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCW,MAAM;oBACJC,oBAAoBxB;oBACpBK,QAAQV,OAAO8B,aAAa,CAACC,YAAY;gBAC3C;gBACA,4EAA4E;gBAC5E,2EAA2E;gBAC3E,4EAA4E;gBAC5EZ,gBAAgB;gBAChBjB;YACF;YAEA,2EAA2E;YAC3E,qDAAqD;YACrD,MAAM,EAAEuB,mBAAmBO,kBAAkB,EAAE,GAAGC,iBAAiB,GACjEP;YAEF,OAAOlB,SAASJ,IAAI,CAAC6B;QACvB;QACAC,QAAQ;QACRC,MAAM;IACR;AACF"}
@@ -1,5 +1,6 @@
1
1
  import { getAvailableSlots } from '../services/AvailabilityService.js';
2
2
  import { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js';
3
+ import { getDayKeyInTimezone, isValidDayKey } from '../utilities/timezoneUtils.js';
3
4
  export function createCheckAvailabilityEndpoint(config) {
4
5
  return {
5
6
  handler: async (req)=>{
@@ -14,15 +15,39 @@ export function createCheckAvailabilityEndpoint(config) {
14
15
  status: 400
15
16
  });
16
17
  }
17
- const parsedDate = new Date(date);
18
- if (isNaN(parsedDate.getTime())) {
18
+ // YYYY-MM-DD is taken as a business-TZ calendar day verbatim; any other
19
+ // parseable date is re-keyed into the business timezone. Never
20
+ // `new Date('YYYY-MM-DD')` — that pins the day to UTC midnight.
21
+ let dayKey;
22
+ if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
23
+ if (!isValidDayKey(date)) {
24
+ return Response.json({
25
+ error: 'Invalid date format. Expected YYYY-MM-DD'
26
+ }, {
27
+ status: 400
28
+ });
29
+ }
30
+ dayKey = date;
31
+ } else {
32
+ const parsed = new Date(date);
33
+ if (isNaN(parsed.getTime())) {
34
+ return Response.json({
35
+ error: 'Invalid date format. Expected YYYY-MM-DD'
36
+ }, {
37
+ status: 400
38
+ });
39
+ }
40
+ dayKey = getDayKeyInTimezone(parsed, config.timezone);
41
+ }
42
+ const guestCountRaw = Number(url.searchParams.get('guestCount') ?? '1');
43
+ if (!Number.isFinite(guestCountRaw)) {
19
44
  return Response.json({
20
- error: 'Invalid date format. Expected YYYY-MM-DD'
45
+ error: 'Invalid guestCount'
21
46
  }, {
22
47
  status: 400
23
48
  });
24
49
  }
25
- const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1);
50
+ const guestCount = Math.max(Math.floor(guestCountRaw), 1);
26
51
  // Resolve required resource set: caller resource(s) ∪ service.requiredResources
27
52
  const explicit = url.searchParams.get('resources');
28
53
  const callerIds = explicit ? explicit.split(',').map((s)=>s.trim()).filter(Boolean) : [
@@ -34,12 +59,35 @@ export function createCheckAvailabilityEndpoint(config) {
34
59
  collection: config.slugs.services,
35
60
  depth: 0,
36
61
  req
37
- });
62
+ }).catch(()=>null);
63
+ if (!svcDoc) {
64
+ return Response.json({
65
+ error: 'Service not found'
66
+ }, {
67
+ status: 404
68
+ });
69
+ }
70
+ // Validate the primary resource id up front — a malformed id in a query
71
+ // surfaces as an adapter cast error (500) instead of a clean 404.
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ const resourceDoc = await req.payload.findByID({
74
+ id: resource,
75
+ collection: config.slugs.resources,
76
+ depth: 0,
77
+ req
78
+ }).catch(()=>null);
79
+ if (!resourceDoc) {
80
+ return Response.json({
81
+ error: 'Resource not found'
82
+ }, {
83
+ status: 404
84
+ });
85
+ }
38
86
  const requiredIds = (svcDoc?.requiredResources ?? []).map((r)=>extractId(r)).filter((r)=>r !== undefined);
39
87
  const resourceIds = mergeResourceIds(callerIds, requiredIds);
40
88
  const slots = await getAvailableSlots({
41
89
  blockingStatuses: config.statusMachine.blockingStatuses,
42
- date: parsedDate,
90
+ date: dayKey,
43
91
  guestCount,
44
92
  payload: req.payload,
45
93
  req,
@@ -48,7 +96,8 @@ export function createCheckAvailabilityEndpoint(config) {
48
96
  resourceSlug: config.slugs.resources,
49
97
  scheduleSlug: config.slugs.schedules,
50
98
  serviceId: service,
51
- serviceSlug: config.slugs.services
99
+ serviceSlug: config.slugs.services,
100
+ timeZone: config.timezone
52
101
  });
53
102
  return Response.json({
54
103
  slots
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/checkAvailability.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { getAvailableSlots } from '../services/AvailabilityService.js'\nimport { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js'\n\nexport function createCheckAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n const url = new URL(req.url!)\n const date = url.searchParams.get('date')\n const resource = url.searchParams.get('resource')\n const service = url.searchParams.get('service')\n\n if (!date || !resource || !service) {\n return Response.json(\n { message: 'Missing required query params: resource, date, service' },\n { status: 400 },\n )\n }\n\n const parsedDate = new Date(date)\n if (isNaN(parsedDate.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n\n const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1)\n\n // Resolve required resource set: caller resource(s) ∪ service.requiredResources\n const explicit = url.searchParams.get('resources')\n const callerIds = explicit ? explicit.split(',').map((s) => s.trim()).filter(Boolean) : [resource]\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const svcDoc = await (req.payload.findByID as any)({\n id: service,\n collection: config.slugs.services,\n depth: 0,\n req,\n })\n const requiredIds = ((svcDoc?.requiredResources as unknown[]) ?? [])\n .map((r) => extractId(r))\n .filter((r): r is number | string => r !== undefined)\n const resourceIds = mergeResourceIds(callerIds, requiredIds)\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: parsedDate,\n guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceIds,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n serviceId: service,\n serviceSlug: config.slugs.services,\n })\n\n return Response.json({ slots })\n },\n method: 'get',\n path: '/reserve/availability',\n }\n}\n"],"names":["getAvailableSlots","extractId","mergeResourceIds","createCheckAvailabilityEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","message","status","parsedDate","Date","isNaN","getTime","error","guestCount","Math","max","Number","explicit","callerIds","split","map","s","trim","filter","Boolean","svcDoc","payload","findByID","id","collection","slugs","services","depth","requiredIds","requiredResources","r","undefined","resourceIds","slots","blockingStatuses","statusMachine","reservationSlug","reservations","resourceSlug","resources","scheduleSlug","schedules","serviceId","serviceSlug","method","path"],"mappings":"AAIA,SAASA,iBAAiB,QAAQ,qCAAoC;AACtE,SAASC,SAAS,EAAEC,gBAAgB,QAAQ,2CAA0C;AAEtF,OAAO,SAASC,gCACdC,MAAuC;IAEvC,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,MAAM,IAAIC,IAAIF,IAAIC,GAAG;YAC3B,MAAME,OAAOF,IAAIG,YAAY,CAACC,GAAG,CAAC;YAClC,MAAMC,WAAWL,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAME,UAAUN,IAAIG,YAAY,CAACC,GAAG,CAAC;YAErC,IAAI,CAACF,QAAQ,CAACG,YAAY,CAACC,SAAS;gBAClC,OAAOC,SAASC,IAAI,CAClB;oBAAEC,SAAS;gBAAyD,GACpE;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMC,aAAa,IAAIC,KAAKV;YAC5B,IAAIW,MAAMF,WAAWG,OAAO,KAAK;gBAC/B,OAAOP,SAASC,IAAI,CAClB;oBAAEO,OAAO;gBAA2C,GACpD;oBAAEL,QAAQ;gBAAI;YAElB;YAEA,MAAMM,aAAaC,KAAKC,GAAG,CAACC,OAAOnB,IAAIG,YAAY,CAACC,GAAG,CAAC,iBAAiB,MAAM;YAE/E,gFAAgF;YAChF,MAAMgB,WAAWpB,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAMiB,YAAYD,WAAWA,SAASE,KAAK,CAAC,KAAKC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI,IAAIC,MAAM,CAACC,WAAW;gBAACtB;aAAS;YAElG,8DAA8D;YAC9D,MAAMuB,SAAS,MAAM,AAAC7B,IAAI8B,OAAO,CAACC,QAAQ,CAAS;gBACjDC,IAAIzB;gBACJ0B,YAAYnC,OAAOoC,KAAK,CAACC,QAAQ;gBACjCC,OAAO;gBACPpC;YACF;YACA,MAAMqC,cAAc,AAAC,CAAA,AAACR,QAAQS,qBAAmC,EAAE,AAAD,EAC/Dd,GAAG,CAAC,CAACe,IAAM5C,UAAU4C,IACrBZ,MAAM,CAAC,CAACY,IAA4BA,MAAMC;YAC7C,MAAMC,cAAc7C,iBAAiB0B,WAAWe;YAEhD,MAAMK,QAAQ,MAAMhD,kBAAkB;gBACpCiD,kBAAkB7C,OAAO8C,aAAa,CAACD,gBAAgB;gBACvDxC,MAAMS;gBACNK;gBACAa,SAAS9B,IAAI8B,OAAO;gBACpB9B;gBACA6C,iBAAiB/C,OAAOoC,KAAK,CAACY,YAAY;gBAC1CL;gBACAM,cAAcjD,OAAOoC,KAAK,CAACc,SAAS;gBACpCC,cAAcnD,OAAOoC,KAAK,CAACgB,SAAS;gBACpCC,WAAW5C;gBACX6C,aAAatD,OAAOoC,KAAK,CAACC,QAAQ;YACpC;YAEA,OAAO3B,SAASC,IAAI,CAAC;gBAAEiC;YAAM;QAC/B;QACAW,QAAQ;QACRC,MAAM;IACR;AACF"}
1
+ {"version":3,"sources":["../../src/endpoints/checkAvailability.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { getAvailableSlots } from '../services/AvailabilityService.js'\nimport { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js'\nimport { getDayKeyInTimezone, isValidDayKey } from '../utilities/timezoneUtils.js'\n\nexport function createCheckAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n const url = new URL(req.url!)\n const date = url.searchParams.get('date')\n const resource = url.searchParams.get('resource')\n const service = url.searchParams.get('service')\n\n if (!date || !resource || !service) {\n return Response.json(\n { message: 'Missing required query params: resource, date, service' },\n { status: 400 },\n )\n }\n\n // YYYY-MM-DD is taken as a business-TZ calendar day verbatim; any other\n // parseable date is re-keyed into the business timezone. Never\n // `new Date('YYYY-MM-DD')` — that pins the day to UTC midnight.\n let dayKey: string\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(date)) {\n if (!isValidDayKey(date)) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n dayKey = date\n } else {\n const parsed = new Date(date)\n if (isNaN(parsed.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n dayKey = getDayKeyInTimezone(parsed, config.timezone)\n }\n\n const guestCountRaw = Number(url.searchParams.get('guestCount') ?? '1')\n if (!Number.isFinite(guestCountRaw)) {\n return Response.json({ error: 'Invalid guestCount' }, { status: 400 })\n }\n const guestCount = Math.max(Math.floor(guestCountRaw), 1)\n\n // Resolve required resource set: caller resource(s) ∪ service.requiredResources\n const explicit = url.searchParams.get('resources')\n const callerIds = explicit ? explicit.split(',').map((s) => s.trim()).filter(Boolean) : [resource]\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const svcDoc = await (req.payload.findByID as any)({\n id: service,\n collection: config.slugs.services,\n depth: 0,\n req,\n }).catch(() => null)\n if (!svcDoc) {\n return Response.json({ error: 'Service not found' }, { status: 404 })\n }\n\n // Validate the primary resource id up front — a malformed id in a query\n // surfaces as an adapter cast error (500) instead of a clean 404.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resourceDoc = await (req.payload.findByID as any)({\n id: resource,\n collection: config.slugs.resources,\n depth: 0,\n req,\n }).catch(() => null)\n if (!resourceDoc) {\n return Response.json({ error: 'Resource not found' }, { status: 404 })\n }\n const requiredIds = ((svcDoc?.requiredResources as unknown[]) ?? [])\n .map((r) => extractId(r))\n .filter((r): r is number | string => r !== undefined)\n const resourceIds = mergeResourceIds(callerIds, requiredIds)\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: dayKey,\n guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceIds,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n serviceId: service,\n serviceSlug: config.slugs.services,\n timeZone: config.timezone,\n })\n\n return Response.json({ slots })\n },\n method: 'get',\n path: '/reserve/availability',\n }\n}\n"],"names":["getAvailableSlots","extractId","mergeResourceIds","getDayKeyInTimezone","isValidDayKey","createCheckAvailabilityEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","message","status","dayKey","test","error","parsed","Date","isNaN","getTime","timezone","guestCountRaw","Number","isFinite","guestCount","Math","max","floor","explicit","callerIds","split","map","s","trim","filter","Boolean","svcDoc","payload","findByID","id","collection","slugs","services","depth","catch","resourceDoc","resources","requiredIds","requiredResources","r","undefined","resourceIds","slots","blockingStatuses","statusMachine","reservationSlug","reservations","resourceSlug","scheduleSlug","schedules","serviceId","serviceSlug","timeZone","method","path"],"mappings":"AAIA,SAASA,iBAAiB,QAAQ,qCAAoC;AACtE,SAASC,SAAS,EAAEC,gBAAgB,QAAQ,2CAA0C;AACtF,SAASC,mBAAmB,EAAEC,aAAa,QAAQ,gCAA+B;AAElF,OAAO,SAASC,gCACdC,MAAuC;IAEvC,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,MAAM,IAAIC,IAAIF,IAAIC,GAAG;YAC3B,MAAME,OAAOF,IAAIG,YAAY,CAACC,GAAG,CAAC;YAClC,MAAMC,WAAWL,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAME,UAAUN,IAAIG,YAAY,CAACC,GAAG,CAAC;YAErC,IAAI,CAACF,QAAQ,CAACG,YAAY,CAACC,SAAS;gBAClC,OAAOC,SAASC,IAAI,CAClB;oBAAEC,SAAS;gBAAyD,GACpE;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,wEAAwE;YACxE,+DAA+D;YAC/D,gEAAgE;YAChE,IAAIC;YACJ,IAAI,sBAAsBC,IAAI,CAACV,OAAO;gBACpC,IAAI,CAACP,cAAcO,OAAO;oBACxB,OAAOK,SAASC,IAAI,CAClB;wBAAEK,OAAO;oBAA2C,GACpD;wBAAEH,QAAQ;oBAAI;gBAElB;gBACAC,SAAST;YACX,OAAO;gBACL,MAAMY,SAAS,IAAIC,KAAKb;gBACxB,IAAIc,MAAMF,OAAOG,OAAO,KAAK;oBAC3B,OAAOV,SAASC,IAAI,CAClB;wBAAEK,OAAO;oBAA2C,GACpD;wBAAEH,QAAQ;oBAAI;gBAElB;gBACAC,SAASjB,oBAAoBoB,QAAQjB,OAAOqB,QAAQ;YACtD;YAEA,MAAMC,gBAAgBC,OAAOpB,IAAIG,YAAY,CAACC,GAAG,CAAC,iBAAiB;YACnE,IAAI,CAACgB,OAAOC,QAAQ,CAACF,gBAAgB;gBACnC,OAAOZ,SAASC,IAAI,CAAC;oBAAEK,OAAO;gBAAqB,GAAG;oBAAEH,QAAQ;gBAAI;YACtE;YACA,MAAMY,aAAaC,KAAKC,GAAG,CAACD,KAAKE,KAAK,CAACN,gBAAgB;YAEvD,gFAAgF;YAChF,MAAMO,WAAW1B,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAMuB,YAAYD,WAAWA,SAASE,KAAK,CAAC,KAAKC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI,IAAIC,MAAM,CAACC,WAAW;gBAAC5B;aAAS;YAElG,8DAA8D;YAC9D,MAAM6B,SAAS,MAAM,AAACnC,IAAIoC,OAAO,CAACC,QAAQ,CAAS;gBACjDC,IAAI/B;gBACJgC,YAAYzC,OAAO0C,KAAK,CAACC,QAAQ;gBACjCC,OAAO;gBACP1C;YACF,GAAG2C,KAAK,CAAC,IAAM;YACf,IAAI,CAACR,QAAQ;gBACX,OAAO3B,SAASC,IAAI,CAAC;oBAAEK,OAAO;gBAAoB,GAAG;oBAAEH,QAAQ;gBAAI;YACrE;YAEA,wEAAwE;YACxE,kEAAkE;YAClE,8DAA8D;YAC9D,MAAMiC,cAAc,MAAM,AAAC5C,IAAIoC,OAAO,CAACC,QAAQ,CAAS;gBACtDC,IAAIhC;gBACJiC,YAAYzC,OAAO0C,KAAK,CAACK,SAAS;gBAClCH,OAAO;gBACP1C;YACF,GAAG2C,KAAK,CAAC,IAAM;YACf,IAAI,CAACC,aAAa;gBAChB,OAAOpC,SAASC,IAAI,CAAC;oBAAEK,OAAO;gBAAqB,GAAG;oBAAEH,QAAQ;gBAAI;YACtE;YACA,MAAMmC,cAAc,AAAC,CAAA,AAACX,QAAQY,qBAAmC,EAAE,AAAD,EAC/DjB,GAAG,CAAC,CAACkB,IAAMvD,UAAUuD,IACrBf,MAAM,CAAC,CAACe,IAA4BA,MAAMC;YAC7C,MAAMC,cAAcxD,iBAAiBkC,WAAWkB;YAEhD,MAAMK,QAAQ,MAAM3D,kBAAkB;gBACpC4D,kBAAkBtD,OAAOuD,aAAa,CAACD,gBAAgB;gBACvDjD,MAAMS;gBACNW;gBACAa,SAASpC,IAAIoC,OAAO;gBACpBpC;gBACAsD,iBAAiBxD,OAAO0C,KAAK,CAACe,YAAY;gBAC1CL;gBACAM,cAAc1D,OAAO0C,KAAK,CAACK,SAAS;gBACpCY,cAAc3D,OAAO0C,KAAK,CAACkB,SAAS;gBACpCC,WAAWpD;gBACXqD,aAAa9D,OAAO0C,KAAK,CAACC,QAAQ;gBAClCoB,UAAU/D,OAAOqB,QAAQ;YAC3B;YAEA,OAAOX,SAASC,IAAI,CAAC;gBAAE0C;YAAM;QAC/B;QACAW,QAAQ;QACRC,MAAM;IACR;AACF"}
@@ -1,23 +1,32 @@
1
+ import { isPrivilegedUser } from '../utilities/userRoles.js';
1
2
  export function createBookingEndpoint(config) {
2
3
  return {
3
4
  handler: async (req)=>{
4
5
  const data = await req.json?.();
5
- // Call beforeBookingCreate plugin hooks before creating the reservation
6
- let bookingData = data;
7
- if (config.hooks?.beforeBookingCreate) {
8
- for (const hook of config.hooks.beforeBookingCreate){
9
- bookingData = await hook({
10
- data: bookingData,
11
- req
12
- }) ?? bookingData;
6
+ // Cancellation tokens are server-generated secrets never accept one
7
+ // from the request body.
8
+ delete data.cancellationToken;
9
+ // Who may book for whom: staff/admin for anyone (walk-ins); an
10
+ // authenticated customer only for themselves; anonymous callers never
11
+ // for an existing customer record (the guest flow covers them).
12
+ if (!isPrivilegedUser(req.user, config)) {
13
+ if (req.user) {
14
+ data.customer = req.user.id;
15
+ } else if (data.customer) {
16
+ return Response.json({
17
+ error: 'Anonymous bookings cannot set a customer'
18
+ }, {
19
+ status: 403
20
+ });
13
21
  }
14
22
  }
15
23
  // Create via Payload Local API — collection hooks handle conflict detection,
16
- // endTime calculation, and status transitions
24
+ // endTime calculation, status transitions, AND the beforeBookingCreate
25
+ // plugin hooks (running them here too made them fire twice per booking).
17
26
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
27
  const reservation = await req.payload.create({
19
28
  collection: config.slugs.reservations,
20
- data: bookingData,
29
+ data,
21
30
  req
22
31
  });
23
32
  // Never expose the cancellation token in the HTTP response — it is delivered
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/createBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nexport function createBookingEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const data = (await req.json?.()) as Record<string, unknown>\n\n // Call beforeBookingCreate plugin hooks before creating the reservation\n let bookingData = data\n if (config.hooks?.beforeBookingCreate) {\n for (const hook of config.hooks.beforeBookingCreate) {\n bookingData = (await hook({ data: bookingData, req })) ?? bookingData\n }\n }\n\n // Create via Payload Local API — collection hooks handle conflict detection,\n // endTime calculation, and status transitions\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const reservation = await (req.payload.create as any)({\n collection: config.slugs.reservations,\n data: bookingData,\n req,\n })\n\n // Never expose the cancellation token in the HTTP response — it is delivered\n // to the guest by the host project via the afterBookingCreate hook.\n const { cancellationToken: _cancellationToken, ...safeReservation } =\n reservation as Record<string, unknown>\n\n return Response.json(safeReservation, { status: 201 })\n },\n method: 'post',\n path: '/reserve/book',\n }\n}\n"],"names":["createBookingEndpoint","config","handler","req","data","json","bookingData","hooks","beforeBookingCreate","hook","reservation","payload","create","collection","slugs","reservations","cancellationToken","_cancellationToken","safeReservation","Response","status","method","path"],"mappings":"AAIA,OAAO,SAASA,sBAAsBC,MAAuC;IAC3E,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,OAAQ,MAAMD,IAAIE,IAAI;YAE5B,wEAAwE;YACxE,IAAIC,cAAcF;YAClB,IAAIH,OAAOM,KAAK,EAAEC,qBAAqB;gBACrC,KAAK,MAAMC,QAAQR,OAAOM,KAAK,CAACC,mBAAmB,CAAE;oBACnDF,cAAc,AAAC,MAAMG,KAAK;wBAAEL,MAAME;wBAAaH;oBAAI,MAAOG;gBAC5D;YACF;YAEA,6EAA6E;YAC7E,8CAA8C;YAC9C,8DAA8D;YAC9D,MAAMI,cAAc,MAAM,AAACP,IAAIQ,OAAO,CAACC,MAAM,CAAS;gBACpDC,YAAYZ,OAAOa,KAAK,CAACC,YAAY;gBACrCX,MAAME;gBACNH;YACF;YAEA,6EAA6E;YAC7E,oEAAoE;YACpE,MAAM,EAAEa,mBAAmBC,kBAAkB,EAAE,GAAGC,iBAAiB,GACjER;YAEF,OAAOS,SAASd,IAAI,CAACa,iBAAiB;gBAAEE,QAAQ;YAAI;QACtD;QACAC,QAAQ;QACRC,MAAM;IACR;AACF"}
1
+ {"version":3,"sources":["../../src/endpoints/createBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { isPrivilegedUser } from '../utilities/userRoles.js'\n\nexport function createBookingEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const data = (await req.json?.()) as Record<string, unknown>\n\n // Cancellation tokens are server-generated secrets never accept one\n // from the request body.\n delete data.cancellationToken\n\n // Who may book for whom: staff/admin for anyone (walk-ins); an\n // authenticated customer only for themselves; anonymous callers never\n // for an existing customer record (the guest flow covers them).\n if (!isPrivilegedUser(req.user, config)) {\n if (req.user) {\n data.customer = req.user.id\n } else if (data.customer) {\n return Response.json(\n { error: 'Anonymous bookings cannot set a customer' },\n { status: 403 },\n )\n }\n }\n\n // Create via Payload Local API — collection hooks handle conflict detection,\n // endTime calculation, status transitions, AND the beforeBookingCreate\n // plugin hooks (running them here too made them fire twice per booking).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const reservation = await (req.payload.create as any)({\n collection: config.slugs.reservations,\n data,\n req,\n })\n\n // Never expose the cancellation token in the HTTP response — it is delivered\n // to the guest by the host project via the afterBookingCreate hook.\n const { cancellationToken: _cancellationToken, ...safeReservation } =\n reservation as Record<string, unknown>\n\n return Response.json(safeReservation, { status: 201 })\n },\n method: 'post',\n path: '/reserve/book',\n }\n}\n"],"names":["isPrivilegedUser","createBookingEndpoint","config","handler","req","data","json","cancellationToken","user","customer","id","Response","error","status","reservation","payload","create","collection","slugs","reservations","_cancellationToken","safeReservation","method","path"],"mappings":"AAIA,SAASA,gBAAgB,QAAQ,4BAA2B;AAE5D,OAAO,SAASC,sBAAsBC,MAAuC;IAC3E,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,OAAQ,MAAMD,IAAIE,IAAI;YAE5B,sEAAsE;YACtE,yBAAyB;YACzB,OAAOD,KAAKE,iBAAiB;YAE7B,+DAA+D;YAC/D,sEAAsE;YACtE,gEAAgE;YAChE,IAAI,CAACP,iBAAiBI,IAAII,IAAI,EAAEN,SAAS;gBACvC,IAAIE,IAAII,IAAI,EAAE;oBACZH,KAAKI,QAAQ,GAAGL,IAAII,IAAI,CAACE,EAAE;gBAC7B,OAAO,IAAIL,KAAKI,QAAQ,EAAE;oBACxB,OAAOE,SAASL,IAAI,CAClB;wBAAEM,OAAO;oBAA2C,GACpD;wBAAEC,QAAQ;oBAAI;gBAElB;YACF;YAEA,6EAA6E;YAC7E,uEAAuE;YACvE,yEAAyE;YACzE,8DAA8D;YAC9D,MAAMC,cAAc,MAAM,AAACV,IAAIW,OAAO,CAACC,MAAM,CAAS;gBACpDC,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCd;gBACAD;YACF;YAEA,6EAA6E;YAC7E,oEAAoE;YACpE,MAAM,EAAEG,mBAAmBa,kBAAkB,EAAE,GAAGC,iBAAiB,GACjEP;YAEF,OAAOH,SAASL,IAAI,CAACe,iBAAiB;gBAAER,QAAQ;YAAI;QACtD;QACAS,QAAQ;QACRC,MAAM;IACR;AACF"}
@@ -33,8 +33,11 @@ export function createCustomerSearchEndpoint(config) {
33
33
  }
34
34
  const url = new URL(req.url);
35
35
  const search = url.searchParams.get('search') ?? '';
36
- const limit = Math.min(Number(url.searchParams.get('limit') ?? '10'), 50);
37
- const page = Math.max(Number(url.searchParams.get('page') ?? '1'), 1);
36
+ const limitRaw = Number(url.searchParams.get('limit') ?? '10');
37
+ const pageRaw = Number(url.searchParams.get('page') ?? '1');
38
+ // Non-numeric input falls back to defaults instead of passing NaN to the DB
39
+ const limit = Number.isFinite(limitRaw) ? Math.min(Math.max(Math.floor(limitRaw), 1), 50) : 10;
40
+ const page = Number.isFinite(pageRaw) ? Math.max(Math.floor(pageRaw), 1) : 1;
38
41
  // Detect which fields exist on the target collection at runtime
39
42
  const collectionConfig = req.payload.collections[config.slugs.customers]?.config;
40
43
  const availableFields = collectionConfig ? getNamedFields(collectionConfig.fields) : new Set();