ironclaw 0.24.0

Secure personal AI assistant that protects your data and expands its capabilities on the fly
Documentation
//! Value/earnings estimation.

use rust_decimal::Decimal;
use rust_decimal_macros::dec;

/// Estimates the value/earnings potential of jobs.
pub struct ValueEstimator {
    /// Minimum profit margin to aim for.
    min_margin: Decimal,
    /// Target profit margin.
    target_margin: Decimal,
}

impl ValueEstimator {
    /// Create a new value estimator.
    pub fn new() -> Self {
        Self {
            min_margin: dec!(0.1),    // 10% minimum
            target_margin: dec!(0.3), // 30% target
        }
    }

    /// Estimate value for a job based on description and cost.
    pub fn estimate(&self, _description: &str, estimated_cost: Decimal) -> Decimal {
        // Simple formula: value = cost + margin
        // In practice, this would analyze the description to estimate complexity
        let margin = estimated_cost * self.target_margin;
        estimated_cost + margin
    }

    /// Calculate minimum acceptable bid.
    pub fn minimum_bid(&self, estimated_cost: Decimal) -> Decimal {
        estimated_cost + (estimated_cost * self.min_margin)
    }

    /// Calculate ideal bid.
    pub fn ideal_bid(&self, estimated_cost: Decimal) -> Decimal {
        estimated_cost + (estimated_cost * self.target_margin)
    }

    /// Check if a job is profitable at a given price.
    pub fn is_profitable(&self, price: Decimal, estimated_cost: Decimal) -> bool {
        if price.is_zero() {
            // With a zero price, the job is only profitable if the cost is negative.
            // This results in a positive profit and an effectively infinite margin.
            return estimated_cost < Decimal::ZERO;
        }
        let margin = (price - estimated_cost) / price;
        margin >= self.min_margin
    }

    /// Calculate profit for a completed job.
    pub fn calculate_profit(&self, earnings: Decimal, actual_cost: Decimal) -> Decimal {
        earnings - actual_cost
    }

    /// Calculate profit margin.
    pub fn calculate_margin(&self, earnings: Decimal, actual_cost: Decimal) -> Decimal {
        if earnings.is_zero() {
            return Decimal::ZERO;
        }
        (earnings - actual_cost) / earnings
    }

    /// Set minimum margin.
    pub fn set_min_margin(&mut self, margin: Decimal) {
        self.min_margin = margin;
    }

    /// Set target margin.
    pub fn set_target_margin(&mut self, margin: Decimal) {
        self.target_margin = margin;
    }
}

impl Default for ValueEstimator {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_value_estimation() {
        let estimator = ValueEstimator::new();

        let cost = dec!(10.0);
        let value = estimator.estimate("test job", cost);

        assert!(value > cost);
    }

    #[test]
    fn test_profitability() {
        let estimator = ValueEstimator::new();

        let cost = dec!(10.0);
        assert!(estimator.is_profitable(dec!(15.0), cost));
        assert!(!estimator.is_profitable(dec!(10.5), cost)); // Only 5% margin
    }

    #[test]
    fn test_margin_calculation() {
        let estimator = ValueEstimator::new();

        let margin = estimator.calculate_margin(dec!(100.0), dec!(70.0));
        assert_eq!(margin, dec!(0.30)); // 30%
    }

    #[test]
    fn test_profitability_zero_price() {
        let estimator = ValueEstimator::new();

        // Zero price should return false, not panic
        assert!(!estimator.is_profitable(Decimal::ZERO, dec!(10.0)));
        assert!(!estimator.is_profitable(Decimal::ZERO, Decimal::ZERO));
        // Negative cost with zero price is profitable (we get paid to do it)
        assert!(estimator.is_profitable(Decimal::ZERO, dec!(-10.0)));
    }

    // === QA Plan P2 - 4.4: Value estimator boundary tests ===

    #[test]
    fn test_profitability_negative_cost() {
        let estimator = ValueEstimator::new();
        // Negative cost means we get paid to do the work -- always profitable
        // with any positive price.
        assert!(estimator.is_profitable(dec!(100.0), dec!(-50.0)));
        assert!(estimator.is_profitable(dec!(1.0), dec!(-0.01)));
    }

    #[test]
    fn test_profitability_cost_exceeds_price() {
        let estimator = ValueEstimator::new();
        // Cost exceeds price → negative margin → not profitable.
        assert!(!estimator.is_profitable(dec!(10.0), dec!(100.0)));
    }

    #[test]
    fn test_margin_zero_earnings() {
        let estimator = ValueEstimator::new();
        // Zero earnings → margin should be zero, not panic from divide-by-zero.
        assert_eq!(
            estimator.calculate_margin(Decimal::ZERO, dec!(50.0)),
            Decimal::ZERO
        );
        assert_eq!(
            estimator.calculate_margin(Decimal::ZERO, Decimal::ZERO),
            Decimal::ZERO
        );
    }

    #[test]
    fn test_estimate_zero_cost() {
        let estimator = ValueEstimator::new();
        // Zero cost → value estimate should be zero (cost + 30% of zero).
        let value = estimator.estimate("free task", Decimal::ZERO);
        assert_eq!(value, Decimal::ZERO);
    }

    #[test]
    fn test_minimum_vs_ideal_bid() {
        let estimator = ValueEstimator::new();
        let cost = dec!(100.0);
        let min_bid = estimator.minimum_bid(cost);
        let ideal_bid = estimator.ideal_bid(cost);
        // Minimum bid should always be less than ideal bid.
        assert!(min_bid < ideal_bid);
        // Both should be above cost.
        assert!(min_bid > cost);
        assert!(ideal_bid > cost);
    }

    #[test]
    fn test_profit_calculation() {
        let estimator = ValueEstimator::new();
        assert_eq!(
            estimator.calculate_profit(dec!(150.0), dec!(100.0)),
            dec!(50.0)
        );
        // Negative profit (loss).
        assert_eq!(
            estimator.calculate_profit(dec!(50.0), dec!(100.0)),
            dec!(-50.0)
        );
    }

    // === Additional boundary / edge-case tests (QA Plan 4.4) ===

    #[test]
    fn is_profitable_with_very_large_values() {
        let estimator = ValueEstimator::new();
        // rust_decimal::Decimal max is ~79_228_162_514_264_337_593_543_950_335.
        // Use values large enough to stress multiplication but within Decimal range.
        let big = Decimal::new(i64::MAX, 0); // 9_223_372_036_854_775_807
        let small = Decimal::new(1, 0);

        // Large price, small cost -- clearly profitable, must not overflow.
        assert!(estimator.is_profitable(big, small));

        // Large cost, small price -- clearly unprofitable.
        assert!(!estimator.is_profitable(small, big));

        // Large equal values: margin = 0, which is < 10% min -- not profitable.
        assert!(!estimator.is_profitable(big, big));
    }

    #[test]
    fn estimate_value_with_very_large_cost() {
        let estimator = ValueEstimator::new();
        let big = Decimal::new(i64::MAX / 2, 0);
        let value = estimator.estimate("big job", big);
        // value = cost + cost * 0.3 = cost * 1.3, should not overflow.
        assert!(value > big);
    }

    #[test]
    fn is_profitable_with_negative_price() {
        let estimator = ValueEstimator::new();
        // Negative price is an unusual edge case. The current formula
        // margin = (price - cost) / price can produce misleading results
        // because dividing two negatives yields a positive.
        //
        // price = -10, cost = 5: margin = (-10 - 5) / -10 = 1.5 >= 0.1
        // The formula says "profitable" even though the scenario is nonsensical.
        // We document the current behavior here; a guard for negative prices
        // could be added in a future hardening pass.
        assert!(estimator.is_profitable(dec!(-10.0), dec!(5.0)));

        // price = -10, cost = -20: margin = (-10 - (-20)) / -10 = -1.0 < 0.1.
        assert!(!estimator.is_profitable(dec!(-10.0), dec!(-20.0)));
    }

    #[test]
    fn calculate_margin_with_negative_earnings() {
        let estimator = ValueEstimator::new();
        // Negative earnings -- margin formula still computes without panic.
        let margin = estimator.calculate_margin(dec!(-100.0), dec!(50.0));
        // (earnings - cost) / earnings = (-100 - 50) / -100 = 1.5
        assert_eq!(margin, dec!(1.5));
    }

    #[test]
    fn calculate_margin_with_both_negative() {
        let estimator = ValueEstimator::new();
        // Both negative: earnings = -50, cost = -100.
        // margin = (-50 - (-100)) / -50 = 50 / -50 = -1.0
        let margin = estimator.calculate_margin(dec!(-50.0), dec!(-100.0));
        assert_eq!(margin, dec!(-1.0));
    }

    #[test]
    fn minimum_bid_with_zero_cost() {
        let estimator = ValueEstimator::new();
        // Zero cost -- both bids should be zero.
        assert_eq!(estimator.minimum_bid(Decimal::ZERO), Decimal::ZERO);
        assert_eq!(estimator.ideal_bid(Decimal::ZERO), Decimal::ZERO);
    }

    #[test]
    fn minimum_bid_with_negative_cost() {
        let estimator = ValueEstimator::new();
        // Negative cost -- the bid formulas still compute (cost + cost * margin),
        // producing a negative bid (we'd pay them).
        let min_bid = estimator.minimum_bid(dec!(-100.0));
        let ideal_bid = estimator.ideal_bid(dec!(-100.0));
        assert!(min_bid < Decimal::ZERO);
        assert!(ideal_bid < Decimal::ZERO);
        // With negative values, ideal (more negative) < minimum (less negative).
        assert!(ideal_bid < min_bid);
    }

    #[test]
    fn estimate_with_negative_cost() {
        let estimator = ValueEstimator::new();
        // Negative cost: value = cost + cost * 0.3 = -100 + (-30) = -130.
        let value = estimator.estimate("refund task", dec!(-100.0));
        assert_eq!(value, dec!(-130.0));
    }

    #[test]
    fn custom_margins_affect_profitability() {
        let mut estimator = ValueEstimator::new();
        let price = dec!(110.0);
        let cost = dec!(100.0);

        // Default 10% min margin: (110 - 100) / 110 ~= 9.09% < 10% -> not profitable.
        assert!(!estimator.is_profitable(price, cost));

        // Lower min margin to 5% -> now 9.09% >= 5% -> profitable.
        estimator.set_min_margin(dec!(0.05));
        assert!(estimator.is_profitable(price, cost));

        // Raise min margin to 50% -> 9.09% < 50% -> not profitable.
        estimator.set_min_margin(dec!(0.50));
        assert!(!estimator.is_profitable(price, cost));
    }

    #[test]
    fn custom_target_margin_affects_bids() {
        let mut estimator = ValueEstimator::new();
        let cost = dec!(100.0);

        let default_ideal = estimator.ideal_bid(cost);
        assert_eq!(default_ideal, dec!(130.0)); // 100 + 30%

        estimator.set_target_margin(dec!(0.5));
        let new_ideal = estimator.ideal_bid(cost);
        assert_eq!(new_ideal, dec!(150.0)); // 100 + 50%
    }

    #[test]
    fn is_profitable_at_exact_margin_boundary() {
        let estimator = ValueEstimator::new();
        // min_margin = 0.1 (10%). Price = 100, cost = 90 -> margin = 10/100 = 0.1.
        // Exactly at boundary -- should be profitable (>=).
        assert!(estimator.is_profitable(dec!(100.0), dec!(90.0)));

        // Slightly below boundary: cost = 90.01 -> margin = 9.99/100 = 0.0999 < 0.1.
        assert!(!estimator.is_profitable(dec!(100.0), dec!(90.01)));
    }

    #[test]
    fn profit_with_zero_values() {
        let estimator = ValueEstimator::new();
        assert_eq!(
            estimator.calculate_profit(Decimal::ZERO, Decimal::ZERO),
            Decimal::ZERO
        );
        assert_eq!(
            estimator.calculate_profit(Decimal::ZERO, dec!(100.0)),
            dec!(-100.0)
        );
        assert_eq!(
            estimator.calculate_profit(dec!(100.0), Decimal::ZERO),
            dec!(100.0)
        );
    }

    #[test]
    fn default_impl_matches_new() {
        let from_new = ValueEstimator::new();
        let from_default = ValueEstimator::default();
        let cost = dec!(100.0);

        // Both should produce identical results.
        assert_eq!(
            from_new.estimate("x", cost),
            from_default.estimate("x", cost)
        );
        assert_eq!(from_new.minimum_bid(cost), from_default.minimum_bid(cost));
        assert_eq!(from_new.ideal_bid(cost), from_default.ideal_bid(cost));
        assert_eq!(
            from_new.is_profitable(dec!(150.0), cost),
            from_default.is_profitable(dec!(150.0), cost)
        );
    }
}