automapper_validation/eval/
abrechnungsdaten.rs1use serde::Deserialize;
18use std::collections::HashMap;
19use std::sync::OnceLock;
20
21use super::{ConditionResult, EvaluationContext};
22use crate::eval::format_validators::{is_edifact_number, validate_max_decimal_places};
23
24const EMBEDDED_JSON: &str = include_str!("../../data/konfigurationen_abrechnungsdaten.json");
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Edition {
29 V1_3c,
31 V1_4,
33}
34
35impl Edition {
36 pub fn of_format_version(fv: &str) -> Option<Self> {
38 match fv {
39 "FV2504" | "FV2510" | "FV2604" => Some(Self::V1_3c),
40 "FV2610" => Some(Self::V1_4),
41 _ => None,
42 }
43 }
44
45 fn key(self) -> &'static str {
46 match self {
47 Self::V1_3c => "1.3c",
48 Self::V1_4 => "1.4",
49 }
50 }
51}
52
53#[derive(Debug, Deserialize)]
54struct RawFile {
55 versions: HashMap<String, RawVersion>,
56}
57
58#[derive(Debug, Deserialize)]
59struct RawVersion {
60 products: Vec<Product>,
61}
62
63#[derive(Debug, Deserialize)]
65pub struct Product {
66 pub code: String,
68 pub name: String,
69 pub property_codes: Vec<PropertyCode>,
71 pub wertedetails: Option<Wertedetails>,
73}
74
75#[derive(Debug, Deserialize)]
76pub struct PropertyCode {
77 pub code: String,
78 pub name: String,
79}
80
81#[derive(Debug, Deserialize)]
84pub struct Wertedetails {
85 pub name: String,
86 pub conditions: Vec<u32>,
87}
88
89impl Wertedetails {
90 pub fn fits(&self, value: &str) -> ConditionResult {
93 let mut result = ConditionResult::True;
94 for condition in &self.conditions {
95 let holds = match condition {
96 970 => ConditionResult::from(value.chars().count() <= 17),
98 909 => ConditionResult::from(
100 is_edifact_number(value) && !value.trim_start().starts_with('-'),
101 ),
102 937 => {
104 ConditionResult::from(is_edifact_number(value) && !value.contains(['.', ',']))
105 }
106 906 => validate_max_decimal_places(value, 3),
108 _ => ConditionResult::Unknown,
109 };
110 match holds {
111 ConditionResult::False => return ConditionResult::False,
112 ConditionResult::Unknown => result = ConditionResult::Unknown,
113 ConditionResult::True => {}
114 }
115 }
116 result
117 }
118}
119
120#[derive(Debug)]
122pub struct AbrechnungsdatenProdukte {
123 editions: HashMap<String, Vec<Product>>,
124}
125
126impl AbrechnungsdatenProdukte {
127 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
128 let raw: RawFile = serde_json::from_str(json)?;
129 Ok(Self {
130 editions: raw
131 .versions
132 .into_iter()
133 .map(|(version, v)| (version, v.products))
134 .collect(),
135 })
136 }
137
138 pub fn embedded() -> &'static Self {
139 static CELL: OnceLock<AbrechnungsdatenProdukte> = OnceLock::new();
140 CELL.get_or_init(|| {
141 AbrechnungsdatenProdukte::from_json(EMBEDDED_JSON)
142 .expect("embedded konfigurationen_abrechnungsdaten.json is malformed")
143 })
144 }
145
146 pub fn products(&self, edition: Edition) -> &[Product] {
148 self.editions
149 .get(edition.key())
150 .map_or(&[], |products| products.as_slice())
151 }
152
153 pub fn product(&self, edition: Edition, code: &str) -> Option<&Product> {
155 self.products(edition).iter().find(|p| p.code == code)
156 }
157}
158
159fn ordered_product(ctx: &EvaluationContext) -> Option<String> {
163 ctx.scoped_find_segments_in("SG29", "PIA")
164 .into_iter()
165 .find(|s| s.get_element(0) == "5")
166 .and_then(|s| s.elements.get(1)?.first().cloned())
167 .filter(|code| !code.is_empty())
168}
169
170fn of_product(
173 ctx: &EvaluationContext,
174 edition: Edition,
175 answer: impl Fn(&Product) -> ConditionResult,
176) -> ConditionResult {
177 let Some(code) = ordered_product(ctx) else {
178 return ConditionResult::Unknown;
179 };
180 match AbrechnungsdatenProdukte::embedded().product(edition, &code) {
181 Some(product) => answer(product),
182 None => ConditionResult::False,
183 }
184}
185
186pub fn product_is_listed(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
190 let code = match ctx.resolved_value {
191 Some(value) => value.to_string(),
192 None => match ordered_product(ctx) {
193 Some(code) => code,
194 None => return ConditionResult::Unknown,
195 },
196 };
197 ConditionResult::from(
198 AbrechnungsdatenProdukte::embedded()
199 .product(edition, &code)
200 .is_some(),
201 )
202}
203
204pub fn has_property_codes(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
207 of_product(ctx, edition, |p| {
208 ConditionResult::from(!p.property_codes.is_empty())
209 })
210}
211
212pub fn property_code_allowed(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
215 let Some(value) = ctx.resolved_value else {
216 return ConditionResult::Unknown;
217 };
218 of_product(ctx, edition, |p| {
219 ConditionResult::from(p.property_codes.iter().any(|c| c.code == value))
220 })
221}
222
223pub fn wertedetails_value_fits(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
227 let Some(value) = ctx.resolved_value else {
228 return ConditionResult::Unknown;
229 };
230 of_product(ctx, edition, |p| match &p.wertedetails {
231 Some(w) => w.fits(value),
232 None => ConditionResult::False,
233 })
234}
235
236pub fn has_wertedetails(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
241 of_product(ctx, edition, |p| {
242 ConditionResult::from(p.wertedetails.is_some())
243 })
244}
245
246pub fn candidate_values(message_type: &str, fv: &str, condition: u32) -> Vec<String> {
251 let Some(edition) = Edition::of_format_version(fv).filter(|_| message_type == "ORDERS") else {
252 return Vec::new();
253 };
254 let products = AbrechnungsdatenProdukte::embedded().products(edition);
255 match condition {
256 158 => products.iter().map(|p| p.code.clone()).collect(),
257 160 => products
258 .iter()
259 .flat_map(|p| p.property_codes.iter().map(|c| c.code.clone()))
260 .collect(),
261 _ => Vec::new(),
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn both_editions_load() {
271 let table = AbrechnungsdatenProdukte::embedded();
272 assert_eq!(table.products(Edition::V1_3c).len(), 9);
273 assert_eq!(table.products(Edition::V1_4).len(), 10);
274 assert!(table.product(Edition::V1_3c, "9991000003246").is_none());
275 let arbeit = table.product(Edition::V1_4, "9991000003246").expect("1.4");
276 assert_eq!(arbeit.name, "spezifische Arbeit");
277 }
278
279 #[test]
280 fn wertedetails_hold_their_formats() {
281 let table = AbrechnungsdatenProdukte::embedded();
282 let w = |code: &str| {
283 table
284 .product(Edition::V1_4, code)
285 .and_then(|p| p.wertedetails.as_ref())
286 .expect("a Wertedetail")
287 };
288 let bilanzkreis = w("9991000002553");
289 assert_eq!(bilanzkreis.fits("11XLIEFERANT2--E"), ConditionResult::True);
290 assert_eq!(
291 bilanzkreis.fits("11XLIEFERANT2--E-TOO-LONG"),
292 ConditionResult::False
293 );
294 let prognose = w("9991000002561");
295 assert_eq!(prognose.fits("4000"), ConditionResult::True);
296 assert_eq!(prognose.fits("4000.5"), ConditionResult::False);
297 assert_eq!(prognose.fits("-1"), ConditionResult::False);
298 assert_eq!(prognose.fits("11XLIEFERANT2--E"), ConditionResult::False);
299 let arbeit = w("9991000003246");
300 assert_eq!(arbeit.fits("1.125"), ConditionResult::True);
301 assert_eq!(arbeit.fits("1.1255"), ConditionResult::False);
302 assert_eq!(arbeit.fits("-0.5"), ConditionResult::False);
303 }
304}