use rust_decimal::Decimal;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Sparte {
#[default]
Strom,
Gas,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KaKlasse {
TarifkundeLow,
TarifkundeMedium,
SonderkundeHigh,
Exempt,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuantityUnit {
Kwh,
Kw,
Monat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettlementType {
NneStrom,
NneGas,
NneSelbstausstellt,
MmmStrom,
MsbRechnung,
GasAwhSperrung,
RedispatchKostenblatt,
}
impl SettlementType {
#[must_use]
pub fn default_pid(self) -> u32 {
match self {
Self::NneStrom => 31001,
Self::NneGas => 31005,
Self::NneSelbstausstellt => 31006,
Self::MmmStrom => 31002,
Self::MsbRechnung => 31009,
Self::GasAwhSperrung => 31011,
Self::RedispatchKostenblatt => 0, }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettlementStatus {
Initial,
Correction,
Reversal,
Final,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LegalReference {
StromNev {
paragraph: &'static str,
},
GasNev {
paragraph: &'static str,
},
Kav {
paragraph: &'static str,
},
Sect14aEnwg {
module: u8,
},
MessZv {
paragraph: &'static str,
},
MsbG {
paragraph: &'static str,
},
BnetzaDecision {
reference: &'static str,
},
BdewAhb {
reference: &'static str,
},
StromNzv {
paragraph: &'static str,
},
GasNzv {
paragraph: &'static str,
},
Enwg {
paragraph: &'static str,
},
ARegV {
paragraph: &'static str,
},
}
impl LegalReference {
#[must_use]
pub fn citation(&self) -> String {
match self {
Self::StromNev { paragraph } => format!("StromNEV {paragraph}"),
Self::GasNev { paragraph } => format!("GasNEV {paragraph}"),
Self::Kav { paragraph } => format!("KAV {paragraph}"),
Self::Sect14aEnwg { module } => format!("§14a EnWG Modul {module}"),
Self::MessZv { paragraph } => format!("MessZV {paragraph}"),
Self::MsbG { paragraph } => format!("MsbG {paragraph}"),
Self::BnetzaDecision { reference } => format!("BNetzA {reference}"),
Self::BdewAhb { reference } => format!("BDEW {reference}"),
Self::StromNzv { paragraph } => format!("StromNZV {paragraph}"),
Self::GasNzv { paragraph } => format!("GasNZV {paragraph}"),
Self::Enwg { paragraph } => format!("EnWG {paragraph}"),
Self::ARegV { paragraph } => format!("ARegV {paragraph}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TariffSource {
PublishedTariffSheet {
sheet_id: String,
},
HistoricalTariff {
valid_from: time::Date,
},
RegulatoryTariff {
decision_ref: &'static str,
},
ContractTariff {
contract_ref: String,
},
ManualOverride {
reason: String,
},
}
#[derive(Debug, Clone)]
pub struct CalculationTrace {
pub explanation: String,
pub input_quantity: Decimal,
pub input_unit_price_eur: Decimal,
pub gross_eur: Decimal,
pub legal_refs: Vec<LegalReference>,
pub tariff_source: Option<TariffSource>,
pub regulatory_reduction_factor: Option<Decimal>,
pub rounding_note: Option<&'static str>,
}
#[derive(Debug, Clone)]
pub struct SettlementWarning {
pub severity: WarningSeverity,
pub code: &'static str,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum WarningSeverity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone)]
pub struct InvoicePosition {
pub number: u32,
pub text: String,
pub quantity: Decimal,
pub unit: QuantityUnit,
pub unit_price_eur: Decimal,
pub net_eur: Decimal,
pub trace: CalculationTrace,
}
#[derive(Debug, Clone)]
pub struct GridSettlement {
pub pid: u32,
pub settlement_type: SettlementType,
pub status: SettlementStatus,
pub rechnungsnummer: String,
pub correction_of: Option<String>,
pub invoice_date: time::Date,
pub due_date: time::Date,
pub period_from: time::Date,
pub period_to: time::Date,
pub nb_mp_id: String,
pub counterparty_mp_id: String,
pub positions: Vec<InvoicePosition>,
pub total_eur: Decimal,
pub warnings: Vec<SettlementWarning>,
}
pub type GridInvoice = GridSettlement;
impl GridSettlement {
#[must_use]
pub fn positions_count(&self) -> usize {
self.positions.len()
}
#[must_use]
pub fn is_clean(&self) -> bool {
!self
.warnings
.iter()
.any(|w| w.severity >= WarningSeverity::Warning)
}
#[must_use]
pub fn all_legal_refs(&self) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
self.positions
.iter()
.flat_map(|p| p.trace.legal_refs.iter().map(|r| r.citation()))
.filter(|c| seen.insert(c.clone()))
.collect()
}
#[must_use]
pub fn recomputed_total(&self) -> Decimal {
self.positions
.iter()
.map(|p| p.net_eur)
.sum::<Decimal>()
.round_dp(2)
}
}
#[derive(Debug, Clone)]
pub struct NneInput {
pub malo_id: String,
pub nb_mp_id: String,
pub lf_mp_id: String,
pub rechnungsnummer: String,
pub period_from: time::Date,
pub period_to: time::Date,
pub invoice_date: time::Date,
pub due_date: time::Date,
pub arbeitsmenge_kwh: Decimal,
pub arbeitspreis_ct_per_kwh: Decimal,
pub arbeitsmenge_ht_kwh: Option<Decimal>,
pub arbeitspreis_ht_ct_per_kwh: Option<Decimal>,
pub arbeitsmenge_nt_kwh: Option<Decimal>,
pub arbeitspreis_nt_ct_per_kwh: Option<Decimal>,
pub spitzenleistung_kw: Option<Decimal>,
pub leistungspreis_eur_per_kw: Option<Decimal>,
pub ka_satz_ct_per_kwh: Option<Decimal>,
pub tariff_sheet_id: Option<String>,
pub sparte: Sparte,
pub ka_klasse: Option<KaKlasse>,
}
#[derive(Debug, Clone)]
pub struct MmmInput {
pub malo_id: String,
pub nb_mp_id: String,
pub lf_mp_id: String,
pub rechnungsnummer: String,
pub period_from: time::Date,
pub period_to: time::Date,
pub invoice_date: time::Date,
pub due_date: time::Date,
pub sparte: Sparte,
pub actual_kwh: Decimal,
pub profil_kwh: Decimal,
pub mehr_preis_ct_per_kwh: Decimal,
pub minder_preis_ct_per_kwh: Decimal,
}
#[derive(Debug, Clone)]
pub struct MsbInput {
pub malo_id: String,
pub nb_mp_id: String,
pub msb_mp_id: String,
pub rechnungsnummer: String,
pub period_from: time::Date,
pub period_to: time::Date,
pub invoice_date: time::Date,
pub due_date: time::Date,
pub grundgebuehr_eur_per_month: Decimal,
pub billing_months: u32,
pub messdienstleistung_eur: Option<Decimal>,
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub is_valid: bool,
pub warnings: Vec<SettlementWarning>,
}
impl ValidationResult {
#[must_use]
pub fn ok() -> Self {
Self {
is_valid: true,
warnings: Vec::new(),
}
}
pub fn push(&mut self, w: SettlementWarning) {
if w.severity == WarningSeverity::Error {
self.is_valid = false;
}
self.warnings.push(w);
}
}
#[must_use]
pub fn validate_nne_input(input: &NneInput) -> ValidationResult {
let mut r = ValidationResult::ok();
if input.period_from >= input.period_to {
r.push(SettlementWarning {
severity: WarningSeverity::Error,
code: "INVALID_PERIOD",
message: "period_from must be strictly before period_to".to_owned(),
});
}
if input.arbeitsmenge_kwh < Decimal::ZERO {
r.push(SettlementWarning {
severity: WarningSeverity::Error,
code: "NEGATIVE_CONSUMPTION",
message: format!("arbeitsmenge_kwh is negative: {}", input.arbeitsmenge_kwh),
});
}
if input.arbeitspreis_ct_per_kwh < Decimal::ZERO {
r.push(SettlementWarning {
severity: WarningSeverity::Warning,
code: "NEGATIVE_ARBEITSPREIS",
message: format!(
"arbeitspreis_ct_per_kwh is negative: {}",
input.arbeitspreis_ct_per_kwh
),
});
}
if input.spitzenleistung_kw.is_some() != input.leistungspreis_eur_per_kw.is_some() {
r.push(SettlementWarning {
severity: WarningSeverity::Error,
code: "MISMATCHED_RLM_FIELDS",
message:
"spitzenleistung_kw and leistungspreis_eur_per_kw must both be set or both absent"
.to_owned(),
});
}
if input.sparte == Sparte::Gas && input.spitzenleistung_kw.is_some() {
r.push(SettlementWarning {
severity: WarningSeverity::Warning,
code: "GAS_WITH_LEISTUNG",
message: "Gas NNE typically does not use Leistungspreis — verify tariff configuration"
.to_owned(),
});
}
r
}
#[must_use]
pub fn validate_mmm_input(input: &MmmInput) -> ValidationResult {
let mut r = ValidationResult::ok();
if input.period_from >= input.period_to {
r.push(SettlementWarning {
severity: WarningSeverity::Error,
code: "INVALID_PERIOD",
message: "period_from must be strictly before period_to".to_owned(),
});
}
if input.mehr_preis_ct_per_kwh < Decimal::ZERO {
r.push(SettlementWarning {
severity: WarningSeverity::Warning,
code: "NEGATIVE_MEHR_PREIS",
message: format!(
"mehr_preis_ct_per_kwh is negative: {}",
input.mehr_preis_ct_per_kwh
),
});
}
if input.minder_preis_ct_per_kwh < Decimal::ZERO {
r.push(SettlementWarning {
severity: WarningSeverity::Warning,
code: "NEGATIVE_MINDER_PREIS",
message: format!(
"minder_preis_ct_per_kwh is negative: {}",
input.minder_preis_ct_per_kwh
),
});
}
r
}
#[must_use]
pub fn validate_msb_input(input: &MsbInput) -> ValidationResult {
let mut r = ValidationResult::ok();
if input.period_from >= input.period_to {
r.push(SettlementWarning {
severity: WarningSeverity::Error,
code: "INVALID_PERIOD",
message: "period_from must be strictly before period_to".to_owned(),
});
}
if input.grundgebuehr_eur_per_month < Decimal::ZERO {
r.push(SettlementWarning {
severity: WarningSeverity::Error,
code: "NEGATIVE_GRUNDGEBUEHR",
message: format!(
"grundgebuehr_eur_per_month is negative: {}",
input.grundgebuehr_eur_per_month
),
});
}
if input.billing_months == 0 {
r.push(SettlementWarning {
severity: WarningSeverity::Error,
code: "ZERO_BILLING_MONTHS",
message: "billing_months must be at least 1".to_owned(),
});
}
r
}