@aglyn/plugins-bookings 1.0.0-beta.218 → 1.0.0-beta.220

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 (47) hide show
  1. package/package.json +12 -12
  2. package/src/lib/components/book-meeting-action.d.ts +7 -4
  3. package/src/lib/components/book-meeting-action.js +5 -5
  4. package/src/lib/components/book-meeting-action.js.map +1 -1
  5. package/src/lib/components/booking.js +10 -10
  6. package/src/lib/components/booking.js.map +1 -1
  7. package/src/lib/components/bookings-console-page.js +17 -12
  8. package/src/lib/components/bookings-console-page.js.map +1 -1
  9. package/src/lib/model/booking-record.d.ts +74 -0
  10. package/src/lib/model/booking-record.js +131 -0
  11. package/src/lib/model/booking-record.js.map +1 -0
  12. package/src/lib/model/booking-time.d.ts +60 -0
  13. package/src/lib/model/booking-time.js +65 -0
  14. package/src/lib/model/booking-time.js.map +1 -0
  15. package/src/lib/model/bookings-record-routes.d.ts +46 -0
  16. package/src/lib/model/bookings-record-routes.js +63 -0
  17. package/src/lib/model/bookings-record-routes.js.map +1 -0
  18. package/src/lib/model/bookings-search-sources.d.ts +19 -0
  19. package/src/lib/model/bookings-search-sources.js +36 -0
  20. package/src/lib/model/bookings-search-sources.js.map +1 -0
  21. package/src/lib/model/bookings.d.ts +4 -4
  22. package/src/lib/model/bookings.js +3 -3
  23. package/src/lib/model/bookings.js.map +1 -1
  24. package/src/lib/model/index.d.ts +2 -0
  25. package/src/lib/model/index.js +2 -0
  26. package/src/lib/model/index.js.map +1 -1
  27. package/src/lib/plugin.js +4 -0
  28. package/src/lib/plugin.js.map +1 -1
  29. package/src/lib/server/billing-webhook.js +18 -9
  30. package/src/lib/server/billing-webhook.js.map +1 -1
  31. package/src/lib/server/booking-crm.d.ts +40 -24
  32. package/src/lib/server/booking-crm.js +84 -26
  33. package/src/lib/server/booking-crm.js.map +1 -1
  34. package/src/lib/server/booking-time-zone.d.ts +32 -0
  35. package/src/lib/server/booking-time-zone.js +59 -0
  36. package/src/lib/server/booking-time-zone.js.map +1 -0
  37. package/src/lib/server.js +56 -45
  38. package/src/lib/server.js.map +1 -1
  39. package/src/lib/tenant-emails.d.ts +29 -0
  40. package/src/lib/tenant-emails.js +134 -0
  41. package/src/lib/tenant-emails.js.map +1 -0
  42. package/src/lib/testing/stand-in-crm-record-routes.d.ts +25 -0
  43. package/src/lib/testing/stand-in-crm-record-routes.js +46 -0
  44. package/src/lib/testing/stand-in-crm-record-routes.js.map +1 -0
  45. package/src/lib/testing/stand-in-tax-profile.d.ts +6 -1
  46. package/src/lib/testing/stand-in-tax-profile.js +5 -1
  47. package/src/lib/testing/stand-in-tax-profile.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/components/bookings-console-page.tsx"],"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'use client'\n\nimport { checkQuota, crmContactByEmailHref, pluginDocsHelp } from '@aglyn/aglyn'\nimport { BOOKINGS_BOOKER_PARAM, normalizeContactEmail } from '@aglyn/aglyn'\nimport { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport { describePaymentRisk, type PaymentRisk } from '@aglyn/aglyn/app-utils/payment-risk'\nimport { type ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { type HostBookingService, isBookingReminderDue } from '../model'\nimport { AppLink, CardDisplay, HelpTip, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n FormControlLabel,\n Stack,\n Switch,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n setDoc,\n updateDoc,\n} from 'firebase/firestore'\nimport { useParams, useSearchParams } from 'next/navigation'\nimport { useCallback, useState } from 'react'\nimport {\n useFirestore,\n useFirestoreCollection,\n useHostResourceApi,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\n\nconst WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']\n\n/** \"09:00-12:00, 13:00-17:00\" → open intervals in minutes. */\nfunction parseWindows(input: string): Array<{ start: number; end: number }> {\n const windows: Array<{ start: number; end: number }> = []\n for (const chunk of input.split(',')) {\n const match = chunk.trim().match(/^(\\d{1,2}):(\\d{2})\\s*-\\s*(\\d{1,2}):(\\d{2})$/)\n if (!match) continue\n const start = Number(match[1]) * 60 + Number(match[2])\n const end = Number(match[3]) * 60 + Number(match[4])\n if (end > start && end <= 24 * 60) windows.push({ start, end })\n }\n return windows\n}\n\nfunction formatWindows(\n windows: Array<{ start: number; end: number }> | undefined,\n): string {\n const pad = (minutes: number) =>\n `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(\n minutes % 60,\n ).padStart(2, '0')}`\n return (windows ?? [])\n .map((window) => `${pad(window.start)}-${pad(window.end)}`)\n .join(', ')\n}\n\ninterface ServiceDraft {\n id: string | null\n name: string\n durationMinutes: string\n priceUsd: string\n timezone: string\n description: string\n /** Per-weekday window text, e.g. \"09:00-17:00\". */\n windowText: string[]\n /** The service's two CRM switches (AGL-2660) — see `HostBookingService`. */\n crmMeetingActivity: boolean\n crmFollowUpTask: boolean\n}\n\n/** A fraud warning, review or chargeback on the booking's payment (AGL-3360). */\nfunction PaymentRiskChip({ risk }: { risk: PaymentRisk | undefined }) {\n const described = describePaymentRisk(risk)\n if (!described) return null\n return (\n <Tooltip title={described.detail}>\n <Chip label={described.label} size=\"small\" color=\"warning\" />\n </Tooltip>\n )\n}\n\n/**\n * Bookings manager (AGL-159): bookable services with weekly availability\n * windows, and the upcoming-bookings list with cancel. Visitors book via\n * the org /api/bookings endpoints; confirmations email through the\n * env-gated Resend path. Plan-gated (`bookings` flag + `servicesPerHost`).\n */\nexport function BookingsConsolePage(props: ConsolePluginPageProps) {\n const { hostId, entitled, org } = props\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The id token the refund route authenticates with (AGL-2315).\n const { data: user } = useUser()\n /*\n * Where a booker's CONTACT is (AGL-2622). A booking updated a person in\n * the CRM by the address it carried, and the row links there by that\n * address — the Contacts list holds the id nothing here does and opens\n * the record on one match. The address comes from the SHARED builder\n * rather than the CRM plugin, the way the commerce dialog's does: a\n * plugin importing a sibling is a cycle waiting for its third edge, and\n * the shared builder is pinned against the CRM's own routes by a spec.\n * `null` until the route params settle, so no row renders a link to\n * nowhere.\n */\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const siteContext =\n params?.orgSlug && params?.host\n ? { orgSlug: String(params.orgSlug), host: String(params.host) }\n : null\n /*\n * The list narrowed to one booker (AGL-2660): a contact's record links\n * here with `?email=`, and the page answers with every booking that\n * address holds — past ones too, because that link asks about a\n * person's history rather than the week ahead. The same match rule as\n * the row's own \"View in CRM\": the address, normalized, equal. `null`\n * outside a router, where there is no query to read.\n */\n const searchParams = useSearchParams()\n const bookerFilter = normalizeContactEmail(searchParams?.get(BOOKINGS_BOOKER_PARAM))\n const { basePath } = props\n\n const {\n data: serviceDocs,\n status: servicesStatus,\n /**\n * The rows this editor is seeded from are unconfirmed by the server\n * (AGL-1358). Editing copies a whole stored service into `draft` and\n * writes all of it back, so `merge: true` protects nothing. `windows` is\n * the weekly availability map, rebuilt in full on every save: a cached\n * seed re-opens slots that were closed and closes ones customers can\n * already book, and `priceUsd` goes back to whatever that snapshot\n * charged.\n */\n fromCache: servicesFromCache,\n } = useFirestoreCollection<any>(\n () =>\n query(collection(firestore, 'hosts', hostId, 'services'), limit(100)),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { data: bookingDocs } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'bookings'),\n orderBy('startsAtMs', 'desc'),\n limit(100),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const services = [...(serviceDocs ?? [])]\n .filter((service: any) => !service.deletedAt)\n .sort((a: any, b: any) =>\n String(a.name ?? '').localeCompare(String(b.name ?? '')),\n )\n const upcoming = [...(bookingDocs ?? [])]\n .filter((booking: any) => booking.endsAtMs >= Date.now())\n .sort((a: any, b: any) => a.startsAtMs - b.startsAtMs)\n // Newest first when narrowed to a booker — the listener already orders\n // by start, descending — so the history reads down from the latest.\n const shown = bookerFilter\n ? (bookingDocs ?? []).filter(\n (booking: any) => normalizeContactEmail(booking.email) === bookerFilter,\n )\n : upcoming\n\n /**\n * The 24-hour reminder queue (AGL-2431).\n *\n * Reminders are sent by the `booking-reminders` beat, not by anything on\n * this page, and until that beat existed they were never sent at all — the\n * feature had a designed email template, white-label branding and an\n * `x-cron-secret` door, and no scheduler pointed at it. So the merchant had\n * no way to tell working from broken, and still would not if this card\n * merely asserted that reminders happen.\n *\n * READ-ONLY, and deliberately no \"send now\" button: the beat runs hourly\n * and stamps `reminderSentAt`, so a manual sender would race it for a\n * reminder nobody can un-send. Same call AGL-2227 made for the recovery\n * queue.\n *\n * `isBookingReminderDue` is the sender's own predicate, against one instant\n * shared by both counts, so this is the queue the next pass will drain\n * rather than an approximation of it.\n */\n const reminderNowMs = Date.now()\n const reminderQueue = (bookingDocs ?? []).filter((booking: any) =>\n isBookingReminderDue(booking, reminderNowMs),\n ).length\n const remindersSent = (bookingDocs ?? []).filter(\n (booking: any) => booking.reminderSentAt,\n ).length\n\n const [draft, setDraft] = useState<ServiceDraft | null>(null)\n\n const handleAdd = useCallback(() => {\n if (!entitled) {\n return void enqueueSnackbar(\n 'Bookings require a Starter plan — see Billing to upgrade',\n { variant: 'warning', persist: false },\n )\n }\n const quota = checkQuota(org, 'servicesPerHost', services.length)\n if (!quota.allowed) {\n return void enqueueSnackbar(\n `Service limit reached (${quota.limit}) — upgrade in Billing`,\n { variant: 'warning', persist: false },\n )\n }\n setDraft({\n id: null,\n name: '',\n durationMinutes: '30',\n priceUsd: '0',\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',\n description: '',\n windowText: WEEKDAYS.map((_, index) =>\n index >= 1 && index <= 5 ? '09:00-17:00' : '',\n ),\n // A booking is a meeting the record has; the follow-up is opt-in.\n crmMeetingActivity: true,\n crmFollowUpTask: false,\n })\n }, [entitled, org, services.length, enqueueSnackbar])\n\n const handleSave = useCallback(async () => {\n if (!draft || !draft.name.trim()) return\n const windows: HostBookingService['windows'] = {}\n draft.windowText.forEach((text, weekday) => {\n const parsed = parseWindows(text)\n if (parsed.length) windows[weekday] = parsed\n })\n const fields = {\n name: draft.name.trim().slice(0, 80),\n durationMinutes: Math.max(\n 5,\n Math.min(480, Math.round(Number(draft.durationMinutes) || 30)),\n ),\n priceUsd: Math.max(0, Math.round(Number(draft.priceUsd) || 0)),\n timezone: draft.timezone.trim() || 'UTC',\n ...(draft.description.trim() && {\n description: draft.description.trim().slice(0, 500),\n }),\n windows,\n // Written explicitly rather than only when on: an edit that switches\n // the meeting off has to land a `false`, and `merge: true` would keep\n // an absent key exactly as it was.\n crmMeetingActivity: draft.crmMeetingActivity,\n crmFollowUpTask: draft.crmFollowUpTask,\n }\n try {\n if (draft.id) {\n /**\n * Edit stays client-direct (no quota consumed) — and is refused when\n * its seed was never confirmed by the server (AGL-1358). `fields` is\n * every editable key of the stored service, `windows` included, so\n * `merge: true` has nothing left to protect.\n *\n * The guard WRAPS the write. An early return is a shape you can keep\n * while losing the protection; here the write is only reachable\n * through the verdict.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'service',\n unreadable: servicesStatus === 'error',\n fromCache: servicesFromCache,\n },\n async () => {\n await setDoc(\n doc(firestore, 'hosts', hostId, 'services', draft.id),\n { ...fields, updatedAt: Timestamp.now() },\n { merge: true },\n )\n },\n )\n // Before `setDraft(null)`, so a refusal keeps the dialog open with\n // every window that was typed. A save that silently does nothing\n // sends the user back to retype a form that will be refused again\n // just as quietly.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n } else {\n // New service rides the quota-enforcing resources API (AGL-473) —\n // it also re-checks the `bookings` entitlement server-side.\n await createHostResource({ hostId, resource: 'service', data: fields })\n }\n setDraft(null)\n enqueueSnackbar('Service saved', { variant: 'success', persist: false })\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [\n draft,\n firestore,\n hostId,\n createHostResource,\n enqueueSnackbar,\n servicesStatus,\n servicesFromCache,\n ])\n\n const handleDeleteService = useCallback(\n (service: any) => async () => {\n const confirmed = await confirm({\n title: 'Delete this service?',\n description: `\"${service.name}\" stops accepting bookings.`,\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(doc(firestore, 'hosts', hostId, 'services', service.$id), {\n deletedAt: Timestamp.now(),\n })\n },\n [confirm, firestore, hostId],\n )\n\n /**\n * Cancelling a PAID booking refunds the guest (AGL-2315).\n *\n * This used to be one `updateDoc` writing `status: 'canceled'`, for a paid\n * booking exactly as for a free one. The slot reopened, the appointment was\n * gone, and the guest had paid for it — the money moved nowhere and nothing\n * in the console said so. That was survivable only while the charge settled\n * in Aglyn's own balance and could be given back by hand; a paid booking is\n * a destination charge now, so the funds are at the MERCHANT and there is no\n * later opportunity to notice.\n *\n * So a paid booking cancels through the refund route, which reverses the\n * merchant's share and Aglyn's fee, and which sets `canceled` itself once\n * the money is actually back. A FAILED refund deliberately leaves the\n * booking standing: a cancelled-and-unrefunded appointment is the worst of\n * the three outcomes, and an admin who sees the row still there knows the\n * cancel did not happen.\n */\n const handleCancelBooking = useCallback(\n (booking: any) => async () => {\n const paidCents = Math.max(0, Number(booking.paidAmountCents ?? 0))\n const refundedCents = Math.max(0, Number(booking.refundedCents ?? 0))\n const outstandingCents = Math.max(0, paidCents - refundedCents)\n const refundable = outstandingCents > 0\n const confirmed = await confirm({\n title: 'Cancel this booking?',\n description:\n `${booking.name} (${booking.email}) — the slot reopens.` +\n (refundable\n ? ` $${(outstandingCents / 100).toFixed(2)} is refunded to them` +\n ' through Stripe.'\n : ''),\n confirmationText: refundable ? 'Cancel and refund' : 'Cancel booking',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n\n if (refundable) {\n // `randomUUID` needs a secure context, which the console always is,\n // but fall back rather than throw on a refund.\n const attemptKey =\n globalThis.crypto?.randomUUID?.() ??\n `${Date.now()}-${Math.random().toString(36).slice(2)}`\n try {\n const response = await authorizedFetch(user, '/api/bookings/refund', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Idempotency-Key': attemptKey,\n },\n body: JSON.stringify({ hostId, bookingId: booking.$id }),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n // A 409 is a guard REFUSING with a body that says what to do\n // instead — a booking paid before the PaymentIntent was recorded\n // names the Stripe dashboard — not the refund machinery failing.\n // It surfaces verbatim, the AGL-1818 rule.\n enqueueSnackbar(payload?.error ?? 'Refund failed', {\n variant: response.status === 409 ? 'warning' : 'error',\n allowDuplicate: true,\n })\n // The booking stays. See the note above: cancelled-and-unrefunded\n // is worse than not cancelled.\n return\n }\n // The route wrote `canceled` itself once the money was back.\n enqueueSnackbar('Booking canceled and refunded', {\n variant: 'success',\n persist: false,\n })\n return\n } catch {\n enqueueSnackbar('Refund failed — the booking was not canceled', {\n variant: 'error',\n allowDuplicate: true,\n })\n return\n }\n }\n\n await updateDoc(\n doc(firestore, 'hosts', hostId, 'bookings', booking.$id),\n { status: 'canceled' },\n )\n enqueueSnackbar('Booking canceled', {\n variant: 'success',\n persist: false,\n })\n },\n [confirm, firestore, hostId, enqueueSnackbar, user],\n )\n\n return (\n <Stack spacing={3}>\n <CardDisplay\n header={'Services'}\n help={pluginDocsHelp('bookings', {\n anchor: '#set-up-bookings',\n excerpt:\n 'The bookable services this site offers — how long each takes, ' +\n 'what it costs, and the hours it can be booked in.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {services.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Define a bookable service — duration, price, and weekly ' +\n 'availability — then visitors can book from your site.'}\n </Typography>\n ) : (\n services.map((service: any) => (\n <Stack\n key={service.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {service.name}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`${service.durationMinutes} min` +\n (Number(service.priceUsd) > 0\n ? ` · $${service.priceUsd}`\n : ' · free') +\n ` · ${service.timezone ?? 'UTC'}`}\n </Typography>\n </Stack>\n <Button\n size=\"small\"\n onClick={() =>\n setDraft({\n id: service.$id,\n name: service.name ?? '',\n durationMinutes: String(service.durationMinutes ?? 30),\n priceUsd: String(service.priceUsd ?? 0),\n timezone: service.timezone ?? 'UTC',\n description: service.description ?? '',\n windowText: WEEKDAYS.map((_, index) =>\n formatWindows(service.windows?.[index]),\n ),\n // Absent reads as ON for the meeting and OFF for the\n // follow-up — the model's defaults, seeded so the\n // switches show what the service actually does.\n crmMeetingActivity: service.crmMeetingActivity !== false,\n crmFollowUpTask: service.crmFollowUpTask === true,\n })\n }\n >\n {'Edit'}\n </Button>\n <Button\n size=\"small\"\n color=\"error\"\n onClick={handleDeleteService(service)}\n >\n {'Delete'}\n </Button>\n </Stack>\n ))\n )}\n <Button\n size=\"small\"\n color=\"primary\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={handleAdd}\n >\n {'Add service'}\n </Button>\n {/* The cap, standing rather than only on refusal (AGL-2113).\n `services.length` is the same count `handleAdd` hands to\n `checkQuota`, so the readout and the gate cannot disagree. */}\n <QuotaReadoutComponent\n ready={org != null}\n used={services.length}\n limit={checkQuota(org, 'servicesPerHost', services.length).limit}\n noun=\"service\"\n />\n </Stack>\n </CardDisplay>\n\n <CardDisplay\n header={bookerFilter ? `Bookings for ${bookerFilter}` : 'Upcoming bookings'}\n help={pluginDocsHelp('bookings', {\n anchor: '#manage',\n excerpt:\n 'Bookings taken on this site, with the customer and the slot each ' +\n 'one holds.',\n })}\n contentGutterX\n contentGutterY\n >\n {bookerFilter ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n component=\"div\"\n sx={{ mb: 1 }}\n >\n {'Every booking this address holds, newest first. '}\n {basePath ? <AppLink href={basePath}>{'Show upcoming bookings'}</AppLink> : null}\n </Typography>\n ) : null}\n {/* What the reminder beat will do next, and what it has done. Named\n \"24-hour reminders\" rather than \"emails\" because that is the only\n mail this queue governs — confirmations go out on booking. */}\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n component=\"div\"\n sx={{ mb: 1 }}\n >\n {`24-hour reminders · ${reminderQueue} due in the next pass · ` +\n `${remindersSent} already sent`}\n {/* Its OWN heading, not the card's `#manage`: a merchant reading\n this line wants the timing and the once-only rule, and landing\n them on \"manage your bookings\" is the presence-not-correctness\n failure a help-coverage check cannot see. */}\n <HelpTip\n {...pluginDocsHelp('bookings', { anchor: '#reminders' })}\n sx={{ ml: 0.5, fontSize: '0.9em' }}\n />\n </Typography>\n {shown.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {bookerFilter ? 'No bookings for this address.' : 'No upcoming bookings.'}\n </Typography>\n ) : (\n <Stack spacing={1}>\n {shown.map((booking: any) => (\n <Stack\n key={booking.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {`${booking.serviceName} — ${booking.name}`}\n {booking.status === 'canceled' ? ' (canceled)' : ''}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`${new Date(booking.startsAtMs).toLocaleString()} · ${\n booking.email\n }`}\n </Typography>\n </Stack>\n <PaymentRiskChip risk={booking.paymentRisk} />\n {siteContext && booking.email ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={crmContactByEmailHref(siteContext, String(booking.email))}\n size=\"small\"\n >\n {'View in CRM'}\n </Button>\n ) : null}\n {booking.status !== 'canceled' ? (\n <Button\n size=\"small\"\n color=\"error\"\n onClick={handleCancelBooking(booking)}\n >\n {'Cancel'}\n </Button>\n ) : null}\n </Stack>\n ))}\n </Stack>\n )}\n </CardDisplay>\n\n <Dialog\n open={Boolean(draft)}\n onClose={() => setDraft(null)}\n maxWidth=\"sm\"\n fullWidth\n >\n <DialogTitle>{draft?.id ? 'Edit service' : 'Add service'}</DialogTitle>\n <DialogContent\n sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, pt: 1 }}\n >\n <TextField\n label=\"Name\"\n value={draft?.name ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, name: event.target.value } : prev,\n )\n }\n size=\"small\"\n autoFocus\n sx={{ mt: 1 }}\n />\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Duration (minutes)\"\n value={draft?.durationMinutes ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n durationMinutes: event.target.value.replace(\n /[^0-9]/g,\n '',\n ),\n }\n : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Price (USD, 0 = free)\"\n // WHAT THIS PRICE DOES NOT INCLUDE (AGL-2028).\n //\n // A paid booking is charged exactly this figure: the handler\n // sends no tax parameter at all, by a stated decision (AGL-2000)\n // — whether a service is taxable is jurisdiction-specific and\n // frequently the opposite answer from goods, so applying the\n // commerce plugin's GOODS sales rate would be confidently wrong\n // rather than merely absent.\n //\n // That decision was recorded in the source and in the\n // `storefrontTaxCollected` ledger, and in neither place can the\n // merchant see it. A service business is one of the three named\n // ICPs, and one who configured a rate in Commerce → Settings →\n // Taxes has every reason to assume it covers the appointments\n // they sell on the same site. Saying so here is the half of\n // AGL-2028 that holds however the rate question is answered.\n //\n // States the MECHANISM and makes no remittance determination —\n // the same line `describeOrderTaxMode` holds. Who owes which\n // authority what attaches by operation of law and is counsel's\n // call (AGL-1904/AGL-1956).\n helperText={`${PLATFORM_BRAND_NAME} charges this price as typed — no tax is added. Your Commerce tax settings are a goods sales rate and do not apply to bookings.`}\n value={draft?.priceUsd ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n priceUsd: event.target.value.replace(/[^0-9]/g, ''),\n }\n : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Timezone\"\n value={draft?.timezone ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, timezone: event.target.value } : prev,\n )\n }\n size=\"small\"\n sx={{ minWidth: 180 }}\n />\n </Stack>\n <TextField\n label=\"Description (optional)\"\n value={draft?.description ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, description: event.target.value } : prev,\n )\n }\n size=\"small\"\n multiline\n minRows={2}\n />\n <Typography variant=\"overline\" color=\"text.secondary\">\n {'Weekly availability'}\n </Typography>\n {WEEKDAYS.map((day, index) => (\n <TextField\n key={day}\n label={day}\n placeholder=\"09:00-12:00, 13:00-17:00 (empty = closed)\"\n value={draft?.windowText[index] ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n windowText: prev.windowText.map((text, i) =>\n i === index ? event.target.value : text,\n ),\n }\n : prev,\n )\n }\n size=\"small\"\n />\n ))}\n <Typography variant=\"overline\" color=\"text.secondary\">\n {'CRM'}\n </Typography>\n {/*\n What a booking of this service does to the booker's CRM record\n (AGL-2660). The meeting is on by default — a booking IS a\n meeting the record has — and the follow-up is the service's\n choice, because not every service earns a call afterwards.\n */}\n <FormControlLabel\n control={\n <Switch\n checked={draft?.crmMeetingActivity ?? true}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, crmMeetingActivity: event.target.checked }\n : prev,\n )\n }\n size=\"small\"\n />\n }\n label=\"Log a meeting on the CRM record when this service is booked\"\n />\n <FormControlLabel\n control={\n <Switch\n checked={draft?.crmFollowUpTask ?? false}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, crmFollowUpTask: event.target.checked }\n : prev,\n )\n }\n size=\"small\"\n />\n }\n label=\"Create a follow-up task on the CRM record when this service is booked\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'The follow-up is due one business day after the slot, on whoever ' +\n 'owns the record. Nothing is filed on a site whose CRM is off.'}\n </Typography>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!draft?.name.trim()}\n onClick={handleSave}\n >\n {'Save service'}\n </Button>\n </DialogActions>\n </Dialog>\n </Stack>\n )\n}\nBookingsConsolePage.displayName = 'BookingsConsolePage'\n\nexport default BookingsConsolePage\n"],"names":["checkQuota","crmContactByEmailHref","pluginDocsHelp","BOOKINGS_BOOKER_PARAM","normalizeContactEmail","PLATFORM_BRAND_NAME","describePaymentRisk","isBookingReminderDue","AppLink","CardDisplay","HelpTip","useConfirmationContext","QuotaReadoutComponent","useSnackbar","Timestamp","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","FormControlLabel","Stack","Switch","TextField","Tooltip","Typography","collection","doc","limit","orderBy","query","setDoc","updateDoc","useParams","useSearchParams","useCallback","useState","useFirestore","useFirestoreCollection","useHostResourceApi","useUser","writeGuardedBySeed","authorizedFetch","WEEKDAYS","parseWindows","input","windows","chunk","split","match","trim","start","Number","end","push","formatWindows","pad","minutes","String","Math","floor","padStart","map","window","join","PaymentRiskChip","risk","described","title","detail","label","size","color","BookingsConsolePage","props","hostId","entitled","org","firestore","createHostResource","enqueueSnackbar","confirm","data","user","params","siteContext","orgSlug","host","searchParams","bookerFilter","get","basePath","serviceDocs","status","servicesStatus","fromCache","servicesFromCache","idField","bookingDocs","services","filter","service","deletedAt","sort","a","b","name","localeCompare","upcoming","booking","endsAtMs","Date","now","startsAtMs","shown","email","reminderNowMs","reminderQueue","length","remindersSent","reminderSentAt","draft","setDraft","handleAdd","variant","persist","quota","allowed","id","durationMinutes","priceUsd","timezone","Intl","DateTimeFormat","resolvedOptions","timeZone","description","windowText","_","index","crmMeetingActivity","crmFollowUpTask","handleSave","forEach","text","weekday","parsed","fields","slice","max","min","round","verdict","subject","unreadable","updatedAt","merge","ok","message","resource","error","console","allowDuplicate","handleDeleteService","confirmed","confirmationText","confirmationButtonProps","then","catch","$id","handleCancelBooking","paidCents","paidAmountCents","refundedCents","outstandingCents","refundable","toFixed","globalThis","attemptKey","crypto","randomUUID","random","toString","response","method","headers","body","JSON","stringify","bookingId","payload","json","spacing","header","help","anchor","excerpt","contentGutterX","contentGutterY","direction","sx","alignItems","flex","minWidth","noWrap","onClick","alignSelf","ready","used","noun","component","mb","href","ml","fontSize","serviceName","toLocaleString","paymentRisk","componentVariant","nativeButton","open","Boolean","onClose","maxWidth","fullWidth","display","flexDirection","gap","pt","value","onChange","event","prev","target","autoFocus","mt","replace","helperText","multiline","minRows","day","placeholder","i","control","checked","disabled","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/components/bookings-console-page.tsx"],"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'use client'\n\nimport { checkQuota, pluginDocsHelp } from '@aglyn/aglyn'\nimport { normalizeContactEmail } from '@aglyn/aglyn'\nimport { pluginRecordByEmailHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { BOOKINGS_BOOKER_PARAM } from '../model/bookings-record-routes'\nimport { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport { describePaymentRisk, type PaymentRisk } from '@aglyn/aglyn/app-utils/payment-risk'\nimport { type ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { type HostBookingService, isBookingReminderDue } from '../model'\nimport { AppLink, CardDisplay, HelpTip, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n FormControlLabel,\n Stack,\n Switch,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n setDoc,\n updateDoc,\n} from 'firebase/firestore'\nimport { useParams, useSearchParams } from 'next/navigation'\nimport { useCallback, useState } from 'react'\nimport {\n useFirestore,\n useFirestoreCollection,\n useHostResourceApi,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\n\nconst WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']\n\n/** \"09:00-12:00, 13:00-17:00\" → open intervals in minutes. */\nfunction parseWindows(input: string): Array<{ start: number; end: number }> {\n const windows: Array<{ start: number; end: number }> = []\n for (const chunk of input.split(',')) {\n const match = chunk.trim().match(/^(\\d{1,2}):(\\d{2})\\s*-\\s*(\\d{1,2}):(\\d{2})$/)\n if (!match) continue\n const start = Number(match[1]) * 60 + Number(match[2])\n const end = Number(match[3]) * 60 + Number(match[4])\n if (end > start && end <= 24 * 60) windows.push({ start, end })\n }\n return windows\n}\n\nfunction formatWindows(\n windows: Array<{ start: number; end: number }> | undefined,\n): string {\n const pad = (minutes: number) =>\n `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(\n minutes % 60,\n ).padStart(2, '0')}`\n return (windows ?? [])\n .map((window) => `${pad(window.start)}-${pad(window.end)}`)\n .join(', ')\n}\n\ninterface ServiceDraft {\n id: string | null\n name: string\n durationMinutes: string\n priceUsd: string\n timezone: string\n description: string\n /** Per-weekday window text, e.g. \"09:00-17:00\". */\n windowText: string[]\n /** The service's two CRM switches (AGL-2660) — see `HostBookingService`. */\n crmMeetingActivity: boolean\n crmFollowUpTask: boolean\n}\n\n/** A fraud warning, review or chargeback on the booking's payment (AGL-3360). */\nfunction PaymentRiskChip({ risk }: { risk: PaymentRisk | undefined }) {\n const described = describePaymentRisk(risk)\n if (!described) return null\n return (\n <Tooltip title={described.detail}>\n <Chip label={described.label} size=\"small\" color=\"warning\" />\n </Tooltip>\n )\n}\n\n/**\n * Bookings manager (AGL-159): bookable services with weekly availability\n * windows, and the upcoming-bookings list with cancel. Visitors book via\n * the org /api/bookings endpoints; confirmations email through the\n * env-gated Resend path. Plan-gated (`bookings` flag + `servicesPerHost`).\n */\nexport function BookingsConsolePage(props: ConsolePluginPageProps) {\n const { hostId, entitled, org } = props\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The id token the refund route authenticates with (AGL-2315).\n const { data: user } = useUser()\n /*\n * Where a booker's CONTACT is (AGL-2622). A booking updated a person in\n * the CRM by the address it carried, and the row links there by that\n * address — the Contacts list holds the id nothing here does and opens\n * the record on one match. The address is the one the plugin that keeps\n * contacts publishes, asked of the record-route registry the way the\n * commerce dialog asks: a plugin importing a sibling is a cycle waiting\n * for its third edge. `null` until the route params settle, and where no\n * plugin keeps contacts, so no row renders a link to nowhere.\n */\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const siteContext =\n params?.orgSlug && params?.host\n ? { orgSlug: String(params.orgSlug), host: String(params.host) }\n : null\n const contactHrefOf = (email: unknown): string | null =>\n siteContext && email ? pluginRecordByEmailHref('contact', siteContext, String(email)) : null\n /*\n * The list narrowed to one booker (AGL-2660): a contact's record links\n * here with `?email=`, and the page answers with every booking that\n * address holds — past ones too, because that link asks about a\n * person's history rather than the week ahead. The same match rule as\n * the row's own \"View in CRM\": the address, normalized, equal. `null`\n * outside a router, where there is no query to read.\n */\n const searchParams = useSearchParams()\n const bookerFilter = normalizeContactEmail(searchParams?.get(BOOKINGS_BOOKER_PARAM))\n const { basePath } = props\n\n const {\n data: serviceDocs,\n status: servicesStatus,\n /**\n * The rows this editor is seeded from are unconfirmed by the server\n * (AGL-1358). Editing copies a whole stored service into `draft` and\n * writes all of it back, so `merge: true` protects nothing. `windows` is\n * the weekly availability map, rebuilt in full on every save: a cached\n * seed re-opens slots that were closed and closes ones customers can\n * already book, and `priceUsd` goes back to whatever that snapshot\n * charged.\n */\n fromCache: servicesFromCache,\n } = useFirestoreCollection<any>(\n () =>\n query(collection(firestore, 'hosts', hostId, 'services'), limit(100)),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { data: bookingDocs } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'bookings'),\n orderBy('startsAtMs', 'desc'),\n limit(100),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const services = [...(serviceDocs ?? [])]\n .filter((service: any) => !service.deletedAt)\n .sort((a: any, b: any) =>\n String(a.name ?? '').localeCompare(String(b.name ?? '')),\n )\n const upcoming = [...(bookingDocs ?? [])]\n .filter((booking: any) => booking.endsAtMs >= Date.now())\n .sort((a: any, b: any) => a.startsAtMs - b.startsAtMs)\n // Newest first when narrowed to a booker — the listener already orders\n // by start, descending — so the history reads down from the latest.\n const shown = bookerFilter\n ? (bookingDocs ?? []).filter(\n (booking: any) => normalizeContactEmail(booking.email) === bookerFilter,\n )\n : upcoming\n\n /**\n * The 24-hour reminder queue (AGL-2431).\n *\n * Reminders are sent by the `booking-reminders` beat, not by anything on\n * this page, and until that beat existed they were never sent at all — the\n * feature had a designed email template, white-label branding and an\n * `x-cron-secret` door, and no scheduler pointed at it. So the merchant had\n * no way to tell working from broken, and still would not if this card\n * merely asserted that reminders happen.\n *\n * READ-ONLY, and deliberately no \"send now\" button: the beat runs hourly\n * and stamps `reminderSentAt`, so a manual sender would race it for a\n * reminder nobody can un-send. Same call AGL-2227 made for the recovery\n * queue.\n *\n * `isBookingReminderDue` is the sender's own predicate, against one instant\n * shared by both counts, so this is the queue the next pass will drain\n * rather than an approximation of it.\n */\n const reminderNowMs = Date.now()\n const reminderQueue = (bookingDocs ?? []).filter((booking: any) =>\n isBookingReminderDue(booking, reminderNowMs),\n ).length\n const remindersSent = (bookingDocs ?? []).filter(\n (booking: any) => booking.reminderSentAt,\n ).length\n\n const [draft, setDraft] = useState<ServiceDraft | null>(null)\n\n const handleAdd = useCallback(() => {\n if (!entitled) {\n return void enqueueSnackbar(\n 'Bookings require a Starter plan — see Billing to upgrade',\n { variant: 'warning', persist: false },\n )\n }\n const quota = checkQuota(org, 'servicesPerHost', services.length)\n if (!quota.allowed) {\n return void enqueueSnackbar(\n `Service limit reached (${quota.limit}) — upgrade in Billing`,\n { variant: 'warning', persist: false },\n )\n }\n setDraft({\n id: null,\n name: '',\n durationMinutes: '30',\n priceUsd: '0',\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',\n description: '',\n windowText: WEEKDAYS.map((_, index) =>\n index >= 1 && index <= 5 ? '09:00-17:00' : '',\n ),\n // A booking is a meeting the record has; the follow-up is opt-in.\n crmMeetingActivity: true,\n crmFollowUpTask: false,\n })\n }, [entitled, org, services.length, enqueueSnackbar])\n\n const handleSave = useCallback(async () => {\n if (!draft || !draft.name.trim()) return\n const windows: HostBookingService['windows'] = {}\n draft.windowText.forEach((text, weekday) => {\n const parsed = parseWindows(text)\n if (parsed.length) windows[weekday] = parsed\n })\n const fields = {\n name: draft.name.trim().slice(0, 80),\n durationMinutes: Math.max(\n 5,\n Math.min(480, Math.round(Number(draft.durationMinutes) || 30)),\n ),\n priceUsd: Math.max(0, Math.round(Number(draft.priceUsd) || 0)),\n timezone: draft.timezone.trim() || 'UTC',\n ...(draft.description.trim() && {\n description: draft.description.trim().slice(0, 500),\n }),\n windows,\n // Written explicitly rather than only when on: an edit that switches\n // the meeting off has to land a `false`, and `merge: true` would keep\n // an absent key exactly as it was.\n crmMeetingActivity: draft.crmMeetingActivity,\n crmFollowUpTask: draft.crmFollowUpTask,\n }\n try {\n if (draft.id) {\n /**\n * Edit stays client-direct (no quota consumed) — and is refused when\n * its seed was never confirmed by the server (AGL-1358). `fields` is\n * every editable key of the stored service, `windows` included, so\n * `merge: true` has nothing left to protect.\n *\n * The guard WRAPS the write. An early return is a shape you can keep\n * while losing the protection; here the write is only reachable\n * through the verdict.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'service',\n unreadable: servicesStatus === 'error',\n fromCache: servicesFromCache,\n },\n async () => {\n await setDoc(\n doc(firestore, 'hosts', hostId, 'services', draft.id),\n { ...fields, updatedAt: Timestamp.now() },\n { merge: true },\n )\n },\n )\n // Before `setDraft(null)`, so a refusal keeps the dialog open with\n // every window that was typed. A save that silently does nothing\n // sends the user back to retype a form that will be refused again\n // just as quietly.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n } else {\n // New service rides the quota-enforcing resources API (AGL-473) —\n // it also re-checks the `bookings` entitlement server-side.\n await createHostResource({ hostId, resource: 'service', data: fields })\n }\n setDraft(null)\n enqueueSnackbar('Service saved', { variant: 'success', persist: false })\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [\n draft,\n firestore,\n hostId,\n createHostResource,\n enqueueSnackbar,\n servicesStatus,\n servicesFromCache,\n ])\n\n const handleDeleteService = useCallback(\n (service: any) => async () => {\n const confirmed = await confirm({\n title: 'Delete this service?',\n description: `\"${service.name}\" stops accepting bookings.`,\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(doc(firestore, 'hosts', hostId, 'services', service.$id), {\n deletedAt: Timestamp.now(),\n })\n },\n [confirm, firestore, hostId],\n )\n\n /**\n * Cancelling a PAID booking refunds the guest (AGL-2315).\n *\n * This used to be one `updateDoc` writing `status: 'canceled'`, for a paid\n * booking exactly as for a free one. The slot reopened, the appointment was\n * gone, and the guest had paid for it — the money moved nowhere and nothing\n * in the console said so. That was survivable only while the charge settled\n * in Aglyn's own balance and could be given back by hand; a paid booking is\n * a destination charge now, so the funds are at the MERCHANT and there is no\n * later opportunity to notice.\n *\n * So a paid booking cancels through the refund route, which reverses the\n * merchant's share and Aglyn's fee, and which sets `canceled` itself once\n * the money is actually back. A FAILED refund deliberately leaves the\n * booking standing: a cancelled-and-unrefunded appointment is the worst of\n * the three outcomes, and an admin who sees the row still there knows the\n * cancel did not happen.\n */\n const handleCancelBooking = useCallback(\n (booking: any) => async () => {\n const paidCents = Math.max(0, Number(booking.paidAmountCents ?? 0))\n const refundedCents = Math.max(0, Number(booking.refundedCents ?? 0))\n const outstandingCents = Math.max(0, paidCents - refundedCents)\n const refundable = outstandingCents > 0\n const confirmed = await confirm({\n title: 'Cancel this booking?',\n description:\n `${booking.name} (${booking.email}) — the slot reopens.` +\n (refundable\n ? ` $${(outstandingCents / 100).toFixed(2)} is refunded to them` +\n ' through Stripe.'\n : ''),\n confirmationText: refundable ? 'Cancel and refund' : 'Cancel booking',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n\n if (refundable) {\n // `randomUUID` needs a secure context, which the console always is,\n // but fall back rather than throw on a refund.\n const attemptKey =\n globalThis.crypto?.randomUUID?.() ??\n `${Date.now()}-${Math.random().toString(36).slice(2)}`\n try {\n const response = await authorizedFetch(user, '/api/bookings/refund', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Idempotency-Key': attemptKey,\n },\n body: JSON.stringify({ hostId, bookingId: booking.$id }),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n // A 409 is a guard REFUSING with a body that says what to do\n // instead — a booking paid before the PaymentIntent was recorded\n // names the Stripe dashboard — not the refund machinery failing.\n // It surfaces verbatim, the AGL-1818 rule.\n enqueueSnackbar(payload?.error ?? 'Refund failed', {\n variant: response.status === 409 ? 'warning' : 'error',\n allowDuplicate: true,\n })\n // The booking stays. See the note above: cancelled-and-unrefunded\n // is worse than not cancelled.\n return\n }\n // The route wrote `canceled` itself once the money was back.\n enqueueSnackbar('Booking canceled and refunded', {\n variant: 'success',\n persist: false,\n })\n return\n } catch {\n enqueueSnackbar('Refund failed — the booking was not canceled', {\n variant: 'error',\n allowDuplicate: true,\n })\n return\n }\n }\n\n await updateDoc(\n doc(firestore, 'hosts', hostId, 'bookings', booking.$id),\n { status: 'canceled' },\n )\n enqueueSnackbar('Booking canceled', {\n variant: 'success',\n persist: false,\n })\n },\n [confirm, firestore, hostId, enqueueSnackbar, user],\n )\n\n return (\n <Stack spacing={3}>\n <CardDisplay\n header={'Services'}\n help={pluginDocsHelp('bookings', {\n anchor: '#set-up-bookings',\n excerpt:\n 'The bookable services this site offers — how long each takes, ' +\n 'what it costs, and the hours it can be booked in.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {services.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Define a bookable service — duration, price, and weekly ' +\n 'availability — then visitors can book from your site.'}\n </Typography>\n ) : (\n services.map((service: any) => (\n <Stack\n key={service.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {service.name}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`${service.durationMinutes} min` +\n (Number(service.priceUsd) > 0\n ? ` · $${service.priceUsd}`\n : ' · free') +\n ` · ${service.timezone ?? 'UTC'}`}\n </Typography>\n </Stack>\n <Button\n size=\"small\"\n onClick={() =>\n setDraft({\n id: service.$id,\n name: service.name ?? '',\n durationMinutes: String(service.durationMinutes ?? 30),\n priceUsd: String(service.priceUsd ?? 0),\n timezone: service.timezone ?? 'UTC',\n description: service.description ?? '',\n windowText: WEEKDAYS.map((_, index) =>\n formatWindows(service.windows?.[index]),\n ),\n // Absent reads as ON for the meeting and OFF for the\n // follow-up — the model's defaults, seeded so the\n // switches show what the service actually does.\n crmMeetingActivity: service.crmMeetingActivity !== false,\n crmFollowUpTask: service.crmFollowUpTask === true,\n })\n }\n >\n {'Edit'}\n </Button>\n <Button\n size=\"small\"\n color=\"error\"\n onClick={handleDeleteService(service)}\n >\n {'Delete'}\n </Button>\n </Stack>\n ))\n )}\n <Button\n size=\"small\"\n color=\"primary\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={handleAdd}\n >\n {'Add service'}\n </Button>\n {/* The cap, standing rather than only on refusal (AGL-2113).\n `services.length` is the same count `handleAdd` hands to\n `checkQuota`, so the readout and the gate cannot disagree. */}\n <QuotaReadoutComponent\n ready={org != null}\n used={services.length}\n limit={checkQuota(org, 'servicesPerHost', services.length).limit}\n noun=\"service\"\n />\n </Stack>\n </CardDisplay>\n\n <CardDisplay\n header={bookerFilter ? `Bookings for ${bookerFilter}` : 'Upcoming bookings'}\n help={pluginDocsHelp('bookings', {\n anchor: '#manage',\n excerpt:\n 'Bookings taken on this site, with the customer and the slot each ' +\n 'one holds.',\n })}\n contentGutterX\n contentGutterY\n >\n {bookerFilter ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n component=\"div\"\n sx={{ mb: 1 }}\n >\n {'Every booking this address holds, newest first. '}\n {basePath ? <AppLink href={basePath}>{'Show upcoming bookings'}</AppLink> : null}\n </Typography>\n ) : null}\n {/* What the reminder beat will do next, and what it has done. Named\n \"24-hour reminders\" rather than \"emails\" because that is the only\n mail this queue governs — confirmations go out on booking. */}\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n component=\"div\"\n sx={{ mb: 1 }}\n >\n {`24-hour reminders · ${reminderQueue} due in the next pass · ` +\n `${remindersSent} already sent`}\n {/* Its OWN heading, not the card's `#manage`: a merchant reading\n this line wants the timing and the once-only rule, and landing\n them on \"manage your bookings\" is the presence-not-correctness\n failure a help-coverage check cannot see. */}\n <HelpTip\n {...pluginDocsHelp('bookings', { anchor: '#reminders' })}\n sx={{ ml: 0.5, fontSize: '0.9em' }}\n />\n </Typography>\n {shown.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {bookerFilter ? 'No bookings for this address.' : 'No upcoming bookings.'}\n </Typography>\n ) : (\n <Stack spacing={1}>\n {shown.map((booking: any) => (\n <Stack\n key={booking.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {`${booking.serviceName} — ${booking.name}`}\n {booking.status === 'canceled' ? ' (canceled)' : ''}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`${new Date(booking.startsAtMs).toLocaleString()} · ${\n booking.email\n }`}\n </Typography>\n </Stack>\n <PaymentRiskChip risk={booking.paymentRisk} />\n {contactHrefOf(booking.email) ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={contactHrefOf(booking.email) ?? undefined}\n size=\"small\"\n >\n {'View in CRM'}\n </Button>\n ) : null}\n {booking.status !== 'canceled' ? (\n <Button\n size=\"small\"\n color=\"error\"\n onClick={handleCancelBooking(booking)}\n >\n {'Cancel'}\n </Button>\n ) : null}\n </Stack>\n ))}\n </Stack>\n )}\n </CardDisplay>\n\n <Dialog\n open={Boolean(draft)}\n onClose={() => setDraft(null)}\n maxWidth=\"sm\"\n fullWidth\n >\n <DialogTitle>{draft?.id ? 'Edit service' : 'Add service'}</DialogTitle>\n <DialogContent\n sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, pt: 1 }}\n >\n <TextField\n label=\"Name\"\n value={draft?.name ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, name: event.target.value } : prev,\n )\n }\n size=\"small\"\n autoFocus\n sx={{ mt: 1 }}\n />\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Duration (minutes)\"\n value={draft?.durationMinutes ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n durationMinutes: event.target.value.replace(\n /[^0-9]/g,\n '',\n ),\n }\n : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Price (USD, 0 = free)\"\n // WHAT THIS PRICE DOES NOT INCLUDE (AGL-2028).\n //\n // A paid booking is charged exactly this figure: the handler\n // sends no tax parameter at all, by a stated decision (AGL-2000)\n // — whether a service is taxable is jurisdiction-specific and\n // frequently the opposite answer from goods, so applying the\n // commerce plugin's GOODS sales rate would be confidently wrong\n // rather than merely absent.\n //\n // That decision was recorded in the source and in the\n // `storefrontTaxCollected` ledger, and in neither place can the\n // merchant see it. A service business is one of the three named\n // ICPs, and one who configured a rate in Commerce → Settings →\n // Taxes has every reason to assume it covers the appointments\n // they sell on the same site. Saying so here is the half of\n // AGL-2028 that holds however the rate question is answered.\n //\n // States the MECHANISM and makes no remittance determination —\n // the same line `describeOrderTaxMode` holds. Who owes which\n // authority what attaches by operation of law and is counsel's\n // call (AGL-1904/AGL-1956).\n helperText={`${PLATFORM_BRAND_NAME} charges this price as typed — no tax is added. Your Commerce tax settings are a goods sales rate and do not apply to bookings.`}\n value={draft?.priceUsd ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n priceUsd: event.target.value.replace(/[^0-9]/g, ''),\n }\n : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Timezone\"\n value={draft?.timezone ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, timezone: event.target.value } : prev,\n )\n }\n size=\"small\"\n sx={{ minWidth: 180 }}\n />\n </Stack>\n <TextField\n label=\"Description (optional)\"\n value={draft?.description ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, description: event.target.value } : prev,\n )\n }\n size=\"small\"\n multiline\n minRows={2}\n />\n <Typography variant=\"overline\" color=\"text.secondary\">\n {'Weekly availability'}\n </Typography>\n {WEEKDAYS.map((day, index) => (\n <TextField\n key={day}\n label={day}\n placeholder=\"09:00-12:00, 13:00-17:00 (empty = closed)\"\n value={draft?.windowText[index] ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n windowText: prev.windowText.map((text, i) =>\n i === index ? event.target.value : text,\n ),\n }\n : prev,\n )\n }\n size=\"small\"\n />\n ))}\n <Typography variant=\"overline\" color=\"text.secondary\">\n {'CRM'}\n </Typography>\n {/*\n What a booking of this service does to the booker's CRM record\n (AGL-2660). The meeting is on by default — a booking IS a\n meeting the record has — and the follow-up is the service's\n choice, because not every service earns a call afterwards.\n */}\n <FormControlLabel\n control={\n <Switch\n checked={draft?.crmMeetingActivity ?? true}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, crmMeetingActivity: event.target.checked }\n : prev,\n )\n }\n size=\"small\"\n />\n }\n label=\"Log a meeting on the CRM record when this service is booked\"\n />\n <FormControlLabel\n control={\n <Switch\n checked={draft?.crmFollowUpTask ?? false}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, crmFollowUpTask: event.target.checked }\n : prev,\n )\n }\n size=\"small\"\n />\n }\n label=\"Create a follow-up task on the CRM record when this service is booked\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'The follow-up is due one business day after the slot, on whoever ' +\n 'owns the record. Nothing is filed on a site whose CRM is off.'}\n </Typography>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!draft?.name.trim()}\n onClick={handleSave}\n >\n {'Save service'}\n </Button>\n </DialogActions>\n </Dialog>\n </Stack>\n )\n}\nBookingsConsolePage.displayName = 'BookingsConsolePage'\n\nexport default BookingsConsolePage\n"],"names":["checkQuota","pluginDocsHelp","normalizeContactEmail","pluginRecordByEmailHref","BOOKINGS_BOOKER_PARAM","PLATFORM_BRAND_NAME","describePaymentRisk","isBookingReminderDue","AppLink","CardDisplay","HelpTip","useConfirmationContext","QuotaReadoutComponent","useSnackbar","Timestamp","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","FormControlLabel","Stack","Switch","TextField","Tooltip","Typography","collection","doc","limit","orderBy","query","setDoc","updateDoc","useParams","useSearchParams","useCallback","useState","useFirestore","useFirestoreCollection","useHostResourceApi","useUser","writeGuardedBySeed","authorizedFetch","WEEKDAYS","parseWindows","input","windows","chunk","split","match","trim","start","Number","end","push","formatWindows","pad","minutes","String","Math","floor","padStart","map","window","join","PaymentRiskChip","risk","described","title","detail","label","size","color","BookingsConsolePage","props","hostId","entitled","org","firestore","createHostResource","enqueueSnackbar","confirm","data","user","params","siteContext","orgSlug","host","contactHrefOf","email","searchParams","bookerFilter","get","basePath","serviceDocs","status","servicesStatus","fromCache","servicesFromCache","idField","bookingDocs","services","filter","service","deletedAt","sort","a","b","name","localeCompare","upcoming","booking","endsAtMs","Date","now","startsAtMs","shown","reminderNowMs","reminderQueue","length","remindersSent","reminderSentAt","draft","setDraft","handleAdd","variant","persist","quota","allowed","id","durationMinutes","priceUsd","timezone","Intl","DateTimeFormat","resolvedOptions","timeZone","description","windowText","_","index","crmMeetingActivity","crmFollowUpTask","handleSave","forEach","text","weekday","parsed","fields","slice","max","min","round","verdict","subject","unreadable","updatedAt","merge","ok","message","resource","error","console","allowDuplicate","handleDeleteService","confirmed","confirmationText","confirmationButtonProps","then","catch","$id","handleCancelBooking","paidCents","paidAmountCents","refundedCents","outstandingCents","refundable","toFixed","globalThis","attemptKey","crypto","randomUUID","random","toString","response","method","headers","body","JSON","stringify","bookingId","payload","json","spacing","header","help","anchor","excerpt","contentGutterX","contentGutterY","direction","sx","alignItems","flex","minWidth","noWrap","onClick","alignSelf","ready","used","noun","component","mb","href","ml","fontSize","serviceName","toLocaleString","paymentRisk","componentVariant","nativeButton","undefined","open","Boolean","onClose","maxWidth","fullWidth","display","flexDirection","gap","pt","value","onChange","event","prev","target","autoFocus","mt","replace","helperText","multiline","minRows","day","placeholder","i","control","checked","disabled","displayName"],"mappings":"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"}
@@ -0,0 +1,74 @@
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
+ /**
18
+ * The query key a booking link carries the record on: `?crm=contact:abc`.
19
+ *
20
+ * The reference rides the LINK rather than relying on the booker's address
21
+ * because the two routinely differ — a rep emails somebody's work address
22
+ * and they book with a personal one — and a booking that could only be
23
+ * matched by address would then land on nobody. The widget copies the value
24
+ * onto the booking request, the booking row keeps it as `crmRef`, and the
25
+ * server hands it to the record system to file the meeting under. The key
26
+ * is the one links already sent carry, so it is never renamed.
27
+ */
28
+ export declare const BOOKING_RECORD_PARAM = "crm";
29
+ /**
30
+ * The query key that preselects a service in the booking widget:
31
+ * `?service={serviceId}`. The widget lists every active service and this
32
+ * opens it on the one the link was about.
33
+ */
34
+ export declare const BOOKING_SERVICE_PARAM = "service";
35
+ /** A record of the record system's, by its kind and id in its own words. */
36
+ export interface BookingRecordRef {
37
+ kind: string;
38
+ id: string;
39
+ }
40
+ /** `contact:abc` — the wire form of a reference. */
41
+ export declare function formatBookingRecordRef(ref: BookingRecordRef): string;
42
+ /**
43
+ * The reference a query value names, or `null` for anything that is not one.
44
+ *
45
+ * Strict on purpose: the value arrives on a public, unauthenticated booking
46
+ * request, so a malformed one is dropped rather than stored — a booking
47
+ * still lands, and is matched by the booker's address instead.
48
+ */
49
+ export declare function parseBookingRecordRef(raw: unknown): BookingRecordRef | null;
50
+ /**
51
+ * What the meeting entry on the timeline says — the service and the slot,
52
+ * in the service's own timezone, with the zone named so a rep in another
53
+ * one is not misled. The body, not the subject: `subject` is the field a
54
+ * sent email carries, and the timeline draws it as one.
55
+ */
56
+ export declare function bookingMeetingBody(input: {
57
+ serviceName: string;
58
+ startsAtMs: number;
59
+ timezone?: string | null;
60
+ }): string;
61
+ /** The title of the task a service files when it asks for a follow-up. */
62
+ export declare function bookingFollowUpTitle(serviceName: string): string;
63
+ /**
64
+ * When the follow-up falls due: ONE BUSINESS DAY after the slot, at the
65
+ * slot's own clock time.
66
+ *
67
+ * A meeting on a Friday is followed up on Monday, one on a Saturday or a
68
+ * Sunday on Monday too; any other day is followed up the next. Whole days
69
+ * are added to the instant rather than a calendar walked, so the arithmetic
70
+ * cannot land on a clock time that does not exist across a DST change —
71
+ * the task is due "about this time tomorrow", and an hour either way is not
72
+ * a fact the task keeps.
73
+ */
74
+ export declare function bookingFollowUpDueMs(endsAtMs: number, timezone?: string | null): number;
@@ -0,0 +1,131 @@
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
+ * WHAT A BOOKING CARRIES BACK TO THE RECORD IT CAME FROM (AGL-2660).
18
+ *
19
+ * A rep drops a booking link into an email from a record; the visitor books
20
+ * through it; the booking comes back to the record as a meeting on its
21
+ * timeline and, when the service asks for one, a follow-up task. The record
22
+ * system draws the "Book a meeting" zone and hands it the record by its kind
23
+ * and id; this plugin builds the link, carries the record on it, and files
24
+ * the meeting back through the core's record-timeline seam, where the owner
25
+ * finds the record. Neither plugin imports the other: what this module holds
26
+ * — the link's query keys, the wire form of the record it carries, and the
27
+ * words and the day of what is filed — is this plugin's own.
28
+ */ import { isValidTimeZone, zonedDateTime } from "@aglyn/shared-util-timestamp/zoned-time";
29
+ /**
30
+ * The query key a booking link carries the record on: `?crm=contact:abc`.
31
+ *
32
+ * The reference rides the LINK rather than relying on the booker's address
33
+ * because the two routinely differ — a rep emails somebody's work address
34
+ * and they book with a personal one — and a booking that could only be
35
+ * matched by address would then land on nobody. The widget copies the value
36
+ * onto the booking request, the booking row keeps it as `crmRef`, and the
37
+ * server hands it to the record system to file the meeting under. The key
38
+ * is the one links already sent carry, so it is never renamed.
39
+ */ export const BOOKING_RECORD_PARAM = 'crm';
40
+ /**
41
+ * The query key that preselects a service in the booking widget:
42
+ * `?service={serviceId}`. The widget lists every active service and this
43
+ * opens it on the one the link was about.
44
+ */ export const BOOKING_SERVICE_PARAM = 'service';
45
+ /**
46
+ * What a kind and an id may look like inside the reference: a lowercase
47
+ * word, and an id with nothing a path or a query could be built from. The
48
+ * value arrives on a public, unauthenticated request; which kinds a record
49
+ * system keeps is its own to say when the booking is filed.
50
+ */ const REF_KIND_PATTERN = /^[a-z][a-z-]{0,31}$/;
51
+ const REF_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
52
+ /** `contact:abc` — the wire form of a reference. */ export function formatBookingRecordRef(ref) {
53
+ return `${ref.kind}:${ref.id}`;
54
+ }
55
+ /**
56
+ * The reference a query value names, or `null` for anything that is not one.
57
+ *
58
+ * Strict on purpose: the value arrives on a public, unauthenticated booking
59
+ * request, so a malformed one is dropped rather than stored — a booking
60
+ * still lands, and is matched by the booker's address instead.
61
+ */ export function parseBookingRecordRef(raw) {
62
+ if (typeof raw !== 'string') return null;
63
+ const separator = raw.indexOf(':');
64
+ if (separator <= 0) return null;
65
+ const kind = raw.slice(0, separator);
66
+ const id = raw.slice(separator + 1);
67
+ if (!REF_KIND_PATTERN.test(kind) || !REF_ID_PATTERN.test(id)) return null;
68
+ return {
69
+ kind,
70
+ id
71
+ };
72
+ }
73
+ /** The slot, as the meeting entry and the task print it. */ function formatSlot(startsAtMs, timezone) {
74
+ try {
75
+ return new Intl.DateTimeFormat('en-US', {
76
+ timeZone: timezone,
77
+ dateStyle: 'full',
78
+ timeStyle: 'short'
79
+ }).format(new Date(startsAtMs));
80
+ } catch (unused) {
81
+ // An unknown zone name on the service falls back to UTC rather than
82
+ // failing the write: a meeting filed with a slightly wrong clock is
83
+ // better than no meeting at all.
84
+ return new Intl.DateTimeFormat('en-US', {
85
+ timeZone: 'UTC',
86
+ dateStyle: 'full',
87
+ timeStyle: 'short'
88
+ }).format(new Date(startsAtMs));
89
+ }
90
+ }
91
+ /**
92
+ * What the meeting entry on the timeline says — the service and the slot,
93
+ * in the service's own timezone, with the zone named so a rep in another
94
+ * one is not misled. The body, not the subject: `subject` is the field a
95
+ * sent email carries, and the timeline draws it as one.
96
+ */ export function bookingMeetingBody(input) {
97
+ var _input_serviceName;
98
+ const timezone = input.timezone || 'UTC';
99
+ const service = String((_input_serviceName = input.serviceName) != null ? _input_serviceName : '').trim() || 'Booking';
100
+ return `${service} — ${formatSlot(input.startsAtMs, timezone)} (${timezone})`;
101
+ }
102
+ /** The title of the task a service files when it asks for a follow-up. */ export function bookingFollowUpTitle(serviceName) {
103
+ const service = String(serviceName != null ? serviceName : '').trim() || 'the booking';
104
+ return `Follow up after ${service}`;
105
+ }
106
+ const DAY_MS = 24 * 60 * 60 * 1000;
107
+ /**
108
+ * Weekday (0 = Sunday … 6 = Saturday) of an instant in a timezone, read in
109
+ * UTC when the zone name is one `Intl` does not know — for the reason
110
+ * `formatSlot` gives.
111
+ */ function weekdayIn(atMs, timezone) {
112
+ return zonedDateTime(atMs, isValidTimeZone(timezone) ? timezone : 'UTC').weekday;
113
+ }
114
+ /**
115
+ * When the follow-up falls due: ONE BUSINESS DAY after the slot, at the
116
+ * slot's own clock time.
117
+ *
118
+ * A meeting on a Friday is followed up on Monday, one on a Saturday or a
119
+ * Sunday on Monday too; any other day is followed up the next. Whole days
120
+ * are added to the instant rather than a calendar walked, so the arithmetic
121
+ * cannot land on a clock time that does not exist across a DST change —
122
+ * the task is due "about this time tomorrow", and an hour either way is not
123
+ * a fact the task keeps.
124
+ */ export function bookingFollowUpDueMs(endsAtMs, timezone) {
125
+ const weekday = weekdayIn(endsAtMs, timezone || 'UTC');
126
+ // Friday (5) skips the weekend; Saturday (6) reaches Monday in two days.
127
+ const daysAhead = weekday === 5 ? 3 : weekday === 6 ? 2 : 1;
128
+ return endsAtMs + daysAhead * DAY_MS;
129
+ }
130
+
131
+ //# sourceMappingURL=booking-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/booking-record.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\n/**\n * WHAT A BOOKING CARRIES BACK TO THE RECORD IT CAME FROM (AGL-2660).\n *\n * A rep drops a booking link into an email from a record; the visitor books\n * through it; the booking comes back to the record as a meeting on its\n * timeline and, when the service asks for one, a follow-up task. The record\n * system draws the \"Book a meeting\" zone and hands it the record by its kind\n * and id; this plugin builds the link, carries the record on it, and files\n * the meeting back through the core's record-timeline seam, where the owner\n * finds the record. Neither plugin imports the other: what this module holds\n * — the link's query keys, the wire form of the record it carries, and the\n * words and the day of what is filed — is this plugin's own.\n */\n\nimport {\n isValidTimeZone,\n zonedDateTime,\n} from '@aglyn/shared-util-timestamp/zoned-time'\n\n/**\n * The query key a booking link carries the record on: `?crm=contact:abc`.\n *\n * The reference rides the LINK rather than relying on the booker's address\n * because the two routinely differ — a rep emails somebody's work address\n * and they book with a personal one — and a booking that could only be\n * matched by address would then land on nobody. The widget copies the value\n * onto the booking request, the booking row keeps it as `crmRef`, and the\n * server hands it to the record system to file the meeting under. The key\n * is the one links already sent carry, so it is never renamed.\n */\nexport const BOOKING_RECORD_PARAM = 'crm'\n\n/**\n * The query key that preselects a service in the booking widget:\n * `?service={serviceId}`. The widget lists every active service and this\n * opens it on the one the link was about.\n */\nexport const BOOKING_SERVICE_PARAM = 'service'\n\n/** A record of the record system's, by its kind and id in its own words. */\nexport interface BookingRecordRef {\n kind: string\n id: string\n}\n\n/**\n * What a kind and an id may look like inside the reference: a lowercase\n * word, and an id with nothing a path or a query could be built from. The\n * value arrives on a public, unauthenticated request; which kinds a record\n * system keeps is its own to say when the booking is filed.\n */\nconst REF_KIND_PATTERN = /^[a-z][a-z-]{0,31}$/\nconst REF_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/\n\n/** `contact:abc` — the wire form of a reference. */\nexport function formatBookingRecordRef(ref: BookingRecordRef): string {\n return `${ref.kind}:${ref.id}`\n}\n\n/**\n * The reference a query value names, or `null` for anything that is not one.\n *\n * Strict on purpose: the value arrives on a public, unauthenticated booking\n * request, so a malformed one is dropped rather than stored — a booking\n * still lands, and is matched by the booker's address instead.\n */\nexport function parseBookingRecordRef(raw: unknown): BookingRecordRef | null {\n if (typeof raw !== 'string') return null\n const separator = raw.indexOf(':')\n if (separator <= 0) return null\n const kind = raw.slice(0, separator)\n const id = raw.slice(separator + 1)\n if (!REF_KIND_PATTERN.test(kind) || !REF_ID_PATTERN.test(id)) return null\n return { kind, id }\n}\n\n/** The slot, as the meeting entry and the task print it. */\nfunction formatSlot(startsAtMs: number, timezone: string): string {\n try {\n return new Intl.DateTimeFormat('en-US', {\n timeZone: timezone,\n dateStyle: 'full',\n timeStyle: 'short',\n }).format(new Date(startsAtMs))\n } catch {\n // An unknown zone name on the service falls back to UTC rather than\n // failing the write: a meeting filed with a slightly wrong clock is\n // better than no meeting at all.\n return new Intl.DateTimeFormat('en-US', {\n timeZone: 'UTC',\n dateStyle: 'full',\n timeStyle: 'short',\n }).format(new Date(startsAtMs))\n }\n}\n\n/**\n * What the meeting entry on the timeline says — the service and the slot,\n * in the service's own timezone, with the zone named so a rep in another\n * one is not misled. The body, not the subject: `subject` is the field a\n * sent email carries, and the timeline draws it as one.\n */\nexport function bookingMeetingBody(input: {\n serviceName: string\n startsAtMs: number\n timezone?: string | null\n}): string {\n const timezone = input.timezone || 'UTC'\n const service = String(input.serviceName ?? '').trim() || 'Booking'\n return `${service} — ${formatSlot(input.startsAtMs, timezone)} (${timezone})`\n}\n\n/** The title of the task a service files when it asks for a follow-up. */\nexport function bookingFollowUpTitle(serviceName: string): string {\n const service = String(serviceName ?? '').trim() || 'the booking'\n return `Follow up after ${service}`\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\n/**\n * Weekday (0 = Sunday … 6 = Saturday) of an instant in a timezone, read in\n * UTC when the zone name is one `Intl` does not know — for the reason\n * `formatSlot` gives.\n */\nfunction weekdayIn(atMs: number, timezone: string): number {\n return zonedDateTime(atMs, isValidTimeZone(timezone) ? timezone : 'UTC').weekday\n}\n\n/**\n * When the follow-up falls due: ONE BUSINESS DAY after the slot, at the\n * slot's own clock time.\n *\n * A meeting on a Friday is followed up on Monday, one on a Saturday or a\n * Sunday on Monday too; any other day is followed up the next. Whole days\n * are added to the instant rather than a calendar walked, so the arithmetic\n * cannot land on a clock time that does not exist across a DST change —\n * the task is due \"about this time tomorrow\", and an hour either way is not\n * a fact the task keeps.\n */\nexport function bookingFollowUpDueMs(\n endsAtMs: number,\n timezone?: string | null,\n): number {\n const weekday = weekdayIn(endsAtMs, timezone || 'UTC')\n // Friday (5) skips the weekend; Saturday (6) reaches Monday in two days.\n const daysAhead = weekday === 5 ? 3 : weekday === 6 ? 2 : 1\n return endsAtMs + daysAhead * DAY_MS\n}\n"],"names":["isValidTimeZone","zonedDateTime","BOOKING_RECORD_PARAM","BOOKING_SERVICE_PARAM","REF_KIND_PATTERN","REF_ID_PATTERN","formatBookingRecordRef","ref","kind","id","parseBookingRecordRef","raw","separator","indexOf","slice","test","formatSlot","startsAtMs","timezone","Intl","DateTimeFormat","timeZone","dateStyle","timeStyle","format","Date","bookingMeetingBody","input","service","String","serviceName","trim","bookingFollowUpTitle","DAY_MS","weekdayIn","atMs","weekday","bookingFollowUpDueMs","endsAtMs","daysAhead"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;CAYC,GAED,SACEA,eAAe,EACfC,aAAa,QACR,0CAAyC;AAEhD;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,uBAAuB,MAAK;AAEzC;;;;CAIC,GACD,OAAO,MAAMC,wBAAwB,UAAS;AAQ9C;;;;;CAKC,GACD,MAAMC,mBAAmB;AACzB,MAAMC,iBAAiB;AAEvB,kDAAkD,GAClD,OAAO,SAASC,uBAAuBC,GAAqB;IAC1D,OAAO,GAAGA,IAAIC,IAAI,CAAC,CAAC,EAAED,IAAIE,EAAE,EAAE;AAChC;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,sBAAsBC,GAAY;IAChD,IAAI,OAAOA,QAAQ,UAAU,OAAO;IACpC,MAAMC,YAAYD,IAAIE,OAAO,CAAC;IAC9B,IAAID,aAAa,GAAG,OAAO;IAC3B,MAAMJ,OAAOG,IAAIG,KAAK,CAAC,GAAGF;IAC1B,MAAMH,KAAKE,IAAIG,KAAK,CAACF,YAAY;IACjC,IAAI,CAACR,iBAAiBW,IAAI,CAACP,SAAS,CAACH,eAAeU,IAAI,CAACN,KAAK,OAAO;IACrE,OAAO;QAAED;QAAMC;IAAG;AACpB;AAEA,0DAA0D,GAC1D,SAASO,WAAWC,UAAkB,EAAEC,QAAgB;IACtD,IAAI;QACF,OAAO,IAAIC,KAAKC,cAAc,CAAC,SAAS;YACtCC,UAAUH;YACVI,WAAW;YACXC,WAAW;QACb,GAAGC,MAAM,CAAC,IAAIC,KAAKR;IACrB,EAAE,eAAM;QACN,oEAAoE;QACpE,oEAAoE;QACpE,iCAAiC;QACjC,OAAO,IAAIE,KAAKC,cAAc,CAAC,SAAS;YACtCC,UAAU;YACVC,WAAW;YACXC,WAAW;QACb,GAAGC,MAAM,CAAC,IAAIC,KAAKR;IACrB;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASS,mBAAmBC,KAIlC;QAEwBA;IADvB,MAAMT,WAAWS,MAAMT,QAAQ,IAAI;IACnC,MAAMU,UAAUC,QAAOF,qBAAAA,MAAMG,WAAW,YAAjBH,qBAAqB,IAAII,IAAI,MAAM;IAC1D,OAAO,GAAGH,QAAQ,GAAG,EAAEZ,WAAWW,MAAMV,UAAU,EAAEC,UAAU,EAAE,EAAEA,SAAS,CAAC,CAAC;AAC/E;AAEA,wEAAwE,GACxE,OAAO,SAASc,qBAAqBF,WAAmB;IACtD,MAAMF,UAAUC,OAAOC,sBAAAA,cAAe,IAAIC,IAAI,MAAM;IACpD,OAAO,CAAC,gBAAgB,EAAEH,SAAS;AACrC;AAEA,MAAMK,SAAS,KAAK,KAAK,KAAK;AAE9B;;;;CAIC,GACD,SAASC,UAAUC,IAAY,EAAEjB,QAAgB;IAC/C,OAAOjB,cAAckC,MAAMnC,gBAAgBkB,YAAYA,WAAW,OAAOkB,OAAO;AAClF;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASC,qBACdC,QAAgB,EAChBpB,QAAwB;IAExB,MAAMkB,UAAUF,UAAUI,UAAUpB,YAAY;IAChD,yEAAyE;IACzE,MAAMqB,YAAYH,YAAY,IAAI,IAAIA,YAAY,IAAI,IAAI;IAC1D,OAAOE,WAAWC,YAAYN;AAChC"}
@@ -0,0 +1,60 @@
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
+ /**
18
+ * The zone a booking's time is told in, to the guest and to the site's
19
+ * managers alike (AGL-3432).
20
+ *
21
+ * THE SERVICE WINS: its weekly windows are defined in its own zone, so that is
22
+ * the zone the slot was offered in. A service that names none (or names one
23
+ * this runtime no longer accepts) reads in the site's zone, then the
24
+ * workspace's, then UTC — `resolveSiteTimeZone`'s chain, so a booking and the
25
+ * site's own dates agree.
26
+ *
27
+ * Every candidate is validated, never trusted: a zone that cannot format would
28
+ * throw a `RangeError` in the middle of a send.
29
+ */
30
+ export declare function bookingTimeZone(input: {
31
+ service?: {
32
+ timezone?: unknown;
33
+ } | null;
34
+ host?: {
35
+ timeZone?: string;
36
+ } | null;
37
+ org?: {
38
+ timeZone?: string;
39
+ } | null;
40
+ }): string;
41
+ /**
42
+ * The zone a booking document carries, when it carries a usable one.
43
+ *
44
+ * Bookings written before the zone was stored have none, and a reader then
45
+ * resolves it from the service and site with {@link bookingTimeZone}.
46
+ */
47
+ export declare function storedBookingTimeZone(booking: {
48
+ timezone?: unknown;
49
+ } | null | undefined): string | undefined;
50
+ /**
51
+ * A booking's start as a guest reads it — "Friday, April 24, 2026 at 3:00 PM"
52
+ * — in `timeZone`. The caller names the zone beside it; the string alone does
53
+ * not say which zone it is in.
54
+ *
55
+ * Never the server's own zone: a formatter with no `timeZone` reads in UTC on
56
+ * the platform's servers and prints a wall-clock time hours away from the
57
+ * appointment. An unusable zone formats in UTC, which is what the caller then
58
+ * names.
59
+ */
60
+ export declare function formatBookingWhen(startsAtMs: number, timeZone: string): string;
@@ -0,0 +1,65 @@
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
+ */ // The leaf, not the barrel: this model is read by the client bundle too, and
17
+ // the zone rules are a dependency-free module.
18
+ import { isSupportedTimeZone, resolveSiteTimeZone } from "@aglyn/aglyn/app-utils/collection-entry-date";
19
+ /**
20
+ * The zone a booking's time is told in, to the guest and to the site's
21
+ * managers alike (AGL-3432).
22
+ *
23
+ * THE SERVICE WINS: its weekly windows are defined in its own zone, so that is
24
+ * the zone the slot was offered in. A service that names none (or names one
25
+ * this runtime no longer accepts) reads in the site's zone, then the
26
+ * workspace's, then UTC — `resolveSiteTimeZone`'s chain, so a booking and the
27
+ * site's own dates agree.
28
+ *
29
+ * Every candidate is validated, never trusted: a zone that cannot format would
30
+ * throw a `RangeError` in the middle of a send.
31
+ */ export function bookingTimeZone(input) {
32
+ var _ref, _input_org, _input_host;
33
+ var _input_service;
34
+ const serviceZone = String((_ref = (_input_service = input.service) == null ? void 0 : _input_service.timezone) != null ? _ref : '').trim();
35
+ if (isSupportedTimeZone(serviceZone)) return serviceZone;
36
+ return resolveSiteTimeZone((_input_org = input.org) != null ? _input_org : null, (_input_host = input.host) != null ? _input_host : null);
37
+ }
38
+ /**
39
+ * The zone a booking document carries, when it carries a usable one.
40
+ *
41
+ * Bookings written before the zone was stored have none, and a reader then
42
+ * resolves it from the service and site with {@link bookingTimeZone}.
43
+ */ export function storedBookingTimeZone(booking) {
44
+ var _ref;
45
+ const zone = String((_ref = booking == null ? void 0 : booking.timezone) != null ? _ref : '').trim();
46
+ return isSupportedTimeZone(zone) ? zone : undefined;
47
+ }
48
+ /**
49
+ * A booking's start as a guest reads it — "Friday, April 24, 2026 at 3:00 PM"
50
+ * — in `timeZone`. The caller names the zone beside it; the string alone does
51
+ * not say which zone it is in.
52
+ *
53
+ * Never the server's own zone: a formatter with no `timeZone` reads in UTC on
54
+ * the platform's servers and prints a wall-clock time hours away from the
55
+ * appointment. An unusable zone formats in UTC, which is what the caller then
56
+ * names.
57
+ */ export function formatBookingWhen(startsAtMs, timeZone) {
58
+ return new Intl.DateTimeFormat('en-US', {
59
+ timeZone: isSupportedTimeZone(timeZone) ? timeZone : 'UTC',
60
+ dateStyle: 'full',
61
+ timeStyle: 'short'
62
+ }).format(new Date(startsAtMs));
63
+ }
64
+
65
+ //# sourceMappingURL=booking-time.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/booking-time.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\n// The leaf, not the barrel: this model is read by the client bundle too, and\n// the zone rules are a dependency-free module.\nimport {\n isSupportedTimeZone,\n resolveSiteTimeZone,\n} from '@aglyn/aglyn/app-utils/collection-entry-date'\n\n/**\n * The zone a booking's time is told in, to the guest and to the site's\n * managers alike (AGL-3432).\n *\n * THE SERVICE WINS: its weekly windows are defined in its own zone, so that is\n * the zone the slot was offered in. A service that names none (or names one\n * this runtime no longer accepts) reads in the site's zone, then the\n * workspace's, then UTC — `resolveSiteTimeZone`'s chain, so a booking and the\n * site's own dates agree.\n *\n * Every candidate is validated, never trusted: a zone that cannot format would\n * throw a `RangeError` in the middle of a send.\n */\nexport function bookingTimeZone(input: {\n service?: { timezone?: unknown } | null\n host?: { timeZone?: string } | null\n org?: { timeZone?: string } | null\n}): string {\n const serviceZone = String(input.service?.timezone ?? '').trim()\n if (isSupportedTimeZone(serviceZone)) return serviceZone\n return resolveSiteTimeZone(input.org ?? null, input.host ?? null)\n}\n\n/**\n * The zone a booking document carries, when it carries a usable one.\n *\n * Bookings written before the zone was stored have none, and a reader then\n * resolves it from the service and site with {@link bookingTimeZone}.\n */\nexport function storedBookingTimeZone(booking: {\n timezone?: unknown\n} | null | undefined): string | undefined {\n const zone = String(booking?.timezone ?? '').trim()\n return isSupportedTimeZone(zone) ? zone : undefined\n}\n\n/**\n * A booking's start as a guest reads it — \"Friday, April 24, 2026 at 3:00 PM\"\n * — in `timeZone`. The caller names the zone beside it; the string alone does\n * not say which zone it is in.\n *\n * Never the server's own zone: a formatter with no `timeZone` reads in UTC on\n * the platform's servers and prints a wall-clock time hours away from the\n * appointment. An unusable zone formats in UTC, which is what the caller then\n * names.\n */\nexport function formatBookingWhen(startsAtMs: number, timeZone: string): string {\n return new Intl.DateTimeFormat('en-US', {\n timeZone: isSupportedTimeZone(timeZone) ? timeZone : 'UTC',\n dateStyle: 'full',\n timeStyle: 'short',\n }).format(new Date(startsAtMs))\n}\n"],"names":["isSupportedTimeZone","resolveSiteTimeZone","bookingTimeZone","input","serviceZone","String","service","timezone","trim","org","host","storedBookingTimeZone","booking","zone","undefined","formatBookingWhen","startsAtMs","timeZone","Intl","DateTimeFormat","dateStyle","timeStyle","format","Date"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,6EAA6E;AAC7E,+CAA+C;AAC/C,SACEA,mBAAmB,EACnBC,mBAAmB,QACd,+CAA8C;AAErD;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,gBAAgBC,KAI/B;cAG4BA,YAAmBA;QAFnBA;IAA3B,MAAMC,cAAcC,gBAAOF,iBAAAA,MAAMG,OAAO,qBAAbH,eAAeI,QAAQ,mBAAI,IAAIC,IAAI;IAC9D,IAAIR,oBAAoBI,cAAc,OAAOA;IAC7C,OAAOH,qBAAoBE,aAAAA,MAAMM,GAAG,YAATN,aAAa,OAAMA,cAAAA,MAAMO,IAAI,YAAVP,cAAc;AAC9D;AAEA;;;;;CAKC,GACD,OAAO,SAASQ,sBAAsBC,OAElB;;IAClB,MAAMC,OAAOR,eAAOO,2BAAAA,QAASL,QAAQ,mBAAI,IAAIC,IAAI;IACjD,OAAOR,oBAAoBa,QAAQA,OAAOC;AAC5C;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASC,kBAAkBC,UAAkB,EAAEC,QAAgB;IACpE,OAAO,IAAIC,KAAKC,cAAc,CAAC,SAAS;QACtCF,UAAUjB,oBAAoBiB,YAAYA,WAAW;QACrDG,WAAW;QACXC,WAAW;IACb,GAAGC,MAAM,CAAC,IAAIC,KAAKP;AACrB"}