use crate::scheme::{SettlementScheme, SettlementType, TariffSource};
use crate::technology::ErzeugungsArt;
use crate::version::EegGesetz;
use rust_decimal::Decimal;
use time::Date;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum SanktionAlt {
VerguetungAufNull,
VerguetungAufMarktwert,
VerguetungReduziert20Prozent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum SanktionsTyp {
FernsteuerbarkeitFehlend,
SpeicherAnforderungNichtErfuellt,
IMssAnforderungNichtErfuellt,
Sect10bVorgabenVerletzt,
MastrNichtRegistriert,
InbetriebnahmeVorgabeVerletzt,
VolleinspeisungspflichtVerletzt,
AusfallverguetungHoechstdauerUeberschritten,
EinspeiseverguetungUnzulaessigeNutzung,
VeraeusserungsformWechselUngueltig,
VeraeusserungsformNachweispflichtVerletzt,
ZuordnungsWechselNichtGemeldet,
DoppelvermarktungsverbotVerletzt,
}
impl SanktionsTyp {
pub const ALL: [Self; 13] = [
Self::FernsteuerbarkeitFehlend,
Self::SpeicherAnforderungNichtErfuellt,
Self::IMssAnforderungNichtErfuellt,
Self::Sect10bVorgabenVerletzt,
Self::AusfallverguetungHoechstdauerUeberschritten,
Self::EinspeiseverguetungUnzulaessigeNutzung,
Self::VeraeusserungsformWechselUngueltig,
Self::VeraeusserungsformNachweispflichtVerletzt,
Self::ZuordnungsWechselNichtGemeldet,
Self::InbetriebnahmeVorgabeVerletzt,
Self::VolleinspeisungspflichtVerletzt,
Self::MastrNichtRegistriert,
Self::DoppelvermarktungsverbotVerletzt,
];
#[must_use]
pub fn nummer(self) -> &'static str {
match self {
Self::FernsteuerbarkeitFehlend => "1",
Self::SpeicherAnforderungNichtErfuellt => "2",
Self::IMssAnforderungNichtErfuellt => "3",
Self::Sect10bVorgabenVerletzt => "4",
Self::AusfallverguetungHoechstdauerUeberschritten => "5",
Self::EinspeiseverguetungUnzulaessigeNutzung => "6",
Self::VeraeusserungsformWechselUngueltig => "7",
Self::VeraeusserungsformNachweispflichtVerletzt => "8",
Self::ZuordnungsWechselNichtGemeldet => "9",
Self::InbetriebnahmeVorgabeVerletzt => "9a",
Self::VolleinspeisungspflichtVerletzt => "10",
Self::MastrNichtRegistriert => "11",
Self::DoppelvermarktungsverbotVerletzt => "12",
}
}
#[must_use]
pub fn as_db_str(self) -> &'static str {
match self {
Self::FernsteuerbarkeitFehlend => "FERNSTEUERBARKEIT_FEHLEND",
Self::SpeicherAnforderungNichtErfuellt => "SPEICHER_ANFORDERUNG_NICHT_ERFUELLT",
Self::IMssAnforderungNichtErfuellt => "I_MSS_ANFORDERUNG_NICHT_ERFUELLT",
Self::Sect10bVorgabenVerletzt => "SECT10B_VORGABEN_VERLETZT",
Self::AusfallverguetungHoechstdauerUeberschritten => {
"AUSFALLVERGUETUNG_HOECHSTDAUER_UEBERSCHRITTEN"
}
Self::EinspeiseverguetungUnzulaessigeNutzung => {
"EINSPEISEVERGUETUNG_UNZULAESSIGE_NUTZUNG"
}
Self::VeraeusserungsformWechselUngueltig => "VERAEUSSERUNGSFORM_WECHSEL_UNGUELTIG",
Self::VeraeusserungsformNachweispflichtVerletzt => {
"VERAEUSSERUNGSFORM_NACHWEISPFLICHT_VERLETZT"
}
Self::ZuordnungsWechselNichtGemeldet => "ZUORDNUNGS_WECHSEL_NICHT_GEMELDET",
Self::InbetriebnahmeVorgabeVerletzt => "INBETRIEBNAHME_VORGABE_VERLETZT",
Self::VolleinspeisungspflichtVerletzt => "VOLLEINSPEISUNGSPFLICHT_VERLETZT",
Self::MastrNichtRegistriert => "MASTR_NICHT_REGISTRIERT",
Self::DoppelvermarktungsverbotVerletzt => "DOPPELVERMARKTUNGSVERBOT_VERLETZT",
}
}
#[must_use]
pub fn from_db_str(s: &str) -> Option<Self> {
Self::ALL.into_iter().find(|t| t.as_db_str() == s)
}
#[must_use]
pub fn abs4_monate(self, monate_des_verstosses: u32) -> u32 {
match self {
Self::VeraeusserungsformWechselUngueltig => monate_des_verstosses.saturating_add(3),
Self::ZuordnungsWechselNichtGemeldet => monate_des_verstosses.saturating_add(1),
Self::DoppelvermarktungsverbotVerletzt => monate_des_verstosses.saturating_add(6),
Self::VolleinspeisungspflichtVerletzt => 12,
_ => monate_des_verstosses,
}
}
}
#[cfg(test)]
mod sanktionstyp_tests {
use super::SanktionsTyp;
#[test]
fn all_holds_every_variant_exactly_once() {
let mut nummern: Vec<&str> = SanktionsTyp::ALL.iter().map(|t| t.nummer()).collect();
nummern.sort_unstable();
nummern.dedup();
assert_eq!(nummern.len(), SanktionsTyp::ALL.len());
let mut tokens: Vec<&str> = SanktionsTyp::ALL.iter().map(|t| t.as_db_str()).collect();
tokens.sort_unstable();
tokens.dedup();
assert_eq!(tokens.len(), SanktionsTyp::ALL.len());
for t in SanktionsTyp::ALL {
assert_eq!(SanktionsTyp::from_db_str(t.as_db_str()), Some(t));
}
assert_eq!(SanktionsTyp::from_db_str("NOT_A_VIOLATION"), None);
}
#[test]
fn abs4_extends_only_the_four_nummern_it_names() {
let extended: Vec<&str> = SanktionsTyp::ALL
.iter()
.filter(|t| t.abs4_monate(2) != 2)
.map(|t| t.nummer())
.collect();
assert_eq!(extended, ["7", "9", "10", "12"]);
assert_eq!(
SanktionsTyp::VeraeusserungsformWechselUngueltig.abs4_monate(2),
5
);
assert_eq!(
SanktionsTyp::ZuordnungsWechselNichtGemeldet.abs4_monate(2),
3
);
assert_eq!(
SanktionsTyp::DoppelvermarktungsverbotVerletzt.abs4_monate(2),
8
);
assert_eq!(
SanktionsTyp::VolleinspeisungspflichtVerletzt.abs4_monate(2),
12
);
assert_eq!(
SanktionsTyp::VolleinspeisungspflichtVerletzt.abs4_monate(11),
12
);
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Pflichtverstoss {
pub typ: SanktionsTyp,
pub leistung_kw: Decimal,
pub monate_des_verstosses: u32,
pub beginn: Option<time::Date>,
pub nachtraeglich_erfuellt: bool,
pub technischer_defekt: bool,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CapacityBlock {
pub leistung_kwp: Decimal,
pub verguetungssatz_ct: Decimal,
pub inbetriebnahme: Date,
pub foerderendedatum: Date,
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SettleInput {
pub scheme: SettlementScheme,
pub tariff_source: TariffSource,
pub settlement_type: SettlementType,
pub einspeisemenge_kwh: Option<Decimal>,
pub marktwert_ct_kwh: Option<Decimal>,
pub sanktion: Option<SanktionAlt>,
pub pflichtverstoss: Vec<Pflichtverstoss>,
pub aw_reductions: crate::aw_reductions::AwReductionContext,
pub negative_price_quarter_hours: Option<u64>,
pub einspeisemanagement_kwh: Option<Decimal>,
pub biomasse: Option<crate::biomasse::BiomassSettlementData>,
pub billing_days_fraction: Option<Decimal>,
pub kwh_during_negative_epex: Option<Decimal>,
pub inbetriebnahme: Option<Date>,
pub inbetriebnahme_typ: crate::technology::InbetriebnahmeTyp,
pub leistung_kwp: Option<Decimal>,
pub foerderendedatum: Option<Date>,
pub billing_date: Option<Date>,
pub capacity_blocks: Vec<CapacityBlock>,
pub eeg_gesetz: EegGesetz,
pub erzeugungsart: Option<ErzeugungsArt>,
#[cfg_attr(feature = "serde", serde(default))]
pub aw_is_gross: bool,
pub biogas_sect44b_eligible_kwh: Option<Decimal>,
pub has_imesys: bool,
pub marktwert_kategorie: Option<crate::scheme::MarktpreisKategorie>,
pub sect51_optin_wirksam_ab: Option<Date>,
pub sect51_abs3_unreported_days: u32,
pub ist_pilotwindanlage: bool,
}
impl SettleInput {
#[must_use]
pub fn effective_eeg_gesetz(&self) -> EegGesetz {
if let crate::scheme::TariffSource::Transitional(rule) = &self.tariff_source
&& let Some(implied) = rule.implied_eeg_gesetz()
{
return implied;
}
self.eeg_gesetz
}
#[must_use]
pub fn negativpreis_regime(&self) -> crate::negativpreis::NegativpreisRegime {
use crate::negativpreis::NegativpreisRegime as R;
if let crate::scheme::TariffSource::Transitional(rule) = &self.tariff_source
&& let Some(implied) = rule.implied_eeg_gesetz()
{
return match implied {
EegGesetz::Eeg2017 => R::Eeg2017,
EegGesetz::Eeg2021 => R::Eeg2021,
EegGesetz::Eeg2023 => self.inbetriebnahme.map_or(R::Solarspitzen, |ibn| {
R::fuer_periode(ibn, self.sect51_optin_wirksam_ab, self.billing_date)
}),
_ => R::Keine,
};
}
if let Some(ibn) = self.inbetriebnahme {
return R::fuer_periode(ibn, self.sect51_optin_wirksam_ab, self.billing_date);
}
match self.eeg_gesetz {
EegGesetz::Eeg2017 => R::Eeg2017,
EegGesetz::Eeg2021 => R::Eeg2021,
EegGesetz::Eeg2023 => R::Solarspitzen,
_ => R::Keine,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SettleOutput {
pub settlement_eur: Option<Decimal>,
pub eligible_kwh: Option<Decimal>,
pub positions: Vec<SettlePosition>,
pub status: SettlementStatus,
pub pflichtzahlung_eur: Option<Decimal>,
pub pflichtzahlung_faelligkeitsdatum: Option<Date>,
pub verlaengerungsanspruch_qh: u64,
pub dezentrale_einspeisung_anspruch_verloren: bool,
pub billing_days_fraction_applied: Option<Decimal>,
pub faelligkeitsdatum: Option<Date>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SettlePosition {
pub description: String,
pub legal_basis: String,
pub kwh: Decimal,
pub rate_ct_kwh: Decimal,
pub eur: Decimal,
}
impl SettlePosition {
pub fn to_line_item(&self) -> billing::LineItem {
use billing::{LineItem, Quantity, RoundingStrategy, UnitPrice};
let rate_eur = self.rate_ct_kwh / rust_decimal::Decimal::from(100);
let mut builder = LineItem::for_usage(
&self.description,
Quantity::new(self.kwh, "kWh").with_code("KWH"),
UnitPrice::new(rate_eur, "EUR/kWh").rounded(6, RoundingStrategy::MidpointAwayFromZero),
)
.meta("legal_basis", self.legal_basis.as_str());
if self.legal_basis.contains("EEG") || self.legal_basis.contains("post-F\u{00f6}rderung") {
builder = builder.tag("eeg");
}
if self.legal_basis.contains("KWKG") {
builder = builder.tag("kwkg");
}
builder = match self.legal_basis.as_str() {
b if b.starts_with("\u{00a7}23a") || b.starts_with("\u{00a7}\u{00a7}22a") => {
builder.tag("marktpraemie")
}
"\u{00a7}21 Abs. 3 EEG 2023" => builder.tag("mieterstrom"),
b if b == "\u{00a7}50b EEG 2023" || b == "\u{00a7}50 EEG 2023" => {
builder.tag("flexibilitaet")
}
b if b.contains("post-F\u{00f6}rderung") => builder.tag("post-eeg-spot"),
"\u{00a7}7 KWKG 2023" => builder.tag("kwk-zuschlag"),
"\u{00a7}21 EEG 2023" => builder.tag("verguetung"),
_ => builder,
};
builder
.build()
.expect("SettlePosition always has a non-empty static description")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum SettlementStatus {
Calculated,
#[default]
NoData,
PriceMissing,
FoerderungBeendet,
JahreskontingentErschoepft,
Sanctioned,
KeinAnspruch,
}