Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ jobs:
working-directory: ./backend
run: |
echo "POSTGRES_TEST_DATABASE_URL=postgresql://testuser:testpassword@localhost:5432/llsc_test" >> .env
echo "POSTGRES_DATABASE_URL=postgresql://testuser:testpassword@localhost:5432/llsc_test" >> .env
echo "SECRET_KEY=test-secret-key-for-ci" >> .env
echo "ENVIRONMENT=test" >> .env

Expand Down Expand Up @@ -163,6 +164,7 @@ jobs:
working-directory: ./backend
run: |
echo "POSTGRES_TEST_DATABASE_URL=postgresql://testuser:testpassword@localhost:5432/llsc_test" >> .env
echo "POSTGRES_DATABASE_URL=postgresql://testuser:testpassword@localhost:5432/llsc_test" >> .env
echo "SECRET_KEY=test-secret-key-for-ci" >> .env
echo "ENVIRONMENT=test" >> .env
echo "TEST_SCRIPT_BACKEND_URL=http://localhost:8000" >> .env
Expand Down
23 changes: 23 additions & 0 deletions backend/app/interfaces/matching_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from abc import ABC, abstractmethod
from typing import Any, Dict, List
from uuid import UUID


class IMatchingService(ABC):
"""
Interface for the Matching Service, defining methods to find
potential matches between users.
"""

@abstractmethod
async def get_matches(self, user_id: UUID) -> List[Dict[str, Any]]:
"""
Find potential matches based on the given user ID.

:param user_id: ID of the user to find matches for
:type user_id: UUID
:return: List of dictionaries with 'user' and 'score' keys
:rtype: List[Dict[str, Any]]
:raises Exception: If matching process fails
"""
pass
33 changes: 33 additions & 0 deletions backend/app/routes/matching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session

from app.schemas.matching import RelevantUsersResponse
from app.services.implementations.matching_service import MatchingService
from app.utilities.db_utils import get_db

router = APIRouter(
prefix="/matching",
tags=["matching"],
)


def get_matching_service(db: Session = Depends(get_db)):
return MatchingService(db)


@router.get("/{user_id}", response_model=RelevantUsersResponse)
async def get_matches(user_id: UUID, matching_service: MatchingService = Depends(get_matching_service)):
"""
Get potential user matches based on the user's profile.
"""
try:
matched_data = await matching_service.get_matches(user_id)
return RelevantUsersResponse(matches=matched_data)
except ValueError as ve:
raise HTTPException(status_code=404, detail=str(ve))
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
26 changes: 26 additions & 0 deletions backend/app/schemas/matching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
Pydantic schemas for matching-related data validation and serialization.
"""

from typing import List

from pydantic import BaseModel

from .user import UserBase


class MatchedUser(BaseModel):
"""
Schema for a matched user with their compatibility score.
"""

user: UserBase
score: float


class RelevantUsersResponse(BaseModel):
"""
Response schema for matching endpoint containing a list of relevant users with scores.
"""

matches: List[MatchedUser]
2 changes: 2 additions & 0 deletions backend/app/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class UserBase(BaseModel):
email: EmailStr
role: UserRole

model_config = ConfigDict(from_attributes=True)


class UserCreateRequest(UserBase):
"""
Expand Down
30 changes: 30 additions & 0 deletions backend/app/seeds/match_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Seed match status data."""

from sqlalchemy.orm import Session

from app.models.MatchStatus import MatchStatus


def seed_match_status(session: Session) -> None:
"""Seed the match_status table with default statuses."""

match_status_data = [
{"id": 1, "name": "pending"},
{"id": 2, "name": "confirmed"},
{"id": 3, "name": "cancelled"},
{"id": 4, "name": "completed"},
{"id": 5, "name": "no_show"},
{"id": 6, "name": "rescheduled"},
]

for status_data in match_status_data:
# Check if status already exists
existing_status = session.query(MatchStatus).filter_by(id=status_data["id"]).first()
if not existing_status:
status = MatchStatus(**status_data)
session.add(status)
print(f"Added match status: {status_data['name']}")
else:
print(f"Match status already exists: {status_data['name']}")

session.commit()
181 changes: 181 additions & 0 deletions backend/app/seeds/ranking_preferences.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
"""Seed ranking preferences data."""

from sqlalchemy.orm import Session

from app.models.RankingPreference import RankingPreference
from app.models.User import User
from app.utilities.form_constants import QualityId, TreatmentId


def seed_ranking_preferences(session: Session) -> None:
"""Seed the ranking_preferences table with sample ranking data for testing all matching cases."""

# Find users by email instead of hardcoding UUIDs

# Test Case 1: Patient wants cancer patient volunteer
sarah_user = session.query(User).filter_by(email="[email protected]").first()
sarah_id = sarah_user.id if sarah_user else None

# Test Case 2: Caregiver wants ONLY cancer patient volunteers
lisa_user = session.query(User).filter_by(email="[email protected]").first()
lisa_id = lisa_user.id if lisa_user else None

# Test Case 3: Caregiver wants ONLY caregiver volunteers
karen_user = session.query(User).filter_by(email="[email protected]").first()
karen_id = karen_user.id if karen_user else None

ranking_data = [
# CASE 1: Sarah (patient) wants patient volunteers - 5 preferences
{
"user_id": sarah_id,
"target_role": "patient",
"kind": "quality",
"quality_id": QualityId.SAME_DIAGNOSIS,
"scope": "self",
"rank": 1,
},
{
"user_id": sarah_id,
"target_role": "patient",
"kind": "quality",
"quality_id": QualityId.SAME_GENDER_IDENTITY,
"scope": "self",
"rank": 2,
},
{
"user_id": sarah_id,
"target_role": "patient",
"kind": "quality",
"quality_id": QualityId.SAME_AGE,
"scope": "self",
"rank": 3,
},
{
"user_id": sarah_id,
"target_role": "patient",
"kind": "treatment",
"treatment_id": TreatmentId.CHEMOTHERAPY,
"scope": "self",
"rank": 4,
},
{
"user_id": sarah_id,
"target_role": "patient",
"kind": "treatment",
"treatment_id": TreatmentId.RADIATION,
"scope": "self",
"rank": 5,
},
# CASE 2: Lisa (caregiver) wants ONLY patient volunteers - 5 preferences
{
"user_id": lisa_id,
"target_role": "patient",
"kind": "quality",
"quality_id": QualityId.SAME_DIAGNOSIS,
"scope": "loved_one",
"rank": 1,
},
{
"user_id": lisa_id,
"target_role": "patient",
"kind": "quality",
"quality_id": QualityId.SAME_GENDER_IDENTITY,
"scope": "loved_one",
"rank": 2,
},
{
"user_id": lisa_id,
"target_role": "patient",
"kind": "quality",
"quality_id": QualityId.SAME_AGE,
"scope": "loved_one",
"rank": 3,
},
{
"user_id": lisa_id,
"target_role": "patient",
"kind": "treatment",
"treatment_id": TreatmentId.CHEMOTHERAPY,
"scope": "loved_one",
"rank": 4,
},
{
"user_id": lisa_id,
"target_role": "patient",
"kind": "treatment",
"treatment_id": TreatmentId.RADIATION,
"scope": "loved_one",
"rank": 5,
},
# CASE 3: Karen (caregiver) wants ONLY caregiver volunteers - 5 preferences
{
"user_id": karen_id,
"target_role": "caregiver",
"kind": "quality",
"quality_id": QualityId.SAME_MARITAL_STATUS,
"scope": "self",
"rank": 1,
},
{
"user_id": karen_id,
"target_role": "caregiver",
"kind": "quality",
"quality_id": QualityId.SAME_PARENTAL_STATUS,
"scope": "self",
"rank": 2,
},
{
"user_id": karen_id,
"target_role": "caregiver",
"kind": "quality",
"quality_id": QualityId.SAME_GENDER_IDENTITY,
"scope": "self",
"rank": 3,
},
{
"user_id": karen_id,
"target_role": "caregiver",
"kind": "quality",
"quality_id": QualityId.SAME_AGE,
"scope": "self",
"rank": 4,
},
{
"user_id": karen_id,
"target_role": "caregiver",
"kind": "quality",
"quality_id": QualityId.SAME_DIAGNOSIS,
"scope": "loved_one", # Match caregiver's loved one diagnosis with volunteer's loved one
"rank": 5,
},
]

for pref_data in ranking_data:
# Skip Karen's preferences if she doesn't exist yet
if pref_data["user_id"] is None:
continue

# Check if preference already exists
existing_pref = (
session.query(RankingPreference)
.filter_by(
user_id=pref_data["user_id"],
target_role=pref_data["target_role"],
kind=pref_data["kind"],
rank=pref_data["rank"],
)
.first()
)

if not existing_pref:
preference = RankingPreference(**pref_data)
session.add(preference)
print(
f"Added ranking preference: {pref_data['kind']} rank {pref_data['rank']} for user {pref_data['user_id']}"
)
else:
print(
f"Ranking preference already exists: {pref_data['kind']} rank {pref_data['rank']} for user {pref_data['user_id']}"
)

session.commit()
4 changes: 4 additions & 0 deletions backend/app/seeds/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
from .experiences import seed_experiences
from .forms import seed_forms
from .qualities import seed_qualities
from .ranking_preferences import seed_ranking_preferences
from .roles import seed_roles
from .treatments import seed_treatments
from .users import seed_users

# Load environment variables
load_dotenv()
Expand Down Expand Up @@ -55,6 +57,8 @@ def seed_database(verbose: bool = True) -> None:
("Experiences", seed_experiences),
("Qualities", seed_qualities),
("Forms", seed_forms),
("Users", seed_users),
("Ranking Preferences", seed_ranking_preferences),
]

for name, seed_func in seed_functions:
Expand Down
Loading