@aglyn/plugins-bookings 1.0.0-beta.200 → 1.0.0-beta.201

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-bookings",
3
- "version": "1.0.0-beta.200",
3
+ "version": "1.0.0-beta.201",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,16 +25,16 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.200",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.200",
30
- "@aglyn/shared-ui-jsx": "1.0.0-beta.200",
31
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.200",
32
- "@aglyn/shared-util-email": "1.0.0-beta.200",
33
- "@aglyn/shared-util-http": "1.0.0-beta.200",
34
- "@aglyn/shared-util-timestamp": "1.0.0-beta.200",
35
- "@aglyn/tenant-data-admin": "1.0.0-beta.200",
36
- "@aglyn/tenant-feature-instance": "1.0.0-beta.200",
37
- "@aglyn/tenant-runtime": "1.0.0-beta.200",
28
+ "@aglyn/aglyn": "1.0.0-beta.201",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.201",
30
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.201",
31
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.201",
32
+ "@aglyn/shared-util-email": "1.0.0-beta.201",
33
+ "@aglyn/shared-util-http": "1.0.0-beta.201",
34
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.201",
35
+ "@aglyn/tenant-data-admin": "1.0.0-beta.201",
36
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.201",
37
+ "@aglyn/tenant-runtime": "1.0.0-beta.201",
38
38
  "@swc/helpers": "0.5.23"
39
39
  },
40
40
  "peerDependencies": {
@@ -19,12 +19,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
19
  import { checkQuota, crmContactByEmailHref, pluginDocsHelp } from "@aglyn/aglyn";
20
20
  import { BOOKINGS_BOOKER_PARAM, normalizeContactEmail } from "@aglyn/aglyn";
21
21
  import { PLATFORM_BRAND_NAME } from "@aglyn/aglyn/app-utils/platform-brand";
22
+ import { describePaymentRisk } from "@aglyn/aglyn/app-utils/payment-risk";
22
23
  import { isBookingReminderDue } from "../model/index.js";
23
24
  import { AppLink, CardDisplay, HelpTip, useConfirmationContext } from "@aglyn/shared-ui-jsx";
24
25
  import QuotaReadoutComponent from "@aglyn/shared-ui-jsx/components/quota-readout.component";
25
26
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
26
27
  import { Timestamp } from "@aglyn/shared-util-timestamp";
27
- import { Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, Stack, Switch, TextField, Typography } from "@mui/material";
28
+ import { Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, Stack, Switch, TextField, Tooltip, Typography } from "@mui/material";
28
29
  import { collection, doc, limit, orderBy, query, setDoc, updateDoc } from "firebase/firestore";
29
30
  import { useParams, useSearchParams } from "next/navigation";
30
31
  import { useCallback, useState } from "react";
@@ -57,6 +58,18 @@ function formatWindows(windows) {
57
58
  const pad = (minutes)=>`${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`;
58
59
  return (windows != null ? windows : []).map((window)=>`${pad(window.start)}-${pad(window.end)}`).join(', ');
59
60
  }
61
+ /** A fraud warning, review or chargeback on the booking's payment (AGL-3360). */ function PaymentRiskChip({ risk }) {
62
+ const described = describePaymentRisk(risk);
63
+ if (!described) return null;
64
+ return /*#__PURE__*/ _jsx(Tooltip, {
65
+ title: described.detail,
66
+ children: /*#__PURE__*/ _jsx(Chip, {
67
+ label: described.label,
68
+ size: "small",
69
+ color: "warning"
70
+ })
71
+ });
72
+ }
60
73
  /**
61
74
  * Bookings manager (AGL-159): bookable services with weekly availability
62
75
  * windows, and the upcoming-bookings list with cancel. Visitors book via
@@ -553,6 +566,9 @@ function formatWindows(windows) {
553
566
  })
554
567
  ]
555
568
  }),
569
+ /*#__PURE__*/ _jsx(PaymentRiskChip, {
570
+ risk: booking.paymentRisk
571
+ }),
556
572
  siteContext && booking.email ? /*#__PURE__*/ _jsx(Button, _extends({
557
573
  component: AppLink
558
574
  }, {
@@ -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 { 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 Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n FormControlLabel,\n Stack,\n Switch,\n TextField,\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/**\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 {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","isBookingReminderDue","AppLink","CardDisplay","HelpTip","useConfirmationContext","QuotaReadoutComponent","useSnackbar","Timestamp","Button","Dialog","DialogActions","DialogContent","DialogTitle","FormControlLabel","Stack","Switch","TextField","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","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","title","confirmationText","confirmationButtonProps","color","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","size","onClick","alignSelf","ready","used","noun","component","mb","href","ml","fontSize","serviceName","toLocaleString","componentVariant","nativeButton","open","Boolean","onClose","maxWidth","fullWidth","display","flexDirection","gap","pt","label","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, 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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"}
@@ -15,9 +15,4 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import type { BillingWebhookHandler } from '@aglyn/aglyn/server';
18
- /**
19
- * Paid-booking section of the platform Stripe webhook (AGL-170/418):
20
- * payment confirms the pendingPayment hold — relocated verbatim from the
21
- * console route; registered via registerBookingsConsoleApi.
22
- */
23
18
  export declare const bookingsBillingWebhookHandler: BillingWebhookHandler;
@@ -20,7 +20,9 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
20
20
  // work scheduled any other way simply does not run.
21
21
  import { after } from "next/server";
22
22
  import { resolveBrandingProfile } from "@aglyn/aglyn/server";
23
- import { firebaseAdmin, getOrgForHost, hostSendingIdentity, meterHostEmail, renderHostEmailWithTokens, sendGa4Purchase } from "@aglyn/tenant-data-admin";
23
+ import { firebaseAdmin, getOrgForHost, hostSendingIdentity, meterHostEmail, notifyHostManagers, renderHostEmailWithTokens, sendGa4Purchase } from "@aglyn/tenant-data-admin";
24
+ import { paymentRiskEventFrom } from "@aglyn/aglyn/app-utils/payment-risk";
25
+ import { recordPaymentRiskOnRecord, settlePaymentRiskDisputeOnRecord } from "@aglyn/tenant-data-admin/server/payment-risk-record";
24
26
  import { captureHostContact } from "@aglyn/tenant-runtime";
25
27
  import { bookingPlatformNetCents, shouldSendBookingPlatformPurchase } from "../model/booking-purchase-analytics.js";
26
28
  import { sendEmail } from "@aglyn/shared-util-email";
@@ -30,21 +32,87 @@ import { fileBookingOnCrm } from "./booking-crm.js";
30
32
  * Paid-booking section of the platform Stripe webhook (AGL-170/418):
31
33
  * payment confirms the pendingPayment hold — relocated verbatim from the
32
34
  * console route; registered via registerBookingsConsoleApi.
33
- */ export const bookingsBillingWebhookHandler = async ({ type, object })=>{
35
+ */ /** gRPC `Status.FAILED_PRECONDITION` — Firestore's "this query needs an index". */ const GRPC_FAILED_PRECONDITION = 9;
36
+ /**
37
+ * The booking a charge paid for, found by the `paymentIntentId` the paid
38
+ * handler below records (AGL-2315), or null. A missing collection-group
39
+ * index is logged and answered null rather than thrown: no redelivery can
40
+ * fix it, and the dispute then reaches staff through the route's
41
+ * unattributed-dispute alert instead of vanishing.
42
+ */ async function findBookingForPayment(paymentIntentId) {
43
+ if (!paymentIntentId) return null;
44
+ const matches = await firebaseAdmin.app().firestore().collectionGroup('bookings').where('paymentIntentId', '==', paymentIntentId).limit(2).get().catch((error)=>{
45
+ if ((error == null ? void 0 : error.code) !== GRPC_FAILED_PRECONDITION) throw error;
46
+ console.error('Booking payment lookup needs the collection-group index on ' + 'bookings.paymentIntentId (AGL-3360)', error);
47
+ return null;
48
+ });
49
+ // One payment intent pays one booking; two is corrupt data, and stamping
50
+ // an arbitrary one would tell the wrong merchant.
51
+ return matches && matches.docs.length === 1 ? matches.docs[0] : null;
52
+ }
53
+ export const bookingsBillingWebhookHandler = async ({ type, object })=>{
34
54
  var _object_metadata;
55
+ // Fraud signals on a paid booking (AGL-3360): an issuer's early fraud
56
+ // warning, a Radar review, or a dispute. Booking deposits never heard
57
+ // about any of them, so the merchant learned of a chargeback from Stripe
58
+ // alone. The signal now lands on the BOOKING, through the shared
59
+ // `paymentRisk` shape every plugin stamps, and the site's managers are
60
+ // told once. Nothing is refunded or canceled: the merchant decides.
61
+ const risk = paymentRiskEventFrom(type, object);
62
+ if (risk) {
63
+ var _ref, _booking_get;
64
+ var _booking_ref_parent_parent;
65
+ const booking = await findBookingForPayment(risk.paymentIntentId);
66
+ if (!booking) return;
67
+ const hostId = String((_ref = (_booking_ref_parent_parent = booking.ref.parent.parent) == null ? void 0 : _booking_ref_parent_parent.id) != null ? _ref : '');
68
+ await recordPaymentRiskOnRecord({
69
+ ref: booking.ref,
70
+ signal: risk.signal,
71
+ hostId,
72
+ subjectLabel: `Booking ${String((_booking_get = booking.get('serviceName')) != null ? _booking_get : booking.id)}`,
73
+ link: `/${hostId}/bookings`,
74
+ notificationType: 'content.booking'
75
+ }, {
76
+ firestore: firebaseAdmin.app().firestore(),
77
+ notify: notifyHostManagers
78
+ });
79
+ return {
80
+ claimed: true,
81
+ hostId
82
+ };
83
+ }
84
+ // A dispute's outcome, on the signal `created` stamped. Claimed only when
85
+ // it was NOT lost: a lost booking dispute takes money back that nothing
86
+ // here reverses or records, which is exactly what the route's
87
+ // unattributed-dispute alert is for, so staff still hear about it.
88
+ if (type === 'charge.dispute.closed') {
89
+ var _ref1, _ref2, _ref3, _ref4;
90
+ var _object_payment_intent, _booking_ref_parent_parent1;
91
+ const paymentIntentId = typeof (object == null ? void 0 : object.payment_intent) === 'string' ? object.payment_intent : String((_ref1 = object == null ? void 0 : (_object_payment_intent = object.payment_intent) == null ? void 0 : _object_payment_intent.id) != null ? _ref1 : '');
92
+ const booking = await findBookingForPayment(paymentIntentId);
93
+ if (!booking) return;
94
+ const outcome = String((_ref2 = object == null ? void 0 : object.status) != null ? _ref2 : '');
95
+ await settlePaymentRiskDisputeOnRecord(booking.ref, String((_ref3 = object == null ? void 0 : object.id) != null ? _ref3 : ''), outcome, {
96
+ firestore: firebaseAdmin.app().firestore()
97
+ });
98
+ return outcome === 'lost' ? undefined : {
99
+ claimed: true,
100
+ hostId: String((_ref4 = (_booking_ref_parent_parent1 = booking.ref.parent.parent) == null ? void 0 : _booking_ref_parent_parent1.id) != null ? _ref4 : '')
101
+ };
102
+ }
35
103
  // Paid bookings (AGL-170): payment confirms the pendingPayment hold.
36
104
  if (type === 'checkout.session.completed' && (object == null ? void 0 : (_object_metadata = object.metadata) == null ? void 0 : _object_metadata.type) === 'booking-payment' && (object == null ? void 0 : object.payment_status) === 'paid') {
37
105
  var _object_metadata1;
38
106
  const { hostId, bookingId } = (_object_metadata1 = object.metadata) != null ? _object_metadata1 : {};
39
107
  if (hostId && bookingId) {
40
- var _ref, _ref1, _ref2, _ref3;
41
- var _object_metadata2, _object_payment_intent, _object_metadata3;
108
+ var _ref5, _ref6, _ref7, _ref8;
109
+ var _object_metadata2, _object_payment_intent1, _object_metadata3;
42
110
  const firestore = firebaseAdmin.app().firestore();
43
111
  const bookingRef = firestore.collection('hosts').doc(String(hostId)).collection('bookings').doc(String(bookingId));
44
112
  // WHAT THE CLIENT HANDED OVER, ALL OF IT. Tax included: this is the
45
113
  // ceiling `refund.ts` reverses against, and the client paid the tax
46
114
  // too — a net figure here would strand part of a refund.
47
- const paidAmountCents = Math.max(0, Math.round(Number((_ref = object == null ? void 0 : object.amount_total) != null ? _ref : 0)));
115
+ const paidAmountCents = Math.max(0, Math.round(Number((_ref5 = object == null ? void 0 : object.amount_total) != null ? _ref5 : 0)));
48
116
  // THE MERCHANT'S SERVICE TAX, SEPARATED OUT (AGL-2028).
49
117
  //
50
118
  // `server.ts` charges the merchant's own service rate as an ordinary
@@ -54,7 +122,7 @@ import { fileBookingOnCrm } from "./booking-crm.js";
54
122
  // `amount_total` is service-plus-tax while Stripe's own tax fields
55
123
  // read zero, and the session's metadata is the only witness — exactly
56
124
  // as it is for a buy-now sale.
57
- const taxCents = Math.max(0, Math.round(Number((_ref1 = object == null ? void 0 : (_object_metadata2 = object.metadata) == null ? void 0 : _object_metadata2.taxCents) != null ? _ref1 : 0)));
125
+ const taxCents = Math.max(0, Math.round(Number((_ref6 = object == null ? void 0 : (_object_metadata2 = object.metadata) == null ? void 0 : _object_metadata2.taxCents) != null ? _ref6 : 0)));
58
126
  // The merchant's REVENUE, which is the charge less the tax. Used for
59
127
  // the contact's lifetime value below: tax is collected on behalf of
60
128
  // an authority and is not money the business earned, so counting it
@@ -75,8 +143,8 @@ import { fileBookingOnCrm } from "./booking-crm.js";
75
143
  // rate follows the plan (AGL-2289's rule) and the plan moves, so
76
144
  // re-resolving it during a refund would reverse a share that was never
77
145
  // taken. `'0'` is a real recorded answer on the 0%-rate tiers.
78
- const paymentIntentId = typeof (object == null ? void 0 : object.payment_intent) === 'string' ? object.payment_intent : String((_ref2 = object == null ? void 0 : (_object_payment_intent = object.payment_intent) == null ? void 0 : _object_payment_intent.id) != null ? _ref2 : '');
79
- const chargedFeeCents = Math.max(0, Math.round(Number((_ref3 = object == null ? void 0 : (_object_metadata3 = object.metadata) == null ? void 0 : _object_metadata3.feeCents) != null ? _ref3 : 0)));
146
+ const paymentIntentId = typeof (object == null ? void 0 : object.payment_intent) === 'string' ? object.payment_intent : String((_ref7 = object == null ? void 0 : (_object_payment_intent1 = object.payment_intent) == null ? void 0 : _object_payment_intent1.id) != null ? _ref7 : '');
147
+ const chargedFeeCents = Math.max(0, Math.round(Number((_ref8 = object == null ? void 0 : (_object_metadata3 = object.metadata) == null ? void 0 : _object_metadata3.feeCents) != null ? _ref8 : 0)));
80
148
  // Redelivery guard (AGL-1755), the AGL-1748 shape. This was an
81
149
  // unconditional merge-set with no status check at all — idempotent
82
150
  // only by accident, because every value it wrote was fixed. A
@@ -303,7 +371,7 @@ import { fileBookingOnCrm } from "./booking-crm.js";
303
371
  // Confirmation email now that payment cleared (env-gated inside
304
372
  // sendEmail, which no-ops when Resend isn't configured).
305
373
  if (booking['email']) {
306
- var _booking_name, _booking_serviceName1, _booking_name1, _booking_serviceName2, _booking_timezone, _ref4, _booking_serviceName3;
374
+ var _booking_name, _booking_serviceName1, _booking_name1, _booking_serviceName2, _booking_timezone, _ref9, _booking_serviceName3;
307
375
  var _this;
308
376
  const when = new Date(Number(booking['startsAtMs'])).toLocaleString('en-US', {
309
377
  dateStyle: 'full',
@@ -324,7 +392,7 @@ import { fileBookingOnCrm } from "./booking-crm.js";
324
392
  const branding = resolveBrandingProfile((_this = await getOrgForHost(String(hostId)).catch(()=>null)) == null ? void 0 : _this.org);
325
393
  await sendEmail(_extends({
326
394
  to: String(booking['email']),
327
- subject: (_ref4 = designed == null ? void 0 : designed.subject) != null ? _ref4 : `Booking confirmed: ${(_booking_serviceName3 = booking['serviceName']) != null ? _booking_serviceName3 : ''}`,
395
+ subject: (_ref9 = designed == null ? void 0 : designed.subject) != null ? _ref9 : `Booking confirmed: ${(_booking_serviceName3 = booking['serviceName']) != null ? _booking_serviceName3 : ''}`,
328
396
  text: (designed == null ? void 0 : designed.text) || fallbackText
329
397
  }, (designed == null ? void 0 : designed.html) ? {
330
398
  html: designed.html
@@ -332,7 +400,10 @@ import { fileBookingOnCrm } from "./booking-crm.js";
332
400
  fromName: branding.fromName,
333
401
  sendingIdentity: await hostSendingIdentity(String(hostId)),
334
402
  audience: 'tenant',
335
- context: 'paid booking confirmation'
403
+ context: 'paid booking confirmation',
404
+ // Owed to the recipient by their own booking: the phishing
405
+ // screen's soft rules never hold it (AGL-3356).
406
+ owedFor: 'booking'
336
407
  }));
337
408
  // Cost meter (AGL-1438), matching the free-booking path.
338
409
  // Transactional: the guest has already paid.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/server/billing-webhook.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// `after()`, never a bare `void promise` (AGL-2327). This handler runs inside\n// the console's `/api/billing/webhook` invocation, which AGL-1133 measured on\n// production is frozen the moment the response is sent — so fire-and-forget\n// work scheduled any other way simply does not run.\nimport { after } from 'next/server'\n\nimport type { BillingWebhookHandler } from '@aglyn/aglyn/server'\nimport { resolveBrandingProfile } from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n getOrgForHost,\n hostSendingIdentity,\n meterHostEmail,\n renderHostEmailWithTokens,\n sendGa4Purchase,\n} from '@aglyn/tenant-data-admin'\nimport { captureHostContact } from '@aglyn/tenant-runtime'\nimport {\n bookingPlatformNetCents,\n shouldSendBookingPlatformPurchase,\n} from '../model/booking-purchase-analytics'\nimport { sendEmail } from '@aglyn/shared-util-email'\nimport { pluginTaxProfile } from '@aglyn/aglyn/plugin-manager/plugin-tax-profile'\nimport { fileBookingOnCrm } from './booking-crm'\n\n/**\n * Paid-booking section of the platform Stripe webhook (AGL-170/418):\n * payment confirms the pendingPayment hold — relocated verbatim from the\n * console route; registered via registerBookingsConsoleApi.\n */\nexport const bookingsBillingWebhookHandler: BillingWebhookHandler = async ({\n type,\n object,\n}) => {\n // Paid bookings (AGL-170): payment confirms the pendingPayment hold.\n if (\n type === 'checkout.session.completed' &&\n object?.metadata?.type === 'booking-payment' &&\n object?.payment_status === 'paid'\n ) {\n const { hostId, bookingId } = object.metadata ?? {}\n if (hostId && bookingId) {\n const firestore = firebaseAdmin.app().firestore()\n const bookingRef = firestore\n .collection('hosts')\n .doc(String(hostId))\n .collection('bookings')\n .doc(String(bookingId))\n // WHAT THE CLIENT HANDED OVER, ALL OF IT. Tax included: this is the\n // ceiling `refund.ts` reverses against, and the client paid the tax\n // too — a net figure here would strand part of a refund.\n const paidAmountCents = Math.max(\n 0,\n Math.round(Number(object?.amount_total ?? 0)),\n )\n // THE MERCHANT'S SERVICE TAX, SEPARATED OUT (AGL-2028).\n //\n // `server.ts` charges the merchant's own service rate as an ordinary\n // `line_items[1]` Stripe is never told is tax (the AGL-1711\n // construction, which is what keeps the figure the MERCHANT's rather\n // than something computed against Aglyn's registrations). So\n // `amount_total` is service-plus-tax while Stripe's own tax fields\n // read zero, and the session's metadata is the only witness — exactly\n // as it is for a buy-now sale.\n const taxCents = Math.max(\n 0,\n Math.round(Number(object?.metadata?.taxCents ?? 0)),\n )\n // The merchant's REVENUE, which is the charge less the tax. Used for\n // the contact's lifetime value below: tax is collected on behalf of\n // an authority and is not money the business earned, so counting it\n // would over-state every service client's worth.\n const serviceCents = Math.max(0, paidAmountCents - taxCents)\n // WHAT A REFUND WILL NEED (AGL-2315). A paid booking is now a\n // destination charge to the merchant's connected account, and\n // reversing one requires the PaymentIntent — which lives only on this\n // event. The booking document recorded `paidAmountCents` and nothing\n // that identifies the charge, so a refund had no handle to pull and\n // the information was gone the moment the webhook returned. Unlike\n // most gaps this one cannot be backfilled from our own data later:\n // every booking taken before this line landed can only be refunded by\n // hand in the Stripe dashboard.\n //\n // `feeCents` is Aglyn's cut as CHARGED, read back off the session's\n // own metadata rather than re-derived from the org at refund time. The\n // rate follows the plan (AGL-2289's rule) and the plan moves, so\n // re-resolving it during a refund would reverse a share that was never\n // taken. `'0'` is a real recorded answer on the 0%-rate tiers.\n const paymentIntentId =\n typeof object?.payment_intent === 'string'\n ? object.payment_intent\n : String(object?.payment_intent?.id ?? '')\n const chargedFeeCents = Math.max(\n 0,\n Math.round(Number(object?.metadata?.feeCents ?? 0)),\n )\n // Redelivery guard (AGL-1755), the AGL-1748 shape. This was an\n // unconditional merge-set with no status check at all — idempotent\n // only by accident, because every value it wrote was fixed. A\n // redelivery re-sent the guest's confirmation and re-metered it, and\n // the moment this handler carries money to a contact the accident stops\n // holding: `purchaseCents` is a `FieldValue.increment`, so every retry\n // would inflate the customer's lifetime value.\n //\n // Keyed on the STATUS, not on the document existing: `server.ts` writes\n // the booking as `pendingPayment` BEFORE it opens the Stripe session,\n // so existence is guaranteed by the time any event arrives and proves\n // nothing — the AGL-1748 reasoning, not AGL-1732's `checkoutSessionId`\n // key, which existed because a sibling event wrote the same path.\n //\n // The key is \"not yet confirmed\" rather than \"is pendingPayment\" on\n // purpose. The lapsed-hold sweeper in `server.ts` cancels a\n // `pendingPayment` booking after 24h, and refusing a payment that\n // landed anyway would take the money and record nothing; `confirmed` is\n // reachable for a PAID booking only by this handler having already run,\n // so it is an exact \"already processed\" test. The existence check is\n // new and deliberate: the old merge-set would CREATE a stub booking\n // from a metadata `bookingId` that pointed at nothing.\n let booking: Record<string, any> = {}\n const confirmedNow = await firestore.runTransaction(\n async (transaction) => {\n const snapshot = await transaction.get(bookingRef)\n if (!snapshot.exists) return false\n if (snapshot.get('status') === 'confirmed') return false\n // \"Already processed\" is WIDER than `confirmed` once money can\n // flow back out (AGL-2315). A refunded booking is moved off\n // `confirmed`, so a redelivery arriving afterwards would otherwise\n // read as unprocessed and re-confirm an appointment the merchant\n // has already cancelled and refunded — re-sending the guest a\n // confirmation for it and re-metering the email. Any evidence this\n // handler has run before is the real test, and a refund counter or\n // a recorded PaymentIntent is exactly that evidence.\n if (\n Number(snapshot.get('refundedCents') ?? 0) > 0 ||\n snapshot.get('paymentIntentId')\n ) {\n return false\n }\n booking = (snapshot.data() as any) ?? {}\n transaction.set(\n bookingRef,\n {\n status: 'confirmed',\n paidAmountCents,\n // WHICH TAX REGIME THIS BOOKING CARRIED (AGL-2028), on the\n // document the merchant reads — the stamp every other\n // storefront money door already carries (AGL-2451).\n //\n // DERIVED from the one shared derivation, never a constant.\n // A hand-written \"manual when the metadata says so\" would be\n // a second rule that can drift from the `storefrontTaxCollected`\n // row filed for the same Stripe id, which is the exact\n // disagreement AGL-2451 exists to prevent.\n //\n // TWO-ARGUMENT, because the manual line makes Stripe report\n // `total_details.amount_tax: 0` on a booking that really did\n // charge the client tax; the one-argument form would stamp\n // `none` on precisely those. `absent` remains a fourth state\n // meaning \"recorded before this shipped\".\n taxMode: pluginTaxProfile().taxModeOf(object, taxCents),\n // The figure itself. Absent rather than a defaulted `0` — a\n // zero written through `merge` is the AGL-1758 shape, and\n // this handler can re-enter (see the widened guard above).\n ...(taxCents > 0 ? { taxCents } : {}),\n // The refund handles (AGL-2315). Written inside the same\n // transaction as the status, so a booking can never read\n // `confirmed` while the means to refund it are missing.\n ...(paymentIntentId ? { paymentIntentId } : {}),\n feeCents: chargedFeeCents,\n // WHICH CHECKOUT PAID FOR THIS (AGL-2481). The guest's browser\n // comes back from Stripe holding a session id and nothing\n // else — a booking id was never in the return URL — so this is\n // what lets `booking-analytics.ts` answer \"what did I just\n // buy?\" for the merchant-side `purchase`, and it is written\n // here because the session id first exists on this event.\n //\n // Inside the transaction with the status on purpose: the\n // lookup refuses anything that is not `confirmed`, so a\n // booking can never be findable by session id while reading as\n // unpaid.\n ...(object?.id ? { checkoutSessionId: String(object.id) } : {}),\n // NOTE the absent `refundedCents: 0`. Seeding it here would be\n // the AGL-1758 shape: a defaulted field written through\n // `merge` destroying the real one. This handler can re-enter\n // after a refund has already accumulated a counter (see the\n // widened guard above), and a zero written back over it would\n // re-open the full amount for refunding a second time. Absent\n // IS zero — every reader coerces with `?? 0`.\n expiresAtMs: firebaseAdmin.firestore.FieldValue.delete(),\n confirmedAt:\n firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n return true\n },\n )\n if (!confirmedNow) return\n // THE RECORD'S MEETING (AGL-2660). Payment is what confirms a paid\n // booking, so this is where it reaches the CRM: the meeting on the\n // record's timeline and, when the service asks, the follow-up task.\n // Inside the idempotency gate for the same reason the GA4 purchase\n // below is — a redelivery must not file a second meeting — and\n // through `after()` for the same reason too: this invocation is\n // frozen the moment the response is sent (AGL-2327). Never thrown:\n // the writer swallows its own failures, and the catch is for the\n // scheduling itself.\n after(() =>\n fileBookingOnCrm(firestore, {\n hostId: String(hostId),\n bookingId: String(bookingId),\n booking,\n }).catch(() => undefined),\n )\n // AGLYN'S REVENUE ON THIS BOOKING (AGL-2481) — into OUR GA4 property.\n //\n // ## Why it is inside the idempotency gate rather than beside it\n //\n // This sits AFTER `if (!confirmedNow) return` and that placement is the\n // whole guard. Stripe redelivers `checkout.session.completed` for up to\n // three days after any 500, and this endpoint 500s on purpose; a\n // redelivery re-enters this handler, the transaction above sees a\n // `confirmed` status (or a refund counter, or a recorded PaymentIntent)\n // and answers false, and everything below — the contact, the email, and\n // now this — is skipped. A `purchase` fired before that gate, or beside\n // it, would inflate our own reported revenue by the redelivery rate.\n //\n // `transaction_id` is the checkout session id, which is the same key\n // the guard turns on, so GA4's own de-duplication is a second\n // independent line of defence: even if the Firestore guard were ever\n // bypassed, GA would collapse the repeats.\n //\n // ## Platform NET, and the fee is the measured one\n //\n // `value` is our fee and not the $95 the guest paid — the AGL-1639\n // settlement. This is our property, the subscription and marketplace\n // `purchase` events in it already mean \"what Aglyn was paid\", and a\n // gross booking figure sitting beside them would make every combined\n // total, ARPA and revenue audience wrong. The merchant's own gross goes\n // to the MERCHANT's property, client-side, and the two never meet.\n //\n // The figure comes off the session's `metadata.feeCents` — the fee\n // Stripe actually charged as `application_fee_amount` — never from the\n // plan's rate re-applied here. See `bookingPlatformNetCents`.\n //\n // ## Scheduling and failure posture\n //\n // `after()`, never a bare `void` (AGL-2327/AGL-2346): this runs inside\n // the console's `/api/billing/webhook` invocation, which is frozen the\n // moment the response is sent, so fire-and-forget work scheduled any\n // other way simply does not run — the exact way marketplace revenue\n // reported to nothing. And `.catch()`, because a throw here would\n // un-claim the Stripe event and turn a missed analytics hit into a\n // repeated billing side effect.\n if (shouldSendBookingPlatformPurchase(object)) {\n const netCents = bookingPlatformNetCents(object)\n after(() =>\n sendGa4Purchase({\n transactionId: String(object?.id ?? ''),\n value: netCents / 100,\n currency: String(object?.currency ?? 'usd'),\n items: [\n {\n // Ids only, no merchant free text. A service name is\n // merchant-authored and one edit away from carrying a\n // person's name into a dimension in OUR property; the id\n // already identifies it. The merchant's own property gets\n // the name, because there it is their own content.\n item_id: String(booking['serviceId'] ?? bookingId),\n item_name: String(booking['serviceId'] ?? bookingId),\n item_category: 'booking',\n // GA expects the items to sum to `value`; one item, one\n // price, and that price is our fee — not the service's.\n price: netCents / 100,\n quantity: 1,\n },\n ],\n // Booking sessions carry no `ga_client_id` today — nothing\n // captures one on a tenant site — so this is read for the day\n // one is stamped and the seed below is what actually resolves.\n clientId: object?.metadata?.ga_client_id,\n // ONE PAYING GUEST, ONE SYNTHETIC USER. Seeded from Stripe's\n // customer where there is one, and otherwise from the guest's\n // email, which `server.ts` passes as `customer_email` and Stripe\n // echoes on `customer_details`. The seed is hashed inside\n // `synthesizeClientId` and never transmitted.\n //\n // The booking id is the LAST resort deliberately: it is unique\n // per appointment, so seeding from it would turn a regular\n // client into a crowd of one-purchase strangers and make ARPA\n // nonsense — precisely what that helper's determinism exists to\n // prevent. Without any seed at all `sendGa4Purchase` returns\n // `no-client-id` and the event is silently never sent, which is\n // the void this whole change exists to close.\n stripeCustomerId:\n String(object?.customer ?? '') ||\n String(object?.customer_details?.email ?? '') ||\n String(bookingId),\n }).catch(() => undefined),\n )\n }\n // Contacts ingestion (AGL-1755). `server.ts` captures the contact at\n // REQUEST time, which is the right place — the booking is written\n // `pendingPayment` and at that moment no money has moved, so passing an\n // amount there would be a lie. But payment completes HERE, and this\n // handler never returned to the contact, so a paid booking's money\n // reached the booking document and nothing else. The same structural\n // shape as AGL-1748's draft branch: the completion handler does\n // everything except the contact.\n //\n // `source` stays `'booking'` — the same source the request-time capture\n // used — so the `sources` map still says where this person came from\n // and no schema change is needed to keep service revenue\n // distinguishable from product revenue. The second interaction is\n // deliberate rather than a duplicate: \"Booked X\" and \"Paid for X\" are\n // two real events, and `mergeContactInteraction` does not dedupe on\n // `refId`.\n //\n // The amount is `amount_total`, the money that moved, per AGL-1698 and\n // AGL-1711 — the booking document stores no price to re-derive it from.\n if (booking['email']) {\n void captureHostContact({\n hostId: String(hostId),\n email: booking['email'],\n ...(booking['name'] ? { name: String(booking['name']) } : {}),\n source: 'booking',\n // Paid, so a customer (AGL-2612) — the request-time capture\n // said `lead`, and this is the door that has seen the money.\n initialLifecycleStage: 'customer',\n // The SERVICE, not the charge (AGL-2028). `amount_total` now\n // includes the merchant's service tax where they set one, and\n // tax is collected for an authority rather than earned — booking\n // it as lifetime value would over-state what every service\n // client is worth. Identical to `paidAmountCents` on the default\n // store, which sets no rate.\n ...(serviceCents > 0 ? { purchaseCents: serviceCents } : {}),\n interaction: {\n refId: String(bookingId),\n summary: `Paid for \"${String(\n booking['serviceName'] ?? 'a service',\n ).slice(0, 60)}\" ($${(serviceCents / 100).toFixed(2)})`,\n },\n })\n }\n // Confirmation email now that payment cleared (env-gated inside\n // sendEmail, which no-ops when Resend isn't configured).\n if (booking['email']) {\n const when = new Date(\n Number(booking['startsAtMs']),\n ).toLocaleString('en-US', {\n dateStyle: 'full',\n timeStyle: 'short',\n })\n const fallbackText =\n `Hi ${booking['name'] ?? ''},\\n\\nPayment received — ` +\n `\"${booking['serviceName'] ?? 'your booking'}\" is ` +\n `confirmed for ${when}.\\n\\nReference: ${bookingId}`\n // Site-owner-designed template when published (AGL-770); null keeps\n // the built-in copy.\n const designed = await renderHostEmailWithTokens(\n firebaseAdmin.app().firestore(),\n String(hostId),\n 'booking-confirmed',\n {\n name: String(booking['name'] ?? ''),\n 'service.name': String(booking['serviceName'] ?? ''),\n when,\n timezone: String(booking['timezone'] ?? ''),\n 'booking.ref': String(bookingId),\n },\n )\n // White-label sender identity (White-Label Phase 3), matching the\n // free-booking path — the store's brand via the one shared resolver.\n const branding = resolveBrandingProfile(\n (await getOrgForHost(String(hostId)).catch(() => null))?.org as never,\n )\n await sendEmail({\n to: String(booking['email']),\n subject:\n designed?.subject ??\n `Booking confirmed: ${booking['serviceName'] ?? ''}`,\n text: designed?.text || fallbackText,\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: branding.fromName,\n sendingIdentity: await hostSendingIdentity(String(hostId)),\n audience: 'tenant',\n context: 'paid booking confirmation',\n })\n // Cost meter (AGL-1438), matching the free-booking path.\n // Transactional: the guest has already paid.\n await meterHostEmail(String(hostId))\n }\n }\n }\n}\n"],"names":["after","resolveBrandingProfile","firebaseAdmin","getOrgForHost","hostSendingIdentity","meterHostEmail","renderHostEmailWithTokens","sendGa4Purchase","captureHostContact","bookingPlatformNetCents","shouldSendBookingPlatformPurchase","sendEmail","pluginTaxProfile","fileBookingOnCrm","bookingsBillingWebhookHandler","type","object","metadata","payment_status","hostId","bookingId","firestore","app","bookingRef","collection","doc","String","paidAmountCents","Math","max","round","Number","amount_total","taxCents","serviceCents","paymentIntentId","payment_intent","id","chargedFeeCents","feeCents","booking","confirmedNow","runTransaction","transaction","snapshot","get","exists","data","set","status","taxMode","taxModeOf","checkoutSessionId","expiresAtMs","FieldValue","delete","confirmedAt","serverTimestamp","merge","catch","undefined","netCents","transactionId","value","currency","items","item_id","item_name","item_category","price","quantity","clientId","ga_client_id","stripeCustomerId","customer","customer_details","email","name","source","initialLifecycleStage","purchaseCents","interaction","refId","summary","slice","toFixed","when","Date","toLocaleString","dateStyle","timeStyle","fallbackText","designed","timezone","branding","org","to","subject","text","html","fromName","sendingIdentity","audience","context"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/server/billing-webhook.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// `after()`, never a bare `void promise` (AGL-2327). This handler runs inside\n// the console's `/api/billing/webhook` invocation, which AGL-1133 measured on\n// production is frozen the moment the response is sent — so fire-and-forget\n// work scheduled any other way simply does not run.\nimport { after } from 'next/server'\n\nimport type { BillingWebhookHandler } from '@aglyn/aglyn/server'\nimport { resolveBrandingProfile } from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n getOrgForHost,\n hostSendingIdentity,\n meterHostEmail,\n notifyHostManagers,\n renderHostEmailWithTokens,\n sendGa4Purchase,\n} from '@aglyn/tenant-data-admin'\nimport { paymentRiskEventFrom } from '@aglyn/aglyn/app-utils/payment-risk'\nimport {\n recordPaymentRiskOnRecord,\n settlePaymentRiskDisputeOnRecord,\n} from '@aglyn/tenant-data-admin/server/payment-risk-record'\nimport { captureHostContact } from '@aglyn/tenant-runtime'\nimport {\n bookingPlatformNetCents,\n shouldSendBookingPlatformPurchase,\n} from '../model/booking-purchase-analytics'\nimport { sendEmail } from '@aglyn/shared-util-email'\nimport { pluginTaxProfile } from '@aglyn/aglyn/plugin-manager/plugin-tax-profile'\nimport { fileBookingOnCrm } from './booking-crm'\n\n/**\n * Paid-booking section of the platform Stripe webhook (AGL-170/418):\n * payment confirms the pendingPayment hold — relocated verbatim from the\n * console route; registered via registerBookingsConsoleApi.\n */\n/** gRPC `Status.FAILED_PRECONDITION` — Firestore's \"this query needs an index\". */\nconst GRPC_FAILED_PRECONDITION = 9\n\n/**\n * The booking a charge paid for, found by the `paymentIntentId` the paid\n * handler below records (AGL-2315), or null. A missing collection-group\n * index is logged and answered null rather than thrown: no redelivery can\n * fix it, and the dispute then reaches staff through the route's\n * unattributed-dispute alert instead of vanishing.\n */\nasync function findBookingForPayment(\n paymentIntentId: string,\n): Promise<FirebaseFirestore.QueryDocumentSnapshot | null> {\n if (!paymentIntentId) return null\n const matches = await firebaseAdmin\n .app()\n .firestore()\n .collectionGroup('bookings')\n .where('paymentIntentId', '==', paymentIntentId)\n .limit(2)\n .get()\n .catch((error: { code?: number }) => {\n if (error?.code !== GRPC_FAILED_PRECONDITION) throw error\n console.error(\n 'Booking payment lookup needs the collection-group index on ' +\n 'bookings.paymentIntentId (AGL-3360)',\n error,\n )\n return null\n })\n // One payment intent pays one booking; two is corrupt data, and stamping\n // an arbitrary one would tell the wrong merchant.\n return matches && matches.docs.length === 1 ? matches.docs[0] : null\n}\n\nexport const bookingsBillingWebhookHandler: BillingWebhookHandler = async ({\n type,\n object,\n}) => {\n // Fraud signals on a paid booking (AGL-3360): an issuer's early fraud\n // warning, a Radar review, or a dispute. Booking deposits never heard\n // about any of them, so the merchant learned of a chargeback from Stripe\n // alone. The signal now lands on the BOOKING, through the shared\n // `paymentRisk` shape every plugin stamps, and the site's managers are\n // told once. Nothing is refunded or canceled: the merchant decides.\n const risk = paymentRiskEventFrom(type, object)\n if (risk) {\n const booking = await findBookingForPayment(risk.paymentIntentId)\n if (!booking) return\n const hostId = String(booking.ref.parent.parent?.id ?? '')\n await recordPaymentRiskOnRecord(\n {\n ref: booking.ref,\n signal: risk.signal,\n hostId,\n subjectLabel: `Booking ${String(booking.get('serviceName') ?? booking.id)}`,\n link: `/${hostId}/bookings`,\n notificationType: 'content.booking',\n },\n { firestore: firebaseAdmin.app().firestore(), notify: notifyHostManagers },\n )\n return { claimed: true, hostId }\n }\n // A dispute's outcome, on the signal `created` stamped. Claimed only when\n // it was NOT lost: a lost booking dispute takes money back that nothing\n // here reverses or records, which is exactly what the route's\n // unattributed-dispute alert is for, so staff still hear about it.\n if (type === 'charge.dispute.closed') {\n const paymentIntentId =\n typeof object?.payment_intent === 'string'\n ? object.payment_intent\n : String(object?.payment_intent?.id ?? '')\n const booking = await findBookingForPayment(paymentIntentId)\n if (!booking) return\n const outcome = String(object?.status ?? '')\n await settlePaymentRiskDisputeOnRecord(\n booking.ref,\n String(object?.id ?? ''),\n outcome,\n { firestore: firebaseAdmin.app().firestore() },\n )\n return outcome === 'lost'\n ? undefined\n : { claimed: true, hostId: String(booking.ref.parent.parent?.id ?? '') }\n }\n // Paid bookings (AGL-170): payment confirms the pendingPayment hold.\n if (\n type === 'checkout.session.completed' &&\n object?.metadata?.type === 'booking-payment' &&\n object?.payment_status === 'paid'\n ) {\n const { hostId, bookingId } = object.metadata ?? {}\n if (hostId && bookingId) {\n const firestore = firebaseAdmin.app().firestore()\n const bookingRef = firestore\n .collection('hosts')\n .doc(String(hostId))\n .collection('bookings')\n .doc(String(bookingId))\n // WHAT THE CLIENT HANDED OVER, ALL OF IT. Tax included: this is the\n // ceiling `refund.ts` reverses against, and the client paid the tax\n // too — a net figure here would strand part of a refund.\n const paidAmountCents = Math.max(\n 0,\n Math.round(Number(object?.amount_total ?? 0)),\n )\n // THE MERCHANT'S SERVICE TAX, SEPARATED OUT (AGL-2028).\n //\n // `server.ts` charges the merchant's own service rate as an ordinary\n // `line_items[1]` Stripe is never told is tax (the AGL-1711\n // construction, which is what keeps the figure the MERCHANT's rather\n // than something computed against Aglyn's registrations). So\n // `amount_total` is service-plus-tax while Stripe's own tax fields\n // read zero, and the session's metadata is the only witness — exactly\n // as it is for a buy-now sale.\n const taxCents = Math.max(\n 0,\n Math.round(Number(object?.metadata?.taxCents ?? 0)),\n )\n // The merchant's REVENUE, which is the charge less the tax. Used for\n // the contact's lifetime value below: tax is collected on behalf of\n // an authority and is not money the business earned, so counting it\n // would over-state every service client's worth.\n const serviceCents = Math.max(0, paidAmountCents - taxCents)\n // WHAT A REFUND WILL NEED (AGL-2315). A paid booking is now a\n // destination charge to the merchant's connected account, and\n // reversing one requires the PaymentIntent — which lives only on this\n // event. The booking document recorded `paidAmountCents` and nothing\n // that identifies the charge, so a refund had no handle to pull and\n // the information was gone the moment the webhook returned. Unlike\n // most gaps this one cannot be backfilled from our own data later:\n // every booking taken before this line landed can only be refunded by\n // hand in the Stripe dashboard.\n //\n // `feeCents` is Aglyn's cut as CHARGED, read back off the session's\n // own metadata rather than re-derived from the org at refund time. The\n // rate follows the plan (AGL-2289's rule) and the plan moves, so\n // re-resolving it during a refund would reverse a share that was never\n // taken. `'0'` is a real recorded answer on the 0%-rate tiers.\n const paymentIntentId =\n typeof object?.payment_intent === 'string'\n ? object.payment_intent\n : String(object?.payment_intent?.id ?? '')\n const chargedFeeCents = Math.max(\n 0,\n Math.round(Number(object?.metadata?.feeCents ?? 0)),\n )\n // Redelivery guard (AGL-1755), the AGL-1748 shape. This was an\n // unconditional merge-set with no status check at all — idempotent\n // only by accident, because every value it wrote was fixed. A\n // redelivery re-sent the guest's confirmation and re-metered it, and\n // the moment this handler carries money to a contact the accident stops\n // holding: `purchaseCents` is a `FieldValue.increment`, so every retry\n // would inflate the customer's lifetime value.\n //\n // Keyed on the STATUS, not on the document existing: `server.ts` writes\n // the booking as `pendingPayment` BEFORE it opens the Stripe session,\n // so existence is guaranteed by the time any event arrives and proves\n // nothing — the AGL-1748 reasoning, not AGL-1732's `checkoutSessionId`\n // key, which existed because a sibling event wrote the same path.\n //\n // The key is \"not yet confirmed\" rather than \"is pendingPayment\" on\n // purpose. The lapsed-hold sweeper in `server.ts` cancels a\n // `pendingPayment` booking after 24h, and refusing a payment that\n // landed anyway would take the money and record nothing; `confirmed` is\n // reachable for a PAID booking only by this handler having already run,\n // so it is an exact \"already processed\" test. The existence check is\n // new and deliberate: the old merge-set would CREATE a stub booking\n // from a metadata `bookingId` that pointed at nothing.\n let booking: Record<string, any> = {}\n const confirmedNow = await firestore.runTransaction(\n async (transaction) => {\n const snapshot = await transaction.get(bookingRef)\n if (!snapshot.exists) return false\n if (snapshot.get('status') === 'confirmed') return false\n // \"Already processed\" is WIDER than `confirmed` once money can\n // flow back out (AGL-2315). A refunded booking is moved off\n // `confirmed`, so a redelivery arriving afterwards would otherwise\n // read as unprocessed and re-confirm an appointment the merchant\n // has already cancelled and refunded — re-sending the guest a\n // confirmation for it and re-metering the email. Any evidence this\n // handler has run before is the real test, and a refund counter or\n // a recorded PaymentIntent is exactly that evidence.\n if (\n Number(snapshot.get('refundedCents') ?? 0) > 0 ||\n snapshot.get('paymentIntentId')\n ) {\n return false\n }\n booking = (snapshot.data() as any) ?? {}\n transaction.set(\n bookingRef,\n {\n status: 'confirmed',\n paidAmountCents,\n // WHICH TAX REGIME THIS BOOKING CARRIED (AGL-2028), on the\n // document the merchant reads — the stamp every other\n // storefront money door already carries (AGL-2451).\n //\n // DERIVED from the one shared derivation, never a constant.\n // A hand-written \"manual when the metadata says so\" would be\n // a second rule that can drift from the `storefrontTaxCollected`\n // row filed for the same Stripe id, which is the exact\n // disagreement AGL-2451 exists to prevent.\n //\n // TWO-ARGUMENT, because the manual line makes Stripe report\n // `total_details.amount_tax: 0` on a booking that really did\n // charge the client tax; the one-argument form would stamp\n // `none` on precisely those. `absent` remains a fourth state\n // meaning \"recorded before this shipped\".\n taxMode: pluginTaxProfile().taxModeOf(object, taxCents),\n // The figure itself. Absent rather than a defaulted `0` — a\n // zero written through `merge` is the AGL-1758 shape, and\n // this handler can re-enter (see the widened guard above).\n ...(taxCents > 0 ? { taxCents } : {}),\n // The refund handles (AGL-2315). Written inside the same\n // transaction as the status, so a booking can never read\n // `confirmed` while the means to refund it are missing.\n ...(paymentIntentId ? { paymentIntentId } : {}),\n feeCents: chargedFeeCents,\n // WHICH CHECKOUT PAID FOR THIS (AGL-2481). The guest's browser\n // comes back from Stripe holding a session id and nothing\n // else — a booking id was never in the return URL — so this is\n // what lets `booking-analytics.ts` answer \"what did I just\n // buy?\" for the merchant-side `purchase`, and it is written\n // here because the session id first exists on this event.\n //\n // Inside the transaction with the status on purpose: the\n // lookup refuses anything that is not `confirmed`, so a\n // booking can never be findable by session id while reading as\n // unpaid.\n ...(object?.id ? { checkoutSessionId: String(object.id) } : {}),\n // NOTE the absent `refundedCents: 0`. Seeding it here would be\n // the AGL-1758 shape: a defaulted field written through\n // `merge` destroying the real one. This handler can re-enter\n // after a refund has already accumulated a counter (see the\n // widened guard above), and a zero written back over it would\n // re-open the full amount for refunding a second time. Absent\n // IS zero — every reader coerces with `?? 0`.\n expiresAtMs: firebaseAdmin.firestore.FieldValue.delete(),\n confirmedAt:\n firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n return true\n },\n )\n if (!confirmedNow) return\n // THE RECORD'S MEETING (AGL-2660). Payment is what confirms a paid\n // booking, so this is where it reaches the CRM: the meeting on the\n // record's timeline and, when the service asks, the follow-up task.\n // Inside the idempotency gate for the same reason the GA4 purchase\n // below is — a redelivery must not file a second meeting — and\n // through `after()` for the same reason too: this invocation is\n // frozen the moment the response is sent (AGL-2327). Never thrown:\n // the writer swallows its own failures, and the catch is for the\n // scheduling itself.\n after(() =>\n fileBookingOnCrm(firestore, {\n hostId: String(hostId),\n bookingId: String(bookingId),\n booking,\n }).catch(() => undefined),\n )\n // AGLYN'S REVENUE ON THIS BOOKING (AGL-2481) — into OUR GA4 property.\n //\n // ## Why it is inside the idempotency gate rather than beside it\n //\n // This sits AFTER `if (!confirmedNow) return` and that placement is the\n // whole guard. Stripe redelivers `checkout.session.completed` for up to\n // three days after any 500, and this endpoint 500s on purpose; a\n // redelivery re-enters this handler, the transaction above sees a\n // `confirmed` status (or a refund counter, or a recorded PaymentIntent)\n // and answers false, and everything below — the contact, the email, and\n // now this — is skipped. A `purchase` fired before that gate, or beside\n // it, would inflate our own reported revenue by the redelivery rate.\n //\n // `transaction_id` is the checkout session id, which is the same key\n // the guard turns on, so GA4's own de-duplication is a second\n // independent line of defence: even if the Firestore guard were ever\n // bypassed, GA would collapse the repeats.\n //\n // ## Platform NET, and the fee is the measured one\n //\n // `value` is our fee and not the $95 the guest paid — the AGL-1639\n // settlement. This is our property, the subscription and marketplace\n // `purchase` events in it already mean \"what Aglyn was paid\", and a\n // gross booking figure sitting beside them would make every combined\n // total, ARPA and revenue audience wrong. The merchant's own gross goes\n // to the MERCHANT's property, client-side, and the two never meet.\n //\n // The figure comes off the session's `metadata.feeCents` — the fee\n // Stripe actually charged as `application_fee_amount` — never from the\n // plan's rate re-applied here. See `bookingPlatformNetCents`.\n //\n // ## Scheduling and failure posture\n //\n // `after()`, never a bare `void` (AGL-2327/AGL-2346): this runs inside\n // the console's `/api/billing/webhook` invocation, which is frozen the\n // moment the response is sent, so fire-and-forget work scheduled any\n // other way simply does not run — the exact way marketplace revenue\n // reported to nothing. And `.catch()`, because a throw here would\n // un-claim the Stripe event and turn a missed analytics hit into a\n // repeated billing side effect.\n if (shouldSendBookingPlatformPurchase(object)) {\n const netCents = bookingPlatformNetCents(object)\n after(() =>\n sendGa4Purchase({\n transactionId: String(object?.id ?? ''),\n value: netCents / 100,\n currency: String(object?.currency ?? 'usd'),\n items: [\n {\n // Ids only, no merchant free text. A service name is\n // merchant-authored and one edit away from carrying a\n // person's name into a dimension in OUR property; the id\n // already identifies it. The merchant's own property gets\n // the name, because there it is their own content.\n item_id: String(booking['serviceId'] ?? bookingId),\n item_name: String(booking['serviceId'] ?? bookingId),\n item_category: 'booking',\n // GA expects the items to sum to `value`; one item, one\n // price, and that price is our fee — not the service's.\n price: netCents / 100,\n quantity: 1,\n },\n ],\n // Booking sessions carry no `ga_client_id` today — nothing\n // captures one on a tenant site — so this is read for the day\n // one is stamped and the seed below is what actually resolves.\n clientId: object?.metadata?.ga_client_id,\n // ONE PAYING GUEST, ONE SYNTHETIC USER. Seeded from Stripe's\n // customer where there is one, and otherwise from the guest's\n // email, which `server.ts` passes as `customer_email` and Stripe\n // echoes on `customer_details`. The seed is hashed inside\n // `synthesizeClientId` and never transmitted.\n //\n // The booking id is the LAST resort deliberately: it is unique\n // per appointment, so seeding from it would turn a regular\n // client into a crowd of one-purchase strangers and make ARPA\n // nonsense — precisely what that helper's determinism exists to\n // prevent. Without any seed at all `sendGa4Purchase` returns\n // `no-client-id` and the event is silently never sent, which is\n // the void this whole change exists to close.\n stripeCustomerId:\n String(object?.customer ?? '') ||\n String(object?.customer_details?.email ?? '') ||\n String(bookingId),\n }).catch(() => undefined),\n )\n }\n // Contacts ingestion (AGL-1755). `server.ts` captures the contact at\n // REQUEST time, which is the right place — the booking is written\n // `pendingPayment` and at that moment no money has moved, so passing an\n // amount there would be a lie. But payment completes HERE, and this\n // handler never returned to the contact, so a paid booking's money\n // reached the booking document and nothing else. The same structural\n // shape as AGL-1748's draft branch: the completion handler does\n // everything except the contact.\n //\n // `source` stays `'booking'` — the same source the request-time capture\n // used — so the `sources` map still says where this person came from\n // and no schema change is needed to keep service revenue\n // distinguishable from product revenue. The second interaction is\n // deliberate rather than a duplicate: \"Booked X\" and \"Paid for X\" are\n // two real events, and `mergeContactInteraction` does not dedupe on\n // `refId`.\n //\n // The amount is `amount_total`, the money that moved, per AGL-1698 and\n // AGL-1711 — the booking document stores no price to re-derive it from.\n if (booking['email']) {\n void captureHostContact({\n hostId: String(hostId),\n email: booking['email'],\n ...(booking['name'] ? { name: String(booking['name']) } : {}),\n source: 'booking',\n // Paid, so a customer (AGL-2612) — the request-time capture\n // said `lead`, and this is the door that has seen the money.\n initialLifecycleStage: 'customer',\n // The SERVICE, not the charge (AGL-2028). `amount_total` now\n // includes the merchant's service tax where they set one, and\n // tax is collected for an authority rather than earned — booking\n // it as lifetime value would over-state what every service\n // client is worth. Identical to `paidAmountCents` on the default\n // store, which sets no rate.\n ...(serviceCents > 0 ? { purchaseCents: serviceCents } : {}),\n interaction: {\n refId: String(bookingId),\n summary: `Paid for \"${String(\n booking['serviceName'] ?? 'a service',\n ).slice(0, 60)}\" ($${(serviceCents / 100).toFixed(2)})`,\n },\n })\n }\n // Confirmation email now that payment cleared (env-gated inside\n // sendEmail, which no-ops when Resend isn't configured).\n if (booking['email']) {\n const when = new Date(\n Number(booking['startsAtMs']),\n ).toLocaleString('en-US', {\n dateStyle: 'full',\n timeStyle: 'short',\n })\n const fallbackText =\n `Hi ${booking['name'] ?? ''},\\n\\nPayment received — ` +\n `\"${booking['serviceName'] ?? 'your booking'}\" is ` +\n `confirmed for ${when}.\\n\\nReference: ${bookingId}`\n // Site-owner-designed template when published (AGL-770); null keeps\n // the built-in copy.\n const designed = await renderHostEmailWithTokens(\n firebaseAdmin.app().firestore(),\n String(hostId),\n 'booking-confirmed',\n {\n name: String(booking['name'] ?? ''),\n 'service.name': String(booking['serviceName'] ?? ''),\n when,\n timezone: String(booking['timezone'] ?? ''),\n 'booking.ref': String(bookingId),\n },\n )\n // White-label sender identity (White-Label Phase 3), matching the\n // free-booking path — the store's brand via the one shared resolver.\n const branding = resolveBrandingProfile(\n (await getOrgForHost(String(hostId)).catch(() => null))?.org as never,\n )\n await sendEmail({\n to: String(booking['email']),\n subject:\n designed?.subject ??\n `Booking confirmed: ${booking['serviceName'] ?? ''}`,\n text: designed?.text || fallbackText,\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: branding.fromName,\n sendingIdentity: await hostSendingIdentity(String(hostId)),\n audience: 'tenant',\n context: 'paid booking confirmation',\n // Owed to the recipient by their own booking: the phishing\n // screen's soft rules never hold it (AGL-3356).\n owedFor: 'booking',\n })\n // Cost meter (AGL-1438), matching the free-booking path.\n // Transactional: the guest has already paid.\n await meterHostEmail(String(hostId))\n }\n }\n }\n}\n"],"names":["after","resolveBrandingProfile","firebaseAdmin","getOrgForHost","hostSendingIdentity","meterHostEmail","notifyHostManagers","renderHostEmailWithTokens","sendGa4Purchase","paymentRiskEventFrom","recordPaymentRiskOnRecord","settlePaymentRiskDisputeOnRecord","captureHostContact","bookingPlatformNetCents","shouldSendBookingPlatformPurchase","sendEmail","pluginTaxProfile","fileBookingOnCrm","GRPC_FAILED_PRECONDITION","findBookingForPayment","paymentIntentId","matches","app","firestore","collectionGroup","where","limit","get","catch","error","code","console","docs","length","bookingsBillingWebhookHandler","type","object","risk","booking","hostId","String","ref","parent","id","signal","subjectLabel","link","notificationType","notify","claimed","payment_intent","outcome","status","undefined","metadata","payment_status","bookingId","bookingRef","collection","doc","paidAmountCents","Math","max","round","Number","amount_total","taxCents","serviceCents","chargedFeeCents","feeCents","confirmedNow","runTransaction","transaction","snapshot","exists","data","set","taxMode","taxModeOf","checkoutSessionId","expiresAtMs","FieldValue","delete","confirmedAt","serverTimestamp","merge","netCents","transactionId","value","currency","items","item_id","item_name","item_category","price","quantity","clientId","ga_client_id","stripeCustomerId","customer","customer_details","email","name","source","initialLifecycleStage","purchaseCents","interaction","refId","summary","slice","toFixed","when","Date","toLocaleString","dateStyle","timeStyle","fallbackText","designed","timezone","branding","org","to","subject","text","html","fromName","sendingIdentity","audience","context","owedFor"],"mappings":"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"}
package/src/lib/server.js CHANGED
@@ -14,7 +14,12 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { checkEntitlement, pluginJobHostGate, registerPluginConfigSchema, registerPluginJob, resolveBrandingProfile, resolveTransactionFeeCents, sanitizeAuthorHtml } from "@aglyn/aglyn/server";
17
+ */ import { checkEntitlement, registerPluginConfigSchema, registerPluginJob, resolveBrandingProfile, resolveTransactionFeeCents, sanitizeAuthorHtml } from "@aglyn/aglyn/server";
18
+ // The leaf, not the barrel: the console's `x-cron-secret` door asks the
19
+ // site's lockdown directly (AGL-3356) — core's registry has no resolver in
20
+ // the console process — and a spec that substitutes the barrel must still
21
+ // reach the real verdict.
22
+ import { siteLockdownJobGate } from "@aglyn/tenant-data-admin/server/tenant-write-lockdown";
18
23
  import { BOOKING_MAX_DAYS_AHEAD, computeOpenSlots, isBookingReminderDue, isSlotOpen, REMINDER_WINDOW_END_HOURS, REMINDER_WINDOW_START_HOURS } from "./model/index.js";
19
24
  import { registerBillingWebhookHandler, registerPluginApiRoute } from "@aglyn/aglyn/server";
20
25
  import { pluginTaxProfile } from "@aglyn/aglyn/plugin-manager/plugin-tax-profile";
@@ -81,6 +86,7 @@ registerPluginJob({
81
86
  });
82
87
  import { firebaseAdmin, getOrgForHost, meterHostEmail, notifyHostManagers, attributeCampaignConversion, resolveCampaignTouch, getPluginConfig, resolveOrgIdForHost, renderHostEmailWithTokens, hostSendingIdentity } from "@aglyn/tenant-data-admin";
83
88
  import { connectLinkageIsReady } from "@aglyn/tenant-data-admin/server/stripe-account-mode";
89
+ import { checkoutSessionCardAuthenticationParams } from "@aglyn/tenant-data-admin/server/stripe-card-authentication";
84
90
  import { emitHostEvent } from "@aglyn/tenant-runtime";
85
91
  import recordCapturedContact from "@aglyn/aglyn/plugin-manager/record-captured-contact";
86
92
  import { isEmailConfigured, loadHostEmail, renderLoadedHostEmail, sendEmail } from "@aglyn/shared-util-email";
@@ -561,7 +567,8 @@ export const slotsHandler = async (req, res)=>{
561
567
  // taxed one files `manual` with the figure, both with no further
562
568
  // wiring.
563
569
  const params = new URLSearchParams(_extends({
564
- mode: 'payment',
570
+ mode: 'payment'
571
+ }, checkoutSessionCardAuthenticationParams('payment'), {
565
572
  'line_items[0][quantity]': '1',
566
573
  'line_items[0][price_data][currency]': 'usd',
567
574
  'line_items[0][price_data][unit_amount]': String(Math.round(priceUsd * 100)),
@@ -700,7 +707,10 @@ export const slotsHandler = async (req, res)=>{
700
707
  fromName: branding.fromName,
701
708
  sendingIdentity: await hostSendingIdentity(hostId),
702
709
  audience: 'tenant',
703
- context: 'booking confirmation'
710
+ context: 'booking confirmation',
711
+ // Owed to the recipient by their own booking: the phishing
712
+ // screen's soft rules never hold it (AGL-3356).
713
+ owedFor: 'booking'
704
714
  }));
705
715
  // Cost meter (AGL-1438). Transactional: counted, never capped — a
706
716
  // confirmation the customer never receives reads to them as a booking
@@ -851,7 +861,10 @@ export const slotsHandler = async (req, res)=>{
851
861
  fromName: (_brandingByHost_get = brandingByHost.get(hostId)) == null ? void 0 : _brandingByHost_get.fromName,
852
862
  sendingIdentity: await hostSendingIdentity(hostId, identityByHost),
853
863
  audience: 'tenant',
854
- context: 'booking reminder'
864
+ context: 'booking reminder',
865
+ // Owed to the recipient by their own booking: the phishing
866
+ // screen's soft rules never hold it (AGL-3356).
867
+ owedFor: 'booking'
855
868
  }));
856
869
  if (result.sent) {
857
870
  // Cost meter (AGL-1438). Transactional: a reminder a quota refused is
@@ -930,7 +943,7 @@ export const slotsHandler = async (req, res)=>{
930
943
  }
931
944
  try {
932
945
  // The manual door asks the same question the beat does (AGL-2495).
933
- return res.status(200).json(await scanBookingReminders(pluginJobHostGate()));
946
+ return res.status(200).json(await scanBookingReminders(siteLockdownJobGate()));
934
947
  } catch (error) {
935
948
  console.error(error);
936
949
  return res.status(500).json({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/bookings/src/lib/server.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 { checkEntitlement,\n pluginJobHostGate,\n type PluginJobHostGate,\n registerPluginConfigSchema,\n registerPluginJob,\n resolveBrandingProfile,\n resolveTransactionFeeCents,\n sanitizeAuthorHtml,\n} from '@aglyn/aglyn/server'\nimport { type BookedInterval, BOOKING_MAX_DAYS_AHEAD, computeOpenSlots, type HostBookingService, isBookingReminderDue, isSlotOpen, REMINDER_WINDOW_END_HOURS, REMINDER_WINDOW_START_HOURS } from './model'\nimport {\n registerBillingWebhookHandler,\n registerPluginApiRoute,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport {\n pluginTaxProfile,\n type PluginResolvedFlatTax,\n} from '@aglyn/aglyn/plugin-manager/plugin-tax-profile'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { BOOKINGS_CONFIG_SCHEMA } from './plugin-config'\nimport { bookingsBillingWebhookHandler } from './server/billing-webhook'\nimport { bookingAnalyticsHandler } from './server/booking-analytics'\nimport { registerBookingFigureReader } from './server/booking-figures'\nimport { bookingRefundHandler } from './server/refund'\n// The booking's way back to the CRM record (AGL-2660): the reference a\n// booking link carried, and the meeting a free booking files on landing.\n// The leaf, not the barrel: this library's specs substitute the barrel\n// wholesale, and a parser that vanished under them would fail every\n// booking they take.\nimport {\n formatCrmBookingRef,\n parseCrmBookingRef,\n} from '@aglyn/aglyn/app-utils/crm-booking'\nimport { fileBookingOnCrm } from './server/booking-crm'\n\n// Settings schema (AGL-428): registered here too so server-only loads\n// (API dispatchers) get defaults-merged getPluginConfig reads.\nregisterPluginConfigSchema(BOOKINGS_CONFIG_SCHEMA)\n\n// Scheduled cleanup (AGL-435): lapse day-old expired payment holds to\n// 'canceled' (the read paths already treat them as released — this keeps\n// the collection tidy and the read-time filter cheap). Bounded + failure\n// tolerant: a missing collection-group index just logs and retries on\n// the next beat.\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'expire-stale-holds',\n intervalMinutes: 6 * 60,\n description: 'Cancel pendingPayment bookings whose hold lapsed >24h ago.',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n const cutoff = Date.now() - 24 * 60 * 60_000\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collectionGroup('bookings')\n .where('status', '==', 'pendingPayment')\n .where('expiresAtMs', '<', cutoff)\n .limit(100)\n .get()\n let lapsed = 0\n for (const doc of snapshot.docs) {\n // LOCKDOWN (AGL-2495). `bookings` live at `hosts/{hostId}/bookings/{id}`,\n // so the grandparent is the host. This flips a booking to `canceled` for\n // a site that may be suspended mid-dispute, and a status a customer can\n // see is not a change to make while the operator is deciding whether the\n // site should exist.\n //\n // SKIPPED, NOT DROPPED: the query selects on `status == 'pendingPayment'`\n // and an age cutoff that only gets truer with time, so an untouched hold\n // is picked up by the next six-hour beat after the lift. The read paths\n // already treat a lapsed hold as released, so waiting costs tidiness and\n // nothing else — which is why this one can afford to wait at all.\n const hostId = doc.ref.parent.parent?.id ?? ''\n if (await gate.isLocked(hostId)) continue\n await doc.ref.set({ status: 'canceled' }, { merge: true })\n lapsed += 1\n }\n if (lapsed) {\n console.info(`bookings: lapsed ${lapsed} stale payment holds`)\n }\n },\n})\nimport {\n firebaseAdmin,\n getOrgForHost,\n meterHostEmail,\n notifyHostManagers,\n attributeCampaignConversion,\n resolveCampaignTouch,\n getPluginConfig,\n resolveOrgIdForHost,\n renderHostEmailWithTokens,\n hostSendingIdentity,\n} from '@aglyn/tenant-data-admin'\nimport { connectLinkageIsReady } from '@aglyn/tenant-data-admin/server/stripe-account-mode'\nimport { emitHostEvent } from '@aglyn/tenant-runtime'\nimport recordCapturedContact from '@aglyn/aglyn/plugin-manager/record-captured-contact'\nimport {\n isEmailConfigured,\n loadHostEmail,\n renderLoadedHostEmail,\n sendEmail,\n type LoadedHostEmail,\n} from '@aglyn/shared-util-email'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n NO_CLIENT_ADDRESS_BUCKET,\n readClientIp,\n} from '@aglyn/aglyn/app-utils/request-ip'\nimport { composeHostComponentNodes } from '@aglyn/aglyn/app-utils/load-referenced-components'\n\nconst BOOKING_EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n// Best-effort per-instance rate limit (mirrors forms/submit).\nconst recentBookingIpHits = new Map<string, number[]>()\nconst BOOKING_RATE_WINDOW_MS = 60_000\nconst BOOKING_RATE_MAX = 5\n\nfunction bookingRateLimited(ip: string): boolean {\n const now = Date.now()\n const hits = (recentBookingIpHits.get(ip) ?? []).filter(\n (t) => now - t < BOOKING_RATE_WINDOW_MS,\n )\n hits.push(now)\n recentBookingIpHits.set(ip, hits)\n return hits.length > BOOKING_RATE_MAX\n}\n\n/**\n * Public open-slot listing (AGL-159): visitors browse a service's next\n * open times. Server-side because bookings (names/emails) must never be\n * client-readable — only the derived busy intervals are used here.\n */\n// Exported for `bookings-slot-window.spec.ts`: the query's bounds are a\n// read-cost and correctness property of THIS handler, and asserting them\n// through the route registry would test the registry instead. Its siblings\n// (`bookHandler`, the refund and analytics handlers) are exported already.\nexport const slotsHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'GET') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.query['hostId'] ?? '')\n const serviceId = String(req.query['serviceId'] ?? '')\n if (!hostId) {\n return res.status(400).json({ error: 'Missing hostId' })\n }\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n\n // No serviceId → public service directory for the booking widget\n // (AGL-160): names/durations/prices only, never availability internals.\n if (!serviceId) {\n const services = await hostRef.collection('services').limit(50).get()\n return res.status(200).json({\n services: services.docs\n .filter((doc) => !doc.get('deletedAt'))\n .map((doc) => ({\n $id: doc.id,\n name: doc.get('name') ?? '',\n durationMinutes: Number(doc.get('durationMinutes') ?? 30),\n priceUsd: Number(doc.get('priceUsd') ?? 0),\n description: doc.get('description') ?? '',\n })),\n })\n }\n const serviceSnapshot = await hostRef\n .collection('services')\n .doc(serviceId)\n .get()\n const service = serviceSnapshot.data() as HostBookingService | undefined\n if (!service || (serviceSnapshot.get('deletedAt') as unknown)) {\n return res.status(404).json({ error: 'Unknown service' })\n }\n const fromMs = Date.now()\n // Booking horizon (AGL-428): configurable via the plugin settings\n // framework; defaults to BOOKING_MAX_DAYS_AHEAD through the schema.\n //\n // Resolved for THIS SITE (AGL-428, AGL-1014), not just the workspace.\n // The horizon is the setting per-site overrides were built for — one\n // chain, one answer, and the flagship branch taking bookings further out\n // — so reading only the org's would leave the console showing a number\n // this endpoint never honors.\n const config = await getPluginConfig(\n await resolveOrgIdForHost(hostId),\n 'bookings',\n { hostId },\n )\n const maxDaysAhead = Number(config.maxDaysAhead ?? BOOKING_MAX_DAYS_AHEAD)\n const toMs = fromMs + maxDaysAhead * 24 * 60 * 60_000\n // Bounded at BOTH ends, to the window the slots are actually computed\n // over. `computeOpenSlots` below is handed `fromMs`/`toMs` and ignores\n // anything outside them, so a booking past the horizon was read, billed\n // and discarded — and worse, it competed for the 500 with the bookings\n // that do matter, so a service booked far ahead could push the near-term\n // stays out of its own availability check and offer a taken slot.\n //\n // A second range on `startsAtMs` needs no new index: the field already\n // carries one, and the range it narrows is the one already here.\n const bookedSnapshot = await hostRef\n .collection('bookings')\n .where('serviceId', '==', serviceId)\n .where('startsAtMs', '>=', fromMs - 24 * 60 * 60_000)\n .where('startsAtMs', '<=', toMs)\n .limit(500)\n .get()\n const booked: BookedInterval[] = bookedSnapshot.docs\n .filter(\n (doc) =>\n doc.get('status') !== 'canceled' &&\n // Expired payment holds release the slot (AGL-170).\n !(\n doc.get('status') === 'pendingPayment' &&\n Number(doc.get('expiresAtMs') ?? 0) < Date.now()\n ),\n )\n .map((doc) => ({\n startsAtMs: Number(doc.get('startsAtMs') ?? 0),\n endsAtMs: Number(doc.get('endsAtMs') ?? 0),\n }))\n const slots = computeOpenSlots(service, fromMs, toMs, booked, 120)\n return res.status(200).json({\n service: {\n name: service.name,\n durationMinutes: service.durationMinutes,\n priceUsd: service.priceUsd ?? 0,\n timezone: service.timezone ?? 'UTC',\n },\n slots,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Slot lookup failed' })\n }\n}\n\n/**\n * Booking creation (AGL-159): validates the slot against the service's\n * windows AND stored bookings inside a transaction (double-booking safe),\n * stores the booking, records a lead, emits the `booking` host event, and\n * sends an env-gated Resend confirmation. Plan gate: the owning org\n * needs the `bookings` flag (dark-launch workspaces pass).\n */\n/**\n * Exported for spec (AGL-2000). The tax decision this handler takes lives in\n * the ABSENCE of a parameter, and an absence is only a decision if something\n * asserts it — see `server-booking-tax.spec.ts`.\n */\nexport const bookHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const serviceId = String(req.body?.serviceId ?? '')\n const startsAtMs = Number(req.body?.startsAtMs ?? 0)\n const name = String(req.body?.name ?? '')\n .trim()\n .slice(0, 80)\n const email = String(req.body?.email ?? '')\n .trim()\n .toLowerCase()\n // Explicit opt-in checkbox (AGL-2499) — a booking is not by itself\n // consent to be emailed marketing, so this is only set when the visitor\n // checked it.\n const marketingConsent = req.body?.marketingConsent === true\n // The CRM record the booking link named (AGL-2660), kept on the row so\n // the booking can be attributed to it even when the visitor books with\n // a different address. Parsed, never stored raw: this is a public door.\n const crmRef = parseCrmBookingRef(req.body?.crmRef)\n if (!hostId || !serviceId || !Number.isFinite(startsAtMs) || !startsAtMs) {\n return res.status(400).json({ error: 'Invalid booking request' })\n }\n if (!name) return res.status(400).json({ error: 'Enter your name' })\n if (!BOOKING_EMAIL_PATTERN.test(email)) {\n return res.status(400).json({ error: 'Enter a valid email' })\n }\n if (startsAtMs < Date.now()) {\n return res.status(409).json({ error: 'That time has already passed' })\n }\n // Keeps counting under the no-address bucket rather than being skipped: an\n // unauthenticated booking endpoint that stops counting writes reservations\n // for anyone who can reach it.\n const ip =\n readClientIp(req.headers, { remoteAddress: req.socket?.remoteAddress }) ??\n NO_CLIENT_ADDRESS_BUCKET\n if (bookingRateLimited(ip)) {\n return res.status(429).json({ error: 'Too many booking attempts' })\n }\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const [hostSnapshot, serviceSnapshot] = await Promise.all([\n hostRef.get(),\n hostRef.collection('services').doc(serviceId).get(),\n ])\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const service = serviceSnapshot.data() as HostBookingService | undefined\n if (!service || (serviceSnapshot.get('deletedAt') as unknown)) {\n return res.status(404).json({ error: 'Unknown service' })\n }\n\n // Plan gate (dark-launch rule preserved). Plan/quota gates ride the\n // owning org's doc (AGL-238). Hoisted out of its block for AGL-2315:\n // the same org doc now also prices the platform's cut below, and reading\n // it twice would let a gate and a fee disagree within one request.\n const ownerOrg = (await getOrgForHost(hostId))?.org\n if (!checkEntitlement(ownerOrg as never, 'bookings')) {\n return res\n .status(403)\n .json({ error: 'Bookings are not enabled on this site' })\n }\n\n const durationMs =\n Math.max(5, Math.round(service.durationMinutes || 30)) * 60_000\n const endsAtMs = startsAtMs + durationMs\n const bookingsRef = hostRef.collection('bookings')\n\n // Paid services (AGL-170): the slot is HELD pending payment — the\n // booking lands as `pendingPayment` with a 15-minute expiry (expired\n // holds release the slot in the collision filters), and the visitor\n // goes to Stripe Checkout; the webhook confirms + emails on payment.\n const priceUsd = Number(service.priceUsd ?? 0)\n const paid = priceUsd > 0\n if (paid && !process.env.STRIPE_SECRET_KEY) {\n return res.status(501).json({\n error: 'Paid bookings are not configured (STRIPE_SECRET_KEY).',\n })\n }\n\n // WHO GETS THE MONEY (AGL-2315). Until this landed, a paid booking opened\n // a Checkout Session with no Connect wiring of any kind — no\n // `transfer_data`, no `application_fee_amount`, no connected account read\n // anywhere in the plugin — so 100% of every paid booking settled in\n // AGLYN'S OWN platform balance and the merchant was never paid at all.\n // Not under-paid: never paid. Every sibling tenant money path (storefront\n // checkout, cart, draft orders, POS, reservations) was already a\n // destination charge to the merchant's connected account; this was the one\n // door that took a card and kept the proceeds.\n //\n // Resolved BEFORE the hold transaction on purpose, the `reserve.ts`\n // ordering: an unconnected merchant is a deterministic refusal the site\n // owner fixes out of band, and discovering it after the slot is written\n // would strand a real appointment slot as `pendingPayment` for 15 minutes\n // on a booking that could never be paid.\n let chargeAccountId = ''\n let feeCents = 0\n // No tax until a paid booking asks the tenant's tax rule what is owed.\n let serviceTax: PluginResolvedFlatTax = { taxCents: 0, label: '', pct: 0 }\n if (paid) {\n const ownerUid = (ownerOrg as { ownerUid?: unknown } | null)?.ownerUid\n const [ownerProfile, storeSnapshot] = await Promise.all([\n ownerUid\n ? firestore.collection('profiles').doc(String(ownerUid)).get()\n : Promise.resolve(null),\n // The merchant's own service rate (AGL-2028), in the SAME round trip\n // as the connected-account read this path already makes.\n hostRef.collection('settings').doc('store').get(),\n ])\n chargeAccountId = String(ownerProfile?.get('stripeAccountId') ?? '')\n if (\n !connectLinkageIsReady(\n {\n accountId: chargeAccountId,\n chargesEnabled: ownerProfile?.get('stripeChargesEnabled'),\n accountLivemode: ownerProfile?.get('stripeAccountLivemode'),\n },\n { subject: `booking host ${hostId}` },\n )\n ) {\n return res.status(409).json({ error: 'Payments are not set up yet' })\n }\n\n // THE TAKE RATE IS THE STOREFRONT LADDER'S, NOT A NEW ONE (AGL-2315).\n //\n // the decision, 2026-08-19: bookings mirror the storefront ladder —\n // the same rate a storefront sale already takes, tapering to 0% on the\n // upper tiers. It is consistent with what merchants already agreed to\n // and needs no new price communication, so pricing stays locked for\n // Sept 1.\n //\n // Implemented by ROUTING THROUGH the existing resolver rather than\n // adding a bookings rate: `resolveTransactionFeePct` already carries a\n // `'service'` axis (it resolves to the digital rate), and a booking is a\n // service sale — `reserve.ts` prices a stay through the identical call.\n // So there is no new constant, no new plan-table row, and no second\n // place for a rate to drift out of step with `/pricing`. Every guard\n // that already holds for the storefront rate holds here for free: the\n // malformed-override floor (AGL-2114), the lapsed-plan re-resolution,\n // and the staff override.\n //\n // FEE BASIS: post-discount ITEMS ONLY, matching the one-time storefront\n // paths and deliberately NOT the subscription path's\n // `application_fee_percent`, which Stripe applies to the whole invoice\n // and so takes a cut of sales tax and shipping (AGL-2317). Here the two\n // bases happen to coincide, because this session carries exactly one\n // line item and, by AGL-2000's stated decision, no tax line and no\n // shipping option. That coincidence is not the reason for the choice —\n // sending a computed CENTS amount rather than a percent is what makes\n // the basis survive the day this path does compute service tax: a fee\n // derived from `priceUsd` stays off the tax the moment tax is added,\n // whereas a percent would silently start taxing the state's money.\n //\n // PLUS STRIPE'S CARD COST, AT COST (AGL-2152). A 0%-fee tier used to\n // take a destination charge with NO application fee at all, which is not\n // \"no take rate\" — Stripe debits 2.9% + 30¢ from the PLATFORM's balance\n // when that charge settles, so every paid booking on Pro and above cost\n // Aglyn money. `resolveTransactionFeeCents` is the same resolver's cents\n // form: the advertised take (still 0% where the ladder says 0%) plus the\n // card cost passed through, so the net per booking is the take and never\n // less. The `Math.max(1, …)` floor on a chargeable take is folded into\n // it, unchanged.\n //\n // The fee base and the charge base are the same number here: by\n // AGL-2000's stated decision this session carries one line item, no tax\n // line and no shipping option, so the card runs for exactly `priceUsd`.\n // SERVICE TAX: the MERCHANT'S OWN RATE, off unless they set one\n // (AGL-2028, answering the decision AGL-2000 recorded here).\n //\n // AGL-2000's reasoning is unchanged and is why this is a field of its\n // own rather than a reuse of the goods table. A service is not goods:\n // whether one is taxable is jurisdiction-specific and frequently the\n // opposite answer, so the AGL-285 `rates[]` are still never read here\n // — a test in `server-booking-tax.spec.ts` pins that with a fully\n // configured sales-tax store. Stripe Tax is still never asked either:\n // it has no service tax code from this handler and would compute a\n // goods rate against AGLYN's registrations (AGL-1904).\n //\n // What changed is that the merchant now has somewhere to answer. A\n // service business is one of the three named ICPs and in many states\n // their services ARE taxable, so \"Aglyn does not compute it\" could only\n // ever be a holding position. They type the rate, we compute it, charge\n // it, record it and stamp the regime; the settings card states plainly\n // that determining what is owed and to whom is theirs. Aglyn takes no\n // tax position, and the existence of a separate, blank-by-default field\n // is also AGL-2000's second reason answered — it is the merchant\n // saying, explicitly, that they mean this to cover bookings.\n //\n // DEFAULT OFF, and load-bearing: an absent, zero, negative or\n // out-of-range rate resolves to zero, so no existing merchant's charge\n // moves because this shipped.\n const taxSettings = ((storeSnapshot?.data() as any)?.tax ?? {}) as {\n service?: unknown\n }\n const chargeCents = Math.round(priceUsd * 100)\n // Asked of the plugin that owns the merchant's tax rule, which throws\n // rather than answer zero if none is loaded: an untaxed charge recorded\n // as untaxed is the failure nobody sees until the merchant owes it.\n serviceTax = pluginTaxProfile().flatTax(\n taxSettings.service,\n chargeCents,\n 'Service tax',\n )\n // THE TWO FEE BASES ARE NOW DIFFERENT NUMBERS (AGL-2028). The take is\n // computed on the SERVICE, never on the state's money — AGL-2317's rule\n // — while Stripe's card cost is debited from the platform balance on\n // the WHOLE charge and so is passed through on the whole charge\n // (AGL-2152). The comment above already promised this: \"a fee derived\n // from `priceUsd` stays off the tax the moment tax is added\". This is\n // the moment.\n feeCents = resolveTransactionFeeCents(\n ownerOrg as never,\n 'service',\n chargeCents,\n chargeCents + serviceTax.taxCents,\n )\n }\n\n // Transaction: re-read overlapping bookings and validate the slot so\n // two simultaneous requests cannot double-book.\n const bookingId = await firestore.runTransaction(async (transaction) => {\n const overlapping = await transaction.get(\n bookingsRef\n .where('serviceId', '==', serviceId)\n .where('startsAtMs', '>=', startsAtMs - 24 * 60 * 60_000)\n .limit(500),\n )\n const booked: BookedInterval[] = overlapping.docs\n .filter(\n (doc) =>\n doc.get('status') !== 'canceled' &&\n // Expired payment holds release the slot (AGL-170).\n !(\n doc.get('status') === 'pendingPayment' &&\n Number(doc.get('expiresAtMs') ?? 0) < Date.now()\n ),\n )\n .map((doc) => ({\n startsAtMs: Number(doc.get('startsAtMs') ?? 0),\n endsAtMs: Number(doc.get('endsAtMs') ?? 0),\n }))\n if (!isSlotOpen(service, startsAtMs, booked)) {\n throw Object.assign(new Error('Slot unavailable'), { code: 409 })\n }\n const bookingRef = bookingsRef.doc()\n transaction.set(bookingRef, {\n serviceId,\n serviceName: service.name ?? '',\n name,\n email,\n startsAtMs,\n endsAtMs,\n status: paid ? 'pendingPayment' : 'confirmed',\n ...(paid && { expiresAtMs: Date.now() + 15 * 60_000 }),\n ...(crmRef ? { crmRef: formatCrmBookingRef(crmRef) } : {}),\n createdAt: FieldValue.serverTimestamp(),\n })\n return bookingRef.id\n })\n\n /*\n * THE CAMPAIGN TOUCH, RESOLVED ONCE FOR THE WHOLE BOOKING.\n *\n * A booking is an identify moment: the visitor was anonymous while they\n * browsed and picked a slot, and this request is the first thing that\n * names them. Both channels are asked here — the labels their device\n * carried, and the campaign mail they clicked, joined on the address they\n * just typed — and the later of the two is credited, exactly as the\n * revenue join credits the later click.\n *\n * Resolved once and handed to the booking record, the contact and the\n * lead below, so the email-channel lookup is one keyed read for the whole\n * request and the three records cannot name different campaigns.\n */\n const bookedAtMs = Date.now()\n const campaignTouch = await resolveCampaignTouch({\n hostId,\n wire: req.body?.campaignTouch,\n email,\n atMs: bookedAtMs,\n })\n void attributeCampaignConversion({\n hostId,\n kind: 'booking',\n refId: bookingId,\n touch: campaignTouch,\n convertedAtMs: bookedAtMs,\n })\n\n /*\n * Contacts ingestion (AGL-197) — booking requests identify a person —\n * reported to whichever plugin keeps people rather than written by this\n * one (AGL-3080). Bookings knows it just met somebody; what a person\n * record is belongs to the plugin that models it, and a workspace that\n * keeps none is a quiet, correct answer here.\n */\n /*\n * A booking request is a LEAD SURFACE (AGL-3232): interest, not a sale\n * — no money has moved, and the payment webhook is the door that makes\n * a customer. The record system files the person as a lead, or lands\n * the request on the contact the workspace already holds for the\n * address; which record, and the lead's ceiling and dedupe, are its to\n * decide. Awaited rather than voided so the lead is there before the\n * response, as it was when this door wrote it itself; the seam never\n * throws, so a refused capture still cannot fail the booking.\n */\n await recordCapturedContact({\n /*\n * Not resolved here. The record system keys a person on the SITE they\n * were met on, so this costs the capture nothing — and resolving it\n * would put a Firestore read in front of the writer, which is the one\n * thing `recordCapturedContact` asks a door not to do.\n */\n orgId: '',\n hostId,\n identity: { email, name: name || undefined },\n interaction: {\n source: 'booking',\n refId: bookingId,\n summary: `Booked \"${String(service.name ?? 'a service').slice(0, 60)}\"`,\n },\n surface: 'lead',\n ...(marketingConsent ? { marketingConsent: true } : {}),\n // Where the visitor ARRIVED from — a fact about this visit and not\n // about the person, which is what `detail` carries.\n ...(campaignTouch ? { detail: { campaignTouch } } : {}),\n })\n\n if (paid) {\n const origin = req.headers.origin ?? `https://${req.headers.host}`\n // TAX ON A PAID BOOKING: the merchant's own service rate, resolved\n // above and default OFF (AGL-2028, answering AGL-2000).\n //\n // The rate rides as an ORDINARY product line Stripe is never told is\n // tax — the AGL-1711 construction every other manual rate in this\n // repo uses. That is what makes the derived `taxMode` read `manual`:\n // the figure is the merchant's own, from their own rate, and is never\n // computed against Aglyn's registrations the way `automatic_tax` would\n // be (AGL-1904). Absent entirely when no rate is set, so a default\n // store's session carries no tax parameter of any kind — the property\n // `server-booking-tax.spec.ts` asserts over the whole emitted body.\n //\n // The sale is recorded either way: `booking-payment` has been in\n // `storefront-tax-record.ts`'s SESSION_TYPES since AGL-2000, so an\n // untaxed booking still files a row reading `taxMode: 'none'` and a\n // taxed one files `manual` with the figure, both with no further\n // wiring.\n const params = new URLSearchParams({\n mode: 'payment',\n 'line_items[0][quantity]': '1',\n 'line_items[0][price_data][currency]': 'usd',\n 'line_items[0][price_data][unit_amount]': String(\n Math.round(priceUsd * 100),\n ),\n 'line_items[0][price_data][product_data][name]': String(\n service.name ?? 'Booking',\n ).slice(0, 120),\n ...(serviceTax.taxCents > 0\n ? {\n 'line_items[1][quantity]': '1',\n 'line_items[1][price_data][currency]': 'usd',\n 'line_items[1][price_data][unit_amount]': String(\n serviceTax.taxCents,\n ),\n 'line_items[1][price_data][product_data][name]': serviceTax.label,\n }\n : {}),\n // Destination charge to the MERCHANT's connected account (AGL-2315).\n // Unconditional: `transfer_data[destination]` is what makes this the\n // merchant's money, and it is required on every paid booking whatever\n // the take rate. Only the fee below is conditional.\n 'payment_intent_data[transfer_data][destination]': chargeAccountId,\n ...(feeCents > 0\n ? { 'payment_intent_data[application_fee_amount]': String(feeCents) }\n : {}),\n // `{CHECKOUT_SESSION_ID}` is Stripe's own template token, substituted\n // on the redirect (AGL-2481). Without it the guest came back holding\n // the word `paid` and nothing else — no id, no amount — so the\n // merchant-side `purchase` had nothing to look itself up by and could\n // not have stated what was actually charged. The id is safe in the\n // URL: Stripe hands it only to the payer, and\n // `booking-analytics.ts` scopes it to the host and answers with a\n // projection rather than the booking document.\n success_url: `${origin}/?booking=paid&session_id={CHECKOUT_SESSION_ID}`,\n cancel_url: `${origin}/?booking=canceled`,\n customer_email: email,\n 'metadata[type]': 'booking-payment',\n 'metadata[hostId]': hostId,\n 'metadata[bookingId]': bookingId,\n // The fee actually charged, recorded on the session exactly as every\n // sibling path records it — the figure a refund reverses and the\n // figure a reconciliation reads. `'0'` on a 0%-fee tier is a real\n // answer, not a missing one.\n 'metadata[feeCents]': String(feeCents),\n // The only witness to the tax (AGL-2028). By the construction above\n // Stripe does not know the second line is tax and reports\n // `total_details.amount_tax: 0`, so the session's own metadata is\n // what `storefront-tax-record.ts` and the confirmation webhook read\n // to record the figure and derive the regime — exactly as\n // `checkout.ts` stamps it for a buy-now sale. Absent when there is no\n // tax, never a reassuring `'0'`: absent and zero are different facts.\n ...(serviceTax.taxCents > 0\n ? {\n 'metadata[taxCents]': String(serviceTax.taxCents),\n 'metadata[taxPct]': String(serviceTax.pct),\n }\n : {}),\n expires_at: String(Math.floor(Date.now() / 1000) + 30 * 60),\n })\n const stripeResponse = await fetch(\n 'https://api.stripe.com/v1/checkout/sessions',\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n // AGL-2147. This was the ONE session-creating path in the repo\n // with no idempotency key: console checkout, enterprise billing,\n // marketplace checkout, and commerce's checkout / cart-checkout /\n // draft-order / reservation / POS paths all carry one, and this\n // takes a card payment exactly as they do.\n //\n // Keyed on the BOOKING, not on a client-minted header: the\n // transaction above mints a fresh `bookingId` per attempt, so it\n // already names this attempt uniquely and needs no widget change\n // to supply. That also sidesteps Stripe's 24h key expiry — a key\n // reused across attempts eventually replays a session that has\n // since expired (these are opened with a 30-minute `expires_at`),\n // handing the guest a dead checkout page instead of a new one.\n //\n // What it closes: a retried POST after Stripe has already created\n // the session but before its response arrived. Without the key\n // that opens a SECOND payable session against one held slot, and\n // a guest who pays both is charged twice for one appointment.\n 'Idempotency-Key': `booking-${bookingId}`,\n },\n body: params.toString(),\n },\n )\n const session = (await stripeResponse.json()) as {\n url?: string\n error?: unknown\n }\n if (!stripeResponse.ok || !session.url) {\n console.error('Stripe booking checkout error', session.error)\n // Release the hold so the slot isn't stuck for 15 minutes.\n await bookingsRef\n .doc(bookingId)\n .set({ status: 'canceled' }, { merge: true })\n .catch(() => undefined)\n return res.status(502).json({ error: 'Payment setup failed' })\n }\n // The lead landed above; the confirmation email + workflow event fire\n // from the payment webhook.\n return res\n .status(200)\n .json({ bookingId, startsAtMs, endsAtMs, checkoutUrl: session.url })\n }\n\n // Event trigger (AGL-128/148/159).\n // In-app notification to the site's managers (AGL-259).\n void notifyHostManagers(hostId, {\n type: 'content.booking',\n title: 'New booking',\n body: new Date(startsAtMs).toLocaleString(),\n link: `/${hostId}/bookings`,\n })\n const { alerts } = await emitHostEvent(hostId, 'booking', {\n serviceName: service.name ?? '',\n email,\n startsAtMs,\n })\n\n // Env-gated confirmation email (same provider as AGL-98).\n if (isEmailConfigured()) {\n const timezone = service.timezone || 'UTC'\n const when = new Intl.DateTimeFormat('en-US', {\n timeZone: timezone,\n dateStyle: 'full',\n timeStyle: 'short',\n }).format(new Date(startsAtMs))\n const fallbackText =\n `Hi ${name},\\n\\nYour booking for \"${service.name}\" is ` +\n `confirmed for ${when} (${timezone}).\\n\\n` +\n `Reference: ${bookingId}`\n // Site-owner-designed template when published (AGL-770); null keeps the\n // built-in copy above.\n const designed = await renderHostEmailWithTokens(\n firestore,\n hostId,\n 'booking-confirmed',\n {\n name: String(name ?? ''),\n 'service.name': String(service.name ?? ''),\n when,\n timezone,\n 'booking.ref': String(bookingId),\n },\n )\n // White-label sender identity (White-Label Phase 3): the store's brand\n // via the one shared resolver, from the owning org doc.\n const branding = resolveBrandingProfile(\n (await getOrgForHost(hostId).catch(() => null))?.org as never,\n )\n await sendEmail({\n to: email,\n subject: designed?.subject ?? `Booking confirmed: ${service.name}`,\n text: designed?.text || fallbackText,\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: branding.fromName,\n sendingIdentity: await hostSendingIdentity(hostId),\n audience: 'tenant',\n context: 'booking confirmation',\n })\n // Cost meter (AGL-1438). Transactional: counted, never capped — a\n // confirmation the customer never receives reads to them as a booking\n // that did not happen.\n await meterHostEmail(hostId)\n }\n\n // THE RECORD'S MEETING (AGL-2660). A free booking is confirmed right\n // here, so this is where it reaches the CRM: the meeting on the\n // record's timeline and, when the service asks, the follow-up task. A\n // paid booking waits for the webhook, which files it once the charge\n // has cleared. Awaited, because a route handler is frozen the moment\n // it answers (AGL-2327) — and never thrown: the slot is the guest's\n // whatever the CRM did.\n await fileBookingOnCrm(firestore, {\n hostId,\n bookingId,\n booking: {\n serviceId,\n serviceName: service.name ?? '',\n email,\n startsAtMs,\n endsAtMs,\n ...(crmRef ? { crmRef: formatCrmBookingRef(crmRef) } : {}),\n },\n service,\n })\n\n return res.status(200).json({ bookingId, startsAtMs, endsAtMs, alerts })\n } catch (error: unknown) {\n if ((error as { code?: number })?.code === 409) {\n return res\n .status(409)\n .json({ error: 'That time was just taken — pick another slot' })\n }\n console.error(error)\n return res.status(500).json({ error: 'Booking failed' })\n }\n}\n\n/**\n * One reminder pass (AGL-160), extracted from its HTTP door in AGL-2431.\n *\n * Finds confirmed bookings starting 23–25 hours out (collection-group query,\n * so one call covers every host), emails each visitor through the env-gated\n * Resend path, and stamps `reminderSentAt` so re-runs are idempotent.\n *\n * WHY THIS IS A FUNCTION NOW. It only ever existed as `POST bookings/reminders`\n * behind `x-cron-secret`, and nothing anywhere POSTed to it — not\n * `scheduled-crons.yml`, not `vercel.json` (which has no `crons` key), not\n * `registerPluginJob`. The route's own docblock said \"invoke hourly from the\n * scheduler\" for as long as it has existed. It never ran, for anyone. Same\n * shape as the two commerce beats in AGL-2227, one plugin over.\n */\nexport async function scanBookingReminders(\n /** The lockdown gate, injected by the caller (AGL-2495). Not optional. */\n gate: PluginJobHostGate,\n): Promise<{\n scanned: number\n sent: number\n skipped: number\n skippedLocked: number\n}> {\n const firestore = firebaseAdmin.app().firestore()\n // ONE instant for the query bounds and for the per-booking predicate. Two\n // `Date.now()` calls would put a booking sitting on an edge inside the\n // query and outside the test, so the pass would fetch it, skip it, and\n // report a `skipped` the console could not explain.\n const nowMs = Date.now()\n const windowStart = nowMs + REMINDER_WINDOW_START_HOURS * 60 * 60 * 1000\n const windowEnd = nowMs + REMINDER_WINDOW_END_HOURS * 60 * 60 * 1000\n const upcoming = await firestore\n .collectionGroup('bookings')\n .where('startsAtMs', '>=', windowStart)\n .where('startsAtMs', '<=', windowEnd)\n .limit(500)\n .get()\n\n let sent = 0\n let skipped = 0\n let skippedLocked = 0\n // Resolve each host's designed reminder template once per run (AGL-770),\n // not once per booking — a busy site has many bookings in the window.\n const templateCache = new Map<string, LoadedHostEmail | null>()\n // White-label brand per host (White-Label Phase 3): resolved once per host\n // from the owning org doc through the one shared resolver, so a reminder\n // reads as the store's brand.\n const brandingByHost = new Map<\n string,\n ReturnType<typeof resolveBrandingProfile>\n >()\n /*\n * One identity resolution per SITE, not per booking.\n *\n * The same reason the branding and template maps above exist: this sweep\n * runs across every site, and an uncached resolution would be two Firestore\n * reads per reminder rather than per host. Scoped to the run, so a domain\n * un-verified between runs is picked up on the next one.\n */\n const identityByHost = new Map<\n string,\n Awaited<ReturnType<typeof hostSendingIdentity>>\n >()\n for (const doc of upcoming.docs) {\n const data = doc.data()\n // The shared predicate, not a local copy of it (AGL-2431): the console\n // card counts what is waiting with this same function, so the number a\n // merchant reads is the number this loop will act on.\n if (!isBookingReminderDue(data as never, nowMs)) {\n skipped += 1\n continue\n }\n const when = new Date(Number(data['startsAtMs'])).toLocaleString(\n 'en-US',\n { dateStyle: 'full', timeStyle: 'short' },\n )\n // bookings live at hosts/{hostId}/bookings/{id}, so the grandparent is\n // the host.\n const hostId = doc.ref.parent.parent?.id ?? ''\n // LOCKDOWN (AGL-2495), ahead of the send, the meter and the\n // `reminderSentAt` stamp — the three things this loop does, all of them\n // for the host. Counted separately from `skipped` on purpose: `skipped`\n // means \"this booking was not due\", and folding a locked site into it\n // would make a suspended merchant's silence read as an ordinary quiet\n // hour on the console card that shows these numbers.\n //\n // SKIPPED, NOT DROPPED: `reminderSentAt` is what retires a booking from\n // this pass, and it is stamped only on a successful send. An unstamped\n // booking is re-scanned hourly — though unlike the other five, this one\n // has a real deadline: the 23–25h window closes. A reminder whose window\n // passed under a lock is genuinely lost, and that is the correct\n // outcome, not a defect to engineer around. A site under takedown should\n // not be mailing its customers, and a reminder that arrives after the\n // appointment is worse than none.\n if (await gate.isLocked(hostId)) {\n skippedLocked += 1\n continue\n }\n let loaded = templateCache.get(hostId)\n if (loaded === undefined) {\n loaded = hostId\n ? await loadHostEmail(firestore, hostId, 'booking-reminder', {\n // The site's header and footer blocks, grafted once per site\n // for the whole batch (AGL-3287).\n compose: composeHostComponentNodes,\n })\n : null\n templateCache.set(hostId, loaded)\n brandingByHost.set(\n hostId,\n resolveBrandingProfile(\n (hostId\n ? await getOrgForHost(hostId).catch(() => null)\n : null\n )?.org as never,\n ),\n )\n }\n const serviceName = String(data['serviceName'] ?? 'your booking')\n const designed = loaded\n ? renderLoadedHostEmail(\n loaded,\n {\n name: String(data['name'] ?? ''),\n 'service.name': serviceName,\n when,\n },\n sanitizeAuthorHtml,\n )\n : null\n const result = await sendEmail({\n to: String(data['email']),\n subject: designed?.subject ?? `Reminder: ${serviceName} tomorrow`,\n text:\n designed?.text ||\n `Hi ${data['name'] ?? ''},\\n\\nA reminder that \"${serviceName}\" is ` +\n `scheduled for ${when}.\\n\\nReference: ${doc.id}`,\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: brandingByHost.get(hostId)?.fromName,\n sendingIdentity: await hostSendingIdentity(hostId, identityByHost),\n audience: 'tenant',\n context: 'booking reminder',\n })\n if (result.sent) {\n // Cost meter (AGL-1438). Transactional: a reminder a quota refused is\n // a missed appointment for the site's own customer.\n await meterHostEmail(hostId)\n sent += 1\n await doc.ref\n .set(\n {\n reminderSentAt:\n firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n .catch(() => undefined)\n }\n }\n return { scanned: upcoming.size, sent, skipped, skippedLocked }\n}\n\n/**\n * The reminder beat (AGL-2431).\n *\n * MODULE SCOPE, like `expire-stale-holds` above and the three commerce beats:\n * the runner route reaches jobs through `ensureAll(['tenantApi'])`, so a\n * registration inside a `register*` function would depend on which entry\n * point happened to be loaded — the AGL-2227 lesson.\n *\n * HOURLY, matching the window's own resolution. The scan mails a two-hour\n * band 23–25h out, so a beat faster than the band buys nothing; a beat slower\n * than 2h could step over the band entirely and skip a day's reminders. An\n * hour sits inside that with room, and the pass is bounded (500 docs) and\n * idempotent — `reminderSentAt` is stamped on send — so an overlapping or\n * repeated beat cannot double-mail anybody.\n */\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'booking-reminders',\n intervalMinutes: 60,\n description: 'Email a 24-hour reminder for each upcoming confirmed booking.',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n // Quietly, not as an error: email is optional per deployment, and a beat\n // that logs hourly on a self-host without Resend buries everything else.\n if (!isEmailConfigured()) return\n const { sent } = await scanBookingReminders(gate)\n if (sent) console.info(`bookings: sent ${sent} booking reminders`)\n },\n})\n\n/**\n * The manual door, kept for ops (AGL-2431): the scheduling now lives in the\n * `booking-reminders` job above, and this stays so a pass can be forced\n * without waiting for the beat.\n */\nconst remindersHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const cronSecret = process.env.CRON_SECRET\n if (!cronSecret) {\n return res\n .status(501)\n .json({ error: 'Reminders are not configured (CRON_SECRET).' })\n }\n if (req.headers['x-cron-secret'] !== cronSecret) {\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n if (!isEmailConfigured()) {\n return res.status(501).json({\n error: 'Reminders are not configured (RESEND_API_KEY, USAGE_EMAIL_FROM).',\n })\n }\n try {\n // The manual door asks the same question the beat does (AGL-2495).\n return res.status(200).json(await scanBookingReminders(pluginJobHostGate()))\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Reminders failed' })\n }\n}\n\n/** Registers the bookings plugin's public (site-facing) API routes (AGL-396). */\nexport function registerBookingsApi(): void {\n registerPluginApiRoute('bookings/slots', slotsHandler)\n registerPluginApiRoute('bookings/book', bookHandler)\n}\n\n/** Registers the bookings plugin's console-side API routes (AGL-396). */\nexport function registerBookingsConsoleApi(): void {\n registerPluginApiRoute('bookings/reminders', remindersHandler)\n // Refunding a paid booking (AGL-2315). Console-side, because it is\n // site-admin-gated and moves money — never a site-facing route.\n registerPluginApiRoute('bookings/refund', bookingRefundHandler)\n // The merchant-side `purchase` lookup (AGL-2481). A public, unauthenticated\n // read like `bookings/slots`, authorised by the unguessable Stripe session\n // id and answering with a projection that carries no guest identity.\n registerPluginApiRoute('bookings/booking-analytics', bookingAnalyticsHandler)\n // Paid-booking confirmation rides the platform Stripe webhook (AGL-418).\n registerBillingWebhookHandler(bookingsBillingWebhookHandler)\n // Bookings by service as a figure table (AGL-2915), for the AI plugin's\n // insights to read by id; the console runs insight jobs.\n registerBookingFigureReader(() => firebaseAdmin.app().firestore())\n}\n"],"names":["checkEntitlement","pluginJobHostGate","registerPluginConfigSchema","registerPluginJob","resolveBrandingProfile","resolveTransactionFeeCents","sanitizeAuthorHtml","BOOKING_MAX_DAYS_AHEAD","computeOpenSlots","isBookingReminderDue","isSlotOpen","REMINDER_WINDOW_END_HOURS","REMINDER_WINDOW_START_HOURS","registerBillingWebhookHandler","registerPluginApiRoute","pluginTaxProfile","BUNDLE_ID","BOOKINGS_CONFIG_SCHEMA","bookingsBillingWebhookHandler","bookingAnalyticsHandler","registerBookingFigureReader","bookingRefundHandler","formatCrmBookingRef","parseCrmBookingRef","fileBookingOnCrm","pluginId","name","intervalMinutes","description","lockdown","scope","handler","gate","cutoff","Date","now","snapshot","firebaseAdmin","app","firestore","collectionGroup","where","limit","get","lapsed","doc","docs","hostId","ref","parent","id","isLocked","set","status","merge","console","info","getOrgForHost","meterHostEmail","notifyHostManagers","attributeCampaignConversion","resolveCampaignTouch","getPluginConfig","resolveOrgIdForHost","renderHostEmailWithTokens","hostSendingIdentity","connectLinkageIsReady","emitHostEvent","recordCapturedContact","isEmailConfigured","loadHostEmail","renderLoadedHostEmail","sendEmail","FieldValue","NO_CLIENT_ADDRESS_BUCKET","readClientIp","composeHostComponentNodes","BOOKING_EMAIL_PATTERN","recentBookingIpHits","Map","BOOKING_RATE_WINDOW_MS","BOOKING_RATE_MAX","bookingRateLimited","ip","hits","filter","t","push","length","slotsHandler","req","res","method","json","error","String","query","serviceId","config","service","hostRef","collection","services","map","$id","durationMinutes","Number","priceUsd","serviceSnapshot","data","fromMs","maxDaysAhead","toMs","bookedSnapshot","booked","startsAtMs","endsAtMs","slots","timezone","bookHandler","body","trim","slice","email","toLowerCase","marketingConsent","crmRef","isFinite","test","headers","remoteAddress","socket","hostSnapshot","Promise","all","exists","ownerOrg","org","durationMs","Math","max","round","bookingsRef","paid","process","env","STRIPE_SECRET_KEY","chargeAccountId","feeCents","serviceTax","taxCents","label","pct","storeSnapshot","ownerUid","ownerProfile","resolve","accountId","chargesEnabled","accountLivemode","subject","taxSettings","tax","chargeCents","flatTax","bookingId","runTransaction","transaction","overlapping","Object","assign","Error","code","bookingRef","serviceName","expiresAtMs","createdAt","serverTimestamp","bookedAtMs","campaignTouch","wire","atMs","kind","refId","touch","convertedAtMs","orgId","identity","undefined","interaction","source","summary","surface","detail","origin","host","params","URLSearchParams","mode","success_url","cancel_url","customer_email","expires_at","floor","stripeResponse","fetch","Authorization","toString","session","ok","url","catch","checkoutUrl","type","title","toLocaleString","link","alerts","when","Intl","DateTimeFormat","timeZone","dateStyle","timeStyle","format","fallbackText","designed","branding","to","text","html","fromName","sendingIdentity","audience","context","booking","scanBookingReminders","nowMs","windowStart","windowEnd","upcoming","sent","skipped","skippedLocked","templateCache","brandingByHost","identityByHost","loaded","compose","result","reminderSentAt","scanned","size","remindersHandler","cronSecret","CRON_SECRET","registerBookingsApi","registerBookingsConsoleApi"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/bookings/src/lib/server.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 { checkEntitlement,\n type PluginJobHostGate,\n registerPluginConfigSchema,\n registerPluginJob,\n resolveBrandingProfile,\n resolveTransactionFeeCents,\n sanitizeAuthorHtml,\n} from '@aglyn/aglyn/server'\n// The leaf, not the barrel: the console's `x-cron-secret` door asks the\n// site's lockdown directly (AGL-3356) — core's registry has no resolver in\n// the console process — and a spec that substitutes the barrel must still\n// reach the real verdict.\nimport { siteLockdownJobGate } from '@aglyn/tenant-data-admin/server/tenant-write-lockdown'\nimport { type BookedInterval, BOOKING_MAX_DAYS_AHEAD, computeOpenSlots, type HostBookingService, isBookingReminderDue, isSlotOpen, REMINDER_WINDOW_END_HOURS, REMINDER_WINDOW_START_HOURS } from './model'\nimport {\n registerBillingWebhookHandler,\n registerPluginApiRoute,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport {\n pluginTaxProfile,\n type PluginResolvedFlatTax,\n} from '@aglyn/aglyn/plugin-manager/plugin-tax-profile'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { BOOKINGS_CONFIG_SCHEMA } from './plugin-config'\nimport { bookingsBillingWebhookHandler } from './server/billing-webhook'\nimport { bookingAnalyticsHandler } from './server/booking-analytics'\nimport { registerBookingFigureReader } from './server/booking-figures'\nimport { bookingRefundHandler } from './server/refund'\n// The booking's way back to the CRM record (AGL-2660): the reference a\n// booking link carried, and the meeting a free booking files on landing.\n// The leaf, not the barrel: this library's specs substitute the barrel\n// wholesale, and a parser that vanished under them would fail every\n// booking they take.\nimport {\n formatCrmBookingRef,\n parseCrmBookingRef,\n} from '@aglyn/aglyn/app-utils/crm-booking'\nimport { fileBookingOnCrm } from './server/booking-crm'\n\n// Settings schema (AGL-428): registered here too so server-only loads\n// (API dispatchers) get defaults-merged getPluginConfig reads.\nregisterPluginConfigSchema(BOOKINGS_CONFIG_SCHEMA)\n\n// Scheduled cleanup (AGL-435): lapse day-old expired payment holds to\n// 'canceled' (the read paths already treat them as released — this keeps\n// the collection tidy and the read-time filter cheap). Bounded + failure\n// tolerant: a missing collection-group index just logs and retries on\n// the next beat.\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'expire-stale-holds',\n intervalMinutes: 6 * 60,\n description: 'Cancel pendingPayment bookings whose hold lapsed >24h ago.',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n const cutoff = Date.now() - 24 * 60 * 60_000\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collectionGroup('bookings')\n .where('status', '==', 'pendingPayment')\n .where('expiresAtMs', '<', cutoff)\n .limit(100)\n .get()\n let lapsed = 0\n for (const doc of snapshot.docs) {\n // LOCKDOWN (AGL-2495). `bookings` live at `hosts/{hostId}/bookings/{id}`,\n // so the grandparent is the host. This flips a booking to `canceled` for\n // a site that may be suspended mid-dispute, and a status a customer can\n // see is not a change to make while the operator is deciding whether the\n // site should exist.\n //\n // SKIPPED, NOT DROPPED: the query selects on `status == 'pendingPayment'`\n // and an age cutoff that only gets truer with time, so an untouched hold\n // is picked up by the next six-hour beat after the lift. The read paths\n // already treat a lapsed hold as released, so waiting costs tidiness and\n // nothing else — which is why this one can afford to wait at all.\n const hostId = doc.ref.parent.parent?.id ?? ''\n if (await gate.isLocked(hostId)) continue\n await doc.ref.set({ status: 'canceled' }, { merge: true })\n lapsed += 1\n }\n if (lapsed) {\n console.info(`bookings: lapsed ${lapsed} stale payment holds`)\n }\n },\n})\nimport {\n firebaseAdmin,\n getOrgForHost,\n meterHostEmail,\n notifyHostManagers,\n attributeCampaignConversion,\n resolveCampaignTouch,\n getPluginConfig,\n resolveOrgIdForHost,\n renderHostEmailWithTokens,\n hostSendingIdentity,\n} from '@aglyn/tenant-data-admin'\nimport { connectLinkageIsReady } from '@aglyn/tenant-data-admin/server/stripe-account-mode'\nimport { checkoutSessionCardAuthenticationParams } from '@aglyn/tenant-data-admin/server/stripe-card-authentication'\nimport { emitHostEvent } from '@aglyn/tenant-runtime'\nimport recordCapturedContact from '@aglyn/aglyn/plugin-manager/record-captured-contact'\nimport {\n isEmailConfigured,\n loadHostEmail,\n renderLoadedHostEmail,\n sendEmail,\n type LoadedHostEmail,\n} from '@aglyn/shared-util-email'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n NO_CLIENT_ADDRESS_BUCKET,\n readClientIp,\n} from '@aglyn/aglyn/app-utils/request-ip'\nimport { composeHostComponentNodes } from '@aglyn/aglyn/app-utils/load-referenced-components'\n\nconst BOOKING_EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n// Best-effort per-instance rate limit (mirrors forms/submit).\nconst recentBookingIpHits = new Map<string, number[]>()\nconst BOOKING_RATE_WINDOW_MS = 60_000\nconst BOOKING_RATE_MAX = 5\n\nfunction bookingRateLimited(ip: string): boolean {\n const now = Date.now()\n const hits = (recentBookingIpHits.get(ip) ?? []).filter(\n (t) => now - t < BOOKING_RATE_WINDOW_MS,\n )\n hits.push(now)\n recentBookingIpHits.set(ip, hits)\n return hits.length > BOOKING_RATE_MAX\n}\n\n/**\n * Public open-slot listing (AGL-159): visitors browse a service's next\n * open times. Server-side because bookings (names/emails) must never be\n * client-readable — only the derived busy intervals are used here.\n */\n// Exported for `bookings-slot-window.spec.ts`: the query's bounds are a\n// read-cost and correctness property of THIS handler, and asserting them\n// through the route registry would test the registry instead. Its siblings\n// (`bookHandler`, the refund and analytics handlers) are exported already.\nexport const slotsHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'GET') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.query['hostId'] ?? '')\n const serviceId = String(req.query['serviceId'] ?? '')\n if (!hostId) {\n return res.status(400).json({ error: 'Missing hostId' })\n }\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n\n // No serviceId → public service directory for the booking widget\n // (AGL-160): names/durations/prices only, never availability internals.\n if (!serviceId) {\n const services = await hostRef.collection('services').limit(50).get()\n return res.status(200).json({\n services: services.docs\n .filter((doc) => !doc.get('deletedAt'))\n .map((doc) => ({\n $id: doc.id,\n name: doc.get('name') ?? '',\n durationMinutes: Number(doc.get('durationMinutes') ?? 30),\n priceUsd: Number(doc.get('priceUsd') ?? 0),\n description: doc.get('description') ?? '',\n })),\n })\n }\n const serviceSnapshot = await hostRef\n .collection('services')\n .doc(serviceId)\n .get()\n const service = serviceSnapshot.data() as HostBookingService | undefined\n if (!service || (serviceSnapshot.get('deletedAt') as unknown)) {\n return res.status(404).json({ error: 'Unknown service' })\n }\n const fromMs = Date.now()\n // Booking horizon (AGL-428): configurable via the plugin settings\n // framework; defaults to BOOKING_MAX_DAYS_AHEAD through the schema.\n //\n // Resolved for THIS SITE (AGL-428, AGL-1014), not just the workspace.\n // The horizon is the setting per-site overrides were built for — one\n // chain, one answer, and the flagship branch taking bookings further out\n // — so reading only the org's would leave the console showing a number\n // this endpoint never honors.\n const config = await getPluginConfig(\n await resolveOrgIdForHost(hostId),\n 'bookings',\n { hostId },\n )\n const maxDaysAhead = Number(config.maxDaysAhead ?? BOOKING_MAX_DAYS_AHEAD)\n const toMs = fromMs + maxDaysAhead * 24 * 60 * 60_000\n // Bounded at BOTH ends, to the window the slots are actually computed\n // over. `computeOpenSlots` below is handed `fromMs`/`toMs` and ignores\n // anything outside them, so a booking past the horizon was read, billed\n // and discarded — and worse, it competed for the 500 with the bookings\n // that do matter, so a service booked far ahead could push the near-term\n // stays out of its own availability check and offer a taken slot.\n //\n // A second range on `startsAtMs` needs no new index: the field already\n // carries one, and the range it narrows is the one already here.\n const bookedSnapshot = await hostRef\n .collection('bookings')\n .where('serviceId', '==', serviceId)\n .where('startsAtMs', '>=', fromMs - 24 * 60 * 60_000)\n .where('startsAtMs', '<=', toMs)\n .limit(500)\n .get()\n const booked: BookedInterval[] = bookedSnapshot.docs\n .filter(\n (doc) =>\n doc.get('status') !== 'canceled' &&\n // Expired payment holds release the slot (AGL-170).\n !(\n doc.get('status') === 'pendingPayment' &&\n Number(doc.get('expiresAtMs') ?? 0) < Date.now()\n ),\n )\n .map((doc) => ({\n startsAtMs: Number(doc.get('startsAtMs') ?? 0),\n endsAtMs: Number(doc.get('endsAtMs') ?? 0),\n }))\n const slots = computeOpenSlots(service, fromMs, toMs, booked, 120)\n return res.status(200).json({\n service: {\n name: service.name,\n durationMinutes: service.durationMinutes,\n priceUsd: service.priceUsd ?? 0,\n timezone: service.timezone ?? 'UTC',\n },\n slots,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Slot lookup failed' })\n }\n}\n\n/**\n * Booking creation (AGL-159): validates the slot against the service's\n * windows AND stored bookings inside a transaction (double-booking safe),\n * stores the booking, records a lead, emits the `booking` host event, and\n * sends an env-gated Resend confirmation. Plan gate: the owning org\n * needs the `bookings` flag (dark-launch workspaces pass).\n */\n/**\n * Exported for spec (AGL-2000). The tax decision this handler takes lives in\n * the ABSENCE of a parameter, and an absence is only a decision if something\n * asserts it — see `server-booking-tax.spec.ts`.\n */\nexport const bookHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const serviceId = String(req.body?.serviceId ?? '')\n const startsAtMs = Number(req.body?.startsAtMs ?? 0)\n const name = String(req.body?.name ?? '')\n .trim()\n .slice(0, 80)\n const email = String(req.body?.email ?? '')\n .trim()\n .toLowerCase()\n // Explicit opt-in checkbox (AGL-2499) — a booking is not by itself\n // consent to be emailed marketing, so this is only set when the visitor\n // checked it.\n const marketingConsent = req.body?.marketingConsent === true\n // The CRM record the booking link named (AGL-2660), kept on the row so\n // the booking can be attributed to it even when the visitor books with\n // a different address. Parsed, never stored raw: this is a public door.\n const crmRef = parseCrmBookingRef(req.body?.crmRef)\n if (!hostId || !serviceId || !Number.isFinite(startsAtMs) || !startsAtMs) {\n return res.status(400).json({ error: 'Invalid booking request' })\n }\n if (!name) return res.status(400).json({ error: 'Enter your name' })\n if (!BOOKING_EMAIL_PATTERN.test(email)) {\n return res.status(400).json({ error: 'Enter a valid email' })\n }\n if (startsAtMs < Date.now()) {\n return res.status(409).json({ error: 'That time has already passed' })\n }\n // Keeps counting under the no-address bucket rather than being skipped: an\n // unauthenticated booking endpoint that stops counting writes reservations\n // for anyone who can reach it.\n const ip =\n readClientIp(req.headers, { remoteAddress: req.socket?.remoteAddress }) ??\n NO_CLIENT_ADDRESS_BUCKET\n if (bookingRateLimited(ip)) {\n return res.status(429).json({ error: 'Too many booking attempts' })\n }\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const [hostSnapshot, serviceSnapshot] = await Promise.all([\n hostRef.get(),\n hostRef.collection('services').doc(serviceId).get(),\n ])\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const service = serviceSnapshot.data() as HostBookingService | undefined\n if (!service || (serviceSnapshot.get('deletedAt') as unknown)) {\n return res.status(404).json({ error: 'Unknown service' })\n }\n\n // Plan gate (dark-launch rule preserved). Plan/quota gates ride the\n // owning org's doc (AGL-238). Hoisted out of its block for AGL-2315:\n // the same org doc now also prices the platform's cut below, and reading\n // it twice would let a gate and a fee disagree within one request.\n const ownerOrg = (await getOrgForHost(hostId))?.org\n if (!checkEntitlement(ownerOrg as never, 'bookings')) {\n return res\n .status(403)\n .json({ error: 'Bookings are not enabled on this site' })\n }\n\n const durationMs =\n Math.max(5, Math.round(service.durationMinutes || 30)) * 60_000\n const endsAtMs = startsAtMs + durationMs\n const bookingsRef = hostRef.collection('bookings')\n\n // Paid services (AGL-170): the slot is HELD pending payment — the\n // booking lands as `pendingPayment` with a 15-minute expiry (expired\n // holds release the slot in the collision filters), and the visitor\n // goes to Stripe Checkout; the webhook confirms + emails on payment.\n const priceUsd = Number(service.priceUsd ?? 0)\n const paid = priceUsd > 0\n if (paid && !process.env.STRIPE_SECRET_KEY) {\n return res.status(501).json({\n error: 'Paid bookings are not configured (STRIPE_SECRET_KEY).',\n })\n }\n\n // WHO GETS THE MONEY (AGL-2315). Until this landed, a paid booking opened\n // a Checkout Session with no Connect wiring of any kind — no\n // `transfer_data`, no `application_fee_amount`, no connected account read\n // anywhere in the plugin — so 100% of every paid booking settled in\n // AGLYN'S OWN platform balance and the merchant was never paid at all.\n // Not under-paid: never paid. Every sibling tenant money path (storefront\n // checkout, cart, draft orders, POS, reservations) was already a\n // destination charge to the merchant's connected account; this was the one\n // door that took a card and kept the proceeds.\n //\n // Resolved BEFORE the hold transaction on purpose, the `reserve.ts`\n // ordering: an unconnected merchant is a deterministic refusal the site\n // owner fixes out of band, and discovering it after the slot is written\n // would strand a real appointment slot as `pendingPayment` for 15 minutes\n // on a booking that could never be paid.\n let chargeAccountId = ''\n let feeCents = 0\n // No tax until a paid booking asks the tenant's tax rule what is owed.\n let serviceTax: PluginResolvedFlatTax = { taxCents: 0, label: '', pct: 0 }\n if (paid) {\n const ownerUid = (ownerOrg as { ownerUid?: unknown } | null)?.ownerUid\n const [ownerProfile, storeSnapshot] = await Promise.all([\n ownerUid\n ? firestore.collection('profiles').doc(String(ownerUid)).get()\n : Promise.resolve(null),\n // The merchant's own service rate (AGL-2028), in the SAME round trip\n // as the connected-account read this path already makes.\n hostRef.collection('settings').doc('store').get(),\n ])\n chargeAccountId = String(ownerProfile?.get('stripeAccountId') ?? '')\n if (\n !connectLinkageIsReady(\n {\n accountId: chargeAccountId,\n chargesEnabled: ownerProfile?.get('stripeChargesEnabled'),\n accountLivemode: ownerProfile?.get('stripeAccountLivemode'),\n },\n { subject: `booking host ${hostId}` },\n )\n ) {\n return res.status(409).json({ error: 'Payments are not set up yet' })\n }\n\n // THE TAKE RATE IS THE STOREFRONT LADDER'S, NOT A NEW ONE (AGL-2315).\n //\n // the decision, 2026-08-19: bookings mirror the storefront ladder —\n // the same rate a storefront sale already takes, tapering to 0% on the\n // upper tiers. It is consistent with what merchants already agreed to\n // and needs no new price communication, so pricing stays locked for\n // Sept 1.\n //\n // Implemented by ROUTING THROUGH the existing resolver rather than\n // adding a bookings rate: `resolveTransactionFeePct` already carries a\n // `'service'` axis (it resolves to the digital rate), and a booking is a\n // service sale — `reserve.ts` prices a stay through the identical call.\n // So there is no new constant, no new plan-table row, and no second\n // place for a rate to drift out of step with `/pricing`. Every guard\n // that already holds for the storefront rate holds here for free: the\n // malformed-override floor (AGL-2114), the lapsed-plan re-resolution,\n // and the staff override.\n //\n // FEE BASIS: post-discount ITEMS ONLY, matching the one-time storefront\n // paths and deliberately NOT the subscription path's\n // `application_fee_percent`, which Stripe applies to the whole invoice\n // and so takes a cut of sales tax and shipping (AGL-2317). Here the two\n // bases happen to coincide, because this session carries exactly one\n // line item and, by AGL-2000's stated decision, no tax line and no\n // shipping option. That coincidence is not the reason for the choice —\n // sending a computed CENTS amount rather than a percent is what makes\n // the basis survive the day this path does compute service tax: a fee\n // derived from `priceUsd` stays off the tax the moment tax is added,\n // whereas a percent would silently start taxing the state's money.\n //\n // PLUS STRIPE'S CARD COST, AT COST (AGL-2152). A 0%-fee tier used to\n // take a destination charge with NO application fee at all, which is not\n // \"no take rate\" — Stripe debits 2.9% + 30¢ from the PLATFORM's balance\n // when that charge settles, so every paid booking on Pro and above cost\n // Aglyn money. `resolveTransactionFeeCents` is the same resolver's cents\n // form: the advertised take (still 0% where the ladder says 0%) plus the\n // card cost passed through, so the net per booking is the take and never\n // less. The `Math.max(1, …)` floor on a chargeable take is folded into\n // it, unchanged.\n //\n // The fee base and the charge base are the same number here: by\n // AGL-2000's stated decision this session carries one line item, no tax\n // line and no shipping option, so the card runs for exactly `priceUsd`.\n // SERVICE TAX: the MERCHANT'S OWN RATE, off unless they set one\n // (AGL-2028, answering the decision AGL-2000 recorded here).\n //\n // AGL-2000's reasoning is unchanged and is why this is a field of its\n // own rather than a reuse of the goods table. A service is not goods:\n // whether one is taxable is jurisdiction-specific and frequently the\n // opposite answer, so the AGL-285 `rates[]` are still never read here\n // — a test in `server-booking-tax.spec.ts` pins that with a fully\n // configured sales-tax store. Stripe Tax is still never asked either:\n // it has no service tax code from this handler and would compute a\n // goods rate against AGLYN's registrations (AGL-1904).\n //\n // What changed is that the merchant now has somewhere to answer. A\n // service business is one of the three named ICPs and in many states\n // their services ARE taxable, so \"Aglyn does not compute it\" could only\n // ever be a holding position. They type the rate, we compute it, charge\n // it, record it and stamp the regime; the settings card states plainly\n // that determining what is owed and to whom is theirs. Aglyn takes no\n // tax position, and the existence of a separate, blank-by-default field\n // is also AGL-2000's second reason answered — it is the merchant\n // saying, explicitly, that they mean this to cover bookings.\n //\n // DEFAULT OFF, and load-bearing: an absent, zero, negative or\n // out-of-range rate resolves to zero, so no existing merchant's charge\n // moves because this shipped.\n const taxSettings = ((storeSnapshot?.data() as any)?.tax ?? {}) as {\n service?: unknown\n }\n const chargeCents = Math.round(priceUsd * 100)\n // Asked of the plugin that owns the merchant's tax rule, which throws\n // rather than answer zero if none is loaded: an untaxed charge recorded\n // as untaxed is the failure nobody sees until the merchant owes it.\n serviceTax = pluginTaxProfile().flatTax(\n taxSettings.service,\n chargeCents,\n 'Service tax',\n )\n // THE TWO FEE BASES ARE NOW DIFFERENT NUMBERS (AGL-2028). The take is\n // computed on the SERVICE, never on the state's money — AGL-2317's rule\n // — while Stripe's card cost is debited from the platform balance on\n // the WHOLE charge and so is passed through on the whole charge\n // (AGL-2152). The comment above already promised this: \"a fee derived\n // from `priceUsd` stays off the tax the moment tax is added\". This is\n // the moment.\n feeCents = resolveTransactionFeeCents(\n ownerOrg as never,\n 'service',\n chargeCents,\n chargeCents + serviceTax.taxCents,\n )\n }\n\n // Transaction: re-read overlapping bookings and validate the slot so\n // two simultaneous requests cannot double-book.\n const bookingId = await firestore.runTransaction(async (transaction) => {\n const overlapping = await transaction.get(\n bookingsRef\n .where('serviceId', '==', serviceId)\n .where('startsAtMs', '>=', startsAtMs - 24 * 60 * 60_000)\n .limit(500),\n )\n const booked: BookedInterval[] = overlapping.docs\n .filter(\n (doc) =>\n doc.get('status') !== 'canceled' &&\n // Expired payment holds release the slot (AGL-170).\n !(\n doc.get('status') === 'pendingPayment' &&\n Number(doc.get('expiresAtMs') ?? 0) < Date.now()\n ),\n )\n .map((doc) => ({\n startsAtMs: Number(doc.get('startsAtMs') ?? 0),\n endsAtMs: Number(doc.get('endsAtMs') ?? 0),\n }))\n if (!isSlotOpen(service, startsAtMs, booked)) {\n throw Object.assign(new Error('Slot unavailable'), { code: 409 })\n }\n const bookingRef = bookingsRef.doc()\n transaction.set(bookingRef, {\n serviceId,\n serviceName: service.name ?? '',\n name,\n email,\n startsAtMs,\n endsAtMs,\n status: paid ? 'pendingPayment' : 'confirmed',\n ...(paid && { expiresAtMs: Date.now() + 15 * 60_000 }),\n ...(crmRef ? { crmRef: formatCrmBookingRef(crmRef) } : {}),\n createdAt: FieldValue.serverTimestamp(),\n })\n return bookingRef.id\n })\n\n /*\n * THE CAMPAIGN TOUCH, RESOLVED ONCE FOR THE WHOLE BOOKING.\n *\n * A booking is an identify moment: the visitor was anonymous while they\n * browsed and picked a slot, and this request is the first thing that\n * names them. Both channels are asked here — the labels their device\n * carried, and the campaign mail they clicked, joined on the address they\n * just typed — and the later of the two is credited, exactly as the\n * revenue join credits the later click.\n *\n * Resolved once and handed to the booking record, the contact and the\n * lead below, so the email-channel lookup is one keyed read for the whole\n * request and the three records cannot name different campaigns.\n */\n const bookedAtMs = Date.now()\n const campaignTouch = await resolveCampaignTouch({\n hostId,\n wire: req.body?.campaignTouch,\n email,\n atMs: bookedAtMs,\n })\n void attributeCampaignConversion({\n hostId,\n kind: 'booking',\n refId: bookingId,\n touch: campaignTouch,\n convertedAtMs: bookedAtMs,\n })\n\n /*\n * Contacts ingestion (AGL-197) — booking requests identify a person —\n * reported to whichever plugin keeps people rather than written by this\n * one (AGL-3080). Bookings knows it just met somebody; what a person\n * record is belongs to the plugin that models it, and a workspace that\n * keeps none is a quiet, correct answer here.\n */\n /*\n * A booking request is a LEAD SURFACE (AGL-3232): interest, not a sale\n * — no money has moved, and the payment webhook is the door that makes\n * a customer. The record system files the person as a lead, or lands\n * the request on the contact the workspace already holds for the\n * address; which record, and the lead's ceiling and dedupe, are its to\n * decide. Awaited rather than voided so the lead is there before the\n * response, as it was when this door wrote it itself; the seam never\n * throws, so a refused capture still cannot fail the booking.\n */\n await recordCapturedContact({\n /*\n * Not resolved here. The record system keys a person on the SITE they\n * were met on, so this costs the capture nothing — and resolving it\n * would put a Firestore read in front of the writer, which is the one\n * thing `recordCapturedContact` asks a door not to do.\n */\n orgId: '',\n hostId,\n identity: { email, name: name || undefined },\n interaction: {\n source: 'booking',\n refId: bookingId,\n summary: `Booked \"${String(service.name ?? 'a service').slice(0, 60)}\"`,\n },\n surface: 'lead',\n ...(marketingConsent ? { marketingConsent: true } : {}),\n // Where the visitor ARRIVED from — a fact about this visit and not\n // about the person, which is what `detail` carries.\n ...(campaignTouch ? { detail: { campaignTouch } } : {}),\n })\n\n if (paid) {\n const origin = req.headers.origin ?? `https://${req.headers.host}`\n // TAX ON A PAID BOOKING: the merchant's own service rate, resolved\n // above and default OFF (AGL-2028, answering AGL-2000).\n //\n // The rate rides as an ORDINARY product line Stripe is never told is\n // tax — the AGL-1711 construction every other manual rate in this\n // repo uses. That is what makes the derived `taxMode` read `manual`:\n // the figure is the merchant's own, from their own rate, and is never\n // computed against Aglyn's registrations the way `automatic_tax` would\n // be (AGL-1904). Absent entirely when no rate is set, so a default\n // store's session carries no tax parameter of any kind — the property\n // `server-booking-tax.spec.ts` asserts over the whole emitted body.\n //\n // The sale is recorded either way: `booking-payment` has been in\n // `storefront-tax-record.ts`'s SESSION_TYPES since AGL-2000, so an\n // untaxed booking still files a row reading `taxMode: 'none'` and a\n // taxed one files `manual` with the figure, both with no further\n // wiring.\n const params = new URLSearchParams({\n mode: 'payment',\n // 3-D Secure, from the one seam every card payment shares (AGL-3360).\n ...checkoutSessionCardAuthenticationParams('payment'),\n 'line_items[0][quantity]': '1',\n 'line_items[0][price_data][currency]': 'usd',\n 'line_items[0][price_data][unit_amount]': String(\n Math.round(priceUsd * 100),\n ),\n 'line_items[0][price_data][product_data][name]': String(\n service.name ?? 'Booking',\n ).slice(0, 120),\n ...(serviceTax.taxCents > 0\n ? {\n 'line_items[1][quantity]': '1',\n 'line_items[1][price_data][currency]': 'usd',\n 'line_items[1][price_data][unit_amount]': String(\n serviceTax.taxCents,\n ),\n 'line_items[1][price_data][product_data][name]': serviceTax.label,\n }\n : {}),\n // Destination charge to the MERCHANT's connected account (AGL-2315).\n // Unconditional: `transfer_data[destination]` is what makes this the\n // merchant's money, and it is required on every paid booking whatever\n // the take rate. Only the fee below is conditional.\n 'payment_intent_data[transfer_data][destination]': chargeAccountId,\n ...(feeCents > 0\n ? { 'payment_intent_data[application_fee_amount]': String(feeCents) }\n : {}),\n // `{CHECKOUT_SESSION_ID}` is Stripe's own template token, substituted\n // on the redirect (AGL-2481). Without it the guest came back holding\n // the word `paid` and nothing else — no id, no amount — so the\n // merchant-side `purchase` had nothing to look itself up by and could\n // not have stated what was actually charged. The id is safe in the\n // URL: Stripe hands it only to the payer, and\n // `booking-analytics.ts` scopes it to the host and answers with a\n // projection rather than the booking document.\n success_url: `${origin}/?booking=paid&session_id={CHECKOUT_SESSION_ID}`,\n cancel_url: `${origin}/?booking=canceled`,\n customer_email: email,\n 'metadata[type]': 'booking-payment',\n 'metadata[hostId]': hostId,\n 'metadata[bookingId]': bookingId,\n // The fee actually charged, recorded on the session exactly as every\n // sibling path records it — the figure a refund reverses and the\n // figure a reconciliation reads. `'0'` on a 0%-fee tier is a real\n // answer, not a missing one.\n 'metadata[feeCents]': String(feeCents),\n // The only witness to the tax (AGL-2028). By the construction above\n // Stripe does not know the second line is tax and reports\n // `total_details.amount_tax: 0`, so the session's own metadata is\n // what `storefront-tax-record.ts` and the confirmation webhook read\n // to record the figure and derive the regime — exactly as\n // `checkout.ts` stamps it for a buy-now sale. Absent when there is no\n // tax, never a reassuring `'0'`: absent and zero are different facts.\n ...(serviceTax.taxCents > 0\n ? {\n 'metadata[taxCents]': String(serviceTax.taxCents),\n 'metadata[taxPct]': String(serviceTax.pct),\n }\n : {}),\n expires_at: String(Math.floor(Date.now() / 1000) + 30 * 60),\n })\n const stripeResponse = await fetch(\n 'https://api.stripe.com/v1/checkout/sessions',\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n // AGL-2147. This was the ONE session-creating path in the repo\n // with no idempotency key: console checkout, enterprise billing,\n // marketplace checkout, and commerce's checkout / cart-checkout /\n // draft-order / reservation / POS paths all carry one, and this\n // takes a card payment exactly as they do.\n //\n // Keyed on the BOOKING, not on a client-minted header: the\n // transaction above mints a fresh `bookingId` per attempt, so it\n // already names this attempt uniquely and needs no widget change\n // to supply. That also sidesteps Stripe's 24h key expiry — a key\n // reused across attempts eventually replays a session that has\n // since expired (these are opened with a 30-minute `expires_at`),\n // handing the guest a dead checkout page instead of a new one.\n //\n // What it closes: a retried POST after Stripe has already created\n // the session but before its response arrived. Without the key\n // that opens a SECOND payable session against one held slot, and\n // a guest who pays both is charged twice for one appointment.\n 'Idempotency-Key': `booking-${bookingId}`,\n },\n body: params.toString(),\n },\n )\n const session = (await stripeResponse.json()) as {\n url?: string\n error?: unknown\n }\n if (!stripeResponse.ok || !session.url) {\n console.error('Stripe booking checkout error', session.error)\n // Release the hold so the slot isn't stuck for 15 minutes.\n await bookingsRef\n .doc(bookingId)\n .set({ status: 'canceled' }, { merge: true })\n .catch(() => undefined)\n return res.status(502).json({ error: 'Payment setup failed' })\n }\n // The lead landed above; the confirmation email + workflow event fire\n // from the payment webhook.\n return res\n .status(200)\n .json({ bookingId, startsAtMs, endsAtMs, checkoutUrl: session.url })\n }\n\n // Event trigger (AGL-128/148/159).\n // In-app notification to the site's managers (AGL-259).\n void notifyHostManagers(hostId, {\n type: 'content.booking',\n title: 'New booking',\n body: new Date(startsAtMs).toLocaleString(),\n link: `/${hostId}/bookings`,\n })\n const { alerts } = await emitHostEvent(hostId, 'booking', {\n serviceName: service.name ?? '',\n email,\n startsAtMs,\n })\n\n // Env-gated confirmation email (same provider as AGL-98).\n if (isEmailConfigured()) {\n const timezone = service.timezone || 'UTC'\n const when = new Intl.DateTimeFormat('en-US', {\n timeZone: timezone,\n dateStyle: 'full',\n timeStyle: 'short',\n }).format(new Date(startsAtMs))\n const fallbackText =\n `Hi ${name},\\n\\nYour booking for \"${service.name}\" is ` +\n `confirmed for ${when} (${timezone}).\\n\\n` +\n `Reference: ${bookingId}`\n // Site-owner-designed template when published (AGL-770); null keeps the\n // built-in copy above.\n const designed = await renderHostEmailWithTokens(\n firestore,\n hostId,\n 'booking-confirmed',\n {\n name: String(name ?? ''),\n 'service.name': String(service.name ?? ''),\n when,\n timezone,\n 'booking.ref': String(bookingId),\n },\n )\n // White-label sender identity (White-Label Phase 3): the store's brand\n // via the one shared resolver, from the owning org doc.\n const branding = resolveBrandingProfile(\n (await getOrgForHost(hostId).catch(() => null))?.org as never,\n )\n await sendEmail({\n to: email,\n subject: designed?.subject ?? `Booking confirmed: ${service.name}`,\n text: designed?.text || fallbackText,\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: branding.fromName,\n sendingIdentity: await hostSendingIdentity(hostId),\n audience: 'tenant',\n context: 'booking confirmation',\n // Owed to the recipient by their own booking: the phishing\n // screen's soft rules never hold it (AGL-3356).\n owedFor: 'booking',\n })\n // Cost meter (AGL-1438). Transactional: counted, never capped — a\n // confirmation the customer never receives reads to them as a booking\n // that did not happen.\n await meterHostEmail(hostId)\n }\n\n // THE RECORD'S MEETING (AGL-2660). A free booking is confirmed right\n // here, so this is where it reaches the CRM: the meeting on the\n // record's timeline and, when the service asks, the follow-up task. A\n // paid booking waits for the webhook, which files it once the charge\n // has cleared. Awaited, because a route handler is frozen the moment\n // it answers (AGL-2327) — and never thrown: the slot is the guest's\n // whatever the CRM did.\n await fileBookingOnCrm(firestore, {\n hostId,\n bookingId,\n booking: {\n serviceId,\n serviceName: service.name ?? '',\n email,\n startsAtMs,\n endsAtMs,\n ...(crmRef ? { crmRef: formatCrmBookingRef(crmRef) } : {}),\n },\n service,\n })\n\n return res.status(200).json({ bookingId, startsAtMs, endsAtMs, alerts })\n } catch (error: unknown) {\n if ((error as { code?: number })?.code === 409) {\n return res\n .status(409)\n .json({ error: 'That time was just taken — pick another slot' })\n }\n console.error(error)\n return res.status(500).json({ error: 'Booking failed' })\n }\n}\n\n/**\n * One reminder pass (AGL-160), extracted from its HTTP door in AGL-2431.\n *\n * Finds confirmed bookings starting 23–25 hours out (collection-group query,\n * so one call covers every host), emails each visitor through the env-gated\n * Resend path, and stamps `reminderSentAt` so re-runs are idempotent.\n *\n * WHY THIS IS A FUNCTION NOW. It only ever existed as `POST bookings/reminders`\n * behind `x-cron-secret`, and nothing anywhere POSTed to it — not\n * `scheduled-crons.yml`, not `vercel.json` (which has no `crons` key), not\n * `registerPluginJob`. The route's own docblock said \"invoke hourly from the\n * scheduler\" for as long as it has existed. It never ran, for anyone. Same\n * shape as the two commerce beats in AGL-2227, one plugin over.\n */\nexport async function scanBookingReminders(\n /** The lockdown gate, injected by the caller (AGL-2495). Not optional. */\n gate: PluginJobHostGate,\n): Promise<{\n scanned: number\n sent: number\n skipped: number\n skippedLocked: number\n}> {\n const firestore = firebaseAdmin.app().firestore()\n // ONE instant for the query bounds and for the per-booking predicate. Two\n // `Date.now()` calls would put a booking sitting on an edge inside the\n // query and outside the test, so the pass would fetch it, skip it, and\n // report a `skipped` the console could not explain.\n const nowMs = Date.now()\n const windowStart = nowMs + REMINDER_WINDOW_START_HOURS * 60 * 60 * 1000\n const windowEnd = nowMs + REMINDER_WINDOW_END_HOURS * 60 * 60 * 1000\n const upcoming = await firestore\n .collectionGroup('bookings')\n .where('startsAtMs', '>=', windowStart)\n .where('startsAtMs', '<=', windowEnd)\n .limit(500)\n .get()\n\n let sent = 0\n let skipped = 0\n let skippedLocked = 0\n // Resolve each host's designed reminder template once per run (AGL-770),\n // not once per booking — a busy site has many bookings in the window.\n const templateCache = new Map<string, LoadedHostEmail | null>()\n // White-label brand per host (White-Label Phase 3): resolved once per host\n // from the owning org doc through the one shared resolver, so a reminder\n // reads as the store's brand.\n const brandingByHost = new Map<\n string,\n ReturnType<typeof resolveBrandingProfile>\n >()\n /*\n * One identity resolution per SITE, not per booking.\n *\n * The same reason the branding and template maps above exist: this sweep\n * runs across every site, and an uncached resolution would be two Firestore\n * reads per reminder rather than per host. Scoped to the run, so a domain\n * un-verified between runs is picked up on the next one.\n */\n const identityByHost = new Map<\n string,\n Awaited<ReturnType<typeof hostSendingIdentity>>\n >()\n for (const doc of upcoming.docs) {\n const data = doc.data()\n // The shared predicate, not a local copy of it (AGL-2431): the console\n // card counts what is waiting with this same function, so the number a\n // merchant reads is the number this loop will act on.\n if (!isBookingReminderDue(data as never, nowMs)) {\n skipped += 1\n continue\n }\n const when = new Date(Number(data['startsAtMs'])).toLocaleString(\n 'en-US',\n { dateStyle: 'full', timeStyle: 'short' },\n )\n // bookings live at hosts/{hostId}/bookings/{id}, so the grandparent is\n // the host.\n const hostId = doc.ref.parent.parent?.id ?? ''\n // LOCKDOWN (AGL-2495), ahead of the send, the meter and the\n // `reminderSentAt` stamp — the three things this loop does, all of them\n // for the host. Counted separately from `skipped` on purpose: `skipped`\n // means \"this booking was not due\", and folding a locked site into it\n // would make a suspended merchant's silence read as an ordinary quiet\n // hour on the console card that shows these numbers.\n //\n // SKIPPED, NOT DROPPED: `reminderSentAt` is what retires a booking from\n // this pass, and it is stamped only on a successful send. An unstamped\n // booking is re-scanned hourly — though unlike the other five, this one\n // has a real deadline: the 23–25h window closes. A reminder whose window\n // passed under a lock is genuinely lost, and that is the correct\n // outcome, not a defect to engineer around. A site under takedown should\n // not be mailing its customers, and a reminder that arrives after the\n // appointment is worse than none.\n if (await gate.isLocked(hostId)) {\n skippedLocked += 1\n continue\n }\n let loaded = templateCache.get(hostId)\n if (loaded === undefined) {\n loaded = hostId\n ? await loadHostEmail(firestore, hostId, 'booking-reminder', {\n // The site's header and footer blocks, grafted once per site\n // for the whole batch (AGL-3287).\n compose: composeHostComponentNodes,\n })\n : null\n templateCache.set(hostId, loaded)\n brandingByHost.set(\n hostId,\n resolveBrandingProfile(\n (hostId\n ? await getOrgForHost(hostId).catch(() => null)\n : null\n )?.org as never,\n ),\n )\n }\n const serviceName = String(data['serviceName'] ?? 'your booking')\n const designed = loaded\n ? renderLoadedHostEmail(\n loaded,\n {\n name: String(data['name'] ?? ''),\n 'service.name': serviceName,\n when,\n },\n sanitizeAuthorHtml,\n )\n : null\n const result = await sendEmail({\n to: String(data['email']),\n subject: designed?.subject ?? `Reminder: ${serviceName} tomorrow`,\n text:\n designed?.text ||\n `Hi ${data['name'] ?? ''},\\n\\nA reminder that \"${serviceName}\" is ` +\n `scheduled for ${when}.\\n\\nReference: ${doc.id}`,\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: brandingByHost.get(hostId)?.fromName,\n sendingIdentity: await hostSendingIdentity(hostId, identityByHost),\n audience: 'tenant',\n context: 'booking reminder',\n // Owed to the recipient by their own booking: the phishing\n // screen's soft rules never hold it (AGL-3356).\n owedFor: 'booking',\n })\n if (result.sent) {\n // Cost meter (AGL-1438). Transactional: a reminder a quota refused is\n // a missed appointment for the site's own customer.\n await meterHostEmail(hostId)\n sent += 1\n await doc.ref\n .set(\n {\n reminderSentAt:\n firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n .catch(() => undefined)\n }\n }\n return { scanned: upcoming.size, sent, skipped, skippedLocked }\n}\n\n/**\n * The reminder beat (AGL-2431).\n *\n * MODULE SCOPE, like `expire-stale-holds` above and the three commerce beats:\n * the runner route reaches jobs through `ensureAll(['tenantApi'])`, so a\n * registration inside a `register*` function would depend on which entry\n * point happened to be loaded — the AGL-2227 lesson.\n *\n * HOURLY, matching the window's own resolution. The scan mails a two-hour\n * band 23–25h out, so a beat faster than the band buys nothing; a beat slower\n * than 2h could step over the band entirely and skip a day's reminders. An\n * hour sits inside that with room, and the pass is bounded (500 docs) and\n * idempotent — `reminderSentAt` is stamped on send — so an overlapping or\n * repeated beat cannot double-mail anybody.\n */\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'booking-reminders',\n intervalMinutes: 60,\n description: 'Email a 24-hour reminder for each upcoming confirmed booking.',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n // Quietly, not as an error: email is optional per deployment, and a beat\n // that logs hourly on a self-host without Resend buries everything else.\n if (!isEmailConfigured()) return\n const { sent } = await scanBookingReminders(gate)\n if (sent) console.info(`bookings: sent ${sent} booking reminders`)\n },\n})\n\n/**\n * The manual door, kept for ops (AGL-2431): the scheduling now lives in the\n * `booking-reminders` job above, and this stays so a pass can be forced\n * without waiting for the beat.\n */\nconst remindersHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const cronSecret = process.env.CRON_SECRET\n if (!cronSecret) {\n return res\n .status(501)\n .json({ error: 'Reminders are not configured (CRON_SECRET).' })\n }\n if (req.headers['x-cron-secret'] !== cronSecret) {\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n if (!isEmailConfigured()) {\n return res.status(501).json({\n error: 'Reminders are not configured (RESEND_API_KEY, USAGE_EMAIL_FROM).',\n })\n }\n try {\n // The manual door asks the same question the beat does (AGL-2495).\n return res.status(200).json(await scanBookingReminders(siteLockdownJobGate()))\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Reminders failed' })\n }\n}\n\n/** Registers the bookings plugin's public (site-facing) API routes (AGL-396). */\nexport function registerBookingsApi(): void {\n registerPluginApiRoute('bookings/slots', slotsHandler)\n registerPluginApiRoute('bookings/book', bookHandler)\n}\n\n/** Registers the bookings plugin's console-side API routes (AGL-396). */\nexport function registerBookingsConsoleApi(): void {\n registerPluginApiRoute('bookings/reminders', remindersHandler)\n // Refunding a paid booking (AGL-2315). Console-side, because it is\n // site-admin-gated and moves money — never a site-facing route.\n registerPluginApiRoute('bookings/refund', bookingRefundHandler)\n // The merchant-side `purchase` lookup (AGL-2481). A public, unauthenticated\n // read like `bookings/slots`, authorised by the unguessable Stripe session\n // id and answering with a projection that carries no guest identity.\n registerPluginApiRoute('bookings/booking-analytics', bookingAnalyticsHandler)\n // Paid-booking confirmation rides the platform Stripe webhook (AGL-418).\n registerBillingWebhookHandler(bookingsBillingWebhookHandler)\n // Bookings by service as a figure table (AGL-2915), for the AI plugin's\n // insights to read by id; the console runs insight jobs.\n registerBookingFigureReader(() => firebaseAdmin.app().firestore())\n}\n"],"names":["checkEntitlement","registerPluginConfigSchema","registerPluginJob","resolveBrandingProfile","resolveTransactionFeeCents","sanitizeAuthorHtml","siteLockdownJobGate","BOOKING_MAX_DAYS_AHEAD","computeOpenSlots","isBookingReminderDue","isSlotOpen","REMINDER_WINDOW_END_HOURS","REMINDER_WINDOW_START_HOURS","registerBillingWebhookHandler","registerPluginApiRoute","pluginTaxProfile","BUNDLE_ID","BOOKINGS_CONFIG_SCHEMA","bookingsBillingWebhookHandler","bookingAnalyticsHandler","registerBookingFigureReader","bookingRefundHandler","formatCrmBookingRef","parseCrmBookingRef","fileBookingOnCrm","pluginId","name","intervalMinutes","description","lockdown","scope","handler","gate","cutoff","Date","now","snapshot","firebaseAdmin","app","firestore","collectionGroup","where","limit","get","lapsed","doc","docs","hostId","ref","parent","id","isLocked","set","status","merge","console","info","getOrgForHost","meterHostEmail","notifyHostManagers","attributeCampaignConversion","resolveCampaignTouch","getPluginConfig","resolveOrgIdForHost","renderHostEmailWithTokens","hostSendingIdentity","connectLinkageIsReady","checkoutSessionCardAuthenticationParams","emitHostEvent","recordCapturedContact","isEmailConfigured","loadHostEmail","renderLoadedHostEmail","sendEmail","FieldValue","NO_CLIENT_ADDRESS_BUCKET","readClientIp","composeHostComponentNodes","BOOKING_EMAIL_PATTERN","recentBookingIpHits","Map","BOOKING_RATE_WINDOW_MS","BOOKING_RATE_MAX","bookingRateLimited","ip","hits","filter","t","push","length","slotsHandler","req","res","method","json","error","String","query","serviceId","config","service","hostRef","collection","services","map","$id","durationMinutes","Number","priceUsd","serviceSnapshot","data","fromMs","maxDaysAhead","toMs","bookedSnapshot","booked","startsAtMs","endsAtMs","slots","timezone","bookHandler","body","trim","slice","email","toLowerCase","marketingConsent","crmRef","isFinite","test","headers","remoteAddress","socket","hostSnapshot","Promise","all","exists","ownerOrg","org","durationMs","Math","max","round","bookingsRef","paid","process","env","STRIPE_SECRET_KEY","chargeAccountId","feeCents","serviceTax","taxCents","label","pct","storeSnapshot","ownerUid","ownerProfile","resolve","accountId","chargesEnabled","accountLivemode","subject","taxSettings","tax","chargeCents","flatTax","bookingId","runTransaction","transaction","overlapping","Object","assign","Error","code","bookingRef","serviceName","expiresAtMs","createdAt","serverTimestamp","bookedAtMs","campaignTouch","wire","atMs","kind","refId","touch","convertedAtMs","orgId","identity","undefined","interaction","source","summary","surface","detail","origin","host","params","URLSearchParams","mode","success_url","cancel_url","customer_email","expires_at","floor","stripeResponse","fetch","Authorization","toString","session","ok","url","catch","checkoutUrl","type","title","toLocaleString","link","alerts","when","Intl","DateTimeFormat","timeZone","dateStyle","timeStyle","format","fallbackText","designed","branding","to","text","html","fromName","sendingIdentity","audience","context","owedFor","booking","scanBookingReminders","nowMs","windowStart","windowEnd","upcoming","sent","skipped","skippedLocked","templateCache","brandingByHost","identityByHost","loaded","compose","result","reminderSentAt","scanned","size","remindersHandler","cronSecret","CRON_SECRET","registerBookingsApi","registerBookingsConsoleApi"],"mappings":"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"}