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),
}
}
}
fn default_category() -> String {
"STROM".to_owned()
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct TariffInput {
#[serde(default = "default_category")]
pub category: String,
#[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 sect14a_modul1_nne_reduktion_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub sect14a_modul3_entschaedigung_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub steuerungsrabatt_modul1_eur_per_kw_year: Option<Decimal>,
#[serde(default)]
pub steuerungsrabatt_modul3_eur_per_kw_year: 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 gas_grundpreis_ct_per_day: Option<Decimal>,
#[serde(default)]
pub gas_arbeitspreis_ct_per_kwh_hs: Option<Decimal>,
#[serde(default)]
pub gas_energiesteuer_befreiung: bool,
#[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_is_renewable: bool,
#[serde(default)]
pub solar_arbeitspreis_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub mieterstrom_aufschlag_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub gemeinschaft_rabatt_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub solar_include_stromsteuer: bool,
#[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 marktwert_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub vermarktungsgebuehr_ct_per_kwh: Option<Decimal>,
#[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 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 service_fee_eur: Option<Decimal>,
#[serde(default)]
pub service_event_price_eur: Option<Decimal>,
#[serde(default)]
pub dynamic_epex: bool,
#[serde(default)]
pub dynamic_epex_floor_ct_kwh: Option<Decimal>,
#[serde(default)]
pub stromsteuer_ct_per_kwh_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)]
pub mwst_rate_override: Option<Decimal>,
#[serde(default)]
pub auf_abschlag_ct_per_kwh: Option<Decimal>,
#[serde(default)]
pub auf_abschlag_eur_per_month: Option<Decimal>,
#[serde(default)]
pub msb_gebuehr_ct_per_day: Option<Decimal>,
#[serde(default)]
pub minimum_invoice_eur_brutto: 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 anlage_kwp: Option<Decimal>,
#[serde(default)]
pub industrie_stromsteuer_befreiung: bool,
#[serde(default, with = "date_option_serde")]
pub preisgarantie_bis: Option<time::Date>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct SeasonalPriceOverride {
pub from_month: u8,
pub to_month: u8,
#[serde(default)]
pub arbeitspreis_ct_per_kwh: Option<rust_decimal::Decimal>,
#[serde(default)]
pub gas_arbeitspreis_ct_per_kwh_hs: Option<rust_decimal::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, serde::Deserialize, serde::Serialize)]
pub struct BlockTierInput {
pub bis_kwh: Option<rust_decimal::Decimal>,
pub preis_ct_per_kwh: rust_decimal::Decimal,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct IndexedPriceConfig {
pub base_ct_per_kwh: rust_decimal::Decimal,
pub spread_ct_per_kwh: rust_decimal::Decimal,
pub index_name: String,
pub index_value: Option<rust_decimal::Decimal>,
pub factor_ct_per_unit: rust_decimal::Decimal,
}
impl IndexedPriceConfig {
#[must_use]
pub fn effective_ct_per_kwh(&self) -> Option<rust_decimal::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{00d7} {:.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{00fc}gbar)",
self.index_name
),
}
}
}
impl TariffInput {
pub fn build_engine(
&self,
grid: &crate::quantities::GridInput,
rates: &crate::rates::RegulatoryRates,
) -> Option<crate::engine::BillingEngine> {
use crate::engine::BillingEngine;
use crate::providers::{
DynamicElectricityProvider, EegProvider, EinspeisungProvider, ElectricityProvider,
EmobilityProvider, GasProvider, HeatProvider, HemsProvider, MwStProvider,
ServiceProvider, SolarProvider,
};
use std::collections::HashMap;
let mwst = rates.effective_mwst(self);
let engine = match self.category.as_str() {
"STROM" | "WAERMEPUMPE" | "WALLBOX" => {
if self.dynamic_epex {
BillingEngine::new()
.add(DynamicElectricityProvider::with_epex_map(
self.clone(),
grid.clone(),
HashMap::new(), ))
.add(MwStProvider::new(mwst))
} else {
BillingEngine::new()
.add(ElectricityProvider::from_tariff(self, grid))
.add(MwStProvider::new(mwst))
}
}
"GAS" => BillingEngine::new()
.add(GasProvider::from_tariff(self, grid))
.add(MwStProvider::new(mwst)),
"WAERME" => BillingEngine::new()
.add(HeatProvider::from_tariff(self))
.add(MwStProvider::new(mwst)),
"SOLAR" => BillingEngine::new()
.add(SolarProvider::from_tariff(self))
.add(MwStProvider::new(mwst)),
"EEG" => BillingEngine::new()
.add(EegProvider::from_tariff(self))
.add(MwStProvider::new(mwst)),
"EINSPEISUNG" => BillingEngine::new()
.add(EinspeisungProvider::from_tariff(self))
.add(MwStProvider::new(mwst)),
"HEMS" => BillingEngine::new()
.add(HemsProvider::from_tariff(self))
.add(MwStProvider::new(mwst)),
"EMOBILITY" => BillingEngine::new()
.add(EmobilityProvider::from_tariff(self))
.add(MwStProvider::new(mwst)),
"ENERGIEDIENSTLEISTUNG" => BillingEngine::new()
.add(ServiceProvider::from_tariff(self))
.add(MwStProvider::new(mwst)),
_ => return None,
};
Some(engine)
}
}
#[derive(Debug, Clone)]
pub enum PricingModel {
Electricity(TariffInput),
DynamicElectricity(TariffInput),
HeatPump(TariffInput),
Wallbox(TariffInput),
Gas(TariffInput),
Heat(TariffInput),
Solar(TariffInput),
Eeg(TariffInput),
Einspeisung(TariffInput),
Hems(TariffInput),
Emobility(TariffInput),
Service(TariffInput),
}
impl PricingModel {
#[must_use]
pub fn tariff(&self) -> &TariffInput {
match self {
Self::Electricity(t)
| Self::DynamicElectricity(t)
| Self::HeatPump(t)
| Self::Wallbox(t)
| Self::Gas(t)
| Self::Heat(t)
| Self::Solar(t)
| Self::Eeg(t)
| Self::Einspeisung(t)
| Self::Hems(t)
| Self::Emobility(t)
| Self::Service(t) => t,
}
}
#[must_use]
pub fn category_str(&self) -> &'static str {
match self {
Self::Electricity(_) | Self::DynamicElectricity(_) => "STROM",
Self::HeatPump(_) => "WAERMEPUMPE",
Self::Wallbox(_) => "WALLBOX",
Self::Gas(_) => "GAS",
Self::Heat(_) => "WAERME",
Self::Solar(_) => "SOLAR",
Self::Eeg(_) => "EEG",
Self::Einspeisung(_) => "EINSPEISUNG",
Self::Hems(_) => "HEMS",
Self::Emobility(_) => "EMOBILITY",
Self::Service(_) => "ENERGIEDIENSTLEISTUNG",
}
}
pub fn build_engine(
&self,
grid: &crate::quantities::GridInput,
rates: &crate::rates::RegulatoryRates,
) -> Result<crate::engine::BillingEngine, String> {
self.tariff()
.build_engine(grid, rates)
.ok_or_else(|| format!("unsupported product category: {}", self.category_str()))
}
}
impl TryFrom<TariffInput> for PricingModel {
type Error = String;
fn try_from(t: TariffInput) -> Result<Self, Self::Error> {
match t.category.as_str() {
"STROM" | "WAERMEPUMPE" | "WALLBOX" if t.dynamic_epex => {
Ok(Self::DynamicElectricity(t))
}
"STROM" => Ok(Self::Electricity(t)),
"WAERMEPUMPE" => Ok(Self::HeatPump(t)),
"WALLBOX" => Ok(Self::Wallbox(t)),
"GAS" => Ok(Self::Gas(t)),
"WAERME" => Ok(Self::Heat(t)),
"SOLAR" => Ok(Self::Solar(t)),
"EEG" => Ok(Self::Eeg(t)),
"EINSPEISUNG" => Ok(Self::Einspeisung(t)),
"HEMS" => Ok(Self::Hems(t)),
"EMOBILITY" => Ok(Self::Emobility(t)),
"ENERGIEDIENSTLEISTUNG" => Ok(Self::Service(t)),
"BUNDLE" => Err("BUNDLE requires component expansion before billing".to_owned()),
other => Err(format!("unknown product category: {other}")),
}
}
}
impl From<PricingModel> for TariffInput {
fn from(m: PricingModel) -> Self {
match m {
PricingModel::Electricity(t)
| PricingModel::DynamicElectricity(t)
| PricingModel::HeatPump(t)
| PricingModel::Wallbox(t)
| PricingModel::Gas(t)
| PricingModel::Heat(t)
| PricingModel::Solar(t)
| PricingModel::Eeg(t)
| PricingModel::Einspeisung(t)
| PricingModel::Hems(t)
| PricingModel::Emobility(t)
| PricingModel::Service(t) => t,
}
}
}