dev #11

Merged
k1nq merged 76 commits from dev into master 2025-11-30 04:48:35 +00:00
35 changed files with 382 additions and 2 deletions
Showing only changes of commit 666e0c49f8 - Show all commits

View File

@ -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)

View File

@ -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",
]

View File

@ -0,0 +1,4 @@
"""Activities API package."""
from .views import router
__all__ = ["router"]

View File

@ -0,0 +1 @@
"""CRUD helpers for activities (to be implemented)."""

View File

@ -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]

View File

@ -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")

View File

@ -0,0 +1,4 @@
"""Analytics API package."""
from .views import router
__all__ = ["router"]

View File

@ -0,0 +1 @@
"""Analytics CRUD/query helpers placeholder."""

View File

@ -0,0 +1 @@
"""Analytics schemas placeholder."""

View File

@ -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")

View File

@ -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,

View File

@ -0,0 +1,4 @@
"""Auth API package."""
from .views import router
__all__ = ["router"]

1
app/api/v1/auth/crud.py Normal file
View File

@ -0,0 +1 @@
"""Auth CRUD/service helpers placeholder."""

11
app/api/v1/auth/models.py Normal file
View File

@ -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

40
app/api/v1/auth/views.py Normal file
View File

@ -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)

View File

@ -0,0 +1,4 @@
"""Contacts API package."""
from .views import router
__all__ = ["router"]

View File

@ -0,0 +1 @@
"""Contacts CRUD placeholder."""

View File

@ -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

View File

@ -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")

View File

@ -0,0 +1,4 @@
"""Deals API package."""
from .views import router
__all__ = ["router"]

1
app/api/v1/deals/crud.py Normal file
View File

@ -0,0 +1 @@
"""Deal CRUD placeholder."""

View File

@ -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

45
app/api/v1/deals/views.py Normal file
View File

@ -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}")

View File

@ -0,0 +1,4 @@
"""Organizations API package."""
from .views import router
__all__ = ["router"]

View File

@ -0,0 +1 @@
"""Organization CRUD placeholder."""

View File

@ -0,0 +1 @@
"""Organization API schemas placeholder."""

View File

@ -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")

View File

@ -0,0 +1,4 @@
"""Tasks API package."""
from .views import router
__all__ = ["router"]

1
app/api/v1/tasks/crud.py Normal file
View File

@ -0,0 +1 @@
"""Task CRUD placeholder."""

View File

@ -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

32
app/api/v1/tasks/views.py Normal file
View File

@ -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")

View File

@ -0,0 +1,4 @@
"""Users API package."""
from .views import router
__all__ = ["router"]

1
app/api/v1/users/crud.py Normal file
View File

@ -0,0 +1 @@
"""User CRUD placeholder."""

View File

@ -0,0 +1 @@
"""User API schemas placeholder."""