0104 — Clara knows a property's floor-plan media (3D tours + drawings) and can send the right one

Context

Camellia now has five Matterport 3D walkthroughs (one per scanned floor plan) and a set of direct, prospect-facing tour pages on the marketing site (camelliadenver.com/tours/<plan>, built on camellia-site PR #12 — held pending CoStar/Apartments.com media-reuse approval). Prospects regularly ask Clara for "pictures", "a video", or "a virtual tour" on every channel, and callers who can't visit in person have no self-serve way to see a unit.

Today Clara cannot help with any of this:

Cross-property check (§5): Yale 25 Station's site has NO 3D tours (its "3D Tour" nav link 404s) but does have public per-plan floor-plan drawings — so the design must degrade per-column, not assume Matterport.

Trello: https://trello.com/c/yxEVcK0B (Matterport integration) and https://trello.com/c/kox84jG0 (stale apartments.com listing, related surface).

Decision

Give every channel one property-level source of truth for tour links, and give Clara one guarded way to resolve "which tour?" so she never types a URL from memory.

1. Storage: PropertyKnowledge.floorPlanMedia (multi-tenant, per floor plan)

Add an optional per-floor-plan media array on PropertyKnowledge:

floorPlanMedia?: {
  label: string;          // PM-facing + prospect-facing name, e.g. "Two Bedroom L"
  bedrooms: number;       // 0 = studio — the matching key
  bathrooms?: number;
  sqft?: number;
  tourUrl?: string;       // self-guided 3D tour — for Camellia, the branded site page
                          //   (https://www.camelliadenver.com/tours/two-bedroom-l),
                          //   NOT the raw my.matterport.com URL
  floorPlanUrl?: string;  // the floor-plan drawing (public image URL) — MMS-able
}[];

One row per floor plan, carrying whichever media exist. "Can you send me the floor plan?" and "can I get a virtual tour?" are the same lookup with a different column.

2. Context: how Clara knows which tour to send

The match key is unit type / floor plan under discussion, which every channel already tracks:

3. Sending: one resolver tool, channel-appropriate delivery

New tool get_floor_plan_media({ bedrooms, media }) (text channels), mediavirtual_tour | floor_plan — returns the exact URL + label from PropertyKnowledge, which Clara includes inline in her reply (same inline pattern as the PROPERTY INFO application-link bullet — D1-A, 2026-08-19, clara-unified.ts). Tool-resolved URLs mean Clara never reconstructs a link from memory — a hallucinated URL to a prospect is worse than no link.

New voice tool send_floor_plan_media({ bedrooms, media }) — covers both "text me the virtual tour" and "can you send me the floor plan?":

  1. Resolves the caller's phone from the live call context (resolveVoiceWebhookContext, the same plumbing the tools route already uses).
  2. Resolves the URL (or floor-plan image) from PropertyKnowledge.floorPlanMedia.
  3. Sends ONE message through the standard consent-gated path — an SMS with the tour link, or an MMS with the floor-plan drawing attached (sendMms) for media: floor_plan (SMS-with-link fallback if MMS fails) — then returns success/failure so Clara can confirm aloud ("Just texted it to you") or gracefully offer email instead.

Delivery timing: the send fires immediately mid-call when allowed, so the caller has it before hanging up. When it can't fire mid-call (quiet hours, transient carrier failure), it queues for post-call delivery via the existing call-ended hook — the one place that already texts the live caller's phone (call-ended/route.ts recap SMS) — or for the next quiet-hours window, and Clara sets that expectation aloud.

Message body is fixed-template and strictly informational: the plan label + the link (or image). No specials, no promotional copy — promo content moves the message to the prior-express-written-consent tier (TCPA KB, researched 2026-06-18).

Guardrails on the send:

4. Explicitly out of scope

5. Cross-property validation: Yale 25 Station (second live data point)

Checked 2026-07-20 against our second prod property (PROP#1773625952029, yale25stationapartments.com, Apartments247 platform):

mediaPages?: {
  toursIndexUrl?: string;   // e.g. camelliadenver.com/tours
  unitMapUrl?: string;      // e.g. SightMap embed/page
  galleryUrl?: string;      // photo gallery page
};

Gaps this exposes in today's config/knowledge base (why this ADR adds fields rather than reusing something):

  1. PropertyKnowledge has no per-floor-plan structure of ANY kind (Yale's live row: amenities/sections/pricing/neighborhood only) — floor plan names, sqft, and images exist nowhere in PropFlow, only on each property's website.
  2. tourUrl is NULL on every checked property and unreachable by prompts/UI (orphaned).
  3. The website knowledge scraper (scrapers/shared.ts) doesn't collect floor-plan or media data, so floorPlanMedia starts manual-entry-only. Follow-up (phase 2 of implementation): teach the scraper to propose floorPlanMedia rows from the property site (plan name / sqft / image URL are machine-readable on both platforms we checked — camellia-site and Apartments247). Scraper proposes, PM confirms in the knowledge UI; the manual path ships first and is the override.

Consequences

Alternatives considered