Skip to main content

WiseAI Realtor — QR Campaigns Expected Output Spec

⛔ STATUS: DRAFT — NOT APPROVED. CLAUDE.md Rule #17 (HARD GATE) is NOT satisfied.

Stage-1 agent research, pre-populated from the realtor design docs + the qr-campaigns.html mock. Do not build the customer-facing screen until the founder approves and confirms the open items in a Stage-2 interview.

Sourced from (read-only, 2026-06-29): design-direction.md (sortable headers, KPI tile, source chips), information-architecture.md (group C "QR Campaigns"), backend-completeness-audit.md (QR a "no competitor has it" BEAT dropped from the rail — restored), data-model.md §4 (source taxonomy qr_scan → yard_sign/open_house/feature_sheet/mailer/business_card), do-not-reinvent.md (the existing /s/[slug]/scan flow), and realtor-mockups/qr-campaigns.html.


0. Scope — what this spec covers, and what it does NOT

This spec covers the QR Campaigns workspace (under Listings): the scan analytics dashboard, the campaign list, and the 6-step create flow that generates a tracked QR + print-ready assets. The differentiator: a scan opens a live AI chat pre-loaded with the listing's context (/s/{slug}/scan/{mls}), not a static form — the BEAT no Canadian competitor has (audit).

Does NOT cover: the public scan landing/chat experience itself in detail (that is the existing /s/[slug]/scan flow + ai-front-desk.md §C chatbot behaviour — this screen creates and measures campaigns that route into it); the full analytics suite (realtor-analytics-mvp.md — QR is one source there); the leads list (realtor-leads-mvp.md).

Build posture: the /s/[slug]/scan yard-sign flow already exists (do-not-reinvent.md); this screen is the builder + analytics on top of it — extend, do not rebuild. A QR-campaign object is proposed/greenfield: it is not yet in data-model.md (the data model has the qr_scan source taxonomy and re_interactions scan events, but no re_qr_campaigns table). The build must add a campaign object (founder-gated migration) or model campaigns on the existing scan/landing config — flag (§1, §9).


1. The mock screen this spec governs + what it reads

Governs: realtor-mockups/qr-campaigns.html

  • Scan analytics: a differentiator badge ("Only on WiseAI: scan → live AI chat"), a date range (30d/7d/All) + Export, KPI tiles (Total scans / Unique visitors / Leads captured / Scan→lead rate at $0 cost-per-lead), Scans over time bar chart (open-house weekend peaks), a Scan→lead funnel (Scans → Unique → Opened AI chat → Captured lead → Showing booked [see §6]), and Top campaigns by scans.
  • Campaign list: a sortable-header table (Campaign, Type, Linked to, Scans [active sort], Leads, Conv., Status, Created) with type filters (Listing sign / Open house / Feature sheet / Direct mail / Buyer guide / Community guide), per-row tracked URL (teambeckett.ca/r/…), and statuses (Active / Paused / Draft "QR not generated").
  • Create drawer: a 6-step vertical stepper — 1 Campaign type, 2 Connect content (listing/page), 3 Landing-page template, 4 Lead-capture fields (incl. Language preference), 5 Generate QR (live QR preview, style/branded options, center logo, encoded link, + the "Scan → opens AI chat with listing context" note routing to /s/terry-and-sheri/scan/X9241885), 6 Download print-ready assets (yard-sign rider / feature sheet / window card / social story PDFs + raw QR PNG/SVG/EPS at 300 dpi).

Reads/writes (verify exact route names; do not fabricate):

SurfaceUnderlying store
Campaign list + KPIsa QR-campaign object (proposed — re_qr_campaigns, NOT yet in data-model.md) + scan events
Scans / unique / over-time / top campaignsscan events (re_interactions interaction_type='qr_scan', with campaign id + listing id in metadata)
Scan→lead funnelqr_scanchat_session re_interactions → captured re_contacts/local_business_leads with source_category='qr_scan'
Connect contentlocal_business_listings (+ pages/guides)
Scan → live AI chatthe existing /s/{slug}/scan/{mls} flow (do-not-reinvent.md) → the RE chatbot with listing context (ai-front-desk.md §C)
Capture fields incl. languagefeeds re_contacts.preferred_language

Demo tenant: Terry & Sheri Real Estate (…0c01, slug terry-and-sheri). Mock campaigns + the /s/terry-and-sheri/scan/… route are demo; NEVER a real customer (feedback_never_modify_customer_data).


2. The AI-Bridge / honesty anchor

  • A scan bridges to a live, honest AI chat. The scan opens Aria pre-loaded with the listing, in the visitor's language, capturing the lead — Aria discloses she is AI, answers from live listing facts only (facts-used receipt, ai-guardrails.md RULE 3), and offers a human. The QR is a front door to the bridge, not a data-harvesting form-wall.
  • Honest scan/lead metrics. Scans, unique visitors, opened-chat, captured leads are real counts; cost-per-lead for QR is honestly $0 (no ad spend). No fabricated numbers for a campaign with no scans (→ "—" / draft state).

3. Role-based visibility

BucketRolesScope
Brokerage managementbrokerage_owner, broker_admin (brokerage scope)All campaigns + brokerage scan analytics; create/edit
Team managementteam_admin (team scope)Team campaigns + team scan analytics; create/edit
Agent / ISAagent, isa (own scope)Own campaigns + their scan→lead attribution; create own
Support / externaltransaction_coordinator, marketing_assistant, external_partner (bounded)View / scoped (marketing_assistant may create per policy); external_partner read-only

4. Expected outputs

4.1 — Scan analytics (KPIs + charts + per-campaign attribution)

Should see:

  • KPI tiles: Total scans, Unique visitors, Leads captured, Scan→lead rate (with $0 cost-per-lead), each with an honest trend.
  • Scans over time (with open-house-weekend peaks highlighted), a Scan→lead funnel (Scans → Unique → Opened AI chat → Captured lead → Showing request [§6]), and Top campaigns by scans.
  • Numbers are real (from scan + chat + lead events); a no-data range reads honestly, not a fabricated chart.

Should NOT see:

  • "Showing booked" as a confirmed AI booking in the funnel (§6 — it is a captured request).
  • Fabricated scan/lead counts; "opens"/vanity metrics presented as engagement (honest metrics — clicks/scans/leads, not opens).

Success: the agent sees, per campaign, how many scans became chats became leads — the attribution the value narrative needs.


4.2 — Campaign list (sortable, typed, with tracked URLs + statuses)

Should see:

  • A sortable table (every relevant column click-to-sort, active column = teal arrow + bold label, persisted per view — the founder sortable-header rule), with type filters and per-row: campaign name + tracked URL, type chip, linked listing/page (+ MLS# where applicable), Scans / Leads / Conv., status (Active / Paused / Draft "QR not generated"), created date.
  • Row click opens the campaign (edit / assets / analytics).

Should NOT see:

  • A draft campaign showing fabricated scan/lead numbers (drafts show "—").
  • A non-sortable header masquerading as sortable.

Success: the agent finds, sorts, and filters campaigns by performance and type.


4.3 — 6-step create flow → generate QR + print-ready assets

Should see:

  • A 6-step drawer with a progress indicator: (1) campaign type (sign / open house / feature sheet / direct mail / buyer guide / community guide), (2) connect content (a listing — with MLS#/price — or a page), (3) landing template (e.g. Listing Spotlight with photos/map/AI chat dock), (4) lead-capture fields (incl. Language preference), (5) Generate QR (live preview, branded/classic/rounded style, optional center logo, the encoded tracked link, and the "Scan → opens AI chat with listing context" explainer routing to /s/{slug}/scan/{mls}), (6) Download print-ready assets (yard-sign rider / feature sheet / window card / social story PDFs + raw QR PNG/SVG/EPS at 300 dpi vector). Save-draft at any step.
  • The generated QR encodes the campaign's own tracked URL on the tenant's host (e.g. teambeckett.ca/r/… / /s/{slug}/scan/{mls}) — every link uses the campaign's own property host, never a churchwiseai.com default (feedback_outreach_links_per_property_host); the path must resolve on the real host (middleware SHARED_API_PREFIXESfeedback_preview_host_hides_middleware_rewrites).

Should NOT see:

  • A QR that routes to a static form-first wall instead of the listing-context AI chat (the differentiator).
  • A tracked link on the wrong host that 404s on the real production host.

Success: an agent creates a tracked QR campaign in under two minutes, scans route to a context-loaded AI chat, and print-ready assets download at print resolution.


5. Empty / loading / error states

StateExpected output
No campaignsDemo-seeded preview + a "Create your first QR campaign" CTA + the differentiator explainer — never a blank list (anti-pattern #6).
No scans yet (new campaign)Honest "No scans yet — place the sign/asset and they'll appear" — not a fabricated chart.
LoadingKPI/chart/table skeletons, not a spinner (anti-pattern #7).
QR generation errorInline "Couldn't generate the QR — retry"; the draft is preserved.
Asset download errorInline retry; raw QR still downloadable.
Linked listing went off-marketCampaign shows a "listing inactive" flag; scan still bridges to the agent/AI honestly (no stale "active" claim — ai-guardrails.md RULE 3).

6. Carried-forward constraints (consistent across the batch)

  • Aria does NOT book/schedule — the funnel's "Showing booked" is a showing REQUEST captured pending agent confirmation, never an AI-confirmed booking (ai-front-desk.md decision 4; relabel).
  • SMS consent-gated + OFF by default; crisis/safety NEVER gated (a scan chat that surfaces a personal crisis still routes to 988 — ai-front-desk.md §V7).
  • Role enum (rbac.ts — implemented source of truth): agent | team_admin | transaction_coordinator | broker_admin | brokerage_owner | marketing_assistant | isa | external_partner (scopes own|team|brokerage).
  • No realtor pricing — gate by role/module; "$0 cost-per-lead" is an honest attribution stat, not a price claim.
  • Honest metrics only; "Not measured"/"—" when unknown. Language never hardcoded (capture-field language list = tenant's enabled set).

7. Accessibility (AODA → WCAG 2.1 AA)

  • Charts (scans-over-time, funnel) have text/table equivalents; not color-only; data labels present.
  • Table: real header/row semantics; aria-sort on sortable columns; status chips convey by text.
  • Create stepper: keyboard-navigable; each step is a labelled region; the QR preview has an accessible name + the encoded URL as text; Back/Save/Activate are labelled buttons; focus managed across steps.
  • Contrast ≥4.5:1; gold accent (peaks/celebration) only; teal actions meet AA; reduced-motion honored.

8. Acceptance checklist (QA runs on the deployed URL)

Behavioural verification on wiseaiagency.com (real host) against the demo tenant (…0c01); never "build passes". Sample at ≥2 timepoints.

// Analytics + attribution
test.fixme('KPI tiles + scans-over-time + scan→lead funnel + top campaigns render from REAL scan/chat/lead events', () => {});
test.fixme('per-campaign scan→lead attribution ties qr_scan → chat_session → captured lead (source_category=qr_scan)', () => {});
test.fixme('a no-scan campaign shows "—"/empty honestly; no fabricated numbers; "opens" never shown as engagement', () => {});

// Campaign list
test.fixme('campaign table sorts on every relevant column (active = teal arrow + bold, persisted); type filters work', () => {});
test.fixme('draft "QR not generated" shows no fabricated scans/leads', () => {});

// Create flow + the differentiator
test.fixme('6-step create flow generates a tracked QR; encoded URL is on the tenant host and resolves on the real host', () => {});
test.fixme('a scan opens the LIVE AI chat pre-loaded with listing context (/s/{slug}/scan/{mls}), not a static form wall', () => {});
test.fixme('print-ready assets download (PDF + raw QR PNG/SVG/EPS at 300 dpi)', () => {});

// Carried-forward
test.fixme('funnel "showing" reads as a captured REQUEST, not AI-"booked"; a crisis in a scan chat still routes to 988', () => {});

// States + a11y
test.fixme('empty/no-scan/loading/error/off-market states render per §5; never blank, never a spinner', () => {});
test.fixme('charts have text equivalents; table aria-sort; stepper keyboard-navigable; reduced-motion honored', () => {});

9. Guardrails for agents building against this spec

  • Rule #17 not satisfied — do not build until founder approval.
  • Extend /s/[slug]/scan, don't rebuild the public scan/landing/chat (do-not-reinvent.md); this screen is the builder + analytics over it.
  • A QR-campaign object is greenfielddata-model.md has the qr_scan source taxonomy + scan re_interactions but no re_qr_campaigns table; add one (founder-gated, additive, IF NOT EXISTS, service-role-grant-only) or model campaigns on existing scan config — confirm with founder. Migrate-before-use (Rule #18).
  • Tracked links use the tenant's own host and must resolve on the real production host (middleware SHARED_API_PREFIXES; feedback_outreach_links_per_property_host, feedback_preview_host_hides_middleware_rewrites).
  • Aria books nothing; SMS off-by-default + consent-gated; crisis never gated; language never hardcoded; honest metrics; verify on the real host; evidence-or-nothing.
  • If code diverges, update the spec first (founder approval), then the code.

End of spec. STATUS: DRAFT — NOT APPROVED. Open items for Stage-2: (1) add a re_qr_campaigns table vs model on existing scan config; (2) the exact tracked-URL scheme (/r/{slug} vanity vs /s/{slug}/scan/{mls}) and host; (3) which landing templates ship in the MVP; (4) whether print-asset generation is in-house or via a service.