Skip to main content

credit_data_simulator/
config.rs

1//! # Simulator Configuration
2//!
3//! This module provides configuration structures for all simulators.
4//! Each simulator has its own configuration with sensible defaults.
5
6use chrono::Timelike;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::time::Duration;
10
11// ============================================================================
12// Main Configuration
13// ============================================================================
14
15/// Configuration for all simulators
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SimulatorConfig {
18    /// Core Banking simulator configuration
19    pub core_banking: CoreBankingConfig,
20    /// Mapping Service simulator configuration
21    pub mapping_service: MappingServiceConfig,
22    /// Rulepack Service simulator configuration
23    pub rulepack_service: RulepackServiceConfig,
24    /// Regulator Endpoint simulator configuration
25    pub regulator_endpoint: RegulatorEndpointConfig,
26}
27
28impl Default for SimulatorConfig {
29    fn default() -> Self {
30        Self {
31            core_banking: CoreBankingConfig::default(),
32            mapping_service: MappingServiceConfig::default(),
33            rulepack_service: RulepackServiceConfig::default(),
34            regulator_endpoint: RegulatorEndpointConfig::default(),
35        }
36    }
37}
38
39impl SimulatorConfig {
40    /// Create a new configuration builder
41    pub fn builder() -> SimulatorConfigBuilder {
42        SimulatorConfigBuilder::default()
43    }
44
45    /// Load configuration from environment variables
46    pub fn from_env() -> Self {
47        let mut config = Self::default();
48
49        if let Ok(port) = std::env::var("CORE_BANKING_PORT") {
50            if let Ok(p) = port.parse() {
51                config.core_banking.port = p;
52            }
53        }
54        if let Ok(port) = std::env::var("MAPPING_SERVICE_PORT") {
55            if let Ok(p) = port.parse() {
56                config.mapping_service.port = p;
57            }
58        }
59        if let Ok(port) = std::env::var("RULEPACK_SERVICE_PORT") {
60            if let Ok(p) = port.parse() {
61                config.rulepack_service.port = p;
62            }
63        }
64        if let Ok(port) = std::env::var("REGULATOR_ENDPOINT_PORT") {
65            if let Ok(p) = port.parse() {
66                config.regulator_endpoint.port = p;
67            }
68        }
69
70        config
71    }
72
73    /// Create configuration for CI environment (faster timeouts, no delays)
74    pub fn for_ci() -> Self {
75        Self {
76            core_banking: CoreBankingConfig {
77                latency: LatencyConfig::none(),
78                ..Default::default()
79            },
80            mapping_service: MappingServiceConfig {
81                latency: LatencyConfig::none(),
82                ..Default::default()
83            },
84            rulepack_service: RulepackServiceConfig {
85                latency: LatencyConfig::none(),
86                ..Default::default()
87            },
88            regulator_endpoint: RegulatorEndpointConfig {
89                latency: LatencyConfig::none(),
90                ..Default::default()
91            },
92        }
93    }
94
95    /// Create configuration for load testing (realistic latencies)
96    pub fn for_load_test() -> Self {
97        Self {
98            core_banking: CoreBankingConfig {
99                latency: LatencyConfig::realistic(),
100                ..Default::default()
101            },
102            mapping_service: MappingServiceConfig {
103                latency: LatencyConfig::realistic(),
104                ..Default::default()
105            },
106            rulepack_service: RulepackServiceConfig {
107                latency: LatencyConfig::realistic(),
108                ..Default::default()
109            },
110            regulator_endpoint: RegulatorEndpointConfig {
111                latency: LatencyConfig::realistic(),
112                ..Default::default()
113            },
114        }
115    }
116}
117
118/// Builder for SimulatorConfig
119#[derive(Debug, Default)]
120pub struct SimulatorConfigBuilder {
121    config: SimulatorConfig,
122}
123
124impl SimulatorConfigBuilder {
125    pub fn core_banking(mut self, config: CoreBankingConfig) -> Self {
126        self.config.core_banking = config;
127        self
128    }
129
130    pub fn mapping_service(mut self, config: MappingServiceConfig) -> Self {
131        self.config.mapping_service = config;
132        self
133    }
134
135    pub fn rulepack_service(mut self, config: RulepackServiceConfig) -> Self {
136        self.config.rulepack_service = config;
137        self
138    }
139
140    pub fn regulator_endpoint(mut self, config: RegulatorEndpointConfig) -> Self {
141        self.config.regulator_endpoint = config;
142        self
143    }
144
145    pub fn build(self) -> SimulatorConfig {
146        self.config
147    }
148}
149
150// ============================================================================
151// Core Banking Configuration
152// ============================================================================
153
154/// Configuration for Core Banking Simulator
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct CoreBankingConfig {
157    /// Whether this simulator is enabled
158    pub enabled: bool,
159    /// Port to listen on
160    pub port: u16,
161    /// Host to bind to
162    pub host: String,
163    /// Default number of records to generate
164    pub default_record_count: u32,
165    /// Maximum records per request
166    pub max_records_per_request: u32,
167    /// Dirty data ratio (0.0 - 1.0)
168    pub default_dirty_ratio: f64,
169    /// Random seed for deterministic generation (None = random)
170    pub seed: Option<u64>,
171    /// Latency configuration
172    pub latency: LatencyConfig,
173    /// Failure injection configuration
174    pub failure_injection: FailureInjectionConfig,
175    /// Data generation configuration
176    pub data_generation: DataGenerationConfig,
177}
178
179impl Default for CoreBankingConfig {
180    fn default() -> Self {
181        Self {
182            enabled: true,
183            port: 18081,
184            host: "127.0.0.1".to_string(),
185            default_record_count: 100_000,
186            max_records_per_request: 50_000_000,
187            default_dirty_ratio: 0.0,
188            seed: None,
189            latency: LatencyConfig::default(),
190            failure_injection: FailureInjectionConfig::default(),
191            data_generation: DataGenerationConfig::default(),
192        }
193    }
194}
195
196impl CoreBankingConfig {
197    pub fn socket_addr(&self) -> String {
198        format!("{}:{}", self.host, self.port)
199    }
200
201    pub fn with_dirty_ratio(mut self, ratio: f64) -> Self {
202        self.default_dirty_ratio = ratio.clamp(0.0, 1.0);
203        self
204    }
205
206    pub fn with_seed(mut self, seed: u64) -> Self {
207        self.seed = Some(seed);
208        self
209    }
210
211    pub fn with_latency(mut self, latency: LatencyConfig) -> Self {
212        self.latency = latency;
213        self
214    }
215}
216
217/// Configuration for data generation
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct DataGenerationConfig {
220    /// Error types to inject and their weights
221    pub error_types: HashMap<String, f64>,
222    /// Whether to generate realistic-looking data
223    pub realistic_data: bool,
224    /// Date range for generated data (days back from now)
225    pub date_range_days: u32,
226    /// Currency to use
227    pub currency: String,
228    /// Minimum credit amount
229    pub min_credit_amount: f64,
230    /// Maximum credit amount
231    pub max_credit_amount: f64,
232}
233
234impl Default for DataGenerationConfig {
235    fn default() -> Self {
236        let mut error_types = HashMap::new();
237        error_types.insert("invalid_nik".to_string(), 0.3);
238        error_types.insert("negative_amount".to_string(), 0.2);
239        error_types.insert("invalid_date".to_string(), 0.2);
240        error_types.insert("missing_field".to_string(), 0.15);
241        error_types.insert("invalid_currency".to_string(), 0.1);
242        error_types.insert("duplicate_record".to_string(), 0.05);
243
244        Self {
245            error_types,
246            realistic_data: true,
247            date_range_days: 365,
248            currency: "IDR".to_string(),
249            min_credit_amount: 1_000_000.0,
250            max_credit_amount: 10_000_000_000.0,
251        }
252    }
253}
254
255// ============================================================================
256// Mapping Service Configuration
257// ============================================================================
258
259/// Configuration for Mapping Service Simulator
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct MappingServiceConfig {
262    /// Whether this simulator is enabled
263    pub enabled: bool,
264    /// Port to listen on
265    pub port: u16,
266    /// Host to bind to
267    pub host: String,
268    /// Available mapping versions
269    pub available_versions: Vec<String>,
270    /// Default version to return
271    pub default_version: String,
272    /// Latency configuration
273    pub latency: LatencyConfig,
274    /// Failure injection configuration
275    pub failure_injection: FailureInjectionConfig,
276    /// Whether to cache mappings in memory
277    pub cache_enabled: bool,
278}
279
280impl Default for MappingServiceConfig {
281    fn default() -> Self {
282        Self {
283            enabled: true,
284            port: 18082,
285            host: "127.0.0.1".to_string(),
286            available_versions: vec!["v1".to_string(), "v2".to_string(), "v3".to_string()],
287            default_version: "v2".to_string(),
288            latency: LatencyConfig::default(),
289            failure_injection: FailureInjectionConfig::default(),
290            cache_enabled: true,
291        }
292    }
293}
294
295impl MappingServiceConfig {
296    pub fn socket_addr(&self) -> String {
297        format!("{}:{}", self.host, self.port)
298    }
299
300    pub fn with_versions(mut self, versions: Vec<String>) -> Self {
301        self.available_versions = versions;
302        self
303    }
304
305    pub fn with_default_version(mut self, version: &str) -> Self {
306        self.default_version = version.to_string();
307        self
308    }
309}
310
311// ============================================================================
312// Rulepack Service Configuration
313// ============================================================================
314
315/// Configuration for Rulepack Service Simulator
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct RulepackServiceConfig {
318    /// Whether this simulator is enabled
319    pub enabled: bool,
320    /// Port to listen on
321    pub port: u16,
322    /// Host to bind to
323    pub host: String,
324    /// Available rulepack versions
325    pub available_versions: Vec<String>,
326    /// Default version to return
327    pub default_version: String,
328    /// Latency configuration
329    pub latency: LatencyConfig,
330    /// Failure injection configuration
331    pub failure_injection: FailureInjectionConfig,
332    /// Whether to include cross-field rules
333    pub include_cross_field_rules: bool,
334}
335
336impl Default for RulepackServiceConfig {
337    fn default() -> Self {
338        Self {
339            enabled: true,
340            port: 18083,
341            host: "127.0.0.1".to_string(),
342            available_versions: vec!["v1".to_string(), "v2".to_string()],
343            default_version: "v1".to_string(),
344            latency: LatencyConfig::default(),
345            failure_injection: FailureInjectionConfig::default(),
346            include_cross_field_rules: true,
347        }
348    }
349}
350
351impl RulepackServiceConfig {
352    pub fn socket_addr(&self) -> String {
353        format!("{}:{}", self.host, self.port)
354    }
355
356    pub fn with_versions(mut self, versions: Vec<String>) -> Self {
357        self.available_versions = versions;
358        self
359    }
360}
361
362// ============================================================================
363// Regulator Endpoint Configuration
364// ============================================================================
365
366/// Configuration for Regulator Endpoint Simulator
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct RegulatorEndpointConfig {
369    /// Whether this simulator is enabled
370    pub enabled: bool,
371    /// Port to listen on
372    pub port: u16,
373    /// Host to bind to
374    pub host: String,
375    /// Response mode
376    pub mode: RegulatorMode,
377    /// Latency configuration
378    pub latency: LatencyConfig,
379    /// Failure injection configuration
380    pub failure_injection: FailureInjectionConfig,
381    /// Whether to enforce idempotency
382    pub enforce_idempotency: bool,
383    /// Maximum submissions to track for idempotency
384    pub max_idempotency_entries: usize,
385    /// Idempotency entry TTL in seconds
386    pub idempotency_ttl_secs: u64,
387    /// Retry-After header value in seconds (for rate limiting)
388    pub retry_after_secs: u32,
389    /// Off-peak hours configuration
390    pub off_peak_config: OffPeakConfig,
391}
392
393impl Default for RegulatorEndpointConfig {
394    fn default() -> Self {
395        Self {
396            enabled: true,
397            port: 18084,
398            host: "127.0.0.1".to_string(),
399            mode: RegulatorMode::Accept,
400            latency: LatencyConfig::default(),
401            failure_injection: FailureInjectionConfig::default(),
402            enforce_idempotency: true,
403            max_idempotency_entries: 10_000,
404            idempotency_ttl_secs: 86400, // 24 hours
405            retry_after_secs: 60,
406            off_peak_config: OffPeakConfig::default(),
407        }
408    }
409}
410
411impl RegulatorEndpointConfig {
412    pub fn socket_addr(&self) -> String {
413        format!("{}:{}", self.host, self.port)
414    }
415
416    pub fn with_mode(mut self, mode: RegulatorMode) -> Self {
417        self.mode = mode;
418        self
419    }
420
421    /// Configure for timeout simulation
422    pub fn timeout_mode(mut self, timeout_ms: u64) -> Self {
423        self.mode = RegulatorMode::Timeout {
424            delay_ms: timeout_ms,
425        };
426        self
427    }
428
429    /// Configure for rejection simulation
430    pub fn reject_mode(mut self, code: &str, message: &str) -> Self {
431        self.mode = RegulatorMode::Reject {
432            error_code: code.to_string(),
433            error_message: message.to_string(),
434        };
435        self
436    }
437
438    /// Configure for intermittent failures
439    pub fn intermittent_mode(mut self, failure_rate: f64) -> Self {
440        self.mode = RegulatorMode::Intermittent {
441            failure_rate: failure_rate.clamp(0.0, 1.0),
442        };
443        self
444    }
445}
446
447/// Regulator response modes
448#[derive(Debug, Clone, Serialize, Deserialize)]
449#[serde(tag = "type", rename_all = "snake_case")]
450pub enum RegulatorMode {
451    /// Accept all submissions
452    Accept,
453    /// Reject all submissions with specified error
454    Reject {
455        error_code: String,
456        error_message: String,
457    },
458    /// Timeout on all submissions (simulate slow response)
459    Timeout { delay_ms: u64 },
460    /// Return 503 Service Unavailable
461    ServiceUnavailable,
462    /// Return 429 Too Many Requests (rate limiting)
463    RateLimited,
464    /// Intermittent failures (random based on rate)
465    Intermittent { failure_rate: f64 },
466    /// Partial rejection (accept some records, reject others)
467    PartialReject { reject_ratio: f64 },
468    /// Queue mode (accept but return pending status)
469    Queued { queue_delay_ms: u64 },
470    /// Custom response
471    Custom {
472        status_code: u16,
473        body: String,
474        headers: HashMap<String, String>,
475    },
476}
477
478impl Default for RegulatorMode {
479    fn default() -> Self {
480        Self::Accept
481    }
482}
483
484/// Off-peak hours configuration
485#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct OffPeakConfig {
487    /// Whether off-peak restrictions are enabled
488    pub enabled: bool,
489    /// Off-peak start hour (0-23)
490    pub start_hour: u8,
491    /// Off-peak end hour (0-23)
492    pub end_hour: u8,
493    /// Timezone offset from UTC (hours)
494    pub timezone_offset: i8,
495    /// Whether to reject submissions outside off-peak hours
496    pub reject_outside_window: bool,
497}
498
499impl Default for OffPeakConfig {
500    fn default() -> Self {
501        Self {
502            enabled: false,
503            start_hour: 22,  // 10 PM
504            end_hour: 6,     // 6 AM
505            timezone_offset: 7, // WIB (UTC+7)
506            reject_outside_window: false,
507        }
508    }
509}
510
511impl OffPeakConfig {
512    /// Check if current time is within off-peak window
513    pub fn is_off_peak_now(&self) -> bool {
514        if !self.enabled {
515            return true; // If not enabled, always allow
516        }
517
518        let now = chrono::Utc::now();
519        let local_hour = ((now.time().hour() as i8 + self.timezone_offset) % 24) as u8;
520
521        if self.start_hour > self.end_hour {
522            // Window crosses midnight (e.g., 22:00 - 06:00)
523            local_hour >= self.start_hour || local_hour < self.end_hour
524        } else {
525            // Window within same day (e.g., 02:00 - 06:00)
526            local_hour >= self.start_hour && local_hour < self.end_hour
527        }
528    }
529}
530
531// ============================================================================
532// Latency Configuration
533// ============================================================================
534
535/// Latency simulation configuration
536#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct LatencyConfig {
538    /// Whether latency simulation is enabled
539    pub enabled: bool,
540    /// Base latency in milliseconds
541    pub base_ms: u64,
542    /// Random jitter range (+/- ms)
543    pub jitter_ms: u64,
544    /// Percentile latencies (p50, p90, p99)
545    pub percentiles: Option<LatencyPercentiles>,
546}
547
548impl Default for LatencyConfig {
549    fn default() -> Self {
550        Self {
551            enabled: false,
552            base_ms: 0,
553            jitter_ms: 0,
554            percentiles: None,
555        }
556    }
557}
558
559impl LatencyConfig {
560    /// No latency (for fast tests)
561    pub fn none() -> Self {
562        Self::default()
563    }
564
565    /// Minimal latency (1-5ms)
566    pub fn minimal() -> Self {
567        Self {
568            enabled: true,
569            base_ms: 1,
570            jitter_ms: 2,
571            percentiles: None,
572        }
573    }
574
575    /// Realistic latency (10-50ms with occasional spikes)
576    pub fn realistic() -> Self {
577        Self {
578            enabled: true,
579            base_ms: 20,
580            jitter_ms: 15,
581            percentiles: Some(LatencyPercentiles {
582                p50_ms: 20,
583                p90_ms: 50,
584                p99_ms: 150,
585            }),
586        }
587    }
588
589    /// High latency (for timeout testing)
590    pub fn high(base_ms: u64) -> Self {
591        Self {
592            enabled: true,
593            base_ms,
594            jitter_ms: base_ms / 10,
595            percentiles: None,
596        }
597    }
598
599    /// Calculate actual latency to apply
600    pub fn calculate_latency(&self) -> Duration {
601        if !self.enabled {
602            return Duration::ZERO;
603        }
604
605        let base = self.base_ms as f64;
606        let jitter = if self.jitter_ms > 0 {
607            let jitter_range = self.jitter_ms as f64;
608            (rand::random::<f64>() * 2.0 - 1.0) * jitter_range
609        } else {
610            0.0
611        };
612
613        // Apply percentile-based latency if configured
614        if let Some(ref percentiles) = self.percentiles {
615            let roll: f64 = rand::random();
616            let latency_ms = if roll > 0.99 {
617                percentiles.p99_ms as f64
618            } else if roll > 0.90 {
619                percentiles.p90_ms as f64
620            } else {
621                percentiles.p50_ms as f64
622            };
623            Duration::from_millis((latency_ms + jitter).max(0.0) as u64)
624        } else {
625            Duration::from_millis((base + jitter).max(0.0) as u64)
626        }
627    }
628
629    /// Apply latency (sleep for calculated duration)
630    pub async fn apply(&self) {
631        let duration = self.calculate_latency();
632        if !duration.is_zero() {
633            tokio::time::sleep(duration).await;
634        }
635    }
636}
637
638/// Latency percentiles
639#[derive(Debug, Clone, Serialize, Deserialize)]
640pub struct LatencyPercentiles {
641    pub p50_ms: u64,
642    pub p90_ms: u64,
643    pub p99_ms: u64,
644}
645
646// ============================================================================
647// Failure Injection Configuration
648// ============================================================================
649
650/// Failure injection configuration
651#[derive(Debug, Clone, Serialize, Deserialize)]
652pub struct FailureInjectionConfig {
653    /// Whether failure injection is enabled
654    pub enabled: bool,
655    /// Probability of failure (0.0 - 1.0)
656    pub failure_rate: f64,
657    /// Types of failures to inject
658    pub failure_types: Vec<FailureType>,
659}
660
661impl Default for FailureInjectionConfig {
662    fn default() -> Self {
663        Self {
664            enabled: false,
665            failure_rate: 0.0,
666            failure_types: vec![FailureType::InternalError],
667        }
668    }
669}
670
671impl FailureInjectionConfig {
672    /// No failures
673    pub fn none() -> Self {
674        Self::default()
675    }
676
677    /// Low failure rate (1%)
678    pub fn low() -> Self {
679        Self {
680            enabled: true,
681            failure_rate: 0.01,
682            failure_types: vec![FailureType::InternalError, FailureType::Timeout],
683        }
684    }
685
686    /// Medium failure rate (5%)
687    pub fn medium() -> Self {
688        Self {
689            enabled: true,
690            failure_rate: 0.05,
691            failure_types: vec![
692                FailureType::InternalError,
693                FailureType::Timeout,
694                FailureType::ServiceUnavailable,
695            ],
696        }
697    }
698
699    /// High failure rate (20%) - for chaos testing
700    pub fn high() -> Self {
701        Self {
702            enabled: true,
703            failure_rate: 0.20,
704            failure_types: vec![
705                FailureType::InternalError,
706                FailureType::Timeout,
707                FailureType::ServiceUnavailable,
708                FailureType::ConnectionReset,
709            ],
710        }
711    }
712
713    /// Check if a failure should be injected
714    pub fn should_fail(&self) -> bool {
715        self.enabled && rand::random::<f64>() < self.failure_rate
716    }
717
718    /// Get a random failure type
719    pub fn random_failure(&self) -> Option<&FailureType> {
720        if self.should_fail() && !self.failure_types.is_empty() {
721            let idx = rand::random::<usize>() % self.failure_types.len();
722            Some(&self.failure_types[idx])
723        } else {
724            None
725        }
726    }
727}
728
729/// Types of failures that can be injected
730#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
731#[serde(rename_all = "snake_case")]
732pub enum FailureType {
733    /// HTTP 500 Internal Server Error
734    InternalError,
735    /// Request timeout
736    Timeout,
737    /// HTTP 503 Service Unavailable
738    ServiceUnavailable,
739    /// Connection reset
740    ConnectionReset,
741    /// HTTP 429 Too Many Requests
742    RateLimited,
743    /// Malformed response
744    MalformedResponse,
745    /// Partial response
746    PartialResponse,
747}
748
749// ============================================================================
750// Tests
751// ============================================================================
752
753#[cfg(test)]
754mod tests {
755    use super::*;
756
757    #[test]
758    fn test_default_config() {
759        let config = SimulatorConfig::default();
760        assert!(config.core_banking.enabled);
761        assert!(config.mapping_service.enabled);
762        assert!(config.rulepack_service.enabled);
763        assert!(config.regulator_endpoint.enabled);
764    }
765
766    #[test]
767    fn test_config_builder() {
768        let config = SimulatorConfig::builder()
769            .core_banking(CoreBankingConfig {
770                port: 9001,
771                ..Default::default()
772            })
773            .build();
774
775        assert_eq!(config.core_banking.port, 9001);
776        assert_eq!(config.mapping_service.port, 18082); // Default
777    }
778
779    #[test]
780    fn test_latency_config_none() {
781        let latency = LatencyConfig::none();
782        assert!(!latency.enabled);
783        assert_eq!(latency.calculate_latency(), Duration::ZERO);
784    }
785
786    #[test]
787    fn test_latency_config_minimal() {
788        let latency = LatencyConfig::minimal();
789        assert!(latency.enabled);
790        let duration = latency.calculate_latency();
791        assert!(duration.as_millis() <= 10);
792    }
793
794    #[test]
795    fn test_failure_injection() {
796        let config = FailureInjectionConfig {
797            enabled: true,
798            failure_rate: 1.0, // Always fail
799            failure_types: vec![FailureType::InternalError],
800        };
801        assert!(config.should_fail());
802        assert_eq!(config.random_failure(), Some(&FailureType::InternalError));
803    }
804
805    #[test]
806    fn test_failure_injection_disabled() {
807        let config = FailureInjectionConfig::none();
808        assert!(!config.should_fail());
809        assert!(config.random_failure().is_none());
810    }
811
812    #[test]
813    fn test_regulator_mode_timeout() {
814        let config = RegulatorEndpointConfig::default().timeout_mode(5000);
815        match config.mode {
816            RegulatorMode::Timeout { delay_ms } => assert_eq!(delay_ms, 5000),
817            _ => panic!("Expected Timeout mode"),
818        }
819    }
820
821    #[test]
822    fn test_regulator_mode_reject() {
823        let config = RegulatorEndpointConfig::default()
824            .reject_mode("ERR001", "Invalid submission");
825        match config.mode {
826            RegulatorMode::Reject { error_code, error_message } => {
827                assert_eq!(error_code, "ERR001");
828                assert_eq!(error_message, "Invalid submission");
829            }
830            _ => panic!("Expected Reject mode"),
831        }
832    }
833
834    #[test]
835    fn test_off_peak_window() {
836        let config = OffPeakConfig {
837            enabled: true,
838            start_hour: 22,
839            end_hour: 6,
840            timezone_offset: 0,
841            reject_outside_window: false,
842        };
843        // This is a time-dependent test, just verify it doesn't panic
844        let _ = config.is_off_peak_now();
845    }
846
847    #[test]
848    fn test_socket_addr() {
849        let config = CoreBankingConfig::default();
850        assert_eq!(config.socket_addr(), "127.0.0.1:18081");
851    }
852
853    #[test]
854    fn test_dirty_ratio_clamping() {
855        let config = CoreBankingConfig::default().with_dirty_ratio(1.5);
856        assert_eq!(config.default_dirty_ratio, 1.0);
857
858        let config = CoreBankingConfig::default().with_dirty_ratio(-0.5);
859        assert_eq!(config.default_dirty_ratio, 0.0);
860    }
861
862    #[test]
863    fn test_config_for_ci() {
864        let config = SimulatorConfig::for_ci();
865        assert!(!config.core_banking.latency.enabled);
866        assert!(!config.mapping_service.latency.enabled);
867    }
868
869    #[test]
870    fn test_config_for_load_test() {
871        let config = SimulatorConfig::for_load_test();
872        assert!(config.core_banking.latency.enabled);
873        assert!(config.regulator_endpoint.latency.enabled);
874    }
875}