Film school equipment checkout app in Flask
Flask app for reserving film gear by date, scanner-driven checkout and return, and overdue flags that block new bookings.
pythonflasksqlalchemysqlitealembicpytest
Built 1 time, downloaded 0 times.
Install from your terminal
Run this in your project folder. It signs you in, shows what changes and asks before it overwrites a file.
npx claude-lab add film-school-equipment-checkout-app-in-flask-c5e92bCLAUDE.md# Film school equipment checkout
This Flask app lets film students reserve camera and lighting gear by date range. Staff scan barcodes to check items out and back in, and late returns flag the student and block new bookings until staff clear the flag. There are two roles: student and staff.
## Stack
- Python 3.12, Flask app factory, Flask-SQLAlchemy, Flask-Migrate
- SQLite for development, Jinja templates, pytest
- Ruff for lint and format
## Commands
- Run dev server: `flask --app app run --debug`
- Tests: `pytest -q`
- Lint: `ruff check .`
- Format: `ruff format .`
- Migrate: `flask --app app db upgrade`
- Seed fake data: `flask --app app seed`
- Flag overdue items: `flask --app app flag-overdue`
## Layout
- `app/__init__.py`: app factory and config loading
- `app/models.py`: SQLAlchemy models
- `app/blueprints/{student,staff,auth}/`: routes only
- `app/services/`: `booking.py`, `checkout.py`, `overdue.py`
- `templates/`: Jinja templates extending `base.html`
- `tests/`: pytest tests and `conftest.py` fixtures
## Domain rules
- Item states: `available`, `reserved`, `checked_out`, `maintenance`.
- A reservation covers a date range. No two reservations may overlap for the same item.
- An overdue item flags the student and blocks new bookings until staff clear the flag after the item is returned.
- Blocked students cannot create reservations.
- Barcodes are unique strings on `Item`.
- Scanners act as keyboards: scan input arrives in a text field and ends with Enter.
## Conventions
- Business logic lives in `app/services/`. Route handlers stay thin: parse input, call a service, render or redirect.
- Store datetimes in UTC. Compare calendar dates in the school timezone from `SCHOOL_TZ`.
- Never delete items that have history. Soft delete with `active=False`.
- The overdue check must be idempotent. Running `flask --app app flag-overdue` twice must change nothing the second time.
- Secrets come from environment variables only. Use `SECRET_KEY=YOUR_SECRET_KEY` as the placeholder in `.env.example`. Never commit `.env`.
- Use the `booking-rules` skill when editing booking, checkout, return or overdue code.
- Use the `new-route` skill when adding a route.
## Workflow
- Plan before coding any change that touches several files.
- Run the `verify` skill before saying a task is done.
- Keep changes small and commit after each passing phase.
## Review
@REVIEW.md
.claude/agents/test-runner.md---
name: test-runner
description: Runs pytest and ruff for this project and reports only the failures. Use it instead of the main session when test or lint output would be long.
model: haiku
tools: Bash, Read
disallowedTools: Edit, Write
maxTurns: 8
---
You run the project's test and lint checks and report only what failed.
Steps:
1. Run `pytest -q` from the project root.
2. Run `ruff check .`.
3. If either command is missing or fails to start, say so in one line and stop.
Report format:
- For each failing test: the test id, a traceback of at most 10 lines, and the source file most likely at fault.
- For each ruff error: file, line, and rule code.
- If everything passes, reply with one line: `pytest: pass, ruff: pass`.
Rules:
- Never edit, create or delete files. You are read-only apart from running commands.
- Do not paste passing output, full logs or unrelated warnings.
- Do not read `.env` or any file under `instance/`.
- Do not suggest fixes beyond naming the likely file.
.claude/settings.json{
"permissions": {
"allow": [
"Bash(pytest:*)",
"Bash(ruff:*)",
"Bash(flask:*)",
"Bash(git status)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(pip install -r requirements.txt)"
],
"ask": [
"Bash(git push:*)",
"Bash(pip install:*)",
"Bash(git commit:*)"
],
"deny": [
"Read(./.env)",
"Read(./.env.local)",
"Read(./.env.development)",
"Read(./.env.production)",
"Read(./.env.test)",
"Bash(rm -rf:*)",
"Bash(git reset --hard:*)",
"Bash(sqlite3:*)"
],
"blockWritesOutsideWorkingDirectories": true
},
"env": {
"CLAUDE_CODE_SUBAGENT_MODEL": "haiku"
},
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "python -c \"import sys,json,subprocess;p=json.load(sys.stdin).get('tool_input',{}).get('file_path','');p.endswith('.py') and subprocess.run(['ruff','format',p])\""
}
]
}
]
}
}
.claude/skills/booking-rules/SKILL.md---
name: booking-rules
description: Use when editing booking, checkout, return or overdue code to apply the domain rules for reservations, item states and late flags.
---
# Booking rules
Load this skill whenever you change reservation, checkout, return or overdue logic. The rules live in `app/services/`. Route handlers call those services and do not repeat the rules.
## Overlap rule
Reservations cover inclusive date ranges. Two ranges for the same item conflict when:
```
start_a <= end_b and start_b <= end_a
```
Consequences:
- A booking ending on May 3 conflicts with a booking starting on May 3. Same-day turnaround is therefore a conflict under the current rule.
- If the school wants same-day turnaround allowed, change the rule in one place (`app/services/booking.py`), add a test for it, and record the decision in the open questions of SPEC.md first.
- End before start is invalid. Reject it in the service, not only in the form.
## Item status transitions
Item states: `available`, `reserved`, `checked_out`, `maintenance`.
| From | To | Allowed | Notes |
|---|---|---|---|
| available | reserved | yes | Booking confirmed for a future range |
| reserved | checked_out | yes | Staff scans student ID, then item barcode |
| reserved | available | yes | Reservation cancelled |
| checked_out | available | yes | Return scan, condition notes recorded |
| available | maintenance | yes | Staff marks item out of service |
| maintenance | available | yes | Staff returns item to service |
| checked_out | maintenance | no | Return first, then move to maintenance |
| maintenance | checked_out | no | Item must be available before checkout |
| available | checked_out | no | Checkout requires a reservation link |
| checked_out | reserved | no | Items return before a new booking is linked |
Every transition goes through a service function that checks the current state. Never assign `item.status` directly from a route.
When an item moves to `maintenance` and has future reservations, the service must flag those reservations for staff review. Do not silently cancel them.
## Overdue and block rules
- An item is late when today (in `SCHOOL_TZ`) is past `due_date` and `in_at` is empty.
- A late item flags its student. A flagged student has `blocked = True`.
- Blocked students cannot create new reservations. Check this in the booking service, not only in the UI.
- Staff clear a flag only after the item is returned and the record shows `in_at`. The clear action is a staff-only service call that writes an audit note.
- The overdue check is idempotent. Running `flask --app app flag-overdue` twice must produce the same flags and no duplicate records.
- Compare calendar dates in `SCHOOL_TZ`. Store timestamps in UTC.
## Data handling
- Never delete items with history. Set `active = False` instead.
- Never delete CheckoutRecord rows.
## Test patterns
- Use `freezegun` (`freeze_time`) for every test that depends on today's date.
- Build test data with factory helpers in `tests/conftest.py`, for example `make_item()`, `make_student()`, `make_reservation()`. Do not copy setup code between tests.
- Use the in-memory SQLite fixture from `conftest.py`. Never point tests at `instance/`.
- Each service change needs a test for the allowed path and at least one forbidden path.
## Edge cases to test
- Same-day return and pickup on the same item.
- Weekend loans: confirm the date math does not skip or double count Saturday and Sunday.
- Item moved to maintenance with future reservations: reservations are flagged, not deleted.
- Booking that ends before it starts.
- Overlap with a cancelled reservation: a cancelled reservation must not block new bookings.
- Overdue check run twice on the same day.
- Student blocked, then cleared, then able to book again.
- Return of an item that is not checked out: reject with a clear error.
.claude/skills/new-route/SKILL.md---
name: new-route
description: Checklist for adding a Flask route in this project so blueprints, services, roles, CSRF, templates and tests stay consistent.
argument-hint: "[blueprint] [route-name]"
disable-model-invocation: false
---
# New route
Use this checklist when adding a page or form to the app. Arguments: blueprint name and route name, for example `staff scan-station`.
## Steps
1. **Service first.** Add or change the business logic in `app/services/` (for example `checkout.py`). The service takes plain values and returns a result or raises a clear error. It does not import `request` or `session`.
2. **Thin route.** In `app/blueprints/<blueprint>/routes.py`, the handler reads the form, calls the service, and renders a template or redirects. No queries or rule checks in the handler.
3. **Role decorator.** Protect the route with the project's role decorator (for example `@staff_required` or `@student_required`). Staff routes must be checked on every method, not only in the menu.
4. **CSRF form.** Every POST form includes the CSRF token field. Do not disable CSRF for any route.
5. **Template.** Create the template under `templates/<blueprint>/` and extend `base.html`. Use Jinja autoescape; do not use `|safe` on user input.
6. **Tests.** Add a test for the allowed role and a test for each denied role (student on a staff route, anonymous user on any protected route). Add a test for the service function.
7. **Verify.** Invoke the `verify` skill before saying the route is done.
## Naming conventions
- Blueprint modules: lowercase, singular noun, matching the folder under `app/blueprints/` (`student`, `staff`, `auth`).
- Endpoint names: `<blueprint>.<verb_noun>`, for example `staff.scan_checkout` or `student.reserve_item`.
- Templates: `templates/<blueprint>/<verb_noun>.html`, matching the endpoint name.
- URLs: lowercase with hyphens, for example `/staff/scan-station`.
- Form field names: snake_case, matching the service argument names.
## Things to avoid
- Raw SQL built from strings. Use SQLAlchemy queries with bound parameters.
- Business rules in templates or route handlers.
- Trusting hidden fields or URL parameters for role or ownership. Check the current user against the record in the service.
- Returning other students' bookings. Students see only their own rows; filter by the logged-in user in the query.
- Adding a dependency. Ask first and list it in INSTALL.md only after approval.
## Done when
- The service has a passing test.
- The route has tests for allowed and denied roles.
- The template extends `base.html` and renders with empty and error states.
- The `verify` skill passes.
.claude/skills/verify/SKILL.md---
name: verify
description: Run after any code change to prove the Flask app still works, including lint, tests, migrations and a smoke run.
when_to_use: Use before claiming a task is done, and after every edit to app code, templates, migrations or tests.
---
# Verify
Run these steps in order from the project root with the venv active. Stop at the first failing step.
## Step 1: Lint and format
```
ruff check .
ruff format --check .
```
Both must exit 0.
## Step 2: Tests
```
pytest -q -x
```
If the quick run passes, run the full suite:
```
pytest -q
```
Both must exit 0 with zero failures.
## Step 3: Migrations on a temp database
Use a throwaway database so the real `instance/` database is never touched:
```
DATABASE_URL=sqlite:///tmp_verify.db flask --app app db upgrade
```
The upgrade must finish without errors. Delete `tmp_verify.db` afterwards.
## Step 4: Smoke run
Start the app in the background:
```
flask --app app run --port 5055 &
```
Wait a few seconds, then check the responses:
```
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:5055/
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:5055/catalog
```
Each code must be 200 or 302. The `/catalog` route arrives in Phase 2, so skip that check until the route exists. Stop the server when done (`kill %1` or the matching PID).
## Step 5: Targeted tests for scan or booking changes
If the change touches checkout, return, booking or overdue code, also run the matching test file by name, for example:
```
pytest -q tests/test_checkout.py
pytest -q tests/test_booking.py
```
Use the file names that exist in `tests/`. Do not skip this step when the change is in those areas.
## Report format
Report each step on its own line as PASS or FAIL. For a failure, show only the first failing output, not the full log.
```
Step 1 lint: PASS
Step 2 tests: FAIL
tests/test_booking.py::test_overlap_same_day - AssertionError ...
```
## Rules
- Never mark a task done if any step fails.
- Fix the failure, then rerun the whole verify skill from Step 1, not only the failed step.
- If a check cannot run (missing tool, no venv), report it as FAIL with the reason.
INSTALL.md# Equipment checkout: setup guide
This guide sets up the project and your first Claude Code session. No prior Flask experience is needed.
## Before you start
- Python 3.12
- git
- Claude Code 2.1.296 or newer. Check with `claude --version`.
## 1. Create the project
```
mkdir equipment-checkout
cd equipment-checkout
git init
python -m venv .venv
```
Activate the environment:
- macOS or Linux: `source .venv/bin/activate`
- Windows PowerShell: `.venv\Scripts\Activate.ps1`
Your prompt should now start with `(.venv)`.
Install the packages:
```
pip install flask flask-sqlalchemy flask-migrate pytest ruff freezegun
pip freeze > requirements.txt
```
Add a `.gitignore` that includes `.venv/`, `.env`, `instance/` and `__pycache__/`.
## 2. Add the project files
These files go in the project folder:
- CLAUDE.md: project memory Claude reads at the start of each session.
- SPEC.md: users, flows, data model and acceptance criteria.
- PLAN.md: five build phases, each ending with a /goal prompt.
- REVIEW.md: checklist used by /code-review.
- INSTALL.md: this guide.
- .claude/settings.json: permissions and defaults.
- .claude/skills/: verify, booking-rules and new-route skills.
- .claude/agents/: test-runner subagent that returns only failures.
Phase 1 creates app/, templates/, tests/, pyproject.toml, .env.example and the first migration. You do not need those yet.
## 3. Set up local secrets
After Phase 1 creates `.env.example`, run:
```
cp .env.example .env
```
Open `.env` and replace YOUR_SECRET_KEY with a long random value. You can generate one with:
```
python -c "import secrets; print(secrets.token_hex(32))"
```
Never commit `.env`. Claude is set up not to read it.
## 4. Install plugins
Start Claude Code in the project folder:
```
claude
```
Inside Claude, run:
```
/plugin install code-review@claude-plugins-official
/plugin install feature-dev@claude-plugins-official
/plugin install commit-commands@claude-plugins-official
/plugin install security-guidance@claude-plugins-official
```
Then run `/doctor` and fix anything it flags before you continue.
What each plugin does:
- code-review: reviews each phase before you merge.
- feature-dev: guides bigger features like booking and scanning.
- commit-commands: makes clean commits.
- security-guidance: catches auth, CSRF and injection mistakes.
## 5. Start Phase 1
Paste the Phase 1 kickoff prompt into Claude. It begins with `/goal Phase 1 skeleton` and covers Phase 1 only.
A /goal keeps Claude working until a small model judges the end condition met. That judge only sees what Claude has shown in the conversation, so Claude must print its test results. Each goal stops after 40 turns at most. To stop one early, run `/goal clear`.
## 6. Between phases
- Run `/clear` between phases and between unrelated tasks.
- Commit after each phase passes.
- Paste the next phase's /goal prompt from PLAN.md.
- Run `/compact` at natural breaks in long work.
- Run `/usage` to see where tokens went.
## Troubleshooting
- **`python` not found or wrong version:** install Python 3.12, then recreate the venv.
- **`(.venv)` not showing:** run the activate command again in the same folder.
- **Port in use:** stop the old server, or run `flask --app app run --port 5056`.
- **Migration errors:** if the dev database has no data you need, delete `instance/` and run `flask --app app db upgrade` again.
- **Command blocked:** check `.claude/settings.json`. Ask Claude before adding packages.
- **Plugin not found:** check the spelling in the install command and run it again.
PLAN.md# Build plan: film school equipment checkout
Build in five phases. Finish and commit each phase before starting the next.
## Working rules
- Read CLAUDE.md and SPEC.md before each phase.
- Run `/clear` between phases.
- Commit after each passing phase with the commit-commands plugin.
- Use the verify skill before saying a phase is done.
## Phase 1: Skeleton and models
Scope:
- App factory in `app/__init__.py`, config from environment variables
- Models: User, Category, Item, Reservation, ReservationItem, CheckoutRecord
- Flask-Migrate initial migration
- `flask --app app seed` command (fake data only)
- Pytest fixtures in `tests/conftest.py` with in-memory SQLite
- Ruff config in `pyproject.toml`
- `requirements.txt` and `.env.example`
Phase 1 ends with this `/goal`:
```
/goal Phase 1 of the film school equipment checkout app is complete. End state: app factory in app/__init__.py loads config from environment variables; models User, Category, Item, Reservation, ReservationItem, CheckoutRecord match SPEC.md; an initial Flask-Migrate migration applies cleanly to a fresh database; `flask --app app seed` creates 2 staff, 3 students, 3 categories and 10 items with unique barcodes; a home route returns 200; tests/conftest.py provides fixtures using in-memory SQLite; at least 6 tests pass covering model creation, unique barcode constraint and the seed command; ruff is configured in pyproject.toml; .env.example contains SECRET_KEY=YOUR_SECRET_KEY. Check: `pytest -q` and `ruff check .` both pass with zero failures, and the migration applies to a fresh database. Constraints: no booking, checkout or auth logic yet; add no dependencies beyond those in INSTALL.md; do not read or write .env; no real student data; keep route handlers thin and put logic in app/services. Use the test-runner subagent for test output. Stop after 40 turns and report what remains.
```
## Phase 2: Reservations
Scope:
- Booking service in `app/services/booking.py` with overlap detection (inclusive date ranges)
- Student catalog and reserve pages
- My bookings page
- Blocked-user check on reservation creation
- Tests for conflicts, touching ranges and blocked users
Phase 2 ends with this `/goal`:
```
/goal Phase 2 reservations are complete. End state: app/services/booking.py creates reservations and rejects overlapping ranges using start_a <= end_b and start_b <= end_a; same-day touching ranges are rejected; end date before start date is rejected; blocked users cannot reserve; catalog, reserve form and my bookings pages render for students and show only their own bookings. Check: `pytest -q` and `ruff check .` both pass with zero failures, and tests cover each conflict case and the blocked user case. Constraints: no checkout, return or auth logic yet; add no dependencies beyond those in INSTALL.md; do not read or write .env; keep route handlers thin and put logic in app/services; do not touch unrelated files. Use the test-runner subagent for test output. Stop after 40 turns and report what remains.
```
## Phase 3: Staff scanning
Scope:
- Checkout and return services in `app/services/checkout.py`
- Scan station page with a text field that accepts scanner input ending in Enter
- Condition notes on return
- Handling of unknown barcodes and invalid state transitions
Phase 3 ends with this `/goal`:
```
/goal Phase 3 staff scanning is complete. End state: app/services/checkout.py implements checkout (sets out_at, due_date, status checked_out) and return (sets in_at, condition notes, status available); the scan station page accepts a barcode typed or scanned with Enter; unknown barcodes show an error and change nothing; checking out a checked_out item is rejected; returning an item not checked out is rejected; tests cover each valid transition and each invalid scan. Check: `pytest -q` and `ruff check .` both pass with zero failures. Constraints: no auth or overdue logic yet; add no dependencies beyond those in INSTALL.md; do not read or write .env; keep route handlers thin and put logic in app/services; do not touch unrelated files. Use the test-runner subagent for test output. Stop after 40 turns and report what remains.
```
## Phase 4: Overdue and blocking
Scope:
- Overdue service in `app/services/overdue.py`
- `flask --app app flag-overdue` CLI command, idempotent
- Flags on students and block logic
- Staff overdue list with a clear action
- Tests with frozen dates (freezegun)
Phase 4 ends with this `/goal`:
```
/goal Phase 4 overdue and blocking is complete. End state: app/services/overdue.py flags a student when an item is past due_date with empty in_at; `flask --app app flag-overdue` is idempotent (running it twice changes nothing the second time); flagged students are blocked from new reservations; the staff overdue list shows late items; a staff clear action unblocks a student only when no late items remain. Check: `pytest -q` and `ruff check .` both pass with zero failures, with tests using freezegun for dates and covering the idempotency and clear rules. Constraints: no auth changes yet; add no dependencies beyond those in INSTALL.md; do not read or write .env; keep route handlers thin and put logic in app/services; do not touch unrelated files. Use the test-runner subagent for test output. Stop after 40 turns and report what remains.
```
## Phase 5: Auth and polish
Scope:
- Login with roles and route protection (student, staff, admin)
- CSRF on all POST forms
- Item admin pages for staff
- Empty and error states
- README with run instructions
Phase 5 ends with this `/goal`:
```
/goal Phase 5 auth and polish is complete. End state: login works with local accounts; student routes show only the student's own data; staff and admin routes reject students with 403; every POST form includes a CSRF token; item admin lets staff add, edit and deactivate items (active=False, no hard deletes); catalog, bookings and overdue pages show empty-state messages; README explains setup and run steps. Check: `pytest -q` and `ruff check .` both pass with zero failures, and tests cover role denial and CSRF rejection. Constraints: add no dependencies beyond those in INSTALL.md without asking first; do not read or write .env; no real student data; keep route handlers thin; do not touch unrelated files. Use the test-runner subagent for test output. Stop after 40 turns and report what remains, including open questions from SPEC.md.
```
## After Phase 5
- Run `/code-review` on the full branch.
- Review findings by severity and fix blockers first.
- Confirm SPEC.md open questions have answers before any v2 work.
REVIEW.md# Review checklist
Use this list for /code-review. Report findings by severity: blocker, major, minor. Skip style nits that ruff covers.
## Booking and dates
- Overlap check uses inclusive ranges: start_a <= end_b and start_b <= end_a.
- Same-day return and pickup follow the rule in the booking-rules skill.
- End date before start date is rejected.
- Dates are stored in UTC and compared as calendar dates in SCHOOL_TZ.
## State and rules
- Item status changes follow the transition table in the booking-rules skill. No silent jumps.
- Blocked and overdue logic cannot be bypassed by direct URL, repeated POST or skipped steps.
- The overdue check stays idempotent.
- Unknown barcodes and returns of items not checked out are rejected, not guessed.
## Access and forms
- Every staff route checks the role on the server. Hiding a link is not enough.
- Students see only their own bookings and records.
- All POST forms carry CSRF protection.
- No raw SQL string building. Use SQLAlchemy queries or bound parameters.
- Jinja autoescape stays on. No |safe on user input.
## Tests and data
- Every service function change has a test.
- Date-dependent tests use freezegun, not the real clock.
- No secrets, .env contents or real student data in the repo or seed.
SPEC.md# Film school equipment checkout: spec
## Purpose
Students reserve camera, lighting, audio and grip gear for a date range. Staff scan barcodes to check gear out and back in. A late return flags the student and blocks new bookings until staff clear the flag.
## Roles
- **Student**: browse the catalog, reserve items for dates, view own bookings, see block status.
- **Staff**: scan out, scan in, manage items, view overdue list, clear flags.
- **Admin** (optional): everything staff can do, plus manage users and categories.
## Data model
- **User**: id, name, email, role (student, staff, admin), blocked (boolean)
- **Category**: id, name
- **Item**: id, barcode (unique string), name, category, status, active, notes
- status: available, reserved, checked_out, maintenance
- active: false means soft deleted; items with history are never hard deleted
- **Reservation**: id, user, start_date, end_date, status
- **ReservationItem**: id, reservation, item
- **CheckoutRecord**: id, item, user, reservation, out_at, due_date, in_at, condition_notes
All timestamps are stored in UTC. Calendar dates are compared in the school timezone set by `SCHOOL_TZ`.
## Flows
### Flow 1: Reserve
1. Student picks a start date and end date.
2. System shows items in the catalog that have no overlapping reservation for those dates.
3. Student selects items and submits.
4. System creates one Reservation with its ReservationItems.
5. Reject if the user is blocked or any selected item conflicts.
### Flow 2: Checkout
1. Staff scans the student ID barcode.
2. Staff scans each item barcode.
3. System links each item to the student's matching reservation for today.
4. System sets out_at to now and due_date to the reservation end date.
5. Item status changes to checked_out.
### Flow 3: Return
1. Staff scans the item barcode.
2. Staff records condition notes.
3. System sets in_at to now and item status to available.
4. If the return is late, the student is flagged and blocked until staff clears the flag.
## Late and block rules
- An item is late when today is past its due_date and in_at is empty.
- A student with a late item is blocked.
- Blocked students cannot create reservations.
- The overdue check is idempotent: running it twice changes nothing the second time.
- Staff clear a flag on the overdue list after the item is returned.
## Pages
- Login
- Catalog
- Reserve form
- My bookings
- Staff dashboard
- Scan station
- Overdue list
- Item admin
## Acceptance criteria
### Reserve
- A student can reserve an available item for a valid date range and sees it in My bookings.
- Booking the same item for an overlapping range is rejected with a clear message.
- Two ranges that touch on the same day (one ends on the day the other starts) are treated as overlapping and rejected.
- End date before start date is rejected.
- A blocked student cannot submit a reservation and sees the block status.
### Checkout
- Staff scanning a student ID then an item barcode with a matching reservation sets the item to checked_out.
- Checkout sets due_date to the reservation end date.
- Scanning an item that is already checked_out is rejected.
- Scanning an unknown barcode shows an error and changes nothing.
### Return
- Scanning a checked-out item sets in_at, records condition notes and sets the item to available.
- Scanning an item that is not checked out is rejected and changes nothing.
- Returning an item after its due_date flags the student and blocks them.
- Scanning an unknown barcode shows an error and changes nothing.
### Overdue and clearing
- The overdue list shows every item that is late.
- Running `flask --app app flag-overdue` twice produces the same flags.
- Staff clearing a flag unblocks the student only when no other late items remain.
## Out of scope for v1
- Email notifications
- Payments and fines
- Mobile app
- Calendar sync
## Open questions
- What is the maximum loan length in days?
- What is the maximum number of items per student at one time?
- Which auth method: local accounts or school SSO?