Skip to main content

mako_emob/
error.rs

1//! What can go wrong forming a Modell-2 allocation.
2
3use rust_decimal::Decimal;
4use thiserror::Error;
5
6/// An invariant of Anlage 6 / the AWH „Zum Modell 2" that the caller's data
7/// breaks.
8///
9/// Every variant names the clause it enforces. None of them is a wire error:
10/// this crate does no I/O, so all of these are statements about the operator's
11/// own records.
12#[derive(Debug, Clone, PartialEq, Eq, Error)]
13#[non_exhaustive]
14pub enum EmobError {
15    /// A Bilanzierungsgebiet may begin, change or end only on the first of a
16    /// month (AWH Kap. 5.3.1.3: „Die Bildung und Änderung von einem BG erfolgt
17    /// nur zum Ersten eines Monats").
18    #[error("{was} must fall on the first of a month, got {date}")]
19    NotFirstOfMonth {
20        /// Which lifecycle instant was wrong.
21        was: &'static str,
22        /// The date the caller supplied.
23        date: time::Date,
24    },
25
26    /// A Ladepunktbetreiber holds at most one Bilanzierungsgebiet per Regelzone
27    /// (AWH Kap. 5.3).
28    #[error("the LPB already holds a Bilanzierungsgebiet in Regelzone {regelzone}")]
29    DoppeltesBilanzierungsgebiet {
30        /// The Regelzone that already has one.
31        regelzone: String,
32    },
33
34    /// The Übergabestelle's Anmeldedatum precedes the BG's Gültigkeitsbeginn
35    /// (AWH Kap. 2.1.2 Nr. 1: „Das Anmeldedatum darf nicht vor dem
36    /// Gültigkeitsbeginn des BG liegen").
37    #[error("Anmeldedatum {anmeldung} precedes the Gültigkeitsbeginn {bg_start} of the BG")]
38    AnmeldungVorBgBeginn {
39        /// The requested Anmeldedatum.
40        anmeldung: time::Date,
41        /// The BG's Gültigkeitsbeginn.
42        bg_start: time::Date,
43    },
44
45    /// The Anmeldedatum falls on or after the day the BG stops being valid.
46    ///
47    /// The AWH states only the lower bound, because a BG that has been
48    /// beendet is not a BG one can register against at all: a Marktlokation
49    /// balanced into an expired Bilanzierungsgebiet has no Bilanzkreis-
50    /// Zuordnung on the day it takes effect, and the BIKO has nothing to
51    /// settle it against (AWH Kap. 5.3.1.3, Anlage 6 §II).
52    #[error("Anmeldedatum {anmeldung} is not before the Gültigkeitsende {bg_ende} of the BG")]
53    AnmeldungNachBgEnde {
54        /// The requested Anmeldedatum.
55        anmeldung: time::Date,
56        /// The first day the BG is no longer valid.
57        bg_ende: time::Date,
58    },
59
60    /// A Kundenanlage carrying an EEG-geförderte Anlage was onboarded on the
61    /// BK6-24-267 path.
62    ///
63    /// The Beschluss is explicit that its scope excludes „Kundenanlagen mit
64    /// EEG-geförderten Anlagen oder anderweitig komplexen Strukturen"
65    /// (S. 28). It does not decide the case either way, so the model cannot be
66    /// applied to it without a bilateral agreement — and applying it anyway
67    /// would balance an EEG-vergütete Einspeisung into the LPB's BG.
68    #[error(
69        "BK6-24-267 S. 28 excludes a Kundenanlage with an EEG-geförderte Anlage from \
70         individueller Netzzugang; record a bilateral agreement before onboarding"
71    )]
72    EegAnlageAusserhalbDesBeschlusses,
73
74    /// The Übergabestelle is not quarter-hour metered.
75    ///
76    /// Anlage 6 §III.1 makes ¼-h measurement — a Zählerstandsgang or
77    /// registrierende Leistungsmessung — a precondition of the model. Without
78    /// it there is nothing to allocate against.
79    #[error("the Übergabestelle is not quarter-hour metered (Anlage 6 §III.1)")]
80    KeineViertelstundenmessung,
81
82    /// The conservation identity of Anlage 6 §IV.1 does not hold.
83    ///
84    /// Raised only where it is a *bug* — a caller that assembled a version by
85    /// hand and got the arithmetic wrong. The ordinary shortfall is not this:
86    /// it is the Deltamenge, which is a quantity with a Bilanzkreis of its own.
87    #[error("conservation broken in {slot}: NGZ {ngz} ≠ Σ {summe} + Δ {delta}")]
88    ErhaltungVerletzt {
89        /// The quarter hour, as an RFC 3339 instant.
90        slot: String,
91        /// The Netzgangzeitreihe value.
92        ngz: Decimal,
93        /// What the parts add up to.
94        summe: Decimal,
95        /// The Deltamenge that was recorded.
96        delta: Decimal,
97    },
98
99    /// A version already sealed as `Final` was edited.
100    ///
101    /// MaBiS Kap. 3.8.2 versions by Erstellungszeitpunkt; once a version has
102    /// settled it is immutable and a correction is a *new* version.
103    #[error("version {erstellungszeitpunkt} is final and cannot be edited")]
104    VersionIstFinal {
105        /// The sealed version's Erstellungszeitpunkt.
106        erstellungszeitpunkt: String,
107    },
108
109    /// A correction arrived after the MaBiS Korrekturfrist.
110    ///
111    /// MaBiS Kap. 3.10 closes the window at the end of month M+7 relative to
112    /// the Bilanzierungsmonat.
113    #[error("corrections to Bilanzierungsmonat {monat} closed on {frist}; {eingang} is too late")]
114    KorrekturfristAbgelaufen {
115        /// The Bilanzierungsmonat, as `YYYY-MM`.
116        monat: String,
117        /// The last day corrections were accepted.
118        frist: time::Date,
119        /// When the correction arrived.
120        eingang: time::Date,
121    },
122
123    /// Two claims in one quarter hour name the same virtual Marktlokation.
124    ///
125    /// Refused rather than summed. A MaLo appearing twice produces two
126    /// [`crate::allocation::Zuordnung`] rows for one Bilanzkreis-Zuordnung,
127    /// and every downstream consumer that keys by MaLo — the BK-SZR
128    /// aggregation above all — silently keeps one of them. Merge the claims
129    /// upstream, where the reason they are two is still known.
130    #[error("virtual Marktlokation {malo} claims the same quarter hour twice")]
131    DoppelterAnspruch {
132        /// The Marktlokation that appears more than once.
133        malo: String,
134    },
135
136    /// A Ladevorgang spans more quarter hours than a session can.
137    ///
138    /// Not a regulatory bound but a corruption guard: the split walks one
139    /// slot at a time, so an `ende` that a backend got wrong by a factor of a
140    /// thousand would otherwise allocate until the process dies. A year of
141    /// quarter hours is already three orders of magnitude past the longest
142    /// real charging session.
143    #[error("session {id} spans {slots} quarter hours, more than the {max} a session may")]
144    LadevorgangZuLang {
145        /// The session's own id.
146        id: String,
147        /// How many quarter hours it would span.
148        slots: u64,
149        /// The most a session may span.
150        max: u64,
151    },
152
153    /// The allocation could not be formed at all.
154    #[error("allocation failed: {0}")]
155    Allocation(String),
156}
157
158impl From<metering::allocation::AllocationError> for EmobError {
159    fn from(e: metering::allocation::AllocationError) -> Self {
160        Self::Allocation(e.to_string())
161    }
162}