# 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, }