REFACTOR ALL APIs

This commit is contained in:
Urtzi Alfaro
2025-10-06 15:27:01 +02:00
parent dc8221bd2f
commit 38fb98bc27
166 changed files with 18454 additions and 13605 deletions

View 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)}")

View File

@@ -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)}")

View File

@@ -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)}")

View File

@@ -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)