Fix AI insights feature issues
This commit is contained in:
@@ -45,3 +45,55 @@ async def get_sales_analytics(
|
||||
except Exception as e:
|
||||
logger.error("Failed to get sales analytics", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get sales analytics: {str(e)}")
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_analytics_route("products/{product_id}/demand-patterns")
|
||||
)
|
||||
@analytics_tier_required
|
||||
async def get_product_demand_patterns(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
product_id: UUID = Path(..., description="Product ID (inventory_product_id)"),
|
||||
start_date: Optional[datetime] = Query(None, description="Start date for analysis"),
|
||||
end_date: Optional[datetime] = Query(None, description="End date for analysis"),
|
||||
min_history_days: int = Query(90, description="Minimum days of history required", ge=30, le=365),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
sales_service: SalesService = Depends(get_sales_service)
|
||||
):
|
||||
"""
|
||||
Analyze demand patterns for a specific product (Professional+ tier required).
|
||||
|
||||
Returns:
|
||||
- Demand trends (increasing/decreasing/stable)
|
||||
- Volatility metrics (coefficient of variation)
|
||||
- Weekly seasonal patterns
|
||||
- Peak/low demand days
|
||||
- Statistical summaries
|
||||
"""
|
||||
try:
|
||||
patterns = await sales_service.analyze_product_demand_patterns(
|
||||
tenant_id=tenant_id,
|
||||
inventory_product_id=product_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
min_history_days=min_history_days
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Retrieved product demand patterns",
|
||||
tenant_id=tenant_id,
|
||||
product_id=product_id
|
||||
)
|
||||
return patterns
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to get product demand patterns",
|
||||
error=str(e),
|
||||
tenant_id=tenant_id,
|
||||
product_id=product_id
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to analyze demand patterns: {str(e)}"
|
||||
)
|
||||
|
||||
@@ -501,15 +501,157 @@ class SalesService:
|
||||
error=str(e), product_id=product_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
async def get_inventory_products_by_category(self, category: str, tenant_id: UUID,
|
||||
async def get_inventory_products_by_category(self, category: str, tenant_id: UUID,
|
||||
product_type: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""Get products by category from inventory service"""
|
||||
try:
|
||||
products = await self.inventory_client.get_products_by_category(category, tenant_id, product_type)
|
||||
logger.info("Retrieved inventory products by category", category=category,
|
||||
logger.info("Retrieved inventory products by category", category=category,
|
||||
count=len(products), tenant_id=tenant_id)
|
||||
return products
|
||||
except Exception as e:
|
||||
logger.error("Failed to get inventory products by category",
|
||||
logger.error("Failed to get inventory products by category",
|
||||
error=str(e), category=category, tenant_id=tenant_id)
|
||||
return []
|
||||
return []
|
||||
|
||||
async def analyze_product_demand_patterns(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
inventory_product_id: UUID,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
min_history_days: int = 90
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze demand patterns for a specific product from historical sales data.
|
||||
|
||||
This method provides insights on:
|
||||
- Demand trends (increasing/decreasing)
|
||||
- Volatility (coefficient of variation)
|
||||
- Weekly seasonal patterns
|
||||
- Peak/low demand days
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant identifier
|
||||
inventory_product_id: Product identifier
|
||||
start_date: Start date for analysis (optional)
|
||||
end_date: End date for analysis (optional)
|
||||
min_history_days: Minimum days of history required
|
||||
|
||||
Returns:
|
||||
Analysis results with patterns, trends, and statistics
|
||||
"""
|
||||
try:
|
||||
import pandas as pd
|
||||
|
||||
logger.info(
|
||||
"Analyzing product demand patterns",
|
||||
tenant_id=tenant_id,
|
||||
inventory_product_id=inventory_product_id
|
||||
)
|
||||
|
||||
# Fetch sales data for the product
|
||||
sales_records = await self.get_product_sales(
|
||||
tenant_id=tenant_id,
|
||||
inventory_product_id=inventory_product_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
if not sales_records or len(sales_records) < min_history_days:
|
||||
return {
|
||||
'analyzed_at': datetime.utcnow().isoformat(),
|
||||
'history_days': len(sales_records) if sales_records else 0,
|
||||
'patterns': {},
|
||||
'trend_analysis': {},
|
||||
'seasonal_factors': {},
|
||||
'statistics': {},
|
||||
'error': f'Insufficient historical data (need {min_history_days} days, got {len(sales_records) if sales_records else 0})'
|
||||
}
|
||||
|
||||
# Convert to DataFrame for analysis
|
||||
sales_data = pd.DataFrame([{
|
||||
'date': record.date,
|
||||
'quantity': record.quantity_sold,
|
||||
'revenue': float(record.revenue) if record.revenue else 0
|
||||
} for record in sales_records])
|
||||
|
||||
sales_data['date'] = pd.to_datetime(sales_data['date'])
|
||||
sales_data = sales_data.sort_values('date')
|
||||
|
||||
# Calculate basic statistics
|
||||
mean_demand = sales_data['quantity'].mean()
|
||||
std_demand = sales_data['quantity'].std()
|
||||
cv = (std_demand / mean_demand) if mean_demand > 0 else 0
|
||||
|
||||
# Trend analysis
|
||||
sales_data['days_since_start'] = (sales_data['date'] - sales_data['date'].min()).dt.days
|
||||
trend_correlation = sales_data['days_since_start'].corr(sales_data['quantity'])
|
||||
is_increasing = trend_correlation > 0.2
|
||||
is_decreasing = trend_correlation < -0.2
|
||||
|
||||
# Seasonal pattern detection (day of week)
|
||||
sales_data['day_of_week'] = sales_data['date'].dt.dayofweek
|
||||
weekly_pattern = sales_data.groupby('day_of_week')['quantity'].mean().to_dict()
|
||||
peak_day = max(weekly_pattern, key=weekly_pattern.get)
|
||||
low_day = min(weekly_pattern, key=weekly_pattern.get)
|
||||
peak_ratio = weekly_pattern[peak_day] / weekly_pattern[low_day] if weekly_pattern[low_day] > 0 else 1.0
|
||||
|
||||
logger.info(
|
||||
"Demand pattern analysis complete",
|
||||
tenant_id=tenant_id,
|
||||
inventory_product_id=inventory_product_id,
|
||||
data_points=len(sales_data),
|
||||
trend_direction='increasing' if is_increasing else 'decreasing' if is_decreasing else 'stable'
|
||||
)
|
||||
|
||||
return {
|
||||
'analyzed_at': datetime.utcnow().isoformat(),
|
||||
'history_days': len(sales_data),
|
||||
'date_range': {
|
||||
'start': sales_data['date'].min().isoformat(),
|
||||
'end': sales_data['date'].max().isoformat()
|
||||
},
|
||||
'statistics': {
|
||||
'mean_demand': round(mean_demand, 2),
|
||||
'std_demand': round(std_demand, 2),
|
||||
'coefficient_of_variation': round(cv, 3),
|
||||
'total_quantity': round(sales_data['quantity'].sum(), 2),
|
||||
'total_revenue': round(sales_data['revenue'].sum(), 2),
|
||||
'min_demand': round(sales_data['quantity'].min(), 2),
|
||||
'max_demand': round(sales_data['quantity'].max(), 2)
|
||||
},
|
||||
'trend_analysis': {
|
||||
'correlation': round(trend_correlation, 3),
|
||||
'is_increasing': is_increasing,
|
||||
'is_decreasing': is_decreasing,
|
||||
'direction': 'increasing' if is_increasing else 'decreasing' if is_decreasing else 'stable'
|
||||
},
|
||||
'patterns': {
|
||||
'weekly_pattern': {str(k): round(v, 2) for k, v in weekly_pattern.items()},
|
||||
'peak_day': int(peak_day),
|
||||
'low_day': int(low_day)
|
||||
},
|
||||
'seasonal_factors': {
|
||||
'peak_ratio': round(peak_ratio, 2),
|
||||
'has_strong_pattern': peak_ratio > 1.5
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error analyzing product demand patterns",
|
||||
tenant_id=tenant_id,
|
||||
inventory_product_id=inventory_product_id,
|
||||
error=str(e),
|
||||
exc_info=True
|
||||
)
|
||||
return {
|
||||
'analyzed_at': datetime.utcnow().isoformat(),
|
||||
'history_days': 0,
|
||||
'patterns': {},
|
||||
'trend_analysis': {},
|
||||
'seasonal_factors': {},
|
||||
'statistics': {},
|
||||
'error': str(e)
|
||||
}
|
||||
Reference in New Issue
Block a user