Add core API endpoints with rolling window logic and integration tests #6

Closed
opened 2026-03-16 01:58:21 +00:00 by forgejo_admin · 0 comments
Contributor

Lineage

plan-mcd-tracker → Phase 5: Core API Endpoints + Integration Tests

Repo

forgejo_admin/mcd-tracker-api

User Story

  • log-code: As a user, I want to log a coupon code when I get a McDonald's receipt
  • slots-remaining: As a user, I want to see how many codes I have left at each location
  • reopen-countdown: As a user, I want to know when my next slot reopens
  • redeem: As a user, I want to mark a code as redeemed when I use it
  • history: As a user, I want to browse my history of codes and redemptions
  • admin-stats: As an admin, I want to see aggregate usage stats across all users

Architecture

  • arch-dataflow-mcd-tracker — implements all 4 runtime flows: log code (Flow 1), check availability (Flow 2), redeem (Flow 3), slot limit rejection (Flow 4)
  • arch-domain-mcd-tracker#rolling-window-logic — the SQL queries from the diagram become SQLAlchemy queries

Context

Phases 1-4 complete. FastAPI app is live with SQLAlchemy models (Location, CouponUsage), Alembic migrations, Keycloak JWT auth (get_current_user, require_role). This phase adds all the business logic routes.

The rolling window is the core business rule: 5 codes per location per user per rolling 30-day window. Each code logged starts an independent 30-day timer (expires_at = used_at + 30 days). When expires_at passes, that slot reopens.

File Targets

Files to create:

  • src/mcd_tracker_api/routes/locations.py — Location CRUD routes
  • src/mcd_tracker_api/routes/codes.py — CouponUsage routes (log code, redeem)
  • src/mcd_tracker_api/routes/dashboard.py — Dashboard + admin stats
  • src/mcd_tracker_api/schemas.py — Pydantic request/response schemas (LocationCreate, LocationResponse, CodeCreate, CodeResponse, SlotStatus, DashboardResponse, AdminStats)
  • tests/test_locations.py — location CRUD tests
  • tests/test_codes.py — code logging, redeem, slot limit tests
  • tests/test_dashboard.py — dashboard + admin stats tests

Files to modify:

  • src/mcd_tracker_api/main.py — register new routers
  • tests/conftest.py — add auth override fixture (mock get_current_user for tests)

Files NOT to touch:

  • src/mcd_tracker_api/auth.py — auth module is complete
  • src/mcd_tracker_api/models.py — models are complete
  • src/mcd_tracker_api/database.py — database layer is complete

Acceptance Criteria

  • POST /locations creates a location for the authenticated user
  • GET /locations returns only the authenticated user's locations
  • POST /locations/{id}/codes logs a code with expires_at = used_at + 30 days
  • POST /locations/{id}/codes returns 409 when 5 active codes exist at that location
  • 409 response includes next_reopen date (MIN(expires_at) of active codes)
  • GET /locations/{id}/codes returns codes for a location (auth user only)
  • PATCH /codes/{id}/redeem sets redeemed=True and redeemed_at=NOW()
  • GET /locations/{id}/slots returns {slots_remaining: N, next_reopen: date|null}
  • GET /dashboard returns all user's locations with slot status in one response
  • GET /admin/stats returns aggregate stats (total users, total codes, popular locations) — admin only
  • GET /admin/stats returns 403 for non-admin users
  • User A cannot see User B's locations or codes (multi-user isolation)
  • Expired codes (expires_at < NOW()) don't count toward the 5-slot limit

Test Expectations

  • test_locations.py — CRUD: create, list, list-empty, create-for-different-user-invisible
  • test_codes.py — log code, log 5 codes, log 6th → 409, redeem, redeem-already-redeemed → 400, expired-code-frees-slot
  • test_dashboard.py — dashboard with mixed locations, admin stats, admin-only guard
  • All tests use mocked auth (override get_current_user dependency) + real Postgres
  • Run command: pytest tests/ -v

Constraints

  • Auth: all routes except /healthz require get_current_user. Admin stats requires require_role("admin")
  • Use Pydantic schemas for all request/response models (not raw dicts)
  • Rolling window: use expires_at > func.now() in SQLAlchemy queries, not Python datetime comparison
  • 409 Conflict for slot limit (not 400 or 403)
  • Dashboard should be a single efficient query (avoid N+1 — use subquery or window function)
  • Follow basketball-api route style: router = APIRouter(prefix="/locations", tags=["locations"])

Checklist

  • PR opened with Closes #5
  • All tests pass (should be 40+ tests total)
  • Ruff clean
  • No unrelated changes
  • project-mcd-tracker — project page
  • phase-mcd-tracker-5-core-api — phase note
  • arch-dataflow-mcd-tracker — runtime flow diagrams
  • arch-domain-mcd-tracker — entity model + rolling window SQL
  • plan-mcd-tracker — parent plan
### Lineage `plan-mcd-tracker` → Phase 5: Core API Endpoints + Integration Tests ### Repo `forgejo_admin/mcd-tracker-api` ### User Story - `log-code`: As a user, I want to log a coupon code when I get a McDonald's receipt - `slots-remaining`: As a user, I want to see how many codes I have left at each location - `reopen-countdown`: As a user, I want to know when my next slot reopens - `redeem`: As a user, I want to mark a code as redeemed when I use it - `history`: As a user, I want to browse my history of codes and redemptions - `admin-stats`: As an admin, I want to see aggregate usage stats across all users ### Architecture - `arch-dataflow-mcd-tracker` — implements all 4 runtime flows: log code (Flow 1), check availability (Flow 2), redeem (Flow 3), slot limit rejection (Flow 4) - `arch-domain-mcd-tracker#rolling-window-logic` — the SQL queries from the diagram become SQLAlchemy queries ### Context Phases 1-4 complete. FastAPI app is live with SQLAlchemy models (Location, CouponUsage), Alembic migrations, Keycloak JWT auth (get_current_user, require_role). This phase adds all the business logic routes. The rolling window is the core business rule: **5 codes per location per user per rolling 30-day window**. Each code logged starts an independent 30-day timer (expires_at = used_at + 30 days). When expires_at passes, that slot reopens. ### File Targets Files to create: - `src/mcd_tracker_api/routes/locations.py` — Location CRUD routes - `src/mcd_tracker_api/routes/codes.py` — CouponUsage routes (log code, redeem) - `src/mcd_tracker_api/routes/dashboard.py` — Dashboard + admin stats - `src/mcd_tracker_api/schemas.py` — Pydantic request/response schemas (LocationCreate, LocationResponse, CodeCreate, CodeResponse, SlotStatus, DashboardResponse, AdminStats) - `tests/test_locations.py` — location CRUD tests - `tests/test_codes.py` — code logging, redeem, slot limit tests - `tests/test_dashboard.py` — dashboard + admin stats tests Files to modify: - `src/mcd_tracker_api/main.py` — register new routers - `tests/conftest.py` — add auth override fixture (mock get_current_user for tests) Files NOT to touch: - `src/mcd_tracker_api/auth.py` — auth module is complete - `src/mcd_tracker_api/models.py` — models are complete - `src/mcd_tracker_api/database.py` — database layer is complete ### Acceptance Criteria - [ ] `POST /locations` creates a location for the authenticated user - [ ] `GET /locations` returns only the authenticated user's locations - [ ] `POST /locations/{id}/codes` logs a code with `expires_at = used_at + 30 days` - [ ] `POST /locations/{id}/codes` returns 409 when 5 active codes exist at that location - [ ] 409 response includes `next_reopen` date (MIN(expires_at) of active codes) - [ ] `GET /locations/{id}/codes` returns codes for a location (auth user only) - [ ] `PATCH /codes/{id}/redeem` sets redeemed=True and redeemed_at=NOW() - [ ] `GET /locations/{id}/slots` returns `{slots_remaining: N, next_reopen: date|null}` - [ ] `GET /dashboard` returns all user's locations with slot status in one response - [ ] `GET /admin/stats` returns aggregate stats (total users, total codes, popular locations) — admin only - [ ] `GET /admin/stats` returns 403 for non-admin users - [ ] User A cannot see User B's locations or codes (multi-user isolation) - [ ] Expired codes (expires_at < NOW()) don't count toward the 5-slot limit ### Test Expectations - [ ] `test_locations.py` — CRUD: create, list, list-empty, create-for-different-user-invisible - [ ] `test_codes.py` — log code, log 5 codes, log 6th → 409, redeem, redeem-already-redeemed → 400, expired-code-frees-slot - [ ] `test_dashboard.py` — dashboard with mixed locations, admin stats, admin-only guard - [ ] All tests use mocked auth (override get_current_user dependency) + real Postgres - Run command: `pytest tests/ -v` ### Constraints - Auth: all routes except `/healthz` require `get_current_user`. Admin stats requires `require_role("admin")` - Use Pydantic schemas for all request/response models (not raw dicts) - Rolling window: use `expires_at > func.now()` in SQLAlchemy queries, not Python datetime comparison - 409 Conflict for slot limit (not 400 or 403) - Dashboard should be a single efficient query (avoid N+1 — use subquery or window function) - Follow basketball-api route style: router = APIRouter(prefix="/locations", tags=["locations"]) ### Checklist - [ ] PR opened with `Closes #5` - [ ] All tests pass (should be 40+ tests total) - [ ] Ruff clean - [ ] No unrelated changes ### Related - `project-mcd-tracker` — project page - `phase-mcd-tracker-5-core-api` — phase note - `arch-dataflow-mcd-tracker` — runtime flow diagrams - `arch-domain-mcd-tracker` — entity model + rolling window SQL - `plan-mcd-tracker` — parent plan
Commenting is not possible because the repository is archived.
No labels
No milestone
No project
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ldraney/mcd-tracker-api#6
No description provided.