Start integrating the onboarding flow with backend 3

This commit is contained in:
Urtzi Alfaro
2025-09-04 23:19:53 +02:00
parent 9eedc2e5f2
commit 0faaa25e58
26 changed files with 314 additions and 767 deletions

View File

@@ -25,7 +25,12 @@ function App() {
return (
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<BrowserRouter
future={{
v7_startTransition: true,
v7_relativeSplatPath: true,
}}
>
<ThemeProvider>
<AuthProvider>
<SSEProvider>

View File

@@ -87,6 +87,26 @@ export const SSEProvider: React.FC<SSEProviderProps> = ({ children }) => {
}
};
// Handle connection events from backend
eventSource.addEventListener('connection', (event) => {
try {
const data = JSON.parse(event.data);
console.log('SSE connection confirmed:', data.message);
} catch (error) {
console.error('Error parsing connection event:', error);
}
});
// Handle heartbeat events
eventSource.addEventListener('heartbeat', (event) => {
try {
const data = JSON.parse(event.data);
console.log('SSE heartbeat received:', new Date(data.timestamp * 1000));
} catch (error) {
console.error('Error parsing heartbeat event:', error);
}
});
eventSource.onerror = (error) => {
console.error('SSE connection error:', error);
setIsConnected(false);
@@ -104,7 +124,7 @@ export const SSEProvider: React.FC<SSEProviderProps> = ({ children }) => {
}
};
// Handle custom event types
// Handle notification events (alerts and recommendations from alert_processor)
eventSource.addEventListener('notification', (event) => {
try {
const data = JSON.parse(event.data);
@@ -116,12 +136,23 @@ export const SSEProvider: React.FC<SSEProviderProps> = ({ children }) => {
setLastEvent(sseEvent);
// Determine toast type based on severity and item_type
let toastType: 'info' | 'success' | 'warning' | 'error' = 'info';
if (data.item_type === 'alert') {
if (data.severity === 'urgent') toastType = 'error';
else if (data.severity === 'high') toastType = 'error';
else if (data.severity === 'medium') toastType = 'warning';
else toastType = 'info';
} else if (data.item_type === 'recommendation') {
toastType = 'info';
}
// Show toast notification
showToast({
type: data.type || 'info',
type: toastType,
title: data.title || 'Notificación',
message: data.message,
duration: data.persistent ? 0 : 5000,
duration: data.severity === 'urgent' ? 0 : 5000, // Keep urgent alerts until dismissed
});
// Trigger registered listeners
@@ -145,10 +176,10 @@ export const SSEProvider: React.FC<SSEProviderProps> = ({ children }) => {
setLastEvent(sseEvent);
// Show inventory alert
// Show inventory alert (high/urgent alerts from alert_processor)
showToast({
type: 'warning',
title: 'Alerta de Inventario',
type: data.severity === 'urgent' ? 'error' : 'warning',
title: data.title || 'Alerta de Inventario',
message: data.message,
duration: 0, // Keep until dismissed
});

View File

@@ -23,6 +23,24 @@ import {
class ProcurementService {
private getTenantId(): string {
const tenantStorage = localStorage.getItem('tenant-storage');
if (tenantStorage) {
try {
const { state } = JSON.parse(tenantStorage);
return state?.currentTenant?.id;
} catch {
return '';
}
}
return '';
}
private getBaseUrl(): string {
const tenantId = this.getTenantId();
return `/tenants/${tenantId}`;
}
// Purchase Order management
async getPurchaseOrders(params?: {
page?: number;
@@ -43,34 +61,34 @@ class ProcurementService {
}
const url = queryParams.toString()
? `/purchase-orders?${queryParams.toString()}`
: `/purchase-orders`;
? `${this.getBaseUrl()}/purchase-orders?${queryParams.toString()}`
: `${this.getBaseUrl()}/purchase-orders`;
return apiClient.get(url);
}
async getPurchaseOrder(orderId: string): Promise<ApiResponse<PurchaseOrderResponse>> {
return apiClient.get(`/purchase-orders/${orderId}`);
return apiClient.get(`${this.getBaseUrl()}/purchase-orders/${orderId}`);
}
async createPurchaseOrder(orderData: PurchaseOrderCreate): Promise<ApiResponse<PurchaseOrderResponse>> {
return apiClient.post(`/purchase-orders`, orderData);
return apiClient.post(`${this.getBaseUrl()}/purchase-orders`, orderData);
}
async updatePurchaseOrder(orderId: string, orderData: PurchaseOrderUpdate): Promise<ApiResponse<PurchaseOrderResponse>> {
return apiClient.put(`/purchase-orders/${orderId}`, orderData);
return apiClient.put(`${this.getBaseUrl()}/purchase-orders/${orderId}`, orderData);
}
async approvePurchaseOrder(orderId: string): Promise<ApiResponse<PurchaseOrderResponse>> {
return apiClient.post(`/purchase-orders/${orderId}/approve`);
return apiClient.post(`${this.getBaseUrl()}/purchase-orders/${orderId}/approve`);
}
async sendPurchaseOrder(orderId: string, sendEmail: boolean = true): Promise<ApiResponse<{ message: string; sent_at: string }>> {
return apiClient.post(`/purchase-orders/${orderId}/send`, { send_email: sendEmail });
return apiClient.post(`${this.getBaseUrl()}/purchase-orders/${orderId}/send`, { send_email: sendEmail });
}
async cancelPurchaseOrder(orderId: string, reason?: string): Promise<ApiResponse<PurchaseOrderResponse>> {
return apiClient.post(`/purchase-orders/${orderId}/cancel`, { reason });
return apiClient.post(`${this.getBaseUrl()}/purchase-orders/${orderId}/cancel`, { reason });
}
// Supplier management
@@ -86,42 +104,42 @@ class ProcurementService {
}
const url = queryParams.toString()
? `/suppliers?${queryParams.toString()}`
: `/suppliers`;
? `${this.getBaseUrl()}/suppliers?${queryParams.toString()}`
: `${this.getBaseUrl()}/suppliers`;
return apiClient.get(url);
}
async getSupplier(supplierId: string): Promise<ApiResponse<SupplierResponse>> {
return apiClient.get(`/suppliers/${supplierId}`);
return apiClient.get(`${this.getBaseUrl()}/suppliers/${supplierId}`);
}
async createSupplier(supplierData: SupplierCreate): Promise<ApiResponse<SupplierResponse>> {
return apiClient.post(`/suppliers`, supplierData);
return apiClient.post(`${this.getBaseUrl()}/suppliers`, supplierData);
}
async updateSupplier(supplierId: string, supplierData: SupplierUpdate): Promise<ApiResponse<SupplierResponse>> {
return apiClient.put(`/suppliers/${supplierId}`, supplierData);
return apiClient.put(`${this.getBaseUrl()}/suppliers/${supplierId}`, supplierData);
}
async deleteSupplier(supplierId: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.delete(`/suppliers/${supplierId}`);
return apiClient.delete(`${this.getBaseUrl()}/suppliers/${supplierId}`);
}
async approveSupplier(supplierId: string, approval: SupplierApproval): Promise<ApiResponse<SupplierResponse>> {
return apiClient.post(`/suppliers/${supplierId}/approve`, approval);
return apiClient.post(`${this.getBaseUrl()}/suppliers/${supplierId}/approve`, approval);
}
async getSupplierStatistics(): Promise<ApiResponse<SupplierStatistics>> {
return apiClient.get(`/suppliers/statistics`);
return apiClient.get(`${this.getBaseUrl()}/suppliers/statistics`);
}
async getActiveSuppliers(): Promise<ApiResponse<SupplierSummary[]>> {
return apiClient.get(`/suppliers/active`);
return apiClient.get(`${this.getBaseUrl()}/suppliers/active`);
}
async getTopSuppliers(limit: number = 10): Promise<ApiResponse<SupplierSummary[]>> {
return apiClient.get(`/suppliers/top?limit=${limit}`);
return apiClient.get(`${this.getBaseUrl()}/suppliers/top?limit=${limit}`);
}
@@ -146,30 +164,30 @@ class ProcurementService {
}
const url = queryParams.toString()
? `/deliveries?${queryParams.toString()}`
: `/deliveries`;
? `${this.getBaseUrl()}/deliveries?${queryParams.toString()}`
: `${this.getBaseUrl()}/deliveries`;
return apiClient.get(url);
}
async getDelivery(deliveryId: string): Promise<ApiResponse<DeliveryResponse>> {
return apiClient.get(`/deliveries/${deliveryId}`);
return apiClient.get(`${this.getBaseUrl()}/deliveries/${deliveryId}`);
}
async createDelivery(deliveryData: DeliveryCreate): Promise<ApiResponse<DeliveryResponse>> {
return apiClient.post(`/deliveries`, deliveryData);
return apiClient.post(`${this.getBaseUrl()}/deliveries`, deliveryData);
}
async updateDelivery(deliveryId: string, deliveryData: Partial<DeliveryCreate>): Promise<ApiResponse<DeliveryResponse>> {
return apiClient.put(`/deliveries/${deliveryId}`, deliveryData);
return apiClient.put(`${this.getBaseUrl()}/deliveries/${deliveryId}`, deliveryData);
}
async updateDeliveryStatus(deliveryId: string, status: DeliveryStatus, notes?: string): Promise<ApiResponse<DeliveryResponse>> {
return apiClient.put(`/deliveries/${deliveryId}/status`, { status, notes });
return apiClient.put(`${this.getBaseUrl()}/deliveries/${deliveryId}/status`, { status, notes });
}
async confirmDeliveryReceipt(deliveryId: string, confirmation: DeliveryReceiptConfirmation): Promise<ApiResponse<DeliveryResponse>> {
return apiClient.post(`/deliveries/${deliveryId}/confirm-receipt`, confirmation);
return apiClient.post(`${this.getBaseUrl()}/deliveries/${deliveryId}/confirm-receipt`, confirmation);
}

View File

@@ -7,9 +7,10 @@ import asyncio
import structlog
from fastapi import FastAPI, Request, HTTPException, Depends, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.responses import JSONResponse, StreamingResponse
import httpx
import time
import redis.asyncio as aioredis
from typing import Dict, Any
from app.core.config import settings
@@ -40,6 +41,9 @@ metrics_collector = MetricsCollector("gateway")
# Service discovery
service_discovery = ServiceDiscovery()
# Redis client for SSE streaming
redis_client = None
# CORS middleware - Add first
app.add_middleware(
CORSMiddleware,
@@ -64,8 +68,16 @@ app.include_router(nominatim.router, prefix="/api/v1/nominatim", tags=["location
@app.on_event("startup")
async def startup_event():
"""Application startup"""
global redis_client
logger.info("Starting API Gateway")
# Connect to Redis for SSE streaming
try:
redis_client = aioredis.from_url(settings.REDIS_URL)
logger.info("Connected to Redis for SSE streaming")
except Exception as e:
logger.error(f"Failed to connect to Redis: {e}")
metrics_collector.register_counter(
"gateway_auth_requests_total",
@@ -94,8 +106,14 @@ async def startup_event():
@app.on_event("shutdown")
async def shutdown_event():
"""Application shutdown"""
global redis_client
logger.info("Shutting down API Gateway")
# Close Redis connection
if redis_client:
await redis_client.close()
# Clean up service discovery
# await service_discovery.cleanup()
@@ -116,6 +134,111 @@ async def metrics():
"""Metrics endpoint for monitoring"""
return {"metrics": "enabled"}
# ================================================================
# SERVER-SENT EVENTS (SSE) ENDPOINT
# ================================================================
@app.get("/api/events")
async def events_stream(request: Request, token: str):
"""Server-Sent Events stream for real-time notifications"""
global redis_client
if not redis_client:
raise HTTPException(status_code=503, detail="SSE service unavailable")
# Extract tenant_id from JWT token (basic extraction - you might want proper JWT validation)
try:
import jwt
import base64
import json as json_lib
# Decode JWT without verification for tenant_id (in production, verify the token)
payload = jwt.decode(token, options={"verify_signature": False})
tenant_id = payload.get('tenant_id')
user_id = payload.get('user_id')
if not tenant_id:
raise HTTPException(status_code=401, detail="Invalid token: missing tenant_id")
except Exception as e:
logger.error(f"Token decode error: {e}")
raise HTTPException(status_code=401, detail="Invalid token")
logger.info(f"SSE connection established for tenant: {tenant_id}")
async def event_generator():
"""Generate server-sent events from Redis pub/sub"""
pubsub = None
try:
# Subscribe to tenant-specific alert channel
pubsub = redis_client.pubsub()
channel_name = f"alerts:{tenant_id}"
await pubsub.subscribe(channel_name)
# Send initial connection event
yield f"event: connection\n"
yield f"data: {json_lib.dumps({'type': 'connected', 'message': 'SSE connection established', 'timestamp': time.time()})}\n\n"
heartbeat_counter = 0
while True:
# Check if client has disconnected
if await request.is_disconnected():
logger.info(f"SSE client disconnected for tenant: {tenant_id}")
break
try:
# Get message from Redis with timeout
message = await asyncio.wait_for(pubsub.get_message(ignore_subscribe_messages=True), timeout=10.0)
if message and message['type'] == 'message':
# Forward the alert/notification from Redis
alert_data = json_lib.loads(message['data'])
# Determine event type based on alert data
event_type = "notification"
if alert_data.get('item_type') == 'alert':
if alert_data.get('severity') in ['high', 'urgent']:
event_type = "inventory_alert"
else:
event_type = "notification"
elif alert_data.get('item_type') == 'recommendation':
event_type = "notification"
yield f"event: {event_type}\n"
yield f"data: {json_lib.dumps(alert_data)}\n\n"
logger.debug(f"SSE message sent to tenant {tenant_id}: {alert_data.get('title')}")
except asyncio.TimeoutError:
# Send heartbeat every 10 timeouts (100 seconds)
heartbeat_counter += 1
if heartbeat_counter >= 10:
yield f"event: heartbeat\n"
yield f"data: {json_lib.dumps({'type': 'heartbeat', 'timestamp': time.time()})}\n\n"
heartbeat_counter = 0
except asyncio.CancelledError:
logger.info(f"SSE connection cancelled for tenant: {tenant_id}")
except Exception as e:
logger.error(f"SSE error for tenant {tenant_id}: {e}")
finally:
if pubsub:
await pubsub.unsubscribe()
await pubsub.close()
logger.info(f"SSE connection closed for tenant: {tenant_id}")
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Cache-Control",
}
)
# ================================================================
# WEBSOCKET ROUTING FOR TRAINING SERVICE
# ================================================================

View File

@@ -179,6 +179,32 @@ async def proxy_tenant_orders(request: Request, tenant_id: str = Path(...), path
target_path = f"/api/v1/tenants/{tenant_id}/orders/{path}".rstrip("/")
return await _proxy_to_orders_service(request, target_path, tenant_id=tenant_id)
# ================================================================
# TENANT-SCOPED SUPPLIER SERVICE ENDPOINTS
# ================================================================
@router.api_route("/{tenant_id}/suppliers/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
@router.api_route("/{tenant_id}/suppliers", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
async def proxy_tenant_suppliers(request: Request, tenant_id: str = Path(...), path: str = ""):
"""Proxy tenant supplier requests to suppliers service"""
if path:
target_path = f"/api/v1/tenants/{tenant_id}/suppliers/{path}".rstrip("/")
else:
target_path = f"/api/v1/tenants/{tenant_id}/suppliers"
return await _proxy_to_suppliers_service(request, target_path, tenant_id=tenant_id)
@router.api_route("/{tenant_id}/purchase-orders{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
async def proxy_tenant_purchase_orders(request: Request, tenant_id: str = Path(...), path: str = ""):
"""Proxy tenant purchase order requests to suppliers service"""
target_path = f"/api/v1/tenants/{tenant_id}/purchase-orders{path}".rstrip("/")
return await _proxy_to_suppliers_service(request, target_path, tenant_id=tenant_id)
@router.api_route("/{tenant_id}/deliveries{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
async def proxy_tenant_deliveries(request: Request, tenant_id: str = Path(...), path: str = ""):
"""Proxy tenant delivery requests to suppliers service"""
target_path = f"/api/v1/tenants/{tenant_id}/deliveries{path}".rstrip("/")
return await _proxy_to_suppliers_service(request, target_path, tenant_id=tenant_id)
# ================================================================
# PROXY HELPER FUNCTIONS
# ================================================================
@@ -219,6 +245,10 @@ async def _proxy_to_orders_service(request: Request, target_path: str, tenant_id
"""Proxy request to orders service"""
return await _proxy_request(request, target_path, settings.ORDERS_SERVICE_URL, tenant_id=tenant_id)
async def _proxy_to_suppliers_service(request: Request, target_path: str, tenant_id: str = None):
"""Proxy request to suppliers service"""
return await _proxy_request(request, target_path, settings.SUPPLIERS_SERVICE_URL, tenant_id=tenant_id)
async def _proxy_request(request: Request, target_path: str, service_url: str, tenant_id: str = None):
"""Generic proxy function with enhanced error handling"""
@@ -246,6 +276,15 @@ async def _proxy_request(request: Request, target_path: str, service_url: str, t
if tenant_id:
headers["X-Tenant-ID"] = tenant_id
# Add user context headers if available
if hasattr(request.state, 'user') and request.state.user:
user = request.state.user
headers["x-user-id"] = str(user.get('user_id', ''))
headers["x-user-email"] = str(user.get('email', ''))
headers["x-user-role"] = str(user.get('role', 'user'))
headers["x-user-full-name"] = str(user.get('full_name', ''))
headers["x-tenant-id"] = tenant_id or str(user.get('tenant_id', ''))
# Get request body if present
body = None
if request.method in ["POST", "PUT", "PATCH"]:

View File

@@ -5,6 +5,7 @@ redis==5.0.1
pydantic==2.5.0
pydantic-settings==2.1.0
python-jose[cryptography]==3.3.0
PyJWT==2.8.0
python-multipart==0.0.6
prometheus-client==0.17.1
python-json-logger==2.0.4

View File

@@ -22,8 +22,7 @@ from app.services.messaging import publish_weather_updated
# Import unified authentication from shared library
from shared.auth.decorators import (
get_current_user_dep,
get_current_tenant_id_dep
get_current_user_dep
)
from sqlalchemy.ext.asyncio import AsyncSession

View File

@@ -16,7 +16,6 @@ from app.schemas.forecasts import (
)
from shared.auth.decorators import (
get_current_user_dep,
get_current_tenant_id_dep,
require_admin_role
)
from shared.database.base import create_database_manager
@@ -38,22 +37,12 @@ async def create_enhanced_single_forecast(
request: ForecastRequest,
tenant_id: str = Path(..., description="Tenant ID"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
):
"""Generate a single product forecast using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj)
try:
# Enhanced tenant validation
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_forecast_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
logger.info("Generating enhanced single forecast",
tenant_id=tenant_id,
inventory_product_id=request.inventory_product_id,
@@ -106,22 +95,12 @@ async def create_enhanced_batch_forecast(
request: BatchForecastRequest,
tenant_id: str = Path(..., description="Tenant ID"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
):
"""Generate batch forecasts using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj)
try:
# Enhanced tenant validation
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_batch_forecast_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
logger.info("Generating enhanced batch forecasts",
tenant_id=tenant_id,
products_count=len(request.inventory_product_ids),
@@ -180,22 +159,12 @@ async def get_enhanced_tenant_forecasts(
skip: int = Query(0, description="Number of records to skip"),
limit: int = Query(100, description="Number of records to return"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
):
"""Get tenant forecasts with enhanced filtering using repository pattern"""
metrics = get_metrics_collector(request_obj)
try:
# Enhanced tenant validation
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_get_forecasts_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
# Record metrics
if metrics:
metrics.increment_counter("enhanced_get_forecasts_total")
@@ -250,22 +219,12 @@ async def get_enhanced_forecast_by_id(
tenant_id: str = Path(..., description="Tenant ID"),
forecast_id: str = Path(..., description="Forecast ID"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
):
"""Get specific forecast by ID using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj)
try:
# Enhanced tenant validation
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_get_forecast_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
# Record metrics
if metrics:
metrics.increment_counter("enhanced_get_forecast_by_id_total")
@@ -308,22 +267,12 @@ async def delete_enhanced_forecast(
tenant_id: str = Path(..., description="Tenant ID"),
forecast_id: str = Path(..., description="Forecast ID"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
):
"""Delete forecast using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj)
try:
# Enhanced tenant validation
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_delete_forecast_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
# Record metrics
if metrics:
metrics.increment_counter("enhanced_delete_forecast_total")
@@ -370,22 +319,12 @@ async def delete_enhanced_forecast(
async def get_enhanced_forecast_statistics(
tenant_id: str = Path(..., description="Tenant ID"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
):
"""Get comprehensive forecast statistics using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj)
try:
# Enhanced tenant validation
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_forecast_statistics_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
# Record metrics
if metrics:
metrics.increment_counter("enhanced_forecast_statistics_total")

View File

@@ -14,7 +14,6 @@ from app.services.forecasting_service import EnhancedForecastingService
from app.schemas.forecasts import ForecastRequest
from shared.auth.decorators import (
get_current_user_dep,
get_current_tenant_id_dep,
require_admin_role
)
from shared.database.base import create_database_manager
@@ -41,21 +40,12 @@ async def generate_enhanced_realtime_prediction(
prediction_request: Dict[str, Any],
tenant_id: str = Path(..., description="Tenant ID"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
prediction_service: PredictionService = Depends(get_enhanced_prediction_service)
):
"""Generate real-time prediction using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj)
try:
# Enhanced tenant validation
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_realtime_prediction_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
logger.info("Generating enhanced real-time prediction",
tenant_id=tenant_id,
@@ -127,21 +117,12 @@ async def generate_enhanced_batch_predictions(
batch_request: Dict[str, Any],
tenant_id: str = Path(..., description="Tenant ID"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
):
"""Generate batch predictions using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj)
try:
# Enhanced tenant validation
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_batch_prediction_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
logger.info("Generating enhanced batch predictions",
tenant_id=tenant_id,
@@ -209,21 +190,12 @@ async def get_enhanced_prediction_cache(
skip: int = Query(0, description="Number of records to skip"),
limit: int = Query(100, description="Number of records to return"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
):
"""Get cached predictions using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj)
try:
# Enhanced tenant validation
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_get_cache_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
# Record metrics
if metrics:
@@ -273,21 +245,12 @@ async def clear_enhanced_prediction_cache(
tenant_id: str = Path(..., description="Tenant ID"),
inventory_product_id: Optional[str] = Query(None, description="Clear cache for specific inventory product ID"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
):
"""Clear prediction cache using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj)
try:
# Enhanced tenant validation
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_clear_cache_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
# Record metrics
if metrics:
@@ -337,21 +300,12 @@ async def get_enhanced_prediction_performance(
start_date: Optional[date] = Query(None, description="Start date filter"),
end_date: Optional[date] = Query(None, description="End date filter"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
):
"""Get prediction performance metrics using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj)
try:
# Enhanced tenant validation
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_get_performance_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
# Record metrics
if metrics:
@@ -398,21 +352,12 @@ async def validate_enhanced_prediction_request(
validation_request: Dict[str, Any],
tenant_id: str = Path(..., description="Tenant ID"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
prediction_service: PredictionService = Depends(get_enhanced_prediction_service)
):
"""Validate prediction request without generating prediction"""
metrics = get_metrics_collector(request_obj)
try:
# Enhanced tenant validation
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_validate_prediction_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
# Record metrics
if metrics:

View File

@@ -11,7 +11,7 @@ from pydantic import BaseModel, Field
import structlog
from app.services.product_classifier import ProductClassifierService, get_product_classifier
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=["classification"])
logger = structlog.get_logger()
@@ -69,16 +69,11 @@ class BatchClassificationResponse(BaseModel):
async def classify_single_product(
request: ProductClassificationRequest,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
classifier: ProductClassifierService = Depends(get_product_classifier)
):
"""Classify a single product for inventory creation"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# Classify the product
suggestion = classifier.classify_product(
request.product_name,
@@ -120,16 +115,11 @@ async def classify_single_product(
async def classify_products_batch(
request: BatchClassificationRequest,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
classifier: ProductClassifierService = Depends(get_product_classifier)
):
"""Classify multiple products for onboarding automation"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
if not request.products:
raise HTTPException(status_code=400, detail="No products provided for classification")

View File

@@ -12,7 +12,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Path, status
from sqlalchemy.ext.asyncio import AsyncSession
import structlog
from shared.auth.decorators import get_current_user_dep, get_current_tenant_id_dep
from shared.auth.decorators import get_current_user_dep
from app.core.database import get_db
from app.services.inventory_service import InventoryService
from app.services.food_safety_service import FoodSafetyService
@@ -50,19 +50,12 @@ async def get_dashboard_service(db: AsyncSession = Depends(get_db)) -> Dashboard
async def get_inventory_dashboard_summary(
tenant_id: UUID = Path(...),
filters: Optional[DashboardFilter] = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Get comprehensive inventory dashboard summary"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
summary = await dashboard_service.get_inventory_dashboard_summary(db, tenant_id, filters)
logger.info("Dashboard summary retrieved",
@@ -84,19 +77,12 @@ async def get_inventory_dashboard_summary(
@router.get("/tenants/{tenant_id}/food-safety", response_model=FoodSafetyDashboard)
async def get_food_safety_dashboard(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
food_safety_service: FoodSafetyService = Depends(lambda: FoodSafetyService()),
db: AsyncSession = Depends(get_db)
):
"""Get food safety dashboard data"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
dashboard = await food_safety_service.get_food_safety_dashboard(db, tenant_id)
logger.info("Food safety dashboard retrieved",
@@ -119,19 +105,12 @@ async def get_food_safety_dashboard(
async def get_inventory_analytics(
tenant_id: UUID = Path(...),
days_back: int = Query(30, ge=1, le=365, description="Number of days to analyze"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Get advanced inventory analytics"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
analytics = await dashboard_service.get_inventory_analytics(db, tenant_id, days_back)
logger.info("Inventory analytics retrieved",
@@ -153,19 +132,12 @@ async def get_inventory_analytics(
@router.get("/tenants/{tenant_id}/business-model", response_model=BusinessModelInsights)
async def get_business_model_insights(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Get business model insights based on inventory patterns"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
insights = await dashboard_service.get_business_model_insights(db, tenant_id)
logger.info("Business model insights retrieved",
@@ -189,19 +161,12 @@ async def get_business_model_insights(
@router.get("/tenants/{tenant_id}/stock-status", response_model=List[StockStatusSummary])
async def get_stock_status_by_category(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Get stock status breakdown by category"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
stock_status = await dashboard_service.get_stock_status_by_category(db, tenant_id)
return stock_status
@@ -220,19 +185,12 @@ async def get_stock_status_by_category(
async def get_alerts_summary(
tenant_id: UUID = Path(...),
filters: Optional[AlertsFilter] = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Get alerts summary by type and severity"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
alerts_summary = await dashboard_service.get_alerts_summary(db, tenant_id, filters)
return alerts_summary
@@ -252,19 +210,12 @@ async def get_recent_activity(
tenant_id: UUID = Path(...),
limit: int = Query(20, ge=1, le=100, description="Number of activities to return"),
activity_types: Optional[List[str]] = Query(None, description="Filter by activity types"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Get recent inventory activity"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
activities = await dashboard_service.get_recent_activity(
db, tenant_id, limit, activity_types
)
@@ -286,19 +237,12 @@ async def get_recent_activity(
@router.get("/tenants/{tenant_id}/live-metrics")
async def get_live_metrics(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Get real-time inventory metrics"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
metrics = await dashboard_service.get_live_metrics(db, tenant_id)
return {
@@ -320,19 +264,12 @@ async def get_live_metrics(
@router.get("/tenants/{tenant_id}/temperature-status")
async def get_temperature_monitoring_status(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
food_safety_service: FoodSafetyService = Depends(lambda: FoodSafetyService()),
db: AsyncSession = Depends(get_db)
):
"""Get current temperature monitoring status"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
temp_status = await food_safety_service.get_temperature_monitoring_status(db, tenant_id)
return {
@@ -355,17 +292,10 @@ async def get_temperature_monitoring_status(
@router.get("/tenants/{tenant_id}/config")
async def get_dashboard_config(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep)
):
"""Get dashboard configuration and settings"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
from app.core.config import settings
config = {
@@ -411,19 +341,12 @@ async def export_dashboard_summary(
format: str = Query("json", description="Export format: json, csv, excel"),
date_from: Optional[datetime] = Query(None, description="Start date for data export"),
date_to: Optional[datetime] = Query(None, description="End date for data export"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Export dashboard summary data"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
if format.lower() not in ["json", "csv", "excel"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -460,17 +383,10 @@ async def export_dashboard_summary(
@router.get("/tenants/{tenant_id}/health")
async def get_dashboard_health(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep)
):
"""Get dashboard service health status"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
return {
"service": "inventory-dashboard",
"status": "healthy",

View File

@@ -12,7 +12,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Path, status
from sqlalchemy.ext.asyncio import AsyncSession
import structlog
from shared.auth.decorators import get_current_user_dep, get_current_tenant_id_dep
from shared.auth.decorators import get_current_user_dep
from app.core.database import get_db
from app.services.food_safety_service import FoodSafetyService
from app.schemas.food_safety import (
@@ -49,19 +49,12 @@ async def get_food_safety_service() -> FoodSafetyService:
async def create_compliance_record(
compliance_data: FoodSafetyComplianceCreate,
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
food_safety_service: FoodSafetyService = Depends(get_food_safety_service),
db: AsyncSession = Depends(get_db)
):
"""Create a new food safety compliance record"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Ensure tenant_id matches
compliance_data.tenant_id = tenant_id
@@ -99,18 +92,11 @@ async def get_compliance_records(
status_filter: Optional[str] = Query(None, description="Filter by compliance status"),
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(100, ge=1, le=1000, description="Number of records to return"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get compliance records with filtering"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Build query filters
filters = {}
if ingredient_id:
@@ -156,19 +142,12 @@ async def update_compliance_record(
compliance_data: FoodSafetyComplianceUpdate,
tenant_id: UUID = Path(...),
compliance_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
food_safety_service: FoodSafetyService = Depends(get_food_safety_service),
db: AsyncSession = Depends(get_db)
):
"""Update an existing compliance record"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
compliance = await food_safety_service.update_compliance_record(
db,
compliance_id,
@@ -206,19 +185,12 @@ async def update_compliance_record(
async def log_temperature(
temp_data: TemperatureLogCreate,
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
food_safety_service: FoodSafetyService = Depends(get_food_safety_service),
db: AsyncSession = Depends(get_db)
):
"""Log a temperature reading"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Ensure tenant_id matches
temp_data.tenant_id = tenant_id
@@ -246,19 +218,12 @@ async def log_temperature(
async def bulk_log_temperatures(
bulk_data: BulkTemperatureLogCreate,
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
food_safety_service: FoodSafetyService = Depends(get_food_safety_service),
db: AsyncSession = Depends(get_db)
):
"""Bulk log temperature readings"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Ensure tenant_id matches for all readings
for reading in bulk_data.readings:
reading.tenant_id = tenant_id
@@ -292,18 +257,11 @@ async def get_temperature_logs(
violations_only: bool = Query(False, description="Show only temperature violations"),
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(100, ge=1, le=1000, description="Number of records to return"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get temperature logs with filtering"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Build query
where_conditions = ["tenant_id = :tenant_id"]
params = {"tenant_id": tenant_id}
@@ -359,19 +317,12 @@ async def get_temperature_logs(
async def create_food_safety_alert(
alert_data: FoodSafetyAlertCreate,
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
food_safety_service: FoodSafetyService = Depends(get_food_safety_service),
db: AsyncSession = Depends(get_db)
):
"""Create a food safety alert"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Ensure tenant_id matches
alert_data.tenant_id = tenant_id
@@ -404,18 +355,11 @@ async def get_food_safety_alerts(
unresolved_only: bool = Query(True, description="Show only unresolved alerts"),
skip: int = Query(0, ge=0, description="Number of alerts to skip"),
limit: int = Query(100, ge=1, le=1000, description="Number of alerts to return"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get food safety alerts with filtering"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Build query filters
where_conditions = ["tenant_id = :tenant_id"]
params = {"tenant_id": tenant_id}
@@ -465,18 +409,11 @@ async def update_food_safety_alert(
alert_data: FoodSafetyAlertUpdate,
tenant_id: UUID = Path(...),
alert_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Update a food safety alert"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Get existing alert
alert_query = "SELECT * FROM food_safety_alerts WHERE id = :alert_id AND tenant_id = :tenant_id"
result = await db.execute(alert_query, {"alert_id": alert_id, "tenant_id": tenant_id})
@@ -538,18 +475,11 @@ async def acknowledge_alert(
tenant_id: UUID = Path(...),
alert_id: UUID = Path(...),
notes: Optional[str] = Query(None, description="Acknowledgment notes"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Acknowledge a food safety alert"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Update alert to acknowledged status
update_query = """
UPDATE food_safety_alerts
@@ -600,18 +530,11 @@ async def acknowledge_alert(
async def get_food_safety_metrics(
tenant_id: UUID = Path(...),
days_back: int = Query(30, ge=1, le=365, description="Number of days to analyze"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get food safety performance metrics"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Calculate compliance rate
compliance_query = """
SELECT
@@ -686,17 +609,10 @@ async def get_food_safety_metrics(
@router.get("/tenants/{tenant_id}/status")
async def get_food_safety_status(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep)
):
"""Get food safety service status"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
return {
"service": "food-safety",
"status": "healthy",

View File

@@ -17,7 +17,7 @@ from app.schemas.inventory import (
InventoryFilter,
PaginatedResponse
)
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=["ingredients"])
@@ -37,16 +37,11 @@ def get_current_user_id(current_user: dict = Depends(get_current_user_dep)) -> U
async def create_ingredient(
ingredient_data: IngredientCreate,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Create a new ingredient"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# Extract user ID - handle service tokens that don't have UUID user_ids
raw_user_id = current_user.get('user_id')
if current_user.get('type') == 'service':
@@ -77,7 +72,6 @@ async def create_ingredient(
async def get_ingredient(
ingredient_id: UUID,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
@@ -111,7 +105,6 @@ async def update_ingredient(
ingredient_id: UUID,
ingredient_data: IngredientUpdate,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
@@ -156,7 +149,6 @@ async def list_ingredients(
is_low_stock: Optional[bool] = Query(None, description="Filter by low stock status"),
needs_reorder: Optional[bool] = Query(None, description="Filter by reorder needed"),
search: Optional[str] = Query(None, description="Search in name, SKU, or barcode"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
@@ -196,7 +188,6 @@ async def list_ingredients(
async def delete_ingredient(
ingredient_id: UUID,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
@@ -234,7 +225,6 @@ async def get_ingredient_stock(
ingredient_id: UUID,
tenant_id: UUID = Path(..., description="Tenant ID"),
include_unavailable: bool = Query(False, description="Include unavailable stock"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):

View File

@@ -19,7 +19,6 @@ from app.services.notification_service import EnhancedNotificationService
from app.models.notifications import NotificationType as ModelNotificationType
from shared.auth.decorators import (
get_current_user_dep,
get_current_tenant_id_dep,
require_role
)
from shared.database.base import create_database_manager
@@ -37,7 +36,6 @@ def get_enhanced_notification_service():
@track_endpoint_metrics("notification_send")
async def send_notification_enhanced(
notification_data: Dict[str, Any],
tenant_id: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
@@ -86,7 +84,7 @@ async def send_notification_enhanced(
# Create notification using enhanced service
notification = await notification_service.create_notification(
tenant_id=tenant_id,
tenant_id=current_user.get("tenant_id"),
sender_id=current_user["user_id"],
notification_type=notification_type,
message=notification_data["message"],
@@ -104,7 +102,7 @@ async def send_notification_enhanced(
logger.info("Notification sent successfully",
notification_id=notification.id,
tenant_id=tenant_id,
tenant_id=current_user.get("tenant_id"),
type=notification_type.value,
priority=priority.value)
@@ -114,7 +112,7 @@ async def send_notification_enhanced(
raise
except Exception as e:
logger.error("Failed to send notification",
tenant_id=tenant_id,
tenant_id=current_user.get("tenant_id"),
sender_id=current_user["user_id"],
error=str(e))
raise HTTPException(

View File

@@ -12,7 +12,7 @@ from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
from fastapi.responses import JSONResponse
import structlog
from shared.auth.decorators import get_current_user_dep, get_current_tenant_id_dep
from shared.auth.decorators import get_current_user_dep
from app.core.database import get_db
from app.services.orders_service import OrdersService
from app.schemas.order_schemas import (
@@ -65,19 +65,12 @@ async def get_orders_service(db = Depends(get_db)) -> OrdersService:
@router.get("/tenants/{tenant_id}/orders/dashboard-summary", response_model=OrdersDashboardSummary)
async def get_dashboard_summary(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get comprehensive dashboard summary for orders"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
summary = await orders_service.get_dashboard_summary(db, tenant_id)
logger.info("Dashboard summary retrieved",
@@ -100,19 +93,12 @@ async def get_dashboard_summary(
async def get_demand_requirements(
tenant_id: UUID = Path(...),
target_date: date = Query(..., description="Date for demand analysis"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get demand requirements for production planning"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
requirements = await orders_service.get_demand_requirements(db, tenant_id, target_date)
logger.info("Demand requirements calculated",
@@ -138,19 +124,12 @@ async def get_demand_requirements(
async def create_order(
order_data: OrderCreate,
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Create a new customer order"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Ensure tenant_id matches
order_data.tenant_id = tenant_id
@@ -184,19 +163,12 @@ async def create_order(
async def get_order(
tenant_id: UUID = Path(...),
order_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get order details with items"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
order = await orders_service.get_order_with_items(db, order_id, tenant_id)
if not order:
raise HTTPException(
@@ -226,19 +198,12 @@ async def get_orders(
end_date: Optional[date] = Query(None, description="End date for date range filter"),
skip: int = Query(0, ge=0, description="Number of orders to skip"),
limit: int = Query(100, ge=1, le=1000, description="Number of orders to return"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get orders with filtering and pagination"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Determine which repository method to use based on filters
if status_filter:
orders = await orders_service.order_repo.get_orders_by_status(
@@ -269,19 +234,12 @@ async def update_order_status(
tenant_id: UUID = Path(...),
order_id: UUID = Path(...),
reason: Optional[str] = Query(None, description="Reason for status change"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Update order status"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Validate status
valid_statuses = ["pending", "confirmed", "in_production", "ready", "out_for_delivery", "delivered", "cancelled", "failed"]
if new_status not in valid_statuses:
@@ -329,19 +287,12 @@ async def update_order_status(
async def create_customer(
customer_data: CustomerCreate,
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Create a new customer"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# Ensure tenant_id matches
customer_data.tenant_id = tenant_id
@@ -383,19 +334,12 @@ async def get_customers(
active_only: bool = Query(True, description="Filter for active customers only"),
skip: int = Query(0, ge=0, description="Number of customers to skip"),
limit: int = Query(100, ge=1, le=1000, description="Number of customers to return"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get customers with filtering and pagination"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
if active_only:
customers = await orders_service.customer_repo.get_active_customers(
db, tenant_id, skip, limit
@@ -419,19 +363,12 @@ async def get_customers(
async def get_customer(
tenant_id: UUID = Path(...),
customer_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get customer details"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
customer = await orders_service.customer_repo.get(db, customer_id, tenant_id)
if not customer:
raise HTTPException(
@@ -458,19 +395,12 @@ async def get_customer(
@router.get("/tenants/{tenant_id}/orders/business-model")
async def detect_business_model(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Detect business model based on order patterns"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
business_model = await orders_service.detect_business_model(db, tenant_id)
return {
@@ -492,17 +422,10 @@ async def detect_business_model(
@router.get("/tenants/{tenant_id}/orders/status")
async def get_service_status(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep)
):
"""Get orders service status"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
return {
"service": "orders-service",
"status": "healthy",

View File

@@ -9,7 +9,7 @@ from uuid import UUID
import structlog
from app.core.database import get_db
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=["pos-config"])
logger = structlog.get_logger()
@@ -20,14 +20,10 @@ async def get_pos_configurations(
tenant_id: UUID = Path(..., description="Tenant ID"),
pos_system: Optional[str] = Query(None, description="Filter by POS system"),
is_active: Optional[bool] = Query(None, description="Filter by active status"),
current_tenant: str = Depends(get_current_tenant_id_dep),
db=Depends(get_db)
):
"""Get POS configurations for a tenant"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# TODO: Implement configuration retrieval
# This is a placeholder for the basic structure
@@ -46,15 +42,11 @@ async def get_pos_configurations(
async def create_pos_configuration(
configuration_data: Dict[str, Any],
tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Create a new POS configuration"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# TODO: Implement configuration creation
logger.info("Creating POS configuration",
@@ -73,14 +65,10 @@ async def create_pos_configuration(
async def get_pos_configuration(
tenant_id: UUID = Path(..., description="Tenant ID"),
config_id: UUID = Path(..., description="Configuration ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
db=Depends(get_db)
):
"""Get a specific POS configuration"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# TODO: Implement configuration retrieval
return {"message": "Configuration details", "id": str(config_id)}
@@ -96,14 +84,10 @@ async def update_pos_configuration(
configuration_data: Dict[str, Any],
tenant_id: UUID = Path(..., description="Tenant ID"),
config_id: UUID = Path(..., description="Configuration ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
db=Depends(get_db)
):
"""Update a POS configuration"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# TODO: Implement configuration update
return {"message": "Configuration updated successfully"}
@@ -118,14 +102,10 @@ async def update_pos_configuration(
async def delete_pos_configuration(
tenant_id: UUID = Path(..., description="Tenant ID"),
config_id: UUID = Path(..., description="Configuration ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
db=Depends(get_db)
):
"""Delete a POS configuration"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# TODO: Implement configuration deletion
return {"message": "Configuration deleted successfully"}
@@ -140,14 +120,10 @@ async def delete_pos_configuration(
async def test_pos_connection(
tenant_id: UUID = Path(..., description="Tenant ID"),
config_id: UUID = Path(..., description="Configuration ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
db=Depends(get_db)
):
"""Test connection to POS system"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# TODO: Implement connection testing
return {

View File

@@ -11,7 +11,7 @@ from datetime import datetime
import structlog
from app.core.database import get_db
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=["sync"])
logger = structlog.get_logger()
@@ -22,15 +22,11 @@ async def trigger_sync(
sync_request: Dict[str, Any] = Body(...),
tenant_id: UUID = Path(..., description="Tenant ID"),
config_id: UUID = Path(..., description="Configuration ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Trigger manual synchronization with POS system"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
sync_type = sync_request.get("sync_type", "incremental") # full, incremental
data_types = sync_request.get("data_types", ["transactions"]) # transactions, products, customers
@@ -68,14 +64,10 @@ async def get_sync_status(
tenant_id: UUID = Path(..., description="Tenant ID"),
config_id: UUID = Path(..., description="Configuration ID"),
limit: int = Query(10, ge=1, le=100, description="Number of sync logs to return"),
current_tenant: str = Depends(get_current_tenant_id_dep),
db=Depends(get_db)
):
"""Get synchronization status and recent sync history"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# TODO: Get sync status from database
# TODO: Get recent sync logs
@@ -107,14 +99,10 @@ async def get_sync_logs(
status: Optional[str] = Query(None, description="Filter by sync status"),
sync_type: Optional[str] = Query(None, description="Filter by sync type"),
data_type: Optional[str] = Query(None, description="Filter by data type"),
current_tenant: str = Depends(get_current_tenant_id_dep),
db=Depends(get_db)
):
"""Get detailed sync logs"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# TODO: Implement log retrieval with filters
@@ -140,14 +128,10 @@ async def get_pos_transactions(
is_synced: Optional[bool] = Query(None, description="Filter by sync status"),
limit: int = Query(50, ge=1, le=200, description="Number of transactions to return"),
offset: int = Query(0, ge=0, description="Number of transactions to skip"),
current_tenant: str = Depends(get_current_tenant_id_dep),
db=Depends(get_db)
):
"""Get POS transactions for a tenant"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# TODO: Implement transaction retrieval with filters
@@ -176,14 +160,10 @@ async def sync_single_transaction(
tenant_id: UUID = Path(..., description="Tenant ID"),
transaction_id: UUID = Path(..., description="Transaction ID"),
force: bool = Query(False, description="Force sync even if already synced"),
current_tenant: str = Depends(get_current_tenant_id_dep),
db=Depends(get_db)
):
"""Manually sync a single transaction to sales service"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# TODO: Implement single transaction sync
@@ -204,14 +184,10 @@ async def sync_single_transaction(
async def get_sync_analytics(
tenant_id: UUID = Path(..., description="Tenant ID"),
days: int = Query(30, ge=1, le=365, description="Number of days to analyze"),
current_tenant: str = Depends(get_current_tenant_id_dep),
db=Depends(get_db)
):
"""Get sync performance analytics"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# TODO: Implement analytics calculation
@@ -244,15 +220,11 @@ async def get_sync_analytics(
async def resync_failed_transactions(
tenant_id: UUID = Path(..., description="Tenant ID"),
days_back: int = Query(7, ge=1, le=90, description="How many days back to resync"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Resync failed transactions from the specified time period"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
logger.info("Resync failed transactions requested",
tenant_id=tenant_id,

View File

@@ -7,11 +7,11 @@ Production API endpoints
from fastapi import APIRouter, Depends, HTTPException, Path, Query
from typing import Optional, List
from datetime import date, datetime
from datetime import date, datetime, timedelta
from uuid import UUID
import structlog
from shared.auth.decorators import get_current_user_dep, get_current_tenant_id_dep
from shared.auth.decorators import get_current_user_dep
from app.core.database import get_db
from app.services.production_service import ProductionService
from app.schemas.production import (
@@ -41,16 +41,11 @@ def get_production_service() -> ProductionService:
@router.get("/tenants/{tenant_id}/production/dashboard-summary", response_model=ProductionDashboardSummary)
async def get_dashboard_summary(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Get production dashboard summary using shared auth"""
try:
# Verify tenant access using shared auth pattern
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
summary = await production_service.get_dashboard_summary(tenant_id)
logger.info("Retrieved production dashboard summary",
@@ -68,7 +63,6 @@ async def get_dashboard_summary(
async def get_daily_requirements(
tenant_id: UUID = Path(...),
date: Optional[date] = Query(None, description="Target date for production requirements"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
@@ -95,7 +89,6 @@ async def get_daily_requirements(
async def get_production_requirements(
tenant_id: UUID = Path(...),
date: Optional[date] = Query(None, description="Target date for production requirements"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
@@ -126,7 +119,6 @@ async def get_production_requirements(
async def create_production_batch(
batch_data: ProductionBatchCreate,
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
@@ -154,7 +146,6 @@ async def create_production_batch(
@router.get("/tenants/{tenant_id}/production/batches/active", response_model=ProductionBatchListResponse)
async def get_active_batches(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
@@ -189,7 +180,6 @@ async def get_active_batches(
async def get_batch_details(
tenant_id: UUID = Path(...),
batch_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
@@ -223,7 +213,6 @@ async def update_batch_status(
status_update: ProductionBatchStatusUpdate,
tenant_id: UUID = Path(...),
batch_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
@@ -259,7 +248,6 @@ async def get_production_schedule(
tenant_id: UUID = Path(...),
start_date: Optional[date] = Query(None, description="Start date for schedule"),
end_date: Optional[date] = Query(None, description="End date for schedule"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
@@ -323,7 +311,6 @@ async def get_production_schedule(
async def get_capacity_status(
tenant_id: UUID = Path(...),
date: Optional[date] = Query(None, description="Date for capacity status"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
@@ -361,7 +348,6 @@ async def get_yield_metrics(
tenant_id: UUID = Path(...),
start_date: date = Query(..., description="Start date for metrics"),
end_date: date = Query(..., description="End date for metrics"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):

View File

@@ -10,7 +10,7 @@ import structlog
import json
from app.services.data_import_service import DataImportService
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=["data-import"])
logger = structlog.get_logger()
@@ -26,14 +26,10 @@ async def validate_json_data(
tenant_id: UUID = Path(..., description="Tenant ID"),
data: Dict[str, Any] = None,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
current_tenant: str = Depends(get_current_tenant_id_dep),
import_service: DataImportService = Depends(get_import_service)
):
"""Validate JSON sales data"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
if not data:
raise HTTPException(status_code=400, detail="No data provided")
@@ -81,18 +77,21 @@ async def validate_sales_data_universal(
data: Optional[Dict[str, Any]] = None,
file_format: Optional[str] = Form(None),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
current_tenant: str = Depends(get_current_tenant_id_dep),
import_service: DataImportService = Depends(get_import_service)
):
"""Universal validation endpoint for sales data - supports files and JSON"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# Debug logging at the start
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:
logger.info("Validating uploaded file", tenant_id=tenant_id, filename=file.filename)
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()
@@ -125,7 +124,7 @@ async def validate_sales_data_universal(
# Handle JSON data validation
elif data:
logger.info("Validating JSON data", tenant_id=tenant_id)
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)
@@ -133,10 +132,13 @@ async def validate_sales_data_universal(
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,
@@ -161,8 +163,9 @@ async def validate_sales_data_universal(
}
except Exception as e:
logger.error("Failed to validate sales data", error=str(e), tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to validate data: {str(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")
@@ -170,7 +173,6 @@ async def validate_csv_data_legacy(
tenant_id: UUID = Path(..., description="Tenant ID"),
file: UploadFile = File(...),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
current_tenant: str = Depends(get_current_tenant_id_dep),
import_service: DataImportService = Depends(get_import_service)
):
"""Legacy CSV validation endpoint - redirects to universal validator"""
@@ -178,7 +180,6 @@ async def validate_csv_data_legacy(
tenant_id=tenant_id,
file=file,
current_user=current_user,
current_tenant=current_tenant,
import_service=import_service
)
@@ -191,14 +192,10 @@ async def import_sales_data(
file_format: Optional[str] = Form(None),
update_existing: bool = Form(False, description="Whether to update existing records"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
current_tenant: str = Depends(get_current_tenant_id_dep),
import_service: DataImportService = Depends(get_import_service)
):
"""Enhanced import sales data - supports multiple file formats and JSON"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# Handle file upload (form data)
if file:
@@ -302,14 +299,10 @@ async def import_csv_data(
file: UploadFile = File(...),
update_existing: bool = Form(False, description="Whether to update existing records"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
current_tenant: str = Depends(get_current_tenant_id_dep),
import_service: DataImportService = Depends(get_import_service)
):
"""Import CSV sales data file"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
if not file.filename.endswith('.csv'):
raise HTTPException(status_code=400, detail="File must be a CSV file")
@@ -354,14 +347,10 @@ async def import_csv_data(
@router.get("/tenants/{tenant_id}/sales/import/template")
async def get_import_template(
tenant_id: UUID = Path(..., description="Tenant ID"),
format: str = "csv",
current_tenant: str = Depends(get_current_tenant_id_dep)
format: str = "csv"
):
"""Get sales data import template"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
if format not in ["csv", "json"]:
raise HTTPException(status_code=400, detail="Format must be 'csv' or 'json'")

View File

@@ -16,7 +16,7 @@ from app.schemas.sales import (
SalesDataQuery
)
from app.services.sales_service import SalesService
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()
@@ -32,15 +32,10 @@ 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),
current_tenant: str = Depends(get_current_tenant_id_dep),
sales_service: SalesService = Depends(get_sales_service)
):
"""Create a new sales record"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
logger.info(
"Creating sales record",
product=sales_data.product_name,
@@ -82,15 +77,10 @@ async def get_sales_records(
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)"),
current_tenant: str = Depends(get_current_tenant_id_dep),
sales_service: SalesService = Depends(get_sales_service)
):
"""Get sales records for a tenant with filtering and pagination"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# Build query parameters
query_params = SalesDataQuery(
start_date=start_date,
@@ -124,15 +114,10 @@ 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"),
current_tenant: str = Depends(get_current_tenant_id_dep),
sales_service: SalesService = Depends(get_sales_service)
):
"""Get sales analytics summary for a tenant"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
analytics = await sales_service.get_sales_analytics(tenant_id, start_date, end_date)
logger.info("Retrieved sales analytics", tenant_id=tenant_id)
@@ -149,15 +134,10 @@ async def get_product_sales(
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"),
current_tenant: str = Depends(get_current_tenant_id_dep),
sales_service: SalesService = Depends(get_sales_service)
):
"""Get sales records for a specific product"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
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)
@@ -171,15 +151,10 @@ async def get_product_sales(
@router.get("/tenants/{tenant_id}/sales/categories", response_model=List[str])
async def get_product_categories(
tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
sales_service: SalesService = Depends(get_sales_service)
):
"""Get distinct product categories from sales data"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
categories = await sales_service.get_product_categories(tenant_id)
return categories
@@ -197,15 +172,10 @@ async def get_product_categories(
async def get_sales_record(
tenant_id: UUID = Path(..., description="Tenant ID"),
record_id: UUID = Path(..., description="Sales record ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
sales_service: SalesService = Depends(get_sales_service)
):
"""Get a specific sales record"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
record = await sales_service.get_sales_record(record_id, tenant_id)
if not record:
@@ -225,15 +195,10 @@ async def update_sales_record(
update_data: SalesDataUpdate,
tenant_id: UUID = Path(..., description="Tenant ID"),
record_id: UUID = Path(..., description="Sales record ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
sales_service: SalesService = Depends(get_sales_service)
):
"""Update a sales record"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
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)
@@ -251,15 +216,10 @@ async def update_sales_record(
async def delete_sales_record(
tenant_id: UUID = Path(..., description="Tenant ID"),
record_id: UUID = Path(..., description="Sales record ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
sales_service: SalesService = Depends(get_sales_service)
):
"""Delete a sales record"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
success = await sales_service.delete_sales_record(record_id, tenant_id)
if not success:
@@ -281,15 +241,10 @@ 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"),
current_tenant: str = Depends(get_current_tenant_id_dep),
sales_service: SalesService = Depends(get_sales_service)
):
"""Mark a sales record as validated"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
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)

View File

@@ -57,7 +57,7 @@ class DataImportService:
"""Enhanced data import service using repository pattern with STRICT validation for production"""
# PRODUCTION VALIDATION CONFIGURATION
STRICT_VALIDATION = True # Set to False for lenient validation, True for production quality
STRICT_VALIDATION = False # Set to False for lenient validation, True for production quality
MAX_QUANTITY_PER_DAY = 10000 # Maximum reasonable quantity per product per day
MAX_REVENUE_PER_ITEM = 100000 # Maximum reasonable revenue per line item
MAX_UNIT_PRICE = 10000 # Maximum reasonable price per unit for bakery items

View File

@@ -12,7 +12,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Path, status
from sqlalchemy.ext.asyncio import AsyncSession
import structlog
from shared.auth.decorators import get_current_user_dep, get_current_tenant_id_dep
from shared.auth.decorators import get_current_user_dep
from app.core.database import get_db
from app.services.performance_service import PerformanceTrackingService, AlertService
from app.services.dashboard_service import DashboardService
@@ -54,19 +54,12 @@ async def calculate_supplier_performance(
period: PerformancePeriod = Query(...),
period_start: datetime = Query(...),
period_end: datetime = Query(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
performance_service: PerformanceTrackingService = Depends(get_performance_service),
db: AsyncSession = Depends(get_db)
):
"""Calculate performance metrics for a supplier"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
metric = await performance_service.calculate_supplier_performance(
db, supplier_id, tenant_id, period, period_start, period_end
)
@@ -104,18 +97,11 @@ async def get_supplier_performance_metrics(
date_from: Optional[datetime] = Query(None),
date_to: Optional[datetime] = Query(None),
limit: int = Query(50, ge=1, le=500),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get performance metrics for a supplier"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# TODO: Implement get_supplier_performance_metrics in service
# For now, return empty list
metrics = []
@@ -139,19 +125,12 @@ async def get_supplier_performance_metrics(
async def evaluate_performance_alerts(
tenant_id: UUID = Path(...),
supplier_id: Optional[UUID] = Query(None, description="Specific supplier to evaluate"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
alert_service: AlertService = Depends(get_alert_service),
db: AsyncSession = Depends(get_db)
):
"""Evaluate and create performance-based alerts"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
alerts = await alert_service.evaluate_performance_alerts(db, tenant_id, supplier_id)
logger.info("Performance alerts evaluated",
@@ -179,18 +158,11 @@ async def get_supplier_alerts(
date_from: Optional[datetime] = Query(None),
date_to: Optional[datetime] = Query(None),
limit: int = Query(50, ge=1, le=500),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get supplier alerts with filtering"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# TODO: Implement get_supplier_alerts in service
# For now, return empty list
alerts = []
@@ -212,18 +184,11 @@ async def update_alert(
alert_update: AlertUpdate,
tenant_id: UUID = Path(...),
alert_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Update an alert (acknowledge, resolve, etc.)"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# TODO: Implement update_alert in service
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
@@ -248,19 +213,12 @@ async def get_performance_dashboard_summary(
tenant_id: UUID = Path(...),
date_from: Optional[datetime] = Query(None),
date_to: Optional[datetime] = Query(None),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Get comprehensive performance dashboard summary"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
summary = await dashboard_service.get_performance_dashboard_summary(
db, tenant_id, date_from, date_to
)
@@ -285,19 +243,12 @@ async def get_supplier_performance_insights(
tenant_id: UUID = Path(...),
supplier_id: UUID = Path(...),
days_back: int = Query(30, ge=1, le=365),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Get detailed performance insights for a specific supplier"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
insights = await dashboard_service.get_supplier_performance_insights(
db, tenant_id, supplier_id, days_back
)
@@ -323,19 +274,12 @@ async def get_supplier_performance_insights(
async def get_performance_analytics(
tenant_id: UUID = Path(...),
period_days: int = Query(90, ge=1, le=365),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Get advanced performance analytics"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
analytics = await dashboard_service.get_performance_analytics(
db, tenant_id, period_days
)
@@ -359,19 +303,12 @@ async def get_performance_analytics(
@router.get("/tenants/{tenant_id}/business-model", response_model=BusinessModelInsights)
async def get_business_model_insights(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Get business model detection and insights"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
insights = await dashboard_service.get_business_model_insights(db, tenant_id)
logger.info("Business model insights retrieved",
@@ -395,19 +332,12 @@ async def get_alert_summary(
tenant_id: UUID = Path(...),
date_from: Optional[datetime] = Query(None),
date_to: Optional[datetime] = Query(None),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Get alert summary by type and severity"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
summary = await dashboard_service.get_alert_summary(db, tenant_id, date_from, date_to)
return summary
@@ -428,18 +358,11 @@ async def get_alert_summary(
async def generate_performance_report(
report_request: PerformanceReportRequest,
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Generate a performance report"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# TODO: Implement report generation
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
@@ -463,18 +386,11 @@ async def export_performance_data(
date_from: Optional[datetime] = Query(None),
date_to: Optional[datetime] = Query(None),
supplier_ids: Optional[List[UUID]] = Query(None),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Export performance data"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
if format.lower() not in ["json", "csv", "excel"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -502,17 +418,10 @@ async def export_performance_data(
@router.get("/tenants/{tenant_id}/config")
async def get_performance_config(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep)
):
"""Get performance tracking configuration"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
from app.core.config import settings
config = {
@@ -565,17 +474,10 @@ async def get_performance_config(
@router.get("/tenants/{tenant_id}/health")
async def get_performance_health(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep)
):
"""Get performance service health status"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
return {
"service": "suppliers-performance",
"status": "healthy",

View File

@@ -3,7 +3,7 @@
Supplier API endpoints
"""
from fastapi import APIRouter, Depends, HTTPException, Query, Path
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Request
from typing import List, Optional
from uuid import UUID
import structlog
@@ -18,23 +18,22 @@ from app.schemas.suppliers import (
from shared.auth.decorators import get_current_user_dep
from typing import Dict, Any
router = APIRouter(prefix="/suppliers", tags=["suppliers"])
router = APIRouter(prefix="/tenants/{tenant_id}/suppliers", tags=["suppliers"])
logger = structlog.get_logger()
@router.post("", response_model=SupplierResponse)
@router.post("/", response_model=SupplierResponse)
async def create_supplier(
supplier_data: SupplierCreate,
tenant_id: str = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Create a new supplier"""
# require_permissions(current_user, ["suppliers:create"])
try:
service = SupplierService(db)
supplier = await service.create_supplier(
tenant_id=current_user.tenant_id,
tenant_id=UUID(tenant_id),
supplier_data=supplier_data,
created_by=current_user.user_id
)
@@ -46,14 +45,15 @@ async def create_supplier(
raise HTTPException(status_code=500, detail="Failed to create supplier")
@router.get("", response_model=List[SupplierSummary])
@router.get("/", response_model=List[SupplierSummary])
async def list_suppliers(
tenant_id: str = Path(..., description="Tenant ID"),
search_term: Optional[str] = Query(None, description="Search term"),
supplier_type: Optional[str] = Query(None, description="Supplier type filter"),
status: Optional[str] = Query(None, description="Status filter"),
limit: int = Query(50, ge=1, le=1000, description="Number of results to return"),
offset: int = Query(0, ge=0, description="Number of results to skip"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""List suppliers with optional filters"""
@@ -69,7 +69,7 @@ async def list_suppliers(
offset=offset
)
suppliers = await service.search_suppliers(
tenant_id=current_user.tenant_id,
tenant_id=UUID(tenant_id),
search_params=search_params
)
return [SupplierSummary.from_orm(supplier) for supplier in suppliers]
@@ -80,7 +80,7 @@ async def list_suppliers(
@router.get("/statistics", response_model=SupplierStatistics)
async def get_supplier_statistics(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_id: str = Path(..., description="Tenant ID"),
db: Session = Depends(get_db)
):
"""Get supplier statistics for dashboard"""
@@ -88,7 +88,7 @@ async def get_supplier_statistics(
try:
service = SupplierService(db)
stats = await service.get_supplier_statistics(current_user.tenant_id)
stats = await service.get_supplier_statistics(UUID(tenant_id))
return SupplierStatistics(**stats)
except Exception as e:
logger.error("Error getting supplier statistics", error=str(e))
@@ -97,7 +97,7 @@ async def get_supplier_statistics(
@router.get("/active", response_model=List[SupplierSummary])
async def get_active_suppliers(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_id: str = Path(..., description="Tenant ID"),
db: Session = Depends(get_db)
):
"""Get all active suppliers"""
@@ -105,7 +105,7 @@ async def get_active_suppliers(
try:
service = SupplierService(db)
suppliers = await service.get_active_suppliers(current_user.tenant_id)
suppliers = await service.get_active_suppliers(UUID(tenant_id))
return [SupplierSummary.from_orm(supplier) for supplier in suppliers]
except Exception as e:
logger.error("Error getting active suppliers", error=str(e))
@@ -114,8 +114,8 @@ async def get_active_suppliers(
@router.get("/top", response_model=List[SupplierSummary])
async def get_top_suppliers(
tenant_id: str = Path(..., description="Tenant ID"),
limit: int = Query(10, ge=1, le=50, description="Number of top suppliers to return"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get top performing suppliers"""
@@ -123,7 +123,7 @@ async def get_top_suppliers(
try:
service = SupplierService(db)
suppliers = await service.get_top_suppliers(current_user.tenant_id, limit)
suppliers = await service.get_top_suppliers(UUID(tenant_id), limit)
return [SupplierSummary.from_orm(supplier) for supplier in suppliers]
except Exception as e:
logger.error("Error getting top suppliers", error=str(e))
@@ -132,8 +132,8 @@ async def get_top_suppliers(
@router.get("/pending-review", response_model=List[SupplierSummary])
async def get_suppliers_needing_review(
tenant_id: str = Path(..., description="Tenant ID"),
days_since_last_order: int = Query(30, ge=1, le=365, description="Days since last order"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get suppliers that may need performance review"""
@@ -142,7 +142,7 @@ async def get_suppliers_needing_review(
try:
service = SupplierService(db)
suppliers = await service.get_suppliers_needing_review(
current_user.tenant_id, days_since_last_order
UUID(tenant_id), days_since_last_order
)
return [SupplierSummary.from_orm(supplier) for supplier in suppliers]
except Exception as e:
@@ -153,7 +153,7 @@ async def get_suppliers_needing_review(
@router.get("/{supplier_id}", response_model=SupplierResponse)
async def get_supplier(
supplier_id: UUID = Path(..., description="Supplier ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_id: str = Path(..., description="Tenant ID"),
db: Session = Depends(get_db)
):
"""Get supplier by ID"""
@@ -166,10 +166,6 @@ async def get_supplier(
if not supplier:
raise HTTPException(status_code=404, detail="Supplier not found")
# Check tenant access
if supplier.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
return SupplierResponse.from_orm(supplier)
except HTTPException:
raise
@@ -191,12 +187,10 @@ async def update_supplier(
try:
service = SupplierService(db)
# Check supplier exists and belongs to tenant
# Check supplier exists
existing_supplier = await service.get_supplier(supplier_id)
if not existing_supplier:
raise HTTPException(status_code=404, detail="Supplier not found")
if existing_supplier.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
supplier = await service.update_supplier(
supplier_id=supplier_id,
@@ -220,7 +214,6 @@ async def update_supplier(
@router.delete("/{supplier_id}")
async def delete_supplier(
supplier_id: UUID = Path(..., description="Supplier ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Delete supplier (soft delete)"""
@@ -229,12 +222,10 @@ async def delete_supplier(
try:
service = SupplierService(db)
# Check supplier exists and belongs to tenant
# Check supplier exists
existing_supplier = await service.get_supplier(supplier_id)
if not existing_supplier:
raise HTTPException(status_code=404, detail="Supplier not found")
if existing_supplier.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
success = await service.delete_supplier(supplier_id)
if not success:
@@ -261,12 +252,10 @@ async def approve_supplier(
try:
service = SupplierService(db)
# Check supplier exists and belongs to tenant
# Check supplier exists
existing_supplier = await service.get_supplier(supplier_id)
if not existing_supplier:
raise HTTPException(status_code=404, detail="Supplier not found")
if existing_supplier.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
if approval_data.action == "approve":
supplier = await service.approve_supplier(
@@ -299,7 +288,7 @@ async def approve_supplier(
@router.get("/types/{supplier_type}", response_model=List[SupplierSummary])
async def get_suppliers_by_type(
supplier_type: str = Path(..., description="Supplier type"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_id: str = Path(..., description="Tenant ID"),
db: Session = Depends(get_db)
):
"""Get suppliers by type"""
@@ -315,7 +304,7 @@ async def get_suppliers_by_type(
raise HTTPException(status_code=400, detail="Invalid supplier type")
service = SupplierService(db)
suppliers = await service.get_suppliers_by_type(current_user.tenant_id, type_enum)
suppliers = await service.get_suppliers_by_type(UUID(tenant_id), type_enum)
return [SupplierSummary.from_orm(supplier) for supplier in suppliers]
except HTTPException:
raise

View File

@@ -27,7 +27,7 @@ from app.services.messaging import (
publish_job_started
)
from shared.auth.decorators import require_admin_role, get_current_user_dep, get_current_tenant_id_dep
from shared.auth.decorators import require_admin_role, get_current_user_dep
from shared.database.base import create_database_manager
from shared.monitoring.decorators import track_execution_time
from shared.monitoring.metrics import get_metrics_collector
@@ -48,7 +48,6 @@ async def start_enhanced_training_job(
tenant_id: str = Path(..., description="Tenant ID"),
background_tasks: BackgroundTasks = BackgroundTasks(),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_training_service: EnhancedTrainingService = Depends(get_enhanced_training_service)
):
"""
@@ -69,15 +68,6 @@ async def start_enhanced_training_job(
metrics = get_metrics_collector(request_obj)
try:
# Enhanced tenant validation
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_training_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
# Generate enhanced job ID
job_id = f"enhanced_training_{tenant_id}_{uuid.uuid4().hex[:8]}"
@@ -310,7 +300,6 @@ async def start_enhanced_single_product_training(
tenant_id: str = Path(..., description="Tenant ID"),
inventory_product_id: str = Path(..., description="Inventory product UUID"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_training_service: EnhancedTrainingService = Depends(get_enhanced_training_service)
):
"""
@@ -325,14 +314,6 @@ async def start_enhanced_single_product_training(
metrics = get_metrics_collector(request_obj)
try:
# Enhanced tenant validation
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_single_product_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
logger.info("Starting enhanced single product training",
inventory_product_id=inventory_product_id,
@@ -390,7 +371,6 @@ async def get_enhanced_training_job_status(
tenant_id: str = Path(..., description="Tenant ID"),
job_id: str = Path(..., description="Job ID"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_training_service: EnhancedTrainingService = Depends(get_enhanced_training_service)
):
"""
@@ -399,14 +379,6 @@ async def get_enhanced_training_job_status(
metrics = get_metrics_collector(request_obj)
try:
# Validate tenant access
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_status_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
# Get status using enhanced service
status_info = await enhanced_training_service.get_training_status(job_id)
@@ -448,7 +420,6 @@ async def get_enhanced_tenant_models(
skip: int = Query(0, description="Number of models to skip"),
limit: int = Query(100, description="Number of models to return"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_training_service: EnhancedTrainingService = Depends(get_enhanced_training_service)
):
"""
@@ -457,14 +428,6 @@ async def get_enhanced_tenant_models(
metrics = get_metrics_collector(request_obj)
try:
# Validate tenant access
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_models_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
# Get models using enhanced service
models = await enhanced_training_service.get_tenant_models(
@@ -508,7 +471,6 @@ async def get_enhanced_model_performance(
tenant_id: str = Path(..., description="Tenant ID"),
model_id: str = Path(..., description="Model ID"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_training_service: EnhancedTrainingService = Depends(get_enhanced_training_service)
):
"""
@@ -517,15 +479,6 @@ async def get_enhanced_model_performance(
metrics = get_metrics_collector(request_obj)
try:
# Validate tenant access
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_performance_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
# Get performance using enhanced service
performance = await enhanced_training_service.get_model_performance(model_id)
@@ -563,7 +516,6 @@ async def get_enhanced_model_performance(
async def get_enhanced_tenant_statistics(
tenant_id: str = Path(..., description="Tenant ID"),
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_training_service: EnhancedTrainingService = Depends(get_enhanced_training_service)
):
"""
@@ -572,14 +524,6 @@ async def get_enhanced_tenant_statistics(
metrics = get_metrics_collector(request_obj)
try:
# Validate tenant access
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_statistics_access_denied_total")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant resources"
)
# Get statistics using enhanced service
statistics = await enhanced_training_service.get_tenant_statistics(tenant_id)

View File

@@ -15,8 +15,7 @@ logger = structlog.get_logger(__name__)
from app.services.messaging import training_publisher
from shared.auth.decorators import (
get_current_user_dep,
get_current_tenant_id_dep
get_current_user_dep
)
# Create WebSocket router

View File

@@ -125,6 +125,7 @@ class BaseServiceSettings(BaseSettings):
NOTIFICATION_SERVICE_URL: str = os.getenv("NOTIFICATION_SERVICE_URL", "http://notification-service:8000")
PRODUCTION_SERVICE_URL: str = os.getenv("PRODUCTION_SERVICE_URL", "http://bakery-production-service:8000")
ORDERS_SERVICE_URL: str = os.getenv("ORDERS_SERVICE_URL", "http://bakery-orders-service:8000")
SUPPLIERS_SERVICE_URL: str = os.getenv("SUPPLIERS_SERVICE_URL", "http://bakery-suppliers-service:8000")
NOMINATIM_SERVICE_URL: str = os.getenv("NOMINATIM_SERVICE_URL", "http://nominatim:8080")
# HTTP Client Settings
@@ -338,6 +339,7 @@ class BaseServiceSettings(BaseSettings):
"notification": self.NOTIFICATION_SERVICE_URL,
"production": self.PRODUCTION_SERVICE_URL,
"orders": self.ORDERS_SERVICE_URL,
"suppliers": self.SUPPLIERS_SERVICE_URL,
}
@property