use core::fmt;
use serde::{Deserialize, Serialize};
use crate::types::{DateTime, Number};
use crate::v2_3_0::tariffs::TariffDimensionType;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DimensionCost {
pub dimension: TariffDimensionType,
pub measured: Number,
pub billed: Number,
pub cost: Number,
pub vat: Number,
pub segments: Vec<PricedSegment>,
}
impl DimensionCost {
#[must_use]
pub fn cost_with_vat(&self) -> Number {
self.cost + self.vat
}
#[must_use]
pub fn was_quantised(&self) -> bool {
self.billed != self.measured
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct PricedSegment {
pub start: DateTime,
pub quantity: Number,
pub price: Number,
pub vat_percentage: Option<Number>,
pub cost: Number,
pub applied: AppliedComponent,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppliedComponent {
pub tariff_id: String,
pub element_index: usize,
pub component_index: usize,
pub because: String,
}
impl fmt::Display for AppliedComponent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"tariff {} element {} component {}",
self.tariff_id, self.element_index, self.component_index
)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TaxLine {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub percentage: Option<Number>,
pub taxable: Number,
pub amount: Number,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PricingNoteCode {
NoPriceComponent,
PeriodSpansPriceChange,
PeriodsOutOfOrder,
TotalClamped,
NegativeTax,
UnattributedTax,
}
impl PricingNoteCode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::NoPriceComponent => "no_price_component",
Self::PeriodsOutOfOrder => "periods_out_of_order",
Self::PeriodSpansPriceChange => "period_spans_price_change",
Self::TotalClamped => "total_clamped",
Self::NegativeTax => "negative_tax",
Self::UnattributedTax => "unattributed_tax",
}
}
}
impl fmt::Display for PricingNoteCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct PricingNote {
pub code: PricingNoteCode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub at: Option<DateTime>,
pub message: String,
}
impl PricingNote {
pub(super) fn new(code: PricingNoteCode, at: Option<DateTime>, message: impl Into<String>) -> Self {
Self { code, at, message: message.into() }
}
}
impl fmt::Display for PricingNote {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.at {
Some(at) => write!(f, "[{}] at {at}: {}", self.code, self.message),
None => write!(f, "[{}] {}", self.code, self.message),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PriceLimitApplied {
Minimum,
Maximum,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CostBreakdown {
pub dimensions: Vec<DimensionCost>,
pub total_excl_vat: Number,
pub total_incl_vat: Number,
pub taxes: Vec<TaxLine>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit_applied: Option<PriceLimitApplied>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub notes: Vec<PricingNote>,
}
impl CostBreakdown {
#[must_use]
pub fn dimension(&self, dimension: TariffDimensionType) -> Option<&DimensionCost> {
self.dimensions.iter().find(|d| d.dimension == dimension)
}
#[must_use]
pub fn dimension_total(&self, dimension: TariffDimensionType) -> Number {
self.dimension(dimension).map_or(Number::ZERO, |d| d.cost)
}
#[must_use]
pub fn total_vat(&self) -> Number {
self.taxes.iter().map(|t| t.amount).sum()
}
pub fn notes_with(&self, code: PricingNoteCode) -> impl Iterator<Item = &PricingNote> {
self.notes.iter().filter(move |n| n.code == code)
}
#[must_use]
pub fn needs_review(&self) -> bool {
!self.notes.is_empty()
}
pub fn applied_components(&self) -> impl Iterator<Item = &AppliedComponent> {
self.dimensions.iter().flat_map(|d| d.segments.iter().map(|s| &s.applied))
}
#[cfg(feature = "v2_3_0")]
#[must_use]
pub fn to_price_v2_3_0(&self) -> crate::v2_3_0::types::Price {
crate::v2_3_0::types::Price {
before_taxes: self.total_excl_vat,
taxes: self
.taxes
.iter()
.map(|t| crate::v2_3_0::types::TaxAmount {
name: crate::types::OcpiText::new_lenient("VAT"),
account_number: None,
percentage: t.percentage,
amount: t.amount,
extensions: crate::types::Extensions::new(),
})
.collect(),
extensions: crate::types::Extensions::new(),
}
}
#[cfg(feature = "v2_2_1")]
#[must_use]
pub fn to_price_v2_2_1(&self) -> crate::v2_2_1::types::Price {
crate::v2_2_1::types::Price {
excl_vat: self.total_excl_vat,
incl_vat: Some(self.total_incl_vat),
extensions: crate::types::Extensions::new(),
}
}
}
impl fmt::Display for PriceLimitApplied {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Minimum => "minimum",
Self::Maximum => "maximum",
})
}
}
impl fmt::Display for CostBreakdown {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for d in &self.dimensions {
writeln!(
f,
"{:<13} {:>10} billed ({:>10} measured) = {:>10} excl. VAT, {:>10} VAT",
d.dimension.as_str(),
d.billed,
d.measured,
d.cost,
d.vat
)?;
}
for tax in &self.taxes {
let rate = tax.percentage.map_or_else(|| "unattributed".to_owned(), |p| format!("{p}%"));
writeln!(f, "{:<13} {:>10} on {:>10}", format!("VAT {rate}"), tax.amount, tax.taxable)?;
}
if let Some(limit) = self.limit_applied {
writeln!(f, "{:<13} the tariff's {limit} price limit moved the total", "LIMIT")?;
}
writeln!(
f,
"{:<13} {:>10} excl. VAT, {:>10} incl. VAT",
"TOTAL", self.total_excl_vat, self.total_incl_vat
)?;
for note in &self.notes {
write!(f, "\n[{}] {}", note.code, note.message)?;
if let Some(at) = note.at {
write!(f, " (at {at})")?;
}
}
Ok(())
}
}