Skip to main content

ocpi_kit/server/
traits.rs

1//! One trait per module and interface. Implement the ones your role serves; the router mounts
2//! exactly those and generates version details that say so.
3//!
4//! Every method takes a [`RequestContext`], which carries the authenticated platform, the request
5//! IDs and the routing headers. Persistence is deliberately absent: OCPI says nothing about how a
6//! party stores its objects, and neither does this crate.
7//!
8//! # Why the traits use `impl Future` rather than `async fn`
9//!
10//! A trait method declared `async fn` produces a future with no `Send` bound, which an axum
11//! handler cannot spawn. Declaring `-> impl Future<Output = …> + Send` fixes the bound while
12//! still letting an implementation write a plain `async fn`.
13
14use core::future::Future;
15
16use crate::transport::{OcpiError, Page, PageQuery, Patch};
17use crate::types::PartyRef;
18
19use super::extract::RequestContext;
20
21/// The result every handler method returns.
22pub type Handled<T> = Result<T, OcpiError>;
23
24// ---------------------------------------------------------------------------------------------
25// Locations
26// ---------------------------------------------------------------------------------------------
27
28/// The CPO side of the Locations module: serving the Locations this party owns.
29///
30/// Spec: 2.3.0 §mod_locations_cpo_interface
31pub trait LocationsSender: Send + Sync + 'static {
32    /// `GET {locations}` — one page of Locations.
33    ///
34    /// The returned [`Page`] carries the `Link`, `X-Total-Count` and `X-Limit` values; build it
35    /// with [`Page::single`] when everything fits in one response.
36    fn list(
37        &self,
38        query: PageQuery,
39        context: RequestContext,
40    ) -> impl Future<Output = Handled<Page<crate::v2_3_0::locations::Location>>> + Send;
41
42    /// `GET {locations}/{location_id}`.
43    fn location(
44        &self,
45        location_id: String,
46        context: RequestContext,
47    ) -> impl Future<Output = Handled<crate::v2_3_0::locations::Location>> + Send;
48
49    /// `GET {locations}/{location_id}/{evse_uid}`.
50    fn evse(
51        &self,
52        location_id: String,
53        evse_uid: String,
54        context: RequestContext,
55    ) -> impl Future<Output = Handled<crate::v2_3_0::locations::Evse>> + Send;
56
57    /// `GET {locations}/{location_id}/{evse_uid}/{connector_id}`.
58    fn connector(
59        &self,
60        location_id: String,
61        evse_uid: String,
62        connector_id: String,
63        context: RequestContext,
64    ) -> impl Future<Output = Handled<crate::v2_3_0::locations::Connector>> + Send;
65}
66
67/// The eMSP side of the Locations module: receiving the Locations another party owns.
68///
69/// These are client-owned objects, so every method carries the `owner` from the URL. The router
70/// has already checked that the authenticated platform speaks for that party.
71///
72/// > *An EVSE is never deleted; a removed EVSE gets `status` `REMOVED`* — so there is no delete
73/// > method here, by design.
74///
75/// Spec: 2.3.0 §mod_locations_emsp_interface
76pub trait LocationsReceiver: Send + Sync + 'static {
77    /// `GET {locations}/{cc}/{party}/{location_id}` — what this party has stored.
78    fn location(
79        &self,
80        owner: PartyRef,
81        location_id: String,
82        context: RequestContext,
83    ) -> impl Future<Output = Handled<crate::v2_3_0::locations::Location>> + Send;
84
85    /// `PUT {locations}/{cc}/{party}/{location_id}`.
86    ///
87    /// Returns whether the object was newly created, which decides between HTTP 201 and 200:
88    ///
89    /// > *HTTP `200 - Ok` when the object already existed and has successfully been updated.
90    /// > HTTP `201 - Created` when the object has been newly created in the server system.*
91    fn put_location(
92        &self,
93        owner: PartyRef,
94        location: crate::v2_3_0::locations::Location,
95        context: RequestContext,
96    ) -> impl Future<Output = Handled<Created>> + Send;
97
98    /// `PUT {locations}/{cc}/{party}/{location_id}/{evse_uid}`.
99    fn put_evse(
100        &self,
101        owner: PartyRef,
102        location_id: String,
103        evse: crate::v2_3_0::locations::Evse,
104        context: RequestContext,
105    ) -> impl Future<Output = Handled<Created>> + Send;
106
107    /// `PUT {locations}/{cc}/{party}/{location_id}/{evse_uid}/{connector_id}`.
108    fn put_connector(
109        &self,
110        owner: PartyRef,
111        location_id: String,
112        evse_uid: String,
113        connector: crate::v2_3_0::locations::Connector,
114        context: RequestContext,
115    ) -> impl Future<Output = Handled<Created>> + Send;
116
117    /// `PATCH` on any of the three levels.
118    ///
119    /// The patch is guaranteed to carry `last_updated`; the extractor refuses one that does not.
120    fn patch(
121        &self,
122        owner: PartyRef,
123        location_id: String,
124        evse_uid: Option<String>,
125        connector_id: Option<String>,
126        patch: Patch<serde_json::Value>,
127        context: RequestContext,
128    ) -> impl Future<Output = Handled<()>> + Send;
129}
130
131/// Whether a `PUT` created the object or replaced one.
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
133pub enum Created {
134    /// The object did not exist; answer with HTTP 201.
135    Yes,
136    /// The object existed and was replaced; answer with HTTP 200.
137    No,
138}
139
140impl Created {
141    /// The HTTP status this outcome maps to.
142    #[must_use]
143    pub const fn http_status(self) -> u16 {
144        match self {
145            Self::Yes => 201,
146            Self::No => 200,
147        }
148    }
149}
150
151impl From<bool> for Created {
152    /// `true` means the object was newly created.
153    fn from(created: bool) -> Self {
154        if created { Self::Yes } else { Self::No }
155    }
156}
157
158// ---------------------------------------------------------------------------------------------
159// Tokens
160// ---------------------------------------------------------------------------------------------
161
162/// The eMSP side of the Tokens module: serving Tokens and answering real-time authorizations.
163///
164/// Spec: 2.3.0 §mod_tokens_emsp_interface
165pub trait TokensSender: Send + Sync + 'static {
166    /// `GET {tokens}` — one page of Tokens.
167    fn list(
168        &self,
169        query: PageQuery,
170        context: RequestContext,
171    ) -> impl Future<Output = Handled<Page<crate::v2_3_0::tokens::Token>>> + Send;
172
173    /// `POST {tokens}/{token_uid}/authorize[?type=]` — a real-time authorization.
174    ///
175    /// > *`2004 Unknown Token`* is the code for a token this party does not know; return
176    /// > [`OcpiError::Remote`] with that code, or an
177    /// > [`AuthorizationInfo`](crate::v2_3_0::tokens::AuthorizationInfo) with
178    /// > [`AllowedType::NotAllowed`](crate::v2_3_0::tokens::AllowedType::NotAllowed) when the
179    /// > token is known but may not charge here.
180    fn authorize(
181        &self,
182        token_uid: String,
183        token_type: Option<crate::v2_3_0::tokens::TokenType>,
184        location: Option<crate::v2_3_0::tokens::LocationReferences>,
185        context: RequestContext,
186    ) -> impl Future<Output = Handled<crate::v2_3_0::tokens::AuthorizationInfo>> + Send;
187}
188
189/// The CPO side of the Tokens module: receiving the Tokens an eMSP owns.
190///
191/// Spec: 2.3.0 §mod_tokens_cpo_interface
192pub trait TokensReceiver: Send + Sync + 'static {
193    /// `GET {tokens}/{cc}/{party}/{token_uid}[?type=]`.
194    fn token(
195        &self,
196        owner: PartyRef,
197        token_uid: String,
198        token_type: Option<crate::v2_3_0::tokens::TokenType>,
199        context: RequestContext,
200    ) -> impl Future<Output = Handled<crate::v2_3_0::tokens::Token>> + Send;
201
202    /// `PUT {tokens}/{cc}/{party}/{token_uid}[?type=]`.
203    fn put_token(
204        &self,
205        owner: PartyRef,
206        token: crate::v2_3_0::tokens::Token,
207        context: RequestContext,
208    ) -> impl Future<Output = Handled<Created>> + Send;
209
210    /// `PATCH {tokens}/{cc}/{party}/{token_uid}[?type=]`.
211    fn patch_token(
212        &self,
213        owner: PartyRef,
214        token_uid: String,
215        token_type: Option<crate::v2_3_0::tokens::TokenType>,
216        patch: Patch<crate::v2_3_0::tokens::Token>,
217        context: RequestContext,
218    ) -> impl Future<Output = Handled<()>> + Send;
219}
220
221// ---------------------------------------------------------------------------------------------
222// CDRs and Sessions
223// ---------------------------------------------------------------------------------------------
224
225/// The CPO side of the CDRs module.
226///
227/// Spec: 2.3.0 §mod_cdrs_cpo_interface
228pub trait CdrsSender: Send + Sync + 'static {
229    /// `GET {cdrs}` — one page of CDRs.
230    fn list(
231        &self,
232        query: PageQuery,
233        context: RequestContext,
234    ) -> impl Future<Output = Handled<Page<crate::v2_3_0::cdrs::Cdr>>> + Send;
235}
236
237/// The eMSP side of the CDRs module.
238///
239/// The only OCPI module where `POST` creates a server-owned object:
240///
241/// > *The eMSP returns the URL to the just created CDR object in the `Location` header field.*
242///
243/// Spec: 2.3.0 §mod_cdrs_emsp_interface
244pub trait CdrsReceiver: Send + Sync + 'static {
245    /// `GET {cdrs}/{cdr_id}` — a CDR previously POSTed here.
246    fn cdr(
247        &self,
248        cdr_id: String,
249        context: RequestContext,
250    ) -> impl Future<Output = Handled<crate::v2_3_0::cdrs::Cdr>> + Send;
251
252    /// `POST {cdrs}` — stores a CDR and returns the URL it can be fetched from.
253    fn post_cdr(
254        &self,
255        cdr: crate::v2_3_0::cdrs::Cdr,
256        context: RequestContext,
257    ) -> impl Future<Output = Handled<crate::types::Url>> + Send;
258}
259
260/// The CPO side of the Sessions module.
261///
262/// Spec: 2.3.0 §mod_sessions_cpo_interface
263pub trait SessionsSender: Send + Sync + 'static {
264    /// `GET {sessions}` — one page of Sessions.
265    fn list(
266        &self,
267        query: PageQuery,
268        context: RequestContext,
269    ) -> impl Future<Output = Handled<Page<crate::v2_3_0::sessions::Session>>> + Send;
270
271    /// `PUT {sessions}/{session_id}/charging_preferences`.
272    ///
273    /// > *If a PUT with ChargingPreferences is received for an EVSE that does not have the
274    /// > capability `CHARGING_PREFERENCES_CAPABLE`, the receiver should respond with an HTTP
275    /// > status of 404 and an OCPI status code of 2001.*
276    fn set_charging_preferences(
277        &self,
278        session_id: String,
279        preferences: crate::v2_3_0::sessions::ChargingPreferences,
280        context: RequestContext,
281    ) -> impl Future<Output = Handled<crate::v2_3_0::sessions::ChargingPreferencesResponse>> + Send;
282}
283
284/// The eMSP side of the Sessions module.
285///
286/// Spec: 2.3.0 §mod_sessions_emsp_interface
287pub trait SessionsReceiver: Send + Sync + 'static {
288    /// `GET {sessions}/{cc}/{party}/{session_id}`.
289    fn session(
290        &self,
291        owner: PartyRef,
292        session_id: String,
293        context: RequestContext,
294    ) -> impl Future<Output = Handled<crate::v2_3_0::sessions::Session>> + Send;
295
296    /// `PUT {sessions}/{cc}/{party}/{session_id}`.
297    fn put_session(
298        &self,
299        owner: PartyRef,
300        session: crate::v2_3_0::sessions::Session,
301        context: RequestContext,
302    ) -> impl Future<Output = Handled<Created>> + Send;
303
304    /// `PATCH {sessions}/{cc}/{party}/{session_id}`.
305    fn patch_session(
306        &self,
307        owner: PartyRef,
308        session_id: String,
309        patch: Patch<crate::v2_3_0::sessions::Session>,
310        context: RequestContext,
311    ) -> impl Future<Output = Handled<()>> + Send;
312}
313
314// ---------------------------------------------------------------------------------------------
315// Tariffs
316// ---------------------------------------------------------------------------------------------
317
318/// The CPO side of the Tariffs module.
319///
320/// Spec: 2.3.0 §mod_tariffs_cpo_interface
321pub trait TariffsSender: Send + Sync + 'static {
322    /// `GET {tariffs}` — one page of Tariffs.
323    fn list(
324        &self,
325        query: PageQuery,
326        context: RequestContext,
327    ) -> impl Future<Output = Handled<Page<crate::v2_3_0::tariffs::Tariff>>> + Send;
328}
329
330/// The eMSP side of the Tariffs module.
331///
332/// The only client-owned module with a `DELETE`: a tariff that no longer exists is simply gone,
333/// unlike an EVSE, which is retired with a status.
334///
335/// Spec: 2.3.0 §mod_tariffs_emsp_interface
336pub trait TariffsReceiver: Send + Sync + 'static {
337    /// `GET {tariffs}/{cc}/{party}/{tariff_id}`.
338    fn tariff(
339        &self,
340        owner: PartyRef,
341        tariff_id: String,
342        context: RequestContext,
343    ) -> impl Future<Output = Handled<crate::v2_3_0::tariffs::Tariff>> + Send;
344
345    /// `PUT {tariffs}/{cc}/{party}/{tariff_id}`.
346    fn put_tariff(
347        &self,
348        owner: PartyRef,
349        tariff: crate::v2_3_0::tariffs::Tariff,
350        context: RequestContext,
351    ) -> impl Future<Output = Handled<Created>> + Send;
352
353    /// `DELETE {tariffs}/{cc}/{party}/{tariff_id}`.
354    fn delete_tariff(
355        &self,
356        owner: PartyRef,
357        tariff_id: String,
358        context: RequestContext,
359    ) -> impl Future<Output = Handled<()>> + Send;
360}
361
362// ---------------------------------------------------------------------------------------------
363// Commands
364// ---------------------------------------------------------------------------------------------
365
366/// The CPO side of the Commands module: receiving commands from an eMSP.
367///
368/// A command is answered twice: immediately with a
369/// [`CommandResponse`](crate::v2_3_0::commands::CommandResponse) carrying a timeout, and later by
370/// POSTing a [`CommandResult`](crate::v2_3_0::commands::CommandResult) to the command's
371/// `response_url`. This trait covers the first; the second is an outgoing request the
372/// implementation makes when the Charge Point answers.
373///
374/// **Check the `response_url` before calling it.** It comes from the peer, and a party that
375/// fetches it unconditionally is an SSRF proxy; the [`client`](crate::client) does this check for
376/// you.
377///
378/// Spec: 2.3.0 §mod_commands_commands_module
379pub trait CommandsReceiver: Send + Sync + 'static {
380    /// `POST {commands}/{command}`.
381    fn command(
382        &self,
383        command: crate::v2_3_0::commands::Command,
384        context: RequestContext,
385    ) -> impl Future<Output = Handled<crate::v2_3_0::commands::CommandResponse>> + Send;
386}
387
388/// The eMSP side of the Commands module: receiving the asynchronous result.
389///
390/// Spec: 2.3.0 §mod_commands_commands_module
391pub trait CommandsSender: Send + Sync + 'static {
392    /// `POST {response_url}` — the Charge Point's eventual answer.
393    ///
394    /// The `unique_id` is whatever the implementation put in the `response_url` when it sent the
395    /// command, which is how the result is matched to the request:
396    ///
397    /// > *This URL might contain a unique ID to be able to distinguish between StartSession
398    /// > requests.*
399    fn command_result(
400        &self,
401        unique_id: String,
402        result: crate::v2_3_0::commands::CommandResult,
403        context: RequestContext,
404    ) -> impl Future<Output = Handled<()>> + Send;
405}
406
407// ---------------------------------------------------------------------------------------------
408// Credentials
409// ---------------------------------------------------------------------------------------------
410
411/// The credentials module, which every implementation must serve.
412///
413/// The four methods are the registration lifecycle. The two 405 rules are the ones most often
414/// missed, and the router does not enforce them for you because only the implementation knows
415/// whether a peer is registered:
416///
417/// > *POST … MUST return a HTTP status code 405: method not allowed if the client has already
418/// > been registered before.*
419/// >
420/// > *PUT … MUST return a HTTP status code 405: method not allowed if the client has not been
421/// > registered yet.*
422///
423/// Return [`OcpiError::MethodNotAllowed`] for those;
424/// [`PeerState`](crate::client::PeerState) has the predicates.
425///
426/// Spec: 2.3.0 §credentials_credentials_endpoint
427pub trait CredentialsHandler: Send + Sync + 'static {
428    /// `GET {credentials}` — this party's own credentials object.
429    fn get(
430        &self,
431        context: RequestContext,
432    ) -> impl Future<Output = Handled<crate::v2_3_0::credentials::Credentials>> + Send;
433
434    /// `POST {credentials}` — register.
435    ///
436    /// The implementation must fetch the client's versions and version details with the token in
437    /// `credentials` **before** answering, and answer `3001` if that fails:
438    ///
439    /// > *When the initializing party requests data from the other party during the open POST call
440    /// > to its credentials endpoint. If one of the GETs can not be processed, the party should
441    /// > return this error in the POST response.*
442    fn post(
443        &self,
444        credentials: crate::v2_3_0::credentials::Credentials,
445        context: RequestContext,
446    ) -> impl Future<Output = Handled<crate::v2_3_0::credentials::Credentials>> + Send;
447
448    /// `PUT {credentials}` — update, rotate the token, or switch version.
449    ///
450    /// > *The server must fetch the client's endpoints again, even if the version has not
451    /// > changed.*
452    fn put(
453        &self,
454        credentials: crate::v2_3_0::credentials::Credentials,
455        context: RequestContext,
456    ) -> impl Future<Output = Handled<crate::v2_3_0::credentials::Credentials>> + Send;
457
458    /// `DELETE {credentials}` — unregister.
459    ///
460    /// > *Both parties must end any automated communication.*
461    fn delete(&self, context: RequestContext) -> impl Future<Output = Handled<()>> + Send;
462}
463
464// ---------------------------------------------------------------------------------------------
465// Hub Client Info
466// ---------------------------------------------------------------------------------------------
467
468/// The hub side of the Hub Client Info module.
469///
470/// A configuration module: its requests carry no routing headers.
471///
472/// Spec: 2.3.0 §mod_hub_client_info_module
473pub trait HubClientInfoSender: Send + Sync + 'static {
474    /// `GET {hubclientinfo}` — one page of `ClientInfo` objects.
475    fn list(
476        &self,
477        query: PageQuery,
478        context: RequestContext,
479    ) -> impl Future<Output = Handled<Page<crate::v2_3_0::hub_client_info::ClientInfo>>> + Send;
480}
481
482/// The client side of the Hub Client Info module.
483///
484/// Spec: 2.3.0 §mod_hub_client_info_module
485pub trait HubClientInfoReceiver: Send + Sync + 'static {
486    /// `GET {hubclientinfo}/{cc}/{party}`.
487    fn client_info(
488        &self,
489        party: PartyRef,
490        context: RequestContext,
491    ) -> impl Future<Output = Handled<crate::v2_3_0::hub_client_info::ClientInfo>> + Send;
492
493    /// `PUT {hubclientinfo}/{cc}/{party}`.
494    fn put_client_info(
495        &self,
496        party: PartyRef,
497        info: crate::v2_3_0::hub_client_info::ClientInfo,
498        context: RequestContext,
499    ) -> impl Future<Output = Handled<Created>> + Send;
500}
501
502// ---------------------------------------------------------------------------------------------
503// Charging Profiles
504// ---------------------------------------------------------------------------------------------
505
506/// The CPO side of the Charging Profiles module: accepting profiles for a running session.
507///
508/// Every method here answers **twice**. The `ChargingProfileResponse` returned from the method is
509/// the CPO's own immediate answer — did it understand the request and manage to pass it to the
510/// EVSE — and, when that answer is `ACCEPTED`, the Charge Point's eventual verdict follows as a
511/// POST to the `response_url` the Sender supplied.
512///
513/// > *The response contains the direct response from the Receiver (Typically CPO), not the
514/// > response from the EVSE itself, that will be sent via an asynchronous POST on the Sender
515/// > interface if this response is `ACCEPTED`.*
516///
517/// **Check the `response_url` before calling it.** It comes from the peer; see
518/// [`UrlPolicy`](crate::types::UrlPolicy).
519///
520/// Spec: 2.3.0 §mod_charging_profiles_cpo_interface
521pub trait ChargingProfilesReceiver: Send + Sync + 'static {
522    /// `GET {chargingprofiles}/{session_id}?duration={duration}&response_url={url}`.
523    ///
524    /// The active profile itself is not returned here — it arrives at `response_url` as an
525    /// [`ActiveChargingProfileResult`](crate::v2_3_0::charging_profiles::ActiveChargingProfileResult).
526    fn active_charging_profile(
527        &self,
528        session_id: String,
529        duration_seconds: u64,
530        response_url: crate::types::Url,
531        context: RequestContext,
532    ) -> impl Future<Output = Handled<crate::v2_3_0::charging_profiles::ChargingProfileResponse>> + Send;
533
534    /// `PUT {chargingprofiles}/{session_id}` with a `SetChargingProfile` body.
535    fn set_charging_profile(
536        &self,
537        session_id: String,
538        request: crate::v2_3_0::charging_profiles::SetChargingProfile,
539        context: RequestContext,
540    ) -> impl Future<Output = Handled<crate::v2_3_0::charging_profiles::ChargingProfileResponse>> + Send;
541
542    /// `DELETE {chargingprofiles}/{session_id}?response_url={url}`.
543    fn clear_charging_profile(
544        &self,
545        session_id: String,
546        response_url: crate::types::Url,
547        context: RequestContext,
548    ) -> impl Future<Output = Handled<crate::v2_3_0::charging_profiles::ChargingProfileResponse>> + Send;
549}
550
551/// The eMSP/SCSP side of the Charging Profiles module: receiving what the Charge Point decided.
552///
553/// # Why the callback paths are three, not one
554///
555/// The specification leaves the `response_url` entirely to the Sender —
556///
557/// > *No structure defined. This is open to the eMSP to define, the URL is provided to the
558/// > Receiver by the Sender.*
559///
560/// — and that freedom is load-bearing here, because the three result bodies are **not**
561/// distinguishable from one another:
562/// [`ChargingProfileResult`](crate::v2_3_0::charging_profiles::ChargingProfileResult) and
563/// [`ClearProfileResult`](crate::v2_3_0::charging_profiles::ClearProfileResult) have identical
564/// shapes, one `result` field each. A single endpoint that sniffed the body could not tell a
565/// rejected PUT from a rejected DELETE.
566///
567/// So [`OcpiRouter::charging_profiles_sender`](crate::server::OcpiRouter::charging_profiles_sender)
568/// mounts one path per result kind, each ending in the Sender's own unique id, and
569/// [`CallbackUrls`](crate::server::CallbackUrls) builds the matching `response_url`s. The kind is
570/// then carried by the URL, exactly as the specification intends.
571///
572/// Spec: 2.3.0 §mod_charging_profiles_emsp_interface
573pub trait ChargingProfilesSender: Send + Sync + 'static {
574    /// The Charge Point's answer to a GET of the active profile.
575    fn active_charging_profile_result(
576        &self,
577        unique_id: String,
578        result: crate::v2_3_0::charging_profiles::ActiveChargingProfileResult,
579        context: RequestContext,
580    ) -> impl Future<Output = Handled<()>> + Send;
581
582    /// The Charge Point's answer to a PUT of a charging profile.
583    fn charging_profile_result(
584        &self,
585        unique_id: String,
586        result: crate::v2_3_0::charging_profiles::ChargingProfileResult,
587        context: RequestContext,
588    ) -> impl Future<Output = Handled<()>> + Send;
589
590    /// The Charge Point's answer to a DELETE of a charging profile.
591    fn clear_profile_result(
592        &self,
593        unique_id: String,
594        result: crate::v2_3_0::charging_profiles::ClearProfileResult,
595        context: RequestContext,
596    ) -> impl Future<Output = Handled<()>> + Send;
597
598    /// `PUT {chargingprofiles}/{session_id}` — the CPO volunteering a changed active profile.
599    ///
600    /// > *The Receiver SHALL call this interface every time it knows changes have been made that
601    /// > influence the ActiveChargingProfile for an ongoing session AND the Sender has at least
602    /// > once successfully called the charging profile Receiver PUT interface for this session.*
603    fn put_active_charging_profile(
604        &self,
605        session_id: String,
606        profile: crate::v2_3_0::charging_profiles::ActiveChargingProfile,
607        context: RequestContext,
608    ) -> impl Future<Output = Handled<()>> + Send;
609}
610
611// ---------------------------------------------------------------------------------------------
612// Payments
613// ---------------------------------------------------------------------------------------------
614
615/// The PTP side of the Payments module: the terminals this Payment Terminal Provider owns.
616///
617/// Note the direction. In Payments the **PTP** is the Sender — it owns the `Terminal` objects —
618/// and the CPO drives them, which is why activation and location assignment are writes *on this
619/// interface* rather than pushes to the CPO.
620///
621/// Spec: 2.3.0 §mod_payments_ptp_interface
622pub trait PaymentsSender: Send + Sync + 'static {
623    /// `GET {payments}/terminals` — one page of Terminals.
624    fn terminals(
625        &self,
626        query: PageQuery,
627        context: RequestContext,
628    ) -> impl Future<Output = Handled<Page<crate::v2_3_0::payments::Terminal>>> + Send;
629
630    /// `GET {payments}/terminals/{terminal_id}`.
631    fn terminal(
632        &self,
633        terminal_id: String,
634        context: RequestContext,
635    ) -> impl Future<Output = Handled<crate::v2_3_0::payments::Terminal>> + Send;
636
637    /// `PUT {payments}/terminals/{terminal_id}` — the CPO updating a terminal's location data.
638    fn put_terminal(
639        &self,
640        terminal_id: String,
641        terminal: crate::v2_3_0::payments::Terminal,
642        context: RequestContext,
643    ) -> impl Future<Output = Handled<crate::v2_3_0::payments::Terminal>> + Send;
644
645    /// `PATCH {payments}/terminals/{terminal_id}`.
646    ///
647    /// > *This PATCH should be used by the CPO to assign location ids and/or evse_uids to a
648    /// > terminal.*
649    fn patch_terminal(
650        &self,
651        terminal_id: String,
652        patch: Patch<crate::v2_3_0::payments::Terminal>,
653        context: RequestContext,
654    ) -> impl Future<Output = Handled<crate::v2_3_0::payments::Terminal>> + Send;
655
656    /// `POST {payments}/terminals/activate`.
657    ///
658    /// > *NOTE: The terminal_id is optional in the activation request as it will be set by the
659    /// > PTP. The cardinality for the remaining fields stays the same.*
660    ///
661    /// A `Terminal` without its `terminal_id` is not a `Terminal`, so the body arrives as a
662    /// [`Patch`] — this crate's type for "an OCPI object with fields left out". Note that it is
663    /// **not** a merge patch: this is a `POST`, nothing is being merged into anything, and the
664    /// rule that a `PATCH` must carry `last_updated` deliberately does not apply. Read the
665    /// fields with [`Patch::as_value`]; do not call [`Patch::apply`].
666    fn activate_terminal(
667        &self,
668        terminal: Patch<crate::v2_3_0::payments::Terminal>,
669        context: RequestContext,
670    ) -> impl Future<Output = Handled<crate::v2_3_0::payments::Terminal>> + Send;
671
672    /// `POST {payments}/terminals/{terminal_id}/deactivate`.
673    fn deactivate_terminal(
674        &self,
675        terminal_id: String,
676        context: RequestContext,
677    ) -> impl Future<Output = Handled<crate::v2_3_0::payments::Terminal>> + Send;
678
679    /// `GET {payments}/financial-advice-confirmations` — one page.
680    fn financial_advice_confirmations(
681        &self,
682        query: PageQuery,
683        context: RequestContext,
684    ) -> impl Future<Output = Handled<Page<crate::v2_3_0::payments::FinancialAdviceConfirmation>>> + Send;
685
686    /// `GET {payments}/financial-advice-confirmations/{id}`.
687    fn financial_advice_confirmation(
688        &self,
689        id: String,
690        context: RequestContext,
691    ) -> impl Future<Output = Handled<crate::v2_3_0::payments::FinancialAdviceConfirmation>> + Send;
692}
693
694/// The CPO side of the Payments module: the PTP's terminals as the CPO stores them.
695///
696/// Unusually for OCPI, these are **POSTs, not PUTs**, and the URL carries no owning party:
697///
698/// > *The POST should be used by the PTP to create a newly shipped terminal on the CPO's system.*
699///
700/// Spec: 2.3.0 §mod_payments_cpo_interface
701pub trait PaymentsReceiver: Send + Sync + 'static {
702    /// `GET {payments}/terminals/{terminal_id}` — what the CPO has stored.
703    fn terminal(
704        &self,
705        terminal_id: String,
706        context: RequestContext,
707    ) -> impl Future<Output = Handled<crate::v2_3_0::payments::Terminal>> + Send;
708
709    /// `POST {payments}/terminals` — the PTP creating a terminal in the CPO's system.
710    fn post_terminal(
711        &self,
712        terminal: crate::v2_3_0::payments::Terminal,
713        context: RequestContext,
714    ) -> impl Future<Output = Handled<crate::v2_3_0::payments::Terminal>> + Send;
715
716    /// `GET {payments}/financial-advice-confirmations/{id}`.
717    fn financial_advice_confirmation(
718        &self,
719        id: String,
720        context: RequestContext,
721    ) -> impl Future<Output = Handled<crate::v2_3_0::payments::FinancialAdviceConfirmation>> + Send;
722
723    /// `POST {payments}/financial-advice-confirmations`.
724    ///
725    /// > *The PTP has to make sure to use the same authorization reference as provided in the
726    /// > Commands.StartSession so that the CPO can properly map the financial advice to the
727    /// > session.*
728    fn post_financial_advice_confirmation(
729        &self,
730        confirmation: crate::v2_3_0::payments::FinancialAdviceConfirmation,
731        context: RequestContext,
732    ) -> impl Future<Output = Handled<crate::v2_3_0::payments::FinancialAdviceConfirmation>> + Send;
733}
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738
739    #[test]
740    fn created_maps_to_the_status_the_spec_gives() {
741        assert_eq!(Created::Yes.http_status(), 201);
742        assert_eq!(Created::No.http_status(), 200);
743        assert_eq!(Created::from(true), Created::Yes);
744        assert_eq!(Created::from(false), Created::No);
745    }
746}