refactor: Convert internal services to structured JSON reasoning

Convert pipe-separated reasoning codes to structured JSON format for:
- Safety stock calculator (statistical calculations, errors)
- Price forecaster (procurement recommendations, volatility)
- Order optimization (EOQ, tier pricing)

This enables i18n translation of internal calculation reasoning
and provides structured data for frontend AI insights display.

Benefits:
- Consistent with PO/Batch reasoning_data format
- Frontend can translate using same i18n infrastructure
- Structured parameters enable rich UI visualization
- No legacy string parsing needed

Changes:
- safety_stock_calculator.py: Replace reasoning str with reasoning_data dict
- price_forecaster.py: Convert recommendation reasoning to structured format
- optimization.py: Update EOQ and tier pricing to use reasoning_data

Part of complete i18n implementation for AI insights.
This commit is contained in:
Claude
2025-11-07 19:22:02 +00:00
parent be8cb20b18
commit 9d284cae46
3 changed files with 162 additions and 38 deletions

View File

@@ -19,7 +19,7 @@ class OrderOptimizationResult:
holding_cost: Decimal
total_cost: Decimal
orders_per_year: float
reasoning: str
reasoning_data: dict
def calculate_economic_order_quantity(
@@ -87,20 +87,36 @@ def optimize_order_quantity(
# Start with EOQ or required quantity, whichever is larger
optimal_qty = max(float(required_quantity), eoq)
# Build structured reasoning code with parameters
reasoning_parts = [f"EOQ:BASE|eoq={eoq:.2f}|required={required_quantity}"]
# Build structured reasoning data
reasoning_data = {
'type': 'eoq_base',
'parameters': {
'eoq': round(eoq, 2),
'required_quantity': float(required_quantity),
'annual_demand': round(annual_demand, 2),
'ordering_cost': ordering_cost,
'holding_cost_rate': holding_cost_rate
},
'constraints_applied': []
}
# Apply minimum order quantity
if min_order_qty and Decimal(optimal_qty) < min_order_qty:
optimal_qty = float(min_order_qty)
reasoning_parts.append(f"EOQ:MOQ_APPLIED|moq={min_order_qty}")
reasoning_data['constraints_applied'].append({
'type': 'moq_applied',
'moq': float(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_parts.append(f"EOQ:MAX_APPLIED|max={max_order_qty}")
reasoning_data['constraints_applied'].append({
'type': 'max_applied',
'max_qty': float(max_order_qty)
})
reasoning = "|".join(reasoning_parts)
reasoning_data['parameters']['optimal_quantity'] = round(optimal_qty, 2)
# Calculate costs
orders_per_year = annual_demand / optimal_qty if optimal_qty > 0 else 0
@@ -114,7 +130,7 @@ def optimize_order_quantity(
holding_cost=Decimal(str(annual_holding_cost)),
total_cost=Decimal(str(total_annual_cost)),
orders_per_year=orders_per_year,
reasoning=reasoning
reasoning_data=reasoning_data
)
@@ -180,7 +196,7 @@ def apply_price_tier_optimization(
base_quantity: Decimal,
unit_price: Decimal,
price_tiers: List[Dict]
) -> Tuple[Decimal, Decimal, str]:
) -> Tuple[Decimal, Decimal, dict]:
"""
Optimize quantity to take advantage of price tiers.
@@ -190,10 +206,16 @@ def apply_price_tier_optimization(
price_tiers: List of dicts with 'min_quantity' and 'unit_price'
Returns:
Tuple of (optimized_quantity, unit_price, reasoning)
Tuple of (optimized_quantity, unit_price, reasoning_data)
"""
if not price_tiers:
return base_quantity, unit_price, "No price tiers available"
return base_quantity, unit_price, {
'type': 'no_tiers',
'parameters': {
'base_quantity': float(base_quantity),
'unit_price': float(unit_price)
}
}
# Sort tiers by min_quantity
sorted_tiers = sorted(price_tiers, key=lambda x: x['min_quantity'])
@@ -211,7 +233,14 @@ def apply_price_tier_optimization(
best_quantity = base_quantity
best_price = current_tier_price
best_savings = Decimal('0')
reasoning = f"TIER_PRICING:CURRENT_TIER|price={current_tier_price}"
reasoning_data = {
'type': 'current_tier',
'parameters': {
'base_quantity': float(base_quantity),
'current_tier_price': float(current_tier_price),
'base_cost': float(base_cost)
}
}
for tier in sorted_tiers:
tier_min_qty = Decimal(str(tier['min_quantity']))
@@ -235,9 +264,19 @@ def apply_price_tier_optimization(
best_quantity = tier_min_qty
best_price = tier_price
best_savings = savings
reasoning = f"TIER_PRICING:UPGRADED|tier_min={tier_min_qty}|savings={savings:.2f}"
reasoning_data = {
'type': 'tier_upgraded',
'parameters': {
'base_quantity': float(base_quantity),
'tier_min_qty': float(tier_min_qty),
'base_price': float(current_tier_price),
'tier_price': float(tier_price),
'savings': round(float(savings), 2),
'additional_qty': float(additional_qty)
}
}
return best_quantity, best_price, reasoning
return best_quantity, best_price, reasoning_data
def aggregate_requirements_for_moq(