2025-07-18 11:51:43 +02:00
|
|
|
# ================================================================
|
2025-07-19 12:09:10 +02:00
|
|
|
# services/data/app/api/sales.py - FIXED VERSION
|
2025-07-18 11:51:43 +02:00
|
|
|
# ================================================================
|
2025-07-19 12:09:10 +02:00
|
|
|
"""Sales data API endpoints with improved error handling"""
|
2025-07-18 11:51:43 +02:00
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Query, Response
|
|
|
|
|
from fastapi.responses import StreamingResponse
|
2025-07-18 11:51:43 +02:00
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
2025-07-19 12:09:10 +02:00
|
|
|
from typing import List, Optional
|
2025-07-18 11:51:43 +02:00
|
|
|
import uuid
|
|
|
|
|
from datetime import datetime
|
2025-07-19 12:09:10 +02:00
|
|
|
import base64
|
|
|
|
|
import structlog
|
2025-07-18 11:51:43 +02:00
|
|
|
|
|
|
|
|
from app.core.database import get_db
|
2025-07-18 16:48:49 +02:00
|
|
|
from app.core.auth import get_current_user, AuthInfo
|
2025-07-18 11:51:43 +02:00
|
|
|
from app.services.sales_service import SalesService
|
|
|
|
|
from app.services.data_import_service import DataImportService
|
|
|
|
|
from app.services.messaging import data_publisher
|
|
|
|
|
from app.schemas.sales import (
|
|
|
|
|
SalesDataCreate,
|
|
|
|
|
SalesDataResponse,
|
|
|
|
|
SalesDataQuery,
|
2025-07-19 12:09:10 +02:00
|
|
|
SalesDataImport,
|
|
|
|
|
SalesImportResult,
|
|
|
|
|
SalesValidationResult,
|
|
|
|
|
SalesExportRequest
|
2025-07-18 11:51:43 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
router = APIRouter()
|
2025-07-19 12:09:10 +02:00
|
|
|
logger = structlog.get_logger()
|
2025-07-18 11:51:43 +02:00
|
|
|
|
|
|
|
|
@router.post("/", response_model=SalesDataResponse)
|
|
|
|
|
async def create_sales_record(
|
|
|
|
|
sales_data: SalesDataCreate,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
2025-07-18 16:48:49 +02:00
|
|
|
current_user: AuthInfo = Depends(get_current_user)
|
2025-07-18 11:51:43 +02:00
|
|
|
):
|
|
|
|
|
"""Create a new sales record"""
|
|
|
|
|
try:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.debug("API: Creating sales record", product=sales_data.product_name, quantity=sales_data.quantity_sold)
|
|
|
|
|
|
2025-07-18 11:51:43 +02:00
|
|
|
record = await SalesService.create_sales_record(sales_data, db)
|
|
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
# Publish event (with error handling)
|
|
|
|
|
try:
|
|
|
|
|
await data_publisher.publish_sales_created({
|
|
|
|
|
"tenant_id": str(sales_data.tenant_id),
|
|
|
|
|
"product_name": sales_data.product_name,
|
|
|
|
|
"quantity_sold": sales_data.quantity_sold,
|
|
|
|
|
"revenue": sales_data.revenue,
|
|
|
|
|
"source": sales_data.source,
|
|
|
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
|
|
|
})
|
|
|
|
|
except Exception as pub_error:
|
|
|
|
|
logger.warning("Failed to publish sales created event", error=str(pub_error))
|
|
|
|
|
# Continue processing - event publishing failure shouldn't break the API
|
2025-07-18 11:51:43 +02:00
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.debug("Successfully created sales record", record_id=record.id)
|
2025-07-18 11:51:43 +02:00
|
|
|
return record
|
2025-07-19 12:09:10 +02:00
|
|
|
|
2025-07-18 11:51:43 +02:00
|
|
|
except Exception as e:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.error("Failed to create sales record", error=str(e))
|
|
|
|
|
import traceback
|
|
|
|
|
logger.error("Sales creation traceback", traceback=traceback.format_exc())
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to create sales record: {str(e)}")
|
2025-07-18 11:51:43 +02:00
|
|
|
|
|
|
|
|
@router.post("/query", response_model=List[SalesDataResponse])
|
|
|
|
|
async def get_sales_data(
|
|
|
|
|
query: SalesDataQuery,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
2025-07-18 16:48:49 +02:00
|
|
|
current_user: AuthInfo = Depends(get_current_user)
|
2025-07-18 11:51:43 +02:00
|
|
|
):
|
|
|
|
|
"""Get sales data by query parameters"""
|
|
|
|
|
try:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.debug("API: Querying sales data", tenant_id=query.tenant_id)
|
|
|
|
|
|
2025-07-18 11:51:43 +02:00
|
|
|
records = await SalesService.get_sales_data(query, db)
|
2025-07-19 12:09:10 +02:00
|
|
|
|
|
|
|
|
logger.debug("Successfully retrieved sales data", count=len(records))
|
2025-07-18 11:51:43 +02:00
|
|
|
return records
|
2025-07-19 12:09:10 +02:00
|
|
|
|
2025-07-18 11:51:43 +02:00
|
|
|
except Exception as e:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.error("Failed to query sales data", error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to query sales data: {str(e)}")
|
2025-07-18 11:51:43 +02:00
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
@router.post("/import", response_model=SalesImportResult)
|
2025-07-18 11:51:43 +02:00
|
|
|
async def import_sales_data(
|
|
|
|
|
tenant_id: str = Form(...),
|
|
|
|
|
file_format: str = Form(...),
|
|
|
|
|
file: UploadFile = File(...),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
2025-07-18 16:48:49 +02:00
|
|
|
current_user: AuthInfo = Depends(get_current_user)
|
2025-07-18 11:51:43 +02:00
|
|
|
):
|
|
|
|
|
"""Import sales data from file"""
|
|
|
|
|
try:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.debug("API: Importing sales data", tenant_id=tenant_id, format=file_format, filename=file.filename)
|
|
|
|
|
|
2025-07-18 11:51:43 +02:00
|
|
|
# Read file content
|
|
|
|
|
content = await file.read()
|
|
|
|
|
file_content = content.decode('utf-8')
|
|
|
|
|
|
|
|
|
|
# Process import
|
|
|
|
|
result = await DataImportService.process_upload(
|
|
|
|
|
tenant_id, file_content, file_format, db
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if result["success"]:
|
2025-07-19 12:09:10 +02:00
|
|
|
# Publish event (with error handling)
|
|
|
|
|
try:
|
|
|
|
|
await data_publisher.publish_data_imported({
|
|
|
|
|
"tenant_id": tenant_id,
|
|
|
|
|
"type": "bulk_import",
|
|
|
|
|
"format": file_format,
|
|
|
|
|
"filename": file.filename,
|
|
|
|
|
"records_created": result["records_created"],
|
|
|
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
|
|
|
})
|
|
|
|
|
except Exception as pub_error:
|
|
|
|
|
logger.warning("Failed to publish data imported event", error=str(pub_error))
|
|
|
|
|
# Continue processing
|
|
|
|
|
|
|
|
|
|
logger.debug("Import completed", success=result["success"], records_created=result.get("records_created", 0))
|
2025-07-18 11:51:43 +02:00
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.error("Failed to import sales data", error=str(e))
|
|
|
|
|
import traceback
|
|
|
|
|
logger.error("Sales import traceback", traceback=traceback.format_exc())
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to import sales data: {str(e)}")
|
2025-07-18 11:51:43 +02:00
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
@router.post("/import/json", response_model=SalesImportResult)
|
2025-07-18 11:51:43 +02:00
|
|
|
async def import_sales_json(
|
|
|
|
|
import_data: SalesDataImport,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
2025-07-18 16:48:49 +02:00
|
|
|
current_user: AuthInfo = Depends(get_current_user)
|
2025-07-18 11:51:43 +02:00
|
|
|
):
|
|
|
|
|
"""Import sales data from JSON"""
|
|
|
|
|
try:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.debug("API: Importing JSON sales data", tenant_id=import_data.tenant_id)
|
|
|
|
|
|
2025-07-18 11:51:43 +02:00
|
|
|
result = await DataImportService.process_upload(
|
|
|
|
|
str(import_data.tenant_id),
|
|
|
|
|
import_data.data,
|
|
|
|
|
import_data.data_format,
|
|
|
|
|
db
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if result["success"]:
|
2025-07-19 12:09:10 +02:00
|
|
|
# Publish event (with error handling)
|
|
|
|
|
try:
|
|
|
|
|
await data_publisher.publish_data_imported({
|
|
|
|
|
"tenant_id": str(import_data.tenant_id),
|
|
|
|
|
"type": "json_import",
|
|
|
|
|
"records_created": result["records_created"],
|
|
|
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
|
|
|
})
|
|
|
|
|
except Exception as pub_error:
|
|
|
|
|
logger.warning("Failed to publish JSON import event", error=str(pub_error))
|
|
|
|
|
# Continue processing
|
|
|
|
|
|
|
|
|
|
logger.debug("JSON import completed", success=result["success"], records_created=result.get("records_created", 0))
|
2025-07-18 11:51:43 +02:00
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.error("Failed to import JSON sales data", error=str(e))
|
|
|
|
|
import traceback
|
|
|
|
|
logger.error("JSON import traceback", traceback=traceback.format_exc())
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to import JSON sales data: {str(e)}")
|
2025-07-18 11:51:43 +02:00
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
@router.post("/import/validate", response_model=SalesValidationResult)
|
2025-07-18 11:51:43 +02:00
|
|
|
async def validate_import_data(
|
|
|
|
|
import_data: SalesDataImport,
|
2025-07-18 16:48:49 +02:00
|
|
|
current_user: AuthInfo = Depends(get_current_user)
|
2025-07-18 11:51:43 +02:00
|
|
|
):
|
|
|
|
|
"""Validate import data before processing"""
|
|
|
|
|
try:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.debug("API: Validating import data", tenant_id=import_data.tenant_id)
|
|
|
|
|
|
2025-07-18 11:51:43 +02:00
|
|
|
validation = await DataImportService.validate_import_data(
|
|
|
|
|
import_data.model_dump()
|
|
|
|
|
)
|
2025-07-19 12:09:10 +02:00
|
|
|
|
|
|
|
|
logger.debug("Validation completed", is_valid=validation.get("is_valid", False))
|
2025-07-18 11:51:43 +02:00
|
|
|
return validation
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.error("Failed to validate import data", error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to validate import data: {str(e)}")
|
2025-07-18 11:51:43 +02:00
|
|
|
|
|
|
|
|
@router.get("/import/template/{format_type}")
|
|
|
|
|
async def get_import_template(
|
|
|
|
|
format_type: str,
|
2025-07-18 16:48:49 +02:00
|
|
|
current_user: AuthInfo = Depends(get_current_user)
|
2025-07-18 11:51:43 +02:00
|
|
|
):
|
|
|
|
|
"""Get import template for specified format"""
|
|
|
|
|
try:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.debug("API: Getting import template", format=format_type)
|
|
|
|
|
|
2025-07-18 11:51:43 +02:00
|
|
|
template = await DataImportService.get_import_template(format_type)
|
|
|
|
|
|
|
|
|
|
if "error" in template:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.warning("Template generation error", error=template["error"])
|
2025-07-18 11:51:43 +02:00
|
|
|
raise HTTPException(status_code=400, detail=template["error"])
|
|
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.debug("Template generated successfully", format=format_type)
|
|
|
|
|
|
2025-07-18 11:51:43 +02:00
|
|
|
if format_type.lower() == "csv":
|
|
|
|
|
return Response(
|
|
|
|
|
content=template["template"],
|
|
|
|
|
media_type="text/csv",
|
|
|
|
|
headers={"Content-Disposition": f"attachment; filename={template['filename']}"}
|
|
|
|
|
)
|
|
|
|
|
elif format_type.lower() == "json":
|
|
|
|
|
return Response(
|
|
|
|
|
content=template["template"],
|
|
|
|
|
media_type="application/json",
|
|
|
|
|
headers={"Content-Disposition": f"attachment; filename={template['filename']}"}
|
|
|
|
|
)
|
|
|
|
|
elif format_type.lower() in ["excel", "xlsx"]:
|
|
|
|
|
return Response(
|
|
|
|
|
content=base64.b64decode(template["template"]),
|
|
|
|
|
media_type=template["content_type"],
|
|
|
|
|
headers={"Content-Disposition": f"attachment; filename={template['filename']}"}
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
return template
|
|
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
2025-07-18 11:51:43 +02:00
|
|
|
except Exception as e:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.error("Failed to generate import template", error=str(e))
|
|
|
|
|
import traceback
|
|
|
|
|
logger.error("Template generation traceback", traceback=traceback.format_exc())
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to generate template: {str(e)}")
|
2025-07-18 11:51:43 +02:00
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
@router.get("/analytics/{tenant_id}")
|
|
|
|
|
async def get_sales_analytics(
|
|
|
|
|
tenant_id: str,
|
|
|
|
|
start_date: Optional[datetime] = Query(None, description="Start date"),
|
|
|
|
|
end_date: Optional[datetime] = Query(None, description="End date"),
|
2025-07-18 11:51:43 +02:00
|
|
|
db: AsyncSession = Depends(get_db),
|
2025-07-18 16:48:49 +02:00
|
|
|
current_user: AuthInfo = Depends(get_current_user)
|
2025-07-18 11:51:43 +02:00
|
|
|
):
|
2025-07-19 12:09:10 +02:00
|
|
|
"""Get sales analytics for tenant"""
|
2025-07-18 11:51:43 +02:00
|
|
|
try:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.debug("API: Getting sales analytics", tenant_id=tenant_id)
|
2025-07-18 11:51:43 +02:00
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
analytics = await SalesService.get_sales_analytics(
|
|
|
|
|
tenant_id, start_date, end_date, db
|
2025-07-18 11:51:43 +02:00
|
|
|
)
|
|
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.debug("Analytics generated successfully", tenant_id=tenant_id)
|
|
|
|
|
return analytics
|
2025-07-18 11:51:43 +02:00
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.error("Failed to generate sales analytics", error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to generate analytics: {str(e)}")
|
2025-07-18 11:51:43 +02:00
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
@router.post("/export/{tenant_id}")
|
|
|
|
|
async def export_sales_data(
|
2025-07-18 11:51:43 +02:00
|
|
|
tenant_id: str,
|
2025-07-19 12:09:10 +02:00
|
|
|
export_format: str = Query("csv", description="Export format: csv, excel, json"),
|
|
|
|
|
start_date: Optional[datetime] = Query(None, description="Start date"),
|
|
|
|
|
end_date: Optional[datetime] = Query(None, description="End date"),
|
|
|
|
|
products: Optional[List[str]] = Query(None, description="Filter by products"),
|
2025-07-18 11:51:43 +02:00
|
|
|
db: AsyncSession = Depends(get_db),
|
2025-07-18 16:48:49 +02:00
|
|
|
current_user: AuthInfo = Depends(get_current_user)
|
2025-07-18 11:51:43 +02:00
|
|
|
):
|
2025-07-19 12:09:10 +02:00
|
|
|
"""Export sales data in specified format"""
|
2025-07-18 11:51:43 +02:00
|
|
|
try:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.debug("API: Exporting sales data", tenant_id=tenant_id, format=export_format)
|
2025-07-18 11:51:43 +02:00
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
export_result = await SalesService.export_sales_data(
|
|
|
|
|
tenant_id, export_format, start_date, end_date, products, db
|
2025-07-18 11:51:43 +02:00
|
|
|
)
|
|
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
if not export_result:
|
|
|
|
|
raise HTTPException(status_code=404, detail="No data found for export")
|
2025-07-18 11:51:43 +02:00
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.debug("Export completed successfully", tenant_id=tenant_id, format=export_format)
|
2025-07-18 11:51:43 +02:00
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
return StreamingResponse(
|
|
|
|
|
iter([export_result["content"]]),
|
|
|
|
|
media_type=export_result["media_type"],
|
|
|
|
|
headers={"Content-Disposition": f"attachment; filename={export_result['filename']}"}
|
2025-07-18 11:51:43 +02:00
|
|
|
)
|
|
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
2025-07-18 11:51:43 +02:00
|
|
|
except Exception as e:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.error("Failed to export sales data", error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to export sales data: {str(e)}")
|
2025-07-18 11:51:43 +02:00
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
@router.delete("/{record_id}")
|
|
|
|
|
async def delete_sales_record(
|
|
|
|
|
record_id: str,
|
2025-07-18 11:51:43 +02:00
|
|
|
db: AsyncSession = Depends(get_db),
|
2025-07-18 16:48:49 +02:00
|
|
|
current_user: AuthInfo = Depends(get_current_user)
|
2025-07-18 11:51:43 +02:00
|
|
|
):
|
2025-07-19 12:09:10 +02:00
|
|
|
"""Delete a sales record"""
|
2025-07-18 11:51:43 +02:00
|
|
|
try:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.debug("API: Deleting sales record", record_id=record_id)
|
2025-07-18 11:51:43 +02:00
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
success = await SalesService.delete_sales_record(record_id, db)
|
2025-07-18 11:51:43 +02:00
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
if not success:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Sales record not found")
|
2025-07-18 11:51:43 +02:00
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.debug("Sales record deleted successfully", record_id=record_id)
|
|
|
|
|
return {"status": "success", "message": "Sales record deleted successfully"}
|
2025-07-18 11:51:43 +02:00
|
|
|
|
2025-07-19 12:09:10 +02:00
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
2025-07-18 11:51:43 +02:00
|
|
|
except Exception as e:
|
2025-07-19 12:09:10 +02:00
|
|
|
logger.error("Failed to delete sales record", error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to delete sales record: {str(e)}")
|