mako_engine/marktrolle.rs
1//! BDEW Rollenmodell — market-participant role configuration.
2//!
3//! The BDEW Rollenmodell für die Marktkommunikation (V2.2, January 2026) explicitly
4//! permits a single legal entity to hold multiple market roles simultaneously.
5//! Common combinations:
6//!
7//! | Combination | Regulatory basis |
8//! |---|---|
9//! | NB + gMSB | §41 MsbG — NB is grundzuständiger MSB for basic meters |
10//! | NB + BKV | Stadtwerke managing their own balance group |
11//! | NB + LF | Vertically integrated utility |
12//! | LF + BKV | Supplier managing its own balance group |
13//!
14//! ## Why role-awareness matters for PID routing
15//!
16//! Several EDIFACT PIDs are **shared across process families** and their correct
17//! inbound destination depends on which role this `makod` instance fills:
18//!
19//! | PID | ORDRSP semantics |
20//! |---|---|
21//! | 19001 (Bestellbestätigung) | → `gpke-konfiguration` when NB receiving from MSB |
22//! | 19001 (Bestellbestätigung) | → `wim-geraeteubernahme` when nMSB receiving from NB |
23//! | 19015 (Bestätigung Gerätewechselabsicht) | → `wim-geraeteubernahme` when NB receiving from nMSB |
24//! | 13003 (MSCONS Summenzeitreihe) | → `mabis-billing` when BKV receiving from BIKO |
25//! | 13003 (MSCONS Summenzeitreihe) | → MaBiS NZR handler when NB receiving from NB |
26//!
27//! By declaring which roles a `makod` instance serves, the engine can register
28//! only the PID routes that apply, preventing both silent dead-letters and
29//! accidental misrouting.
30//!
31//! ## Conflict guard
32//!
33//! [`PidRouter`] panics at build time if two modules register the same PID to
34//! **different** workflow names. Set explicit [`DeploymentRoles`] to exclude
35//! conflicting registrations from modules that don't apply to this instance.
36//!
37//! [`PidRouter`]: crate::pid_router::PidRouter
38
39use std::collections::HashSet;
40
41// ── Marktrolle ────────────────────────────────────────────────────────────────
42
43/// A BDEW market-participant role (Marktrolle).
44///
45/// Declares which roles this `makod` deployment fills within the German energy
46/// market communication (MaKo) ecosystem. A single deployment may hold several
47/// roles simultaneously (see module-level docs).
48///
49/// # Non-exhaustive
50///
51/// New roles may be added as BDEW regulations expand. Match with `_` in
52/// exhaustive arms or use [`DeploymentRoles::contains`] for membership checks.
53#[non_exhaustive]
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub enum Marktrolle {
56 /// Netzbetreiber (NB) — distribution/transmission network operator.
57 ///
58 /// Receives GPKE ANFRAGE messages (55001/55002/55017), issues ANTWORT
59 /// messages (55003–55006), runs GPKE Konfiguration (17134/17135 outbound
60 /// ORDERS, 19001/19002 inbound ORDRSP).
61 Nb,
62
63 /// Lieferant (LF) — energy supplier.
64 ///
65 /// Initiates GPKE Lieferbeginn/Lieferende, receives ANTWORT from NB.
66 /// Registers as inbound-ANTWORT recipient (55003–55006/55018) for the
67 /// LF-side anmeldung workflow.
68 Lf,
69
70 /// grundzuständiger Messstellenbetreiber (gMSB) — incumbent meter operator.
71 ///
72 /// Receives WiM UTILMD device-change messages (11001–11003). Often the same
73 /// legal entity as the NB (§41 MsbG).
74 Msb,
75
76 /// nicht-grundzuständiger Messstellenbetreiber (nMSB) — challenger meter operator.
77 ///
78 /// Sends WiM UTILMD device-change requests (11001) and WiM Geräteübernahme
79 /// ORDERS (17001, 17009). Receives inbound ORDRSP responses 19001/19002
80 /// (Bestellbestätigung/Ablehnung) and 19015/19016 (Gerätewechselabsicht).
81 Nmsb,
82
83 /// abgebender Messstellenbetreiber (aMSB) — outgoing meter operator.
84 ///
85 /// Receives WiM Abmeldung/Kündigung UTILMD (11002). This role is often
86 /// held by the gMSB after a successful nMSB takeover.
87 Amsb,
88
89 /// Bilanzkreisverantwortlicher (BKV) — balance responsible party.
90 ///
91 /// Receives MABIS billing MSCONS (PID 13003 from BIKO: Abrechnungssummenzeitreihe).
92 Bkv,
93
94 /// Übertragungsnetzbetreiber (ÜNB) — transmission system operator.
95 ///
96 /// Issues BG-SZR Kategorie B/C and BK-SZR Kategorie B/C MSCONS (PID 13003).
97 Uenb,
98
99 /// Bilanzkoordinator (BIKO) — balancing coordinator.
100 ///
101 /// Issues Abrechnungssummenzeitreihe MSCONS (PID 13003) to BKV and NB-DZR.
102 Biko,
103}
104
105// ── DeploymentRoles ───────────────────────────────────────────────────────────
106
107/// The set of [`Marktrolle`]s this `makod` deployment fills.
108///
109/// Used by [`EngineModule::register_pids_with_roles`] to conditionally register
110/// PID routes based on which roles are active. Modules check
111/// `roles.contains(Marktrolle::Nb)` before registering role-specific PIDs.
112///
113/// # Constructors
114///
115/// - [`DeploymentRoles::all()`] — registers everything regardless of role
116/// (useful for development and single-role deployments, default).
117/// - [`DeploymentRoles::from_roles`] — explicit set for multi-role conflict resolution.
118/// - Convenience methods: [`nb()`], [`lf()`], [`msb()`], [`nmsb()`] etc.
119///
120/// # Conflict guard
121///
122/// When two modules both register the same PID to **different** workflow names,
123/// `EngineBuilder::build` will detect the conflict and panic. Set exclusive roles
124/// to ensure only one workflow is registered per shared PID:
125///
126/// ```rust,ignore
127/// // NB deployment: GPKE registers 19001/19002 → gpke-konfiguration
128/// // nMSB deployment: WiM registers 19001/19002 → wim-geraeteubernahme
129/// // Combined (conflict!): set roles to prevent double-registration:
130/// use mako_engine::marktrolle::{DeploymentRoles, Marktrolle};
131///
132/// let roles = DeploymentRoles::from_roles([Marktrolle::Nb]);
133/// // Now only GPKE registers 19001/19002; WiM skips its nMSB-conditional block.
134/// ```
135///
136/// [`EngineModule::register_pids_with_roles`]: crate::builder::EngineModule::register_pids_with_roles
137/// [`nb()`]: DeploymentRoles::nb
138/// [`lf()`]: DeploymentRoles::lf
139/// [`msb()`]: DeploymentRoles::msb
140/// [`nmsb()`]: DeploymentRoles::nmsb
141#[derive(Debug, Clone)]
142pub struct DeploymentRoles {
143 /// When `true`, `contains()` returns `true` for every role (matches all).
144 all: bool,
145 roles: HashSet<Marktrolle>,
146}
147
148impl Default for DeploymentRoles {
149 /// Defaults to `all` — every role is considered active.
150 ///
151 /// This preserves backward-compatible behavior (all PIDs registered) for
152 /// deployments that have not yet configured explicit roles. Set explicit
153 /// roles via [`DeploymentRoles::from_roles`] for multi-role conflict safety.
154 fn default() -> Self {
155 Self::all()
156 }
157}
158
159impl DeploymentRoles {
160 /// All roles active — `contains` always returns `true`.
161 ///
162 /// The default for `EngineBuilder`. Modules register all their PIDs
163 /// unconditionally, identical to the pre-role-aware behavior.
164 ///
165 /// **Warning:** if two modules register the same PID to different workflows
166 /// and `all()` is active, the conflict guard in `PidRouter` will panic at
167 /// build time. Use [`from_roles`] to specify exactly which roles apply.
168 ///
169 /// [`from_roles`]: DeploymentRoles::from_roles
170 #[must_use]
171 pub fn all() -> Self {
172 Self {
173 all: true,
174 roles: HashSet::new(),
175 }
176 }
177
178 /// Construct from an explicit set of active roles.
179 ///
180 /// Only modules whose role-conditional PID blocks include at least one of
181 /// these roles will register those PIDs. All non-role-conditional PID blocks
182 /// (i.e., those that don't call `roles.contains(...)`) are always registered.
183 #[must_use]
184 pub fn from_roles(roles: impl IntoIterator<Item = Marktrolle>) -> Self {
185 Self {
186 all: false,
187 roles: roles.into_iter().collect(),
188 }
189 }
190
191 /// Return `true` when `role` is active.
192 ///
193 /// Always returns `true` for [`DeploymentRoles::all()`].
194 #[must_use]
195 pub fn contains(&self, role: Marktrolle) -> bool {
196 self.all || self.roles.contains(&role)
197 }
198
199 /// Return `true` when this is the [`all()`] sentinel (no explicit role list).
200 ///
201 /// [`all()`]: DeploymentRoles::all
202 #[must_use]
203 pub fn is_all(&self) -> bool {
204 self.all
205 }
206
207 // ── Convenience constructors ──────────────────────────────────────────────
208
209 /// NB-only deployment (most common for grid operators).
210 #[must_use]
211 pub fn nb() -> Self {
212 Self::from_roles([Marktrolle::Nb])
213 }
214
215 /// LF-only deployment (supplier side).
216 #[must_use]
217 pub fn lf() -> Self {
218 Self::from_roles([Marktrolle::Lf])
219 }
220
221 /// gMSB-only deployment (incumbent meter operator).
222 #[must_use]
223 pub fn msb() -> Self {
224 Self::from_roles([Marktrolle::Msb])
225 }
226
227 /// nMSB-only deployment (challenger meter operator).
228 #[must_use]
229 pub fn nmsb() -> Self {
230 Self::from_roles([Marktrolle::Nmsb])
231 }
232
233 /// NB + gMSB (most common municipal utility / Stadtwerke combination).
234 #[must_use]
235 pub fn nb_msb() -> Self {
236 Self::from_roles([Marktrolle::Nb, Marktrolle::Msb])
237 }
238
239 /// NB + BKV (grid operator that also manages its own balance group).
240 #[must_use]
241 pub fn nb_bkv() -> Self {
242 Self::from_roles([Marktrolle::Nb, Marktrolle::Bkv])
243 }
244
245 /// Add a role to an existing set, returning a new `DeploymentRoles`.
246 #[must_use]
247 pub fn with(mut self, role: Marktrolle) -> Self {
248 if !self.all {
249 self.roles.insert(role);
250 }
251 self
252 }
253}
254
255impl FromIterator<Marktrolle> for DeploymentRoles {
256 fn from_iter<T: IntoIterator<Item = Marktrolle>>(iter: T) -> Self {
257 Self::from_roles(iter)
258 }
259}