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