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-itemization",
"The header total should equal the sum of all line items, taxes, and adjustments.",
header_total_should_be_sum_of_all_line_items_taxes_and_adjustments,
),
Rule::new(
"schema-version-should-be-current",
"The schema should be updated to the latest version.",
crate::schema_version::schema_version_should_be_current,
)
]
}
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
}
fn get_i64(value: &serde_json::Value, key: &str) -> i64 {
value.get(key).and_then(|v| v.as_i64()).unwrap_or(0)
}
fn header_total_should_be_sum_of_all_line_items_taxes_and_adjustments(
data: &serde_json::Value,
) -> Option<ViolationDetails> {
let mut line_items_total = 0;
let mut taxes_total = 0;
let mut adjustments_total = 0;
let header_total = data
.get("header")
.and_then(|header| header.get("total"))
.and_then(|total| total.as_i64())
.unwrap_or(0);
let flight = data
.get("itemization")
.and_then(|itemization| itemization.get("flight"))
.and_then(|flight| if flight.is_null() { None } else { Some(flight) });
if flight.is_none() {
return None;
}
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 {
line_items_total += get_i64(&ticket, "fare");
if let Some(taxes) = ticket.get("taxes").and_then(|taxes| taxes.as_array()) {
for tax in taxes {
taxes_total += get_i64(tax, "amount");
}
}
if let Some(segments) = ticket.get("segments").and_then(|s| s.as_array()) {
for segment in segments {
line_items_total += get_i64(segment, "fare");
if let Some(adjustments) = segment
.get("adjustments")
.and_then(|adjustments| adjustments.as_array())
{
for adjustment in adjustments {
adjustments_total += get_i64(adjustment, "amount");
}
}
if let Some(taxes) = segment.get("taxes").and_then(|taxes| taxes.as_array()) {
for tax in taxes {
taxes_total += get_i64(tax, "amount");
}
}
}
}
}
}
if let Some(adjustments) = data
.get("itemization")
.and_then(|itemization| itemization.get("flight"))
.and_then(|flight| flight.get("invoice_level_adjustments"))
.and_then(|adjustments| adjustments.as_array())
{
for adjustment in adjustments {
adjustments_total += get_i64(adjustment, "amount");
}
}
if line_items_total + taxes_total + adjustments_total == header_total {
None
} else {
Some(ViolationDetails {
details: Some(format!(
"line_items_total={}; taxes_total={}; adjustments_total={}; header_total={}",
line_items_total, taxes_total, adjustments_total, header_total
)),
})
}
}