feat: implement volunteer badge gamification system
Overview
Implements a badge-based gamification system for volunteers in the eHRS application. Volunteers are automatically awarded badges based on their medical camp attendance count and consecutive participation streaks.
What does this MR do and why?
feat: implement volunteer badge gamification system
Motivation: Encourage volunteer engagement and retention by recognizing contributions through structured achievement badges.
Approach:
- Badges are evaluated and awarded automatically when a coordinator marks a volunteer's attendance (
attendance = True) - A manual evaluation endpoint is also available for coordinators/admins
- Badge calculation uses total attended visits and longest consecutive attendance streak
- Badge definitions are seeded automatically on app startup — no manual setup required
Trade-offs:
- Badge evaluation runs as a background task (non-blocking) — acceptable for current volume, can be moved to a task queue (Celery/Redis) if needed at scale
- Race conditions on concurrent evaluation are handled gracefully via
IntegrityErrorcatch + rollback
Changes Made
Files Added
| File | Purpose |
|---|---|
app/models/badge.py |
Badge and UserBadge SQLAlchemy models with BadgeCriteriaEnum
|
app/schemas/badge.py |
Pydantic response schemas (BadgesResponse, BadgeProgressResponse, EvaluateBadgesResponse) |
app/services/badge_service.py |
Badge evaluation logic, progress tracking, auto-seeding |
alembic/versions/f1a2b3c4d5e6_create_badges_and_user_badges_tables.py |
Migration for badges and user_badges tables |
tests/test_services/test_badge_service.py |
28 unit tests for badge service |
tests/test_api/test_badge_routes.py |
9 integration tests for badge endpoints |
Files Modified
| File | Change |
|---|---|
app/models/__init__.py |
Export Badge, UserBadge, BadgeCriteriaEnum
|
app/models/user.py |
Add badges relationship to User
|
app/api/v1/routes/users_routes.py |
Add 5 badge endpoints |
app/api/v1/routes/medical_camp_routes.py |
Trigger badge evaluation via BackgroundTasks on volunteer attendance |
app/main.py |
Seed badge definitions on app startup |
scripts/seed_database.py |
Add seed_badges() in Phase 4, clear badges on --clear
|
tests/unit/services/test_medical_camp_service.py |
Patch badge service in attendance test to fix commit count assertion |
API Endpoints Added
| Method | Route | Auth | Description |
|---|---|---|---|
GET |
/api/v1/users/me/badges |
Any user | Get own earned badges |
GET |
/api/v1/users/me/badge-progress |
Any user | Get own badge progress |
GET |
/api/v1/users/{user_id}/badges |
Coordinator/Admin | Get any user's badges |
GET |
/api/v1/users/{user_id}/badge-progress |
Coordinator/Admin | Get any user's badge progress |
POST |
/api/v1/users/{user_id}/evaluate-badges |
Coordinator/Admin | Manually trigger badge evaluation |
Database Schema
badges table: id, name, description, criteria_type (enum: visits/consecutive_visits), criteria_value
user_badges table: id, user_id (FK), badge_id (FK), earned_at — unique constraint on (user_id, badge_id) prevents duplicates
Technical Details
Badge evaluation flow:
- Coordinator marks volunteer attendance →
attendance = True -
BackgroundTasksfiresevaluate_and_award_badges(db, user_id) - Counts total attended visits (
camp_role=volunteer,attendance=True) - Calculates max consecutive streak by walking all camp dates in order
- Checks all 9 badge criteria, skips already-earned badges
- Inserts new
UserBadgerecords → commits
9 Badge definitions:
| Badge | Type | Threshold |
|---|---|---|
| First Step | visits | 1 |
| Certified Volunteer | visits | 3 |
| Consistent Volunteer | visits | 5 |
| On a Roll | consecutive | 3 |
| Bronze Volunteer | visits | 6 |
| Century Contributor | visits | 10 |
| Silver Volunteer | visits | 12 |
| Gold Volunteer | visits | 20 |
| Elite Volunteer | visits | 36 |
Type of Change
-
✨ New feature (non-breaking change that adds functionality) -
🧪 Test update
Related Issues / References
Closes #94 (closed)
Screenshots or Screen Recordings
{
"badges": [
{
"name": "First Step",
"description": "Awarded for first camp visit",
"earned_at": "2026-04-19T11:06:01.294826"
}
],
"count": 1
}
GET /users/me/badge-progress
{
"progress": [
{ "badge": "First Step", "current": 3, "required": 1 },
{ "badge": "Certified Volunteer", "current": 3, "required": 3 },
{ "badge": "On a Roll", "current": 3, "required": 3 },
{ "badge": "Consistent Volunteer", "current": 3, "required": 5 }
],
"next_badge": { "badge": "Consistent Volunteer", "current": 3, "required": 5 }
}
POST /users/{user_id}/evaluate-badges
{
"message": "2 new badge(s) awarded",
"new_badges": [
{ "name": "Certified Volunteer", "description": "Awarded for 3 camp visits", "earned_at": "2026-04-19T11:25:07.660647" },
{ "name": "On a Roll", "description": "Awarded for 3 consecutive camp visits", "earned_at": "2026-04-19T11:25:07.660647" }
],
"total_badges": 3
}
How to Validate Locally
# 1. Start services
docker compose up --build -d
# 2. Seed database
docker compose exec api uv run python -m scripts.seed_database --clear
# 3. Get tokens
TOKEN=$(curl -s -X POST "http://localhost:8000/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{"book_no": 2, "password": "volunteer123"}' | jq -r '.access_token')
ADMIN=$(curl -s -X POST "http://localhost:8000/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{"book_no": 1, "password": "admin123"}' | jq -r '.access_token')
# 4. Get volunteer user_id
USER_ID=$(curl -s "http://localhost:8000/api/v1/users/?role=volunteer" \
-H "Authorization: Bearer $ADMIN" | jq -r '.[0].id')
# 5. Mark attendance (triggers badge evaluation in background)
curl -s -X PUT "http://localhost:8000/api/v1/medical-camps/camp/$USER_ID/attendance" \
-H "Authorization: Bearer $ADMIN" \
-H "Content-Type: application/json" \
-d '{"logout": false, "camp_role": "volunteer"}' | jq
# 6. Check badges
curl -s "http://localhost:8000/api/v1/users/$USER_ID/badges" \
-H "Authorization: Bearer $ADMIN" | jq
# 7. Check progress
curl -s "http://localhost:8000/api/v1/users/$USER_ID/badge-progress" \
-H "Authorization: Bearer $ADMIN" | jq
# 8. Manual evaluation
curl -s -X POST "http://localhost:8000/api/v1/users/$USER_ID/evaluate-badges" \
-H "Authorization: Bearer $ADMIN" | jq
Previous behaviour: No badge system existed.
New behaviour:
- Badges awarded automatically on volunteer attendance
- No duplicate badges ever assigned
- Multiple badges can be awarded simultaneously
- Consecutive streak correctly resets on missed camps
Testing Done
-
Unit tests added/updated -
API endpoint tests passing
Test Cases Covered:
| Scenario | Expected Result | Status |
|---|---|---|
| User with 0 visits gets no badges | Empty badge list | |
| User with 1 visit gets First Step | Badge assigned | |
| User with 3 visits gets First Step + Certified | Multiple badges at once | |
| 3 consecutive camps awards On a Roll | Streak correctly calculated | |
| Missed camp breaks streak | Streak resets | |
| Already-earned badge not re-assigned | Idempotent, no duplicates | |
| Signup does NOT trigger badges | No badge service call | |
| Attendance DOES trigger badges | Badge assigned on attendance | |
| Concurrent evaluation (IntegrityError) | Graceful rollback, no crash | |
| Invalid UUID returns 400 | HTTP 400 | |
| Non-existent user returns 404 | HTTP 404 | |
| Volunteer cannot access other user's badges | HTTP 403 |
Test Commands Run:
# Run all tests
uv run pytest
# Run badge-specific tests
uv run pytest tests/test_services/test_badge_service.py tests/test_api/test_badge_routes.py -v
# Result: 1207 passed, 6 skipped, 0 failed
Code Quality Checklist
Code Standards
-
Code follows project conventions (naming, structure, formatting) -
No debug statements or commented-out code left -
No unused imports, variables, or functions -
No duplicate code (DRY principle followed) -
Type hints are properly defined -
Ruff checks pass
Python & FastAPI Best Practices
-
Functions follow single-responsibility principle -
Dependency injection used appropriately -
Pydantic models used for request/response validation -
SQLAlchemy queries are optimized (no N+1 queries) -
Error handling is comprehensive
API Design
-
RESTful conventions followed -
Proper HTTP status codes returned -
Input validation implemented (UUID format check) -
Authentication/authorization enforced -
Role-based access control used -
API documentation (docstrings) updated
Database & Migrations
-
Database migrations created -
Migration version points to latest ( f1a2b3c4d5e6) -
Migrations are reversible (downgrade scripts included) -
Indexes added for user_idonuser_badges -
No raw SQL queries (SQLAlchemy ORM used) -
Data integrity constraints maintained (unique constraint on user_id + badge_id)
Security
-
No sensitive data logged -
SQL injection prevention verified (ORM used) -
Authentication tokens handled securely
Error Handling
-
Errors caught and handled gracefully -
User-friendly error messages returned -
HTTP error responses follow API standards
Documentation
-
API documentation updated (docstrings on all 5 endpoints) -
CHANGELOG.md will be updated
Known Limitations / Technical Debt
- Badge evaluation is synchronous within a background task — for high-volume environments, consider moving to Celery/Redis queue
- Only
camp_role=volunteerattendance counts toward badges — attending as doctor/coordinator does not contribute (by design, documented in endpoint docstrings)
Additional Notes
- Badge definitions auto-seed on app startup — no manual DB seeding required for badges
- The
_badges_seededmodule-level flag ensures the seeding check is a zero-cost no-op after the first confirmation - Migration
f1a2b3c4d5e6is fully idempotent — usesIF NOT EXISTSfor the enum type and table existence checks, safe to run against DBs that may already have partial schema
MR Acceptance Checklist
Quality & Correctness
-
Code works as intended and solves the stated problem -
No bugs introduced (existing functionality not broken) -
Edge cases handled appropriately
Maintainability
-
Code is readable and well-organized -
Code is testable and well-tested -
Follows project patterns and conventions
Acceptance Review
-
Reviewed by at least 1 teammate -
Reviewed by product owner




