1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct MappingConfig {
82 pub version: String,
84 pub name: String,
86 pub description: String,
88 pub target_schema: String,
90 pub field_mappings: Vec<FieldMapping>,
92 pub transformations: Vec<TransformationRule>,
94 pub validation_rules: Vec<MappingValidation>,
96 pub created_at: String,
98 pub updated_at: String,
100 pub is_active: bool,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub metadata: Option<HashMap<String, serde_json::Value>>,
105}
106
107impl MappingConfig {
108 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct FieldMapping {
222 pub source_field: String,
224 pub target_field: String,
226 pub mapping_type: FieldMappingType,
228 #[serde(skip_serializing_if = "Option::is_none")]
230 pub expression: Option<String>,
231 #[serde(skip_serializing_if = "Option::is_none")]
233 pub default_value: Option<String>,
234 pub required: bool,
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub description: Option<String>,
239}
240
241impl FieldMapping {
242 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 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 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 pub fn required(mut self) -> Self {
283 self.required = true;
284 self
285 }
286
287 pub fn with_description(mut self, description: &str) -> Self {
289 self.description = Some(description.to_string());
290 self
291 }
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
296#[serde(rename_all = "snake_case")]
297pub enum FieldMappingType {
298 Direct,
300 Computed,
302 Concat,
304 Lookup,
306 Constant,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct TransformationRule {
313 pub name: String,
315 pub target_field: String,
317 pub expression: String,
319 pub order: u32,
321 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#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct MappingValidation {
345 pub validation_type: ValidationType,
347 pub field: String,
349 #[serde(skip_serializing_if = "Option::is_none")]
351 pub params: Option<HashMap<String, serde_json::Value>>,
352 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#[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#[derive(Debug, Deserialize)]
418pub struct ListMappingsParams {
419 pub active_only: Option<bool>,
420}
421
422#[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
431pub 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 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
469pub struct MappingServiceSimulator {
475 state: SharedState<MappingServiceState>,
476 config: MappingServiceConfig,
477}
478
479impl MappingServiceSimulator {
480 pub fn new(config: MappingServiceConfig) -> Self {
482 let state = shared_state(MappingServiceState::new(config.clone()));
483 Self { state, config }
484 }
485
486 pub async fn run(&self, shutdown_rx: oneshot::Receiver<()>) -> SimulatorResult<()> {
488 {
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 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 pub async fn get_mapping(&self, version: &str) -> Option<MappingConfig> {
532 self.state.read().await.mappings.get(version).cloned()
533 }
534
535 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
579async 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
600async 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 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 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
657async 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 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 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
691async 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
705async 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
722async 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
752async 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
760async 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 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#[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#[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 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 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 assert!(state.mappings.contains_key("v1"));
896 assert!(state.mappings.contains_key("v2"));
897 }
898}