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 Z29: &str = "Z29";
pub const Z43: &str = "Z43";
pub const ZB3: &str = "ZB3";
pub const Z28: &str = "Z28";
pub const Z30: &str = "Z30";
pub const Z04: &str = "Z04";
pub const Z07: &str = "Z07";
pub const Z08: &str = "Z08";
pub const Z09: &str = "Z09";
pub const E01: &str = "E01";
pub const E02: &str = "E02";
pub const E03: &str = "E03";
pub const E04: &str = "E04";
pub const E05: &str = "E05";
pub const E06: &str = "E06";
}
#[must_use]
pub fn recommended_action(code: &ErcCode) -> ErcAction {
match code.as_str() {
codes::E01 | codes::Z43 => ErcAction::RetryWithCorrection { field: "malo_id" },
codes::E02 => ErcAction::RetryWithCorrection { field: "address" },
codes::E03 => ErcAction::EscalateToOperator {
reason: "LF GLN not recognised by NB — check Marktteilnehmerverzeichnis",
},
codes::E04 | codes::Z04 => ErcAction::RetryWithCorrection {
field: "process_date",
},
codes::E05 => ErcAction::WaitAndRetry {
reason: "concurrent in-flight process present; wait for completion then retry",
},
codes::E06 => ErcAction::EscalateToOperator {
reason: "invalid Prüfidentifikator in outbound message — engineering alert",
},
codes::Z07 => ErcAction::EscalateToOperator {
reason: "not authorised for this supply point",
},
codes::Z08 => ErcAction::RetryWithCorrection { field: "meter_id" },
codes::Z09 => ErcAction::RetryWithCorrection { field: "melo_id" },
codes::Z28 | codes::Z30 | codes::ZB3 => ErcAction::AbortProcess,
codes::Z29 => ErcAction::EscalateToOperator {
reason: "process not found or unclassified error",
},
_ => 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::E02);
assert_eq!(code.to_string(), "E02");
assert_eq!(code.as_str(), "E02");
}
#[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::ZB3);
let s: &str = code.as_ref();
assert_eq!(s, "ZB3");
}
#[test]
fn recommended_action_e01_retry_malo() {
assert!(matches!(
recommended_action(&ErcCode::new(codes::E01)),
ErcAction::RetryWithCorrection { field: "malo_id" }
));
}
#[test]
fn recommended_action_e02_retry_address() {
assert!(matches!(
recommended_action(&ErcCode::new(codes::E02)),
ErcAction::RetryWithCorrection { field: "address" }
));
}
#[test]
fn recommended_action_e04_retry_date() {
assert!(matches!(
recommended_action(&ErcCode::new(codes::E04)),
ErcAction::RetryWithCorrection {
field: "process_date"
}
));
}
#[test]
fn recommended_action_e05_wait_and_retry() {
assert!(matches!(
recommended_action(&ErcCode::new(codes::E05)),
ErcAction::WaitAndRetry { .. }
));
}
#[test]
fn recommended_action_e03_escalate() {
assert!(matches!(
recommended_action(&ErcCode::new(codes::E03)),
ErcAction::EscalateToOperator { .. }
));
}
#[test]
fn recommended_action_e06_escalate() {
assert!(matches!(
recommended_action(&ErcCode::new(codes::E06)),
ErcAction::EscalateToOperator { .. }
));
}
#[test]
fn recommended_action_z29_escalate() {
assert!(matches!(
recommended_action(&ErcCode::new(codes::Z29)),
ErcAction::EscalateToOperator { .. }
));
}
#[test]
fn recommended_action_zb3_abort() {
assert_eq!(
recommended_action(&ErcCode::new(codes::ZB3)),
ErcAction::AbortProcess
);
}
#[test]
fn recommended_action_z28_abort() {
assert_eq!(
recommended_action(&ErcCode::new(codes::Z28)),
ErcAction::AbortProcess
);
}
#[test]
fn recommended_action_z30_abort() {
assert_eq!(
recommended_action(&ErcCode::new(codes::Z30)),
ErcAction::AbortProcess
);
}
#[test]
fn recommended_action_z43_retry_malo() {
assert!(matches!(
recommended_action(&ErcCode::new(codes::Z43)),
ErcAction::RetryWithCorrection { field: "malo_id" }
));
}
#[test]
fn recommended_action_unknown_escalates() {
assert!(matches!(
recommended_action(&ErcCode::new("X99")),
ErcAction::EscalateToOperator { .. }
));
}
#[test]
fn all_standard_codes_have_recommendations() {
for (code_str, expected_variant) in [
(codes::E01, "RetryWithCorrection"),
(codes::E02, "RetryWithCorrection"),
(codes::E03, "EscalateToOperator"),
(codes::E04, "RetryWithCorrection"),
(codes::E05, "WaitAndRetry"),
(codes::E06, "EscalateToOperator"),
(codes::Z04, "RetryWithCorrection"),
(codes::Z07, "EscalateToOperator"),
(codes::Z08, "RetryWithCorrection"),
(codes::Z09, "RetryWithCorrection"),
(codes::Z28, "AbortProcess"),
(codes::Z29, "EscalateToOperator"),
(codes::Z30, "AbortProcess"),
(codes::Z43, "RetryWithCorrection"),
(codes::ZB3, "AbortProcess"),
] {
let action = recommended_action(&ErcCode::new(code_str));
let variant_name = match &action {
ErcAction::RetryWithCorrection { .. } => "RetryWithCorrection",
ErcAction::EscalateToOperator { .. } => "EscalateToOperator",
ErcAction::AbortProcess => "AbortProcess",
ErcAction::WaitAndRetry { .. } => "WaitAndRetry",
};
assert_eq!(
variant_name, expected_variant,
"ERC code {code_str}: expected {expected_variant}, got {variant_name}"
);
}
}
#[test]
fn erc_code_in_json_macro() {
let payload = serde_json::json!({
"error_code": codes::Z29,
"reason": "test",
});
assert_eq!(payload["error_code"], "Z29");
}
}