Skip to main content

ocpi_kit/v2_2_1/
sessions.rs

1//! The *Sessions* module of OCPI 2.2.1, as a delta from
2//! [`v2_3_0::sessions`](crate::v2_3_0::sessions).
3//!
4//! Only [`Session`] is redefined, because its `total_cost` is the 2.2.1
5//! [`Price`] and its `cdr_token` the 2.2.1
6//! [`CdrToken`].
7//!
8//! Spec: 2.2.1 §mod_sessions_sessions_module
9
10use bon::Builder;
11use serde::{Deserialize, Serialize};
12
13use crate::types::validate_fields;
14use crate::types::{
15    CiString, CountryCode, Currency, DateTime, Extensions, Number, OcpiString, PartyId, PartyRef, Validate,
16    Validator, ViolationCode,
17};
18
19use super::cdrs::{AuthMethod, CdrDimensionType, CdrToken, ChargingPeriod};
20use super::types::Price;
21
22// Wire-identical to OCPI 2.3.0.
23pub use crate::v2_3_0::sessions::{
24    ChargingPreferences, ChargingPreferencesResponse, ProfileType, SessionStatus,
25};
26
27/// One charging session, as it stands right now, in OCPI 2.2.1.
28///
29/// Spec: 2.2.1 §mod_sessions_session_object
30#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
31#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
32#[builder(on(_, into))]
33pub struct Session {
34    /// ISO-3166 alpha-2 country code of the CPO that 'owns' this Session.
35    pub country_code: CountryCode,
36    /// ID of the CPO that 'owns' this Session.
37    pub party_id: PartyId,
38    /// The unique id that identifies the charging session in the CPO platform.
39    pub id: CiString<36>,
40    /// When the session became `ACTIVE` in the Charge Point.
41    pub start_date_time: DateTime,
42    /// When the session was completed.
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub end_date_time: Option<DateTime>,
45    /// How many kWh were charged.
46    pub kwh: Number,
47    /// Token used to start this charging session.
48    pub cdr_token: CdrToken,
49    /// Method used for authentication.
50    pub auth_method: AuthMethod,
51    /// Reference to the authorization given by the eMSP.
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub authorization_reference: Option<CiString<36>>,
54    /// `Location.id` on which the charging session is or was happening.
55    pub location_id: CiString<36>,
56    /// `EVSE.uid` on which the charging session is or was happening. May be `#NA`.
57    pub evse_uid: CiString<36>,
58    /// `Connector.id` where the charging session is or was happening. May be `#NA`.
59    pub connector_id: CiString<36>,
60    /// Optional identification of the kWh meter.
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub meter_id: Option<OcpiString<255>>,
63    /// ISO 4217 code of the currency used for this session.
64    pub currency: Currency,
65    /// Charging Periods that can be used to calculate and verify the total cost.
66    #[serde(default, skip_serializing_if = "Vec::is_empty")]
67    #[builder(default)]
68    pub charging_periods: Vec<ChargingPeriod>,
69    /// The total cost of the session.
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub total_cost: Option<Price>,
72    /// The status of the session.
73    pub status: SessionStatus,
74    /// Timestamp when this Session was last updated (or created).
75    pub last_updated: DateTime,
76    /// Undocumented JSON fields, preserved verbatim.
77    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
78    #[builder(default)]
79    pub extensions: Extensions,
80}
81
82impl Session {
83    /// The CPO that owns this Session.
84    #[must_use]
85    pub fn owner_party(&self) -> PartyRef {
86        PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
87    }
88
89    /// Whether an EVSE and connector have been assigned yet.
90    #[must_use]
91    pub fn has_assigned_evse(&self) -> bool {
92        !self.evse_uid.is_not_available() && !self.connector_id.is_not_available()
93    }
94
95    /// The total volume of one dimension across every charging period.
96    #[must_use]
97    pub fn dimension_total(&self, dimension: CdrDimensionType) -> Number {
98        self.charging_periods
99            .iter()
100            .flat_map(|p| p.dimensions.iter())
101            .filter(|d| d.dimension_type == dimension)
102            .map(|d| d.volume)
103            .sum()
104    }
105}
106
107impl Validate for Session {
108    fn validate_in(&self, v: &mut Validator) {
109        validate_fields!(
110            self,
111            v,
112            country_code,
113            party_id,
114            id,
115            start_date_time,
116            end_date_time,
117            kwh,
118            cdr_token,
119            auth_method,
120            authorization_reference,
121            location_id,
122            evse_uid,
123            connector_id,
124            meter_id,
125            currency,
126            charging_periods,
127            total_cost,
128            status,
129            last_updated,
130        );
131        crate::v2_3_0::cdrs::validate_period_sequence(
132            &self.charging_periods.iter().map(|p| p.start_date_time).collect::<Vec<_>>(),
133            self.start_date_time,
134            self.end_date_time,
135            v,
136        );
137        if self.end_date_time.is_some_and(|end| end < self.start_date_time) {
138            v.report_at(
139                "end_date_time",
140                ViolationCode::Inconsistent,
141                "a session cannot end before it starts",
142            );
143        }
144        if self.kwh.is_negative() {
145            v.report_at("kwh", ViolationCode::OutOfRange, "a session cannot charge negative energy");
146        }
147        if self.status == SessionStatus::Completed && self.end_date_time.is_none() {
148            v.report_at(
149                "end_date_time",
150                ViolationCode::MissingConditional,
151                "a COMPLETED session has finished and should carry the time it finished",
152            );
153        }
154    }
155}