diff --git a/backend/app/api/agents.py b/backend/app/api/agents.py index b1cf144..d34b637 100644 --- a/backend/app/api/agents.py +++ b/backend/app/api/agents.py @@ -1,17 +1,91 @@ """Agents REST API. -Endpoints for Agency agents (Supabase Auth; link via supabase_user_id). +Full CRUD for agents resource. """ -from fastapi import APIRouter, Response, status +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from ..database import get_db +from ..models import Agent +from ..schemas import AgentCreate, AgentUpdate, Agent as AgentSchema +from ..services import agents as agent_service router = APIRouter() +# Helpers + + +async def get_agent_or_404(agent_id: str, db: AsyncSession) -> Agent: + """ + Reusable helper: looks up agent by ID + If not found, raise a 404 immediately + """ + agent = await agent_service.get_agent(agent_id, db) + if not agent: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Agent with id '{agent_id}' not found.", + ) + return agent + + +# Endpoints + + +@router.get("", response_model=list[AgentSchema]) +async def list_agents(db: AsyncSession = Depends(get_db)): + """ + GET /api/agents + Returns list of all agents + """ + return await agent_service.list_agents(db) + + +@router.post("", response_model=AgentSchema, status_code=status.HTTP_201_CREATED) +async def create_agent(agent: AgentCreate, db: AsyncSession = Depends(get_db)): + """ + Creates a new agent. + """ + if not await agent_service.validate_agency_exists(agent.agency_id, db): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Agency with id '{agent.agency_id}' not found.", + ) + return await agent_service.create_agent(agent, db) + + +@router.get("/{agent_id}", response_model=AgentSchema) +async def get_agent(agent_id: str, db: AsyncSession = Depends(get_db)): + """ + Returns a single agent. + """ + return await get_agent_or_404(agent_id, db) + + +@router.put("/{agent_id}", response_model=AgentSchema) +async def update_agent( + agent_id: str, + payload: AgentUpdate, + db: AsyncSession = Depends(get_db), +): + """ + Updates an existing agent. + """ + agent = await get_agent_or_404(agent_id, db) + if "agency_id" in payload.model_dump(exclude_unset=True): + if not await agent_service.validate_agency_exists(payload.agency_id, db): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Agency with id '{payload.agency_id}' not found.", + ) + return await agent_service.update_agent(agent, payload, db) + -@router.get("") -async def list_agents(): - """List agents. Placeholder — implement with Agent model and schemas.""" - return Response( - content="Not implemented — see docs/STARTER_BACKEND_GUIDE.md", - status_code=status.HTTP_501_NOT_IMPLEMENTED, - ) +@router.delete("/{agent_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_agent(agent_id: str, db: AsyncSession = Depends(get_db)): + """ + Deletes an agent. + """ + agent = await get_agent_or_404(agent_id, db) + await agent_service.delete_agent(agent, db) diff --git a/backend/app/services/agents.py b/backend/app/services/agents.py new file mode 100644 index 0000000..baa6265 --- /dev/null +++ b/backend/app/services/agents.py @@ -0,0 +1,81 @@ +"""Agents service.""" + +from sqlalchemy import select +from sqlalchemy.exc import IntegrityError +from sqlalchemy.ext.asyncio import AsyncSession + +from ..models import Agency, Agent +from ..schemas import AgentCreate, AgentUpdate + + +async def validate_agency_exists(agency_id: str, db: AsyncSession) -> bool: + """ + Return True if the agency exists, False otherwise. + """ + result = await db.execute(select(Agency).where(Agency.id == agency_id)) + return result.scalar_one_or_none() is not None + + +async def list_agents(db: AsyncSession) -> list[Agent]: + """ + Return all agents ordered by last name, first name. + """ + result = await db.execute(select(Agent).order_by(Agent.last_name, Agent.first_name)) + return result.scalars().all() + + +async def get_agent(agent_id: str, db: AsyncSession) -> Agent | None: + """ + Return an agent by id, or None if not found. + """ + result = await db.execute(select(Agent).where(Agent.id == agent_id)) + return result.scalar_one_or_none() + + +async def create_agent(payload: AgentCreate, db: AsyncSession) -> Agent: + """ + Create and return a new agent. + """ + db_agent = Agent(**payload.model_dump()) + db.add(db_agent) + try: + await db.commit() + except IntegrityError as e: + await db.rollback() + raise ValueError(f"Unable to create agent: {str(e.orig)}") from e + await db.refresh(db_agent) + return db_agent + + +async def update_agent( + agent: Agent, + payload: AgentUpdate, + db: AsyncSession, +) -> Agent: + """ + Update and return an existing agent. + """ + update_data = payload.model_dump(exclude_unset=True) + if not update_data: + raise ValueError("No update fields were provided.") + for key, value in update_data.items(): + setattr(agent, key, value) + try: + await db.commit() + except IntegrityError as e: + await db.rollback() + raise ValueError(f"Unable to update agent: {str(e.orig)}") from e + await db.refresh(agent) + return agent + + +async def delete_agent(agent: Agent, db: AsyncSession) -> None: + """ + Delete an agent permanently. + """ + await db.delete(agent) + try: + await db.commit() + except IntegrityError as e: + await db.rollback() + raise ValueError(f"Unable to delete agent: {str(e.orig)}") from e