feat: add module definition completeness validation and permissions

Add new validation rules MOD-020 to MOD-023 for module definition
completeness and standardize permissions across all modules.

Changes:
- Add MOD-020: Module definitions must have required attributes
- Add MOD-021: Modules with menus should have features
- Add MOD-022: Feature modules should have permissions
- Add MOD-023: Modules with routers should use get_*_with_routers pattern

Module permissions added:
- analytics: view, export, manage_dashboards
- billing: view_tiers, manage_tiers, view_subscriptions, manage_subscriptions, view_invoices
- cart: view, manage
- checkout: view_settings, manage_settings
- cms: view_pages, manage_pages, view_media, manage_media, manage_themes
- loyalty: view_programs, manage_programs, view_rewards, manage_rewards
- marketplace: view_integration, manage_integration, sync_products
- messaging: view_messages, send_messages, manage_templates
- payments: view_gateways, manage_gateways, view_transactions

Module improvements:
- Complete cart module with features and permissions
- Complete checkout module with features and permissions
- Add features to catalog module
- Add version to cms module
- Fix loyalty platform_router attachment
- Add path definitions to payments module
- Remove empty scheduled_tasks from dev_tools module

Documentation:
- Update module-system.md with new validation rules
- Update architecture-rules.md with MOD-020 to MOD-023

Tests:
- Add unit tests for module definition completeness
- Add tests for permission structure validation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-02 18:23:04 +01:00
parent 30a5c75e74
commit 967f08e4ba
50 changed files with 1014 additions and 66 deletions

View File

@@ -551,3 +551,175 @@ module_rules:
- "__init__.py"
- "base.py"
- "auth.py"
# =========================================================================
# Module Definition Completeness Rules
# =========================================================================
- id: "MOD-020"
name: "Module definition must have required attributes"
severity: "warning"
description: |
Module definitions should include at minimum:
- code: Module identifier
- name: Human-readable name
- description: What the module does
- version: Semantic version
- features: List of features (unless infrastructure module)
- permissions: Access control definitions (unless internal or storefront-only)
EXAMPLES (incomplete):
module = ModuleDefinition(
code="cart",
name="Shopping Cart",
description="...",
version="1.0.0",
# Missing features and permissions
)
EXAMPLES (complete):
module = ModuleDefinition(
code="orders",
name="Order Management",
description="...",
version="1.0.0",
features=["order_management", "fulfillment_tracking"],
permissions=[
PermissionDefinition(id="orders.view", ...),
],
)
EXCEPTIONS:
- is_internal=True modules may skip permissions
- Infrastructure modules (is_core=True with no UI) may skip features
- Storefront-only modules (session-based, no admin UI) may have minimal permissions
WHY THIS MATTERS:
- Consistency: All modules follow the same definition pattern
- RBAC: Permissions enable proper role-based access control
- Feature flags: Features enable selective module functionality
pattern:
file_pattern: "app/modules/*/definition.py"
required_attributes:
- "code"
- "name"
- "description"
- "version"
- id: "MOD-021"
name: "Modules with menus should have features"
severity: "warning"
description: |
If a module defines menu items or menu sections, it should also
define features to describe what functionality it provides.
Menus indicate the module has UI and user-facing functionality,
which should be documented as features.
WRONG:
module = ModuleDefinition(
code="billing",
menus={
FrontendType.ADMIN: [...],
},
# Missing features list
)
RIGHT:
module = ModuleDefinition(
code="billing",
features=[
"subscription_management",
"billing_history",
"invoice_generation",
],
menus={
FrontendType.ADMIN: [...],
},
)
WHY THIS MATTERS:
- Documentation: Features describe what the module does
- Feature flags: Enables/disables specific functionality
- Consistency: All UI modules describe their capabilities
pattern:
file_pattern: "app/modules/*/definition.py"
validates:
- "menus -> features"
- id: "MOD-022"
name: "Feature modules should have permissions"
severity: "info"
description: |
Modules with features should define permissions unless:
- is_internal=True (internal tools like dev_tools)
- Storefront-only module (session-based, no authentication)
Permissions enable role-based access control (RBAC) for module
functionality.
EXCEPTIONS:
- is_internal=True modules (internal tooling)
- Modules with only storefront features (cart, checkout without admin UI)
- Infrastructure modules (contracts, core utilities)
EXAMPLE:
module = ModuleDefinition(
code="billing",
features=["subscription_management", ...],
permissions=[
PermissionDefinition(
id="billing.view_subscriptions",
label_key="billing.permissions.view_subscriptions",
description_key="billing.permissions.view_subscriptions_desc",
category="billing",
),
],
)
WHY THIS MATTERS:
- RBAC: Permissions enable proper access control
- Security: Restrict who can access module features
- Consistency: All feature modules define their access rules
pattern:
file_pattern: "app/modules/*/definition.py"
validates:
- "features -> permissions"
exceptions:
- "is_internal=True"
- id: "MOD-023"
name: "Modules with routers should use get_*_with_routers pattern"
severity: "info"
description: |
Modules that define routers (admin_router, vendor_router, etc.)
should follow the lazy import pattern with a dedicated function:
def get_{module}_module_with_routers() -> ModuleDefinition:
This pattern:
1. Avoids circular imports during module initialization
2. Ensures routers are attached at the right time
3. Provides a consistent API for router registration
WRONG:
# Direct router assignment at module level
module.admin_router = admin_router
RIGHT:
def _get_admin_router():
from app.modules.orders.routes.admin import admin_router
return admin_router
def get_orders_module_with_routers() -> ModuleDefinition:
orders_module.admin_router = _get_admin_router()
return orders_module
WHY THIS MATTERS:
- Prevents circular imports
- Consistent pattern across all modules
- Clear API for module registration
pattern:
file_pattern: "app/modules/*/definition.py"
validates:
- "router imports -> get_*_with_routers function"

View File

@@ -6,7 +6,7 @@ Defines the analytics module including its features, menu items,
route configurations, and self-contained module settings.
"""
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition
from app.modules.enums import FrontendType
@@ -37,6 +37,27 @@ analytics_module = ModuleDefinition(
"export_reports", # Export to CSV/Excel
"usage_metrics", # Usage and performance metrics
],
# Module-driven permissions
permissions=[
PermissionDefinition(
id="analytics.view",
label_key="analytics.permissions.view",
description_key="analytics.permissions.view_desc",
category="analytics",
),
PermissionDefinition(
id="analytics.export",
label_key="analytics.permissions.export",
description_key="analytics.permissions.export_desc",
category="analytics",
),
PermissionDefinition(
id="analytics.manage_dashboards",
label_key="analytics.permissions.manage_dashboards",
description_key="analytics.permissions.manage_dashboards_desc",
category="analytics",
),
],
menu_items={
FrontendType.ADMIN: [
# Analytics appears in dashboard for admin

View File

@@ -6,7 +6,7 @@ Defines the billing module including its features, menu items,
route configurations, and scheduled tasks.
"""
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, ScheduledTask
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition, ScheduledTask
from app.modules.enums import FrontendType
@@ -42,6 +42,39 @@ billing_module = ModuleDefinition(
"trial_management", # Manage vendor trial periods
"limit_overrides", # Override tier limits per vendor
],
# Module-driven permissions
permissions=[
PermissionDefinition(
id="billing.view_tiers",
label_key="billing.permissions.view_tiers",
description_key="billing.permissions.view_tiers_desc",
category="billing",
),
PermissionDefinition(
id="billing.manage_tiers",
label_key="billing.permissions.manage_tiers",
description_key="billing.permissions.manage_tiers_desc",
category="billing",
),
PermissionDefinition(
id="billing.view_subscriptions",
label_key="billing.permissions.view_subscriptions",
description_key="billing.permissions.view_subscriptions_desc",
category="billing",
),
PermissionDefinition(
id="billing.manage_subscriptions",
label_key="billing.permissions.manage_subscriptions",
description_key="billing.permissions.manage_subscriptions_desc",
category="billing",
),
PermissionDefinition(
id="billing.view_invoices",
label_key="billing.permissions.view_invoices",
description_key="billing.permissions.view_invoices_desc",
category="billing",
),
],
menu_items={
FrontendType.ADMIN: [
"subscription-tiers", # Manage tier definitions

View File

@@ -6,7 +6,8 @@ This module provides shopping cart functionality for customer storefronts.
It is session-based and does not require customer authentication.
"""
from app.modules.base import ModuleDefinition
from app.modules.base import ModuleDefinition, PermissionDefinition
module = ModuleDefinition(
code="cart",
@@ -15,4 +16,29 @@ module = ModuleDefinition(
version="1.0.0",
is_self_contained=True,
requires=["inventory"], # Checks inventory availability
features=[
"cart_management", # Basic cart CRUD operations
"cart_persistence", # Session and database persistence
"cart_item_operations", # Add, update, remove items
"shipping_calculation", # Calculate shipping for cart
"promotion_application", # Apply discounts and promotions
],
# Note: Cart is primarily session-based storefront functionality.
# These permissions are for admin access to cart data/settings.
permissions=[
PermissionDefinition(
id="cart.view",
label_key="cart.permissions.view",
description_key="cart.permissions.view_desc",
category="cart",
),
PermissionDefinition(
id="cart.manage",
label_key="cart.permissions.manage",
description_key="cart.permissions.manage_desc",
category="cart",
),
],
# Cart is storefront-only - no admin/vendor menus needed
menu_items={},
)

View File

@@ -16,6 +16,14 @@ module = ModuleDefinition(
version="1.0.0",
is_self_contained=True,
requires=["inventory"],
features=[
"product_catalog", # Core product catalog functionality
"product_search", # Search and filtering
"product_variants", # Product variants management
"product_categories", # Category organization
"product_attributes", # Custom attributes
"product_import_export", # Bulk import/export
],
# Module-driven permissions
permissions=[
PermissionDefinition(

View File

@@ -1,7 +1,13 @@
# app/modules/checkout/definition.py
"""Checkout module definition."""
"""
Checkout module definition.
This module handles the checkout flow, converting cart contents into orders.
Orchestrates payment processing and order creation.
"""
from app.modules.base import ModuleDefinition, PermissionDefinition
from app.modules.base import ModuleDefinition
module = ModuleDefinition(
code="checkout",
@@ -10,4 +16,29 @@ module = ModuleDefinition(
version="1.0.0",
is_self_contained=True,
requires=["cart", "orders", "payments", "customers"],
features=[
"checkout_flow", # Multi-step checkout process
"order_creation", # Create orders from cart
"payment_processing", # Payment integration during checkout
"checkout_validation", # Address, inventory, payment validation
"guest_checkout", # Allow checkout without account
],
# Note: Checkout is primarily storefront functionality.
# These permissions are for admin access to checkout settings.
permissions=[
PermissionDefinition(
id="checkout.view_settings",
label_key="checkout.permissions.view_settings",
description_key="checkout.permissions.view_settings_desc",
category="checkout",
),
PermissionDefinition(
id="checkout.manage_settings",
label_key="checkout.permissions.manage_settings",
description_key="checkout.permissions.manage_settings_desc",
category="checkout",
),
],
# Checkout is storefront-only - no admin/vendor menus needed
menu_items={},
)

View File

@@ -12,7 +12,7 @@ This is a self-contained module with:
- Templates: app.modules.cms.templates (namespaced as cms/)
"""
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition
from app.modules.enums import FrontendType
@@ -35,6 +35,7 @@ cms_module = ModuleDefinition(
code="cms",
name="Content Management",
description="Content pages, media library, and vendor themes.",
version="1.0.0",
features=[
"cms_basic", # Basic page editing
"cms_custom_pages", # Custom page creation
@@ -43,6 +44,39 @@ cms_module = ModuleDefinition(
"cms_seo", # SEO tools
"media_library", # Media file management
],
# Module-driven permissions
permissions=[
PermissionDefinition(
id="cms.view_pages",
label_key="cms.permissions.view_pages",
description_key="cms.permissions.view_pages_desc",
category="cms",
),
PermissionDefinition(
id="cms.manage_pages",
label_key="cms.permissions.manage_pages",
description_key="cms.permissions.manage_pages_desc",
category="cms",
),
PermissionDefinition(
id="cms.view_media",
label_key="cms.permissions.view_media",
description_key="cms.permissions.view_media_desc",
category="cms",
),
PermissionDefinition(
id="cms.manage_media",
label_key="cms.permissions.manage_media",
description_key="cms.permissions.manage_media_desc",
category="cms",
),
PermissionDefinition(
id="cms.manage_themes",
label_key="cms.permissions.manage_themes",
description_key="cms.permissions.manage_themes_desc",
category="cms",
),
],
menu_items={
FrontendType.ADMIN: [
"content-pages", # Platform content pages

View File

@@ -85,20 +85,8 @@ dev_tools_module = ModuleDefinition(
schemas_path="app.modules.dev_tools.schemas",
exceptions_path="app.modules.dev_tools.exceptions",
tasks_path="app.modules.dev_tools.tasks",
# =========================================================================
# Scheduled Tasks
# =========================================================================
# Note: Code quality and test tasks are on-demand, not scheduled.
# If scheduled scans are desired, they can be added here:
# scheduled_tasks=[
# ScheduledTask(
# name="dev_tools.nightly_code_scan",
# task="app.modules.dev_tools.tasks.code_quality.execute_code_quality_scan",
# schedule="0 2 * * *", # Daily at 02:00
# options={"queue": "long_running"},
# ),
# ],
scheduled_tasks=[],
# If scheduled scans are desired, add ScheduledTask entries here.
)

View File

@@ -6,7 +6,7 @@ Defines the loyalty module including its features, menu items,
route configurations, and scheduled tasks.
"""
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, ScheduledTask
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition, ScheduledTask
from app.modules.enums import FrontendType
@@ -24,11 +24,11 @@ def _get_vendor_router():
return vendor_router
def _get_public_router():
"""Lazy import of public router to avoid circular imports."""
from app.modules.loyalty.routes.api.public import public_router
def _get_platform_router():
"""Lazy import of platform router to avoid circular imports."""
from app.modules.loyalty.routes.api.platform import platform_router
return public_router
return platform_router
# Loyalty module definition
@@ -60,6 +60,33 @@ loyalty_module = ModuleDefinition(
"loyalty_stats", # Dashboard statistics
"loyalty_reports", # Transaction reports
],
# Module-driven permissions
permissions=[
PermissionDefinition(
id="loyalty.view_programs",
label_key="loyalty.permissions.view_programs",
description_key="loyalty.permissions.view_programs_desc",
category="loyalty",
),
PermissionDefinition(
id="loyalty.manage_programs",
label_key="loyalty.permissions.manage_programs",
description_key="loyalty.permissions.manage_programs_desc",
category="loyalty",
),
PermissionDefinition(
id="loyalty.view_rewards",
label_key="loyalty.permissions.view_rewards",
description_key="loyalty.permissions.view_rewards_desc",
category="loyalty",
),
PermissionDefinition(
id="loyalty.manage_rewards",
label_key="loyalty.permissions.manage_rewards",
description_key="loyalty.permissions.manage_rewards_desc",
category="loyalty",
),
],
menu_items={
FrontendType.ADMIN: [
"loyalty-programs", # View all programs
@@ -168,8 +195,7 @@ def get_loyalty_module_with_routers() -> ModuleDefinition:
"""
loyalty_module.admin_router = _get_admin_router()
loyalty_module.vendor_router = _get_vendor_router()
# Note: public_router needs to be attached separately in main.py
# as it doesn't require authentication
loyalty_module.platform_router = _get_platform_router()
return loyalty_module

View File

@@ -8,7 +8,7 @@ dependencies, route configurations, and scheduled tasks.
Note: This module requires the inventory module to be enabled.
"""
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, ScheduledTask
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition, ScheduledTask
from app.modules.enums import FrontendType
@@ -43,6 +43,27 @@ marketplace_module = ModuleDefinition(
"order_import", # Import orders from marketplace
"marketplace_analytics", # Marketplace performance metrics
],
# Module-driven permissions
permissions=[
PermissionDefinition(
id="marketplace.view_integration",
label_key="marketplace.permissions.view_integration",
description_key="marketplace.permissions.view_integration_desc",
category="marketplace",
),
PermissionDefinition(
id="marketplace.manage_integration",
label_key="marketplace.permissions.manage_integration",
description_key="marketplace.permissions.manage_integration_desc",
category="marketplace",
),
PermissionDefinition(
id="marketplace.sync_products",
label_key="marketplace.permissions.sync_products",
description_key="marketplace.permissions.sync_products_desc",
category="marketplace",
),
],
menu_items={
FrontendType.ADMIN: [
"marketplace-letzshop", # Marketplace monitoring

View File

@@ -6,7 +6,7 @@ Defines the messaging module including its features, menu items,
route configurations, and self-contained module settings.
"""
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition
from app.modules.enums import FrontendType
@@ -37,6 +37,27 @@ messaging_module = ModuleDefinition(
"message_attachments", # File attachments
"admin_notifications", # System admin notifications
],
# Module-driven permissions
permissions=[
PermissionDefinition(
id="messaging.view_messages",
label_key="messaging.permissions.view_messages",
description_key="messaging.permissions.view_messages_desc",
category="messaging",
),
PermissionDefinition(
id="messaging.send_messages",
label_key="messaging.permissions.send_messages",
description_key="messaging.permissions.send_messages_desc",
category="messaging",
),
PermissionDefinition(
id="messaging.manage_templates",
label_key="messaging.permissions.manage_templates",
description_key="messaging.permissions.manage_templates_desc",
category="messaging",
),
],
menu_items={
FrontendType.ADMIN: [
"messages", # Admin messages

View File

@@ -15,7 +15,7 @@ This separation allows:
3. Orders without billing (customer payments only)
"""
from app.modules.base import ModuleDefinition
from app.modules.base import ModuleDefinition, PermissionDefinition
from app.modules.enums import FrontendType
@@ -51,6 +51,27 @@ payments_module = ModuleDefinition(
"bank_transfer", # Bank transfer support
"transaction_history", # Transaction records
],
# Module-driven permissions
permissions=[
PermissionDefinition(
id="payments.view_gateways",
label_key="payments.permissions.view_gateways",
description_key="payments.permissions.view_gateways_desc",
category="payments",
),
PermissionDefinition(
id="payments.manage_gateways",
label_key="payments.permissions.manage_gateways",
description_key="payments.permissions.manage_gateways_desc",
category="payments",
),
PermissionDefinition(
id="payments.view_transactions",
label_key="payments.permissions.view_transactions",
description_key="payments.permissions.view_transactions_desc",
category="payments",
),
],
menu_items={
FrontendType.ADMIN: [
"payment-gateways", # Configure payment gateways
@@ -61,7 +82,14 @@ payments_module = ModuleDefinition(
},
is_core=False,
is_internal=False,
is_self_contained=True, # Enable auto-discovery from routes/api/
# =========================================================================
# Self-Contained Module Configuration
# =========================================================================
is_self_contained=True,
services_path="app.modules.payments.services",
models_path="app.modules.payments.models",
schemas_path="app.modules.payments.schemas",
exceptions_path="app.modules.payments.exceptions",
)

View File

@@ -72,33 +72,35 @@ touch app/modules/mymodule/exceptions.py
## Three-Tier Classification
### Core Modules (4)
### Core Modules (5)
Core modules are **always enabled** and cannot be disabled. They provide fundamental platform functionality.
| Module | Description | Key Features |
|--------|-------------|--------------|
| `core` | Dashboard, settings, profile | Basic platform operation |
| `tenancy` | Platform, company, vendor, admin user management | Multi-tenant infrastructure |
| `cms` | Content pages, media library, themes | Content management |
| `customers` | Customer database, profiles, segmentation | Customer data management |
| Module | Description | Key Features | Permissions |
|--------|-------------|--------------|-------------|
| `contracts` | Cross-module protocols and interfaces | Service protocols, type-safe interfaces | - |
| `core` | Dashboard, settings, profile | Basic platform operation | 5 |
| `cms` | Content pages, media library, themes | Content management | 5 |
| `customers` | Customer database, profiles, segmentation | Customer data management | 4 |
| `tenancy` | Platform, company, vendor, admin user management | Multi-tenant infrastructure | 4 |
### Optional Modules (10)
### Optional Modules (11)
Optional modules can be **enabled or disabled per platform**. They provide additional functionality that may not be needed by all platforms.
| Module | Dependencies | Description |
|--------|--------------|-------------|
| `cart` | - | Shopping cart management, session-based carts |
| `catalog` | - | Customer-facing product browsing |
| `checkout` | `cart`, `orders`, `payments` | Cart-to-order conversion, checkout flow |
| `payments` | - | Payment gateway integrations (Stripe, PayPal, etc.) |
| `billing` | `payments` | Platform subscriptions, vendor invoices |
| `inventory` | - | Stock management, locations |
| `orders` | `payments` | Order management, customer checkout |
| `marketplace` | `inventory` | Letzshop integration |
| `analytics` | - | Reports, dashboards |
| `messaging` | - | Messages, notifications |
| Module | Dependencies | Description | Permissions |
|--------|--------------|-------------|-------------|
| `analytics` | - | Reports, dashboards | 3 |
| `billing` | `payments` | Platform subscriptions, vendor invoices | 5 |
| `cart` | `inventory` | Shopping cart management, session-based carts | 2 |
| `catalog` | `inventory` | Customer-facing product browsing | 6 |
| `checkout` | `cart`, `orders`, `payments`, `customers` | Cart-to-order conversion, checkout flow | 2 |
| `inventory` | - | Stock management, locations | 3 |
| `loyalty` | `customers` | Stamp/points loyalty programs, wallet integration | 4 |
| `marketplace` | `inventory` | Letzshop integration | 3 |
| `messaging` | - | Messages, notifications | 3 |
| `orders` | `payments` | Order management, customer checkout | 4 |
| `payments` | - | Payment gateway integrations (Stripe, PayPal, etc.) | 3 |
### Internal Modules (2)
@@ -167,8 +169,8 @@ Each module must have a `definition.py` with a `ModuleDefinition` instance:
```python
# app/modules/analytics/definition.py
from app.modules.base import ModuleDefinition
from models.database.admin_menu_config import FrontendType
from app.modules.base import ModuleDefinition, PermissionDefinition
from app.modules.enums import FrontendType
analytics_module = ModuleDefinition(
# Identity
@@ -191,6 +193,22 @@ analytics_module = ModuleDefinition(
"custom_reports",
],
# Module-driven permissions (RBAC)
permissions=[
PermissionDefinition(
id="analytics.view",
label_key="analytics.permissions.view",
description_key="analytics.permissions.view_desc",
category="analytics",
),
PermissionDefinition(
id="analytics.export",
label_key="analytics.permissions.export",
description_key="analytics.permissions.export_desc",
category="analytics",
),
],
# Menu items per frontend
menu_items={
FrontendType.ADMIN: [], # Analytics uses dashboard
@@ -218,6 +236,7 @@ analytics_module = ModuleDefinition(
| `version` | `str` | Semantic version (default: "1.0.0") |
| `requires` | `list[str]` | Module codes this depends on |
| `features` | `list[str]` | Feature codes for tier gating |
| `permissions` | `list[PermissionDefinition]` | RBAC permission definitions |
| `menu_items` | `dict` | Menu items per frontend type |
| `is_core` | `bool` | Cannot be disabled if True |
| `is_internal` | `bool` | Admin-only if True |
@@ -929,6 +948,10 @@ The architecture validator (`scripts/validate_architecture.py`) enforces module
| MOD-017 | ERROR | Services must be in modules, not `app/services/` |
| MOD-018 | ERROR | Tasks must be in modules, not `app/tasks/` |
| MOD-019 | ERROR | Schemas must be in modules, not `models/schema/` |
| MOD-020 | WARNING | Module definition must have required attributes (code, name, description, version, features) |
| MOD-021 | WARNING | Modules with menus should have features defined |
| MOD-022 | INFO | Feature modules should have permissions (unless internal or storefront-only) |
| MOD-023 | INFO | Modules with routers should use `get_*_with_routers` pattern |
Run validation:
```bash

View File

@@ -850,6 +850,93 @@ Use mobile-first responsive classes.
---
## Module Structure Rules
Module rules enforce consistent structure and completeness across all modules in `app/modules/`.
### MOD-020: Module Definition Completeness
**Severity:** Warning
Module definitions should include required attributes: code, name, description, version, and features.
```python
# ✅ Good - Complete definition
module = ModuleDefinition(
code="billing",
name="Billing & Subscriptions",
description="Platform subscription management",
version="1.0.0",
features=["subscription_management", "billing_history"],
permissions=[...],
)
# ❌ Bad - Missing features
module = ModuleDefinition(
code="billing",
name="Billing",
description="...",
version="1.0.0",
# Missing features and permissions
)
```
### MOD-021: Modules with Menus Should Have Features
**Severity:** Warning
If a module defines menu items or menu sections, it should also define features.
```python
# ❌ Bad - Has menus but no features
module = ModuleDefinition(
code="billing",
menus={FrontendType.ADMIN: [...]},
# Missing features!
)
```
### MOD-022: Feature Modules Should Have Permissions
**Severity:** Info
Modules with features should define permissions for RBAC, unless:
- `is_internal=True` (internal tools)
- Storefront-only module (session-based, no admin UI)
```python
# ✅ Good - Features with permissions
module = ModuleDefinition(
code="billing",
features=["subscription_management"],
permissions=[
PermissionDefinition(
id="billing.view_subscriptions",
label_key="billing.permissions.view_subscriptions",
description_key="billing.permissions.view_subscriptions_desc",
category="billing",
),
],
)
```
### MOD-023: Router Pattern Consistency
**Severity:** Info
Modules with routers should use the `get_*_with_routers()` pattern for lazy imports.
```python
# ✅ Good - Lazy router pattern
def _get_admin_router():
from app.modules.billing.routes.api.admin import admin_router
return admin_router
def get_billing_module_with_routers() -> ModuleDefinition:
billing_module.admin_router = _get_admin_router()
return billing_module
```
See [Module System Architecture](../architecture/module-system.md) for complete MOD-001 to MOD-019 rules.
---
## Security & Multi-Tenancy Rules
### Multi-Tenancy Rules
@@ -1030,20 +1117,21 @@ All rules are defined in `.architecture-rules.yaml`. To modify rules:
## Summary Statistics
| Category | Rules | Errors | Warnings |
|----------|-------|--------|----------|
| Backend | 20 | 15 | 5 |
| Frontend JS | 7 | 6 | 1 |
| Frontend Templates | 8 | 4 | 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** | **58** | **38** | **20** |
| Category | Rules | Errors | Warnings | Info |
|----------|-------|--------|----------|------|
| Backend | 20 | 15 | 5 | 0 |
| Module Structure | 23 | 7 | 10 | 6 |
| Frontend JS | 7 | 6 | 1 | 0 |
| Frontend Templates | 8 | 4 | 4 | 0 |
| Frontend Macros | 5 | 2 | 3 | 0 |
| Frontend Components | 1 | 0 | 1 | 0 |
| Frontend Styling | 4 | 1 | 3 | 0 |
| Naming | 5 | 3 | 2 | 0 |
| Security | 5 | 5 | 0 | 0 |
| Quality | 3 | 2 | 1 | 0 |
| **Total** | **81** | **45** | **30** | **6** |
---
**Last Updated:** 2025-12-21
**Version:** 2.4
**Last Updated:** 2026-02-02
**Version:** 2.5

View File

@@ -4351,6 +4351,117 @@ class ArchitectureValidator:
suggestion="Create 'exceptions.py' or 'exceptions/__init__.py'",
)
# MOD-020: Check module definition has required attributes
self._validate_module_definition_completeness(module_dir, module_name, definition_file, definition_content)
def _validate_module_definition_completeness(
self,
module_dir: Path,
module_name: str,
definition_file: Path,
definition_content: str
):
"""
Validate module definition completeness (MOD-020 to MOD-023).
Checks:
- MOD-020: Required attributes (code, name, description, version, features, permissions)
- MOD-021: Modules with menus should have features
- MOD-022: Feature modules should have permissions
- MOD-023: Modules with routers should use get_*_with_routers pattern
"""
# Detect module characteristics
is_internal = "is_internal=True" in definition_content or "is_internal = True" in definition_content
is_core_infrastructure = (
("is_core=True" in definition_content or "is_core = True" in definition_content) and
("menu_items={}" in definition_content or "menu_items = {}" in definition_content or "menu_items" not in definition_content)
)
has_features = "features=[" in definition_content or "features = [" in definition_content
has_permissions = "permissions=[" in definition_content or "permissions = [" in definition_content
has_menus = "menus={" in definition_content or "menus = {" in definition_content
has_menu_items = (
"menu_items={" in definition_content and
not re.search(r"menu_items\s*=\s*\{\s*\}", definition_content)
)
# Check for router lazy import pattern
has_router_imports = "_get_admin_router" in definition_content or "_get_vendor_router" in definition_content
has_get_with_routers = re.search(r"def get_\w+_module_with_routers\s*\(", definition_content)
# MOD-020: Check required attributes
# Basic required attributes
required_attrs = ["code=", "name=", "description=", "version="]
for attr in required_attrs:
if attr not in definition_content:
attr_name = attr.replace("=", "")
self._add_violation(
rule_id="MOD-020",
rule_name="Module definition must have required attributes",
severity=Severity.WARNING,
file_path=definition_file,
line_number=1,
message=f"Module '{module_name}' missing required attribute '{attr_name}'",
context="ModuleDefinition()",
suggestion=f"Add '{attr_name}' to ModuleDefinition",
)
# Check features (unless infrastructure module)
if not has_features and not is_core_infrastructure:
self._add_violation(
rule_id="MOD-020",
rule_name="Module definition must have required attributes",
severity=Severity.WARNING,
file_path=definition_file,
line_number=1,
message=f"Module '{module_name}' missing 'features' list",
context="ModuleDefinition() without features",
suggestion="Add 'features=[...]' to describe module capabilities",
)
# MOD-021: Modules with menus should have features
if (has_menus or has_menu_items) and not has_features:
self._add_violation(
rule_id="MOD-021",
rule_name="Modules with menus should have features",
severity=Severity.WARNING,
file_path=definition_file,
line_number=1,
message=f"Module '{module_name}' has menus but no features defined",
context="menus={...} without features=[...]",
suggestion="Add 'features=[...]' to describe what the module provides",
)
# MOD-022: Feature modules should have permissions
if has_features and not has_permissions and not is_internal:
# Check if it's a storefront-only module (cart, checkout)
is_storefront_only = module_name in ["cart", "checkout"]
if not is_storefront_only:
self._add_violation(
rule_id="MOD-022",
rule_name="Feature modules should have permissions",
severity=Severity.INFO,
file_path=definition_file,
line_number=1,
message=f"Module '{module_name}' has features but no permissions defined",
context="features=[...] without permissions=[...]",
suggestion="Add 'permissions=[PermissionDefinition(...), ...]' for RBAC",
)
# MOD-023: Modules with routers should use get_*_with_routers pattern
if has_router_imports and not has_get_with_routers:
self._add_violation(
rule_id="MOD-023",
rule_name="Modules with routers should use get_*_with_routers pattern",
severity=Severity.INFO,
file_path=definition_file,
line_number=1,
message=f"Module '{module_name}' has router imports but no get_*_with_routers() function",
context="_get_admin_router() or _get_vendor_router() without wrapper",
suggestion=f"Add 'def get_{module_name}_module_with_routers()' function",
)
def _validate_legacy_locations(self, target_path: Path):
"""
Validate that code is not in legacy locations (MOD-016 to MOD-019).

View File

@@ -0,0 +1,2 @@
# tests/unit/modules/__init__.py
"""Unit tests for module system."""

View File

@@ -0,0 +1,315 @@
# tests/unit/modules/test_module_definitions.py
"""
Unit tests for module definitions.
Tests cover:
- Module definition completeness (MOD-020)
- Menu/feature consistency (MOD-021)
- Permission coverage (MOD-022)
- Router pattern consistency (MOD-023)
"""
import pytest
from app.modules.registry import MODULES
from app.modules.base import ModuleDefinition, PermissionDefinition
@pytest.mark.unit
class TestModuleDefinitionCompleteness:
"""Test MOD-020: Module definitions have required attributes."""
def test_all_modules_have_code(self):
"""Test that all modules have a code defined."""
for code, module in MODULES.items():
assert module.code, f"Module {code} missing 'code' attribute"
assert module.code == code, f"Module code mismatch: {module.code} != {code}"
def test_all_modules_have_name(self):
"""Test that all modules have a name defined."""
for code, module in MODULES.items():
assert module.name, f"Module {code} missing 'name' attribute"
def test_all_modules_have_description(self):
"""Test that all modules have a description defined."""
for code, module in MODULES.items():
assert module.description, f"Module {code} missing 'description' attribute"
def test_all_modules_have_version(self):
"""Test that all modules have a version defined."""
for code, module in MODULES.items():
assert module.version, f"Module {code} missing 'version' attribute"
# Version should be semantic versioning format
parts = module.version.split(".")
assert len(parts) >= 2, f"Module {code} has invalid version format: {module.version}"
def test_all_modules_have_features(self):
"""Test that all modules have features defined."""
for code, module in MODULES.items():
assert module.features is not None, f"Module {code} missing 'features' attribute"
assert isinstance(module.features, list), f"Module {code} features should be a list"
# All modules should have at least one feature
assert len(module.features) > 0, f"Module {code} has empty features list"
@pytest.mark.unit
class TestModulePermissions:
"""Test MOD-022: Feature modules should have permissions."""
# Modules exempt from permission requirements
EXEMPT_MODULES = {
"contracts", # Infrastructure module
"dev-tools", # Internal module (is_internal=True)
"monitoring", # Internal module (is_internal=True)
}
def test_feature_modules_have_permissions(self):
"""Test that modules with features have permissions (unless exempt)."""
for code, module in MODULES.items():
if code in self.EXEMPT_MODULES:
continue
# If module has features, it should have permissions
if module.features:
assert module.permissions is not None, (
f"Module {code} has features but no permissions"
)
assert len(module.permissions) > 0, (
f"Module {code} has features but empty permissions list"
)
def test_internal_modules_exempt_from_permissions(self):
"""Test that internal modules don't require permissions."""
internal_modules = [m for m in MODULES.values() if getattr(m, "is_internal", False)]
# Internal modules exist
assert len(internal_modules) > 0, "No internal modules found"
# They may or may not have permissions (exempt from requirement)
def test_permission_structure(self):
"""Test that permissions have required fields."""
for code, module in MODULES.items():
if not module.permissions:
continue
for perm in module.permissions:
assert isinstance(perm, PermissionDefinition), (
f"Module {code} permission is not a PermissionDefinition"
)
assert perm.id, f"Module {code} has permission without id"
assert perm.label_key, f"Module {code} permission {perm.id} missing label_key"
assert perm.description_key, (
f"Module {code} permission {perm.id} missing description_key"
)
assert perm.category, f"Module {code} permission {perm.id} missing category"
def test_permission_ids_follow_convention(self):
"""Test that permission IDs follow the module.action convention."""
for code, module in MODULES.items():
if not module.permissions:
continue
for perm in module.permissions:
# Permission ID should contain a dot
assert "." in perm.id, (
f"Module {code} permission {perm.id} should follow 'module.action' format"
)
def test_permission_counts(self):
"""Test that modules have expected permission counts."""
# Modules that should have permissions
modules_with_permissions = {
code: module
for code, module in MODULES.items()
if module.permissions
}
# At least 15 modules should have permissions
assert len(modules_with_permissions) >= 15, (
f"Expected at least 15 modules with permissions, got {len(modules_with_permissions)}"
)
@pytest.mark.unit
class TestModuleMenuFeatureConsistency:
"""Test MOD-021: Modules with menus should have features."""
def test_modules_with_menus_have_features(self):
"""Test that modules with menu definitions have features."""
for code, module in MODULES.items():
has_menus = getattr(module, "menus", None) and len(module.menus) > 0
has_menu_items = (
getattr(module, "menu_items", None) and
any(items for items in module.menu_items.values())
)
if has_menus or has_menu_items:
assert module.features and len(module.features) > 0, (
f"Module {code} has menus but no features defined"
)
@pytest.mark.unit
class TestModuleRouterPattern:
"""Test MOD-023: Router pattern consistency."""
def test_modules_with_routers_attribute(self):
"""Test that modules can have router attributes attached."""
# After calling get_*_with_routers(), modules should have router attrs
# This test just verifies the attribute exists (may be None)
for code, module in MODULES.items():
# These are optional attributes set by get_*_with_routers()
# Just verify they can be accessed without error
_ = getattr(module, "admin_router", None)
_ = getattr(module, "vendor_router", None)
_ = getattr(module, "platform_router", None)
@pytest.mark.unit
class TestSpecificModulePermissions:
"""Test specific modules have their expected permissions."""
def test_cart_module_permissions(self):
"""Test cart module has required permissions."""
module = MODULES.get("cart")
assert module is not None
assert module.permissions is not None
perm_ids = {p.id for p in module.permissions}
assert "cart.view" in perm_ids
assert "cart.manage" in perm_ids
def test_checkout_module_permissions(self):
"""Test checkout module has required permissions."""
module = MODULES.get("checkout")
assert module is not None
assert module.permissions is not None
perm_ids = {p.id for p in module.permissions}
assert "checkout.view_settings" in perm_ids
assert "checkout.manage_settings" in perm_ids
def test_analytics_module_permissions(self):
"""Test analytics module has required permissions."""
module = MODULES.get("analytics")
assert module is not None
assert module.permissions is not None
perm_ids = {p.id for p in module.permissions}
assert "analytics.view" in perm_ids
assert "analytics.export" in perm_ids
assert "analytics.manage_dashboards" in perm_ids
def test_billing_module_permissions(self):
"""Test billing module has required permissions."""
module = MODULES.get("billing")
assert module is not None
assert module.permissions is not None
perm_ids = {p.id for p in module.permissions}
assert "billing.view_tiers" in perm_ids
assert "billing.manage_tiers" in perm_ids
assert "billing.view_subscriptions" in perm_ids
assert "billing.manage_subscriptions" in perm_ids
assert "billing.view_invoices" in perm_ids
def test_cms_module_permissions(self):
"""Test cms module has required permissions."""
module = MODULES.get("cms")
assert module is not None
assert module.permissions is not None
perm_ids = {p.id for p in module.permissions}
assert "cms.view_pages" in perm_ids
assert "cms.manage_pages" in perm_ids
assert "cms.view_media" in perm_ids
assert "cms.manage_media" in perm_ids
assert "cms.manage_themes" in perm_ids
def test_loyalty_module_permissions(self):
"""Test loyalty module has required permissions."""
module = MODULES.get("loyalty")
assert module is not None
assert module.permissions is not None
perm_ids = {p.id for p in module.permissions}
assert "loyalty.view_programs" in perm_ids
assert "loyalty.manage_programs" in perm_ids
assert "loyalty.view_rewards" in perm_ids
assert "loyalty.manage_rewards" in perm_ids
def test_marketplace_module_permissions(self):
"""Test marketplace module has required permissions."""
module = MODULES.get("marketplace")
assert module is not None
assert module.permissions is not None
perm_ids = {p.id for p in module.permissions}
assert "marketplace.view_integration" in perm_ids
assert "marketplace.manage_integration" in perm_ids
assert "marketplace.sync_products" in perm_ids
def test_messaging_module_permissions(self):
"""Test messaging module has required permissions."""
module = MODULES.get("messaging")
assert module is not None
assert module.permissions is not None
perm_ids = {p.id for p in module.permissions}
assert "messaging.view_messages" in perm_ids
assert "messaging.send_messages" in perm_ids
assert "messaging.manage_templates" in perm_ids
def test_payments_module_permissions(self):
"""Test payments module has required permissions."""
module = MODULES.get("payments")
assert module is not None
assert module.permissions is not None
perm_ids = {p.id for p in module.permissions}
assert "payments.view_gateways" in perm_ids
assert "payments.manage_gateways" in perm_ids
assert "payments.view_transactions" in perm_ids
@pytest.mark.unit
class TestModuleFeatures:
"""Test module feature definitions."""
def test_cart_module_features(self):
"""Test cart module has required features."""
module = MODULES.get("cart")
assert module is not None
expected_features = {
"cart_management",
"cart_persistence",
"cart_item_operations",
"shipping_calculation",
"promotion_application",
}
assert expected_features.issubset(set(module.features))
def test_checkout_module_features(self):
"""Test checkout module has required features."""
module = MODULES.get("checkout")
assert module is not None
expected_features = {
"checkout_flow",
"order_creation",
"payment_processing",
"checkout_validation",
"guest_checkout",
}
assert expected_features.issubset(set(module.features))
def test_catalog_module_features(self):
"""Test catalog module has required features."""
module = MODULES.get("catalog")
assert module is not None
expected_features = {
"product_catalog",
"product_search",
}
assert expected_features.issubset(set(module.features))