use rust_decimal::Decimal;
use time::{Date, OffsetDateTime};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum MeterStatus {
#[default]
Active,
Removed,
Pending,
Deployed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum MeterLifecycleEventType {
Installed,
Replaced,
Removed,
Updated,
Recalibrated,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MeterLifecycleEvent {
pub event_id: String,
pub meter_serial: String,
pub melo_id: String,
pub event_type: MeterLifecycleEventType,
pub occurred_at: OffsetDateTime,
pub reading_kwh: Option<Decimal>,
pub obis_code: Option<crate::obis::ObisCode>,
pub reason: Option<String>,
pub triggered_by_pid: Option<u32>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MeterExchangeEvent {
pub exchange_id: String,
pub melo_id: String,
pub old_meter_serial: String,
pub old_final_reading_kwh: Decimal,
pub new_meter_serial: String,
pub new_first_reading_kwh: Decimal,
pub exchange_at: OffsetDateTime,
pub exchange_date: Date,
pub triggered_by_pid: Option<u32>,
pub insrpt_process_id: Option<String>,
pub performed_by: Option<String>,
}
impl MeterExchangeEvent {
#[must_use]
pub fn consumption_old_meter_kwh(&self, period_start_reading: Decimal) -> Decimal {
(self.old_final_reading_kwh - period_start_reading).max(Decimal::ZERO)
}
#[must_use]
pub fn consumption_new_meter_kwh(&self, period_end_reading: Decimal) -> Decimal {
(period_end_reading - self.new_first_reading_kwh).max(Decimal::ZERO)
}
#[must_use]
pub fn total_consumption_kwh(
&self,
period_start_reading: Decimal,
period_end_reading: Decimal,
) -> Decimal {
self.consumption_old_meter_kwh(period_start_reading)
+ self.consumption_new_meter_kwh(period_end_reading)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
use time::macros::{date, datetime};
fn make_exchange() -> MeterExchangeEvent {
MeterExchangeEvent {
exchange_id: "EX-001".to_owned(),
melo_id: "DE000123456789".to_owned(),
old_meter_serial: "OLD-1234".to_owned(),
old_final_reading_kwh: dec!(12500),
new_meter_serial: "NEW-5678".to_owned(),
new_first_reading_kwh: dec!(0), exchange_at: datetime!(2026-06-15 8:00 UTC),
exchange_date: date!(2026 - 06 - 15),
triggered_by_pid: Some(23003),
insrpt_process_id: None,
performed_by: Some("MSB-9900357000004".to_owned()),
}
}
#[test]
fn consumption_split_across_exchange() {
let ex = make_exchange();
let old = ex.consumption_old_meter_kwh(dec!(12000));
let new = ex.consumption_new_meter_kwh(dec!(800));
assert_eq!(old, dec!(500));
assert_eq!(new, dec!(800));
assert_eq!(ex.total_consumption_kwh(dec!(12000), dec!(800)), dec!(1300));
}
#[test]
fn rollover_protection() {
let ex = make_exchange();
let old = ex.consumption_old_meter_kwh(dec!(13000)); assert_eq!(old, Decimal::ZERO, "rollover should return 0, not negative");
}
}