# 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