# shared/clients/sales_client.py """ Sales Service Client Handles all API calls to the sales service """ import httpx import structlog from datetime import date from typing import Dict, Any, Optional, List, Union from .base_service_client import BaseServiceClient from shared.config.base import BaseServiceSettings logger = structlog.get_logger() class SalesServiceClient(BaseServiceClient): """Client for communicating with the sales service""" def __init__(self, config: BaseServiceSettings, calling_service_name: str = "unknown"): super().__init__(calling_service_name, config) self.service_url = config.SALES_SERVICE_URL def get_service_base_path(self) -> str: return "/api/v1" # ================================================================ # SALES DATA (with advanced pagination support) # ================================================================ async def get_sales_data( self, tenant_id: str, start_date: Optional[str] = None, end_date: Optional[str] = None, product_id: Optional[str] = None, aggregation: str = "daily" ) -> Optional[List[Dict[str, Any]]]: """Get sales data for a date range""" params = {"aggregation": aggregation} if start_date: params["start_date"] = start_date if end_date: params["end_date"] = end_date if product_id: params["product_id"] = product_id result = await self.get("sales/sales", tenant_id=tenant_id, params=params) # Handle both list and dict responses if result is None: return None elif isinstance(result, list): return result elif isinstance(result, dict): return result.get("sales", []) else: return None async def get_all_sales_data( self, tenant_id: str, start_date: Optional[str] = None, end_date: Optional[str] = None, product_id: Optional[str] = None, aggregation: str = "daily", page_size: int = 1000, max_pages: int = 100 ) -> List[Dict[str, Any]]: """ Get ALL sales data using pagination (equivalent to original fetch_sales_data) Retrieves all records without pagination limits """ params = {"aggregation": aggregation} if start_date: params["start_date"] = start_date if end_date: params["end_date"] = end_date if product_id: params["product_id"] = product_id # Use the inherited paginated request method try: all_records = await self.get_paginated( "sales/sales", tenant_id=tenant_id, params=params, page_size=page_size, max_pages=max_pages, timeout=2000.0 ) logger.info(f"Successfully fetched {len(all_records)} total sales records via sales service", tenant_id=tenant_id) return all_records except Exception as e: logger.error(f"Failed to fetch paginated sales data: {e}") return [] async def upload_sales_data( self, tenant_id: str, sales_data: List[Dict[str, Any]] ) -> Optional[Dict[str, Any]]: """Upload sales data""" data = {"sales": sales_data} return await self.post("sales/sales", data=data, tenant_id=tenant_id) # ================================================================ # PRODUCTS # ================================================================ async def get_products(self, tenant_id: str) -> Optional[List[Dict[str, Any]]]: """Get all products for a tenant""" result = await self.get("sales/products", tenant_id=tenant_id) return result.get("products", []) if result else None async def get_product(self, tenant_id: str, product_id: str) -> Optional[Dict[str, Any]]: """Get a specific product""" return await self.get(f"sales/products/{product_id}", tenant_id=tenant_id) async def create_product( self, tenant_id: str, name: str, category: str, price: float, **kwargs ) -> Optional[Dict[str, Any]]: """Create a new product""" data = { "name": name, "category": category, "price": price, **kwargs } return await self.post("sales/products", data=data, tenant_id=tenant_id) async def update_product( self, tenant_id: str, product_id: str, **updates ) -> Optional[Dict[str, Any]]: """Update a product""" return await self.put(f"sales/products/{product_id}", data=updates, tenant_id=tenant_id) async def create_sales_record( self, tenant_id: str, sales_data: Dict[str, Any] ) -> Optional[Dict[str, Any]]: """Create a new sales record Args: tenant_id: Tenant ID sales_data: Sales record data including: - inventory_product_id: Optional UUID for inventory tracking - product_name: Product name - product_category: Product category - quantity_sold: Quantity sold - unit_price: Unit price - total_amount: Total amount - sale_date: Sale date (YYYY-MM-DD) - sales_channel: Sales channel (retail, wholesale, online, pos, etc.) - source: Data source (manual, pos_sync, import, etc.) - payment_method: Payment method - notes: Optional notes Returns: Created sales record or None if failed """ try: result = await self.post("sales/sales", data=sales_data, tenant_id=tenant_id) if result: logger.info("Created sales record via client", tenant_id=tenant_id, product=sales_data.get("product_name")) return result except Exception as e: logger.error("Failed to create sales record", error=str(e), tenant_id=tenant_id) return None async def get_sales_summary( self, tenant_id: str, start_date: date, end_date: date ) -> Dict[str, Any]: """ Get sales summary/analytics for a tenant. This method calls the sales analytics summary endpoint which provides aggregated sales metrics over a date range. Args: tenant_id: The tenant UUID start_date: Start date for summary range end_date: End date for summary range Returns: Sales summary data including metrics like total sales, revenue, etc. """ params = { "start_date": start_date.isoformat(), "end_date": end_date.isoformat() } return await self.get( "sales/analytics/summary", tenant_id=tenant_id, params=params ) async def get_sales_summary_batch( self, tenant_ids: List[str], start_date: date, end_date: date ) -> Dict[str, Any]: """ Get sales summaries for multiple tenants in a single request. Phase 2 optimization: Eliminates N+1 query patterns for enterprise dashboards. Args: tenant_ids: List of tenant IDs to fetch start_date: Start date for summary range end_date: End date for summary range Returns: Dict mapping tenant_id -> sales summary """ try: if not tenant_ids: return {} if len(tenant_ids) > 100: logger.warning("Batch request exceeds max tenant limit", requested=len(tenant_ids)) tenant_ids = tenant_ids[:100] data = { "tenant_ids": tenant_ids, "start_date": start_date.isoformat(), "end_date": end_date.isoformat() } result = await self.post( "sales/batch/sales-summary", data=data, tenant_id=tenant_ids[0] # Use first tenant for auth context ) summaries = result if isinstance(result, dict) else {} logger.info( "Batch retrieved sales summaries", requested=len(tenant_ids), found=len(summaries), start_date=start_date.isoformat(), end_date=end_date.isoformat() ) return summaries except Exception as e: logger.error( "Error batch fetching sales summaries", error=str(e), tenant_count=len(tenant_ids) ) return {} async def get_product_demand_patterns( self, tenant_id: str, product_id: str, start_date: Optional[date] = None, end_date: Optional[date] = None, min_history_days: int = 90 ) -> Dict[str, Any]: """ Get demand pattern analysis for a specific product. Args: tenant_id: Tenant identifier product_id: Product identifier (inventory_product_id) start_date: Start date for analysis end_date: End date for analysis min_history_days: Minimum days of history required Returns: Demand pattern analysis including trends, seasonality, and statistics """ try: params = {"min_history_days": min_history_days} if start_date: params["start_date"] = start_date.isoformat() if end_date: params["end_date"] = end_date.isoformat() result = await self.get( f"sales/analytics/products/{product_id}/demand-patterns", tenant_id=tenant_id, params=params ) logger.info( "Retrieved product demand patterns", tenant_id=tenant_id, product_id=product_id ) return result if result else {} except Exception as e: logger.error( "Failed to get product demand patterns", error=str(e), tenant_id=tenant_id, product_id=product_id ) return {} # ================================================================ # DATA IMPORT # ================================================================ async def import_sales_data( self, tenant_id: str, file_content: str, file_format: str, filename: Optional[str] = None ) -> Optional[Dict[str, Any]]: """Import sales data from CSV/Excel/JSON""" data = { "content": file_content, "format": file_format, "filename": filename } return await self.post("sales/operations/import", data=data, tenant_id=tenant_id)