diff --git a/services/procurement/app/ml/price_forecaster.py b/services/procurement/app/ml/price_forecaster.py index 5df56b54..755b0a47 100644 --- a/services/procurement/app/ml/price_forecaster.py +++ b/services/procurement/app/ml/price_forecaster.py @@ -498,35 +498,36 @@ class PriceForecaster: # Calculate expected price change expected_change_pct = ((forecast_mean - current_price) / current_price * 100) if current_price > 0 else 0 - # Decision logic + # Decision logic with i18n-friendly reasoning codes if expected_change_pct < -5: # Price expected to drop >5% action = 'wait' - reasoning = f'Price expected to decrease by {abs(expected_change_pct):.1f}% in next 30 days. Delay purchase.' + reasoning = f'PRICE_FORECAST:DECREASE_EXPECTED|change_pct={abs(expected_change_pct):.1f}|days=30' urgency = 'low' elif expected_change_pct > 5: # Price expected to increase >5% action = 'buy_now' - reasoning = f'Price expected to increase by {expected_change_pct:.1f}% in next 30 days. Purchase soon.' + reasoning = f'PRICE_FORECAST:INCREASE_EXPECTED|change_pct={expected_change_pct:.1f}|days=30' urgency = 'high' elif volatility['volatility_level'] == 'high': # High volatility - wait for dip action = 'wait_for_dip' - reasoning = f'High price volatility (CV={volatility["coefficient_of_variation"]:.2f}). Wait for favorable dip.' + reasoning = f'PRICE_FORECAST:HIGH_VOLATILITY|coefficient={volatility["coefficient_of_variation"]:.2f}' urgency = 'medium' elif current_price < price_stats['mean_price'] * 0.95: # Currently below average + below_avg_pct = ((price_stats["mean_price"] - current_price) / price_stats["mean_price"] * 100) action = 'buy_now' - reasoning = f'Current price €{current_price:.2f} is {((price_stats["mean_price"] - current_price) / price_stats["mean_price"] * 100):.1f}% below average. Good buying opportunity.' + reasoning = f'PRICE_FORECAST:BELOW_AVERAGE|current_price={current_price:.2f}|below_avg_pct={below_avg_pct:.1f}' urgency = 'medium' else: # Neutral action = 'normal_purchase' - reasoning = 'Price stable. Follow normal procurement schedule.' + reasoning = 'PRICE_FORECAST:STABLE' urgency = 'low' # Optimal purchase timing diff --git a/services/procurement/app/services/safety_stock_calculator.py b/services/procurement/app/services/safety_stock_calculator.py index ba88087b..a48a1a47 100644 --- a/services/procurement/app/services/safety_stock_calculator.py +++ b/services/procurement/app/services/safety_stock_calculator.py @@ -117,10 +117,8 @@ class SafetyStockCalculator: # Determine confidence confidence = self._determine_confidence(demand_std_dev, lead_time_days) - reasoning = ( - f"Service level {service_level*100:.1f}% (Z={z_score:.2f}) × " - f"Demand σ={demand_std_dev:.2f} × √{lead_time_days} days" - ) + # Use calculation method as reasoning code with parameters + reasoning = f"CALC:STATISTICAL_Z_SCORE|service_level={service_level*100:.1f}|z_score={z_score:.2f}|demand_std={demand_std_dev:.2f}|lead_time={lead_time_days}" return SafetyStockResult( safety_stock_quantity=Decimal(str(round(safety_stock, 2))), @@ -211,10 +209,8 @@ class SafetyStockCalculator: confidence = 'high' if lead_time_std_dev > 0 else 'medium' - reasoning = ( - f"Advanced formula considering both demand variability " - f"(σ={demand_std_dev:.2f}) and lead time variability (σ={lead_time_std_dev:.1f} days)" - ) + # Use calculation method as reasoning code with parameters + reasoning = f"CALC:ADVANCED_VARIABILITY|demand_std={demand_std_dev:.2f}|lead_time_std={lead_time_std_dev:.1f}" return SafetyStockResult( safety_stock_quantity=Decimal(str(round(safety_stock, 2))), @@ -249,7 +245,8 @@ class SafetyStockCalculator: lead_time_demand = average_demand * lead_time_days safety_stock = lead_time_demand * percentage - reasoning = f"{percentage*100:.0f}% of lead time demand ({lead_time_demand:.2f})" + # Use calculation method as reasoning code with parameters + reasoning = f"CALC:FIXED_PERCENTAGE|percentage={percentage*100:.0f}|lead_time_demand={lead_time_demand:.2f}" return SafetyStockResult( safety_stock_quantity=Decimal(str(round(safety_stock, 2))), diff --git a/shared/utils/optimization.py b/shared/utils/optimization.py index d525b758..8ec3bd59 100644 --- a/shared/utils/optimization.py +++ b/shared/utils/optimization.py @@ -87,17 +87,20 @@ def optimize_order_quantity( # Start with EOQ or required quantity, whichever is larger optimal_qty = max(float(required_quantity), eoq) - reasoning = f"Base EOQ: {eoq:.2f}, Required: {required_quantity}" + # Build structured reasoning code with parameters + reasoning_parts = [f"EOQ:BASE|eoq={eoq:.2f}|required={required_quantity}"] # Apply minimum order quantity if min_order_qty and Decimal(optimal_qty) < min_order_qty: optimal_qty = float(min_order_qty) - reasoning += f", Applied MOQ: {min_order_qty}" + reasoning_parts.append(f"EOQ:MOQ_APPLIED|moq={min_order_qty}") # Apply maximum order quantity if max_order_qty and Decimal(optimal_qty) > max_order_qty: optimal_qty = float(max_order_qty) - reasoning += f", Capped at max: {max_order_qty}" + reasoning_parts.append(f"EOQ:MAX_APPLIED|max={max_order_qty}") + + reasoning = "|".join(reasoning_parts) # Calculate costs orders_per_year = annual_demand / optimal_qty if optimal_qty > 0 else 0 @@ -208,7 +211,7 @@ def apply_price_tier_optimization( best_quantity = base_quantity best_price = current_tier_price best_savings = Decimal('0') - reasoning = f"Current tier price: ${current_tier_price}" + reasoning = f"TIER_PRICING:CURRENT_TIER|price={current_tier_price}" for tier in sorted_tiers: tier_min_qty = Decimal(str(tier['min_quantity'])) @@ -232,7 +235,7 @@ def apply_price_tier_optimization( best_quantity = tier_min_qty best_price = tier_price best_savings = savings - reasoning = f"Upgraded to tier {tier_min_qty}+ for ${savings:.2f} savings" + reasoning = f"TIER_PRICING:UPGRADED|tier_min={tier_min_qty}|savings={savings:.2f}" return best_quantity, best_price, reasoning