1use crate::{
34 config::{FailureType, RulepackServiceConfig},
35 ApiResponse, HealthStatus, ResponseMeta, SharedState, SimulatorError, SimulatorResult,
36 SimulatorStats, Simulator, shared_state,
37};
38use axum::{
39 extract::{Path, Query, State},
40 http::StatusCode,
41 response::{IntoResponse, Json},
42 routing::{get, post},
43 Router,
44};
45use serde::{Deserialize, Serialize};
46use std::collections::HashMap;
47use std::time::Instant;
48use tokio::sync::oneshot;
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct RulepackConfig {
57 pub version: String,
59 pub name: String,
61 pub description: String,
63 pub rules: Vec<ValidationRule>,
65 pub cross_field_rules: Vec<CrossFieldRule>,
67 pub created_at: String,
69 pub is_active: bool,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ValidationRule {
76 pub rule_id: String,
78 pub field: String,
80 pub rule_type: RuleType,
82 pub severity: Severity,
84 pub message: String,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub params: Option<HashMap<String, serde_json::Value>>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
93#[serde(rename_all = "snake_case")]
94pub enum RuleType {
95 Required,
96 Length,
97 Range,
98 Pattern,
99 Enum,
100 DateFormat,
101 Custom,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
106#[serde(rename_all = "snake_case")]
107pub enum Severity {
108 Error,
109 Warning,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct CrossFieldRule {
115 pub rule_id: String,
116 pub rule_type: CrossFieldRuleType,
117 pub fields: Vec<String>,
118 pub severity: Severity,
119 pub message: String,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub params: Option<HashMap<String, serde_json::Value>>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
126#[serde(rename_all = "snake_case")]
127pub enum CrossFieldRuleType {
128 Compare,
129 Conditional,
130 Custom,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct ValidationResult {
136 pub record_index: usize,
137 pub valid: bool,
138 pub errors: Vec<RuleViolation>,
139 pub warnings: Vec<RuleViolation>,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct RuleViolation {
145 pub rule_id: String,
146 pub field: String,
147 pub message: String,
148 pub severity: Severity,
149}
150
151#[derive(Debug, Deserialize)]
153pub struct ValidateRequest {
154 pub records: Vec<serde_json::Value>,
155}
156
157#[derive(Debug, Deserialize)]
159pub struct ListRulepacksParams {
160 pub active_only: Option<bool>,
161}
162
163impl RulepackConfig {
164 pub fn v1() -> Self {
166 Self {
167 version: "v1".to_string(),
168 name: "SLIK Basic Rulepack v1".to_string(),
169 description: "Basic validation rules for SLIK credit reporting".to_string(),
170 rules: vec![
171 ValidationRule {
172 rule_id: "R001".to_string(),
173 field: "nik".to_string(),
174 rule_type: RuleType::Required,
175 severity: Severity::Error,
176 message: "NIK is required".to_string(),
177 params: None,
178 },
179 ValidationRule {
180 rule_id: "R002".to_string(),
181 field: "nik".to_string(),
182 rule_type: RuleType::Length,
183 severity: Severity::Error,
184 message: "NIK must be exactly 16 digits".to_string(),
185 params: Some({
186 let mut p = HashMap::new();
187 p.insert("min".to_string(), serde_json::json!(16));
188 p.insert("max".to_string(), serde_json::json!(16));
189 p
190 }),
191 },
192 ValidationRule {
193 rule_id: "R003".to_string(),
194 field: "nama_lengkap".to_string(),
195 rule_type: RuleType::Required,
196 severity: Severity::Error,
197 message: "Full name is required".to_string(),
198 params: None,
199 },
200 ValidationRule {
201 rule_id: "R004".to_string(),
202 field: "jumlah_kredit".to_string(),
203 rule_type: RuleType::Range,
204 severity: Severity::Error,
205 message: "Credit amount must be positive".to_string(),
206 params: Some({
207 let mut p = HashMap::new();
208 p.insert("min".to_string(), serde_json::json!(1));
209 p
210 }),
211 },
212 ValidationRule {
213 rule_id: "R005".to_string(),
214 field: "kolektabilitas".to_string(),
215 rule_type: RuleType::Range,
216 severity: Severity::Error,
217 message: "Collectability must be between 1 and 5".to_string(),
218 params: Some({
219 let mut p = HashMap::new();
220 p.insert("min".to_string(), serde_json::json!(1));
221 p.insert("max".to_string(), serde_json::json!(5));
222 p
223 }),
224 },
225 ValidationRule {
226 rule_id: "R006".to_string(),
227 field: "mata_uang".to_string(),
228 rule_type: RuleType::Enum,
229 severity: Severity::Error,
230 message: "Currency must be a valid ISO 4217 code".to_string(),
231 params: Some({
232 let mut p = HashMap::new();
233 p.insert("values".to_string(), serde_json::json!(["IDR", "USD", "EUR", "SGD", "JPY"]));
234 p
235 }),
236 },
237 ],
238 cross_field_rules: vec![
239 CrossFieldRule {
240 rule_id: "CF001".to_string(),
241 rule_type: CrossFieldRuleType::Compare,
242 fields: vec!["tanggal_mulai".to_string(), "tanggal_jatuh_tempo".to_string()],
243 severity: Severity::Error,
244 message: "Start date must be before maturity date".to_string(),
245 params: Some({
246 let mut p = HashMap::new();
247 p.insert("operator".to_string(), serde_json::json!("<="));
248 p
249 }),
250 },
251 CrossFieldRule {
252 rule_id: "CF002".to_string(),
253 rule_type: CrossFieldRuleType::Compare,
254 fields: vec!["saldo_outstanding".to_string(), "jumlah_kredit".to_string()],
255 severity: Severity::Warning,
256 message: "Outstanding balance should not exceed credit amount".to_string(),
257 params: Some({
258 let mut p = HashMap::new();
259 p.insert("operator".to_string(), serde_json::json!("<="));
260 p
261 }),
262 },
263 ],
264 created_at: "2023-01-15T00:00:00Z".to_string(),
265 is_active: true,
266 }
267 }
268
269 pub fn v2() -> Self {
271 let mut rulepack = Self::v1();
272 rulepack.version = "v2".to_string();
273 rulepack.name = "SLIK Enhanced Rulepack v2".to_string();
274 rulepack.description = "Enhanced validation with additional regulatory checks".to_string();
275
276 rulepack.rules.extend(vec![
277 ValidationRule {
278 rule_id: "R007".to_string(),
279 field: "tanggal_mulai".to_string(),
280 rule_type: RuleType::DateFormat,
281 severity: Severity::Error,
282 message: "Start date must be in YYYY-MM-DD format".to_string(),
283 params: Some({
284 let mut p = HashMap::new();
285 p.insert("format".to_string(), serde_json::json!("%Y-%m-%d"));
286 p
287 }),
288 },
289 ValidationRule {
290 rule_id: "R008".to_string(),
291 field: "kode_cabang".to_string(),
292 rule_type: RuleType::Pattern,
293 severity: Severity::Error,
294 message: "Branch code must be 3 letters followed by 3 digits".to_string(),
295 params: Some({
296 let mut p = HashMap::new();
297 p.insert("pattern".to_string(), serde_json::json!(r"^[A-Z]{3}\d{3}$"));
298 p
299 }),
300 },
301 ]);
302
303 rulepack.created_at = "2024-01-10T00:00:00Z".to_string();
304 rulepack
305 }
306}
307
308pub struct RulepackServiceState {
314 pub config: RulepackServiceConfig,
315 pub rulepacks: HashMap<String, RulepackConfig>,
316 pub stats: SimulatorStats,
317 pub started_at: Instant,
318 pub ready: bool,
319}
320
321impl RulepackServiceState {
322 pub fn new(config: RulepackServiceConfig) -> Self {
323 let mut rulepacks = HashMap::new();
324
325 if config.available_versions.contains(&"v1".to_string()) {
326 rulepacks.insert("v1".to_string(), RulepackConfig::v1());
327 }
328 if config.available_versions.contains(&"v2".to_string()) {
329 rulepacks.insert("v2".to_string(), RulepackConfig::v2());
330 }
331
332 Self {
333 config,
334 rulepacks,
335 stats: SimulatorStats::default(),
336 started_at: Instant::now(),
337 ready: false,
338 }
339 }
340}
341
342pub struct RulepackServiceSimulator {
348 state: SharedState<RulepackServiceState>,
349 config: RulepackServiceConfig,
350}
351
352impl RulepackServiceSimulator {
353 pub fn new(config: RulepackServiceConfig) -> Self {
355 let state = shared_state(RulepackServiceState::new(config.clone()));
356 Self { state, config }
357 }
358
359 pub async fn run(&self, shutdown_rx: oneshot::Receiver<()>) -> SimulatorResult<()> {
361 {
362 let mut state = self.state.write().await;
363 state.ready = true;
364 }
365
366 let app = self.create_router();
367 let addr: std::net::SocketAddr = self.config.socket_addr().parse()
368 .map_err(|e| SimulatorError::ConfigError(format!("Invalid address: {}", e)))?;
369
370 tracing::info!("Rulepack Service Simulator listening on {}", addr);
371
372 let listener = tokio::net::TcpListener::bind(addr).await
373 .map_err(|e| SimulatorError::BindError(e.to_string()))?;
374
375 axum::serve(listener, app)
376 .with_graceful_shutdown(async {
377 let _ = shutdown_rx.await;
378 tracing::info!("Rulepack Service Simulator shutting down");
379 })
380 .await
381 .map_err(|e| SimulatorError::StartError(e.to_string()))?;
382
383 Ok(())
384 }
385
386 fn create_router(&self) -> Router {
387 let state = self.state.clone();
388
389 Router::new()
390 .route("/health", get(health_handler))
391 .route("/api/v1/rulepacks", get(list_rulepacks_handler))
392 .route("/api/v1/rulepacks", post(create_rulepack_handler))
393 .route("/api/v1/rulepacks/:version", get(get_rulepack_handler))
394 .route("/api/v1/rulepacks/:version/validate", post(validate_handler))
395 .route("/api/v1/stats", get(stats_handler))
396 .route("/api/v1/reset", post(reset_handler))
397 .with_state(state)
398 }
399
400 pub async fn get_rulepack(&self, version: &str) -> Option<RulepackConfig> {
402 self.state.read().await.rulepacks.get(version).cloned()
403 }
404}
405
406#[async_trait::async_trait]
407impl Simulator for RulepackServiceSimulator {
408 fn name(&self) -> &str {
409 "rulepack-service"
410 }
411
412 fn port(&self) -> u16 {
413 self.config.port
414 }
415
416 async fn health(&self) -> HealthStatus {
417 let state = self.state.read().await;
418 let uptime = state.started_at.elapsed().as_secs();
419
420 if state.ready {
421 HealthStatus::healthy(self.name(), "1.0.0", uptime)
422 .with_details("rulepack_count", serde_json::json!(state.rulepacks.len()))
423 .with_details("available_versions", serde_json::json!(
424 state.rulepacks.keys().collect::<Vec<_>>()
425 ))
426 } else {
427 HealthStatus::unhealthy(self.name(), "Not ready")
428 }
429 }
430
431 async fn stats(&self) -> SimulatorStats {
432 self.state.read().await.stats.clone()
433 }
434
435 async fn reset_stats(&self) {
436 self.state.write().await.stats = SimulatorStats::default();
437 }
438
439 async fn is_ready(&self) -> bool {
440 self.state.read().await.ready
441 }
442}
443
444async fn health_handler(
449 State(state): State<SharedState<RulepackServiceState>>,
450) -> impl IntoResponse {
451 let state = state.read().await;
452 let uptime = state.started_at.elapsed().as_secs();
453
454 if state.ready {
455 let health = HealthStatus::healthy("rulepack-service", "1.0.0", uptime)
456 .with_details("rulepack_count", serde_json::json!(state.rulepacks.len()));
457 (StatusCode::OK, Json(health))
458 } else {
459 let health = HealthStatus::unhealthy("rulepack-service", "Not ready");
460 (StatusCode::SERVICE_UNAVAILABLE, Json(health))
461 }
462}
463
464#[derive(Debug, Clone, Serialize)]
466pub struct RulepackSummary {
467 pub version: String,
468 pub name: String,
469 pub rule_count: usize,
470 pub cross_field_rule_count: usize,
471 pub is_active: bool,
472}
473
474async fn list_rulepacks_handler(
475 State(state): State<SharedState<RulepackServiceState>>,
476 Query(params): Query<ListRulepacksParams>,
477) -> impl IntoResponse {
478 let start = Instant::now();
479 let mut state_guard = state.write().await;
480
481 let failure = state_guard.config.failure_injection.random_failure().cloned();
482 if let Some(ref failure) = failure {
483 state_guard.stats.record_request("/api/v1/rulepacks", false, start.elapsed().as_millis() as f64);
484 return match failure {
485 FailureType::InternalError => {
486 (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<Vec<RulepackSummary>>::error("ERR500", "Internal server error")))
487 }
488 _ => {
489 (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<Vec<RulepackSummary>>::error("ERR500", "Internal server error")))
490 }
491 };
492 }
493
494 state_guard.config.latency.apply().await;
495
496 let active_only = params.active_only.unwrap_or(false);
497 let summaries: Vec<RulepackSummary> = state_guard.rulepacks
498 .values()
499 .filter(|r| !active_only || r.is_active)
500 .map(|r| RulepackSummary {
501 version: r.version.clone(),
502 name: r.name.clone(),
503 rule_count: r.rules.len(),
504 cross_field_rule_count: r.cross_field_rules.len(),
505 is_active: r.is_active,
506 })
507 .collect();
508
509 state_guard.stats.record_request("/api/v1/rulepacks", true, start.elapsed().as_millis() as f64);
510
511 let meta = ResponseMeta {
512 page: None,
513 page_size: None,
514 total_count: Some(summaries.len() as u64),
515 total_pages: None,
516 processing_time_ms: Some(start.elapsed().as_millis() as u64),
517 extra: None,
518 };
519
520 (StatusCode::OK, Json(ApiResponse::success_with_meta(summaries, meta)))
521}
522
523async fn get_rulepack_handler(
524 State(state): State<SharedState<RulepackServiceState>>,
525 Path(version): Path<String>,
526) -> impl IntoResponse {
527 let start = Instant::now();
528 let mut state_guard = state.write().await;
529
530 state_guard.config.latency.apply().await;
531
532 if let Some(rulepack) = state_guard.rulepacks.get(&version).cloned() {
533 state_guard.stats.record_request(&format!("/api/v1/rulepacks/{}", version), true, start.elapsed().as_millis() as f64);
534 (StatusCode::OK, Json(ApiResponse::success(rulepack)))
535 } else {
536 state_guard.stats.record_request(&format!("/api/v1/rulepacks/{}", version), false, start.elapsed().as_millis() as f64);
537 (StatusCode::NOT_FOUND, Json(ApiResponse::<RulepackConfig>::error("NOT_FOUND", &format!("Rulepack version '{}' not found", version))))
538 }
539}
540
541async fn validate_handler(
542 State(state): State<SharedState<RulepackServiceState>>,
543 Path(version): Path<String>,
544 Json(request): Json<ValidateRequest>,
545) -> impl IntoResponse {
546 let start = Instant::now();
547 let mut state_guard = state.write().await;
548
549 state_guard.config.latency.apply().await;
550
551 let rulepack = match state_guard.rulepacks.get(&version) {
552 Some(rp) => rp.clone(),
553 None => {
554 state_guard.stats.record_request(&format!("/api/v1/rulepacks/{}/validate", version), false, start.elapsed().as_millis() as f64);
555 return (StatusCode::NOT_FOUND, Json(ApiResponse::<Vec<ValidationResult>>::error("NOT_FOUND", &format!("Rulepack '{}' not found", version))));
556 }
557 };
558
559 let results: Vec<ValidationResult> = request.records.iter().enumerate().map(|(idx, record)| {
561 let mut errors = Vec::new();
562 let mut warnings = Vec::new();
563
564 for rule in &rulepack.rules {
565 let value = record.get(&rule.field);
566 let violation = match rule.rule_type {
567 RuleType::Required => {
568 match value {
569 None => Some(rule.message.clone()),
570 Some(v) if v.is_null() || (v.is_string() && v.as_str().unwrap_or("").is_empty()) => Some(rule.message.clone()),
571 _ => None,
572 }
573 }
574 RuleType::Range => {
575 if let (Some(val), Some(params)) = (value, &rule.params) {
576 if let Some(num) = val.as_f64() {
577 let min = params.get("min").and_then(|v| v.as_f64());
578 let max = params.get("max").and_then(|v| v.as_f64());
579 if let Some(min) = min {
580 if num < min { return_violation(true, &rule.message) } else { None }
581 } else if let Some(max) = max {
582 if num > max { return_violation(true, &rule.message) } else { None }
583 } else { None }
584 } else { None }
585 } else { None }
586 }
587 RuleType::Length => {
588 if let (Some(val), Some(params)) = (value, &rule.params) {
589 if let Some(s) = val.as_str() {
590 let len = s.len() as u64;
591 let min = params.get("min").and_then(|v| v.as_u64()).unwrap_or(0);
592 let max = params.get("max").and_then(|v| v.as_u64()).unwrap_or(u64::MAX);
593 if len < min || len > max { Some(rule.message.clone()) } else { None }
594 } else { None }
595 } else { None }
596 }
597 _ => None, };
599
600 if let Some(msg) = violation {
601 let v = RuleViolation {
602 rule_id: rule.rule_id.clone(),
603 field: rule.field.clone(),
604 message: msg,
605 severity: rule.severity.clone(),
606 };
607 match rule.severity {
608 Severity::Error => errors.push(v),
609 Severity::Warning => warnings.push(v),
610 }
611 }
612 }
613
614 ValidationResult {
615 record_index: idx,
616 valid: errors.is_empty(),
617 errors,
618 warnings,
619 }
620 }).collect();
621
622 state_guard.stats.record_request(&format!("/api/v1/rulepacks/{}/validate", version), true, start.elapsed().as_millis() as f64);
623
624 let meta = ResponseMeta {
625 page: None,
626 page_size: None,
627 total_count: Some(results.len() as u64),
628 total_pages: None,
629 processing_time_ms: Some(start.elapsed().as_millis() as u64),
630 extra: None,
631 };
632
633 (StatusCode::OK, Json(ApiResponse::success_with_meta(results, meta)))
634}
635
636fn return_violation(should: bool, msg: &str) -> Option<String> {
637 if should { Some(msg.to_string()) } else { None }
638}
639
640#[derive(Debug, Deserialize)]
642pub struct CreateRulepackRequest {
643 pub version: String,
644 pub name: String,
645 pub description: Option<String>,
646 pub rules: Vec<ValidationRule>,
647 #[serde(default)]
648 pub cross_field_rules: Vec<CrossFieldRule>,
649}
650
651async fn create_rulepack_handler(
652 State(state): State<SharedState<RulepackServiceState>>,
653 Json(request): Json<CreateRulepackRequest>,
654) -> impl IntoResponse {
655 let mut state_guard = state.write().await;
656
657 if state_guard.rulepacks.contains_key(&request.version) {
658 return (StatusCode::CONFLICT, Json(ApiResponse::<RulepackConfig>::error("CONFLICT", &format!("Rulepack version '{}' already exists", request.version))));
659 }
660
661 let rulepack = RulepackConfig {
662 version: request.version.clone(),
663 name: request.name,
664 description: request.description.unwrap_or_default(),
665 rules: request.rules,
666 cross_field_rules: request.cross_field_rules,
667 created_at: chrono::Utc::now().to_rfc3339(),
668 is_active: true,
669 };
670
671 state_guard.rulepacks.insert(request.version, rulepack.clone());
672 (StatusCode::CREATED, Json(ApiResponse::success(rulepack)))
673}
674
675async fn stats_handler(
676 State(state): State<SharedState<RulepackServiceState>>,
677) -> impl IntoResponse {
678 let state_guard = state.read().await;
679 Json(ApiResponse::success(state_guard.stats.clone()))
680}
681
682async fn reset_handler(
683 State(state): State<SharedState<RulepackServiceState>>,
684) -> impl IntoResponse {
685 let mut state_guard = state.write().await;
686 state_guard.stats = SimulatorStats::default();
687
688 state_guard.rulepacks.clear();
689 if state_guard.config.available_versions.contains(&"v1".to_string()) {
690 state_guard.rulepacks.insert("v1".to_string(), RulepackConfig::v1());
691 }
692 if state_guard.config.available_versions.contains(&"v2".to_string()) {
693 state_guard.rulepacks.insert("v2".to_string(), RulepackConfig::v2());
694 }
695
696 Json(ApiResponse::success(serde_json::json!({
697 "reset": true,
698 "rulepack_count": state_guard.rulepacks.len()
699 })))
700}