REFACTOR data service

This commit is contained in:
Urtzi Alfaro
2025-08-12 18:17:30 +02:00
parent 7c237c0acc
commit fbe7470ad9
149 changed files with 8528 additions and 7393 deletions

View File

@@ -0,0 +1,6 @@
# services/sales/app/repositories/__init__.py
from .sales_repository import SalesRepository
from .product_repository import ProductRepository
__all__ = ["SalesRepository", "ProductRepository"]

View 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

View 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