payload-reserve 2.2.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +1 -0
  2. package/dist/components/CalendarView/CalendarView.module.css +16 -0
  3. package/dist/components/CalendarView/index.js +25 -21
  4. package/dist/components/CalendarView/index.js.map +1 -1
  5. package/dist/defaults.js +1 -0
  6. package/dist/defaults.js.map +1 -1
  7. package/dist/endpoints/checkAvailability.js +15 -0
  8. package/dist/endpoints/checkAvailability.js.map +1 -1
  9. package/dist/endpoints/getSlots.js +15 -0
  10. package/dist/endpoints/getSlots.js.map +1 -1
  11. package/dist/endpoints/resourceAvailability.d.ts +2 -0
  12. package/dist/endpoints/resourceAvailability.js +27 -2
  13. package/dist/endpoints/resourceAvailability.js.map +1 -1
  14. package/dist/hooks/reservations/validateConflicts.js +15 -0
  15. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  16. package/dist/services/AvailabilityService.d.ts +5 -0
  17. package/dist/services/AvailabilityService.js +149 -28
  18. package/dist/services/AvailabilityService.js.map +1 -1
  19. package/dist/types.d.ts +3 -0
  20. package/dist/types.js.map +1 -1
  21. package/dist/utilities/externalPillLabel.d.ts +10 -0
  22. package/dist/utilities/externalPillLabel.js +23 -0
  23. package/dist/utilities/externalPillLabel.js.map +1 -0
  24. package/dist/utilities/reservationResourceFilter.d.ts +19 -0
  25. package/dist/utilities/reservationResourceFilter.js +18 -0
  26. package/dist/utilities/reservationResourceFilter.js.map +1 -0
  27. package/dist/utilities/reserveDebug.d.ts +26 -0
  28. package/dist/utilities/reserveDebug.js +35 -0
  29. package/dist/utilities/reserveDebug.js.map +1 -0
  30. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type {\n ReservationPluginConfig,\n ResolvedReservationPluginConfig,\n ResolvedStaffProvisioningConfig,\n StatusMachineConfig,\n} from './types.js'\n\nimport { DEFAULT_STATUS_MACHINE } from './types.js'\nimport { validateTimezone } from './utilities/timezoneUtils.js'\n\nfunction validateStatusMachine(sm: StatusMachineConfig): void {\n if (!sm.statuses.includes(sm.defaultStatus)) {\n throw new Error(`statusMachine.defaultStatus \"${sm.defaultStatus}\" is not in statuses array`)\n }\n for (const s of sm.blockingStatuses) {\n if (!sm.statuses.includes(s)) {\n throw new Error(`statusMachine.blockingStatuses contains \"${s}\" which is not in statuses array`)\n }\n }\n for (const s of sm.terminalStatuses) {\n if (!sm.statuses.includes(s)) {\n throw new Error(`statusMachine.terminalStatuses contains \"${s}\" which is not in statuses array`)\n }\n }\n for (const [from, targets] of Object.entries(sm.transitions)) {\n if (!sm.statuses.includes(from)) {\n throw new Error(`statusMachine.transitions has key \"${from}\" which is not in statuses array`)\n }\n for (const to of targets) {\n if (!sm.statuses.includes(to)) {\n throw new Error(`statusMachine.transitions[\"${from}\"] targets \"${to}\" which is not in statuses array`)\n }\n }\n }\n // A terminal status must have no outgoing transitions — otherwise the config\n // contradicts itself (the status is declared terminal but can be left).\n for (const s of sm.terminalStatuses) {\n if ((sm.transitions[s]?.length ?? 0) > 0) {\n throw new Error(\n `statusMachine.terminalStatuses contains \"${s}\" but transitions[\"${s}\"] is non-empty — a terminal status cannot have outgoing transitions`,\n )\n }\n }\n // A reservation is born in defaultStatus, so it must not be terminal.\n if (sm.terminalStatuses.includes(sm.defaultStatus)) {\n throw new Error(`statusMachine.defaultStatus \"${sm.defaultStatus}\" cannot be a terminal status`)\n }\n if (!sm.statuses.includes(sm.confirmStatus)) {\n throw new Error(`statusMachine.confirmStatus \"${sm.confirmStatus}\" is not in statuses array`)\n }\n if (!sm.statuses.includes(sm.cancelStatus)) {\n throw new Error(`statusMachine.cancelStatus \"${sm.cancelStatus}\" is not in statuses array`)\n }\n}\n\nexport const DEFAULT_RESOURCE_TYPES = ['staff', 'equipment', 'room']\nexport const DEFAULT_LEAVE_TYPES = ['vacation', 'sick', 'personal', 'closure', 'other']\n\nfunction resolveStaffProvisioning(\n pluginOptions: ReservationPluginConfig,\n resourceTypes: string[],\n): ResolvedStaffProvisioningConfig | undefined {\n const sp = pluginOptions.staffProvisioning\n if (!sp) {\n return undefined\n }\n\n if (!pluginOptions.resourceOwnerMode) {\n throw new Error('staffProvisioning requires resourceOwnerMode to be enabled')\n }\n if (sp.staffRoles.length === 0) {\n throw new Error('staffProvisioning.staffRoles must be a non-empty array')\n }\n const resourceType = sp.resourceType ?? 'staff'\n if (!resourceTypes.includes(resourceType)) {\n throw new Error(\n `staffProvisioning.resourceType \"${resourceType}\" is not in resourceTypes [${resourceTypes.join(', ')}]`,\n )\n }\n const userCollection = sp.userCollection ?? pluginOptions.userCollection\n if (!userCollection) {\n throw new Error(\n 'staffProvisioning.userCollection is required when top-level userCollection is unset',\n )\n }\n\n return {\n beforeCreate: sp.beforeCreate,\n nameFrom: sp.nameFrom ?? 'name',\n resourceType,\n roleField: sp.roleField ?? 'role',\n staffRoles: sp.staffRoles,\n userCollection,\n }\n}\n\nexport const DEFAULT_SLUGS = {\n customers: 'customers',\n media: 'media',\n reservations: 'reservations',\n resources: 'resources',\n schedules: 'schedules',\n services: 'services',\n} as const\n\nexport const DEFAULT_ADMIN_GROUP = 'Reservations'\nexport const DEFAULT_ALLOW_GUEST_BOOKING = false\nexport const DEFAULT_BUFFER_TIME = 0\nexport const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24\n\nexport function resolveConfig(\n pluginOptions: ReservationPluginConfig,\n): ResolvedReservationPluginConfig {\n // A disabled plugin still resolves (so collections register with the right\n // slugs for schema stability) but skips all config validation — temporarily\n // disabling a misconfigured plugin should not throw at boot (review C3).\n const disabled = pluginOptions.disabled ?? false\n\n if (!disabled) {\n if (pluginOptions.resourceTypes !== undefined && pluginOptions.resourceTypes.length === 0) {\n throw new Error('resourceTypes must be a non-empty array')\n }\n if (pluginOptions.leaveTypes !== undefined && pluginOptions.leaveTypes.length === 0) {\n throw new Error('leaveTypes must be a non-empty array')\n }\n }\n\n const resourceTypes = pluginOptions.resourceTypes ?? DEFAULT_RESOURCE_TYPES\n const userStatusMachine = pluginOptions.statusMachine\n const rom = pluginOptions.resourceOwnerMode\n const resolved: ResolvedReservationPluginConfig = {\n access: pluginOptions.access ?? {},\n adminGroup: pluginOptions.adminGroup ?? DEFAULT_ADMIN_GROUP,\n allowGuestBooking: pluginOptions.allowGuestBooking ?? DEFAULT_ALLOW_GUEST_BOOKING,\n cancellationNoticePeriod:\n pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,\n collectionOverrides: pluginOptions.collectionOverrides ?? {},\n defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,\n disabled: pluginOptions.disabled ?? false,\n extraReservationFields: pluginOptions.extraReservationFields ?? [],\n getExternalBusy: pluginOptions.getExternalBusy,\n // Real value is set by the plugin once config.collections is known (C8)\n hasMediaCollection: false,\n hooks: pluginOptions.hooks ?? {},\n leaveTypes: pluginOptions.leaveTypes ?? DEFAULT_LEAVE_TYPES,\n localized: false,\n multiTenant: {\n cookieName: pluginOptions.multiTenant?.cookieName ?? 'payload-tenant',\n tenantField: pluginOptions.multiTenant?.tenantField ?? 'tenant',\n timezoneField: pluginOptions.multiTenant?.timezoneField ?? 'timezone',\n },\n resourceOwnerMode: rom\n ? {\n adminRoles: rom.adminRoles ?? [],\n ownedServices: rom.ownedServices ?? false,\n ownerCollection: rom.ownerCollection,\n ownerField: rom.ownerField ?? 'owner',\n roleField: rom.roleField ?? pluginOptions.staffProvisioning?.roleField ?? 'role',\n }\n : undefined,\n resourceTypes,\n slugs: {\n customers: pluginOptions.slugs?.customers ?? DEFAULT_SLUGS.customers,\n media: pluginOptions.slugs?.media ?? DEFAULT_SLUGS.media,\n reservations: pluginOptions.slugs?.reservations ?? DEFAULT_SLUGS.reservations,\n resources: pluginOptions.slugs?.resources ?? DEFAULT_SLUGS.resources,\n schedules: pluginOptions.slugs?.schedules ?? DEFAULT_SLUGS.schedules,\n services: pluginOptions.slugs?.services ?? DEFAULT_SLUGS.services,\n },\n staffProvisioning: disabled\n ? undefined\n : resolveStaffProvisioning(pluginOptions, resourceTypes),\n statusMachine: userStatusMachine\n ? {\n blockingStatuses:\n userStatusMachine.blockingStatuses ?? DEFAULT_STATUS_MACHINE.blockingStatuses,\n cancelStatus: userStatusMachine.cancelStatus ?? DEFAULT_STATUS_MACHINE.cancelStatus,\n confirmStatus: userStatusMachine.confirmStatus ?? DEFAULT_STATUS_MACHINE.confirmStatus,\n defaultStatus: userStatusMachine.defaultStatus ?? DEFAULT_STATUS_MACHINE.defaultStatus,\n statuses: userStatusMachine.statuses ?? DEFAULT_STATUS_MACHINE.statuses,\n terminalStatuses:\n userStatusMachine.terminalStatuses ?? DEFAULT_STATUS_MACHINE.terminalStatuses,\n transitions: userStatusMachine.transitions ?? DEFAULT_STATUS_MACHINE.transitions,\n }\n : { ...DEFAULT_STATUS_MACHINE },\n timezone: pluginOptions.timezone ?? 'UTC',\n userCollection: pluginOptions.userCollection ?? undefined,\n }\n\n if (!disabled) {\n validateStatusMachine(resolved.statusMachine)\n validateTimezone(resolved.timezone)\n }\n\n return resolved\n}\n"],"names":["DEFAULT_STATUS_MACHINE","validateTimezone","validateStatusMachine","sm","statuses","includes","defaultStatus","Error","s","blockingStatuses","terminalStatuses","from","targets","Object","entries","transitions","to","length","confirmStatus","cancelStatus","DEFAULT_RESOURCE_TYPES","DEFAULT_LEAVE_TYPES","resolveStaffProvisioning","pluginOptions","resourceTypes","sp","staffProvisioning","undefined","resourceOwnerMode","staffRoles","resourceType","join","userCollection","beforeCreate","nameFrom","roleField","DEFAULT_SLUGS","customers","media","reservations","resources","schedules","services","DEFAULT_ADMIN_GROUP","DEFAULT_ALLOW_GUEST_BOOKING","DEFAULT_BUFFER_TIME","DEFAULT_CANCELLATION_NOTICE_PERIOD","resolveConfig","disabled","leaveTypes","userStatusMachine","statusMachine","rom","resolved","access","adminGroup","allowGuestBooking","cancellationNoticePeriod","collectionOverrides","defaultBufferTime","extraReservationFields","getExternalBusy","hasMediaCollection","hooks","localized","multiTenant","cookieName","tenantField","timezoneField","adminRoles","ownedServices","ownerCollection","ownerField","slugs","timezone"],"mappings":"AAOA,SAASA,sBAAsB,QAAQ,aAAY;AACnD,SAASC,gBAAgB,QAAQ,+BAA8B;AAE/D,SAASC,sBAAsBC,EAAuB;IACpD,IAAI,CAACA,GAAGC,QAAQ,CAACC,QAAQ,CAACF,GAAGG,aAAa,GAAG;QAC3C,MAAM,IAAIC,MAAM,CAAC,6BAA6B,EAAEJ,GAAGG,aAAa,CAAC,0BAA0B,CAAC;IAC9F;IACA,KAAK,MAAME,KAAKL,GAAGM,gBAAgB,CAAE;QACnC,IAAI,CAACN,GAAGC,QAAQ,CAACC,QAAQ,CAACG,IAAI;YAC5B,MAAM,IAAID,MAAM,CAAC,yCAAyC,EAAEC,EAAE,gCAAgC,CAAC;QACjG;IACF;IACA,KAAK,MAAMA,KAAKL,GAAGO,gBAAgB,CAAE;QACnC,IAAI,CAACP,GAAGC,QAAQ,CAACC,QAAQ,CAACG,IAAI;YAC5B,MAAM,IAAID,MAAM,CAAC,yCAAyC,EAAEC,EAAE,gCAAgC,CAAC;QACjG;IACF;IACA,KAAK,MAAM,CAACG,MAAMC,QAAQ,IAAIC,OAAOC,OAAO,CAACX,GAAGY,WAAW,EAAG;QAC5D,IAAI,CAACZ,GAAGC,QAAQ,CAACC,QAAQ,CAACM,OAAO;YAC/B,MAAM,IAAIJ,MAAM,CAAC,mCAAmC,EAAEI,KAAK,gCAAgC,CAAC;QAC9F;QACA,KAAK,MAAMK,MAAMJ,QAAS;YACxB,IAAI,CAACT,GAAGC,QAAQ,CAACC,QAAQ,CAACW,KAAK;gBAC7B,MAAM,IAAIT,MAAM,CAAC,2BAA2B,EAAEI,KAAK,YAAY,EAAEK,GAAG,gCAAgC,CAAC;YACvG;QACF;IACF;IACA,6EAA6E;IAC7E,wEAAwE;IACxE,KAAK,MAAMR,KAAKL,GAAGO,gBAAgB,CAAE;QACnC,IAAI,AAACP,CAAAA,GAAGY,WAAW,CAACP,EAAE,EAAES,UAAU,CAAA,IAAK,GAAG;YACxC,MAAM,IAAIV,MACR,CAAC,yCAAyC,EAAEC,EAAE,mBAAmB,EAAEA,EAAE,oEAAoE,CAAC;QAE9I;IACF;IACA,sEAAsE;IACtE,IAAIL,GAAGO,gBAAgB,CAACL,QAAQ,CAACF,GAAGG,aAAa,GAAG;QAClD,MAAM,IAAIC,MAAM,CAAC,6BAA6B,EAAEJ,GAAGG,aAAa,CAAC,6BAA6B,CAAC;IACjG;IACA,IAAI,CAACH,GAAGC,QAAQ,CAACC,QAAQ,CAACF,GAAGe,aAAa,GAAG;QAC3C,MAAM,IAAIX,MAAM,CAAC,6BAA6B,EAAEJ,GAAGe,aAAa,CAAC,0BAA0B,CAAC;IAC9F;IACA,IAAI,CAACf,GAAGC,QAAQ,CAACC,QAAQ,CAACF,GAAGgB,YAAY,GAAG;QAC1C,MAAM,IAAIZ,MAAM,CAAC,4BAA4B,EAAEJ,GAAGgB,YAAY,CAAC,0BAA0B,CAAC;IAC5F;AACF;AAEA,OAAO,MAAMC,yBAAyB;IAAC;IAAS;IAAa;CAAO,CAAA;AACpE,OAAO,MAAMC,sBAAsB;IAAC;IAAY;IAAQ;IAAY;IAAW;CAAQ,CAAA;AAEvF,SAASC,yBACPC,aAAsC,EACtCC,aAAuB;IAEvB,MAAMC,KAAKF,cAAcG,iBAAiB;IAC1C,IAAI,CAACD,IAAI;QACP,OAAOE;IACT;IAEA,IAAI,CAACJ,cAAcK,iBAAiB,EAAE;QACpC,MAAM,IAAIrB,MAAM;IAClB;IACA,IAAIkB,GAAGI,UAAU,CAACZ,MAAM,KAAK,GAAG;QAC9B,MAAM,IAAIV,MAAM;IAClB;IACA,MAAMuB,eAAeL,GAAGK,YAAY,IAAI;IACxC,IAAI,CAACN,cAAcnB,QAAQ,CAACyB,eAAe;QACzC,MAAM,IAAIvB,MACR,CAAC,gCAAgC,EAAEuB,aAAa,2BAA2B,EAAEN,cAAcO,IAAI,CAAC,MAAM,CAAC,CAAC;IAE5G;IACA,MAAMC,iBAAiBP,GAAGO,cAAc,IAAIT,cAAcS,cAAc;IACxE,IAAI,CAACA,gBAAgB;QACnB,MAAM,IAAIzB,MACR;IAEJ;IAEA,OAAO;QACL0B,cAAcR,GAAGQ,YAAY;QAC7BC,UAAUT,GAAGS,QAAQ,IAAI;QACzBJ;QACAK,WAAWV,GAAGU,SAAS,IAAI;QAC3BN,YAAYJ,GAAGI,UAAU;QACzBG;IACF;AACF;AAEA,OAAO,MAAMI,gBAAgB;IAC3BC,WAAW;IACXC,OAAO;IACPC,cAAc;IACdC,WAAW;IACXC,WAAW;IACXC,UAAU;AACZ,EAAU;AAEV,OAAO,MAAMC,sBAAsB,eAAc;AACjD,OAAO,MAAMC,8BAA8B,MAAK;AAChD,OAAO,MAAMC,sBAAsB,EAAC;AACpC,OAAO,MAAMC,qCAAqC,GAAE;AAEpD,OAAO,SAASC,cACdxB,aAAsC;IAEtC,2EAA2E;IAC3E,4EAA4E;IAC5E,yEAAyE;IACzE,MAAMyB,WAAWzB,cAAcyB,QAAQ,IAAI;IAE3C,IAAI,CAACA,UAAU;QACb,IAAIzB,cAAcC,aAAa,KAAKG,aAAaJ,cAAcC,aAAa,CAACP,MAAM,KAAK,GAAG;YACzF,MAAM,IAAIV,MAAM;QAClB;QACA,IAAIgB,cAAc0B,UAAU,KAAKtB,aAAaJ,cAAc0B,UAAU,CAAChC,MAAM,KAAK,GAAG;YACnF,MAAM,IAAIV,MAAM;QAClB;IACF;IAEA,MAAMiB,gBAAgBD,cAAcC,aAAa,IAAIJ;IACrD,MAAM8B,oBAAoB3B,cAAc4B,aAAa;IACrD,MAAMC,MAAM7B,cAAcK,iBAAiB;IAC3C,MAAMyB,WAA4C;QAChDC,QAAQ/B,cAAc+B,MAAM,IAAI,CAAC;QACjCC,YAAYhC,cAAcgC,UAAU,IAAIZ;QACxCa,mBAAmBjC,cAAciC,iBAAiB,IAAIZ;QACtDa,0BACElC,cAAckC,wBAAwB,IAAIX;QAC5CY,qBAAqBnC,cAAcmC,mBAAmB,IAAI,CAAC;QAC3DC,mBAAmBpC,cAAcoC,iBAAiB,IAAId;QACtDG,UAAUzB,cAAcyB,QAAQ,IAAI;QACpCY,wBAAwBrC,cAAcqC,sBAAsB,IAAI,EAAE;QAClEC,iBAAiBtC,cAAcsC,eAAe;QAC9C,wEAAwE;QACxEC,oBAAoB;QACpBC,OAAOxC,cAAcwC,KAAK,IAAI,CAAC;QAC/Bd,YAAY1B,cAAc0B,UAAU,IAAI5B;QACxC2C,WAAW;QACXC,aAAa;YACXC,YAAY3C,cAAc0C,WAAW,EAAEC,cAAc;YACrDC,aAAa5C,cAAc0C,WAAW,EAAEE,eAAe;YACvDC,eAAe7C,cAAc0C,WAAW,EAAEG,iBAAiB;QAC7D;QACAxC,mBAAmBwB,MACf;YACEiB,YAAYjB,IAAIiB,UAAU,IAAI,EAAE;YAChCC,eAAelB,IAAIkB,aAAa,IAAI;YACpCC,iBAAiBnB,IAAImB,eAAe;YACpCC,YAAYpB,IAAIoB,UAAU,IAAI;YAC9BrC,WAAWiB,IAAIjB,SAAS,IAAIZ,cAAcG,iBAAiB,EAAES,aAAa;QAC5E,IACAR;QACJH;QACAiD,OAAO;YACLpC,WAAWd,cAAckD,KAAK,EAAEpC,aAAaD,cAAcC,SAAS;YACpEC,OAAOf,cAAckD,KAAK,EAAEnC,SAASF,cAAcE,KAAK;YACxDC,cAAchB,cAAckD,KAAK,EAAElC,gBAAgBH,cAAcG,YAAY;YAC7EC,WAAWjB,cAAckD,KAAK,EAAEjC,aAAaJ,cAAcI,SAAS;YACpEC,WAAWlB,cAAckD,KAAK,EAAEhC,aAAaL,cAAcK,SAAS;YACpEC,UAAUnB,cAAckD,KAAK,EAAE/B,YAAYN,cAAcM,QAAQ;QACnE;QACAhB,mBAAmBsB,WACfrB,YACAL,yBAAyBC,eAAeC;QAC5C2B,eAAeD,oBACX;YACEzC,kBACEyC,kBAAkBzC,gBAAgB,IAAIT,uBAAuBS,gBAAgB;YAC/EU,cAAc+B,kBAAkB/B,YAAY,IAAInB,uBAAuBmB,YAAY;YACnFD,eAAegC,kBAAkBhC,aAAa,IAAIlB,uBAAuBkB,aAAa;YACtFZ,eAAe4C,kBAAkB5C,aAAa,IAAIN,uBAAuBM,aAAa;YACtFF,UAAU8C,kBAAkB9C,QAAQ,IAAIJ,uBAAuBI,QAAQ;YACvEM,kBACEwC,kBAAkBxC,gBAAgB,IAAIV,uBAAuBU,gBAAgB;YAC/EK,aAAamC,kBAAkBnC,WAAW,IAAIf,uBAAuBe,WAAW;QAClF,IACA;YAAE,GAAGf,sBAAsB;QAAC;QAChC0E,UAAUnD,cAAcmD,QAAQ,IAAI;QACpC1C,gBAAgBT,cAAcS,cAAc,IAAIL;IAClD;IAEA,IAAI,CAACqB,UAAU;QACb9C,sBAAsBmD,SAASF,aAAa;QAC5ClD,iBAAiBoD,SAASqB,QAAQ;IACpC;IAEA,OAAOrB;AACT"}
1
+ {"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type {\n ReservationPluginConfig,\n ResolvedReservationPluginConfig,\n ResolvedStaffProvisioningConfig,\n StatusMachineConfig,\n} from './types.js'\n\nimport { DEFAULT_STATUS_MACHINE } from './types.js'\nimport { validateTimezone } from './utilities/timezoneUtils.js'\n\nfunction validateStatusMachine(sm: StatusMachineConfig): void {\n if (!sm.statuses.includes(sm.defaultStatus)) {\n throw new Error(`statusMachine.defaultStatus \"${sm.defaultStatus}\" is not in statuses array`)\n }\n for (const s of sm.blockingStatuses) {\n if (!sm.statuses.includes(s)) {\n throw new Error(`statusMachine.blockingStatuses contains \"${s}\" which is not in statuses array`)\n }\n }\n for (const s of sm.terminalStatuses) {\n if (!sm.statuses.includes(s)) {\n throw new Error(`statusMachine.terminalStatuses contains \"${s}\" which is not in statuses array`)\n }\n }\n for (const [from, targets] of Object.entries(sm.transitions)) {\n if (!sm.statuses.includes(from)) {\n throw new Error(`statusMachine.transitions has key \"${from}\" which is not in statuses array`)\n }\n for (const to of targets) {\n if (!sm.statuses.includes(to)) {\n throw new Error(`statusMachine.transitions[\"${from}\"] targets \"${to}\" which is not in statuses array`)\n }\n }\n }\n // A terminal status must have no outgoing transitions — otherwise the config\n // contradicts itself (the status is declared terminal but can be left).\n for (const s of sm.terminalStatuses) {\n if ((sm.transitions[s]?.length ?? 0) > 0) {\n throw new Error(\n `statusMachine.terminalStatuses contains \"${s}\" but transitions[\"${s}\"] is non-empty — a terminal status cannot have outgoing transitions`,\n )\n }\n }\n // A reservation is born in defaultStatus, so it must not be terminal.\n if (sm.terminalStatuses.includes(sm.defaultStatus)) {\n throw new Error(`statusMachine.defaultStatus \"${sm.defaultStatus}\" cannot be a terminal status`)\n }\n if (!sm.statuses.includes(sm.confirmStatus)) {\n throw new Error(`statusMachine.confirmStatus \"${sm.confirmStatus}\" is not in statuses array`)\n }\n if (!sm.statuses.includes(sm.cancelStatus)) {\n throw new Error(`statusMachine.cancelStatus \"${sm.cancelStatus}\" is not in statuses array`)\n }\n}\n\nexport const DEFAULT_RESOURCE_TYPES = ['staff', 'equipment', 'room']\nexport const DEFAULT_LEAVE_TYPES = ['vacation', 'sick', 'personal', 'closure', 'other']\n\nfunction resolveStaffProvisioning(\n pluginOptions: ReservationPluginConfig,\n resourceTypes: string[],\n): ResolvedStaffProvisioningConfig | undefined {\n const sp = pluginOptions.staffProvisioning\n if (!sp) {\n return undefined\n }\n\n if (!pluginOptions.resourceOwnerMode) {\n throw new Error('staffProvisioning requires resourceOwnerMode to be enabled')\n }\n if (sp.staffRoles.length === 0) {\n throw new Error('staffProvisioning.staffRoles must be a non-empty array')\n }\n const resourceType = sp.resourceType ?? 'staff'\n if (!resourceTypes.includes(resourceType)) {\n throw new Error(\n `staffProvisioning.resourceType \"${resourceType}\" is not in resourceTypes [${resourceTypes.join(', ')}]`,\n )\n }\n const userCollection = sp.userCollection ?? pluginOptions.userCollection\n if (!userCollection) {\n throw new Error(\n 'staffProvisioning.userCollection is required when top-level userCollection is unset',\n )\n }\n\n return {\n beforeCreate: sp.beforeCreate,\n nameFrom: sp.nameFrom ?? 'name',\n resourceType,\n roleField: sp.roleField ?? 'role',\n staffRoles: sp.staffRoles,\n userCollection,\n }\n}\n\nexport const DEFAULT_SLUGS = {\n customers: 'customers',\n media: 'media',\n reservations: 'reservations',\n resources: 'resources',\n schedules: 'schedules',\n services: 'services',\n} as const\n\nexport const DEFAULT_ADMIN_GROUP = 'Reservations'\nexport const DEFAULT_ALLOW_GUEST_BOOKING = false\nexport const DEFAULT_BUFFER_TIME = 0\nexport const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24\n\nexport function resolveConfig(\n pluginOptions: ReservationPluginConfig,\n): ResolvedReservationPluginConfig {\n // A disabled plugin still resolves (so collections register with the right\n // slugs for schema stability) but skips all config validation — temporarily\n // disabling a misconfigured plugin should not throw at boot (review C3).\n const disabled = pluginOptions.disabled ?? false\n\n if (!disabled) {\n if (pluginOptions.resourceTypes !== undefined && pluginOptions.resourceTypes.length === 0) {\n throw new Error('resourceTypes must be a non-empty array')\n }\n if (pluginOptions.leaveTypes !== undefined && pluginOptions.leaveTypes.length === 0) {\n throw new Error('leaveTypes must be a non-empty array')\n }\n }\n\n const resourceTypes = pluginOptions.resourceTypes ?? DEFAULT_RESOURCE_TYPES\n const userStatusMachine = pluginOptions.statusMachine\n const rom = pluginOptions.resourceOwnerMode\n const resolved: ResolvedReservationPluginConfig = {\n access: pluginOptions.access ?? {},\n adminGroup: pluginOptions.adminGroup ?? DEFAULT_ADMIN_GROUP,\n allowGuestBooking: pluginOptions.allowGuestBooking ?? DEFAULT_ALLOW_GUEST_BOOKING,\n cancellationNoticePeriod:\n pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,\n collectionOverrides: pluginOptions.collectionOverrides ?? {},\n debug: pluginOptions.debug ?? false,\n defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,\n disabled: pluginOptions.disabled ?? false,\n extraReservationFields: pluginOptions.extraReservationFields ?? [],\n getExternalBusy: pluginOptions.getExternalBusy,\n // Real value is set by the plugin once config.collections is known (C8)\n hasMediaCollection: false,\n hooks: pluginOptions.hooks ?? {},\n leaveTypes: pluginOptions.leaveTypes ?? DEFAULT_LEAVE_TYPES,\n localized: false,\n multiTenant: {\n cookieName: pluginOptions.multiTenant?.cookieName ?? 'payload-tenant',\n tenantField: pluginOptions.multiTenant?.tenantField ?? 'tenant',\n timezoneField: pluginOptions.multiTenant?.timezoneField ?? 'timezone',\n },\n resourceOwnerMode: rom\n ? {\n adminRoles: rom.adminRoles ?? [],\n ownedServices: rom.ownedServices ?? false,\n ownerCollection: rom.ownerCollection,\n ownerField: rom.ownerField ?? 'owner',\n roleField: rom.roleField ?? pluginOptions.staffProvisioning?.roleField ?? 'role',\n }\n : undefined,\n resourceTypes,\n slugs: {\n customers: pluginOptions.slugs?.customers ?? DEFAULT_SLUGS.customers,\n media: pluginOptions.slugs?.media ?? DEFAULT_SLUGS.media,\n reservations: pluginOptions.slugs?.reservations ?? DEFAULT_SLUGS.reservations,\n resources: pluginOptions.slugs?.resources ?? DEFAULT_SLUGS.resources,\n schedules: pluginOptions.slugs?.schedules ?? DEFAULT_SLUGS.schedules,\n services: pluginOptions.slugs?.services ?? DEFAULT_SLUGS.services,\n },\n staffProvisioning: disabled\n ? undefined\n : resolveStaffProvisioning(pluginOptions, resourceTypes),\n statusMachine: userStatusMachine\n ? {\n blockingStatuses:\n userStatusMachine.blockingStatuses ?? DEFAULT_STATUS_MACHINE.blockingStatuses,\n cancelStatus: userStatusMachine.cancelStatus ?? DEFAULT_STATUS_MACHINE.cancelStatus,\n confirmStatus: userStatusMachine.confirmStatus ?? DEFAULT_STATUS_MACHINE.confirmStatus,\n defaultStatus: userStatusMachine.defaultStatus ?? DEFAULT_STATUS_MACHINE.defaultStatus,\n statuses: userStatusMachine.statuses ?? DEFAULT_STATUS_MACHINE.statuses,\n terminalStatuses:\n userStatusMachine.terminalStatuses ?? DEFAULT_STATUS_MACHINE.terminalStatuses,\n transitions: userStatusMachine.transitions ?? DEFAULT_STATUS_MACHINE.transitions,\n }\n : { ...DEFAULT_STATUS_MACHINE },\n timezone: pluginOptions.timezone ?? 'UTC',\n userCollection: pluginOptions.userCollection ?? undefined,\n }\n\n if (!disabled) {\n validateStatusMachine(resolved.statusMachine)\n validateTimezone(resolved.timezone)\n }\n\n return resolved\n}\n"],"names":["DEFAULT_STATUS_MACHINE","validateTimezone","validateStatusMachine","sm","statuses","includes","defaultStatus","Error","s","blockingStatuses","terminalStatuses","from","targets","Object","entries","transitions","to","length","confirmStatus","cancelStatus","DEFAULT_RESOURCE_TYPES","DEFAULT_LEAVE_TYPES","resolveStaffProvisioning","pluginOptions","resourceTypes","sp","staffProvisioning","undefined","resourceOwnerMode","staffRoles","resourceType","join","userCollection","beforeCreate","nameFrom","roleField","DEFAULT_SLUGS","customers","media","reservations","resources","schedules","services","DEFAULT_ADMIN_GROUP","DEFAULT_ALLOW_GUEST_BOOKING","DEFAULT_BUFFER_TIME","DEFAULT_CANCELLATION_NOTICE_PERIOD","resolveConfig","disabled","leaveTypes","userStatusMachine","statusMachine","rom","resolved","access","adminGroup","allowGuestBooking","cancellationNoticePeriod","collectionOverrides","debug","defaultBufferTime","extraReservationFields","getExternalBusy","hasMediaCollection","hooks","localized","multiTenant","cookieName","tenantField","timezoneField","adminRoles","ownedServices","ownerCollection","ownerField","slugs","timezone"],"mappings":"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"}
@@ -1,9 +1,11 @@
1
1
  import { getAvailableSlots } from '../services/AvailabilityService.js';
2
+ import { createReserveDebug } from '../utilities/reserveDebug.js';
2
3
  import { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js';
3
4
  import { getDayKeyInTimezone, isValidDayKey } from '../utilities/timezoneUtils.js';
4
5
  export function createCheckAvailabilityEndpoint(config) {
5
6
  return {
6
7
  handler: async (req)=>{
8
+ const dbg = createReserveDebug(req.payload.logger, config.debug);
7
9
  const url = new URL(req.url);
8
10
  const date = url.searchParams.get('date');
9
11
  const resource = url.searchParams.get('resource');
@@ -85,9 +87,18 @@ export function createCheckAvailabilityEndpoint(config) {
85
87
  }
86
88
  const requiredIds = (svcDoc?.requiredResources ?? []).map((r)=>extractId(r)).filter((r)=>r !== undefined);
87
89
  const resourceIds = mergeResourceIds(callerIds, requiredIds);
90
+ dbg.dbg('request', {
91
+ date: dayKey,
92
+ endpoint: 'availability',
93
+ guestCount,
94
+ resourceIds,
95
+ serviceId: service,
96
+ timeZone: config.timezone
97
+ });
88
98
  const slots = await getAvailableSlots({
89
99
  blockingStatuses: config.statusMachine.blockingStatuses,
90
100
  date: dayKey,
101
+ debug: dbg,
91
102
  getExternalBusy: config.getExternalBusy,
92
103
  guestCount,
93
104
  payload: req.payload,
@@ -100,6 +111,10 @@ export function createCheckAvailabilityEndpoint(config) {
100
111
  serviceSlug: config.slugs.services,
101
112
  timeZone: config.timezone
102
113
  });
114
+ dbg.dbg('response', {
115
+ endpoint: 'availability',
116
+ slotCount: slots.length
117
+ });
103
118
  return Response.json({
104
119
  slots
105
120
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/checkAvailability.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { getAvailableSlots } from '../services/AvailabilityService.js'\nimport { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js'\nimport { getDayKeyInTimezone, isValidDayKey } from '../utilities/timezoneUtils.js'\n\nexport function createCheckAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n const url = new URL(req.url!)\n const date = url.searchParams.get('date')\n const resource = url.searchParams.get('resource')\n const service = url.searchParams.get('service')\n\n if (!date || !resource || !service) {\n return Response.json(\n { message: 'Missing required query params: resource, date, service' },\n { status: 400 },\n )\n }\n\n // YYYY-MM-DD is taken as a business-TZ calendar day verbatim; any other\n // parseable date is re-keyed into the business timezone. Never\n // `new Date('YYYY-MM-DD')` — that pins the day to UTC midnight.\n let dayKey: string\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(date)) {\n if (!isValidDayKey(date)) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n dayKey = date\n } else {\n const parsed = new Date(date)\n if (isNaN(parsed.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n dayKey = getDayKeyInTimezone(parsed, config.timezone)\n }\n\n const guestCountRaw = Number(url.searchParams.get('guestCount') ?? '1')\n if (!Number.isFinite(guestCountRaw)) {\n return Response.json({ error: 'Invalid guestCount' }, { status: 400 })\n }\n const guestCount = Math.max(Math.floor(guestCountRaw), 1)\n\n // Resolve required resource set: caller resource(s) ∪ service.requiredResources\n const explicit = url.searchParams.get('resources')\n const callerIds = explicit ? explicit.split(',').map((s) => s.trim()).filter(Boolean) : [resource]\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const svcDoc = await (req.payload.findByID as any)({\n id: service,\n collection: config.slugs.services,\n depth: 0,\n req,\n }).catch(() => null)\n if (!svcDoc) {\n return Response.json({ error: 'Service not found' }, { status: 404 })\n }\n\n // Validate the primary resource id up front — a malformed id in a query\n // surfaces as an adapter cast error (500) instead of a clean 404.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resourceDoc = await (req.payload.findByID as any)({\n id: resource,\n collection: config.slugs.resources,\n depth: 0,\n req,\n }).catch(() => null)\n if (!resourceDoc) {\n return Response.json({ error: 'Resource not found' }, { status: 404 })\n }\n const requiredIds = ((svcDoc?.requiredResources as unknown[]) ?? [])\n .map((r) => extractId(r))\n .filter((r): r is number | string => r !== undefined)\n const resourceIds = mergeResourceIds(callerIds, requiredIds)\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: dayKey,\n getExternalBusy: config.getExternalBusy,\n guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceIds,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n serviceId: service,\n serviceSlug: config.slugs.services,\n timeZone: config.timezone,\n })\n\n return Response.json({ slots })\n },\n method: 'get',\n path: '/reserve/availability',\n }\n}\n"],"names":["getAvailableSlots","extractId","mergeResourceIds","getDayKeyInTimezone","isValidDayKey","createCheckAvailabilityEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","message","status","dayKey","test","error","parsed","Date","isNaN","getTime","timezone","guestCountRaw","Number","isFinite","guestCount","Math","max","floor","explicit","callerIds","split","map","s","trim","filter","Boolean","svcDoc","payload","findByID","id","collection","slugs","services","depth","catch","resourceDoc","resources","requiredIds","requiredResources","r","undefined","resourceIds","slots","blockingStatuses","statusMachine","getExternalBusy","reservationSlug","reservations","resourceSlug","scheduleSlug","schedules","serviceId","serviceSlug","timeZone","method","path"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/endpoints/checkAvailability.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { getAvailableSlots } from '../services/AvailabilityService.js'\nimport { createReserveDebug } from '../utilities/reserveDebug.js'\nimport { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js'\nimport { getDayKeyInTimezone, isValidDayKey } from '../utilities/timezoneUtils.js'\n\nexport function createCheckAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n const dbg = createReserveDebug(req.payload.logger, config.debug)\n const url = new URL(req.url!)\n const date = url.searchParams.get('date')\n const resource = url.searchParams.get('resource')\n const service = url.searchParams.get('service')\n\n if (!date || !resource || !service) {\n return Response.json(\n { message: 'Missing required query params: resource, date, service' },\n { status: 400 },\n )\n }\n\n // YYYY-MM-DD is taken as a business-TZ calendar day verbatim; any other\n // parseable date is re-keyed into the business timezone. Never\n // `new Date('YYYY-MM-DD')` — that pins the day to UTC midnight.\n let dayKey: string\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(date)) {\n if (!isValidDayKey(date)) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n dayKey = date\n } else {\n const parsed = new Date(date)\n if (isNaN(parsed.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n dayKey = getDayKeyInTimezone(parsed, config.timezone)\n }\n\n const guestCountRaw = Number(url.searchParams.get('guestCount') ?? '1')\n if (!Number.isFinite(guestCountRaw)) {\n return Response.json({ error: 'Invalid guestCount' }, { status: 400 })\n }\n const guestCount = Math.max(Math.floor(guestCountRaw), 1)\n\n // Resolve required resource set: caller resource(s) ∪ service.requiredResources\n const explicit = url.searchParams.get('resources')\n const callerIds = explicit ? explicit.split(',').map((s) => s.trim()).filter(Boolean) : [resource]\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const svcDoc = await (req.payload.findByID as any)({\n id: service,\n collection: config.slugs.services,\n depth: 0,\n req,\n }).catch(() => null)\n if (!svcDoc) {\n return Response.json({ error: 'Service not found' }, { status: 404 })\n }\n\n // Validate the primary resource id up front — a malformed id in a query\n // surfaces as an adapter cast error (500) instead of a clean 404.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resourceDoc = await (req.payload.findByID as any)({\n id: resource,\n collection: config.slugs.resources,\n depth: 0,\n req,\n }).catch(() => null)\n if (!resourceDoc) {\n return Response.json({ error: 'Resource not found' }, { status: 404 })\n }\n const requiredIds = ((svcDoc?.requiredResources as unknown[]) ?? [])\n .map((r) => extractId(r))\n .filter((r): r is number | string => r !== undefined)\n const resourceIds = mergeResourceIds(callerIds, requiredIds)\n\n dbg.dbg('request', {\n date: dayKey,\n endpoint: 'availability',\n guestCount,\n resourceIds,\n serviceId: service,\n timeZone: config.timezone,\n })\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: dayKey,\n debug: dbg,\n getExternalBusy: config.getExternalBusy,\n guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceIds,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n serviceId: service,\n serviceSlug: config.slugs.services,\n timeZone: config.timezone,\n })\n\n dbg.dbg('response', { endpoint: 'availability', slotCount: slots.length })\n\n return Response.json({ slots })\n },\n method: 'get',\n path: '/reserve/availability',\n }\n}\n"],"names":["getAvailableSlots","createReserveDebug","extractId","mergeResourceIds","getDayKeyInTimezone","isValidDayKey","createCheckAvailabilityEndpoint","config","handler","req","dbg","payload","logger","debug","url","URL","date","searchParams","get","resource","service","Response","json","message","status","dayKey","test","error","parsed","Date","isNaN","getTime","timezone","guestCountRaw","Number","isFinite","guestCount","Math","max","floor","explicit","callerIds","split","map","s","trim","filter","Boolean","svcDoc","findByID","id","collection","slugs","services","depth","catch","resourceDoc","resources","requiredIds","requiredResources","r","undefined","resourceIds","endpoint","serviceId","timeZone","slots","blockingStatuses","statusMachine","getExternalBusy","reservationSlug","reservations","resourceSlug","scheduleSlug","schedules","serviceSlug","slotCount","length","method","path"],"mappings":"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"}
@@ -1,9 +1,11 @@
1
1
  import { getAvailableSlots } from '../services/AvailabilityService.js';
2
+ import { createReserveDebug } from '../utilities/reserveDebug.js';
2
3
  import { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js';
3
4
  import { getDayKeyInTimezone, isValidDayKey } from '../utilities/timezoneUtils.js';
4
5
  export function createGetSlotsEndpoint(config) {
5
6
  return {
6
7
  handler: async (req)=>{
8
+ const dbg = createReserveDebug(req.payload.logger, config.debug);
7
9
  const url = new URL(req.url);
8
10
  const date = url.searchParams.get('date');
9
11
  const resource = url.searchParams.get('resource');
@@ -85,9 +87,18 @@ export function createGetSlotsEndpoint(config) {
85
87
  }
86
88
  const requiredIds = (svcDoc?.requiredResources ?? []).map((r)=>extractId(r)).filter((r)=>r !== undefined);
87
89
  const resourceIds = mergeResourceIds(callerIds, requiredIds);
90
+ dbg.dbg('request', {
91
+ date: dayKey,
92
+ endpoint: 'slots',
93
+ guestCount,
94
+ resourceIds,
95
+ serviceId: service,
96
+ timeZone: config.timezone
97
+ });
88
98
  const slots = await getAvailableSlots({
89
99
  blockingStatuses: config.statusMachine.blockingStatuses,
90
100
  date: dayKey,
101
+ debug: dbg,
91
102
  getExternalBusy: config.getExternalBusy,
92
103
  guestCount,
93
104
  payload: req.payload,
@@ -100,6 +111,10 @@ export function createGetSlotsEndpoint(config) {
100
111
  serviceSlug: config.slugs.services,
101
112
  timeZone: config.timezone
102
113
  });
114
+ dbg.dbg('response', {
115
+ endpoint: 'slots',
116
+ slotCount: slots.length
117
+ });
103
118
  return Response.json({
104
119
  date,
105
120
  guestCount,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/getSlots.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { getAvailableSlots } from '../services/AvailabilityService.js'\nimport { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js'\nimport { getDayKeyInTimezone, isValidDayKey } from '../utilities/timezoneUtils.js'\n\nexport function createGetSlotsEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const url = new URL(req.url!)\n const date = url.searchParams.get('date')\n const resource = url.searchParams.get('resource')\n const service = url.searchParams.get('service')\n\n if (!date || !resource || !service) {\n return Response.json(\n { error: 'Missing required query params: resource, date, service' },\n { status: 400 },\n )\n }\n\n // YYYY-MM-DD is taken as a business-TZ calendar day verbatim; any other\n // parseable date is re-keyed into the business timezone. Never\n // `new Date('YYYY-MM-DD')` — that pins the day to UTC midnight.\n let dayKey: string\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(date)) {\n if (!isValidDayKey(date)) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n dayKey = date\n } else {\n const parsed = new Date(date)\n if (isNaN(parsed.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n dayKey = getDayKeyInTimezone(parsed, config.timezone)\n }\n\n const guestCountRaw = Number(url.searchParams.get('guestCount') ?? '1')\n if (!Number.isFinite(guestCountRaw)) {\n return Response.json({ error: 'Invalid guestCount' }, { status: 400 })\n }\n const guestCount = Math.max(Math.floor(guestCountRaw), 1)\n\n // Resolve required resource set: caller resource(s) ∪ service.requiredResources\n const explicit = url.searchParams.get('resources')\n const callerIds = explicit ? explicit.split(',').map((s) => s.trim()).filter(Boolean) : [resource]\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const svcDoc = await (req.payload.findByID as any)({\n id: service,\n collection: config.slugs.services,\n depth: 0,\n req,\n }).catch(() => null)\n if (!svcDoc) {\n return Response.json({ error: 'Service not found' }, { status: 404 })\n }\n\n // Validate the primary resource id up front — a malformed id in a query\n // surfaces as an adapter cast error (500) instead of a clean 404.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resourceDoc = await (req.payload.findByID as any)({\n id: resource,\n collection: config.slugs.resources,\n depth: 0,\n req,\n }).catch(() => null)\n if (!resourceDoc) {\n return Response.json({ error: 'Resource not found' }, { status: 404 })\n }\n const requiredIds = ((svcDoc?.requiredResources as unknown[]) ?? [])\n .map((r) => extractId(r))\n .filter((r): r is number | string => r !== undefined)\n const resourceIds = mergeResourceIds(callerIds, requiredIds)\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: dayKey,\n getExternalBusy: config.getExternalBusy,\n guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceIds,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n serviceId: service,\n serviceSlug: config.slugs.services,\n timeZone: config.timezone,\n })\n\n return Response.json({\n date,\n guestCount,\n slots: slots.map((s) => ({ end: s.end.toISOString(), start: s.start.toISOString() })),\n })\n },\n method: 'get',\n path: '/reserve/slots',\n }\n}\n"],"names":["getAvailableSlots","extractId","mergeResourceIds","getDayKeyInTimezone","isValidDayKey","createGetSlotsEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","error","status","dayKey","test","parsed","Date","isNaN","getTime","timezone","guestCountRaw","Number","isFinite","guestCount","Math","max","floor","explicit","callerIds","split","map","s","trim","filter","Boolean","svcDoc","payload","findByID","id","collection","slugs","services","depth","catch","resourceDoc","resources","requiredIds","requiredResources","r","undefined","resourceIds","slots","blockingStatuses","statusMachine","getExternalBusy","reservationSlug","reservations","resourceSlug","scheduleSlug","schedules","serviceId","serviceSlug","timeZone","end","toISOString","start","method","path"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/endpoints/getSlots.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { getAvailableSlots } from '../services/AvailabilityService.js'\nimport { createReserveDebug } from '../utilities/reserveDebug.js'\nimport { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js'\nimport { getDayKeyInTimezone, isValidDayKey } from '../utilities/timezoneUtils.js'\n\nexport function createGetSlotsEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const dbg = createReserveDebug(req.payload.logger, config.debug)\n const url = new URL(req.url!)\n const date = url.searchParams.get('date')\n const resource = url.searchParams.get('resource')\n const service = url.searchParams.get('service')\n\n if (!date || !resource || !service) {\n return Response.json(\n { error: 'Missing required query params: resource, date, service' },\n { status: 400 },\n )\n }\n\n // YYYY-MM-DD is taken as a business-TZ calendar day verbatim; any other\n // parseable date is re-keyed into the business timezone. Never\n // `new Date('YYYY-MM-DD')` — that pins the day to UTC midnight.\n let dayKey: string\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(date)) {\n if (!isValidDayKey(date)) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n dayKey = date\n } else {\n const parsed = new Date(date)\n if (isNaN(parsed.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n dayKey = getDayKeyInTimezone(parsed, config.timezone)\n }\n\n const guestCountRaw = Number(url.searchParams.get('guestCount') ?? '1')\n if (!Number.isFinite(guestCountRaw)) {\n return Response.json({ error: 'Invalid guestCount' }, { status: 400 })\n }\n const guestCount = Math.max(Math.floor(guestCountRaw), 1)\n\n // Resolve required resource set: caller resource(s) ∪ service.requiredResources\n const explicit = url.searchParams.get('resources')\n const callerIds = explicit ? explicit.split(',').map((s) => s.trim()).filter(Boolean) : [resource]\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const svcDoc = await (req.payload.findByID as any)({\n id: service,\n collection: config.slugs.services,\n depth: 0,\n req,\n }).catch(() => null)\n if (!svcDoc) {\n return Response.json({ error: 'Service not found' }, { status: 404 })\n }\n\n // Validate the primary resource id up front — a malformed id in a query\n // surfaces as an adapter cast error (500) instead of a clean 404.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resourceDoc = await (req.payload.findByID as any)({\n id: resource,\n collection: config.slugs.resources,\n depth: 0,\n req,\n }).catch(() => null)\n if (!resourceDoc) {\n return Response.json({ error: 'Resource not found' }, { status: 404 })\n }\n const requiredIds = ((svcDoc?.requiredResources as unknown[]) ?? [])\n .map((r) => extractId(r))\n .filter((r): r is number | string => r !== undefined)\n const resourceIds = mergeResourceIds(callerIds, requiredIds)\n\n dbg.dbg('request', {\n date: dayKey,\n endpoint: 'slots',\n guestCount,\n resourceIds,\n serviceId: service,\n timeZone: config.timezone,\n })\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: dayKey,\n debug: dbg,\n getExternalBusy: config.getExternalBusy,\n guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceIds,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n serviceId: service,\n serviceSlug: config.slugs.services,\n timeZone: config.timezone,\n })\n\n dbg.dbg('response', { endpoint: 'slots', slotCount: slots.length })\n\n return Response.json({\n date,\n guestCount,\n slots: slots.map((s) => ({ end: s.end.toISOString(), start: s.start.toISOString() })),\n })\n },\n method: 'get',\n path: '/reserve/slots',\n }\n}\n"],"names":["getAvailableSlots","createReserveDebug","extractId","mergeResourceIds","getDayKeyInTimezone","isValidDayKey","createGetSlotsEndpoint","config","handler","req","dbg","payload","logger","debug","url","URL","date","searchParams","get","resource","service","Response","json","error","status","dayKey","test","parsed","Date","isNaN","getTime","timezone","guestCountRaw","Number","isFinite","guestCount","Math","max","floor","explicit","callerIds","split","map","s","trim","filter","Boolean","svcDoc","findByID","id","collection","slugs","services","depth","catch","resourceDoc","resources","requiredIds","requiredResources","r","undefined","resourceIds","endpoint","serviceId","timeZone","slots","blockingStatuses","statusMachine","getExternalBusy","reservationSlug","reservations","resourceSlug","scheduleSlug","schedules","serviceSlug","slotCount","length","end","toISOString","start","method","path"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import type { Endpoint, Payload, PayloadRequest } from 'payload';
2
2
  import type { ExternalBusyInterval, GetExternalBusy, ResolvedReservationPluginConfig } from '../types.js';
3
+ import { type ReserveDebug } from '../utilities/reserveDebug.js';
3
4
  type DayAvailability = {
4
5
  date: string;
5
6
  shiftWindows: Array<{
@@ -35,6 +36,7 @@ export type ResourceAvailability = {
35
36
  };
36
37
  export declare function buildResourceAvailability(params: {
37
38
  blockingStatuses: string[];
39
+ debug?: ReserveDebug;
38
40
  end: Date;
39
41
  getExternalBusy?: GetExternalBusy;
40
42
  payload: Payload;
@@ -1,3 +1,4 @@
1
+ import { createReserveDebug, NOOP_RESERVE_DEBUG } from '../utilities/reserveDebug.js';
1
2
  import { resolveScheduleForDate } from '../utilities/scheduleUtils.js';
2
3
  import { readCookie } from '../utilities/tenantFilter.js';
3
4
  import { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js';
@@ -55,7 +56,8 @@ const MAX_RANGE_MS = 90 * 86_400_000;
55
56
  }));
56
57
  }
57
58
  export async function buildResourceAvailability(params) {
58
- const { blockingStatuses, end, getExternalBusy, payload, req, reservationSlug, resourceId, resourceSlug, scheduleSlug, start, timeZone } = params;
59
+ const { blockingStatuses, debug, end, getExternalBusy, payload, req, reservationSlug, resourceId, resourceSlug, scheduleSlug, start, timeZone } = params;
60
+ const trace = debug ?? NOOP_RESERVE_DEBUG;
59
61
  // depth 1 so `services` are populated (their `requiredResources` come back as ids)
60
62
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
63
  const resource = await payload.findByID({
@@ -197,7 +199,12 @@ export async function buildResourceAvailability(params) {
197
199
  resourceId,
198
200
  start
199
201
  })).filter((iv)=>!isNaN(Date.parse(iv.start)) && !isNaN(Date.parse(iv.end)));
200
- } catch {
202
+ } catch (err) {
203
+ trace.dbg('error', {
204
+ err,
205
+ resourceId,
206
+ where: 'getExternalBusy'
207
+ });
201
208
  external = [];
202
209
  }
203
210
  }
@@ -230,6 +237,7 @@ export function createResourceAvailabilityEndpoint(config) {
230
237
  status: 403
231
238
  });
232
239
  }
240
+ const dbg = createReserveDebug(req.payload.logger, config.debug);
233
241
  const url = new URL(req.url);
234
242
  const resource = url.searchParams.get('resource');
235
243
  const start = url.searchParams.get('start');
@@ -278,8 +286,16 @@ export function createResourceAvailabilityEndpoint(config) {
278
286
  tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),
279
287
  timezoneField: config.multiTenant.timezoneField
280
288
  });
289
+ dbg.dbg('request', {
290
+ end,
291
+ endpoint: 'resource-availability',
292
+ resource,
293
+ start,
294
+ timeZone
295
+ });
281
296
  const result = await buildResourceAvailability({
282
297
  blockingStatuses: config.statusMachine.blockingStatuses,
298
+ debug: dbg,
283
299
  end: endDate,
284
300
  getExternalBusy: config.getExternalBusy,
285
301
  payload: req.payload,
@@ -292,12 +308,21 @@ export function createResourceAvailabilityEndpoint(config) {
292
308
  timeZone
293
309
  });
294
310
  if (!result) {
311
+ dbg.dbg('response', {
312
+ endpoint: 'resource-availability',
313
+ reason: 'resource_not_found'
314
+ });
295
315
  return Response.json({
296
316
  error: 'Resource not found'
297
317
  }, {
298
318
  status: 404
299
319
  });
300
320
  }
321
+ dbg.dbg('response', {
322
+ dayCount: result.days.length,
323
+ endpoint: 'resource-availability',
324
+ externalCount: result.external.length
325
+ });
301
326
  return Response.json(result);
302
327
  },
303
328
  method: 'get',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/resourceAvailability.ts"],"sourcesContent":["import type { Endpoint, Payload, PayloadRequest, Where } from 'payload'\n\nimport type {\n ExternalBusyInterval,\n GetExternalBusy,\n ResolvedReservationPluginConfig,\n} from '../types.js'\n\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { readCookie } from '../utilities/tenantFilter.js'\nimport { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js'\nimport {\n addDaysToDayKey,\n combineDayKeyAndTime,\n getDayKeyInTimezone,\n} from '../utilities/timezoneUtils.js'\nimport { isPrivilegedUser } from '../utilities/userRoles.js'\n\nconst MAX_RANGE_MS = 90 * 86_400_000\n\ntype DayAvailability = {\n date: string\n shiftWindows: Array<{ end: string; start: string }>\n timeOff: Array<{ end: string; reason?: string; start: string; type?: string }>\n}\n\ntype Busy = Array<{ end: string; start: string; units: number }>\n\nexport type ResourceAvailability = {\n busy: Busy\n capacityMode: 'per-guest' | 'per-reservation'\n days: DayAvailability[]\n /** External busy intervals (calendar sync etc.) from getExternalBusy — display-only here; enforcement lives in checkAvailability. */\n external: ExternalBusyInterval[]\n quantity: number\n /** Capacity of resources this resource's services also require (e.g. a chair pool). */\n requiredPools: Array<{ busy: Busy; quantity: number }>\n /** IANA zone the day windows were resolved in (selected tenant's zone, else global). */\n timeZone: string\n}\n\n/** Busy intervals (with capacity units) for one resource over [start, end). */\nasync function busyFor(args: {\n blockingStatuses: string[]\n capacityMode: 'per-guest' | 'per-reservation'\n end: Date\n payload: Payload\n reservationSlug: string\n resourceId: number | string\n start: Date\n}): Promise<Busy> {\n const { blockingStatuses, capacityMode, end, payload, reservationSlug, resourceId, start } = args\n const where: Where = {\n and: [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: end.toISOString() } },\n { endTime: { greater_than: start.toISOString() } },\n { or: [{ resource: { equals: resourceId } }, { 'items.resource': { equals: resourceId } }] },\n ],\n }\n // limit:0 = all matching — bounded by the endpoint's 90-day range cap, so this\n // can't run away, and the grid no longer silently drops busy intervals (D9).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({\n collection: reservationSlug,\n depth: 0,\n limit: 0,\n where,\n })\n return (docs as Array<Record<string, unknown>>)\n .filter((r) => r.startTime && r.endTime)\n .map((r) => ({\n end: new Date(r.endTime as string).toISOString(),\n start: new Date(r.startTime as string).toISOString(),\n units: capacityMode === 'per-guest' ? ((r.guestCount as number) ?? 1) : 1,\n }))\n}\n\nexport async function buildResourceAvailability(params: {\n blockingStatuses: string[]\n end: Date\n getExternalBusy?: GetExternalBusy\n payload: Payload\n req?: PayloadRequest\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n scheduleSlug: string\n start: Date\n timeZone: string\n}): Promise<null | ResourceAvailability> {\n const {\n blockingStatuses,\n end,\n getExternalBusy,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n scheduleSlug,\n start,\n timeZone,\n } = params\n\n // depth 1 so `services` are populated (their `requiredResources` come back as ids)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: resourceSlug,\n depth: 1,\n }).catch(() => null)\n if (!resource) {\n return null\n }\n const quantity = (resource?.quantity as number) ?? 1\n const capacityMode =\n (resource?.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: schedules } = await (payload.find as any)({\n collection: scheduleSlug,\n depth: 0,\n limit: 100,\n where: { and: [{ active: { equals: true } }, { resource: { equals: resourceId } }] },\n })\n\n type RawException = {\n date: string\n endDate?: string\n reason?: string\n type?: string\n }\n\n const days: DayAvailability[] = []\n const startKey = getDayKeyInTimezone(start, timeZone)\n const lastKey = getDayKeyInTimezone(new Date(end.getTime() - 1), timeZone)\n for (let date = startKey; date <= lastKey; date = addDaysToDayKey(date, 1)) {\n const shiftWindows: DayAvailability['shiftWindows'] = []\n const timeOff: DayAvailability['timeOff'] = []\n\n // A11: an exception on ANY of the resource's schedules makes the whole\n // resource unavailable that day. Find the first matching exception (if any)\n // — it suppresses all shift windows and marks the full day as time-off.\n let dayException: RawException | undefined\n for (const sched of schedules as Array<Record<string, unknown>>) {\n const exceptions = (sched.exceptions as RawException[] | undefined) ?? []\n for (const exc of exceptions) {\n const excStart = getDayKeyInTimezone(new Date(exc.date), timeZone)\n const excEnd = exc.endDate ? getDayKeyInTimezone(new Date(exc.endDate), timeZone) : excStart\n if (date >= excStart && date <= excEnd) {\n dayException = exc\n break\n }\n }\n if (dayException) {\n break\n }\n }\n\n if (dayException) {\n const dayStart = combineDayKeyAndTime(date, '00:00', timeZone)\n const dayEnd = new Date(combineDayKeyAndTime(date, '23:59', timeZone).getTime() + 59_999)\n timeOff.push({\n type: dayException.type,\n end: dayEnd.toISOString(),\n reason: dayException.reason,\n start: dayStart.toISOString(),\n })\n } else {\n for (const sched of schedules as Array<Record<string, unknown>>) {\n // resolveScheduleForDate accepts a Schedule-shaped object; cast through unknown\n const ranges = resolveScheduleForDate(\n sched as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n timeZone,\n )\n for (const r of ranges) {\n shiftWindows.push({ end: r.end.toISOString(), start: r.start.toISOString() })\n }\n }\n }\n\n days.push({ date, shiftWindows, timeOff })\n }\n\n const busy = await busyFor({\n blockingStatuses,\n capacityMode,\n end,\n payload,\n reservationSlug,\n resourceId,\n start,\n })\n\n // Resources this resource's services ALSO require (e.g. a shared chair pool).\n // A slot isn't truly bookable if any of these is at capacity, even when the\n // resource itself is free — so the calendar reflects real availability.\n const poolIds = new Set<string>()\n for (const svc of (resource?.services as Array<Record<string, unknown>>) ?? []) {\n const reqs = (typeof svc === 'object' ? (svc.requiredResources as unknown[]) : []) ?? []\n for (const rr of reqs) {\n const id: number | string | undefined =\n typeof rr === 'object' && rr !== null\n ? (rr as { id?: number | string }).id\n : (rr as number | string)\n if (id != null && String(id) !== String(resourceId)) {\n poolIds.add(String(id))\n }\n }\n }\n\n const requiredPools: ResourceAvailability['requiredPools'] = []\n for (const poolId of poolIds) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const pool = await (payload.findByID as any)({\n id: poolId,\n collection: resourceSlug,\n depth: 0,\n }).catch(() => null)\n if (!pool) {\n continue\n }\n const poolCapacityMode =\n (pool.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n requiredPools.push({\n busy: await busyFor({\n blockingStatuses,\n capacityMode: poolCapacityMode,\n end,\n payload,\n reservationSlug,\n resourceId: poolId,\n start,\n }),\n quantity: (pool.quantity as number) ?? 1,\n })\n }\n\n // External busy — display only (enforcement lives in checkAvailability).\n // Fail-open: a resolver error must never break the grid.\n let external: ExternalBusyInterval[] = []\n if (getExternalBusy && req) {\n try {\n external = (await getExternalBusy({ end, req, resourceId, start })).filter(\n (iv) => !isNaN(Date.parse(iv.start)) && !isNaN(Date.parse(iv.end)),\n )\n } catch {\n external = []\n }\n }\n\n return { busy, capacityMode, days, external, quantity, requiredPools, timeZone }\n}\n\nexport function createResourceAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n // The grid data (every reservation's busy window) is staff/admin-only —\n // same gate as customer search.\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n if (!isPrivilegedUser(req.user, config)) {\n return Response.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const url = new URL(req.url!)\n const resource = url.searchParams.get('resource')\n const start = url.searchParams.get('start')\n const end = url.searchParams.get('end')\n\n if (!resource || !start || !end) {\n return Response.json(\n { error: 'Missing required query params: resource, start, end' },\n { status: 400 },\n )\n }\n\n const startDate = new Date(start)\n const endDate = new Date(end)\n if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {\n return Response.json({ error: 'Invalid start/end date' }, { status: 400 })\n }\n if (endDate <= startDate) {\n return Response.json({ error: 'end must be after start' }, { status: 400 })\n }\n // Unbounded ranges turn the per-day resolution loop into a CPU sink\n if (endDate.getTime() - startDate.getTime() > MAX_RANGE_MS) {\n return Response.json({ error: 'Date range too large (max 90 days)' }, { status: 400 })\n }\n\n // In multiTenant mode, resolve day-boundaries in the SELECTED tenant's zone\n // (tenant timezone → global → UTC). Degrades to the global zone for plain\n // installs: no tenant relationship on reservations / no tenant cookie ⇒ no\n // DB read, same output as before.\n const reservationsCollection = req.payload.config.collections?.find(\n (c) => c.slug === config.slugs.reservations,\n )\n const timeZone = await getEffectiveTenantTimezone({\n globalTimezone: config.timezone,\n payload: req.payload,\n scopedCollection: reservationsCollection as { fields?: unknown[] } | undefined,\n tenantField: config.multiTenant.tenantField,\n tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),\n timezoneField: config.multiTenant.timezoneField,\n })\n\n const result = await buildResourceAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n end: endDate,\n getExternalBusy: config.getExternalBusy,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: resource,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n start: startDate,\n timeZone,\n })\n\n if (!result) {\n return Response.json({ error: 'Resource not found' }, { status: 404 })\n }\n\n return Response.json(result)\n },\n method: 'get',\n path: '/reserve/resource-availability',\n }\n}\n"],"names":["resolveScheduleForDate","readCookie","getEffectiveTenantTimezone","addDaysToDayKey","combineDayKeyAndTime","getDayKeyInTimezone","isPrivilegedUser","MAX_RANGE_MS","busyFor","args","blockingStatuses","capacityMode","end","payload","reservationSlug","resourceId","start","where","and","status","in","startTime","less_than","toISOString","endTime","greater_than","or","resource","equals","docs","find","collection","depth","limit","filter","r","map","Date","units","guestCount","buildResourceAvailability","params","getExternalBusy","req","resourceSlug","scheduleSlug","timeZone","findByID","id","catch","quantity","schedules","active","days","startKey","lastKey","getTime","date","shiftWindows","timeOff","dayException","sched","exceptions","exc","excStart","excEnd","endDate","dayStart","dayEnd","push","type","reason","ranges","busy","poolIds","Set","svc","services","reqs","requiredResources","rr","String","add","requiredPools","poolId","pool","poolCapacityMode","external","iv","isNaN","parse","createResourceAvailabilityEndpoint","config","handler","user","Response","json","error","url","URL","searchParams","get","startDate","reservationsCollection","collections","c","slug","slugs","reservations","globalTimezone","timezone","scopedCollection","tenantField","multiTenant","tenantId","headers","cookieName","timezoneField","result","statusMachine","resources","method","path"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/endpoints/resourceAvailability.ts"],"sourcesContent":["import type { Endpoint, Payload, PayloadRequest, Where } from 'payload'\n\nimport type {\n ExternalBusyInterval,\n GetExternalBusy,\n ResolvedReservationPluginConfig,\n} from '../types.js'\n\nimport {\n createReserveDebug,\n NOOP_RESERVE_DEBUG,\n type ReserveDebug,\n} from '../utilities/reserveDebug.js'\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { readCookie } from '../utilities/tenantFilter.js'\nimport { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js'\nimport {\n addDaysToDayKey,\n combineDayKeyAndTime,\n getDayKeyInTimezone,\n} from '../utilities/timezoneUtils.js'\nimport { isPrivilegedUser } from '../utilities/userRoles.js'\n\nconst MAX_RANGE_MS = 90 * 86_400_000\n\ntype DayAvailability = {\n date: string\n shiftWindows: Array<{ end: string; start: string }>\n timeOff: Array<{ end: string; reason?: string; start: string; type?: string }>\n}\n\ntype Busy = Array<{ end: string; start: string; units: number }>\n\nexport type ResourceAvailability = {\n busy: Busy\n capacityMode: 'per-guest' | 'per-reservation'\n days: DayAvailability[]\n /** External busy intervals (calendar sync etc.) from getExternalBusy — display-only here; enforcement lives in checkAvailability. */\n external: ExternalBusyInterval[]\n quantity: number\n /** Capacity of resources this resource's services also require (e.g. a chair pool). */\n requiredPools: Array<{ busy: Busy; quantity: number }>\n /** IANA zone the day windows were resolved in (selected tenant's zone, else global). */\n timeZone: string\n}\n\n/** Busy intervals (with capacity units) for one resource over [start, end). */\nasync function busyFor(args: {\n blockingStatuses: string[]\n capacityMode: 'per-guest' | 'per-reservation'\n end: Date\n payload: Payload\n reservationSlug: string\n resourceId: number | string\n start: Date\n}): Promise<Busy> {\n const { blockingStatuses, capacityMode, end, payload, reservationSlug, resourceId, start } = args\n const where: Where = {\n and: [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: end.toISOString() } },\n { endTime: { greater_than: start.toISOString() } },\n { or: [{ resource: { equals: resourceId } }, { 'items.resource': { equals: resourceId } }] },\n ],\n }\n // limit:0 = all matching — bounded by the endpoint's 90-day range cap, so this\n // can't run away, and the grid no longer silently drops busy intervals (D9).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({\n collection: reservationSlug,\n depth: 0,\n limit: 0,\n where,\n })\n return (docs as Array<Record<string, unknown>>)\n .filter((r) => r.startTime && r.endTime)\n .map((r) => ({\n end: new Date(r.endTime as string).toISOString(),\n start: new Date(r.startTime as string).toISOString(),\n units: capacityMode === 'per-guest' ? ((r.guestCount as number) ?? 1) : 1,\n }))\n}\n\nexport async function buildResourceAvailability(params: {\n blockingStatuses: string[]\n debug?: ReserveDebug\n end: Date\n getExternalBusy?: GetExternalBusy\n payload: Payload\n req?: PayloadRequest\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n scheduleSlug: string\n start: Date\n timeZone: string\n}): Promise<null | ResourceAvailability> {\n const {\n blockingStatuses,\n debug,\n end,\n getExternalBusy,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n scheduleSlug,\n start,\n timeZone,\n } = params\n const trace = debug ?? NOOP_RESERVE_DEBUG\n\n // depth 1 so `services` are populated (their `requiredResources` come back as ids)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: resourceSlug,\n depth: 1,\n }).catch(() => null)\n if (!resource) {\n return null\n }\n const quantity = (resource?.quantity as number) ?? 1\n const capacityMode =\n (resource?.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: schedules } = await (payload.find as any)({\n collection: scheduleSlug,\n depth: 0,\n limit: 100,\n where: { and: [{ active: { equals: true } }, { resource: { equals: resourceId } }] },\n })\n\n type RawException = {\n date: string\n endDate?: string\n reason?: string\n type?: string\n }\n\n const days: DayAvailability[] = []\n const startKey = getDayKeyInTimezone(start, timeZone)\n const lastKey = getDayKeyInTimezone(new Date(end.getTime() - 1), timeZone)\n for (let date = startKey; date <= lastKey; date = addDaysToDayKey(date, 1)) {\n const shiftWindows: DayAvailability['shiftWindows'] = []\n const timeOff: DayAvailability['timeOff'] = []\n\n // A11: an exception on ANY of the resource's schedules makes the whole\n // resource unavailable that day. Find the first matching exception (if any)\n // — it suppresses all shift windows and marks the full day as time-off.\n let dayException: RawException | undefined\n for (const sched of schedules as Array<Record<string, unknown>>) {\n const exceptions = (sched.exceptions as RawException[] | undefined) ?? []\n for (const exc of exceptions) {\n const excStart = getDayKeyInTimezone(new Date(exc.date), timeZone)\n const excEnd = exc.endDate ? getDayKeyInTimezone(new Date(exc.endDate), timeZone) : excStart\n if (date >= excStart && date <= excEnd) {\n dayException = exc\n break\n }\n }\n if (dayException) {\n break\n }\n }\n\n if (dayException) {\n const dayStart = combineDayKeyAndTime(date, '00:00', timeZone)\n const dayEnd = new Date(combineDayKeyAndTime(date, '23:59', timeZone).getTime() + 59_999)\n timeOff.push({\n type: dayException.type,\n end: dayEnd.toISOString(),\n reason: dayException.reason,\n start: dayStart.toISOString(),\n })\n } else {\n for (const sched of schedules as Array<Record<string, unknown>>) {\n // resolveScheduleForDate accepts a Schedule-shaped object; cast through unknown\n const ranges = resolveScheduleForDate(\n sched as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n timeZone,\n )\n for (const r of ranges) {\n shiftWindows.push({ end: r.end.toISOString(), start: r.start.toISOString() })\n }\n }\n }\n\n days.push({ date, shiftWindows, timeOff })\n }\n\n const busy = await busyFor({\n blockingStatuses,\n capacityMode,\n end,\n payload,\n reservationSlug,\n resourceId,\n start,\n })\n\n // Resources this resource's services ALSO require (e.g. a shared chair pool).\n // A slot isn't truly bookable if any of these is at capacity, even when the\n // resource itself is free — so the calendar reflects real availability.\n const poolIds = new Set<string>()\n for (const svc of (resource?.services as Array<Record<string, unknown>>) ?? []) {\n const reqs = (typeof svc === 'object' ? (svc.requiredResources as unknown[]) : []) ?? []\n for (const rr of reqs) {\n const id: number | string | undefined =\n typeof rr === 'object' && rr !== null\n ? (rr as { id?: number | string }).id\n : (rr as number | string)\n if (id != null && String(id) !== String(resourceId)) {\n poolIds.add(String(id))\n }\n }\n }\n\n const requiredPools: ResourceAvailability['requiredPools'] = []\n for (const poolId of poolIds) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const pool = await (payload.findByID as any)({\n id: poolId,\n collection: resourceSlug,\n depth: 0,\n }).catch(() => null)\n if (!pool) {\n continue\n }\n const poolCapacityMode =\n (pool.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n requiredPools.push({\n busy: await busyFor({\n blockingStatuses,\n capacityMode: poolCapacityMode,\n end,\n payload,\n reservationSlug,\n resourceId: poolId,\n start,\n }),\n quantity: (pool.quantity as number) ?? 1,\n })\n }\n\n // External busy — display only (enforcement lives in checkAvailability).\n // Fail-open: a resolver error must never break the grid.\n let external: ExternalBusyInterval[] = []\n if (getExternalBusy && req) {\n try {\n external = (await getExternalBusy({ end, req, resourceId, start })).filter(\n (iv) => !isNaN(Date.parse(iv.start)) && !isNaN(Date.parse(iv.end)),\n )\n } catch (err) {\n trace.dbg('error', { err, resourceId, where: 'getExternalBusy' })\n external = []\n }\n }\n\n return { busy, capacityMode, days, external, quantity, requiredPools, timeZone }\n}\n\nexport function createResourceAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n // The grid data (every reservation's busy window) is staff/admin-only —\n // same gate as customer search.\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n if (!isPrivilegedUser(req.user, config)) {\n return Response.json({ error: 'Forbidden' }, { status: 403 })\n }\n const dbg = createReserveDebug(req.payload.logger, config.debug)\n\n const url = new URL(req.url!)\n const resource = url.searchParams.get('resource')\n const start = url.searchParams.get('start')\n const end = url.searchParams.get('end')\n\n if (!resource || !start || !end) {\n return Response.json(\n { error: 'Missing required query params: resource, start, end' },\n { status: 400 },\n )\n }\n\n const startDate = new Date(start)\n const endDate = new Date(end)\n if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {\n return Response.json({ error: 'Invalid start/end date' }, { status: 400 })\n }\n if (endDate <= startDate) {\n return Response.json({ error: 'end must be after start' }, { status: 400 })\n }\n // Unbounded ranges turn the per-day resolution loop into a CPU sink\n if (endDate.getTime() - startDate.getTime() > MAX_RANGE_MS) {\n return Response.json({ error: 'Date range too large (max 90 days)' }, { status: 400 })\n }\n\n // In multiTenant mode, resolve day-boundaries in the SELECTED tenant's zone\n // (tenant timezone → global → UTC). Degrades to the global zone for plain\n // installs: no tenant relationship on reservations / no tenant cookie ⇒ no\n // DB read, same output as before.\n const reservationsCollection = req.payload.config.collections?.find(\n (c) => c.slug === config.slugs.reservations,\n )\n const timeZone = await getEffectiveTenantTimezone({\n globalTimezone: config.timezone,\n payload: req.payload,\n scopedCollection: reservationsCollection as { fields?: unknown[] } | undefined,\n tenantField: config.multiTenant.tenantField,\n tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),\n timezoneField: config.multiTenant.timezoneField,\n })\n\n dbg.dbg('request', { end, endpoint: 'resource-availability', resource, start, timeZone })\n\n const result = await buildResourceAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n debug: dbg,\n end: endDate,\n getExternalBusy: config.getExternalBusy,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: resource,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n start: startDate,\n timeZone,\n })\n\n if (!result) {\n dbg.dbg('response', { endpoint: 'resource-availability', reason: 'resource_not_found' })\n return Response.json({ error: 'Resource not found' }, { status: 404 })\n }\n\n dbg.dbg('response', {\n dayCount: result.days.length,\n endpoint: 'resource-availability',\n externalCount: result.external.length,\n })\n return Response.json(result)\n },\n method: 'get',\n path: '/reserve/resource-availability',\n }\n}\n"],"names":["createReserveDebug","NOOP_RESERVE_DEBUG","resolveScheduleForDate","readCookie","getEffectiveTenantTimezone","addDaysToDayKey","combineDayKeyAndTime","getDayKeyInTimezone","isPrivilegedUser","MAX_RANGE_MS","busyFor","args","blockingStatuses","capacityMode","end","payload","reservationSlug","resourceId","start","where","and","status","in","startTime","less_than","toISOString","endTime","greater_than","or","resource","equals","docs","find","collection","depth","limit","filter","r","map","Date","units","guestCount","buildResourceAvailability","params","debug","getExternalBusy","req","resourceSlug","scheduleSlug","timeZone","trace","findByID","id","catch","quantity","schedules","active","days","startKey","lastKey","getTime","date","shiftWindows","timeOff","dayException","sched","exceptions","exc","excStart","excEnd","endDate","dayStart","dayEnd","push","type","reason","ranges","busy","poolIds","Set","svc","services","reqs","requiredResources","rr","String","add","requiredPools","poolId","pool","poolCapacityMode","external","iv","isNaN","parse","err","dbg","createResourceAvailabilityEndpoint","config","handler","user","Response","json","error","logger","url","URL","searchParams","get","startDate","reservationsCollection","collections","c","slug","slugs","reservations","globalTimezone","timezone","scopedCollection","tenantField","multiTenant","tenantId","headers","cookieName","timezoneField","endpoint","result","statusMachine","resources","dayCount","length","externalCount","method","path"],"mappings":"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"}
@@ -1,11 +1,13 @@
1
1
  import { ValidationError } from 'payload';
2
2
  import { checkAvailability } from '../../services/AvailabilityService.js';
3
3
  import { mergeReservationData, schedulingFieldsChanged } from '../../utilities/reservationChanges.js';
4
+ import { createReserveDebug } from '../../utilities/reserveDebug.js';
4
5
  import { extractId, resolveReservationItems } from '../../utilities/resolveReservationItems.js';
5
6
  export const validateConflicts = (config)=>async ({ context, data, operation, originalDoc, req })=>{
6
7
  if (context?.skipReservationHooks) {
7
8
  return data;
8
9
  }
10
+ const dbg = createReserveDebug(req.payload.logger, config.debug);
9
11
  const isUpdate = operation === 'update';
10
12
  // Skip when an update touches no scheduling-relevant field, so reservations
11
13
  // booked under older buffers/schedules can still take benign edits.
@@ -55,6 +57,7 @@ export const validateConflicts = (config)=>async ({ context, data, operation, or
55
57
  blockingStatuses: config.statusMachine.blockingStatuses,
56
58
  bufferAfter,
57
59
  bufferBefore,
60
+ debug: dbg,
58
61
  endTime: new Date(item.endTime),
59
62
  excludeReservationId: isUpdate ? originalDoc?.id : undefined,
60
63
  getExternalBusy: config.getExternalBusy,
@@ -68,7 +71,19 @@ export const validateConflicts = (config)=>async ({ context, data, operation, or
68
71
  siblingItems,
69
72
  startTime: new Date(item.startTime)
70
73
  });
74
+ dbg.dbg('conflict_check', {
75
+ available: result.available,
76
+ index: i,
77
+ resourceId: item.resource,
78
+ startTime: new Date(item.startTime).toISOString()
79
+ });
71
80
  if (!result.available) {
81
+ dbg.dbg('conflict_reject', {
82
+ index: i,
83
+ reason: result.reason,
84
+ resourceId: item.resource,
85
+ startTime: new Date(item.startTime).toISOString()
86
+ });
72
87
  throw new ValidationError({
73
88
  errors: [
74
89
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/reservations/validateConflicts.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { checkAvailability } from '../../services/AvailabilityService.js'\nimport {\n mergeReservationData,\n schedulingFieldsChanged,\n} from '../../utilities/reservationChanges.js'\nimport { extractId, resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const validateConflicts =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {\n return data\n }\n\n const isUpdate = operation === 'update'\n\n // Skip when an update touches no scheduling-relevant field, so reservations\n // booked under older buffers/schedules can still take benign edits.\n if (\n isUpdate &&\n !schedulingFieldsChanged({\n blockingStatuses: config.statusMachine.blockingStatuses,\n data: data as Record<string, unknown>,\n originalDoc: originalDoc as Record<string, unknown> | undefined,\n })\n ) {\n return data\n }\n\n // Validate the merged document — Payload usually backfills update patches\n // from originalDoc before beforeChange, but the hook must not rely on it.\n const source = isUpdate\n ? mergeReservationData(\n data as Record<string, unknown>,\n originalDoc as Record<string, unknown> | undefined,\n )\n : (data as Record<string, unknown>)\n\n const items = resolveReservationItems(source)\n\n if (items.length === 0) {\n return data\n }\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n if (!item.endTime) {\n continue\n }\n\n // Fetch buffer times from the item's own service (not just the primary)\n const itemServiceId = item.service ?? extractId(source.service)\n let bufferBefore = config.defaultBufferTime\n let bufferAfter = config.defaultBufferTime\n\n if (itemServiceId) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: itemServiceId,\n collection: config.slugs.services,\n req,\n })\n if (service) {\n bufferBefore = (service.bufferTimeBefore as number) ?? config.defaultBufferTime\n bufferAfter = (service.bufferTimeAfter as number) ?? config.defaultBufferTime\n }\n } catch {\n // Use defaults if service lookup fails\n }\n }\n\n // Other items of this same booking on the same resource count as occupancy\n // so two items can't double-book one resource within one create (review A5).\n const siblingItems = items.filter((other, j) => j !== i && other.resource === item.resource)\n\n const result = await checkAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime: new Date(item.endTime),\n excludeReservationId: isUpdate ? originalDoc?.id : undefined,\n getExternalBusy: config.getExternalBusy,\n guestCount: item.guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: item.resource,\n resourceSlug: config.slugs.resources,\n servicesSlug: config.slugs.services,\n siblingItems,\n startTime: new Date(item.startTime),\n })\n\n if (!result.available) {\n throw new ValidationError({\n errors: [\n {\n message: result.reason ?? (req.t as PluginT)('reservation:errorConflict'),\n path: items.length > 1 ? `items.${i}.startTime` : 'startTime',\n },\n ],\n })\n }\n }\n\n return data\n }\n"],"names":["ValidationError","checkAvailability","mergeReservationData","schedulingFieldsChanged","extractId","resolveReservationItems","validateConflicts","config","context","data","operation","originalDoc","req","skipReservationHooks","isUpdate","blockingStatuses","statusMachine","source","items","length","i","item","endTime","itemServiceId","service","bufferBefore","defaultBufferTime","bufferAfter","payload","findByID","id","collection","slugs","services","bufferTimeBefore","bufferTimeAfter","siblingItems","filter","other","j","resource","result","Date","excludeReservationId","undefined","getExternalBusy","guestCount","reservationSlug","reservations","resourceId","resourceSlug","resources","servicesSlug","startTime","available","errors","message","reason","t","path"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/hooks/reservations/validateConflicts.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { checkAvailability } from '../../services/AvailabilityService.js'\nimport {\n mergeReservationData,\n schedulingFieldsChanged,\n} from '../../utilities/reservationChanges.js'\nimport { createReserveDebug } from '../../utilities/reserveDebug.js'\nimport { extractId, resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const validateConflicts =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {\n return data\n }\n\n const dbg = createReserveDebug(req.payload.logger, config.debug)\n\n const isUpdate = operation === 'update'\n\n // Skip when an update touches no scheduling-relevant field, so reservations\n // booked under older buffers/schedules can still take benign edits.\n if (\n isUpdate &&\n !schedulingFieldsChanged({\n blockingStatuses: config.statusMachine.blockingStatuses,\n data: data as Record<string, unknown>,\n originalDoc: originalDoc as Record<string, unknown> | undefined,\n })\n ) {\n return data\n }\n\n // Validate the merged document — Payload usually backfills update patches\n // from originalDoc before beforeChange, but the hook must not rely on it.\n const source = isUpdate\n ? mergeReservationData(\n data as Record<string, unknown>,\n originalDoc as Record<string, unknown> | undefined,\n )\n : (data as Record<string, unknown>)\n\n const items = resolveReservationItems(source)\n\n if (items.length === 0) {\n return data\n }\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n if (!item.endTime) {\n continue\n }\n\n // Fetch buffer times from the item's own service (not just the primary)\n const itemServiceId = item.service ?? extractId(source.service)\n let bufferBefore = config.defaultBufferTime\n let bufferAfter = config.defaultBufferTime\n\n if (itemServiceId) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: itemServiceId,\n collection: config.slugs.services,\n req,\n })\n if (service) {\n bufferBefore = (service.bufferTimeBefore as number) ?? config.defaultBufferTime\n bufferAfter = (service.bufferTimeAfter as number) ?? config.defaultBufferTime\n }\n } catch {\n // Use defaults if service lookup fails\n }\n }\n\n // Other items of this same booking on the same resource count as occupancy\n // so two items can't double-book one resource within one create (review A5).\n const siblingItems = items.filter((other, j) => j !== i && other.resource === item.resource)\n\n const result = await checkAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n bufferAfter,\n bufferBefore,\n debug: dbg,\n endTime: new Date(item.endTime),\n excludeReservationId: isUpdate ? originalDoc?.id : undefined,\n getExternalBusy: config.getExternalBusy,\n guestCount: item.guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: item.resource,\n resourceSlug: config.slugs.resources,\n servicesSlug: config.slugs.services,\n siblingItems,\n startTime: new Date(item.startTime),\n })\n\n dbg.dbg('conflict_check', {\n available: result.available,\n index: i,\n resourceId: item.resource,\n startTime: new Date(item.startTime).toISOString(),\n })\n\n if (!result.available) {\n dbg.dbg('conflict_reject', {\n index: i,\n reason: result.reason,\n resourceId: item.resource,\n startTime: new Date(item.startTime).toISOString(),\n })\n\n throw new ValidationError({\n errors: [\n {\n message: result.reason ?? (req.t as PluginT)('reservation:errorConflict'),\n path: items.length > 1 ? `items.${i}.startTime` : 'startTime',\n },\n ],\n })\n }\n }\n\n return data\n }\n"],"names":["ValidationError","checkAvailability","mergeReservationData","schedulingFieldsChanged","createReserveDebug","extractId","resolveReservationItems","validateConflicts","config","context","data","operation","originalDoc","req","skipReservationHooks","dbg","payload","logger","debug","isUpdate","blockingStatuses","statusMachine","source","items","length","i","item","endTime","itemServiceId","service","bufferBefore","defaultBufferTime","bufferAfter","findByID","id","collection","slugs","services","bufferTimeBefore","bufferTimeAfter","siblingItems","filter","other","j","resource","result","Date","excludeReservationId","undefined","getExternalBusy","guestCount","reservationSlug","reservations","resourceId","resourceSlug","resources","servicesSlug","startTime","available","index","toISOString","reason","errors","message","t","path"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import type { Payload, PayloadRequest, Where } from 'payload';
2
2
  import type { CapacityMode, DurationType, GetExternalBusy, StatusMachineConfig } from '../types.js';
3
3
  import type { ResolvedItem } from '../utilities/resolveReservationItems.js';
4
+ import { type ReserveDebug } from '../utilities/reserveDebug.js';
4
5
  /** A window during which a resource is occupied. Reservation/sibling
5
6
  * occupancies are expanded by buffer times; external intervals (from
6
7
  * `getExternalBusy`) are used as-is — only the candidate window itself
@@ -76,6 +77,8 @@ export declare function checkAvailability(params: {
76
77
  blockingStatuses: string[];
77
78
  bufferAfter: number;
78
79
  bufferBefore: number;
80
+ /** Optional tracer — emits check/check_result/error lines when enabled. */
81
+ debug?: ReserveDebug;
79
82
  endTime: Date;
80
83
  excludeReservationId?: number | string;
81
84
  /** External busy resolver (calendar sync etc.) — intervals block the whole resource. */
@@ -99,6 +102,8 @@ export declare function checkAvailability(params: {
99
102
  export declare function getAvailableSlots(params: {
100
103
  blockingStatuses: string[];
101
104
  date: Date | string;
105
+ /** Optional tracer — emits per-stage slot-generation lines when enabled. */
106
+ debug?: ReserveDebug;
102
107
  /** External busy resolver (calendar sync etc.) — intervals block the whole resource. */
103
108
  getExternalBusy?: GetExternalBusy;
104
109
  guestCount?: number;