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