Skip to main content

bo4e_core/com/
surcharge.rs

1//! Surcharge/discount (AufAbschlag) component.
2
3use serde::{Deserialize, Serialize};
4
5use crate::enums::{Currency, SurchargeTarget, SurchargeType};
6use crate::traits::{Bo4eMeta, Bo4eObject};
7
8use super::PriceTier;
9
10/// A surcharge or discount applied to a price.
11///
12/// German: AufAbschlag
13///
14/// # Example
15///
16/// ```rust
17/// use bo4e_core::com::Surcharge;
18///
19/// let surcharge = Surcharge {
20///     description: Some("Ökosteuer".to_string()),
21///     value: Some(0.02),
22///     ..Default::default()
23/// };
24/// ```
25#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
26#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
27#[cfg_attr(feature = "json-schema", schemars(rename = "AufAbschlag"))]
28#[serde(rename_all = "camelCase")]
29pub struct Surcharge {
30    /// BO4E metadata
31    #[serde(flatten)]
32    pub meta: Bo4eMeta,
33
34    /// Name/designation of the surcharge (Bezeichnung)
35    #[serde(skip_serializing_if = "Option::is_none")]
36    #[cfg_attr(feature = "json-schema", schemars(rename = "bezeichnung"))]
37    pub description: Option<String>,
38
39    /// Type of surcharge (AufAbschlagstyp)
40    #[serde(skip_serializing_if = "Option::is_none")]
41    #[cfg_attr(feature = "json-schema", schemars(rename = "aufAbschlagstyp"))]
42    pub surcharge_type: Option<SurchargeType>,
43
44    /// Value of the surcharge (Wert)
45    /// Positive = surcharge, negative = discount
46    #[serde(skip_serializing_if = "Option::is_none")]
47    #[cfg_attr(feature = "json-schema", schemars(rename = "wert"))]
48    pub value: Option<f64>,
49
50    /// Currency unit for absolute surcharges (Waehrungseinheit)
51    #[serde(skip_serializing_if = "Option::is_none")]
52    #[cfg_attr(feature = "json-schema", schemars(rename = "waehrungseinheit"))]
53    pub currency: Option<Currency>,
54
55    /// Target price/cost category (AufAbschlagsziel)
56    #[serde(skip_serializing_if = "Option::is_none")]
57    #[cfg_attr(feature = "json-schema", schemars(rename = "aufAbschlagsziel"))]
58    pub target: Option<SurchargeTarget>,
59
60    /// Tiered surcharge values (Staffeln)
61    #[serde(default, skip_serializing_if = "Vec::is_empty")]
62    #[cfg_attr(feature = "json-schema", schemars(rename = "staffeln"))]
63    pub tiers: Vec<PriceTier>,
64
65    /// Additional description (Beschreibung)
66    #[serde(skip_serializing_if = "Option::is_none")]
67    #[cfg_attr(feature = "json-schema", schemars(rename = "beschreibung"))]
68    pub details: Option<String>,
69
70    /// Website for published information (Website)
71    #[serde(skip_serializing_if = "Option::is_none")]
72    #[cfg_attr(feature = "json-schema", schemars(rename = "website"))]
73    pub website: Option<String>,
74}
75
76impl Bo4eObject for Surcharge {
77    fn type_name_german() -> &'static str {
78        "AufAbschlag"
79    }
80
81    fn type_name_english() -> &'static str {
82        "Surcharge"
83    }
84
85    fn meta(&self) -> &Bo4eMeta {
86        &self.meta
87    }
88
89    fn meta_mut(&mut self) -> &mut Bo4eMeta {
90        &mut self.meta
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_surcharge() {
100        let surcharge = Surcharge {
101            description: Some("Ökosteuer".to_string()),
102            value: Some(0.02),
103            surcharge_type: Some(SurchargeType::Absolute),
104            currency: Some(Currency::Eur),
105            ..Default::default()
106        };
107
108        let json = serde_json::to_string(&surcharge).unwrap();
109        let parsed: Surcharge = serde_json::from_str(&json).unwrap();
110        assert_eq!(surcharge, parsed);
111    }
112
113    #[test]
114    fn test_discount() {
115        let discount = Surcharge {
116            description: Some("Neukunden-Rabatt".to_string()),
117            value: Some(-50.0),
118            surcharge_type: Some(SurchargeType::Absolute),
119            currency: Some(Currency::Eur),
120            ..Default::default()
121        };
122
123        assert!(discount.value.unwrap() < 0.0);
124    }
125
126    #[test]
127    fn test_percentage_surcharge() {
128        let surcharge = Surcharge {
129            description: Some("MwSt".to_string()),
130            value: Some(19.0),
131            surcharge_type: Some(SurchargeType::Relative),
132            ..Default::default()
133        };
134
135        assert_eq!(surcharge.surcharge_type, Some(SurchargeType::Relative));
136    }
137
138    #[test]
139    fn test_tiered_surcharge() {
140        let surcharge = Surcharge {
141            description: Some("Staffelrabatt".to_string()),
142            tiers: vec![
143                PriceTier {
144                    lower_limit: Some(0.0),
145                    upper_limit: Some(1000.0),
146                    unit_price: Some(0.05),
147                    ..Default::default()
148                },
149                PriceTier {
150                    lower_limit: Some(1000.0),
151                    upper_limit: Some(5000.0),
152                    unit_price: Some(0.03),
153                    ..Default::default()
154                },
155            ],
156            ..Default::default()
157        };
158
159        assert_eq!(surcharge.tiers.len(), 2);
160    }
161
162    #[test]
163    fn test_default() {
164        let surcharge = Surcharge::default();
165        assert!(surcharge.description.is_none());
166        assert!(surcharge.value.is_none());
167        assert!(surcharge.tiers.is_empty());
168    }
169
170    #[test]
171    fn test_bo4e_object_impl() {
172        assert_eq!(Surcharge::type_name_german(), "AufAbschlag");
173        assert_eq!(Surcharge::type_name_english(), "Surcharge");
174    }
175}