Skip to main content

ocpi_kit/client/
modules.rs

1//! Typed clients for the OCPI modules, in the canonical 2.3.0 model.
2//!
3//! Each module has up to two clients: a **Sender** client, which pulls from the party that owns the
4//! data, and a **Receiver** client, which pushes to the party that receives it.
5//!
6//! They take and return [`v2_3_0`](crate::v2_3_0) objects **whatever version the peer speaks**: a
7//! 2.2.1 CPO's Locations arrive here as 2.3.0 objects, and a `PUT` back to it is written in 2.2.1.
8//! Load-bearing rather than convenient — a 2.2.1 `Tariff` has no `tax_included`, which 2.3.0
9//! requires. The translation is [`convert`](crate::convert), and an outgoing object that loses a
10//! field logs a `tracing` warning naming it by JSON Pointer.
11//!
12//! [`ModuleClient`]'s own `get`/`put`/`post`/`patch`/`list` translate nothing and decode exactly
13//! the type you name; the `*_bridged` variants beside them are what the typed clients use.
14
15use http::Method;
16use serde::Serialize;
17use serde::de::DeserializeOwned;
18
19use crate::convert::wire::{BridgeError, ObjectKind};
20use crate::transport::{
21    OcpiError, OcpiRequest, Page, PageQuery, Patch, ReceiverEndpoint, RequestIds, RoutingHeaders,
22    SenderEndpoint,
23};
24use crate::types::{PartyRef, Url, Validate};
25use crate::v2_3_0::tokens::TokenType;
26use crate::{InterfaceRole, ModuleId};
27
28use super::http::{Transport, check_outgoing};
29use super::paging::PageStream;
30use super::peer::Peer;
31
32/// The shared plumbing of every module client.
33#[derive(Clone, Debug)]
34pub struct ModuleClient<'a> {
35    transport: &'a Transport,
36    peer: &'a Peer,
37    module: ModuleId,
38    from: PartyRef,
39    to: Option<PartyRef>,
40}
41
42impl<'a> ModuleClient<'a> {
43    /// Builds a client for one module of one peer.
44    ///
45    /// `from` is the party this process is speaking as; `to` is the party at the peer, which
46    /// defaults to the peer's only party when it has just one.
47    #[must_use]
48    pub fn new(transport: &'a Transport, peer: &'a Peer, module: ModuleId, from: PartyRef) -> Self {
49        let to = peer.default_party().cloned();
50        Self { transport, peer, module, from, to }
51    }
52
53    /// Addresses a specific party at the peer, for a platform that hosts several.
54    #[must_use]
55    pub fn to(mut self, party: PartyRef) -> Self {
56        self.to = Some(party);
57        self
58    }
59
60    /// Omits the `OCPI-to-*` headers, making this an Open Routing Request.
61    ///
62    /// > *For an Open Routing Request, the TO headers in the request from the requesting party to
63    /// > the Hub MUST be omitted.*
64    #[must_use]
65    pub fn open_routing(mut self) -> Self {
66        self.to = None;
67        self
68    }
69
70    /// The peer this client talks to.
71    #[must_use]
72    pub const fn peer(&self) -> &Peer {
73        self.peer
74    }
75
76    /// The Sender endpoint of this module, if the peer implements it.
77    #[must_use]
78    pub fn sender_endpoint(&self) -> Option<SenderEndpoint> {
79        self.peer.sender(&self.module)
80    }
81
82    /// The Receiver endpoint of this module, if the peer implements it.
83    #[must_use]
84    pub fn receiver_endpoint(&self) -> Option<ReceiverEndpoint> {
85        self.peer.receiver(&self.module)
86    }
87
88    fn routing(&self) -> RoutingHeaders {
89        RoutingHeaders { to: self.to.clone(), from: self.from.clone() }
90    }
91
92    fn request(&self, method: Method, url: Url) -> OcpiRequest {
93        OcpiRequest::new(method, url, self.module.clone()).routed(self.routing())
94    }
95
96    fn missing(&self, role: InterfaceRole) -> OcpiError {
97        OcpiError::NotFound(format!(
98            "the peer does not implement the {} interface of the {} module",
99            role, self.module
100        ))
101    }
102
103    /// `GET {url}`, decoding one object.
104    ///
105    /// # Errors
106    ///
107    /// Propagates transport, decoding and OCPI-level errors.
108    pub async fn get<T: DeserializeOwned>(&self, url: Url) -> Result<T, OcpiError> {
109        let request = self.request(Method::GET, url);
110        self.transport.send(&request, self.peer.token(), self.peer.quirks()).await
111    }
112
113    /// `GET {url}`, decoding one page of a list endpoint.
114    ///
115    /// # Errors
116    ///
117    /// Propagates transport, decoding and OCPI-level errors.
118    pub async fn get_page<T: DeserializeOwned>(&self, url: Url) -> Result<Page<T>, OcpiError> {
119        let request = self.request(Method::GET, url);
120        self.transport.send_page(&request, self.peer.token(), self.peer.quirks()).await
121    }
122
123    /// `PUT {url}` with a body, discarding the response payload.
124    ///
125    /// # Errors
126    ///
127    /// Returns [`OcpiError::Invalid`] when the body does not conform and the client is configured
128    /// to check outgoing objects, plus the usual transport and OCPI errors.
129    pub async fn put<T: Serialize + Validate>(&self, url: Url, body: &T) -> Result<(), OcpiError> {
130        check_outgoing(body, self.transport.config())?;
131        let request = self.request(Method::PUT, url).with_body(body)?;
132        let (response, _) = self
133            .transport
134            .send_with_headers::<serde_json::Value>(&request, self.peer.token(), self.peer.quirks())
135            .await?;
136        if response.is_success() { Ok(()) } else { Err(response.into_result().unwrap_err()) }
137    }
138
139    /// `POST {url}` with a body, decoding the response payload.
140    ///
141    /// # Errors
142    ///
143    /// As [`ModuleClient::put`].
144    pub async fn post<B: Serialize + Validate, T: DeserializeOwned>(
145        &self,
146        url: Url,
147        body: &B,
148    ) -> Result<T, OcpiError> {
149        check_outgoing(body, self.transport.config())?;
150        let request = self.request(Method::POST, url).with_body(body)?;
151        self.transport.send(&request, self.peer.token(), self.peer.quirks()).await
152    }
153
154    /// `PATCH {url}` with a merge patch.
155    ///
156    /// The patch must carry `last_updated`; the specification's own example of a `2001 Invalid or
157    /// missing parameters` is a PATCH that does not.
158    ///
159    /// # Errors
160    ///
161    /// Returns [`OcpiError::Decode`] when the patch has no `last_updated`, plus the usual
162    /// transport and OCPI errors.
163    pub async fn patch<T>(&self, url: Url, patch: &Patch<T>) -> Result<(), OcpiError> {
164        if patch.last_updated().is_none() {
165            return Err(OcpiError::Decode {
166                path: "/last_updated".to_owned(),
167                message: "a PATCH must carry `last_updated`".to_owned(),
168            });
169        }
170        let request = self.request(Method::PATCH, url).with_body(patch.as_value())?;
171        let (response, _) = self
172            .transport
173            .send_with_headers::<serde_json::Value>(&request, self.peer.token(), self.peer.quirks())
174            .await?;
175        if response.is_success() { Ok(()) } else { Err(response.into_result().unwrap_err()) }
176    }
177
178    /// `DELETE {url}`.
179    ///
180    /// # Errors
181    ///
182    /// Propagates transport and OCPI-level errors.
183    pub async fn delete(&self, url: Url) -> Result<(), OcpiError> {
184        let request = self.request(Method::DELETE, url);
185        let (response, _) = self
186            .transport
187            .send_with_headers::<serde_json::Value>(&request, self.peer.token(), self.peer.quirks())
188            .await?;
189        if response.is_success() { Ok(()) } else { Err(response.into_result().unwrap_err()) }
190    }
191
192    /// The peer's version, when it is one this crate has to translate for. `None` is the fast
193    /// path: the peer already speaks the canonical model.
194    fn foreign_version(&self) -> Option<&crate::VersionNumber> {
195        let version = self.peer.version();
196        (*version != crate::CANONICAL_VERSION).then_some(version)
197    }
198
199    /// `GET {url}`, translating the peer's version into the canonical model.
200    ///
201    /// # Errors
202    ///
203    /// As [`ModuleClient::get`], plus [`OcpiError::Decode`] when the peer's document is not the
204    /// object this endpoint carries, and [`OcpiError::Unsupported`] when this build has no
205    /// conversions for the peer's version.
206    pub async fn get_bridged<T: DeserializeOwned>(&self, url: Url, kind: ObjectKind) -> Result<T, OcpiError> {
207        let Some(theirs) = self.foreign_version() else { return self.get(url).await };
208        let value: serde_json::Value = self.get(url).await?;
209        let converted =
210            kind.bridge(theirs, &crate::CANONICAL_VERSION, value).map_err(|e| bridge_error(e, kind))?;
211        decode(converted.value)
212    }
213
214    /// `PUT {url}`, writing the body in the version the peer speaks.
215    ///
216    /// # Errors
217    ///
218    /// As [`ModuleClient::put`], plus [`OcpiError::Unsupported`] when this build cannot write the
219    /// peer's version.
220    pub async fn put_bridged<T: Serialize + Validate>(
221        &self,
222        url: Url,
223        body: &T,
224        kind: ObjectKind,
225    ) -> Result<(), OcpiError> {
226        check_outgoing(body, self.transport.config())?;
227        let Some(value) = self.for_peer(body, kind)? else { return self.put(url, body).await };
228        let request = self.request(Method::PUT, url).with_body(&value)?;
229        self.expect_success(request).await
230    }
231
232    /// `POST {url}`, writing the body in the peer's version and reading the answer back out of it.
233    ///
234    /// `request_kind` and `response_kind` are separate because two OCPI endpoints send one object
235    /// and answer with another — `POST {tokens}/{uid}/authorize` takes a `LocationReferences` and
236    /// returns an `AuthorizationInfo`.
237    ///
238    /// # Errors
239    ///
240    /// As [`ModuleClient::post`], plus the translation errors of
241    /// [`ModuleClient::get_bridged`].
242    pub async fn post_bridged<B: Serialize + Validate, T: DeserializeOwned>(
243        &self,
244        url: Url,
245        body: &B,
246        request_kind: Option<ObjectKind>,
247        response_kind: Option<ObjectKind>,
248    ) -> Result<T, OcpiError> {
249        check_outgoing(body, self.transport.config())?;
250        let Some(theirs) = self.foreign_version().cloned() else {
251            return self.post(url, body).await;
252        };
253        let request = match request_kind.and_then(|k| self.for_peer(body, k).transpose()) {
254            Some(value) => self.request(Method::POST, url).with_body(&value?)?,
255            None => self.request(Method::POST, url).with_body(body)?,
256        };
257        let answer: serde_json::Value =
258            self.transport.send(&request, self.peer.token(), self.peer.quirks()).await?;
259        let Some(kind) = response_kind else { return decode(answer) };
260        let converted =
261            kind.bridge(&theirs, &crate::CANONICAL_VERSION, answer).map_err(|e| bridge_error(e, kind))?;
262        decode(converted.value)
263    }
264
265    /// `PATCH {url}` against a peer on another version.
266    ///
267    /// A merge patch is not an object, so it cannot be decoded, converted and re-encoded. It does
268    /// not have to be: a patch writing only fields the two versions agree about means the same
269    /// thing in both. One that writes a field they disagree about is refused, with the
270    /// specification's own GET → PUT recovery in the message.
271    ///
272    /// # Errors
273    ///
274    /// As [`ModuleClient::patch`], plus [`OcpiError::Unsupported`] when the patch writes a field
275    /// whose shape differs between the two versions.
276    pub async fn patch_bridged<T>(
277        &self,
278        url: Url,
279        patch: &Patch<T>,
280        kind: ObjectKind,
281    ) -> Result<(), OcpiError> {
282        if let Some(theirs) = self.foreign_version()
283            && !kind.patch_crosses_unchanged(&patch.fields())
284        {
285            return Err(OcpiError::Unsupported(format!(
286                "this PATCH writes {:?}, and a {kind} does not carry {} the same way in OCPI \
287                 {theirs} as in OCPI {}; a merge patch is not an object, so it cannot be \
288                 translated. GET the object and PUT it back instead, which is the recovery the \
289                 specification prescribes for a refused PATCH",
290                patch.fields(),
291                kind.divergent_fields().join(", "),
292                crate::CANONICAL_VERSION,
293            )));
294        }
295        self.patch(url, patch).await
296    }
297
298    /// Crawls a list endpoint, translating every page into the canonical model.
299    ///
300    /// # Errors
301    ///
302    /// As [`ModuleClient::list`].
303    pub fn list_bridged<T: DeserializeOwned + Send + 'static>(
304        &self,
305        query: PageQuery,
306        kind: ObjectKind,
307    ) -> Result<PageStream<'a, T>, OcpiError> {
308        Ok(self.list(query)?.bridging(kind))
309    }
310
311    /// Serialises `body` in the peer's version, or `None` when nothing has to change.
312    fn for_peer<T: Serialize>(
313        &self,
314        body: &T,
315        kind: ObjectKind,
316    ) -> Result<Option<serde_json::Value>, OcpiError> {
317        let Some(theirs) = self.foreign_version() else { return Ok(None) };
318        let value = serde_json::to_value(body)
319            .map_err(|e| OcpiError::Decode { path: "/".to_owned(), message: e.to_string() })?;
320        let converted =
321            kind.bridge(&crate::CANONICAL_VERSION, theirs, value).map_err(|e| bridge_error(e, kind))?;
322        if let Some(note) = converted.lossy.to_status_message() {
323            tracing::warn!(
324                ocpi.peer_version = %theirs,
325                ocpi.object = %kind,
326                "{note}",
327            );
328        }
329        Ok(Some(converted.value))
330    }
331
332    async fn expect_success(&self, request: OcpiRequest) -> Result<(), OcpiError> {
333        let (response, _) = self
334            .transport
335            .send_with_headers::<serde_json::Value>(&request, self.peer.token(), self.peer.quirks())
336            .await?;
337        if response.is_success() { Ok(()) } else { Err(response.into_result().unwrap_err()) }
338    }
339
340    /// Crawls every page of a Sender list endpoint.
341    ///
342    /// The objects arrive exactly as the peer wrote them; see [`ModuleClient::list_bridged`] for
343    /// the version-translating form the typed clients use.
344    ///
345    /// # Errors
346    ///
347    /// Returns [`OcpiError::NotFound`] when the peer does not implement the Sender interface.
348    pub fn list<T: DeserializeOwned + Send + 'static>(
349        &self,
350        query: PageQuery,
351    ) -> Result<PageStream<'a, T>, OcpiError> {
352        let endpoint = self.sender_endpoint().ok_or_else(|| self.missing(InterfaceRole::Sender))?;
353        let query = match self.peer.quirks().peer_max_page_limit {
354            Some(max) => query.clamped_to(max),
355            None => query,
356        };
357        Ok(PageStream::new(
358            self.transport,
359            self.peer,
360            self.module.clone(),
361            self.routing(),
362            endpoint.list(&query),
363        ))
364    }
365}
366
367impl<'a> ModuleClient<'a> {
368    /// A paginated crawl starting from an explicit URL rather than the module's own list
369    /// endpoint.
370    ///
371    /// Most modules have one list endpoint and [`list`](Self::list) finds it. Payments is the
372    /// exception: it declares a single `ModuleID` and then addresses its two interfaces through
373    /// two different endpoint variables, which version discovery cannot express, so the sub-path
374    /// has to come from the caller. See
375    /// [`SenderEndpoint::payments_terminals`](crate::transport::SenderEndpoint::payments_terminals).
376    #[must_use]
377    pub fn list_from<T: DeserializeOwned + Send + 'static>(
378        &self,
379        base: &Url,
380        query: &PageQuery,
381    ) -> PageStream<'a, T> {
382        PageStream::new(self.transport, self.peer, self.module.clone(), self.routing(), query.apply_to(base))
383    }
384}
385
386/// Pulls Locations from a CPO.
387///
388/// Spec: 2.3.0 §mod_locations_cpo_interface
389#[derive(Clone, Debug)]
390pub struct LocationsSender<'a>(ModuleClient<'a>);
391
392impl<'a> LocationsSender<'a> {
393    /// Wraps a module client.
394    #[must_use]
395    pub const fn new(client: ModuleClient<'a>) -> Self {
396        Self(client)
397    }
398
399    /// `GET {locations}` — every Location, paginated.
400    ///
401    /// # Errors
402    ///
403    /// As [`ModuleClient::list`].
404    pub fn list(
405        &self,
406        query: PageQuery,
407    ) -> Result<PageStream<'a, crate::v2_3_0::locations::Location>, OcpiError> {
408        self.0.list_bridged(query, ObjectKind::Location)
409    }
410
411    /// `GET {locations}/{location_id}`.
412    ///
413    /// # Errors
414    ///
415    /// Propagates transport and OCPI-level errors.
416    pub async fn location(&self, location_id: &str) -> Result<crate::v2_3_0::locations::Location, OcpiError> {
417        let endpoint = self.0.sender_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Sender))?;
418        self.0.get_bridged(endpoint.location(location_id, None, None), ObjectKind::Location).await
419    }
420
421    /// `GET {locations}/{location_id}/{evse_uid}`.
422    ///
423    /// # Errors
424    ///
425    /// Propagates transport and OCPI-level errors.
426    pub async fn evse(
427        &self,
428        location_id: &str,
429        evse_uid: &str,
430    ) -> Result<crate::v2_3_0::locations::Evse, OcpiError> {
431        let endpoint = self.0.sender_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Sender))?;
432        self.0.get_bridged(endpoint.location(location_id, Some(evse_uid), None), ObjectKind::Evse).await
433    }
434
435    /// `GET {locations}/{location_id}/{evse_uid}/{connector_id}`.
436    ///
437    /// # Errors
438    ///
439    /// Propagates transport and OCPI-level errors.
440    pub async fn connector(
441        &self,
442        location_id: &str,
443        evse_uid: &str,
444        connector_id: &str,
445    ) -> Result<crate::v2_3_0::locations::Connector, OcpiError> {
446        let endpoint = self.0.sender_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Sender))?;
447        self.0
448            .get_bridged(
449                endpoint.location(location_id, Some(evse_uid), Some(connector_id)),
450                ObjectKind::Connector,
451            )
452            .await
453    }
454}
455
456/// Pushes Locations to an eMSP or NSP.
457///
458/// These are **client-owned objects**: the URL carries the owner's `country_code` and `party_id`,
459/// and `POST` is not used.
460///
461/// > *POST is not supported for these kinds of modules. PUT is used to send new objects.*
462///
463/// Spec: 2.3.0 §mod_locations_emsp_interface
464#[derive(Clone, Debug)]
465pub struct LocationsReceiver<'a>(ModuleClient<'a>);
466
467impl<'a> LocationsReceiver<'a> {
468    /// Wraps a module client.
469    #[must_use]
470    pub const fn new(client: ModuleClient<'a>) -> Self {
471        Self(client)
472    }
473
474    /// `PUT {locations}/{country_code}/{party_id}/{location_id}`.
475    ///
476    /// # Errors
477    ///
478    /// Propagates validation, transport and OCPI-level errors.
479    pub async fn put_location(
480        &self,
481        owner: &PartyRef,
482        location: &crate::v2_3_0::locations::Location,
483    ) -> Result<(), OcpiError> {
484        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
485        self.0
486            .put_bridged(
487                endpoint.location(owner, location.id.as_str(), None, None),
488                location,
489                ObjectKind::Location,
490            )
491            .await
492    }
493
494    /// `PUT {locations}/{country_code}/{party_id}/{location_id}/{evse_uid}`.
495    ///
496    /// # Errors
497    ///
498    /// Propagates validation, transport and OCPI-level errors.
499    pub async fn put_evse(
500        &self,
501        owner: &PartyRef,
502        location_id: &str,
503        evse: &crate::v2_3_0::locations::Evse,
504    ) -> Result<(), OcpiError> {
505        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
506        self.0
507            .put_bridged(
508                endpoint.location(owner, location_id, Some(evse.uid.as_str()), None),
509                evse,
510                ObjectKind::Evse,
511            )
512            .await
513    }
514
515    /// `PATCH {locations}/{country_code}/{party_id}/{location_id}[/{evse_uid}[/{connector_id}]]`.
516    ///
517    /// This is how an EVSE is retired: *"REMOVED via PATCH status, never DELETE"*.
518    ///
519    /// # Errors
520    ///
521    /// Propagates transport and OCPI-level errors, and refuses a patch without `last_updated`.
522    pub async fn patch<T>(
523        &self,
524        owner: &PartyRef,
525        location_id: &str,
526        evse_uid: Option<&str>,
527        connector_id: Option<&str>,
528        patch: &Patch<T>,
529    ) -> Result<(), OcpiError> {
530        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
531        let kind = match (evse_uid, connector_id) {
532            (None, _) => ObjectKind::Location,
533            (Some(_), None) => ObjectKind::Evse,
534            (Some(_), Some(_)) => ObjectKind::Connector,
535        };
536        self.0.patch_bridged(endpoint.location(owner, location_id, evse_uid, connector_id), patch, kind).await
537    }
538}
539
540/// Real-time authorization and Token pulls, on the eMSP's Sender interface.
541///
542/// Spec: 2.3.0 §mod_tokens_emsp_interface
543#[derive(Clone, Debug)]
544pub struct TokensSender<'a>(ModuleClient<'a>);
545
546impl<'a> TokensSender<'a> {
547    /// Wraps a module client.
548    #[must_use]
549    pub const fn new(client: ModuleClient<'a>) -> Self {
550        Self(client)
551    }
552
553    /// `GET {tokens}` — every Token, paginated.
554    ///
555    /// # Errors
556    ///
557    /// As [`ModuleClient::list`].
558    pub fn list(&self, query: PageQuery) -> Result<PageStream<'a, crate::v2_3_0::tokens::Token>, OcpiError> {
559        self.0.list_bridged(query, ObjectKind::Token)
560    }
561
562    /// `POST {tokens}/{token_uid}/authorize[?type=]` — a real-time authorization.
563    ///
564    /// > *`LocationReferences`: Location and EVSEs for which the driver wants to charge.*
565    ///
566    /// # Errors
567    ///
568    /// Propagates transport and OCPI-level errors; a `2004 Unknown Token` from the eMSP arrives
569    /// as [`OcpiError::Remote`].
570    pub async fn authorize(
571        &self,
572        token_uid: &str,
573        token_type: Option<crate::v2_3_0::tokens::TokenType>,
574        location: Option<&crate::v2_3_0::tokens::LocationReferences>,
575    ) -> Result<crate::v2_3_0::tokens::AuthorizationInfo, OcpiError> {
576        let endpoint = self.0.sender_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Sender))?;
577        let url = endpoint.token_authorize(
578            token_uid,
579            token_type.as_ref().map(super::super::v2_3_0::tokens::TokenType::as_str),
580        );
581        // The request is a `LocationReferences`, which is the same object in both versions;
582        // the answer is an `AuthorizationInfo`, which is not.
583        match location {
584            Some(references) => {
585                self.0.post_bridged(url, references, None, Some(ObjectKind::AuthorizationInfo)).await
586            }
587            // The body is optional; an empty object keeps the Content-Type consistent.
588            None => {
589                self.0
590                    .post_bridged(url, &serde_json::json!({}), None, Some(ObjectKind::AuthorizationInfo))
591                    .await
592            }
593        }
594    }
595}
596
597/// Pulls CDRs from a CPO, and pushes them to an eMSP.
598///
599/// The CDRs module is the one place OCPI uses a server-owned `POST`:
600///
601/// > *POST … returns the URL to the new object in the `Location` header.*
602///
603/// Spec: 2.3.0 §mod_cdrs_cdrs_module
604#[derive(Clone, Debug)]
605pub struct CdrsClient<'a>(ModuleClient<'a>);
606
607impl<'a> CdrsClient<'a> {
608    /// Wraps a module client.
609    #[must_use]
610    pub const fn new(client: ModuleClient<'a>) -> Self {
611        Self(client)
612    }
613
614    /// `GET {cdrs}` — every CDR, paginated.
615    ///
616    /// # Errors
617    ///
618    /// As [`ModuleClient::list`].
619    pub fn list(&self, query: PageQuery) -> Result<PageStream<'a, crate::v2_3_0::cdrs::Cdr>, OcpiError> {
620        self.0.list_bridged(query, ObjectKind::Cdr)
621    }
622
623    /// `POST {cdrs}` — pushes a CDR, returning the URL from the `Location` response header.
624    ///
625    /// # Errors
626    ///
627    /// Propagates validation, transport and OCPI-level errors.
628    pub async fn post(&self, cdr: &crate::v2_3_0::cdrs::Cdr) -> Result<Option<Url>, OcpiError> {
629        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
630        check_outgoing(cdr, self.0.transport.config())?;
631        let request = match self.0.for_peer(cdr, ObjectKind::Cdr)? {
632            Some(value) => self.0.request(Method::POST, endpoint.base().clone()).with_body(&value)?,
633            None => self.0.request(Method::POST, endpoint.base().clone()).with_body(cdr)?,
634        };
635        let (response, headers) = self
636            .0
637            .transport
638            .send_with_headers::<serde_json::Value>(&request, self.0.peer.token(), self.0.peer.quirks())
639            .await?;
640        if !response.is_success() {
641            return Err(response.into_result().unwrap_err());
642        }
643        Ok(crate::transport::header_str(&headers, &crate::transport::headers::LOCATION).map(Url::new_lenient))
644    }
645}
646
647/// Sends commands to a CPO.
648///
649/// Spec: 2.3.0 §mod_commands_commands_module
650#[derive(Clone, Debug)]
651pub struct CommandsClient<'a>(ModuleClient<'a>);
652
653impl<'a> CommandsClient<'a> {
654    /// Wraps a module client.
655    #[must_use]
656    pub const fn new(client: ModuleClient<'a>) -> Self {
657        Self(client)
658    }
659
660    /// `POST {commands}/{command}` — sends a command and returns the synchronous response.
661    ///
662    /// The [`CommandResponse`](crate::v2_3_0::commands::CommandResponse) carries a `timeout`; the
663    /// asynchronous [`CommandResult`](crate::v2_3_0::commands::CommandResult) arrives later at the
664    /// `response_url` the command carried, which this party must be serving.
665    ///
666    /// # Errors
667    ///
668    /// Propagates validation, transport and OCPI-level errors.
669    pub async fn send(
670        &self,
671        command: &crate::v2_3_0::commands::Command,
672    ) -> Result<crate::v2_3_0::commands::CommandResponse, OcpiError> {
673        use crate::v2_3_0::commands::Command;
674        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
675        // The `response_url` is a URL this party will be called back on, so it is checked against
676        // the same policy as anything else this process would fetch.
677        let url = endpoint.base().join(command.command_type().as_str());
678        match command {
679            // `CommandResponse` is the same object in every version; only two of the five
680            // request bodies carry a `Token`, and only that is translated.
681            Command::CancelReservation(c) => self.0.post_bridged(url, c, None, None).await,
682            Command::ReserveNow(c) => {
683                self.0.post_bridged(url, c.as_ref(), Some(ObjectKind::ReserveNow), None).await
684            }
685            Command::StartSession(c) => {
686                self.0.post_bridged(url, c.as_ref(), Some(ObjectKind::StartSession), None).await
687            }
688            Command::StopSession(c) => self.0.post_bridged(url, c, None, None).await,
689            Command::UnlockConnector(c) => self.0.post_bridged(url, c, None, None).await,
690        }
691    }
692}
693
694/// Pulls Sessions from a CPO, and sets a driver's charging preferences.
695///
696/// Spec: 2.3.0 §mod_sessions_cpo_interface
697#[derive(Clone, Debug)]
698pub struct SessionsSender<'a>(ModuleClient<'a>);
699
700impl<'a> SessionsSender<'a> {
701    /// Wraps a module client.
702    #[must_use]
703    pub const fn new(client: ModuleClient<'a>) -> Self {
704        Self(client)
705    }
706
707    /// `GET {sessions}` — every Session, paginated.
708    ///
709    /// # Errors
710    ///
711    /// As [`ModuleClient::list`].
712    pub fn list(
713        &self,
714        query: PageQuery,
715    ) -> Result<PageStream<'a, crate::v2_3_0::sessions::Session>, OcpiError> {
716        self.0.list_bridged(query, ObjectKind::Session)
717    }
718
719    /// `PUT {sessions}/{session_id}/charging_preferences`.
720    ///
721    /// The response is a
722    /// [`ChargingPreferencesResponse`](crate::v2_3_0::sessions::ChargingPreferencesResponse), not
723    /// an acknowledgement: a CPO that accepts the request may still answer
724    /// `NOT_POSSIBLE`, and the caller has to look.
725    ///
726    /// # Errors
727    ///
728    /// Propagates validation, transport and OCPI-level errors.
729    pub async fn set_charging_preferences(
730        &self,
731        session_id: &str,
732        preferences: &crate::v2_3_0::sessions::ChargingPreferences,
733    ) -> Result<crate::v2_3_0::sessions::ChargingPreferencesResponse, OcpiError> {
734        let endpoint = self.0.sender_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Sender))?;
735        check_outgoing(preferences, self.0.transport.config())?;
736        let request =
737            self.0.request(Method::PUT, endpoint.charging_preferences(session_id)).with_body(preferences)?;
738        self.0.transport.send(&request, self.0.peer.token(), self.0.peer.quirks()).await
739    }
740}
741
742/// Pushes Sessions to an eMSP.
743///
744/// Spec: 2.3.0 §mod_sessions_emsp_interface
745#[derive(Clone, Debug)]
746pub struct SessionsReceiver<'a>(ModuleClient<'a>);
747
748impl<'a> SessionsReceiver<'a> {
749    /// Wraps a module client.
750    #[must_use]
751    pub const fn new(client: ModuleClient<'a>) -> Self {
752        Self(client)
753    }
754
755    /// `GET {sessions}/{country_code}/{party_id}/{session_id}` — what the peer has stored.
756    ///
757    /// # Errors
758    ///
759    /// Propagates transport and OCPI-level errors.
760    pub async fn session(
761        &self,
762        owner: &PartyRef,
763        session_id: &str,
764    ) -> Result<crate::v2_3_0::sessions::Session, OcpiError> {
765        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
766        self.0.get_bridged(endpoint.object(owner, session_id), ObjectKind::Session).await
767    }
768
769    /// `PUT {sessions}/{country_code}/{party_id}/{session_id}`.
770    ///
771    /// # Errors
772    ///
773    /// Propagates validation, transport and OCPI-level errors.
774    pub async fn put_session(
775        &self,
776        owner: &PartyRef,
777        session: &crate::v2_3_0::sessions::Session,
778    ) -> Result<(), OcpiError> {
779        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
780        self.0.put_bridged(endpoint.object(owner, session.id.as_str()), session, ObjectKind::Session).await
781    }
782
783    /// `PATCH {sessions}/{country_code}/{party_id}/{session_id}`.
784    ///
785    /// # Errors
786    ///
787    /// Propagates transport and OCPI-level errors, and refuses a patch without `last_updated`.
788    pub async fn patch<T>(
789        &self,
790        owner: &PartyRef,
791        session_id: &str,
792        patch: &Patch<T>,
793    ) -> Result<(), OcpiError> {
794        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
795        self.0.patch_bridged(endpoint.object(owner, session_id), patch, ObjectKind::Session).await
796    }
797}
798
799/// Pulls Tariffs from a CPO.
800///
801/// Spec: 2.3.0 §mod_tariffs_cpo_interface
802#[derive(Clone, Debug)]
803pub struct TariffsSender<'a>(ModuleClient<'a>);
804
805impl<'a> TariffsSender<'a> {
806    /// Wraps a module client.
807    #[must_use]
808    pub const fn new(client: ModuleClient<'a>) -> Self {
809        Self(client)
810    }
811
812    /// `GET {tariffs}` — every Tariff, paginated.
813    ///
814    /// # Errors
815    ///
816    /// As [`ModuleClient::list`].
817    pub fn list(
818        &self,
819        query: PageQuery,
820    ) -> Result<PageStream<'a, crate::v2_3_0::tariffs::Tariff>, OcpiError> {
821        self.0.list_bridged(query, ObjectKind::Tariff)
822    }
823}
824
825/// Pushes Tariffs to an eMSP.
826///
827/// The Tariffs Receiver interface is the one client-owned-object interface with a `DELETE`:
828/// a Tariff that no longer applies is removed, not marked.
829///
830/// Spec: 2.3.0 §mod_tariffs_emsp_interface
831#[derive(Clone, Debug)]
832pub struct TariffsReceiver<'a>(ModuleClient<'a>);
833
834impl<'a> TariffsReceiver<'a> {
835    /// Wraps a module client.
836    #[must_use]
837    pub const fn new(client: ModuleClient<'a>) -> Self {
838        Self(client)
839    }
840
841    /// `GET {tariffs}/{country_code}/{party_id}/{tariff_id}`.
842    ///
843    /// # Errors
844    ///
845    /// Propagates transport and OCPI-level errors.
846    pub async fn tariff(
847        &self,
848        owner: &PartyRef,
849        tariff_id: &str,
850    ) -> Result<crate::v2_3_0::tariffs::Tariff, OcpiError> {
851        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
852        self.0.get_bridged(endpoint.object(owner, tariff_id), ObjectKind::Tariff).await
853    }
854
855    /// `PUT {tariffs}/{country_code}/{party_id}/{tariff_id}`.
856    ///
857    /// # Errors
858    ///
859    /// Propagates validation, transport and OCPI-level errors.
860    pub async fn put_tariff(
861        &self,
862        owner: &PartyRef,
863        tariff: &crate::v2_3_0::tariffs::Tariff,
864    ) -> Result<(), OcpiError> {
865        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
866        self.0.put_bridged(endpoint.object(owner, tariff.id.as_str()), tariff, ObjectKind::Tariff).await
867    }
868
869    /// `DELETE {tariffs}/{country_code}/{party_id}/{tariff_id}`.
870    ///
871    /// # Errors
872    ///
873    /// Propagates transport and OCPI-level errors.
874    pub async fn delete_tariff(&self, owner: &PartyRef, tariff_id: &str) -> Result<(), OcpiError> {
875        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
876        self.0.delete(endpoint.object(owner, tariff_id)).await
877    }
878}
879
880/// Pushes Tokens to a CPO.
881///
882/// Spec: 2.3.0 §mod_tokens_cpo_interface
883#[derive(Clone, Debug)]
884pub struct TokensReceiver<'a>(ModuleClient<'a>);
885
886impl<'a> TokensReceiver<'a> {
887    /// Wraps a module client.
888    #[must_use]
889    pub const fn new(client: ModuleClient<'a>) -> Self {
890        Self(client)
891    }
892
893    /// `GET {tokens}/{country_code}/{party_id}/{token_uid}[?type=]`.
894    ///
895    /// # Errors
896    ///
897    /// Propagates transport and OCPI-level errors.
898    pub async fn token(
899        &self,
900        owner: &PartyRef,
901        token_uid: &str,
902        token_type: Option<crate::v2_3_0::tokens::TokenType>,
903    ) -> Result<crate::v2_3_0::tokens::Token, OcpiError> {
904        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
905        self.0
906            .get_bridged(
907                endpoint.token(owner, token_uid, token_type.as_ref().map(TokenType::as_str)),
908                ObjectKind::Token,
909            )
910            .await
911    }
912
913    /// `PUT {tokens}/{country_code}/{party_id}/{token_uid}[?type=]`.
914    ///
915    /// The `type` is taken from the Token itself, which is where the peer will look for it too.
916    ///
917    /// # Errors
918    ///
919    /// Propagates validation, transport and OCPI-level errors.
920    pub async fn put_token(
921        &self,
922        owner: &PartyRef,
923        token: &crate::v2_3_0::tokens::Token,
924    ) -> Result<(), OcpiError> {
925        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
926        let url = endpoint.token(owner, token.uid.as_str(), Some(token.token_type.as_str()));
927        self.0.put_bridged(url, token, ObjectKind::Token).await
928    }
929
930    /// `PATCH {tokens}/{country_code}/{party_id}/{token_uid}[?type=]`.
931    ///
932    /// # Errors
933    ///
934    /// Propagates transport and OCPI-level errors, and refuses a patch without `last_updated`.
935    pub async fn patch<T>(
936        &self,
937        owner: &PartyRef,
938        token_uid: &str,
939        token_type: Option<crate::v2_3_0::tokens::TokenType>,
940        patch: &Patch<T>,
941    ) -> Result<(), OcpiError> {
942        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
943        let url = endpoint.token(owner, token_uid, token_type.as_ref().map(TokenType::as_str));
944        self.0.patch_bridged(url, patch, ObjectKind::Token).await
945    }
946}
947
948/// Drives a CPO's Charging Profiles Receiver interface, as an eMSP or SCSP.
949///
950/// Every method here answers twice: the [`ChargingProfileResponse`] returned is the CPO's own
951/// immediate verdict, and — when that is `ACCEPTED` — the Charge Point's answer follows at the
952/// `response_url`, which this party must be serving. Build those URLs with
953/// [`CallbackUrls`](crate::server::CallbackUrls) if the server side is this crate's too.
954///
955/// [`ChargingProfileResponse`]: crate::v2_3_0::charging_profiles::ChargingProfileResponse
956///
957/// Spec: 2.3.0 §mod_charging_profiles_cpo_interface
958#[derive(Clone, Debug)]
959pub struct ChargingProfilesClient<'a>(ModuleClient<'a>);
960
961impl<'a> ChargingProfilesClient<'a> {
962    /// Wraps a module client.
963    #[must_use]
964    pub const fn new(client: ModuleClient<'a>) -> Self {
965        Self(client)
966    }
967
968    /// `GET {chargingprofiles}/{session_id}?duration=&response_url=`.
969    ///
970    /// # Errors
971    ///
972    /// Propagates transport and OCPI-level errors.
973    pub async fn active_charging_profile(
974        &self,
975        session_id: &str,
976        duration_seconds: u64,
977        response_url: &Url,
978    ) -> Result<crate::v2_3_0::charging_profiles::ChargingProfileResponse, OcpiError> {
979        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
980        self.0.get(endpoint.active_charging_profile(session_id, duration_seconds, response_url)).await
981    }
982
983    /// `PUT {chargingprofiles}/{session_id}` with a `SetChargingProfile` body.
984    ///
985    /// # Errors
986    ///
987    /// Propagates validation, transport and OCPI-level errors.
988    pub async fn set_charging_profile(
989        &self,
990        session_id: &str,
991        request: &crate::v2_3_0::charging_profiles::SetChargingProfile,
992    ) -> Result<crate::v2_3_0::charging_profiles::ChargingProfileResponse, OcpiError> {
993        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
994        check_outgoing(request, self.0.transport.config())?;
995        let outgoing =
996            self.0.request(Method::PUT, endpoint.charging_profile(session_id)).with_body(request)?;
997        self.0.transport.send(&outgoing, self.0.peer.token(), self.0.peer.quirks()).await
998    }
999
1000    /// `DELETE {chargingprofiles}/{session_id}?response_url=`.
1001    ///
1002    /// # Errors
1003    ///
1004    /// Propagates transport and OCPI-level errors.
1005    pub async fn clear_charging_profile(
1006        &self,
1007        session_id: &str,
1008        response_url: &Url,
1009    ) -> Result<crate::v2_3_0::charging_profiles::ChargingProfileResponse, OcpiError> {
1010        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
1011        let outgoing =
1012            self.0.request(Method::DELETE, endpoint.clear_charging_profile(session_id, response_url));
1013        self.0.transport.send(&outgoing, self.0.peer.token(), self.0.peer.quirks()).await
1014    }
1015
1016    /// `PUT {chargingprofiles}/{session_id}` on the **Sender** interface — a CPO volunteering a
1017    /// changed active profile to the party that set one.
1018    ///
1019    /// # Errors
1020    ///
1021    /// Propagates validation, transport and OCPI-level errors.
1022    pub async fn push_active_charging_profile(
1023        &self,
1024        session_id: &str,
1025        profile: &crate::v2_3_0::charging_profiles::ActiveChargingProfile,
1026    ) -> Result<(), OcpiError> {
1027        let endpoint = self.0.sender_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Sender))?;
1028        self.0.put(endpoint.object(session_id), profile).await
1029    }
1030}
1031
1032/// Reads and pushes `ClientInfo`, the hub's view of who is connected.
1033///
1034/// A configuration module: these requests carry no routing headers, which
1035/// [`OcpiRequest::routed`](crate::transport::OcpiRequest::routed) enforces on the way out.
1036///
1037/// Spec: 2.3.0 §mod_hub_client_info_module
1038#[derive(Clone, Debug)]
1039pub struct HubClientInfoClient<'a>(ModuleClient<'a>);
1040
1041impl<'a> HubClientInfoClient<'a> {
1042    /// Wraps a module client.
1043    #[must_use]
1044    pub const fn new(client: ModuleClient<'a>) -> Self {
1045        Self(client)
1046    }
1047
1048    /// `GET {hubclientinfo}` — every `ClientInfo` the hub publishes, paginated.
1049    ///
1050    /// # Errors
1051    ///
1052    /// As [`ModuleClient::list`].
1053    pub fn list(
1054        &self,
1055        query: PageQuery,
1056    ) -> Result<PageStream<'a, crate::v2_3_0::hub_client_info::ClientInfo>, OcpiError> {
1057        self.0.list_bridged(query, ObjectKind::ClientInfo)
1058    }
1059
1060    /// `GET {hubclientinfo}/{country_code}/{party_id}` — one party's status.
1061    ///
1062    /// # Errors
1063    ///
1064    /// Propagates transport and OCPI-level errors.
1065    pub async fn client_info(
1066        &self,
1067        party: &PartyRef,
1068    ) -> Result<crate::v2_3_0::hub_client_info::ClientInfo, OcpiError> {
1069        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
1070        let url = endpoint.base().join(party.country_code.as_str()).join(party.party_id.as_str());
1071        self.0.get_bridged(url, ObjectKind::ClientInfo).await
1072    }
1073
1074    /// `PUT {hubclientinfo}/{country_code}/{party_id}` — the hub telling a client about a party.
1075    ///
1076    /// # Errors
1077    ///
1078    /// Propagates validation, transport and OCPI-level errors.
1079    pub async fn put_client_info(
1080        &self,
1081        info: &crate::v2_3_0::hub_client_info::ClientInfo,
1082    ) -> Result<(), OcpiError> {
1083        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
1084        let url = endpoint.base().join(info.country_code.as_str()).join(info.party_id.as_str());
1085        self.0.put_bridged(url, info, ObjectKind::ClientInfo).await
1086    }
1087}
1088
1089/// The Payments module, from either side.
1090///
1091/// The PTP owns the objects, so the *Sender* methods are the ones a CPO calls to drive a
1092/// terminal, and the *Receiver* methods are the ones a PTP calls to seed the CPO's copy. See
1093/// [`SenderEndpoint::payments_terminals`](crate::transport::SenderEndpoint::payments_terminals)
1094/// for how the module's two endpoint URLs are resolved from one discovered `payments` endpoint.
1095///
1096/// Spec: 2.3.0 §mod_payments_payments_module
1097#[derive(Clone, Debug)]
1098pub struct PaymentsClient<'a>(ModuleClient<'a>);
1099
1100impl<'a> PaymentsClient<'a> {
1101    /// Wraps a module client.
1102    #[must_use]
1103    pub const fn new(client: ModuleClient<'a>) -> Self {
1104        Self(client)
1105    }
1106
1107    fn terminals(&self) -> Result<SenderEndpoint, OcpiError> {
1108        Ok(self
1109            .0
1110            .sender_endpoint()
1111            .ok_or_else(|| self.0.missing(InterfaceRole::Sender))?
1112            .payments_terminals())
1113    }
1114
1115    fn confirmations(&self) -> Result<SenderEndpoint, OcpiError> {
1116        Ok(self
1117            .0
1118            .sender_endpoint()
1119            .ok_or_else(|| self.0.missing(InterfaceRole::Sender))?
1120            .payments_financial_advice_confirmations())
1121    }
1122
1123    /// `GET {payments}/terminals` — every Terminal, paginated.
1124    ///
1125    /// # Errors
1126    ///
1127    /// Propagates transport and OCPI-level errors.
1128    pub fn list_terminals(
1129        &self,
1130        query: PageQuery,
1131    ) -> Result<PageStream<'a, crate::v2_3_0::payments::Terminal>, OcpiError> {
1132        let endpoint = self.terminals()?;
1133        Ok(PageStream::new(
1134            self.0.transport,
1135            self.0.peer,
1136            self.0.module.clone(),
1137            self.0.routing(),
1138            endpoint.list(&query),
1139        ))
1140    }
1141
1142    /// `GET {payments}/terminals/{terminal_id}`.
1143    ///
1144    /// # Errors
1145    ///
1146    /// Propagates transport and OCPI-level errors.
1147    pub async fn terminal(&self, terminal_id: &str) -> Result<crate::v2_3_0::payments::Terminal, OcpiError> {
1148        self.0.get(self.terminals()?.terminal(terminal_id)).await
1149    }
1150
1151    /// `PUT {payments}/terminals/{terminal_id}` — the CPO updating a terminal's location data.
1152    ///
1153    /// # Errors
1154    ///
1155    /// Propagates validation, transport and OCPI-level errors.
1156    pub async fn put_terminal(
1157        &self,
1158        terminal: &crate::v2_3_0::payments::Terminal,
1159    ) -> Result<crate::v2_3_0::payments::Terminal, OcpiError> {
1160        check_outgoing(terminal, self.0.transport.config())?;
1161        let url = self.terminals()?.terminal(terminal.terminal_id.as_str());
1162        let request = self.0.request(Method::PUT, url).with_body(terminal)?;
1163        self.0.transport.send(&request, self.0.peer.token(), self.0.peer.quirks()).await
1164    }
1165
1166    /// `PATCH {payments}/terminals/{terminal_id}` — assigning Locations or EVSEs.
1167    ///
1168    /// # Errors
1169    ///
1170    /// Propagates transport and OCPI-level errors.
1171    pub async fn patch_terminal<T>(
1172        &self,
1173        terminal_id: &str,
1174        patch: &Patch<T>,
1175    ) -> Result<crate::v2_3_0::payments::Terminal, OcpiError> {
1176        let url = self.terminals()?.terminal(terminal_id);
1177        let request = self.0.request(Method::PATCH, url).with_body(patch.as_value())?;
1178        self.0.transport.send(&request, self.0.peer.token(), self.0.peer.quirks()).await
1179    }
1180
1181    /// `POST {payments}/terminals/activate`.
1182    ///
1183    /// The body is a [`Patch`] rather than a `Terminal`, because
1184    /// *"the terminal_id is optional in the activation request as it will be set by the PTP"* —
1185    /// which is not a `Terminal`.
1186    ///
1187    /// # Errors
1188    ///
1189    /// Propagates transport and OCPI-level errors.
1190    pub async fn activate_terminal<T>(
1191        &self,
1192        terminal: &Patch<T>,
1193    ) -> Result<crate::v2_3_0::payments::Terminal, OcpiError> {
1194        let request = self
1195            .0
1196            .request(Method::POST, self.terminals()?.terminal_activate())
1197            .with_body(terminal.as_value())?;
1198        self.0.transport.send(&request, self.0.peer.token(), self.0.peer.quirks()).await
1199    }
1200
1201    /// `POST {payments}/terminals/{terminal_id}/deactivate`.
1202    ///
1203    /// # Errors
1204    ///
1205    /// Propagates transport and OCPI-level errors.
1206    pub async fn deactivate_terminal(
1207        &self,
1208        terminal_id: &str,
1209    ) -> Result<crate::v2_3_0::payments::Terminal, OcpiError> {
1210        let url = self.terminals()?.terminal_deactivate(terminal_id);
1211        let request = self.0.request(Method::POST, url).with_body(&serde_json::json!({}))?;
1212        self.0.transport.send(&request, self.0.peer.token(), self.0.peer.quirks()).await
1213    }
1214
1215    /// `GET {payments}/financial-advice-confirmations` — paginated.
1216    ///
1217    /// # Errors
1218    ///
1219    /// Propagates transport and OCPI-level errors.
1220    pub fn list_financial_advice_confirmations(
1221        &self,
1222        query: PageQuery,
1223    ) -> Result<PageStream<'a, crate::v2_3_0::payments::FinancialAdviceConfirmation>, OcpiError> {
1224        let endpoint = self.confirmations()?;
1225        Ok(PageStream::new(
1226            self.0.transport,
1227            self.0.peer,
1228            self.0.module.clone(),
1229            self.0.routing(),
1230            endpoint.list(&query),
1231        ))
1232    }
1233
1234    /// `GET {payments}/financial-advice-confirmations/{id}`.
1235    ///
1236    /// # Errors
1237    ///
1238    /// Propagates transport and OCPI-level errors.
1239    pub async fn financial_advice_confirmation(
1240        &self,
1241        id: &str,
1242    ) -> Result<crate::v2_3_0::payments::FinancialAdviceConfirmation, OcpiError> {
1243        self.0.get(self.confirmations()?.object(id)).await
1244    }
1245
1246    /// `POST {payments}/terminals` on the CPO's **Receiver** interface — the PTP creating a
1247    /// terminal in the CPO's system.
1248    ///
1249    /// # Errors
1250    ///
1251    /// Propagates validation, transport and OCPI-level errors.
1252    pub async fn post_terminal_to_receiver(
1253        &self,
1254        terminal: &crate::v2_3_0::payments::Terminal,
1255    ) -> Result<crate::v2_3_0::payments::Terminal, OcpiError> {
1256        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
1257        check_outgoing(terminal, self.0.transport.config())?;
1258        let url = endpoint.payments_terminals().base().clone();
1259        let request = self.0.request(Method::POST, url).with_body(terminal)?;
1260        self.0.transport.send(&request, self.0.peer.token(), self.0.peer.quirks()).await
1261    }
1262
1263    /// `POST {payments}/financial-advice-confirmations` on the CPO's **Receiver** interface.
1264    ///
1265    /// # Errors
1266    ///
1267    /// Propagates validation, transport and OCPI-level errors.
1268    pub async fn post_financial_advice_confirmation(
1269        &self,
1270        confirmation: &crate::v2_3_0::payments::FinancialAdviceConfirmation,
1271    ) -> Result<crate::v2_3_0::payments::FinancialAdviceConfirmation, OcpiError> {
1272        let endpoint = self.0.receiver_endpoint().ok_or_else(|| self.0.missing(InterfaceRole::Receiver))?;
1273        check_outgoing(confirmation, self.0.transport.config())?;
1274        let url = endpoint.payments_financial_advice_confirmations().base().clone();
1275        let request = self.0.request(Method::POST, url).with_body(confirmation)?;
1276        self.0.transport.send(&request, self.0.peer.token(), self.0.peer.quirks()).await
1277    }
1278}
1279
1280/// Convenience constructors hanging off a [`Peer`].
1281impl Peer {
1282    /// A client for an arbitrary module.
1283    #[must_use]
1284    pub fn module<'a>(
1285        &'a self,
1286        transport: &'a Transport,
1287        module: ModuleId,
1288        from: PartyRef,
1289    ) -> ModuleClient<'a> {
1290        ModuleClient::new(transport, self, module, from)
1291    }
1292
1293    /// The Locations Sender client: pull Locations from this peer.
1294    #[must_use]
1295    pub fn locations<'a>(&'a self, transport: &'a Transport, from: PartyRef) -> LocationsSender<'a> {
1296        LocationsSender::new(self.module(transport, ModuleId::Locations, from))
1297    }
1298
1299    /// The Locations Receiver client: push Locations to this peer.
1300    #[must_use]
1301    pub fn locations_receiver<'a>(
1302        &'a self,
1303        transport: &'a Transport,
1304        from: PartyRef,
1305    ) -> LocationsReceiver<'a> {
1306        LocationsReceiver::new(self.module(transport, ModuleId::Locations, from))
1307    }
1308
1309    /// The Tokens Sender client: pull Tokens from, and authorize against, this peer.
1310    #[must_use]
1311    pub fn tokens<'a>(&'a self, transport: &'a Transport, from: PartyRef) -> TokensSender<'a> {
1312        TokensSender::new(self.module(transport, ModuleId::Tokens, from))
1313    }
1314
1315    /// The CDRs client.
1316    #[must_use]
1317    pub fn cdrs<'a>(&'a self, transport: &'a Transport, from: PartyRef) -> CdrsClient<'a> {
1318        CdrsClient::new(self.module(transport, ModuleId::Cdrs, from))
1319    }
1320
1321    /// The Tokens Receiver client: push Tokens to this peer.
1322    #[must_use]
1323    pub fn tokens_receiver<'a>(&'a self, transport: &'a Transport, from: PartyRef) -> TokensReceiver<'a> {
1324        TokensReceiver::new(self.module(transport, ModuleId::Tokens, from))
1325    }
1326
1327    /// The Sessions Sender client: pull Sessions from this peer.
1328    #[must_use]
1329    pub fn sessions<'a>(&'a self, transport: &'a Transport, from: PartyRef) -> SessionsSender<'a> {
1330        SessionsSender::new(self.module(transport, ModuleId::Sessions, from))
1331    }
1332
1333    /// The Sessions Receiver client: push Sessions to this peer.
1334    #[must_use]
1335    pub fn sessions_receiver<'a>(&'a self, transport: &'a Transport, from: PartyRef) -> SessionsReceiver<'a> {
1336        SessionsReceiver::new(self.module(transport, ModuleId::Sessions, from))
1337    }
1338
1339    /// The Tariffs Sender client: pull Tariffs from this peer.
1340    #[must_use]
1341    pub fn tariffs<'a>(&'a self, transport: &'a Transport, from: PartyRef) -> TariffsSender<'a> {
1342        TariffsSender::new(self.module(transport, ModuleId::Tariffs, from))
1343    }
1344
1345    /// The Tariffs Receiver client: push Tariffs to this peer.
1346    #[must_use]
1347    pub fn tariffs_receiver<'a>(&'a self, transport: &'a Transport, from: PartyRef) -> TariffsReceiver<'a> {
1348        TariffsReceiver::new(self.module(transport, ModuleId::Tariffs, from))
1349    }
1350
1351    /// The Commands client.
1352    #[must_use]
1353    pub fn commands<'a>(&'a self, transport: &'a Transport, from: PartyRef) -> CommandsClient<'a> {
1354        CommandsClient::new(self.module(transport, ModuleId::Commands, from))
1355    }
1356
1357    /// The Charging Profiles client.
1358    #[must_use]
1359    pub fn charging_profiles<'a>(
1360        &'a self,
1361        transport: &'a Transport,
1362        from: PartyRef,
1363    ) -> ChargingProfilesClient<'a> {
1364        ChargingProfilesClient::new(self.module(transport, ModuleId::ChargingProfiles, from))
1365    }
1366
1367    /// The Hub Client Info client.
1368    #[must_use]
1369    pub fn hub_client_info<'a>(
1370        &'a self,
1371        transport: &'a Transport,
1372        from: PartyRef,
1373    ) -> HubClientInfoClient<'a> {
1374        HubClientInfoClient::new(self.module(transport, ModuleId::HubClientInfo, from))
1375    }
1376
1377    /// The Payments client.
1378    #[must_use]
1379    pub fn payments<'a>(&'a self, transport: &'a Transport, from: PartyRef) -> PaymentsClient<'a> {
1380        PaymentsClient::new(self.module(transport, ModuleId::Payments, from))
1381    }
1382}
1383
1384/// Turns a translation failure into the OCPI error a caller can act on.
1385fn bridge_error(error: BridgeError, kind: ObjectKind) -> OcpiError {
1386    match error {
1387        BridgeError::Unsupported { from, to } => OcpiError::Unsupported(format!(
1388            "this build has no conversions between OCPI {from} and OCPI {to}, so a {kind} cannot \
1389             be carried between them"
1390        )),
1391        BridgeError::Decode { version, message, .. } => OcpiError::Decode {
1392            path: "/".to_owned(),
1393            message: format!("the peer's OCPI {version} {kind} could not be read: {message}"),
1394        },
1395    }
1396}
1397
1398/// Decodes a translated document into the canonical type it is now written as.
1399fn decode<T: DeserializeOwned>(value: serde_json::Value) -> Result<T, OcpiError> {
1400    serde_path_to_error::deserialize(value)
1401        .map_err(|e| OcpiError::Decode { path: e.path().to_string(), message: e.into_inner().to_string() })
1402}
1403
1404/// Convenience: `RequestIds` for a caller that wants to correlate several requests.
1405#[must_use]
1406pub fn correlated_ids() -> RequestIds {
1407    RequestIds::generate()
1408}