# services/sales/app/api/sales.py """ Sales API Endpoints """ from fastapi import APIRouter, Depends, HTTPException, Query, Path 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 shared.auth.decorators import get_current_user_dep, get_current_tenant_id_dep router = APIRouter(tags=["sales"]) logger = structlog.get_logger() def get_sales_service(): """Dependency injection for SalesService""" return SalesService() @router.post("/tenants/{tenant_id}/sales", response_model=SalesDataResponse) 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), current_tenant: str = Depends(get_current_tenant_id_dep), sales_service: SalesService = Depends(get_sales_service) ): """Create a new sales record""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") 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") ) # Create the record 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("/tenants/{tenant_id}/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)"), current_tenant: str = Depends(get_current_tenant_id_dep), sales_service: SalesService = Depends(get_sales_service) ): """Get sales records for a tenant with filtering and pagination""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") # Build query parameters 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("/tenants/{tenant_id}/sales/analytics/summary") async def get_sales_analytics( 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"), current_tenant: str = Depends(get_current_tenant_id_dep), sales_service: SalesService = Depends(get_sales_service) ): """Get sales analytics summary for a tenant""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") analytics = await sales_service.get_sales_analytics(tenant_id, start_date, end_date) logger.info("Retrieved sales analytics", tenant_id=tenant_id) return analytics except Exception as e: logger.error("Failed to get sales analytics", error=str(e), tenant_id=tenant_id) raise HTTPException(status_code=500, detail=f"Failed to get sales analytics: {str(e)}") @router.get("/tenants/{tenant_id}/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"), current_tenant: str = Depends(get_current_tenant_id_dep), sales_service: SalesService = Depends(get_sales_service) ): """Get sales records for a specific product""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") 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.get("/tenants/{tenant_id}/sales/categories", response_model=List[str]) async def get_product_categories( tenant_id: UUID = Path(..., description="Tenant ID"), current_tenant: str = Depends(get_current_tenant_id_dep), sales_service: SalesService = Depends(get_sales_service) ): """Get distinct product categories from sales data""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") 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)}") # ================================================================ # PARAMETERIZED ROUTES - Keep these at the end to avoid conflicts # ================================================================ @router.get("/tenants/{tenant_id}/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"), current_tenant: str = Depends(get_current_tenant_id_dep), sales_service: SalesService = Depends(get_sales_service) ): """Get a specific sales record""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") 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("/tenants/{tenant_id}/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"), current_tenant: str = Depends(get_current_tenant_id_dep), sales_service: SalesService = Depends(get_sales_service) ): """Update a sales record""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") 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("/tenants/{tenant_id}/sales/{record_id}") async def delete_sales_record( tenant_id: UUID = Path(..., description="Tenant ID"), record_id: UUID = Path(..., description="Sales record ID"), current_tenant: str = Depends(get_current_tenant_id_dep), sales_service: SalesService = Depends(get_sales_service) ): """Delete a sales record""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") success = await sales_service.delete_sales_record(record_id, tenant_id) if not success: raise HTTPException(status_code=404, detail="Sales record not found") 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 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.post("/tenants/{tenant_id}/sales/{record_id}/validate", 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"), current_tenant: str = Depends(get_current_tenant_id_dep), sales_service: SalesService = Depends(get_sales_service) ): """Mark a sales record as validated""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") 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)}") # ================================================================ # INVENTORY INTEGRATION ENDPOINTS # ================================================================ @router.get("/tenants/{tenant_id}/inventory/products/search") async def search_inventory_products( tenant_id: UUID = Path(..., description="Tenant ID"), search: str = Query(..., description="Search term"), product_type: Optional[str] = Query(None, description="Product type filter"), current_tenant: str = Depends(get_current_tenant_id_dep), sales_service: SalesService = Depends(get_sales_service) ): """Search products in inventory service""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") products = await sales_service.search_inventory_products(search, tenant_id, product_type) return {"items": products, "count": len(products)} except Exception as e: logger.error("Failed to search inventory products", error=str(e), tenant_id=tenant_id) raise HTTPException(status_code=500, detail=f"Failed to search inventory products: {str(e)}") @router.get("/tenants/{tenant_id}/inventory/products/{product_id}") async def get_inventory_product( tenant_id: UUID = Path(..., description="Tenant ID"), product_id: UUID = Path(..., description="Product ID from inventory service"), current_tenant: str = Depends(get_current_tenant_id_dep), sales_service: SalesService = Depends(get_sales_service) ): """Get product details from inventory service""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") product = await sales_service.get_inventory_product(product_id, tenant_id) if not product: raise HTTPException(status_code=404, detail="Product not found in inventory") return product except HTTPException: raise except Exception as e: logger.error("Failed to get inventory product", error=str(e), product_id=product_id, tenant_id=tenant_id) raise HTTPException(status_code=500, detail=f"Failed to get inventory product: {str(e)}") @router.get("/tenants/{tenant_id}/inventory/products/category/{category}") async def get_inventory_products_by_category( tenant_id: UUID = Path(..., description="Tenant ID"), category: str = Path(..., description="Product category"), product_type: Optional[str] = Query(None, description="Product type filter"), current_tenant: str = Depends(get_current_tenant_id_dep), sales_service: SalesService = Depends(get_sales_service) ): """Get products by category from inventory service""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") products = await sales_service.get_inventory_products_by_category(category, tenant_id, product_type) return {"items": products, "count": len(products)} except Exception as e: logger.error("Failed to get inventory products by category", error=str(e), category=category, tenant_id=tenant_id) raise HTTPException(status_code=500, detail=f"Failed to get inventory products by category: {str(e)}")