ocpi-tariffs 0.51.0

OCPI tariff calculations
Documentation
//! The language-independent intermediate representation of a tariff explanation.
//!
//! [`build`](super::tariff::build) reads a tariff and produces this structure, deciding *what* to
//! say: which tiers apply, how a tier is bounded, whether a catch-all reads as "the remaining ..."
//! or "otherwise", and so on. A [renderer](super::render) then decides *how* to say it in a given
//! language. Keeping every semantic decision here means each language only supplies vocabulary,
//! number formatting and word order, never business logic, so all languages describe the same
//! tariff identically.

use chrono::{DateTime, NaiveDate, NaiveTime, TimeDelta, Utc};

use crate::{
    currency, money::VatOrigin, tariff::v2x::DimensionType, Ampere, Kw, Kwh, Money, Price, Weekday,
};

/// A complete explanation of a tariff.
///
/// The `currency` applies to every amount in the explanation; it is carried once here rather than
/// on each amount because a tariff has a single currency.
pub(super) struct Explanation {
    pub currency: currency::Code,
    pub body: Body,
}

/// The substance of an explanation: either a sequence of topic sections, or a single reason the
/// tariff has nothing to charge.
pub(super) enum Body {
    /// One paragraph per topic, in narration order (energy, charging time, idle time, flat fee,
    /// price bounds, validity window).
    Sections(Vec<Section>),

    /// The tariff produces no charging narrative; this is why, phrased in terms of the tariff.
    Fallback(Fallback),
}

/// A single top-level topic of the explanation, rendered as one paragraph (or bulleted block).
pub(super) enum Section {
    /// A metered dimension: energy, charging time or idle time.
    Dimension(Dimension),

    /// The flat (per-session) fee, or fees.
    Flat(Flat),

    /// The overall `min_price`/`max_price` bounds on a whole session.
    Bounds(Bounds),

    /// The validity window of the tariff itself.
    Validity(Validity),
}

/// A metered dimension narrated as a sequence of priced tiers.
pub(super) struct Dimension {
    /// Which metered dimension this is; the renderer maps it to a subject, per-unit phrase and the
    /// noun used for a "remaining ..." catch-all. Never [`DimensionType::Flat`].
    pub kind: DimensionType,

    /// The priced tiers, in matching order.
    pub tiers: Vec<Tier>,

    /// When every reachable tier shares one billing step larger than the smallest unit, that step
    /// is described once for the whole dimension rather than per tier. `None` when the step varies
    /// per tier or is the smallest unit (continuous billing).
    pub uniform_step: Option<u64>,

    /// Whether tiers were dropped as unreachable because an earlier tier already matches every
    /// session; the renderer appends a note when this is set.
    pub dropped_unreachable: bool,
}

/// A single priced tier of a metered dimension.
pub(super) struct Tier {
    /// The condition under which this tier applies.
    pub condition: Condition,

    /// The rate charged while this tier applies.
    pub rate: Rate,

    /// This tier's own billing step, when it must be noted inline (the step varies across tiers and
    /// this tier bills in steps larger than the smallest unit). `None` when the step is described
    /// once for the whole dimension, is the smallest unit, or the tier is free.
    pub step: Option<u64>,
}

/// The rate charged by a tier.
pub(super) enum Rate {
    /// A zero rate, narrated as "free" rather than an amount.
    Free,

    /// A nonzero rate and the VAT applied to it.
    Priced { amount: Money, vat: VatOrigin },
}

/// The flat (per-session) fee, narrated as one or more tiers.
pub(super) struct Flat {
    /// The flat tiers, in matching order. Always at least one, and at least one with a nonzero fee
    /// (a lone zero fee produces no section at all).
    pub tiers: Vec<FlatTier>,
}

/// A single tier of the flat fee.
pub(super) struct FlatTier {
    /// The condition under which this fee applies.
    pub condition: Condition,

    /// The fee charged per session while this tier applies.
    pub fee: FlatFee,
}

/// The fee charged by a flat tier.
pub(super) enum FlatFee {
    /// A zero fee, narrated as "no fee".
    NoFee,

    /// A nonzero per-session fee and the VAT applied to it.
    Charged { amount: Money, vat: VatOrigin },
}

/// The condition under which a tier applies.
///
/// The pricing engine matches elements top-down, so a tier's condition is really "the earlier tiers
/// did not match, and this tier's own restrictions do". [`Remaining`](Condition::Remaining) and
/// [`Otherwise`](Condition::Otherwise) capture the two ways a trailing catch-all reads.
pub(super) enum Condition {
    /// The first tier has no restrictions of its own; it always applies.
    Always,

    /// A trailing catch-all reached after purely consumption-bounded tiers, read as "for the
    /// remaining ..." (the remaining energy, the remaining charging time, ...).
    Remaining,

    /// A trailing catch-all reached after a tier gated by something other than a plain consumption
    /// bound (time of day, power, an energy threshold on a time tier, ...), read as "otherwise".
    Otherwise,

    /// The tier applies only when all of these hold, in the order they should be listed.
    When(Vec<ConditionPart>),
}

/// One clause of a tier's condition.
pub(super) enum ConditionPart {
    /// A time-of-day window (wall clock, not elapsed session time).
    TimeWindow(TimeWindow),

    /// The session falls on one of these weekdays.
    Weekdays(Vec<Weekday>),

    /// A calendar-date range the session must fall within.
    DateRange {
        start: Option<NaiveDate>,
        end: Option<NaiveDate>,
    },

    /// Charging power is at least this.
    MinPower(Kw),

    /// Charging power is below this.
    MaxPower(Kw),

    /// Charging current is at least this.
    MinCurrent(Ampere),

    /// Charging current is below this.
    MaxCurrent(Ampere),

    /// The session has lasted within this span (elapsed time, e.g. "the first 3 hours").
    DurationScope(Scope<TimeDelta>),

    /// The session has consumed within this much energy (e.g. "the first 20 kWh").
    EnergyScope(Scope<Kwh>),
}

/// A time-of-day window, pre-classified so the renderer never has to compare the two times.
pub(super) enum TimeWindow {
    /// Start equals end: an empty window, so the tier never applies.
    Empty { start: NaiveTime, end: NaiveTime },

    /// End is before start: the window wraps past midnight into the next day.
    Wrapping { start: NaiveTime, end: NaiveTime },

    /// A plain window from start to end within one day.
    Between { start: NaiveTime, end: NaiveTime },

    /// From a start time onwards, with no end.
    From { start: NaiveTime },

    /// Up to an end time, with no start.
    Before { end: NaiveTime },
}

/// A one- or two-sided bound on a consumption quantity (elapsed duration or consumed energy).
pub(super) enum Scope<T> {
    /// Bounded above only: "for the first `T`".
    UpTo(T),

    /// Bounded below only: "after the first `T`".
    After(T),

    /// Bounded on both ends.
    Between(T, T),
}

/// The overall `min_price`/`max_price` bounds on a whole session. At least one is present.
pub(super) struct Bounds {
    pub min: Option<Price>,
    pub max: Option<Price>,
}

/// The validity window of the tariff itself.
pub(super) enum Validity {
    /// Valid only between two instants.
    Between {
        start: DateTime<Utc>,
        end: DateTime<Utc>,
    },

    /// Only becomes active from an instant onwards.
    From { start: DateTime<Utc> },

    /// No longer valid from an instant onwards.
    Until { end: DateTime<Utc> },
}

/// Why a tariff produces no charging narrative, phrased as a cause in the tariff.
pub(super) enum Fallback {
    /// Every element applies only to reservation sessions, so none apply to a regular session.
    ReservationOnly,

    /// No applicable element defines any price component to charge on.
    NoPriceComponents,

    /// The only charge is a flat fee of zero.
    FreeFlatOnly,
}