Skip to main content

ocpi_tariffs/
price.rs

1//! Price a CDR using a tariff and compare the prices embedded in the CDR with the prices computed here.
2
3#[cfg(test)]
4pub mod test;
5
6#[cfg(test)]
7mod test_normalize_periods;
8
9#[cfg(test)]
10mod test_periods;
11
12#[cfg(test)]
13mod test_real_world;
14
15#[cfg(test)]
16mod test_validate_cdr;
17
18#[cfg(test)]
19mod test_warning_path_map;
20
21mod tariff;
22mod v211;
23mod v221;
24
25use std::{borrow::Cow, collections::BTreeMap, fmt, ops::Range};
26
27use chrono::{DateTime, Datelike as _, TimeDelta, Utc};
28use chrono_tz::Tz;
29use rust_decimal::Decimal;
30use tracing::{debug, error, instrument, trace};
31
32use crate::{
33    country, currency, datetime,
34    duration::{self, AsHms as _, Hms},
35    enumeration, from_warning_all,
36    json::{self, FromJson as _},
37    money::{self, VatOrigin},
38    number::{self, RoundDecimal as _},
39    string,
40    warning::{
41        self, GatherDeferredWarnings as _, GatherWarnings as _, IntoCaveat as _,
42        IntoCaveatDeferred as _, VerdictExt as _, WithElement as _,
43    },
44    Ampere, Caveat, Cost, DisplayOption, Kw, Kwh, Money, ParseError, Price, SaturatingAdd as _,
45    SaturatingSub as _, Version, Versioned as _,
46};
47
48use tariff::Tariff;
49
50pub type Verdict<T> = crate::Verdict<T, Warning>;
51type VerdictDeferred<T> = warning::VerdictDeferred<T, Warning>;
52
53/// A normalized/expanded form of a charging period to make the pricing calculation simpler.
54///
55/// The simplicity comes through avoiding having to look up the next period to figure out the end
56/// of the current period.
57#[derive(Debug)]
58struct PeriodNormalized {
59    /// The set of quantities consumed across the duration of the `Period`.
60    consumed: Consumed,
61
62    /// A snapshot of the values of various quantities at the start of the charge period.
63    start_snapshot: TotalsSnapshot,
64
65    /// A snapshot of the values of various quantities at the end of the charge period.
66    end_snapshot: TotalsSnapshot,
67}
68
69/// The set of quantities consumed across the duration of the `Period`.
70#[derive(Clone)]
71#[cfg_attr(test, derive(Default))]
72pub(crate) struct Consumed {
73    /// The peak current during this period.
74    pub current_max: Option<Ampere>,
75
76    /// The lowest current during this period.
77    pub current_min: Option<Ampere>,
78
79    /// The charging time consumed in this period.
80    pub duration_charging: Option<TimeDelta>,
81
82    /// The parking/idle time consumed in this period.
83    pub duration_idle: Option<TimeDelta>,
84
85    /// The energy consumed in this period.
86    pub energy: Option<Kwh>,
87
88    /// The maximum power reached during this period.
89    pub power_max: Option<Kw>,
90
91    /// The minimum power reached during this period.
92    pub power_min: Option<Kw>,
93}
94
95impl fmt::Debug for Consumed {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.debug_struct("Consumed")
98            .field("current_max", &self.current_max)
99            .field("current_min", &self.current_min)
100            .field(
101                "duration_charging",
102                &self.duration_charging.map(|dt| dt.as_hms()),
103            )
104            .field("duration_idle", &self.duration_idle.map(|dt| dt.as_hms()))
105            .field("energy", &self.energy)
106            .field("power_max", &self.power_max)
107            .field("power_min", &self.power_min)
108            .finish()
109    }
110}
111
112/// A snapshot of the values of various quantities at the start and end of the charge period.
113#[derive(Clone)]
114struct TotalsSnapshot {
115    /// The `DateTime` this snapshot of total quantities was taken.
116    date_time: DateTime<Utc>,
117
118    /// The total energy consumed during a charging period.
119    energy: Kwh,
120
121    /// The local timezone.
122    local_timezone: Tz,
123
124    /// The total charging duration during a charging period.
125    duration_charging: TimeDelta,
126
127    /// The total period duration during a charging period.
128    duration_total: TimeDelta,
129}
130
131impl fmt::Debug for TotalsSnapshot {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        f.debug_struct("TotalsSnapshot")
134            .field("date_time", &self.date_time)
135            .field("energy", &self.energy)
136            .field("local_timezone", &self.local_timezone)
137            .field("duration_charging", &self.duration_charging.as_hms())
138            .field("duration_total", &self.duration_total.as_hms())
139            .finish()
140    }
141}
142
143impl TotalsSnapshot {
144    /// Create a snapshot where all quantities are zero.
145    fn zero(date_time: DateTime<Utc>, local_timezone: Tz) -> Self {
146        Self {
147            date_time,
148            energy: Kwh::zero(),
149            local_timezone,
150            duration_charging: TimeDelta::zero(),
151            duration_total: TimeDelta::zero(),
152        }
153    }
154
155    /// Create a new snapshot based on the current snapshot with consumed quantities added.
156    fn next(&self, consumed: &Consumed, date_time: DateTime<Utc>) -> Self {
157        let duration = date_time.signed_duration_since(self.date_time);
158
159        let mut next = Self {
160            date_time,
161            energy: self.energy,
162            local_timezone: self.local_timezone,
163            duration_charging: self.duration_charging,
164            duration_total: self.duration_total.saturating_add(duration),
165        };
166
167        if let Some(duration) = consumed.duration_charging {
168            next.duration_charging = next.duration_charging.saturating_add(duration);
169        }
170
171        if let Some(energy) = consumed.energy {
172            next.energy = next.energy.saturating_add(energy);
173        }
174        next
175    }
176
177    /// Return the local time of this snapshot.
178    fn local_time(&self) -> chrono::NaiveTime {
179        self.date_time.with_timezone(&self.local_timezone).time()
180    }
181
182    /// Return the local date of this snapshot.
183    fn local_date(&self) -> chrono::NaiveDate {
184        self.date_time
185            .with_timezone(&self.local_timezone)
186            .date_naive()
187    }
188
189    /// Return the local `Weekday` of this snapshot.
190    fn local_weekday(&self) -> chrono::Weekday {
191        self.date_time.with_timezone(&self.local_timezone).weekday()
192    }
193}
194
195/// Structure containing the charge session priced according to the specified tariff.
196/// The fields prefixed `total` correspond to CDR fields with the same name.
197pub struct Report {
198    /// Charge session details per period.
199    pub periods: Vec<PeriodReport>,
200
201    /// The index of the tariff that was used for pricing.
202    pub tariff_used: TariffOrigin,
203
204    /// A list of reports for each tariff found in the CDR or supplied to the [`cdr::price`](crate::cdr::price) function.
205    ///
206    /// The order of the `tariff::Report`s are the same as the order in which they are given.
207    pub tariff_reports: Vec<TariffReport>,
208
209    /// Time-zone that was either specified or detected.
210    pub timezone: String,
211
212    /* Billed Quantities */
213    /// The total charging time after applying step-size.
214    pub billed_charging_time: Option<TimeDelta>,
215
216    /// The total energy after applying step-size.
217    pub billed_energy: Option<Kwh>,
218
219    /// The total idle time after applying step-size.
220    pub billed_idle_time: Option<TimeDelta>,
221
222    /* Totals */
223    /// Total duration of the charging session (excluding not charging), in hours.
224    ///
225    /// This is a total that has no direct source field in the `CDR` as it is calculated in the
226    /// [`cdr::price`](crate::cdr::price) function.
227    pub total_charging_time: Option<TimeDelta>,
228
229    /// Total energy charged, in kWh.
230    pub total_energy: Total<Kwh, Option<Kwh>>,
231
232    /// Total duration of the charging session where the EV was not charging (no energy was transferred between EVSE and EV).
233    ///
234    /// See: `total_parking_time` field in <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc#131-cdr-object>.
235    /// Note: We use `total_idle_time` as it's clearer than `total_parking_time`.
236    /// Some people interpret `parking` to mean the total time that the vehicle is standing by the charge point.
237    /// The OCPI spec defines `parking` as the time spent not charging.
238    pub total_idle_time: Total<Option<TimeDelta>>,
239
240    /// Total duration of the charging session (including the duration of charging and idle phases).
241    pub total_time: Total<TimeDelta>,
242
243    /* Costs */
244    /// Total sum of all the costs of this transaction in the specified currency.
245    pub total_cost: Total<Price, Option<Price>>,
246
247    /// Total sum of all the cost of all the energy used, in the specified currency.
248    pub total_energy_cost: Total<Option<Price>>,
249
250    /// Total sum of all the fixed costs in the specified currency, except fixed price components of `parking` and `reservation`.
251    /// The cost not depending on amount of time/energy used etc. Can contain costs like a start tariff.
252    pub total_fixed_cost: Total<Option<Price>>,
253
254    /// Total sum of all the costs related to idleness during this transaction. This includes fixed price components, in the specified currency.
255    ///
256    /// See: `total_parking_cost` field in <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc#131-cdr-object>.
257    /// Note: We use `total_idle_cost` as it's clearer than `total_parking_cost`.
258    /// Some people interpret `parking` to mean the total time that the vehicle is standing by the charge point.
259    /// The OCPI spec defines `parking` as the time spent not charging.
260    pub total_idle_cost: Total<Option<Price>>,
261
262    /// Total sum of all the cost related to a reservation of a Charge Point, including fixed price components, in the specified currency.
263    pub total_reservation_cost: Total<Option<Price>>,
264
265    /// Total sum of all the cost related to duration of charging during this transaction, in the specified currency.
266    ///
267    /// See: `total_time_cost` field in <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc#131-cdr-object>.
268    /// Note: We use `total_charging_time_cost` as it's clearer than `total_time_cost`.
269    pub total_charging_time_cost: Total<Option<Price>>,
270}
271
272impl fmt::Debug for Report {
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        f.debug_struct("Report")
275            .field("periods", &self.periods)
276            .field("tariff_used", &self.tariff_used)
277            .field("tariff_reports", &self.tariff_reports)
278            .field("timezone", &self.timezone)
279            .field(
280                "billed_charging_time",
281                &self.billed_charging_time.map(|dt| dt.as_hms()),
282            )
283            .field("billed_energy", &self.billed_energy)
284            .field(
285                "billed_idle_time",
286                &self.billed_idle_time.map(|dt| dt.as_hms()),
287            )
288            .field(
289                "total_charging_time",
290                &self.total_charging_time.map(|dt| dt.as_hms()),
291            )
292            .field("total_energy", &self.total_energy)
293            .field("total_idle_time", &self.total_idle_time)
294            .field("total_time", &self.total_time)
295            .field("total_cost", &self.total_cost)
296            .field("total_energy_cost", &self.total_energy_cost)
297            .field("total_fixed_cost", &self.total_fixed_cost)
298            .field("total_idle_cost", &self.total_idle_cost)
299            .field("total_reservation_cost", &self.total_reservation_cost)
300            .field("total_charging_time_cost", &self.total_charging_time_cost)
301            .finish()
302    }
303}
304
305/// The warnings that happen when pricing a CDR.
306#[derive(Debug)]
307pub enum Warning {
308    Country(country::Warning),
309    Currency(currency::Warning),
310    DateTime(datetime::Warning),
311    Decode(json::decode::Warning),
312    Duration(duration::Warning),
313    Enum(enumeration::Warning),
314
315    /// The `$.country` field should be an alpha-2 country code.
316    ///
317    /// The alpha-3 code can be converted into an alpha-3 but the caller should be warned.
318    CountryShouldBeAlpha2,
319
320    /// The given dimension should have a volume.
321    DimensionShouldHaveVolume {
322        dimension_name: &'static str,
323    },
324
325    /// A field in the tariff doesn't have the expected type.
326    FieldInvalidType {
327        /// The type that the given field should have according to the schema.
328        expected_type: json::ValueKind,
329    },
330
331    /// A field in the tariff doesn't have the expected value.
332    FieldInvalidValue {
333        /// The value encountered.
334        value: String,
335
336        /// A message about what values are expected for this field.
337        message: Cow<'static, str>,
338    },
339
340    /// The given field is required.
341    FieldRequired {
342        field_name: Cow<'static, str>,
343    },
344
345    /// An internal error occurred.
346    ///
347    /// The details are logged using debug level.
348    InternalError,
349
350    Money(money::Warning),
351
352    /// The CDR has no charging periods.
353    NoPeriods,
354
355    /// No valid tariff has been found in the list of provided tariffs.
356    /// The tariff list can be sourced from either the tariffs contained in the CDR or from a list
357    /// provided by the caller.
358    ///
359    /// A valid tariff must have a start date-time before the start of the session and an end
360    /// date-time after the start of the session.
361    ///
362    /// If the CDR does not contain any tariffs consider providing a them using [`TariffSource`]
363    /// when calling [`cdr::price`](crate::cdr::price).
364    NoValidTariff,
365
366    Number(number::Warning),
367
368    /// An error occurred while deserializing a `CDR` or tariff.
369    Parse(ParseError),
370
371    /// The `start_date_time` of at least one of the `charging_periods` is outside of the
372    /// CDR's `start_date_time`-`end_date_time` range.
373    PeriodsOutsideStartEndDateTime {
374        cdr_range: Range<DateTime<Utc>>,
375        period_range: PeriodRange,
376    },
377
378    String(string::Warning),
379
380    /// Converting the `tariff::Versioned` into a structured `tariff::v221::Tariff` caused an
381    /// unrecoverable error.
382    Tariff(crate::tariff::Warning),
383}
384
385impl Warning {
386    /// Create a new `Warning::FieldInvalidValue` where the field is built from the given `json::Element`.
387    fn field_invalid_value(
388        value: impl Into<String>,
389        message: impl Into<Cow<'static, str>>,
390    ) -> Self {
391        Warning::FieldInvalidValue {
392            value: value.into(),
393            message: message.into(),
394        }
395    }
396}
397
398impl fmt::Display for Warning {
399    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400        match self {
401            Self::Country(warning) => write!(f, "{warning}"),
402            Self::CountryShouldBeAlpha2 => {
403                f.write_str("The `$.country` field should be an alpha-2 country code.")
404            }
405            Self::Currency(warning) => write!(f, "{warning}"),
406            Self::DateTime(warning) => write!(f, "{warning}"),
407            Self::Decode(warning) => write!(f, "{warning}"),
408            Self::DimensionShouldHaveVolume { dimension_name } => {
409                write!(f, "Dimension `{dimension_name}` should have volume")
410            }
411            Self::Duration(warning) => write!(f, "{warning}"),
412            Self::Enum(warning) => write!(f, "{warning}"),
413            Self::FieldInvalidType { expected_type } => {
414                write!(f, "Field has invalid type. Expected type `{expected_type}`")
415            }
416            Self::FieldInvalidValue { value, message } => {
417                write!(f, "Field has invalid value `{value}`: {message}")
418            }
419            Self::FieldRequired { field_name } => {
420                write!(f, "Field is required: `{field_name}`")
421            }
422            Self::InternalError => f.write_str("Internal error"),
423            Self::Money(warning) => write!(f, "{warning}"),
424            Self::NoPeriods => f.write_str("The CDR has no charging periods"),
425            Self::NoValidTariff => {
426                f.write_str("No valid tariff has been found in the list of provided tariffs")
427            }
428            Self::Number(warning) => write!(f, "{warning}"),
429            Self::Parse(err) => {
430                write!(f, "{err}")
431            }
432            Self::PeriodsOutsideStartEndDateTime {
433                cdr_range: Range { start, end },
434                period_range,
435            } => {
436                write!(
437                    f,
438                    "The CDR's charging period time range is not contained within the `start_date_time` \
439                    and `end_date_time`; cdr: [start: {start}, end: {end}], period: {period_range}",
440                )
441            }
442            Self::String(warning) => write!(f, "{warning}"),
443            Self::Tariff(warnings) => {
444                write!(f, "Tariff warnings: {warnings:?}")
445            }
446        }
447    }
448}
449
450impl crate::Warning for Warning {
451    fn id(&self) -> warning::Id {
452        match self {
453            Self::Country(warning) => warning.id(),
454            Self::CountryShouldBeAlpha2 => warning::Id::from_static("country_should_be_alpha_2"),
455            Self::Currency(warning) => warning.id(),
456            Self::DateTime(warning) => warning.id(),
457            Self::Decode(warning) => warning.id(),
458            Self::DimensionShouldHaveVolume { dimension_name } => {
459                warning::Id::from_string(format!("dimension_should_have_volume({dimension_name})"))
460            }
461            Self::Duration(warning) => warning.id(),
462            Self::Enum(warning) => warning.id(),
463            Self::FieldInvalidType { expected_type } => {
464                warning::Id::from_string(format!("field_invalid_type({expected_type})"))
465            }
466            Self::FieldInvalidValue { value, .. } => {
467                warning::Id::from_string(format!("field_invalid_value({value})"))
468            }
469            Self::FieldRequired { field_name } => {
470                warning::Id::from_string(format!("field_required({field_name})"))
471            }
472            Self::InternalError => warning::Id::from_static("internal_error"),
473            Self::Money(warning) => warning.id(),
474            Self::NoPeriods => warning::Id::from_static("no_periods"),
475            Self::NoValidTariff => warning::Id::from_static("no_valid_tariff"),
476            Self::Number(warning) => warning.id(),
477            Self::Parse(ParseError { object: _, kind }) => kind.id(),
478            Self::PeriodsOutsideStartEndDateTime { .. } => {
479                warning::Id::from_static("periods_outside_start_end_date_time")
480            }
481            Self::String(warning) => warning.id(),
482            Self::Tariff(warning) => warning.id(),
483        }
484    }
485}
486
487from_warning_all!(
488    country::Warning => Warning::Country,
489    currency::Warning => Warning::Currency,
490    datetime::Warning => Warning::DateTime,
491    duration::Warning => Warning::Duration,
492    enumeration::Warning => Warning::Enum,
493    json::decode::Warning => Warning::Decode,
494    money::Warning => Warning::Money,
495    number::Warning => Warning::Number,
496    string::Warning => Warning::String,
497    crate::tariff::Warning => Warning::Tariff
498);
499
500/// A report of parsing and using the referenced tariff to price a CDR.
501#[derive(Debug)]
502pub struct TariffReport {
503    /// The id of the tariff.
504    pub origin: TariffOrigin,
505
506    /// Warnings from parsing a tariff.
507    ///
508    /// Each entry in the map is an element path and a list of associated warnings.
509    pub warnings: BTreeMap<warning::Path, Vec<crate::tariff::Warning>>,
510}
511
512/// The origin data for a tariff.
513#[derive(Clone, Debug)]
514pub struct TariffOrigin {
515    /// The index of the tariff in the CDR JSON or in the list of override tariffs.
516    pub index: usize,
517
518    /// The value of the `id` field in the tariff JSON.
519    pub id: String,
520
521    // The currency code of the tariff.
522    pub currency: currency::Code,
523}
524
525/// A CDR charge period in a normalized form ready for pricing.
526#[derive(Debug)]
527pub(crate) struct Period {
528    /// The start time of this period.
529    pub start_date_time: DateTime<Utc>,
530
531    /// The quantities consumed during this period.
532    pub consumed: Consumed,
533}
534
535/// A structure containing a report for each dimension.
536#[derive(Debug)]
537pub struct Dimensions {
538    /// Energy consumed.
539    pub energy: Dimension<Kwh>,
540
541    /// Flat fee without unit for `step_size`.
542    pub flat: Dimension<()>,
543
544    /// Duration of time charging.
545    pub duration_charging: Dimension<TimeDelta>,
546
547    /// Duration of time not charging.
548    pub duration_idle: Dimension<TimeDelta>,
549}
550
551impl Dimensions {
552    /// Create a new `Dimensions` object.
553    fn new(components: ComponentSet, consumed: &Consumed) -> Self {
554        let ComponentSet {
555            energy: energy_price,
556            flat: flat_price,
557            duration_charging: duration_charging_price,
558            duration_idle: duration_idle_price,
559        } = components;
560
561        let Consumed {
562            duration_charging,
563            duration_idle,
564            energy,
565            current_max: _,
566            current_min: _,
567            power_max: _,
568            power_min: _,
569        } = consumed;
570
571        Self {
572            energy: Dimension {
573                price: energy_price,
574                volume: *energy,
575                billed_volume: *energy,
576            },
577            flat: Dimension {
578                price: flat_price,
579                volume: Some(()),
580                billed_volume: Some(()),
581            },
582            duration_charging: Dimension {
583                price: duration_charging_price,
584                volume: *duration_charging,
585                billed_volume: *duration_charging,
586            },
587            duration_idle: Dimension {
588                price: duration_idle_price,
589                volume: *duration_idle,
590                billed_volume: *duration_idle,
591            },
592        }
593    }
594}
595
596#[derive(Debug)]
597/// A report for a single dimension during a single period.
598pub struct Dimension<V> {
599    /// The price component that was active during this period for this dimension.
600    /// It could be that no price component was active during this period for this dimension in
601    /// which case `price` is `None`.
602    pub price: Option<Component>,
603
604    /// The volume of this dimension during this period, as received in the provided charge detail record.
605    /// It could be that no volume was provided during this period for this dimension in which case
606    /// the `volume` is `None`.
607    pub volume: Option<V>,
608
609    /// This field contains the optional value of `volume` after a potential step size was applied.
610    /// Step size is applied over the total volume during the whole session of a dimension. But the
611    /// resulting additional volume should be billed according to the price component in this
612    /// period.
613    ///
614    /// If no step-size was applied for this period, the volume is exactly equal to the `volume`
615    /// field.
616    pub billed_volume: Option<V>,
617}
618
619impl<V: Cost> Dimension<V> {
620    /// The total cost of this dimension during a period.
621    pub fn cost(&self) -> Option<Price> {
622        let (Some(volume), Some(price_component)) = (&self.billed_volume, &self.price) else {
623            return None;
624        };
625
626        let excl_vat = volume.cost(price_component.price);
627
628        let incl_vat = match price_component.vat {
629            VatOrigin::Provided(vat) => Some(excl_vat.apply_vat(vat)),
630            VatOrigin::NotProvided => Some(excl_vat),
631            VatOrigin::Unknown => None,
632        };
633
634        Some(Price { excl_vat, incl_vat })
635    }
636}
637
638/// A set of price `Component`s, one for each dimension.
639///
640/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#142-pricecomponent-class>.
641/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#145-tariffdimensiontype-enum>.
642#[derive(Debug)]
643pub struct ComponentSet {
644    /// Energy consumed.
645    pub energy: Option<Component>,
646
647    /// Flat fee without unit for `step_size`.
648    pub flat: Option<Component>,
649
650    /// Duration of time charging.
651    pub duration_charging: Option<Component>,
652
653    /// Duration of time not charging.
654    pub duration_idle: Option<Component>,
655}
656
657impl ComponentSet {
658    /// Returns true if all components are `Some`.
659    fn has_all_components(&self) -> bool {
660        let Self {
661            energy,
662            flat,
663            duration_charging,
664            duration_idle,
665        } = self;
666
667        flat.is_some() && energy.is_some() && duration_idle.is_some() && duration_charging.is_some()
668    }
669}
670
671/// A Price Component describes how a certain amount of a certain dimension being consumed
672/// translates into an amount of money owed.
673///
674/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#142-pricecomponent-class>.
675#[derive(Clone, Debug)]
676pub struct Component {
677    /// Price per unit (excl. VAT) for this dimension.
678    price: Money,
679
680    /// Applicable VAT percentage for this tariff dimension. If omitted, no VAT is applicable.
681    /// Not providing a VAT is different from 0% VAT, which would be a value of 0.0 here.
682    vat: VatOrigin,
683
684    /// Minimum amount to be billed. That is, the dimension will be billed in this `step_size` blocks.
685    /// Consumed amounts are rounded up to the smallest multiple of `step_size` that is greater than
686    /// the consumed amount.
687    ///
688    /// For example: if type is TIME and `step_size` has a value of 300, then time will be billed in
689    /// blocks of 5 minutes. If 6 minutes were consumed, 10 minutes (2 blocks of `step_size`) will
690    /// be billed.
691    step_size: u64,
692}
693
694impl Component {
695    /// Create a new `Component` object.
696    fn new(component: &crate::tariff::v221::PriceComponent) -> Self {
697        let crate::tariff::v221::PriceComponent {
698            price,
699            vat,
700            step_size,
701            dimension_type: _,
702        } = component;
703
704        Self {
705            price: *price,
706            vat: *vat,
707            step_size: *step_size,
708        }
709    }
710
711    /// Return the price of the `Component`.
712    pub fn price(&self) -> Money {
713        self.price
714    }
715
716    /// Associate this `PriceComponent` with a period index.
717    fn into_period(self, period_index: usize) -> PeriodComponent {
718        PeriodComponent {
719            period_index,
720            component: self,
721        }
722    }
723}
724
725/// A `PriceComponent` with the index of the period it's associated with.
726#[derive(Debug)]
727struct PeriodComponent {
728    /// The index of the period this `PriceComponent` came from.
729    period_index: usize,
730
731    /// The `PriceComponent`.
732    component: Component,
733}
734/// A related source and calculated pair of total amounts.
735///
736/// This is used to express the source and calculated amounts for the total fields of a `CDR`.
737///
738/// - `total_cost`
739/// - `total_fixed_cost`
740/// - `total_energy`
741/// - `total_energy_cost`
742/// - `total_time`
743/// - `total_time_cost`
744/// - `total_parking_time`
745/// - `total_parking_cost`
746/// - `total_reservation_cost`
747#[derive(Debug)]
748pub struct Total<TCdr, TCalc = TCdr> {
749    /// The source value from the `CDR`.
750    pub cdr: TCdr,
751
752    /// The value calculated by the [`cdr::price`](crate::cdr::price) function.
753    pub calculated: TCalc,
754}
755
756/// The range of time the CDR periods span.
757#[derive(Debug)]
758pub enum PeriodRange {
759    /// There are many periods in the CDR and so the range is from the `start_date_time` of the first to
760    /// the `start_date_time` of the last.
761    Many(Range<DateTime<Utc>>),
762
763    /// There is one period in the CDR and so one `start_date_time`.
764    Single(DateTime<Utc>),
765}
766
767impl fmt::Display for PeriodRange {
768    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
769        match self {
770            PeriodRange::Many(Range { start, end }) => write!(f, "[start: {start}, end: {end}]"),
771            PeriodRange::Single(date_time) => write!(f, "{date_time}"),
772        }
773    }
774}
775
776/// Where should the tariffs come from when pricing a `CDR`.
777///
778/// Used with [`cdr::price`](crate::cdr::price).
779#[derive(Debug)]
780pub enum TariffSource<'buf> {
781    /// Use the tariffs from the `CDR`.
782    UseCdr,
783
784    /// Ignore the tariffs from the `CDR` and use these instead.
785    Override(Vec<crate::tariff::Versioned<'buf>>),
786}
787
788impl<'buf> TariffSource<'buf> {
789    /// Convenience method to provide a single override tariff.
790    pub fn single(tariff: crate::tariff::Versioned<'buf>) -> Self {
791        Self::Override(vec![tariff])
792    }
793}
794
795/// Price a CDR.
796///
797/// See: [`crate::cdr::price`].
798#[instrument(skip_all)]
799pub(super) fn cdr(
800    cdr_elem: &crate::cdr::Versioned<'_>,
801    tariff_source: TariffSource<'_>,
802    timezone: Tz,
803) -> Verdict<Report> {
804    let source_version = cdr_elem.version();
805    let cdr = parse_cdr(cdr_elem)?;
806
807    match tariff_source {
808        TariffSource::UseCdr => {
809            let (v221::cdr::WithTariffs { cdr, tariffs }, warnings) = cdr.into_parts();
810            debug!("Using tariffs from CDR");
811            let tariffs = tariffs
812                .iter()
813                .map(|elem| {
814                    // Parse the nested tariffs based on the original version of the CDR.
815                    match source_version {
816                        Version::V221 => crate::tariff::v221::Tariff::from_json(elem),
817                        Version::V211 => {
818                            let tariff = crate::tariff::v211::Tariff::from_json(elem);
819                            // `ocpi-tariffs` considers the `v221` tariff to be the "normalized" version.
820                            // A `v211` tariff is converted to a `v221` tariff for various operations.
821                            tariff.map_caveat(crate::tariff::v221::Tariff::from)
822                        }
823                    }
824                })
825                .collect::<Result<Vec<_>, _>>()?;
826
827            let cdr = cdr.into_caveat(warnings);
828
829            Ok(price_v221_cdr_with_tariffs(
830                cdr_elem, cdr, tariffs, timezone,
831            )?)
832        }
833        TariffSource::Override(tariffs) => {
834            let cdr = cdr.map(v221::cdr::WithTariffs::discard_tariffs);
835
836            debug!("Using override tariffs");
837            let tariffs = tariffs
838                .iter()
839                .map(tariff::parse)
840                .collect::<Result<Vec<_>, _>>()?;
841
842            Ok(price_v221_cdr_with_tariffs(
843                cdr_elem, cdr, tariffs, timezone,
844            )?)
845        }
846    }
847}
848
849/// Price a single charge-session using a tariff selected from a list.
850///
851/// Returns a report containing the totals, subtotals, and a breakdown of the calculation.
852/// Price a single charge-session using a single tariff.
853///
854/// Returns a report containing the totals, subtotals, and a breakdown of the calculation.
855fn price_v221_cdr_with_tariffs(
856    cdr_elem: &crate::cdr::Versioned<'_>,
857    cdr: Caveat<v221::Cdr, Warning>,
858    tariffs: Vec<Caveat<crate::tariff::v221::Tariff<'_>, crate::tariff::Warning>>,
859    timezone: Tz,
860) -> Verdict<Report> {
861    debug!(?timezone, version = ?cdr_elem.version(), "Pricing CDR");
862    let (cdr, mut warnings) = cdr.into_parts();
863    let v221::Cdr {
864        start_date_time,
865        end_date_time,
866        charging_periods,
867        totals: cdr_totals,
868    } = cdr;
869
870    // Convert each versioned tariff JSON to a structured tariff.
871    //
872    // This generates a list of `TariffReport`s that are returned to the caller in the `Report`.
873    // One of the structured tariffs is selected for use in the `price_periods` function.
874    let (tariff_reports, tariffs): (Vec<_>, Vec<_>) = tariffs
875        .into_iter()
876        .enumerate()
877        .map(|(index, tariff)| {
878            let (tariff, warnings) = tariff.into_parts();
879            (
880                TariffReport {
881                    origin: TariffOrigin {
882                        index,
883                        id: tariff.id.to_string(),
884                        currency: tariff.currency,
885                    },
886                    warnings: warnings.into_path_map(),
887                },
888                tariff,
889            )
890        })
891        .unzip();
892
893    debug!(tariffs = ?tariffs.iter().map(|t| t.id).collect::<Vec<_>>(), "Found tariffs(by id) in CDR");
894
895    let tariffs_normalized = tariff::normalize_all(&tariffs);
896    let Some((tariff_index, tariff)) =
897        tariff::find_first_active(tariffs_normalized, start_date_time)
898    else {
899        return warnings.bail(Warning::NoValidTariff, cdr_elem.as_element());
900    };
901
902    debug!(tariff_index, id = ?tariff.id(), "Found active tariff");
903    debug!(%timezone, "Found timezone");
904    // Convert the CDRs periods to the API input period.
905    let periods = charging_periods
906        .into_iter()
907        .map(Period::try_from)
908        .collect::<Result<Vec<_>, _>>()
909        .map_err(|err| warning::ErrorSet::with_warn(Warning::Parse(err), cdr_elem.as_element()))?;
910
911    let periods = normalize_periods(periods, end_date_time, timezone);
912    let price_cdr_report = price_periods(&periods, &tariff)
913        .with_element(cdr_elem.as_element())?
914        .gather_warnings_into(&mut warnings);
915
916    let report = generate_report(
917        &cdr_totals,
918        timezone,
919        tariff_reports,
920        price_cdr_report,
921        TariffOrigin {
922            index: tariff_index,
923            id: tariff.id().to_owned(),
924            currency: tariff.currency(),
925        },
926    );
927
928    Ok(report.into_caveat(warnings))
929}
930
931/// Price a list of normalized [`Period`]s using a [`Versioned`](crate::tariff::Versioned) tariff.
932pub(crate) fn periods(
933    end_date_time: DateTime<Utc>,
934    timezone: Tz,
935    tariff_elem: &crate::tariff::v221::Tariff<'_>,
936    mut periods: Vec<Period>,
937) -> VerdictDeferred<PeriodsReport> {
938    // Make sure the periods are sorted by time as the start date of one period determines the end
939    // date of the previous period.
940    periods.sort_by_key(|p| p.start_date_time);
941    let tariff = Tariff::from_v221(tariff_elem);
942    let periods = normalize_periods(periods, end_date_time, timezone);
943    price_periods(&periods, &tariff)
944}
945
946fn normalize_periods(
947    periods: Vec<Period>,
948    end_date_time: DateTime<Utc>,
949    local_timezone: Tz,
950) -> Vec<PeriodNormalized> {
951    debug!("Normalizing CDR periods");
952
953    // Each new period is linked to the previous periods data.
954    let mut previous_end_snapshot = Option::<TotalsSnapshot>::None;
955
956    // The end-date of the first period is the start-date of the second and so on.
957    let end_dates = {
958        let mut end_dates = periods
959            .iter()
960            .skip(1)
961            .map(|p| p.start_date_time)
962            .collect::<Vec<_>>();
963
964        // The last end-date is the end-date of the CDR.
965        end_dates.push(end_date_time);
966        end_dates
967    };
968
969    let periods = periods
970        .into_iter()
971        .zip(end_dates)
972        .enumerate()
973        .map(|(index, (period, end_date_time))| {
974            trace!(index, "processing\n{period:#?}");
975            let Period {
976                start_date_time,
977                consumed,
978            } = period;
979
980            let period = if let Some(prev_end_snapshot) = previous_end_snapshot.take() {
981                let start_snapshot = prev_end_snapshot;
982                let end_snapshot = start_snapshot.next(&consumed, end_date_time);
983
984                let period = PeriodNormalized {
985                    consumed,
986                    start_snapshot,
987                    end_snapshot,
988                };
989                trace!("Adding new period based on the last added\n{period:#?}");
990                period
991            } else {
992                let start_snapshot = TotalsSnapshot::zero(start_date_time, local_timezone);
993                let end_snapshot = start_snapshot.next(&consumed, end_date_time);
994
995                let period = PeriodNormalized {
996                    consumed,
997                    start_snapshot,
998                    end_snapshot,
999                };
1000                trace!("Adding new period\n{period:#?}");
1001                period
1002            };
1003
1004            previous_end_snapshot.replace(period.end_snapshot.clone());
1005            period
1006        })
1007        .collect::<Vec<_>>();
1008
1009    periods
1010}
1011
1012/// Price the given set of CDR periods using a normalized `Tariff`.
1013fn price_periods(periods: &[PeriodNormalized], tariff: &Tariff) -> VerdictDeferred<PeriodsReport> {
1014    debug!(count = periods.len(), "Pricing CDR periods");
1015
1016    if tracing::enabled!(tracing::Level::TRACE) {
1017        trace!("# CDR period list:");
1018        for period in periods {
1019            trace!("{period:#?}");
1020        }
1021    }
1022
1023    let period_totals = period_totals(periods, tariff);
1024    let (billed, warnings) = period_totals.calculate_billed()?.into_parts();
1025    let (billable, periods, totals) = billed;
1026    let total_costs = total_costs(&periods, tariff);
1027    let report = PeriodsReport {
1028        billable,
1029        periods,
1030        totals,
1031        total_costs,
1032    };
1033
1034    Ok(report.into_caveat_deferred(warnings))
1035}
1036
1037/// The internal report generated from the [`periods`] fn.
1038pub(crate) struct PeriodsReport {
1039    /// The billable dimensions calculated by applying the step-size to each dimension.
1040    pub billable: Billable,
1041
1042    /// A list of reports for each charging period that occurred during a session.
1043    pub periods: Vec<PeriodReport>,
1044
1045    /// The totals for each dimension.
1046    pub totals: Totals,
1047
1048    /// The total costs for each dimension.
1049    pub total_costs: TotalCosts,
1050}
1051
1052/// A report for a single charging period that occurred during a session.
1053///
1054/// A charging period is a period of time that has relevance for the total costs of a CDR.
1055/// During a charging session, different parameters change all the time, like the amount of energy used,
1056/// or the time of day. These changes can result in another [`PriceComponent`](https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#142-pricecomponent-class) of the Tariff becoming active.
1057#[derive(Debug)]
1058pub struct PeriodReport {
1059    /// The start time of this period.
1060    pub start_date_time: DateTime<Utc>,
1061
1062    /// The end time of this period.
1063    pub end_date_time: DateTime<Utc>,
1064
1065    /// A structure that contains results per dimension.
1066    pub dimensions: Dimensions,
1067}
1068
1069impl PeriodReport {
1070    /// Create a new `PeriodReport` object.
1071    fn new(period: &PeriodNormalized, dimensions: Dimensions) -> Self {
1072        Self {
1073            start_date_time: period.start_snapshot.date_time,
1074            end_date_time: period.end_snapshot.date_time,
1075            dimensions,
1076        }
1077    }
1078
1079    /// The total cost of all dimensions in this period.
1080    pub fn cost(&self) -> Option<Price> {
1081        [
1082            self.dimensions.duration_charging.cost(),
1083            self.dimensions.duration_idle.cost(),
1084            self.dimensions.flat.cost(),
1085            self.dimensions.energy.cost(),
1086        ]
1087        .into_iter()
1088        .fold(None, |accum, next| {
1089            if accum.is_none() && next.is_none() {
1090                None
1091            } else {
1092                Some(
1093                    accum
1094                        .unwrap_or_default()
1095                        .saturating_add(next.unwrap_or_default()),
1096                )
1097            }
1098        })
1099    }
1100}
1101
1102/// The result of normalizing the CDR charging periods.
1103#[derive(Debug)]
1104struct PeriodTotals {
1105    /// The list of normalized periods.
1106    periods: Vec<PeriodReport>,
1107
1108    /// The computed step size.
1109    step_size: StepSize,
1110
1111    /// The totals for each dimension.
1112    totals: Totals,
1113}
1114
1115/// The totals for each dimension.
1116#[derive(Debug, Default)]
1117pub(crate) struct Totals {
1118    /// The total energy used during a session.
1119    pub energy: Option<Kwh>,
1120
1121    /// The total charging time used during a session.
1122    ///
1123    /// Some if the charging happened during the session.
1124    pub duration_charging: Option<TimeDelta>,
1125
1126    /// The total idle time during a session.
1127    ///
1128    /// Some if there was idle time during the session.
1129    pub duration_idle: Option<TimeDelta>,
1130}
1131
1132impl PeriodTotals {
1133    /// Calculate the billed dimensions by applying the step-size to each dimension.
1134    ///
1135    /// Applying the step size can mutate the dimension values contained in the `Period`.
1136    fn calculate_billed(self) -> VerdictDeferred<(Billable, Vec<PeriodReport>, Totals)> {
1137        let mut warnings = warning::SetDeferred::new();
1138        let Self {
1139            mut periods,
1140            step_size,
1141            totals,
1142        } = self;
1143        let duration_charging = totals
1144            .duration_charging
1145            .map(|dt| step_size.apply_time(&mut periods, dt))
1146            .transpose()?
1147            .gather_deferred_warnings_into(&mut warnings);
1148        let energy = totals
1149            .energy
1150            .map(|kwh| step_size.apply_energy(&mut periods, kwh))
1151            .transpose()?
1152            .gather_deferred_warnings_into(&mut warnings);
1153        let duration_idle = totals
1154            .duration_idle
1155            .map(|dt| step_size.apply_idle_time(&mut periods, dt))
1156            .transpose()?
1157            .gather_deferred_warnings_into(&mut warnings);
1158        let billed = Billable {
1159            duration_charging,
1160            duration_idle,
1161            energy,
1162        };
1163        Ok((billed, periods, totals).into_caveat_deferred(warnings))
1164    }
1165}
1166
1167/// The billable dimensions calculated by applying the step-size to each dimension.
1168#[derive(Debug)]
1169pub(crate) struct Billable {
1170    /// The billable charging time.
1171    duration_charging: Option<TimeDelta>,
1172
1173    /// The billable idle time.
1174    duration_idle: Option<TimeDelta>,
1175
1176    /// The billable energy use.
1177    energy: Option<Kwh>,
1178}
1179
1180/// Map the `session::ChargePeriod`s to a normalized `Period` and calculate the step size and
1181/// totals for each dimension.
1182fn period_totals(periods: &[PeriodNormalized], tariff: &Tariff) -> PeriodTotals {
1183    let mut has_flat_fee = false;
1184    let mut step_size = StepSize::default();
1185    let mut totals = Totals::default();
1186
1187    debug!(
1188        tariff_id = tariff.id(),
1189        period_count = periods.len(),
1190        "Accumulating dimension totals for each period"
1191    );
1192
1193    let periods = periods
1194        .iter()
1195        .enumerate()
1196        .map(|(index, period)| {
1197            let mut component_set = tariff.active_components(period);
1198            trace!(
1199                index,
1200                "Creating charge period with Dimension\n{period:#?}\n{component_set:#?}"
1201            );
1202
1203            if component_set.flat.is_some() {
1204                if has_flat_fee {
1205                    component_set.flat = None;
1206                } else {
1207                    has_flat_fee = true;
1208                }
1209            }
1210
1211            step_size.update(index, period, &component_set);
1212
1213            trace!(period_index = index, "Step size updated\n{step_size:#?}");
1214
1215            let dimensions = Dimensions::new(component_set, &period.consumed);
1216
1217            trace!(period_index = index, "Dimensions created\n{dimensions:#?}");
1218
1219            if let Some(dt) = dimensions.duration_charging.volume {
1220                let acc = totals.duration_charging.get_or_insert_default();
1221                *acc = acc.saturating_add(dt);
1222            }
1223
1224            if let Some(kwh) = dimensions.energy.volume {
1225                let acc = totals.energy.get_or_insert_default();
1226                *acc = acc.saturating_add(kwh);
1227            }
1228
1229            if let Some(dt) = dimensions.duration_idle.volume {
1230                let acc = totals.duration_idle.get_or_insert_default();
1231                *acc = acc.saturating_add(dt);
1232            }
1233
1234            trace!(period_index = index, ?totals, "Update totals");
1235
1236            PeriodReport::new(period, dimensions)
1237        })
1238        .collect::<Vec<_>>();
1239
1240    PeriodTotals {
1241        periods,
1242        step_size,
1243        totals,
1244    }
1245}
1246
1247/// The total costs for each dimension.
1248#[derive(Debug, Default)]
1249pub(crate) struct TotalCosts {
1250    /// The [`Price`] for all energy used during a session.
1251    pub energy: Option<Price>,
1252
1253    /// The [`Price`] for all flat rates applied during a session.
1254    pub fixed: Option<Price>,
1255
1256    /// The [`Price`] for all charging time used during a session.
1257    pub duration_charging: Option<Price>,
1258
1259    /// The [`Price`] for all idle time used during a session.
1260    pub duration_idle: Option<Price>,
1261}
1262
1263impl TotalCosts {
1264    /// Summate each dimension total into a single total.
1265    ///
1266    /// Return `None` if there are no cost dimensions otherwise return `Some`.
1267    pub(crate) fn total(&self) -> Option<Price> {
1268        let Self {
1269            energy,
1270            fixed,
1271            duration_charging,
1272            duration_idle,
1273        } = self;
1274        debug!(
1275            energy = %DisplayOption(*energy),
1276            fixed = %DisplayOption(*fixed),
1277            duration_charging = %DisplayOption(*duration_charging),
1278            duration_idle = %DisplayOption(*duration_idle),
1279            "Calculating total costs."
1280        );
1281        [energy, fixed, duration_charging, duration_idle]
1282            .into_iter()
1283            .fold(None, |accum: Option<Price>, next| match (accum, next) {
1284                (None, None) => None,
1285                _ => Some(
1286                    accum
1287                        .unwrap_or_default()
1288                        .saturating_add(next.unwrap_or_default()),
1289                ),
1290            })
1291    }
1292}
1293
1294/// Accumulate total costs per dimension across all periods.
1295fn total_costs(periods: &[PeriodReport], tariff: &Tariff) -> TotalCosts {
1296    let mut total_costs = TotalCosts::default();
1297
1298    debug!(
1299        tariff_id = tariff.id(),
1300        period_count = periods.len(),
1301        "Accumulating dimension costs for each period"
1302    );
1303    for (index, period) in periods.iter().enumerate() {
1304        let dimensions = &period.dimensions;
1305
1306        trace!(period_index = index, "Processing period");
1307
1308        let energy_cost = dimensions.energy.cost();
1309        let fixed_cost = dimensions.flat.cost();
1310        let duration_charging_cost = dimensions.duration_charging.cost();
1311        let duration_idle_cost = dimensions.duration_idle.cost();
1312
1313        trace!(?total_costs.energy, ?energy_cost, "Energy cost");
1314        trace!(?total_costs.duration_charging, ?duration_charging_cost, "Charging cost");
1315        trace!(?total_costs.duration_idle, ?duration_idle_cost, "Idle cost");
1316        trace!(?total_costs.fixed, ?fixed_cost, "Fixed cost");
1317
1318        total_costs.energy = match (total_costs.energy, energy_cost) {
1319            (None, None) => None,
1320            (total, period) => Some(
1321                total
1322                    .unwrap_or_default()
1323                    .saturating_add(period.unwrap_or_default()),
1324            ),
1325        };
1326
1327        total_costs.duration_charging =
1328            match (total_costs.duration_charging, duration_charging_cost) {
1329                (None, None) => None,
1330                (total, period) => Some(
1331                    total
1332                        .unwrap_or_default()
1333                        .saturating_add(period.unwrap_or_default()),
1334                ),
1335            };
1336
1337        total_costs.duration_idle = match (total_costs.duration_idle, duration_idle_cost) {
1338            (None, None) => None,
1339            (total, period) => Some(
1340                total
1341                    .unwrap_or_default()
1342                    .saturating_add(period.unwrap_or_default()),
1343            ),
1344        };
1345
1346        total_costs.fixed = match (total_costs.fixed, fixed_cost) {
1347            (None, None) => None,
1348            (total, period) => Some(
1349                total
1350                    .unwrap_or_default()
1351                    .saturating_add(period.unwrap_or_default()),
1352            ),
1353        };
1354
1355        trace!(period_index = index, ?total_costs, "Update totals");
1356    }
1357
1358    total_costs
1359}
1360
1361fn generate_report(
1362    cdr_totals: &v221::cdr::Totals,
1363    timezone: Tz,
1364    tariff_reports: Vec<TariffReport>,
1365    price_periods_report: PeriodsReport,
1366    tariff_used: TariffOrigin,
1367) -> Report {
1368    let PeriodsReport {
1369        billable,
1370        periods,
1371        totals,
1372        total_costs,
1373    } = price_periods_report;
1374    trace!("Update billed totals {billable:#?}");
1375
1376    let total_cost = total_costs.total();
1377
1378    debug!(total_cost = %DisplayOption(total_cost.as_ref()));
1379
1380    let total_time = {
1381        debug!(
1382            period_start = %DisplayOption(periods.first().map(|p| p.start_date_time)),
1383            period_end = %DisplayOption(periods.last().map(|p| p.end_date_time)),
1384            "Calculating `total_time`"
1385        );
1386
1387        periods
1388            .first()
1389            .zip(periods.last())
1390            .map(|(first, last)| {
1391                last.end_date_time
1392                    .signed_duration_since(first.start_date_time)
1393            })
1394            .unwrap_or_default()
1395    };
1396    debug!(total_time = %Hms(total_time));
1397
1398    let report = Report {
1399        periods,
1400        tariff_used,
1401        timezone: timezone.to_string(),
1402        billed_idle_time: billable.duration_idle,
1403        billed_energy: billable.energy.round_to_ocpi_scale(),
1404        billed_charging_time: billable.duration_charging,
1405        tariff_reports,
1406        total_charging_time: totals.duration_charging,
1407        total_cost: Total {
1408            cdr: cdr_totals.cost.round_to_ocpi_scale(),
1409            calculated: total_cost.round_to_ocpi_scale(),
1410        },
1411        total_charging_time_cost: Total {
1412            cdr: cdr_totals.duration_charging_cost.round_to_ocpi_scale(),
1413            calculated: total_costs.duration_charging.round_to_ocpi_scale(),
1414        },
1415        total_time: Total {
1416            cdr: cdr_totals.duration_charging,
1417            calculated: total_time,
1418        },
1419        total_idle_cost: Total {
1420            cdr: cdr_totals.duration_idle_cost.round_to_ocpi_scale(),
1421            calculated: total_costs.duration_idle.round_to_ocpi_scale(),
1422        },
1423        total_idle_time: Total {
1424            cdr: cdr_totals.duration_idle,
1425            calculated: totals.duration_idle,
1426        },
1427        total_energy_cost: Total {
1428            cdr: cdr_totals.energy_cost.round_to_ocpi_scale(),
1429            calculated: total_costs.energy.round_to_ocpi_scale(),
1430        },
1431        total_energy: Total {
1432            cdr: cdr_totals.energy.round_to_ocpi_scale(),
1433            calculated: totals.energy.round_to_ocpi_scale(),
1434        },
1435        total_fixed_cost: Total {
1436            cdr: cdr_totals.fixed_cost.round_to_ocpi_scale(),
1437            calculated: total_costs.fixed.round_to_ocpi_scale(),
1438        },
1439        total_reservation_cost: Total {
1440            cdr: cdr_totals.reservation_cost.round_to_ocpi_scale(),
1441            calculated: None,
1442        },
1443    };
1444
1445    trace!("{report:#?}");
1446
1447    report
1448}
1449
1450/// The step size of three dimensions.
1451///
1452/// Used to represent the totals of a CDR period.
1453#[derive(Debug, Default)]
1454struct StepSize {
1455    duration_charging: Option<PeriodComponent>,
1456    duration_idle: Option<PeriodComponent>,
1457    energy: Option<PeriodComponent>,
1458}
1459
1460/// Return the duration as a `Decimal` amount of seconds.
1461fn delta_as_seconds_dec(delta: TimeDelta) -> Decimal {
1462    Decimal::from(delta.num_milliseconds())
1463        .checked_div(Decimal::from(duration::MILLIS_IN_SEC))
1464        .expect("Can't overflow; See test `as_seconds_dec_should_not_overflow`")
1465}
1466
1467/// Create a `HoursDecimal` from a `Decimal` amount of seconds.
1468fn delta_from_seconds_dec(seconds: Decimal) -> VerdictDeferred<TimeDelta> {
1469    let millis = seconds.saturating_mul(Decimal::from(duration::MILLIS_IN_SEC));
1470    let Ok(millis) = i64::try_from(millis) else {
1471        return Err(warning::ErrorSetDeferred::with_warn(
1472            duration::Warning::Overflow.into(),
1473        ));
1474    };
1475    let Some(delta) = TimeDelta::try_milliseconds(millis) else {
1476        return Err(warning::ErrorSetDeferred::with_warn(
1477            duration::Warning::Overflow.into(),
1478        ));
1479    };
1480    Ok(delta.into_caveat_deferred(warning::SetDeferred::new()))
1481}
1482
1483impl StepSize {
1484    /// Update the values of the `StepSize`.
1485    ///
1486    /// This is based on whether the current CDR [`PeriodNormalized`]
1487    /// consumed a quantity and the tariff has an active `PriceComponent` for the given period.
1488    fn update(
1489        &mut self,
1490        period_index: usize,
1491        period: &PeriodNormalized,
1492        components: &ComponentSet,
1493    ) {
1494        if period.consumed.energy.is_some() {
1495            if let Some(energy) = components.energy.clone() {
1496                self.energy.replace(energy.into_period(period_index));
1497            }
1498        }
1499
1500        if period.consumed.duration_charging.is_some() {
1501            if let Some(duration) = components.duration_charging.clone() {
1502                self.duration_charging
1503                    .replace(duration.into_period(period_index));
1504            }
1505        }
1506
1507        if period.consumed.duration_idle.is_some() {
1508            if let Some(duration) = components.duration_idle.clone() {
1509                self.duration_idle
1510                    .replace(duration.into_period(period_index));
1511            }
1512        }
1513    }
1514
1515    fn apply_time(
1516        &self,
1517        periods: &mut [PeriodReport],
1518        total: TimeDelta,
1519    ) -> VerdictDeferred<TimeDelta> {
1520        let (Some(component), None) = (&self.duration_charging, &self.duration_idle) else {
1521            return Ok(total.into_caveat_deferred(warning::SetDeferred::new()));
1522        };
1523
1524        let PeriodComponent {
1525            period_index,
1526            component,
1527        } = component;
1528
1529        let Some(period) = periods.get_mut(*period_index) else {
1530            error!(period_index, "Invalid period index");
1531            return Err(warning::ErrorSetDeferred::with_warn(Warning::InternalError));
1532        };
1533        let Some(volume) = period.dimensions.duration_charging.billed_volume.as_mut() else {
1534            return Err(warning::ErrorSetDeferred::with_warn(
1535                Warning::DimensionShouldHaveVolume {
1536                    dimension_name: "time",
1537                },
1538            ));
1539        };
1540
1541        duration_step_size(total, volume, component.step_size)
1542    }
1543
1544    fn apply_idle_time(
1545        &self,
1546        periods: &mut [PeriodReport],
1547        total: TimeDelta,
1548    ) -> VerdictDeferred<TimeDelta> {
1549        let warnings = warning::SetDeferred::new();
1550        let Some(component) = &self.duration_idle else {
1551            return Ok(total.into_caveat_deferred(warnings));
1552        };
1553
1554        let PeriodComponent {
1555            period_index,
1556            component,
1557        } = component;
1558
1559        let Some(period) = periods.get_mut(*period_index) else {
1560            error!(period_index, "Invalid period index");
1561            return warnings.bail(Warning::InternalError);
1562        };
1563        let Some(volume) = period.dimensions.duration_idle.billed_volume.as_mut() else {
1564            return warnings.bail(Warning::DimensionShouldHaveVolume {
1565                dimension_name: "parking_time",
1566            });
1567        };
1568
1569        duration_step_size(total, volume, component.step_size)
1570    }
1571
1572    fn apply_energy(
1573        &self,
1574        periods: &mut [PeriodReport],
1575        total_volume: Kwh,
1576    ) -> VerdictDeferred<Kwh> {
1577        let warnings = warning::SetDeferred::new();
1578        let Some(component) = &self.energy else {
1579            return Ok(total_volume.into_caveat_deferred(warnings));
1580        };
1581
1582        let PeriodComponent {
1583            period_index,
1584            component,
1585        } = component;
1586
1587        if component.step_size == 0 {
1588            return Ok(total_volume.into_caveat_deferred(warnings));
1589        }
1590
1591        let Some(period) = periods.get_mut(*period_index) else {
1592            error!(period_index, "Invalid period index");
1593            return warnings.bail(Warning::InternalError);
1594        };
1595        let step_size = Decimal::from(component.step_size);
1596
1597        let Some(period_billed_volume) = period.dimensions.energy.billed_volume.as_mut() else {
1598            return warnings.bail(Warning::DimensionShouldHaveVolume {
1599                dimension_name: "energy",
1600            });
1601        };
1602
1603        let Some(watt_hours) = total_volume.watt_hours().checked_div(step_size) else {
1604            return warnings.bail(duration::Warning::Overflow.into());
1605        };
1606
1607        let total_billed_volume = Kwh::from_watt_hours(watt_hours.ceil().saturating_mul(step_size));
1608        let period_delta_volume = total_billed_volume.saturating_sub(total_volume);
1609        *period_billed_volume = period_billed_volume.saturating_add(period_delta_volume);
1610
1611        Ok(total_billed_volume.into_caveat_deferred(warnings))
1612    }
1613}
1614
1615/// Apply a duration based step size for either `time` or `idle_time`.
1616fn duration_step_size(
1617    total_volume: TimeDelta,
1618    period_billed_volume: &mut TimeDelta,
1619    step_size: u64,
1620) -> VerdictDeferred<TimeDelta> {
1621    if step_size == 0 {
1622        return Ok(total_volume.into_caveat_deferred(warning::SetDeferred::new()));
1623    }
1624
1625    let total_seconds = delta_as_seconds_dec(total_volume);
1626    let step_size = Decimal::from(step_size);
1627
1628    let Some(x) = total_seconds.checked_div(step_size) else {
1629        return Err(warning::ErrorSetDeferred::with_warn(
1630            duration::Warning::Overflow.into(),
1631        ));
1632    };
1633    let total_billed_volume = delta_from_seconds_dec(x.ceil().saturating_mul(step_size))?;
1634
1635    let period_delta_volume = total_billed_volume.saturating_sub(total_volume);
1636    *period_billed_volume = period_billed_volume.saturating_add(period_delta_volume);
1637
1638    Ok(total_billed_volume)
1639}
1640
1641fn parse_cdr<'buf>(cdr: &crate::cdr::Versioned<'buf>) -> Verdict<v221::cdr::WithTariffs<'buf>> {
1642    match cdr.version() {
1643        Version::V211 => {
1644            let cdr = v211::cdr::WithTariffs::from_json(cdr.as_element())?;
1645            Ok(cdr.map(v221::cdr::WithTariffs::from))
1646        }
1647        Version::V221 => v221::cdr::WithTariffs::from_json(cdr.as_element()),
1648    }
1649}