# Domain Models — Reference Guide

This document explains the domain model pattern used in BanGUI's backend and where to find examples.

---

## What Are Domain Models?

Domain models (e.g., `DomainActiveBanList`, `DomainJailConfig`) are **frozen dataclasses** that represent pure business logic. They are defined in `app/models/{domain}_domain.py` and are **returned by services**.

Response models (e.g., `ActiveBanListResponse`, `JailConfigResponse`) are **Pydantic models** defined in `app/models/{domain}.py`. They are used **only by routers** for HTTP serialization.

---

## Why This Separation?

```
Service (returns domain model)
    ↓
Router (converts domain → response via mapper)
    ↓
HTTP Response (Pydantic model)
```

**Benefits:**
- Domain logic evolves without affecting API shape
- Services are reusable across different frontends (GraphQL, gRPC, CLI)
- Testing is simpler (no Pydantic overhead)
- Changes to endpoint responses don't require service changes

---

## Existing Domain Models

| Domain | Domain Model(s) | Mapper Module |
|--------|----------------|---------------|
| **Ban** | `DomainActiveBanList`, `DomainActiveBan`, `DomainBansByCountry` | `ban_mappers.py` |
| **Jail** | `DomainJailList`, `DomainJailDetail`, `DomainJailBannedIps`, `DomainActiveBan` | `jail_mappers.py` |
| **Config** | `DomainJailConfig`, `DomainJailConfigList`, `DomainGlobalConfig`, `DomainServiceStatus`, `DomainBantimeEscalation`, `DomainFilterConfig`, `DomainFilterList`, `DomainRegexTest`, `DomainMapColorThresholds` | `config_mappers.py` |
| **History** | `DomainHistoryList`, `DomainHistoryBanItem`, `DomainIpDetail`, `DomainIpTimelineEvent` | `history_mappers.py` |
| **Server** | `DomainServerSettings`, `DomainServerSettingsResult` | `server_mappers.py` |
| **Blocklist** | `DomainBlocklistSource`, `DomainImportLogEntry`, `DomainImportLogList`, `DomainImportSourceResult`, `DomainImportRunResult`, `DomainPreviewResult`, `DomainScheduleConfig`, `DomainScheduleInfo` | `blocklist_mappers.py` |

---

## The Pattern — Step by Step

### Step 1: Define Domain Model in `app/models/{domain}_domain.py`

```python
from dataclasses import dataclass

@dataclass(frozen=True)
class DomainJailConfig:
    """Configuration snapshot of a single jail (domain model)."""

    name: str
    ban_time: int
    max_retry: int
    find_time: int
    fail_regex: list[str]
    actions: list[str]                    # ← no default BEFORE default = FIELD ORDER ERROR
    date_pattern: str | None = None        # ← all fields with defaults come AFTER
    log_encoding: LogEncoding = "UTF-8"
```

**⚠️ Field Order Rule:** All fields without defaults must appear before all fields with defaults.

### Step 2: Add Mapper in `app/mappers/{domain}_mappers.py`

```python
def map_domain_jail_config_to_response(domain: DomainJailConfig) -> JailConfig:
    """Convert domain jail config to response model."""
    return JailConfig(
        name=domain.name,
        ban_time=domain.ban_time,
        ...
    )
```

### Step 3: Service Returns Domain Model

```python
# In app/services/jail_service.py
from app.models.config_domain import DomainJailConfig, DomainJailConfigList

async def get_jail_config(socket_path: str, name: str) -> DomainJailConfig:
    ...
    return DomainJailConfig(...)    # ← return domain model
```

### Step 4: Router Uses Mapper at Boundary

```python
# In app/routers/jail_config.py
from app.mappers import config_mappers

@router.get("/{name}", response_model=JailConfigResponse)
async def get_jail_config(...) -> JailConfigResponse:
    domain_result = await config_service.get_jail_config(socket_path, name)
    return config_mappers.map_domain_jail_config_to_response(domain_result)
```

---

## Reference Implementation

`ban_service.py` + `ban_mappers.py` is the canonical example of the correct pattern. Study it first when adding a new service.

---

## Common Issues

### Field Ordering Error

```
TypeError: non-default argument 'actions' follows default argument
```

**Fix:** Move all fields with defaults (`field: T | None = None`) after all fields without defaults.

### Forgetting the Mapper

If you refactor a service to return a domain model but forget to update the router, you'll get a type mismatch at the boundary. Always update router + service together.
