import uuid from datetime import date from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_current_user, get_db_session, require_study_member, require_study_roles from app.crud import audit as audit_crud from app.crud import imp_batch as batch_crud from app.crud import imp_tx as tx_crud from app.crud import study as study_crud from app.crud import subject as subject_crud from app.crud import member as member_crud from app.schemas.imp import TxCreate, TxRead from app.services.imp import create_transaction_and_apply_inventory, NEGATIVE_TYPES router = APIRouter() async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): study = await study_crud.get(db, study_id) if not study: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") return study async def _validate_entities(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UUID, batch_id: uuid.UUID, subject_id: uuid.UUID | None): batch = await batch_crud.get_batch(db, batch_id) if not batch or batch.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Batch not found") from app.models.site import Site site = await db.get(Site, site_id) if not site or site.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Site not found") if subject_id: subj = await subject_crud.get_subject(db, subject_id) if not subj or subj.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Subject not found") if batch.expiry_date and date.today() > batch.expiry_date and batch_id: # forbid outflow when expired pass @router.post( "/", response_model=TxRead, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_study_roles(["PM", "IMP"]))], ) async def create_transaction( study_id: uuid.UUID, tx_in: TxCreate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> TxRead: await _ensure_study_exists(db, study_id) await _validate_entities(db, study_id, tx_in.site_id, tx_in.batch_id, tx_in.subject_id) if tx_in.tx_type in NEGATIVE_TYPES: batch = await batch_crud.get_batch(db, tx_in.batch_id) if batch and batch.status == "EXPIRED": raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot dispense from expired batch") try: tx = await create_transaction_and_apply_inventory( db, study_id=study_id, site_id=tx_in.site_id, batch_id=tx_in.batch_id, subject_id=tx_in.subject_id, tx_type=tx_in.tx_type, quantity=tx_in.quantity, tx_date=tx_in.tx_date, reference=tx_in.reference, notes=tx_in.notes, operator_id=current_user.id, operator_role=current_user.role, ) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc return TxRead.model_validate(tx) @router.get( "/", response_model=list[TxRead], dependencies=[Depends(require_study_member())], ) async def list_transactions( study_id: uuid.UUID, site_id: uuid.UUID | None = None, batch_id: uuid.UUID | None = None, subject_id: uuid.UUID | None = None, tx_type: str | None = None, date_from: date | None = None, date_to: date | None = None, skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db_session), ) -> list[TxRead]: await _ensure_study_exists(db, study_id) txs = await tx_crud.list_txs( db, study_id, site_id=site_id, batch_id=batch_id, subject_id=subject_id, tx_type=tx_type, date_from=date_from, date_to=date_to, skip=skip, limit=limit, ) return [TxRead.model_validate(t) for t in txs]