# Architecture Patterns & Design Decisions

This document describes the architectural patterns and design decisions that must be followed throughout the codebase.

> **Note:** These patterns are enforced automatically by `scripts/validate/validate_architecture.py`. Run the validator before committing code.

---

## Table of Contents

1. [Core Principles](#core-principles)
2. [Layered Architecture](#layered-architecture)
3. [API Endpoint Patterns](#api-endpoint-patterns)
4. [Service Layer Patterns](#service-layer-patterns)
5. [Model Patterns](#model-patterns)
6. [Exception Handling](#exception-handling)
7. [JavaScript Patterns](#javascript-patterns)
8. [Validation](#validation)

---

## Core Principles

### 1. Separation of Concerns

**Each layer has specific responsibilities:**

- **Routes/Endpoints**: HTTP handling, validation, authentication, response formatting
- **Services**: Business logic, data processing, orchestration
- **Models**: Data structure and persistence

**❌ Bad Example - Business logic in endpoint:**

```python
@router.post("/stores")
async def create_store(store: StoreCreate, db: Session = Depends(get_db)):
    # ❌ BAD: Business logic in endpoint
    if db.query(Store).filter(Store.subdomain == store.subdomain).first():
        raise HTTPException(status_code=409, detail="Store exists")

    db_store = Store(**store.dict())
    db.add(db_store)
    db.commit()
    db.refresh(db_store)
    return db_store
```

**✅ Good Example - Delegated to service:**

```python
@router.post("/stores", response_model=StoreResponse)
async def create_store(
    store: StoreCreate,
    current_user: User = Depends(get_current_admin),
    db: Session = Depends(get_db)
):
    try:
        # ✅ GOOD: Delegate to service
        result = store_service.create_store(db, store)
        return result
    except StoreAlreadyExistsError as e:
        raise HTTPException(status_code=409, detail=str(e))
    except Exception as e:
        logger.error(f"Failed to create store: {e}")
        raise HTTPException(status_code=500, detail="Internal server error")
```

### 2. Type Safety

Use Pydantic for API validation, SQLAlchemy for database models.

### 3. Proper Exception Handling

Services throw domain exceptions, routes convert to HTTP responses.

---

## Layered Architecture

```
┌─────────────────────────────────────────────────────────┐
│                     API Layer                            │
│  (app/api/v1/**/*.py, app/routes/**/*.py)               │
│                                                          │
│  Responsibilities:                                       │
│  - HTTP request/response handling                       │
│  - Authentication/Authorization                         │
│  - Input validation (Pydantic models)                   │
│  - Exception handling (domain → HTTP)                   │
│                                                          │
│  ❌ Should NOT:                                          │
│  - Contain business logic                               │
│  - Directly access database (except via services)       │
│  - Raise domain exceptions                              │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│                   Service Layer                          │
│  (app/services/**/*.py)                                 │
│                                                          │
│  Responsibilities:                                       │
│  - Business logic                                        │
│  - Data validation (business rules)                     │
│  - Database operations                                   │
│  - Orchestration of multiple operations                 │
│                                                          │
│  ❌ Should NOT:                                          │
│  - Know about HTTP (no HTTPException)                   │
│  - Create database sessions (accept as parameter)       │
│  - Handle HTTP-specific concerns                        │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│                   Data Layer                             │
│  (app/models/**/*.py)                                   │
│                                                          │
│  Responsibilities:                                       │
│  - Database schema (SQLAlchemy models)                  │
│  - API schemas (Pydantic models)                        │
│  - Data structure definitions                           │
│                                                          │
│  ❌ Should NOT:                                          │
│  - Mix SQLAlchemy and Pydantic in same class            │
│  - Contain business logic                               │
└─────────────────────────────────────────────────────────┘
```

---

## API Endpoint Patterns

### Rule API-001: Use Pydantic Models

**All endpoints MUST use Pydantic models for request/response.**

```python
# ✅ GOOD: Pydantic models for type safety
class StoreCreate(BaseModel):
    name: str = Field(..., max_length=200)
    subdomain: str = Field(..., max_length=100)
    is_active: bool = True

class StoreResponse(BaseModel):
    id: int
    name: str
    subdomain: str
    created_at: datetime

    class Config:
        from_attributes = True  # For SQLAlchemy compatibility

@router.post("/stores", response_model=StoreResponse)
async def create_store(store: StoreCreate, db: Session = Depends(get_db)):
    result = store_service.create_store(db, store)
    return result
```

```python
# ❌ BAD: Raw dict, no validation
@router.post("/stores")
async def create_store(data: dict):
    return {"name": data["name"]}  # No type safety!
```

### Rule API-002: No Business Logic in Endpoints

**Endpoints should only handle HTTP concerns.**

```python
# ✅ GOOD: Delegate to service
@router.post("/stores")
async def create_store(store: StoreCreate, db: Session = Depends(get_db)):
    result = store_service.create_store(db, store)
    return result
```

```python
# ❌ BAD: Business logic in endpoint
@router.post("/stores")
async def create_store(store: StoreCreate, db: Session = Depends(get_db)):
    # ❌ Database operations belong in service!
    db_store = Store(**store.dict())
    db.add(db_store)
    db.commit()
    return db_store
```

### Rule API-003: Proper Exception Handling

**Catch service exceptions and convert to HTTPException.**

```python
# ✅ GOOD: Proper exception handling
@router.post("/stores", response_model=StoreResponse)
async def create_store(store: StoreCreate, db: Session = Depends(get_db)):
    try:
        result = store_service.create_store(db, store)
        return result
    except StoreAlreadyExistsError as e:
        raise HTTPException(status_code=409, detail=str(e))
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        logger.error(f"Unexpected error creating store: {e}")
        raise HTTPException(status_code=500, detail="Internal server error")
```

### Rule API-004: Authentication

**Protected endpoints must use dependency injection for auth.**

```python
# ✅ GOOD: Use Depends for auth
@router.post("/stores")
async def create_store(
    store: StoreCreate,
    current_user: User = Depends(get_current_admin),  # ✅ Auth required
    db: Session = Depends(get_db)
):
    result = store_service.create_store(db, store)
    return result
```

### Rule API-005: Store Context from Token (Not URL)

**Store API endpoints MUST extract store context from JWT token, NOT from URL.**

> **Rationale:** Embedding store context in JWT tokens enables clean RESTful API endpoints, eliminates URL-based store detection issues, and improves security by cryptographically signing store access.

**❌ BAD: URL-based store detection**

```python
from middleware.store_context import require_store_context

@router.get("/products")
def get_products(
    store: Store = Depends(require_store_context()),  # ❌ Requires store in URL
    current_user: User = Depends(get_current_store_api),
    db: Session = Depends(get_db),
):
    # This fails on /api/v1/store/products (no store in URL)
    products = product_service.get_store_products(db, store.id)
    return products
```

**Issues with URL-based approach:**
- ❌ Only works with routes like `/store/{store_code}/dashboard`
- ❌ Fails on API routes like `/api/v1/store/products` (no store in URL)
- ❌ Inconsistent between page routes and API routes
- ❌ Violates RESTful API design
- ❌ Requires database lookup on every request

**✅ GOOD: Token-based store context**

```python
@router.get("/products")
def get_products(
    current_user: User = Depends(get_current_store_api),  # ✅ Store in token
    db: Session = Depends(get_db),
):
    # Extract store from JWT token
    if not hasattr(current_user, "token_store_id"):
        raise HTTPException(
            status_code=400,
            detail="Token missing store information. Please login again.",
        )

    store_id = current_user.token_store_id

    # Use store_id from token
    products = product_service.get_store_products(db, store_id)
    return products
```

**Benefits of token-based approach:**
- ✅ Works on all routes (page and API)
- ✅ Clean RESTful API endpoints
- ✅ Store context cryptographically signed in JWT
- ✅ No database lookup needed for store detection
- ✅ Consistent authentication mechanism
- ✅ Security: Cannot be tampered with by client

**Token structure:**
```json
{
  "sub": "user_id",
  "username": "john.doe",
  "store_id": 123,           ← Store context
  "store_code": "ORION",  ← Store code
  "store_role": "Owner"      ← Store role
}
```

**Available token attributes:**
- `current_user.token_store_id` - Store ID (use for database queries)
- `current_user.token_store_code` - Store code (use for logging)
- `current_user.token_store_role` - Store role (Owner, Manager, etc.)

**Migration checklist:**
1. Remove `store: Store = Depends(require_store_context())`
2. Remove unused imports: `from middleware.store_context import require_store_context`
3. Extract store from token: `store_id = current_user.token_store_id`
4. Add token validation check (see example above)
5. Update logging to use `current_user.token_store_code`

**See also:** `docs/backend/store-in-token-architecture.md` for complete migration guide

**Files requiring migration:**
- `app/api/v1/store/customers.py`
- `app/api/v1/store/notifications.py`
- `app/api/v1/store/media.py`
- `app/api/v1/store/marketplace.py`
- `app/api/v1/store/inventory.py`
- `app/api/v1/store/settings.py`
- `app/api/v1/store/analytics.py`
- `app/api/v1/store/payments.py`
- `app/api/v1/store/profile.py`

---

## Service Layer Patterns

### Rule SVC-001: No HTTPException in Services

**Services should NOT know about HTTP. Raise domain exceptions instead.**

```python
# ✅ GOOD: Domain exception
class StoreAlreadyExistsError(Exception):
    """Raised when store with same subdomain already exists"""
    pass

class StoreService:
    def create_store(self, db: Session, store_data: StoreCreate):
        if self._store_exists(db, store_data.subdomain):
            raise StoreAlreadyExistsError(
                f"Store with subdomain '{store_data.subdomain}' already exists"
            )

        # Business logic...
        store = Store(**store_data.dict())
        db.add(store)
        db.commit()
        return store
```

```python
# ❌ BAD: HTTPException in service
class StoreService:
    def create_store(self, db: Session, store_data: StoreCreate):
        if self._store_exists(db, store_data.subdomain):
            # ❌ Service shouldn't know about HTTP!
            raise HTTPException(status_code=409, detail="Store exists")
```

### Rule SVC-002: Create Custom Exception Classes

**Don't use generic `Exception`. Create specific domain exceptions.**

```python
# ✅ GOOD: Specific exceptions
class StoreError(Exception):
    """Base exception for store-related errors"""
    pass

class StoreNotFoundError(StoreError):
    """Raised when store is not found"""
    pass

class StoreAlreadyExistsError(StoreError):
    """Raised when store already exists"""
    pass

class StoreService:
    def get_store(self, db: Session, store_code: str):
        store = db.query(Store).filter(Store.store_code == store_code).first()
        if not store:
            raise StoreNotFoundError(f"Store '{store_code}' not found")
        return store
```

```python
# ❌ BAD: Generic Exception
class StoreService:
    def get_store(self, db: Session, store_code: str):
        store = db.query(Store).filter(Store.store_code == store_code).first()
        if not store:
            raise Exception("Store not found")  # ❌ Too generic!
        return store
```

### Rule SVC-003: Database Session as Parameter

**Services should receive database session as parameter, not create it internally.**

```python
# ✅ GOOD: db session as parameter
class StoreService:
    def create_store(self, db: Session, store_data: StoreCreate):
        store = Store(**store_data.dict())
        db.add(store)
        db.commit()
        db.refresh(store)
        return store

    def _store_exists(self, db: Session, subdomain: str) -> bool:
        return db.query(Store).filter(Store.subdomain == subdomain).first() is not None
```

```python
# ❌ BAD: Creating session internally
class StoreService:
    def create_store(self, store_data: StoreCreate):
        # ❌ Don't create session here - makes testing hard
        db = SessionLocal()
        store = Store(**store_data.dict())
        db.add(store)
        db.commit()
        return store
```

**Benefits:**

- Testability (can inject mock session)
- Transaction control (caller controls commit/rollback)
- Resource management (caller handles session lifecycle)

### Rule SVC-004: Use Pydantic for Input Validation

**Service methods should accept Pydantic models for complex inputs.**

```python
# ✅ GOOD: Pydantic model ensures validation
class StoreService:
    def create_store(self, db: Session, store_data: StoreCreate):
        # store_data is already validated by Pydantic
        store = Store(**store_data.dict())
        db.add(store)
        db.commit()
        return store
```

---

## Model Patterns

### Rule MDL-001: SQLAlchemy for Database Models

```python
# ✅ GOOD: SQLAlchemy model
from sqlalchemy import Column, Integer, String, Boolean
from app.database import Base

class Store(Base):
    __tablename__ = "stores"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String(200), nullable=False)
    subdomain = Column(String(100), unique=True, nullable=False)
    is_active = Column(Boolean, default=True)
```

### Rule MDL-002: Separate Pydantic from SQLAlchemy

**NEVER mix SQLAlchemy and Pydantic in the same class.**

```python
# ❌ BAD: Mixing SQLAlchemy and Pydantic
class Store(Base, BaseModel):  # ❌ Don't do this!
    __tablename__ = "stores"
    name: str = Column(String(200))
```

```python
# ✅ GOOD: Separate models
# Database model (app/models/store.py)
class Store(Base):
    __tablename__ = "stores"
    id = Column(Integer, primary_key=True)
    name = Column(String(200), nullable=False)

# API model (app/api/v1/admin/stores.py)
class StoreCreate(BaseModel):
    name: str = Field(..., max_length=200)

class StoreResponse(BaseModel):
    id: int
    name: str

    class Config:
        from_attributes = True
```

---

## Exception Handling

### Rule EXC-001: Domain-Specific Exceptions

**Create exception hierarchy in `app/exceptions/`**

```python
# app/exceptions/store_exceptions.py

class StoreError(Exception):
    """Base exception for store domain"""
    pass

class StoreNotFoundError(StoreError):
    """Store does not exist"""
    pass

class StoreAlreadyExistsError(StoreError):
    """Store already exists"""
    pass

class StoreValidationError(StoreError):
    """Store data validation failed"""
    pass
```

### Rule EXC-002: Never Use Bare Except

```python
# ❌ BAD: Bare except
try:
    result = do_something()
except:  # ❌ Catches EVERYTHING including KeyboardInterrupt!
    pass
```

```python
# ✅ GOOD: Specific exceptions
try:
    result = do_something()
except ValueError as e:
    logger.error(f"Validation error: {e}")
except DatabaseError as e:
    logger.error(f"Database error: {e}")
except Exception as e:
    logger.error(f"Unexpected error: {e}")
    raise
```

---

## JavaScript Patterns

### Rule JS-001: Use apiClient Directly

```javascript
// ✅ GOOD
const stores = await apiClient.get('/api/v1/stores');
```

```javascript
// ❌ BAD
const stores = await window.apiClient.get('/api/v1/stores');
```

### Rule JS-002: Use Centralized Logger

```javascript
// ✅ GOOD: Centralized logger
const storeLog = window.LogConfig.createLogger('stores');
storeLog.info('Loading stores...');
storeLog.error('Failed to load stores:', error);
```

```javascript
// ❌ BAD: console
console.log('Loading stores...');  // ❌ Use logger instead
```

### Rule JS-003: Alpine Components Pattern

```javascript
// ✅ GOOD: Proper Alpine component
function storesManager() {
    return {
        // ✅ Inherit base layout functionality
        ...data(),

        // ✅ Set page identifier for sidebar
        currentPage: 'stores',

        // Component state
        stores: [],
        loading: false,

        // ✅ Init with guard
        async init() {
            if (window._storesInitialized) {
                return;
            }
            window._storesInitialized = true;

            await this.loadStores();
        }
    };
}
```

---

## Validation

### Running the Validator

```bash
# Validate entire codebase
python scripts/validate/validate_architecture.py

# Validate specific directory
python scripts/validate/validate_architecture.py app/api/

# Verbose output with context
python scripts/validate/validate_architecture.py --verbose

# Errors only (suppress warnings)
python scripts/validate/validate_architecture.py --errors-only
```

### Pre-commit Hook

Install pre-commit to validate automatically before commits:

```bash
# Install pre-commit
pip install pre-commit

# Setup hooks
pre-commit install

# Run manually
pre-commit run --all-files
```

### CI/CD Integration

Add to your CI pipeline:

```yaml
# .github/workflows/ci.yml
- name: Validate Architecture
  run: |
    python scripts/validate/validate_architecture.py
```

---

## Quick Reference

| Layer | Responsibility | Can Use | Cannot Use |
|-------|---------------|---------|------------|
| **API Endpoints** | HTTP handling, auth, validation | Pydantic, HTTPException, Depends | Direct DB access, business logic |
| **Services** | Business logic, orchestration | DB session, domain exceptions | HTTPException, HTTP concepts |
| **Models** | Data structure | SQLAlchemy OR Pydantic | Mixing both in same class |

---

## Common Violations and Fixes

### Violation: Business logic in endpoint

```python
# Before
@router.post("/stores")
async def create_store(store: StoreCreate, db: Session = Depends(get_db)):
    db_store = Store(**store.dict())
    db.add(db_store)
    db.commit()
    return db_store
```

```python
# After ✅
@router.post("/stores")
async def create_store(store: StoreCreate, db: Session = Depends(get_db)):
    try:
        return store_service.create_store(db, store)
    except StoreAlreadyExistsError as e:
        raise HTTPException(status_code=409, detail=str(e))
```

### Violation: HTTPException in service

```python
# Before
class StoreService:
    def create_store(self, db: Session, store_data):
        if exists:
            raise HTTPException(status_code=409, detail="Exists")
```

```python
# After ✅
class StoreService:
    def create_store(self, db: Session, store_data):
        if exists:
            raise StoreAlreadyExistsError("Store already exists")
```

---

**Remember:** These patterns are enforced automatically. Run `python scripts/validate/validate_architecture.py` before committing!
