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 });
}
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 });
}
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
}