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 as _, ToHoursDecimal},
44 energy::{Ampere, Kw, Kwh},
45 from_warning_all, into_caveat, into_caveat_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 cpo_country_code: Option<country::Code>,
104
105 pub cpo_currency_code: currency::Code,
107
108 pub party_id: Option<String>,
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(Debug)]
154pub struct ChargingPeriod {
155 pub start_date_time: DateTime<Utc>,
158
159 pub dimensions: Vec<Dimension>,
161
162 pub tariff_id: Option<String>,
166}
167
168#[derive(Debug)]
172pub struct Dimension {
173 pub dimension_type: DimensionType,
174
175 pub volume: Decimal,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
183pub enum DimensionType {
184 Energy,
186
187 MaxCurrent,
189
190 MinCurrent,
192
193 MaxPower,
195
196 MinPower,
198
199 ParkingTime,
201
202 ReservationTime,
204
205 Time,
207}
208
209into_caveat_all!(Report, Timeline);
210
211#[derive(Clone)]
213pub struct Config {
214 pub timezone: chrono_tz::Tz,
216
217 pub end_date_time: DateTime<Utc>,
219
220 pub max_current_supply_amp: Decimal,
222
223 pub requested_kwh: Decimal,
228
229 pub max_power_supply_kw: Decimal,
238
239 pub start_date_time: DateTime<Utc>,
241}
242
243pub fn cdr_from_tariff(tariff_elem: &tariff::Versioned<'_>, config: Config) -> Verdict<Report> {
245 let mut warnings = warning::Set::new();
246 let (metrics, timezone) = metrics(tariff_elem, config)?.gather_warnings_into(&mut warnings);
254
255 let tariff = match tariff_elem.version() {
256 Version::V211 => {
257 let tariff = tariff::v211::Tariff::from_json(tariff_elem.as_element())?
258 .gather_warnings_into(&mut warnings);
259
260 tariff::v221::Tariff::from(tariff)
261 }
262 Version::V221 => tariff::v221::Tariff::from_json(tariff_elem.as_element())?
263 .gather_warnings_into(&mut warnings),
264 };
265
266 if !is_tariff_active(&metrics.start_date_time, &tariff) {
267 warnings.insert(tariff::Warning::NotActive.into(), tariff_elem.as_element());
268 }
269
270 let timeline = timeline(timezone, &metrics, &tariff);
271 let charging_periods = charge_periods(&metrics, timeline);
272
273 let report = price::periods(metrics.end_date_time, timezone, &tariff, charging_periods)
274 .with_element(tariff_elem.as_element())?
275 .gather_warnings_into(&mut warnings);
276
277 let price::PeriodsReport {
278 billable: _,
279 periods,
280 totals,
281 total_costs,
282 } = report;
283
284 let charging_periods = periods
285 .into_iter()
286 .map(|period| {
287 let price::PeriodReport {
288 start_date_time,
289 end_date_time: _,
290 dimensions,
291 } = period;
292 let time = dimensions
293 .duration_charging
294 .volume
295 .as_ref()
296 .map(|dt| Dimension {
297 dimension_type: DimensionType::Time,
298 volume: ToHoursDecimal::to_hours_dec(dt),
299 });
300 let parking_time = dimensions
301 .duration_parking
302 .volume
303 .as_ref()
304 .map(|dt| Dimension {
305 dimension_type: DimensionType::ParkingTime,
306 volume: ToHoursDecimal::to_hours_dec(dt),
307 });
308 let energy = dimensions.energy.volume.as_ref().map(|kwh| Dimension {
309 dimension_type: DimensionType::Energy,
310 volume: (*kwh).into(),
311 });
312 let dimensions = vec![energy, parking_time, time]
313 .into_iter()
314 .flatten()
315 .collect();
316
317 ChargingPeriod {
318 start_date_time,
319 dimensions,
320 tariff_id: Some(tariff.id.to_string()),
321 }
322 })
323 .collect();
324
325 let mut total_cost = total_costs.total();
326
327 if let Some(total_cost) = total_cost.as_mut() {
328 if let Some(min_price) = tariff.min_price {
329 if *total_cost < min_price {
330 *total_cost = min_price;
331 warnings.insert(
332 tariff::Warning::TotalCostClampedToMin.into(),
333 tariff_elem.as_element(),
334 );
335 }
336 }
337
338 if let Some(max_price) = tariff.max_price {
339 if *total_cost > max_price {
340 *total_cost = max_price;
341 warnings.insert(
342 tariff::Warning::TotalCostClampedToMin.into(),
343 tariff_elem.as_element(),
344 );
345 }
346 }
347 }
348
349 let report = Report {
350 tariff_id: tariff.id.to_string(),
351 tariff_currency_code: tariff.currency,
352 partial_cdr: PartialCdr {
353 cpo_country_code: tariff.country_code,
354 party_id: tariff.party_id.as_ref().map(ToString::to_string),
355 start_date_time: metrics.start_date_time,
356 end_date_time: metrics.end_date_time,
357 cpo_currency_code: tariff.currency,
358 total_energy: totals.energy.round_to_ocpi_scale(),
359 total_charging_duration: totals.duration_charging,
360 total_parking_duration: totals.duration_parking,
361 total_cost: total_cost.round_to_ocpi_scale(),
362 total_energy_cost: total_costs.energy.round_to_ocpi_scale(),
363 total_fixed_cost: total_costs.fixed.round_to_ocpi_scale(),
364 total_parking_duration_cost: total_costs.duration_parking.round_to_ocpi_scale(),
365 total_charging_duration_cost: total_costs.duration_charging.round_to_ocpi_scale(),
366 charging_periods,
367 },
368 };
369
370 Ok(report.into_caveat(warnings))
371}
372
373fn timeline(
375 timezone: chrono_tz::Tz,
376 metrics: &Metrics,
377 tariff: &tariff::v221::Tariff<'_>,
378) -> Timeline {
379 let mut events = vec![];
380
381 let Metrics {
382 start_date_time: cdr_start,
383 end_date_time: cdr_end,
384 duration_charging,
385 duration_parking,
386 max_power_supply,
387 max_current_supply,
388
389 energy_supplied: _,
390 } = metrics;
391
392 events.push(Event {
393 duration_from_start: TimeDelta::seconds(0),
394 kind: EventKind::SessionStart,
395 });
396
397 events.push(Event {
398 duration_from_start: *duration_charging,
399 kind: EventKind::ChargingEnd,
400 });
401
402 if let Some(duration_parking) = duration_parking {
403 let duration_from_start = duration_charging.saturating_add(*duration_parking);
404 events.push(Event {
405 duration_from_start,
406 kind: EventKind::ParkingEnd {
407 start: metrics.duration_charging,
408 },
409 });
410 }
411
412 let mut emit_current = false;
415
416 let mut emit_power = false;
419
420 for elem in &tariff.elements {
421 if let Some((time_restrictions, energy_restrictions)) = elem
422 .restrictions
423 .as_ref()
424 .map(tariff::v221::Restrictions::restrictions_by_category)
425 {
426 let mut time_events =
427 generate_time_events(timezone, *cdr_start..*cdr_end, time_restrictions);
428
429 let v2x::EnergyRestrictions {
430 min_kwh,
431 max_kwh,
432 min_current,
433 max_current,
434 min_power,
435 max_power,
436 } = energy_restrictions;
437
438 if !emit_current {
439 emit_current = (min_current..=max_current).contains(&Some(*max_current_supply));
444 }
445
446 if !emit_power {
447 emit_power = (min_power..=max_power).contains(&Some(*max_power_supply));
452 }
453
454 let mut energy_events = generate_energy_events(
455 metrics.duration_charging,
456 metrics.energy_supplied,
457 min_kwh,
458 max_kwh,
459 );
460
461 events.append(&mut time_events);
462 events.append(&mut energy_events);
463 }
464 }
465
466 Timeline {
467 events,
468 emit_current,
469 emit_power,
470 }
471}
472
473fn generate_time_events(
475 timezone: chrono_tz::Tz,
476 cdr_span: DateTimeSpan,
477 restrictions: v2x::TimeRestrictions,
478) -> Vec<Event> {
479 const MIDNIGHT: NaiveTime = NaiveTime::from_hms_opt(0, 0, 0)
480 .expect("The hour, minute and second values are correct and hardcoded");
481 const ONE_DAY: TimeDelta = TimeDelta::days(1);
482
483 let v2x::TimeRestrictions {
484 start_time,
485 end_time,
486 start_date,
487 end_date,
488 min_duration,
489 max_duration,
490 weekdays,
491 } = restrictions;
492 let mut events = vec![];
493
494 let cdr_duration = cdr_span.end.signed_duration_since(cdr_span.start);
495
496 if let Some(min_duration) = min_duration.filter(|dt| &cdr_duration < dt) {
498 events.push(Event {
499 duration_from_start: min_duration,
500 kind: EventKind::MinDuration,
501 });
502 }
503
504 if let Some(max_duration) = max_duration.filter(|dt| &cdr_duration < dt) {
506 events.push(Event {
507 duration_from_start: max_duration,
508 kind: EventKind::MaxDuration,
509 });
510 }
511
512 let (start_date_time, end_date_time) =
522 if let (Some(start_time), Some(end_time)) = (start_time, end_time) {
523 if end_time < start_time {
524 (
525 start_date.map(|d| d.and_time(start_time)),
526 end_date.map(|d| {
527 let (end_time, _) = end_time.overflowing_add_signed(ONE_DAY);
528 d.and_time(end_time)
529 }),
530 )
531 } else {
532 (
533 start_date.map(|d| d.and_time(start_time)),
534 end_date.map(|d| d.and_time(end_time)),
535 )
536 }
537 } else {
538 (
539 start_date.map(|d| d.and_time(start_time.unwrap_or(MIDNIGHT))),
540 end_date.map(|d| d.and_time(end_time.unwrap_or(MIDNIGHT))),
541 )
542 };
543
544 let event_span = clamp_date_time_span(
547 start_date_time.and_then(|d| local_to_utc(timezone, d)),
548 end_date_time.and_then(|d| local_to_utc(timezone, d)),
549 cdr_span,
550 );
551
552 if let Some(start_time) = start_time {
553 let mut start_events =
554 gen_naive_time_events(&event_span, start_time, &weekdays, EventKind::StartTime);
555 events.append(&mut start_events);
556 }
557
558 if let Some(end_time) = end_time {
559 let mut end_events =
560 gen_naive_time_events(&event_span, end_time, &weekdays, EventKind::EndTime);
561 events.append(&mut end_events);
562 }
563
564 events
565}
566
567fn local_to_utc(timezone: chrono_tz::Tz, date_time: NaiveDateTime) -> Option<DateTime<Utc>> {
573 use chrono::offset::LocalResult;
574
575 let result = date_time.and_local_timezone(timezone);
576
577 let local_date_time = match result {
578 LocalResult::Single(d) => d,
579 LocalResult::Ambiguous(earliest, _latest) => earliest,
580 LocalResult::None => return None,
581 };
582
583 Some(local_date_time.to_utc())
584}
585
586fn gen_naive_time_events(
588 event_span: &Range<DateTime<Utc>>,
589 time: NaiveTime,
590 weekdays: &v2x::WeekdaySet,
591 kind: EventKind,
592) -> Vec<Event> {
593 let mut events = vec![];
594 let time_delta = time.signed_duration_since(event_span.start.time());
595 let cdr_duration = event_span.end.signed_duration_since(event_span.start);
596
597 let time_delta = if time_delta.num_seconds().is_negative() {
600 let (time_delta, _) = time.overflowing_add_signed(TimeDelta::days(1));
601 time_delta.signed_duration_since(event_span.start.time())
602 } else {
603 time_delta
604 };
605
606 if time_delta.num_seconds().is_negative() {
608 return vec![];
609 }
610
611 let Some(remainder) = cdr_duration.checked_sub(&time_delta) else {
613 warn!("TimeDelta overflow");
614 return events;
615 };
616
617 if remainder.num_seconds().is_positive() {
618 let duration_from_start = time_delta;
619 let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
620 warn!("Date out of range");
621 return events;
622 };
623
624 if weekdays.contains(date.weekday()) {
625 events.push(Event {
627 duration_from_start: time_delta,
628 kind,
629 });
630 }
631
632 for day in 1..=remainder.num_days() {
633 let Some(duration_from_start) = time_delta.checked_add(&TimeDelta::days(day)) else {
634 warn!("Date out of range");
635 break;
636 };
637 let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
638 warn!("Date out of range");
639 break;
640 };
641
642 if weekdays.contains(date.weekday()) {
643 events.push(Event {
644 duration_from_start,
645 kind,
646 });
647 }
648 }
649 }
650
651 events
652}
653
654fn generate_energy_events(
656 duration_charging: TimeDelta,
657 energy_supplied: Kwh,
658 min_kwh: Option<Kwh>,
659 max_kwh: Option<Kwh>,
660) -> Vec<Event> {
661 let mut events = vec![];
662
663 if let Some(duration_from_start) =
664 min_kwh.and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
665 {
666 events.push(Event {
667 duration_from_start,
668 kind: EventKind::MinKwh,
669 });
670 }
671
672 if let Some(duration_from_start) =
673 max_kwh.and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
674 {
675 events.push(Event {
676 duration_from_start,
677 kind: EventKind::MaxKwh,
678 });
679 }
680
681 events
682}
683
684fn power_to_time(power: Kwh, power_total: Kwh, duration_total: TimeDelta) -> Option<TimeDelta> {
686 use rust_decimal::prelude::ToPrimitive;
687
688 let power = Decimal::from(power);
691 let power_total = Decimal::from(power_total);
693 let Some(factor) = power.checked_div(power_total) else {
696 return Some(TimeDelta::zero());
697 };
698
699 if factor.is_sign_negative() || factor > dec!(1.0) {
700 return None;
701 }
702
703 let duration_from_start = factor.checked_mul(Decimal::from(duration_total.num_seconds()))?;
704 duration_from_start.to_i64().map(TimeDelta::seconds)
705}
706
707fn charge_periods(metrics: &Metrics, timeline: Timeline) -> Vec<price::Period> {
709 enum ChargingPhase {
711 Charging,
712 Parking,
713 }
714
715 let Metrics {
716 start_date_time: cdr_start,
717 max_power_supply,
718 max_current_supply,
719
720 end_date_time: _,
721 duration_charging: _,
722 duration_parking: _,
723 energy_supplied: _,
724 } = metrics;
725
726 let Timeline {
727 mut events,
728 emit_current,
729 emit_power,
730 } = timeline;
731
732 events.sort_unstable_by_key(|e| e.duration_from_start);
733
734 let mut periods = vec![];
735 let emit_current = emit_current.then_some(*max_current_supply);
736 let emit_power = emit_power.then_some(*max_power_supply);
737 let mut charging_phase = ChargingPhase::Charging;
739
740 for items in events.windows(2) {
741 let [event, event_next] = items else {
742 unreachable!("The window size is 2");
743 };
744
745 let Event {
746 duration_from_start,
747 kind,
748 } = event;
749
750 if let EventKind::ChargingEnd = kind {
751 charging_phase = ChargingPhase::Parking;
752 }
753
754 let Some(duration) = event_next
755 .duration_from_start
756 .checked_sub(duration_from_start)
757 else {
758 warn!("TimeDelta overflow");
759 break;
760 };
761 let Some(start_date_time) = cdr_start.checked_add_signed(*duration_from_start) else {
762 warn!("TimeDelta overflow");
763 break;
764 };
765
766 let consumed = if let ChargingPhase::Charging = charging_phase {
767 let Some(energy) =
768 Decimal::from(*max_power_supply).checked_mul(duration.to_hours_dec())
769 else {
770 warn!("Decimal overflow");
771 break;
772 };
773 price::Consumed {
774 duration_charging: Some(duration),
775 duration_parking: None,
776 energy: Some(Kwh::from_decimal(energy)),
777 current_max: emit_current,
778 current_min: emit_current,
779 power_max: emit_power,
780 power_min: emit_power,
781 }
782 } else {
783 price::Consumed {
784 duration_charging: None,
785 duration_parking: Some(duration),
786 energy: None,
787 current_max: None,
788 current_min: None,
789 power_max: None,
790 power_min: None,
791 }
792 };
793
794 let period = price::Period {
795 start_date_time,
796 consumed,
797 };
798
799 periods.push(period);
800 }
801
802 periods
803}
804
805fn clamp_date_time_span(
811 min_date: Option<DateTime<Utc>>,
812 max_date: Option<DateTime<Utc>>,
813 span: DateTimeSpan,
814) -> DateTimeSpan {
815 let (min_date, max_date) = (min(min_date, max_date), max(min_date, max_date));
817
818 let start = min_date.filter(|d| &span.start < d).unwrap_or(span.start);
819 let end = max_date.filter(|d| &span.end > d).unwrap_or(span.end);
820
821 DateTimeSpan { start, end }
822}
823
824struct Timeline {
826 events: Vec<Event>,
828
829 emit_current: bool,
831
832 emit_power: bool,
834}
835
836#[derive(Debug)]
838struct Event {
839 duration_from_start: TimeDelta,
841
842 kind: EventKind,
844}
845
846#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
848enum EventKind {
849 SessionStart,
855
856 ChargingEnd,
861
862 ParkingEnd {
867 start: TimeDelta,
869 },
870
871 StartTime,
872
873 EndTime,
874
875 MinDuration,
880
881 MaxDuration,
886
887 MinKwh,
889
890 MaxKwh,
892}
893
894#[derive(Debug)]
896struct Metrics {
897 end_date_time: DateTime<Utc>,
899
900 start_date_time: DateTime<Utc>,
902
903 duration_charging: TimeDelta,
908
909 duration_parking: Option<TimeDelta>,
913
914 energy_supplied: Kwh,
916
917 max_current_supply: Ampere,
919
920 max_power_supply: Kw,
922}
923
924into_caveat!(Metrics);
925
926#[instrument(skip_all)]
928fn metrics(elem: &tariff::Versioned<'_>, config: Config) -> Verdict<(Metrics, chrono_tz::Tz)> {
929 const SECS_IN_HOUR: Decimal = dec!(3600);
930
931 let warnings = warning::Set::new();
932
933 let Config {
934 start_date_time,
935 end_date_time,
936 max_power_supply_kw,
937 requested_kwh: max_energy_battery_kwh,
938 max_current_supply_amp,
939 timezone,
940 } = config;
941 let duration_session = end_date_time.signed_duration_since(start_date_time);
942
943 debug!("duration_session: {}", duration_session.as_hms());
944
945 if duration_session.num_seconds().is_negative() {
947 return warnings.bail(Warning::StartDateTimeIsAfterEndDateTime, elem.as_element());
948 }
949
950 if duration_session.num_seconds() < MIN_CS_DURATION_SECS {
951 return warnings.bail(Warning::DurationBelowMinimum, elem.as_element());
952 }
953
954 let duration_full_charge_hours = some_dec_or_bail!(
956 elem,
957 max_energy_battery_kwh.checked_div(max_power_supply_kw),
958 warnings,
959 "Unable to calculate changing time"
960 );
961 debug!(
962 "duration_full_charge: {}",
963 duration_full_charge_hours.as_hms()
964 );
965
966 let charge_duration_hours =
968 Decimal::min(duration_full_charge_hours, duration_session.to_hours_dec());
969
970 let power_supplied_kwh = some_dec_or_bail!(
971 elem,
972 max_energy_battery_kwh.checked_div(charge_duration_hours),
973 warnings,
974 "Unable to calculate the power supplied during the charging time"
975 );
976
977 let charging_duration_secs = some_dec_or_bail!(
979 elem,
980 charge_duration_hours.checked_mul(SECS_IN_HOUR),
981 warnings,
982 "Unable to convert charging time from hours to seconds"
983 );
984
985 let charging_duration_secs = some_dec_or_bail!(
986 elem,
987 charging_duration_secs.to_i64(),
988 warnings,
989 "Unable to convert charging duration Decimal to i64"
990 );
991 let duration_charging = TimeDelta::seconds(charging_duration_secs);
992
993 let duration_parking = some_dec_or_bail!(
994 elem,
995 duration_session.checked_sub(&duration_charging),
996 warnings,
997 "Unable to calculate `idle_duration`"
998 );
999
1000 debug!(
1001 "duration_charging: {}, duration_parking: {}",
1002 duration_charging.as_hms(),
1003 duration_parking.as_hms()
1004 );
1005
1006 let metrics = Metrics {
1007 end_date_time,
1008 start_date_time,
1009 duration_charging,
1010 duration_parking: Some(duration_parking).filter(|dt| dt.num_seconds().is_positive()),
1011 energy_supplied: Kwh::from_decimal(power_supplied_kwh),
1012 max_current_supply: Ampere::from_decimal(max_current_supply_amp),
1013 max_power_supply: Kw::from_decimal(max_power_supply_kw),
1014 };
1015
1016 Ok((metrics, timezone).into_caveat(warnings))
1017}
1018
1019fn is_tariff_active(cdr_start: &DateTime<Utc>, tariff: &tariff::v221::Tariff<'_>) -> bool {
1020 match (tariff.start_date_time, tariff.end_date_time) {
1021 (None, None) => true,
1022 (None, Some(end)) => (..end).contains(cdr_start),
1023 (Some(start), None) => (start..).contains(cdr_start),
1024 (Some(start), Some(end)) => (start..end).contains(cdr_start),
1025 }
1026}
1027
1028#[derive(Debug)]
1029pub enum Warning {
1030 Decimal(&'static str),
1032
1033 DurationBelowMinimum,
1035
1036 Price(price::Warning),
1037
1038 StartDateTimeIsAfterEndDateTime,
1040
1041 Tariff(tariff::Warning),
1042}
1043
1044impl crate::Warning for Warning {
1045 fn id(&self) -> warning::Id {
1046 match self {
1047 Self::Decimal(_) => warning::Id::from_static("decimal_error"),
1048 Self::DurationBelowMinimum => warning::Id::from_static("duration_below_minimum"),
1049 Self::Price(kind) => kind.id(),
1050 Self::StartDateTimeIsAfterEndDateTime => {
1051 warning::Id::from_static("start_time_after_end_time")
1052 }
1053 Self::Tariff(kind) => kind.id(),
1054 }
1055 }
1056}
1057
1058impl fmt::Display for Warning {
1059 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1060 match self {
1061 Self::Decimal(msg) => f.write_str(msg),
1062 Self::DurationBelowMinimum => write!(
1063 f,
1064 "The duration of the chargesession is below the minimum: {MIN_CS_DURATION_SECS}"
1065 ),
1066 Self::Price(warnings) => {
1067 write!(f, "Price warnings: {warnings:?}")
1068 }
1069 Self::StartDateTimeIsAfterEndDateTime => {
1070 write!(f, "The `start_date_time` is after the `end_date_time`")
1071 }
1072 Self::Tariff(warnings) => {
1073 write!(f, "Tariff warnings: {warnings:?}")
1074 }
1075 }
1076 }
1077}
1078
1079from_warning_all!(
1080 tariff::Warning => Warning::Tariff,
1081 price::Warning => Warning::Price
1082);