Skip to main content

bo4e_core/com/
tax_amount.rs

1//! Tax amount (Steuerbetrag) component.
2
3use serde::{Deserialize, Serialize};
4
5use crate::enums::{Currency, TaxType};
6use crate::traits::{Bo4eMeta, Bo4eObject};
7
8/// A calculated tax amount.
9///
10/// German: Steuerbetrag
11///
12/// # Example
13///
14/// ```rust
15/// use bo4e_core::com::TaxAmount;
16/// use bo4e_core::enums::{Currency, TaxType};
17///
18/// let tax = TaxAmount {
19///     tax_type: Some(TaxType::ValueAddedTax),
20///     tax_rate: Some(19.0),
21///     basis_value: Some(100.0),
22///     tax_value: Some(19.0),
23///     currency: Some(Currency::Eur),
24///     ..Default::default()
25/// };
26/// ```
27#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
28#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
29#[cfg_attr(feature = "json-schema", schemars(rename = "Steuerbetrag"))]
30#[serde(rename_all = "camelCase")]
31pub struct TaxAmount {
32    /// BO4E metadata
33    #[serde(flatten)]
34    pub meta: Bo4eMeta,
35
36    /// Type of tax (Steuerart)
37    #[serde(skip_serializing_if = "Option::is_none")]
38    #[cfg_attr(feature = "json-schema", schemars(rename = "steuerart"))]
39    pub tax_type: Option<TaxType>,
40
41    /// Tax rate as percentage (Steuersatz)
42    #[serde(skip_serializing_if = "Option::is_none")]
43    #[cfg_attr(feature = "json-schema", schemars(rename = "steuersatz"))]
44    pub tax_rate: Option<f64>,
45
46    /// Net amount on which tax was calculated (Basiswert)
47    #[serde(skip_serializing_if = "Option::is_none")]
48    #[cfg_attr(feature = "json-schema", schemars(rename = "basiswert"))]
49    pub basis_value: Option<f64>,
50
51    /// Calculated tax amount (Steuerwert)
52    #[serde(skip_serializing_if = "Option::is_none")]
53    #[cfg_attr(feature = "json-schema", schemars(rename = "steuerwert"))]
54    pub tax_value: Option<f64>,
55
56    /// Currency (Waehrungscode)
57    #[serde(skip_serializing_if = "Option::is_none")]
58    #[cfg_attr(feature = "json-schema", schemars(rename = "waehrungscode"))]
59    pub currency: Option<Currency>,
60}
61
62impl Bo4eObject for TaxAmount {
63    fn type_name_german() -> &'static str {
64        "Steuerbetrag"
65    }
66
67    fn type_name_english() -> &'static str {
68        "TaxAmount"
69    }
70
71    fn meta(&self) -> &Bo4eMeta {
72        &self.meta
73    }
74
75    fn meta_mut(&mut self) -> &mut Bo4eMeta {
76        &mut self.meta
77    }
78}
79
80impl TaxAmount {
81    /// Calculate VAT 19% on a net amount.
82    pub fn vat_19(net_amount: f64) -> Self {
83        Self {
84            tax_type: Some(TaxType::ValueAddedTax),
85            tax_rate: Some(19.0),
86            basis_value: Some(net_amount),
87            tax_value: Some(net_amount * 0.19),
88            currency: Some(Currency::Eur),
89            ..Default::default()
90        }
91    }
92
93    /// Calculate VAT 7% on a net amount.
94    pub fn vat_7(net_amount: f64) -> Self {
95        Self {
96            tax_type: Some(TaxType::ValueAddedTax),
97            tax_rate: Some(7.0),
98            basis_value: Some(net_amount),
99            tax_value: Some(net_amount * 0.07),
100            currency: Some(Currency::Eur),
101            ..Default::default()
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_vat_19() {
112        let tax = TaxAmount::vat_19(100.0);
113        assert_eq!(tax.tax_type, Some(TaxType::ValueAddedTax));
114        assert_eq!(tax.tax_rate, Some(19.0));
115        assert_eq!(tax.basis_value, Some(100.0));
116        assert_eq!(tax.tax_value, Some(19.0));
117    }
118
119    #[test]
120    fn test_vat_7() {
121        let tax = TaxAmount::vat_7(100.0);
122        assert_eq!(tax.tax_type, Some(TaxType::ValueAddedTax));
123        assert_eq!(tax.tax_rate, Some(7.0));
124        assert_eq!(tax.basis_value, Some(100.0));
125        // Use approximate comparison due to floating point precision
126        assert!((tax.tax_value.unwrap() - 7.0).abs() < 0.0001);
127    }
128
129    #[test]
130    fn test_default() {
131        let tax = TaxAmount::default();
132        assert!(tax.tax_type.is_none());
133        assert!(tax.tax_rate.is_none());
134        assert!(tax.basis_value.is_none());
135        assert!(tax.tax_value.is_none());
136    }
137
138    #[test]
139    fn test_serialize() {
140        let tax = TaxAmount::vat_19(250.0);
141        let json = serde_json::to_string(&tax).unwrap();
142        assert!(json.contains(r#""taxRate":19"#));
143        assert!(json.contains(r#""basisValue":250"#));
144    }
145
146    #[test]
147    fn test_roundtrip() {
148        let tax = TaxAmount {
149            tax_type: Some(TaxType::ValueAddedTax),
150            tax_rate: Some(19.0),
151            basis_value: Some(123.45),
152            tax_value: Some(23.4555),
153            currency: Some(Currency::Eur),
154            ..Default::default()
155        };
156
157        let json = serde_json::to_string(&tax).unwrap();
158        let parsed: TaxAmount = serde_json::from_str(&json).unwrap();
159        assert_eq!(tax, parsed);
160    }
161
162    #[test]
163    fn test_bo4e_object_impl() {
164        assert_eq!(TaxAmount::type_name_german(), "Steuerbetrag");
165        assert_eq!(TaxAmount::type_name_english(), "TaxAmount");
166    }
167}