Skip to main content

Acceptance Spec - Realtor Website → Testimonials tab

  • Status: Built 2026-10-29 (John: "build it"), pending independent QA sign-off
  • Product: WiseAI Realtor System → /realtor/app/website (Website builder) + Reviews manager + Aria corpus
  • Owner surface: /realtor/app/website?tab=testimonials (realtor Supabase Auth dashboard)
  • Related: realtor-website-builder-mvp.md, realtor-reviews-manager-mvp.md §4.5, real-estate-content-editor-mvp (the /business/[token] editor), realtor-video-module-mvp.md (the immediate-publish pattern)
  • Code: src/components/realtor/website/TestimonialsTab.tsx, src/app/api/real-estate/website/testimonials/route.ts, src/lib/real-estate/website/server/testimonials-resolve.ts, src/lib/real-estate/server/site-content.ts (caps + cleanMultilineText), src/components/realtor/reviews/ReviewsManagerBody.tsx, src/lib/real-estate/reviews/server/reviews-manager-data.ts, migration migrations/20261029_realtor_testimonials_tab_product_knowledge.sql

1. Why (the customer problem)​

On 2026-10-08 Sheri Beckett (Team Beckett, a real free case-study customer) asked whether a new Google review loads onto teambeckett.ca automatically, was told no, and replied "Is there a way for me to add the review". The question sat 20 days because there was no honest answer:

  • The only working add form (RealEstateWebsiteEditor → Testimonials) is mounted in the /business/[token] dashboard, which realtor accounts never see (they log in at /realtor/app via Supabase Auth).
  • The realtor Website builder had Hero, Team bios and Videos tabs but no Testimonials tab.
  • The Reviews manager's gallery rendered "Add to site" as a <span> with no button behind it.
  • Aria's corpus (product_knowledge category realtor_operations) told her to curate testimonials "in the Reviews manager", i.e. at the dead label.

A second trap found while building: the server sanitizer capped a quote at 600 chars (60 rows) and collapsed paragraph breaks. Team Beckett's committed config carries 72 real reviews, 27 longer than 600 chars (longest 3,899) and 24 multi-paragraph. Any editor that round-trips the list through that sanitizer (the /business/[token] editor does) would have truncated and reflowed a real customer's reviews on the first save.

2. Scope​

In: a Testimonials tab in the realtor Website builder (add / edit / reorder / remove, star rating, optional client photo, per-new-row consent tick, immediate publish); a resolved-list GET so the tab never opens blank; a contract-guarded PUT; caps widened and paragraph breaks preserved; the Reviews gallery shows what is on the site and links to the tab; ?tab= and ?add=1 deep links; Aria corpus rows rewritten plus one new row answering the Google-reviews question.

Out (explicit non-goals): pulling Google reviews automatically (Google offers no supported path for this; the copy says so), a stored consent record (the override shape has no field; the tick is a UI gate — follow-up), a collect-only "unpublished" testimonial store, editing from the public site, per-testimonial scheduling, the /business/[token] editor's own UI (unchanged but it now benefits from the wider caps).

3. Data shape (unchanged, re-used)​

Testimonials remain a per-tenant override on local_businesses.metadata.website.{draft,published}.testimonials as RETestimonialOverride[] (quote, author, location?, rating?, photo?). The public template merges with WHOLE-ARRAY REPLACE semantics: a non-empty override replaces config.testimonials; absent/empty falls back to config. No migration, no new columns.

site-content.ts changes: LIMITS.testimonials 60 → 120, LIMITS.testimonialQuote 600 → 4000, new exported cleanMultilineText (keeps \n\n, collapses other whitespace per line, strips tags/control chars) used for the quote only. LIMITS is now exported.

4. Behaviour​

4.1 GET /api/real-estate/website/testimonials?account_id=​

  • Auth settings:branding:edit (the Website page's gate, same as hero/videos).
  • Returns { testimonials, source: 'stored' | 'config', limits } where testimonials is the RESOLVED list: the stored override (published, else draft, re-sanitized) when non-empty, otherwise the committed config list in override shape. source says which won.
  • 503 not_provisioned for a tenant with no committed RE config.

4.2 PUT /api/real-estate/website/testimonials?account_id= { value }​

  • value: [...] sanitize and save to BOTH draft and published (immediate publish), then revalidate /s/[slug] and /s/[slug]/reviews.
  • value: null or [] clears the override (site falls back to config).
  • No value key → 400 value_required (never clears — the hero route's 2026-08-19 lesson). Non-array → 400 value_invalid.
  • More than LIMITS.testimonials rows → 400 too_many (never silently truncate). Any row without a non-blank quote → 400 quote_required with the row index (never silently drop). Sanitized count ≠ sent count → 400.
  • Response { ok, testimonials, source } with the resolved list.

4.3 The tab (/realtor/app/website?tab=testimonials)​

  • Loads the resolved list; shows n of 120, "the top one shows first", and "these are the testimonials we set up for you" when source === 'config'.
  • "+ Add testimonial" PREPENDS a blank row (newest first: the homepage shows the first six, the Reviews page shows all). Fields: quote (textarea, cap 4000), first name (cap 80), town/role (optional, cap 80), 1–5 stars (click again to clear), optional photo (upload via the existing /api/real-estate/website/upload, kind gallery, ≤15 MB image).
  • Every row: move up / move down / remove.
  • A row added in this session shows a consent tick: "The client agreed this can appear on my website (we show a first name only, never a full name or address)". Save refuses until every new row is ticked and names the row. Rows already on the site are not re-asked.
  • "Save & publish" is disabled until something changed; on success shows "Live on your site · saved HH:MM" and the rows become non-new.
  • ?tab=testimonials&add=1 opens the tab with a blank row ready and focused.
  • Copy states plainly that Google reviews don't copy over on their own.

4.4 Reviews manager (/realtor/app/reviews)​

  • The gallery card is now "Testimonials on your website": shows the RESOLVED list (so a testimonial added from the tab appears here), a count pill, the first six, "See and edit all" when more, and for curators (reviews:testimonial:edit) a real "+ Add a testimonial" link to /realtor/app/website?tab=testimonials&add=1. The dead "Add to site" label is gone.

4.5 Aria / support corpus​

  • acf5e17c… rewritten: "How do I add a testimonial or client review to my website?" → the tab, step by step.
  • 3620486f… and a194817c… rewritten to point testimonial management at Website → Testimonials and to say Google reviews do not flow automatically.
  • New row 7c2e0b64… (realtor_operations): "Do Google reviews load onto my website automatically?" → No, and the steps.
  • After apply: validate_product_knowledge() CRITICAL/WARNING must be empty for these rows.

5. Should see / should NOT see​

Should see (as Sheri, owner of Team Beckett): Website → Testimonials opens with her 72 existing reviews listed, oldest at the bottom; Add testimonial → paste → first name → tick consent → Save & publish → the review is the first card on teambeckett.ca and on teambeckett.ca/reviews within a page reload; Reviews page gallery shows it with "On website ✓" and the count went up by one; Aria, asked "how do I add a Google review to my site", names the Website → Testimonials tab.

Should NOT see: an empty Testimonials tab for a tenant whose reviews live in config; any of the 72 reviews shortened or reflowed after a save (round-trip test pins this against the real config); a save that silently dropped a row or clipped the list; "Add to site" with nothing behind it; Aria pointing at the Reviews manager for adding; a testimonial publishable without the consent tick on a new row; the Hero/Team/Videos overrides touched by a testimonials save.

6. Verification (evidence required before merge)​

  1. Unit: src/lib/real-estate/website/server/__tests__/testimonials-resolve.test.ts (resolution rules, cleanMultilineText, ROUND TRIP against the real Beckett config with positive controls) and src/app/api/real-estate/website/__tests__/testimonials-put-body-contract.test.ts (PUT contract, Reviews link, tab registration, ?tab= validation). Both in .github/workflows/test.yml.
  2. site-content-hero.test.ts still green (shared module).
  3. Independent QA on the Vercel preview: tab renders with the resolved list (count visible), add → consent gate blocks → tick → save → GET returns the new row first; Reviews gallery link lands on the tab with a blank row; PUT without value → 400. QA must use a test realtor account, never the Beckett account (customer data).
  4. After merge: apply the corpus migration, run validate_product_knowledge(), purge chatbot_response_cache if the support bubble cached the old answer, then ask Aria the question on the real host.
  5. Beckett's own first save is John's call (customer data write): the round-trip test is the evidence that it is lossless.

7. Open​

  • Stored consent evidence (who ticked, when) is a follow-up; the override shape has no field for it.
  • Pulling Google reviews automatically is not possible through a supported Google API for a business profile the realtor owns without Google Business Profile API access and verification; parked, copy says "paste it".
  • The /business/[token] editor still drops quote-less rows silently and does not deep-link here; out of scope.