use en16931::DocumentReference;
use en16931::invoice::*;
use en16931::profiles::{self, En16931, PeppolBis3, XRechnung};
use en16931::validation::profile::{ProfileMarker, Validated};
use en16931::*;
use rust_decimal::dec;
fn amount(s: &str) -> InvoiceAmount {
InvoiceAmount::parse(s).unwrap()
}
fn pct(v: i64) -> Percentage {
Percentage::new(rust_decimal::Decimal::from(v))
}
fn core_valid() -> Invoice {
let party = |name: &str, country: &str| Party {
name: Some(name.to_owned()),
address: PostalAddress {
country: Some(Code::new(country)),
..Default::default()
},
electronic_address: Some(Identifier::schemed(name, "0088")),
vat_identifier: Some(format!("{country}123456789")), ..Default::default()
};
let line = InvoiceLine {
id: "1".to_owned(),
note: None,
order_line_reference: None,
accounting_reference: None,
object_identifier: None,
quantity: Quantity::new(dec!(1)),
unit_code: Code::new("C62"),
net_amount: amount("100.00"),
period: None,
allowances: vec![],
charges: vec![],
price: PriceDetails {
net_price: UnitPriceAmount::new(dec!(100)),
price_discount: None,
gross_price: None,
base_quantity: None,
base_quantity_code: None,
},
vat: LineVat {
category: Code::new("S"),
rate: Some(pct(19)),
},
item: Item {
name: Some("Widget".to_owned()),
..Default::default()
},
};
Invoice::builder(
profiles::EN16931.specification_id,
"INV-1",
Date::parse("2026-06-30").unwrap(),
Code::new("380"),
Code::new("EUR"),
)
.seller(party("Seller GmbH", "DE"))
.buyer(party("Buyer BV", "NL"))
.due_date(Date::parse("2026-07-30").unwrap())
.line(line)
.vat_breakdown(VatBreakdown {
taxable_amount: amount("100.00"),
tax_amount: amount("19.00"),
category: Code::new("S"),
rate: Some(pct(19)),
exemption_reason: None,
exemption_reason_code: None,
})
.totals(DocumentTotals {
line_total: amount("100.00"),
allowance_total: None,
charge_total: None,
taxable_total: amount("100.00"),
vat_total: Some(amount("19.00")),
vat_total_accounting: None,
gross_total: amount("119.00"),
paid: None,
rounding: None,
due: amount("119.00"),
})
.build()
}
fn xrechnung_valid() -> Invoice {
let mut inv = core_valid();
inv.specification_id = Some(profiles::XRECHNUNG.specification_id.to_owned());
inv.buyer_reference = Some("04011000-12345-34".to_owned()); inv.seller.address.city = Some("Berlin".to_owned()); inv.seller.address.post_code = Some("10115".to_owned()); inv.delivery = Some(Delivery {
date: Some(Date::parse("2026-06-30").unwrap()), ..Default::default()
});
inv.business_process = Some("urn:fdc:peppol.eu:2017:poacc:billing:01:1.0".to_owned());
inv.seller.contact = Contact {
name: Some("Frau Muster".to_owned()), phone: Some("+49 30 123456".to_owned()), email: Some("rechnung@seller.de".to_owned()), };
inv.buyer.address.city = Some("Amsterdam".to_owned()); inv.buyer.address.post_code = Some("1011".to_owned()); inv.payment = Some(PaymentInstructions {
means_code: Some(Code::new("58")), means: Some(PaymentMeans::CreditTransfer(vec![CreditTransfer {
account_identifier: Some("DE89370400440532013000".to_owned()),
..Default::default()
}])),
..Default::default()
}); inv
}
#[test]
fn a_cius_restricts_and_the_direction_matters() {
let core = core_valid();
assert!(
profiles::EN16931.validate(&core).is_valid(),
"{}",
profiles::EN16931.validate(&core)
);
let report = profiles::XRECHNUNG.validate(&core);
assert!(!report.is_valid());
for id in [
"BR-DE-1", "BR-DE-3", "BR-DE-4", "BR-DE-5", "BR-DE-6", "BR-DE-7", "BR-DE-8", "BR-DE-9",
"BR-DE-15", "BR-DE-21",
] {
assert!(report.has(id), "{id} did not fire:\n{report}");
}
let xr = xrechnung_valid();
assert!(
profiles::XRECHNUNG.validate(&xr).is_valid(),
"{}",
profiles::XRECHNUNG.validate(&xr)
);
assert!(profiles::EN16931.validate(&xr).is_valid());
}
#[test]
fn findings_carry_the_real_br_de_ids_and_business_term_paths() {
let report = profiles::XRECHNUNG.validate(&core_valid());
let f = report
.fatal()
.find(|f| f.rule == "BR-DE-3")
.expect("BR-DE-3");
assert_eq!(f.path.to_string(), "BG-4/BT-37");
assert!(f.message.contains("Seller city"), "{}", f.message);
}
#[test]
fn br_de_14_requires_bt_119_where_br_48_exempts_it() {
let mut inv = xrechnung_valid();
inv.lines[0].vat = LineVat {
category: Code::new("O"),
rate: None, };
inv.vat_breakdown = vec![VatBreakdown {
taxable_amount: amount("100.00"),
tax_amount: amount("0.00"),
category: Code::new("O"),
rate: None, exemption_reason: Some("Not subject to VAT".to_owned()),
exemption_reason_code: None,
}];
inv.totals.vat_total = Some(amount("0.00"));
inv.totals.gross_total = amount("100.00");
inv.totals.due = amount("100.00");
assert!(
!profiles::EN16931.validate(&inv).has("BR-48"),
"BR-48 exempts category O"
);
assert!(
profiles::XRECHNUNG.validate(&inv).has("BR-DE-14"),
"BR-DE-14 has no category exception"
);
}
#[test]
fn xrechnung_and_peppol_genuinely_disagree() {
let mut inv = xrechnung_valid();
inv.type_code = Some(Code::new("389")); inv.business_process = Some("urn:fdc:peppol.eu:2017:poacc:billing:01:1.0".to_owned());
assert!(!profiles::XRECHNUNG.validate(&inv).has("BR-DE-17"));
assert!(
profiles::PEPPOL_BIS_3
.validate(&inv)
.has("PEPPOL-EN16931-P0100")
);
inv.type_code = Some(Code::new("386"));
assert!(profiles::XRECHNUNG.validate(&inv).has("BR-DE-17"));
assert!(
!profiles::PEPPOL_BIS_3
.validate(&inv)
.has("PEPPOL-EN16931-P0100")
);
}
#[test]
fn a_document_selects_its_own_rule_set() {
let inv = xrechnung_valid();
let declared = inv.specification_id.as_deref().unwrap();
let profile = profiles::for_specification_id(declared).expect("known profile");
assert_eq!(profile.id, "XRechnung 3.0");
assert!(profile.validate(&inv).is_valid());
}
#[test]
fn a_proof_survives_the_call_boundary_and_widens_for_free() {
fn serialise_xrechnung(v: &Validated<XRechnung>) -> String {
v.invoice().number.clone().unwrap_or_default()
}
fn accepts_core(_: Validated<En16931>) {}
let proof: Validated<XRechnung> = Validated::new(xrechnung_valid())
.map_err(|b| b.1.to_string())
.unwrap();
assert_eq!(serialise_xrechnung(&proof), "INV-1");
accepts_core(proof.widen());
let rejected = Validated::<XRechnung>::new(core_valid()).unwrap_err();
let (returned, report) = *rejected;
assert_eq!(returned.number.as_deref(), Some("INV-1"));
assert!(report.has("BR-DE-15"));
}
#[test]
fn conformance_is_reported_honestly() {
let conformant: Vec<&str> = profiles::ALL
.iter()
.filter(|p| p.is_conformant_cius())
.map(|p| p.id)
.collect();
assert_eq!(
conformant,
["EN 16931", "XRechnung 3.0", "Peppol BIS Billing 3.0"],
"a profile that suppresses a core rule is not a conformant CIUS"
);
for p in profiles::ALL {
assert_eq!(
p.is_conformant_cius(),
p.suppressed.is_empty(),
"{} — conformance and suppression must agree",
p.id
);
}
}
#[test]
fn a_conformant_cius_never_accepts_what_core_rejects() {
let docs = [core_valid(), xrechnung_valid()];
for p in profiles::ALL.iter().filter(|p| p.is_conformant_cius()) {
for doc in &docs {
let mut doc = doc.clone();
doc.specification_id = Some(p.specification_id.to_owned());
if p.validate(&doc).is_valid() {
let core = en16931::validate(&doc);
assert!(
core.is_valid(),
"{} accepted a document core EN 16931 rejects, so §4.4.4's \
widening guarantee does not hold for it:\n{core}",
p.id
);
}
}
}
}
#[test]
fn profile_reports_state_their_own_coverage() {
let core_only = profiles::EN16931.validate(&core_valid());
let with_cius = profiles::XRECHNUNG.validate(&core_valid());
assert!(
with_cius.rules_checked() > core_only.rules_checked(),
"{} vs {}",
with_cius.rules_checked(),
core_only.rules_checked()
);
}
#[test]
fn markers_match_their_profiles() {
assert_eq!(En16931::PROFILE.id, profiles::EN16931.id);
assert_eq!(XRechnung::PROFILE.id, profiles::XRECHNUNG.id);
assert_eq!(PeppolBis3::PROFILE.id, profiles::PEPPOL_BIS_3.id);
}
#[test]
fn peppols_line_arithmetic_is_a_third_tolerance_regime() {
let build = |net: &str| {
let mut inv = core_valid();
inv.specification_id = Some(profiles::PEPPOL_BIS_3.specification_id.to_owned());
inv.buyer_reference = Some("REF".to_owned()); inv.lines[0].quantity = Quantity::new(dec!(1));
inv.lines[0].price.net_price = UnitPriceAmount::new(dec!(100));
inv.lines[0].net_amount = amount(net);
inv.totals.line_total = amount(net);
inv.totals.taxable_total = amount(net);
inv.vat_breakdown[0].taxable_amount = amount(net);
inv
};
let inv = build("100.02");
assert!(
!profiles::PEPPOL_BIS_3
.validate(&inv)
.has("PEPPOL-EN16931-R120")
);
let inv = build("100.03");
assert!(
profiles::PEPPOL_BIS_3
.validate(&inv)
.has("PEPPOL-EN16931-R120")
);
let inv = build("999.00");
assert!(!profiles::EN16931.validate(&inv).has("PEPPOL-EN16931-R120"));
}
#[test]
fn r046_is_exact_where_r040_is_tolerant() {
let mut inv = core_valid();
inv.specification_id = Some(profiles::PEPPOL_BIS_3.specification_id.to_owned());
inv.buyer_reference = Some("REF".to_owned());
inv.lines[0].price.gross_price = Some(UnitPriceAmount::new(dec!(101)));
inv.lines[0].price.price_discount = Some(UnitPriceAmount::new(dec!(1)));
assert!(
!profiles::PEPPOL_BIS_3
.validate(&inv)
.has("PEPPOL-EN16931-R046")
);
inv.lines[0].price.price_discount = Some(UnitPriceAmount::new(dec!(0.99)));
assert!(
profiles::PEPPOL_BIS_3
.validate(&inv)
.has("PEPPOL-EN16931-R046")
);
}
#[test]
fn r130_compares_two_terms_neither_type_owns() {
let mut inv = core_valid();
inv.specification_id = Some(profiles::PEPPOL_BIS_3.specification_id.to_owned());
inv.buyer_reference = Some("REF".to_owned());
inv.lines[0].price.base_quantity = Some(Quantity::new(dec!(1)));
inv.lines[0].price.base_quantity_code = Some(Code::new("H87"));
let report = profiles::PEPPOL_BIS_3.validate(&inv);
assert!(report.has("PEPPOL-EN16931-R130"), "{report}");
inv.lines[0].price.base_quantity_code = Some(Code::new("C62"));
assert!(
!profiles::PEPPOL_BIS_3
.validate(&inv)
.has("PEPPOL-EN16931-R130")
);
}
#[test]
fn a_zero_base_quantity_is_reported_and_never_divided_by() {
let mut inv = core_valid();
inv.specification_id = Some(profiles::PEPPOL_BIS_3.specification_id.to_owned());
inv.buyer_reference = Some("REF".to_owned());
inv.lines[0].price.base_quantity = Some(Quantity::ZERO);
let report = profiles::PEPPOL_BIS_3.validate(&inv); assert!(report.has("PEPPOL-EN16931-R121"), "{report}");
assert!(
!report.has("PEPPOL-EN16931-R120"),
"R120 must skip, not divide"
);
}
#[test]
fn profiles_carry_conditional_rules_that_restrictions_cannot_express() {
assert!(
profiles::EN16931.extra_rules.is_empty(),
"core adds nothing"
);
assert!(!profiles::PEPPOL_BIS_3.extra_rules.is_empty());
assert!(!profiles::XRECHNUNG.extra_rules.is_empty());
}
#[test]
fn the_payment_groups_are_mutually_exclusive_by_construction() {
let mut inv = xrechnung_valid();
assert!(!profiles::XRECHNUNG.validate(&inv).has("BR-DE-23-a"));
inv.payment.as_mut().unwrap().means_code = Some(Code::new("48"));
let report = profiles::XRECHNUNG.validate(&inv);
assert!(report.has("BR-DE-24-a"), "{report}");
inv.payment.as_mut().unwrap().means = Some(PaymentMeans::Card(PaymentCard {
primary_account_number: Some("############1234".to_owned()),
holder_name: Some("A. Muster".to_owned()),
}));
assert!(!profiles::XRECHNUNG.validate(&inv).has("BR-DE-24-a"));
}
#[test]
fn direct_debit_requires_its_own_terms_and_a_real_iban() {
let mut inv = xrechnung_valid();
inv.payment = Some(PaymentInstructions {
means_code: Some(Code::new("59")), means: Some(PaymentMeans::DirectDebit(DirectDebit::default())),
..Default::default()
});
let report = profiles::XRECHNUNG.validate(&inv);
assert!(
report.has("BR-DE-30"),
"BT-90 creditor identifier: {report}"
);
assert!(report.has("BR-DE-31"), "BT-91 debited account: {report}");
assert!(
report.has("PEPPOL-EN16931-R061"),
"R061 replaced BR-DE-29 and is merged into XRechnung:\n{report}"
);
inv.payment.as_mut().unwrap().means = Some(PaymentMeans::DirectDebit(DirectDebit {
mandate_reference: Some("MANDATE-1".to_owned()),
creditor_identifier: Some("DE98ZZZ09999999999".to_owned()),
debited_account: Some("DE89370400440532013001".to_owned()), }));
let report = profiles::XRECHNUNG.validate(&inv);
assert!(!report.has("BR-DE-30"));
assert!(!report.has("BR-DE-31"));
assert!(report.has("BR-DE-20"), "mod-97 catches the typo: {report}");
assert!(report.is_valid(), "{report}");
inv.payment.as_mut().unwrap().means = Some(PaymentMeans::DirectDebit(DirectDebit {
mandate_reference: Some("MANDATE-1".to_owned()),
creditor_identifier: Some("DE98ZZZ09999999999".to_owned()),
debited_account: Some("DE89370400440532013000".to_owned()),
}));
assert!(!profiles::XRECHNUNG.validate(&inv).has("BR-DE-20"));
}
#[test]
fn a_corrected_invoice_must_reference_the_original() {
let mut inv = xrechnung_valid();
inv.type_code = Some(Code::new("384"));
assert!(profiles::XRECHNUNG.validate(&inv).has("BR-DE-26"));
inv.preceding_invoices = vec![PrecedingInvoice {
reference: DocumentReference::new("INV-2026-000"),
issue_date: Some(Date::parse("2026-05-31").unwrap()),
}];
assert!(!profiles::XRECHNUNG.validate(&inv).has("BR-DE-26"));
}
#[test]
fn contact_formats_are_shape_checked() {
let mut inv = xrechnung_valid();
inv.seller.contact.phone = Some("ext.".to_owned()); assert!(profiles::XRECHNUNG.validate(&inv).has("BR-DE-27"));
inv.seller.contact.phone = Some("+49 30 123456".to_owned());
assert!(!profiles::XRECHNUNG.validate(&inv).has("BR-DE-27"));
inv.seller.contact.email = Some("not-an-address".to_owned());
assert!(profiles::XRECHNUNG.validate(&inv).has("BR-DE-28"));
inv.seller.contact.email = Some("rechnung@seller.de".to_owned());
assert!(!profiles::XRECHNUNG.validate(&inv).has("BR-DE-28"));
}
#[test]
fn a_vat_charging_seller_needs_a_tax_identifier() {
let mut inv = xrechnung_valid();
inv.seller.vat_identifier = None;
inv.seller.tax_registration = None;
let report = profiles::XRECHNUNG.validate(&inv);
assert!(report.has("BR-DE-16"), "{report}");
inv.seller.tax_registration = Some("DE 199/123/45678".to_owned());
assert!(!profiles::XRECHNUNG.validate(&inv).has("BR-DE-16"));
}
#[test]
fn xrechnung_rewrites_r120_on_the_way_in() {
let mut inv = xrechnung_valid();
inv.lines[0].price.net_price = UnitPriceAmount::new(dec!(1));
let de = profiles::XRECHNUNG.validate(&inv);
assert!(de.has("PEPPOL-EN16931-R120"), "R120 still runs:\n{de}");
assert!(
de.is_valid(),
"…but only as a warning, so the document stands:\n{de}"
);
assert!(
de.warnings().any(|f| f.rule.contains("R120")),
"and it is reported as one:\n{de}"
);
let mut peppol_doc = inv.clone();
peppol_doc.specification_id = Some(profiles::PEPPOL_BIS_3.specification_id.to_owned());
let pe = profiles::PEPPOL_BIS_3.validate(&peppol_doc);
assert!(!pe.is_valid(), "Peppol keeps it fatal:\n{pe}");
let mut huf = xrechnung_valid();
huf.currency = Some(Code::new("HUF"));
huf.lines[0].net_amount = InvoiceAmount::parse("100.30").unwrap();
assert!(
!profiles::XRECHNUNG
.validate(&huf)
.has("PEPPOL-EN16931-R120"),
"0.30 is within XRechnung's HUF slack"
);
let mut huf_peppol = huf.clone();
huf_peppol.specification_id = Some(profiles::PEPPOL_BIS_3.specification_id.to_owned());
assert!(
profiles::PEPPOL_BIS_3
.validate(&huf_peppol)
.has("PEPPOL-EN16931-R120"),
"…and outside Peppol's, which is 0.02 for every currency"
);
}
#[test]
fn the_unmerged_peppol_rules_stay_out_of_xrechnung() {
let ids: Vec<&str> = profiles::XRECHNUNG
.extra_rules
.iter()
.map(|r| r.id.as_str())
.collect();
for unmerged in [
"PEPPOL-EN16931-CL001",
"PEPPOL-EN16931-CL008",
"PEPPOL-EN16931-P0104",
"PEPPOL-EN16931-P0112",
] {
assert!(!ids.contains(&unmerged), "{unmerged} must not be merged");
}
assert!(ids.contains(&"PEPPOL-EN16931-R061"));
assert!(ids.contains(&"PEPPOL-EN16931-R120"));
}
#[test]
fn a_cvd_invoice_can_be_core_invalid() {
let mut inv = xrechnung_valid();
inv.specification_id = Some(profiles::XRECHNUNG_CVD.specification_id.to_owned());
inv.contract_reference = Some(en16931::DocumentReference::new("V-2026-88"));
inv.tender_reference = Some(en16931::DocumentReference::new("LOS-3"));
inv.lines[0].item.classification_identifiers = vec![Identifier::schemed("N1", "CVD")];
inv.lines[0].item.attributes = vec![ItemAttribute {
name: Some("cva".to_owned()),
value: Some("zero-emission".to_owned()),
}];
let cvd = profiles::XRECHNUNG_CVD.validate(&inv);
assert!(cvd.is_valid(), "a conforming CVD invoice:\n{cvd}");
let core = en16931::validate(&inv);
assert!(
core.has("BR-CL-13"),
"…which core EN 16931 rejects, because `CVD` is not in UNTDID 7143:\n{core}"
);
assert!(
!profiles::XRECHNUNG_CVD.is_conformant_cius(),
"so CVD is not a conformant CIUS, and nothing may widen out of it"
);
}
#[test]
fn suppression_is_loud() {
use en16931::validation::Check;
let mut inv = xrechnung_valid();
inv.buyer_reference = None;
let plain = profiles::XRECHNUNG.validate(&inv);
assert!(plain.has("BR-DE-15"), "the rule fires normally:\n{plain}");
assert!(plain.suppressed().is_empty());
let deviated = Check::new(&profiles::XRECHNUNG)
.without("BR-DE-15")
.run(&inv);
assert!(!deviated.has("BR-DE-15"), "suppressed:\n{deviated}");
assert_eq!(deviated.suppressed(), ["BR-DE-15"]);
assert!(
deviated.to_string().contains("suppressed and NOT checked"),
"a stored report must not misrepresent what ran:\n{deviated}"
);
assert!(deviated.rules_checked() < plain.rules_checked());
}
#[test]
fn suppression_matches_ids_canonically() {
use en16931::validation::Check;
let inv = core_valid();
let report = Check::new(&profiles::EN16931).without("br-co-3").run(&inv);
assert_eq!(report.suppressed(), ["br-co-3"]);
}
#[test]
fn a_deviated_run_refuses_to_prove() {
use en16931::validation::{Check, ProveError};
let inv = xrechnung_valid();
let proof = Check::new(&profiles::XRECHNUNG).prove::<XRechnung>(inv.clone());
assert!(proof.is_ok(), "a clean run proves");
let refused = Check::new(&profiles::XRECHNUNG)
.without("BR-DE-15")
.prove::<XRechnung>(inv);
match refused {
Err(ProveError::Suppressed(ids)) => assert_eq!(ids, ["BR-DE-15"]),
Err(e) => panic!("wrong error: {e}"),
Ok(_) => panic!("a suppressed run must not yield a proof"),
}
}
#[test]
fn a_report_says_what_it_checked_against() {
let report = profiles::XRECHNUNG.validate(&Invoice::default());
assert_eq!(report.profile(), Some("XRechnung 3.0"));
assert_eq!(report.edition(), en16931::Edition::En2017A1);
let shown = report.to_string();
assert!(shown.starts_with("XRechnung 3.0 validation"), "{shown}");
let core = en16931::validate(&Invoice::default());
assert_eq!(core.profile(), None);
assert!(core.to_string().starts_with("EN 16931 validation"));
}
#[test]
fn the_core_path_and_the_en16931_profile_agree() {
for invoice in [en16931::Invoice::default(), corpus_invoice()] {
let core = en16931::validate(&invoice);
let profile = en16931::profiles::EN16931.validate(&invoice);
assert_eq!(
core.rules_checked(),
profile.rules_checked(),
"the two paths disagree about how many rules ran"
);
let core_rules: Vec<&str> = core.findings().iter().map(|f| f.rule.as_str()).collect();
let profile_rules: Vec<&str> = profile.findings().iter().map(|f| f.rule.as_str()).collect();
assert_eq!(
core_rules, profile_rules,
"the two paths disagree on findings"
);
}
}
fn corpus_invoice() -> en16931::Invoice {
let mut inv = en16931::Invoice::default();
inv.extensions
.third_party_payments
.push(en16931::ThirdPartyPayment {
payment_type: Some("BG-DEX-01".into()),
amount: None,
description: None,
});
inv
}