Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
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