use crate::app::OrderEntryState;
pub(crate) fn validate(
state: &OrderEntryState,
buying_power: f64,
market_open: bool,
) -> Option<String> {
if state.symbol.trim().is_empty() {
return Some("Symbol cannot be empty".into());
}
if !market_open && !state.gtc_order {
return Some("Market is closed — switch to GTC or wait for market open".into());
}
let qty: Option<f64> = if state.qty_input.is_empty() {
None
} else {
match state.qty_input.parse::<f64>() {
Ok(v) if v > 0.0 => Some(v),
Ok(_) => return Some("Quantity must be greater than zero".into()),
Err(_) => return Some("Quantity is not a valid number".into()),
}
};
let price: Option<f64> = if state.market_order {
None
} else {
match state.price_input.parse::<f64>() {
Ok(v) if v > 0.0 => Some(v),
Ok(_) => return Some("Price must be greater than zero".into()),
Err(_) => return Some("Price is not a valid number for a LIMIT order".into()),
}
};
if let (Some(q), Some(p)) = (qty, price) {
let est_total = q * p;
if est_total > buying_power {
return Some(format!(
"Order total ${:.2} exceeds buying power ${:.2}",
est_total, buying_power
));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::OrderEntryState;
fn base_state() -> OrderEntryState {
let mut s = OrderEntryState::new("AAPL".into());
s.market_order = false; s.gtc_order = false; s.qty_input = "10".into();
s.price_input = "150.00".into();
s
}
#[test]
fn valid_limit_order_returns_none() {
let state = base_state();
assert_eq!(validate(&state, 10_000.0, true), None);
}
#[test]
fn valid_market_order_returns_none() {
let mut state = base_state();
state.market_order = true;
state.price_input.clear(); assert_eq!(validate(&state, 10_000.0, true), None);
}
#[test]
fn empty_symbol_fails() {
let mut state = base_state();
state.symbol.clear();
assert!(validate(&state, 10_000.0, true).is_some());
}
#[test]
fn whitespace_only_symbol_fails() {
let mut state = base_state();
state.symbol = " ".into();
assert!(validate(&state, 10_000.0, true).is_some());
}
#[test]
fn zero_qty_fails() {
let mut state = base_state();
state.qty_input = "0".into();
assert!(validate(&state, 10_000.0, true).is_some());
}
#[test]
fn negative_qty_fails() {
let mut state = base_state();
state.qty_input = "-5".into();
assert!(validate(&state, 10_000.0, true).is_some());
}
#[test]
fn non_numeric_qty_fails() {
let mut state = base_state();
state.qty_input = "abc".into();
assert!(validate(&state, 10_000.0, true).is_some());
}
#[test]
fn empty_qty_is_allowed_as_notional() {
let mut state = base_state();
state.qty_input.clear(); assert_eq!(validate(&state, 10_000.0, true), None);
}
#[test]
fn zero_price_on_limit_fails() {
let mut state = base_state();
state.price_input = "0".into();
assert!(validate(&state, 10_000.0, true).is_some());
}
#[test]
fn negative_price_on_limit_fails() {
let mut state = base_state();
state.price_input = "-1.0".into();
assert!(validate(&state, 10_000.0, true).is_some());
}
#[test]
fn non_numeric_price_on_limit_fails() {
let mut state = base_state();
state.price_input = "abc".into();
assert!(validate(&state, 10_000.0, true).is_some());
}
#[test]
fn price_not_required_for_market_order() {
let mut state = base_state();
state.market_order = true;
state.price_input = "not-a-number".into(); assert_eq!(validate(&state, 10_000.0, true), None);
}
#[test]
fn total_exceeding_buying_power_fails() {
let state = base_state(); assert!(validate(&state, 1_000.0, true).is_some());
}
#[test]
fn total_exactly_at_buying_power_passes() {
let state = base_state(); assert_eq!(validate(&state, 1_500.0, true), None);
}
#[test]
fn error_message_contains_amounts_when_exceeding_buying_power() {
let state = base_state(); let msg = validate(&state, 500.0, true).expect("should fail");
assert!(msg.contains("1500.00"), "got: {msg}");
assert!(msg.contains("500.00"), "got: {msg}");
}
#[test]
fn day_order_when_market_closed_fails() {
let mut state = base_state();
state.gtc_order = false; let msg = validate(&state, 10_000.0, false).expect("should fail");
assert!(
msg.to_lowercase().contains("closed") || msg.to_lowercase().contains("gtc"),
"expected closed/GTC mention, got: {msg}"
);
}
#[test]
fn gtc_order_when_market_closed_passes() {
let mut state = base_state();
state.gtc_order = true; assert_eq!(validate(&state, 10_000.0, false), None);
}
#[test]
fn day_order_when_market_open_passes() {
let state = base_state(); assert_eq!(validate(&state, 10_000.0, true), None);
}
#[test]
fn market_closed_error_checked_before_other_errors() {
let mut state = base_state();
state.gtc_order = false;
state.qty_input = "-99".into();
let msg = validate(&state, 10_000.0, false).expect("should fail");
assert!(
msg.to_lowercase().contains("closed") || msg.to_lowercase().contains("gtc"),
"market-closed check should run before qty check; got: {msg}"
);
}
}