Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
56 changes: 31 additions & 25 deletions backend/app/api/agencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,39 @@
Use this module as the reference pattern for other resources.
"""

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from fastapi import APIRouter, Depends, HTTPException, Response, status
from sqlalchemy.ext.asyncio import AsyncSession

from ..database import get_db
from ..models import Agency
from ..schemas import AgencyCreate, AgencyUpdate, Agency as AgencySchema
from ..schemas import Agency as AgencySchema
from ..schemas import AgencyCreate, AgencyUpdate
from ..services import agency_service

router = APIRouter()


@router.get("", response_model=list[AgencySchema])
async def list_agencies(db: AsyncSession = Depends(get_db)):
"""List all agencies."""
result = await db.execute(select(Agency).order_by(Agency.name))
return list(result.scalars().all())
return await agency_service.list_agencies(db)


@router.post("", response_model=AgencySchema, status_code=status.HTTP_201_CREATED)
async def create_agency(agency: AgencyCreate, db: AsyncSession = Depends(get_db)):
"""Create a new agency."""
db_agency = Agency(**agency.model_dump())
db.add(db_agency)
await db.commit()
await db.refresh(db_agency)
return db_agency
try:
return await agency_service.create_agency(db, agency)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
) from e


@router.get("/{agency_id}", response_model=AgencySchema)
async def get_agency(agency_id: str, db: AsyncSession = Depends(get_db)):
"""Get a single agency by ID."""
result = await db.execute(select(Agency).where(Agency.id == agency_id))
agency = result.scalar_one_or_none()
agency = await agency_service.get_agency(db, agency_id)
if not agency:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
Expand All @@ -52,30 +52,36 @@ async def update_agency(
db: AsyncSession = Depends(get_db),
):
"""Update an agency."""
result = await db.execute(select(Agency).where(Agency.id == agency_id))
agency = result.scalar_one_or_none()
agency = await agency_service.get_agency(db, agency_id)
if not agency:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Agency not found",
)
data = payload.model_dump(exclude_unset=True)
for key, value in data.items():
setattr(agency, key, value)
await db.commit()
await db.refresh(agency)
return agency
try:
return await agency_service.update_agency(db, agency, payload)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
) from e


@router.delete("/{agency_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_agency(agency_id: str, db: AsyncSession = Depends(get_db)):
"""Delete an agency."""
result = await db.execute(select(Agency).where(Agency.id == agency_id))
agency = result.scalar_one_or_none()
agency = await agency_service.get_agency(db, agency_id)
if not agency:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Agency not found",
)
await db.delete(agency)
await db.commit()
try:
await agency_service.delete_agency(db, agency)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
) from e

return Response(status_code=status.HTTP_204_NO_CONTENT)
89 changes: 80 additions & 9 deletions backend/app/api/routes.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,88 @@
"""Routes REST API.

Endpoints for dispatch routes (pickup/dropoff furniture lists).
Full CRUD implementation for the Routes resource.
"""

from fastapi import APIRouter, Response, status
from fastapi import APIRouter, Depends, HTTPException, Response, status
from sqlalchemy.ext.asyncio import AsyncSession

from ..database import get_db
from ..schemas import Route as RouteSchema
from ..schemas import RouteCreate, RouteUpdate
from ..services import route_service

router = APIRouter()


@router.get("")
async def list_routes():
"""List routes. Placeholder — implement with Route model and schemas."""
return Response(
content="Not implemented — see docs/STARTER_BACKEND_GUIDE.md",
status_code=status.HTTP_501_NOT_IMPLEMENTED,
)
@router.get("", response_model=list[RouteSchema])
async def list_routes(db: AsyncSession = Depends(get_db)):
"""List all routes."""
return await route_service.list_routes(db)


@router.post("", response_model=RouteSchema, status_code=status.HTTP_201_CREATED)
async def create_route(payload: RouteCreate, db: AsyncSession = Depends(get_db)):
"""Create a new route."""
try:
return await route_service.create_route(db, payload)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
) from e


@router.get("/{route_id}", response_model=RouteSchema)
async def get_route(route_id: str, db: AsyncSession = Depends(get_db)):
"""Get a single route by ID."""
route = await route_service.get_route(db, route_id)
if not route:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Route not found",
)
return route


@router.put("/{route_id}", response_model=RouteSchema)
async def update_route(
route_id: str,
payload: RouteUpdate,
db: AsyncSession = Depends(get_db),
):
"""Update a route."""
route = await route_service.get_route(db, route_id)
if not route:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Route not found",
)

try:
return await route_service.update_route(db, route, payload)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
) from e


@router.delete("/{route_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_route(route_id: str, db: AsyncSession = Depends(get_db)):
"""Delete a route."""
route = await route_service.get_route(db, route_id)
if not route:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Route not found",
)

try:
await route_service.delete_route(db, route)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
) from e

return Response(status_code=status.HTTP_204_NO_CONTENT)
3 changes: 2 additions & 1 deletion backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
"""

from functools import lru_cache
from pydantic_settings import BaseSettings
from typing import List

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
"""Application settings."""
Expand Down
2 changes: 1 addition & 1 deletion backend/app/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@see https://docs.sqlalchemy.org/en/20/orm/
"""

from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine, AsyncSession
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import declarative_base

from .config import get_settings
Expand Down
5 changes: 3 additions & 2 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
dependencies configured.
"""

from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager

from .config import get_settings
from .api import router as api_router
from .config import get_settings

settings = get_settings()

Expand Down
11 changes: 1 addition & 10 deletions backend/app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
"""Models package initialization."""

from .base import (
Admin,
Agency,
Agent,
Client,
Donor,
Furniture,
Referral,
Route,
)
from .base import Admin, Agency, Agent, Client, Donor, Furniture, Referral, Route

__all__ = [
"Admin",
Expand Down
3 changes: 3 additions & 0 deletions backend/app/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
Business logic layer; API routes depend on services, not the reverse.
"""

from . import agencies as agency_service
from . import routes as route_service
from . import donors as donor_service
from . import clients as clients_service

__all__ = ["agency_service", "route_service"]
__all__ = ["donor_service", "clients_service"]
74 changes: 74 additions & 0 deletions backend/app/services/agencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Agency services module.

Contains business logic for agency-related operations.
"""

import logging

from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

logger = logging.getLogger(__name__)

from ..models import Agency
from ..schemas import AgencyCreate, AgencyUpdate


async def list_agencies(db: AsyncSession) -> list[Agency]:
"""List all agencies ordered by name."""
result = await db.execute(select(Agency).order_by(Agency.name))
return list(result.scalars().all())


async def get_agency(db: AsyncSession, agency_id: str) -> Agency | None:
"""Get a single agency by ID, or None when not found."""
result = await db.execute(select(Agency).where(Agency.id == agency_id))
return result.scalar_one_or_none()


async def create_agency(db: AsyncSession, payload: AgencyCreate) -> Agency:
"""Create a new agency."""
db_agency = Agency(**payload.model_dump())
db.add(db_agency)
try:
await db.commit()
except IntegrityError as e:
await db.rollback()
logger.exception("IntegrityError creating agency: %s", e.orig)
raise ValueError("Unable to create agency due to a data conflict.") from e
await db.refresh(db_agency)
return db_agency


async def update_agency(
db: AsyncSession, agency: Agency, payload: AgencyUpdate
) -> Agency:
"""Update an existing agency."""
data = payload.model_dump(exclude_unset=True)

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

for key, value in data.items():
setattr(agency, key, value)

try:
await db.commit()
except IntegrityError as e:
await db.rollback()
logger.exception("IntegrityError updating agency %s: %s", agency.id, e.orig)
raise ValueError("Update failed due to a data conflict.") from e
await db.refresh(agency)
return agency


async def delete_agency(db: AsyncSession, agency: Agency) -> None:
"""Delete an existing agency."""
await db.delete(agency)
try:
await db.commit()
except IntegrityError as e:
await db.rollback()
logger.exception("IntegrityError deleting agency %s: %s", agency.id, e.orig)
raise ValueError("Unable to delete agency due to a data conflict.") from e
Loading