diff --git a/.architecture-rules/module.yaml b/.architecture-rules/module.yaml index 6e418fe9..e617330b 100644 --- a/.architecture-rules/module.yaml +++ b/.architecture-rules/module.yaml @@ -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" diff --git a/app/api/v1/public/__init__.py b/app/api/v1/platform/__init__.py similarity index 100% rename from app/api/v1/public/__init__.py rename to app/api/v1/platform/__init__.py diff --git a/app/api/v1/public/signup.py b/app/api/v1/platform/signup.py similarity index 100% rename from app/api/v1/public/signup.py rename to app/api/v1/platform/signup.py diff --git a/app/modules/analytics/definition.py b/app/modules/analytics/definition.py index 3f2cd10c..e7241e90 100644 --- a/app/modules/analytics/definition.py +++ b/app/modules/analytics/definition.py @@ -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 diff --git a/app/modules/billing/definition.py b/app/modules/billing/definition.py index bd4c09de..48df80f9 100644 --- a/app/modules/billing/definition.py +++ b/app/modules/billing/definition.py @@ -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 diff --git a/app/modules/billing/routes/api/public.py b/app/modules/billing/routes/api/platform.py similarity index 100% rename from app/modules/billing/routes/api/public.py rename to app/modules/billing/routes/api/platform.py diff --git a/app/modules/billing/routes/pages/public.py b/app/modules/billing/routes/pages/platform.py similarity index 100% rename from app/modules/billing/routes/pages/public.py rename to app/modules/billing/routes/pages/platform.py diff --git a/app/modules/billing/templates/billing/public/pricing.html b/app/modules/billing/templates/billing/platform/pricing.html similarity index 100% rename from app/modules/billing/templates/billing/public/pricing.html rename to app/modules/billing/templates/billing/platform/pricing.html diff --git a/app/modules/billing/templates/billing/public/signup-success.html b/app/modules/billing/templates/billing/platform/signup-success.html similarity index 100% rename from app/modules/billing/templates/billing/public/signup-success.html rename to app/modules/billing/templates/billing/platform/signup-success.html diff --git a/app/modules/billing/templates/billing/public/signup.html b/app/modules/billing/templates/billing/platform/signup.html similarity index 100% rename from app/modules/billing/templates/billing/public/signup.html rename to app/modules/billing/templates/billing/platform/signup.html diff --git a/app/modules/cart/definition.py b/app/modules/cart/definition.py index 7bb2a966..2e0a347c 100644 --- a/app/modules/cart/definition.py +++ b/app/modules/cart/definition.py @@ -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={}, ) diff --git a/app/modules/catalog/definition.py b/app/modules/catalog/definition.py index ba672021..009ea55e 100644 --- a/app/modules/catalog/definition.py +++ b/app/modules/catalog/definition.py @@ -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( diff --git a/app/modules/checkout/definition.py b/app/modules/checkout/definition.py index 127c16b9..50c719fd 100644 --- a/app/modules/checkout/definition.py +++ b/app/modules/checkout/definition.py @@ -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={}, ) diff --git a/app/modules/cms/definition.py b/app/modules/cms/definition.py index 343cc735..50e94e84 100644 --- a/app/modules/cms/definition.py +++ b/app/modules/cms/definition.py @@ -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 diff --git a/app/modules/cms/routes/pages/public.py b/app/modules/cms/routes/pages/platform.py similarity index 100% rename from app/modules/cms/routes/pages/public.py rename to app/modules/cms/routes/pages/platform.py diff --git a/app/modules/cms/templates/cms/public/content-page.html b/app/modules/cms/templates/cms/platform/content-page.html similarity index 100% rename from app/modules/cms/templates/cms/public/content-page.html rename to app/modules/cms/templates/cms/platform/content-page.html diff --git a/app/modules/cms/templates/cms/public/homepage-default.html b/app/modules/cms/templates/cms/platform/homepage-default.html similarity index 100% rename from app/modules/cms/templates/cms/public/homepage-default.html rename to app/modules/cms/templates/cms/platform/homepage-default.html diff --git a/app/modules/cms/templates/cms/public/homepage-minimal.html b/app/modules/cms/templates/cms/platform/homepage-minimal.html similarity index 100% rename from app/modules/cms/templates/cms/public/homepage-minimal.html rename to app/modules/cms/templates/cms/platform/homepage-minimal.html diff --git a/app/modules/cms/templates/cms/public/homepage-modern.html b/app/modules/cms/templates/cms/platform/homepage-modern.html similarity index 100% rename from app/modules/cms/templates/cms/public/homepage-modern.html rename to app/modules/cms/templates/cms/platform/homepage-modern.html diff --git a/app/modules/cms/templates/cms/public/homepage-wizamart.html b/app/modules/cms/templates/cms/platform/homepage-wizamart.html similarity index 100% rename from app/modules/cms/templates/cms/public/homepage-wizamart.html rename to app/modules/cms/templates/cms/platform/homepage-wizamart.html diff --git a/app/modules/cms/templates/cms/public/sections/_cta.html b/app/modules/cms/templates/cms/platform/sections/_cta.html similarity index 100% rename from app/modules/cms/templates/cms/public/sections/_cta.html rename to app/modules/cms/templates/cms/platform/sections/_cta.html diff --git a/app/modules/cms/templates/cms/public/sections/_features.html b/app/modules/cms/templates/cms/platform/sections/_features.html similarity index 100% rename from app/modules/cms/templates/cms/public/sections/_features.html rename to app/modules/cms/templates/cms/platform/sections/_features.html diff --git a/app/modules/cms/templates/cms/public/sections/_hero.html b/app/modules/cms/templates/cms/platform/sections/_hero.html similarity index 100% rename from app/modules/cms/templates/cms/public/sections/_hero.html rename to app/modules/cms/templates/cms/platform/sections/_hero.html diff --git a/app/modules/cms/templates/cms/public/sections/_pricing.html b/app/modules/cms/templates/cms/platform/sections/_pricing.html similarity index 100% rename from app/modules/cms/templates/cms/public/sections/_pricing.html rename to app/modules/cms/templates/cms/platform/sections/_pricing.html diff --git a/app/modules/core/routes/api/public.py b/app/modules/core/routes/api/platform.py similarity index 100% rename from app/modules/core/routes/api/public.py rename to app/modules/core/routes/api/platform.py diff --git a/app/modules/dev_tools/definition.py b/app/modules/dev_tools/definition.py index 08de8680..3884363c 100644 --- a/app/modules/dev_tools/definition.py +++ b/app/modules/dev_tools/definition.py @@ -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. ) diff --git a/app/modules/loyalty/definition.py b/app/modules/loyalty/definition.py index 85f9de2c..423c8976 100644 --- a/app/modules/loyalty/definition.py +++ b/app/modules/loyalty/definition.py @@ -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 diff --git a/app/modules/loyalty/routes/api/public.py b/app/modules/loyalty/routes/api/platform.py similarity index 100% rename from app/modules/loyalty/routes/api/public.py rename to app/modules/loyalty/routes/api/platform.py diff --git a/app/modules/marketplace/definition.py b/app/modules/marketplace/definition.py index 78f7d6ad..5e3a5c6c 100644 --- a/app/modules/marketplace/definition.py +++ b/app/modules/marketplace/definition.py @@ -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 diff --git a/app/modules/marketplace/routes/api/public.py b/app/modules/marketplace/routes/api/platform.py similarity index 100% rename from app/modules/marketplace/routes/api/public.py rename to app/modules/marketplace/routes/api/platform.py diff --git a/app/modules/marketplace/routes/pages/public.py b/app/modules/marketplace/routes/pages/platform.py similarity index 100% rename from app/modules/marketplace/routes/pages/public.py rename to app/modules/marketplace/routes/pages/platform.py diff --git a/app/modules/marketplace/templates/marketplace/public/find-shop.html b/app/modules/marketplace/templates/marketplace/platform/find-shop.html similarity index 100% rename from app/modules/marketplace/templates/marketplace/public/find-shop.html rename to app/modules/marketplace/templates/marketplace/platform/find-shop.html diff --git a/app/modules/messaging/definition.py b/app/modules/messaging/definition.py index 75f04bac..2e03ba9c 100644 --- a/app/modules/messaging/definition.py +++ b/app/modules/messaging/definition.py @@ -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 diff --git a/app/modules/payments/definition.py b/app/modules/payments/definition.py index 73fd2e43..ce1305b8 100644 --- a/app/modules/payments/definition.py +++ b/app/modules/payments/definition.py @@ -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", ) diff --git a/app/templates/public/base.html b/app/templates/platform/base.html similarity index 100% rename from app/templates/public/base.html rename to app/templates/platform/base.html diff --git a/docs/architecture/module-system.md b/docs/architecture/module-system.md index 70a69114..dcc8aba8 100644 --- a/docs/architecture/module-system.md +++ b/docs/architecture/module-system.md @@ -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 diff --git a/docs/development/architecture-rules.md b/docs/development/architecture-rules.md index 40b161cc..2243acc0 100644 --- a/docs/development/architecture-rules.md +++ b/docs/development/architecture-rules.md @@ -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 diff --git a/scripts/validate_architecture.py b/scripts/validate_architecture.py index 85c000dc..b719c4b3 100755 --- a/scripts/validate_architecture.py +++ b/scripts/validate_architecture.py @@ -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). diff --git a/static/public/css/.gitkeep b/static/platform/css/.gitkeep similarity index 100% rename from static/public/css/.gitkeep rename to static/platform/css/.gitkeep diff --git a/static/public/css/tailwind.css b/static/platform/css/tailwind.css similarity index 100% rename from static/public/css/tailwind.css rename to static/platform/css/tailwind.css diff --git a/static/public/css/tailwind.output.css b/static/platform/css/tailwind.output.css similarity index 100% rename from static/public/css/tailwind.output.css rename to static/platform/css/tailwind.output.css diff --git a/static/public/img/.gitkeep b/static/platform/img/.gitkeep similarity index 100% rename from static/public/img/.gitkeep rename to static/platform/img/.gitkeep diff --git a/static/public/js/.gitkeep b/static/platform/js/.gitkeep similarity index 100% rename from static/public/js/.gitkeep rename to static/platform/js/.gitkeep diff --git a/tests/integration/api/v1/public/README.md b/tests/integration/api/v1/platform/README.md similarity index 100% rename from tests/integration/api/v1/public/README.md rename to tests/integration/api/v1/platform/README.md diff --git a/tests/integration/api/v1/public/__init__.py b/tests/integration/api/v1/platform/__init__.py similarity index 100% rename from tests/integration/api/v1/public/__init__.py rename to tests/integration/api/v1/platform/__init__.py diff --git a/tests/integration/api/v1/public/test_letzshop_vendors.py b/tests/integration/api/v1/platform/test_letzshop_vendors.py similarity index 100% rename from tests/integration/api/v1/public/test_letzshop_vendors.py rename to tests/integration/api/v1/platform/test_letzshop_vendors.py diff --git a/tests/integration/api/v1/public/test_pricing.py b/tests/integration/api/v1/platform/test_pricing.py similarity index 100% rename from tests/integration/api/v1/public/test_pricing.py rename to tests/integration/api/v1/platform/test_pricing.py diff --git a/tests/integration/api/v1/public/test_signup.py b/tests/integration/api/v1/platform/test_signup.py similarity index 100% rename from tests/integration/api/v1/public/test_signup.py rename to tests/integration/api/v1/platform/test_signup.py diff --git a/tests/unit/modules/__init__.py b/tests/unit/modules/__init__.py new file mode 100644 index 00000000..3b797624 --- /dev/null +++ b/tests/unit/modules/__init__.py @@ -0,0 +1,2 @@ +# tests/unit/modules/__init__.py +"""Unit tests for module system.""" diff --git a/tests/unit/modules/test_module_definitions.py b/tests/unit/modules/test_module_definitions.py new file mode 100644 index 00000000..786b8229 --- /dev/null +++ b/tests/unit/modules/test_module_definitions.py @@ -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))