292 lines
13 KiB
Python
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)}") |