# Easy Ads — Product Requirements

## Original Problem Statement
Easy Ads is a SaaS platform that simplifies Meta Ads for local SMEs. White-label model: master ad-account stays with us, customer's Facebook Page is the actor. Goal: app download → live ad in under 10 minutes.

## Architecture
- **Backend:** FastAPI + MongoDB, JWT cookie auth, Emergent Object Storage, official `stripe` SDK (subscriptions + €5 extra-adjustment + Customer Portal + webhook), Shotstack (AI-edited 9:16 ad with title/beats/CTA/endcard/brand identity), **Gemini 3 Flash** via Emergent LLM key (storyboard with separated title/caption/on-screen-text in user's language), Claude Sonnet 4.5 vision (blur validation).
- **Frontend:** React 19, Tailwind, Shadcn/ui, recharts, Phosphor Icons, sonner, react-i18next (EN/PT/IT/ES/FR — Landing fully translated EN+PT), **react-leaflet + OpenStreetMap** (free geofence map with no API key).
- **Theme:** Swiss/Brutalist with Klein Blue (#002FA7), sharp 0-radius edges.

## Plans
| Plan | Price | Reach | Adjustments |
|------|-------|-------|-------------|
| Basic Presence | €19/mo | 200–1.5k | 1/mo + €5 extra |
| Local Growth | €49/mo | 1.5k–3k | 1/mo + €5 extra |
| Boosted Reach | €79/mo | 3k–10k | 1/mo + €5 extra |
| Ultimate Scale | €119/mo | 10k+ | 1/mo + €5 extra |

`daily_budget_eur = round(price * 0.50 / 30.5, 2)` stored on every campaign.

## AI-edited Shotstack ad (Gemini 3 Flash)
On `POST /api/campaigns/{id}/render-video`:
- **Inputs:** `music_id`, `duration_seconds` (30–60s, customer-chosen), `brand_text` (optional company text overlay), `brand_logo_media_id` (optional uploaded logo).
- **Storyboard (separated):** `title` ≤24c · `caption` ≤120c (Meta post body) · per-clip `beats[]` ≤24c · `cta` ≤14c · `endcard` ≤20c · `brand_default` ≤24c.
- **Template hook:** "Looking for the best [Niche] in [City]? Visit [Business Name]!"
- **Timeline tracks:** endcard+title → brand identity (logo OR text) → sticky CTA bar → per-beat overlays → visual clips (with audio muted).
- Persisted **before** the Shotstack POST so the storyboard survives editor outages.

## Stripe webhook PAUSE/RESUME
- `invoice.payment_failed` / `customer.subscription.deleted` → all active campaigns → `paused` with `paused_reason`.
- `payment_succeeded` / `subscription.updated (active)` → only billing-paused campaigns re-activate.

## Geofence map (free)
Leaflet + OpenStreetMap (Nominatim geocoding, no API key). Shown on:
- Onboarding Plan step (alongside the radius slider)
- CampaignDetail under the daily-reach chart, with "Edit address" link to Profile.

## Onboarding (5 steps)
Profile → Niche → **Content (8 cyclic photo-or-video slots, min 1)** → Plan + Map → Review (AI auto-fills headline + description in user's language).

## CampaignDetail extras
- Adjustments quota (1 free / mo + €5 extra-adjustment Stripe one-off)
- Music modal (19 royalty-free tracks)
- **Video duration slider 30–60s** (customer choice)
- **Brand identity overlay** (logo OR text)
- AI-edited Shotstack render + full-screen Preview button
- Geofence map

## MOCKED
- Meta Marketing API. Real fields prepared (page_id, actor_id, ad_account_id, geo_locations.custom_locations[radius+address], daily_budget_eur). Will flip when user provides Meta App ID/Secret + long-lived token + master ad-account ID.
- Stripe webhook signature verification deferred to production (`STRIPE_WEBHOOK_SECRET`).

## Verified (iter-7)
- Backend pytest: **70/70** including duration clamp (30–60), variable media count (1/4/8), brand-text/logo persistence, vision validate fix (FileContent), storyboard schema (title/caption/beats/cta/endcard/brand_default).
- Frontend Playwright: 8 upload slots, video duration slider, brand controls, Leaflet geofence map with marker + radius circle, render-video posts duration_seconds + brand_text correctly.

## Backlog
**P0** — Real Meta Marketing API integration (needs credentials)
**P0** — Stripe webhook signature verification
**P1** — Resend transactional emails (campaign live, invoice, cancellation, video-ready)
**P1** — Best-frame analysis: Gemini Vision over uploaded videos to pick the most informative segments (currently we just trim 1s in)
**P1** — ToS PDF + signed receipt
**P2** — IT/ES/FR Landing copy beyond hero, RN apps, multi-location accounts

## Next Tasks
1. **Fase 3 — Stripe Regional**: MB WAY, Multibanco, SEPA Direct Debit, Stripe Tax PT NIF, invoice fetch + "View Campaign" button
2. **Fase 4 — Meta 2-Tier Business Manager**: Child BM creation, System Admin User attach, LOC linkage, closed billing, FB Page association (needs Meta long-lived token + Parent BM ID confirmed)
3. Resend transactional emails
4. Stripe webhook signature verification (production)

## Phase 2 — Cinematic Video + Approval Gate (2026-02 — done)
**Cinematic rendering** (`routes/video.py`):
- Aspect ratio picker: `9:16` (stories/reels 720x1280) or `1:1` (feed 720x720)
- 4 curated vibes: `professional` → corporate, `energetic` → gameplay, `minimal` → ambient, `luxury` → smooth (elegant jazz)
- Lower-thirds only — text lives in the bottom 30% with linear-gradient 0→55% black at the bottom for readability
- 10% safe-zone padding on all edges to dodge Instagram/Facebook UI chrome
- Montserrat: 900 weight for hero titles, 700 for body/beats, 900 for CTA
- Ken Burns zoomIn effect on static images (Shotstack `effect: "zoomIn"`)
- Cross-fade 0.8s transitions between all clips
- 45-char hard limit on every overlay (title/beat/endcard/brand_default) — enforced in Gemini prompt + `_trunc` fallback

**Approval gate** (`routes/campaigns.py`):
- New campaigns start at `approval_status="pending_review"` and `status="active"` (Meta is mocked)
- `POST /api/campaigns/{id}/approve` — requires `render_url` first (409 otherwise), flips to `approval_status="approved"` with `approved_at`
- Campaign dispatch to Meta (Phase 4) will gate on `approval_status == "approved"`

**AI niche-script translation** (`routes/catalog.py`):
- `GET /api/niches?lang={pt|it|es|fr}` auto-translates script titles+descriptions via Gemini 2.5-flash
- Cached forever in `db.script_translations` keyed by `"{niche_id}:{lang}"`
- `asyncio.Semaphore(5)` caps concurrent Gemini calls on cold-cache bursts

**Onboarding UX** (`pages/Onboarding.jsx`):
- Live GeofenceMap preview on Step 0 (Profile) — re-geocodes as user types the address
- Camera capture button alongside Upload in Step 2 (Content) — `<input capture="environment">` opens the phone camera
- Step 5 (Review) now renders preset pickers (aspect + vibe) → "Render preview" button → preview video (polls `/render/{id}`) → "Approve & launch" (calls `/approve`)
- Re-render button available when the user wants a different cinematic preset

**Refactor (prior — Iter-10)**: `server.py` split from 1292 → 73 lines into `core.py` + 9 `routes/*.py` modules. 102/103 backend tests pass.
