Files
descrybe/docs/marketing-suite-design.md
T

493 lines
22 KiB
Markdown
Raw Normal View History

# Marketing Suite — architecture design (Descrybe v2)
**Status:** Design only — no implementation in this doc.
**Date:** 2026-08-04
**Repos:** `f:/laragon/www/_MY/descrybe-v2`
**Inputs:** [PRICING-AND-USER-GROWTH.md](../../descrybe/PRICING-AND-USER-GROWTH.md) (sibling), [process-and-sell-summary.md](process-and-sell-summary.md), [features.md](features.md), existing `internal/billing`, `internal/woocommerce`, `internal/processing`, `internal/mail`, Settings + Nav.
---
## 0. North star
Descrybes moat remains **feeds → map → process → export/Woo**. Marketing add-ons sit **on top of a live catalog + Woo connection**, not as a second product that competes with “paste ChatGPT into a plugin.”
```
[ Catalog + Brand kit ]
├── Product AI copy (existing pipeline) ── credits / BYOK
└── Marketing Suite
├── Audience (orders + opt-in)
├── Campaigns (seasonal / category / product)
├── Reviews (sync + social proof in copy)
└── SEO recommendations (catalog-derived)
```
**Principle:** Defaults get a merchant from “Black Friday email for buyers of Category X” in minutes. Advanced (custom prompts, BYOK, segment builders) stays behind optional panels.
---
## 1. Context from todays codebase
| Area | Today (v2) | Implication for Marketing |
|------|------------|---------------------------|
| **Woo** | Company-level `woocommerce_configs`; **outbound product push** only (`internal/woocommerce`); worker claims sync jobs | Must **add inbound** order (+ later review) sync; keep credentials encrypted; reuse same store config |
| **Credits** | `credit_balances` + `AssertCanStartProcessing` / `ConsumeCredits(..., featureName)` | Extend with feature names: `campaign_copy`, `seo_recommend`, `brand_kit_ai`; same wallet |
| **Plans** | `plans` / `company_plans` + SKU caps; public Free/Starter/Growth/Business/Enterprise in `pricing-data.ts` | Need **feature flags per plan** (not only meters) for Marketing |
| **AI** | Worker `Completer` (OpenAI or heuristic); no tenant BYOK yet | Campaign/SEO/Brand AI share Completer + future BYOK seam |
| **Mail** | Platform SMTP for invites (`internal/mail`); noop if disabled | **Campaign send ≠ platform invite SMTP** — tenant-verified provider required |
| **Nav / Settings** | Nav: PIM core + WooCommerce extra; Settings tabs: profile / company / api-keys / team | Add **Marketing** nav group; Settings → **Integrations** tab (Woo, Email, Reviews, AI keys) |
| **Jobs** | DB claim + `NOTIFY` for processing + Woo | Same pattern for `order_sync`, `review_sync`, `campaign_send`, `campaign_generate` |
### Pricing doc conflict (resolve explicitly)
[PRICING-AND-USER-GROWTH.md](../../descrybe/PRICING-AND-USER-GROWTH.md) currently proposes **Free = 50 AI credits / mo**.
**This suite design requires Free = ZERO AI credits** and gates campaigns / SEO AI / brand AI.
**ASSUMPTION (recommended):** Align product policy as:
| Plan | Core product AI | Marketing AI (campaigns / SEO / brand kit AI) |
|------|-----------------|-----------------------------------------------|
| **Free** | **0 credits** (map/export/Woo test only; reverse-trial exception below) | **Blocked** |
| **Starter+** | Included pack per plan | Included; burns same credit wallet (or BYOK) |
| **Reverse trial (14d Growth)** | Full Growth credits | Full Marketing allowed |
Update `pricing-data.ts` + plan seeds when implementing — Free must not advertise “50 AI credits” if policy is zero.
---
## 2. Product modules
### 2.1 Email campaigns
**Jobs:** Draft → preview → schedule/send seasonal or catalog-driven emails using Descrybe catalog + Woo audiences.
#### Campaign types (MVP → later)
| Type | Trigger / audience seed | Copy source |
|------|-------------------------|-------------|
| **Seasonal template** | Christmas, Black Friday, New Year, Summer (system templates) | Default prompts + brand kit |
| **By category** | Products in selected category tree node(s) | Catalog titles/images + formula fields |
| **By product set** | Explicit SKUs / product IDs / collection | Same |
| **Purchase-based** | Bought X / not bought Y (needs order sync) | Same + segment filters |
#### Prompt model (mirror product formulas)
```
campaign.prompt_mode = "default" | "custom"
campaign.system_prompt_override? // only if custom + plan allows
campaign.user_prompt_vars = { season, category_names, product_snippets, brand_voice, cta_url }
```
- **Default:** curated seasonal/category prompts + Brand kit (voice, tone, banned words).
- **Custom:** editable system/user prompt (Growth+); stored per campaign, versioned.
- **Preview:** generate N sample emails (13) before send; each preview burns credits (or BYOK).
#### AI path
| Path | When | Billing |
|------|------|---------|
| **Managed** | Default | Debit `campaign_copy` (+ token packs via existing `openai_token_k`) |
| **BYOK** | Growth add-on / Business+ | Completer uses tenant key; **no credit burn for inference**; still need paid plan feature flag |
**Never send AI-generated body without human confirm** on first send of a campaign (checkbox “I reviewed the preview”). Resends of an approved version can skip.
#### Data model (sketch)
```
marketing_campaigns
id, company_id, name, type, status (draft|scheduled|sending|sent|cancelled)
season_key?, category_ids[], product_ids[]
audience_query jsonb -- segment DSL (see §3)
prompt_mode, prompts jsonb
subject, preview_text, body_html, body_text
schedule_at?, sent_at?
ai_provider (managed|byok), credit_cost_est
created_by, created_at, updated_at
marketing_campaign_sends
id, campaign_id, recipient_hash, email_encrypted_or_token
status (queued|sent|bounced|unsubscribed|failed)
provider_message_id?, error?, sent_at?
marketing_campaign_events -- opens/clicks later (P2)
```
Worker jobs: `campaign_generate`, `campaign_send_batch` (chunked, rate-limited).
---
### 2.2 Order sync (Woo → audience)
**Goal:** Build segments like “purchased product/category X in last 90d AND never purchased Y.”
#### Sync design
- Extend Woo **Integrations** config (same `woocommerce_configs` row or sibling `integration_woocommerce_orders`):
- `orders_enabled`, `orders_last_synced_at`, `orders_cursor` (modified_after / page)
- Worker job `woo_order_sync` (incremental, bounded pages per tick)
- REST: `GET /wp-json/wc/v3/orders` (+ line items); map to local tables
```
woo_customers
company_id, woo_customer_id, email_hash, email_enc, name, marketing_opt_in, unsubscribed_at, ...
woo_orders
company_id, woo_order_id, customer_id, status, ordered_at, currency, total, raw_meta jsonb
woo_order_items
company_id, order_id, sku, product_id_local?, woo_product_id, quantity, category_ids_snapshot[]
```
**Privacy:** store emails encrypted at rest; display masked; use `email_hash` for uniqueness. Prefer Woo `marketing_opt_in` / billing consent fields when present.
#### Audience DSL (JSON, server-validated)
```json
{
"op": "and",
"rules": [
{ "type": "purchased_sku", "skus": ["ABC-1"], "since_days": 365 },
{ "type": "not_purchased_sku", "skus": ["ABC-2"], "since_days": 365 },
{ "type": "purchased_category", "category_ids": ["uuid…"], "since_days": 90 },
{ "type": "marketing_opt_in", "equals": true }
]
}
```
Resolver compiles to SQL with **hard filters**: `unsubscribed_at IS NULL`, `marketing_opt_in = true`, bounce suppression list. Always AND these safety filters server-side (client cannot disable).
**ASSUMPTION:** Guests without account email are skipped unless order billing email present and opted-in.
---
### 2.3 Reviews sync
| Provider | Phase | Notes |
|----------|-------|-------|
| **WooCommerce product reviews** (`/wp-json/wc/v3/products/reviews`) | **P1** | Same store credentials; incremental sync |
| **Placeholder providers** (Judge.me, Yotpo, Trustpilot) | **P2** | Interface only: `ReviewProvider` with `ListSince(cursor)`; UI “Coming soon” + webhook stub |
```
product_reviews
company_id, provider, external_id, product_sku, rating, title, body, author_hash
reviewed_at, synced_at, usable_in_marketing bool
```
Uses: social proof blocks in campaign templates; optional “mention top reviews” in default prompts; SEO module “products with < N reviews.”
---
### 2.4 SEO recommendations (from catalog)
**Not** a full site crawler in P0. Catalog-first checks that reuse processed products + categories + standard fields:
| Check | Signal | Output |
|-------|--------|--------|
| Thin title / description | Length, uniqueness | Rewrite suggestion (AI = paid) |
| Missing attributes | Required attrs empty | Fill via process job CTA |
| Duplicate titles | Collision count | Merge / differentiate |
| Weak category paths | Depth / orphan | Taxonomy tip |
| Image gaps | No primary image | Upload / feed map tip |
| Keyword coverage | Brand kit primary keywords vs title | Nudge |
```
seo_recommendation_runs
id, company_id, status, created_at, summary jsonb
seo_recommendations
id, run_id, product_id?, severity, code, message, suggested_action
ai_draft?, applied_at?
```
- **Free:** show **rule-based** list only (no AI drafts).
- **Paid:** “Generate rewrite” → `seo_recommend` credits / BYOK → optional apply to product fields via existing product PATCH.
---
### 2.5 Brand kit
Company-scoped creative constraints for **product copy + campaigns**.
```
brand_kits
company_id PK
voice_summary text -- short “we sound like…”
tone_tags text[] -- e.g. friendly, expert, playful
banned_phrases text[]
preferred_phrases text[]
primary_color, secondary_color
logo_url / logo_file_id
fonts jsonb? -- optional
locale_default
updated_at
```
- **Manual edit:** available Starter+ (or Free read-only empty kit?). **ASSUMPTION:** Free can **view** empty kit + upgrade CTA; edit + AI-assist = paid.
- **AI assist (“Infer brand from catalog”):** paid; feature `brand_kit_ai`.
- Injected into processing enhance prompts **and** campaign defaults (single source of truth).
UI: Settings → Company **or** Marketing → Brand kit (prefer **Marketing → Brand kit** with deep-link from Integrations).
---
## 3. Plan gating & Free tier
### 3.1 Feature matrix
| Capability | Free | Starter | Growth | Business+ |
|------------|------|---------|--------|-----------|
| SKU / feed / storage meters | Per pricing ladder | ✓ | ✓ | ✓ |
| **AI credits** | **0** | Pack | Pack | Pack / BYOK |
| Product processing AI | ✗ (unless reverse trial) | ✓ | ✓ | ✓ |
| Marketing nav (read-only teasers) | Teaser + upgrade | ✓ | ✓ | ✓ |
| Campaigns create/send | ✗ | ✓ (caps) | ✓ | ✓ |
| Campaign AI generate | ✗ | Credits | Credits/BYOK | ✓ |
| Order sync | ✗ (or read-only test 1 page?) | ✓ | ✓ | ✓ |
| Audience “bought X not Y” | ✗ | ✓ | ✓ | ✓ |
| Reviews sync (Woo) | ✗ | ✓ | ✓ | ✓ |
| SEO rule-based | ✓ (limited rows) | ✓ | ✓ | ✓ |
| SEO AI rewrite | ✗ | ✓ | ✓ | ✓ |
| Brand kit edit | ✗ | ✓ | ✓ | ✓ |
| Brand kit AI | ✗ | ✓ | ✓ | ✓ |
| BYOK | ✗ | ✗ | Add-on | Included |
**ASSUMPTION:** Starter is the smallest plan that unlocks Marketing Suite send + order sync. Free stays acquisition for **feed aha**, not email ESP.
### 3.2 Enforcement seams (reuse billing)
Extend `internal/billing`:
```go
// Pseudocode — design only
AssertFeature(ctx, companyID, "marketing.campaigns")
AssertFeature(ctx, companyID, "marketing.order_sync")
AssertCanSpendCredits(ctx, companyID, feature, est)
```
- Plan row gains `features jsonb` (or `plan_entitlements` table).
- API returns `403` + machine code `feature_gated` / `insufficient_credits` for UI `UpgradeBanner`.
- Reverse trial: temporary Growth entitlements; day 15 → Free (0 credits, Marketing hard-off). Persist draft campaigns read-only.
### 3.3 Caps (Starter starter-limits)
| Meter | Starter (example) | Growth |
|-------|-------------------|--------|
| Campaigns / mo | 5 | 50 |
| Recipients / send | 2_000 | 25_000 |
| Order sync lookback | 180d | 730d |
| SEO AI rewrites / mo | 50 | 1_000 |
Exact numbers tunable; enforce server-side.
---
## 4. Integration style (UX IA)
### 4.1 Settings → Integrations
Replace scattered `/woocommerce` as the *only* integration home. Keep `/woocommerce` as redirect → Settings Integrations for one release if needed.
**Settings tabs:** `profile` | `company` | `team` | `api-keys` | **`integrations`**
Integrations cards:
1. **WooCommerce** — store URL, keys, test connection, product sync toggle, **order sync** toggle, **reviews sync** toggle
2. **Email sending** — provider (Resend / SES / SMTP / Postmark), from-domain, **verification status**, rate limits
3. **AI keys (BYOK)** — Growth+; encrypted; never logged
4. **Reviews providers** — Woo (active) + placeholders
### 4.2 Marketing nav
New top-level group (after Export Feeds / before Billing):
```
Marketing
├─ Campaigns
├─ Audiences
├─ Reviews
├─ SEO
└─ Brand kit
```
Free users see routes with locked empty-states + upgrade CTAs (not 404).
---
## 5. Safety (non-negotiable)
| Rule | Implementation |
|------|----------------|
| **No send without verified provider** | `email_provider.status == verified` AND DNS/domain checks passed; else Send button disabled + API `412 email_not_verified` |
| **Opt-out / unsubscribe** | Every email: `List-Unsubscribe` + one-click POST; landing page; set `unsubscribed_at`; suppress forever until re-opt-in via Woo/consent |
| **Opt-in required** | Audience resolver always requires marketing consent |
| **Rate limits** | Per-company send RPM/RPH; provider caps; exponential backoff; circuit breaker on bounce rate > threshold |
| **PII** | No emails in logs; hash + encrypt; admin support tools mask by default |
| **Double opt-in for imported lists** | If CSV import ever added (P2): require confirmation — Woo-sourced opted-in only in P0/P1 |
| **Abuse** | Cap Free teasers; paid caps; platform admin kill-switch per company |
| **Legal copy** | Footer template mandatory (company address, unsubscribe) before schedule |
Platform invite SMTP (`internal/mail`) **must not** be used for marketing blasts.
---
## 6. UX principles
1. **Happy path wizard:** Pick template (BF/Christmas) → pick category/products → auto audience suggestion → generate 1 preview → connect email if missing → schedule.
2. **Advanced accordion:** custom prompts, raw audience DSL, UTM params, send-time optimization (P2).
3. **Reuse catalog chrome:** product pickers, category tree, UpgradeBanner, processing job progress patterns.
4. **Empty states teach the core path:** “Connect Woo + sync orders to target buyers of X.”
5. **Credit honesty:** show estimated credit cost before Generate; BYOK badge when active.
---
## 7. Interactive tutorial (activation)
Extend the pricing docs ≤15 min aha into a **guided checklist** (dashboard + optional spotlight tour):
| Step | Route / action | Done when |
|------|----------------|-----------|
| 1. Add feed | `/feeds` | Feed created with URL/file |
| 2. Map fields | `/feeds/{id}/mapping` | Required fields mapped (auto-map OK) |
| 3. Process sample | `/processing` | ≥10 products `completed` (credits: trial/paid) |
| 4. Export or Woo push | `/export-feeds` or Integrations | Export generated **or** Woo test sync OK |
| 5. Brand kit (paid) | `/marketing/brand-kit` | Voice saved **or** skipped with “later” |
| 6. Campaigns (paid) | `/marketing/campaigns` | Draft campaign created (send optional) |
- Persist `onboarding_progress` on `company_settings`.
- Free users complete 14; 56 show locked steps with value props.
- Tutorial deep-links must survive refresh; dismissible; “Replay tour” in help.
Aligns with [process-and-sell-summary.md](process-and-sell-summary.md) happy path + Marketing as step after export.
---
## 8. Technical architecture (v2 packages)
```
apps/api/internal/
marketing/ # campaigns, audiences, brand kit, seo services
integrations/
email/ # provider interface: Send, VerifyDomain, Webhooks
reviews/ # ReviewProvider
billing/ # + AssertFeature, feature costs
woocommerce/ # + OrdersClient, ReviewsClient, sync workers
apps/api/sql/schema/
01x_marketing.sql # tables above
01y_integrations_email.sql
apps/web/src/routes/
marketing/campaigns/
marketing/audiences/
marketing/reviews/
marketing/seo/
marketing/brand-kit/
settings/ # + integrations tab
unsubscribe/[token]/ # public
```
**Completer sharing:** marketing generation calls the same `processing.Completer` interface; inject company BYOK client when entitlement + key present.
**Idempotency:** send batches keyed by `(campaign_id, recipient_hash)`; sync cursors monotonic.
---
## 9. API sketch (dashboard + future public)
| Method | Path | Notes |
|--------|------|-------|
| GET/PATCH | `/api/integrations/woocommerce` | Existing + order/review flags |
| POST | `/api/integrations/woocommerce/sync-orders` | Enqueue |
| GET/PATCH | `/api/integrations/email` | Provider config |
| POST | `/api/integrations/email/verify` | Start verification |
| GET/PATCH | `/api/marketing/brand-kit` | |
| GET/POST | `/api/marketing/campaigns` | |
| POST | `/api/marketing/campaigns/{id}/generate` | Credits gate |
| POST | `/api/marketing/campaigns/{id}/schedule` | Requires verified email |
| POST | `/api/marketing/audiences/preview` | Count + sample (masked) |
| POST | `/api/marketing/seo/runs` | |
| POST | `/api/public/unsubscribe` | Token auth |
Public API v1 exposure of Marketing = **Business+ / later**; dashboard session auth first.
---
## 10. Risks & open decisions
| Topic | Risk | Recommendation |
|-------|------|----------------|
| Free AI credits vs pricing doc | Conflicting promises | **Ship Free = 0 AI**; update pricing doc + `pricing-data.ts` |
| Becoming an ESP | Deliverability / spam liability | Verified domain only; start with low caps; consider “export to Klaviyo” P2 |
| Order PII | GDPR | Encrypt, retention policy, delete-with-company, DPA |
| Woo permission scopes | Keys may lack orders/reviews | TestConnection checks endpoints; clear UI errors |
| Custom prompts abuse | Prompt injection / brand damage | Length limits; strip tools; require preview ack |
**Open questions (non-blocking for P0 scaffolding):**
1. First ESP: Resend vs SES vs tenant SMTP only?
2. Starter recipient cap exact number?
3. Free SEO rule-based: show all issues or cap at 20?
---
## 11. Sibling-agent checklist
### P0 — foundation (do first; unblocks everything)
- [ ] **P0.1** Align Free plan: **0 AI credits**; Marketing feature flags on `plans`; update `pricing-data.ts` + seeds; reverse-trial keeps Growth marketing.
- [ ] **P0.2** `billing.AssertFeature` + credit feature names (`campaign_copy`, `seo_recommend`, `brand_kit_ai`); wire `UpgradeBanner` codes.
- [ ] **P0.3** Settings → **Integrations** tab; move Woo config UX there (redirect old `/woocommerce`).
- [ ] **P0.4** Marketing nav shell + locked empty states for Free.
- [ ] **P0.5** Brand kit schema + CRUD (manual fields only).
- [ ] **P0.6** Email provider integration model + **verified-only send gate** (even if provider is stubbed behind interface).
- [ ] **P0.7** Campaign draft CRUD + default seasonal templates + generate-preview job (no mass send yet).
- [ ] **P0.8** Unsubscribe token endpoint + suppression list.
- [ ] **P0.9** Woo **order sync** (incremental) + `woo_orders` / items / customers tables.
- [ ] **P0.10** Audience preview: purchased X / not purchased Y + opt-in hard filters.
- [ ] **P0.11** Onboarding tutorial steps 14 wired to real completion signals; 56 gated.
- [ ] **P0.12** Safety tests: cannot send if unverified; cannot include unsubscribed; Free cannot generate campaign AI.
### P1 — sellable Marketing MVP
- [ ] **P1.1** Campaign schedule/send batches via verified provider + rate limits + bounce basic handling.
- [ ] **P1.2** Custom vs default prompts UI; credit estimate + BYOK seam (if BYOK platform work ready).
- [ ] **P1.3** Woo product **reviews** sync + Reviews Marketing page.
- [ ] **P1.4** SEO recommendation runs (rule-based + paid AI rewrite).
- [ ] **P1.5** Brand kit injection into product enhance + campaign defaults.
- [ ] **P1.6** Cap enforcement (campaigns/mo, recipients/send).
- [ ] **P1.7** Tutorial steps 56 + “first Black Friday campaign” recipe.
- [ ] **P1.8** Ops: kill-switch, send metrics on billing overview.
### P2 — expand
- [ ] **P2.1** Placeholder review providers (Judge.me / Yotpo) + webhooks.
- [ ] **P2.2** Open/click tracking; A/B subjects.
- [ ] **P2.3** Export audience / campaign to Klaviyo/Mailchimp.
- [ ] **P2.4** Site crawl SEO (beyond catalog).
- [ ] **P2.5** Public API for campaigns.
- [ ] **P2.6** Multi-language campaign variants from catalog locales.
- [ ] **P2.7** AI “infer brand kit from best products.”
---
## 12. Verification expectations (when built)
- Unit: audience SQL compiler, feature gate matrix, unsubscribe token.
- Integration: Woo orders pagination mocked; send blocked without verify.
- UI: Free user sees locks; Starter can preview generate with credits.
- No claim-done without `AssertFeature` coverage on every mutating Marketing route.
---
## 13. Rollback
- Feature flags `MARKETING_ENABLED` (platform) + per-plan entitlements.
- Schema additive only; drop via goose Down if needed.
- Disabling flag hides nav and 404s APIs without deleting drafts.
---
## Related docs
- Sibling pricing: `f:/laragon/www/_MY/descrybe/PRICING-AND-USER-GROWTH.md`
- [process-and-sell-summary.md](process-and-sell-summary.md)
- [features.md](features.md) · [status-and-gaps.md](status-and-gaps.md) · [security-notes.md](security-notes.md)