import uuid from datetime import date, timedelta from typing import Sequence from sqlalchemy import or_, select, update as sa_update from sqlalchemy.ext.asyncio import AsyncSession from app.models.ae import AdverseEvent from app.models.site import Site from app.models.subject import Subject from app.schemas.ae import AECreate, AEUpdate def _seriousness_rank(value: str | None) -> int | None: if not value: return None normalized = str(value).strip().upper() roman_map = {"I": 1, "II": 2, "III": 3, "IV": 4, "V": 5} legacy_map = {"SERIOUS": 5, "NON_SERIOUS": 1} return roman_map.get(normalized) or legacy_map.get(normalized) def _calc_due_date(onset: date | None, seriousness: str) -> date | None: if onset is None: return None rank = _seriousness_rank(seriousness) delta = 1 if rank and rank >= 3 else 7 return onset + timedelta(days=delta) async def _validate_site_subject(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UUID | None, subject_id: uuid.UUID | None): if site_id: result = await db.execute(select(Site).where(Site.id == site_id)) site = result.scalar_one_or_none() if not site or site.study_id != study_id: raise ValueError("分中心不属于当前项目") if not site.is_active: raise ValueError("中心已停用") if subject_id: result = await db.execute(select(Subject).where(Subject.id == subject_id)) subj = result.scalar_one_or_none() if not subj or subj.study_id != study_id: raise ValueError("参与者不属于当前项目") result = await db.execute(select(Site).where(Site.id == subj.site_id)) subj_site = result.scalar_one_or_none() if subj_site and not subj_site.is_active: raise ValueError("中心已停用") async def create_ae( db: AsyncSession, study_id: uuid.UUID, ae_in: AECreate, *, created_by: uuid.UUID, ) -> AdverseEvent: await _validate_site_subject(db, study_id, ae_in.site_id, ae_in.subject_id) due_date = _calc_due_date(ae_in.onset_date, ae_in.seriousness) ae = AdverseEvent( study_id=study_id, site_id=ae_in.site_id, subject_id=ae_in.subject_id, visit_id=ae_in.visit_id, term=ae_in.term, onset_date=ae_in.onset_date, resolution_date=None, seriousness=ae_in.seriousness.value, severity=ae_in.seriousness.value, causality=ae_in.causality, action_taken=None, outcome=ae_in.outcome, reported_to_sponsor=False, is_sae=bool(ae_in.is_sae or ae_in.is_susar), is_susar=bool(ae_in.is_susar), report_due_date=due_date, status="NEW", description=ae_in.description, created_by=created_by, ) db.add(ae) await db.commit() await db.refresh(ae) return ae async def get_ae(db: AsyncSession, ae_id: uuid.UUID) -> AdverseEvent | None: result = await db.execute(select(AdverseEvent).where(AdverseEvent.id == ae_id)) return result.scalar_one_or_none() async def list_ae( db: AsyncSession, study_id: uuid.UUID, status: str | None = None, seriousness: str | None = None, site_id: uuid.UUID | None = None, subject_id: uuid.UUID | None = None, overdue: bool | None = None, site_ids: set[uuid.UUID] | None = None, ) -> Sequence[AdverseEvent]: stmt = select(AdverseEvent).where(AdverseEvent.study_id == study_id) if site_ids is not None: if not site_ids: return [] subject_site_ids = select(Subject.id).where(Subject.site_id.in_(site_ids)) stmt = stmt.where( or_( AdverseEvent.site_id.in_(site_ids), AdverseEvent.subject_id.in_(subject_site_ids), ) ) if status: stmt = stmt.where(AdverseEvent.status == status) if seriousness: stmt = stmt.where(AdverseEvent.seriousness == seriousness) if site_id: stmt = stmt.where(AdverseEvent.site_id == site_id) if subject_id: stmt = stmt.where(AdverseEvent.subject_id == subject_id) if overdue is True: stmt = stmt.where(AdverseEvent.report_due_date < date.today(), AdverseEvent.status != "CLOSED") if overdue is False: stmt = stmt.where((AdverseEvent.report_due_date >= date.today()) | (AdverseEvent.report_due_date.is_(None)) | (AdverseEvent.status == "CLOSED")) result = await db.execute(stmt) return result.scalars().all() async def update_ae(db: AsyncSession, study_id: uuid.UUID, ae: AdverseEvent, ae_in: AEUpdate) -> AdverseEvent: update_data = ae_in.model_dump(exclude_unset=True) if "is_susar" in update_data and update_data["is_susar"]: update_data["is_sae"] = True if "is_sae" in update_data and update_data["is_sae"] is False: update_data["is_susar"] = False if "seriousness" in update_data: severity_value = update_data["seriousness"].value if hasattr(update_data["seriousness"], "value") else str(update_data["seriousness"]) update_data["seriousness"] = severity_value update_data["severity"] = severity_value if "subject_id" in update_data: await _validate_site_subject(db, study_id, None, update_data["subject_id"]) if "onset_date" in update_data or "seriousness" in update_data: onset = update_data.get("onset_date", ae.onset_date) seriousness = update_data.get("seriousness", ae.seriousness) update_data["report_due_date"] = _calc_due_date(onset, seriousness) if update_data: await db.execute( sa_update(AdverseEvent) .where(AdverseEvent.id == ae.id) .values(**update_data) ) await db.commit() await db.refresh(ae) return ae async def delete_ae(db: AsyncSession, ae: AdverseEvent) -> None: await db.delete(ae) await db.commit()