@aglyn/plugins-bookings 1.0.0-beta.229 → 1.0.0-beta.231

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 (35) hide show
  1. package/README.md +1 -1
  2. package/package.json +11 -11
  3. package/src/lib/components/book-meeting-action.js +3 -2
  4. package/src/lib/components/book-meeting-action.js.map +1 -1
  5. package/src/lib/components/booking.js +4 -0
  6. package/src/lib/components/booking.js.map +1 -1
  7. package/src/lib/components/bookings-console-page.js +62 -4
  8. package/src/lib/components/bookings-console-page.js.map +1 -1
  9. package/src/lib/model/booking-in-person.d.ts +72 -0
  10. package/src/lib/model/booking-in-person.js +71 -0
  11. package/src/lib/model/booking-in-person.js.map +1 -0
  12. package/src/lib/model/booking-manage.d.ts +66 -0
  13. package/src/lib/model/booking-manage.js +90 -0
  14. package/src/lib/model/booking-manage.js.map +1 -0
  15. package/src/lib/model/bookings.d.ts +22 -0
  16. package/src/lib/model/bookings.js +11 -0
  17. package/src/lib/model/bookings.js.map +1 -1
  18. package/src/lib/server/booking-manage.d.ts +77 -0
  19. package/src/lib/server/booking-manage.js +306 -0
  20. package/src/lib/server/booking-manage.js.map +1 -0
  21. package/src/lib/server/booking-service-ai-capability.d.ts +50 -0
  22. package/src/lib/server/booking-service-ai-capability.js +203 -0
  23. package/src/lib/server/booking-service-ai-capability.js.map +1 -0
  24. package/src/lib/server/booking-service-drafts.d.ts +107 -0
  25. package/src/lib/server/booking-service-drafts.js +320 -0
  26. package/src/lib/server/booking-service-drafts.js.map +1 -0
  27. package/src/lib/server/in-person-payment.d.ts +20 -0
  28. package/src/lib/server/in-person-payment.js +469 -0
  29. package/src/lib/server/in-person-payment.js.map +1 -0
  30. package/src/lib/server.js +24 -4
  31. package/src/lib/server.js.map +1 -1
  32. package/src/lib/tenant-emails.js +59 -0
  33. package/src/lib/tenant-emails.js.map +1 -1
  34. package/src/lib/utils/use-booking-purchase-event.js +5 -0
  35. package/src/lib/utils/use-booking-purchase-event.js.map +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/server/booking-manage.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport type { PluginApiHandler, PluginApiRequest } from '@aglyn/aglyn/app-utils/api-plugins'\nimport { isEmailConfigured, sendEmail } from '@aglyn/shared-util-email'\nimport {\n firebaseAdmin,\n getOrgForHost,\n hostSendingIdentity,\n meterHostEmail,\n renderHostEmailWithTokens,\n} from '@aglyn/tenant-data-admin'\nimport { getSiteLockdown } from '@aglyn/tenant-data-admin/server/tenant-write-lockdown'\nimport { resolveBrandingProfile } from '@aglyn/aglyn/server'\nimport {\n bookingDurationMs,\n checkInRefusal,\n type ManagedBooking,\n rescheduleRefusal,\n} from '../model/booking-manage'\nimport { type BookedInterval, type HostBookingService, isSlotOpen } from '../model/bookings'\nimport { bookingTimeZone, formatBookingWhen, storedBookingTimeZone } from '../model/booking-time'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\n\n/*\n * Checking a guest in and moving a booking (AGL-3621), for the site's team.\n *\n * The native Aglyn app's bookings calendar calls these; any console surface\n * may too. Each is a member route: the caller's ID token names them, the\n * host's `memberRoles` must give them a content-writing role (the same roles the rules let write\n * a booking directly), and a locked-down site refuses. Each decides inside\n * ONE transaction, re-reading the booking, so two phones acting at once\n * cannot both move it or check in a guest whose booking was just canceled.\n *\n * Moving a booking re-checks the new slot against the service's hours and\n * every other live booking of that service in the same transaction the\n * public booking route uses, so a move can never double-book. It keeps the\n * booking's own length, clears the reminder stamp so the 24-hour reminder\n * goes out for the new time, and tells the guest by email.\n */\n\n/** The Firestore surface these routes use: the Admin SDK's, narrowed so a spec can hand in a double. */\nexport interface ManageDocSnapshot {\n readonly exists: boolean\n readonly id?: string\n get(field: string): unknown\n data(): Record<string, unknown> | undefined\n}\nexport interface ManageDocRef {\n readonly id: string\n}\nexport interface ManageQuery {\n where(field: string, op: '==' | '>=' | '<', value: unknown): ManageQuery\n limit(count: number): ManageQuery\n}\nexport interface ManageTransaction {\n get(ref: ManageDocRef): Promise<ManageDocSnapshot>\n get(query: ManageQuery): Promise<{ docs: ManageDocSnapshot[] }>\n update(ref: ManageDocRef, data: Record<string, unknown>): unknown\n}\nexport interface ManageFirestore {\n doc(path: string): ManageDocRef & { get(): Promise<ManageDocSnapshot> }\n collection(path: string): ManageQuery\n runTransaction<T>(fn: (transaction: ManageTransaction) => Promise<T>): Promise<T>\n}\n\nexport interface RescheduleNotice {\n hostId: string\n bookingId: string\n to: string\n name: string\n serviceName: string\n when: string\n previousWhen: string\n timezone: string\n}\n\nexport interface BookingManageDeps {\n firestore: () => ManageFirestore\n verifyIdToken: (token: string) => Promise<{ uid: string }>\n /** True when the site is locked down and must not take writes. */\n siteLocked: (hostId: string) => Promise<boolean>\n /** The zone a booking is told in when the document carries none. */\n timeZoneFor: (hostId: string, service: HostBookingService | undefined) => Promise<string>\n notifyRescheduled: (notice: RescheduleNotice) => Promise<void>\n now: () => number\n}\n\n/** Field the booking stamps when the guest arrives. */\nexport const CHECKED_IN_AT = 'checkedInAtMs'\nexport const CHECKED_IN_BY = 'checkedInBy'\n\nclass Refusal extends Error {\n constructor(\n readonly status: number,\n message: string,\n ) {\n super(message)\n }\n}\n\nconst bearerOf = (req: PluginApiRequest): string => {\n const header = String(req.headers['authorization'] ?? req.headers['Authorization'] ?? '')\n return header.startsWith('Bearer ') ? header.slice('Bearer '.length) : ''\n}\n\nconst bodyOf = (req: PluginApiRequest): Record<string, unknown> => {\n if (typeof req.body === 'string') {\n try {\n return JSON.parse(req.body) as Record<string, unknown>\n } catch {\n return {}\n }\n }\n return (req.body ?? {}) as Record<string, unknown>\n}\n\n/** The member, their right to write this site's bookings, and the site being open. */\nasync function authorize(\n deps: BookingManageDeps,\n req: PluginApiRequest,\n hostId: string,\n): Promise<{ uid: string }> {\n const token = bearerOf(req)\n if (!token) throw new Refusal(401, 'Unauthenticated')\n let uid: string\n try {\n uid = (await deps.verifyIdToken(token)).uid\n } catch (error) {\n if (!isRefusedIdToken(error)) throw error\n throw new Refusal(401, 'Unauthenticated')\n }\n const host = await deps.firestore().doc(`hosts/${hostId}`).get()\n if (!host.exists) throw new Refusal(404, 'Unknown site')\n const role = ((host.get('memberRoles') ?? {}) as Record<string, unknown>)[uid]\n if (!hostRoleCanWrite(role)) throw new Refusal(403, 'Your role on this site cannot change bookings')\n if (await deps.siteLocked(hostId)) throw new Refusal(423, 'This site is locked and cannot take changes right now')\n return { uid }\n}\n\nfunction answer(res: Parameters<PluginApiHandler>[1], error: unknown): void {\n if (error instanceof Refusal) {\n res.status(error.status).json({ error: error.message })\n return\n }\n console.error('[bookings/manage]', error)\n res.status(500).json({ error: 'That did not go through. Try again.' })\n}\n\nexport function createBookingCheckInHandler(deps: BookingManageDeps): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const body = bodyOf(req)\n const hostId = String(body['hostId'] ?? '')\n const bookingId = String(body['bookingId'] ?? '')\n const checkedIn = body['checkedIn'] !== false\n if (!hostId || !bookingId) return res.status(400).json({ error: 'Missing hostId or bookingId' })\n try {\n const { uid } = await authorize(deps, req, hostId)\n const firestore = deps.firestore()\n const ref = firestore.doc(`hosts/${hostId}/bookings/${bookingId}`)\n const result = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n if (!snapshot.exists) throw new Refusal(404, 'Unknown booking')\n const booking = (snapshot.data() ?? {}) as ManagedBooking\n const nowMs = deps.now()\n const refusal = checkInRefusal(booking, checkedIn, nowMs)\n if (refusal) throw new Refusal(409, refusal)\n const already = Number(booking.checkedInAtMs ?? 0) > 0\n // Already where it was asked to be: a retried tap changes nothing.\n if (already === checkedIn) return { checkedInAtMs: already ? Number(booking.checkedInAtMs) : null }\n if (checkedIn) {\n transaction.update(ref, { [CHECKED_IN_AT]: nowMs, [CHECKED_IN_BY]: uid })\n return { checkedInAtMs: nowMs }\n }\n transaction.update(ref, { [CHECKED_IN_AT]: null, [CHECKED_IN_BY]: null })\n return { checkedInAtMs: null }\n })\n return res.status(200).json({ ok: true, ...result })\n } catch (error) {\n return answer(res, error)\n }\n }\n}\n\nexport function createBookingRescheduleHandler(deps: BookingManageDeps): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const body = bodyOf(req)\n const hostId = String(body['hostId'] ?? '')\n const bookingId = String(body['bookingId'] ?? '')\n const startsAtMs = Number(body['startsAtMs'])\n if (!hostId || !bookingId) return res.status(400).json({ error: 'Missing hostId or bookingId' })\n if (!Number.isFinite(startsAtMs) || startsAtMs <= 0) return res.status(400).json({ error: 'Pick a new time' })\n try {\n const { uid } = await authorize(deps, req, hostId)\n const firestore = deps.firestore()\n const ref = firestore.doc(`hosts/${hostId}/bookings/${bookingId}`)\n const first = await ref.get()\n if (!first.exists) throw new Refusal(404, 'Unknown booking')\n const serviceId = String(first.get('serviceId') ?? '')\n const serviceSnapshot = serviceId ? await firestore.doc(`hosts/${hostId}/services/${serviceId}`).get() : null\n const service = serviceSnapshot?.exists ? (serviceSnapshot.data() as unknown as HostBookingService) : undefined\n if (!service || serviceSnapshot?.get('deletedAt')) {\n throw new Refusal(409, 'This booking’s service no longer exists, so it cannot move')\n }\n\n const moved = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n if (!snapshot.exists) throw new Refusal(404, 'Unknown booking')\n const booking = (snapshot.data() ?? {}) as ManagedBooking & Record<string, unknown>\n const nowMs = deps.now()\n const refusal = rescheduleRefusal(booking, startsAtMs, nowMs)\n if (refusal) throw new Refusal(409, refusal)\n const previousStartsAtMs = Number(booking.startsAtMs ?? 0)\n // Already at that time: a retried move writes nothing and tells no one twice.\n if (previousStartsAtMs === startsAtMs) return null\n const durationMs = bookingDurationMs(booking, service.durationMinutes)\n // The public booking route's own read: every booking of this service\n // from a day before the new time, so one running into it counts.\n const nearby = await transaction.get(\n firestore\n .collection(`hosts/${hostId}/bookings`)\n .where('serviceId', '==', serviceId)\n .where('startsAtMs', '>=', startsAtMs - 24 * 60 * 60_000)\n .limit(500),\n )\n const booked: BookedInterval[] = nearby.docs\n .filter((doc) => doc.id !== bookingId)\n .filter((doc) => {\n const status = doc.get('status')\n if (status === 'canceled') return false\n return !(status === 'pendingPayment' && Number(doc.get('expiresAtMs') ?? 0) < nowMs)\n })\n .map((doc) => ({ startsAtMs: Number(doc.get('startsAtMs') ?? 0), endsAtMs: Number(doc.get('endsAtMs') ?? 0) }))\n if (!isSlotOpen(service, startsAtMs, booked)) throw new Refusal(409, 'That time is not open. Pick another.')\n const endsAtMs = startsAtMs + durationMs\n transaction.update(ref, {\n startsAtMs,\n endsAtMs,\n rescheduledAtMs: nowMs,\n rescheduledBy: uid,\n rescheduledFromMs: previousStartsAtMs,\n // The reminder is owed again, for the new time.\n reminderSentAt: null,\n })\n return { booking, previousStartsAtMs, endsAtMs }\n })\n\n if (!moved) {\n return res.status(200).json({ ok: true, startsAtMs, endsAtMs: Number(first.get('endsAtMs') ?? 0), notified: false })\n }\n const to = String(moved.booking['email'] ?? '').trim()\n let notified = false\n if (to) {\n const timezone = storedBookingTimeZone({ timezone: moved.booking['timezone'] }) ?? (await deps.timeZoneFor(hostId, service))\n try {\n await deps.notifyRescheduled({\n hostId,\n bookingId,\n to,\n name: String(moved.booking['name'] ?? ''),\n serviceName: String(moved.booking['serviceName'] ?? service.name ?? ''),\n when: formatBookingWhen(startsAtMs, timezone),\n previousWhen: formatBookingWhen(moved.previousStartsAtMs, timezone),\n timezone,\n })\n notified = true\n } catch (error) {\n // The booking has moved whatever the mail did; the screen says the guest was not told.\n console.error('[bookings/reschedule] notice failed', hostId, bookingId, error)\n }\n }\n return res.status(200).json({ ok: true, startsAtMs, endsAtMs: moved.endsAtMs, notified })\n } catch (error) {\n return answer(res, error)\n }\n }\n}\n\n/** The email that tells a guest their booking moved, as the confirmation is sent. */\nexport async function sendRescheduledNotice(notice: RescheduleNotice): Promise<void> {\n if (!isEmailConfigured()) throw new Error('Email is not configured')\n const firestore = firebaseAdmin.app().firestore()\n const fallbackText =\n `Hi ${notice.name},\\n\\nYour booking for \"${notice.serviceName}\" is now on ` +\n `${notice.when} (${notice.timezone}). It was on ${notice.previousWhen}.\\n\\n` +\n `Reference: ${notice.bookingId}`\n const designed = await renderHostEmailWithTokens(firestore, notice.hostId, 'booking-rescheduled', {\n name: notice.name,\n 'service.name': notice.serviceName,\n when: notice.when,\n previousWhen: notice.previousWhen,\n timezone: notice.timezone,\n 'booking.ref': notice.bookingId,\n })\n const branding = resolveBrandingProfile((await getOrgForHost(notice.hostId).catch(() => null))?.org as never)\n await sendEmail({\n to: notice.to,\n subject: designed?.subject ?? `New time for ${notice.serviceName}`,\n text: designed?.text || fallbackText,\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: branding.fromName,\n sendingIdentity: await hostSendingIdentity(notice.hostId),\n audience: 'tenant',\n context: 'booking rescheduled',\n owedFor: 'booking',\n })\n await meterHostEmail(notice.hostId)\n}\n\n/** The production wiring: the Admin SDK, the site lockdown verdict and the real send. */\nexport function bookingManageDeps(): BookingManageDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore() as unknown as ManageFirestore,\n verifyIdToken: (token) => firebaseAdmin.app().auth().verifyIdToken(token),\n siteLocked: async (hostId) => Boolean(await getSiteLockdown(hostId)),\n timeZoneFor: async (hostId, service) => {\n const firestore = firebaseAdmin.app().firestore()\n const [host, owner] = await Promise.all([\n firestore.collection('hosts').doc(hostId).get(),\n getOrgForHost(hostId).catch(() => null),\n ])\n return bookingTimeZone({\n service: service ?? null,\n host: host.data() as { timeZone?: string } | undefined,\n org: owner?.org as { timeZone?: string } | null | undefined,\n })\n },\n notifyRescheduled: sendRescheduledNotice,\n now: () => Date.now(),\n }\n}\n"],"names":["hostRoleCanWrite","isEmailConfigured","sendEmail","firebaseAdmin","getOrgForHost","hostSendingIdentity","meterHostEmail","renderHostEmailWithTokens","getSiteLockdown","resolveBrandingProfile","bookingDurationMs","checkInRefusal","rescheduleRefusal","isSlotOpen","bookingTimeZone","formatBookingWhen","storedBookingTimeZone","isRefusedIdToken","CHECKED_IN_AT","CHECKED_IN_BY","Refusal","Error","status","message","bearerOf","req","header","String","headers","startsWith","slice","length","bodyOf","body","JSON","parse","authorize","deps","hostId","host","token","uid","verifyIdToken","error","firestore","doc","get","exists","role","siteLocked","answer","res","json","console","createBookingCheckInHandler","method","bookingId","checkedIn","ref","result","runTransaction","transaction","snapshot","booking","data","nowMs","now","refusal","already","Number","checkedInAtMs","update","ok","createBookingRescheduleHandler","startsAtMs","isFinite","first","moved","serviceId","serviceSnapshot","service","undefined","previousStartsAtMs","durationMs","durationMinutes","nearby","collection","where","limit","booked","docs","filter","id","map","endsAtMs","rescheduledAtMs","rescheduledBy","rescheduledFromMs","reminderSentAt","notified","to","trim","timezone","timeZoneFor","notifyRescheduled","name","serviceName","when","previousWhen","sendRescheduledNotice","notice","app","fallbackText","designed","branding","catch","org","subject","text","html","fromName","sendingIdentity","audience","context","owedFor","bookingManageDeps","auth","Boolean","owner","Promise","all","Date"],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type PluginAiCapability, type PluginAiCapabilityArgs } from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities';
18
+ /**
19
+ * WHAT AN AI BUILD CAN MAKE IN BOOKINGS (AGL-3616): a bookable service, as a
20
+ * draft.
21
+ *
22
+ * The capability the AI plugin's build planner offers for "a way to book
23
+ * me". Its item is written by this plugin's `booking-service` draft writer,
24
+ * so every rule is the writer's: the plan's `bookings` feature and service
25
+ * allowance, the member's role, the shape a service takes. The service is
26
+ * born a draft and takes no bookings until a person activates it on the
27
+ * Bookings page.
28
+ *
29
+ * The arguments are flat, as the contract requires: one set of opening hours
30
+ * on the weekdays named, which `draftContent` turns into the weekly windows
31
+ * the service stores. A page that places the Booking block depends on the
32
+ * service; when the service cannot be made, the page is built without the
33
+ * block (`degrade: 'omit'`).
34
+ */
35
+ /** The operation's name in a build plan. */
36
+ export declare const BOOKING_SERVICE_AI_OP = "booking-service";
37
+ /** Weekday names as the planner writes them, in the model's order (0 = Sunday). */
38
+ export declare const BOOKING_SERVICE_AI_DAYS: readonly ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
39
+ /**
40
+ * The writer's content for an item. Hours that cannot be read are handed on
41
+ * as a window the writer refuses, so the item fails with the writer's own
42
+ * sentence rather than being stored with hours nobody asked for.
43
+ */
44
+ export declare function bookingServiceDraftContentFromArgs(args: PluginAiCapabilityArgs): Readonly<Record<string, unknown>>;
45
+ export declare const bookingServiceAiCapability: PluginAiCapability;
46
+ /**
47
+ * Registers the capability; the console surface calls it beside the writer,
48
+ * since only the console runs AI jobs (AGL-3026). Idempotent.
49
+ */
50
+ export declare function registerBookingServiceAiCapability(): void;
@@ -0,0 +1,203 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { registerPluginAiCapability } from "@aglyn/aglyn/plugin-manager/plugin-ai-capabilities";
18
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
19
+ import { BOOKING_FIELD_ASKS } from "../model/booking-contact-fields.js";
20
+ import { BOOKING_PRICE_DISPLAYS } from "../model/booking-price.js";
21
+ import { BOOKING_MAX_DURATION_MINUTES, BOOKING_MIN_DURATION_MINUTES } from "../model/bookings.js";
22
+ import { BOOKING_SERVICE_DESCRIPTION_MAX, BOOKING_SERVICE_DRAFT_RESOURCE, BOOKING_SERVICE_NAME_MAX, BOOKING_SERVICE_PRICE_MAX_USD } from "./booking-service-drafts.js";
23
+ /**
24
+ * WHAT AN AI BUILD CAN MAKE IN BOOKINGS (AGL-3616): a bookable service, as a
25
+ * draft.
26
+ *
27
+ * The capability the AI plugin's build planner offers for "a way to book
28
+ * me". Its item is written by this plugin's `booking-service` draft writer,
29
+ * so every rule is the writer's: the plan's `bookings` feature and service
30
+ * allowance, the member's role, the shape a service takes. The service is
31
+ * born a draft and takes no bookings until a person activates it on the
32
+ * Bookings page.
33
+ *
34
+ * The arguments are flat, as the contract requires: one set of opening hours
35
+ * on the weekdays named, which `draftContent` turns into the weekly windows
36
+ * the service stores. A page that places the Booking block depends on the
37
+ * service; when the service cannot be made, the page is built without the
38
+ * block (`degrade: 'omit'`).
39
+ */ /** The operation's name in a build plan. */ export const BOOKING_SERVICE_AI_OP = 'booking-service';
40
+ /** Weekday names as the planner writes them, in the model's order (0 = Sunday). */ export const BOOKING_SERVICE_AI_DAYS = [
41
+ 'sun',
42
+ 'mon',
43
+ 'tue',
44
+ 'wed',
45
+ 'thu',
46
+ 'fri',
47
+ 'sat'
48
+ ];
49
+ /** "09:30" → 570; anything else → `null`. */ function minutesOf(value) {
50
+ const match = typeof value === 'string' ? value.trim().match(/^(\d{1,2}):(\d{2})$/) : null;
51
+ if (!match) return null;
52
+ const hours = Number(match[1]);
53
+ const minutes = Number(match[2]);
54
+ if (hours > 24 || minutes > 59 || hours === 24 && minutes > 0) return null;
55
+ return hours * 60 + minutes;
56
+ }
57
+ /**
58
+ * The writer's content for an item. Hours that cannot be read are handed on
59
+ * as a window the writer refuses, so the item fails with the writer's own
60
+ * sentence rather than being stored with hours nobody asked for.
61
+ */ export function bookingServiceDraftContentFromArgs(args) {
62
+ const opens = minutesOf(args['opensAt']);
63
+ const closes = minutesOf(args['closesAt']);
64
+ const window = opens === null || closes === null ? {
65
+ start: -1,
66
+ end: -1
67
+ } : {
68
+ start: opens,
69
+ end: closes
70
+ };
71
+ const days = Array.isArray(args['days']) ? args['days'] : [];
72
+ const windows = {};
73
+ for (const day of days){
74
+ const weekday = BOOKING_SERVICE_AI_DAYS.indexOf(day);
75
+ if (weekday >= 0) windows[weekday] = [
76
+ window
77
+ ];
78
+ }
79
+ const copied = [
80
+ 'name',
81
+ 'durationMinutes',
82
+ 'description',
83
+ 'timezone',
84
+ 'priceDisplay',
85
+ 'priceUsd',
86
+ 'askPhone',
87
+ 'askAddress'
88
+ ];
89
+ return _extends({}, Object.fromEntries(copied.filter((key)=>args[key] !== undefined).map((key)=>[
90
+ key,
91
+ args[key]
92
+ ])), {
93
+ windows
94
+ });
95
+ }
96
+ export const bookingServiceAiCapability = {
97
+ op: BOOKING_SERVICE_AI_OP,
98
+ noun: 'booking service',
99
+ where: 'Bookings → Services',
100
+ intents: [
101
+ 'a service visitors can book online, with its length, weekly hours and price — set up as a draft to activate',
102
+ 'a way for visitors to book an appointment, consultation or estimate visit'
103
+ ],
104
+ argsSchema: {
105
+ type: 'object',
106
+ properties: {
107
+ name: {
108
+ type: 'string',
109
+ description: 'What the service is called on the booking widget, e.g. "Free estimate visit".',
110
+ maxLength: BOOKING_SERVICE_NAME_MAX
111
+ },
112
+ durationMinutes: {
113
+ type: 'integer',
114
+ description: 'How long one booking takes, in minutes.',
115
+ minimum: BOOKING_MIN_DURATION_MINUTES,
116
+ maximum: BOOKING_MAX_DURATION_MINUTES
117
+ },
118
+ description: {
119
+ type: 'string',
120
+ description: 'One or two sentences on what the visitor is booking.',
121
+ maxLength: BOOKING_SERVICE_DESCRIPTION_MAX
122
+ },
123
+ days: {
124
+ type: 'array',
125
+ description: 'The weekdays it can be booked on.',
126
+ items: {
127
+ type: 'string',
128
+ enum: BOOKING_SERVICE_AI_DAYS
129
+ },
130
+ maxItems: 7
131
+ },
132
+ opensAt: {
133
+ type: 'string',
134
+ description: 'When bookable hours start on those days, 24-hour "HH:MM", e.g. "09:00".',
135
+ maxLength: 5
136
+ },
137
+ closesAt: {
138
+ type: 'string',
139
+ description: 'When bookable hours end on those days, 24-hour "HH:MM", e.g. "17:00".',
140
+ maxLength: 5
141
+ },
142
+ timezone: {
143
+ type: 'string',
144
+ description: 'The IANA time zone the hours are in, e.g. "America/Chicago".',
145
+ maxLength: 64
146
+ },
147
+ priceDisplay: {
148
+ type: 'string',
149
+ description: 'How the price is stated: "fixed" charges priceUsd when booked; "varies", "estimate" ' + '(a free estimate) and "contact" show a label and book with no charge. Use "fixed" only ' + 'when the request states the price; otherwise "contact" unless it says otherwise.',
150
+ enum: BOOKING_PRICE_DISPLAYS
151
+ },
152
+ priceUsd: {
153
+ type: 'integer',
154
+ description: 'The price in whole dollars, only with priceDisplay "fixed" and only when the request states it.',
155
+ minimum: 0,
156
+ maximum: BOOKING_SERVICE_PRICE_MAX_USD
157
+ },
158
+ askPhone: {
159
+ type: 'string',
160
+ description: 'Whether the booking form asks for a phone number.',
161
+ enum: BOOKING_FIELD_ASKS
162
+ },
163
+ askAddress: {
164
+ type: 'string',
165
+ description: 'Whether the booking form asks for the address the job is at, for work done on site.',
166
+ enum: BOOKING_FIELD_ASKS
167
+ }
168
+ },
169
+ required: [
170
+ 'name',
171
+ 'durationMinutes',
172
+ 'days',
173
+ 'opensAt',
174
+ 'closesAt',
175
+ 'timezone'
176
+ ],
177
+ additionalProperties: false
178
+ },
179
+ maxPerPlan: 5,
180
+ // Free includes no services (`servicesPerHost: 0`) and no bookings.
181
+ freeAllowed: false,
182
+ feature: 'bookings',
183
+ quota: 'servicesPerHost',
184
+ draftResource: BOOKING_SERVICE_DRAFT_RESOURCE,
185
+ // Written without a model: the planner already filled the arguments.
186
+ estimateCredits: ()=>0,
187
+ degrade: 'omit',
188
+ // A page that books this service places the Booking block.
189
+ pageBlocks: [
190
+ 'booking'
191
+ ],
192
+ draftContent: (item)=>bookingServiceDraftContentFromArgs(item.args)
193
+ };
194
+ /**
195
+ * Registers the capability; the console surface calls it beside the writer,
196
+ * since only the console runs AI jobs (AGL-3026). Idempotent.
197
+ */ export function registerBookingServiceAiCapability() {
198
+ registerPluginAiCapability(bookingServiceAiCapability, {
199
+ pluginId: BUNDLE_ID
200
+ });
201
+ }
202
+
203
+ //# sourceMappingURL=booking-service-ai-capability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/server/booking-service-ai-capability.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n registerPluginAiCapability,\n type PluginAiCapability,\n type PluginAiCapabilityArgs,\n} from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { BOOKING_FIELD_ASKS } from '../model/booking-contact-fields'\nimport { BOOKING_PRICE_DISPLAYS } from '../model/booking-price'\nimport { BOOKING_MAX_DURATION_MINUTES, BOOKING_MIN_DURATION_MINUTES } from '../model/bookings'\nimport {\n BOOKING_SERVICE_DESCRIPTION_MAX,\n BOOKING_SERVICE_DRAFT_RESOURCE,\n BOOKING_SERVICE_NAME_MAX,\n BOOKING_SERVICE_PRICE_MAX_USD,\n} from './booking-service-drafts'\n\n/**\n * WHAT AN AI BUILD CAN MAKE IN BOOKINGS (AGL-3616): a bookable service, as a\n * draft.\n *\n * The capability the AI plugin's build planner offers for \"a way to book\n * me\". Its item is written by this plugin's `booking-service` draft writer,\n * so every rule is the writer's: the plan's `bookings` feature and service\n * allowance, the member's role, the shape a service takes. The service is\n * born a draft and takes no bookings until a person activates it on the\n * Bookings page.\n *\n * The arguments are flat, as the contract requires: one set of opening hours\n * on the weekdays named, which `draftContent` turns into the weekly windows\n * the service stores. A page that places the Booking block depends on the\n * service; when the service cannot be made, the page is built without the\n * block (`degrade: 'omit'`).\n */\n\n/** The operation's name in a build plan. */\nexport const BOOKING_SERVICE_AI_OP = 'booking-service'\n\n/** Weekday names as the planner writes them, in the model's order (0 = Sunday). */\nexport const BOOKING_SERVICE_AI_DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] as const\n\n/** \"09:30\" → 570; anything else → `null`. */\nfunction minutesOf(value: unknown): number | null {\n const match = typeof value === 'string' ? value.trim().match(/^(\\d{1,2}):(\\d{2})$/) : null\n if (!match) return null\n const hours = Number(match[1])\n const minutes = Number(match[2])\n if (hours > 24 || minutes > 59 || (hours === 24 && minutes > 0)) return null\n return hours * 60 + minutes\n}\n\n/**\n * The writer's content for an item. Hours that cannot be read are handed on\n * as a window the writer refuses, so the item fails with the writer's own\n * sentence rather than being stored with hours nobody asked for.\n */\nexport function bookingServiceDraftContentFromArgs(\n args: PluginAiCapabilityArgs,\n): Readonly<Record<string, unknown>> {\n const opens = minutesOf(args['opensAt'])\n const closes = minutesOf(args['closesAt'])\n const window = opens === null || closes === null ? { start: -1, end: -1 } : { start: opens, end: closes }\n const days = Array.isArray(args['days']) ? (args['days'] as readonly string[]) : []\n const windows: Record<number, Array<{ start: number; end: number }>> = {}\n for (const day of days) {\n const weekday = (BOOKING_SERVICE_AI_DAYS as readonly string[]).indexOf(day)\n if (weekday >= 0) windows[weekday] = [window]\n }\n const copied = ['name', 'durationMinutes', 'description', 'timezone', 'priceDisplay', 'priceUsd', 'askPhone', 'askAddress']\n return {\n ...Object.fromEntries(copied.filter((key) => args[key] !== undefined).map((key) => [key, args[key]])),\n windows,\n }\n}\n\nexport const bookingServiceAiCapability: PluginAiCapability = {\n op: BOOKING_SERVICE_AI_OP,\n noun: 'booking service',\n where: 'Bookings → Services',\n intents: [\n 'a service visitors can book online, with its length, weekly hours and price — set up as a draft to activate',\n 'a way for visitors to book an appointment, consultation or estimate visit',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n name: {\n type: 'string',\n description: 'What the service is called on the booking widget, e.g. \"Free estimate visit\".',\n maxLength: BOOKING_SERVICE_NAME_MAX,\n },\n durationMinutes: {\n type: 'integer',\n description: 'How long one booking takes, in minutes.',\n minimum: BOOKING_MIN_DURATION_MINUTES,\n maximum: BOOKING_MAX_DURATION_MINUTES,\n },\n description: {\n type: 'string',\n description: 'One or two sentences on what the visitor is booking.',\n maxLength: BOOKING_SERVICE_DESCRIPTION_MAX,\n },\n days: {\n type: 'array',\n description: 'The weekdays it can be booked on.',\n items: { type: 'string', enum: BOOKING_SERVICE_AI_DAYS },\n maxItems: 7,\n },\n opensAt: {\n type: 'string',\n description: 'When bookable hours start on those days, 24-hour \"HH:MM\", e.g. \"09:00\".',\n maxLength: 5,\n },\n closesAt: {\n type: 'string',\n description: 'When bookable hours end on those days, 24-hour \"HH:MM\", e.g. \"17:00\".',\n maxLength: 5,\n },\n timezone: {\n type: 'string',\n description: 'The IANA time zone the hours are in, e.g. \"America/Chicago\".',\n maxLength: 64,\n },\n priceDisplay: {\n type: 'string',\n description:\n 'How the price is stated: \"fixed\" charges priceUsd when booked; \"varies\", \"estimate\" ' +\n '(a free estimate) and \"contact\" show a label and book with no charge. Use \"fixed\" only ' +\n 'when the request states the price; otherwise \"contact\" unless it says otherwise.',\n enum: BOOKING_PRICE_DISPLAYS,\n },\n priceUsd: {\n type: 'integer',\n description: 'The price in whole dollars, only with priceDisplay \"fixed\" and only when the request states it.',\n minimum: 0,\n maximum: BOOKING_SERVICE_PRICE_MAX_USD,\n },\n askPhone: {\n type: 'string',\n description: 'Whether the booking form asks for a phone number.',\n enum: BOOKING_FIELD_ASKS,\n },\n askAddress: {\n type: 'string',\n description: 'Whether the booking form asks for the address the job is at, for work done on site.',\n enum: BOOKING_FIELD_ASKS,\n },\n },\n required: ['name', 'durationMinutes', 'days', 'opensAt', 'closesAt', 'timezone'],\n additionalProperties: false,\n },\n maxPerPlan: 5,\n // Free includes no services (`servicesPerHost: 0`) and no bookings.\n freeAllowed: false,\n feature: 'bookings',\n quota: 'servicesPerHost',\n draftResource: BOOKING_SERVICE_DRAFT_RESOURCE,\n // Written without a model: the planner already filled the arguments.\n estimateCredits: () => 0,\n degrade: 'omit',\n // A page that books this service places the Booking block.\n pageBlocks: ['booking'],\n draftContent: (item) => bookingServiceDraftContentFromArgs(item.args),\n}\n\n/**\n * Registers the capability; the console surface calls it beside the writer,\n * since only the console runs AI jobs (AGL-3026). Idempotent.\n */\nexport function registerBookingServiceAiCapability(): void {\n registerPluginAiCapability(bookingServiceAiCapability, { pluginId: BUNDLE_ID })\n}\n"],"names":["registerPluginAiCapability","BUNDLE_ID","BOOKING_FIELD_ASKS","BOOKING_PRICE_DISPLAYS","BOOKING_MAX_DURATION_MINUTES","BOOKING_MIN_DURATION_MINUTES","BOOKING_SERVICE_DESCRIPTION_MAX","BOOKING_SERVICE_DRAFT_RESOURCE","BOOKING_SERVICE_NAME_MAX","BOOKING_SERVICE_PRICE_MAX_USD","BOOKING_SERVICE_AI_OP","BOOKING_SERVICE_AI_DAYS","minutesOf","value","match","trim","hours","Number","minutes","bookingServiceDraftContentFromArgs","args","opens","closes","window","start","end","days","Array","isArray","windows","day","weekday","indexOf","copied","Object","fromEntries","filter","key","undefined","map","bookingServiceAiCapability","op","noun","where","intents","argsSchema","type","properties","name","description","maxLength","durationMinutes","minimum","maximum","items","enum","maxItems","opensAt","closesAt","timezone","priceDisplay","priceUsd","askPhone","askAddress","required","additionalProperties","maxPerPlan","freeAllowed","feature","quota","draftResource","estimateCredits","degrade","pageBlocks","draftContent","item","registerBookingServiceAiCapability","pluginId"],"mappings":"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"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type PluginDraftCheck, type PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
18
+ import { type BookingFieldAsk } from '../model/booking-contact-fields';
19
+ import { type BookingPriceDisplay } from '../model/booking-price';
20
+ import { type HostBookingService } from '../model/bookings';
21
+ /**
22
+ * A BOOKING SERVICE ANOTHER PLUGIN ASKS FOR (AGL-3616).
23
+ *
24
+ * The draft writer this plugin registers for the `booking-service` resource
25
+ * on the core's resource-drafts seam. A plugin that sets a site up from a
26
+ * brief asks for the writer by name and gets this plugin's rules:
27
+ *
28
+ * - THE DOCUMENT is a `hosts/{hostId}/services/{id}` service in the shape
29
+ * the console's service dialog saves, born `status: 'draft'`. A draft is
30
+ * offered nowhere — not in the Booking block's list, not by its slots, not
31
+ * by the booking route — until a person activates it on the Bookings page.
32
+ * - THE ROOM is the `servicesPerHost` allowance, counted the way the
33
+ * resources route counts it and inside the transaction that creates, so
34
+ * two writes at once cannot both take the last slot. Free includes none.
35
+ * - THE PLAN is the `bookings` feature, and THE ROLE is a member who may
36
+ * write the site's content — the resources route's two gates, refused in
37
+ * its words.
38
+ * - THE PRICE is a label (`contact`, `varies`, `estimate`) unless the
39
+ * content states a fixed price, so nothing the caller did not say is
40
+ * charged. No price stated and no label asked for is `contact`.
41
+ *
42
+ * Nothing is published or charged. The writer touches the one new document.
43
+ */
44
+ type Firestore = FirebaseFirestore.Firestore;
45
+ /** The resource name this writer is registered under. */
46
+ export declare const BOOKING_SERVICE_DRAFT_RESOURCE = "booking-service";
47
+ /** The longest name the console's dialog keeps. */
48
+ export declare const BOOKING_SERVICE_NAME_MAX = 80;
49
+ /** The longest description the console's dialog keeps. */
50
+ export declare const BOOKING_SERVICE_DESCRIPTION_MAX = 500;
51
+ /** The most open intervals one weekday holds. */
52
+ export declare const BOOKING_SERVICE_WINDOWS_PER_DAY_MAX = 12;
53
+ /** The highest fixed price a draft may state, in whole dollars. */
54
+ export declare const BOOKING_SERVICE_PRICE_MAX_USD = 100000;
55
+ /** The resources route's refusal for a member who may not write the site. */
56
+ export declare const BOOKING_SERVICE_ROLE_REFUSAL = "Editing requires the editor role";
57
+ /** The resources route's refusal for a plan without the feature. */
58
+ export declare const BOOKING_SERVICE_PLAN_REFUSAL = "This feature is not included in your plan \u2014 see Billing";
59
+ /** The resources route's refusal at the plan's allowance. */
60
+ export declare function bookingServiceLimitRefusal(limit: number): string;
61
+ /**
62
+ * What a caller sends as `content`. `windows` is keyed by weekday
63
+ * (0 = Sunday … 6 = Saturday), each a list of `{ start, end }` in minutes
64
+ * since midnight in `timezone`. `priceUsd` is read only with
65
+ * `priceDisplay: 'fixed'`, which needs it.
66
+ */
67
+ export interface BookingServiceDraftContent {
68
+ name: string;
69
+ durationMinutes: number;
70
+ description?: string;
71
+ windows: Partial<Record<number, Array<{
72
+ start: number;
73
+ end: number;
74
+ }>>>;
75
+ timezone: string;
76
+ priceDisplay?: BookingPriceDisplay;
77
+ priceUsd?: number;
78
+ askPhone?: BookingFieldAsk;
79
+ askAddress?: BookingFieldAsk;
80
+ crmFollowUpTask?: boolean;
81
+ crmMeetingActivity?: boolean;
82
+ }
83
+ /** The service document's editable fields, as the writer stores them. */
84
+ export type BookingServiceDraftFields = Required<Pick<HostBookingService, 'name' | 'durationMinutes' | 'windows' | 'timezone' | 'priceDisplay' | 'priceUsd' | 'askPhone' | 'askAddress' | 'crmFollowUpTask' | 'crmMeetingActivity'>> & Pick<HostBookingService, 'description'>;
85
+ export type BookingServiceContentRead = {
86
+ ok: true;
87
+ value: BookingServiceDraftFields;
88
+ } | {
89
+ ok: false;
90
+ problems: string[];
91
+ };
92
+ /** The content as the writer stores it, or every problem that stops it. */
93
+ export declare function readBookingServiceContent(content: Readonly<Record<string, unknown>>): BookingServiceContentRead;
94
+ /** Whether content is a service this plugin would store, with what it says about it. Pure. */
95
+ export declare function checkBookingServiceContent(content: Readonly<Record<string, unknown>>): PluginDraftCheck;
96
+ export interface BookingServiceDraftWriterDeps {
97
+ /** The Admin SDK handle; specs hand in a double. */
98
+ firestore?: () => Firestore;
99
+ }
100
+ export declare function createBookingServiceDraftWriter(deps?: BookingServiceDraftWriterDeps): PluginResourceDraftWriter;
101
+ export declare const bookingServiceDraftWriter: PluginResourceDraftWriter;
102
+ /**
103
+ * Registers the writer; the console surface calls it, since only the console
104
+ * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.
105
+ */
106
+ export declare function registerBookingServiceDraftWriter(): void;
107
+ export {};