from typing import Optional from .cost_calculator import CostCalculator class BudgetOptimizer: """Multi-constraint budget optimizer with 3-tier plan generation and priority allocation.""" TIER_ORDER = ['budget', 'mid_range', 'luxury'] BUDGET_PRIORITY_DEFAULTS = { 'accommodation': 0.35, 'food': 0.25, 'activities': 0.20, 'transport': 0.15, 'misc': 0.05, } def __init__(self, destinations: list): self.destinations = {d['id']: d for d in destinations} def get_destination(self, dest_id: str) -> Optional[dict]: return self.destinations.get(dest_id) def generate_three_tier_plans( self, destination: dict, days: int, num_adults: int = 2, num_children: int = 0, activities: Optional[list] = None, month: Optional[int] = None ) -> list[dict]: """Generate budget/mid_range/luxury plans with full cost breakdown.""" plans = [] for tier in self.TIER_ORDER: plan = CostCalculator.calculate_trip_total( destination=destination, days=days, tier=tier, num_adults=num_adults, num_children=num_children, activities=activities, month=month, ) tier_data = destination.get('cost_tiers', {}).get(tier, {}) plan['tier_description_vi'] = tier_data.get('description_vi', '') plan['tier_description_en'] = tier_data.get('description_en', '') plans.append(plan) return plans def check_budget_feasibility( self, destination: dict, total_budget: float, currency: str, days: int, num_adults: int = 2, num_children: int = 0, month: Optional[int] = None ) -> dict: """Check if a given budget is feasible and suggest the best matching tier.""" budget_vnd = total_budget if currency == 'USD': budget_vnd = CostCalculator.convert_currency(total_budget, 'USD', 'VND') plans = self.generate_three_tier_plans( destination, days, num_adults, num_children, month=month ) feasible_tiers = [] recommended_tier = None for plan in plans: if plan['total'] <= budget_vnd: feasible_tiers.append(plan['tier']) recommended_tier = plan['tier'] # last feasible = highest affordable if not feasible_tiers: # Budget too low even for budget tier budget_plan = plans[0] shortfall = budget_plan['total'] - budget_vnd return { 'feasible': False, 'budget_vnd': budget_vnd, 'minimum_required_vnd': budget_plan['total'], 'shortfall_vnd': int(shortfall), 'suggestion': 'reduce_days_or_people', 'recommended_days': max(1, int(days * budget_vnd / budget_plan['total'])), 'plans': plans, } return { 'feasible': True, 'budget_vnd': budget_vnd, 'feasible_tiers': feasible_tiers, 'recommended_tier': recommended_tier, 'remaining_budget_vnd': int(budget_vnd - next( p['total'] for p in plans if p['tier'] == recommended_tier )), 'plans': plans, } def optimize_with_priorities( self, destination: dict, total_budget: float, currency: str, days: int, num_adults: int = 2, num_children: int = 0, priorities: Optional[dict] = None, month: Optional[int] = None ) -> dict: """Allocate budget based on user spending priorities. priorities: {'accommodation': 'high', 'food': 'high', 'activities': 'low', ...} Values: 'high', 'medium', 'low' """ budget_vnd = total_budget if currency == 'USD': budget_vnd = CostCalculator.convert_currency(total_budget, 'USD', 'VND') # Convert priority labels to weights priority_weights = dict(self.BUDGET_PRIORITY_DEFAULTS) if priorities: multipliers = {'high': 1.5, 'medium': 1.0, 'low': 0.5} for category, level in priorities.items(): if category in priority_weights: priority_weights[category] *= multipliers.get(level, 1.0) # Normalize weights to sum to 1.0 total_weight = sum(priority_weights.values()) if total_weight > 0: priority_weights = {k: v / total_weight for k, v in priority_weights.items()} # Allocate budget per category allocation = {} for category, weight in priority_weights.items(): allocation[category] = int(budget_vnd * weight) # Determine tier per category based on allocation daily_budget_accommodation = allocation['accommodation'] // max(1, days - 1) cost_tiers = destination.get('cost_tiers', {}) # Find best matching tier for accommodation accom_tier = 'budget' for tier in self.TIER_ORDER: tier_cost = cost_tiers.get(tier, {}).get('accommodation_vnd', 0) if tier_cost <= daily_budget_accommodation: accom_tier = tier # Find best matching tier for food daily_food_budget = allocation['food'] // days total_people = num_adults + num_children per_person_food = daily_food_budget // max(1, total_people) per_meal_budget = per_person_food // 3 food_tier = 'budget' for tier in self.TIER_ORDER: tier_cost = cost_tiers.get(tier, {}).get('food_per_meal_vnd', 0) if tier_cost <= per_meal_budget: food_tier = tier return { 'total_budget_vnd': int(budget_vnd), 'allocation': allocation, 'priority_weights': {k: round(v, 3) for k, v in priority_weights.items()}, 'recommended_tiers': { 'accommodation': accom_tier, 'food': food_tier, }, 'daily_breakdown': { 'accommodation_per_night': daily_budget_accommodation, 'food_per_day': daily_food_budget, 'activities_per_day': allocation['activities'] // days, 'transport_per_day': allocation['transport'] // days, }, 'per_person_per_day': int(budget_vnd // max(1, total_people) // days), } def compare_destinations_cost( self, dest_ids: list[str], days: int, num_adults: int = 2, num_children: int = 0, tier: str = 'mid_range', month: Optional[int] = None ) -> list[dict]: """Compare costs across multiple destinations.""" results = [] for dest_id in dest_ids: dest = self.destinations.get(dest_id) if not dest: continue cost = CostCalculator.calculate_trip_total( destination=dest, days=days, tier=tier, num_adults=num_adults, num_children=num_children, month=month, ) cost['destination_id'] = dest_id cost['destination_name'] = dest.get('name', '') cost['destination_name_en'] = dest.get('name_en', '') cost['best_months'] = dest.get('best_months', []) cost['tags'] = dest.get('tags', []) results.append(cost) results.sort(key=lambda x: x['total']) return results