# 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, ForeignKey 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 reference to inventory service (REQUIRED) inventory_product_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Reference to inventory.ingredients.id # 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_location', 'tenant_id', 'location_id'), # Analytics queries Index('idx_sales_date_range', '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'), # Primary product reference index Index('idx_sales_inventory_product', 'inventory_product_id', 'tenant_id'), Index('idx_sales_product_date', 'inventory_product_id', '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, 'inventory_product_id': str(self.inventory_product_id), '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 # Product model removed - using inventory service as single source of truth # Product data is now referenced via inventory_product_id in SalesData model 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, }