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>
12 KiB
Customer-Orders Architecture
This document describes the consumer-agnostic customer architecture, following the same pattern as Media Architecture.
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:
Customerstores customer data (email, name, addresses, preferences)CustomerServicehandles 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_idlinks 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)
# 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:
# 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)
# 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)
# 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:
# 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
# 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
# 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
# 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
- Module Independence: Orders can be disabled without affecting customers
- Extensibility: New modules easily reference customers
- No Hidden Dependencies: Dependencies flow in one direction
- Clean Separation: Customers handles identity, consumers handle their domain
- Testability: Can test customers without any consumer modules
- Single Responsibility: Each module owns its domain
Anti-Patterns to Avoid
Don't: Import Consumer Models in Customers
# 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
# 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
# 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:
# 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_serviceget_customer_statistics()→orders.services.order_metrics.get_customer_order_metrics()
Related Documentation
- Media Architecture - Similar pattern for media files
- Module System Architecture - Module structure and dependencies
- Cross-Module Import Rules - Import restrictions
- Metrics Provider Pattern - Provider pattern for statistics