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 ( return (
<ErrorBoundary> <ErrorBoundary>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<BrowserRouter> <BrowserRouter
future={{
v7_startTransition: true,
v7_relativeSplatPath: true,
}}
>
<ThemeProvider> <ThemeProvider>
<AuthProvider> <AuthProvider>
<SSEProvider> <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) => { eventSource.onerror = (error) => {
console.error('SSE connection error:', error); console.error('SSE connection error:', error);
setIsConnected(false); 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) => { eventSource.addEventListener('notification', (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
@@ -116,12 +136,23 @@ export const SSEProvider: React.FC<SSEProviderProps> = ({ children }) => {
setLastEvent(sseEvent); 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 // Show toast notification
showToast({ showToast({
type: data.type || 'info', type: toastType,
title: data.title || 'Notificación', title: data.title || 'Notificación',
message: data.message, message: data.message,
duration: data.persistent ? 0 : 5000, duration: data.severity === 'urgent' ? 0 : 5000, // Keep urgent alerts until dismissed
}); });
// Trigger registered listeners // Trigger registered listeners
@@ -145,10 +176,10 @@ export const SSEProvider: React.FC<SSEProviderProps> = ({ children }) => {
setLastEvent(sseEvent); setLastEvent(sseEvent);
// Show inventory alert // Show inventory alert (high/urgent alerts from alert_processor)
showToast({ showToast({
type: 'warning', type: data.severity === 'urgent' ? 'error' : 'warning',
title: 'Alerta de Inventario', title: data.title || 'Alerta de Inventario',
message: data.message, message: data.message,
duration: 0, // Keep until dismissed duration: 0, // Keep until dismissed
}); });

View File

@@ -23,6 +23,24 @@ import {
class ProcurementService { 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 // Purchase Order management
async getPurchaseOrders(params?: { async getPurchaseOrders(params?: {
page?: number; page?: number;
@@ -43,34 +61,34 @@ class ProcurementService {
} }
const url = queryParams.toString() const url = queryParams.toString()
? `/purchase-orders?${queryParams.toString()}` ? `${this.getBaseUrl()}/purchase-orders?${queryParams.toString()}`
: `/purchase-orders`; : `${this.getBaseUrl()}/purchase-orders`;
return apiClient.get(url); return apiClient.get(url);
} }
async getPurchaseOrder(orderId: string): Promise<ApiResponse<PurchaseOrderResponse>> { 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>> { 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>> { 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>> { 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 }>> { 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>> { 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 // Supplier management
@@ -86,42 +104,42 @@ class ProcurementService {
} }
const url = queryParams.toString() const url = queryParams.toString()
? `/suppliers?${queryParams.toString()}` ? `${this.getBaseUrl()}/suppliers?${queryParams.toString()}`
: `/suppliers`; : `${this.getBaseUrl()}/suppliers`;
return apiClient.get(url); return apiClient.get(url);
} }
async getSupplier(supplierId: string): Promise<ApiResponse<SupplierResponse>> { 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>> { 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>> { 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 }>> { 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>> { 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>> { async getSupplierStatistics(): Promise<ApiResponse<SupplierStatistics>> {
return apiClient.get(`/suppliers/statistics`); return apiClient.get(`${this.getBaseUrl()}/suppliers/statistics`);
} }
async getActiveSuppliers(): Promise<ApiResponse<SupplierSummary[]>> { 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[]>> { 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() const url = queryParams.toString()
? `/deliveries?${queryParams.toString()}` ? `${this.getBaseUrl()}/deliveries?${queryParams.toString()}`
: `/deliveries`; : `${this.getBaseUrl()}/deliveries`;
return apiClient.get(url); return apiClient.get(url);
} }
async getDelivery(deliveryId: string): Promise<ApiResponse<DeliveryResponse>> { 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>> { 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>> { 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>> { 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>> { 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 import structlog
from fastapi import FastAPI, Request, HTTPException, Depends, WebSocket, WebSocketDisconnect from fastapi import FastAPI, Request, HTTPException, Depends, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse, StreamingResponse
import httpx import httpx
import time import time
import redis.asyncio as aioredis
from typing import Dict, Any from typing import Dict, Any
from app.core.config import settings from app.core.config import settings
@@ -40,6 +41,9 @@ metrics_collector = MetricsCollector("gateway")
# Service discovery # Service discovery
service_discovery = ServiceDiscovery() service_discovery = ServiceDiscovery()
# Redis client for SSE streaming
redis_client = None
# CORS middleware - Add first # CORS middleware - Add first
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
@@ -64,8 +68,16 @@ app.include_router(nominatim.router, prefix="/api/v1/nominatim", tags=["location
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
"""Application startup""" """Application startup"""
global redis_client
logger.info("Starting API Gateway") 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( metrics_collector.register_counter(
"gateway_auth_requests_total", "gateway_auth_requests_total",
@@ -94,8 +106,14 @@ async def startup_event():
@app.on_event("shutdown") @app.on_event("shutdown")
async def shutdown_event(): async def shutdown_event():
"""Application shutdown""" """Application shutdown"""
global redis_client
logger.info("Shutting down API Gateway") logger.info("Shutting down API Gateway")
# Close Redis connection
if redis_client:
await redis_client.close()
# Clean up service discovery # Clean up service discovery
# await service_discovery.cleanup() # await service_discovery.cleanup()
@@ -116,6 +134,111 @@ async def metrics():
"""Metrics endpoint for monitoring""" """Metrics endpoint for monitoring"""
return {"metrics": "enabled"} 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 # 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("/") target_path = f"/api/v1/tenants/{tenant_id}/orders/{path}".rstrip("/")
return await _proxy_to_orders_service(request, target_path, tenant_id=tenant_id) 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 # 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""" """Proxy request to orders service"""
return await _proxy_request(request, target_path, settings.ORDERS_SERVICE_URL, tenant_id=tenant_id) 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): async def _proxy_request(request: Request, target_path: str, service_url: str, tenant_id: str = None):
"""Generic proxy function with enhanced error handling""" """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: if tenant_id:
headers["X-Tenant-ID"] = 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 # Get request body if present
body = None body = None
if request.method in ["POST", "PUT", "PATCH"]: if request.method in ["POST", "PUT", "PATCH"]:

View File

@@ -5,6 +5,7 @@ redis==5.0.1
pydantic==2.5.0 pydantic==2.5.0
pydantic-settings==2.1.0 pydantic-settings==2.1.0
python-jose[cryptography]==3.3.0 python-jose[cryptography]==3.3.0
PyJWT==2.8.0
python-multipart==0.0.6 python-multipart==0.0.6
prometheus-client==0.17.1 prometheus-client==0.17.1
python-json-logger==2.0.4 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 # Import unified authentication from shared library
from shared.auth.decorators import ( from shared.auth.decorators import (
get_current_user_dep, get_current_user_dep
get_current_tenant_id_dep
) )
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession

View File

@@ -16,7 +16,6 @@ from app.schemas.forecasts import (
) )
from shared.auth.decorators import ( from shared.auth.decorators import (
get_current_user_dep, get_current_user_dep,
get_current_tenant_id_dep,
require_admin_role require_admin_role
) )
from shared.database.base import create_database_manager from shared.database.base import create_database_manager
@@ -38,22 +37,12 @@ async def create_enhanced_single_forecast(
request: ForecastRequest, request: ForecastRequest,
tenant_id: str = Path(..., description="Tenant ID"), tenant_id: str = Path(..., description="Tenant ID"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
): ):
"""Generate a single product forecast using enhanced repository pattern""" """Generate a single product forecast using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj) metrics = get_metrics_collector(request_obj)
try: 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", logger.info("Generating enhanced single forecast",
tenant_id=tenant_id, tenant_id=tenant_id,
inventory_product_id=request.inventory_product_id, inventory_product_id=request.inventory_product_id,
@@ -106,22 +95,12 @@ async def create_enhanced_batch_forecast(
request: BatchForecastRequest, request: BatchForecastRequest,
tenant_id: str = Path(..., description="Tenant ID"), tenant_id: str = Path(..., description="Tenant ID"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
): ):
"""Generate batch forecasts using enhanced repository pattern""" """Generate batch forecasts using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj) metrics = get_metrics_collector(request_obj)
try: 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", logger.info("Generating enhanced batch forecasts",
tenant_id=tenant_id, tenant_id=tenant_id,
products_count=len(request.inventory_product_ids), 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"), skip: int = Query(0, description="Number of records to skip"),
limit: int = Query(100, description="Number of records to return"), limit: int = Query(100, description="Number of records to return"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
): ):
"""Get tenant forecasts with enhanced filtering using repository pattern""" """Get tenant forecasts with enhanced filtering using repository pattern"""
metrics = get_metrics_collector(request_obj) metrics = get_metrics_collector(request_obj)
try: 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 # Record metrics
if metrics: if metrics:
metrics.increment_counter("enhanced_get_forecasts_total") 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"), tenant_id: str = Path(..., description="Tenant ID"),
forecast_id: str = Path(..., description="Forecast ID"), forecast_id: str = Path(..., description="Forecast ID"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
): ):
"""Get specific forecast by ID using enhanced repository pattern""" """Get specific forecast by ID using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj) metrics = get_metrics_collector(request_obj)
try: 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 # Record metrics
if metrics: if metrics:
metrics.increment_counter("enhanced_get_forecast_by_id_total") 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"), tenant_id: str = Path(..., description="Tenant ID"),
forecast_id: str = Path(..., description="Forecast ID"), forecast_id: str = Path(..., description="Forecast ID"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
): ):
"""Delete forecast using enhanced repository pattern""" """Delete forecast using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj) metrics = get_metrics_collector(request_obj)
try: 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 # Record metrics
if metrics: if metrics:
metrics.increment_counter("enhanced_delete_forecast_total") metrics.increment_counter("enhanced_delete_forecast_total")
@@ -370,22 +319,12 @@ async def delete_enhanced_forecast(
async def get_enhanced_forecast_statistics( async def get_enhanced_forecast_statistics(
tenant_id: str = Path(..., description="Tenant ID"), tenant_id: str = Path(..., description="Tenant ID"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
): ):
"""Get comprehensive forecast statistics using enhanced repository pattern""" """Get comprehensive forecast statistics using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj) metrics = get_metrics_collector(request_obj)
try: 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 # Record metrics
if metrics: if metrics:
metrics.increment_counter("enhanced_forecast_statistics_total") 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 app.schemas.forecasts import ForecastRequest
from shared.auth.decorators import ( from shared.auth.decorators import (
get_current_user_dep, get_current_user_dep,
get_current_tenant_id_dep,
require_admin_role require_admin_role
) )
from shared.database.base import create_database_manager from shared.database.base import create_database_manager
@@ -41,21 +40,12 @@ async def generate_enhanced_realtime_prediction(
prediction_request: Dict[str, Any], prediction_request: Dict[str, Any],
tenant_id: str = Path(..., description="Tenant ID"), tenant_id: str = Path(..., description="Tenant ID"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
prediction_service: PredictionService = Depends(get_enhanced_prediction_service) prediction_service: PredictionService = Depends(get_enhanced_prediction_service)
): ):
"""Generate real-time prediction using enhanced repository pattern""" """Generate real-time prediction using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj) metrics = get_metrics_collector(request_obj)
try: 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", logger.info("Generating enhanced real-time prediction",
tenant_id=tenant_id, tenant_id=tenant_id,
@@ -127,21 +117,12 @@ async def generate_enhanced_batch_predictions(
batch_request: Dict[str, Any], batch_request: Dict[str, Any],
tenant_id: str = Path(..., description="Tenant ID"), tenant_id: str = Path(..., description="Tenant ID"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
): ):
"""Generate batch predictions using enhanced repository pattern""" """Generate batch predictions using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj) metrics = get_metrics_collector(request_obj)
try: 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", logger.info("Generating enhanced batch predictions",
tenant_id=tenant_id, tenant_id=tenant_id,
@@ -209,21 +190,12 @@ async def get_enhanced_prediction_cache(
skip: int = Query(0, description="Number of records to skip"), skip: int = Query(0, description="Number of records to skip"),
limit: int = Query(100, description="Number of records to return"), limit: int = Query(100, description="Number of records to return"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
): ):
"""Get cached predictions using enhanced repository pattern""" """Get cached predictions using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj) metrics = get_metrics_collector(request_obj)
try: 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 # Record metrics
if metrics: if metrics:
@@ -273,21 +245,12 @@ async def clear_enhanced_prediction_cache(
tenant_id: str = Path(..., description="Tenant ID"), tenant_id: str = Path(..., description="Tenant ID"),
inventory_product_id: Optional[str] = Query(None, description="Clear cache for specific inventory product ID"), inventory_product_id: Optional[str] = Query(None, description="Clear cache for specific inventory product ID"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
): ):
"""Clear prediction cache using enhanced repository pattern""" """Clear prediction cache using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj) metrics = get_metrics_collector(request_obj)
try: 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 # Record metrics
if metrics: if metrics:
@@ -337,21 +300,12 @@ async def get_enhanced_prediction_performance(
start_date: Optional[date] = Query(None, description="Start date filter"), start_date: Optional[date] = Query(None, description="Start date filter"),
end_date: Optional[date] = Query(None, description="End date filter"), end_date: Optional[date] = Query(None, description="End date filter"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
): ):
"""Get prediction performance metrics using enhanced repository pattern""" """Get prediction performance metrics using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj) metrics = get_metrics_collector(request_obj)
try: 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 # Record metrics
if metrics: if metrics:
@@ -398,21 +352,12 @@ async def validate_enhanced_prediction_request(
validation_request: Dict[str, Any], validation_request: Dict[str, Any],
tenant_id: str = Path(..., description="Tenant ID"), tenant_id: str = Path(..., description="Tenant ID"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
prediction_service: PredictionService = Depends(get_enhanced_prediction_service) prediction_service: PredictionService = Depends(get_enhanced_prediction_service)
): ):
"""Validate prediction request without generating prediction""" """Validate prediction request without generating prediction"""
metrics = get_metrics_collector(request_obj) metrics = get_metrics_collector(request_obj)
try: 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 # Record metrics
if metrics: if metrics:

View File

@@ -11,7 +11,7 @@ from pydantic import BaseModel, Field
import structlog import structlog
from app.services.product_classifier import ProductClassifierService, get_product_classifier 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"]) router = APIRouter(tags=["classification"])
logger = structlog.get_logger() logger = structlog.get_logger()
@@ -69,16 +69,11 @@ class BatchClassificationResponse(BaseModel):
async def classify_single_product( async def classify_single_product(
request: ProductClassificationRequest, request: ProductClassificationRequest,
tenant_id: UUID = Path(..., description="Tenant ID"), 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), current_user: Dict[str, Any] = Depends(get_current_user_dep),
classifier: ProductClassifierService = Depends(get_product_classifier) classifier: ProductClassifierService = Depends(get_product_classifier)
): ):
"""Classify a single product for inventory creation""" """Classify a single product for inventory creation"""
try: try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# Classify the product # Classify the product
suggestion = classifier.classify_product( suggestion = classifier.classify_product(
request.product_name, request.product_name,
@@ -120,16 +115,11 @@ async def classify_single_product(
async def classify_products_batch( async def classify_products_batch(
request: BatchClassificationRequest, request: BatchClassificationRequest,
tenant_id: UUID = Path(..., description="Tenant ID"), 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), current_user: Dict[str, Any] = Depends(get_current_user_dep),
classifier: ProductClassifierService = Depends(get_product_classifier) classifier: ProductClassifierService = Depends(get_product_classifier)
): ):
"""Classify multiple products for onboarding automation""" """Classify multiple products for onboarding automation"""
try: 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: if not request.products:
raise HTTPException(status_code=400, detail="No products provided for classification") 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 from sqlalchemy.ext.asyncio import AsyncSession
import structlog 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.core.database import get_db
from app.services.inventory_service import InventoryService from app.services.inventory_service import InventoryService
from app.services.food_safety_service import FoodSafetyService 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( async def get_inventory_dashboard_summary(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
filters: Optional[DashboardFilter] = None, filters: Optional[DashboardFilter] = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep), current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service), dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get comprehensive inventory dashboard summary""" """Get comprehensive inventory dashboard summary"""
try: 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) summary = await dashboard_service.get_inventory_dashboard_summary(db, tenant_id, filters)
logger.info("Dashboard summary retrieved", 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) @router.get("/tenants/{tenant_id}/food-safety", response_model=FoodSafetyDashboard)
async def get_food_safety_dashboard( async def get_food_safety_dashboard(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep), current_user: dict = Depends(get_current_user_dep),
food_safety_service: FoodSafetyService = Depends(lambda: FoodSafetyService()), food_safety_service: FoodSafetyService = Depends(lambda: FoodSafetyService()),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get food safety dashboard data""" """Get food safety dashboard data"""
try: 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) dashboard = await food_safety_service.get_food_safety_dashboard(db, tenant_id)
logger.info("Food safety dashboard retrieved", logger.info("Food safety dashboard retrieved",
@@ -119,19 +105,12 @@ async def get_food_safety_dashboard(
async def get_inventory_analytics( async def get_inventory_analytics(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
days_back: int = Query(30, ge=1, le=365, description="Number of days to analyze"), 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), current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service), dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get advanced inventory analytics""" """Get advanced inventory analytics"""
try: 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) analytics = await dashboard_service.get_inventory_analytics(db, tenant_id, days_back)
logger.info("Inventory analytics retrieved", logger.info("Inventory analytics retrieved",
@@ -153,19 +132,12 @@ async def get_inventory_analytics(
@router.get("/tenants/{tenant_id}/business-model", response_model=BusinessModelInsights) @router.get("/tenants/{tenant_id}/business-model", response_model=BusinessModelInsights)
async def get_business_model_insights( async def get_business_model_insights(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep), current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service), dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get business model insights based on inventory patterns""" """Get business model insights based on inventory patterns"""
try: 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) insights = await dashboard_service.get_business_model_insights(db, tenant_id)
logger.info("Business model insights retrieved", 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]) @router.get("/tenants/{tenant_id}/stock-status", response_model=List[StockStatusSummary])
async def get_stock_status_by_category( async def get_stock_status_by_category(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep), current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service), dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get stock status breakdown by category""" """Get stock status breakdown by category"""
try: 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) stock_status = await dashboard_service.get_stock_status_by_category(db, tenant_id)
return stock_status return stock_status
@@ -220,19 +185,12 @@ async def get_stock_status_by_category(
async def get_alerts_summary( async def get_alerts_summary(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
filters: Optional[AlertsFilter] = None, filters: Optional[AlertsFilter] = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep), current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service), dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get alerts summary by type and severity""" """Get alerts summary by type and severity"""
try: 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) alerts_summary = await dashboard_service.get_alerts_summary(db, tenant_id, filters)
return alerts_summary return alerts_summary
@@ -252,19 +210,12 @@ async def get_recent_activity(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
limit: int = Query(20, ge=1, le=100, description="Number of activities to return"), 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"), 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), current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service), dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get recent inventory activity""" """Get recent inventory activity"""
try: 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( activities = await dashboard_service.get_recent_activity(
db, tenant_id, limit, activity_types db, tenant_id, limit, activity_types
) )
@@ -286,19 +237,12 @@ async def get_recent_activity(
@router.get("/tenants/{tenant_id}/live-metrics") @router.get("/tenants/{tenant_id}/live-metrics")
async def get_live_metrics( async def get_live_metrics(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep), current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service), dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get real-time inventory metrics""" """Get real-time inventory metrics"""
try: 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) metrics = await dashboard_service.get_live_metrics(db, tenant_id)
return { return {
@@ -320,19 +264,12 @@ async def get_live_metrics(
@router.get("/tenants/{tenant_id}/temperature-status") @router.get("/tenants/{tenant_id}/temperature-status")
async def get_temperature_monitoring_status( async def get_temperature_monitoring_status(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep), current_user: dict = Depends(get_current_user_dep),
food_safety_service: FoodSafetyService = Depends(lambda: FoodSafetyService()), food_safety_service: FoodSafetyService = Depends(lambda: FoodSafetyService()),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get current temperature monitoring status""" """Get current temperature monitoring status"""
try: 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) temp_status = await food_safety_service.get_temperature_monitoring_status(db, tenant_id)
return { return {
@@ -355,17 +292,10 @@ async def get_temperature_monitoring_status(
@router.get("/tenants/{tenant_id}/config") @router.get("/tenants/{tenant_id}/config")
async def get_dashboard_config( async def get_dashboard_config(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep) current_user: dict = Depends(get_current_user_dep)
): ):
"""Get dashboard configuration and settings""" """Get dashboard configuration and settings"""
try: 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 from app.core.config import settings
config = { config = {
@@ -411,19 +341,12 @@ async def export_dashboard_summary(
format: str = Query("json", description="Export format: json, csv, excel"), format: str = Query("json", description="Export format: json, csv, excel"),
date_from: Optional[datetime] = Query(None, description="Start date for data export"), date_from: Optional[datetime] = Query(None, description="Start date for data export"),
date_to: Optional[datetime] = Query(None, description="End 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), current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service), dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Export dashboard summary data""" """Export dashboard summary data"""
try: 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"]: if format.lower() not in ["json", "csv", "excel"]:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@@ -460,17 +383,10 @@ async def export_dashboard_summary(
@router.get("/tenants/{tenant_id}/health") @router.get("/tenants/{tenant_id}/health")
async def get_dashboard_health( async def get_dashboard_health(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep) current_user: dict = Depends(get_current_user_dep)
): ):
"""Get dashboard service health status""" """Get dashboard service health status"""
try: try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
return { return {
"service": "inventory-dashboard", "service": "inventory-dashboard",
"status": "healthy", "status": "healthy",

View File

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

View File

@@ -17,7 +17,7 @@ from app.schemas.inventory import (
InventoryFilter, InventoryFilter,
PaginatedResponse 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"]) 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( async def create_ingredient(
ingredient_data: IngredientCreate, ingredient_data: IngredientCreate,
tenant_id: UUID = Path(..., description="Tenant ID"), tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep), current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Create a new ingredient""" """Create a new ingredient"""
try: 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 # Extract user ID - handle service tokens that don't have UUID user_ids
raw_user_id = current_user.get('user_id') raw_user_id = current_user.get('user_id')
if current_user.get('type') == 'service': if current_user.get('type') == 'service':
@@ -77,7 +72,6 @@ async def create_ingredient(
async def get_ingredient( async def get_ingredient(
ingredient_id: UUID, ingredient_id: UUID,
tenant_id: UUID = Path(..., description="Tenant ID"), tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep), current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
@@ -111,7 +105,6 @@ async def update_ingredient(
ingredient_id: UUID, ingredient_id: UUID,
ingredient_data: IngredientUpdate, ingredient_data: IngredientUpdate,
tenant_id: UUID = Path(..., description="Tenant ID"), tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep), current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db) 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"), is_low_stock: Optional[bool] = Query(None, description="Filter by low stock status"),
needs_reorder: Optional[bool] = Query(None, description="Filter by reorder needed"), needs_reorder: Optional[bool] = Query(None, description="Filter by reorder needed"),
search: Optional[str] = Query(None, description="Search in name, SKU, or barcode"), 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), current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
@@ -196,7 +188,6 @@ async def list_ingredients(
async def delete_ingredient( async def delete_ingredient(
ingredient_id: UUID, ingredient_id: UUID,
tenant_id: UUID = Path(..., description="Tenant ID"), tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep), current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
@@ -234,7 +225,6 @@ async def get_ingredient_stock(
ingredient_id: UUID, ingredient_id: UUID,
tenant_id: UUID = Path(..., description="Tenant ID"), tenant_id: UUID = Path(..., description="Tenant ID"),
include_unavailable: bool = Query(False, description="Include unavailable stock"), 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), current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db) 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 app.models.notifications import NotificationType as ModelNotificationType
from shared.auth.decorators import ( from shared.auth.decorators import (
get_current_user_dep, get_current_user_dep,
get_current_tenant_id_dep,
require_role require_role
) )
from shared.database.base import create_database_manager from shared.database.base import create_database_manager
@@ -37,7 +36,6 @@ def get_enhanced_notification_service():
@track_endpoint_metrics("notification_send") @track_endpoint_metrics("notification_send")
async def send_notification_enhanced( async def send_notification_enhanced(
notification_data: Dict[str, Any], notification_data: Dict[str, Any],
tenant_id: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep), current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service) notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
): ):
@@ -86,7 +84,7 @@ async def send_notification_enhanced(
# Create notification using enhanced service # Create notification using enhanced service
notification = await notification_service.create_notification( notification = await notification_service.create_notification(
tenant_id=tenant_id, tenant_id=current_user.get("tenant_id"),
sender_id=current_user["user_id"], sender_id=current_user["user_id"],
notification_type=notification_type, notification_type=notification_type,
message=notification_data["message"], message=notification_data["message"],
@@ -104,7 +102,7 @@ async def send_notification_enhanced(
logger.info("Notification sent successfully", logger.info("Notification sent successfully",
notification_id=notification.id, notification_id=notification.id,
tenant_id=tenant_id, tenant_id=current_user.get("tenant_id"),
type=notification_type.value, type=notification_type.value,
priority=priority.value) priority=priority.value)
@@ -114,7 +112,7 @@ async def send_notification_enhanced(
raise raise
except Exception as e: except Exception as e:
logger.error("Failed to send notification", logger.error("Failed to send notification",
tenant_id=tenant_id, tenant_id=current_user.get("tenant_id"),
sender_id=current_user["user_id"], sender_id=current_user["user_id"],
error=str(e)) error=str(e))
raise HTTPException( raise HTTPException(

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import structlog
import json import json
from app.services.data_import_service import DataImportService 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"]) router = APIRouter(tags=["data-import"])
logger = structlog.get_logger() logger = structlog.get_logger()
@@ -26,14 +26,10 @@ async def validate_json_data(
tenant_id: UUID = Path(..., description="Tenant ID"), tenant_id: UUID = Path(..., description="Tenant ID"),
data: Dict[str, Any] = None, data: Dict[str, Any] = None,
current_user: Dict[str, Any] = Depends(get_current_user_dep), 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_service: DataImportService = Depends(get_import_service)
): ):
"""Validate JSON sales data""" """Validate JSON sales data"""
try: try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
if not data: if not data:
raise HTTPException(status_code=400, detail="No data provided") 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, data: Optional[Dict[str, Any]] = None,
file_format: Optional[str] = Form(None), file_format: Optional[str] = Form(None),
current_user: Dict[str, Any] = Depends(get_current_user_dep), 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_service: DataImportService = Depends(get_import_service)
): ):
"""Universal validation endpoint for sales data - supports files and JSON""" """Universal validation endpoint for sales data - supports files and JSON"""
try: try:
# Verify tenant access # Debug logging at the start
if str(tenant_id) != current_tenant: logger.info("=== VALIDATION ENDPOINT CALLED ===",
raise HTTPException(status_code=403, detail="Access denied to this tenant") 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 # Handle file upload validation
if file: if file and file.filename:
logger.info("Validating uploaded file", tenant_id=tenant_id, filename=file.filename) logger.info("Processing file upload branch", tenant_id=tenant_id, filename=file.filename)
# Auto-detect format from filename # Auto-detect format from filename
filename = file.filename.lower() filename = file.filename.lower()
@@ -125,7 +124,7 @@ async def validate_sales_data_universal(
# Handle JSON data validation # Handle JSON data validation
elif data: 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 = data.copy()
validation_data["tenant_id"] = str(tenant_id) validation_data["tenant_id"] = str(tenant_id)
@@ -133,10 +132,13 @@ async def validate_sales_data_universal(
validation_data["data_format"] = "json" validation_data["data_format"] = "json"
else: 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") raise HTTPException(status_code=400, detail="No file or data provided for validation")
# Perform 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) 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", logger.info("Validation completed",
tenant_id=tenant_id, tenant_id=tenant_id,
@@ -161,8 +163,9 @@ async def validate_sales_data_universal(
} }
except Exception as e: except Exception as e:
logger.error("Failed to validate sales data", error=str(e), tenant_id=tenant_id) error_msg = str(e) if e else "Unknown error occurred during validation"
raise HTTPException(status_code=500, detail=f"Failed to validate data: {str(e)}") logger.error("Failed to validate sales data", error=error_msg, tenant_id=tenant_id, exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to validate data: {error_msg}")
@router.post("/tenants/{tenant_id}/sales/import/validate-csv") @router.post("/tenants/{tenant_id}/sales/import/validate-csv")
@@ -170,7 +173,6 @@ async def validate_csv_data_legacy(
tenant_id: UUID = Path(..., description="Tenant ID"), tenant_id: UUID = Path(..., description="Tenant ID"),
file: UploadFile = File(...), file: UploadFile = File(...),
current_user: Dict[str, Any] = Depends(get_current_user_dep), 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_service: DataImportService = Depends(get_import_service)
): ):
"""Legacy CSV validation endpoint - redirects to universal validator""" """Legacy CSV validation endpoint - redirects to universal validator"""
@@ -178,7 +180,6 @@ async def validate_csv_data_legacy(
tenant_id=tenant_id, tenant_id=tenant_id,
file=file, file=file,
current_user=current_user, current_user=current_user,
current_tenant=current_tenant,
import_service=import_service import_service=import_service
) )
@@ -191,14 +192,10 @@ async def import_sales_data(
file_format: Optional[str] = Form(None), file_format: Optional[str] = Form(None),
update_existing: bool = Form(False, description="Whether to update existing records"), update_existing: bool = Form(False, description="Whether to update existing records"),
current_user: Dict[str, Any] = Depends(get_current_user_dep), 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_service: DataImportService = Depends(get_import_service)
): ):
"""Enhanced import sales data - supports multiple file formats and JSON""" """Enhanced import sales data - supports multiple file formats and JSON"""
try: 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) # Handle file upload (form data)
if file: if file:
@@ -302,14 +299,10 @@ async def import_csv_data(
file: UploadFile = File(...), file: UploadFile = File(...),
update_existing: bool = Form(False, description="Whether to update existing records"), update_existing: bool = Form(False, description="Whether to update existing records"),
current_user: Dict[str, Any] = Depends(get_current_user_dep), 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_service: DataImportService = Depends(get_import_service)
): ):
"""Import CSV sales data file""" """Import CSV sales data file"""
try: 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'): if not file.filename.endswith('.csv'):
raise HTTPException(status_code=400, detail="File must be a CSV file") 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") @router.get("/tenants/{tenant_id}/sales/import/template")
async def get_import_template( async def get_import_template(
tenant_id: UUID = Path(..., description="Tenant ID"), tenant_id: UUID = Path(..., description="Tenant ID"),
format: str = "csv", format: str = "csv"
current_tenant: str = Depends(get_current_tenant_id_dep)
): ):
"""Get sales data import template""" """Get sales data import template"""
try: 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"]: if format not in ["csv", "json"]:
raise HTTPException(status_code=400, detail="Format must be 'csv' or '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 SalesDataQuery
) )
from app.services.sales_service import SalesService 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"]) router = APIRouter(tags=["sales"])
logger = structlog.get_logger() logger = structlog.get_logger()
@@ -32,15 +32,10 @@ async def create_sales_record(
sales_data: SalesDataCreate, sales_data: SalesDataCreate,
tenant_id: UUID = Path(..., description="Tenant ID"), tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep), 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) sales_service: SalesService = Depends(get_sales_service)
): ):
"""Create a new sales record""" """Create a new sales record"""
try: try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
logger.info( logger.info(
"Creating sales record", "Creating sales record",
product=sales_data.product_name, 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"), offset: int = Query(0, ge=0, description="Number of records to skip"),
order_by: str = Query("date", description="Field to order by"), order_by: str = Query("date", description="Field to order by"),
order_direction: str = Query("desc", description="Order direction (asc/desc)"), 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) sales_service: SalesService = Depends(get_sales_service)
): ):
"""Get sales records for a tenant with filtering and pagination""" """Get sales records for a tenant with filtering and pagination"""
try: try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# Build query parameters # Build query parameters
query_params = SalesDataQuery( query_params = SalesDataQuery(
start_date=start_date, start_date=start_date,
@@ -124,15 +114,10 @@ async def get_sales_analytics(
tenant_id: UUID = Path(..., description="Tenant ID"), tenant_id: UUID = Path(..., description="Tenant ID"),
start_date: Optional[datetime] = Query(None, description="Start date filter"), start_date: Optional[datetime] = Query(None, description="Start date filter"),
end_date: Optional[datetime] = Query(None, description="End 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) sales_service: SalesService = Depends(get_sales_service)
): ):
"""Get sales analytics summary for a tenant""" """Get sales analytics summary for a tenant"""
try: 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) analytics = await sales_service.get_sales_analytics(tenant_id, start_date, end_date)
logger.info("Retrieved sales analytics", tenant_id=tenant_id) 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"), inventory_product_id: UUID = Path(..., description="Inventory product ID"),
start_date: Optional[datetime] = Query(None, description="Start date filter"), start_date: Optional[datetime] = Query(None, description="Start date filter"),
end_date: Optional[datetime] = Query(None, description="End 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) sales_service: SalesService = Depends(get_sales_service)
): ):
"""Get sales records for a specific product""" """Get sales records for a specific product"""
try: 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) 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) 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]) @router.get("/tenants/{tenant_id}/sales/categories", response_model=List[str])
async def get_product_categories( async def get_product_categories(
tenant_id: UUID = Path(..., description="Tenant ID"), tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
sales_service: SalesService = Depends(get_sales_service) sales_service: SalesService = Depends(get_sales_service)
): ):
"""Get distinct product categories from sales data""" """Get distinct product categories from sales data"""
try: 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) categories = await sales_service.get_product_categories(tenant_id)
return categories return categories
@@ -197,15 +172,10 @@ async def get_product_categories(
async def get_sales_record( async def get_sales_record(
tenant_id: UUID = Path(..., description="Tenant ID"), tenant_id: UUID = Path(..., description="Tenant ID"),
record_id: UUID = Path(..., description="Sales record 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) sales_service: SalesService = Depends(get_sales_service)
): ):
"""Get a specific sales record""" """Get a specific sales record"""
try: 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) record = await sales_service.get_sales_record(record_id, tenant_id)
if not record: if not record:
@@ -225,15 +195,10 @@ async def update_sales_record(
update_data: SalesDataUpdate, update_data: SalesDataUpdate,
tenant_id: UUID = Path(..., description="Tenant ID"), tenant_id: UUID = Path(..., description="Tenant ID"),
record_id: UUID = Path(..., description="Sales record 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) sales_service: SalesService = Depends(get_sales_service)
): ):
"""Update a sales record""" """Update a sales record"""
try: 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) 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) 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( async def delete_sales_record(
tenant_id: UUID = Path(..., description="Tenant ID"), tenant_id: UUID = Path(..., description="Tenant ID"),
record_id: UUID = Path(..., description="Sales record 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) sales_service: SalesService = Depends(get_sales_service)
): ):
"""Delete a sales record""" """Delete a sales record"""
try: 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) success = await sales_service.delete_sales_record(record_id, tenant_id)
if not success: if not success:
@@ -281,15 +241,10 @@ async def validate_sales_record(
tenant_id: UUID = Path(..., description="Tenant ID"), tenant_id: UUID = Path(..., description="Tenant ID"),
record_id: UUID = Path(..., description="Sales record ID"), record_id: UUID = Path(..., description="Sales record ID"),
validation_notes: Optional[str] = Query(None, description="Validation notes"), 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) sales_service: SalesService = Depends(get_sales_service)
): ):
"""Mark a sales record as validated""" """Mark a sales record as validated"""
try: 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) 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) 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""" """Enhanced data import service using repository pattern with STRICT validation for production"""
# PRODUCTION VALIDATION CONFIGURATION # 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_QUANTITY_PER_DAY = 10000 # Maximum reasonable quantity per product per day
MAX_REVENUE_PER_ITEM = 100000 # Maximum reasonable revenue per line item MAX_REVENUE_PER_ITEM = 100000 # Maximum reasonable revenue per line item
MAX_UNIT_PRICE = 10000 # Maximum reasonable price per unit for bakery items 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 from sqlalchemy.ext.asyncio import AsyncSession
import structlog 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.core.database import get_db
from app.services.performance_service import PerformanceTrackingService, AlertService from app.services.performance_service import PerformanceTrackingService, AlertService
from app.services.dashboard_service import DashboardService from app.services.dashboard_service import DashboardService
@@ -54,19 +54,12 @@ async def calculate_supplier_performance(
period: PerformancePeriod = Query(...), period: PerformancePeriod = Query(...),
period_start: datetime = Query(...), period_start: datetime = Query(...),
period_end: datetime = Query(...), period_end: datetime = Query(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep), current_user: dict = Depends(get_current_user_dep),
performance_service: PerformanceTrackingService = Depends(get_performance_service), performance_service: PerformanceTrackingService = Depends(get_performance_service),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Calculate performance metrics for a supplier""" """Calculate performance metrics for a supplier"""
try: 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( metric = await performance_service.calculate_supplier_performance(
db, supplier_id, tenant_id, period, period_start, period_end 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_from: Optional[datetime] = Query(None),
date_to: Optional[datetime] = Query(None), date_to: Optional[datetime] = Query(None),
limit: int = Query(50, ge=1, le=500), 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), current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get performance metrics for a supplier""" """Get performance metrics for a supplier"""
try: 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 # TODO: Implement get_supplier_performance_metrics in service
# For now, return empty list # For now, return empty list
metrics = [] metrics = []
@@ -139,19 +125,12 @@ async def get_supplier_performance_metrics(
async def evaluate_performance_alerts( async def evaluate_performance_alerts(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
supplier_id: Optional[UUID] = Query(None, description="Specific supplier to evaluate"), 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), current_user: dict = Depends(get_current_user_dep),
alert_service: AlertService = Depends(get_alert_service), alert_service: AlertService = Depends(get_alert_service),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Evaluate and create performance-based alerts""" """Evaluate and create performance-based alerts"""
try: 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) alerts = await alert_service.evaluate_performance_alerts(db, tenant_id, supplier_id)
logger.info("Performance alerts evaluated", logger.info("Performance alerts evaluated",
@@ -179,18 +158,11 @@ async def get_supplier_alerts(
date_from: Optional[datetime] = Query(None), date_from: Optional[datetime] = Query(None),
date_to: Optional[datetime] = Query(None), date_to: Optional[datetime] = Query(None),
limit: int = Query(50, ge=1, le=500), 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), current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get supplier alerts with filtering""" """Get supplier alerts with filtering"""
try: 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 # TODO: Implement get_supplier_alerts in service
# For now, return empty list # For now, return empty list
alerts = [] alerts = []
@@ -212,18 +184,11 @@ async def update_alert(
alert_update: AlertUpdate, alert_update: AlertUpdate,
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
alert_id: UUID = Path(...), alert_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep), current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Update an alert (acknowledge, resolve, etc.)""" """Update an alert (acknowledge, resolve, etc.)"""
try: 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 # TODO: Implement update_alert in service
raise HTTPException( raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, status_code=status.HTTP_501_NOT_IMPLEMENTED,
@@ -248,19 +213,12 @@ async def get_performance_dashboard_summary(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
date_from: Optional[datetime] = Query(None), date_from: Optional[datetime] = Query(None),
date_to: 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), current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service), dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get comprehensive performance dashboard summary""" """Get comprehensive performance dashboard summary"""
try: 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( summary = await dashboard_service.get_performance_dashboard_summary(
db, tenant_id, date_from, date_to db, tenant_id, date_from, date_to
) )
@@ -285,19 +243,12 @@ async def get_supplier_performance_insights(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
supplier_id: UUID = Path(...), supplier_id: UUID = Path(...),
days_back: int = Query(30, ge=1, le=365), 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), current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service), dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get detailed performance insights for a specific supplier""" """Get detailed performance insights for a specific supplier"""
try: 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( insights = await dashboard_service.get_supplier_performance_insights(
db, tenant_id, supplier_id, days_back db, tenant_id, supplier_id, days_back
) )
@@ -323,19 +274,12 @@ async def get_supplier_performance_insights(
async def get_performance_analytics( async def get_performance_analytics(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
period_days: int = Query(90, ge=1, le=365), 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), current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service), dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get advanced performance analytics""" """Get advanced performance analytics"""
try: 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( analytics = await dashboard_service.get_performance_analytics(
db, tenant_id, period_days db, tenant_id, period_days
) )
@@ -359,19 +303,12 @@ async def get_performance_analytics(
@router.get("/tenants/{tenant_id}/business-model", response_model=BusinessModelInsights) @router.get("/tenants/{tenant_id}/business-model", response_model=BusinessModelInsights)
async def get_business_model_insights( async def get_business_model_insights(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep), current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service), dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get business model detection and insights""" """Get business model detection and insights"""
try: 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) insights = await dashboard_service.get_business_model_insights(db, tenant_id)
logger.info("Business model insights retrieved", logger.info("Business model insights retrieved",
@@ -395,19 +332,12 @@ async def get_alert_summary(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
date_from: Optional[datetime] = Query(None), date_from: Optional[datetime] = Query(None),
date_to: 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), current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service), dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get alert summary by type and severity""" """Get alert summary by type and severity"""
try: 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) summary = await dashboard_service.get_alert_summary(db, tenant_id, date_from, date_to)
return summary return summary
@@ -428,18 +358,11 @@ async def get_alert_summary(
async def generate_performance_report( async def generate_performance_report(
report_request: PerformanceReportRequest, report_request: PerformanceReportRequest,
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep), current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Generate a performance report""" """Generate a performance report"""
try: 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 # TODO: Implement report generation
raise HTTPException( raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, status_code=status.HTTP_501_NOT_IMPLEMENTED,
@@ -463,18 +386,11 @@ async def export_performance_data(
date_from: Optional[datetime] = Query(None), date_from: Optional[datetime] = Query(None),
date_to: Optional[datetime] = Query(None), date_to: Optional[datetime] = Query(None),
supplier_ids: Optional[List[UUID]] = 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), current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Export performance data""" """Export performance data"""
try: 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"]: if format.lower() not in ["json", "csv", "excel"]:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@@ -502,17 +418,10 @@ async def export_performance_data(
@router.get("/tenants/{tenant_id}/config") @router.get("/tenants/{tenant_id}/config")
async def get_performance_config( async def get_performance_config(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep) current_user: dict = Depends(get_current_user_dep)
): ):
"""Get performance tracking configuration""" """Get performance tracking configuration"""
try: 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 from app.core.config import settings
config = { config = {
@@ -565,17 +474,10 @@ async def get_performance_config(
@router.get("/tenants/{tenant_id}/health") @router.get("/tenants/{tenant_id}/health")
async def get_performance_health( async def get_performance_health(
tenant_id: UUID = Path(...), tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep) current_user: dict = Depends(get_current_user_dep)
): ):
"""Get performance service health status""" """Get performance service health status"""
try: try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
return { return {
"service": "suppliers-performance", "service": "suppliers-performance",
"status": "healthy", "status": "healthy",

View File

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

View File

@@ -27,7 +27,7 @@ from app.services.messaging import (
publish_job_started 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.database.base import create_database_manager
from shared.monitoring.decorators import track_execution_time from shared.monitoring.decorators import track_execution_time
from shared.monitoring.metrics import get_metrics_collector 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"), tenant_id: str = Path(..., description="Tenant ID"),
background_tasks: BackgroundTasks = BackgroundTasks(), background_tasks: BackgroundTasks = BackgroundTasks(),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_training_service: EnhancedTrainingService = Depends(get_enhanced_training_service) 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) metrics = get_metrics_collector(request_obj)
try: 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 # Generate enhanced job ID
job_id = f"enhanced_training_{tenant_id}_{uuid.uuid4().hex[:8]}" 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"), tenant_id: str = Path(..., description="Tenant ID"),
inventory_product_id: str = Path(..., description="Inventory product UUID"), inventory_product_id: str = Path(..., description="Inventory product UUID"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_training_service: EnhancedTrainingService = Depends(get_enhanced_training_service) 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) metrics = get_metrics_collector(request_obj)
try: 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", logger.info("Starting enhanced single product training",
inventory_product_id=inventory_product_id, inventory_product_id=inventory_product_id,
@@ -390,7 +371,6 @@ async def get_enhanced_training_job_status(
tenant_id: str = Path(..., description="Tenant ID"), tenant_id: str = Path(..., description="Tenant ID"),
job_id: str = Path(..., description="Job ID"), job_id: str = Path(..., description="Job ID"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_training_service: EnhancedTrainingService = Depends(get_enhanced_training_service) 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) metrics = get_metrics_collector(request_obj)
try: 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 # Get status using enhanced service
status_info = await enhanced_training_service.get_training_status(job_id) 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"), skip: int = Query(0, description="Number of models to skip"),
limit: int = Query(100, description="Number of models to return"), limit: int = Query(100, description="Number of models to return"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_training_service: EnhancedTrainingService = Depends(get_enhanced_training_service) 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) metrics = get_metrics_collector(request_obj)
try: 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 # Get models using enhanced service
models = await enhanced_training_service.get_tenant_models( 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"), tenant_id: str = Path(..., description="Tenant ID"),
model_id: str = Path(..., description="Model ID"), model_id: str = Path(..., description="Model ID"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_training_service: EnhancedTrainingService = Depends(get_enhanced_training_service) 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) metrics = get_metrics_collector(request_obj)
try: 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 # Get performance using enhanced service
performance = await enhanced_training_service.get_model_performance(model_id) 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( async def get_enhanced_tenant_statistics(
tenant_id: str = Path(..., description="Tenant ID"), tenant_id: str = Path(..., description="Tenant ID"),
request_obj: Request = None, request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_training_service: EnhancedTrainingService = Depends(get_enhanced_training_service) 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) metrics = get_metrics_collector(request_obj)
try: 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 # Get statistics using enhanced service
statistics = await enhanced_training_service.get_tenant_statistics(tenant_id) 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 app.services.messaging import training_publisher
from shared.auth.decorators import ( from shared.auth.decorators import (
get_current_user_dep, get_current_user_dep
get_current_tenant_id_dep
) )
# Create WebSocket router # 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") 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") 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") 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") NOMINATIM_SERVICE_URL: str = os.getenv("NOMINATIM_SERVICE_URL", "http://nominatim:8080")
# HTTP Client Settings # HTTP Client Settings
@@ -338,6 +339,7 @@ class BaseServiceSettings(BaseSettings):
"notification": self.NOTIFICATION_SERVICE_URL, "notification": self.NOTIFICATION_SERVICE_URL,
"production": self.PRODUCTION_SERVICE_URL, "production": self.PRODUCTION_SERVICE_URL,
"orders": self.ORDERS_SERVICE_URL, "orders": self.ORDERS_SERVICE_URL,
"suppliers": self.SUPPLIERS_SERVICE_URL,
} }
@property @property