REFACTOR data service
This commit is contained in:
6
services/sales/app/repositories/__init__.py
Normal file
6
services/sales/app/repositories/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# services/sales/app/repositories/__init__.py
|
||||
|
||||
from .sales_repository import SalesRepository
|
||||
from .product_repository import ProductRepository
|
||||
|
||||
__all__ = ["SalesRepository", "ProductRepository"]
|
||||
193
services/sales/app/repositories/product_repository.py
Normal file
193
services/sales/app/repositories/product_repository.py
Normal file
@@ -0,0 +1,193 @@
|
||||
# services/sales/app/repositories/product_repository.py
|
||||
"""
|
||||
Product Repository using Repository Pattern
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
from sqlalchemy import select, and_, or_, desc, asc
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import structlog
|
||||
|
||||
from app.models.sales import Product
|
||||
from app.schemas.sales import ProductCreate, ProductUpdate
|
||||
from shared.database.repository import BaseRepository
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class ProductRepository(BaseRepository[Product, ProductCreate, ProductUpdate]):
|
||||
"""Repository for product operations"""
|
||||
|
||||
def __init__(self, db_session: AsyncSession):
|
||||
super().__init__(Product, db_session)
|
||||
|
||||
async def create_product(self, product_data: ProductCreate, tenant_id: UUID) -> Product:
|
||||
"""Create a new product"""
|
||||
try:
|
||||
# Prepare data
|
||||
create_data = product_data.model_dump()
|
||||
create_data['tenant_id'] = tenant_id
|
||||
|
||||
# Create product
|
||||
product = await self.create(create_data)
|
||||
logger.info(
|
||||
"Created product",
|
||||
product_id=product.id,
|
||||
name=product.name,
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
return product
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to create product", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def get_by_tenant(self, tenant_id: UUID, include_inactive: bool = False) -> List[Product]:
|
||||
"""Get all products for a tenant"""
|
||||
try:
|
||||
stmt = select(Product).where(Product.tenant_id == tenant_id)
|
||||
|
||||
if not include_inactive:
|
||||
stmt = stmt.where(Product.is_active == True)
|
||||
|
||||
stmt = stmt.order_by(Product.category, Product.name)
|
||||
|
||||
result = await self.db_session.execute(stmt)
|
||||
products = result.scalars().all()
|
||||
|
||||
logger.info(
|
||||
"Retrieved products",
|
||||
count=len(products),
|
||||
tenant_id=tenant_id,
|
||||
include_inactive=include_inactive
|
||||
)
|
||||
return list(products)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get products", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def get_by_category(self, tenant_id: UUID, category: str) -> List[Product]:
|
||||
"""Get products by category"""
|
||||
try:
|
||||
stmt = select(Product).where(
|
||||
and_(
|
||||
Product.tenant_id == tenant_id,
|
||||
Product.category == category,
|
||||
Product.is_active == True
|
||||
)
|
||||
).order_by(Product.name)
|
||||
|
||||
result = await self.db_session.execute(stmt)
|
||||
products = result.scalars().all()
|
||||
|
||||
return list(products)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get products by category", error=str(e), tenant_id=tenant_id, category=category)
|
||||
raise
|
||||
|
||||
async def get_by_name(self, tenant_id: UUID, name: str) -> Optional[Product]:
|
||||
"""Get product by name"""
|
||||
try:
|
||||
stmt = select(Product).where(
|
||||
and_(
|
||||
Product.tenant_id == tenant_id,
|
||||
Product.name == name
|
||||
)
|
||||
)
|
||||
|
||||
result = await self.db_session.execute(stmt)
|
||||
product = result.scalar_one_or_none()
|
||||
|
||||
return product
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get product by name", error=str(e), tenant_id=tenant_id, name=name)
|
||||
raise
|
||||
|
||||
async def get_by_sku(self, tenant_id: UUID, sku: str) -> Optional[Product]:
|
||||
"""Get product by SKU"""
|
||||
try:
|
||||
stmt = select(Product).where(
|
||||
and_(
|
||||
Product.tenant_id == tenant_id,
|
||||
Product.sku == sku
|
||||
)
|
||||
)
|
||||
|
||||
result = await self.db_session.execute(stmt)
|
||||
product = result.scalar_one_or_none()
|
||||
|
||||
return product
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get product by SKU", error=str(e), tenant_id=tenant_id, sku=sku)
|
||||
raise
|
||||
|
||||
async def search_products(self, tenant_id: UUID, query: str, limit: int = 50) -> List[Product]:
|
||||
"""Search products by name or SKU"""
|
||||
try:
|
||||
stmt = select(Product).where(
|
||||
and_(
|
||||
Product.tenant_id == tenant_id,
|
||||
Product.is_active == True,
|
||||
or_(
|
||||
Product.name.ilike(f"%{query}%"),
|
||||
Product.sku.ilike(f"%{query}%"),
|
||||
Product.description.ilike(f"%{query}%")
|
||||
)
|
||||
)
|
||||
).order_by(Product.name).limit(limit)
|
||||
|
||||
result = await self.db_session.execute(stmt)
|
||||
products = result.scalars().all()
|
||||
|
||||
return list(products)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to search products", error=str(e), tenant_id=tenant_id, query=query)
|
||||
raise
|
||||
|
||||
async def get_categories(self, tenant_id: UUID) -> List[str]:
|
||||
"""Get distinct product categories for a tenant"""
|
||||
try:
|
||||
stmt = select(Product.category).where(
|
||||
and_(
|
||||
Product.tenant_id == tenant_id,
|
||||
Product.is_active == True,
|
||||
Product.category.is_not(None)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
result = await self.db_session.execute(stmt)
|
||||
categories = [row[0] for row in result if row[0]]
|
||||
|
||||
return sorted(categories)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get product categories", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def deactivate_product(self, product_id: UUID) -> Product:
|
||||
"""Deactivate a product"""
|
||||
try:
|
||||
product = await self.update(product_id, {'is_active': False})
|
||||
logger.info("Deactivated product", product_id=product_id)
|
||||
return product
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to deactivate product", error=str(e), product_id=product_id)
|
||||
raise
|
||||
|
||||
async def activate_product(self, product_id: UUID) -> Product:
|
||||
"""Activate a product"""
|
||||
try:
|
||||
product = await self.update(product_id, {'is_active': True})
|
||||
logger.info("Activated product", product_id=product_id)
|
||||
return product
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to activate product", error=str(e), product_id=product_id)
|
||||
raise
|
||||
296
services/sales/app/repositories/sales_repository.py
Normal file
296
services/sales/app/repositories/sales_repository.py
Normal file
@@ -0,0 +1,296 @@
|
||||
# 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,
|
||||
product=record.product_name,
|
||||
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)
|
||||
if query_params.product_name:
|
||||
stmt = stmt.where(SalesData.product_name.ilike(f"%{query_params.product_name}%"))
|
||||
if query_params.product_category:
|
||||
stmt = stmt.where(SalesData.product_category == query_params.product_category)
|
||||
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
|
||||
stmt = stmt.order_by(desc(SalesData.date)).limit(50)
|
||||
|
||||
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_product(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
product_name: str,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None
|
||||
) -> List[SalesData]:
|
||||
"""Get sales records for a specific product"""
|
||||
try:
|
||||
stmt = select(SalesData).where(
|
||||
and_(
|
||||
SalesData.tenant_id == tenant_id,
|
||||
SalesData.product_name == product_name
|
||||
)
|
||||
)
|
||||
|
||||
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, product=product_name)
|
||||
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.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.product_name
|
||||
).order_by(
|
||||
desc(func.sum(SalesData.revenue))
|
||||
).limit(10)
|
||||
|
||||
top_products_result = await self.session.execute(top_products_query)
|
||||
top_products = [
|
||||
{
|
||||
'product_name': row.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:
|
||||
stmt = select(SalesData.product_category).where(
|
||||
and_(
|
||||
SalesData.tenant_id == tenant_id,
|
||||
SalesData.product_category.is_not(None)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
result = await self.session.execute(stmt)
|
||||
categories = [row[0] for row in result if row[0]]
|
||||
|
||||
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 get_product_statistics(self, tenant_id: str) -> List[Dict[str, Any]]:
|
||||
"""Get product statistics for tenant"""
|
||||
try:
|
||||
stmt = select(SalesData.product_name).where(
|
||||
and_(
|
||||
SalesData.tenant_id == tenant_id,
|
||||
SalesData.product_name.is_not(None)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
result = await self.session.execute(stmt)
|
||||
products = [row[0] for row in result if row[0]]
|
||||
|
||||
return sorted(products)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get product categories", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
Reference in New Issue
Block a user