# Profile Module — DDD Template for New Modules

This document captures the complete structure of the profile module as a reusable template for adding new DDD modules to the streaming app backend.

## What It Covers

6 domain entities, full CRUD, repository protocol (28 methods), SQLAlchemy models (6 tables), Alembic migration, and 18 REST API endpoints — all in one module.

## Module Structure

```
app/modules/profile/
├── __init__.py
├── domain/
│   ├── __init__.py
│   └── entities.py          # 6 @dataclass entities
├── application/
│   ├── __init__.py
│   ├── interfaces.py        # ProfileRepository Protocol (28 methods)
│   └── use_cases.py         # ProfileUseCases (all operations)
├── infrastructure/
│   ├── __init__.py
│   ├── models.py            # 6 SQLAlchemy models with FK CASCADE
│   └── repositories.py      # Full SQLAlchemyProfileRepository impl
└── presentation/
    ├── __init__.py
    ├── schemas.py            # 12 Pydantic schemas
    ├── dependencies.py       # DI wiring
    └── routers.py            # 18 endpoints
```

## Layer Template

### 1. Domain Entities (`domain/entities.py`)

```python
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime

@dataclass
class ProfileEntity:
    id: int | None
    user_id: int
    name: str
    avatar: str | None
    is_default: bool = False
    created_at: datetime | None = None
    updated_at: datetime | None = None
```

Rules:
- Pure Python, zero external dependencies
- `@dataclass` for simplicity (not Pydantic — that's for presentation)
- `id: int | None` — None means not persisted yet
- Defaults and optionals for fields the API consumer may not provide

### 2. Application Interfaces (`application/interfaces.py`)

```python
from typing import Protocol

class ProfileRepository(Protocol):
    async def create(self, user_id: int, name: str, avatar: str | None) -> ProfileEntity: ...
    async def get_by_id(self, profile_id: int) -> ProfileEntity | None: ...
    async def list_by_user(self, user_id: int) -> list[ProfileEntity]: ...
    # ... 28 total methods
```

Rules:
- `Protocol` (structural subtyping) not ABC
- Return domain entities, not Pydantic schemas or ORM models
- Group by domain concept: Profile CRUD, Watch History, Continue Watching, Watchlist, Playback Progress, Preferences

### 3. Application Use Cases (`application/use_cases.py`)

```python
class ProfileUseCases:
    def __init__(self, repo: ProfileRepository) -> None:
        self._repo = repo

    async def create_profile(self, user_id: int, name: str, avatar: str | None = None) -> ProfileEntity:
        profiles = await self._repo.list_by_user(user_id)
        profile = await self._repo.create(user_id, name, avatar)
        if len(profiles) == 0:  # First profile = auto-default
            await self._repo.set_default(user_id, profile.id)
            profile.is_default = True
        return profile
```

Patterns:
- Use case constructor takes the Protocol port, not a concrete class
- Business logic (first profile = default) lives here, not in the repo
- Each method validates ownership before operating
- Raise `ValueError` for domain errors (caught by router → 400)
- Accept primitive types from the API boundary, not Pydantic models

### 4. Infrastructure Models (`infrastructure/models.py`)

```python
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base

class ProfileModel(Base):
    __tablename__ = "profiles"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
    name: Mapped[str] = mapped_column(String(100), nullable=False)
```

Rules:
- Import from `app.core.database.Base` (shared single Base)
- `ondelete="CASCADE"` for all FK relationships
- Index on foreign key columns that are queried frequently
- Timestamps with `default=lambda: datetime.now(UTC)`
- Use `Mapped[]` type annotations (SQLAlchemy 2.0 style)

### 5. Infrastructure Repository (`infrastructure/repositories.py`)

```python
def _profile_to_entity(model: ProfileModel) -> ProfileEntity:
    return ProfileEntity(id=model.id, user_id=model.user_id, ...)

class SQLAlchemyProfileRepository(ProfileRepository):
    def __init__(self, session: AsyncSession) -> None:
        self.session = session

    async def create(self, user_id: int, name: str, avatar: str | None) -> ProfileEntity:
        model = ProfileModel(user_id=user_id, name=name, avatar=avatar)
        self.session.add(model)
        await self.session.flush()
        await self.session.refresh(model)
        return _profile_to_entity(model)
```

Patterns:
- Dedicated mapper functions (`_model_to_entity`) — one per entity type
- `flush()` not `commit()` — session lifecycle managed by caller (get_async_session)
- Refresh after flush to get auto-generated IDs and defaults
- Use `select()`, `update()` from SQLAlchemy, avoid raw SQL

### 6. Presentation Schemas (`presentation/schemas.py`)

```python
class ProfileCreateRequest(BaseModel):
    name: str = Field(min_length=1, max_length=100)
    avatar: str | None = None

class ProfileResponse(BaseModel):
    id: int
    user_id: int
    name: str
    avatar: str | None = None
    is_default: bool = False
```

Rules:
- Request schemas: validation rules (min_length, max_length, pattern)
- Response schemas: mirror domain entities, add `model_config`
- Use `Field(pattern=r"^(movie|tv)$")` for enum-like string validation
- One schema per distinct API shape

### 7. Presentation Dependencies (`presentation/dependencies.py`)

```python
def get_profile_use_cases(
    session: Annotated[AsyncIterator[AsyncSession], Depends(get_async_session)],
) -> ProfileUseCases:
    repo = SQLAlchemyProfileRepository(session)
    return ProfileUseCases(repo=repo)
```

Pattern:
- FastAPI `Depends(get_async_session)` for session injection
- Construct the concrete repo inside the dependency
- Return the use case with its port wired up

### 8. Presentation Routers (`presentation/routers.py`)

```python
router = APIRouter(prefix="/profiles", tags=["profiles"])

@router.get("/", response_model=list[ProfileResponse])
async def list_profiles(
    user_id: Annotated[int, Depends(require_user_id)],
    use_cases: Annotated[ProfileUseCases, Depends(get_profile_use_cases)],
) -> list[ProfileResponse]:
    profiles = await use_cases.list_profiles(user_id)
    return [ProfileResponse.model_validate(p) for p in profiles]
```

Patterns:
- `APIRouter(prefix="/resource-name")` at module level
- `response_model` = Pydantic schema
- `model_validate()` to convert domain entities to response schemas
- `_handle_value_error()` helper for consistent 400 responses
- Auth via `require_user_id` dependency injection
- Sub-resources nested under `/{profile_id}/history`, `/{profile_id}/watchlist`, etc.

### 9. Wiring in main.py

```python
from app.modules.profile.presentation.routers import router as profile_router
# ... in create_app():
app.include_router(profile_router, prefix="/api/v1")
```

### 10. Wiring in alembic/env.py

```python
from app.modules.profile.infrastructure import models as _profile_models  # noqa: F401
```

### 11. Migration

```bash
alembic revision --autogenerate -m "add profile tables"
```

## Endpoints Generated

| Method | Path | Description |
|--------|------|-------------|
| GET | /api/v1/profiles/ | List profiles |
| POST | /api/v1/profiles/ | Create profile |
| GET | /api/v1/profiles/{id} | Get profile |
| PATCH | /api/v1/profiles/{id} | Update profile |
| DELETE | /api/v1/profiles/{id} | Delete profile |
| POST | /api/v1/profiles/{id}/default | Set as default |
| GET | /api/v1/profiles/{id}/history | Get watch history |
| POST | /api/v1/profiles/{id}/history | Add to history |
| DELETE | /api/v1/profiles/{id}/history | Clear history |
| GET | /api/v1/profiles/{id}/continue-watching | Get list |
| PUT | /api/v1/profiles/{id}/continue-watching | Update position |
| DELETE | /api/v1/profiles/{id}/continue-watching/{tmdb_id} | Remove entry |
| GET | /api/v1/profiles/{id}/watchlist | Get watchlist |
| POST | /api/v1/profiles/{id}/watchlist | Add to watchlist |
| GET | /api/v1/profiles/{id}/watchlist/{tmdb_id}/check | Check presence |
| DELETE | /api/v1/profiles/{id}/watchlist/{tmdb_id} | Remove from watchlist |
| PUT | /api/v1/profiles/{id}/progress | Update playback |
| GET | /api/v1/profiles/{id}/progress | All progress |
| GET | /api/v1/profiles/{id}/progress/{tmdb_id} | Single progress |
| GET | /api/v1/profiles/{id}/preferences | Get prefs |
| PATCH | /api/v1/profiles/{id}/preferences | Update prefs |

## Reuse Steps

To create a similar module from scratch:

1. Copy this structure: `mkdir -p app/modules/<name>/{domain,application,infrastructure,presentation}`
2. Create `__init__.py` in each directory
3. Build entities first (domain/), then ports (application/), then use cases (application/)
4. Build models (infrastructure/), then repository (infrastructure/)
5. Build schemas (presentation/), then dependencies, then routers
6. Register in `alembic/env.py` and `app/main.py`
7. Run migration: `alembic revision --autogenerate -m "add <name> tables"`
8. Run `ruff check`, `mypy`, `pytest -q --tb=no` — fix any issues
9. Commit
