REFACTOR API gateway

This commit is contained in:
Urtzi Alfaro
2025-07-26 18:46:52 +02:00
parent e49893e10a
commit e4885db828
24 changed files with 1049 additions and 1080 deletions

View File

@@ -1,13 +1,13 @@
# ================================================================
# services/data/app/api/sales.py - UPDATED WITH UNIFIED AUTH
# services/data/app/api/sales.py - FIXED FOR NEW TENANT-SCOPED ARCHITECTURE
# ================================================================
"""Sales data API endpoints with unified authentication"""
"""Sales data API endpoints with tenant-scoped URLs"""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Query, Response
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Query, Response, Path
from fastapi.responses import StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Optional, Dict, Any
import uuid
from uuid import UUID
from datetime import datetime
import base64
import structlog
@@ -31,22 +31,23 @@ from app.services.messaging import (
)
# Import unified authentication from shared library
from shared.auth.decorators import (
get_current_user_dep,
get_current_tenant_id_dep
)
from shared.auth.decorators import get_current_user_dep
router = APIRouter(tags=["sales"])
logger = structlog.get_logger()
@router.post("/", response_model=SalesDataResponse)
# ================================================================
# TENANT-SCOPED SALES ENDPOINTS
# ================================================================
@router.post("/tenants/{tenant_id}/sales", response_model=SalesDataResponse)
async def create_sales_record(
sales_data: SalesDataCreate,
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Create a new sales record"""
"""Create a new sales record for tenant"""
try:
logger.debug("Creating sales record",
product=sales_data.product_name,
@@ -54,7 +55,7 @@ async def create_sales_record(
tenant_id=tenant_id,
user_id=current_user["user_id"])
# Override tenant_id from token/header
# Override tenant_id from URL path (gateway already verified access)
sales_data.tenant_id = tenant_id
record = await SalesService.create_sales_record(sales_data, db)
@@ -85,14 +86,14 @@ async def create_sales_record(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to create sales record: {str(e)}")
@router.post("/bulk", response_model=List[SalesDataResponse])
@router.post("/tenants/{tenant_id}/sales/bulk", response_model=List[SalesDataResponse])
async def create_bulk_sales(
sales_data: List[SalesDataCreate],
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Create multiple sales records"""
"""Create multiple sales records for tenant"""
try:
logger.debug("Creating bulk sales records",
count=len(sales_data),
@@ -127,16 +128,16 @@ async def create_bulk_sales(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to create bulk sales records: {str(e)}")
@router.get("/", response_model=List[SalesDataResponse])
@router.get("/tenants/{tenant_id}/sales", response_model=List[SalesDataResponse])
async def get_sales_data(
start_date: Optional[datetime] = Query(None),
end_date: Optional[datetime] = Query(None),
product_name: Optional[str] = Query(None),
tenant_id: str = Depends(get_current_tenant_id_dep),
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"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get sales data with filters"""
"""Get sales data for tenant with filters"""
try:
logger.debug("Querying sales data",
tenant_id=tenant_id,
@@ -164,15 +165,15 @@ async def get_sales_data(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to query sales data: {str(e)}")
@router.post("/import", response_model=SalesImportResult)
@router.post("/tenants/{tenant_id}/sales/import", response_model=SalesImportResult)
async def import_sales_data(
tenant_id: UUID = Path(..., description="Tenant ID"),
file: UploadFile = File(...),
file_format: str = Form(...),
tenant_id: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Import sales data from file"""
"""Import sales data from file for tenant"""
try:
logger.info("Importing sales data",
tenant_id=tenant_id,
@@ -220,26 +221,27 @@ async def import_sales_data(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to import sales data: {str(e)}")
@router.post("/import/validate", response_model=SalesValidationResult)
@router.post("/tenants/{tenant_id}/sales/import/validate", response_model=SalesValidationResult)
async def validate_import_data(
import_data: SalesDataImport,
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep)
):
"""Validate import data before processing"""
"""Validate import data - Gateway already verified tenant access"""
try:
logger.debug("Validating import data", tenant_id=tenant_id)
logger.debug("Validating import data",
tenant_id=tenant_id,
user_id=current_user["user_id"])
# Override tenant_id
# Set tenant context from URL path
import_data.tenant_id = tenant_id
validation = await DataImportService.validate_import_data(
import_data.model_dump()
)
validation = await DataImportService.validate_import_data(import_data.model_dump())
logger.debug("Validation completed",
is_valid=validation.get("is_valid", False),
tenant_id=tenant_id)
return validation
except Exception as e:
@@ -248,15 +250,17 @@ async def validate_import_data(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to validate import data: {str(e)}")
@router.get("/import/template/{format_type}")
@router.get("/tenants/{tenant_id}/sales/import/template/{format_type}")
async def get_import_template(
format_type: str,
tenant_id: UUID = Path(..., description="Tenant ID"),
format_type: str = Path(..., description="Template format: csv, json, excel"),
current_user: Dict[str, Any] = Depends(get_current_user_dep)
):
"""Get import template for specified format"""
try:
logger.debug("Getting import template",
format=format_type,
tenant_id=tenant_id,
user_id=current_user["user_id"])
template = await DataImportService.get_import_template(format_type)
@@ -265,7 +269,9 @@ async def get_import_template(
logger.warning("Template generation error", error=template["error"])
raise HTTPException(status_code=400, detail=template["error"])
logger.debug("Template generated successfully", format=format_type)
logger.debug("Template generated successfully",
format=format_type,
tenant_id=tenant_id)
if format_type.lower() == "csv":
return Response(
@@ -291,14 +297,16 @@ async def get_import_template(
except HTTPException:
raise
except Exception as e:
logger.error("Failed to generate import template", error=str(e))
logger.error("Failed to generate import template",
error=str(e),
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to generate template: {str(e)}")
@router.get("/analytics")
@router.get("/tenants/{tenant_id}/sales/analytics")
async def get_sales_analytics(
tenant_id: UUID = Path(..., description="Tenant ID"),
start_date: Optional[datetime] = Query(None, description="Start date"),
end_date: Optional[datetime] = Query(None, description="End date"),
tenant_id: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
@@ -322,17 +330,17 @@ async def get_sales_analytics(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to generate analytics: {str(e)}")
@router.post("/export")
@router.post("/tenants/{tenant_id}/sales/export")
async def export_sales_data(
tenant_id: UUID = Path(..., description="Tenant ID"),
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"),
tenant_id: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Export sales data in specified format"""
"""Export sales data in specified format for tenant"""
try:
logger.info("Exporting sales data",
tenant_id=tenant_id,
@@ -376,14 +384,14 @@ async def export_sales_data(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to export sales data: {str(e)}")
@router.delete("/{record_id}")
@router.delete("/tenants/{tenant_id}/sales/{record_id}")
async def delete_sales_record(
record_id: str,
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
record_id: str = Path(..., description="Sales record ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Delete a sales record"""
"""Delete a sales record for tenant"""
try:
logger.info("Deleting sales record",
record_id=record_id,
@@ -413,14 +421,14 @@ async def delete_sales_record(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to delete sales record: {str(e)}")
@router.get("/summary")
@router.get("/tenants/{tenant_id}/sales/summary")
async def get_sales_summary(
tenant_id: UUID = Path(..., description="Tenant ID"),
period: str = Query("daily", description="Summary period: daily, weekly, monthly"),
tenant_id: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get sales summary for specified period"""
"""Get sales summary for specified period for tenant"""
try:
logger.debug("Getting sales summary",
tenant_id=tenant_id,
@@ -437,13 +445,13 @@ async def get_sales_summary(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to generate summary: {str(e)}")
@router.get("/products")
@router.get("/tenants/{tenant_id}/sales/products")
async def get_products_list(
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get list of all products with sales data"""
"""Get list of all products with sales data for tenant"""
try:
logger.debug("Getting products list", tenant_id=tenant_id)
@@ -458,4 +466,78 @@ async def get_products_list(
logger.error("Failed to get products list",
error=str(e),
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to get products list: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to get products list: {str(e)}")
@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: str = Path(..., description="Sales record ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get a specific sales record for tenant"""
try:
logger.debug("Getting sales record",
record_id=record_id,
tenant_id=tenant_id)
record = await SalesService.get_sales_record(record_id, db)
if not record or record.tenant_id != tenant_id:
raise HTTPException(status_code=404, detail="Sales record not found")
logger.debug("Sales record retrieved",
record_id=record_id,
tenant_id=tenant_id)
return record
except HTTPException:
raise
except Exception as e:
logger.error("Failed to get sales record",
error=str(e),
tenant_id=tenant_id,
record_id=record_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(
sales_data: SalesDataCreate,
record_id: str = Path(..., description="Sales record ID"),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Update a sales record for tenant"""
try:
logger.info("Updating sales record",
record_id=record_id,
tenant_id=tenant_id,
user_id=current_user["user_id"])
# Verify record exists and belongs to tenant
existing_record = await SalesService.get_sales_record(record_id, db)
if not existing_record or existing_record.tenant_id != tenant_id:
raise HTTPException(status_code=404, detail="Sales record not found")
# Override tenant_id from URL path
sales_data.tenant_id = tenant_id
updated_record = await SalesService.update_sales_record(record_id, sales_data, db)
if not updated_record:
raise HTTPException(status_code=404, detail="Sales record not found")
logger.info("Sales record updated successfully",
record_id=record_id,
tenant_id=tenant_id)
return updated_record
except HTTPException:
raise
except Exception as e:
logger.error("Failed to update sales record",
error=str(e),
tenant_id=tenant_id,
record_id=record_id)
raise HTTPException(status_code=500, detail=f"Failed to update sales record: {str(e)}")

View File

@@ -3,11 +3,12 @@
# ================================================================
"""Traffic data API endpoints with improved error handling"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import APIRouter, Depends, HTTPException, Query, Path
from typing import List, Dict, Any
from datetime import datetime, timedelta
import structlog
from uuid import UUID
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.services.traffic_service import TrafficService
@@ -23,14 +24,15 @@ from shared.auth.decorators import (
get_current_tenant_id_dep
)
router = APIRouter()
router = APIRouter(tags=["traffic"])
traffic_service = TrafficService()
logger = structlog.get_logger()
@router.get("/current", response_model=TrafficDataResponse)
@router.get("/tenants/{tenant_id}/current", response_model=TrafficDataResponse)
async def get_current_traffic(
latitude: float = Query(..., description="Latitude"),
longitude: float = Query(..., description="Longitude"),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
):
"""Get current traffic data for location"""
@@ -69,13 +71,14 @@ async def get_current_traffic(
logger.error("Traffic API traceback", traceback=traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.get("/historical", response_model=List[TrafficDataResponse])
@router.get("/tenants/{tenant_id}/historical", response_model=List[TrafficDataResponse])
async def get_historical_traffic(
latitude: float = Query(..., description="Latitude"),
longitude: float = Query(..., description="Longitude"),
start_date: datetime = Query(..., description="Start date"),
end_date: datetime = Query(..., description="End date"),
db: AsyncSession = Depends(get_db),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
):
"""Get historical traffic data"""
@@ -115,11 +118,12 @@ async def get_historical_traffic(
logger.error("Unexpected error in historical traffic API", error=str(e))
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.post("/store")
@router.post("/tenants/{tenant_id}/store")
async def store_traffic_data(
latitude: float = Query(..., description="Latitude"),
longitude: float = Query(..., description="Longitude"),
db: AsyncSession = Depends(get_db),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep)
):
"""Store current traffic data to database"""

View File

@@ -1,10 +1,11 @@
# services/data/app/api/weather.py - UPDATED WITH UNIFIED AUTH
"""Weather data API endpoints with unified authentication"""
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks, Path
from typing import List, Optional, Dict, Any
from datetime import datetime, date
import structlog
from uuid import UUID
from app.schemas.external import (
WeatherDataResponse,
@@ -19,14 +20,14 @@ from shared.auth.decorators import (
get_current_tenant_id_dep
)
router = APIRouter(prefix="/weather", tags=["weather"])
router = APIRouter(tags=["weather"])
logger = structlog.get_logger()
@router.get("/current", response_model=WeatherDataResponse)
@router.get("/tenants/{tenant_id}/current", response_model=WeatherDataResponse)
async def get_current_weather(
latitude: float = Query(..., description="Latitude"),
longitude: float = Query(..., description="Longitude"),
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
):
"""Get current weather data for location"""
@@ -64,12 +65,12 @@ async def get_current_weather(
logger.error("Failed to get current weather", error=str(e))
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.get("/forecast", response_model=List[WeatherForecastResponse])
@router.get("/tenants/{tenant_id}/forecast", response_model=List[WeatherForecastResponse])
async def get_weather_forecast(
latitude: float = Query(..., description="Latitude"),
longitude: float = Query(..., description="Longitude"),
days: int = Query(7, description="Number of forecast days", ge=1, le=14),
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
):
"""Get weather forecast for location"""
@@ -108,13 +109,13 @@ async def get_weather_forecast(
logger.error("Failed to get weather forecast", error=str(e))
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.get("/history", response_model=List[WeatherDataResponse])
@router.get("/tenants/{tenant_id}/history", response_model=List[WeatherDataResponse])
async def get_weather_history(
start_date: date = Query(..., description="Start date"),
end_date: date = Query(..., description="End date"),
latitude: float = Query(..., description="Latitude"),
longitude: float = Query(..., description="Longitude"),
tenant_id: str = Depends(get_current_tenant_id_dep)
tenant_id: str = Path(..., description="Tenant ID")
):
"""Get historical weather data"""
try:
@@ -134,11 +135,11 @@ async def get_weather_history(
logger.error("Failed to get weather history", error=str(e))
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.post("/sync")
@router.post("/tenants/{tenant_id}/sync")
async def sync_weather_data(
background_tasks: BackgroundTasks,
force: bool = Query(False, description="Force sync even if recently synced"),
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
):
"""Manually trigger weather data synchronization"""