versa_semval 0.7.2

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

pub struct Rule<'a, F>
where
  F: Fn(&'a serde_json::Value) -> Option<ViolationDetails>,
{
  pub name: &'a str,
  pub description: &'a str,
  pub evaluate: F,
}

impl<'a, F> Rule<'a, F>
where
  F: Fn(&'a serde_json::Value) -> Option<ViolationDetails>,
{
  fn new(name: &'a str, description: &'a str, evaluate: F) -> Self {
    Self {
      name,
      description,
      evaluate,
    }
  }
}

pub fn rules<'a>() -> Vec<Rule<'a, fn(&'a serde_json::Value) -> Option<ViolationDetails>>> {
  vec![
    Rule::new(
      "header_paid_should_be_sum_of_payments",
      "The sum of all payments should always equal the amount in the header's 'paid' field.",
      header_paid_should_be_sum_of_payments,
    ),
    Rule::new(
      "all_headers_should_have_tz",
      "All receipts should include a timezone that best represents either the customer's physical location at the time of purchase, or the merchant's region of business.",
      all_headers_should_have_tz
    ),
    Rule::new(
      "flight_fare_at_ticket_or_segment_exclusively",
      "Flight receipts should set the fare at the ticket level, or the segment level, but not both.",
      flight_fare_at_ticket_or_segment_exclusively,
    ),
    Rule::new(
      "header_total_should_be_sum_of_all_line_items_taxes_and_adjustments",
      "The header total should equal the sum of all line items, taxes, and adjustments.",
      crate::total::header_total_should_be_sum_of_all_line_items_taxes_and_adjustments,
    ),
    Rule::new(
      "itemization_should_should_not_be_empty_if_totals_greater_than_zero",
      "The itemization should always be populated if the total or subtotal is greater than zero.",
      crate::itemization::itemization_should_should_not_be_empty_if_totals_greater_than_zero,
    ),
    Rule::new(
      "schema_version_should_be_current",
      "The schema should be updated to the latest version.",
      crate::schema_version::schema_version_should_be_current,
    ),
    Rule::new(
      "subtotal_should_equal_sum_of_line_items",
      "The subtotal should equal the sum of all line item amounts.",
      crate::subtotal::subtotal_should_equal_sum_of_line_items,
    )
  ]
}

fn header_paid_should_be_sum_of_payments<'a>(
  data: &'a serde_json::Value,
) -> Option<ViolationDetails> {
  let payments_array = data.get("payments").and_then(|val| val.as_array());

  let paid = data
    .get("header")
    .and_then(|header| header.get("paid"))
    .and_then(|paid| paid.as_i64());

  if let Some(paid) = paid {
    let mut payment_subtotal = 0;
    if let Some(payments) = payments_array {
      for payment in payments {
        payment_subtotal += payment
          .get("amount")
          .and_then(|amount| amount.as_i64())
          .unwrap_or(0);
      }
    }
    if payment_subtotal != paid {
      return Some(ViolationDetails {
        details: Some(format!(
          "paid={}; sum_of_payments={}",
          paid, payment_subtotal
        )),
      });
    }
    return None;
  }
  return Some(ViolationDetails { details: None });
}

// paid should always equal sum of payments

fn all_headers_should_have_tz<'a>(data: &'a serde_json::Value) -> Option<ViolationDetails> {
  let timezone = data
    .get("header")
    .and_then(|header| header.get("location"))
    .and_then(|location| location.get("address"))
    .and_then(|address| address.get("tz"));

  if let Some(tz) = timezone {
    if (!tz.is_null()) && tz.is_string() {
      return None;
    }
  }
  return Some(ViolationDetails { details: None });
}

// Do flight tickets have either ticket level or segment level fare data, but not both

fn flight_fare_at_ticket_or_segment_exclusively<'a>(
  data: &'a serde_json::Value,
) -> Option<ViolationDetails> {
  let mut fare_on_ticket = false;
  let mut fare_on_segment = false;

  let tickets = data
    .get("itemization")
    .and_then(|itemization| itemization.get("flight"))
    .and_then(|flight| flight.get("tickets"))
    .and_then(|tickets| tickets.as_array());

  if let Some(tickets) = tickets {
    for ticket in tickets {
      if let Some(fare) = ticket.get("fare") {
        if !fare.is_null() {
          fare_on_ticket = true;
        }
      }
      if let Some(segments) = ticket.get("segments").and_then(|s| s.as_array()) {
        for segment in segments {
          if let Some(fare) = segment.get("fare") {
            if !fare.is_null() {
              fare_on_segment = true;
            }
          }
        }
      }
    }
  }
  if fare_on_ticket && fare_on_segment {
    return Some(ViolationDetails { details: None });
  }

  None
}