use crate::validation::Severity;
use crate::validation::profile::{
ArtefactRef, Profile, ProfileMarker, Restriction, Underlies, terms as t,
};
use crate::validation::rules::peppol;
pub const CEN: ArtefactRef = ArtefactRef {
authority: "CEN",
repo: "ConnectingEurope/eInvoicing-EN16931",
git_ref: crate::ARTEFACT_VERSION,
};
pub const KOSIT_SCHEMATRON: ArtefactRef = ArtefactRef {
authority: "KoSIT",
repo: "itplr-kosit/xrechnung-schematron",
git_ref: "v2.5.0",
};
pub const KOSIT_CONFIG: ArtefactRef = ArtefactRef {
authority: "KoSIT",
repo: "itplr-kosit/validator-configuration-xrechnung",
git_ref: "v2026-01-31",
};
pub const PEPPOL: ArtefactRef = ArtefactRef {
authority: "OpenPeppol",
repo: "OpenPEPPOL/peppol-bis-invoice-3",
git_ref: "v3.0.20",
};
pub static EN16931: Profile = Profile {
id: "EN 16931",
edition: crate::Edition::En2017A1,
specification_id: "urn:cen.eu:en16931:2017",
artefacts: &[CEN],
underlying: &[],
restrictions: &[],
extra_rules: &[],
extensions: &[],
levels: &[],
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct En16931;
impl ProfileMarker for En16931 {
const PROFILE: &'static Profile = &EN16931;
}
static XR_TYPE_CODES: &[&str] = &["326", "380", "384", "389", "381", "875", "876", "877"];
static XR_SPEC_IDS: &[&str] = &[
"urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0",
"urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0\
#conformant#urn:xeinkauf.de:kosit:extension:xrechnung_3.0",
"urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0\
#compliant#urn:xeinkauf.de:kosit:xrechnung:cvd_0.9",
];
pub static XRECHNUNG: Profile = Profile {
id: "XRechnung 3.0",
edition: crate::Edition::En2017A1,
specification_id: XR_SPEC_IDS[0],
artefacts: &[CEN, KOSIT_SCHEMATRON, KOSIT_CONFIG, PEPPOL],
underlying: &["EN 16931", "Peppol BIS Billing 3.0"],
restrictions: &[
Restriction::Mandatory {
id: "BR-DE-1",
term: &t::PAYMENT_INSTRUCTIONS,
},
Restriction::Mandatory {
id: "BR-DE-3",
term: &t::SELLER_CITY,
},
Restriction::Mandatory {
id: "BR-DE-4",
term: &t::SELLER_POST_CODE,
},
Restriction::Mandatory {
id: "BR-DE-5",
term: &t::SELLER_CONTACT_POINT,
},
Restriction::Mandatory {
id: "BR-DE-6",
term: &t::SELLER_CONTACT_PHONE,
},
Restriction::Mandatory {
id: "BR-DE-7",
term: &t::SELLER_CONTACT_EMAIL,
},
Restriction::Mandatory {
id: "BR-DE-8",
term: &t::BUYER_CITY,
},
Restriction::Mandatory {
id: "BR-DE-9",
term: &t::BUYER_POST_CODE,
},
Restriction::Mandatory {
id: "BR-DE-14",
term: &t::VAT_RATE,
},
Restriction::Mandatory {
id: "BR-DE-15",
term: &t::BUYER_REFERENCE,
},
Restriction::CodeValues {
id: "BR-DE-17",
term: &t::TYPE_CODE,
allowed: XR_TYPE_CODES,
},
Restriction::CodeValues {
id: "BR-DE-21",
term: &t::SPECIFICATION_ID,
allowed: XR_SPEC_IDS,
},
],
extra_rules: XR_EXTRA,
extensions: &[],
levels: XR_LEVELS,
};
static XR_LEVELS: &[(&str, Severity)] = &[
("BR-CL-21", Severity::Warning),
("BR-CL-23", Severity::Warning),
];
static XR_EXTRA: &[&crate::validation::Rule] = &const {
let x = crate::validation::rules::xrechnung::ALL;
let p = peppol::FOR_XRECHNUNG;
let mut out = [x[0]; 43];
let mut i = 0;
while i < x.len() {
out[i] = x[i];
i += 1;
}
let mut j = 0;
while j < p.len() {
out[x.len() + j] = p[j];
j += 1;
}
assert!(x.len() + p.len() == 43);
out
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct XRechnung;
impl ProfileMarker for XRechnung {
const PROFILE: &'static Profile = &XRECHNUNG;
}
pub(crate) const PEPPOL_SPEC_ID: &str =
"urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0";
pub static PEPPOL_BIS_3: Profile = Profile {
id: "Peppol BIS Billing 3.0",
edition: crate::Edition::En2017A1,
specification_id: PEPPOL_SPEC_ID,
artefacts: &[CEN, PEPPOL],
underlying: &["EN 16931"],
restrictions: &[],
extra_rules: crate::validation::rules::peppol::ALL,
extensions: &[],
levels: &[],
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PeppolBis3;
impl ProfileMarker for PeppolBis3 {
const PROFILE: &'static Profile = &PEPPOL_BIS_3;
}
impl Underlies<PeppolBis3> for En16931 {}
static XR_CVD_EXTRA: &[&crate::validation::Rule] = &const {
let x = XR_EXTRA;
let c = crate::validation::rules::xrechnung::cvd::ALL;
let mut out = [x[0]; 51];
let mut i = 0;
while i < x.len() {
out[i] = x[i];
i += 1;
}
let mut j = 0;
while j < c.len() {
out[x.len() + j] = c[j];
j += 1;
}
assert!(x.len() + c.len() == 51);
out
};
pub static XRECHNUNG_CVD: Profile = Profile {
id: "XRechnung 3.0 CVD",
edition: crate::Edition::En2017A1,
specification_id: XR_SPEC_IDS[2],
artefacts: &[CEN, KOSIT_SCHEMATRON, KOSIT_CONFIG, PEPPOL],
underlying: &["EN 16931", "XRechnung 3.0"],
restrictions: XRECHNUNG.restrictions,
extra_rules: XR_CVD_EXTRA,
extensions: &[],
levels: &[
("BR-CL-21", Severity::Warning),
("BR-CL-23", Severity::Warning),
("BR-CL-13", Severity::Info),
],
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct XRechnungCvd;
impl ProfileMarker for XRechnungCvd {
const PROFILE: &'static Profile = &XRECHNUNG_CVD;
}
static XR_EXT_EXTRA: &[&crate::validation::Rule] = &const {
let x = XR_EXTRA;
let e = crate::validation::rules::xrechnung::extension::ALL;
let mut out = [x[0]; 57];
let mut i = 0;
while i < x.len() {
out[i] = x[i];
i += 1;
}
let mut j = 0;
while j < e.len() {
out[x.len() + j] = e[j];
j += 1;
}
assert!(x.len() + e.len() == 57);
out
};
pub static XRECHNUNG_EXTENSION: Profile = Profile {
id: "XRechnung 3.0 Extension",
edition: crate::Edition::En2017A1,
specification_id: XR_SPEC_IDS[1],
artefacts: &[CEN, KOSIT_SCHEMATRON, KOSIT_CONFIG, PEPPOL],
underlying: &["XRechnung 3.0"],
restrictions: XRECHNUNG.restrictions,
extra_rules: XR_EXT_EXTRA,
extensions: &[
crate::extensions::SUB_INVOICE_LINES,
crate::extensions::THIRD_PARTY_PAYMENTS,
],
levels: &[
("BR-CL-21", Severity::Info), ("BR-CL-23", Severity::Warning), ("BR-CL-24", Severity::Info), ("BR-CL-10", Severity::Info), ("BR-CL-11", Severity::Info), ("BR-CL-25", Severity::Info), ("BR-CL-26", Severity::Info), ("BR-CO-16", Severity::Info),
],
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct XRechnungExtension;
impl ProfileMarker for XRechnungExtension {
const PROFILE: &'static Profile = &XRECHNUNG_EXTENSION;
}
pub static ALL: &[&Profile] = &[
&EN16931,
&XRECHNUNG,
&XRECHNUNG_CVD,
&XRECHNUNG_EXTENSION,
&PEPPOL_BIS_3,
];
#[must_use]
pub fn for_specification_id(id: &str) -> Option<&'static Profile> {
ALL.iter()
.copied()
.find(|p| p.specification_id == id)
.or_else(|| {
id.starts_with(XR_SPEC_IDS[0]).then_some(&XRECHNUNG)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profiles_are_siblings_not_a_scale() {
let peppol_invoice_codes = |c: &str| {
let inv = crate::Invoice {
business_process: Some("urn:fdc:peppol.eu:2017:poacc:billing:01:1.0".to_owned()),
type_code: Some(crate::invoice::Code::new(c)),
..Default::default()
};
PEPPOL_BIS_3.validate(&inv).has("PEPPOL-EN16931-P0100")
};
assert!(XR_TYPE_CODES.contains(&"389"));
assert!(peppol_invoice_codes("389"), "Peppol rejects 389");
assert!(!peppol_invoice_codes("386"), "Peppol accepts 386");
assert!(!XR_TYPE_CODES.contains(&"386"));
}
#[test]
fn a_document_selects_its_own_profile_through_bt_24() {
assert_eq!(
for_specification_id(XRECHNUNG.specification_id).map(|p| p.id),
Some("XRechnung 3.0")
);
assert_eq!(
for_specification_id(XR_SPEC_IDS[1]).map(|p| p.id),
Some("XRechnung 3.0 Extension")
);
assert_eq!(
for_specification_id(XR_SPEC_IDS[2]).map(|p| p.id),
Some("XRechnung 3.0 CVD")
);
assert_eq!(
for_specification_id(&format!("{}#conformant#urn:nope", XR_SPEC_IDS[0])).map(|p| p.id),
Some("XRechnung 3.0")
);
assert_eq!(
for_specification_id(EN16931.specification_id).map(|p| p.id),
Some("EN 16931")
);
assert!(for_specification_id("urn:nonsense").is_none());
}
#[test]
fn every_profile_states_what_4_4_2_requires() {
for p in ALL {
assert!(!p.id.is_empty());
assert!(!p.specification_id.is_empty());
assert_eq!(p.underlying.is_empty(), p.id == "EN 16931", "{}", p.id);
let relaxes = p.levels.iter().any(|(id, level)| {
crate::validation::rules::explain(id).is_some_and(|r| *level > r.severity)
});
assert_eq!(
p.is_conformant_cius(),
!relaxes,
"{} — conformance must follow the level overrides",
p.id
);
}
}
#[test]
fn exactly_three_profiles_are_not_conformant_ciuses() {
let not: Vec<&str> = ALL
.iter()
.filter(|p| !p.is_conformant_cius())
.map(|p| p.id)
.collect();
assert_eq!(
not,
[
"XRechnung 3.0",
"XRechnung 3.0 CVD",
"XRechnung 3.0 Extension"
]
);
}
#[test]
fn every_level_override_names_a_rule_that_runs() {
for p in ALL {
let ids: Vec<&str> = p.check_ids().collect();
for (id, _) in p.levels {
assert!(
ids.contains(id),
"{} re-levels {id}, which it does not run",
p.id
);
}
}
}
#[test]
fn a_relaxed_rule_is_reported_rather_than_dropped() {
use crate::validation::Severity;
use crate::{Invoice, InvoiceAmount, InvoiceLine, Percentage, Quantity};
let mut inv = Invoice::default();
inv.lines.push(InvoiceLine::new(
"1",
"Widget",
Quantity::ONE,
"NOT-A-UNIT", InvoiceAmount::ZERO,
"S",
Some(Percentage::new(rust_decimal::Decimal::from(19))),
));
let core = EN16931.validate(&inv);
assert!(
core.fatal().any(|f| f.rule == "BR-CL-23"),
"fatal in the core model"
);
let xr = XRECHNUNG.validate(&inv);
let f = xr
.findings()
.iter()
.find(|f| f.rule == "BR-CL-23")
.expect("still reported");
assert_eq!(f.severity, Severity::Warning, "KoSIT's customLevel");
}
}