Files
padelnomics/docs/USER_FLOWS.md
Deeman 454b362c88 feat: admin email hub — sent log, inbox, compose, audiences, delivery tracking
Add full email management at /admin/emails with:
- email_log table tracking all outgoing emails with resend_id + delivery events
- inbound_emails table for Resend webhook-received messages
- Resend webhook handler (/webhooks/resend) updating delivery status in real-time
- send_email() returns resend_id (str|None) instead of bool; all 9 worker
  handlers pass email_type= for per-type filtering
- Admin UI: sent log with HTMX filters, email detail with API-enriched HTML
  preview, inbox with unread badges + reply, compose with branded wrapping,
  audience management with contact list/remove
- Sidebar Email section with unread badge via blueprint context processor

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 13:00:23 +01:00

230 lines
10 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.
# User Flows
All user-facing flows through the padelnomics app. Use this as the reference when writing E2E tests or auditing coverage.
---
## 1. Visitor → Planner
**Entry:** `/<lang>/` → click "Planner" in nav
| Step | URL | Notes |
|------|-----|-------|
| 1 | `GET /<lang>/planner/` | Wizard loads with default state (indoor, 6 courts, rent). `s` = default state, `d` = calc results. |
| 2 | Adjust any slider | `POST /<lang>/planner/calculate` (HTMX, `hx-trigger="input changed delay:200ms"`) → returns `#tab-content` partial |
| 3 | Switch result tab | `POST /<lang>/planner/calculate` with `activeTab=<tab>` → HTMX swaps `#tab-content` |
| 4 | View charts | Charts embedded as `<script type="application/json" id="chartX-data">` in response. `initCharts()` in `planner.js` renders them. |
| 5 | Wizard preview | `#wizPreview` updated OOB (`hx-swap-oob="true"`) with CAPEX / monthly CF / IRR |
**Auth required:** No (logged-in users get their default scenario pre-loaded)
**HTMX partials:** `calculate_response.html`, `wizard_preview.html`
**Key state:** `s` (validated via `validate_state()`), `d` (calc output via `calc()`)
---
## 2. Visitor → Quote Request (from Planner)
**Entry:** Planner → quote sidebar (>1400px wide) or inline CTA (results tabs, <1400px)
| Step | URL | Notes |
|------|-----|-------|
| 1 | `GET /<lang>/leads/quote` | Planner passes state via query params — step 1 pre-populated |
| 29 | `POST /<lang>/leads/quote/step/<n>` | HTMX: each step swaps `#q-step` content; progress bar OOB-swapped into `#q-progress` |
| 9 (submit) | `POST /<lang>/leads/quote` | Standard HTML form POST (not HTMX) |
| After submit | Verification email sent if guest; or lead created directly if logged in |
| Verify | `GET /<lang>/leads/verify?token=...&lead=...` | Token validated → lead status updated → success page |
**Auth required:** No (guests get email verification; logged-in users skip verification)
**Key validation:** Step 1: facility_type required. Step 2: country required. Step 5: timeline required. Step 7: stakeholder_type required. Step 9: name, email, phone, consent required.
**Email sent:** `send_quote_verification` (worker task) — verify URL must include `/<lang>/`
---
## 3. Visitor → Quote Request (Direct)
Same as Flow 2 but arrives at `/<lang>/leads/quote` directly (no planner state). Step 1 shows the full editable form (no pre-fill).
---
## 4. Visitor → Directory
**Entry:** `/<lang>/directory/` via nav
| Step | URL | Notes |
|------|-----|-------|
| 1 | `GET /<lang>/directory/` | Lists all suppliers, filter UI visible |
| 2 | Search/filter | `GET /<lang>/directory/results?q=...&country=...&category=...` HTMX swaps `#dir-results` |
| 3 | Click supplier card | `GET /<lang>/directory/<slug>` — supplier detail page |
| 4 | Send enquiry | `POST /<lang>/directory/<slug>/enquiry` HTMX swaps `#enquiry-result` |
| 4b | External link | `GET /<lang>/directory/<slug>/website` → 302 redirect (click-tracking) |
| 4c | Get quote | `GET /<lang>/directory/<slug>/quote` → redirect to quote wizard |
**Auth required:** No
**Category/country labels:** Must be translated per `g.lang` via `get_directory_labels(lang)`
---
## 5. Visitor → Signup
**Entry:** Any CTA "Create Account" / "Sign up" → `/auth/signup`
| Step | URL | Notes |
|------|-----|-------|
| 1 | `GET /auth/signup` | Signup form (or waitlist form if `WAITLIST_MODE=true`) |
| 2 | Submit email | `POST /auth/signup` → sends magic link via `send_welcome` + `send_magic_link` tasks |
| 3 | Click email link | `GET /auth/verify?token=...` → session created, redirect to `next` param or `/dashboard/` |
| 4 | Dashboard | `GET /dashboard/` |
**Auth required:** No
**Language detection:** Auth routes have no `<lang>` prefix — lang detected from cookie or `Accept-Language` header via `@bp.before_request`
---
## 6. Returning User → Login
**Entry:** `/auth/login`
| Step | URL | Notes |
|------|-----|-------|
| 1 | `GET /auth/login` | Login form |
| 2 | Submit email | `POST /auth/login` → enqueues `send_magic_link` |
| 3 | Confirmation | `GET /auth/magic-link-sent` |
| 4 | Click email link | `GET /auth/verify?token=...` → session set, redirect |
| 5 | Dashboard or prior page | Session `user_id` set |
**Dev shortcut:** `GET /auth/dev-login?email=test@example.com` (DEBUG mode only) — instant login, used in tests
---
## 7. User → Save/Load Scenario
**Entry:** Planner while logged in
| Step | URL | Notes |
|------|-----|-------|
| 1 | Open scenarios panel | `GET /<lang>/planner/scenarios` HTMX partial — lists saved scenarios |
| 2 | Save current state | `POST /<lang>/planner/scenarios/save` (JSON body: `{name, state}`) → creates/updates scenario |
| 3 | Load scenario | `GET /<lang>/planner/scenarios/<id>` → returns scenario JSON → JS populates form |
| 4 | Set default | `POST /<lang>/planner/scenarios/<id>/default` |
| 5 | Delete | `DELETE /<lang>/planner/scenarios/<id>` |
**Auth required:** Yes (`@login_required`)
---
## 8. User → Export PDF
**Entry:** Planner → "Export" tab/button
| Step | URL | Notes |
|------|-----|-------|
| 1 | `GET /<lang>/planner/export` | Export options page (or waitlist if `WAITLIST_MODE=true`) |
| 2 | Checkout | `POST /<lang>/planner/export/checkout` → returns Paddle checkout URL (JSON) |
| 3 | Paddle checkout | External Paddle overlay/redirect |
| 4 | Post-checkout | `GET /<lang>/planner/export/success` |
| 5 | Download PDF | `GET /<lang>/planner/export/<id>` (checks subscription/purchase) |
**Auth required:** Yes (`@login_required`)
**Email sent:** `send_welcome` (if new user), PDF ready notification
---
## 9. Supplier → Signup
**Entry:** `/<lang>/suppliers/signup` or from nav "For Suppliers"
| Step | URL | Notes |
|------|-----|-------|
| 1 | `GET /<lang>/suppliers/signup` | Plan selection (or waitlist if `WAITLIST_MODE=true`) |
| 24 | `POST /<lang>/suppliers/signup/step/<n>` | HTMX wizard: step 2 = details, step 3 = credits, step 4 = contact |
| Checkout | `POST /<lang>/suppliers/signup/checkout` | Returns Paddle URL |
| Success | `GET /<lang>/suppliers/signup/success` | Post-checkout confirmation |
**Auth required:** No
**Plans:** `basic` (free listing), `growth` (leads), `pro` (pro listing + leads)
---
## 10. Supplier → Dashboard
**Entry:** Login → redirect to `/<lang>/suppliers/dashboard` (if supplier role)
| Tab | URL | Notes |
|-----|-----|-------|
| Overview | `GET /<lang>/suppliers/dashboard/overview` | Stats: views, leads, credits |
| Lead Feed | `GET /<lang>/suppliers/dashboard/leads` | Lead cards (teased for basic, unlockable for growth/pro) |
| Listing | `GET /<lang>/suppliers/dashboard/listing` | Edit supplier profile |
| Boosts | `GET /<lang>/suppliers/dashboard/boosts` | Purchase credit packs |
**Auth required:** Yes — `@_supplier_required` (basic+); lead tabs require `@_lead_tier_required` (growth/pro)
**Dashboard shell:** `GET /<lang>/suppliers/dashboard` — tabs loaded via HTMX
---
## 11. Supplier → Unlock Lead
**Entry:** Lead feed in supplier dashboard
| Step | URL | Notes |
|------|-----|-------|
| 1 | View teased lead | `GET /<lang>/suppliers/dashboard/leads` — lead shown with blurred contact info |
| 2 | Unlock | `POST /<lang>/suppliers/leads/<id>/unlock` — deducts 1 credit, reveals full lead |
| 3 | Receive email | `send_lead_forward_email` task enqueued — full project brief sent to supplier |
| 4 | Entrepreneur notified | `send_lead_matched_notification` task — notifies entrepreneur a supplier was matched |
**Auth required:** Yes — `@_lead_tier_required`
**Credit check:** Server-side check; if 0 credits → redirect to boosts tab
---
## 12. Admin Flows
**Entry:** `/admin/` (requires `@role_required("admin")`)
| Area | URL | What you can do |
|------|-----|-----------------|
| Dashboard | `GET /admin/` | Stats overview |
| Users | `GET /admin/users`, `/admin/users/<id>` | List, view, impersonate |
| Leads | `GET /admin/leads`, `/admin/leads/<id>` | List, filter, view detail, change status, forward to supplier, create |
| Suppliers | `GET /admin/suppliers`, `/admin/suppliers/<id>` | List, view, adjust credits, change tier, create |
| Feedback | `GET /admin/feedback` | View all submitted feedback |
| Email Sent Log | `GET /admin/emails`, `/admin/emails/<id>` | List all outgoing emails (filter by type/event/search), detail with API-enriched HTML preview |
| Email Inbox | `GET /admin/emails/inbox`, `/admin/emails/inbox/<id>` | Inbound emails (unread badge), detail with sandboxed HTML, inline reply |
| Email Compose | `GET /admin/emails/compose` | Send ad-hoc emails with from-address selection + optional branded wrapping |
| Audiences | `GET /admin/emails/audiences`, `/admin/emails/audiences/<id>/contacts` | Resend audiences, contact list, remove contacts |
| Article Templates | `GET /admin/templates` | CRUD + bulk generate articles from template+data |
| Published Scenarios | `GET /admin/scenarios` | CRUD public scenario cards (shown on landing) |
| Articles | `GET /admin/articles` | CRUD, publish/unpublish, rebuild HTML |
| Task Queue | `GET /admin/tasks` | View worker tasks, retry/delete failed |
**Dev shortcut:** `/auth/dev-login?email=<admin-email>` where email is in `config.ADMIN_EMAILS`
---
## Route Prefix Reference
| Blueprint | URL Prefix | Lang-prefixed? |
|-----------|------------|----------------|
| `public` | `/<lang>` | Yes |
| `planner` | `/<lang>/planner` | Yes |
| `directory` | `/<lang>/directory` | Yes |
| `leads` | `/<lang>/leads` | Yes |
| `suppliers` | `/<lang>/suppliers` | Yes |
| `content` | `/<lang>` (catch-all, registered last) | Yes |
| `auth` | `/auth` | No |
| `dashboard` | `/dashboard` | No |
| `billing` | `/billing` | No |
| `admin` | `/admin` | No |
| `webhooks` | `/webhooks` | No |
**Language detection for non-prefixed blueprints:** Cookie (`lang`) → `Accept-Language` header → fallback `"en"`
---
## Known Test Shortcuts
- **Dev login (no magic link):** `GET /auth/dev-login?email=...` (only when `DEBUG=True`)
- **Admin login:** `GET /auth/dev-login?email=<email-in-ADMIN_EMAILS>`
- **Quote verify URL pattern:** `GET /<lang>/leads/verify?token=...&lead=...`
- **Auth verify URL pattern:** `GET /auth/verify?token=...`