@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.
- package/README.md +1 -1
- package/package.json +11 -11
- package/src/lib/components/book-meeting-action.js +3 -2
- package/src/lib/components/book-meeting-action.js.map +1 -1
- package/src/lib/components/booking.js +4 -0
- package/src/lib/components/booking.js.map +1 -1
- package/src/lib/components/bookings-console-page.js +62 -4
- package/src/lib/components/bookings-console-page.js.map +1 -1
- package/src/lib/model/booking-in-person.d.ts +72 -0
- package/src/lib/model/booking-in-person.js +71 -0
- package/src/lib/model/booking-in-person.js.map +1 -0
- package/src/lib/model/booking-manage.d.ts +66 -0
- package/src/lib/model/booking-manage.js +90 -0
- package/src/lib/model/booking-manage.js.map +1 -0
- package/src/lib/model/bookings.d.ts +22 -0
- package/src/lib/model/bookings.js +11 -0
- package/src/lib/model/bookings.js.map +1 -1
- package/src/lib/server/booking-manage.d.ts +77 -0
- package/src/lib/server/booking-manage.js +306 -0
- package/src/lib/server/booking-manage.js.map +1 -0
- package/src/lib/server/booking-service-ai-capability.d.ts +50 -0
- package/src/lib/server/booking-service-ai-capability.js +203 -0
- package/src/lib/server/booking-service-ai-capability.js.map +1 -0
- package/src/lib/server/booking-service-drafts.d.ts +107 -0
- package/src/lib/server/booking-service-drafts.js +320 -0
- package/src/lib/server/booking-service-drafts.js.map +1 -0
- package/src/lib/server/in-person-payment.d.ts +20 -0
- package/src/lib/server/in-person-payment.js +469 -0
- package/src/lib/server/in-person-payment.js.map +1 -0
- package/src/lib/server.js +24 -4
- package/src/lib/server.js.map +1 -1
- package/src/lib/tenant-emails.js +59 -0
- package/src/lib/tenant-emails.js.map +1 -1
- package/src/lib/utils/use-booking-purchase-event.js +5 -0
- 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":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,0BAA0B,QAGrB,qDAAoD;AAC3D,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,kBAAkB,QAAQ,qCAAiC;AACpE,SAASC,sBAAsB,QAAQ,4BAAwB;AAC/D,SAASC,4BAA4B,EAAEC,4BAA4B,QAAQ,uBAAmB;AAC9F,SACEC,+BAA+B,EAC/BC,8BAA8B,EAC9BC,wBAAwB,EACxBC,6BAA6B,QACxB,8BAA0B;AAEjC;;;;;;;;;;;;;;;;CAgBC,GAED,0CAA0C,GAC1C,OAAO,MAAMC,wBAAwB,kBAAiB;AAEtD,iFAAiF,GACjF,OAAO,MAAMC,0BAA0B;IAAC;IAAO;IAAO;IAAO;IAAO;IAAO;IAAO;CAAM,CAAS;AAEjG,2CAA2C,GAC3C,SAASC,UAAUC,KAAc;IAC/B,MAAMC,QAAQ,OAAOD,UAAU,WAAWA,MAAME,IAAI,GAAGD,KAAK,CAAC,yBAAyB;IACtF,IAAI,CAACA,OAAO,OAAO;IACnB,MAAME,QAAQC,OAAOH,KAAK,CAAC,EAAE;IAC7B,MAAMI,UAAUD,OAAOH,KAAK,CAAC,EAAE;IAC/B,IAAIE,QAAQ,MAAME,UAAU,MAAOF,UAAU,MAAME,UAAU,GAAI,OAAO;IACxE,OAAOF,QAAQ,KAAKE;AACtB;AAEA;;;;CAIC,GACD,OAAO,SAASC,mCACdC,IAA4B;IAE5B,MAAMC,QAAQT,UAAUQ,IAAI,CAAC,UAAU;IACvC,MAAME,SAASV,UAAUQ,IAAI,CAAC,WAAW;IACzC,MAAMG,SAASF,UAAU,QAAQC,WAAW,OAAO;QAAEE,OAAO,CAAC;QAAGC,KAAK,CAAC;IAAE,IAAI;QAAED,OAAOH;QAAOI,KAAKH;IAAO;IACxG,MAAMI,OAAOC,MAAMC,OAAO,CAACR,IAAI,CAAC,OAAO,IAAKA,IAAI,CAAC,OAAO,GAAyB,EAAE;IACnF,MAAMS,UAAiE,CAAC;IACxE,KAAK,MAAMC,OAAOJ,KAAM;QACtB,MAAMK,UAAU,AAACpB,wBAA8CqB,OAAO,CAACF;QACvE,IAAIC,WAAW,GAAGF,OAAO,CAACE,QAAQ,GAAG;YAACR;SAAO;IAC/C;IACA,MAAMU,SAAS;QAAC;QAAQ;QAAmB;QAAe;QAAY;QAAgB;QAAY;QAAY;KAAa;IAC3H,OAAO,aACFC,OAAOC,WAAW,CAACF,OAAOG,MAAM,CAAC,CAACC,MAAQjB,IAAI,CAACiB,IAAI,KAAKC,WAAWC,GAAG,CAAC,CAACF,MAAQ;YAACA;YAAKjB,IAAI,CAACiB,IAAI;SAAC;QACnGR;;AAEJ;AAEA,OAAO,MAAMW,6BAAiD;IAC5DC,IAAI/B;IACJgC,MAAM;IACNC,OAAO;IACPC,SAAS;QACP;QACA;KACD;IACDC,YAAY;QACVC,MAAM;QACNC,YAAY;YACVC,MAAM;gBACJF,MAAM;gBACNG,aAAa;gBACbC,WAAW1C;YACb;YACA2C,iBAAiB;gBACfL,MAAM;gBACNG,aAAa;gBACbG,SAAS/C;gBACTgD,SAASjD;YACX;YACA6C,aAAa;gBACXH,MAAM;gBACNG,aAAa;gBACbC,WAAW5C;YACb;YACAoB,MAAM;gBACJoB,MAAM;gBACNG,aAAa;gBACbK,OAAO;oBAAER,MAAM;oBAAUS,MAAM5C;gBAAwB;gBACvD6C,UAAU;YACZ;YACAC,SAAS;gBACPX,MAAM;gBACNG,aAAa;gBACbC,WAAW;YACb;YACAQ,UAAU;gBACRZ,MAAM;gBACNG,aAAa;gBACbC,WAAW;YACb;YACAS,UAAU;gBACRb,MAAM;gBACNG,aAAa;gBACbC,WAAW;YACb;YACAU,cAAc;gBACZd,MAAM;gBACNG,aACE,yFACA,4FACA;gBACFM,MAAMpD;YACR;YACA0D,UAAU;gBACRf,MAAM;gBACNG,aAAa;gBACbG,SAAS;gBACTC,SAAS5C;YACX;YACAqD,UAAU;gBACRhB,MAAM;gBACNG,aAAa;gBACbM,MAAMrD;YACR;YACA6D,YAAY;gBACVjB,MAAM;gBACNG,aAAa;gBACbM,MAAMrD;YACR;QACF;QACA8D,UAAU;YAAC;YAAQ;YAAmB;YAAQ;YAAW;YAAY;SAAW;QAChFC,sBAAsB;IACxB;IACAC,YAAY;IACZ,oEAAoE;IACpEC,aAAa;IACbC,SAAS;IACTC,OAAO;IACPC,eAAe/D;IACf,qEAAqE;IACrEgE,iBAAiB,IAAM;IACvBC,SAAS;IACT,2DAA2D;IAC3DC,YAAY;QAAC;KAAU;IACvBC,cAAc,CAACC,OAASxD,mCAAmCwD,KAAKvD,IAAI;AACtE,EAAC;AAED;;;CAGC,GACD,OAAO,SAASwD;IACd5E,2BAA2BwC,4BAA4B;QAAEqC,UAAU5E;IAAU;AAC/E"}
|
|
@@ -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 {};
|