feat(roles): add admin store roles page, permission i18n, and menu integration
Some checks failed
Some checks failed
- 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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
464
docs/architecture/cross-module-migration-plan.md
Normal file
464
docs/architecture/cross-module-migration-plan.md
Normal 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
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user