use serde::Deserialize;
use std::collections::HashMap;
use std::sync::OnceLock;
use super::{ConditionResult, EvaluationContext};
use crate::eval::format_validators::{
is_edifact_number, validate_max_decimal_places, validate_numeric,
validate_technische_ressource_id,
};
const EMBEDDED_JSON: &str = include_str!("../../data/konfigurationen_abrechnungsdaten.json");
const ANMELDUNG_JSON: &str = include_str!("../../data/konfigurationen_anmeldung.json");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Edition {
V1_3c,
V1_4,
}
impl Edition {
pub fn of_format_version(fv: &str) -> Option<Self> {
match fv {
"FV2504" | "FV2510" | "FV2604" => Some(Self::V1_3c),
"FV2610" => Some(Self::V1_4),
_ => None,
}
}
fn key(self) -> &'static str {
match self {
Self::V1_3c => "1.3c",
Self::V1_4 => "1.4",
}
}
}
#[derive(Debug, Deserialize)]
struct RawFile {
versions: HashMap<String, RawVersion>,
}
#[derive(Debug, Deserialize)]
struct RawVersion {
products: Vec<Product>,
}
#[derive(Debug, Deserialize)]
pub struct Product {
pub code: String,
pub name: String,
pub property_codes: Vec<PropertyCode>,
pub wertedetails: Option<Wertedetails>,
#[serde(default)]
pub pids: Vec<String>,
#[serde(default)]
pub chapter: Option<String>,
#[serde(default)]
pub max_repetitions: Option<u32>,
}
impl Product {
pub fn orderable_by(&self, pid: &str) -> bool {
self.pids.is_empty() || self.pids.iter().any(|p| p == pid)
}
}
#[derive(Debug, Deserialize)]
pub struct PropertyCode {
pub code: String,
pub name: String,
}
#[derive(Debug, Deserialize)]
pub struct Wertedetails {
pub name: String,
pub conditions: Vec<u32>,
#[serde(default)]
pub variants: Vec<WertedetailsVariant>,
}
#[derive(Debug, Deserialize)]
pub struct WertedetailsVariant {
pub with_property: String,
pub conditions: Vec<u32>,
}
pub const EXAMPLE_BILANZKREIS: &str = "11XBILANZKREIS-O";
impl Wertedetails {
pub fn fits_beside(&self, value: &str, property: Option<&str>) -> ConditionResult {
if self.variants.is_empty() {
return holds(&self.conditions, value);
}
if let Some(v) = self
.variants
.iter()
.find(|v| Some(v.with_property.as_str()) == property)
{
return holds(&v.conditions, value);
}
let results = self.variants.iter().map(|v| holds(&v.conditions, value));
results.fold(ConditionResult::False, |best, r| match (best, r) {
(ConditionResult::True, _) | (_, ConditionResult::True) => ConditionResult::True,
(ConditionResult::Unknown, _) | (_, ConditionResult::Unknown) => {
ConditionResult::Unknown
}
_ => ConditionResult::False,
})
}
pub fn fits(&self, value: &str) -> ConditionResult {
self.fits_beside(value, None)
}
pub fn needed_beside(&self, property: Option<&str>) -> bool {
self.variants.is_empty()
|| self
.variants
.iter()
.any(|v| Some(v.with_property.as_str()) == property)
}
pub fn suits_sample(&self, value: &str, property: Option<&str>) -> bool {
self.fits_beside(value, property) == ConditionResult::True
&& (!self.conditions.contains(&970) || is_eic(value))
}
pub fn example_value(&self, property: Option<&str>) -> Option<&'static str> {
[EXAMPLE_BILANZKREIS, "D1234567890", "4000", "50", "1.125"]
.into_iter()
.filter(|v| !self.conditions.contains(&970) || *v == EXAMPLE_BILANZKREIS)
.find(|v| self.fits_beside(v, property) == ConditionResult::True)
}
}
pub fn is_eic(value: &str) -> bool {
let digit = |c: char| match c {
'0'..='9' => Some(c as u32 - '0' as u32),
'A'..='Z' => Some(c as u32 - 'A' as u32 + 10),
'-' => Some(36),
_ => None,
};
let Some(digits) = value.chars().map(digit).collect::<Option<Vec<u32>>>() else {
return false;
};
if digits.len() != 16 {
return false;
}
let sum: u32 = digits[..15]
.iter()
.enumerate()
.map(|(i, d)| d * (16 - i as u32))
.sum();
let check = 36 - (sum + 36) % 37;
check != 36 && digits[15] == check
}
fn holds(conditions: &[u32], value: &str) -> ConditionResult {
let mut result = ConditionResult::True;
for condition in conditions {
let holds = match condition {
970 => ConditionResult::from(value.chars().count() <= 17),
909 => ConditionResult::from(
is_edifact_number(value) && !value.trim_start().starts_with('-'),
),
937 => ConditionResult::from(is_edifact_number(value) && !value.contains(['.', ','])),
906 => validate_max_decimal_places(value, 3),
914 => validate_numeric(value, ">", 0.0),
922 => validate_technische_ressource_id(value),
930 => validate_max_decimal_places(value, 2),
955 => validate_numeric(value, "<", 100.0),
_ => ConditionResult::Unknown,
};
match holds {
ConditionResult::False => return ConditionResult::False,
ConditionResult::Unknown => result = ConditionResult::Unknown,
ConditionResult::True => {}
}
}
result
}
#[derive(Debug)]
pub struct AbrechnungsdatenProdukte {
editions: HashMap<String, Vec<Product>>,
}
impl AbrechnungsdatenProdukte {
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
let raw: RawFile = serde_json::from_str(json)?;
Ok(Self {
editions: raw
.versions
.into_iter()
.map(|(version, v)| (version, v.products))
.collect(),
})
}
pub fn embedded() -> &'static Self {
static CELL: OnceLock<AbrechnungsdatenProdukte> = OnceLock::new();
CELL.get_or_init(|| {
AbrechnungsdatenProdukte::from_json(EMBEDDED_JSON)
.expect("embedded konfigurationen_abrechnungsdaten.json is malformed")
})
}
pub fn anmeldung() -> &'static Self {
static CELL: OnceLock<AbrechnungsdatenProdukte> = OnceLock::new();
CELL.get_or_init(|| {
AbrechnungsdatenProdukte::from_json(ANMELDUNG_JSON)
.expect("embedded konfigurationen_anmeldung.json is malformed")
})
}
pub fn products(&self, edition: Edition) -> &[Product] {
self.editions
.get(edition.key())
.map_or(&[], |products| products.as_slice())
}
pub fn product(&self, edition: Edition, code: &str) -> Option<&Product> {
self.products(edition).iter().find(|p| p.code == code)
}
}
pub fn chapter6_product(edition: Edition, code: &str) -> Option<&'static Product> {
AbrechnungsdatenProdukte::anmeldung()
.product(edition, code)
.or_else(|| AbrechnungsdatenProdukte::embedded().product(edition, code))
}
fn ordered_product(ctx: &EvaluationContext) -> Option<String> {
ctx.scoped_find_segments_in("SG29", "PIA")
.into_iter()
.find(|s| s.get_element(0) == "5")
.and_then(|s| s.elements.get(1)?.first().cloned())
.filter(|code| !code.is_empty())
}
fn of_product(
ctx: &EvaluationContext,
edition: Edition,
answer: impl Fn(&Product) -> ConditionResult,
) -> ConditionResult {
let Some(code) = ordered_product(ctx) else {
return ConditionResult::Unknown;
};
match AbrechnungsdatenProdukte::embedded().product(edition, &code) {
Some(product) => answer(product),
None => ConditionResult::False,
}
}
pub fn product_is_listed(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
let code = match ctx.resolved_value {
Some(value) => value.to_string(),
None => match ordered_product(ctx) {
Some(code) => code,
None => return ConditionResult::Unknown,
},
};
ConditionResult::from(
AbrechnungsdatenProdukte::embedded()
.product(edition, &code)
.is_some(),
)
}
pub fn has_property_codes(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
of_product(ctx, edition, |p| {
ConditionResult::from(!p.property_codes.is_empty())
})
}
pub fn property_code_allowed(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
let Some(value) = ctx.resolved_value else {
return ConditionResult::Unknown;
};
of_product(ctx, edition, |p| {
ConditionResult::from(p.property_codes.iter().any(|c| c.code == value))
})
}
pub fn wertedetails_value_fits(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
let Some(value) = ctx.resolved_value else {
return ConditionResult::Unknown;
};
of_product(ctx, edition, |p| match &p.wertedetails {
Some(w) => w.fits(value),
None => ConditionResult::False,
})
}
pub fn has_wertedetails(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
of_product(ctx, edition, |p| {
ConditionResult::from(p.wertedetails.is_some())
})
}
fn anmeldung_product(ctx: &EvaluationContext) -> Option<String> {
let mut codes: Vec<String> = ctx
.scoped_find_segments_in("SG8", "PIA")
.into_iter()
.filter(|s| s.get_element(0) == "5" && s.get_component(1, 1) == "Z11")
.filter_map(|s| s.elements.get(1)?.first().cloned())
.filter(|code| !code.is_empty())
.collect();
codes.dedup();
match codes.as_slice() {
[one] => Some(one.clone()),
_ => None,
}
}
fn anmeldung_property(ctx: &EvaluationContext) -> Option<String> {
ctx.scoped_find_segments_in("SG8", "CAV")
.into_iter()
.find(|s| s.get_component(0, 0) == "ZH9")
.map(|s| s.get_component(0, 3).to_string())
.filter(|code| !code.is_empty())
}
fn of_anmeldung_product(
ctx: &EvaluationContext,
edition: Edition,
answer: impl Fn(&Product) -> ConditionResult,
) -> ConditionResult {
let Some(code) = anmeldung_product(ctx) else {
return ConditionResult::Unknown;
};
match AbrechnungsdatenProdukte::anmeldung().product(edition, &code) {
Some(product) => answer(product),
None => ConditionResult::False,
}
}
pub fn anmeldung_product_allowed(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
let code = match ctx.resolved_value {
Some(value) => value.to_string(),
None => match anmeldung_product(ctx) {
Some(code) => code,
None => return ConditionResult::Unknown,
},
};
let table = AbrechnungsdatenProdukte::anmeldung().products(edition);
let pid = ctx.pruefidentifikator;
let named = table.iter().any(|p| p.pids.iter().any(|q| q == pid));
ConditionResult::from(
table
.iter()
.any(|p| p.code == code && (!named || p.orderable_by(pid))),
)
}
pub fn anmeldung_has_property_codes(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
of_anmeldung_product(ctx, edition, |p| {
ConditionResult::from(!p.property_codes.is_empty())
})
}
pub fn anmeldung_property_code_allowed(
ctx: &EvaluationContext,
edition: Edition,
) -> ConditionResult {
let Some(value) = ctx.resolved_value else {
return ConditionResult::Unknown;
};
of_anmeldung_product(ctx, edition, |p| {
ConditionResult::from(p.property_codes.iter().any(|c| c.code == value))
})
}
pub fn anmeldung_has_wertedetails(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
let property = anmeldung_property(ctx);
of_anmeldung_product(ctx, edition, |p| {
ConditionResult::from(
p.wertedetails
.as_ref()
.is_some_and(|w| w.needed_beside(property.as_deref())),
)
})
}
pub fn anmeldung_wertedetails_value_fits(
ctx: &EvaluationContext,
edition: Edition,
) -> ConditionResult {
let Some(value) = ctx.resolved_value else {
return ConditionResult::Unknown;
};
let property = anmeldung_property(ctx);
of_anmeldung_product(ctx, edition, |p| match &p.wertedetails {
Some(w) => w.fits_beside(value, property.as_deref()),
None => ConditionResult::False,
})
}
pub fn candidate_values(message_type: &str, fv: &str, condition: u32) -> Vec<String> {
let Some(edition) = Edition::of_format_version(fv).filter(|_| message_type == "ORDERS") else {
return Vec::new();
};
let products = AbrechnungsdatenProdukte::embedded().products(edition);
match condition {
158 => products.iter().map(|p| p.code.clone()).collect(),
160 => products
.iter()
.flat_map(|p| p.property_codes.iter().map(|c| c.code.clone()))
.collect(),
_ => Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn both_editions_load() {
let table = AbrechnungsdatenProdukte::embedded();
assert_eq!(table.products(Edition::V1_3c).len(), 9);
assert_eq!(table.products(Edition::V1_4).len(), 10);
assert!(table.product(Edition::V1_3c, "9991000003246").is_none());
let arbeit = table.product(Edition::V1_4, "9991000003246").expect("1.4");
assert_eq!(arbeit.name, "spezifische Arbeit");
}
#[test]
fn wertedetails_hold_their_formats() {
let table = AbrechnungsdatenProdukte::embedded();
let w = |code: &str| {
table
.product(Edition::V1_4, code)
.and_then(|p| p.wertedetails.as_ref())
.expect("a Wertedetail")
};
let bilanzkreis = w("9991000002553");
assert_eq!(bilanzkreis.fits("11XLIEFERANT2--E"), ConditionResult::True);
assert_eq!(
bilanzkreis.fits("11XLIEFERANT2--E-TOO-LONG"),
ConditionResult::False
);
let prognose = w("9991000002561");
assert_eq!(prognose.fits("4000"), ConditionResult::True);
assert_eq!(prognose.fits("4000.5"), ConditionResult::False);
assert_eq!(prognose.fits("-1"), ConditionResult::False);
assert_eq!(prognose.fits("11XLIEFERANT2--E"), ConditionResult::False);
let arbeit = w("9991000003246");
assert_eq!(arbeit.fits("1.125"), ConditionResult::True);
assert_eq!(arbeit.fits("1.1255"), ConditionResult::False);
assert_eq!(arbeit.fits("-0.5"), ConditionResult::False);
}
#[test]
fn anmeldung_products_name_their_pids() {
let table = AbrechnungsdatenProdukte::anmeldung();
assert_eq!(table.products(Edition::V1_3c).len(), 18);
assert_eq!(table.products(Edition::V1_4).len(), 26);
let bilanzkreis = table
.product(Edition::V1_3c, "9991000002082")
.expect("Bilanzkreis");
assert_eq!(bilanzkreis.chapter.as_deref(), Some("6.1.1"));
assert!(bilanzkreis.property_codes.is_empty());
assert!(bilanzkreis.orderable_by("55014"));
let for_55014: Vec<&str> = table
.products(Edition::V1_3c)
.iter()
.filter(|p| p.orderable_by("55014"))
.map(|p| p.code.as_str())
.collect();
assert_eq!(for_55014, ["9991000002082"]);
let w = bilanzkreis.wertedetails.as_ref().expect("a Wertedetail");
assert_eq!(w.example_value(None), Some(EXAMPLE_BILANZKREIS));
let verbrauchsart = table
.product(Edition::V1_3c, "9991000002727")
.expect("Verbrauchsart");
assert_eq!(verbrauchsart.max_repetitions, Some(5));
assert!(verbrauchsart.wertedetails.is_none());
}
#[test]
fn tranchengroesse_takes_its_value_beside_its_property() {
let table = AbrechnungsdatenProdukte::anmeldung();
let w = |edition| {
table
.product(edition, "9991000002090")
.and_then(|p| p.wertedetails.as_ref())
.expect("Tranchengröße")
};
let share = Some("9991000003014");
let factor = Some("9991000003022");
let tr = Some("9991000003220");
let old = w(Edition::V1_3c);
assert!(old.needed_beside(share));
assert!(!old.needed_beside(factor));
assert_eq!(old.fits_beside("50.25", share), ConditionResult::True);
assert_eq!(old.fits_beside("100", share), ConditionResult::False);
assert_eq!(old.fits_beside("0", share), ConditionResult::False);
assert_eq!(old.fits_beside("50.125", share), ConditionResult::False);
assert_eq!(old.example_value(share), Some("50"));
let new = w(Edition::V1_4);
assert!(new.needed_beside(tr));
assert_eq!(new.example_value(tr), Some("D1234567890"));
assert_eq!(new.fits_beside("50", tr), ConditionResult::False);
assert_eq!(new.fits_beside("D1234567890", None), ConditionResult::True);
}
#[test]
fn example_bilanzkreis_is_a_valid_eic() {
assert!(is_eic(EXAMPLE_BILANZKREIS));
assert!(is_eic("11XLIEFERANT2--E"));
assert!(is_eic("10YDE-RWENET---I"));
assert!(!is_eic("10YDE-RWENET---J"));
assert!(!is_eic("DKWP98GBANI7"));
}
}