1use chrono::Timelike;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::time::Duration;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SimulatorConfig {
18 pub core_banking: CoreBankingConfig,
20 pub mapping_service: MappingServiceConfig,
22 pub rulepack_service: RulepackServiceConfig,
24 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 pub fn builder() -> SimulatorConfigBuilder {
42 SimulatorConfigBuilder::default()
43 }
44
45 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 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 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct CoreBankingConfig {
157 pub enabled: bool,
159 pub port: u16,
161 pub host: String,
163 pub default_record_count: u32,
165 pub max_records_per_request: u32,
167 pub default_dirty_ratio: f64,
169 pub seed: Option<u64>,
171 pub latency: LatencyConfig,
173 pub failure_injection: FailureInjectionConfig,
175 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#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct DataGenerationConfig {
220 pub error_types: HashMap<String, f64>,
222 pub realistic_data: bool,
224 pub date_range_days: u32,
226 pub currency: String,
228 pub min_credit_amount: f64,
230 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#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct MappingServiceConfig {
262 pub enabled: bool,
264 pub port: u16,
266 pub host: String,
268 pub available_versions: Vec<String>,
270 pub default_version: String,
272 pub latency: LatencyConfig,
274 pub failure_injection: FailureInjectionConfig,
276 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#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct RulepackServiceConfig {
318 pub enabled: bool,
320 pub port: u16,
322 pub host: String,
324 pub available_versions: Vec<String>,
326 pub default_version: String,
328 pub latency: LatencyConfig,
330 pub failure_injection: FailureInjectionConfig,
332 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#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct RegulatorEndpointConfig {
369 pub enabled: bool,
371 pub port: u16,
373 pub host: String,
375 pub mode: RegulatorMode,
377 pub latency: LatencyConfig,
379 pub failure_injection: FailureInjectionConfig,
381 pub enforce_idempotency: bool,
383 pub max_idempotency_entries: usize,
385 pub idempotency_ttl_secs: u64,
387 pub retry_after_secs: u32,
389 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, 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
449#[serde(tag = "type", rename_all = "snake_case")]
450pub enum RegulatorMode {
451 Accept,
453 Reject {
455 error_code: String,
456 error_message: String,
457 },
458 Timeout { delay_ms: u64 },
460 ServiceUnavailable,
462 RateLimited,
464 Intermittent { failure_rate: f64 },
466 PartialReject { reject_ratio: f64 },
468 Queued { queue_delay_ms: u64 },
470 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#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct OffPeakConfig {
487 pub enabled: bool,
489 pub start_hour: u8,
491 pub end_hour: u8,
493 pub timezone_offset: i8,
495 pub reject_outside_window: bool,
497}
498
499impl Default for OffPeakConfig {
500 fn default() -> Self {
501 Self {
502 enabled: false,
503 start_hour: 22, end_hour: 6, timezone_offset: 7, reject_outside_window: false,
507 }
508 }
509}
510
511impl OffPeakConfig {
512 pub fn is_off_peak_now(&self) -> bool {
514 if !self.enabled {
515 return true; }
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 local_hour >= self.start_hour || local_hour < self.end_hour
524 } else {
525 local_hour >= self.start_hour && local_hour < self.end_hour
527 }
528 }
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct LatencyConfig {
538 pub enabled: bool,
540 pub base_ms: u64,
542 pub jitter_ms: u64,
544 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 pub fn none() -> Self {
562 Self::default()
563 }
564
565 pub fn minimal() -> Self {
567 Self {
568 enabled: true,
569 base_ms: 1,
570 jitter_ms: 2,
571 percentiles: None,
572 }
573 }
574
575 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 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 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 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 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
652pub struct FailureInjectionConfig {
653 pub enabled: bool,
655 pub failure_rate: f64,
657 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 pub fn none() -> Self {
674 Self::default()
675 }
676
677 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 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 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 pub fn should_fail(&self) -> bool {
715 self.enabled && rand::random::<f64>() < self.failure_rate
716 }
717
718 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
731#[serde(rename_all = "snake_case")]
732pub enum FailureType {
733 InternalError,
735 Timeout,
737 ServiceUnavailable,
739 ConnectionReset,
741 RateLimited,
743 MalformedResponse,
745 PartialResponse,
747}
748
749#[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); }
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, 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 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}