vinindkoeb/app/routers/participants.py
carsten ded23efc6a Add admin CRUD for participants (incl. active/inactive)
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>
2026-09-28 13:05:08 +02:00

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()