Move 39 documentation files from top-level docs/ into each module's docs/ folder, accessible via symlinks from docs/modules/. Create data-model.md files for 10 modules with full schema documentation. Replace originals with redirect stubs. Remove empty guide stubs. Modules migrated: tenancy, billing, loyalty, marketplace, orders, messaging, cms, catalog, inventory, hosting, prospecting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
346 lines
12 KiB
Markdown
346 lines
12 KiB
Markdown
# Customer-Orders Architecture
|
|
|
|
This document describes the consumer-agnostic customer architecture, following the same pattern as [Media Architecture](../../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)
|
|
store_id = Column(Integer, ForeignKey("stores.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, store_id, customer_data):
|
|
"""Create a new customer."""
|
|
...
|
|
|
|
def get_customer(self, db, store_id, customer_id):
|
|
"""Get a customer by ID."""
|
|
...
|
|
|
|
def update_customer(self, db, store_id, customer_id, customer_data):
|
|
"""Update customer profile."""
|
|
...
|
|
|
|
def login_customer(self, db, store_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)
|
|
store_id = Column(Integer, ForeignKey("stores.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, store_id, customer_id, skip=0, limit=50):
|
|
"""Get orders for a specific customer."""
|
|
...
|
|
|
|
def get_recent_orders(self, db, store_id, customer_id, limit=5):
|
|
"""Get recent orders for a customer."""
|
|
...
|
|
|
|
def get_order_count(self, db, store_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, store_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/store/customers → List customers
|
|
GET /api/store/customers/{id} → Customer details (no order stats)
|
|
PUT /api/store/customers/{id} → Update customer
|
|
PUT /api/store/customers/{id}/status → Toggle active status
|
|
```
|
|
|
|
### Orders Module Endpoints
|
|
|
|
Customer order data (owned by orders):
|
|
|
|
```
|
|
GET /api/store/customers/{id}/orders → Customer's order history
|
|
GET /api/store/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)
|
|
store_id = Column(Integer, ForeignKey("stores.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, store_id, customer_id):
|
|
"""Get loyalty points for a customer."""
|
|
...
|
|
|
|
def add_points(self, db, store_id, customer_id, points, reason):
|
|
"""Add points to customer's balance."""
|
|
...
|
|
```
|
|
|
|
### Step 3: Add Routes in Your Module
|
|
|
|
```python
|
|
# app/modules/loyalty/routes/api/store.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, store_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, store_id, customer_id):
|
|
from app.modules.orders.models import Order # Lazy import
|
|
...
|
|
|
|
def get_customer_statistics(self, db, store_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](../../architecture/media-architecture.md) - Similar pattern for media files
|
|
- [Module System Architecture](../../architecture/module-system.md) - Module structure and dependencies
|
|
- [Cross-Module Import Rules](../../architecture/cross-module-import-rules.md) - Import restrictions
|
|
- [Metrics Provider Pattern](../../architecture/metrics-provider-pattern.md) - Provider pattern for statistics
|