librsigstopup 0.1.0

Super safe library untuk simulasi perhitungan top-up dengan JSON API, verbose logging, dan full trace
Documentation
pub mod config;
pub mod error;
pub mod types;
use crate::core::*;
use crate::handler::config::CalculatorConfig;
use crate::handler::error::{CalculationError, Result};
use crate::handler::types::{CalculationRequest, CalculationResponse};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::time::Instant;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TopUpHandler {
    config: CalculatorConfig,
    insurance_calculator: InsuranceCalculator,
    tax_calculator: TaxCalculator,
}
impl TopUpHandler {
    pub fn new() -> Self {
        tracing::info!(event = "handler_initialized", "TopUpHandler initialized");
        Self {
            config: CalculatorConfig::default(),
            insurance_calculator: InsuranceCalculator::default(),
            tax_calculator: TaxCalculator::default(),
        }
    }
    pub fn init_verbose() -> Self {
        use tracing_subscriber::prelude::*;
        let stdout = tracing_subscriber::fmt::layer()
            .json()
            .with_current_span(true)
            .with_span_list(true)
            .with_target(true)
            .with_level(true)
            .with_thread_ids(true)
            .with_file(true)
            .with_line_number(true);
        let subscriber = tracing_subscriber::registry()
            .with(stdout)
            .with(tracing_subscriber::EnvFilter::from_default_env());
        tracing::subscriber::set_global_default(subscriber).expect("Failed to set subscriber");
        tracing::info!(
            event = "handler_initialized_verbose",
            mode = "verbose",
            format = "json",
            "TopUpHandler initialized with verbose logging"
        );
        Self::new()
    }
    pub fn with_config(mut self, config: CalculatorConfig) -> Self {
        self.config = config.clone();
        self.insurance_calculator = InsuranceCalculator::with_config(config.insurance.clone());
        self.tax_calculator = TaxCalculator::with_config(config.tax.clone());
        self
    }
    pub async fn calculate(&self, input: LoanInput) -> Result<LoanResult> {
        let start_time = Instant::now();
        let request_id = uuid::Uuid::new_v4().to_string();
        tracing::info!(
            event = "calculation_started",
            request_id = %request_id,
            input = %serde_json::to_string(&input).unwrap_or_default(),
            "Calculation started"
        );
        input.validate_detailed()?;
        let asuransi = self.insurance_calculator.calculate(input.pinjaman);
        let pajak = self.tax_calculator.calculate(input.pajak);
        let sisa_pokok = RemainingInstallmentCalculator::calculate_remaining_principal(
            input.pinjaman,
            input.angsuran,
            input.sisa_tenor,
        );
        let total_didapat = TotalCalculator::calculate(
            input.pinjaman,
            input.angsuran,
            input.sisa_tenor,
            asuransi,
            pajak,
            input.diskon,
        );
        let calculation_time = start_time.elapsed().as_millis();
        let calculations = CalculationComponents {
            pinjaman: input.pinjaman,
            angsuran: input.angsuran,
            sisa_tenor: input.sisa_tenor,
            sisa_pokok,
            asuransi,
            pajak,
            diskon: input.diskon,
        };
        let mut result = LoanResult::new(input, calculations, total_didapat, calculation_time);
        result.request_id = request_id.clone();
        tracing::info!(
            event = "calculation_completed",
            request_id = %request_id,
            total_didapat = %total_didapat,
            calculation_time_ms = calculation_time,
            result = %serde_json::to_string(&result).unwrap_or_default(),
            "Calculation completed successfully"
        );
        Ok(result)
    }
    pub async fn calculate_from_json(&self, json: &str) -> Result<String> {
        tracing::info!(
            event = "calculate_from_json_started",
            "Processing JSON request"
        );
        let request: CalculationRequest = serde_json::from_str(json)
            .map_err(|e| CalculationError::JsonParseError(e.to_string()))?;
        let input = LoanInput::builder()
            .pinjaman(request.pinjaman)
            .angsuran(request.angsuran)
            .sisa_tenor(request.sisa_tenor)
            .diskon(request.diskon.unwrap_or(0.0))
            .pajak(request.pajak.unwrap_or(0.0))
            .build()?;
        let result = self.calculate(input).await?;
        let response = CalculationResponse::from_result(result);
        let json_response = serde_json::to_string_pretty(&response)
            .map_err(|e| CalculationError::JsonParseError(e.to_string()))?;
        tracing::info!(
            event = "calculate_from_json_completed",
            response_length = json_response.len(),
            "JSON request processed successfully"
        );
        Ok(json_response)
    }
    pub async fn health_check(&self) -> serde_json::Value {
        tracing::info!(event = "health_check", "Performing health check");
        serde_json::json!({
            "status": "healthy",
            "timestamp": Utc::now().to_rfc3339(),
            "version": env!("CARGO_PKG_VERSION"),
            "config": {
                "insurance_percentage": self.config.insurance.percentage.to_string(),
                "admin_fee": self.config.tax.admin_fee.to_string(),
            }
        })
    }
}
impl Default for TopUpHandler {
    fn default() -> Self {
        Self::new()
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    #[tokio::test]
    async fn test_calculate() {
        let handler = TopUpHandler::new();
        let input = LoanInput::builder()
            .pinjaman(10_000_000.0)
            .angsuran(500_000.0)
            .sisa_tenor(5)
            .diskon(200_000.0)
            .pajak(150_000.0)
            .build()
            .unwrap();
        let result = handler.calculate(input).await.unwrap();
        assert_eq!(result.total_didapat.to_string(), "7400000.00");
    }
    #[tokio::test]
    async fn test_calculate_from_json() {
        let handler = TopUpHandler::new();
        let json = r#"{
            "pinjaman": 10000000,
            "angsuran": 500000,
            "sisa_tenor": 5,
            "diskon": 200000,
            "pajak": 150000
        }"#;
        let result = handler.calculate_from_json(json).await.unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
        assert_eq!(parsed["total_didapat"], "7400000.00");
    }
}