payload-reserve 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +217 -4
  2. package/dist/collections/Holds.d.ts +18 -0
  3. package/dist/collections/Holds.js +113 -0
  4. package/dist/collections/Holds.js.map +1 -0
  5. package/dist/collections/Reservations.js +8 -0
  6. package/dist/collections/Reservations.js.map +1 -1
  7. package/dist/collections/Resources.js +35 -1
  8. package/dist/collections/Resources.js.map +1 -1
  9. package/dist/collections/Services.js +41 -0
  10. package/dist/collections/Services.js.map +1 -1
  11. package/dist/components/CalendarView/LaneTimelineView.js +4 -5
  12. package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
  13. package/dist/components/CalendarView/index.js +55 -71
  14. package/dist/components/CalendarView/index.js.map +1 -1
  15. package/dist/components/DashboardWidget/DashboardWidgetServer.js +11 -74
  16. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  17. package/dist/components/DashboardWidget/fetchDashboardStats.d.ts +42 -0
  18. package/dist/components/DashboardWidget/fetchDashboardStats.js +113 -0
  19. package/dist/components/DashboardWidget/fetchDashboardStats.js.map +1 -0
  20. package/dist/defaults.d.ts +2 -0
  21. package/dist/defaults.js +11 -0
  22. package/dist/defaults.js.map +1 -1
  23. package/dist/endpoints/cancelBooking.js +43 -14
  24. package/dist/endpoints/cancelBooking.js.map +1 -1
  25. package/dist/endpoints/checkAvailability.js +12 -1
  26. package/dist/endpoints/checkAvailability.js.map +1 -1
  27. package/dist/endpoints/createBooking.js +108 -7
  28. package/dist/endpoints/createBooking.js.map +1 -1
  29. package/dist/endpoints/customerSearch.js +6 -0
  30. package/dist/endpoints/customerSearch.js.map +1 -1
  31. package/dist/endpoints/effectiveTimezone.js +1 -0
  32. package/dist/endpoints/effectiveTimezone.js.map +1 -1
  33. package/dist/endpoints/getSlots.js +12 -1
  34. package/dist/endpoints/getSlots.js.map +1 -1
  35. package/dist/endpoints/holdSlot.d.ts +12 -0
  36. package/dist/endpoints/holdSlot.js +126 -0
  37. package/dist/endpoints/holdSlot.js.map +1 -0
  38. package/dist/endpoints/releaseSlot.d.ts +8 -0
  39. package/dist/endpoints/releaseSlot.js +34 -0
  40. package/dist/endpoints/releaseSlot.js.map +1 -0
  41. package/dist/endpoints/resourceAvailability.d.ts +2 -0
  42. package/dist/endpoints/resourceAvailability.js +79 -3
  43. package/dist/endpoints/resourceAvailability.js.map +1 -1
  44. package/dist/hooks/holds/validateHoldSlot.d.ts +14 -0
  45. package/dist/hooks/holds/validateHoldSlot.js +90 -0
  46. package/dist/hooks/holds/validateHoldSlot.js.map +1 -0
  47. package/dist/hooks/reservations/acquireBookingLock.d.ts +30 -0
  48. package/dist/hooks/reservations/acquireBookingLock.js +71 -0
  49. package/dist/hooks/reservations/acquireBookingLock.js.map +1 -0
  50. package/dist/hooks/reservations/calculateEndTime.js +13 -1
  51. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  52. package/dist/hooks/reservations/expandRequiredResources.js +3 -0
  53. package/dist/hooks/reservations/expandRequiredResources.js.map +1 -1
  54. package/dist/hooks/reservations/validateActive.d.ts +11 -0
  55. package/dist/hooks/reservations/validateActive.js +103 -0
  56. package/dist/hooks/reservations/validateActive.js.map +1 -0
  57. package/dist/hooks/reservations/validateConflicts.js +7 -1
  58. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  59. package/dist/hooks/reservations/validateGuestBooking.js +3 -0
  60. package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
  61. package/dist/hooks/shared/preventDeleteWhenReferenced.d.ts +42 -0
  62. package/dist/hooks/shared/preventDeleteWhenReferenced.js +87 -0
  63. package/dist/hooks/shared/preventDeleteWhenReferenced.js.map +1 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/plugin.js +179 -3
  67. package/dist/plugin.js.map +1 -1
  68. package/dist/services/AvailabilityService.d.ts +46 -5
  69. package/dist/services/AvailabilityService.js +168 -12
  70. package/dist/services/AvailabilityService.js.map +1 -1
  71. package/dist/services/HoldService.d.ts +53 -0
  72. package/dist/services/HoldService.js +169 -0
  73. package/dist/services/HoldService.js.map +1 -0
  74. package/dist/services/index.d.ts +1 -0
  75. package/dist/services/index.js.map +1 -1
  76. package/dist/translations/ar.json +5 -1
  77. package/dist/translations/de.json +5 -1
  78. package/dist/translations/en.json +5 -1
  79. package/dist/translations/es.json +5 -1
  80. package/dist/translations/fa.json +5 -1
  81. package/dist/translations/fr.json +5 -1
  82. package/dist/translations/hi.json +5 -1
  83. package/dist/translations/id.json +5 -1
  84. package/dist/translations/pl.json +5 -1
  85. package/dist/translations/ru.json +5 -1
  86. package/dist/translations/tr.json +5 -1
  87. package/dist/translations/zh.json +5 -1
  88. package/dist/types.d.ts +29 -0
  89. package/dist/types.js.map +1 -1
  90. package/dist/utilities/calendarGrid.d.ts +44 -0
  91. package/dist/utilities/calendarGrid.js +67 -0
  92. package/dist/utilities/calendarGrid.js.map +1 -0
  93. package/dist/utilities/reservationChanges.d.ts +6 -1
  94. package/dist/utilities/reservationChanges.js +1 -1
  95. package/dist/utilities/reservationChanges.js.map +1 -1
  96. package/dist/utilities/resolveReservationItems.d.ts +13 -2
  97. package/dist/utilities/resolveReservationItems.js +87 -2
  98. package/dist/utilities/resolveReservationItems.js.map +1 -1
  99. package/dist/utilities/retryOnWriteConflict.d.ts +66 -0
  100. package/dist/utilities/retryOnWriteConflict.js +135 -0
  101. package/dist/utilities/retryOnWriteConflict.js.map +1 -0
  102. package/dist/utilities/tenantTimezone.d.ts +77 -1
  103. package/dist/utilities/tenantTimezone.js +163 -2
  104. package/dist/utilities/tenantTimezone.js.map +1 -1
  105. package/dist/utilities/transactionSupport.d.ts +13 -0
  106. package/dist/utilities/transactionSupport.js +30 -0
  107. package/dist/utilities/transactionSupport.js.map +1 -0
  108. package/package.json +13 -13
@@ -1 +1 @@
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 { getEffectiveTenantTimezone } from '../../utilities/tenantTimezone.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; timezoneField?: string }\n | undefined) ?? {}\n const cookieName = tenantConfig.cookieName ?? 'payload-tenant'\n const tenantField = tenantConfig.tenantField ?? 'tenant'\n const timezoneField = tenantConfig.timezoneField ?? 'timezone'\n const reservationsCollection = payload.config.collections?.find((c) => c.slug === slugs.reservations)\n const tenantId = readCookie(req.headers.get('cookie'), cookieName)\n const tenantWhere = tenantWhereClause({\n hasField: collectionHasTenantField(reservationsCollection as { fields?: unknown[] } | undefined, tenantField),\n tenantField,\n tenantId,\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 — and in\n // multiTenant mode that's the SELECTED tenant's zone (tenant → global → UTC).\n const reservationTimezone: string = await getEffectiveTenantTimezone({\n globalTimezone: payload.config.admin?.custom?.reservationTimezone ?? 'UTC',\n payload,\n scopedCollection: reservationsCollection as { fields?: unknown[] } | undefined,\n tenantField,\n tenantId,\n timezoneField,\n })\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","getEffectiveTenantTimezone","addDaysToDayKey","combineDayKeyAndTime","getDayKeyInTimezone","styles","DashboardWidgetServer","props","req","i18n","payload","t","slugs","config","admin","custom","reservationSlugs","tenantConfig","reservationTenant","cookieName","tenantField","timezoneField","reservationsCollection","collections","find","c","slug","reservations","tenantId","headers","get","tenantWhere","hasField","statusMachine","reservationStatusMachine","blockingStatuses","terminalStatuses","blockingSet","Set","terminalSet","reservationTimezone","globalTimezone","scopedCollection","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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"}
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 { getEffectiveTenantTimezone } from '../../utilities/tenantTimezone.js'\nimport {\n addDaysToDayKey,\n combineDayKeyAndTime,\n getDayKeyInTimezone,\n} from '../../utilities/timezoneUtils.js'\nimport styles from './DashboardWidget.module.css'\nimport { fetchDashboardStats } from './fetchDashboardStats.js'\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; timezoneField?: string }\n | undefined) ?? {}\n const cookieName = tenantConfig.cookieName ?? 'payload-tenant'\n const tenantField = tenantConfig.tenantField ?? 'tenant'\n const timezoneField = tenantConfig.timezoneField ?? 'timezone'\n const reservationsCollection = payload.config.collections?.find((c) => c.slug === slugs.reservations)\n const tenantId = readCookie(req.headers.get('cookie'), cookieName)\n const tenantWhere = tenantWhereClause({\n hasField: collectionHasTenantField(reservationsCollection as { fields?: unknown[] } | undefined, tenantField),\n tenantField,\n tenantId,\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\n // \"Today\" is the business timezone's calendar day, not the server's — and in\n // multiTenant mode that's the SELECTED tenant's zone (tenant → global → UTC).\n const reservationTimezone: string = await getEffectiveTenantTimezone({\n globalTimezone: payload.config.admin?.custom?.reservationTimezone ?? 'UTC',\n payload,\n req,\n scopedCollection: reservationsCollection as { fields?: unknown[] } | undefined,\n tenantField,\n tenantId,\n timezoneField,\n })\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 const { active, nextAppointment, terminal, total, upcoming } = await fetchDashboardStats({\n blockingStatuses,\n now,\n payload,\n req,\n reservationsSlug: slugs.reservations,\n terminalStatuses,\n where,\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 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","getEffectiveTenantTimezone","addDaysToDayKey","combineDayKeyAndTime","getDayKeyInTimezone","styles","fetchDashboardStats","DashboardWidgetServer","props","req","i18n","payload","t","slugs","config","admin","custom","reservationSlugs","tenantConfig","reservationTenant","cookieName","tenantField","timezoneField","reservationsCollection","collections","find","c","slug","reservations","tenantId","headers","get","tenantWhere","hasField","statusMachine","reservationStatusMachine","blockingStatuses","terminalStatuses","reservationTimezone","globalTimezone","scopedCollection","now","Date","todayKey","startOfDay","endOfDay","where","startTime","greater_than_equal","toISOString","less_than","Object","assign","active","nextAppointment","terminal","total","upcoming","reservationsSlug","div","className","wrapper","h3","title","statsGrid","statCard","span","statValue","statLabel","strong","p","toLocaleTimeString","hour","minute","timeZone","status","noData"],"mappings":";AAKA,SAASA,wBAAwB,EAAEC,UAAU,EAAEC,iBAAiB,QAAQ,kCAAiC;AACzG,SAASC,0BAA0B,QAAQ,oCAAmC;AAC9E,SACEC,eAAe,EACfC,oBAAoB,EACpBC,mBAAmB,QACd,mCAAkC;AACzC,OAAOC,YAAY,+BAA8B;AACjD,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,OAAO,MAAMC,wBAAwB,OAAOC;IAC1C,MAAM,EAAEC,GAAG,EAAE,GAAGD;IAChB,MAAM,EAAEE,IAAI,EAAEC,OAAO,EAAE,GAAGF;IAC1B,MAAMG,IAAIF,KAAKE,CAAC;IAEhB,MAAMC,QAAQF,QAAQG,MAAM,CAACC,KAAK,EAAEC,QAAQC;IAC5C,IAAI,CAACJ,OAAO;QACV,OAAO;IACT;IAEA,MAAMK,eACJ,AAACP,QAAQG,MAAM,CAACC,KAAK,EAAEC,QAAQG,qBAEb,CAAC;IACrB,MAAMC,aAAaF,aAAaE,UAAU,IAAI;IAC9C,MAAMC,cAAcH,aAAaG,WAAW,IAAI;IAChD,MAAMC,gBAAgBJ,aAAaI,aAAa,IAAI;IACpD,MAAMC,yBAAyBZ,QAAQG,MAAM,CAACU,WAAW,EAAEC,KAAK,CAACC,IAAMA,EAAEC,IAAI,KAAKd,MAAMe,YAAY;IACpG,MAAMC,WAAW9B,WAAWU,IAAIqB,OAAO,CAACC,GAAG,CAAC,WAAWX;IACvD,MAAMY,cAAchC,kBAAkB;QACpCiC,UAAUnC,yBAAyByB,wBAA8DF;QACjGA;QACAQ;IACF;IAEA,iEAAiE;IACjE,MAAMK,gBACJvB,QAAQG,MAAM,CAACC,KAAK,EAAEC,QAAQmB;IAChC,MAAMC,mBAA6BF,eAAeE,oBAAoB,EAAE;IACxE,MAAMC,mBAA6BH,eAAeG,oBAAoB,EAAE;IAExE,6EAA6E;IAC7E,8EAA8E;IAC9E,MAAMC,sBAA8B,MAAMrC,2BAA2B;QACnEsC,gBAAgB5B,QAAQG,MAAM,CAACC,KAAK,EAAEC,QAAQsB,uBAAuB;QACrE3B;QACAF;QACA+B,kBAAkBjB;QAClBF;QACAQ;QACAP;IACF;IACA,MAAMmB,MAAM,IAAIC;IAChB,MAAMC,WAAWvC,oBAAoBqC,KAAKH;IAC1C,MAAMM,aAAazC,qBAAqBwC,UAAU,SAASL;IAC3D,MAAMO,WAAW1C,qBAAqBD,gBAAgByC,UAAU,IAAI,SAASL;IAE7E,MAAMQ,QAAe;QACnBC,WAAW;YACTC,oBAAoBJ,WAAWK,WAAW;YAC1CC,WAAWL,SAASI,WAAW;QACjC;IACF;IACA,IAAIjB,aAAa;QACfmB,OAAOC,MAAM,CAACN,OAAOd;IACvB;IAEA,MAAM,EAAEqB,MAAM,EAAEC,eAAe,EAAEC,QAAQ,EAAEC,KAAK,EAAEC,QAAQ,EAAE,GAAG,MAAMnD,oBAAoB;QACvF8B;QACAK;QACA9B;QACAF;QACAiD,kBAAkB7C,MAAMe,YAAY;QACpCS;QACAS;IACF;IAEA,qBACE,MAACa;QAAIC,WAAWvD,OAAOwD,OAAO;;0BAC5B,KAACC;gBAAGF,WAAWvD,OAAO0D,KAAK;0BAAGnD,EAAE;;0BAChC,MAAC+C;gBAAIC,WAAWvD,OAAO2D,SAAS;;kCAC9B,MAACL;wBAAIC,WAAWvD,OAAO4D,QAAQ;;0CAC7B,KAACC;gCAAKN,WAAWvD,OAAO8D,SAAS;0CAAGX;;0CACpC,KAACU;gCAAKN,WAAWvD,OAAO+D,SAAS;0CAAGxD,EAAE;;;;kCAExC,MAAC+C;wBAAIC,WAAWvD,OAAO4D,QAAQ;;0CAC7B,KAACC;gCAAKN,WAAWvD,OAAO8D,SAAS;0CAAGd;;0CACpC,KAACa;gCAAKN,WAAWvD,OAAO+D,SAAS;0CAAGxD,EAAE;;;;kCAExC,MAAC+C;wBAAIC,WAAWvD,OAAO4D,QAAQ;;0CAC7B,KAACC;gCAAKN,WAAWvD,OAAO8D,SAAS;0CAAGV;;0CACpC,KAACS;gCAAKN,WAAWvD,OAAO+D,SAAS;0CAAGxD,EAAE;;;;kCAExC,MAAC+C;wBAAIC,WAAWvD,OAAO4D,QAAQ;;0CAC7B,KAACC;gCAAKN,WAAWvD,OAAO8D,SAAS;0CAAGZ;;0CACpC,KAACW;gCAAKN,WAAWvD,OAAO+D,SAAS;0CAAGxD,EAAE;;;;;;YAGzC0C,gCACC,MAACK;gBAAIC,WAAWvD,OAAOiD,eAAe;;kCACpC,KAACe;kCAAQzD,EAAE;;kCACX,MAAC0D;;4BACE1D,EAAE;4BAA8B;4BAChC,IAAI8B,KAAKY,gBAAgBP,SAAS,EAAYwB,kBAAkB,CAAC,EAAE,EAAE;gCACpEC,MAAM;gCACNC,QAAQ;gCACRC,UAAUpC;4BACZ;;;kCAEF,MAACgC;;4BACE1D,EAAE;4BAA+B;4BAAE0C,gBAAgBqB,MAAM;;;;+BAI9D,KAACL;gBAAEV,WAAWvD,OAAOuE,MAAM;0BAAGhE,EAAE;;;;AAIxC,EAAC"}
@@ -0,0 +1,42 @@
1
+ import type { Payload, PayloadRequest, Where } from 'payload';
2
+ export type DashboardStats = {
3
+ active: number;
4
+ nextAppointment: Record<string, unknown> | undefined;
5
+ terminal: number;
6
+ total: number;
7
+ upcoming: number;
8
+ };
9
+ /**
10
+ * The dashboard widget's five aggregate reads, split out of the RSC so they can
11
+ * be tested against a real Payload instance — a React Server Component has no
12
+ * render harness in this repo.
13
+ *
14
+ * Every read is access-checked (`overrideAccess: false` + `req`). Counts are
15
+ * used rather than a capped fetch+filter so they stay accurate past 100
16
+ * reservations/day (review D7).
17
+ *
18
+ * `disableErrors: true` is load-bearing, not defensive noise. When an access
19
+ * function returns boolean `false` — multi-tenant's `withTenantAccess` does
20
+ * exactly that for a user with no tenant memberships, and any consumer-supplied
21
+ * `access.reservations.read` may too — Payload's `executeAccess` THROWS
22
+ * `Forbidden` unless errors are disabled. This helper runs inside an async React
23
+ * Server Component, so that throw is a dashboard render failure, not a status
24
+ * code. With the flag, `count`/`find` short-circuit to `{ totalDocs: 0 }` /
25
+ * `{ docs: [] }` and the widget renders zeros — which is the honest answer for a
26
+ * caller allowed to see nothing.
27
+ *
28
+ * `disableErrors` also widens any OTHER falsy access result (not just boolean
29
+ * `false`) to "no constraint". Payload's `AccessResult` type forbids returning
30
+ * one, and multi-tenant still layers its own constraint underneath, so this is
31
+ * theoretical — but do not remove the flag to close it. Removing it trades a
32
+ * theoretical widening for the real Forbidden-throw crash above.
33
+ */
34
+ export declare function fetchDashboardStats(args: {
35
+ blockingStatuses: string[];
36
+ now: Date;
37
+ payload: Payload;
38
+ req: PayloadRequest;
39
+ reservationsSlug: string;
40
+ terminalStatuses: string[];
41
+ where: Where;
42
+ }): Promise<DashboardStats>;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The dashboard widget's five aggregate reads, split out of the RSC so they can
3
+ * be tested against a real Payload instance — a React Server Component has no
4
+ * render harness in this repo.
5
+ *
6
+ * Every read is access-checked (`overrideAccess: false` + `req`). Counts are
7
+ * used rather than a capped fetch+filter so they stay accurate past 100
8
+ * reservations/day (review D7).
9
+ *
10
+ * `disableErrors: true` is load-bearing, not defensive noise. When an access
11
+ * function returns boolean `false` — multi-tenant's `withTenantAccess` does
12
+ * exactly that for a user with no tenant memberships, and any consumer-supplied
13
+ * `access.reservations.read` may too — Payload's `executeAccess` THROWS
14
+ * `Forbidden` unless errors are disabled. This helper runs inside an async React
15
+ * Server Component, so that throw is a dashboard render failure, not a status
16
+ * code. With the flag, `count`/`find` short-circuit to `{ totalDocs: 0 }` /
17
+ * `{ docs: [] }` and the widget renders zeros — which is the honest answer for a
18
+ * caller allowed to see nothing.
19
+ *
20
+ * `disableErrors` also widens any OTHER falsy access result (not just boolean
21
+ * `false`) to "no constraint". Payload's `AccessResult` type forbids returning
22
+ * one, and multi-tenant still layers its own constraint underneath, so this is
23
+ * theoretical — but do not remove the flag to close it. Removing it trades a
24
+ * theoretical widening for the real Forbidden-throw crash above.
25
+ */ export async function fetchDashboardStats(args) {
26
+ const { blockingStatuses, now, payload, req, reservationsSlug, terminalStatuses, where } = args;
27
+ // The slug is resolved at runtime from admin.custom, so it is a plain string
28
+ // rather than a literal CollectionSlug — the same cast the rest of the plugin uses.
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ const collection = reservationsSlug;
31
+ const countWhere = (extra)=>extra ? {
32
+ and: [
33
+ where,
34
+ extra
35
+ ]
36
+ } : where;
37
+ const upcomingWhere = countWhere({
38
+ and: [
39
+ {
40
+ status: {
41
+ in: blockingStatuses
42
+ }
43
+ },
44
+ {
45
+ startTime: {
46
+ greater_than: now.toISOString()
47
+ }
48
+ }
49
+ ]
50
+ });
51
+ // Concurrency is safe despite the shared `req`: createLocalReq mutates it, but
52
+ // its only await sits behind `req?.i18n ||`, and a real request always carries
53
+ // i18n — so each call's mutations complete synchronously with no interleaving.
54
+ const [total, active, terminal, upcoming, nextResult] = await Promise.all([
55
+ payload.count({
56
+ collection,
57
+ disableErrors: true,
58
+ overrideAccess: false,
59
+ req,
60
+ where
61
+ }).then((r)=>r.totalDocs),
62
+ blockingStatuses.length ? payload.count({
63
+ collection,
64
+ disableErrors: true,
65
+ overrideAccess: false,
66
+ req,
67
+ where: countWhere({
68
+ status: {
69
+ in: blockingStatuses
70
+ }
71
+ })
72
+ }).then((r)=>r.totalDocs) : Promise.resolve(0),
73
+ terminalStatuses.length ? payload.count({
74
+ collection,
75
+ disableErrors: true,
76
+ overrideAccess: false,
77
+ req,
78
+ where: countWhere({
79
+ status: {
80
+ in: terminalStatuses
81
+ }
82
+ })
83
+ }).then((r)=>r.totalDocs) : Promise.resolve(0),
84
+ blockingStatuses.length ? payload.count({
85
+ collection,
86
+ disableErrors: true,
87
+ overrideAccess: false,
88
+ req,
89
+ where: upcomingWhere
90
+ }).then((r)=>r.totalDocs) : Promise.resolve(0),
91
+ blockingStatuses.length ? payload.find({
92
+ collection,
93
+ disableErrors: true,
94
+ limit: 1,
95
+ overrideAccess: false,
96
+ req,
97
+ sort: 'startTime',
98
+ where: upcomingWhere
99
+ }) : Promise.resolve({
100
+ docs: []
101
+ })
102
+ ]);
103
+ return {
104
+ active,
105
+ // Next appointment = the earliest upcoming blocking reservation
106
+ nextAppointment: nextResult.docs[0],
107
+ terminal,
108
+ total,
109
+ upcoming
110
+ };
111
+ }
112
+
113
+ //# sourceMappingURL=fetchDashboardStats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/DashboardWidget/fetchDashboardStats.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nexport type DashboardStats = {\n active: number\n nextAppointment: Record<string, unknown> | undefined\n terminal: number\n total: number\n upcoming: number\n}\n\n/**\n * The dashboard widget's five aggregate reads, split out of the RSC so they can\n * be tested against a real Payload instance — a React Server Component has no\n * render harness in this repo.\n *\n * Every read is access-checked (`overrideAccess: false` + `req`). Counts are\n * used rather than a capped fetch+filter so they stay accurate past 100\n * reservations/day (review D7).\n *\n * `disableErrors: true` is load-bearing, not defensive noise. When an access\n * function returns boolean `false` — multi-tenant's `withTenantAccess` does\n * exactly that for a user with no tenant memberships, and any consumer-supplied\n * `access.reservations.read` may too — Payload's `executeAccess` THROWS\n * `Forbidden` unless errors are disabled. This helper runs inside an async React\n * Server Component, so that throw is a dashboard render failure, not a status\n * code. With the flag, `count`/`find` short-circuit to `{ totalDocs: 0 }` /\n * `{ docs: [] }` and the widget renders zeros — which is the honest answer for a\n * caller allowed to see nothing.\n *\n * `disableErrors` also widens any OTHER falsy access result (not just boolean\n * `false`) to \"no constraint\". Payload's `AccessResult` type forbids returning\n * one, and multi-tenant still layers its own constraint underneath, so this is\n * theoretical — but do not remove the flag to close it. Removing it trades a\n * theoretical widening for the real Forbidden-throw crash above.\n */\nexport async function fetchDashboardStats(args: {\n blockingStatuses: string[]\n now: Date\n payload: Payload\n req: PayloadRequest\n reservationsSlug: string\n terminalStatuses: string[]\n where: Where\n}): Promise<DashboardStats> {\n const { blockingStatuses, now, payload, req, reservationsSlug, terminalStatuses, where } = args\n\n // The slug is resolved at runtime from admin.custom, so it is a plain string\n // rather than a literal CollectionSlug — the same cast the rest of the plugin uses.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const collection = reservationsSlug as any\n const countWhere = (extra?: Where): Where => (extra ? { and: [where, extra] } : where)\n const upcomingWhere = countWhere({\n and: [{ status: { in: blockingStatuses } }, { startTime: { greater_than: now.toISOString() } }],\n })\n\n // Concurrency is safe despite the shared `req`: createLocalReq mutates it, but\n // its only await sits behind `req?.i18n ||`, and a real request always carries\n // i18n — so each call's mutations complete synchronously with no interleaving.\n const [total, active, terminal, upcoming, nextResult] = await Promise.all([\n payload\n .count({ collection, disableErrors: true, overrideAccess: false, req, where })\n .then((r) => r.totalDocs),\n blockingStatuses.length\n ? payload\n .count({\n collection,\n disableErrors: true,\n overrideAccess: false,\n req,\n where: countWhere({ status: { in: blockingStatuses } }),\n })\n .then((r) => r.totalDocs)\n : Promise.resolve(0),\n terminalStatuses.length\n ? payload\n .count({\n collection,\n disableErrors: true,\n overrideAccess: false,\n req,\n where: countWhere({ status: { in: terminalStatuses } }),\n })\n .then((r) => r.totalDocs)\n : Promise.resolve(0),\n blockingStatuses.length\n ? payload\n .count({\n collection,\n disableErrors: true,\n overrideAccess: false,\n req,\n where: upcomingWhere,\n })\n .then((r) => r.totalDocs)\n : Promise.resolve(0),\n blockingStatuses.length\n ? payload.find({\n collection,\n disableErrors: true,\n limit: 1,\n overrideAccess: false,\n req,\n sort: 'startTime',\n where: upcomingWhere,\n })\n : Promise.resolve({ docs: [] as Record<string, unknown>[] }),\n ])\n\n return {\n active,\n // Next appointment = the earliest upcoming blocking reservation\n nextAppointment: nextResult.docs[0] as Record<string, unknown> | undefined,\n terminal,\n total,\n upcoming,\n }\n}\n"],"names":["fetchDashboardStats","args","blockingStatuses","now","payload","req","reservationsSlug","terminalStatuses","where","collection","countWhere","extra","and","upcomingWhere","status","in","startTime","greater_than","toISOString","total","active","terminal","upcoming","nextResult","Promise","all","count","disableErrors","overrideAccess","then","r","totalDocs","length","resolve","find","limit","sort","docs","nextAppointment"],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GACD,OAAO,eAAeA,oBAAoBC,IAQzC;IACC,MAAM,EAAEC,gBAAgB,EAAEC,GAAG,EAAEC,OAAO,EAAEC,GAAG,EAAEC,gBAAgB,EAAEC,gBAAgB,EAAEC,KAAK,EAAE,GAAGP;IAE3F,6EAA6E;IAC7E,oFAAoF;IACpF,8DAA8D;IAC9D,MAAMQ,aAAaH;IACnB,MAAMI,aAAa,CAACC,QAA0BA,QAAQ;YAAEC,KAAK;gBAACJ;gBAAOG;aAAM;QAAC,IAAIH;IAChF,MAAMK,gBAAgBH,WAAW;QAC/BE,KAAK;YAAC;gBAAEE,QAAQ;oBAAEC,IAAIb;gBAAiB;YAAE;YAAG;gBAAEc,WAAW;oBAAEC,cAAcd,IAAIe,WAAW;gBAAG;YAAE;SAAE;IACjG;IAEA,+EAA+E;IAC/E,+EAA+E;IAC/E,+EAA+E;IAC/E,MAAM,CAACC,OAAOC,QAAQC,UAAUC,UAAUC,WAAW,GAAG,MAAMC,QAAQC,GAAG,CAAC;QACxErB,QACGsB,KAAK,CAAC;YAAEjB;YAAYkB,eAAe;YAAMC,gBAAgB;YAAOvB;YAAKG;QAAM,GAC3EqB,IAAI,CAAC,CAACC,IAAMA,EAAEC,SAAS;QAC1B7B,iBAAiB8B,MAAM,GACnB5B,QACGsB,KAAK,CAAC;YACLjB;YACAkB,eAAe;YACfC,gBAAgB;YAChBvB;YACAG,OAAOE,WAAW;gBAAEI,QAAQ;oBAAEC,IAAIb;gBAAiB;YAAE;QACvD,GACC2B,IAAI,CAAC,CAACC,IAAMA,EAAEC,SAAS,IAC1BP,QAAQS,OAAO,CAAC;QACpB1B,iBAAiByB,MAAM,GACnB5B,QACGsB,KAAK,CAAC;YACLjB;YACAkB,eAAe;YACfC,gBAAgB;YAChBvB;YACAG,OAAOE,WAAW;gBAAEI,QAAQ;oBAAEC,IAAIR;gBAAiB;YAAE;QACvD,GACCsB,IAAI,CAAC,CAACC,IAAMA,EAAEC,SAAS,IAC1BP,QAAQS,OAAO,CAAC;QACpB/B,iBAAiB8B,MAAM,GACnB5B,QACGsB,KAAK,CAAC;YACLjB;YACAkB,eAAe;YACfC,gBAAgB;YAChBvB;YACAG,OAAOK;QACT,GACCgB,IAAI,CAAC,CAACC,IAAMA,EAAEC,SAAS,IAC1BP,QAAQS,OAAO,CAAC;QACpB/B,iBAAiB8B,MAAM,GACnB5B,QAAQ8B,IAAI,CAAC;YACXzB;YACAkB,eAAe;YACfQ,OAAO;YACPP,gBAAgB;YAChBvB;YACA+B,MAAM;YACN5B,OAAOK;QACT,KACAW,QAAQS,OAAO,CAAC;YAAEI,MAAM,EAAE;QAA8B;KAC7D;IAED,OAAO;QACLjB;QACA,gEAAgE;QAChEkB,iBAAiBf,WAAWc,IAAI,CAAC,EAAE;QACnChB;QACAF;QACAG;IACF;AACF"}
@@ -3,12 +3,14 @@ export declare const DEFAULT_RESOURCE_TYPES: string[];
3
3
  export declare const DEFAULT_LEAVE_TYPES: string[];
4
4
  export declare const DEFAULT_SLUGS: {
5
5
  readonly customers: "customers";
6
+ readonly holds: "reservation-holds";
6
7
  readonly media: "media";
7
8
  readonly reservations: "reservations";
8
9
  readonly resources: "resources";
9
10
  readonly schedules: "schedules";
10
11
  readonly services: "services";
11
12
  };
13
+ export declare const DEFAULT_HOLD_TTL_MINUTES = 10;
12
14
  export declare const DEFAULT_ADMIN_GROUP = "Reservations";
13
15
  export declare const DEFAULT_ALLOW_GUEST_BOOKING = false;
14
16
  export declare const DEFAULT_BUFFER_TIME = 0;
package/dist/defaults.js CHANGED
@@ -84,12 +84,14 @@ function resolveStaffProvisioning(pluginOptions, resourceTypes) {
84
84
  }
85
85
  export const DEFAULT_SLUGS = {
86
86
  customers: 'customers',
87
+ holds: 'reservation-holds',
87
88
  media: 'media',
88
89
  reservations: 'reservations',
89
90
  resources: 'resources',
90
91
  schedules: 'schedules',
91
92
  services: 'services'
92
93
  };
94
+ export const DEFAULT_HOLD_TTL_MINUTES = 10;
93
95
  export const DEFAULT_ADMIN_GROUP = 'Reservations';
94
96
  export const DEFAULT_ALLOW_GUEST_BOOKING = false;
95
97
  export const DEFAULT_BUFFER_TIME = 0;
@@ -119,10 +121,14 @@ export function resolveConfig(pluginOptions) {
119
121
  debug: pluginOptions.debug ?? false,
120
122
  defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,
121
123
  disabled: pluginOptions.disabled ?? false,
124
+ enforceActive: pluginOptions.enforceActive ?? true,
122
125
  extraReservationFields: pluginOptions.extraReservationFields ?? [],
123
126
  getExternalBusy: pluginOptions.getExternalBusy,
124
127
  // Real value is set by the plugin once config.collections is known (C8)
125
128
  hasMediaCollection: false,
129
+ // Real value is set by the plugin once Resources is built — gates the
130
+ // Services.resources join (see Step 3a).
131
+ hasResourceServicesField: false,
126
132
  hooks: pluginOptions.hooks ?? {},
127
133
  leaveTypes: pluginOptions.leaveTypes ?? DEFAULT_LEAVE_TYPES,
128
134
  localized: false,
@@ -139,8 +145,13 @@ export function resolveConfig(pluginOptions) {
139
145
  roleField: rom.roleField ?? pluginOptions.staffProvisioning?.roleField ?? 'role'
140
146
  } : undefined,
141
147
  resourceTypes,
148
+ slotHolds: {
149
+ enabled: !disabled && (pluginOptions.slotHolds?.enabled ?? false),
150
+ ttlMinutes: pluginOptions.slotHolds?.ttlMinutes ?? DEFAULT_HOLD_TTL_MINUTES
151
+ },
142
152
  slugs: {
143
153
  customers: pluginOptions.slugs?.customers ?? DEFAULT_SLUGS.customers,
154
+ holds: pluginOptions.slugs?.holds ?? DEFAULT_SLUGS.holds,
144
155
  media: pluginOptions.slugs?.media ?? DEFAULT_SLUGS.media,
145
156
  reservations: pluginOptions.slugs?.reservations ?? DEFAULT_SLUGS.reservations,
146
157
  resources: pluginOptions.slugs?.resources ?? DEFAULT_SLUGS.resources,
@@ -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'\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 debug: pluginOptions.debug ?? false,\n defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,\n disabled: pluginOptions.disabled ?? false,\n extraReservationFields: pluginOptions.extraReservationFields ?? [],\n getExternalBusy: pluginOptions.getExternalBusy,\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 timezoneField: pluginOptions.multiTenant?.timezoneField ?? 'timezone',\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","debug","defaultBufferTime","extraReservationFields","getExternalBusy","hasMediaCollection","hooks","localized","multiTenant","cookieName","tenantField","timezoneField","adminRoles","ownedServices","ownerCollection","ownerField","slugs","timezone"],"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 holds: 'reservation-holds',\n media: 'media',\n reservations: 'reservations',\n resources: 'resources',\n schedules: 'schedules',\n services: 'services',\n} as const\n\nexport const DEFAULT_HOLD_TTL_MINUTES = 10\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 debug: pluginOptions.debug ?? false,\n defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,\n disabled: pluginOptions.disabled ?? false,\n enforceActive: pluginOptions.enforceActive ?? true,\n extraReservationFields: pluginOptions.extraReservationFields ?? [],\n getExternalBusy: pluginOptions.getExternalBusy,\n // Real value is set by the plugin once config.collections is known (C8)\n hasMediaCollection: false,\n // Real value is set by the plugin once Resources is built — gates the\n // Services.resources join (see Step 3a).\n hasResourceServicesField: 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 timezoneField: pluginOptions.multiTenant?.timezoneField ?? 'timezone',\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 slotHolds: {\n enabled: !disabled && (pluginOptions.slotHolds?.enabled ?? false),\n ttlMinutes: pluginOptions.slotHolds?.ttlMinutes ?? DEFAULT_HOLD_TTL_MINUTES,\n },\n slugs: {\n customers: pluginOptions.slugs?.customers ?? DEFAULT_SLUGS.customers,\n holds: pluginOptions.slugs?.holds ?? DEFAULT_SLUGS.holds,\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","holds","media","reservations","resources","schedules","services","DEFAULT_HOLD_TTL_MINUTES","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","debug","defaultBufferTime","enforceActive","extraReservationFields","getExternalBusy","hasMediaCollection","hasResourceServicesField","hooks","localized","multiTenant","cookieName","tenantField","timezoneField","adminRoles","ownedServices","ownerCollection","ownerField","slotHolds","enabled","ttlMinutes","slugs","timezone"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { isTransientWriteConflict, retryOnWriteConflict } from '../utilities/retryOnWriteConflict.js';
1
2
  import { isPrivilegedUser } from '../utilities/userRoles.js';
2
3
  export function createCancelBookingEndpoint(config) {
3
4
  return {
@@ -20,6 +21,13 @@ export function createCancelBookingEndpoint(config) {
20
21
  overrideAccess: true,
21
22
  req
22
23
  });
24
+ // Chosen per authorization path, then applied to the update below:
25
+ // - guest with a matching token: the token IS the authorization
26
+ // - owner: owner-mode's `update: adminOnly` would otherwise wrongly
27
+ // block a customer cancelling their own booking
28
+ // - privileged non-owner: delegate, so owner-mode AND multi-tenant
29
+ // isolation both apply
30
+ let delegateAccess = false;
23
31
  if (req.user) {
24
32
  // Authenticated path: owner (customer === req.user) or admin/staff.
25
33
  const customerId = typeof existing.customer === 'object' ? existing.customer?.id : existing.customer;
@@ -33,6 +41,7 @@ export function createCancelBookingEndpoint(config) {
33
41
  status: 403
34
42
  });
35
43
  }
44
+ delegateAccess = !isOwner;
36
45
  } else {
37
46
  // Guest path: match the cancellation token.
38
47
  if (!token || !existing.cancellationToken || token !== existing.cancellationToken) {
@@ -43,20 +52,40 @@ export function createCancelBookingEndpoint(config) {
43
52
  });
44
53
  }
45
54
  }
46
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
- const reservation = await req.payload.update({
48
- id: reservationId,
49
- collection: config.slugs.reservations,
50
- data: {
51
- cancellationReason: reason,
52
- status: config.statusMachine.cancelStatus
53
- },
54
- // Authorization (owner / admin / matching token) is already enforced above.
55
- // overrideAccess lets the write proceed for anonymous guest cancellations,
56
- // and avoids a spurious 500 when resourceOwnerMode restricts update access.
57
- overrideAccess: true,
58
- req
59
- });
55
+ let reservation;
56
+ try {
57
+ reservation = await retryOnWriteConflict(()=>// eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ req.payload.update({
59
+ id: reservationId,
60
+ collection: config.slugs.reservations,
61
+ data: {
62
+ cancellationReason: reason,
63
+ status: config.statusMachine.cancelStatus
64
+ },
65
+ overrideAccess: !delegateAccess,
66
+ req
67
+ }), {
68
+ req
69
+ });
70
+ } catch (err) {
71
+ if (isTransientWriteConflict(err)) {
72
+ return Response.json({
73
+ error: 'That booking is being modified. Please try again.',
74
+ retryable: true
75
+ }, {
76
+ status: 409
77
+ });
78
+ }
79
+ // A denied delegate write is an authorization failure, not a 500.
80
+ if (err?.status === 403) {
81
+ return Response.json({
82
+ message: 'Forbidden'
83
+ }, {
84
+ status: 403
85
+ });
86
+ }
87
+ throw err;
88
+ }
60
89
  // Strip the cancellation token from the response, consistent with the book
61
90
  // endpoint — it must never be echoed back over HTTP.
62
91
  const { cancellationToken: _cancellationToken, ...safeReservation } = reservation;
@@ -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: 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
+ {"version":3,"sources":["../../src/endpoints/cancelBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport {\n isTransientWriteConflict,\n retryOnWriteConflict,\n} from '../utilities/retryOnWriteConflict.js'\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 // Chosen per authorization path, then applied to the update below:\n // - guest with a matching token: the token IS the authorization\n // - owner: owner-mode's `update: adminOnly` would otherwise wrongly\n // block a customer cancelling their own booking\n // - privileged non-owner: delegate, so owner-mode AND multi-tenant\n // isolation both apply\n let delegateAccess = false\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 delegateAccess = !isOwner\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 let reservation: Record<string, unknown>\n try {\n reservation = await retryOnWriteConflict(\n () =>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (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 overrideAccess: !delegateAccess,\n req,\n }) as Promise<Record<string, unknown>>,\n { req },\n )\n } catch (err) {\n if (isTransientWriteConflict(err)) {\n return Response.json(\n { error: 'That booking is being modified. Please try again.', retryable: true },\n { status: 409 },\n )\n }\n // A denied delegate write is an authorization failure, not a 500.\n if ((err as { status?: number })?.status === 403) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n throw err\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 } = reservation\n\n return Response.json(safeReservation)\n },\n method: 'post',\n path: '/reserve/cancel',\n }\n}\n"],"names":["isTransientWriteConflict","retryOnWriteConflict","isPrivilegedUser","createCancelBookingEndpoint","config","handler","req","body","json","reason","reservationId","token","Response","message","status","existing","payload","findByID","id","collection","slugs","reservations","depth","overrideAccess","delegateAccess","user","customerId","customer","isOwner","isAdmin","cancellationToken","reservation","update","data","cancellationReason","statusMachine","cancelStatus","err","error","retryable","_cancellationToken","safeReservation","method","path"],"mappings":"AAIA,SACEA,wBAAwB,EACxBC,oBAAoB,QACf,uCAAsC;AAC7C,SAASC,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,mEAAmE;YACnE,iEAAiE;YACjE,qEAAqE;YACrE,mDAAmD;YACnD,oEAAoE;YACpE,0BAA0B;YAC1B,IAAIkB,iBAAiB;YAErB,IAAIlB,IAAImB,IAAI,EAAE;gBACZ,oEAAoE;gBACpE,MAAMC,aACJ,OAAOX,SAASY,QAAQ,KAAK,WAAWZ,SAASY,QAAQ,EAAET,KAAKH,SAASY,QAAQ;gBACnF,MAAMC,UAAUF,eAAepB,IAAImB,IAAI,CAACP,EAAE;gBAC1C,uEAAuE;gBACvE,MAAMW,UAAU3B,iBAAiBI,IAAImB,IAAI,EAAErB;gBAC3C,IAAI,CAACwB,WAAW,CAACC,SAAS;oBACxB,OAAOjB,SAASJ,IAAI,CAAC;wBAAEK,SAAS;oBAAY,GAAG;wBAAEC,QAAQ;oBAAI;gBAC/D;gBACAU,iBAAiB,CAACI;YACpB,OAAO;gBACL,4CAA4C;gBAC5C,IAAI,CAACjB,SAAS,CAACI,SAASe,iBAAiB,IAAInB,UAAUI,SAASe,iBAAiB,EAAE;oBACjF,OAAOlB,SAASJ,IAAI,CAAC;wBAAEK,SAAS;oBAAY,GAAG;wBAAEC,QAAQ;oBAAI;gBAC/D;YACF;YAEA,IAAIiB;YACJ,IAAI;gBACFA,cAAc,MAAM9B,qBAClB,IAEE,AADA,8DAA8D;oBAC7DK,IAAIU,OAAO,CAACgB,MAAM,CAAS;wBAC1Bd,IAAIR;wBACJS,YAAYf,OAAOgB,KAAK,CAACC,YAAY;wBACrCY,MAAM;4BACJC,oBAAoBzB;4BACpBK,QAAQV,OAAO+B,aAAa,CAACC,YAAY;wBAC3C;wBACAb,gBAAgB,CAACC;wBACjBlB;oBACF,IACF;oBAAEA;gBAAI;YAEV,EAAE,OAAO+B,KAAK;gBACZ,IAAIrC,yBAAyBqC,MAAM;oBACjC,OAAOzB,SAASJ,IAAI,CAClB;wBAAE8B,OAAO;wBAAqDC,WAAW;oBAAK,GAC9E;wBAAEzB,QAAQ;oBAAI;gBAElB;gBACA,kEAAkE;gBAClE,IAAI,AAACuB,KAA6BvB,WAAW,KAAK;oBAChD,OAAOF,SAASJ,IAAI,CAAC;wBAAEK,SAAS;oBAAY,GAAG;wBAAEC,QAAQ;oBAAI;gBAC/D;gBACA,MAAMuB;YACR;YAEA,2EAA2E;YAC3E,qDAAqD;YACrD,MAAM,EAAEP,mBAAmBU,kBAAkB,EAAE,GAAGC,iBAAiB,GAAGV;YAEtE,OAAOnB,SAASJ,IAAI,CAACiC;QACvB;QACAC,QAAQ;QACRC,MAAM;IACR;AACF"}
@@ -60,6 +60,9 @@ export function createCheckAvailabilityEndpoint(config) {
60
60
  id: service,
61
61
  collection: config.slugs.services,
62
62
  depth: 0,
63
+ // Skip the resources join — internal logic never reads it, and without this
64
+ // every service read becomes an aggregation with a $lookup.
65
+ joins: false,
63
66
  req
64
67
  }).catch(()=>null);
65
68
  if (!svcDoc) {
@@ -95,12 +98,16 @@ export function createCheckAvailabilityEndpoint(config) {
95
98
  serviceId: service,
96
99
  timeZone: config.timezone
97
100
  });
98
- const slots = await getAvailableSlots({
101
+ const { reason, slots } = await getAvailableSlots({
99
102
  blockingStatuses: config.statusMachine.blockingStatuses,
100
103
  date: dayKey,
101
104
  debug: dbg,
105
+ enforceActive: config.enforceActive,
102
106
  getExternalBusy: config.getExternalBusy,
103
107
  guestCount,
108
+ // Held slots are busy. Omitting this advertises a slot the write path
109
+ // will reject with a 409.
110
+ holdsSlug: config.slotHolds.enabled ? config.slugs.holds : undefined,
104
111
  payload: req.payload,
105
112
  req,
106
113
  reservationSlug: config.slugs.reservations,
@@ -113,9 +120,13 @@ export function createCheckAvailabilityEndpoint(config) {
113
120
  });
114
121
  dbg.dbg('response', {
115
122
  endpoint: 'availability',
123
+ reason,
116
124
  slotCount: slots.length
117
125
  });
118
126
  return Response.json({
127
+ ...reason ? {
128
+ reason
129
+ } : {},
119
130
  slots
120
131
  });
121
132
  },
@@ -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 { createReserveDebug } from '../utilities/reserveDebug.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 dbg = createReserveDebug(req.payload.logger, config.debug)\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 dbg.dbg('request', {\n date: dayKey,\n endpoint: 'availability',\n guestCount,\n resourceIds,\n serviceId: service,\n timeZone: config.timezone,\n })\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: dayKey,\n debug: dbg,\n getExternalBusy: config.getExternalBusy,\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 dbg.dbg('response', { endpoint: 'availability', slotCount: slots.length })\n\n return Response.json({ slots })\n },\n method: 'get',\n path: '/reserve/availability',\n }\n}\n"],"names":["getAvailableSlots","createReserveDebug","extractId","mergeResourceIds","getDayKeyInTimezone","isValidDayKey","createCheckAvailabilityEndpoint","config","handler","req","dbg","payload","logger","debug","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","findByID","id","collection","slugs","services","depth","catch","resourceDoc","resources","requiredIds","requiredResources","r","undefined","resourceIds","endpoint","serviceId","timeZone","slots","blockingStatuses","statusMachine","getExternalBusy","reservationSlug","reservations","resourceSlug","scheduleSlug","schedules","serviceSlug","slotCount","length","method","path"],"mappings":"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"}
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 { createReserveDebug } from '../utilities/reserveDebug.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 dbg = createReserveDebug(req.payload.logger, config.debug)\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 // Skip the resources join — internal logic never reads it, and without this\n // every service read becomes an aggregation with a $lookup.\n joins: false,\n req,\n }).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 dbg.dbg('request', {\n date: dayKey,\n endpoint: 'availability',\n guestCount,\n resourceIds,\n serviceId: service,\n timeZone: config.timezone,\n })\n\n const { reason, slots } = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: dayKey,\n debug: dbg,\n enforceActive: config.enforceActive,\n getExternalBusy: config.getExternalBusy,\n guestCount,\n // Held slots are busy. Omitting this advertises a slot the write path\n // will reject with a 409.\n holdsSlug: config.slotHolds.enabled ? config.slugs.holds : undefined,\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 dbg.dbg('response', { endpoint: 'availability', reason, slotCount: slots.length })\n\n return Response.json({ ...(reason ? { reason } : {}), slots })\n },\n method: 'get',\n path: '/reserve/availability',\n }\n}\n"],"names":["getAvailableSlots","createReserveDebug","extractId","mergeResourceIds","getDayKeyInTimezone","isValidDayKey","createCheckAvailabilityEndpoint","config","handler","req","dbg","payload","logger","debug","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","findByID","id","collection","slugs","services","depth","joins","catch","resourceDoc","resources","requiredIds","requiredResources","r","undefined","resourceIds","endpoint","serviceId","timeZone","reason","slots","blockingStatuses","statusMachine","enforceActive","getExternalBusy","holdsSlug","slotHolds","enabled","holds","reservationSlug","reservations","resourceSlug","scheduleSlug","schedules","serviceSlug","slotCount","length","method","path"],"mappings":"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"}