versa_semval 0.10.0

Cross-platform module for semantic validation of Versa data
Documentation
use crate::model::ViolationDetails;

pub fn service_non_recurring_should_not_have_interval<'a>(
  data: &'a serde_json::Value,
) -> Option<ViolationDetails> {
  let service_items = data
    .get("itemization")
    .and_then(|itemization| itemization.get("service"))
    .and_then(|service| service.get("service_items"))
    .and_then(|items| items.as_array());

  if let Some(items) = service_items {
    for (index, item) in items.iter().enumerate() {
      let recurring = item.get("recurring").and_then(|r| r.as_bool());
      let interval = item.get("interval");
      let interval_count = item.get("interval_count");

      // If recurring is explicitly false, interval and interval_count should be null
      if recurring == Some(false) {
        let has_interval = interval.is_some() && !interval.unwrap().is_null();
        let has_interval_count = interval_count.is_some() && !interval_count.unwrap().is_null();

        if has_interval || has_interval_count {
          return Some(ViolationDetails {
            details: Some(format!(
              "service_items[{}]: recurring=false but interval={} and interval_count={}",
              index,
              interval.and_then(|v| v.as_str()).unwrap_or("null"),
              interval_count
                .and_then(|v| v.as_i64())
                .map(|n| n.to_string())
                .unwrap_or_else(|| "null".to_string())
            )),
          });
        }
      }
    }
  }

  None
}

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

  #[test]
  fn test_non_recurring_without_interval_passes() {
    let data = json!({
      "itemization": {
        "service": {
          "service_items": [
            {
              "recurring": false,
              "amount": 1000
            }
          ]
        }
      }
    });

    assert!(service_non_recurring_should_not_have_interval(&data).is_none());
  }

  #[test]
  fn test_non_recurring_with_null_interval_passes() {
    let data = json!({
      "itemization": {
        "service": {
          "service_items": [
            {
              "recurring": false,
              "interval": null,
              "interval_count": null,
              "amount": 1000
            }
          ]
        }
      }
    });

    assert!(service_non_recurring_should_not_have_interval(&data).is_none());
  }

  #[test]
  fn test_recurring_with_interval_passes() {
    let data = json!({
      "itemization": {
        "service": {
          "service_items": [
            {
              "recurring": true,
              "interval": "month",
              "interval_count": 1,
              "amount": 1000
            }
          ]
        }
      }
    });

    assert!(service_non_recurring_should_not_have_interval(&data).is_none());
  }

  #[test]
  fn test_non_recurring_with_interval_fails() {
    let data = json!({
      "itemization": {
        "service": {
          "service_items": [
            {
              "recurring": false,
              "interval": "month",
              "amount": 1000
            }
          ]
        }
      }
    });

    let result = service_non_recurring_should_not_have_interval(&data);
    assert!(result.is_some());
    let violation = result.unwrap();
    assert!(violation.details.is_some());
    assert!(violation.details.unwrap().contains("service_items[0]"));
  }

  #[test]
  fn test_non_recurring_with_interval_count_fails() {
    let data = json!({
      "itemization": {
        "service": {
          "service_items": [
            {
              "recurring": false,
              "interval_count": 3,
              "amount": 1000
            }
          ]
        }
      }
    });

    let result = service_non_recurring_should_not_have_interval(&data);
    assert!(result.is_some());
  }

  #[test]
  fn test_non_recurring_with_both_interval_and_count_fails() {
    let data = json!({
      "itemization": {
        "service": {
          "service_items": [
            {
              "recurring": false,
              "interval": "week",
              "interval_count": 2,
              "amount": 1000
            }
          ]
        }
      }
    });

    let result = service_non_recurring_should_not_have_interval(&data);
    assert!(result.is_some());
  }

  #[test]
  fn test_no_service_itemization_passes() {
    let data = json!({
      "itemization": {
        "general": {
          "items": []
        }
      }
    });

    assert!(service_non_recurring_should_not_have_interval(&data).is_none());
  }

  #[test]
  fn test_multiple_items_first_violates() {
    let data = json!({
      "itemization": {
        "service": {
          "service_items": [
            {
              "recurring": false,
              "interval": "month",
              "amount": 1000
            },
            {
              "recurring": true,
              "interval": "year",
              "interval_count": 1,
              "amount": 2000
            }
          ]
        }
      }
    });

    let result = service_non_recurring_should_not_have_interval(&data);
    assert!(result.is_some());
    assert!(
      result
        .unwrap()
        .details
        .unwrap()
        .contains("service_items[0]")
    );
  }

  #[test]
  fn test_multiple_items_all_valid() {
    let data = json!({
      "itemization": {
        "service": {
          "service_items": [
            {
              "recurring": false,
              "amount": 1000
            },
            {
              "recurring": true,
              "interval": "month",
              "interval_count": 1,
              "amount": 2000
            }
          ]
        }
      }
    });

    assert!(service_non_recurring_should_not_have_interval(&data).is_none());
  }
}