use crate::model::ViolationDetails;
pub fn subtotal_should_equal_sum_of_line_items(
data: &serde_json::Value,
) -> Option<ViolationDetails> {
let subtotal = data
.get("header")
.and_then(|header| header.get("subtotal"))
.and_then(|subtotal| subtotal.as_i64())
.unwrap_or(0);
let itemization = data.get("itemization")?;
let mut line_items_total = 0i64;
if let Some(general) = itemization.get("general") {
if let Some(items) = general.get("items").and_then(|i| i.as_array()) {
for item in items {
if let Some(amount) = item.get("amount").and_then(|a| a.as_i64()) {
line_items_total += amount;
}
}
}
}
if let Some(ecommerce) = itemization.get("ecommerce") {
if let Some(shipments) = ecommerce.get("shipments").and_then(|s| s.as_array()) {
for shipment in shipments {
if let Some(items) = shipment.get("items").and_then(|i| i.as_array()) {
for item in items {
if let Some(amount) = item.get("amount").and_then(|a| a.as_i64()) {
line_items_total += amount;
}
}
}
}
}
if let Some(invoice_items) = ecommerce
.get("invoice_level_line_items")
.and_then(|i| i.as_array())
{
for item in invoice_items {
if let Some(amount) = item.get("amount").and_then(|a| a.as_i64()) {
line_items_total += amount;
}
}
}
}
if let Some(lodging) = itemization.get("lodging") {
if let Some(items) = lodging.get("items").and_then(|i| i.as_array()) {
for item in items {
if let Some(amount) = item.get("amount").and_then(|a| a.as_i64()) {
line_items_total += amount;
}
}
}
}
if let Some(car_rental) = itemization.get("car_rental") {
if let Some(items) = car_rental.get("items").and_then(|i| i.as_array()) {
for item in items {
if let Some(amount) = item.get("amount").and_then(|a| a.as_i64()) {
line_items_total += amount;
}
}
}
}
if let Some(subscription) = itemization.get("subscription") {
if let Some(items) = subscription.get("items").and_then(|i| i.as_array()) {
for item in items {
if let Some(total) = item.get("total").and_then(|t| t.as_i64()) {
line_items_total += total;
}
}
}
else if let Some(items) = subscription
.get("subscription_items")
.and_then(|i| i.as_array())
{
for item in items {
if let Some(amount) = item.get("amount").and_then(|a| a.as_i64()) {
line_items_total += amount;
}
}
}
}
if let Some(flight) = itemization.get("flight") {
if let Some(tickets) = flight.get("tickets").and_then(|t| t.as_array()) {
for ticket in tickets {
if let Some(subtotal) = ticket.get("subtotal").and_then(|s| s.as_i64()) {
line_items_total += subtotal;
} else if let Some(fare) = ticket.get("fare").and_then(|f| f.as_i64()) {
line_items_total += fare;
} else {
if let Some(segments) = ticket.get("segments").and_then(|s| s.as_array()) {
for segment in segments {
if let Some(segment_fare) = segment.get("fare").and_then(|f| f.as_i64()) {
line_items_total += segment_fare;
}
}
}
}
}
}
}
if let Some(transit_route) = itemization.get("transit_route") {
if let Some(items) = transit_route
.get("transit_route_items")
.and_then(|i| i.as_array())
{
for item in items {
if let Some(fare) = item.get("fare").and_then(|f| f.as_i64()) {
line_items_total += fare;
}
}
}
}
if subtotal != line_items_total {
return Some(ViolationDetails {
details: Some(format!(
"subtotal={}; sum_of_line_items={}",
subtotal, line_items_total
)),
});
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_subtotal_equals_sum_of_general_items() {
let valid_receipt = serde_json::json!({
"header": { "subtotal": 300 },
"itemization": {
"general": {
"items": [
{ "amount": 100 },
{ "amount": 150 },
{ "amount": 50 }
]
}
}
});
let result = subtotal_should_equal_sum_of_line_items(&valid_receipt);
assert!(result.is_none());
}
#[test]
fn test_subtotal_not_equals_sum_of_items() {
let invalid_receipt = serde_json::json!({
"header": { "subtotal": 400 },
"itemization": {
"general": {
"items": [
{ "amount": 100 },
{ "amount": 150 },
{ "amount": 50 }
]
}
}
});
let result = subtotal_should_equal_sum_of_line_items(&invalid_receipt);
assert!(result.is_some());
let violation = result.unwrap();
assert_eq!(
violation.details,
Some("subtotal=400; sum_of_line_items=300".to_string())
);
}
#[test]
fn test_subtotal_with_multiple_itemization_types() {
let valid_receipt = serde_json::json!({
"header": { "subtotal": 600 },
"itemization": {
"general": {
"items": [
{ "amount": 100 },
{ "amount": 150 }
]
},
"ecommerce": {
"shipments": [
{
"items": [
{ "amount": 200 },
{ "amount": 150 }
]
}
]
}
}
});
let result = subtotal_should_equal_sum_of_line_items(&valid_receipt);
assert!(result.is_none());
}
#[test]
fn test_subtotal_with_flight_ticket_fare() {
let valid_receipt = serde_json::json!({
"header": { "subtotal": 1000 },
"itemization": {
"flight": {
"tickets": [
{ "fare": 600 },
{ "fare": 400 }
]
}
}
});
let result = subtotal_should_equal_sum_of_line_items(&valid_receipt);
assert!(result.is_none());
}
#[test]
fn test_subtotal_with_segment_fares() {
let valid_receipt = serde_json::json!({
"header": { "subtotal": 1200 },
"itemization": {
"flight": {
"tickets": [
{
"segments": [
{ "fare": 300 },
{ "fare": 300 }
]
},
{
"segments": [
{ "fare": 300 },
{ "fare": 300 }
]
}
]
}
}
});
let result = subtotal_should_equal_sum_of_line_items(&valid_receipt);
assert!(result.is_none());
}
#[test]
fn test_subtotal_with_subscription_items() {
let valid_receipt = serde_json::json!({
"header": { "subtotal": 1500 },
"itemization": {
"subscription": {
"subscription_items": [
{ "amount": 1000 },
{ "amount": 500 }
]
}
}
});
let result = subtotal_should_equal_sum_of_line_items(&valid_receipt);
assert!(result.is_none());
}
#[test]
fn test_subtotal_with_missing_itemization() {
let receipt = serde_json::json!({
"header": { "subtotal": 100 }
});
let result = subtotal_should_equal_sum_of_line_items(&receipt);
assert!(result.is_none()); }
#[test]
fn test_subtotal_with_empty_itemization() {
let receipt = serde_json::json!({
"header": { "subtotal": 0 },
"itemization": {}
});
let result = subtotal_should_equal_sum_of_line_items(&receipt);
assert!(result.is_none()); }
#[test]
fn test_subtotal_mismatch_with_empty_itemization() {
let receipt = serde_json::json!({
"header": { "subtotal": 100 },
"itemization": {}
});
let result = subtotal_should_equal_sum_of_line_items(&receipt);
assert!(result.is_some());
let violation = result.unwrap();
assert_eq!(
violation.details,
Some("subtotal=100; sum_of_line_items=0".to_string())
);
}
}