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}