use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use time::Date;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DegressionTier {
None,
Low,
Medium,
Standard,
High,
Maximum,
}
impl DegressionTier {
#[must_use]
pub fn rate_pct_per_quarter(self) -> Decimal {
match self {
Self::None => dec!(0.00),
Self::Low => dec!(0.25),
Self::Medium => dec!(0.50),
Self::Standard => dec!(1.00),
Self::High => dec!(1.40),
Self::Maximum => dec!(1.50),
}
}
#[must_use]
pub fn from_gw_expansion(gw_added_prev_year: Decimal) -> Self {
if gw_added_prev_year <= dec!(9) {
Self::None
} else if gw_added_prev_year <= dec!(12) {
Self::Low
} else if gw_added_prev_year <= dec!(13) {
Self::Medium
} else if gw_added_prev_year <= dec!(14) {
Self::Standard
} else if gw_added_prev_year <= dec!(15) {
Self::High
} else {
Self::Maximum
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Quarter {
pub year: i32,
pub quarter: u8,
}
impl Quarter {
#[must_use]
pub fn from_date(d: Date) -> Self {
let month = d.month() as u8;
Self {
year: d.year(),
quarter: (month - 1) / 3 + 1,
}
}
#[must_use]
pub fn quarters_since(self, base: Quarter) -> i32 {
(self.year - base.year) * 4 + (self.quarter as i32 - base.quarter as i32)
}
}
impl std::fmt::Display for Quarter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Q{}/{}", self.quarter, self.year)
}
}
pub const SOLARPAKET_I_REFERENCE_QUARTER: Quarter = Quarter {
year: 2024,
quarter: 2,
};
#[must_use]
pub fn apply_degression(
reference_rate_ct: Decimal,
quarters_elapsed: i32,
tier: DegressionTier,
) -> Decimal {
if quarters_elapsed <= 0 {
return reference_rate_ct.round_dp(2);
}
let factor_per_quarter = Decimal::ONE - tier.rate_pct_per_quarter() / dec!(100);
let n = quarters_elapsed as u32;
let factor = (0..n).fold(Decimal::ONE, |acc, _| acc * factor_per_quarter);
(reference_rate_ct * factor).round_dp(2)
}
#[must_use]
fn solarpaket_i_ueberschuss_base(leistung_kwp: Decimal) -> Option<Decimal> {
if leistung_kwp <= dec!(10) {
Some(dec!(8.51))
} else if leistung_kwp <= dec!(40) {
Some(dec!(7.43))
} else if leistung_kwp <= dec!(1000) {
Some(dec!(7.64)) } else {
None }
}
#[must_use]
fn solarpaket_i_volleinspeisung_base(leistung_kwp: Decimal) -> Option<Decimal> {
if leistung_kwp <= dec!(10) {
Some(dec!(13.31)) } else if leistung_kwp <= dec!(40) {
Some(dec!(11.23)) } else if leistung_kwp <= dec!(100) {
Some(dec!(12.74)) } else if leistung_kwp <= dec!(400) {
Some(dec!(10.84)) } else if leistung_kwp <= dec!(1000) {
Some(dec!(9.54)) } else {
None }
}
#[must_use]
pub fn solar_ueberschuss_rate_for_quarter(
commissioning_quarter: Quarter,
leistung_kwp: Decimal,
tier: DegressionTier,
) -> Option<Decimal> {
let n = commissioning_quarter.quarters_since(SOLARPAKET_I_REFERENCE_QUARTER);
if n < 0 {
return None; }
let base = solarpaket_i_ueberschuss_base(leistung_kwp)?;
Some(apply_degression(base, n, tier))
}
#[must_use]
pub fn solar_volleinspeisung_rate_for_quarter(
commissioning_quarter: Quarter,
leistung_kwp: Decimal,
tier: DegressionTier,
) -> Option<Decimal> {
let n = commissioning_quarter.quarters_since(SOLARPAKET_I_REFERENCE_QUARTER);
if n < 0 {
return None;
}
let base = solarpaket_i_volleinspeisung_base(leistung_kwp)?;
Some(apply_degression(base, n, tier))
}
#[must_use]
pub fn solar_rate_at_commissioning(
commissioning_date: Date,
leistung_kwp: Decimal,
volleinspeisung: bool,
tier: DegressionTier,
) -> Option<Decimal> {
let q = Quarter::from_date(commissioning_date);
if volleinspeisung {
solar_volleinspeisung_rate_for_quarter(q, leistung_kwp, tier)
} else {
solar_ueberschuss_rate_for_quarter(q, leistung_kwp, tier)
}
}
#[cfg(test)]
mod tests {
use super::*;
use time::macros::date;
#[test]
fn quarter_from_date_all_quarters() {
assert_eq!(
Quarter::from_date(date!(2024 - 01 - 15)),
Quarter {
year: 2024,
quarter: 1
}
);
assert_eq!(
Quarter::from_date(date!(2024 - 05 - 01)),
Quarter {
year: 2024,
quarter: 2
}
);
assert_eq!(
Quarter::from_date(date!(2024 - 09 - 30)),
Quarter {
year: 2024,
quarter: 3
}
);
assert_eq!(
Quarter::from_date(date!(2024 - 10 - 01)),
Quarter {
year: 2024,
quarter: 4
}
);
assert_eq!(
Quarter::from_date(date!(2024 - 12 - 31)),
Quarter {
year: 2024,
quarter: 4
}
);
}
#[test]
fn quarters_since_same_year() {
let q1 = Quarter {
year: 2024,
quarter: 1,
};
let q4 = Quarter {
year: 2024,
quarter: 4,
};
assert_eq!(q4.quarters_since(q1), 3);
assert_eq!(q1.quarters_since(q4), -3);
}
#[test]
fn quarters_since_cross_year() {
let start = Quarter {
year: 2024,
quarter: 1,
};
let end = Quarter {
year: 2025,
quarter: 3,
};
assert_eq!(end.quarters_since(start), 6);
}
#[test]
fn no_degression_keeps_rate() {
assert_eq!(
apply_degression(dec!(8.51), 8, DegressionTier::None),
dec!(8.51)
);
assert_eq!(
apply_degression(dec!(8.51), 0, DegressionTier::Standard),
dec!(8.51)
);
}
#[test]
fn standard_1pct_4_quarters() {
let rate = apply_degression(dec!(8.51), 4, DegressionTier::Standard);
assert_eq!(rate, dec!(8.17));
}
#[test]
fn low_025pct_degression() {
let rate = apply_degression(dec!(8.51), 4, DegressionTier::Low);
assert!(rate > dec!(8.40) && rate <= dec!(8.51));
}
#[test]
fn maximum_15pct_degression() {
let rate = apply_degression(dec!(8.51), 8, DegressionTier::Maximum);
assert!(rate > dec!(7.40) && rate < dec!(8.51));
}
#[test]
fn solar_ueberschuss_reference_quarter_unchanged() {
let rate = solar_ueberschuss_rate_for_quarter(
SOLARPAKET_I_REFERENCE_QUARTER,
dec!(9),
DegressionTier::None,
);
assert_eq!(rate, Some(dec!(8.51)));
}
#[test]
fn solar_ueberschuss_2_quarters_standard() {
let rate = solar_ueberschuss_rate_for_quarter(
Quarter {
year: 2024,
quarter: 4,
},
dec!(9),
DegressionTier::Standard,
);
assert_eq!(rate, Some(dec!(8.34)));
}
#[test]
fn solar_ueberschuss_before_solarpaket_returns_none() {
let rate = solar_ueberschuss_rate_for_quarter(
Quarter {
year: 2024,
quarter: 1,
},
dec!(9),
DegressionTier::Standard,
);
assert!(rate.is_none());
}
#[test]
fn solar_volleinspeisung_reference_quarter() {
let rate = solar_volleinspeisung_rate_for_quarter(
SOLARPAKET_I_REFERENCE_QUARTER,
dec!(9),
DegressionTier::None,
);
assert_eq!(rate, Some(dec!(13.31)));
}
#[test]
fn solar_rate_at_commissioning_date() {
let rate = solar_rate_at_commissioning(
date!(2025 - 03 - 01),
dec!(9),
false,
DegressionTier::Standard,
);
assert_eq!(rate, Some(dec!(8.26)));
}
#[test]
fn tier_from_gw_expansion_all_brackets() {
assert_eq!(
DegressionTier::from_gw_expansion(dec!(8.5)),
DegressionTier::None
);
assert_eq!(
DegressionTier::from_gw_expansion(dec!(9.0)),
DegressionTier::None
);
assert_eq!(
DegressionTier::from_gw_expansion(dec!(9.1)),
DegressionTier::Low
);
assert_eq!(
DegressionTier::from_gw_expansion(dec!(12.0)),
DegressionTier::Low
);
assert_eq!(
DegressionTier::from_gw_expansion(dec!(12.5)),
DegressionTier::Medium
);
assert_eq!(
DegressionTier::from_gw_expansion(dec!(13.5)),
DegressionTier::Standard
);
assert_eq!(
DegressionTier::from_gw_expansion(dec!(14.5)),
DegressionTier::High
);
assert_eq!(
DegressionTier::from_gw_expansion(dec!(16.0)),
DegressionTier::Maximum
);
}
#[test]
fn large_plant_above_1mwp_returns_none() {
let rate = solar_ueberschuss_rate_for_quarter(
SOLARPAKET_I_REFERENCE_QUARTER,
dec!(1500), DegressionTier::None,
);
assert!(rate.is_none());
}
#[test]
fn quarter_display() {
let q = Quarter {
year: 2024,
quarter: 2,
};
assert_eq!(format!("{q}"), "Q2/2024");
}
}