Initial commit - production deployment
This commit is contained in:
5
services/sales/app/repositories/__init__.py
Normal file
5
services/sales/app/repositories/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# services/sales/app/repositories/__init__.py
|
||||
|
||||
from .sales_repository import SalesRepository
|
||||
|
||||
__all__ = ["SalesRepository"]
|
||||
335
services/sales/app/repositories/sales_repository.py
Normal file
335
services/sales/app/repositories/sales_repository.py
Normal file
@@ -0,0 +1,335 @@
|
||||
# services/sales/app/repositories/sales_repository.py
|
||||
"""
|
||||
Sales Repository using Repository Pattern
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
from sqlalchemy import select, func, and_, or_, desc, asc
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import structlog
|
||||
|
||||
from app.models.sales import SalesData
|
||||
from app.schemas.sales import SalesDataCreate, SalesDataUpdate, SalesDataQuery
|
||||
from shared.database.repository import BaseRepository
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class SalesRepository(BaseRepository[SalesData, SalesDataCreate, SalesDataUpdate]):
|
||||
"""Repository for sales data operations"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
super().__init__(SalesData, session)
|
||||
|
||||
async def create_sales_record(self, sales_data: SalesDataCreate, tenant_id: UUID) -> SalesData:
|
||||
"""Create a new sales record"""
|
||||
try:
|
||||
# Prepare data
|
||||
create_data = sales_data.model_dump()
|
||||
create_data['tenant_id'] = tenant_id
|
||||
|
||||
# Calculate weekend flag if not provided
|
||||
if sales_data.date and create_data.get('is_weekend') is None:
|
||||
create_data['is_weekend'] = sales_data.date.weekday() >= 5
|
||||
|
||||
# Create record
|
||||
record = await self.create(create_data)
|
||||
logger.info(
|
||||
"Created sales record",
|
||||
record_id=record.id,
|
||||
inventory_product_id=record.inventory_product_id,
|
||||
quantity=record.quantity_sold,
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
return record
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to create sales record", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def get_by_tenant(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
query_params: Optional[SalesDataQuery] = None
|
||||
) -> List[SalesData]:
|
||||
"""Get sales records by tenant with optional filtering"""
|
||||
try:
|
||||
# Build base query
|
||||
stmt = select(SalesData).where(SalesData.tenant_id == tenant_id)
|
||||
|
||||
# Apply filters if query_params provided
|
||||
if query_params:
|
||||
if query_params.start_date:
|
||||
stmt = stmt.where(SalesData.date >= query_params.start_date)
|
||||
if query_params.end_date:
|
||||
stmt = stmt.where(SalesData.date <= query_params.end_date)
|
||||
# Note: product_name queries now require joining with inventory service
|
||||
# if query_params.product_name:
|
||||
# # Would need to join with inventory service to filter by product name
|
||||
# pass
|
||||
# Note: product_category field was removed - filtering by category now requires inventory service
|
||||
# if query_params.product_category:
|
||||
# # Would need to join with inventory service to filter by product category
|
||||
# pass
|
||||
if hasattr(query_params, 'inventory_product_id') and query_params.inventory_product_id:
|
||||
stmt = stmt.where(SalesData.inventory_product_id == query_params.inventory_product_id)
|
||||
if query_params.location_id:
|
||||
stmt = stmt.where(SalesData.location_id == query_params.location_id)
|
||||
if query_params.sales_channel:
|
||||
stmt = stmt.where(SalesData.sales_channel == query_params.sales_channel)
|
||||
if query_params.source:
|
||||
stmt = stmt.where(SalesData.source == query_params.source)
|
||||
if query_params.is_validated is not None:
|
||||
stmt = stmt.where(SalesData.is_validated == query_params.is_validated)
|
||||
|
||||
# Apply ordering
|
||||
if query_params.order_by and hasattr(SalesData, query_params.order_by):
|
||||
order_col = getattr(SalesData, query_params.order_by)
|
||||
if query_params.order_direction == 'asc':
|
||||
stmt = stmt.order_by(asc(order_col))
|
||||
else:
|
||||
stmt = stmt.order_by(desc(order_col))
|
||||
else:
|
||||
stmt = stmt.order_by(desc(SalesData.date))
|
||||
|
||||
# Apply pagination
|
||||
stmt = stmt.offset(query_params.offset).limit(query_params.limit)
|
||||
else:
|
||||
# Default ordering with safety limit for direct repository calls
|
||||
# Note: API calls always provide query_params, so this only applies to direct usage
|
||||
stmt = stmt.order_by(desc(SalesData.date)).limit(10000)
|
||||
|
||||
result = await self.session.execute(stmt)
|
||||
records = result.scalars().all()
|
||||
|
||||
logger.info(
|
||||
"Retrieved sales records",
|
||||
count=len(records),
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
return list(records)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get sales records", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def get_by_inventory_product(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
inventory_product_id: UUID,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None
|
||||
) -> List[SalesData]:
|
||||
"""Get sales records for a specific inventory product"""
|
||||
try:
|
||||
stmt = select(SalesData).where(
|
||||
and_(
|
||||
SalesData.tenant_id == tenant_id,
|
||||
SalesData.inventory_product_id == inventory_product_id
|
||||
)
|
||||
)
|
||||
|
||||
if start_date:
|
||||
stmt = stmt.where(SalesData.date >= start_date)
|
||||
if end_date:
|
||||
stmt = stmt.where(SalesData.date <= end_date)
|
||||
|
||||
stmt = stmt.order_by(desc(SalesData.date))
|
||||
|
||||
result = await self.session.execute(stmt)
|
||||
records = result.scalars().all()
|
||||
|
||||
return list(records)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get product sales", error=str(e), tenant_id=tenant_id, inventory_product_id=inventory_product_id)
|
||||
raise
|
||||
|
||||
async def get_analytics(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Get sales analytics for a tenant"""
|
||||
try:
|
||||
# Build base query
|
||||
base_query = select(SalesData).where(SalesData.tenant_id == tenant_id)
|
||||
|
||||
if start_date:
|
||||
base_query = base_query.where(SalesData.date >= start_date)
|
||||
if end_date:
|
||||
base_query = base_query.where(SalesData.date <= end_date)
|
||||
|
||||
# Total revenue and quantity
|
||||
summary_query = select(
|
||||
func.sum(SalesData.revenue).label('total_revenue'),
|
||||
func.sum(SalesData.quantity_sold).label('total_quantity'),
|
||||
func.count().label('total_transactions'),
|
||||
func.avg(SalesData.revenue).label('avg_transaction_value')
|
||||
).where(SalesData.tenant_id == tenant_id)
|
||||
|
||||
if start_date:
|
||||
summary_query = summary_query.where(SalesData.date >= start_date)
|
||||
if end_date:
|
||||
summary_query = summary_query.where(SalesData.date <= end_date)
|
||||
|
||||
result = await self.session.execute(summary_query)
|
||||
summary = result.first()
|
||||
|
||||
# Top products
|
||||
top_products_query = select(
|
||||
SalesData.inventory_product_id, # Note: was product_name
|
||||
func.sum(SalesData.revenue).label('revenue'),
|
||||
func.sum(SalesData.quantity_sold).label('quantity')
|
||||
).where(SalesData.tenant_id == tenant_id)
|
||||
|
||||
if start_date:
|
||||
top_products_query = top_products_query.where(SalesData.date >= start_date)
|
||||
if end_date:
|
||||
top_products_query = top_products_query.where(SalesData.date <= end_date)
|
||||
|
||||
top_products_query = top_products_query.group_by(
|
||||
SalesData.inventory_product_id # Note: was product_name
|
||||
).order_by(
|
||||
desc(func.sum(SalesData.revenue))
|
||||
).limit(10)
|
||||
|
||||
top_products_result = await self.session.execute(top_products_query)
|
||||
top_products = [
|
||||
{
|
||||
'inventory_product_id': str(row.inventory_product_id), # Note: was product_name
|
||||
'revenue': float(row.revenue) if row.revenue else 0,
|
||||
'quantity': row.quantity or 0
|
||||
}
|
||||
for row in top_products_result
|
||||
]
|
||||
|
||||
# Sales by channel
|
||||
channel_query = select(
|
||||
SalesData.sales_channel,
|
||||
func.sum(SalesData.revenue).label('revenue'),
|
||||
func.count().label('transactions')
|
||||
).where(SalesData.tenant_id == tenant_id)
|
||||
|
||||
if start_date:
|
||||
channel_query = channel_query.where(SalesData.date >= start_date)
|
||||
if end_date:
|
||||
channel_query = channel_query.where(SalesData.date <= end_date)
|
||||
|
||||
channel_query = channel_query.group_by(SalesData.sales_channel)
|
||||
|
||||
channel_result = await self.session.execute(channel_query)
|
||||
sales_by_channel = {
|
||||
row.sales_channel: {
|
||||
'revenue': float(row.revenue) if row.revenue else 0,
|
||||
'transactions': row.transactions or 0
|
||||
}
|
||||
for row in channel_result
|
||||
}
|
||||
|
||||
return {
|
||||
'total_revenue': float(summary.total_revenue) if summary.total_revenue else 0,
|
||||
'total_quantity': summary.total_quantity or 0,
|
||||
'total_transactions': summary.total_transactions or 0,
|
||||
'average_transaction_value': float(summary.avg_transaction_value) if summary.avg_transaction_value else 0,
|
||||
'top_products': top_products,
|
||||
'sales_by_channel': sales_by_channel
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get sales analytics", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def get_product_categories(self, tenant_id: UUID) -> List[str]:
|
||||
"""Get distinct product categories for a tenant"""
|
||||
try:
|
||||
# Note: product_category field was removed - categories now managed via inventory service
|
||||
# This method should be updated to query categories from inventory service
|
||||
# For now, return empty list to avoid breaking existing code
|
||||
logger.warning("get_product_categories called but product_category field was removed",
|
||||
tenant_id=tenant_id)
|
||||
categories = []
|
||||
|
||||
return sorted(categories)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get product categories", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def validate_record(self, record_id: UUID, validation_notes: Optional[str] = None) -> SalesData:
|
||||
"""Mark a sales record as validated"""
|
||||
try:
|
||||
record = await self.get_by_id(record_id)
|
||||
if not record:
|
||||
raise ValueError(f"Sales record {record_id} not found")
|
||||
|
||||
update_data = {
|
||||
'is_validated': True,
|
||||
'validation_notes': validation_notes
|
||||
}
|
||||
|
||||
updated_record = await self.update(record_id, update_data)
|
||||
|
||||
logger.info("Validated sales record", record_id=record_id)
|
||||
return updated_record
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to validate sales record", error=str(e), record_id=record_id)
|
||||
raise
|
||||
|
||||
async def create_sales_records_bulk(
|
||||
self,
|
||||
sales_data_list: List[SalesDataCreate],
|
||||
tenant_id: UUID,
|
||||
return_records: bool = False
|
||||
) -> int | List[SalesData]:
|
||||
"""Bulk insert sales records for performance optimization
|
||||
|
||||
Args:
|
||||
sales_data_list: List of sales data to create
|
||||
tenant_id: Tenant ID
|
||||
return_records: If True, returns list of created records instead of count
|
||||
|
||||
Returns:
|
||||
Either count of created records (int) or list of created records (List[SalesData])
|
||||
"""
|
||||
try:
|
||||
from uuid import uuid4
|
||||
|
||||
records = []
|
||||
for sales_data in sales_data_list:
|
||||
is_weekend = sales_data.date.weekday() >= 5 if sales_data.date else False
|
||||
|
||||
record = SalesData(
|
||||
id=uuid4(),
|
||||
tenant_id=tenant_id,
|
||||
date=sales_data.date,
|
||||
inventory_product_id=sales_data.inventory_product_id,
|
||||
quantity_sold=sales_data.quantity_sold,
|
||||
unit_price=sales_data.unit_price,
|
||||
revenue=sales_data.revenue,
|
||||
location_id=sales_data.location_id,
|
||||
sales_channel=sales_data.sales_channel,
|
||||
source=sales_data.source,
|
||||
is_weekend=is_weekend,
|
||||
is_validated=getattr(sales_data, 'is_validated', False)
|
||||
)
|
||||
records.append(record)
|
||||
|
||||
self.session.add_all(records)
|
||||
await self.session.flush()
|
||||
|
||||
logger.info(
|
||||
"Bulk created sales records",
|
||||
count=len(records),
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
|
||||
return records if return_records else len(records)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to bulk create sales records", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
Reference in New Issue
Block a user