REFACTOR ALL APIs
This commit is contained in:
43
services/sales/app/api/analytics.py
Normal file
43
services/sales/app/api/analytics.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# services/sales/app/api/analytics.py
|
||||
"""
|
||||
Sales Analytics API - Reporting, statistics, and insights
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
import structlog
|
||||
|
||||
from app.services.sales_service import SalesService
|
||||
from shared.routing import RouteBuilder
|
||||
|
||||
route_builder = RouteBuilder('sales')
|
||||
router = APIRouter(tags=["sales-analytics"])
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
def get_sales_service():
|
||||
"""Dependency injection for SalesService"""
|
||||
return SalesService()
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_analytics_route("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"),
|
||||
sales_service: SalesService = Depends(get_sales_service)
|
||||
):
|
||||
"""Get sales analytics summary for a tenant"""
|
||||
try:
|
||||
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)}")
|
||||
@@ -1,27 +1,88 @@
|
||||
# services/sales/app/api/import_data.py
|
||||
# services/sales/app/api/sales_operations.py
|
||||
"""
|
||||
Sales Data Import API Endpoints
|
||||
Sales Operations API - Business operations and complex workflows
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Path
|
||||
from typing import Dict, Any, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path, UploadFile, File, Form
|
||||
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 shared.auth.decorators import get_current_user_dep
|
||||
from shared.auth.access_control import require_user_role
|
||||
from shared.routing import RouteBuilder
|
||||
|
||||
router = APIRouter(tags=["data-import"])
|
||||
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("/tenants/{tenant_id}/sales/import/validate-json")
|
||||
@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,
|
||||
@@ -30,31 +91,27 @@ async def validate_json_data(
|
||||
):
|
||||
"""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", [])))
|
||||
|
||||
# Validate the data - handle different input formats
|
||||
|
||||
if "records" in data:
|
||||
# New format with records array
|
||||
validation_data = {
|
||||
"tenant_id": str(tenant_id),
|
||||
"data": json.dumps(data.get("records", [])),
|
||||
"data_format": "json"
|
||||
}
|
||||
else:
|
||||
# Legacy format where the entire payload is the validation data
|
||||
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,
|
||||
@@ -64,13 +121,15 @@ async def validate_json_data(
|
||||
"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("/tenants/{tenant_id}/sales/import/validate")
|
||||
@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),
|
||||
@@ -81,19 +140,16 @@ async def validate_sales_data_universal(
|
||||
):
|
||||
"""Universal validation endpoint for sales data - supports files and JSON"""
|
||||
try:
|
||||
# Debug logging at the start
|
||||
logger.info("=== VALIDATION ENDPOINT CALLED ===",
|
||||
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)
|
||||
|
||||
# Handle file upload validation
|
||||
|
||||
if file and file.filename:
|
||||
logger.info("Processing file upload branch", tenant_id=tenant_id, filename=file.filename)
|
||||
|
||||
# Auto-detect format from filename
|
||||
|
||||
filename = file.filename.lower()
|
||||
if filename.endswith('.csv'):
|
||||
detected_format = 'csv'
|
||||
@@ -102,49 +158,44 @@ async def validate_sales_data_universal(
|
||||
elif filename.endswith('.json'):
|
||||
detected_format = 'json'
|
||||
else:
|
||||
detected_format = file_format or 'csv' # Default to CSV
|
||||
|
||||
# Read file content
|
||||
detected_format = file_format or 'csv'
|
||||
|
||||
content = await file.read()
|
||||
|
||||
|
||||
if detected_format in ['xlsx', 'xls', 'excel']:
|
||||
# For Excel files, encode as base64
|
||||
import base64
|
||||
file_content = base64.b64encode(content).decode('utf-8')
|
||||
else:
|
||||
# For CSV/JSON, decode as text
|
||||
file_content = content.decode('utf-8')
|
||||
|
||||
|
||||
validation_data = {
|
||||
"tenant_id": str(tenant_id),
|
||||
"data": file_content,
|
||||
"data_format": detected_format,
|
||||
"filename": file.filename
|
||||
}
|
||||
|
||||
# Handle JSON data validation
|
||||
|
||||
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")
|
||||
|
||||
# Perform 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,
|
||||
|
||||
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,
|
||||
@@ -161,14 +212,19 @@ async def validate_sales_data_universal(
|
||||
"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("/tenants/{tenant_id}/sales/import/validate-csv")
|
||||
@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(...),
|
||||
@@ -184,7 +240,9 @@ async def validate_csv_data_legacy(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/tenants/{tenant_id}/sales/import")
|
||||
@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,
|
||||
@@ -196,15 +254,12 @@ async def import_sales_data(
|
||||
):
|
||||
"""Enhanced import sales data - supports multiple file formats and JSON"""
|
||||
try:
|
||||
|
||||
# Handle file upload (form data)
|
||||
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)
|
||||
|
||||
# Auto-detect format from filename
|
||||
|
||||
filename = file.filename.lower()
|
||||
if filename.endswith('.csv'):
|
||||
detected_format = 'csv'
|
||||
@@ -213,34 +268,27 @@ async def import_sales_data(
|
||||
elif filename.endswith('.json'):
|
||||
detected_format = 'json'
|
||||
else:
|
||||
detected_format = file_format or 'csv' # Default to CSV
|
||||
|
||||
# Read file content
|
||||
detected_format = file_format or 'csv'
|
||||
|
||||
content = await file.read()
|
||||
|
||||
|
||||
if detected_format in ['xlsx', 'xls', 'excel']:
|
||||
# For Excel files, encode as base64
|
||||
import base64
|
||||
file_content = base64.b64encode(content).decode('utf-8')
|
||||
else:
|
||||
# For CSV/JSON, decode as text
|
||||
file_content = content.decode('utf-8')
|
||||
|
||||
# Import the file using enhanced service
|
||||
|
||||
import_result = await import_service.process_import(
|
||||
str(tenant_id), # Ensure string type
|
||||
str(tenant_id),
|
||||
file_content,
|
||||
detected_format,
|
||||
filename=file.filename
|
||||
)
|
||||
|
||||
# Handle JSON data
|
||||
|
||||
elif data:
|
||||
logger.info("Starting enhanced JSON data import", tenant_id=tenant_id, record_count=len(data.get("records", [])))
|
||||
|
||||
# Import the data - handle different input formats
|
||||
|
||||
if "records" in data:
|
||||
# New format with records array
|
||||
records_json = json.dumps(data.get("records", []))
|
||||
import_result = await import_service.process_import(
|
||||
str(tenant_id),
|
||||
@@ -248,7 +296,6 @@ async def import_sales_data(
|
||||
"json"
|
||||
)
|
||||
else:
|
||||
# Legacy format - data field contains the data directly
|
||||
import_result = await import_service.process_import(
|
||||
str(tenant_id),
|
||||
data.get("data", ""),
|
||||
@@ -256,15 +303,14 @@ async def import_sales_data(
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="No data or file provided")
|
||||
|
||||
logger.info("Enhanced import completed",
|
||||
tenant_id=tenant_id,
|
||||
|
||||
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)
|
||||
|
||||
# Return enhanced response matching frontend expectations
|
||||
|
||||
response = {
|
||||
"success": import_result.success,
|
||||
"records_processed": import_result.records_processed,
|
||||
@@ -274,26 +320,27 @@ async def import_sales_data(
|
||||
"errors": import_result.errors,
|
||||
"warnings": import_result.warnings,
|
||||
"processing_time_seconds": import_result.processing_time_seconds,
|
||||
"records_imported": import_result.records_created, # Frontend compatibility
|
||||
"records_imported": import_result.records_created,
|
||||
"message": f"Successfully imported {import_result.records_created} records" if import_result.success else "Import completed with errors"
|
||||
}
|
||||
|
||||
# Add file-specific information if available
|
||||
|
||||
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("/tenants/{tenant_id}/sales/import/csv")
|
||||
@router.post(
|
||||
route_builder.build_operations_route("import/csv")
|
||||
)
|
||||
async def import_csv_data(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
file: UploadFile = File(...),
|
||||
@@ -303,31 +350,28 @@ async def import_csv_data(
|
||||
):
|
||||
"""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)
|
||||
|
||||
# Read file content
|
||||
|
||||
content = await file.read()
|
||||
file_content = content.decode('utf-8')
|
||||
|
||||
# Import the data
|
||||
|
||||
import_result = await import_service.process_import(
|
||||
tenant_id,
|
||||
file_content,
|
||||
"csv",
|
||||
filename=file.filename
|
||||
)
|
||||
|
||||
logger.info("CSV import completed",
|
||||
tenant_id=tenant_id,
|
||||
|
||||
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,
|
||||
@@ -338,23 +382,24 @@ async def import_csv_data(
|
||||
"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("/tenants/{tenant_id}/sales/import/template")
|
||||
@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:
|
||||
@@ -363,7 +408,7 @@ async def get_import_template(
|
||||
{
|
||||
"date": "2024-01-01T10:00:00Z",
|
||||
"product_name": "Sample Product",
|
||||
"product_category": "Sample Category",
|
||||
"product_category": "Sample Category",
|
||||
"product_sku": "SAMPLE001",
|
||||
"quantity_sold": 1,
|
||||
"unit_price": 10.50,
|
||||
@@ -380,9 +425,9 @@ async def get_import_template(
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
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)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get import template: {str(e)}")
|
||||
@@ -1,24 +1,27 @@
|
||||
# services/sales/app/api/sales.py
|
||||
# services/sales/app/api/sales_records.py
|
||||
"""
|
||||
Sales API Endpoints
|
||||
Sales Records API - Atomic CRUD operations on SalesData model
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path
|
||||
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,
|
||||
SalesDataCreate,
|
||||
SalesDataUpdate,
|
||||
SalesDataResponse,
|
||||
SalesDataResponse,
|
||||
SalesDataQuery
|
||||
)
|
||||
from app.services.sales_service import SalesService
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.auth.access_control import require_user_role
|
||||
from shared.routing import RouteBuilder
|
||||
|
||||
router = APIRouter(tags=["sales"])
|
||||
route_builder = RouteBuilder('sales')
|
||||
router = APIRouter(tags=["sales-records"])
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
@@ -27,7 +30,12 @@ def get_sales_service():
|
||||
return SalesService()
|
||||
|
||||
|
||||
@router.post("/tenants/{tenant_id}/sales", response_model=SalesDataResponse)
|
||||
@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"),
|
||||
@@ -37,23 +45,22 @@ async def create_sales_record(
|
||||
"""Create a new sales record"""
|
||||
try:
|
||||
logger.info(
|
||||
"Creating sales record",
|
||||
product=sales_data.product_name,
|
||||
"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,
|
||||
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))
|
||||
@@ -62,7 +69,10 @@ async def create_sales_record(
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create sales record: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/sales", response_model=List[SalesDataResponse])
|
||||
@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"),
|
||||
@@ -81,7 +91,6 @@ async def get_sales_records(
|
||||
):
|
||||
"""Get sales records for a tenant with filtering and pagination"""
|
||||
try:
|
||||
# Build query parameters
|
||||
query_params = SalesDataQuery(
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
@@ -96,79 +105,21 @@ async def get_sales_records(
|
||||
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"),
|
||||
sales_service: SalesService = Depends(get_sales_service)
|
||||
):
|
||||
"""Get sales analytics summary for a tenant"""
|
||||
try:
|
||||
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"),
|
||||
sales_service: SalesService = Depends(get_sales_service)
|
||||
):
|
||||
"""Get sales records for a specific product"""
|
||||
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.get("/tenants/{tenant_id}/sales/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)}")
|
||||
|
||||
|
||||
# ================================================================
|
||||
# PARAMETERIZED ROUTES - Keep these at the end to avoid conflicts
|
||||
# ================================================================
|
||||
|
||||
@router.get("/tenants/{tenant_id}/sales/{record_id}", response_model=SalesDataResponse)
|
||||
@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"),
|
||||
@@ -177,12 +128,12 @@ async def get_sales_record(
|
||||
"""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:
|
||||
@@ -190,7 +141,10 @@ async def get_sales_record(
|
||||
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)
|
||||
@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"),
|
||||
@@ -200,10 +154,10 @@ async def update_sales_record(
|
||||
"""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))
|
||||
@@ -212,7 +166,9 @@ async def update_sales_record(
|
||||
raise HTTPException(status_code=500, detail=f"Failed to update sales record: {str(e)}")
|
||||
|
||||
|
||||
@router.delete("/tenants/{tenant_id}/sales/{record_id}")
|
||||
@router.delete(
|
||||
route_builder.build_resource_detail_route("sales", "record_id")
|
||||
)
|
||||
async def delete_sales_record(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
record_id: UUID = Path(..., description="Sales record ID"),
|
||||
@@ -221,13 +177,13 @@ async def delete_sales_record(
|
||||
"""Delete a sales record"""
|
||||
try:
|
||||
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))
|
||||
@@ -236,23 +192,19 @@ async def delete_sales_record(
|
||||
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(
|
||||
@router.get(
|
||||
route_builder.build_base_route("categories"),
|
||||
response_model=List[str]
|
||||
)
|
||||
async def get_product_categories(
|
||||
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"""
|
||||
"""Get distinct product categories from sales data"""
|
||||
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))
|
||||
categories = await sales_service.get_product_categories(tenant_id)
|
||||
return categories
|
||||
|
||||
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)}")
|
||||
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)}")
|
||||
@@ -8,9 +8,9 @@ from sqlalchemy import text
|
||||
from app.core.config import settings
|
||||
from app.core.database import database_manager
|
||||
from shared.service_base import StandardFastAPIService
|
||||
# Include routers - import router BEFORE sales router to avoid conflicts
|
||||
from app.api.sales import router as sales_router
|
||||
from app.api.import_data import router as import_router
|
||||
|
||||
# Import API routers
|
||||
from app.api import sales_records, sales_operations, analytics
|
||||
|
||||
|
||||
class SalesService(StandardFastAPIService):
|
||||
@@ -48,7 +48,7 @@ class SalesService(StandardFastAPIService):
|
||||
version="1.0.0",
|
||||
log_level=settings.LOG_LEVEL,
|
||||
cors_origins=settings.CORS_ORIGINS,
|
||||
api_prefix="/api/v1",
|
||||
api_prefix="", # Empty because RouteBuilder already includes /api/v1
|
||||
database_manager=database_manager,
|
||||
expected_tables=sales_expected_tables
|
||||
)
|
||||
@@ -145,5 +145,6 @@ service.setup_standard_endpoints()
|
||||
service.setup_custom_endpoints()
|
||||
|
||||
# Include routers
|
||||
service.add_router(import_router, tags=["import"])
|
||||
service.add_router(sales_router, tags=["sales"])
|
||||
service.add_router(sales_records.router)
|
||||
service.add_router(sales_operations.router)
|
||||
service.add_router(analytics.router)
|
||||
Reference in New Issue
Block a user