Add more services
This commit is contained in:
546
services/orders/app/services/orders_service.py
Normal file
546
services/orders/app/services/orders_service.py
Normal 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))
|
||||
Reference in New Issue
Block a user