use crate::v2_2_1 as old;
use crate::v2_3_0 as new;
use super::{Converted, Downgrade, Lossy, Upgrade};
pub const IMPLIED_VAT_NAME: &str = "VAT";
impl Upgrade<new::types::Price> for old::types::Price {
fn upgrade(self) -> Converted<new::types::Price> {
let mut taxes = Vec::new();
if let Some(incl) = self.incl_vat {
let amount = incl - self.excl_vat;
taxes.push(new::types::TaxAmount {
name: crate::types::OcpiText::new_lenient(IMPLIED_VAT_NAME),
account_number: None,
percentage: None,
amount,
extensions: crate::types::Extensions::new(),
});
}
Converted::lossless(new::types::Price {
before_taxes: self.excl_vat,
taxes,
extensions: self.extensions,
})
}
}
impl Downgrade<old::types::Price> for new::types::Price {
fn downgrade(self) -> Converted<old::types::Price> {
let mut lossy = Lossy::none();
let incl_vat = if self.taxes.is_empty() { None } else { Some(self.after_taxes()) };
for (i, tax) in self.taxes.iter().enumerate() {
let carries_detail = tax.percentage.is_some() || tax.account_number.is_some();
if self.taxes.len() > 1 || tax.name.as_str() != IMPLIED_VAT_NAME || carries_detail {
lossy.record(
format!("/taxes/{i}"),
format!(
"the tax {:?} was folded into `incl_vat`; OCPI 2.2.1 has no way to name \
or itemise a tax",
tax.name.as_str()
),
);
}
}
Converted::new(
old::types::Price { excl_vat: self.before_taxes, incl_vat, extensions: self.extensions },
lossy,
)
}
}
impl Upgrade<new::types::Role> for old::types::Role {
fn upgrade(self) -> Converted<new::types::Role> {
use new::types::Role as N;
use old::types::Role as O;
let mut lossy = Lossy::none();
let value = match self {
O::Cpo => N::Cpo,
O::Emsp => N::Emsp,
O::Nap => N::Nap,
O::Nsp => N::Nsp,
O::Other => N::Other,
O::Scsp => N::Scsp,
O::Hub => {
lossy.record(
"",
"the HUB role was removed in OCPI 2.3.0 and became OTHER; a hub is identified \
by `Credentials.hub_party_id` there",
);
N::Other
}
};
Converted::new(value, lossy)
}
}
impl Downgrade<old::types::Role> for new::types::Role {
fn downgrade(self) -> Converted<old::types::Role> {
use new::types::Role as N;
use old::types::Role as O;
Converted::lossless(match self {
N::Cpo => O::Cpo,
N::Emsp => O::Emsp,
N::Nap => O::Nap,
N::Nsp => O::Nsp,
N::Other => O::Other,
N::Scsp => O::Scsp,
})
}
}
macro_rules! convert_by_wire_value {
($($old:path => $new:path),* $(,)?) => {$(
impl Upgrade<$new> for $old {
fn upgrade(self) -> Converted<$new> {
Converted::lossless(<$new>::from(self.as_str()))
}
}
impl Downgrade<$old> for $new {
fn downgrade(self) -> Converted<$old> {
Converted::lossless(<$old>::from(self.as_str()))
}
}
)*};
}
convert_by_wire_value! {
old::locations::ConnectorType => new::locations::ConnectorType,
old::locations::ParkingRestriction => new::locations::ParkingRestriction,
old::tokens::TokenType => new::tokens::TokenType,
}
impl Upgrade<new::locations::PublishTokenType> for old::locations::PublishTokenType {
fn upgrade(self) -> Converted<new::locations::PublishTokenType> {
let token_type = self.token_type.map(|t| t.upgrade().value);
Converted::lossless(new::locations::PublishTokenType {
uid: self.uid,
token_type,
visual_number: self.visual_number,
issuer: self.issuer,
group_id: self.group_id,
extensions: self.extensions,
})
}
}
impl Downgrade<old::locations::PublishTokenType> for new::locations::PublishTokenType {
fn downgrade(self) -> Converted<old::locations::PublishTokenType> {
let token_type = self.token_type.map(|t| t.downgrade().value);
Converted::lossless(old::locations::PublishTokenType {
uid: self.uid,
token_type,
visual_number: self.visual_number,
issuer: self.issuer,
group_id: self.group_id,
extensions: self.extensions,
})
}
}
impl Upgrade<new::locations::Connector> for old::locations::Connector {
fn upgrade(self) -> Converted<new::locations::Connector> {
Converted::lossless(new::locations::Connector {
id: self.id,
standard: self.standard.upgrade().value,
format: self.format,
power_type: self.power_type,
max_voltage: self.max_voltage,
max_amperage: self.max_amperage,
max_electric_power: self.max_electric_power,
tariff_ids: self.tariff_ids,
terms_and_conditions: self.terms_and_conditions,
capabilities: Vec::new(),
last_updated: self.last_updated,
extensions: self.extensions,
})
}
}
impl Downgrade<old::locations::Connector> for new::locations::Connector {
fn downgrade(self) -> Converted<old::locations::Connector> {
let mut lossy = Lossy::none();
if !self.capabilities.is_empty() {
lossy.record(
"/capabilities",
"OCPI 2.2.1 has no Connector.capabilities, so ISO 15118 Plug & Charge support \
cannot be expressed",
);
}
Converted::new(
old::locations::Connector {
id: self.id,
standard: self.standard.downgrade().value,
format: self.format,
power_type: self.power_type,
max_voltage: self.max_voltage,
max_amperage: self.max_amperage,
max_electric_power: self.max_electric_power,
tariff_ids: self.tariff_ids,
terms_and_conditions: self.terms_and_conditions,
last_updated: self.last_updated,
extensions: self.extensions,
},
lossy,
)
}
}
impl Upgrade<new::locations::Evse> for old::locations::Evse {
fn upgrade(self) -> Converted<new::locations::Evse> {
let mut lossy = Lossy::none();
let mut connectors = Vec::with_capacity(self.connectors.len());
for (i, connector) in self.connectors.into_iter().enumerate() {
let converted = connector.upgrade();
lossy.absorb(&format!("/connectors/{i}"), converted.lossy);
connectors.push(converted.value);
}
Converted::new(
new::locations::Evse {
uid: self.uid,
evse_id: self.evse_id,
status: self.status,
status_schedule: self.status_schedule,
capabilities: self.capabilities,
connectors,
floor_level: self.floor_level,
coordinates: self.coordinates,
physical_reference: self.physical_reference,
directions: self.directions,
parking_restrictions: self
.parking_restrictions
.into_iter()
.map(|p| p.upgrade().value)
.collect(),
parking: Vec::new(),
images: self.images,
accepted_service_providers: Vec::new(),
last_updated: self.last_updated,
extensions: self.extensions,
},
lossy,
)
}
}
impl Downgrade<old::locations::Evse> for new::locations::Evse {
fn downgrade(self) -> Converted<old::locations::Evse> {
let mut lossy = Lossy::none();
if !self.parking.is_empty() {
lossy.record(
"/parking",
format!(
"{} EVSEParking reference(s) dropped: OCPI 2.2.1 has no Parking object, which \
is what EU AFIR reporting to a National Access Point needs",
self.parking.len()
),
);
}
if !self.accepted_service_providers.is_empty() {
lossy.record(
"/accepted_service_providers",
"OCPI 2.2.1 has no EVSE.accepted_service_providers; the list of eMSPs accepted at \
this EVSE cannot be expressed",
);
}
let mut connectors = Vec::with_capacity(self.connectors.len());
for (i, connector) in self.connectors.into_iter().enumerate() {
let converted = connector.downgrade();
lossy.absorb(&format!("/connectors/{i}"), converted.lossy);
connectors.push(converted.value);
}
Converted::new(
old::locations::Evse {
uid: self.uid,
evse_id: self.evse_id,
status: self.status,
status_schedule: self.status_schedule,
capabilities: self.capabilities,
connectors,
floor_level: self.floor_level,
coordinates: self.coordinates,
physical_reference: self.physical_reference,
directions: self.directions,
parking_restrictions: self
.parking_restrictions
.into_iter()
.map(|p| p.downgrade().value)
.collect(),
images: self.images,
last_updated: self.last_updated,
extensions: self.extensions,
},
lossy,
)
}
}
impl Upgrade<new::locations::Location> for old::locations::Location {
fn upgrade(self) -> Converted<new::locations::Location> {
let mut lossy = Lossy::none();
let mut evses = Vec::with_capacity(self.evses.len());
for (i, evse) in self.evses.into_iter().enumerate() {
let converted = evse.upgrade();
lossy.absorb(&format!("/evses/{i}"), converted.lossy);
evses.push(converted.value);
}
let mut publish_allowed_to = Vec::with_capacity(self.publish_allowed_to.len());
for token in self.publish_allowed_to {
publish_allowed_to.push(token.upgrade().value);
}
Converted::new(
new::locations::Location {
country_code: self.country_code,
party_id: self.party_id,
id: self.id,
publish: self.publish,
publish_allowed_to,
name: self.name,
address: self.address,
city: self.city,
postal_code: self.postal_code,
state: self.state,
country: self.country,
coordinates: self.coordinates,
related_locations: self.related_locations,
parking_type: self.parking_type,
evses,
parking_places: Vec::new(),
directions: self.directions,
operator: self.operator,
suboperator: self.suboperator,
owner: self.owner,
facilities: self.facilities,
time_zone: self.time_zone,
opening_times: self.opening_times,
charging_when_closed: self.charging_when_closed,
images: self.images,
energy_mix: self.energy_mix,
help_phone: None,
last_updated: self.last_updated,
extensions: self.extensions,
},
lossy,
)
}
}
impl Downgrade<old::locations::Location> for new::locations::Location {
fn downgrade(self) -> Converted<old::locations::Location> {
let mut lossy = Lossy::none();
if !self.parking_places.is_empty() {
lossy.record(
"/parking_places",
format!(
"{} Parking object(s) dropped: OCPI 2.2.1 has no such object",
self.parking_places.len()
),
);
}
if self.help_phone.is_some() {
lossy.record("/help_phone", "OCPI 2.2.1 has no Location.help_phone");
}
let mut evses = Vec::with_capacity(self.evses.len());
for (i, evse) in self.evses.into_iter().enumerate() {
let converted = evse.downgrade();
lossy.absorb(&format!("/evses/{i}"), converted.lossy);
evses.push(converted.value);
}
let publish_allowed_to = self.publish_allowed_to.into_iter().map(|t| t.downgrade().value).collect();
Converted::new(
old::locations::Location {
country_code: self.country_code,
party_id: self.party_id,
id: self.id,
publish: self.publish,
publish_allowed_to,
name: self.name,
address: self.address,
city: self.city,
postal_code: self.postal_code,
state: self.state,
country: self.country,
coordinates: self.coordinates,
related_locations: self.related_locations,
parking_type: self.parking_type,
evses,
directions: self.directions,
operator: self.operator,
suboperator: self.suboperator,
owner: self.owner,
facilities: self.facilities,
time_zone: self.time_zone,
opening_times: self.opening_times,
charging_when_closed: self.charging_when_closed,
images: self.images,
energy_mix: self.energy_mix,
last_updated: self.last_updated,
extensions: self.extensions,
},
lossy,
)
}
}
impl Upgrade<new::tariffs::Tariff> for old::tariffs::Tariff {
fn upgrade(self) -> Converted<new::tariffs::Tariff> {
let to_limit = |p: old::types::Price| new::tariffs::PriceLimit {
before_taxes: p.excl_vat,
after_taxes: p.incl_vat,
extensions: p.extensions,
};
Converted::lossless(new::tariffs::Tariff {
country_code: self.country_code,
party_id: self.party_id,
id: self.id,
currency: self.currency,
tariff_type: self.tariff_type,
tariff_alt_text: self.tariff_alt_text,
tariff_alt_url: self.tariff_alt_url,
min_price: self.min_price.map(to_limit),
max_price: self.max_price.map(to_limit),
preauthorize_amount: None,
elements: self.elements,
tax_included: new::tariffs::TaxIncluded::No,
start_date_time: self.start_date_time,
end_date_time: self.end_date_time,
energy_mix: self.energy_mix,
last_updated: self.last_updated,
extensions: self.extensions,
})
}
}
impl Downgrade<old::tariffs::Tariff> for new::tariffs::Tariff {
fn downgrade(self) -> Converted<old::tariffs::Tariff> {
let mut lossy = Lossy::none();
match self.tax_included {
new::tariffs::TaxIncluded::No => {}
new::tariffs::TaxIncluded::Yes => lossy.record(
"/tax_included",
"this Tariff's prices include tax, but a 2.2.1 `PriceComponent.price` is defined \
as excluding VAT; the receiving party will read the same numbers as pre-tax \
amounts",
),
new::tariffs::TaxIncluded::NotApplicable => lossy.record(
"/tax_included",
"N/A (no taxes are applicable) cannot be expressed in OCPI 2.2.1, which always \
treats prices as excluding VAT",
),
}
if self.preauthorize_amount.is_some() {
lossy.record(
"/preauthorize_amount",
"OCPI 2.2.1 has no Payments module, so the preauthorization amount is dropped",
);
}
let mut to_price = |p: new::tariffs::PriceLimit, field: &str| {
if p.after_taxes.is_none() {
lossy.record(
format!("/{field}"),
"the pre-tax limit is kept as `excl_vat`; OCPI 2.2.1 has no separate \
after-tax bound",
);
}
old::types::Price { excl_vat: p.before_taxes, incl_vat: p.after_taxes, extensions: p.extensions }
};
let min_price = self.min_price.map(|p| to_price(p, "min_price"));
let max_price = self.max_price.map(|p| to_price(p, "max_price"));
Converted::new(
old::tariffs::Tariff {
country_code: self.country_code,
party_id: self.party_id,
id: self.id,
currency: self.currency,
tariff_type: self.tariff_type,
tariff_alt_text: self.tariff_alt_text,
tariff_alt_url: self.tariff_alt_url,
min_price,
max_price,
elements: self.elements,
start_date_time: self.start_date_time,
end_date_time: self.end_date_time,
energy_mix: self.energy_mix,
last_updated: self.last_updated,
extensions: self.extensions,
},
lossy,
)
}
}
impl Upgrade<new::tokens::Token> for old::tokens::Token {
fn upgrade(self) -> Converted<new::tokens::Token> {
Converted::lossless(new::tokens::Token {
country_code: self.country_code,
party_id: self.party_id,
uid: self.uid,
token_type: self.token_type.upgrade().value,
contract_id: self.contract_id,
visual_number: self.visual_number,
issuer: self.issuer,
group_id: self.group_id,
valid: self.valid,
whitelist: self.whitelist,
language: self.language,
default_profile_type: self.default_profile_type,
energy_contract: self.energy_contract,
last_updated: self.last_updated,
extensions: self.extensions,
})
}
}
impl Downgrade<old::tokens::Token> for new::tokens::Token {
fn downgrade(self) -> Converted<old::tokens::Token> {
Converted::lossless(old::tokens::Token {
country_code: self.country_code,
party_id: self.party_id,
uid: self.uid,
token_type: self.token_type.downgrade().value,
contract_id: self.contract_id,
visual_number: self.visual_number,
issuer: self.issuer,
group_id: self.group_id,
valid: self.valid,
whitelist: self.whitelist,
language: self.language,
default_profile_type: self.default_profile_type,
energy_contract: self.energy_contract,
last_updated: self.last_updated,
extensions: self.extensions,
})
}
}
impl Upgrade<new::tokens::AuthorizationInfo> for old::tokens::AuthorizationInfo {
fn upgrade(self) -> Converted<new::tokens::AuthorizationInfo> {
Converted::lossless(new::tokens::AuthorizationInfo {
allowed: self.allowed,
token: self.token.upgrade().value,
location: self.location,
authorization_reference: self.authorization_reference,
info: self.info,
extensions: self.extensions,
})
}
}
impl Downgrade<old::tokens::AuthorizationInfo> for new::tokens::AuthorizationInfo {
fn downgrade(self) -> Converted<old::tokens::AuthorizationInfo> {
Converted::lossless(old::tokens::AuthorizationInfo {
allowed: self.allowed,
token: self.token.downgrade().value,
location: self.location,
authorization_reference: self.authorization_reference,
info: self.info,
extensions: self.extensions,
})
}
}
impl Upgrade<new::commands::StartSession> for old::commands::StartSession {
fn upgrade(self) -> Converted<new::commands::StartSession> {
Converted::lossless(new::commands::StartSession {
response_url: self.response_url,
token: self.token.upgrade().value,
location_id: self.location_id,
evse_uid: self.evse_uid,
connector_id: self.connector_id,
authorization_reference: self.authorization_reference,
extensions: self.extensions,
})
}
}
impl Downgrade<old::commands::StartSession> for new::commands::StartSession {
fn downgrade(self) -> Converted<old::commands::StartSession> {
Converted::lossless(old::commands::StartSession {
response_url: self.response_url,
token: self.token.downgrade().value,
location_id: self.location_id,
evse_uid: self.evse_uid,
connector_id: self.connector_id,
authorization_reference: self.authorization_reference,
extensions: self.extensions,
})
}
}
impl Upgrade<new::commands::ReserveNow> for old::commands::ReserveNow {
fn upgrade(self) -> Converted<new::commands::ReserveNow> {
Converted::lossless(new::commands::ReserveNow {
response_url: self.response_url,
token: self.token.upgrade().value,
expiry_date: self.expiry_date,
reservation_id: self.reservation_id,
location_id: self.location_id,
evse_uid: self.evse_uid,
authorization_reference: self.authorization_reference,
extensions: self.extensions,
})
}
}
impl Downgrade<old::commands::ReserveNow> for new::commands::ReserveNow {
fn downgrade(self) -> Converted<old::commands::ReserveNow> {
Converted::lossless(old::commands::ReserveNow {
response_url: self.response_url,
token: self.token.downgrade().value,
expiry_date: self.expiry_date,
reservation_id: self.reservation_id,
location_id: self.location_id,
evse_uid: self.evse_uid,
authorization_reference: self.authorization_reference,
extensions: self.extensions,
})
}
}
impl Upgrade<new::cdrs::CdrToken> for old::cdrs::CdrToken {
fn upgrade(self) -> Converted<new::cdrs::CdrToken> {
Converted::lossless(new::cdrs::CdrToken {
country_code: self.country_code,
party_id: self.party_id,
uid: self.uid,
token_type: self.token_type.upgrade().value,
contract_id: self.contract_id,
extensions: self.extensions,
})
}
}
impl Downgrade<old::cdrs::CdrToken> for new::cdrs::CdrToken {
fn downgrade(self) -> Converted<old::cdrs::CdrToken> {
Converted::lossless(old::cdrs::CdrToken {
country_code: self.country_code,
party_id: self.party_id,
uid: self.uid,
token_type: self.token_type.downgrade().value,
contract_id: self.contract_id,
extensions: self.extensions,
})
}
}
impl Upgrade<new::cdrs::CdrLocation> for old::cdrs::CdrLocation {
fn upgrade(self) -> Converted<new::cdrs::CdrLocation> {
Converted::lossless(new::cdrs::CdrLocation {
id: self.id,
name: self.name,
address: self.address,
city: self.city,
postal_code: self.postal_code,
state: self.state,
country: self.country,
coordinates: self.coordinates,
evse_uid: self.evse_uid,
evse_id: self.evse_id,
connector_id: self.connector_id,
connector_standard: self.connector_standard.upgrade().value,
connector_format: self.connector_format,
connector_power_type: self.connector_power_type,
extensions: self.extensions,
})
}
}
impl Downgrade<old::cdrs::CdrLocation> for new::cdrs::CdrLocation {
fn downgrade(self) -> Converted<old::cdrs::CdrLocation> {
Converted::lossless(old::cdrs::CdrLocation {
id: self.id,
name: self.name,
address: self.address,
city: self.city,
postal_code: self.postal_code,
state: self.state,
country: self.country,
coordinates: self.coordinates,
evse_uid: self.evse_uid,
evse_id: self.evse_id,
connector_id: self.connector_id,
connector_standard: self.connector_standard.downgrade().value,
connector_format: self.connector_format,
connector_power_type: self.connector_power_type,
extensions: self.extensions,
})
}
}
macro_rules! price_field {
($lossy:ident, $field:literal, $value:expr, $dir:ident) => {
$value.map(|p| {
let converted = Downgrade::<old::types::Price>::$dir(p);
$lossy.absorb(concat!("/", $field), converted.lossy);
converted.value
})
};
}
impl Upgrade<new::cdrs::Cdr> for old::cdrs::Cdr {
fn upgrade(self) -> Converted<new::cdrs::Cdr> {
let mut lossy = Lossy::none();
let mut tariffs = Vec::with_capacity(self.tariffs.len());
for (i, tariff) in self.tariffs.into_iter().enumerate() {
let converted = tariff.upgrade();
lossy.absorb(&format!("/tariffs/{i}"), converted.lossy);
tariffs.push(converted.value);
}
let up = |p: old::types::Price| Upgrade::<new::types::Price>::upgrade(p).value;
Converted::new(
new::cdrs::Cdr {
country_code: self.country_code,
party_id: self.party_id,
id: self.id,
start_date_time: self.start_date_time,
end_date_time: self.end_date_time,
session_id: self.session_id,
cdr_token: self.cdr_token.upgrade().value,
auth_method: self.auth_method,
authorization_reference: self.authorization_reference,
#[cfg(feature = "bookings")]
booking_id: None,
cdr_location: self.cdr_location.upgrade().value,
meter_id: self.meter_id,
currency: self.currency,
tariffs,
charging_periods: self.charging_periods,
signed_data: self.signed_data,
total_cost: up(self.total_cost),
total_fixed_cost: self.total_fixed_cost.map(up),
total_energy: self.total_energy,
total_energy_cost: self.total_energy_cost.map(up),
total_time: self.total_time,
total_time_cost: self.total_time_cost.map(up),
total_parking_time: self.total_parking_time,
total_parking_cost: self.total_parking_cost.map(up),
total_reservation_cost: self.total_reservation_cost.map(up),
remark: self.remark,
invoice_reference_id: self.invoice_reference_id,
credit: self.credit,
credit_reference_id: self.credit_reference_id,
home_charging_compensation: self.home_charging_compensation,
last_updated: self.last_updated,
extensions: self.extensions,
},
lossy,
)
}
}
impl Downgrade<old::cdrs::Cdr> for new::cdrs::Cdr {
fn downgrade(self) -> Converted<old::cdrs::Cdr> {
let mut lossy = Lossy::none();
let mut tariffs = Vec::with_capacity(self.tariffs.len());
for (i, tariff) in self.tariffs.into_iter().enumerate() {
let converted = tariff.downgrade();
lossy.absorb(&format!("/tariffs/{i}"), converted.lossy);
tariffs.push(converted.value);
}
let total_cost = {
let converted = self.total_cost.downgrade();
lossy.absorb("/total_cost", converted.lossy);
converted.value
};
let total_fixed_cost = price_field!(lossy, "total_fixed_cost", self.total_fixed_cost, downgrade);
let total_energy_cost = price_field!(lossy, "total_energy_cost", self.total_energy_cost, downgrade);
let total_time_cost = price_field!(lossy, "total_time_cost", self.total_time_cost, downgrade);
let total_parking_cost =
price_field!(lossy, "total_parking_cost", self.total_parking_cost, downgrade);
let total_reservation_cost =
price_field!(lossy, "total_reservation_cost", self.total_reservation_cost, downgrade);
#[cfg(feature = "bookings")]
if self.booking_id.is_some() {
lossy.record(
"/booking_id",
"OCPI 2.2.1 has no Bookings module, so the booking this CDR settles cannot be \
named; the charge is carried across but the reservation it belongs to is not",
);
}
Converted::new(
old::cdrs::Cdr {
country_code: self.country_code,
party_id: self.party_id,
id: self.id,
start_date_time: self.start_date_time,
end_date_time: self.end_date_time,
session_id: self.session_id,
cdr_token: self.cdr_token.downgrade().value,
auth_method: self.auth_method,
authorization_reference: self.authorization_reference,
cdr_location: self.cdr_location.downgrade().value,
meter_id: self.meter_id,
currency: self.currency,
tariffs,
charging_periods: self.charging_periods,
signed_data: self.signed_data,
total_cost,
total_fixed_cost,
total_energy: self.total_energy,
total_energy_cost,
total_time: self.total_time,
total_time_cost,
total_parking_time: self.total_parking_time,
total_parking_cost,
total_reservation_cost,
remark: self.remark,
invoice_reference_id: self.invoice_reference_id,
credit: self.credit,
credit_reference_id: self.credit_reference_id,
home_charging_compensation: self.home_charging_compensation,
last_updated: self.last_updated,
extensions: self.extensions,
},
lossy,
)
}
}
impl Upgrade<new::sessions::Session> for old::sessions::Session {
fn upgrade(self) -> Converted<new::sessions::Session> {
Converted::lossless(new::sessions::Session {
country_code: self.country_code,
party_id: self.party_id,
id: self.id,
start_date_time: self.start_date_time,
end_date_time: self.end_date_time,
kwh: self.kwh,
cdr_token: self.cdr_token.upgrade().value,
auth_method: self.auth_method,
authorization_reference: self.authorization_reference,
location_id: self.location_id,
evse_uid: self.evse_uid,
connector_id: self.connector_id,
meter_id: self.meter_id,
currency: self.currency,
charging_periods: self.charging_periods,
total_cost: self.total_cost.map(|p| Upgrade::<new::types::Price>::upgrade(p).value),
status: self.status,
last_updated: self.last_updated,
extensions: self.extensions,
})
}
}
impl Downgrade<old::sessions::Session> for new::sessions::Session {
fn downgrade(self) -> Converted<old::sessions::Session> {
let mut lossy = Lossy::none();
let total_cost = price_field!(lossy, "total_cost", self.total_cost, downgrade);
Converted::new(
old::sessions::Session {
country_code: self.country_code,
party_id: self.party_id,
id: self.id,
start_date_time: self.start_date_time,
end_date_time: self.end_date_time,
kwh: self.kwh,
cdr_token: self.cdr_token.downgrade().value,
auth_method: self.auth_method,
authorization_reference: self.authorization_reference,
location_id: self.location_id,
evse_uid: self.evse_uid,
connector_id: self.connector_id,
meter_id: self.meter_id,
currency: self.currency,
charging_periods: self.charging_periods,
total_cost,
status: self.status,
last_updated: self.last_updated,
extensions: self.extensions,
},
lossy,
)
}
}
impl Upgrade<new::credentials::Credentials> for old::credentials::Credentials {
fn upgrade(self) -> Converted<new::credentials::Credentials> {
let mut lossy = Lossy::none();
let hub_party_id =
self.roles.iter().find(|r| r.role == old::types::Role::Hub).map(|r| r.party().to_hub_party_id());
let mut roles = Vec::with_capacity(self.roles.len());
for (i, role) in self.roles.into_iter().enumerate() {
if role.role == old::types::Role::Hub {
continue;
}
let converted = Upgrade::<new::types::Role>::upgrade(role.role);
lossy.absorb(&format!("/roles/{i}/role"), converted.lossy);
roles.push(new::credentials::CredentialsRole {
role: converted.value,
business_details: role.business_details,
party_id: role.party_id,
country_code: role.country_code,
extensions: role.extensions,
});
}
Converted::new(
new::credentials::Credentials {
token: self.token,
url: self.url,
hub_party_id,
roles,
extensions: self.extensions,
},
lossy,
)
}
}
impl Downgrade<old::credentials::Credentials> for new::credentials::Credentials {
fn downgrade(self) -> Converted<old::credentials::Credentials> {
let mut lossy = Lossy::none();
let mut roles: Vec<old::credentials::CredentialsRole> = self
.roles
.iter()
.map(|role| old::credentials::CredentialsRole {
role: Downgrade::<old::types::Role>::downgrade(role.role).value,
business_details: role.business_details.clone(),
party_id: role.party_id.clone(),
country_code: role.country_code.clone(),
extensions: role.extensions.clone(),
})
.collect();
if let Some(hub) = self.hub_party() {
match self.roles.first() {
Some(first) => {
lossy.record(
"/hub_party_id",
format!(
"re-expressed as a 2.2.1 HUB role for {hub}; its business_details were \
copied from {} because OCPI 2.3.0 does not carry them for the hub \
party itself",
first.party()
),
);
roles.push(old::credentials::CredentialsRole {
role: old::types::Role::Hub,
business_details: first.business_details.clone(),
party_id: hub.party_id,
country_code: hub.country_code,
extensions: crate::types::Extensions::new(),
});
}
None => lossy.record(
"/hub_party_id",
format!("cannot be expressed in OCPI 2.2.1: no role to model {hub} on"),
),
}
}
Converted::new(
old::credentials::Credentials {
token: self.token,
url: self.url,
roles,
extensions: self.extensions,
},
lossy,
)
}
}
impl Upgrade<new::hub_client_info::ClientInfo> for old::hub_client_info::ClientInfo {
fn upgrade(self) -> Converted<new::hub_client_info::ClientInfo> {
let converted = Upgrade::<new::types::Role>::upgrade(self.role);
let mut lossy = Lossy::none();
lossy.absorb("/role", converted.lossy);
Converted::new(
new::hub_client_info::ClientInfo {
party_id: self.party_id,
country_code: self.country_code,
role: converted.value,
status: self.status,
last_updated: self.last_updated,
extensions: self.extensions,
},
lossy,
)
}
}
impl Downgrade<old::hub_client_info::ClientInfo> for new::hub_client_info::ClientInfo {
fn downgrade(self) -> Converted<old::hub_client_info::ClientInfo> {
Converted::lossless(old::hub_client_info::ClientInfo {
party_id: self.party_id,
country_code: self.country_code,
role: Downgrade::<old::types::Role>::downgrade(self.role).value,
status: self.status,
last_updated: self.last_updated,
extensions: self.extensions,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{DateTime, Validate};
fn dt() -> DateTime {
"2024-01-01T00:00:00Z".parse().unwrap()
}
#[test]
fn price_survives_a_round_trip_when_it_has_at_most_one_vat_line() {
let original = old::types::Price::with_vat("5.00".parse().unwrap(), "5.50".parse().unwrap());
let up: new::types::Price = original.clone().upgrade().expect_lossless();
assert_eq!(up.taxes.len(), 1);
assert_eq!(up.taxes[0].name.as_str(), "VAT");
let back: old::types::Price = up.downgrade().expect_lossless();
assert_eq!(back, original);
}
#[test]
fn several_named_taxes_collapse_and_say_so() {
let mut price = new::types::Price::new("5.00".parse().unwrap());
price.taxes.push(new::types::TaxAmount::new("GST", None, "0.25".parse().unwrap()).unwrap());
price.taxes.push(new::types::TaxAmount::new("QST", None, "0.50".parse().unwrap()).unwrap());
let converted = price.downgrade();
assert_eq!(converted.value.excl_vat.to_string(), "5.00");
assert_eq!(converted.value.incl_vat.unwrap().to_string(), "5.75");
assert_eq!(converted.lossy.len(), 2, "both tax names are reported");
assert!(converted.lossy.as_slice()[0].reason.contains("GST"));
}
#[test]
fn connector_types_added_in_2_3_0_survive_a_downgrade_as_their_wire_value() {
let mcs = new::locations::ConnectorType::Mcs;
let old_type: old::locations::ConnectorType = mcs.clone().downgrade().expect_lossless();
assert!(!old_type.is_known(), "2.2.1 does not define MCS");
assert_eq!(old_type.as_str(), "MCS", "but the wire value is unchanged");
let round_trip: new::locations::ConnectorType = old_type.upgrade().expect_lossless();
assert_eq!(round_trip, mcs);
}
#[test]
fn a_2_2_1_hub_role_becomes_hub_party_id_and_back() {
let business = old::locations::BusinessDetails::builder().name("Example Hub").build();
let credentials = old::credentials::Credentials::builder()
.token("token")
.url(crate::types::Url::new("https://hub.example.com/versions").unwrap())
.roles(vec![
old::credentials::CredentialsRole::builder()
.role(old::types::Role::Cpo)
.business_details(business.clone())
.party_id("TNM")
.country_code("NL")
.build(),
old::credentials::CredentialsRole::builder()
.role(old::types::Role::Hub)
.business_details(business)
.party_id("HUB")
.country_code("NL")
.build(),
])
.build();
let up: new::credentials::Credentials = credentials.upgrade().expect_lossless();
assert_eq!(up.hub_party_id.as_ref().unwrap().as_str(), "NLHUB");
assert_eq!(up.roles.len(), 1, "the HUB role became hub_party_id");
assert!(up.is_routing_platform());
let back = up.downgrade();
assert!(back.value.is_hub());
assert_eq!(back.value.roles.len(), 2);
assert_eq!(back.lossy.len(), 1);
assert_eq!(back.lossy.as_slice()[0].pointer, "/hub_party_id");
}
#[test]
fn a_tax_inclusive_tariff_cannot_be_downgraded_faithfully() {
let tariff = new::tariffs::Tariff::builder()
.country_code("CA")
.party_id("ABC")
.id("1")
.currency("CAD")
.elements(vec![
new::tariffs::TariffElement::builder()
.price_components(vec![new::tariffs::PriceComponent::new(
new::tariffs::TariffDimensionType::Time,
"2.10".parse().unwrap(),
)])
.build(),
])
.tax_included(new::tariffs::TaxIncluded::Yes)
.last_updated(dt())
.build();
let converted = tariff.downgrade();
assert_eq!(converted.lossy.as_slice()[0].pointer, "/tax_included");
assert!(converted.lossy.as_slice()[0].reason.contains("excluding VAT"));
}
#[test]
fn a_2_2_1_tariff_upgrades_to_tax_excluded() {
let tariff = old::tariffs::Tariff::builder()
.country_code("DE")
.party_id("ALL")
.id("12")
.currency("EUR")
.elements(vec![
old::tariffs::TariffElement::builder()
.price_components(vec![old::tariffs::PriceComponent::new(
old::tariffs::TariffDimensionType::Time,
"2.00".parse().unwrap(),
)])
.build(),
])
.last_updated(dt())
.build();
let up: new::tariffs::Tariff = tariff.upgrade().expect_lossless();
assert_eq!(up.tax_included, new::tariffs::TaxIncluded::No);
assert!(up.validate().is_ok());
}
#[test]
fn location_fields_added_in_2_3_0_are_reported_when_dropped() {
let mut location = new::locations::Location::builder()
.country_code("NL")
.party_id("TNM")
.id("LOC1")
.publish(true)
.address("Street 1")
.city("Amsterdam")
.country("NLD")
.coordinates(new::locations::GeoLocation::new("52.010000", "4.350000").unwrap())
.time_zone("Europe/Amsterdam")
.help_phone(crate::types::CiString::<25>::new("+31201234567").unwrap())
.last_updated(dt())
.build();
location.parking_places.push(
new::locations::Parking::builder()
.id("P1")
.vehicle_types(vec![new::locations::VehicleType::PersonalVehicle])
.restricted_to_type(false)
.reservation_required(false)
.build(),
);
let converted = location.downgrade();
let pointers: Vec<&str> = converted.lossy.as_slice().iter().map(|l| l.pointer.as_str()).collect();
assert!(pointers.contains(&"/parking_places"), "{pointers:?}");
assert!(pointers.contains(&"/help_phone"), "{pointers:?}");
assert!(converted.value.validate().is_ok(), "the downgraded object still conforms");
}
}