payload-reserve 2.2.1 → 2.4.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 +1 -0
- package/dist/components/CalendarView/CalendarView.module.css +16 -0
- package/dist/components/CalendarView/index.js +25 -21
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/defaults.js +1 -0
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +15 -0
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/getSlots.js +15 -0
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/resourceAvailability.d.ts +2 -0
- package/dist/endpoints/resourceAvailability.js +27 -2
- package/dist/endpoints/resourceAvailability.js.map +1 -1
- package/dist/hooks/reservations/validateConflicts.js +15 -0
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +5 -0
- package/dist/services/AvailabilityService.js +149 -28
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/externalPillLabel.d.ts +10 -0
- package/dist/utilities/externalPillLabel.js +23 -0
- package/dist/utilities/externalPillLabel.js.map +1 -0
- package/dist/utilities/reservationResourceFilter.d.ts +19 -0
- package/dist/utilities/reservationResourceFilter.js +18 -0
- package/dist/utilities/reservationResourceFilter.js.map +1 -0
- package/dist/utilities/reserveDebug.d.ts +26 -0
- package/dist/utilities/reserveDebug.js +35 -0
- package/dist/utilities/reserveDebug.js.map +1 -0
- package/package.json +1 -1
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 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,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');
|
|
@@ -85,9 +87,18 @@ export function createCheckAvailabilityEndpoint(config) {
|
|
|
85
87
|
}
|
|
86
88
|
const requiredIds = (svcDoc?.requiredResources ?? []).map((r)=>extractId(r)).filter((r)=>r !== undefined);
|
|
87
89
|
const resourceIds = mergeResourceIds(callerIds, requiredIds);
|
|
90
|
+
dbg.dbg('request', {
|
|
91
|
+
date: dayKey,
|
|
92
|
+
endpoint: 'availability',
|
|
93
|
+
guestCount,
|
|
94
|
+
resourceIds,
|
|
95
|
+
serviceId: service,
|
|
96
|
+
timeZone: config.timezone
|
|
97
|
+
});
|
|
88
98
|
const slots = await getAvailableSlots({
|
|
89
99
|
blockingStatuses: config.statusMachine.blockingStatuses,
|
|
90
100
|
date: dayKey,
|
|
101
|
+
debug: dbg,
|
|
91
102
|
getExternalBusy: config.getExternalBusy,
|
|
92
103
|
guestCount,
|
|
93
104
|
payload: req.payload,
|
|
@@ -100,6 +111,10 @@ export function createCheckAvailabilityEndpoint(config) {
|
|
|
100
111
|
serviceSlug: config.slugs.services,
|
|
101
112
|
timeZone: config.timezone
|
|
102
113
|
});
|
|
114
|
+
dbg.dbg('response', {
|
|
115
|
+
endpoint: 'availability',
|
|
116
|
+
slotCount: slots.length
|
|
117
|
+
});
|
|
103
118
|
return Response.json({
|
|
104
119
|
slots
|
|
105
120
|
});
|
|
@@ -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 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,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 createGetSlotsEndpoint(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');
|
|
@@ -85,9 +87,18 @@ export function createGetSlotsEndpoint(config) {
|
|
|
85
87
|
}
|
|
86
88
|
const requiredIds = (svcDoc?.requiredResources ?? []).map((r)=>extractId(r)).filter((r)=>r !== undefined);
|
|
87
89
|
const resourceIds = mergeResourceIds(callerIds, requiredIds);
|
|
90
|
+
dbg.dbg('request', {
|
|
91
|
+
date: dayKey,
|
|
92
|
+
endpoint: 'slots',
|
|
93
|
+
guestCount,
|
|
94
|
+
resourceIds,
|
|
95
|
+
serviceId: service,
|
|
96
|
+
timeZone: config.timezone
|
|
97
|
+
});
|
|
88
98
|
const slots = await getAvailableSlots({
|
|
89
99
|
blockingStatuses: config.statusMachine.blockingStatuses,
|
|
90
100
|
date: dayKey,
|
|
101
|
+
debug: dbg,
|
|
91
102
|
getExternalBusy: config.getExternalBusy,
|
|
92
103
|
guestCount,
|
|
93
104
|
payload: req.payload,
|
|
@@ -100,6 +111,10 @@ export function createGetSlotsEndpoint(config) {
|
|
|
100
111
|
serviceSlug: config.slugs.services,
|
|
101
112
|
timeZone: config.timezone
|
|
102
113
|
});
|
|
114
|
+
dbg.dbg('response', {
|
|
115
|
+
endpoint: 'slots',
|
|
116
|
+
slotCount: slots.length
|
|
117
|
+
});
|
|
103
118
|
return Response.json({
|
|
104
119
|
date,
|
|
105
120
|
guestCount,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/getSlots.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 createGetSlotsEndpoint(config: ResolvedReservationPluginConfig): 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 { error: '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({\n date,\n guestCount,\n slots: slots.map((s) => ({ end: s.end.toISOString(), start: s.start.toISOString() })),\n })\n },\n method: 'get',\n path: '/reserve/slots',\n }\n}\n"],"names":["getAvailableSlots","extractId","mergeResourceIds","getDayKeyInTimezone","isValidDayKey","createGetSlotsEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","error","status","dayKey","test","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/getSlots.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 createGetSlotsEndpoint(config: ResolvedReservationPluginConfig): 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 { error: '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: 'slots',\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: 'slots', slotCount: slots.length })\n\n return Response.json({\n date,\n guestCount,\n slots: slots.map((s) => ({ end: s.end.toISOString(), start: s.start.toISOString() })),\n })\n },\n method: 'get',\n path: '/reserve/slots',\n }\n}\n"],"names":["getAvailableSlots","createReserveDebug","extractId","mergeResourceIds","getDayKeyInTimezone","isValidDayKey","createGetSlotsEndpoint","config","handler","req","dbg","payload","logger","debug","url","URL","date","searchParams","get","resource","service","Response","json","error","status","dayKey","test","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","end","toISOString","start","method","path"],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Endpoint, Payload, PayloadRequest } from 'payload';
|
|
2
2
|
import type { ExternalBusyInterval, GetExternalBusy, ResolvedReservationPluginConfig } from '../types.js';
|
|
3
|
+
import { type ReserveDebug } from '../utilities/reserveDebug.js';
|
|
3
4
|
type DayAvailability = {
|
|
4
5
|
date: string;
|
|
5
6
|
shiftWindows: Array<{
|
|
@@ -35,6 +36,7 @@ export type ResourceAvailability = {
|
|
|
35
36
|
};
|
|
36
37
|
export declare function buildResourceAvailability(params: {
|
|
37
38
|
blockingStatuses: string[];
|
|
39
|
+
debug?: ReserveDebug;
|
|
38
40
|
end: Date;
|
|
39
41
|
getExternalBusy?: GetExternalBusy;
|
|
40
42
|
payload: Payload;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createReserveDebug, NOOP_RESERVE_DEBUG } from '../utilities/reserveDebug.js';
|
|
1
2
|
import { resolveScheduleForDate } from '../utilities/scheduleUtils.js';
|
|
2
3
|
import { readCookie } from '../utilities/tenantFilter.js';
|
|
3
4
|
import { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js';
|
|
@@ -55,7 +56,8 @@ const MAX_RANGE_MS = 90 * 86_400_000;
|
|
|
55
56
|
}));
|
|
56
57
|
}
|
|
57
58
|
export async function buildResourceAvailability(params) {
|
|
58
|
-
const { blockingStatuses, end, getExternalBusy, payload, req, reservationSlug, resourceId, resourceSlug, scheduleSlug, start, timeZone } = params;
|
|
59
|
+
const { blockingStatuses, debug, end, getExternalBusy, payload, req, reservationSlug, resourceId, resourceSlug, scheduleSlug, start, timeZone } = params;
|
|
60
|
+
const trace = debug ?? NOOP_RESERVE_DEBUG;
|
|
59
61
|
// depth 1 so `services` are populated (their `requiredResources` come back as ids)
|
|
60
62
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
63
|
const resource = await payload.findByID({
|
|
@@ -197,7 +199,12 @@ export async function buildResourceAvailability(params) {
|
|
|
197
199
|
resourceId,
|
|
198
200
|
start
|
|
199
201
|
})).filter((iv)=>!isNaN(Date.parse(iv.start)) && !isNaN(Date.parse(iv.end)));
|
|
200
|
-
} catch
|
|
202
|
+
} catch (err) {
|
|
203
|
+
trace.dbg('error', {
|
|
204
|
+
err,
|
|
205
|
+
resourceId,
|
|
206
|
+
where: 'getExternalBusy'
|
|
207
|
+
});
|
|
201
208
|
external = [];
|
|
202
209
|
}
|
|
203
210
|
}
|
|
@@ -230,6 +237,7 @@ export function createResourceAvailabilityEndpoint(config) {
|
|
|
230
237
|
status: 403
|
|
231
238
|
});
|
|
232
239
|
}
|
|
240
|
+
const dbg = createReserveDebug(req.payload.logger, config.debug);
|
|
233
241
|
const url = new URL(req.url);
|
|
234
242
|
const resource = url.searchParams.get('resource');
|
|
235
243
|
const start = url.searchParams.get('start');
|
|
@@ -278,8 +286,16 @@ export function createResourceAvailabilityEndpoint(config) {
|
|
|
278
286
|
tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),
|
|
279
287
|
timezoneField: config.multiTenant.timezoneField
|
|
280
288
|
});
|
|
289
|
+
dbg.dbg('request', {
|
|
290
|
+
end,
|
|
291
|
+
endpoint: 'resource-availability',
|
|
292
|
+
resource,
|
|
293
|
+
start,
|
|
294
|
+
timeZone
|
|
295
|
+
});
|
|
281
296
|
const result = await buildResourceAvailability({
|
|
282
297
|
blockingStatuses: config.statusMachine.blockingStatuses,
|
|
298
|
+
debug: dbg,
|
|
283
299
|
end: endDate,
|
|
284
300
|
getExternalBusy: config.getExternalBusy,
|
|
285
301
|
payload: req.payload,
|
|
@@ -292,12 +308,21 @@ export function createResourceAvailabilityEndpoint(config) {
|
|
|
292
308
|
timeZone
|
|
293
309
|
});
|
|
294
310
|
if (!result) {
|
|
311
|
+
dbg.dbg('response', {
|
|
312
|
+
endpoint: 'resource-availability',
|
|
313
|
+
reason: 'resource_not_found'
|
|
314
|
+
});
|
|
295
315
|
return Response.json({
|
|
296
316
|
error: 'Resource not found'
|
|
297
317
|
}, {
|
|
298
318
|
status: 404
|
|
299
319
|
});
|
|
300
320
|
}
|
|
321
|
+
dbg.dbg('response', {
|
|
322
|
+
dayCount: result.days.length,
|
|
323
|
+
endpoint: 'resource-availability',
|
|
324
|
+
externalCount: result.external.length
|
|
325
|
+
});
|
|
301
326
|
return Response.json(result);
|
|
302
327
|
},
|
|
303
328
|
method: 'get',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/resourceAvailability.ts"],"sourcesContent":["import type { Endpoint, Payload, PayloadRequest, Where } from 'payload'\n\nimport type {\n ExternalBusyInterval,\n GetExternalBusy,\n ResolvedReservationPluginConfig,\n} from '../types.js'\n\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { readCookie } from '../utilities/tenantFilter.js'\nimport { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js'\nimport {\n addDaysToDayKey,\n combineDayKeyAndTime,\n getDayKeyInTimezone,\n} from '../utilities/timezoneUtils.js'\nimport { isPrivilegedUser } from '../utilities/userRoles.js'\n\nconst MAX_RANGE_MS = 90 * 86_400_000\n\ntype DayAvailability = {\n date: string\n shiftWindows: Array<{ end: string; start: string }>\n timeOff: Array<{ end: string; reason?: string; start: string; type?: string }>\n}\n\ntype Busy = Array<{ end: string; start: string; units: number }>\n\nexport type ResourceAvailability = {\n busy: Busy\n capacityMode: 'per-guest' | 'per-reservation'\n days: DayAvailability[]\n /** External busy intervals (calendar sync etc.) from getExternalBusy — display-only here; enforcement lives in checkAvailability. */\n external: ExternalBusyInterval[]\n quantity: number\n /** Capacity of resources this resource's services also require (e.g. a chair pool). */\n requiredPools: Array<{ busy: Busy; quantity: number }>\n /** IANA zone the day windows were resolved in (selected tenant's zone, else global). */\n timeZone: string\n}\n\n/** Busy intervals (with capacity units) for one resource over [start, end). */\nasync function busyFor(args: {\n blockingStatuses: string[]\n capacityMode: 'per-guest' | 'per-reservation'\n end: Date\n payload: Payload\n reservationSlug: string\n resourceId: number | string\n start: Date\n}): Promise<Busy> {\n const { blockingStatuses, capacityMode, end, payload, reservationSlug, resourceId, start } = args\n const where: Where = {\n and: [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: end.toISOString() } },\n { endTime: { greater_than: start.toISOString() } },\n { or: [{ resource: { equals: resourceId } }, { 'items.resource': { equals: resourceId } }] },\n ],\n }\n // limit:0 = all matching — bounded by the endpoint's 90-day range cap, so this\n // can't run away, and the grid no longer silently drops busy intervals (D9).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({\n collection: reservationSlug,\n depth: 0,\n limit: 0,\n where,\n })\n return (docs as Array<Record<string, unknown>>)\n .filter((r) => r.startTime && r.endTime)\n .map((r) => ({\n end: new Date(r.endTime as string).toISOString(),\n start: new Date(r.startTime as string).toISOString(),\n units: capacityMode === 'per-guest' ? ((r.guestCount as number) ?? 1) : 1,\n }))\n}\n\nexport async function buildResourceAvailability(params: {\n blockingStatuses: string[]\n end: Date\n getExternalBusy?: GetExternalBusy\n payload: Payload\n req?: PayloadRequest\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n scheduleSlug: string\n start: Date\n timeZone: string\n}): Promise<null | ResourceAvailability> {\n const {\n blockingStatuses,\n end,\n getExternalBusy,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n scheduleSlug,\n start,\n timeZone,\n } = params\n\n // depth 1 so `services` are populated (their `requiredResources` come back as ids)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: resourceSlug,\n depth: 1,\n }).catch(() => null)\n if (!resource) {\n return null\n }\n const quantity = (resource?.quantity as number) ?? 1\n const capacityMode =\n (resource?.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: schedules } = await (payload.find as any)({\n collection: scheduleSlug,\n depth: 0,\n limit: 100,\n where: { and: [{ active: { equals: true } }, { resource: { equals: resourceId } }] },\n })\n\n type RawException = {\n date: string\n endDate?: string\n reason?: string\n type?: string\n }\n\n const days: DayAvailability[] = []\n const startKey = getDayKeyInTimezone(start, timeZone)\n const lastKey = getDayKeyInTimezone(new Date(end.getTime() - 1), timeZone)\n for (let date = startKey; date <= lastKey; date = addDaysToDayKey(date, 1)) {\n const shiftWindows: DayAvailability['shiftWindows'] = []\n const timeOff: DayAvailability['timeOff'] = []\n\n // A11: an exception on ANY of the resource's schedules makes the whole\n // resource unavailable that day. Find the first matching exception (if any)\n // — it suppresses all shift windows and marks the full day as time-off.\n let dayException: RawException | undefined\n for (const sched of schedules as Array<Record<string, unknown>>) {\n const exceptions = (sched.exceptions as RawException[] | undefined) ?? []\n for (const exc of exceptions) {\n const excStart = getDayKeyInTimezone(new Date(exc.date), timeZone)\n const excEnd = exc.endDate ? getDayKeyInTimezone(new Date(exc.endDate), timeZone) : excStart\n if (date >= excStart && date <= excEnd) {\n dayException = exc\n break\n }\n }\n if (dayException) {\n break\n }\n }\n\n if (dayException) {\n const dayStart = combineDayKeyAndTime(date, '00:00', timeZone)\n const dayEnd = new Date(combineDayKeyAndTime(date, '23:59', timeZone).getTime() + 59_999)\n timeOff.push({\n type: dayException.type,\n end: dayEnd.toISOString(),\n reason: dayException.reason,\n start: dayStart.toISOString(),\n })\n } else {\n for (const sched of schedules as Array<Record<string, unknown>>) {\n // resolveScheduleForDate accepts a Schedule-shaped object; cast through unknown\n const ranges = resolveScheduleForDate(\n sched as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n timeZone,\n )\n for (const r of ranges) {\n shiftWindows.push({ end: r.end.toISOString(), start: r.start.toISOString() })\n }\n }\n }\n\n days.push({ date, shiftWindows, timeOff })\n }\n\n const busy = await busyFor({\n blockingStatuses,\n capacityMode,\n end,\n payload,\n reservationSlug,\n resourceId,\n start,\n })\n\n // Resources this resource's services ALSO require (e.g. a shared chair pool).\n // A slot isn't truly bookable if any of these is at capacity, even when the\n // resource itself is free — so the calendar reflects real availability.\n const poolIds = new Set<string>()\n for (const svc of (resource?.services as Array<Record<string, unknown>>) ?? []) {\n const reqs = (typeof svc === 'object' ? (svc.requiredResources as unknown[]) : []) ?? []\n for (const rr of reqs) {\n const id: number | string | undefined =\n typeof rr === 'object' && rr !== null\n ? (rr as { id?: number | string }).id\n : (rr as number | string)\n if (id != null && String(id) !== String(resourceId)) {\n poolIds.add(String(id))\n }\n }\n }\n\n const requiredPools: ResourceAvailability['requiredPools'] = []\n for (const poolId of poolIds) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const pool = await (payload.findByID as any)({\n id: poolId,\n collection: resourceSlug,\n depth: 0,\n }).catch(() => null)\n if (!pool) {\n continue\n }\n const poolCapacityMode =\n (pool.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n requiredPools.push({\n busy: await busyFor({\n blockingStatuses,\n capacityMode: poolCapacityMode,\n end,\n payload,\n reservationSlug,\n resourceId: poolId,\n start,\n }),\n quantity: (pool.quantity as number) ?? 1,\n })\n }\n\n // External busy — display only (enforcement lives in checkAvailability).\n // Fail-open: a resolver error must never break the grid.\n let external: ExternalBusyInterval[] = []\n if (getExternalBusy && req) {\n try {\n external = (await getExternalBusy({ end, req, resourceId, start })).filter(\n (iv) => !isNaN(Date.parse(iv.start)) && !isNaN(Date.parse(iv.end)),\n )\n } catch {\n external = []\n }\n }\n\n return { busy, capacityMode, days, external, quantity, requiredPools, timeZone }\n}\n\nexport function createResourceAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n // The grid data (every reservation's busy window) is staff/admin-only —\n // same gate as customer search.\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n if (!isPrivilegedUser(req.user, config)) {\n return Response.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const url = new URL(req.url!)\n const resource = url.searchParams.get('resource')\n const start = url.searchParams.get('start')\n const end = url.searchParams.get('end')\n\n if (!resource || !start || !end) {\n return Response.json(\n { error: 'Missing required query params: resource, start, end' },\n { status: 400 },\n )\n }\n\n const startDate = new Date(start)\n const endDate = new Date(end)\n if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {\n return Response.json({ error: 'Invalid start/end date' }, { status: 400 })\n }\n if (endDate <= startDate) {\n return Response.json({ error: 'end must be after start' }, { status: 400 })\n }\n // Unbounded ranges turn the per-day resolution loop into a CPU sink\n if (endDate.getTime() - startDate.getTime() > MAX_RANGE_MS) {\n return Response.json({ error: 'Date range too large (max 90 days)' }, { status: 400 })\n }\n\n // In multiTenant mode, resolve day-boundaries in the SELECTED tenant's zone\n // (tenant timezone → global → UTC). Degrades to the global zone for plain\n // installs: no tenant relationship on reservations / no tenant cookie ⇒ no\n // DB read, same output as before.\n const reservationsCollection = req.payload.config.collections?.find(\n (c) => c.slug === config.slugs.reservations,\n )\n const timeZone = await getEffectiveTenantTimezone({\n globalTimezone: config.timezone,\n payload: req.payload,\n scopedCollection: reservationsCollection as { fields?: unknown[] } | undefined,\n tenantField: config.multiTenant.tenantField,\n tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),\n timezoneField: config.multiTenant.timezoneField,\n })\n\n const result = await buildResourceAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n end: endDate,\n getExternalBusy: config.getExternalBusy,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: resource,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n start: startDate,\n timeZone,\n })\n\n if (!result) {\n return Response.json({ error: 'Resource not found' }, { status: 404 })\n }\n\n return Response.json(result)\n },\n method: 'get',\n path: '/reserve/resource-availability',\n }\n}\n"],"names":["resolveScheduleForDate","readCookie","getEffectiveTenantTimezone","addDaysToDayKey","combineDayKeyAndTime","getDayKeyInTimezone","isPrivilegedUser","MAX_RANGE_MS","busyFor","args","blockingStatuses","capacityMode","end","payload","reservationSlug","resourceId","start","where","and","status","in","startTime","less_than","toISOString","endTime","greater_than","or","resource","equals","docs","find","collection","depth","limit","filter","r","map","Date","units","guestCount","buildResourceAvailability","params","getExternalBusy","req","resourceSlug","scheduleSlug","timeZone","findByID","id","catch","quantity","schedules","active","days","startKey","lastKey","getTime","date","shiftWindows","timeOff","dayException","sched","exceptions","exc","excStart","excEnd","endDate","dayStart","dayEnd","push","type","reason","ranges","busy","poolIds","Set","svc","services","reqs","requiredResources","rr","String","add","requiredPools","poolId","pool","poolCapacityMode","external","iv","isNaN","parse","createResourceAvailabilityEndpoint","config","handler","user","Response","json","error","url","URL","searchParams","get","startDate","reservationsCollection","collections","c","slug","slugs","reservations","globalTimezone","timezone","scopedCollection","tenantField","multiTenant","tenantId","headers","cookieName","timezoneField","result","statusMachine","resources","method","path"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/resourceAvailability.ts"],"sourcesContent":["import type { Endpoint, Payload, PayloadRequest, Where } from 'payload'\n\nimport type {\n ExternalBusyInterval,\n GetExternalBusy,\n ResolvedReservationPluginConfig,\n} from '../types.js'\n\nimport {\n createReserveDebug,\n NOOP_RESERVE_DEBUG,\n type ReserveDebug,\n} from '../utilities/reserveDebug.js'\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { readCookie } from '../utilities/tenantFilter.js'\nimport { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js'\nimport {\n addDaysToDayKey,\n combineDayKeyAndTime,\n getDayKeyInTimezone,\n} from '../utilities/timezoneUtils.js'\nimport { isPrivilegedUser } from '../utilities/userRoles.js'\n\nconst MAX_RANGE_MS = 90 * 86_400_000\n\ntype DayAvailability = {\n date: string\n shiftWindows: Array<{ end: string; start: string }>\n timeOff: Array<{ end: string; reason?: string; start: string; type?: string }>\n}\n\ntype Busy = Array<{ end: string; start: string; units: number }>\n\nexport type ResourceAvailability = {\n busy: Busy\n capacityMode: 'per-guest' | 'per-reservation'\n days: DayAvailability[]\n /** External busy intervals (calendar sync etc.) from getExternalBusy — display-only here; enforcement lives in checkAvailability. */\n external: ExternalBusyInterval[]\n quantity: number\n /** Capacity of resources this resource's services also require (e.g. a chair pool). */\n requiredPools: Array<{ busy: Busy; quantity: number }>\n /** IANA zone the day windows were resolved in (selected tenant's zone, else global). */\n timeZone: string\n}\n\n/** Busy intervals (with capacity units) for one resource over [start, end). */\nasync function busyFor(args: {\n blockingStatuses: string[]\n capacityMode: 'per-guest' | 'per-reservation'\n end: Date\n payload: Payload\n reservationSlug: string\n resourceId: number | string\n start: Date\n}): Promise<Busy> {\n const { blockingStatuses, capacityMode, end, payload, reservationSlug, resourceId, start } = args\n const where: Where = {\n and: [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: end.toISOString() } },\n { endTime: { greater_than: start.toISOString() } },\n { or: [{ resource: { equals: resourceId } }, { 'items.resource': { equals: resourceId } }] },\n ],\n }\n // limit:0 = all matching — bounded by the endpoint's 90-day range cap, so this\n // can't run away, and the grid no longer silently drops busy intervals (D9).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({\n collection: reservationSlug,\n depth: 0,\n limit: 0,\n where,\n })\n return (docs as Array<Record<string, unknown>>)\n .filter((r) => r.startTime && r.endTime)\n .map((r) => ({\n end: new Date(r.endTime as string).toISOString(),\n start: new Date(r.startTime as string).toISOString(),\n units: capacityMode === 'per-guest' ? ((r.guestCount as number) ?? 1) : 1,\n }))\n}\n\nexport async function buildResourceAvailability(params: {\n blockingStatuses: string[]\n debug?: ReserveDebug\n end: Date\n getExternalBusy?: GetExternalBusy\n payload: Payload\n req?: PayloadRequest\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n scheduleSlug: string\n start: Date\n timeZone: string\n}): Promise<null | ResourceAvailability> {\n const {\n blockingStatuses,\n debug,\n end,\n getExternalBusy,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n scheduleSlug,\n start,\n timeZone,\n } = params\n const trace = debug ?? NOOP_RESERVE_DEBUG\n\n // depth 1 so `services` are populated (their `requiredResources` come back as ids)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: resourceSlug,\n depth: 1,\n }).catch(() => null)\n if (!resource) {\n return null\n }\n const quantity = (resource?.quantity as number) ?? 1\n const capacityMode =\n (resource?.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: schedules } = await (payload.find as any)({\n collection: scheduleSlug,\n depth: 0,\n limit: 100,\n where: { and: [{ active: { equals: true } }, { resource: { equals: resourceId } }] },\n })\n\n type RawException = {\n date: string\n endDate?: string\n reason?: string\n type?: string\n }\n\n const days: DayAvailability[] = []\n const startKey = getDayKeyInTimezone(start, timeZone)\n const lastKey = getDayKeyInTimezone(new Date(end.getTime() - 1), timeZone)\n for (let date = startKey; date <= lastKey; date = addDaysToDayKey(date, 1)) {\n const shiftWindows: DayAvailability['shiftWindows'] = []\n const timeOff: DayAvailability['timeOff'] = []\n\n // A11: an exception on ANY of the resource's schedules makes the whole\n // resource unavailable that day. Find the first matching exception (if any)\n // — it suppresses all shift windows and marks the full day as time-off.\n let dayException: RawException | undefined\n for (const sched of schedules as Array<Record<string, unknown>>) {\n const exceptions = (sched.exceptions as RawException[] | undefined) ?? []\n for (const exc of exceptions) {\n const excStart = getDayKeyInTimezone(new Date(exc.date), timeZone)\n const excEnd = exc.endDate ? getDayKeyInTimezone(new Date(exc.endDate), timeZone) : excStart\n if (date >= excStart && date <= excEnd) {\n dayException = exc\n break\n }\n }\n if (dayException) {\n break\n }\n }\n\n if (dayException) {\n const dayStart = combineDayKeyAndTime(date, '00:00', timeZone)\n const dayEnd = new Date(combineDayKeyAndTime(date, '23:59', timeZone).getTime() + 59_999)\n timeOff.push({\n type: dayException.type,\n end: dayEnd.toISOString(),\n reason: dayException.reason,\n start: dayStart.toISOString(),\n })\n } else {\n for (const sched of schedules as Array<Record<string, unknown>>) {\n // resolveScheduleForDate accepts a Schedule-shaped object; cast through unknown\n const ranges = resolveScheduleForDate(\n sched as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n timeZone,\n )\n for (const r of ranges) {\n shiftWindows.push({ end: r.end.toISOString(), start: r.start.toISOString() })\n }\n }\n }\n\n days.push({ date, shiftWindows, timeOff })\n }\n\n const busy = await busyFor({\n blockingStatuses,\n capacityMode,\n end,\n payload,\n reservationSlug,\n resourceId,\n start,\n })\n\n // Resources this resource's services ALSO require (e.g. a shared chair pool).\n // A slot isn't truly bookable if any of these is at capacity, even when the\n // resource itself is free — so the calendar reflects real availability.\n const poolIds = new Set<string>()\n for (const svc of (resource?.services as Array<Record<string, unknown>>) ?? []) {\n const reqs = (typeof svc === 'object' ? (svc.requiredResources as unknown[]) : []) ?? []\n for (const rr of reqs) {\n const id: number | string | undefined =\n typeof rr === 'object' && rr !== null\n ? (rr as { id?: number | string }).id\n : (rr as number | string)\n if (id != null && String(id) !== String(resourceId)) {\n poolIds.add(String(id))\n }\n }\n }\n\n const requiredPools: ResourceAvailability['requiredPools'] = []\n for (const poolId of poolIds) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const pool = await (payload.findByID as any)({\n id: poolId,\n collection: resourceSlug,\n depth: 0,\n }).catch(() => null)\n if (!pool) {\n continue\n }\n const poolCapacityMode =\n (pool.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n requiredPools.push({\n busy: await busyFor({\n blockingStatuses,\n capacityMode: poolCapacityMode,\n end,\n payload,\n reservationSlug,\n resourceId: poolId,\n start,\n }),\n quantity: (pool.quantity as number) ?? 1,\n })\n }\n\n // External busy — display only (enforcement lives in checkAvailability).\n // Fail-open: a resolver error must never break the grid.\n let external: ExternalBusyInterval[] = []\n if (getExternalBusy && req) {\n try {\n external = (await getExternalBusy({ end, req, resourceId, start })).filter(\n (iv) => !isNaN(Date.parse(iv.start)) && !isNaN(Date.parse(iv.end)),\n )\n } catch (err) {\n trace.dbg('error', { err, resourceId, where: 'getExternalBusy' })\n external = []\n }\n }\n\n return { busy, capacityMode, days, external, quantity, requiredPools, timeZone }\n}\n\nexport function createResourceAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n // The grid data (every reservation's busy window) is staff/admin-only —\n // same gate as customer search.\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n if (!isPrivilegedUser(req.user, config)) {\n return Response.json({ error: 'Forbidden' }, { status: 403 })\n }\n const dbg = createReserveDebug(req.payload.logger, config.debug)\n\n const url = new URL(req.url!)\n const resource = url.searchParams.get('resource')\n const start = url.searchParams.get('start')\n const end = url.searchParams.get('end')\n\n if (!resource || !start || !end) {\n return Response.json(\n { error: 'Missing required query params: resource, start, end' },\n { status: 400 },\n )\n }\n\n const startDate = new Date(start)\n const endDate = new Date(end)\n if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {\n return Response.json({ error: 'Invalid start/end date' }, { status: 400 })\n }\n if (endDate <= startDate) {\n return Response.json({ error: 'end must be after start' }, { status: 400 })\n }\n // Unbounded ranges turn the per-day resolution loop into a CPU sink\n if (endDate.getTime() - startDate.getTime() > MAX_RANGE_MS) {\n return Response.json({ error: 'Date range too large (max 90 days)' }, { status: 400 })\n }\n\n // In multiTenant mode, resolve day-boundaries in the SELECTED tenant's zone\n // (tenant timezone → global → UTC). Degrades to the global zone for plain\n // installs: no tenant relationship on reservations / no tenant cookie ⇒ no\n // DB read, same output as before.\n const reservationsCollection = req.payload.config.collections?.find(\n (c) => c.slug === config.slugs.reservations,\n )\n const timeZone = await getEffectiveTenantTimezone({\n globalTimezone: config.timezone,\n payload: req.payload,\n scopedCollection: reservationsCollection as { fields?: unknown[] } | undefined,\n tenantField: config.multiTenant.tenantField,\n tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),\n timezoneField: config.multiTenant.timezoneField,\n })\n\n dbg.dbg('request', { end, endpoint: 'resource-availability', resource, start, timeZone })\n\n const result = await buildResourceAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n debug: dbg,\n end: endDate,\n getExternalBusy: config.getExternalBusy,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: resource,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n start: startDate,\n timeZone,\n })\n\n if (!result) {\n dbg.dbg('response', { endpoint: 'resource-availability', reason: 'resource_not_found' })\n return Response.json({ error: 'Resource not found' }, { status: 404 })\n }\n\n dbg.dbg('response', {\n dayCount: result.days.length,\n endpoint: 'resource-availability',\n externalCount: result.external.length,\n })\n return Response.json(result)\n },\n method: 'get',\n path: '/reserve/resource-availability',\n }\n}\n"],"names":["createReserveDebug","NOOP_RESERVE_DEBUG","resolveScheduleForDate","readCookie","getEffectiveTenantTimezone","addDaysToDayKey","combineDayKeyAndTime","getDayKeyInTimezone","isPrivilegedUser","MAX_RANGE_MS","busyFor","args","blockingStatuses","capacityMode","end","payload","reservationSlug","resourceId","start","where","and","status","in","startTime","less_than","toISOString","endTime","greater_than","or","resource","equals","docs","find","collection","depth","limit","filter","r","map","Date","units","guestCount","buildResourceAvailability","params","debug","getExternalBusy","req","resourceSlug","scheduleSlug","timeZone","trace","findByID","id","catch","quantity","schedules","active","days","startKey","lastKey","getTime","date","shiftWindows","timeOff","dayException","sched","exceptions","exc","excStart","excEnd","endDate","dayStart","dayEnd","push","type","reason","ranges","busy","poolIds","Set","svc","services","reqs","requiredResources","rr","String","add","requiredPools","poolId","pool","poolCapacityMode","external","iv","isNaN","parse","err","dbg","createResourceAvailabilityEndpoint","config","handler","user","Response","json","error","logger","url","URL","searchParams","get","startDate","reservationsCollection","collections","c","slug","slugs","reservations","globalTimezone","timezone","scopedCollection","tenantField","multiTenant","tenantId","headers","cookieName","timezoneField","endpoint","result","statusMachine","resources","dayCount","length","externalCount","method","path"],"mappings":"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"}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { ValidationError } from 'payload';
|
|
2
2
|
import { checkAvailability } from '../../services/AvailabilityService.js';
|
|
3
3
|
import { mergeReservationData, schedulingFieldsChanged } from '../../utilities/reservationChanges.js';
|
|
4
|
+
import { createReserveDebug } from '../../utilities/reserveDebug.js';
|
|
4
5
|
import { extractId, resolveReservationItems } from '../../utilities/resolveReservationItems.js';
|
|
5
6
|
export const validateConflicts = (config)=>async ({ context, data, operation, originalDoc, req })=>{
|
|
6
7
|
if (context?.skipReservationHooks) {
|
|
7
8
|
return data;
|
|
8
9
|
}
|
|
10
|
+
const dbg = createReserveDebug(req.payload.logger, config.debug);
|
|
9
11
|
const isUpdate = operation === 'update';
|
|
10
12
|
// Skip when an update touches no scheduling-relevant field, so reservations
|
|
11
13
|
// booked under older buffers/schedules can still take benign edits.
|
|
@@ -55,6 +57,7 @@ export const validateConflicts = (config)=>async ({ context, data, operation, or
|
|
|
55
57
|
blockingStatuses: config.statusMachine.blockingStatuses,
|
|
56
58
|
bufferAfter,
|
|
57
59
|
bufferBefore,
|
|
60
|
+
debug: dbg,
|
|
58
61
|
endTime: new Date(item.endTime),
|
|
59
62
|
excludeReservationId: isUpdate ? originalDoc?.id : undefined,
|
|
60
63
|
getExternalBusy: config.getExternalBusy,
|
|
@@ -68,7 +71,19 @@ export const validateConflicts = (config)=>async ({ context, data, operation, or
|
|
|
68
71
|
siblingItems,
|
|
69
72
|
startTime: new Date(item.startTime)
|
|
70
73
|
});
|
|
74
|
+
dbg.dbg('conflict_check', {
|
|
75
|
+
available: result.available,
|
|
76
|
+
index: i,
|
|
77
|
+
resourceId: item.resource,
|
|
78
|
+
startTime: new Date(item.startTime).toISOString()
|
|
79
|
+
});
|
|
71
80
|
if (!result.available) {
|
|
81
|
+
dbg.dbg('conflict_reject', {
|
|
82
|
+
index: i,
|
|
83
|
+
reason: result.reason,
|
|
84
|
+
resourceId: item.resource,
|
|
85
|
+
startTime: new Date(item.startTime).toISOString()
|
|
86
|
+
});
|
|
72
87
|
throw new ValidationError({
|
|
73
88
|
errors: [
|
|
74
89
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/hooks/reservations/validateConflicts.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { checkAvailability } from '../../services/AvailabilityService.js'\nimport {\n mergeReservationData,\n schedulingFieldsChanged,\n} from '../../utilities/reservationChanges.js'\nimport { extractId, resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const validateConflicts =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {\n return data\n }\n\n const isUpdate = operation === 'update'\n\n // Skip when an update touches no scheduling-relevant field, so reservations\n // booked under older buffers/schedules can still take benign edits.\n if (\n isUpdate &&\n !schedulingFieldsChanged({\n blockingStatuses: config.statusMachine.blockingStatuses,\n data: data as Record<string, unknown>,\n originalDoc: originalDoc as Record<string, unknown> | undefined,\n })\n ) {\n return data\n }\n\n // Validate the merged document — Payload usually backfills update patches\n // from originalDoc before beforeChange, but the hook must not rely on it.\n const source = isUpdate\n ? mergeReservationData(\n data as Record<string, unknown>,\n originalDoc as Record<string, unknown> | undefined,\n )\n : (data as Record<string, unknown>)\n\n const items = resolveReservationItems(source)\n\n if (items.length === 0) {\n return data\n }\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n if (!item.endTime) {\n continue\n }\n\n // Fetch buffer times from the item's own service (not just the primary)\n const itemServiceId = item.service ?? extractId(source.service)\n let bufferBefore = config.defaultBufferTime\n let bufferAfter = config.defaultBufferTime\n\n if (itemServiceId) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: itemServiceId,\n collection: config.slugs.services,\n req,\n })\n if (service) {\n bufferBefore = (service.bufferTimeBefore as number) ?? config.defaultBufferTime\n bufferAfter = (service.bufferTimeAfter as number) ?? config.defaultBufferTime\n }\n } catch {\n // Use defaults if service lookup fails\n }\n }\n\n // Other items of this same booking on the same resource count as occupancy\n // so two items can't double-book one resource within one create (review A5).\n const siblingItems = items.filter((other, j) => j !== i && other.resource === item.resource)\n\n const result = await checkAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime: new Date(item.endTime),\n excludeReservationId: isUpdate ? originalDoc?.id : undefined,\n getExternalBusy: config.getExternalBusy,\n guestCount: item.guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: item.resource,\n resourceSlug: config.slugs.resources,\n servicesSlug: config.slugs.services,\n siblingItems,\n startTime: new Date(item.startTime),\n })\n\n if (!result.available) {\n throw new ValidationError({\n errors: [\n {\n message: result.reason ?? (req.t as PluginT)('reservation:errorConflict'),\n path: items.length > 1 ? `items.${i}.startTime` : 'startTime',\n },\n ],\n })\n }\n }\n\n return data\n }\n"],"names":["ValidationError","checkAvailability","mergeReservationData","schedulingFieldsChanged","extractId","resolveReservationItems","validateConflicts","config","context","data","operation","originalDoc","req","skipReservationHooks","isUpdate","blockingStatuses","statusMachine","source","items","length","i","item","endTime","itemServiceId","service","bufferBefore","defaultBufferTime","bufferAfter","
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/reservations/validateConflicts.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { checkAvailability } from '../../services/AvailabilityService.js'\nimport {\n mergeReservationData,\n schedulingFieldsChanged,\n} from '../../utilities/reservationChanges.js'\nimport { createReserveDebug } from '../../utilities/reserveDebug.js'\nimport { extractId, resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const validateConflicts =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {\n return data\n }\n\n const dbg = createReserveDebug(req.payload.logger, config.debug)\n\n const isUpdate = operation === 'update'\n\n // Skip when an update touches no scheduling-relevant field, so reservations\n // booked under older buffers/schedules can still take benign edits.\n if (\n isUpdate &&\n !schedulingFieldsChanged({\n blockingStatuses: config.statusMachine.blockingStatuses,\n data: data as Record<string, unknown>,\n originalDoc: originalDoc as Record<string, unknown> | undefined,\n })\n ) {\n return data\n }\n\n // Validate the merged document — Payload usually backfills update patches\n // from originalDoc before beforeChange, but the hook must not rely on it.\n const source = isUpdate\n ? mergeReservationData(\n data as Record<string, unknown>,\n originalDoc as Record<string, unknown> | undefined,\n )\n : (data as Record<string, unknown>)\n\n const items = resolveReservationItems(source)\n\n if (items.length === 0) {\n return data\n }\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n if (!item.endTime) {\n continue\n }\n\n // Fetch buffer times from the item's own service (not just the primary)\n const itemServiceId = item.service ?? extractId(source.service)\n let bufferBefore = config.defaultBufferTime\n let bufferAfter = config.defaultBufferTime\n\n if (itemServiceId) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: itemServiceId,\n collection: config.slugs.services,\n req,\n })\n if (service) {\n bufferBefore = (service.bufferTimeBefore as number) ?? config.defaultBufferTime\n bufferAfter = (service.bufferTimeAfter as number) ?? config.defaultBufferTime\n }\n } catch {\n // Use defaults if service lookup fails\n }\n }\n\n // Other items of this same booking on the same resource count as occupancy\n // so two items can't double-book one resource within one create (review A5).\n const siblingItems = items.filter((other, j) => j !== i && other.resource === item.resource)\n\n const result = await checkAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n bufferAfter,\n bufferBefore,\n debug: dbg,\n endTime: new Date(item.endTime),\n excludeReservationId: isUpdate ? originalDoc?.id : undefined,\n getExternalBusy: config.getExternalBusy,\n guestCount: item.guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: item.resource,\n resourceSlug: config.slugs.resources,\n servicesSlug: config.slugs.services,\n siblingItems,\n startTime: new Date(item.startTime),\n })\n\n dbg.dbg('conflict_check', {\n available: result.available,\n index: i,\n resourceId: item.resource,\n startTime: new Date(item.startTime).toISOString(),\n })\n\n if (!result.available) {\n dbg.dbg('conflict_reject', {\n index: i,\n reason: result.reason,\n resourceId: item.resource,\n startTime: new Date(item.startTime).toISOString(),\n })\n\n throw new ValidationError({\n errors: [\n {\n message: result.reason ?? (req.t as PluginT)('reservation:errorConflict'),\n path: items.length > 1 ? `items.${i}.startTime` : 'startTime',\n },\n ],\n })\n }\n }\n\n return data\n }\n"],"names":["ValidationError","checkAvailability","mergeReservationData","schedulingFieldsChanged","createReserveDebug","extractId","resolveReservationItems","validateConflicts","config","context","data","operation","originalDoc","req","skipReservationHooks","dbg","payload","logger","debug","isUpdate","blockingStatuses","statusMachine","source","items","length","i","item","endTime","itemServiceId","service","bufferBefore","defaultBufferTime","bufferAfter","findByID","id","collection","slugs","services","bufferTimeBefore","bufferTimeAfter","siblingItems","filter","other","j","resource","result","Date","excludeReservationId","undefined","getExternalBusy","guestCount","reservationSlug","reservations","resourceId","resourceSlug","resources","servicesSlug","startTime","available","index","toISOString","reason","errors","message","t","path"],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Payload, PayloadRequest, Where } from 'payload';
|
|
2
2
|
import type { CapacityMode, DurationType, GetExternalBusy, StatusMachineConfig } from '../types.js';
|
|
3
3
|
import type { ResolvedItem } from '../utilities/resolveReservationItems.js';
|
|
4
|
+
import { type ReserveDebug } from '../utilities/reserveDebug.js';
|
|
4
5
|
/** A window during which a resource is occupied. Reservation/sibling
|
|
5
6
|
* occupancies are expanded by buffer times; external intervals (from
|
|
6
7
|
* `getExternalBusy`) are used as-is — only the candidate window itself
|
|
@@ -76,6 +77,8 @@ export declare function checkAvailability(params: {
|
|
|
76
77
|
blockingStatuses: string[];
|
|
77
78
|
bufferAfter: number;
|
|
78
79
|
bufferBefore: number;
|
|
80
|
+
/** Optional tracer — emits check/check_result/error lines when enabled. */
|
|
81
|
+
debug?: ReserveDebug;
|
|
79
82
|
endTime: Date;
|
|
80
83
|
excludeReservationId?: number | string;
|
|
81
84
|
/** External busy resolver (calendar sync etc.) — intervals block the whole resource. */
|
|
@@ -99,6 +102,8 @@ export declare function checkAvailability(params: {
|
|
|
99
102
|
export declare function getAvailableSlots(params: {
|
|
100
103
|
blockingStatuses: string[];
|
|
101
104
|
date: Date | string;
|
|
105
|
+
/** Optional tracer — emits per-stage slot-generation lines when enabled. */
|
|
106
|
+
debug?: ReserveDebug;
|
|
102
107
|
/** External busy resolver (calendar sync etc.) — intervals block the whole resource. */
|
|
103
108
|
getExternalBusy?: GetExternalBusy;
|
|
104
109
|
guestCount?: number;
|