# services/sales/app/api/sales_records.py """ Sales Records API - Atomic CRUD operations on SalesData model """ from fastapi import APIRouter, Depends, HTTPException, Query, Path, status from typing import List, Optional, Dict, Any from uuid import UUID from datetime import datetime import structlog from app.schemas.sales import ( SalesDataCreate, SalesDataUpdate, SalesDataResponse, SalesDataQuery ) from app.services.sales_service import SalesService from app.models import AuditLog from shared.auth.decorators import get_current_user_dep from shared.auth.access_control import require_user_role from shared.routing import RouteBuilder from shared.security import create_audit_logger, AuditSeverity, AuditAction route_builder = RouteBuilder('sales') router = APIRouter(tags=["sales-records"]) logger = structlog.get_logger() # Initialize audit logger audit_logger = create_audit_logger("sales-service", AuditLog) def get_sales_service(): """Dependency injection for SalesService""" return SalesService() @router.post( route_builder.build_base_route("sales"), response_model=SalesDataResponse, status_code=status.HTTP_201_CREATED ) @require_user_role(['admin', 'owner', 'member']) async def create_sales_record( sales_data: SalesDataCreate, tenant_id: UUID = Path(..., description="Tenant ID"), current_user: Dict[str, Any] = Depends(get_current_user_dep), sales_service: SalesService = Depends(get_sales_service) ): """Create a new sales record""" try: logger.info( "Creating sales record", product=sales_data.product_name, quantity=sales_data.quantity_sold, tenant_id=tenant_id, user_id=current_user.get("user_id") ) record = await sales_service.create_sales_record( sales_data, tenant_id, user_id=UUID(current_user["user_id"]) if current_user.get("user_id") else None ) logger.info("Successfully created sales record", record_id=record.id, tenant_id=tenant_id) return record except ValueError as ve: logger.warning("Validation error creating sales record", error=str(ve), tenant_id=tenant_id) raise HTTPException(status_code=400, detail=str(ve)) except Exception as e: logger.error("Failed to create sales record", error=str(e), tenant_id=tenant_id) raise HTTPException(status_code=500, detail=f"Failed to create sales record: {str(e)}") @router.get( route_builder.build_base_route("sales"), response_model=List[SalesDataResponse] ) async def get_sales_records( tenant_id: UUID = Path(..., description="Tenant ID"), start_date: Optional[datetime] = Query(None, description="Start date filter"), end_date: Optional[datetime] = Query(None, description="End date filter"), product_name: Optional[str] = Query(None, description="Product name filter"), product_category: Optional[str] = Query(None, description="Product category filter"), location_id: Optional[str] = Query(None, description="Location filter"), sales_channel: Optional[str] = Query(None, description="Sales channel filter"), source: Optional[str] = Query(None, description="Data source filter"), is_validated: Optional[bool] = Query(None, description="Validation status filter"), limit: int = Query(50, ge=1, le=1000, description="Number of records to return"), offset: int = Query(0, ge=0, description="Number of records to skip"), order_by: str = Query("date", description="Field to order by"), order_direction: str = Query("desc", description="Order direction (asc/desc)"), sales_service: SalesService = Depends(get_sales_service) ): """Get sales records for a tenant with filtering and pagination""" try: query_params = SalesDataQuery( start_date=start_date, end_date=end_date, product_name=product_name, product_category=product_category, location_id=location_id, sales_channel=sales_channel, source=source, is_validated=is_validated, limit=limit, offset=offset, order_by=order_by, order_direction=order_direction ) records = await sales_service.get_sales_records(tenant_id, query_params) logger.info("Retrieved sales records", count=len(records), tenant_id=tenant_id) return records except Exception as e: logger.error("Failed to get sales records", error=str(e), tenant_id=tenant_id) raise HTTPException(status_code=500, detail=f"Failed to get sales records: {str(e)}") @router.get( route_builder.build_resource_detail_route("sales", "record_id"), response_model=SalesDataResponse ) async def get_sales_record( tenant_id: UUID = Path(..., description="Tenant ID"), record_id: UUID = Path(..., description="Sales record ID"), sales_service: SalesService = Depends(get_sales_service) ): """Get a specific sales record""" try: record = await sales_service.get_sales_record(record_id, tenant_id) if not record: raise HTTPException(status_code=404, detail="Sales record not found") return record except HTTPException: raise except Exception as e: logger.error("Failed to get sales record", error=str(e), record_id=record_id, tenant_id=tenant_id) raise HTTPException(status_code=500, detail=f"Failed to get sales record: {str(e)}") @router.put( route_builder.build_resource_detail_route("sales", "record_id"), response_model=SalesDataResponse ) async def update_sales_record( update_data: SalesDataUpdate, tenant_id: UUID = Path(..., description="Tenant ID"), record_id: UUID = Path(..., description="Sales record ID"), sales_service: SalesService = Depends(get_sales_service) ): """Update a sales record""" try: updated_record = await sales_service.update_sales_record(record_id, update_data, tenant_id) logger.info("Updated sales record", record_id=record_id, tenant_id=tenant_id) return updated_record except ValueError as ve: logger.warning("Validation error updating 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 update sales record", error=str(e), record_id=record_id, tenant_id=tenant_id) raise HTTPException(status_code=500, detail=f"Failed to update sales record: {str(e)}") @router.delete( route_builder.build_resource_detail_route("sales", "record_id") ) @require_user_role(['admin', 'owner']) async def delete_sales_record( tenant_id: UUID = Path(..., description="Tenant ID"), record_id: UUID = Path(..., description="Sales record ID"), current_user: Dict[str, Any] = Depends(get_current_user_dep), sales_service: SalesService = Depends(get_sales_service) ): """Delete a sales record (Admin+ only)""" try: # Get record details before deletion for audit log record = await sales_service.get_sales_record(record_id, tenant_id) success = await sales_service.delete_sales_record(record_id, tenant_id) if not success: raise HTTPException(status_code=404, detail="Sales record not found") # Log audit event for sales record deletion try: from app.core.database import get_db db = next(get_db()) await audit_logger.log_deletion( db_session=db, tenant_id=str(tenant_id), user_id=current_user["user_id"], resource_type="sales_record", resource_id=str(record_id), resource_data={ "product_name": record.product_name if record else None, "quantity_sold": record.quantity_sold if record else None, "sale_date": record.date.isoformat() if record and record.date else None } if record else None, description=f"Deleted sales record for {record.product_name if record else 'unknown product'}", endpoint=f"/sales/{record_id}", method="DELETE" ) except Exception as audit_error: logger.warning("Failed to log audit event", error=str(audit_error)) logger.info("Deleted sales record", record_id=record_id, tenant_id=tenant_id) return {"message": "Sales record deleted successfully"} except ValueError as ve: logger.warning("Error deleting sales record", error=str(ve), record_id=record_id) raise HTTPException(status_code=400, detail=str(ve)) except HTTPException: raise except Exception as e: logger.error("Failed to delete sales record", error=str(e), record_id=record_id, tenant_id=tenant_id) raise HTTPException(status_code=500, detail=f"Failed to delete sales record: {str(e)}") @router.get( route_builder.build_base_route("categories"), response_model=List[str] ) async def get_product_categories( tenant_id: UUID = Path(..., description="Tenant ID"), sales_service: SalesService = Depends(get_sales_service) ): """Get distinct product categories from sales data""" try: categories = await sales_service.get_product_categories(tenant_id) return categories except Exception as e: logger.error("Failed to get product categories", error=str(e), tenant_id=tenant_id) raise HTTPException(status_code=500, detail=f"Failed to get product categories: {str(e)}")