Files
bakery-ia/services/orders/app/services/orders_service.py
2025-10-27 16:33:26 +01:00

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))