feat(roles): add admin store roles page, permission i18n, and menu integration
Some checks failed
CI / ruff (push) Successful in 9s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running

- Add admin store roles page with merchant→store cascading for superadmin
  and store-only selection for platform admin
- Add permission catalog API with translated labels/descriptions (en/fr/de/lb)
- Add permission translations to all 15 module locale files (60 files total)
- Add info icon tooltips for permission descriptions in role editor
- Add store roles menu item and admin menu item in module definition
- Fix store-selector.js URL construction bug when apiEndpoint has query params
- Add admin store roles API (CRUD + platform scoping)
- Add integration tests for admin store roles and permission catalog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 23:31:27 +01:00
parent 2b55e7458b
commit f95db7c0b1
83 changed files with 3491 additions and 513 deletions

View File

@@ -203,7 +203,19 @@ The system provides 5 preset roles with predefined permission sets:
| `viewer` | Read-only access | ~6 |
| `marketing` | Marketing and customer data | ~7 |
Preset roles are created automatically on first access. Store owners can also create custom roles with any combination of the ~75 available permissions.
Preset roles are created automatically on first access. Store owners can also create custom roles with any combination of the available permissions via the role editor UI at `/store/{store_code}/team/roles`.
### Custom Role Management
Store owners can create, edit, and delete custom roles via:
- **Store UI:** `/store/{store_code}/team/roles` (Alpine.js permission matrix)
- **Store API:** `POST/PUT/DELETE /api/v1/store/team/roles`
- **Admin UI:** `/admin/store-roles` (with Tom Select store picker)
- **Admin API:** `GET/POST/PUT/DELETE /api/v1/admin/store-roles?store_id=X`
The **Permission Catalog API** (`GET /api/v1/store/team/permissions/catalog`) returns all permissions grouped by category with labels and descriptions for the UI.
Admin access is scoped: **super admins** can manage any store, while **platform admins** can only manage stores within their assigned platforms (validated via `StorePlatform` table).
### Enforcement Points
@@ -255,6 +267,7 @@ A user can have **different roles in different stores**. For example, a user mig
## Related Documentation
- [Authentication & RBAC](auth-rbac.md) — JWT auth, user roles, enforcement methods
- [Store RBAC](../backend/store-rbac.md) — Custom role CRUD, permission catalog API, admin role management
- [Menu Management](menu-management.md) — Menu discovery, visibility config, AdminMenuConfig
- [Module System](module-system.md) — Module architecture, auto-discovery, classification
- [Feature Gating](../implementation/feature-gating-system.md) — Tier-based feature limits

View File

@@ -121,6 +121,52 @@ async def update_merchant_endpoint(merchant_id: int, data: MerchantUpdate, db: S
**Status:** 📝 **ACCEPTED** - Inline styles OK for admin pages
### Category 6: Cross-Module Model Imports (HIGH Priority)
**Violation:** MOD-025 - Modules importing and querying models from other modules
**Date Added:** 2026-02-26
**Total Violations:** ~84 (services and route files, excluding tests and type-hints)
**Subcategories:**
| Cat | Description | Count | Priority |
|-----|-------------|-------|----------|
| 1 | Direct queries on another module's models | ~47 | URGENT |
| 2 | Creating instances of another module's models | ~15 | URGENT |
| 3 | Aggregation/count queries across module boundaries | ~11 | URGENT |
| 4 | Join queries involving another module's models | ~4 | URGENT |
| 5 | UserContext legacy import path (74 files) | 74 | URGENT |
**Top Violating Module Pairs:**
- `billing → tenancy`: 31 violations
- `loyalty → tenancy`: 23 violations
- `marketplace → tenancy`: 18 violations
- `core → tenancy`: 11 violations
- `cms → tenancy`: 8 violations
- `analytics → tenancy/catalog/orders`: 8 violations
- `inventory → catalog`: 3 violations
- `marketplace → catalog/orders`: 5 violations
**Resolution:** Migrate all cross-module model imports to service calls. See [Cross-Module Migration Plan](cross-module-migration-plan.md).
**Status:** :construction: **IN PROGRESS** - Migration plan created, executing per-module
### Category 7: Provider Pattern Gaps (MEDIUM Priority — Incremental)
**Violation:** Modules with data that should be exposed via providers but aren't
**Date Added:** 2026-02-26
| Provider | Implementing | Should Add |
|----------|-------------|------------|
| MetricsProvider | 8 modules | loyalty, payments, analytics |
| WidgetProvider | 2 modules (marketplace, tenancy) | orders, billing, catalog, inventory, loyalty |
| AuditProvider | 1 module (monitoring) | OK — single backend is the design |
**Status:** :memo: **PLANNED** - Will add incrementally as we work on each module
## Architecture Validation Philosophy
### What We Enforce Strictly:
@@ -149,6 +195,9 @@ async def update_merchant_endpoint(merchant_id: int, data: MerchantUpdate, db: S
| Service patterns | ~50 | Medium | 📝 Incremental |
| Simple queries in endpoints | ~10 | Low | 📝 Case-by-case |
| Template inline styles | ~110 | Low | ✅ Accepted |
| **Cross-module model imports** | **~84** | **High** | **🔄 Migrating** |
| **UserContext legacy path** | **74** | **High** | **🔄 Migrating** |
| **Provider pattern gaps** | **~8** | **Medium** | **📝 Incremental** |
## Validation Command
@@ -164,7 +213,14 @@ python scripts/validate/validate_architecture.py
- [x] Add comments to intentional violations
### Short Term (Next Sprint)
- [ ] Move UserContext to tenancy.schemas, update 74 imports (Cat 5)
- [ ] Add missing service methods to tenancy for cross-module consumers (Cat 1)
- [ ] Migrate direct model queries to service calls (Cat 1-4)
- [ ] Create Pydantic response models for top 10 endpoints
### Medium Term
- [ ] Add widget providers to orders, billing, catalog, inventory, loyalty (P5)
- [ ] Add metrics providers to loyalty, payments (P5)
- [ ] Refactor 2-3 services to use dependency injection
- [ ] Move complex queries to service layer

View File

@@ -2,15 +2,29 @@
This document defines the strict import rules that ensure the module system remains decoupled, testable, and resilient. These rules are critical for maintaining a truly modular architecture.
## Core Principle
## Core Principles
**Core modules NEVER import from optional modules.**
### Principle 1: Core modules NEVER import from optional modules
This is the fundamental rule that enables optional modules to be truly optional. When a core module imports from an optional module:
- The app crashes if that module is disabled
- You can't test core functionality in isolation
- You create a hidden dependency that violates the architecture
### Principle 2: Services over models — NEVER import another module's models
**If module A needs data from module B, it MUST call module B's service methods.**
Modules must NEVER import and query another module's SQLAlchemy models directly. This applies to ALL cross-module interactions — core-to-core, optional-to-core, and optional-to-optional.
When a module imports another module's models, it:
- Couples to the internal schema (column names, relationships, table structure)
- Bypasses business logic, validation, and access control in the owning service
- Makes refactoring the model owner's schema a breaking change for all consumers
- Scatters query logic across multiple modules instead of centralizing it
**The owning module's service is the ONLY authorized gateway to its data.**
## Module Classification
### Core Modules (Always Enabled)
@@ -35,30 +49,70 @@ This is the fundamental rule that enables optional modules to be truly optional.
## Import Rules Matrix
| From \ To | Core | Optional | Contracts |
|-----------|------|----------|-----------|
| **Core** | :white_check_mark: | :x: FORBIDDEN | :white_check_mark: |
| **Optional** | :white_check_mark: | :warning: With care | :white_check_mark: |
| **Contracts** | :x: | :x: | :white_check_mark: |
| From \ To | Core Services | Core Models | Optional Services | Optional Models | Contracts |
|-----------|--------------|-------------|-------------------|-----------------|-----------|
| **Core** | :white_check_mark: | :x: Use services | :x: FORBIDDEN | :x: FORBIDDEN | :white_check_mark: |
| **Optional** | :white_check_mark: | :x: Use services | :warning: With care | :x: Use services | :white_check_mark: |
| **Contracts** | :x: | :x: | :x: | :x: | :white_check_mark: |
### Explanation
1. **Core → Core**: Allowed. Core modules can import from each other (e.g., billing imports from tenancy)
1. **Any → Any Services**: Allowed (with core→optional restriction). Import the service, call its methods.
2. **Core → Optional**: **FORBIDDEN**. This is the most important rule. Core modules must never have direct imports from optional modules.
2. **Any → Any Models**: **FORBIDDEN**. Never import another module's SQLAlchemy models. Use that module's service instead.
3. **Core → Contracts**: Allowed. Contracts define shared protocols and data structures.
3. **Core → Optional**: **FORBIDDEN** (both services and models). Use provider protocols instead.
4. **Optional → Core**: Allowed. Optional modules can use core functionality.
4. **Optional → Core Services**: Allowed. Optional modules can call core service methods.
5. **Optional → Optional**: Allowed with care. Check dependencies in `definition.py` to ensure proper ordering.
5. **Optional → Optional Services**: Allowed with care. Declare dependency in `definition.py`.
6. **Optional → Contracts**: Allowed. This is how optional modules implement protocols.
6. **Any → Contracts**: Allowed. Contracts define shared protocols and data structures.
7. **Contracts → Anything**: Contracts should only depend on stdlib/typing/Protocol. No module imports.
## Anti-Patterns (DO NOT DO)
### Cross-Module Model Import (MOD-025)
```python
# app/modules/orders/services/order_service.py
# BAD: Importing and querying another module's models
from app.modules.catalog.models import Product
class OrderService:
def get_order_with_products(self, db, order_id):
order = db.query(Order).filter_by(id=order_id).first()
# BAD: Direct query on catalog's model
products = db.query(Product).filter(Product.id.in_(product_ids)).all()
return order, products
```
```python
# GOOD: Call the owning module's service
from app.modules.catalog.services import product_service
class OrderService:
def get_order_with_products(self, db, order_id):
order = db.query(Order).filter_by(id=order_id).first()
# GOOD: Catalog service owns Product data access
products = product_service.get_products_by_ids(db, product_ids)
return order, products
```
### Cross-Module Aggregation Query
```python
# BAD: Counting another module's models directly
from app.modules.orders.models import Order
count = db.query(func.count(Order.id)).filter_by(store_id=store_id).scalar()
# GOOD: Ask the owning service
from app.modules.orders.services import order_service
count = order_service.get_order_count(db, store_id=store_id)
```
### Direct Import from Optional Module
```python
@@ -97,6 +151,36 @@ def process_import(job: MarketplaceImportJob) -> None: # Crashes if disabled
## Approved Patterns
### 0. Cross-Module Service Calls (Primary Pattern)
The default way to access another module's data is through its service layer:
```python
# app/modules/inventory/services/inventory_service.py
# GOOD: Import the service, not the model
from app.modules.catalog.services import product_service
class InventoryService:
def get_stock_for_product(self, db, product_id):
# Verify product exists via catalog service
product = product_service.get_product_by_id(db, product_id)
if not product:
raise InventoryError("Product not found")
# Query own models
return db.query(StockLevel).filter_by(product_id=product_id).first()
```
Each module should expose these standard service methods for external consumers:
| Method Pattern | Purpose |
|---------------|---------|
| `get_{entity}_by_id(db, id)` | Single entity lookup |
| `list_{entities}(db, **filters)` | Filtered list |
| `get_{entity}_count(db, **filters)` | Count query |
| `search_{entities}(db, query, **filters)` | Text search |
| `get_{entities}_by_ids(db, ids)` | Batch lookup |
### 1. Provider Protocol Pattern (Metrics & Widgets)
Use the provider protocol pattern for cross-module data:
@@ -190,6 +274,8 @@ The architecture validator (`scripts/validate/validate_architecture.py`) include
| IMPORT-001 | ERROR | Core module imports from optional module |
| IMPORT-002 | WARNING | Optional module imports from unrelated optional module |
| IMPORT-003 | INFO | Consider using protocol pattern instead of direct import |
| MOD-025 | ERROR | Module imports models from another module (use services) |
| MOD-026 | ERROR | Cross-module data access not going through service layer |
Run validation:
```bash
@@ -291,7 +377,9 @@ grep -r "from app.modules.orders" app/modules/core/ && exit 1
| Rule | Enforcement |
|------|-------------|
| Core → Optional = FORBIDDEN | Architecture validation, CI checks |
| Use Protocol pattern | Code review, documentation |
| Cross-module model imports = FORBIDDEN | MOD-025 rule, code review |
| Use services for cross-module data | MOD-026 rule, code review |
| Use Protocol pattern for core→optional | Code review, documentation |
| Lazy factory functions | Required for definition.py |
| TYPE_CHECKING imports | Required for type hints across modules |
| Registry-based discovery | Required for all cross-module access |
@@ -301,6 +389,8 @@ Following these rules ensures:
- Testing can be done in isolation
- New modules can be added without modifying core
- The app remains stable when modules fail
- Module schemas can evolve independently
- Data access logic is centralized in the owning service
## Related Documentation
@@ -308,3 +398,4 @@ Following these rules ensures:
- [Metrics Provider Pattern](metrics-provider-pattern.md) - Numeric statistics architecture
- [Widget Provider Pattern](widget-provider-pattern.md) - Dashboard widgets architecture
- [Architecture Violations Status](architecture-violations-status.md) - Current violation tracking
- [Cross-Module Migration Plan](cross-module-migration-plan.md) - Migration plan for resolving all cross-module violations

View File

@@ -0,0 +1,464 @@
# Cross-Module Import Migration Plan
**Created:** 2026-02-26
**Status:** In Progress
**Rules:** MOD-025, MOD-026
This document tracks the migration of all cross-module model imports to proper service-based access patterns.
## Overview
| Category | Description | Files | Priority | Status |
|----------|-------------|-------|----------|--------|
| Cat 5 | UserContext legacy import path | 74 | URGENT | Pending |
| Cat 1 | Direct queries on another module's models | ~47 | URGENT | Pending |
| Cat 2 | Creating instances across module boundaries | ~15 | URGENT | Pending |
| Cat 3 | Aggregation/count queries across boundaries | ~11 | URGENT | Pending |
| Cat 4 | Join queries involving another module's models | ~4 | URGENT | Pending |
| P5 | Provider pattern gaps (widgets, metrics) | ~8 modules | Incremental | Pending |
| P6 | Route variable naming standardization | ~109 files | Low | Deferred |
---
## Cat 5: Move UserContext to Tenancy Module (74 files)
**Priority:** URGENT — mechanical, low risk, high impact
**Approach:** Move definition, update all imports in one batch
### What
`UserContext` is defined in `models/schema/auth.py` (legacy location). Per MOD-019, schemas belong in their module. UserContext is a tenancy concern (user identity, platform/store context).
### Steps
1. **Move `UserContext` class** from `models/schema/auth.py` to `app/modules/tenancy/schemas/auth.py`
- Keep all properties and methods intact
- Also move related schemas used alongside it: `UserLogin`, `LogoutResponse`, `StoreUserResponse`
2. **Add re-export in legacy location** (temporary backwards compat):
```python
# models/schema/auth.py
from app.modules.tenancy.schemas.auth import UserContext # noqa: F401
```
3. **Update all 74 import sites** from:
```python
from models.schema.auth import UserContext
```
to:
```python
from app.modules.tenancy.schemas.auth import UserContext
```
4. **Remove legacy re-export** once all imports are updated
### Files to Update (by module)
**app/api/** (1 file)
- `app/api/deps.py`
**app/modules/tenancy/** (15 files)
- `routes/api/admin_merchants.py`
- `routes/api/admin_module_config.py`
- `routes/api/admin_merchant_domains.py`
- `routes/api/admin_modules.py`
- `routes/api/admin_platforms.py`
- `routes/api/admin_store_domains.py`
- `routes/api/admin_stores.py`
- `routes/api/admin_users.py`
- `routes/api/merchant.py`
- `routes/api/store_auth.py` (also imports `LogoutResponse`, `StoreUserResponse`, `UserLogin`)
- `routes/api/store_profile.py`
- `routes/api/store_team.py`
- `routes/pages/merchant.py`
- `services/admin_platform_service.py`
- `tests/integration/test_merchant_routes.py`
**app/modules/core/** (12 files)
- `routes/api/admin_dashboard.py`
- `routes/api/admin_menu_config.py`
- `routes/api/admin_settings.py`
- `routes/api/merchant_menu.py`
- `routes/api/store_dashboard.py`
- `routes/api/store_menu.py`
- `routes/api/store_settings.py`
- `routes/pages/merchant.py`
- `tests/integration/test_merchant_dashboard_routes.py`
- `tests/integration/test_merchant_menu_routes.py`
- `tests/integration/test_store_dashboard_routes.py`
**app/modules/billing/** (9 files)
- `routes/api/admin.py`
- `routes/api/admin_features.py`
- `routes/api/store.py`
- `routes/api/store_addons.py`
- `routes/api/store_checkout.py`
- `routes/api/store_features.py`
- `routes/api/store_usage.py`
- `routes/pages/merchant.py`
- `tests/integration/test_merchant_routes.py`
**app/modules/marketplace/** (8 files)
- `routes/api/admin_letzshop.py`
- `routes/api/admin_marketplace.py`
- `routes/api/admin_products.py`
- `routes/api/store_letzshop.py`
- `routes/api/store_marketplace.py`
- `routes/api/store_onboarding.py`
- `tests/integration/test_store_page_routes.py`
- `tests/unit/test_store_page_routes.py`
**app/modules/messaging/** (7 files)
- `routes/api/admin_email_templates.py`
- `routes/api/admin_messages.py`
- `routes/api/admin_notifications.py`
- `routes/api/store_email_settings.py`
- `routes/api/store_email_templates.py`
- `routes/api/store_messages.py`
- `routes/api/store_notifications.py`
**app/modules/orders/** (6 files)
- `routes/api/admin.py`
- `routes/api/admin_exceptions.py`
- `routes/api/store.py`
- `routes/api/store_customer_orders.py`
- `routes/api/store_exceptions.py`
- `routes/api/store_invoices.py`
**app/modules/monitoring/** (6 files)
- `routes/api/admin_audit.py`
- `routes/api/admin_code_quality.py`
- `routes/api/admin_logs.py`
- `routes/api/admin_platform_health.py`
- `routes/api/admin_tasks.py`
- `routes/api/admin_tests.py`
**app/modules/cms/** (4 files)
- `routes/api/admin_images.py`
- `routes/api/admin_media.py`
- `routes/api/admin_store_themes.py`
- `routes/api/store_media.py`
**app/modules/catalog/** (2 files)
- `routes/api/admin.py`
- `routes/api/store.py`
**app/modules/customers/** (2 files)
- `routes/api/admin.py`
- `routes/api/store.py`
**app/modules/inventory/** (2 files)
- `routes/api/admin.py`
- `routes/api/store.py`
**app/modules/loyalty/** (2 files)
- `routes/pages/merchant.py`
- `tests/conftest.py`
**app/modules/payments/** (1 file)
- `routes/api/store.py`
**tests/** (1 file)
- `tests/unit/api/test_deps.py`
**docs/** (2 files — update code examples)
- `docs/architecture/user-context-pattern.md`
- `docs/proposals/decouple-modules.md`
---
## Cat 1: Direct Queries → Service Methods (~47 violations)
**Priority:** URGENT — requires creating new service methods first
**Approach:** Per-module, add service methods then migrate consumers
### Required New Service Methods
#### Tenancy Module (most consumed)
The tenancy module needs these public service methods for cross-module consumers:
```python
# app/modules/tenancy/services/merchant_service.py (new or extend existing)
class MerchantService:
def get_merchant_by_id(self, db, merchant_id) -> Merchant | None
def get_merchant_by_owner_id(self, db, owner_user_id) -> Merchant | None
def get_merchants_for_platform(self, db, platform_id, **filters) -> list[Merchant]
def get_merchant_count(self, db, platform_id=None) -> int
def search_merchants(self, db, query, platform_id=None) -> list[Merchant]
# app/modules/tenancy/services/store_service.py (extend existing)
class StoreService:
def get_store_by_id(self, db, store_id) -> Store | None
def get_stores_for_merchant(self, db, merchant_id) -> list[Store]
def get_stores_for_platform(self, db, platform_id, **filters) -> list[Store]
def get_store_count(self, db, merchant_id=None, platform_id=None) -> int
def get_active_store_count(self, db, platform_id) -> int
# app/modules/tenancy/services/platform_service.py (extend existing)
class PlatformService:
def get_platform_by_id(self, db, platform_id) -> Platform | None
def list_platforms(self, db) -> list[Platform]
# app/modules/tenancy/services/user_service.py (extend existing)
class UserService:
def get_user_by_id(self, db, user_id) -> User | None
def get_user_by_email(self, db, email) -> User | None
def get_store_users(self, db, store_id) -> list[StoreUser]
```
#### Catalog Module
```python
# app/modules/catalog/services/product_service.py (extend existing)
class ProductService:
def get_product_by_id(self, db, product_id) -> Product | None
def get_products_by_ids(self, db, product_ids) -> list[Product]
def get_product_count(self, db, store_id=None) -> int
```
#### Orders Module
```python
# app/modules/orders/services/order_service.py (extend existing)
class OrderService:
def get_order_by_id(self, db, order_id) -> Order | None
def get_order_count(self, db, store_id=None, **filters) -> int
def get_orders_for_store(self, db, store_id, **filters) -> list[Order]
```
### Migration Per Consuming Module
#### billing → tenancy (13 direct queries)
| File | What it queries | Replace with |
|------|----------------|--------------|
| `services/admin_subscription_service.py:279` | `db.query(Platform)` | `platform_service.get_platform_by_id()` |
| `services/admin_subscription_service.py:285` | `db.query(Store)` | `store_service.get_store_by_id()` |
| `services/admin_subscription_service.py:362` | `db.query(Store).filter()` | `store_service.get_stores_for_merchant()` |
| `services/billing_service.py:158` | `db.query(Store)` | `store_service.get_store_by_id()` |
| `services/billing_service.py:497` | `db.query(Merchant)` | `merchant_service.get_merchant_by_id()` |
| `services/feature_service.py:118,145` | `db.query(StorePlatform)` | `store_service.get_store_platform()` |
| `services/store_platform_sync_service.py:14` | `db.query(StorePlatform)` | `store_service` methods |
| `services/stripe_service.py:26,297,316` | `db.query(Merchant/Store)` | `merchant_service/store_service` |
| `services/subscription_service.py:56,74,178,191` | `db.query(Platform/Store)` | service methods |
| `services/usage_service.py:22` | `db.query(Store)` | `store_service` |
#### loyalty → tenancy (10 direct queries)
| File | What it queries | Replace with |
|------|----------------|--------------|
| `services/card_service.py` | `db.query(Store/User)` | `store_service/user_service` |
| `services/program_service.py` | `db.query(Merchant)` | `merchant_service` |
| `services/admin_loyalty_service.py` | `db.query(Merchant)` | `merchant_service` |
#### marketplace → tenancy (5 direct queries)
| File | What it queries | Replace with |
|------|----------------|--------------|
| `services/letzshop/order_service.py:76` | `db.query(Store)` | `store_service.get_store_by_id()` |
| `services/letzshop/order_service.py:100,110` | `db.query(Order)` | `order_service.get_order_count()` |
| `services/marketplace_metrics.py:203` | `db.query(StorePlatform)` | `store_service.get_store_count()` |
#### core → tenancy (3 direct queries)
| File | What it queries | Replace with |
|------|----------------|--------------|
| `services/auth_service.py:156` | `db.query(Merchant)` | `merchant_service.get_merchant_by_owner_id()` |
| `services/auth_service.py:25` | `db.query(Store)` | `store_service.get_store_by_id()` |
| `services/menu_service.py:351` | `db.query(Store).join()` | `store_service.get_stores_for_merchant()` |
#### analytics → tenancy/catalog (4 direct queries)
| File | What it queries | Replace with |
|------|----------------|--------------|
| `services/stats_service.py:69` | `db.query(Product)` | `product_service.get_product_count()` |
| `services/stats_service.py:75` | `db.query(Product)` | `product_service` methods |
| `services/stats_service.py:88-92` | `db.query(MarketplaceProduct)` | marketplace service |
| `services/stats_service.py:229` | `db.query(Product)` aggregation | `product_service` |
#### Other modules (various)
| Module | File | Replace with |
|--------|------|--------------|
| `inventory` | `services/inventory_transaction_service.py:236` | `order_service.get_order_by_id()` |
| `cms` | `services/cms_features.py:160` | `store_service.get_store_count()` |
| `customers` | `services/admin_customer_service.py:16` | `store_service.get_store_by_id()` |
---
## Cat 2: Model Creation Across Boundaries (~15 violations)
**Priority:** URGENT
**Approach:** Add create/factory methods to owning services
Most of these are in **test fixtures** (acceptable exception) but a few are in production code:
### Production Code Violations
| File | Creates | Replace with |
|------|---------|--------------|
| `contracts/audit.py:117` | `AdminAuditLog()` | Use audit provider `log_action()` |
| `billing/services/store_platform_sync_service.py` | `StorePlatform()` | `store_service.create_store_platform()` |
| `marketplace/services/letzshop/order_service.py` | `Order/OrderItem()` | `order_service.create_order()` |
### Test Fixtures (Acceptable — Document as Exception)
Test files creating models from other modules for integration test setup is acceptable but should be documented:
```python
# ACCEPTABLE in tests — document with comment:
# Test fixture: creates tenancy models for integration test setup
merchant = Merchant(name="Test", ...)
```
---
## Cat 3: Aggregation/Count Queries (~11 violations)
**Priority:** URGENT
**Approach:** Add count/stats methods to owning services
| File | Query | Replace with |
|------|-------|--------------|
| `marketplace/services/letzshop/order_service.py:100` | `func.count(Order.id)` | `order_service.get_order_count()` |
| `marketplace/services/letzshop/order_service.py:110` | `func.count(Order.id)` | `order_service.get_order_count()` |
| `catalog/services/catalog_features.py:92` | `StorePlatform` count | `store_service.get_active_store_count()` |
| `cms/services/cms_features.py:160` | `StorePlatform` count | `store_service.get_active_store_count()` |
| `marketplace/services/marketplace_metrics.py:203` | `StorePlatform` count | `store_service.get_active_store_count()` |
| `customers/services/customer_features.py` | Store count | `store_service.get_store_count()` |
| `inventory/services/inventory_features.py` | Store count | `store_service.get_store_count()` |
| `orders/services/order_features.py` | Store count | `store_service.get_store_count()` |
---
## Cat 4: Join Queries (~4 violations)
**Priority:** URGENT
**Approach:** Decompose into service calls or add service methods
| File | Join | Resolution |
|------|------|------------|
| `catalog/services/store_product_service.py:19` | `.join(Store)` | Use `store_service.get_store_by_id()` + own query |
| `core/services/menu_service.py:351` | `.join(Store)` | `store_service.get_stores_for_merchant()` |
| `customers/services/admin_customer_service.py:16` | `.join(Store)` | `store_service.get_store_by_id()` + own query |
| `messaging/services/messaging_service.py:653` | `.join(StoreUser)` | `user_service.get_store_users()` + filter |
---
## P5: Provider Pattern Gaps (Incremental)
**Priority:** MEDIUM — implement as each module is touched
### Widget Providers to Add
Currently only 2 modules (marketplace, tenancy) provide dashboard widgets. These modules have valuable dashboard data:
| Module | Widget Ideas | Implementation |
|--------|-------------|----------------|
| **orders** | Recent orders list, order status breakdown | `services/order_widgets.py` |
| **billing** | Subscription status, revenue trend | `services/billing_widgets.py` |
| **catalog** | Product summary, category breakdown | `services/catalog_widgets.py` |
| **inventory** | Stock summary, low-stock alerts | `services/inventory_widgets.py` |
| **loyalty** | Program stats, member engagement | `services/loyalty_widgets.py` |
| **customers** | Customer growth, segments | `services/customer_widgets.py` |
### Metrics Providers to Add
| Module | Metric Ideas | Implementation |
|--------|-------------|----------------|
| **loyalty** | Active programs, total enrollments, points issued | `services/loyalty_metrics.py` |
| **payments** | Transaction volume, success rate, gateway stats | `services/payment_metrics.py` |
| **analytics** | Report count, active dashboards | `services/analytics_metrics.py` |
### Implementation Template
```python
# app/modules/{module}/services/{module}_widgets.py
from app.modules.contracts.widgets import (
DashboardWidgetProviderProtocol,
ListWidget,
BreakdownWidget,
)
class {Module}WidgetProvider:
@property
def widget_category(self) -> str:
return "{module}"
def get_store_widgets(self, db, store_id, context=None):
# Return list of ListWidget/BreakdownWidget
...
def get_platform_widgets(self, db, platform_id, context=None):
...
{module}_widget_provider = {Module}WidgetProvider()
```
Register in `definition.py`:
```python
def _get_widget_provider():
from app.modules.{module}.services.{module}_widgets import {module}_widget_provider
return {module}_widget_provider
{module}_module = ModuleDefinition(
...
widget_provider=_get_widget_provider,
)
```
---
## P6: Route Variable Naming (Deferred)
**Priority:** LOW — cosmetic, no functional impact
**Count:** ~109 files use `admin_router`/`store_router` instead of `router`
Per MOD-010, route files should export a `router` variable. Many files use `admin_router` or `store_router` instead. The route discovery system currently handles both patterns.
**Decision:** Defer to a future cleanup sprint. This is purely naming consistency and has no architectural impact.
---
## Execution Order
### Phase 1: Foundation (Do First)
1. **Cat 5**: Move UserContext to `tenancy.schemas.auth` — mechanical, enables clean imports
2. **Add service methods to tenancy** — most modules depend on tenancy, need methods first
### Phase 2: High-Impact Migrations (URGENT)
3. **Cat 1 - billing→tenancy**: 13 violations, highest count
4. **Cat 1 - loyalty→tenancy**: 10 violations
5. **Cat 1 - marketplace→tenancy/catalog/orders**: 10 violations
6. **Cat 1 - core→tenancy**: 3 violations
7. **Cat 1 - analytics→tenancy/catalog**: 4 violations
### Phase 3: Remaining Migrations (URGENT)
8. **Cat 2**: Model creation violations (3 production files)
9. **Cat 3**: All aggregation queries (11 files)
10. **Cat 4**: All join queries (4 files)
11. **Cat 1**: Remaining modules (cms, customers, inventory, messaging, monitoring)
### Phase 4: Provider Enrichment (Incremental)
12. **P5**: Add widget providers to orders, billing, catalog (highest value)
13. **P5**: Add metrics providers to loyalty, payments
14. **P5**: Add remaining widget providers as modules are touched
### Phase 5: Cleanup (Deferred)
15. **P6**: Route variable naming standardization
---
## Testing Strategy
For each migration:
1. Add/verify service method has a unit test
2. Run existing integration tests to confirm no regressions
3. Run `python scripts/validate/validate_architecture.py` to verify violation count decreases
## Related Documentation
- [Cross-Module Import Rules](cross-module-import-rules.md) — The rules being enforced
- [Architecture Violations Status](architecture-violations-status.md) — Violation tracking
- [Module System Architecture](module-system.md) — Module structure reference

View File

@@ -699,4 +699,146 @@ Marketing (7 permissions, specialized)
---
## Custom Role Management
### Overview
Store owners can create, edit, and delete custom roles with granular permission selection. Preset roles (manager, staff, support, viewer, marketing) cannot be deleted but can be edited.
### Store Role CRUD API
All endpoints require **store owner** authentication.
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/store/team/roles` | List all roles (creates defaults if none exist) |
| `POST` | `/api/v1/store/team/roles` | Create a custom role |
| `PUT` | `/api/v1/store/team/roles/{id}` | Update a role's name/permissions |
| `DELETE` | `/api/v1/store/team/roles/{id}` | Delete a custom role |
**Validation rules:**
- Cannot create/rename a role to a preset name (manager, staff, etc.)
- Cannot delete preset roles
- Cannot delete a role with assigned team members
- All permission IDs are validated against the module-discovered permission catalog
### Permission Catalog API
Returns all available permissions grouped by category, with human-readable labels and descriptions.
```
GET /api/v1/store/team/permissions/catalog
```
**Required Permission:** `team.view`
**Response:**
```json
{
"categories": [
{
"id": "team",
"label": "tenancy.permissions.category.team",
"permissions": [
{
"id": "team.view",
"label": "tenancy.permissions.team_view",
"description": "tenancy.permissions.team_view_desc",
"is_owner_only": false
}
]
}
]
}
```
Permissions are discovered from all module `definition.py` files via `PermissionDiscoveryService.get_permissions_by_category()`.
### Role Editor UI
The role editor page at `/store/{store_code}/team/roles`:
- Lists all roles (preset + custom) with permission counts
- Modal for creating/editing roles with a permission matrix
- Permissions displayed with labels, IDs, and hover descriptions
- Category-level "Select All / Deselect All" toggles
- Owner-only permissions marked with an "Owner" badge
**Alpine.js component:** `storeRoles()` in `app/modules/tenancy/static/store/js/roles.js`
**Menu location:** Account section > Roles (requires `team.view` permission)
---
## Admin Store Roles Management
### Overview
Super admins and platform admins can manage roles for any store via the admin panel. Platform admins are scoped to stores within their assigned platforms.
### Admin Role CRUD API
All endpoints require **admin authentication** (`get_current_admin_api`).
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/admin/store-roles?store_id=X` | List roles for a store |
| `GET` | `/api/v1/admin/store-roles/permissions/catalog` | Permission catalog |
| `POST` | `/api/v1/admin/store-roles?store_id=X` | Create a role |
| `PUT` | `/api/v1/admin/store-roles/{id}?store_id=X` | Update a role |
| `DELETE` | `/api/v1/admin/store-roles/{id}?store_id=X` | Delete a role |
### Platform Admin Scoping
Platform admins can only access stores that belong to one of their assigned platforms:
```python
# In StoreTeamService.validate_admin_store_access():
# 1. Super admin (accessible_platform_ids is None) → access all stores
# 2. Platform admin → store must exist in StorePlatform where
# platform_id is in the admin's accessible_platform_ids
```
The scoping is enforced at the service layer via `validate_admin_store_access()`, called by every admin endpoint before performing operations.
### Admin UI
Page at `/admin/store-roles`:
- Tom Select store search/selector (shared `initStoreSelector()` component)
- Platform admins see only stores in their assigned platforms
- Same role CRUD and permission matrix as the store-side UI
- Located in the "User Management" admin menu section
**Alpine.js component:** `adminStoreRoles()` in `app/modules/tenancy/static/admin/js/store-roles.js`
### Audit Trail
All role operations are logged via `AuditAggregatorService`:
| Action | Description |
|--------|-------------|
| `role.create` | Custom role created |
| `role.update` | Role name or permissions modified |
| `role.delete` | Custom role deleted |
| `member.role_change` | Team member assigned a different role |
| `member.invite` | Team member invited |
| `member.remove` | Team member removed |
---
## Key Files
| File | Purpose |
|------|---------|
| `app/modules/tenancy/services/store_team_service.py` | Role CRUD, platform scoping, audit trail |
| `app/modules/tenancy/services/permission_discovery_service.py` | Permission catalog, role presets |
| `app/modules/tenancy/routes/api/store_team.py` | Store team & role API endpoints |
| `app/modules/tenancy/routes/api/admin_store_roles.py` | Admin store role API endpoints |
| `app/modules/tenancy/schemas/team.py` | Request/response schemas |
| `app/modules/tenancy/static/store/js/roles.js` | Store role editor Alpine.js component |
| `app/modules/tenancy/static/admin/js/store-roles.js` | Admin role editor Alpine.js component |
| `app/modules/tenancy/templates/tenancy/store/roles.html` | Store role editor template |
| `app/modules/tenancy/templates/tenancy/admin/store-roles.html` | Admin role editor template |
---
This RBAC system provides flexible, secure access control for store dashboards with clear separation between owners and team members.