Reviews Engine (Real-Estate MVP) — Expected Output Spec
✅ STATUS: APPROVED — 2026-06-24, founder (John Moelker).
The five §1 decisions are confirmed on their RECOMMENDED DEFAULTS (owner- pressed sends; no unguarded cron; operator-marked auto-stop; real-estate only; 24-month EBR window; bundled pricing). Build phase = the owner-facing engine (multi-platform targets, create-request, CSV import, suppression/unsubscribe, CASL footer, funnel dashboard, owner-pressed sends with DRY-RUN by default); the automated multi-touch cadence cron is DEFERRED to a separate, guarded follow-up. Sends to real clients remain founder/owner-gated (house rule) and dry-run by default. The Phase-2 schema is already applied — verify columns via
information_schema; no new migration in this phase (Rule #18 still applies).
Sourced from the seven Reviews Engine design docs (all read-only research, 2026-06-24):
REVIEWS_ENGINE_PLAN.md,REVIEWS_ENGINE_ARCH.md,REVIEWS_ENGINE_DATAMODEL.md,REVIEWS_ENGINE_VERTICALS.md,REVIEWS_ENGINE_COMPLIANCE.md,REVIEWS_ENGINE_INFRA_MAP.md,REVIEWS_ENGINE_PLATFORMS.md. Every "exists today" claim traces to the infra map's verifiedinformation_schemareads; every compliance rule traces to the compliance/platforms docs' named sources.
0. Scope — what this spec covers, and what it does NOT
This spec covers the Real-Estate MVP of the Reviews Engine: an owner-operated
tool inside the WiseAI Agency dashboard (wiseaiagency.com/business/[token]) that
nudges a real-estate agent's own past and recent clients — by email (SMS
deferred, §1.3) — to leave a review on the platforms AI search engines cite
(Google Business Profile primary; RankMyAgent → Realtor.ca secondary), with a
multi-touch reminder cadence that auto-stops when the client reviews or opts
out, a working CASL opt-out spine, and a review-funnel dashboard.
~70% already exists in the production local_business_* platform (Lane 7) — a
single-shot, email-first, Google-only, owner-pressed "Review Request" flow with a
non-gating /r/[token] private-feedback surface. This MVP is an extension of
that, not a rebuild. This spec is the sibling of ai-front-desk.md and
local-business-photos-branding.md (the approved voice/chatbot + media specs for
the same dashboard) and does not re-specify the dashboard shell, leads inbox,
module gating, brand chrome, or the local_businesses lifecycle — those are
assumed and cross-referenced.
Real Estate is the FIRST vertical of a portable engine. The ARCH/DATAMODEL docs design a vertical-agnostic CORE (cadence, suppression, tracking, consent gate, dashboard) + a thin per-vertical ADAPTER (platform registry, copy, consent windows, industry rules). This spec accepts that boundary but only specifies the real-estate adapter; other verticals are explicitly out of scope (§7) and several are legally GATED (§5.7).
| Covered here (NEW for real-estate MVP) | Assumed / cross-referenced (already specced) |
|---|---|
| Multi-platform review targets (Google + RankMyAgent) | /business/[token] dashboard shell + tabs (ai-front-desk.md, photos spec) |
| Owner-approved batch + multi-touch auto-stopping cadence | Leads inbox + pipeline, owner notifications mechanics |
| CASL EBR consent gate + client opt-out/suppression spine | /r/[token] private-feedback page (non-gating — kept as-is) |
| Past-client import (last 24 months, EBR-logged) | sendLocalBusinessMessage transport (email/SMS, dry-run, audit) |
| Review-funnel dashboard (sent → clicked → reviewed) | reviews module entitlement gating (modules.ts) |
| Extended click-redirect allowlist (Google already covered) | Brand chrome / host-aliasing on wiseaiagency.com |
1. The five OPEN DECISIONS — RECOMMENDED DEFAULTS (founder to confirm)
These are the open product decisions (PLAN §6, COMPLIANCE §4) that gate the build. Each carries a recommended default the spec is written against. The founder must confirm or change each before this spec is approved. Until then, the spec text below assumes the defaults; if the founder changes one, the affected sections change with it.
Decision 1 — Cadence / send authority — [RECOMMENDED DEFAULT — founder to confirm]
Owner-approved batches. A human at the business (the agent/owner) presses Send for each batch and each cadence run. NO unguarded cron may auto-send to real clients. This is the house rule that never lifts (
feedback_never_cron_send_campaigns,feedback_db_writes_ok_sends_need_blessing): DB writes by the engine (drafting requests, importing, scheduling, suppression) are fine without a human; sends to real people require a human press.Concretely (PLAN §6.2 option b): the cadence engine drafts each touch and computes its due time, but a scheduled reminder is only dispatched after the owner has approved the batch it belongs to (and any global / per-business kill switch is off). A cron may surface due reminders for owner approval and may dispatch only owner-approved, consent-checked, suppression-checked, rate-limited, auto-stopping touches — it may never originate an unapproved send. No review send ever routes through the cold-outreach machinery (
/api/cron/outreach-send,leads/email_unsubscribers/campaign_members).
Decision 2 — Auto-stop mechanism — [RECOMMENDED DEFAULT — founder to confirm]
Operator-marked "reviewed" for MVP. The owner clicks a "they reviewed us" control (or the request is marked
reviewedmanually); Google Business Profile API auto-detection is deferred to a later phase. For MVP,clickedis a soft signal (logged, optionally slows cadence) andreviewed/opted_outare the hard stops. Any transition to a terminal state (reviewed,private_feedback,cancelled,failed) or an opt-out nullsnext_reminder_at. Thereviewed_detected_viacolumn records provenance (manual/owner_confirmed/clicked_proxy/gbp_api) so adding GBP auto-detection later is a logic change, not a migration.
Decision 3 — Vertical scope — [RECOMMENDED DEFAULT — founder to confirm]
Real Estate only for v1. Veterinary is the next adapter (low legal risk — CVO no-pressure mode). Dental / medical are deferred pending legal review (RCDSO/CPSO testimonial bans + PHIPA/HIPAA — COMPLIANCE §3.2–3.3, §3.6). Funeral is GATED (bereavement delay + BAO — §5.7). A vertical whose posture is unset fails closed (BLOCKED) — never "allowed."
Decision 4 — Pricing — [RECOMMENDED DEFAULT — founder to confirm]
Bundled in the pilot; metering deferred. The Reviews Engine is included for pilot real-estate tenants (it is the Local Authority package's review value per
ai-front-desk.md§8). No per-send pricing, no usage meter, no paywall in the MVP. A send-volume cap exists only as an abuse/cost guardrail (§4.2 — reuse the existing 100/business/day limit), not as a billing meter.
Decision 5 — Past-client reactivation window — [RECOMMENDED DEFAULT — founder to confirm]
Last 24 months (CASL EBR), owner-approved batch. Past clients who purchased/leased (or signed a written contract) within 24 months, and inquirers within 6 months, are eligible under CASL implied consent. Every imported row must carry an
ebr_date(or a stated basis) — no date, no send (burden of proof on sender). Reactivation runs as an owner-approved batch (Decision 1), and every message carries sender ID + a current mailing address + a working unsubscribe (CASL, §5.1–5.2). Contacts outside the window import as non-sendable (review/manual only).
2. The AI-Bridge / honesty anchor (applies to every behaviour below)
Per knowledge/architecture/ai-bridge-principle.md and the engine's compliance
spine, three honesty rules sit above every behaviour:
- No gating, ever. The same public review ask goes to every eligible
client — no pre-screen, no happy→public / unhappy→private routing. The
existing
/r/[token]private-feedback surface is compliant only because it never gates who is asked for a public review (it never shows a public-review link conditioned on rating). The build must NOT add sentiment routing around it. (Google policy + FTC 16 CFR Part 465, penalties up to $51,744/violation.) - No incentives. The engine never offers a reward/discount/gift/entry for leaving, changing, or removing a review. No incentive field exists in the data model — do not add one.
- No fake / no cross-copy. The engine only ever sends a client a link to write their own review; it never drafts, pre-fills, submits, or warehouses review content. It never auto-replicates review text onto Rate-My-Agent (that platform penalizes cross-platform copying). A reviewer-initiated "also share on Google" nudge to the same client is allowed (RankMyAgent facilitates this).
3. User states (which state an expected output applies to)
| State | Condition | Engine behaviour |
|---|---|---|
| Eligible client, consent valid | ebr_date inside window, not suppressed, business active | Full cadence (§4.1–4.4) |
| Consent lapsed | ebr_date outside the EBR window at send time | No send — request stays non-sendable; importer marks it skipped (§4.5, §5.3) |
| Suppressed client | on local_business_client_suppressions (email/sms/all) | No send — skipped before every touch; persists across batches (§4.4) |
| Reviewed / opted-out | terminal state reached | Cadence auto-stops; next_reminder_at nulled (§4.3) |
| Cadence paused | cadence_paused = true (manual hold) | No further touches until resumed |
| Batch not yet approved | drafts staged, owner has not pressed Send | No send — drafts visible to owner; nothing dispatched (Decision 1) |
| Paused / cancelled business | local_businesses.status ∈ {paused, cancelled} | Engine read-only; no sends; dashboard editors read-only (consistent with local-business-platform.md) |
Demo tenant (real-estate): Terry & Sheri Real Estate
(local_businesses.id = 00000000-0000-4000-a000-000000000c01, slug
terry-and-sheri, vertical real_estate). The reviews module must be enabled
on it for the tab to render. It has 0 review requests today; MVP seeds real-estate
demo content + platform targets (Google + RankMyAgent). NEVER use a real customer
tenant as a write target (feedback_never_modify_customer_data).
4. Expected outputs — the engine behaviours
4.1 — Owner sets up review targets (multi-platform)
State: business has the reviews module; owner opens the Setup tab.
Should see (the owner):
- A multi-platform targets editor that captures, for real estate: a Google
Place ID (primary, priority 1) and a RankMyAgent request-review link
(secondary, priority 2); optionally Rate-My-Agent and Facebook. Each target has
enabled+priority. - The existing single
google_review_urlis migrated into aplatform:'google'target as the seed (DATAMODEL §6 step 8) — no owner re-entry required. - A note that for Google the engine stores the Place ID (never expires) and
builds
https://search.google.com/local/writereview?placeid=<id>at send time. - A RankMyAgent compliance note: reviewer-initiated Google cross-post is OK; the engine never copies review text.
Should NOT see:
- A free-text box that accepts arbitrary review hosts not on the allowlist.
- Any "auto-copy this review to other platforms" control (cross-copy is banned).
- Rate-My-Agent presented as a place the engine will replicate text to.
Data-model touchpoints (NEW — migrate-before-use):
local_business_review_targets(one row per(business_id, platform),UNIQUE (business_id, platform)); or the lighter jsonb-on-setup-profile path (founder decision PLAN §6.4; recommend the table).platformis a soft-validated text key (google, rankmyagent, rate_my_agent, facebook, …) — a loose CHECK guards typos; the authoritative per-vertical catalog lives in code.
Success criteria:
- A real-estate tenant has at least a Google target (seeded) and can add a RankMyAgent target; both render with correct priority. Verified on the demo tenant.
4.2 — Click-redirect allowlist covers the platforms (blocking)
State: a client taps a tracked review link.
Expected behaviour:
- Every platform link 302s through
/api/local-business/review-requests/track/clickto the platform's write-review URL — it does not 400. - Google hosts (
search.google.com,g.page,maps.google.com) are already inALLOWED_HOSTS— do not re-add. The build adds:rankmyagent.com,rate-my-agent.com(hyphenated),realtor.ca,facebook.com(+ future-vertical hosts only when those verticals onboard). - Naming trap (blocking): add
rate-my-agent.com(hyphenated, the Canadian platform). Never addratemyagent.com(un-hyphenated = an unrelated Australian/US company). The hyphen is load-bearing. - A click advances
sent → clickedand logs an inbound event (existing behaviour).
Verification gate:
- Each new platform link verified to 302 (not 400) on the real production host
(
wiseaiagency.com— host-aliasing applies; preview hosts skip the hostname-rewrite middleware,feedback_preview_host_hides_middleware_rewrites).
4.3 — Multi-touch cadence + auto-stop (owner-approved)
State: an eligible, consent-valid, non-suppressed client is enrolled and the owner has approved the batch (Decision 1).
Should happen (the client receives):
- An initial review request email pointing to the priority-1 target (Google), with a tracked one-tap link.
- If no review/opt-out occurs, a gentler reminder after a business-day delay,
and a second reminder after a further delay. Default cadence:
initial → +3 business days → +7 business days, max 3 touches
[RECOMMENDED DEFAULT — founder to confirm; PLAN §6.5]. Per-business override viacadence_steps_days/max_touches. - Reminders land on business days (reuse
addBusinessDaysinfollowup-rules.ts).
Auto-stop (the hard guarantee):
- The cadence stops immediately and
next_reminder_atis nulled when the request reaches any terminal state —reviewed,private_feedback,cancelled,failed— or the client opts out (opted_out_atset), orcadence_step >= max_touches, orcadence_paused = true. - For MVP,
reviewedis set by operator/owner action[RECOMMENDED DEFAULT — Decision 2];reviewed_detected_viarecords how. GBP auto-detection is a later phase and requires no migration to add.
Should NOT happen:
- A reminder sent after the client reviewed or opted out (auto-stop must fire).
- A send to a client whose EBR window has lapsed since enrollment —
canSend()re-checks consent + suppression before EVERY touch (a client may lapse or opt out mid-cadence). - Any touch dispatched for a batch the owner has not approved (Decision 1).
- A reminder routed through the cold-outreach send path.
INBOX_COMPOSE_DRY_RUNignored — dry-run must short-circuit and still audit.
Data-model touchpoints (NEW — migrate-before-use, on local_business_review_requests):
target_id,cadence_step,max_touches,next_reminder_at,last_reminder_at,cadence_paused,reviewed_detected_via.- No new status value is strictly required (cadence drives off columns); a
dedicated terminal
opted_outstatus is optional and, if chosen, must widen the verifiedstatusCHECK and updatestatuses.tsnextReviewRequestStatusesin the same change (grep callers first).
Success criteria:
- A demo enrollment runs initial → reminder_1 → reminder_2, and stops the instant
the request is marked reviewed or opted-out — verified by observing
next_reminder_atnulling and the message log showing no further touch (sample state at ≥2 timepoints; behaviour, not DOM presence).
4.4 — Client opt-out / suppression spine (CASL)
State: a client clicks unsubscribe (email) or replies STOP (SMS, when SMS is enabled in a later phase).
Should happen (the client):
- A working, free, single-step unsubscribe link in every email (and a STOP keyword for SMS) immediately suppresses them; an idempotent confirmation page is shown.
- The opt-out is honored within 10 business days (standard immediately; the engine honors on receipt).
Data-model touchpoints (NEW — migrate-before-use):
local_business_client_suppressions— business-scoped (email/sms/all), partial-unique on(business_id, lower(email))and(business_id, phone). This is the client-side spine — deliberately separate from the cold-outreachemail_unsubscribers/leads/campaign_memberstables (reuse the pattern, not the tables). Onlocal_business_review_requests:opted_out_at,unsubscribe_token(UNIQUE, issued lazily at send likefeedback_token).- A Resend bounce/complaint on a review send should add the contact to
suppressions (wire
local_business_message_logs.provider_message_idinto/api/webhooks/resend/events— later phase; no schema change).
Should NOT happen:
- A suppressed client receiving any further touch (checked before every send).
- An opt-out that does not persist across future batches.
- The unsubscribe link pointing at a
churchwiseai.comdefault — every link MUST use the campaign's own property host (wiseaiagency.com) and resolve on the real host (feedback_outreach_links_per_property_host).
Success criteria:
- A simulated unsubscribe writes a suppression row, the confirmation page is idempotent, and the next cadence tick skips that client — verified end-to-end on the real host.
4.5 — Past-client import + reactivation (24-month EBR)
State: the owner uploads a past-client list (CSV at MVP: name, email, phone, transaction/inquiry date).
Should happen (the owner):
- Each row is validated:
ebr_daterequired (else rejected — no date, no send);ebr_basisderived (purchase_24mofrom a transaction date); consent window checked (≤24 months purchase / ≤6 months inquiry) — lapsed → skipped (imported as non-sendable, not sent). Email lower-cased, phone E.164-normalized; de-duped against existing leads + suppressions. - A summary returns to the owner:
{ imported, skipped, rejected }with a skip-reason histogram ({duplicate, no_ebr_date, suppressed, lapsed}). - Imported, in-window contacts land as
draftreview requests (carryingebr_basis+ebr_date+import_id); they are not sent until the owner approves the batch (Decision 1).
Data-model touchpoints (NEW — migrate-before-use):
local_business_client_imports(batch provenance + counts);local_business_review_requests.import_id,ebr_basis,ebr_date.consent_validis computed at send time, never stored (storing it goes stale and violates burden-of-proof).
Should NOT happen:
- A row without an
ebr_datebecoming sendable. - An out-of-window contact enqueued for sending.
- An import auto-sending anything (Decision 1 — owner presses Send).
Success criteria:
- A test CSV with mixed rows (valid / no-date / lapsed / duplicate) imports with correct accept/skip/reject counts; valid rows are staged as drafts and nothing is sent until approval. Verified against the demo tenant only.
4.6 — Review-funnel dashboard
State: owner opens the Review Requests tab / Monthly Report.
Should see (the owner):
- A funnel per platform: sent → clicked → reviewed / private-feedback, with velocity, layered onto the existing tab and Monthly Report panel.
- Honest metrics only: real sent / clicked / reviewed / private-feedback /
bounced counts. Never "Opened/Delivered" theater presented as engagement;
clickedis the real engagement signal. Un-provisioned/empty state reads honestly (e.g. "No requests yet"), not fabricated numbers. - SMS is shown as "coming soon" until the SMS sender + SMS-consent path lands (§1.3 / later phase) — email-first MVP.
Should NOT see:
- Fabricated counts for a business that has sent nothing.
- A "review text / star rating over time" panel built on the Places API (Places content may not be cached/stored — only Place IDs; persistent rating history needs the owned-location Business Profile API, later phase).
Success criteria:
- After a demo run, the funnel reflects the real
local_business_message_logs/ request statuses for the period.
5. Compliance guardrails the build MUST enforce (not bolted on)
A build that violates any of these is not done. Sources: REVIEWS_ENGINE_COMPLIANCE.md,
REVIEWS_ENGINE_PLATFORMS.md. [CORE] = one global rule for every vertical;
[CFG] = per-vertical setting (unset → fail-closed/BLOCKED).
5.1 CASL EBR consent gate — [CORE]
Every request carries ebr_basis + a concrete ebr_date; consent_valid is
computed at send time (24mo purchase / 6mo inquiry / 24mo written contract);
no date or lapsed window → no send. Burden of proof is on the sender — the date
is the audit record. Importer requires it per row (§4.5).
5.2 CASL message form on EVERY send — [CORE]
Every email (and SMS, when enabled) carries: (a) sender identification (business
name), (b) a current mailing address valid ≥60 days, (c) a clear/free/working
unsubscribe (STOP for SMS). A send with no resolvable mailing address is
blocked. Opt-outs honored within 10 business days. Which address (brokerage
vs agency) is per-business [CFG] (PLAN §6.8 — founder to confirm the source);
defaults to the business address.
5.3 Treat every review message as a regulated commercial message — [CORE]
Comply as if it is a CASL CEM / CAN-SPAM commercial message (full ID + address + opt-out + consent basis). No "transactional exemption" path. A CASL-compliant send is automatically CAN-SPAM-compliant on consent.
5.4 NO review gating — [CORE]
Same public ask to every eligible client; no sentiment routing; /r/[token] never
gates the public ask. (Google policy + FTC 16 CFR Part 465.) The dominant funeral
review tool's happy/unhappy "smart routing" is exactly what we must NOT build.
5.5 NO incentives; NO fake/self-authored/cross-copied reviews — [CORE]
No reward tied to a review (no incentive field in the model). Engine never drafts/
pre-fills/submits review content. Never auto-copy text to Rate-My-Agent
[CFG, real-estate]; reviewer-initiated RankMyAgent→Google cross-post nudge OK.
5.6 Google Places API storage constraint — [CORE]
Never cache/store Places-API review text/ratings; store only Place IDs; persistent rating history (later phase) comes from the owned-location Business Profile API.
5.7 Per-vertical testimonial posture — [CFG], fail-closed
Real-estate (LIVE this MVP): solicit = allowed; republish in own ads =
allowed only with client consent + RECO registrant-ID footer + CREA
material-connection disclosure. MVP collects only — it does NOT republish
testimonials into the business's own marketing; that path is out of scope (§7).
GATED verticals (dental, medical, funeral) must NOT be enabled in production
until their open legal question is cleared by counsel + a knowledge/acceptance/
spec exists. An unset posture defaults to BLOCKED (Decision 3).
5.8 Owner-gated sends; never the cold machinery — [CORE]
Sends to real clients are founder/owner-gated (Decision 1). The cadence sender's hard guards (all non-negotiable): consent-checked + suppression-checked
- gating-safe + rate-limited (reuse 100/business/day) + auto-stop + DRY_RUN-respected
- audited, on a dedicated authenticated path — never
/api/cron/outreach-send, the cold tables, or any cold-campaign code. A founder kill switch disables the cadence globally + per-business.
6. Referenced data-model changes (NOT yet applied — migrate-before-use)
All NEW columns/tables below are PROPOSAL ONLY (DATAMODEL §6 carries the
⛔ DO NOT APPLY YET SQL). Before ANY CREATE TABLE/ALTER TABLE: grep every
repo for callers of the affected tables/columns, document coverage in the
migration header (Rule #18 / never-migrate-before-audit), and apply founder-gated.
Migrations land in churchwiseai-web/migrations/ (AI-product feature). Re-verify
every EXISTING column via information_schema.columns at build time.
| Object | Status | Purpose |
|---|---|---|
local_business_review_targets | NEW table | one row per (business_id, platform); UNIQUE(business_id, platform); seed from setup_profiles.google_review_url |
local_business_review_requests.target_id | NEW col | which platform the ask points at (FK → targets) |
local_business_review_requests.{cadence_step,max_touches,next_reminder_at,last_reminder_at,cadence_paused} | NEW cols | cadence machinery |
local_business_review_requests.reviewed_detected_via | NEW col | auto-stop provenance (manual/gbp_api/clicked_proxy/owner_confirmed) |
local_business_review_requests.{ebr_basis,ebr_date,opted_out_at,unsubscribe_token,import_id} | NEW cols | CASL consent + opt-out + import provenance |
local_business_review_settings (or fold into setup_profiles) | NEW table (optional) | cadence_steps_days, max_touches, casl_sender_name, casl_mailing_address, sms_from_number |
local_business_client_suppressions | NEW table | business-scoped opt-out spine |
local_business_client_imports | NEW table | past-client import provenance + counts |
local_business_message_logs | EXISTING — no schema change | reuse metadata jsonb for {cadence_step, platform, target_id, event} + provider_message_id |
track/click ALLOWED_HOSTS | EXISTING — extend (code) | add RankMyAgent / rate-my-agent.com / realtor.ca / facebook.com (NOT ratemyagent.com) |
Optional flagged widenings (only if the founder picks them; each touches a
verified CHECK + has code callers — grep + update code in the same change): a
dedicated terminal opted_out status (else drive opt-out off opted_out_at); a
dedicated import value on local_business_leads.source (else map to manual).
7. OUT OF SCOPE (explicit)
The MVP does NOT include, and the build must not silently add:
- Any vertical other than Real Estate. Vet is next; dental/medical deferred pending legal review; funeral GATED (Decision 3, §5.7).
- Unguarded cron auto-send to real clients. Sends are owner-approved batches only (Decision 1). No new cron may originate an unapproved send.
- GBP / Realtor.ca review auto-detection. Auto-stop is operator-marked for MVP (Decision 2); GBP API v4 detection is a later phase.
- SMS to clients (this MVP is email-first). SMS needs a per-business sender DID for non-voice tenants + a recorded SMS-consent basis (TCPA prior-express-written-consent zone for US) + an inbound STOP webhook — all later phases. The UI shows SMS as "coming soon."
- Republishing testimonials in the agent's own ads/site. MVP collects only; any republish path needs the RECO/CREA consent + registrant-ID + material-connection flow (§5.7) and a separate spec.
- Pricing / metering / paywall. Bundled in the pilot (Decision 4).
- Persistent review-text / rating-over-time warehousing. Places API content may not be stored (§5.6).
- CRM (non-CSV) imports. CSV only at MVP; CRM connectors are later.
- Multi-location targets (
location_id) and the Resend-webhook → suppression wiring (later phase).
8. Acceptance test stubs (Stage 3 — test.fixme() skeletons, created on approval)
Evidence is behavioural verification on the deployed wiseaiagency.com host
(real host, not preview) plus DB row inspection — never "build passes." Run only
against the demo tenant (…0c01).
// Targets + allowlist
test.fixme('Google target is seeded from google_review_url; RankMyAgent target can be added', () => {});
test.fixme('every platform review link 302s through track/click (not 400) on wiseaiagency.com', () => {});
test.fixme('rate-my-agent.com is allowlisted; ratemyagent.com (un-hyphenated) is NOT', () => {});
// Consent + cadence + auto-stop
test.fixme('a request with no ebr_date cannot be sent', () => {});
test.fixme('cadence runs initial → +3bd → +7bd (max 3) only after owner approves the batch', () => {});
test.fixme('marking reviewed/opted-out nulls next_reminder_at — no further touch fires', () => {});
test.fixme('canSend re-checks consent + suppression before EVERY touch (lapsed mid-cadence → skipped)', () => {});
// Opt-out / suppression
test.fixme('unsubscribe writes a business-scoped suppression row; confirmation page idempotent', () => {});
test.fixme('suppressed client is skipped on the next cadence tick; persists across batches', () => {});
test.fixme('unsubscribe link uses the wiseaiagency.com host and resolves on the real host', () => {});
// Import
test.fixme('CSV import: ebr_date required (reject), lapsed/duplicate skipped, valid staged as draft (not sent)', () => {});
// Compliance / guardrails
test.fixme('every email carries sender ID + mailing address + working unsubscribe; no-address send blocked', () => {});
test.fixme('no review send routes through the cold-outreach cron/tables', () => {});
test.fixme('founder kill switch disables the cadence globally + per-business', () => {});
// Dashboard
test.fixme('review funnel shows real sent/clicked/reviewed counts; no fabricated numbers; SMS shows "coming soon"', () => {});
9. Guardrails for agents building against this spec (after approval)
- Rule #17 is NOT satisfied yet. Do not start the customer-facing build until the founder approves this spec AND confirms the five decisions in §1.
- Migrate-before-use. All NEW columns/tables (§6) need a founder-gated migration with caller-grep first. A column is not "used" until it exists. The DATAMODEL SQL is PROPOSAL ONLY.
- Sends to real clients are the gate that never lifts. DB writes (drafting,
importing, scheduling, suppression) by the engine are fine; a human presses
Send. No cron may originate an unapproved send
(
feedback_never_cron_send_campaigns,feedback_db_writes_ok_sends_need_blessing). - Verify on the real host.
wiseaiagency.comhost-aliasing + any new email/API path must be in middlewareSHARED_API_PREFIXES; preview hosts hide rewrites. Links must use the property's own host, never the churchwiseai.com default (feedback_outreach_links_per_property_host). - Evidence-or-nothing. Behavioural verification (sample state at ≥2 timepoints and assert change) + DB rows; never claim done on a green build alone.
- Knowledge-sync (Rule #16). When the build modifies any file referenced here
or in the infra map, update the doc in the same PR; run
pnpm derive --check. - Honest metrics only (Clicks/Replies/Reviewed/real bounce — never Delivered/Opened theater), per the outbound process map.
- If code diverges from this spec, the code is wrong — update the spec first (founder approval), then the code.
End of spec. STATUS: APPROVED (2026-06-24, founder). Five §1 decisions confirmed on recommended defaults. Sends to real clients remain owner-gated and dry-run by default. Build phase = owner-facing engine; the auto-cadence cron is deferred to a separate guarded follow-up.