Initial commit - production deployment
This commit is contained in:
12
services/sales/app/models/__init__.py
Normal file
12
services/sales/app/models/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
# Import AuditLog model for this service
|
||||
from shared.security import create_audit_log_model
|
||||
from shared.database.base import Base
|
||||
|
||||
# Create audit log model for this service
|
||||
AuditLog = create_audit_log_model(Base)
|
||||
# services/sales/app/models/__init__.py
|
||||
|
||||
from .sales import SalesData, SalesImportJob
|
||||
|
||||
__all__ = ["SalesData", "SalesImportJob", "AuditLog"]
|
||||
171
services/sales/app/models/sales.py
Normal file
171
services/sales/app/models/sales.py
Normal file
@@ -0,0 +1,171 @@
|
||||
# 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,
|
||||
}
|
||||
Reference in New Issue
Block a user