Environment: Windows 11, bash shell. Project root: J:\CLAUDE\PROJECTS\Wakeword (master).

PROBLEM
Stripe webhook idempotency in `console/backend/app/routes/billing.py` uses an in-memory `OrderedDict[str, None]` (1000-event cap). This survives only one process lifetime. If the backend restarts while Stripe is mid-redelivery (which Stripe does aggressively for failed events), the cache is empty and the same event ID can be processed twice → double tier-update, double email, double quota grant.

Move the idempotency cache from in-memory to **Postgres**. Schema:
- Table `processed_stripe_events` with columns `event_id TEXT PRIMARY KEY, processed_at TIMESTAMPTZ DEFAULT NOW()`.
- On webhook receipt, INSERT `event_id` with `ON CONFLICT (event_id) DO NOTHING`. If `INSERT` affected 0 rows (i.e., event already processed), early-return 200 OK without re-running tier-update logic.
- Periodic cleanup: DELETE rows where `processed_at < NOW() - INTERVAL '30 days'` (run on every webhook receipt — opportunistic, low cost, keeps the table bounded).

INVESTIGATE FIRST
1. Read `console/backend/app/routes/billing.py` — find `_processed_events` and where it's gated. Note the function signature, the event-extraction flow, and whether it's called from FastAPI or a side path.
2. Read `console/backend/app/database.py` to see the Postgres connection pattern.
3. Read `console/backend/alembic/versions/` (latest) to see the migration template.
4. Read existing tests in `console/tests/test_billing.py` (Codex extended this earlier) to see how webhooks are tested.

DECIDE & IMPLEMENT

Backend code changes:
1. Create `console/backend/app/models.py` model `ProcessedStripeEvent` (or wherever SQLAlchemy models live in this codebase — check existing patterns).
2. Replace the `_processed_events` OrderedDict logic in `routes/billing.py` with a Postgres lookup/insert.
3. Add the cleanup query (opportunistic, run before the lookup).

Migration:
4. New Alembic migration in `console/backend/alembic/versions/` named like `<sha>_add_processed_stripe_events_table.py`. Use today's date prefix (`20260507_`).

Tests:
5. Update `console/tests/test_billing.py` (Codex's recently-added webhook idempotency test) to match the new contract — duplicate event still early-returns, but now backed by DB.
6. The FakeSession-based unit test should NOT need a real Postgres connection — make sure the test doesn't break. If it would require Postgres, mark it `@pytest.mark.integration` and add a non-integration version using mock.

CONSTRAINTS
- NEVER `git add -A`. Stage explicit files.
- NEVER push. Don't touch live deploy.
- The migration MUST be additive (CREATE TABLE) — never DROP, never ALTER NOT NULL on existing data.
- Do NOT use PowerShell with complex quoting.
- If `routes/billing.py` calls `_processed_events` from multiple sites, keep the helper function's interface but change its implementation.

PROVE IT
1. `cd console && python -m pytest tests/test_billing.py --no-cov --timeout=60 -q` — must still pass.
2. `cd console && python -m pytest tests/ --no-cov --timeout=60 --ignore=tests/e2e -q 2>&1 | tail -3` — overall count must be `>= previous (147 passed)`.
3. Show the migration file and confirm it has both `upgrade()` and `downgrade()`.

REPORT
- Files modified.
- Commit SHAs (logical: model + migration + route + tests).
- Final pytest summary.
- Any decisions about test mocking strategy.
- Heads-up to me: the next backend deploy needs to run `alembic upgrade head` (which `entrypoint.sh` does automatically) — confirm this is true.

Time budget: ~25 min.
