use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
mod date_option_serde {
use serde::{Deserialize, Deserializer, Serializer};
use time::Date;
use time::macros::format_description;
const FORMAT: &[time::format_description::FormatItem<'static>] =
format_description!("[year]-[month]-[day]");
pub fn serialize<S: Serializer>(date: &Option<Date>, s: S) -> Result<S::Ok, S::Error> {
match date {
Some(d) => s.serialize_some(&d.format(FORMAT).map_err(serde::ser::Error::custom)?),
None => s.serialize_none(),
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Date>, D::Error> {
let opt: Option<String> = Option::deserialize(d)?;
match opt {
None => Ok(None),
Some(s) => Date::parse(&s, FORMAT)
.map(Some)
.map_err(serde::de::Error::custom),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EnergieQuellen {
#[serde(default)]
pub erneuerbar_pct: Decimal,
#[serde(default)]
pub nuclear_pct: Option<Decimal>,
#[serde(default)]
pub fossil_pct: Option<Decimal>,
pub co2_g_per_kwh: Decimal,
#[serde(default)]
pub hkn_certified: bool,
#[serde(default)]
pub hkn_country: Option<String>,
#[serde(default)]
pub beschreibung: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SeasonalPriceOverride {
pub from_month: u8,
pub to_month: u8,
#[serde(default)]
pub arbeitspreis_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub gas_arbeitspreis_ct_per_kwh_hs: Option<Decimal>,
#[serde(default)]
pub label: Option<String>,
}
impl SeasonalPriceOverride {
#[must_use]
pub fn contains_month(&self, month: u8) -> bool {
if self.from_month <= self.to_month {
month >= self.from_month && month <= self.to_month
} else {
month >= self.from_month || month <= self.to_month
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct BlockTierInput {
pub bis_kwh: Option<Decimal>,
pub preis_ct_per_kwh: Decimal,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct IndexedPriceConfig {
pub base_ct_per_kwh: Decimal,
pub spread_ct_per_kwh: Decimal,
pub index_name: String,
pub index_value: Option<Decimal>,
pub factor_ct_per_unit: Decimal,
}
impl IndexedPriceConfig {
#[must_use]
pub fn effective_ct_per_kwh(&self) -> Option<Decimal> {
let idx = self.index_value?;
Some(self.base_ct_per_kwh + self.spread_ct_per_kwh + idx * self.factor_ct_per_unit)
}
#[must_use]
pub fn position_description(&self) -> String {
match self.index_value {
Some(v) => format!(
"Arbeitspreis {} ({:.4} + {:.4} + {:.4}\u{d7}{:.4}\u{202f}ct/kWh)",
self.index_name,
self.base_ct_per_kwh,
self.spread_ct_per_kwh,
self.factor_ct_per_unit,
v,
),
None => format!(
"Arbeitspreis {} (Indexwert nicht verf\u{fc}gbar)",
self.index_name
),
}
}
}
fn solar_stromsteuer_default() -> crate::steuer::StromsteuerTarif {
crate::steuer::StromsteuerTarif::Befreiung {
grund: crate::steuer::StromsteuerBefreiung::Kleinanlage,
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ElectricityProduct {
#[serde(default)]
pub product_code: Option<String>,
#[serde(default)]
pub grundpreis_ct_per_day: Option<Decimal>,
#[serde(default)]
pub arbeitspreis_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub arbeitspreis_ht_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub arbeitspreis_nt_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub register_count: Option<serde_json::Value>,
#[serde(default)]
pub leistungspreis_strom_ct_per_kw_month: Option<Decimal>,
#[serde(default)]
pub block_tiers: Option<Vec<BlockTierInput>>,
#[serde(default)]
pub indexed_price: Option<IndexedPriceConfig>,
#[serde(default)]
pub seasonal_prices: Option<Vec<SeasonalPriceOverride>>,
#[serde(default)]
pub dynamic_epex: bool,
#[serde(default)]
pub dynamic_epex_floor_ct_kwh: Option<Decimal>,
#[serde(default)]
pub dynamic_epex_cap_ct_kwh: Option<Decimal>,
#[serde(default)]
pub dynamic_aufschlag_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub dynamic_price_source: Option<String>,
#[serde(default)]
pub auf_abschlag_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub auf_abschlag_eur_per_month: Option<Decimal>,
#[serde(default)]
pub sofortbonus_eur: Option<Decimal>,
#[serde(default)]
pub treuebonus_eur_per_year: Option<Decimal>,
#[serde(default)]
pub msb_gebuehr_ct_per_day: Option<Decimal>,
#[serde(default)]
pub stromsteuer_tarif: crate::steuer::StromsteuerTarif,
#[serde(default)]
pub steuerentlastungen: Vec<crate::steuer::Steuerentlastung>,
#[serde(default)]
pub mwst_rate_override: Option<Decimal>,
#[serde(default)]
pub stromsteuer_ct_per_kwh_override: Option<Decimal>,
#[serde(default, with = "date_option_serde")]
pub preisgarantie_bis: Option<time::Date>,
#[serde(default)]
pub minimum_invoice_eur_brutto: Option<Decimal>,
#[serde(default)]
pub anlage_kwp: Option<Decimal>,
#[serde(default)]
pub energiequellen: Option<EnergieQuellen>,
#[serde(default)]
pub eeg_gutschrift_eur: Option<Decimal>,
#[serde(default)]
pub eeg_gutschrift_kleinunternehmer_19_ustg: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ControllableLoadProduct {
#[serde(flatten)]
pub base: ElectricityProduct,
#[serde(default)]
pub sect14a_modul2_nne_reduktion_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub sect14a_modul3_nne_ht_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub sect14a_modul3_nne_st_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub sect14a_modul3_nne_nt_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub sect14a_steuerungsentschaedigung_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub sect14a_modul1_pauschale_eur_per_year: Option<Decimal>,
#[serde(default)]
pub sect14a_steuerungsentschaedigung_eur_per_kw_year: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GasProduct {
#[serde(default)]
pub product_code: Option<String>,
#[serde(default)]
pub gas_grundpreis_ct_per_day: Option<Decimal>,
#[serde(default)]
pub gas_arbeitspreis_ct_per_kwh_hs: Option<Decimal>,
#[serde(default)]
pub gas_leistungspreis_ct_per_kw_month: Option<Decimal>,
#[serde(default)]
pub gas_indexed_price: Option<IndexedPriceConfig>,
#[serde(default)]
pub seasonal_prices: Option<Vec<SeasonalPriceOverride>>,
#[serde(default)]
pub energiesteuer_tarif: crate::steuer::EnergiesteuerTarif,
#[serde(default)]
pub steuerentlastungen: Vec<crate::steuer::Steuerentlastung>,
#[serde(default)]
pub auf_abschlag_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub auf_abschlag_eur_per_month: Option<Decimal>,
#[serde(default)]
pub mwst_rate_override: Option<Decimal>,
#[serde(default)]
pub energiesteuer_gas_ct_per_kwh_override: Option<Decimal>,
#[serde(default)]
pub behg_gas_ct_per_kwh_override: Option<Decimal>,
#[serde(default, with = "date_option_serde")]
pub preisgarantie_bis: Option<time::Date>,
#[serde(default)]
pub minimum_invoice_eur_brutto: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HeatProduct {
#[serde(default)]
pub product_code: Option<String>,
#[serde(default)]
pub waerme_grundpreis_eur_per_month: Option<Decimal>,
#[serde(default)]
pub waerme_leistungspreis_eur_per_kw_year: Option<Decimal>,
#[serde(default)]
pub waerme_leistungspreis_eur_per_kw_month: Option<Decimal>,
#[serde(default)]
pub waerme_arbeitspreis_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub waerme_indexed_price: Option<IndexedPriceConfig>,
#[serde(default)]
pub waerme_erneuerbar_anteil_pct: Option<Decimal>,
#[serde(default)]
pub waerme_co2_kosten_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub waerme_co2_emission_g_per_kwh: Option<Decimal>,
#[serde(default)]
pub mwst_rate_override: Option<Decimal>,
#[serde(default)]
pub minimum_invoice_eur_brutto: Option<Decimal>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AbwasserRegime {
#[default]
PublicLawFee,
PrivateLawCharge,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct WaterProduct {
#[serde(default)]
pub product_code: Option<String>,
#[serde(default)]
pub wasser_grundpreis_eur_per_month: Option<Decimal>,
#[serde(default)]
pub wasser_mengenpreis_eur_per_m3: Option<Decimal>,
#[serde(default)]
pub schmutzwasser_eur_per_m3: Option<Decimal>,
#[serde(default)]
pub niederschlagswasser_eur_per_m2_year: Option<Decimal>,
#[serde(default)]
pub abwasser_regime: AbwasserRegime,
#[serde(default)]
pub mwst_rate_override: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SolarProduct {
#[serde(default)]
pub product_code: Option<String>,
#[serde(default)]
pub solar_arbeitspreis_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub arbeitspreis_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub grundversorgung_arbeitspreis_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub gemeinschaft_rabatt_ct_per_kwh: Option<Decimal>,
#[serde(default = "solar_stromsteuer_default")]
pub stromsteuer_tarif: crate::steuer::StromsteuerTarif,
#[serde(default)]
pub anlage_kwp: Option<Decimal>,
#[serde(default)]
pub kleinunternehmer_19_ustg: bool,
#[serde(default)]
pub mwst_rate_override: Option<Decimal>,
#[serde(default)]
pub minimum_invoice_eur_brutto: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EegProduct {
#[serde(default)]
pub product_code: Option<String>,
#[serde(default)]
pub eeg_verguetungssatz_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub eeg_marktpraemie_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub eeg_managementpraemie_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub kwkg_zuschlag_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub anlage_kwp: Option<Decimal>,
#[serde(default)]
pub kleinunternehmer_19_ustg: bool,
#[serde(default)]
pub mwst_rate_override: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EinspeisungProduct {
#[serde(default)]
pub product_code: Option<String>,
#[serde(default)]
pub marktwert_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub vermarktungsgebuehr_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub kleinunternehmer_19_ustg: bool,
#[serde(default)]
pub mwst_rate_override: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HemsProduct {
#[serde(default)]
pub product_code: Option<String>,
#[serde(default)]
pub hems_subscription_eur_per_month: Option<Decimal>,
#[serde(default)]
pub hems_optimization_event_eur: Option<Decimal>,
#[serde(default)]
pub hems_readout_event_eur: Option<Decimal>,
#[serde(default)]
pub mwst_rate_override: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EmobilityProduct {
#[serde(default)]
pub product_code: Option<String>,
#[serde(default)]
pub emobility_service_fee_eur: Option<Decimal>,
#[serde(default)]
pub emobility_kwh_price_ct: Option<Decimal>,
#[serde(default)]
pub emobility_session_fee_eur: Option<Decimal>,
#[serde(default)]
pub emobility_roaming_fee_eur: Option<Decimal>,
#[serde(default)]
pub mwst_rate_override: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ServiceProduct {
#[serde(default)]
pub product_code: Option<String>,
#[serde(default)]
pub service_fee_eur: Option<Decimal>,
#[serde(default)]
pub service_event_price_eur: Option<Decimal>,
#[serde(default)]
pub mwst_rate_override: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SharingProduct {
#[serde(flatten)]
pub electricity: ElectricityProduct,
#[serde(default)]
pub sharing_credit_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub sharing_description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "category")]
pub enum Product {
#[serde(rename = "STROM")]
Strom(ElectricityProduct),
#[serde(rename = "WAERMEPUMPE")]
Waermepumpe(ControllableLoadProduct),
#[serde(rename = "WALLBOX")]
Wallbox(ControllableLoadProduct),
#[serde(rename = "GAS")]
Gas(GasProduct),
#[serde(rename = "WAERME")]
Waerme(HeatProduct),
#[serde(rename = "WASSER")]
Wasser(WaterProduct),
#[serde(rename = "SOLAR")]
Solar(SolarProduct),
#[serde(rename = "EEG")]
Eeg(EegProduct),
#[serde(rename = "EINSPEISUNG")]
Einspeisung(EinspeisungProduct),
#[serde(rename = "HEMS")]
Hems(HemsProduct),
#[serde(rename = "EMOBILITY")]
Emobility(EmobilityProduct),
#[serde(rename = "ENERGIEDIENSTLEISTUNG")]
Energiedienstleistung(ServiceProduct),
#[serde(rename = "SHARING")]
Sharing(SharingProduct),
}
impl Product {
#[must_use]
pub fn category_str(&self) -> &'static str {
match self {
Self::Strom(_) => "STROM",
Self::Waermepumpe(_) => "WAERMEPUMPE",
Self::Wallbox(_) => "WALLBOX",
Self::Gas(_) => "GAS",
Self::Waerme(_) => "WAERME",
Self::Wasser(_) => "WASSER",
Self::Solar(_) => "SOLAR",
Self::Eeg(_) => "EEG",
Self::Einspeisung(_) => "EINSPEISUNG",
Self::Hems(_) => "HEMS",
Self::Emobility(_) => "EMOBILITY",
Self::Energiedienstleistung(_) => "ENERGIEDIENSTLEISTUNG",
Self::Sharing(_) => "SHARING",
}
}
#[must_use]
pub fn product_code(&self) -> Option<&str> {
match self {
Self::Strom(p) => p.product_code.as_deref(),
Self::Waermepumpe(p) | Self::Wallbox(p) => p.base.product_code.as_deref(),
Self::Gas(p) => p.product_code.as_deref(),
Self::Waerme(p) => p.product_code.as_deref(),
Self::Wasser(p) => p.product_code.as_deref(),
Self::Solar(p) => p.product_code.as_deref(),
Self::Eeg(p) => p.product_code.as_deref(),
Self::Einspeisung(p) => p.product_code.as_deref(),
Self::Hems(p) => p.product_code.as_deref(),
Self::Emobility(p) => p.product_code.as_deref(),
Self::Energiedienstleistung(p) => p.product_code.as_deref(),
Self::Sharing(p) => p.electricity.product_code.as_deref(),
}
}
#[must_use]
pub fn energiequellen(&self) -> Option<&EnergieQuellen> {
match self {
Self::Strom(p) => p.energiequellen.as_ref(),
Self::Waermepumpe(p) | Self::Wallbox(p) => p.base.energiequellen.as_ref(),
_ => None,
}
}
#[must_use]
pub fn minimum_invoice_eur_brutto(&self) -> Option<Decimal> {
match self {
Self::Strom(p) => p.minimum_invoice_eur_brutto,
Self::Waermepumpe(p) | Self::Wallbox(p) => p.base.minimum_invoice_eur_brutto,
Self::Gas(p) => p.minimum_invoice_eur_brutto,
Self::Waerme(p) => p.minimum_invoice_eur_brutto,
Self::Solar(p) => p.minimum_invoice_eur_brutto,
_ => None,
}
}
#[must_use]
pub fn build_engine(
&self,
grid: &crate::quantities::GridInput,
rates: &crate::rates::RegulatoryRates,
) -> crate::engine::BillingEngine {
use crate::engine::BillingEngine;
use crate::providers::{
ControllableLoadProvider, DynamicElectricityProvider, EegProvider, EinspeisungProvider,
ElectricityProvider, EmobilityProvider, EnergyShareProvider, GasProvider, HeatProvider,
HemsProvider, MwStProvider, ServiceProvider, SolarProvider, WaterProvider,
};
match self {
Self::Strom(p) => {
let mwst = rates.effective_mwst_electricity(p);
if p.dynamic_epex {
BillingEngine::new()
.add(DynamicElectricityProvider::new(p.clone(), grid.clone()))
.add(MwStProvider::new(mwst))
} else {
BillingEngine::new()
.add(ElectricityProvider::new(p.clone(), grid.clone()))
.add(MwStProvider::new(mwst))
}
}
Self::Waermepumpe(p) | Self::Wallbox(p) => {
let mwst = rates.effective_mwst_electricity(&p.base);
BillingEngine::new()
.add(ControllableLoadProvider::new(p.clone(), grid.clone()))
.add(MwStProvider::new(mwst))
}
Self::Gas(p) => {
let mwst = p.mwst_rate_override.unwrap_or(rates.mwst_rate);
BillingEngine::new()
.add(GasProvider::new(p.clone(), grid.clone()))
.add(MwStProvider::new(mwst))
}
Self::Waerme(p) => {
let mwst = p.mwst_rate_override.unwrap_or(rates.mwst_rate);
BillingEngine::new()
.add(HeatProvider::new(p.clone()))
.add(MwStProvider::new(mwst))
}
Self::Wasser(p) => {
let mwst = p.mwst_rate_override.unwrap_or(rust_decimal::dec!(0.07));
BillingEngine::new()
.add(WaterProvider::new(p.clone()))
.add(MwStProvider::new(mwst))
}
Self::Solar(p) => {
let mwst = rates.effective_mwst_solar(p);
BillingEngine::new()
.add(SolarProvider::new(p.clone()))
.add(MwStProvider::new(mwst))
}
Self::Eeg(p) => {
let mwst = rates.effective_mwst_eeg(p);
BillingEngine::new()
.add(EegProvider::new(p.clone()))
.add(MwStProvider::new(mwst))
}
Self::Einspeisung(p) => {
let mwst = if let Some(r) = p.mwst_rate_override {
r
} else if p.kleinunternehmer_19_ustg {
rust_decimal::Decimal::ZERO
} else {
rates.mwst_rate
};
BillingEngine::new()
.add(EinspeisungProvider::new(p.clone()))
.add(MwStProvider::new(mwst))
}
Self::Hems(p) => {
let mwst = p.mwst_rate_override.unwrap_or(rates.mwst_rate);
BillingEngine::new()
.add(HemsProvider::new(p.clone()))
.add(MwStProvider::new(mwst))
}
Self::Emobility(p) => {
let mwst = p.mwst_rate_override.unwrap_or(rates.mwst_rate);
BillingEngine::new()
.add(EmobilityProvider::new(p.clone()))
.add(MwStProvider::new(mwst))
}
Self::Energiedienstleistung(p) => {
let mwst = p.mwst_rate_override.unwrap_or(rates.mwst_rate);
BillingEngine::new()
.add(ServiceProvider::new(p.clone()))
.add(MwStProvider::new(mwst))
}
Self::Sharing(p) => {
let mwst = rates.effective_mwst_electricity(&p.electricity);
BillingEngine::new()
.add(ElectricityProvider::new(
p.electricity.clone(),
grid.clone(),
))
.add(EnergyShareProvider::new(p.clone()))
.add(MwStProvider::new(mwst))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn product_strom_roundtrip() {
let json = r#"{"category":"STROM","arbeitspreis_ct_per_kwh":"28.5","grundpreis_ct_per_day":"8.0"}"#;
let p: Product = serde_json::from_str(json).unwrap();
match &p {
Product::Strom(e) => {
assert_eq!(e.arbeitspreis_ct_per_kwh, Some(rust_decimal::dec!(28.5)));
}
_ => panic!("expected Strom"),
}
let json2 = serde_json::to_string(&p).unwrap();
let p2: Product = serde_json::from_str(&json2).unwrap();
assert_eq!(p.category_str(), p2.category_str());
}
#[test]
fn product_waermepumpe_flattens_electricity_base() {
let json = r#"{"category":"WAERMEPUMPE","arbeitspreis_ct_per_kwh":"20.0","sect14a_modul2_nne_reduktion_ct_per_kwh":"1.5"}"#;
let p: Product = serde_json::from_str(json).unwrap();
match p {
Product::Waermepumpe(c) => {
assert_eq!(
c.base.arbeitspreis_ct_per_kwh,
Some(rust_decimal::dec!(20.0))
);
assert_eq!(
c.sect14a_modul2_nne_reduktion_ct_per_kwh,
Some(rust_decimal::dec!(1.5))
);
}
_ => panic!("expected Waermepumpe"),
}
}
#[test]
fn product_gas_roundtrip() {
let json = r#"{"category":"GAS","gas_arbeitspreis_ct_per_kwh_hs":"7.5","gas_grundpreis_ct_per_day":"5.0"}"#;
let p: Product = serde_json::from_str(json).unwrap();
match p {
Product::Gas(g) => {
assert_eq!(
g.gas_arbeitspreis_ct_per_kwh_hs,
Some(rust_decimal::dec!(7.5))
);
}
_ => panic!("expected Gas"),
}
}
#[test]
fn product_unknown_category_errors() {
let json = r#"{"category":"UNKNOWN_PRODUCT","foo":1}"#;
assert!(serde_json::from_str::<Product>(json).is_err());
}
}