Files
orion/docs/architecture/architecture-patterns.md
Samir Boulahtit cc74970223 feat: add logging, marketplace, and admin enhancements
Database & Migrations:
- Add application_logs table migration for hybrid cloud logging
- Add companies table migration and restructure vendor relationships

Logging System:
- Implement hybrid logging system (database + file)
- Add log_service for centralized log management
- Create admin logs page with filtering and viewing capabilities
- Add init_log_settings.py script for log configuration
- Enhance core logging with database integration

Marketplace Integration:
- Add marketplace admin page with product management
- Create marketplace vendor page with product listings
- Implement marketplace.js for both admin and vendor interfaces
- Add marketplace integration documentation

Admin Enhancements:
- Add imports management page and functionality
- Create settings page for admin configuration
- Add vendor themes management page
- Enhance vendor detail and edit pages
- Improve code quality dashboard and violation details
- Add logs viewing and management
- Update icons guide and shared icon system

Architecture & Documentation:
- Document frontend structure and component architecture
- Document models structure and relationships
- Add vendor-in-token architecture documentation
- Add vendor RBAC (role-based access control) documentation
- Document marketplace integration patterns
- Update architecture patterns documentation

Infrastructure:
- Add platform static files structure (css, img, js)
- Move architecture_scan.py to proper models location
- Update model imports and registrations
- Enhance exception handling
- Update dependency injection patterns

UI/UX:
- Improve vendor edit interface
- Update admin user interface
- Enhance page templates documentation
- Add vendor marketplace interface
2025-12-01 21:51:07 +01:00

21 KiB

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
  2. Layered Architecture
  3. API Endpoint Patterns
  4. Service Layer Patterns
  5. Model Patterns
  6. Exception Handling
  7. JavaScript Patterns
  8. 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:

@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:

@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.

# ✅ 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
# ❌ 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.

# ✅ 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
# ❌ 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.

# ✅ 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.

# ✅ 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

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

@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:

{
  "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.

# ✅ 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
# ❌ 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.

# ✅ 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
# ❌ 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.

# ✅ 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
# ❌ 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.

# ✅ 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

# ✅ 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.

# ❌ BAD: Mixing SQLAlchemy and Pydantic
class Vendor(Base, BaseModel):  # ❌ Don't do this!
    __tablename__ = "vendors"
    name: str = Column(String(200))
# ✅ 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/

# 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

# ❌ BAD: Bare except
try:
    result = do_something()
except:  # ❌ Catches EVERYTHING including KeyboardInterrupt!
    pass
# ✅ 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

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

Rule JS-002: Use Centralized Logger

// ✅ GOOD: Centralized logger
const vendorLog = window.LogConfig.createLogger('vendors');
vendorLog.info('Loading vendors...');
vendorLog.error('Failed to load vendors:', error);
// ❌ BAD: console
console.log('Loading vendors...');  // ❌ Use logger instead

Rule JS-003: Alpine Components Pattern

// ✅ 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

# 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:

# 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:

# .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

# 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
# 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

# Before
class VendorService:
    def create_vendor(self, db: Session, vendor_data):
        if exists:
            raise HTTPException(status_code=409, detail="Exists")
# 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!