Skip to main content

ocpi_tariffs/
lib.rs

1//! # OCPI Tariffs library
2//!
3//! Calculate the (sub)totals of a [charge session](https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc)
4//! using the [`cdr::price`] function and use the generated [`price::Report`] to review and compare the calculated
5//! totals versus the sources from the `CDR`.
6//!
7//! - Use [`json::parse_object`] to parse a CDR or tariff `&str` into a [`json::Document`].
8//! - Use [`cdr::infer_version`] or [`tariff::infer_version`] to guess which OCPI [`Version`] a CDR or tariff is.
9//! - Use the [`cdr::from_json`] and [`tariff::from_json`] functions to check a [`json::Document`] against the schema for a given version.
10//! - Use [`tariff::lint`] to lint a tariff: flag the constructs a schema-valid tariff can still get wrong.
11//!
12//! # Examples
13//!
14//! ## Price a CDR with embedded tariff
15//!
16//! If you have a CDR JSON with an embedded tariff you can price the CDR with the following code:
17//!
18//! ```rust
19//! # use ocpi_tariffs::{cdr, json, price, warning, Version};
20//! #
21//! # const CDR_JSON: &str = include_str!("cdr.json");
22//!
23//! let doc = json::parse_object(CDR_JSON)?;
24//! let (cdr, _warnings) = cdr::from_json(doc, Version::V211).into_parts();
25//!
26//! let report = cdr::price(&cdr, price::TariffSource::UseCdr, chrono_tz::Tz::Europe__Amsterdam).unwrap();
27//! let (report, warnings) = report.into_parts();
28//!
29//! if !warnings.is_empty() {
30//!     eprintln!("Pricing the CDR resulted in `{}` warnings", warnings.len_warnings());
31//!
32//!     for group in warnings {
33//!         let (element, warnings) = group.to_parts();
34//!         eprintln!("  {}", element.path);
35//!
36//!         for warning in warnings {
37//!             eprintln!("    - {warning}");
38//!         }
39//!     }
40//! }
41//!
42//! # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
43//! ```
44//!
45//! ## Price a CDR using tariff in separate JSON file
46//!
47//! If you have a CDR JSON with a tariff in a separate JSON file you can price the CDR with the
48//! following code:
49//!
50//! ```rust
51//! # use ocpi_tariffs::{cdr, json, price, tariff, warning, Version};
52//! #
53//! # const CDR_JSON: &str = include_str!("cdr.json");
54//! # const TARIFF_JSON: &str = include_str!("tariff.json");
55//!
56//! let cdr_doc = json::parse_object(CDR_JSON)?;
57//! let (cdr, _cdr_warnings) = cdr::from_json(cdr_doc, Version::V211).into_parts();
58//!
59//! let tariff_doc = json::parse_object(TARIFF_JSON)?;
60//! let (tariff, _tariff_warnings) = tariff::from_json(tariff_doc, Version::V211).into_parts();
61//!
62//! let report = cdr::price(&cdr, price::TariffSource::Override(vec![tariff]), chrono_tz::Tz::Europe__Amsterdam).unwrap();
63//! let (report, warnings) = report.into_parts();
64//!
65//! if !warnings.is_empty() {
66//!     eprintln!("Pricing the CDR resulted in `{}` warnings", warnings.len_warnings());
67//!
68//!     for group in warnings {
69//!         let (element, warnings) = group.to_parts();
70//!         eprintln!("  {}", element.path);
71//!
72//!         for warning in warnings {
73//!             eprintln!("    - {warning}");
74//!         }
75//!     }
76//! }
77//!
78//! # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
79//! ```
80//!
81
82#[cfg(test)]
83mod test;
84
85#[cfg(test)]
86mod test_rust_decimal_arbitrary_precision;
87
88pub mod cdr;
89pub mod country;
90pub mod currency;
91pub mod datetime;
92pub mod duration;
93mod energy;
94pub mod explain;
95pub mod generate;
96pub mod guess;
97pub mod json;
98pub mod lint;
99pub mod money;
100pub mod number;
101pub mod price;
102pub mod schema;
103pub mod string;
104pub mod tariff;
105pub mod timezone;
106pub mod warning;
107pub mod weekday;
108
109use std::fmt;
110
111#[doc(inline)]
112pub use duration::{ToDuration, ToHoursDecimal};
113#[doc(inline)]
114pub use energy::{Ampere, Kw, Kwh};
115#[doc(inline)]
116pub use explain::Language;
117#[doc(inline)]
118pub use money::{Cost, Money, Price, Vat};
119#[doc(inline)]
120use schema::FromSchema;
121use warning::IntoCaveat;
122#[doc(inline)]
123pub use warning::{Caveat, Verdict, VerdictExt, Warning};
124use weekday::Weekday;
125
126/// The Id for a tariff used in the pricing of a CDR.
127pub type TariffId = String;
128
129/// The OCPI versions supported by this crate.
130#[derive(Clone, Copy, Debug, PartialEq)]
131pub enum Version {
132    /// OCPI version 2.2.1.
133    ///
134    /// See: <https://github.com/ocpi/ocpi/tree/release-2.2.1-bugfixes>.
135    V221,
136
137    /// OCPI version 2.1.1.
138    ///
139    /// See: <https://github.com/ocpi/ocpi/tree/release-2.1.1-bugfixes>.
140    V211,
141}
142
143impl Versioned for Version {
144    fn version(&self) -> Version {
145        *self
146    }
147}
148
149impl fmt::Display for Version {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        match self {
152            Version::V221 => f.write_str("v221"),
153            Version::V211 => f.write_str("v211"),
154        }
155    }
156}
157
158/// An object for a specific OCPI [`Version`].
159pub trait Versioned: fmt::Debug {
160    /// Return the OCPI `Version` of this object.
161    fn version(&self) -> Version;
162}
163
164/// An object with an uncertain [`Version`].
165pub trait Unversioned: fmt::Debug {
166    /// The concrete [`Versioned`] type.
167    type Versioned: Versioned;
168
169    /// Forced an [`Unversioned`] object to be the given [`Version`].
170    ///
171    /// This does not change the structure of the OCPI object.
172    /// It simply relabels the object as a different OCPI Version.
173    ///
174    /// Use this with care.
175    fn force_into_versioned(self, version: Version) -> Self::Versioned;
176}
177
178/// Add two types together and saturate to max if the addition operation overflows.
179///
180/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
181trait SaturatingAdd {
182    /// Add two types together and saturate to max if the addition operation overflows.
183    #[must_use]
184    fn saturating_add(self, other: Self) -> Self;
185}
186
187/// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
188///
189/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
190trait SaturatingSub {
191    /// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
192    #[must_use]
193    fn saturating_sub(self, other: Self) -> Self;
194}
195
196/// A debug utility to `Display` an `Option<T>` as either `Display::fmt(T)` or the null set `∅`.
197struct DisplayOption<T>(Option<T>)
198where
199    T: fmt::Display;
200
201impl<T> fmt::Display for DisplayOption<T>
202where
203    T: fmt::Display,
204{
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        match &self.0 {
207            Some(v) => fmt::Display::fmt(v, f),
208            None => f.write_str("∅"),
209        }
210    }
211}