use bon::Builder;
use serde::{Deserialize, Serialize};
use crate::types::validate_fields;
use crate::types::{
CiString, ContractId, CountryCode, Currency, DateTime, EvseId, Extensions, Number, OcpiString, PartyId,
PartyRef, Validate, Validator, ViolationCode,
};
use super::locations::{ConnectorFormat, ConnectorType, GeoLocation, PowerType};
use super::tariffs::Tariff;
use super::tokens::TokenType;
use super::types::Price;
pub use crate::v2_3_0::cdrs::{
AuthMethod, CdrDimension, CdrDimensionType, ChargingPeriod, NON_CREDIT_ID_MAX_LEN, SignedData,
SignedValue,
};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct Cdr {
pub country_code: CountryCode,
pub party_id: PartyId,
pub id: CiString<39>,
pub start_date_time: DateTime,
pub end_date_time: DateTime,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<CiString<36>>,
pub cdr_token: CdrToken,
pub auth_method: AuthMethod,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authorization_reference: Option<CiString<36>>,
pub cdr_location: CdrLocation,
#[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>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signed_data: Option<SignedData>,
pub total_cost: Price,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_fixed_cost: Option<Price>,
pub total_energy: Number,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_energy_cost: Option<Price>,
pub total_time: Number,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_time_cost: Option<Price>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_parking_time: Option<Number>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_parking_cost: Option<Price>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_reservation_cost: Option<Price>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remark: Option<OcpiString<255>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub invoice_reference_id: Option<CiString<39>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub credit: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub credit_reference_id: Option<CiString<39>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub home_charging_compensation: Option<bool>,
pub last_updated: DateTime,
#[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
#[builder(default)]
pub extensions: Extensions,
}
impl Cdr {
#[must_use]
pub fn owner_party(&self) -> PartyRef {
PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
}
#[must_use]
pub fn is_credit(&self) -> bool {
self.credit.unwrap_or(false)
}
#[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,
country_code,
party_id,
id,
start_date_time,
end_date_time,
session_id,
cdr_token,
auth_method,
authorization_reference,
cdr_location,
meter_id,
currency,
tariffs,
charging_periods,
signed_data,
total_cost,
total_fixed_cost,
total_energy,
total_energy_cost,
total_time,
total_time_cost,
total_parking_time,
total_parking_cost,
total_reservation_cost,
remark,
invoice_reference_id,
credit_reference_id,
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.end_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.is_credit() && self.id.len() > NON_CREDIT_ID_MAX_LEN {
v.report_at(
"id",
ViolationCode::TooLong,
format!("a non-credit CDR id may be at most {NON_CREDIT_ID_MAX_LEN} characters"),
);
}
if self.is_credit() && self.credit_reference_id.is_none() {
v.report_at(
"credit_reference_id",
ViolationCode::MissingConditional,
"is required to be set for a Credit CDR",
);
}
if self.end_date_time < self.start_date_time && self.start_date_time.unix_timestamp() != 0 {
v.report_at(
"end_date_time",
ViolationCode::Inconsistent,
"a session cannot end before it starts",
);
}
for (i, period) in self.charging_periods.iter().enumerate() {
for (j, dim) in period.dimensions.iter().enumerate() {
if dim.dimension_type.is_session_only() {
v.enter("charging_periods");
v.enter(&i.to_string());
v.enter("dimensions");
v.enter(&j.to_string());
v.report_at(
"type",
ViolationCode::Inconsistent,
format!("{} SHALL only be used in Sessions", dim.dimension_type),
);
v.leave();
v.leave();
v.leave();
v.leave();
}
}
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct CdrToken {
pub country_code: CountryCode,
pub party_id: PartyId,
pub uid: CiString<36>,
#[serde(rename = "type")]
pub token_type: TokenType,
pub contract_id: ContractId,
#[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
#[builder(default)]
pub extensions: Extensions,
}
impl CdrToken {
#[must_use]
pub fn owner_party(&self) -> PartyRef {
PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
}
}
impl Validate for CdrToken {
fn validate_in(&self, v: &mut Validator) {
validate_fields!(self, v, country_code, party_id, uid, token_type as "type", contract_id);
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct CdrLocation {
pub id: CiString<36>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<OcpiString<255>>,
pub address: OcpiString<45>,
pub city: OcpiString<45>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub postal_code: Option<OcpiString<10>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state: Option<OcpiString<20>>,
pub country: OcpiString<3>,
pub coordinates: GeoLocation,
pub evse_uid: CiString<36>,
pub evse_id: EvseId,
pub connector_id: CiString<36>,
pub connector_standard: ConnectorType,
pub connector_format: ConnectorFormat,
pub connector_power_type: PowerType,
#[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
#[builder(default)]
pub extensions: Extensions,
}
impl CdrLocation {
#[must_use]
pub fn is_reservation_only(&self) -> bool {
self.evse_uid.is_not_available()
|| self.evse_id.is_not_available()
|| self.connector_id.is_not_available()
}
}
impl Validate for CdrLocation {
fn validate_in(&self, v: &mut Validator) {
validate_fields!(
self,
v,
id,
name,
address,
city,
postal_code,
state,
country,
coordinates,
evse_uid,
evse_id,
connector_id,
connector_standard,
connector_format,
connector_power_type,
);
}
}