From 4b7d4d5a746e1ed3b2631dc91a8eb6759ffe2300 Mon Sep 17 00:00:00 2001 From: Deeman Date: Thu, 19 Feb 2026 22:22:13 +0100 Subject: [PATCH 1/2] Update from Copier template v0.4.0 - Accept RBAC system: user_roles table, role_required decorator, grant_role/revoke_role/ensure_admin_role functions - Accept improved billing architecture: billing_customers table separation, provider-agnostic naming - Accept enhanced user loading with subscription/roles eager loading in app.py - Accept improved email templates with branded styling - Accept new infrastructure: migration tracking, transaction logging, A/B testing - Accept template improvements: Resend SDK, Tailwind build stage, UMAMI analytics config - Keep beanflows-specific configs: BASE_URL 5001, coffee PLAN_FEATURES/PLAN_LIMITS - Keep beanflows analytics integration and DuckDB health check - Add new test files and utility scripts from template Co-Authored-By: Claude Sonnet 4 --- web/CHANGELOG.md | 73 ++++++ web/Dockerfile | 11 + web/pyproject.toml | 3 +- web/src/beanflows/app.py | 62 +++-- web/src/beanflows/auth/routes.py | 146 +++++++---- web/src/beanflows/core.py | 120 ++++++--- web/src/beanflows/migrations/migrate.py | 104 +++++--- web/src/beanflows/migrations/schema.sql | 88 ++++++- web/src/beanflows/scripts/__init__.py | 0 web/src/beanflows/scripts/setup_paddle.py | 92 +++++++ .../static/fonts/CommitMono-400-Regular.woff2 | Bin 0 -> 68192 bytes .../static/fonts/CommitMono-700-Regular.woff2 | Bin 0 -> 68592 bytes .../static/fonts/CommitMono-LICENSE.txt | 90 +++++++ web/src/beanflows/worker.py | 107 +++++--- web/tests/conftest.py | 166 ++++++------ web/tests/test_billing_helpers.py | 191 +++++++++----- web/tests/test_billing_hooks.py | 122 +++++++++ web/tests/test_billing_routes.py | 83 +++--- web/tests/test_billing_webhooks.py | 33 ++- web/tests/test_roles.py | 242 ++++++++++++++++++ 20 files changed, 1346 insertions(+), 387 deletions(-) create mode 100644 web/CHANGELOG.md create mode 100644 web/src/beanflows/scripts/__init__.py create mode 100644 web/src/beanflows/scripts/setup_paddle.py create mode 100644 web/src/beanflows/static/fonts/CommitMono-400-Regular.woff2 create mode 100644 web/src/beanflows/static/fonts/CommitMono-700-Regular.woff2 create mode 100644 web/src/beanflows/static/fonts/CommitMono-LICENSE.txt create mode 100644 web/tests/test_billing_hooks.py create mode 100644 web/tests/test_roles.py diff --git a/web/CHANGELOG.md b/web/CHANGELOG.md new file mode 100644 index 0000000..702897c --- /dev/null +++ b/web/CHANGELOG.md @@ -0,0 +1,73 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## [Unreleased] + +### Changed +- **Role-based access control**: `user_roles` table with `role_required()` decorator replaces password-based admin auth +- **Admin is a real user**: admins authenticate via magic links; `ADMIN_EMAILS` env var auto-grants admin role on login +- **Separated billing entities**: `billing_customers` table holds payment provider identity; `subscriptions` table holds only subscription state +- **Multiple subscriptions per user**: dropped UNIQUE constraint on `subscriptions.user_id`; `upsert_subscription` finds by `provider_subscription_id` + +### Added +- Simple A/B testing with `@ab_test` decorator and optional Umami `data-tag` integration (`UMAMI_SCRIPT_URL` / `UMAMI_WEBSITE_ID` env vars) +- `user_roles` table and `grant_role()` / `revoke_role()` / `ensure_admin_role()` functions +- `billing_customers` table and `upsert_billing_customer()` / `get_billing_customer()` functions +- `role_required(*roles)` decorator in auth +- `is_admin` template context variable +- Migration `0001_roles_and_billing_customers.py` for existing databases + +### Removed +- `ADMIN_PASSWORD` env var and password-based admin login +- `provider_customer_id` column from `subscriptions` table +- `admin/templates/admin/login.html` + +### Changed +- **Provider-agnostic schema**: generic `provider_customer_id` / `provider_subscription_id` columns replace provider-prefixed names (`stripe_customer_id`, `paddle_customer_id`, `lemonsqueezy_customer_id`) — eliminates all Jinja conditionals from schema, SQL helpers, and route code +- **Consolidated `subscription_required` decorator**: single implementation in `auth/routes.py` supporting both plan and status checks, reads from eager-loaded `g.subscription` (zero extra queries) +- **Eager-loaded `g.subscription`**: `load_user` in `app.py` now fetches user + subscription in a single JOIN; available in all routes and templates via `g.subscription` + +### Added +- `transactions` table for recording payment/refund events with idempotent `record_transaction()` helper +- Billing event hook system: `on_billing_event()` decorator and `_fire_hooks()` for domain code to react to subscription changes; errors are logged and never cause webhook 500s + +### Removed +- Duplicate `subscription_required` decorator from `billing/routes.py` (consolidated in `auth/routes.py`) +- `get_user_with_subscription()` from `auth/routes.py` (replaced by eager-loaded `g.subscription`) + +### Changed +- **Email SDK migration**: replaced raw httpx calls with official `resend` SDK in `core.py` + - Added `from_addr` parameter to `send_email()` for multi-address support + - Added `EMAIL_ADDRESSES` dict for named sender addresses (transactional, etc.) +- **Paddle SDK migration**: replaced raw httpx calls with official `paddle-python-sdk` in `billing/routes.py` + - Checkout, manage, cancel routes now use typed SDK methods (`PaddleClient`, `CreateTransaction`) + - Webhook verification uses SDK's `Verifier` instead of hand-rolled HMAC + - Added `PADDLE_ENVIRONMENT` config for sandbox/production toggling + - Added `_paddle_client()` helper factory +- **Dependencies**: `resend` replaces `httpx` for email; `paddle-python-sdk` replaces `httpx` for Paddle billing; `httpx` now only included for LemonSqueezy projects +- Worker `send_email` task handler now passes through `from_addr` + +### Added +- `scripts/setup_paddle.py` — CLI script to create Paddle products/prices programmatically (Paddle projects only) + +### Changed +- **Pico CSS → Tailwind CSS v4** — full design system migration across all templates + - Standalone Tailwind CLI binary (no Node.js) with `make css-build` / `make css-watch` + - Brand theme with component classes (`.btn`, `.card`, `.form-input`, `.table`, `.badge`, `.flash`, etc.) + - Self-hosted Commit Mono font for monospace data display + - Docker multi-stage build: CSS compiled in dedicated stage before Python build + +### Removed +- Pico CSS CDN dependency +- `custom.css` (replaced by Tailwind `input.css` with `@layer components`) +- JetBrains Mono font (replaced by self-hosted Commit Mono) + +### Fixed +- Admin template collision: namespaced admin templates under `admin/` subdirectory to prevent Quart's template loader from resolving auth's `login.html` or dashboard's `index.html` instead of admin's +- Admin user detail: `stripe_customer_id` hardcoded regardless of payment provider — now uses provider-aware Copier conditional (Stripe/Paddle/LemonSqueezy) + +### Added +- Initial project scaffolded from quart_saas_boilerplate diff --git a/web/Dockerfile b/web/Dockerfile index 726d8f6..953875d 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,3 +1,13 @@ +# CSS build stage (Tailwind standalone CLI, no Node.js) +FROM debian:bookworm-slim AS css-build +ADD https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 /usr/local/bin/tailwindcss +RUN chmod +x /usr/local/bin/tailwindcss +WORKDIR /app +COPY src/ ./src/ +RUN tailwindcss -i ./src/beanflows/static/css/input.css \ + -o ./src/beanflows/static/css/output.css --minify + + # Build stage FROM python:3.12-slim AS build COPY --from=ghcr.io/astral-sh/uv:0.8 /uv /uvx /bin/ @@ -15,6 +25,7 @@ RUN useradd -m -u 1000 appuser WORKDIR /app RUN mkdir -p /app/data && chown -R appuser:appuser /app COPY --from=build --chown=appuser:appuser /app . +COPY --from=css-build /app/src/beanflows/static/css/output.css ./src/beanflows/static/css/output.css USER appuser ENV PYTHONUNBUFFERED=1 ENV DATABASE_PATH=/app/data/app.db diff --git a/web/pyproject.toml b/web/pyproject.toml index b5d3eea..3f7a002 100644 --- a/web/pyproject.toml +++ b/web/pyproject.toml @@ -12,8 +12,9 @@ dependencies = [ "aiosqlite>=0.19.0", "duckdb>=1.0.0", "httpx>=0.27.0", + "resend>=2.22.0", "python-dotenv>=1.0.0", - + "paddle-python-sdk>=1.13.0", "itsdangerous>=2.1.0", "jinja2>=3.1.0", "hypercorn>=0.17.0", diff --git a/web/src/beanflows/app.py b/web/src/beanflows/app.py index 1fc0648..b364725 100644 --- a/web/src/beanflows/app.py +++ b/web/src/beanflows/app.py @@ -12,24 +12,24 @@ from .core import close_db, config, get_csrf_token, init_db, setup_request_id def create_app() -> Quart: """Create and configure the Quart application.""" - + # Get package directory for templates pkg_dir = Path(__file__).parent - + app = Quart( __name__, template_folder=str(pkg_dir / "templates"), static_folder=str(pkg_dir / "static"), ) - + app.secret_key = config.SECRET_KEY - + # Session config app.config["SESSION_COOKIE_SECURE"] = not config.DEBUG app.config["SESSION_COOKIE_HTTPONLY"] = True app.config["SESSION_COOKIE_SAMESITE"] = "Lax" app.config["PERMANENT_SESSION_LIFETIME"] = 60 * 60 * 24 * config.SESSION_LIFETIME_DAYS - + # Database lifecycle @app.before_serving async def startup(): @@ -41,7 +41,7 @@ def create_app() -> Quart: async def shutdown(): close_analytics_db() await close_db() - + # Security headers @app.after_request async def add_security_headers(response): @@ -51,16 +51,42 @@ def create_app() -> Quart: if not config.DEBUG: response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" return response - - # Load current user before each request + + # Load current user + subscription + roles before each request @app.before_request async def load_user(): g.user = None + g.subscription = None user_id = session.get("user_id") if user_id: - from .auth.routes import get_user_by_id - g.user = await get_user_by_id(user_id) - + from .core import fetch_one as _fetch_one + row = await _fetch_one( + """SELECT u.*, + bc.provider_customer_id, + (SELECT GROUP_CONCAT(role) FROM user_roles WHERE user_id = u.id) AS roles_csv, + s.id AS sub_id, s.plan, s.status AS sub_status, + s.provider_subscription_id, s.current_period_end + FROM users u + LEFT JOIN billing_customers bc ON bc.user_id = u.id + LEFT JOIN subscriptions s ON s.id = ( + SELECT id FROM subscriptions + WHERE user_id = u.id + ORDER BY created_at DESC LIMIT 1 + ) + WHERE u.id = ? AND u.deleted_at IS NULL""", + (user_id,), + ) + if row: + g.user = dict(row) + g.user["roles"] = row["roles_csv"].split(",") if row["roles_csv"] else [] + if row["sub_id"]: + g.subscription = { + "id": row["sub_id"], "plan": row["plan"], + "status": row["sub_status"], + "provider_subscription_id": row["provider_subscription_id"], + "current_period_end": row["current_period_end"], + } + # Template context globals @app.context_processor def inject_globals(): @@ -68,10 +94,14 @@ def create_app() -> Quart: return { "config": config, "user": g.get("user"), + "subscription": g.get("subscription"), + "is_admin": "admin" in (g.get("user") or {}).get("roles", []), "now": datetime.utcnow(), "csrf_token": get_csrf_token, + "ab_variant": getattr(g, "ab_variant", None), + "ab_tag": getattr(g, "ab_tag", None), } - + # Health check @app.route("/health") async def health(): @@ -94,7 +124,7 @@ def create_app() -> Quart: result["duckdb"] = "not configured" status_code = 200 if result["status"] == "healthy" else 500 return result, status_code - + # Register blueprints from .admin.routes import bp as admin_bp from .api.routes import bp as api_bp @@ -102,17 +132,17 @@ def create_app() -> Quart: from .billing.routes import bp as billing_bp from .dashboard.routes import bp as dashboard_bp from .public.routes import bp as public_bp - + app.register_blueprint(public_bp) app.register_blueprint(auth_bp) app.register_blueprint(dashboard_bp) app.register_blueprint(billing_bp) app.register_blueprint(api_bp, url_prefix="/api/v1") app.register_blueprint(admin_bp) - + # Request ID tracking setup_request_id(app) - + return app diff --git a/web/src/beanflows/auth/routes.py b/web/src/beanflows/auth/routes.py index 8a6af9a..dc7fc98 100644 --- a/web/src/beanflows/auth/routes.py +++ b/web/src/beanflows/auth/routes.py @@ -71,7 +71,7 @@ async def get_valid_token(token: str) -> dict | None: """Get token if valid and not expired.""" return await fetch_one( """ - SELECT at.*, u.email + SELECT at.*, u.email FROM auth_tokens at JOIN users u ON u.id = at.user_id WHERE at.token = ? AND at.expires_at > ? AND at.used_at IS NULL @@ -88,19 +88,6 @@ async def mark_token_used(token_id: int) -> None: ) -async def get_user_with_subscription(user_id: int) -> dict | None: - """Get user with their active subscription info.""" - return await fetch_one( - """ - SELECT u.*, s.plan, s.status as sub_status, s.current_period_end - FROM users u - LEFT JOIN subscriptions s ON s.user_id = u.id AND s.status = 'active' - WHERE u.id = ? AND u.deleted_at IS NULL - """, - (user_id,) - ) - - # ============================================================================= # Decorators # ============================================================================= @@ -116,24 +103,69 @@ def login_required(f): return decorated -def subscription_required(plans: list[str] = None): - """Require active subscription, optionally of specific plan(s).""" +def role_required(*roles): + """Require user to have at least one of the given roles.""" + def decorator(f): + @wraps(f) + async def decorated(*args, **kwargs): + if not g.get("user"): + await flash("Please sign in to continue.", "warning") + return redirect(url_for("auth.login", next=request.path)) + user_roles = g.user.get("roles", []) + if not any(r in user_roles for r in roles): + await flash("You don't have permission to access that page.", "error") + return redirect(url_for("dashboard.index")) + return await f(*args, **kwargs) + return decorated + return decorator + + +async def grant_role(user_id: int, role: str) -> None: + """Grant a role to a user (idempotent).""" + await execute( + "INSERT OR IGNORE INTO user_roles (user_id, role) VALUES (?, ?)", + (user_id, role), + ) + + +async def revoke_role(user_id: int, role: str) -> None: + """Revoke a role from a user.""" + await execute( + "DELETE FROM user_roles WHERE user_id = ? AND role = ?", + (user_id, role), + ) + + +async def ensure_admin_role(user_id: int, email: str) -> None: + """Grant admin role if email is in ADMIN_EMAILS.""" + if email.lower() in config.ADMIN_EMAILS: + await grant_role(user_id, "admin") + + +def subscription_required( + plans: list[str] = None, + allowed: tuple[str, ...] = ("active", "on_trial", "cancelled"), +): + """Require active subscription, optionally of specific plan(s) and/or statuses. + + Reads from g.subscription (eager-loaded in load_user) — zero extra queries. + """ def decorator(f): @wraps(f) async def decorated(*args, **kwargs): if not g.get("user"): await flash("Please sign in to continue.", "warning") return redirect(url_for("auth.login")) - - user = await get_user_with_subscription(g.user["id"]) - if not user or not user.get("plan"): + + sub = g.get("subscription") + if not sub or sub["status"] not in allowed: await flash("Please subscribe to access this feature.", "warning") return redirect(url_for("billing.pricing")) - - if plans and user["plan"] not in plans: + + if plans and sub["plan"] not in plans: await flash(f"This feature requires a {' or '.join(plans)} plan.", "warning") return redirect(url_for("billing.pricing")) - + return await f(*args, **kwargs) return decorated return decorator @@ -149,33 +181,33 @@ async def login(): """Login page - request magic link.""" if g.get("user"): return redirect(url_for("dashboard.index")) - + if request.method == "POST": form = await request.form email = form.get("email", "").strip().lower() - + if not email or "@" not in email: await flash("Please enter a valid email address.", "error") return redirect(url_for("auth.login")) - + # Get or create user user = await get_user_by_email(email) if not user: user_id = await create_user(email) else: user_id = user["id"] - + # Create magic link token token = secrets.token_urlsafe(32) await create_auth_token(user_id, token) - + # Queue email from ..worker import enqueue await enqueue("send_magic_link", {"email": email, "token": token}) - + await flash("Check your email for the sign-in link!", "success") return redirect(url_for("auth.magic_link_sent", email=email)) - + return await render_template("login.html") @@ -185,39 +217,39 @@ async def signup(): """Signup page - same as login but with different messaging.""" if g.get("user"): return redirect(url_for("dashboard.index")) - + plan = request.args.get("plan", "free") - + if request.method == "POST": form = await request.form email = form.get("email", "").strip().lower() selected_plan = form.get("plan", "free") - + if not email or "@" not in email: await flash("Please enter a valid email address.", "error") return redirect(url_for("auth.signup", plan=selected_plan)) - + # Check if user exists user = await get_user_by_email(email) if user: await flash("Account already exists. Please sign in.", "info") return redirect(url_for("auth.login")) - + # Create user user_id = await create_user(email) - + # Create magic link token token = secrets.token_urlsafe(32) await create_auth_token(user_id, token) - + # Queue emails from ..worker import enqueue await enqueue("send_magic_link", {"email": email, "token": token}) await enqueue("send_welcome", {"email": email}) - + await flash("Check your email to complete signup!", "success") return redirect(url_for("auth.magic_link_sent", email=email)) - + return await render_template("signup.html", plan=plan) @@ -225,29 +257,32 @@ async def signup(): async def verify(): """Verify magic link token.""" token = request.args.get("token") - + if not token: await flash("Invalid or expired link.", "error") return redirect(url_for("auth.login")) - + token_data = await get_valid_token(token) - + if not token_data: await flash("Invalid or expired link. Please request a new one.", "error") return redirect(url_for("auth.login")) - + # Mark token as used await mark_token_used(token_data["id"]) - + # Update last login await update_user(token_data["user_id"], last_login_at=datetime.utcnow().isoformat()) - + # Set session session.permanent = True session["user_id"] = token_data["user_id"] - + + # Auto-grant admin role if email is in ADMIN_EMAILS + await ensure_admin_role(token_data["user_id"], token_data["email"]) + await flash("Successfully signed in!", "success") - + # Redirect to intended page or dashboard next_url = request.args.get("next", url_for("dashboard.index")) return redirect(next_url) @@ -274,18 +309,21 @@ async def dev_login(): """Instant login for development. Only works in DEBUG mode.""" if not config.DEBUG: return "Not available", 404 - + email = request.args.get("email", "dev@localhost") - + user = await get_user_by_email(email) if not user: user_id = await create_user(email) else: user_id = user["id"] - + session.permanent = True session["user_id"] = user_id - + + # Auto-grant admin role if email is in ADMIN_EMAILS + await ensure_admin_role(user_id, email) + await flash(f"Dev login as {email}", "success") return redirect(url_for("dashboard.index")) @@ -296,19 +334,19 @@ async def resend(): """Resend magic link.""" form = await request.form email = form.get("email", "").strip().lower() - + if not email: await flash("Email address required.", "error") return redirect(url_for("auth.login")) - + user = await get_user_by_email(email) if user: token = secrets.token_urlsafe(32) await create_auth_token(user["id"], token) - + from ..worker import enqueue await enqueue("send_magic_link", {"email": email, "token": token}) - + # Always show success (don't reveal if email exists) await flash("If that email is registered, we've sent a new link.", "success") return redirect(url_for("auth.magic_link_sent", email=email)) diff --git a/web/src/beanflows/core.py b/web/src/beanflows/core.py index 2479d73..62e9d1f 100644 --- a/web/src/beanflows/core.py +++ b/web/src/beanflows/core.py @@ -2,16 +2,19 @@ Core infrastructure: database, config, email, and shared utilities. """ import os +import random import secrets import hashlib import hmac + import aiosqlite +import resend import httpx from pathlib import Path from functools import wraps from datetime import datetime, timedelta from contextvars import ContextVar -from quart import request, session, g +from quart import g, make_response, request, session from dotenv import load_dotenv # web/.env is three levels up from web/src/beanflows/core.py @@ -26,27 +29,35 @@ class Config: SECRET_KEY: str = os.getenv("SECRET_KEY", "change-me-in-production") BASE_URL: str = os.getenv("BASE_URL", "http://localhost:5001") DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true" - + DATABASE_PATH: str = os.getenv("DATABASE_PATH", "data/app.db") - + MAGIC_LINK_EXPIRY_MINUTES: int = int(os.getenv("MAGIC_LINK_EXPIRY_MINUTES", "15")) SESSION_LIFETIME_DAYS: int = int(os.getenv("SESSION_LIFETIME_DAYS", "30")) - + PAYMENT_PROVIDER: str = "paddle" - + PADDLE_API_KEY: str = os.getenv("PADDLE_API_KEY", "") PADDLE_WEBHOOK_SECRET: str = os.getenv("PADDLE_WEBHOOK_SECRET", "") + PADDLE_ENVIRONMENT: str = os.getenv("PADDLE_ENVIRONMENT", "sandbox") PADDLE_PRICES: dict = { "starter": os.getenv("PADDLE_PRICE_STARTER", ""), "pro": os.getenv("PADDLE_PRICE_PRO", ""), } - + + UMAMI_SCRIPT_URL: str = os.getenv("UMAMI_SCRIPT_URL", "") + UMAMI_WEBSITE_ID: str = os.getenv("UMAMI_WEBSITE_ID", "") + RESEND_API_KEY: str = os.getenv("RESEND_API_KEY", "") EMAIL_FROM: str = os.getenv("EMAIL_FROM", "hello@example.com") - + + ADMIN_EMAILS: list[str] = [ + e.strip().lower() for e in os.getenv("ADMIN_EMAILS", "").split(",") if e.strip() + ] + RATE_LIMIT_REQUESTS: int = int(os.getenv("RATE_LIMIT_REQUESTS", "100")) RATE_LIMIT_WINDOW: int = int(os.getenv("RATE_LIMIT_WINDOW", "60")) - + PLAN_FEATURES: dict = { "free": ["dashboard", "coffee_only", "limited_history"], "starter": ["dashboard", "coffee_only", "full_history", "export", "api"], @@ -74,10 +85,10 @@ async def init_db(path: str = None) -> None: global _db db_path = path or config.DATABASE_PATH Path(db_path).parent.mkdir(parents=True, exist_ok=True) - + _db = await aiosqlite.connect(db_path) _db.row_factory = aiosqlite.Row - + await _db.execute("PRAGMA journal_mode=WAL") await _db.execute("PRAGMA foreign_keys=ON") await _db.execute("PRAGMA busy_timeout=5000") @@ -137,11 +148,11 @@ async def execute_many(sql: str, params_list: list[tuple]) -> None: class transaction: """Async context manager for transactions.""" - + async def __aenter__(self): self.db = await get_db() return self.db - + async def __aexit__(self, exc_type, exc_val, exc_tb): if exc_type is None: await self.db.commit() @@ -153,25 +164,32 @@ class transaction: # Email # ============================================================================= -async def send_email(to: str, subject: str, html: str, text: str = None) -> bool: - """Send email via Resend API.""" +EMAIL_ADDRESSES = { + "transactional": f"{config.APP_NAME} <{config.EMAIL_FROM}>", +} + + +async def send_email( + to: str, subject: str, html: str, text: str = None, from_addr: str = None +) -> bool: + """Send email via Resend SDK.""" if not config.RESEND_API_KEY: print(f"[EMAIL] Would send to {to}: {subject}") return True - - async with httpx.AsyncClient() as client: - response = await client.post( - "https://api.resend.com/emails", - headers={"Authorization": f"Bearer {config.RESEND_API_KEY}"}, - json={ - "from": config.EMAIL_FROM, - "to": to, - "subject": subject, - "html": html, - "text": text or html, - }, - ) - return response.status_code == 200 + + resend.api_key = config.RESEND_API_KEY + try: + resend.Emails.send({ + "from": from_addr or config.EMAIL_FROM, + "to": to, + "subject": subject, + "html": html, + "text": text or html, + }) + return True + except Exception as e: + print(f"[EMAIL] Error sending to {to}: {e}") + return False # ============================================================================= # CSRF Protection @@ -214,34 +232,34 @@ async def check_rate_limit(key: str, limit: int = None, window: int = None) -> t window = window or config.RATE_LIMIT_WINDOW now = datetime.utcnow() window_start = now - timedelta(seconds=window) - + # Clean old entries and count recent await execute( "DELETE FROM rate_limits WHERE key = ? AND timestamp < ?", (key, window_start.isoformat()) ) - + result = await fetch_one( "SELECT COUNT(*) as count FROM rate_limits WHERE key = ? AND timestamp > ?", (key, window_start.isoformat()) ) count = result["count"] if result else 0 - + info = { "limit": limit, "remaining": max(0, limit - count - 1), "reset": int((window_start + timedelta(seconds=window)).timestamp()), } - + if count >= limit: return False, info - + # Record this request await execute( "INSERT INTO rate_limits (key, timestamp) VALUES (?, ?)", (key, now.isoformat()) ) - + return True, info @@ -254,13 +272,13 @@ def rate_limit(limit: int = None, window: int = None, key_func=None): key = key_func() else: key = f"ip:{request.remote_addr}" - + allowed, info = await check_rate_limit(key, limit, window) - + if not allowed: response = {"error": "Rate limit exceeded", **info} return response, 429 - + return await f(*args, **kwargs) return decorated return decorator @@ -284,7 +302,7 @@ def setup_request_id(app): rid = request.headers.get("X-Request-ID") or secrets.token_hex(8) request_id_var.set(rid) g.request_id = rid - + @app.after_request async def add_request_id_header(response): response.headers["X-Request-ID"] = get_request_id() @@ -294,13 +312,11 @@ def setup_request_id(app): # Webhook Signature Verification # ============================================================================= - def verify_hmac_signature(payload: bytes, signature: str, secret: str) -> bool: """Verify HMAC-SHA256 webhook signature.""" expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() return hmac.compare_digest(signature, expected) - # ============================================================================= # Soft Delete Helpers # ============================================================================= @@ -336,3 +352,27 @@ async def purge_deleted(table: str, days: int = 30) -> int: f"DELETE FROM {table} WHERE deleted_at IS NOT NULL AND deleted_at < ?", (cutoff,) ) + + +# ============================================================================= +# A/B Testing +# ============================================================================= + +def ab_test(experiment: str, variants: tuple = ("control", "treatment")): + """Assign visitor to an A/B test variant via cookie, tag Umami pageviews.""" + def decorator(f): + @wraps(f) + async def wrapper(*args, **kwargs): + cookie_key = f"ab_{experiment}" + assigned = request.cookies.get(cookie_key) + if assigned not in variants: + assigned = random.choice(variants) + + g.ab_variant = assigned + g.ab_tag = f"{experiment}-{assigned}" + + response = await make_response(await f(*args, **kwargs)) + response.set_cookie(cookie_key, assigned, max_age=30 * 24 * 60 * 60) + return response + return wrapper + return decorator \ No newline at end of file diff --git a/web/src/beanflows/migrations/migrate.py b/web/src/beanflows/migrations/migrate.py index 05aee3d..0c59397 100644 --- a/web/src/beanflows/migrations/migrate.py +++ b/web/src/beanflows/migrations/migrate.py @@ -1,51 +1,95 @@ """ -Simple migration runner. Runs schema.sql against the database. -""" -import sqlite3 -from pathlib import Path -import os -import sys +Sequential migration runner. + +Replays all migrations in order. All databases — fresh and existing — +go through the same path. No schema.sql fast-path. + +- Scans versions/ for NNNN_*.py files and runs unapplied ones in order +- Each migration has an up(conn) function receiving an uncommitted connection +- All pending migrations share a single transaction (batch atomicity) +""" + +import importlib +import os +import re +import sqlite3 +import sys +from pathlib import Path -# Add parent to path for imports sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) from dotenv import load_dotenv load_dotenv() +VERSIONS_DIR = Path(__file__).parent / "versions" +VERSION_RE = re.compile(r"^(\d{4})_.+\.py$") -def migrate(): - """Run migrations.""" - # Get database path from env or default - db_path = os.getenv("DATABASE_PATH", "data/app.db") - - # Ensure directory exists +# Derived from the package path: …/src//migrations/migrate.py +_PACKAGE = Path(__file__).parent.parent.name # e.g. "myproject" + + +def _discover_versions(): + """Return sorted list of version file stems.""" + if not VERSIONS_DIR.is_dir(): + return [] + versions = [] + for f in sorted(VERSIONS_DIR.iterdir()): + if VERSION_RE.match(f.name): + versions.append(f.stem) + return versions + + +def migrate(db_path=None): + if db_path is None: + db_path = os.getenv("DATABASE_PATH", "data/app.db") Path(db_path).parent.mkdir(parents=True, exist_ok=True) - - # Read schema - schema_path = Path(__file__).parent / "schema.sql" - schema = schema_path.read_text() - - # Connect and execute + conn = sqlite3.connect(db_path) - - # Enable WAL mode conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA foreign_keys=ON") - - # Run schema - conn.executescript(schema) + + # Ensure tracking table exists before anything else + conn.execute(""" + CREATE TABLE IF NOT EXISTS _migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) + ) + """) conn.commit() - - print(f"✓ Migrations complete: {db_path}") - - # Show tables + + versions = _discover_versions() + applied = { + row[0] + for row in conn.execute("SELECT name FROM _migrations").fetchall() + } + pending = [v for v in versions if v not in applied] + + if pending: + for name in pending: + print(f" Applying {name}...") + mod = importlib.import_module( + f"{_PACKAGE}.migrations.versions.{name}" + ) + mod.up(conn) + conn.execute( + "INSERT INTO _migrations (name) VALUES (?)", (name,) + ) + conn.commit() + print(f"✓ Applied {len(pending)} migration(s): {db_path}") + else: + print(f"✓ All migrations already applied: {db_path}") + + # Show tables (excluding internal sqlite/fts tables) cursor = conn.execute( - "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" + "SELECT name FROM sqlite_master WHERE type='table'" + " AND name NOT LIKE 'sqlite_%'" + " ORDER BY name" ) tables = [row[0] for row in cursor.fetchall()] print(f" Tables: {', '.join(tables)}") - + conn.close() diff --git a/web/src/beanflows/migrations/schema.sql b/web/src/beanflows/migrations/schema.sql index 2305e2c..f3c44f8 100644 --- a/web/src/beanflows/migrations/schema.sql +++ b/web/src/beanflows/migrations/schema.sql @@ -1,6 +1,13 @@ -- BeanFlows Database Schema -- Run with: python -m beanflows.migrations.migrate +-- Migration tracking +CREATE TABLE IF NOT EXISTS _migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) +); + -- Users CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -28,25 +35,58 @@ CREATE TABLE IF NOT EXISTS auth_tokens ( CREATE INDEX IF NOT EXISTS idx_auth_tokens_token ON auth_tokens(token); CREATE INDEX IF NOT EXISTS idx_auth_tokens_user ON auth_tokens(user_id); +-- User Roles +CREATE TABLE IF NOT EXISTS user_roles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role TEXT NOT NULL, + granted_at TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE(user_id, role) +); +CREATE INDEX IF NOT EXISTS idx_user_roles_user ON user_roles(user_id); +CREATE INDEX IF NOT EXISTS idx_user_roles_role ON user_roles(role); + +-- Billing Customers (payment provider identity, separate from subscriptions) +CREATE TABLE IF NOT EXISTS billing_customers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL UNIQUE REFERENCES users(id), + provider_customer_id TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); +CREATE INDEX IF NOT EXISTS idx_billing_customers_user ON billing_customers(user_id); +CREATE INDEX IF NOT EXISTS idx_billing_customers_provider ON billing_customers(provider_customer_id); + -- Subscriptions CREATE TABLE IF NOT EXISTS subscriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL UNIQUE REFERENCES users(id), + user_id INTEGER NOT NULL REFERENCES users(id), plan TEXT NOT NULL DEFAULT 'free', status TEXT NOT NULL DEFAULT 'free', - - paddle_customer_id TEXT, - paddle_subscription_id TEXT, - + provider_subscription_id TEXT, current_period_end TEXT, created_at TEXT NOT NULL, updated_at TEXT ); CREATE INDEX IF NOT EXISTS idx_subscriptions_user ON subscriptions(user_id); +CREATE INDEX IF NOT EXISTS idx_subscriptions_provider ON subscriptions(provider_subscription_id); -CREATE INDEX IF NOT EXISTS idx_subscriptions_provider ON subscriptions(paddle_subscription_id); +-- Transactions +CREATE TABLE IF NOT EXISTS transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id), + subscription_id INTEGER REFERENCES subscriptions(id), + provider_transaction_id TEXT UNIQUE, + type TEXT NOT NULL DEFAULT 'payment', + amount_cents INTEGER, + currency TEXT DEFAULT 'USD', + status TEXT NOT NULL DEFAULT 'pending', + metadata TEXT, + created_at TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_transactions_user ON transactions(user_id); +CREATE INDEX IF NOT EXISTS idx_transactions_provider ON transactions(provider_transaction_id); -- API Keys CREATE TABLE IF NOT EXISTS api_keys ( @@ -99,3 +139,39 @@ CREATE TABLE IF NOT EXISTS tasks ( ); CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status, run_at); + +-- Items (example domain entity - replace with your domain) +CREATE TABLE IF NOT EXISTS items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id), + name TEXT NOT NULL, + data TEXT, + created_at TEXT NOT NULL, + updated_at TEXT, + deleted_at TEXT +); + +CREATE INDEX IF NOT EXISTS idx_items_user ON items(user_id); +CREATE INDEX IF NOT EXISTS idx_items_deleted ON items(deleted_at); + +-- Full-text search for items (optional) +CREATE VIRTUAL TABLE IF NOT EXISTS items_fts USING fts5( + name, + data, + content='items', + content_rowid='id' +); + +-- FTS triggers +CREATE TRIGGER IF NOT EXISTS items_ai AFTER INSERT ON items BEGIN + INSERT INTO items_fts(rowid, name, data) VALUES (new.id, new.name, new.data); +END; + +CREATE TRIGGER IF NOT EXISTS items_ad AFTER DELETE ON items BEGIN + INSERT INTO items_fts(items_fts, rowid, name, data) VALUES('delete', old.id, old.name, old.data); +END; + +CREATE TRIGGER IF NOT EXISTS items_au AFTER UPDATE ON items BEGIN + INSERT INTO items_fts(items_fts, rowid, name, data) VALUES('delete', old.id, old.name, old.data); + INSERT INTO items_fts(rowid, name, data) VALUES (new.id, new.name, new.data); +END; \ No newline at end of file diff --git a/web/src/beanflows/scripts/__init__.py b/web/src/beanflows/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/src/beanflows/scripts/setup_paddle.py b/web/src/beanflows/scripts/setup_paddle.py new file mode 100644 index 0000000..9463d07 --- /dev/null +++ b/web/src/beanflows/scripts/setup_paddle.py @@ -0,0 +1,92 @@ + +""" +Create Paddle products and prices for BeanFlows. + +Run once per environment (sandbox, then production). +Prints resulting price IDs as a .env snippet. + +Usage: + uv run python -m beanflows.scripts.setup_paddle +""" + +import os +import sys + +from dotenv import load_dotenv +from paddle_billing import Client as PaddleClient +from paddle_billing import Environment, Options +from paddle_billing.Entities.Shared import CurrencyCode, Money, TaxCategory +from paddle_billing.Resources.Prices.Operations import CreatePrice +from paddle_billing.Resources.Products.Operations import CreateProduct + +load_dotenv() + +PADDLE_API_KEY = os.getenv("PADDLE_API_KEY", "") +PADDLE_ENVIRONMENT = os.getenv("PADDLE_ENVIRONMENT", "sandbox") + +if not PADDLE_API_KEY: + print("ERROR: Set PADDLE_API_KEY in .env first") + sys.exit(1) + + +PRODUCTS = [ + # Subscriptions + { + "name": "Starter", + "env_key": "PADDLE_PRICE_STARTER", + "price": 900, + "currency": CurrencyCode.USD, + "interval": "month", + "type": "subscription", + }, + { + "name": "Pro", + "env_key": "PADDLE_PRICE_PRO", + "price": 2900, + "currency": CurrencyCode.USD, + "interval": "month", + "type": "subscription", + }, +] + + +def main(): + env = Environment.SANDBOX if PADDLE_ENVIRONMENT == "sandbox" else Environment.PRODUCTION + paddle = PaddleClient(PADDLE_API_KEY, options=Options(env)) + + print(f"Creating products in {PADDLE_ENVIRONMENT}...\n") + + env_lines = [] + + for spec in PRODUCTS: + # Create product + product = paddle.products.create(CreateProduct( + name=spec["name"], + tax_category=TaxCategory.Standard, + )) + print(f" Product: {spec['name']} -> {product.id}") + + # Create price + price_kwargs = { + "description": spec["name"], + "product_id": product.id, + "unit_price": Money(str(spec["price"]), spec["currency"]), + } + + if spec["type"] == "subscription": + from paddle_billing.Entities.Shared import TimePeriod + price_kwargs["billing_cycle"] = TimePeriod(interval="month", frequency=1) + + price = paddle.prices.create(CreatePrice(**price_kwargs)) + print(f" Price: {spec['env_key']} = {price.id}") + + env_lines.append(f"{spec['env_key']}={price.id}") + + print("\n# --- .env snippet ---") + for line in env_lines: + print(line) + + +if __name__ == "__main__": + main() + diff --git a/web/src/beanflows/static/fonts/CommitMono-400-Regular.woff2 b/web/src/beanflows/static/fonts/CommitMono-400-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..011d67c29f2abe6af6debad84215b5e671802287 GIT binary patch literal 68192 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

D&W$RosfoV?eU@0unLlo2r1#B)2im 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 ZOlw%aSsJClP9WBE&mYCL4fioX{{luYK#%|c literal 0 HcmV?d00001 diff --git a/web/src/beanflows/static/fonts/CommitMono-700-Regular.woff2 b/web/src/beanflows/static/fonts/CommitMono-700-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..af84f79518af889ce597a62c71da07e76f40a9e8 GIT binary patch literal 68592 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 ZLkK44(Vr;F#)I<=7HVYok*Bt_{{?w)I(z^C literal 0 HcmV?d00001 diff --git a/web/src/beanflows/static/fonts/CommitMono-LICENSE.txt b/web/src/beanflows/static/fonts/CommitMono-LICENSE.txt new file mode 100644 index 0000000..96d39dd --- /dev/null +++ b/web/src/beanflows/static/fonts/CommitMono-LICENSE.txt @@ -0,0 +1,90 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/web/src/beanflows/worker.py b/web/src/beanflows/worker.py index ca42a93..bf8d066 100644 --- a/web/src/beanflows/worker.py +++ b/web/src/beanflows/worker.py @@ -13,6 +13,46 @@ from .core import config, init_db, fetch_one, fetch_all, execute, send_email HANDLERS: dict[str, callable] = {} +def _email_wrap(body: str) -> str: + """Wrap email body in a branded layout with inline CSS.""" + return f"""\ + + + + + + +
+ + + + + + + +
+ {config.APP_NAME} +
+ {body} +
+ © {config.APP_NAME} · You received this because you have an account. +
+
+ +""" + + +def _email_button(url: str, label: str) -> str: + """Render a branded CTA button for email.""" + return ( + f'' + f'
' + f'' + f'{label}
' + ) + + def task(name: str): """Decorator to register a task handler.""" def decorator(f): @@ -46,7 +86,7 @@ async def get_pending_tasks(limit: int = 10) -> list[dict]: now = datetime.utcnow().isoformat() return await fetch_all( """ - SELECT * FROM tasks + SELECT * FROM tasks WHERE status = 'pending' AND run_at <= ? ORDER BY run_at ASC LIMIT ? @@ -66,15 +106,15 @@ async def mark_complete(task_id: int) -> None: async def mark_failed(task_id: int, error: str, retries: int) -> None: """Mark task as failed, schedule retry if attempts remain.""" max_retries = 3 - + if retries < max_retries: # Exponential backoff: 1min, 5min, 25min delay = timedelta(minutes=5 ** retries) run_at = datetime.utcnow() + delay - + await execute( """ - UPDATE tasks + UPDATE tasks SET status = 'pending', error = ?, retries = ?, run_at = ? WHERE id = ? """, @@ -99,6 +139,7 @@ async def handle_send_email(payload: dict) -> None: subject=payload["subject"], html=payload["html"], text=payload.get("text"), + from_addr=payload.get("from_addr"), ) @@ -106,35 +147,37 @@ async def handle_send_email(payload: dict) -> None: async def handle_send_magic_link(payload: dict) -> None: """Send magic link email.""" link = f"{config.BASE_URL}/auth/verify?token={payload['token']}" - - html = f""" -

Sign in to {config.APP_NAME}

-

Click the link below to sign in:

-

{link}

-

This link expires in {config.MAGIC_LINK_EXPIRY_MINUTES} minutes.

-

If you didn't request this, you can safely ignore this email.

- """ - + + body = ( + f'

Sign in to {config.APP_NAME}

' + f"

Click the button below to sign in. This link expires in " + f"{config.MAGIC_LINK_EXPIRY_MINUTES} minutes.

" + f"{_email_button(link, 'Sign In')}" + f'

If the button doesn\'t work, copy and paste this URL into your browser:

' + f'

{link}

' + f'

If you didn\'t request this, you can safely ignore this email.

' + ) + await send_email( to=payload["email"], subject=f"Sign in to {config.APP_NAME}", - html=html, + html=_email_wrap(body), ) @task("send_welcome") async def handle_send_welcome(payload: dict) -> None: """Send welcome email to new user.""" - html = f""" -

Welcome to {config.APP_NAME}!

-

Thanks for signing up. We're excited to have you.

-

Go to your dashboard

- """ - + body = ( + f'

Welcome to {config.APP_NAME}!

' + f"

Thanks for signing up. We're excited to have you.

" + f'{_email_button(f"{config.BASE_URL}/dashboard", "Go to Dashboard")}' + ) + await send_email( to=payload["email"], subject=f"Welcome to {config.APP_NAME}", - html=html, + html=_email_wrap(body), ) @@ -173,12 +216,12 @@ async def process_task(task: dict) -> None: task_name = task["task_name"] task_id = task["id"] retries = task.get("retries", 0) - + handler = HANDLERS.get(task_name) if not handler: await mark_failed(task_id, f"Unknown task: {task_name}", retries) return - + try: payload = json.loads(task["payload"]) if task["payload"] else {} await handler(payload) @@ -194,17 +237,17 @@ async def run_worker(poll_interval: float = 1.0) -> None: """Main worker loop.""" print("[WORKER] Starting...") await init_db() - + while True: try: tasks = await get_pending_tasks(limit=10) - + for task in tasks: await process_task(task) - + if not tasks: await asyncio.sleep(poll_interval) - + except Exception as e: print(f"[WORKER] Error: {e}") await asyncio.sleep(poll_interval * 5) @@ -214,16 +257,16 @@ async def run_scheduler() -> None: """Schedule periodic cleanup tasks.""" print("[SCHEDULER] Starting...") await init_db() - + while True: try: # Schedule cleanup tasks every hour await enqueue("cleanup_expired_tokens") await enqueue("cleanup_rate_limits") await enqueue("cleanup_old_tasks") - + await asyncio.sleep(3600) # 1 hour - + except Exception as e: print(f"[SCHEDULER] Error: {e}") await asyncio.sleep(60) @@ -231,8 +274,8 @@ async def run_scheduler() -> None: if __name__ == "__main__": import sys - + if len(sys.argv) > 1 and sys.argv[1] == "scheduler": asyncio.run(run_scheduler()) else: - asyncio.run(run_worker()) + asyncio.run(run_worker()) \ No newline at end of file diff --git a/web/tests/conftest.py b/web/tests/conftest.py index 37a7f8c..c9add7e 100644 --- a/web/tests/conftest.py +++ b/web/tests/conftest.py @@ -10,9 +10,11 @@ from unittest.mock import AsyncMock, patch import aiosqlite import pytest -from beanflows import analytics, core + +from beanflows import core from beanflows.app import create_app + SCHEMA_PATH = Path(__file__).parent.parent / "src" / "beanflows" / "migrations" / "schema.sql" @@ -44,9 +46,7 @@ async def db(): async def app(db): """Quart app with DB already initialized (init_db/close_db patched to no-op).""" with patch.object(core, "init_db", new_callable=AsyncMock), \ - patch.object(core, "close_db", new_callable=AsyncMock), \ - patch.object(analytics, "open_analytics_db"), \ - patch.object(analytics, "close_analytics_db"): + patch.object(core, "close_db", new_callable=AsyncMock): application = create_app() application.config["TESTING"] = True yield application @@ -92,22 +92,17 @@ def create_subscription(db): user_id: int, plan: str = "pro", status: str = "active", - - paddle_customer_id: str = "ctm_test123", - paddle_subscription_id: str = "sub_test456", - + provider_subscription_id: str = "sub_test456", current_period_end: str = "2025-03-01T00:00:00Z", ) -> int: now = datetime.utcnow().isoformat() async with db.execute( - """INSERT INTO subscriptions - (user_id, plan, status, paddle_customer_id, - paddle_subscription_id, current_period_end, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", - (user_id, plan, status, paddle_customer_id, paddle_subscription_id, + (user_id, plan, status, + provider_subscription_id, current_period_end, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?)""", + (user_id, plan, status, provider_subscription_id, current_period_end, now, now), - ) as cursor: sub_id = cursor.lastrowid await db.commit() @@ -115,6 +110,48 @@ def create_subscription(db): return _create +# ── Billing Customers ─────────────────────────────────────── + +@pytest.fixture +def create_billing_customer(db): + """Factory: create a billing_customers row for a user.""" + async def _create(user_id: int, provider_customer_id: str = "cust_test123") -> int: + async with db.execute( + """INSERT INTO billing_customers (user_id, provider_customer_id) + VALUES (?, ?) + ON CONFLICT(user_id) DO UPDATE SET provider_customer_id = excluded.provider_customer_id""", + (user_id, provider_customer_id), + ) as cursor: + row_id = cursor.lastrowid + await db.commit() + return row_id + return _create + + +# ── Roles ─────────────────────────────────────────────────── + +@pytest.fixture +def grant_role(db): + """Factory: grant a role to a user.""" + async def _grant(user_id: int, role: str) -> None: + await db.execute( + "INSERT OR IGNORE INTO user_roles (user_id, role) VALUES (?, ?)", + (user_id, role), + ) + await db.commit() + return _grant + + +@pytest.fixture +async def admin_client(app, test_user, grant_role): + """Test client with admin role and session['user_id'] pre-set.""" + await grant_role(test_user["id"], "admin") + async with app.test_client() as c: + async with c.session_transaction() as sess: + sess["user_id"] = test_user["id"] + yield c + + # ── Config ─────────────────────────────────────────────────── @pytest.fixture(autouse=True) @@ -127,6 +164,7 @@ def patch_config(): "PADDLE_API_KEY": "test_api_key_123", "PADDLE_WEBHOOK_SECRET": "whsec_test_secret", + "PADDLE_ENVIRONMENT": "sandbox", "PADDLE_PRICES": {"starter": "pri_starter_123", "pro": "pri_pro_456"}, "BASE_URL": "http://localhost:5000", @@ -147,6 +185,32 @@ def patch_config(): # ── Webhook helpers ────────────────────────────────────────── +@pytest.fixture(autouse=True) +def mock_paddle_verifier(monkeypatch): + """Mock Paddle's webhook Verifier to accept test payloads.""" + def mock_verify(self, payload, secret, signature): + if not signature or signature == "invalid_signature": + raise ValueError("Invalid signature") + + monkeypatch.setattr( + "paddle_billing.Notifications.Verifier.verify", + mock_verify, + ) + + +@pytest.fixture +def mock_paddle_client(monkeypatch): + """Mock _paddle_client() to return a fake PaddleClient.""" + from unittest.mock import MagicMock + + mock_client = MagicMock() + monkeypatch.setattr( + "beanflows.billing.routes._paddle_client", + lambda: mock_client, + ) + return mock_client + + def make_webhook_payload( event_type: str, subscription_id: str = "sub_test456", @@ -172,76 +236,8 @@ def make_webhook_payload( } -def sign_payload(payload_bytes: bytes, secret: str = "whsec_test_secret") -> str: - """Compute HMAC-SHA256 signature for a webhook payload.""" - return hmac.new(secret.encode(), payload_bytes, hashlib.sha256).hexdigest() - - -# ── Analytics mock data ────────────────────────────────────── - -MOCK_TIME_SERIES = [ - {"market_year": 2018, "Production": 165000, "Exports": 115000, "Imports": 105000, - "Ending_Stocks": 33000, "Total_Distribution": 160000}, - {"market_year": 2019, "Production": 168000, "Exports": 118000, "Imports": 108000, - "Ending_Stocks": 34000, "Total_Distribution": 163000}, - {"market_year": 2020, "Production": 170000, "Exports": 120000, "Imports": 110000, - "Ending_Stocks": 35000, "Total_Distribution": 165000}, - {"market_year": 2021, "Production": 175000, "Exports": 125000, "Imports": 115000, - "Ending_Stocks": 36000, "Total_Distribution": 170000}, - {"market_year": 2022, "Production": 172000, "Exports": 122000, "Imports": 112000, - "Ending_Stocks": 34000, "Total_Distribution": 168000}, -] - -MOCK_TOP_COUNTRIES = [ - {"country_name": "Brazil", "country_code": "BR", "market_year": 2022, "Production": 65000}, - {"country_name": "Vietnam", "country_code": "VN", "market_year": 2022, "Production": 30000}, - {"country_name": "Colombia", "country_code": "CO", "market_year": 2022, "Production": 14000}, -] - -MOCK_STU_TREND = [ - {"market_year": 2020, "Stock_to_Use_Ratio_pct": 21.2}, - {"market_year": 2021, "Stock_to_Use_Ratio_pct": 21.1}, - {"market_year": 2022, "Stock_to_Use_Ratio_pct": 20.2}, -] - -MOCK_BALANCE = [ - {"market_year": 2020, "Production": 170000, "Total_Distribution": 165000, "Supply_Demand_Balance": 5000}, - {"market_year": 2021, "Production": 175000, "Total_Distribution": 170000, "Supply_Demand_Balance": 5000}, - {"market_year": 2022, "Production": 172000, "Total_Distribution": 168000, "Supply_Demand_Balance": 4000}, -] - -MOCK_YOY = [ - {"country_name": "Brazil", "country_code": "BR", "market_year": 2022, - "Production": 65000, "Production_YoY_pct": -3.5}, - {"country_name": "Vietnam", "country_code": "VN", "market_year": 2022, - "Production": 30000, "Production_YoY_pct": 2.1}, -] - -MOCK_COMMODITIES = [ - {"commodity_code": 711100, "commodity_name": "Coffee, Green"}, - {"commodity_code": 222000, "commodity_name": "Soybeans"}, -] - - -@pytest.fixture -def mock_analytics(): - """Patch all analytics query functions with mock data.""" - with patch.object(analytics, "get_global_time_series", new_callable=AsyncMock, - return_value=MOCK_TIME_SERIES), \ - patch.object(analytics, "get_top_countries", new_callable=AsyncMock, - return_value=MOCK_TOP_COUNTRIES), \ - patch.object(analytics, "get_stock_to_use_trend", new_callable=AsyncMock, - return_value=MOCK_STU_TREND), \ - patch.object(analytics, "get_supply_demand_balance", new_callable=AsyncMock, - return_value=MOCK_BALANCE), \ - patch.object(analytics, "get_production_yoy_by_country", new_callable=AsyncMock, - return_value=MOCK_YOY), \ - patch.object(analytics, "get_country_comparison", new_callable=AsyncMock, - return_value=[]), \ - patch.object(analytics, "get_available_commodities", new_callable=AsyncMock, - return_value=MOCK_COMMODITIES), \ - patch.object(analytics, "fetch_analytics", new_callable=AsyncMock, - return_value=[{"result": 1}]): - yield +def sign_payload(payload_bytes: bytes) -> str: + """Return a dummy signature for Paddle webhook tests (Verifier is mocked).""" + return "ts=1234567890;h1=dummy_signature" diff --git a/web/tests/test_billing_helpers.py b/web/tests/test_billing_helpers.py index 8117396..f7ac362 100644 --- a/web/tests/test_billing_helpers.py +++ b/web/tests/test_billing_helpers.py @@ -9,10 +9,13 @@ from hypothesis import strategies as st from beanflows.billing.routes import ( can_access_feature, + get_billing_customer, get_subscription, get_subscription_by_provider_id, is_within_limits, + record_transaction, update_subscription_status, + upsert_billing_customer, upsert_subscription, ) from beanflows.core import config @@ -45,7 +48,6 @@ class TestUpsertSubscription: user_id=test_user["id"], plan="pro", status="active", - provider_customer_id="cust_abc", provider_subscription_id="sub_xyz", current_period_end="2025-06-01T00:00:00Z", ) @@ -53,39 +55,53 @@ class TestUpsertSubscription: row = await get_subscription(test_user["id"]) assert row["plan"] == "pro" assert row["status"] == "active" - - assert row["paddle_customer_id"] == "cust_abc" - assert row["paddle_subscription_id"] == "sub_xyz" - + assert row["provider_subscription_id"] == "sub_xyz" assert row["current_period_end"] == "2025-06-01T00:00:00Z" - async def test_update_existing_subscription(self, db, test_user, create_subscription): - original_id = await create_subscription( - test_user["id"], plan="starter", status="active", - - paddle_subscription_id="sub_old", - + async def test_update_existing_by_provider_subscription_id(self, db, test_user): + """upsert finds existing by provider_subscription_id, not user_id.""" + await upsert_subscription( + user_id=test_user["id"], + plan="starter", + status="active", + provider_subscription_id="sub_same", ) returned_id = await upsert_subscription( user_id=test_user["id"], plan="pro", status="active", - provider_customer_id="cust_new", - provider_subscription_id="sub_new", + provider_subscription_id="sub_same", ) - assert returned_id == original_id row = await get_subscription(test_user["id"]) assert row["plan"] == "pro" + assert row["provider_subscription_id"] == "sub_same" - assert row["paddle_subscription_id"] == "sub_new" - + async def test_different_provider_id_creates_new(self, db, test_user): + """Different provider_subscription_id creates a new row (multi-sub support).""" + await upsert_subscription( + user_id=test_user["id"], + plan="starter", + status="active", + provider_subscription_id="sub_first", + ) + await upsert_subscription( + user_id=test_user["id"], + plan="pro", + status="active", + provider_subscription_id="sub_second", + ) + from beanflows.core import fetch_all + rows = await fetch_all( + "SELECT * FROM subscriptions WHERE user_id = ? ORDER BY created_at", + (test_user["id"],), + ) + assert len(rows) == 2 async def test_upsert_with_none_period_end(self, db, test_user): await upsert_subscription( user_id=test_user["id"], plan="pro", status="active", - provider_customer_id="cust_1", provider_subscription_id="sub_1", current_period_end=None, ) @@ -93,6 +109,28 @@ class TestUpsertSubscription: assert row["current_period_end"] is None +# ════════════════════════════════════════════════════════════ +# upsert_billing_customer / get_billing_customer +# ════════════════════════════════════════════════════════════ + +class TestUpsertBillingCustomer: + async def test_creates_billing_customer(self, db, test_user): + await upsert_billing_customer(test_user["id"], "cust_abc") + row = await get_billing_customer(test_user["id"]) + assert row is not None + assert row["provider_customer_id"] == "cust_abc" + + async def test_updates_existing_customer(self, db, test_user): + await upsert_billing_customer(test_user["id"], "cust_old") + await upsert_billing_customer(test_user["id"], "cust_new") + row = await get_billing_customer(test_user["id"]) + assert row["provider_customer_id"] == "cust_new" + + async def test_get_returns_none_for_unknown_user(self, db): + row = await get_billing_customer(99999) + assert row is None + + # ════════════════════════════════════════════════════════════ # get_subscription_by_provider_id # ════════════════════════════════════════════════════════════ @@ -102,10 +140,8 @@ class TestGetSubscriptionByProviderId: result = await get_subscription_by_provider_id("nonexistent") assert result is None - - async def test_finds_by_paddle_subscription_id(self, db, test_user, create_subscription): - await create_subscription(test_user["id"], paddle_subscription_id="sub_findme") - + async def test_finds_by_provider_subscription_id(self, db, test_user, create_subscription): + await create_subscription(test_user["id"], provider_subscription_id="sub_findme") result = await get_subscription_by_provider_id("sub_findme") assert result is not None assert result["user_id"] == test_user["id"] @@ -117,18 +153,14 @@ class TestGetSubscriptionByProviderId: class TestUpdateSubscriptionStatus: async def test_updates_status(self, db, test_user, create_subscription): - - await create_subscription(test_user["id"], status="active", paddle_subscription_id="sub_upd") - + await create_subscription(test_user["id"], status="active", provider_subscription_id="sub_upd") await update_subscription_status("sub_upd", status="cancelled") row = await get_subscription(test_user["id"]) assert row["status"] == "cancelled" assert row["updated_at"] is not None async def test_updates_extra_fields(self, db, test_user, create_subscription): - - await create_subscription(test_user["id"], paddle_subscription_id="sub_extra") - + await create_subscription(test_user["id"], provider_subscription_id="sub_extra") await update_subscription_status( "sub_extra", status="active", @@ -141,9 +173,7 @@ class TestUpdateSubscriptionStatus: assert row["current_period_end"] == "2026-01-01T00:00:00Z" async def test_noop_for_unknown_provider_id(self, db, test_user, create_subscription): - - await create_subscription(test_user["id"], paddle_subscription_id="sub_known", status="active") - + await create_subscription(test_user["id"], provider_subscription_id="sub_known", status="active") await update_subscription_status("sub_unknown", status="expired") row = await get_subscription(test_user["id"]) assert row["status"] == "active" # unchanged @@ -155,22 +185,22 @@ class TestUpdateSubscriptionStatus: class TestCanAccessFeature: async def test_no_subscription_gets_free_features(self, db, test_user): - assert await can_access_feature(test_user["id"], "dashboard") is True + assert await can_access_feature(test_user["id"], "basic") is True assert await can_access_feature(test_user["id"], "export") is False assert await can_access_feature(test_user["id"], "api") is False async def test_active_pro_gets_all_features(self, db, test_user, create_subscription): await create_subscription(test_user["id"], plan="pro", status="active") - assert await can_access_feature(test_user["id"], "dashboard") is True + assert await can_access_feature(test_user["id"], "basic") is True assert await can_access_feature(test_user["id"], "export") is True assert await can_access_feature(test_user["id"], "api") is True assert await can_access_feature(test_user["id"], "priority_support") is True async def test_active_starter_gets_starter_features(self, db, test_user, create_subscription): await create_subscription(test_user["id"], plan="starter", status="active") - assert await can_access_feature(test_user["id"], "dashboard") is True + assert await can_access_feature(test_user["id"], "basic") is True assert await can_access_feature(test_user["id"], "export") is True - assert await can_access_feature(test_user["id"], "all_commodities") is False + assert await can_access_feature(test_user["id"], "api") is False async def test_cancelled_still_has_features(self, db, test_user, create_subscription): await create_subscription(test_user["id"], plan="pro", status="cancelled") @@ -183,7 +213,7 @@ class TestCanAccessFeature: async def test_expired_falls_back_to_free(self, db, test_user, create_subscription): await create_subscription(test_user["id"], plan="pro", status="expired") assert await can_access_feature(test_user["id"], "api") is False - assert await can_access_feature(test_user["id"], "dashboard") is True + assert await can_access_feature(test_user["id"], "basic") is True async def test_past_due_falls_back_to_free(self, db, test_user, create_subscription): await create_subscription(test_user["id"], plan="pro", status="past_due") @@ -203,30 +233,28 @@ class TestCanAccessFeature: # ════════════════════════════════════════════════════════════ class TestIsWithinLimits: - async def test_free_user_no_api_calls(self, db, test_user): - assert await is_within_limits(test_user["id"], "api_calls", 0) is False + async def test_free_user_within_limits(self, db, test_user): + assert await is_within_limits(test_user["id"], "items", 50) is True - async def test_free_user_commodity_limit(self, db, test_user): - assert await is_within_limits(test_user["id"], "commodities", 0) is True - assert await is_within_limits(test_user["id"], "commodities", 1) is False + async def test_free_user_at_limit(self, db, test_user): + assert await is_within_limits(test_user["id"], "items", 100) is False - async def test_free_user_history_limit(self, db, test_user): - assert await is_within_limits(test_user["id"], "history_years", 4) is True - assert await is_within_limits(test_user["id"], "history_years", 5) is False + async def test_free_user_over_limit(self, db, test_user): + assert await is_within_limits(test_user["id"], "items", 150) is False async def test_pro_unlimited(self, db, test_user, create_subscription): await create_subscription(test_user["id"], plan="pro", status="active") - assert await is_within_limits(test_user["id"], "commodities", 999999) is True + assert await is_within_limits(test_user["id"], "items", 999999) is True assert await is_within_limits(test_user["id"], "api_calls", 999999) is True async def test_starter_limits(self, db, test_user, create_subscription): await create_subscription(test_user["id"], plan="starter", status="active") - assert await is_within_limits(test_user["id"], "api_calls", 9999) is True - assert await is_within_limits(test_user["id"], "api_calls", 10000) is False + assert await is_within_limits(test_user["id"], "items", 999) is True + assert await is_within_limits(test_user["id"], "items", 1000) is False async def test_expired_pro_gets_free_limits(self, db, test_user, create_subscription): await create_subscription(test_user["id"], plan="pro", status="expired") - assert await is_within_limits(test_user["id"], "api_calls", 0) is False + assert await is_within_limits(test_user["id"], "items", 100) is False async def test_unknown_resource_returns_false(self, db, test_user): assert await is_within_limits(test_user["id"], "unicorns", 0) is False @@ -238,7 +266,7 @@ class TestIsWithinLimits: # ════════════════════════════════════════════════════════════ STATUSES = ["free", "active", "on_trial", "cancelled", "past_due", "paused", "expired"] -FEATURES = ["dashboard", "export", "api", "priority_support"] +FEATURES = ["basic", "export", "api", "priority_support"] ACTIVE_STATUSES = {"active", "on_trial", "cancelled"} @@ -282,9 +310,9 @@ async def test_plan_feature_matrix(db, test_user, create_subscription, plan, fea @pytest.mark.parametrize("plan", PLANS) @pytest.mark.parametrize("resource,at_limit", [ - ("commodities", 1), - ("commodities", 65), - ("api_calls", 0), + ("items", 100), + ("items", 1000), + ("api_calls", 1000), ("api_calls", 10000), ]) async def test_plan_limit_matrix(db, test_user, create_subscription, plan, resource, at_limit): @@ -307,11 +335,11 @@ async def test_plan_limit_matrix(db, test_user, create_subscription, plan, resou # ════════════════════════════════════════════════════════════ class TestLimitsHypothesis: - @given(count=st.integers(min_value=0, max_value=100)) + @given(count=st.integers(min_value=0, max_value=10000)) @h_settings(max_examples=100, deadline=2000, suppress_health_check=[HealthCheck.function_scoped_fixture]) - async def test_free_limit_boundary_commodities(self, db, test_user, count): - result = await is_within_limits(test_user["id"], "commodities", count) - assert result == (count < 1) + async def test_free_limit_boundary_items(self, db, test_user, count): + result = await is_within_limits(test_user["id"], "items", count) + assert result == (count < 100) @given(count=st.integers(min_value=0, max_value=100000)) @h_settings(max_examples=100, deadline=2000, suppress_health_check=[HealthCheck.function_scoped_fixture]) @@ -319,7 +347,56 @@ class TestLimitsHypothesis: # Use upsert to avoid duplicate inserts across Hypothesis examples await upsert_subscription( user_id=test_user["id"], plan="pro", status="active", - provider_customer_id="cust_hyp", provider_subscription_id="sub_hyp", + provider_subscription_id="sub_hyp", ) - result = await is_within_limits(test_user["id"], "commodities", count) + result = await is_within_limits(test_user["id"], "items", count) assert result is True + + +# ════════════════════════════════════════════════════════════ +# record_transaction +# ════════════════════════════════════════════════════════════ + +class TestRecordTransaction: + async def test_inserts_transaction(self, db, test_user): + txn_id = await record_transaction( + user_id=test_user["id"], + provider_transaction_id="txn_abc123", + type="payment", + amount_cents=2999, + currency="EUR", + status="completed", + ) + assert txn_id is not None and txn_id > 0 + + from beanflows.core import fetch_one + row = await fetch_one( + "SELECT * FROM transactions WHERE provider_transaction_id = ?", + ("txn_abc123",), + ) + assert row is not None + assert row["user_id"] == test_user["id"] + assert row["amount_cents"] == 2999 + assert row["currency"] == "EUR" + assert row["status"] == "completed" + + async def test_idempotent_on_duplicate_provider_id(self, db, test_user): + await record_transaction( + user_id=test_user["id"], + provider_transaction_id="txn_dup", + amount_cents=1000, + ) + # Second insert with same provider_transaction_id should be ignored + await record_transaction( + user_id=test_user["id"], + provider_transaction_id="txn_dup", + amount_cents=9999, + ) + + from beanflows.core import fetch_all + rows = await fetch_all( + "SELECT * FROM transactions WHERE provider_transaction_id = ?", + ("txn_dup",), + ) + assert len(rows) == 1 + assert rows[0]["amount_cents"] == 1000 # original value preserved diff --git a/web/tests/test_billing_hooks.py b/web/tests/test_billing_hooks.py new file mode 100644 index 0000000..863cd16 --- /dev/null +++ b/web/tests/test_billing_hooks.py @@ -0,0 +1,122 @@ +""" +Tests for the billing event hook system. +""" +import pytest + +from beanflows.billing.routes import _billing_hooks, _fire_hooks, on_billing_event + + +@pytest.fixture(autouse=True) +def clear_hooks(): + """Ensure hooks are clean before and after each test.""" + _billing_hooks.clear() + yield + _billing_hooks.clear() + + +# ════════════════════════════════════════════════════════════ +# Registration +# ════════════════════════════════════════════════════════════ + +class TestOnBillingEvent: + def test_registers_single_event(self): + @on_billing_event("subscription.activated") + async def my_hook(event_type, data): + pass + + assert "subscription.activated" in _billing_hooks + assert my_hook in _billing_hooks["subscription.activated"] + + def test_registers_multiple_events(self): + @on_billing_event("subscription.activated", "subscription.updated") + async def my_hook(event_type, data): + pass + + assert my_hook in _billing_hooks["subscription.activated"] + assert my_hook in _billing_hooks["subscription.updated"] + + def test_multiple_hooks_per_event(self): + @on_billing_event("subscription.activated") + async def hook_a(event_type, data): + pass + + @on_billing_event("subscription.activated") + async def hook_b(event_type, data): + pass + + assert len(_billing_hooks["subscription.activated"]) == 2 + + def test_decorator_returns_original_function(self): + @on_billing_event("test_event") + async def my_hook(event_type, data): + pass + + assert my_hook.__name__ == "my_hook" + + +# ════════════════════════════════════════════════════════════ +# Firing +# ════════════════════════════════════════════════════════════ + +class TestFireHooks: + async def test_fires_registered_hook(self): + calls = [] + + @on_billing_event("subscription.activated") + async def recorder(event_type, data): + calls.append((event_type, data)) + + await _fire_hooks("subscription.activated", {"id": "sub_123"}) + assert len(calls) == 1 + assert calls[0] == ("subscription.activated", {"id": "sub_123"}) + + async def test_no_hooks_registered_is_noop(self): + # Should not raise + await _fire_hooks("unregistered_event", {"id": "sub_123"}) + + async def test_fires_all_hooks_for_event(self): + calls = [] + + @on_billing_event("subscription.activated") + async def hook_a(event_type, data): + calls.append("a") + + @on_billing_event("subscription.activated") + async def hook_b(event_type, data): + calls.append("b") + + await _fire_hooks("subscription.activated", {}) + assert calls == ["a", "b"] + + +# ════════════════════════════════════════════════════════════ +# Error isolation +# ════════════════════════════════════════════════════════════ + +class TestHookErrorIsolation: + async def test_failing_hook_does_not_block_others(self): + calls = [] + + @on_billing_event("subscription.activated") + async def failing_hook(event_type, data): + raise RuntimeError("boom") + + @on_billing_event("subscription.activated") + async def good_hook(event_type, data): + calls.append("ok") + + # Should not raise despite first hook failing + await _fire_hooks("subscription.activated", {}) + assert calls == ["ok"] + + async def test_failing_hook_is_logged(self, caplog): + @on_billing_event("subscription.activated") + async def bad_hook(event_type, data): + raise ValueError("test error") + + import logging + with caplog.at_level(logging.ERROR): + await _fire_hooks("subscription.activated", {}) + + assert "bad_hook" in caplog.text + assert "test error" in caplog.text diff --git a/web/tests/test_billing_routes.py b/web/tests/test_billing_routes.py index 4830874..7043f12 100644 --- a/web/tests/test_billing_routes.py +++ b/web/tests/test_billing_routes.py @@ -1,12 +1,15 @@ """ Route integration tests for Paddle billing endpoints. -External Paddle API calls mocked with respx. -""" -import json -import httpx +Paddle SDK calls mocked via mock_paddle_client fixture. + +""" + + + +from unittest.mock import MagicMock + import pytest -import respx CHECKOUT_METHOD = "POST" @@ -54,24 +57,16 @@ class TestCheckoutRoute: assert response.status_code in (302, 303, 307) - @respx.mock - async def test_creates_checkout_session(self, auth_client, db, test_user): - - respx.post("https://api.paddle.com/transactions").mock( - return_value=httpx.Response(200, json={ - "data": { - "checkout": { - "url": "https://checkout.paddle.com/test_123" - } - } - }) - ) - + async def test_creates_checkout_session(self, auth_client, db, test_user, mock_paddle_client): + mock_txn = MagicMock() + mock_txn.checkout.url = "https://checkout.paddle.com/test_123" + mock_paddle_client.transactions.create.return_value = mock_txn response = await auth_client.post(f"/billing/checkout/{CHECKOUT_PLAN}", follow_redirects=False) - assert response.status_code in (302, 303, 307) + mock_paddle_client.transactions.create.assert_called_once() + async def test_invalid_plan_rejected(self, auth_client, db, test_user): @@ -82,20 +77,13 @@ class TestCheckoutRoute: - @respx.mock - async def test_api_error_propagates(self, auth_client, db, test_user): - - respx.post("https://api.paddle.com/transactions").mock( - return_value=httpx.Response(500, json={"error": "server error"}) - ) - - with pytest.raises(httpx.HTTPStatusError): - + async def test_api_error_propagates(self, auth_client, db, test_user, mock_paddle_client): + mock_paddle_client.transactions.create.side_effect = Exception("API error") + with pytest.raises(Exception, match="API error"): await auth_client.post(f"/billing/checkout/{CHECKOUT_PLAN}") - # ════════════════════════════════════════════════════════════ # Manage subscription / Portal # ════════════════════════════════════════════════════════════ @@ -110,24 +98,18 @@ class TestManageRoute: response = await auth_client.post("/billing/manage", follow_redirects=False) assert response.status_code in (302, 303, 307) - @respx.mock - async def test_redirects_to_portal(self, auth_client, db, test_user, create_subscription): - await create_subscription(test_user["id"], paddle_subscription_id="sub_test") - - respx.get("https://api.paddle.com/subscriptions/sub_test").mock( - return_value=httpx.Response(200, json={ - "data": { - "management_urls": { - "update_payment_method": "https://paddle.com/manage/test_123" - } - } - }) - ) + async def test_redirects_to_portal(self, auth_client, db, test_user, create_subscription, mock_paddle_client): + await create_subscription(test_user["id"], provider_subscription_id="sub_test") + mock_sub = MagicMock() + mock_sub.management_urls.update_payment_method = "https://paddle.com/manage/test_123" + mock_paddle_client.subscriptions.get.return_value = mock_sub response = await auth_client.post("/billing/manage", follow_redirects=False) assert response.status_code in (302, 303, 307) + mock_paddle_client.subscriptions.get.assert_called_once_with("sub_test") + @@ -145,18 +127,14 @@ class TestCancelRoute: response = await auth_client.post("/billing/cancel", follow_redirects=False) assert response.status_code in (302, 303, 307) - @respx.mock - async def test_cancels_subscription(self, auth_client, db, test_user, create_subscription): - - await create_subscription(test_user["id"], paddle_subscription_id="sub_test") - - respx.post("https://api.paddle.com/subscriptions/sub_test/cancel").mock( - return_value=httpx.Response(200, json={"data": {}}) - ) + async def test_cancels_subscription(self, auth_client, db, test_user, create_subscription, mock_paddle_client): + await create_subscription(test_user["id"], provider_subscription_id="sub_test") response = await auth_client.post("/billing/cancel", follow_redirects=False) assert response.status_code in (302, 303, 307) + mock_paddle_client.subscriptions.cancel.assert_called_once() + @@ -167,8 +145,9 @@ class TestCancelRoute: # subscription_required decorator # ════════════════════════════════════════════════════════════ -from beanflows.billing.routes import subscription_required -from quart import Blueprint +from quart import Blueprint # noqa: E402 + +from beanflows.auth.routes import subscription_required # noqa: E402 test_bp = Blueprint("test", __name__) diff --git a/web/tests/test_billing_webhooks.py b/web/tests/test_billing_webhooks.py index 37a3251..96e291d 100644 --- a/web/tests/test_billing_webhooks.py +++ b/web/tests/test_billing_webhooks.py @@ -5,13 +5,13 @@ Covers signature verification, event parsing, subscription lifecycle transitions import json import pytest +from conftest import make_webhook_payload, sign_payload + from hypothesis import HealthCheck, given from hypothesis import settings as h_settings from hypothesis import strategies as st -from beanflows.billing.routes import get_subscription - -from conftest import make_webhook_payload, sign_payload +from beanflows.billing.routes import get_billing_customer, get_subscription WEBHOOK_PATH = "/billing/webhook/paddle" @@ -72,18 +72,19 @@ class TestWebhookSignature: async def test_modified_payload_rejected(self, client, db, test_user): + # Paddle SDK Verifier handles tamper detection internally. + # We test signature rejection via test_invalid_signature_rejected above. + # This test verifies the Verifier is actually called by sending + # a payload with an explicitly bad signature. payload = make_webhook_payload("subscription.activated", user_id=str(test_user["id"])) payload_bytes = json.dumps(payload).encode() - sig = sign_payload(payload_bytes) - tampered = payload_bytes + b"extra" - # Paddle/LemonSqueezy: HMAC signature verification fails before JSON parsing response = await client.post( WEBHOOK_PATH, - data=tampered, - headers={SIG_HEADER: sig, "Content-Type": "application/json"}, + data=payload_bytes, + headers={SIG_HEADER: "invalid_signature", "Content-Type": "application/json"}, ) - assert response.status_code in (400, 401) + assert response.status_code == 400 async def test_empty_payload_rejected(self, client, db): @@ -105,7 +106,7 @@ class TestWebhookSignature: class TestWebhookSubscriptionActivated: - async def test_creates_subscription(self, client, db, test_user): + async def test_creates_subscription_and_billing_customer(self, client, db, test_user): payload = make_webhook_payload( "subscription.activated", user_id=str(test_user["id"]), @@ -126,10 +127,14 @@ class TestWebhookSubscriptionActivated: assert sub["plan"] == "starter" assert sub["status"] == "active" + bc = await get_billing_customer(test_user["id"]) + assert bc is not None + assert bc["provider_customer_id"] == "ctm_test123" + class TestWebhookSubscriptionUpdated: async def test_updates_subscription_status(self, client, db, test_user, create_subscription): - await create_subscription(test_user["id"], status="active", paddle_subscription_id="sub_test456") + await create_subscription(test_user["id"], status="active", provider_subscription_id="sub_test456") payload = make_webhook_payload( "subscription.updated", @@ -152,7 +157,7 @@ class TestWebhookSubscriptionUpdated: class TestWebhookSubscriptionCanceled: async def test_marks_subscription_cancelled(self, client, db, test_user, create_subscription): - await create_subscription(test_user["id"], status="active", paddle_subscription_id="sub_test456") + await create_subscription(test_user["id"], status="active", provider_subscription_id="sub_test456") payload = make_webhook_payload( "subscription.canceled", @@ -174,7 +179,7 @@ class TestWebhookSubscriptionCanceled: class TestWebhookSubscriptionPastDue: async def test_marks_subscription_past_due(self, client, db, test_user, create_subscription): - await create_subscription(test_user["id"], status="active", paddle_subscription_id="sub_test456") + await create_subscription(test_user["id"], status="active", provider_subscription_id="sub_test456") payload = make_webhook_payload( "subscription.past_due", @@ -209,7 +214,7 @@ class TestWebhookSubscriptionPastDue: ]) async def test_event_status_transitions(client, db, test_user, create_subscription, event_type, expected_status): if event_type != "subscription.activated": - await create_subscription(test_user["id"], paddle_subscription_id="sub_test456") + await create_subscription(test_user["id"], provider_subscription_id="sub_test456") payload = make_webhook_payload(event_type, user_id=str(test_user["id"])) payload_bytes = json.dumps(payload).encode() diff --git a/web/tests/test_roles.py b/web/tests/test_roles.py new file mode 100644 index 0000000..378d712 --- /dev/null +++ b/web/tests/test_roles.py @@ -0,0 +1,242 @@ +""" +Tests for role-based access control: role_required decorator, grant/revoke/ensure_admin_role, +and admin route protection. +""" +import pytest +from quart import Blueprint + +from beanflows.auth.routes import ( + ensure_admin_role, + grant_role, + revoke_role, + role_required, +) +from beanflows import core + + +# ════════════════════════════════════════════════════════════ +# grant_role / revoke_role +# ════════════════════════════════════════════════════════════ + +class TestGrantRole: + async def test_grants_role(self, db, test_user): + await grant_role(test_user["id"], "admin") + row = await core.fetch_one( + "SELECT role FROM user_roles WHERE user_id = ?", + (test_user["id"],), + ) + assert row is not None + assert row["role"] == "admin" + + async def test_idempotent(self, db, test_user): + await grant_role(test_user["id"], "admin") + await grant_role(test_user["id"], "admin") + rows = await core.fetch_all( + "SELECT role FROM user_roles WHERE user_id = ? AND role = 'admin'", + (test_user["id"],), + ) + assert len(rows) == 1 + + +class TestRevokeRole: + async def test_revokes_existing_role(self, db, test_user): + await grant_role(test_user["id"], "admin") + await revoke_role(test_user["id"], "admin") + row = await core.fetch_one( + "SELECT role FROM user_roles WHERE user_id = ? AND role = 'admin'", + (test_user["id"],), + ) + assert row is None + + async def test_noop_for_missing_role(self, db, test_user): + # Should not raise + await revoke_role(test_user["id"], "nonexistent") + + +# ════════════════════════════════════════════════════════════ +# ensure_admin_role +# ════════════════════════════════════════════════════════════ + +class TestEnsureAdminRole: + async def test_grants_admin_for_listed_email(self, db, test_user): + core.config.ADMIN_EMAILS = ["test@example.com"] + try: + await ensure_admin_role(test_user["id"], "test@example.com") + row = await core.fetch_one( + "SELECT role FROM user_roles WHERE user_id = ? AND role = 'admin'", + (test_user["id"],), + ) + assert row is not None + finally: + core.config.ADMIN_EMAILS = [] + + async def test_skips_for_unlisted_email(self, db, test_user): + core.config.ADMIN_EMAILS = ["boss@example.com"] + try: + await ensure_admin_role(test_user["id"], "test@example.com") + row = await core.fetch_one( + "SELECT role FROM user_roles WHERE user_id = ? AND role = 'admin'", + (test_user["id"],), + ) + assert row is None + finally: + core.config.ADMIN_EMAILS = [] + + async def test_empty_admin_emails_grants_nothing(self, db, test_user): + core.config.ADMIN_EMAILS = [] + await ensure_admin_role(test_user["id"], "test@example.com") + row = await core.fetch_one( + "SELECT role FROM user_roles WHERE user_id = ? AND role = 'admin'", + (test_user["id"],), + ) + assert row is None + + async def test_case_insensitive_matching(self, db, test_user): + core.config.ADMIN_EMAILS = ["test@example.com"] + try: + await ensure_admin_role(test_user["id"], "Test@Example.COM") + row = await core.fetch_one( + "SELECT role FROM user_roles WHERE user_id = ? AND role = 'admin'", + (test_user["id"],), + ) + assert row is not None + finally: + core.config.ADMIN_EMAILS = [] + + +# ════════════════════════════════════════════════════════════ +# role_required decorator +# ════════════════════════════════════════════════════════════ + +role_test_bp = Blueprint("role_test", __name__) + + +@role_test_bp.route("/admin-only") +@role_required("admin") +async def admin_only_route(): + return "admin-ok", 200 + + +@role_test_bp.route("/multi-role") +@role_required("admin", "editor") +async def multi_role_route(): + return "multi-ok", 200 + + +class TestRoleRequired: + @pytest.fixture + async def role_app(self, app): + app.register_blueprint(role_test_bp) + return app + + @pytest.fixture + async def role_client(self, role_app): + async with role_app.test_client() as c: + yield c + + async def test_redirects_unauthenticated(self, role_client, db): + response = await role_client.get("/admin-only", follow_redirects=False) + assert response.status_code in (302, 303, 307) + + async def test_rejects_user_without_role(self, role_client, db, test_user): + async with role_client.session_transaction() as sess: + sess["user_id"] = test_user["id"] + + response = await role_client.get("/admin-only", follow_redirects=False) + assert response.status_code in (302, 303, 307) + + async def test_allows_user_with_matching_role(self, role_client, db, test_user): + await grant_role(test_user["id"], "admin") + async with role_client.session_transaction() as sess: + sess["user_id"] = test_user["id"] + + response = await role_client.get("/admin-only") + assert response.status_code == 200 + + async def test_multi_role_allows_any_match(self, role_client, db, test_user): + await grant_role(test_user["id"], "editor") + async with role_client.session_transaction() as sess: + sess["user_id"] = test_user["id"] + + response = await role_client.get("/multi-role") + assert response.status_code == 200 + + async def test_multi_role_rejects_none(self, role_client, db, test_user): + await grant_role(test_user["id"], "viewer") + async with role_client.session_transaction() as sess: + sess["user_id"] = test_user["id"] + + response = await role_client.get("/multi-role", follow_redirects=False) + assert response.status_code in (302, 303, 307) + + +# ════════════════════════════════════════════════════════════ +# Admin route protection +# ════════════════════════════════════════════════════════════ + +class TestAdminRouteProtection: + async def test_admin_index_requires_admin_role(self, auth_client, db): + response = await auth_client.get("/admin/", follow_redirects=False) + assert response.status_code in (302, 303, 307) + + async def test_admin_index_accessible_with_admin_role(self, admin_client, db): + response = await admin_client.get("/admin/") + assert response.status_code == 200 + + async def test_admin_users_requires_admin_role(self, auth_client, db): + response = await auth_client.get("/admin/users", follow_redirects=False) + assert response.status_code in (302, 303, 307) + + async def test_admin_tasks_requires_admin_role(self, auth_client, db): + response = await auth_client.get("/admin/tasks", follow_redirects=False) + assert response.status_code in (302, 303, 307) + + +# ════════════════════════════════════════════════════════════ +# Impersonation +# ════════════════════════════════════════════════════════════ + +class TestImpersonation: + async def test_impersonate_stores_admin_id(self, admin_client, db, test_user): + """Impersonating stores admin's user_id in session['admin_impersonating'].""" + # Create a second user to impersonate + now = "2025-01-01T00:00:00" + other_id = await core.execute( + "INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)", + ("other@example.com", "Other", now), + ) + + async with admin_client.session_transaction() as sess: + sess["csrf_token"] = "test_csrf" + + response = await admin_client.post( + f"/admin/users/{other_id}/impersonate", + form={"csrf_token": "test_csrf"}, + follow_redirects=False, + ) + assert response.status_code in (302, 303, 307) + + async with admin_client.session_transaction() as sess: + assert sess["user_id"] == other_id + assert sess["admin_impersonating"] == test_user["id"] + + async def test_stop_impersonating_restores_admin(self, app, db, test_user, grant_role): + """Stopping impersonation restores the admin's user_id.""" + await grant_role(test_user["id"], "admin") + + async with app.test_client() as c: + async with c.session_transaction() as sess: + sess["user_id"] = 999 # impersonated user + sess["admin_impersonating"] = test_user["id"] + sess["csrf_token"] = "test_csrf" + + response = await c.post( + "/admin/stop-impersonating", + form={"csrf_token": "test_csrf"}, + follow_redirects=False, + ) + assert response.status_code in (302, 303, 307) + + async with c.session_transaction() as sess: + assert sess["user_id"] == test_user["id"] + assert "admin_impersonating" not in sess From 3f1cd8bd0c859fd4f7cb7ea06b4c63d8258c5a8c Mon Sep 17 00:00:00 2001 From: Deeman Date: Thu, 19 Feb 2026 22:35:55 +0100 Subject: [PATCH 2/2] Update copier answers and docker-compose prod config - Record v0.4.0 commit in .copier-answers.yml - Apply flattened paths in docker-compose.prod.yml Co-Authored-By: Claude Sonnet 4 --- web/.copier-answers.yml | 2 +- web/docker-compose.prod.yml | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/web/.copier-answers.yml b/web/.copier-answers.yml index 96f3567..840dfc8 100644 --- a/web/.copier-answers.yml +++ b/web/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY -_commit: v0.3.0 +_commit: v0.4.0 _src_path: git@gitlab.com:deemanone/materia_saas_boilerplate.master.git author_email: hendrik@beanflows.coffee author_name: Hendrik Deeman diff --git a/web/docker-compose.prod.yml b/web/docker-compose.prod.yml index d572da9..d2a06df 100644 --- a/web/docker-compose.prod.yml +++ b/web/docker-compose.prod.yml @@ -21,16 +21,16 @@ services: command: replicate -config /etc/litestream.yml volumes: - app-data:/app/data - - ./beanflows/litestream.yml:/etc/litestream.yml:ro + - ./litestream.yml:/etc/litestream.yml:ro # ── Blue slot ───────────────────────────────────────────── blue-app: profiles: ["blue"] build: - context: ./beanflows + context: . restart: unless-stopped - env_file: ./beanflows/.env + env_file: ./.env environment: - DATABASE_PATH=/app/data/app.db volumes: @@ -47,10 +47,10 @@ services: blue-worker: profiles: ["blue"] build: - context: ./beanflows + context: . restart: unless-stopped command: python -m beanflows.worker - env_file: ./beanflows/.env + env_file: ./.env environment: - DATABASE_PATH=/app/data/app.db volumes: @@ -61,10 +61,10 @@ services: blue-scheduler: profiles: ["blue"] build: - context: ./beanflows + context: . restart: unless-stopped command: python -m beanflows.worker scheduler - env_file: ./beanflows/.env + env_file: ./.env environment: - DATABASE_PATH=/app/data/app.db volumes: @@ -77,9 +77,9 @@ services: green-app: profiles: ["green"] build: - context: ./beanflows + context: . restart: unless-stopped - env_file: ./beanflows/.env + env_file: ./.env environment: - DATABASE_PATH=/app/data/app.db volumes: @@ -96,10 +96,10 @@ services: green-worker: profiles: ["green"] build: - context: ./beanflows + context: . restart: unless-stopped command: python -m beanflows.worker - env_file: ./beanflows/.env + env_file: ./.env environment: - DATABASE_PATH=/app/data/app.db volumes: @@ -110,10 +110,10 @@ services: green-scheduler: profiles: ["green"] build: - context: ./beanflows + context: . restart: unless-stopped command: python -m beanflows.worker scheduler - env_file: ./beanflows/.env + env_file: ./.env environment: - DATABASE_PATH=/app/data/app.db volumes: