Skip to main content

credit_data_simulator/
lib.rs

1//! # Credit Data Simulator
2//!
3//! Multi-service credit data pipeline simulator for testing and benchmarking
4//! [VIL](https://github.com/OceanOS-id/VIL) NDJSON pipeline examples.
5//! Also usable as a standalone mock for any credit/banking data integration testing.
6//!
7//! ## Services
8//!
9//! - **CoreBankingSimulator** (:18081) — Credit records with NDJSON bulk export, pagination, dirty data ratio
10//! - **MappingServiceSimulator** (:18082) — Versioned SLIK field mapping configurations
11//! - **RulepackServiceSimulator** (:18083) — Versioned validation rules engine
12//! - **RegulatorEndpointSimulator** (:18084) — OJK regulatory submission endpoint
13//!
14//! ## Usage
15//!
16//! ```rust,ignore
17//! use credit_data_simulator::{SimulatorServer, SimulatorConfig};
18//!
19//! #[tokio::test]
20//! async fn test_with_simulators() {
21//!     let config = SimulatorConfig::default();
22//!     let server = SimulatorServer::start(config).await.unwrap();
23//!     // Run tests...
24//!     server.shutdown().await;
25//! }
26//! ```
27
28pub mod config;
29pub mod core_banking;
30pub mod engine;
31pub mod mapping_service;
32pub mod models;
33pub mod regulator_endpoint;
34pub mod rulepack_service;
35
36pub use config::*;
37pub use core_banking::CoreBankingSimulator;
38pub use engine::{EngineSimulator, AdminSimulator};
39pub use mapping_service::MappingServiceSimulator;
40pub use regulator_endpoint::RegulatorEndpointSimulator;
41pub use rulepack_service::RulepackServiceSimulator;
42
43use std::collections::HashMap;
44
45use std::sync::Arc;
46use tokio::sync::{oneshot, RwLock};
47
48// ============================================================================
49// Common Types
50// ============================================================================
51
52/// Result type for simulator operations
53pub type SimulatorResult<T> = Result<T, SimulatorError>;
54
55/// Errors that can occur in simulators
56#[derive(Debug, Clone)]
57pub enum SimulatorError {
58    /// Server failed to bind to address
59    BindError(String),
60    /// Server failed to start
61    StartError(String),
62    /// Configuration error
63    ConfigError(String),
64    /// Internal error
65    InternalError(String),
66    /// Simulator not found
67    NotFound(String),
68    /// Timeout occurred
69    Timeout,
70    /// Service unavailable (simulated)
71    ServiceUnavailable,
72}
73
74impl std::fmt::Display for SimulatorError {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        match self {
77            Self::BindError(msg) => write!(f, "Bind error: {}", msg),
78            Self::StartError(msg) => write!(f, "Start error: {}", msg),
79            Self::ConfigError(msg) => write!(f, "Config error: {}", msg),
80            Self::InternalError(msg) => write!(f, "Internal error: {}", msg),
81            Self::NotFound(msg) => write!(f, "Not found: {}", msg),
82            Self::Timeout => write!(f, "Timeout"),
83            Self::ServiceUnavailable => write!(f, "Service unavailable"),
84        }
85    }
86}
87
88impl std::error::Error for SimulatorError {}
89
90/// Health status of a simulator
91#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
92pub struct HealthStatus {
93    pub service: String,
94    pub status: String,
95    pub version: String,
96    pub uptime_secs: u64,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub details: Option<serde_json::Value>,
99}
100
101impl HealthStatus {
102    pub fn healthy(service: &str, version: &str, uptime_secs: u64) -> Self {
103        Self {
104            service: service.to_string(),
105            status: "healthy".to_string(),
106            version: version.to_string(),
107            uptime_secs,
108            details: None,
109        }
110    }
111
112    pub fn unhealthy(service: &str, reason: &str) -> Self {
113        let mut details = serde_json::Map::new();
114        details.insert(
115            "reason".to_string(),
116            serde_json::Value::String(reason.to_string()),
117        );
118        Self {
119            service: service.to_string(),
120            status: "unhealthy".to_string(),
121            version: "unknown".to_string(),
122            uptime_secs: 0,
123            details: Some(serde_json::Value::Object(details)),
124        }
125    }
126
127    pub fn with_details(mut self, key: &str, value: serde_json::Value) -> Self {
128        if self.details.is_none() {
129            self.details = Some(serde_json::Value::Object(serde_json::Map::new()));
130        }
131
132        if let Some(serde_json::Value::Object(map)) = &mut self.details {
133            map.insert(key.to_string(), value);
134        }
135        self
136    }
137}
138
139/// Statistics tracked by simulators
140#[derive(Debug, Clone, Default, serde::Deserialize)]
141pub struct SimulatorStats {
142    pub requests_total: u64,
143    pub requests_success: u64,
144    pub requests_failed: u64,
145    pub requests_timeout: u64,
146    pub bytes_sent: u64,
147    pub bytes_received: u64,
148    pub total_latency_ms: f64,
149    #[serde(skip_serializing_if = "HashMap::is_empty")]
150    pub endpoint_counts: HashMap<String, u64>,
151}
152
153impl serde::Serialize for SimulatorStats {
154    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
155    where
156        S: serde::Serializer,
157    {
158        use serde::ser::SerializeStruct;
159        let mut state = serializer.serialize_struct("SimulatorStats", 8)?;
160        state.serialize_field("requests_total", &self.requests_total)?;
161        state.serialize_field("requests_success", &self.requests_success)?;
162        state.serialize_field("requests_failed", &self.requests_failed)?;
163        state.serialize_field("requests_timeout", &self.requests_timeout)?;
164        state.serialize_field("bytes_sent", &self.bytes_sent)?;
165        state.serialize_field("bytes_received", &self.bytes_received)?;
166        state.serialize_field("avg_latency_ms", &self.avg_latency_ms())?;
167        if !self.endpoint_counts.is_empty() {
168            state.serialize_field("endpoint_counts", &self.endpoint_counts)?;
169        }
170        state.end()
171    }
172}
173
174impl SimulatorStats {
175    pub fn avg_latency_ms(&self) -> f64 {
176        if self.requests_total > 0 {
177            self.total_latency_ms / self.requests_total as f64
178        } else {
179            0.0
180        }
181    }
182
183    pub fn record_request(&mut self, endpoint: &str, success: bool, latency_ms: f64) {
184        self.requests_total += 1;
185        if success {
186            self.requests_success += 1;
187        } else {
188            self.requests_failed += 1;
189        }
190
191        self.total_latency_ms += latency_ms;
192
193        *self.endpoint_counts.entry(endpoint.to_string()).or_insert(0) += 1;
194    }
195
196    pub fn record_timeout(&mut self) {
197        self.requests_total += 1;
198        self.requests_timeout += 1;
199    }
200
201    pub fn record_bytes(&mut self, sent: u64, received: u64) {
202        self.bytes_sent += sent;
203        self.bytes_received += received;
204    }
205}
206
207// ============================================================================
208// Simulator Trait
209// ============================================================================
210
211/// Trait for all simulators
212#[async_trait::async_trait]
213pub trait Simulator: Send + Sync {
214    /// Get the name of this simulator
215    fn name(&self) -> &str;
216
217    /// Get the port this simulator is running on
218    fn port(&self) -> u16;
219
220    /// Get the base URL for this simulator
221    fn base_url(&self) -> String {
222        format!("http://127.0.0.1:{}", self.port())
223    }
224
225    /// Get health status
226    async fn health(&self) -> HealthStatus;
227
228    /// Get statistics
229    async fn stats(&self) -> SimulatorStats;
230
231    /// Reset statistics
232    async fn reset_stats(&self);
233
234    /// Check if the simulator is ready to accept requests
235    async fn is_ready(&self) -> bool {
236        self.health().await.status == "healthy"
237    }
238}
239
240// ============================================================================
241// Simulator Server (Unified)
242// ============================================================================
243
244/// Manages all simulators as a unified server
245pub struct SimulatorServer {
246    config: SimulatorConfig,
247    core_banking: Option<Arc<CoreBankingSimulator>>,
248    mapping_service: Option<Arc<MappingServiceSimulator>>,
249    rulepack_service: Option<Arc<RulepackServiceSimulator>>,
250    regulator_endpoint: Option<Arc<RegulatorEndpointSimulator>>,
251    shutdown_txs: Vec<oneshot::Sender<()>>,
252    started_at: std::time::Instant,
253}
254
255impl SimulatorServer {
256    /// Create a new simulator server with the given configuration
257    pub fn new(config: SimulatorConfig) -> Self {
258        Self {
259            config,
260            core_banking: None,
261            mapping_service: None,
262            rulepack_service: None,
263            regulator_endpoint: None,
264            shutdown_txs: Vec::new(),
265            started_at: std::time::Instant::now(),
266        }
267    }
268
269    /// Start all configured simulators
270    pub async fn start(config: SimulatorConfig) -> SimulatorResult<Self> {
271        let mut server = Self::new(config.clone());
272
273        // Start Core Banking Simulator
274        if config.core_banking.enabled {
275            let simulator = CoreBankingSimulator::new(config.core_banking.clone());
276            let (shutdown_tx, shutdown_rx) = oneshot::channel();
277            let sim = Arc::new(simulator);
278            let sim_clone = sim.clone();
279
280            tokio::spawn(async move {
281                if let Err(e) = sim_clone.run(shutdown_rx).await {
282                    tracing::error!("Core Banking Simulator error: {}", e);
283                }
284            });
285
286            server.core_banking = Some(sim);
287            server.shutdown_txs.push(shutdown_tx);
288            tracing::info!(
289                "Core Banking Simulator started on port {}",
290                config.core_banking.port
291            );
292        }
293
294        // Start Mapping Service Simulator
295        if config.mapping_service.enabled {
296            let simulator = MappingServiceSimulator::new(config.mapping_service.clone());
297            let (shutdown_tx, shutdown_rx) = oneshot::channel();
298            let sim = Arc::new(simulator);
299            let sim_clone = sim.clone();
300
301            tokio::spawn(async move {
302                if let Err(e) = sim_clone.run(shutdown_rx).await {
303                    tracing::error!("Mapping Service Simulator error: {}", e);
304                }
305            });
306
307            server.mapping_service = Some(sim);
308            server.shutdown_txs.push(shutdown_tx);
309            tracing::info!(
310                "Mapping Service Simulator started on port {}",
311                config.mapping_service.port
312            );
313        }
314
315        // Start Rulepack Service Simulator
316        if config.rulepack_service.enabled {
317            let simulator = RulepackServiceSimulator::new(config.rulepack_service.clone());
318            let (shutdown_tx, shutdown_rx) = oneshot::channel();
319            let sim = Arc::new(simulator);
320            let sim_clone = sim.clone();
321
322            tokio::spawn(async move {
323                if let Err(e) = sim_clone.run(shutdown_rx).await {
324                    tracing::error!("Rulepack Service Simulator error: {}", e);
325                }
326            });
327
328            server.rulepack_service = Some(sim);
329            server.shutdown_txs.push(shutdown_tx);
330            tracing::info!(
331                "Rulepack Service Simulator started on port {}",
332                config.rulepack_service.port
333            );
334        }
335
336        // Start Regulator Endpoint Simulator
337        if config.regulator_endpoint.enabled {
338            let simulator = RegulatorEndpointSimulator::new(config.regulator_endpoint.clone());
339            let (shutdown_tx, shutdown_rx) = oneshot::channel();
340            let sim = Arc::new(simulator);
341            let sim_clone = sim.clone();
342
343            tokio::spawn(async move {
344                if let Err(e) = sim_clone.run(shutdown_rx).await {
345                    tracing::error!("Regulator Endpoint Simulator error: {}", e);
346                }
347            });
348
349            server.regulator_endpoint = Some(sim);
350            server.shutdown_txs.push(shutdown_tx);
351            tracing::info!(
352                "Regulator Endpoint Simulator started on port {}",
353                config.regulator_endpoint.port
354            );
355        }
356
357        // Wait for all simulators to be ready
358        server.wait_for_ready(std::time::Duration::from_secs(10)).await?;
359
360        Ok(server)
361    }
362
363    /// Wait for all simulators to be ready
364    pub async fn wait_for_ready(&self, timeout: std::time::Duration) -> SimulatorResult<()> {
365        let start = std::time::Instant::now();
366
367        loop {
368            if start.elapsed() > timeout {
369                return Err(SimulatorError::Timeout);
370            }
371
372            let mut all_ready = true;
373
374            if let Some(ref sim) = self.core_banking {
375                if !sim.is_ready().await {
376                    all_ready = false;
377                }
378            }
379            if let Some(ref sim) = self.mapping_service {
380                if !sim.is_ready().await {
381                    all_ready = false;
382                }
383            }
384            if let Some(ref sim) = self.rulepack_service {
385                if !sim.is_ready().await {
386                    all_ready = false;
387                }
388            }
389            if let Some(ref sim) = self.regulator_endpoint {
390                if !sim.is_ready().await {
391                    all_ready = false;
392                }
393            }
394
395            if all_ready {
396                return Ok(());
397            }
398
399            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
400        }
401    }
402
403    /// Shutdown all simulators
404    pub async fn shutdown(self) {
405        for tx in self.shutdown_txs {
406            let _ = tx.send(());
407        }
408        tracing::info!("All simulators shut down");
409    }
410
411    /// Get the Core Banking simulator
412    pub fn core_banking(&self) -> Option<&Arc<CoreBankingSimulator>> {
413        self.core_banking.as_ref()
414    }
415
416    /// Get the Mapping Service simulator
417    pub fn mapping_service(&self) -> Option<&Arc<MappingServiceSimulator>> {
418        self.mapping_service.as_ref()
419    }
420
421    /// Get the Rulepack Service simulator
422    pub fn rulepack_service(&self) -> Option<&Arc<RulepackServiceSimulator>> {
423        self.rulepack_service.as_ref()
424    }
425
426    /// Get the Regulator Endpoint simulator
427    pub fn regulator_endpoint(&self) -> Option<&Arc<RegulatorEndpointSimulator>> {
428        self.regulator_endpoint.as_ref()
429    }
430
431    /// Get uptime in seconds
432    pub fn uptime_secs(&self) -> u64 {
433        self.started_at.elapsed().as_secs()
434    }
435
436    /// Get combined statistics from all simulators
437    pub async fn combined_stats(&self) -> HashMap<String, SimulatorStats> {
438        let mut stats = HashMap::new();
439
440        if let Some(ref sim) = self.core_banking {
441            stats.insert(sim.name().to_string(), sim.stats().await);
442        }
443        if let Some(ref sim) = self.mapping_service {
444            stats.insert(sim.name().to_string(), sim.stats().await);
445        }
446        if let Some(ref sim) = self.rulepack_service {
447            stats.insert(sim.name().to_string(), sim.stats().await);
448        }
449        if let Some(ref sim) = self.regulator_endpoint {
450            stats.insert(sim.name().to_string(), sim.stats().await);
451        }
452
453        stats
454    }
455
456    /// Reset all statistics
457    pub async fn reset_all_stats(&self) {
458        if let Some(ref sim) = self.core_banking {
459            sim.reset_stats().await;
460        }
461        if let Some(ref sim) = self.mapping_service {
462            sim.reset_stats().await;
463        }
464        if let Some(ref sim) = self.rulepack_service {
465            sim.reset_stats().await;
466        }
467        if let Some(ref sim) = self.regulator_endpoint {
468            sim.reset_stats().await;
469        }
470    }
471
472    /// Get endpoint URLs for all simulators
473    pub fn endpoints(&self) -> SimulatorEndpoints {
474        SimulatorEndpoints {
475            core_banking: self.core_banking.as_ref().map(|s| s.base_url()),
476            mapping_service: self.mapping_service.as_ref().map(|s| s.base_url()),
477            rulepack_service: self.rulepack_service.as_ref().map(|s| s.base_url()),
478            regulator_endpoint: self.regulator_endpoint.as_ref().map(|s| s.base_url()),
479        }
480    }
481}
482
483/// Endpoint URLs for all simulators
484#[derive(Debug, Clone, serde::Serialize)]
485pub struct SimulatorEndpoints {
486    pub core_banking: Option<String>,
487    pub mapping_service: Option<String>,
488    pub rulepack_service: Option<String>,
489    pub regulator_endpoint: Option<String>,
490}
491
492// ============================================================================
493// Test Utilities
494// ============================================================================
495
496/// Quick start helper for tests - starts all simulators with default config
497pub async fn start_all_simulators() -> SimulatorResult<SimulatorServer> {
498    let config = SimulatorConfig::default();
499    SimulatorServer::start(config).await
500}
501
502/// Quick start helper for tests - starts simulators with custom ports
503pub async fn start_simulators_on_ports(
504    core_banking_port: u16,
505    mapping_port: u16,
506    rulepack_port: u16,
507    regulator_port: u16,
508) -> SimulatorResult<SimulatorServer> {
509    let mut config = SimulatorConfig::default();
510    config.core_banking.port = core_banking_port;
511    config.mapping_service.port = mapping_port;
512    config.rulepack_service.port = rulepack_port;
513    config.regulator_endpoint.port = regulator_port;
514    SimulatorServer::start(config).await
515}
516
517/// Find available ports for simulators
518pub async fn find_available_ports(count: usize) -> Vec<u16> {
519    let mut ports = Vec::with_capacity(count);
520    for _ in 0..count {
521        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
522        let port = listener.local_addr().unwrap().port();
523        ports.push(port);
524        drop(listener);
525    }
526    ports
527}
528
529// ============================================================================
530// Shared State Types
531// ============================================================================
532
533/// Thread-safe wrapper for simulator state
534pub type SharedState<T> = Arc<RwLock<T>>;
535
536/// Create a new shared state
537pub fn shared_state<T>(value: T) -> SharedState<T> {
538    Arc::new(RwLock::new(value))
539}
540
541// ============================================================================
542// HTTP Response Helpers
543// ============================================================================
544
545/// Standard JSON response wrapper
546#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
547pub struct ApiResponse<T> {
548    pub success: bool,
549    #[serde(skip_serializing_if = "Option::is_none")]
550    pub data: Option<T>,
551    #[serde(skip_serializing_if = "Option::is_none")]
552    pub error: Option<ApiError>,
553    #[serde(skip_serializing_if = "Option::is_none")]
554    pub meta: Option<ResponseMeta>,
555}
556
557impl<T> ApiResponse<T> {
558    pub fn success(data: T) -> Self {
559        Self {
560            success: true,
561            data: Some(data),
562            error: None,
563            meta: None,
564        }
565    }
566
567    pub fn success_with_meta(data: T, meta: ResponseMeta) -> Self {
568        Self {
569            success: true,
570            data: Some(data),
571            error: None,
572            meta: Some(meta),
573        }
574    }
575
576    pub fn error(code: &str, message: &str) -> ApiResponse<T> {
577        ApiResponse {
578            success: false,
579            data: None,
580            error: Some(ApiError {
581                code: code.to_string(),
582                message: message.to_string(),
583                details: None,
584            }),
585            meta: None,
586        }
587    }
588}
589
590/// API error details
591#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
592pub struct ApiError {
593    pub code: String,
594    pub message: String,
595    #[serde(skip_serializing_if = "Option::is_none")]
596    pub details: Option<serde_json::Value>,
597}
598
599/// Response metadata for pagination, timing, etc.
600#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
601pub struct ResponseMeta {
602    #[serde(skip_serializing_if = "Option::is_none")]
603    pub page: Option<u32>,
604    #[serde(skip_serializing_if = "Option::is_none")]
605    pub page_size: Option<u32>,
606    #[serde(skip_serializing_if = "Option::is_none")]
607    pub total_count: Option<u64>,
608    #[serde(skip_serializing_if = "Option::is_none")]
609    pub total_pages: Option<u32>,
610    #[serde(skip_serializing_if = "Option::is_none")]
611    pub processing_time_ms: Option<u64>,
612    #[serde(flatten, skip_serializing_if = "Option::is_none")]
613    pub extra: Option<HashMap<String, serde_json::Value>>,
614}
615
616impl ResponseMeta {
617    pub fn paginated(page: u32, page_size: u32, total_count: u64) -> Self {
618        let total_pages = if page_size == 0 {
619            0
620        } else {
621            ((total_count + page_size as u64 - 1) / page_size as u64) as u32
622        };
623        Self {
624            page: Some(page),
625            page_size: Some(page_size),
626            total_count: Some(total_count),
627            total_pages: Some(total_pages),
628            processing_time_ms: None,
629            extra: None,
630        }
631    }
632
633    pub fn with_timing(mut self, ms: u64) -> Self {
634        self.processing_time_ms = Some(ms);
635        self
636    }
637
638    pub fn with_extra(mut self, key: &str, value: serde_json::Value) -> Self {
639        if self.extra.is_none() {
640            self.extra = Some(HashMap::new());
641        }
642        if let Some(map) = &mut self.extra {
643            map.insert(key.to_string(), value);
644        }
645        self
646    }
647}
648
649// ============================================================================
650// Tests
651// ============================================================================
652
653#[cfg(test)]
654mod tests {
655    use super::*;
656
657    #[test]
658    fn test_health_status_healthy() {
659        let status = HealthStatus::healthy("test-service", "1.0.0", 100);
660        assert_eq!(status.status, "healthy");
661        assert_eq!(status.service, "test-service");
662        assert_eq!(status.version, "1.0.0");
663        assert_eq!(status.uptime_secs, 100);
664    }
665
666    #[test]
667    fn test_health_status_unhealthy() {
668        let status = HealthStatus::unhealthy("test-service", "connection failed");
669        assert_eq!(status.status, "unhealthy");
670        assert!(status.details.is_some());
671    }
672
673    #[test]
674    fn test_simulator_stats_record() {
675        let mut stats = SimulatorStats::default();
676        stats.record_request("/api/test", true, 10.0);
677        stats.record_request("/api/test", true, 20.0);
678        stats.record_request("/api/other", false, 30.0);
679
680        assert_eq!(stats.requests_total, 3);
681        assert_eq!(stats.requests_success, 2);
682        assert_eq!(stats.requests_failed, 1);
683        assert_eq!(stats.endpoint_counts.get("/api/test"), Some(&2));
684        assert_eq!(stats.endpoint_counts.get("/api/other"), Some(&1));
685    }
686
687    #[test]
688    fn test_api_response_success() {
689        let response: ApiResponse<String> = ApiResponse::success("test data".to_string());
690        assert!(response.success);
691        assert_eq!(response.data, Some("test data".to_string()));
692        assert!(response.error.is_none());
693    }
694
695    #[test]
696    fn test_api_response_error() {
697        let response: ApiResponse<String> = ApiResponse::error("ERR001", "Something went wrong");
698        assert!(!response.success);
699        assert!(response.data.is_none());
700        assert!(response.error.is_some());
701        assert_eq!(response.error.as_ref().unwrap().code, "ERR001");
702    }
703
704    #[test]
705    fn test_response_meta_paginated() {
706        let meta = ResponseMeta::paginated(1, 10, 95);
707        assert_eq!(meta.page, Some(1));
708        assert_eq!(meta.page_size, Some(10));
709        assert_eq!(meta.total_count, Some(95));
710        assert_eq!(meta.total_pages, Some(10)); // ceil(95/10) = 10
711    }
712}