Files
bakery-ia/services/sales/app/models/sales.py
2025-08-12 18:17:30 +02:00

238 lines
10 KiB
Python

# services/sales/app/models/sales.py
"""
Sales data models for Sales Service
Enhanced with additional fields and relationships
"""
from sqlalchemy import Column, String, DateTime, Float, Integer, Text, Index, Boolean, Numeric
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
import uuid
from datetime import datetime, timezone
from typing import Dict, Any, Optional
from shared.database.base import Base
class SalesData(Base):
"""Enhanced sales data model"""
__tablename__ = "sales_data"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
date = Column(DateTime(timezone=True), nullable=False, index=True)
# Product information
product_name = Column(String(255), nullable=False, index=True)
product_category = Column(String(100), nullable=True, index=True)
product_sku = Column(String(100), nullable=True, index=True)
# Sales data
quantity_sold = Column(Integer, nullable=False)
unit_price = Column(Numeric(10, 2), nullable=True)
revenue = Column(Numeric(10, 2), nullable=False)
cost_of_goods = Column(Numeric(10, 2), nullable=True) # For profit calculation
discount_applied = Column(Numeric(5, 2), nullable=True, default=0.0) # Percentage
# Location and channel
location_id = Column(String(100), nullable=True, index=True)
sales_channel = Column(String(50), nullable=True, default="in_store") # in_store, online, delivery
# Data source and quality
source = Column(String(50), nullable=False, default="manual") # manual, pos, online, import
is_validated = Column(Boolean, default=False)
validation_notes = Column(Text, nullable=True)
# Additional metadata
notes = Column(Text, nullable=True)
weather_condition = Column(String(50), nullable=True) # For correlation analysis
is_holiday = Column(Boolean, default=False)
is_weekend = Column(Boolean, default=False)
# Audit fields
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc))
created_by = Column(UUID(as_uuid=True), nullable=True) # User ID
# Performance-optimized indexes
__table_args__ = (
# Core query patterns
Index('idx_sales_tenant_date', 'tenant_id', 'date'),
Index('idx_sales_tenant_product', 'tenant_id', 'product_name'),
Index('idx_sales_tenant_location', 'tenant_id', 'location_id'),
Index('idx_sales_tenant_category', 'tenant_id', 'product_category'),
# Analytics queries
Index('idx_sales_date_range', 'date', 'tenant_id'),
Index('idx_sales_product_date', 'product_name', 'date', 'tenant_id'),
Index('idx_sales_channel_date', 'sales_channel', 'date', 'tenant_id'),
# Data quality queries
Index('idx_sales_source_validated', 'source', 'is_validated', 'tenant_id'),
Index('idx_sales_sku_date', 'product_sku', 'date', 'tenant_id'),
)
def to_dict(self) -> Dict[str, Any]:
"""Convert model to dictionary for API responses"""
return {
'id': str(self.id),
'tenant_id': str(self.tenant_id),
'date': self.date.isoformat() if self.date else None,
'product_name': self.product_name,
'product_category': self.product_category,
'product_sku': self.product_sku,
'quantity_sold': self.quantity_sold,
'unit_price': float(self.unit_price) if self.unit_price else None,
'revenue': float(self.revenue) if self.revenue else None,
'cost_of_goods': float(self.cost_of_goods) if self.cost_of_goods else None,
'discount_applied': float(self.discount_applied) if self.discount_applied else None,
'location_id': self.location_id,
'sales_channel': self.sales_channel,
'source': self.source,
'is_validated': self.is_validated,
'validation_notes': self.validation_notes,
'notes': self.notes,
'weather_condition': self.weather_condition,
'is_holiday': self.is_holiday,
'is_weekend': self.is_weekend,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
'created_by': str(self.created_by) if self.created_by else None,
}
@property
def profit_margin(self) -> Optional[float]:
"""Calculate profit margin if cost data is available"""
if self.revenue and self.cost_of_goods:
return float((self.revenue - self.cost_of_goods) / self.revenue * 100)
return None
class Product(Base):
"""Product catalog model - future expansion"""
__tablename__ = "products"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# Product identification
name = Column(String(255), nullable=False, index=True)
sku = Column(String(100), nullable=True, index=True)
category = Column(String(100), nullable=True, index=True)
subcategory = Column(String(100), nullable=True)
# Product details
description = Column(Text, nullable=True)
unit_of_measure = Column(String(20), nullable=False, default="unit")
weight = Column(Float, nullable=True) # in grams
volume = Column(Float, nullable=True) # in ml
# Pricing
base_price = Column(Numeric(10, 2), nullable=True)
cost_price = Column(Numeric(10, 2), nullable=True)
# Status
is_active = Column(Boolean, default=True)
is_seasonal = Column(Boolean, default=False)
seasonal_start = Column(DateTime(timezone=True), nullable=True)
seasonal_end = Column(DateTime(timezone=True), nullable=True)
# Audit fields
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc))
__table_args__ = (
Index('idx_products_tenant_name', 'tenant_id', 'name', unique=True),
Index('idx_products_tenant_sku', 'tenant_id', 'sku'),
Index('idx_products_category', 'tenant_id', 'category', 'is_active'),
Index('idx_products_seasonal', 'is_seasonal', 'seasonal_start', 'seasonal_end'),
)
def to_dict(self) -> Dict[str, Any]:
"""Convert model to dictionary for API responses"""
return {
'id': str(self.id),
'tenant_id': str(self.tenant_id),
'name': self.name,
'sku': self.sku,
'category': self.category,
'subcategory': self.subcategory,
'description': self.description,
'unit_of_measure': self.unit_of_measure,
'weight': self.weight,
'volume': self.volume,
'base_price': float(self.base_price) if self.base_price else None,
'cost_price': float(self.cost_price) if self.cost_price else None,
'is_active': self.is_active,
'is_seasonal': self.is_seasonal,
'seasonal_start': self.seasonal_start.isoformat() if self.seasonal_start else None,
'seasonal_end': self.seasonal_end.isoformat() if self.seasonal_end else None,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
}
class SalesImportJob(Base):
"""Track sales data import jobs"""
__tablename__ = "sales_import_jobs"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# Job details
filename = Column(String(255), nullable=False)
file_size = Column(Integer, nullable=True)
import_type = Column(String(50), nullable=False, default="csv") # csv, xlsx, api
# Processing status
status = Column(String(20), nullable=False, default="pending") # pending, processing, completed, failed
progress_percentage = Column(Float, default=0.0)
# Results
total_rows = Column(Integer, default=0)
processed_rows = Column(Integer, default=0)
successful_imports = Column(Integer, default=0)
failed_imports = Column(Integer, default=0)
duplicate_rows = Column(Integer, default=0)
# Error tracking
error_message = Column(Text, nullable=True)
validation_errors = Column(Text, nullable=True) # JSON string of validation errors
# Timestamps
started_at = Column(DateTime(timezone=True), nullable=True)
completed_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
created_by = Column(UUID(as_uuid=True), nullable=True)
__table_args__ = (
Index('idx_import_jobs_tenant_status', 'tenant_id', 'status', 'created_at'),
Index('idx_import_jobs_status_date', 'status', 'created_at'),
)
def to_dict(self) -> Dict[str, Any]:
"""Convert model to dictionary for API responses"""
return {
'id': str(self.id),
'tenant_id': str(self.tenant_id),
'filename': self.filename,
'file_size': self.file_size,
'import_type': self.import_type,
'status': self.status,
'progress_percentage': self.progress_percentage,
'total_rows': self.total_rows,
'processed_rows': self.processed_rows,
'successful_imports': self.successful_imports,
'failed_imports': self.failed_imports,
'duplicate_rows': self.duplicate_rows,
'error_message': self.error_message,
'validation_errors': self.validation_errors,
'started_at': self.started_at.isoformat() if self.started_at else None,
'completed_at': self.completed_at.isoformat() if self.completed_at else None,
'created_at': self.created_at.isoformat() if self.created_at else None,
'created_by': str(self.created_by) if self.created_by else None,
}