use bon::Builder;
use serde::{Deserialize, Serialize};
use crate::ocpi_lenient_enum;
use crate::types::validate_fields;
use crate::types::{
CiString, Currency, DateTime, Extensions, Number, OcpiString, Validate, Validator, ViolationCode,
};
use super::locations::Location;
use super::tariffs::Tariff;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct Cdr {
pub id: CiString<36>,
pub start_date_time: DateTime,
pub stop_date_time: DateTime,
pub auth_id: OcpiString<36>,
pub auth_method: AuthMethod,
pub location: Location,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub meter_id: Option<OcpiString<255>>,
pub currency: Currency,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[builder(default)]
pub tariffs: Vec<Tariff>,
pub charging_periods: Vec<ChargingPeriod>,
pub total_cost: Number,
pub total_energy: Number,
pub total_time: Number,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_parking_time: Option<Number>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remark: Option<OcpiString<255>>,
pub last_updated: DateTime,
#[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
#[builder(default)]
pub extensions: Extensions,
}
impl Cdr {
#[must_use]
pub fn total_charging_time(&self) -> Number {
self.total_time - self.total_parking_time.unwrap_or(Number::ZERO)
}
#[must_use]
pub fn dimension_total(&self, dimension: CdrDimensionType) -> Number {
self.charging_periods
.iter()
.flat_map(|p| p.dimensions.iter())
.filter(|d| d.dimension_type == dimension)
.map(|d| d.volume)
.sum()
}
}
impl Validate for Cdr {
fn validate_in(&self, v: &mut Validator) {
validate_fields!(
self,
v,
id,
start_date_time,
stop_date_time,
auth_id,
auth_method,
location,
meter_id,
currency,
tariffs,
charging_periods,
total_cost,
total_energy,
total_time,
total_parking_time,
remark,
last_updated,
);
crate::v2_3_0::cdrs::validate_period_sequence(
&self.charging_periods.iter().map(|p| p.start_date_time).collect::<Vec<_>>(),
self.start_date_time,
Some(self.stop_date_time),
v,
);
if self.charging_periods.is_empty() {
v.report_at(
"charging_periods",
ViolationCode::EmptyRequiredList,
"a CDR has cardinality `+` charging_periods: at least one is required",
);
}
if self.stop_date_time < self.start_date_time {
v.report_at(
"stop_date_time",
ViolationCode::Inconsistent,
"a session cannot stop before it starts",
);
}
if self.total_parking_time.is_some_and(|p| p > self.total_time) {
v.report_at(
"total_parking_time",
ViolationCode::Inconsistent,
"cannot exceed total_time, of which it is a part",
);
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct ChargingPeriod {
pub start_date_time: DateTime,
pub dimensions: Vec<CdrDimension>,
#[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
#[builder(default)]
pub extensions: Extensions,
}
impl ChargingPeriod {
#[must_use]
pub fn volume(&self, dimension: CdrDimensionType) -> Option<Number> {
self.dimensions.iter().find(|d| d.dimension_type == dimension).map(|d| d.volume)
}
}
impl Validate for ChargingPeriod {
fn validate_in(&self, v: &mut Validator) {
validate_fields!(self, v, start_date_time, dimensions);
if self.dimensions.is_empty() {
v.report_at(
"dimensions",
ViolationCode::EmptyRequiredList,
"a ChargingPeriod has cardinality `+` dimensions: at least one is required",
);
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct CdrDimension {
#[serde(rename = "type")]
pub dimension_type: CdrDimensionType,
pub volume: Number,
#[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
pub extensions: Extensions,
}
impl CdrDimension {
#[must_use]
pub fn new(dimension_type: CdrDimensionType, volume: Number) -> Self {
Self { dimension_type, volume, extensions: Extensions::new() }
}
}
impl Validate for CdrDimension {
fn validate_in(&self, v: &mut Validator) {
validate_fields!(self, v, dimension_type as "type", volume);
}
}
ocpi_lenient_enum! {
pub enum AuthMethod {
AuthRequest = "AUTH_REQUEST",
Whitelist = "WHITELIST",
}
}
ocpi_lenient_enum! {
pub enum CdrDimensionType {
Energy = "ENERGY",
Flat = "FLAT",
MaxCurrent = "MAX_CURRENT",
MinCurrent = "MIN_CURRENT",
ParkingTime = "PARKING_TIME",
Time = "TIME",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_2_1_1_dimension_set_is_the_small_one() {
assert_eq!(CdrDimensionType::ALL_KNOWN.len(), 6);
assert!(!CdrDimensionType::from("STATE_OF_CHARGE").is_known());
assert!(!AuthMethod::from("COMMAND").is_known(), "COMMAND arrived with the Commands module");
}
#[test]
fn the_cost_is_a_bare_number_excluding_vat() {
let json = r#"{"volume":12.5,"type":"ENERGY"}"#;
let dimension: CdrDimension = serde_json::from_str(json).unwrap();
assert_eq!(dimension.dimension_type, CdrDimensionType::Energy);
assert_eq!(serde_json::to_string(&dimension).unwrap(), r#"{"type":"ENERGY","volume":12.5}"#);
}
}