use chrono_tz::Tz;
use clap::Parser;
use console::style;
use ocpi_tariffs::{cdr, price, tariff, timezone, warning::VerdictExt as _};
use crate::{
opts::{job_description, load_cdr, load_tariff, FilePath, TariffArgs},
print::{self, Col, Table},
Error, ObjectKind,
};
#[derive(Parser)]
pub struct Command {
#[command(flatten)]
args: TariffArgs,
}
impl Command {
pub fn run(self) -> Result<(), Error> {
let Self {
args:
TariffArgs {
cdr,
tariff,
timezone,
ocpi_version,
},
} = self;
let ocpi_version = ocpi_version.into();
let cdr_file_path = cdr.map(FilePath::from_path_buf).transpose()?;
let tariff_file_path = tariff.map(FilePath::from_path_buf).transpose()?;
let job_description = job_description(cdr_file_path.as_ref(), tariff_file_path.as_ref());
eprintln!("{} {}", style("Analyzing").green().bold(), job_description,);
let cdr_json = load_cdr(cdr_file_path, ocpi_version)?;
let report = cdr::parse_with_version(&cdr_json, ocpi_version)?;
let cdr::ParseReport {
cdr,
unexpected_fields,
} = report;
let timezone = if let Some(tz) = timezone {
tz.parse().map_err(Error::InvalidTimezone)?
} else {
let timezone = match timezone::find_or_infer(&cdr) {
Ok(tz) => tz,
Err(err_set) => {
let (error, warnings) = err_set.into_parts();
print::timezone_error(&error);
print::timezone_warnings(&warnings);
return Err(Error::Handled);
}
};
let (timezone_source, warnings) = timezone.into_parts();
print::unexpected_fields(ObjectKind::Cdr, &unexpected_fields);
print::timezone_warnings(&warnings);
timezone_source.into_timezone()
};
let report = if let Some(path) = tariff_file_path {
let tariff_json = load_tariff(&path, ocpi_version)?;
let tariff::ParseReport {
tariff,
unexpected_fields,
} = tariff::parse_with_version(&tariff_json, ocpi_version)?;
print::unexpected_fields(ObjectKind::Tariff, &unexpected_fields);
cdr::price(&cdr, price::TariffSource::Override(vec![tariff]), timezone).only_error()?
} else {
cdr::price(&cdr, price::TariffSource::UseCdr, timezone).only_error()?
};
let (report, warnings) = report.into_parts();
eprintln!(
"Completed {} {}, using time-zone `{}`:\n",
style("analyzing").green().bold(),
job_description,
style(&timezone).blue(),
);
print::cdr_warnings(&warnings);
print::tariff_reports(&report.tariff_reports);
let timezone: Tz = report.timezone.parse().map_err(Error::InvalidTimezone)?;
{
let table = periods_summary(&report, timezone);
println!("{table}");
}
{
let (outcome, table) = compare::totals(&report);
println!("{table}");
match outcome {
compare::Outcome::AllValid => {
eprintln!(
"Pricing report {} all totals in the CDR.",
style("matches").green().bold()
);
}
compare::Outcome::SomeInvalid => {
eprintln!(
"Pricing report does {} totals in the CDR.",
style("not match").red().bold()
);
}
}
}
Ok(())
}
}
#[must_use]
fn periods_summary(report: &price::Report, timezone: Tz) -> String {
let price::Report {
periods,
tariff_used,
tariff_reports: _,
timezone: _,
billed_charging_time: _,
billed_energy: _,
billed_parking_time: _,
total_charging_time: _,
total_energy,
total_parking_time,
total_time,
total_cost: _,
total_energy_cost,
total_fixed_cost,
total_parking_cost,
total_reservation_cost: _,
total_time_cost,
} = report;
let currency = tariff_used.currency;
let mut table = Table::header(&[
Col {
label: &style("Period").green(),
width: 25,
},
Col::empty(8),
Col {
label: &style("Energy").green(),
width: 10,
},
Col {
label: &style("Charging Time").green(),
width: 13,
},
Col {
label: &style("Parking Time").green(),
width: 13,
},
Col {
label: &style("Flat").green(),
width: 10,
},
]);
for period in periods {
let start_time = period.start_date_time.with_timezone(&timezone);
let dim = &period.dimensions;
table.print_row(&[
&start_time,
&style("Volume").green(),
&dim.energy.volume.unwrap_or_default(),
&dim.duration_charging.volume.unwrap_or_default(),
&dim.duration_parking.volume.unwrap_or_default(),
&dim.flat
.price
.as_ref()
.map(|_| "x".to_string())
.unwrap_or_default(),
]);
table.print_row(&[
&"",
&style("Price").green(),
&dim.energy
.price
.as_ref()
.map(|p| p.price)
.unwrap_or_default()
.display_currency(currency),
&dim.duration_charging
.price
.as_ref()
.map(|p| p.price)
.unwrap_or_default()
.display_currency(currency),
&dim.duration_parking
.price
.as_ref()
.map(|p| p.price)
.unwrap_or_default()
.display_currency(currency),
&dim.flat
.price
.as_ref()
.map(|p| p.price)
.unwrap_or_default()
.display_currency(currency),
]);
}
table.print_line();
table.print_row(&[
&style("Total").green(),
&style("Volume").green(),
&print::Optional(total_energy.calculated),
&total_time.calculated,
&print::Optional(total_parking_time.calculated),
&"",
]);
table.print_row(&[
&"",
&style("Price").green(),
&total_energy_cost
.calculated
.map(|p| p.excl_vat)
.unwrap_or_default()
.display_currency(currency),
&total_time_cost
.calculated
.map(|p| p.excl_vat)
.unwrap_or_default()
.display_currency(currency),
&total_parking_cost
.calculated
.map(|p| p.excl_vat)
.unwrap_or_default()
.display_currency(currency),
&total_fixed_cost
.calculated
.map(|p| p.excl_vat)
.unwrap_or_default()
.display_currency(currency),
]);
table.finish()
}
mod compare {
use console::style;
use ocpi_tariffs::{price, Kwh};
use crate::print::{self, Col, Table};
#[derive(Copy, Clone)]
pub(super) enum Outcome {
AllValid,
SomeInvalid,
}
#[must_use]
pub(super) fn totals(report: &price::Report) -> (Outcome, String) {
let price::Report {
periods: _,
tariff_used,
tariff_reports: _,
timezone: _,
billed_charging_time: _,
billed_energy: _,
billed_parking_time: _,
total_charging_time: _,
total_energy,
total_parking_time,
total_time,
total_cost,
total_energy_cost,
total_fixed_cost,
total_parking_cost,
total_reservation_cost: _,
total_time_cost,
} = report;
let currency = tariff_used.currency;
let mut all_totals_valid = true;
let mut table = Table::header(&[
Col {
label: &style("Property").green(),
width: 28,
},
Col {
label: &style("Report").green(),
width: 10,
},
Col {
label: &style("CDR").green(),
width: 10,
},
]);
{
let valid = total_time.cdr == total_time.calculated;
table.print_valid_row(
valid,
"Total Time",
&[&total_time.calculated, &total_time.cdr],
);
all_totals_valid &= valid;
}
{
let valid = total_parking_time.cdr == total_parking_time.calculated;
table.print_valid_row(
valid,
"Total Parking Time",
&[
&print::Optional(total_parking_time.calculated),
&total_parking_time.cdr.unwrap_or_default(),
],
);
all_totals_valid &= valid;
}
{
let valid = total_energy
.calculated
.map(|kwh| kwh == total_energy.cdr)
.unwrap_or(true);
table.print_valid_row(
valid,
"Total Energy",
&[
&print::Optional(total_energy.calculated.map(Kwh::rescale)),
&total_energy.cdr,
],
);
all_totals_valid &= valid;
}
{
let valid = total_cost
.calculated
.map(|p| p == total_cost.cdr)
.unwrap_or(true);
table.print_valid_row(
valid,
"Total Cost (Excl.)",
&[
&total_cost
.calculated
.map(|p| p.excl_vat)
.unwrap_or_default()
.display_currency(currency),
&total_cost.cdr.rescale().excl_vat.display_currency(currency),
],
);
table.print_valid_row(
valid,
"Total Cost (Incl.)",
&[
&total_cost
.calculated
.and_then(|p| p.incl_vat)
.unwrap_or_default()
.display_currency(currency),
&total_cost
.cdr
.incl_vat
.unwrap_or_default()
.display_currency(currency),
],
);
all_totals_valid &= valid;
}
{
let valid = total_time_cost
.calculated
.zip(total_time_cost.cdr)
.map(|(l, r)| l == r)
.unwrap_or(true);
table.print_valid_row(
valid,
"Total Time Cost (Excl.)",
&[
&total_time_cost
.calculated
.map(|p| p.rescale().excl_vat)
.unwrap_or_default()
.display_currency(currency),
&total_time_cost
.cdr
.map(|p| p.excl_vat)
.unwrap_or_default()
.display_currency(currency),
],
);
table.print_valid_row(
valid,
"Total Time Cost (Incl.)",
&[
&total_time_cost
.calculated
.and_then(|p| p.rescale().incl_vat)
.unwrap_or_default()
.display_currency(currency),
&total_time_cost
.cdr
.and_then(|p| p.incl_vat)
.unwrap_or_default()
.display_currency(currency),
],
);
all_totals_valid &= valid;
}
{
let valid = total_fixed_cost
.calculated
.zip(total_fixed_cost.cdr)
.map(|(l, r)| l == r)
.unwrap_or(true);
table.print_valid_row(
valid,
"Total Fixed Cost (Excl.)",
&[
&total_fixed_cost
.calculated
.map(|p| p.excl_vat)
.unwrap_or_default()
.display_currency(currency),
&total_fixed_cost
.cdr
.map(|p| p.excl_vat)
.unwrap_or_default()
.display_currency(currency),
],
);
table.print_valid_row(
valid,
"Total Fixed Cost (Incl.)",
&[
&total_fixed_cost
.calculated
.and_then(|p| p.incl_vat)
.unwrap_or_default()
.display_currency(currency),
&total_fixed_cost
.cdr
.and_then(|p| p.incl_vat)
.unwrap_or_default()
.display_currency(currency),
],
);
all_totals_valid &= valid;
}
{
let valid = total_energy_cost
.calculated
.zip(total_energy_cost.cdr)
.map(|(l, r)| l == r)
.unwrap_or(true);
table.print_valid_row(
valid,
"Total Energy Cost (Excl.)",
&[
&total_energy_cost
.calculated
.map(|p| p.excl_vat)
.unwrap_or_default()
.display_currency(currency),
&total_energy_cost
.cdr
.map(|p| p.excl_vat)
.unwrap_or_default()
.display_currency(currency),
],
);
table.print_valid_row(
valid,
"Total Energy Cost (Incl.)",
&[
&total_energy_cost
.calculated
.and_then(|p| p.incl_vat)
.unwrap_or_default()
.display_currency(currency),
&total_energy_cost
.cdr
.and_then(|p| p.incl_vat)
.unwrap_or_default()
.display_currency(currency),
],
);
all_totals_valid &= valid;
}
{
let valid = total_parking_cost
.calculated
.zip(total_parking_cost.cdr)
.map(|(l, r)| l == r)
.unwrap_or(true);
table.print_valid_row(
valid,
"Total Parking Cost (Excl.)",
&[
&total_parking_cost
.calculated
.map(|p| p.excl_vat)
.unwrap_or_default()
.display_currency(currency),
&total_parking_cost
.cdr
.map(|p| p.excl_vat)
.unwrap_or_default()
.display_currency(currency),
],
);
table.print_valid_row(
valid,
"Total Parking Cost (Incl.)",
&[
&total_parking_cost
.calculated
.and_then(|p| p.incl_vat)
.unwrap_or_default()
.display_currency(currency),
&total_parking_cost
.cdr
.and_then(|p| p.incl_vat)
.unwrap_or_default()
.display_currency(currency),
],
);
all_totals_valid &= valid;
}
let outcome = if all_totals_valid {
Outcome::AllValid
} else {
Outcome::SomeInvalid
};
(outcome, table.finish())
}
}