Skip to main content

ocpi_kit/server/
router.rs

1//! The router: mount the interfaces this party serves, get an `axum::Router`.
2
3use std::sync::Arc;
4
5use axum::Router;
6use axum::extract::{Path, Query, State};
7use axum::routing::{delete, get, post, put};
8
9use crate::transport::{OcpiError, Page, PageMeta, Quirks};
10use crate::types::{PartyRef, Url};
11use crate::{InterfaceRole, ModuleId, VersionNumber};
12
13use super::auth::{MountedModules, TokenStore};
14use super::error::{OcpiErrorResponse, OcpiReply};
15use super::extract::{
16    Auth, AuthState, ContentTypePolicy, Ids, OcpiJson, OcpiPatch, Owner, Page as PageParams, RequestContext,
17    Routing, accepts_json,
18};
19use super::traits::{
20    CdrsReceiver, CdrsSender, ChargingProfilesReceiver, ChargingProfilesSender, CommandsReceiver,
21    CommandsSender, CredentialsHandler, HubClientInfoReceiver, HubClientInfoSender, LocationsReceiver,
22    LocationsSender, PaymentsReceiver, PaymentsSender, SessionsReceiver, SessionsSender, TariffsReceiver,
23    TariffsSender, TokensReceiver, TokensSender,
24};
25
26/// How the server behaves.
27#[derive(Clone, Debug)]
28#[non_exhaustive]
29pub struct ServerConfig {
30    /// The interoperability profile for incoming requests, chiefly whether an unencoded
31    /// `Authorization` token is accepted.
32    pub quirks: Quirks,
33    /// The largest page a list endpoint will return, which is what `X-Limit` advertises.
34    ///
35    /// > *`X-Limit`: The maximum number of objects that the server can return.*
36    pub max_page_limit: u64,
37    /// A path segment inserted before every **Receiver** interface, e.g. `receiver`.
38    ///
39    /// # Why this exists
40    ///
41    /// The Locations Sender and Receiver interfaces have structurally identical URLs:
42    ///
43    /// ```text
44    /// Sender:   {locations}/{location_id}/{evse_uid}/{connector_id}
45    /// Receiver: {locations}/{country_code}/{party_id}/{location_id}
46    /// ```
47    ///
48    /// Three path segments either way, so no router can tell them apart. The specification does
49    /// not have this problem because the two are *different endpoints* with independently chosen
50    /// URLs — in practice `/ocpi/cpo/2.3.0/locations` and `/ocpi/emsp/2.3.0/locations` — and
51    /// *"The exact URL can be found by fetching the endpoint information from the API info
52    /// endpoint"*.
53    ///
54    /// A platform that is both CPO and eMSP therefore has two choices, and this field picks
55    /// between them:
56    ///
57    /// * `Some("receiver")` (the default) — one router, one `/versions`, and the Receiver
58    ///   interfaces published one segment deeper. The generated version details say so.
59    /// * `None` — the conventional split, with one [`OcpiRouter`] per role nested under its own
60    ///   base URL; set it with [`one_router_per_role`](Self::one_router_per_role). Mounting both
61    ///   interfaces of an ambiguously-shaped module on one router with no prefix is a
62    ///   configuration error and panics at start-up with an explanation.
63    ///
64    /// Charging Profiles (`{session_id}` on both sides) and Payments
65    /// (`terminals/{terminal_id}` on both sides) have the same problem as Locations.
66    ///
67    /// Spec: 2.3.0 §transport_and_format_interface_endpoints
68    pub receiver_path_prefix: Option<String>,
69}
70
71impl Default for ServerConfig {
72    fn default() -> Self {
73        Self {
74            quirks: Quirks::default(),
75            max_page_limit: 100,
76            receiver_path_prefix: Some("receiver".to_owned()),
77        }
78    }
79}
80
81impl ServerConfig {
82    /// Sets the interoperability profile for incoming requests.
83    #[must_use]
84    pub fn with_quirks(mut self, quirks: Quirks) -> Self {
85        self.quirks = quirks;
86        self
87    }
88
89    /// Sets the largest page a list endpoint will return, which is what `X-Limit` advertises and
90    /// what the [`Page`](super::Page) extractor clamps an incoming `limit` to.
91    #[must_use]
92    pub const fn with_max_page_limit(mut self, limit: u64) -> Self {
93        self.max_page_limit = limit;
94        self
95    }
96
97    /// Publishes the Receiver interfaces under `prefix` instead of the default `receiver`.
98    #[must_use]
99    pub fn with_receiver_path_prefix(mut self, prefix: impl Into<String>) -> Self {
100        self.receiver_path_prefix = Some(prefix.into());
101        self
102    }
103
104    /// Publishes the Receiver interfaces at their bare paths, for a deployment that runs **one
105    /// router per role**.
106    ///
107    /// This is the conventional OCPI split — `/ocpi/cpo/2.3.0/locations` and
108    /// `/ocpi/emsp/2.3.0/locations` are different endpoints with independently chosen URLs — and
109    /// it is the right choice when each role has its own base URL. It is *not* a choice a
110    /// platform serving both roles from one router can make: see
111    /// [`receiver_path_prefix`](Self::receiver_path_prefix) for why, and expect a panic at
112    /// start-up if you try.
113    #[must_use]
114    pub fn one_router_per_role(mut self) -> Self {
115        self.receiver_path_prefix = None;
116        self
117    }
118}
119
120/// Everything the extractors and handlers share.
121pub struct OcpiState {
122    tokens: Arc<dyn TokenStore>,
123    config: ServerConfig,
124    base_url: Url,
125    mounted: MountedModules,
126    version: VersionNumber,
127}
128
129impl OcpiState {
130    /// The configuration in use.
131    #[must_use]
132    pub const fn config(&self) -> &ServerConfig {
133        &self.config
134    }
135
136    /// The base URL this server's endpoints are published under.
137    #[must_use]
138    pub const fn base_url(&self) -> &Url {
139        &self.base_url
140    }
141
142    /// The modules and interfaces that were mounted.
143    #[must_use]
144    pub const fn mounted(&self) -> &MountedModules {
145        &self.mounted
146    }
147
148    /// The OCPI version this router publishes.
149    #[must_use]
150    pub const fn version(&self) -> &VersionNumber {
151        &self.version
152    }
153
154    /// The version details this server advertises, generated from what was mounted.
155    #[must_use]
156    pub fn version_details(&self) -> crate::v2_3_0::versions::VersionDetails {
157        let endpoints = self
158            .mounted
159            .all()
160            .iter()
161            .map(|(module, role)| {
162                let mut url = self.base_url.clone();
163                if *role == InterfaceRole::Receiver
164                    && let Some(prefix) = &self.config.receiver_path_prefix
165                {
166                    url = url.join(prefix);
167                }
168                crate::v2_3_0::versions::Endpoint::new(module.clone(), *role, url.join(module.as_str()))
169            })
170            .collect();
171        crate::v2_3_0::versions::VersionDetails::new(self.version.clone(), endpoints)
172    }
173}
174
175impl AuthState for Arc<OcpiState> {
176    fn tokens(&self) -> &dyn TokenStore {
177        self.tokens.as_ref()
178    }
179    fn quirks(&self) -> &Quirks {
180        &self.config.quirks
181    }
182}
183
184impl super::extract::PagePolicy for Arc<OcpiState> {
185    fn max_page_limit(&self) -> u64 {
186        self.config.max_page_limit
187    }
188}
189
190impl ContentTypePolicy for Arc<OcpiState> {
191    fn accepts_content_type(&self, headers: &http::HeaderMap) -> bool {
192        accepts_json(headers, self.config.quirks.lenient_content_type)
193    }
194}
195
196/// Builds the `axum::Router` that serves one OCPI version.
197///
198/// ```no_run
199/// use std::sync::Arc;
200/// use ocpi_kit::server::{InMemoryTokenStore, OcpiRouter};
201/// use ocpi_kit::types::Url;
202/// use ocpi_kit::VersionNumber;
203///
204/// # fn build(locations: impl ocpi_kit::server::LocationsSender,
205/// #          credentials: impl ocpi_kit::server::CredentialsHandler)
206/// # -> Result<axum::Router, Box<dyn std::error::Error>> {
207/// let router = OcpiRouter::new(
208///         VersionNumber::V2_3_0,
209///         Url::new("https://cpo.example.com/ocpi/cpo/2.3.0")?,
210///         Arc::new(InMemoryTokenStore::new()),
211///     )
212///     .credentials(credentials)
213///     .locations_sender(locations)
214///     .build();
215/// # Ok(router)
216/// # }
217/// ```
218///
219/// The version details endpoint is generated from exactly what was mounted, so a peer's
220/// discovery can never disagree with what this server actually serves.
221pub struct OcpiRouter {
222    router: Router<Arc<OcpiState>>,
223    tokens: Arc<dyn TokenStore>,
224    config: ServerConfig,
225    base_url: Url,
226    mounted: MountedModules,
227    version: VersionNumber,
228}
229
230impl OcpiRouter {
231    /// The prefix Receiver routes are mounted under, captured before the router is moved.
232    fn receiver_prefix(&self) -> Option<String> {
233        self.config.receiver_path_prefix.clone()
234    }
235
236    /// Refuses, with an explanation, a combination no router can serve.
237    ///
238    /// # Panics
239    ///
240    /// Panics when both interfaces of a module whose two interfaces have the same URL shape are
241    /// mounted on one router with no [`ServerConfig::receiver_path_prefix`]. Better a clear
242    /// message at start-up than the routing conflict `axum` would raise a moment later.
243    ///
244    /// Checked in **both** mount orders, because the conflict does not care which came first.
245    fn check_interface_conflict(&self, module: &ModuleId, mounting: InterfaceRole) {
246        // Locations:        `{loc}/{evse}/{conn}`   vs `{cc}/{party}/{loc}`
247        // ChargingProfiles: `{session_id}`          vs `{session_id}`
248        // Payments:         `terminals/{id}`        vs `terminals/{id}`
249        let ambiguous =
250            matches!(module, ModuleId::Locations | ModuleId::ChargingProfiles | ModuleId::Payments);
251        let other =
252            if mounting == InterfaceRole::Sender { InterfaceRole::Receiver } else { InterfaceRole::Sender };
253        assert!(
254            !(ambiguous
255                && self.config.receiver_path_prefix.is_none()
256                && self.mounted.contains(module, other)),
257            "cannot mount both interfaces of the {module} module on one router with no \
258                 receiver path prefix: the Sender and Receiver URLs have the same shape, so no \
259                 route ordering can tell them apart. Either set \
260                 ServerConfig::receiver_path_prefix, or build one OcpiRouter per interface role \
261                 and nest them under different base URLs.",
262        );
263    }
264
265    /// Starts a router for one OCPI version, published under `base_url`.
266    #[must_use]
267    pub fn new(version: VersionNumber, base_url: Url, tokens: Arc<dyn TokenStore>) -> Self {
268        Self {
269            router: Router::new(),
270            tokens,
271            config: ServerConfig::default(),
272            base_url,
273            mounted: MountedModules::new(),
274            version,
275        }
276    }
277
278    /// Overrides the server configuration.
279    #[must_use]
280    pub fn with_config(mut self, config: ServerConfig) -> Self {
281        self.config = config;
282        self
283    }
284
285    /// Mounts the credentials module, which every implementation must serve.
286    #[must_use]
287    pub fn credentials<H: CredentialsHandler>(mut self, handler: H) -> Self {
288        let handler = Arc::new(handler);
289        self.mounted.add(ModuleId::Credentials, InterfaceRole::Sender);
290        let get_handler = Arc::clone(&handler);
291        let post_handler = Arc::clone(&handler);
292        let put_handler = Arc::clone(&handler);
293        let delete_handler = handler;
294        self.router = self.router.route(
295            "/credentials",
296            get(async move |auth: Auth, ids: Ids| -> Result<_, OcpiErrorResponse> {
297                let context = context_of(auth, ids, Routing(None), &ModuleId::Credentials)?;
298                let ids = context.ids.clone();
299                get_handler
300                    .get(context)
301                    .await
302                    .map(|c| OcpiReply::ok(c).with_ids(ids.clone()))
303                    .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
304            })
305            .post(
306                async move |auth: Auth,
307                            ids: Ids,
308                            OcpiJson(body): OcpiJson<crate::v2_3_0::credentials::Credentials>|
309                            -> Result<_, OcpiErrorResponse> {
310                    let context = context_of(auth, ids, Routing(None), &ModuleId::Credentials)?;
311                    let ids = context.ids.clone();
312                    post_handler
313                        .post(body, context)
314                        .await
315                        .map(|c| OcpiReply::ok(c).with_ids(ids.clone()))
316                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
317                },
318            )
319            .put(
320                async move |auth: Auth,
321                            ids: Ids,
322                            OcpiJson(body): OcpiJson<crate::v2_3_0::credentials::Credentials>|
323                            -> Result<_, OcpiErrorResponse> {
324                    let context = context_of(auth, ids, Routing(None), &ModuleId::Credentials)?;
325                    let ids = context.ids.clone();
326                    put_handler
327                        .put(body, context)
328                        .await
329                        .map(|c| OcpiReply::ok(c).with_ids(ids.clone()))
330                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
331                },
332            )
333            .delete(async move |auth: Auth, ids: Ids| -> Result<_, OcpiErrorResponse> {
334                let context = context_of(auth, ids, Routing(None), &ModuleId::Credentials)?;
335                let ids = context.ids.clone();
336                delete_handler
337                    .delete(context)
338                    .await
339                    .map(|()| OcpiReply::<()>::no_content().with_ids(ids.clone()))
340                    .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
341            }),
342        );
343        self
344    }
345
346    /// Mounts the Locations Sender interface: `GET` on locations, EVSEs and connectors.
347    #[must_use]
348    pub fn locations_sender<H: LocationsSender>(mut self, handler: H) -> Self {
349        self.check_interface_conflict(&ModuleId::Locations, InterfaceRole::Sender);
350        let handler = Arc::new(handler);
351        self.mounted.add(ModuleId::Locations, InterfaceRole::Sender);
352
353        let list = Arc::clone(&handler);
354        let one = Arc::clone(&handler);
355        let evse = Arc::clone(&handler);
356        let connector = handler;
357
358        self.router = self
359            .router
360            .route(
361                "/locations",
362                get(
363                    async move |auth: Auth,
364                                ids: Ids,
365                                routing: Routing,
366                                PageParams(query): PageParams,
367                                State(state): State<Arc<OcpiState>>|
368                                -> Result<_, OcpiErrorResponse> {
369                        let context = context_of(auth, ids, routing, &ModuleId::Locations)?;
370                        let ids = context.ids.clone();
371                        let responder = context.addressed_to().cloned();
372                        let response_routing = responder.as_ref().and_then(|r| context.response_routing(r));
373                        list.list(query, context)
374                            .await
375                            .map(|page| page_reply(page, &state))
376                            .map(|reply| {
377                                let reply = reply.with_ids(ids.clone());
378                                match response_routing {
379                                    Some(r) => reply.with_routing(r),
380                                    None => reply,
381                                }
382                            })
383                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
384                    },
385                ),
386            )
387            .route(
388                "/locations/{location_id}",
389                get(
390                    async move |auth: Auth,
391                                ids: Ids,
392                                routing: Routing,
393                                Path(location_id): Path<String>|
394                                -> Result<_, OcpiErrorResponse> {
395                        let context = context_of(auth, ids, routing, &ModuleId::Locations)?;
396                        let ids = context.ids.clone();
397                        one.location(location_id, context)
398                            .await
399                            .map(|l| OcpiReply::ok(l).with_ids(ids.clone()))
400                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
401                    },
402                ),
403            )
404            .route(
405                "/locations/{location_id}/{evse_uid}",
406                get(
407                    async move |auth: Auth,
408                                ids: Ids,
409                                routing: Routing,
410                                Path((location_id, evse_uid)): Path<(String, String)>|
411                                -> Result<_, OcpiErrorResponse> {
412                        let context = context_of(auth, ids, routing, &ModuleId::Locations)?;
413                        let ids = context.ids.clone();
414                        evse.evse(location_id, evse_uid, context)
415                            .await
416                            .map(|e| OcpiReply::ok(e).with_ids(ids.clone()))
417                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
418                    },
419                ),
420            )
421            .route(
422                "/locations/{location_id}/{evse_uid}/{connector_id}",
423                get(
424                    async move |auth: Auth,
425                                ids: Ids,
426                                routing: Routing,
427                                Path((location_id, evse_uid, connector_id)): Path<(
428                        String,
429                        String,
430                        String,
431                    )>|
432                                -> Result<_, OcpiErrorResponse> {
433                        let context = context_of(auth, ids, routing, &ModuleId::Locations)?;
434                        let ids = context.ids.clone();
435                        connector
436                            .connector(location_id, evse_uid, connector_id, context)
437                            .await
438                            .map(|c| OcpiReply::ok(c).with_ids(ids.clone()))
439                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
440                    },
441                ),
442            );
443        self
444    }
445
446    /// Mounts the Locations Receiver interface: the client-owned-object `GET`, `PUT` and `PATCH`.
447    #[must_use]
448    pub fn locations_receiver<H: LocationsReceiver>(mut self, handler: H) -> Self {
449        let prefix = self.receiver_prefix();
450        self.check_interface_conflict(&ModuleId::Locations, InterfaceRole::Receiver);
451        let handler = Arc::new(handler);
452        self.mounted.add(ModuleId::Locations, InterfaceRole::Receiver);
453
454        let get_one = Arc::clone(&handler);
455        let put_location = Arc::clone(&handler);
456        let put_evse = Arc::clone(&handler);
457        let patch_any = handler;
458
459        self.router = self
460            .router
461            .route(
462                &receiver_path(prefix.as_deref(), "/locations/{country_code}/{party_id}/{location_id}"),
463                get(
464                    async move |auth: Auth,
465                                ids: Ids,
466                                routing: Routing,
467                                Path((country_code, party_id, location_id)): Path<(
468                        String,
469                        String,
470                        String,
471                    )>|
472                                -> Result<_, OcpiErrorResponse> {
473                        let (context, owner) = owned_context(
474                            auth,
475                            ids,
476                            routing,
477                            &ModuleId::Locations,
478                            &country_code,
479                            &party_id,
480                        )?;
481                        let ids = context.ids.clone();
482                        get_one
483                            .location(owner, location_id, context)
484                            .await
485                            .map(|l| OcpiReply::ok(l).with_ids(ids.clone()))
486                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
487                    },
488                )
489                .put(
490                    async move |auth: Auth,
491                                ids: Ids,
492                                routing: Routing,
493                                Path((country_code, party_id, location_id)): Path<(
494                        String,
495                        String,
496                        String,
497                    )>,
498                                OcpiJson(location): OcpiJson<crate::v2_3_0::locations::Location>|
499                                -> Result<_, OcpiErrorResponse> {
500                        let (context, owner) = owned_context(
501                            auth,
502                            ids,
503                            routing,
504                            &ModuleId::Locations,
505                            &country_code,
506                            &party_id,
507                        )?;
508                        let ids = context.ids.clone();
509                        // "server implementations are advised to return 2001 when the {object-id}
510                        //  in the URL is different from the id in the object being pushed"
511                        if !location.id.eq_ignore_case(&location_id) {
512                            return Err(OcpiErrorResponse::new(OcpiError::Decode {
513                                path: "/id".to_owned(),
514                                message: format!(
515                                    "the object id {:?} does not match the {location_id:?} in the URL",
516                                    location.id.as_str()
517                                ),
518                            })
519                            .with_ids(ids));
520                        }
521                        put_location
522                            .put_location(owner, location, context)
523                            .await
524                            .map(|created| {
525                                OcpiReply::<()>::no_content()
526                                    .with_http_status(status_of(created))
527                                    .with_ids(ids.clone())
528                            })
529                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
530                    },
531                )
532                .patch(
533                    async move |auth: Auth,
534                                ids: Ids,
535                                routing: Routing,
536                                Path((country_code, party_id, location_id)): Path<(
537                        String,
538                        String,
539                        String,
540                    )>,
541                                OcpiPatch(patch): OcpiPatch<serde_json::Value>|
542                                -> Result<_, OcpiErrorResponse> {
543                        let (context, owner) = owned_context(
544                            auth,
545                            ids,
546                            routing,
547                            &ModuleId::Locations,
548                            &country_code,
549                            &party_id,
550                        )?;
551                        let ids = context.ids.clone();
552                        patch_any
553                            .patch(owner, location_id, None, None, patch, context)
554                            .await
555                            .map(|()| OcpiReply::<()>::no_content().with_ids(ids.clone()))
556                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
557                    },
558                ),
559            )
560            .route(
561                &receiver_path(
562                    prefix.as_deref(),
563                    "/locations/{country_code}/{party_id}/{location_id}/{evse_uid}",
564                ),
565                put(
566                    async move |auth: Auth,
567                                ids: Ids,
568                                routing: Routing,
569                                Path((country_code, party_id, location_id, _evse_uid)): Path<(
570                        String,
571                        String,
572                        String,
573                        String,
574                    )>,
575                                OcpiJson(evse): OcpiJson<crate::v2_3_0::locations::Evse>|
576                                -> Result<_, OcpiErrorResponse> {
577                        let (context, owner) = owned_context(
578                            auth,
579                            ids,
580                            routing,
581                            &ModuleId::Locations,
582                            &country_code,
583                            &party_id,
584                        )?;
585                        let ids = context.ids.clone();
586                        put_evse
587                            .put_evse(owner, location_id, evse, context)
588                            .await
589                            .map(|created| {
590                                OcpiReply::<()>::no_content()
591                                    .with_http_status(status_of(created))
592                                    .with_ids(ids.clone())
593                            })
594                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
595                    },
596                ),
597            );
598        self
599    }
600
601    /// Mounts the Tokens Sender interface: `GET` list and real-time authorization.
602    #[must_use]
603    pub fn tokens_sender<H: TokensSender>(mut self, handler: H) -> Self {
604        let handler = Arc::new(handler);
605        self.mounted.add(ModuleId::Tokens, InterfaceRole::Sender);
606        let list = Arc::clone(&handler);
607        let authorize = handler;
608
609        self.router = self
610            .router
611            .route(
612                "/tokens",
613                get(
614                    async move |auth: Auth,
615                                ids: Ids,
616                                routing: Routing,
617                                PageParams(query): PageParams,
618                                State(state): State<Arc<OcpiState>>|
619                                -> Result<_, OcpiErrorResponse> {
620                        let context = context_of(auth, ids, routing, &ModuleId::Tokens)?;
621                        let ids = context.ids.clone();
622                        list.list(query, context)
623                            .await
624                            .map(|page| page_reply(page, &state).with_ids(ids.clone()))
625                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
626                    },
627                ),
628            )
629            .route(
630                "/tokens/{token_uid}/authorize",
631                post(
632                    async move |auth: Auth,
633                                ids: Ids,
634                                routing: Routing,
635                                Path(token_uid): Path<String>,
636                                Query(params): Query<TokenTypeQuery>,
637                                body: axum::body::Bytes|
638                                -> Result<_, OcpiErrorResponse> {
639                        let context = context_of(auth, ids, routing, &ModuleId::Tokens)?;
640                        let ids = context.ids.clone();
641                        // "LocationReferences: Location and EVSEs for which the driver wants to
642                        //  charge" — the body is optional, so an empty or `{}` body means
643                        //  "anywhere".
644                        let location =
645                            decode_optional_body::<crate::v2_3_0::tokens::LocationReferences>(&body)
646                                .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids.clone()))?;
647                        authorize
648                            .authorize(token_uid, params.token_type, location, context)
649                            .await
650                            .map(|info| OcpiReply::ok(info).with_ids(ids.clone()))
651                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
652                    },
653                ),
654            );
655        self
656    }
657
658    /// Mounts the Tokens Receiver interface.
659    #[must_use]
660    pub fn tokens_receiver<H: TokensReceiver>(mut self, handler: H) -> Self {
661        let prefix = self.receiver_prefix();
662        let handler = Arc::new(handler);
663        self.mounted.add(ModuleId::Tokens, InterfaceRole::Receiver);
664        let get_one = Arc::clone(&handler);
665        let put_one = Arc::clone(&handler);
666        let patch_one = handler;
667
668        self.router = self.router.route(
669            &receiver_path(prefix.as_deref(), "/tokens/{country_code}/{party_id}/{token_uid}"),
670            get(
671                async move |auth: Auth,
672                            ids: Ids,
673                            routing: Routing,
674                            Path((country_code, party_id, token_uid)): Path<(String, String, String)>,
675                            Query(params): Query<TokenTypeQuery>|
676                            -> Result<_, OcpiErrorResponse> {
677                    let (context, owner) =
678                        owned_context(auth, ids, routing, &ModuleId::Tokens, &country_code, &party_id)?;
679                    let ids = context.ids.clone();
680                    get_one
681                        .token(owner, token_uid, params.token_type, context)
682                        .await
683                        .map(|t| OcpiReply::ok(t).with_ids(ids.clone()))
684                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
685                },
686            )
687            .put(
688                async move |auth: Auth,
689                            ids: Ids,
690                            routing: Routing,
691                            Path((country_code, party_id, _token_uid)): Path<(String, String, String)>,
692                            OcpiJson(token): OcpiJson<crate::v2_3_0::tokens::Token>|
693                            -> Result<_, OcpiErrorResponse> {
694                    let (context, owner) =
695                        owned_context(auth, ids, routing, &ModuleId::Tokens, &country_code, &party_id)?;
696                    let ids = context.ids.clone();
697                    put_one
698                        .put_token(owner, token, context)
699                        .await
700                        .map(|created| {
701                            OcpiReply::<()>::no_content()
702                                .with_http_status(status_of(created))
703                                .with_ids(ids.clone())
704                        })
705                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
706                },
707            )
708            .patch(
709                async move |auth: Auth,
710                            ids: Ids,
711                            routing: Routing,
712                            Path((country_code, party_id, token_uid)): Path<(String, String, String)>,
713                            Query(params): Query<TokenTypeQuery>,
714                            OcpiPatch(patch): OcpiPatch<crate::v2_3_0::tokens::Token>|
715                            -> Result<_, OcpiErrorResponse> {
716                    let (context, owner) =
717                        owned_context(auth, ids, routing, &ModuleId::Tokens, &country_code, &party_id)?;
718                    let ids = context.ids.clone();
719                    patch_one
720                        .patch_token(owner, token_uid, params.token_type, patch, context)
721                        .await
722                        .map(|()| OcpiReply::<()>::no_content().with_ids(ids.clone()))
723                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
724                },
725            ),
726        );
727        self
728    }
729
730    /// Mounts the CDRs Sender interface.
731    #[must_use]
732    pub fn cdrs_sender<H: CdrsSender>(mut self, handler: H) -> Self {
733        let handler = Arc::new(handler);
734        self.mounted.add(ModuleId::Cdrs, InterfaceRole::Sender);
735        self.router = self.router.route(
736            "/cdrs",
737            get(
738                async move |auth: Auth,
739                            ids: Ids,
740                            routing: Routing,
741                            PageParams(query): PageParams,
742                            State(state): State<Arc<OcpiState>>|
743                            -> Result<_, OcpiErrorResponse> {
744                    let context = context_of(auth, ids, routing, &ModuleId::Cdrs)?;
745                    let ids = context.ids.clone();
746                    handler
747                        .list(query, context)
748                        .await
749                        .map(|page| page_reply(page, &state).with_ids(ids.clone()))
750                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
751                },
752            ),
753        );
754        self
755    }
756
757    /// Mounts the CDRs Receiver interface, including the `POST` that returns a `Location` header.
758    #[must_use]
759    pub fn cdrs_receiver<H: CdrsReceiver>(mut self, handler: H) -> Self {
760        let prefix = self.receiver_prefix();
761        let handler = Arc::new(handler);
762        self.mounted.add(ModuleId::Cdrs, InterfaceRole::Receiver);
763        let get_one = Arc::clone(&handler);
764        let post_one = handler;
765
766        self.router = self
767            .router
768            .route(
769                &receiver_path(prefix.as_deref(), "/cdrs"),
770                post(
771                    async move |auth: Auth,
772                                ids: Ids,
773                                routing: Routing,
774                                OcpiJson(cdr): OcpiJson<crate::v2_3_0::cdrs::Cdr>|
775                                -> Result<_, OcpiErrorResponse> {
776                        let context = context_of(auth, ids, routing, &ModuleId::Cdrs)?;
777                        let ids = context.ids.clone();
778                        post_one
779                            .post_cdr(cdr, context)
780                            .await
781                            .map(|location| {
782                                let mut headers = http::HeaderMap::new();
783                                if let Ok(value) = http::HeaderValue::from_str(location.as_str()) {
784                                    headers.insert(crate::transport::headers::LOCATION, value);
785                                }
786                                OcpiReply::<()>::no_content()
787                                    .with_http_status(http::StatusCode::CREATED)
788                                    .with_headers(headers)
789                                    .with_ids(ids.clone())
790                            })
791                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
792                    },
793                ),
794            )
795            .route(
796                &receiver_path(prefix.as_deref(), "/cdrs/{cdr_id}"),
797                get(
798                    async move |auth: Auth,
799                                ids: Ids,
800                                routing: Routing,
801                                Path(cdr_id): Path<String>|
802                                -> Result<_, OcpiErrorResponse> {
803                        let context = context_of(auth, ids, routing, &ModuleId::Cdrs)?;
804                        let ids = context.ids.clone();
805                        get_one
806                            .cdr(cdr_id, context)
807                            .await
808                            .map(|c| OcpiReply::ok(c).with_ids(ids.clone()))
809                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
810                    },
811                ),
812            );
813        self
814    }
815
816    /// Mounts the Sessions Sender interface, including `PUT charging_preferences`.
817    #[must_use]
818    pub fn sessions_sender<H: SessionsSender>(mut self, handler: H) -> Self {
819        let handler = Arc::new(handler);
820        self.mounted.add(ModuleId::Sessions, InterfaceRole::Sender);
821        let list = Arc::clone(&handler);
822        let preferences = handler;
823
824        self.router = self
825            .router
826            .route(
827                "/sessions",
828                get(
829                    async move |auth: Auth,
830                                ids: Ids,
831                                routing: Routing,
832                                PageParams(query): PageParams,
833                                State(state): State<Arc<OcpiState>>|
834                                -> Result<_, OcpiErrorResponse> {
835                        let context = context_of(auth, ids, routing, &ModuleId::Sessions)?;
836                        let ids = context.ids.clone();
837                        list.list(query, context)
838                            .await
839                            .map(|page| page_reply(page, &state).with_ids(ids.clone()))
840                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
841                    },
842                ),
843            )
844            .route(
845                "/sessions/{session_id}/charging_preferences",
846                put(
847                    async move |auth: Auth,
848                                ids: Ids,
849                                routing: Routing,
850                                Path(session_id): Path<String>,
851                                OcpiJson(body): OcpiJson<crate::v2_3_0::sessions::ChargingPreferences>|
852                                -> Result<_, OcpiErrorResponse> {
853                        let context = context_of(auth, ids, routing, &ModuleId::Sessions)?;
854                        let ids = context.ids.clone();
855                        preferences
856                            .set_charging_preferences(session_id, body, context)
857                            .await
858                            .map(|r| OcpiReply::ok(r).with_ids(ids.clone()))
859                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
860                    },
861                ),
862            );
863        self
864    }
865
866    /// Mounts the Sessions Receiver interface.
867    #[must_use]
868    pub fn sessions_receiver<H: SessionsReceiver>(mut self, handler: H) -> Self {
869        let prefix = self.receiver_prefix();
870        let handler = Arc::new(handler);
871        self.mounted.add(ModuleId::Sessions, InterfaceRole::Receiver);
872        let get_one = Arc::clone(&handler);
873        let put_one = Arc::clone(&handler);
874        let patch_one = handler;
875
876        self.router = self.router.route(
877            &receiver_path(prefix.as_deref(), "/sessions/{country_code}/{party_id}/{session_id}"),
878            get(
879                async move |auth: Auth,
880                            ids: Ids,
881                            routing: Routing,
882                            Path((country_code, party_id, session_id)): Path<(String, String, String)>|
883                            -> Result<_, OcpiErrorResponse> {
884                    let (context, owner) =
885                        owned_context(auth, ids, routing, &ModuleId::Sessions, &country_code, &party_id)?;
886                    let ids = context.ids.clone();
887                    get_one
888                        .session(owner, session_id, context)
889                        .await
890                        .map(|s| OcpiReply::ok(s).with_ids(ids.clone()))
891                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
892                },
893            )
894            .put(
895                async move |auth: Auth,
896                            ids: Ids,
897                            routing: Routing,
898                            Path((country_code, party_id, _session_id)): Path<(String, String, String)>,
899                            OcpiJson(session): OcpiJson<crate::v2_3_0::sessions::Session>|
900                            -> Result<_, OcpiErrorResponse> {
901                    let (context, owner) =
902                        owned_context(auth, ids, routing, &ModuleId::Sessions, &country_code, &party_id)?;
903                    let ids = context.ids.clone();
904                    put_one
905                        .put_session(owner, session, context)
906                        .await
907                        .map(|created| {
908                            OcpiReply::<()>::no_content()
909                                .with_http_status(status_of(created))
910                                .with_ids(ids.clone())
911                        })
912                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
913                },
914            )
915            .patch(
916                async move |auth: Auth,
917                            ids: Ids,
918                            routing: Routing,
919                            Path((country_code, party_id, session_id)): Path<(String, String, String)>,
920                            OcpiPatch(patch): OcpiPatch<crate::v2_3_0::sessions::Session>|
921                            -> Result<_, OcpiErrorResponse> {
922                    let (context, owner) =
923                        owned_context(auth, ids, routing, &ModuleId::Sessions, &country_code, &party_id)?;
924                    let ids = context.ids.clone();
925                    patch_one
926                        .patch_session(owner, session_id, patch, context)
927                        .await
928                        .map(|()| OcpiReply::<()>::no_content().with_ids(ids.clone()))
929                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
930                },
931            ),
932        );
933        self
934    }
935
936    /// Mounts the Tariffs Sender interface.
937    #[must_use]
938    pub fn tariffs_sender<H: TariffsSender>(mut self, handler: H) -> Self {
939        let handler = Arc::new(handler);
940        self.mounted.add(ModuleId::Tariffs, InterfaceRole::Sender);
941        self.router = self.router.route(
942            "/tariffs",
943            get(
944                async move |auth: Auth,
945                            ids: Ids,
946                            routing: Routing,
947                            PageParams(query): PageParams,
948                            State(state): State<Arc<OcpiState>>|
949                            -> Result<_, OcpiErrorResponse> {
950                    let context = context_of(auth, ids, routing, &ModuleId::Tariffs)?;
951                    let ids = context.ids.clone();
952                    handler
953                        .list(query, context)
954                        .await
955                        .map(|page| page_reply(page, &state).with_ids(ids.clone()))
956                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
957                },
958            ),
959        );
960        self
961    }
962
963    /// Mounts the Tariffs Receiver interface, including the `DELETE`.
964    #[must_use]
965    pub fn tariffs_receiver<H: TariffsReceiver>(mut self, handler: H) -> Self {
966        let prefix = self.receiver_prefix();
967        let handler = Arc::new(handler);
968        self.mounted.add(ModuleId::Tariffs, InterfaceRole::Receiver);
969        let get_one = Arc::clone(&handler);
970        let put_one = Arc::clone(&handler);
971        let delete_one = handler;
972
973        self.router = self.router.route(
974            &receiver_path(prefix.as_deref(), "/tariffs/{country_code}/{party_id}/{tariff_id}"),
975            get(
976                async move |auth: Auth,
977                            ids: Ids,
978                            routing: Routing,
979                            Path((country_code, party_id, tariff_id)): Path<(String, String, String)>|
980                            -> Result<_, OcpiErrorResponse> {
981                    let (context, owner) =
982                        owned_context(auth, ids, routing, &ModuleId::Tariffs, &country_code, &party_id)?;
983                    let ids = context.ids.clone();
984                    get_one
985                        .tariff(owner, tariff_id, context)
986                        .await
987                        .map(|t| OcpiReply::ok(t).with_ids(ids.clone()))
988                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
989                },
990            )
991            .put(
992                async move |auth: Auth,
993                            ids: Ids,
994                            routing: Routing,
995                            Path((country_code, party_id, _tariff_id)): Path<(String, String, String)>,
996                            OcpiJson(tariff): OcpiJson<crate::v2_3_0::tariffs::Tariff>|
997                            -> Result<_, OcpiErrorResponse> {
998                    let (context, owner) =
999                        owned_context(auth, ids, routing, &ModuleId::Tariffs, &country_code, &party_id)?;
1000                    let ids = context.ids.clone();
1001                    put_one
1002                        .put_tariff(owner, tariff, context)
1003                        .await
1004                        .map(|created| {
1005                            OcpiReply::<()>::no_content()
1006                                .with_http_status(status_of(created))
1007                                .with_ids(ids.clone())
1008                        })
1009                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1010                },
1011            ),
1012        );
1013        self.router = self.router.route(
1014            &receiver_path(prefix.as_deref(), "/tariffs/{country_code}/{party_id}/{tariff_id}/"),
1015            delete(
1016                async move |auth: Auth,
1017                            ids: Ids,
1018                            routing: Routing,
1019                            Path((country_code, party_id, tariff_id)): Path<(String, String, String)>|
1020                            -> Result<_, OcpiErrorResponse> {
1021                    let (context, owner) =
1022                        owned_context(auth, ids, routing, &ModuleId::Tariffs, &country_code, &party_id)?;
1023                    let ids = context.ids.clone();
1024                    delete_one
1025                        .delete_tariff(owner, tariff_id, context)
1026                        .await
1027                        .map(|()| OcpiReply::<()>::no_content().with_ids(ids.clone()))
1028                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1029                },
1030            ),
1031        );
1032        self
1033    }
1034
1035    /// Mounts the Commands Receiver interface: `POST {commands}/{command}`.
1036    #[must_use]
1037    pub fn commands_receiver<H: CommandsReceiver>(mut self, handler: H) -> Self {
1038        let handler = Arc::new(handler);
1039        self.mounted.add(ModuleId::Commands, InterfaceRole::Receiver);
1040        self.router = self.router.route(
1041            "/commands/{command}",
1042            post(
1043                async move |auth: Auth,
1044                            ids: Ids,
1045                            routing: Routing,
1046                            Path(command_name): Path<String>,
1047                            body: axum::body::Bytes|
1048                            -> Result<_, OcpiErrorResponse> {
1049                    let context = context_of(auth, ids, routing, &ModuleId::Commands)?;
1050                    let ids = context.ids.clone();
1051                    let command = parse_command(&command_name, &body)
1052                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids.clone()))?;
1053                    handler
1054                        .command(command, context)
1055                        .await
1056                        .map(|r| OcpiReply::ok(r).with_ids(ids.clone()))
1057                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1058                },
1059            ),
1060        );
1061        self
1062    }
1063
1064    /// Mounts the Hub Client Info Sender interface: `GET {hubclientinfo}`.
1065    ///
1066    /// A configuration module, so its requests carry no routing headers.
1067    #[must_use]
1068    pub fn hub_client_info_sender<H: HubClientInfoSender>(mut self, handler: H) -> Self {
1069        let handler = Arc::new(handler);
1070        self.mounted.add(ModuleId::HubClientInfo, InterfaceRole::Sender);
1071        self.router = self.router.route(
1072            "/hubclientinfo",
1073            get(
1074                async move |auth: Auth,
1075                            ids: Ids,
1076                            PageParams(query): PageParams,
1077                            State(state): State<Arc<OcpiState>>|
1078                            -> Result<_, OcpiErrorResponse> {
1079                    let context = context_of(auth, ids, Routing(None), &ModuleId::HubClientInfo)?;
1080                    let ids = context.ids.clone();
1081                    handler
1082                        .list(query, context)
1083                        .await
1084                        .map(|page| page_reply(page, &state).with_ids(ids.clone()))
1085                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1086                },
1087            ),
1088        );
1089        self
1090    }
1091
1092    /// Mounts the Hub Client Info Receiver interface: `GET`/`PUT {hubclientinfo}/{cc}/{party}`.
1093    ///
1094    /// The party in the URL is the party the `ClientInfo` is **about**, not the party that owns
1095    /// the object — a hub pushes information about every other party to each of its clients — so
1096    /// the ownership rule that guards the other client-owned-object URLs deliberately does not
1097    /// apply here.
1098    #[must_use]
1099    pub fn hub_client_info_receiver<H: HubClientInfoReceiver>(mut self, handler: H) -> Self {
1100        let prefix = self.receiver_prefix();
1101        let handler = Arc::new(handler);
1102        self.mounted.add(ModuleId::HubClientInfo, InterfaceRole::Receiver);
1103        let get_one = Arc::clone(&handler);
1104        let put_one = handler;
1105
1106        self.router = self.router.route(
1107            &receiver_path(prefix.as_deref(), "/hubclientinfo/{country_code}/{party_id}"),
1108            get(
1109                async move |auth: Auth,
1110                            ids: Ids,
1111                            Path((country_code, party_id)): Path<(String, String)>|
1112                            -> Result<_, OcpiErrorResponse> {
1113                    let request_ids = ids.0.clone();
1114                    let context = context_of(auth, ids, Routing(None), &ModuleId::HubClientInfo)?;
1115                    let Owner(party) = Owner::from_path(&country_code, &party_id)
1116                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(request_ids.clone()))?;
1117                    get_one
1118                        .client_info(party, context)
1119                        .await
1120                        .map(|info| OcpiReply::ok(info).with_ids(request_ids.clone()))
1121                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(request_ids))
1122                },
1123            )
1124            .put(
1125                async move |auth: Auth,
1126                            ids: Ids,
1127                            Path((country_code, party_id)): Path<(String, String)>,
1128                            OcpiJson(info): OcpiJson<crate::v2_3_0::hub_client_info::ClientInfo>|
1129                            -> Result<_, OcpiErrorResponse> {
1130                    let request_ids = ids.0.clone();
1131                    let context = context_of(auth, ids, Routing(None), &ModuleId::HubClientInfo)?;
1132                    let Owner(party) = Owner::from_path(&country_code, &party_id)
1133                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(request_ids.clone()))?;
1134                    put_one
1135                        .put_client_info(party, info, context)
1136                        .await
1137                        .map(|created| {
1138                            OcpiReply::<()>::no_content()
1139                                .with_http_status(status_of(created))
1140                                .with_ids(request_ids.clone())
1141                        })
1142                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(request_ids))
1143                },
1144            ),
1145        );
1146        self
1147    }
1148
1149    /// Mounts the Commands Sender interface: the callback the Receiver POSTs its result to.
1150    ///
1151    /// The path is `{commands}/{command}/{unique_id}`, which is the shape the specification's own
1152    /// example uses (`.../commands/RESERVE_NOW/1234`). Build the matching `response_url` with
1153    /// [`CallbackUrls::command_result`] so the two can never drift apart.
1154    ///
1155    /// > *This URL might contain a unique ID to be able to distinguish between StartSession
1156    /// > requests.*
1157    #[must_use]
1158    pub fn commands_sender<H: CommandsSender>(mut self, handler: H) -> Self {
1159        let handler = Arc::new(handler);
1160        self.mounted.add(ModuleId::Commands, InterfaceRole::Sender);
1161        self.router = self.router.route(
1162            "/commands/{command}/{unique_id}",
1163            post(
1164                async move |auth: Auth,
1165                            ids: Ids,
1166                            routing: Routing,
1167                            Path((_command, unique_id)): Path<(String, String)>,
1168                            OcpiJson(result): OcpiJson<crate::v2_3_0::commands::CommandResult>|
1169                            -> Result<_, OcpiErrorResponse> {
1170                    let context = context_of(auth, ids, routing, &ModuleId::Commands)?;
1171                    let ids = context.ids.clone();
1172                    handler
1173                        .command_result(unique_id, result, context)
1174                        .await
1175                        .map(|()| OcpiReply::<()>::no_content().with_ids(ids.clone()))
1176                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1177                },
1178            ),
1179        );
1180        self
1181    }
1182
1183    /// Mounts the Charging Profiles Receiver interface: `GET`/`PUT`/`DELETE` on a session.
1184    #[must_use]
1185    pub fn charging_profiles_receiver<H: ChargingProfilesReceiver>(mut self, handler: H) -> Self {
1186        self.check_interface_conflict(&ModuleId::ChargingProfiles, InterfaceRole::Receiver);
1187        let prefix = self.receiver_prefix();
1188        let handler = Arc::new(handler);
1189        self.mounted.add(ModuleId::ChargingProfiles, InterfaceRole::Receiver);
1190        let get_one = Arc::clone(&handler);
1191        let put_one = Arc::clone(&handler);
1192        let delete_one = handler;
1193
1194        self.router = self.router.route(
1195            &receiver_path(prefix.as_deref(), "/chargingprofiles/{session_id}"),
1196            get(
1197                async move |auth: Auth,
1198                            ids: Ids,
1199                            routing: Routing,
1200                            Path(session_id): Path<String>,
1201                            Query(q): Query<ActiveProfileQuery>|
1202                            -> Result<_, OcpiErrorResponse> {
1203                    let request_ids = ids.0.clone();
1204                    let context = context_of(auth, ids, routing, &ModuleId::ChargingProfiles)?;
1205                    let response_url = q
1206                        .response_url
1207                        .ok_or_else(|| missing_query("response_url"))
1208                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(request_ids.clone()))?;
1209                    get_one
1210                        .active_charging_profile(session_id, q.duration.unwrap_or(0), response_url, context)
1211                        .await
1212                        .map(|r| OcpiReply::ok(r).with_ids(request_ids.clone()))
1213                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(request_ids))
1214                },
1215            )
1216            .put(
1217                async move |auth: Auth,
1218                            ids: Ids,
1219                            routing: Routing,
1220                            Path(session_id): Path<String>,
1221                            OcpiJson(request): OcpiJson<
1222                    crate::v2_3_0::charging_profiles::SetChargingProfile,
1223                >|
1224                            -> Result<_, OcpiErrorResponse> {
1225                    let context = context_of(auth, ids, routing, &ModuleId::ChargingProfiles)?;
1226                    let ids = context.ids.clone();
1227                    put_one
1228                        .set_charging_profile(session_id, request, context)
1229                        .await
1230                        .map(|r| OcpiReply::ok(r).with_ids(ids.clone()))
1231                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1232                },
1233            )
1234            .delete(
1235                async move |auth: Auth,
1236                            ids: Ids,
1237                            routing: Routing,
1238                            Path(session_id): Path<String>,
1239                            Query(q): Query<ActiveProfileQuery>|
1240                            -> Result<_, OcpiErrorResponse> {
1241                    let request_ids = ids.0.clone();
1242                    let context = context_of(auth, ids, routing, &ModuleId::ChargingProfiles)?;
1243                    let response_url = q
1244                        .response_url
1245                        .ok_or_else(|| missing_query("response_url"))
1246                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(request_ids.clone()))?;
1247                    delete_one
1248                        .clear_charging_profile(session_id, response_url, context)
1249                        .await
1250                        .map(|r| OcpiReply::ok(r).with_ids(request_ids.clone()))
1251                        .map_err(|e| OcpiErrorResponse::new(e).with_ids(request_ids))
1252                },
1253            ),
1254        );
1255        self
1256    }
1257
1258    /// Mounts the Charging Profiles Sender interface: the three result callbacks and the
1259    /// CPO-initiated `PUT` of a changed active profile.
1260    ///
1261    /// See [`ChargingProfilesSender`] for why there are three callback paths rather than one, and
1262    /// [`CallbackUrls`] for building the `response_url`s that reach them.
1263    #[must_use]
1264    pub fn charging_profiles_sender<H: ChargingProfilesSender>(mut self, handler: H) -> Self {
1265        self.check_interface_conflict(&ModuleId::ChargingProfiles, InterfaceRole::Sender);
1266        let handler = Arc::new(handler);
1267        self.mounted.add(ModuleId::ChargingProfiles, InterfaceRole::Sender);
1268        let active = Arc::clone(&handler);
1269        let set = Arc::clone(&handler);
1270        let clear = Arc::clone(&handler);
1271        let pushed = handler;
1272
1273        self.router = self
1274            .router
1275            .route(
1276                "/chargingprofiles/result/active/{unique_id}",
1277                post(
1278                    async move |auth: Auth,
1279                                ids: Ids,
1280                                routing: Routing,
1281                                Path(unique_id): Path<String>,
1282                                OcpiJson(result): OcpiJson<
1283                        crate::v2_3_0::charging_profiles::ActiveChargingProfileResult,
1284                    >|
1285                                -> Result<_, OcpiErrorResponse> {
1286                        let context = context_of(auth, ids, routing, &ModuleId::ChargingProfiles)?;
1287                        let ids = context.ids.clone();
1288                        active
1289                            .active_charging_profile_result(unique_id, result, context)
1290                            .await
1291                            .map(|()| OcpiReply::<()>::no_content().with_ids(ids.clone()))
1292                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1293                    },
1294                ),
1295            )
1296            .route(
1297                "/chargingprofiles/result/set/{unique_id}",
1298                post(
1299                    async move |auth: Auth,
1300                                ids: Ids,
1301                                routing: Routing,
1302                                Path(unique_id): Path<String>,
1303                                OcpiJson(result): OcpiJson<
1304                        crate::v2_3_0::charging_profiles::ChargingProfileResult,
1305                    >|
1306                                -> Result<_, OcpiErrorResponse> {
1307                        let context = context_of(auth, ids, routing, &ModuleId::ChargingProfiles)?;
1308                        let ids = context.ids.clone();
1309                        set.charging_profile_result(unique_id, result, context)
1310                            .await
1311                            .map(|()| OcpiReply::<()>::no_content().with_ids(ids.clone()))
1312                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1313                    },
1314                ),
1315            )
1316            .route(
1317                "/chargingprofiles/result/clear/{unique_id}",
1318                post(
1319                    async move |auth: Auth,
1320                                ids: Ids,
1321                                routing: Routing,
1322                                Path(unique_id): Path<String>,
1323                                OcpiJson(result): OcpiJson<
1324                        crate::v2_3_0::charging_profiles::ClearProfileResult,
1325                    >|
1326                                -> Result<_, OcpiErrorResponse> {
1327                        let context = context_of(auth, ids, routing, &ModuleId::ChargingProfiles)?;
1328                        let ids = context.ids.clone();
1329                        clear
1330                            .clear_profile_result(unique_id, result, context)
1331                            .await
1332                            .map(|()| OcpiReply::<()>::no_content().with_ids(ids.clone()))
1333                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1334                    },
1335                ),
1336            )
1337            .route(
1338                "/chargingprofiles/{session_id}",
1339                put(
1340                    async move |auth: Auth,
1341                                ids: Ids,
1342                                routing: Routing,
1343                                Path(session_id): Path<String>,
1344                                OcpiJson(profile): OcpiJson<
1345                        crate::v2_3_0::charging_profiles::ActiveChargingProfile,
1346                    >|
1347                                -> Result<_, OcpiErrorResponse> {
1348                        let context = context_of(auth, ids, routing, &ModuleId::ChargingProfiles)?;
1349                        let ids = context.ids.clone();
1350                        pushed
1351                            .put_active_charging_profile(session_id, profile, context)
1352                            .await
1353                            .map(|()| OcpiReply::<()>::no_content().with_ids(ids.clone()))
1354                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1355                    },
1356                ),
1357            );
1358        self
1359    }
1360
1361    /// Mounts the Payments Sender interface: the terminals and financial advice this PTP owns.
1362    #[must_use]
1363    pub fn payments_sender<H: PaymentsSender>(mut self, handler: H) -> Self {
1364        self.check_interface_conflict(&ModuleId::Payments, InterfaceRole::Sender);
1365        let handler = Arc::new(handler);
1366        self.mounted.add(ModuleId::Payments, InterfaceRole::Sender);
1367        let list = Arc::clone(&handler);
1368        let one = Arc::clone(&handler);
1369        let put_one = Arc::clone(&handler);
1370        let patch_one = Arc::clone(&handler);
1371        let activate = Arc::clone(&handler);
1372        let deactivate = Arc::clone(&handler);
1373        let fac_list = Arc::clone(&handler);
1374        let fac_one = handler;
1375
1376        self.router = self
1377            .router
1378            .route(
1379                "/payments/terminals",
1380                get(
1381                    async move |auth: Auth,
1382                                ids: Ids,
1383                                routing: Routing,
1384                                PageParams(query): PageParams,
1385                                State(state): State<Arc<OcpiState>>|
1386                                -> Result<_, OcpiErrorResponse> {
1387                        let context = context_of(auth, ids, routing, &ModuleId::Payments)?;
1388                        let ids = context.ids.clone();
1389                        list.terminals(query, context)
1390                            .await
1391                            .map(|page| page_reply(page, &state).with_ids(ids.clone()))
1392                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1393                    },
1394                ),
1395            )
1396            // A static segment beats a capture at the same position, so this stays reachable
1397            // even though `/payments/terminals/{terminal_id}` also matches it.
1398            .route(
1399                "/payments/terminals/activate",
1400                post(
1401                    async move |auth: Auth,
1402                                ids: Ids,
1403                                routing: Routing,
1404                                body: axum::body::Bytes|
1405                                -> Result<_, OcpiErrorResponse> {
1406                        let request_ids = ids.0.clone();
1407                        let context = context_of(auth, ids, routing, &ModuleId::Payments)?;
1408                        // Deliberately not `OcpiPatch`: this is a POST carrying a partial
1409                        // object, not an RFC 7396 merge patch, so the rule that a PATCH must
1410                        // carry `last_updated` does not apply to it.
1411                        let terminal = partial_object(&body)
1412                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(request_ids.clone()))?;
1413                        activate
1414                            .activate_terminal(terminal, context)
1415                            .await
1416                            .map(|t| OcpiReply::ok(t).with_ids(request_ids.clone()))
1417                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(request_ids))
1418                    },
1419                ),
1420            )
1421            .route(
1422                "/payments/terminals/{terminal_id}",
1423                get(
1424                    async move |auth: Auth,
1425                                ids: Ids,
1426                                routing: Routing,
1427                                Path(terminal_id): Path<String>|
1428                                -> Result<_, OcpiErrorResponse> {
1429                        let context = context_of(auth, ids, routing, &ModuleId::Payments)?;
1430                        let ids = context.ids.clone();
1431                        one.terminal(terminal_id, context)
1432                            .await
1433                            .map(|t| OcpiReply::ok(t).with_ids(ids.clone()))
1434                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1435                    },
1436                )
1437                .put(
1438                    async move |auth: Auth,
1439                                ids: Ids,
1440                                routing: Routing,
1441                                Path(terminal_id): Path<String>,
1442                                OcpiJson(terminal): OcpiJson<crate::v2_3_0::payments::Terminal>|
1443                                -> Result<_, OcpiErrorResponse> {
1444                        let context = context_of(auth, ids, routing, &ModuleId::Payments)?;
1445                        let ids = context.ids.clone();
1446                        put_one
1447                            .put_terminal(terminal_id, terminal, context)
1448                            .await
1449                            .map(|t| OcpiReply::ok(t).with_ids(ids.clone()))
1450                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1451                    },
1452                )
1453                .patch(
1454                    async move |auth: Auth,
1455                                ids: Ids,
1456                                routing: Routing,
1457                                Path(terminal_id): Path<String>,
1458                                OcpiPatch(patch): OcpiPatch<crate::v2_3_0::payments::Terminal>|
1459                                -> Result<_, OcpiErrorResponse> {
1460                        let context = context_of(auth, ids, routing, &ModuleId::Payments)?;
1461                        let ids = context.ids.clone();
1462                        patch_one
1463                            .patch_terminal(terminal_id, patch, context)
1464                            .await
1465                            .map(|t| OcpiReply::ok(t).with_ids(ids.clone()))
1466                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1467                    },
1468                ),
1469            )
1470            .route(
1471                "/payments/terminals/{terminal_id}/deactivate",
1472                post(
1473                    async move |auth: Auth,
1474                                ids: Ids,
1475                                routing: Routing,
1476                                Path(terminal_id): Path<String>|
1477                                -> Result<_, OcpiErrorResponse> {
1478                        let context = context_of(auth, ids, routing, &ModuleId::Payments)?;
1479                        let ids = context.ids.clone();
1480                        deactivate
1481                            .deactivate_terminal(terminal_id, context)
1482                            .await
1483                            .map(|t| OcpiReply::ok(t).with_ids(ids.clone()))
1484                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1485                    },
1486                ),
1487            )
1488            .route(
1489                "/payments/financial-advice-confirmations",
1490                get(
1491                    async move |auth: Auth,
1492                                ids: Ids,
1493                                routing: Routing,
1494                                PageParams(query): PageParams,
1495                                State(state): State<Arc<OcpiState>>|
1496                                -> Result<_, OcpiErrorResponse> {
1497                        let context = context_of(auth, ids, routing, &ModuleId::Payments)?;
1498                        let ids = context.ids.clone();
1499                        fac_list
1500                            .financial_advice_confirmations(query, context)
1501                            .await
1502                            .map(|page| page_reply(page, &state).with_ids(ids.clone()))
1503                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1504                    },
1505                ),
1506            )
1507            .route(
1508                "/payments/financial-advice-confirmations/{id}",
1509                get(
1510                    async move |auth: Auth,
1511                                ids: Ids,
1512                                routing: Routing,
1513                                Path(id): Path<String>|
1514                                -> Result<_, OcpiErrorResponse> {
1515                        let context = context_of(auth, ids, routing, &ModuleId::Payments)?;
1516                        let ids = context.ids.clone();
1517                        fac_one
1518                            .financial_advice_confirmation(id, context)
1519                            .await
1520                            .map(|f| OcpiReply::ok(f).with_ids(ids.clone()))
1521                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1522                    },
1523                ),
1524            );
1525        self
1526    }
1527
1528    /// Mounts the Payments Receiver interface: the CPO's copy of the PTP's objects.
1529    ///
1530    /// Note the `POST`: unlike every other Receiver interface in OCPI these objects are created
1531    /// with `POST` and their URLs carry no owning party.
1532    #[must_use]
1533    pub fn payments_receiver<H: PaymentsReceiver>(mut self, handler: H) -> Self {
1534        self.check_interface_conflict(&ModuleId::Payments, InterfaceRole::Receiver);
1535        let prefix = self.receiver_prefix();
1536        let handler = Arc::new(handler);
1537        self.mounted.add(ModuleId::Payments, InterfaceRole::Receiver);
1538        let get_terminal = Arc::clone(&handler);
1539        let post_terminal = Arc::clone(&handler);
1540        let get_fac = Arc::clone(&handler);
1541        let post_fac = handler;
1542
1543        self.router = self
1544            .router
1545            .route(
1546                &receiver_path(prefix.as_deref(), "/payments/terminals"),
1547                post(
1548                    async move |auth: Auth,
1549                                ids: Ids,
1550                                routing: Routing,
1551                                OcpiJson(terminal): OcpiJson<crate::v2_3_0::payments::Terminal>|
1552                                -> Result<_, OcpiErrorResponse> {
1553                        let context = context_of(auth, ids, routing, &ModuleId::Payments)?;
1554                        let ids = context.ids.clone();
1555                        post_terminal
1556                            .post_terminal(terminal, context)
1557                            .await
1558                            .map(|t| OcpiReply::created(t).with_ids(ids.clone()))
1559                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1560                    },
1561                ),
1562            )
1563            .route(
1564                &receiver_path(prefix.as_deref(), "/payments/terminals/{terminal_id}"),
1565                get(
1566                    async move |auth: Auth,
1567                                ids: Ids,
1568                                routing: Routing,
1569                                Path(terminal_id): Path<String>|
1570                                -> Result<_, OcpiErrorResponse> {
1571                        let context = context_of(auth, ids, routing, &ModuleId::Payments)?;
1572                        let ids = context.ids.clone();
1573                        get_terminal
1574                            .terminal(terminal_id, context)
1575                            .await
1576                            .map(|t| OcpiReply::ok(t).with_ids(ids.clone()))
1577                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1578                    },
1579                ),
1580            )
1581            .route(
1582                &receiver_path(prefix.as_deref(), "/payments/financial-advice-confirmations"),
1583                post(
1584                    async move |auth: Auth,
1585                                ids: Ids,
1586                                routing: Routing,
1587                                OcpiJson(confirmation): OcpiJson<
1588                        crate::v2_3_0::payments::FinancialAdviceConfirmation,
1589                    >|
1590                                -> Result<_, OcpiErrorResponse> {
1591                        let context = context_of(auth, ids, routing, &ModuleId::Payments)?;
1592                        let ids = context.ids.clone();
1593                        post_fac
1594                            .post_financial_advice_confirmation(confirmation, context)
1595                            .await
1596                            .map(|f| OcpiReply::created(f).with_ids(ids.clone()))
1597                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1598                    },
1599                ),
1600            )
1601            .route(
1602                &receiver_path(prefix.as_deref(), "/payments/financial-advice-confirmations/{id}"),
1603                get(
1604                    async move |auth: Auth,
1605                                ids: Ids,
1606                                routing: Routing,
1607                                Path(id): Path<String>|
1608                                -> Result<_, OcpiErrorResponse> {
1609                        let context = context_of(auth, ids, routing, &ModuleId::Payments)?;
1610                        let ids = context.ids.clone();
1611                        get_fac
1612                            .financial_advice_confirmation(id, context)
1613                            .await
1614                            .map(|f| OcpiReply::ok(f).with_ids(ids.clone()))
1615                            .map_err(|e| OcpiErrorResponse::new(e).with_ids(ids))
1616                    },
1617                ),
1618            );
1619        self
1620    }
1621
1622    /// Finishes the router.
1623    ///
1624    /// The `/versions` and version-details endpoints are added automatically and describe exactly
1625    /// what was mounted.
1626    /// # Panics
1627    ///
1628    /// Panics when the router publishes a version this build cannot translate the canonical model
1629    /// into — today, anything other than 2.3.0 or 2.2.1. Handlers are written against
1630    /// [`v2_3_0`](crate::v2_3_0), so a router that advertised OCPI 2.1.1 would answer a partner
1631    /// with objects in a shape that version does not have; better a message at start-up than an
1632    /// invoice built from it.
1633    pub fn build(self) -> Router {
1634        assert!(
1635            crate::convert::wire::bridgeable(&crate::CANONICAL_VERSION, &self.version),
1636            "this build cannot serve OCPI {}: the handler traits speak the canonical {} model and \
1637             there are no conversions between the two, so every response would be in a shape {} \
1638             does not define. Mount a version this build can translate, or serve the canonical \
1639             one.",
1640            self.version,
1641            crate::CANONICAL_VERSION,
1642            self.version,
1643        );
1644        let bridging = self.version != crate::CANONICAL_VERSION;
1645        let state = Arc::new(OcpiState {
1646            tokens: self.tokens,
1647            config: self.config,
1648            base_url: self.base_url,
1649            mounted: self.mounted,
1650            version: self.version,
1651        });
1652        let details_state = Arc::clone(&state);
1653        let versions_state = Arc::clone(&state);
1654
1655        let mut router = self
1656            .router
1657            .route(
1658                "/",
1659                get(async move |auth: Auth, ids: Ids| -> Result<_, OcpiErrorResponse> {
1660                    let context = context_of(auth, ids, Routing(None), &ModuleId::Versions)?;
1661                    let ids = context.ids.clone();
1662                    Ok(OcpiReply::ok(details_state.version_details()).with_ids(ids))
1663                }),
1664            )
1665            .route(
1666                "/versions",
1667                get(async move |auth: Auth, ids: Ids| -> Result<_, OcpiErrorResponse> {
1668                    let context = context_of(auth, ids, Routing(None), &ModuleId::Versions)?;
1669                    let ids = context.ids.clone();
1670                    let versions = vec![crate::v2_3_0::versions::Version::new(
1671                        versions_state.version.clone(),
1672                        versions_state.base_url.clone(),
1673                    )];
1674                    Ok(OcpiReply::ok(versions).with_ids(ids))
1675                }),
1676            )
1677            .with_state(Arc::clone(&state));
1678        // Only when it has work to do: on a canonical router nothing is buffered or re-encoded.
1679        if bridging {
1680            router = router.layer(axum::middleware::from_fn_with_state(state, super::bridge::translate));
1681        }
1682        router
1683    }
1684}
1685
1686/// The path a Receiver route is mounted at, applying [`ServerConfig::receiver_path_prefix`].
1687fn receiver_path(prefix: Option<&str>, suffix: &str) -> String {
1688    match prefix {
1689        Some(prefix) => format!("/{}{suffix}", prefix.trim_matches('/')),
1690        None => suffix.to_owned(),
1691    }
1692}
1693
1694/// The `?duration=` and `?response_url=` parameters of the Charging Profiles Receiver interface.
1695///
1696/// > *NOTE: As it is not common to add a body to a GET request, all parameters are added to the
1697/// > URL.*
1698#[derive(Debug, serde::Deserialize)]
1699struct ActiveProfileQuery {
1700    #[serde(default)]
1701    duration: Option<u64>,
1702    #[serde(default)]
1703    response_url: Option<Url>,
1704}
1705
1706/// Decodes a body that is an object with fields left out, as `POST .../terminals/activate` is.
1707///
1708/// This is not a merge patch — nothing is being merged — so it is decoded as a plain JSON object
1709/// and wrapped, rather than going through the [`OcpiPatch`] extractor and picking up the rule
1710/// that a `PATCH` must carry `last_updated`.
1711fn partial_object<T>(body: &[u8]) -> Result<crate::transport::Patch<T>, OcpiError> {
1712    let value: serde_json::Value =
1713        serde_json::from_slice(body).map_err(|e| OcpiError::MalformedJson(e.to_string()))?;
1714    if !value.is_object() {
1715        return Err(OcpiError::Decode { path: "/".to_owned(), message: "expected a JSON object".to_owned() });
1716    }
1717    Ok(crate::transport::Patch::from_value(value))
1718}
1719
1720/// The `2001` for a URL parameter the specification marks required.
1721fn missing_query(name: &str) -> OcpiError {
1722    OcpiError::Decode { path: format!("?{name}"), message: format!("the {name} query parameter is required") }
1723}
1724
1725/// Builds the `response_url`s that reach the callbacks this crate's router mounts.
1726///
1727/// The asynchronous halves of the Commands and Charging Profiles modules are the one place where
1728/// OCPI hands the URL shape to the implementation:
1729///
1730/// > *No structure defined. This is open to the eMSP to define, the URL is provided to the
1731/// > Receiver by the Sender.*
1732///
1733/// That freedom is easy to get wrong in a way that only shows up minutes later, when a Charge
1734/// Point answers into a 404. So the Sender-side mounts on [`OcpiRouter`] use fixed shapes, and
1735/// this type builds exactly those — pass the same `base_url` the router is published under and
1736/// the two cannot drift apart.
1737///
1738/// ```
1739/// use ocpi_kit::server::CallbackUrls;
1740/// use ocpi_kit::types::Url;
1741///
1742/// let urls = CallbackUrls::new(Url::new("https://msp.example.com/ocpi/emsp/2.3.0").unwrap());
1743/// assert_eq!(
1744///     urls.command_result("RESERVE_NOW", "1234").as_str(),
1745///     "https://msp.example.com/ocpi/emsp/2.3.0/commands/RESERVE_NOW/1234",
1746/// );
1747/// assert_eq!(
1748///     urls.clear_profile_result("5678").as_str(),
1749///     "https://msp.example.com/ocpi/emsp/2.3.0/chargingprofiles/result/clear/5678",
1750/// );
1751/// ```
1752#[derive(Clone, Debug, PartialEq, Eq)]
1753pub struct CallbackUrls {
1754    base: Url,
1755}
1756
1757impl CallbackUrls {
1758    /// Callback URLs under `base_url`, which must be the URL this party's router is published at.
1759    #[must_use]
1760    pub const fn new(base_url: Url) -> Self {
1761        Self { base: base_url }
1762    }
1763
1764    /// The base URL these callbacks are built from.
1765    #[must_use]
1766    pub const fn base(&self) -> &Url {
1767        &self.base
1768    }
1769
1770    /// `{base}/commands/{command}/{unique_id}` — where a `CommandResult` is expected.
1771    ///
1772    /// Put this in the `response_url` of a
1773    /// [`Command`](crate::v2_3_0::commands::Command); it reaches
1774    /// [`CommandsSender::command_result`] with `unique_id`.
1775    #[must_use]
1776    pub fn command_result(&self, command: &str, unique_id: &str) -> Url {
1777        self.base.join("commands").join(command).join(unique_id)
1778    }
1779
1780    /// `{base}/chargingprofiles/result/active/{unique_id}` — for an `ActiveChargingProfileResult`.
1781    #[must_use]
1782    pub fn active_charging_profile_result(&self, unique_id: &str) -> Url {
1783        self.base.join("chargingprofiles/result/active").join(unique_id)
1784    }
1785
1786    /// `{base}/chargingprofiles/result/set/{unique_id}` — for a `ChargingProfileResult`.
1787    #[must_use]
1788    pub fn charging_profile_result(&self, unique_id: &str) -> Url {
1789        self.base.join("chargingprofiles/result/set").join(unique_id)
1790    }
1791
1792    /// `{base}/chargingprofiles/result/clear/{unique_id}` — for a `ClearProfileResult`.
1793    #[must_use]
1794    pub fn clear_profile_result(&self, unique_id: &str) -> Url {
1795        self.base.join("chargingprofiles/result/clear").join(unique_id)
1796    }
1797}
1798
1799/// The `?type=` query parameter that the Tokens module uses.
1800#[derive(Debug, serde::Deserialize)]
1801struct TokenTypeQuery {
1802    #[serde(rename = "type", default)]
1803    token_type: Option<crate::v2_3_0::tokens::TokenType>,
1804}
1805
1806/// Decodes a request body that the specification marks optional.
1807///
1808/// An empty body, and a body that is an empty JSON object, both mean "not given".
1809fn decode_optional_body<T: serde::de::DeserializeOwned>(body: &[u8]) -> Result<Option<T>, OcpiError> {
1810    let trimmed = body.trim_ascii();
1811    if trimmed.is_empty() || trimmed == b"{}" {
1812        return Ok(None);
1813    }
1814    let mut de = serde_json::Deserializer::from_slice(trimmed);
1815    serde_path_to_error::deserialize(&mut de).map(Some).map_err(|e| OcpiError::Decode {
1816        path: format!("/{}", e.path()),
1817        message: e.into_inner().to_string(),
1818    })
1819}
1820
1821fn status_of(created: super::traits::Created) -> http::StatusCode {
1822    http::StatusCode::from_u16(created.http_status()).unwrap_or(http::StatusCode::OK)
1823}
1824
1825fn page_reply<T: crate::types::Validate>(page: Page<T>, state: &Arc<OcpiState>) -> OcpiReply<Vec<T>> {
1826    let mut headers = http::HeaderMap::new();
1827    let meta = PageMeta { limit: Some(state.config.max_page_limit), ..page.meta };
1828    meta.write_to(&mut headers);
1829    OcpiReply::ok(page.items).with_headers(headers)
1830}
1831
1832/// Builds the handler context, enforcing the `CREDENTIALS_TOKEN_A` scope rule.
1833fn context_of(
1834    Auth(peer): Auth,
1835    Ids(ids): Ids,
1836    Routing(routing): Routing,
1837    module: &ModuleId,
1838) -> Result<RequestContext, OcpiErrorResponse> {
1839    peer.check_scope(module).map_err(|e| OcpiErrorResponse::new(e).with_ids(ids.clone()))?;
1840    Ok(RequestContext { peer, ids, routing })
1841}
1842
1843/// Builds the context for a client-owned-object URL, enforcing the ownership rule as well.
1844fn owned_context(
1845    auth: Auth,
1846    ids: Ids,
1847    routing: Routing,
1848    module: &ModuleId,
1849    country_code: &str,
1850    party_id: &str,
1851) -> Result<(RequestContext, PartyRef), OcpiErrorResponse> {
1852    let request_ids = ids.0.clone();
1853    let context = context_of(auth, ids, routing, module)?;
1854    let Owner(owner) = Owner::from_path(country_code, party_id)
1855        .map_err(|e| OcpiErrorResponse::new(e).with_ids(request_ids.clone()))?;
1856    context.peer.check_ownership(&owner).map_err(|e| OcpiErrorResponse::new(e).with_ids(request_ids))?;
1857    Ok((context, owner))
1858}
1859
1860/// Decodes the body of `POST {commands}/{command}` into the right command object.
1861fn parse_command(name: &str, body: &[u8]) -> Result<crate::v2_3_0::commands::Command, OcpiError> {
1862    use crate::v2_3_0::commands::{
1863        CancelReservation, Command, CommandType, ReserveNow, StartSession, StopSession, UnlockConnector,
1864    };
1865
1866    fn decode<T: serde::de::DeserializeOwned>(body: &[u8]) -> Result<T, OcpiError> {
1867        let mut de = serde_json::Deserializer::from_slice(body);
1868        serde_path_to_error::deserialize(&mut de).map_err(|e| OcpiError::Decode {
1869            path: format!("/{}", e.path()),
1870            message: e.into_inner().to_string(),
1871        })
1872    }
1873
1874    match CommandType::from(name) {
1875        CommandType::CancelReservation => Ok(Command::CancelReservation(decode::<CancelReservation>(body)?)),
1876        CommandType::ReserveNow => Ok(Command::ReserveNow(Box::new(decode::<ReserveNow>(body)?))),
1877        CommandType::StartSession => Ok(Command::StartSession(Box::new(decode::<StartSession>(body)?))),
1878        CommandType::StopSession => Ok(Command::StopSession(decode::<StopSession>(body)?)),
1879        CommandType::UnlockConnector => Ok(Command::UnlockConnector(decode::<UnlockConnector>(body)?)),
1880        other => Err(OcpiError::NotFound(format!("no such command: {other}"))),
1881    }
1882}
1883
1884#[cfg(test)]
1885mod tests {
1886    use super::*;
1887
1888    #[test]
1889    fn commands_are_decoded_by_the_name_in_the_url() {
1890        let body = br#"{"response_url":"https://msp.example.com/cb/1","session_id":"101"}"#;
1891        let command = parse_command("STOP_SESSION", body).unwrap();
1892        assert_eq!(command.command_type(), crate::v2_3_0::commands::CommandType::StopSession);
1893        assert!(parse_command("nltnm-CUSTOM", body).is_err());
1894        // A body that is not the command it claims to be reports where it went wrong.
1895        let err = parse_command("STOP_SESSION", b"{}").unwrap_err();
1896        assert_eq!(err.status_code(), crate::transport::StatusCode::INVALID_PARAMETERS);
1897    }
1898
1899    fn router(config: ServerConfig) -> OcpiRouter {
1900        OcpiRouter::new(
1901            VersionNumber::V2_3_0,
1902            Url::new("https://cpo.example.com/ocpi/cpo/2.3.0").expect("a valid URL"),
1903            Arc::new(super::super::InMemoryTokenStore::new()),
1904        )
1905        .with_config(config)
1906    }
1907
1908    /// The ambiguity does not care which interface was mounted first, so neither may the check.
1909    ///
1910    /// It fired from the Receiver mounts only, so mounting Receiver-then-Sender fell through to
1911    /// whatever `axum` says about a route conflict — a different message, at a different moment,
1912    /// about a different thing.
1913    #[test]
1914    fn an_ambiguous_pair_is_refused_in_both_mount_orders() {
1915        for module in [ModuleId::Locations, ModuleId::ChargingProfiles, ModuleId::Payments] {
1916            for (first, second) in [
1917                (InterfaceRole::Sender, InterfaceRole::Receiver),
1918                (InterfaceRole::Receiver, InterfaceRole::Sender),
1919            ] {
1920                let mut r = router(ServerConfig::default().one_router_per_role());
1921                r.mounted.add(module.clone(), first);
1922                let refused = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1923                    r.check_interface_conflict(&module, second);
1924                }));
1925                assert!(refused.is_err(), "{module} mounted {first} then {second} must be refused");
1926            }
1927        }
1928    }
1929
1930    #[test]
1931    fn a_receiver_path_prefix_is_what_makes_the_pair_servable() {
1932        let mut r = router(ServerConfig::default());
1933        r.mounted.add(ModuleId::Locations, InterfaceRole::Sender);
1934        // The default prefix publishes the Receiver one segment deeper, so there is no ambiguity.
1935        r.check_interface_conflict(&ModuleId::Locations, InterfaceRole::Receiver);
1936
1937        // And a module whose two interfaces have different URL shapes never had the problem.
1938        let mut r = router(ServerConfig::default().one_router_per_role());
1939        r.mounted.add(ModuleId::Tokens, InterfaceRole::Sender);
1940        r.check_interface_conflict(&ModuleId::Tokens, InterfaceRole::Receiver);
1941    }
1942
1943    #[test]
1944    fn every_documented_alternative_is_reachable_from_outside_the_crate() {
1945        // `ServerConfig` is `#[non_exhaustive]`, so a struct literal is not an option for a user;
1946        // the builders are the whole API and each one must exist.
1947        let config = ServerConfig::default()
1948            .with_quirks(Quirks::default())
1949            .with_max_page_limit(25)
1950            .with_receiver_path_prefix("emsp");
1951        assert_eq!(config.max_page_limit, 25);
1952        assert_eq!(config.receiver_path_prefix.as_deref(), Some("emsp"));
1953        assert_eq!(ServerConfig::default().one_router_per_role().receiver_path_prefix, None);
1954    }
1955}