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_v211;
14
15#[cfg(test)]
16mod test_real_world_v221;
17
18#[cfg(test)]
19mod test_validate_cdr;
20
21mod tariff;
22mod v211;
23mod v221;
24
25use std::{borrow::Cow, collections::BTreeMap, fmt, ops::Range};
26
27use chrono::{DateTime, Datelike, TimeDelta, Utc};
28use chrono_tz::Tz;
29use rust_decimal::Decimal;
30use tracing::{debug, error, instrument, trace};
31
32use crate::{
33 country, currency, datetime,
34 duration::{self, Hms},
35 from_warning_all, into_caveat_all,
36 json::{self, FromJson as _},
37 money,
38 number::{self, RoundDecimal},
39 string,
40 warning::{
41 self, GatherDeferredWarnings as _, GatherWarnings as _, IntoCaveat,
42 IntoCaveatDeferred as _, VerdictExt as _, WithElement as _,
43 },
44 weekday, Ampere, Caveat, Cost, DisplayOption, Kw, Kwh, Money, ParseError, Price,
45 SaturatingAdd as _, SaturatingSub as _, VatApplicable, Version, Versioned as _,
46};
47
48use tariff::Tariff;
49
50type Verdict<T> = crate::Verdict<T, Warning>;
51type VerdictDeferred<T> = warning::VerdictDeferred<T, Warning>;
52
53into_caveat_all!(PeriodNormalized, PeriodsReport, Report);
54
55#[derive(Debug)]
60struct PeriodNormalized {
61 consumed: Consumed,
63
64 start_snapshot: TotalsSnapshot,
66
67 end_snapshot: TotalsSnapshot,
69}
70
71#[derive(Clone, Debug)]
73#[cfg_attr(test, derive(Default))]
74pub(crate) struct Consumed {
75 pub current_max: Option<Ampere>,
77
78 pub current_min: Option<Ampere>,
80
81 pub duration_charging: Option<TimeDelta>,
83
84 pub duration_parking: Option<TimeDelta>,
86
87 pub energy: Option<Kwh>,
89
90 pub power_max: Option<Kw>,
92
93 pub power_min: Option<Kw>,
95}
96
97#[derive(Clone, Debug)]
99struct TotalsSnapshot {
100 date_time: DateTime<Utc>,
102
103 energy: Kwh,
105
106 local_timezone: Tz,
108
109 duration_charging: TimeDelta,
111
112 duration_total: TimeDelta,
114}
115
116impl TotalsSnapshot {
117 fn zero(date_time: DateTime<Utc>, local_timezone: Tz) -> Self {
119 Self {
120 date_time,
121 energy: Kwh::zero(),
122 local_timezone,
123 duration_charging: TimeDelta::zero(),
124 duration_total: TimeDelta::zero(),
125 }
126 }
127
128 fn next(&self, consumed: &Consumed, date_time: DateTime<Utc>) -> Self {
130 let duration = date_time.signed_duration_since(self.date_time);
131
132 let mut next = Self {
133 date_time,
134 energy: self.energy,
135 local_timezone: self.local_timezone,
136 duration_charging: self.duration_charging,
137 duration_total: self.duration_total.saturating_add(duration),
138 };
139
140 if let Some(duration) = consumed.duration_charging {
141 next.duration_charging = next.duration_charging.saturating_add(duration);
142 }
143
144 if let Some(energy) = consumed.energy {
145 next.energy = next.energy.saturating_add(energy);
146 }
147
148 next
149 }
150
151 fn local_time(&self) -> chrono::NaiveTime {
153 self.date_time.with_timezone(&self.local_timezone).time()
154 }
155
156 fn local_date(&self) -> chrono::NaiveDate {
158 self.date_time
159 .with_timezone(&self.local_timezone)
160 .date_naive()
161 }
162
163 fn local_weekday(&self) -> chrono::Weekday {
165 self.date_time.with_timezone(&self.local_timezone).weekday()
166 }
167}
168
169#[derive(Debug)]
172pub struct Report {
173 pub periods: Vec<PeriodReport>,
175
176 pub tariff_used: TariffOrigin,
178
179 pub tariff_reports: Vec<TariffReport>,
183
184 pub timezone: String,
186
187 pub billed_charging_time: Option<TimeDelta>,
190
191 pub billed_energy: Option<Kwh>,
193
194 pub billed_parking_time: Option<TimeDelta>,
196
197 pub total_charging_time: Option<TimeDelta>,
203
204 pub total_energy: Total<Kwh, Option<Kwh>>,
206
207 pub total_parking_time: Total<Option<TimeDelta>>,
209
210 pub total_time: Total<TimeDelta>,
212
213 pub total_cost: Total<Price, Option<Price>>,
216
217 pub total_energy_cost: Total<Option<Price>>,
219
220 pub total_fixed_cost: Total<Option<Price>>,
222
223 pub total_parking_cost: Total<Option<Price>>,
225
226 pub total_reservation_cost: Total<Option<Price>>,
228
229 pub total_time_cost: Total<Option<Price>>,
231}
232
233#[derive(Debug)]
235pub enum Warning {
236 Country(country::Warning),
237 Currency(currency::Warning),
238 DateTime(datetime::Warning),
239 Decode(json::decode::Warning),
240 Duration(duration::Warning),
241
242 CountryShouldBeAlpha2,
246
247 DimensionShouldHaveVolume {
249 dimension_name: &'static str,
250 },
251
252 FieldInvalidType {
254 expected_type: json::ValueKind,
256 },
257
258 FieldInvalidValue {
260 value: String,
262
263 message: Cow<'static, str>,
265 },
266
267 FieldRequired {
269 field_name: Cow<'static, str>,
270 },
271
272 InternalError,
276
277 Money(money::Warning),
278
279 NoPeriods,
281
282 NoValidTariff,
292
293 Number(number::Warning),
294
295 Parse(ParseError),
297
298 PeriodsOutsideStartEndDateTime {
301 cdr_range: Range<DateTime<Utc>>,
302 period_range: PeriodRange,
303 },
304
305 String(string::Warning),
306
307 Tariff(crate::tariff::Warning),
310
311 Weekday(weekday::Warning),
312}
313
314impl Warning {
315 fn field_invalid_value(
317 value: impl Into<String>,
318 message: impl Into<Cow<'static, str>>,
319 ) -> Self {
320 Warning::FieldInvalidValue {
321 value: value.into(),
322 message: message.into(),
323 }
324 }
325}
326
327impl fmt::Display for Warning {
328 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329 match self {
330 Self::Country(warning_kind) => write!(f, "{warning_kind}"),
331 Self::CountryShouldBeAlpha2 => {
332 f.write_str("The `$.country` field should be an alpha-2 country code.")
333 }
334 Self::Currency(warning_kind) => write!(f, "{warning_kind}"),
335 Self::DateTime(warning_kind) => write!(f, "{warning_kind}"),
336 Self::Decode(warning_kind) => write!(f, "{warning_kind}"),
337 Self::DimensionShouldHaveVolume { dimension_name } => {
338 write!(f, "Dimension `{dimension_name}` should have volume")
339 }
340 Self::Duration(warning_kind) => write!(f, "{warning_kind}"),
341 Self::FieldInvalidType { expected_type } => {
342 write!(f, "Field has invalid type. Expected type `{expected_type}`")
343 }
344 Self::FieldInvalidValue { value, message } => {
345 write!(f, "Field has invalid value `{value}`: {message}")
346 }
347 Self::FieldRequired { field_name } => {
348 write!(f, "Field is required: `{field_name}`")
349 }
350 Self::InternalError => f.write_str("Internal error"),
351 Self::Money(warning_kind) => write!(f, "{warning_kind}"),
352 Self::NoPeriods => f.write_str("The CDR has no charging periods"),
353 Self::NoValidTariff => {
354 f.write_str("No valid tariff has been found in the list of provided tariffs")
355 }
356 Self::Number(warning_kind) => write!(f, "{warning_kind}"),
357 Self::Parse(err) => {
358 write!(f, "{err}")
359 }
360 Self::PeriodsOutsideStartEndDateTime {
361 cdr_range: Range { start, end },
362 period_range,
363 } => {
364 write!(
365 f,
366 "The CDR's charging period time range is not contained within the `start_date_time` \
367 and `end_date_time`; cdr: [start: {start}, end: {end}], period: {period_range}",
368 )
369 }
370 Self::String(warning_kind) => write!(f, "{warning_kind}"),
371 Self::Tariff(warnings) => {
372 write!(f, "Tariff warnings: {warnings:?}")
373 }
374 Self::Weekday(warning_kind) => write!(f, "{warning_kind}"),
375 }
376 }
377}
378
379impl crate::Warning for Warning {
380 fn id(&self) -> warning::Id {
381 match self {
382 Self::Country(kind) => kind.id(),
383 Self::CountryShouldBeAlpha2 => warning::Id::from_static("country_should_be_alpha_2"),
384 Self::Currency(kind) => kind.id(),
385 Self::DateTime(kind) => kind.id(),
386 Self::Decode(kind) => kind.id(),
387 Self::DimensionShouldHaveVolume { dimension_name } => {
388 warning::Id::from_string(format!("dimension_should_have_volume({dimension_name})"))
389 }
390 Self::Duration(kind) => kind.id(),
391 Self::FieldInvalidType { expected_type } => {
392 warning::Id::from_string(format!("field_invalid_type({expected_type})"))
393 }
394 Self::FieldInvalidValue { value, .. } => {
395 warning::Id::from_string(format!("field_invalid_value({value})"))
396 }
397 Self::FieldRequired { field_name } => {
398 warning::Id::from_string(format!("field_required({field_name})"))
399 }
400 Self::InternalError => warning::Id::from_static("internal_error"),
401 Self::Money(kind) => kind.id(),
402 Self::NoPeriods => warning::Id::from_static("no_periods"),
403 Self::NoValidTariff => warning::Id::from_static("no_valid_tariff"),
404 Self::Number(kind) => kind.id(),
405 Self::Parse(ParseError { object: _, kind }) => kind.id(),
406 Self::PeriodsOutsideStartEndDateTime { .. } => {
407 warning::Id::from_static("periods_outside_start_end_date_time")
408 }
409 Self::String(kind) => kind.id(),
410 Self::Tariff(kind) => kind.id(),
411 Self::Weekday(kind) => kind.id(),
412 }
413 }
414}
415
416from_warning_all!(
417 country::Warning => Warning::Country,
418 currency::Warning => Warning::Currency,
419 datetime::Warning => Warning::DateTime,
420 duration::Warning => Warning::Duration,
421 json::decode::Warning => Warning::Decode,
422 money::Warning => Warning::Money,
423 number::Warning => Warning::Number,
424 string::Warning => Warning::String,
425 crate::tariff::Warning => Warning::Tariff,
426 weekday::Warning => Warning::Weekday
427);
428
429#[derive(Debug)]
431pub struct TariffReport {
432 pub origin: TariffOrigin,
434
435 pub warnings: BTreeMap<warning::Path, Vec<crate::tariff::Warning>>,
439}
440
441#[derive(Clone, Debug)]
443pub struct TariffOrigin {
444 pub index: usize,
446
447 pub id: String,
449
450 pub currency: currency::Code,
452}
453
454#[derive(Debug)]
456pub(crate) struct Period {
457 pub start_date_time: DateTime<Utc>,
459
460 pub consumed: Consumed,
462}
463
464#[derive(Debug)]
466pub struct Dimensions {
467 pub energy: Dimension<Kwh>,
469
470 pub flat: Dimension<()>,
472
473 pub duration_charging: Dimension<TimeDelta>,
475
476 pub duration_parking: Dimension<TimeDelta>,
478}
479
480impl Dimensions {
481 fn new(components: ComponentSet, consumed: &Consumed) -> Self {
482 let ComponentSet {
483 energy: energy_price,
484 flat: flat_price,
485 duration_charging: duration_charging_price,
486 duration_parking: duration_parking_price,
487 } = components;
488
489 let Consumed {
490 duration_charging,
491 duration_parking,
492 energy,
493 current_max: _,
494 current_min: _,
495 power_max: _,
496 power_min: _,
497 } = consumed;
498
499 Self {
500 energy: Dimension {
501 price: energy_price,
502 volume: *energy,
503 billed_volume: *energy,
504 },
505 flat: Dimension {
506 price: flat_price,
507 volume: Some(()),
508 billed_volume: Some(()),
509 },
510 duration_charging: Dimension {
511 price: duration_charging_price,
512 volume: *duration_charging,
513 billed_volume: *duration_charging,
514 },
515 duration_parking: Dimension {
516 price: duration_parking_price,
517 volume: *duration_parking,
518 billed_volume: *duration_parking,
519 },
520 }
521 }
522}
523
524#[derive(Debug)]
525pub struct Dimension<V> {
527 pub price: Option<Component>,
531
532 pub volume: Option<V>,
536
537 pub billed_volume: Option<V>,
545}
546
547impl<V: Cost> Dimension<V> {
548 pub fn cost(&self) -> Option<Price> {
550 let (Some(volume), Some(price_component)) = (&self.billed_volume, &self.price) else {
551 return None;
552 };
553
554 let excl_vat = volume.cost(price_component.price);
555
556 let incl_vat = match price_component.vat {
557 VatApplicable::Applicable(vat) => Some(excl_vat.apply_vat(vat)),
558 VatApplicable::Inapplicable => Some(excl_vat),
559 VatApplicable::Unknown => None,
560 };
561
562 Some(Price { excl_vat, incl_vat })
563 }
564}
565
566#[derive(Debug)]
571pub struct ComponentSet {
572 pub energy: Option<Component>,
574
575 pub flat: Option<Component>,
577
578 pub duration_charging: Option<Component>,
580
581 pub duration_parking: Option<Component>,
583}
584
585impl ComponentSet {
586 fn has_all_components(&self) -> bool {
588 let Self {
589 energy,
590 flat,
591 duration_charging,
592 duration_parking,
593 } = self;
594
595 flat.is_some()
596 && energy.is_some()
597 && duration_parking.is_some()
598 && duration_charging.is_some()
599 }
600}
601
602#[derive(Clone, Debug)]
607pub struct Component {
608 pub tariff_element_index: usize,
610
611 pub price: Money,
613
614 pub vat: VatApplicable,
617
618 pub step_size: u64,
626}
627
628impl Component {
629 fn new(component: &crate::tariff::v221::PriceComponent, tariff_element_index: usize) -> Self {
630 let crate::tariff::v221::PriceComponent {
631 price,
632 vat,
633 step_size,
634 dimension_type: _,
635 } = component;
636
637 Self {
638 tariff_element_index,
639 price: *price,
640 vat: *vat,
641 step_size: *step_size,
642 }
643 }
644}
645
646#[derive(Debug)]
660pub struct Total<TCdr, TCalc = TCdr> {
661 pub cdr: TCdr,
663
664 pub calculated: TCalc,
666}
667
668#[derive(Debug)]
670pub enum PeriodRange {
671 Many(Range<DateTime<Utc>>),
674
675 Single(DateTime<Utc>),
677}
678
679impl fmt::Display for PeriodRange {
680 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
681 match self {
682 PeriodRange::Many(Range { start, end }) => write!(f, "[start: {start}, end: {end}]"),
683 PeriodRange::Single(date_time) => write!(f, "{date_time}"),
684 }
685 }
686}
687
688#[derive(Debug)]
692pub enum TariffSource<'buf> {
693 UseCdr,
695
696 Override(Vec<crate::tariff::Versioned<'buf>>),
698}
699
700impl<'buf> TariffSource<'buf> {
701 pub fn single(tariff: crate::tariff::Versioned<'buf>) -> Self {
703 Self::Override(vec![tariff])
704 }
705}
706
707#[instrument(skip_all)]
708pub(super) fn cdr(
709 cdr_elem: &crate::cdr::Versioned<'_>,
710 tariff_source: TariffSource<'_>,
711 timezone: Tz,
712) -> Verdict<Report> {
713 let cdr = parse_cdr(cdr_elem)?;
714
715 match tariff_source {
716 TariffSource::UseCdr => {
717 let (v221::cdr::WithTariffs { cdr, tariffs }, warnings) = cdr.into_parts();
718 debug!("Using tariffs from CDR");
719 let tariffs = tariffs
720 .iter()
721 .map(|elem| {
722 let tariff = crate::tariff::v211::Tariff::from_json(elem);
723 tariff.map_caveat(crate::tariff::v221::Tariff::from)
724 })
725 .collect::<Result<Vec<_>, _>>()?;
726
727 let cdr = cdr.into_caveat(warnings);
728
729 Ok(price_v221_cdr_with_tariffs(
730 cdr_elem, cdr, tariffs, timezone,
731 )?)
732 }
733 TariffSource::Override(tariffs) => {
734 let cdr = cdr.map(v221::cdr::WithTariffs::discard_tariffs);
735
736 debug!("Using override tariffs");
737 let tariffs = tariffs
738 .iter()
739 .map(tariff::parse)
740 .collect::<Result<Vec<_>, _>>()?;
741
742 Ok(price_v221_cdr_with_tariffs(
743 cdr_elem, cdr, tariffs, timezone,
744 )?)
745 }
746 }
747}
748
749fn price_v221_cdr_with_tariffs(
756 cdr_elem: &crate::cdr::Versioned<'_>,
757 cdr: Caveat<v221::Cdr, Warning>,
758 tariffs: Vec<Caveat<crate::tariff::v221::Tariff<'_>, crate::tariff::Warning>>,
759 timezone: Tz,
760) -> Verdict<Report> {
761 debug!(?timezone, version = ?cdr_elem.version(), "Pricing CDR");
762 let (cdr, mut warnings) = cdr.into_parts();
763 let v221::Cdr {
764 start_date_time,
765 end_date_time,
766 charging_periods,
767 totals: cdr_totals,
768 } = cdr;
769
770 let (tariff_reports, tariffs): (Vec<_>, Vec<_>) = tariffs
775 .into_iter()
776 .enumerate()
777 .map(|(index, tariff)| {
778 let (tariff, warnings) = tariff.into_parts();
779 (
780 TariffReport {
781 origin: TariffOrigin {
782 index,
783 id: tariff.id.to_string(),
784 currency: tariff.currency,
785 },
786 warnings: warnings.into_path_map(),
787 },
788 tariff,
789 )
790 })
791 .unzip();
792
793 debug!(tariffs = ?tariffs.iter().map(|t| t.id).collect::<Vec<_>>(), "Found tariffs(by id) in CDR");
794
795 let tariffs_normalized = tariff::normalize_all(&tariffs);
796 let Some((tariff_index, tariff)) =
797 tariff::find_first_active(tariffs_normalized, start_date_time)
798 else {
799 return warnings.bail(Warning::NoValidTariff, cdr_elem.as_element());
800 };
801
802 debug!(tariff_index, id = ?tariff.id(), "Found active tariff");
803 debug!(%timezone, "Found timezone");
804 let periods = charging_periods
806 .into_iter()
807 .map(Period::try_from)
808 .collect::<Result<Vec<_>, _>>()
809 .map_err(|err| warning::ErrorSet::with_warn(Warning::Parse(err), cdr_elem.as_element()))?;
810
811 let periods = normalize_periods(periods, end_date_time, timezone);
812 let price_cdr_report = price_periods(&periods, &tariff)
813 .with_element(cdr_elem.as_element())?
814 .gather_warnings_into(&mut warnings);
815
816 let report = generate_report(
817 &cdr_totals,
818 timezone,
819 tariff_reports,
820 price_cdr_report,
821 TariffOrigin {
822 index: tariff_index,
823 id: tariff.id().to_string(),
824 currency: tariff.currency(),
825 },
826 );
827
828 Ok(report.into_caveat(warnings))
829}
830
831pub(crate) fn periods(
833 end_date_time: DateTime<Utc>,
834 timezone: Tz,
835 tariff_elem: &crate::tariff::v221::Tariff<'_>,
836 mut periods: Vec<Period>,
837) -> VerdictDeferred<PeriodsReport> {
838 periods.sort_by_key(|p| p.start_date_time);
841 let tariff = Tariff::from_v221(tariff_elem);
842 let periods = normalize_periods(periods, end_date_time, timezone);
843 price_periods(&periods, &tariff)
844}
845
846fn normalize_periods(
847 periods: Vec<Period>,
848 end_date_time: DateTime<Utc>,
849 local_timezone: Tz,
850) -> Vec<PeriodNormalized> {
851 debug!("Normalizing CDR periods");
852
853 let mut previous_end_snapshot = Option::<TotalsSnapshot>::None;
855
856 let end_dates = {
858 let mut end_dates = periods
859 .iter()
860 .skip(1)
861 .map(|p| p.start_date_time)
862 .collect::<Vec<_>>();
863
864 end_dates.push(end_date_time);
866 end_dates
867 };
868
869 let periods = periods
870 .into_iter()
871 .zip(end_dates)
872 .enumerate()
873 .map(|(index, (period, end_date_time))| {
874 trace!(index, "processing\n{period:#?}");
875 let Period {
876 start_date_time,
877 consumed,
878 } = period;
879
880 let period = if let Some(prev_end_snapshot) = previous_end_snapshot.take() {
881 let start_snapshot = prev_end_snapshot;
882 let end_snapshot = start_snapshot.next(&consumed, end_date_time);
883
884 let period = PeriodNormalized {
885 consumed,
886 start_snapshot,
887 end_snapshot,
888 };
889 trace!("Adding new period based on the last added\n{period:#?}");
890 period
891 } else {
892 let start_snapshot = TotalsSnapshot::zero(start_date_time, local_timezone);
893 let end_snapshot = start_snapshot.next(&consumed, end_date_time);
894
895 let period = PeriodNormalized {
896 consumed,
897 start_snapshot,
898 end_snapshot,
899 };
900 trace!("Adding new period\n{period:#?}");
901 period
902 };
903
904 previous_end_snapshot.replace(period.end_snapshot.clone());
905 period
906 })
907 .collect::<Vec<_>>();
908
909 periods
910}
911
912fn price_periods(periods: &[PeriodNormalized], tariff: &Tariff) -> VerdictDeferred<PeriodsReport> {
914 debug!(count = periods.len(), "Pricing CDR periods");
915
916 if tracing::enabled!(tracing::Level::TRACE) {
917 trace!("# CDR period list:");
918 for period in periods {
919 trace!("{period:#?}");
920 }
921 }
922
923 let period_totals = period_totals(periods, tariff);
924 let (billed, warnings) = period_totals.calculate_billed()?.into_parts();
925 let (billable, periods, totals) = billed;
926 let total_costs = total_costs(&periods, tariff);
927 let report = PeriodsReport {
928 billable,
929 periods,
930 totals,
931 total_costs,
932 };
933
934 Ok(report.into_caveat_deferred(warnings))
935}
936
937pub(crate) struct PeriodsReport {
939 pub billable: Billable,
941
942 pub periods: Vec<PeriodReport>,
944
945 pub totals: Totals,
947
948 pub total_costs: TotalCosts,
950}
951
952#[derive(Debug)]
958pub struct PeriodReport {
959 pub start_date_time: DateTime<Utc>,
961
962 pub end_date_time: DateTime<Utc>,
964
965 pub dimensions: Dimensions,
967}
968
969impl PeriodReport {
970 fn new(period: &PeriodNormalized, dimensions: Dimensions) -> Self {
971 Self {
972 start_date_time: period.start_snapshot.date_time,
973 end_date_time: period.end_snapshot.date_time,
974 dimensions,
975 }
976 }
977
978 pub fn cost(&self) -> Option<Price> {
980 [
981 self.dimensions.duration_charging.cost(),
982 self.dimensions.duration_parking.cost(),
983 self.dimensions.flat.cost(),
984 self.dimensions.energy.cost(),
985 ]
986 .into_iter()
987 .fold(None, |accum, next| {
988 if accum.is_none() && next.is_none() {
989 None
990 } else {
991 Some(
992 accum
993 .unwrap_or_default()
994 .saturating_add(next.unwrap_or_default()),
995 )
996 }
997 })
998 }
999}
1000
1001#[derive(Debug)]
1003struct PeriodTotals {
1004 periods: Vec<PeriodReport>,
1006
1007 step_size: StepSize,
1009
1010 totals: Totals,
1012}
1013
1014#[derive(Debug, Default)]
1016pub(crate) struct Totals {
1017 pub energy: Option<Kwh>,
1019
1020 pub duration_charging: Option<TimeDelta>,
1024
1025 pub duration_parking: Option<TimeDelta>,
1029}
1030
1031impl PeriodTotals {
1032 fn calculate_billed(self) -> VerdictDeferred<(Billable, Vec<PeriodReport>, Totals)> {
1036 let mut warnings = warning::SetDeferred::new();
1037 let Self {
1038 mut periods,
1039 step_size,
1040 totals,
1041 } = self;
1042 let charging_time = totals
1043 .duration_charging
1044 .map(|dt| step_size.apply_time(&mut periods, dt))
1045 .transpose()?
1046 .gather_deferred_warnings_into(&mut warnings);
1047 let energy = totals
1048 .energy
1049 .map(|kwh| step_size.apply_energy(&mut periods, kwh))
1050 .transpose()?
1051 .gather_deferred_warnings_into(&mut warnings);
1052 let parking_time = totals
1053 .duration_parking
1054 .map(|dt| step_size.apply_parking_time(&mut periods, dt))
1055 .transpose()?
1056 .gather_deferred_warnings_into(&mut warnings);
1057 let billed = Billable {
1058 charging_time,
1059 energy,
1060 parking_time,
1061 };
1062 Ok((billed, periods, totals).into_caveat_deferred(warnings))
1063 }
1064}
1065
1066#[derive(Debug)]
1068pub(crate) struct Billable {
1069 charging_time: Option<TimeDelta>,
1071
1072 energy: Option<Kwh>,
1074
1075 parking_time: Option<TimeDelta>,
1077}
1078
1079fn period_totals(periods: &[PeriodNormalized], tariff: &Tariff) -> PeriodTotals {
1082 let mut has_flat_fee = false;
1083 let mut step_size = StepSize::new();
1084 let mut totals = Totals::default();
1085
1086 debug!(
1087 tariff_id = tariff.id(),
1088 period_count = periods.len(),
1089 "Accumulating dimension totals for each period"
1090 );
1091
1092 let periods = periods
1093 .iter()
1094 .enumerate()
1095 .map(|(index, period)| {
1096 let mut component_set = tariff.active_components(period);
1097 trace!(
1098 index,
1099 "Creating charge period with Dimension\n{period:#?}\n{component_set:#?}"
1100 );
1101
1102 if component_set.flat.is_some() {
1103 if has_flat_fee {
1104 component_set.flat = None;
1105 } else {
1106 has_flat_fee = true;
1107 }
1108 }
1109
1110 step_size.update(index, &component_set, period);
1111
1112 trace!(period_index = index, "Step size updated\n{step_size:#?}");
1113
1114 let dimensions = Dimensions::new(component_set, &period.consumed);
1115
1116 trace!(period_index = index, "Dimensions created\n{dimensions:#?}");
1117
1118 if let Some(dt) = dimensions.duration_charging.volume {
1119 let acc = totals.duration_charging.get_or_insert_default();
1120 *acc = acc.saturating_add(dt);
1121 }
1122
1123 if let Some(kwh) = dimensions.energy.volume {
1124 let acc = totals.energy.get_or_insert_default();
1125 *acc = acc.saturating_add(kwh);
1126 }
1127
1128 if let Some(dt) = dimensions.duration_parking.volume {
1129 let acc = totals.duration_parking.get_or_insert_default();
1130 *acc = acc.saturating_add(dt);
1131 }
1132
1133 trace!(period_index = index, ?totals, "Update totals");
1134
1135 PeriodReport::new(period, dimensions)
1136 })
1137 .collect::<Vec<_>>();
1138
1139 PeriodTotals {
1140 periods,
1141 step_size,
1142 totals,
1143 }
1144}
1145
1146#[derive(Debug, Default)]
1148pub(crate) struct TotalCosts {
1149 pub energy: Option<Price>,
1151
1152 pub fixed: Option<Price>,
1154
1155 pub duration_charging: Option<Price>,
1157
1158 pub duration_parking: Option<Price>,
1160}
1161
1162impl TotalCosts {
1163 pub(crate) fn total(&self) -> Option<Price> {
1167 let Self {
1168 energy,
1169 fixed,
1170 duration_charging,
1171 duration_parking,
1172 } = self;
1173 debug!(
1174 energy = %DisplayOption(*energy),
1175 fixed = %DisplayOption(*fixed),
1176 duration_charging = %DisplayOption(*duration_charging),
1177 duration_parking = %DisplayOption(*duration_parking),
1178 "Calculating total costs."
1179 );
1180 [energy, fixed, duration_charging, duration_parking]
1181 .into_iter()
1182 .fold(None, |accum: Option<Price>, next| match (accum, next) {
1183 (None, None) => None,
1184 _ => Some(
1185 accum
1186 .unwrap_or_default()
1187 .saturating_add(next.unwrap_or_default()),
1188 ),
1189 })
1190 }
1191}
1192
1193fn total_costs(periods: &[PeriodReport], tariff: &Tariff) -> TotalCosts {
1195 let mut total_costs = TotalCosts::default();
1196
1197 debug!(
1198 tariff_id = tariff.id(),
1199 period_count = periods.len(),
1200 "Accumulating dimension costs for each period"
1201 );
1202 for (index, period) in periods.iter().enumerate() {
1203 let dimensions = &period.dimensions;
1204
1205 trace!(period_index = index, "Processing period");
1206
1207 let energy_cost = dimensions.energy.cost();
1208 let fixed_cost = dimensions.flat.cost();
1209 let duration_charging_cost = dimensions.duration_charging.cost();
1210 let duration_parking_cost = dimensions.duration_parking.cost();
1211
1212 trace!(?total_costs.energy, ?energy_cost, "Energy cost");
1213 trace!(?total_costs.duration_charging, ?duration_charging_cost, "Charging cost");
1214 trace!(?total_costs.duration_parking, ?duration_parking_cost, "Parking cost");
1215 trace!(?total_costs.fixed, ?fixed_cost, "Fixed cost");
1216
1217 total_costs.energy = match (total_costs.energy, energy_cost) {
1218 (None, None) => None,
1219 (total, period) => Some(
1220 total
1221 .unwrap_or_default()
1222 .saturating_add(period.unwrap_or_default()),
1223 ),
1224 };
1225
1226 total_costs.duration_charging =
1227 match (total_costs.duration_charging, duration_charging_cost) {
1228 (None, None) => None,
1229 (total, period) => Some(
1230 total
1231 .unwrap_or_default()
1232 .saturating_add(period.unwrap_or_default()),
1233 ),
1234 };
1235
1236 total_costs.duration_parking = match (total_costs.duration_parking, duration_parking_cost) {
1237 (None, None) => None,
1238 (total, period) => Some(
1239 total
1240 .unwrap_or_default()
1241 .saturating_add(period.unwrap_or_default()),
1242 ),
1243 };
1244
1245 total_costs.fixed = match (total_costs.fixed, fixed_cost) {
1246 (None, None) => None,
1247 (total, period) => Some(
1248 total
1249 .unwrap_or_default()
1250 .saturating_add(period.unwrap_or_default()),
1251 ),
1252 };
1253
1254 trace!(period_index = index, ?total_costs, "Update totals");
1255 }
1256
1257 total_costs
1258}
1259
1260fn generate_report(
1261 cdr_totals: &v221::cdr::Totals,
1262 timezone: Tz,
1263 tariff_reports: Vec<TariffReport>,
1264 price_periods_report: PeriodsReport,
1265 tariff_used: TariffOrigin,
1266) -> Report {
1267 let PeriodsReport {
1268 billable,
1269 periods,
1270 totals,
1271 total_costs,
1272 } = price_periods_report;
1273 trace!("Update billed totals {billable:#?}");
1274
1275 let total_cost = total_costs.total();
1276
1277 debug!(total_cost = %DisplayOption(total_cost.as_ref()));
1278
1279 let total_time = {
1280 debug!(
1281 period_start = %DisplayOption(periods.first().map(|p| p.start_date_time)),
1282 period_end = %DisplayOption(periods.last().map(|p| p.end_date_time)),
1283 "Calculating `total_time`"
1284 );
1285
1286 periods
1287 .first()
1288 .zip(periods.last())
1289 .map(|(first, last)| {
1290 last.end_date_time
1291 .signed_duration_since(first.start_date_time)
1292 })
1293 .unwrap_or_default()
1294 };
1295 debug!(?total_time, total_time_hms = %Hms(total_time));
1296
1297 let report = Report {
1298 periods,
1299 tariff_used,
1300 timezone: timezone.to_string(),
1301 billed_parking_time: billable.parking_time,
1302 billed_energy: billable.energy.round_to_ocpi_scale(),
1303 billed_charging_time: billable.charging_time,
1304 tariff_reports,
1305 total_charging_time: totals.duration_charging,
1306 total_cost: Total {
1307 cdr: cdr_totals.cost.round_to_ocpi_scale(),
1308 calculated: total_cost.round_to_ocpi_scale(),
1309 },
1310 total_time_cost: Total {
1311 cdr: cdr_totals.time_cost.round_to_ocpi_scale(),
1312 calculated: total_costs.duration_charging.round_to_ocpi_scale(),
1313 },
1314 total_time: Total {
1315 cdr: cdr_totals.time,
1316 calculated: total_time,
1317 },
1318 total_parking_cost: Total {
1319 cdr: cdr_totals.parking_cost.round_to_ocpi_scale(),
1320 calculated: total_costs.duration_parking.round_to_ocpi_scale(),
1321 },
1322 total_parking_time: Total {
1323 cdr: cdr_totals.parking_time,
1324 calculated: totals.duration_parking,
1325 },
1326 total_energy_cost: Total {
1327 cdr: cdr_totals.energy_cost.round_to_ocpi_scale(),
1328 calculated: total_costs.energy.round_to_ocpi_scale(),
1329 },
1330 total_energy: Total {
1331 cdr: cdr_totals.energy.round_to_ocpi_scale(),
1332 calculated: totals.energy.round_to_ocpi_scale(),
1333 },
1334 total_fixed_cost: Total {
1335 cdr: cdr_totals.fixed_cost.round_to_ocpi_scale(),
1336 calculated: total_costs.fixed.round_to_ocpi_scale(),
1337 },
1338 total_reservation_cost: Total {
1339 cdr: cdr_totals.reservation_cost.round_to_ocpi_scale(),
1340 calculated: None,
1341 },
1342 };
1343
1344 trace!("{report:#?}");
1345
1346 report
1347}
1348
1349#[derive(Debug)]
1350struct StepSize {
1351 charging_time: Option<(usize, Component)>,
1352 parking_time: Option<(usize, Component)>,
1353 energy: Option<(usize, Component)>,
1354}
1355
1356fn delta_as_seconds_dec(delta: TimeDelta) -> Decimal {
1358 Decimal::from(delta.num_milliseconds())
1359 .checked_div(Decimal::from(duration::MILLIS_IN_SEC))
1360 .expect("Can't overflow; See test `as_seconds_dec_should_not_overflow`")
1361}
1362
1363fn delta_from_seconds_dec(seconds: Decimal) -> VerdictDeferred<TimeDelta> {
1365 let millis = seconds.saturating_mul(Decimal::from(duration::MILLIS_IN_SEC));
1366 let Ok(millis) = i64::try_from(millis) else {
1367 return Err(warning::ErrorSetDeferred::with_warn(
1368 duration::Warning::Overflow.into(),
1369 ));
1370 };
1371 let Some(delta) = TimeDelta::try_milliseconds(millis) else {
1372 return Err(warning::ErrorSetDeferred::with_warn(
1373 duration::Warning::Overflow.into(),
1374 ));
1375 };
1376 Ok(delta.into_caveat_deferred(warning::SetDeferred::new()))
1377}
1378
1379impl StepSize {
1380 fn new() -> Self {
1381 Self {
1382 charging_time: None,
1383 parking_time: None,
1384 energy: None,
1385 }
1386 }
1387
1388 fn update(&mut self, index: usize, components: &ComponentSet, period: &PeriodNormalized) {
1389 if period.consumed.energy.is_some() {
1390 if let Some(energy) = components.energy.clone() {
1391 self.energy = Some((index, energy));
1392 }
1393 }
1394
1395 if period.consumed.duration_charging.is_some() {
1396 if let Some(time) = components.duration_charging.clone() {
1397 self.charging_time = Some((index, time));
1398 }
1399 }
1400
1401 if period.consumed.duration_parking.is_some() {
1402 if let Some(parking) = components.duration_parking.clone() {
1403 self.parking_time = Some((index, parking));
1404 }
1405 }
1406 }
1407
1408 fn duration_step_size(
1409 total_volume: TimeDelta,
1410 period_billed_volume: &mut TimeDelta,
1411 step_size: u64,
1412 ) -> VerdictDeferred<TimeDelta> {
1413 if step_size == 0 {
1414 return Ok(total_volume.into_caveat_deferred(warning::SetDeferred::new()));
1415 }
1416
1417 let total_seconds = delta_as_seconds_dec(total_volume);
1418 let step_size = Decimal::from(step_size);
1419
1420 let Some(x) = total_seconds.checked_div(step_size) else {
1421 return Err(warning::ErrorSetDeferred::with_warn(
1422 duration::Warning::Overflow.into(),
1423 ));
1424 };
1425 let total_billed_volume = delta_from_seconds_dec(x.ceil().saturating_mul(step_size))?;
1426
1427 let period_delta_volume = total_billed_volume.saturating_sub(total_volume);
1428 *period_billed_volume = period_billed_volume.saturating_add(period_delta_volume);
1429
1430 Ok(total_billed_volume)
1431 }
1432
1433 fn apply_time(
1434 &self,
1435 periods: &mut [PeriodReport],
1436 total: TimeDelta,
1437 ) -> VerdictDeferred<TimeDelta> {
1438 let (Some((time_index, price)), None) = (&self.charging_time, &self.parking_time) else {
1439 return Ok(total.into_caveat_deferred(warning::SetDeferred::new()));
1440 };
1441
1442 let Some(period) = periods.get_mut(*time_index) else {
1443 error!(time_index, "Invalid period index");
1444 return Err(warning::ErrorSetDeferred::with_warn(Warning::InternalError));
1445 };
1446 let Some(volume) = period.dimensions.duration_charging.billed_volume.as_mut() else {
1447 return Err(warning::ErrorSetDeferred::with_warn(
1448 Warning::DimensionShouldHaveVolume {
1449 dimension_name: "time",
1450 },
1451 ));
1452 };
1453
1454 Self::duration_step_size(total, volume, price.step_size)
1455 }
1456
1457 fn apply_parking_time(
1458 &self,
1459 periods: &mut [PeriodReport],
1460 total: TimeDelta,
1461 ) -> VerdictDeferred<TimeDelta> {
1462 let warnings = warning::SetDeferred::new();
1463 let Some((parking_index, price)) = &self.parking_time else {
1464 return Ok(total.into_caveat_deferred(warnings));
1465 };
1466
1467 let Some(period) = periods.get_mut(*parking_index) else {
1468 error!(parking_index, "Invalid period index");
1469 return warnings.bail(Warning::InternalError);
1470 };
1471 let Some(volume) = period.dimensions.duration_parking.billed_volume.as_mut() else {
1472 return warnings.bail(Warning::DimensionShouldHaveVolume {
1473 dimension_name: "parking_time",
1474 });
1475 };
1476
1477 Self::duration_step_size(total, volume, price.step_size)
1478 }
1479
1480 fn apply_energy(
1481 &self,
1482 periods: &mut [PeriodReport],
1483 total_volume: Kwh,
1484 ) -> VerdictDeferred<Kwh> {
1485 let warnings = warning::SetDeferred::new();
1486 let Some((energy_index, price)) = &self.energy else {
1487 return Ok(total_volume.into_caveat_deferred(warnings));
1488 };
1489
1490 if price.step_size == 0 {
1491 return Ok(total_volume.into_caveat_deferred(warnings));
1492 }
1493
1494 let Some(period) = periods.get_mut(*energy_index) else {
1495 error!(energy_index, "Invalid period index");
1496 return warnings.bail(Warning::InternalError);
1497 };
1498 let step_size = Decimal::from(price.step_size);
1499
1500 let Some(period_billed_volume) = period.dimensions.energy.billed_volume.as_mut() else {
1501 return warnings.bail(Warning::DimensionShouldHaveVolume {
1502 dimension_name: "energy",
1503 });
1504 };
1505
1506 let Some(watt_hours) = total_volume.watt_hours().checked_div(step_size) else {
1507 return warnings.bail(duration::Warning::Overflow.into());
1508 };
1509
1510 let total_billed_volume = Kwh::from_watt_hours(watt_hours.ceil().saturating_mul(step_size));
1511 let period_delta_volume = total_billed_volume.saturating_sub(total_volume);
1512 *period_billed_volume = period_billed_volume.saturating_add(period_delta_volume);
1513
1514 Ok(total_billed_volume.into_caveat_deferred(warnings))
1515 }
1516}
1517
1518fn parse_cdr<'caller: 'buf, 'buf>(
1519 cdr: &'caller crate::cdr::Versioned<'buf>,
1520) -> Verdict<v221::cdr::WithTariffs<'buf>> {
1521 match cdr.version() {
1522 Version::V211 => {
1523 let cdr = v211::cdr::WithTariffs::from_json(cdr.as_element())?;
1524 Ok(cdr.map(v221::cdr::WithTariffs::from))
1525 }
1526 Version::V221 => v221::cdr::WithTariffs::from_json(cdr.as_element()),
1527 }
1528}