Skip to main content

credit_data_simulator/
rulepack_service.rs

1//! # Rulepack Service Simulator
2//!
3//! Simulates the Rule Engine service responsible for validating data against
4//! complex business rules and regulatory requirements.
5//!
6//! ## Business Purpose
7//!
8//! The Rulepack Service ensures that all data submitted to the regulator
9//! complies with the defined schemas, validation rules, and cross-field constraints.
10//! It acts as the final gatekeeper before data submission.
11//!
12//! ## Key Features
13//!
14//! - **Versioning**: Manages multiple rulepack versions (v1, v2, minimal).
15//! - **Validation Rule Types**:
16//!   - `Required`: Mandatory field checks.
17//!   - `Length`, `Range`, `Pattern`: Format validation.
18//!   - `Enum`: Value list validation.
19//!   - `DateFormat`: Date string validity.
20//! - **Cross-Field Validation**:
21//!   - `Compare`: Compare two fields (e.g. start_date <= end_date).
22//!   - `Conditional`: If Field A = X, then Field B must be present.
23//! - **Severity Levels**: Support for Errors (blocking) and Warnings (non-blocking).
24//!
25//! ## API Endpoints
26//!
27//! - `GET /api/v1/rulepacks` - List available rulepacks.
28//! - `GET /api/v1/rulepacks/:version` - Get rulepack definition.
29//! - `POST /api/v1/rulepacks/:version/validate` - Validate records.
30//! - `POST /api/v1/rulepacks` - Create dynamic rulepack (testing).
31//! - `GET /api/v1/stats` - Get simulator statistics.
32
33use 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// ============================================================================
51// Data Models
52// ============================================================================
53
54/// Rulepack configuration for a specific version
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct RulepackConfig {
57    /// Version identifier
58    pub version: String,
59    /// Display name
60    pub name: String,
61    /// Description
62    pub description: String,
63    /// Validation rules
64    pub rules: Vec<ValidationRule>,
65    /// Cross-field rules
66    pub cross_field_rules: Vec<CrossFieldRule>,
67    /// Creation timestamp
68    pub created_at: String,
69    /// Whether this rulepack is active
70    pub is_active: bool,
71}
72
73/// Individual validation rule
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ValidationRule {
76    /// Rule identifier
77    pub rule_id: String,
78    /// Target field
79    pub field: String,
80    /// Rule type
81    pub rule_type: RuleType,
82    /// Severity (error or warning)
83    pub severity: Severity,
84    /// Error message
85    pub message: String,
86    /// Rule parameters
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub params: Option<HashMap<String, serde_json::Value>>,
89}
90
91/// Type of validation rule
92#[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/// Severity level
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
106#[serde(rename_all = "snake_case")]
107pub enum Severity {
108    Error,
109    Warning,
110}
111
112/// Cross-field validation rule
113#[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/// Type of cross-field validation
125#[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/// Validation result for a single record
134#[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/// Individual rule violation
143#[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/// Request to validate records
152#[derive(Debug, Deserialize)]
153pub struct ValidateRequest {
154    pub records: Vec<serde_json::Value>,
155}
156
157/// Query params for listing rulepacks
158#[derive(Debug, Deserialize)]
159pub struct ListRulepacksParams {
160    pub active_only: Option<bool>,
161}
162
163impl RulepackConfig {
164    /// Create v1 rulepack (basic SLIK rules)
165    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    /// Create v2 rulepack (enhanced with additional checks)
270    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
308// ============================================================================
309// Simulator State
310// ============================================================================
311
312/// Internal state for Rulepack Service Simulator
313pub 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
342// ============================================================================
343// Rulepack Service Simulator
344// ============================================================================
345
346/// Rulepack Service Simulator implementation
347pub struct RulepackServiceSimulator {
348    state: SharedState<RulepackServiceState>,
349    config: RulepackServiceConfig,
350}
351
352impl RulepackServiceSimulator {
353    /// Create a new Rulepack Service Simulator
354    pub fn new(config: RulepackServiceConfig) -> Self {
355        let state = shared_state(RulepackServiceState::new(config.clone()));
356        Self { state, config }
357    }
358
359    /// Run the simulator HTTP server
360    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    /// Get a specific rulepack version
401    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
444// ============================================================================
445// HTTP Handlers
446// ============================================================================
447
448async 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/// Rulepack summary for listing
465#[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    // Basic validation of each record
560    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, // Other rule types: pass for now
598            };
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/// Request to create a new rulepack
641#[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}