diff --git a/app/api/routes.py b/app/api/routes.py index 901f74f..8c2b877 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -1,9 +1,24 @@ """Root API router that aggregates versioned routers.""" from fastapi import APIRouter -from app.api.v1 import auth, users +from app.api.v1 import ( + activities, + analytics, + auth, + contacts, + deals, + organizations, + tasks, + users, +) from app.core.config import settings api_router = APIRouter() -api_router.include_router(users.router, prefix=settings.api_v1_prefix) api_router.include_router(auth.router, prefix=settings.api_v1_prefix) +api_router.include_router(users.router, prefix=settings.api_v1_prefix) +api_router.include_router(organizations.router, prefix=settings.api_v1_prefix) +api_router.include_router(contacts.router, prefix=settings.api_v1_prefix) +api_router.include_router(deals.router, prefix=settings.api_v1_prefix) +api_router.include_router(tasks.router, prefix=settings.api_v1_prefix) +api_router.include_router(activities.router, prefix=settings.api_v1_prefix) +api_router.include_router(analytics.router, prefix=settings.api_v1_prefix) diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py index 0f6c969..66eb61c 100644 --- a/app/api/v1/__init__.py +++ b/app/api/v1/__init__.py @@ -1 +1,22 @@ """Version 1 API routers.""" +from . import ( + activities, + analytics, + auth, + contacts, + deals, + organizations, + tasks, + users, +) + +__all__ = [ + "activities", + "analytics", + "auth", + "contacts", + "deals", + "organizations", + "tasks", + "users", +] diff --git a/app/api/v1/activities/__init__.py b/app/api/v1/activities/__init__.py new file mode 100644 index 0000000..23c693e --- /dev/null +++ b/app/api/v1/activities/__init__.py @@ -0,0 +1,4 @@ +"""Activities API package.""" +from .views import router + +__all__ = ["router"] diff --git a/app/api/v1/activities/crud.py b/app/api/v1/activities/crud.py new file mode 100644 index 0000000..46d3aff --- /dev/null +++ b/app/api/v1/activities/crud.py @@ -0,0 +1 @@ +"""CRUD helpers for activities (to be implemented).""" diff --git a/app/api/v1/activities/models.py b/app/api/v1/activities/models.py new file mode 100644 index 0000000..498b177 --- /dev/null +++ b/app/api/v1/activities/models.py @@ -0,0 +1,11 @@ +"""Pydantic schemas for activity endpoints.""" +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import BaseModel + + +class ActivityCommentPayload(BaseModel): + type: Literal["comment"] + payload: dict[str, Any] diff --git a/app/api/v1/activities/views.py b/app/api/v1/activities/views.py new file mode 100644 index 0000000..dcf594a --- /dev/null +++ b/app/api/v1/activities/views.py @@ -0,0 +1,26 @@ +"""Activity timeline API stubs.""" +from __future__ import annotations + +from fastapi import APIRouter, status + +from .models import ActivityCommentPayload + +router = APIRouter(prefix="/deals/{deal_id}/activities", tags=["activities"]) + + +def _stub(endpoint: str) -> dict[str, str]: + return {"detail": f"{endpoint} is not implemented yet"} + + +@router.get("/", status_code=status.HTTP_501_NOT_IMPLEMENTED) +async def list_activities(deal_id: int) -> dict[str, str]: + """Placeholder for listing deal activities.""" + _ = deal_id + return _stub("GET /deals/{deal_id}/activities") + + +@router.post("/", status_code=status.HTTP_501_NOT_IMPLEMENTED) +async def create_activity_comment(deal_id: int, payload: ActivityCommentPayload) -> dict[str, str]: + """Placeholder for adding a comment activity to a deal.""" + _ = (deal_id, payload) + return _stub("POST /deals/{deal_id}/activities") diff --git a/app/api/v1/analytics/__init__.py b/app/api/v1/analytics/__init__.py new file mode 100644 index 0000000..e177bd2 --- /dev/null +++ b/app/api/v1/analytics/__init__.py @@ -0,0 +1,4 @@ +"""Analytics API package.""" +from .views import router + +__all__ = ["router"] diff --git a/app/api/v1/analytics/crud.py b/app/api/v1/analytics/crud.py new file mode 100644 index 0000000..f285a92 --- /dev/null +++ b/app/api/v1/analytics/crud.py @@ -0,0 +1 @@ +"""Analytics CRUD/query helpers placeholder.""" diff --git a/app/api/v1/analytics/models.py b/app/api/v1/analytics/models.py new file mode 100644 index 0000000..760bda7 --- /dev/null +++ b/app/api/v1/analytics/models.py @@ -0,0 +1 @@ +"""Analytics schemas placeholder.""" diff --git a/app/api/v1/analytics/views.py b/app/api/v1/analytics/views.py new file mode 100644 index 0000000..de3ea27 --- /dev/null +++ b/app/api/v1/analytics/views.py @@ -0,0 +1,23 @@ +"""Analytics API stubs (deal summary and funnel).""" +from __future__ import annotations + +from fastapi import APIRouter, Query, status + +router = APIRouter(prefix="/analytics", tags=["analytics"]) + + +def _stub(endpoint: str) -> dict[str, str]: + return {"detail": f"{endpoint} is not implemented yet"} + + +@router.get("/deals/summary", status_code=status.HTTP_501_NOT_IMPLEMENTED) +async def deals_summary(days: int = Query(30, ge=1, le=180)) -> dict[str, str]: + """Placeholder for aggregated deal statistics.""" + _ = days + return _stub("GET /analytics/deals/summary") + + +@router.get("/deals/funnel", status_code=status.HTTP_501_NOT_IMPLEMENTED) +async def deals_funnel() -> dict[str, str]: + """Placeholder for funnel analytics.""" + return _stub("GET /analytics/deals/funnel") diff --git a/app/api/v1/auth.py b/app/api/v1/auth.py index ba3c86f..f39f74c 100644 --- a/app/api/v1/auth.py +++ b/app/api/v1/auth.py @@ -2,6 +2,7 @@ from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel, EmailStr from app.api.deps import get_auth_service from app.models.token import LoginRequest, TokenResponse @@ -10,6 +11,29 @@ from app.services.auth_service import AuthService, InvalidCredentialsError router = APIRouter(prefix="/auth", tags=["auth"]) +class RegisterRequest(BaseModel): + email: EmailStr + password: str + name: str + organization_name: str + + +def _stub(detail: str) -> dict[str, str]: + return {"detail": detail} + + +@router.post("/register", status_code=status.HTTP_501_NOT_IMPLEMENTED) +async def register_user(_: RegisterRequest) -> dict[str, str]: + """Placeholder for user plus organization registration flow.""" + return _stub("POST /auth/register is not implemented yet") + + +@router.post("/login", status_code=status.HTTP_501_NOT_IMPLEMENTED) +async def login(_: LoginRequest) -> dict[str, str]: + """Placeholder for login shortcut endpoint defined in the spec.""" + return _stub("POST /auth/login is not implemented yet") + + @router.post("/token", response_model=TokenResponse) async def login_for_access_token( credentials: LoginRequest, diff --git a/app/api/v1/auth/__init__.py b/app/api/v1/auth/__init__.py new file mode 100644 index 0000000..8fc31a8 --- /dev/null +++ b/app/api/v1/auth/__init__.py @@ -0,0 +1,4 @@ +"""Auth API package.""" +from .views import router + +__all__ = ["router"] diff --git a/app/api/v1/auth/crud.py b/app/api/v1/auth/crud.py new file mode 100644 index 0000000..600514b --- /dev/null +++ b/app/api/v1/auth/crud.py @@ -0,0 +1 @@ +"""Auth CRUD/service helpers placeholder.""" diff --git a/app/api/v1/auth/models.py b/app/api/v1/auth/models.py new file mode 100644 index 0000000..1d1f6cb --- /dev/null +++ b/app/api/v1/auth/models.py @@ -0,0 +1,11 @@ +"""Auth-specific Pydantic schemas.""" +from __future__ import annotations + +from pydantic import BaseModel, EmailStr + + +class RegisterRequest(BaseModel): + email: EmailStr + password: str + name: str + organization_name: str diff --git a/app/api/v1/auth/views.py b/app/api/v1/auth/views.py new file mode 100644 index 0000000..61635f7 --- /dev/null +++ b/app/api/v1/auth/views.py @@ -0,0 +1,40 @@ +"""Authentication API endpoints.""" +from __future__ import annotations + +from fastapi import APIRouter, Depends, HTTPException, status + +from app.api.deps import get_auth_service +from app.models.token import LoginRequest, TokenResponse +from app.services.auth_service import AuthService, InvalidCredentialsError + +from .models import RegisterRequest + +router = APIRouter(prefix="/auth", tags=["auth"]) + + +def _stub(detail: str) -> dict[str, str]: + return {"detail": detail} + + +@router.post("/register", status_code=status.HTTP_501_NOT_IMPLEMENTED) +async def register_user(_: RegisterRequest) -> dict[str, str]: + """Placeholder for user plus organization registration flow.""" + return _stub("POST /auth/register is not implemented yet") + + +@router.post("/login", status_code=status.HTTP_501_NOT_IMPLEMENTED) +async def login(_: LoginRequest) -> dict[str, str]: + """Placeholder for login shortcut endpoint defined in the spec.""" + return _stub("POST /auth/login is not implemented yet") + + +@router.post("/token", response_model=TokenResponse) +async def login_for_access_token( + credentials: LoginRequest, + service: AuthService = Depends(get_auth_service), +) -> TokenResponse: + try: + user = await service.authenticate(credentials.email, credentials.password) + except InvalidCredentialsError as exc: # pragma: no cover - thin API + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc + return service.create_access_token(user) diff --git a/app/api/v1/contacts/__init__.py b/app/api/v1/contacts/__init__.py new file mode 100644 index 0000000..8047a39 --- /dev/null +++ b/app/api/v1/contacts/__init__.py @@ -0,0 +1,4 @@ +"""Contacts API package.""" +from .views import router + +__all__ = ["router"] diff --git a/app/api/v1/contacts/crud.py b/app/api/v1/contacts/crud.py new file mode 100644 index 0000000..519972b --- /dev/null +++ b/app/api/v1/contacts/crud.py @@ -0,0 +1 @@ +"""Contacts CRUD placeholder.""" diff --git a/app/api/v1/contacts/models.py b/app/api/v1/contacts/models.py new file mode 100644 index 0000000..659c192 --- /dev/null +++ b/app/api/v1/contacts/models.py @@ -0,0 +1,10 @@ +"""Contact API schemas.""" +from __future__ import annotations + +from pydantic import BaseModel, EmailStr + + +class ContactCreatePayload(BaseModel): + name: str + email: EmailStr | None = None + phone: str | None = None diff --git a/app/api/v1/contacts/views.py b/app/api/v1/contacts/views.py new file mode 100644 index 0000000..19dd0b1 --- /dev/null +++ b/app/api/v1/contacts/views.py @@ -0,0 +1,30 @@ +"""Contact API stubs required by the spec.""" +from __future__ import annotations + +from fastapi import APIRouter, Query, status + +from .models import ContactCreatePayload + +router = APIRouter(prefix="/contacts", tags=["contacts"]) + + +def _stub(endpoint: str) -> dict[str, str]: + return {"detail": f"{endpoint} is not implemented yet"} + + +@router.get("/", status_code=status.HTTP_501_NOT_IMPLEMENTED) +async def list_contacts( + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=100), + search: str | None = None, + owner_id: int | None = None, +) -> dict[str, str]: + """Placeholder list endpoint supporting the required filters.""" + return _stub("GET /contacts") + + +@router.post("/", status_code=status.HTTP_501_NOT_IMPLEMENTED) +async def create_contact(payload: ContactCreatePayload) -> dict[str, str]: + """Placeholder for creating a contact within the current organization.""" + _ = payload + return _stub("POST /contacts") diff --git a/app/api/v1/deals/__init__.py b/app/api/v1/deals/__init__.py new file mode 100644 index 0000000..bf0962f --- /dev/null +++ b/app/api/v1/deals/__init__.py @@ -0,0 +1,4 @@ +"""Deals API package.""" +from .views import router + +__all__ = ["router"] diff --git a/app/api/v1/deals/crud.py b/app/api/v1/deals/crud.py new file mode 100644 index 0000000..1c3b117 --- /dev/null +++ b/app/api/v1/deals/crud.py @@ -0,0 +1 @@ +"""Deal CRUD placeholder.""" diff --git a/app/api/v1/deals/models.py b/app/api/v1/deals/models.py new file mode 100644 index 0000000..3ebb08e --- /dev/null +++ b/app/api/v1/deals/models.py @@ -0,0 +1,20 @@ +"""Deal API schemas.""" +from __future__ import annotations + +from decimal import Decimal + +from pydantic import BaseModel + + +class DealCreatePayload(BaseModel): + contact_id: int + title: str + amount: Decimal | None = None + currency: str | None = None + + +class DealUpdatePayload(BaseModel): + status: str | None = None + stage: str | None = None + amount: Decimal | None = None + currency: str | None = None diff --git a/app/api/v1/deals/views.py b/app/api/v1/deals/views.py new file mode 100644 index 0000000..d7df5a6 --- /dev/null +++ b/app/api/v1/deals/views.py @@ -0,0 +1,45 @@ +"""Deal API stubs covering list/create/update operations.""" +from __future__ import annotations + +from decimal import Decimal + +from fastapi import APIRouter, Query, status + +from .models import DealCreatePayload, DealUpdatePayload + +router = APIRouter(prefix="/deals", tags=["deals"]) + + +def _stub(endpoint: str) -> dict[str, str]: + return {"detail": f"{endpoint} is not implemented yet"} + + +@router.get("/", status_code=status.HTTP_501_NOT_IMPLEMENTED) +async def list_deals( + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=100), + status_filter: list[str] | None = Query(default=None, alias="status"), + min_amount: Decimal | None = None, + max_amount: Decimal | None = None, + stage: str | None = None, + owner_id: int | None = None, + order_by: str | None = None, + order: str | None = Query(default=None, regex="^(asc|desc)$"), +) -> dict[str, str]: + """Placeholder for deal filtering endpoint.""" + _ = (status_filter,) + return _stub("GET /deals") + + +@router.post("/", status_code=status.HTTP_501_NOT_IMPLEMENTED) +async def create_deal(payload: DealCreatePayload) -> dict[str, str]: + """Placeholder for creating a new deal.""" + _ = payload + return _stub("POST /deals") + + +@router.patch("/{deal_id}", status_code=status.HTTP_501_NOT_IMPLEMENTED) +async def update_deal(deal_id: int, payload: DealUpdatePayload) -> dict[str, str]: + """Placeholder for modifying deal status or stage.""" + _ = (deal_id, payload) + return _stub("PATCH /deals/{deal_id}") diff --git a/app/api/v1/organizations/__init__.py b/app/api/v1/organizations/__init__.py new file mode 100644 index 0000000..f8cf943 --- /dev/null +++ b/app/api/v1/organizations/__init__.py @@ -0,0 +1,4 @@ +"""Organizations API package.""" +from .views import router + +__all__ = ["router"] diff --git a/app/api/v1/organizations/crud.py b/app/api/v1/organizations/crud.py new file mode 100644 index 0000000..7731ce7 --- /dev/null +++ b/app/api/v1/organizations/crud.py @@ -0,0 +1 @@ +"""Organization CRUD placeholder.""" diff --git a/app/api/v1/organizations/models.py b/app/api/v1/organizations/models.py new file mode 100644 index 0000000..87b2603 --- /dev/null +++ b/app/api/v1/organizations/models.py @@ -0,0 +1 @@ +"""Organization API schemas placeholder.""" diff --git a/app/api/v1/organizations/views.py b/app/api/v1/organizations/views.py new file mode 100644 index 0000000..9e56a4d --- /dev/null +++ b/app/api/v1/organizations/views.py @@ -0,0 +1,16 @@ +"""Organization-related API stubs.""" +from __future__ import annotations + +from fastapi import APIRouter, status + +router = APIRouter(prefix="/organizations", tags=["organizations"]) + + +def _stub(endpoint: str) -> dict[str, str]: + return {"detail": f"{endpoint} is not implemented yet"} + + +@router.get("/me", status_code=status.HTTP_501_NOT_IMPLEMENTED) +async def list_user_organizations() -> dict[str, str]: + """Placeholder for returning organizations linked to the current user.""" + return _stub("GET /organizations/me") diff --git a/app/api/v1/tasks/__init__.py b/app/api/v1/tasks/__init__.py new file mode 100644 index 0000000..18eff37 --- /dev/null +++ b/app/api/v1/tasks/__init__.py @@ -0,0 +1,4 @@ +"""Tasks API package.""" +from .views import router + +__all__ = ["router"] diff --git a/app/api/v1/tasks/crud.py b/app/api/v1/tasks/crud.py new file mode 100644 index 0000000..54ac549 --- /dev/null +++ b/app/api/v1/tasks/crud.py @@ -0,0 +1 @@ +"""Task CRUD placeholder.""" diff --git a/app/api/v1/tasks/models.py b/app/api/v1/tasks/models.py new file mode 100644 index 0000000..f677e6a --- /dev/null +++ b/app/api/v1/tasks/models.py @@ -0,0 +1,13 @@ +"""Task API schemas.""" +from __future__ import annotations + +from datetime import date + +from pydantic import BaseModel + + +class TaskCreatePayload(BaseModel): + deal_id: int + title: str + description: str | None = None + due_date: date diff --git a/app/api/v1/tasks/views.py b/app/api/v1/tasks/views.py new file mode 100644 index 0000000..4955ad7 --- /dev/null +++ b/app/api/v1/tasks/views.py @@ -0,0 +1,32 @@ +"""Task API stubs supporting list/create operations.""" +from __future__ import annotations + +from datetime import date + +from fastapi import APIRouter, Query, status + +from .models import TaskCreatePayload + +router = APIRouter(prefix="/tasks", tags=["tasks"]) + + +def _stub(endpoint: str) -> dict[str, str]: + return {"detail": f"{endpoint} is not implemented yet"} + + +@router.get("/", status_code=status.HTTP_501_NOT_IMPLEMENTED) +async def list_tasks( + deal_id: int | None = None, + only_open: bool = False, + due_before: date | None = Query(default=None), + due_after: date | None = Query(default=None), +) -> dict[str, str]: + """Placeholder for task filtering endpoint.""" + return _stub("GET /tasks") + + +@router.post("/", status_code=status.HTTP_501_NOT_IMPLEMENTED) +async def create_task(payload: TaskCreatePayload) -> dict[str, str]: + """Placeholder for creating a task linked to a deal.""" + _ = payload + return _stub("POST /tasks") diff --git a/app/api/v1/users/__init__.py b/app/api/v1/users/__init__.py new file mode 100644 index 0000000..10e1bbc --- /dev/null +++ b/app/api/v1/users/__init__.py @@ -0,0 +1,4 @@ +"""Users API package.""" +from .views import router + +__all__ = ["router"] diff --git a/app/api/v1/users/crud.py b/app/api/v1/users/crud.py new file mode 100644 index 0000000..eacf20a --- /dev/null +++ b/app/api/v1/users/crud.py @@ -0,0 +1 @@ +"""User CRUD placeholder.""" diff --git a/app/api/v1/users/models.py b/app/api/v1/users/models.py new file mode 100644 index 0000000..83a5dd9 --- /dev/null +++ b/app/api/v1/users/models.py @@ -0,0 +1 @@ +"""User API schemas placeholder.""" diff --git a/app/api/v1/users.py b/app/api/v1/users/views.py similarity index 100% rename from app/api/v1/users.py rename to app/api/v1/users/views.py