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/services/AvailabilityService.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport type { CapacityMode, DurationType, GetExternalBusy, StatusMachineConfig } from '../types.js'\nimport type { ResolvedItem } from '../utilities/resolveReservationItems.js'\n\nimport { resolveReservationItems } from '../utilities/resolveReservationItems.js'\nimport { isExceptionDate, resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport {\n addMinutes,\n computeBlockedWindow,\n doRangesOverlap,\n intersectIntervals,\n} from '../utilities/slotUtils.js'\nimport { endOfDayInTimezone } from '../utilities/timezoneUtils.js'\n\n/** A window during which a resource is occupied. Reservation/sibling\n * occupancies are expanded by buffer times; external intervals (from\n * `getExternalBusy`) are used as-is — only the candidate window itself\n * carries buffers when checked against them. */\nexport type Occupancy = { blockedEnd: Date; blockedStart: Date; units: number }\n\n/** Coarse pre-filter widen: covers any realistic neighbor buffer (buffers are\n * minutes). The precise per-item overlap check runs in memory afterwards. */\nconst COARSE_MARGIN_MS = 24 * 60 * 60 * 1000\n\n// --- Pure functions (no DB) ---\n\nexport function computeEndTime(params: {\n durationType: DurationType\n endTime?: Date\n serviceDuration: number\n startTime: Date\n timeZone?: string\n}): { durationMinutes: number; endTime: Date } {\n const { durationType, serviceDuration, startTime } = params\n\n if (durationType === 'full-day') {\n const end = endOfDayInTimezone(startTime, params.timeZone ?? 'UTC')\n const durationMinutes = Math.round((end.getTime() - startTime.getTime()) / 60_000)\n return { durationMinutes, endTime: end }\n }\n\n if (durationType === 'flexible' && params.endTime) {\n const durationMinutes = Math.round(\n (params.endTime.getTime() - startTime.getTime()) / 60_000,\n )\n return { durationMinutes, endTime: params.endTime }\n }\n\n // fixed duration (default)\n const endTime = addMinutes(startTime, serviceDuration)\n return { durationMinutes: serviceDuration, endTime }\n}\n\nexport function buildOverlapQuery(params: {\n blockingStatuses: string[]\n effectiveEnd: Date\n effectiveStart: Date\n excludeReservationId?: number | string\n resourceId: number | string\n}): Where {\n const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } =\n params\n\n const conditions: Where[] = [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\n {\n or: [\n { resource: { equals: resourceId } },\n { 'items.resource': { equals: resourceId } },\n ],\n },\n ]\n\n if (excludeReservationId) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\n/**\n * Coarse superset query: blocking reservations whose top-level (span) window\n * comes within COARSE_MARGIN_MS of the candidate window and reference the\n * resource at top level or in items[]. The precise per-item overlap is computed\n * in memory afterwards — the top-level span is a superset of every item's\n * window, so this never misses a real conflict (margin covers neighbor buffers).\n */\nexport function buildCoarseOverlapQuery(params: {\n blockingStatuses: string[]\n candidateEnd: Date\n candidateStart: Date\n excludeReservationId?: number | string\n resourceId: number | string\n}): Where {\n const { blockingStatuses, candidateEnd, candidateStart, excludeReservationId, resourceId } =\n params\n const windowStart = new Date(candidateStart.getTime() - COARSE_MARGIN_MS)\n const windowEnd = new Date(candidateEnd.getTime() + COARSE_MARGIN_MS)\n\n const conditions: Where[] = [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: windowEnd.toISOString() } },\n { endTime: { greater_than: windowStart.toISOString() } },\n {\n or: [{ resource: { equals: resourceId } }, { 'items.resource': { equals: resourceId } }],\n },\n ]\n\n if (excludeReservationId !== undefined) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\n/**\n * The occupancy windows a set of resolved items imposes on `resourceId`. Each\n * matching item's [startTime, endTime) is expanded by that item's own service\n * buffers (so neighbor buffers are enforced — review A3), and only the items\n * that actually reference `resourceId` count (so a multi-resource booking blocks\n * each resource only for its own item's window — review A4).\n */\nexport async function itemsToOccupancies(params: {\n bufferFor: (serviceId: number | string | undefined) => Promise<{ after: number; before: number }>\n capacityMode: CapacityMode\n items: ResolvedItem[]\n resourceId: number | string\n}): Promise<Occupancy[]> {\n const { bufferFor, capacityMode, items, resourceId } = params\n const occupancies: Occupancy[] = []\n\n for (const item of items) {\n if (String(item.resource) !== String(resourceId) || !item.endTime) {\n continue\n }\n const { after, before } = await bufferFor(item.service)\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n new Date(item.startTime),\n new Date(item.endTime),\n before,\n after,\n )\n occupancies.push({\n blockedEnd: effectiveEnd,\n blockedStart: effectiveStart,\n units: capacityMode === 'per-guest' ? item.guestCount : 1,\n })\n }\n\n return occupancies\n}\n\n/** Occupancy a single fetched reservation imposes on `resourceId`. */\nexport async function reservationOccupancies(params: {\n bufferFor: (serviceId: number | string | undefined) => Promise<{ after: number; before: number }>\n capacityMode: CapacityMode\n reservation: Record<string, unknown>\n resourceId: number | string\n}): Promise<Occupancy[]> {\n const { bufferFor, capacityMode, reservation, resourceId } = params\n return itemsToOccupancies({\n bufferFor,\n capacityMode,\n items: resolveReservationItems(reservation),\n resourceId,\n })\n}\n\nexport function isBlockingStatus(\n status: string,\n statusMachine: StatusMachineConfig,\n): boolean {\n return statusMachine.blockingStatuses.includes(status)\n}\n\nexport function validateTransition(\n fromStatus: string,\n toStatus: string,\n statusMachine: StatusMachineConfig,\n): { reason?: string; valid: boolean } {\n const allowed = statusMachine.transitions[fromStatus]\n if (!allowed) {\n return { reason: `Unknown status: ${fromStatus}`, valid: false }\n }\n if (!allowed.includes(toStatus)) {\n return {\n reason: `Cannot transition from \"${fromStatus}\" to \"${toStatus}\"`,\n valid: false,\n }\n }\n return { valid: true }\n}\n\n// --- DB functions (use Payload Local API only) ---\n\nexport async function checkAvailability(params: {\n blockingStatuses: string[]\n bufferAfter: number\n bufferBefore: number\n endTime: Date\n excludeReservationId?: number | string\n /** External busy resolver (calendar sync etc.) — intervals block the whole resource. */\n getExternalBusy?: GetExternalBusy\n guestCount: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n servicesSlug: string\n /** Other items from the same booking — counted as occupancy (review A5). */\n siblingItems?: ResolvedItem[]\n startTime: Date\n}): Promise<{\n available: boolean\n currentCount: number\n reason?: string\n totalCapacity: number\n}> {\n const {\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime,\n excludeReservationId,\n getExternalBusy,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n servicesSlug,\n siblingItems,\n startTime,\n } = params\n\n // Fetch resource for quantity and capacity mode\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: 0,\n req,\n })\n const quantity = (resource.quantity as number) ?? 1\n const capacityMode = ((resource.capacityMode as string) ?? 'per-reservation') as CapacityMode\n\n // Candidate window expanded by its own buffers\n const { effectiveEnd: candidateEnd, effectiveStart: candidateStart } = computeBlockedWindow(\n startTime,\n endTime,\n bufferBefore,\n bufferAfter,\n )\n\n // Coarse superset fetch — precise per-item overlap is computed in memory below\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 req,\n where: buildCoarseOverlapQuery({\n blockingStatuses,\n candidateEnd,\n candidateStart,\n excludeReservationId,\n resourceId,\n }),\n })\n\n // Per-call cache: fetch each distinct service's buffers at most once\n const bufferCache = new Map<string, { after: number; before: number }>()\n const bufferFor = async (\n serviceId: number | string | undefined,\n ): Promise<{ after: number; before: number }> => {\n const key = serviceId === undefined ? '' : String(serviceId)\n const cached = bufferCache.get(key)\n if (cached) {\n return cached\n }\n let result = { after: 0, before: 0 }\n if (serviceId !== undefined) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: servicesSlug,\n depth: 0,\n req,\n })\n if (service) {\n result = {\n after: (service.bufferTimeAfter as number) ?? 0,\n before: (service.bufferTimeBefore as number) ?? 0,\n }\n }\n } catch {\n // service missing — no buffers\n }\n }\n bufferCache.set(key, result)\n return result\n }\n\n const fetchedOccupancies = (\n await Promise.all(\n (docs as Array<Record<string, unknown>>).map((doc) =>\n reservationOccupancies({ bufferFor, capacityMode, reservation: doc, resourceId }),\n ),\n )\n ).flat()\n\n // Sibling items from the same booking (review A5) — expanded with the same\n // per-service buffers and capacity mode.\n const siblingOccupancies = siblingItems\n ? await itemsToOccupancies({ bufferFor, capacityMode, items: siblingItems, resourceId })\n : []\n\n // External busy (calendar sync etc.) — a synced calendar isn't unit-aware, so\n // an external event blocks the WHOLE resource (units = quantity), not one\n // unit. Fail-open: a resolver error must never block a real booking.\n let externalOccupancies: Occupancy[] = []\n if (getExternalBusy) {\n try {\n const intervals = await getExternalBusy({\n end: candidateEnd,\n req,\n resourceId,\n start: candidateStart,\n })\n externalOccupancies = intervals\n .map((iv) => ({\n blockedEnd: new Date(iv.end),\n blockedStart: new Date(iv.start),\n units: quantity,\n }))\n .filter((o) => !isNaN(o.blockedStart.getTime()) && !isNaN(o.blockedEnd.getTime()))\n } catch {\n externalOccupancies = []\n }\n }\n\n const occupancies = [...fetchedOccupancies, ...siblingOccupancies, ...externalOccupancies]\n\n // Sum the units of every occupancy whose buffered window overlaps the candidate\n const currentUnits = occupancies.reduce(\n (sum, occ) =>\n doRangesOverlap(candidateStart, candidateEnd, occ.blockedStart, occ.blockedEnd)\n ? sum + occ.units\n : sum,\n 0,\n )\n\n const candidateUnits = capacityMode === 'per-guest' ? guestCount : 1\n const available = currentUnits + candidateUnits <= quantity\n\n return {\n available,\n currentCount: currentUnits,\n reason: available\n ? undefined\n : capacityMode === 'per-guest'\n ? 'Guest capacity exceeded'\n : 'All units are booked for this time',\n totalCapacity: quantity,\n }\n}\n\nexport async function getAvailableSlots(params: {\n blockingStatuses: string[]\n date: Date | string\n /** External busy resolver (calendar sync etc.) — intervals block the whole resource. */\n getExternalBusy?: GetExternalBusy\n guestCount?: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId?: number | string\n resourceIds?: Array<number | string>\n resourceSlug: string\n scheduleSlug: string\n serviceId: number | string\n serviceSlug: string\n timeZone?: string\n}): Promise<Array<{ end: Date; start: Date }>> {\n const {\n blockingStatuses,\n date,\n getExternalBusy,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceIds,\n resourceSlug,\n scheduleSlug,\n serviceId,\n serviceSlug,\n timeZone,\n } = params\n\n const tz = timeZone ?? 'UTC'\n\n // Resolve the set of resources to intersect (single-resource callers still work)\n const ids =\n resourceIds && resourceIds.length > 0\n ? resourceIds\n : resourceId !== undefined\n ? [resourceId]\n : []\n if (ids.length === 0) {\n return []\n }\n\n // 1. Service for duration + buffer times (from the primary service)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: serviceSlug,\n depth: 0,\n req,\n })\n const duration = (service.duration as number) ?? 60\n const bufferBefore = (service.bufferTimeBefore as number) ?? 0\n const bufferAfter = (service.bufferTimeAfter as number) ?? 0\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n\n // 2. Per resource: fetch schedules and resolve to windows. A resource with >=1\n // schedule is \"schedule-bearing\" and constrains time; a resource with zero\n // schedules is capacity-only and contributes no time windows.\n const scheduleBearingWindowLists: Array<Array<{ end: Date; start: Date }>> = []\n for (const rid of ids) {\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 req,\n where: {\n and: [{ resource: { equals: rid } }, { active: { equals: true } }],\n },\n })\n if (!schedules || schedules.length === 0) {\n continue\n }\n // A11: an exception on ANY of the resource's schedules makes the whole\n // resource unavailable that day — not just the schedule it's recorded on.\n const exceptedToday = (schedules as Array<Record<string, unknown>>).some((s) =>\n isExceptionDate(\n date,\n (s.exceptions as Array<{ date: string; endDate?: string }> | undefined) ?? [],\n tz,\n ),\n )\n if (exceptedToday) {\n scheduleBearingWindowLists.push([])\n continue\n }\n const windows: Array<{ end: Date; start: Date }> = []\n for (const schedule of schedules) {\n windows.push(\n ...resolveScheduleForDate(\n schedule as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n tz,\n ),\n )\n }\n scheduleBearingWindowLists.push(windows)\n }\n\n // No resource constrains time → no basis for generating slots\n if (scheduleBearingWindowLists.length === 0) {\n return []\n }\n\n // 3. Intersect all schedule-bearing window lists\n let timeRanges = scheduleBearingWindowLists[0]\n for (let i = 1; i < scheduleBearingWindowLists.length; i++) {\n timeRanges = intersectIntervals(timeRanges, scheduleBearingWindowLists[i])\n }\n if (timeRanges.length === 0) {\n return []\n }\n\n // 4. Candidate slot sizing\n // NOTE: epoch-trick sizing is only meaningful for fixed/flexible durations.\n // full-day services return early via the range-as-slot branch below and never\n // consume slotDuration — keep it that way if reordering this function.\n const { endTime: slotEndOffset } = computeEndTime({\n durationType,\n serviceDuration: duration,\n startTime: new Date(0),\n timeZone: tz,\n })\n const slotDuration = Math.round(slotEndOffset.getTime() / 60_000)\n const effectiveDuration = durationType === 'fixed' ? duration : slotDuration\n\n // Helper: a window is available only if EVERY required resource is free\n const allAvailable = async (\n start: Date,\n end: Date,\n bBefore: number,\n bAfter: number,\n ): Promise<boolean> => {\n for (const rid of ids) {\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter: bAfter,\n bufferBefore: bBefore,\n endTime: end,\n getExternalBusy,\n guestCount: guestCount ?? 1,\n payload,\n req,\n reservationSlug,\n resourceId: rid,\n resourceSlug,\n servicesSlug: serviceSlug,\n startTime: start,\n })\n if (!result.available) {\n return false\n }\n }\n return true\n }\n\n const availableSlots: Array<{ end: Date; start: Date }> = []\n\n // Full-day: offer each range as a single slot if all resources are free\n if (durationType === 'full-day') {\n for (const range of timeRanges) {\n if (await allAvailable(range.start, range.end, 0, 0)) {\n availableSlots.push({ end: range.end, start: range.start })\n }\n }\n return availableSlots\n }\n\n const stepSize = Math.min(effectiveDuration, 15)\n\n for (const range of timeRanges) {\n let candidateStart = new Date(range.start)\n\n while (true) {\n const candidateEnd = addMinutes(candidateStart, effectiveDuration)\n if (candidateEnd > range.end) {\n break\n }\n\n if (await allAvailable(candidateStart, candidateEnd, bufferBefore, bufferAfter)) {\n availableSlots.push({ end: candidateEnd, start: new Date(candidateStart) })\n }\n\n candidateStart = addMinutes(candidateStart, stepSize)\n }\n }\n\n return availableSlots\n}\n"],"names":["resolveReservationItems","isExceptionDate","resolveScheduleForDate","addMinutes","computeBlockedWindow","doRangesOverlap","intersectIntervals","endOfDayInTimezone","COARSE_MARGIN_MS","computeEndTime","params","durationType","serviceDuration","startTime","end","timeZone","durationMinutes","Math","round","getTime","endTime","buildOverlapQuery","blockingStatuses","effectiveEnd","effectiveStart","excludeReservationId","resourceId","conditions","status","in","less_than","toISOString","greater_than","or","resource","equals","push","id","not_equals","and","buildCoarseOverlapQuery","candidateEnd","candidateStart","windowStart","Date","windowEnd","undefined","itemsToOccupancies","bufferFor","capacityMode","items","occupancies","item","String","after","before","service","blockedEnd","blockedStart","units","guestCount","reservationOccupancies","reservation","isBlockingStatus","statusMachine","includes","validateTransition","fromStatus","toStatus","allowed","transitions","reason","valid","checkAvailability","bufferAfter","bufferBefore","getExternalBusy","payload","req","reservationSlug","resourceSlug","servicesSlug","siblingItems","findByID","collection","depth","quantity","docs","find","limit","where","bufferCache","Map","serviceId","key","cached","get","result","bufferTimeAfter","bufferTimeBefore","set","fetchedOccupancies","Promise","all","map","doc","flat","siblingOccupancies","externalOccupancies","intervals","start","iv","filter","o","isNaN","currentUnits","reduce","sum","occ","candidateUnits","available","currentCount","totalCapacity","getAvailableSlots","date","resourceIds","scheduleSlug","serviceSlug","tz","ids","length","duration","scheduleBearingWindowLists","rid","schedules","active","exceptedToday","some","s","exceptions","windows","schedule","timeRanges","i","slotEndOffset","slotDuration","effectiveDuration","allAvailable","bBefore","bAfter","availableSlots","range","stepSize","min"],"mappings":"AAKA,SAASA,uBAAuB,QAAQ,0CAAyC;AACjF,SAASC,eAAe,EAAEC,sBAAsB,QAAQ,gCAA+B;AACvF,SACEC,UAAU,EACVC,oBAAoB,EACpBC,eAAe,EACfC,kBAAkB,QACb,4BAA2B;AAClC,SAASC,kBAAkB,QAAQ,gCAA+B;AAQlE;2EAC2E,GAC3E,MAAMC,mBAAmB,KAAK,KAAK,KAAK;AAExC,iCAAiC;AAEjC,OAAO,SAASC,eAAeC,MAM9B;IACC,MAAM,EAAEC,YAAY,EAAEC,eAAe,EAAEC,SAAS,EAAE,GAAGH;IAErD,IAAIC,iBAAiB,YAAY;QAC/B,MAAMG,MAAMP,mBAAmBM,WAAWH,OAAOK,QAAQ,IAAI;QAC7D,MAAMC,kBAAkBC,KAAKC,KAAK,CAAC,AAACJ,CAAAA,IAAIK,OAAO,KAAKN,UAAUM,OAAO,EAAC,IAAK;QAC3E,OAAO;YAAEH;YAAiBI,SAASN;QAAI;IACzC;IAEA,IAAIH,iBAAiB,cAAcD,OAAOU,OAAO,EAAE;QACjD,MAAMJ,kBAAkBC,KAAKC,KAAK,CAChC,AAACR,CAAAA,OAAOU,OAAO,CAACD,OAAO,KAAKN,UAAUM,OAAO,EAAC,IAAK;QAErD,OAAO;YAAEH;YAAiBI,SAASV,OAAOU,OAAO;QAAC;IACpD;IAEA,2BAA2B;IAC3B,MAAMA,UAAUjB,WAAWU,WAAWD;IACtC,OAAO;QAAEI,iBAAiBJ;QAAiBQ;IAAQ;AACrD;AAEA,OAAO,SAASC,kBAAkBX,MAMjC;IACC,MAAM,EAAEY,gBAAgB,EAAEC,YAAY,EAAEC,cAAc,EAAEC,oBAAoB,EAAEC,UAAU,EAAE,GACxFhB;IAEF,MAAMiB,aAAsB;QAC1B;YAAEC,QAAQ;gBAAEC,IAAIP;YAAiB;QAAE;QACnC;YAAET,WAAW;gBAAEiB,WAAWP,aAAaQ,WAAW;YAAG;QAAE;QACvD;YAAEX,SAAS;gBAAEY,cAAcR,eAAeO,WAAW;YAAG;QAAE;QAC1D;YACEE,IAAI;gBACF;oBAAEC,UAAU;wBAAEC,QAAQT;oBAAW;gBAAE;gBACnC;oBAAE,kBAAkB;wBAAES,QAAQT;oBAAW;gBAAE;aAC5C;QACH;KACD;IAED,IAAID,sBAAsB;QACxBE,WAAWS,IAAI,CAAC;YAAEC,IAAI;gBAAEC,YAAYb;YAAqB;QAAE;IAC7D;IAEA,OAAO;QAAEc,KAAKZ;IAAW;AAC3B;AAEA;;;;;;CAMC,GACD,OAAO,SAASa,wBAAwB9B,MAMvC;IACC,MAAM,EAAEY,gBAAgB,EAAEmB,YAAY,EAAEC,cAAc,EAAEjB,oBAAoB,EAAEC,UAAU,EAAE,GACxFhB;IACF,MAAMiC,cAAc,IAAIC,KAAKF,eAAevB,OAAO,KAAKX;IACxD,MAAMqC,YAAY,IAAID,KAAKH,aAAatB,OAAO,KAAKX;IAEpD,MAAMmB,aAAsB;QAC1B;YAAEC,QAAQ;gBAAEC,IAAIP;YAAiB;QAAE;QACnC;YAAET,WAAW;gBAAEiB,WAAWe,UAAUd,WAAW;YAAG;QAAE;QACpD;YAAEX,SAAS;gBAAEY,cAAcW,YAAYZ,WAAW;YAAG;QAAE;QACvD;YACEE,IAAI;gBAAC;oBAAEC,UAAU;wBAAEC,QAAQT;oBAAW;gBAAE;gBAAG;oBAAE,kBAAkB;wBAAES,QAAQT;oBAAW;gBAAE;aAAE;QAC1F;KACD;IAED,IAAID,yBAAyBqB,WAAW;QACtCnB,WAAWS,IAAI,CAAC;YAAEC,IAAI;gBAAEC,YAAYb;YAAqB;QAAE;IAC7D;IAEA,OAAO;QAAEc,KAAKZ;IAAW;AAC3B;AAEA;;;;;;CAMC,GACD,OAAO,eAAeoB,mBAAmBrC,MAKxC;IACC,MAAM,EAAEsC,SAAS,EAAEC,YAAY,EAAEC,KAAK,EAAExB,UAAU,EAAE,GAAGhB;IACvD,MAAMyC,cAA2B,EAAE;IAEnC,KAAK,MAAMC,QAAQF,MAAO;QACxB,IAAIG,OAAOD,KAAKlB,QAAQ,MAAMmB,OAAO3B,eAAe,CAAC0B,KAAKhC,OAAO,EAAE;YACjE;QACF;QACA,MAAM,EAAEkC,KAAK,EAAEC,MAAM,EAAE,GAAG,MAAMP,UAAUI,KAAKI,OAAO;QACtD,MAAM,EAAEjC,YAAY,EAAEC,cAAc,EAAE,GAAGpB,qBACvC,IAAIwC,KAAKQ,KAAKvC,SAAS,GACvB,IAAI+B,KAAKQ,KAAKhC,OAAO,GACrBmC,QACAD;QAEFH,YAAYf,IAAI,CAAC;YACfqB,YAAYlC;YACZmC,cAAclC;YACdmC,OAAOV,iBAAiB,cAAcG,KAAKQ,UAAU,GAAG;QAC1D;IACF;IAEA,OAAOT;AACT;AAEA,oEAAoE,GACpE,OAAO,eAAeU,uBAAuBnD,MAK5C;IACC,MAAM,EAAEsC,SAAS,EAAEC,YAAY,EAAEa,WAAW,EAAEpC,UAAU,EAAE,GAAGhB;IAC7D,OAAOqC,mBAAmB;QACxBC;QACAC;QACAC,OAAOlD,wBAAwB8D;QAC/BpC;IACF;AACF;AAEA,OAAO,SAASqC,iBACdnC,MAAc,EACdoC,aAAkC;IAElC,OAAOA,cAAc1C,gBAAgB,CAAC2C,QAAQ,CAACrC;AACjD;AAEA,OAAO,SAASsC,mBACdC,UAAkB,EAClBC,QAAgB,EAChBJ,aAAkC;IAElC,MAAMK,UAAUL,cAAcM,WAAW,CAACH,WAAW;IACrD,IAAI,CAACE,SAAS;QACZ,OAAO;YAAEE,QAAQ,CAAC,gBAAgB,EAAEJ,YAAY;YAAEK,OAAO;QAAM;IACjE;IACA,IAAI,CAACH,QAAQJ,QAAQ,CAACG,WAAW;QAC/B,OAAO;YACLG,QAAQ,CAAC,wBAAwB,EAAEJ,WAAW,MAAM,EAAEC,SAAS,CAAC,CAAC;YACjEI,OAAO;QACT;IACF;IACA,OAAO;QAAEA,OAAO;IAAK;AACvB;AAEA,oDAAoD;AAEpD,OAAO,eAAeC,kBAAkB/D,MAkBvC;IAMC,MAAM,EACJY,gBAAgB,EAChBoD,WAAW,EACXC,YAAY,EACZvD,OAAO,EACPK,oBAAoB,EACpBmD,eAAe,EACfhB,UAAU,EACViB,OAAO,EACPC,GAAG,EACHC,eAAe,EACfrD,UAAU,EACVsD,YAAY,EACZC,YAAY,EACZC,YAAY,EACZrE,SAAS,EACV,GAAGH;IAEJ,gDAAgD;IAChD,8DAA8D;IAC9D,MAAMwB,WAAW,MAAM,AAAC2C,QAAQM,QAAQ,CAAS;QAC/C9C,IAAIX;QACJ0D,YAAYJ;QACZK,OAAO;QACPP;IACF;IACA,MAAMQ,WAAW,AAACpD,SAASoD,QAAQ,IAAe;IAClD,MAAMrC,eAAgB,AAACf,SAASe,YAAY,IAAe;IAE3D,+CAA+C;IAC/C,MAAM,EAAE1B,cAAckB,YAAY,EAAEjB,gBAAgBkB,cAAc,EAAE,GAAGtC,qBACrES,WACAO,SACAuD,cACAD;IAGF,+EAA+E;IAC/E,8DAA8D;IAC9D,MAAM,EAAEa,IAAI,EAAE,GAAG,MAAM,AAACV,QAAQW,IAAI,CAAS;QAC3CJ,YAAYL;QACZM,OAAO;QACPI,OAAO;QACPX;QACAY,OAAOlD,wBAAwB;YAC7BlB;YACAmB;YACAC;YACAjB;YACAC;QACF;IACF;IAEA,qEAAqE;IACrE,MAAMiE,cAAc,IAAIC;IACxB,MAAM5C,YAAY,OAChB6C;QAEA,MAAMC,MAAMD,cAAc/C,YAAY,KAAKO,OAAOwC;QAClD,MAAME,SAASJ,YAAYK,GAAG,CAACF;QAC/B,IAAIC,QAAQ;YACV,OAAOA;QACT;QACA,IAAIE,SAAS;YAAE3C,OAAO;YAAGC,QAAQ;QAAE;QACnC,IAAIsC,cAAc/C,WAAW;YAC3B,IAAI;gBACF,8DAA8D;gBAC9D,MAAMU,UAAU,MAAM,AAACqB,QAAQM,QAAQ,CAAS;oBAC9C9C,IAAIwD;oBACJT,YAAYH;oBACZI,OAAO;oBACPP;gBACF;gBACA,IAAItB,SAAS;oBACXyC,SAAS;wBACP3C,OAAO,AAACE,QAAQ0C,eAAe,IAAe;wBAC9C3C,QAAQ,AAACC,QAAQ2C,gBAAgB,IAAe;oBAClD;gBACF;YACF,EAAE,OAAM;YACN,+BAA+B;YACjC;QACF;QACAR,YAAYS,GAAG,CAACN,KAAKG;QACrB,OAAOA;IACT;IAEA,MAAMI,qBAAqB,AACzB,CAAA,MAAMC,QAAQC,GAAG,CACf,AAAChB,KAAwCiB,GAAG,CAAC,CAACC,MAC5C5C,uBAAuB;YAAEb;YAAWC;YAAca,aAAa2C;YAAK/E;QAAW,IAEnF,EACAgF,IAAI;IAEN,2EAA2E;IAC3E,yCAAyC;IACzC,MAAMC,qBAAqBzB,eACvB,MAAMnC,mBAAmB;QAAEC;QAAWC;QAAcC,OAAOgC;QAAcxD;IAAW,KACpF,EAAE;IAEN,8EAA8E;IAC9E,0EAA0E;IAC1E,qEAAqE;IACrE,IAAIkF,sBAAmC,EAAE;IACzC,IAAIhC,iBAAiB;QACnB,IAAI;YACF,MAAMiC,YAAY,MAAMjC,gBAAgB;gBACtC9D,KAAK2B;gBACLqC;gBACApD;gBACAoF,OAAOpE;YACT;YACAkE,sBAAsBC,UACnBL,GAAG,CAAC,CAACO,KAAQ,CAAA;oBACZtD,YAAY,IAAIb,KAAKmE,GAAGjG,GAAG;oBAC3B4C,cAAc,IAAId,KAAKmE,GAAGD,KAAK;oBAC/BnD,OAAO2B;gBACT,CAAA,GACC0B,MAAM,CAAC,CAACC,IAAM,CAACC,MAAMD,EAAEvD,YAAY,CAACvC,OAAO,OAAO,CAAC+F,MAAMD,EAAExD,UAAU,CAACtC,OAAO;QAClF,EAAE,OAAM;YACNyF,sBAAsB,EAAE;QAC1B;IACF;IAEA,MAAMzD,cAAc;WAAIkD;WAAuBM;WAAuBC;KAAoB;IAE1F,gFAAgF;IAChF,MAAMO,eAAehE,YAAYiE,MAAM,CACrC,CAACC,KAAKC,MACJjH,gBAAgBqC,gBAAgBD,cAAc6E,IAAI5D,YAAY,EAAE4D,IAAI7D,UAAU,IAC1E4D,MAAMC,IAAI3D,KAAK,GACf0D,KACN;IAGF,MAAME,iBAAiBtE,iBAAiB,cAAcW,aAAa;IACnE,MAAM4D,YAAYL,eAAeI,kBAAkBjC;IAEnD,OAAO;QACLkC;QACAC,cAAcN;QACd5C,QAAQiD,YACJ1E,YACAG,iBAAiB,cACf,4BACA;QACNyE,eAAepC;IACjB;AACF;AAEA,OAAO,eAAeqC,kBAAkBjH,MAgBvC;IACC,MAAM,EACJY,gBAAgB,EAChBsG,IAAI,EACJhD,eAAe,EACfhB,UAAU,EACViB,OAAO,EACPC,GAAG,EACHC,eAAe,EACfrD,UAAU,EACVmG,WAAW,EACX7C,YAAY,EACZ8C,YAAY,EACZjC,SAAS,EACTkC,WAAW,EACXhH,QAAQ,EACT,GAAGL;IAEJ,MAAMsH,KAAKjH,YAAY;IAEvB,iFAAiF;IACjF,MAAMkH,MACJJ,eAAeA,YAAYK,MAAM,GAAG,IAChCL,cACAnG,eAAeoB,YACb;QAACpB;KAAW,GACZ,EAAE;IACV,IAAIuG,IAAIC,MAAM,KAAK,GAAG;QACpB,OAAO,EAAE;IACX;IAEA,oEAAoE;IACpE,8DAA8D;IAC9D,MAAM1E,UAAU,MAAM,AAACqB,QAAQM,QAAQ,CAAS;QAC9C9C,IAAIwD;QACJT,YAAY2C;QACZ1C,OAAO;QACPP;IACF;IACA,MAAMqD,WAAW,AAAC3E,QAAQ2E,QAAQ,IAAe;IACjD,MAAMxD,eAAe,AAACnB,QAAQ2C,gBAAgB,IAAe;IAC7D,MAAMzB,cAAc,AAAClB,QAAQ0C,eAAe,IAAe;IAC3D,MAAMvF,eAAgB,AAAC6C,QAAQ7C,YAAY,IAAe;IAE1D,+EAA+E;IAC/E,8EAA8E;IAC9E,iEAAiE;IACjE,MAAMyH,6BAAuE,EAAE;IAC/E,KAAK,MAAMC,OAAOJ,IAAK;QACrB,8DAA8D;QAC9D,MAAM,EAAE1C,MAAM+C,SAAS,EAAE,GAAG,MAAM,AAACzD,QAAQW,IAAI,CAAS;YACtDJ,YAAY0C;YACZzC,OAAO;YACPI,OAAO;YACPX;YACAY,OAAO;gBACLnD,KAAK;oBAAC;wBAAEL,UAAU;4BAAEC,QAAQkG;wBAAI;oBAAE;oBAAG;wBAAEE,QAAQ;4BAAEpG,QAAQ;wBAAK;oBAAE;iBAAE;YACpE;QACF;QACA,IAAI,CAACmG,aAAaA,UAAUJ,MAAM,KAAK,GAAG;YACxC;QACF;QACA,uEAAuE;QACvE,0EAA0E;QAC1E,MAAMM,gBAAgB,AAACF,UAA6CG,IAAI,CAAC,CAACC,IACxEzI,gBACE2H,MACA,AAACc,EAAEC,UAAU,IAA8D,EAAE,EAC7EX;QAGJ,IAAIQ,eAAe;YACjBJ,2BAA2BhG,IAAI,CAAC,EAAE;YAClC;QACF;QACA,MAAMwG,UAA6C,EAAE;QACrD,KAAK,MAAMC,YAAYP,UAAW;YAChCM,QAAQxG,IAAI,IACPlC,uBACD2I,UACAjB,MACAI;QAGN;QACAI,2BAA2BhG,IAAI,CAACwG;IAClC;IAEA,8DAA8D;IAC9D,IAAIR,2BAA2BF,MAAM,KAAK,GAAG;QAC3C,OAAO,EAAE;IACX;IAEA,iDAAiD;IACjD,IAAIY,aAAaV,0BAA0B,CAAC,EAAE;IAC9C,IAAK,IAAIW,IAAI,GAAGA,IAAIX,2BAA2BF,MAAM,EAAEa,IAAK;QAC1DD,aAAaxI,mBAAmBwI,YAAYV,0BAA0B,CAACW,EAAE;IAC3E;IACA,IAAID,WAAWZ,MAAM,KAAK,GAAG;QAC3B,OAAO,EAAE;IACX;IAEA,2BAA2B;IAC3B,4EAA4E;IAC5E,8EAA8E;IAC9E,uEAAuE;IACvE,MAAM,EAAE9G,SAAS4H,aAAa,EAAE,GAAGvI,eAAe;QAChDE;QACAC,iBAAiBuH;QACjBtH,WAAW,IAAI+B,KAAK;QACpB7B,UAAUiH;IACZ;IACA,MAAMiB,eAAehI,KAAKC,KAAK,CAAC8H,cAAc7H,OAAO,KAAK;IAC1D,MAAM+H,oBAAoBvI,iBAAiB,UAAUwH,WAAWc;IAEhE,wEAAwE;IACxE,MAAME,eAAe,OACnBrC,OACAhG,KACAsI,SACAC;QAEA,KAAK,MAAMhB,OAAOJ,IAAK;YACrB,MAAMhC,SAAS,MAAMxB,kBAAkB;gBACrCnD;gBACAoD,aAAa2E;gBACb1E,cAAcyE;gBACdhI,SAASN;gBACT8D;gBACAhB,YAAYA,cAAc;gBAC1BiB;gBACAC;gBACAC;gBACArD,YAAY2G;gBACZrD;gBACAC,cAAc8C;gBACdlH,WAAWiG;YACb;YACA,IAAI,CAACb,OAAOuB,SAAS,EAAE;gBACrB,OAAO;YACT;QACF;QACA,OAAO;IACT;IAEA,MAAM8B,iBAAoD,EAAE;IAE5D,wEAAwE;IACxE,IAAI3I,iBAAiB,YAAY;QAC/B,KAAK,MAAM4I,SAAST,WAAY;YAC9B,IAAI,MAAMK,aAAaI,MAAMzC,KAAK,EAAEyC,MAAMzI,GAAG,EAAE,GAAG,IAAI;gBACpDwI,eAAelH,IAAI,CAAC;oBAAEtB,KAAKyI,MAAMzI,GAAG;oBAAEgG,OAAOyC,MAAMzC,KAAK;gBAAC;YAC3D;QACF;QACA,OAAOwC;IACT;IAEA,MAAME,WAAWvI,KAAKwI,GAAG,CAACP,mBAAmB;IAE7C,KAAK,MAAMK,SAAST,WAAY;QAC9B,IAAIpG,iBAAiB,IAAIE,KAAK2G,MAAMzC,KAAK;QAEzC,MAAO,KAAM;YACX,MAAMrE,eAAetC,WAAWuC,gBAAgBwG;YAChD,IAAIzG,eAAe8G,MAAMzI,GAAG,EAAE;gBAC5B;YACF;YAEA,IAAI,MAAMqI,aAAazG,gBAAgBD,cAAckC,cAAcD,cAAc;gBAC/E4E,eAAelH,IAAI,CAAC;oBAAEtB,KAAK2B;oBAAcqE,OAAO,IAAIlE,KAAKF;gBAAgB;YAC3E;YAEAA,iBAAiBvC,WAAWuC,gBAAgB8G;QAC9C;IACF;IAEA,OAAOF;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../src/services/AvailabilityService.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport type { CapacityMode, DurationType, GetExternalBusy, StatusMachineConfig } from '../types.js'\nimport type { ResolvedItem } from '../utilities/resolveReservationItems.js'\n\nimport { NOOP_RESERVE_DEBUG, type ReserveDebug } from '../utilities/reserveDebug.js'\nimport { resolveReservationItems } from '../utilities/resolveReservationItems.js'\nimport { isExceptionDate, resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport {\n addMinutes,\n computeBlockedWindow,\n doRangesOverlap,\n intersectIntervals,\n} from '../utilities/slotUtils.js'\nimport { endOfDayInTimezone } from '../utilities/timezoneUtils.js'\n\n/** A window during which a resource is occupied. Reservation/sibling\n * occupancies are expanded by buffer times; external intervals (from\n * `getExternalBusy`) are used as-is — only the candidate window itself\n * carries buffers when checked against them. */\nexport type Occupancy = { blockedEnd: Date; blockedStart: Date; units: number }\n\n/** Coarse pre-filter widen: covers any realistic neighbor buffer (buffers are\n * minutes). The precise per-item overlap check runs in memory afterwards. */\nconst COARSE_MARGIN_MS = 24 * 60 * 60 * 1000\n\n// --- Pure functions (no DB) ---\n\nexport function computeEndTime(params: {\n durationType: DurationType\n endTime?: Date\n serviceDuration: number\n startTime: Date\n timeZone?: string\n}): { durationMinutes: number; endTime: Date } {\n const { durationType, serviceDuration, startTime } = params\n\n if (durationType === 'full-day') {\n const end = endOfDayInTimezone(startTime, params.timeZone ?? 'UTC')\n const durationMinutes = Math.round((end.getTime() - startTime.getTime()) / 60_000)\n return { durationMinutes, endTime: end }\n }\n\n if (durationType === 'flexible' && params.endTime) {\n const durationMinutes = Math.round(\n (params.endTime.getTime() - startTime.getTime()) / 60_000,\n )\n return { durationMinutes, endTime: params.endTime }\n }\n\n // fixed duration (default)\n const endTime = addMinutes(startTime, serviceDuration)\n return { durationMinutes: serviceDuration, endTime }\n}\n\nexport function buildOverlapQuery(params: {\n blockingStatuses: string[]\n effectiveEnd: Date\n effectiveStart: Date\n excludeReservationId?: number | string\n resourceId: number | string\n}): Where {\n const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } =\n params\n\n const conditions: Where[] = [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\n {\n or: [\n { resource: { equals: resourceId } },\n { 'items.resource': { equals: resourceId } },\n ],\n },\n ]\n\n if (excludeReservationId) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\n/**\n * Coarse superset query: blocking reservations whose top-level (span) window\n * comes within COARSE_MARGIN_MS of the candidate window and reference the\n * resource at top level or in items[]. The precise per-item overlap is computed\n * in memory afterwards — the top-level span is a superset of every item's\n * window, so this never misses a real conflict (margin covers neighbor buffers).\n */\nexport function buildCoarseOverlapQuery(params: {\n blockingStatuses: string[]\n candidateEnd: Date\n candidateStart: Date\n excludeReservationId?: number | string\n resourceId: number | string\n}): Where {\n const { blockingStatuses, candidateEnd, candidateStart, excludeReservationId, resourceId } =\n params\n const windowStart = new Date(candidateStart.getTime() - COARSE_MARGIN_MS)\n const windowEnd = new Date(candidateEnd.getTime() + COARSE_MARGIN_MS)\n\n const conditions: Where[] = [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: windowEnd.toISOString() } },\n { endTime: { greater_than: windowStart.toISOString() } },\n {\n or: [{ resource: { equals: resourceId } }, { 'items.resource': { equals: resourceId } }],\n },\n ]\n\n if (excludeReservationId !== undefined) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\n/**\n * The occupancy windows a set of resolved items imposes on `resourceId`. Each\n * matching item's [startTime, endTime) is expanded by that item's own service\n * buffers (so neighbor buffers are enforced — review A3), and only the items\n * that actually reference `resourceId` count (so a multi-resource booking blocks\n * each resource only for its own item's window — review A4).\n */\nexport async function itemsToOccupancies(params: {\n bufferFor: (serviceId: number | string | undefined) => Promise<{ after: number; before: number }>\n capacityMode: CapacityMode\n items: ResolvedItem[]\n resourceId: number | string\n}): Promise<Occupancy[]> {\n const { bufferFor, capacityMode, items, resourceId } = params\n const occupancies: Occupancy[] = []\n\n for (const item of items) {\n if (String(item.resource) !== String(resourceId) || !item.endTime) {\n continue\n }\n const { after, before } = await bufferFor(item.service)\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n new Date(item.startTime),\n new Date(item.endTime),\n before,\n after,\n )\n occupancies.push({\n blockedEnd: effectiveEnd,\n blockedStart: effectiveStart,\n units: capacityMode === 'per-guest' ? item.guestCount : 1,\n })\n }\n\n return occupancies\n}\n\n/**\n * Occupancy a single fetched reservation imposes on `resourceId`.\n *\n * Resolves `reservation` LENIENTLY: this is a stored document, which — unlike\n * a write in progress — can carry an inverted top-level window from a\n * `context.skipReservationHooks` write or data predating that check. Lenient\n * mode skips parent synthesis instead of throwing, so a malformed top-level\n * window never discards the real `items[]` occupancies also present on the\n * same row (see the lenient-mode note in resolveReservationItems.ts).\n */\nexport async function reservationOccupancies(params: {\n bufferFor: (serviceId: number | string | undefined) => Promise<{ after: number; before: number }>\n capacityMode: CapacityMode\n reservation: Record<string, unknown>\n resourceId: number | string\n}): Promise<Occupancy[]> {\n const { bufferFor, capacityMode, reservation, resourceId } = params\n return itemsToOccupancies({\n bufferFor,\n capacityMode,\n items: resolveReservationItems(reservation, { lenient: true }),\n resourceId,\n })\n}\n\nexport function isBlockingStatus(\n status: string,\n statusMachine: StatusMachineConfig,\n): boolean {\n return statusMachine.blockingStatuses.includes(status)\n}\n\nexport function validateTransition(\n fromStatus: string,\n toStatus: string,\n statusMachine: StatusMachineConfig,\n): { reason?: string; valid: boolean } {\n const allowed = statusMachine.transitions[fromStatus]\n if (!allowed) {\n return { reason: `Unknown status: ${fromStatus}`, valid: false }\n }\n if (!allowed.includes(toStatus)) {\n return {\n reason: `Cannot transition from \"${fromStatus}\" to \"${toStatus}\"`,\n valid: false,\n }\n }\n return { valid: true }\n}\n\n// --- DB functions (use Payload Local API only) ---\n\nexport async function checkAvailability(params: {\n blockingStatuses: string[]\n bufferAfter: number\n bufferBefore: number\n /** Optional tracer — emits check/check_result/error lines when enabled. */\n debug?: ReserveDebug\n endTime: Date\n /** Token of the hold being converted into this booking — never self-blocks. */\n excludeHoldToken?: string\n excludeReservationId?: number | string\n /** External busy resolver (calendar sync etc.) — intervals block the whole resource. */\n getExternalBusy?: GetExternalBusy\n guestCount: number\n /** Slug of the slot-holds collection; omit to ignore holds entirely. */\n holdsSlug?: string\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n servicesSlug: string\n /** Other items from the same booking — counted as occupancy (review A5). */\n siblingItems?: ResolvedItem[]\n startTime: Date\n}): Promise<{\n available: boolean\n currentCount: number\n reason?: string\n totalCapacity: number\n}> {\n const {\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n debug,\n endTime,\n excludeHoldToken,\n excludeReservationId,\n getExternalBusy,\n guestCount,\n holdsSlug,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n servicesSlug,\n siblingItems,\n startTime,\n } = params\n\n const trace = debug ?? NOOP_RESERVE_DEBUG\n\n // Fetch resource for quantity and capacity mode\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: 0,\n req,\n })\n const quantity = (resource.quantity as number) ?? 1\n const capacityMode = ((resource.capacityMode as string) ?? 'per-reservation') as CapacityMode\n\n // Candidate window expanded by its own buffers\n const { effectiveEnd: candidateEnd, effectiveStart: candidateStart } = computeBlockedWindow(\n startTime,\n endTime,\n bufferBefore,\n bufferAfter,\n )\n\n // Coarse superset fetch — precise per-item overlap is computed in memory below\n const coarseWhere = buildCoarseOverlapQuery({\n blockingStatuses,\n candidateEnd,\n candidateStart,\n excludeReservationId,\n resourceId,\n })\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 req,\n where: coarseWhere,\n })\n\n trace.dbg('check', {\n blockingReservations: (docs as unknown[]).length,\n candidateEnd: candidateEnd.toISOString(),\n candidateStart: candidateStart.toISOString(),\n capacityMode,\n coarseWhere,\n quantity,\n resourceId,\n })\n\n // Per-call cache: fetch each distinct service's buffers at most once\n const bufferCache = new Map<string, { after: number; before: number }>()\n const bufferFor = async (\n serviceId: number | string | undefined,\n ): Promise<{ after: number; before: number }> => {\n const key = serviceId === undefined ? '' : String(serviceId)\n const cached = bufferCache.get(key)\n if (cached) {\n return cached\n }\n let result = { after: 0, before: 0 }\n if (serviceId !== undefined) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: servicesSlug,\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 })\n if (service) {\n result = {\n after: (service.bufferTimeAfter as number) ?? 0,\n before: (service.bufferTimeBefore as number) ?? 0,\n }\n }\n } catch (err) {\n trace.dbg('error', { err, serviceId, where: 'bufferFor' })\n }\n }\n bufferCache.set(key, result)\n return result\n }\n\n // reservationOccupancies resolves each doc LENIENTLY (see its own doc\n // comment) — an inverted top-level window on a stored row skips parent\n // synthesis rather than throwing, so this never needs a try/catch: every\n // real items[] occupancy on the row still comes back.\n //\n // SEQUENTIAL, not Promise.all, and that is load-bearing. Each iteration can\n // reach `bufferFor` -> `payload.findByID` on the shared `req`, and this whole\n // function runs inside the booking's own transaction on the write path. A\n // MongoDB ClientSession cannot carry concurrent operations inside a\n // transaction: when two do collide, the loser's `count`/`findByID` calls\n // `killTransaction` from its own catch, which rolls back and clears the\n // transaction the enclosing create/delete owns, and the survivor then reports\n // `NoSuchTransaction` (\"transaction number N does not match any in-progress\n // transactions\"). Buffers are per-service and cached, so the loop is at most\n // one read per DISTINCT neighbouring service either way.\n const fetchedOccupancies: Occupancy[] = []\n for (const doc of docs as Array<Record<string, unknown>>) {\n fetchedOccupancies.push(\n ...(await reservationOccupancies({ bufferFor, capacityMode, reservation: doc, resourceId })),\n )\n }\n\n // Sibling items from the same booking (review A5) — expanded with the same\n // per-service buffers and capacity mode.\n const siblingOccupancies = siblingItems\n ? await itemsToOccupancies({ bufferFor, capacityMode, items: siblingItems, resourceId })\n : []\n\n // External busy (calendar sync etc.) — a synced calendar isn't unit-aware, so\n // an external event blocks the WHOLE resource (units = quantity), not one\n // unit. Fail-open: a resolver error must never block a real booking.\n let externalOccupancies: Occupancy[] = []\n if (getExternalBusy) {\n try {\n const intervals = await getExternalBusy({\n end: candidateEnd,\n req,\n resourceId,\n start: candidateStart,\n })\n externalOccupancies = intervals\n .map((iv) => ({\n blockedEnd: new Date(iv.end),\n blockedStart: new Date(iv.start),\n units: quantity,\n }))\n .filter((o) => !isNaN(o.blockedStart.getTime()) && !isNaN(o.blockedEnd.getTime()))\n } catch (err) {\n trace.dbg('error', { err, resourceId, where: 'getExternalBusy' })\n externalOccupancies = []\n }\n }\n\n // Unexpired slot holds occupy the resource exactly as a blocking reservation\n // does. Expiry is a read-time predicate rather than a TTL index so the\n // behaviour is identical on Mongo, Postgres and SQLite. `excludeHoldToken`\n // is the bearer converting their OWN hold into a booking — without it the\n // hold would block the very booking it was taken to protect.\n const holdOccupancies: Occupancy[] = []\n if (holdsSlug) {\n const holdWhere: Where = {\n and: [\n { resource: { equals: resourceId } },\n { expiresAt: { greater_than: new Date().toISOString() } },\n { startTime: { less_than: candidateEnd.toISOString() } },\n { endTime: { greater_than: candidateStart.toISOString() } },\n ...(excludeHoldToken ? [{ token: { not_equals: excludeHoldToken } }] : []),\n ],\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: holdDocs } = await (payload.find as any)({\n collection: holdsSlug,\n depth: 0,\n limit: 0,\n req,\n where: holdWhere,\n })\n\n // Sequential for the same reason the reservation loop above is: `bufferFor`\n // reads through the shared `req`, and concurrent operations on one MongoDB\n // session inside a transaction abort it.\n for (const hold of holdDocs as Array<Record<string, unknown>>) {\n // Expand by the held service's own buffers, so a hold reserves the same\n // real-world window the resulting booking will.\n const { after, before } = await bufferFor(hold.service as number | string | undefined)\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n new Date(hold.startTime as string),\n new Date(hold.endTime as string),\n before,\n after,\n )\n holdOccupancies.push({\n blockedEnd: effectiveEnd,\n blockedStart: effectiveStart,\n units: capacityMode === 'per-guest' ? ((hold.guestCount as number) ?? 1) : 1,\n })\n }\n\n trace.dbg('check_holds', { activeHolds: holdOccupancies.length, resourceId })\n }\n\n const occupancies = [\n ...fetchedOccupancies,\n ...siblingOccupancies,\n ...externalOccupancies,\n ...holdOccupancies,\n ]\n\n // Sum the units of every occupancy whose buffered window overlaps the candidate\n const currentUnits = occupancies.reduce(\n (sum, occ) =>\n doRangesOverlap(candidateStart, candidateEnd, occ.blockedStart, occ.blockedEnd)\n ? sum + occ.units\n : sum,\n 0,\n )\n\n const candidateUnits = capacityMode === 'per-guest' ? guestCount : 1\n const available = currentUnits + candidateUnits <= quantity\n\n trace.dbg('check_result', {\n available,\n candidateUnits,\n currentUnits,\n occupancySources: {\n external: externalOccupancies.length,\n fetched: fetchedOccupancies.length,\n sibling: siblingOccupancies.length,\n },\n quantity,\n resourceId,\n })\n\n return {\n available,\n currentCount: currentUnits,\n reason: available\n ? undefined\n : capacityMode === 'per-guest'\n ? 'Guest capacity exceeded'\n : 'All units are booked for this time',\n totalCapacity: quantity,\n }\n}\n\n/**\n * Why availability came back empty. Returned alongside the slots so an empty\n * result is diagnosable without turning on `debug`.\n *\n * `no_active_schedules` and `date_excepted` are deliberately absent: they are\n * per-resource `skip` traces inside a loop, not return points — they funnel\n * into `no_windows` — and stay debug-only.\n *\n * `window_too_short` is reachable only from the general (non-full-day) branch:\n * the full-day branch returns `no_windows` before it, so it always has at least\n * one candidate.\n */\nexport type EmptyReason =\n | 'all_slots_taken'\n | 'empty_intersection'\n | 'no_resource_ids'\n | 'no_windows'\n | 'resource_inactive'\n | 'service_inactive'\n | 'window_too_short'\n\nexport async function getAvailableSlots(params: {\n blockingStatuses: string[]\n date: Date | string\n /** Optional tracer — emits per-stage slot-generation lines when enabled. */\n debug?: ReserveDebug\n /** Skip the service/resource `active` short-circuits when explicitly `false`. */\n enforceActive?: boolean\n /** External busy resolver (calendar sync etc.) — intervals block the whole resource. */\n getExternalBusy?: GetExternalBusy\n guestCount?: number\n /**\n * Slug of the slot-holds collection; omit to ignore holds entirely.\n *\n * Load-bearing, not optional polish: without it every read-path caller\n * (`/reserve/availability`, `/reserve/slots`, the reservation form's slot\n * picker) advertises a held slot as FREE, and the customer who clicks it is\n * rejected by the write path — which DOES count holds — as a 409. Pass it\n * whenever `slotHolds.enabled`.\n */\n holdsSlug?: string\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId?: number | string\n resourceIds?: Array<number | string>\n resourceSlug: string\n scheduleSlug: string\n serviceId: number | string\n serviceSlug: string\n timeZone?: string\n}): Promise<{ reason?: EmptyReason; slots: Array<{ end: Date; start: Date }> }> {\n const {\n blockingStatuses,\n date,\n debug,\n enforceActive,\n getExternalBusy,\n guestCount,\n holdsSlug,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceIds,\n resourceSlug,\n scheduleSlug,\n serviceId,\n serviceSlug,\n timeZone,\n } = params\n\n const tz = timeZone ?? 'UTC'\n\n // Resolve the set of resources to intersect (single-resource callers still work)\n const ids =\n resourceIds && resourceIds.length > 0\n ? resourceIds\n : resourceId !== undefined\n ? [resourceId]\n : []\n\n const trace = (debug ?? NOOP_RESERVE_DEBUG).child({ resourceIds: ids, serviceId })\n trace.dbg('input', {\n date: date instanceof Date ? date.toISOString() : date,\n guestCount: guestCount ?? 1,\n timeZone: tz,\n })\n\n if (ids.length === 0) {\n trace.dbg('empty', { reason: 'no_resource_ids' })\n return { reason: 'no_resource_ids', slots: [] }\n }\n\n // 1. Service for duration + buffer times (from the primary service)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: serviceSlug,\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 })\n\n if (enforceActive !== false && service?.active === false) {\n trace.dbg('empty', { reason: 'service_inactive', serviceId })\n return { reason: 'service_inactive', slots: [] }\n }\n\n const duration = (service.duration as number) ?? 60\n const bufferBefore = (service.bufferTimeBefore as number) ?? 0\n const bufferAfter = (service.bufferTimeAfter as number) ?? 0\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n trace.dbg('service', { bufferAfter, bufferBefore, duration, durationType })\n\n // 2. Per resource: fetch schedules and resolve to windows. A resource with >=1\n // schedule is \"schedule-bearing\" and constrains time; a resource with zero\n // schedules is capacity-only and contributes no time windows.\n const scheduleBearingWindowLists: Array<Array<{ end: Date; start: Date }>> = []\n for (const rid of ids) {\n // This read exists solely to feed the active check below — skip it\n // entirely when enforceActive is off so an opted-out consumer doesn't pay\n // for a findByID whose result is never read.\n if (enforceActive !== false) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resourceDoc = await (payload.findByID as any)({\n id: rid,\n collection: resourceSlug,\n depth: 0,\n // Skip joins — internal logic never reads them, and without this every\n // resource read becomes an aggregation with a $lookup.\n joins: false,\n req,\n }).catch(() => null)\n if (resourceDoc?.active === false) {\n trace.dbg('empty', { reason: 'resource_inactive', resourceId: rid })\n return { reason: 'resource_inactive', slots: [] }\n }\n }\n\n const scheduleWhere = { and: [{ resource: { equals: rid } }, { active: { equals: true } }] }\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 req,\n where: scheduleWhere,\n })\n trace.dbg('schedules', {\n activeFlags: (schedules as Array<Record<string, unknown>>).map((s) => s.active),\n resourceId: rid,\n rowsFound: (schedules as unknown[]).length,\n scheduleWhere,\n })\n if (!schedules || schedules.length === 0) {\n trace.dbg('skip', { reason: 'no_active_schedules', resourceId: rid, rowsFound: 0 })\n continue\n }\n // A11: an exception on ANY of the resource's schedules makes the whole\n // resource unavailable that day — not just the schedule it's recorded on.\n let matched: { exception: unknown; scheduleId: unknown } | undefined\n for (const s of schedules as Array<Record<string, unknown>>) {\n const exs = (s.exceptions as Array<{ date: string; endDate?: string }> | undefined) ?? []\n if (isExceptionDate(date, exs, tz)) {\n matched = { exception: exs.find((e) => isExceptionDate(date, [e], tz)), scheduleId: s.id }\n break\n }\n }\n if (matched) {\n trace.dbg('skip', {\n matchedException: matched.exception,\n reason: 'date_excepted',\n resourceId: rid,\n scheduleId: matched.scheduleId,\n })\n scheduleBearingWindowLists.push([])\n continue\n }\n const windows: Array<{ end: Date; start: Date }> = []\n for (const schedule of schedules) {\n windows.push(\n ...resolveScheduleForDate(\n schedule as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n tz,\n ),\n )\n }\n trace.dbg('windows', {\n resourceId: rid,\n windowCount: windows.length,\n windows: windows.map((w) => ({ end: w.end.toISOString(), start: w.start.toISOString() })),\n windowsEmpty: windows.length === 0,\n })\n scheduleBearingWindowLists.push(windows)\n }\n\n // No resource constrains time → no basis for generating slots\n if (scheduleBearingWindowLists.length === 0) {\n trace.dbg('empty', { reason: 'no_windows' })\n return { reason: 'no_windows', slots: [] }\n }\n\n // 3. Intersect all schedule-bearing window lists\n let timeRanges = scheduleBearingWindowLists[0]\n for (let i = 1; i < scheduleBearingWindowLists.length; i++) {\n timeRanges = intersectIntervals(timeRanges, scheduleBearingWindowLists[i])\n }\n if (timeRanges.length === 0) {\n trace.dbg('empty', {\n perResourceWindows: scheduleBearingWindowLists.map((list) =>\n list.map((w) => ({ end: w.end.toISOString(), start: w.start.toISOString() })),\n ),\n reason: 'empty_intersection',\n })\n return { reason: 'empty_intersection', slots: [] }\n }\n\n // 4. Candidate slot sizing\n // NOTE: epoch-trick sizing is only meaningful for fixed/flexible durations.\n // full-day services return early via the range-as-slot branch below and never\n // consume slotDuration — keep it that way if reordering this function.\n const { endTime: slotEndOffset } = computeEndTime({\n durationType,\n serviceDuration: duration,\n startTime: new Date(0),\n timeZone: tz,\n })\n const slotDuration = Math.round(slotEndOffset.getTime() / 60_000)\n const effectiveDuration = durationType === 'fixed' ? duration : slotDuration\n trace.dbg('sizing', {\n effectiveDuration,\n slotDuration,\n stepSize: Math.min(effectiveDuration, 15),\n })\n\n // Helper: a window is available only if EVERY required resource is free\n const allAvailable = async (\n start: Date,\n end: Date,\n bBefore: number,\n bAfter: number,\n ): Promise<boolean> => {\n for (const rid of ids) {\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter: bAfter,\n bufferBefore: bBefore,\n debug: trace,\n endTime: end,\n getExternalBusy,\n guestCount: guestCount ?? 1,\n holdsSlug,\n payload,\n req,\n reservationSlug,\n resourceId: rid,\n resourceSlug,\n servicesSlug: serviceSlug,\n startTime: start,\n })\n if (!result.available) {\n return false\n }\n }\n return true\n }\n\n const availableSlots: Array<{ end: Date; start: Date }> = []\n\n // Full-day: offer each range as a single slot if all resources are free\n if (durationType === 'full-day') {\n let candidatesGenerated = 0\n for (const range of timeRanges) {\n candidatesGenerated++\n if (await allAvailable(range.start, range.end, 0, 0)) {\n availableSlots.push({ end: range.end, start: range.start })\n }\n }\n trace.dbg('result', {\n candidatesAvailable: availableSlots.length,\n candidatesGenerated,\n durationType: 'full-day',\n returnedCount: availableSlots.length,\n })\n return {\n ...(availableSlots.length === 0 ? { reason: 'all_slots_taken' as const } : {}),\n slots: availableSlots,\n }\n }\n\n const stepSize = Math.min(effectiveDuration, 15)\n let candidatesGenerated = 0\n\n for (const range of timeRanges) {\n let candidateStart = new Date(range.start)\n\n while (true) {\n const candidateEnd = addMinutes(candidateStart, effectiveDuration)\n if (candidateEnd > range.end) {\n break\n }\n\n candidatesGenerated++\n if (await allAvailable(candidateStart, candidateEnd, bufferBefore, bufferAfter)) {\n availableSlots.push({ end: candidateEnd, start: new Date(candidateStart) })\n }\n\n candidateStart = addMinutes(candidateStart, stepSize)\n }\n }\n\n trace.dbg('result', {\n candidatesAvailable: availableSlots.length,\n candidatesGenerated,\n returnedCount: availableSlots.length,\n })\n return {\n ...(availableSlots.length === 0\n ? {\n reason: (candidatesGenerated === 0\n ? 'window_too_short'\n : 'all_slots_taken') as EmptyReason,\n }\n : {}),\n slots: availableSlots,\n }\n}\n"],"names":["NOOP_RESERVE_DEBUG","resolveReservationItems","isExceptionDate","resolveScheduleForDate","addMinutes","computeBlockedWindow","doRangesOverlap","intersectIntervals","endOfDayInTimezone","COARSE_MARGIN_MS","computeEndTime","params","durationType","serviceDuration","startTime","end","timeZone","durationMinutes","Math","round","getTime","endTime","buildOverlapQuery","blockingStatuses","effectiveEnd","effectiveStart","excludeReservationId","resourceId","conditions","status","in","less_than","toISOString","greater_than","or","resource","equals","push","id","not_equals","and","buildCoarseOverlapQuery","candidateEnd","candidateStart","windowStart","Date","windowEnd","undefined","itemsToOccupancies","bufferFor","capacityMode","items","occupancies","item","String","after","before","service","blockedEnd","blockedStart","units","guestCount","reservationOccupancies","reservation","lenient","isBlockingStatus","statusMachine","includes","validateTransition","fromStatus","toStatus","allowed","transitions","reason","valid","checkAvailability","bufferAfter","bufferBefore","debug","excludeHoldToken","getExternalBusy","holdsSlug","payload","req","reservationSlug","resourceSlug","servicesSlug","siblingItems","trace","findByID","collection","depth","quantity","coarseWhere","docs","find","limit","where","dbg","blockingReservations","length","bufferCache","Map","serviceId","key","cached","get","result","joins","bufferTimeAfter","bufferTimeBefore","err","set","fetchedOccupancies","doc","siblingOccupancies","externalOccupancies","intervals","start","map","iv","filter","o","isNaN","holdOccupancies","holdWhere","expiresAt","token","holdDocs","hold","activeHolds","currentUnits","reduce","sum","occ","candidateUnits","available","occupancySources","external","fetched","sibling","currentCount","totalCapacity","getAvailableSlots","date","enforceActive","resourceIds","scheduleSlug","serviceSlug","tz","ids","child","slots","active","duration","scheduleBearingWindowLists","rid","resourceDoc","catch","scheduleWhere","schedules","activeFlags","s","rowsFound","matched","exs","exceptions","exception","e","scheduleId","matchedException","windows","schedule","windowCount","w","windowsEmpty","timeRanges","i","perResourceWindows","list","slotEndOffset","slotDuration","effectiveDuration","stepSize","min","allAvailable","bBefore","bAfter","availableSlots","candidatesGenerated","range","candidatesAvailable","returnedCount"],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { ResolvedReservationPluginConfig } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Why a hold was refused — a CLOSED set, deliberately.
|
|
5
|
+
*
|
|
6
|
+
* `/reserve/hold` is reachable without authentication, so the refusal reason is
|
|
7
|
+
* attacker-visible output. An earlier version echoed `error.message` from any
|
|
8
|
+
* failed write, which leaked internal detail (DB errors, `Forbidden`, genuine
|
|
9
|
+
* bugs) and reported all of it as `409 slot_taken` — a status that tells a
|
|
10
|
+
* well-behaved client the slot is gone and it should stop, for conditions that
|
|
11
|
+
* are nothing of the sort. Everything not enumerated here now propagates as a
|
|
12
|
+
* thrown error, so it surfaces as a 500 the operator can see rather than a
|
|
13
|
+
* plausible-looking 409 the caller cannot act on.
|
|
14
|
+
*/
|
|
15
|
+
export type HoldRefusalReason = 'resource_not_found' | 'service_inactive' | 'service_not_found' | 'slot_taken';
|
|
16
|
+
export type TakeHoldResult = {
|
|
17
|
+
hold: {
|
|
18
|
+
expiresAt: string;
|
|
19
|
+
id: number | string;
|
|
20
|
+
token: string;
|
|
21
|
+
};
|
|
22
|
+
ok: true;
|
|
23
|
+
} | {
|
|
24
|
+
ok: false;
|
|
25
|
+
reason: HoldRefusalReason;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Claim a slot for `config.slotHolds.ttlMinutes` while the caller completes an
|
|
29
|
+
* external step (typically payment).
|
|
30
|
+
*
|
|
31
|
+
* This function only assembles the row. The parts that make a hold trustworthy
|
|
32
|
+
* — claiming the resource lock and running the same `checkAvailability` a
|
|
33
|
+
* booking runs — live in the `validateHoldSlot` beforeChange hook, because they
|
|
34
|
+
* must share the transaction Payload opens around `create`. Doing them here
|
|
35
|
+
* instead granted 3 of 8 simultaneous holds for one slot.
|
|
36
|
+
*/
|
|
37
|
+
export declare function takeHold(params: {
|
|
38
|
+
config: ResolvedReservationPluginConfig;
|
|
39
|
+
endTime?: Date;
|
|
40
|
+
guestCount?: number;
|
|
41
|
+
req: PayloadRequest;
|
|
42
|
+
resourceId: number | string;
|
|
43
|
+
serviceId: number | string;
|
|
44
|
+
startTime: Date;
|
|
45
|
+
}): Promise<TakeHoldResult>;
|
|
46
|
+
/** Release a hold early. Idempotent: releasing an unknown token is not an error. */
|
|
47
|
+
export declare function releaseHold(params: {
|
|
48
|
+
config: ResolvedReservationPluginConfig;
|
|
49
|
+
req: PayloadRequest;
|
|
50
|
+
token: string;
|
|
51
|
+
}): Promise<{
|
|
52
|
+
released: number;
|
|
53
|
+
}>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { ValidationError } from 'payload';
|
|
2
|
+
import { isTransientWriteConflict } from '../utilities/retryOnWriteConflict.js';
|
|
3
|
+
import { computeEndTime } from './AvailabilityService.js';
|
|
4
|
+
/**
|
|
5
|
+
* Claim a slot for `config.slotHolds.ttlMinutes` while the caller completes an
|
|
6
|
+
* external step (typically payment).
|
|
7
|
+
*
|
|
8
|
+
* This function only assembles the row. The parts that make a hold trustworthy
|
|
9
|
+
* — claiming the resource lock and running the same `checkAvailability` a
|
|
10
|
+
* booking runs — live in the `validateHoldSlot` beforeChange hook, because they
|
|
11
|
+
* must share the transaction Payload opens around `create`. Doing them here
|
|
12
|
+
* instead granted 3 of 8 simultaneous holds for one slot.
|
|
13
|
+
*/ export async function takeHold(params) {
|
|
14
|
+
const { config, guestCount = 1, req, resourceId, serviceId, startTime } = params;
|
|
15
|
+
const { payload } = req;
|
|
16
|
+
// `disableErrors` is what makes the `!service` guard below reachable at all:
|
|
17
|
+
// without it `findByID` THROWS `NotFound`, and this call sits outside the
|
|
18
|
+
// try/catch, so a bad service id escaped as a raw 404 while a bad resource id
|
|
19
|
+
// came back as a 409 with an internal message. The trailing `.catch` covers
|
|
20
|
+
// the adapter's own cast error for a malformed id (the same treatment
|
|
21
|
+
// `/reserve/availability` and `/reserve/slots` already give it) — to a caller
|
|
22
|
+
// both mean exactly "no such service".
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
const service = await payload.findByID({
|
|
25
|
+
id: serviceId,
|
|
26
|
+
collection: config.slugs.services,
|
|
27
|
+
depth: 0,
|
|
28
|
+
disableErrors: true,
|
|
29
|
+
joins: false,
|
|
30
|
+
req
|
|
31
|
+
}).catch(()=>null);
|
|
32
|
+
if (!service) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
reason: 'service_not_found'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (config.enforceActive && service.active === false) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
reason: 'service_inactive'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Same treatment for the resource. `validateHoldSlot` writes the resource's
|
|
45
|
+
// booking lock through `payload.db.updateOne`, which for an unknown or
|
|
46
|
+
// malformed id throws an adapter-level error — previously echoed verbatim as
|
|
47
|
+
// a 409. Resolving it here turns it into a clean 404.
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
const resource = await payload.findByID({
|
|
50
|
+
id: resourceId,
|
|
51
|
+
collection: config.slugs.resources,
|
|
52
|
+
depth: 0,
|
|
53
|
+
disableErrors: true,
|
|
54
|
+
req
|
|
55
|
+
}).catch(()=>null);
|
|
56
|
+
if (!resource) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
reason: 'resource_not_found'
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const { endTime } = computeEndTime({
|
|
63
|
+
durationType: service.durationType ?? 'fixed',
|
|
64
|
+
endTime: params.endTime,
|
|
65
|
+
serviceDuration: service.duration ?? 0,
|
|
66
|
+
startTime,
|
|
67
|
+
timeZone: config.timezone
|
|
68
|
+
});
|
|
69
|
+
// Expired-row sweep. Purely hygienic — every read already filters on
|
|
70
|
+
// expiresAt — so it must never fail the hold.
|
|
71
|
+
try {
|
|
72
|
+
await payload.delete({
|
|
73
|
+
collection: config.slugs.holds,
|
|
74
|
+
req,
|
|
75
|
+
where: {
|
|
76
|
+
expiresAt: {
|
|
77
|
+
less_than: new Date().toISOString()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
} catch {
|
|
82
|
+
// Ignored by design: expiry is enforced at read time, not by this sweep.
|
|
83
|
+
//
|
|
84
|
+
// But swallowing the error is not enough to keep that promise. If this
|
|
85
|
+
// delete's `beginTransaction` REJECTED, `initTransaction` already stored the
|
|
86
|
+
// rejected promise on `req.transactionID` and `killTransaction`'s guard
|
|
87
|
+
// skips promises — so the `create` below would short-circuit inside
|
|
88
|
+
// `initTransaction` and rethrow THE SWEEP'S error, failing the hold on
|
|
89
|
+
// behalf of a step documented as never able to. `retryOnWriteConflict`'s
|
|
90
|
+
// between-attempts clearing cannot reach this: the poisoning happens
|
|
91
|
+
// mid-attempt. Clearing it here restores the invariant.
|
|
92
|
+
//
|
|
93
|
+
// This cannot drop a live transaction. A sweep that COMMITTED cleared the id
|
|
94
|
+
// itself; a sweep that failed any other way had `killTransaction` clear it
|
|
95
|
+
// (Payload does that unconditionally, even for an id it only joined). The
|
|
96
|
+
// poisoned promise is the one shape that can still be sitting here.
|
|
97
|
+
if (req.transactionID !== undefined) {
|
|
98
|
+
delete req.transactionID;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const token = crypto.randomUUID();
|
|
102
|
+
const expiresAt = new Date(Date.now() + config.slotHolds.ttlMinutes * 60_000).toISOString();
|
|
103
|
+
// The lock, the availability check and this insert must share one
|
|
104
|
+
// transaction for the lock to serialize anything — Payload opens that
|
|
105
|
+
// transaction around `create`, and validateHoldSlot runs inside it.
|
|
106
|
+
let created;
|
|
107
|
+
try {
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
+
created = await payload.create({
|
|
110
|
+
collection: config.slugs.holds,
|
|
111
|
+
data: {
|
|
112
|
+
customer: req.user?.id,
|
|
113
|
+
endTime: endTime.toISOString(),
|
|
114
|
+
expiresAt,
|
|
115
|
+
guestCount,
|
|
116
|
+
resource: resourceId,
|
|
117
|
+
service: serviceId,
|
|
118
|
+
startTime: startTime.toISOString(),
|
|
119
|
+
token
|
|
120
|
+
},
|
|
121
|
+
req
|
|
122
|
+
});
|
|
123
|
+
} catch (error) {
|
|
124
|
+
// A lost lock race must REJECT, not resolve: `retryOnWriteConflict` (which
|
|
125
|
+
// wraps every caller of this function) only retries a rejected promise, so
|
|
126
|
+
// returning here made the whole retry wrapper inert and collapsed a
|
|
127
|
+
// `quantity: 3` resource to one hold under a burst.
|
|
128
|
+
if (isTransientWriteConflict(error)) {
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
// validateHoldSlot signals genuine unavailability — the slot is booked,
|
|
132
|
+
// held, out of capacity, or the service went inactive — as a
|
|
133
|
+
// ValidationError. That is the only refusal this catch recognises.
|
|
134
|
+
if (error instanceof ValidationError) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
reason: 'slot_taken'
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// Anything else is a real failure, not a refusal. Propagate it rather than
|
|
141
|
+
// dressing it up as a 409 carrying its own message.
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
hold: {
|
|
146
|
+
id: created.id,
|
|
147
|
+
expiresAt,
|
|
148
|
+
token
|
|
149
|
+
},
|
|
150
|
+
ok: true
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** Release a hold early. Idempotent: releasing an unknown token is not an error. */ export async function releaseHold(params) {
|
|
154
|
+
const { config, req, token } = params;
|
|
155
|
+
const { docs } = await req.payload.delete({
|
|
156
|
+
collection: config.slugs.holds,
|
|
157
|
+
req,
|
|
158
|
+
where: {
|
|
159
|
+
token: {
|
|
160
|
+
equals: token
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
released: docs.length
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
//# sourceMappingURL=HoldService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/services/HoldService.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { isTransientWriteConflict } from '../utilities/retryOnWriteConflict.js'\nimport { computeEndTime } from './AvailabilityService.js'\n\n/**\n * Why a hold was refused — a CLOSED set, deliberately.\n *\n * `/reserve/hold` is reachable without authentication, so the refusal reason is\n * attacker-visible output. An earlier version echoed `error.message` from any\n * failed write, which leaked internal detail (DB errors, `Forbidden`, genuine\n * bugs) and reported all of it as `409 slot_taken` — a status that tells a\n * well-behaved client the slot is gone and it should stop, for conditions that\n * are nothing of the sort. Everything not enumerated here now propagates as a\n * thrown error, so it surfaces as a 500 the operator can see rather than a\n * plausible-looking 409 the caller cannot act on.\n */\nexport type HoldRefusalReason =\n | 'resource_not_found'\n | 'service_inactive'\n | 'service_not_found'\n | 'slot_taken'\n\nexport type TakeHoldResult =\n | { hold: { expiresAt: string; id: number | string; token: string }; ok: true }\n | { ok: false; reason: HoldRefusalReason }\n\n/**\n * Claim a slot for `config.slotHolds.ttlMinutes` while the caller completes an\n * external step (typically payment).\n *\n * This function only assembles the row. The parts that make a hold trustworthy\n * — claiming the resource lock and running the same `checkAvailability` a\n * booking runs — live in the `validateHoldSlot` beforeChange hook, because they\n * must share the transaction Payload opens around `create`. Doing them here\n * instead granted 3 of 8 simultaneous holds for one slot.\n */\nexport async function takeHold(params: {\n config: ResolvedReservationPluginConfig\n endTime?: Date\n guestCount?: number\n req: PayloadRequest\n resourceId: number | string\n serviceId: number | string\n startTime: Date\n}): Promise<TakeHoldResult> {\n const { config, guestCount = 1, req, resourceId, serviceId, startTime } = params\n const { payload } = req\n\n // `disableErrors` is what makes the `!service` guard below reachable at all:\n // without it `findByID` THROWS `NotFound`, and this call sits outside the\n // try/catch, so a bad service id escaped as a raw 404 while a bad resource id\n // came back as a 409 with an internal message. The trailing `.catch` covers\n // the adapter's own cast error for a malformed id (the same treatment\n // `/reserve/availability` and `/reserve/slots` already give it) — to a caller\n // both mean exactly \"no such service\".\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: config.slugs.services,\n depth: 0,\n disableErrors: true,\n joins: false,\n req,\n }).catch(() => null)\n\n if (!service) {\n return { ok: false, reason: 'service_not_found' }\n }\n\n if (config.enforceActive && service.active === false) {\n return { ok: false, reason: 'service_inactive' }\n }\n\n // Same treatment for the resource. `validateHoldSlot` writes the resource's\n // booking lock through `payload.db.updateOne`, which for an unknown or\n // malformed id throws an adapter-level error — previously echoed verbatim as\n // a 409. Resolving it here turns it into a clean 404.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: config.slugs.resources,\n depth: 0,\n disableErrors: true,\n req,\n }).catch(() => null)\n\n if (!resource) {\n return { ok: false, reason: 'resource_not_found' }\n }\n\n const { endTime } = computeEndTime({\n durationType: (service.durationType as 'fixed') ?? 'fixed',\n endTime: params.endTime,\n serviceDuration: (service.duration as number) ?? 0,\n startTime,\n timeZone: config.timezone,\n })\n\n // Expired-row sweep. Purely hygienic — every read already filters on\n // expiresAt — so it must never fail the hold.\n try {\n await payload.delete({\n collection: config.slugs.holds,\n req,\n where: { expiresAt: { less_than: new Date().toISOString() } },\n })\n } catch {\n // Ignored by design: expiry is enforced at read time, not by this sweep.\n //\n // But swallowing the error is not enough to keep that promise. If this\n // delete's `beginTransaction` REJECTED, `initTransaction` already stored the\n // rejected promise on `req.transactionID` and `killTransaction`'s guard\n // skips promises — so the `create` below would short-circuit inside\n // `initTransaction` and rethrow THE SWEEP'S error, failing the hold on\n // behalf of a step documented as never able to. `retryOnWriteConflict`'s\n // between-attempts clearing cannot reach this: the poisoning happens\n // mid-attempt. Clearing it here restores the invariant.\n //\n // This cannot drop a live transaction. A sweep that COMMITTED cleared the id\n // itself; a sweep that failed any other way had `killTransaction` clear it\n // (Payload does that unconditionally, even for an id it only joined). The\n // poisoned promise is the one shape that can still be sitting here.\n if (req.transactionID !== undefined) {\n delete req.transactionID\n }\n }\n\n const token = crypto.randomUUID()\n const expiresAt = new Date(Date.now() + config.slotHolds.ttlMinutes * 60_000).toISOString()\n\n // The lock, the availability check and this insert must share one\n // transaction for the lock to serialize anything — Payload opens that\n // transaction around `create`, and validateHoldSlot runs inside it.\n let created: { id: number | string }\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n created = await (payload.create as any)({\n collection: config.slugs.holds,\n data: {\n customer: req.user?.id,\n endTime: endTime.toISOString(),\n expiresAt,\n guestCount,\n resource: resourceId,\n service: serviceId,\n startTime: startTime.toISOString(),\n token,\n },\n req,\n })\n } catch (error) {\n // A lost lock race must REJECT, not resolve: `retryOnWriteConflict` (which\n // wraps every caller of this function) only retries a rejected promise, so\n // returning here made the whole retry wrapper inert and collapsed a\n // `quantity: 3` resource to one hold under a burst.\n if (isTransientWriteConflict(error)) {\n throw error\n }\n // validateHoldSlot signals genuine unavailability — the slot is booked,\n // held, out of capacity, or the service went inactive — as a\n // ValidationError. That is the only refusal this catch recognises.\n if (error instanceof ValidationError) {\n return { ok: false, reason: 'slot_taken' }\n }\n // Anything else is a real failure, not a refusal. Propagate it rather than\n // dressing it up as a 409 carrying its own message.\n throw error\n }\n\n return { hold: { id: created.id, expiresAt, token }, ok: true }\n}\n\n/** Release a hold early. Idempotent: releasing an unknown token is not an error. */\nexport async function releaseHold(params: {\n config: ResolvedReservationPluginConfig\n req: PayloadRequest\n token: string\n}): Promise<{ released: number }> {\n const { config, req, token } = params\n\n const { docs } = await req.payload.delete({\n collection: config.slugs.holds,\n req,\n where: { token: { equals: token } },\n })\n\n return { released: docs.length }\n}\n"],"names":["ValidationError","isTransientWriteConflict","computeEndTime","takeHold","params","config","guestCount","req","resourceId","serviceId","startTime","payload","service","findByID","id","collection","slugs","services","depth","disableErrors","joins","catch","ok","reason","enforceActive","active","resource","resources","endTime","durationType","serviceDuration","duration","timeZone","timezone","delete","holds","where","expiresAt","less_than","Date","toISOString","transactionID","undefined","token","crypto","randomUUID","now","slotHolds","ttlMinutes","created","create","data","customer","user","error","hold","releaseHold","docs","equals","released","length"],"mappings":"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"}
|
package/dist/services/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/services/index.ts"],"sourcesContent":["export {\n buildOverlapQuery,\n checkAvailability,\n computeEndTime,\n getAvailableSlots,\n isBlockingStatus,\n validateTransition,\n} from './AvailabilityService.js'\n"],"names":["buildOverlapQuery","checkAvailability","computeEndTime","getAvailableSlots","isBlockingStatus","validateTransition"],"mappings":"AAAA,SACEA,iBAAiB,EACjBC,iBAAiB,EACjBC,cAAc,EACdC,iBAAiB,EACjBC,gBAAgB,EAChBC,kBAAkB,QACb,2BAA0B"}
|
|
1
|
+
{"version":3,"sources":["../../src/services/index.ts"],"sourcesContent":["export {\n buildOverlapQuery,\n checkAvailability,\n computeEndTime,\n getAvailableSlots,\n isBlockingStatus,\n validateTransition,\n} from './AvailabilityService.js'\nexport type { EmptyReason } from './AvailabilityService.js'\n"],"names":["buildOverlapQuery","checkAvailability","computeEndTime","getAvailableSlots","isBlockingStatus","validateTransition"],"mappings":"AAAA,SACEA,iBAAiB,EACjBC,iBAAiB,EACjBC,cAAc,EACdC,iBAAiB,EACjBC,gBAAgB,EAChBC,kBAAkB,QACb,2BAA0B"}
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "لا يمكن الانتقال من \"{{from}}\" إلى \"{{to}}\".",
|
|
62
62
|
"errorCancellationNotice": "يتطلب الإلغاء إشعاراً مسبقاً لا يقل عن {{period}} ساعة. لم يتبقَّ سوى {{hours}} ساعة على الموعد.",
|
|
63
63
|
"errorConflict": "يتعارض هذا الموعد الزمني مع حجز قائم لهذا المورد.",
|
|
64
|
+
"errorServiceInactive": "الخدمة \"{{name}}\" غير نشطة ولا يمكن حجزها.",
|
|
65
|
+
"errorResourceInactive": "المورد \"{{name}}\" غير نشط ولا يمكن حجزه.",
|
|
64
66
|
"calendarLoading": "جارٍ تحميل الحجوزات...",
|
|
65
67
|
"calendarToday": "اليوم",
|
|
66
68
|
"calendarCreateNew": "إنشاء جديد",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "التصفية حسب المورد",
|
|
129
131
|
"fieldOwner": "المالك",
|
|
130
132
|
"fieldResourceType": "نوع المورد",
|
|
133
|
+
"fieldResources": "الموارد",
|
|
134
|
+
"fieldResourcesDesc": "الموارد التي يمكنها تقديم هذه الخدمة. لتغيير ذلك، افتح المورد وعدّل حقل الخدمات الخاص به.",
|
|
131
135
|
"fieldRequiredResources": "الموارد المطلوبة",
|
|
132
|
-
"fieldRequiredResourcesDesc": "مجموعات الموارد الإضافية التي يشغلها كل حجز لهذه الخدمة (مثل الكرسي). يتم توسيع الحجوزات تلقائياً لتشمل هذه الموارد، وتُحظر إذا كانت أي مجموعة ممتلئة.",
|
|
136
|
+
"fieldRequiredResourcesDesc": "مجموعات الموارد الإضافية التي يشغلها كل حجز لهذه الخدمة (مثل الكرسي). يتم توسيع الحجوزات تلقائياً لتشمل هذه الموارد، وتُحظر إذا كانت أي مجموعة ممتلئة. وهذا يختلف عن الموارد التي يمكنها تقديم هذه الخدمة — راجع الموارد أعلاه.",
|
|
133
137
|
"fieldAllowGuestBooking": "حجز الضيف",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "السماح بالحجوزات دون حساب عميل. \"وراثة\" تستخدم الإعداد الافتراضي على مستوى الإضافة.",
|
|
135
139
|
"guestBookingInherit": "وراثة الإعداد الافتراضي للإضافة",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "Wechsel von \"{{from}}\" zu \"{{to}}\" nicht möglich.",
|
|
62
62
|
"errorCancellationNotice": "Stornierungen erfordern eine Vorlaufzeit von mindestens {{period}} Stunden. Nur noch {{hours}} Stunden bis zum Termin.",
|
|
63
63
|
"errorConflict": "Dieses Zeitfenster überschneidet sich mit einer bestehenden Reservierung für diese Ressource.",
|
|
64
|
+
"errorServiceInactive": "Die Leistung „{{name}}\" ist nicht aktiv und kann nicht gebucht werden.",
|
|
65
|
+
"errorResourceInactive": "Die Ressource „{{name}}\" ist nicht aktiv und kann nicht gebucht werden.",
|
|
64
66
|
"calendarLoading": "Reservierungen werden geladen...",
|
|
65
67
|
"calendarToday": "Heute",
|
|
66
68
|
"calendarCreateNew": "Neu erstellen",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "Nach Ressource filtern",
|
|
129
131
|
"fieldOwner": "Inhaber",
|
|
130
132
|
"fieldResourceType": "Ressourcentyp",
|
|
133
|
+
"fieldResources": "Ressourcen",
|
|
134
|
+
"fieldResourcesDesc": "Ressourcen, die diese Leistung erbringen können. Zum Ändern die Ressource öffnen und dort das Feld „Leistungen\" bearbeiten.",
|
|
131
135
|
"fieldRequiredResources": "Erforderliche Ressourcen",
|
|
132
|
-
"fieldRequiredResourcesDesc": "Zusätzliche Ressourcenpools, die jede Buchung dieser Leistung belegt (z. B. ein Stuhl). Buchungen werden automatisch um diese erweitert und blockiert, wenn ein Pool ausgelastet ist.",
|
|
136
|
+
"fieldRequiredResourcesDesc": "Zusätzliche Ressourcenpools, die jede Buchung dieser Leistung belegt (z. B. ein Stuhl). Buchungen werden automatisch um diese erweitert und blockiert, wenn ein Pool ausgelastet ist. Das ist nicht dasselbe wie die Ressourcen, die diese Leistung erbringen können – siehe „Ressourcen\" oben.",
|
|
133
137
|
"fieldAllowGuestBooking": "Gastbuchung",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "Buchungen ohne Kundenkonto zulassen. \"Übernehmen\" verwendet die Plugin-Standardeinstellung.",
|
|
135
139
|
"guestBookingInherit": "Plugin-Standard übernehmen",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "Cannot transition from \"{{from}}\" to \"{{to}}\".",
|
|
62
62
|
"errorCancellationNotice": "Cancellations require at least {{period}} hours notice. Only {{hours}} hours until the appointment.",
|
|
63
63
|
"errorConflict": "This time slot conflicts with an existing reservation for this resource.",
|
|
64
|
+
"errorServiceInactive": "Service \"{{name}}\" is not active and cannot be booked.",
|
|
65
|
+
"errorResourceInactive": "Resource \"{{name}}\" is not active and cannot be booked.",
|
|
64
66
|
"calendarLoading": "Loading reservations...",
|
|
65
67
|
"calendarToday": "Today",
|
|
66
68
|
"calendarCreateNew": "Create New",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "Filter by resource",
|
|
129
131
|
"fieldOwner": "Owner",
|
|
130
132
|
"fieldResourceType": "Resource type",
|
|
133
|
+
"fieldResources": "Resources",
|
|
134
|
+
"fieldResourcesDesc": "Resources that can perform this service. To change this, open the resource and edit its Services field.",
|
|
131
135
|
"fieldRequiredResources": "Required resources",
|
|
132
|
-
"fieldRequiredResourcesDesc": "
|
|
136
|
+
"fieldRequiredResourcesDesc": "Extra resource pools every booking of this service also occupies (e.g. a chair). Bookings are auto-expanded to include these and are blocked if any pool is full. This is not the same as which resources perform the service — see Resources above.",
|
|
133
137
|
"fieldAllowGuestBooking": "Guest booking",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "Allow bookings without a customer account. \"Inherit\" uses the plugin-level default.",
|
|
135
139
|
"guestBookingInherit": "Inherit plugin default",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "No se puede cambiar de \"{{from}}\" a \"{{to}}\".",
|
|
62
62
|
"errorCancellationNotice": "Las cancelaciones requieren al menos {{period}} horas de antelación. Solo quedan {{hours}} horas hasta la cita.",
|
|
63
63
|
"errorConflict": "Esta franja horaria entra en conflicto con una reserva existente para este recurso.",
|
|
64
|
+
"errorServiceInactive": "El servicio \"{{name}}\" no está activo y no se puede reservar.",
|
|
65
|
+
"errorResourceInactive": "El recurso \"{{name}}\" no está activo y no se puede reservar.",
|
|
64
66
|
"calendarLoading": "Cargando reservas...",
|
|
65
67
|
"calendarToday": "Hoy",
|
|
66
68
|
"calendarCreateNew": "Crear nueva",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "Filtrar por recurso",
|
|
129
131
|
"fieldOwner": "Propietario",
|
|
130
132
|
"fieldResourceType": "Tipo de recurso",
|
|
133
|
+
"fieldResources": "Recursos",
|
|
134
|
+
"fieldResourcesDesc": "Recursos que pueden realizar este servicio. Para cambiarlo, abre el recurso y edita su campo Servicios.",
|
|
131
135
|
"fieldRequiredResources": "Recursos necesarios",
|
|
132
|
-
"fieldRequiredResourcesDesc": "Grupos de recursos adicionales que ocupa cada reserva de este servicio (p. ej. una silla). Las reservas se amplían automáticamente para incluirlos y se bloquean si algún grupo está completo.",
|
|
136
|
+
"fieldRequiredResourcesDesc": "Grupos de recursos adicionales que ocupa cada reserva de este servicio (p. ej. una silla). Las reservas se amplían automáticamente para incluirlos y se bloquean si algún grupo está completo. Esto no es lo mismo que los recursos que pueden realizar este servicio. Consulta Recursos arriba.",
|
|
133
137
|
"fieldAllowGuestBooking": "Reserva de invitado",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "Permitir reservas sin una cuenta de cliente. \"Heredar\" usa el valor predeterminado del plugin.",
|
|
135
139
|
"guestBookingInherit": "Heredar valor predeterminado del plugin",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "امکان تغییر از «{{from}}» به «{{to}}» وجود ندارد.",
|
|
62
62
|
"errorCancellationNotice": "لغو رزرو به حداقل {{period}} ساعت اطلاعرسانی پیش نیاز دارد. تنها {{hours}} ساعت تا زمان قرار باقی مانده است.",
|
|
63
63
|
"errorConflict": "این بازه زمانی با یک رزرو موجود برای این منبع تداخل دارد.",
|
|
64
|
+
"errorServiceInactive": "سرویس «{{name}}» فعال نیست و قابل رزرو نیست.",
|
|
65
|
+
"errorResourceInactive": "منبع «{{name}}» فعال نیست و قابل رزرو نیست.",
|
|
64
66
|
"calendarLoading": "در حال بارگذاری رزروها...",
|
|
65
67
|
"calendarToday": "امروز",
|
|
66
68
|
"calendarCreateNew": "ایجاد جدید",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "فیلتر بر اساس منبع",
|
|
129
131
|
"fieldOwner": "مالک",
|
|
130
132
|
"fieldResourceType": "نوع منبع",
|
|
133
|
+
"fieldResources": "منابع",
|
|
134
|
+
"fieldResourcesDesc": "منابعی که میتوانند این سرویس را ارائه دهند. برای تغییر، منبع را باز کنید و فیلد سرویسهای آن را ویرایش کنید.",
|
|
131
135
|
"fieldRequiredResources": "منابع موردنیاز",
|
|
132
|
-
"fieldRequiredResourcesDesc": "مخازن منابع اضافی که هر رزرو این خدمت آنها را اشغال میکند (مثلاً یک صندلی). رزروها بهطور خودکار برای دربرگرفتن این موارد گسترش مییابند و در صورت پر بودن هر مخزن مسدود میشوند.",
|
|
136
|
+
"fieldRequiredResourcesDesc": "مخازن منابع اضافی که هر رزرو این خدمت آنها را اشغال میکند (مثلاً یک صندلی). رزروها بهطور خودکار برای دربرگرفتن این موارد گسترش مییابند و در صورت پر بودن هر مخزن مسدود میشوند. این با منابعی که میتوانند این خدمت را ارائه دهند تفاوت دارد — منابع را در بالا ببینید.",
|
|
133
137
|
"fieldAllowGuestBooking": "رزرو مهمان",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "اجازه رزرو بدون حساب مشتری. «ارثبری» از پیشفرض سطح افزونه استفاده میکند.",
|
|
135
139
|
"guestBookingInherit": "ارثبری از پیشفرض افزونه",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "Impossible de passer de « {{from}} » à « {{to}} ».",
|
|
62
62
|
"errorCancellationNotice": "Les annulations nécessitent un préavis d'au moins {{period}} heures. Il ne reste que {{hours}} heures avant le rendez-vous.",
|
|
63
63
|
"errorConflict": "Ce créneau horaire est en conflit avec une réservation existante pour cette ressource.",
|
|
64
|
+
"errorServiceInactive": "Le service « {{name}} » n'est pas actif et ne peut pas être réservé.",
|
|
65
|
+
"errorResourceInactive": "La ressource « {{name}} » n'est pas active et ne peut pas être réservée.",
|
|
64
66
|
"calendarLoading": "Chargement des réservations...",
|
|
65
67
|
"calendarToday": "Aujourd'hui",
|
|
66
68
|
"calendarCreateNew": "Créer",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "Filtrer par ressource",
|
|
129
131
|
"fieldOwner": "Propriétaire",
|
|
130
132
|
"fieldResourceType": "Type de ressource",
|
|
133
|
+
"fieldResources": "Ressources",
|
|
134
|
+
"fieldResourcesDesc": "Ressources pouvant réaliser ce service. Pour le modifier, ouvrez la ressource et modifiez son champ Services.",
|
|
131
135
|
"fieldRequiredResources": "Ressources requises",
|
|
132
|
-
"fieldRequiredResourcesDesc": "Pools de ressources supplémentaires occupés par chaque réservation de cette prestation (par ex. un fauteuil). Les réservations sont automatiquement étendues pour les inclure et sont bloquées si l'un des pools est complet.",
|
|
136
|
+
"fieldRequiredResourcesDesc": "Pools de ressources supplémentaires occupés par chaque réservation de cette prestation (par ex. un fauteuil). Les réservations sont automatiquement étendues pour les inclure et sont bloquées si l'un des pools est complet. Ce n'est pas la même chose que les ressources pouvant réaliser cette prestation — voir Ressources ci-dessus.",
|
|
133
137
|
"fieldAllowGuestBooking": "Réservation invité",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "Autoriser les réservations sans compte client. « Hériter » utilise la valeur par défaut définie au niveau du plugin.",
|
|
135
139
|
"guestBookingInherit": "Hériter de la valeur par défaut du plugin",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "\"{{from}}\" से \"{{to}}\" में नहीं बदला जा सकता।",
|
|
62
62
|
"errorCancellationNotice": "रद्दीकरण के लिए कम से कम {{period}} घंटे की सूचना आवश्यक है। अपॉइंटमेंट में केवल {{hours}} घंटे शेष हैं।",
|
|
63
63
|
"errorConflict": "यह समय स्लॉट इस संसाधन के किसी मौजूदा आरक्षण से टकराता है।",
|
|
64
|
+
"errorServiceInactive": "सेवा \"{{name}}\" सक्रिय नहीं है और बुक नहीं की जा सकती।",
|
|
65
|
+
"errorResourceInactive": "संसाधन \"{{name}}\" सक्रिय नहीं है और बुक नहीं किया जा सकता।",
|
|
64
66
|
"calendarLoading": "आरक्षण लोड हो रहे हैं...",
|
|
65
67
|
"calendarToday": "आज",
|
|
66
68
|
"calendarCreateNew": "नया बनाएँ",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "संसाधन के अनुसार फ़िल्टर करें",
|
|
129
131
|
"fieldOwner": "स्वामी",
|
|
130
132
|
"fieldResourceType": "संसाधन प्रकार",
|
|
133
|
+
"fieldResources": "संसाधन",
|
|
134
|
+
"fieldResourcesDesc": "वे संसाधन जो यह सेवा कर सकते हैं। बदलने के लिए संसाधन खोलें और उसका सेवाएँ फ़ील्ड संपादित करें।",
|
|
131
135
|
"fieldRequiredResources": "आवश्यक संसाधन",
|
|
132
|
-
"fieldRequiredResourcesDesc": "अतिरिक्त संसाधन पूल जो इस सेवा की हर बुकिंग में उपयोग होते हैं (उदाहरण के लिए एक कुर्सी)। बुकिंग को इन्हें शामिल करने के लिए स्वतः विस्तारित किया जाता है और यदि कोई पूल भरा हो तो बुकिंग अवरुद्ध हो जाती है।",
|
|
136
|
+
"fieldRequiredResourcesDesc": "अतिरिक्त संसाधन पूल जो इस सेवा की हर बुकिंग में उपयोग होते हैं (उदाहरण के लिए एक कुर्सी)। बुकिंग को इन्हें शामिल करने के लिए स्वतः विस्तारित किया जाता है और यदि कोई पूल भरा हो तो बुकिंग अवरुद्ध हो जाती है। यह इससे अलग है कि कौन-से संसाधन यह सेवा कर सकते हैं — ऊपर संसाधन देखें।",
|
|
133
137
|
"fieldAllowGuestBooking": "अतिथि बुकिंग",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "ग्राहक खाते के बिना बुकिंग की अनुमति दें। \"इनहेरिट\" प्लगइन-स्तरीय डिफ़ॉल्ट का उपयोग करता है।",
|
|
135
139
|
"guestBookingInherit": "प्लगइन डिफ़ॉल्ट इनहेरिट करें",
|