Files
bakery-ia/services/sales/app/api/sales_records.py

245 lines
9.5 KiB
Python
Raw Normal View History

2025-10-06 15:27:01 +02:00
# services/sales/app/api/sales_records.py
2025-08-12 18:17:30 +02:00
"""
2025-10-06 15:27:01 +02:00
Sales Records API - Atomic CRUD operations on SalesData model
2025-08-12 18:17:30 +02:00
"""
2025-10-06 15:27:01 +02:00
from fastapi import APIRouter, Depends, HTTPException, Query, Path, status
2025-08-12 18:17:30 +02:00
from typing import List, Optional, Dict, Any
from uuid import UUID
from datetime import datetime
import structlog
from app.schemas.sales import (
2025-10-06 15:27:01 +02:00
SalesDataCreate,
2025-08-12 18:17:30 +02:00
SalesDataUpdate,
2025-10-06 15:27:01 +02:00
SalesDataResponse,
2025-08-12 18:17:30 +02:00
SalesDataQuery
)
from app.services.sales_service import SalesService
2025-10-29 06:58:05 +01:00
from app.models import AuditLog
from shared.auth.decorators import get_current_user_dep
2025-10-06 15:27:01 +02:00
from shared.auth.access_control import require_user_role
from shared.routing import RouteBuilder
from shared.security import create_audit_logger, AuditSeverity, AuditAction
2025-08-12 18:17:30 +02:00
2025-10-06 15:27:01 +02:00
route_builder = RouteBuilder('sales')
router = APIRouter(tags=["sales-records"])
2025-08-12 18:17:30 +02:00
logger = structlog.get_logger()
# Initialize audit logger
2025-10-29 06:58:05 +01:00
audit_logger = create_audit_logger("sales-service", AuditLog)
2025-08-12 18:17:30 +02:00
def get_sales_service():
"""Dependency injection for SalesService"""
return SalesService()
2025-10-06 15:27:01 +02:00
@router.post(
route_builder.build_base_route("sales"),
response_model=SalesDataResponse,
status_code=status.HTTP_201_CREATED
)
@require_user_role(['admin', 'owner', 'member'])
2025-08-12 18:17:30 +02:00
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(
2025-10-06 15:27:01 +02:00
"Creating sales record",
product=sales_data.product_name,
2025-08-12 18:17:30 +02:00
quantity=sales_data.quantity_sold,
tenant_id=tenant_id,
user_id=current_user.get("user_id")
)
2025-10-06 15:27:01 +02:00
2025-08-12 18:17:30 +02:00
record = await sales_service.create_sales_record(
2025-10-06 15:27:01 +02:00
sales_data,
tenant_id,
2025-08-12 18:17:30 +02:00
user_id=UUID(current_user["user_id"]) if current_user.get("user_id") else None
)
2025-10-06 15:27:01 +02:00
2025-08-12 18:17:30 +02:00
logger.info("Successfully created sales record", record_id=record.id, tenant_id=tenant_id)
return record
2025-10-06 15:27:01 +02:00
2025-08-12 18:17:30 +02:00
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)}")
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_base_route("sales"),
response_model=List[SalesDataResponse]
)
2025-08-12 18:17:30 +02:00
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
)
2025-10-06 15:27:01 +02:00
2025-08-12 18:17:30 +02:00
records = await sales_service.get_sales_records(tenant_id, query_params)
2025-10-06 15:27:01 +02:00
2025-08-12 18:17:30 +02:00
logger.info("Retrieved sales records", count=len(records), tenant_id=tenant_id)
return records
2025-10-06 15:27:01 +02:00
2025-08-12 18:17:30 +02:00
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)}")
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_resource_detail_route("sales", "record_id"),
response_model=SalesDataResponse
)
2025-08-12 18:17:30 +02:00
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)
2025-10-06 15:27:01 +02:00
2025-08-12 18:17:30 +02:00
if not record:
raise HTTPException(status_code=404, detail="Sales record not found")
2025-10-06 15:27:01 +02:00
2025-08-12 18:17:30 +02:00
return record
2025-10-06 15:27:01 +02:00
2025-08-12 18:17:30 +02:00
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)}")
2025-10-06 15:27:01 +02:00
@router.put(
route_builder.build_resource_detail_route("sales", "record_id"),
response_model=SalesDataResponse
)
2025-08-12 18:17:30 +02:00
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)
2025-10-06 15:27:01 +02:00
2025-08-12 18:17:30 +02:00
logger.info("Updated sales record", record_id=record_id, tenant_id=tenant_id)
return updated_record
2025-10-06 15:27:01 +02:00
2025-08-12 18:17:30 +02:00
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)}")
2025-10-06 15:27:01 +02:00
@router.delete(
route_builder.build_resource_detail_route("sales", "record_id")
)
@require_user_role(['admin', 'owner'])
2025-08-12 18:17:30 +02:00
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),
2025-08-12 18:17:30 +02:00
sales_service: SalesService = Depends(get_sales_service)
):
"""Delete a sales record (Admin+ only)"""
2025-08-12 18:17:30 +02:00
try:
# Get record details before deletion for audit log
record = await sales_service.get_sales_record(record_id, tenant_id)
2025-08-12 18:17:30 +02:00
success = await sales_service.delete_sales_record(record_id, tenant_id)
2025-10-06 15:27:01 +02:00
2025-08-12 18:17:30 +02:00
if not success:
raise HTTPException(status_code=404, detail="Sales record not found")
2025-10-06 15:27:01 +02:00
# 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))
2025-08-12 18:17:30 +02:00
logger.info("Deleted sales record", record_id=record_id, tenant_id=tenant_id)
return {"message": "Sales record deleted successfully"}
2025-10-06 15:27:01 +02:00
2025-08-12 18:17:30 +02:00
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
2025-08-12 18:17:30 +02:00
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)}")
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_base_route("categories"),
response_model=List[str]
)
async def get_product_categories(
2025-08-12 18:17:30 +02:00
tenant_id: UUID = Path(..., description="Tenant ID"),
sales_service: SalesService = Depends(get_sales_service)
):
2025-10-06 15:27:01 +02:00
"""Get distinct product categories from sales data"""
2025-08-12 18:17:30 +02:00
try:
2025-10-06 15:27:01 +02:00
categories = await sales_service.get_product_categories(tenant_id)
return categories
2025-08-12 18:17:30 +02:00
except Exception as e:
2025-10-06 15:27:01 +02:00
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)}")