use eeg_billing::RoundMoney;
use eeg_billing::{
SettleInput, SettlementScheme, SettlementStatus, calculate_settlement, foerderendedatum_eeg,
};
use rust_decimal::Decimal;
use proptest::prelude::*;
fn arb_kwh() -> impl Strategy<Value = Decimal> {
(0i64..=1_000_000_000i64).prop_map(|n| Decimal::new(n, 2))
}
fn arb_rate_ct() -> impl Strategy<Value = Decimal> {
(0i64..=500_000i64).prop_map(|n| Decimal::new(n, 4))
}
fn arb_epex_ct() -> impl Strategy<Value = Decimal> {
(-1000i64..=20000i64).prop_map(|n| Decimal::new(n, 2))
}
fn arb_epex_ct_nonneg() -> impl Strategy<Value = Decimal> {
(0i64..=20000i64).prop_map(|n| Decimal::new(n, 2))
}
fn arb_leistung_kw() -> impl Strategy<Value = Decimal> {
(1i64..=500_000i64).prop_map(Decimal::from)
}
fn arb_kwk_paid() -> impl Strategy<Value = Decimal> {
(0i64..=10_000_000_000i64).prop_map(|n| Decimal::new(n, 2))
}
proptest! {
#[test]
fn inv1_feed_in_tariff_formula_exact(
kwh in arb_kwh(),
rate in arb_rate_ct(),
) {
let out = calculate_settlement(&SettleInput {
scheme: SettlementScheme::FeedInTariff { verguetungssatz_ct: rate },
einspeisemenge_kwh: Some(kwh),
..SettleInput::default()
});
prop_assume!(out.status == SettlementStatus::Calculated);
let actual = out.settlement_eur.expect("Calculated must have settlement_eur");
let expected = (kwh * rate / Decimal::from(100)).round_kfm(5);
let diff = (actual - expected).abs();
prop_assert!(
diff <= Decimal::new(1, 5),
"FeedInTariff: expected {expected} EUR, got {actual} EUR (diff {diff})"
);
}
}
proptest! {
#[test]
fn inv2_market_premium_never_negative(
kwh in arb_kwh(),
aw_ct in arb_rate_ct(),
epex_ct in arb_epex_ct(),
) {
let out = calculate_settlement(&SettleInput {
scheme: SettlementScheme::MarketPremium {
direktverm_aw_ct: aw_ct,
wind_korrekturfaktor: None,
wind_standort: None,
},
einspeisemenge_kwh: Some(kwh),
marktwert_ct_kwh: Some(epex_ct),
..SettleInput::default()
});
if let Some(eur) = out.settlement_eur {
prop_assert!(
eur >= Decimal::ZERO,
"MarketPremium returned negative EUR {eur} (aw={aw_ct}, epex={epex_ct}, kwh={kwh})"
);
}
}
}
proptest! {
#[test]
fn inv3_positions_sum_equals_settlement_eur(
kwh in arb_kwh(),
rate in arb_rate_ct(),
) {
let out = calculate_settlement(&SettleInput {
scheme: SettlementScheme::FeedInTariff { verguetungssatz_ct: rate },
einspeisemenge_kwh: Some(kwh),
..SettleInput::default()
});
let positions_total: Decimal = out.positions.iter().map(|p| p.eur).sum();
if let Some(eur) = out.settlement_eur {
let diff = (positions_total - eur).abs();
prop_assert!(
diff <= Decimal::new(1, 5),
"positions sum {positions_total} != settlement_eur {eur} (diff {diff})"
);
} else {
prop_assert!(
positions_total == Decimal::ZERO || out.positions.is_empty(),
"positions sum {positions_total} must be zero when settlement_eur is None"
);
}
}
}
proptest! {
#[test]
fn inv4_sect51_deduction_bounded(
kwh in arb_kwh(),
negative_kwh in arb_kwh(),
rate in arb_rate_ct(),
) {
let out = calculate_settlement(&SettleInput {
scheme: SettlementScheme::FeedInTariff { verguetungssatz_ct: rate },
einspeisemenge_kwh: Some(kwh),
kwh_during_negative_epex: Some(negative_kwh),
..SettleInput::default()
});
if let Some(eligible) = out.eligible_kwh {
prop_assert!(
eligible <= kwh,
"eligible_kwh {eligible} exceeds einspeisemenge {kwh}"
);
prop_assert!(
eligible >= Decimal::ZERO,
"eligible_kwh {eligible} is negative"
);
}
}
}
proptest! {
#[test]
fn inv5_foerderung_beendet_when_expired(
kwh in arb_kwh(),
rate in arb_rate_ct(),
year_offset in 0u32..=15u32,
) {
let inbetriebnahme = time::Date::from_calendar_date(
1995 + year_offset as i32,
time::Month::June,
1,
)
.unwrap();
let foerderendedatum = foerderendedatum_eeg(inbetriebnahme).unwrap();
let billing_date_after = foerderendedatum
.replace_year(foerderendedatum.year() + 1)
.unwrap();
let out = calculate_settlement(&SettleInput {
scheme: SettlementScheme::FeedInTariff { verguetungssatz_ct: rate },
einspeisemenge_kwh: Some(kwh),
foerderendedatum: Some(foerderendedatum),
billing_date: Some(billing_date_after),
..SettleInput::default()
});
prop_assert_eq!(
out.status,
SettlementStatus::FoerderungBeendet,
"billing_date > foerderendedatum: expected FoerderungBeendet"
);
let _ = billing_date_after; }
}
proptest! {
#[test]
fn inv6_kwkg_surcharge_non_negative(
kwh in arb_kwh(),
rate_ct in arb_rate_ct(),
max_kwh in arb_kwk_paid(),
paid_kwh in arb_kwk_paid(),
) {
let out = calculate_settlement(&SettleInput {
scheme: SettlementScheme::KwkSurcharge {
verguetungssatz_ct: rate_ct,
max_kwh: Some(max_kwh),
jahres_restkontingent_kwh: None,
kwh_paid_gesamt: Some(paid_kwh),
},
einspeisemenge_kwh: Some(kwh),
..SettleInput::default()
});
if let Some(eur) = out.settlement_eur {
prop_assert!(
eur >= Decimal::ZERO,
"KWK surcharge returned negative EUR {eur}"
);
}
if let Some(eligible) = out.eligible_kwh {
prop_assert!(eligible >= Decimal::ZERO, "KWKG eligible_kwh {eligible} is negative");
}
}
}
proptest! {
#[test]
fn inv7_pflichtzahlung_non_negative(
kwh in arb_kwh(),
rate in arb_rate_ct(),
leistung in arb_leistung_kw(),
months in 0u32..=24u32,
fulfilled in any::<bool>(),
defect in any::<bool>(),
) {
use eeg_billing::{Pflichtverstoss, SanktionsTyp};
let violation = Pflichtverstoss {
typ: SanktionsTyp::FernsteuerbarkeitFehlend,
leistung_kw: leistung,
monate_des_verstosses: months,
beginn: None,
nachtraeglich_erfuellt: fulfilled,
technischer_defekt: defect,
};
let out = calculate_settlement(&SettleInput {
scheme: SettlementScheme::FeedInTariff { verguetungssatz_ct: rate },
einspeisemenge_kwh: Some(kwh),
pflichtverstoss: vec![violation],
..SettleInput::default()
});
if let Some(penalty) = out.pflichtzahlung_eur {
prop_assert!(
penalty >= Decimal::ZERO,
"Pflichtzahlung {penalty} is negative (leistung={leistung}, months={months})"
);
}
}
}
proptest! {
#[test]
fn inv8_settlement_eur_present_iff_calculable(
rate in arb_rate_ct(),
) {
let out_nodata = calculate_settlement(&SettleInput {
scheme: SettlementScheme::FeedInTariff { verguetungssatz_ct: rate },
einspeisemenge_kwh: None,
..SettleInput::default()
});
prop_assert_eq!(out_nodata.status, SettlementStatus::NoData);
prop_assert!(out_nodata.settlement_eur.is_none(), "NoData must have None settlement_eur");
let out_pricemissing = calculate_settlement(&SettleInput {
scheme: SettlementScheme::MarketPremium {
direktverm_aw_ct: rate,
wind_korrekturfaktor: None,
wind_standort: None,
},
einspeisemenge_kwh: Some(Decimal::from(100)),
marktwert_ct_kwh: None, ..SettleInput::default()
});
prop_assert_eq!(out_pricemissing.status, SettlementStatus::PriceMissing);
prop_assert!(out_pricemissing.settlement_eur.is_none(), "PriceMissing must have None settlement_eur");
}
}
proptest! {
#[test]
fn inv9_tenant_electricity_non_negative(
kwh in arb_kwh(),
verguetung_ct in arb_rate_ct(),
zuschlag_ct in arb_rate_ct(),
) {
let out = calculate_settlement(&SettleInput {
scheme: SettlementScheme::TenantElectricity {
verguetungssatz_ct: verguetung_ct,
mieter_zuschlag_ct: Some(zuschlag_ct),
},
einspeisemenge_kwh: Some(kwh),
..SettleInput::default()
});
if let Some(eur) = out.settlement_eur {
prop_assert!(
eur >= Decimal::ZERO,
"TenantElectricity returned negative EUR {eur}"
);
}
}
}
proptest! {
#[test]
fn inv10_post_eeg_with_nonneg_floor_is_nonneg(
kwh in arb_kwh(),
epex in arb_epex_ct(), floor in arb_epex_ct_nonneg(), ) {
let out = calculate_settlement(&SettleInput {
scheme: SettlementScheme::PostEeg {
price_floor: Some(floor),
},
einspeisemenge_kwh: Some(kwh),
marktwert_ct_kwh: Some(epex),
..SettleInput::default()
});
if let Some(eur) = out.settlement_eur {
prop_assert!(
eur >= Decimal::ZERO,
"PostEeg with floor={floor} returned negative EUR {eur} (epex={epex})"
);
}
}
}
proptest! {
#[test]
fn foerderendedatum_zero_rate_still_beendet(
year_offset in 0u32..=10u32,
) {
let inbetriebnahme = time::Date::from_calendar_date(
2000 + year_offset as i32,
time::Month::January,
1,
)
.unwrap();
let foerderendedatum = foerderendedatum_eeg(inbetriebnahme).unwrap();
let billing_after = foerderendedatum.replace_year(foerderendedatum.year() + 2).unwrap();
let out = calculate_settlement(&SettleInput {
scheme: SettlementScheme::FeedInTariff { verguetungssatz_ct: Decimal::ZERO },
einspeisemenge_kwh: Some(Decimal::from(1000)),
foerderendedatum: Some(foerderendedatum),
billing_date: Some(billing_after),
..SettleInput::default()
});
prop_assert_eq!(out.status, SettlementStatus::FoerderungBeendet);
}
}
proptest! {
#[test]
fn feed_in_tariff_zero_kwh_is_zero_eur(rate in arb_rate_ct()) {
let out = calculate_settlement(&SettleInput {
scheme: SettlementScheme::FeedInTariff { verguetungssatz_ct: rate },
einspeisemenge_kwh: Some(Decimal::ZERO),
..SettleInput::default()
});
prop_assert_eq!(out.status, SettlementStatus::Calculated);
prop_assert_eq!(out.settlement_eur, Some(Decimal::ZERO));
}
}