Initial commit - production deployment

This commit is contained in:
2026-01-21 17:17:16 +01:00
commit c23d00dd92
2289 changed files with 638440 additions and 0 deletions

View File

@@ -0,0 +1,289 @@
# ================================================================
# 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,
tenant_id: 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 tenant_id if the model supports it and it's provided
if tenant_id and hasattr(self.model, 'tenant_id'):
obj_data['tenant_id'] = tenant_id
# 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,628 @@
# ================================================================
# services/orders/app/repositories/order_repository.py
# ================================================================
"""
Order-related repositories for Orders Service
"""
from datetime import datetime, date, timedelta
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
async def count_created_since(
self,
db: AsyncSession,
tenant_id: UUID,
since_date: datetime
) -> int:
"""Count customers created since a specific date"""
try:
query = select(func.count()).select_from(Customer).where(
and_(
Customer.tenant_id == tenant_id,
Customer.created_at >= since_date
)
)
result = await db.execute(query)
return result.scalar()
except Exception as e:
logger.error("Error counting customers created since date",
tenant_id=str(tenant_id),
since_date=str(since_date),
error=str(e))
raise
class OrderRepository(BaseRepository[CustomerOrder, OrderCreate, OrderUpdate]):
"""Repository for customer order operations"""
def __init__(self):
super().__init__(CustomerOrder)
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[CustomerOrder]:
"""Get multiple orders with eager loading of items and customer"""
try:
query = select(self.model).options(
selectinload(CustomerOrder.items),
selectinload(CustomerOrder.customer)
)
# Apply tenant filter
if tenant_id:
query = query.where(self.model.tenant_id == tenant_id)
# Apply additional filters
if filters:
for key, value in filters.items():
if hasattr(self.model, key) and value is not None:
field = getattr(self.model, key)
if isinstance(value, list):
query = query.where(field.in_(value))
else:
query = query.where(field == value)
# Apply ordering
if order_by and hasattr(self.model, order_by):
order_column = getattr(self.model, order_by)
if order_desc:
query = query.order_by(order_column.desc())
else:
query = query.order_by(order_column)
else:
# Default ordering by order_date desc
query = query.order_by(CustomerOrder.order_date.desc())
# Apply 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 orders", error=str(e))
raise
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"
)
)
)
# Calculate repeat customers rate
# Count customers who have made more than one order
repeat_customers_query = await db.execute(
select(func.count()).select_from(
select(CustomerOrder.customer_id)
.where(CustomerOrder.tenant_id == tenant_id)
.group_by(CustomerOrder.customer_id)
.having(func.count(CustomerOrder.id) > 1)
.subquery()
)
)
total_customers_query = await db.execute(
select(func.count(func.distinct(CustomerOrder.customer_id))).where(
CustomerOrder.tenant_id == tenant_id
)
)
repeat_customers_count = repeat_customers_query.scalar() or 0
total_customers_count = total_customers_query.scalar() or 0
repeat_customers_rate = Decimal("0.0")
if total_customers_count > 0:
repeat_customers_rate = Decimal(str(repeat_customers_count)) / Decimal(str(total_customers_count))
repeat_customers_rate = repeat_customers_rate * Decimal("100.0") # Convert to percentage
# Calculate order fulfillment rate
total_orders_query = await db.execute(
select(func.count()).where(
and_(
CustomerOrder.tenant_id == tenant_id,
CustomerOrder.status != "cancelled"
)
)
)
fulfilled_orders_query = await db.execute(
select(func.count()).where(
and_(
CustomerOrder.tenant_id == tenant_id,
CustomerOrder.status.in_(["delivered", "completed"])
)
)
)
total_orders_count = total_orders_query.scalar() or 0
fulfilled_orders_count = fulfilled_orders_query.scalar() or 0
fulfillment_rate = Decimal("0.0")
if total_orders_count > 0:
fulfillment_rate = Decimal(str(fulfilled_orders_count)) / Decimal(str(total_orders_count))
fulfillment_rate = fulfillment_rate * Decimal("100.0") # Convert to percentage
# Calculate on-time delivery rate
on_time_delivered_query = await db.execute(
select(func.count()).where(
and_(
CustomerOrder.tenant_id == tenant_id,
CustomerOrder.status == "delivered",
CustomerOrder.actual_delivery_date <= CustomerOrder.requested_delivery_date
)
)
)
total_delivered_query = await db.execute(
select(func.count()).where(
and_(
CustomerOrder.tenant_id == tenant_id,
CustomerOrder.status == "delivered"
)
)
)
on_time_delivered_count = on_time_delivered_query.scalar() or 0
total_delivered_count = total_delivered_query.scalar() or 0
on_time_delivery_rate = Decimal("0.0")
if total_delivered_count > 0:
on_time_delivery_rate = Decimal(str(on_time_delivered_count)) / Decimal(str(total_delivered_count))
on_time_delivery_rate = on_time_delivery_rate * Decimal("100.0") # Convert to percentage
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(),
"repeat_customers_rate": repeat_customers_rate,
"fulfillment_rate": fulfillment_rate,
"on_time_delivery_rate": on_time_delivery_rate,
"repeat_customers_count": repeat_customers_count,
"total_customers_count": total_customers_count,
"total_orders_count": total_orders_count,
"fulfilled_orders_count": fulfilled_orders_count,
"on_time_delivered_count": on_time_delivered_count,
"total_delivered_count": total_delivered_count
}
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