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
22 changes: 22 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Repository Guidelines

## Project Structure & Module Organization
The repository separates API and UI concerns. `backend/app` houses the FastAPI stack—middlewares, models, services, routes, schemas, and utilities—with migrations in `backend/migrations`. Automated tests live in `backend/tests` (unit and functional) plus `e2e-tests` for request-driven smoke checks. The Next.js client resides in `frontend/src`, where `components`, `pages`, `contexts`, `APIClients`, and `utils` cover the feature surface.

## Build, Test, and Development Commands
- `docker-compose up -d` boots Postgres and supporting services; pair with `docker-compose down -v` to rebuild from scratch.
- `cd backend && pdm install` prepares Python deps; `pdm run dev` starts FastAPI on `http://localhost:8080`.
- `cd frontend && npm install` sets up the UI; `npm run dev` runs the Next.js app on `http://localhost:3000`.
- `pdm run tests` executes the backend pytest suite; `npm run lint` runs ESLint/TypeScript checks; `npm run build` verifies a production bundle.

## Coding Style & Naming Conventions
Backend code targets Python 3.12 with Ruff enforcing 120-character lines, import sorting, and lint rules; run `pdm run ruff format` before committing. Use snake_case for files, modules, and functions, PascalCase for SQLAlchemy models and Pydantic schemas, and keep service interfaces under `services/interfaces`. Frontend TypeScript follows Prettier defaults (2-space indentation) and Next.js conventions: PascalCase React components, camelCase hooks, and colocated styles in `src/styles` when needed.

## Testing Guidelines
Pytest expects files named `test_*.py` in `backend/tests`, with async fixtures available for FastAPI routes. Focus unit tests on service logic and place HTTP flow checks in `tests/functional`. Gather coverage with `pytest --cov=app` when shipping high-risk changes. The `e2e-tests` directory hosts request-based regression scripts; run them against a live stack (`BACKEND_URL` set) before deploying auth or entity flows. Frontend currently relies on linting and type checks—add component tests under `frontend/src/__tests__` when introducing complex interactions.

## Commit & Pull Request Guidelines
Commits should be atomic and written in imperative mood (e.g., “Add intake confirmation flow”), mirroring existing history. Reference tickets in the body when relevant. Pull requests must summarize scope, note migrations or env updates, and include screenshots or GIFs for UI changes. Confirm linting, formatting, and tests ran successfully, and call out follow-up items so reviewers can assess risk quickly.

## Security & Configuration Tips
Never commit secrets. Copy `.env.sample` into `backend/.env` and `frontend/.env`, sourcing values from the LLSC vault. Keep `backend/serviceAccountKey.json` local; it is already gitignored. When adding loggers, register names via `app/utilities/constants.py` to preserve structured logging across services.
180 changes: 180 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

LLSC (Leukemia & Lymphoma Society of Canada) is a full-stack application built for matching participants with volunteers. The application uses a monorepo structure with separate frontend and backend services.

**Tech Stack:**
- Backend: FastAPI (Python 3.12), SQLAlchemy, PostgreSQL, Alembic migrations
- Frontend: Next.js, React, TypeScript, Chakra UI
- Infrastructure: Docker, Docker Compose
- Auth: Firebase Authentication
- Email: AWS SES

## Development Commands

### Initial Setup
```bash
# Start Docker containers (required for database)
docker-compose up -d

# Backend setup
cd backend
pdm install
pdm run dev # Runs on http://localhost:8000

# Frontend setup
cd frontend
npm install
npm run dev # Runs on http://localhost:3000
```

### Backend Commands
```bash
cd backend

# Development
pdm run dev # Start FastAPI dev server (port 8000)

# Database
pdm run alembic revision --autogenerate -m "message" # Create migration
pdm run alembic upgrade head # Apply migrations
pdm run seed # Seed database with reference data
pdm run db-reset # Reset DB: down, up, migrate, seed

# Testing
pdm run tests # Run pytest tests

# Linting & Formatting
pdm run ruff check . # Check linting
pdm run ruff check --fix . # Fix linting issues
pdm run ruff format . # Format code
pdm run precommit # Run pre-commit hooks manually
```

### Frontend Commands
```bash
cd frontend

npm run dev # Start Next.js dev server
npm run build # Build for production
npm run lint # Run ESLint
npm run lint:fix # Fix ESLint issues
npm run prettier:check # Check formatting
npm run prettier:fix # Fix formatting
npm run format # Run both prettier and eslint fixes
```

### Docker Commands
```bash
# Database access
docker exec -it llsc_db psql -U postgres -d llsc

# Container management
docker-compose up --build # Rebuild and start
docker-compose down # Stop containers
docker-compose down --volumes # Stop and remove volumes
docker ps # List running containers
```

## Architecture

### Backend Structure

The backend follows a **layered service architecture**:

- **Models** (`app/models/`): SQLAlchemy ORM models. All models must be imported in `app/models/__init__.py` for Alembic autogeneration to work.
- **Schemas** (`app/schemas/`): Pydantic schemas for request/response validation.
- **Routes** (`app/routes/`): FastAPI route handlers that accept requests and return responses.
- **Services** (`app/services/`): Business logic layer that routes call into.
- `implementations/`: Concrete service implementations
- **Interfaces** (`app/interfaces/`): Abstract base classes defining service contracts.
- **Middleware** (`app/middleware/`): Request/response middleware (e.g., `AuthMiddleware`).
- **Utilities** (`app/utilities/`): Shared utilities, constants, Firebase, SES initialization.
- **Seeds** (`app/seeds/`): Database seeding scripts for reference data.

**Key Backend Files:**
- `app/server.py`: FastAPI application initialization, middleware setup, route registration
- `app/__init__.py`: Contains `run_migrations()` which auto-runs Alembic migrations on startup
- `app/utilities/constants.py`: Contains `LOGGER_NAME()` function for creating standardized loggers

**Authentication Flow:**
- Firebase tokens are validated via `AuthMiddleware`
- Public paths are defined in `server.py` (`PUBLIC_PATHS`)
- Protected routes require valid Firebase auth tokens

**Database Migrations:**
When adding a new model, you MUST:
1. Create the model file in `app/models/`
2. Import it in `app/models/__init__.py` and add to `__all__`
3. Run `pdm run alembic revision --autogenerate -m "description"`
4. Run `pdm run alembic upgrade head`

### Frontend Structure

The frontend is a **Next.js application** using TypeScript and Chakra UI:

- **Pages** (`src/pages/`): Next.js file-based routing
- `participant/`: Participant-specific pages (intake, ranking)
- `volunteer/`: Volunteer-specific pages (intake, secondary application)
- `admin/`: Admin dashboard pages
- **Components** (`src/components/`): Reusable React components
- `auth/`: Authentication-related components
- `intake/`: Form intake components
- `ranking/`: Ranking interface components
- `ui/`: Base UI components
- **API Clients** (`src/APIClients/`): API communication layer with backend
- **Contexts** (`src/contexts/`): React Context providers for global state
- **Hooks** (`src/hooks/`): Custom React hooks
- **Types** (`src/types/`): TypeScript type definitions
- **Utils** (`src/utils/`): Utility functions

**Key Frontend Files:**
- `src/pages/_app.tsx`: Next.js app wrapper, providers setup
- `src/pages/_document.tsx`: Custom document for Next.js

## Version Control

### Branching
- Branch off `main` for all feature work
- Branch naming: `<github-username>/<ticket-number>-description` (e.g., `mslwang/LLSC-42-readme-update`)
- Use rebase instead of merge to integrate main: `git pull origin main --rebase`

### Commits
- Commits should be atomic and self-contained
- Use imperative tense with capitalized first word (e.g., "Add user authentication")
- Squash trivial commits (typos, formatting) using `git rebase -i`

## Logging

To add a logger to any file:
```python
from app.utilities.constants import LOGGER_NAME
import logging

log = logging.getLogger(LOGGER_NAME("my_service"))
```

New logger names must be added to `alembic.ini` under the logger section.

## Testing

- Backend tests: `pdm run tests` (runs pytest in `backend/tests/`)
- Test structure: `tests/unit/` and `tests/functional/`
- CI runs linting, formatting checks, unit tests, and security scans on every PR

## Firebase Configuration

Backend requires:
1. `serviceAccountKey.json` in `backend/` directory (obtain from Firebase Console)
2. `FIREBASE_WEB_API_KEY` in `.env` file

## Environment Variables

Environment variables are stored in:
- Backend: `.env` in project root (loaded by backend service)
- Frontend: `frontend/.env`

Secrets are documented in the LLSC Notion workspace.
22 changes: 22 additions & 0 deletions backend/app/models/User.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import uuid
from enum import Enum as PyEnum

from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
from sqlalchemy import Enum as SQLEnum
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship

from .Base import Base
from .Match import Match


class FormStatus(str, PyEnum):
INTAKE_TODO = "intake-todo"
INTAKE_SUBMITTED = "intake-submitted"
RANKING_TODO = "ranking-todo"
RANKING_SUBMITTED = "ranking-submitted"
SECONDARY_APPLICATION_TODO = "secondary-application-todo"
SECONDARY_APPLICATION_SUBMITTED = "secondary-application-submitted"
COMPLETED = "completed"


class User(Base):
__tablename__ = "users"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
Expand All @@ -18,6 +30,16 @@ class User(Base):
auth_id = Column(String, nullable=False)
approved = Column(Boolean, default=False)
active = Column(Boolean, nullable=False, default=True)
form_status = Column(
SQLEnum(
FormStatus,
name="form_status_enum",
create_type=False,
values_callable=lambda enum_cls: [member.value for member in enum_cls],
),
nullable=False,
default=FormStatus.INTAKE_TODO,
)

role = relationship("Role")

Expand Down
3 changes: 2 additions & 1 deletion backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from .SuggestedTime import suggested_times
from .TimeBlock import TimeBlock
from .Treatment import Treatment
from .User import User
from .User import FormStatus, User
from .UserData import UserData

# Used to avoid import errors for the models
Expand All @@ -42,6 +42,7 @@
"RankingPreference",
"Form",
"FormSubmission",
"FormStatus",
]

log = logging.getLogger(LOGGER_NAME("models"))
Expand Down
1 change: 1 addition & 0 deletions backend/app/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ async def get_current_user(
role_id=user.role_id,
auth_id=user.auth_id,
approved=user.approved,
form_status=user.form_status,
)
except HTTPException:
raise
Expand Down
13 changes: 13 additions & 0 deletions backend/app/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ def to_role_id(cls, role: "UserRole") -> int:
return role_map[role]


class FormStatus(str, Enum):
INTAKE_TODO = "intake-todo"
INTAKE_SUBMITTED = "intake-submitted"
RANKING_TODO = "ranking-todo"
RANKING_SUBMITTED = "ranking-submitted"
SECONDARY_APPLICATION_TODO = "secondary-application-todo"
SECONDARY_APPLICATION_SUBMITTED = "secondary-application-submitted"
COMPLETED = "completed"


class UserBase(BaseModel):
"""
Base schema for user model with common attributes shared across schemas.
Expand Down Expand Up @@ -91,6 +101,7 @@ class UserUpdateRequest(BaseModel):
email: Optional[EmailStr] = None
role: Optional[UserRole] = None
approved: Optional[bool] = None
form_status: Optional[FormStatus] = None


class UserCreateResponse(BaseModel):
Expand All @@ -105,6 +116,7 @@ class UserCreateResponse(BaseModel):
role_id: int
auth_id: str
approved: bool
form_status: FormStatus

# from_attributes enables automatic mapping from SQLAlchemy model to Pydantic model
model_config = ConfigDict(from_attributes=True)
Expand All @@ -123,6 +135,7 @@ class UserResponse(BaseModel):
auth_id: str
approved: bool
role: "RoleResponse"
form_status: FormStatus

model_config = ConfigDict(from_attributes=True)

Expand Down
3 changes: 2 additions & 1 deletion backend/app/seeds/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from app.models.Experience import Experience
from app.models.Treatment import Treatment
from app.models.User import User
from app.models.User import FormStatus, User
from app.models.UserData import UserData
from app.utilities.form_constants import ExperienceId, TreatmentId

Expand Down Expand Up @@ -316,6 +316,7 @@ def seed_users(session: Session) -> None:
auth_id=user_info["user_data"]["auth_id"],
approved=True,
active=True,
form_status=FormStatus.INTAKE_TODO,
)
session.add(user)
session.flush() # Get user ID
Expand Down
10 changes: 9 additions & 1 deletion backend/app/services/implementations/intake_form_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from sqlalchemy.orm import Session

from app.models import Experience, Treatment, User, UserData
from app.models import Experience, FormStatus, Treatment, User, UserData

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -72,6 +72,14 @@ def process_form_submission(self, user_id: str, form_data: Dict[str, Any]) -> Us
if owning_user and owning_user.email:
user_data.email = owning_user.email

# Update form status for the owning user without regressing progress
owning_user = self.db.query(User).filter(User.id == user_data.user_id).first()
if owning_user and owning_user.form_status in {
FormStatus.INTAKE_TODO,
FormStatus.INTAKE_SUBMITTED,
}:
owning_user.form_status = FormStatus.INTAKE_SUBMITTED

# Commit all changes
self.db.commit()
self.db.refresh(user_data)
Expand Down
9 changes: 8 additions & 1 deletion backend/app/services/implementations/ranking_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from sqlalchemy.orm import Session

from app.models import Quality, User, UserData
from app.models import FormStatus, Quality, User, UserData
from app.models.RankingPreference import RankingPreference


Expand Down Expand Up @@ -205,4 +205,11 @@ def save_preferences(self, user_auth_id: str, target: str, items: List[Dict]) ->
)
if normalized:
self.db.bulk_save_objects(normalized)

if user.form_status in (
FormStatus.RANKING_TODO,
FormStatus.RANKING_SUBMITTED,
):
user.form_status = FormStatus.RANKING_SUBMITTED

self.db.commit()
Loading