librsigstopup 0.1.0

Super safe library untuk simulasi perhitungan top-up dengan JSON API, verbose logging, dan full trace
Documentation
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");
    }
}