Files
bakery-ia/services/data/app/services/sales_service.py
2025-08-08 09:08:41 +02:00

292 lines
13 KiB
Python

"""
Sales Service with Repository Pattern
Enhanced service using the new repository architecture for better separation of concerns
"""
from typing import List, Dict, Any, Optional
from datetime import datetime
import structlog
from app.repositories.sales_repository import SalesRepository
from app.models.sales import SalesData
from app.schemas.sales import (
SalesDataCreate,
SalesDataResponse,
SalesDataQuery,
SalesAggregation,
SalesImportResult,
SalesValidationResult
)
from shared.database.unit_of_work import UnitOfWork
from shared.database.transactions import transactional
from shared.database.exceptions import DatabaseError, ValidationError
logger = structlog.get_logger()
class SalesService:
"""Enhanced Sales Service using Repository Pattern and Unit of Work"""
def __init__(self, database_manager):
"""Initialize service with database manager for dependency injection"""
self.database_manager = database_manager
async def create_sales_record(self, sales_data: SalesDataCreate, tenant_id: str) -> SalesDataResponse:
"""Create a new sales record using repository pattern"""
try:
async with self.database_manager.get_session() as session:
async with UnitOfWork(session) as uow:
# Register sales repository
sales_repo = uow.register_repository("sales", SalesRepository, SalesData)
# Ensure tenant_id is set
record_data = sales_data.model_dump()
record_data["tenant_id"] = tenant_id
# Validate the data first
validation_result = await sales_repo.validate_sales_data(record_data)
if not validation_result["is_valid"]:
raise ValidationError(f"Invalid sales data: {validation_result['errors']}")
# Create the record
db_record = await sales_repo.create(record_data)
# Commit transaction
await uow.commit()
logger.debug("Sales record created",
record_id=db_record.id,
product=db_record.product_name,
tenant_id=tenant_id)
return SalesDataResponse.model_validate(db_record)
except ValidationError:
raise
except Exception as e:
logger.error("Failed to create sales record",
tenant_id=tenant_id,
product=sales_data.product_name,
error=str(e))
raise DatabaseError(f"Failed to create sales record: {str(e)}")
async def get_sales_data(self, query: SalesDataQuery) -> List[SalesDataResponse]:
"""Get sales data based on query parameters using repository pattern"""
try:
async with self.database_manager.get_session() as session:
async with UnitOfWork(session) as uow:
sales_repo = uow.register_repository("sales", SalesRepository, SalesData)
# Use repository's advanced query method
records = await sales_repo.get_by_tenant_and_date_range(
tenant_id=str(query.tenant_id),
start_date=query.start_date,
end_date=query.end_date,
product_names=query.product_names,
location_ids=query.location_ids,
skip=query.offset or 0,
limit=query.limit or 100
)
logger.debug("Sales data retrieved",
count=len(records),
tenant_id=query.tenant_id)
return [SalesDataResponse.model_validate(record) for record in records]
except Exception as e:
logger.error("Failed to retrieve sales data",
tenant_id=query.tenant_id,
error=str(e))
raise DatabaseError(f"Failed to retrieve sales data: {str(e)}")
async def get_sales_analytics(self, tenant_id: str, start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None) -> Dict[str, Any]:
"""Get comprehensive sales analytics using repository pattern"""
try:
async with self.database_manager.get_session() as session:
async with UnitOfWork(session) as uow:
sales_repo = uow.register_repository("sales", SalesRepository, SalesData)
# Get summary data
summary = await sales_repo.get_sales_summary(
tenant_id=tenant_id,
start_date=start_date,
end_date=end_date
)
# Get top products
top_products = await sales_repo.get_top_products(
tenant_id=tenant_id,
start_date=start_date,
end_date=end_date,
limit=5
)
# Get aggregated data by day
daily_aggregation = await sales_repo.get_sales_aggregation(
tenant_id=tenant_id,
start_date=start_date,
end_date=end_date,
group_by="daily"
)
analytics = {
**summary,
"top_products": top_products,
"daily_sales": daily_aggregation[:30], # Last 30 days
"average_order_value": (
summary["total_revenue"] / max(summary["total_sales"], 1)
if summary["total_sales"] > 0 else 0.0
)
}
logger.debug("Sales analytics generated",
tenant_id=tenant_id,
total_records=analytics["total_sales"])
return analytics
except Exception as e:
logger.error("Failed to generate sales analytics",
tenant_id=tenant_id,
error=str(e))
raise DatabaseError(f"Failed to generate analytics: {str(e)}")
async def get_sales_aggregation(self, tenant_id: str, start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None, group_by: str = "daily") -> List[SalesAggregation]:
"""Get sales aggregation data"""
try:
async with self.database_manager.get_session() as session:
async with UnitOfWork(session) as uow:
sales_repo = uow.register_repository("sales", SalesRepository, SalesData)
aggregations = await sales_repo.get_sales_aggregation(
tenant_id=tenant_id,
start_date=start_date,
end_date=end_date,
group_by=group_by
)
return [
SalesAggregation(
period=agg["period"],
date=agg["date"],
product_name=agg["product_name"],
total_quantity=agg["total_quantity"],
total_revenue=agg["total_revenue"],
average_quantity=agg["average_quantity"],
average_revenue=agg["average_revenue"],
record_count=agg["record_count"]
)
for agg in aggregations
]
except Exception as e:
logger.error("Failed to get sales aggregation",
tenant_id=tenant_id,
error=str(e))
raise DatabaseError(f"Failed to get aggregation: {str(e)}")
async def export_sales_data(self, tenant_id: str, export_format: str, start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None, products: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
"""Export sales data in specified format using repository pattern"""
try:
async with self.database_manager.get_session() as session:
async with UnitOfWork(session) as uow:
sales_repo = uow.register_repository("sales", SalesRepository, SalesData)
# Get sales data based on filters
records = await sales_repo.get_by_tenant_and_date_range(
tenant_id=tenant_id,
start_date=start_date,
end_date=end_date,
product_names=products,
skip=0,
limit=10000 # Large limit for export
)
if not records:
return None
# Simple CSV export
if export_format.lower() == "csv":
import io
output = io.StringIO()
output.write("date,product_name,quantity_sold,revenue,location_id,source\n")
for record in records:
output.write(f"{record.date},{record.product_name},{record.quantity_sold},{record.revenue},{record.location_id or ''},{record.source}\n")
logger.info("Sales data exported",
tenant_id=tenant_id,
format=export_format,
record_count=len(records))
return {
"content": output.getvalue(),
"media_type": "text/csv",
"filename": f"sales_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
}
return None
except Exception as e:
logger.error("Failed to export sales data",
tenant_id=tenant_id,
error=str(e))
raise DatabaseError(f"Failed to export sales data: {str(e)}")
async def delete_sales_record(self, record_id: str, tenant_id: str) -> bool:
"""Delete a sales record using repository pattern"""
try:
async with self.database_manager.get_session() as session:
async with UnitOfWork(session) as uow:
sales_repo = uow.register_repository("sales", SalesRepository, SalesData)
# First verify the record exists and belongs to the tenant
record = await sales_repo.get_by_id(record_id)
if not record:
return False
if str(record.tenant_id) != tenant_id:
raise ValidationError("Record does not belong to the specified tenant")
# Delete the record
success = await sales_repo.delete(record_id)
if success:
logger.info("Sales record deleted",
record_id=record_id,
tenant_id=tenant_id)
return success
except ValidationError:
raise
except Exception as e:
logger.error("Failed to delete sales record",
record_id=record_id,
error=str(e))
raise DatabaseError(f"Failed to delete sales record: {str(e)}")
async def get_products_list(self, tenant_id: str) -> List[Dict[str, Any]]:
"""Get list of all products with sales data for tenant using repository pattern"""
try:
async with self.database_manager.get_session() as session:
async with UnitOfWork(session) as uow:
sales_repo = uow.register_repository("sales", SalesRepository, SalesData)
# Use repository method for product statistics
products = await sales_repo.get_product_statistics(tenant_id)
logger.debug("Products list retrieved successfully",
tenant_id=tenant_id,
product_count=len(products))
return products
except Exception as e:
logger.error("Failed to get products list",
error=str(e),
tenant_id=tenant_id)
raise DatabaseError(f"Failed to get products list: {str(e)}")