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");
}
}