use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ErcCode(Box<str>);
impl ErcCode {
pub fn new(code: impl Into<Box<str>>) -> Self {
Self(code.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ErcCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for ErcCode {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<&str> for ErcCode {
fn from(s: &str) -> Self {
Self::new(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErcAction {
RetryWithCorrection {
field: &'static str,
},
EscalateToOperator {
reason: &'static str,
},
AbortProcess,
WaitAndRetry {
reason: &'static str,
},
}
pub mod codes {
pub const Z10: &str = "Z10";
pub const Z14: &str = "Z14";
pub const Z15: &str = "Z15";
pub const Z16: &str = "Z16";
pub const Z17: &str = "Z17";
pub const Z18: &str = "Z18";
pub const Z19: &str = "Z19";
pub const Z20: &str = "Z20";
pub const Z21: &str = "Z21";
pub const Z24: &str = "Z24";
pub const Z25: &str = "Z25";
pub const Z26: &str = "Z26";
pub const Z27: &str = "Z27";
pub const Z29: &str = "Z29";
pub const Z30: &str = "Z30";
pub const Z31: &str = "Z31";
pub const Z33: &str = "Z33";
pub const Z34: &str = "Z34";
pub const Z35: &str = "Z35";
pub const Z37: &str = "Z37";
pub const Z38: &str = "Z38";
pub const Z39: &str = "Z39";
pub const Z40: &str = "Z40";
pub const Z41: &str = "Z41";
pub const Z42: &str = "Z42";
pub const Z43: &str = "Z43";
pub const Z44: &str = "Z44";
pub const ALL: [&str; 27] = [
Z10, Z14, Z15, Z16, Z17, Z18, Z19, Z20, Z21, Z24, Z25, Z26, Z27, Z29, Z30, Z31, Z33, Z34,
Z35, Z37, Z38, Z39, Z40, Z41, Z42, Z43, Z44,
];
}
#[must_use]
pub fn requires_ortsangabe(code: &str) -> bool {
matches!(
code,
codes::Z29 | codes::Z35 | codes::Z38 | codes::Z39 | codes::Z40 | codes::Z41
)
}
#[must_use]
pub fn recommended_action(code: &ErcCode) -> ErcAction {
match code.as_str() {
codes::Z29 | codes::Z35 | codes::Z38 | codes::Z39 | codes::Z40 => {
ErcAction::RetryWithCorrection { field: "message" }
}
codes::Z21 => ErcAction::RetryWithCorrection {
field: "vorgangsnummer",
},
codes::Z27 => ErcAction::RetryWithCorrection { field: "zaehlwert" },
codes::Z34 | codes::Z41 => ErcAction::RetryWithCorrection {
field: "zeitintervall",
},
codes::Z30 => ErcAction::RetryWithCorrection { field: "zeitreihe" },
codes::Z19 => ErcAction::RetryWithCorrection {
field: "geraetenummer",
},
codes::Z20 => ErcAction::RetryWithCorrection { field: "obis" },
codes::Z31 | codes::Z37 | codes::Z43 => ErcAction::AbortProcess,
codes::Z33 => ErcAction::WaitAndRetry {
reason: "referenced Geschäftsvorfall-Tupel not present at the receiver yet",
},
codes::Z10 | codes::Z14 | codes::Z15 => ErcAction::EscalateToOperator {
reason: "the receiver does not know this ID — check the Marktpartner master data",
},
codes::Z16 => ErcAction::EscalateToOperator {
reason: "object has left the Netzgebiet — the receiving NB is no longer responsible",
},
codes::Z17 | codes::Z18 | codes::Z24 | codes::Z25 | codes::Z26 => {
ErcAction::EscalateToOperator {
reason: "party is not assigned to the object at that time — check the Zuordnung",
}
}
codes::Z42 => ErcAction::EscalateToOperator {
reason: "Konfigurations-ID unknown at the receiver",
},
codes::Z44 => ErcAction::EscalateToOperator {
reason: "the object's property differs from the one the Geschäftsvorfall codes",
},
_ => ErcAction::EscalateToOperator {
reason: "unknown ERC code — manual review required",
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn erc_code_roundtrips_json() {
let code = ErcCode::new(codes::Z29);
let json = serde_json::to_string(&code).unwrap();
assert_eq!(json, r#""Z29""#);
let back: ErcCode = serde_json::from_str(&json).unwrap();
assert_eq!(back, code);
}
#[test]
fn erc_code_display_matches_inner() {
let code = ErcCode::new(codes::Z39);
assert_eq!(code.to_string(), "Z39");
assert_eq!(code.as_str(), "Z39");
}
#[test]
fn erc_code_from_str() {
let code = ErcCode::from(codes::Z43);
assert_eq!(code.as_str(), codes::Z43);
}
#[test]
fn erc_code_as_ref() {
let code = ErcCode::new(codes::Z31);
let s: &str = code.as_ref();
assert_eq!(s, "Z31");
}
#[test]
fn a_message_level_defect_is_a_retry() {
for c in [codes::Z29, codes::Z35, codes::Z38, codes::Z39, codes::Z40] {
assert!(
matches!(
recommended_action(&ErcCode::new(c)),
ErcAction::RetryWithCorrection { field: "message" }
),
"{c}",
);
}
}
#[test]
fn a_receiver_decision_is_not_retried() {
for c in [codes::Z31, codes::Z37, codes::Z43] {
assert_eq!(
recommended_action(&ErcCode::new(c)),
ErcAction::AbortProcess,
"{c}",
);
}
}
#[test]
fn an_unknown_correlation_waits() {
assert!(matches!(
recommended_action(&ErcCode::new(codes::Z33)),
ErcAction::WaitAndRetry { .. }
));
}
#[test]
fn recommended_action_unknown_escalates() {
assert!(matches!(
recommended_action(&ErcCode::new("X99")),
ErcAction::EscalateToOperator { .. }
));
}
#[test]
fn every_code_has_its_own_recommendation() {
for c in codes::ALL {
let action = recommended_action(&ErcCode::new(c));
assert!(
!matches!(
action,
ErcAction::EscalateToOperator {
reason: "unknown ERC code — manual review required"
}
),
"{c} falls through to the catch-all",
);
}
}
#[test]
fn the_ortsangabe_codes_match_the_ahb_conditions() {
let obliged: Vec<&str> = codes::ALL
.into_iter()
.filter(|c| requires_ortsangabe(c))
.collect();
assert_eq!(obliged, ["Z29", "Z35", "Z38", "Z39", "Z40", "Z41"]);
}
#[test]
fn erc_code_in_json_macro() {
let payload = serde_json::json!({
"error_code": codes::Z29,
"reason": "test",
});
assert_eq!(payload["error_code"], "Z29");
}
}