Skip to content

feat: implement volunteer badge gamification system

Suma Pullaiahgari requested to merge ehrs-badges into develop

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 IntegrityError catch + 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:

  1. Coordinator marks volunteer attendance → attendance = True
  2. BackgroundTasks fires evaluate_and_award_badges(db, user_id)
  3. Counts total attended visits (camp_role=volunteer, attendance=True)
  4. Calculates max consecutive streak by walking all camp dates in order
  5. Checks all 9 badge criteria, skips already-earned badges
  6. Inserts new UserBadge records → 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

image image image image image GET /users/me/badges

{
  "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_id on user_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=volunteer attendance 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_seeded module-level flag ensures the seeding check is a zero-cost no-op after the first confirmation
  • Migration f1a2b3c4d5e6 is fully idempotent — uses IF NOT EXISTS for 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

Merge request reports

Loading