# Architecture Rules Reference

This document provides a comprehensive reference for all architectural rules enforced by the `scripts/validate_architecture.py` validator.

## Overview

The architecture validator ensures consistent patterns, separation of concerns, and best practices across the entire codebase. Rules are organized by category and severity level.

**Version:** 2.0
**Total Rules:** 50+
**Configuration File:** `.architecture-rules.yaml`

## Running the Validator

### Using Make Commands (Recommended)

```bash
# Check all files
make arch-check

# Check a single file
make arch-check-file file="app/api/v1/admin/vendors.py"

# Check all files related to an entity (company, vendor, user, etc.)
make arch-check-object name="company"
make arch-check-object name="vendor"

# Full QA (includes arch-check)
make qa
```

### Using Python Directly

```bash
# Check all files
python scripts/validate_architecture.py

# Check specific directory
python scripts/validate_architecture.py -d app/api/

# Check a single file
python scripts/validate_architecture.py -f app/api/v1/admin/vendors.py

# Check all files for an entity
python scripts/validate_architecture.py -o company
python scripts/validate_architecture.py -o vendor

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

# JSON output (for CI/CD)
python scripts/validate_architecture.py --json
```

### Output Format

The validator displays a summary table for validated files:

```
📋 FILE SUMMARY:
--------------------------------------------------------------------------------
  File                           Status    Errors   Warnings
  -----------------------------  --------  -------  --------
  app/api/v1/admin/companies.py  ❌ FAILED  6        9
  app/services/company_service.py ✅ PASSED  0        0
  models/database/company.py     ✅ PASSED  0        0
--------------------------------------------------------------------------------
  Total: 3 files | ✅ 2 passed | ❌ 1 failed | 6 errors | 9 warnings
```

## Severity Levels

| Severity | Description | Exit Code | Action Required |
|----------|-------------|-----------|-----------------|
| **Error** | Critical architectural violation | 1 | Must fix before committing |
| **Warning** | Pattern deviation | 0 | Should fix, doesn't block |
| **Info** | Suggestion for improvement | 0 | Optional improvement |

---

## Core Architectural Principles

1. **Separation of Concerns** - API endpoints handle HTTP, services handle business logic
2. **Layered Architecture** - Routes → Services → Models
3. **Type Safety** - Pydantic for API, SQLAlchemy for database
4. **Proper Exception Handling** - Domain exceptions in services, HTTPException in routes
5. **Multi-Tenancy** - All queries scoped to vendor_id
6. **Consistent Naming** - API files: plural, Services: singular+service, Models: singular

---

## Backend Rules

### API Endpoint Rules (app/api/v1/**/*.py)

#### API-001: Use Pydantic Models for Request/Response
**Severity:** Error

All API endpoints must use Pydantic models (BaseModel) for request bodies and response models. Never use raw dicts or SQLAlchemy models directly.

```python
# ✅ Good
@router.post("/vendors", response_model=VendorResponse)
async def create_vendor(vendor: VendorCreate):
    return vendor_service.create_vendor(db, vendor)

# ❌ Bad
@router.post("/vendors")
async def create_vendor(data: dict):
    return {"name": data["name"]}  # No validation!
```

#### API-002: No Business Logic in Endpoints
**Severity:** Error

API endpoints should only handle HTTP concerns (validation, auth, response formatting). All business logic must be delegated to service layer.

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

# ❌ Bad
@router.post("/vendors")
async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
    db_vendor = Vendor(name=vendor.name)
    db.add(db_vendor)  # Business logic in endpoint!
    db.commit()
    return db_vendor
```

**Anti-patterns detected:**
- `db.add(`
- `db.commit()`
- `db.query(`

#### API-003: No HTTPException in Endpoints
**Severity:** Error

API endpoints must NOT raise HTTPException directly. Instead, let domain exceptions bubble up to the global exception handler which converts them to appropriate HTTP responses.

```python
# ✅ Good - Let domain exceptions bubble up
@router.post("/vendors")
async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
    # Service raises VendorAlreadyExistsException if duplicate
    # Global handler converts to 409 Conflict
    return vendor_service.create_vendor(db, vendor)

# ❌ Bad - Don't raise HTTPException directly
@router.post("/vendors")
async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
    if vendor_service.exists(db, vendor.subdomain):
        raise HTTPException(status_code=409, detail="Vendor exists")  # BAD!
    return vendor_service.create_vendor(db, vendor)
```

**Pattern:** Services raise domain exceptions → Global handler converts to HTTP responses

#### API-004: Proper Authentication
**Severity:** Warning

Protected endpoints must use Depends() for authentication.

```python
# ✅ Good - Protected endpoint with authentication
@router.post("/vendors")
async def create_vendor(
    vendor: VendorCreate,
    current_user: User = Depends(get_current_admin),
    db: Session = Depends(get_db)
):
    pass
```

**Auto-Excluded Files:**

The validator automatically skips API-004 checks for authentication endpoint files (`*/auth.py`) since login, logout, and registration endpoints are intentionally public.

**Marking Public Endpoints:**

For other intentionally public endpoints (webhooks, health checks, etc.), use a comment marker:

```python
# ✅ Good - Webhook endpoint marked as public
# public - Stripe webhook receives external callbacks
@router.post("/webhook/stripe")
def stripe_webhook(request: Request):
    ...

# ✅ Good - Using noqa style
@router.post("/health")  # noqa: API-004
def health_check():
    ...
```

**Recognized markers:**
- `# public` - descriptive marker for intentionally unauthenticated endpoints
- `# noqa: API-004` - standard noqa style to suppress the warning

#### API-005: Multi-Tenant Scoping
**Severity:** Error

All queries in vendor/shop contexts must filter by vendor_id.

---

### Service Layer Rules (app/services/**/*.py)

#### SVC-001: No HTTPException in Services
**Severity:** Error

Services are business logic layer - they should NOT know about HTTP. Raise domain-specific exceptions instead.

```python
# ✅ Good
class VendorService:
    def create_vendor(self, db: Session, vendor_data):
        if self._vendor_exists(db, vendor_data.subdomain):
            raise VendorAlreadyExistsError(f"Vendor {vendor_data.subdomain} exists")

# ❌ Bad
class VendorService:
    def create_vendor(self, db: Session, vendor_data):
        if self._vendor_exists(db, vendor_data.subdomain):
            raise HTTPException(status_code=409, detail="Vendor exists")  # BAD!
```

#### SVC-002: Use Proper Exception Handling
**Severity:** Error

Services should raise meaningful domain exceptions, not generic Exception.

```python
# ✅ Good
class VendorAlreadyExistsError(Exception):
    pass

def create_vendor(self, db: Session, vendor_data):
    if self._vendor_exists(db, vendor_data.subdomain):
        raise VendorAlreadyExistsError(f"Subdomain {vendor_data.subdomain} taken")

# ❌ Bad
def create_vendor(self, db: Session, vendor_data):
    if self._vendor_exists(db, vendor_data.subdomain):
        raise Exception("Vendor exists")  # Too generic!
```

#### SVC-003: Accept DB Session as Parameter
**Severity:** Error

Service methods should receive database session as a parameter for testability and transaction control.

```python
# ✅ Good
def create_vendor(self, db: Session, vendor_data: VendorCreate):
    vendor = Vendor(**vendor_data.dict())
    db.add(vendor)
    db.commit()
    return vendor

# ❌ Bad
def create_vendor(self, vendor_data: VendorCreate):
    db = SessionLocal()  # Don't create session inside!
    vendor = Vendor(**vendor_data.dict())
    db.add(vendor)
    db.commit()
```

#### SVC-004: Use Pydantic Models for Input
**Severity:** Warning

Service methods should accept Pydantic models for complex inputs to ensure type safety.

#### SVC-005: Scope Queries to vendor_id
**Severity:** Error

All database queries must be scoped to vendor_id to prevent cross-tenant data access.

---

### Model Rules (models/**/*.py)

#### MDL-001: Database Models Use SQLAlchemy Base
**Severity:** Error

All database models must inherit from SQLAlchemy Base.

#### MDL-002: Never Mix SQLAlchemy and Pydantic
**Severity:** Error

```python
# ❌ Bad
class Product(Base, BaseModel):  # NEVER DO THIS!
    pass
```

Keep them separate:
- `models/database/product.py` - SQLAlchemy models
- `models/schema/product.py` - Pydantic models

#### MDL-003: Use from_attributes in Pydantic
**Severity:** Error

Pydantic response models must enable `from_attributes` to work with SQLAlchemy models.

```python
# ✅ Good
class ProductResponse(BaseModel):
    id: int
    name: str

    class Config:
        from_attributes = True
```

#### MDL-004: Use Singular Table Names
**Severity:** Warning

Database table names should be singular lowercase (e.g., 'vendor' not 'vendors').

---

### Exception Handling Rules

#### EXC-001: Define Custom Exceptions
**Severity:** Warning

Create domain-specific exceptions in `app/exceptions/` for better error handling.

```python
# ✅ Good - app/exceptions/vendor.py
class VendorError(Exception):
    """Base exception for vendor-related errors"""
    pass

class VendorNotFoundError(VendorError):
    pass

class VendorAlreadyExistsError(VendorError):
    pass
```

#### EXC-002: Never Use Bare Except
**Severity:** Error

```python
# ❌ Bad
try:
    result = service.do_something()
except:  # Too broad!
    pass

# ✅ Good
try:
    result = service.do_something()
except ValueError as e:
    logger.error(f"Validation error: {e}")
except Exception as e:
    logger.error(f"Unexpected error: {e}")
```

#### EXC-003: Log Exceptions with Context
**Severity:** Warning

When catching exceptions, log them with context and stack trace.

```python
# ✅ Good
try:
    result = service.process()
except Exception as e:
    logger.error(f"Processing failed: {e}", exc_info=True)
```

---

### Naming Convention Rules

#### NAM-001: API Files Use PLURAL Names
**Severity:** Error

```
✅ app/api/v1/admin/vendors.py
✅ app/api/v1/admin/products.py
❌ app/api/v1/admin/vendor.py
```

Exceptions: `__init__.py`, `auth.py`, `health.py`

#### NAM-002: Service Files Use SINGULAR + service
**Severity:** Error

```
✅ app/services/vendor_service.py
✅ app/services/product_service.py
❌ app/services/vendors_service.py
```

#### NAM-003: Model Files Use SINGULAR Names
**Severity:** Error

```
✅ models/database/product.py
✅ models/schema/vendor.py
❌ models/database/products.py
```

#### NAM-004: Use 'vendor' not 'shop'
**Severity:** Warning

Use consistent terminology: 'vendor' for shop owners, 'shop' only for customer-facing frontend.

```python
# ✅ Good
vendor_id
vendor_service

# ❌ Bad
shop_id
shop_service
```

#### NAM-005: Use 'inventory' not 'stock'
**Severity:** Warning

```python
# ✅ Good
inventory_service
inventory_level

# ❌ Bad
stock_service
stock_level
```

---

## Frontend Rules

### JavaScript Rules (static/**/js/**/*.js)

#### JS-001: Use Centralized Logger
**Severity:** Error

Never use console.log, console.error, console.warn directly. Use window.LogConfig.createLogger().

```javascript
// ✅ Good
const pageLog = window.LogConfig.createLogger('dashboard');
pageLog.info('Dashboard loaded');
pageLog.error('Failed to load data', error);

// ❌ Bad
console.log('Dashboard loaded');
console.error('Failed to load data', error);
```

Exceptions: Bootstrap messages with `console.log('✅'` allowed.

#### JS-002: Use Lowercase apiClient
**Severity:** Error

```javascript
// ✅ Good
await apiClient.get('/api/v1/vendors');
await apiClient.post('/api/v1/products', data);

// ❌ Bad
await ApiClient.get('/api/v1/vendors');
await API_CLIENT.post('/api/v1/products', data);
```

#### JS-003: Alpine Components Must Inherit ...data()
**Severity:** Error

All Alpine.js components must inherit base layout data using spread operator.

```javascript
// ✅ Good
function adminDashboard() {
    return {
        ...data(),  // Inherit base layout data
        currentPage: 'dashboard',
        // ... component-specific data
    };
}

// ❌ Bad
function adminDashboard() {
    return {
        currentPage: 'dashboard',
        // Missing ...data()!
    };
}
```

#### JS-004: Set currentPage in Components
**Severity:** Error

All Alpine.js page components must set a currentPage identifier.

```javascript
// ✅ Good
return {
    ...data(),
    currentPage: 'dashboard',  // Required!
    // ...
};
```

#### JS-005: Initialization Guards
**Severity:** Error

Init methods should prevent duplicate initialization.

```javascript
// ✅ Good
init() {
    if (window._pageInitialized) return;
    window._pageInitialized = true;

    // Initialization logic...
}
```

#### JS-006: Error Handling for Async Operations
**Severity:** Error

All API calls and async operations must have error handling.

```javascript
// ✅ Good
async loadData() {
    try {
        const response = await apiClient.get('/api/v1/data');
        this.items = response.data;
    } catch (error) {
        window.LogConfig.logError(error, 'Load Data');
        Utils.showToast('Failed to load data', 'error');
    }
}
```

#### JS-007: Set Loading State
**Severity:** Warning

Loading state should be set before and cleared after async operations.

```javascript
// ✅ Good
async loadData() {
    this.loading = true;
    try {
        const response = await apiClient.get('/api/v1/data');
        this.items = response.data;
    } catch (error) {
        // Error handling
    } finally {
        this.loading = false;
    }
}
```

---

### Template Rules (app/templates/**/*.html)

#### TPL-001: Admin Templates Extend admin/base.html
**Severity:** Error

All admin templates must extend the base template for consistent layout (sidebar, navigation, etc.).

```jinja
{# ✅ Good - Extends base template #}
{% extends "admin/base.html" %}
{% block content %}
    ...
{% endblock %}
```

**Auto-Excluded Files:**

The validator automatically skips TPL-001 checks for:

- `login.html` - Standalone login page (no sidebar/navigation needed)
- `errors/*.html` - Error pages extend `errors/base.html` instead
- `test-*.html` - Test/development templates
- `base.html` - The base template itself
- `partials/*.html` - Partial templates included in other templates

**Marking Standalone Templates:**

For other templates that intentionally don't extend base.html, use a comment marker in the first 5 lines:

```jinja
{# standalone - Minimal monitoring page without admin chrome #}
<!DOCTYPE html>
<html>
...
```

**Recognized markers:**
- `{# standalone #}` - Jinja comment style
- `{# noqa: TPL-001 #}` - Standard noqa style
- `<!-- standalone -->` - HTML comment style

#### TPL-002: Vendor Templates Extend vendor/base.html
**Severity:** Error

```jinja
✅ {% extends "vendor/base.html" %}
```

#### TPL-003: Shop Templates Extend shop/base.html
**Severity:** Error

```jinja
✅ {% extends "shop/base.html" %}
```

#### TPL-004: Use x-text for Dynamic Content
**Severity:** Warning

Use x-text directive for dynamic content to prevent XSS vulnerabilities.

```html
✅ <p x-text="item.name"></p>
❌ <p>{{ item.name }}</p>
```

#### TPL-005: Use x-html ONLY for Safe Content
**Severity:** Error

Use x-html only for trusted content like icons, never for user-generated content.

```html
✅ <span x-html="$icon('home', 'w-5 h-5')"></span>
❌ <div x-html="userComment"></div>  <!-- XSS vulnerability! -->
```

#### TPL-006: Implement Loading State
**Severity:** Warning

All templates that load data should show loading state.

```html
✅ <div x-show="loading">Loading...</div>
```

#### TPL-007: Implement Empty State
**Severity:** Warning

Show empty state when lists have no items.

```html
✅ <template x-if="items.length === 0">
    <p>No items found</p>
</template>
```

---

### Jinja Macro Rules (app/templates/**/*.html)

#### MAC-001: Use Shared Macros for Repeated Patterns
**Severity:** Error

Never copy-paste HTML patterns. Use or create shared macros in `app/templates/shared/macros/`.

```jinja
{# ✅ Good - Using shared macro #}
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
{% call table_wrapper() %}
    {{ table_header(['Name', 'Email', 'Status']) }}
    <tbody>...</tbody>
{% endcall %}

{# ❌ Bad - Copy-pasting table HTML #}
<div class="w-full overflow-hidden rounded-lg shadow-xs">
    <div class="w-full overflow-x-auto">
        <table class="w-full whitespace-no-wrap">
            <thead>...</thead>
            <tbody>...</tbody>
        </table>
    </div>
</div>
```

#### MAC-002: Import Macros at Template Top
**Severity:** Warning

All macro imports should be at the top of the template, after `{% extends %}`.

```jinja
{# ✅ Good #}
{% extends "admin/base.html" %}
{% from 'shared/macros/pagination.html' import pagination %}
{% from 'shared/macros/tables.html' import table_wrapper %}
{% from 'shared/macros/alerts.html' import loading_state %}

{% block content %}
...
{% endblock %}
```

#### MAC-003: Use {% call %} for Wrapper Macros
**Severity:** Error

Wrapper macros that use `{{ caller() }}` must be invoked with `{% call %}`.

```jinja
{# ✅ Good #}
{% call table_wrapper() %}
    {{ table_header(['Col1', 'Col2']) }}
{% endcall %}

{% call tabs_nav() %}
    {{ tab_button('tab1', 'First') }}
{% endcall %}

{# ❌ Bad - Missing call block #}
{{ table_wrapper() }}  {# This won't render inner content! #}
```

#### MAC-004: Document New Macros
**Severity:** Warning

New macros must have JSDoc-style documentation comments.

```jinja
{# ✅ Good #}
{#
  Number Stepper
  ==============
  A number input with +/- buttons.

  Parameters:
    - model: Alpine.js x-model variable (required)
    - min: Minimum value (default: 1)
    - max: Maximum value (optional)

  Usage:
    {{ number_stepper(model='quantity', min=1, max=99) }}
#}
{% macro number_stepper(model, min=1, max=none) %}
...
{% endmacro %}
```

#### MAC-005: Add Components to Reference Page
**Severity:** Warning

New shared components should be added to `/admin/components` page with live demos.

---

### Frontend Component Rules (app/templates/**/*.html)

#### FE-008: Use number_stepper Macro for Quantity Inputs
**Severity:** Warning

Use the shared `number_stepper` macro instead of raw `<input type="number">` for consistent styling and dark mode support.

```jinja
{# ✅ Good - Using number_stepper macro #}
{% from 'shared/macros/inputs.html' import number_stepper %}
{{ number_stepper(model='quantity', min=1, max=99) }}

{# ❌ Bad - Raw number input #}
<input type="number" x-model="quantity" min="1" max="99" />
```

**Suppress with `noqa` for ID fields:**
```jinja
{# noqa: FE-008 - User ID is typed directly, not incremented #}
<input type="number" x-model="userId" placeholder="User ID" />
```

---

### Styling Rules (app/templates/**/*.html)

#### CSS-001: Use Tailwind Utility Classes
**Severity:** Warning

Prefer Tailwind utility classes over custom CSS.

#### CSS-002: Support Dark Mode
**Severity:** Warning

All color classes should include dark mode variants.

```html
✅ class="bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
❌ class="bg-white text-gray-900"
```

#### CSS-003: Shop Templates Use CSS Variables
**Severity:** Error

Shop templates must use CSS variables for vendor-specific theming.

```html
✅ <button style="background-color: var(--color-primary)">Buy Now</button>
❌ <button class="bg-blue-600">Buy Now</button>
```

#### CSS-004: Mobile-First Responsive Design
**Severity:** Warning

Use mobile-first responsive classes.

```html
✅ class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
❌ class="grid grid-cols-4"
```

---

## Security & Multi-Tenancy Rules

### Multi-Tenancy Rules

#### MT-001: Scope All Queries to vendor_id
**Severity:** Error

In vendor/shop contexts, all database queries must filter by vendor_id.

```python
# ✅ Good
def get_products(self, db: Session, vendor_id: int):
    return db.query(Product).filter(Product.vendor_id == vendor_id).all()

# ❌ Bad
def get_products(self, db: Session):
    return db.query(Product).all()  # Cross-tenant data leak!
```

#### MT-002: No Cross-Vendor Data Access
**Severity:** Error

Queries must never access data from other vendors.

---

### Authentication & Authorization Rules

#### AUTH-001: Use JWT Tokens
**Severity:** Error

Authentication must use JWT tokens in Authorization: Bearer header.

#### AUTH-002: Role-Based Access Control
**Severity:** Error

Use Depends(get_current_admin/vendor/customer) for role checks.

```python
# ✅ Good
@router.get("/admin/users")
async def list_users(current_user: User = Depends(get_current_admin)):
    pass
```

#### AUTH-003: Never Store Plain Passwords
**Severity:** Error

Always hash passwords with bcrypt before storing.

---

## Middleware Rules

#### MDW-001: Middleware Naming
**Severity:** Warning

Middleware files should be named with simple nouns (auth.py, not auth_middleware.py).

```
✅ middleware/auth.py
✅ middleware/context.py
❌ middleware/auth_middleware.py
```

#### MDW-002: Vendor Context Injection
**Severity:** Error

Vendor context middleware must set `request.state.vendor_id` and `request.state.vendor`.

---

## Code Quality Rules

#### QUAL-001: Format with Ruff
**Severity:** Error

All code must be formatted with Ruff before committing.

```bash
make format
```

#### QUAL-002: Pass Ruff Linting
**Severity:** Error

All code must pass Ruff linting before committing.

```bash
make lint
```

#### QUAL-003: Use Type Hints
**Severity:** Warning

Add type hints to function parameters and return types.

```python
# ✅ Good
def create_product(self, db: Session, data: ProductCreate) -> Product:
    pass

# Better
from typing import Optional

def get_product(self, product_id: int) -> Optional[Product]:
    pass
```

---

## Ignored Patterns

The validator ignores these files/patterns:

- Test files: `**/*_test.py`, `**/test_*.py`
- Cache: `**/__pycache__/**`
- Migrations: `**/alembic/versions/**`
- Dependencies: `**/node_modules/**`, `**/.venv/**`, `**/venv/**`
- Build artifacts: `**/build/**`, `**/dist/**`

Special exceptions:
- `app/core/exceptions.py` - Allowed to use HTTPException
- `app/exceptions/handler.py` - Converts to HTTPException

---

## Quick Reference

### Pre-Commit Checklist

Before committing code:

- [ ] Run `make format` (Ruff formatting)
- [ ] Run `make lint` (Ruff + mypy)
- [ ] Run `python scripts/validate_architecture.py`
- [ ] Fix all **Error** level violations
- [ ] Review **Warning** level violations
- [ ] Run relevant tests

### Common Violations and Fixes

| Violation | Quick Fix |
|-----------|-----------|
| HTTPException in service | Create custom exception in `app/exceptions/` |
| HTTPException in endpoint | Let domain exceptions bubble up to global handler |
| Business logic in endpoint | Move to service layer |
| console.log in JS | Use `window.LogConfig.createLogger()` |
| Missing ...data() | Add spread operator in component return |
| Bare except clause | Specify exception type |
| Raw dict return | Create Pydantic response model |
| Template not extending base | Add `{% extends %}` or `{# standalone #}` marker |

---

## Configuration

All rules are defined in `.architecture-rules.yaml`. To modify rules:

1. Edit `.architecture-rules.yaml`
2. Update `scripts/validate_architecture.py` if implementing new checks
3. Run validator to test changes
4. Update this documentation

---

## Related Documentation

- [Code Quality Guide](code-quality.md)
- [Contributing Guide](contributing.md)
- [Architecture Overview](../architecture/overview.md)
- [Backend Development](../backend/overview.md)
- [Frontend Development](../frontend/overview.md)

---

## Summary Statistics

| Category | Rules | Errors | Warnings |
|----------|-------|--------|----------|
| Backend | 20 | 15 | 5 |
| Frontend JS | 7 | 6 | 1 |
| Frontend Templates | 7 | 3 | 4 |
| Frontend Macros | 5 | 2 | 3 |
| Frontend Components | 1 | 0 | 1 |
| Frontend Styling | 4 | 1 | 3 |
| Naming | 5 | 3 | 2 |
| Security | 5 | 5 | 0 |
| Quality | 3 | 2 | 1 |
| **Total** | **57** | **37** | **20** |

---

**Last Updated:** 2025-12-07
**Version:** 2.3
