Nobody has started this yet — be first.
Business impact
GET /habits/{id}/stats is the number a user sees on any "how am I doing this week" screen. Right now, a check-in made today never counts toward this week's (or this month's) completion rate -- it only starts counting tomorrow. A user who checks in every single day will see their weekly completion rate permanently one day behind reality, which reads as "the app doesn't notice when I show up." This is exactly the kind of quiet correctness bug that erodes trust in a habit-tracking product without ever throwing an error -- nobody files a support ticket for "my percentage looks 1/7th too low," they just stop trusting the number.
Problem
habit_stats computes the trailing window as window_start = today - timedelta(days=days), then window = {window_start + timedelta(days=i) for i in range(days)}. For a 7-day week, this produces the date range [today-7, ..., today-1] -- today itself is never included in the set.
Current behavior
A check-in made today still shows "completed_days": 0 (and a correspondingly low completion_rate) in GET /habits/{id}/stats?period=week, even though the user just checked in a moment ago.
Expected behavior
The trailing week/month window should be the days calendar days ending at and including today: [today-(days-1), ..., today]. A check-in made today must be reflected in completed_days and completion_rate immediately, not the next day.
Steps to reproduce
cd fastapi/habit_tracker source .venv/bin/activate # or: python3 -m venv .venv && pip install -r requirements-dev.txt python - <<'PY' from fastapi.testclient import TestClient from app.main import app c = TestClient(app) c.post("/auth/register", json={"email": "demo@example.com", "password": "supersecret1"}) token = c.post("/auth/login", data={"username": "demo@example.com", "password": "supersecret1"}).json()["access_token"] h = c.post("/habits", json={"name": "Meditate", "timezone": "UTC"}, headers={"Authorization": f"Bearer {token}"}).json() c.post(f"/habits/{h['id']}/checkins", json={}, headers={"Authorization": f"Bearer {token}"}) # check in today print(c.get(f"/habits/{h['id']}/stats?period=week", headers={"Authorization": f"Bearer {token}"}).json()) PY
Why this matters
window_start is off by one calendar day, which shifts the entire window back by a day instead of shrinking or growing it symmetrically. It's the classic "did I mean N days back inclusive of today, or N days back exclusive" boundary mistake -- the same category of bug the rest of this project's compute_current_streak/compute_longest_streak functions were clearly written carefully to avoid (see README.md's "today not yet checked in" discussion), which makes this one stand out as an oversight rather than a design choice.
Suggested approach
Look at how window_start relates to days and to today. Ask: for a 7-day window that must include today, what's the earliest date in that window? Compare against how compute_current_streak and compute_longest_streak already reason about "today" as an inclusive endpoint elsewhere in this same file.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket01_stats_window.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
$git checkout -b fix/stats-window-off-by-oneFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/stats-window-off-by-oneSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign indocker compose up --buildfastapi/habit_trackerRun the project's own suite with pytest -v (or pytest tests/ -q for a quick pass/fail count) from fastapi/habit_tracker -- a fresh clone should show a handful of failures until TICKET-03 and TICKET-06 are fixed.
Work the tickets in practice_tickets/ (TICKET-01 through TICKET-07); each names one pytest file under practice_tickets/tests/:
pytest practice_tickets/tests/test_ticket01_stats_window.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 7, clean pass/fail summary
./practice_tickets/run_tickets.sh 03 07 # just a subset
practice_tickets/tests/ is intentionally outside pyproject.toml's testpaths = ["tests"], so a plain pytest run from the repo root never picks these up -- they're learning exercises, not part of the project's CI-gating regression suite.
Level 1
Fix a bug
Read existing behaviour, correct it.