Skip to main content

ocpi_kit/v2_2_1/
tokens.rs

1//! The *Tokens* module of OCPI 2.2.1, as a delta from [`v2_3_0::tokens`](crate::v2_3_0::tokens).
2//!
3//! The only change 2.3.0 made is [`TokenType`]: it gained `EMAID`, for ISO 15118 Plug & Charge,
4//! and became an `OpenEnum`. [`Token`] and [`AuthorizationInfo`] are redefined here only because
5//! they carry that type.
6//!
7//! Spec: 2.2.1 §mod_tokens_tokens_module
8
9use bon::Builder;
10use serde::{Deserialize, Serialize};
11
12use crate::ocpi_lenient_enum;
13use crate::types::validate_fields;
14use crate::types::{
15    CiString, ContractId, CountryCode, DateTime, DisplayText, Extensions, OcpiString, PartyId, PartyRef,
16    Validate, Validator, ViolationCode,
17};
18
19use super::sessions::ProfileType;
20
21// Wire-identical to OCPI 2.3.0.
22pub use crate::v2_3_0::tokens::{
23    AllowedType, AuthorizationDecision, EnergyContract, LocationReferences, WhitelistType,
24};
25
26/// A token an EV driver uses to authorize charging, in OCPI 2.2.1.
27///
28/// Spec: 2.2.1 §mod_tokens_token_object
29#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
30#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
31#[builder(on(_, into))]
32pub struct Token {
33    /// ISO-3166 alpha-2 country code of the MSP that 'owns' this Token.
34    pub country_code: CountryCode,
35    /// ID of the eMSP that 'owns' this Token.
36    pub party_id: PartyId,
37    /// Unique ID by which this Token, combined with its type, can be identified.
38    pub uid: CiString<36>,
39    /// Type of the token.
40    #[serde(rename = "type")]
41    pub token_type: TokenType,
42    /// Uniquely identifies the EV driver contract token within the eMSP's platform.
43    pub contract_id: ContractId,
44    /// Visual readable number/identification as printed on the Token.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub visual_number: Option<OcpiString<64>>,
47    /// Issuing company, most of the time the name printed on the token.
48    pub issuer: OcpiString<64>,
49    /// Groups a couple of tokens so a session started with one can be stopped with another.
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub group_id: Option<CiString<36>>,
52    /// Whether this Token is valid.
53    pub valid: bool,
54    /// What type of white-listing is allowed.
55    pub whitelist: WhitelistType,
56    /// Language Code ISO 639-1: the Token owner's preferred interface language.
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub language: Option<OcpiString<2>>,
59    /// The default Charging Preference profile type.
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub default_profile_type: Option<ProfileType>,
62    /// The driver's own energy supplier/contract.
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub energy_contract: Option<EnergyContract>,
65    /// Timestamp when this Token was last updated (or created).
66    pub last_updated: DateTime,
67    /// Undocumented JSON fields, preserved verbatim.
68    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
69    #[builder(default)]
70    pub extensions: Extensions,
71}
72
73impl Token {
74    /// The eMSP that owns this Token.
75    #[must_use]
76    pub fn owner_party(&self) -> PartyRef {
77        PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
78    }
79
80    /// What a CPO should do with this Token when a driver presents it.
81    ///
82    /// See [`v2_3_0::tokens::Token::authorization_decision`](crate::v2_3_0::tokens::Token::authorization_decision);
83    /// the whitelist semantics are identical in both versions.
84    #[must_use]
85    pub fn authorization_decision(&self, online: bool) -> AuthorizationDecision {
86        match self.whitelist {
87            WhitelistType::Always => AuthorizationDecision::AllowFromCache,
88            WhitelistType::Allowed => {
89                if online {
90                    AuthorizationDecision::AuthorizeRealtime
91                } else if self.valid {
92                    AuthorizationDecision::AllowFromCache
93                } else {
94                    AuthorizationDecision::Deny
95                }
96            }
97            WhitelistType::AllowedOffline => {
98                if online {
99                    AuthorizationDecision::AuthorizeRealtime
100                } else {
101                    AuthorizationDecision::AllowFromCache
102                }
103            }
104            WhitelistType::Never => {
105                if online {
106                    AuthorizationDecision::AuthorizeRealtime
107                } else {
108                    AuthorizationDecision::Deny
109                }
110            }
111        }
112    }
113}
114
115impl Validate for Token {
116    fn validate_in(&self, v: &mut Validator) {
117        validate_fields!(
118            self, v, country_code, party_id, uid, token_type as "type", contract_id,
119            visual_number, issuer, group_id, whitelist, language, default_profile_type,
120            energy_contract, last_updated,
121        );
122        if self.group_id.as_ref().is_some_and(|g| g.len() > 20) {
123            v.report_at(
124                "group_id",
125                ViolationCode::Inconsistent,
126                "OCPP 1.5/1.6 only supports group IDs up to 20 characters",
127            );
128        }
129    }
130}
131
132/// The response to a real-time authorization request, in OCPI 2.2.1.
133///
134/// Spec: 2.2.1 §mod_tokens_authorizationinfo_object
135#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
136#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
137#[builder(on(_, into))]
138pub struct AuthorizationInfo {
139    /// Status of the Token, and whether charging is allowed at the optionally given location.
140    pub allowed: AllowedType,
141    /// The complete Token object for which this authorization was requested.
142    pub token: Token,
143    /// The location, if it was in the request and the driver may charge there.
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub location: Option<LocationReferences>,
146    /// Reference to the authorization, echoed later in the relevant Session and CDR.
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub authorization_reference: Option<CiString<36>>,
149    /// Additional information to display to the EV driver.
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub info: Option<DisplayText>,
152    /// Undocumented JSON fields, preserved verbatim.
153    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
154    #[builder(default)]
155    pub extensions: Extensions,
156}
157
158impl Validate for AuthorizationInfo {
159    fn validate_in(&self, v: &mut Validator) {
160        validate_fields!(self, v, allowed, token, location, authorization_reference, info);
161        if self.allowed != AllowedType::Allowed && self.location.is_some() {
162            v.report_at(
163                "location",
164                ViolationCode::Inconsistent,
165                "a location is only returned when the driver is allowed to charge there",
166            );
167        }
168    }
169}
170
171ocpi_lenient_enum! {
172    /// The type of a Token, in OCPI 2.2.1.
173    ///
174    /// OCPI 2.3.0 added `EMAID` and made the enum open. See [`ocpi_lenient_enum!`] for why an
175    /// unrecognised value is still decoded here.
176    ///
177    /// Spec: 2.2.1 §mod_tokens_tokentype_enum
178    pub enum TokenType {
179        /// One-time-use Token ID generated by a server or app.
180        AdHocUser = "AD_HOC_USER",
181        /// Token ID generated by a server or app to identify a user of an app.
182        AppUser = "APP_USER",
183        /// Other type of token.
184        Other = "OTHER",
185        /// RFID Token.
186        Rfid = "RFID",
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn emaid_arrived_in_2_3_0() {
196        assert_eq!(TokenType::ALL_KNOWN.len(), 4);
197        let emaid: TokenType = "EMAID".into();
198        assert!(!emaid.is_known());
199        assert!(emaid.validate().is_err(), "2.2.1 declares TokenType closed");
200        assert_eq!(serde_json::to_string(&emaid).unwrap(), "\"EMAID\"", "but the value survives");
201    }
202
203    #[test]
204    fn whitelist_semantics_are_unchanged_between_versions() {
205        let t = Token::builder()
206            .country_code("NL")
207            .party_id("TNM")
208            .uid("012345678")
209            .token_type(TokenType::Rfid)
210            .contract_id("NL-TNM-C12345678-X")
211            .issuer("TheNewMotion")
212            .valid(true)
213            .whitelist(WhitelistType::Never)
214            .last_updated("2024-01-01T00:00:00Z".parse::<DateTime>().unwrap())
215            .build();
216        assert_eq!(t.authorization_decision(false), AuthorizationDecision::Deny);
217        assert_eq!(t.authorization_decision(true), AuthorizationDecision::AuthorizeRealtime);
218        assert!(t.validate().is_ok());
219    }
220}