Files
descrybe/docs/marketing-suite-design.md
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

493 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)