1#[cfg(test)]
4pub mod test;
5
6#[cfg(test)]
7mod test_normalize_periods;
8
9#[cfg(test)]
10mod test_periods;
11
12#[cfg(test)]
13mod test_real_world;
14
15#[cfg(test)]
16mod test_validate_cdr;
17
18#[cfg(test)]
19mod test_current_and_power_restrictions;
20
21#[cfg(test)]
22mod test_reservation_restriction;
23
24#[cfg(test)]
25mod test_min_max_price;
26
27#[cfg(test)]
28mod test_warning_path_map;
29
30mod tariff;
31mod v211;
32mod v221;
33
34use std::{borrow::Cow, collections::BTreeMap, fmt, ops::Range};
35
36use chrono::{DateTime, Datelike as _, TimeDelta, Utc};
37use chrono_tz::Tz;
38use rust_decimal::Decimal;
39use tariff::Tariff;
40use tracing::{debug, instrument, trace};
41
42use crate::{
43 country, currency, datetime,
44 duration::{self, AsHms as _, Hms},
45 enumeration, from_warning_all,
46 json::{self, FromJson as _},
47 money::{self, VatOrigin},
48 number::{self, RoundDecimal as _},
49 string,
50 warning::{
51 self, GatherDeferredWarnings as _, GatherWarnings as _, IntoCaveat as _,
52 IntoCaveatDeferred as _, VerdictExt as _, WithElement as _,
53 },
54 Ampere, Caveat, Cost, DisplayOption, Kw, Kwh, Money, Price, SaturatingAdd as _,
55 SaturatingSub as _, Version, Versioned as _,
56};
57
58pub type Verdict<T> = crate::Verdict<T, Warning>;
59type VerdictDeferred<T> = warning::VerdictDeferred<T, Warning>;
60
61#[derive(Debug)]
66struct PeriodNormalized {
67 consumed: Consumed,
69
70 start_snapshot: TotalsSnapshot,
72
73 end_snapshot: TotalsSnapshot,
75}
76
77#[derive(Clone)]
79#[cfg_attr(test, derive(Default))]
80pub(crate) struct Consumed {
81 pub current_max: Option<Ampere>,
83
84 pub current_min: Option<Ampere>,
86
87 pub duration_charging: Option<TimeDelta>,
89
90 pub duration_idle: Option<TimeDelta>,
92
93 pub energy: Option<Kwh>,
95
96 pub power_max: Option<Kw>,
98
99 pub power_min: Option<Kw>,
101}
102
103impl fmt::Debug for Consumed {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 f.debug_struct("Consumed")
106 .field("current_max", &self.current_max)
107 .field("current_min", &self.current_min)
108 .field(
109 "duration_charging",
110 &self.duration_charging.map(|dt| dt.as_hms()),
111 )
112 .field("duration_idle", &self.duration_idle.map(|dt| dt.as_hms()))
113 .field("energy", &self.energy)
114 .field("power_max", &self.power_max)
115 .field("power_min", &self.power_min)
116 .finish()
117 }
118}
119
120#[derive(Clone)]
122struct TotalsSnapshot {
123 date_time: DateTime<Utc>,
125
126 energy: Kwh,
128
129 local_timezone: Tz,
131
132 duration_charging: TimeDelta,
134
135 duration_total: TimeDelta,
137}
138
139impl fmt::Debug for TotalsSnapshot {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 f.debug_struct("TotalsSnapshot")
142 .field("date_time", &self.date_time)
143 .field("energy", &self.energy)
144 .field("local_timezone", &self.local_timezone)
145 .field("duration_charging", &self.duration_charging.as_hms())
146 .field("duration_total", &self.duration_total.as_hms())
147 .finish()
148 }
149}
150
151impl TotalsSnapshot {
152 fn zero(date_time: DateTime<Utc>, local_timezone: Tz) -> Self {
154 Self {
155 date_time,
156 energy: Kwh::zero(),
157 local_timezone,
158 duration_charging: TimeDelta::zero(),
159 duration_total: TimeDelta::zero(),
160 }
161 }
162
163 fn next(&self, consumed: &Consumed, date_time: DateTime<Utc>) -> Self {
165 let duration = date_time.signed_duration_since(self.date_time);
166
167 let mut next = Self {
168 date_time,
169 energy: self.energy,
170 local_timezone: self.local_timezone,
171 duration_charging: self.duration_charging,
172 duration_total: self.duration_total.saturating_add(duration),
173 };
174
175 if let Some(duration) = consumed.duration_charging {
176 next.duration_charging = next.duration_charging.saturating_add(duration);
177 }
178
179 if let Some(energy) = consumed.energy {
180 next.energy = next.energy.saturating_add(energy);
181 }
182 next
183 }
184
185 fn local_time(&self) -> chrono::NaiveTime {
187 self.date_time.with_timezone(&self.local_timezone).time()
188 }
189
190 fn local_date(&self) -> chrono::NaiveDate {
192 self.date_time
193 .with_timezone(&self.local_timezone)
194 .date_naive()
195 }
196
197 fn local_weekday(&self) -> chrono::Weekday {
199 self.date_time.with_timezone(&self.local_timezone).weekday()
200 }
201}
202
203pub struct Report {
206 pub periods: Vec<PeriodReport>,
208
209 pub tariff_used: TariffOrigin,
211
212 pub tariff_reports: Vec<TariffReport>,
216
217 pub timezone: String,
219
220 pub billed_charging_time: Option<TimeDelta>,
223
224 pub billed_energy: Option<Kwh>,
226
227 pub billed_idle_time: Option<TimeDelta>,
229
230 pub total_charging_time: Option<TimeDelta>,
236
237 pub total_energy: Total<Kwh, Option<Kwh>>,
239
240 pub total_idle_time: Total<Option<TimeDelta>>,
247
248 pub total_time: Total<TimeDelta>,
250
251 pub total_cost: Total<Price, Option<Price>>,
254
255 pub total_energy_cost: Total<Option<Price>>,
257
258 pub total_fixed_cost: Total<Option<Price>>,
261
262 pub total_idle_cost: Total<Option<Price>>,
269
270 pub total_charging_time_cost: Total<Option<Price>>,
275}
276
277impl fmt::Debug for Report {
278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 f.debug_struct("Report")
280 .field("periods", &self.periods)
281 .field("tariff_used", &self.tariff_used)
282 .field("tariff_reports", &self.tariff_reports)
283 .field("timezone", &self.timezone)
284 .field(
285 "billed_charging_time",
286 &self.billed_charging_time.map(|dt| dt.as_hms()),
287 )
288 .field("billed_energy", &self.billed_energy)
289 .field(
290 "billed_idle_time",
291 &self.billed_idle_time.map(|dt| dt.as_hms()),
292 )
293 .field(
294 "total_charging_time",
295 &self.total_charging_time.map(|dt| dt.as_hms()),
296 )
297 .field("total_energy", &self.total_energy)
298 .field("total_idle_time", &self.total_idle_time)
299 .field("total_time", &self.total_time)
300 .field("total_cost", &self.total_cost)
301 .field("total_energy_cost", &self.total_energy_cost)
302 .field("total_fixed_cost", &self.total_fixed_cost)
303 .field("total_idle_cost", &self.total_idle_cost)
304 .field("total_charging_time_cost", &self.total_charging_time_cost)
305 .finish()
306 }
307}
308
309#[derive(Debug)]
311pub enum Warning {
312 Country(country::Warning),
313 Currency(currency::Warning),
314 DateTime(datetime::Warning),
315 Decode(json::decode::Warning),
316 Duration(duration::Warning),
317 Enum(enumeration::Warning),
318
319 CountryShouldBeAlpha2,
323
324 FieldInvalidType {
326 expected_type: json::ValueKind,
328 },
329
330 FieldInvalidValue {
332 value: String,
334
335 message: Cow<'static, str>,
337 },
338
339 FieldRequired {
341 field_name: Cow<'static, str>,
342 },
343
344 Money(money::Warning),
345
346 NoPeriods,
348
349 NoValidTariff,
359
360 Number(number::Warning),
361
362 PeriodsOutsideStartEndDateTime {
365 cdr_range: Range<DateTime<Utc>>,
366 period_range: PeriodRange,
367 },
368
369 String(string::Warning),
370
371 Tariff(crate::tariff::Warning),
374}
375
376impl Warning {
377 fn field_invalid_value(
379 value: impl Into<String>,
380 message: impl Into<Cow<'static, str>>,
381 ) -> Self {
382 Warning::FieldInvalidValue {
383 value: value.into(),
384 message: message.into(),
385 }
386 }
387}
388
389impl fmt::Display for Warning {
390 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391 match self {
392 Self::Country(warning) => write!(f, "{warning}"),
393 Self::CountryShouldBeAlpha2 => {
394 f.write_str("The `$.country` field should be an alpha-2 country code.")
395 }
396 Self::Currency(warning) => write!(f, "{warning}"),
397 Self::DateTime(warning) => write!(f, "{warning}"),
398 Self::Decode(warning) => write!(f, "{warning}"),
399 Self::Duration(warning) => write!(f, "{warning}"),
400 Self::Enum(warning) => write!(f, "{warning}"),
401 Self::FieldInvalidType { expected_type } => {
402 write!(f, "Field has invalid type. Expected type `{expected_type}`")
403 }
404 Self::FieldInvalidValue { value, message } => {
405 write!(f, "Field has invalid value `{value}`: {message}")
406 }
407 Self::FieldRequired { field_name } => {
408 write!(f, "Field is required: `{field_name}`")
409 }
410 Self::Money(warning) => write!(f, "{warning}"),
411 Self::NoPeriods => f.write_str("The CDR has no charging periods"),
412 Self::NoValidTariff => {
413 f.write_str("No valid tariff has been found in the list of provided tariffs")
414 }
415 Self::Number(warning) => write!(f, "{warning}"),
416 Self::PeriodsOutsideStartEndDateTime {
417 cdr_range: Range { start, end },
418 period_range,
419 } => {
420 write!(
421 f,
422 "The CDR's charging period time range is not contained within the `start_date_time` \
423 and `end_date_time`; cdr: [start: {start}, end: {end}], period: {period_range}",
424 )
425 }
426 Self::String(warning) => write!(f, "{warning}"),
427 Self::Tariff(warnings) => {
428 write!(f, "Tariff warnings: {warnings:?}")
429 }
430 }
431 }
432}
433
434impl crate::Warning for Warning {
435 fn id(&self) -> warning::Id {
436 match self {
437 Self::Country(warning) => warning.id(),
438 Self::CountryShouldBeAlpha2 => warning::Id::from_static("country_should_be_alpha_2"),
439 Self::Currency(warning) => warning.id(),
440 Self::DateTime(warning) => warning.id(),
441 Self::Decode(warning) => warning.id(),
442 Self::Duration(warning) => warning.id(),
443 Self::Enum(warning) => warning.id(),
444 Self::FieldInvalidType { expected_type } => {
445 warning::Id::from_string(format!("field_invalid_type({expected_type})"))
446 }
447 Self::FieldInvalidValue { value, .. } => {
448 warning::Id::from_string(format!("field_invalid_value({value})"))
449 }
450 Self::FieldRequired { field_name } => {
451 warning::Id::from_string(format!("field_required({field_name})"))
452 }
453 Self::Money(warning) => warning.id(),
454 Self::NoPeriods => warning::Id::from_static("no_periods"),
455 Self::NoValidTariff => warning::Id::from_static("no_valid_tariff"),
456 Self::Number(warning) => warning.id(),
457 Self::PeriodsOutsideStartEndDateTime { .. } => {
458 warning::Id::from_static("periods_outside_start_end_date_time")
459 }
460 Self::String(warning) => warning.id(),
461 Self::Tariff(warning) => warning.id(),
462 }
463 }
464}
465
466from_warning_all!(
467 country::Warning => Warning::Country,
468 currency::Warning => Warning::Currency,
469 datetime::Warning => Warning::DateTime,
470 duration::Warning => Warning::Duration,
471 enumeration::Warning => Warning::Enum,
472 json::decode::Warning => Warning::Decode,
473 money::Warning => Warning::Money,
474 number::Warning => Warning::Number,
475 string::Warning => Warning::String,
476 crate::tariff::Warning => Warning::Tariff
477);
478
479#[derive(Debug)]
481pub struct TariffReport {
482 pub origin: TariffOrigin,
484
485 pub warnings: BTreeMap<json::Path, Vec<crate::tariff::Warning>>,
489}
490
491#[derive(Clone, Debug)]
493pub struct TariffOrigin {
494 pub index: usize,
496
497 pub id: String,
499
500 pub currency: currency::Code,
502}
503
504#[derive(Debug)]
506pub(crate) struct Period {
507 pub start_date_time: DateTime<Utc>,
509
510 pub consumed: Consumed,
512}
513
514#[derive(Debug)]
516pub struct Dimensions {
517 pub energy: Option<Dimension<Kwh>>,
519
520 pub flat: Dimension<()>,
522
523 pub duration_charging: Option<Dimension<TimeDelta>>,
525
526 pub duration_idle: Option<Dimension<TimeDelta>>,
528}
529
530impl Dimensions {
531 fn new(components: ComponentSet, consumed: &Consumed) -> Self {
533 let ComponentSet {
534 energy: energy_price,
535 flat: flat_price,
536 duration_charging: duration_charging_price,
537 duration_idle: duration_idle_price,
538 } = components;
539
540 let Consumed {
541 duration_charging,
542 duration_idle,
543 energy,
544 current_max: _,
545 current_min: _,
546 power_max: _,
547 power_min: _,
548 } = consumed;
549
550 Self {
551 energy: (*energy).map(|e| Dimension {
552 price: energy_price,
553 volume: e,
554 billed_volume: e,
555 }),
556 flat: Dimension {
557 price: flat_price,
558 volume: (),
559 billed_volume: (),
560 },
561 duration_charging: (*duration_charging).map(|dc| Dimension {
562 price: duration_charging_price,
563 volume: dc,
564 billed_volume: dc,
565 }),
566 duration_idle: (*duration_idle).map(|di| Dimension {
567 price: duration_idle_price,
568 volume: di,
569 billed_volume: di,
570 }),
571 }
572 }
573}
574
575#[derive(Debug)]
576pub struct Dimension<V> {
578 pub price: Option<Component>,
582
583 pub volume: V,
585
586 pub billed_volume: V,
594}
595
596impl<V: Cost> Dimension<V> {
597 pub fn cost(&self) -> Option<Price> {
599 let Some(price_component) = &self.price else {
600 return None;
601 };
602
603 let excl_vat = self.billed_volume.cost(price_component.price);
604
605 let incl_vat = match price_component.vat {
606 VatOrigin::Provided(vat) => Some(excl_vat.apply_vat(vat)),
607 VatOrigin::NotProvided => Some(excl_vat),
608 VatOrigin::Unknown => None,
609 };
610
611 Some(Price { excl_vat, incl_vat })
612 }
613}
614
615#[derive(Debug)]
620pub struct ComponentSet {
621 pub energy: Option<Component>,
623
624 pub flat: Option<Component>,
626
627 pub duration_charging: Option<Component>,
629
630 pub duration_idle: Option<Component>,
632}
633
634impl ComponentSet {
635 fn has_all_components(&self) -> bool {
637 let Self {
638 energy,
639 flat,
640 duration_charging,
641 duration_idle,
642 } = self;
643
644 flat.is_some() && energy.is_some() && duration_idle.is_some() && duration_charging.is_some()
645 }
646}
647
648#[derive(Clone, Debug)]
653pub struct Component {
654 price: Money,
656
657 vat: VatOrigin,
660
661 step_size: u64,
669}
670
671impl Component {
672 fn new(component: &crate::tariff::v221::PriceComponent) -> Self {
674 let crate::tariff::v221::PriceComponent {
675 price,
676 vat,
677 step_size,
678 dimension_type: _,
679 } = component;
680
681 Self {
682 price: *price,
683 vat: *vat,
684 step_size: *step_size,
685 }
686 }
687
688 pub fn price(&self) -> Money {
690 self.price
691 }
692}
693
694#[derive(Debug)]
707pub struct Total<TCdr, TCalc = TCdr> {
708 pub cdr: TCdr,
710
711 pub calculated: TCalc,
713}
714
715#[derive(Debug)]
717pub enum PeriodRange {
718 Many(Range<DateTime<Utc>>),
721
722 Single(DateTime<Utc>),
724}
725
726impl fmt::Display for PeriodRange {
727 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
728 match self {
729 PeriodRange::Many(Range { start, end }) => write!(f, "[start: {start}, end: {end}]"),
730 PeriodRange::Single(date_time) => write!(f, "{date_time}"),
731 }
732 }
733}
734
735#[derive(Debug)]
739pub enum TariffSource<'buf> {
740 UseCdr,
742
743 Override(Vec<crate::tariff::Versioned<'buf>>),
745}
746
747impl<'buf> TariffSource<'buf> {
748 pub fn single(tariff: crate::tariff::Versioned<'buf>) -> Self {
750 Self::Override(vec![tariff])
751 }
752}
753
754#[instrument(skip_all)]
758pub(super) fn cdr(
759 cdr_elem: &crate::cdr::Versioned<'_>,
760 tariff_source: TariffSource<'_>,
761 timezone: Tz,
762) -> Verdict<Report> {
763 let source_version = cdr_elem.version();
764 let cdr = parse_cdr(cdr_elem)?;
765
766 match tariff_source {
767 TariffSource::UseCdr => {
768 let (v221::cdr::WithTariffs { cdr, tariffs }, warnings) = cdr.into_parts();
769 debug!("Using tariffs from CDR");
770 let tariffs = tariffs
771 .iter()
772 .map(|elem| {
773 match source_version {
775 Version::V221 => crate::tariff::v221::Tariff::from_json(elem),
776 Version::V211 => {
777 let tariff = crate::tariff::v211::Tariff::from_json(elem);
778 tariff.map_caveat(crate::tariff::v221::Tariff::from)
781 }
782 }
783 })
784 .collect::<Result<Vec<_>, _>>()?;
785
786 let cdr = cdr.into_caveat(warnings);
787
788 Ok(price_v221_cdr_with_tariffs(
789 cdr_elem, cdr, tariffs, timezone,
790 )?)
791 }
792 TariffSource::Override(tariffs) => {
793 let cdr = cdr.map(v221::cdr::WithTariffs::discard_tariffs);
794
795 debug!("Using override tariffs");
796 let tariffs = tariffs
797 .iter()
798 .map(tariff::parse)
799 .collect::<Result<Vec<_>, _>>()?;
800
801 Ok(price_v221_cdr_with_tariffs(
802 cdr_elem, cdr, tariffs, timezone,
803 )?)
804 }
805 }
806}
807
808fn price_v221_cdr_with_tariffs(
815 cdr_elem: &crate::cdr::Versioned<'_>,
816 cdr: Caveat<v221::Cdr, Warning>,
817 tariffs: Vec<Caveat<crate::tariff::v221::Tariff<'_>, crate::tariff::Warning>>,
818 timezone: Tz,
819) -> Verdict<Report> {
820 debug!(?timezone, version = ?cdr_elem.version(), "Pricing CDR");
821 let (cdr, mut warnings) = cdr.into_parts();
822 let v221::Cdr {
823 start_date_time,
824 end_date_time,
825 charging_periods,
826 totals: cdr_totals,
827 } = cdr;
828
829 let (tariff_reports, tariffs): (Vec<_>, Vec<_>) = tariffs
834 .into_iter()
835 .enumerate()
836 .map(|(index, tariff)| {
837 let (tariff, warnings) = tariff.into_parts();
838 (
839 TariffReport {
840 origin: TariffOrigin {
841 index,
842 id: tariff.id.to_string(),
843 currency: tariff.currency,
844 },
845 warnings: warnings.into_path_map(),
846 },
847 tariff,
848 )
849 })
850 .unzip();
851
852 debug!(tariffs = ?tariffs.iter().map(|t| t.id).collect::<Vec<_>>(), "Found tariffs(by id) in CDR");
853
854 let tariffs_normalized = tariff::normalize_all(&tariffs);
855 let Some((tariff_index, tariff)) =
856 tariff::find_first_active(tariffs_normalized, start_date_time)
857 else {
858 return warnings.bail(cdr_elem.as_element(), Warning::NoValidTariff);
859 };
860
861 debug!(tariff_index, id = ?tariff.id(), "Found active tariff");
862 debug!(%timezone, "Found timezone");
863
864 let periods = charging_periods.into_iter().map(Period::from).collect();
866
867 let periods = normalize_periods(periods, end_date_time, timezone);
868 let price_cdr_report = price_periods(&periods, &tariff)
869 .with_element(cdr_elem.as_element())?
870 .gather_warnings_into(&mut warnings);
871
872 if tariff.has_reservation_elements() {
873 warnings.insert(
874 cdr_elem.as_element(),
875 Warning::Tariff(crate::tariff::Warning::ReservationElementSkipped),
876 );
877 }
878
879 let mut report = generate_report(
880 &cdr_totals,
881 timezone,
882 tariff_reports,
883 price_cdr_report,
884 TariffOrigin {
885 index: tariff_index,
886 id: tariff.id().to_owned(),
887 currency: tariff.currency(),
888 },
889 );
890
891 if let Some(total_cost) = report.total_cost.calculated.as_mut() {
892 if let Some(min_price) = tariff.min_price() {
893 if *total_cost < min_price {
894 *total_cost = min_price;
895 warnings.insert(
896 cdr_elem.as_element(),
897 crate::tariff::Warning::TotalCostClampedToMin.into(),
898 );
899 }
900 }
901
902 if let Some(max_price) = tariff.max_price() {
903 if *total_cost > max_price {
904 *total_cost = max_price;
905 warnings.insert(
906 cdr_elem.as_element(),
907 crate::tariff::Warning::TotalCostClampedToMax.into(),
908 );
909 }
910 }
911 }
912
913 Ok(report.into_caveat(warnings))
914}
915
916pub(crate) fn periods(
918 end_date_time: DateTime<Utc>,
919 timezone: Tz,
920 tariff_elem: &crate::tariff::v221::Tariff<'_>,
921 mut periods: Vec<Period>,
922) -> VerdictDeferred<PeriodsReport> {
923 periods.sort_by_key(|p| p.start_date_time);
926 let tariff = Tariff::from_v221(tariff_elem);
927 let periods = normalize_periods(periods, end_date_time, timezone);
928 price_periods(&periods, &tariff)
929}
930
931fn normalize_periods(
932 periods: Vec<Period>,
933 end_date_time: DateTime<Utc>,
934 local_timezone: Tz,
935) -> Vec<PeriodNormalized> {
936 debug!("Normalizing CDR periods");
937
938 let mut previous_end_snapshot = Option::<TotalsSnapshot>::None;
940
941 let end_dates = {
943 let mut end_dates = periods
944 .iter()
945 .skip(1)
946 .map(|p| p.start_date_time)
947 .collect::<Vec<_>>();
948
949 end_dates.push(end_date_time);
951 end_dates
952 };
953
954 let periods = periods
955 .into_iter()
956 .zip(end_dates)
957 .enumerate()
958 .map(|(index, (period, end_date_time))| {
959 trace!(index, "processing\n{period:#?}");
960 let Period {
961 start_date_time,
962 consumed,
963 } = period;
964
965 let period = if let Some(prev_end_snapshot) = previous_end_snapshot.take() {
966 let start_snapshot = prev_end_snapshot;
967 let end_snapshot = start_snapshot.next(&consumed, end_date_time);
968
969 let period = PeriodNormalized {
970 consumed,
971 start_snapshot,
972 end_snapshot,
973 };
974 trace!("Adding new period based on the last added\n{period:#?}");
975 period
976 } else {
977 let start_snapshot = TotalsSnapshot::zero(start_date_time, local_timezone);
978 let end_snapshot = start_snapshot.next(&consumed, end_date_time);
979
980 let period = PeriodNormalized {
981 consumed,
982 start_snapshot,
983 end_snapshot,
984 };
985 trace!("Adding new period\n{period:#?}");
986 period
987 };
988
989 previous_end_snapshot.replace(period.end_snapshot.clone());
990 period
991 })
992 .collect::<Vec<_>>();
993
994 periods
995}
996
997fn price_periods(periods: &[PeriodNormalized], tariff: &Tariff) -> VerdictDeferred<PeriodsReport> {
999 debug!(count = periods.len(), "Pricing CDR periods");
1000
1001 if tracing::enabled!(tracing::Level::TRACE) {
1002 trace!("# CDR period list:");
1003 for period in periods {
1004 trace!("{period:#?}");
1005 }
1006 }
1007
1008 let period_totals = period_totals(periods, tariff);
1009 let (billed, mut warnings) = period_totals.calculate_billed()?.into_parts();
1010
1011 if tariff.has_reservation_elements() {
1012 warnings.insert(Warning::Tariff(
1013 crate::tariff::Warning::ReservationElementSkipped,
1014 ));
1015 }
1016
1017 let (billable, periods, totals) = billed;
1018 let total_costs = total_costs(&periods, tariff);
1019 let report = PeriodsReport {
1020 billable,
1021 periods,
1022 totals,
1023 total_costs,
1024 };
1025
1026 Ok(report.into_caveat_deferred(warnings))
1027}
1028
1029pub(crate) struct PeriodsReport {
1031 pub billable: Billable,
1033
1034 pub periods: Vec<PeriodReport>,
1036
1037 pub totals: Totals,
1039
1040 pub total_costs: TotalCosts,
1042}
1043
1044#[derive(Debug)]
1050pub struct PeriodReport {
1051 pub start_date_time: DateTime<Utc>,
1053
1054 pub end_date_time: DateTime<Utc>,
1056
1057 pub dimensions: Dimensions,
1059}
1060
1061impl PeriodReport {
1062 pub fn cost(&self) -> Option<Price> {
1064 [
1065 self.dimensions
1066 .duration_charging
1067 .as_ref()
1068 .and_then(Dimension::cost),
1069 self.dimensions
1070 .duration_idle
1071 .as_ref()
1072 .and_then(Dimension::cost),
1073 self.dimensions.flat.cost(),
1074 self.dimensions.energy.as_ref().and_then(Dimension::cost),
1075 ]
1076 .into_iter()
1077 .fold(None, |accum, next| {
1078 if accum.is_none() && next.is_none() {
1079 None
1080 } else {
1081 Some(
1082 accum
1083 .unwrap_or_default()
1084 .saturating_add(next.unwrap_or_default()),
1085 )
1086 }
1087 })
1088 }
1089}
1090
1091#[derive(Debug)]
1096struct PeriodReportScratch {
1097 start_date_time: DateTime<Utc>,
1098 end_date_time: DateTime<Utc>,
1099 dimensions: Dimensions,
1100 step_size_duration_charging: Option<Component>,
1101 step_size_duration_idle: Option<Component>,
1102 step_size_energy: Option<Component>,
1103}
1104
1105impl From<PeriodReportScratch> for PeriodReport {
1106 fn from(scratch: PeriodReportScratch) -> Self {
1107 Self {
1108 start_date_time: scratch.start_date_time,
1109 end_date_time: scratch.end_date_time,
1110 dimensions: scratch.dimensions,
1111 }
1112 }
1113}
1114
1115#[derive(Debug)]
1117struct PeriodTotals {
1118 periods: Vec<PeriodReportScratch>,
1120
1121 totals: Totals,
1123}
1124
1125#[derive(Debug, Default)]
1127pub(crate) struct Totals {
1128 pub energy: Option<Kwh>,
1130
1131 pub duration_charging: Option<TimeDelta>,
1135
1136 pub duration_idle: Option<TimeDelta>,
1140}
1141
1142impl PeriodTotals {
1143 fn calculate_billed(self) -> VerdictDeferred<(Billable, Vec<PeriodReport>, Totals)> {
1145 let mut warnings = warning::SetDeferred::new();
1146 let Self {
1147 mut periods,
1148 totals,
1149 } = self;
1150
1151 let billable =
1152 apply_step_sizes(&mut periods, &totals)?.gather_deferred_warnings_into(&mut warnings);
1153
1154 let periods = periods.into_iter().map(PeriodReport::from).collect();
1155
1156 Ok((billable, periods, totals).into_caveat_deferred(warnings))
1157 }
1158}
1159
1160#[derive(Debug)]
1162pub(crate) struct Billable {
1163 duration_charging: Option<TimeDelta>,
1165
1166 duration_idle: Option<TimeDelta>,
1168
1169 energy: Option<Kwh>,
1171}
1172
1173fn period_totals(periods: &[PeriodNormalized], tariff: &Tariff) -> PeriodTotals {
1176 let mut has_flat_fee = false;
1177 let mut totals = Totals::default();
1178
1179 debug!(
1180 tariff_id = tariff.id(),
1181 period_count = periods.len(),
1182 "Accumulating dimension totals for each period"
1183 );
1184
1185 let periods = periods
1186 .iter()
1187 .enumerate()
1188 .map(|(index, period)| {
1189 let mut component_set = tariff.active_components(period);
1190 trace!(
1191 index,
1192 "Creating charge period with Dimension\n{period:#?}\n{component_set:#?}"
1193 );
1194
1195 if component_set.flat.is_some() {
1196 if has_flat_fee {
1197 component_set.flat = None;
1198 } else {
1199 has_flat_fee = true;
1200 }
1201 }
1202
1203 let step_size_duration_charging = if period.consumed.duration_charging.is_some() {
1205 component_set.duration_charging.clone()
1206 } else {
1207 None
1208 };
1209 let step_size_duration_idle = if period.consumed.duration_idle.is_some() {
1210 component_set.duration_idle.clone()
1211 } else {
1212 None
1213 };
1214 let step_size_energy = if period.consumed.energy.is_some() {
1215 component_set.energy.clone()
1216 } else {
1217 None
1218 };
1219
1220 let dimensions = Dimensions::new(component_set, &period.consumed);
1221
1222 trace!(period_index = index, "Dimensions created\n{dimensions:#?}");
1223
1224 if let Some(dim) = &dimensions.duration_charging {
1225 let acc = totals.duration_charging.get_or_insert_default();
1226 *acc = acc.saturating_add(dim.volume);
1227 }
1228
1229 if let Some(dim) = &dimensions.energy {
1230 let acc = totals.energy.get_or_insert_default();
1231 *acc = acc.saturating_add(dim.volume);
1232 }
1233
1234 if let Some(dim) = &dimensions.duration_idle {
1235 let acc = totals.duration_idle.get_or_insert_default();
1236 *acc = acc.saturating_add(dim.volume);
1237 }
1238
1239 trace!(period_index = index, ?totals, "Update totals");
1240
1241 PeriodReportScratch {
1242 start_date_time: period.start_snapshot.date_time,
1243 end_date_time: period.end_snapshot.date_time,
1244 dimensions,
1245 step_size_duration_charging,
1246 step_size_duration_idle,
1247 step_size_energy,
1248 }
1249 })
1250 .collect::<Vec<_>>();
1251
1252 PeriodTotals { periods, totals }
1253}
1254
1255#[derive(Debug, Default)]
1257pub(crate) struct TotalCosts {
1258 pub energy: Option<Price>,
1260
1261 pub fixed: Option<Price>,
1263
1264 pub duration_charging: Option<Price>,
1266
1267 pub duration_idle: Option<Price>,
1269}
1270
1271impl TotalCosts {
1272 pub(crate) fn total(&self) -> Option<Price> {
1276 let Self {
1277 energy,
1278 fixed,
1279 duration_charging,
1280 duration_idle,
1281 } = self;
1282 debug!(
1283 energy = %DisplayOption(*energy),
1284 fixed = %DisplayOption(*fixed),
1285 duration_charging = %DisplayOption(*duration_charging),
1286 duration_idle = %DisplayOption(*duration_idle),
1287 "Calculating total costs."
1288 );
1289 [energy, fixed, duration_charging, duration_idle]
1290 .into_iter()
1291 .fold(None, |accum: Option<Price>, next| match (accum, next) {
1292 (None, None) => None,
1293 _ => Some(
1294 accum
1295 .unwrap_or_default()
1296 .saturating_add(next.unwrap_or_default()),
1297 ),
1298 })
1299 }
1300}
1301
1302fn total_costs(periods: &[PeriodReport], tariff: &Tariff) -> TotalCosts {
1304 let mut total_costs = TotalCosts::default();
1305
1306 debug!(
1307 tariff_id = tariff.id(),
1308 period_count = periods.len(),
1309 "Accumulating dimension costs for each period"
1310 );
1311 for (index, period) in periods.iter().enumerate() {
1312 let dimensions = &period.dimensions;
1313
1314 trace!(period_index = index, "Processing period");
1315
1316 let energy_cost = dimensions.energy.as_ref().and_then(Dimension::cost);
1317 let fixed_cost = dimensions.flat.cost();
1318 let duration_charging_cost = dimensions
1319 .duration_charging
1320 .as_ref()
1321 .and_then(Dimension::cost);
1322 let duration_idle_cost = dimensions.duration_idle.as_ref().and_then(Dimension::cost);
1323
1324 trace!(?total_costs.energy, ?energy_cost, "Energy cost");
1325 trace!(?total_costs.duration_charging, ?duration_charging_cost, "Charging cost");
1326 trace!(?total_costs.duration_idle, ?duration_idle_cost, "Idle cost");
1327 trace!(?total_costs.fixed, ?fixed_cost, "Fixed cost");
1328
1329 total_costs.energy = match (total_costs.energy, energy_cost) {
1330 (None, None) => None,
1331 (total, period) => Some(
1332 total
1333 .unwrap_or_default()
1334 .saturating_add(period.unwrap_or_default()),
1335 ),
1336 };
1337
1338 total_costs.duration_charging =
1339 match (total_costs.duration_charging, duration_charging_cost) {
1340 (None, None) => None,
1341 (total, period) => Some(
1342 total
1343 .unwrap_or_default()
1344 .saturating_add(period.unwrap_or_default()),
1345 ),
1346 };
1347
1348 total_costs.duration_idle = match (total_costs.duration_idle, duration_idle_cost) {
1349 (None, None) => None,
1350 (total, period) => Some(
1351 total
1352 .unwrap_or_default()
1353 .saturating_add(period.unwrap_or_default()),
1354 ),
1355 };
1356
1357 total_costs.fixed = match (total_costs.fixed, fixed_cost) {
1358 (None, None) => None,
1359 (total, period) => Some(
1360 total
1361 .unwrap_or_default()
1362 .saturating_add(period.unwrap_or_default()),
1363 ),
1364 };
1365
1366 trace!(period_index = index, ?total_costs, "Update totals");
1367 }
1368
1369 total_costs
1370}
1371
1372fn generate_report(
1373 cdr_totals: &v221::cdr::Totals,
1374 timezone: Tz,
1375 tariff_reports: Vec<TariffReport>,
1376 price_periods_report: PeriodsReport,
1377 tariff_used: TariffOrigin,
1378) -> Report {
1379 let PeriodsReport {
1380 billable,
1381 periods,
1382 totals,
1383 total_costs,
1384 } = price_periods_report;
1385 trace!("Update billed totals {billable:#?}");
1386
1387 let total_cost = total_costs.total();
1388
1389 debug!(total_cost = %DisplayOption(total_cost.as_ref()));
1390
1391 let total_time = {
1392 debug!(
1393 period_start = %DisplayOption(periods.first().map(|p| p.start_date_time)),
1394 period_end = %DisplayOption(periods.last().map(|p| p.end_date_time)),
1395 "Calculating `total_time`"
1396 );
1397
1398 periods
1399 .first()
1400 .zip(periods.last())
1401 .map(|(first, last)| {
1402 last.end_date_time
1403 .signed_duration_since(first.start_date_time)
1404 })
1405 .unwrap_or_default()
1406 };
1407 debug!(total_time = %Hms(total_time));
1408
1409 let report = Report {
1410 periods,
1411 tariff_used,
1412 timezone: timezone.to_string(),
1413 billed_idle_time: billable.duration_idle,
1414 billed_energy: billable.energy.round_to_ocpi_scale(),
1415 billed_charging_time: billable.duration_charging,
1416 tariff_reports,
1417 total_charging_time: totals.duration_charging,
1418 total_cost: Total {
1419 cdr: cdr_totals.cost.round_to_ocpi_scale(),
1420 calculated: total_cost.round_to_ocpi_scale(),
1421 },
1422 total_charging_time_cost: Total {
1423 cdr: cdr_totals.duration_charging_cost.round_to_ocpi_scale(),
1424 calculated: total_costs.duration_charging.round_to_ocpi_scale(),
1425 },
1426 total_time: Total {
1427 cdr: cdr_totals.duration_charging,
1428 calculated: total_time,
1429 },
1430 total_idle_cost: Total {
1431 cdr: cdr_totals.duration_idle_cost.round_to_ocpi_scale(),
1432 calculated: total_costs.duration_idle.round_to_ocpi_scale(),
1433 },
1434 total_idle_time: Total {
1435 cdr: cdr_totals.duration_idle,
1436 calculated: totals.duration_idle,
1437 },
1438 total_energy_cost: Total {
1439 cdr: cdr_totals.energy_cost.round_to_ocpi_scale(),
1440 calculated: total_costs.energy.round_to_ocpi_scale(),
1441 },
1442 total_energy: Total {
1443 cdr: cdr_totals.energy.round_to_ocpi_scale(),
1444 calculated: totals.energy.round_to_ocpi_scale(),
1445 },
1446 total_fixed_cost: Total {
1447 cdr: cdr_totals.fixed_cost.round_to_ocpi_scale(),
1448 calculated: total_costs.fixed.round_to_ocpi_scale(),
1449 },
1450 };
1451
1452 trace!("{report:#?}");
1453
1454 report
1455}
1456
1457fn apply_step_sizes(
1460 periods: &mut [PeriodReportScratch],
1461 totals: &Totals,
1462) -> VerdictDeferred<Billable> {
1463 let mut warnings = warning::SetDeferred::new();
1464
1465 let has_idle_step_size = periods.iter().any(|p| p.step_size_duration_idle.is_some());
1466
1467 let duration_charging = if let Some(total) = totals.duration_charging {
1468 let mut result = Some(total);
1469 for period in periods.iter_mut().rev() {
1470 let Some(step) = period
1471 .step_size_duration_charging
1472 .as_ref()
1473 .map(|c| c.step_size)
1474 else {
1475 continue;
1476 };
1477 if has_idle_step_size {
1478 result = Some(total);
1479 } else if let Some(dim) = period.dimensions.duration_charging.as_mut() {
1480 let dt = duration_step_size(total, &mut dim.billed_volume, step)?
1481 .gather_deferred_warnings_into(&mut warnings);
1482 result = Some(dt);
1483 }
1484 break;
1485 }
1486 result
1487 } else {
1488 None
1489 };
1490
1491 let duration_idle = if let Some(total) = totals.duration_idle {
1492 let mut result = Some(total);
1493 for period in periods.iter_mut().rev() {
1494 let Some(step) = period.step_size_duration_idle.as_ref().map(|c| c.step_size) else {
1495 continue;
1496 };
1497 if let Some(dim) = period.dimensions.duration_idle.as_mut() {
1498 let dt = duration_step_size(total, &mut dim.billed_volume, step)?
1499 .gather_deferred_warnings_into(&mut warnings);
1500 result = Some(dt);
1501 }
1502 break;
1503 }
1504 result
1505 } else {
1506 None
1507 };
1508
1509 let energy = if let Some(total) = totals.energy {
1510 let mut result = Some(total);
1511 for period in periods.iter_mut().rev() {
1512 let Some(step) = period.step_size_energy.as_ref().map(|c| c.step_size) else {
1513 continue;
1514 };
1515 if step == 0 {
1516 result = Some(total);
1517 } else {
1518 let step_dec = Decimal::from(step);
1519 if let Some(dim) = period.dimensions.energy.as_mut() {
1520 let Some(watt_hours) = total.watt_hours().checked_div(step_dec) else {
1521 return warnings.bail(duration::Warning::Overflow.into());
1522 };
1523 let total_billed_volume =
1524 Kwh::from_watt_hours(watt_hours.ceil().saturating_mul(step_dec));
1525 let period_delta_volume = total_billed_volume.saturating_sub(total);
1526 dim.billed_volume = dim.billed_volume.saturating_add(period_delta_volume);
1527 result = Some(total_billed_volume);
1528 }
1529 }
1530 break;
1531 }
1532 result
1533 } else {
1534 None
1535 };
1536
1537 Ok(Billable {
1538 duration_charging,
1539 duration_idle,
1540 energy,
1541 }
1542 .into_caveat_deferred(warnings))
1543}
1544
1545fn delta_as_seconds_dec(delta: TimeDelta) -> Decimal {
1547 Decimal::from(delta.num_milliseconds())
1548 .checked_div(Decimal::from(duration::MILLIS_IN_SEC))
1549 .expect("Can't overflow; See test `as_seconds_dec_should_not_overflow`")
1550}
1551
1552fn delta_from_seconds_dec(seconds: Decimal) -> VerdictDeferred<TimeDelta> {
1554 let millis = seconds.saturating_mul(Decimal::from(duration::MILLIS_IN_SEC));
1555 let Ok(millis) = i64::try_from(millis) else {
1556 return Err(warning::ErrorSetDeferred::with_warn(
1557 duration::Warning::Overflow.into(),
1558 ));
1559 };
1560 let Some(delta) = TimeDelta::try_milliseconds(millis) else {
1561 return Err(warning::ErrorSetDeferred::with_warn(
1562 duration::Warning::Overflow.into(),
1563 ));
1564 };
1565 Ok(delta.into_caveat_deferred(warning::SetDeferred::new()))
1566}
1567
1568fn duration_step_size(
1570 total_volume: TimeDelta,
1571 period_billed_volume: &mut TimeDelta,
1572 step_size: u64,
1573) -> VerdictDeferred<TimeDelta> {
1574 if step_size == 0 {
1575 return Ok(total_volume.into_caveat_deferred(warning::SetDeferred::new()));
1576 }
1577
1578 let total_seconds = delta_as_seconds_dec(total_volume);
1579 let step_size = Decimal::from(step_size);
1580
1581 let Some(x) = total_seconds.checked_div(step_size) else {
1582 return Err(warning::ErrorSetDeferred::with_warn(
1583 duration::Warning::Overflow.into(),
1584 ));
1585 };
1586 let total_billed_volume = delta_from_seconds_dec(x.ceil().saturating_mul(step_size))?;
1587
1588 let period_delta_volume = total_billed_volume.saturating_sub(total_volume);
1589 *period_billed_volume = period_billed_volume.saturating_add(period_delta_volume);
1590
1591 Ok(total_billed_volume)
1592}
1593
1594fn parse_cdr<'buf>(cdr: &crate::cdr::Versioned<'buf>) -> Verdict<v221::cdr::WithTariffs<'buf>> {
1595 match cdr.version() {
1596 Version::V211 => {
1597 let cdr = v211::cdr::WithTariffs::from_json(cdr.as_element())?;
1598 Ok(cdr.map(v221::cdr::WithTariffs::from))
1599 }
1600 Version::V221 => v221::cdr::WithTariffs::from_json(cdr.as_element()),
1601 }
1602}