use rust_decimal::Decimal;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NettingResult {
pub net_vergütung_eur: Decimal,
pub residual_pflichtzahlung_eur: Decimal,
pub netting_applied: bool,
}
#[must_use]
pub fn apply_sect52_netting(vergütung_eur: Decimal, pflichtzahlung_eur: Decimal) -> NettingResult {
if pflichtzahlung_eur.is_zero() {
return NettingResult {
net_vergütung_eur: vergütung_eur,
residual_pflichtzahlung_eur: Decimal::ZERO,
netting_applied: false,
};
}
let net_vergütung = (vergütung_eur - pflichtzahlung_eur).max(Decimal::ZERO);
let residual = (pflichtzahlung_eur - vergütung_eur).max(Decimal::ZERO);
NettingResult {
net_vergütung_eur: net_vergütung,
residual_pflichtzahlung_eur: residual,
netting_applied: true,
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ReductionPipeline {
pub pflichtzahlung_eur: Option<Decimal>,
pub apply_sect52_netting: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ReductionPipelineResult {
pub net_vergütung_eur: Decimal,
pub residual_pflichtzahlung_eur: Decimal,
pub total_reductions_eur: Decimal,
}
impl ReductionPipeline {
#[must_use]
pub fn none() -> Self {
Self {
pflichtzahlung_eur: None,
apply_sect52_netting: false,
}
}
#[must_use]
pub fn apply(&self, gross_settlement_eur: Decimal) -> ReductionPipelineResult {
let Some(pz) = self.pflichtzahlung_eur.filter(|p| *p > Decimal::ZERO) else {
return ReductionPipelineResult {
net_vergütung_eur: gross_settlement_eur,
residual_pflichtzahlung_eur: Decimal::ZERO,
total_reductions_eur: Decimal::ZERO,
};
};
if !self.apply_sect52_netting {
return ReductionPipelineResult {
net_vergütung_eur: gross_settlement_eur,
residual_pflichtzahlung_eur: pz,
total_reductions_eur: Decimal::ZERO,
};
}
let netting = apply_sect52_netting(gross_settlement_eur, pz);
ReductionPipelineResult {
net_vergütung_eur: netting.net_vergütung_eur,
residual_pflichtzahlung_eur: netting.residual_pflichtzahlung_eur,
total_reductions_eur: pz - netting.residual_pflichtzahlung_eur,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::dec;
#[test]
fn netting_normal_case() {
let result = apply_sect52_netting(dec!(42.55), dec!(10.00));
assert_eq!(result.net_vergütung_eur, dec!(32.55));
assert_eq!(result.residual_pflichtzahlung_eur, dec!(0));
assert!(result.netting_applied);
}
#[test]
fn netting_penalty_exceeds_vergutung() {
let result = apply_sect52_netting(dec!(30.00), dec!(50.00));
assert_eq!(result.net_vergütung_eur, dec!(0));
assert_eq!(result.residual_pflichtzahlung_eur, dec!(20.00));
}
#[test]
fn netting_zero_penalty_no_change() {
let result = apply_sect52_netting(dec!(42.55), dec!(0));
assert_eq!(result.net_vergütung_eur, dec!(42.55));
assert!(!result.netting_applied);
}
#[test]
fn pipeline_none_no_change() {
let result = ReductionPipeline::none().apply(dec!(42.55));
assert_eq!(result.net_vergütung_eur, dec!(42.55));
assert_eq!(result.total_reductions_eur, dec!(0));
}
#[test]
fn pipeline_sect52_netting() {
let pipeline = ReductionPipeline {
pflichtzahlung_eur: Some(dec!(10.00)),
apply_sect52_netting: true,
};
let result = pipeline.apply(dec!(42.55));
assert_eq!(result.net_vergütung_eur, dec!(32.55));
assert_eq!(result.residual_pflichtzahlung_eur, dec!(0));
}
#[test]
fn pipeline_netting_without_applying() {
let pipeline = ReductionPipeline {
pflichtzahlung_eur: Some(dec!(10.00)),
apply_sect52_netting: false,
};
let result = pipeline.apply(dec!(42.55));
assert_eq!(result.net_vergütung_eur, dec!(42.55));
assert_eq!(result.residual_pflichtzahlung_eur, dec!(10.00));
}
}