486 lines
19 KiB
Python
486 lines
19 KiB
Python
# ================================================================
|
|
# 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.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,
|
|
):
|
|
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
|
|
|
|
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
|
|
|
|
|
|
# 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
|
|
|
|
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_created_since(
|
|
db,
|
|
tenant_id,
|
|
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 from actual data
|
|
fulfillment_rate = metrics.get("fulfillment_rate", Decimal("0.0")) # Use actual calculated rate
|
|
on_time_delivery_rate = metrics.get("on_time_delivery_rate", Decimal("0.0")) # Use actual calculated rate
|
|
repeat_customers_rate = metrics.get("repeat_customers_rate", Decimal("0.0")) # Use actual calculated rate
|
|
|
|
# Use the actual calculated values from the repository
|
|
order_fulfillment_rate = metrics.get("fulfillment_rate", Decimal("0.0"))
|
|
on_time_delivery_rate_metric = metrics.get("on_time_delivery_rate", Decimal("0.0"))
|
|
repeat_customers_rate_metric = metrics.get("repeat_customers_rate", Decimal("0.0"))
|
|
|
|
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_metric,
|
|
average_order_value=metrics["average_order_value"],
|
|
order_fulfillment_rate=order_fulfillment_rate,
|
|
on_time_delivery_rate=on_time_delivery_rate_metric,
|
|
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 _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))
|