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:
@@ -551,3 +551,175 @@ module_rules:
|
|||||||
- "__init__.py"
|
- "__init__.py"
|
||||||
- "base.py"
|
- "base.py"
|
||||||
- "auth.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"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Defines the analytics module including its features, menu items,
|
|||||||
route configurations, and self-contained module settings.
|
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
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
|
|
||||||
@@ -37,6 +37,27 @@ analytics_module = ModuleDefinition(
|
|||||||
"export_reports", # Export to CSV/Excel
|
"export_reports", # Export to CSV/Excel
|
||||||
"usage_metrics", # Usage and performance metrics
|
"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={
|
menu_items={
|
||||||
FrontendType.ADMIN: [
|
FrontendType.ADMIN: [
|
||||||
# Analytics appears in dashboard for admin
|
# Analytics appears in dashboard for admin
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Defines the billing module including its features, menu items,
|
|||||||
route configurations, and scheduled tasks.
|
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
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
|
|
||||||
@@ -42,6 +42,39 @@ billing_module = ModuleDefinition(
|
|||||||
"trial_management", # Manage vendor trial periods
|
"trial_management", # Manage vendor trial periods
|
||||||
"limit_overrides", # Override tier limits per vendor
|
"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={
|
menu_items={
|
||||||
FrontendType.ADMIN: [
|
FrontendType.ADMIN: [
|
||||||
"subscription-tiers", # Manage tier definitions
|
"subscription-tiers", # Manage tier definitions
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ This module provides shopping cart functionality for customer storefronts.
|
|||||||
It is session-based and does not require customer authentication.
|
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(
|
module = ModuleDefinition(
|
||||||
code="cart",
|
code="cart",
|
||||||
@@ -15,4 +16,29 @@ module = ModuleDefinition(
|
|||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
is_self_contained=True,
|
is_self_contained=True,
|
||||||
requires=["inventory"], # Checks inventory availability
|
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={},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ module = ModuleDefinition(
|
|||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
is_self_contained=True,
|
is_self_contained=True,
|
||||||
requires=["inventory"],
|
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
|
# Module-driven permissions
|
||||||
permissions=[
|
permissions=[
|
||||||
PermissionDefinition(
|
PermissionDefinition(
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
# app/modules/checkout/definition.py
|
# 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(
|
module = ModuleDefinition(
|
||||||
code="checkout",
|
code="checkout",
|
||||||
@@ -10,4 +16,29 @@ module = ModuleDefinition(
|
|||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
is_self_contained=True,
|
is_self_contained=True,
|
||||||
requires=["cart", "orders", "payments", "customers"],
|
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={},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ This is a self-contained module with:
|
|||||||
- Templates: app.modules.cms.templates (namespaced as cms/)
|
- 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
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ cms_module = ModuleDefinition(
|
|||||||
code="cms",
|
code="cms",
|
||||||
name="Content Management",
|
name="Content Management",
|
||||||
description="Content pages, media library, and vendor themes.",
|
description="Content pages, media library, and vendor themes.",
|
||||||
|
version="1.0.0",
|
||||||
features=[
|
features=[
|
||||||
"cms_basic", # Basic page editing
|
"cms_basic", # Basic page editing
|
||||||
"cms_custom_pages", # Custom page creation
|
"cms_custom_pages", # Custom page creation
|
||||||
@@ -43,6 +44,39 @@ cms_module = ModuleDefinition(
|
|||||||
"cms_seo", # SEO tools
|
"cms_seo", # SEO tools
|
||||||
"media_library", # Media file management
|
"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={
|
menu_items={
|
||||||
FrontendType.ADMIN: [
|
FrontendType.ADMIN: [
|
||||||
"content-pages", # Platform content pages
|
"content-pages", # Platform content pages
|
||||||
|
|||||||
@@ -85,20 +85,8 @@ dev_tools_module = ModuleDefinition(
|
|||||||
schemas_path="app.modules.dev_tools.schemas",
|
schemas_path="app.modules.dev_tools.schemas",
|
||||||
exceptions_path="app.modules.dev_tools.exceptions",
|
exceptions_path="app.modules.dev_tools.exceptions",
|
||||||
tasks_path="app.modules.dev_tools.tasks",
|
tasks_path="app.modules.dev_tools.tasks",
|
||||||
# =========================================================================
|
|
||||||
# Scheduled Tasks
|
|
||||||
# =========================================================================
|
|
||||||
# Note: Code quality and test tasks are on-demand, not scheduled.
|
# Note: Code quality and test tasks are on-demand, not scheduled.
|
||||||
# If scheduled scans are desired, they can be added here:
|
# If scheduled scans are desired, add ScheduledTask entries 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=[],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Defines the loyalty module including its features, menu items,
|
|||||||
route configurations, and scheduled tasks.
|
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
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
|
|
||||||
@@ -24,11 +24,11 @@ def _get_vendor_router():
|
|||||||
return vendor_router
|
return vendor_router
|
||||||
|
|
||||||
|
|
||||||
def _get_public_router():
|
def _get_platform_router():
|
||||||
"""Lazy import of public router to avoid circular imports."""
|
"""Lazy import of platform router to avoid circular imports."""
|
||||||
from app.modules.loyalty.routes.api.public import public_router
|
from app.modules.loyalty.routes.api.platform import platform_router
|
||||||
|
|
||||||
return public_router
|
return platform_router
|
||||||
|
|
||||||
|
|
||||||
# Loyalty module definition
|
# Loyalty module definition
|
||||||
@@ -60,6 +60,33 @@ loyalty_module = ModuleDefinition(
|
|||||||
"loyalty_stats", # Dashboard statistics
|
"loyalty_stats", # Dashboard statistics
|
||||||
"loyalty_reports", # Transaction reports
|
"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={
|
menu_items={
|
||||||
FrontendType.ADMIN: [
|
FrontendType.ADMIN: [
|
||||||
"loyalty-programs", # View all programs
|
"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.admin_router = _get_admin_router()
|
||||||
loyalty_module.vendor_router = _get_vendor_router()
|
loyalty_module.vendor_router = _get_vendor_router()
|
||||||
# Note: public_router needs to be attached separately in main.py
|
loyalty_module.platform_router = _get_platform_router()
|
||||||
# as it doesn't require authentication
|
|
||||||
return loyalty_module
|
return loyalty_module
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ dependencies, route configurations, and scheduled tasks.
|
|||||||
Note: This module requires the inventory module to be enabled.
|
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
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
|
|
||||||
@@ -43,6 +43,27 @@ marketplace_module = ModuleDefinition(
|
|||||||
"order_import", # Import orders from marketplace
|
"order_import", # Import orders from marketplace
|
||||||
"marketplace_analytics", # Marketplace performance metrics
|
"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={
|
menu_items={
|
||||||
FrontendType.ADMIN: [
|
FrontendType.ADMIN: [
|
||||||
"marketplace-letzshop", # Marketplace monitoring
|
"marketplace-letzshop", # Marketplace monitoring
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Defines the messaging module including its features, menu items,
|
|||||||
route configurations, and self-contained module settings.
|
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
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
|
|
||||||
@@ -37,6 +37,27 @@ messaging_module = ModuleDefinition(
|
|||||||
"message_attachments", # File attachments
|
"message_attachments", # File attachments
|
||||||
"admin_notifications", # System admin notifications
|
"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={
|
menu_items={
|
||||||
FrontendType.ADMIN: [
|
FrontendType.ADMIN: [
|
||||||
"messages", # Admin messages
|
"messages", # Admin messages
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ This separation allows:
|
|||||||
3. Orders without billing (customer payments only)
|
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
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
|
|
||||||
@@ -51,6 +51,27 @@ payments_module = ModuleDefinition(
|
|||||||
"bank_transfer", # Bank transfer support
|
"bank_transfer", # Bank transfer support
|
||||||
"transaction_history", # Transaction records
|
"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={
|
menu_items={
|
||||||
FrontendType.ADMIN: [
|
FrontendType.ADMIN: [
|
||||||
"payment-gateways", # Configure payment gateways
|
"payment-gateways", # Configure payment gateways
|
||||||
@@ -61,7 +82,14 @@ payments_module = ModuleDefinition(
|
|||||||
},
|
},
|
||||||
is_core=False,
|
is_core=False,
|
||||||
is_internal=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",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -72,33 +72,35 @@ touch app/modules/mymodule/exceptions.py
|
|||||||
|
|
||||||
## Three-Tier Classification
|
## Three-Tier Classification
|
||||||
|
|
||||||
### Core Modules (4)
|
### Core Modules (5)
|
||||||
|
|
||||||
Core modules are **always enabled** and cannot be disabled. They provide fundamental platform functionality.
|
Core modules are **always enabled** and cannot be disabled. They provide fundamental platform functionality.
|
||||||
|
|
||||||
| Module | Description | Key Features |
|
| Module | Description | Key Features | Permissions |
|
||||||
|--------|-------------|--------------|
|
|--------|-------------|--------------|-------------|
|
||||||
| `core` | Dashboard, settings, profile | Basic platform operation |
|
| `contracts` | Cross-module protocols and interfaces | Service protocols, type-safe interfaces | - |
|
||||||
| `tenancy` | Platform, company, vendor, admin user management | Multi-tenant infrastructure |
|
| `core` | Dashboard, settings, profile | Basic platform operation | 5 |
|
||||||
| `cms` | Content pages, media library, themes | Content management |
|
| `cms` | Content pages, media library, themes | Content management | 5 |
|
||||||
| `customers` | Customer database, profiles, segmentation | Customer data management |
|
| `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.
|
Optional modules can be **enabled or disabled per platform**. They provide additional functionality that may not be needed by all platforms.
|
||||||
|
|
||||||
| Module | Dependencies | Description |
|
| Module | Dependencies | Description | Permissions |
|
||||||
|--------|--------------|-------------|
|
|--------|--------------|-------------|-------------|
|
||||||
| `cart` | - | Shopping cart management, session-based carts |
|
| `analytics` | - | Reports, dashboards | 3 |
|
||||||
| `catalog` | - | Customer-facing product browsing |
|
| `billing` | `payments` | Platform subscriptions, vendor invoices | 5 |
|
||||||
| `checkout` | `cart`, `orders`, `payments` | Cart-to-order conversion, checkout flow |
|
| `cart` | `inventory` | Shopping cart management, session-based carts | 2 |
|
||||||
| `payments` | - | Payment gateway integrations (Stripe, PayPal, etc.) |
|
| `catalog` | `inventory` | Customer-facing product browsing | 6 |
|
||||||
| `billing` | `payments` | Platform subscriptions, vendor invoices |
|
| `checkout` | `cart`, `orders`, `payments`, `customers` | Cart-to-order conversion, checkout flow | 2 |
|
||||||
| `inventory` | - | Stock management, locations |
|
| `inventory` | - | Stock management, locations | 3 |
|
||||||
| `orders` | `payments` | Order management, customer checkout |
|
| `loyalty` | `customers` | Stamp/points loyalty programs, wallet integration | 4 |
|
||||||
| `marketplace` | `inventory` | Letzshop integration |
|
| `marketplace` | `inventory` | Letzshop integration | 3 |
|
||||||
| `analytics` | - | Reports, dashboards |
|
| `messaging` | - | Messages, notifications | 3 |
|
||||||
| `messaging` | - | Messages, notifications |
|
| `orders` | `payments` | Order management, customer checkout | 4 |
|
||||||
|
| `payments` | - | Payment gateway integrations (Stripe, PayPal, etc.) | 3 |
|
||||||
|
|
||||||
### Internal Modules (2)
|
### Internal Modules (2)
|
||||||
|
|
||||||
@@ -167,8 +169,8 @@ Each module must have a `definition.py` with a `ModuleDefinition` instance:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# app/modules/analytics/definition.py
|
# app/modules/analytics/definition.py
|
||||||
from app.modules.base import ModuleDefinition
|
from app.modules.base import ModuleDefinition, PermissionDefinition
|
||||||
from models.database.admin_menu_config import FrontendType
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
analytics_module = ModuleDefinition(
|
analytics_module = ModuleDefinition(
|
||||||
# Identity
|
# Identity
|
||||||
@@ -191,6 +193,22 @@ analytics_module = ModuleDefinition(
|
|||||||
"custom_reports",
|
"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 per frontend
|
||||||
menu_items={
|
menu_items={
|
||||||
FrontendType.ADMIN: [], # Analytics uses dashboard
|
FrontendType.ADMIN: [], # Analytics uses dashboard
|
||||||
@@ -218,6 +236,7 @@ analytics_module = ModuleDefinition(
|
|||||||
| `version` | `str` | Semantic version (default: "1.0.0") |
|
| `version` | `str` | Semantic version (default: "1.0.0") |
|
||||||
| `requires` | `list[str]` | Module codes this depends on |
|
| `requires` | `list[str]` | Module codes this depends on |
|
||||||
| `features` | `list[str]` | Feature codes for tier gating |
|
| `features` | `list[str]` | Feature codes for tier gating |
|
||||||
|
| `permissions` | `list[PermissionDefinition]` | RBAC permission definitions |
|
||||||
| `menu_items` | `dict` | Menu items per frontend type |
|
| `menu_items` | `dict` | Menu items per frontend type |
|
||||||
| `is_core` | `bool` | Cannot be disabled if True |
|
| `is_core` | `bool` | Cannot be disabled if True |
|
||||||
| `is_internal` | `bool` | Admin-only 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-017 | ERROR | Services must be in modules, not `app/services/` |
|
||||||
| MOD-018 | ERROR | Tasks must be in modules, not `app/tasks/` |
|
| MOD-018 | ERROR | Tasks must be in modules, not `app/tasks/` |
|
||||||
| MOD-019 | ERROR | Schemas must be in modules, not `models/schema/` |
|
| 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:
|
Run validation:
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -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
|
## Security & Multi-Tenancy Rules
|
||||||
|
|
||||||
### Multi-Tenancy Rules
|
### Multi-Tenancy Rules
|
||||||
@@ -1030,20 +1117,21 @@ All rules are defined in `.architecture-rules.yaml`. To modify rules:
|
|||||||
|
|
||||||
## Summary Statistics
|
## Summary Statistics
|
||||||
|
|
||||||
| Category | Rules | Errors | Warnings |
|
| Category | Rules | Errors | Warnings | Info |
|
||||||
|----------|-------|--------|----------|
|
|----------|-------|--------|----------|------|
|
||||||
| Backend | 20 | 15 | 5 |
|
| Backend | 20 | 15 | 5 | 0 |
|
||||||
| Frontend JS | 7 | 6 | 1 |
|
| Module Structure | 23 | 7 | 10 | 6 |
|
||||||
| Frontend Templates | 8 | 4 | 4 |
|
| Frontend JS | 7 | 6 | 1 | 0 |
|
||||||
| Frontend Macros | 5 | 2 | 3 |
|
| Frontend Templates | 8 | 4 | 4 | 0 |
|
||||||
| Frontend Components | 1 | 0 | 1 |
|
| Frontend Macros | 5 | 2 | 3 | 0 |
|
||||||
| Frontend Styling | 4 | 1 | 3 |
|
| Frontend Components | 1 | 0 | 1 | 0 |
|
||||||
| Naming | 5 | 3 | 2 |
|
| Frontend Styling | 4 | 1 | 3 | 0 |
|
||||||
| Security | 5 | 5 | 0 |
|
| Naming | 5 | 3 | 2 | 0 |
|
||||||
| Quality | 3 | 2 | 1 |
|
| Security | 5 | 5 | 0 | 0 |
|
||||||
| **Total** | **58** | **38** | **20** |
|
| Quality | 3 | 2 | 1 | 0 |
|
||||||
|
| **Total** | **81** | **45** | **30** | **6** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated:** 2025-12-21
|
**Last Updated:** 2026-02-02
|
||||||
**Version:** 2.4
|
**Version:** 2.5
|
||||||
|
|||||||
@@ -4351,6 +4351,117 @@ class ArchitectureValidator:
|
|||||||
suggestion="Create 'exceptions.py' or 'exceptions/__init__.py'",
|
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):
|
def _validate_legacy_locations(self, target_path: Path):
|
||||||
"""
|
"""
|
||||||
Validate that code is not in legacy locations (MOD-016 to MOD-019).
|
Validate that code is not in legacy locations (MOD-016 to MOD-019).
|
||||||
|
|||||||
2
tests/unit/modules/__init__.py
Normal file
2
tests/unit/modules/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# tests/unit/modules/__init__.py
|
||||||
|
"""Unit tests for module system."""
|
||||||
315
tests/unit/modules/test_module_definitions.py
Normal file
315
tests/unit/modules/test_module_definitions.py
Normal 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))
|
||||||
Reference in New Issue
Block a user