# 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_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("/vendors")
async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
    # ❌ BAD: Business logic in endpoint
    if db.query(Vendor).filter(Vendor.subdomain == vendor.subdomain).first():
        raise HTTPException(status_code=409, detail="Vendor exists")

    db_vendor = Vendor(**vendor.dict())
    db.add(db_vendor)
    db.commit()
    db.refresh(db_vendor)
    return db_vendor
```

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

```python
@router.post("/vendors", response_model=VendorResponse)
async def create_vendor(
    vendor: VendorCreate,
    current_user: User = Depends(get_current_admin),
    db: Session = Depends(get_db)
):
    try:
        # ✅ GOOD: Delegate to service
        result = vendor_service.create_vendor(db, vendor)
        return result
    except VendorAlreadyExistsError as e:
        raise HTTPException(status_code=409, detail=str(e))
    except Exception as e:
        logger.error(f"Failed to create vendor: {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 VendorCreate(BaseModel):
    name: str = Field(..., max_length=200)
    subdomain: str = Field(..., max_length=100)
    is_active: bool = True

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

    class Config:
        from_attributes = True  # For SQLAlchemy compatibility

@router.post("/vendors", response_model=VendorResponse)
async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
    result = vendor_service.create_vendor(db, vendor)
    return result
```

```python
# ❌ BAD: Raw dict, no validation
@router.post("/vendors")
async def create_vendor(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("/vendors")
async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
    result = vendor_service.create_vendor(db, vendor)
    return result
```

```python
# ❌ BAD: Business logic in endpoint
@router.post("/vendors")
async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
    # ❌ Database operations belong in service!
    db_vendor = Vendor(**vendor.dict())
    db.add(db_vendor)
    db.commit()
    return db_vendor
```

### Rule API-003: Proper Exception Handling

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

```python
# ✅ GOOD: Proper exception handling
@router.post("/vendors", response_model=VendorResponse)
async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
    try:
        result = vendor_service.create_vendor(db, vendor)
        return result
    except VendorAlreadyExistsError 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 vendor: {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("/vendors")
async def create_vendor(
    vendor: VendorCreate,
    current_user: User = Depends(get_current_admin),  # ✅ Auth required
    db: Session = Depends(get_db)
):
    result = vendor_service.create_vendor(db, vendor)
    return result
```

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

**Vendor API endpoints MUST extract vendor context from JWT token, NOT from URL.**

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

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

```python
from middleware.vendor_context import require_vendor_context

@router.get("/products")
def get_products(
    vendor: Vendor = Depends(require_vendor_context()),  # ❌ Requires vendor in URL
    current_user: User = Depends(get_current_vendor_api),
    db: Session = Depends(get_db),
):
    # This fails on /api/v1/vendor/products (no vendor in URL)
    products = product_service.get_vendor_products(db, vendor.id)
    return products
```

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

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

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

    vendor_id = current_user.token_vendor_id

    # Use vendor_id from token
    products = product_service.get_vendor_products(db, vendor_id)
    return products
```

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

**Token structure:**
```json
{
  "sub": "user_id",
  "username": "john.doe",
  "vendor_id": 123,           ← Vendor context
  "vendor_code": "WIZAMART",  ← Vendor code
  "vendor_role": "Owner"      ← Vendor role
}
```

**Available token attributes:**
- `current_user.token_vendor_id` - Vendor ID (use for database queries)
- `current_user.token_vendor_code` - Vendor code (use for logging)
- `current_user.token_vendor_role` - Vendor role (Owner, Manager, etc.)

**Migration checklist:**
1. Remove `vendor: Vendor = Depends(require_vendor_context())`
2. Remove unused imports: `from middleware.vendor_context import require_vendor_context`
3. Extract vendor from token: `vendor_id = current_user.token_vendor_id`
4. Add token validation check (see example above)
5. Update logging to use `current_user.token_vendor_code`

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

**Files requiring migration:**
- `app/api/v1/vendor/customers.py`
- `app/api/v1/vendor/notifications.py`
- `app/api/v1/vendor/media.py`
- `app/api/v1/vendor/marketplace.py`
- `app/api/v1/vendor/inventory.py`
- `app/api/v1/vendor/settings.py`
- `app/api/v1/vendor/analytics.py`
- `app/api/v1/vendor/payments.py`
- `app/api/v1/vendor/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 VendorAlreadyExistsError(Exception):
    """Raised when vendor with same subdomain already exists"""
    pass

class VendorService:
    def create_vendor(self, db: Session, vendor_data: VendorCreate):
        if self._vendor_exists(db, vendor_data.subdomain):
            raise VendorAlreadyExistsError(
                f"Vendor with subdomain '{vendor_data.subdomain}' already exists"
            )

        # Business logic...
        vendor = Vendor(**vendor_data.dict())
        db.add(vendor)
        db.commit()
        return vendor
```

```python
# ❌ BAD: HTTPException in service
class VendorService:
    def create_vendor(self, db: Session, vendor_data: VendorCreate):
        if self._vendor_exists(db, vendor_data.subdomain):
            # ❌ Service shouldn't know about HTTP!
            raise HTTPException(status_code=409, detail="Vendor exists")
```

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

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

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

class VendorNotFoundError(VendorError):
    """Raised when vendor is not found"""
    pass

class VendorAlreadyExistsError(VendorError):
    """Raised when vendor already exists"""
    pass

class VendorService:
    def get_vendor(self, db: Session, vendor_code: str):
        vendor = db.query(Vendor).filter(Vendor.vendor_code == vendor_code).first()
        if not vendor:
            raise VendorNotFoundError(f"Vendor '{vendor_code}' not found")
        return vendor
```

```python
# ❌ BAD: Generic Exception
class VendorService:
    def get_vendor(self, db: Session, vendor_code: str):
        vendor = db.query(Vendor).filter(Vendor.vendor_code == vendor_code).first()
        if not vendor:
            raise Exception("Vendor not found")  # ❌ Too generic!
        return vendor
```

### 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 VendorService:
    def create_vendor(self, db: Session, vendor_data: VendorCreate):
        vendor = Vendor(**vendor_data.dict())
        db.add(vendor)
        db.commit()
        db.refresh(vendor)
        return vendor

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

```python
# ❌ BAD: Creating session internally
class VendorService:
    def create_vendor(self, vendor_data: VendorCreate):
        # ❌ Don't create session here - makes testing hard
        db = SessionLocal()
        vendor = Vendor(**vendor_data.dict())
        db.add(vendor)
        db.commit()
        return vendor
```

**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 VendorService:
    def create_vendor(self, db: Session, vendor_data: VendorCreate):
        # vendor_data is already validated by Pydantic
        vendor = Vendor(**vendor_data.dict())
        db.add(vendor)
        db.commit()
        return vendor
```

---

## 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 Vendor(Base):
    __tablename__ = "vendors"

    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 Vendor(Base, BaseModel):  # ❌ Don't do this!
    __tablename__ = "vendors"
    name: str = Column(String(200))
```

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

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

class VendorResponse(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/vendor_exceptions.py

class VendorError(Exception):
    """Base exception for vendor domain"""
    pass

class VendorNotFoundError(VendorError):
    """Vendor does not exist"""
    pass

class VendorAlreadyExistsError(VendorError):
    """Vendor already exists"""
    pass

class VendorValidationError(VendorError):
    """Vendor 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 vendors = await apiClient.get('/api/v1/vendors');
```

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

### Rule JS-002: Use Centralized Logger

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

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

### Rule JS-003: Alpine Components Pattern

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

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

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

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

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

---

## Validation

### Running the Validator

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

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

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

# Errors only (suppress warnings)
python scripts/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_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("/vendors")
async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
    db_vendor = Vendor(**vendor.dict())
    db.add(db_vendor)
    db.commit()
    return db_vendor
```

```python
# After ✅
@router.post("/vendors")
async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
    try:
        return vendor_service.create_vendor(db, vendor)
    except VendorAlreadyExistsError as e:
        raise HTTPException(status_code=409, detail=str(e))
```

### Violation: HTTPException in service

```python
# Before
class VendorService:
    def create_vendor(self, db: Session, vendor_data):
        if exists:
            raise HTTPException(status_code=409, detail="Exists")
```

```python
# After ✅
class VendorService:
    def create_vendor(self, db: Session, vendor_data):
        if exists:
            raise VendorAlreadyExistsError("Vendor already exists")
```

---

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