use metering::QualityFlag;
use metering::interval::MeasurementUnit;
use rust_decimal::Decimal;
use time::OffsetDateTime;
use crate::version::ScopedVersion;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoredValue {
pub value: Decimal,
pub unit: MeasurementUnit,
pub quality: QualityFlag,
pub version: ScopedVersion,
pub recorded_at: OffsetDateTime,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Effect {
Inserted,
Superseded,
Shadowed,
Duplicate,
}
impl Effect {
pub fn changed_current_value(self) -> bool {
matches!(self, Self::Inserted | Self::Superseded)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Displacement {
pub malo_id: String,
pub obis_code: String,
pub from: OffsetDateTime,
pub to: OffsetDateTime,
pub identity: Vec<(String, String)>,
pub effect: Effect,
pub superseded: Option<StoredValue>,
pub written: StoredValue,
}
impl Displacement {
pub fn current(&self) -> &StoredValue {
match self.effect {
Effect::Inserted | Effect::Superseded => &self.written,
Effect::Shadowed | Effect::Duplicate => {
self.superseded.as_ref().unwrap_or(&self.written)
}
}
}
pub fn value_changed(&self) -> bool {
match &self.superseded {
Some(prior) => self.effect.changed_current_value() && prior.value != self.written.value,
None => self.effect.changed_current_value(),
}
}
pub fn quality_changed(&self) -> bool {
match &self.superseded {
Some(prior) => {
self.effect.changed_current_value() && prior.quality != self.written.quality
}
None => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::version::{Version, VersionScope};
use time::macros::datetime;
fn value(v: i64, version: u128, quality: QualityFlag) -> StoredValue {
StoredValue {
value: Decimal::new(v, 0),
unit: MeasurementUnit::KiloWattHour,
quality,
version: ScopedVersion::new(
VersionScope::new("99", 2026, 7).unwrap(),
Version::new(version).unwrap(),
),
recorded_at: datetime!(2026-07-27 06:00 UTC),
}
}
fn displacement(
effect: Effect,
prior: Option<StoredValue>,
written: StoredValue,
) -> Displacement {
Displacement {
malo_id: "12345678901".into(),
obis_code: "1-0:1.8.0".into(),
from: datetime!(2026-07-20 00:00 UTC),
to: datetime!(2026-07-20 00:15 UTC),
identity: Vec::new(),
effect,
superseded: prior,
written,
}
}
#[test]
fn only_a_real_change_gates_an_audit_row() {
assert!(Effect::Inserted.changed_current_value());
assert!(Effect::Superseded.changed_current_value());
assert!(!Effect::Shadowed.changed_current_value());
assert!(!Effect::Duplicate.changed_current_value());
}
#[test]
fn a_shadowed_write_reports_the_value_that_still_holds() {
let winner = value(50, 20_260_728_000_009, QualityFlag::Measured);
let d = displacement(
Effect::Shadowed,
Some(winner.clone()),
value(10, 20_260_720_000_001, QualityFlag::Measured),
);
assert_eq!(d.current(), &winner);
assert!(!d.value_changed());
}
#[test]
fn a_restated_quantity_is_a_correction_but_not_a_change_in_the_amount() {
let d = displacement(
Effect::Superseded,
Some(value(10, 20_260_720_000_001, QualityFlag::Measured)),
value(10, 20_260_728_000_002, QualityFlag::Corrected),
);
assert!(d.effect.changed_current_value());
assert!(!d.value_changed(), "the quantity is the same");
assert!(d.quality_changed(), "but the quality is not");
}
#[test]
fn a_substitute_replaced_by_a_measurement_is_visible_even_at_an_equal_value() {
let d = displacement(
Effect::Superseded,
Some(value(10, 20_260_720_000_001, QualityFlag::Substituted)),
value(10, 20_260_728_000_002, QualityFlag::Measured),
);
assert!(d.quality_changed());
assert!(!d.value_changed());
}
#[test]
fn a_first_value_supersedes_nothing_and_still_counts_as_a_change() {
let d = displacement(
Effect::Inserted,
None,
value(10, 20_260_720_000_001, QualityFlag::Measured),
);
assert!(d.value_changed());
assert!(
!d.quality_changed(),
"there is no prior quality to differ from"
);
assert_eq!(d.current().value, Decimal::new(10, 0));
}
}