Skip to content
Open
92 changes: 83 additions & 9 deletions backend/app/api/agents.py
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 3 additions & 0 deletions backend/app/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import agents as agents_service

__all__ = ["agents_service"]
79 changes: 79 additions & 0 deletions backend/app/services/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

potentially add a check like

if not update_data:
    raise ValueError("No update fields were provided.")

to avoid committing without modifying anything (#17 for reference)

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