Acceptance Spec — Realtor Showing Scheduling: Availability Windows + Slots + Two-Sided Confirm (MVP)
- Status: Proposed (MVP) — 2026-07-03
- Product: WiseAI Realtor System → AI front office (Aria) +
/realtor/app/calendar+/realtor/app/settings/calendar - Priority: NOW (revises the booking step of the just-shipped calendar-write MVP before it's demoed to Team Beckett)
- Related / supersedes-in-part:
realtor-calendar-booking-mvp(this spec replaces its §5 "book a firm event immediately" step; it reuses that spec's connect + free/busy + event-write plumbing),ai-front-desk,data-model.md §1.8 - AI-Bridge:
knowledge/architecture/ai-bridge-principle.md
1. Why (problem)
The shipped calendar-write MVP treats "book a showing" as: create a firm calendar event at a precise time and invite the buyer — immediately. Two founder-caught errors make that wrong for real estate:
- A showing is two-sided. The buyer's agent doesn't control when a home can be shown — the listing agent, seller, and occupant do. Aria must never drop a confirmed showing onto a time nobody on the listing side agreed to.
- A showing is one slot inside a window, not a "busy" block. An agent doing showings "Wednesday evening" can fit ~6 back-to-back showings into 5–8 PM. Treating one showing as blocking the whole window is wrong — availability is capacity within recurring windows, not binary free/busy.
This spec models showing scheduling correctly: recurring agent availability windows → discrete bookable slots (capacity) → a buyer picks a slot → a tentative hold goes on the agent's calendar → the agent confirms with the listing side → only then does it become firm and the buyer is invited.
2. Scope
In: per-agent recurring showing-availability windows (weekday + time range +
slot length, deriving capacity); per-agent day-part definitions (what
morning/afternoon/evening mean) so Aria can map fuzzy buyer availability; a pure
slot-generation function (windows − calendar free/busy − already-booked showings);
a re_showings lifecycle (requested → held → confirmed → completed/cancelled); a
tentative hold on the chosen slot only (not the window); a confirm task +
listing-side contact capture; firm-up + buyer invite on confirm. Aria captures
fuzzy windows ("Wednesday evening") and offers concrete open slots.
Out (fast-follow / deferred): travel-time buffers between showings; per-day caps
beyond per-window; multi-agent round-robin; one-off blackout dates (a simple
vacation block is a nice-to-have, not required); BrokerBay/ShowingTime integration
(the real listing-side coordination system — API-blocked, tracked as a partnership
ask in AUDIT_realtor_integrations_strategy_2026-07-03.md); Calendly adoption;
auto-sending the listing-side outreach (Aria drafts it; a human sends — the
platform never auto-sends on the agent's behalf).
3. Data shape (new re_* tables + reuse)
re_agent_showing_prefs -- 1 row per agent (global scheduling prefs)
agent_id uuid PK FK → local_business_agents(id) ON DELETE CASCADE
business_id uuid NOT NULL FK
timezone text NOT NULL DEFAULT 'America/Toronto'
default_slot_minutes int NOT NULL DEFAULT 30
day_parts jsonb NOT NULL DEFAULT
'{"morning":["08:00","12:00"],"afternoon":["12:00","17:00"],"evening":["17:00","20:00"]}'
-- agent-editable: some agents' "evening" starts at 16:00
re_agent_showing_windows -- many rows per agent (recurring weekly availability)
id uuid PK
business_id uuid NOT NULL FK
agent_id uuid NOT NULL FK → local_business_agents(id) ON DELETE CASCADE
weekday smallint NOT NULL -- 0=Sun … 6=Sat
start_time time NOT NULL -- e.g. 17:00
end_time time NOT NULL -- e.g. 20:00 (capacity = (end-start)/slot_minutes)
slot_minutes int NOT NULL DEFAULT 30 -- overrides prefs.default for this window
max_per_window int NULL -- optional lower cap than the derived capacity
active boolean NOT NULL DEFAULT true
re_showings -- the showing lifecycle (one per showing request)
id uuid PK
business_id uuid NOT NULL FK
agent_id uuid NOT NULL FK → local_business_agents(id)
contact_id uuid NULL FK → re_contacts(id) -- the buyer
side text NOT NULL -- 'buyer_showing' (other brokerage's listing)
-- | 'own_listing' (we control the listing side)
listing_id uuid NULL FK → local_business_listings(id) -- when it's ours
listing_mls text NULL
property_address text NULL
listing_side_contact jsonb NULL -- {name, phone, email} for buyer_showing confirm
stage text NOT NULL DEFAULT 'requested'
-- 'requested' (buyer gave availability, no slot held)
-- 'held' (a specific slot is tentatively held on the agent's calendar)
-- 'confirmed' (listing side agreed → firm event + buyer invited)
-- 'completed' | 'cancelled' | 'declined'
preferred_windows jsonb NOT NULL DEFAULT '[]' -- normalized buyer availability
chosen_start timestamptz NULL -- the held/firm slot start
chosen_end timestamptz NULL
calendar_event_id text NULL -- the hold event (upgraded to firm on confirm)
calendar_provider text NULL -- 'google' | 'microsoft'
buyer_invited boolean NOT NULL DEFAULT false -- ONLY true once confirmed
confirm_task_id uuid NULL FK → re_tasks(id)
created_by text
created_at/updated_at timestamptz
Reuse (do not rebuild): re_calendar_connections + the calendar client
(free/busy read, event create/update/cancel) from the calendar-write MVP. This spec
adds scheduling semantics; the provider plumbing already exists.
4. The core logic (pure + unit-tested)
generateOpenSlots(windows, prefs, freeBusy, bookedShowings, dateRange, buyerWindows?)
— a pure function (mirrors the existing booking-logic.ts pattern):
- Expand each
re_agent_showing_windowsrow acrossdateRangeinto candidate slots ofslot_minutes(capacity = window length / slot length, capped bymax_per_window). - Subtract hard conflicts: any slot overlapping the agent's calendar free/busy (a real meeting) is removed.
- Subtract taken slots: any slot already held/confirmed in
re_showingsis removed — so a 3-hour window keeps offering its remaining slots (multiple showings per window, the founder's requirement). - Fuzzy-window mapping: if the buyer said "Wednesday evening," map via
prefs.day_parts.evening→ a time range, intersect with that weekday's windows, and return only slots inside it. Support multiple buyer windows ("Wed evening or Mon morning"). - Returns concrete offerable slots (e.g. Wed 5:00, 5:30, 6:00…).
5. Flow (states)
- Capture (Aria, chat/voice): buyer expresses interest in a property + their
availability in natural language ("Wednesday evening"). Aria normalizes it into
preferred_windowsand creates are_showingsrowstage='requested'(no hold yet). - Offer: Aria calls
generateOpenSlots(...)scoped to the buyer's windows and offers concrete open slots ("I can do Wednesday at 5:00, 5:45, or 6:30"). If the buyer is vague, the request staysrequestedfor the agent to work. - Hold (buyer picks a slot, or agent picks one): set
chosen_start/end, place a tentative hold on the agent's calendar for that slot only — titled "Showing requested — 45 Southside (pending listing-side confirmation)" with the AI-arranged disclosure — setstage='held',buyer_invited=false. Create a confirm task (re_taskstypeshowing) — "Confirm Wed 5:00 showing of 45 Southside with the listing agent" — and, for abuyer_showing, capture/neededlisting_side_contact. Aria may draft (never auto-send) the listing-side outreach. - Confirm (human): once the listing side agrees, the agent confirms → the hold
event is upgraded to a firm appointment,
stage='confirmed', the buyer is now invited (buyer_invited=true), the confirm task closes. - Cancel / decline / reschedule: free the slot (it returns to the open pool), remove/void the calendar event, notify as appropriate. Fails closed — a failed calendar op never leaves a phantom firm booking.
6. AI-Bridge guardrail
Aria proposes slots and places a tentative hold; she never confirms a cross-party time and never marks a showing firm. The firm-up + buyer invite is a human action (or an explicit agent-approved step). The hold event always discloses it was AI-arranged and is pending confirmation. Aria drafts listing-side outreach but does not send it. This keeps showing coordination — an inherently multi-party commitment — on the human side of the bridge.
7. Owner surface
- Settings → Calendar (or a new "Showing availability" panel): the agent sets weekly windows (weekday + start/end + slot length + optional cap) and their day-part definitions. Simple, editable, defaulted sensibly (e.g. no windows = Aria captures requests but offers no auto-slots until set).
- Calendar / Lead drawer: a showing shows its
stage(Requested / Held / Confirmed) with a one-click Confirm (opens the confirm step) — never an auto-confirm. - All authed client calls carry
?account_id=; window/prefs writes UPSERT (per the account_id-in-query + UPSERT-config regression traps).
8. Acceptance criteria
- Capacity, not blocking: two showings can be held in the same 5–8 PM window; after holding Wed 5:00, the open-slots for that window still include 5:30, 6:00, … (a window is not consumed by one showing).
- Slot generation is pure + tested:
generateOpenSlotshas unit tests proving window expansion,slot_minutescapacity,max_per_windowcap, subtraction of calendar free/busy conflicts, subtraction of already-held slots, and fuzzy-window mapping (incl. an agent whose "evening" = 16:00–20:00, and multiple buyer windows). - Tentative first: picking a slot creates
stage='held', a hold event marked pending with the AI disclosure,buyer_invited=false, and a confirm task — the buyer is not invited yet. - Confirm is human + firms up: confirming sets
stage='confirmed', upgrades the event to firm, and only THEN invites the buyer (buyer_invited=true). - Two-sided honesty: a
buyer_showingrequires alisting_side_contactcapture / confirm step; Aria never auto-confirms it.own_listingmay skip the external contact but still confirms the seller/occupant. - Cancel frees the slot: cancelling a held/confirmed showing returns its slot to the open pool and voids the calendar event; no phantom bookings.
- Bridge: every hold event carries the AI-arranged + pending-confirmation disclosure; no code path lets Aria mark a showing confirmed on her own.
- account_id + UPSERT on all availability/prefs writes.
9. Verification (deployed URL)
- Seed an agent with a Wed 17:00–20:00 window (30-min slots) on the TeamMoelker
showcase account; via the flow, hold Wed 5:00 for one buyer and confirm the window
still offers 5:30+; assert the
re_showingsrow isheld, the calendar event is the tentative title, andbuyer_invited=false. Confirm it → assertconfirmed, firm event,buyer_invited=true. Sample state at ≥2 timepoints (held → confirmed). - Unit:
node --import tsx --teston thegenerateOpenSlotssuite passes. - Negative: a slot overlapping a real calendar event is never offered; a second hold can't take an already-held slot.
This spec revises realtor-calendar-booking-mvp.md §5. Before demoing to Team
Beckett, the shipped co-pilot "Confirm → immediate firm event + invite" behavior
should be gated/updated to the held→confirm flow above so we never claim a
cross-party showing that wasn't agreed to.