Full CRUD on Participant scoped to the caller's organization via new get_route_in_organization/get_participant_in_organization helpers in app/dependencies.py (shared cross-cutting spot, reused by task 6). PATCH is_active is the primary deactivation path — replaces the old spreadsheet-era "empty order" trick and preserves order history for participants who leave and later return. DELETE is a separate, superadmin-gated hard-delete for GDPR erasure requests, which cascades to the participant's orders. Adds UNIQUE(route_id, email) at the DB level (existing 308 participants already conform, per task 3's dedup) plus email normalization on create/update, so case-variant duplicates can't reappear via the API. Every write path relies on catching the constraint's IntegrityError for a clean 409 rather than a racy pre-check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
83 lines
3.3 KiB
Python
83 lines
3.3 KiB
Python
from typing import Optional
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, status
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlmodel import Session, select
|
|
|
|
from app.db import SessionDep
|
|
from app.dependencies import (
|
|
CurrentSuperuser,
|
|
CurrentUser,
|
|
get_participant_in_organization,
|
|
get_route_in_organization,
|
|
)
|
|
from app.models.participant import Participant, ParticipantCreate, ParticipantPublic, ParticipantUpdate
|
|
from app.models.route import Route
|
|
|
|
router = APIRouter(prefix="/participants", tags=["participants"])
|
|
|
|
|
|
def _commit_or_conflict(session: Session) -> None:
|
|
try:
|
|
session.commit()
|
|
except IntegrityError as exc:
|
|
session.rollback()
|
|
if "uq_participant_route_email" in str(getattr(exc, "orig", exc)):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail="A participant with this email already exists on this route",
|
|
) from exc
|
|
raise
|
|
|
|
|
|
@router.post("", response_model=ParticipantPublic, status_code=status.HTTP_201_CREATED)
|
|
def create_participant(payload: ParticipantCreate, session: SessionDep, current_user: CurrentUser) -> Participant:
|
|
get_route_in_organization(session, payload.route_id, current_user.organization_id)
|
|
participant = Participant.model_validate(payload)
|
|
session.add(participant)
|
|
_commit_or_conflict(session)
|
|
session.refresh(participant)
|
|
return participant
|
|
|
|
|
|
@router.get("", response_model=list[ParticipantPublic])
|
|
def list_participants(
|
|
session: SessionDep,
|
|
current_user: CurrentUser,
|
|
route_id: Optional[int] = None,
|
|
is_active: Optional[bool] = None,
|
|
limit: int = Query(default=100, le=500, gt=0),
|
|
offset: int = Query(default=0, ge=0),
|
|
) -> list[Participant]:
|
|
statement = select(Participant).join(Route).where(Route.organization_id == current_user.organization_id)
|
|
if route_id is not None:
|
|
statement = statement.where(Participant.route_id == route_id)
|
|
if is_active is not None:
|
|
statement = statement.where(Participant.is_active == is_active)
|
|
statement = statement.order_by(Participant.id).offset(offset).limit(limit)
|
|
return list(session.exec(statement).all())
|
|
|
|
|
|
@router.get("/{participant_id}", response_model=ParticipantPublic)
|
|
def get_participant(participant_id: int, session: SessionDep, current_user: CurrentUser) -> Participant:
|
|
return get_participant_in_organization(session, participant_id, current_user.organization_id)
|
|
|
|
|
|
@router.patch("/{participant_id}", response_model=ParticipantPublic)
|
|
def update_participant(
|
|
participant_id: int, payload: ParticipantUpdate, session: SessionDep, current_user: CurrentUser
|
|
) -> Participant:
|
|
participant = get_participant_in_organization(session, participant_id, current_user.organization_id)
|
|
for field, value in payload.model_dump(exclude_unset=True).items():
|
|
setattr(participant, field, value)
|
|
session.add(participant)
|
|
_commit_or_conflict(session)
|
|
session.refresh(participant)
|
|
return participant
|
|
|
|
|
|
@router.delete("/{participant_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def delete_participant(participant_id: int, session: SessionDep, current_user: CurrentSuperuser) -> None:
|
|
participant = get_participant_in_organization(session, participant_id, current_user.organization_id)
|
|
session.delete(participant)
|
|
session.commit()
|