Nobody has started this yet — be first.
Business impact
A common, realistic user story: someone was meditating every day for a week but forgot to open the app, and now wants to log the last 5 days at once instead of firing 5 separate POST /checkins calls by hand (or, worse, giving up on logging them and taking a fake streak reset on the chin). Right now there is no batch check-in endpoint at all -- the single-day POST /habits/{id}/checkins is the only write path. This is a real feature gap that shows up in almost every habit-tracker product on the market ("backfill," "log a missed day").
Problem
app/schemas.py::BulkCheckInCreate (start_date, end_date) and the POST /habits/{habit_id}/checkins/bulk route already exist and call crud.create_checkins_bulk -- but crud.create_checkins_bulk is a stub that unconditionally raises NotImplementedError.
Current behavior
POST /habits/{id}/checkins/bulk currently returns a 500 Internal Server Error for any date range, because crud.create_checkins_bulk unconditionally raises NotImplementedError.
Expected behavior
POST /habits/{id}/checkins/bulk with {"start_date": "...", "end_date": "..."} creates (or no-ops on) a check-in for every calendar date in that range, inclusive on both ends, and returns the full list of CheckInOut rows covering the range. It must be idempotent with respect to dates that were already checked in -- individually or via an earlier bulk call -- the same guarantee create_checkin already provides for a single day.
Steps to reproduce
uvicorn app.main:app --reload
curl -s -X POST http://127.0.0.1:8000/habits/<id>/checkins/bulk -H "Authorization: Bearer <token>"
-H 'Content-Type: application/json' -d '{"start_date": "2026-01-01", "end_date": "2026-01-05"}'
Why this matters
This is a "cross-component" ticket: it touches the schema layer (a new input shape distinct from the single-day CheckInCreate), the route layer, and the CRUD layer, and it has to compose correctly with an existing invariant -- the UniqueConstraint("habit_id", "date") in app/models.py -- rather than fighting it. A naive per-date insert that doesn't check for existing rows first will crash on the very first overlapping date instead of degrading gracefully, exactly the same unique-constraint collision TICKET-07 is about.
Suggested approach
crud.create_checkin(db, habit, checkin_date) already implements the single-day "create if not present" logic this ticket needs, once per date. Think about whether looping and calling it once per day in the range (vs. re-deriving the same existence-check-then-insert logic inline) is the right level of reuse here, and what end_date < start_date or an unreasonably large range should do.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket05_bulk_checkin.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 feat/bulk-checkin-backfillFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/bulk-checkin-backfillSubmit 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 2
Implement a feature
Extend the system within its own patterns.