# Padelnomics

Plan, finance, and build your padel business.

## Quick Start

```bash
./scripts/dev_setup.sh   # one-time: deps, .env, migrations, seed data, CSS
./scripts/dev_run.sh     # start app + worker + CSS watcher
```

Then open:
- **App**: http://localhost:5000
- **Dev login**: http://localhost:5000/auth/dev-login?email=dev@localhost
- **Admin**: http://localhost:5000/admin (password: `admin`)

## Local Development Setup

### Prerequisites

- Python 3.12+
- [uv](https://docs.astral.sh/uv/) (Python package manager)
- WeasyPrint system dependencies (for PDF export):
  - Fedora/RHEL: `sudo dnf install pango gdk-pixbuf2 cairo`
  - Debian/Ubuntu: `sudo apt install libpango-1.0-0 libgdk-pixbuf2.0-0 libcairo2`
  - macOS: `brew install pango gdk-pixbuf cairo`

### 1. Install dependencies

```bash
cd padelnomics
uv sync
```

### 2. Configure environment

```bash
cp .env.example .env
# Edit .env if you want to change defaults — it works out of the box for dev
```

Key defaults for local dev:
- `DEBUG=true` — enables dev login, console email output, verbose errors
- `ADMIN_PASSWORD=admin` — admin panel login at `/admin`
- `DATABASE_PATH=data/app.db` — SQLite file, auto-created
- Paddle/Resend keys left blank — checkout overlay and email sending disabled,
  magic links print to console

### 3. Run database migrations

```bash
uv run python -m padelnomics.migrations.migrate
```

This creates `data/app.db` with all tables. Fresh databases get the full
schema in one shot; existing databases get incremental migrations applied.

### 4. (Optional) Set up Paddle sandbox products

Only needed if you want to test actual checkout flows:

```bash
# Add your Paddle sandbox keys to .env first:
# PADDLE_API_KEY=test_xxx
# PADDLE_CLIENT_TOKEN=test_xxx
# PADDLE_WEBHOOK_SECRET=pdl_ntfset_xxx

uv run python -m padelnomics.scripts.setup_paddle
```

This creates all products/prices in Paddle sandbox and writes the IDs to the
`paddle_products` table. Without this, checkout buttons will show an error
("Invalid plan selected") but everything else works.

### 5. Start the app

```bash
uv run python -m padelnomics.app
```

App runs at **http://localhost:5000**.

### 6. Start the background worker (separate terminal)

```bash
uv run python -m padelnomics.worker
```

The worker processes background tasks: emails, lead forwarding, PDF generation,
credit refills. Without it, queued tasks stay pending but the app still works.

### 7. CSS (Tailwind)

```bash
make css-watch   # rebuild on file changes (dev)
make css-build   # one-off minified build (CI/Docker)
```

First run downloads the Tailwind standalone CLI to `bin/tailwindcss`.

Edit `src/padelnomics/static/css/input.css` for theme tokens, base styles,
and component classes.

---

## Testing Each Feature Locally

### Authentication

1. Go to http://localhost:5000/auth/login
2. **Dev shortcut**: http://localhost:5000/auth/dev-login?email=test@example.com
   — instant login, no email needed (DEBUG mode only)
3. Magic links: submit any email → link prints to console (no Resend key needed)

### Financial Planner

1. Log in → http://localhost:5000/planner/
2. Create a scenario, fill in parameters, click Calculate
3. View results across tabs (Investment, Revenue, Cash Flow, Metrics)

### Business Plan PDF Export

1. Create at least one scenario with calculated results
2. Click "Export Business Plan (PDF)" in the planner sidebar, or go to
   http://localhost:5000/planner/export
3. **Without Paddle**: checkout will fail (no products in DB). To test the
   PDF generation directly, you can:
   - Insert a test export record in the DB:
     ```bash
     uv run python -c "
     import sqlite3
     conn = sqlite3.connect('data/app.db')
     conn.execute('''INSERT INTO business_plan_exports
       (user_id, scenario_id, language, status)
       VALUES (1, 1, 'en', 'pending')''')
     conn.commit()
     print('Export record created with id:', conn.execute('SELECT last_insert_rowid()').fetchone()[0])
     "
     ```
   - Then visit http://localhost:5000/planner/export (the generating page
     will show). The worker generates the PDF if running.
4. **With Paddle sandbox**: complete checkout → webhook triggers worker →
   PDF generated → download link appears

### Quote Request (Lead Submission)

1. Go to http://localhost:5000/leads/quote
2. Walk through the 9-step wizard
3. If logged in: lead submitted directly
4. If guest: verification email prints to console → click the link → lead
   goes live

### Supplier Directory

1. http://localhost:5000/directory/ — full-text search, tier-based ordering
2. Click a supplier → profile page at `/directory/<slug>`
3. Click tracking routes: `/directory/<slug>/website` and `/directory/<slug>/quote`
   redirect to supplier website / quote form

### Supplier Signup + Dashboard

1. Visit http://localhost:5000/suppliers — the "For Suppliers" landing page
2. Click "Get Started" on a plan → redirects to `/suppliers/signup`
3. Walk through the 4-step HTMX wizard (plan → boosts → credits → checkout)
4. **Without Paddle**: the final checkout step will fail. To simulate a
   signed-up supplier, insert test data:
   ```bash
   uv run python -c "
   import sqlite3
   conn = sqlite3.connect('data/app.db')
   # Update an existing supplier to be claimed by user 1 with growth tier
   conn.execute('''UPDATE suppliers SET
     claimed_by = 1, tier = 'growth', credit_balance = 30
     WHERE id = 1''')
   conn.commit()
   print('Supplier 1 claimed by user 1 with growth tier')
   "
   ```
5. Log in as that user → http://localhost:5000/suppliers/dashboard
6. Browse tabs: Overview, Lead Feed, My Listing, Boost & Upsells
7. Lead Feed: unlock leads (costs credits), filter by heat/country/timeline
8. My Listing: edit profile fields, upload logo
9. Boosts: purchase boosts and credit packs (needs Paddle for actual checkout)

### Admin Panel

1. Go to http://localhost:5000/admin
2. Log in with password: `admin` (default in dev)
3. **Dashboard**: user stats, lead funnel, supplier stats, task queue health
4. **Leads** (`/admin/leads`): filter by status/heat/country, view detail,
   update status, forward to supplier
5. **Suppliers** (`/admin/suppliers`): filter by tier/country/name, view detail
   with credit ledger + boost history, adjust credits, change tier
6. **Feedback** (`/admin/feedback`): view all feedback submissions
7. **Users** (`/admin/users`): search, impersonate users
8. **Tasks** (`/admin/tasks`): view/retry/delete background tasks

### Feedback Widget

1. Click the "Feedback" button in the top navbar (visible on every page)
2. Type a message → Send
3. View submissions at http://localhost:5000/admin/feedback

### Seeding Test Data

The database starts empty. To populate it with test data for development:

```bash
uv run python -c "
import sqlite3, json
conn = sqlite3.connect('data/app.db')

# Create a test supplier
conn.execute('''INSERT OR IGNORE INTO suppliers
  (name, slug, category, country_code, city, tier, credit_balance,
   short_description, website, contact_name, contact_email)
  VALUES
  ('Padel Pro Courts', 'padel-pro-courts', 'court_manufacturer', 'DE', 'Munich',
   'free', 0, 'Premium padel court manufacturer', 'https://example.com',
   'Max Mueller', 'max@example.com')''')

# Create a test lead
conn.execute('''INSERT OR IGNORE INTO lead_requests
  (lead_type, facility_type, court_count, glass_type, country, location,
   timeline, budget_estimate, heat_score, credit_cost, status,
   contact_name, contact_email, contact_phone, stakeholder_type,
   verified_at, build_context, financing_status)
  VALUES
  ('quote', 'indoor_rent', 6, 'panoramic', 'Germany', 'Berlin',
   '3-6 months', 450000, 'hot', 35, 'new',
   'John Doe', 'john@example.com', '+49123456', 'investor',
   datetime('now'), 'lease_signed', 'loan_approved')''')

conn.commit()
print('Test data seeded')
"
```

### Resend Test Emails

When `RESEND_API_KEY` is blank (default), all emails print to the console —
no Resend account needed.

When you have a Resend API key, you can use their test addresses to simulate
delivery outcomes without a verified domain:

| Address | Behavior |
|---------|----------|
| `delivered@resend.dev` | Accepted, simulates successful delivery |
| `bounced@resend.dev` | Simulates a hard bounce |
| `complained@resend.dev` | Simulates a spam complaint |
| `suppressed@resend.dev` | Simulates a suppressed recipient |

These support `+label` syntax (e.g. `delivered+test1@resend.dev`) for unique
recipients. You can also send **from** `onboarding@resend.dev` without a
verified domain.

---

## Waitlist Mode

Waitlist mode allows you to validate market demand before building features. When enabled, selected routes show waitlist signup forms instead of normal flows. Perfect for lean startup smoke tests and pre-launch validation.

### Quick Start

```bash
# In .env
WAITLIST_MODE=true
RESEND_AUDIENCE_WAITLIST=aud_xxx  # Optional: for bulk launch campaigns
```

Restart the app, then visit:
- http://localhost:5000/auth/signup — entrepreneur waitlist
- http://localhost:5000/suppliers/signup — supplier waitlist
- http://localhost:5000/planner/export — feature gate example

Submit an email → see confirmation → check the `waitlist` table:

```bash
uv run python -c "
import sqlite3
conn = sqlite3.connect('data/app.db')
rows = conn.execute('SELECT * FROM waitlist ORDER BY created_at DESC').fetchall()
for row in rows: print(row)
"
```

### How It Works

Two abstractions eliminate code duplication:

1. **`@waitlist_gate(template, **context)` decorator** — intercepts GET requests when `WAITLIST_MODE=true`
2. **`capture_waitlist_email(email, intent, plan)` helper** — handles DB + email + Resend

Example usage:

```python
from padelnomics.core import capture_waitlist_email, waitlist_gate

@bp.route("/signup", methods=["GET", "POST"])
@csrf_protect
@waitlist_gate("waitlist.html", plan=lambda: request.args.get("plan", "free"))
async def signup():
    if config.WAITLIST_MODE and request.method == "POST":
        email = (await request.form).get("email", "").strip().lower()
        await capture_waitlist_email(email, intent="signup")
        return await render_template("waitlist_confirmed.html", email=email)

    # Normal signup flow below...
```

### Features

- **Zero dependencies** — SQLite table + worker tasks, no SaaS required
- **Idempotent** — duplicate submissions ignored (same email + intent)
- **Silent failures** — DB/Resend errors don't break user flow
- **Resend integration** — auto-adds to audience for launch campaigns
- **Intent-based** — same email can sign up for multiple intents (entrepreneur + supplier)
- **Plan-aware** — captures plan selection for pricing validation
- **IP tracking** — records IP address for spam detection
- **Email confirmations** — sends plan-specific confirmation emails via worker

### Documentation

See **[docs/WAITLIST.md](docs/WAITLIST.md)** for:
- Full architecture explanation
- Adding waitlist to new routes
- Customizing email templates
- Database schema and queries
- Testing guide
- Troubleshooting
- Migration guide (refactoring duplicated code)
- FAQ

### Turn Off

```bash
# In .env
WAITLIST_MODE=false
```

Restart → all routes revert to normal flows.

---

## Architecture Overview

```
padelnomics/src/padelnomics/
├── app.py              # Application factory, blueprint registration
├── core.py             # Config, database, email, CSRF, rate limiting
├── credits.py          # Credit system (balance, ledger, unlock)
├── businessplan.py     # WeasyPrint PDF generation engine
├── worker.py           # SQLite-based background task queue
├── migrations/
│   ├── schema.sql      # Full schema (source of truth for fresh DBs)
│   ├── migrate.py      # Migration runner
│   └── versions/       # Incremental migrations (0001-0008)
├── scripts/
│   └── setup_paddle.py # Create Paddle products + write IDs to DB
├── templates/
│   ├── base.html       # Layout: nav, footer, HTMX, Paddle.js, Umami
│   └── businessplan/   # PDF templates (plan.html, plan.css)
├── static/             # CSS, JS, images
├── admin/              # Admin panel (dashboard, users, leads, suppliers, feedback)
├── auth/               # Magic link auth, dev-login
├── billing/            # Paddle checkout, webhooks, subscription management
├── dashboard/          # User dashboard
├── directory/          # Supplier directory (FTS5 search, profiles)
├── leads/              # Quote request wizard
├── planner/            # Financial planner + PDF export routes
├── public/             # Landing, marketing, legal, feedback endpoint
└── suppliers/          # Supplier signup wizard, dashboard (4 tabs), lead feed
```

### Key Patterns

- **HTMX everywhere**: server renders HTML partials, HTMX swaps them. No
  client-side state management. Forms use `hx-post`, filters use `hx-get`.
- **Paddle.js overlay checkout**: server returns JSON `{items, customData,
  settings}`, frontend calls `Paddle.Checkout.open()`. Webhook handles the rest.
- **Credit system**: heat-based pricing (hot=35, warm=20, cool=8). Suppliers
  unlock leads by spending credits. Ledger-based balance tracking.
- **SQLite + aiosqlite**: single-file database with WAL mode. Background
  worker uses same DB file.
- **Blueprints**: each domain has its own blueprint with template folder.

---

## Docker (Production)

```bash
docker compose up -d        # app + worker + scheduler + litestream
docker compose logs -f app  # tail logs
```

## CI/CD

Pull-based deployment via Gitea Actions — no SSH keys or deploy credentials in CI.

1. Push to master → Gitea Actions runs tests (`.gitea/workflows/ci.yaml`)
2. On success, CI creates tag `v<run_number>` using the built-in `github.token`
3. On-server supervisor polls for new tags every 60s and deploys automatically

**Server-side one-time setup:**
```bash
bash infra/setup_server.sh    # creates padelnomics_service user, keys, dirs
ssh root@<server> 'bash -s' < infra/bootstrap_supervisor.sh
```

1. `setup_server.sh` generates an ed25519 SSH deploy key — add the printed public key to Gitea:
   `git.padelnomics.io → padelnomics → Settings → Deploy Keys → Add key (read-only)`
2. Add the printed age public key to `.sops.yaml`, re-encrypt, commit + push
3. Run `bootstrap_supervisor.sh` — clones from `git.padelnomics.io:2222`, decrypts secrets, starts systemd supervisor
