payload-reserve 2.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +218 -4
- package/dist/collections/Holds.d.ts +18 -0
- package/dist/collections/Holds.js +113 -0
- package/dist/collections/Holds.js.map +1 -0
- package/dist/collections/Reservations.js +8 -0
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.js +35 -1
- package/dist/collections/Resources.js.map +1 -1
- package/dist/collections/Services.js +41 -0
- package/dist/collections/Services.js.map +1 -1
- package/dist/components/CalendarView/LaneTimelineView.js +4 -5
- package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
- package/dist/components/CalendarView/index.js +55 -71
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/components/DashboardWidget/DashboardWidgetServer.js +11 -74
- package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
- package/dist/components/DashboardWidget/fetchDashboardStats.d.ts +42 -0
- package/dist/components/DashboardWidget/fetchDashboardStats.js +113 -0
- package/dist/components/DashboardWidget/fetchDashboardStats.js.map +1 -0
- package/dist/defaults.d.ts +2 -0
- package/dist/defaults.js +12 -0
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.js +43 -14
- package/dist/endpoints/cancelBooking.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +27 -1
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/createBooking.js +108 -7
- package/dist/endpoints/createBooking.js.map +1 -1
- package/dist/endpoints/customerSearch.js +6 -0
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/effectiveTimezone.js +1 -0
- package/dist/endpoints/effectiveTimezone.js.map +1 -1
- package/dist/endpoints/getSlots.js +27 -1
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/holdSlot.d.ts +12 -0
- package/dist/endpoints/holdSlot.js +126 -0
- package/dist/endpoints/holdSlot.js.map +1 -0
- package/dist/endpoints/releaseSlot.d.ts +8 -0
- package/dist/endpoints/releaseSlot.js +34 -0
- package/dist/endpoints/releaseSlot.js.map +1 -0
- package/dist/endpoints/resourceAvailability.d.ts +4 -0
- package/dist/endpoints/resourceAvailability.js +105 -4
- package/dist/endpoints/resourceAvailability.js.map +1 -1
- package/dist/hooks/holds/validateHoldSlot.d.ts +14 -0
- package/dist/hooks/holds/validateHoldSlot.js +90 -0
- package/dist/hooks/holds/validateHoldSlot.js.map +1 -0
- package/dist/hooks/reservations/acquireBookingLock.d.ts +30 -0
- package/dist/hooks/reservations/acquireBookingLock.js +71 -0
- package/dist/hooks/reservations/acquireBookingLock.js.map +1 -0
- package/dist/hooks/reservations/calculateEndTime.js +13 -1
- package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
- package/dist/hooks/reservations/expandRequiredResources.js +3 -0
- package/dist/hooks/reservations/expandRequiredResources.js.map +1 -1
- package/dist/hooks/reservations/validateActive.d.ts +11 -0
- package/dist/hooks/reservations/validateActive.js +103 -0
- package/dist/hooks/reservations/validateActive.js.map +1 -0
- package/dist/hooks/reservations/validateConflicts.js +22 -1
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/hooks/reservations/validateGuestBooking.js +3 -0
- package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
- package/dist/hooks/shared/preventDeleteWhenReferenced.d.ts +42 -0
- package/dist/hooks/shared/preventDeleteWhenReferenced.js +87 -0
- package/dist/hooks/shared/preventDeleteWhenReferenced.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +179 -3
- package/dist/plugin.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +51 -5
- package/dist/services/AvailabilityService.js +315 -38
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/services/HoldService.d.ts +53 -0
- package/dist/services/HoldService.js +169 -0
- package/dist/services/HoldService.js.map +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/translations/ar.json +5 -1
- package/dist/translations/de.json +5 -1
- package/dist/translations/en.json +5 -1
- package/dist/translations/es.json +5 -1
- package/dist/translations/fa.json +5 -1
- package/dist/translations/fr.json +5 -1
- package/dist/translations/hi.json +5 -1
- package/dist/translations/id.json +5 -1
- package/dist/translations/pl.json +5 -1
- package/dist/translations/ru.json +5 -1
- package/dist/translations/tr.json +5 -1
- package/dist/translations/zh.json +5 -1
- package/dist/types.d.ts +32 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/calendarGrid.d.ts +44 -0
- package/dist/utilities/calendarGrid.js +67 -0
- package/dist/utilities/calendarGrid.js.map +1 -0
- package/dist/utilities/reservationChanges.d.ts +6 -1
- package/dist/utilities/reservationChanges.js +1 -1
- package/dist/utilities/reservationChanges.js.map +1 -1
- package/dist/utilities/reserveDebug.d.ts +26 -0
- package/dist/utilities/reserveDebug.js +35 -0
- package/dist/utilities/reserveDebug.js.map +1 -0
- package/dist/utilities/resolveReservationItems.d.ts +13 -2
- package/dist/utilities/resolveReservationItems.js +87 -2
- package/dist/utilities/resolveReservationItems.js.map +1 -1
- package/dist/utilities/retryOnWriteConflict.d.ts +66 -0
- package/dist/utilities/retryOnWriteConflict.js +135 -0
- package/dist/utilities/retryOnWriteConflict.js.map +1 -0
- package/dist/utilities/tenantTimezone.d.ts +77 -1
- package/dist/utilities/tenantTimezone.js +163 -2
- package/dist/utilities/tenantTimezone.js.map +1 -1
- package/dist/utilities/transactionSupport.d.ts +13 -0
- package/dist/utilities/transactionSupport.js +30 -0
- package/dist/utilities/transactionSupport.js.map +1 -0
- 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":"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"}
|
|
@@ -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"}
|
package/dist/defaults.d.ts
CHANGED
|
@@ -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;
|
|
@@ -116,12 +118,17 @@ export function resolveConfig(pluginOptions) {
|
|
|
116
118
|
allowGuestBooking: pluginOptions.allowGuestBooking ?? DEFAULT_ALLOW_GUEST_BOOKING,
|
|
117
119
|
cancellationNoticePeriod: pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,
|
|
118
120
|
collectionOverrides: pluginOptions.collectionOverrides ?? {},
|
|
121
|
+
debug: pluginOptions.debug ?? false,
|
|
119
122
|
defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,
|
|
120
123
|
disabled: pluginOptions.disabled ?? false,
|
|
124
|
+
enforceActive: pluginOptions.enforceActive ?? true,
|
|
121
125
|
extraReservationFields: pluginOptions.extraReservationFields ?? [],
|
|
122
126
|
getExternalBusy: pluginOptions.getExternalBusy,
|
|
123
127
|
// Real value is set by the plugin once config.collections is known (C8)
|
|
124
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,
|
|
125
132
|
hooks: pluginOptions.hooks ?? {},
|
|
126
133
|
leaveTypes: pluginOptions.leaveTypes ?? DEFAULT_LEAVE_TYPES,
|
|
127
134
|
localized: false,
|
|
@@ -138,8 +145,13 @@ export function resolveConfig(pluginOptions) {
|
|
|
138
145
|
roleField: rom.roleField ?? pluginOptions.staffProvisioning?.roleField ?? 'role'
|
|
139
146
|
} : undefined,
|
|
140
147
|
resourceTypes,
|
|
148
|
+
slotHolds: {
|
|
149
|
+
enabled: !disabled && (pluginOptions.slotHolds?.enabled ?? false),
|
|
150
|
+
ttlMinutes: pluginOptions.slotHolds?.ttlMinutes ?? DEFAULT_HOLD_TTL_MINUTES
|
|
151
|
+
},
|
|
141
152
|
slugs: {
|
|
142
153
|
customers: pluginOptions.slugs?.customers ?? DEFAULT_SLUGS.customers,
|
|
154
|
+
holds: pluginOptions.slugs?.holds ?? DEFAULT_SLUGS.holds,
|
|
143
155
|
media: pluginOptions.slugs?.media ?? DEFAULT_SLUGS.media,
|
|
144
156
|
reservations: pluginOptions.slugs?.reservations ?? DEFAULT_SLUGS.reservations,
|
|
145
157
|
resources: pluginOptions.slugs?.resources ?? DEFAULT_SLUGS.resources,
|
package/dist/defaults.js.map
CHANGED
|
@@ -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 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","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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { getAvailableSlots } from '../services/AvailabilityService.js';
|
|
2
|
+
import { createReserveDebug } from '../utilities/reserveDebug.js';
|
|
2
3
|
import { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js';
|
|
3
4
|
import { getDayKeyInTimezone, isValidDayKey } from '../utilities/timezoneUtils.js';
|
|
4
5
|
export function createCheckAvailabilityEndpoint(config) {
|
|
5
6
|
return {
|
|
6
7
|
handler: async (req)=>{
|
|
8
|
+
const dbg = createReserveDebug(req.payload.logger, config.debug);
|
|
7
9
|
const url = new URL(req.url);
|
|
8
10
|
const date = url.searchParams.get('date');
|
|
9
11
|
const resource = url.searchParams.get('resource');
|
|
@@ -58,6 +60,9 @@ export function createCheckAvailabilityEndpoint(config) {
|
|
|
58
60
|
id: service,
|
|
59
61
|
collection: config.slugs.services,
|
|
60
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,
|
|
61
66
|
req
|
|
62
67
|
}).catch(()=>null);
|
|
63
68
|
if (!svcDoc) {
|
|
@@ -85,11 +90,24 @@ export function createCheckAvailabilityEndpoint(config) {
|
|
|
85
90
|
}
|
|
86
91
|
const requiredIds = (svcDoc?.requiredResources ?? []).map((r)=>extractId(r)).filter((r)=>r !== undefined);
|
|
87
92
|
const resourceIds = mergeResourceIds(callerIds, requiredIds);
|
|
88
|
-
|
|
93
|
+
dbg.dbg('request', {
|
|
94
|
+
date: dayKey,
|
|
95
|
+
endpoint: 'availability',
|
|
96
|
+
guestCount,
|
|
97
|
+
resourceIds,
|
|
98
|
+
serviceId: service,
|
|
99
|
+
timeZone: config.timezone
|
|
100
|
+
});
|
|
101
|
+
const { reason, slots } = await getAvailableSlots({
|
|
89
102
|
blockingStatuses: config.statusMachine.blockingStatuses,
|
|
90
103
|
date: dayKey,
|
|
104
|
+
debug: dbg,
|
|
105
|
+
enforceActive: config.enforceActive,
|
|
91
106
|
getExternalBusy: config.getExternalBusy,
|
|
92
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,
|
|
93
111
|
payload: req.payload,
|
|
94
112
|
req,
|
|
95
113
|
reservationSlug: config.slugs.reservations,
|
|
@@ -100,7 +118,15 @@ export function createCheckAvailabilityEndpoint(config) {
|
|
|
100
118
|
serviceSlug: config.slugs.services,
|
|
101
119
|
timeZone: config.timezone
|
|
102
120
|
});
|
|
121
|
+
dbg.dbg('response', {
|
|
122
|
+
endpoint: 'availability',
|
|
123
|
+
reason,
|
|
124
|
+
slotCount: slots.length
|
|
125
|
+
});
|
|
103
126
|
return Response.json({
|
|
127
|
+
...reason ? {
|
|
128
|
+
reason
|
|
129
|
+
} : {},
|
|
104
130
|
slots
|
|
105
131
|
});
|
|
106
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 { 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 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 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","
|
|
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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"}
|