# services/sales/app/api/sales_operations.py """ Sales Operations API - Business operations and complex workflows """ from fastapi import APIRouter, Depends, HTTPException, Query, Path, UploadFile, File, Form from sqlalchemy.ext.asyncio import AsyncSession from typing import List, Optional, Dict, Any from uuid import UUID from datetime import datetime import structlog import json from app.schemas.sales import SalesDataResponse from app.services.sales_service import SalesService from app.services.data_import_service import DataImportService from app.core.database import get_db from shared.auth.decorators import get_current_user_dep from shared.auth.access_control import require_user_role from shared.routing import RouteBuilder route_builder = RouteBuilder('sales') router = APIRouter(tags=["sales-operations"]) logger = structlog.get_logger() def get_sales_service(): """Dependency injection for SalesService""" return SalesService() def get_import_service(): """Dependency injection for DataImportService""" return DataImportService() @router.post( route_builder.build_operations_route("validate-record"), response_model=SalesDataResponse ) async def validate_sales_record( tenant_id: UUID = Path(..., description="Tenant ID"), record_id: UUID = Path(..., description="Sales record ID"), validation_notes: Optional[str] = Query(None, description="Validation notes"), sales_service: SalesService = Depends(get_sales_service) ): """Mark a sales record as validated""" try: validated_record = await sales_service.validate_sales_record(record_id, tenant_id, validation_notes) logger.info("Validated sales record", record_id=record_id, tenant_id=tenant_id) return validated_record except ValueError as ve: logger.warning("Error validating sales record", error=str(ve), record_id=record_id) raise HTTPException(status_code=400, detail=str(ve)) except Exception as e: logger.error("Failed to validate sales record", error=str(e), record_id=record_id, tenant_id=tenant_id) raise HTTPException(status_code=500, detail=f"Failed to validate sales record: {str(e)}") @router.get( route_builder.build_nested_resource_route("inventory-products", "inventory_product_id", "sales"), response_model=List[SalesDataResponse] ) async def get_product_sales( tenant_id: UUID = Path(..., description="Tenant ID"), inventory_product_id: UUID = Path(..., description="Inventory product ID"), start_date: Optional[datetime] = Query(None, description="Start date filter"), end_date: Optional[datetime] = Query(None, description="End date filter"), sales_service: SalesService = Depends(get_sales_service) ): """Get sales records for a specific product (cross-service query)""" try: records = await sales_service.get_product_sales(tenant_id, inventory_product_id, start_date, end_date) logger.info("Retrieved product sales", count=len(records), inventory_product_id=inventory_product_id, tenant_id=tenant_id) return records except Exception as e: logger.error("Failed to get product sales", error=str(e), tenant_id=tenant_id, inventory_product_id=inventory_product_id) raise HTTPException(status_code=500, detail=f"Failed to get product sales: {str(e)}") @router.post( route_builder.build_operations_route("import/validate-json") ) async def validate_json_data( tenant_id: UUID = Path(..., description="Tenant ID"), data: Dict[str, Any] = None, current_user: Dict[str, Any] = Depends(get_current_user_dep), import_service: DataImportService = Depends(get_import_service) ): """Validate JSON sales data""" try: if not data: raise HTTPException(status_code=400, detail="No data provided") logger.info("Validating JSON data", tenant_id=tenant_id, record_count=len(data.get("records", []))) if "records" in data: validation_data = { "tenant_id": str(tenant_id), "data": json.dumps(data.get("records", [])), "data_format": "json" } else: validation_data = data.copy() validation_data["tenant_id"] = str(tenant_id) if "data_format" not in validation_data: validation_data["data_format"] = "json" validation_result = await import_service.validate_import_data(validation_data) logger.info("JSON validation completed", tenant_id=tenant_id, valid=validation_result.is_valid) return { "is_valid": validation_result.is_valid, "total_records": validation_result.total_records, "valid_records": validation_result.valid_records, "invalid_records": validation_result.invalid_records, "errors": validation_result.errors, "warnings": validation_result.warnings, "summary": validation_result.summary } except Exception as e: logger.error("Failed to validate JSON data", error=str(e), tenant_id=tenant_id) raise HTTPException(status_code=500, detail=f"Failed to validate data: {str(e)}") @router.post( route_builder.build_operations_route("import/validate") ) async def validate_sales_data_universal( tenant_id: UUID = Path(..., description="Tenant ID"), file: Optional[UploadFile] = File(None), data: Optional[Dict[str, Any]] = None, file_format: Optional[str] = Form(None), current_user: Dict[str, Any] = Depends(get_current_user_dep), import_service: DataImportService = Depends(get_import_service) ): """Universal validation endpoint for sales data - supports files and JSON""" try: logger.info("=== VALIDATION ENDPOINT CALLED ===", tenant_id=tenant_id, file_present=file is not None, file_filename=file.filename if file else None, data_present=data is not None, file_format=file_format) if file and file.filename: logger.info("Processing file upload branch", tenant_id=tenant_id, filename=file.filename) filename = file.filename.lower() if filename.endswith('.csv'): detected_format = 'csv' elif filename.endswith('.xlsx') or filename.endswith('.xls'): detected_format = 'excel' elif filename.endswith('.json'): detected_format = 'json' else: detected_format = file_format or 'csv' content = await file.read() if detected_format in ['xlsx', 'xls', 'excel']: import base64 file_content = base64.b64encode(content).decode('utf-8') else: file_content = content.decode('utf-8') validation_data = { "tenant_id": str(tenant_id), "data": file_content, "data_format": detected_format, "filename": file.filename } elif data: logger.info("Processing JSON data branch", tenant_id=tenant_id, data_keys=list(data.keys()) if data else []) validation_data = data.copy() validation_data["tenant_id"] = str(tenant_id) if "data_format" not in validation_data: validation_data["data_format"] = "json" else: logger.error("No file or data provided", tenant_id=tenant_id, file_present=file is not None, data_present=data is not None) raise HTTPException(status_code=400, detail="No file or data provided for validation") logger.info("About to call validate_import_data", validation_data_keys=list(validation_data.keys()), data_size=len(validation_data.get("data", ""))) validation_result = await import_service.validate_import_data(validation_data) logger.info("Validation completed", is_valid=validation_result.is_valid, errors_count=len(validation_result.errors)) logger.info("Validation completed", tenant_id=tenant_id, valid=validation_result.is_valid, total_records=validation_result.total_records) return { "is_valid": validation_result.is_valid, "total_records": validation_result.total_records, "valid_records": validation_result.valid_records, "invalid_records": validation_result.invalid_records, "errors": validation_result.errors, "warnings": validation_result.warnings, "summary": validation_result.summary, "unique_products": validation_result.unique_products, "product_list": validation_result.product_list, "message": "Validation completed successfully" if validation_result.is_valid else "Validation found errors", "details": { "total_records": validation_result.total_records, "format": validation_data.get("data_format", "unknown") } } except HTTPException: # Re-raise HTTP exceptions as-is (don't convert to 500) raise except Exception as e: error_msg = str(e) if e else "Unknown error occurred during validation" logger.error("Failed to validate sales data", error=error_msg, tenant_id=tenant_id, exc_info=True) raise HTTPException(status_code=500, detail=f"Failed to validate data: {error_msg}") @router.post( route_builder.build_operations_route("import/validate-csv") ) async def validate_csv_data_legacy( tenant_id: UUID = Path(..., description="Tenant ID"), file: UploadFile = File(...), current_user: Dict[str, Any] = Depends(get_current_user_dep), import_service: DataImportService = Depends(get_import_service) ): """Legacy CSV validation endpoint - redirects to universal validator""" return await validate_sales_data_universal( tenant_id=tenant_id, file=file, current_user=current_user, import_service=import_service ) @router.post( route_builder.build_operations_route("import") ) async def import_sales_data( tenant_id: UUID = Path(..., description="Tenant ID"), data: Optional[Dict[str, Any]] = None, file: Optional[UploadFile] = File(None), file_format: Optional[str] = Form(None), update_existing: bool = Form(False, description="Whether to update existing records"), current_user: Dict[str, Any] = Depends(get_current_user_dep), import_service: DataImportService = Depends(get_import_service) ): """Enhanced import sales data - supports multiple file formats and JSON""" try: if file: if not file.filename: raise HTTPException(status_code=400, detail="No file provided") logger.info("Starting enhanced file import", tenant_id=tenant_id, filename=file.filename) filename = file.filename.lower() if filename.endswith('.csv'): detected_format = 'csv' elif filename.endswith('.xlsx') or filename.endswith('.xls'): detected_format = 'excel' elif filename.endswith('.json'): detected_format = 'json' else: detected_format = file_format or 'csv' content = await file.read() if detected_format in ['xlsx', 'xls', 'excel']: import base64 file_content = base64.b64encode(content).decode('utf-8') else: file_content = content.decode('utf-8') import_result = await import_service.process_import( str(tenant_id), file_content, detected_format, filename=file.filename ) elif data: logger.info("Starting enhanced JSON data import", tenant_id=tenant_id, record_count=len(data.get("records", []))) if "records" in data: records_json = json.dumps(data.get("records", [])) import_result = await import_service.process_import( str(tenant_id), records_json, "json" ) else: import_result = await import_service.process_import( str(tenant_id), data.get("data", ""), data.get("data_format", "json") ) else: raise HTTPException(status_code=400, detail="No data or file provided") logger.info("Enhanced import completed", tenant_id=tenant_id, created=import_result.records_created, updated=import_result.records_updated, failed=import_result.records_failed, processing_time=import_result.processing_time_seconds) response = { "success": import_result.success, "records_processed": import_result.records_processed, "records_created": import_result.records_created, "records_updated": import_result.records_updated, "records_failed": import_result.records_failed, "errors": import_result.errors, "warnings": import_result.warnings, "processing_time_seconds": import_result.processing_time_seconds, "records_imported": import_result.records_created, "message": f"Successfully imported {import_result.records_created} records" if import_result.success else "Import completed with errors" } if file: response["file_info"] = { "name": file.filename, "format": detected_format, "size_bytes": len(content) if 'content' in locals() else 0 } return response except Exception as e: logger.error("Failed to import sales data", error=str(e), tenant_id=tenant_id, exc_info=True) raise HTTPException(status_code=500, detail=f"Failed to import data: {str(e)}") @router.post( route_builder.build_operations_route("import/csv") ) async def import_csv_data( tenant_id: UUID = Path(..., description="Tenant ID"), file: UploadFile = File(...), update_existing: bool = Form(False, description="Whether to update existing records"), current_user: Dict[str, Any] = Depends(get_current_user_dep), import_service: DataImportService = Depends(get_import_service) ): """Import CSV sales data file""" try: if not file.filename.endswith('.csv'): raise HTTPException(status_code=400, detail="File must be a CSV file") logger.info("Starting CSV data import", tenant_id=tenant_id, filename=file.filename) content = await file.read() file_content = content.decode('utf-8') import_result = await import_service.process_import( tenant_id, file_content, "csv", filename=file.filename ) logger.info("CSV import completed", tenant_id=tenant_id, filename=file.filename, created=import_result.records_created, updated=import_result.records_updated, failed=import_result.records_failed) return { "success": import_result.success, "records_processed": import_result.records_processed, "records_created": import_result.records_created, "records_updated": import_result.records_updated, "records_failed": import_result.records_failed, "errors": import_result.errors, "warnings": import_result.warnings, "processing_time_seconds": import_result.processing_time_seconds } except Exception as e: logger.error("Failed to import CSV data", error=str(e), tenant_id=tenant_id) raise HTTPException(status_code=500, detail=f"Failed to import CSV data: {str(e)}") @router.get( route_builder.build_operations_route("import/template") ) async def get_import_template( tenant_id: UUID = Path(..., description="Tenant ID"), format: str = "csv" ): """Get sales data import template""" try: if format not in ["csv", "json"]: raise HTTPException(status_code=400, detail="Format must be 'csv' or 'json'") if format == "csv": template = "date,product_name,product_category,product_sku,quantity_sold,unit_price,revenue,cost_of_goods,discount_applied,location_id,sales_channel,source,notes,weather_condition,is_holiday,is_weekend" else: template = { "records": [ { "date": "2024-01-01T10:00:00Z", "product_name": "Sample Product", "product_category": "Sample Category", "product_sku": "SAMPLE001", "quantity_sold": 1, "unit_price": 10.50, "revenue": 10.50, "cost_of_goods": 5.25, "discount_applied": 0.0, "location_id": "LOC001", "sales_channel": "in_store", "source": "manual", "notes": "Sample sales record", "weather_condition": "sunny", "is_holiday": False, "is_weekend": False } ] } return {"template": template, "format": format} except Exception as e: logger.error("Failed to get import template", error=str(e), tenant_id=tenant_id) raise HTTPException(status_code=500, detail=f"Failed to get import template: {str(e)}") # ============================================================================ # Tenant Data Deletion Operations (Internal Service Only) # ============================================================================ from shared.auth.access_control import service_only_access from shared.services.tenant_deletion import TenantDataDeletionResult from app.services.tenant_deletion_service import SalesTenantDeletionService @router.delete( route_builder.build_base_route("tenant/{tenant_id}", include_tenant_prefix=False), response_model=dict ) @service_only_access async def delete_tenant_data( tenant_id: str = Path(..., description="Tenant ID to delete data for"), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """ Delete all sales data for a tenant (Internal service only) """ try: logger.info("sales.tenant_deletion.api_called", tenant_id=tenant_id) deletion_service = SalesTenantDeletionService(db) result = await deletion_service.safe_delete_tenant_data(tenant_id) if not result.success: raise HTTPException( status_code=500, detail=f"Tenant data deletion failed: {', '.join(result.errors)}" ) return { "message": "Tenant data deletion completed successfully", "summary": result.to_dict() } except HTTPException: raise except Exception as e: logger.error("sales.tenant_deletion.api_error", tenant_id=tenant_id, error=str(e), exc_info=True) raise HTTPException(status_code=500, detail=f"Failed to delete tenant data: {str(e)}") @router.get( route_builder.build_base_route("tenant/{tenant_id}/deletion-preview", include_tenant_prefix=False), response_model=dict ) @service_only_access async def preview_tenant_data_deletion( tenant_id: str = Path(..., description="Tenant ID to preview deletion for"), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """ Preview what data would be deleted for a tenant (dry-run) """ try: logger.info("sales.tenant_deletion.preview_called", tenant_id=tenant_id) deletion_service = SalesTenantDeletionService(db) preview_data = await deletion_service.get_tenant_data_preview(tenant_id) result = TenantDataDeletionResult(tenant_id=tenant_id, service_name=deletion_service.service_name) result.deleted_counts = preview_data result.success = True if not result.success: raise HTTPException( status_code=500, detail=f"Tenant deletion preview failed: {', '.join(result.errors)}" ) return { "tenant_id": tenant_id, "service": "sales-service", "data_counts": result.deleted_counts, "total_items": sum(result.deleted_counts.values()) } except HTTPException: raise except Exception as e: logger.error("sales.tenant_deletion.preview_error", tenant_id=tenant_id, error=str(e), exc_info=True) raise HTTPException(status_code=500, detail=f"Failed to preview tenant data deletion: {str(e)}")