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};
const EMBEDDED_JSON: &str = include_str!("../../data/konfigurationen_abrechnungsdaten.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>,
}
#[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>,
}
impl Wertedetails {
pub fn fits(&self, value: &str) -> ConditionResult {
let mut result = ConditionResult::True;
for condition in &self.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),
_ => 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 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)
}
}
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())
})
}
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);
}
}