# 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
