Files
bakery-ia/services/inventory/app/services/sustainability_service.py
2025-12-15 13:39:33 +01:00

883 lines
37 KiB
Python

# ================================================================
# services/inventory/app/services/sustainability_service.py
# ================================================================
"""
Sustainability Service - Environmental Impact & SDG Compliance Tracking
Aligned with UN SDG 12.3 and EU Farm to Fork Strategy
"""
from datetime import datetime, timedelta
from decimal import Decimal
from typing import Dict, Any, Optional, List
from uuid import UUID
import structlog
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.repositories.stock_movement_repository import StockMovementRepository
from app.repositories.food_safety_repository import FoodSafetyRepository
from shared.clients.production_client import create_production_client
logger = structlog.get_logger()
# Environmental Impact Constants (Research-based averages for bakery products)
class EnvironmentalConstants:
"""Environmental impact factors for bakery production"""
# CO2 equivalent per kg of food waste (kg CO2e/kg)
# Source: EU Commission, average for baked goods
CO2_PER_KG_WASTE = 1.9
# Water footprint (liters per kg of ingredient)
WATER_FOOTPRINT = {
'flour': 1827, # Wheat flour
'dairy': 1020, # Average dairy products
'eggs': 3265, # Eggs
'sugar': 1782, # Sugar
'yeast': 500, # Estimated for yeast
'fats': 1600, # Butter/oils average
'default': 1500 # Conservative default
}
# Land use per kg (m² per kg)
LAND_USE_PER_KG = 3.4
# Average trees needed to offset 1 ton CO2
TREES_PER_TON_CO2 = 50
# EU bakery waste baseline (average industry waste %)
EU_BAKERY_BASELINE_WASTE = 0.25 # 25% average
# UN SDG 12.3 target: 50% reduction by 2030
SDG_TARGET_REDUCTION = 0.50
class SustainabilityService:
"""Service for calculating environmental impact and SDG compliance"""
def __init__(self):
pass
async def get_sustainability_metrics(
self,
db: AsyncSession,
tenant_id: UUID,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None
) -> Dict[str, Any]:
"""
Get comprehensive sustainability metrics for a tenant
Returns metrics aligned with:
- UN SDG 12.3 (Food waste reduction)
- EU Farm to Fork Strategy
- Green Deal objectives
"""
try:
# Default to last 30 days if no date range provided
if not end_date:
end_date = datetime.now()
if not start_date:
start_date = end_date - timedelta(days=30)
# Get waste data from production and inventory
waste_data = await self._get_waste_data(db, tenant_id, start_date, end_date)
# Check if there's sufficient data for meaningful calculations
# Minimum: 50kg production to avoid false metrics on empty accounts
total_production = waste_data['total_production_kg']
has_sufficient_data = total_production >= 50.0
logger.info(
"Checking data sufficiency for sustainability metrics",
tenant_id=str(tenant_id),
total_production=total_production,
has_sufficient_data=has_sufficient_data
)
# If insufficient data, return a special "collecting data" state
if not has_sufficient_data:
return self._get_insufficient_data_response(start_date, end_date, waste_data)
# Calculate environmental impact
environmental_impact = self._calculate_environmental_impact(waste_data)
# Calculate SDG compliance
sdg_compliance = await self._calculate_sdg_compliance(
db, tenant_id, waste_data, start_date, end_date
)
# Calculate avoided waste (through AI predictions)
avoided_waste = await self._calculate_avoided_waste(
db, tenant_id, start_date, end_date
)
# Calculate financial impact
financial_impact = self._calculate_financial_impact(waste_data)
return {
'period': {
'start_date': start_date.isoformat(),
'end_date': end_date.isoformat(),
'days': (end_date - start_date).days
},
'waste_metrics': {
'total_waste_kg': waste_data['total_waste_kg'],
'production_waste_kg': waste_data['production_waste_kg'],
'expired_waste_kg': waste_data['expired_waste_kg'],
'waste_percentage': waste_data['waste_percentage'],
'waste_by_reason': waste_data['waste_by_reason']
},
'environmental_impact': environmental_impact,
'sdg_compliance': sdg_compliance,
'avoided_waste': avoided_waste,
'financial_impact': financial_impact,
'grant_readiness': self._assess_grant_readiness(sdg_compliance),
'data_sufficient': True
}
except Exception as e:
logger.error("Failed to calculate sustainability metrics",
tenant_id=str(tenant_id), error=str(e))
raise
def _get_insufficient_data_response(
self,
start_date: datetime,
end_date: datetime,
waste_data: Dict[str, Any]
) -> Dict[str, Any]:
"""
Return response for tenants with insufficient data
This prevents showing misleading "100% compliant" status for empty accounts
"""
return {
'period': {
'start_date': start_date.isoformat(),
'end_date': end_date.isoformat(),
'days': (end_date - start_date).days
},
'waste_metrics': {
'total_waste_kg': 0.0,
'production_waste_kg': 0.0,
'expired_waste_kg': 0.0,
'waste_percentage': 0.0,
'waste_by_reason': {}
},
'environmental_impact': {
'co2_emissions': {
'kg': 0.0,
'tons': 0.0,
'trees_to_offset': 0.0
},
'water_footprint': {
'liters': 0.0,
'cubic_meters': 0.0
},
'land_use': {
'square_meters': 0.0,
'hectares': 0.0
},
'human_equivalents': {
'car_km_equivalent': 0.0,
'smartphone_charges': 0.0,
'showers_equivalent': 0.0,
'trees_planted': 0.0
}
},
'sdg_compliance': {
'sdg_12_3': {
'baseline_waste_percentage': 0.0,
'current_waste_percentage': 0.0,
'reduction_achieved': 0.0,
'target_reduction': 50.0,
'progress_to_target': 0.0,
'status': 'insufficient_data',
'status_label': 'Collecting Baseline Data',
'target_waste_percentage': 0.0
},
'baseline_period': 'not_available',
'certification_ready': False,
'improvement_areas': ['start_production_tracking']
},
'avoided_waste': {
'waste_avoided_kg': 0.0,
'ai_assisted_batches': 0,
'environmental_impact_avoided': {
'co2_kg': 0.0,
'water_liters': 0.0
},
'methodology': 'insufficient_data'
},
'financial_impact': {
'waste_cost_eur': 0.0,
'cost_per_kg': 3.50,
'potential_monthly_savings': 0.0,
'annual_projection': 0.0
},
'grant_readiness': {
'overall_readiness_percentage': 0.0,
'grant_programs': {
'life_circular_economy': {
'eligible': False,
'confidence': 'low',
'requirements_met': False,
'funding_eur': 73_000_000,
'deadline': '2025-09-23',
'program_type': 'grant'
},
'horizon_europe_cluster_6': {
'eligible': False,
'confidence': 'low',
'requirements_met': False,
'funding_eur': 880_000_000,
'deadline': 'rolling_2025',
'program_type': 'grant'
},
'fedima_sustainability_grant': {
'eligible': False,
'confidence': 'low',
'requirements_met': False,
'funding_eur': 20_000,
'deadline': '2025-06-30',
'program_type': 'grant',
'sector_specific': 'bakery'
},
'eit_food_retail': {
'eligible': False,
'confidence': 'low',
'requirements_met': False,
'funding_eur': 45_000,
'deadline': 'rolling',
'program_type': 'grant',
'sector_specific': 'retail'
},
'un_sdg_certified': {
'eligible': False,
'confidence': 'low',
'requirements_met': False,
'funding_eur': 0,
'deadline': 'ongoing',
'program_type': 'certification'
}
},
'recommended_applications': [],
'spain_compliance': {
'law_1_2025': False,
'circular_economy_strategy': False
}
},
'data_sufficient': False,
'minimum_production_required_kg': 50.0,
'current_production_kg': waste_data['total_production_kg']
}
async def _get_waste_data(
self,
db: AsyncSession,
tenant_id: UUID,
start_date: datetime,
end_date: datetime
) -> Dict[str, Any]:
"""Get waste data from production service and inventory"""
try:
# Get production waste data via HTTP call to production service
production_waste_data = await self._get_production_waste_data(
tenant_id, start_date, end_date
)
prod_data = production_waste_data if production_waste_data else {
'total_production_waste': 0,
'total_defects': 0,
'total_planned': 0,
'total_actual': 0
}
# Query inventory waste using repository
stock_movement_repo = StockMovementRepository(db)
inventory_waste = await stock_movement_repo.get_inventory_waste_total(
tenant_id=tenant_id,
start_date=start_date,
end_date=end_date
)
# Calculate totals
production_waste = float(prod_data.get('total_production_waste', 0) or 0)
defect_waste = float(prod_data.get('total_defects', 0) or 0)
total_waste = production_waste + defect_waste + inventory_waste
total_production = float(prod_data.get('total_planned', 0) or 0)
waste_percentage = (total_waste / total_production * 100) if total_production > 0 else 0
# Categorize waste by reason
waste_by_reason = {
'production_defects': defect_waste,
'production_waste': production_waste - defect_waste,
'expired_inventory': inventory_waste * 0.7, # Estimate: 70% expires
'damaged_inventory': inventory_waste * 0.3, # Estimate: 30% damaged
}
# Count waste incidents from stock movements
total_waste_incidents = 0
try:
# Calculate days back from start_date to now
days_back = (end_date - start_date).days if start_date and end_date else 30
waste_movements = await stock_movement_repo.get_waste_movements(
tenant_id=tenant_id,
days_back=days_back,
limit=1000 # Get all waste movements
)
total_waste_incidents = len(waste_movements) if waste_movements else 0
except Exception as e:
logger.warning("Could not get waste incidents count", error=str(e))
total_waste_incidents = 0
return {
'total_waste_kg': total_waste,
'production_waste_kg': production_waste + defect_waste,
'expired_waste_kg': inventory_waste,
'waste_percentage': waste_percentage,
'total_production_kg': total_production,
'waste_by_reason': waste_by_reason,
'waste_incidents': total_waste_incidents
}
except Exception as e:
logger.error("Failed to get waste data", error=str(e))
raise
async def _get_production_waste_data(
self,
tenant_id: UUID,
start_date: datetime,
end_date: datetime
) -> Optional[Dict[str, Any]]:
"""Get production waste data from production service using shared client"""
try:
# Use the shared production client with proper authentication and resilience
production_client = create_production_client(settings)
data = await production_client.get_waste_analytics(
str(tenant_id),
start_date.isoformat(),
end_date.isoformat()
)
if data:
logger.info(
"Retrieved production waste data via production client",
tenant_id=str(tenant_id),
total_waste=data.get('total_production_waste', 0)
)
return data
else:
# Client returned None, return zeros as fallback
logger.warning(
"Production waste analytics returned None, using zeros",
tenant_id=str(tenant_id)
)
return {
'total_production_waste': 0,
'total_defects': 0,
'total_planned': 0,
'total_actual': 0
}
except Exception as e:
logger.error(
"Error calling production service for waste data via client",
error=str(e),
tenant_id=str(tenant_id)
)
# Return zeros on error to not break the flow
return {
'total_production_waste': 0,
'total_defects': 0,
'total_planned': 0,
'total_actual': 0
}
def _calculate_environmental_impact(self, waste_data: Dict[str, Any]) -> Dict[str, Any]:
"""Calculate environmental impact of food waste"""
try:
total_waste_kg = waste_data['total_waste_kg']
# CO2 emissions
co2_emissions_kg = total_waste_kg * EnvironmentalConstants.CO2_PER_KG_WASTE
co2_emissions_tons = co2_emissions_kg / 1000
# Equivalent trees to offset
trees_equivalent = co2_emissions_tons * EnvironmentalConstants.TREES_PER_TON_CO2
# Water footprint (using average for bakery products)
water_liters = total_waste_kg * EnvironmentalConstants.WATER_FOOTPRINT['default']
# Land use
land_use_m2 = total_waste_kg * EnvironmentalConstants.LAND_USE_PER_KG
# Human-readable equivalents for marketing
equivalents = {
'car_km': co2_emissions_kg / 0.12, # Average car emits 120g CO2/km
'smartphone_charges': (co2_emissions_kg * 1000) / 8, # 8g CO2 per charge
'showers': water_liters / 65, # Average shower uses 65L
'trees_year_growth': trees_equivalent
}
return {
'co2_emissions': {
'kg': round(co2_emissions_kg, 2),
'tons': round(co2_emissions_tons, 4),
'trees_to_offset': round(trees_equivalent, 1)
},
'water_footprint': {
'liters': round(water_liters, 2),
'cubic_meters': round(water_liters / 1000, 2)
},
'land_use': {
'square_meters': round(land_use_m2, 2),
'hectares': round(land_use_m2 / 10000, 4)
},
'human_equivalents': {
'car_km_equivalent': round(equivalents['car_km'], 0),
'smartphone_charges': round(equivalents['smartphone_charges'], 0),
'showers_equivalent': round(equivalents['showers'], 0),
'trees_planted': round(equivalents['trees_year_growth'], 1)
}
}
except Exception as e:
logger.error("Failed to calculate environmental impact", error=str(e))
raise
async def _calculate_sdg_compliance(
self,
db: AsyncSession,
tenant_id: UUID,
waste_data: Dict[str, Any],
start_date: datetime,
end_date: datetime
) -> Dict[str, Any]:
"""
Calculate compliance with UN SDG 12.3
Target: Halve per capita global food waste by 2030
IMPORTANT: This method assumes sufficient data validation was done upstream.
It should only be called when waste_data has meaningful production volumes.
"""
try:
# Get baseline (first 90 days of operation)
baseline = await self._get_baseline_waste(db, tenant_id)
current_waste_percentage = waste_data['waste_percentage']
total_production = waste_data['total_production_kg']
# Check if we have a real baseline from production history
has_real_baseline = baseline.get('data_available', False)
baseline_percentage = baseline.get('waste_percentage', 0.0)
# If no real baseline AND insufficient current production, we can't make comparisons
if not has_real_baseline and total_production < 50:
logger.warning(
"Cannot calculate SDG compliance without baseline or sufficient production",
tenant_id=str(tenant_id),
total_production=total_production
)
return self._get_insufficient_sdg_data()
# If we have no real baseline but have current production, use it as baseline
if not has_real_baseline:
logger.info(
"Using current period as baseline (no historical data available)",
tenant_id=str(tenant_id),
current_waste_percentage=current_waste_percentage
)
baseline_percentage = current_waste_percentage
# Set reduction to 0 since we're establishing baseline
reduction_percentage = 0
progress_to_target = 0
status = 'baseline'
status_label = 'Establishing Baseline'
else:
# We have a real baseline - calculate actual reduction
# If current waste is higher than baseline, show negative reduction (worse than baseline)
# If current waste is lower than baseline, show positive reduction (better than baseline)
if baseline_percentage > 0:
reduction_percentage = ((baseline_percentage - current_waste_percentage) / baseline_percentage) * 100
else:
reduction_percentage = 0
# Calculate progress toward 50% reduction target
# The target is to achieve 50% reduction from baseline
# So if baseline is 25%, target is to reach 12.5% (25% * 0.5)
target_reduction_percentage = 50.0
target_waste_percentage = baseline_percentage * (1 - (target_reduction_percentage / 100))
# Calculate progress: how much of the 50% target has been achieved
# If we've reduced from 25% to 19.28%, we've achieved (25-19.28)/(25-12.5) = 5.72/12.5 = 45.8% of target
if baseline_percentage > target_waste_percentage:
max_possible_reduction = baseline_percentage - target_waste_percentage
actual_reduction = baseline_percentage - current_waste_percentage
progress_to_target = (actual_reduction / max_possible_reduction) * 100 if max_possible_reduction > 0 else 0
else:
# If current is already better than target
progress_to_target = 100.0 if current_waste_percentage <= target_waste_percentage else 0.0
# Ensure progress doesn't exceed 100%
progress_to_target = min(progress_to_target, 100.0)
# Status assessment based on actual reduction achieved
if reduction_percentage >= 50:
status = 'sdg_compliant'
status_label = 'SDG 12.3 Compliant'
elif reduction_percentage >= 30:
status = 'on_track'
status_label = 'On Track to Compliance'
elif reduction_percentage >= 10:
status = 'progressing'
status_label = 'Making Progress'
elif reduction_percentage > 0:
status = 'improving'
status_label = 'Improving'
elif reduction_percentage < 0:
status = 'above_baseline'
status_label = 'Above Baseline'
else:
status = 'baseline'
status_label = 'At Baseline'
return {
'sdg_12_3': {
'baseline_waste_percentage': round(baseline_percentage, 2),
'current_waste_percentage': round(current_waste_percentage, 2),
'reduction_achieved': round(reduction_percentage, 2),
'target_reduction': 50.0,
'progress_to_target': round(max(progress_to_target, 0), 1), # Ensure non-negative
'status': status,
'status_label': status_label,
'target_waste_percentage': round(baseline_percentage * 0.5, 2) if baseline_percentage > 0 else 0.0
},
'baseline_period': baseline.get('period', 'current_period'),
'certification_ready': reduction_percentage >= 50 if has_real_baseline else False,
'improvement_areas': self._identify_improvement_areas(waste_data)
}
except Exception as e:
logger.error("Failed to calculate SDG compliance", error=str(e))
raise
def _get_insufficient_sdg_data(self) -> Dict[str, Any]:
"""Return SDG compliance structure for insufficient data case"""
return {
'sdg_12_3': {
'baseline_waste_percentage': 0.0,
'current_waste_percentage': 0.0,
'reduction_achieved': 0.0,
'target_reduction': 50.0,
'progress_to_target': 0.0,
'status': 'insufficient_data',
'status_label': 'Collecting Baseline Data',
'target_waste_percentage': 0.0
},
'baseline_period': 'not_available',
'certification_ready': False,
'improvement_areas': ['start_production_tracking']
}
async def _get_baseline_waste(
self,
db: AsyncSession,
tenant_id: UUID
) -> Dict[str, Any]:
"""Get baseline waste percentage from production service using shared client"""
try:
# Use the shared production client with proper authentication and resilience
production_client = create_production_client(settings)
baseline_data = await production_client.get_baseline(str(tenant_id))
if baseline_data and baseline_data.get('data_available', False):
# Production service has real baseline data
logger.info(
"Retrieved baseline from production service via client",
tenant_id=str(tenant_id),
baseline_percentage=baseline_data.get('waste_percentage', 0)
)
return {
'waste_percentage': baseline_data['waste_percentage'],
'period': baseline_data['period'].get('type', 'first_90_days'),
'total_production_kg': baseline_data.get('total_production_kg', 0),
'total_waste_kg': baseline_data.get('total_waste_kg', 0)
}
else:
# Production service doesn't have enough data yet
logger.info(
"Production service baseline not available, using industry average",
tenant_id=str(tenant_id)
)
return {
'waste_percentage': EnvironmentalConstants.EU_BAKERY_BASELINE_WASTE * 100,
'period': 'industry_average',
'note': 'Using EU bakery industry average of 25% as baseline'
}
except Exception as e:
logger.warning(
"Error calling production service for baseline via client, using industry average",
error=str(e),
tenant_id=str(tenant_id)
)
# Fallback to industry average
return {
'waste_percentage': EnvironmentalConstants.EU_BAKERY_BASELINE_WASTE * 100,
'period': 'industry_average',
'note': 'Using EU bakery industry average of 25% as baseline'
}
async def _calculate_avoided_waste(
self,
db: AsyncSession,
tenant_id: UUID,
start_date: datetime,
end_date: datetime
) -> Dict[str, Any]:
"""
Calculate waste avoided through AI predictions and smart planning
This is a KEY metric for marketing and grant applications
"""
try:
# Get AI-assisted batch data from production service
production_data = await self._get_production_waste_data(tenant_id, start_date, end_date)
# Extract data with AI batch tracking
total_planned = production_data.get('total_planned', 0) if production_data else 0
total_waste = production_data.get('total_production_waste', 0) if production_data else 0
ai_assisted_batches = production_data.get('ai_assisted_batches', 0) if production_data else 0
# Estimate waste avoided by comparing to industry average
if total_planned > 0:
# Industry average waste: 25%
# Current actual waste from production
industry_expected_waste = total_planned * EnvironmentalConstants.EU_BAKERY_BASELINE_WASTE
actual_waste = total_waste
estimated_avoided = max(0, industry_expected_waste - actual_waste)
# Calculate environmental impact of avoided waste
avoided_co2 = estimated_avoided * EnvironmentalConstants.CO2_PER_KG_WASTE
avoided_water = estimated_avoided * EnvironmentalConstants.WATER_FOOTPRINT['default']
return {
'waste_avoided_kg': round(estimated_avoided, 2),
'ai_assisted_batches': ai_assisted_batches,
'environmental_impact_avoided': {
'co2_kg': round(avoided_co2, 2),
'water_liters': round(avoided_water, 2)
},
'methodology': 'compared_to_industry_baseline'
}
else:
return {
'waste_avoided_kg': 0,
'ai_assisted_batches': 0,
'environmental_impact_avoided': {
'co2_kg': 0,
'water_liters': 0
},
'methodology': 'insufficient_data'
}
except Exception as e:
logger.error("Failed to calculate avoided waste", error=str(e))
return {
'waste_avoided_kg': 0,
'ai_assisted_batches': 0,
'environmental_impact_avoided': {
'co2_kg': 0,
'water_liters': 0
},
'methodology': 'error_occurred'
}
def _calculate_financial_impact(self, waste_data: Dict[str, Any]) -> Dict[str, Any]:
"""Calculate financial impact of food waste"""
# Average cost per kg of bakery products: €3.50
avg_cost_per_kg = 3.50
total_waste_kg = waste_data['total_waste_kg']
waste_cost = total_waste_kg * avg_cost_per_kg
# If waste was reduced by 30%, potential savings
potential_savings = waste_cost * 0.30
return {
'waste_cost_eur': round(waste_cost, 2),
'cost_per_kg': avg_cost_per_kg,
'potential_monthly_savings': round(potential_savings, 2),
'annual_projection': round(waste_cost * 12, 2)
}
def _identify_improvement_areas(self, waste_data: Dict[str, Any]) -> List[str]:
"""Identify areas for improvement based on waste data"""
areas = []
waste_by_reason = waste_data.get('waste_by_reason', {})
if waste_by_reason.get('production_defects', 0) > waste_data['total_waste_kg'] * 0.3:
areas.append('quality_control_in_production')
if waste_by_reason.get('expired_inventory', 0) > waste_data['total_waste_kg'] * 0.4:
areas.append('inventory_rotation_management')
if waste_data.get('waste_percentage', 0) > 20:
areas.append('demand_forecasting_accuracy')
if not areas:
areas.append('maintain_current_practices')
return areas
def _assess_grant_readiness(self, sdg_compliance: Dict[str, Any]) -> Dict[str, Any]:
"""
Assess readiness for EU grant programs accessible to Spanish bakeries and retail.
Based on 2026 verified research. Updated Dec 2025.
"""
reduction = sdg_compliance['sdg_12_3']['reduction_achieved']
grants = {
'horizon_europe_food_systems': {
'eligible': reduction >= 20,
'confidence': 'high' if reduction >= 35 else 'medium' if reduction >= 20 else 'low',
'requirements_met': reduction >= 20,
'funding_eur': 12_000_000, # €3-12M per project
'deadline': '2026-02-18',
'program_type': 'grant',
'category': 'European Union'
},
'horizon_europe_circular_sme': {
'eligible': reduction >= 15,
'confidence': 'high' if reduction >= 25 else 'medium' if reduction >= 15 else 'low',
'requirements_met': reduction >= 15,
'funding_eur': 10_000_000, # €10M total program
'deadline': '2026-02-18',
'program_type': 'grant',
'category': 'European Union'
},
'eit_food_impact_2026': {
'eligible': reduction >= 15,
'confidence': 'high' if reduction >= 25 else 'medium' if reduction >= 15 else 'low',
'requirements_met': reduction >= 15,
'funding_eur': 1_000_000, # €50K-1M range
'deadline': 'rolling_2026',
'program_type': 'grant',
'category': 'European Union'
},
'eib_circular_economy': {
'eligible': reduction >= 10,
'confidence': 'high' if reduction >= 20 else 'medium' if reduction >= 10 else 'low',
'requirements_met': reduction >= 10,
'funding_eur': 12_500_000, # Up to €12.5M loans
'deadline': 'ongoing_2026',
'program_type': 'loan',
'category': 'European Union'
},
'circular_economy_perte': {
'eligible': reduction >= 15,
'confidence': 'high' if reduction >= 25 else 'medium' if reduction >= 15 else 'low',
'requirements_met': reduction >= 15,
'funding_eur': 10_000_000, # €150K-10M range
'deadline': 'rolling_until_2026',
'program_type': 'grant',
'category': 'Spain'
},
'planes_turismo_2026': {
'eligible': reduction >= 10,
'confidence': 'medium',
'requirements_met': reduction >= 10,
'funding_eur': 500_000, # Variable by region
'deadline': '2026-12-31',
'program_type': 'grant',
'category': 'Spain',
'sector_specific': 'tourism'
},
'un_sdg_certified': {
'eligible': reduction >= 50,
'confidence': 'high' if reduction >= 50 else 'low',
'requirements_met': reduction >= 50,
'funding_eur': 0, # Certification, not funding
'deadline': 'ongoing',
'program_type': 'certification',
'category': 'International'
}
}
overall_readiness = sum(1 for g in grants.values() if g['eligible']) / len(grants) * 100
return {
'overall_readiness_percentage': round(overall_readiness, 1),
'grant_programs': grants,
'recommended_applications': [
name for name, details in grants.items() if details['eligible']
],
'spain_compliance': {
'law_1_2025': True, # Spanish food waste prevention law
'circular_economy_strategy': True # Spanish Circular Economy Strategy
}
}
async def export_grant_report(
self,
db: AsyncSession,
tenant_id: UUID,
grant_type: str = 'general',
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None
) -> Dict[str, Any]:
"""
Generate export-ready report for grant applications
Formats data according to common grant application requirements
"""
try:
metrics = await self.get_sustainability_metrics(
db, tenant_id, start_date, end_date
)
# Format for grant applications
report = {
'report_metadata': {
'generated_at': datetime.now().isoformat(),
'report_type': grant_type,
'period': metrics['period'],
'tenant_id': str(tenant_id)
},
'executive_summary': {
'total_waste_reduced_kg': metrics['waste_metrics']['total_waste_kg'],
'waste_reduction_percentage': metrics['sdg_compliance']['sdg_12_3']['reduction_achieved'],
'co2_emissions_avoided_kg': metrics['environmental_impact']['co2_emissions']['kg'],
'financial_savings_eur': metrics['financial_impact']['waste_cost_eur'],
'sdg_compliance_status': metrics['sdg_compliance']['sdg_12_3']['status_label']
},
'detailed_metrics': metrics,
'certifications': {
'sdg_12_3_compliant': metrics['sdg_compliance']['certification_ready'],
'grant_programs_eligible': metrics['grant_readiness']['recommended_applications']
},
'supporting_data': {
'baseline_comparison': {
'baseline': metrics['sdg_compliance']['sdg_12_3']['baseline_waste_percentage'],
'current': metrics['sdg_compliance']['sdg_12_3']['current_waste_percentage'],
'improvement': metrics['sdg_compliance']['sdg_12_3']['reduction_achieved']
},
'environmental_benefits': metrics['environmental_impact'],
'financial_benefits': metrics['financial_impact']
}
}
return report
except Exception as e:
logger.error("Failed to generate grant report", error=str(e))
raise