use rust_decimal::{Decimal, dec};
use crate::error::BillingError;
pub use ::billing::TaxCategory;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Leistungsart {
SonstigeLeistung,
LieferungStrom,
LieferungGas,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Wiederverkaeuferstatus {
pub leistender: bool,
pub empfaenger: bool,
}
impl Wiederverkaeuferstatus {
pub const KEINER: Self = Self {
leistender: false,
empfaenger: false,
};
pub const BEIDE: Self = Self {
leistender: true,
empfaenger: true,
};
#[must_use]
pub const fn verlagert(self, art: Leistungsart) -> bool {
match art {
Leistungsart::SonstigeLeistung => false,
Leistungsart::LieferungStrom => self.leistender && self.empfaenger,
Leistungsart::LieferungGas => self.empfaenger,
}
}
}
pub const HINWEIS_REVERSE_CHARGE: &str = "Steuerschuldnerschaft des Leistungsempfängers";
pub const REGELSTEUERSATZ: Decimal = dec!(19);
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct Steuerausweis {
pub kategorie: TaxCategory,
pub satz_prozent: Decimal,
pub bemessungsgrundlage_eur: Decimal,
pub steuer_eur: Decimal,
pub hinweis: Option<&'static str>,
pub rechtsgrundlage: &'static str,
}
impl Steuerausweis {
#[must_use]
pub fn brutto_eur(&self) -> Decimal {
self.bemessungsgrundlage_eur + self.steuer_eur
}
}
#[must_use]
pub fn regelsatz_prozent(art: Leistungsart, from: time::Date, to: time::Date) -> Option<Decimal> {
type Fenster = (time::Date, time::Date, Decimal);
const COVID: Fenster = (
time::macros::date!(2020 - 07 - 01),
time::macros::date!(2020 - 12 - 31),
dec!(16),
);
const GAS: Fenster = (
time::macros::date!(2022 - 10 - 01),
time::macros::date!(2024 - 03 - 31),
dec!(7),
);
let fenster: &[Fenster] = match art {
Leistungsart::LieferungGas => &[COVID, GAS],
Leistungsart::SonstigeLeistung | Leistungsart::LieferungStrom => &[COVID],
};
for (von, bis, satz) in fenster {
if from >= *von && to <= *bis {
return Some(*satz);
}
if from <= *bis && to >= *von {
return None;
}
}
Some(REGELSTEUERSATZ)
}
pub fn steuerausweis(
netto_eur: Decimal,
art: Leistungsart,
status: Wiederverkaeuferstatus,
period: crate::SettlementPeriod,
) -> Result<Steuerausweis, BillingError> {
if status.verlagert(art) {
return Ok(Steuerausweis {
kategorie: TaxCategory::ReverseCharge,
satz_prozent: Decimal::ZERO,
bemessungsgrundlage_eur: netto_eur,
steuer_eur: Decimal::ZERO,
hinweis: Some(HINWEIS_REVERSE_CHARGE),
rechtsgrundlage: match art {
Leistungsart::LieferungStrom | Leistungsart::LieferungGas => {
"§13b Abs. 2 Nr. 5 Buchst. b UStG"
}
Leistungsart::SonstigeLeistung => unreachable!("never shifted"),
},
});
}
let satz = regelsatz_prozent(art, period.from(), period.to()).ok_or_else(|| {
BillingError::InvalidInput {
reason: format!(
"the delivery period {} – {} straddles an Umsatzsteuer rate change; split it \
at the Stichtag and settle each part, rather than billing the whole period \
at one rate",
period.from(),
period.to()
),
}
})?;
Ok(Steuerausweis {
kategorie: TaxCategory::Standard,
satz_prozent: satz,
bemessungsgrundlage_eur: netto_eur,
steuer_eur: (netto_eur * satz / dec!(100))
.round_dp_with_strategy(2, rust_decimal::RoundingStrategy::MidpointAwayFromZero),
hinweis: None,
rechtsgrundlage: "§12 Abs. 1 UStG",
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SettlementPeriod;
use time::macros::date;
fn period(from: time::Date, to: time::Date) -> SettlementPeriod {
SettlementPeriod::new(from, to).expect("ordered")
}
#[test]
fn a_network_service_is_never_reverse_charged() {
for status in [
Wiederverkaeuferstatus::KEINER,
Wiederverkaeuferstatus::BEIDE,
] {
assert!(!status.verlagert(Leistungsart::SonstigeLeistung));
}
}
#[test]
fn the_sect13b_condition_is_asymmetric_between_the_sparten() {
let nur_empfaenger = Wiederverkaeuferstatus {
leistender: false,
empfaenger: true,
};
let nur_leistender = Wiederverkaeuferstatus {
leistender: true,
empfaenger: false,
};
assert!(!nur_empfaenger.verlagert(Leistungsart::LieferungStrom));
assert!(!nur_leistender.verlagert(Leistungsart::LieferungStrom));
assert!(Wiederverkaeuferstatus::BEIDE.verlagert(Leistungsart::LieferungStrom));
assert!(nur_empfaenger.verlagert(Leistungsart::LieferungGas));
assert!(!nur_leistender.verlagert(Leistungsart::LieferungGas));
assert!(Wiederverkaeuferstatus::BEIDE.verlagert(Leistungsart::LieferungGas));
}
#[test]
fn a_reverse_charge_states_no_tax_and_says_why() {
let s = steuerausweis(
dec!(1000),
Leistungsart::LieferungStrom,
Wiederverkaeuferstatus::BEIDE,
period(date!(2026 - 01 - 01), date!(2026 - 01 - 31)),
)
.expect("computable");
assert_eq!(s.kategorie, TaxCategory::ReverseCharge);
assert_eq!(s.steuer_eur, Decimal::ZERO);
assert_eq!(s.satz_prozent, Decimal::ZERO);
assert_eq!(s.hinweis, Some(HINWEIS_REVERSE_CHARGE));
assert_eq!(s.brutto_eur(), dec!(1000));
}
#[test]
fn an_ordinary_supply_is_taxed_at_the_regelsteuersatz() {
let s = steuerausweis(
dec!(1234.56),
Leistungsart::SonstigeLeistung,
Wiederverkaeuferstatus::KEINER,
period(date!(2026 - 01 - 01), date!(2026 - 01 - 31)),
)
.expect("computable");
assert_eq!(s.kategorie, TaxCategory::Standard);
assert_eq!(s.satz_prozent, dec!(19));
assert_eq!(s.steuer_eur, dec!(234.57), "1234.56 × 19 % = 234.5664");
assert_eq!(s.brutto_eur(), dec!(1469.13));
assert_eq!(s.hinweis, None);
}
#[test]
fn the_gas_reduction_reached_the_commodity_not_the_network() {
let im_fenster = (date!(2023 - 01 - 01), date!(2023 - 01 - 31));
assert_eq!(
regelsatz_prozent(Leistungsart::LieferungGas, im_fenster.0, im_fenster.1),
Some(dec!(7))
);
assert_eq!(
regelsatz_prozent(Leistungsart::SonstigeLeistung, im_fenster.0, im_fenster.1),
Some(dec!(19)),
"Netznutzung Gas is a service — §28 Abs. 5 UStG reduced Lieferungen"
);
assert_eq!(
regelsatz_prozent(Leistungsart::LieferungStrom, im_fenster.0, im_fenster.1),
Some(dec!(19)),
"the reduction was for gas and Fernwärme, never for electricity"
);
}
#[test]
fn the_covid_reduction_reached_every_supply() {
let im_fenster = (date!(2020 - 08 - 01), date!(2020 - 08 - 31));
for art in [
Leistungsart::SonstigeLeistung,
Leistungsart::LieferungStrom,
Leistungsart::LieferungGas,
] {
assert_eq!(
regelsatz_prozent(art, im_fenster.0, im_fenster.1),
Some(dec!(16))
);
}
}
#[test]
fn a_period_spanning_a_rate_change_is_refused() {
assert_eq!(
regelsatz_prozent(
Leistungsart::LieferungGas,
date!(2024 - 03 - 01),
date!(2024 - 04 - 30)
),
None
);
let refused = steuerausweis(
dec!(1000),
Leistungsart::LieferungGas,
Wiederverkaeuferstatus::KEINER,
period(date!(2024 - 03 - 01), date!(2024 - 04 - 30)),
);
assert!(refused.is_err(), "{refused:?}");
assert_eq!(
regelsatz_prozent(
Leistungsart::LieferungGas,
date!(2024 - 03 - 01),
date!(2024 - 03 - 31)
),
Some(dec!(7))
);
assert_eq!(
regelsatz_prozent(
Leistungsart::LieferungGas,
date!(2024 - 04 - 01),
date!(2024 - 04 - 30)
),
Some(dec!(19))
);
}
#[test]
fn a_credit_carries_its_tax_with_the_same_sign() {
let s = steuerausweis(
dec!(-500),
Leistungsart::LieferungStrom,
Wiederverkaeuferstatus::KEINER,
period(date!(2026 - 01 - 01), date!(2026 - 01 - 31)),
)
.expect("computable");
assert_eq!(s.steuer_eur, dec!(-95.00));
assert_eq!(s.brutto_eur(), dec!(-595.00));
}
}