use serde::Serialize;
use serde_json::Value;
use crate::error::AppError;
use crate::raw;
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum VerificationState {
Verified,
Unverified,
}
#[serde_with::skip_serializing_none]
#[derive(Clone, Debug, Serialize)]
pub(crate) struct OrderActionResult {
pub action: String,
pub order_id: Option<i64>,
pub location: Option<String>,
pub order: Option<Value>,
pub verification_state: VerificationState,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub verification_failures: Vec<String>,
pub verified_order: Option<Value>,
pub digest: Option<String>,
pub original_command: Option<String>,
}
#[cfg_attr(coverage_nightly, coverage(off))]
pub(crate) async fn verify_order(
client: &schwab::Client,
account: &str,
order_id: Option<i64>,
action: &str,
location: Option<String>,
submitted_order: Option<Value>,
) -> OrderActionResult {
let Some(id) = order_id else {
return result_without_order_id(action, location, submitted_order);
};
match client.get_order(account, id).await {
Ok(order) => match serde_json::to_value(&order) {
Ok(value) => result_from_verified_order(
id,
action,
location,
submitted_order,
raw::sanitize_order(value),
),
Err(e) => OrderActionResult {
action: action.to_string(),
order_id: Some(id),
location,
order: submitted_order,
verification_state: VerificationState::Unverified,
verification_failures: vec![format!("failed to serialize order: {e}")],
verified_order: None,
digest: None,
original_command: None,
},
},
Err(e) => result_from_retrieval_failure(id, action, location, submitted_order, &e),
}
}
fn result_without_order_id(
action: &str,
location: Option<String>,
submitted_order: Option<Value>,
) -> OrderActionResult {
OrderActionResult {
action: action.to_string(),
order_id: None,
location,
order: submitted_order,
verification_state: VerificationState::Unverified,
verification_failures: vec!["no order ID returned by API".to_string()],
verified_order: None,
digest: None,
original_command: None,
}
}
fn result_from_retrieval_failure(
id: i64,
action: &str,
location: Option<String>,
submitted_order: Option<Value>,
error: &dyn std::fmt::Display,
) -> OrderActionResult {
OrderActionResult {
action: action.to_string(),
order_id: Some(id),
location,
order: submitted_order,
verification_state: VerificationState::Unverified,
verification_failures: vec![format!("failed to retrieve order: {error}")],
verified_order: None,
digest: None,
original_command: None,
}
}
fn result_from_verified_order(
id: i64,
action: &str,
location: Option<String>,
submitted_order: Option<Value>,
verified_order: Value,
) -> OrderActionResult {
let verification_failures = verification_failures(action, &verified_order);
let verification_state = if verification_failures.is_empty() {
VerificationState::Verified
} else {
VerificationState::Unverified
};
OrderActionResult {
action: action.to_string(),
order_id: Some(id),
location,
order: submitted_order,
verification_state,
verification_failures,
verified_order: Some(verified_order),
digest: None,
original_command: None,
}
}
fn verification_failures(action: &str, order: &Value) -> Vec<String> {
if action != "cancel" {
return vec![];
}
match order.get("status").and_then(Value::as_str) {
Some("CANCELED") => vec![],
Some(status) => vec![format!(
"cancel not confirmed: expected status CANCELED, got {status}"
)],
None => vec!["cancel not confirmed: verified order did not include a status".to_string()],
}
}
pub(crate) fn action_value(result: OrderActionResult) -> Result<Value, AppError> {
Ok(serde_json::to_value(&result)?)
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn action_value_preserves_unverified_failures() {
let result = OrderActionResult {
action: "place".to_string(),
order_id: Some(12345),
location: Some("https://api.schwab.com/orders/12345".to_string()),
order: Some(json!({"orderType": "LIMIT"})),
verification_state: VerificationState::Unverified,
verification_failures: vec!["failed to retrieve order: timeout".to_string()],
verified_order: None,
digest: None,
original_command: None,
};
let data = action_value(result).unwrap();
assert_eq!(data["verification_state"], "unverified");
assert_eq!(
data["verification_failures"][0],
"failed to retrieve order: timeout"
);
}
#[test]
fn action_value_verified_includes_all_fields() {
let result = OrderActionResult {
action: "place".to_string(),
order_id: Some(12345),
location: Some("https://api.schwab.com/orders/12345".to_string()),
order: Some(json!({"orderType": "LIMIT"})),
verification_state: VerificationState::Verified,
verification_failures: vec![],
verified_order: Some(json!({"orderId": 12345, "status": "WORKING"})),
digest: None,
original_command: None,
};
let data = action_value(result).unwrap();
assert_eq!(data["order_id"], 12345);
assert_eq!(data["verification_state"], "verified");
assert_eq!(data["order"]["orderType"], "LIMIT");
assert_eq!(data["verified_order"]["status"], "WORKING");
}
#[test]
fn action_value_omits_empty_verification_failures() {
let result = OrderActionResult {
action: "cancel".to_string(),
order_id: Some(99999),
location: None,
order: None,
verification_state: VerificationState::Verified,
verification_failures: vec![],
verified_order: Some(json!({"orderId": 99999, "status": "CANCELED"})),
digest: None,
original_command: None,
};
let serialized = serde_json::to_string(&action_value(result).unwrap()).unwrap();
assert!(!serialized.contains("verification_failures"));
}
#[test]
fn action_value_includes_submitted_order_when_present() {
let submitted = json!({"orderType": "LIMIT", "price": 150.0});
let result = OrderActionResult {
action: "place".to_string(),
order_id: Some(12345),
location: None,
order: Some(submitted.clone()),
verification_state: VerificationState::Verified,
verification_failures: vec![],
verified_order: Some(json!({"orderId": 12345, "status": "WORKING"})),
digest: None,
original_command: None,
};
let data = action_value(result).unwrap();
assert_eq!(data["order"]["price"], 150.0);
}
#[test]
fn unverified_result_without_order_id() {
let result = result_without_order_id("place", None, Some(json!({"orderType": "MARKET"})));
let data = action_value(result).unwrap();
assert!(data["order_id"].is_null());
assert_eq!(data["verification_state"], "unverified");
assert_eq!(
data["verification_failures"][0],
"no order ID returned by API"
);
}
#[test]
fn retrieval_failure_result_is_unverified() {
let result = result_from_retrieval_failure(
12345,
"place",
Some("https://api.schwab.com/orders/12345".to_string()),
Some(json!({"orderType": "LIMIT"})),
&"timeout",
);
assert!(matches!(
result.verification_state,
VerificationState::Unverified
));
assert_eq!(result.order_id, Some(12345));
assert_eq!(result.order.unwrap()["orderType"], "LIMIT");
assert_eq!(result.verified_order, None);
assert_eq!(
result.verification_failures,
vec!["failed to retrieve order: timeout"]
);
}
#[test]
fn action_value_includes_preview_context() {
let result = OrderActionResult {
action: "place".to_string(),
order_id: Some(12345),
location: None,
order: Some(json!({"orderType": "LIMIT"})),
verification_state: VerificationState::Verified,
verification_failures: vec![],
verified_order: Some(json!({"orderId": 12345, "status": "WORKING"})),
digest: Some("abc123def456".to_string()),
original_command: Some("order.option.buy-to-open".to_string()),
};
let data = action_value(result).unwrap();
assert_eq!(data["digest"], "abc123def456");
assert_eq!(data["original_command"], "order.option.buy-to-open");
}
#[test]
fn place_verification_accepts_retrieved_order() {
let result = result_from_verified_order(
12345,
"place",
None,
Some(json!({"orderType": "LIMIT"})),
json!({"orderId": 12345, "status": "WORKING"}),
);
assert!(matches!(
result.verification_state,
VerificationState::Verified
));
assert!(result.verification_failures.is_empty());
assert_eq!(result.order.unwrap()["orderType"], "LIMIT");
assert_eq!(result.verified_order.unwrap()["status"], "WORKING");
}
#[test]
fn cancel_verification_requires_canceled_status() {
let result = result_from_verified_order(
12345,
"cancel",
None,
None,
json!({"orderId": 12345, "status": "PENDING_CANCEL"}),
);
assert!(matches!(
result.verification_state,
VerificationState::Unverified
));
assert_eq!(
result.verification_failures,
vec!["cancel not confirmed: expected status CANCELED, got PENDING_CANCEL"]
);
assert_eq!(result.verified_order.unwrap()["status"], "PENDING_CANCEL");
}
#[test]
fn cancel_verification_accepts_canceled_status() {
let result = result_from_verified_order(
12345,
"cancel",
None,
None,
json!({"orderId": 12345, "status": "CANCELED"}),
);
assert!(matches!(
result.verification_state,
VerificationState::Verified
));
assert!(result.verification_failures.is_empty());
}
#[test]
fn cancel_verification_requires_status_field() {
let result =
result_from_verified_order(12345, "cancel", None, None, json!({"orderId": 12345}));
assert!(matches!(
result.verification_state,
VerificationState::Unverified
));
assert_eq!(
result.verification_failures,
vec!["cancel not confirmed: verified order did not include a status"]
);
assert_eq!(result.verified_order.unwrap()["orderId"], 12345);
}
}