use rust_decimal::Decimal;
use crate::position::BillingWarning;
#[derive(Debug, Clone, thiserror::Error)]
pub enum EngineError {
#[error(
"billing blocked by regulatory validation: {}",
blocking_summary(warnings)
)]
ValidationBlocked {
warnings: Vec<BillingWarning>,
},
#[error("tariff field {field} out of monetary range: {value}")]
PriceOutOfRange {
field: String,
value: Decimal,
},
#[error("invalid billing period: {from} is after {to}")]
InvalidPeriod {
from: time::Date,
to: time::Date,
},
#[error("allocation mismatch: {fractions} fractions vs {contexts} contexts")]
AllocationMismatch {
fractions: usize,
contexts: usize,
},
#[error("allocation weights must be non-negative and sum above zero, got sum {sum}")]
AllocationWeightsInvalid {
sum: Decimal,
},
#[error("GGV Nutzungsplan shares must sum to 1, got {sum}")]
NutzungsplanSharesInvalid {
sum: Decimal,
},
#[error("{field} cannot be represented in the EN 16931 model: {value}")]
Unrepresentable {
field: String,
value: String,
},
#[error("EN 16931 reconciliation failed: {reason}")]
ReconciliationFailed {
reason: String,
},
#[error(transparent)]
Arithmetic(#[from] billing::BillingError),
}
impl EngineError {
#[must_use]
pub const fn code(&self) -> &'static str {
match self {
Self::ValidationBlocked { .. } => "VALIDATION_BLOCKED",
Self::PriceOutOfRange { .. } => "PRICE_OUT_OF_RANGE",
Self::InvalidPeriod { .. } => "INVALID_PERIOD",
Self::AllocationMismatch { .. } => "ALLOCATION_MISMATCH",
Self::AllocationWeightsInvalid { .. } => "ALLOCATION_WEIGHTS_INVALID",
Self::NutzungsplanSharesInvalid { .. } => "NUTZUNGSPLAN_SHARES_INVALID",
Self::Unrepresentable { .. } => "UNREPRESENTABLE",
Self::ReconciliationFailed { .. } => "RECONCILIATION_FAILED",
Self::Arithmetic(_) => "ARITHMETIC",
}
}
#[must_use]
pub fn blocking_warnings(&self) -> &[BillingWarning] {
match self {
Self::ValidationBlocked { warnings } => warnings,
_ => &[],
}
}
}
fn blocking_summary(warnings: &[BillingWarning]) -> String {
warnings
.iter()
.filter(|w| w.severity == crate::position::WarningSeverity::Error)
.map(|w| format!("{}: {}", w.code, w.message))
.collect::<Vec<_>>()
.join("; ")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::position::WarningSeverity;
#[test]
fn validation_blocked_displays_codes() {
let err = EngineError::ValidationBlocked {
warnings: vec![
BillingWarning {
code: "ESTIMATED_READING",
severity: WarningSeverity::Warning,
message: "reading estimated".into(),
},
BillingWarning {
code: "MODUL3_AND_FLAT_NNE",
severity: WarningSeverity::Error,
message: "both configured".into(),
},
],
};
let s = err.to_string();
assert!(s.contains("MODUL3_AND_FLAT_NNE: both configured"), "{s}");
assert!(!s.contains("ESTIMATED_READING"), "{s}");
assert_eq!(err.code(), "VALIDATION_BLOCKED");
assert_eq!(err.blocking_warnings().len(), 2);
}
#[test]
fn arithmetic_passthrough() {
let inner = billing::BillingError::InvalidInput {
reason: "negative quantity".into(),
};
let err: EngineError = inner.into();
assert_eq!(err.code(), "ARITHMETIC");
assert!(err.to_string().contains("negative quantity"));
}
}