diff --git a/app/api/v1/activities/views.py b/app/api/v1/activities.py similarity index 84% rename from app/api/v1/activities/views.py rename to app/api/v1/activities.py index cbd77c6..d33a18a 100644 --- a/app/api/v1/activities/views.py +++ b/app/api/v1/activities.py @@ -1,7 +1,10 @@ -"""Activity timeline endpoints backed by ActivityService.""" +"""Activity timeline endpoints and payload schemas.""" from __future__ import annotations +from typing import Literal + from fastapi import APIRouter, Depends, HTTPException, Query, status +from pydantic import BaseModel, Field from app.api.deps import get_activity_service, get_organization_context from app.models.activity import ActivityRead @@ -13,7 +16,18 @@ from app.services.activity_service import ( ) from app.services.organization_service import OrganizationContext -from .models import ActivityCommentPayload + +class ActivityCommentBody(BaseModel): + text: str = Field(..., min_length=1, max_length=2000) + + +class ActivityCommentPayload(BaseModel): + type: Literal["comment"] = "comment" + payload: ActivityCommentBody + + def extract_text(self) -> str: + return self.payload.text.strip() + router = APIRouter(prefix="/deals/{deal_id}/activities", tags=["activities"]) diff --git a/app/api/v1/activities/__init__.py b/app/api/v1/activities/__init__.py deleted file mode 100644 index 23c693e..0000000 --- a/app/api/v1/activities/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""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 deleted file mode 100644 index 46d3aff..0000000 --- a/app/api/v1/activities/crud.py +++ /dev/null @@ -1 +0,0 @@ -"""CRUD helpers for activities (to be implemented).""" diff --git a/app/api/v1/activities/models.py b/app/api/v1/activities/models.py deleted file mode 100644 index 4b6abaf..0000000 --- a/app/api/v1/activities/models.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Pydantic schemas for activity endpoints.""" -from __future__ import annotations - -from typing import Literal - -from pydantic import BaseModel, Field - - -class ActivityCommentBody(BaseModel): - text: str = Field(..., min_length=1, max_length=2000) - - -class ActivityCommentPayload(BaseModel): - type: Literal["comment"] = "comment" - payload: ActivityCommentBody - - def extract_text(self) -> str: - return self.payload.text.strip() diff --git a/app/api/v1/analytics/views.py b/app/api/v1/analytics.py similarity index 100% rename from app/api/v1/analytics/views.py rename to app/api/v1/analytics.py diff --git a/app/api/v1/analytics/__init__.py b/app/api/v1/analytics/__init__.py deleted file mode 100644 index e177bd2..0000000 --- a/app/api/v1/analytics/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""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 deleted file mode 100644 index f285a92..0000000 --- a/app/api/v1/analytics/crud.py +++ /dev/null @@ -1 +0,0 @@ -"""Analytics CRUD/query helpers placeholder.""" diff --git a/app/api/v1/analytics/models.py b/app/api/v1/analytics/models.py deleted file mode 100644 index 760bda7..0000000 --- a/app/api/v1/analytics/models.py +++ /dev/null @@ -1 +0,0 @@ -"""Analytics schemas placeholder.""" diff --git a/app/api/v1/auth/views.py b/app/api/v1/auth.py similarity index 93% rename from app/api/v1/auth/views.py rename to app/api/v1/auth.py index ed07679..a424036 100644 --- a/app/api/v1/auth/views.py +++ b/app/api/v1/auth.py @@ -1,6 +1,7 @@ -"""Authentication API endpoints.""" +"""Authentication API endpoints and payloads.""" from __future__ import annotations +from pydantic import BaseModel, EmailStr from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.exc import IntegrityError @@ -13,7 +14,13 @@ from app.models.user import UserCreate from app.repositories.user_repo import UserRepository from app.services.auth_service import AuthService, InvalidCredentialsError -from .models import RegisterRequest + +class RegisterRequest(BaseModel): + email: EmailStr + password: str + name: str + organization_name: str + router = APIRouter(prefix="/auth", tags=["auth"]) diff --git a/app/api/v1/auth/__init__.py b/app/api/v1/auth/__init__.py deleted file mode 100644 index 8fc31a8..0000000 --- a/app/api/v1/auth/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""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 deleted file mode 100644 index 600514b..0000000 --- a/app/api/v1/auth/crud.py +++ /dev/null @@ -1 +0,0 @@ -"""Auth CRUD/service helpers placeholder.""" diff --git a/app/api/v1/auth/models.py b/app/api/v1/auth/models.py deleted file mode 100644 index 1d1f6cb..0000000 --- a/app/api/v1/auth/models.py +++ /dev/null @@ -1,11 +0,0 @@ -"""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/contacts/views.py b/app/api/v1/contacts.py similarity index 85% rename from app/api/v1/contacts/views.py rename to app/api/v1/contacts.py index 8fa8529..808b769 100644 --- a/app/api/v1/contacts/views.py +++ b/app/api/v1/contacts.py @@ -1,12 +1,18 @@ -"""Contact API stubs required by the spec.""" +"""Contact API stubs and schemas.""" from __future__ import annotations from fastapi import APIRouter, Depends, Query, status +from pydantic import BaseModel, EmailStr from app.api.deps import get_organization_context from app.services.organization_service import OrganizationContext -from .models import ContactCreatePayload + +class ContactCreatePayload(BaseModel): + name: str + email: EmailStr | None = None + phone: str | None = None + router = APIRouter(prefix="/contacts", tags=["contacts"]) diff --git a/app/api/v1/contacts/__init__.py b/app/api/v1/contacts/__init__.py deleted file mode 100644 index 8047a39..0000000 --- a/app/api/v1/contacts/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""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 deleted file mode 100644 index 519972b..0000000 --- a/app/api/v1/contacts/crud.py +++ /dev/null @@ -1 +0,0 @@ -"""Contacts CRUD placeholder.""" diff --git a/app/api/v1/contacts/models.py b/app/api/v1/contacts/models.py deleted file mode 100644 index 659c192..0000000 --- a/app/api/v1/contacts/models.py +++ /dev/null @@ -1,10 +0,0 @@ -"""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/deals/views.py b/app/api/v1/deals.py similarity index 83% rename from app/api/v1/deals/views.py rename to app/api/v1/deals.py index 937b97b..dff58dc 100644 --- a/app/api/v1/deals/views.py +++ b/app/api/v1/deals.py @@ -1,12 +1,13 @@ -"""Deal API endpoints backed by DealService.""" +"""Deal API endpoints backed by DealService with inline payload schemas.""" from __future__ import annotations from decimal import Decimal from fastapi import APIRouter, Depends, HTTPException, Query, status +from pydantic import BaseModel from app.api.deps import get_deal_repository, get_deal_service, get_organization_context -from app.models.deal import DealRead, DealStage, DealStatus +from app.models.deal import DealCreate, DealRead, DealStage, DealStatus from app.repositories.deal_repo import DealRepository, DealAccessError, DealQueryParams from app.services.deal_service import ( DealService, @@ -16,7 +17,31 @@ from app.services.deal_service import ( ) from app.services.organization_service import OrganizationContext -from .models import DealCreatePayload, DealUpdatePayload + +class DealCreatePayload(BaseModel): + contact_id: int + title: str + amount: Decimal | None = None + currency: str | None = None + owner_id: int | None = None + + def to_domain(self, *, organization_id: int, fallback_owner: int) -> DealCreate: + return DealCreate( + organization_id=organization_id, + contact_id=self.contact_id, + owner_id=self.owner_id or fallback_owner, + title=self.title, + amount=self.amount, + currency=self.currency, + ) + + +class DealUpdatePayload(BaseModel): + status: DealStatus | None = None + stage: DealStage | None = None + amount: Decimal | None = None + currency: str | None = None + router = APIRouter(prefix="/deals", tags=["deals"]) diff --git a/app/api/v1/deals/__init__.py b/app/api/v1/deals/__init__.py deleted file mode 100644 index bf0962f..0000000 --- a/app/api/v1/deals/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""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 deleted file mode 100644 index 1c3b117..0000000 --- a/app/api/v1/deals/crud.py +++ /dev/null @@ -1 +0,0 @@ -"""Deal CRUD placeholder.""" diff --git a/app/api/v1/deals/models.py b/app/api/v1/deals/models.py deleted file mode 100644 index 620320f..0000000 --- a/app/api/v1/deals/models.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Deal API schemas.""" -from __future__ import annotations - -from decimal import Decimal - -from pydantic import BaseModel - -from app.models.deal import DealCreate, DealStage, DealStatus - - -class DealCreatePayload(BaseModel): - contact_id: int - title: str - amount: Decimal | None = None - currency: str | None = None - owner_id: int | None = None - - def to_domain(self, *, organization_id: int, fallback_owner: int) -> DealCreate: - return DealCreate( - organization_id=organization_id, - contact_id=self.contact_id, - owner_id=self.owner_id or fallback_owner, - title=self.title, - amount=self.amount, - currency=self.currency, - ) - - -class DealUpdatePayload(BaseModel): - status: DealStatus | None = None - stage: DealStage | None = None - amount: Decimal | None = None - currency: str | None = None diff --git a/app/api/v1/organizations/views.py b/app/api/v1/organizations.py similarity index 100% rename from app/api/v1/organizations/views.py rename to app/api/v1/organizations.py diff --git a/app/api/v1/organizations/__init__.py b/app/api/v1/organizations/__init__.py deleted file mode 100644 index f8cf943..0000000 --- a/app/api/v1/organizations/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""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 deleted file mode 100644 index 7731ce7..0000000 --- a/app/api/v1/organizations/crud.py +++ /dev/null @@ -1 +0,0 @@ -"""Organization CRUD placeholder.""" diff --git a/app/api/v1/organizations/models.py b/app/api/v1/organizations/models.py deleted file mode 100644 index 87b2603..0000000 --- a/app/api/v1/organizations/models.py +++ /dev/null @@ -1 +0,0 @@ -"""Organization API schemas placeholder.""" diff --git a/app/api/v1/tasks/views.py b/app/api/v1/tasks.py similarity index 66% rename from app/api/v1/tasks/views.py rename to app/api/v1/tasks.py index ea32ff0..00deec6 100644 --- a/app/api/v1/tasks/views.py +++ b/app/api/v1/tasks.py @@ -1,12 +1,13 @@ -"""Task API endpoints backed by TaskService.""" +"""Task API endpoints with inline schemas.""" from __future__ import annotations -from datetime import date +from datetime import date, datetime, time, timezone from fastapi import APIRouter, Depends, HTTPException, Query, status +from pydantic import BaseModel from app.api.deps import get_organization_context, get_task_service -from app.models.task import TaskRead +from app.models.task import TaskCreate, TaskRead from app.services.organization_service import OrganizationContext from app.services.task_service import ( TaskDueDateError, @@ -16,7 +17,34 @@ from app.services.task_service import ( TaskService, ) -from .models import TaskCreatePayload, to_range_boundary + +class TaskCreatePayload(BaseModel): + deal_id: int + title: str + description: str | None = None + due_date: date | None = None + + def to_domain(self) -> TaskCreate: + return TaskCreate( + deal_id=self.deal_id, + title=self.title, + description=self.description, + due_date=_date_to_datetime(self.due_date) if self.due_date else None, + ) + + +def to_range_boundary(value: date | None, *, end_of_day: bool) -> datetime | None: + """Convert a date query param to an inclusive datetime boundary.""" + + if value is None: + return None + boundary_time = time(23, 59, 59, 999999) if end_of_day else time(0, 0, 0) + return datetime.combine(value, boundary_time, tzinfo=timezone.utc) + + +def _date_to_datetime(value: date) -> datetime: + return datetime.combine(value, time(0, 0, 0), tzinfo=timezone.utc) + router = APIRouter(prefix="/tasks", tags=["tasks"]) diff --git a/app/api/v1/tasks/__init__.py b/app/api/v1/tasks/__init__.py deleted file mode 100644 index 18eff37..0000000 --- a/app/api/v1/tasks/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""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 deleted file mode 100644 index 54ac549..0000000 --- a/app/api/v1/tasks/crud.py +++ /dev/null @@ -1 +0,0 @@ -"""Task CRUD placeholder.""" diff --git a/app/api/v1/tasks/models.py b/app/api/v1/tasks/models.py deleted file mode 100644 index 3e4d71e..0000000 --- a/app/api/v1/tasks/models.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Task API schemas and helpers.""" -from __future__ import annotations - -from datetime import date, datetime, time, timezone - -from pydantic import BaseModel - -from app.models.task import TaskCreate - - -class TaskCreatePayload(BaseModel): - deal_id: int - title: str - description: str | None = None - due_date: date | None = None - - def to_domain(self) -> TaskCreate: - return TaskCreate( - deal_id=self.deal_id, - title=self.title, - description=self.description, - due_date=_date_to_datetime(self.due_date) if self.due_date else None, - ) - - -def to_range_boundary(value: date | None, *, end_of_day: bool) -> datetime | None: - """Convert a date query param to an inclusive datetime boundary.""" - - if value is None: - return None - boundary_time = time(23, 59, 59, 999999) if end_of_day else time(0, 0, 0) - return datetime.combine(value, boundary_time, tzinfo=timezone.utc) - - -def _date_to_datetime(value: date) -> datetime: - return datetime.combine(value, time(0, 0, 0), tzinfo=timezone.utc) diff --git a/app/api/v1/users/views.py b/app/api/v1/users.py similarity index 100% rename from app/api/v1/users/views.py rename to app/api/v1/users.py diff --git a/app/api/v1/users/__init__.py b/app/api/v1/users/__init__.py deleted file mode 100644 index 10e1bbc..0000000 --- a/app/api/v1/users/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""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 deleted file mode 100644 index eacf20a..0000000 --- a/app/api/v1/users/crud.py +++ /dev/null @@ -1 +0,0 @@ -"""User CRUD placeholder.""" diff --git a/app/api/v1/users/models.py b/app/api/v1/users/models.py deleted file mode 100644 index 83a5dd9..0000000 --- a/app/api/v1/users/models.py +++ /dev/null @@ -1 +0,0 @@ -"""User API schemas placeholder."""