payload-reserve 2.3.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +218 -4
  2. package/dist/collections/Holds.d.ts +18 -0
  3. package/dist/collections/Holds.js +113 -0
  4. package/dist/collections/Holds.js.map +1 -0
  5. package/dist/collections/Reservations.js +8 -0
  6. package/dist/collections/Reservations.js.map +1 -1
  7. package/dist/collections/Resources.js +35 -1
  8. package/dist/collections/Resources.js.map +1 -1
  9. package/dist/collections/Services.js +41 -0
  10. package/dist/collections/Services.js.map +1 -1
  11. package/dist/components/CalendarView/LaneTimelineView.js +4 -5
  12. package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
  13. package/dist/components/CalendarView/index.js +55 -71
  14. package/dist/components/CalendarView/index.js.map +1 -1
  15. package/dist/components/DashboardWidget/DashboardWidgetServer.js +11 -74
  16. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  17. package/dist/components/DashboardWidget/fetchDashboardStats.d.ts +42 -0
  18. package/dist/components/DashboardWidget/fetchDashboardStats.js +113 -0
  19. package/dist/components/DashboardWidget/fetchDashboardStats.js.map +1 -0
  20. package/dist/defaults.d.ts +2 -0
  21. package/dist/defaults.js +12 -0
  22. package/dist/defaults.js.map +1 -1
  23. package/dist/endpoints/cancelBooking.js +43 -14
  24. package/dist/endpoints/cancelBooking.js.map +1 -1
  25. package/dist/endpoints/checkAvailability.js +27 -1
  26. package/dist/endpoints/checkAvailability.js.map +1 -1
  27. package/dist/endpoints/createBooking.js +108 -7
  28. package/dist/endpoints/createBooking.js.map +1 -1
  29. package/dist/endpoints/customerSearch.js +6 -0
  30. package/dist/endpoints/customerSearch.js.map +1 -1
  31. package/dist/endpoints/effectiveTimezone.js +1 -0
  32. package/dist/endpoints/effectiveTimezone.js.map +1 -1
  33. package/dist/endpoints/getSlots.js +27 -1
  34. package/dist/endpoints/getSlots.js.map +1 -1
  35. package/dist/endpoints/holdSlot.d.ts +12 -0
  36. package/dist/endpoints/holdSlot.js +126 -0
  37. package/dist/endpoints/holdSlot.js.map +1 -0
  38. package/dist/endpoints/releaseSlot.d.ts +8 -0
  39. package/dist/endpoints/releaseSlot.js +34 -0
  40. package/dist/endpoints/releaseSlot.js.map +1 -0
  41. package/dist/endpoints/resourceAvailability.d.ts +4 -0
  42. package/dist/endpoints/resourceAvailability.js +105 -4
  43. package/dist/endpoints/resourceAvailability.js.map +1 -1
  44. package/dist/hooks/holds/validateHoldSlot.d.ts +14 -0
  45. package/dist/hooks/holds/validateHoldSlot.js +90 -0
  46. package/dist/hooks/holds/validateHoldSlot.js.map +1 -0
  47. package/dist/hooks/reservations/acquireBookingLock.d.ts +30 -0
  48. package/dist/hooks/reservations/acquireBookingLock.js +71 -0
  49. package/dist/hooks/reservations/acquireBookingLock.js.map +1 -0
  50. package/dist/hooks/reservations/calculateEndTime.js +13 -1
  51. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  52. package/dist/hooks/reservations/expandRequiredResources.js +3 -0
  53. package/dist/hooks/reservations/expandRequiredResources.js.map +1 -1
  54. package/dist/hooks/reservations/validateActive.d.ts +11 -0
  55. package/dist/hooks/reservations/validateActive.js +103 -0
  56. package/dist/hooks/reservations/validateActive.js.map +1 -0
  57. package/dist/hooks/reservations/validateConflicts.js +22 -1
  58. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  59. package/dist/hooks/reservations/validateGuestBooking.js +3 -0
  60. package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
  61. package/dist/hooks/shared/preventDeleteWhenReferenced.d.ts +42 -0
  62. package/dist/hooks/shared/preventDeleteWhenReferenced.js +87 -0
  63. package/dist/hooks/shared/preventDeleteWhenReferenced.js.map +1 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/plugin.js +179 -3
  67. package/dist/plugin.js.map +1 -1
  68. package/dist/services/AvailabilityService.d.ts +51 -5
  69. package/dist/services/AvailabilityService.js +315 -38
  70. package/dist/services/AvailabilityService.js.map +1 -1
  71. package/dist/services/HoldService.d.ts +53 -0
  72. package/dist/services/HoldService.js +169 -0
  73. package/dist/services/HoldService.js.map +1 -0
  74. package/dist/services/index.d.ts +1 -0
  75. package/dist/services/index.js.map +1 -1
  76. package/dist/translations/ar.json +5 -1
  77. package/dist/translations/de.json +5 -1
  78. package/dist/translations/en.json +5 -1
  79. package/dist/translations/es.json +5 -1
  80. package/dist/translations/fa.json +5 -1
  81. package/dist/translations/fr.json +5 -1
  82. package/dist/translations/hi.json +5 -1
  83. package/dist/translations/id.json +5 -1
  84. package/dist/translations/pl.json +5 -1
  85. package/dist/translations/ru.json +5 -1
  86. package/dist/translations/tr.json +5 -1
  87. package/dist/translations/zh.json +5 -1
  88. package/dist/types.d.ts +32 -0
  89. package/dist/types.js.map +1 -1
  90. package/dist/utilities/calendarGrid.d.ts +44 -0
  91. package/dist/utilities/calendarGrid.js +67 -0
  92. package/dist/utilities/calendarGrid.js.map +1 -0
  93. package/dist/utilities/reservationChanges.d.ts +6 -1
  94. package/dist/utilities/reservationChanges.js +1 -1
  95. package/dist/utilities/reservationChanges.js.map +1 -1
  96. package/dist/utilities/reserveDebug.d.ts +26 -0
  97. package/dist/utilities/reserveDebug.js +35 -0
  98. package/dist/utilities/reserveDebug.js.map +1 -0
  99. package/dist/utilities/resolveReservationItems.d.ts +13 -2
  100. package/dist/utilities/resolveReservationItems.js +87 -2
  101. package/dist/utilities/resolveReservationItems.js.map +1 -1
  102. package/dist/utilities/retryOnWriteConflict.d.ts +66 -0
  103. package/dist/utilities/retryOnWriteConflict.js +135 -0
  104. package/dist/utilities/retryOnWriteConflict.js.map +1 -0
  105. package/dist/utilities/tenantTimezone.d.ts +77 -1
  106. package/dist/utilities/tenantTimezone.js +163 -2
  107. package/dist/utilities/tenantTimezone.js.map +1 -1
  108. package/dist/utilities/transactionSupport.d.ts +13 -0
  109. package/dist/utilities/transactionSupport.js +30 -0
  110. package/dist/utilities/transactionSupport.js.map +1 -0
  111. package/package.json +13 -13
package/dist/plugin.js CHANGED
@@ -1,5 +1,7 @@
1
+ import { flattenAllFields, getFieldByPath } from 'payload';
1
2
  import { deepMergeSimple } from 'payload/shared';
2
3
  import { createCustomersCollection } from './collections/Customers.js';
4
+ import { createHoldsCollection } from './collections/Holds.js';
3
5
  import { createReservationsCollection } from './collections/Reservations.js';
4
6
  import { createResourcesCollection } from './collections/Resources.js';
5
7
  import { createSchedulesCollection } from './collections/Schedules.js';
@@ -11,10 +13,32 @@ import { createBookingEndpoint } from './endpoints/createBooking.js';
11
13
  import { createCustomerSearchEndpoint } from './endpoints/customerSearch.js';
12
14
  import { createEffectiveTimezoneEndpoint } from './endpoints/effectiveTimezone.js';
13
15
  import { createGetSlotsEndpoint } from './endpoints/getSlots.js';
16
+ import { createHoldSlotEndpoint } from './endpoints/holdSlot.js';
17
+ import { createReleaseSlotEndpoint } from './endpoints/releaseSlot.js';
14
18
  import { createResourceAvailabilityEndpoint } from './endpoints/resourceAvailability.js';
15
19
  import { provisionStaffResource } from './hooks/users/provisionStaffResource.js';
16
20
  import { translations } from './translations/index.js';
17
21
  import { applyCollectionOverride } from './utilities/collectionOverrides.js';
22
+ import { collectionHasTenantField } from './utilities/tenantFilter.js';
23
+ import { supportsTransactions } from './utilities/transactionSupport.js';
24
+ /**
25
+ * The array field `@payloadcms/plugin-multi-tenant` pushes onto the admin users
26
+ * collection to hold a user's tenant memberships. Its own default
27
+ * (`plugin-multi-tenant/dist/defaults.js`); consumers can rename it, which is
28
+ * one of the blind spots the boot diagnostic's comment calls out.
29
+ */ const MT_TENANTS_ARRAY_FIELD = 'tenants';
30
+ /** True if the collection has a top-level `array` field named `fieldName`. */ function collectionHasArrayField(collection, fieldName) {
31
+ if (!collection || !Array.isArray(collection.fields)) {
32
+ return false;
33
+ }
34
+ return collection.fields.some((f)=>{
35
+ if (typeof f !== 'object' || f === null) {
36
+ return false;
37
+ }
38
+ const field = f;
39
+ return field.name === fieldName && field.type === 'array';
40
+ });
41
+ }
18
42
  /**
19
43
  * All named field paths reachable from a field list, descending through
20
44
  * presentational containers (tabs, rows, collapsibles, unnamed groups) that
@@ -125,11 +149,38 @@ export const payloadReserve = (pluginOptions = {})=>(config)=>{
125
149
  // exists, so installs without one don't hit an opaque init error (C8).
126
150
  resolved.hasMediaCollection = config.collections.some((col)=>col.slug === resolved.slugs.media);
127
151
  const ov = resolved.collectionOverrides;
128
- config.collections.push(applyCollectionOverride(createServicesCollection(resolved), ov.services), applyCollectionOverride(createResourcesCollection(resolved), ov.resources), applyCollectionOverride(createSchedulesCollection(resolved), ov.schedules), applyCollectionOverride(createReservationsCollection(resolved), ov.reservations), // The customers override applies only in standalone mode; in userCollection
152
+ const resourcesCollection = applyCollectionOverride(createResourcesCollection(resolved), ov.resources);
153
+ // The join's `on: 'services'` only resolves when Payload's own
154
+ // sanitizeJoinField can resolve it — mirror that resolution exactly
155
+ // (flattenAllFields + getFieldByPath) rather than re-implementing the
156
+ // traversal, so this can't drift from Payload's behavior on upgrade.
157
+ // Unnamed containers (row/collapsible/unnamed group/unnamed tabs) are
158
+ // hoisted by flattenAllFields and still resolve; only removal, renaming,
159
+ // or nesting inside a NAMED group/tab breaks it — that's the case this
160
+ // gate exists to catch instead of crashing init with InvalidFieldJoin.
161
+ const resourceServicesTarget = getFieldByPath({
162
+ // A throwaway copy: flattenAllFields caches by array identity even when
163
+ // `cache` isn't passed, and sanitizeFields mutates-and-returns the same
164
+ // array instance. Flattening resourcesCollection.fields directly would
165
+ // seed that cache with a pre-sanitization snapshot under a key Payload's
166
+ // own sanitizeJoinField (cache: true) could later be served from.
167
+ fields: flattenAllFields({
168
+ fields: [
169
+ ...resourcesCollection.fields
170
+ ]
171
+ }),
172
+ path: 'services'
173
+ });
174
+ resolved.hasResourceServicesField = resourceServicesTarget !== null && (resourceServicesTarget.field.type === 'relationship' || resourceServicesTarget.field.type === 'upload');
175
+ config.collections.push(applyCollectionOverride(createServicesCollection(resolved), ov.services), resourcesCollection, applyCollectionOverride(createSchedulesCollection(resolved), ov.schedules), applyCollectionOverride(createReservationsCollection(resolved), ov.reservations), // The customers override applies only in standalone mode; in userCollection
129
176
  // mode the host owns that collection and can edit it directly.
130
177
  ...resolved.userCollection ? [] : [
131
178
  applyCollectionOverride(createCustomersCollection(resolved), ov.customers)
132
- ]);
179
+ ], // Only when slot holds are enabled — an install that never opts in gets no
180
+ // extra collection and no schema change.
181
+ ...resolved.slotHolds.enabled ? [
182
+ createHoldsCollection(resolved)
183
+ ] : []);
133
184
  // C3: collections are registered (above) even when disabled so the DB schema
134
185
  // stays stable; behavior (hooks, endpoints, admin, provisioning) is inert.
135
186
  if (resolved.disabled) {
@@ -141,11 +192,136 @@ export const payloadReserve = (pluginOptions = {})=>(config)=>{
141
192
  }
142
193
  return config;
143
194
  }
195
+ // Boot diagnostics. ONE wrapper for all of them so a second condition can
196
+ // never double-wrap onInit. Each check is evaluated at init, not here:
197
+ // payloadReserve runs BEFORE multi-tenant, so at plugin time no tenant
198
+ // field exists on anything yet.
199
+ const hostOnInit = config.onInit;
200
+ config.onInit = async (payload)=>{
201
+ await hostOnInit?.(payload);
202
+ try {
203
+ // The Services `resources` join is omitted when a collectionOverrides.resources
204
+ // override removed, renamed, or nested `services` inside a NAMED group/tab.
205
+ // hasResourceServicesField is only ever false because of such an override —
206
+ // the default Resources collection always carries the field — so this cannot
207
+ // fire spuriously. Warn rather than throw: restructuring your own collection
208
+ // is a supported customization, not invalid config.
209
+ if (!resolved.hasResourceServicesField) {
210
+ payload.logger.warn(`payload-reserve: the "${resolved.slugs.services}" collection's read-only "resources" join was skipped because "${resolved.slugs.resources}.services" is not a top-level relationship field. A collectionOverrides.resources override removed, renamed, or nested it inside a named group or tab.`);
211
+ }
212
+ // Multi-tenant only scopes collections listed in ITS OWN `collections`
213
+ // option, and stays silent about slugs you never listed. So a consumer
214
+ // can enable multi-tenant, forget these collections, and get no signal
215
+ // while every booking stays globally readable.
216
+ //
217
+ // Detecting "multi-tenant is enabled" is a HEURISTIC — the plugin
218
+ // exposes nothing at init to check directly — so we look for two
219
+ // independent traces of it and warn if EITHER shows up. Each covers the
220
+ // other's blind spot:
221
+ // 1. some collection carries a top-level `tenantField`. Misses the
222
+ // consumer who scoped ONLY this plugin's collections and forgot
223
+ // every one of them, leaving nothing else with the field.
224
+ // 2. an auth collection carries multi-tenant's `tenants` membership
225
+ // array. Misses installs that set
226
+ // `tenantsArrayField.includeDefaultField: false` or renamed it.
227
+ // Neither is exact, and the union can still fire on a config that
228
+ // merely looks tenant-shaped — hence a warning, never a throw.
229
+ const tenantField = resolved.multiTenant.tenantField;
230
+ const all = payload.config.collections ?? [];
231
+ const scoped = all.filter((c)=>collectionHasTenantField(c, tenantField));
232
+ const hasMembershipArray = all.some((c)=>Boolean(c.auth) && collectionHasArrayField(c, MT_TENANTS_ARRAY_FIELD));
233
+ if (scoped.length > 0 || hasMembershipArray) {
234
+ const candidates = [
235
+ resolved.slugs.reservations,
236
+ resolved.slugs.resources,
237
+ resolved.slugs.schedules,
238
+ resolved.slugs.services
239
+ ];
240
+ // Standalone mode only. `customers` is then a plugin-generated auth
241
+ // collection holding the PII that customer-search reads, so leaving it
242
+ // unscoped leaks every tenant's customers — the case most worth
243
+ // catching. Under `userCollection` that slug points at the HOST's auth
244
+ // collection, which multi-tenant scopes via the `tenants` membership
245
+ // array rather than a flat field, so checking it would always
246
+ // false-positive.
247
+ if (!resolved.userCollection) {
248
+ candidates.push(resolved.slugs.customers);
249
+ }
250
+ // Only exists when slot holds are opted in. A hold row carries a
251
+ // bearer `token`: unscoped, a tenant-A staff account with Local-API
252
+ // reach (or any consumer code that reads holds) sees tenant B's
253
+ // tokens, and a token is enough to release B's hold or book its slot.
254
+ if (resolved.slotHolds.enabled) {
255
+ candidates.push(resolved.slugs.holds);
256
+ }
257
+ const unscoped = candidates.filter((slug)=>{
258
+ const collection = all.find((c)=>c.slug === slug);
259
+ return collection && !collectionHasTenantField(collection, tenantField);
260
+ });
261
+ if (unscoped.length > 0) {
262
+ payload.logger.warn(`payload-reserve: these collections are NOT tenant-scoped: ${unscoped.join(', ')}. This config looks like it enables multi-tenancy (another collection carries a "${tenantField}" field, or an auth collection carries a "${MT_TENANTS_ARRAY_FIELD}" membership array) — detection is a heuristic, so disregard this if you are not using multi-tenancy. Otherwise add these slugs to the multi-tenant plugin's "collections" option, or their documents stay readable across tenants.`);
263
+ }
264
+ // createBooking's tenant-membership probe (callerMayUseTenant, in
265
+ // src/utilities/tenantTimezone.ts) is a real membership check only
266
+ // when the caller authenticates against the SAME collection
267
+ // multi-tenant wraps as its admin/tenant-owning collection —
268
+ // `withTenantAccess` only applies its membership constraint under
269
+ // that condition. In standalone mode a customer authenticates
270
+ // against `slugs.customers`, a collection multi-tenant never wraps,
271
+ // so the probe's tenants-collection read passes for ANY tenant id
272
+ // for a logged-in customer: this is detectable (unlike a host
273
+ // setting multi-tenant's own `useTenantsCollectionAccess: false`,
274
+ // which is not observable from here), so warn about it.
275
+ if (!resolved.userCollection) {
276
+ payload.logger.warn(`payload-reserve: in standalone mode (no "userCollection"), /reserve/book's tenant-membership check may not actually verify a logged-in customer's membership — customers authenticate against "${resolved.slugs.customers}", a collection multi-tenant never wraps, so its probe read passes for any tenant id. Detection is the same heuristic as the warning above (a top-level "${tenantField}" field is matched by NAME, not by type or relationship target, and the check cannot see whether "${resolved.slugs.customers}" is itself multi-tenant's admin/tenant-owning collection), so disregard this if you are not using multi-tenancy. Otherwise a malicious customer could supply an explicit foreign tenant: set "userCollection" to point at multi-tenant's own admin auth collection, or add your own tenant-membership check in front of /reserve/book.`);
277
+ }
278
+ // userCollection mode: the host's auth collection is where customer PII
279
+ // lives, and customerSearch reads it. Multi-tenant scopes it via the
280
+ // `tenants` membership ARRAY, so the flat-field check above deliberately
281
+ // skips it — but an install with neither the array nor the flat field is
282
+ // genuinely unscoped and would otherwise get no signal whatsoever.
283
+ if (resolved.userCollection) {
284
+ const userCollection = all.find((c)=>c.slug === resolved.slugs.customers);
285
+ const scopedByArray = userCollection && collectionHasArrayField(userCollection, MT_TENANTS_ARRAY_FIELD);
286
+ const scopedByField = userCollection && collectionHasTenantField(userCollection, tenantField);
287
+ if (userCollection && !scopedByArray && !scopedByField) {
288
+ // The remedy is NOT "add it to multi-tenant's collections option" —
289
+ // confirmed against @payloadcms/plugin-multi-tenant's own source
290
+ // (index.ts): it resolves ONE "admin users" collection (config.admin.user,
291
+ // else the first auth-enabled collection) and pushes the tenants ARRAY
292
+ // onto that one automatically; listing THAT SAME collection in
293
+ // `collections` is actively rejected — multi-tenant logs its own
294
+ // console.warn and skips tenant-field processing for it entirely. Only a
295
+ // SEPARATE auth collection from the admin one can be scoped via
296
+ // `collections` (it gets an ordinary flat tenant field there). This
297
+ // plugin cannot tell which case applies from here, so the message
298
+ // explains both rather than asserting the wrong one.
299
+ payload.logger.warn(`payload-reserve: the "${resolved.slugs.customers}" collection backs userCollection mode and holds the customer PII that /api/reservation-customer-search reads, but it carries neither a "${tenantField}" field nor a "${MT_TENANTS_ARRAY_FIELD}" membership array. Detection is a heuristic, so disregard this if you are not using multi-tenancy, or if you renamed the tenants array field, or set "tenantsArrayField.includeDefaultField: false" — both are supported multi-tenant configurations this check cannot see. Otherwise: multi-tenant does NOT scope an auth collection by listing it in its "collections" option — it rejects that with its own warning and skips the collection entirely. If this is the collection multi-tenant treats as its admin/tenant-owning collection ("admin.user" in your Payload config, or your only auth-enabled collection), it needs the "${MT_TENANTS_ARRAY_FIELD}" array instead, which multi-tenant adds automatically unless disabled. Only a SEPARATE auth collection from that one belongs in the "collections" option, where it gets an ordinary "${tenantField}" field.`);
300
+ }
301
+ }
302
+ }
303
+ // The booking lock only serializes anything inside a transaction. On a
304
+ // standalone mongod Payload skips transactions entirely, so the lock
305
+ // degrades to a no-op and concurrent bookings double-book again — with
306
+ // no error anywhere. Say so at boot; there is no runtime signal.
307
+ if (!resolved.disabled && !await supportsTransactions(payload)) {
308
+ payload.logger.warn('payload-reserve: this database does not support transactions, so concurrent bookings for the same slot can double-book. MongoDB needs a replica set (even single-node) for transaction support. Postgres supports transactions by default. SQLite requires transactionOptions to be set on the adapter, or it silently runs without them.');
309
+ }
310
+ } catch {
311
+ // A diagnostic must never break boot — that is the whole reason these
312
+ // are warnings and not throws. The host onInit await stays OUTSIDE.
313
+ }
314
+ };
144
315
  // Register custom endpoints
145
316
  if (!config.endpoints) {
146
317
  config.endpoints = [];
147
318
  }
148
- config.endpoints.push(createCancelBookingEndpoint(resolved), createCheckAvailabilityEndpoint(resolved), createBookingEndpoint(resolved), createCustomerSearchEndpoint(resolved), createEffectiveTimezoneEndpoint(resolved), createGetSlotsEndpoint(resolved), createResourceAvailabilityEndpoint(resolved));
319
+ config.endpoints.push(createCancelBookingEndpoint(resolved), createCheckAvailabilityEndpoint(resolved), createBookingEndpoint(resolved), createCustomerSearchEndpoint(resolved), createEffectiveTimezoneEndpoint(resolved), createGetSlotsEndpoint(resolved), createResourceAvailabilityEndpoint(resolved), // Only when slot holds are enabled — an install that never opts in gets
320
+ // no new routes.
321
+ ...resolved.slotHolds.enabled ? [
322
+ createHoldSlotEndpoint(resolved),
323
+ createReleaseSlotEndpoint(resolved)
324
+ ] : []);
149
325
  // Wire staff auto-provisioning onto the staff user collection
150
326
  if (resolved.staffProvisioning) {
151
327
  const staffUserSlug = resolved.staffProvisioning.userCollection;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { CollectionSlug, Config, Field } from 'payload'\n\nimport { deepMergeSimple } from 'payload/shared'\n\nimport type { ReservationPluginConfig } from './types.js'\n\nimport { createCustomersCollection } from './collections/Customers.js'\nimport { createReservationsCollection } from './collections/Reservations.js'\nimport { createResourcesCollection } from './collections/Resources.js'\nimport { createSchedulesCollection } from './collections/Schedules.js'\nimport { createServicesCollection } from './collections/Services.js'\nimport { resolveConfig } from './defaults.js'\nimport { createCancelBookingEndpoint } from './endpoints/cancelBooking.js'\nimport { createCheckAvailabilityEndpoint } from './endpoints/checkAvailability.js'\nimport { createBookingEndpoint } from './endpoints/createBooking.js'\nimport { createCustomerSearchEndpoint } from './endpoints/customerSearch.js'\nimport { createEffectiveTimezoneEndpoint } from './endpoints/effectiveTimezone.js'\nimport { createGetSlotsEndpoint } from './endpoints/getSlots.js'\nimport { createResourceAvailabilityEndpoint } from './endpoints/resourceAvailability.js'\nimport { provisionStaffResource } from './hooks/users/provisionStaffResource.js'\nimport { type PluginT, translations } from './translations/index.js'\nimport { applyCollectionOverride } from './utilities/collectionOverrides.js'\n\n/**\n * All named field paths reachable from a field list, descending through\n * presentational containers (tabs, rows, collapsibles, unnamed groups) that\n * don't create their own data nesting — so dedup catches a field declared\n * inside one of them. Named groups/arrays DO nest data, so we don't recurse\n * into them (a `name` inside a named group is a different path).\n */\nfunction collectFieldNames(fields: Field[]): Set<string> {\n const names = new Set<string>()\n const walk = (list: Field[]): void => {\n for (const field of list) {\n if ('name' in field && field.name) {\n names.add(field.name)\n } else if ('tabs' in field && Array.isArray(field.tabs)) {\n for (const tab of field.tabs) {\n if ('name' in tab && tab.name) {\n names.add(tab.name)\n } else if (Array.isArray(tab.fields)) {\n walk(tab.fields)\n }\n }\n } else if ('fields' in field && Array.isArray(field.fields)) {\n // row / collapsible / unnamed group\n walk(field.fields)\n }\n }\n }\n walk(fields)\n return names\n}\n\nexport const payloadReserve =\n (pluginOptions: ReservationPluginConfig = {}) =>\n (config: Config): Config => {\n const resolved = resolveConfig(pluginOptions)\n\n // Detect localization from the Payload config\n if (config.localization) {\n resolved.localized = true\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n if (resolved.userCollection) {\n // Extend the existing auth collection with customer fields\n const targetCollection = config.collections.find(\n (col) => col.slug === resolved.userCollection,\n )\n\n if (!targetCollection) {\n // Fail loudly rather than silently skipping field injection and pointing\n // the customers slug at a collection that doesn't exist (review C2).\n throw new Error(\n `payload-reserve: userCollection \"${resolved.userCollection}\" was not found in config.collections. ` +\n `Define it before payloadReserve() runs, or correct the slug.`,\n )\n }\n\n {\n // Collect existing field names — descend into presentational containers\n // (tabs/rows/collapsibles/groups) so a field nested there isn't\n // re-injected at the top level (review C4).\n const existingFieldNames = collectFieldNames(targetCollection.fields)\n\n // Fields to inject if not already present. `name` is added so that\n // admin.useAsTitle: 'name' works out of the box on the extended user\n // collection (matches the v1.0.0 behaviour documented in README/SKILL).\n // It is NOT required — an existing users collection may have rows\n // without a name, and forcing required would fail their next update (C4).\n const fieldsToAdd: Field[] = [\n {\n name: 'name',\n type: 'text',\n maxLength: 200,\n },\n {\n name: 'phone',\n type: 'text',\n maxLength: 50,\n },\n {\n name: 'notes',\n type: 'textarea',\n },\n {\n name: 'bookings',\n type: 'join',\n collection: resolved.slugs.reservations as unknown as CollectionSlug,\n on: 'customer',\n },\n ]\n\n for (const field of fieldsToAdd) {\n const fieldName = 'name' in field ? field.name : undefined\n if (fieldName && !existingFieldNames.has(fieldName)) {\n targetCollection.fields.push(field)\n }\n }\n }\n\n // Point the customers slug at the user collection so other parts of the\n // plugin (endpoints, hooks) reference the correct collection\n resolved.slugs.customers = resolved.userCollection\n }\n\n // The slugs this plugin is about to register (Customers only in standalone mode)\n const slugsToRegister = [\n resolved.slugs.services,\n resolved.slugs.resources,\n resolved.slugs.schedules,\n resolved.slugs.reservations,\n ...(resolved.userCollection ? [] : [resolved.slugs.customers]),\n ]\n\n // C11: fail with a clear, actionable error on slug collision instead of\n // Payload's generic DuplicateCollection throw.\n for (const slug of slugsToRegister) {\n if (config.collections.some((col) => col.slug === slug)) {\n throw new Error(\n `payload-reserve: a collection with slug \"${slug}\" already exists. ` +\n `Override the plugin's slug via the \\`slugs\\` option.`,\n )\n }\n }\n\n // Image upload fields are added only when the media collection actually\n // exists, so installs without one don't hit an opaque init error (C8).\n resolved.hasMediaCollection = config.collections.some(\n (col) => col.slug === resolved.slugs.media,\n )\n\n const ov = resolved.collectionOverrides\n config.collections.push(\n applyCollectionOverride(createServicesCollection(resolved), ov.services),\n applyCollectionOverride(createResourcesCollection(resolved), ov.resources),\n applyCollectionOverride(createSchedulesCollection(resolved), ov.schedules),\n applyCollectionOverride(createReservationsCollection(resolved), ov.reservations),\n // The customers override applies only in standalone mode; in userCollection\n // mode the host owns that collection and can edit it directly.\n ...(resolved.userCollection\n ? []\n : [applyCollectionOverride(createCustomersCollection(resolved), ov.customers)]),\n )\n\n // C3: collections are registered (above) even when disabled so the DB schema\n // stays stable; behavior (hooks, endpoints, admin, provisioning) is inert.\n if (resolved.disabled) {\n for (const slug of slugsToRegister) {\n const col = config.collections.find((c) => c.slug === slug)\n if (col) {\n delete col.hooks\n }\n }\n return config\n }\n\n // Register custom endpoints\n if (!config.endpoints) {config.endpoints = []}\n config.endpoints.push(\n createCancelBookingEndpoint(resolved),\n createCheckAvailabilityEndpoint(resolved),\n createBookingEndpoint(resolved),\n createCustomerSearchEndpoint(resolved),\n createEffectiveTimezoneEndpoint(resolved),\n createGetSlotsEndpoint(resolved),\n createResourceAvailabilityEndpoint(resolved),\n )\n\n // Wire staff auto-provisioning onto the staff user collection\n if (resolved.staffProvisioning) {\n const staffUserSlug = resolved.staffProvisioning.userCollection\n const staffCollection = config.collections.find((col) => col.slug === staffUserSlug)\n if (!staffCollection) {\n throw new Error(\n `staffProvisioning.userCollection \"${staffUserSlug}\" was not found in config.collections`,\n )\n }\n staffCollection.hooks = {\n ...staffCollection.hooks,\n afterChange: [\n ...(staffCollection.hooks?.afterChange ?? []),\n provisionStaffResource(resolved),\n ],\n }\n }\n\n // Set up admin configuration\n if (!config.admin) {config.admin = {}}\n if (!config.admin.components) {config.admin.components = {}}\n\n // Store slugs and status machine in admin custom for component access\n if (!config.admin.custom) {config.admin.custom = {}}\n config.admin.custom.reservationSlugs = {\n ...resolved.slugs,\n }\n config.admin.custom.reservationStatusMachine = resolved.statusMachine\n config.admin.custom.reservationTenant = resolved.multiTenant\n config.admin.custom.reservationTimezone = resolved.timezone\n\n // Add dashboard widget\n if (!config.admin.dashboard) {\n config.admin.dashboard = { widgets: [] }\n }\n if (!config.admin.dashboard.widgets) {\n config.admin.dashboard.widgets = []\n }\n config.admin.dashboard.widgets.push({\n slug: 'reservation-todays-reservations',\n Component: 'payload-reserve/rsc#DashboardWidgetServer',\n label: ({ t }) => (t as PluginT)('reservation:dashboardTitle'),\n maxWidth: 'large',\n minWidth: 'medium',\n })\n\n // Add availability overview as custom admin view\n if (!config.admin.components.views) {\n config.admin.components.views = {}\n }\n ;(config.admin.components.views as Record<string, unknown>)['reservation-availability'] = {\n Component: 'payload-reserve/client#AvailabilityOverview',\n path: '/reservation-availability',\n }\n\n // Merge plugin translations (user translations take precedence)\n config.i18n = {\n ...(config.i18n ?? {}),\n translations: deepMergeSimple(\n translations,\n (config.i18n?.translations as Record<string, Record<string, unknown>>) ?? {},\n ),\n }\n\n return config\n }\n"],"names":["deepMergeSimple","createCustomersCollection","createReservationsCollection","createResourcesCollection","createSchedulesCollection","createServicesCollection","resolveConfig","createCancelBookingEndpoint","createCheckAvailabilityEndpoint","createBookingEndpoint","createCustomerSearchEndpoint","createEffectiveTimezoneEndpoint","createGetSlotsEndpoint","createResourceAvailabilityEndpoint","provisionStaffResource","translations","applyCollectionOverride","collectFieldNames","fields","names","Set","walk","list","field","name","add","Array","isArray","tabs","tab","payloadReserve","pluginOptions","config","resolved","localization","localized","collections","userCollection","targetCollection","find","col","slug","Error","existingFieldNames","fieldsToAdd","type","maxLength","collection","slugs","reservations","on","fieldName","undefined","has","push","customers","slugsToRegister","services","resources","schedules","some","hasMediaCollection","media","ov","collectionOverrides","disabled","c","hooks","endpoints","staffProvisioning","staffUserSlug","staffCollection","afterChange","admin","components","custom","reservationSlugs","reservationStatusMachine","statusMachine","reservationTenant","multiTenant","reservationTimezone","timezone","dashboard","widgets","Component","label","t","maxWidth","minWidth","views","path","i18n"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { CollectionSlug, Config, Field } from 'payload'\n\nimport { flattenAllFields, getFieldByPath } from 'payload'\nimport { deepMergeSimple } from 'payload/shared'\n\nimport type { ReservationPluginConfig } from './types.js'\n\nimport { createCustomersCollection } from './collections/Customers.js'\nimport { createHoldsCollection } from './collections/Holds.js'\nimport { createReservationsCollection } from './collections/Reservations.js'\nimport { createResourcesCollection } from './collections/Resources.js'\nimport { createSchedulesCollection } from './collections/Schedules.js'\nimport { createServicesCollection } from './collections/Services.js'\nimport { resolveConfig } from './defaults.js'\nimport { createCancelBookingEndpoint } from './endpoints/cancelBooking.js'\nimport { createCheckAvailabilityEndpoint } from './endpoints/checkAvailability.js'\nimport { createBookingEndpoint } from './endpoints/createBooking.js'\nimport { createCustomerSearchEndpoint } from './endpoints/customerSearch.js'\nimport { createEffectiveTimezoneEndpoint } from './endpoints/effectiveTimezone.js'\nimport { createGetSlotsEndpoint } from './endpoints/getSlots.js'\nimport { createHoldSlotEndpoint } from './endpoints/holdSlot.js'\nimport { createReleaseSlotEndpoint } from './endpoints/releaseSlot.js'\nimport { createResourceAvailabilityEndpoint } from './endpoints/resourceAvailability.js'\nimport { provisionStaffResource } from './hooks/users/provisionStaffResource.js'\nimport { type PluginT, translations } from './translations/index.js'\nimport { applyCollectionOverride } from './utilities/collectionOverrides.js'\nimport { collectionHasTenantField } from './utilities/tenantFilter.js'\nimport { supportsTransactions } from './utilities/transactionSupport.js'\n\n/**\n * The array field `@payloadcms/plugin-multi-tenant` pushes onto the admin users\n * collection to hold a user's tenant memberships. Its own default\n * (`plugin-multi-tenant/dist/defaults.js`); consumers can rename it, which is\n * one of the blind spots the boot diagnostic's comment calls out.\n */\nconst MT_TENANTS_ARRAY_FIELD = 'tenants'\n\n/** True if the collection has a top-level `array` field named `fieldName`. */\nfunction collectionHasArrayField(\n collection: { fields?: unknown[] } | null | undefined,\n fieldName: string,\n): boolean {\n if (!collection || !Array.isArray(collection.fields)) {\n return false\n }\n return collection.fields.some((f) => {\n if (typeof f !== 'object' || f === null) {\n return false\n }\n const field = f as { name?: string; type?: string }\n return field.name === fieldName && field.type === 'array'\n })\n}\n\n/**\n * All named field paths reachable from a field list, descending through\n * presentational containers (tabs, rows, collapsibles, unnamed groups) that\n * don't create their own data nesting — so dedup catches a field declared\n * inside one of them. Named groups/arrays DO nest data, so we don't recurse\n * into them (a `name` inside a named group is a different path).\n */\nfunction collectFieldNames(fields: Field[]): Set<string> {\n const names = new Set<string>()\n const walk = (list: Field[]): void => {\n for (const field of list) {\n if ('name' in field && field.name) {\n names.add(field.name)\n } else if ('tabs' in field && Array.isArray(field.tabs)) {\n for (const tab of field.tabs) {\n if ('name' in tab && tab.name) {\n names.add(tab.name)\n } else if (Array.isArray(tab.fields)) {\n walk(tab.fields)\n }\n }\n } else if ('fields' in field && Array.isArray(field.fields)) {\n // row / collapsible / unnamed group\n walk(field.fields)\n }\n }\n }\n walk(fields)\n return names\n}\n\nexport const payloadReserve =\n (pluginOptions: ReservationPluginConfig = {}) =>\n (config: Config): Config => {\n const resolved = resolveConfig(pluginOptions)\n\n // Detect localization from the Payload config\n if (config.localization) {\n resolved.localized = true\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n if (resolved.userCollection) {\n // Extend the existing auth collection with customer fields\n const targetCollection = config.collections.find(\n (col) => col.slug === resolved.userCollection,\n )\n\n if (!targetCollection) {\n // Fail loudly rather than silently skipping field injection and pointing\n // the customers slug at a collection that doesn't exist (review C2).\n throw new Error(\n `payload-reserve: userCollection \"${resolved.userCollection}\" was not found in config.collections. ` +\n `Define it before payloadReserve() runs, or correct the slug.`,\n )\n }\n\n {\n // Collect existing field names — descend into presentational containers\n // (tabs/rows/collapsibles/groups) so a field nested there isn't\n // re-injected at the top level (review C4).\n const existingFieldNames = collectFieldNames(targetCollection.fields)\n\n // Fields to inject if not already present. `name` is added so that\n // admin.useAsTitle: 'name' works out of the box on the extended user\n // collection (matches the v1.0.0 behaviour documented in README/SKILL).\n // It is NOT required — an existing users collection may have rows\n // without a name, and forcing required would fail their next update (C4).\n const fieldsToAdd: Field[] = [\n {\n name: 'name',\n type: 'text',\n maxLength: 200,\n },\n {\n name: 'phone',\n type: 'text',\n maxLength: 50,\n },\n {\n name: 'notes',\n type: 'textarea',\n },\n {\n name: 'bookings',\n type: 'join',\n collection: resolved.slugs.reservations as unknown as CollectionSlug,\n on: 'customer',\n },\n ]\n\n for (const field of fieldsToAdd) {\n const fieldName = 'name' in field ? field.name : undefined\n if (fieldName && !existingFieldNames.has(fieldName)) {\n targetCollection.fields.push(field)\n }\n }\n }\n\n // Point the customers slug at the user collection so other parts of the\n // plugin (endpoints, hooks) reference the correct collection\n resolved.slugs.customers = resolved.userCollection\n }\n\n // The slugs this plugin is about to register (Customers only in standalone mode)\n const slugsToRegister = [\n resolved.slugs.services,\n resolved.slugs.resources,\n resolved.slugs.schedules,\n resolved.slugs.reservations,\n ...(resolved.userCollection ? [] : [resolved.slugs.customers]),\n ]\n\n // C11: fail with a clear, actionable error on slug collision instead of\n // Payload's generic DuplicateCollection throw.\n for (const slug of slugsToRegister) {\n if (config.collections.some((col) => col.slug === slug)) {\n throw new Error(\n `payload-reserve: a collection with slug \"${slug}\" already exists. ` +\n `Override the plugin's slug via the \\`slugs\\` option.`,\n )\n }\n }\n\n // Image upload fields are added only when the media collection actually\n // exists, so installs without one don't hit an opaque init error (C8).\n resolved.hasMediaCollection = config.collections.some(\n (col) => col.slug === resolved.slugs.media,\n )\n\n const ov = resolved.collectionOverrides\n const resourcesCollection = applyCollectionOverride(\n createResourcesCollection(resolved),\n ov.resources,\n )\n // The join's `on: 'services'` only resolves when Payload's own\n // sanitizeJoinField can resolve it — mirror that resolution exactly\n // (flattenAllFields + getFieldByPath) rather than re-implementing the\n // traversal, so this can't drift from Payload's behavior on upgrade.\n // Unnamed containers (row/collapsible/unnamed group/unnamed tabs) are\n // hoisted by flattenAllFields and still resolve; only removal, renaming,\n // or nesting inside a NAMED group/tab breaks it — that's the case this\n // gate exists to catch instead of crashing init with InvalidFieldJoin.\n const resourceServicesTarget = getFieldByPath({\n // A throwaway copy: flattenAllFields caches by array identity even when\n // `cache` isn't passed, and sanitizeFields mutates-and-returns the same\n // array instance. Flattening resourcesCollection.fields directly would\n // seed that cache with a pre-sanitization snapshot under a key Payload's\n // own sanitizeJoinField (cache: true) could later be served from.\n fields: flattenAllFields({ fields: [...resourcesCollection.fields] }),\n path: 'services',\n })\n resolved.hasResourceServicesField =\n resourceServicesTarget !== null &&\n (resourceServicesTarget.field.type === 'relationship' ||\n resourceServicesTarget.field.type === 'upload')\n\n config.collections.push(\n applyCollectionOverride(createServicesCollection(resolved), ov.services),\n resourcesCollection,\n applyCollectionOverride(createSchedulesCollection(resolved), ov.schedules),\n applyCollectionOverride(createReservationsCollection(resolved), ov.reservations),\n // The customers override applies only in standalone mode; in userCollection\n // mode the host owns that collection and can edit it directly.\n ...(resolved.userCollection\n ? []\n : [applyCollectionOverride(createCustomersCollection(resolved), ov.customers)]),\n // Only when slot holds are enabled — an install that never opts in gets no\n // extra collection and no schema change.\n ...(resolved.slotHolds.enabled ? [createHoldsCollection(resolved)] : []),\n )\n\n // C3: collections are registered (above) even when disabled so the DB schema\n // stays stable; behavior (hooks, endpoints, admin, provisioning) is inert.\n if (resolved.disabled) {\n for (const slug of slugsToRegister) {\n const col = config.collections.find((c) => c.slug === slug)\n if (col) {\n delete col.hooks\n }\n }\n return config\n }\n\n // Boot diagnostics. ONE wrapper for all of them so a second condition can\n // never double-wrap onInit. Each check is evaluated at init, not here:\n // payloadReserve runs BEFORE multi-tenant, so at plugin time no tenant\n // field exists on anything yet.\n const hostOnInit = config.onInit\n config.onInit = async (payload) => {\n await hostOnInit?.(payload)\n try {\n // The Services `resources` join is omitted when a collectionOverrides.resources\n // override removed, renamed, or nested `services` inside a NAMED group/tab.\n // hasResourceServicesField is only ever false because of such an override —\n // the default Resources collection always carries the field — so this cannot\n // fire spuriously. Warn rather than throw: restructuring your own collection\n // is a supported customization, not invalid config.\n if (!resolved.hasResourceServicesField) {\n payload.logger.warn(\n `payload-reserve: the \"${resolved.slugs.services}\" collection's read-only \"resources\" join was skipped because \"${resolved.slugs.resources}.services\" is not a top-level relationship field. A collectionOverrides.resources override removed, renamed, or nested it inside a named group or tab.`,\n )\n }\n\n // Multi-tenant only scopes collections listed in ITS OWN `collections`\n // option, and stays silent about slugs you never listed. So a consumer\n // can enable multi-tenant, forget these collections, and get no signal\n // while every booking stays globally readable.\n //\n // Detecting \"multi-tenant is enabled\" is a HEURISTIC — the plugin\n // exposes nothing at init to check directly — so we look for two\n // independent traces of it and warn if EITHER shows up. Each covers the\n // other's blind spot:\n // 1. some collection carries a top-level `tenantField`. Misses the\n // consumer who scoped ONLY this plugin's collections and forgot\n // every one of them, leaving nothing else with the field.\n // 2. an auth collection carries multi-tenant's `tenants` membership\n // array. Misses installs that set\n // `tenantsArrayField.includeDefaultField: false` or renamed it.\n // Neither is exact, and the union can still fire on a config that\n // merely looks tenant-shaped — hence a warning, never a throw.\n const tenantField = resolved.multiTenant.tenantField\n const all = (payload.config.collections ?? []) as Array<{\n auth?: unknown\n fields?: unknown[]\n slug: string\n }>\n const scoped = all.filter((c) => collectionHasTenantField(c, tenantField))\n const hasMembershipArray = all.some(\n (c) => Boolean(c.auth) && collectionHasArrayField(c, MT_TENANTS_ARRAY_FIELD),\n )\n if (scoped.length > 0 || hasMembershipArray) {\n const candidates = [\n resolved.slugs.reservations,\n resolved.slugs.resources,\n resolved.slugs.schedules,\n resolved.slugs.services,\n ]\n // Standalone mode only. `customers` is then a plugin-generated auth\n // collection holding the PII that customer-search reads, so leaving it\n // unscoped leaks every tenant's customers — the case most worth\n // catching. Under `userCollection` that slug points at the HOST's auth\n // collection, which multi-tenant scopes via the `tenants` membership\n // array rather than a flat field, so checking it would always\n // false-positive.\n if (!resolved.userCollection) {\n candidates.push(resolved.slugs.customers)\n }\n // Only exists when slot holds are opted in. A hold row carries a\n // bearer `token`: unscoped, a tenant-A staff account with Local-API\n // reach (or any consumer code that reads holds) sees tenant B's\n // tokens, and a token is enough to release B's hold or book its slot.\n if (resolved.slotHolds.enabled) {\n candidates.push(resolved.slugs.holds)\n }\n const unscoped = candidates.filter((slug) => {\n const collection = all.find((c) => c.slug === slug)\n return collection && !collectionHasTenantField(collection, tenantField)\n })\n if (unscoped.length > 0) {\n payload.logger.warn(\n `payload-reserve: these collections are NOT tenant-scoped: ${unscoped.join(', ')}. This config looks like it enables multi-tenancy (another collection carries a \"${tenantField}\" field, or an auth collection carries a \"${MT_TENANTS_ARRAY_FIELD}\" membership array) — detection is a heuristic, so disregard this if you are not using multi-tenancy. Otherwise add these slugs to the multi-tenant plugin's \"collections\" option, or their documents stay readable across tenants.`,\n )\n }\n\n // createBooking's tenant-membership probe (callerMayUseTenant, in\n // src/utilities/tenantTimezone.ts) is a real membership check only\n // when the caller authenticates against the SAME collection\n // multi-tenant wraps as its admin/tenant-owning collection —\n // `withTenantAccess` only applies its membership constraint under\n // that condition. In standalone mode a customer authenticates\n // against `slugs.customers`, a collection multi-tenant never wraps,\n // so the probe's tenants-collection read passes for ANY tenant id\n // for a logged-in customer: this is detectable (unlike a host\n // setting multi-tenant's own `useTenantsCollectionAccess: false`,\n // which is not observable from here), so warn about it.\n if (!resolved.userCollection) {\n payload.logger.warn(\n `payload-reserve: in standalone mode (no \"userCollection\"), /reserve/book's tenant-membership check may not actually verify a logged-in customer's membership — customers authenticate against \"${resolved.slugs.customers}\", a collection multi-tenant never wraps, so its probe read passes for any tenant id. Detection is the same heuristic as the warning above (a top-level \"${tenantField}\" field is matched by NAME, not by type or relationship target, and the check cannot see whether \"${resolved.slugs.customers}\" is itself multi-tenant's admin/tenant-owning collection), so disregard this if you are not using multi-tenancy. Otherwise a malicious customer could supply an explicit foreign tenant: set \"userCollection\" to point at multi-tenant's own admin auth collection, or add your own tenant-membership check in front of /reserve/book.`,\n )\n }\n\n // userCollection mode: the host's auth collection is where customer PII\n // lives, and customerSearch reads it. Multi-tenant scopes it via the\n // `tenants` membership ARRAY, so the flat-field check above deliberately\n // skips it — but an install with neither the array nor the flat field is\n // genuinely unscoped and would otherwise get no signal whatsoever.\n if (resolved.userCollection) {\n const userCollection = all.find((c) => c.slug === resolved.slugs.customers)\n const scopedByArray =\n userCollection && collectionHasArrayField(userCollection, MT_TENANTS_ARRAY_FIELD)\n const scopedByField =\n userCollection && collectionHasTenantField(userCollection, tenantField)\n\n if (userCollection && !scopedByArray && !scopedByField) {\n // The remedy is NOT \"add it to multi-tenant's collections option\" —\n // confirmed against @payloadcms/plugin-multi-tenant's own source\n // (index.ts): it resolves ONE \"admin users\" collection (config.admin.user,\n // else the first auth-enabled collection) and pushes the tenants ARRAY\n // onto that one automatically; listing THAT SAME collection in\n // `collections` is actively rejected — multi-tenant logs its own\n // console.warn and skips tenant-field processing for it entirely. Only a\n // SEPARATE auth collection from the admin one can be scoped via\n // `collections` (it gets an ordinary flat tenant field there). This\n // plugin cannot tell which case applies from here, so the message\n // explains both rather than asserting the wrong one.\n payload.logger.warn(\n `payload-reserve: the \"${resolved.slugs.customers}\" collection backs userCollection mode and holds the customer PII that /api/reservation-customer-search reads, but it carries neither a \"${tenantField}\" field nor a \"${MT_TENANTS_ARRAY_FIELD}\" membership array. Detection is a heuristic, so disregard this if you are not using multi-tenancy, or if you renamed the tenants array field, or set \"tenantsArrayField.includeDefaultField: false\" — both are supported multi-tenant configurations this check cannot see. Otherwise: multi-tenant does NOT scope an auth collection by listing it in its \"collections\" option — it rejects that with its own warning and skips the collection entirely. If this is the collection multi-tenant treats as its admin/tenant-owning collection (\"admin.user\" in your Payload config, or your only auth-enabled collection), it needs the \"${MT_TENANTS_ARRAY_FIELD}\" array instead, which multi-tenant adds automatically unless disabled. Only a SEPARATE auth collection from that one belongs in the \"collections\" option, where it gets an ordinary \"${tenantField}\" field.`,\n )\n }\n }\n }\n\n // The booking lock only serializes anything inside a transaction. On a\n // standalone mongod Payload skips transactions entirely, so the lock\n // degrades to a no-op and concurrent bookings double-book again — with\n // no error anywhere. Say so at boot; there is no runtime signal.\n if (!resolved.disabled && !(await supportsTransactions(payload))) {\n payload.logger.warn(\n 'payload-reserve: this database does not support transactions, so concurrent bookings for the same slot can double-book. MongoDB needs a replica set (even single-node) for transaction support. Postgres supports transactions by default. SQLite requires transactionOptions to be set on the adapter, or it silently runs without them.',\n )\n }\n } catch {\n // A diagnostic must never break boot — that is the whole reason these\n // are warnings and not throws. The host onInit await stays OUTSIDE.\n }\n }\n\n // Register custom endpoints\n if (!config.endpoints) {config.endpoints = []}\n config.endpoints.push(\n createCancelBookingEndpoint(resolved),\n createCheckAvailabilityEndpoint(resolved),\n createBookingEndpoint(resolved),\n createCustomerSearchEndpoint(resolved),\n createEffectiveTimezoneEndpoint(resolved),\n createGetSlotsEndpoint(resolved),\n createResourceAvailabilityEndpoint(resolved),\n // Only when slot holds are enabled — an install that never opts in gets\n // no new routes.\n ...(resolved.slotHolds.enabled\n ? [createHoldSlotEndpoint(resolved), createReleaseSlotEndpoint(resolved)]\n : []),\n )\n\n // Wire staff auto-provisioning onto the staff user collection\n if (resolved.staffProvisioning) {\n const staffUserSlug = resolved.staffProvisioning.userCollection\n const staffCollection = config.collections.find((col) => col.slug === staffUserSlug)\n if (!staffCollection) {\n throw new Error(\n `staffProvisioning.userCollection \"${staffUserSlug}\" was not found in config.collections`,\n )\n }\n staffCollection.hooks = {\n ...staffCollection.hooks,\n afterChange: [\n ...(staffCollection.hooks?.afterChange ?? []),\n provisionStaffResource(resolved),\n ],\n }\n }\n\n // Set up admin configuration\n if (!config.admin) {config.admin = {}}\n if (!config.admin.components) {config.admin.components = {}}\n\n // Store slugs and status machine in admin custom for component access\n if (!config.admin.custom) {config.admin.custom = {}}\n config.admin.custom.reservationSlugs = {\n ...resolved.slugs,\n }\n config.admin.custom.reservationStatusMachine = resolved.statusMachine\n config.admin.custom.reservationTenant = resolved.multiTenant\n config.admin.custom.reservationTimezone = resolved.timezone\n\n // Add dashboard widget\n if (!config.admin.dashboard) {\n config.admin.dashboard = { widgets: [] }\n }\n if (!config.admin.dashboard.widgets) {\n config.admin.dashboard.widgets = []\n }\n config.admin.dashboard.widgets.push({\n slug: 'reservation-todays-reservations',\n Component: 'payload-reserve/rsc#DashboardWidgetServer',\n label: ({ t }) => (t as PluginT)('reservation:dashboardTitle'),\n maxWidth: 'large',\n minWidth: 'medium',\n })\n\n // Add availability overview as custom admin view\n if (!config.admin.components.views) {\n config.admin.components.views = {}\n }\n ;(config.admin.components.views as Record<string, unknown>)['reservation-availability'] = {\n Component: 'payload-reserve/client#AvailabilityOverview',\n path: '/reservation-availability',\n }\n\n // Merge plugin translations (user translations take precedence)\n config.i18n = {\n ...(config.i18n ?? {}),\n translations: deepMergeSimple(\n translations,\n (config.i18n?.translations as Record<string, Record<string, unknown>>) ?? {},\n ),\n }\n\n return config\n }\n"],"names":["flattenAllFields","getFieldByPath","deepMergeSimple","createCustomersCollection","createHoldsCollection","createReservationsCollection","createResourcesCollection","createSchedulesCollection","createServicesCollection","resolveConfig","createCancelBookingEndpoint","createCheckAvailabilityEndpoint","createBookingEndpoint","createCustomerSearchEndpoint","createEffectiveTimezoneEndpoint","createGetSlotsEndpoint","createHoldSlotEndpoint","createReleaseSlotEndpoint","createResourceAvailabilityEndpoint","provisionStaffResource","translations","applyCollectionOverride","collectionHasTenantField","supportsTransactions","MT_TENANTS_ARRAY_FIELD","collectionHasArrayField","collection","fieldName","Array","isArray","fields","some","f","field","name","type","collectFieldNames","names","Set","walk","list","add","tabs","tab","payloadReserve","pluginOptions","config","resolved","localization","localized","collections","userCollection","targetCollection","find","col","slug","Error","existingFieldNames","fieldsToAdd","maxLength","slugs","reservations","on","undefined","has","push","customers","slugsToRegister","services","resources","schedules","hasMediaCollection","media","ov","collectionOverrides","resourcesCollection","resourceServicesTarget","path","hasResourceServicesField","slotHolds","enabled","disabled","c","hooks","hostOnInit","onInit","payload","logger","warn","tenantField","multiTenant","all","scoped","filter","hasMembershipArray","Boolean","auth","length","candidates","holds","unscoped","join","scopedByArray","scopedByField","endpoints","staffProvisioning","staffUserSlug","staffCollection","afterChange","admin","components","custom","reservationSlugs","reservationStatusMachine","statusMachine","reservationTenant","reservationTimezone","timezone","dashboard","widgets","Component","label","t","maxWidth","minWidth","views","i18n"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import type { Payload, PayloadRequest, Where } from 'payload';
2
2
  import type { CapacityMode, DurationType, GetExternalBusy, StatusMachineConfig } from '../types.js';
3
3
  import type { ResolvedItem } from '../utilities/resolveReservationItems.js';
4
+ import { type ReserveDebug } from '../utilities/reserveDebug.js';
4
5
  /** A window during which a resource is occupied. Reservation/sibling
5
6
  * occupancies are expanded by buffer times; external intervals (from
6
7
  * `getExternalBusy`) are used as-is — only the candidate window itself
@@ -57,7 +58,16 @@ export declare function itemsToOccupancies(params: {
57
58
  items: ResolvedItem[];
58
59
  resourceId: number | string;
59
60
  }): Promise<Occupancy[]>;
60
- /** Occupancy a single fetched reservation imposes on `resourceId`. */
61
+ /**
62
+ * Occupancy a single fetched reservation imposes on `resourceId`.
63
+ *
64
+ * Resolves `reservation` LENIENTLY: this is a stored document, which — unlike
65
+ * a write in progress — can carry an inverted top-level window from a
66
+ * `context.skipReservationHooks` write or data predating that check. Lenient
67
+ * mode skips parent synthesis instead of throwing, so a malformed top-level
68
+ * window never discards the real `items[]` occupancies also present on the
69
+ * same row (see the lenient-mode note in resolveReservationItems.ts).
70
+ */
61
71
  export declare function reservationOccupancies(params: {
62
72
  bufferFor: (serviceId: number | string | undefined) => Promise<{
63
73
  after: number;
@@ -76,11 +86,17 @@ export declare function checkAvailability(params: {
76
86
  blockingStatuses: string[];
77
87
  bufferAfter: number;
78
88
  bufferBefore: number;
89
+ /** Optional tracer — emits check/check_result/error lines when enabled. */
90
+ debug?: ReserveDebug;
79
91
  endTime: Date;
92
+ /** Token of the hold being converted into this booking — never self-blocks. */
93
+ excludeHoldToken?: string;
80
94
  excludeReservationId?: number | string;
81
95
  /** External busy resolver (calendar sync etc.) — intervals block the whole resource. */
82
96
  getExternalBusy?: GetExternalBusy;
83
97
  guestCount: number;
98
+ /** Slug of the slot-holds collection; omit to ignore holds entirely. */
99
+ holdsSlug?: string;
84
100
  payload: Payload;
85
101
  req: PayloadRequest;
86
102
  reservationSlug: string;
@@ -96,12 +112,39 @@ export declare function checkAvailability(params: {
96
112
  reason?: string;
97
113
  totalCapacity: number;
98
114
  }>;
115
+ /**
116
+ * Why availability came back empty. Returned alongside the slots so an empty
117
+ * result is diagnosable without turning on `debug`.
118
+ *
119
+ * `no_active_schedules` and `date_excepted` are deliberately absent: they are
120
+ * per-resource `skip` traces inside a loop, not return points — they funnel
121
+ * into `no_windows` — and stay debug-only.
122
+ *
123
+ * `window_too_short` is reachable only from the general (non-full-day) branch:
124
+ * the full-day branch returns `no_windows` before it, so it always has at least
125
+ * one candidate.
126
+ */
127
+ export type EmptyReason = 'all_slots_taken' | 'empty_intersection' | 'no_resource_ids' | 'no_windows' | 'resource_inactive' | 'service_inactive' | 'window_too_short';
99
128
  export declare function getAvailableSlots(params: {
100
129
  blockingStatuses: string[];
101
130
  date: Date | string;
131
+ /** Optional tracer — emits per-stage slot-generation lines when enabled. */
132
+ debug?: ReserveDebug;
133
+ /** Skip the service/resource `active` short-circuits when explicitly `false`. */
134
+ enforceActive?: boolean;
102
135
  /** External busy resolver (calendar sync etc.) — intervals block the whole resource. */
103
136
  getExternalBusy?: GetExternalBusy;
104
137
  guestCount?: number;
138
+ /**
139
+ * Slug of the slot-holds collection; omit to ignore holds entirely.
140
+ *
141
+ * Load-bearing, not optional polish: without it every read-path caller
142
+ * (`/reserve/availability`, `/reserve/slots`, the reservation form's slot
143
+ * picker) advertises a held slot as FREE, and the customer who clicks it is
144
+ * rejected by the write path — which DOES count holds — as a 409. Pass it
145
+ * whenever `slotHolds.enabled`.
146
+ */
147
+ holdsSlug?: string;
105
148
  payload: Payload;
106
149
  req: PayloadRequest;
107
150
  reservationSlug: string;
@@ -112,7 +155,10 @@ export declare function getAvailableSlots(params: {
112
155
  serviceId: number | string;
113
156
  serviceSlug: string;
114
157
  timeZone?: string;
115
- }): Promise<Array<{
116
- end: Date;
117
- start: Date;
118
- }>>;
158
+ }): Promise<{
159
+ reason?: EmptyReason;
160
+ slots: Array<{
161
+ end: Date;
162
+ start: Date;
163
+ }>;
164
+ }>;