ocpi-tariffs 0.19.1

OCPI tariff calculations
Documentation
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,
};

/// A normalized tariff that doesn't correspond to any particular OCPI version.
#[derive(Debug)]
pub struct Tariff<'a> {
    id: Cow<'a, str>,
    elements: Vec<Element>,
    start_date_time: Option<DateTime>,
    end_date_time: Option<DateTime>,
}

/// An OCPI tariff dimension
pub trait Dimension: Copy {
    /// The cost of this dimension at a certain price.
    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 {
    /// A tariffs field does not have the expected type.
    InvalidType {
        /// The field name of a tariffs JSON with the invalid type.
        field_name: &'static str,
        /// The type that the given field should have according to the schema.
        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) => {
                        // if the dimension type is unknown we log it as an error and continue on with the pricing.
                        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,
        }
    }

    /// Returns true if all components are `Some`.
    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()
    }
}

/// A Price Component describes how a certain amount of a certain dimension being consumed
/// translates into an amount of money owed.
#[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>();
    }
}