use core::fmt;
use super::{contains, generated as lists};
use crate::invoice::Code;
pub struct Withdrawn {
pub code: &'static str,
pub was: &'static str,
pub use_instead: Option<&'static str>,
pub note: Option<&'static str>,
}
pub static WITHDRAWN: &[Withdrawn] = &[
Withdrawn {
code: "9901",
was: "DK:CPR — Danish Ministry of the Interior and Health",
use_instead: Some("0096"),
note: None,
},
Withdrawn {
code: "9902",
was: "DK:CVR — the Danish Commerce and Companies Agency",
use_instead: Some("0184"),
note: None,
},
Withdrawn {
code: "9904",
was: "DK:SE — Danish Ministry of Taxation",
use_instead: Some("0198"),
note: None,
},
Withdrawn {
code: "9905",
was: "DK:VANS — Danish VANS providers",
use_instead: None,
note: Some(
"withdrawn on 2023-11-30 with no successor; use the party's own registry scheme",
),
},
Withdrawn {
code: "9906",
was: "IT:VAT — Italian VAT number",
use_instead: Some("0211"),
note: None,
},
Withdrawn {
code: "9907",
was: "IT:CF — Italian codice fiscale",
use_instead: Some("0210"),
note: None,
},
Withdrawn {
code: "9917",
was: "IS:KT — Icelandic kennitala",
use_instead: Some("0196"),
note: None,
},
Withdrawn {
code: "9921",
was: "IT:IPA — Indice delle Pubbliche Amministrazioni",
use_instead: Some("0201"),
note: None,
},
Withdrawn {
code: "9954",
was: "NL:OINO — Dutch Overheidsidentificatienummer",
use_instead: Some("0190"),
note: None,
},
Withdrawn {
code: "9955",
was: "SE:VAT — Swedish VAT number",
use_instead: Some("0007"),
note: None,
},
Withdrawn {
code: "9956",
was: "BE:CBE — Belgian Crossroads Bank for Enterprises",
use_instead: Some("0208"),
note: None,
},
Withdrawn {
code: "9958",
was: "DE:LID — the Peppol Leitweg-ID scheme",
use_instead: Some("0204"),
note: Some(
"the Leitweg-ID itself belongs in BT-10 (buyer reference) under XRechnung's BR-DE-15; \
0204 is the scheme for addressing that authority as a Peppol participant",
),
},
];
#[must_use]
pub fn withdrawn(code: &str) -> Option<&'static Withdrawn> {
WITHDRAWN.iter().find(|w| w.code == code)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodeError {
pub terms: &'static str,
pub list: &'static str,
pub rule: &'static str,
pub value: String,
pub hint: Option<String>,
}
impl fmt::Display for CodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:?} is not in {} ({}, {})",
self.value, self.list, self.terms, self.rule
)?;
if let Some(h) = &self.hint {
write!(f, " — {h}")?;
}
Ok(())
}
}
impl core::error::Error for CodeError {}
pub struct CodeList {
pub terms: &'static str,
pub name: &'static str,
pub rule: &'static str,
pub values: &'static [&'static str],
pub tracks_withdrawals: bool,
}
impl CodeList {
#[must_use]
pub fn accepts(&self, value: &str) -> bool {
contains(self.values, value)
}
pub fn check(&self, value: &str) -> Result<Code, CodeError> {
if self.accepts(value) {
return Ok(Code::new(value));
}
Err(CodeError {
terms: self.terms,
list: self.name,
rule: self.rule,
value: value.to_owned(),
hint: self.advice(value),
})
}
#[must_use]
pub fn advice(&self, value: &str) -> Option<String> {
if self.accepts(value) {
return None;
}
if self.tracks_withdrawals
&& let Some(w) = withdrawn(value)
{
let mut s = format!("{value} was {}, and has been withdrawn", w.was);
if let Some(r) = w.use_instead {
s.push_str(&format!("; use {r} instead"));
}
if let Some(n) = w.note {
s.push_str(&format!(" ({n})"));
}
return Some(s);
}
let trimmed = value.trim();
if trimmed != value && self.accepts(trimmed) {
return Some(format!(
"{trimmed:?} is valid — this value has surrounding whitespace, and \
EN 16931-1 §6.5.8 requires codes \"entered exactly as shown\""
));
}
self.values
.iter()
.find(|c| c.eq_ignore_ascii_case(trimmed))
.map(|c| format!("did you mean {c:?}? Code lists are case-sensitive"))
}
}
macro_rules! guarded {
($(
$(#[$attr:meta])*
$konst:ident / $func:ident = $values:ident,
terms: $terms:literal, list: $name:literal, rule: $rule:literal
$(, withdrawals: $w:literal)?;
)*) => {
$(
#[doc = concat!("The ", $name, " — ", $terms, ", checked by `", $rule, "`.")]
pub static $konst: CodeList = CodeList {
terms: $terms,
name: $name,
rule: $rule,
values: lists::$values,
tracks_withdrawals: false $(|| $w)?,
};
$(#[$attr])*
#[doc = concat!("[`CodeError`] when the value is not in [`", stringify!($konst), "`].")]
pub fn $func(value: &str) -> Result<Code, CodeError> {
$konst.check(value)
}
)*
pub static ALL: &[&CodeList] = &[$(&$konst),*];
};
}
guarded! {
EAS / eas = EAS_SCHEMES,
terms: "BT-34 / BT-49", list: "the CEF EAS code list", rule: "BR-CL-25",
withdrawals: true;
UNIT / unit = UNIT_CODES,
terms: "BT-130 / BT-150", list: "UN/ECE Recommendation 20 with the Rec 21 extension",
rule: "BR-CL-23";
CURRENCY / currency = CURRENCY_CODES,
terms: "BT-5 / BT-6", list: "ISO 4217 alpha-3", rule: "BR-CL-04";
COUNTRY / country = COUNTRY_CODES,
terms: "BT-40 / BT-55 / BT-69 / BT-80", list: "ISO 3166-1 alpha-2", rule: "BR-CL-14";
INVOICE_TYPE / invoice_type = INVOICE_TYPE_CODES,
terms: "BT-3 (invoice)", list: "UNTDID 1001, invoice subset", rule: "BR-CL-01";
CREDIT_NOTE_TYPE / credit_note_type = CREDIT_NOTE_TYPE_CODES,
terms: "BT-3 (credit note)", list: "UNTDID 1001, credit-note subset", rule: "BR-CL-01";
VAT_CATEGORY / vat_category = VAT_CATEGORY_CODES,
terms: "BT-95 / BT-102 / BT-118 / BT-151", list: "UNCL 5305", rule: "BR-CL-17";
PAYMENT_MEANS / payment_means = PAYMENT_MEANS_CODES,
terms: "BT-81", list: "UNTDID 4461", rule: "BR-CL-16";
VATEX / vatex = VATEX_CODES,
terms: "BT-121", list: "the CEF VATEX code list", rule: "PEPPOL-EN16931-CL002";
ALLOWANCE_REASON / allowance_reason = ALLOWANCE_REASON_CODES,
terms: "BT-98 / BT-140", list: "UNCL 5189", rule: "BR-CL-19";
CHARGE_REASON / charge_reason = CHARGE_REASON_CODES,
terms: "BT-105 / BT-145", list: "UNCL 7161", rule: "BR-CL-20";
NOTE_SUBJECT / note_subject = NOTE_SUBJECT_CODES,
terms: "BT-21", list: "UNCL 4451", rule: "BR-CL-08";
VAT_POINT_DATE / vat_point_date = VAT_POINT_DATE_CODES,
terms: "BT-8", list: "UNTDID 2005, the three EN 16931 values", rule: "BR-CL-05";
REFERENCE_QUALIFIER / reference_qualifier = REFERENCE_QUALIFIERS,
terms: "BT-18-1 / BT-128-1", list: "UNTDID 1153", rule: "BR-CL-07";
ICD / icd = ICD_SCHEMES,
terms: "BT-29-1 / BT-46-1 / BT-71-1 / BT-157-1", list: "the ISO 6523 ICD list",
rule: "BR-CL-10";
ITEM_CLASSIFICATION / item_classification = ITEM_CLASSIFICATION_SCHEMES,
terms: "BT-158-1", list: "UNTDID 7143", rule: "BR-CL-13";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn withdrawn_codes_are_really_gone() {
for w in WITHDRAWN {
assert!(
!EAS.accepts(w.code),
"{} is in EAS_SCHEMES, so it is not withdrawn",
w.code
);
if let Some(r) = w.use_instead {
assert!(
EAS.accepts(r),
"{} advises {r}, which is not in EAS_SCHEMES",
w.code
);
}
}
}
#[test]
fn the_leitweg_scheme_names_its_successor_and_bt_10() {
let e = eas("9958").unwrap_err();
let msg = e.to_string();
assert!(msg.contains("BR-CL-25"), "{msg}");
assert!(msg.contains("withdrawn"), "{msg}");
assert!(msg.contains("use 0204 instead"), "{msg}");
assert!(msg.contains("BT-10"), "{msg}");
assert!(eas("0204").is_ok(), "the successor is accepted");
assert!(eas("0088").is_ok(), "GLN, which is what a BDEW MP-ID uses");
}
#[test]
fn case_and_whitespace_are_named_rather_than_silently_fixed() {
let e = unit("kwh").unwrap_err();
assert!(e.to_string().contains("did you mean \"KWH\""), "{e}");
let e = unit(" KWH").unwrap_err();
assert!(e.to_string().contains("whitespace"), "{e}");
assert!(unit("KWH").is_ok());
}
#[test]
fn case_folding_is_unambiguous() {
for list in ALL {
let mut folded: Vec<String> = list.values.iter().map(|v| v.to_uppercase()).collect();
folded.sort_unstable();
let before = folded.len();
folded.dedup();
assert_eq!(
before,
folded.len(),
"{} has two values differing only in case, so a case hint could mislead",
list.name
);
}
}
#[test]
fn nothing_is_guessed() {
let e = unit("FURLONG").unwrap_err();
assert_eq!(e.hint, None, "no edit distance, no guessing");
assert!(!UNIT.accepts("KWQ"));
assert_eq!(UNIT.advice("KWQ"), None);
assert_eq!(UNIT.advice("KWH"), None);
}
#[test]
fn guarded_lists_agree_with_their_rules() {
assert_eq!(EAS.values, lists::EAS_SCHEMES);
assert_eq!(UNIT.values, lists::UNIT_CODES);
assert_eq!(VAT_CATEGORY.values, lists::VAT_CATEGORY_CODES);
for list in ALL {
assert!(!list.values.is_empty(), "{} is empty", list.name);
assert!(
list.rule.starts_with("BR-") || list.rule.starts_with("PEPPOL-"),
"{} names {}, which is not a rule id",
list.name,
list.rule
);
}
}
#[test]
fn the_two_bt_3_lists_are_guarded_separately() {
assert!(invoice_type("380").is_ok());
assert!(invoice_type("381").is_err());
assert!(credit_note_type("381").is_ok());
assert!(credit_note_type("380").is_err());
}
}