375 lines
12 KiB
Python
375 lines
12 KiB
Python
|
|
# ================================================================
|
||
|
|
# services/inventory/app/api/sustainability.py
|
||
|
|
# ================================================================
|
||
|
|
"""
|
||
|
|
Sustainability API endpoints for Environmental Impact & SDG Compliance
|
||
|
|
Following standardized URL structure: /api/v1/tenants/{tenant_id}/sustainability/{operation}
|
||
|
|
"""
|
||
|
|
|
||
|
|
from datetime import datetime, timedelta
|
||
|
|
from typing import Optional
|
||
|
|
from uuid import UUID
|
||
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Path, status
|
||
|
|
from fastapi.responses import JSONResponse
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
import structlog
|
||
|
|
|
||
|
|
from shared.auth.decorators import get_current_user_dep
|
||
|
|
from app.core.database import get_db
|
||
|
|
from app.services.sustainability_service import SustainabilityService
|
||
|
|
from app.schemas.sustainability import (
|
||
|
|
SustainabilityMetrics,
|
||
|
|
GrantReport,
|
||
|
|
SustainabilityWidgetData,
|
||
|
|
SustainabilityMetricsRequest,
|
||
|
|
GrantReportRequest
|
||
|
|
)
|
||
|
|
from shared.routing import RouteBuilder
|
||
|
|
|
||
|
|
logger = structlog.get_logger()
|
||
|
|
|
||
|
|
# Create route builder for consistent URL structure
|
||
|
|
route_builder = RouteBuilder('sustainability')
|
||
|
|
|
||
|
|
router = APIRouter(tags=["sustainability"])
|
||
|
|
|
||
|
|
|
||
|
|
# ===== Dependency Injection =====
|
||
|
|
|
||
|
|
async def get_sustainability_service() -> SustainabilityService:
|
||
|
|
"""Get sustainability service instance"""
|
||
|
|
return SustainabilityService()
|
||
|
|
|
||
|
|
|
||
|
|
# ===== SUSTAINABILITY ENDPOINTS =====
|
||
|
|
|
||
|
|
@router.get(
|
||
|
|
"/api/v1/tenants/{tenant_id}/sustainability/metrics",
|
||
|
|
response_model=SustainabilityMetrics,
|
||
|
|
summary="Get Sustainability Metrics",
|
||
|
|
description="Get comprehensive sustainability metrics including environmental impact, SDG compliance, and grant readiness"
|
||
|
|
)
|
||
|
|
async def get_sustainability_metrics(
|
||
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||
|
|
start_date: Optional[datetime] = Query(None, description="Start date for metrics (default: 30 days ago)"),
|
||
|
|
end_date: Optional[datetime] = Query(None, description="End date for metrics (default: now)"),
|
||
|
|
current_user: dict = Depends(get_current_user_dep),
|
||
|
|
sustainability_service: SustainabilityService = Depends(get_sustainability_service),
|
||
|
|
db: AsyncSession = Depends(get_db)
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Get comprehensive sustainability metrics for the tenant.
|
||
|
|
|
||
|
|
**Includes:**
|
||
|
|
- Food waste metrics (production, inventory, total)
|
||
|
|
- Environmental impact (CO2, water, land use)
|
||
|
|
- UN SDG 12.3 compliance tracking
|
||
|
|
- Waste avoided through AI predictions
|
||
|
|
- Financial impact analysis
|
||
|
|
- Grant program eligibility assessment
|
||
|
|
|
||
|
|
**Use cases:**
|
||
|
|
- Dashboard displays
|
||
|
|
- Grant applications
|
||
|
|
- Sustainability reporting
|
||
|
|
- Compliance verification
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
metrics = await sustainability_service.get_sustainability_metrics(
|
||
|
|
db=db,
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
start_date=start_date,
|
||
|
|
end_date=end_date
|
||
|
|
)
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"Sustainability metrics retrieved",
|
||
|
|
tenant_id=str(tenant_id),
|
||
|
|
user_id=current_user.get('user_id'),
|
||
|
|
waste_reduction=metrics.get('sdg_compliance', {}).get('sdg_12_3', {}).get('reduction_achieved', 0)
|
||
|
|
)
|
||
|
|
|
||
|
|
return metrics
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Error getting sustainability metrics",
|
||
|
|
tenant_id=str(tenant_id),
|
||
|
|
error=str(e)
|
||
|
|
)
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
|
|
detail=f"Failed to retrieve sustainability metrics: {str(e)}"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@router.get(
|
||
|
|
"/api/v1/tenants/{tenant_id}/sustainability/widget",
|
||
|
|
response_model=SustainabilityWidgetData,
|
||
|
|
summary="Get Sustainability Widget Data",
|
||
|
|
description="Get simplified sustainability data optimized for dashboard widgets"
|
||
|
|
)
|
||
|
|
async def get_sustainability_widget_data(
|
||
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||
|
|
days: int = Query(30, ge=1, le=365, description="Number of days to analyze"),
|
||
|
|
current_user: dict = Depends(get_current_user_dep),
|
||
|
|
sustainability_service: SustainabilityService = Depends(get_sustainability_service),
|
||
|
|
db: AsyncSession = Depends(get_db)
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Get simplified sustainability metrics for dashboard widgets.
|
||
|
|
|
||
|
|
**Optimized for:**
|
||
|
|
- Dashboard displays
|
||
|
|
- Quick overview cards
|
||
|
|
- Real-time monitoring
|
||
|
|
|
||
|
|
**Returns:**
|
||
|
|
- Key metrics only
|
||
|
|
- Human-readable values
|
||
|
|
- Status indicators
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
end_date = datetime.now()
|
||
|
|
start_date = end_date - timedelta(days=days)
|
||
|
|
|
||
|
|
metrics = await sustainability_service.get_sustainability_metrics(
|
||
|
|
db=db,
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
start_date=start_date,
|
||
|
|
end_date=end_date
|
||
|
|
)
|
||
|
|
|
||
|
|
# Extract widget-friendly data
|
||
|
|
widget_data = {
|
||
|
|
'total_waste_kg': metrics['waste_metrics']['total_waste_kg'],
|
||
|
|
'waste_reduction_percentage': metrics['sdg_compliance']['sdg_12_3']['reduction_achieved'],
|
||
|
|
'co2_saved_kg': metrics['environmental_impact']['co2_emissions']['kg'],
|
||
|
|
'water_saved_liters': metrics['environmental_impact']['water_footprint']['liters'],
|
||
|
|
'trees_equivalent': metrics['environmental_impact']['co2_emissions']['trees_to_offset'],
|
||
|
|
'sdg_status': metrics['sdg_compliance']['sdg_12_3']['status'],
|
||
|
|
'sdg_progress': metrics['sdg_compliance']['sdg_12_3']['progress_to_target'],
|
||
|
|
'grant_programs_ready': len(metrics['grant_readiness']['recommended_applications']),
|
||
|
|
'financial_savings_eur': metrics['financial_impact']['waste_cost_eur']
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"Widget data retrieved",
|
||
|
|
tenant_id=str(tenant_id),
|
||
|
|
user_id=current_user.get('user_id')
|
||
|
|
)
|
||
|
|
|
||
|
|
return widget_data
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Error getting widget data",
|
||
|
|
tenant_id=str(tenant_id),
|
||
|
|
error=str(e)
|
||
|
|
)
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
|
|
detail=f"Failed to retrieve widget data: {str(e)}"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@router.post(
|
||
|
|
"/api/v1/tenants/{tenant_id}/sustainability/export/grant-report",
|
||
|
|
response_model=GrantReport,
|
||
|
|
summary="Export Grant Application Report",
|
||
|
|
description="Generate a comprehensive report formatted for grant applications"
|
||
|
|
)
|
||
|
|
async def export_grant_report(
|
||
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||
|
|
request: GrantReportRequest = None,
|
||
|
|
current_user: dict = Depends(get_current_user_dep),
|
||
|
|
sustainability_service: SustainabilityService = Depends(get_sustainability_service),
|
||
|
|
db: AsyncSession = Depends(get_db)
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Generate comprehensive grant application report.
|
||
|
|
|
||
|
|
**Supported grant types:**
|
||
|
|
- `general`: General sustainability report
|
||
|
|
- `eu_horizon`: EU Horizon Europe format
|
||
|
|
- `farm_to_fork`: EU Farm to Fork Strategy
|
||
|
|
- `circular_economy`: Circular Economy grants
|
||
|
|
- `un_sdg`: UN SDG certification
|
||
|
|
|
||
|
|
**Export formats:**
|
||
|
|
- `json`: JSON format (default)
|
||
|
|
- `pdf`: PDF document (future)
|
||
|
|
- `csv`: CSV export (future)
|
||
|
|
|
||
|
|
**Use cases:**
|
||
|
|
- Grant applications
|
||
|
|
- Compliance reporting
|
||
|
|
- Investor presentations
|
||
|
|
- Certification requests
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
if request is None:
|
||
|
|
request = GrantReportRequest()
|
||
|
|
|
||
|
|
report = await sustainability_service.export_grant_report(
|
||
|
|
db=db,
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
grant_type=request.grant_type,
|
||
|
|
start_date=request.start_date,
|
||
|
|
end_date=request.end_date
|
||
|
|
)
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"Grant report exported",
|
||
|
|
tenant_id=str(tenant_id),
|
||
|
|
grant_type=request.grant_type,
|
||
|
|
user_id=current_user.get('user_id')
|
||
|
|
)
|
||
|
|
|
||
|
|
# For now, return JSON. In future, support PDF/CSV generation
|
||
|
|
if request.format == 'json':
|
||
|
|
return report
|
||
|
|
else:
|
||
|
|
# Future: Generate PDF or CSV
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||
|
|
detail=f"Export format '{request.format}' not yet implemented. Use 'json' for now."
|
||
|
|
)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Error exporting grant report",
|
||
|
|
tenant_id=str(tenant_id),
|
||
|
|
error=str(e)
|
||
|
|
)
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
|
|
detail=f"Failed to export grant report: {str(e)}"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@router.get(
|
||
|
|
"/api/v1/tenants/{tenant_id}/sustainability/sdg-compliance",
|
||
|
|
summary="Get SDG 12.3 Compliance Status",
|
||
|
|
description="Get detailed UN SDG 12.3 compliance status and progress"
|
||
|
|
)
|
||
|
|
async def get_sdg_compliance(
|
||
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||
|
|
current_user: dict = Depends(get_current_user_dep),
|
||
|
|
sustainability_service: SustainabilityService = Depends(get_sustainability_service),
|
||
|
|
db: AsyncSession = Depends(get_db)
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Get detailed UN SDG 12.3 compliance information.
|
||
|
|
|
||
|
|
**SDG 12.3 Target:**
|
||
|
|
By 2030, halve per capita global food waste at the retail and consumer levels
|
||
|
|
and reduce food losses along production and supply chains, including post-harvest losses.
|
||
|
|
|
||
|
|
**Returns:**
|
||
|
|
- Current compliance status
|
||
|
|
- Progress toward 50% reduction target
|
||
|
|
- Baseline comparison
|
||
|
|
- Certification readiness
|
||
|
|
- Improvement recommendations
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
metrics = await sustainability_service.get_sustainability_metrics(
|
||
|
|
db=db,
|
||
|
|
tenant_id=tenant_id
|
||
|
|
)
|
||
|
|
|
||
|
|
sdg_data = {
|
||
|
|
'sdg_12_3_compliance': metrics['sdg_compliance']['sdg_12_3'],
|
||
|
|
'baseline_period': metrics['sdg_compliance']['baseline_period'],
|
||
|
|
'certification_ready': metrics['sdg_compliance']['certification_ready'],
|
||
|
|
'improvement_areas': metrics['sdg_compliance']['improvement_areas'],
|
||
|
|
'current_waste': metrics['waste_metrics'],
|
||
|
|
'environmental_impact': metrics['environmental_impact']
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"SDG compliance data retrieved",
|
||
|
|
tenant_id=str(tenant_id),
|
||
|
|
status=sdg_data['sdg_12_3_compliance']['status']
|
||
|
|
)
|
||
|
|
|
||
|
|
return sdg_data
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Error getting SDG compliance",
|
||
|
|
tenant_id=str(tenant_id),
|
||
|
|
error=str(e)
|
||
|
|
)
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
|
|
detail=f"Failed to retrieve SDG compliance data: {str(e)}"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@router.get(
|
||
|
|
"/api/v1/tenants/{tenant_id}/sustainability/environmental-impact",
|
||
|
|
summary="Get Environmental Impact",
|
||
|
|
description="Get detailed environmental impact metrics"
|
||
|
|
)
|
||
|
|
async def get_environmental_impact(
|
||
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||
|
|
days: int = Query(30, ge=1, le=365, description="Number of days to analyze"),
|
||
|
|
current_user: dict = Depends(get_current_user_dep),
|
||
|
|
sustainability_service: SustainabilityService = Depends(get_sustainability_service),
|
||
|
|
db: AsyncSession = Depends(get_db)
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Get detailed environmental impact of food waste.
|
||
|
|
|
||
|
|
**Metrics included:**
|
||
|
|
- CO2 emissions (kg and tons)
|
||
|
|
- Water footprint (liters and cubic meters)
|
||
|
|
- Land use (m² and hectares)
|
||
|
|
- Human-relatable equivalents (car km, showers, etc.)
|
||
|
|
|
||
|
|
**Use cases:**
|
||
|
|
- Sustainability reports
|
||
|
|
- Marketing materials
|
||
|
|
- Customer communication
|
||
|
|
- ESG reporting
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
end_date = datetime.now()
|
||
|
|
start_date = end_date - timedelta(days=days)
|
||
|
|
|
||
|
|
metrics = await sustainability_service.get_sustainability_metrics(
|
||
|
|
db=db,
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
start_date=start_date,
|
||
|
|
end_date=end_date
|
||
|
|
)
|
||
|
|
|
||
|
|
impact_data = {
|
||
|
|
'period': metrics['period'],
|
||
|
|
'waste_metrics': metrics['waste_metrics'],
|
||
|
|
'environmental_impact': metrics['environmental_impact'],
|
||
|
|
'avoided_impact': metrics['avoided_waste']['environmental_impact_avoided'],
|
||
|
|
'financial_impact': metrics['financial_impact']
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"Environmental impact data retrieved",
|
||
|
|
tenant_id=str(tenant_id),
|
||
|
|
co2_kg=impact_data['environmental_impact']['co2_emissions']['kg']
|
||
|
|
)
|
||
|
|
|
||
|
|
return impact_data
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Error getting environmental impact",
|
||
|
|
tenant_id=str(tenant_id),
|
||
|
|
error=str(e)
|
||
|
|
)
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
|
|
detail=f"Failed to retrieve environmental impact: {str(e)}"
|
||
|
|
)
|