1use 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
24pub use crate::v2_3_0::cdrs::{
26 AuthMethod, CdrDimension, CdrDimensionType, ChargingPeriod, NON_CREDIT_ID_MAX_LEN, SignedData,
27 SignedValue,
28};
29
30#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
34#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
35#[builder(on(_, into))]
36pub struct Cdr {
37 pub country_code: CountryCode,
39 pub party_id: PartyId,
41 pub id: CiString<39>,
43 pub start_date_time: DateTime,
45 pub end_date_time: DateTime,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub session_id: Option<CiString<36>>,
50 pub cdr_token: CdrToken,
52 pub auth_method: AuthMethod,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub authorization_reference: Option<CiString<36>>,
57 pub cdr_location: CdrLocation,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub meter_id: Option<OcpiString<255>>,
62 pub currency: Currency,
64 #[serde(default, skip_serializing_if = "Vec::is_empty")]
66 #[builder(default)]
67 pub tariffs: Vec<Tariff>,
68 pub charging_periods: Vec<ChargingPeriod>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub signed_data: Option<SignedData>,
73 pub total_cost: Price,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub total_fixed_cost: Option<Price>,
78 pub total_energy: Number,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub total_energy_cost: Option<Price>,
83 pub total_time: Number,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub total_time_cost: Option<Price>,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub total_parking_time: Option<Number>,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub total_parking_cost: Option<Price>,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub total_reservation_cost: Option<Price>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub remark: Option<OcpiString<255>>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub invoice_reference_id: Option<CiString<39>>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub credit: Option<bool>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub credit_reference_id: Option<CiString<39>>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub home_charging_compensation: Option<bool>,
112 pub last_updated: DateTime,
114 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
116 #[builder(default)]
117 pub extensions: Extensions,
118}
119
120impl Cdr {
121 #[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 #[must_use]
129 pub fn is_credit(&self) -> bool {
130 self.credit.unwrap_or(false)
131 }
132
133 #[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 #[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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
245#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
246#[builder(on(_, into))]
247pub struct CdrToken {
248 pub country_code: CountryCode,
250 pub party_id: PartyId,
252 pub uid: CiString<36>,
254 #[serde(rename = "type")]
256 pub token_type: TokenType,
257 pub contract_id: ContractId,
259 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
261 #[builder(default)]
262 pub extensions: Extensions,
263}
264
265impl CdrToken {
266 #[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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
283#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
284#[builder(on(_, into))]
285pub struct CdrLocation {
286 pub id: CiString<36>,
288 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub name: Option<OcpiString<255>>,
291 pub address: OcpiString<45>,
293 pub city: OcpiString<45>,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
297 pub postal_code: Option<OcpiString<10>>,
298 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub state: Option<OcpiString<20>>,
301 pub country: OcpiString<3>,
303 pub coordinates: GeoLocation,
305 pub evse_uid: CiString<36>,
307 pub evse_id: EvseId,
309 pub connector_id: CiString<36>,
311 pub connector_standard: ConnectorType,
313 pub connector_format: ConnectorFormat,
315 pub connector_power_type: PowerType,
317 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
319 #[builder(default)]
320 pub extensions: Extensions,
321}
322
323impl CdrLocation {
324 #[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}