#[cfg(test)]
mod test;
#[cfg(test)]
mod test_price;
use std::fmt;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use crate::{
currency, from_warning_all, impl_dec_newtype,
json::{self, FieldsAsExt as _},
number::{self, approx_eq_dec, FromDecimal as _, IsZero, RoundDecimal},
warning::{self, GatherWarnings as _, IntoCaveat},
SaturatingAdd as _, Verdict,
};
pub trait Cost: Copy {
fn cost(&self, money: Money) -> Money;
}
impl Cost for () {
fn cost(&self, money: Money) -> Money {
money
}
}
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
ExclusiveVatGreaterThanInclusive,
InvalidType { type_found: json::ValueKind },
MissingExclVatField,
Number(number::Warning),
}
impl Warning {
fn invalid_type(elem: &json::Element<'_>) -> Self {
Self::InvalidType {
type_found: elem.value().kind(),
}
}
}
impl fmt::Display for Warning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ExclusiveVatGreaterThanInclusive => write!(
f,
"The `excl_vat` field is greater than the `incl_vat` field"
),
Self::InvalidType { type_found } => {
write!(f, "The value should be an object but is `{type_found}`")
}
Self::MissingExclVatField => write!(f, "The `excl_vat` field is required."),
Self::Number(kind) => fmt::Display::fmt(kind, f),
}
}
}
impl crate::Warning for Warning {
fn id(&self) -> warning::Id {
match self {
Self::ExclusiveVatGreaterThanInclusive => {
warning::Id::from_static("exclusive_vat_greater_than_inclusive")
}
Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
Self::MissingExclVatField => warning::Id::from_static("missing_excl_vat_field"),
Self::Number(kind) => kind.id(),
}
}
}
from_warning_all!(number::Warning => Warning::Number);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
#[cfg_attr(test, derive(serde::Deserialize))]
pub struct Price {
pub excl_vat: Money,
#[cfg_attr(test, serde(default))]
pub incl_vat: Option<Money>,
}
impl RoundDecimal for Price {
fn round_to_ocpi_scale(self) -> Self {
let Self { excl_vat, incl_vat } = self;
Self {
excl_vat: excl_vat.round_to_ocpi_scale(),
incl_vat: incl_vat.round_to_ocpi_scale(),
}
}
}
impl fmt::Display for Price {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(incl_vat) = self.incl_vat {
if f.alternate() {
write!(f, "{{ -vat: {:#}, +vat: {:#} }}", self.excl_vat, incl_vat)
} else {
write!(f, "{{ -vat: {}, +vat: {} }}", self.excl_vat, incl_vat)
}
} else {
fmt::Display::fmt(&self.excl_vat, f)
}
}
}
impl json::FromJson<'_> for Price {
type Warning = Warning;
fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::Warning> {
let mut warnings = warning::Set::new();
let value = elem.as_value();
let Some(fields) = value.as_object_fields() else {
return warnings.bail(Warning::invalid_type(elem), elem);
};
let Some(excl_vat) = fields.find_field("excl_vat") else {
return warnings.bail(Warning::MissingExclVatField, elem);
};
let excl_vat = Money::from_json(excl_vat.element())?.gather_warnings_into(&mut warnings);
let incl_vat = fields
.find_field("incl_vat")
.map(|f| Money::from_json(f.element()))
.transpose()?
.gather_warnings_into(&mut warnings);
if let Some(incl_vat) = incl_vat {
if excl_vat > incl_vat {
warnings.insert(Warning::ExclusiveVatGreaterThanInclusive, elem);
}
}
Ok(Self { excl_vat, incl_vat }.into_caveat(warnings))
}
}
impl IsZero for Price {
fn is_zero(&self) -> bool {
self.excl_vat.is_zero() && self.incl_vat.is_none_or(|v| v.is_zero())
}
}
impl Price {
pub fn zero() -> Self {
Self {
excl_vat: Money::zero(),
incl_vat: Some(Money::zero()),
}
}
#[must_use]
pub fn rescale(self) -> Self {
Self {
excl_vat: self.excl_vat.rescale(),
incl_vat: self.incl_vat.map(Money::rescale),
}
}
#[must_use]
pub(crate) fn saturating_add(self, rhs: Self) -> Self {
let incl_vat = self
.incl_vat
.zip(rhs.incl_vat)
.map(|(lhs, rhs)| lhs.saturating_add(rhs));
Self {
excl_vat: self.excl_vat.saturating_add(rhs.excl_vat),
incl_vat,
}
}
#[must_use]
pub fn round_dp(self, digits: u32) -> Self {
Self {
excl_vat: self.excl_vat.round_dp(digits),
incl_vat: self.incl_vat.map(|v| v.round_dp(digits)),
}
}
pub fn display_currency(&self, currency: currency::Code) -> DisplayPriceCurrency<'_> {
DisplayPriceCurrency {
currency,
price: self,
}
}
}
pub struct DisplayPriceCurrency<'a> {
currency: currency::Code,
price: &'a Price,
}
impl fmt::Display for DisplayPriceCurrency<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(incl_vat) = self.price.incl_vat {
write!(
f,
"{{ -vat: {:#}, +vat: {:#} }}",
self.price.excl_vat, incl_vat
)
} else {
fmt::Display::fmt(&self.price.excl_vat.display_currency(self.currency), f)
}
}
}
impl Default for Price {
fn default() -> Self {
Self::zero()
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
#[cfg_attr(test, derive(serde::Deserialize))]
pub struct Money(Decimal);
impl_dec_newtype!(Money, "ยค");
impl IsZero for Money {
fn is_zero(&self) -> bool {
const TOLERANCE: Decimal = dec!(0.01);
approx_eq_dec(&self.0, &Decimal::ZERO, TOLERANCE)
}
}
impl Money {
#[must_use]
pub(crate) const fn zero() -> Self {
Self(Decimal::ZERO)
}
#[must_use]
pub fn apply_vat(self, vat: Vat) -> Self {
const ONE: Decimal = dec!(1);
let x = vat.as_unit_interval().saturating_add(ONE);
Self(self.0.saturating_mul(x))
}
pub fn display_currency(&self, currency: currency::Code) -> DisplayCurrency<'_> {
DisplayCurrency {
currency,
money: self,
}
}
}
pub struct DisplayCurrency<'a> {
currency: currency::Code,
money: &'a Money,
}
impl fmt::Display for DisplayCurrency<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{:#}", self.currency.into_symbol(), self.money)
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct Vat(Decimal);
impl_dec_newtype!(Vat, "%");
impl Vat {
#[expect(clippy::missing_panics_doc, reason = "The divisor is non-zero")]
pub fn as_unit_interval(self) -> Decimal {
const PERCENT: Decimal = dec!(100);
self.0.checked_div(PERCENT).expect("divisor is non-zero")
}
}
#[derive(Clone, Copy, Debug)]
pub enum VatApplicable {
Unknown,
Inapplicable,
Applicable(Vat),
}
impl json::FromJson<'_> for VatApplicable {
type Warning = number::Warning;
fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::Warning> {
let vat = Decimal::from_json(elem)?;
Ok(vat.map(|d| Self::Applicable(Vat::from_decimal(d))))
}
}