use en16931::invoice::*;
use en16931::{Date, InvoiceAmount, Percentage, Quantity, UnitPriceAmount, profiles, validate};
use proptest::prelude::*;
use rust_decimal::Decimal;
fn any_amount() -> impl Strategy<Value = InvoiceAmount> {
prop_oneof![
Just(InvoiceAmount::from_minor_units(i64::MAX)),
Just(InvoiceAmount::from_minor_units(i64::MIN)),
Just(InvoiceAmount::ZERO),
any::<i64>().prop_map(InvoiceAmount::from_minor_units),
(-1_000_000i64..1_000_000).prop_map(InvoiceAmount::from_minor_units),
]
}
fn any_code() -> impl Strategy<Value = Code> {
prop_oneof![
Just(Code::new("")),
Just(Code::new("S")),
Just(Code::new("Z")),
Just(Code::new("O")),
Just(Code::new("EUR")),
"[A-Za-z0-9 ]{0,8}".prop_map(Code::new),
]
}
fn any_percentage() -> impl Strategy<Value = Percentage> {
prop_oneof![
Just(Percentage::ZERO),
Just(Percentage::new(Decimal::MAX)),
Just(Percentage::new(Decimal::MIN)),
(-100i64..1000).prop_map(|v| Percentage::new(Decimal::from(v))),
]
}
fn any_quantity() -> impl Strategy<Value = Quantity> {
prop_oneof![
Just(Quantity::new(Decimal::ZERO)),
Just(Quantity::new(Decimal::MAX)),
Just(Quantity::new(Decimal::MIN)),
(-1000i64..1000).prop_map(|v| Quantity::new(Decimal::from(v))),
]
}
fn any_line() -> impl Strategy<Value = InvoiceLine> {
(
any_amount(),
any_quantity(),
any_code(),
any_code(),
any_percentage(),
any::<bool>(),
)
.prop_map(|(net, qty, unit, cat, rate, has_rate)| InvoiceLine {
id: String::new(),
note: None,
order_line_reference: None,
accounting_reference: None,
object_identifier: None,
quantity: qty,
unit_code: unit,
net_amount: net,
period: None,
allowances: vec![],
charges: vec![],
price: PriceDetails {
base_quantity: Some(qty),
net_price: UnitPriceAmount::new(Decimal::from(2)),
..Default::default()
},
vat: LineVat {
category: cat,
rate: has_rate.then_some(rate),
},
item: Item::default(),
})
}
fn any_breakdown() -> impl Strategy<Value = VatBreakdown> {
(any_amount(), any_amount(), any_code(), any_percentage()).prop_map(
|(taxable, tax, cat, rate)| VatBreakdown {
taxable_amount: taxable,
tax_amount: tax,
category: cat,
rate: Some(rate),
exemption_reason: None,
exemption_reason_code: None,
},
)
}
prop_compose! {
fn any_invoice()(
lines in prop::collection::vec(any_line(), 0..12),
breakdown in prop::collection::vec(any_breakdown(), 0..6),
line_total in any_amount(),
taxable in any_amount(),
vat in proptest::option::of(any_amount()),
gross in any_amount(),
due in any_amount(),
currency in proptest::option::of(any_code()),
type_code in proptest::option::of(any_code()),
credit_note in any::<bool>(),
) -> Invoice {
let mut inv = Invoice::default();
inv.kind = if credit_note { DocumentKind::CreditNote } else { DocumentKind::Invoice };
inv.currency = currency;
inv.type_code = type_code;
inv.lines = lines;
inv.vat_breakdown = breakdown;
inv.totals = DocumentTotals {
line_total,
taxable_total: taxable,
vat_total: vat,
gross_total: gross,
due,
..Default::default()
};
inv
}
}
proptest! {
#[test]
fn validate_never_panics(inv in any_invoice()) {
let report = validate(&inv);
let _ = report.is_valid();
let _ = report.rules_checked();
let _ = report.fatal().count();
let _ = report.warnings().count();
let _ = report.info().count();
let _ = report.advisory().count();
let _ = report.to_string();
}
#[test]
fn no_profile_panics(inv in any_invoice()) {
for p in profiles::ALL {
let report = p.validate(&inv);
let _ = report.is_valid();
let _ = report.to_string();
}
}
#[test]
fn reports_are_deterministic(inv in any_invoice()) {
let a = validate(&inv);
let b = validate(&inv);
prop_assert_eq!(a.findings().len(), b.findings().len());
for (x, y) in a.findings().iter().zip(b.findings()) {
prop_assert_eq!(&x.rule, &y.rule);
prop_assert_eq!(x.path.to_string(), y.path.to_string());
}
}
#[test]
fn every_finding_can_be_explained(inv in any_invoice()) {
use en16931::validation::rules::{explain, explain_restriction};
for p in profiles::ALL {
for f in p.validate(&inv).findings() {
prop_assert!(
explain(&f.rule).is_some() || explain_restriction(&f.rule).is_some(),
"{} reports {} and nothing can explain it", p.id, f.rule
);
}
}
}
#[test]
fn conformant_profiles_never_accept_what_core_rejects(inv in any_invoice()) {
for p in profiles::ALL.iter().filter(|p| p.is_conformant_cius()) {
let mut doc = inv.clone();
doc.specification_id = Some(p.specification_id.to_owned());
if p.validate(&doc).is_valid() {
prop_assert!(
validate(&doc).is_valid(),
"{} accepted a document core EN 16931 rejects", p.id
);
}
}
}
#[test]
fn is_valid_means_no_fatal_findings(inv in any_invoice()) {
let report = validate(&inv);
prop_assert_eq!(report.is_valid(), report.fatal().count() == 0);
}
}
fn line(net: InvoiceAmount) -> InvoiceLine {
InvoiceLine {
id: String::new(),
note: None,
order_line_reference: None,
accounting_reference: None,
object_identifier: None,
quantity: Quantity::new(Decimal::ONE),
unit_code: Code::new("C62"),
net_amount: net,
period: None,
allowances: vec![],
charges: vec![],
price: PriceDetails::default(),
vat: LineVat::default(),
item: Item::default(),
}
}
#[test]
fn the_extremes_are_survivable() {
let mut inv = Invoice::default();
inv.totals.line_total = InvoiceAmount::from_minor_units(i64::MAX);
inv.lines = (0..3)
.map(|_| line(InvoiceAmount::from_minor_units(i64::MAX)))
.collect();
let _ = validate(&inv);
let mut inv = Invoice::default();
let mut l = line(InvoiceAmount::ZERO);
l.price.base_quantity = Some(Quantity::new(Decimal::ZERO));
inv.lines = vec![l];
for p in profiles::ALL {
let _ = p.validate(&inv);
}
assert!(Date::parse("9999-12-31").is_ok());
assert!(Date::parse("0000-01-01").is_ok());
}