Skip to main content

ocpi_kit/v2_2_1/
cdrs.rs

1//! The *CDRs* module of OCPI 2.2.1, as a delta from [`v2_3_0::cdrs`](crate::v2_3_0::cdrs).
2//!
3//! The field list is unchanged between the two versions; what changed is the types the fields
4//! carry — [`Price`], [`TokenType`] and
5//! [`ConnectorType`] — so [`Cdr`], [`CdrToken`] and
6//! [`CdrLocation`] are redefined and everything else is re-exported.
7//!
8//! Spec: 2.2.1 §mod_cdrs_cdrs_module
9
10use bon::Builder;
11use serde::{Deserialize, Serialize};
12
13use crate::types::validate_fields;
14use crate::types::{
15    CiString, ContractId, CountryCode, Currency, DateTime, EvseId, Extensions, Number, OcpiString, PartyId,
16    PartyRef, Validate, Validator, ViolationCode,
17};
18
19use super::locations::{ConnectorFormat, ConnectorType, GeoLocation, PowerType};
20use super::tariffs::Tariff;
21use super::tokens::TokenType;
22use super::types::Price;
23
24// Wire-identical to OCPI 2.3.0.
25pub use crate::v2_3_0::cdrs::{
26    AuthMethod, CdrDimension, CdrDimensionType, ChargingPeriod, NON_CREDIT_ID_MAX_LEN, SignedData,
27    SignedValue,
28};
29
30/// A Charge Detail Record: one charging session and its costs, in OCPI 2.2.1.
31///
32/// Spec: 2.2.1 §mod_cdrs_cdr_object
33#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
34#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
35#[builder(on(_, into))]
36pub struct Cdr {
37    /// ISO-3166 alpha-2 country code of the CPO that 'owns' this CDR.
38    pub country_code: CountryCode,
39    /// ID of the CPO that 'owns' this CDR.
40    pub party_id: PartyId,
41    /// Uniquely identifies the CDR, unique per `country_code`/`party_id` combination.
42    pub id: CiString<39>,
43    /// Start of the charging session, or of the reservation when there was no session.
44    pub start_date_time: DateTime,
45    /// When the session was completed.
46    pub end_date_time: DateTime,
47    /// The Session this CDR belongs to.
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub session_id: Option<CiString<36>>,
50    /// Token used to start this charging session.
51    pub cdr_token: CdrToken,
52    /// Method used for authentication. The last method used during the session.
53    pub auth_method: AuthMethod,
54    /// Reference to the authorization given by the eMSP.
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub authorization_reference: Option<CiString<36>>,
57    /// Where the charging session took place.
58    pub cdr_location: CdrLocation,
59    /// Identification of the meter inside the Charge Point.
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub meter_id: Option<OcpiString<255>>,
62    /// Currency of the CDR in ISO 4217 code.
63    pub currency: Currency,
64    /// Relevant Tariffs, as they were at the start of the session.
65    #[serde(default, skip_serializing_if = "Vec::is_empty")]
66    #[builder(default)]
67    pub tariffs: Vec<Tariff>,
68    /// Charging Periods that make up this session. Cardinality `+`.
69    pub charging_periods: Vec<ChargingPeriod>,
70    /// Signed metering data belonging to this session.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub signed_data: Option<SignedData>,
73    /// Total sum of all the costs of this transaction.
74    pub total_cost: Price,
75    /// Total of the fixed costs, except fixed price components of parking and reservation.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub total_fixed_cost: Option<Price>,
78    /// Total energy charged, in kWh.
79    pub total_energy: Number,
80    /// Total cost of all the energy used.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub total_energy_cost: Option<Price>,
83    /// Total duration of the charging session, in hours.
84    pub total_time: Number,
85    /// Total cost related to the duration of charging.
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub total_time_cost: Option<Price>,
88    /// Total duration during which the EV was not charging, in hours.
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub total_parking_time: Option<Number>,
91    /// Total cost related to parking, including fixed price components.
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub total_parking_cost: Option<Price>,
94    /// Total cost related to a reservation, including fixed price components.
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub total_reservation_cost: Option<Price>,
97    /// Human-readable remark.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub remark: Option<OcpiString<255>>,
100    /// Reference to an invoice that will later be sent for this CDR.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub invoice_reference_id: Option<CiString<39>>,
103    /// Whether this is a Credit CDR. Requires `credit_reference_id`.
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub credit: Option<bool>,
106    /// The `id` of the CDR this Credit CDR corrects.
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub credit_reference_id: Option<CiString<39>>,
109    /// Whether the energy cost of this home-charging session is compensated to the EV driver.
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub home_charging_compensation: Option<bool>,
112    /// Timestamp when this CDR was last updated (or created).
113    pub last_updated: DateTime,
114    /// Undocumented JSON fields, preserved verbatim.
115    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
116    #[builder(default)]
117    pub extensions: Extensions,
118}
119
120impl Cdr {
121    /// The CPO that owns this CDR.
122    #[must_use]
123    pub fn owner_party(&self) -> PartyRef {
124        PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
125    }
126
127    /// Whether this is a Credit CDR.
128    #[must_use]
129    pub fn is_credit(&self) -> bool {
130        self.credit.unwrap_or(false)
131    }
132
133    /// The time the EV was actually charging, in hours.
134    #[must_use]
135    pub fn total_charging_time(&self) -> Number {
136        self.total_time - self.total_parking_time.unwrap_or(Number::ZERO)
137    }
138
139    /// The total volume of one dimension across every charging period.
140    #[must_use]
141    pub fn dimension_total(&self, dimension: CdrDimensionType) -> Number {
142        self.charging_periods
143            .iter()
144            .flat_map(|p| p.dimensions.iter())
145            .filter(|d| d.dimension_type == dimension)
146            .map(|d| d.volume)
147            .sum()
148    }
149}
150
151impl Validate for Cdr {
152    fn validate_in(&self, v: &mut Validator) {
153        validate_fields!(
154            self,
155            v,
156            country_code,
157            party_id,
158            id,
159            start_date_time,
160            end_date_time,
161            session_id,
162            cdr_token,
163            auth_method,
164            authorization_reference,
165            cdr_location,
166            meter_id,
167            currency,
168            tariffs,
169            charging_periods,
170            signed_data,
171            total_cost,
172            total_fixed_cost,
173            total_energy,
174            total_energy_cost,
175            total_time,
176            total_time_cost,
177            total_parking_time,
178            total_parking_cost,
179            total_reservation_cost,
180            remark,
181            invoice_reference_id,
182            credit_reference_id,
183            last_updated,
184        );
185        crate::v2_3_0::cdrs::validate_period_sequence(
186            &self.charging_periods.iter().map(|p| p.start_date_time).collect::<Vec<_>>(),
187            self.start_date_time,
188            Some(self.end_date_time),
189            v,
190        );
191        if self.charging_periods.is_empty() {
192            v.report_at(
193                "charging_periods",
194                ViolationCode::EmptyRequiredList,
195                "a CDR has cardinality `+` charging_periods: at least one is required",
196            );
197        }
198        if !self.is_credit() && self.id.len() > NON_CREDIT_ID_MAX_LEN {
199            v.report_at(
200                "id",
201                ViolationCode::TooLong,
202                format!("a non-credit CDR id may be at most {NON_CREDIT_ID_MAX_LEN} characters"),
203            );
204        }
205        if self.is_credit() && self.credit_reference_id.is_none() {
206            v.report_at(
207                "credit_reference_id",
208                ViolationCode::MissingConditional,
209                "is required to be set for a Credit CDR",
210            );
211        }
212        if self.end_date_time < self.start_date_time && self.start_date_time.unix_timestamp() != 0 {
213            v.report_at(
214                "end_date_time",
215                ViolationCode::Inconsistent,
216                "a session cannot end before it starts",
217            );
218        }
219        for (i, period) in self.charging_periods.iter().enumerate() {
220            for (j, dim) in period.dimensions.iter().enumerate() {
221                if dim.dimension_type.is_session_only() {
222                    v.enter("charging_periods");
223                    v.enter(&i.to_string());
224                    v.enter("dimensions");
225                    v.enter(&j.to_string());
226                    v.report_at(
227                        "type",
228                        ViolationCode::Inconsistent,
229                        format!("{} SHALL only be used in Sessions", dim.dimension_type),
230                    );
231                    v.leave();
232                    v.leave();
233                    v.leave();
234                    v.leave();
235                }
236            }
237        }
238    }
239}
240
241/// The token that started a session, as recorded in a CDR or Session, in OCPI 2.2.1.
242///
243/// Spec: 2.2.1 §mod_cdrs_cdr_token_object
244#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
245#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
246#[builder(on(_, into))]
247pub struct CdrToken {
248    /// ISO-3166 alpha-2 country code of the MSP that 'owns' this Token.
249    pub country_code: CountryCode,
250    /// ID of the eMSP that 'owns' this Token.
251    pub party_id: PartyId,
252    /// Unique ID by which this Token can be identified by the CPO's system.
253    pub uid: CiString<36>,
254    /// Type of the token.
255    #[serde(rename = "type")]
256    pub token_type: TokenType,
257    /// Uniquely identifies the EV driver contract token within the eMSP's platform.
258    pub contract_id: ContractId,
259    /// Undocumented JSON fields, preserved verbatim.
260    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
261    #[builder(default)]
262    pub extensions: Extensions,
263}
264
265impl CdrToken {
266    /// The eMSP that owns this Token.
267    #[must_use]
268    pub fn owner_party(&self) -> PartyRef {
269        PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
270    }
271}
272
273impl Validate for CdrToken {
274    fn validate_in(&self, v: &mut Validator) {
275        validate_fields!(self, v, country_code, party_id, uid, token_type as "type", contract_id);
276    }
277}
278
279/// The parts of a Location that a CDR needs, in OCPI 2.2.1.
280///
281/// Spec: 2.2.1 §mod_cdrs_cdr_location_class
282#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
283#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
284#[builder(on(_, into))]
285pub struct CdrLocation {
286    /// Uniquely identifies the location within the CPO's platform.
287    pub id: CiString<36>,
288    /// Display name of the location.
289    #[serde(default, skip_serializing_if = "Option::is_none")]
290    pub name: Option<OcpiString<255>>,
291    /// Street/block name and house number if available.
292    pub address: OcpiString<45>,
293    /// City or town.
294    pub city: OcpiString<45>,
295    /// Postal code of the location.
296    #[serde(default, skip_serializing_if = "Option::is_none")]
297    pub postal_code: Option<OcpiString<10>>,
298    /// State, only to be used when relevant.
299    #[serde(default, skip_serializing_if = "Option::is_none")]
300    pub state: Option<OcpiString<20>>,
301    /// ISO 3166-1 alpha-3 code for the country of this location.
302    pub country: OcpiString<3>,
303    /// Coordinates of the location.
304    pub coordinates: GeoLocation,
305    /// The EVSE's technical identifier. May be `#NA`.
306    pub evse_uid: CiString<36>,
307    /// The EVSE's human-readable ID. May be `#NA`.
308    pub evse_id: EvseId,
309    /// Identifier of the connector within the EVSE. May be `#NA`.
310    pub connector_id: CiString<36>,
311    /// The standard of the installed connector.
312    pub connector_standard: ConnectorType,
313    /// The format (socket/cable) of the installed connector.
314    pub connector_format: ConnectorFormat,
315    /// Whether the connector supplies AC or DC, and on how many phases.
316    pub connector_power_type: PowerType,
317    /// Undocumented JSON fields, preserved verbatim.
318    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
319    #[builder(default)]
320    pub extensions: Extensions,
321}
322
323impl CdrLocation {
324    /// Whether this CDR records a reservation that never became a charging session.
325    #[must_use]
326    pub fn is_reservation_only(&self) -> bool {
327        self.evse_uid.is_not_available()
328            || self.evse_id.is_not_available()
329            || self.connector_id.is_not_available()
330    }
331}
332
333impl Validate for CdrLocation {
334    fn validate_in(&self, v: &mut Validator) {
335        validate_fields!(
336            self,
337            v,
338            id,
339            name,
340            address,
341            city,
342            postal_code,
343            state,
344            country,
345            coordinates,
346            evse_uid,
347            evse_id,
348            connector_id,
349            connector_standard,
350            connector_format,
351            connector_power_type,
352        );
353    }
354}