use crate::handler::error::ValidationError;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoanInput {
pub pinjaman: Decimal,
pub angsuran: Decimal,
pub sisa_tenor: u32,
pub diskon: Decimal,
pub pajak: Decimal,
}
impl LoanInput {
pub fn builder() -> LoanInputBuilder {
LoanInputBuilder::new()
}
pub fn validate_detailed(&self) -> Result<(), ValidationError> {
if self.pinjaman <= Decimal::ZERO {
return Err(ValidationError::new(
"pinjaman",
"Pinjaman harus lebih dari 0",
));
}
if self.angsuran < Decimal::ZERO {
return Err(ValidationError::new(
"angsuran",
"Angsuran tidak boleh negatif",
));
}
if self.diskon < Decimal::ZERO {
return Err(ValidationError::new("diskon", "Diskon tidak boleh negatif"));
}
if self.pajak < Decimal::ZERO {
return Err(ValidationError::new("pajak", "Pajak tidak boleh negatif"));
}
tracing::info!(
event = "input_validated",
pinjaman = %self.pinjaman,
angsuran = %self.angsuran,
sisa_tenor = self.sisa_tenor,
"Input validation passed"
);
Ok(())
}
pub fn total_angsuran_terbayar(&self) -> Decimal {
self.angsuran * Decimal::from(self.sisa_tenor)
}
}
#[derive(Debug, Default)]
pub struct LoanInputBuilder {
pinjaman: Option<Decimal>,
angsuran: Option<Decimal>,
sisa_tenor: Option<u32>,
diskon: Option<Decimal>,
pajak: Option<Decimal>,
}
impl LoanInputBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn pinjaman(mut self, value: f64) -> Self {
self.pinjaman = Some(Decimal::from_f64_retain(value).unwrap());
self
}
pub fn angsuran(mut self, value: f64) -> Self {
self.angsuran = Some(Decimal::from_f64_retain(value).unwrap());
self
}
pub fn sisa_tenor(mut self, value: u32) -> Self {
self.sisa_tenor = Some(value);
self
}
pub fn diskon(mut self, value: f64) -> Self {
self.diskon = Some(Decimal::from_f64_retain(value).unwrap());
self
}
pub fn pajak(mut self, value: f64) -> Self {
self.pajak = Some(Decimal::from_f64_retain(value).unwrap());
self
}
pub fn build(self) -> Result<LoanInput, ValidationError> {
let input = LoanInput {
pinjaman: self
.pinjaman
.ok_or_else(|| ValidationError::new("pinjaman", "Pinjaman harus diisi"))?,
angsuran: self
.angsuran
.ok_or_else(|| ValidationError::new("angsuran", "Angsuran harus diisi"))?,
sisa_tenor: self
.sisa_tenor
.ok_or_else(|| ValidationError::new("sisa_tenor", "Sisa tenor harus diisi"))?,
diskon: self.diskon.unwrap_or(dec!(0)),
pajak: self.pajak.unwrap_or(dec!(0)),
};
input.validate_detailed()?;
Ok(input)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_builder_success() {
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();
assert_eq!(input.pinjaman, dec!(10_000_000));
assert_eq!(input.angsuran, dec!(500_000));
assert_eq!(input.sisa_tenor, 5);
assert_eq!(input.diskon, dec!(200_000));
assert_eq!(input.pajak, dec!(150_000));
}
#[test]
fn test_builder_validation_negative_pinjaman() {
let result = LoanInput::builder()
.pinjaman(-1000.0)
.angsuran(500_000.0)
.sisa_tenor(5)
.build();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Pinjaman harus lebih dari 0"));
}
#[test]
fn test_builder_validation_negative_angsuran() {
let result = LoanInput::builder()
.pinjaman(10_000_000.0)
.angsuran(-500_000.0)
.sisa_tenor(5)
.build();
assert!(result.is_err());
}
#[test]
fn test_builder_default_diskon_pajak() {
let input = LoanInput::builder()
.pinjaman(10_000_000.0)
.angsuran(500_000.0)
.sisa_tenor(5)
.build()
.unwrap();
assert_eq!(input.diskon, dec!(0));
assert_eq!(input.pajak, dec!(0));
}
#[test]
fn test_total_angsuran_terbayar() {
let input = LoanInput::builder()
.pinjaman(10_000_000.0)
.angsuran(500_000.0)
.sisa_tenor(5)
.build()
.unwrap();
assert_eq!(input.total_angsuran_terbayar(), dec!(2_500_000));
}
#[test]
fn test_validate_detailed_success() {
let input = LoanInput::builder()
.pinjaman(1_000_000.0)
.angsuran(100_000.0)
.sisa_tenor(3)
.diskon(50_000.0)
.pajak(75_000.0)
.build()
.unwrap();
assert!(input.validate_detailed().is_ok());
}
#[test]
fn test_validate_detailed_zero_pinjaman() {
let input = LoanInput {
pinjaman: dec!(0),
angsuran: dec!(100_000),
sisa_tenor: 3,
diskon: dec!(0),
pajak: dec!(0),
};
assert!(input.validate_detailed().is_err());
}
#[test]
fn test_decimal_precision() {
let input = LoanInput::builder()
.pinjaman(10_000_000.50)
.angsuran(500_000.75)
.sisa_tenor(5)
.build()
.unwrap();
assert_eq!(input.pinjaman.to_string(), "10000000.50");
assert_eq!(input.angsuran.to_string(), "500000.75");
}
}