use std::{borrow::Cow, fmt};
use serde::Serialize;
use tracing::{debug, trace};
use crate::{
json::{self, RawValueExt},
DateTime, HoursDecimal, Kwh, Money,
};
use super::{
restriction::{collect_restrictions, Restriction},
session::ChargePeriod,
v221,
};
#[derive(Debug)]
pub struct Tariff<'a> {
id: Cow<'a, str>,
elements: Vec<Element>,
start_date_time: Option<DateTime>,
end_date_time: Option<DateTime>,
}
pub trait Dimension: Copy {
fn cost(&self, price: Money) -> Money;
}
impl Dimension for () {
fn cost(&self, price: Money) -> Money {
price
}
}
impl Dimension for Kwh {
fn cost(&self, price: Money) -> Money {
price.kwh_cost(*self)
}
}
impl Dimension for HoursDecimal {
fn cost(&self, price: Money) -> Money {
price.time_cost(*self)
}
}
#[derive(Debug)]
pub enum Error {
InvalidType {
field_name: &'static str,
expected_type: json::ValueKind,
},
}
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::InvalidType {
field_name,
expected_type,
} => write!(
f,
"Invalid type for field: `{field_name}`; expected: `{expected_type:?}`"
),
}
}
}
impl<'a> Tariff<'a> {
pub fn new(tariff: &v221::Tariff<'a>) -> Result<Self, Error> {
let v221::Tariff {
id,
elements,
start_date_time,
end_date_time,
} = tariff;
let elements = elements
.iter()
.enumerate()
.map(|(element_index, element)| Element::new(element, element_index))
.collect();
let Some(id) = id.as_str() else {
return Err(Error::InvalidType {
field_name: "id",
expected_type: json::ValueKind::String,
});
};
Ok(Self {
id,
start_date_time: *start_date_time,
end_date_time: *end_date_time,
elements,
})
}
pub fn id(&self) -> &str {
&self.id
}
pub fn active_components(&self, period: &ChargePeriod) -> PriceComponents {
let mut components = PriceComponents::new();
for tariff_element in &self.elements {
trace!("{period:#?}");
if !tariff_element.is_active(period) {
continue;
}
if components.time.is_none() {
components.time = tariff_element.components.time;
}
if components.parking.is_none() {
components.parking = tariff_element.components.parking;
}
if components.energy.is_none() {
components.energy = tariff_element.components.energy;
}
if components.flat.is_none() {
components.flat = tariff_element.components.flat;
}
if components.has_all_components() {
break;
}
}
components
}
pub fn is_active(&self, start_time: DateTime) -> bool {
let is_after_start = self
.start_date_time
.map(|s| start_time >= s)
.unwrap_or(true);
let is_before_end = self.end_date_time.map(|s| start_time < s).unwrap_or(true);
is_after_start && is_before_end
}
}
#[derive(Debug)]
struct Element {
restrictions: Vec<Restriction>,
components: PriceComponents,
}
impl Element {
fn new(ocpi_element: &v221::tariff::Element, element_index: usize) -> Self {
let restrictions = if let Some(restrictions) = &ocpi_element.restrictions {
collect_restrictions(restrictions)
} else {
Vec::new()
};
let mut components = PriceComponents::new();
for ocpi_component in &ocpi_element.price_components {
let price_component = PriceComponent::new(ocpi_component, element_index);
let dimension_type: v221::tariff::DimensionType =
match serde_json::from_str(ocpi_component.dimension_type.get()) {
Ok(v) => v,
Err(err) => {
debug!("{err}");
continue;
}
};
match dimension_type {
v221::tariff::DimensionType::Flat => components.flat.get_or_insert(price_component),
v221::tariff::DimensionType::Time => components.time.get_or_insert(price_component),
v221::tariff::DimensionType::ParkingTime => {
components.parking.get_or_insert(price_component)
}
v221::tariff::DimensionType::Energy => {
components.energy.get_or_insert(price_component)
}
};
}
Self {
restrictions,
components,
}
}
pub fn is_active(&self, period: &ChargePeriod) -> bool {
for restriction in &self.restrictions {
if !restriction.instant_validity_exclusive(&period.start_instant) {
return false;
}
if !restriction.period_validity(&period.period_data) {
return false;
}
}
true
}
#[expect(dead_code, reason = "pending use in linter")]
pub fn is_active_at_end(&self, period: &ChargePeriod) -> bool {
for restriction in &self.restrictions {
if !restriction.instant_validity_inclusive(&period.end_instant) {
return false;
}
}
true
}
}
#[derive(Debug)]
pub struct PriceComponents {
pub flat: Option<PriceComponent>,
pub energy: Option<PriceComponent>,
pub parking: Option<PriceComponent>,
pub time: Option<PriceComponent>,
}
impl PriceComponents {
fn new() -> Self {
Self {
flat: None,
energy: None,
parking: None,
time: None,
}
}
pub fn has_all_components(&self) -> bool {
let Self {
flat,
energy,
parking,
time,
} = self;
flat.is_some() && energy.is_some() && parking.is_some() && time.is_some()
}
}
#[derive(Clone, Copy, Debug, Serialize)]
pub struct PriceComponent {
pub tariff_element_index: usize,
pub price: Money,
pub vat: v221::tariff::CompatibilityVat,
pub step_size: u64,
}
impl PriceComponent {
fn new(component: &v221::tariff::PriceComponent, tariff_element_index: usize) -> Self {
let v221::tariff::PriceComponent {
price,
vat,
step_size,
dimension_type: _,
} = component;
Self {
tariff_element_index,
price: *price,
vat: *vat,
step_size: *step_size,
}
}
}
#[cfg(test)]
mod test {
use super::Error;
#[test]
const fn error_should_be_send_and_sync() {
const fn f<T: Send + Sync>() {}
f::<Error>();
}
}