@aglyn/plugins-bookings 1.0.0-beta.231 → 1.0.0-beta.233
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/package.json +11 -11
- package/src/lib/components/bookings-console-page.js +41 -96
- package/src/lib/components/bookings-console-page.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +3 -0
- package/src/lib/declarations.console-server.js +20 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/model/booking-events.d.ts +52 -0
- package/src/lib/model/booking-events.js +53 -0
- package/src/lib/model/booking-events.js.map +1 -0
- package/src/lib/model/booking-manage.d.ts +7 -0
- package/src/lib/model/booking-manage.js +14 -0
- package/src/lib/model/booking-manage.js.map +1 -1
- package/src/lib/model/booking-service-form.d.ts +104 -0
- package/src/lib/model/booking-service-form.js +131 -0
- package/src/lib/model/booking-service-form.js.map +1 -0
- package/src/lib/model/booking-view.d.ts +63 -0
- package/src/lib/model/booking-view.js +70 -0
- package/src/lib/model/booking-view.js.map +1 -0
- package/src/lib/server/api-v1/bookings.d.ts +33 -0
- package/src/lib/server/api-v1/bookings.js +103 -0
- package/src/lib/server/api-v1/bookings.js.map +1 -0
- package/src/lib/server/api-v1/openapi.d.ts +22 -0
- package/src/lib/server/api-v1/openapi.js +107 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -0
- package/src/lib/server/billing-webhook.js +17 -0
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/booking-events.d.ts +40 -0
- package/src/lib/server/booking-events.js +50 -0
- package/src/lib/server/booking-events.js.map +1 -0
- package/src/lib/server/booking-manage.d.ts +16 -0
- package/src/lib/server/booking-manage.js +76 -2
- package/src/lib/server/booking-manage.js.map +1 -1
- package/src/lib/server/refund.js +17 -1
- package/src/lib/server/refund.js.map +1 -1
- package/src/lib/server.js +22 -3
- package/src/lib/server.js.map +1 -1
package/src/lib/server.js.map
CHANGED
|
@@ -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 {\n type SiteReturnHost,\n siteReturnOrigin,\n} from '@aglyn/aglyn/app-utils/site-return-url'\nimport { checkEntitlement,\n type PluginJobHostGate,\n registerPluginConfigSchema,\n registerPluginJob,\n resolveBrandingProfile,\n resolveTransactionFeeCents,\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 {\n loadHostEmailWithTokens,\n renderLoadedHostEmailWithTokens,\n type LoadedHostEmailWithTokens,\n} from '@aglyn/tenant-data-admin/server/host-email-tokens'\nimport { type BookedInterval, BOOKING_MAX_DAYS_AHEAD, bookingChargeUsd, bookingContactAsks, bookingPriceDisplay, bookingContactLines, readBookingContactFields, bookingServiceIsOffered, bookingTimeZone, computeOpenSlotPage, formatBookingWhen, type HostBookingService, isBookingReminderDue, isSlotOpen, REMINDER_WINDOW_END_HOURS, REMINDER_WINDOW_START_HOURS } from './model'\nimport { bookingTimeZoneFor } from './server/booking-time-zone'\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 { registerBookingServiceAiCapability } from './server/booking-service-ai-capability'\nimport { registerBookingServiceDraftWriter } from './server/booking-service-drafts'\nimport { bookingRefundHandler } from './server/refund'\nimport { bookingInPersonPaymentHandler } from './server/in-person-payment'\nimport {\n bookingManageDeps,\n createBookingCheckInHandler,\n createBookingRescheduleHandler,\n} from './server/booking-manage'\n// The booking's way back to the record it was booked from (AGL-2660): the\n// reference a booking link carried, and the meeting a free booking files on\n// landing.\nimport {\n formatBookingRecordRef,\n parseBookingRecordRef,\n} from './model/booking-record'\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 getPluginConfig,\n resolveOrgIdForHost,\n renderHostEmailWithTokens,\n hostSendingIdentity,\n} from '@aglyn/tenant-data-admin'\nimport { merchantAccountIsReady } from '@aglyn/tenant-data-admin/server/payment-provider'\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 CONVERSION_TOUCH_DETAIL,\n creditConversion,\n resolveConversionTouch,\n} from '@aglyn/aglyn/plugin-manager/plugin-conversion-credit'\nimport {\n isEmailConfigured,\n sendEmail,\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'\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 // A draft service is offered nowhere until it is activated (AGL-3616).\n services: services.docs\n .filter((doc) => bookingServiceIsOffered(doc.data()))\n .map((doc) => ({\n $id: doc.id,\n name: doc.get('name') ?? '',\n durationMinutes: Number(doc.get('durationMinutes') ?? 30),\n // What booking it charges, never a price a label stands in for\n // (AGL-3475): a labeled service shows its label and charges 0.\n priceUsd: bookingChargeUsd(doc.data()),\n priceDisplay: bookingPriceDisplay(doc.get('priceDisplay')),\n description: doc.get('description') ?? '',\n // What the widget asks for beyond a name and an email (AGL-3493),\n // as the booking route will hold it: the widget renders and\n // checks the fields from these, the route checks them again.\n askPhone: bookingContactAsks(doc.data()).phone,\n askAddress: bookingContactAsks(doc.data()).address,\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 // A draft has no slots to show (AGL-3616): it reads as no service at all.\n if (!service || !bookingServiceIsOffered(service)) {\n return res.status(404).json({ error: 'Unknown service' })\n }\n const nowMs = 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 // Measured from NOW whichever page is asked for: the horizon is how far\n // ahead a visitor may book, not how far past the page they are on.\n const toMs = nowMs + maxDaysAhead * 24 * 60 * 60_000\n // The page asked for (AGL-3492): `from` is the `nextFromMs` the previous\n // page answered with. Clamped to now, so a stale or hand-written value\n // can never list a time that has passed, and a value past the horizon\n // lists nothing rather than reaching beyond it.\n const askedFromMs = Number(req.query['from'] ?? 0)\n const fromMs = Number.isFinite(askedFromMs)\n ? Math.min(Math.max(askedFromMs, nowMs), toMs)\n : nowMs\n // Bounded at BOTH ends, to the window the slots are actually computed\n // over. `computeOpenSlotPage` 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 // One page never walks further than BOOKING_MAX_DAYS_AHEAD past its\n // start, so a horizon set further out than that is read a page's\n // worth at a time, not all at once.\n .where(\n 'startsAtMs',\n '<=',\n Math.min(toMs, fromMs + BOOKING_MAX_DAYS_AHEAD * 24 * 60 * 60_000),\n )\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 // Bounded in DAYS, not slots (AGL-3492): a page is the widget's day\n // strip, whole, with every open time each of those days has. The flat\n // cap of 120 it replaces ran out three and a half weekdays in at\n // 15-minute steps, so the widget showed a calendar that ended on\n // Thursday at lunch and a horizon that was never reachable.\n const { slots, nextFromMs } = computeOpenSlotPage(\n service,\n fromMs,\n toMs,\n booked,\n )\n return res.status(200).json({\n service: {\n name: service.name,\n durationMinutes: service.durationMinutes,\n priceUsd: bookingChargeUsd(service),\n priceDisplay: bookingPriceDisplay(service.priceDisplay),\n timezone: service.timezone ?? 'UTC',\n },\n slots,\n // Where the next page starts; `null` once this one reached the horizon.\n nextFromMs,\n // The configured horizon, so the widget's empty state names it.\n horizonDays: maxDaysAhead,\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 record the booking link named (AGL-2660), kept on the row so the\n // booking can be attributed to it even when the visitor books with a\n // different address. Parsed, never stored raw: this is a public door.\n const crmRef = parseBookingRecordRef(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 // Refused for a draft as for a deleted service (AGL-3616): a request can\n // name a service id the directory never listed.\n if (!service || !bookingServiceIsOffered(service)) {\n return res.status(404).json({ error: 'Unknown service' })\n }\n // The phone and the address, held to what THIS service asks for\n // (AGL-3493) and checked here whatever the widget already checked: this\n // is a public door, and a request can be written by hand. A field the\n // service does not ask for is dropped rather than stored.\n const contact = readBookingContactFields(bookingContactAsks(service), {\n phone: req.body?.phone,\n address: req.body?.address,\n })\n if ('error' in contact) return res.status(400).json({ error: contact.error })\n const { phone, address } = contact.fields\n // `Phone: …`, `Address: …`, for the managers' notification below.\n const contactLines = bookingContactLines(contact.fields)\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 // The zone this booking's time is told in (AGL-3432), stored on the\n // booking so the paid confirmation and the reminder — which read the\n // booking and not the service — state the same wall-clock time as the\n // free confirmation below, and name the zone it is in.\n const timezone = bookingTimeZone({\n service,\n host: hostSnapshot.data?.() as { timeZone?: string } | undefined,\n org: ownerOrg as { timeZone?: string } | null | undefined,\n })\n const when = formatBookingWhen(startsAtMs, timezone)\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 // A service that states its price as a label books with no charge\n // (AGL-3475), whatever price it stores.\n const priceUsd = bookingChargeUsd(service)\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, serviceRate] = 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 — asked of\n // the plugin that keeps the merchant's tax settings, which reads them\n // where it stores them.\n pluginTaxProfile().flatRate(hostId, 'service'),\n ])\n chargeAccountId = String(ownerProfile?.get('stripeAccountId') ?? '')\n if (\n !merchantAccountIsReady(\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 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 serviceRate,\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 ...(phone ? { phone } : {}),\n ...(address ? { address } : {}),\n startsAtMs,\n endsAtMs,\n timezone,\n status: paid ? 'pendingPayment' : 'confirmed',\n ...(paid && { expiresAtMs: Date.now() + 15 * 60_000 }),\n ...(crmRef ? { crmRef: formatBookingRecordRef(crmRef) } : {}),\n createdAt: FieldValue.serverTimestamp(),\n })\n return bookingRef.id\n })\n\n /*\n * WHERE THE VISITOR ARRIVED FROM, 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. The plugin that credits outcomes\n * (`plugin-conversion-credit`) is asked with what their device carried\n * and the address they just typed, and decides what earns the credit.\n *\n * Resolved once and handed to the booking record, the contact and the\n * lead below, so the lookup is paid once for the whole request and the\n * three records cannot be credited to different arrivals.\n */\n const bookedAtMs = Date.now()\n const arrival = await resolveConversionTouch({\n hostId,\n wire: req.body?.campaignTouch,\n email,\n atMs: bookedAtMs,\n })\n void creditConversion({\n hostId,\n kind: 'booking',\n refId: bookingId,\n touch: arrival,\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 // The phone the booker gave (AGL-3493), onto the person only where\n // the record holds none: a number somebody typed on the record, or\n // gave the business before, is not replaced by one typed into a\n // booking form. The address is the JOB's, not the person's, and\n // rides the meeting below instead.\n ...(phone ? { profileFill: { phone } } : {}),\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 ...(arrival ? { detail: { [CONVERSION_TOUCH_DETAIL]: arrival } } : {}),\n })\n\n if (paid) {\n // The site's own origin, never a header's say-so (AGL-3363): a\n // caller writes `Origin` and `Host`, and Stripe sends the payer here.\n const origin = siteReturnOrigin({\n candidates: [\n typeof req.headers.origin === 'string' ? req.headers.origin : '',\n `https://${req.headers.host}`,\n ],\n site: hostSnapshot.data?.() as SiteReturnHost | undefined,\n requestHost: String(req.headers.host ?? ''),\n })\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 and the managers'\n // notification go out 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): who booked what,\n // and when, in the booking's own zone and naming it (AGL-3432), and the\n // phone and address the service asked for (AGL-3493) — the two things\n // an on-site service needs before it can do anything with the booking.\n void notifyHostManagers(hostId, {\n type: 'content.booking',\n title: 'New booking on {site}',\n body:\n `${name} (${email}) booked ${String(service.name ?? 'a service')} ` +\n `on {site} for ${when} (${timezone}).` +\n (contactLines.length ? ` ${contactLines.join('. ')}.` : ''),\n link: `/${hostId}/bookings`,\n })\n const { alerts } = await emitHostEvent(\n hostId,\n 'booking',\n { serviceName: service.name ?? '', email, startsAtMs },\n // The visitor who booked (AGL-3376).\n { actor: { kind: 'visitor', email } },\n )\n\n // Env-gated confirmation email (same provider as AGL-98).\n if (isEmailConfigured()) {\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 // A free booking charged nothing: empty, so the line is left out.\n 'booking.payment': '',\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 ...(phone ? { phone } : {}),\n ...(address ? { address } : {}),\n startsAtMs,\n endsAtMs,\n ...(crmRef ? { crmRef: formatBookingRecordRef(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, LoadedHostEmailWithTokens | 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 // The zone for a booking that predates storing one (AGL-3432), resolved\n // once per service for the whole run.\n const zoneByService = new Map<string, Promise<string>>()\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 // 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 // The booking's own zone, named beside the time (AGL-3432). A formatter\n // with no zone reads in the server's — UTC — and states the wrong hour.\n const timezone = await bookingTimeZoneFor(\n firestore,\n hostId,\n data,\n zoneByService,\n )\n const when = formatBookingWhen(Number(data['startsAtMs']), timezone)\n let loaded = templateCache.get(hostId)\n if (loaded === undefined) {\n loaded = hostId\n ? await // The template, or the built-in copy in the site's header and\n // footer, once per site for the whole batch (AGL-3287, AGL-3370).\n loadHostEmailWithTokens(firestore, hostId, 'booking-reminder')\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 ? renderLoadedHostEmailWithTokens(\n loaded,\n {\n name: String(data['name'] ?? ''),\n 'service.name': serviceName,\n when,\n timezone,\n },\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} (${timezone}).\\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 // A deposit opens a Stripe Checkout Session for whoever books, so the\n // dispatcher holds this door to the card-testing counters (AGL-3363).\n registerPluginApiRoute('bookings/book', bookHandler, { cardPayment: true })\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 // Paying for a booking at the counter, on the native Aglyn POS app's reader\n // (AGL-3618). Staff-gated like a refund, and it moves money the same way.\n registerPluginApiRoute('bookings/in-person-payment', bookingInPersonPaymentHandler)\n // Checking a guest in and moving a booking (AGL-3621), for the site's\n // team in the native Aglyn app: member routes, one transaction each.\n registerPluginApiRoute('bookings/check-in', createBookingCheckInHandler(bookingManageDeps()))\n registerPluginApiRoute('bookings/reschedule', createBookingRescheduleHandler(bookingManageDeps()))\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 // A draft service another plugin asks for by name (AGL-3616) — an AI build\n // setting a site up from a brief. The console runs AI jobs (AGL-3026).\n registerBookingServiceDraftWriter()\n // …and the operation an AI build plans for it, which that writer executes.\n registerBookingServiceAiCapability()\n}\n"],"names":["siteReturnOrigin","checkEntitlement","registerPluginConfigSchema","registerPluginJob","resolveBrandingProfile","resolveTransactionFeeCents","siteLockdownJobGate","loadHostEmailWithTokens","renderLoadedHostEmailWithTokens","BOOKING_MAX_DAYS_AHEAD","bookingChargeUsd","bookingContactAsks","bookingPriceDisplay","bookingContactLines","readBookingContactFields","bookingServiceIsOffered","bookingTimeZone","computeOpenSlotPage","formatBookingWhen","isBookingReminderDue","isSlotOpen","REMINDER_WINDOW_END_HOURS","REMINDER_WINDOW_START_HOURS","bookingTimeZoneFor","registerBillingWebhookHandler","registerPluginApiRoute","pluginTaxProfile","BUNDLE_ID","BOOKINGS_CONFIG_SCHEMA","bookingsBillingWebhookHandler","bookingAnalyticsHandler","registerBookingFigureReader","registerBookingServiceAiCapability","registerBookingServiceDraftWriter","bookingRefundHandler","bookingInPersonPaymentHandler","bookingManageDeps","createBookingCheckInHandler","createBookingRescheduleHandler","formatBookingRecordRef","parseBookingRecordRef","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","getPluginConfig","resolveOrgIdForHost","renderHostEmailWithTokens","hostSendingIdentity","merchantAccountIsReady","checkoutSessionCardAuthenticationParams","emitHostEvent","recordCapturedContact","CONVERSION_TOUCH_DETAIL","creditConversion","resolveConversionTouch","isEmailConfigured","sendEmail","FieldValue","NO_CLIENT_ADDRESS_BUCKET","readClientIp","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","data","map","$id","durationMinutes","Number","priceUsd","priceDisplay","askPhone","phone","askAddress","address","serviceSnapshot","nowMs","maxDaysAhead","toMs","askedFromMs","fromMs","isFinite","Math","min","max","bookedSnapshot","booked","startsAtMs","endsAtMs","slots","nextFromMs","timezone","horizonDays","bookHandler","body","trim","slice","email","toLowerCase","marketingConsent","crmRef","test","headers","remoteAddress","socket","hostSnapshot","Promise","all","exists","contact","fields","contactLines","ownerOrg","org","durationMs","round","bookingsRef","host","when","paid","process","env","STRIPE_SECRET_KEY","chargeAccountId","feeCents","serviceTax","taxCents","label","pct","ownerUid","ownerProfile","serviceRate","resolve","flatRate","accountId","chargesEnabled","accountLivemode","subject","chargeCents","flatTax","bookingId","runTransaction","transaction","overlapping","Object","assign","Error","code","bookingRef","serviceName","expiresAtMs","createdAt","serverTimestamp","bookedAtMs","arrival","wire","campaignTouch","atMs","kind","refId","touch","convertedAtMs","orgId","identity","undefined","interaction","source","summary","surface","profileFill","detail","origin","candidates","site","requestHost","params","URLSearchParams","mode","success_url","cancel_url","customer_email","expires_at","floor","stripeResponse","fetch","Authorization","toString","session","ok","url","catch","checkoutUrl","type","title","join","link","alerts","actor","fallbackText","designed","branding","to","text","html","fromName","sendingIdentity","audience","context","owedFor","booking","scanBookingReminders","windowStart","windowEnd","upcoming","sent","skipped","skippedLocked","templateCache","brandingByHost","identityByHost","zoneByService","loaded","result","reminderSentAt","scanned","size","remindersHandler","cronSecret","CRON_SECRET","registerBookingsApi","cardPayment","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 {\n type SiteReturnHost,\n siteReturnOrigin,\n} from '@aglyn/aglyn/app-utils/site-return-url'\nimport { checkEntitlement,\n type PluginJobHostGate,\n registerPluginConfigSchema,\n registerPluginJob,\n resolveBrandingProfile,\n resolveTransactionFeeCents,\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 {\n loadHostEmailWithTokens,\n renderLoadedHostEmailWithTokens,\n type LoadedHostEmailWithTokens,\n} from '@aglyn/tenant-data-admin/server/host-email-tokens'\nimport { type BookedInterval, BOOKING_MAX_DAYS_AHEAD, bookingChargeUsd, bookingContactAsks, bookingPriceDisplay, bookingContactLines, readBookingContactFields, bookingServiceIsOffered, bookingTimeZone, computeOpenSlotPage, formatBookingWhen, type HostBookingService, isBookingReminderDue, isSlotOpen, REMINDER_WINDOW_END_HOURS, REMINDER_WINDOW_START_HOURS } from './model'\nimport { bookingTimeZoneFor } from './server/booking-time-zone'\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 { registerBookingServiceAiCapability } from './server/booking-service-ai-capability'\nimport { registerBookingServiceDraftWriter } from './server/booking-service-drafts'\nimport { bookingRefundHandler } from './server/refund'\nimport { stageBookingEvent } from './server/booking-events'\nimport { bookingInPersonPaymentHandler } from './server/in-person-payment'\nimport {\n bookingManageDeps,\n createBookingCheckInHandler,\n createBookingCancelHandler,\n createBookingRescheduleHandler,\n} from './server/booking-manage'\n// The booking's way back to the record it was booked from (AGL-2660): the\n// reference a booking link carried, and the meeting a free booking files on\n// landing.\nimport {\n formatBookingRecordRef,\n parseBookingRecordRef,\n} from './model/booking-record'\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 getPluginConfig,\n resolveOrgIdForHost,\n renderHostEmailWithTokens,\n hostSendingIdentity,\n} from '@aglyn/tenant-data-admin'\nimport { merchantAccountIsReady } from '@aglyn/tenant-data-admin/server/payment-provider'\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 CONVERSION_TOUCH_DETAIL,\n creditConversion,\n resolveConversionTouch,\n} from '@aglyn/aglyn/plugin-manager/plugin-conversion-credit'\nimport {\n isEmailConfigured,\n sendEmail,\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'\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 // A draft service is offered nowhere until it is activated (AGL-3616).\n services: services.docs\n .filter((doc) => bookingServiceIsOffered(doc.data()))\n .map((doc) => ({\n $id: doc.id,\n name: doc.get('name') ?? '',\n durationMinutes: Number(doc.get('durationMinutes') ?? 30),\n // What booking it charges, never a price a label stands in for\n // (AGL-3475): a labeled service shows its label and charges 0.\n priceUsd: bookingChargeUsd(doc.data()),\n priceDisplay: bookingPriceDisplay(doc.get('priceDisplay')),\n description: doc.get('description') ?? '',\n // What the widget asks for beyond a name and an email (AGL-3493),\n // as the booking route will hold it: the widget renders and\n // checks the fields from these, the route checks them again.\n askPhone: bookingContactAsks(doc.data()).phone,\n askAddress: bookingContactAsks(doc.data()).address,\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 // A draft has no slots to show (AGL-3616): it reads as no service at all.\n if (!service || !bookingServiceIsOffered(service)) {\n return res.status(404).json({ error: 'Unknown service' })\n }\n const nowMs = 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 // Measured from NOW whichever page is asked for: the horizon is how far\n // ahead a visitor may book, not how far past the page they are on.\n const toMs = nowMs + maxDaysAhead * 24 * 60 * 60_000\n // The page asked for (AGL-3492): `from` is the `nextFromMs` the previous\n // page answered with. Clamped to now, so a stale or hand-written value\n // can never list a time that has passed, and a value past the horizon\n // lists nothing rather than reaching beyond it.\n const askedFromMs = Number(req.query['from'] ?? 0)\n const fromMs = Number.isFinite(askedFromMs)\n ? Math.min(Math.max(askedFromMs, nowMs), toMs)\n : nowMs\n // Bounded at BOTH ends, to the window the slots are actually computed\n // over. `computeOpenSlotPage` 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 // One page never walks further than BOOKING_MAX_DAYS_AHEAD past its\n // start, so a horizon set further out than that is read a page's\n // worth at a time, not all at once.\n .where(\n 'startsAtMs',\n '<=',\n Math.min(toMs, fromMs + BOOKING_MAX_DAYS_AHEAD * 24 * 60 * 60_000),\n )\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 // Bounded in DAYS, not slots (AGL-3492): a page is the widget's day\n // strip, whole, with every open time each of those days has. The flat\n // cap of 120 it replaces ran out three and a half weekdays in at\n // 15-minute steps, so the widget showed a calendar that ended on\n // Thursday at lunch and a horizon that was never reachable.\n const { slots, nextFromMs } = computeOpenSlotPage(\n service,\n fromMs,\n toMs,\n booked,\n )\n return res.status(200).json({\n service: {\n name: service.name,\n durationMinutes: service.durationMinutes,\n priceUsd: bookingChargeUsd(service),\n priceDisplay: bookingPriceDisplay(service.priceDisplay),\n timezone: service.timezone ?? 'UTC',\n },\n slots,\n // Where the next page starts; `null` once this one reached the horizon.\n nextFromMs,\n // The configured horizon, so the widget's empty state names it.\n horizonDays: maxDaysAhead,\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 record the booking link named (AGL-2660), kept on the row so the\n // booking can be attributed to it even when the visitor books with a\n // different address. Parsed, never stored raw: this is a public door.\n const crmRef = parseBookingRecordRef(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 // Refused for a draft as for a deleted service (AGL-3616): a request can\n // name a service id the directory never listed.\n if (!service || !bookingServiceIsOffered(service)) {\n return res.status(404).json({ error: 'Unknown service' })\n }\n // The phone and the address, held to what THIS service asks for\n // (AGL-3493) and checked here whatever the widget already checked: this\n // is a public door, and a request can be written by hand. A field the\n // service does not ask for is dropped rather than stored.\n const contact = readBookingContactFields(bookingContactAsks(service), {\n phone: req.body?.phone,\n address: req.body?.address,\n })\n if ('error' in contact) return res.status(400).json({ error: contact.error })\n const { phone, address } = contact.fields\n // `Phone: …`, `Address: …`, for the managers' notification below.\n const contactLines = bookingContactLines(contact.fields)\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 // The zone this booking's time is told in (AGL-3432), stored on the\n // booking so the paid confirmation and the reminder — which read the\n // booking and not the service — state the same wall-clock time as the\n // free confirmation below, and name the zone it is in.\n const timezone = bookingTimeZone({\n service,\n host: hostSnapshot.data?.() as { timeZone?: string } | undefined,\n org: ownerOrg as { timeZone?: string } | null | undefined,\n })\n const when = formatBookingWhen(startsAtMs, timezone)\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 // A service that states its price as a label books with no charge\n // (AGL-3475), whatever price it stores.\n const priceUsd = bookingChargeUsd(service)\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, serviceRate] = 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 — asked of\n // the plugin that keeps the merchant's tax settings, which reads them\n // where it stores them.\n pluginTaxProfile().flatRate(hostId, 'service'),\n ])\n chargeAccountId = String(ownerProfile?.get('stripeAccountId') ?? '')\n if (\n !merchantAccountIsReady(\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 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 serviceRate,\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 const record = {\n serviceId,\n serviceName: service.name ?? '',\n name,\n email,\n ...(phone ? { phone } : {}),\n ...(address ? { address } : {}),\n startsAtMs,\n endsAtMs,\n timezone,\n status: paid ? 'pendingPayment' : 'confirmed',\n ...(paid && { expiresAtMs: Date.now() + 15 * 60_000 }),\n ...(crmRef ? { crmRef: formatBookingRecordRef(crmRef) } : {}),\n }\n transaction.set(bookingRef, { ...record, createdAt: FieldValue.serverTimestamp() })\n // A free booking is confirmed as it is made (AGL-3643); a paid one\n // raises `booking.created` from the payment webhook instead.\n if (!paid) {\n const nowMs = Date.now()\n stageBookingEvent(transaction, firestore, {\n event: 'booking.created',\n hostId,\n bookingId: bookingRef.id,\n booking: { ...record, createdAt: nowMs },\n nowMs,\n })\n }\n return bookingRef.id\n })\n\n /*\n * WHERE THE VISITOR ARRIVED FROM, 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. The plugin that credits outcomes\n * (`plugin-conversion-credit`) is asked with what their device carried\n * and the address they just typed, and decides what earns the credit.\n *\n * Resolved once and handed to the booking record, the contact and the\n * lead below, so the lookup is paid once for the whole request and the\n * three records cannot be credited to different arrivals.\n */\n const bookedAtMs = Date.now()\n const arrival = await resolveConversionTouch({\n hostId,\n wire: req.body?.campaignTouch,\n email,\n atMs: bookedAtMs,\n })\n void creditConversion({\n hostId,\n kind: 'booking',\n refId: bookingId,\n touch: arrival,\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 // The phone the booker gave (AGL-3493), onto the person only where\n // the record holds none: a number somebody typed on the record, or\n // gave the business before, is not replaced by one typed into a\n // booking form. The address is the JOB's, not the person's, and\n // rides the meeting below instead.\n ...(phone ? { profileFill: { phone } } : {}),\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 ...(arrival ? { detail: { [CONVERSION_TOUCH_DETAIL]: arrival } } : {}),\n })\n\n if (paid) {\n // The site's own origin, never a header's say-so (AGL-3363): a\n // caller writes `Origin` and `Host`, and Stripe sends the payer here.\n const origin = siteReturnOrigin({\n candidates: [\n typeof req.headers.origin === 'string' ? req.headers.origin : '',\n `https://${req.headers.host}`,\n ],\n site: hostSnapshot.data?.() as SiteReturnHost | undefined,\n requestHost: String(req.headers.host ?? ''),\n })\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 and the managers'\n // notification go out 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): who booked what,\n // and when, in the booking's own zone and naming it (AGL-3432), and the\n // phone and address the service asked for (AGL-3493) — the two things\n // an on-site service needs before it can do anything with the booking.\n void notifyHostManagers(hostId, {\n type: 'content.booking',\n title: 'New booking on {site}',\n body:\n `${name} (${email}) booked ${String(service.name ?? 'a service')} ` +\n `on {site} for ${when} (${timezone}).` +\n (contactLines.length ? ` ${contactLines.join('. ')}.` : ''),\n link: `/${hostId}/bookings`,\n })\n const { alerts } = await emitHostEvent(\n hostId,\n 'booking',\n { serviceName: service.name ?? '', email, startsAtMs },\n // The visitor who booked (AGL-3376).\n { actor: { kind: 'visitor', email } },\n )\n\n // Env-gated confirmation email (same provider as AGL-98).\n if (isEmailConfigured()) {\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 // A free booking charged nothing: empty, so the line is left out.\n 'booking.payment': '',\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 ...(phone ? { phone } : {}),\n ...(address ? { address } : {}),\n startsAtMs,\n endsAtMs,\n ...(crmRef ? { crmRef: formatBookingRecordRef(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, LoadedHostEmailWithTokens | 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 // The zone for a booking that predates storing one (AGL-3432), resolved\n // once per service for the whole run.\n const zoneByService = new Map<string, Promise<string>>()\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 // 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 // The booking's own zone, named beside the time (AGL-3432). A formatter\n // with no zone reads in the server's — UTC — and states the wrong hour.\n const timezone = await bookingTimeZoneFor(\n firestore,\n hostId,\n data,\n zoneByService,\n )\n const when = formatBookingWhen(Number(data['startsAtMs']), timezone)\n let loaded = templateCache.get(hostId)\n if (loaded === undefined) {\n loaded = hostId\n ? await // The template, or the built-in copy in the site's header and\n // footer, once per site for the whole batch (AGL-3287, AGL-3370).\n loadHostEmailWithTokens(firestore, hostId, 'booking-reminder')\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 ? renderLoadedHostEmailWithTokens(\n loaded,\n {\n name: String(data['name'] ?? ''),\n 'service.name': serviceName,\n when,\n timezone,\n },\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} (${timezone}).\\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 // A deposit opens a Stripe Checkout Session for whoever books, so the\n // dispatcher holds this door to the card-testing counters (AGL-3363).\n registerPluginApiRoute('bookings/book', bookHandler, { cardPayment: true })\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 // Paying for a booking at the counter, on the native Aglyn POS app's reader\n // (AGL-3618). Staff-gated like a refund, and it moves money the same way.\n registerPluginApiRoute('bookings/in-person-payment', bookingInPersonPaymentHandler)\n // Checking a guest in and moving a booking (AGL-3621), for the site's\n // team in the native Aglyn app: member routes, one transaction each.\n registerPluginApiRoute('bookings/check-in', createBookingCheckInHandler(bookingManageDeps()))\n registerPluginApiRoute('bookings/reschedule', createBookingRescheduleHandler(bookingManageDeps()))\n // Canceling a booking with no money on it (AGL-3643): the console's cancel\n // for a free booking, so the cancel reaches the plugins listening for it.\n registerPluginApiRoute('bookings/cancel', createBookingCancelHandler(bookingManageDeps()))\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 // A draft service another plugin asks for by name (AGL-3616) — an AI build\n // setting a site up from a brief. The console runs AI jobs (AGL-3026).\n registerBookingServiceDraftWriter()\n // …and the operation an AI build plans for it, which that writer executes.\n registerBookingServiceAiCapability()\n}\n"],"names":["siteReturnOrigin","checkEntitlement","registerPluginConfigSchema","registerPluginJob","resolveBrandingProfile","resolveTransactionFeeCents","siteLockdownJobGate","loadHostEmailWithTokens","renderLoadedHostEmailWithTokens","BOOKING_MAX_DAYS_AHEAD","bookingChargeUsd","bookingContactAsks","bookingPriceDisplay","bookingContactLines","readBookingContactFields","bookingServiceIsOffered","bookingTimeZone","computeOpenSlotPage","formatBookingWhen","isBookingReminderDue","isSlotOpen","REMINDER_WINDOW_END_HOURS","REMINDER_WINDOW_START_HOURS","bookingTimeZoneFor","registerBillingWebhookHandler","registerPluginApiRoute","pluginTaxProfile","BUNDLE_ID","BOOKINGS_CONFIG_SCHEMA","bookingsBillingWebhookHandler","bookingAnalyticsHandler","registerBookingFigureReader","registerBookingServiceAiCapability","registerBookingServiceDraftWriter","bookingRefundHandler","stageBookingEvent","bookingInPersonPaymentHandler","bookingManageDeps","createBookingCheckInHandler","createBookingCancelHandler","createBookingRescheduleHandler","formatBookingRecordRef","parseBookingRecordRef","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","getPluginConfig","resolveOrgIdForHost","renderHostEmailWithTokens","hostSendingIdentity","merchantAccountIsReady","checkoutSessionCardAuthenticationParams","emitHostEvent","recordCapturedContact","CONVERSION_TOUCH_DETAIL","creditConversion","resolveConversionTouch","isEmailConfigured","sendEmail","FieldValue","NO_CLIENT_ADDRESS_BUCKET","readClientIp","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","data","map","$id","durationMinutes","Number","priceUsd","priceDisplay","askPhone","phone","askAddress","address","serviceSnapshot","nowMs","maxDaysAhead","toMs","askedFromMs","fromMs","isFinite","Math","min","max","bookedSnapshot","booked","startsAtMs","endsAtMs","slots","nextFromMs","timezone","horizonDays","bookHandler","body","trim","slice","email","toLowerCase","marketingConsent","crmRef","test","headers","remoteAddress","socket","hostSnapshot","Promise","all","exists","contact","fields","contactLines","ownerOrg","org","durationMs","round","bookingsRef","host","when","paid","process","env","STRIPE_SECRET_KEY","chargeAccountId","feeCents","serviceTax","taxCents","label","pct","ownerUid","ownerProfile","serviceRate","resolve","flatRate","accountId","chargesEnabled","accountLivemode","subject","chargeCents","flatTax","bookingId","runTransaction","transaction","overlapping","Object","assign","Error","code","bookingRef","record","serviceName","expiresAtMs","createdAt","serverTimestamp","event","booking","bookedAtMs","arrival","wire","campaignTouch","atMs","kind","refId","touch","convertedAtMs","orgId","identity","undefined","interaction","source","summary","surface","profileFill","detail","origin","candidates","site","requestHost","params","URLSearchParams","mode","success_url","cancel_url","customer_email","expires_at","floor","stripeResponse","fetch","Authorization","toString","session","ok","url","catch","checkoutUrl","type","title","join","link","alerts","actor","fallbackText","designed","branding","to","text","html","fromName","sendingIdentity","audience","context","owedFor","scanBookingReminders","windowStart","windowEnd","upcoming","sent","skipped","skippedLocked","templateCache","brandingByHost","identityByHost","zoneByService","loaded","result","reminderSentAt","scanned","size","remindersHandler","cronSecret","CRON_SECRET","registerBookingsApi","cardPayment","registerBookingsConsoleApi"],"mappings":"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"}
|