1#[cfg(test)]
2mod test;
3
4#[cfg(test)]
5mod test_clamp_date_time_span;
6
7#[cfg(test)]
8mod test_gen_time_events;
9
10#[cfg(test)]
11mod test_generate;
12
13#[cfg(test)]
14mod test_generate_from_single_elem_tariff;
15
16#[cfg(test)]
17mod test_local_to_utc;
18
19#[cfg(test)]
20mod test_periods;
21
22#[cfg(test)]
23mod test_power_to_time;
24
25#[cfg(test)]
26mod test_popular_tariffs;
27
28mod v2x;
29
30use std::{
31 cmp::{max, min},
32 fmt,
33 ops::Range,
34};
35
36use chrono::{DateTime, Datelike as _, NaiveDateTime, NaiveTime, TimeDelta, Utc};
37use rust_decimal::{prelude::ToPrimitive, Decimal};
38use rust_decimal_macros::dec;
39use tracing::{debug, instrument, warn};
40
41use crate::{
42 country, currency,
43 duration::{AsHms, ToHoursDecimal},
44 energy::{Ampere, Kw, Kwh},
45 from_warning_all,
46 json::FromJson as _,
47 number::{FromDecimal as _, RoundDecimal},
48 price, tariff,
49 warning::{self, GatherWarnings as _, IntoCaveat, WithElement as _},
50 Price, SaturatingAdd, Version, Versioned,
51};
52
53const MIN_CS_DURATION_SECS: i64 = 120;
55
56type DateTimeSpan = Range<DateTime<Utc>>;
57type Verdict<T> = crate::Verdict<T, Warning>;
58pub type Caveat<T> = warning::Caveat<T, Warning>;
59
60macro_rules! some_dec_or_bail {
62 ($elem:expr, $opt:expr, $warnings:expr, $msg:literal) => {
63 match $opt {
64 Some(v) => v,
65 None => {
66 return $warnings.bail(Warning::Decimal($msg), $elem.as_element());
67 }
68 }
69 };
70}
71
72#[derive(Debug)]
74pub struct Report {
75 pub tariff_id: String,
77
78 pub tariff_currency_code: currency::Code,
80
81 pub partial_cdr: PartialCdr,
88}
89
90#[derive(Debug)]
98pub struct PartialCdr {
99 pub currency_code: currency::Code,
101
102 pub party_id: Option<CpoId>,
110
111 pub start_date_time: DateTime<Utc>,
113
114 pub end_date_time: DateTime<Utc>,
116
117 pub total_energy: Option<Kwh>,
119
120 pub total_charging_duration: Option<TimeDelta>,
124
125 pub total_parking_duration: Option<TimeDelta>,
129
130 pub total_cost: Option<Price>,
132
133 pub total_energy_cost: Option<Price>,
135
136 pub total_fixed_cost: Option<Price>,
138
139 pub total_parking_duration_cost: Option<Price>,
141
142 pub total_charging_duration_cost: Option<Price>,
144
145 pub charging_periods: Vec<ChargingPeriod>,
148}
149
150#[derive(Clone, Debug)]
155pub struct CpoId {
156 pub country_code: country::Code,
158
159 pub id: String,
161}
162
163impl<'buf> From<tariff::CpoId<'buf>> for CpoId {
164 fn from(value: tariff::CpoId<'buf>) -> Self {
165 let tariff::CpoId { country_code, id } = value;
166 CpoId {
167 country_code,
168 id: id.to_string(),
169 }
170 }
171}
172
173impl fmt::Display for CpoId {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 write!(f, "{}{}", self.country_code.into_alpha_2_str(), self.id)
177 }
178}
179
180#[derive(Debug)]
184pub struct ChargingPeriod {
185 pub start_date_time: DateTime<Utc>,
188
189 pub dimensions: Vec<Dimension>,
191
192 pub tariff_id: Option<String>,
196}
197
198#[derive(Debug)]
202pub struct Dimension {
203 pub dimension_type: DimensionType,
204
205 pub volume: Decimal,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
213pub enum DimensionType {
214 Energy,
216
217 MaxCurrent,
219
220 MinCurrent,
222
223 MaxPower,
225
226 MinPower,
228
229 ParkingTime,
231
232 ReservationTime,
234
235 Time,
237}
238
239#[derive(Clone)]
241pub struct Config {
242 pub timezone: chrono_tz::Tz,
244
245 pub end_date_time: DateTime<Utc>,
247
248 pub max_current_supply_amp: Decimal,
250
251 pub requested_kwh: Decimal,
256
257 pub max_power_supply_kw: Decimal,
266
267 pub start_date_time: DateTime<Utc>,
269}
270
271pub fn cdr_from_tariff(tariff_elem: &tariff::Versioned<'_>, config: &Config) -> Verdict<Report> {
273 let mut warnings = warning::Set::new();
274 let (metrics, timezone) = metrics(tariff_elem, config)?.gather_warnings_into(&mut warnings);
282
283 let tariff = match tariff_elem.version() {
284 Version::V211 => {
285 let tariff = tariff::v211::Tariff::from_json(tariff_elem.as_element())?
286 .gather_warnings_into(&mut warnings);
287
288 tariff::v221::Tariff::from(tariff)
289 }
290 Version::V221 => tariff::v221::Tariff::from_json(tariff_elem.as_element())?
291 .gather_warnings_into(&mut warnings),
292 };
293
294 if !is_tariff_active(&metrics.start_date_time, &tariff) {
295 warnings.insert(tariff::Warning::NotActive.into(), tariff_elem.as_element());
296 }
297
298 let timeline = timeline(timezone, &metrics, &tariff);
299 let charging_periods = charge_periods(&metrics, timeline);
300
301 let report = price::periods(metrics.end_date_time, timezone, &tariff, charging_periods)
302 .with_element(tariff_elem.as_element())?
303 .gather_warnings_into(&mut warnings);
304
305 let price::PeriodsReport {
306 billable: _,
307 periods,
308 totals,
309 total_costs,
310 } = report;
311
312 let charging_periods = periods
313 .into_iter()
314 .map(|period| {
315 let price::PeriodReport {
316 start_date_time,
317 end_date_time: _,
318 dimensions,
319 } = period;
320 let time = dimensions
321 .duration_charging
322 .volume
323 .as_ref()
324 .map(|dt| Dimension {
325 dimension_type: DimensionType::Time,
326 volume: ToHoursDecimal::to_hours_dec(dt),
327 });
328 let parking_time = dimensions
329 .duration_parking
330 .volume
331 .as_ref()
332 .map(|dt| Dimension {
333 dimension_type: DimensionType::ParkingTime,
334 volume: ToHoursDecimal::to_hours_dec(dt),
335 });
336 let energy = dimensions.energy.volume.as_ref().map(|kwh| Dimension {
337 dimension_type: DimensionType::Energy,
338 volume: (*kwh).into(),
339 });
340 let dimensions = vec![energy, parking_time, time]
341 .into_iter()
342 .flatten()
343 .collect();
344
345 ChargingPeriod {
346 start_date_time,
347 dimensions,
348 tariff_id: Some(tariff.id.to_string()),
349 }
350 })
351 .collect();
352
353 let mut total_cost = total_costs.total();
354
355 if let Some(total_cost) = total_cost.as_mut() {
356 if let Some(min_price) = tariff.min_price {
357 if *total_cost < min_price {
358 *total_cost = min_price;
359 warnings.insert(
360 tariff::Warning::TotalCostClampedToMin.into(),
361 tariff_elem.as_element(),
362 );
363 }
364 }
365
366 if let Some(max_price) = tariff.max_price {
367 if *total_cost > max_price {
368 *total_cost = max_price;
369 warnings.insert(
370 tariff::Warning::TotalCostClampedToMin.into(),
371 tariff_elem.as_element(),
372 );
373 }
374 }
375 }
376
377 let report = Report {
378 tariff_id: tariff.id.to_string(),
379 tariff_currency_code: tariff.currency,
380 partial_cdr: PartialCdr {
381 party_id: tariff.party_id.map(CpoId::from),
382 start_date_time: metrics.start_date_time,
383 end_date_time: metrics.end_date_time,
384 currency_code: tariff.currency,
385 total_energy: totals.energy.round_to_ocpi_scale(),
386 total_charging_duration: totals.duration_charging,
387 total_parking_duration: totals.duration_parking,
388 total_cost: total_cost.round_to_ocpi_scale(),
389 total_energy_cost: total_costs.energy.round_to_ocpi_scale(),
390 total_fixed_cost: total_costs.fixed.round_to_ocpi_scale(),
391 total_parking_duration_cost: total_costs.duration_parking.round_to_ocpi_scale(),
392 total_charging_duration_cost: total_costs.duration_charging.round_to_ocpi_scale(),
393 charging_periods,
394 },
395 };
396
397 Ok(report.into_caveat(warnings))
398}
399
400struct EventCollector {
402 session_duration: TimeDelta,
404
405 events: Vec<Event>,
407}
408
409impl EventCollector {
410 fn with_session_duration(session_duration: TimeDelta) -> Self {
412 Self {
413 session_duration,
414 events: vec![],
415 }
416 }
417
418 fn push(&mut self, duration_from_start: TimeDelta, event_kind: EventKind) {
420 if duration_from_start <= self.session_duration {
421 self.events.push(Event {
422 duration_from_start,
423 kind: event_kind,
424 });
425 }
426 }
427
428 fn push_with(&mut self, event_kind: EventKind) -> impl FnOnce(TimeDelta) + use<'_> {
430 move |dt| {
431 self.push(dt, event_kind);
432 }
433 }
434
435 fn into_inner(self) -> Vec<Event> {
437 self.events
438 }
439}
440
441fn timeline(
443 timezone: chrono_tz::Tz,
444 metrics: &Metrics,
445 tariff: &tariff::v221::Tariff<'_>,
446) -> Timeline {
447 let Metrics {
448 start_date_time: cdr_start,
449 end_date_time: cdr_end,
450 duration_charging,
451 duration_parking,
452 max_power_supply,
453 max_current_supply,
454
455 energy_supplied: _,
456 } = metrics;
457
458 let mut events = {
459 let session_duration = duration_parking.map(|d| duration_charging.saturating_add(d));
460 let mut events =
461 EventCollector::with_session_duration(session_duration.unwrap_or(*duration_charging));
462
463 events.push(TimeDelta::seconds(0), EventKind::SessionStart);
464 events.push(*duration_charging, EventKind::ChargingEnd);
465 session_duration.map(events.push_with(EventKind::ParkingEnd {
466 start: *duration_charging,
467 }));
468
469 events
470 };
471
472 let mut emit_current = false;
475
476 let mut emit_power = false;
479
480 for elem in &tariff.elements {
481 if let Some((time_restrictions, energy_restrictions)) = elem
482 .restrictions
483 .as_ref()
484 .map(tariff::v221::Restrictions::restrictions_by_category)
485 {
486 generate_time_events(
487 &mut events,
488 timezone,
489 *cdr_start..*cdr_end,
490 time_restrictions,
491 );
492
493 let v2x::EnergyRestrictions {
494 min_kwh,
495 max_kwh,
496 min_current,
497 max_current,
498 min_power,
499 max_power,
500 } = energy_restrictions;
501
502 if !emit_current {
503 emit_current = (min_current..=max_current).contains(&Some(*max_current_supply));
508 }
509
510 if !emit_power {
511 emit_power = (min_power..=max_power).contains(&Some(*max_power_supply));
516 }
517
518 generate_energy_events(
519 &mut events,
520 metrics.duration_charging,
521 metrics.energy_supplied,
522 min_kwh,
523 max_kwh,
524 );
525 }
526 }
527
528 let events = events.into_inner();
529
530 Timeline {
531 events,
532 emit_current,
533 emit_power,
534 }
535}
536
537fn generate_time_events(
539 events: &mut EventCollector,
540 timezone: chrono_tz::Tz,
541 cdr_span: DateTimeSpan,
542 restrictions: v2x::TimeRestrictions,
543) {
544 const MIDNIGHT: NaiveTime = NaiveTime::from_hms_opt(0, 0, 0)
545 .expect("The hour, minute and second values are correct and hardcoded");
546 const ONE_DAY: TimeDelta = TimeDelta::days(1);
547
548 let v2x::TimeRestrictions {
549 start_time,
550 end_time,
551 start_date,
552 end_date,
553 min_duration,
554 max_duration,
555 weekdays,
556 } = restrictions;
557
558 let cdr_duration = cdr_span.end.signed_duration_since(cdr_span.start);
559
560 min_duration
562 .filter(|dt| &cdr_duration < dt)
563 .map(events.push_with(EventKind::MinDuration));
564
565 max_duration
567 .filter(|dt| &cdr_duration < dt)
568 .map(events.push_with(EventKind::MaxDuration));
569
570 let (start_date_time, end_date_time) =
580 if let (Some(start_time), Some(end_time)) = (start_time, end_time) {
581 if end_time < start_time {
582 (
583 start_date.map(|d| d.and_time(start_time)),
584 end_date.map(|d| {
585 let (end_time, _) = end_time.overflowing_add_signed(ONE_DAY);
586 d.and_time(end_time)
587 }),
588 )
589 } else {
590 (
591 start_date.map(|d| d.and_time(start_time)),
592 end_date.map(|d| d.and_time(end_time)),
593 )
594 }
595 } else {
596 (
597 start_date.map(|d| d.and_time(start_time.unwrap_or(MIDNIGHT))),
598 end_date.map(|d| d.and_time(end_time.unwrap_or(MIDNIGHT))),
599 )
600 };
601
602 let event_span = clamp_date_time_span(
605 start_date_time.and_then(|d| local_to_utc(timezone, d)),
606 end_date_time.and_then(|d| local_to_utc(timezone, d)),
607 cdr_span,
608 );
609
610 if let Some(start_time) = start_time {
611 gen_naive_time_events(
612 events,
613 &event_span,
614 start_time,
615 &weekdays,
616 EventKind::StartTime,
617 );
618 }
619
620 if let Some(end_time) = end_time {
621 gen_naive_time_events(events, &event_span, end_time, &weekdays, EventKind::EndTime);
622 }
623}
624
625fn local_to_utc(timezone: chrono_tz::Tz, date_time: NaiveDateTime) -> Option<DateTime<Utc>> {
631 use chrono::offset::LocalResult;
632
633 let result = date_time.and_local_timezone(timezone);
634
635 let local_date_time = match result {
636 LocalResult::Single(d) => d,
637 LocalResult::Ambiguous(earliest, _latest) => earliest,
638 LocalResult::None => return None,
639 };
640
641 Some(local_date_time.to_utc())
642}
643
644fn gen_naive_time_events(
646 events: &mut EventCollector,
647 event_span: &Range<DateTime<Utc>>,
648 time: NaiveTime,
649 weekdays: &v2x::WeekdaySet,
650 kind: EventKind,
651) {
652 let time_delta = time.signed_duration_since(event_span.start.time());
653 let cdr_duration = event_span.end.signed_duration_since(event_span.start);
654
655 let time_delta = if time_delta.num_seconds().is_negative() {
658 let (time_delta, _) = time.overflowing_add_signed(TimeDelta::days(1));
659 time_delta.signed_duration_since(event_span.start.time())
660 } else {
661 time_delta
662 };
663
664 if time_delta.num_seconds().is_negative() {
666 return;
667 }
668
669 let Some(remainder) = cdr_duration.checked_sub(&time_delta) else {
671 warn!("TimeDelta overflow");
672 return;
673 };
674
675 if remainder.num_seconds().is_positive() {
676 let duration_from_start = time_delta;
677 let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
678 warn!("Date out of range");
679 return;
680 };
681
682 if weekdays.contains(date.weekday()) {
683 events.push(time_delta, kind);
685 }
686
687 for day in 1..=remainder.num_days() {
688 let Some(duration_from_start) = time_delta.checked_add(&TimeDelta::days(day)) else {
689 warn!("Date out of range");
690 break;
691 };
692 let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
693 warn!("Date out of range");
694 break;
695 };
696
697 if weekdays.contains(date.weekday()) {
698 events.push(duration_from_start, kind);
699 }
700 }
701 }
702}
703
704fn generate_energy_events(
706 events: &mut EventCollector,
707 duration_charging: TimeDelta,
708 energy_supplied: Kwh,
709 min_kwh: Option<Kwh>,
710 max_kwh: Option<Kwh>,
711) {
712 min_kwh
713 .and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
714 .map(events.push_with(EventKind::MinKwh));
715
716 max_kwh
717 .and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
718 .map(events.push_with(EventKind::MaxKwh));
719}
720
721fn power_to_time(power: Kwh, power_total: Kwh, duration_total: TimeDelta) -> Option<TimeDelta> {
723 use rust_decimal::prelude::ToPrimitive;
724
725 let power = Decimal::from(power);
728 let power_total = Decimal::from(power_total);
730 let Some(factor) = power.checked_div(power_total) else {
733 return Some(TimeDelta::zero());
734 };
735
736 if factor.is_sign_negative() || factor > dec!(1.0) {
737 return None;
738 }
739
740 let duration_from_start = factor.checked_mul(Decimal::from(duration_total.num_seconds()))?;
741 duration_from_start.to_i64().map(TimeDelta::seconds)
742}
743
744fn charge_periods(metrics: &Metrics, timeline: Timeline) -> Vec<price::Period> {
746 enum ChargingPhase {
748 Charging,
749 Parking,
750 }
751
752 let Metrics {
753 start_date_time: cdr_start,
754 max_power_supply,
755 max_current_supply,
756
757 end_date_time: _,
758 duration_charging: _,
759 duration_parking: _,
760 energy_supplied: _,
761 } = metrics;
762
763 let Timeline {
764 mut events,
765 emit_current,
766 emit_power,
767 } = timeline;
768
769 events.sort_unstable_by_key(|e| e.duration_from_start);
770
771 let mut periods = vec![];
772 let emit_current = emit_current.then_some(*max_current_supply);
773 let emit_power = emit_power.then_some(*max_power_supply);
774 let mut charging_phase = ChargingPhase::Charging;
776
777 for items in events.windows(2) {
778 let [event, event_next] = items else {
779 unreachable!("The window size is 2");
780 };
781
782 let Event {
783 duration_from_start,
784 kind,
785 } = event;
786
787 if let EventKind::ChargingEnd = kind {
788 charging_phase = ChargingPhase::Parking;
789 }
790
791 let Some(duration) = event_next
792 .duration_from_start
793 .checked_sub(duration_from_start)
794 else {
795 warn!("TimeDelta overflow");
796 break;
797 };
798
799 let Some(start_date_time) = cdr_start.checked_add_signed(*duration_from_start) else {
800 warn!("TimeDelta overflow");
801 break;
802 };
803
804 let consumed = if let ChargingPhase::Charging = charging_phase {
805 let Some(energy) =
806 Decimal::from(*max_power_supply).checked_mul(duration.to_hours_dec())
807 else {
808 warn!("Decimal overflow");
809 break;
810 };
811 price::Consumed {
812 duration_charging: Some(duration),
813 duration_parking: None,
814 energy: Some(Kwh::from_decimal(energy)),
815 current_max: emit_current,
816 current_min: emit_current,
817 power_max: emit_power,
818 power_min: emit_power,
819 }
820 } else {
821 price::Consumed {
822 duration_charging: None,
823 duration_parking: Some(duration),
824 energy: None,
825 current_max: None,
826 current_min: None,
827 power_max: None,
828 power_min: None,
829 }
830 };
831
832 let period = price::Period {
833 start_date_time,
834 consumed,
835 };
836
837 periods.push(period);
838 }
839
840 periods
841}
842
843fn clamp_date_time_span(
849 min_date: Option<DateTime<Utc>>,
850 max_date: Option<DateTime<Utc>>,
851 span: DateTimeSpan,
852) -> DateTimeSpan {
853 let (min_date, max_date) = (min(min_date, max_date), max(min_date, max_date));
855
856 let start = min_date.filter(|d| &span.start < d).unwrap_or(span.start);
857 let end = max_date.filter(|d| &span.end > d).unwrap_or(span.end);
858
859 DateTimeSpan { start, end }
860}
861
862struct Timeline {
864 events: Vec<Event>,
866
867 emit_current: bool,
869
870 emit_power: bool,
872}
873
874struct Event {
876 duration_from_start: TimeDelta,
878
879 kind: EventKind,
881}
882
883impl fmt::Debug for Event {
884 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
885 f.debug_struct("Event")
886 .field("duration_from_start", &self.duration_from_start.as_hms())
887 .field("kind", &self.kind)
888 .finish()
889 }
890}
891
892#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
894enum EventKind {
895 SessionStart,
901
902 ChargingEnd,
907
908 ParkingEnd {
913 start: TimeDelta,
915 },
916
917 StartTime,
918
919 EndTime,
920
921 MinDuration,
926
927 MaxDuration,
932
933 MinKwh,
935
936 MaxKwh,
938}
939
940impl fmt::Debug for EventKind {
941 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
942 match self {
943 Self::SessionStart => write!(f, "SessionStart"),
944 Self::ChargingEnd => write!(f, "ChargingEnd"),
945 Self::ParkingEnd { start } => f
946 .debug_struct("ParkingEnd")
947 .field("start", &start.as_hms())
948 .finish(),
949 Self::StartTime => write!(f, "StartTime"),
950 Self::EndTime => write!(f, "EndTime"),
951 Self::MinDuration => write!(f, "MinDuration"),
952 Self::MaxDuration => write!(f, "MaxDuration"),
953 Self::MinKwh => write!(f, "MinKwh"),
954 Self::MaxKwh => write!(f, "MaxKwh"),
955 }
956 }
957}
958
959#[derive(Debug)]
961struct Metrics {
962 end_date_time: DateTime<Utc>,
964
965 start_date_time: DateTime<Utc>,
967
968 duration_charging: TimeDelta,
973
974 duration_parking: Option<TimeDelta>,
978
979 energy_supplied: Kwh,
981
982 max_current_supply: Ampere,
984
985 max_power_supply: Kw,
987}
988
989#[instrument(skip_all)]
991fn metrics(elem: &tariff::Versioned<'_>, config: &Config) -> Verdict<(Metrics, chrono_tz::Tz)> {
992 const SECS_IN_HOUR: Decimal = dec!(3600);
993
994 let warnings = warning::Set::new();
995
996 let Config {
997 start_date_time,
998 end_date_time,
999 max_power_supply_kw,
1000 requested_kwh: max_energy_battery_kwh,
1001 max_current_supply_amp,
1002 timezone,
1003 } = config;
1004 let duration_session = end_date_time.signed_duration_since(start_date_time);
1005
1006 debug!("duration_session: {}", duration_session.as_hms());
1007
1008 if duration_session.num_seconds().is_negative() {
1010 return warnings.bail(Warning::StartDateTimeIsAfterEndDateTime, elem.as_element());
1011 }
1012
1013 if duration_session.num_seconds() < MIN_CS_DURATION_SECS {
1014 return warnings.bail(Warning::DurationBelowMinimum, elem.as_element());
1015 }
1016
1017 let duration_full_charge_hours = some_dec_or_bail!(
1019 elem,
1020 max_energy_battery_kwh.checked_div(*max_power_supply_kw),
1021 warnings,
1022 "Unable to calculate changing time"
1023 );
1024 debug!(
1025 "duration_full_charge: {}",
1026 duration_full_charge_hours.as_hms()
1027 );
1028
1029 let charging_duration_hours =
1031 Decimal::min(duration_full_charge_hours, duration_session.to_hours_dec());
1032
1033 let power_supplied_kwh = some_dec_or_bail!(
1034 elem,
1035 max_energy_battery_kwh.checked_div(charging_duration_hours),
1036 warnings,
1037 "Unable to calculate the power supplied during the charging time"
1038 );
1039
1040 let charging_duration_secs = some_dec_or_bail!(
1042 elem,
1043 charging_duration_hours.checked_mul(SECS_IN_HOUR),
1044 warnings,
1045 "Unable to convert charging time from hours to seconds"
1046 );
1047
1048 let charging_duration_secs = some_dec_or_bail!(
1049 elem,
1050 charging_duration_secs.round().to_i64(),
1051 warnings,
1052 "Unable to convert charging duration Decimal to i64"
1053 );
1054 let duration_charging = TimeDelta::seconds(charging_duration_secs);
1055
1056 let duration_parking = some_dec_or_bail!(
1057 elem,
1058 duration_session.checked_sub(&duration_charging),
1059 warnings,
1060 "Unable to calculate `idle_duration`"
1061 );
1062
1063 debug!(
1064 "duration_charging: {}, duration_parking: {}",
1065 duration_charging.as_hms(),
1066 duration_parking.as_hms()
1067 );
1068
1069 let metrics = Metrics {
1070 end_date_time: *end_date_time,
1071 start_date_time: *start_date_time,
1072 duration_charging,
1073 duration_parking: Some(duration_parking).filter(|dt| dt.num_seconds().is_positive()),
1074 energy_supplied: Kwh::from_decimal(power_supplied_kwh),
1075 max_current_supply: Ampere::from_decimal(*max_current_supply_amp),
1076 max_power_supply: Kw::from_decimal(*max_power_supply_kw),
1077 };
1078
1079 Ok((metrics, *timezone).into_caveat(warnings))
1080}
1081
1082fn is_tariff_active(cdr_start: &DateTime<Utc>, tariff: &tariff::v221::Tariff<'_>) -> bool {
1083 match (tariff.start_date_time, tariff.end_date_time) {
1084 (None, None) => true,
1085 (None, Some(end)) => (..end).contains(cdr_start),
1086 (Some(start), None) => (start..).contains(cdr_start),
1087 (Some(start), Some(end)) => (start..end).contains(cdr_start),
1088 }
1089}
1090
1091#[derive(Debug)]
1092pub enum Warning {
1093 Decimal(&'static str),
1095
1096 DurationBelowMinimum,
1098
1099 Price(price::Warning),
1100
1101 StartDateTimeIsAfterEndDateTime,
1103
1104 Tariff(tariff::Warning),
1105}
1106
1107impl crate::Warning for Warning {
1108 fn id(&self) -> warning::Id {
1109 match self {
1110 Self::Decimal(_) => warning::Id::from_static("decimal_error"),
1111 Self::DurationBelowMinimum => warning::Id::from_static("duration_below_minimum"),
1112 Self::Price(kind) => kind.id(),
1113 Self::StartDateTimeIsAfterEndDateTime => {
1114 warning::Id::from_static("start_time_after_end_time")
1115 }
1116 Self::Tariff(kind) => kind.id(),
1117 }
1118 }
1119}
1120
1121impl fmt::Display for Warning {
1122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1123 match self {
1124 Self::Decimal(msg) => f.write_str(msg),
1125 Self::DurationBelowMinimum => write!(
1126 f,
1127 "The duration of the chargesession is below the minimum: {MIN_CS_DURATION_SECS}"
1128 ),
1129 Self::Price(warnings) => {
1130 write!(f, "Price warnings: {warnings:?}")
1131 }
1132 Self::StartDateTimeIsAfterEndDateTime => {
1133 write!(f, "The `start_date_time` is after the `end_date_time`")
1134 }
1135 Self::Tariff(warnings) => {
1136 write!(f, "Tariff warnings: {warnings:?}")
1137 }
1138 }
1139 }
1140}
1141
1142from_warning_all!(
1143 tariff::Warning => Warning::Tariff,
1144 price::Warning => Warning::Price
1145);