use time::Date;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum SettlementPeriodState {
Active,
Reduced,
Suspended,
Interrupted,
PostEeg,
Ended,
}
impl SettlementPeriodState {
#[must_use]
pub fn is_payable(self) -> bool {
matches!(
self,
Self::Active | Self::Reduced | Self::PostEeg | Self::Interrupted
)
}
#[must_use]
pub fn requires_operator_action(self) -> bool {
matches!(self, Self::Suspended | Self::Reduced)
}
#[must_use]
pub fn is_terminal(self) -> bool {
self == Self::Ended
}
#[must_use]
pub fn to_db_str(self) -> &'static str {
match self {
Self::Active => "active",
Self::Reduced => "reduced",
Self::Suspended => "suspended",
Self::Interrupted => "interrupted",
Self::PostEeg => "post_eeg",
Self::Ended => "ended",
}
}
pub fn from_db_str(s: &str) -> Result<Self, InvalidSettlementPeriodState> {
match s {
"active" => Ok(Self::Active),
"reduced" => Ok(Self::Reduced),
"suspended" => Ok(Self::Suspended),
"interrupted" => Ok(Self::Interrupted),
"post_eeg" => Ok(Self::PostEeg),
"ended" => Ok(Self::Ended),
other => Err(InvalidSettlementPeriodState(other.to_owned())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("invalid settlement_period_state: '{0}'")]
pub struct InvalidSettlementPeriodState(pub String);
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct StateTransition {
pub from: SettlementPeriodState,
pub to: SettlementPeriodState,
pub effective_from: Date,
pub reason: StateTransitionReason,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum StateTransitionReason {
InitialCommissioning,
MastrRegistered,
FernsteuerbarkeitmInstalled,
DirektvermarktungStarted,
DirektvermarktungEnded,
Sect52ViolationDetected,
Sect52ViolationResolved,
FoerderungExpired,
PostEegStarted,
Decommissioned,
Repowering,
}
#[must_use]
pub fn derive_settlement_state(
mastr_registriert: bool,
fernsteuerbarkeit_datum: Option<Date>,
leistung_kwp: rust_decimal::Decimal,
foerderendedatum: Option<Date>,
billing_date: Date,
eeg_gesetz_year: i16,
) -> SettlementPeriodState {
use rust_decimal_macros::dec;
if let Some(fed) = foerderendedatum
&& billing_date > fed
{
return SettlementPeriodState::PostEeg;
}
if !mastr_registriert {
return if eeg_gesetz_year >= 2023 {
SettlementPeriodState::Reduced
} else {
SettlementPeriodState::Suspended
};
}
let fernsteuerbarkeit_required = leistung_kwp >= dec!(25);
if fernsteuerbarkeit_required && fernsteuerbarkeit_datum.is_none() {
return if eeg_gesetz_year >= 2023 {
SettlementPeriodState::Reduced
} else {
SettlementPeriodState::Reduced };
}
SettlementPeriodState::Active
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
use time::macros::date;
#[test]
fn db_roundtrip_all_states() {
let states = [
SettlementPeriodState::Active,
SettlementPeriodState::Reduced,
SettlementPeriodState::Suspended,
SettlementPeriodState::Interrupted,
SettlementPeriodState::PostEeg,
SettlementPeriodState::Ended,
];
for s in states {
let db = s.to_db_str();
let parsed = SettlementPeriodState::from_db_str(db).unwrap();
assert_eq!(s, parsed, "roundtrip failed for {s:?}");
}
}
#[test]
fn unknown_db_str_returns_error() {
assert!(SettlementPeriodState::from_db_str("unknown").is_err());
}
#[test]
fn is_payable_states() {
assert!(SettlementPeriodState::Active.is_payable());
assert!(SettlementPeriodState::Reduced.is_payable());
assert!(SettlementPeriodState::PostEeg.is_payable());
assert!(SettlementPeriodState::Interrupted.is_payable());
assert!(!SettlementPeriodState::Suspended.is_payable());
assert!(!SettlementPeriodState::Ended.is_payable());
}
#[test]
fn derive_active_healthy_plant() {
let state = derive_settlement_state(
true,
Some(date!(2024 - 01 - 01)),
dec!(50),
Some(date!(2040 - 12 - 31)),
date!(2026 - 07 - 01),
2023,
);
assert_eq!(state, SettlementPeriodState::Active);
}
#[test]
fn derive_post_eeg_expired() {
let state = derive_settlement_state(
true,
None,
dec!(50),
Some(date!(2020 - 12 - 31)),
date!(2026 - 07 - 01),
2023,
);
assert_eq!(state, SettlementPeriodState::PostEeg);
}
#[test]
fn derive_reduced_eeg2023_mastr_missing() {
let state = derive_settlement_state(
false,
None,
dec!(50),
Some(date!(2040 - 12 - 31)),
date!(2026 - 07 - 01),
2023,
);
assert_eq!(state, SettlementPeriodState::Reduced);
}
#[test]
fn derive_suspended_eeg2017_mastr_missing() {
let state = derive_settlement_state(
false,
None,
dec!(50),
Some(date!(2040 - 12 - 31)),
date!(2026 - 07 - 01),
2017,
);
assert_eq!(state, SettlementPeriodState::Suspended);
}
#[test]
fn derive_reduced_fernsteuerbarkeit_missing_eeg2023() {
let state = derive_settlement_state(
true,
None,
dec!(50), Some(date!(2040 - 12 - 31)),
date!(2026 - 07 - 01),
2023,
);
assert_eq!(state, SettlementPeriodState::Reduced);
}
#[test]
fn derive_active_small_plant_no_fernsteuerbarkeit_needed() {
let state = derive_settlement_state(
true,
None,
dec!(5),
Some(date!(2040 - 12 - 31)),
date!(2026 - 07 - 01),
2023,
);
assert_eq!(state, SettlementPeriodState::Active);
}
}