Skip to main content

credit_data_simulator/
mapping_service.rs

1//! # Mapping Service Simulator
2//!
3//! Simulates a Mapping Service that provides versioned field mapping configurations
4//! for transforming source data to SLIK format.
5//!
6//! ## Endpoints
7//!
8//! - `GET /health` - Health check
9//! - `GET /api/v1/mappings` - List available mapping versions
10//! - `GET /api/v1/mappings/:version` - Get mapping configuration by version
11//! - `GET /api/v1/mappings/:version/fields` - Get field mappings for a version
12//! - `GET /api/v1/mappings/:version/validate` - Validate a mapping version exists
13//! - `POST /api/v1/mappings` - Create a new mapping version (for testing)
14//! - `GET /api/v1/stats` - Get simulator statistics
15//! - `POST /api/v1/reset` - Reset statistics
16
17//! # Mapping Service Simulator
18//!
19//! Simulates the service responsible for mapping source data (e.g. Core Banking)
20//! to the target schema (e.g. SLIK/OJK format).
21//!
22//! ## Business Purpose
23//!
24//! The Mapping Service creates an abstraction layer between internal data structures
25//! and external regulatory reporting requirements. It allows the system to:
26//! 1. Transform data fields (rename, format, calculate).
27//! 2. Apply default values for missing data.
28//! 3. Validate data integrity before processing.
29//! 4. Handle multiple versions of mapping configurations (e.g., for different regulation periods).
30//!
31//! ## Key Features
32//!
33//! - **Versioning**: Supports multiple active mapping versions (v1, v2, v3).
34//! - **Transformation Types**:
35//!   - `Direct`: Copy value as-is.
36//!   - `Computed`: Calculate value using an expression.
37//!   - `Concat`: Combine multiple fields.
38//!   - `Lookup`: Map values using a reference table.
39//!   - `Constant`: Use a fixed value.
40//! - **Validation**:
41//!   - Required fields.
42//!   - Length constraints.
43//!   - Pattern matching (Regex).
44//!   - Range checks (Min/Max).
45//!
46//! ## Input/Output
47//!
48//! - **Input**: Source JSON record (e.g., CreditRecord from Core Banking).
49//! - **Output**: Mapped/Transformed JSON record ready for Rulepack validation.
50//!
51//! ## API Endpoints
52//!
53//! - `GET /api/v1/mappings` - List all available mapping configurations.
54//! - `GET /api/v1/mappings/:version` - Get a specific mapping configuration.
55//! - `GET /api/v1/mappings/:version/fields` - Get just the field definitions.
56//! - `POST /api/v1/mappings/:version/validate` - Validate a record against the mapping schema.
57
58use crate::{
59    config::{FailureType, MappingServiceConfig},
60    ApiResponse, HealthStatus, ResponseMeta, SharedState, SimulatorError, SimulatorResult,
61    SimulatorStats, Simulator, shared_state,
62};
63use axum::{
64    extract::{Path, Query, State},
65    http::StatusCode,
66    response::{IntoResponse, Json},
67    routing::{get, post},
68    Router,
69};
70use serde::{Deserialize, Serialize};
71use std::collections::HashMap;
72use std::time::Instant;
73use tokio::sync::oneshot;
74
75// ============================================================================
76// Data Models
77// ============================================================================
78
79/// Mapping configuration for a specific version
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct MappingConfig {
82    /// Version identifier (e.g., "v1", "v2")
83    pub version: String,
84    /// Display name
85    pub name: String,
86    /// Description
87    pub description: String,
88    /// Target schema version
89    pub target_schema: String,
90    /// Field mappings
91    pub field_mappings: Vec<FieldMapping>,
92    /// Transformation rules
93    pub transformations: Vec<TransformationRule>,
94    /// Validation rules for the mapping
95    pub validation_rules: Vec<MappingValidation>,
96    /// Creation timestamp
97    pub created_at: String,
98    /// Last update timestamp
99    pub updated_at: String,
100    /// Whether this mapping is active
101    pub is_active: bool,
102    /// Metadata
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub metadata: Option<HashMap<String, serde_json::Value>>,
105}
106
107impl MappingConfig {
108    /// Create v1 mapping configuration (basic mapping)
109    pub fn v1() -> Self {
110        Self {
111            version: "v1".to_string(),
112            name: "SLIK Basic Mapping v1".to_string(),
113            description: "Basic field mapping for SLIK credit reporting".to_string(),
114            target_schema: "SLIK-2023".to_string(),
115            field_mappings: vec![
116                FieldMapping::direct("id", "credit_id"),
117                FieldMapping::direct("nik", "debtor_nik"),
118                FieldMapping::direct("nama_lengkap", "debtor_name"),
119                FieldMapping::direct("jenis_fasilitas", "facility_type"),
120                FieldMapping::direct("jumlah_kredit", "credit_amount"),
121                FieldMapping::direct("mata_uang", "currency"),
122                FieldMapping::direct("suku_bunga", "interest_rate"),
123                FieldMapping::direct("tanggal_mulai", "start_date"),
124                FieldMapping::direct("tanggal_jatuh_tempo", "maturity_date"),
125                FieldMapping::direct("saldo_outstanding", "outstanding_balance"),
126                FieldMapping::direct("kolektabilitas", "collectability"),
127                FieldMapping::direct("kode_cabang", "branch_code"),
128                FieldMapping::direct("account_officer", "officer_id"),
129            ],
130            transformations: vec![
131                TransformationRule::new("uppercase", "debtor_name", "UPPER(value)"),
132                TransformationRule::new("date_format", "start_date", "FORMAT(value, 'YYYYMMDD')"),
133                TransformationRule::new("date_format", "maturity_date", "FORMAT(value, 'YYYYMMDD')"),
134            ],
135            validation_rules: vec![
136                MappingValidation::required("debtor_nik"),
137                MappingValidation::required("debtor_name"),
138                MappingValidation::required("credit_amount"),
139            ],
140            created_at: "2023-01-15T00:00:00Z".to_string(),
141            updated_at: "2023-06-20T00:00:00Z".to_string(),
142            is_active: true,
143            metadata: None,
144        }
145    }
146
147    /// Create v2 mapping configuration (enhanced with additional fields)
148    pub fn v2() -> Self {
149        let mut mapping = Self::v1();
150        mapping.version = "v2".to_string();
151        mapping.name = "SLIK Enhanced Mapping v2".to_string();
152        mapping.description = "Enhanced field mapping with additional fields and transformations".to_string();
153        mapping.target_schema = "SLIK-2024".to_string();
154
155        // Add additional field mappings
156        mapping.field_mappings.extend(vec![
157            FieldMapping::direct("last_updated", "last_update_timestamp"),
158            FieldMapping::computed("risk_category", "CASE WHEN kolektabilitas <= 2 THEN 'LOW' WHEN kolektabilitas <= 3 THEN 'MEDIUM' ELSE 'HIGH' END"),
159            FieldMapping::computed("is_performing", "kolektabilitas <= 2"),
160            FieldMapping::with_default("reporting_period", "period", "CURRENT_MONTH"),
161        ]);
162
163        // Add additional transformations
164        mapping.transformations.extend(vec![
165            TransformationRule::new("trim", "debtor_nik", "TRIM(value)"),
166            TransformationRule::new("numeric_format", "credit_amount", "FORMAT(value, '0.00')"),
167            TransformationRule::new("numeric_format", "outstanding_balance", "FORMAT(value, '0.00')"),
168        ]);
169
170        // Add additional validations
171        mapping.validation_rules.extend(vec![
172            MappingValidation::required("facility_type"),
173            MappingValidation::required("currency"),
174            MappingValidation::range("collectability", 1, 5),
175        ]);
176
177        mapping.created_at = "2024-01-10T00:00:00Z".to_string();
178        mapping.updated_at = "2024-03-15T00:00:00Z".to_string();
179
180        mapping
181    }
182
183    /// Create v3 mapping configuration (with regulatory compliance fields)
184    pub fn v3() -> Self {
185        let mut mapping = Self::v2();
186        mapping.version = "v3".to_string();
187        mapping.name = "SLIK Regulatory Mapping v3".to_string();
188        mapping.description = "Regulatory compliance mapping with OJK required fields".to_string();
189        mapping.target_schema = "SLIK-2024-REG".to_string();
190
191        // Add regulatory compliance fields
192        mapping.field_mappings.extend(vec![
193            FieldMapping::computed("reporting_bank_code", "'BANKXYZ'"),
194            FieldMapping::computed("reporting_timestamp", "NOW()"),
195            FieldMapping::computed("record_hash", "SHA256(CONCAT(credit_id, debtor_nik, credit_amount))"),
196            FieldMapping::with_default("restructured_flag", "is_restructured", "N"),
197            FieldMapping::with_default("write_off_flag", "is_write_off", "N"),
198        ]);
199
200        // Add regulatory transformations
201        mapping.transformations.extend(vec![
202            TransformationRule::new("pad_left", "debtor_nik", "LPAD(value, 16, '0')"),
203            TransformationRule::new("currency_normalize", "currency", "IF(value='IDR', 'IDR', 'FCY')"),
204        ]);
205
206        // Add regulatory validations
207        mapping.validation_rules.extend(vec![
208            MappingValidation::length("debtor_nik", 16, 16),
209            MappingValidation::pattern("branch_code", r"^[A-Z]{3}\d{3}$"),
210        ]);
211
212        mapping.created_at = "2024-06-01T00:00:00Z".to_string();
213        mapping.updated_at = "2024-06-01T00:00:00Z".to_string();
214
215        mapping
216    }
217}
218
219/// Individual field mapping
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct FieldMapping {
222    /// Source field name
223    pub source_field: String,
224    /// Target field name
225    pub target_field: String,
226    /// Mapping type
227    pub mapping_type: FieldMappingType,
228    /// Expression for computed fields
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub expression: Option<String>,
231    /// Default value if source is null/missing
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub default_value: Option<String>,
234    /// Whether this field is required
235    pub required: bool,
236    /// Field description
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub description: Option<String>,
239}
240
241impl FieldMapping {
242    /// Create a direct field mapping (1:1)
243    pub fn direct(source: &str, target: &str) -> Self {
244        Self {
245            source_field: source.to_string(),
246            target_field: target.to_string(),
247            mapping_type: FieldMappingType::Direct,
248            expression: None,
249            default_value: None,
250            required: false,
251            description: None,
252        }
253    }
254
255    /// Create a computed field mapping
256    pub fn computed(target: &str, expression: &str) -> Self {
257        Self {
258            source_field: "".to_string(),
259            target_field: target.to_string(),
260            mapping_type: FieldMappingType::Computed,
261            expression: Some(expression.to_string()),
262            default_value: None,
263            required: false,
264            description: None,
265        }
266    }
267
268    /// Create a field mapping with default value
269    pub fn with_default(source: &str, target: &str, default: &str) -> Self {
270        Self {
271            source_field: source.to_string(),
272            target_field: target.to_string(),
273            mapping_type: FieldMappingType::Direct,
274            expression: None,
275            default_value: Some(default.to_string()),
276            required: false,
277            description: None,
278        }
279    }
280
281    /// Set the field as required
282    pub fn required(mut self) -> Self {
283        self.required = true;
284        self
285    }
286
287    /// Set the description
288    pub fn with_description(mut self, description: &str) -> Self {
289        self.description = Some(description.to_string());
290        self
291    }
292}
293
294/// Type of field mapping
295#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
296#[serde(rename_all = "snake_case")]
297pub enum FieldMappingType {
298    /// Direct 1:1 field mapping
299    Direct,
300    /// Computed field from expression
301    Computed,
302    /// Concatenation of multiple fields
303    Concat,
304    /// Lookup from reference table
305    Lookup,
306    /// Constant value
307    Constant,
308}
309
310/// Transformation rule to apply to a field
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct TransformationRule {
313    /// Rule name
314    pub name: String,
315    /// Target field to transform
316    pub target_field: String,
317    /// Transformation expression
318    pub expression: String,
319    /// Order of execution
320    pub order: u32,
321    /// Whether this transformation is enabled
322    pub enabled: bool,
323}
324
325impl TransformationRule {
326    pub fn new(name: &str, target_field: &str, expression: &str) -> Self {
327        Self {
328            name: name.to_string(),
329            target_field: target_field.to_string(),
330            expression: expression.to_string(),
331            order: 0,
332            enabled: true,
333        }
334    }
335
336    pub fn with_order(mut self, order: u32) -> Self {
337        self.order = order;
338        self
339    }
340}
341
342/// Validation rule for mapping
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct MappingValidation {
345    /// Validation type
346    pub validation_type: ValidationType,
347    /// Target field
348    pub field: String,
349    /// Validation parameters
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub params: Option<HashMap<String, serde_json::Value>>,
352    /// Error message if validation fails
353    pub error_message: String,
354}
355
356impl MappingValidation {
357    pub fn required(field: &str) -> Self {
358        Self {
359            validation_type: ValidationType::Required,
360            field: field.to_string(),
361            params: None,
362            error_message: format!("Field '{}' is required", field),
363        }
364    }
365
366    pub fn length(field: &str, min: u32, max: u32) -> Self {
367        let mut params = HashMap::new();
368        params.insert("min".to_string(), serde_json::json!(min));
369        params.insert("max".to_string(), serde_json::json!(max));
370
371        Self {
372            validation_type: ValidationType::Length,
373            field: field.to_string(),
374            params: Some(params),
375            error_message: format!("Field '{}' must be between {} and {} characters", field, min, max),
376        }
377    }
378
379    pub fn range(field: &str, min: i64, max: i64) -> Self {
380        let mut params = HashMap::new();
381        params.insert("min".to_string(), serde_json::json!(min));
382        params.insert("max".to_string(), serde_json::json!(max));
383
384        Self {
385            validation_type: ValidationType::Range,
386            field: field.to_string(),
387            params: Some(params),
388            error_message: format!("Field '{}' must be between {} and {}", field, min, max),
389        }
390    }
391
392    pub fn pattern(field: &str, pattern: &str) -> Self {
393        let mut params = HashMap::new();
394        params.insert("pattern".to_string(), serde_json::json!(pattern));
395
396        Self {
397            validation_type: ValidationType::Pattern,
398            field: field.to_string(),
399            params: Some(params),
400            error_message: format!("Field '{}' must match pattern '{}'", field, pattern),
401        }
402    }
403}
404
405/// Type of validation
406#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
407#[serde(rename_all = "snake_case")]
408pub enum ValidationType {
409    Required,
410    Length,
411    Range,
412    Pattern,
413    Custom,
414}
415
416/// Query parameters for listing mappings
417#[derive(Debug, Deserialize)]
418pub struct ListMappingsParams {
419    pub active_only: Option<bool>,
420}
421
422/// Request to create a new mapping
423#[derive(Debug, Deserialize)]
424pub struct CreateMappingRequest {
425    pub version: String,
426    pub name: String,
427    pub description: Option<String>,
428    pub field_mappings: Vec<FieldMapping>,
429}
430
431// ============================================================================
432// Simulator State
433// ============================================================================
434
435/// Internal state for Mapping Service Simulator
436pub struct MappingServiceState {
437    pub config: MappingServiceConfig,
438    pub mappings: HashMap<String, MappingConfig>,
439    pub stats: SimulatorStats,
440    pub started_at: Instant,
441    pub ready: bool,
442}
443
444impl MappingServiceState {
445    pub fn new(config: MappingServiceConfig) -> Self {
446        let mut mappings = HashMap::new();
447
448        // Pre-populate with default versions
449        if config.available_versions.contains(&"v1".to_string()) {
450            mappings.insert("v1".to_string(), MappingConfig::v1());
451        }
452        if config.available_versions.contains(&"v2".to_string()) {
453            mappings.insert("v2".to_string(), MappingConfig::v2());
454        }
455        if config.available_versions.contains(&"v3".to_string()) {
456            mappings.insert("v3".to_string(), MappingConfig::v3());
457        }
458
459        Self {
460            config,
461            mappings,
462            stats: SimulatorStats::default(),
463            started_at: Instant::now(),
464            ready: false,
465        }
466    }
467}
468
469// ============================================================================
470// Mapping Service Simulator
471// ============================================================================
472
473/// Mapping Service Simulator implementation
474pub struct MappingServiceSimulator {
475    state: SharedState<MappingServiceState>,
476    config: MappingServiceConfig,
477}
478
479impl MappingServiceSimulator {
480    /// Create a new Mapping Service Simulator
481    pub fn new(config: MappingServiceConfig) -> Self {
482        let state = shared_state(MappingServiceState::new(config.clone()));
483        Self { state, config }
484    }
485
486    /// Run the simulator HTTP server
487    pub async fn run(&self, shutdown_rx: oneshot::Receiver<()>) -> SimulatorResult<()> {
488        // Mark as ready
489        {
490            let mut state = self.state.write().await;
491            state.ready = true;
492        }
493
494        let app = self.create_router();
495        let addr: std::net::SocketAddr = self.config.socket_addr().parse()
496            .map_err(|e| SimulatorError::ConfigError(format!("Invalid address: {}", e)))?;
497
498        tracing::info!("Mapping Service Simulator listening on {}", addr);
499
500        let listener = tokio::net::TcpListener::bind(addr).await
501            .map_err(|e| SimulatorError::BindError(e.to_string()))?;
502
503        axum::serve(listener, app)
504            .with_graceful_shutdown(async {
505                let _ = shutdown_rx.await;
506                tracing::info!("Mapping Service Simulator shutting down");
507            })
508            .await
509            .map_err(|e| SimulatorError::StartError(e.to_string()))?;
510
511        Ok(())
512    }
513
514    /// Create the router with all endpoints
515    fn create_router(&self) -> Router {
516        let state = self.state.clone();
517
518        Router::new()
519            .route("/health", get(health_handler))
520            .route("/api/v1/mappings", get(list_mappings_handler))
521            .route("/api/v1/mappings", post(create_mapping_handler))
522            .route("/api/v1/mappings/:version", get(get_mapping_handler))
523            .route("/api/v1/mappings/:version/fields", get(get_fields_handler))
524            .route("/api/v1/mappings/:version/validate", get(validate_mapping_handler))
525            .route("/api/v1/stats", get(stats_handler))
526            .route("/api/v1/reset", post(reset_handler))
527            .with_state(state)
528    }
529
530    /// Get a specific mapping version
531    pub async fn get_mapping(&self, version: &str) -> Option<MappingConfig> {
532        self.state.read().await.mappings.get(version).cloned()
533    }
534
535    /// Add a custom mapping (for testing)
536    pub async fn add_mapping(&self, mapping: MappingConfig) {
537        self.state.write().await.mappings.insert(mapping.version.clone(), mapping);
538    }
539}
540
541#[async_trait::async_trait]
542impl Simulator for MappingServiceSimulator {
543    fn name(&self) -> &str {
544        "mapping-service"
545    }
546
547    fn port(&self) -> u16 {
548        self.config.port
549    }
550
551    async fn health(&self) -> HealthStatus {
552        let state = self.state.read().await;
553        let uptime = state.started_at.elapsed().as_secs();
554
555        if state.ready {
556            HealthStatus::healthy(self.name(), "1.0.0", uptime)
557                .with_details("mapping_count", serde_json::json!(state.mappings.len()))
558                .with_details("available_versions", serde_json::json!(
559                    state.mappings.keys().collect::<Vec<_>>()
560                ))
561        } else {
562            HealthStatus::unhealthy(self.name(), "Not ready")
563        }
564    }
565
566    async fn stats(&self) -> SimulatorStats {
567        self.state.read().await.stats.clone()
568    }
569
570    async fn reset_stats(&self) {
571        self.state.write().await.stats = SimulatorStats::default();
572    }
573
574    async fn is_ready(&self) -> bool {
575        self.state.read().await.ready
576    }
577}
578
579// ============================================================================
580// HTTP Handlers
581// ============================================================================
582
583/// Health check endpoint
584async fn health_handler(
585    State(state): State<SharedState<MappingServiceState>>,
586) -> impl IntoResponse {
587    let state = state.read().await;
588    let uptime = state.started_at.elapsed().as_secs();
589
590    if state.ready {
591        let health = HealthStatus::healthy("mapping-service", "1.0.0", uptime)
592            .with_details("mapping_count", serde_json::json!(state.mappings.len()));
593        (StatusCode::OK, Json(health))
594    } else {
595        let health = HealthStatus::unhealthy("mapping-service", "Not ready");
596        (StatusCode::SERVICE_UNAVAILABLE, Json(health))
597    }
598}
599
600/// List available mappings
601async fn list_mappings_handler(
602    State(state): State<SharedState<MappingServiceState>>,
603    Query(params): Query<ListMappingsParams>,
604) -> impl IntoResponse {
605    let start = Instant::now();
606    let mut state_guard = state.write().await;
607
608    // Check for failure injection
609    let failure = state_guard.config.failure_injection.random_failure().cloned();
610    if let Some(ref failure) = failure {
611        state_guard.stats.record_request("/api/v1/mappings", false, start.elapsed().as_millis() as f64);
612        return match failure {
613            FailureType::InternalError => {
614                (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<Vec<MappingSummary>>::error("ERR500", "Internal server error")))
615            }
616            FailureType::ServiceUnavailable => {
617                (StatusCode::SERVICE_UNAVAILABLE, Json(ApiResponse::<Vec<MappingSummary>>::error("ERR503", "Service unavailable")))
618            }
619            _ => {
620                (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<Vec<MappingSummary>>::error("ERR500", "Internal server error")))
621            }
622        };
623    }
624
625    // Apply latency
626    state_guard.config.latency.apply().await;
627
628    let active_only = params.active_only.unwrap_or(false);
629
630    let summaries: Vec<MappingSummary> = state_guard.mappings
631        .values()
632        .filter(|m| !active_only || m.is_active)
633        .map(|m| MappingSummary {
634            version: m.version.clone(),
635            name: m.name.clone(),
636            target_schema: m.target_schema.clone(),
637            field_count: m.field_mappings.len(),
638            is_active: m.is_active,
639            updated_at: m.updated_at.clone(),
640        })
641        .collect();
642
643    state_guard.stats.record_request("/api/v1/mappings", true, start.elapsed().as_millis() as f64);
644
645    let meta = ResponseMeta {
646        page: None,
647        page_size: None,
648        total_count: Some(summaries.len() as u64),
649        total_pages: None,
650        processing_time_ms: Some(start.elapsed().as_millis() as u64),
651        extra: None,
652    };
653
654    (StatusCode::OK, Json(ApiResponse::success_with_meta(summaries, meta)))
655}
656
657/// Get mapping by version
658async fn get_mapping_handler(
659    State(state): State<SharedState<MappingServiceState>>,
660    Path(version): Path<String>,
661) -> impl IntoResponse {
662    let start = Instant::now();
663    let mut state_guard = state.write().await;
664
665    // Check for failure injection
666    let failure = state_guard.config.failure_injection.random_failure().cloned();
667    if let Some(ref failure) = failure {
668        state_guard.stats.record_request(&format!("/api/v1/mappings/{}", version), false, start.elapsed().as_millis() as f64);
669        return match failure {
670            FailureType::InternalError => {
671                (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<MappingConfig>::error("ERR500", "Internal server error")))
672            }
673            _ => {
674                (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<MappingConfig>::error("ERR500", "Internal server error")))
675            }
676        };
677    }
678
679    // Apply latency
680    state_guard.config.latency.apply().await;
681
682    if let Some(mapping) = state_guard.mappings.get(&version).cloned() {
683        state_guard.stats.record_request(&format!("/api/v1/mappings/{}", version), true, start.elapsed().as_millis() as f64);
684        (StatusCode::OK, Json(ApiResponse::success(mapping)))
685    } else {
686        state_guard.stats.record_request(&format!("/api/v1/mappings/{}", version), false, start.elapsed().as_millis() as f64);
687        (StatusCode::NOT_FOUND, Json(ApiResponse::<MappingConfig>::error("NOT_FOUND", &format!("Mapping version '{}' not found", version))))
688    }
689}
690
691/// Get field mappings for a version
692async fn get_fields_handler(
693    State(state): State<SharedState<MappingServiceState>>,
694    Path(version): Path<String>,
695) -> impl IntoResponse {
696    let state_guard = state.read().await;
697
698    if let Some(mapping) = state_guard.mappings.get(&version) {
699        (StatusCode::OK, Json(ApiResponse::success(mapping.field_mappings.clone())))
700    } else {
701        (StatusCode::NOT_FOUND, Json(ApiResponse::<Vec<FieldMapping>>::error("NOT_FOUND", &format!("Mapping version '{}' not found", version))))
702    }
703}
704
705/// Validate a mapping version exists
706async fn validate_mapping_handler(
707    State(state): State<SharedState<MappingServiceState>>,
708    Path(version): Path<String>,
709) -> impl IntoResponse {
710    let state_guard = state.read().await;
711
712    let exists = state_guard.mappings.contains_key(&version);
713    let is_active = state_guard.mappings.get(&version).map(|m| m.is_active).unwrap_or(false);
714
715    Json(ApiResponse::success(serde_json::json!({
716        "version": version,
717        "exists": exists,
718        "is_active": is_active
719    })))
720}
721
722/// Create a new mapping (for testing)
723async fn create_mapping_handler(
724    State(state): State<SharedState<MappingServiceState>>,
725    Json(request): Json<CreateMappingRequest>,
726) -> impl IntoResponse {
727    let mut state_guard = state.write().await;
728
729    if state_guard.mappings.contains_key(&request.version) {
730        return (StatusCode::CONFLICT, Json(ApiResponse::<MappingConfig>::error("CONFLICT", &format!("Mapping version '{}' already exists", request.version))));
731    }
732
733    let mapping = MappingConfig {
734        version: request.version.clone(),
735        name: request.name,
736        description: request.description.unwrap_or_default(),
737        target_schema: "SLIK-CUSTOM".to_string(),
738        field_mappings: request.field_mappings,
739        transformations: Vec::new(),
740        validation_rules: Vec::new(),
741        created_at: chrono::Utc::now().to_rfc3339(),
742        updated_at: chrono::Utc::now().to_rfc3339(),
743        is_active: true,
744        metadata: None,
745    };
746
747    state_guard.mappings.insert(request.version, mapping.clone());
748
749    (StatusCode::CREATED, Json(ApiResponse::success(mapping)))
750}
751
752/// Get simulator statistics
753async fn stats_handler(
754    State(state): State<SharedState<MappingServiceState>>,
755) -> impl IntoResponse {
756    let state_guard = state.read().await;
757    Json(ApiResponse::success(state_guard.stats.clone()))
758}
759
760/// Reset simulator state
761async fn reset_handler(
762    State(state): State<SharedState<MappingServiceState>>,
763) -> impl IntoResponse {
764    let mut state_guard = state.write().await;
765    state_guard.stats = SimulatorStats::default();
766
767    // Re-initialize default mappings
768    state_guard.mappings.clear();
769    if state_guard.config.available_versions.contains(&"v1".to_string()) {
770        state_guard.mappings.insert("v1".to_string(), MappingConfig::v1());
771    }
772    if state_guard.config.available_versions.contains(&"v2".to_string()) {
773        state_guard.mappings.insert("v2".to_string(), MappingConfig::v2());
774    }
775    if state_guard.config.available_versions.contains(&"v3".to_string()) {
776        state_guard.mappings.insert("v3".to_string(), MappingConfig::v3());
777    }
778
779    Json(ApiResponse::success(serde_json::json!({
780        "reset": true,
781        "mapping_count": state_guard.mappings.len()
782    })))
783}
784
785/// Summary of a mapping for listing
786#[derive(Debug, Clone, Serialize, Deserialize)]
787pub struct MappingSummary {
788    pub version: String,
789    pub name: String,
790    pub target_schema: String,
791    pub field_count: usize,
792    pub is_active: bool,
793    pub updated_at: String,
794}
795
796// ============================================================================
797// Tests
798// ============================================================================
799
800#[cfg(test)]
801mod tests {
802    use super::*;
803
804    #[test]
805    fn test_mapping_config_v1() {
806        let mapping = MappingConfig::v1();
807        assert_eq!(mapping.version, "v1");
808        assert_eq!(mapping.target_schema, "SLIK-2023");
809        assert!(!mapping.field_mappings.is_empty());
810        assert!(mapping.is_active);
811    }
812
813    #[test]
814    fn test_mapping_config_v2() {
815        let mapping = MappingConfig::v2();
816        assert_eq!(mapping.version, "v2");
817        assert_eq!(mapping.target_schema, "SLIK-2024");
818        // v2 should have more fields than v1
819        assert!(mapping.field_mappings.len() > MappingConfig::v1().field_mappings.len());
820    }
821
822    #[test]
823    fn test_mapping_config_v3() {
824        let mapping = MappingConfig::v3();
825        assert_eq!(mapping.version, "v3");
826        assert_eq!(mapping.target_schema, "SLIK-2024-REG");
827        // v3 should have regulatory fields
828        assert!(mapping.field_mappings.iter().any(|f| f.target_field == "record_hash"));
829    }
830
831    #[test]
832    fn test_field_mapping_direct() {
833        let mapping = FieldMapping::direct("source", "target");
834        assert_eq!(mapping.source_field, "source");
835        assert_eq!(mapping.target_field, "target");
836        assert_eq!(mapping.mapping_type, FieldMappingType::Direct);
837        assert!(mapping.expression.is_none());
838    }
839
840    #[test]
841    fn test_field_mapping_computed() {
842        let mapping = FieldMapping::computed("target", "UPPER(value)");
843        assert_eq!(mapping.target_field, "target");
844        assert_eq!(mapping.mapping_type, FieldMappingType::Computed);
845        assert_eq!(mapping.expression, Some("UPPER(value)".to_string()));
846    }
847
848    #[test]
849    fn test_field_mapping_with_default() {
850        let mapping = FieldMapping::with_default("source", "target", "DEFAULT");
851        assert_eq!(mapping.default_value, Some("DEFAULT".to_string()));
852    }
853
854    #[test]
855    fn test_mapping_validation_required() {
856        let validation = MappingValidation::required("field_name");
857        assert_eq!(validation.validation_type, ValidationType::Required);
858        assert_eq!(validation.field, "field_name");
859    }
860
861    #[test]
862    fn test_mapping_validation_length() {
863        let validation = MappingValidation::length("nik", 16, 16);
864        assert_eq!(validation.validation_type, ValidationType::Length);
865        let params = validation.params.unwrap();
866        assert_eq!(params.get("min"), Some(&serde_json::json!(16)));
867        assert_eq!(params.get("max"), Some(&serde_json::json!(16)));
868    }
869
870    #[test]
871    fn test_mapping_validation_range() {
872        let validation = MappingValidation::range("collectability", 1, 5);
873        assert_eq!(validation.validation_type, ValidationType::Range);
874        let params = validation.params.unwrap();
875        assert_eq!(params.get("min"), Some(&serde_json::json!(1)));
876        assert_eq!(params.get("max"), Some(&serde_json::json!(5)));
877    }
878
879    #[test]
880    fn test_transformation_rule() {
881        let rule = TransformationRule::new("uppercase", "name", "UPPER(value)")
882            .with_order(1);
883        assert_eq!(rule.name, "uppercase");
884        assert_eq!(rule.target_field, "name");
885        assert_eq!(rule.order, 1);
886        assert!(rule.enabled);
887    }
888
889    #[test]
890    fn test_state_initialization() {
891        let config = MappingServiceConfig::default();
892        let state = MappingServiceState::new(config);
893
894        // Should have pre-populated mappings
895        assert!(state.mappings.contains_key("v1"));
896        assert!(state.mappings.contains_key("v2"));
897    }
898}