payload-reserve 1.0.3 → 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 +61 -5
- package/dist/collections/Reservations.js +6 -2
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.js +31 -2
- package/dist/collections/Resources.js.map +1 -1
- package/dist/collections/Schedules.js +53 -5
- package/dist/collections/Schedules.js.map +1 -1
- package/dist/collections/Services.js +32 -2
- package/dist/collections/Services.js.map +1 -1
- package/dist/defaults.js +35 -1
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.js +20 -0
- package/dist/endpoints/cancelBooking.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +11 -1
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/customerSearch.js +8 -0
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/getSlots.js +1 -0
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/hooks/reservations/onStatusChange.js +39 -17
- package/dist/hooks/reservations/onStatusChange.js.map +1 -1
- package/dist/hooks/reservations/validateConflicts.js +23 -22
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/hooks/reservations/validateStatusTransition.js +16 -6
- package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +1 -0
- package/dist/services/AvailabilityService.js +30 -4
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/types.d.ts +20 -1
- package/dist/types.js.map +1 -1
- package/dist/utilities/ownerAccess.d.ts +24 -0
- package/dist/utilities/ownerAccess.js +128 -0
- package/dist/utilities/ownerAccess.js.map +1 -0
- package/dist/utilities/resolveReservationItems.d.ts +2 -1
- package/dist/utilities/resolveReservationItems.js +47 -5
- package/dist/utilities/resolveReservationItems.js.map +1 -1
- package/package.json +5 -2
|
@@ -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","
|
|
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:
|
|
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:
|
|
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","
|
|
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":"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"}
|
|
@@ -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;
|
|
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"}
|
|
@@ -1,35 +1,57 @@
|
|
|
1
|
-
export const onStatusChange = (config)=>async ({ doc, previousDoc, req })=>{
|
|
1
|
+
export const onStatusChange = (config)=>async ({ context, doc, previousDoc, req })=>{
|
|
2
|
+
if (context?.skipReservationHooks) {
|
|
3
|
+
return doc;
|
|
4
|
+
}
|
|
2
5
|
if (!previousDoc || previousDoc.status === doc.status) {
|
|
3
6
|
return doc;
|
|
4
7
|
}
|
|
5
8
|
const prev = previousDoc.status;
|
|
6
9
|
const next = doc.status;
|
|
7
|
-
// Call generic afterStatusChange plugin hooks
|
|
8
10
|
if (config.hooks?.afterStatusChange) {
|
|
9
11
|
for (const hook of config.hooks.afterStatusChange){
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
+
}
|
|
16
25
|
}
|
|
17
26
|
}
|
|
18
|
-
// Call specific hooks based on transition
|
|
19
27
|
if (next === 'confirmed' && config.hooks?.afterBookingConfirm) {
|
|
20
28
|
for (const hook of config.hooks.afterBookingConfirm){
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
}
|
|
25
40
|
}
|
|
26
41
|
}
|
|
27
42
|
if (next === 'cancelled' && config.hooks?.afterBookingCancel) {
|
|
28
43
|
for (const hook of config.hooks.afterBookingCancel){
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
}
|
|
33
55
|
}
|
|
34
56
|
}
|
|
35
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 ({ doc, previousDoc, req }) => {\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
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
16
|
+
const nonDefaultStatuses = statusMachine.transitions[defaultStatus] ?? [];
|
|
17
|
+
const allowedOnCreate = isAdmin || hasContextBypass ? [
|
|
13
18
|
defaultStatus,
|
|
14
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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"}
|
|
@@ -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
|
-
|
|
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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"}
|