use time::{Date, Duration, Month};
pub const ACK_FRIST: Duration = Duration::minutes(3);
pub const VORABINFORMATION_PROGNOSEMODELL: Duration = Duration::minutes(30);
pub const AUSFALLARBEIT_FOLGEMONATE: u32 = 3;
pub const WETTERDATEN_WERKTAGE: u32 = 4;
pub const STAMMDATEN_GUELTIG_AB_MIN_WERKTAGE: u32 = 5;
pub const STAMMDATEN_GUELTIG_AB_MIN_WERKTAGE_LANG: u32 = 10;
pub const STAMMDATEN_GUELTIG_AB_MAX_JAHRE: i32 = 2;
pub const PLANWERT_UEBERFUEHRUNG_VORLAUF_MONATE: u32 = 6;
pub const PLANWERT_NEUE_SR_MITTEILUNG_WERKTAGE: u32 = 5;
pub const PLANWERT_NEUE_SR_INFORMATION_WERKTAGE: u32 = 10;
pub const PLANWERT_WIRKSAMKEITSTERMINE: [(Month, u8); 4] = [
(Month::January, 1),
(Month::April, 1),
(Month::July, 1),
(Month::October, 1),
];
#[must_use]
pub fn ist_wirksamkeitstermin(date: Date) -> bool {
PLANWERT_WIRKSAMKEITSTERMINE
.iter()
.any(|&(m, d)| date.month() == m && date.day() == d)
}
#[must_use]
pub fn ausfallarbeit_endet_am(massnahme_ende: Date) -> Date {
let mut d = massnahme_ende;
for _ in 0..AUSFALLARBEIT_FOLGEMONATE {
d = d
.replace_day(1)
.expect("day 1 exists in every month")
.checked_add(Duration::days(32))
.expect("date overflow")
.replace_day(1)
.expect("day 1 exists in every month");
}
let letzter = time::util::days_in_month(d.month(), d.year());
d.replace_day(letzter).expect("last day of a real month")
}
pub mod ack_regeln {
pub const EINE_ACK_JE_UEBERTRAGUNGSDATEI: bool = true;
pub const FEHLENDE_ACK_BEDEUTET_NICHT_BEARBEITET: bool = true;
pub const ACK_WIRD_NICHT_QUITTIERT: bool = true;
pub const VERSPAETETE_ACK_BRICHT_KEINE_GESCHAEFTSFRIST: bool = true;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AckErgebnis {
Positiv,
SyntaxfehlerXsd,
NichtVerarbeitbar,
}
impl AckErgebnis {
#[must_use]
pub fn wird_verarbeitet(self) -> bool {
self == Self::Positiv
}
#[must_use]
pub fn reason_code(self) -> &'static str {
match self {
Self::Positiv => "A01",
Self::SyntaxfehlerXsd | Self::NichtVerarbeitbar => "A02",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Betreiberfristen {
pub aktivierung_antwort: Duration,
pub vorabinformation_planwertmodell: Duration,
pub kostenblatt_stichtag: u8,
pub stammdaten_weiterleitung_werktage: u32,
}
impl Betreiberfristen {
#[must_use]
pub const fn historisch() -> Self {
Self {
aktivierung_antwort: Duration::minutes(5),
vorabinformation_planwertmodell: Duration::minutes(30),
kostenblatt_stichtag: 15,
stammdaten_weiterleitung_werktage: 1,
}
}
pub fn validate(self) -> Result<(), String> {
if !self.aktivierung_antwort.is_positive() {
return Err("aktivierung_antwort muss positiv sein".to_owned());
}
if !self.vorabinformation_planwertmodell.is_positive() {
return Err("vorabinformation_planwertmodell muss positiv sein".to_owned());
}
if !(1..=28).contains(&self.kostenblatt_stichtag) {
return Err(format!(
"kostenblatt_stichtag {} liegt außerhalb von 1..=28 — ein späterer Tag \
existiert im Februar nicht",
self.kostenblatt_stichtag
));
}
if self.stammdaten_weiterleitung_werktage == 0 {
return Err("stammdaten_weiterleitung_werktage muss mindestens 1 sein".to_owned());
}
Ok(())
}
}
impl Default for Betreiberfristen {
fn default() -> Self {
Self::historisch()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn d(y: i32, m: Month, day: u8) -> Date {
Date::from_calendar_date(y, m, day).expect("valid date")
}
#[test]
fn the_ack_frist_is_three_minutes() {
assert_eq!(ACK_FRIST, Duration::minutes(3));
assert!(ACK_FRIST < Duration::hours(6));
}
#[test]
fn ausfallarbeit_closes_at_the_end_of_the_third_following_month() {
assert_eq!(
ausfallarbeit_endet_am(d(2026, Month::January, 14)),
d(2026, Month::April, 30)
);
assert_eq!(
ausfallarbeit_endet_am(d(2026, Month::January, 31)),
d(2026, Month::April, 30)
);
assert_eq!(
ausfallarbeit_endet_am(d(2027, Month::November, 30)),
d(2028, Month::February, 29)
);
}
#[test]
fn only_the_four_quarter_days_are_wirksamkeitstermine() {
for (m, day) in PLANWERT_WIRKSAMKEITSTERMINE {
assert!(ist_wirksamkeitstermin(d(2027, m, day)));
}
for date in [
d(2027, Month::January, 2),
d(2027, Month::February, 1),
d(2027, Month::June, 1),
d(2027, Month::December, 31),
] {
assert!(!ist_wirksamkeitstermin(date), "{date}");
}
}
#[test]
fn a_positive_ack_is_the_only_one_that_processes() {
assert!(AckErgebnis::Positiv.wird_verarbeitet());
assert_eq!(AckErgebnis::Positiv.reason_code(), "A01");
for e in [AckErgebnis::SyntaxfehlerXsd, AckErgebnis::NichtVerarbeitbar] {
assert!(!e.wird_verarbeitet());
assert_eq!(e.reason_code(), "A02", "both negatives ride A02");
}
}
#[test]
fn the_historical_defaults_are_the_bk6_20_05x_figures() {
let f = Betreiberfristen::historisch();
assert_eq!(f.aktivierung_antwort, Duration::minutes(5));
assert_eq!(f.kostenblatt_stichtag, 15);
assert_eq!(f.stammdaten_weiterleitung_werktage, 1);
f.validate().expect("the historical figures are in range");
}
#[test]
fn a_kostenblatt_stichtag_that_february_does_not_have_is_refused() {
let mut f = Betreiberfristen::historisch();
f.kostenblatt_stichtag = 30;
assert!(f.validate().is_err());
f.kostenblatt_stichtag = 0;
assert!(f.validate().is_err());
f.kostenblatt_stichtag = 28;
assert!(f.validate().is_ok());
}
#[test]
fn a_non_positive_window_is_refused() {
let mut f = Betreiberfristen::historisch();
f.aktivierung_antwort = Duration::ZERO;
assert!(f.validate().is_err());
let mut f = Betreiberfristen::historisch();
f.stammdaten_weiterleitung_werktage = 0;
assert!(f.validate().is_err());
}
#[test]
fn the_prognosemodell_vorabinformation_is_the_only_sourced_abruf_window() {
assert_eq!(VORABINFORMATION_PROGNOSEMODELL, Duration::minutes(30));
}
}