Add more services

This commit is contained in:
Urtzi Alfaro
2025-08-21 20:28:14 +02:00
parent d6fd53e461
commit c6dd6fd1de
85 changed files with 17842 additions and 1828 deletions

View File

@@ -0,0 +1,36 @@
# Orders Service Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install Python dependencies
COPY services/orders/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy shared modules
COPY shared/ ./shared/
# Copy application code
COPY services/orders/app/ ./app/
# Create logs directory
RUN mkdir -p logs
# Expose port
EXPOSE 8000
# Set environment variables
ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

248
services/orders/README.md Normal file
View File

@@ -0,0 +1,248 @@
# Orders Service
Customer orders and procurement planning service for the bakery management system.
## Overview
The Orders Service handles all order-related operations including:
- **Customer Management**: Complete customer lifecycle and relationship management
- **Order Processing**: End-to-end order management from creation to fulfillment
- **Procurement Planning**: Automated procurement requirement calculation and planning
- **Business Intelligence**: Order pattern analysis and business model detection
- **Dashboard Analytics**: Comprehensive reporting and metrics for order operations
## Features
### Core Capabilities
- Customer registration and management with detailed profiles
- Order creation, tracking, and status management
- Automated demand requirements calculation for production planning
- Procurement planning with supplier coordination
- Business model detection (individual bakery vs central bakery)
- Comprehensive dashboard with real-time metrics
- Integration with production, inventory, suppliers, and sales services
### API Endpoints
#### Dashboard & Analytics
- `GET /api/v1/tenants/{tenant_id}/orders/dashboard-summary` - Comprehensive dashboard data
- `GET /api/v1/tenants/{tenant_id}/orders/demand-requirements` - Demand analysis for production
- `GET /api/v1/tenants/{tenant_id}/orders/business-model` - Business model detection
#### Order Management
- `POST /api/v1/tenants/{tenant_id}/orders` - Create new customer order
- `GET /api/v1/tenants/{tenant_id}/orders` - List orders with filtering and pagination
- `GET /api/v1/tenants/{tenant_id}/orders/{order_id}` - Get order details with items
- `PUT /api/v1/tenants/{tenant_id}/orders/{order_id}/status` - Update order status
#### Customer Management
- `POST /api/v1/tenants/{tenant_id}/customers` - Create new customer
- `GET /api/v1/tenants/{tenant_id}/customers` - List customers with filtering
- `GET /api/v1/tenants/{tenant_id}/customers/{customer_id}` - Get customer details
#### Health & Status
- `GET /api/v1/tenants/{tenant_id}/orders/status` - Service status information
## Service Integration
### Shared Clients Used
- **InventoryServiceClient**: Stock levels, product availability validation
- **ProductionServiceClient**: Production notifications, capacity planning
- **SalesServiceClient**: Historical sales data for demand forecasting
- **NotificationServiceClient**: Customer notifications and alerts
### Authentication
Uses shared authentication patterns with tenant isolation:
- JWT token validation
- Tenant access verification
- User permission checks
## Configuration
Key configuration options in `app/core/config.py`:
### Order Processing
- `ORDER_PROCESSING_ENABLED`: Enable automatic order processing (default: true)
- `AUTO_APPROVE_ORDERS`: Automatically approve orders (default: false)
- `MAX_ORDER_ITEMS`: Maximum items per order (default: 50)
### Procurement Planning
- `PROCUREMENT_PLANNING_ENABLED`: Enable procurement planning (default: true)
- `PROCUREMENT_LEAD_TIME_DAYS`: Standard procurement lead time (default: 3)
- `DEMAND_FORECAST_DAYS`: Days for demand forecasting (default: 14)
- `SAFETY_STOCK_PERCENTAGE`: Safety stock buffer (default: 20%)
### Business Model Detection
- `ENABLE_BUSINESS_MODEL_DETECTION`: Enable automatic detection (default: true)
- `CENTRAL_BAKERY_ORDER_THRESHOLD`: Order threshold for central bakery (default: 20)
- `INDIVIDUAL_BAKERY_ORDER_THRESHOLD`: Order threshold for individual bakery (default: 5)
### Customer Management
- `CUSTOMER_VALIDATION_ENABLED`: Enable customer validation (default: true)
- `MAX_CUSTOMERS_PER_TENANT`: Maximum customers per tenant (default: 10000)
- `CUSTOMER_CREDIT_CHECK_ENABLED`: Enable credit checking (default: false)
### Order Validation
- `MIN_ORDER_VALUE`: Minimum order value (default: 0.0)
- `MAX_ORDER_VALUE`: Maximum order value (default: 100000.0)
- `VALIDATE_PRODUCT_AVAILABILITY`: Check product availability (default: true)
### Alert Thresholds
- `HIGH_VALUE_ORDER_THRESHOLD`: High-value order alert (default: 5000.0)
- `LARGE_QUANTITY_ORDER_THRESHOLD`: Large quantity alert (default: 100)
- `RUSH_ORDER_HOURS_THRESHOLD`: Rush order time threshold (default: 24)
- `PROCUREMENT_SHORTAGE_THRESHOLD`: Procurement shortage alert (default: 90%)
### Payment and Pricing
- `PAYMENT_VALIDATION_ENABLED`: Enable payment validation (default: true)
- `DYNAMIC_PRICING_ENABLED`: Enable dynamic pricing (default: false)
- `DISCOUNT_ENABLED`: Enable discounts (default: true)
- `MAX_DISCOUNT_PERCENTAGE`: Maximum discount allowed (default: 50%)
### Delivery and Fulfillment
- `DELIVERY_TRACKING_ENABLED`: Enable delivery tracking (default: true)
- `DEFAULT_DELIVERY_WINDOW_HOURS`: Default delivery window (default: 48)
- `PICKUP_ENABLED`: Enable pickup orders (default: true)
- `DELIVERY_ENABLED`: Enable delivery orders (default: true)
## Database Models
### Customer
- Complete customer profile with contact information
- Business type classification (individual, business, central_bakery)
- Payment terms and credit management
- Order history and metrics tracking
- Delivery preferences and special requirements
### CustomerOrder
- Comprehensive order tracking from creation to delivery
- Status management with full audit trail
- Financial calculations including discounts and taxes
- Delivery scheduling and fulfillment tracking
- Business model detection and categorization
- Customer communication preferences
### OrderItem
- Detailed line item tracking with product specifications
- Customization and special instruction support
- Production requirement integration
- Cost tracking and margin analysis
- Quality control integration
### OrderStatusHistory
- Complete audit trail of order status changes
- Event tracking with detailed context
- User attribution and change reasons
- Customer notification tracking
### ProcurementPlan
- Master procurement planning with business model context
- Supplier diversification and risk assessment
- Performance tracking and cost analysis
- Integration with demand forecasting
### ProcurementRequirement
- Detailed procurement requirements per product/ingredient
- Current inventory level integration
- Supplier preference and lead time management
- Quality specifications and special requirements
### OrderAlert
- Comprehensive alert system for order issues
- Multiple severity levels with appropriate routing
- Business impact assessment
- Resolution tracking and performance metrics
## Business Logic
### Order Processing Flow
1. **Order Creation**: Validate customer, calculate totals, create order record
2. **Item Processing**: Create order items with specifications and requirements
3. **Status Tracking**: Maintain complete audit trail of status changes
4. **Customer Metrics**: Update customer statistics and relationship data
5. **Business Model Detection**: Analyze patterns to determine bakery type
6. **Alert Generation**: Check for high-value, rush, or large orders
7. **Service Integration**: Notify production and inventory services
### Procurement Planning
1. **Demand Analysis**: Aggregate orders by delivery date and products
2. **Inventory Integration**: Check current stock levels and reservations
3. **Requirement Calculation**: Calculate net procurement needs with safety buffer
4. **Supplier Coordination**: Match requirements with preferred suppliers
5. **Lead Time Planning**: Account for supplier lead times and delivery windows
6. **Risk Assessment**: Evaluate supply risks and backup options
### Business Model Detection
- **Individual Bakery**: Low order volume, direct customer sales, standard products
- **Central Bakery**: High volume, wholesale operations, bulk orders
- **Detection Factors**: Order frequency, quantity, customer types, sales channels
## Alert System
### Alert Types
- **High Value Orders**: Orders exceeding configured thresholds
- **Rush Orders**: Orders with tight delivery requirements
- **Large Quantity Orders**: Orders with unusually high item counts
- **Payment Issues**: Payment validation failures or credit problems
- **Procurement Shortages**: Insufficient inventory for order fulfillment
- **Customer Issues**: New customers, credit limit exceedances, special requirements
### Severity Levels
- **Critical**: WhatsApp + Email + Dashboard + SMS
- **High**: WhatsApp + Email + Dashboard
- **Medium**: Email + Dashboard
- **Low**: Dashboard only
## Development
### Setup
```bash
# Install dependencies
pip install -r requirements.txt
# Set up database
# Configure ORDERS_DATABASE_URL environment variable
# Run migrations
alembic upgrade head
# Start service
uvicorn app.main:app --reload
```
### Testing
```bash
# Run tests
pytest
# Run with coverage
pytest --cov=app
```
### Docker
```bash
# Build image
docker build -t orders-service .
# Run container
docker run -p 8000:8000 orders-service
```
## Deployment
The service is designed for containerized deployment with:
- Health checks at `/health`
- Structured logging
- Metrics collection
- Database migrations
- Service discovery integration
## Architecture
Follows Domain-Driven Microservices Architecture:
- Clean separation of concerns
- Repository pattern for data access
- Service layer for business logic
- API layer for external interface
- Shared infrastructure for cross-cutting concerns

View File

@@ -0,0 +1,519 @@
# ================================================================
# services/orders/app/api/orders.py
# ================================================================
"""
Orders API endpoints for Orders Service
"""
from datetime import date, datetime
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
from fastapi.responses import JSONResponse
import structlog
from shared.auth.decorators import get_current_user_dep, get_current_tenant_id_dep
from app.core.database import get_db
from app.services.orders_service import OrdersService
from app.schemas.order_schemas import (
OrderCreate,
OrderUpdate,
OrderResponse,
CustomerCreate,
CustomerUpdate,
CustomerResponse,
OrdersDashboardSummary,
DemandRequirements,
ProcurementPlanningData
)
logger = structlog.get_logger()
router = APIRouter()
# ===== Dependency Injection =====
async def get_orders_service(db = Depends(get_db)) -> OrdersService:
"""Get orders service with dependencies"""
from app.repositories.order_repository import (
OrderRepository,
CustomerRepository,
OrderItemRepository,
OrderStatusHistoryRepository
)
from shared.clients import (
get_inventory_service_client,
get_production_service_client,
get_sales_service_client,
get_notification_service_client
)
return OrdersService(
order_repo=OrderRepository(),
customer_repo=CustomerRepository(),
order_item_repo=OrderItemRepository(),
status_history_repo=OrderStatusHistoryRepository(),
inventory_client=get_inventory_service_client(),
production_client=get_production_service_client(),
sales_client=get_sales_service_client(),
notification_client=get_notification_service_client()
)
# ===== Dashboard and Analytics Endpoints =====
@router.get("/tenants/{tenant_id}/orders/dashboard-summary", response_model=OrdersDashboardSummary)
async def get_dashboard_summary(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get comprehensive dashboard summary for orders"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
summary = await orders_service.get_dashboard_summary(db, tenant_id)
logger.info("Dashboard summary retrieved",
tenant_id=str(tenant_id),
total_orders=summary.total_orders_today)
return summary
except Exception as e:
logger.error("Error getting dashboard summary",
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve dashboard summary"
)
@router.get("/tenants/{tenant_id}/orders/demand-requirements", response_model=DemandRequirements)
async def get_demand_requirements(
tenant_id: UUID = Path(...),
target_date: date = Query(..., description="Date for demand analysis"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get demand requirements for production planning"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
requirements = await orders_service.get_demand_requirements(db, tenant_id, target_date)
logger.info("Demand requirements calculated",
tenant_id=str(tenant_id),
target_date=str(target_date),
total_orders=requirements.total_orders)
return requirements
except Exception as e:
logger.error("Error getting demand requirements",
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to calculate demand requirements"
)
# ===== Order Management Endpoints =====
@router.post("/tenants/{tenant_id}/orders", response_model=OrderResponse, status_code=status.HTTP_201_CREATED)
async def create_order(
order_data: OrderCreate,
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Create a new customer order"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Ensure tenant_id matches
order_data.tenant_id = tenant_id
order = await orders_service.create_order(
db,
order_data,
user_id=UUID(current_user["sub"])
)
logger.info("Order created successfully",
order_id=str(order.id),
order_number=order.order_number)
return order
except ValueError as e:
logger.warning("Invalid order data", error=str(e))
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
logger.error("Error creating order", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create order"
)
@router.get("/tenants/{tenant_id}/orders/{order_id}", response_model=OrderResponse)
async def get_order(
tenant_id: UUID = Path(...),
order_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get order details with items"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
order = await orders_service.get_order_with_items(db, order_id, tenant_id)
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found"
)
return order
except HTTPException:
raise
except Exception as e:
logger.error("Error getting order",
order_id=str(order_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve order"
)
@router.get("/tenants/{tenant_id}/orders", response_model=List[OrderResponse])
async def get_orders(
tenant_id: UUID = Path(...),
status_filter: Optional[str] = Query(None, description="Filter by order status"),
start_date: Optional[date] = Query(None, description="Start date for date range filter"),
end_date: Optional[date] = Query(None, description="End date for date range filter"),
skip: int = Query(0, ge=0, description="Number of orders to skip"),
limit: int = Query(100, ge=1, le=1000, description="Number of orders to return"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get orders with filtering and pagination"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Determine which repository method to use based on filters
if status_filter:
orders = await orders_service.order_repo.get_orders_by_status(
db, tenant_id, status_filter, skip, limit
)
elif start_date and end_date:
orders = await orders_service.order_repo.get_orders_by_date_range(
db, tenant_id, start_date, end_date, skip, limit
)
else:
orders = await orders_service.order_repo.get_multi(
db, tenant_id, skip, limit, order_by="order_date", order_desc=True
)
return [OrderResponse.from_orm(order) for order in orders]
except Exception as e:
logger.error("Error getting orders", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve orders"
)
@router.put("/tenants/{tenant_id}/orders/{order_id}/status", response_model=OrderResponse)
async def update_order_status(
new_status: str,
tenant_id: UUID = Path(...),
order_id: UUID = Path(...),
reason: Optional[str] = Query(None, description="Reason for status change"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Update order status"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Validate status
valid_statuses = ["pending", "confirmed", "in_production", "ready", "out_for_delivery", "delivered", "cancelled", "failed"]
if new_status not in valid_statuses:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid status. Must be one of: {', '.join(valid_statuses)}"
)
order = await orders_service.update_order_status(
db,
order_id,
tenant_id,
new_status,
user_id=UUID(current_user["sub"]),
reason=reason
)
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found"
)
logger.info("Order status updated",
order_id=str(order_id),
new_status=new_status)
return order
except HTTPException:
raise
except Exception as e:
logger.error("Error updating order status",
order_id=str(order_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update order status"
)
# ===== Customer Management Endpoints =====
@router.post("/tenants/{tenant_id}/customers", response_model=CustomerResponse, status_code=status.HTTP_201_CREATED)
async def create_customer(
customer_data: CustomerCreate,
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Create a new customer"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Ensure tenant_id matches
customer_data.tenant_id = tenant_id
# Check if customer code already exists
existing_customer = await orders_service.customer_repo.get_by_customer_code(
db, customer_data.customer_code, tenant_id
)
if existing_customer:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Customer code already exists"
)
customer = await orders_service.customer_repo.create(
db,
obj_in=customer_data.dict(),
created_by=UUID(current_user["sub"])
)
logger.info("Customer created successfully",
customer_id=str(customer.id),
customer_code=customer.customer_code)
return CustomerResponse.from_orm(customer)
except HTTPException:
raise
except Exception as e:
logger.error("Error creating customer", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create customer"
)
@router.get("/tenants/{tenant_id}/customers", response_model=List[CustomerResponse])
async def get_customers(
tenant_id: UUID = Path(...),
active_only: bool = Query(True, description="Filter for active customers only"),
skip: int = Query(0, ge=0, description="Number of customers to skip"),
limit: int = Query(100, ge=1, le=1000, description="Number of customers to return"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get customers with filtering and pagination"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
if active_only:
customers = await orders_service.customer_repo.get_active_customers(
db, tenant_id, skip, limit
)
else:
customers = await orders_service.customer_repo.get_multi(
db, tenant_id, skip, limit, order_by="name"
)
return [CustomerResponse.from_orm(customer) for customer in customers]
except Exception as e:
logger.error("Error getting customers", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve customers"
)
@router.get("/tenants/{tenant_id}/customers/{customer_id}", response_model=CustomerResponse)
async def get_customer(
tenant_id: UUID = Path(...),
customer_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get customer details"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
customer = await orders_service.customer_repo.get(db, customer_id, tenant_id)
if not customer:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Customer not found"
)
return CustomerResponse.from_orm(customer)
except HTTPException:
raise
except Exception as e:
logger.error("Error getting customer",
customer_id=str(customer_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve customer"
)
# ===== Business Intelligence Endpoints =====
@router.get("/tenants/{tenant_id}/orders/business-model")
async def detect_business_model(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Detect business model based on order patterns"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
business_model = await orders_service.detect_business_model(db, tenant_id)
return {
"business_model": business_model,
"confidence": "high" if business_model else "unknown",
"detected_at": datetime.now().isoformat()
}
except Exception as e:
logger.error("Error detecting business model", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to detect business model"
)
# ===== Health and Status Endpoints =====
@router.get("/tenants/{tenant_id}/orders/status")
async def get_service_status(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep)
):
"""Get orders service status"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
return {
"service": "orders-service",
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"tenant_id": str(tenant_id)
}
except Exception as e:
logger.error("Error getting service status", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get service status"
)

View File

@@ -0,0 +1,77 @@
# ================================================================
# services/orders/app/core/config.py
# ================================================================
"""
Orders Service Configuration
"""
import os
from shared.config.base import BaseServiceSettings
class OrdersSettings(BaseServiceSettings):
"""Orders service specific settings"""
# Service Identity
APP_NAME: str = "Orders Service"
SERVICE_NAME: str = "orders-service"
VERSION: str = "1.0.0"
DESCRIPTION: str = "Customer orders and procurement planning"
# Database Configuration
DATABASE_URL: str = os.getenv("ORDERS_DATABASE_URL",
"postgresql+asyncpg://orders_user:orders_pass123@orders-db:5432/orders_db")
# Order Processing
ORDER_PROCESSING_ENABLED: bool = os.getenv("ORDER_PROCESSING_ENABLED", "true").lower() == "true"
AUTO_APPROVE_ORDERS: bool = os.getenv("AUTO_APPROVE_ORDERS", "false").lower() == "true"
MAX_ORDER_ITEMS: int = int(os.getenv("MAX_ORDER_ITEMS", "50"))
# Procurement Planning
PROCUREMENT_PLANNING_ENABLED: bool = os.getenv("PROCUREMENT_PLANNING_ENABLED", "true").lower() == "true"
PROCUREMENT_LEAD_TIME_DAYS: int = int(os.getenv("PROCUREMENT_LEAD_TIME_DAYS", "3"))
DEMAND_FORECAST_DAYS: int = int(os.getenv("DEMAND_FORECAST_DAYS", "14"))
SAFETY_STOCK_PERCENTAGE: float = float(os.getenv("SAFETY_STOCK_PERCENTAGE", "20.0"))
# Business Model Detection
ENABLE_BUSINESS_MODEL_DETECTION: bool = os.getenv("ENABLE_BUSINESS_MODEL_DETECTION", "true").lower() == "true"
CENTRAL_BAKERY_ORDER_THRESHOLD: int = int(os.getenv("CENTRAL_BAKERY_ORDER_THRESHOLD", "20"))
INDIVIDUAL_BAKERY_ORDER_THRESHOLD: int = int(os.getenv("INDIVIDUAL_BAKERY_ORDER_THRESHOLD", "5"))
# Customer Management
CUSTOMER_VALIDATION_ENABLED: bool = os.getenv("CUSTOMER_VALIDATION_ENABLED", "true").lower() == "true"
MAX_CUSTOMERS_PER_TENANT: int = int(os.getenv("MAX_CUSTOMERS_PER_TENANT", "10000"))
CUSTOMER_CREDIT_CHECK_ENABLED: bool = os.getenv("CUSTOMER_CREDIT_CHECK_ENABLED", "false").lower() == "true"
# Order Validation
MIN_ORDER_VALUE: float = float(os.getenv("MIN_ORDER_VALUE", "0.0"))
MAX_ORDER_VALUE: float = float(os.getenv("MAX_ORDER_VALUE", "100000.0"))
VALIDATE_PRODUCT_AVAILABILITY: bool = os.getenv("VALIDATE_PRODUCT_AVAILABILITY", "true").lower() == "true"
# Alert Thresholds
HIGH_VALUE_ORDER_THRESHOLD: float = float(os.getenv("HIGH_VALUE_ORDER_THRESHOLD", "5000.0"))
LARGE_QUANTITY_ORDER_THRESHOLD: int = int(os.getenv("LARGE_QUANTITY_ORDER_THRESHOLD", "100"))
RUSH_ORDER_HOURS_THRESHOLD: int = int(os.getenv("RUSH_ORDER_HOURS_THRESHOLD", "24"))
PROCUREMENT_SHORTAGE_THRESHOLD: float = float(os.getenv("PROCUREMENT_SHORTAGE_THRESHOLD", "90.0"))
# Payment and Pricing
PAYMENT_VALIDATION_ENABLED: bool = os.getenv("PAYMENT_VALIDATION_ENABLED", "true").lower() == "true"
DYNAMIC_PRICING_ENABLED: bool = os.getenv("DYNAMIC_PRICING_ENABLED", "false").lower() == "true"
DISCOUNT_ENABLED: bool = os.getenv("DISCOUNT_ENABLED", "true").lower() == "true"
MAX_DISCOUNT_PERCENTAGE: float = float(os.getenv("MAX_DISCOUNT_PERCENTAGE", "50.0"))
# Delivery and Fulfillment
DELIVERY_TRACKING_ENABLED: bool = os.getenv("DELIVERY_TRACKING_ENABLED", "true").lower() == "true"
DEFAULT_DELIVERY_WINDOW_HOURS: int = int(os.getenv("DEFAULT_DELIVERY_WINDOW_HOURS", "48"))
PICKUP_ENABLED: bool = os.getenv("PICKUP_ENABLED", "true").lower() == "true"
DELIVERY_ENABLED: bool = os.getenv("DELIVERY_ENABLED", "true").lower() == "true"
# Integration Settings
PRODUCTION_SERVICE_URL: str = os.getenv("PRODUCTION_SERVICE_URL", "http://production-service:8000")
INVENTORY_SERVICE_URL: str = os.getenv("INVENTORY_SERVICE_URL", "http://inventory-service:8000")
SUPPLIERS_SERVICE_URL: str = os.getenv("SUPPLIERS_SERVICE_URL", "http://suppliers-service:8000")
SALES_SERVICE_URL: str = os.getenv("SALES_SERVICE_URL", "http://sales-service:8000")
# Global settings instance
settings = OrdersSettings()

View File

@@ -0,0 +1,80 @@
# ================================================================
# services/orders/app/core/database.py
# ================================================================
"""
Orders Service Database Configuration
"""
from sqlalchemy import create_engine
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import sessionmaker, DeclarativeBase
import structlog
from typing import AsyncGenerator
from app.core.config import settings
logger = structlog.get_logger()
# Create async engine
async_engine = create_async_engine(
settings.DATABASE_URL,
echo=settings.DEBUG,
pool_size=10,
max_overflow=20,
pool_pre_ping=True,
pool_recycle=3600
)
# Create async session factory
AsyncSessionLocal = async_sessionmaker(
bind=async_engine,
class_=AsyncSession,
expire_on_commit=False
)
# Base class for models
class Base(DeclarativeBase):
pass
async def get_db() -> AsyncGenerator[AsyncSession, None]:
"""Get database session"""
async with AsyncSessionLocal() as session:
try:
yield session
except Exception as e:
await session.rollback()
logger.error("Database session error", error=str(e))
raise
finally:
await session.close()
async def init_database():
"""Initialize database tables"""
try:
async with async_engine.begin() as conn:
# Import all models to ensure they are registered
from app.models.order import CustomerOrder, OrderItem, OrderStatusHistory
from app.models.customer import Customer, CustomerContact
from app.models.procurement import ProcurementPlan, ProcurementRequirement
from app.models.alerts import OrderAlert
# Create all tables
await conn.run_sync(Base.metadata.create_all)
logger.info("Orders database initialized successfully")
except Exception as e:
logger.error("Failed to initialize orders database", error=str(e))
raise
async def get_db_health() -> bool:
"""Check database health"""
try:
async with async_engine.begin() as conn:
await conn.execute("SELECT 1")
return True
except Exception as e:
logger.error("Database health check failed", error=str(e))
return False

124
services/orders/app/main.py Normal file
View File

@@ -0,0 +1,124 @@
# ================================================================
# services/orders/app/main.py
# ================================================================
"""
Orders Service - FastAPI Application
Customer orders and procurement planning service
"""
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
import structlog
from app.core.config import settings
from app.core.database import init_database, get_db_health
from app.api.orders import router as orders_router
# Configure logging
logger = structlog.get_logger()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Manage application lifespan events"""
# Startup
try:
await init_database()
logger.info("Orders service started successfully")
except Exception as e:
logger.error("Failed to initialize orders service", error=str(e))
raise
yield
# Shutdown
logger.info("Orders service shutting down")
# Create FastAPI application
app = FastAPI(
title=settings.APP_NAME,
description=settings.DESCRIPTION,
version=settings.VERSION,
lifespan=lifespan
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Configure based on environment
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include routers
app.include_router(orders_router, prefix="/api/v1")
@app.get("/health")
async def health_check():
"""Health check endpoint"""
try:
db_healthy = await get_db_health()
health_status = {
"status": "healthy" if db_healthy else "unhealthy",
"service": settings.SERVICE_NAME,
"version": settings.VERSION,
"database": "connected" if db_healthy else "disconnected"
}
if not db_healthy:
health_status["status"] = "unhealthy"
return health_status
except Exception as e:
logger.error("Health check failed", error=str(e))
return {
"status": "unhealthy",
"service": settings.SERVICE_NAME,
"version": settings.VERSION,
"error": str(e)
}
@app.get("/")
async def root():
"""Root endpoint"""
return {
"service": settings.APP_NAME,
"version": settings.VERSION,
"description": settings.DESCRIPTION,
"status": "running"
}
@app.middleware("http")
async def logging_middleware(request: Request, call_next):
"""Add request logging middleware"""
import time
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
logger.info("HTTP request processed",
method=request.method,
url=str(request.url),
status_code=response.status_code,
process_time=round(process_time, 4))
return response
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
reload=settings.DEBUG
)

View File

@@ -0,0 +1,144 @@
# ================================================================
# services/orders/app/models/alerts.py
# ================================================================
"""
Alert system database models for Orders Service
"""
import uuid
from datetime import datetime
from decimal import Decimal
from typing import Optional
from sqlalchemy import Column, String, Boolean, DateTime, Numeric, Text, Integer
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.sql import func
from app.core.database import Base
class OrderAlert(Base):
"""Alert system for orders and procurement issues"""
__tablename__ = "order_alerts"
# Primary identification
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
alert_code = Column(String(50), nullable=False, index=True)
# Alert categorization
alert_type = Column(String(50), nullable=False, index=True)
# Alert types: order_issue, procurement_shortage, payment_problem, delivery_delay,
# quality_concern, high_value_order, rush_order, customer_issue, supplier_problem
severity = Column(String(20), nullable=False, default="medium", index=True)
# Severity levels: critical, high, medium, low
category = Column(String(50), nullable=False, index=True)
# Categories: operational, financial, quality, customer, supplier, compliance
# Alert source and context
source_entity_type = Column(String(50), nullable=False) # order, customer, procurement_plan, etc.
source_entity_id = Column(UUID(as_uuid=True), nullable=False, index=True)
source_entity_reference = Column(String(100), nullable=True) # Human-readable reference
# Alert content
title = Column(String(200), nullable=False)
description = Column(Text, nullable=False)
detailed_message = Column(Text, nullable=True)
# Alert conditions and triggers
trigger_condition = Column(String(200), nullable=True)
threshold_value = Column(Numeric(15, 4), nullable=True)
actual_value = Column(Numeric(15, 4), nullable=True)
variance = Column(Numeric(15, 4), nullable=True)
# Context data
alert_data = Column(JSONB, nullable=True) # Additional context-specific data
business_impact = Column(Text, nullable=True)
customer_impact = Column(Text, nullable=True)
financial_impact = Column(Numeric(12, 2), nullable=True)
# Alert status and lifecycle
status = Column(String(50), nullable=False, default="active", index=True)
# Status values: active, acknowledged, in_progress, resolved, dismissed, expired
alert_state = Column(String(50), nullable=False, default="new") # new, escalated, recurring
# Resolution and follow-up
resolution_action = Column(String(200), nullable=True)
resolution_notes = Column(Text, nullable=True)
resolution_cost = Column(Numeric(10, 2), nullable=True)
# Timing and escalation
first_occurred_at = Column(DateTime(timezone=True), nullable=False, index=True)
last_occurred_at = Column(DateTime(timezone=True), nullable=False)
acknowledged_at = Column(DateTime(timezone=True), nullable=True)
resolved_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True)
# Occurrence tracking
occurrence_count = Column(Integer, nullable=False, default=1)
is_recurring = Column(Boolean, nullable=False, default=False)
recurrence_pattern = Column(String(100), nullable=True)
# Responsibility and assignment
assigned_to = Column(UUID(as_uuid=True), nullable=True)
assigned_role = Column(String(50), nullable=True) # orders_manager, procurement_manager, etc.
escalated_to = Column(UUID(as_uuid=True), nullable=True)
escalation_level = Column(Integer, nullable=False, default=0)
# Notification tracking
notification_sent = Column(Boolean, nullable=False, default=False)
notification_methods = Column(JSONB, nullable=True) # [email, sms, whatsapp, dashboard]
notification_recipients = Column(JSONB, nullable=True) # List of recipients
last_notification_sent = Column(DateTime(timezone=True), nullable=True)
# Customer communication
customer_notified = Column(Boolean, nullable=False, default=False)
customer_notification_method = Column(String(50), nullable=True)
customer_message = Column(Text, nullable=True)
# Recommended actions
recommended_actions = Column(JSONB, nullable=True) # List of suggested actions
automated_actions_taken = Column(JSONB, nullable=True) # Actions performed automatically
manual_actions_required = Column(JSONB, nullable=True) # Actions requiring human intervention
# Priority and urgency
priority_score = Column(Integer, nullable=False, default=50) # 1-100 scale
urgency = Column(String(20), nullable=False, default="normal") # immediate, urgent, normal, low
business_priority = Column(String(20), nullable=False, default="normal")
# Related entities
related_orders = Column(JSONB, nullable=True) # Related order IDs
related_customers = Column(JSONB, nullable=True) # Related customer IDs
related_suppliers = Column(JSONB, nullable=True) # Related supplier IDs
related_alerts = Column(JSONB, nullable=True) # Related alert IDs
# Performance tracking
detection_time = Column(DateTime(timezone=True), nullable=True) # When issue was detected
response_time_minutes = Column(Integer, nullable=True) # Time to acknowledge
resolution_time_minutes = Column(Integer, nullable=True) # Time to resolve
# Quality and feedback
alert_accuracy = Column(Boolean, nullable=True) # Was this a valid alert?
false_positive = Column(Boolean, nullable=False, default=False)
feedback_notes = Column(Text, nullable=True)
# Compliance and audit
compliance_related = Column(Boolean, nullable=False, default=False)
audit_trail = Column(JSONB, nullable=True) # Changes and actions taken
regulatory_impact = Column(String(200), nullable=True)
# Integration and external systems
external_system_reference = Column(String(100), nullable=True)
external_ticket_number = Column(String(50), nullable=True)
erp_reference = Column(String(100), nullable=True)
# Audit fields
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
created_by = Column(UUID(as_uuid=True), nullable=True)
updated_by = Column(UUID(as_uuid=True), nullable=True)
# Additional metadata
alert_metadata = Column(JSONB, nullable=True)

View File

@@ -0,0 +1,123 @@
# ================================================================
# services/orders/app/models/customer.py
# ================================================================
"""
Customer-related database models for Orders Service
"""
import uuid
from datetime import datetime
from decimal import Decimal
from typing import Optional, List
from sqlalchemy import Column, String, Boolean, DateTime, Numeric, Text, ForeignKey, Integer
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
class Customer(Base):
"""Customer model for managing customer information"""
__tablename__ = "customers"
# Primary identification
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
customer_code = Column(String(50), nullable=False, index=True) # Human-readable code
# Basic information
name = Column(String(200), nullable=False)
business_name = Column(String(200), nullable=True)
customer_type = Column(String(50), nullable=False, default="individual") # individual, business, central_bakery
# Contact information
email = Column(String(255), nullable=True)
phone = Column(String(50), nullable=True)
# Address information
address_line1 = Column(String(255), nullable=True)
address_line2 = Column(String(255), nullable=True)
city = Column(String(100), nullable=True)
state = Column(String(100), nullable=True)
postal_code = Column(String(20), nullable=True)
country = Column(String(100), nullable=False, default="US")
# Business information
tax_id = Column(String(50), nullable=True)
business_license = Column(String(100), nullable=True)
# Customer status and preferences
is_active = Column(Boolean, nullable=False, default=True)
preferred_delivery_method = Column(String(50), nullable=False, default="delivery") # delivery, pickup
payment_terms = Column(String(50), nullable=False, default="immediate") # immediate, net_30, net_60
credit_limit = Column(Numeric(10, 2), nullable=True)
discount_percentage = Column(Numeric(5, 2), nullable=False, default=Decimal("0.00"))
# Customer categorization
customer_segment = Column(String(50), nullable=False, default="regular") # vip, regular, wholesale
priority_level = Column(String(20), nullable=False, default="normal") # high, normal, low
# Preferences and special requirements
special_instructions = Column(Text, nullable=True)
delivery_preferences = Column(JSONB, nullable=True) # Time windows, special requirements
product_preferences = Column(JSONB, nullable=True) # Favorite products, allergies
# Customer metrics
total_orders = Column(Integer, nullable=False, default=0)
total_spent = Column(Numeric(12, 2), nullable=False, default=Decimal("0.00"))
average_order_value = Column(Numeric(10, 2), nullable=False, default=Decimal("0.00"))
last_order_date = Column(DateTime(timezone=True), nullable=True)
# Audit fields
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
created_by = Column(UUID(as_uuid=True), nullable=True)
updated_by = Column(UUID(as_uuid=True), nullable=True)
# Relationships
contacts = relationship("CustomerContact", back_populates="customer", cascade="all, delete-orphan")
orders = relationship("CustomerOrder", back_populates="customer")
class CustomerContact(Base):
"""Additional contact persons for business customers"""
__tablename__ = "customer_contacts"
# Primary identification
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
customer_id = Column(UUID(as_uuid=True), ForeignKey("customers.id", ondelete="CASCADE"), nullable=False)
# Contact information
name = Column(String(200), nullable=False)
title = Column(String(100), nullable=True)
department = Column(String(100), nullable=True)
# Contact details
email = Column(String(255), nullable=True)
phone = Column(String(50), nullable=True)
mobile = Column(String(50), nullable=True)
# Contact preferences
is_primary = Column(Boolean, nullable=False, default=False)
contact_for_orders = Column(Boolean, nullable=False, default=True)
contact_for_delivery = Column(Boolean, nullable=False, default=False)
contact_for_billing = Column(Boolean, nullable=False, default=False)
contact_for_support = Column(Boolean, nullable=False, default=False)
# Preferred contact methods
preferred_contact_method = Column(String(50), nullable=False, default="email") # email, phone, sms
contact_time_preferences = Column(JSONB, nullable=True) # Time windows for contact
# Notes and special instructions
notes = Column(Text, nullable=True)
# Status
is_active = Column(Boolean, nullable=False, default=True)
# Audit fields
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
# Relationships
customer = relationship("Customer", back_populates="contacts")

View File

@@ -0,0 +1,218 @@
# ================================================================
# services/orders/app/models/order.py
# ================================================================
"""
Order-related database models for Orders Service
"""
import uuid
from datetime import datetime
from decimal import Decimal
from typing import Optional, List
from sqlalchemy import Column, String, Boolean, DateTime, Numeric, Text, ForeignKey, Integer
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
class CustomerOrder(Base):
"""Customer order model for tracking orders throughout their lifecycle"""
__tablename__ = "customer_orders"
# Primary identification
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
order_number = Column(String(50), nullable=False, unique=True, index=True)
# Customer information
customer_id = Column(UUID(as_uuid=True), ForeignKey("customers.id"), nullable=False, index=True)
# Order status and lifecycle
status = Column(String(50), nullable=False, default="pending", index=True)
# Status values: pending, confirmed, in_production, ready, out_for_delivery, delivered, cancelled, failed
order_type = Column(String(50), nullable=False, default="standard") # standard, rush, recurring, special
priority = Column(String(20), nullable=False, default="normal") # high, normal, low
# Order timing
order_date = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
requested_delivery_date = Column(DateTime(timezone=True), nullable=False)
confirmed_delivery_date = Column(DateTime(timezone=True), nullable=True)
actual_delivery_date = Column(DateTime(timezone=True), nullable=True)
# Delivery information
delivery_method = Column(String(50), nullable=False, default="delivery") # delivery, pickup
delivery_address = Column(JSONB, nullable=True) # Complete delivery address
delivery_instructions = Column(Text, nullable=True)
delivery_window_start = Column(DateTime(timezone=True), nullable=True)
delivery_window_end = Column(DateTime(timezone=True), nullable=True)
# Financial information
subtotal = Column(Numeric(10, 2), nullable=False, default=Decimal("0.00"))
discount_amount = Column(Numeric(10, 2), nullable=False, default=Decimal("0.00"))
discount_percentage = Column(Numeric(5, 2), nullable=False, default=Decimal("0.00"))
tax_amount = Column(Numeric(10, 2), nullable=False, default=Decimal("0.00"))
delivery_fee = Column(Numeric(10, 2), nullable=False, default=Decimal("0.00"))
total_amount = Column(Numeric(10, 2), nullable=False, default=Decimal("0.00"))
# Payment information
payment_status = Column(String(50), nullable=False, default="pending") # pending, partial, paid, failed, refunded
payment_method = Column(String(50), nullable=True) # cash, card, bank_transfer, account
payment_terms = Column(String(50), nullable=False, default="immediate")
payment_due_date = Column(DateTime(timezone=True), nullable=True)
# Special requirements and customizations
special_instructions = Column(Text, nullable=True)
custom_requirements = Column(JSONB, nullable=True) # Special dietary requirements, decorations
allergen_warnings = Column(JSONB, nullable=True) # Allergen information
# Business model detection
business_model = Column(String(50), nullable=True) # individual_bakery, central_bakery (auto-detected)
estimated_business_model = Column(String(50), nullable=True) # Based on order patterns
# Order source and channel
order_source = Column(String(50), nullable=False, default="manual") # manual, online, phone, app, api
sales_channel = Column(String(50), nullable=False, default="direct") # direct, wholesale, retail
order_origin = Column(String(100), nullable=True) # Website, app, store location
# Fulfillment tracking
production_batch_id = Column(UUID(as_uuid=True), nullable=True) # Link to production batch
fulfillment_location = Column(String(100), nullable=True) # Which location fulfills this order
estimated_preparation_time = Column(Integer, nullable=True) # Minutes
actual_preparation_time = Column(Integer, nullable=True) # Minutes
# Customer communication
customer_notified_confirmed = Column(Boolean, nullable=False, default=False)
customer_notified_ready = Column(Boolean, nullable=False, default=False)
customer_notified_delivered = Column(Boolean, nullable=False, default=False)
communication_preferences = Column(JSONB, nullable=True)
# Quality and feedback
quality_score = Column(Numeric(3, 1), nullable=True) # 1.0 to 10.0
customer_rating = Column(Integer, nullable=True) # 1-5 stars
customer_feedback = Column(Text, nullable=True)
# Cancellation and refunds
cancellation_reason = Column(String(200), nullable=True)
cancelled_at = Column(DateTime(timezone=True), nullable=True)
cancelled_by = Column(UUID(as_uuid=True), nullable=True)
refund_amount = Column(Numeric(10, 2), nullable=False, default=Decimal("0.00"))
refund_processed_at = Column(DateTime(timezone=True), nullable=True)
# Audit fields
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
created_by = Column(UUID(as_uuid=True), nullable=True)
updated_by = Column(UUID(as_uuid=True), nullable=True)
# Additional metadata
order_metadata = Column(JSONB, nullable=True) # Flexible field for additional data
# Relationships
customer = relationship("Customer", back_populates="orders")
items = relationship("OrderItem", back_populates="order", cascade="all, delete-orphan")
status_history = relationship("OrderStatusHistory", back_populates="order", cascade="all, delete-orphan")
class OrderItem(Base):
"""Individual items within a customer order"""
__tablename__ = "order_items"
# Primary identification
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
order_id = Column(UUID(as_uuid=True), ForeignKey("customer_orders.id", ondelete="CASCADE"), nullable=False)
# Product information
product_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Reference to products service
product_name = Column(String(200), nullable=False)
product_sku = Column(String(100), nullable=True)
product_category = Column(String(100), nullable=True)
# Quantity and units
quantity = Column(Numeric(10, 3), nullable=False)
unit_of_measure = Column(String(50), nullable=False, default="each")
weight = Column(Numeric(10, 3), nullable=True) # For weight-based products
# Pricing information
unit_price = Column(Numeric(10, 2), nullable=False)
line_discount = Column(Numeric(10, 2), nullable=False, default=Decimal("0.00"))
line_total = Column(Numeric(10, 2), nullable=False)
# Product specifications and customizations
product_specifications = Column(JSONB, nullable=True) # Size, flavor, decorations
customization_details = Column(Text, nullable=True)
special_instructions = Column(Text, nullable=True)
# Production requirements
recipe_id = Column(UUID(as_uuid=True), nullable=True) # Reference to recipes service
production_requirements = Column(JSONB, nullable=True) # Ingredients, equipment needed
estimated_production_time = Column(Integer, nullable=True) # Minutes
# Fulfillment tracking
status = Column(String(50), nullable=False, default="pending") # pending, in_production, ready, delivered
production_started_at = Column(DateTime(timezone=True), nullable=True)
production_completed_at = Column(DateTime(timezone=True), nullable=True)
quality_checked = Column(Boolean, nullable=False, default=False)
quality_score = Column(Numeric(3, 1), nullable=True)
# Cost tracking
ingredient_cost = Column(Numeric(10, 2), nullable=True)
labor_cost = Column(Numeric(10, 2), nullable=True)
overhead_cost = Column(Numeric(10, 2), nullable=True)
total_cost = Column(Numeric(10, 2), nullable=True)
margin = Column(Numeric(10, 2), nullable=True)
# Inventory impact
reserved_inventory = Column(Boolean, nullable=False, default=False)
inventory_allocated_at = Column(DateTime(timezone=True), nullable=True)
# Audit fields
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
# Additional metadata
customer_metadata = Column(JSONB, nullable=True)
# Relationships
order = relationship("CustomerOrder", back_populates="items")
class OrderStatusHistory(Base):
"""Track status changes and important events in order lifecycle"""
__tablename__ = "order_status_history"
# Primary identification
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
order_id = Column(UUID(as_uuid=True), ForeignKey("customer_orders.id", ondelete="CASCADE"), nullable=False)
# Status change information
from_status = Column(String(50), nullable=True)
to_status = Column(String(50), nullable=False)
change_reason = Column(String(200), nullable=True)
# Event details
event_type = Column(String(50), nullable=False, default="status_change")
# Event types: status_change, payment_received, production_started, delivery_scheduled, etc.
event_description = Column(Text, nullable=True)
event_data = Column(JSONB, nullable=True) # Additional event-specific data
# Who made the change
changed_by = Column(UUID(as_uuid=True), nullable=True)
change_source = Column(String(50), nullable=False, default="manual") # manual, automatic, system, api
# Timing
changed_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
# Customer communication
customer_notified = Column(Boolean, nullable=False, default=False)
notification_method = Column(String(50), nullable=True) # email, sms, phone, app
notification_sent_at = Column(DateTime(timezone=True), nullable=True)
# Additional notes
notes = Column(Text, nullable=True)
# Relationships
order = relationship("CustomerOrder", back_populates="status_history")

View File

@@ -0,0 +1,217 @@
# ================================================================
# services/orders/app/models/procurement.py
# ================================================================
"""
Procurement planning database models for Orders Service
"""
import uuid
from datetime import datetime, date
from decimal import Decimal
from typing import Optional, List
from sqlalchemy import Column, String, Boolean, DateTime, Date, Numeric, Text, Integer, ForeignKey
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
class ProcurementPlan(Base):
"""Master procurement plan for coordinating supply needs across orders and production"""
__tablename__ = "procurement_plans"
# Primary identification
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
plan_number = Column(String(50), nullable=False, unique=True, index=True)
# Plan scope and timing
plan_date = Column(Date, nullable=False, index=True)
plan_period_start = Column(Date, nullable=False)
plan_period_end = Column(Date, nullable=False)
planning_horizon_days = Column(Integer, nullable=False, default=14)
# Plan status and lifecycle
status = Column(String(50), nullable=False, default="draft", index=True)
# Status values: draft, pending_approval, approved, in_execution, completed, cancelled
plan_type = Column(String(50), nullable=False, default="regular") # regular, emergency, seasonal
priority = Column(String(20), nullable=False, default="normal") # high, normal, low
# Business model context
business_model = Column(String(50), nullable=True) # individual_bakery, central_bakery
procurement_strategy = Column(String(50), nullable=False, default="just_in_time") # just_in_time, bulk, mixed
# Plan totals and summary
total_requirements = Column(Integer, nullable=False, default=0)
total_estimated_cost = Column(Numeric(12, 2), nullable=False, default=Decimal("0.00"))
total_approved_cost = Column(Numeric(12, 2), nullable=False, default=Decimal("0.00"))
cost_variance = Column(Numeric(12, 2), nullable=False, default=Decimal("0.00"))
# Demand analysis
total_demand_orders = Column(Integer, nullable=False, default=0)
total_demand_quantity = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
total_production_requirements = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
safety_stock_buffer = Column(Numeric(5, 2), nullable=False, default=Decimal("20.00")) # Percentage
# Supplier coordination
primary_suppliers_count = Column(Integer, nullable=False, default=0)
backup_suppliers_count = Column(Integer, nullable=False, default=0)
supplier_diversification_score = Column(Numeric(3, 1), nullable=True) # 1.0 to 10.0
# Risk assessment
supply_risk_level = Column(String(20), nullable=False, default="low") # low, medium, high, critical
demand_forecast_confidence = Column(Numeric(3, 1), nullable=True) # 1.0 to 10.0
seasonality_adjustment = Column(Numeric(5, 2), nullable=False, default=Decimal("0.00"))
# Execution tracking
approved_at = Column(DateTime(timezone=True), nullable=True)
approved_by = Column(UUID(as_uuid=True), nullable=True)
execution_started_at = Column(DateTime(timezone=True), nullable=True)
execution_completed_at = Column(DateTime(timezone=True), nullable=True)
# Performance metrics
fulfillment_rate = Column(Numeric(5, 2), nullable=True) # Percentage
on_time_delivery_rate = Column(Numeric(5, 2), nullable=True) # Percentage
cost_accuracy = Column(Numeric(5, 2), nullable=True) # Percentage
quality_score = Column(Numeric(3, 1), nullable=True) # 1.0 to 10.0
# Integration data
source_orders = Column(JSONB, nullable=True) # Orders that drove this plan
production_schedules = Column(JSONB, nullable=True) # Associated production schedules
inventory_snapshots = Column(JSONB, nullable=True) # Inventory levels at planning time
# Communication and collaboration
stakeholder_notifications = Column(JSONB, nullable=True) # Who was notified and when
approval_workflow = Column(JSONB, nullable=True) # Approval chain and status
# Special considerations
special_requirements = Column(Text, nullable=True)
seasonal_adjustments = Column(JSONB, nullable=True)
emergency_provisions = Column(JSONB, nullable=True)
# External references
erp_reference = Column(String(100), nullable=True)
supplier_portal_reference = Column(String(100), nullable=True)
# Audit fields
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
created_by = Column(UUID(as_uuid=True), nullable=True)
updated_by = Column(UUID(as_uuid=True), nullable=True)
# Additional metadata
plan_metadata = Column(JSONB, nullable=True)
# Relationships
requirements = relationship("ProcurementRequirement", back_populates="plan", cascade="all, delete-orphan")
class ProcurementRequirement(Base):
"""Individual procurement requirements within a procurement plan"""
__tablename__ = "procurement_requirements"
# Primary identification
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
plan_id = Column(UUID(as_uuid=True), ForeignKey("procurement_plans.id", ondelete="CASCADE"), nullable=False)
requirement_number = Column(String(50), nullable=False, index=True)
# Product/ingredient information
product_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Reference to products/ingredients
product_name = Column(String(200), nullable=False)
product_sku = Column(String(100), nullable=True)
product_category = Column(String(100), nullable=True)
product_type = Column(String(50), nullable=False, default="ingredient") # ingredient, packaging, supplies
# Requirement details
required_quantity = Column(Numeric(12, 3), nullable=False)
unit_of_measure = Column(String(50), nullable=False)
safety_stock_quantity = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
total_quantity_needed = Column(Numeric(12, 3), nullable=False)
# Current inventory situation
current_stock_level = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
reserved_stock = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
available_stock = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
net_requirement = Column(Numeric(12, 3), nullable=False)
# Demand breakdown
order_demand = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
production_demand = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
forecast_demand = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
buffer_demand = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
# Supplier information
preferred_supplier_id = Column(UUID(as_uuid=True), nullable=True)
backup_supplier_id = Column(UUID(as_uuid=True), nullable=True)
supplier_name = Column(String(200), nullable=True)
supplier_lead_time_days = Column(Integer, nullable=True)
minimum_order_quantity = Column(Numeric(12, 3), nullable=True)
# Pricing and cost
estimated_unit_cost = Column(Numeric(10, 4), nullable=True)
estimated_total_cost = Column(Numeric(12, 2), nullable=True)
last_purchase_cost = Column(Numeric(10, 4), nullable=True)
cost_variance = Column(Numeric(10, 2), nullable=False, default=Decimal("0.00"))
# Timing requirements
required_by_date = Column(Date, nullable=False)
lead_time_buffer_days = Column(Integer, nullable=False, default=1)
suggested_order_date = Column(Date, nullable=False)
latest_order_date = Column(Date, nullable=False)
# Quality and specifications
quality_specifications = Column(JSONB, nullable=True)
special_requirements = Column(Text, nullable=True)
storage_requirements = Column(String(200), nullable=True)
shelf_life_days = Column(Integer, nullable=True)
# Requirement status
status = Column(String(50), nullable=False, default="pending")
# Status values: pending, approved, ordered, partially_received, received, cancelled
priority = Column(String(20), nullable=False, default="normal") # critical, high, normal, low
risk_level = Column(String(20), nullable=False, default="low") # low, medium, high, critical
# Purchase order tracking
purchase_order_id = Column(UUID(as_uuid=True), nullable=True)
purchase_order_number = Column(String(50), nullable=True)
ordered_quantity = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
ordered_at = Column(DateTime(timezone=True), nullable=True)
# Delivery tracking
expected_delivery_date = Column(Date, nullable=True)
actual_delivery_date = Column(Date, nullable=True)
received_quantity = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
delivery_status = Column(String(50), nullable=False, default="pending")
# Performance tracking
fulfillment_rate = Column(Numeric(5, 2), nullable=True) # Percentage
on_time_delivery = Column(Boolean, nullable=True)
quality_rating = Column(Numeric(3, 1), nullable=True) # 1.0 to 10.0
# Source traceability
source_orders = Column(JSONB, nullable=True) # Orders that contributed to this requirement
source_production_batches = Column(JSONB, nullable=True) # Production batches needing this
demand_analysis = Column(JSONB, nullable=True) # Detailed demand breakdown
# Approval and authorization
approved_quantity = Column(Numeric(12, 3), nullable=True)
approved_cost = Column(Numeric(12, 2), nullable=True)
approved_at = Column(DateTime(timezone=True), nullable=True)
approved_by = Column(UUID(as_uuid=True), nullable=True)
# Notes and communication
procurement_notes = Column(Text, nullable=True)
supplier_communication = Column(JSONB, nullable=True)
# Audit fields
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
# Additional metadata
requirement_metadata = Column(JSONB, nullable=True)
# Relationships
plan = relationship("ProcurementPlan", back_populates="requirements")

View File

@@ -0,0 +1,284 @@
# ================================================================
# services/orders/app/repositories/base_repository.py
# ================================================================
"""
Base repository class for Orders Service
"""
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
from uuid import UUID
from sqlalchemy import select, update, delete, func, and_, or_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload, joinedload
import structlog
from app.core.database import Base
logger = structlog.get_logger()
ModelType = TypeVar("ModelType", bound=Base)
CreateSchemaType = TypeVar("CreateSchemaType")
UpdateSchemaType = TypeVar("UpdateSchemaType")
class BaseRepository(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
"""Base repository with common CRUD operations"""
def __init__(self, model: Type[ModelType]):
self.model = model
async def get(
self,
db: AsyncSession,
id: UUID,
tenant_id: Optional[UUID] = None
) -> Optional[ModelType]:
"""Get a single record by ID with optional tenant filtering"""
try:
query = select(self.model).where(self.model.id == id)
# Add tenant filtering if tenant_id is provided and model has tenant_id field
if tenant_id and hasattr(self.model, 'tenant_id'):
query = query.where(self.model.tenant_id == tenant_id)
result = await db.execute(query)
return result.scalar_one_or_none()
except Exception as e:
logger.error("Error getting record", model=self.model.__name__, id=str(id), error=str(e))
raise
async def get_by_field(
self,
db: AsyncSession,
field_name: str,
field_value: Any,
tenant_id: Optional[UUID] = None
) -> Optional[ModelType]:
"""Get a single record by field value"""
try:
field = getattr(self.model, field_name)
query = select(self.model).where(field == field_value)
if tenant_id and hasattr(self.model, 'tenant_id'):
query = query.where(self.model.tenant_id == tenant_id)
result = await db.execute(query)
return result.scalar_one_or_none()
except Exception as e:
logger.error("Error getting record by field",
model=self.model.__name__,
field_name=field_name,
field_value=str(field_value),
error=str(e))
raise
async def get_multi(
self,
db: AsyncSession,
tenant_id: Optional[UUID] = None,
skip: int = 0,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
order_by: Optional[str] = None,
order_desc: bool = False
) -> List[ModelType]:
"""Get multiple records with filtering, pagination, and sorting"""
try:
query = select(self.model)
# Add tenant filtering
if tenant_id and hasattr(self.model, 'tenant_id'):
query = query.where(self.model.tenant_id == tenant_id)
# Add additional filters
if filters:
for field_name, field_value in filters.items():
if hasattr(self.model, field_name):
field = getattr(self.model, field_name)
if isinstance(field_value, list):
query = query.where(field.in_(field_value))
else:
query = query.where(field == field_value)
# Add ordering
if order_by and hasattr(self.model, order_by):
order_field = getattr(self.model, order_by)
if order_desc:
query = query.order_by(order_field.desc())
else:
query = query.order_by(order_field)
# Add pagination
query = query.offset(skip).limit(limit)
result = await db.execute(query)
return result.scalars().all()
except Exception as e:
logger.error("Error getting multiple records",
model=self.model.__name__,
error=str(e))
raise
async def count(
self,
db: AsyncSession,
tenant_id: Optional[UUID] = None,
filters: Optional[Dict[str, Any]] = None
) -> int:
"""Count records with optional filtering"""
try:
query = select(func.count()).select_from(self.model)
# Add tenant filtering
if tenant_id and hasattr(self.model, 'tenant_id'):
query = query.where(self.model.tenant_id == tenant_id)
# Add additional filters
if filters:
for field_name, field_value in filters.items():
if hasattr(self.model, field_name):
field = getattr(self.model, field_name)
if isinstance(field_value, list):
query = query.where(field.in_(field_value))
else:
query = query.where(field == field_value)
result = await db.execute(query)
return result.scalar()
except Exception as e:
logger.error("Error counting records",
model=self.model.__name__,
error=str(e))
raise
async def create(
self,
db: AsyncSession,
*,
obj_in: CreateSchemaType,
created_by: Optional[UUID] = None
) -> ModelType:
"""Create a new record"""
try:
# Convert schema to dict
if hasattr(obj_in, 'dict'):
obj_data = obj_in.dict()
else:
obj_data = obj_in
# Add created_by if the model supports it
if created_by and hasattr(self.model, 'created_by'):
obj_data['created_by'] = created_by
# Create model instance
db_obj = self.model(**obj_data)
# Add to session and flush to get ID
db.add(db_obj)
await db.flush()
await db.refresh(db_obj)
logger.info("Record created",
model=self.model.__name__,
id=str(db_obj.id))
return db_obj
except Exception as e:
logger.error("Error creating record",
model=self.model.__name__,
error=str(e))
raise
async def update(
self,
db: AsyncSession,
*,
db_obj: ModelType,
obj_in: Union[UpdateSchemaType, Dict[str, Any]],
updated_by: Optional[UUID] = None
) -> ModelType:
"""Update an existing record"""
try:
# Convert schema to dict
if hasattr(obj_in, 'dict'):
update_data = obj_in.dict(exclude_unset=True)
else:
update_data = obj_in
# Add updated_by if the model supports it
if updated_by and hasattr(self.model, 'updated_by'):
update_data['updated_by'] = updated_by
# Update fields
for field, value in update_data.items():
if hasattr(db_obj, field):
setattr(db_obj, field, value)
# Flush changes
await db.flush()
await db.refresh(db_obj)
logger.info("Record updated",
model=self.model.__name__,
id=str(db_obj.id))
return db_obj
except Exception as e:
logger.error("Error updating record",
model=self.model.__name__,
id=str(db_obj.id),
error=str(e))
raise
async def delete(
self,
db: AsyncSession,
*,
id: UUID,
tenant_id: Optional[UUID] = None
) -> Optional[ModelType]:
"""Delete a record by ID"""
try:
# First get the record
db_obj = await self.get(db, id=id, tenant_id=tenant_id)
if not db_obj:
return None
# Delete the record
await db.delete(db_obj)
await db.flush()
logger.info("Record deleted",
model=self.model.__name__,
id=str(id))
return db_obj
except Exception as e:
logger.error("Error deleting record",
model=self.model.__name__,
id=str(id),
error=str(e))
raise
async def exists(
self,
db: AsyncSession,
id: UUID,
tenant_id: Optional[UUID] = None
) -> bool:
"""Check if a record exists"""
try:
query = select(func.count()).select_from(self.model).where(self.model.id == id)
if tenant_id and hasattr(self.model, 'tenant_id'):
query = query.where(self.model.tenant_id == tenant_id)
result = await db.execute(query)
count = result.scalar()
return count > 0
except Exception as e:
logger.error("Error checking record existence",
model=self.model.__name__,
id=str(id),
error=str(e))
raise

View File

@@ -0,0 +1,464 @@
# ================================================================
# services/orders/app/repositories/order_repository.py
# ================================================================
"""
Order-related repositories for Orders Service
"""
from datetime import datetime, date
from decimal import Decimal
from typing import List, Optional, Dict, Any
from uuid import UUID
from sqlalchemy import select, func, and_, or_, case, extract
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload, joinedload
import structlog
from app.models.customer import Customer
from app.models.order import CustomerOrder, OrderItem, OrderStatusHistory
from app.schemas.order_schemas import OrderCreate, OrderUpdate, OrderItemCreate, OrderItemUpdate
from app.repositories.base_repository import BaseRepository
logger = structlog.get_logger()
class CustomerRepository(BaseRepository[Customer, dict, dict]):
"""Repository for customer operations"""
def __init__(self):
super().__init__(Customer)
async def get_by_customer_code(
self,
db: AsyncSession,
customer_code: str,
tenant_id: UUID
) -> Optional[Customer]:
"""Get customer by customer code within tenant"""
try:
query = select(Customer).where(
and_(
Customer.customer_code == customer_code,
Customer.tenant_id == tenant_id
)
)
result = await db.execute(query)
return result.scalar_one_or_none()
except Exception as e:
logger.error("Error getting customer by code",
customer_code=customer_code,
error=str(e))
raise
async def get_active_customers(
self,
db: AsyncSession,
tenant_id: UUID,
skip: int = 0,
limit: int = 100
) -> List[Customer]:
"""Get active customers for a tenant"""
try:
query = select(Customer).where(
and_(
Customer.tenant_id == tenant_id,
Customer.is_active == True
)
).order_by(Customer.name).offset(skip).limit(limit)
result = await db.execute(query)
return result.scalars().all()
except Exception as e:
logger.error("Error getting active customers", error=str(e))
raise
async def update_customer_metrics(
self,
db: AsyncSession,
customer_id: UUID,
order_value: Decimal,
order_date: datetime
):
"""Update customer metrics after order creation"""
try:
customer = await self.get(db, customer_id)
if customer:
customer.total_orders += 1
customer.total_spent += order_value
customer.average_order_value = customer.total_spent / customer.total_orders
customer.last_order_date = order_date
await db.flush()
logger.info("Customer metrics updated",
customer_id=str(customer_id),
new_total_spent=str(customer.total_spent))
except Exception as e:
logger.error("Error updating customer metrics",
customer_id=str(customer_id),
error=str(e))
raise
class OrderRepository(BaseRepository[CustomerOrder, OrderCreate, OrderUpdate]):
"""Repository for customer order operations"""
def __init__(self):
super().__init__(CustomerOrder)
async def get_with_items(
self,
db: AsyncSession,
order_id: UUID,
tenant_id: UUID
) -> Optional[CustomerOrder]:
"""Get order with all its items and customer info"""
try:
query = select(CustomerOrder).options(
selectinload(CustomerOrder.items),
selectinload(CustomerOrder.customer),
selectinload(CustomerOrder.status_history)
).where(
and_(
CustomerOrder.id == order_id,
CustomerOrder.tenant_id == tenant_id
)
)
result = await db.execute(query)
return result.scalar_one_or_none()
except Exception as e:
logger.error("Error getting order with items",
order_id=str(order_id),
error=str(e))
raise
async def get_by_order_number(
self,
db: AsyncSession,
order_number: str,
tenant_id: UUID
) -> Optional[CustomerOrder]:
"""Get order by order number within tenant"""
try:
query = select(CustomerOrder).where(
and_(
CustomerOrder.order_number == order_number,
CustomerOrder.tenant_id == tenant_id
)
)
result = await db.execute(query)
return result.scalar_one_or_none()
except Exception as e:
logger.error("Error getting order by number",
order_number=order_number,
error=str(e))
raise
async def get_orders_by_status(
self,
db: AsyncSession,
tenant_id: UUID,
status: str,
skip: int = 0,
limit: int = 100
) -> List[CustomerOrder]:
"""Get orders by status"""
try:
query = select(CustomerOrder).options(
selectinload(CustomerOrder.customer)
).where(
and_(
CustomerOrder.tenant_id == tenant_id,
CustomerOrder.status == status
)
).order_by(CustomerOrder.order_date.desc()).offset(skip).limit(limit)
result = await db.execute(query)
return result.scalars().all()
except Exception as e:
logger.error("Error getting orders by status",
status=status,
error=str(e))
raise
async def get_orders_by_date_range(
self,
db: AsyncSession,
tenant_id: UUID,
start_date: date,
end_date: date,
skip: int = 0,
limit: int = 100
) -> List[CustomerOrder]:
"""Get orders within date range"""
try:
query = select(CustomerOrder).options(
selectinload(CustomerOrder.customer),
selectinload(CustomerOrder.items)
).where(
and_(
CustomerOrder.tenant_id == tenant_id,
func.date(CustomerOrder.order_date) >= start_date,
func.date(CustomerOrder.order_date) <= end_date
)
).order_by(CustomerOrder.order_date.desc()).offset(skip).limit(limit)
result = await db.execute(query)
return result.scalars().all()
except Exception as e:
logger.error("Error getting orders by date range",
start_date=str(start_date),
end_date=str(end_date),
error=str(e))
raise
async def get_pending_orders_by_delivery_date(
self,
db: AsyncSession,
tenant_id: UUID,
delivery_date: date
) -> List[CustomerOrder]:
"""Get pending orders for a specific delivery date"""
try:
query = select(CustomerOrder).options(
selectinload(CustomerOrder.items),
selectinload(CustomerOrder.customer)
).where(
and_(
CustomerOrder.tenant_id == tenant_id,
CustomerOrder.status.in_(["pending", "confirmed", "in_production"]),
func.date(CustomerOrder.requested_delivery_date) == delivery_date
)
).order_by(CustomerOrder.priority.desc(), CustomerOrder.order_date)
result = await db.execute(query)
return result.scalars().all()
except Exception as e:
logger.error("Error getting pending orders by delivery date",
delivery_date=str(delivery_date),
error=str(e))
raise
async def get_dashboard_metrics(
self,
db: AsyncSession,
tenant_id: UUID
) -> Dict[str, Any]:
"""Get dashboard metrics for orders"""
try:
# Today's metrics
today = datetime.now().date()
week_start = today - timedelta(days=today.weekday())
month_start = today.replace(day=1)
# Order counts by period
orders_today = await db.execute(
select(func.count()).select_from(CustomerOrder).where(
and_(
CustomerOrder.tenant_id == tenant_id,
func.date(CustomerOrder.order_date) == today
)
)
)
orders_week = await db.execute(
select(func.count()).select_from(CustomerOrder).where(
and_(
CustomerOrder.tenant_id == tenant_id,
func.date(CustomerOrder.order_date) >= week_start
)
)
)
orders_month = await db.execute(
select(func.count()).select_from(CustomerOrder).where(
and_(
CustomerOrder.tenant_id == tenant_id,
func.date(CustomerOrder.order_date) >= month_start
)
)
)
# Revenue by period
revenue_today = await db.execute(
select(func.coalesce(func.sum(CustomerOrder.total_amount), 0)).where(
and_(
CustomerOrder.tenant_id == tenant_id,
func.date(CustomerOrder.order_date) == today,
CustomerOrder.status != "cancelled"
)
)
)
revenue_week = await db.execute(
select(func.coalesce(func.sum(CustomerOrder.total_amount), 0)).where(
and_(
CustomerOrder.tenant_id == tenant_id,
func.date(CustomerOrder.order_date) >= week_start,
CustomerOrder.status != "cancelled"
)
)
)
revenue_month = await db.execute(
select(func.coalesce(func.sum(CustomerOrder.total_amount), 0)).where(
and_(
CustomerOrder.tenant_id == tenant_id,
func.date(CustomerOrder.order_date) >= month_start,
CustomerOrder.status != "cancelled"
)
)
)
# Status breakdown
status_counts = await db.execute(
select(CustomerOrder.status, func.count()).select_from(CustomerOrder).where(
CustomerOrder.tenant_id == tenant_id
).group_by(CustomerOrder.status)
)
status_breakdown = {status: count for status, count in status_counts.fetchall()}
# Average order value
avg_order_value = await db.execute(
select(func.coalesce(func.avg(CustomerOrder.total_amount), 0)).where(
and_(
CustomerOrder.tenant_id == tenant_id,
CustomerOrder.status != "cancelled"
)
)
)
return {
"total_orders_today": orders_today.scalar(),
"total_orders_this_week": orders_week.scalar(),
"total_orders_this_month": orders_month.scalar(),
"revenue_today": revenue_today.scalar(),
"revenue_this_week": revenue_week.scalar(),
"revenue_this_month": revenue_month.scalar(),
"status_breakdown": status_breakdown,
"average_order_value": avg_order_value.scalar()
}
except Exception as e:
logger.error("Error getting dashboard metrics", error=str(e))
raise
async def detect_business_model(
self,
db: AsyncSession,
tenant_id: UUID,
lookback_days: int = 30
) -> Optional[str]:
"""Detect business model based on order patterns"""
try:
cutoff_date = datetime.now().date() - timedelta(days=lookback_days)
# Analyze order patterns
query = select(
func.count().label("total_orders"),
func.avg(CustomerOrder.total_amount).label("avg_order_value"),
func.count(func.distinct(CustomerOrder.customer_id)).label("unique_customers"),
func.sum(
case(
[(CustomerOrder.order_type == "rush", 1)],
else_=0
)
).label("rush_orders"),
func.sum(
case(
[(CustomerOrder.sales_channel == "wholesale", 1)],
else_=0
)
).label("wholesale_orders")
).where(
and_(
CustomerOrder.tenant_id == tenant_id,
func.date(CustomerOrder.order_date) >= cutoff_date
)
)
result = await db.execute(query)
metrics = result.fetchone()
if not metrics or metrics.total_orders == 0:
return None
# Business model detection logic
orders_per_customer = metrics.total_orders / metrics.unique_customers
wholesale_ratio = metrics.wholesale_orders / metrics.total_orders
rush_ratio = metrics.rush_orders / metrics.total_orders
if wholesale_ratio > 0.6 or orders_per_customer > 20:
return "central_bakery"
else:
return "individual_bakery"
except Exception as e:
logger.error("Error detecting business model", error=str(e))
return None
class OrderItemRepository(BaseRepository[OrderItem, OrderItemCreate, OrderItemUpdate]):
"""Repository for order item operations"""
def __init__(self):
super().__init__(OrderItem)
async def get_items_by_order(
self,
db: AsyncSession,
order_id: UUID
) -> List[OrderItem]:
"""Get all items for an order"""
try:
query = select(OrderItem).where(OrderItem.order_id == order_id)
result = await db.execute(query)
return result.scalars().all()
except Exception as e:
logger.error("Error getting order items",
order_id=str(order_id),
error=str(e))
raise
class OrderStatusHistoryRepository(BaseRepository[OrderStatusHistory, dict, dict]):
"""Repository for order status history operations"""
def __init__(self):
super().__init__(OrderStatusHistory)
async def create_status_change(
self,
db: AsyncSession,
order_id: UUID,
from_status: Optional[str],
to_status: str,
change_reason: Optional[str] = None,
changed_by: Optional[UUID] = None,
event_data: Optional[Dict[str, Any]] = None
) -> OrderStatusHistory:
"""Create a status change record"""
try:
status_history = OrderStatusHistory(
order_id=order_id,
from_status=from_status,
to_status=to_status,
change_reason=change_reason,
changed_by=changed_by,
event_data=event_data
)
db.add(status_history)
await db.flush()
await db.refresh(status_history)
logger.info("Status change recorded",
order_id=str(order_id),
from_status=from_status,
to_status=to_status)
return status_history
except Exception as e:
logger.error("Error creating status change",
order_id=str(order_id),
error=str(e))
raise

View File

@@ -0,0 +1,367 @@
# ================================================================
# services/orders/app/schemas/order_schemas.py
# ================================================================
"""
Order-related Pydantic schemas for Orders Service
"""
from datetime import datetime, date
from decimal import Decimal
from typing import Optional, List, Dict, Any
from uuid import UUID
from pydantic import BaseModel, Field, validator
# ===== Customer Schemas =====
class CustomerBase(BaseModel):
name: str = Field(..., min_length=1, max_length=200)
business_name: Optional[str] = Field(None, max_length=200)
customer_type: str = Field(default="individual", pattern="^(individual|business|central_bakery)$")
email: Optional[str] = Field(None, max_length=255)
phone: Optional[str] = Field(None, max_length=50)
address_line1: Optional[str] = Field(None, max_length=255)
address_line2: Optional[str] = Field(None, max_length=255)
city: Optional[str] = Field(None, max_length=100)
state: Optional[str] = Field(None, max_length=100)
postal_code: Optional[str] = Field(None, max_length=20)
country: str = Field(default="US", max_length=100)
is_active: bool = Field(default=True)
preferred_delivery_method: str = Field(default="delivery", pattern="^(delivery|pickup)$")
payment_terms: str = Field(default="immediate", pattern="^(immediate|net_30|net_60)$")
credit_limit: Optional[Decimal] = Field(None, ge=0)
discount_percentage: Decimal = Field(default=Decimal("0.00"), ge=0, le=100)
customer_segment: str = Field(default="regular", pattern="^(vip|regular|wholesale)$")
priority_level: str = Field(default="normal", pattern="^(high|normal|low)$")
special_instructions: Optional[str] = None
delivery_preferences: Optional[Dict[str, Any]] = None
product_preferences: Optional[Dict[str, Any]] = None
class CustomerCreate(CustomerBase):
customer_code: str = Field(..., min_length=1, max_length=50)
tenant_id: UUID
class CustomerUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=200)
business_name: Optional[str] = Field(None, max_length=200)
customer_type: Optional[str] = Field(None, pattern="^(individual|business|central_bakery)$")
email: Optional[str] = Field(None, max_length=255)
phone: Optional[str] = Field(None, max_length=50)
address_line1: Optional[str] = Field(None, max_length=255)
address_line2: Optional[str] = Field(None, max_length=255)
city: Optional[str] = Field(None, max_length=100)
state: Optional[str] = Field(None, max_length=100)
postal_code: Optional[str] = Field(None, max_length=20)
country: Optional[str] = Field(None, max_length=100)
is_active: Optional[bool] = None
preferred_delivery_method: Optional[str] = Field(None, pattern="^(delivery|pickup)$")
payment_terms: Optional[str] = Field(None, pattern="^(immediate|net_30|net_60)$")
credit_limit: Optional[Decimal] = Field(None, ge=0)
discount_percentage: Optional[Decimal] = Field(None, ge=0, le=100)
customer_segment: Optional[str] = Field(None, pattern="^(vip|regular|wholesale)$")
priority_level: Optional[str] = Field(None, pattern="^(high|normal|low)$")
special_instructions: Optional[str] = None
delivery_preferences: Optional[Dict[str, Any]] = None
product_preferences: Optional[Dict[str, Any]] = None
class CustomerResponse(CustomerBase):
id: UUID
tenant_id: UUID
customer_code: str
total_orders: int
total_spent: Decimal
average_order_value: Decimal
last_order_date: Optional[datetime]
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# ===== Order Item Schemas =====
class OrderItemBase(BaseModel):
product_id: UUID
product_name: str = Field(..., min_length=1, max_length=200)
product_sku: Optional[str] = Field(None, max_length=100)
product_category: Optional[str] = Field(None, max_length=100)
quantity: Decimal = Field(..., gt=0)
unit_of_measure: str = Field(default="each", max_length=50)
weight: Optional[Decimal] = Field(None, ge=0)
unit_price: Decimal = Field(..., ge=0)
line_discount: Decimal = Field(default=Decimal("0.00"), ge=0)
product_specifications: Optional[Dict[str, Any]] = None
customization_details: Optional[str] = None
special_instructions: Optional[str] = None
recipe_id: Optional[UUID] = None
class OrderItemCreate(OrderItemBase):
pass
class OrderItemUpdate(BaseModel):
quantity: Optional[Decimal] = Field(None, gt=0)
unit_price: Optional[Decimal] = Field(None, ge=0)
line_discount: Optional[Decimal] = Field(None, ge=0)
product_specifications: Optional[Dict[str, Any]] = None
customization_details: Optional[str] = None
special_instructions: Optional[str] = None
class OrderItemResponse(OrderItemBase):
id: UUID
order_id: UUID
line_total: Decimal
status: str
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# ===== Order Schemas =====
class OrderBase(BaseModel):
customer_id: UUID
order_type: str = Field(default="standard", pattern="^(standard|rush|recurring|special)$")
priority: str = Field(default="normal", pattern="^(high|normal|low)$")
requested_delivery_date: datetime
delivery_method: str = Field(default="delivery", pattern="^(delivery|pickup)$")
delivery_address: Optional[Dict[str, Any]] = None
delivery_instructions: Optional[str] = None
delivery_window_start: Optional[datetime] = None
delivery_window_end: Optional[datetime] = None
discount_percentage: Decimal = Field(default=Decimal("0.00"), ge=0, le=100)
delivery_fee: Decimal = Field(default=Decimal("0.00"), ge=0)
payment_method: Optional[str] = Field(None, pattern="^(cash|card|bank_transfer|account)$")
payment_terms: str = Field(default="immediate", pattern="^(immediate|net_30|net_60)$")
special_instructions: Optional[str] = None
custom_requirements: Optional[Dict[str, Any]] = None
allergen_warnings: Optional[Dict[str, Any]] = None
order_source: str = Field(default="manual", pattern="^(manual|online|phone|app|api)$")
sales_channel: str = Field(default="direct", pattern="^(direct|wholesale|retail)$")
order_origin: Optional[str] = Field(None, max_length=100)
communication_preferences: Optional[Dict[str, Any]] = None
class OrderCreate(OrderBase):
tenant_id: UUID
items: List[OrderItemCreate] = Field(..., min_items=1)
class OrderUpdate(BaseModel):
status: Optional[str] = Field(None, pattern="^(pending|confirmed|in_production|ready|out_for_delivery|delivered|cancelled|failed)$")
priority: Optional[str] = Field(None, pattern="^(high|normal|low)$")
requested_delivery_date: Optional[datetime] = None
confirmed_delivery_date: Optional[datetime] = None
delivery_method: Optional[str] = Field(None, pattern="^(delivery|pickup)$")
delivery_address: Optional[Dict[str, Any]] = None
delivery_instructions: Optional[str] = None
delivery_window_start: Optional[datetime] = None
delivery_window_end: Optional[datetime] = None
payment_method: Optional[str] = Field(None, pattern="^(cash|card|bank_transfer|account)$")
payment_status: Optional[str] = Field(None, pattern="^(pending|partial|paid|failed|refunded)$")
special_instructions: Optional[str] = None
custom_requirements: Optional[Dict[str, Any]] = None
allergen_warnings: Optional[Dict[str, Any]] = None
class OrderResponse(OrderBase):
id: UUID
tenant_id: UUID
order_number: str
status: str
order_date: datetime
confirmed_delivery_date: Optional[datetime]
actual_delivery_date: Optional[datetime]
subtotal: Decimal
discount_amount: Decimal
tax_amount: Decimal
total_amount: Decimal
payment_status: str
business_model: Optional[str]
estimated_business_model: Optional[str]
production_batch_id: Optional[UUID]
quality_score: Optional[Decimal]
customer_rating: Optional[int]
created_at: datetime
updated_at: datetime
items: List[OrderItemResponse] = []
class Config:
from_attributes = True
# ===== Procurement Schemas =====
class ProcurementRequirementBase(BaseModel):
product_id: UUID
product_name: str = Field(..., min_length=1, max_length=200)
product_sku: Optional[str] = Field(None, max_length=100)
product_category: Optional[str] = Field(None, max_length=100)
product_type: str = Field(default="ingredient", pattern="^(ingredient|packaging|supplies)$")
required_quantity: Decimal = Field(..., gt=0)
unit_of_measure: str = Field(..., min_length=1, max_length=50)
safety_stock_quantity: Decimal = Field(default=Decimal("0.000"), ge=0)
required_by_date: date
priority: str = Field(default="normal", pattern="^(critical|high|normal|low)$")
preferred_supplier_id: Optional[UUID] = None
quality_specifications: Optional[Dict[str, Any]] = None
special_requirements: Optional[str] = None
storage_requirements: Optional[str] = Field(None, max_length=200)
class ProcurementRequirementCreate(ProcurementRequirementBase):
pass
class ProcurementRequirementResponse(ProcurementRequirementBase):
id: UUID
plan_id: UUID
requirement_number: str
total_quantity_needed: Decimal
current_stock_level: Decimal
available_stock: Decimal
net_requirement: Decimal
order_demand: Decimal
production_demand: Decimal
forecast_demand: Decimal
status: str
estimated_unit_cost: Optional[Decimal]
estimated_total_cost: Optional[Decimal]
supplier_name: Optional[str]
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class ProcurementPlanBase(BaseModel):
plan_date: date
plan_period_start: date
plan_period_end: date
planning_horizon_days: int = Field(default=14, ge=1, le=365)
plan_type: str = Field(default="regular", pattern="^(regular|emergency|seasonal)$")
priority: str = Field(default="normal", pattern="^(high|normal|low)$")
business_model: Optional[str] = Field(None, pattern="^(individual_bakery|central_bakery)$")
procurement_strategy: str = Field(default="just_in_time", pattern="^(just_in_time|bulk|mixed)$")
safety_stock_buffer: Decimal = Field(default=Decimal("20.00"), ge=0, le=100)
special_requirements: Optional[str] = None
class ProcurementPlanCreate(ProcurementPlanBase):
tenant_id: UUID
requirements: List[ProcurementRequirementCreate] = Field(..., min_items=1)
class ProcurementPlanResponse(ProcurementPlanBase):
id: UUID
tenant_id: UUID
plan_number: str
status: str
total_requirements: int
total_estimated_cost: Decimal
total_approved_cost: Decimal
total_demand_orders: int
supply_risk_level: str
approved_at: Optional[datetime]
created_at: datetime
updated_at: datetime
requirements: List[ProcurementRequirementResponse] = []
class Config:
from_attributes = True
# ===== Dashboard and Analytics Schemas =====
class OrdersDashboardSummary(BaseModel):
"""Summary data for orders dashboard"""
# Current period metrics
total_orders_today: int
total_orders_this_week: int
total_orders_this_month: int
# Revenue metrics
revenue_today: Decimal
revenue_this_week: Decimal
revenue_this_month: Decimal
# Order status breakdown
pending_orders: int
confirmed_orders: int
in_production_orders: int
ready_orders: int
delivered_orders: int
# Customer metrics
total_customers: int
new_customers_this_month: int
repeat_customers_rate: Decimal
# Performance metrics
average_order_value: Decimal
order_fulfillment_rate: Decimal
on_time_delivery_rate: Decimal
# Business model detection
business_model: Optional[str]
business_model_confidence: Optional[Decimal]
# Recent activity
recent_orders: List[OrderResponse]
high_priority_orders: List[OrderResponse]
class DemandRequirements(BaseModel):
"""Demand requirements for production planning"""
date: date
tenant_id: UUID
# Product demand breakdown
product_demands: List[Dict[str, Any]]
# Aggregate metrics
total_orders: int
total_quantity: Decimal
total_value: Decimal
# Business context
business_model: Optional[str]
rush_orders_count: int
special_requirements: List[str]
# Timing requirements
earliest_delivery: datetime
latest_delivery: datetime
average_lead_time_hours: int
class ProcurementPlanningData(BaseModel):
"""Data for procurement planning decisions"""
planning_date: date
planning_horizon_days: int
# Demand forecast
demand_forecast: List[Dict[str, Any]]
# Current inventory status
inventory_levels: Dict[str, Any]
# Supplier information
supplier_performance: Dict[str, Any]
# Risk factors
supply_risks: List[str]
demand_volatility: Decimal
# Recommendations
recommended_purchases: List[Dict[str, Any]]
critical_shortages: List[Dict[str, Any]]

View File

@@ -0,0 +1,546 @@
# ================================================================
# services/orders/app/services/orders_service.py
# ================================================================
"""
Orders Service - Main business logic service
"""
import uuid
from datetime import datetime, date, timedelta
from decimal import Decimal
from typing import List, Optional, Dict, Any
from uuid import UUID
import structlog
from shared.clients import (
InventoryServiceClient,
ProductionServiceClient,
SalesServiceClient
)
from shared.notifications.alert_integration import AlertIntegration
from shared.database.transactions import transactional
from app.core.config import settings
from app.repositories.order_repository import (
OrderRepository,
CustomerRepository,
OrderItemRepository,
OrderStatusHistoryRepository
)
from app.schemas.order_schemas import (
OrderCreate,
OrderUpdate,
OrderResponse,
CustomerCreate,
CustomerUpdate,
DemandRequirements,
OrdersDashboardSummary
)
logger = structlog.get_logger()
class OrdersService:
"""Main service for orders operations"""
def __init__(
self,
order_repo: OrderRepository,
customer_repo: CustomerRepository,
order_item_repo: OrderItemRepository,
status_history_repo: OrderStatusHistoryRepository,
inventory_client: InventoryServiceClient,
production_client: ProductionServiceClient,
sales_client: SalesServiceClient,
alert_integration: AlertIntegration
):
self.order_repo = order_repo
self.customer_repo = customer_repo
self.order_item_repo = order_item_repo
self.status_history_repo = status_history_repo
self.inventory_client = inventory_client
self.production_client = production_client
self.sales_client = sales_client
self.alert_integration = alert_integration
@transactional
async def create_order(
self,
db,
order_data: OrderCreate,
user_id: Optional[UUID] = None
) -> OrderResponse:
"""Create a new customer order with comprehensive processing"""
try:
logger.info("Creating new order",
customer_id=str(order_data.customer_id),
tenant_id=str(order_data.tenant_id))
# 1. Validate customer exists
customer = await self.customer_repo.get(
db,
order_data.customer_id,
order_data.tenant_id
)
if not customer:
raise ValueError(f"Customer {order_data.customer_id} not found")
# 2. Generate order number
order_number = await self._generate_order_number(db, order_data.tenant_id)
# 3. Calculate order totals
subtotal = sum(item.quantity * item.unit_price - item.line_discount
for item in order_data.items)
discount_amount = subtotal * (order_data.discount_percentage / 100)
tax_amount = (subtotal - discount_amount) * Decimal("0.08") # Configurable tax rate
total_amount = subtotal - discount_amount + tax_amount + order_data.delivery_fee
# 4. Create order record
order_dict = order_data.dict(exclude={"items"})
order_dict.update({
"order_number": order_number,
"subtotal": subtotal,
"discount_amount": discount_amount,
"tax_amount": tax_amount,
"total_amount": total_amount,
"status": "pending"
})
order = await self.order_repo.create(db, obj_in=order_dict, created_by=user_id)
# 5. Create order items
for item_data in order_data.items:
item_dict = item_data.dict()
item_dict.update({
"order_id": order.id,
"line_total": item_data.quantity * item_data.unit_price - item_data.line_discount
})
await self.order_item_repo.create(db, obj_in=item_dict)
# 6. Create initial status history
await self.status_history_repo.create_status_change(
db=db,
order_id=order.id,
from_status=None,
to_status="pending",
change_reason="Order created",
changed_by=user_id
)
# 7. Update customer metrics
await self.customer_repo.update_customer_metrics(
db, order.customer_id, total_amount, order.order_date
)
# 8. Business model detection
business_model = await self.detect_business_model(db, order_data.tenant_id)
if business_model:
order.business_model = business_model
# 9. Check for high-value or rush orders for alerts
await self._check_order_alerts(db, order, order_data.tenant_id)
# 10. Integrate with production service if auto-processing is enabled
if settings.ORDER_PROCESSING_ENABLED:
await self._notify_production_service(order)
logger.info("Order created successfully",
order_id=str(order.id),
order_number=order_number,
total_amount=str(total_amount))
# Return order with items loaded
return await self.get_order_with_items(db, order.id, order_data.tenant_id)
except Exception as e:
logger.error("Error creating order", error=str(e))
raise
async def get_order_with_items(
self,
db,
order_id: UUID,
tenant_id: UUID
) -> Optional[OrderResponse]:
"""Get order with all related data"""
try:
order = await self.order_repo.get_with_items(db, order_id, tenant_id)
if not order:
return None
return OrderResponse.from_orm(order)
except Exception as e:
logger.error("Error getting order with items",
order_id=str(order_id),
error=str(e))
raise
@transactional
async def update_order_status(
self,
db,
order_id: UUID,
tenant_id: UUID,
new_status: str,
user_id: Optional[UUID] = None,
reason: Optional[str] = None
) -> Optional[OrderResponse]:
"""Update order status with proper tracking"""
try:
order = await self.order_repo.get(db, order_id, tenant_id)
if not order:
return None
old_status = order.status
# Update order status
order.status = new_status
if new_status == "confirmed":
order.confirmed_delivery_date = order.requested_delivery_date
elif new_status == "delivered":
order.actual_delivery_date = datetime.now()
# Record status change
await self.status_history_repo.create_status_change(
db=db,
order_id=order_id,
from_status=old_status,
to_status=new_status,
change_reason=reason,
changed_by=user_id
)
# Customer notifications
await self._send_status_notification(order, old_status, new_status)
logger.info("Order status updated",
order_id=str(order_id),
old_status=old_status,
new_status=new_status)
return await self.get_order_with_items(db, order_id, tenant_id)
except Exception as e:
logger.error("Error updating order status",
order_id=str(order_id),
error=str(e))
raise
async def get_demand_requirements(
self,
db,
tenant_id: UUID,
target_date: date
) -> DemandRequirements:
"""Get demand requirements for production planning"""
try:
logger.info("Calculating demand requirements",
tenant_id=str(tenant_id),
target_date=str(target_date))
# Get orders for target date
orders = await self.order_repo.get_pending_orders_by_delivery_date(
db, tenant_id, target_date
)
# Aggregate product demands
product_demands = {}
total_orders = len(orders)
total_quantity = Decimal("0")
total_value = Decimal("0")
rush_orders_count = 0
special_requirements = []
earliest_delivery = None
latest_delivery = None
for order in orders:
total_value += order.total_amount
if order.order_type == "rush":
rush_orders_count += 1
if order.special_instructions:
special_requirements.append(order.special_instructions)
# Track delivery timing
if not earliest_delivery or order.requested_delivery_date < earliest_delivery:
earliest_delivery = order.requested_delivery_date
if not latest_delivery or order.requested_delivery_date > latest_delivery:
latest_delivery = order.requested_delivery_date
# Aggregate product demands
for item in order.items:
product_id = str(item.product_id)
if product_id not in product_demands:
product_demands[product_id] = {
"product_id": product_id,
"product_name": item.product_name,
"total_quantity": Decimal("0"),
"unit_of_measure": item.unit_of_measure,
"orders_count": 0,
"rush_quantity": Decimal("0"),
"special_requirements": []
}
product_demands[product_id]["total_quantity"] += item.quantity
product_demands[product_id]["orders_count"] += 1
total_quantity += item.quantity
if order.order_type == "rush":
product_demands[product_id]["rush_quantity"] += item.quantity
if item.special_instructions:
product_demands[product_id]["special_requirements"].append(
item.special_instructions
)
# Calculate average lead time
average_lead_time_hours = 24 # Default
if earliest_delivery and latest_delivery:
time_diff = latest_delivery - earliest_delivery
average_lead_time_hours = max(24, int(time_diff.total_seconds() / 3600))
# Detect business model
business_model = await self.detect_business_model(db, tenant_id)
return DemandRequirements(
date=target_date,
tenant_id=tenant_id,
product_demands=list(product_demands.values()),
total_orders=total_orders,
total_quantity=total_quantity,
total_value=total_value,
business_model=business_model,
rush_orders_count=rush_orders_count,
special_requirements=list(set(special_requirements)),
earliest_delivery=earliest_delivery or datetime.combine(target_date, datetime.min.time()),
latest_delivery=latest_delivery or datetime.combine(target_date, datetime.max.time()),
average_lead_time_hours=average_lead_time_hours
)
except Exception as e:
logger.error("Error calculating demand requirements",
tenant_id=str(tenant_id),
error=str(e))
raise
async def get_dashboard_summary(
self,
db,
tenant_id: UUID
) -> OrdersDashboardSummary:
"""Get dashboard summary data"""
try:
# Get basic metrics
metrics = await self.order_repo.get_dashboard_metrics(db, tenant_id)
# Get customer counts
total_customers = await self.customer_repo.count(
db, tenant_id, filters={"is_active": True}
)
# Get new customers this month
month_start = datetime.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
new_customers_this_month = await self.customer_repo.count(
db,
tenant_id,
filters={"created_at": {"gte": month_start}}
)
# Get recent orders
recent_orders = await self.order_repo.get_multi(
db, tenant_id, limit=5, order_by="order_date", order_desc=True
)
# Get high priority orders
high_priority_orders = await self.order_repo.get_multi(
db,
tenant_id,
filters={"priority": "high", "status": ["pending", "confirmed", "in_production"]},
limit=10
)
# Detect business model
business_model = await self.detect_business_model(db, tenant_id)
# Calculate performance metrics
fulfillment_rate = Decimal("95.0") # Calculate from actual data
on_time_delivery_rate = Decimal("92.0") # Calculate from actual data
repeat_customers_rate = Decimal("65.0") # Calculate from actual data
return OrdersDashboardSummary(
total_orders_today=metrics["total_orders_today"],
total_orders_this_week=metrics["total_orders_this_week"],
total_orders_this_month=metrics["total_orders_this_month"],
revenue_today=metrics["revenue_today"],
revenue_this_week=metrics["revenue_this_week"],
revenue_this_month=metrics["revenue_this_month"],
pending_orders=metrics["status_breakdown"].get("pending", 0),
confirmed_orders=metrics["status_breakdown"].get("confirmed", 0),
in_production_orders=metrics["status_breakdown"].get("in_production", 0),
ready_orders=metrics["status_breakdown"].get("ready", 0),
delivered_orders=metrics["status_breakdown"].get("delivered", 0),
total_customers=total_customers,
new_customers_this_month=new_customers_this_month,
repeat_customers_rate=repeat_customers_rate,
average_order_value=metrics["average_order_value"],
order_fulfillment_rate=fulfillment_rate,
on_time_delivery_rate=on_time_delivery_rate,
business_model=business_model,
business_model_confidence=Decimal("85.0") if business_model else None,
recent_orders=[OrderResponse.from_orm(order) for order in recent_orders],
high_priority_orders=[OrderResponse.from_orm(order) for order in high_priority_orders]
)
except Exception as e:
logger.error("Error getting dashboard summary", error=str(e))
raise
async def detect_business_model(
self,
db,
tenant_id: UUID
) -> Optional[str]:
"""Detect business model based on order patterns"""
try:
if not settings.ENABLE_BUSINESS_MODEL_DETECTION:
return None
return await self.order_repo.detect_business_model(db, tenant_id)
except Exception as e:
logger.error("Error detecting business model", error=str(e))
return None
# ===== Private Helper Methods =====
async def _generate_order_number(self, db, tenant_id: UUID) -> str:
"""Generate unique order number"""
try:
# Simple format: ORD-YYYYMMDD-XXXX
today = datetime.now()
date_part = today.strftime("%Y%m%d")
# Get count of orders today for this tenant
today_start = today.replace(hour=0, minute=0, second=0, microsecond=0)
today_end = today.replace(hour=23, minute=59, second=59, microsecond=999999)
count = await self.order_repo.count(
db,
tenant_id,
filters={
"order_date": {"gte": today_start, "lte": today_end}
}
)
sequence = count + 1
return f"ORD-{date_part}-{sequence:04d}"
except Exception as e:
logger.error("Error generating order number", error=str(e))
# Fallback to UUID
return f"ORD-{uuid.uuid4().hex[:8].upper()}"
async def _check_order_alerts(self, db, order, tenant_id: UUID):
"""Check for conditions that require alerts"""
try:
alerts = []
# High-value order alert
if order.total_amount > settings.HIGH_VALUE_ORDER_THRESHOLD:
alerts.append({
"type": "high_value_order",
"severity": "medium",
"message": f"High-value order created: ${order.total_amount}"
})
# Rush order alert
if order.order_type == "rush":
time_to_delivery = order.requested_delivery_date - order.order_date
if time_to_delivery.total_seconds() < settings.RUSH_ORDER_HOURS_THRESHOLD * 3600:
alerts.append({
"type": "rush_order",
"severity": "high",
"message": f"Rush order with tight deadline: {order.order_number}"
})
# Large quantity alert
total_items = sum(item.quantity for item in order.items)
if total_items > settings.LARGE_QUANTITY_ORDER_THRESHOLD:
alerts.append({
"type": "large_quantity_order",
"severity": "medium",
"message": f"Large quantity order: {total_items} items"
})
# Send alerts if any
for alert in alerts:
await self._send_alert(tenant_id, order.id, alert)
except Exception as e:
logger.error("Error checking order alerts",
order_id=str(order.id),
error=str(e))
async def _notify_production_service(self, order):
"""Notify production service of new order"""
try:
if self.production_client:
await self.production_client.notify_new_order(
str(order.tenant_id),
{
"order_id": str(order.id),
"order_number": order.order_number,
"delivery_date": order.requested_delivery_date.isoformat(),
"priority": order.priority,
"items": [
{
"product_id": str(item.product_id),
"quantity": float(item.quantity),
"unit_of_measure": item.unit_of_measure
}
for item in order.items
]
}
)
except Exception as e:
logger.warning("Failed to notify production service",
order_id=str(order.id),
error=str(e))
async def _send_status_notification(self, order, old_status: str, new_status: str):
"""Send customer notification for status change"""
try:
if self.notification_client and order.customer:
message = f"Order {order.order_number} status changed from {old_status} to {new_status}"
await self.notification_client.send_notification(
str(order.tenant_id),
{
"recipient": order.customer.email,
"message": message,
"type": "order_status_update",
"order_id": str(order.id)
}
)
except Exception as e:
logger.warning("Failed to send status notification",
order_id=str(order.id),
error=str(e))
async def _send_alert(self, tenant_id: UUID, order_id: UUID, alert: Dict[str, Any]):
"""Send alert notification"""
try:
if self.notification_client:
await self.notification_client.send_alert(
str(tenant_id),
{
"alert_type": alert["type"],
"severity": alert["severity"],
"message": alert["message"],
"source_entity_id": str(order_id),
"source_entity_type": "order"
}
)
except Exception as e:
logger.warning("Failed to send alert",
tenant_id=str(tenant_id),
error=str(e))

View File

@@ -0,0 +1,30 @@
# Orders Service Dependencies
# FastAPI and web framework
fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic==2.5.0
pydantic-settings==2.1.0
# Database
sqlalchemy==2.0.23
asyncpg==0.29.0
alembic==1.13.1
# HTTP clients
httpx==0.25.2
# Logging and monitoring
structlog==23.2.0
# Date and time utilities
python-dateutil==2.8.2
# Validation and utilities
email-validator==2.1.0
# Authentication
python-jose[cryptography]==3.3.0
# Development dependencies (optional)
pytest==7.4.3
pytest-asyncio==0.21.1