1use crate::v2_2_1 as old;
12use crate::v2_3_0 as new;
13
14use super::{Converted, Downgrade, Lossy, Upgrade};
15
16pub const IMPLIED_VAT_NAME: &str = "VAT";
25
26impl Upgrade<new::types::Price> for old::types::Price {
27 fn upgrade(self) -> Converted<new::types::Price> {
33 let mut taxes = Vec::new();
34 if let Some(incl) = self.incl_vat {
35 let amount = incl - self.excl_vat;
36 taxes.push(new::types::TaxAmount {
37 name: crate::types::OcpiText::new_lenient(IMPLIED_VAT_NAME),
38 account_number: None,
39 percentage: None,
40 amount,
41 extensions: crate::types::Extensions::new(),
42 });
43 }
44 Converted::lossless(new::types::Price {
45 before_taxes: self.excl_vat,
46 taxes,
47 extensions: self.extensions,
48 })
49 }
50}
51
52impl Downgrade<old::types::Price> for new::types::Price {
53 fn downgrade(self) -> Converted<old::types::Price> {
59 let mut lossy = Lossy::none();
60 let incl_vat = if self.taxes.is_empty() { None } else { Some(self.after_taxes()) };
61 for (i, tax) in self.taxes.iter().enumerate() {
62 let carries_detail = tax.percentage.is_some() || tax.account_number.is_some();
63 if self.taxes.len() > 1 || tax.name.as_str() != IMPLIED_VAT_NAME || carries_detail {
64 lossy.record(
65 format!("/taxes/{i}"),
66 format!(
67 "the tax {:?} was folded into `incl_vat`; OCPI 2.2.1 has no way to name \
68 or itemise a tax",
69 tax.name.as_str()
70 ),
71 );
72 }
73 }
74 Converted::new(
75 old::types::Price { excl_vat: self.before_taxes, incl_vat, extensions: self.extensions },
76 lossy,
77 )
78 }
79}
80
81impl Upgrade<new::types::Role> for old::types::Role {
86 fn upgrade(self) -> Converted<new::types::Role> {
91 use new::types::Role as N;
92 use old::types::Role as O;
93 let mut lossy = Lossy::none();
94 let value = match self {
95 O::Cpo => N::Cpo,
96 O::Emsp => N::Emsp,
97 O::Nap => N::Nap,
98 O::Nsp => N::Nsp,
99 O::Other => N::Other,
100 O::Scsp => N::Scsp,
101 O::Hub => {
102 lossy.record(
103 "",
104 "the HUB role was removed in OCPI 2.3.0 and became OTHER; a hub is identified \
105 by `Credentials.hub_party_id` there",
106 );
107 N::Other
108 }
109 };
110 Converted::new(value, lossy)
111 }
112}
113
114impl Downgrade<old::types::Role> for new::types::Role {
115 fn downgrade(self) -> Converted<old::types::Role> {
117 use new::types::Role as N;
118 use old::types::Role as O;
119 Converted::lossless(match self {
120 N::Cpo => O::Cpo,
121 N::Emsp => O::Emsp,
122 N::Nap => O::Nap,
123 N::Nsp => O::Nsp,
124 N::Other => O::Other,
125 N::Scsp => O::Scsp,
126 })
127 }
128}
129
130macro_rules! convert_by_wire_value {
139 ($($old:path => $new:path),* $(,)?) => {$(
140 impl Upgrade<$new> for $old {
141 fn upgrade(self) -> Converted<$new> {
142 Converted::lossless(<$new>::from(self.as_str()))
143 }
144 }
145 impl Downgrade<$old> for $new {
146 fn downgrade(self) -> Converted<$old> {
147 Converted::lossless(<$old>::from(self.as_str()))
148 }
149 }
150 )*};
151}
152
153convert_by_wire_value! {
154 old::locations::ConnectorType => new::locations::ConnectorType,
155 old::locations::ParkingRestriction => new::locations::ParkingRestriction,
156 old::tokens::TokenType => new::tokens::TokenType,
157}
158
159impl Upgrade<new::locations::PublishTokenType> for old::locations::PublishTokenType {
164 fn upgrade(self) -> Converted<new::locations::PublishTokenType> {
165 let token_type = self.token_type.map(|t| t.upgrade().value);
166 Converted::lossless(new::locations::PublishTokenType {
167 uid: self.uid,
168 token_type,
169 visual_number: self.visual_number,
170 issuer: self.issuer,
171 group_id: self.group_id,
172 extensions: self.extensions,
173 })
174 }
175}
176
177impl Downgrade<old::locations::PublishTokenType> for new::locations::PublishTokenType {
178 fn downgrade(self) -> Converted<old::locations::PublishTokenType> {
179 let token_type = self.token_type.map(|t| t.downgrade().value);
180 Converted::lossless(old::locations::PublishTokenType {
181 uid: self.uid,
182 token_type,
183 visual_number: self.visual_number,
184 issuer: self.issuer,
185 group_id: self.group_id,
186 extensions: self.extensions,
187 })
188 }
189}
190
191impl Upgrade<new::locations::Connector> for old::locations::Connector {
192 fn upgrade(self) -> Converted<new::locations::Connector> {
195 Converted::lossless(new::locations::Connector {
196 id: self.id,
197 standard: self.standard.upgrade().value,
198 format: self.format,
199 power_type: self.power_type,
200 max_voltage: self.max_voltage,
201 max_amperage: self.max_amperage,
202 max_electric_power: self.max_electric_power,
203 tariff_ids: self.tariff_ids,
204 terms_and_conditions: self.terms_and_conditions,
205 capabilities: Vec::new(),
206 last_updated: self.last_updated,
207 extensions: self.extensions,
208 })
209 }
210}
211
212impl Downgrade<old::locations::Connector> for new::locations::Connector {
213 fn downgrade(self) -> Converted<old::locations::Connector> {
214 let mut lossy = Lossy::none();
215 if !self.capabilities.is_empty() {
216 lossy.record(
217 "/capabilities",
218 "OCPI 2.2.1 has no Connector.capabilities, so ISO 15118 Plug & Charge support \
219 cannot be expressed",
220 );
221 }
222 Converted::new(
223 old::locations::Connector {
224 id: self.id,
225 standard: self.standard.downgrade().value,
226 format: self.format,
227 power_type: self.power_type,
228 max_voltage: self.max_voltage,
229 max_amperage: self.max_amperage,
230 max_electric_power: self.max_electric_power,
231 tariff_ids: self.tariff_ids,
232 terms_and_conditions: self.terms_and_conditions,
233 last_updated: self.last_updated,
234 extensions: self.extensions,
235 },
236 lossy,
237 )
238 }
239}
240
241impl Upgrade<new::locations::Evse> for old::locations::Evse {
242 fn upgrade(self) -> Converted<new::locations::Evse> {
243 let mut lossy = Lossy::none();
244 let mut connectors = Vec::with_capacity(self.connectors.len());
245 for (i, connector) in self.connectors.into_iter().enumerate() {
246 let converted = connector.upgrade();
247 lossy.absorb(&format!("/connectors/{i}"), converted.lossy);
248 connectors.push(converted.value);
249 }
250 Converted::new(
251 new::locations::Evse {
252 uid: self.uid,
253 evse_id: self.evse_id,
254 status: self.status,
255 status_schedule: self.status_schedule,
256 capabilities: self.capabilities,
257 connectors,
258 floor_level: self.floor_level,
259 coordinates: self.coordinates,
260 physical_reference: self.physical_reference,
261 directions: self.directions,
262 parking_restrictions: self
263 .parking_restrictions
264 .into_iter()
265 .map(|p| p.upgrade().value)
266 .collect(),
267 parking: Vec::new(),
268 images: self.images,
269 accepted_service_providers: Vec::new(),
270 last_updated: self.last_updated,
271 extensions: self.extensions,
272 },
273 lossy,
274 )
275 }
276}
277
278impl Downgrade<old::locations::Evse> for new::locations::Evse {
279 fn downgrade(self) -> Converted<old::locations::Evse> {
280 let mut lossy = Lossy::none();
281 if !self.parking.is_empty() {
282 lossy.record(
283 "/parking",
284 format!(
285 "{} EVSEParking reference(s) dropped: OCPI 2.2.1 has no Parking object, which \
286 is what EU AFIR reporting to a National Access Point needs",
287 self.parking.len()
288 ),
289 );
290 }
291 if !self.accepted_service_providers.is_empty() {
292 lossy.record(
293 "/accepted_service_providers",
294 "OCPI 2.2.1 has no EVSE.accepted_service_providers; the list of eMSPs accepted at \
295 this EVSE cannot be expressed",
296 );
297 }
298 let mut connectors = Vec::with_capacity(self.connectors.len());
299 for (i, connector) in self.connectors.into_iter().enumerate() {
300 let converted = connector.downgrade();
301 lossy.absorb(&format!("/connectors/{i}"), converted.lossy);
302 connectors.push(converted.value);
303 }
304 Converted::new(
305 old::locations::Evse {
306 uid: self.uid,
307 evse_id: self.evse_id,
308 status: self.status,
309 status_schedule: self.status_schedule,
310 capabilities: self.capabilities,
311 connectors,
312 floor_level: self.floor_level,
313 coordinates: self.coordinates,
314 physical_reference: self.physical_reference,
315 directions: self.directions,
316 parking_restrictions: self
317 .parking_restrictions
318 .into_iter()
319 .map(|p| p.downgrade().value)
320 .collect(),
321 images: self.images,
322 last_updated: self.last_updated,
323 extensions: self.extensions,
324 },
325 lossy,
326 )
327 }
328}
329
330impl Upgrade<new::locations::Location> for old::locations::Location {
331 fn upgrade(self) -> Converted<new::locations::Location> {
332 let mut lossy = Lossy::none();
333 let mut evses = Vec::with_capacity(self.evses.len());
334 for (i, evse) in self.evses.into_iter().enumerate() {
335 let converted = evse.upgrade();
336 lossy.absorb(&format!("/evses/{i}"), converted.lossy);
337 evses.push(converted.value);
338 }
339 let mut publish_allowed_to = Vec::with_capacity(self.publish_allowed_to.len());
340 for token in self.publish_allowed_to {
341 publish_allowed_to.push(token.upgrade().value);
342 }
343 Converted::new(
344 new::locations::Location {
345 country_code: self.country_code,
346 party_id: self.party_id,
347 id: self.id,
348 publish: self.publish,
349 publish_allowed_to,
350 name: self.name,
351 address: self.address,
352 city: self.city,
353 postal_code: self.postal_code,
354 state: self.state,
355 country: self.country,
356 coordinates: self.coordinates,
357 related_locations: self.related_locations,
358 parking_type: self.parking_type,
359 evses,
360 parking_places: Vec::new(),
361 directions: self.directions,
362 operator: self.operator,
363 suboperator: self.suboperator,
364 owner: self.owner,
365 facilities: self.facilities,
366 time_zone: self.time_zone,
367 opening_times: self.opening_times,
368 charging_when_closed: self.charging_when_closed,
369 images: self.images,
370 energy_mix: self.energy_mix,
371 help_phone: None,
372 last_updated: self.last_updated,
373 extensions: self.extensions,
374 },
375 lossy,
376 )
377 }
378}
379
380impl Downgrade<old::locations::Location> for new::locations::Location {
381 fn downgrade(self) -> Converted<old::locations::Location> {
382 let mut lossy = Lossy::none();
383 if !self.parking_places.is_empty() {
384 lossy.record(
385 "/parking_places",
386 format!(
387 "{} Parking object(s) dropped: OCPI 2.2.1 has no such object",
388 self.parking_places.len()
389 ),
390 );
391 }
392 if self.help_phone.is_some() {
393 lossy.record("/help_phone", "OCPI 2.2.1 has no Location.help_phone");
394 }
395 let mut evses = Vec::with_capacity(self.evses.len());
396 for (i, evse) in self.evses.into_iter().enumerate() {
397 let converted = evse.downgrade();
398 lossy.absorb(&format!("/evses/{i}"), converted.lossy);
399 evses.push(converted.value);
400 }
401 let publish_allowed_to = self.publish_allowed_to.into_iter().map(|t| t.downgrade().value).collect();
402 Converted::new(
403 old::locations::Location {
404 country_code: self.country_code,
405 party_id: self.party_id,
406 id: self.id,
407 publish: self.publish,
408 publish_allowed_to,
409 name: self.name,
410 address: self.address,
411 city: self.city,
412 postal_code: self.postal_code,
413 state: self.state,
414 country: self.country,
415 coordinates: self.coordinates,
416 related_locations: self.related_locations,
417 parking_type: self.parking_type,
418 evses,
419 directions: self.directions,
420 operator: self.operator,
421 suboperator: self.suboperator,
422 owner: self.owner,
423 facilities: self.facilities,
424 time_zone: self.time_zone,
425 opening_times: self.opening_times,
426 charging_when_closed: self.charging_when_closed,
427 images: self.images,
428 energy_mix: self.energy_mix,
429 last_updated: self.last_updated,
430 extensions: self.extensions,
431 },
432 lossy,
433 )
434 }
435}
436
437impl Upgrade<new::tariffs::Tariff> for old::tariffs::Tariff {
442 fn upgrade(self) -> Converted<new::tariffs::Tariff> {
450 let to_limit = |p: old::types::Price| new::tariffs::PriceLimit {
451 before_taxes: p.excl_vat,
452 after_taxes: p.incl_vat,
453 extensions: p.extensions,
454 };
455 Converted::lossless(new::tariffs::Tariff {
456 country_code: self.country_code,
457 party_id: self.party_id,
458 id: self.id,
459 currency: self.currency,
460 tariff_type: self.tariff_type,
461 tariff_alt_text: self.tariff_alt_text,
462 tariff_alt_url: self.tariff_alt_url,
463 min_price: self.min_price.map(to_limit),
464 max_price: self.max_price.map(to_limit),
465 preauthorize_amount: None,
466 elements: self.elements,
467 tax_included: new::tariffs::TaxIncluded::No,
468 start_date_time: self.start_date_time,
469 end_date_time: self.end_date_time,
470 energy_mix: self.energy_mix,
471 last_updated: self.last_updated,
472 extensions: self.extensions,
473 })
474 }
475}
476
477impl Downgrade<old::tariffs::Tariff> for new::tariffs::Tariff {
478 fn downgrade(self) -> Converted<old::tariffs::Tariff> {
482 let mut lossy = Lossy::none();
483 match self.tax_included {
484 new::tariffs::TaxIncluded::No => {}
485 new::tariffs::TaxIncluded::Yes => lossy.record(
486 "/tax_included",
487 "this Tariff's prices include tax, but a 2.2.1 `PriceComponent.price` is defined \
488 as excluding VAT; the receiving party will read the same numbers as pre-tax \
489 amounts",
490 ),
491 new::tariffs::TaxIncluded::NotApplicable => lossy.record(
492 "/tax_included",
493 "N/A (no taxes are applicable) cannot be expressed in OCPI 2.2.1, which always \
494 treats prices as excluding VAT",
495 ),
496 }
497 if self.preauthorize_amount.is_some() {
498 lossy.record(
499 "/preauthorize_amount",
500 "OCPI 2.2.1 has no Payments module, so the preauthorization amount is dropped",
501 );
502 }
503 let mut to_price = |p: new::tariffs::PriceLimit, field: &str| {
504 if p.after_taxes.is_none() {
505 lossy.record(
506 format!("/{field}"),
507 "the pre-tax limit is kept as `excl_vat`; OCPI 2.2.1 has no separate \
508 after-tax bound",
509 );
510 }
511 old::types::Price { excl_vat: p.before_taxes, incl_vat: p.after_taxes, extensions: p.extensions }
512 };
513 let min_price = self.min_price.map(|p| to_price(p, "min_price"));
514 let max_price = self.max_price.map(|p| to_price(p, "max_price"));
515 Converted::new(
516 old::tariffs::Tariff {
517 country_code: self.country_code,
518 party_id: self.party_id,
519 id: self.id,
520 currency: self.currency,
521 tariff_type: self.tariff_type,
522 tariff_alt_text: self.tariff_alt_text,
523 tariff_alt_url: self.tariff_alt_url,
524 min_price,
525 max_price,
526 elements: self.elements,
527 start_date_time: self.start_date_time,
528 end_date_time: self.end_date_time,
529 energy_mix: self.energy_mix,
530 last_updated: self.last_updated,
531 extensions: self.extensions,
532 },
533 lossy,
534 )
535 }
536}
537
538impl Upgrade<new::tokens::Token> for old::tokens::Token {
543 fn upgrade(self) -> Converted<new::tokens::Token> {
544 Converted::lossless(new::tokens::Token {
545 country_code: self.country_code,
546 party_id: self.party_id,
547 uid: self.uid,
548 token_type: self.token_type.upgrade().value,
549 contract_id: self.contract_id,
550 visual_number: self.visual_number,
551 issuer: self.issuer,
552 group_id: self.group_id,
553 valid: self.valid,
554 whitelist: self.whitelist,
555 language: self.language,
556 default_profile_type: self.default_profile_type,
557 energy_contract: self.energy_contract,
558 last_updated: self.last_updated,
559 extensions: self.extensions,
560 })
561 }
562}
563
564impl Downgrade<old::tokens::Token> for new::tokens::Token {
565 fn downgrade(self) -> Converted<old::tokens::Token> {
566 Converted::lossless(old::tokens::Token {
567 country_code: self.country_code,
568 party_id: self.party_id,
569 uid: self.uid,
570 token_type: self.token_type.downgrade().value,
571 contract_id: self.contract_id,
572 visual_number: self.visual_number,
573 issuer: self.issuer,
574 group_id: self.group_id,
575 valid: self.valid,
576 whitelist: self.whitelist,
577 language: self.language,
578 default_profile_type: self.default_profile_type,
579 energy_contract: self.energy_contract,
580 last_updated: self.last_updated,
581 extensions: self.extensions,
582 })
583 }
584}
585
586impl Upgrade<new::tokens::AuthorizationInfo> for old::tokens::AuthorizationInfo {
587 fn upgrade(self) -> Converted<new::tokens::AuthorizationInfo> {
588 Converted::lossless(new::tokens::AuthorizationInfo {
589 allowed: self.allowed,
590 token: self.token.upgrade().value,
591 location: self.location,
592 authorization_reference: self.authorization_reference,
593 info: self.info,
594 extensions: self.extensions,
595 })
596 }
597}
598
599impl Downgrade<old::tokens::AuthorizationInfo> for new::tokens::AuthorizationInfo {
600 fn downgrade(self) -> Converted<old::tokens::AuthorizationInfo> {
601 Converted::lossless(old::tokens::AuthorizationInfo {
602 allowed: self.allowed,
603 token: self.token.downgrade().value,
604 location: self.location,
605 authorization_reference: self.authorization_reference,
606 info: self.info,
607 extensions: self.extensions,
608 })
609 }
610}
611
612impl Upgrade<new::commands::StartSession> for old::commands::StartSession {
621 fn upgrade(self) -> Converted<new::commands::StartSession> {
622 Converted::lossless(new::commands::StartSession {
623 response_url: self.response_url,
624 token: self.token.upgrade().value,
625 location_id: self.location_id,
626 evse_uid: self.evse_uid,
627 connector_id: self.connector_id,
628 authorization_reference: self.authorization_reference,
629 extensions: self.extensions,
630 })
631 }
632}
633
634impl Downgrade<old::commands::StartSession> for new::commands::StartSession {
635 fn downgrade(self) -> Converted<old::commands::StartSession> {
636 Converted::lossless(old::commands::StartSession {
637 response_url: self.response_url,
638 token: self.token.downgrade().value,
639 location_id: self.location_id,
640 evse_uid: self.evse_uid,
641 connector_id: self.connector_id,
642 authorization_reference: self.authorization_reference,
643 extensions: self.extensions,
644 })
645 }
646}
647
648impl Upgrade<new::commands::ReserveNow> for old::commands::ReserveNow {
649 fn upgrade(self) -> Converted<new::commands::ReserveNow> {
650 Converted::lossless(new::commands::ReserveNow {
651 response_url: self.response_url,
652 token: self.token.upgrade().value,
653 expiry_date: self.expiry_date,
654 reservation_id: self.reservation_id,
655 location_id: self.location_id,
656 evse_uid: self.evse_uid,
657 authorization_reference: self.authorization_reference,
658 extensions: self.extensions,
659 })
660 }
661}
662
663impl Downgrade<old::commands::ReserveNow> for new::commands::ReserveNow {
664 fn downgrade(self) -> Converted<old::commands::ReserveNow> {
665 Converted::lossless(old::commands::ReserveNow {
666 response_url: self.response_url,
667 token: self.token.downgrade().value,
668 expiry_date: self.expiry_date,
669 reservation_id: self.reservation_id,
670 location_id: self.location_id,
671 evse_uid: self.evse_uid,
672 authorization_reference: self.authorization_reference,
673 extensions: self.extensions,
674 })
675 }
676}
677
678impl Upgrade<new::cdrs::CdrToken> for old::cdrs::CdrToken {
683 fn upgrade(self) -> Converted<new::cdrs::CdrToken> {
684 Converted::lossless(new::cdrs::CdrToken {
685 country_code: self.country_code,
686 party_id: self.party_id,
687 uid: self.uid,
688 token_type: self.token_type.upgrade().value,
689 contract_id: self.contract_id,
690 extensions: self.extensions,
691 })
692 }
693}
694
695impl Downgrade<old::cdrs::CdrToken> for new::cdrs::CdrToken {
696 fn downgrade(self) -> Converted<old::cdrs::CdrToken> {
697 Converted::lossless(old::cdrs::CdrToken {
698 country_code: self.country_code,
699 party_id: self.party_id,
700 uid: self.uid,
701 token_type: self.token_type.downgrade().value,
702 contract_id: self.contract_id,
703 extensions: self.extensions,
704 })
705 }
706}
707
708impl Upgrade<new::cdrs::CdrLocation> for old::cdrs::CdrLocation {
709 fn upgrade(self) -> Converted<new::cdrs::CdrLocation> {
710 Converted::lossless(new::cdrs::CdrLocation {
711 id: self.id,
712 name: self.name,
713 address: self.address,
714 city: self.city,
715 postal_code: self.postal_code,
716 state: self.state,
717 country: self.country,
718 coordinates: self.coordinates,
719 evse_uid: self.evse_uid,
720 evse_id: self.evse_id,
721 connector_id: self.connector_id,
722 connector_standard: self.connector_standard.upgrade().value,
723 connector_format: self.connector_format,
724 connector_power_type: self.connector_power_type,
725 extensions: self.extensions,
726 })
727 }
728}
729
730impl Downgrade<old::cdrs::CdrLocation> for new::cdrs::CdrLocation {
731 fn downgrade(self) -> Converted<old::cdrs::CdrLocation> {
732 Converted::lossless(old::cdrs::CdrLocation {
733 id: self.id,
734 name: self.name,
735 address: self.address,
736 city: self.city,
737 postal_code: self.postal_code,
738 state: self.state,
739 country: self.country,
740 coordinates: self.coordinates,
741 evse_uid: self.evse_uid,
742 evse_id: self.evse_id,
743 connector_id: self.connector_id,
744 connector_standard: self.connector_standard.downgrade().value,
745 connector_format: self.connector_format,
746 connector_power_type: self.connector_power_type,
747 extensions: self.extensions,
748 })
749 }
750}
751
752macro_rules! price_field {
754 ($lossy:ident, $field:literal, $value:expr, $dir:ident) => {
755 $value.map(|p| {
756 let converted = Downgrade::<old::types::Price>::$dir(p);
757 $lossy.absorb(concat!("/", $field), converted.lossy);
758 converted.value
759 })
760 };
761}
762
763impl Upgrade<new::cdrs::Cdr> for old::cdrs::Cdr {
764 fn upgrade(self) -> Converted<new::cdrs::Cdr> {
765 let mut lossy = Lossy::none();
766 let mut tariffs = Vec::with_capacity(self.tariffs.len());
767 for (i, tariff) in self.tariffs.into_iter().enumerate() {
768 let converted = tariff.upgrade();
769 lossy.absorb(&format!("/tariffs/{i}"), converted.lossy);
770 tariffs.push(converted.value);
771 }
772 let up = |p: old::types::Price| Upgrade::<new::types::Price>::upgrade(p).value;
773 Converted::new(
774 new::cdrs::Cdr {
775 country_code: self.country_code,
776 party_id: self.party_id,
777 id: self.id,
778 start_date_time: self.start_date_time,
779 end_date_time: self.end_date_time,
780 session_id: self.session_id,
781 cdr_token: self.cdr_token.upgrade().value,
782 auth_method: self.auth_method,
783 authorization_reference: self.authorization_reference,
784 #[cfg(feature = "bookings")]
786 booking_id: None,
787 cdr_location: self.cdr_location.upgrade().value,
788 meter_id: self.meter_id,
789 currency: self.currency,
790 tariffs,
791 charging_periods: self.charging_periods,
792 signed_data: self.signed_data,
793 total_cost: up(self.total_cost),
794 total_fixed_cost: self.total_fixed_cost.map(up),
795 total_energy: self.total_energy,
796 total_energy_cost: self.total_energy_cost.map(up),
797 total_time: self.total_time,
798 total_time_cost: self.total_time_cost.map(up),
799 total_parking_time: self.total_parking_time,
800 total_parking_cost: self.total_parking_cost.map(up),
801 total_reservation_cost: self.total_reservation_cost.map(up),
802 remark: self.remark,
803 invoice_reference_id: self.invoice_reference_id,
804 credit: self.credit,
805 credit_reference_id: self.credit_reference_id,
806 home_charging_compensation: self.home_charging_compensation,
807 last_updated: self.last_updated,
808 extensions: self.extensions,
809 },
810 lossy,
811 )
812 }
813}
814
815impl Downgrade<old::cdrs::Cdr> for new::cdrs::Cdr {
816 fn downgrade(self) -> Converted<old::cdrs::Cdr> {
817 let mut lossy = Lossy::none();
818 let mut tariffs = Vec::with_capacity(self.tariffs.len());
819 for (i, tariff) in self.tariffs.into_iter().enumerate() {
820 let converted = tariff.downgrade();
821 lossy.absorb(&format!("/tariffs/{i}"), converted.lossy);
822 tariffs.push(converted.value);
823 }
824 let total_cost = {
825 let converted = self.total_cost.downgrade();
826 lossy.absorb("/total_cost", converted.lossy);
827 converted.value
828 };
829 let total_fixed_cost = price_field!(lossy, "total_fixed_cost", self.total_fixed_cost, downgrade);
830 let total_energy_cost = price_field!(lossy, "total_energy_cost", self.total_energy_cost, downgrade);
831 let total_time_cost = price_field!(lossy, "total_time_cost", self.total_time_cost, downgrade);
832 let total_parking_cost =
833 price_field!(lossy, "total_parking_cost", self.total_parking_cost, downgrade);
834 let total_reservation_cost =
835 price_field!(lossy, "total_reservation_cost", self.total_reservation_cost, downgrade);
836 #[cfg(feature = "bookings")]
837 if self.booking_id.is_some() {
838 lossy.record(
839 "/booking_id",
840 "OCPI 2.2.1 has no Bookings module, so the booking this CDR settles cannot be \
841 named; the charge is carried across but the reservation it belongs to is not",
842 );
843 }
844 Converted::new(
845 old::cdrs::Cdr {
846 country_code: self.country_code,
847 party_id: self.party_id,
848 id: self.id,
849 start_date_time: self.start_date_time,
850 end_date_time: self.end_date_time,
851 session_id: self.session_id,
852 cdr_token: self.cdr_token.downgrade().value,
853 auth_method: self.auth_method,
854 authorization_reference: self.authorization_reference,
855 cdr_location: self.cdr_location.downgrade().value,
856 meter_id: self.meter_id,
857 currency: self.currency,
858 tariffs,
859 charging_periods: self.charging_periods,
860 signed_data: self.signed_data,
861 total_cost,
862 total_fixed_cost,
863 total_energy: self.total_energy,
864 total_energy_cost,
865 total_time: self.total_time,
866 total_time_cost,
867 total_parking_time: self.total_parking_time,
868 total_parking_cost,
869 total_reservation_cost,
870 remark: self.remark,
871 invoice_reference_id: self.invoice_reference_id,
872 credit: self.credit,
873 credit_reference_id: self.credit_reference_id,
874 home_charging_compensation: self.home_charging_compensation,
875 last_updated: self.last_updated,
876 extensions: self.extensions,
877 },
878 lossy,
879 )
880 }
881}
882
883impl Upgrade<new::sessions::Session> for old::sessions::Session {
884 fn upgrade(self) -> Converted<new::sessions::Session> {
885 Converted::lossless(new::sessions::Session {
886 country_code: self.country_code,
887 party_id: self.party_id,
888 id: self.id,
889 start_date_time: self.start_date_time,
890 end_date_time: self.end_date_time,
891 kwh: self.kwh,
892 cdr_token: self.cdr_token.upgrade().value,
893 auth_method: self.auth_method,
894 authorization_reference: self.authorization_reference,
895 location_id: self.location_id,
896 evse_uid: self.evse_uid,
897 connector_id: self.connector_id,
898 meter_id: self.meter_id,
899 currency: self.currency,
900 charging_periods: self.charging_periods,
901 total_cost: self.total_cost.map(|p| Upgrade::<new::types::Price>::upgrade(p).value),
902 status: self.status,
903 last_updated: self.last_updated,
904 extensions: self.extensions,
905 })
906 }
907}
908
909impl Downgrade<old::sessions::Session> for new::sessions::Session {
910 fn downgrade(self) -> Converted<old::sessions::Session> {
911 let mut lossy = Lossy::none();
912 let total_cost = price_field!(lossy, "total_cost", self.total_cost, downgrade);
913 Converted::new(
914 old::sessions::Session {
915 country_code: self.country_code,
916 party_id: self.party_id,
917 id: self.id,
918 start_date_time: self.start_date_time,
919 end_date_time: self.end_date_time,
920 kwh: self.kwh,
921 cdr_token: self.cdr_token.downgrade().value,
922 auth_method: self.auth_method,
923 authorization_reference: self.authorization_reference,
924 location_id: self.location_id,
925 evse_uid: self.evse_uid,
926 connector_id: self.connector_id,
927 meter_id: self.meter_id,
928 currency: self.currency,
929 charging_periods: self.charging_periods,
930 total_cost,
931 status: self.status,
932 last_updated: self.last_updated,
933 extensions: self.extensions,
934 },
935 lossy,
936 )
937 }
938}
939
940impl Upgrade<new::credentials::Credentials> for old::credentials::Credentials {
945 fn upgrade(self) -> Converted<new::credentials::Credentials> {
951 let mut lossy = Lossy::none();
952 let hub_party_id =
953 self.roles.iter().find(|r| r.role == old::types::Role::Hub).map(|r| r.party().to_hub_party_id());
954 let mut roles = Vec::with_capacity(self.roles.len());
955 for (i, role) in self.roles.into_iter().enumerate() {
956 if role.role == old::types::Role::Hub {
957 continue;
959 }
960 let converted = Upgrade::<new::types::Role>::upgrade(role.role);
961 lossy.absorb(&format!("/roles/{i}/role"), converted.lossy);
962 roles.push(new::credentials::CredentialsRole {
963 role: converted.value,
964 business_details: role.business_details,
965 party_id: role.party_id,
966 country_code: role.country_code,
967 extensions: role.extensions,
968 });
969 }
970 Converted::new(
971 new::credentials::Credentials {
972 token: self.token,
973 url: self.url,
974 hub_party_id,
975 roles,
976 extensions: self.extensions,
977 },
978 lossy,
979 )
980 }
981}
982
983impl Downgrade<old::credentials::Credentials> for new::credentials::Credentials {
984 fn downgrade(self) -> Converted<old::credentials::Credentials> {
989 let mut lossy = Lossy::none();
990 let mut roles: Vec<old::credentials::CredentialsRole> = self
991 .roles
992 .iter()
993 .map(|role| old::credentials::CredentialsRole {
994 role: Downgrade::<old::types::Role>::downgrade(role.role).value,
995 business_details: role.business_details.clone(),
996 party_id: role.party_id.clone(),
997 country_code: role.country_code.clone(),
998 extensions: role.extensions.clone(),
999 })
1000 .collect();
1001
1002 if let Some(hub) = self.hub_party() {
1003 match self.roles.first() {
1004 Some(first) => {
1005 lossy.record(
1006 "/hub_party_id",
1007 format!(
1008 "re-expressed as a 2.2.1 HUB role for {hub}; its business_details were \
1009 copied from {} because OCPI 2.3.0 does not carry them for the hub \
1010 party itself",
1011 first.party()
1012 ),
1013 );
1014 roles.push(old::credentials::CredentialsRole {
1015 role: old::types::Role::Hub,
1016 business_details: first.business_details.clone(),
1017 party_id: hub.party_id,
1018 country_code: hub.country_code,
1019 extensions: crate::types::Extensions::new(),
1020 });
1021 }
1022 None => lossy.record(
1023 "/hub_party_id",
1024 format!("cannot be expressed in OCPI 2.2.1: no role to model {hub} on"),
1025 ),
1026 }
1027 }
1028
1029 Converted::new(
1030 old::credentials::Credentials {
1031 token: self.token,
1032 url: self.url,
1033 roles,
1034 extensions: self.extensions,
1035 },
1036 lossy,
1037 )
1038 }
1039}
1040
1041impl Upgrade<new::hub_client_info::ClientInfo> for old::hub_client_info::ClientInfo {
1046 fn upgrade(self) -> Converted<new::hub_client_info::ClientInfo> {
1047 let converted = Upgrade::<new::types::Role>::upgrade(self.role);
1048 let mut lossy = Lossy::none();
1049 lossy.absorb("/role", converted.lossy);
1050 Converted::new(
1051 new::hub_client_info::ClientInfo {
1052 party_id: self.party_id,
1053 country_code: self.country_code,
1054 role: converted.value,
1055 status: self.status,
1056 last_updated: self.last_updated,
1057 extensions: self.extensions,
1058 },
1059 lossy,
1060 )
1061 }
1062}
1063
1064impl Downgrade<old::hub_client_info::ClientInfo> for new::hub_client_info::ClientInfo {
1065 fn downgrade(self) -> Converted<old::hub_client_info::ClientInfo> {
1066 Converted::lossless(old::hub_client_info::ClientInfo {
1067 party_id: self.party_id,
1068 country_code: self.country_code,
1069 role: Downgrade::<old::types::Role>::downgrade(self.role).value,
1070 status: self.status,
1071 last_updated: self.last_updated,
1072 extensions: self.extensions,
1073 })
1074 }
1075}
1076
1077#[cfg(test)]
1078mod tests {
1079 use super::*;
1080 use crate::types::{DateTime, Validate};
1081
1082 fn dt() -> DateTime {
1083 "2024-01-01T00:00:00Z".parse().unwrap()
1084 }
1085
1086 #[test]
1087 fn price_survives_a_round_trip_when_it_has_at_most_one_vat_line() {
1088 let original = old::types::Price::with_vat("5.00".parse().unwrap(), "5.50".parse().unwrap());
1089 let up: new::types::Price = original.clone().upgrade().expect_lossless();
1090 assert_eq!(up.taxes.len(), 1);
1091 assert_eq!(up.taxes[0].name.as_str(), "VAT");
1092 let back: old::types::Price = up.downgrade().expect_lossless();
1093 assert_eq!(back, original);
1094 }
1095
1096 #[test]
1097 fn several_named_taxes_collapse_and_say_so() {
1098 let mut price = new::types::Price::new("5.00".parse().unwrap());
1099 price.taxes.push(new::types::TaxAmount::new("GST", None, "0.25".parse().unwrap()).unwrap());
1100 price.taxes.push(new::types::TaxAmount::new("QST", None, "0.50".parse().unwrap()).unwrap());
1101 let converted = price.downgrade();
1102 assert_eq!(converted.value.excl_vat.to_string(), "5.00");
1103 assert_eq!(converted.value.incl_vat.unwrap().to_string(), "5.75");
1104 assert_eq!(converted.lossy.len(), 2, "both tax names are reported");
1105 assert!(converted.lossy.as_slice()[0].reason.contains("GST"));
1106 }
1107
1108 #[test]
1109 fn connector_types_added_in_2_3_0_survive_a_downgrade_as_their_wire_value() {
1110 let mcs = new::locations::ConnectorType::Mcs;
1111 let old_type: old::locations::ConnectorType = mcs.clone().downgrade().expect_lossless();
1112 assert!(!old_type.is_known(), "2.2.1 does not define MCS");
1113 assert_eq!(old_type.as_str(), "MCS", "but the wire value is unchanged");
1114 let round_trip: new::locations::ConnectorType = old_type.upgrade().expect_lossless();
1115 assert_eq!(round_trip, mcs);
1116 }
1117
1118 #[test]
1119 fn a_2_2_1_hub_role_becomes_hub_party_id_and_back() {
1120 let business = old::locations::BusinessDetails::builder().name("Example Hub").build();
1121 let credentials = old::credentials::Credentials::builder()
1122 .token("token")
1123 .url(crate::types::Url::new("https://hub.example.com/versions").unwrap())
1124 .roles(vec![
1125 old::credentials::CredentialsRole::builder()
1126 .role(old::types::Role::Cpo)
1127 .business_details(business.clone())
1128 .party_id("TNM")
1129 .country_code("NL")
1130 .build(),
1131 old::credentials::CredentialsRole::builder()
1132 .role(old::types::Role::Hub)
1133 .business_details(business)
1134 .party_id("HUB")
1135 .country_code("NL")
1136 .build(),
1137 ])
1138 .build();
1139
1140 let up: new::credentials::Credentials = credentials.upgrade().expect_lossless();
1141 assert_eq!(up.hub_party_id.as_ref().unwrap().as_str(), "NLHUB");
1142 assert_eq!(up.roles.len(), 1, "the HUB role became hub_party_id");
1143 assert!(up.is_routing_platform());
1144
1145 let back = up.downgrade();
1146 assert!(back.value.is_hub());
1147 assert_eq!(back.value.roles.len(), 2);
1148 assert_eq!(back.lossy.len(), 1);
1150 assert_eq!(back.lossy.as_slice()[0].pointer, "/hub_party_id");
1151 }
1152
1153 #[test]
1154 fn a_tax_inclusive_tariff_cannot_be_downgraded_faithfully() {
1155 let tariff = new::tariffs::Tariff::builder()
1156 .country_code("CA")
1157 .party_id("ABC")
1158 .id("1")
1159 .currency("CAD")
1160 .elements(vec![
1161 new::tariffs::TariffElement::builder()
1162 .price_components(vec![new::tariffs::PriceComponent::new(
1163 new::tariffs::TariffDimensionType::Time,
1164 "2.10".parse().unwrap(),
1165 )])
1166 .build(),
1167 ])
1168 .tax_included(new::tariffs::TaxIncluded::Yes)
1169 .last_updated(dt())
1170 .build();
1171 let converted = tariff.downgrade();
1172 assert_eq!(converted.lossy.as_slice()[0].pointer, "/tax_included");
1173 assert!(converted.lossy.as_slice()[0].reason.contains("excluding VAT"));
1174 }
1175
1176 #[test]
1177 fn a_2_2_1_tariff_upgrades_to_tax_excluded() {
1178 let tariff = old::tariffs::Tariff::builder()
1179 .country_code("DE")
1180 .party_id("ALL")
1181 .id("12")
1182 .currency("EUR")
1183 .elements(vec![
1184 old::tariffs::TariffElement::builder()
1185 .price_components(vec![old::tariffs::PriceComponent::new(
1186 old::tariffs::TariffDimensionType::Time,
1187 "2.00".parse().unwrap(),
1188 )])
1189 .build(),
1190 ])
1191 .last_updated(dt())
1192 .build();
1193 let up: new::tariffs::Tariff = tariff.upgrade().expect_lossless();
1194 assert_eq!(up.tax_included, new::tariffs::TaxIncluded::No);
1195 assert!(up.validate().is_ok());
1196 }
1197
1198 #[test]
1199 fn location_fields_added_in_2_3_0_are_reported_when_dropped() {
1200 let mut location = new::locations::Location::builder()
1201 .country_code("NL")
1202 .party_id("TNM")
1203 .id("LOC1")
1204 .publish(true)
1205 .address("Street 1")
1206 .city("Amsterdam")
1207 .country("NLD")
1208 .coordinates(new::locations::GeoLocation::new("52.010000", "4.350000").unwrap())
1209 .time_zone("Europe/Amsterdam")
1210 .help_phone(crate::types::CiString::<25>::new("+31201234567").unwrap())
1211 .last_updated(dt())
1212 .build();
1213 location.parking_places.push(
1214 new::locations::Parking::builder()
1215 .id("P1")
1216 .vehicle_types(vec![new::locations::VehicleType::PersonalVehicle])
1217 .restricted_to_type(false)
1218 .reservation_required(false)
1219 .build(),
1220 );
1221 let converted = location.downgrade();
1222 let pointers: Vec<&str> = converted.lossy.as_slice().iter().map(|l| l.pointer.as_str()).collect();
1223 assert!(pointers.contains(&"/parking_places"), "{pointers:?}");
1224 assert!(pointers.contains(&"/help_phone"), "{pointers:?}");
1225 assert!(converted.value.validate().is_ok(), "the downgraded object still conforms");
1226 }
1227}