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.htmlmock. 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 taxonomyqr_scan→ yard_sign/open_house/feature_sheet/mailer/business_card),do-not-reinvent.md(the existing/s/[slug]/scanflow), andrealtor-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):
| Surface | Underlying store |
|---|---|
| Campaign list + KPIs | a QR-campaign object (proposed — re_qr_campaigns, NOT yet in data-model.md) + scan events |
| Scans / unique / over-time / top campaigns | scan events (re_interactions interaction_type='qr_scan', with campaign id + listing id in metadata) |
| Scan→lead funnel | qr_scan → chat_session re_interactions → captured re_contacts/local_business_leads with source_category='qr_scan' |
| Connect content | local_business_listings (+ pages/guides) |
| Scan → live AI chat | the existing /s/{slug}/scan/{mls} flow (do-not-reinvent.md) → the RE chatbot with listing context (ai-front-desk.md §C) |
| Capture fields incl. language | feeds 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.mdRULE 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
| Bucket | Roles | Scope |
|---|---|---|
| Brokerage management | brokerage_owner, broker_admin (brokerage scope) | All campaigns + brokerage scan analytics; create/edit |
| Team management | team_admin (team scope) | Team campaigns + team scan analytics; create/edit |
| Agent / ISA | agent, isa (own scope) | Own campaigns + their scan→lead attribution; create own |
| Support / external | transaction_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 (middlewareSHARED_API_PREFIXES—feedback_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
| State | Expected output |
|---|---|
| No campaigns | Demo-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. |
| Loading | KPI/chart/table skeletons, not a spinner (anti-pattern #7). |
| QR generation error | Inline "Couldn't generate the QR — retry"; the draft is preserved. |
| Asset download error | Inline retry; raw QR still downloadable. |
| Linked listing went off-market | Campaign 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.mddecision 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-sorton 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 greenfield —
data-model.mdhas theqr_scansource taxonomy + scanre_interactionsbut nore_qr_campaignstable; 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.