Skip to main content

Realtor — "Show conditional listings as For sale"

Why​

Team Beckett (tenant 00000000-0000-4000-a000-000000000c01, slug terry-and-sheri, teambeckett.ca) asked that a listing with an accepted conditional offer show "For sale" instead of "Conditional" on their public site — "Conditional" was scaring buyers off. Built as a self-serve dashboard toggle for ALL realtor customers, default OFF (today's behaviour unchanged), turned ON for Team Beckett only after founder approval.

What must stay true no matter the setting​

  • The STORED listing status never changes. mapResoStatus() (src/lib/real-estate/ddf/map.ts) keeps mapping the PropTx/DDF feed's "Active Under Contract" / "Sold Conditional" / "Leased Conditional" values to 'conditional' — the 2026-09-11 P0 fix this depends on is unaffected.
  • Full-board search still includes conditional listings (server/listing-search.ts).
  • The listing DETAIL page (/s/[slug]/listing/[mls], ListingDetailView.tsx) is unaffected by this setting — out of scope for v1 (only the card badge in ui/ListingCard.tsx:statusBadge() reads the flag).
  • AI chat + voice (founder decision 2026-09-25, John: "it should say for sale... and have the agent followup with the info that there is an offer"):
    • Flag OFF → unchanged ("Sale pending (conditional)" in chat, "sale-conditional" on voice).
    • Flag ON → chat (chat-knowledge.ts statusLabel(), via /api/chatbot/demo) and the real-estate voice agent (voice-agent-livekit/verticals/real_estate/tools.py format_listing_facts()) present conditional listings as "For sale"/"For lease" and do not volunteer offer details.
    • HONESTY FLOOR (AI Bridge Principle): if a visitor/caller asks directly about offers, conditions, or whether the home is really available, the assistant NEVER denies it or says there are no offers — it says the agent will share the latest on offers for that home and offers to take their details (existing lead/callback tools).
    • The chat search_listings tool result carries no status field (before or after), so it needs no change.
    • PRs: churchwiseai-web#1723 (chat), #1722 (voice; deploy needs QA voice review + founder go).

States​

1. Flag OFF (default — every tenant unless they opt in)​

  • A status: 'conditional' sale listing's card badge reads "Conditional" (brass background) — byte-identical to pre-existing behaviour.
  • A status: 'conditional' lease listing's card badge also reads "Conditional" (the lease/sale distinction only applies to the active-status default branch).
  • Applies identically on every surface that renders ListingCard: homepage Featured Listings, /search, /sold (moot — sold listings never carry conditional), /coming-soon (moot — same reason), /my-homes favourites.

2. Flag ON​

  • A status: 'conditional' sale listing's card badge reads "For sale" — same green (theme.forest) background as an ordinary active listing. Visually indistinguishable from a genuinely active listing's badge.
  • A status: 'conditional' lease listing's card badge reads "For lease", never "For sale" — the lease/sale distinction (isLeaseListing()) is preserved; a conditional lease must never be mislabeled as a sale (this repeats the 2026-08-19 Team Beckett rental mislabeling bug in the opposite direction if it regresses).
  • Sold / pending / coming_soon / active badges are completely unaffected — only the conditional case branches on the flag.
  • The price overlay, MLS number, brokerage attribution line, and every other card field are unaffected — only the status badge label/colour changes.

Dashboard​

  • Setting lives at /realtor/app/settings/feeds -> "Listing display" panel (Tracking & Compliance tab region of the Website builder, WebsiteBuilderBody.tsx's ComplianceTab).
  • Checkbox label: "Show conditional listings as For sale".
  • Help text: "When a listing has an accepted conditional offer, your website shows it as For sale until it's sold. Off: it shows as Conditional. Check that this fits your board's display rules."
  • Save is immediate-publish (like the hero/videos editors) — no separate "Publish" step; the public site reflects the new setting within the Data Cache revalidation the save triggers (revalidateRealEstateSiteConfig + revalidatePath on /s/[slug], /search, /sold, /coming-soon).
  • Gated settings:compliance:edit RBAC capability — available to every realtor tenant (NOT restricted to the Team Beckett/TeamMoelker demo floor the older bearer-token /api/real-estate/site-content editor carries).
  • Toggling OFF after being ON immediately reverts every conditional badge to "Conditional" (no caching lag beyond the same revalidation window).

Storage​

  • local_businesses.metadata.listing_display = { conditional_shows_as_for_sale: boolean, updated_at: string } — additive jsonb key, no schema/DDL change. Same pattern as metadata.ddf, metadata.brokerage_identity, metadata.vow.
  • Normalized via src/lib/real-estate/listing-display-settings.ts (normalizeListingDisplaySettings — any absent/malformed value resolves to false, never throws).
  • Resolved into RESiteConfig.conditionalShowsAsForSale per-request in server/site-content.ts:resolveRealEstateSiteConfigLive, before the listings-mode branches, so it applies uniformly to every render mode (homepage/search/sold/coming-soon/full).

API​

  • GET /api/real-estate/website/listing-display?account_id=<id> returns { settings: { conditionalShowsAsForSale } }.
  • PUT /api/real-estate/website/listing-display?account_id=<id> with body { value: { conditionalShowsAsForSale: boolean } } saves and returns the new settings. A malformed value (non-object/null/array) is a 400, never a silent no-op success.
  • Tenant is always resolved from the authenticated realtor membership context (resolveRealtorContext) — never trusted from the request body.

Turning it on for Team Beckett (tenant c01)​

Requires founder approval before execution (no agent DB write). Either:

  • Dashboard (preferred): sign in as a Team Beckett realtor member with settings:compliance:edit, open /realtor/app/settings/feeds, toggle "Show conditional listings as For sale" on.
  • SQL (founder-run only):
UPDATE local_businesses
SET metadata = jsonb_set(
coalesce(metadata, '{}'::jsonb),
'{listing_display}',
jsonb_build_object(
'conditional_shows_as_for_sale', true,
'updated_at', now()::text
)
)
WHERE id = '00000000-0000-4000-a000-000000000c01';

Then either wait out the Data Cache TTL or call revalidateRealEstateSiteConfig('00000000-0000-4000-a000-000000000c01') (e.g. via a redeploy or the dashboard save path) so teambeckett.ca reflects it immediately.

Verification checklist (run against the deployed URL before declaring done)​

  • With the flag OFF on a tenant with a real conditional listing (or a seeded fixture), the card badge reads "Conditional".
  • Toggle ON via the dashboard; confirm the API response ok: true and the returned settings.conditionalShowsAsForSale === true.
  • Reload the public site (or the affected page) and confirm the same listing's badge now reads "For sale" (or "For lease" if it's a lease row).
  • Confirm the listing DETAIL page is unchanged.
  • Flag ON: ask the site chat (and, once #1722 is deployed, the voice line) about a conditional listing that is in its knowledge — it is described as for sale. Then ask "are there any offers on it?" — the answer must NOT deny an offer; it says the agent will share the latest and offers to take details.
  • Toggle OFF; confirm the badge reverts.
  • Confirm no OTHER tenant's badge changed during this test (the default stays OFF for everyone else).