use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Profile {
Minimum,
BasicWl,
Basic,
En16931,
Extended,
XRechnung,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IsInvoice {
Yes,
No(&'static str),
Unknown,
}
impl Profile {
#[must_use]
pub const fn is_en16931_invoice(self) -> IsInvoice {
match self {
Self::Basic | Self::En16931 | Self::Extended | Self::XRechnung => IsInvoice::Yes,
Self::Minimum => {
IsInvoice::No("MINIMUM carries totals only and no lines: it cannot satisfy BR-16")
}
Self::BasicWl => IsInvoice::No("BASIC WL is 'without lines': it cannot satisfy BR-16"),
Self::Unknown => IsInvoice::Unknown,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Minimum => "MINIMUM",
Self::BasicWl => "BASIC WL",
Self::Basic => "BASIC",
Self::En16931 => "EN 16931",
Self::Extended => "EXTENDED",
Self::XRechnung => "XRECHNUNG",
Self::Unknown => "UNKNOWN",
}
}
#[must_use]
pub fn parse(id: &str) -> Self {
let id = id.trim().to_ascii_lowercase();
let tail = id.rsplit(':').next().unwrap_or(&id);
if tail.contains("minimum") {
Self::Minimum
} else if tail.contains("basicwl") || id.contains("basic wl") {
Self::BasicWl
} else if tail.contains("basic") {
Self::Basic
} else if tail.contains("extended") {
Self::Extended
} else if id.contains("xrechnung") {
Self::XRechnung
} else if id.contains("en16931") {
Self::En16931
} else {
Self::Unknown
}
}
}
impl fmt::Display for Profile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_two_that_are_not_invoices_say_so() {
assert!(matches!(
Profile::Minimum.is_en16931_invoice(),
IsInvoice::No(_)
));
assert!(matches!(
Profile::BasicWl.is_en16931_invoice(),
IsInvoice::No(_)
));
for p in [
Profile::Basic,
Profile::En16931,
Profile::Extended,
Profile::XRechnung,
] {
assert_eq!(p.is_en16931_invoice(), IsInvoice::Yes, "{p}");
}
assert_eq!(Profile::Unknown.is_en16931_invoice(), IsInvoice::Unknown);
}
#[test]
fn the_refusal_carries_a_reason() {
let IsInvoice::No(why) = Profile::Minimum.is_en16931_invoice() else {
panic!("MINIMUM is not an invoice");
};
assert!(why.contains("BR-16"), "{why}");
}
#[test]
fn parsing_is_not_fooled_by_prefixes() {
assert_eq!(
Profile::parse("urn:factur-x.eu:1p0:basicwl"),
Profile::BasicWl
);
assert_eq!(Profile::parse("urn:factur-x.eu:1p0:basic"), Profile::Basic);
assert_eq!(
Profile::parse("urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended"),
Profile::Extended
);
assert_eq!(Profile::parse("urn:cen.eu:en16931:2017"), Profile::En16931);
assert_eq!(
Profile::parse("urn:factur-x.eu:1p0:minimum"),
Profile::Minimum
);
}
#[test]
fn an_unrecognised_profile_is_not_quietly_the_core_model() {
assert_eq!(Profile::parse("urn:something:else"), Profile::Unknown);
assert_eq!(Profile::parse(""), Profile::Unknown);
assert_ne!(Profile::parse("nonsense"), Profile::En16931);
}
#[test]
fn case_and_whitespace_do_not_change_the_profile() {
assert_eq!(
Profile::parse(" URN:FACTUR-X.EU:1P0:BASIC "),
Profile::Basic
);
}
}