payload-reserve 1.1.0 → 1.2.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 CHANGED
@@ -12,7 +12,7 @@ Designed for salons, clinics, hotels, restaurants, event venues, and any busines
12
12
  - **User Collection Extension** — Optionally extend your existing auth collection with booking fields; set `userCollection: undefined` (default) to use a standalone Customers collection
13
13
  - **Resource Owner Multi-Tenancy** — Opt-in `resourceOwnerMode` wires ownership access control so each resource owner (host) sees only their own listings and reservations
14
14
  - **Configurable Status Machine** — Define your own statuses, transitions, blocking states, and terminal states
15
- - **Double-Booking Prevention** — Server-side conflict detection with configurable buffer times; respects capacity modes
15
+ - **Double-Booking Prevention** — Server-side conflict detection with per-item buffer times; respects capacity modes
16
16
  - **Auto End Time** — Calculates `endTime` from `startTime + service.duration` automatically
17
17
  - **Three Duration Types** — `fixed` (service duration), `flexible` (customer-specified end), and `full-day` bookings
18
18
  - **Multi-Resource Bookings** — Single reservation that spans multiple resources simultaneously via the `items` array
@@ -21,8 +21,8 @@ Designed for salons, clinics, hotels, restaurants, event venues, and any busines
21
21
  - **Extra Reservation Fields** — Inject custom fields into the Reservations collection via `extraReservationFields` without forking the plugin
22
22
  - **Cancellation Policy** — Configurable minimum notice period enforcement
23
23
  - **Plugin Hooks API** — Seven lifecycle hooks (`beforeBookingCreate`, `afterBookingCreate`, `beforeBookingConfirm`, `afterBookingConfirm`, `beforeBookingCancel`, `afterBookingCancel`, `afterStatusChange`) for integrating email, Stripe, and external systems
24
- - **Availability Service** — Pure functions and DB helpers for slot generation and conflict checking
25
- - **Public REST API** — Five pre-built endpoints for availability, slot listing, booking, cancellation, and customer search
24
+ - **Availability Service** — Pure functions and DB helpers for slot generation (15-min step) and conflict checking with guest-count-aware filtering
25
+ - **Public REST API** — Five pre-built endpoints for availability, slot listing, booking, cancellation, and customer search — with ownership enforcement and input validation
26
26
  - **Calendar View** — Month/week/day calendar replacing the default reservations list view
27
27
  - **Dashboard Widget** — Server component showing today's booking stats
28
28
  - **Availability Overview** — Weekly grid of resource availability vs. booked slots
@@ -105,3 +105,29 @@ The `access` override in plugin config always takes precedence over the auto-wir
105
105
  | [Examples](https://github.com/elghaied/payload-reserve/blob/main/docs/examples.md) | Salon, hotel, restaurant, event venue, Stripe, email, multi-tenant (resource owner mode) |
106
106
  | [Advanced](https://github.com/elghaied/payload-reserve/blob/main/docs/advanced.md) | DB indexes, reconciliation job for race condition detection |
107
107
  | [Development](https://github.com/elghaied/payload-reserve/blob/main/docs/development.md) | Prerequisites, commands, project file tree |
108
+ | [v1.2.0 Breaking Changes](https://github.com/elghaied/payload-reserve/blob/main/docs/BREAKING-CHANGES-v1.2.md) | Migration guide for upgrading to v1.2.0 |
109
+
110
+ ---
111
+
112
+ ## Contributing
113
+
114
+ This project uses [Changesets](https://github.com/changesets/changesets) for versioning and changelogs.
115
+
116
+ When making a change that should appear in the release notes, run:
117
+
118
+ ```bash
119
+ pnpm changeset
120
+ ```
121
+
122
+ This prompts for the semver bump type (patch/minor/major) and a summary. Commit the generated changeset file with your PR.
123
+
124
+ **Releasing:**
125
+
126
+ ```bash
127
+ pnpm changeset:version # consume changesets, bump version, update CHANGELOG.md
128
+ git add -A && git commit -m "release v<version>"
129
+ git tag v<version>
130
+ git push && git push --tags
131
+ ```
132
+
133
+ The GitHub Action will create a release with the changelog content and publish to npm.
@@ -1,4 +1,12 @@
1
+ import { ValidationError } from 'payload';
1
2
  import { makeScheduleOwnerAccess } from '../utilities/ownerAccess.js';
3
+ const TIME_REGEX = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
4
+ const validateTime = (value)=>{
5
+ if (!value) {
6
+ return true;
7
+ } // required handles emptiness
8
+ return TIME_REGEX.test(value) || 'Invalid time format. Use HH:mm (e.g., 09:00, 17:30)';
9
+ };
2
10
  export function createSchedulesCollection(config) {
3
11
  const rom = config.resourceOwnerMode;
4
12
  const access = config.access.schedules ?? (rom ? makeScheduleOwnerAccess(rom) : {});
@@ -89,7 +97,8 @@ export function createSchedulesCollection(config) {
89
97
  placeholder: '09:00'
90
98
  },
91
99
  label: ({ t })=>t('reservation:fieldStartTimeHHmm'),
92
- required: true
100
+ required: true,
101
+ validate: validateTime
93
102
  },
94
103
  {
95
104
  name: 'endTime',
@@ -98,7 +107,8 @@ export function createSchedulesCollection(config) {
98
107
  placeholder: '17:00'
99
108
  },
100
109
  label: ({ t })=>t('reservation:fieldEndTimeHHmm'),
101
- required: true
110
+ required: true,
111
+ validate: validateTime
102
112
  }
103
113
  ],
104
114
  label: ({ t })=>t('reservation:fieldRecurringSlots')
@@ -128,7 +138,8 @@ export function createSchedulesCollection(config) {
128
138
  placeholder: '09:00'
129
139
  },
130
140
  label: ({ t })=>t('reservation:fieldStartTimeHHmm'),
131
- required: true
141
+ required: true,
142
+ validate: validateTime
132
143
  },
133
144
  {
134
145
  name: 'endTime',
@@ -137,7 +148,8 @@ export function createSchedulesCollection(config) {
137
148
  placeholder: '17:00'
138
149
  },
139
150
  label: ({ t })=>t('reservation:fieldEndTimeHHmm'),
140
- required: true
151
+ required: true,
152
+ validate: validateTime
141
153
  }
142
154
  ],
143
155
  label: ({ t })=>t('reservation:fieldManualSlots')
@@ -175,6 +187,39 @@ export function createSchedulesCollection(config) {
175
187
  label: ({ t })=>t('reservation:fieldActive')
176
188
  }
177
189
  ],
190
+ hooks: {
191
+ beforeValidate: [
192
+ ({ data })=>{
193
+ const slots = data?.recurringSlots ?? [];
194
+ for (const slot of slots){
195
+ if (slot.startTime && slot.endTime && slot.startTime >= slot.endTime) {
196
+ throw new ValidationError({
197
+ errors: [
198
+ {
199
+ message: 'endTime must be after startTime',
200
+ path: 'recurringSlots'
201
+ }
202
+ ]
203
+ });
204
+ }
205
+ }
206
+ const manual = data?.manualSlots ?? [];
207
+ for (const slot of manual){
208
+ if (slot.startTime && slot.endTime && slot.startTime >= slot.endTime) {
209
+ throw new ValidationError({
210
+ errors: [
211
+ {
212
+ message: 'endTime must be after startTime',
213
+ path: 'manualSlots'
214
+ }
215
+ ]
216
+ });
217
+ }
218
+ }
219
+ return data;
220
+ }
221
+ ]
222
+ },
178
223
  labels: {
179
224
  plural: ({ t })=>t('reservation:collectionSchedules'),
180
225
  singular: ({ t })=>t('reservation:collectionSchedules')
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/collections/Schedules.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug } from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { makeScheduleOwnerAccess } from '../utilities/ownerAccess.js'\n\nexport function createSchedulesCollection(\n config: ResolvedReservationPluginConfig,\n): CollectionConfig {\n const rom = config.resourceOwnerMode\n const access =\n config.access.schedules ?? (rom ? makeScheduleOwnerAccess(rom) : {})\n\n return {\n slug: config.slugs.schedules,\n access,\n admin: {\n group: config.adminGroup,\n useAsTitle: 'name',\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldName'),\n required: true,\n },\n {\n name: 'resource',\n type: 'relationship',\n label: ({ t }) => (t as PluginT)('reservation:fieldResource'),\n relationTo: config.slugs.resources as unknown as CollectionSlug,\n required: true,\n },\n {\n name: 'scheduleType',\n type: 'select',\n defaultValue: 'recurring',\n label: ({ t }) => (t as PluginT)('reservation:fieldScheduleType'),\n options: [\n {\n label: ({ t }) => (t as PluginT)('reservation:scheduleTypeRecurring'),\n value: 'recurring',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:scheduleTypeManual'),\n value: 'manual',\n },\n ],\n },\n {\n name: 'recurringSlots',\n type: 'array',\n admin: {\n condition: (_, siblingData) => siblingData?.scheduleType === 'recurring',\n },\n fields: [\n {\n name: 'day',\n type: 'select',\n label: ({ t }) => (t as PluginT)('reservation:fieldDay'),\n options: [\n { label: ({ t }) => (t as PluginT)('reservation:dayMonday'), value: 'mon' },\n { label: ({ t }) => (t as PluginT)('reservation:dayTuesday'), value: 'tue' },\n { label: ({ t }) => (t as PluginT)('reservation:dayWednesday'), value: 'wed' },\n { label: ({ t }) => (t as PluginT)('reservation:dayThursday'), value: 'thu' },\n { label: ({ t }) => (t as PluginT)('reservation:dayFriday'), value: 'fri' },\n { label: ({ t }) => (t as PluginT)('reservation:daySaturday'), value: 'sat' },\n { label: ({ t }) => (t as PluginT)('reservation:daySunday'), value: 'sun' },\n ],\n required: true,\n },\n {\n name: 'startTime',\n type: 'text',\n admin: {\n placeholder: '09:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTimeHHmm'),\n required: true,\n },\n {\n name: 'endTime',\n type: 'text',\n admin: {\n placeholder: '17:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTimeHHmm'),\n required: true,\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldRecurringSlots'),\n },\n {\n name: 'manualSlots',\n type: 'array',\n admin: {\n condition: (_, siblingData) => siblingData?.scheduleType === 'manual',\n },\n fields: [\n {\n name: 'date',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayOnly',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldDate'),\n required: true,\n },\n {\n name: 'startTime',\n type: 'text',\n admin: {\n placeholder: '09:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTimeHHmm'),\n required: true,\n },\n {\n name: 'endTime',\n type: 'text',\n admin: {\n placeholder: '17:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTimeHHmm'),\n required: true,\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldManualSlots'),\n },\n {\n name: 'exceptions',\n type: 'array',\n fields: [\n {\n name: 'date',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayOnly',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldDate'),\n required: true,\n },\n {\n name: 'reason',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldReason'),\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldExceptions'),\n },\n {\n name: 'active',\n type: 'checkbox',\n admin: {\n position: 'sidebar',\n },\n defaultValue: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldActive'),\n },\n ],\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionSchedules'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionSchedules'),\n },\n }\n}\n"],"names":["makeScheduleOwnerAccess","createSchedulesCollection","config","rom","resourceOwnerMode","access","schedules","slug","slugs","admin","group","adminGroup","useAsTitle","fields","name","type","label","t","required","relationTo","resources","defaultValue","options","value","condition","_","siblingData","scheduleType","placeholder","date","pickerAppearance","position","labels","plural","singular"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/collections/Schedules.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { makeScheduleOwnerAccess } from '../utilities/ownerAccess.js'\n\nconst TIME_REGEX = /^(?:[01]\\d|2[0-3]):[0-5]\\d$/\n\nconst validateTime = (value: null | string | undefined): string | true => {\n if (!value) { return true } // required handles emptiness\n return TIME_REGEX.test(value) || 'Invalid time format. Use HH:mm (e.g., 09:00, 17:30)'\n}\n\nexport function createSchedulesCollection(\n config: ResolvedReservationPluginConfig,\n): CollectionConfig {\n const rom = config.resourceOwnerMode\n const access =\n config.access.schedules ?? (rom ? makeScheduleOwnerAccess(rom) : {})\n\n return {\n slug: config.slugs.schedules,\n access,\n admin: {\n group: config.adminGroup,\n useAsTitle: 'name',\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldName'),\n required: true,\n },\n {\n name: 'resource',\n type: 'relationship',\n label: ({ t }) => (t as PluginT)('reservation:fieldResource'),\n relationTo: config.slugs.resources as unknown as CollectionSlug,\n required: true,\n },\n {\n name: 'scheduleType',\n type: 'select',\n defaultValue: 'recurring',\n label: ({ t }) => (t as PluginT)('reservation:fieldScheduleType'),\n options: [\n {\n label: ({ t }) => (t as PluginT)('reservation:scheduleTypeRecurring'),\n value: 'recurring',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:scheduleTypeManual'),\n value: 'manual',\n },\n ],\n },\n {\n name: 'recurringSlots',\n type: 'array',\n admin: {\n condition: (_, siblingData) => siblingData?.scheduleType === 'recurring',\n },\n fields: [\n {\n name: 'day',\n type: 'select',\n label: ({ t }) => (t as PluginT)('reservation:fieldDay'),\n options: [\n { label: ({ t }) => (t as PluginT)('reservation:dayMonday'), value: 'mon' },\n { label: ({ t }) => (t as PluginT)('reservation:dayTuesday'), value: 'tue' },\n { label: ({ t }) => (t as PluginT)('reservation:dayWednesday'), value: 'wed' },\n { label: ({ t }) => (t as PluginT)('reservation:dayThursday'), value: 'thu' },\n { label: ({ t }) => (t as PluginT)('reservation:dayFriday'), value: 'fri' },\n { label: ({ t }) => (t as PluginT)('reservation:daySaturday'), value: 'sat' },\n { label: ({ t }) => (t as PluginT)('reservation:daySunday'), value: 'sun' },\n ],\n required: true,\n },\n {\n name: 'startTime',\n type: 'text',\n admin: {\n placeholder: '09:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTimeHHmm'),\n required: true,\n validate: validateTime,\n },\n {\n name: 'endTime',\n type: 'text',\n admin: {\n placeholder: '17:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTimeHHmm'),\n required: true,\n validate: validateTime,\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldRecurringSlots'),\n },\n {\n name: 'manualSlots',\n type: 'array',\n admin: {\n condition: (_, siblingData) => siblingData?.scheduleType === 'manual',\n },\n fields: [\n {\n name: 'date',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayOnly',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldDate'),\n required: true,\n },\n {\n name: 'startTime',\n type: 'text',\n admin: {\n placeholder: '09:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTimeHHmm'),\n required: true,\n validate: validateTime,\n },\n {\n name: 'endTime',\n type: 'text',\n admin: {\n placeholder: '17:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTimeHHmm'),\n required: true,\n validate: validateTime,\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldManualSlots'),\n },\n {\n name: 'exceptions',\n type: 'array',\n fields: [\n {\n name: 'date',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayOnly',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldDate'),\n required: true,\n },\n {\n name: 'reason',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldReason'),\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldExceptions'),\n },\n {\n name: 'active',\n type: 'checkbox',\n admin: {\n position: 'sidebar',\n },\n defaultValue: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldActive'),\n },\n ],\n hooks: {\n beforeValidate: [\n ({ data }) => {\n const slots = (data?.recurringSlots as Array<{ endTime?: string; startTime?: string }>) ?? []\n for (const slot of slots) {\n if (slot.startTime && slot.endTime && slot.startTime >= slot.endTime) {\n throw new ValidationError({\n errors: [{ message: 'endTime must be after startTime', path: 'recurringSlots' }],\n })\n }\n }\n const manual = (data?.manualSlots as Array<{ endTime?: string; startTime?: string }>) ?? []\n for (const slot of manual) {\n if (slot.startTime && slot.endTime && slot.startTime >= slot.endTime) {\n throw new ValidationError({\n errors: [{ message: 'endTime must be after startTime', path: 'manualSlots' }],\n })\n }\n }\n return data\n },\n ],\n },\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionSchedules'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionSchedules'),\n },\n }\n}\n"],"names":["ValidationError","makeScheduleOwnerAccess","TIME_REGEX","validateTime","value","test","createSchedulesCollection","config","rom","resourceOwnerMode","access","schedules","slug","slugs","admin","group","adminGroup","useAsTitle","fields","name","type","label","t","required","relationTo","resources","defaultValue","options","condition","_","siblingData","scheduleType","placeholder","validate","date","pickerAppearance","position","hooks","beforeValidate","data","slots","recurringSlots","slot","startTime","endTime","errors","message","path","manual","manualSlots","labels","plural","singular"],"mappings":"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"}
package/dist/defaults.js CHANGED
@@ -1,4 +1,29 @@
1
1
  import { DEFAULT_STATUS_MACHINE } from './types.js';
2
+ function validateStatusMachine(sm) {
3
+ if (!sm.statuses.includes(sm.defaultStatus)) {
4
+ throw new Error(`statusMachine.defaultStatus "${sm.defaultStatus}" is not in statuses array`);
5
+ }
6
+ for (const s of sm.blockingStatuses){
7
+ if (!sm.statuses.includes(s)) {
8
+ throw new Error(`statusMachine.blockingStatuses contains "${s}" which is not in statuses array`);
9
+ }
10
+ }
11
+ for (const s of sm.terminalStatuses){
12
+ if (!sm.statuses.includes(s)) {
13
+ throw new Error(`statusMachine.terminalStatuses contains "${s}" which is not in statuses array`);
14
+ }
15
+ }
16
+ for (const [from, targets] of Object.entries(sm.transitions)){
17
+ if (!sm.statuses.includes(from)) {
18
+ throw new Error(`statusMachine.transitions has key "${from}" which is not in statuses array`);
19
+ }
20
+ for (const to of targets){
21
+ if (!sm.statuses.includes(to)) {
22
+ throw new Error(`statusMachine.transitions["${from}"] targets "${to}" which is not in statuses array`);
23
+ }
24
+ }
25
+ }
26
+ }
2
27
  export const DEFAULT_SLUGS = {
3
28
  customers: 'customers',
4
29
  media: 'media',
@@ -13,7 +38,7 @@ export const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24;
13
38
  export function resolveConfig(pluginOptions) {
14
39
  const userStatusMachine = pluginOptions.statusMachine;
15
40
  const rom = pluginOptions.resourceOwnerMode;
16
- return {
41
+ const resolved = {
17
42
  access: pluginOptions.access ?? {},
18
43
  adminGroup: pluginOptions.adminGroup ?? DEFAULT_ADMIN_GROUP,
19
44
  cancellationNoticePeriod: pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,
@@ -46,6 +71,8 @@ export function resolveConfig(pluginOptions) {
46
71
  },
47
72
  userCollection: pluginOptions.userCollection ?? undefined
48
73
  };
74
+ validateStatusMachine(resolved.statusMachine);
75
+ return resolved;
49
76
  }
50
77
 
51
78
  //# sourceMappingURL=defaults.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type { ReservationPluginConfig, ResolvedReservationPluginConfig } from './types.js'\n\nimport { DEFAULT_STATUS_MACHINE } from './types.js'\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_BUFFER_TIME = 0\nexport const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24\n\nexport function resolveConfig(\n pluginOptions: ReservationPluginConfig,\n): ResolvedReservationPluginConfig {\n const userStatusMachine = pluginOptions.statusMachine\n const rom = pluginOptions.resourceOwnerMode\n return {\n access: pluginOptions.access ?? {},\n adminGroup: pluginOptions.adminGroup ?? DEFAULT_ADMIN_GROUP,\n cancellationNoticePeriod:\n pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,\n defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,\n disabled: pluginOptions.disabled ?? false,\n extraReservationFields: pluginOptions.extraReservationFields ?? [],\n hooks: pluginOptions.hooks ?? {},\n localized: false,\n resourceOwnerMode: rom\n ? {\n adminRoles: rom.adminRoles ?? [],\n ownedServices: rom.ownedServices ?? false,\n ownerField: rom.ownerField ?? 'owner',\n }\n : undefined,\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 statusMachine: userStatusMachine\n ? {\n blockingStatuses:\n userStatusMachine.blockingStatuses ?? DEFAULT_STATUS_MACHINE.blockingStatuses,\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 userCollection: pluginOptions.userCollection ?? undefined,\n }\n}\n"],"names":["DEFAULT_STATUS_MACHINE","DEFAULT_SLUGS","customers","media","reservations","resources","schedules","services","DEFAULT_ADMIN_GROUP","DEFAULT_BUFFER_TIME","DEFAULT_CANCELLATION_NOTICE_PERIOD","resolveConfig","pluginOptions","userStatusMachine","statusMachine","rom","resourceOwnerMode","access","adminGroup","cancellationNoticePeriod","defaultBufferTime","disabled","extraReservationFields","hooks","localized","adminRoles","ownedServices","ownerField","undefined","slugs","blockingStatuses","defaultStatus","statuses","terminalStatuses","transitions","userCollection"],"mappings":"AAEA,SAASA,sBAAsB,QAAQ,aAAY;AAEnD,OAAO,MAAMC,gBAAgB;IAC3BC,WAAW;IACXC,OAAO;IACPC,cAAc;IACdC,WAAW;IACXC,WAAW;IACXC,UAAU;AACZ,EAAU;AAEV,OAAO,MAAMC,sBAAsB,eAAc;AACjD,OAAO,MAAMC,sBAAsB,EAAC;AACpC,OAAO,MAAMC,qCAAqC,GAAE;AAEpD,OAAO,SAASC,cACdC,aAAsC;IAEtC,MAAMC,oBAAoBD,cAAcE,aAAa;IACrD,MAAMC,MAAMH,cAAcI,iBAAiB;IAC3C,OAAO;QACLC,QAAQL,cAAcK,MAAM,IAAI,CAAC;QACjCC,YAAYN,cAAcM,UAAU,IAAIV;QACxCW,0BACEP,cAAcO,wBAAwB,IAAIT;QAC5CU,mBAAmBR,cAAcQ,iBAAiB,IAAIX;QACtDY,UAAUT,cAAcS,QAAQ,IAAI;QACpCC,wBAAwBV,cAAcU,sBAAsB,IAAI,EAAE;QAClEC,OAAOX,cAAcW,KAAK,IAAI,CAAC;QAC/BC,WAAW;QACXR,mBAAmBD,MACf;YACEU,YAAYV,IAAIU,UAAU,IAAI,EAAE;YAChCC,eAAeX,IAAIW,aAAa,IAAI;YACpCC,YAAYZ,IAAIY,UAAU,IAAI;QAChC,IACAC;QACJC,OAAO;YACL3B,WAAWU,cAAciB,KAAK,EAAE3B,aAAaD,cAAcC,SAAS;YACpEC,OAAOS,cAAciB,KAAK,EAAE1B,SAASF,cAAcE,KAAK;YACxDC,cAAcQ,cAAciB,KAAK,EAAEzB,gBAAgBH,cAAcG,YAAY;YAC7EC,WAAWO,cAAciB,KAAK,EAAExB,aAAaJ,cAAcI,SAAS;YACpEC,WAAWM,cAAciB,KAAK,EAAEvB,aAAaL,cAAcK,SAAS;YACpEC,UAAUK,cAAciB,KAAK,EAAEtB,YAAYN,cAAcM,QAAQ;QACnE;QACAO,eAAeD,oBACX;YACEiB,kBACEjB,kBAAkBiB,gBAAgB,IAAI9B,uBAAuB8B,gBAAgB;YAC/EC,eAAelB,kBAAkBkB,aAAa,IAAI/B,uBAAuB+B,aAAa;YACtFC,UAAUnB,kBAAkBmB,QAAQ,IAAIhC,uBAAuBgC,QAAQ;YACvEC,kBACEpB,kBAAkBoB,gBAAgB,IAAIjC,uBAAuBiC,gBAAgB;YAC/EC,aAAarB,kBAAkBqB,WAAW,IAAIlC,uBAAuBkC,WAAW;QAClF,IACA;YAAE,GAAGlC,sBAAsB;QAAC;QAChCmC,gBAAgBvB,cAAcuB,cAAc,IAAIP;IAClD;AACF"}
1
+ {"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type { ReservationPluginConfig, ResolvedReservationPluginConfig, StatusMachineConfig } from './types.js'\n\nimport { DEFAULT_STATUS_MACHINE } from './types.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}\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_BUFFER_TIME = 0\nexport const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24\n\nexport function resolveConfig(\n pluginOptions: ReservationPluginConfig,\n): ResolvedReservationPluginConfig {\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 cancellationNoticePeriod:\n pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,\n defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,\n disabled: pluginOptions.disabled ?? false,\n extraReservationFields: pluginOptions.extraReservationFields ?? [],\n hooks: pluginOptions.hooks ?? {},\n localized: false,\n resourceOwnerMode: rom\n ? {\n adminRoles: rom.adminRoles ?? [],\n ownedServices: rom.ownedServices ?? false,\n ownerField: rom.ownerField ?? 'owner',\n }\n : undefined,\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 statusMachine: userStatusMachine\n ? {\n blockingStatuses:\n userStatusMachine.blockingStatuses ?? DEFAULT_STATUS_MACHINE.blockingStatuses,\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 userCollection: pluginOptions.userCollection ?? undefined,\n }\n\n validateStatusMachine(resolved.statusMachine)\n\n return resolved\n}\n"],"names":["DEFAULT_STATUS_MACHINE","validateStatusMachine","sm","statuses","includes","defaultStatus","Error","s","blockingStatuses","terminalStatuses","from","targets","Object","entries","transitions","to","DEFAULT_SLUGS","customers","media","reservations","resources","schedules","services","DEFAULT_ADMIN_GROUP","DEFAULT_BUFFER_TIME","DEFAULT_CANCELLATION_NOTICE_PERIOD","resolveConfig","pluginOptions","userStatusMachine","statusMachine","rom","resourceOwnerMode","resolved","access","adminGroup","cancellationNoticePeriod","defaultBufferTime","disabled","extraReservationFields","hooks","localized","adminRoles","ownedServices","ownerField","undefined","slugs","userCollection"],"mappings":"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"}
@@ -17,6 +17,26 @@ export function createCancelBookingEndpoint(config) {
17
17
  status: 400
18
18
  });
19
19
  }
20
+ // Fetch the reservation to check ownership
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ const existing = await req.payload.findByID({
23
+ id: reservationId,
24
+ collection: config.slugs.reservations,
25
+ depth: 0,
26
+ req
27
+ });
28
+ // Check ownership: customer must match req.user
29
+ const customerId = typeof existing.customer === 'object' ? existing.customer?.id : existing.customer;
30
+ const isOwner = customerId === req.user.id;
31
+ // Admin = user from non-customer collection
32
+ const isAdmin = req.user.collection !== config.slugs.customers;
33
+ if (!isOwner && !isAdmin) {
34
+ return Response.json({
35
+ message: 'Forbidden'
36
+ }, {
37
+ status: 403
38
+ });
39
+ }
20
40
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
41
  const reservation = await req.payload.update({
22
42
  id: reservationId,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/cancelBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nexport function createCancelBookingEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n if (!req.user) {\n return Response.json({ message: 'Unauthorized' }, { status: 401 })\n }\n\n const body = await req.json?.()\n const { reason, reservationId } = (body ?? {}) as {\n reason?: string\n reservationId?: string\n }\n\n if (!reservationId) {\n return Response.json({ message: 'reservationId is required' }, { status: 400 })\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const reservation = await (req.payload.update as any)({\n id: reservationId,\n collection: config.slugs.reservations,\n data: {\n cancellationReason: reason,\n status: 'cancelled',\n },\n req,\n })\n\n return Response.json(reservation)\n },\n method: 'post',\n path: '/reserve/cancel',\n }\n}\n"],"names":["createCancelBookingEndpoint","config","handler","req","user","Response","json","message","status","body","reason","reservationId","reservation","payload","update","id","collection","slugs","reservations","data","cancellationReason","method","path"],"mappings":"AAIA,OAAO,SAASA,4BAA4BC,MAAuC;IACjF,OAAO;QACLC,SAAS,OAAOC;YACd,IAAI,CAACA,IAAIC,IAAI,EAAE;gBACb,OAAOC,SAASC,IAAI,CAAC;oBAAEC,SAAS;gBAAe,GAAG;oBAAEC,QAAQ;gBAAI;YAClE;YAEA,MAAMC,OAAO,MAAMN,IAAIG,IAAI;YAC3B,MAAM,EAAEI,MAAM,EAAEC,aAAa,EAAE,GAAIF,QAAQ,CAAC;YAK5C,IAAI,CAACE,eAAe;gBAClB,OAAON,SAASC,IAAI,CAAC;oBAAEC,SAAS;gBAA4B,GAAG;oBAAEC,QAAQ;gBAAI;YAC/E;YAEA,8DAA8D;YAC9D,MAAMI,cAAc,MAAM,AAACT,IAAIU,OAAO,CAACC,MAAM,CAAS;gBACpDC,IAAIJ;gBACJK,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCC,MAAM;oBACJC,oBAAoBV;oBACpBF,QAAQ;gBACV;gBACAL;YACF;YAEA,OAAOE,SAASC,IAAI,CAACM;QACvB;QACAS,QAAQ;QACRC,MAAM;IACR;AACF"}
1
+ {"version":3,"sources":["../../src/endpoints/cancelBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nexport function createCancelBookingEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n if (!req.user) {\n return Response.json({ message: 'Unauthorized' }, { status: 401 })\n }\n\n const body = await req.json?.()\n const { reason, reservationId } = (body ?? {}) as {\n reason?: string\n reservationId?: string\n }\n\n if (!reservationId) {\n return Response.json({ message: 'reservationId is required' }, { status: 400 })\n }\n\n // Fetch the reservation to check ownership\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const existing = await (req.payload.findByID as any)({\n id: reservationId,\n collection: config.slugs.reservations,\n depth: 0,\n req,\n })\n\n // Check ownership: customer must match req.user\n const customerId =\n typeof existing.customer === 'object' ? existing.customer?.id : existing.customer\n const isOwner = customerId === req.user.id\n // Admin = user from non-customer collection\n const isAdmin = req.user.collection !== config.slugs.customers\n\n if (!isOwner && !isAdmin) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const reservation = await (req.payload.update as any)({\n id: reservationId,\n collection: config.slugs.reservations,\n data: {\n cancellationReason: reason,\n status: 'cancelled',\n },\n req,\n })\n\n return Response.json(reservation)\n },\n method: 'post',\n path: '/reserve/cancel',\n }\n}\n"],"names":["createCancelBookingEndpoint","config","handler","req","user","Response","json","message","status","body","reason","reservationId","existing","payload","findByID","id","collection","slugs","reservations","depth","customerId","customer","isOwner","isAdmin","customers","reservation","update","data","cancellationReason","method","path"],"mappings":"AAIA,OAAO,SAASA,4BAA4BC,MAAuC;IACjF,OAAO;QACLC,SAAS,OAAOC;YACd,IAAI,CAACA,IAAIC,IAAI,EAAE;gBACb,OAAOC,SAASC,IAAI,CAAC;oBAAEC,SAAS;gBAAe,GAAG;oBAAEC,QAAQ;gBAAI;YAClE;YAEA,MAAMC,OAAO,MAAMN,IAAIG,IAAI;YAC3B,MAAM,EAAEI,MAAM,EAAEC,aAAa,EAAE,GAAIF,QAAQ,CAAC;YAK5C,IAAI,CAACE,eAAe;gBAClB,OAAON,SAASC,IAAI,CAAC;oBAAEC,SAAS;gBAA4B,GAAG;oBAAEC,QAAQ;gBAAI;YAC/E;YAEA,2CAA2C;YAC3C,8DAA8D;YAC9D,MAAMI,WAAW,MAAM,AAACT,IAAIU,OAAO,CAACC,QAAQ,CAAS;gBACnDC,IAAIJ;gBACJK,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCC,OAAO;gBACPhB;YACF;YAEA,gDAAgD;YAChD,MAAMiB,aACJ,OAAOR,SAASS,QAAQ,KAAK,WAAWT,SAASS,QAAQ,EAAEN,KAAKH,SAASS,QAAQ;YACnF,MAAMC,UAAUF,eAAejB,IAAIC,IAAI,CAACW,EAAE;YAC1C,4CAA4C;YAC5C,MAAMQ,UAAUpB,IAAIC,IAAI,CAACY,UAAU,KAAKf,OAAOgB,KAAK,CAACO,SAAS;YAE9D,IAAI,CAACF,WAAW,CAACC,SAAS;gBACxB,OAAOlB,SAASC,IAAI,CAAC;oBAAEC,SAAS;gBAAY,GAAG;oBAAEC,QAAQ;gBAAI;YAC/D;YAEA,8DAA8D;YAC9D,MAAMiB,cAAc,MAAM,AAACtB,IAAIU,OAAO,CAACa,MAAM,CAAS;gBACpDX,IAAIJ;gBACJK,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCS,MAAM;oBACJC,oBAAoBlB;oBACpBF,QAAQ;gBACV;gBACAL;YACF;YAEA,OAAOE,SAASC,IAAI,CAACmB;QACvB;QACAI,QAAQ;QACRC,MAAM;IACR;AACF"}
@@ -13,9 +13,19 @@ export function createCheckAvailabilityEndpoint(config) {
13
13
  status: 400
14
14
  });
15
15
  }
16
+ const parsedDate = new Date(date);
17
+ if (isNaN(parsedDate.getTime())) {
18
+ return Response.json({
19
+ error: 'Invalid date format. Expected YYYY-MM-DD'
20
+ }, {
21
+ status: 400
22
+ });
23
+ }
24
+ const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1);
16
25
  const slots = await getAvailableSlots({
17
26
  blockingStatuses: config.statusMachine.blockingStatuses,
18
- date: new Date(date),
27
+ date: parsedDate,
28
+ guestCount,
19
29
  payload: req.payload,
20
30
  req,
21
31
  reservationSlug: config.slugs.reservations,
@@ -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'\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 const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: new Date(date),\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 serviceId: service,\n serviceSlug: config.slugs.services,\n })\n\n return Response.json({ slots })\n },\n method: 'get',\n path: '/reserve/availability',\n }\n}\n"],"names":["getAvailableSlots","createCheckAvailabilityEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","message","status","slots","blockingStatuses","statusMachine","Date","payload","reservationSlug","slugs","reservations","resourceId","resourceSlug","resources","scheduleSlug","schedules","serviceId","serviceSlug","services","method","path"],"mappings":"AAIA,SAASA,iBAAiB,QAAQ,qCAAoC;AAEtE,OAAO,SAASC,gCACdC,MAAuC;IAEvC,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,MAAM,IAAIC,IAAIF,IAAIC,GAAG;YAC3B,MAAME,OAAOF,IAAIG,YAAY,CAACC,GAAG,CAAC;YAClC,MAAMC,WAAWL,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAME,UAAUN,IAAIG,YAAY,CAACC,GAAG,CAAC;YAErC,IAAI,CAACF,QAAQ,CAACG,YAAY,CAACC,SAAS;gBAClC,OAAOC,SAASC,IAAI,CAClB;oBAAEC,SAAS;gBAAyD,GACpE;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMC,QAAQ,MAAMhB,kBAAkB;gBACpCiB,kBAAkBf,OAAOgB,aAAa,CAACD,gBAAgB;gBACvDV,MAAM,IAAIY,KAAKZ;gBACfa,SAAShB,IAAIgB,OAAO;gBACpBhB;gBACAiB,iBAAiBnB,OAAOoB,KAAK,CAACC,YAAY;gBAC1CC,YAAYd;gBACZe,cAAcvB,OAAOoB,KAAK,CAACI,SAAS;gBACpCC,cAAczB,OAAOoB,KAAK,CAACM,SAAS;gBACpCC,WAAWlB;gBACXmB,aAAa5B,OAAOoB,KAAK,CAACS,QAAQ;YACpC;YAEA,OAAOnB,SAASC,IAAI,CAAC;gBAAEG;YAAM;QAC/B;QACAgB,QAAQ;QACRC,MAAM;IACR;AACF"}
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'\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 const parsedDate = new Date(date)\n if (isNaN(parsedDate.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n\n const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1)\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: parsedDate,\n guestCount,\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 serviceId: service,\n serviceSlug: config.slugs.services,\n })\n\n return Response.json({ slots })\n },\n method: 'get',\n path: '/reserve/availability',\n }\n}\n"],"names":["getAvailableSlots","createCheckAvailabilityEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","message","status","parsedDate","Date","isNaN","getTime","error","guestCount","Math","max","Number","slots","blockingStatuses","statusMachine","payload","reservationSlug","slugs","reservations","resourceId","resourceSlug","resources","scheduleSlug","schedules","serviceId","serviceSlug","services","method","path"],"mappings":"AAIA,SAASA,iBAAiB,QAAQ,qCAAoC;AAEtE,OAAO,SAASC,gCACdC,MAAuC;IAEvC,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,MAAM,IAAIC,IAAIF,IAAIC,GAAG;YAC3B,MAAME,OAAOF,IAAIG,YAAY,CAACC,GAAG,CAAC;YAClC,MAAMC,WAAWL,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAME,UAAUN,IAAIG,YAAY,CAACC,GAAG,CAAC;YAErC,IAAI,CAACF,QAAQ,CAACG,YAAY,CAACC,SAAS;gBAClC,OAAOC,SAASC,IAAI,CAClB;oBAAEC,SAAS;gBAAyD,GACpE;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMC,aAAa,IAAIC,KAAKV;YAC5B,IAAIW,MAAMF,WAAWG,OAAO,KAAK;gBAC/B,OAAOP,SAASC,IAAI,CAClB;oBAAEO,OAAO;gBAA2C,GACpD;oBAAEL,QAAQ;gBAAI;YAElB;YAEA,MAAMM,aAAaC,KAAKC,GAAG,CAACC,OAAOnB,IAAIG,YAAY,CAACC,GAAG,CAAC,iBAAiB,MAAM;YAE/E,MAAMgB,QAAQ,MAAMzB,kBAAkB;gBACpC0B,kBAAkBxB,OAAOyB,aAAa,CAACD,gBAAgB;gBACvDnB,MAAMS;gBACNK;gBACAO,SAASxB,IAAIwB,OAAO;gBACpBxB;gBACAyB,iBAAiB3B,OAAO4B,KAAK,CAACC,YAAY;gBAC1CC,YAAYtB;gBACZuB,cAAc/B,OAAO4B,KAAK,CAACI,SAAS;gBACpCC,cAAcjC,OAAO4B,KAAK,CAACM,SAAS;gBACpCC,WAAW1B;gBACX2B,aAAapC,OAAO4B,KAAK,CAACS,QAAQ;YACpC;YAEA,OAAO3B,SAASC,IAAI,CAAC;gBAAEY;YAAM;QAC/B;QACAe,QAAQ;QACRC,MAAM;IACR;AACF"}
@@ -21,6 +21,14 @@ export function createCustomerSearchEndpoint(config) {
21
21
  status: 401
22
22
  });
23
23
  }
24
+ // Only allow staff/admin users (non-customer collection) to search customers
25
+ if (req.user.collection === config.slugs.customers) {
26
+ return Response.json({
27
+ message: 'Forbidden'
28
+ }, {
29
+ status: 403
30
+ });
31
+ }
24
32
  const url = new URL(req.url);
25
33
  const search = url.searchParams.get('search') ?? '';
26
34
  const limit = Math.min(Number(url.searchParams.get('limit') ?? '10'), 50);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/customerSearch.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, Field, Where } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\n/**\n * Inspect a collection's field list and return the set of top-level named\n * fields as a plain Set<string>. Unnamed fields (rows, groups without a name,\n * etc.) are skipped.\n */\nfunction getNamedFields(fields: Field[]): Set<string> {\n const names = new Set<string>()\n for (const field of fields) {\n if ('name' in field) {\n names.add(field.name)\n }\n }\n return names\n}\n\nexport function createCustomerSearchEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n if (!req.user) {\n return Response.json({ message: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url!)\n const search = url.searchParams.get('search') ?? ''\n const limit = Math.min(Number(url.searchParams.get('limit') ?? '10'), 50)\n const page = Math.max(Number(url.searchParams.get('page') ?? '1'), 1)\n\n // Detect which fields exist on the target collection at runtime\n const collectionConfig = req.payload.collections[config.slugs.customers as unknown as CollectionSlug]?.config\n const availableFields: Set<string> = collectionConfig\n ? getNamedFields(collectionConfig.fields)\n : new Set()\n\n const hasName = availableFields.has('name')\n const hasFirstName = availableFields.has('firstName')\n const hasLastName = availableFields.has('lastName')\n const hasPhone = availableFields.has('phone')\n\n let where: Where = {}\n\n if (search) {\n const orClauses: Where[] = []\n\n if (hasName) {\n orClauses.push({ name: { contains: search } })\n }\n if (hasFirstName) {\n orClauses.push({ firstName: { contains: search } })\n }\n if (hasLastName) {\n orClauses.push({ lastName: { contains: search } })\n }\n // email is always present on auth collections\n orClauses.push({ email: { contains: search } })\n if (hasPhone) {\n orClauses.push({ phone: { contains: search } })\n }\n\n where = { or: orClauses }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const result = await (req.payload.find as any)({\n collection: config.slugs.customers,\n limit,\n page,\n where,\n })\n\n return Response.json({\n docs: (result.docs as Record<string, unknown>[]).map((doc) => {\n const entry: Record<string, unknown> = {\n id: doc['id'],\n email: doc['email'] ?? '',\n }\n\n if (hasName) {\n entry['name'] = doc['name'] ?? ''\n }\n if (hasFirstName) {\n entry['firstName'] = doc['firstName'] ?? ''\n }\n if (hasLastName) {\n entry['lastName'] = doc['lastName'] ?? ''\n }\n if (hasPhone) {\n entry['phone'] = doc['phone'] ?? ''\n }\n\n return entry\n }),\n hasNextPage: result.hasNextPage,\n totalDocs: result.totalDocs,\n })\n },\n method: 'get',\n path: '/reservation-customer-search',\n }\n}\n"],"names":["getNamedFields","fields","names","Set","field","add","name","createCustomerSearchEndpoint","config","handler","req","user","Response","json","message","status","url","URL","search","searchParams","get","limit","Math","min","Number","page","max","collectionConfig","payload","collections","slugs","customers","availableFields","hasName","has","hasFirstName","hasLastName","hasPhone","where","orClauses","push","contains","firstName","lastName","email","phone","or","result","find","collection","docs","map","doc","entry","id","hasNextPage","totalDocs","method","path"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/endpoints/customerSearch.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, Field, Where } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\n/**\n * Inspect a collection's field list and return the set of top-level named\n * fields as a plain Set<string>. Unnamed fields (rows, groups without a name,\n * etc.) are skipped.\n */\nfunction getNamedFields(fields: Field[]): Set<string> {\n const names = new Set<string>()\n for (const field of fields) {\n if ('name' in field) {\n names.add(field.name)\n }\n }\n return names\n}\n\nexport function createCustomerSearchEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n if (!req.user) {\n return Response.json({ message: 'Unauthorized' }, { status: 401 })\n }\n\n // Only allow staff/admin users (non-customer collection) to search customers\n if (req.user.collection === config.slugs.customers) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n\n const url = new URL(req.url!)\n const search = url.searchParams.get('search') ?? ''\n const limit = Math.min(Number(url.searchParams.get('limit') ?? '10'), 50)\n const page = Math.max(Number(url.searchParams.get('page') ?? '1'), 1)\n\n // Detect which fields exist on the target collection at runtime\n const collectionConfig = req.payload.collections[config.slugs.customers as unknown as CollectionSlug]?.config\n const availableFields: Set<string> = collectionConfig\n ? getNamedFields(collectionConfig.fields)\n : new Set()\n\n const hasName = availableFields.has('name')\n const hasFirstName = availableFields.has('firstName')\n const hasLastName = availableFields.has('lastName')\n const hasPhone = availableFields.has('phone')\n\n let where: Where = {}\n\n if (search) {\n const orClauses: Where[] = []\n\n if (hasName) {\n orClauses.push({ name: { contains: search } })\n }\n if (hasFirstName) {\n orClauses.push({ firstName: { contains: search } })\n }\n if (hasLastName) {\n orClauses.push({ lastName: { contains: search } })\n }\n // email is always present on auth collections\n orClauses.push({ email: { contains: search } })\n if (hasPhone) {\n orClauses.push({ phone: { contains: search } })\n }\n\n where = { or: orClauses }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const result = await (req.payload.find as any)({\n collection: config.slugs.customers,\n limit,\n page,\n where,\n })\n\n return Response.json({\n docs: (result.docs as Record<string, unknown>[]).map((doc) => {\n const entry: Record<string, unknown> = {\n id: doc['id'],\n email: doc['email'] ?? '',\n }\n\n if (hasName) {\n entry['name'] = doc['name'] ?? ''\n }\n if (hasFirstName) {\n entry['firstName'] = doc['firstName'] ?? ''\n }\n if (hasLastName) {\n entry['lastName'] = doc['lastName'] ?? ''\n }\n if (hasPhone) {\n entry['phone'] = doc['phone'] ?? ''\n }\n\n return entry\n }),\n hasNextPage: result.hasNextPage,\n totalDocs: result.totalDocs,\n })\n },\n method: 'get',\n path: '/reservation-customer-search',\n }\n}\n"],"names":["getNamedFields","fields","names","Set","field","add","name","createCustomerSearchEndpoint","config","handler","req","user","Response","json","message","status","collection","slugs","customers","url","URL","search","searchParams","get","limit","Math","min","Number","page","max","collectionConfig","payload","collections","availableFields","hasName","has","hasFirstName","hasLastName","hasPhone","where","orClauses","push","contains","firstName","lastName","email","phone","or","result","find","docs","map","doc","entry","id","hasNextPage","totalDocs","method","path"],"mappings":"AAIA;;;;CAIC,GACD,SAASA,eAAeC,MAAe;IACrC,MAAMC,QAAQ,IAAIC;IAClB,KAAK,MAAMC,SAASH,OAAQ;QAC1B,IAAI,UAAUG,OAAO;YACnBF,MAAMG,GAAG,CAACD,MAAME,IAAI;QACtB;IACF;IACA,OAAOJ;AACT;AAEA,OAAO,SAASK,6BACdC,MAAuC;IAEvC,OAAO;QACLC,SAAS,OAAOC;YACd,IAAI,CAACA,IAAIC,IAAI,EAAE;gBACb,OAAOC,SAASC,IAAI,CAAC;oBAAEC,SAAS;gBAAe,GAAG;oBAAEC,QAAQ;gBAAI;YAClE;YAEA,6EAA6E;YAC7E,IAAIL,IAAIC,IAAI,CAACK,UAAU,KAAKR,OAAOS,KAAK,CAACC,SAAS,EAAE;gBAClD,OAAON,SAASC,IAAI,CAAC;oBAAEC,SAAS;gBAAY,GAAG;oBAAEC,QAAQ;gBAAI;YAC/D;YAEA,MAAMI,MAAM,IAAIC,IAAIV,IAAIS,GAAG;YAC3B,MAAME,SAASF,IAAIG,YAAY,CAACC,GAAG,CAAC,aAAa;YACjD,MAAMC,QAAQC,KAAKC,GAAG,CAACC,OAAOR,IAAIG,YAAY,CAACC,GAAG,CAAC,YAAY,OAAO;YACtE,MAAMK,OAAOH,KAAKI,GAAG,CAACF,OAAOR,IAAIG,YAAY,CAACC,GAAG,CAAC,WAAW,MAAM;YAEnE,gEAAgE;YAChE,MAAMO,mBAAmBpB,IAAIqB,OAAO,CAACC,WAAW,CAACxB,OAAOS,KAAK,CAACC,SAAS,CAA8B,EAAEV;YACvG,MAAMyB,kBAA+BH,mBACjC9B,eAAe8B,iBAAiB7B,MAAM,IACtC,IAAIE;YAER,MAAM+B,UAAUD,gBAAgBE,GAAG,CAAC;YACpC,MAAMC,eAAeH,gBAAgBE,GAAG,CAAC;YACzC,MAAME,cAAcJ,gBAAgBE,GAAG,CAAC;YACxC,MAAMG,WAAWL,gBAAgBE,GAAG,CAAC;YAErC,IAAII,QAAe,CAAC;YAEpB,IAAIlB,QAAQ;gBACV,MAAMmB,YAAqB,EAAE;gBAE7B,IAAIN,SAAS;oBACXM,UAAUC,IAAI,CAAC;wBAAEnC,MAAM;4BAAEoC,UAAUrB;wBAAO;oBAAE;gBAC9C;gBACA,IAAIe,cAAc;oBAChBI,UAAUC,IAAI,CAAC;wBAAEE,WAAW;4BAAED,UAAUrB;wBAAO;oBAAE;gBACnD;gBACA,IAAIgB,aAAa;oBACfG,UAAUC,IAAI,CAAC;wBAAEG,UAAU;4BAAEF,UAAUrB;wBAAO;oBAAE;gBAClD;gBACA,8CAA8C;gBAC9CmB,UAAUC,IAAI,CAAC;oBAAEI,OAAO;wBAAEH,UAAUrB;oBAAO;gBAAE;gBAC7C,IAAIiB,UAAU;oBACZE,UAAUC,IAAI,CAAC;wBAAEK,OAAO;4BAAEJ,UAAUrB;wBAAO;oBAAE;gBAC/C;gBAEAkB,QAAQ;oBAAEQ,IAAIP;gBAAU;YAC1B;YAEA,8DAA8D;YAC9D,MAAMQ,SAAS,MAAM,AAACtC,IAAIqB,OAAO,CAACkB,IAAI,CAAS;gBAC7CjC,YAAYR,OAAOS,KAAK,CAACC,SAAS;gBAClCM;gBACAI;gBACAW;YACF;YAEA,OAAO3B,SAASC,IAAI,CAAC;gBACnBqC,MAAM,AAACF,OAAOE,IAAI,CAA+BC,GAAG,CAAC,CAACC;oBACpD,MAAMC,QAAiC;wBACrCC,IAAIF,GAAG,CAAC,KAAK;wBACbP,OAAOO,GAAG,CAAC,QAAQ,IAAI;oBACzB;oBAEA,IAAIlB,SAAS;wBACXmB,KAAK,CAAC,OAAO,GAAGD,GAAG,CAAC,OAAO,IAAI;oBACjC;oBACA,IAAIhB,cAAc;wBAChBiB,KAAK,CAAC,YAAY,GAAGD,GAAG,CAAC,YAAY,IAAI;oBAC3C;oBACA,IAAIf,aAAa;wBACfgB,KAAK,CAAC,WAAW,GAAGD,GAAG,CAAC,WAAW,IAAI;oBACzC;oBACA,IAAId,UAAU;wBACZe,KAAK,CAAC,QAAQ,GAAGD,GAAG,CAAC,QAAQ,IAAI;oBACnC;oBAEA,OAAOC;gBACT;gBACAE,aAAaP,OAAOO,WAAW;gBAC/BC,WAAWR,OAAOQ,SAAS;YAC7B;QACF;QACAC,QAAQ;QACRC,MAAM;IACR;AACF"}
@@ -25,6 +25,7 @@ export function createGetSlotsEndpoint(config) {
25
25
  const slots = await getAvailableSlots({
26
26
  blockingStatuses: config.statusMachine.blockingStatuses,
27
27
  date: parsedDate,
28
+ guestCount,
28
29
  payload: req.payload,
29
30
  req,
30
31
  reservationSlug: config.slugs.reservations,
@@ -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'\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 const parsedDate = new Date(date)\n if (isNaN(parsedDate.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n\n const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1)\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: parsedDate,\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 serviceId: service,\n serviceSlug: config.slugs.services,\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","createGetSlotsEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","error","status","parsedDate","Date","isNaN","getTime","guestCount","Math","max","Number","slots","blockingStatuses","statusMachine","payload","reservationSlug","slugs","reservations","resourceId","resourceSlug","resources","scheduleSlug","schedules","serviceId","serviceSlug","services","map","s","end","toISOString","start","method","path"],"mappings":"AAIA,SAASA,iBAAiB,QAAQ,qCAAoC;AAEtE,OAAO,SAASC,uBAAuBC,MAAuC;IAC5E,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,MAAM,IAAIC,IAAIF,IAAIC,GAAG;YAC3B,MAAME,OAAOF,IAAIG,YAAY,CAACC,GAAG,CAAC;YAClC,MAAMC,WAAWL,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAME,UAAUN,IAAIG,YAAY,CAACC,GAAG,CAAC;YAErC,IAAI,CAACF,QAAQ,CAACG,YAAY,CAACC,SAAS;gBAClC,OAAOC,SAASC,IAAI,CAClB;oBAAEC,OAAO;gBAAyD,GAClE;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMC,aAAa,IAAIC,KAAKV;YAC5B,IAAIW,MAAMF,WAAWG,OAAO,KAAK;gBAC/B,OAAOP,SAASC,IAAI,CAClB;oBAAEC,OAAO;gBAA2C,GACpD;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMK,aAAaC,KAAKC,GAAG,CAACC,OAAOlB,IAAIG,YAAY,CAACC,GAAG,CAAC,iBAAiB,MAAM;YAE/E,MAAMe,QAAQ,MAAMxB,kBAAkB;gBACpCyB,kBAAkBvB,OAAOwB,aAAa,CAACD,gBAAgB;gBACvDlB,MAAMS;gBACNW,SAASvB,IAAIuB,OAAO;gBACpBvB;gBACAwB,iBAAiB1B,OAAO2B,KAAK,CAACC,YAAY;gBAC1CC,YAAYrB;gBACZsB,cAAc9B,OAAO2B,KAAK,CAACI,SAAS;gBACpCC,cAAchC,OAAO2B,KAAK,CAACM,SAAS;gBACpCC,WAAWzB;gBACX0B,aAAanC,OAAO2B,KAAK,CAACS,QAAQ;YACpC;YAEA,OAAO1B,SAASC,IAAI,CAAC;gBACnBN;gBACAa;gBACAI,OAAOA,MAAMe,GAAG,CAAC,CAACC,IAAO,CAAA;wBAAEC,KAAKD,EAAEC,GAAG,CAACC,WAAW;wBAAIC,OAAOH,EAAEG,KAAK,CAACD,WAAW;oBAAG,CAAA;YACpF;QACF;QACAE,QAAQ;QACRC,MAAM;IACR;AACF"}
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'\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 const parsedDate = new Date(date)\n if (isNaN(parsedDate.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n\n const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1)\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: parsedDate,\n guestCount,\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 serviceId: service,\n serviceSlug: config.slugs.services,\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","createGetSlotsEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","error","status","parsedDate","Date","isNaN","getTime","guestCount","Math","max","Number","slots","blockingStatuses","statusMachine","payload","reservationSlug","slugs","reservations","resourceId","resourceSlug","resources","scheduleSlug","schedules","serviceId","serviceSlug","services","map","s","end","toISOString","start","method","path"],"mappings":"AAIA,SAASA,iBAAiB,QAAQ,qCAAoC;AAEtE,OAAO,SAASC,uBAAuBC,MAAuC;IAC5E,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,MAAM,IAAIC,IAAIF,IAAIC,GAAG;YAC3B,MAAME,OAAOF,IAAIG,YAAY,CAACC,GAAG,CAAC;YAClC,MAAMC,WAAWL,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAME,UAAUN,IAAIG,YAAY,CAACC,GAAG,CAAC;YAErC,IAAI,CAACF,QAAQ,CAACG,YAAY,CAACC,SAAS;gBAClC,OAAOC,SAASC,IAAI,CAClB;oBAAEC,OAAO;gBAAyD,GAClE;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMC,aAAa,IAAIC,KAAKV;YAC5B,IAAIW,MAAMF,WAAWG,OAAO,KAAK;gBAC/B,OAAOP,SAASC,IAAI,CAClB;oBAAEC,OAAO;gBAA2C,GACpD;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMK,aAAaC,KAAKC,GAAG,CAACC,OAAOlB,IAAIG,YAAY,CAACC,GAAG,CAAC,iBAAiB,MAAM;YAE/E,MAAMe,QAAQ,MAAMxB,kBAAkB;gBACpCyB,kBAAkBvB,OAAOwB,aAAa,CAACD,gBAAgB;gBACvDlB,MAAMS;gBACNI;gBACAO,SAASvB,IAAIuB,OAAO;gBACpBvB;gBACAwB,iBAAiB1B,OAAO2B,KAAK,CAACC,YAAY;gBAC1CC,YAAYrB;gBACZsB,cAAc9B,OAAO2B,KAAK,CAACI,SAAS;gBACpCC,cAAchC,OAAO2B,KAAK,CAACM,SAAS;gBACpCC,WAAWzB;gBACX0B,aAAanC,OAAO2B,KAAK,CAACS,QAAQ;YACpC;YAEA,OAAO1B,SAASC,IAAI,CAAC;gBACnBN;gBACAa;gBACAI,OAAOA,MAAMe,GAAG,CAAC,CAACC,IAAO,CAAA;wBAAEC,KAAKD,EAAEC,GAAG,CAACC,WAAW;wBAAIC,OAAOH,EAAEG,KAAK,CAACD,WAAW;oBAAG,CAAA;YACpF;QACF;QACAE,QAAQ;QACRC,MAAM;IACR;AACF"}
@@ -7,32 +7,51 @@ export const onStatusChange = (config)=>async ({ context, doc, previousDoc, req
7
7
  }
8
8
  const prev = previousDoc.status;
9
9
  const next = doc.status;
10
- // Call generic afterStatusChange plugin hooks
11
10
  if (config.hooks?.afterStatusChange) {
12
11
  for (const hook of config.hooks.afterStatusChange){
13
- await hook({
14
- doc: doc,
15
- newStatus: next,
16
- previousStatus: prev,
17
- req
18
- });
12
+ try {
13
+ await hook({
14
+ doc: doc,
15
+ newStatus: next,
16
+ previousStatus: prev,
17
+ req
18
+ });
19
+ } catch (err) {
20
+ req.payload.logger.error({
21
+ err,
22
+ msg: `afterStatusChange hook failed for reservation ${doc.id}`
23
+ });
24
+ }
19
25
  }
20
26
  }
21
- // Call specific hooks based on transition
22
27
  if (next === 'confirmed' && config.hooks?.afterBookingConfirm) {
23
28
  for (const hook of config.hooks.afterBookingConfirm){
24
- await hook({
25
- doc: doc,
26
- req
27
- });
29
+ try {
30
+ await hook({
31
+ doc: doc,
32
+ req
33
+ });
34
+ } catch (err) {
35
+ req.payload.logger.error({
36
+ err,
37
+ msg: `afterBookingConfirm hook failed for reservation ${doc.id}`
38
+ });
39
+ }
28
40
  }
29
41
  }
30
42
  if (next === 'cancelled' && config.hooks?.afterBookingCancel) {
31
43
  for (const hook of config.hooks.afterBookingCancel){
32
- await hook({
33
- doc: doc,
34
- req
35
- });
44
+ try {
45
+ await hook({
46
+ doc: doc,
47
+ req
48
+ });
49
+ } catch (err) {
50
+ req.payload.logger.error({
51
+ err,
52
+ msg: `afterBookingCancel hook failed for reservation ${doc.id}`
53
+ });
54
+ }
36
55
  }
37
56
  }
38
57
  return doc;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/reservations/onStatusChange.ts"],"sourcesContent":["import type { CollectionAfterChangeHook } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nexport const onStatusChange =\n (config: ResolvedReservationPluginConfig): CollectionAfterChangeHook =>\n async ({ context, doc, previousDoc, req }) => {\n if (context?.skipReservationHooks) {return doc}\n if (!previousDoc || previousDoc.status === doc.status) {return doc}\n\n const prev = previousDoc.status as string\n const next = doc.status as string\n\n // Call generic afterStatusChange plugin hooks\n if (config.hooks?.afterStatusChange) {\n for (const hook of config.hooks.afterStatusChange) {\n await hook({ doc: doc as Record<string, unknown>, newStatus: next, previousStatus: prev, req })\n }\n }\n\n // Call specific hooks based on transition\n if (next === 'confirmed' && config.hooks?.afterBookingConfirm) {\n for (const hook of config.hooks.afterBookingConfirm) {\n await hook({ doc: doc as Record<string, unknown>, req })\n }\n }\n if (next === 'cancelled' && config.hooks?.afterBookingCancel) {\n for (const hook of config.hooks.afterBookingCancel) {\n await hook({ doc: doc as Record<string, unknown>, req })\n }\n }\n\n return doc\n }\n"],"names":["onStatusChange","config","context","doc","previousDoc","req","skipReservationHooks","status","prev","next","hooks","afterStatusChange","hook","newStatus","previousStatus","afterBookingConfirm","afterBookingCancel"],"mappings":"AAIA,OAAO,MAAMA,iBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,GAAG,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACvC,IAAIH,SAASI,sBAAsB;YAAC,OAAOH;QAAG;QAC9C,IAAI,CAACC,eAAeA,YAAYG,MAAM,KAAKJ,IAAII,MAAM,EAAE;YAAC,OAAOJ;QAAG;QAElE,MAAMK,OAAOJ,YAAYG,MAAM;QAC/B,MAAME,OAAON,IAAII,MAAM;QAEvB,8CAA8C;QAC9C,IAAIN,OAAOS,KAAK,EAAEC,mBAAmB;YACnC,KAAK,MAAMC,QAAQX,OAAOS,KAAK,CAACC,iBAAiB,CAAE;gBACjD,MAAMC,KAAK;oBAAET,KAAKA;oBAAgCU,WAAWJ;oBAAMK,gBAAgBN;oBAAMH;gBAAI;YAC/F;QACF;QAEA,0CAA0C;QAC1C,IAAII,SAAS,eAAeR,OAAOS,KAAK,EAAEK,qBAAqB;YAC7D,KAAK,MAAMH,QAAQX,OAAOS,KAAK,CAACK,mBAAmB,CAAE;gBACnD,MAAMH,KAAK;oBAAET,KAAKA;oBAAgCE;gBAAI;YACxD;QACF;QACA,IAAII,SAAS,eAAeR,OAAOS,KAAK,EAAEM,oBAAoB;YAC5D,KAAK,MAAMJ,QAAQX,OAAOS,KAAK,CAACM,kBAAkB,CAAE;gBAClD,MAAMJ,KAAK;oBAAET,KAAKA;oBAAgCE;gBAAI;YACxD;QACF;QAEA,OAAOF;IACT,EAAC"}
1
+ {"version":3,"sources":["../../../src/hooks/reservations/onStatusChange.ts"],"sourcesContent":["import type { CollectionAfterChangeHook } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nexport const onStatusChange =\n (config: ResolvedReservationPluginConfig): CollectionAfterChangeHook =>\n async ({ context, doc, previousDoc, req }) => {\n if (context?.skipReservationHooks) {return doc}\n if (!previousDoc || previousDoc.status === doc.status) {return doc}\n\n const prev = previousDoc.status as string\n const next = doc.status as string\n\n if (config.hooks?.afterStatusChange) {\n for (const hook of config.hooks.afterStatusChange) {\n try {\n await hook({ doc: doc as Record<string, unknown>, newStatus: next, previousStatus: prev, req })\n } catch (err) {\n req.payload.logger.error({ err, msg: `afterStatusChange hook failed for reservation ${doc.id}` })\n }\n }\n }\n\n if (next === 'confirmed' && config.hooks?.afterBookingConfirm) {\n for (const hook of config.hooks.afterBookingConfirm) {\n try {\n await hook({ doc: doc as Record<string, unknown>, req })\n } catch (err) {\n req.payload.logger.error({ err, msg: `afterBookingConfirm hook failed for reservation ${doc.id}` })\n }\n }\n }\n if (next === 'cancelled' && config.hooks?.afterBookingCancel) {\n for (const hook of config.hooks.afterBookingCancel) {\n try {\n await hook({ doc: doc as Record<string, unknown>, req })\n } catch (err) {\n req.payload.logger.error({ err, msg: `afterBookingCancel hook failed for reservation ${doc.id}` })\n }\n }\n }\n\n return doc\n }\n"],"names":["onStatusChange","config","context","doc","previousDoc","req","skipReservationHooks","status","prev","next","hooks","afterStatusChange","hook","newStatus","previousStatus","err","payload","logger","error","msg","id","afterBookingConfirm","afterBookingCancel"],"mappings":"AAIA,OAAO,MAAMA,iBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,GAAG,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACvC,IAAIH,SAASI,sBAAsB;YAAC,OAAOH;QAAG;QAC9C,IAAI,CAACC,eAAeA,YAAYG,MAAM,KAAKJ,IAAII,MAAM,EAAE;YAAC,OAAOJ;QAAG;QAElE,MAAMK,OAAOJ,YAAYG,MAAM;QAC/B,MAAME,OAAON,IAAII,MAAM;QAEvB,IAAIN,OAAOS,KAAK,EAAEC,mBAAmB;YACnC,KAAK,MAAMC,QAAQX,OAAOS,KAAK,CAACC,iBAAiB,CAAE;gBACjD,IAAI;oBACF,MAAMC,KAAK;wBAAET,KAAKA;wBAAgCU,WAAWJ;wBAAMK,gBAAgBN;wBAAMH;oBAAI;gBAC/F,EAAE,OAAOU,KAAK;oBACZV,IAAIW,OAAO,CAACC,MAAM,CAACC,KAAK,CAAC;wBAAEH;wBAAKI,KAAK,CAAC,8CAA8C,EAAEhB,IAAIiB,EAAE,EAAE;oBAAC;gBACjG;YACF;QACF;QAEA,IAAIX,SAAS,eAAeR,OAAOS,KAAK,EAAEW,qBAAqB;YAC7D,KAAK,MAAMT,QAAQX,OAAOS,KAAK,CAACW,mBAAmB,CAAE;gBACnD,IAAI;oBACF,MAAMT,KAAK;wBAAET,KAAKA;wBAAgCE;oBAAI;gBACxD,EAAE,OAAOU,KAAK;oBACZV,IAAIW,OAAO,CAACC,MAAM,CAACC,KAAK,CAAC;wBAAEH;wBAAKI,KAAK,CAAC,gDAAgD,EAAEhB,IAAIiB,EAAE,EAAE;oBAAC;gBACnG;YACF;QACF;QACA,IAAIX,SAAS,eAAeR,OAAOS,KAAK,EAAEY,oBAAoB;YAC5D,KAAK,MAAMV,QAAQX,OAAOS,KAAK,CAACY,kBAAkB,CAAE;gBAClD,IAAI;oBACF,MAAMV,KAAK;wBAAET,KAAKA;wBAAgCE;oBAAI;gBACxD,EAAE,OAAOU,KAAK;oBACZV,IAAIW,OAAO,CAACC,MAAM,CAACC,KAAK,CAAC;wBAAEH;wBAAKI,KAAK,CAAC,+CAA+C,EAAEhB,IAAIiB,EAAE,EAAE;oBAAC;gBAClG;YACF;QACF;QAEA,OAAOjB;IACT,EAAC"}
@@ -9,30 +9,31 @@ export const validateConflicts = (config)=>async ({ context, data, operation, or
9
9
  if (items.length === 0) {
10
10
  return data;
11
11
  }
12
- // Fetch buffer times from the primary service
13
- const serviceId = typeof data?.service === 'object' ? data.service.id : data?.service;
14
- let bufferBefore = config.defaultBufferTime;
15
- let bufferAfter = config.defaultBufferTime;
16
- if (serviceId) {
17
- try {
18
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
- const service = await req.payload.findByID({
20
- id: serviceId,
21
- collection: config.slugs.services,
22
- req
23
- });
24
- if (service) {
25
- bufferBefore = service.bufferTimeBefore ?? config.defaultBufferTime;
26
- bufferAfter = service.bufferTimeAfter ?? config.defaultBufferTime;
27
- }
28
- } catch {
29
- // Use defaults if service lookup fails
30
- }
31
- }
32
- for (const item of items){
12
+ for(let i = 0; i < items.length; i++){
13
+ const item = items[i];
33
14
  if (!item.endTime) {
34
15
  continue;
35
16
  }
17
+ // Fetch buffer times from the item's own service (not just the primary)
18
+ const itemServiceId = item.service ?? (typeof data?.service === 'object' ? data.service.id : data?.service);
19
+ let bufferBefore = config.defaultBufferTime;
20
+ let bufferAfter = config.defaultBufferTime;
21
+ if (itemServiceId) {
22
+ try {
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ const service = await req.payload.findByID({
25
+ id: itemServiceId,
26
+ collection: config.slugs.services,
27
+ req
28
+ });
29
+ if (service) {
30
+ bufferBefore = service.bufferTimeBefore ?? config.defaultBufferTime;
31
+ bufferAfter = service.bufferTimeAfter ?? config.defaultBufferTime;
32
+ }
33
+ } catch {
34
+ // Use defaults if service lookup fails
35
+ }
36
+ }
36
37
  const result = await checkAvailability({
37
38
  blockingStatuses: config.statusMachine.blockingStatuses,
38
39
  bufferAfter,
@@ -52,7 +53,7 @@ export const validateConflicts = (config)=>async ({ context, data, operation, or
52
53
  errors: [
53
54
  {
54
55
  message: result.reason ?? req.t('reservation:errorConflict'),
55
- path: 'startTime'
56
+ path: items.length > 1 ? `items.${i}.startTime` : 'startTime'
56
57
  }
57
58
  ]
58
59
  });
@@ -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 { resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const validateConflicts =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n const items = resolveReservationItems(data as Record<string, unknown>)\n\n if (items.length === 0) {return data}\n\n // Fetch buffer times from the primary service\n const serviceId = typeof data?.service === 'object' ? data.service.id : data?.service\n let bufferBefore = config.defaultBufferTime\n let bufferAfter = config.defaultBufferTime\n\n if (serviceId) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: serviceId,\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 for (const item of items) {\n if (!item.endTime) {continue}\n\n const result = await checkAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime: new Date(item.endTime),\n excludeReservationId: operation === 'update' ? originalDoc?.id : undefined,\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 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: 'startTime',\n },\n ],\n })\n }\n }\n\n return data\n }\n"],"names":["ValidationError","checkAvailability","resolveReservationItems","validateConflicts","config","context","data","operation","originalDoc","req","skipReservationHooks","items","length","serviceId","service","id","bufferBefore","defaultBufferTime","bufferAfter","payload","findByID","collection","slugs","services","bufferTimeBefore","bufferTimeAfter","item","endTime","result","blockingStatuses","statusMachine","Date","excludeReservationId","undefined","guestCount","reservationSlug","reservations","resourceId","resource","resourceSlug","resources","startTime","available","errors","message","reason","t","path"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAKzC,SAASC,iBAAiB,QAAQ,wCAAuC;AACzE,SAASC,uBAAuB,QAAQ,6CAA4C;AAEpF,OAAO,MAAMC,oBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACnD,IAAIJ,SAASK,sBAAsB;YAAC,OAAOJ;QAAI;QAE/C,MAAMK,QAAQT,wBAAwBI;QAEtC,IAAIK,MAAMC,MAAM,KAAK,GAAG;YAAC,OAAON;QAAI;QAEpC,8CAA8C;QAC9C,MAAMO,YAAY,OAAOP,MAAMQ,YAAY,WAAWR,KAAKQ,OAAO,CAACC,EAAE,GAAGT,MAAMQ;QAC9E,IAAIE,eAAeZ,OAAOa,iBAAiB;QAC3C,IAAIC,cAAcd,OAAOa,iBAAiB;QAE1C,IAAIJ,WAAW;YACb,IAAI;gBACF,8DAA8D;gBAC9D,MAAMC,UAAU,MAAM,AAACL,IAAIU,OAAO,CAACC,QAAQ,CAAS;oBAClDL,IAAIF;oBACJQ,YAAYjB,OAAOkB,KAAK,CAACC,QAAQ;oBACjCd;gBACF;gBACA,IAAIK,SAAS;oBACXE,eAAe,AAACF,QAAQU,gBAAgB,IAAepB,OAAOa,iBAAiB;oBAC/EC,cAAc,AAACJ,QAAQW,eAAe,IAAerB,OAAOa,iBAAiB;gBAC/E;YACF,EAAE,OAAM;YACN,uCAAuC;YACzC;QACF;QAEA,KAAK,MAAMS,QAAQf,MAAO;YACxB,IAAI,CAACe,KAAKC,OAAO,EAAE;gBAAC;YAAQ;YAE5B,MAAMC,SAAS,MAAM3B,kBAAkB;gBACrC4B,kBAAkBzB,OAAO0B,aAAa,CAACD,gBAAgB;gBACvDX;gBACAF;gBACAW,SAAS,IAAII,KAAKL,KAAKC,OAAO;gBAC9BK,sBAAsBzB,cAAc,WAAWC,aAAaO,KAAKkB;gBACjEC,YAAYR,KAAKQ,UAAU;gBAC3Bf,SAASV,IAAIU,OAAO;gBACpBV;gBACA0B,iBAAiB/B,OAAOkB,KAAK,CAACc,YAAY;gBAC1CC,YAAYX,KAAKY,QAAQ;gBACzBC,cAAcnC,OAAOkB,KAAK,CAACkB,SAAS;gBACpCC,WAAW,IAAIV,KAAKL,KAAKe,SAAS;YACpC;YAEA,IAAI,CAACb,OAAOc,SAAS,EAAE;gBACrB,MAAM,IAAI1C,gBAAgB;oBACxB2C,QAAQ;wBACN;4BACEC,SAAShB,OAAOiB,MAAM,IAAI,AAACpC,IAAIqC,CAAC,CAAa;4BAC7CC,MAAM;wBACR;qBACD;gBACH;YACF;QACF;QAEA,OAAOzC;IACT,EAAC"}
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 { resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const validateConflicts =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n const items = resolveReservationItems(data as Record<string, unknown>)\n\n if (items.length === 0) {return data}\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n if (!item.endTime) {continue}\n\n // Fetch buffer times from the item's own service (not just the primary)\n const itemServiceId = item.service\n ?? (typeof data?.service === 'object' ? data.service.id : data?.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 const result = await checkAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime: new Date(item.endTime),\n excludeReservationId: operation === 'update' ? originalDoc?.id : undefined,\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 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","resolveReservationItems","validateConflicts","config","context","data","operation","originalDoc","req","skipReservationHooks","items","length","i","item","endTime","itemServiceId","service","id","bufferBefore","defaultBufferTime","bufferAfter","payload","findByID","collection","slugs","services","bufferTimeBefore","bufferTimeAfter","result","blockingStatuses","statusMachine","Date","excludeReservationId","undefined","guestCount","reservationSlug","reservations","resourceId","resource","resourceSlug","resources","startTime","available","errors","message","reason","t","path"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAKzC,SAASC,iBAAiB,QAAQ,wCAAuC;AACzE,SAASC,uBAAuB,QAAQ,6CAA4C;AAEpF,OAAO,MAAMC,oBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACnD,IAAIJ,SAASK,sBAAsB;YAAC,OAAOJ;QAAI;QAE/C,MAAMK,QAAQT,wBAAwBI;QAEtC,IAAIK,MAAMC,MAAM,KAAK,GAAG;YAAC,OAAON;QAAI;QAEpC,IAAK,IAAIO,IAAI,GAAGA,IAAIF,MAAMC,MAAM,EAAEC,IAAK;YACrC,MAAMC,OAAOH,KAAK,CAACE,EAAE;YACrB,IAAI,CAACC,KAAKC,OAAO,EAAE;gBAAC;YAAQ;YAE5B,wEAAwE;YACxE,MAAMC,gBAAgBF,KAAKG,OAAO,IAC5B,CAAA,OAAOX,MAAMW,YAAY,WAAWX,KAAKW,OAAO,CAACC,EAAE,GAAGZ,MAAMW,OAAM;YACxE,IAAIE,eAAef,OAAOgB,iBAAiB;YAC3C,IAAIC,cAAcjB,OAAOgB,iBAAiB;YAE1C,IAAIJ,eAAe;gBACjB,IAAI;oBACF,8DAA8D;oBAC9D,MAAMC,UAAU,MAAM,AAACR,IAAIa,OAAO,CAACC,QAAQ,CAAS;wBAClDL,IAAIF;wBACJQ,YAAYpB,OAAOqB,KAAK,CAACC,QAAQ;wBACjCjB;oBACF;oBACA,IAAIQ,SAAS;wBACXE,eAAe,AAACF,QAAQU,gBAAgB,IAAevB,OAAOgB,iBAAiB;wBAC/EC,cAAc,AAACJ,QAAQW,eAAe,IAAexB,OAAOgB,iBAAiB;oBAC/E;gBACF,EAAE,OAAM;gBACN,uCAAuC;gBACzC;YACF;YAEA,MAAMS,SAAS,MAAM5B,kBAAkB;gBACrC6B,kBAAkB1B,OAAO2B,aAAa,CAACD,gBAAgB;gBACvDT;gBACAF;gBACAJ,SAAS,IAAIiB,KAAKlB,KAAKC,OAAO;gBAC9BkB,sBAAsB1B,cAAc,WAAWC,aAAaU,KAAKgB;gBACjEC,YAAYrB,KAAKqB,UAAU;gBAC3Bb,SAASb,IAAIa,OAAO;gBACpBb;gBACA2B,iBAAiBhC,OAAOqB,KAAK,CAACY,YAAY;gBAC1CC,YAAYxB,KAAKyB,QAAQ;gBACzBC,cAAcpC,OAAOqB,KAAK,CAACgB,SAAS;gBACpCC,WAAW,IAAIV,KAAKlB,KAAK4B,SAAS;YACpC;YAEA,IAAI,CAACb,OAAOc,SAAS,EAAE;gBACrB,MAAM,IAAI3C,gBAAgB;oBACxB4C,QAAQ;wBACN;4BACEC,SAAShB,OAAOiB,MAAM,IAAI,AAACrC,IAAIsC,CAAC,CAAa;4BAC7CC,MAAMrC,MAAMC,MAAM,GAAG,IAAI,CAAC,MAAM,EAAEC,EAAE,UAAU,CAAC,GAAG;wBACpD;qBACD;gBACH;YACF;QACF;QAEA,OAAOP;IACT,EAAC"}
@@ -7,11 +7,16 @@ export const validateStatusTransition = (config)=>async ({ context, data, operat
7
7
  const newStatus = data?.status;
8
8
  const { statusMachine } = config;
9
9
  if (operation === 'create') {
10
- const isAdmin = Boolean(req.user);
10
+ // context.allowConfirmedOnCreate is the escape hatch for payment hooks
11
+ // that need to create confirmed reservations programmatically
12
+ const hasContextBypass = Boolean(context?.allowConfirmedOnCreate);
13
+ // Admin = user from a non-customer collection (e.g., 'users' admin collection)
14
+ const isAdmin = req.user != null && req.user.collection !== config.slugs.customers;
11
15
  const defaultStatus = statusMachine.defaultStatus;
12
- const allowedOnCreate = isAdmin ? [
16
+ const nonDefaultStatuses = statusMachine.transitions[defaultStatus] ?? [];
17
+ const allowedOnCreate = isAdmin || hasContextBypass ? [
13
18
  defaultStatus,
14
- 'confirmed'
19
+ ...nonDefaultStatuses
15
20
  ] : [
16
21
  defaultStatus
17
22
  ];
@@ -28,7 +33,6 @@ export const validateStatusTransition = (config)=>async ({ context, data, operat
28
33
  ]
29
34
  });
30
35
  }
31
- // Call beforeBookingCreate hooks (handled by plugin hooks wrapper)
32
36
  return data;
33
37
  }
34
38
  // On update
@@ -53,7 +57,10 @@ export const validateStatusTransition = (config)=>async ({ context, data, operat
53
57
  if (newStatus === 'confirmed' && config.hooks?.beforeBookingConfirm) {
54
58
  for (const hook of config.hooks.beforeBookingConfirm){
55
59
  await hook({
56
- doc: originalDoc,
60
+ doc: {
61
+ ...originalDoc,
62
+ ...data
63
+ },
57
64
  newStatus,
58
65
  req
59
66
  });
@@ -63,7 +70,10 @@ export const validateStatusTransition = (config)=>async ({ context, data, operat
63
70
  if (newStatus === 'cancelled' && config.hooks?.beforeBookingCancel) {
64
71
  for (const hook of config.hooks.beforeBookingCancel){
65
72
  await hook({
66
- doc: originalDoc,
73
+ doc: {
74
+ ...originalDoc,
75
+ ...data
76
+ },
67
77
  reason: data?.cancellationReason,
68
78
  req
69
79
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/reservations/validateStatusTransition.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 { validateTransition } from '../../services/AvailabilityService.js'\n\nexport const validateStatusTransition =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n const newStatus = data?.status as string | undefined\n const { statusMachine } = config\n\n if (operation === 'create') {\n const isAdmin = Boolean(req.user)\n const defaultStatus = statusMachine.defaultStatus\n const allowedOnCreate: string[] = isAdmin\n ? [defaultStatus, 'confirmed']\n : [defaultStatus]\n\n if (newStatus && !allowedOnCreate.includes(newStatus)) {\n const allowed = allowedOnCreate.map((s) => `\"${s}\"`).join(' or ')\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorInvalidCreateStatus', { allowed }),\n path: 'status',\n },\n ],\n })\n }\n\n // Call beforeBookingCreate hooks (handled by plugin hooks wrapper)\n return data\n }\n\n // On update\n if (operation === 'update' && newStatus) {\n const previousStatus = originalDoc?.status as string | undefined\n\n if (previousStatus && previousStatus !== newStatus) {\n const result = validateTransition(previousStatus, newStatus, statusMachine)\n\n if (!result.valid) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorInvalidTransition', {\n from: previousStatus,\n to: newStatus,\n }),\n path: 'status',\n },\n ],\n })\n }\n\n // Call beforeBookingConfirm plugin hooks\n if (newStatus === 'confirmed' && config.hooks?.beforeBookingConfirm) {\n for (const hook of config.hooks.beforeBookingConfirm) {\n await hook({\n doc: originalDoc as Record<string, unknown>,\n newStatus,\n req,\n })\n }\n }\n\n // Call beforeBookingCancel plugin hooks\n if (newStatus === 'cancelled' && config.hooks?.beforeBookingCancel) {\n for (const hook of config.hooks.beforeBookingCancel) {\n await hook({\n doc: originalDoc as Record<string, unknown>,\n reason: data?.cancellationReason as string | undefined,\n req,\n })\n }\n }\n }\n }\n\n return data\n }\n"],"names":["ValidationError","validateTransition","validateStatusTransition","config","context","data","operation","originalDoc","req","skipReservationHooks","newStatus","status","statusMachine","isAdmin","Boolean","user","defaultStatus","allowedOnCreate","includes","allowed","map","s","join","errors","message","t","path","previousStatus","result","valid","from","to","hooks","beforeBookingConfirm","hook","doc","beforeBookingCancel","reason","cancellationReason"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAKzC,SAASC,kBAAkB,QAAQ,wCAAuC;AAE1E,OAAO,MAAMC,2BACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACnD,IAAIJ,SAASK,sBAAsB;YAAC,OAAOJ;QAAI;QAE/C,MAAMK,YAAYL,MAAMM;QACxB,MAAM,EAAEC,aAAa,EAAE,GAAGT;QAE1B,IAAIG,cAAc,UAAU;YAC1B,MAAMO,UAAUC,QAAQN,IAAIO,IAAI;YAChC,MAAMC,gBAAgBJ,cAAcI,aAAa;YACjD,MAAMC,kBAA4BJ,UAC9B;gBAACG;gBAAe;aAAY,GAC5B;gBAACA;aAAc;YAEnB,IAAIN,aAAa,CAACO,gBAAgBC,QAAQ,CAACR,YAAY;gBACrD,MAAMS,UAAUF,gBAAgBG,GAAG,CAAC,CAACC,IAAM,CAAC,CAAC,EAAEA,EAAE,CAAC,CAAC,EAAEC,IAAI,CAAC;gBAC1D,MAAM,IAAItB,gBAAgB;oBACxBuB,QAAQ;wBACN;4BACEC,SAAS,AAAChB,IAAIiB,CAAC,CAAa,wCAAwC;gCAAEN;4BAAQ;4BAC9EO,MAAM;wBACR;qBACD;gBACH;YACF;YAEA,mEAAmE;YACnE,OAAOrB;QACT;QAEA,YAAY;QACZ,IAAIC,cAAc,YAAYI,WAAW;YACvC,MAAMiB,iBAAiBpB,aAAaI;YAEpC,IAAIgB,kBAAkBA,mBAAmBjB,WAAW;gBAClD,MAAMkB,SAAS3B,mBAAmB0B,gBAAgBjB,WAAWE;gBAE7D,IAAI,CAACgB,OAAOC,KAAK,EAAE;oBACjB,MAAM,IAAI7B,gBAAgB;wBACxBuB,QAAQ;4BACN;gCACEC,SAAS,AAAChB,IAAIiB,CAAC,CAAa,sCAAsC;oCAChEK,MAAMH;oCACNI,IAAIrB;gCACN;gCACAgB,MAAM;4BACR;yBACD;oBACH;gBACF;gBAEA,yCAAyC;gBACzC,IAAIhB,cAAc,eAAeP,OAAO6B,KAAK,EAAEC,sBAAsB;oBACnE,KAAK,MAAMC,QAAQ/B,OAAO6B,KAAK,CAACC,oBAAoB,CAAE;wBACpD,MAAMC,KAAK;4BACTC,KAAK5B;4BACLG;4BACAF;wBACF;oBACF;gBACF;gBAEA,wCAAwC;gBACxC,IAAIE,cAAc,eAAeP,OAAO6B,KAAK,EAAEI,qBAAqB;oBAClE,KAAK,MAAMF,QAAQ/B,OAAO6B,KAAK,CAACI,mBAAmB,CAAE;wBACnD,MAAMF,KAAK;4BACTC,KAAK5B;4BACL8B,QAAQhC,MAAMiC;4BACd9B;wBACF;oBACF;gBACF;YACF;QACF;QAEA,OAAOH;IACT,EAAC"}
1
+ {"version":3,"sources":["../../../src/hooks/reservations/validateStatusTransition.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 { validateTransition } from '../../services/AvailabilityService.js'\n\nexport const validateStatusTransition =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n const newStatus = data?.status as string | undefined\n const { statusMachine } = config\n\n if (operation === 'create') {\n // context.allowConfirmedOnCreate is the escape hatch for payment hooks\n // that need to create confirmed reservations programmatically\n const hasContextBypass = Boolean(context?.allowConfirmedOnCreate)\n // Admin = user from a non-customer collection (e.g., 'users' admin collection)\n const isAdmin = req.user != null && req.user.collection !== config.slugs.customers\n const defaultStatus = statusMachine.defaultStatus\n const nonDefaultStatuses = statusMachine.transitions[defaultStatus] ?? []\n const allowedOnCreate: string[] = (isAdmin || hasContextBypass)\n ? [defaultStatus, ...nonDefaultStatuses]\n : [defaultStatus]\n\n if (newStatus && !allowedOnCreate.includes(newStatus)) {\n const allowed = allowedOnCreate.map((s) => `\"${s}\"`).join(' or ')\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorInvalidCreateStatus', { allowed }),\n path: 'status',\n },\n ],\n })\n }\n\n return data\n }\n\n // On update\n if (operation === 'update' && newStatus) {\n const previousStatus = originalDoc?.status as string | undefined\n\n if (previousStatus && previousStatus !== newStatus) {\n const result = validateTransition(previousStatus, newStatus, statusMachine)\n\n if (!result.valid) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorInvalidTransition', {\n from: previousStatus,\n to: newStatus,\n }),\n path: 'status',\n },\n ],\n })\n }\n\n // Call beforeBookingConfirm plugin hooks\n if (newStatus === 'confirmed' && config.hooks?.beforeBookingConfirm) {\n for (const hook of config.hooks.beforeBookingConfirm) {\n await hook({\n doc: { ...(originalDoc as Record<string, unknown>), ...(data as Record<string, unknown>) },\n newStatus,\n req,\n })\n }\n }\n\n // Call beforeBookingCancel plugin hooks\n if (newStatus === 'cancelled' && config.hooks?.beforeBookingCancel) {\n for (const hook of config.hooks.beforeBookingCancel) {\n await hook({\n doc: { ...(originalDoc as Record<string, unknown>), ...(data as Record<string, unknown>) },\n reason: data?.cancellationReason as string | undefined,\n req,\n })\n }\n }\n }\n }\n\n return data\n }\n"],"names":["ValidationError","validateTransition","validateStatusTransition","config","context","data","operation","originalDoc","req","skipReservationHooks","newStatus","status","statusMachine","hasContextBypass","Boolean","allowConfirmedOnCreate","isAdmin","user","collection","slugs","customers","defaultStatus","nonDefaultStatuses","transitions","allowedOnCreate","includes","allowed","map","s","join","errors","message","t","path","previousStatus","result","valid","from","to","hooks","beforeBookingConfirm","hook","doc","beforeBookingCancel","reason","cancellationReason"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAKzC,SAASC,kBAAkB,QAAQ,wCAAuC;AAE1E,OAAO,MAAMC,2BACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACnD,IAAIJ,SAASK,sBAAsB;YAAC,OAAOJ;QAAI;QAE/C,MAAMK,YAAYL,MAAMM;QACxB,MAAM,EAAEC,aAAa,EAAE,GAAGT;QAE1B,IAAIG,cAAc,UAAU;YAC1B,uEAAuE;YACvE,8DAA8D;YAC9D,MAAMO,mBAAmBC,QAAQV,SAASW;YAC1C,+EAA+E;YAC/E,MAAMC,UAAUR,IAAIS,IAAI,IAAI,QAAQT,IAAIS,IAAI,CAACC,UAAU,KAAKf,OAAOgB,KAAK,CAACC,SAAS;YAClF,MAAMC,gBAAgBT,cAAcS,aAAa;YACjD,MAAMC,qBAAqBV,cAAcW,WAAW,CAACF,cAAc,IAAI,EAAE;YACzE,MAAMG,kBAA4B,AAACR,WAAWH,mBAC1C;gBAACQ;mBAAkBC;aAAmB,GACtC;gBAACD;aAAc;YAEnB,IAAIX,aAAa,CAACc,gBAAgBC,QAAQ,CAACf,YAAY;gBACrD,MAAMgB,UAAUF,gBAAgBG,GAAG,CAAC,CAACC,IAAM,CAAC,CAAC,EAAEA,EAAE,CAAC,CAAC,EAAEC,IAAI,CAAC;gBAC1D,MAAM,IAAI7B,gBAAgB;oBACxB8B,QAAQ;wBACN;4BACEC,SAAS,AAACvB,IAAIwB,CAAC,CAAa,wCAAwC;gCAAEN;4BAAQ;4BAC9EO,MAAM;wBACR;qBACD;gBACH;YACF;YAEA,OAAO5B;QACT;QAEA,YAAY;QACZ,IAAIC,cAAc,YAAYI,WAAW;YACvC,MAAMwB,iBAAiB3B,aAAaI;YAEpC,IAAIuB,kBAAkBA,mBAAmBxB,WAAW;gBAClD,MAAMyB,SAASlC,mBAAmBiC,gBAAgBxB,WAAWE;gBAE7D,IAAI,CAACuB,OAAOC,KAAK,EAAE;oBACjB,MAAM,IAAIpC,gBAAgB;wBACxB8B,QAAQ;4BACN;gCACEC,SAAS,AAACvB,IAAIwB,CAAC,CAAa,sCAAsC;oCAChEK,MAAMH;oCACNI,IAAI5B;gCACN;gCACAuB,MAAM;4BACR;yBACD;oBACH;gBACF;gBAEA,yCAAyC;gBACzC,IAAIvB,cAAc,eAAeP,OAAOoC,KAAK,EAAEC,sBAAsB;oBACnE,KAAK,MAAMC,QAAQtC,OAAOoC,KAAK,CAACC,oBAAoB,CAAE;wBACpD,MAAMC,KAAK;4BACTC,KAAK;gCAAE,GAAInC,WAAW;gCAA8B,GAAIF,IAAI;4BAA6B;4BACzFK;4BACAF;wBACF;oBACF;gBACF;gBAEA,wCAAwC;gBACxC,IAAIE,cAAc,eAAeP,OAAOoC,KAAK,EAAEI,qBAAqB;oBAClE,KAAK,MAAMF,QAAQtC,OAAOoC,KAAK,CAACI,mBAAmB,CAAE;wBACnD,MAAMF,KAAK;4BACTC,KAAK;gCAAE,GAAInC,WAAW;gCAA8B,GAAIF,IAAI;4BAA6B;4BACzFuC,QAAQvC,MAAMwC;4BACdrC;wBACF;oBACF;gBACF;YACF;QACF;QAEA,OAAOH;IACT,EAAC"}
@@ -43,6 +43,7 @@ export declare function checkAvailability(params: {
43
43
  export declare function getAvailableSlots(params: {
44
44
  blockingStatuses: string[];
45
45
  date: Date;
46
+ guestCount?: number;
46
47
  payload: Payload;
47
48
  req: PayloadRequest;
48
49
  reservationSlug: string;
@@ -142,7 +142,7 @@ export async function checkAvailability(params) {
142
142
  };
143
143
  }
144
144
  export async function getAvailableSlots(params) {
145
- const { blockingStatuses, date, payload, req, reservationSlug, resourceId, resourceSlug, scheduleSlug, serviceId, serviceSlug } = params;
145
+ const { blockingStatuses, date, guestCount, payload, req, reservationSlug, resourceId, resourceSlug, scheduleSlug, serviceId, serviceSlug } = params;
146
146
  // 1. Fetch service for duration + buffer times
147
147
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
148
  const service = await payload.findByID({
@@ -195,6 +195,33 @@ export async function getAvailableSlots(params) {
195
195
  const slotDuration = Math.round(slotEndOffset.getTime() / 60_000);
196
196
  const effectiveDuration = durationType === 'fixed' ? duration : slotDuration;
197
197
  const availableSlots = [];
198
+ // For full-day services, offer the entire range as a single slot per range
199
+ if (durationType === 'full-day') {
200
+ for (const range of timeRanges){
201
+ const result = await checkAvailability({
202
+ blockingStatuses,
203
+ bufferAfter: 0,
204
+ bufferBefore: 0,
205
+ endTime: range.end,
206
+ guestCount: guestCount ?? 1,
207
+ payload,
208
+ req,
209
+ reservationSlug,
210
+ resourceId,
211
+ resourceSlug,
212
+ startTime: range.start
213
+ });
214
+ if (result.available) {
215
+ availableSlots.push({
216
+ end: range.end,
217
+ start: range.start
218
+ });
219
+ }
220
+ }
221
+ return availableSlots;
222
+ }
223
+ // Step by a smaller increment to catch slots between buffer gaps
224
+ const stepSize = Math.min(effectiveDuration, 15);
198
225
  for (const range of timeRanges){
199
226
  let candidateStart = new Date(range.start);
200
227
  while(true){
@@ -208,7 +235,7 @@ export async function getAvailableSlots(params) {
208
235
  bufferAfter,
209
236
  bufferBefore,
210
237
  endTime: candidateEnd,
211
- guestCount: 1,
238
+ guestCount: guestCount ?? 1,
212
239
  payload,
213
240
  req,
214
241
  reservationSlug,
@@ -222,8 +249,7 @@ export async function getAvailableSlots(params) {
222
249
  start: new Date(candidateStart)
223
250
  });
224
251
  }
225
- // Move to next slot (service duration as step)
226
- candidateStart = addMinutes(candidateStart, effectiveDuration);
252
+ candidateStart = addMinutes(candidateStart, stepSize);
227
253
  }
228
254
  }
229
255
  return availableSlots;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/services/AvailabilityService.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport type { CapacityMode, DurationType, StatusMachineConfig } from '../types.js'\n\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { addMinutes, computeBlockedWindow } from '../utilities/slotUtils.js'\n\n// --- Pure functions (no DB) ---\n\nexport function computeEndTime(params: {\n durationType: DurationType\n endTime?: Date\n serviceDuration: number\n startTime: Date\n}): { durationMinutes: number; endTime: Date } {\n const { durationType, serviceDuration, startTime } = params\n\n if (durationType === 'full-day') {\n const end = new Date(startTime)\n end.setHours(23, 59, 59, 999)\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?: string\n resourceId: string\n}): Where {\n const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } =\n params\n\n const conditions: Where[] = [\n { resource: { equals: resourceId } },\n { status: { in: blockingStatuses } },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\n ]\n\n if (excludeReservationId) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\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?: string\n guestCount: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: string\n resourceSlug: string\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 guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\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 // Compute effective window with buffers\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n startTime,\n endTime,\n bufferBefore,\n bufferAfter,\n )\n\n // Build overlap query\n const where = buildOverlapQuery({\n blockingStatuses,\n effectiveEnd,\n effectiveStart,\n excludeReservationId,\n resourceId,\n })\n\n if (capacityMode === 'per-guest') {\n // Must fetch docs to sum guestCount\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 select: { guestCount: true },\n where,\n })\n const currentGuests = docs.reduce(\n (sum: number, doc: Record<string, unknown>) => sum + ((doc.guestCount as number) ?? 1),\n 0,\n )\n return {\n available: currentGuests + guestCount <= quantity,\n currentCount: currentGuests,\n reason:\n currentGuests + guestCount > quantity ? 'Guest capacity exceeded' : undefined,\n totalCapacity: quantity,\n }\n }\n\n // per-reservation mode: count is sufficient\n // TODO: batch queries — linear per-item cost acceptable for 2-5 items\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { totalDocs } = await (payload.count as any)({\n collection: reservationSlug,\n req,\n where,\n })\n return {\n available: totalDocs + 1 <= quantity,\n currentCount: totalDocs,\n reason: totalDocs + 1 > quantity ? 'All units are booked for this time' : undefined,\n totalCapacity: quantity,\n }\n}\n\nexport async function getAvailableSlots(params: {\n blockingStatuses: string[]\n date: Date\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: string\n resourceSlug: string\n scheduleSlug: string\n serviceId: string\n serviceSlug: string\n}): Promise<Array<{ end: Date; start: Date }>> {\n const {\n blockingStatuses,\n date,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n scheduleSlug,\n serviceId,\n serviceSlug,\n } = params\n\n // 1. Fetch service for duration + buffer times\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. Fetch resource's schedules for the date\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: resourceId } }, { active: { equals: true } }],\n },\n })\n\n // 3. Resolve schedules to time ranges for the date\n const timeRanges: Array<{ end: Date; start: Date }> = []\n for (const schedule of schedules) {\n const ranges = resolveScheduleForDate(\n schedule as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n )\n timeRanges.push(...ranges)\n }\n\n if (timeRanges.length === 0) {\n return []\n }\n\n // 4. Generate candidate slots from schedule ranges\n const { endTime: slotEndOffset } = computeEndTime({\n durationType,\n serviceDuration: duration,\n startTime: new Date(0),\n })\n const slotDuration = Math.round(slotEndOffset.getTime() / 60_000)\n const effectiveDuration = durationType === 'fixed' ? duration : slotDuration\n\n const availableSlots: Array<{ end: Date; start: Date }> = []\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) {break}\n\n // 5. Check availability for each candidate slot\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime: candidateEnd,\n guestCount: 1,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n startTime: candidateStart,\n })\n\n if (result.available) {\n availableSlots.push({ end: candidateEnd, start: new Date(candidateStart) })\n }\n\n // Move to next slot (service duration as step)\n candidateStart = addMinutes(candidateStart, effectiveDuration)\n }\n }\n\n return availableSlots\n}\n"],"names":["resolveScheduleForDate","addMinutes","computeBlockedWindow","computeEndTime","params","durationType","serviceDuration","startTime","end","Date","setHours","durationMinutes","Math","round","getTime","endTime","buildOverlapQuery","blockingStatuses","effectiveEnd","effectiveStart","excludeReservationId","resourceId","conditions","resource","equals","status","in","less_than","toISOString","greater_than","push","id","not_equals","and","isBlockingStatus","statusMachine","includes","validateTransition","fromStatus","toStatus","allowed","transitions","reason","valid","checkAvailability","bufferAfter","bufferBefore","guestCount","payload","req","reservationSlug","resourceSlug","findByID","collection","depth","quantity","capacityMode","where","docs","find","limit","select","currentGuests","reduce","sum","doc","available","currentCount","undefined","totalCapacity","totalDocs","count","getAvailableSlots","date","scheduleSlug","serviceId","serviceSlug","service","duration","bufferTimeBefore","bufferTimeAfter","schedules","active","timeRanges","schedule","ranges","length","slotEndOffset","slotDuration","effectiveDuration","availableSlots","range","candidateStart","start","candidateEnd","result"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/services/AvailabilityService.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport type { CapacityMode, DurationType, StatusMachineConfig } from '../types.js'\n\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { addMinutes, computeBlockedWindow } from '../utilities/slotUtils.js'\n\n// --- Pure functions (no DB) ---\n\nexport function computeEndTime(params: {\n durationType: DurationType\n endTime?: Date\n serviceDuration: number\n startTime: Date\n}): { durationMinutes: number; endTime: Date } {\n const { durationType, serviceDuration, startTime } = params\n\n if (durationType === 'full-day') {\n const end = new Date(startTime)\n end.setHours(23, 59, 59, 999)\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?: string\n resourceId: string\n}): Where {\n const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } =\n params\n\n const conditions: Where[] = [\n { resource: { equals: resourceId } },\n { status: { in: blockingStatuses } },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\n ]\n\n if (excludeReservationId) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\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?: string\n guestCount: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: string\n resourceSlug: string\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 guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\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 // Compute effective window with buffers\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n startTime,\n endTime,\n bufferBefore,\n bufferAfter,\n )\n\n // Build overlap query\n const where = buildOverlapQuery({\n blockingStatuses,\n effectiveEnd,\n effectiveStart,\n excludeReservationId,\n resourceId,\n })\n\n if (capacityMode === 'per-guest') {\n // Must fetch docs to sum guestCount\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 select: { guestCount: true },\n where,\n })\n const currentGuests = docs.reduce(\n (sum: number, doc: Record<string, unknown>) => sum + ((doc.guestCount as number) ?? 1),\n 0,\n )\n return {\n available: currentGuests + guestCount <= quantity,\n currentCount: currentGuests,\n reason:\n currentGuests + guestCount > quantity ? 'Guest capacity exceeded' : undefined,\n totalCapacity: quantity,\n }\n }\n\n // per-reservation mode: count is sufficient\n // TODO: batch queries — linear per-item cost acceptable for 2-5 items\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { totalDocs } = await (payload.count as any)({\n collection: reservationSlug,\n req,\n where,\n })\n return {\n available: totalDocs + 1 <= quantity,\n currentCount: totalDocs,\n reason: totalDocs + 1 > quantity ? 'All units are booked for this time' : undefined,\n totalCapacity: quantity,\n }\n}\n\nexport async function getAvailableSlots(params: {\n blockingStatuses: string[]\n date: Date\n guestCount?: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: string\n resourceSlug: string\n scheduleSlug: string\n serviceId: string\n serviceSlug: string\n}): Promise<Array<{ end: Date; start: Date }>> {\n const {\n blockingStatuses,\n date,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n scheduleSlug,\n serviceId,\n serviceSlug,\n } = params\n\n // 1. Fetch service for duration + buffer times\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. Fetch resource's schedules for the date\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: resourceId } }, { active: { equals: true } }],\n },\n })\n\n // 3. Resolve schedules to time ranges for the date\n const timeRanges: Array<{ end: Date; start: Date }> = []\n for (const schedule of schedules) {\n const ranges = resolveScheduleForDate(\n schedule as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n )\n timeRanges.push(...ranges)\n }\n\n if (timeRanges.length === 0) {\n return []\n }\n\n // 4. Generate candidate slots from schedule ranges\n const { endTime: slotEndOffset } = computeEndTime({\n durationType,\n serviceDuration: duration,\n startTime: new Date(0),\n })\n const slotDuration = Math.round(slotEndOffset.getTime() / 60_000)\n const effectiveDuration = durationType === 'fixed' ? duration : slotDuration\n\n const availableSlots: Array<{ end: Date; start: Date }> = []\n\n // For full-day services, offer the entire range as a single slot per range\n if (durationType === 'full-day') {\n for (const range of timeRanges) {\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter: 0,\n bufferBefore: 0,\n endTime: range.end,\n guestCount: guestCount ?? 1,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n startTime: range.start,\n })\n if (result.available) {\n availableSlots.push({ end: range.end, start: range.start })\n }\n }\n return availableSlots\n }\n\n // Step by a smaller increment to catch slots between buffer gaps\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) {break}\n\n // 5. Check availability for each candidate slot\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime: candidateEnd,\n guestCount: guestCount ?? 1,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n startTime: candidateStart,\n })\n\n if (result.available) {\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":["resolveScheduleForDate","addMinutes","computeBlockedWindow","computeEndTime","params","durationType","serviceDuration","startTime","end","Date","setHours","durationMinutes","Math","round","getTime","endTime","buildOverlapQuery","blockingStatuses","effectiveEnd","effectiveStart","excludeReservationId","resourceId","conditions","resource","equals","status","in","less_than","toISOString","greater_than","push","id","not_equals","and","isBlockingStatus","statusMachine","includes","validateTransition","fromStatus","toStatus","allowed","transitions","reason","valid","checkAvailability","bufferAfter","bufferBefore","guestCount","payload","req","reservationSlug","resourceSlug","findByID","collection","depth","quantity","capacityMode","where","docs","find","limit","select","currentGuests","reduce","sum","doc","available","currentCount","undefined","totalCapacity","totalDocs","count","getAvailableSlots","date","scheduleSlug","serviceId","serviceSlug","service","duration","bufferTimeBefore","bufferTimeAfter","schedules","active","timeRanges","schedule","ranges","length","slotEndOffset","slotDuration","effectiveDuration","availableSlots","range","result","start","stepSize","min","candidateStart","candidateEnd"],"mappings":"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"}
@@ -9,7 +9,8 @@ export type ResolvedItem = {
9
9
  * Normalize reservation data into a list of resource-level items.
10
10
  *
11
11
  * - If items[] is populated -> return items (filling defaults from parent).
12
- * Items missing startTime or resource are filtered out.
12
+ * Items missing startTime or resource throw a ValidationError.
13
+ * Duplicate (resource, startTime) pairs throw a ValidationError.
13
14
  * - If items[] is empty/absent -> return single item from top-level fields
14
15
  *
15
16
  * Every downstream function (conflict check, endTime calc, availability)
@@ -1,8 +1,10 @@
1
+ import { ValidationError } from 'payload';
1
2
  /**
2
3
  * Normalize reservation data into a list of resource-level items.
3
4
  *
4
5
  * - If items[] is populated -> return items (filling defaults from parent).
5
- * Items missing startTime or resource are filtered out.
6
+ * Items missing startTime or resource throw a ValidationError.
7
+ * Duplicate (resource, startTime) pairs throw a ValidationError.
6
8
  * - If items[] is empty/absent -> return single item from top-level fields
7
9
  *
8
10
  * Every downstream function (conflict check, endTime calc, availability)
@@ -10,13 +12,53 @@
10
12
  */ export function resolveReservationItems(data) {
11
13
  const items = data.items;
12
14
  if (items && items.length > 0) {
13
- return items.map((item)=>({
15
+ const resolved = [];
16
+ const seen = new Set();
17
+ for(let i = 0; i < items.length; i++){
18
+ const item = items[i];
19
+ const resource = extractId(item.resource) || extractId(data.resource) || '';
20
+ const startTime = item.startTime ?? data.startTime;
21
+ if (!resource) {
22
+ throw new ValidationError({
23
+ errors: [
24
+ {
25
+ message: `Item ${i} is missing a resource`,
26
+ path: `items.${i}.resource`
27
+ }
28
+ ]
29
+ });
30
+ }
31
+ if (!startTime) {
32
+ throw new ValidationError({
33
+ errors: [
34
+ {
35
+ message: `Item ${i} is missing a startTime`,
36
+ path: `items.${i}.startTime`
37
+ }
38
+ ]
39
+ });
40
+ }
41
+ const key = `${resource}::${startTime}`;
42
+ if (seen.has(key)) {
43
+ throw new ValidationError({
44
+ errors: [
45
+ {
46
+ message: `Duplicate booking: item ${i} has the same resource and startTime as a previous item`,
47
+ path: `items.${i}.startTime`
48
+ }
49
+ ]
50
+ });
51
+ }
52
+ seen.add(key);
53
+ resolved.push({
14
54
  endTime: item.endTime ?? data.endTime,
15
55
  guestCount: item.guestCount ?? data.guestCount ?? 1,
16
- resource: extractId(item.resource) || extractId(data.resource) || '',
56
+ resource,
17
57
  service: extractId(item.service) || extractId(data.service) || undefined,
18
- startTime: item.startTime ?? data.startTime
19
- })).filter((item)=>Boolean(item.resource) && Boolean(item.startTime));
58
+ startTime
59
+ });
60
+ }
61
+ return resolved;
20
62
  }
21
63
  // Single-resource fallback (current behavior)
22
64
  if (!data.resource || !data.startTime) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utilities/resolveReservationItems.ts"],"sourcesContent":["export type ResolvedItem = {\n endTime: string\n guestCount: number\n resource: string\n service?: string\n startTime: string\n}\n\n/**\n * Normalize reservation data into a list of resource-level items.\n *\n * - If items[] is populated -> return items (filling defaults from parent).\n * Items missing startTime or resource are filtered out.\n * - If items[] is empty/absent -> return single item from top-level fields\n *\n * Every downstream function (conflict check, endTime calc, availability)\n * works with ResolvedItem[], never with raw reservation data.\n */\nexport function resolveReservationItems(data: Record<string, unknown>): ResolvedItem[] {\n const items = data.items as Array<Record<string, unknown>> | undefined\n\n if (items && items.length > 0) {\n return items\n .map((item) => ({\n endTime: (item.endTime as string) ?? (data.endTime as string),\n guestCount: (item.guestCount as number) ?? (data.guestCount as number) ?? 1,\n resource: extractId(item.resource) || extractId(data.resource) || '',\n service: extractId(item.service) || extractId(data.service) || undefined,\n startTime: (item.startTime as string) ?? (data.startTime as string),\n }))\n .filter((item) => Boolean(item.resource) && Boolean(item.startTime))\n }\n\n // Single-resource fallback (current behavior)\n if (!data.resource || !data.startTime) {\n return []\n }\n\n return [\n {\n endTime: data.endTime as string,\n guestCount: (data.guestCount as number) ?? 1,\n resource: extractId(data.resource) || '',\n service: extractId(data.service) || undefined,\n startTime: data.startTime as string,\n },\n ]\n}\n\nfunction extractId(value: unknown): string | undefined {\n if (typeof value === 'string' && value) {\n return value\n }\n if (value && typeof value === 'object' && 'id' in value) {\n return (value as { id: string }).id\n }\n return undefined\n}\n"],"names":["resolveReservationItems","data","items","length","map","item","endTime","guestCount","resource","extractId","service","undefined","startTime","filter","Boolean","value","id"],"mappings":"AAQA;;;;;;;;;CASC,GACD,OAAO,SAASA,wBAAwBC,IAA6B;IACnE,MAAMC,QAAQD,KAAKC,KAAK;IAExB,IAAIA,SAASA,MAAMC,MAAM,GAAG,GAAG;QAC7B,OAAOD,MACJE,GAAG,CAAC,CAACC,OAAU,CAAA;gBACdC,SAAS,AAACD,KAAKC,OAAO,IAAgBL,KAAKK,OAAO;gBAClDC,YAAY,AAACF,KAAKE,UAAU,IAAgBN,KAAKM,UAAU,IAAe;gBAC1EC,UAAUC,UAAUJ,KAAKG,QAAQ,KAAKC,UAAUR,KAAKO,QAAQ,KAAK;gBAClEE,SAASD,UAAUJ,KAAKK,OAAO,KAAKD,UAAUR,KAAKS,OAAO,KAAKC;gBAC/DC,WAAW,AAACP,KAAKO,SAAS,IAAgBX,KAAKW,SAAS;YAC1D,CAAA,GACCC,MAAM,CAAC,CAACR,OAASS,QAAQT,KAAKG,QAAQ,KAAKM,QAAQT,KAAKO,SAAS;IACtE;IAEA,8CAA8C;IAC9C,IAAI,CAACX,KAAKO,QAAQ,IAAI,CAACP,KAAKW,SAAS,EAAE;QACrC,OAAO,EAAE;IACX;IAEA,OAAO;QACL;YACEN,SAASL,KAAKK,OAAO;YACrBC,YAAY,AAACN,KAAKM,UAAU,IAAe;YAC3CC,UAAUC,UAAUR,KAAKO,QAAQ,KAAK;YACtCE,SAASD,UAAUR,KAAKS,OAAO,KAAKC;YACpCC,WAAWX,KAAKW,SAAS;QAC3B;KACD;AACH;AAEA,SAASH,UAAUM,KAAc;IAC/B,IAAI,OAAOA,UAAU,YAAYA,OAAO;QACtC,OAAOA;IACT;IACA,IAAIA,SAAS,OAAOA,UAAU,YAAY,QAAQA,OAAO;QACvD,OAAO,AAACA,MAAyBC,EAAE;IACrC;IACA,OAAOL;AACT"}
1
+ {"version":3,"sources":["../../src/utilities/resolveReservationItems.ts"],"sourcesContent":["import { ValidationError } from 'payload'\n\nexport type ResolvedItem = {\n endTime: string\n guestCount: number\n resource: string\n service?: string\n startTime: string\n}\n\n/**\n * Normalize reservation data into a list of resource-level items.\n *\n * - If items[] is populated -> return items (filling defaults from parent).\n * Items missing startTime or resource throw a ValidationError.\n * Duplicate (resource, startTime) pairs throw a ValidationError.\n * - If items[] is empty/absent -> return single item from top-level fields\n *\n * Every downstream function (conflict check, endTime calc, availability)\n * works with ResolvedItem[], never with raw reservation data.\n */\nexport function resolveReservationItems(data: Record<string, unknown>): ResolvedItem[] {\n const items = data.items as Array<Record<string, unknown>> | undefined\n\n if (items && items.length > 0) {\n const resolved: ResolvedItem[] = []\n const seen = new Set<string>()\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n const resource = extractId(item.resource) || extractId(data.resource) || ''\n const startTime = (item.startTime as string) ?? (data.startTime as string)\n\n if (!resource) {\n throw new ValidationError({\n errors: [\n {\n message: `Item ${i} is missing a resource`,\n path: `items.${i}.resource`,\n },\n ],\n })\n }\n\n if (!startTime) {\n throw new ValidationError({\n errors: [\n {\n message: `Item ${i} is missing a startTime`,\n path: `items.${i}.startTime`,\n },\n ],\n })\n }\n\n const key = `${resource}::${startTime}`\n if (seen.has(key)) {\n throw new ValidationError({\n errors: [\n {\n message: `Duplicate booking: item ${i} has the same resource and startTime as a previous item`,\n path: `items.${i}.startTime`,\n },\n ],\n })\n }\n seen.add(key)\n\n resolved.push({\n endTime: (item.endTime as string) ?? (data.endTime as string),\n guestCount: (item.guestCount as number) ?? (data.guestCount as number) ?? 1,\n resource,\n service: extractId(item.service) || extractId(data.service) || undefined,\n startTime,\n })\n }\n\n return resolved\n }\n\n // Single-resource fallback (current behavior)\n if (!data.resource || !data.startTime) {\n return []\n }\n\n return [\n {\n endTime: data.endTime as string,\n guestCount: (data.guestCount as number) ?? 1,\n resource: extractId(data.resource) || '',\n service: extractId(data.service) || undefined,\n startTime: data.startTime as string,\n },\n ]\n}\n\nfunction extractId(value: unknown): string | undefined {\n if (typeof value === 'string' && value) {\n return value\n }\n if (value && typeof value === 'object' && 'id' in value) {\n return (value as { id: string }).id\n }\n return undefined\n}\n"],"names":["ValidationError","resolveReservationItems","data","items","length","resolved","seen","Set","i","item","resource","extractId","startTime","errors","message","path","key","has","add","push","endTime","guestCount","service","undefined","value","id"],"mappings":"AAAA,SAASA,eAAe,QAAQ,UAAS;AAUzC;;;;;;;;;;CAUC,GACD,OAAO,SAASC,wBAAwBC,IAA6B;IACnE,MAAMC,QAAQD,KAAKC,KAAK;IAExB,IAAIA,SAASA,MAAMC,MAAM,GAAG,GAAG;QAC7B,MAAMC,WAA2B,EAAE;QACnC,MAAMC,OAAO,IAAIC;QAEjB,IAAK,IAAIC,IAAI,GAAGA,IAAIL,MAAMC,MAAM,EAAEI,IAAK;YACrC,MAAMC,OAAON,KAAK,CAACK,EAAE;YACrB,MAAME,WAAWC,UAAUF,KAAKC,QAAQ,KAAKC,UAAUT,KAAKQ,QAAQ,KAAK;YACzE,MAAME,YAAY,AAACH,KAAKG,SAAS,IAAgBV,KAAKU,SAAS;YAE/D,IAAI,CAACF,UAAU;gBACb,MAAM,IAAIV,gBAAgB;oBACxBa,QAAQ;wBACN;4BACEC,SAAS,CAAC,KAAK,EAAEN,EAAE,sBAAsB,CAAC;4BAC1CO,MAAM,CAAC,MAAM,EAAEP,EAAE,SAAS,CAAC;wBAC7B;qBACD;gBACH;YACF;YAEA,IAAI,CAACI,WAAW;gBACd,MAAM,IAAIZ,gBAAgB;oBACxBa,QAAQ;wBACN;4BACEC,SAAS,CAAC,KAAK,EAAEN,EAAE,uBAAuB,CAAC;4BAC3CO,MAAM,CAAC,MAAM,EAAEP,EAAE,UAAU,CAAC;wBAC9B;qBACD;gBACH;YACF;YAEA,MAAMQ,MAAM,GAAGN,SAAS,EAAE,EAAEE,WAAW;YACvC,IAAIN,KAAKW,GAAG,CAACD,MAAM;gBACjB,MAAM,IAAIhB,gBAAgB;oBACxBa,QAAQ;wBACN;4BACEC,SAAS,CAAC,wBAAwB,EAAEN,EAAE,uDAAuD,CAAC;4BAC9FO,MAAM,CAAC,MAAM,EAAEP,EAAE,UAAU,CAAC;wBAC9B;qBACD;gBACH;YACF;YACAF,KAAKY,GAAG,CAACF;YAETX,SAASc,IAAI,CAAC;gBACZC,SAAS,AAACX,KAAKW,OAAO,IAAgBlB,KAAKkB,OAAO;gBAClDC,YAAY,AAACZ,KAAKY,UAAU,IAAgBnB,KAAKmB,UAAU,IAAe;gBAC1EX;gBACAY,SAASX,UAAUF,KAAKa,OAAO,KAAKX,UAAUT,KAAKoB,OAAO,KAAKC;gBAC/DX;YACF;QACF;QAEA,OAAOP;IACT;IAEA,8CAA8C;IAC9C,IAAI,CAACH,KAAKQ,QAAQ,IAAI,CAACR,KAAKU,SAAS,EAAE;QACrC,OAAO,EAAE;IACX;IAEA,OAAO;QACL;YACEQ,SAASlB,KAAKkB,OAAO;YACrBC,YAAY,AAACnB,KAAKmB,UAAU,IAAe;YAC3CX,UAAUC,UAAUT,KAAKQ,QAAQ,KAAK;YACtCY,SAASX,UAAUT,KAAKoB,OAAO,KAAKC;YACpCX,WAAWV,KAAKU,SAAS;QAC3B;KACD;AACH;AAEA,SAASD,UAAUa,KAAc;IAC/B,IAAI,OAAOA,UAAU,YAAYA,OAAO;QACtC,OAAOA;IACT;IACA,IAAIA,SAAS,OAAOA,UAAU,YAAY,QAAQA,OAAO;QACvD,OAAO,AAACA,MAAyBC,EAAE;IACrC;IACA,OAAOF;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-reserve",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A Payload CMS 3.x plugin for reservation and booking management with conflict detection, status workflows, and calendar UI",
5
5
  "keywords": [
6
6
  "payload",
@@ -46,14 +46,15 @@
46
46
  "dist"
47
47
  ],
48
48
  "devDependencies": {
49
+ "@changesets/cli": "^2.30.0",
49
50
  "@eslint/eslintrc": "^3.2.0",
50
51
  "@payloadcms/db-mongodb": "3.77.0",
51
52
  "@payloadcms/db-postgres": "3.77.0",
52
53
  "@payloadcms/db-sqlite": "3.77.0",
53
54
  "@payloadcms/eslint-config": "3.9.0",
54
- "@payloadcms/translations": "3.77.0",
55
55
  "@payloadcms/next": "3.77.0",
56
56
  "@payloadcms/richtext-lexical": "3.77.0",
57
+ "@payloadcms/translations": "3.77.0",
57
58
  "@payloadcms/ui": "3.77.0",
58
59
  "@playwright/test": "1.56.1",
59
60
  "@swc-node/register": "1.10.9",
@@ -104,6 +105,8 @@
104
105
  "generate:types": "pnpm dev:generate-types",
105
106
  "lint": "eslint",
106
107
  "lint:fix": "eslint ./src --fix",
108
+ "changeset": "changeset",
109
+ "changeset:version": "changeset version",
107
110
  "test": "pnpm test:int && pnpm test:e2e",
108
111
  "test:e2e": "playwright test",
109
112
  "test:int": "vitest"