Skip to main content

automapper_validation/eval/
providers.rs

1//! Concrete [`ExternalConditionProvider`] implementations.
2//!
3//! - [`MapExternalProvider`]: wraps a `HashMap<String, bool>` for simple lookup.
4//! - [`CompositeExternalProvider`]: chains multiple providers, returning the first
5//!   non-[`Unknown`](super::ConditionResult::Unknown) result.
6
7use std::collections::{HashMap, HashSet};
8
9use super::evaluator::{ConditionResult, ExternalConditionProvider};
10
11/// Countries where postal codes (PLZ) are required in the EU/EEA energy market.
12const COUNTRIES_WITH_PLZ: &[&str] = &[
13    "DE", "AT", "CH", "BE", "BG", "CZ", "DK", "EE", "FI", "FR", "GR", "HR", "HU", "IE", "IT", "LT",
14    "LU", "LV", "MT", "NL", "PL", "PT", "RO", "SE", "SI", "SK", "ES", "GB", "NO", "CY", "IS", "LI",
15];
16
17const COUNTRY_PLZ_CONDITIONS: &[&str] = &[
18    "country_code_has_plz",
19    "country_has_postal_code",
20    "country_has_postal_code_requirement",
21];
22
23/// Provider that resolves country→postal code (PLZ) conditions.
24///
25/// Returns True if the country code is in the set of countries with PLZ requirements,
26/// False if it's a known country without PLZ, Unknown if no country code is set.
27pub struct CountryPostalCodeProvider {
28    country_code: Option<String>,
29}
30
31impl CountryPostalCodeProvider {
32    /// Create a new provider with the given ISO 3166-1 alpha-2 country code.
33    pub fn new(country_code: Option<String>) -> Self {
34        Self { country_code }
35    }
36}
37
38impl ExternalConditionProvider for CountryPostalCodeProvider {
39    fn evaluate(&self, condition_name: &str) -> ConditionResult {
40        if !COUNTRY_PLZ_CONDITIONS.contains(&condition_name) {
41            return ConditionResult::Unknown;
42        }
43        match &self.country_code {
44            Some(code) => ConditionResult::from(COUNTRIES_WITH_PLZ.contains(&code.as_str())),
45            None => ConditionResult::Unknown,
46        }
47    }
48}
49
50/// An [`ExternalConditionProvider`] backed by a `HashMap<String, bool>`.
51///
52/// Returns `True`/`False` for keys present in the map and `Unknown` for
53/// missing keys. This is the simplest way for API callers to supply
54/// external condition values.
55///
56/// # Example
57///
58/// ```
59/// use std::collections::HashMap;
60/// use automapper_validation::{MapExternalProvider, ConditionResult};
61/// use automapper_validation::eval::ExternalConditionProvider;
62///
63/// let mut conditions = HashMap::new();
64/// conditions.insert("DateKnown".to_string(), true);
65///
66/// let provider = MapExternalProvider::new(conditions);
67/// assert_eq!(provider.evaluate("DateKnown"), ConditionResult::True);
68/// assert_eq!(provider.evaluate("Unknown"), ConditionResult::Unknown);
69/// ```
70pub struct MapExternalProvider {
71    conditions: HashMap<String, bool>,
72}
73
74impl MapExternalProvider {
75    /// Creates a new `MapExternalProvider` from the given condition map.
76    pub fn new(conditions: HashMap<String, bool>) -> Self {
77        Self { conditions }
78    }
79}
80
81impl ExternalConditionProvider for MapExternalProvider {
82    fn evaluate(&self, condition_name: &str) -> ConditionResult {
83        match self.conditions.get(condition_name) {
84            Some(true) => ConditionResult::True,
85            Some(false) => ConditionResult::False,
86            None => ConditionResult::Unknown,
87        }
88    }
89}
90
91/// An [`ExternalConditionProvider`] that delegates to multiple providers in order.
92///
93/// For each `evaluate()` call, providers are consulted in sequence. The first
94/// provider that returns a non-[`Unknown`](ConditionResult::Unknown) result wins.
95/// If all providers return `Unknown` (or there are no providers), `Unknown` is
96/// returned.
97///
98/// This is useful for layering: e.g., a caller-supplied map on top of a
99/// system-default provider.
100pub struct CompositeExternalProvider {
101    providers: Vec<Box<dyn ExternalConditionProvider>>,
102}
103
104impl CompositeExternalProvider {
105    /// Creates a new `CompositeExternalProvider` from the given provider list.
106    ///
107    /// Providers are consulted in the order they appear in the vector.
108    pub fn new(providers: Vec<Box<dyn ExternalConditionProvider>>) -> Self {
109        Self { providers }
110    }
111
112    /// Build a composite provider with the standard static providers.
113    ///
114    /// - `sector`: If Some, adds a `SectorProvider`
115    /// - `roles`: If Some((sender_roles, recipient_roles)), adds a `MarketRoleProvider`
116    /// - `code_list_json`: If Some, adds a `CodeListProvider` loaded from JSON
117    /// - `konfigurationen_json`: If Some((json, product_code)), adds a `KonfigurationenProvider`
118    pub fn with_defaults(
119        sector: Option<Sector>,
120        roles: Option<(Vec<MarketRole>, Vec<MarketRole>)>,
121        code_list_json: Option<&str>,
122    ) -> Self {
123        Self::builder()
124            .sector(sector)
125            .roles(roles)
126            .code_list_json(code_list_json)
127            .build()
128    }
129
130    /// Start building a composite provider with fine-grained control.
131    pub fn builder() -> CompositeProviderBuilder {
132        CompositeProviderBuilder::default()
133    }
134}
135
136/// Builder for constructing a [`CompositeExternalProvider`] with multiple provider types.
137#[derive(Default)]
138pub struct CompositeProviderBuilder {
139    sector: Option<Sector>,
140    roles: Option<(Vec<MarketRole>, Vec<MarketRole>)>,
141    code_list_json: Option<String>,
142    konfigurationen_json: Option<String>,
143    product_code: Option<String>,
144    country_code: Option<String>,
145}
146
147impl CompositeProviderBuilder {
148    pub fn sector(mut self, sector: Option<Sector>) -> Self {
149        self.sector = sector;
150        self
151    }
152
153    pub fn roles(mut self, roles: Option<(Vec<MarketRole>, Vec<MarketRole>)>) -> Self {
154        self.roles = roles;
155        self
156    }
157
158    pub fn code_list_json(mut self, json: Option<&str>) -> Self {
159        self.code_list_json = json.map(String::from);
160        self
161    }
162
163    pub fn konfigurationen_json(mut self, json: &str) -> Self {
164        self.konfigurationen_json = Some(json.to_string());
165        self
166    }
167
168    pub fn product_code(mut self, code: Option<String>) -> Self {
169        self.product_code = code;
170        self
171    }
172
173    pub fn country_code(mut self, code: Option<String>) -> Self {
174        self.country_code = code;
175        self
176    }
177
178    pub fn build(self) -> CompositeExternalProvider {
179        let mut providers: Vec<Box<dyn ExternalConditionProvider>> = Vec::new();
180
181        if let Some(sector) = self.sector {
182            providers.push(Box::new(SectorProvider::new(sector)));
183        }
184        if let Some((sender, recipient)) = self.roles {
185            providers.push(Box::new(MarketRoleProvider::new(sender, recipient)));
186        }
187        if let Some(json) = &self.code_list_json {
188            if let Ok(provider) = CodeListProvider::from_json(json) {
189                providers.push(Box::new(provider));
190            }
191        }
192        if let Some(json) = &self.konfigurationen_json {
193            if let Ok(provider) = KonfigurationenProvider::from_json(json, self.product_code) {
194                providers.push(Box::new(provider));
195            }
196        }
197        if self.country_code.is_some() {
198            providers.push(Box::new(CountryPostalCodeProvider::new(self.country_code)));
199        }
200
201        CompositeExternalProvider::new(providers)
202    }
203}
204
205impl ExternalConditionProvider for CompositeExternalProvider {
206    fn evaluate(&self, condition_name: &str) -> ConditionResult {
207        for provider in &self.providers {
208            let result = provider.evaluate(condition_name);
209            if !result.is_unknown() {
210                return result;
211            }
212        }
213        ConditionResult::Unknown
214    }
215}
216
217/// The present moment, as a caller tells it ([`ExternalConditionProvider::now`]).
218pub type Clock = std::sync::Arc<dyn Fn() -> chrono::DateTime<chrono::Utc> + Send + Sync>;
219
220/// `inner`'s answers, with `clock` as the present moment: for a caller on a
221/// simulated clock, whose messages are dated by it (mako-twin's sw-sim), so
222/// that UTILMD `[494]` ("nicht nach der Erstellung des Dokuments") judges
223/// them by that clock rather than the wall clock.
224pub struct ClockedProvider<P> {
225    inner: P,
226    clock: Clock,
227}
228
229impl<P: ExternalConditionProvider> ClockedProvider<P> {
230    pub fn new(inner: P, clock: Clock) -> Self {
231        Self { inner, clock }
232    }
233}
234
235impl<P: ExternalConditionProvider> ExternalConditionProvider for ClockedProvider<P> {
236    fn evaluate(&self, condition_name: &str) -> ConditionResult {
237        self.inner.evaluate(condition_name)
238    }
239
240    fn now(&self) -> chrono::DateTime<chrono::Utc> {
241        (self.clock)()
242    }
243}
244
245/// Provider that checks whether a value belongs to a known code list.
246///
247/// Condition name format: `"code_in_<data_element_id>:<value>"`
248/// Returns True if the value is in the list, False if not, Unknown if the list is unknown.
249pub struct CodeListProvider {
250    lists: HashMap<String, HashSet<String>>,
251}
252
253impl CodeListProvider {
254    pub fn new(lists: HashMap<String, Vec<String>>) -> Self {
255        Self {
256            lists: lists
257                .into_iter()
258                .map(|(k, v)| (k, v.into_iter().collect()))
259                .collect(),
260        }
261    }
262
263    /// Load from the JSON format produced by `extract-code-lists` CLI.
264    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
265        #[derive(serde::Deserialize)]
266        struct Entry {
267            codes: Vec<CodeValue>,
268        }
269        #[derive(serde::Deserialize)]
270        struct CodeValue {
271            value: String,
272        }
273
274        let raw: HashMap<String, Entry> = serde_json::from_str(json)?;
275        let lists = raw
276            .into_iter()
277            .map(|(k, v)| (k, v.codes.into_iter().map(|c| c.value).collect()))
278            .collect();
279        Ok(Self { lists })
280    }
281
282    /// Load from a JSON file path.
283    pub fn from_json_file(
284        path: &std::path::Path,
285    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
286        let json = std::fs::read_to_string(path)?;
287        Ok(Self::from_json(&json)?)
288    }
289}
290
291impl ExternalConditionProvider for CodeListProvider {
292    fn evaluate(&self, condition_name: &str) -> ConditionResult {
293        let rest = match condition_name.strip_prefix("code_in_") {
294            Some(r) => r,
295            None => return ConditionResult::Unknown,
296        };
297        let (de_id, value) = match rest.split_once(':') {
298            Some(pair) => pair,
299            None => return ConditionResult::Unknown,
300        };
301        match self.lists.get(de_id) {
302            Some(set) => ConditionResult::from(set.contains(value)),
303            None => ConditionResult::Unknown,
304        }
305    }
306}
307
308/// Energy sector (Strom = electricity, Gas = natural gas).
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310pub enum Sector {
311    Strom,
312    Gas,
313}
314
315/// Provider that resolves sector-based conditions from deployment configuration.
316pub struct SectorProvider {
317    sector: Sector,
318}
319
320impl SectorProvider {
321    pub fn new(sector: Sector) -> Self {
322        Self { sector }
323    }
324
325    /// Create from variant string (e.g., "Strom", "Gas").
326    pub fn from_variant(variant: &str) -> Option<Self> {
327        match variant {
328            "Strom" => Some(Self::new(Sector::Strom)),
329            "Gas" => Some(Self::new(Sector::Gas)),
330            _ => None,
331        }
332    }
333}
334
335const STROM_CONDITIONS: &[&str] = &[
336    "recipient_is_strom",
337    "recipient_market_sector_is_strom",
338    "recipient_is_electricity_sector",
339    "sender_is_strom",
340    "mp_id_is_strom",
341    "mp_id_is_strom_sector",
342    "mp_id_is_electricity_sector",
343    "mp_id_from_electricity_sector",
344    "mp_id_only_strom",
345    "mp_id_strom_only",
346    "mp_id_sparte_strom",
347    "market_location_is_electricity",
348    "marktpartner_is_strom",
349    "location_is_strom",
350    "metering_point_is_strom",
351    "network_location_is_strom",
352];
353
354const GAS_CONDITIONS: &[&str] = &[
355    "recipient_is_gas",
356    "recipient_market_sector_is_gas",
357    "recipient_is_gas_sector",
358    "sender_is_gas",
359    "sender_is_gas_sector",
360    "mp_id_is_gas",
361    "mp_id_is_gas_sector",
362    "market_location_is_gas",
363    "marktpartner_is_gas",
364    "location_is_gas",
365    "metering_point_is_gas",
366    "network_location_is_gas",
367    "recipient_is_msb_gas",
368];
369
370impl ExternalConditionProvider for SectorProvider {
371    fn evaluate(&self, condition_name: &str) -> ConditionResult {
372        if STROM_CONDITIONS.contains(&condition_name) {
373            return ConditionResult::from(self.sector == Sector::Strom);
374        }
375        if GAS_CONDITIONS.contains(&condition_name) {
376            return ConditionResult::from(self.sector == Sector::Gas);
377        }
378        ConditionResult::Unknown
379    }
380}
381
382/// Market participant roles in the German energy market.
383#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
384pub enum MarketRole {
385    /// Lieferant (supplier)
386    LF,
387    /// Netzbetreiber (network operator)
388    NB,
389    /// Messstellenbetreiber (metering point operator)
390    MSB,
391    /// Messdienstleister (metering service provider)
392    MDL,
393    /// Übertragungsnetzbetreiber (TSO)
394    UENB,
395    /// Bilanzkreisverantwortlicher
396    BKV,
397    /// Bilanzkoordinator
398    BIKO,
399    /// Einsatzverantwortlicher
400    ESA,
401    /// Marktgebietsverantwortlicher (market area manager)
402    MGV,
403    /// Kunde (customer)
404    KN,
405}
406
407impl MarketRole {
408    fn from_suffix(s: &str) -> Option<Self> {
409        match s {
410            "lf" => Some(Self::LF),
411            "nb" => Some(Self::NB),
412            "msb" => Some(Self::MSB),
413            "mdl" => Some(Self::MDL),
414            "uenb" => Some(Self::UENB),
415            "bkv" => Some(Self::BKV),
416            "biko" => Some(Self::BIKO),
417            "esa" => Some(Self::ESA),
418            "mgv" => Some(Self::MGV),
419            "kn" => Some(Self::KN),
420            _ => None,
421        }
422    }
423
424    /// Parse a compound role suffix like "lf_msb_nb" or "lf_or_nb" into multiple roles.
425    /// Tries `_or_` splitting first, then falls back to `_` splitting.
426    /// Returns None if any component is unrecognized.
427    fn parse_compound(s: &str) -> Option<Vec<Self>> {
428        // Try splitting on "_or_" first (e.g., "lf_or_nb")
429        if s.contains("_or_") {
430            let parts: Vec<&str> = s.split("_or_").collect();
431            let mut roles = Vec::new();
432            for part in parts {
433                roles.push(Self::from_suffix(part)?);
434            }
435            if !roles.is_empty() {
436                return Some(roles);
437            }
438        }
439        // Fall back to splitting on "_" (e.g., "lf_msb_nb")
440        let parts: Vec<&str> = s.split('_').collect();
441        let mut roles = Vec::new();
442        for part in parts {
443            roles.push(Self::from_suffix(part)?);
444        }
445        if roles.is_empty() {
446            None
447        } else {
448            Some(roles)
449        }
450    }
451}
452
453/// Provider that resolves sender/recipient market role conditions.
454pub struct MarketRoleProvider {
455    sender_roles: HashSet<MarketRole>,
456    recipient_roles: HashSet<MarketRole>,
457}
458
459impl MarketRoleProvider {
460    pub fn new(sender_roles: Vec<MarketRole>, recipient_roles: Vec<MarketRole>) -> Self {
461        Self {
462            sender_roles: sender_roles.into_iter().collect(),
463            recipient_roles: recipient_roles.into_iter().collect(),
464        }
465    }
466}
467
468impl ExternalConditionProvider for MarketRoleProvider {
469    fn evaluate(&self, condition_name: &str) -> ConditionResult {
470        // Single role: sender_is_lf, recipient_is_nb
471        if let Some(role_str) = condition_name.strip_prefix("sender_is_") {
472            if let Some(role) = MarketRole::from_suffix(role_str) {
473                return ConditionResult::from(self.sender_roles.contains(&role));
474            }
475            // Compound: sender_is_lf_nb = sender is LF OR NB
476            if let Some(roles) = MarketRole::parse_compound(role_str) {
477                return ConditionResult::from(roles.iter().any(|r| self.sender_roles.contains(r)));
478            }
479        }
480        if let Some(role_str) = condition_name.strip_prefix("recipient_is_") {
481            if let Some(role) = MarketRole::from_suffix(role_str) {
482                return ConditionResult::from(self.recipient_roles.contains(&role));
483            }
484            // Compound: recipient_is_lf_msb = recipient is LF OR MSB
485            if let Some(roles) = MarketRole::parse_compound(role_str) {
486                return ConditionResult::from(
487                    roles.iter().any(|r| self.recipient_roles.contains(r)),
488                );
489            }
490        }
491        if let Some(role_str) = condition_name.strip_prefix("sender_role_is_not_") {
492            if let Some(role) = MarketRole::from_suffix(role_str) {
493                return ConditionResult::from(!self.sender_roles.contains(&role));
494            }
495        }
496        if let Some(role_str) = condition_name.strip_prefix("recipient_role_is_not_") {
497            if let Some(role) = MarketRole::from_suffix(role_str) {
498                return ConditionResult::from(!self.recipient_roles.contains(&role));
499            }
500        }
501        // recipient_not_<roles> = recipient is NOT any of the given roles
502        if let Some(role_str) = condition_name.strip_prefix("recipient_not_") {
503            if let Some(role) = MarketRole::from_suffix(role_str) {
504                return ConditionResult::from(!self.recipient_roles.contains(&role));
505            }
506            if let Some(roles) = MarketRole::parse_compound(role_str) {
507                return ConditionResult::from(
508                    !roles.iter().any(|r| self.recipient_roles.contains(r)),
509                );
510            }
511        }
512        // sender_not_<roles> = sender is NOT any of the given roles
513        if let Some(role_str) = condition_name.strip_prefix("sender_not_") {
514            if let Some(role) = MarketRole::from_suffix(role_str) {
515                return ConditionResult::from(!self.sender_roles.contains(&role));
516            }
517            if let Some(roles) = MarketRole::parse_compound(role_str) {
518                return ConditionResult::from(!roles.iter().any(|r| self.sender_roles.contains(r)));
519            }
520        }
521        // mp_id_role_is_<role> — treated like sender role check
522        if let Some(role_str) = condition_name.strip_prefix("mp_id_role_is_") {
523            if let Some(role) = MarketRole::from_suffix(role_str) {
524                return ConditionResult::from(self.sender_roles.contains(&role));
525            }
526        }
527        ConditionResult::Unknown
528    }
529}
530
531/// Provider that resolves product/Konfigurationen conditions based on a product code.
532///
533/// Loads the categorized product codes from the Codeliste der Konfigurationen and checks
534/// whether a given product code belongs to specific categories.
535///
536/// Supports conditions like:
537/// - `messprodukt_standard_marktlokation` — product is in chapter 2.1
538/// - `messprodukt_standard_tranche` — product is in chapter 2.2
539/// - `config_product_leistungskurve` — product is the Leistungskurve product
540/// - `code_list_membership_check` — product is in any category of the Konfigurationen
541///
542/// Aliases that map alternative condition names to Konfigurationen lookups.
543enum KonfigurationenAlias {
544    /// Check against all_codes (same as code_list_membership_check).
545    AllCodes,
546    /// Check against a single category by canonical name.
547    Category(&'static str),
548    /// Check against the union of all messprodukt_* categories.
549    MessproduktUnion,
550}
551
552fn resolve_konfigurationen_alias(condition_name: &str) -> Option<KonfigurationenAlias> {
553    match condition_name {
554        "is_konfigurationsprodukt_code" | "lin_prefix_is_valid_product_code" => {
555            Some(KonfigurationenAlias::AllCodes)
556        }
557        "is_messprodukt_code" => Some(KonfigurationenAlias::MessproduktUnion),
558        "messprodukts_typ2_smgw"
559        | "product_in_typ2_smgw_codelist"
560        | "order_contains_smgw_type2_product" => {
561            Some(KonfigurationenAlias::Category("messprodukt_typ2_smgw"))
562        }
563        "valid_adhoc_steuerkanal_product" => Some(KonfigurationenAlias::Category(
564            "config_product_adhoc_steuerkanal",
565        )),
566        "product_code_level_messlokation" => Some(KonfigurationenAlias::Category(
567            "messprodukt_standard_messlokation",
568        )),
569        "product_code_level_netzlokation" => Some(KonfigurationenAlias::Category(
570            "messprodukt_standard_netzlokation",
571        )),
572        "product_code_abrechnungsdaten_valid" => Some(KonfigurationenAlias::Category(
573            "produkte_aenderung_abrechnungsdaten",
574        )),
575        "lin_product_code_is_lokationsaenderung_strom" => Some(KonfigurationenAlias::Category(
576            "produkte_aenderung_lokation",
577        )),
578        _ => None,
579    }
580}
581
582/// Prefixes for messprodukt_* category matching to build the union set.
583const MESSPRODUKT_CATEGORY_PREFIXES: &[&str] = &[
584    "messprodukt_standard_",
585    "messprodukt_typ2_smgw",
586    "messprodukt_esa",
587];
588
589pub struct KonfigurationenProvider {
590    /// Map from category name to set of product codes.
591    categories: HashMap<String, HashSet<String>>,
592    /// All known product codes across all categories.
593    all_codes: HashSet<String>,
594    /// Union of all messprodukt_* categories.
595    messprodukt_union: HashSet<String>,
596    /// The product code to check against.
597    product_code: Option<String>,
598}
599
600impl KonfigurationenProvider {
601    /// Create from pre-parsed categories and a product code to check.
602    pub fn new(categories: HashMap<String, Vec<String>>, product_code: Option<String>) -> Self {
603        let mut all_codes = HashSet::new();
604        let categories: HashMap<String, HashSet<String>> = categories
605            .into_iter()
606            .map(|(k, v)| {
607                for code in &v {
608                    all_codes.insert(code.clone());
609                }
610                (k, v.into_iter().collect())
611            })
612            .collect();
613
614        // Build the messprodukt union from all matching categories
615        let mut messprodukt_union = HashSet::new();
616        for (name, codes) in &categories {
617            let is_messprodukt = MESSPRODUKT_CATEGORY_PREFIXES
618                .iter()
619                .any(|prefix| name.starts_with(prefix) || name == prefix);
620            if is_messprodukt {
621                messprodukt_union.extend(codes.iter().cloned());
622            }
623        }
624
625        Self {
626            categories,
627            all_codes,
628            messprodukt_union,
629            product_code,
630        }
631    }
632
633    /// Load from the JSON format of `konfigurationen_code_lists.json`.
634    pub fn from_json(json: &str, product_code: Option<String>) -> Result<Self, serde_json::Error> {
635        #[derive(serde::Deserialize)]
636        struct KonfigurationenFile {
637            categories: HashMap<String, Vec<String>>,
638            #[serde(default)]
639            all_product_codes: Vec<String>,
640        }
641        let file: KonfigurationenFile = serde_json::from_str(json)?;
642        let mut provider = Self::new(file.categories, product_code);
643        // Merge explicit all_product_codes list if present
644        for code in file.all_product_codes {
645            provider.all_codes.insert(code);
646        }
647        Ok(provider)
648    }
649
650    /// Load from a JSON file path.
651    pub fn from_json_file(
652        path: &std::path::Path,
653        product_code: Option<String>,
654    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
655        let json = std::fs::read_to_string(path)?;
656        Ok(Self::from_json(&json, product_code)?)
657    }
658}
659
660impl ExternalConditionProvider for KonfigurationenProvider {
661    fn evaluate(&self, condition_name: &str) -> ConditionResult {
662        let code = match &self.product_code {
663            Some(c) => c,
664            None => return ConditionResult::Unknown,
665        };
666
667        // Generic membership: is this product in the Konfigurationen at all?
668        if condition_name == "code_list_membership_check" {
669            return ConditionResult::from(self.all_codes.contains(code.as_str()));
670        }
671
672        // Check aliases before category lookup
673        if let Some(alias) = resolve_konfigurationen_alias(condition_name) {
674            return match alias {
675                KonfigurationenAlias::AllCodes => {
676                    ConditionResult::from(self.all_codes.contains(code.as_str()))
677                }
678                KonfigurationenAlias::Category(cat) => {
679                    if let Some(set) = self.categories.get(cat) {
680                        ConditionResult::from(set.contains(code.as_str()))
681                    } else {
682                        ConditionResult::Unknown
683                    }
684                }
685                KonfigurationenAlias::MessproduktUnion => {
686                    ConditionResult::from(self.messprodukt_union.contains(code.as_str()))
687                }
688            };
689        }
690
691        // Category-specific: is this product in the named category?
692        if let Some(set) = self.categories.get(condition_name) {
693            return ConditionResult::from(set.contains(code.as_str()));
694        }
695
696        ConditionResult::Unknown
697    }
698}
699
700#[cfg(test)]
701mod tests {
702    use super::*;
703
704    // ---- MapExternalProvider tests ----
705
706    #[test]
707    fn map_provider_returns_true_for_true_entry() {
708        let mut conditions = HashMap::new();
709        conditions.insert("DateKnown".to_string(), true);
710        let provider = MapExternalProvider::new(conditions);
711
712        assert_eq!(provider.evaluate("DateKnown"), ConditionResult::True);
713    }
714
715    #[test]
716    fn map_provider_returns_false_for_false_entry() {
717        let mut conditions = HashMap::new();
718        conditions.insert("MessageSplitting".to_string(), false);
719        let provider = MapExternalProvider::new(conditions);
720
721        assert_eq!(
722            provider.evaluate("MessageSplitting"),
723            ConditionResult::False
724        );
725    }
726
727    #[test]
728    fn map_provider_returns_unknown_for_missing_key() {
729        let mut conditions = HashMap::new();
730        conditions.insert("DateKnown".to_string(), true);
731        let provider = MapExternalProvider::new(conditions);
732
733        assert_eq!(provider.evaluate("NonExistent"), ConditionResult::Unknown);
734    }
735
736    #[test]
737    fn map_provider_empty_map_returns_unknown() {
738        let provider = MapExternalProvider::new(HashMap::new());
739
740        assert_eq!(provider.evaluate("Anything"), ConditionResult::Unknown);
741    }
742
743    // ---- CompositeExternalProvider tests ----
744
745    #[test]
746    fn composite_first_known_wins() {
747        // Provider 1 knows "A" = true, but not "B"
748        let mut p1_map = HashMap::new();
749        p1_map.insert("A".to_string(), true);
750        let p1 = MapExternalProvider::new(p1_map);
751
752        // Provider 2 knows "B" = false, but not "A"
753        let mut p2_map = HashMap::new();
754        p2_map.insert("B".to_string(), false);
755        let p2 = MapExternalProvider::new(p2_map);
756
757        let composite = CompositeExternalProvider::new(vec![Box::new(p1), Box::new(p2)]);
758
759        // "A" resolved by p1
760        assert_eq!(composite.evaluate("A"), ConditionResult::True);
761        // "B" not in p1 (Unknown), resolved by p2
762        assert_eq!(composite.evaluate("B"), ConditionResult::False);
763    }
764
765    #[test]
766    fn composite_all_unknown_returns_unknown() {
767        // Two providers, neither knows "X"
768        let p1 = MapExternalProvider::new(HashMap::new());
769        let p2 = MapExternalProvider::new(HashMap::new());
770
771        let composite = CompositeExternalProvider::new(vec![Box::new(p1), Box::new(p2)]);
772
773        assert_eq!(composite.evaluate("X"), ConditionResult::Unknown);
774    }
775
776    #[test]
777    fn composite_empty_returns_unknown() {
778        let composite = CompositeExternalProvider::new(vec![]);
779
780        assert_eq!(composite.evaluate("Anything"), ConditionResult::Unknown);
781    }
782
783    // ---- CodeListProvider tests ----
784
785    #[test]
786    fn test_code_list_provider_known_code() {
787        let mut lists = HashMap::new();
788        lists.insert(
789            "7111".to_string(),
790            vec!["Z91".to_string(), "Z90".to_string()],
791        );
792        let provider = CodeListProvider::new(lists);
793        assert_eq!(provider.evaluate("code_in_7111:Z91"), ConditionResult::True);
794        assert_eq!(
795            provider.evaluate("code_in_7111:ZZZ"),
796            ConditionResult::False
797        );
798        assert_eq!(
799            provider.evaluate("code_in_9999:Z91"),
800            ConditionResult::Unknown
801        );
802    }
803
804    #[test]
805    fn test_code_list_provider_loads_json() {
806        let json = r#"{
807            "7111": { "name": "Eigenschaft", "codes": [{"value": "Z91", "name": "MSB"}] },
808            "3225": { "name": "Ort", "codes": [{"value": "Z16", "name": "MaLo"}] }
809        }"#;
810        let provider = CodeListProvider::from_json(json).unwrap();
811        assert_eq!(provider.evaluate("code_in_7111:Z91"), ConditionResult::True);
812        assert_eq!(provider.evaluate("code_in_3225:Z16"), ConditionResult::True);
813        assert_eq!(
814            provider.evaluate("code_in_3225:Z99"),
815            ConditionResult::False
816        );
817    }
818
819    #[test]
820    fn test_code_list_provider_invalid_format() {
821        let mut lists = HashMap::new();
822        lists.insert("7111".to_string(), vec!["Z91".to_string()]);
823        let provider = CodeListProvider::new(lists);
824        assert_eq!(
825            provider.evaluate("not_a_code_check"),
826            ConditionResult::Unknown
827        );
828        assert_eq!(provider.evaluate("code_in_7111"), ConditionResult::Unknown);
829        // no colon
830    }
831
832    // ---- SectorProvider tests ----
833
834    #[test]
835    fn test_sector_provider_strom() {
836        let provider = SectorProvider::new(Sector::Strom);
837        assert_eq!(
838            provider.evaluate("recipient_is_strom"),
839            ConditionResult::True
840        );
841        assert_eq!(
842            provider.evaluate("recipient_is_gas"),
843            ConditionResult::False
844        );
845        assert_eq!(provider.evaluate("mp_id_is_strom"), ConditionResult::True);
846        assert_eq!(provider.evaluate("mp_id_is_gas"), ConditionResult::False);
847        assert_eq!(
848            provider.evaluate("market_location_is_electricity"),
849            ConditionResult::True
850        );
851        assert_eq!(
852            provider.evaluate("market_location_is_gas"),
853            ConditionResult::False
854        );
855        assert_eq!(
856            provider.evaluate("unrelated_condition"),
857            ConditionResult::Unknown
858        );
859    }
860
861    #[test]
862    fn test_sector_provider_gas() {
863        let provider = SectorProvider::new(Sector::Gas);
864        assert_eq!(
865            provider.evaluate("recipient_is_strom"),
866            ConditionResult::False
867        );
868        assert_eq!(provider.evaluate("recipient_is_gas"), ConditionResult::True);
869        assert_eq!(
870            provider.evaluate("recipient_is_msb_gas"),
871            ConditionResult::True
872        );
873    }
874
875    #[test]
876    fn test_sector_from_variant() {
877        assert!(SectorProvider::from_variant("Strom").is_some());
878        assert!(SectorProvider::from_variant("Gas").is_some());
879        assert!(SectorProvider::from_variant("Water").is_none());
880    }
881
882    // ---- MarketRoleProvider tests ----
883
884    #[test]
885    fn test_market_role_provider() {
886        let provider =
887            MarketRoleProvider::new(vec![MarketRole::LF], vec![MarketRole::NB, MarketRole::MSB]);
888        assert_eq!(provider.evaluate("sender_is_lf"), ConditionResult::True);
889        assert_eq!(provider.evaluate("sender_is_nb"), ConditionResult::False);
890        assert_eq!(provider.evaluate("recipient_is_nb"), ConditionResult::True);
891        assert_eq!(provider.evaluate("recipient_is_msb"), ConditionResult::True);
892        assert_eq!(provider.evaluate("recipient_is_lf"), ConditionResult::False);
893        assert_eq!(provider.evaluate("unrelated"), ConditionResult::Unknown);
894    }
895
896    #[test]
897    fn test_market_role_provider_negated() {
898        let provider = MarketRoleProvider::new(vec![MarketRole::MSB], vec![MarketRole::NB]);
899        assert_eq!(
900            provider.evaluate("sender_role_is_not_msb"),
901            ConditionResult::False
902        );
903        assert_eq!(
904            provider.evaluate("sender_role_is_not_lf"),
905            ConditionResult::True
906        );
907        assert_eq!(
908            provider.evaluate("recipient_role_is_not_nb"),
909            ConditionResult::False
910        );
911        assert_eq!(
912            provider.evaluate("recipient_role_is_not_lf"),
913            ConditionResult::True
914        );
915    }
916
917    #[test]
918    fn test_market_role_unknown_suffix() {
919        let provider = MarketRoleProvider::new(vec![MarketRole::LF], vec![]);
920        // "sender_is_xyz" — xyz not a known role, so Unknown
921        assert_eq!(provider.evaluate("sender_is_xyz"), ConditionResult::Unknown);
922    }
923
924    #[test]
925    fn test_market_role_compound() {
926        let provider = MarketRoleProvider::new(vec![MarketRole::LF], vec![MarketRole::NB]);
927        // recipient_is_lf_msb = recipient is LF OR MSB → NB is neither → False
928        assert_eq!(
929            provider.evaluate("recipient_is_lf_msb"),
930            ConditionResult::False
931        );
932        // recipient_is_nb_lf = recipient is NB OR LF → NB matches → True
933        assert_eq!(
934            provider.evaluate("recipient_is_nb_lf"),
935            ConditionResult::True
936        );
937        // recipient_is_lf_nb_msb = LF or NB or MSB → NB matches → True
938        assert_eq!(
939            provider.evaluate("recipient_is_lf_nb_msb"),
940            ConditionResult::True
941        );
942        // mp_id_role_is_lf → sender has LF → True
943        assert_eq!(provider.evaluate("mp_id_role_is_lf"), ConditionResult::True);
944    }
945
946    #[test]
947    fn test_sector_provider_additional_names() {
948        let provider = SectorProvider::new(Sector::Strom);
949        assert_eq!(
950            provider.evaluate("mp_id_is_strom_sector"),
951            ConditionResult::True
952        );
953        assert_eq!(
954            provider.evaluate("marktpartner_is_strom"),
955            ConditionResult::True
956        );
957        assert_eq!(
958            provider.evaluate("recipient_is_gas_sector"),
959            ConditionResult::False
960        );
961    }
962
963    #[test]
964    fn test_composite_with_defaults() {
965        let composite = CompositeExternalProvider::with_defaults(
966            Some(Sector::Strom),
967            Some((vec![MarketRole::LF], vec![MarketRole::NB])),
968            None,
969        );
970        // Sector resolved
971        assert_eq!(
972            composite.evaluate("recipient_is_strom"),
973            ConditionResult::True
974        );
975        // Market role resolved
976        assert_eq!(composite.evaluate("sender_is_lf"), ConditionResult::True);
977        // Unknown conditions still return Unknown
978        assert_eq!(composite.evaluate("some_unknown"), ConditionResult::Unknown);
979    }
980
981    // ---- KonfigurationenProvider tests ----
982
983    fn make_konfigurationen_provider(product_code: Option<&str>) -> KonfigurationenProvider {
984        let mut categories = HashMap::new();
985        categories.insert(
986            "messprodukt_standard_marktlokation".to_string(),
987            vec!["9991000000044".to_string(), "9991000000052".to_string()],
988        );
989        categories.insert(
990            "messprodukt_standard_tranche".to_string(),
991            vec!["9991000000143".to_string()],
992        );
993        categories.insert(
994            "config_product_leistungskurve".to_string(),
995            vec!["9991000000721".to_string()],
996        );
997        KonfigurationenProvider::new(categories, product_code.map(String::from))
998    }
999
1000    #[test]
1001    fn test_konfigurationen_category_match() {
1002        let provider = make_konfigurationen_provider(Some("9991000000044"));
1003        assert_eq!(
1004            provider.evaluate("messprodukt_standard_marktlokation"),
1005            ConditionResult::True
1006        );
1007        assert_eq!(
1008            provider.evaluate("messprodukt_standard_tranche"),
1009            ConditionResult::False
1010        );
1011        assert_eq!(
1012            provider.evaluate("config_product_leistungskurve"),
1013            ConditionResult::False
1014        );
1015    }
1016
1017    #[test]
1018    fn test_konfigurationen_generic_membership() {
1019        let provider = make_konfigurationen_provider(Some("9991000000044"));
1020        assert_eq!(
1021            provider.evaluate("code_list_membership_check"),
1022            ConditionResult::True
1023        );
1024
1025        let provider2 = make_konfigurationen_provider(Some("0000000000000"));
1026        assert_eq!(
1027            provider2.evaluate("code_list_membership_check"),
1028            ConditionResult::False
1029        );
1030    }
1031
1032    #[test]
1033    fn test_konfigurationen_no_product_code() {
1034        let provider = make_konfigurationen_provider(None);
1035        assert_eq!(
1036            provider.evaluate("messprodukt_standard_marktlokation"),
1037            ConditionResult::Unknown
1038        );
1039        assert_eq!(
1040            provider.evaluate("code_list_membership_check"),
1041            ConditionResult::Unknown
1042        );
1043    }
1044
1045    #[test]
1046    fn test_konfigurationen_unknown_condition() {
1047        let provider = make_konfigurationen_provider(Some("9991000000044"));
1048        assert_eq!(
1049            provider.evaluate("completely_unrelated"),
1050            ConditionResult::Unknown
1051        );
1052    }
1053
1054    #[test]
1055    fn test_konfigurationen_from_json() {
1056        let json = r#"{
1057            "source": "Test",
1058            "categories": {
1059                "messprodukt_standard_marktlokation": ["9991000000044", "9991000000052"],
1060                "config_product_leistungskurve": ["9991000000721"]
1061            },
1062            "all_product_codes": ["9991000000044", "9991000000052", "9991000000721"]
1063        }"#;
1064        let provider =
1065            KonfigurationenProvider::from_json(json, Some("9991000000721".to_string())).unwrap();
1066        assert_eq!(
1067            provider.evaluate("messprodukt_standard_marktlokation"),
1068            ConditionResult::False
1069        );
1070        assert_eq!(
1071            provider.evaluate("config_product_leistungskurve"),
1072            ConditionResult::True
1073        );
1074        assert_eq!(
1075            provider.evaluate("code_list_membership_check"),
1076            ConditionResult::True
1077        );
1078    }
1079
1080    #[test]
1081    fn test_konfigurationen_specific_product() {
1082        // Test config_product_leistungskurve matches exactly one code
1083        let provider = make_konfigurationen_provider(Some("9991000000721"));
1084        assert_eq!(
1085            provider.evaluate("config_product_leistungskurve"),
1086            ConditionResult::True
1087        );
1088        assert_eq!(
1089            provider.evaluate("messprodukt_standard_marktlokation"),
1090            ConditionResult::False
1091        );
1092    }
1093
1094    // ---- CountryPostalCodeProvider tests ----
1095
1096    #[test]
1097    fn test_country_plz_provider_de() {
1098        let provider = CountryPostalCodeProvider::new(Some("DE".to_string()));
1099        assert_eq!(
1100            provider.evaluate("country_code_has_plz"),
1101            ConditionResult::True
1102        );
1103        assert_eq!(
1104            provider.evaluate("country_has_postal_code"),
1105            ConditionResult::True
1106        );
1107        assert_eq!(
1108            provider.evaluate("country_has_postal_code_requirement"),
1109            ConditionResult::True
1110        );
1111    }
1112
1113    #[test]
1114    fn test_country_plz_provider_various_countries() {
1115        for code in &["AT", "CH", "FR", "NL", "GB", "NO", "IS", "LI"] {
1116            let provider = CountryPostalCodeProvider::new(Some(code.to_string()));
1117            assert_eq!(
1118                provider.evaluate("country_code_has_plz"),
1119                ConditionResult::True,
1120                "{code} should have PLZ"
1121            );
1122        }
1123    }
1124
1125    #[test]
1126    fn test_country_plz_provider_unknown_country() {
1127        let provider = CountryPostalCodeProvider::new(Some("XX".to_string()));
1128        assert_eq!(
1129            provider.evaluate("country_code_has_plz"),
1130            ConditionResult::False
1131        );
1132    }
1133
1134    #[test]
1135    fn test_country_plz_provider_no_country() {
1136        let provider = CountryPostalCodeProvider::new(None);
1137        assert_eq!(
1138            provider.evaluate("country_code_has_plz"),
1139            ConditionResult::Unknown
1140        );
1141    }
1142
1143    #[test]
1144    fn test_country_plz_provider_unrelated_condition() {
1145        let provider = CountryPostalCodeProvider::new(Some("DE".to_string()));
1146        assert_eq!(
1147            provider.evaluate("unrelated_condition"),
1148            ConditionResult::Unknown
1149        );
1150    }
1151
1152    // ---- SectorProvider: sender_is_gas_sector ----
1153
1154    #[test]
1155    fn test_sector_provider_sender_is_gas_sector() {
1156        let provider = SectorProvider::new(Sector::Gas);
1157        assert_eq!(
1158            provider.evaluate("sender_is_gas_sector"),
1159            ConditionResult::True
1160        );
1161        let provider_strom = SectorProvider::new(Sector::Strom);
1162        assert_eq!(
1163            provider_strom.evaluate("sender_is_gas_sector"),
1164            ConditionResult::False
1165        );
1166    }
1167
1168    // ---- MarketRole: MGV and KN ----
1169
1170    #[test]
1171    fn test_market_role_mgv_kn() {
1172        let provider = MarketRoleProvider::new(vec![MarketRole::MGV], vec![MarketRole::KN]);
1173        assert_eq!(provider.evaluate("sender_is_mgv"), ConditionResult::True);
1174        assert_eq!(provider.evaluate("recipient_is_kn"), ConditionResult::True);
1175        assert_eq!(provider.evaluate("sender_is_kn"), ConditionResult::False);
1176        assert_eq!(
1177            provider.evaluate("recipient_is_mgv"),
1178            ConditionResult::False
1179        );
1180    }
1181
1182    // ---- MarketRole: _or_ compound ----
1183
1184    #[test]
1185    fn test_market_role_or_compound() {
1186        let provider = MarketRoleProvider::new(vec![MarketRole::LF], vec![MarketRole::NB]);
1187        // recipient_is_lf_or_nb = recipient is LF OR NB → NB matches → True
1188        assert_eq!(
1189            provider.evaluate("recipient_is_lf_or_nb"),
1190            ConditionResult::True
1191        );
1192        // recipient_is_msb_or_mdl = recipient is MSB OR MDL → NB is neither → False
1193        assert_eq!(
1194            provider.evaluate("recipient_is_msb_or_mdl"),
1195            ConditionResult::False
1196        );
1197    }
1198
1199    // ---- MarketRole: recipient_not_ prefix ----
1200
1201    #[test]
1202    fn test_market_role_recipient_not() {
1203        let provider = MarketRoleProvider::new(vec![MarketRole::LF], vec![MarketRole::NB]);
1204        // recipient_not_mgv_or_kn = recipient is NOT (MGV or KN) → NB is neither → True
1205        assert_eq!(
1206            provider.evaluate("recipient_not_mgv_or_kn"),
1207            ConditionResult::True
1208        );
1209        // recipient_not_nb = recipient is NOT NB → False (NB is the recipient)
1210        assert_eq!(
1211            provider.evaluate("recipient_not_nb"),
1212            ConditionResult::False
1213        );
1214        // recipient_not_lf = recipient is NOT LF → True (NB is the recipient)
1215        assert_eq!(provider.evaluate("recipient_not_lf"), ConditionResult::True);
1216        // sender_not_lf = sender is NOT LF → False (LF is the sender)
1217        assert_eq!(provider.evaluate("sender_not_lf"), ConditionResult::False);
1218        // sender_not_nb_or_msb = sender is NOT (NB or MSB) → LF is neither → True
1219        assert_eq!(
1220            provider.evaluate("sender_not_nb_or_msb"),
1221            ConditionResult::True
1222        );
1223    }
1224
1225    // ---- KonfigurationenProvider alias tests ----
1226
1227    fn make_konfigurationen_provider_with_messprodukt(
1228        product_code: Option<&str>,
1229    ) -> KonfigurationenProvider {
1230        let mut categories = HashMap::new();
1231        categories.insert(
1232            "messprodukt_standard_marktlokation".to_string(),
1233            vec!["9991000000044".to_string(), "9991000000052".to_string()],
1234        );
1235        categories.insert(
1236            "messprodukt_standard_tranche".to_string(),
1237            vec!["9991000000143".to_string()],
1238        );
1239        categories.insert(
1240            "messprodukt_standard_messlokation".to_string(),
1241            vec!["9991000000200".to_string()],
1242        );
1243        categories.insert(
1244            "messprodukt_standard_netzlokation".to_string(),
1245            vec!["9991000000300".to_string()],
1246        );
1247        categories.insert(
1248            "messprodukt_typ2_smgw".to_string(),
1249            vec!["9991000000500".to_string()],
1250        );
1251        categories.insert(
1252            "messprodukt_esa".to_string(),
1253            vec!["9991000000600".to_string()],
1254        );
1255        categories.insert(
1256            "config_product_leistungskurve".to_string(),
1257            vec!["9991000000721".to_string()],
1258        );
1259        categories.insert(
1260            "config_product_adhoc_steuerkanal".to_string(),
1261            vec!["9991000000800".to_string()],
1262        );
1263        categories.insert(
1264            "produkte_aenderung_abrechnungsdaten".to_string(),
1265            vec!["9991000000900".to_string()],
1266        );
1267        categories.insert(
1268            "produkte_aenderung_lokation".to_string(),
1269            vec!["9991000001000".to_string()],
1270        );
1271        KonfigurationenProvider::new(categories, product_code.map(String::from))
1272    }
1273
1274    #[test]
1275    fn test_konfigurationen_alias_all_codes() {
1276        let provider = make_konfigurationen_provider_with_messprodukt(Some("9991000000044"));
1277        // is_konfigurationsprodukt_code → all_codes check
1278        assert_eq!(
1279            provider.evaluate("is_konfigurationsprodukt_code"),
1280            ConditionResult::True
1281        );
1282        // lin_prefix_is_valid_product_code → all_codes check
1283        assert_eq!(
1284            provider.evaluate("lin_prefix_is_valid_product_code"),
1285            ConditionResult::True
1286        );
1287
1288        let provider2 = make_konfigurationen_provider_with_messprodukt(Some("0000000000000"));
1289        assert_eq!(
1290            provider2.evaluate("is_konfigurationsprodukt_code"),
1291            ConditionResult::False
1292        );
1293    }
1294
1295    #[test]
1296    fn test_konfigurationen_alias_messprodukt_union() {
1297        // Code in messprodukt_standard_marktlokation (part of union)
1298        let provider = make_konfigurationen_provider_with_messprodukt(Some("9991000000044"));
1299        assert_eq!(
1300            provider.evaluate("is_messprodukt_code"),
1301            ConditionResult::True
1302        );
1303
1304        // Code in messprodukt_typ2_smgw (part of union)
1305        let provider2 = make_konfigurationen_provider_with_messprodukt(Some("9991000000500"));
1306        assert_eq!(
1307            provider2.evaluate("is_messprodukt_code"),
1308            ConditionResult::True
1309        );
1310
1311        // Code in messprodukt_esa (part of union)
1312        let provider3 = make_konfigurationen_provider_with_messprodukt(Some("9991000000600"));
1313        assert_eq!(
1314            provider3.evaluate("is_messprodukt_code"),
1315            ConditionResult::True
1316        );
1317
1318        // Code in config_product_leistungskurve (NOT in union)
1319        let provider4 = make_konfigurationen_provider_with_messprodukt(Some("9991000000721"));
1320        assert_eq!(
1321            provider4.evaluate("is_messprodukt_code"),
1322            ConditionResult::False
1323        );
1324    }
1325
1326    #[test]
1327    fn test_konfigurationen_alias_single_category() {
1328        let provider = make_konfigurationen_provider_with_messprodukt(Some("9991000000500"));
1329        // messprodukts_typ2_smgw → messprodukt_typ2_smgw
1330        assert_eq!(
1331            provider.evaluate("messprodukts_typ2_smgw"),
1332            ConditionResult::True
1333        );
1334        // product_in_typ2_smgw_codelist → messprodukt_typ2_smgw
1335        assert_eq!(
1336            provider.evaluate("product_in_typ2_smgw_codelist"),
1337            ConditionResult::True
1338        );
1339        // order_contains_smgw_type2_product → messprodukt_typ2_smgw
1340        assert_eq!(
1341            provider.evaluate("order_contains_smgw_type2_product"),
1342            ConditionResult::True
1343        );
1344
1345        let provider2 = make_konfigurationen_provider_with_messprodukt(Some("9991000000800"));
1346        // valid_adhoc_steuerkanal_product → config_product_adhoc_steuerkanal
1347        assert_eq!(
1348            provider2.evaluate("valid_adhoc_steuerkanal_product"),
1349            ConditionResult::True
1350        );
1351
1352        let provider3 = make_konfigurationen_provider_with_messprodukt(Some("9991000000200"));
1353        // product_code_level_messlokation → messprodukt_standard_messlokation
1354        assert_eq!(
1355            provider3.evaluate("product_code_level_messlokation"),
1356            ConditionResult::True
1357        );
1358
1359        let provider4 = make_konfigurationen_provider_with_messprodukt(Some("9991000000300"));
1360        // product_code_level_netzlokation → messprodukt_standard_netzlokation
1361        assert_eq!(
1362            provider4.evaluate("product_code_level_netzlokation"),
1363            ConditionResult::True
1364        );
1365
1366        let provider5 = make_konfigurationen_provider_with_messprodukt(Some("9991000000900"));
1367        // product_code_abrechnungsdaten_valid → produkte_aenderung_abrechnungsdaten
1368        assert_eq!(
1369            provider5.evaluate("product_code_abrechnungsdaten_valid"),
1370            ConditionResult::True
1371        );
1372
1373        let provider6 = make_konfigurationen_provider_with_messprodukt(Some("9991000001000"));
1374        // lin_product_code_is_lokationsaenderung_strom → produkte_aenderung_lokation
1375        assert_eq!(
1376            provider6.evaluate("lin_product_code_is_lokationsaenderung_strom"),
1377            ConditionResult::True
1378        );
1379    }
1380
1381    // ---- CompositeProviderBuilder: country_code ----
1382
1383    #[test]
1384    fn test_builder_with_country_code() {
1385        let composite = CompositeExternalProvider::builder()
1386            .country_code(Some("DE".to_string()))
1387            .build();
1388        assert_eq!(
1389            composite.evaluate("country_code_has_plz"),
1390            ConditionResult::True
1391        );
1392    }
1393}