refactor: fix architecture violations with provider patterns and dependency inversion

Major changes:
- Add AuditProvider protocol for cross-module audit logging
- Move customer order operations to orders module (dependency inversion)
- Add customer order metrics via MetricsProvider pattern
- Fix missing db parameter in get_admin_context() calls
- Move ProductMedia relationship to catalog module (proper ownership)
- Add marketplace breakdown stats to marketplace_widgets

New files:
- contracts/audit.py - AuditProviderProtocol
- core/services/audit_aggregator.py - Aggregates audit providers
- monitoring/services/audit_provider.py - Monitoring audit implementation
- orders/services/customer_order_service.py - Customer order operations
- orders/routes/api/vendor_customer_orders.py - Customer order endpoints
- catalog/services/product_media_service.py - Product media service
- Architecture documentation for patterns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 21:32:32 +01:00
parent bd43e21940
commit 39dff4ab7d
34 changed files with 2751 additions and 407 deletions

View File

@@ -0,0 +1,265 @@
# Audit Provider Pattern
The audit provider pattern enables modules to provide audit logging backends without creating cross-module dependencies. This allows the monitoring module to be truly optional while still providing audit logging when enabled.
## Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ Admin Action Request │
│ (Settings update, User management, etc.) │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ AuditAggregatorService │
│ (app/modules/core/services/audit_aggregator.py) │
│ │
│ • Discovers AuditProviders from all registered modules │
│ • Calls log_action() on all available providers │
│ • Handles graceful degradation if no providers available │
└─────────────────────────────────────────────────────────────────────┘
┌───────────────┼───────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ DatabaseAudit │ │ (Future) │ │ (Future) │
│ Provider │ │ FileProvider │ │ CloudWatch │
│ (monitoring) │ │ │ │ │
└───────────────┘ └───────────────┘ └───────────────┘
┌───────────────┐
│AdminAuditLog │
│ (table) │
└───────────────┘
```
## Problem Solved
Before this pattern, core modules had **hard imports** from the monitoring module:
```python
# BAD: Core module with hard dependency on optional/internal module
from app.modules.monitoring.services.admin_audit_service import admin_audit_service
def create_setting(...):
result = settings_service.create_setting(...)
admin_audit_service.log_action(...) # Crashes if monitoring disabled!
```
This violated the architecture rule: **Core modules cannot depend on optional modules.**
## Solution: Protocol-Based Audit Logging
The monitoring module implements `AuditProviderProtocol` and registers it in its `definition.py`. The `AuditAggregatorService` in core discovers and uses audit providers from all modules.
## Key Components
### 1. AuditEvent Dataclass
Standard structure for audit events:
```python
# app/modules/contracts/audit.py
from dataclasses import dataclass
from typing import Any
@dataclass
class AuditEvent:
admin_user_id: int # ID of admin performing action
action: str # Action name (e.g., "create_setting")
target_type: str # Target type (e.g., "setting", "user")
target_id: str # Target identifier
details: dict[str, Any] | None = None # Additional context
ip_address: str | None = None # Admin's IP address
user_agent: str | None = None # Browser user agent
request_id: str | None = None # Request correlation ID
```
### 2. AuditProviderProtocol
Protocol that modules implement:
```python
# app/modules/contracts/audit.py
from typing import Protocol, runtime_checkable
@runtime_checkable
class AuditProviderProtocol(Protocol):
@property
def audit_backend(self) -> str:
"""Backend name (e.g., 'database', 'file', 'cloudwatch')."""
...
def log_action(self, db: Session, event: AuditEvent) -> bool:
"""Log an audit event. Returns True on success."""
...
```
### 3. AuditAggregatorService
The aggregator in core that discovers and uses providers:
```python
# app/modules/core/services/audit_aggregator.py
class AuditAggregatorService:
def _get_enabled_providers(self, db: Session):
"""Discover audit providers from registered modules."""
from app.modules.registry import MODULES
for module in MODULES.values():
if module.has_audit_provider():
provider = module.get_audit_provider_instance()
if provider:
yield (module, provider)
def log_action(self, db: Session, event: AuditEvent) -> bool:
"""Log to all available providers."""
providers = list(self._get_enabled_providers(db))
if not providers:
# No providers - acceptable, audit is optional
return False
any_success = False
for module, provider in providers:
try:
if provider.log_action(db, event):
any_success = True
except Exception as e:
logger.warning(f"Audit provider {module.code} failed: {e}")
return any_success
def log(self, db, admin_user_id, action, target_type, target_id, **kwargs):
"""Convenience method with individual parameters."""
event = AuditEvent(
admin_user_id=admin_user_id,
action=action,
target_type=target_type,
target_id=str(target_id),
**kwargs
)
return self.log_action(db, event)
```
## Implementing a Provider
### Step 1: Create the Provider
```python
# app/modules/monitoring/services/audit_provider.py
from app.modules.contracts.audit import AuditEvent, AuditProviderProtocol
from app.modules.tenancy.models import AdminAuditLog
class DatabaseAuditProvider:
@property
def audit_backend(self) -> str:
return "database"
def log_action(self, db: Session, event: AuditEvent) -> bool:
try:
audit_log = AdminAuditLog(
admin_user_id=event.admin_user_id,
action=event.action,
target_type=event.target_type,
target_id=event.target_id,
details=event.details or {},
ip_address=event.ip_address,
user_agent=event.user_agent,
request_id=event.request_id,
)
db.add(audit_log)
db.flush()
return True
except Exception as e:
logger.error(f"Failed to log audit: {e}")
return False
audit_provider = DatabaseAuditProvider()
```
### Step 2: Register in Module Definition
```python
# app/modules/monitoring/definition.py
def _get_audit_provider():
"""Lazy import to avoid circular imports."""
from app.modules.monitoring.services.audit_provider import audit_provider
return audit_provider
monitoring_module = ModuleDefinition(
code="monitoring",
name="Platform Monitoring",
audit_provider=_get_audit_provider, # Register the provider
...
)
```
### Step 3: Use via Aggregator
```python
# app/modules/core/routes/api/admin_settings.py
from app.modules.core.services.audit_aggregator import audit_aggregator
@router.post("/settings")
def create_setting(setting_data: SettingCreate, ...):
result = settings_service.create_setting(db, setting_data)
# Log via aggregator - works even if monitoring is disabled
audit_aggregator.log(
db=db,
admin_user_id=current_admin.id,
action="create_setting",
target_type="setting",
target_id=setting_data.key,
details={"category": setting_data.category},
)
db.commit()
return result
```
## Graceful Degradation
When no audit providers are registered (e.g., monitoring module disabled):
- `audit_aggregator.log()` returns `False`
- No exceptions are raised
- The operation continues normally
- A debug log message notes the absence of providers
This ensures core functionality works regardless of whether auditing is available.
## Multiple Backends
The pattern supports multiple audit backends simultaneously:
```python
# All registered providers receive the event
audit_aggregator.log(...)
# Provider 1: Database (AdminAuditLog table)
# Provider 2: CloudWatch Logs (if aws-monitoring module enabled)
# Provider 3: File-based audit log (if file-audit module enabled)
```
## Benefits
1. **Decoupling**: Core modules don't depend on monitoring
2. **Optional Auditing**: Monitoring can be disabled without breaking the app
3. **Extensibility**: Easy to add new audit backends (file, cloud, SIEM)
4. **Testability**: Can mock audit providers in tests
5. **Fault Tolerance**: One provider failure doesn't affect others
## Related Documentation
- [Cross-Module Import Rules](cross-module-import-rules.md) - Import restrictions
- [Metrics Provider Pattern](metrics-provider-pattern.md) - Similar pattern for statistics
- [Widget Provider Pattern](widget-provider-pattern.md) - Similar pattern for dashboard widgets
- [Module System Architecture](module-system.md) - Module structure and auto-discovery

View File

@@ -0,0 +1,345 @@
# Customer-Orders Architecture
This document describes the consumer-agnostic customer architecture, following the same pattern as [Media Architecture](media-architecture.md).
## Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ CONSUMER MODULES │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Orders │ │ Loyalty │ │ Future │ │
│ │ │ │ (future) │ │ Module │ │
│ │ Order model │ │LoyaltyPoints│ │ XxxCustomer │ │
│ │ (customer_id)│ │(customer_id)│ │ │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Customers Module │ │
│ │ │ │
│ │ Customer (generic, consumer-agnostic storage) │ │
│ │ CustomerService (CRUD, authentication, profile management) │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
## Design Principles
### 1. Consumer-Agnostic Customer Storage
The customers module provides **generic customer storage** without knowing what entities will reference customers:
- `Customer` stores customer data (email, name, addresses, preferences)
- `CustomerService` handles CRUD, authentication, and profile management
- Customers module has **no knowledge** of orders, loyalty points, or any specific consumers
### 2. Consumer-Owned Relationships
Each module that references customers defines its **own relationship**:
- **Orders**: `Order.customer_id` links orders to customers
- **Future Loyalty**: Would define `LoyaltyPoints.customer_id`
- **Future Subscriptions**: Would define `Subscription.customer_id`
This follows the principle: **The consumer knows what it needs, the provider doesn't need to know who uses it.**
### 3. Correct Dependency Direction
```
WRONG (Hidden Dependency):
Customers → Orders (customers imports Order model)
CORRECT:
Orders → Customers (orders references Customer via FK)
```
Optional modules (orders) depend on core modules (customers), never the reverse.
## Key Components
### Customer Model (Customers Module)
```python
# app/modules/customers/models/customer.py
class Customer(Base, TimestampMixin):
"""Generic customer - consumer-agnostic."""
__tablename__ = "customers"
id = Column(Integer, primary_key=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
# Authentication
email = Column(String(255), nullable=False)
hashed_password = Column(String(255))
# Profile
first_name = Column(String(100))
last_name = Column(String(100))
phone = Column(String(50))
customer_number = Column(String(50), unique=True)
# Preferences
marketing_consent = Column(Boolean, default=False)
preferred_language = Column(String(10))
# Status
is_active = Column(Boolean, default=True)
# Note: Consumer-specific relationships (orders, loyalty points, etc.)
# are defined in their respective modules. Customers module doesn't
# know about specific consumers.
```
### CustomerService (Customers Module)
The `CustomerService` provides generic operations:
```python
# app/modules/customers/services/customer_service.py
class CustomerService:
"""Generic customer operations - consumer-agnostic."""
def create_customer(self, db, vendor_id, customer_data):
"""Create a new customer."""
...
def get_customer(self, db, vendor_id, customer_id):
"""Get a customer by ID."""
...
def update_customer(self, db, vendor_id, customer_id, customer_data):
"""Update customer profile."""
...
def login_customer(self, db, vendor_id, email, password):
"""Authenticate a customer."""
...
# Note: Customer order methods have been moved to the orders module.
# Use orders.services.customer_order_service for order-related operations.
```
### Order Model (Orders Module)
```python
# app/modules/orders/models/order.py
class Order(Base, TimestampMixin):
"""Order with customer reference - orders owns the relationship."""
__tablename__ = "orders"
id = Column(Integer, primary_key=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
# Customer reference - orders module owns this relationship
customer_id = Column(Integer, ForeignKey("customers.id"))
# Order data
order_number = Column(String(50), unique=True)
status = Column(String(20), default="pending")
total_cents = Column(Integer)
...
# Relationship to customer
customer = relationship("Customer", lazy="joined")
```
### CustomerOrderService (Orders Module)
```python
# app/modules/orders/services/customer_order_service.py
class CustomerOrderService:
"""Customer-order operations - owned by orders module."""
def get_customer_orders(self, db, vendor_id, customer_id, skip=0, limit=50):
"""Get orders for a specific customer."""
...
def get_recent_orders(self, db, vendor_id, customer_id, limit=5):
"""Get recent orders for a customer."""
...
def get_order_count(self, db, vendor_id, customer_id):
"""Get total order count for a customer."""
...
```
### Customer Order Metrics (Orders Module)
Order statistics for customers use the MetricsProvider pattern:
```python
# app/modules/orders/services/order_metrics.py
class OrderMetricsProvider:
"""Metrics provider including customer-level order metrics."""
def get_customer_order_metrics(self, db, vendor_id, customer_id, context=None):
"""
Get order metrics for a specific customer.
Returns MetricValue objects for:
- total_orders: Total orders placed
- total_spent: Total amount spent
- avg_order_value: Average order value
- last_order_date: Date of most recent order
- first_order_date: Date of first order
"""
...
```
## API Endpoints
### Customers Module Endpoints
Customer CRUD operations (no order data):
```
GET /api/vendor/customers → List customers
GET /api/vendor/customers/{id} → Customer details (no order stats)
PUT /api/vendor/customers/{id} → Update customer
PUT /api/vendor/customers/{id}/status → Toggle active status
```
### Orders Module Endpoints
Customer order data (owned by orders):
```
GET /api/vendor/customers/{id}/orders → Customer's order history
GET /api/vendor/customers/{id}/order-stats → Customer's order statistics
```
## Adding Customer References to a New Module
When creating a module that references customers (e.g., a loyalty module):
### Step 1: Reference Customer via Foreign Key
```python
# app/modules/loyalty/models/loyalty_points.py
class LoyaltyPoints(Base):
"""Loyalty points - owned by loyalty module."""
__tablename__ = "loyalty_points"
id = Column(Integer, primary_key=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"))
# Reference to customer - loyalty module owns this
customer_id = Column(Integer, ForeignKey("customers.id"))
points_balance = Column(Integer, default=0)
tier = Column(String(20), default="bronze")
# Relationship
customer = relationship("Customer", lazy="joined")
```
### Step 2: Create Your Service
```python
# app/modules/loyalty/services/customer_loyalty_service.py
class CustomerLoyaltyService:
"""Customer loyalty operations - owned by loyalty module."""
def get_customer_points(self, db, vendor_id, customer_id):
"""Get loyalty points for a customer."""
...
def add_points(self, db, vendor_id, customer_id, points, reason):
"""Add points to customer's balance."""
...
```
### Step 3: Add Routes in Your Module
```python
# app/modules/loyalty/routes/api/vendor.py
@router.get("/customers/{customer_id}/loyalty")
def get_customer_loyalty(customer_id: int, ...):
"""Get loyalty information for a customer."""
return loyalty_service.get_customer_points(db, vendor_id, customer_id)
```
## Benefits of This Architecture
1. **Module Independence**: Orders can be disabled without affecting customers
2. **Extensibility**: New modules easily reference customers
3. **No Hidden Dependencies**: Dependencies flow in one direction
4. **Clean Separation**: Customers handles identity, consumers handle their domain
5. **Testability**: Can test customers without any consumer modules
6. **Single Responsibility**: Each module owns its domain
## Anti-Patterns to Avoid
### Don't: Import Consumer Models in Customers
```python
# BAD - Creates hidden dependency
class CustomerService:
def get_customer_orders(self, db, customer_id):
from app.modules.orders.models import Order # Wrong!
return db.query(Order).filter(Order.customer_id == customer_id).all()
```
### Don't: Add Consumer-Specific Fields to Customer
```python
# BAD - Customer shouldn't know about orders
class Customer(Base):
# These create coupling to orders module
total_orders = Column(Integer) # Wrong approach
last_order_date = Column(DateTime) # Wrong approach
```
Instead, query order data from the orders module when needed.
### Don't: Put Consumer Routes in Customers Module
```python
# BAD - customers/routes shouldn't serve order data
@router.get("/customers/{id}/orders")
def get_customer_orders(customer_id: int):
from app.modules.orders.models import Order # Wrong!
...
```
## Migration Note
Previously, the customers module had methods that imported from orders:
```python
# OLD (removed)
class CustomerService:
def get_customer_orders(self, db, vendor_id, customer_id):
from app.modules.orders.models import Order # Lazy import
...
def get_customer_statistics(self, db, vendor_id, customer_id):
from app.modules.orders.models import Order # Lazy import
...
```
These have been moved to the orders module:
- `get_customer_orders()``orders.services.customer_order_service`
- `get_customer_statistics()``orders.services.order_metrics.get_customer_order_metrics()`
## Related Documentation
- [Media Architecture](media-architecture.md) - Similar pattern for media files
- [Module System Architecture](module-system.md) - Module structure and dependencies
- [Cross-Module Import Rules](cross-module-import-rules.md) - Import restrictions
- [Metrics Provider Pattern](metrics-provider-pattern.md) - Provider pattern for statistics

View File

@@ -0,0 +1,298 @@
# Media Management Architecture
This document describes the consumer-agnostic media architecture used in the platform.
## Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ CONSUMER MODULES │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Catalog │ │ Art Gallery │ │ Future │ │
│ │ │ │ (future) │ │ Module │ │
│ │ ProductMedia│ │ GalleryMedia│ │ XxxMedia │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ CMS Module │ │
│ │ │ │
│ │ MediaFile (generic, consumer-agnostic storage) │ │
│ │ MediaService (upload, download, metadata management) │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
## Design Principles
### 1. Consumer-Agnostic Storage
The CMS module provides **generic media storage** without knowing what entities will use the media files. This means:
- `MediaFile` stores file metadata (path, size, type, dimensions)
- `MediaService` handles upload, download, and file operations
- CMS has **no knowledge** of products, galleries, or any specific consumers
### 2. Consumer-Owned Associations
Each module that needs media defines its **own association table**:
- **Catalog**: `ProductMedia` links products to media files
- **Future Art Gallery**: Would define `GalleryMedia`
- **Future Blog**: Would define `PostMedia`
This follows the principle: **The consumer knows what it needs, the provider doesn't need to know who uses it.**
### 3. Correct Dependency Direction
```
WRONG (Hidden Dependency):
CMS → Catalog (CMS knows about Product)
CORRECT:
Catalog → CMS (Catalog uses MediaFile)
```
Optional modules (catalog) depend on core modules (cms), never the reverse.
## Key Components
### MediaFile Model (CMS)
```python
# app/modules/cms/models/media.py
class MediaFile(Base, TimestampMixin):
"""Generic vendor media file - consumer-agnostic."""
__tablename__ = "media_files"
id = Column(Integer, primary_key=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
# File identification
filename = Column(String(255), nullable=False) # UUID-based
original_filename = Column(String(255))
file_path = Column(String(500), nullable=False)
# File properties
media_type = Column(String(20), nullable=False) # image, video, document
mime_type = Column(String(100))
file_size = Column(Integer)
# Image/video dimensions
width = Column(Integer)
height = Column(Integer)
# Metadata
alt_text = Column(String(500))
description = Column(Text)
folder = Column(String(100), default="general")
# Usage tracking (updated by consumers)
usage_count = Column(Integer, default=0)
# Note: Consumer-specific associations (ProductMedia, etc.) are defined
# in their respective modules. CMS doesn't know about specific consumers.
```
### MediaService (CMS)
The `MediaService` provides generic operations:
```python
# app/modules/cms/services/media_service.py
class MediaService:
"""Generic media operations - consumer-agnostic."""
async def upload_file(self, db, vendor_id, file_content, filename, folder="general"):
"""Upload a file to vendor's media library."""
...
def get_media(self, db, vendor_id, media_id):
"""Get a media file by ID."""
...
def get_media_library(self, db, vendor_id, skip=0, limit=100, **filters):
"""List vendor's media files with filtering."""
...
def update_media_metadata(self, db, vendor_id, media_id, **metadata):
"""Update file metadata (alt_text, description, etc.)."""
...
def delete_media(self, db, vendor_id, media_id):
"""Delete a media file."""
...
```
### ProductMedia Model (Catalog)
```python
# app/modules/catalog/models/product_media.py
class ProductMedia(Base, TimestampMixin):
"""Product-specific media association - owned by catalog."""
__tablename__ = "product_media"
id = Column(Integer, primary_key=True)
product_id = Column(Integer, ForeignKey("products.id", ondelete="CASCADE"))
media_id = Column(Integer, ForeignKey("media_files.id", ondelete="CASCADE"))
# Usage type: main_image, gallery, variant, thumbnail, swatch
usage_type = Column(String(50), nullable=False, default="gallery")
display_order = Column(Integer, default=0)
variant_id = Column(Integer) # For variant-specific images
# Relationships
product = relationship("Product", back_populates="media_associations")
media = relationship("MediaFile", lazy="joined")
```
### ProductMediaService (Catalog)
```python
# app/modules/catalog/services/product_media_service.py
class ProductMediaService:
"""Product-media association operations - catalog-specific."""
def attach_media_to_product(self, db, vendor_id, product_id, media_id,
usage_type="gallery", display_order=0):
"""Attach a media file to a product."""
# Verify ownership, create ProductMedia association
...
def detach_media_from_product(self, db, vendor_id, product_id, media_id,
usage_type=None):
"""Detach media from a product."""
...
def get_product_media(self, db, product_id, usage_type=None):
"""Get media associations for a product."""
...
def set_main_image(self, db, vendor_id, product_id, media_id):
"""Set the main image for a product."""
...
```
## Adding Media Support to a New Module
When creating a new module that needs media (e.g., an art gallery module):
### Step 1: Create Your Association Model
```python
# app/modules/art_gallery/models/gallery_media.py
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from app.core.database import Base
class GalleryMedia(Base):
"""Gallery-specific media association."""
__tablename__ = "gallery_media"
id = Column(Integer, primary_key=True)
artwork_id = Column(Integer, ForeignKey("artworks.id", ondelete="CASCADE"))
media_id = Column(Integer, ForeignKey("media_files.id", ondelete="CASCADE"))
# Gallery-specific fields
is_primary = Column(Boolean, default=False)
caption = Column(String(500))
# Relationships
artwork = relationship("Artwork", back_populates="media_associations")
media = relationship("MediaFile", lazy="joined")
```
### Step 2: Create Your Service
```python
# app/modules/art_gallery/services/gallery_media_service.py
from app.modules.cms.models import MediaFile
from app.modules.art_gallery.models import GalleryMedia, Artwork
class GalleryMediaService:
def attach_media_to_artwork(self, db, artist_id, artwork_id, media_id, **kwargs):
# Verify artwork belongs to artist
# Verify media belongs to artist (vendor_id)
# Create GalleryMedia association
...
```
### Step 3: Use CMS MediaService for File Operations
```python
from app.modules.cms.services.media_service import media_service
# Upload a new file
media_file = await media_service.upload_file(
db=db,
vendor_id=artist_id,
file_content=file_bytes,
filename="artwork.jpg",
folder="artworks",
)
# Then attach it to your entity
gallery_media_service.attach_media_to_artwork(
db=db,
artist_id=artist_id,
artwork_id=artwork.id,
media_id=media_file.id,
is_primary=True,
)
```
## Benefits of This Architecture
1. **Module Independence**: Catalog can be disabled without affecting CMS
2. **Extensibility**: New modules easily add media support
3. **No Hidden Dependencies**: Dependencies flow in one direction
4. **Clean Separation**: CMS handles storage, consumers handle associations
5. **Testability**: Can test CMS without any consumer modules
6. **Single Responsibility**: Each module owns its domain
## Anti-Patterns to Avoid
### Don't: Add Consumer References to MediaFile
```python
# BAD - Creates hidden dependency
class MediaFile(Base):
# CMS now depends on catalog!
product_associations = relationship("ProductMedia", ...)
```
### Don't: Put Consumer Logic in MediaService
```python
# BAD - CMS shouldn't know about products
class MediaService:
def attach_to_product(self, db, product_id, media_id):
from app.modules.catalog.models import ProductMedia # Wrong!
```
### Don't: Query Consumer Models from CMS
```python
# BAD - Hidden dependency
def get_media_usage(self, db, media_id):
from app.modules.catalog.models import ProductMedia # Wrong!
return db.query(ProductMedia).filter(...).all()
```
## Related Documentation
- [Module System Architecture](module-system.md) - Module structure and dependencies
- [Cross-Module Import Rules](cross-module-import-rules.md) - Import restrictions
- [Audit Provider Pattern](audit-provider-pattern.md) - Similar provider pattern for audit logging