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::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, ToDuration, 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
72macro_rules! some_time_delta_or_bail {
74 ($elem:expr, $opt:expr, $warnings:expr, $msg:literal) => {
75 match $opt {
76 Some(v) => v,
77 None => {
78 return $warnings.bail(Warning::TimeDelta($msg), $elem.as_element());
79 }
80 }
81 };
82}
83
84#[derive(Debug)]
86pub struct Report {
87 pub tariff_id: String,
89
90 pub tariff_currency_code: currency::Code,
92
93 pub partial_cdr: PartialCdr,
100}
101
102#[derive(Debug)]
110pub struct PartialCdr {
111 pub currency_code: currency::Code,
113
114 pub party_id: Option<CpoId>,
122
123 pub start_date_time: DateTime<Utc>,
125
126 pub end_date_time: DateTime<Utc>,
128
129 pub total_energy: Option<Kwh>,
131
132 pub total_charging_duration: Option<TimeDelta>,
136
137 pub total_parking_duration: Option<TimeDelta>,
141
142 pub total_cost: Option<Price>,
144
145 pub total_energy_cost: Option<Price>,
147
148 pub total_fixed_cost: Option<Price>,
150
151 pub total_parking_duration_cost: Option<Price>,
153
154 pub total_charging_duration_cost: Option<Price>,
156
157 pub charging_periods: Vec<ChargingPeriod>,
160}
161
162#[derive(Clone, Debug)]
167pub struct CpoId {
168 pub country_code: country::Code,
170
171 pub id: String,
173}
174
175impl<'buf> From<tariff::CpoId<'buf>> for CpoId {
176 fn from(value: tariff::CpoId<'buf>) -> Self {
177 let tariff::CpoId { country_code, id } = value;
178 CpoId {
179 country_code,
180 id: id.to_string(),
181 }
182 }
183}
184
185impl fmt::Display for CpoId {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 write!(f, "{}{}", self.country_code.into_alpha_2_str(), self.id)
189 }
190}
191
192#[derive(Debug)]
196pub struct ChargingPeriod {
197 pub start_date_time: DateTime<Utc>,
200
201 pub dimensions: Vec<Dimension>,
203
204 pub tariff_id: Option<String>,
208}
209
210#[derive(Debug)]
214pub struct Dimension {
215 pub dimension_type: DimensionType,
216
217 pub volume: Decimal,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
225pub enum DimensionType {
226 Energy,
228
229 MaxCurrent,
231
232 MinCurrent,
234
235 MaxPower,
237
238 MinPower,
240
241 ParkingTime,
243
244 ReservationTime,
246
247 Time,
249}
250
251#[derive(Clone)]
253pub struct Config {
254 pub timezone: chrono_tz::Tz,
256
257 pub end_date_time: DateTime<Utc>,
259
260 pub max_current_supply_amp: Decimal,
262
263 pub requested_kwh: Decimal,
268
269 pub max_power_supply_kw: Decimal,
278
279 pub start_date_time: DateTime<Utc>,
281}
282
283pub fn cdr_from_tariff(tariff_elem: &tariff::Versioned<'_>, config: &Config) -> Verdict<Report> {
285 let mut warnings = warning::Set::new();
286 let (metrics, timezone) = metrics(tariff_elem, config)?.gather_warnings_into(&mut warnings);
294
295 let tariff = match tariff_elem.version() {
296 Version::V211 => {
297 let tariff = tariff::v211::Tariff::from_json(tariff_elem.as_element())?
298 .gather_warnings_into(&mut warnings);
299
300 tariff::v221::Tariff::from(tariff)
301 }
302 Version::V221 => tariff::v221::Tariff::from_json(tariff_elem.as_element())?
303 .gather_warnings_into(&mut warnings),
304 };
305
306 if !is_tariff_active(&metrics.start_date_time, &tariff) {
307 warnings.insert(tariff::Warning::NotActive.into(), tariff_elem.as_element());
308 }
309
310 let timeline = timeline(timezone, &metrics, &tariff);
311 let charging_periods = charge_periods(&metrics, timeline);
312
313 let report = price::periods(metrics.end_date_time, timezone, &tariff, charging_periods)
314 .with_element(tariff_elem.as_element())?
315 .gather_warnings_into(&mut warnings);
316
317 let price::PeriodsReport {
318 billable: _,
319 periods,
320 totals,
321 total_costs,
322 } = report;
323
324 let charging_periods = periods
325 .into_iter()
326 .map(|period| {
327 let price::PeriodReport {
328 start_date_time,
329 end_date_time: _,
330 dimensions,
331 } = period;
332 let time = dimensions
333 .duration_charging
334 .volume
335 .as_ref()
336 .map(|dt| Dimension {
337 dimension_type: DimensionType::Time,
338 volume: ToHoursDecimal::to_hours_dec_in_ocpi_precision(dt),
339 });
340 let parking_time = dimensions
341 .duration_parking
342 .volume
343 .as_ref()
344 .map(|dt| Dimension {
345 dimension_type: DimensionType::ParkingTime,
346 volume: ToHoursDecimal::to_hours_dec_in_ocpi_precision(dt),
347 });
348 let energy = dimensions.energy.volume.as_ref().map(|kwh| Dimension {
349 dimension_type: DimensionType::Energy,
350 volume: (*kwh).into(),
351 });
352 let dimensions = vec![energy, parking_time, time]
353 .into_iter()
354 .flatten()
355 .collect();
356
357 ChargingPeriod {
358 start_date_time,
359 dimensions,
360 tariff_id: Some(tariff.id.to_string()),
361 }
362 })
363 .collect();
364
365 let mut total_cost = total_costs.total();
366
367 if let Some(total_cost) = total_cost.as_mut() {
368 if let Some(min_price) = tariff.min_price {
369 if *total_cost < min_price {
370 *total_cost = min_price;
371 warnings.insert(
372 tariff::Warning::TotalCostClampedToMin.into(),
373 tariff_elem.as_element(),
374 );
375 }
376 }
377
378 if let Some(max_price) = tariff.max_price {
379 if *total_cost > max_price {
380 *total_cost = max_price;
381 warnings.insert(
382 tariff::Warning::TotalCostClampedToMin.into(),
383 tariff_elem.as_element(),
384 );
385 }
386 }
387 }
388
389 let report = Report {
390 tariff_id: tariff.id.to_string(),
391 tariff_currency_code: tariff.currency,
392 partial_cdr: PartialCdr {
393 party_id: tariff.party_id.map(CpoId::from),
394 start_date_time: metrics.start_date_time,
395 end_date_time: metrics.end_date_time,
396 currency_code: tariff.currency,
397 total_energy: totals.energy.round_to_ocpi_scale(),
398 total_charging_duration: totals.duration_charging,
399 total_parking_duration: totals.duration_parking,
400 total_cost: total_cost.round_to_ocpi_scale(),
401 total_energy_cost: total_costs.energy.round_to_ocpi_scale(),
402 total_fixed_cost: total_costs.fixed.round_to_ocpi_scale(),
403 total_parking_duration_cost: total_costs.duration_parking.round_to_ocpi_scale(),
404 total_charging_duration_cost: total_costs.duration_charging.round_to_ocpi_scale(),
405 charging_periods,
406 },
407 };
408
409 Ok(report.into_caveat(warnings))
410}
411
412struct EventCollector {
414 session_duration: TimeDelta,
416
417 events: Vec<Event>,
419}
420
421impl EventCollector {
422 fn with_session_duration(session_duration: TimeDelta) -> Self {
424 Self {
425 session_duration,
426 events: vec![],
427 }
428 }
429
430 fn push(&mut self, duration_from_start: TimeDelta, event_kind: EventKind) {
432 if duration_from_start <= self.session_duration {
433 self.events.push(Event {
434 duration_from_start,
435 kind: event_kind,
436 });
437 }
438 }
439
440 fn push_with(&mut self, event_kind: EventKind) -> impl FnOnce(TimeDelta) + use<'_> {
442 move |dt| {
443 self.push(dt, event_kind);
444 }
445 }
446
447 fn into_inner(self) -> Vec<Event> {
449 self.events
450 }
451}
452
453fn timeline(
455 timezone: chrono_tz::Tz,
456 metrics: &Metrics,
457 tariff: &tariff::v221::Tariff<'_>,
458) -> Timeline {
459 let Metrics {
460 start_date_time: cdr_start,
461 end_date_time: cdr_end,
462 duration_charging,
463 duration_parking,
464 max_power_supply,
465 max_current_supply,
466
467 energy_supplied: _,
468 } = metrics;
469
470 let mut events = {
471 let session_duration = duration_parking.map(|d| duration_charging.saturating_add(d));
472 let mut events =
473 EventCollector::with_session_duration(session_duration.unwrap_or(*duration_charging));
474
475 events.push(TimeDelta::seconds(0), EventKind::SessionStart);
476 events.push(*duration_charging, EventKind::ChargingEnd);
477 session_duration.map(events.push_with(EventKind::ParkingEnd {
478 start: *duration_charging,
479 }));
480
481 events
482 };
483
484 let mut emit_current = false;
487
488 let mut emit_power = false;
491
492 for elem in &tariff.elements {
493 if let Some((time_restrictions, energy_restrictions)) = elem
494 .restrictions
495 .as_ref()
496 .map(tariff::v221::Restrictions::restrictions_by_category)
497 {
498 generate_time_events(
499 &mut events,
500 timezone,
501 *cdr_start..*cdr_end,
502 time_restrictions,
503 );
504
505 let v2x::EnergyRestrictions {
506 min_kwh,
507 max_kwh,
508 min_current,
509 max_current,
510 min_power,
511 max_power,
512 } = energy_restrictions;
513
514 if !emit_current {
515 emit_current = (min_current..=max_current).contains(&Some(*max_current_supply));
520 }
521
522 if !emit_power {
523 emit_power = (min_power..=max_power).contains(&Some(*max_power_supply));
528 }
529
530 generate_energy_events(
531 &mut events,
532 metrics.duration_charging,
533 metrics.energy_supplied,
534 min_kwh,
535 max_kwh,
536 );
537 }
538 }
539
540 let events = events.into_inner();
541
542 Timeline {
543 events,
544 emit_current,
545 emit_power,
546 }
547}
548
549fn generate_time_events(
551 events: &mut EventCollector,
552 timezone: chrono_tz::Tz,
553 cdr_span: DateTimeSpan,
554 restrictions: v2x::TimeRestrictions,
555) {
556 const MIDNIGHT: NaiveTime = NaiveTime::from_hms_opt(0, 0, 0)
557 .expect("The hour, minute and second values are correct and hardcoded");
558 const ONE_DAY: TimeDelta = TimeDelta::days(1);
559
560 let v2x::TimeRestrictions {
561 start_time,
562 end_time,
563 start_date,
564 end_date,
565 min_duration,
566 max_duration,
567 weekdays,
568 } = restrictions;
569
570 let cdr_duration = cdr_span.end.signed_duration_since(cdr_span.start);
571
572 min_duration
574 .filter(|dt| &cdr_duration < dt)
575 .map(events.push_with(EventKind::MinDuration));
576
577 max_duration
579 .filter(|dt| &cdr_duration < dt)
580 .map(events.push_with(EventKind::MaxDuration));
581
582 let (start_date_time, end_date_time) =
592 if let (Some(start_time), Some(end_time)) = (start_time, end_time) {
593 if end_time < start_time {
594 (
595 start_date.map(|d| d.and_time(start_time)),
596 end_date.map(|d| {
597 let (end_time, _) = end_time.overflowing_add_signed(ONE_DAY);
598 d.and_time(end_time)
599 }),
600 )
601 } else {
602 (
603 start_date.map(|d| d.and_time(start_time)),
604 end_date.map(|d| d.and_time(end_time)),
605 )
606 }
607 } else {
608 (
609 start_date.map(|d| d.and_time(start_time.unwrap_or(MIDNIGHT))),
610 end_date.map(|d| d.and_time(end_time.unwrap_or(MIDNIGHT))),
611 )
612 };
613
614 let event_span = clamp_date_time_span(
617 start_date_time.and_then(|d| local_to_utc(timezone, d)),
618 end_date_time.and_then(|d| local_to_utc(timezone, d)),
619 cdr_span,
620 );
621
622 if let Some(start_time) = start_time {
623 gen_naive_time_events(
624 events,
625 &event_span,
626 start_time,
627 &weekdays,
628 EventKind::StartTime,
629 );
630 }
631
632 if let Some(end_time) = end_time {
633 gen_naive_time_events(events, &event_span, end_time, &weekdays, EventKind::EndTime);
634 }
635}
636
637fn local_to_utc(timezone: chrono_tz::Tz, date_time: NaiveDateTime) -> Option<DateTime<Utc>> {
643 use chrono::offset::LocalResult;
644
645 let result = date_time.and_local_timezone(timezone);
646
647 let local_date_time = match result {
648 LocalResult::Single(d) => d,
649 LocalResult::Ambiguous(earliest, _latest) => earliest,
650 LocalResult::None => return None,
651 };
652
653 Some(local_date_time.to_utc())
654}
655
656fn gen_naive_time_events(
658 events: &mut EventCollector,
659 event_span: &Range<DateTime<Utc>>,
660 time: NaiveTime,
661 weekdays: &v2x::WeekdaySet,
662 kind: EventKind,
663) {
664 let time_delta = time.signed_duration_since(event_span.start.time());
665 let cdr_duration = event_span.end.signed_duration_since(event_span.start);
666
667 let time_delta = if time_delta.num_seconds().is_negative() {
670 let (time_delta, _) = time.overflowing_add_signed(TimeDelta::days(1));
671 time_delta.signed_duration_since(event_span.start.time())
672 } else {
673 time_delta
674 };
675
676 if time_delta.num_seconds().is_negative() {
678 return;
679 }
680
681 let Some(remainder) = cdr_duration.checked_sub(&time_delta) else {
683 warn!("TimeDelta overflow");
684 return;
685 };
686
687 if remainder.num_seconds().is_positive() {
688 let duration_from_start = time_delta;
689 let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
690 warn!("Date out of range");
691 return;
692 };
693
694 if weekdays.contains(date.weekday()) {
695 events.push(time_delta, kind);
697 }
698
699 for day in 1..=remainder.num_days() {
700 let Some(duration_from_start) = time_delta.checked_add(&TimeDelta::days(day)) else {
701 warn!("Date out of range");
702 break;
703 };
704 let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
705 warn!("Date out of range");
706 break;
707 };
708
709 if weekdays.contains(date.weekday()) {
710 events.push(duration_from_start, kind);
711 }
712 }
713 }
714}
715
716fn generate_energy_events(
718 events: &mut EventCollector,
719 duration_charging: TimeDelta,
720 energy_supplied: Kwh,
721 min_kwh: Option<Kwh>,
722 max_kwh: Option<Kwh>,
723) {
724 min_kwh
725 .and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
726 .map(events.push_with(EventKind::MinKwh));
727
728 max_kwh
729 .and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
730 .map(events.push_with(EventKind::MaxKwh));
731}
732
733#[instrument]
735fn power_to_time(power: Kwh, power_total: Kwh, duration_total: TimeDelta) -> Option<TimeDelta> {
736 if power == power_total {
739 return Some(duration_total);
740 }
741
742 let power = Decimal::from(power);
745 let power_total = Decimal::from(power_total);
747
748 let Some(factor) = power.checked_div(power_total) else {
750 return Some(TimeDelta::zero());
751 };
752
753 if factor.is_sign_negative() || factor > dec!(1.0) {
754 return None;
755 }
756
757 let hours_dec = duration_total.to_hours_dec();
758 let duration_from_start = factor.checked_mul(hours_dec)?;
759 Some(duration_from_start.to_duration_ceil_nanos())
760}
761
762fn charge_periods(metrics: &Metrics, timeline: Timeline) -> Vec<price::Period> {
764 enum ChargingPhase {
766 Charging,
767 Parking,
768 }
769
770 let Metrics {
771 start_date_time: cdr_start,
772 max_power_supply,
773 max_current_supply,
774
775 end_date_time: _,
776 duration_charging: _,
777 duration_parking: _,
778 energy_supplied: _,
779 } = metrics;
780
781 let Timeline {
782 mut events,
783 emit_current,
784 emit_power,
785 } = timeline;
786
787 events.sort_unstable_by_key(|e| e.duration_from_start);
788
789 let mut periods = vec![];
790 let emit_current = emit_current.then_some(*max_current_supply);
791 let emit_power = emit_power.then_some(*max_power_supply);
792 let mut charging_phase = ChargingPhase::Charging;
794
795 for items in events.windows(2) {
796 let [event, event_next] = items else {
797 unreachable!("The window size is 2");
798 };
799
800 let Event {
801 duration_from_start,
802 kind,
803 } = event;
804
805 if let EventKind::ChargingEnd = kind {
806 charging_phase = ChargingPhase::Parking;
807 }
808
809 let Some(duration) = event_next
810 .duration_from_start
811 .checked_sub(duration_from_start)
812 else {
813 warn!("TimeDelta overflow");
814 break;
815 };
816
817 let Some(start_date_time) = cdr_start.checked_add_signed(*duration_from_start) else {
818 warn!("TimeDelta overflow");
819 break;
820 };
821
822 let consumed = if let ChargingPhase::Charging = charging_phase {
823 let Some(energy) =
824 Decimal::from(*max_power_supply).checked_mul(duration.to_hours_dec())
825 else {
826 warn!("Decimal overflow");
827 break;
828 };
829 price::Consumed {
830 duration_charging: Some(duration),
831 duration_parking: None,
832 energy: Some(Kwh::from_decimal(energy)),
833 current_max: emit_current,
834 current_min: emit_current,
835 power_max: emit_power,
836 power_min: emit_power,
837 }
838 } else {
839 price::Consumed {
840 duration_charging: None,
841 duration_parking: Some(duration),
842 energy: None,
843 current_max: None,
844 current_min: None,
845 power_max: None,
846 power_min: None,
847 }
848 };
849
850 let period = price::Period {
851 start_date_time,
852 consumed,
853 };
854
855 periods.push(period);
856 }
857
858 periods
859}
860
861fn clamp_date_time_span(
867 min_date: Option<DateTime<Utc>>,
868 max_date: Option<DateTime<Utc>>,
869 span: DateTimeSpan,
870) -> DateTimeSpan {
871 let (min_date, max_date) = (min(min_date, max_date), max(min_date, max_date));
873
874 let start = min_date.filter(|d| &span.start < d).unwrap_or(span.start);
875 let end = max_date.filter(|d| &span.end > d).unwrap_or(span.end);
876
877 DateTimeSpan { start, end }
878}
879
880struct Timeline {
882 events: Vec<Event>,
884
885 emit_current: bool,
887
888 emit_power: bool,
890}
891
892struct Event {
894 duration_from_start: TimeDelta,
896
897 kind: EventKind,
899}
900
901impl fmt::Debug for Event {
902 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
903 f.debug_struct("Event")
904 .field("duration_from_start", &self.duration_from_start.as_hms())
905 .field("kind", &self.kind)
906 .finish()
907 }
908}
909
910#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
912enum EventKind {
913 SessionStart,
919
920 ChargingEnd,
925
926 ParkingEnd {
931 start: TimeDelta,
933 },
934
935 StartTime,
936
937 EndTime,
938
939 MinDuration,
944
945 MaxDuration,
950
951 MinKwh,
953
954 MaxKwh,
956}
957
958impl fmt::Debug for EventKind {
959 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
960 match self {
961 Self::SessionStart => write!(f, "SessionStart"),
962 Self::ChargingEnd => write!(f, "ChargingEnd"),
963 Self::ParkingEnd { start } => f
964 .debug_struct("ParkingEnd")
965 .field("start", &start.as_hms())
966 .finish(),
967 Self::StartTime => write!(f, "StartTime"),
968 Self::EndTime => write!(f, "EndTime"),
969 Self::MinDuration => write!(f, "MinDuration"),
970 Self::MaxDuration => write!(f, "MaxDuration"),
971 Self::MinKwh => write!(f, "MinKwh"),
972 Self::MaxKwh => write!(f, "MaxKwh"),
973 }
974 }
975}
976
977#[derive(Debug)]
979struct Metrics {
980 end_date_time: DateTime<Utc>,
982
983 start_date_time: DateTime<Utc>,
985
986 duration_charging: TimeDelta,
991
992 duration_parking: Option<TimeDelta>,
996
997 energy_supplied: Kwh,
999
1000 max_current_supply: Ampere,
1002
1003 max_power_supply: Kw,
1005}
1006
1007#[instrument(skip_all)]
1009fn metrics(elem: &tariff::Versioned<'_>, config: &Config) -> Verdict<(Metrics, chrono_tz::Tz)> {
1010 let warnings = warning::Set::new();
1011
1012 let Config {
1013 start_date_time,
1014 end_date_time,
1015 max_power_supply_kw,
1016 requested_kwh: max_energy_battery_kwh,
1017 max_current_supply_amp,
1018 timezone,
1019 } = config;
1020 let duration_session = end_date_time.signed_duration_since(start_date_time);
1021
1022 debug!("duration_session: {}", duration_session.as_hms());
1023
1024 if duration_session.abs() != duration_session {
1026 return warnings.bail(Warning::StartDateTimeIsAfterEndDateTime, elem.as_element());
1027 }
1028
1029 if duration_session.num_seconds() < MIN_CS_DURATION_SECS {
1030 return warnings.bail(Warning::DurationBelowMinimum, elem.as_element());
1031 }
1032
1033 let duration_full_charge = some_dec_or_bail!(
1035 elem,
1036 max_energy_battery_kwh.checked_div(*max_power_supply_kw),
1037 warnings,
1038 "Unable to calculate changing time"
1039 )
1040 .to_duration_ceil_nanos();
1041 debug!("duration_full_charge: {}", duration_full_charge.as_hms());
1042
1043 let duration_charging = TimeDelta::min(duration_full_charge, duration_session);
1045
1046 let energy_supplied_kwh = some_dec_or_bail!(
1047 elem,
1048 max_energy_battery_kwh.checked_div(duration_charging.to_hours_dec()),
1049 warnings,
1050 "Unable to calculate the power supplied during the charging time"
1051 );
1052
1053 let duration_parking = some_time_delta_or_bail!(
1054 elem,
1055 duration_session.checked_sub(&duration_charging),
1056 warnings,
1057 "Unable to calculate `idle_duration`"
1058 );
1059
1060 debug!(
1061 "duration_charging: {}, duration_parking: {}",
1062 duration_charging.as_hms(),
1063 duration_parking.as_hms()
1064 );
1065
1066 let metrics = Metrics {
1067 end_date_time: *end_date_time,
1068 start_date_time: *start_date_time,
1069 duration_charging,
1070 duration_parking: Some(duration_parking).filter(|dt| dt.num_seconds().is_positive()),
1071 energy_supplied: Kwh::from_decimal(energy_supplied_kwh),
1072 max_current_supply: Ampere::from_decimal(*max_current_supply_amp),
1073 max_power_supply: Kw::from_decimal(*max_power_supply_kw),
1074 };
1075
1076 Ok((metrics, *timezone).into_caveat(warnings))
1077}
1078
1079fn is_tariff_active(cdr_start: &DateTime<Utc>, tariff: &tariff::v221::Tariff<'_>) -> bool {
1080 match (tariff.start_date_time, tariff.end_date_time) {
1081 (None, None) => true,
1082 (None, Some(end)) => (..end).contains(cdr_start),
1083 (Some(start), None) => (start..).contains(cdr_start),
1084 (Some(start), Some(end)) => (start..end).contains(cdr_start),
1085 }
1086}
1087
1088#[derive(Debug)]
1089pub enum Warning {
1090 Decimal(&'static str),
1092
1093 DurationBelowMinimum,
1095
1096 Price(price::Warning),
1097
1098 StartDateTimeIsAfterEndDateTime,
1100
1101 Tariff(tariff::Warning),
1102
1103 TimeDelta(&'static str),
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::TimeDelta(_) => warning::Id::from_static("timedelta_error"),
1117 Self::Tariff(kind) => kind.id(),
1118 }
1119 }
1120}
1121
1122impl fmt::Display for Warning {
1123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1124 match self {
1125 Self::Decimal(msg) | Self::TimeDelta(msg) => f.write_str(msg),
1126 Self::DurationBelowMinimum => write!(
1127 f,
1128 "The duration of the chargesession is below the minimum: {MIN_CS_DURATION_SECS}"
1129 ),
1130 Self::Price(warnings) => {
1131 write!(f, "Price warnings: {warnings:?}")
1132 }
1133 Self::StartDateTimeIsAfterEndDateTime => {
1134 write!(f, "The `start_date_time` is after the `end_date_time`")
1135 }
1136 Self::Tariff(warnings) => {
1137 write!(f, "Tariff warnings: {warnings:?}")
1138 }
1139 }
1140 }
1141}
1142
1143from_warning_all!(
1144 tariff::Warning => Warning::Tariff,
1145 price::Warning => Warning::Price
1146);