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
3mod tariff;
4mod v211;
5mod v221;
6
7use std::{borrow::Cow, collections::BTreeMap, fmt, ops::Range};
8
9use chrono::{DateTime, Datelike, TimeDelta, Utc};
10use chrono_tz::Tz;
11use rust_decimal::Decimal;
12use tracing::{debug, instrument, trace};
13
14use crate::{
15    country, currency, datetime,
16    duration::{self, Hms},
17    from_warning_set_to,
18    json::{self, FromJson as _},
19    money,
20    number::{self, FromDecimal},
21    string,
22    warning::{self, IntoCaveat},
23    weekday, Ampere, Caveat, Cost, DisplayOption, Kw, Kwh, Money, ParseError, Price,
24    SaturatingAdd as _, SaturatingSub as _, VatApplicable, Verdict, VerdictExt as _, Version,
25    Versioned as _,
26};
27
28use tariff::Tariff;
29
30pub type WarningMap = BTreeMap<String, Vec<WarningKind>>;
31
32/// A normalized/expanded form of a charging period to make the pricing calculation simpler.
33///
34/// The simplicity comes through avoiding having to look up the next period to figure out the end
35/// of the current period.
36#[derive(Debug)]
37struct PeriodNormalized {
38    /// The set of quantities consumed across the duration of the `Period`.
39    consumed: Consumed,
40
41    /// A snapshot of the values of various quantities at the start of the charge period.
42    start_snapshot: TotalsSnapshot,
43
44    /// A snapshot of the values of various quantities at the end of the charge period.
45    end_snapshot: TotalsSnapshot,
46}
47
48/// The set of quantities consumed across the duration of the `Period`.
49#[derive(Clone, Debug)]
50#[cfg_attr(test, derive(Default))]
51pub(crate) struct Consumed {
52    /// The peak current during this period.
53    pub current_max: Option<Ampere>,
54
55    /// The lowest current during this period.
56    pub current_min: Option<Ampere>,
57
58    /// The charging time consumed in this period.
59    pub duration_charging: Option<TimeDelta>,
60
61    /// The parking time consumed in this period.
62    pub duration_parking: Option<TimeDelta>,
63
64    /// The energy consumed in this period.
65    pub energy: Option<Kwh>,
66
67    /// The maximum power reached during this period.
68    pub power_max: Option<Kw>,
69
70    /// The minimum power reached during this period.
71    pub power_min: Option<Kw>,
72}
73
74/// A snapshot of the values of various quantities at the start and end of the charge period.
75#[derive(Clone, Debug)]
76struct TotalsSnapshot {
77    /// The `DateTime` this snapshot of total quantities was taken.
78    date_time: DateTime<Utc>,
79
80    /// The total energy consumed during a charging period.
81    energy: Kwh,
82
83    /// The local timezone.
84    local_timezone: Tz,
85
86    /// The total charging duration during a charging period.
87    duration_charging: TimeDelta,
88
89    /// The total period duration during a charging period.
90    duration_total: TimeDelta,
91}
92
93impl TotalsSnapshot {
94    /// Create a snapshot where all quantities are zero.
95    fn zero(date_time: DateTime<Utc>, local_timezone: Tz) -> Self {
96        Self {
97            date_time,
98            energy: Kwh::zero(),
99            local_timezone,
100            duration_charging: TimeDelta::zero(),
101            duration_total: TimeDelta::zero(),
102        }
103    }
104
105    /// Create a new snapshot based on the current snapshot with consumed quantities added.
106    fn next(&self, consumed: &Consumed, date_time: DateTime<Utc>) -> Self {
107        let duration = date_time.signed_duration_since(self.date_time);
108
109        let mut next = Self {
110            date_time,
111            energy: self.energy,
112            local_timezone: self.local_timezone,
113            duration_charging: self.duration_charging,
114            duration_total: self
115                .duration_total
116                .checked_add(&duration)
117                .unwrap_or(TimeDelta::MAX),
118        };
119
120        if let Some(duration) = consumed.duration_charging {
121            next.duration_charging = next
122                .duration_charging
123                .checked_add(&duration)
124                .unwrap_or(TimeDelta::MAX);
125        }
126
127        if let Some(energy) = consumed.energy {
128            next.energy = next.energy.saturating_add(energy);
129        }
130
131        next
132    }
133
134    /// Return the local time of this snapshot.
135    fn local_time(&self) -> chrono::NaiveTime {
136        self.date_time.with_timezone(&self.local_timezone).time()
137    }
138
139    /// Return the local date of this snapshot.
140    fn local_date(&self) -> chrono::NaiveDate {
141        self.date_time
142            .with_timezone(&self.local_timezone)
143            .date_naive()
144    }
145
146    /// Return the local `Weekday` of this snapshot.
147    fn local_weekday(&self) -> chrono::Weekday {
148        self.date_time.with_timezone(&self.local_timezone).weekday()
149    }
150}
151
152/// Structure containing the charge session priced according to the specified tariff.
153/// The fields prefixed `total` correspond to CDR fields with the same name.
154#[derive(Debug)]
155pub struct Report {
156    /// Warnings from parsing a CDR.
157    ///
158    /// Each entry in the map is an element path and a list of associated warnings.
159    pub warnings: BTreeMap<String, Vec<WarningKind>>,
160
161    /// Charge session details per period.
162    pub periods: Vec<PeriodReport>,
163
164    /// The index of the tariff that was used for pricing.
165    pub tariff_used: TariffOrigin,
166
167    /// A list of reports for each tariff found in the CDR or supplied to the [`cdr::price`](crate::cdr::price) function.
168    ///
169    /// The order of the `tariff::Report`s are the same as the order in which they are given.
170    pub tariff_reports: Vec<TariffReport>,
171
172    /// Time-zone that was either specified or detected.
173    pub timezone: String,
174
175    /* Billed Quantities */
176    /// The total charging time after applying step-size.
177    pub billed_charging_time: Option<TimeDelta>,
178
179    /// The total energy after applying step-size.
180    pub billed_energy: Option<Kwh>,
181
182    /// The total parking time after applying step-size
183    pub billed_parking_time: Option<TimeDelta>,
184
185    /* Totals */
186    /// Total duration of the charging session (excluding not charging), in hours.
187    ///
188    /// This is a total that has no direct source field in the `CDR` as it is calculated in the
189    /// [`cdr::price`](crate::cdr::price) function.
190    pub total_charging_time: Option<TimeDelta>,
191
192    /// Total energy charged, in kWh.
193    pub total_energy: Total<Kwh, Option<Kwh>>,
194
195    /// Total duration of the charging session where the EV was not charging (no energy was transferred between EVSE and EV).
196    pub total_parking_time: Total<Option<TimeDelta>>,
197
198    /// Total duration of the charging session (including the duration of charging and not charging).
199    pub total_time: Total<TimeDelta>,
200
201    /* Costs */
202    /// Total sum of all the costs of this transaction in the specified currency.
203    pub total_cost: Total<Price, Option<Price>>,
204
205    /// Total sum of all the cost of all the energy used, in the specified currency.
206    pub total_energy_cost: Total<Option<Price>>,
207
208    /// Total sum of all the fixed costs in the specified currency, except fixed price components of parking and reservation. The cost not depending on amount of time/energy used etc. Can contain costs like a start tariff.
209    pub total_fixed_cost: Total<Option<Price>>,
210
211    /// Total sum of all the cost related to parking of this transaction, including fixed price components, in the specified currency.
212    pub total_parking_cost: Total<Option<Price>>,
213
214    /// Total sum of all the cost related to a reservation of a Charge Point, including fixed price components, in the specified currency.
215    pub total_reservation_cost: Total<Option<Price>>,
216
217    /// Total sum of all the cost related to duration of charging during this transaction, in the specified currency.
218    pub total_time_cost: Total<Option<Price>>,
219}
220
221/// The warnings that happen when pricing a CDR.
222#[derive(Debug)]
223pub enum WarningKind {
224    /// The CDR location is not a valid ISO 3166-1 alpha-3 code.
225    Country(country::WarningKind),
226    Currency(currency::WarningKind),
227    DateTime(datetime::WarningKind),
228    Decode(json::decode::WarningKind),
229    Duration(duration::WarningKind),
230
231    /// A field in the tariff doesn't have the expected type.
232    FieldInvalidType {
233        /// The type that the given field should have according to the schema.
234        expected_type: json::ValueKind,
235    },
236
237    /// A field in the tariff doesn't have the expected value.
238    FieldInvalidValue {
239        /// The value encountered.
240        value: String,
241
242        /// A message about what values are expected for this field.
243        message: Cow<'static, str>,
244    },
245
246    /// The given field is required.
247    FieldRequired {
248        field_name: Cow<'static, str>,
249    },
250
251    Money(money::WarningKind),
252
253    /// The CDR has no charging periods.
254    NoPeriods,
255
256    Number(number::WarningKind),
257
258    /// The `start_date_time` of at least one of the `charging_periods` is outside of the
259    /// CDR's `start_date_time`-`end_date_time` range.
260    PeriodsOutsideStartEndDateTime {
261        cdr_range: Range<DateTime<Utc>>,
262        period_range: PeriodRange,
263    },
264
265    String(string::WarningKind),
266    Weekday(weekday::WarningKind),
267}
268
269impl WarningKind {
270    /// Create a new `WarningKind::FieldInvalidValue` where the field is built from the given `json::Element`.
271    fn field_invalid_value(
272        value: impl Into<String>,
273        message: impl Into<Cow<'static, str>>,
274    ) -> Self {
275        WarningKind::FieldInvalidValue {
276            value: value.into(),
277            message: message.into(),
278        }
279    }
280}
281
282impl fmt::Display for WarningKind {
283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284        match self {
285            Self::String(warning_kind) => write!(f, "{warning_kind}"),
286            Self::Country(warning_kind) => write!(f, "{warning_kind}"),
287            Self::Currency(warning_kind) => write!(f, "{warning_kind}"),
288            Self::DateTime(warning_kind) => write!(f, "{warning_kind}"),
289            Self::Decode(warning_kind) => write!(f, "{warning_kind}"),
290            Self::Duration(warning_kind) => write!(f, "{warning_kind}"),
291            Self::FieldInvalidType { expected_type } => {
292                write!(f, "Field has invalid type. Expected type `{expected_type}`")
293            }
294            Self::FieldInvalidValue { value, message } => {
295                write!(f, "Field has invalid value `{value}`: {message}")
296            }
297            Self::FieldRequired { field_name } => {
298                write!(f, "Field is required: {field_name}")
299            }
300            Self::Money(warning_kind) => write!(f, "{warning_kind}"),
301            Self::NoPeriods => f.write_str("The CDR has no charging periods"),
302            Self::Number(warning_kind) => write!(f, "{warning_kind}"),
303            Self::PeriodsOutsideStartEndDateTime {
304                cdr_range,
305                period_range,
306            } => {
307                write!(f, "The CDR's charging period time range is not contained within the `start_date_time` and `end_date_time`; cdr_range: {}-{}, period_range: {}", cdr_range.start, cdr_range.end, period_range)
308            }
309            Self::Weekday(warning_kind) => write!(f, "{warning_kind}"),
310        }
311    }
312}
313
314impl warning::Kind for WarningKind {
315    fn id(&self) -> Cow<'static, str> {
316        match self {
317            Self::String(kind) => kind.id(),
318            Self::Country(kind) => kind.id(),
319            Self::Currency(kind) => kind.id(),
320            Self::DateTime(kind) => kind.id(),
321            Self::Decode(kind) => kind.id(),
322            Self::Duration(kind) => kind.id(),
323            Self::FieldInvalidType { .. } => "field_invalid_type".into(),
324            Self::FieldInvalidValue { .. } => "field_invalid_value".into(),
325            Self::FieldRequired { .. } => "field_required".into(),
326            Self::Money(kind) => kind.id(),
327            Self::NoPeriods => "no_periods".into(),
328            Self::Number(kind) => kind.id(),
329            WarningKind::PeriodsOutsideStartEndDateTime { .. } => {
330                "periods_outside_start_end_date_time".into()
331            }
332            Self::Weekday(kind) => kind.id(),
333        }
334    }
335}
336
337impl From<country::WarningKind> for WarningKind {
338    fn from(warn_kind: country::WarningKind) -> Self {
339        Self::Country(warn_kind)
340    }
341}
342
343impl From<currency::WarningKind> for WarningKind {
344    fn from(warn_kind: currency::WarningKind) -> Self {
345        Self::Currency(warn_kind)
346    }
347}
348
349impl From<datetime::WarningKind> for WarningKind {
350    fn from(warn_kind: datetime::WarningKind) -> Self {
351        Self::DateTime(warn_kind)
352    }
353}
354
355impl From<duration::WarningKind> for WarningKind {
356    fn from(warn_kind: duration::WarningKind) -> Self {
357        Self::Duration(warn_kind)
358    }
359}
360
361impl From<json::decode::WarningKind> for WarningKind {
362    fn from(warn_kind: json::decode::WarningKind) -> Self {
363        Self::Decode(warn_kind)
364    }
365}
366
367impl From<money::WarningKind> for WarningKind {
368    fn from(warn_kind: money::WarningKind) -> Self {
369        Self::Money(warn_kind)
370    }
371}
372
373impl From<number::WarningKind> for WarningKind {
374    fn from(warn_kind: number::WarningKind) -> Self {
375        Self::Number(warn_kind)
376    }
377}
378
379impl From<string::WarningKind> for WarningKind {
380    fn from(warn_kind: string::WarningKind) -> Self {
381        Self::String(warn_kind)
382    }
383}
384
385impl From<weekday::WarningKind> for WarningKind {
386    fn from(warn_kind: weekday::WarningKind) -> Self {
387        Self::Weekday(warn_kind)
388    }
389}
390
391from_warning_set_to!(string::WarningKind => WarningKind);
392from_warning_set_to!(country::WarningKind => WarningKind);
393from_warning_set_to!(currency::WarningKind => WarningKind);
394from_warning_set_to!(datetime::WarningKind => WarningKind);
395from_warning_set_to!(duration::WarningKind => WarningKind);
396from_warning_set_to!(weekday::WarningKind => WarningKind);
397from_warning_set_to!(number::WarningKind => WarningKind);
398from_warning_set_to!(money::WarningKind => WarningKind);
399
400/// A report of parsing and using the referenced tariff to price a CDR.
401#[derive(Debug)]
402pub struct TariffReport {
403    /// The id of the tariff.
404    pub origin: TariffOrigin,
405
406    /// Warnings from parsing a tariff.
407    ///
408    /// Each entry in the map is an element path and a list of associated warnings.
409    pub warnings: BTreeMap<String, Vec<crate::tariff::WarningKind>>,
410}
411
412/// The origin data for a tariff.
413#[derive(Clone, Debug)]
414pub struct TariffOrigin {
415    /// The index of the tariff in the CDR JSON or in the list of override tariffs.
416    pub index: usize,
417
418    /// The value of the `id` field in the tariff JSON.
419    pub id: String,
420}
421
422/// A CDR charge period in a normalized form ready for pricing.
423#[derive(Debug)]
424pub(crate) struct Period {
425    /// The start time of this period.
426    pub start_date_time: DateTime<Utc>,
427
428    /// The quantities consumed during this period.
429    pub consumed: Consumed,
430}
431
432/// A structure containing a report for each dimension.
433#[derive(Debug)]
434pub struct Dimensions {
435    /// Energy consumed.
436    pub energy: Dimension<Kwh>,
437
438    /// Flat fee without unit for `step_size`.
439    pub flat: Dimension<()>,
440
441    /// Duration of time charging.
442    pub duration_charging: Dimension<TimeDelta>,
443
444    /// Duration of time not charging.
445    pub duration_parking: Dimension<TimeDelta>,
446}
447
448impl Dimensions {
449    fn new(components: ComponentSet, consumed: &Consumed) -> Self {
450        Self {
451            energy: Dimension::new(components.energy, consumed.energy),
452            flat: Dimension::new(components.flat, Some(())),
453            duration_charging: Dimension::new(
454                components.duration_charging,
455                consumed.duration_charging,
456            ),
457            duration_parking: Dimension::new(
458                components.duration_parking,
459                consumed.duration_parking,
460            ),
461        }
462    }
463}
464
465#[derive(Debug)]
466/// A report for a single dimension during a single period.
467pub struct Dimension<V> {
468    /// The price component that was active during this period for this dimension.
469    /// It could be that no price component was active during this period for this dimension in
470    /// which case `price` is `None`.
471    pub price: Option<Component>,
472
473    /// The volume of this dimension during this period, as received in the provided charge detail record.
474    /// It could be that no volume was provided during this period for this dimension in which case
475    /// the `volume` is `None`.
476    pub volume: Option<V>,
477
478    /// This field contains the optional value of `volume` after a potential step size was applied.
479    /// Step size is applied over the total volume during the whole session of a dimension. But the
480    /// resulting additional volume should be billed according to the price component in this
481    /// period.
482    ///
483    /// If no step-size was applied for this period, the volume is exactly equal to the `volume`
484    /// field.
485    pub billed_volume: Option<V>,
486}
487
488impl<V> Dimension<V>
489where
490    V: Copy,
491{
492    fn new(price_component: Option<Component>, volume: Option<V>) -> Self {
493        Self {
494            price: price_component,
495            volume,
496            billed_volume: volume,
497        }
498    }
499}
500
501impl<V: Cost> Dimension<V> {
502    /// The total cost of this dimension during a period.
503    pub fn cost(&self) -> Option<Price> {
504        if let (Some(volume), Some(price_component)) = (&self.billed_volume, &self.price) {
505            let excl_vat = volume.cost(Money::from_decimal(price_component.price));
506
507            let incl_vat = match price_component.vat {
508                VatApplicable::Applicable(vat) => Some(excl_vat.apply_vat(vat)),
509                VatApplicable::Inapplicable => Some(excl_vat),
510                VatApplicable::Unknown => None,
511            };
512
513            Some(Price { excl_vat, incl_vat })
514        } else {
515            None
516        }
517    }
518}
519
520/// A set of price `Component`s, one for each dimension.
521///
522/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#142-pricecomponent-class>
523/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#145-tariffdimensiontype-enum>
524#[derive(Debug)]
525pub struct ComponentSet {
526    /// Energy consumed.
527    pub energy: Option<Component>,
528
529    /// Flat fee without unit for `step_size`.
530    pub flat: Option<Component>,
531
532    /// Duration of time charging.
533    pub duration_charging: Option<Component>,
534
535    /// Duration of time not charging.
536    pub duration_parking: Option<Component>,
537}
538
539impl ComponentSet {
540    fn new() -> Self {
541        Self {
542            energy: None,
543            flat: None,
544            duration_charging: None,
545            duration_parking: None,
546        }
547    }
548
549    /// Returns true if all components are `Some`.
550    fn has_all_components(&self) -> bool {
551        let Self {
552            energy,
553            flat,
554            duration_charging,
555            duration_parking,
556        } = self;
557
558        flat.is_some()
559            && energy.is_some()
560            && duration_parking.is_some()
561            && duration_charging.is_some()
562    }
563}
564
565/// A Price Component describes how a certain amount of a certain dimension being consumed
566/// translates into an amount of money owed.
567///
568/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#142-pricecomponent-class>
569#[derive(Clone, Debug)]
570pub struct Component {
571    /// The index of the tariff this `Component` lives in.
572    pub tariff_element_index: usize,
573
574    /// Price per unit (excl. VAT) for this dimension.
575    pub price: Decimal,
576
577    /// Applicable VAT percentage for this tariff dimension. If omitted, no VAT is applicable.
578    /// Not providing a VAT is different from 0% VAT, which would be a value of 0.0 here.
579    pub vat: VatApplicable,
580
581    /// Minimum amount to be billed. That is, the dimension will be billed in this `step_size` blocks.
582    /// Consumed amounts are rounded up to the smallest multiple of `step_size` that is greater than
583    /// the consumed amount.
584    ///
585    /// For example: if type is TIME and `step_size` has a value of 300, then time will be billed in
586    /// blocks of 5 minutes. If 6 minutes were consumed, 10 minutes (2 blocks of `step_size`) will
587    /// be billed.
588    pub step_size: u64,
589}
590
591impl Component {
592    fn new(component: &crate::tariff::v221::PriceComponent, tariff_element_index: usize) -> Self {
593        let crate::tariff::v221::PriceComponent {
594            price,
595            vat,
596            step_size,
597            dimension_type: _,
598        } = component;
599
600        Self {
601            tariff_element_index,
602            price: *price,
603            vat: *vat,
604            step_size: *step_size,
605        }
606    }
607}
608
609/// A related source and calculated pair of total amounts.
610///
611/// This is used to express the source and calculated amounts for the total fields of a `CDR`.
612///
613/// - `total_cost`
614/// - `total_fixed_cost`
615/// - `total_energy`
616/// - `total_energy_cost`
617/// - `total_time`
618/// - `total_time_cost`
619/// - `total_parking_time`
620/// - `total_parking_cost`
621/// - `total_reservation_cost`
622#[derive(Debug)]
623pub struct Total<TCdr, TCalc = TCdr> {
624    /// The source value from the `CDR`.
625    pub cdr: TCdr,
626
627    /// The value calculated by the [`cdr::price`](crate::cdr::price) function.
628    pub calculated: TCalc,
629}
630
631/// Possible errors when pricing a charge session.
632#[derive(Debug)]
633pub enum Error {
634    /// An error occurred while parsing a CDR.
635    Cdr(warning::Set<WarningKind>),
636
637    /// An error occurred while deserializing a `CDR` or tariff.
638    Parse(ParseError),
639
640    /// The given dimension should have a volume
641    DimensionShouldHaveVolume { dimension_name: &'static str },
642
643    /// A numeric overflow occurred while creating a duration.
644    DurationOverflow,
645
646    /// An internal programming error.
647    Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
648
649    /// No valid tariff has been found in the list of provided tariffs.
650    /// The tariff list can be sourced from either the tariffs contained in the CDR or from a list
651    /// provided by the caller.
652    ///
653    /// A valid tariff must have a start date-time before the start of the session and an end
654    /// date-time after the start of the session.
655    ///
656    /// If the CDR does not contain any tariffs consider providing a them using [`TariffSource`]
657    /// when calling [`cdr::price`](crate::cdr::price).
658    NoValidTariff,
659
660    /// Converting the `tariff::Versioned` into a structured `tariff::v221::Tariff` caused an
661    /// unrecoverable error.
662    Tariff(warning::Set<crate::tariff::WarningKind>),
663}
664
665impl From<InvalidPeriodIndex> for Error {
666    fn from(err: InvalidPeriodIndex) -> Self {
667        Self::Internal(err.into())
668    }
669}
670
671#[derive(Debug)]
672struct InvalidPeriodIndex(&'static str);
673
674impl std::error::Error for InvalidPeriodIndex {}
675
676impl fmt::Display for InvalidPeriodIndex {
677    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
678        write!(f, "Invalid index for period `{}`", self.0)
679    }
680}
681
682/// The range of time the CDR periods span.
683#[derive(Debug)]
684pub enum PeriodRange {
685    /// There are many periods in the CDR and so the range is from the `start_date_time` of the first to
686    /// the `start_date_time` of the last.
687    Many(Range<DateTime<Utc>>),
688
689    /// There is one period in the CDR and so one `start_date_time`.
690    Single(DateTime<Utc>),
691}
692
693impl fmt::Display for PeriodRange {
694    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
695        match self {
696            PeriodRange::Many(Range { start, end }) => write!(f, "{start}-{end}"),
697            PeriodRange::Single(date_time) => write!(f, "{date_time}"),
698        }
699    }
700}
701
702impl From<ParseError> for Error {
703    fn from(err: ParseError) -> Self {
704        Error::Parse(err)
705    }
706}
707
708impl From<duration::Error> for Error {
709    fn from(err: duration::Error) -> Self {
710        match err {
711            duration::Error::Overflow => Self::DurationOverflow,
712        }
713    }
714}
715
716impl std::error::Error for Error {
717    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
718        if let Error::Internal(err) = self {
719            Some(&**err)
720        } else {
721            None
722        }
723    }
724}
725
726impl fmt::Display for Error {
727    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
728        match self {
729            Self::Cdr(warnings) => {
730                write!(f, "CDR warnings: {warnings:?}")
731            }
732            Self::Parse(err) => {
733                write!(f, "{err}")
734            }
735            Self::DimensionShouldHaveVolume { dimension_name } => {
736                write!(f, "Dimension `{dimension_name}` should have volume")
737            }
738            Self::DurationOverflow => {
739                f.write_str("A numeric overflow occurred while creating a duration")
740            }
741            Self::Internal(err) => {
742                write!(f, "Internal: {err}")
743            }
744            Self::NoValidTariff => {
745                f.write_str("No valid tariff has been found in the list of provided tariffs")
746            }
747            Self::Tariff(warnings) => {
748                write!(f, "Tariff warnings: {warnings:?}")
749            }
750        }
751    }
752}
753
754#[derive(Debug)]
755enum InternalError {
756    InvalidPeriodIndex {
757        index: usize,
758        field_name: &'static str,
759    },
760}
761
762impl std::error::Error for InternalError {}
763
764impl From<InternalError> for Error {
765    fn from(err: InternalError) -> Self {
766        Error::Internal(Box::new(err))
767    }
768}
769
770impl fmt::Display for InternalError {
771    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
772        match self {
773            InternalError::InvalidPeriodIndex { field_name, index } => {
774                write!(
775                    f,
776                    "Invalid period index for `{field_name}`; index: `{index}`"
777                )
778            }
779        }
780    }
781}
782
783/// Where should the tariffs come from when pricing a `CDR`.
784///
785/// Used with [`cdr::price`](crate::cdr::price).
786#[derive(Debug)]
787pub enum TariffSource<'buf> {
788    /// Use the tariffs from the `CDR`.
789    UseCdr,
790
791    /// Ignore the tariffs from the `CDR` and use these instead
792    Override(Vec<crate::tariff::Versioned<'buf>>),
793}
794
795#[instrument(skip_all)]
796pub(super) fn cdr(
797    cdr_elem: &crate::cdr::Versioned<'_>,
798    tariff_source: TariffSource<'_>,
799    timezone: Tz,
800) -> Result<Report, Error> {
801    let cdr = parse_cdr(cdr_elem).map_err(Error::Cdr)?;
802
803    match tariff_source {
804        TariffSource::UseCdr => {
805            let (v221::cdr::WithTariffs { cdr, tariffs }, warnings) = cdr.into_parts();
806            debug!("Using tariffs from CDR");
807            let tariffs = tariffs
808                .iter()
809                .map(|elem| {
810                    let tariff = crate::tariff::v211::Tariff::from_json(elem);
811                    tariff.map_caveat(crate::tariff::v221::Tariff::from)
812                })
813                .collect::<Result<Vec<_>, _>>()
814                .map_err(Error::Tariff)?;
815
816            let cdr = cdr.into_caveat(warnings);
817
818            Ok(price_v221_cdr_with_tariffs(
819                cdr_elem, cdr, tariffs, timezone,
820            )?)
821        }
822        TariffSource::Override(tariffs) => {
823            let cdr = cdr.map(v221::cdr::WithTariffs::discard_tariffs);
824
825            debug!("Using override tariffs");
826            let tariffs = tariffs
827                .iter()
828                .map(parse_tariff)
829                .collect::<Result<Vec<_>, _>>()
830                .map_err(Error::Tariff)?;
831
832            Ok(price_v221_cdr_with_tariffs(
833                cdr_elem, cdr, tariffs, timezone,
834            )?)
835        }
836    }
837}
838
839fn parse_tariff<'caller: 'buf, 'buf>(
840    tariff: &'caller crate::tariff::Versioned<'buf>,
841) -> Verdict<crate::tariff::v221::Tariff<'buf>, crate::tariff::WarningKind> {
842    match tariff.version() {
843        Version::V211 => {
844            let tariff = crate::tariff::v211::Tariff::from_json(tariff.as_element());
845            tariff.map_caveat(crate::tariff::v221::Tariff::from)
846        }
847        Version::V221 => crate::tariff::v221::Tariff::from_json(tariff.as_element()),
848    }
849}
850/// Price a single charge-session using a tariff selected from a list.
851///
852/// Returns a report containing the totals, subtotals, and a breakdown of the calculation.
853/// Price a single charge-session using a single tariff.
854///
855/// Returns a report containing the totals, subtotals, and a breakdown of the calculation.
856fn price_v221_cdr_with_tariffs(
857    cdr_elem: &crate::cdr::Versioned<'_>,
858    cdr: Caveat<v221::Cdr, WarningKind>,
859    tariffs: Vec<Caveat<crate::tariff::v221::Tariff<'_>, crate::tariff::WarningKind>>,
860    timezone: Tz,
861) -> Result<Report, Error> {
862    debug!(?timezone, version = ?cdr_elem.version(), "Pricing CDR");
863
864    let (tariff_reports, tariffs): (Vec<_>, Vec<_>) = tariffs
865        .into_iter()
866        .enumerate()
867        .map(|(index, tariff)| {
868            let (tariff, warnings) = tariff.into_parts();
869            let warnings = {
870                warnings
871                    .into_group_by_elem(cdr_elem.as_element())
872                    .map(|warning::IntoGroup { element, warnings }| {
873                        (element.path().to_string(), warnings)
874                    })
875                    .collect()
876            };
877            (
878                TariffReport {
879                    origin: TariffOrigin {
880                        index,
881                        id: tariff.id.to_string(),
882                    },
883                    warnings,
884                },
885                tariff,
886            )
887        })
888        .unzip();
889
890    debug!(tariffs = ?tariffs.iter().map(|t| t.id).collect::<Vec<_>>(), "Found tariffs(by id) in CDR");
891
892    let tariffs_normalized = tariff::normalize_all(&tariffs);
893    let (tariff_index, tariff) = tariff::find_first_active(tariffs_normalized, cdr.start_date_time)
894        .ok_or(Error::NoValidTariff)?;
895
896    debug!(tariff_index, id = ?tariff.id(), "Found active tariff");
897    debug!(%timezone, "Found timezone");
898
899    let cs_periods = v221::cdr::normalize_periods(&cdr, timezone)?;
900    let price_cdr_report = price_periods(&cs_periods, &tariff)?;
901
902    Ok(generate_report(
903        cdr_elem,
904        cdr,
905        timezone,
906        tariff_reports,
907        price_cdr_report,
908        TariffOrigin {
909            index: tariff_index,
910            id: tariff.id().to_string(),
911        },
912    ))
913}
914
915/// Price a list of normalized [`Period`]s using a [`Versioned`](crate::tariff::Versioned) tariff.
916pub(crate) fn periods(
917    end_date_time: DateTime<Utc>,
918    timezone: Tz,
919    tariff: &crate::tariff::v221::Tariff<'_>,
920    periods: &mut [Period],
921) -> Result<PeriodsReport, Error> {
922    // Make sure the periods are sorted by time as the start date of one period determines the end
923    // date of the previous period.
924    periods.sort_by_key(|p| p.start_date_time);
925    let mut out_periods = Vec::<PeriodNormalized>::new();
926
927    for (index, period) in periods.iter().enumerate() {
928        trace!(index, "processing\n{period:#?}");
929
930        let next_index = index + 1;
931
932        let end_date_time = if let Some(next_period) = periods.get(next_index) {
933            next_period.start_date_time
934        } else {
935            end_date_time
936        };
937
938        let next = if let Some(last) = out_periods.last() {
939            let start_snapshot = last.end_snapshot.clone();
940            let end_snapshot = start_snapshot.next(&period.consumed, end_date_time);
941
942            let period = PeriodNormalized {
943                consumed: period.consumed.clone(),
944                start_snapshot,
945                end_snapshot,
946            };
947            trace!("Adding new period based on the last added\n{period:#?}\n{last:#?}");
948            period
949        } else {
950            let start_snapshot = TotalsSnapshot::zero(period.start_date_time, timezone);
951            let end_snapshot = start_snapshot.next(&period.consumed, end_date_time);
952
953            let period = PeriodNormalized {
954                consumed: period.consumed.clone(),
955                start_snapshot,
956                end_snapshot,
957            };
958            trace!("Adding new period\n{period:#?}");
959            period
960        };
961
962        out_periods.push(next);
963    }
964
965    let tariff = Tariff::from_v221(tariff);
966    price_periods(&out_periods, &tariff)
967}
968
969/// Price the given set of CDR periods using a normalized `Tariff`.
970fn price_periods(periods: &[PeriodNormalized], tariff: &Tariff) -> Result<PeriodsReport, Error> {
971    debug!(count = periods.len(), "Pricing CDR periods");
972
973    if tracing::enabled!(tracing::Level::TRACE) {
974        trace!("# CDR period list:");
975        for period in periods {
976            trace!("{period:#?}");
977        }
978    }
979
980    let period_totals = period_totals(periods, tariff);
981    let (billable, periods, totals) = period_totals.calculate_billed()?;
982    let total_costs = total_costs(&periods, tariff);
983
984    Ok(PeriodsReport {
985        billable,
986        periods,
987        totals,
988        total_costs,
989    })
990}
991
992/// The internal report generated from the [`periods`] fn.
993pub(crate) struct PeriodsReport {
994    /// The billable dimensions calculated by applying the step-size to each dimension.
995    pub billable: Billable,
996
997    /// A list of reports for each charging period that occurred during a session.
998    pub periods: Vec<PeriodReport>,
999
1000    /// The totals for each dimension.
1001    pub totals: Totals,
1002
1003    /// The total costs for each dimension.
1004    pub total_costs: TotalCosts,
1005}
1006
1007/// A report for a single charging period that occurred during a session.
1008///
1009/// A charging period is a period of time that has relevance for the total costs of a CDR.
1010/// During a charging session, different parameters change all the time, like the amount of energy used,
1011/// 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.
1012#[derive(Debug)]
1013pub struct PeriodReport {
1014    /// The start time of this period.
1015    pub start_date_time: DateTime<Utc>,
1016
1017    /// The end time of this period.
1018    pub end_date_time: DateTime<Utc>,
1019
1020    /// A structure that contains results per dimension.
1021    pub dimensions: Dimensions,
1022}
1023
1024impl PeriodReport {
1025    fn new(period: &PeriodNormalized, dimensions: Dimensions) -> Self {
1026        Self {
1027            start_date_time: period.start_snapshot.date_time,
1028            end_date_time: period.end_snapshot.date_time,
1029            dimensions,
1030        }
1031    }
1032
1033    /// The total cost of all dimensions in this period.
1034    pub fn cost(&self) -> Option<Price> {
1035        [
1036            self.dimensions.duration_charging.cost(),
1037            self.dimensions.duration_parking.cost(),
1038            self.dimensions.flat.cost(),
1039            self.dimensions.energy.cost(),
1040        ]
1041        .into_iter()
1042        .fold(None, |accum, next| {
1043            if accum.is_none() && next.is_none() {
1044                None
1045            } else {
1046                Some(
1047                    accum
1048                        .unwrap_or_default()
1049                        .saturating_add(next.unwrap_or_default()),
1050                )
1051            }
1052        })
1053    }
1054}
1055
1056/// The result of normalizing the CDR charging periods.
1057struct PeriodTotals {
1058    /// The list of normalized periods.
1059    periods: Vec<PeriodReport>,
1060
1061    /// The computed step size.
1062    step_size: StepSize,
1063
1064    /// The totals for each dimension.
1065    totals: Totals,
1066}
1067
1068/// The totals for each dimension.
1069#[derive(Debug, Default)]
1070pub(crate) struct Totals {
1071    /// The total energy used during a session.
1072    pub energy: Option<Kwh>,
1073
1074    /// The total charging time used during a session.
1075    pub duration_charging: Option<TimeDelta>,
1076
1077    /// The total parking time used during a session.
1078    pub duration_parking: Option<TimeDelta>,
1079}
1080
1081impl PeriodTotals {
1082    /// Calculate the billed dimensions by applying the step-size to each dimension.
1083    ///
1084    /// Applying the step size can mutate the dimension values contained in the `Period`.
1085    fn calculate_billed(self) -> Result<(Billable, Vec<PeriodReport>, Totals), Error> {
1086        let Self {
1087            mut periods,
1088            step_size,
1089            totals,
1090        } = self;
1091        let charging_time = totals
1092            .duration_charging
1093            .map(|dt| step_size.apply_time(&mut periods, dt))
1094            .transpose()?;
1095        let energy = totals
1096            .energy
1097            .map(|kwh| step_size.apply_energy(&mut periods, kwh))
1098            .transpose()?;
1099        let parking_time = totals
1100            .duration_parking
1101            .map(|dt| step_size.apply_parking_time(&mut periods, dt))
1102            .transpose()?;
1103        let billed = Billable {
1104            charging_time,
1105            energy,
1106            parking_time,
1107        };
1108        Ok((billed, periods, totals))
1109    }
1110}
1111
1112/// The billable dimensions calculated by applying the step-size to each dimension.
1113#[derive(Debug)]
1114pub(crate) struct Billable {
1115    /// The billable charging time.
1116    charging_time: Option<TimeDelta>,
1117
1118    /// The billable energy use.
1119    energy: Option<Kwh>,
1120
1121    /// The billable parking time.
1122    parking_time: Option<TimeDelta>,
1123}
1124
1125/// Map the `session::ChargePeriod`s to a normalized `Period` and calculate the step size and
1126/// totals for each dimension.
1127fn period_totals(periods: &[PeriodNormalized], tariff: &Tariff) -> PeriodTotals {
1128    let mut has_flat_fee = false;
1129    let mut step_size = StepSize::new();
1130    let mut totals = Totals::default();
1131
1132    debug!(
1133        tariff_id = tariff.id(),
1134        period_count = periods.len(),
1135        "Accumulating dimension totals for each period"
1136    );
1137
1138    let periods = periods
1139        .iter()
1140        .enumerate()
1141        .map(|(index, period)| {
1142            let mut component_set = tariff.active_components(period);
1143            trace!(
1144                index,
1145                "Creating charge period with Dimension\n{period:#?}\n{component_set:#?}"
1146            );
1147
1148            if component_set.flat.is_some() {
1149                if has_flat_fee {
1150                    component_set.flat = None;
1151                } else {
1152                    has_flat_fee = true;
1153                }
1154            }
1155
1156            step_size.update(index, &component_set, period);
1157
1158            trace!(period_index = index, "Step size updated\n{step_size:#?}");
1159
1160            let dimensions = Dimensions::new(component_set, &period.consumed);
1161
1162            trace!(period_index = index, "Dimensions created\n{dimensions:#?}");
1163
1164            if let Some(dt) = dimensions.duration_charging.volume {
1165                let acc = totals.duration_charging.get_or_insert_default();
1166                *acc = acc.saturating_add(dt);
1167            }
1168
1169            if let Some(kwh) = dimensions.energy.volume {
1170                let acc = totals.energy.get_or_insert_default();
1171                *acc = acc.saturating_add(kwh);
1172            }
1173
1174            if let Some(dt) = dimensions.duration_parking.volume {
1175                let acc = totals.duration_parking.get_or_insert_default();
1176                *acc = acc.saturating_add(dt);
1177            }
1178
1179            trace!(period_index = index, ?totals, "Update totals");
1180
1181            PeriodReport::new(period, dimensions)
1182        })
1183        .collect::<Vec<_>>();
1184
1185    PeriodTotals {
1186        periods,
1187        step_size,
1188        totals,
1189    }
1190}
1191
1192/// The total costs for each dimension.
1193#[derive(Debug, Default)]
1194pub(crate) struct TotalCosts {
1195    /// The [`Price`] for all energy used during a session.
1196    pub energy: Option<Price>,
1197
1198    /// The [`Price`] for all flat rates applied during a session.
1199    pub fixed: Option<Price>,
1200
1201    /// The [`Price`] for all charging time used during a session.
1202    pub duration_charging: Option<Price>,
1203
1204    /// The [`Price`] for all parking time used during a session.
1205    pub duration_parking: Option<Price>,
1206}
1207
1208impl TotalCosts {
1209    /// Summate each dimension total into a single total.
1210    ///
1211    /// Return `None` if there are no cost dimensions otherwise return `Some`.
1212    pub(crate) fn total(&self) -> Option<Price> {
1213        let Self {
1214            energy,
1215            fixed,
1216            duration_charging,
1217            duration_parking,
1218        } = self;
1219        debug!(
1220            energy = %DisplayOption(*energy),
1221            fixed = %DisplayOption(*fixed),
1222            duration_charging = %DisplayOption(*duration_charging),
1223            duration_parking = %DisplayOption(*duration_parking),
1224            "Calculating total costs."
1225        );
1226        [energy, fixed, duration_charging, duration_parking]
1227            .into_iter()
1228            .fold(None, |accum: Option<Price>, next| match (accum, next) {
1229                (None, None) => None,
1230                _ => Some(
1231                    accum
1232                        .unwrap_or_default()
1233                        .saturating_add(next.unwrap_or_default()),
1234                ),
1235            })
1236    }
1237}
1238
1239/// Accumulate total costs per dimension across all periods.
1240fn total_costs(periods: &[PeriodReport], tariff: &Tariff) -> TotalCosts {
1241    let mut total_costs = TotalCosts::default();
1242
1243    debug!(
1244        tariff_id = tariff.id(),
1245        period_count = periods.len(),
1246        "Accumulating dimension costs for each period"
1247    );
1248    for (index, period) in periods.iter().enumerate() {
1249        let dimensions = &period.dimensions;
1250
1251        trace!(period_index = index, "Processing period");
1252
1253        let energy_cost = dimensions.energy.cost();
1254        let fixed_cost = dimensions.flat.cost();
1255        let duration_charging_cost = dimensions.duration_charging.cost();
1256        let duration_parking_cost = dimensions.duration_parking.cost();
1257
1258        trace!(?total_costs.energy, ?energy_cost, "Energy cost");
1259        trace!(?total_costs.duration_charging, ?duration_charging_cost, "Energy cost");
1260        trace!(?total_costs.duration_parking, ?duration_parking_cost, "Energy cost");
1261        trace!(?total_costs.fixed, ?fixed_cost, "Energy cost");
1262
1263        total_costs.energy = match (total_costs.energy, energy_cost) {
1264            (None, None) => None,
1265            (total, period) => Some(
1266                total
1267                    .unwrap_or_default()
1268                    .saturating_add(period.unwrap_or_default()),
1269            ),
1270        };
1271
1272        total_costs.duration_charging =
1273            match (total_costs.duration_charging, duration_charging_cost) {
1274                (None, None) => None,
1275                (total, period) => Some(
1276                    total
1277                        .unwrap_or_default()
1278                        .saturating_add(period.unwrap_or_default()),
1279                ),
1280            };
1281
1282        total_costs.duration_parking = match (total_costs.duration_parking, duration_parking_cost) {
1283            (None, None) => None,
1284            (total, period) => Some(
1285                total
1286                    .unwrap_or_default()
1287                    .saturating_add(period.unwrap_or_default()),
1288            ),
1289        };
1290
1291        total_costs.fixed = match (total_costs.fixed, fixed_cost) {
1292            (None, None) => None,
1293            (total, period) => Some(
1294                total
1295                    .unwrap_or_default()
1296                    .saturating_add(period.unwrap_or_default()),
1297            ),
1298        };
1299
1300        trace!(period_index = index, ?total_costs, "Update totals");
1301    }
1302
1303    total_costs
1304}
1305
1306fn generate_report(
1307    cdr_elem: &crate::cdr::Versioned<'_>,
1308    cdr: Caveat<v221::Cdr, WarningKind>,
1309    timezone: Tz,
1310    tariff_reports: Vec<TariffReport>,
1311    price_periods_report: PeriodsReport,
1312    tariff_used: TariffOrigin,
1313) -> Report {
1314    let (cdr, warnings) = cdr.into_parts();
1315    let PeriodsReport {
1316        billable,
1317        periods,
1318        totals,
1319        total_costs,
1320    } = price_periods_report;
1321    trace!("Update billed totals {billable:#?}");
1322
1323    let total_cost = total_costs.total();
1324
1325    debug!(total_cost = %DisplayOption(total_cost.as_ref()));
1326
1327    let total_time = {
1328        debug!(
1329            period_start = %DisplayOption(periods.first().map(|p| p.start_date_time)),
1330            period_end = %DisplayOption(periods.last().map(|p| p.end_date_time)),
1331            "Calculating `total_time`"
1332        );
1333
1334        periods
1335            .first()
1336            .zip(periods.last())
1337            .map(|(first, last)| {
1338                last.end_date_time
1339                    .signed_duration_since(first.start_date_time)
1340            })
1341            .unwrap_or_default()
1342    };
1343    debug!(total_time = %Hms(total_time));
1344
1345    // Convert the warnings into heap based representation applicable for the report.
1346    let warnings = {
1347        warnings
1348            .into_group_by_elem(cdr_elem.as_element())
1349            .map(|warning::IntoGroup { element, warnings }| (element.path().to_string(), warnings))
1350            .collect()
1351    };
1352
1353    let report = Report {
1354        warnings,
1355        periods,
1356        tariff_used,
1357        timezone: timezone.to_string(),
1358        billed_parking_time: billable.parking_time,
1359        billed_energy: billable.energy,
1360        billed_charging_time: billable.charging_time,
1361        tariff_reports,
1362        total_charging_time: totals.duration_charging,
1363        total_cost: Total {
1364            cdr: cdr.total_cost,
1365            calculated: total_cost,
1366        },
1367        total_time_cost: Total {
1368            cdr: cdr.total_time_cost,
1369            calculated: total_costs.duration_charging,
1370        },
1371        total_time: Total {
1372            cdr: cdr.total_time,
1373            calculated: total_time,
1374        },
1375        total_parking_cost: Total {
1376            cdr: cdr.total_parking_cost,
1377            calculated: total_costs.duration_parking,
1378        },
1379        total_parking_time: Total {
1380            cdr: cdr.total_parking_time,
1381            calculated: totals.duration_parking,
1382        },
1383        total_energy_cost: Total {
1384            cdr: cdr.total_energy_cost,
1385            calculated: total_costs.energy,
1386        },
1387        total_energy: Total {
1388            cdr: cdr.total_energy,
1389            calculated: totals.energy,
1390        },
1391        total_fixed_cost: Total {
1392            cdr: cdr.total_fixed_cost,
1393            calculated: total_costs.fixed,
1394        },
1395        total_reservation_cost: Total {
1396            cdr: cdr.total_reservation_cost,
1397            calculated: None,
1398        },
1399    };
1400
1401    trace!("{report:#?}");
1402
1403    report
1404}
1405
1406#[derive(Debug)]
1407struct StepSize {
1408    charging_time: Option<(usize, Component)>,
1409    parking_time: Option<(usize, Component)>,
1410    energy: Option<(usize, Component)>,
1411}
1412
1413/// Return the duration as a `Decimal` amount of seconds.
1414fn delta_as_seconds_dec(delta: TimeDelta) -> Decimal {
1415    Decimal::from(delta.num_milliseconds())
1416        .checked_div(Decimal::from(duration::MILLIS_IN_SEC))
1417        .expect("Can't overflow; See test `as_seconds_dec_should_not_overflow`")
1418}
1419
1420/// Create a `HoursDecimal` from a `Decimal` amount of seconds.
1421fn delta_from_seconds_dec(seconds: Decimal) -> Result<TimeDelta, duration::Error> {
1422    let millis = seconds.saturating_mul(Decimal::from(duration::MILLIS_IN_SEC));
1423    let millis = i64::try_from(millis)?;
1424    let delta = TimeDelta::try_milliseconds(millis).ok_or(duration::Error::Overflow)?;
1425    Ok(delta)
1426}
1427
1428impl StepSize {
1429    fn new() -> Self {
1430        Self {
1431            charging_time: None,
1432            parking_time: None,
1433            energy: None,
1434        }
1435    }
1436
1437    fn update(&mut self, index: usize, components: &ComponentSet, period: &PeriodNormalized) {
1438        if period.consumed.energy.is_some() {
1439            if let Some(energy) = components.energy.clone() {
1440                self.energy = Some((index, energy));
1441            }
1442        }
1443
1444        if period.consumed.duration_charging.is_some() {
1445            if let Some(time) = components.duration_charging.clone() {
1446                self.charging_time = Some((index, time));
1447            }
1448        }
1449
1450        if period.consumed.duration_parking.is_some() {
1451            if let Some(parking) = components.duration_parking.clone() {
1452                self.parking_time = Some((index, parking));
1453            }
1454        }
1455    }
1456
1457    fn duration_step_size(
1458        total_volume: TimeDelta,
1459        period_billed_volume: &mut TimeDelta,
1460        step_size: u64,
1461    ) -> Result<TimeDelta, Error> {
1462        if step_size == 0 {
1463            return Ok(total_volume);
1464        }
1465
1466        let total_seconds = delta_as_seconds_dec(total_volume);
1467        let step_size = Decimal::from(step_size);
1468
1469        let total_billed_volume = delta_from_seconds_dec(
1470            total_seconds
1471                .checked_div(step_size)
1472                .ok_or(Error::DurationOverflow)?
1473                .ceil()
1474                .saturating_mul(step_size),
1475        )?;
1476
1477        let period_delta_volume = total_billed_volume.saturating_sub(total_volume);
1478        *period_billed_volume = period_billed_volume.saturating_add(period_delta_volume);
1479
1480        Ok(total_billed_volume)
1481    }
1482
1483    fn apply_time(
1484        &self,
1485        periods: &mut [PeriodReport],
1486        total: TimeDelta,
1487    ) -> Result<TimeDelta, Error> {
1488        let (Some((time_index, price)), None) = (&self.charging_time, &self.parking_time) else {
1489            return Ok(total);
1490        };
1491
1492        let Some(period) = periods.get_mut(*time_index) else {
1493            return Err(InternalError::InvalidPeriodIndex {
1494                index: *time_index,
1495                field_name: "apply_time",
1496            }
1497            .into());
1498        };
1499        let volume = period
1500            .dimensions
1501            .duration_charging
1502            .billed_volume
1503            .as_mut()
1504            .ok_or(Error::DimensionShouldHaveVolume {
1505                dimension_name: "time",
1506            })?;
1507
1508        Self::duration_step_size(total, volume, price.step_size)
1509    }
1510
1511    fn apply_parking_time(
1512        &self,
1513        periods: &mut [PeriodReport],
1514        total: TimeDelta,
1515    ) -> Result<TimeDelta, Error> {
1516        let Some((parking_index, price)) = &self.parking_time else {
1517            return Ok(total);
1518        };
1519
1520        let Some(period) = periods.get_mut(*parking_index) else {
1521            return Err(InternalError::InvalidPeriodIndex {
1522                index: *parking_index,
1523                field_name: "apply_parking_time",
1524            }
1525            .into());
1526        };
1527        let volume = period
1528            .dimensions
1529            .duration_parking
1530            .billed_volume
1531            .as_mut()
1532            .ok_or(Error::DimensionShouldHaveVolume {
1533                dimension_name: "parking_time",
1534            })?;
1535
1536        Self::duration_step_size(total, volume, price.step_size)
1537    }
1538
1539    fn apply_energy(&self, periods: &mut [PeriodReport], total_volume: Kwh) -> Result<Kwh, Error> {
1540        let Some((energy_index, price)) = &self.energy else {
1541            return Ok(total_volume);
1542        };
1543
1544        if price.step_size == 0 {
1545            return Ok(total_volume);
1546        }
1547
1548        let Some(period) = periods.get_mut(*energy_index) else {
1549            return Err(InternalError::InvalidPeriodIndex {
1550                index: *energy_index,
1551                field_name: "apply_energy",
1552            }
1553            .into());
1554        };
1555        let step_size = Decimal::from(price.step_size);
1556
1557        let period_billed_volume = period.dimensions.energy.billed_volume.as_mut().ok_or(
1558            Error::DimensionShouldHaveVolume {
1559                dimension_name: "energy",
1560            },
1561        )?;
1562
1563        let total_billed_volume = Kwh::from_watt_hours(
1564            total_volume
1565                .watt_hours()
1566                .checked_div(step_size)
1567                .ok_or(Error::DurationOverflow)?
1568                .ceil()
1569                .saturating_mul(step_size),
1570        );
1571
1572        let period_delta_volume = total_billed_volume.saturating_sub(total_volume);
1573        *period_billed_volume = period_billed_volume.saturating_add(period_delta_volume);
1574
1575        Ok(total_billed_volume)
1576    }
1577}
1578
1579fn parse_cdr<'caller: 'buf, 'buf>(
1580    cdr: &'caller crate::cdr::Versioned<'buf>,
1581) -> Verdict<v221::cdr::WithTariffs<'buf>, WarningKind> {
1582    match cdr.version() {
1583        Version::V211 => {
1584            let cdr = v211::cdr::WithTariffs::from_json(cdr.as_element())?;
1585            Ok(cdr.map(v221::cdr::WithTariffs::from))
1586        }
1587        Version::V221 => v221::cdr::WithTariffs::from_json(cdr.as_element()),
1588    }
1589}
1590
1591#[cfg(test)]
1592pub mod test {
1593    #![allow(clippy::missing_panics_doc, reason = "tests are allowed to panic")]
1594    #![allow(clippy::panic, reason = "tests are allowed panic")]
1595
1596    use std::collections::{BTreeMap, BTreeSet};
1597
1598    use chrono::TimeDelta;
1599    use rust_decimal::Decimal;
1600    use serde::Deserialize;
1601    use tracing::debug;
1602
1603    use crate::{
1604        assert_approx_eq, cdr,
1605        duration::ToHoursDecimal,
1606        json, number,
1607        test::{ApproxEq, ExpectFile, Expectation},
1608        timezone,
1609        warning::{self, Kind as _},
1610        Kwh, Price,
1611    };
1612
1613    use super::{Error, Report, TariffReport, Total};
1614
1615    // Decimal precision used when comparing the outcomes of the calculation with the CDR.
1616    const PRECISION: u32 = 2;
1617
1618    #[test]
1619    const fn error_should_be_send_and_sync() {
1620        const fn f<T: Send + Sync>() {}
1621
1622        f::<Error>();
1623    }
1624
1625    pub trait UnwrapReport {
1626        #[track_caller]
1627        fn unwrap_report(self, cdr: &cdr::Versioned<'_>) -> Report;
1628    }
1629
1630    impl UnwrapReport for Result<Report, Error> {
1631        fn unwrap_report(self, cdr: &cdr::Versioned<'_>) -> Report {
1632            match self {
1633                Ok(v) => v,
1634                Err(err) => match err {
1635                    Error::Cdr(warnings) => {
1636                        panic!(
1637                            "pricing CDR failed:\n{:?}",
1638                            warning::SetWriter::new(cdr.as_element(), &warnings)
1639                        );
1640                    }
1641                    Error::Tariff(warnings) => {
1642                        panic!(
1643                            "parsing tariff failed:\n{:?}",
1644                            warning::SetWriter::new(cdr.as_element(), &warnings)
1645                        );
1646                    }
1647                    _ => {
1648                        panic!("pricing CDR failed: {err:?}");
1649                    }
1650                },
1651            }
1652        }
1653    }
1654
1655    /// A `TimeDelta` wrapper used to serialize and deserialize to/from a `Decimal` representation of hours
1656    #[derive(Debug, Default)]
1657    pub(crate) struct HoursDecimal(Decimal);
1658
1659    impl ToHoursDecimal for HoursDecimal {
1660        fn to_hours_dec(&self) -> Decimal {
1661            self.0
1662        }
1663    }
1664
1665    /// Deserialize bytes into a `Decimal` applying the scale defined in the OCPI spec.
1666    ///
1667    /// Called from the `impl Deserialize` for a `Decimal` newtype.
1668    fn decimal<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
1669    where
1670        D: serde::Deserializer<'de>,
1671    {
1672        use serde::Deserialize;
1673
1674        let mut d = <Decimal as Deserialize>::deserialize(deserializer)?;
1675        d.rescale(number::SCALE);
1676        Ok(d)
1677    }
1678
1679    impl<'de> Deserialize<'de> for HoursDecimal {
1680        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1681        where
1682            D: serde::Deserializer<'de>,
1683        {
1684            decimal(deserializer).map(Self)
1685        }
1686    }
1687
1688    #[derive(serde::Deserialize)]
1689    pub(crate) struct Expect {
1690        /// Expectations for the result of calling `timezone::find_or_infer`.
1691        pub timezone_find: Option<timezone::test::FindOrInferExpect>,
1692
1693        /// Expectations for the result of calling `cdr::parse*`.
1694        pub tariff_parse: Option<ParseExpect>,
1695
1696        /// Expectations for the result of calling `cdr::parse*`.
1697        pub cdr_parse: Option<ParseExpect>,
1698
1699        /// Expectations for the result of calling `cdr::price*`.
1700        pub cdr_price: Option<PriceExpect>,
1701    }
1702
1703    /// The `Expect` is used to parse the JSON but the tests use the individual fields in separate
1704    /// asset functions. Each of those functions needs to know the `expect_file_name`.
1705    #[expect(
1706        clippy::struct_field_names,
1707        reason = "When deconstructed these fields will always be called *_expect. This avoids having to rename them in-place."
1708    )]
1709    pub(crate) struct ExpectFields {
1710        /// Expectations for the result of calling `timezone::find_or_infer`.
1711        pub timezone_find_expect: ExpectFile<timezone::test::FindOrInferExpect>,
1712
1713        /// Expectations for the result of calling `cdr::parse*`.
1714        pub tariff_parse_expect: ExpectFile<ParseExpect>,
1715
1716        /// Expectations for the result of calling `cdr::parse*`.
1717        pub cdr_parse_expect: ExpectFile<ParseExpect>,
1718
1719        /// Expectations for the result of calling `cdr::price*`.
1720        pub cdr_price_expect: ExpectFile<PriceExpect>,
1721    }
1722
1723    impl ExpectFile<Expect> {
1724        /// Split the `ExpectFile<Expect>` into its constituent fields and repackage them as `ExpectFile`s.
1725        pub(crate) fn into_fields(self) -> ExpectFields {
1726            let ExpectFile {
1727                value,
1728                expect_file_name,
1729            } = self;
1730
1731            match value {
1732                Some(expect) => {
1733                    let Expect {
1734                        timezone_find,
1735                        tariff_parse,
1736                        cdr_parse,
1737                        cdr_price,
1738                    } = expect;
1739                    ExpectFields {
1740                        timezone_find_expect: ExpectFile::with_value(
1741                            timezone_find,
1742                            &expect_file_name,
1743                        ),
1744                        tariff_parse_expect: ExpectFile::with_value(
1745                            tariff_parse,
1746                            &expect_file_name,
1747                        ),
1748                        cdr_parse_expect: ExpectFile::with_value(cdr_parse, &expect_file_name),
1749                        cdr_price_expect: ExpectFile::with_value(cdr_price, &expect_file_name),
1750                    }
1751                }
1752                None => ExpectFields {
1753                    timezone_find_expect: ExpectFile::only_file_name(&expect_file_name),
1754                    tariff_parse_expect: ExpectFile::only_file_name(&expect_file_name),
1755                    cdr_parse_expect: ExpectFile::only_file_name(&expect_file_name),
1756                    cdr_price_expect: ExpectFile::only_file_name(&expect_file_name),
1757                },
1758            }
1759        }
1760    }
1761
1762    pub(crate) fn assert_parse_report(
1763        unexpected_fields: json::UnexpectedFields<'_>,
1764        expect: ExpectFile<ParseExpect>,
1765    ) {
1766        let ExpectFile {
1767            value,
1768            expect_file_name,
1769        } = expect;
1770        let unexpected_fields_expect = value
1771            .map(|exp| exp.unexpected_fields)
1772            .unwrap_or(Expectation::Absent);
1773
1774        if let Expectation::Present(expectation) = unexpected_fields_expect {
1775            let unexpected_fields_expect = expectation.expect_value();
1776
1777            for field in unexpected_fields {
1778                assert!(
1779                    unexpected_fields_expect.contains(&field.to_string()),
1780                    "The CDR has an unexpected field that's not expected in `{expect_file_name}`: `{field}`"
1781                );
1782            }
1783        } else {
1784            assert!(
1785                unexpected_fields.is_empty(),
1786                "The CDR has unexpected fields but the expect file doesn't `{expect_file_name}`; {unexpected_fields:#}",
1787            );
1788        }
1789    }
1790
1791    pub(crate) fn assert_price_report(report: Report, expect: ExpectFile<PriceExpect>) {
1792        let Report {
1793            warnings,
1794            mut tariff_reports,
1795            periods: _,
1796            tariff_used,
1797            timezone: _,
1798            billed_energy: _,
1799            billed_parking_time: _,
1800            billed_charging_time: _,
1801            total_charging_time: _,
1802            total_cost,
1803            total_fixed_cost,
1804            total_time,
1805            total_time_cost,
1806            total_energy,
1807            total_energy_cost,
1808            total_parking_time,
1809            total_parking_cost,
1810            total_reservation_cost,
1811        } = report;
1812
1813        let ExpectFile {
1814            value: expect,
1815            expect_file_name,
1816        } = expect;
1817
1818        // This destructure isn't pretty but it's at least simple to maintain.
1819        // The alternative is getting involved with references of references when processing each borrowed field.
1820        let (
1821            warnings_expect,
1822            tariff_index_expect,
1823            tariff_id_expect,
1824            tariff_reports_expect,
1825            total_cost_expectation,
1826            total_fixed_cost_expectation,
1827            total_time_expectation,
1828            total_time_cost_expectation,
1829            total_energy_expectation,
1830            total_energy_cost_expectation,
1831            total_parking_time_expectation,
1832            total_parking_cost_expectation,
1833            total_reservation_cost_expectation,
1834        ) = expect
1835            .map(|exp| {
1836                let PriceExpect {
1837                    warnings,
1838                    tariff_index,
1839                    tariff_id,
1840                    tariff_reports,
1841                    total_cost,
1842                    total_fixed_cost,
1843                    total_time,
1844                    total_time_cost,
1845                    total_energy,
1846                    total_energy_cost,
1847                    total_parking_time,
1848                    total_parking_cost,
1849                    total_reservation_cost,
1850                } = exp;
1851
1852                (
1853                    warnings,
1854                    tariff_index,
1855                    tariff_id,
1856                    tariff_reports,
1857                    total_cost,
1858                    total_fixed_cost,
1859                    total_time,
1860                    total_time_cost,
1861                    total_energy,
1862                    total_energy_cost,
1863                    total_parking_time,
1864                    total_parking_cost,
1865                    total_reservation_cost,
1866                )
1867            })
1868            .unwrap_or((
1869                Expectation::Absent,
1870                Expectation::Absent,
1871                Expectation::Absent,
1872                Expectation::Absent,
1873                Expectation::Absent,
1874                Expectation::Absent,
1875                Expectation::Absent,
1876                Expectation::Absent,
1877                Expectation::Absent,
1878                Expectation::Absent,
1879                Expectation::Absent,
1880                Expectation::Absent,
1881                Expectation::Absent,
1882            ));
1883
1884        if let Expectation::Present(expectation) = warnings_expect {
1885            let warnings_expect = expectation.expect_value();
1886
1887            debug!("{warnings_expect:?}");
1888
1889            for (elem_path, warnings) in warnings {
1890                let Some(warnings_expect) = warnings_expect.get(&*elem_path) else {
1891                    let warning_ids = warnings
1892                        .iter()
1893                        .map(|k| format!("  \"{}\",", k.id()))
1894                        .collect::<Vec<_>>()
1895                        .join("\n");
1896
1897                    panic!("No warnings expected `{expect_file_name}` for `Element` at `{elem_path}` but {} warnings were reported:\n[\n{}\n]", warnings.len(), warning_ids);
1898                };
1899
1900                let warnings_expect = warnings_expect
1901                    .iter()
1902                    .map(|s| &**s)
1903                    .collect::<BTreeSet<_>>();
1904
1905                for warning_kind in warnings {
1906                    let id = warning_kind.id();
1907                    assert!(
1908                        warnings_expect.contains(&*id),
1909                        "Unexpected warning `{id}` for `Element` at `{elem_path}`"
1910                    );
1911                }
1912            }
1913        } else {
1914            assert!(warnings.is_empty(), "The CDR has warnings; {warnings:?}",);
1915        }
1916
1917        if let Expectation::Present(expectation) = tariff_reports_expect {
1918            let tariff_reports_expect: BTreeMap<_, _> = expectation
1919                .expect_value()
1920                .into_iter()
1921                .map(|TariffReportExpect { id, warnings }| (id, warnings))
1922                .collect();
1923
1924            for report in &mut tariff_reports {
1925                let TariffReport { origin, warnings } = report;
1926                let id = &origin.id;
1927                let Some(warnings_expect) = tariff_reports_expect.get(id) else {
1928                    panic!("A tariff with {id} is not expected `{expect_file_name}`");
1929                };
1930
1931                debug!("{warnings_expect:?}");
1932
1933                for (elem_path, warnings) in warnings {
1934                    let Some(warnings_expect) = warnings_expect.get(elem_path) else {
1935                        let warning_ids = warnings
1936                            .iter()
1937                            .map(|k| format!("  \"{}\",", k.id()))
1938                            .collect::<Vec<_>>()
1939                            .join("\n");
1940
1941                        panic!("No warnings expected for `Element` at `{elem_path}` but {} warnings were reported:\n[\n{}\n]", warnings.len(), warning_ids);
1942                    };
1943
1944                    let warnings_expect = warnings_expect
1945                        .iter()
1946                        .map(|s| &**s)
1947                        .collect::<BTreeSet<_>>();
1948
1949                    for warning_kind in warnings {
1950                        let id = warning_kind.id();
1951                        assert!(
1952                            warnings_expect.contains(&*id),
1953                            "Unexpected warning `{id}` for `Element` at `{elem_path}`"
1954                        );
1955                    }
1956                }
1957            }
1958        } else {
1959            for report in &tariff_reports {
1960                let TariffReport { origin, warnings } = report;
1961
1962                let id = &origin.id;
1963
1964                assert!(
1965                    warnings.is_empty(),
1966                    "The tariff with id `{id}` has warnings.\n {warnings:?}"
1967                );
1968            }
1969        }
1970
1971        if let Expectation::Present(expectation) = tariff_id_expect {
1972            assert_eq!(tariff_used.id, expectation.expect_value());
1973        }
1974
1975        if let Expectation::Present(expectation) = tariff_index_expect {
1976            assert_eq!(tariff_used.index, expectation.expect_value());
1977        }
1978
1979        total_cost_expectation.expect_price("total_cost", &total_cost);
1980        total_fixed_cost_expectation.expect_opt_price("total_fixed_cost", &total_fixed_cost);
1981        total_time_expectation.expect_duration("total_time", &total_time);
1982        total_time_cost_expectation.expect_opt_price("total_time_cost", &total_time_cost);
1983        total_energy_expectation.expect_opt_kwh("total_energy", &total_energy);
1984        total_energy_cost_expectation.expect_opt_price("total_energy_cost", &total_energy_cost);
1985        total_parking_time_expectation
1986            .expect_opt_duration("total_parking_time", &total_parking_time);
1987        total_parking_cost_expectation.expect_opt_price("total_parking_cost", &total_parking_cost);
1988        total_reservation_cost_expectation
1989            .expect_opt_price("total_reservation_cost", &total_reservation_cost);
1990    }
1991
1992    /// Expectations for the result of calling `cdr::parse*`.
1993    #[derive(serde::Deserialize)]
1994    pub struct ParseExpect {
1995        #[serde(default)]
1996        unexpected_fields: Expectation<Vec<String>>,
1997    }
1998
1999    /// Expectations for the result of calling `cdr::price`.
2000    #[derive(serde::Deserialize)]
2001    pub struct PriceExpect {
2002        /// Expected Warnings from parsing a CDR.
2003        ///
2004        /// Each entry in the map is an element path and a list of associated warnings.
2005        #[serde(default)]
2006        warnings: Expectation<BTreeMap<String, Vec<String>>>,
2007
2008        /// Index of the tariff that was found to be active.
2009        #[serde(default)]
2010        tariff_index: Expectation<usize>,
2011
2012        /// Id of the tariff that was found to be active.
2013        #[serde(default)]
2014        tariff_id: Expectation<String>,
2015
2016        /// A list of the tariff IDs found in the CDR or supplied to the [`cdr::price`](crate::cdr::price) function.
2017        ///
2018        /// Each tariff may have a set of unexpected fields encountered while parsing the tariff.
2019        #[serde(default)]
2020        tariff_reports: Expectation<Vec<TariffReportExpect>>,
2021
2022        /// Total sum of all the costs of this transaction in the specified currency.
2023        #[serde(default)]
2024        total_cost: Expectation<Price>,
2025
2026        /// Total sum of all the fixed costs in the specified currency, except fixed price components of parking and reservation. The cost not depending on amount of time/energy used etc. Can contain costs like a start tariff.
2027        #[serde(default)]
2028        total_fixed_cost: Expectation<Price>,
2029
2030        /// Total duration of the charging session (including the duration of charging and not charging), in hours.
2031        #[serde(default)]
2032        total_time: Expectation<HoursDecimal>,
2033
2034        /// Total sum of all the cost related to duration of charging during this transaction, in the specified currency.
2035        #[serde(default)]
2036        total_time_cost: Expectation<Price>,
2037
2038        /// Total energy charged, in kWh.
2039        #[serde(default)]
2040        total_energy: Expectation<Kwh>,
2041
2042        /// Total sum of all the cost of all the energy used, in the specified currency.
2043        #[serde(default)]
2044        total_energy_cost: Expectation<Price>,
2045
2046        /// Total duration of the charging session where the EV was not charging (no energy was transferred between EVSE and EV), in hours.
2047        #[serde(default)]
2048        total_parking_time: Expectation<HoursDecimal>,
2049
2050        /// Total sum of all the cost related to parking of this transaction, including fixed price components, in the specified currency.
2051        #[serde(default)]
2052        total_parking_cost: Expectation<Price>,
2053
2054        /// Total sum of all the cost related to a reservation of a Charge Point, including fixed price components, in the specified currency.
2055        #[serde(default)]
2056        total_reservation_cost: Expectation<Price>,
2057    }
2058
2059    #[derive(Debug, Deserialize)]
2060    struct TariffReportExpect {
2061        /// The id of the tariff.
2062        id: String,
2063
2064        /// Expected Warnings from parsing a tariff.
2065        ///
2066        /// Each entry in the map is an element path and a list of associated warnings.
2067        #[serde(default)]
2068        warnings: BTreeMap<String, Vec<String>>,
2069    }
2070
2071    impl Expectation<Price> {
2072        #[track_caller]
2073        fn expect_opt_price(self, field_name: &str, total: &Total<Option<Price>>) {
2074            if let Expectation::Present(expect_value) = self {
2075                match (expect_value.into_option(), total.calculated) {
2076                    (Some(a), Some(b)) => assert!(
2077                        a.approx_eq(&b),
2078                        "Expected `{a}` but `{b}` was calculated for `{field_name}`"
2079                    ),
2080                    (Some(a), None) => {
2081                        panic!("Expected `{a}`, but no price was calculated for `{field_name}`")
2082                    }
2083                    (None, Some(b)) => {
2084                        panic!("Expected no value, but `{b}` was calculated for `{field_name}`")
2085                    }
2086                    (None, None) => (),
2087                }
2088            } else {
2089                match (total.cdr, total.calculated) {
2090                    (None, None) => (),
2091                    (None, Some(calculated)) => {
2092                        assert!(calculated.is_zero(), "The CDR field `{field_name}` doesn't have a value but a value was calculated; calculated: {calculated}");
2093                    }
2094                    (Some(cdr), None) => {
2095                        assert!(
2096                            cdr.is_zero(),
2097                            "The CDR field `{field_name}` has a value but the calculated value is none; cdr: {cdr}"
2098                        );
2099                    }
2100                    (Some(cdr), Some(calculated)) => {
2101                        assert!(
2102                            cdr.approx_eq(&calculated),
2103                            "Comparing `{field_name}` field with CDR"
2104                        );
2105                    }
2106                }
2107            }
2108        }
2109
2110        #[track_caller]
2111        fn expect_price(self, field_name: &str, total: &Total<Price, Option<Price>>) {
2112            if let Expectation::Present(expect_value) = self {
2113                match (expect_value.into_option(), total.calculated) {
2114                    (Some(a), Some(b)) => assert!(
2115                        a.approx_eq(&b),
2116                        "Expected `{a}` but `{b}` was calculated for `{field_name}`"
2117                    ),
2118                    (Some(a), None) => {
2119                        panic!("Expected `{a}`, but no price was calculated for `{field_name}`")
2120                    }
2121                    (None, Some(b)) => {
2122                        panic!("Expected no value, but `{b}` was calculated for `{field_name}`")
2123                    }
2124                    (None, None) => (),
2125                }
2126            } else if let Some(calculated) = total.calculated {
2127                assert!(
2128                    total.cdr.approx_eq(&calculated),
2129                    "CDR contains `{}` but `{}` was calculated for `{field_name}`",
2130                    total.cdr,
2131                    calculated
2132                );
2133            } else {
2134                assert!(
2135                    total.cdr.is_zero(),
2136                    "The CDR field `{field_name}` has a value but the calculated value is none; cdr: {:?}",
2137                    total.cdr
2138                );
2139            }
2140        }
2141    }
2142
2143    impl Expectation<HoursDecimal> {
2144        #[track_caller]
2145        fn expect_duration(self, field_name: &str, total: &Total<TimeDelta>) {
2146            if let Expectation::Present(expect_value) = self {
2147                assert_approx_eq!(
2148                    expect_value.expect_value().to_hours_dec(),
2149                    total.calculated.to_hours_dec(),
2150                    "Comparing `{field_name}` field with expectation"
2151                );
2152            } else {
2153                assert_approx_eq!(
2154                    total.cdr.to_hours_dec(),
2155                    total.calculated.to_hours_dec(),
2156                    "Comparing `{field_name}` field with CDR"
2157                );
2158            }
2159        }
2160
2161        #[track_caller]
2162        fn expect_opt_duration(
2163            self,
2164            field_name: &str,
2165            total: &Total<Option<TimeDelta>, Option<TimeDelta>>,
2166        ) {
2167            if let Expectation::Present(expect_value) = self {
2168                assert_approx_eq!(
2169                    expect_value
2170                        .into_option()
2171                        .unwrap_or_default()
2172                        .to_hours_dec(),
2173                    &total
2174                        .calculated
2175                        .as_ref()
2176                        .map(ToHoursDecimal::to_hours_dec)
2177                        .unwrap_or_default(),
2178                    "Comparing `{field_name}` field with expectation"
2179                );
2180            } else {
2181                assert_approx_eq!(
2182                    total.cdr.unwrap_or_default().to_hours_dec(),
2183                    total.calculated.unwrap_or_default().to_hours_dec(),
2184                    "Comparing `{field_name}` field with CDR"
2185                );
2186            }
2187        }
2188    }
2189
2190    impl Expectation<Kwh> {
2191        #[track_caller]
2192        fn expect_opt_kwh(self, field_name: &str, total: &Total<Kwh, Option<Kwh>>) {
2193            if let Expectation::Present(expect_value) = self {
2194                assert_eq!(
2195                    expect_value
2196                        .into_option()
2197                        .map(|kwh| kwh.round_dp(PRECISION)),
2198                    total
2199                        .calculated
2200                        .map(|kwh| kwh.rescale().round_dp(PRECISION)),
2201                    "Comparing `{field_name}` field with expectation"
2202                );
2203            } else {
2204                assert_eq!(
2205                    total.cdr.round_dp(PRECISION),
2206                    total
2207                        .calculated
2208                        .map(|kwh| kwh.rescale().round_dp(PRECISION))
2209                        .unwrap_or_default(),
2210                    "Comparing `{field_name}` field with CDR"
2211                );
2212            }
2213        }
2214    }
2215}
2216
2217#[cfg(test)]
2218mod test_periods {
2219    #![allow(clippy::as_conversions, reason = "tests are allowed to panic")]
2220    #![allow(clippy::panic, reason = "tests are allowed panic")]
2221
2222    use chrono::Utc;
2223    use chrono_tz::Tz;
2224    use rust_decimal::Decimal;
2225    use rust_decimal_macros::dec;
2226
2227    use crate::{
2228        assert_approx_eq, cdr,
2229        price::{self, parse_tariff, test::UnwrapReport},
2230        tariff, Kwh, Version,
2231    };
2232
2233    use super::{Consumed, Period, TariffSource};
2234
2235    #[test]
2236    fn should_price_periods_from_time_and_parking_time_cdr_and_tariff() {
2237        const VERSION: Version = Version::V211;
2238        const CDR_JSON: &str = include_str!(
2239            "../test_data/v211/real_world/time_and_parking_time_separate_tariff/cdr.json"
2240        );
2241        const TARIFF_JSON: &str = include_str!(
2242            "../test_data/v211/real_world/time_and_parking_time_separate_tariff/tariff.json"
2243        );
2244        // Every period has a 15 minute duration.
2245        const PERIOD_DURATION: chrono::TimeDelta = chrono::TimeDelta::minutes(15);
2246
2247        /// Create `TIME` period for each energy value provided.
2248        ///
2249        /// Each `TIME` period is the same duration.
2250        /// But has a different `start_date_time`.
2251        fn charging(start_date_time: &str, energy: Vec<Decimal>) -> Vec<Period> {
2252            let start: chrono::DateTime<Utc> = start_date_time.parse().unwrap();
2253
2254            energy
2255                .into_iter()
2256                .enumerate()
2257                .map(|(i, kwh)| {
2258                    let i = i32::try_from(i).unwrap();
2259                    let start_date_time = start + (PERIOD_DURATION * i);
2260
2261                    Period {
2262                        start_date_time,
2263                        consumed: Consumed {
2264                            duration_charging: Some(PERIOD_DURATION),
2265                            energy: Some(kwh.into()),
2266                            ..Default::default()
2267                        },
2268                    }
2269                })
2270                .collect()
2271        }
2272
2273        /// Create `period_count` number of `PARKING_TIME` periods.
2274        ///
2275        /// Each `PARKING_TIME` period is the same duration and energy usage (0kWh)
2276        /// but has a different `start_date_time`.
2277        fn parking(start_date_time: &str, period_count: usize) -> Vec<Period> {
2278            // Every parking period has a consumed energy of zero.
2279            let period_energy = Kwh::from(0);
2280            let start: chrono::DateTime<Utc> = start_date_time.parse().unwrap();
2281
2282            let period_count = i32::try_from(period_count).unwrap();
2283            // Add uniform periods except for the last one
2284            let mut periods: Vec<Period> = (0..period_count - 1)
2285                .map(|i| {
2286                    let start_date_time = start + (PERIOD_DURATION * i);
2287
2288                    Period {
2289                        start_date_time,
2290                        consumed: Consumed {
2291                            duration_parking: Some(PERIOD_DURATION),
2292                            energy: Some(period_energy),
2293                            ..Default::default()
2294                        },
2295                    }
2296                })
2297                .collect();
2298
2299            let start_date_time = start + (PERIOD_DURATION * (period_count - 1));
2300
2301            // The last period is a 10 minutes period instead of 15 minutes.
2302            periods.push(Period {
2303                start_date_time,
2304                consumed: Consumed {
2305                    duration_parking: Some(chrono::TimeDelta::seconds(644)),
2306                    energy: Some(period_energy),
2307                    ..Default::default()
2308                },
2309            });
2310
2311            periods
2312        }
2313
2314        let report = cdr::parse_with_version(CDR_JSON, VERSION).unwrap();
2315        let cdr::ParseReport {
2316            cdr,
2317            unexpected_fields,
2318        } = report;
2319
2320        assert!(unexpected_fields.is_empty());
2321        let tariff::ParseReport {
2322            tariff,
2323            unexpected_fields,
2324        } = tariff::parse_with_version(TARIFF_JSON, VERSION).unwrap();
2325        assert!(unexpected_fields.is_empty());
2326
2327        // If you know the version and timezone of a CDR you simply pass them into the `cdr::price` fn.
2328        let report = cdr::price(
2329            &cdr,
2330            TariffSource::Override(vec![tariff.clone()]),
2331            Tz::Europe__Amsterdam,
2332        )
2333        .unwrap_report(&cdr);
2334
2335        let price::Report {
2336            warnings,
2337            // We are not concerned with warnings in this test
2338            periods,
2339            // We are not concerned with the tariff reports in this test
2340            tariff_used: _,
2341            tariff_reports: _,
2342            timezone: _,
2343            billed_energy,
2344            billed_parking_time,
2345            billed_charging_time,
2346            total_charging_time,
2347            total_energy,
2348            total_parking_time,
2349            // The `total_time` simply the addition of `total_charging_time` and `total_parking_time`.
2350            total_time: _,
2351            total_cost,
2352            total_energy_cost,
2353            total_fixed_cost,
2354            total_parking_cost,
2355            // Reservation costs are not computed during pricing.
2356            total_reservation_cost: _,
2357            total_time_cost,
2358        } = report;
2359
2360        assert!(warnings.is_empty());
2361
2362        let mut cdr_periods = charging(
2363            "2025-04-09T16:12:54.000Z",
2364            vec![
2365                dec!(2.75),
2366                dec!(2.77),
2367                dec!(1.88),
2368                dec!(2.1),
2369                dec!(2.09),
2370                dec!(2.11),
2371                dec!(2.09),
2372                dec!(2.09),
2373                dec!(2.09),
2374                dec!(2.09),
2375                dec!(2.09),
2376                dec!(2.09),
2377                dec!(2.09),
2378                dec!(2.11),
2379                dec!(2.13),
2380                dec!(2.09),
2381                dec!(2.11),
2382                dec!(2.12),
2383                dec!(2.13),
2384                dec!(2.1),
2385                dec!(2.0),
2386                dec!(0.69),
2387                dec!(0.11),
2388            ],
2389        );
2390        let mut periods_parking = parking("2025-04-09T21:57:55.000Z", 47);
2391
2392        cdr_periods.append(&mut periods_parking);
2393        cdr_periods.sort_by_key(|p| p.start_date_time);
2394
2395        assert_eq!(
2396            cdr_periods.len(),
2397            periods.len(),
2398            "The amount of `price::Report` periods should equal the periods given to the `price::periods` fn"
2399        );
2400        assert_eq!(
2401            periods.len(),
2402            70,
2403            "The `time_and_parking/cdr.json` has 70 `charging_periods`"
2404        );
2405
2406        assert!(periods
2407            .iter()
2408            .map(|p| p.start_date_time)
2409            .collect::<Vec<_>>()
2410            .is_sorted());
2411
2412        let (tariff, warnings) = parse_tariff(&tariff).unwrap().into_parts();
2413        assert!(warnings.is_empty());
2414
2415        let periods_report = price::periods(
2416            "2025-04-10T09:38:38.000Z".parse().unwrap(),
2417            chrono_tz::Europe::Amsterdam,
2418            &tariff,
2419            &mut cdr_periods,
2420        )
2421        .unwrap();
2422
2423        let price::PeriodsReport {
2424            billable,
2425            periods,
2426            totals,
2427            total_costs,
2428        } = periods_report;
2429
2430        assert_eq!(
2431            cdr_periods.len(),
2432            periods.len(),
2433            "The amount of `price::Report` periods should equal the periods given to the `price::periods` fn"
2434        );
2435        assert_eq!(
2436            periods.len(),
2437            70,
2438            "The `time_and_parking/cdr.json` has 70 `charging_periods`"
2439        );
2440
2441        assert_approx_eq!(billable.charging_time, billed_charging_time);
2442        assert_approx_eq!(billable.energy, billed_energy);
2443        assert_approx_eq!(billable.parking_time, billed_parking_time,);
2444
2445        assert_approx_eq!(totals.duration_charging, total_charging_time);
2446        assert_approx_eq!(totals.energy, total_energy.calculated);
2447        assert_approx_eq!(totals.duration_parking, total_parking_time.calculated);
2448
2449        assert_approx_eq!(total_costs.duration_charging, total_time_cost.calculated,);
2450        assert_approx_eq!(total_costs.energy, total_energy_cost.calculated,);
2451        assert_approx_eq!(total_costs.fixed, total_fixed_cost.calculated);
2452        assert_approx_eq!(total_costs.duration_parking, total_parking_cost.calculated);
2453        assert_approx_eq!(total_costs.total(), total_cost.calculated);
2454    }
2455}
2456
2457#[cfg(test)]
2458mod test_validate_cdr {
2459    use assert_matches::assert_matches;
2460
2461    use crate::{
2462        cdr,
2463        json::FromJson,
2464        price::{self, v221, WarningKind},
2465        test::{self, datetime_from_str},
2466    };
2467
2468    #[test]
2469    fn should_pass_parse_validation() {
2470        test::setup();
2471        let json = cdr_json("2022-01-13T16:00:00Z", "2022-01-13T19:12:00Z");
2472        let cdr::ParseReport {
2473            cdr,
2474            unexpected_fields,
2475        } = cdr::parse_with_version(&json, crate::Version::V221).unwrap();
2476        assert!(unexpected_fields.is_empty());
2477        let (_cdr, warnings) = v221::Cdr::from_json(cdr.as_element()).unwrap().into_parts();
2478        assert!(warnings.is_empty());
2479    }
2480
2481    #[test]
2482    fn should_fail_validation_start_end_range_doesnt_overlap_with_periods() {
2483        test::setup();
2484
2485        let json = cdr_json("2022-02-13T16:00:00Z", "2022-02-13T19:12:00Z");
2486        let cdr::ParseReport {
2487            cdr,
2488            unexpected_fields,
2489        } = cdr::parse_with_version(&json, crate::Version::V221).unwrap();
2490        assert!(unexpected_fields.is_empty());
2491        let (_cdr, warnings) = v221::Cdr::from_json(cdr.as_element()).unwrap().into_parts();
2492        let warnings = warnings.into_kind_vec();
2493        let [warning] = warnings.try_into().unwrap();
2494        let (cdr_range, period_range) = assert_matches!(warning, WarningKind::PeriodsOutsideStartEndDateTime { cdr_range, period_range } => (cdr_range, period_range));
2495
2496        {
2497            assert_eq!(cdr_range.start, datetime_from_str("2022-02-13T16:00:00Z"));
2498            assert_eq!(cdr_range.end, datetime_from_str("2022-02-13T19:12:00Z"));
2499        }
2500        {
2501            let period_range =
2502                assert_matches!(period_range, price::PeriodRange::Many(range) => range);
2503
2504            assert_eq!(
2505                period_range.start,
2506                datetime_from_str("2022-01-13T16:00:00Z")
2507            );
2508            assert_eq!(period_range.end, datetime_from_str("2022-01-13T18:30:00Z"));
2509        }
2510    }
2511
2512    fn cdr_json(start_date_time: &str, end_date_time: &str) -> String {
2513        let value = serde_json::json!({
2514            "start_date_time": start_date_time,
2515            "end_date_time": end_date_time,
2516            "currency": "EUR",
2517            "tariffs": [],
2518            "cdr_location": {
2519                "country": "NLD"
2520            },
2521            "charging_periods": [
2522                {
2523                    "start_date_time": "2022-01-13T16:00:00Z",
2524                    "dimensions": [
2525                        {
2526                            "type": "TIME",
2527                            "volume": 2.5
2528                        }
2529                    ]
2530                },
2531                {
2532                    "start_date_time": "2022-01-13T18:30:00Z",
2533                    "dimensions": [
2534                        {
2535                            "type": "PARKING_TIME",
2536                            "volume": 0.7
2537                        }
2538                    ]
2539                }
2540            ],
2541            "total_cost": {
2542                "excl_vat": 11.25,
2543                "incl_vat": 12.75
2544            },
2545            "total_time_cost": {
2546                "excl_vat": 7.5,
2547                "incl_vat": 8.25
2548            },
2549            "total_parking_time": 0.7,
2550            "total_parking_cost": {
2551                "excl_vat": 3.75,
2552                "incl_vat": 4.5
2553            },
2554            "total_time": 3.2,
2555            "total_energy": 0,
2556            "last_updated": "2022-01-13T00:00:00Z"
2557        });
2558
2559        value.to_string()
2560    }
2561}
2562
2563#[cfg(test)]
2564mod test_real_world_v211 {
2565    use std::path::Path;
2566
2567    use crate::{
2568        cdr,
2569        price::{
2570            self,
2571            test::{Expect, ExpectFields, UnwrapReport},
2572        },
2573        tariff, test, timezone, Version,
2574    };
2575
2576    #[test_each::file(
2577        glob = "ocpi-tariffs/test_data/v211/real_world/*/cdr*.json",
2578        name(segments = 2)
2579    )]
2580    fn test_price_cdr(cdr_json: &str, path: &Path) {
2581        const VERSION: Version = Version::V211;
2582
2583        test::setup();
2584
2585        let expect_json = test::read_expect_json(path, "price");
2586        let expect = test::parse_expect_json::<Expect>(expect_json.as_deref());
2587
2588        let ExpectFields {
2589            timezone_find_expect,
2590            tariff_parse_expect,
2591            cdr_parse_expect,
2592            cdr_price_expect,
2593        } = expect.into_fields();
2594
2595        let tariff_json = std::fs::read_to_string(path.parent().unwrap().join("tariff.json")).ok();
2596        let tariff = tariff_json
2597            .as_deref()
2598            .map(|json| tariff::parse_with_version(json, VERSION))
2599            .transpose()
2600            .unwrap();
2601
2602        let tariff = if let Some(parse_report) = tariff {
2603            let tariff::ParseReport {
2604                tariff,
2605                unexpected_fields,
2606            } = parse_report;
2607            price::test::assert_parse_report(unexpected_fields, tariff_parse_expect);
2608            price::TariffSource::Override(vec![tariff])
2609        } else {
2610            assert!(tariff_parse_expect.value.is_none(), "There is no separate tariff to parse so there is no need to define a `tariff_parse` expectation");
2611            price::TariffSource::UseCdr
2612        };
2613
2614        let report = cdr::parse_with_version(cdr_json, VERSION).unwrap();
2615        let cdr::ParseReport {
2616            cdr,
2617            unexpected_fields,
2618        } = report;
2619        price::test::assert_parse_report(unexpected_fields, cdr_parse_expect);
2620
2621        let (timezone_source, warnings) = timezone::find_or_infer(&cdr).into_parts();
2622        let timezone_source = timezone_source.unwrap();
2623
2624        timezone::test::assert_find_or_infer_outcome(
2625            &cdr,
2626            timezone_source,
2627            timezone_find_expect,
2628            &warnings,
2629        );
2630
2631        let report = cdr::price(&cdr, tariff, timezone_source.into_timezone()).unwrap_report(&cdr);
2632        price::test::assert_price_report(report, cdr_price_expect);
2633    }
2634}
2635
2636#[cfg(test)]
2637mod test_real_world_v221 {
2638    use std::path::Path;
2639
2640    use crate::{
2641        cdr,
2642        price::{
2643            self,
2644            test::{ExpectFields, UnwrapReport},
2645        },
2646        tariff, test, timezone, Version,
2647    };
2648
2649    #[test_each::file(
2650        glob = "ocpi-tariffs/test_data/v221/real_world/*/cdr*.json",
2651        name(segments = 2)
2652    )]
2653    fn test_price_cdr(cdr_json: &str, path: &Path) {
2654        const VERSION: Version = Version::V221;
2655
2656        test::setup();
2657
2658        let expect_json = test::read_expect_json(path, "price");
2659        let expect = test::parse_expect_json(expect_json.as_deref());
2660        let ExpectFields {
2661            timezone_find_expect,
2662            tariff_parse_expect,
2663            cdr_parse_expect,
2664            cdr_price_expect,
2665        } = expect.into_fields();
2666
2667        let tariff_json = std::fs::read_to_string(path.parent().unwrap().join("tariff.json")).ok();
2668        let tariff = tariff_json
2669            .as_deref()
2670            .map(|json| tariff::parse_with_version(json, VERSION))
2671            .transpose()
2672            .unwrap();
2673        let tariff = tariff
2674            .map(|report| {
2675                let tariff::ParseReport {
2676                    tariff,
2677                    unexpected_fields,
2678                } = report;
2679                price::test::assert_parse_report(unexpected_fields, tariff_parse_expect);
2680                price::TariffSource::Override(vec![tariff])
2681            })
2682            .unwrap_or(price::TariffSource::UseCdr);
2683
2684        let report = cdr::parse_with_version(cdr_json, VERSION).unwrap();
2685        let cdr::ParseReport {
2686            cdr,
2687            unexpected_fields,
2688        } = report;
2689        price::test::assert_parse_report(unexpected_fields, cdr_parse_expect);
2690
2691        let (timezone_source, warnings) = timezone::find_or_infer(&cdr).into_parts();
2692        let timezone_source = timezone_source.unwrap();
2693
2694        timezone::test::assert_find_or_infer_outcome(
2695            &cdr,
2696            timezone_source,
2697            timezone_find_expect,
2698            &warnings,
2699        );
2700
2701        // The v221 tariff location does not contain a timezone field, this timezone should be
2702        // used from the `Location`.
2703        //
2704        // The tariff's time related fields are in UTC and can be converted to local time by using
2705        // the timezone from the `Location` object.
2706        //
2707        // > `start_date_time`:
2708        // >
2709        // > The time when this tariff becomes active, in UTC, time_zone field of the Location can be used to convert to local time.
2710        // >
2711        // > See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#131-tariff-object>
2712        //
2713        // See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_locations.asciidoc>
2714        let report = cdr::price(&cdr, tariff, timezone_source.into_timezone()).unwrap_report(&cdr);
2715        price::test::assert_price_report(report, cdr_price_expect);
2716    }
2717}