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}