1use bon::Builder;
10use serde::{Deserialize, Serialize};
11
12use crate::ocpi_lenient_enum;
13use crate::types::validate_fields;
14use crate::types::{Currency, DateTime, Extensions, Number, OcpiString, Validate, Validator, ViolationCode};
15
16use super::cdrs::{AuthMethod, CdrDimensionType, ChargingPeriod};
17use super::locations::Location;
18
19#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
23#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
24#[builder(on(_, into))]
25pub struct Session {
26 pub id: OcpiString<36>,
28 #[serde(rename = "start_datetime")]
34 pub start_date_time: DateTime,
35 #[serde(rename = "end_datetime", default, skip_serializing_if = "Option::is_none")]
37 pub end_date_time: Option<DateTime>,
38 pub kwh: Number,
40 pub auth_id: OcpiString<36>,
42 pub auth_method: AuthMethod,
44 pub location: Location,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub meter_id: Option<OcpiString<255>>,
49 pub currency: Currency,
51 #[serde(default, skip_serializing_if = "Vec::is_empty")]
53 #[builder(default)]
54 pub charging_periods: Vec<ChargingPeriod>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub total_cost: Option<Number>,
61 pub status: SessionStatus,
63 pub last_updated: DateTime,
65 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
67 #[builder(default)]
68 pub extensions: Extensions,
69}
70
71impl Session {
72 #[must_use]
74 pub fn dimension_total(&self, dimension: CdrDimensionType) -> Number {
75 self.charging_periods
76 .iter()
77 .flat_map(|p| p.dimensions.iter())
78 .filter(|d| d.dimension_type == dimension)
79 .map(|d| d.volume)
80 .sum()
81 }
82}
83
84impl Validate for Session {
85 fn validate_in(&self, v: &mut Validator) {
86 validate_fields!(
87 self, v,
88 id,
89 start_date_time as "start_datetime",
90 end_date_time as "end_datetime",
91 kwh, auth_id, auth_method, location, meter_id, currency, charging_periods, status,
92 last_updated,
93 );
94 if self.end_date_time.is_some_and(|end| end < self.start_date_time) {
95 v.report_at("end_datetime", ViolationCode::Inconsistent, "a session cannot end before it starts");
96 }
97 if self.kwh.is_negative() {
98 v.report_at("kwh", ViolationCode::OutOfRange, "a session cannot charge negative energy");
99 }
100 crate::v2_3_0::cdrs::validate_period_sequence(
101 &self.charging_periods.iter().map(|p| p.start_date_time).collect::<Vec<_>>(),
102 self.start_date_time,
103 self.end_date_time,
104 v,
105 );
106 if self.status == SessionStatus::Completed && self.end_date_time.is_none() {
107 v.report_at(
108 "end_datetime",
109 ViolationCode::MissingConditional,
110 "a COMPLETED session has finished and should carry the time it finished",
111 );
112 }
113 }
114}
115
116ocpi_lenient_enum! {
117 pub enum SessionStatus {
123 Active = "ACTIVE",
125 Completed = "COMPLETED",
127 Invalid = "INVALID",
129 Pending = "PENDING",
131 }
132}
133
134impl SessionStatus {
135 #[must_use]
137 pub fn is_terminal(&self) -> bool {
138 matches!(self, Self::Completed | Self::Invalid)
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn the_timestamp_fields_use_the_2_1_1_spelling_on_the_wire() {
148 let json = r#"{"id":"101","start_datetime":"2015-06-29T22:39:09Z","kwh":0,"auth_id":"DE8ACC12E46L89","auth_method":"WHITELIST","location":{"id":"LOC1","type":"ON_STREET","address":"a","city":"b","postal_code":"c","country":"NLD","coordinates":{"latitude":"51.047599","longitude":"3.729944"},"last_updated":"2015-06-29T20:39:09Z"},"currency":"EUR","status":"PENDING","last_updated":"2015-06-29T22:39:09Z"}"#;
149 let session: Session = serde_json::from_str(json).unwrap();
150 assert!(session.validate().is_ok());
151 let encoded = serde_json::to_string(&session).unwrap();
152 assert!(encoded.contains("\"start_datetime\""), "not start_date_time: {encoded}");
153 assert_eq!(encoded, json);
154 }
155
156 #[test]
157 fn a_violation_points_at_the_wire_field_name() {
158 let mut session: Session = serde_json::from_str(
159 r#"{"id":"101","start_datetime":"2015-06-29T22:39:09Z","kwh":0,"auth_id":"X","auth_method":"WHITELIST","location":{"id":"LOC1","type":"ON_STREET","address":"a","city":"b","postal_code":"c","country":"NLD","coordinates":{"latitude":"51.047599","longitude":"3.729944"},"last_updated":"2015-06-29T20:39:09Z"},"currency":"EUR","status":"PENDING","last_updated":"2015-06-29T22:39:09Z"}"#,
160 )
161 .unwrap();
162 session.status = SessionStatus::Completed;
163 let err = session.validate().unwrap_err();
164 assert_eq!(err.as_slice()[0].pointer, "/end_datetime");
165 }
166
167 #[test]
168 fn the_reservation_status_arrived_later() {
169 assert_eq!(SessionStatus::ALL_KNOWN.len(), 4);
170 assert!(!SessionStatus::from("RESERVATION").is_known());
171 }
172}