Skip to content

feat(badges): implement volunteer badge system for engagement & gamification

Suma Pullaiahgari requested to merge ehrs-badges-bhavitha into develop

Merge Request — Volunteer Badge System

Overview

Implements the Volunteer Badge System — a gamification feature that automatically rewards volunteers with badges based on their medical camp attendance and participation consistency. Badges are awarded only when attendance is marked True, not on signup.

What does this MR do and why?

Motivation: Enhance volunteer engagement and retention by recognizing contributions through a structured badge system with 9 achievement levels.

Approach: Badges are evaluated and assigned automatically when a coordinator marks a user's attendance. A manual evaluation endpoint is also available. Badge calculation counts total attended visits and longest consecutive attendance streak.

Trade-offs: Badge evaluation is synchronous with attendance marking — acceptable for current user volume. Can be moved async later.

Changes Made

Files Modified

  • app/api/v1/routes/badge_routes.py — Added 3 new endpoints (/me/badges, /me/badge-progress, /{user_id}/evaluate-badges)
  • app/services/badge_service.py — Fixed 2 bugs in existing badge logic (consecutive visits was broken, progress flag was inverted)
  • app/services/medical_camp_service.py — Removed badge assignment from signup; badges now trigger only on attendance
  • tests/test_services/test_medical_camp_service.py — Updated 6 test mocks to handle badge service queries

Files Added

  • alembic/versions/e3f5a6b7c8d9_fix_badgecriteriaenum_typo.py — Migration that renames typo'd enum badgcriteriaenumbadgecriteriaenum
  • tests/test_services/test_badge_service.py — 20 unit tests covering all badge service functions

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • 💥 Breaking change
  • 📝 Documentation update
  • ️ Refactor (no functional changes)
  • Performance improvement
  • 🧪 Test update
  • 🔧 Configuration change
  • 🚨 Security fix
  • 🗑️ Deprecation

Related Issues / References

Screenshots

image image

API Documentation (/docs)

5 badge endpoints visible under the users tag:

Method Path Description
GET /users/me/badges Get own badges
GET /users/me/badge-progress Get own badge progress
GET /users/{user_id}/badges Get any user's badges
GET /users/{user_id}/badge-progress Get any user's progress
POST /users/{user_id}/evaluate-badges Manually trigger evaluation

Example: GET /users/me/badges

{
  "badges": [
    {
      "name": "First Step",
      "description": "Awarded for first camp visit",
      "earned_at": "2026-04-10T14:30:00"
    },
    {
      "name": "Certified Volunteer",
      "description": "Awarded for 3 camp visits",
      "earned_at": "2026-04-11T09:00:00"
    }
  ],
  "count": 2
}

Example: GET /users/me/badge-progress

{
  "progress": [
    { "badge": "First Step", "current": 5, "required": 1 },
    { "badge": "Certified Volunteer", "current": 5, "required": 3 },
    { "badge": "Consistent Volunteer", "current": 5, "required": 5 },
    { "badge": "On a Roll", "current": 2, "required": 3 },
    { "badge": "Century Contributor", "current": 5, "required": 10 }
  ],
  "next_badge": { "badge": "On a Roll", "current": 2, "required": 3 }
}

How to Validate Locally

Previous Behavior

  • Signup triggered badge evaluation (incorrect — user got badges without attending)
  • Consecutive visit logic always returned 1 (broken date comparison)
  • Badge progress showed earned badges as unearned (inverted flag)
  • Enum badgecriteriaenum didn't exist (typo badgcriteriaenum in migration)

Changes Made

  1. Badge assignment removed from signup_user_for_camp() — signup creates records only
  2. Badge assignment retained in update_user_attendance() — fires only when attendance = True
  3. Consecutive visits rewritten: fetches ALL records, builds streak from attendance flag
  4. Migration created to rename badgcriteriaenumbadgecriteriaenum
  5. Added 3 new endpoints for self-lookup and manual evaluation

New Behavior

  1. Admin/Coordinator marks attendance → attendance = True → badges evaluated
  2. Volunteer signs up → NO badge assigned
  3. Consecutive streak correctly counts attended camps, resets on missed attendance
  4. Badge progress correctly shows earned vs unearned

Testing done

  • Unit tests added/updated
  • API endpoint tests passing

Test Cases Covered:

Scenario Expected Result Status
Invalid UUID returns empty/zero Graceful response, no crash
User with 0 visits gets no badges Empty badge list
User with 1 visit gets First Step badge Badge assigned
User with 3 visits gets First Step + Certified Multiple badges at once
User with 5 consecutive visits gets On a Roll Streak correctly calculated
Missed attendance breaks streak Streak resets to 0
Already-earned badge not re-assigned Idempotent, no duplicates
Signup does NOT trigger badges No badge service call in signup
Attendance DOES trigger badges Badge assigned on attendance mark
Badge progress shows correct earned/unearned earned flag is accurate
next_badge shows closest unearned Correct next target
Multiple badges awarded simultaneously All eligible badges assigned in one call

Test Commands Run:

# Run all tests
uv run pytest tests/

# Run badge-specific tests
uv run pytest tests/test_services/test_badge_service.py -v

# Run medical camp service tests (signup/attendance)
uv run pytest tests/test_services/test_medical_camp_service.py -v

# Result: 1185 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:
    ruff check .
    ruff format . --check

Python & FastAPI Best Practices

  • Functions follow single-responsibility principle
  • Async/await used correctly (no blocking calls in async functions)
  • Dependency injection used appropriately
  • Pydantic models used for request/response validation
  • SQLAlchemy queries are optimized (no N+1 queries)
  • Error handling is comprehensive (try/except with proper logging)

API Design

  • RESTful conventions followed
  • Proper HTTP status codes returned
  • Input validation implemented (UUID format check)
  • Authentication/authorization enforced
  • Role-based access control used for user restriction
  • API documentation (docstrings) updated

Database & Migrations

  • Database migrations created
  • Database migration version points to latest
  • Migrations are reversible (downgrade scripts included)
  • Indexes added for frequently queried fields
  • No raw SQL queries (using SQLAlchemy ORM)
  • Data integrity constraints maintained (unique constraint on user_id + badge_id)

Security

  • No sensitive data logged (passwords, tokens, PII)
  • SQL injection prevention verified (ORM used)
  • Input sanitization implemented
  • Authentication tokens handled securely
  • CORS settings appropriate
  • Security scan passes:
    bandit -r app/

Error Handling

  • Errors are caught and handled gracefully
  • User-friendly error messages returned
  • Errors are logged appropriately
  • HTTP error responses follow API standards

Documentation

  • README.md updated (if setup steps changed)
  • .env.example updated — N/A (no new env vars)
  • API documentation updated (docstrings, OpenAPI specs)
  • CHANGELOG.md will be updated (if applicable)
  • Code comments explain complex logic (not what, but why)

Known Limitations / Technical Debt

  1. Badge evaluation is synchronous — runs in the same request as attendance update. For high-volume environments, consider moving to an async background task.
  2. Race condition on concurrent badge evaluation — handled by unique constraint, but results in IntegrityError instead of graceful handling. Recommend adding try/except IntegrityError in a follow-up.
  3. Visit count includes all roles — if a user attends as both patient and volunteer, both count toward badges. If badges should be volunteer-role-only, add camp_role filter in a follow-up.

Additional Notes

  • The existing badge foundation (models, 2 endpoints, seed script) was already implemented by a previous developer. This MR fixes 2 critical bugs, corrects the badge trigger location, adds 3 endpoints, and adds comprehensive tests.
  • Migration e3f5a6b7c8d9 safely renames a typo'd enum (badgcriteriaenumbadgecriteriaenum) using conditional PostgreSQL logic — works for teammates who have or haven't run the old migration.
  • No existing functionality was broken. All 1185 existing tests pass.

Badge Flow

User signs up for camp → NO badge

Admin/Coordinator marks attendance → attendance = True

badge_service.assign_badges_to_user() called

1. Count total attended visits (attendance = True)
2. Calculate longest consecutive attendance streak
3. Check all 9 badge criteria
4. Skip already-earned badges (unique constraint prevents duplicates)
5. Assign all eligible badges → commit

Badge records inserted into user_badges

9 Badge Definitions

Badge Type Threshold
🌱 First Step visits 1
🎓 Certified Volunteer visits 3
🔄 Consistent Volunteer visits 5
🔥 On a Roll consecutive 3
💯 Century Contributor visits 10
🥉 Bronze Volunteer visits 6
🥈 Silver Volunteer visits 12
🥇 Gold Volunteer visits 20
💎 Elite Volunteer visits 36

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 closes #94 (closed)

Merge request reports

Loading