Skip to main content

bo4e_core/bo/
invoice.rs

1//! Invoice (Rechnung) business object.
2
3use chrono::NaiveDate;
4use serde::{Deserialize, Serialize};
5
6use crate::com::{Amount, InvoicePosition, TimePeriod};
7use crate::enums::{Division, InvoiceStatus, InvoiceType};
8use crate::traits::{Bo4eMeta, Bo4eObject};
9
10/// An invoice for energy services.
11///
12/// German: Rechnung
13///
14/// # Example
15///
16/// ```rust
17/// use bo4e_core::bo::Invoice;
18/// use bo4e_core::com::Amount;
19/// use bo4e_core::enums::{InvoiceStatus, InvoiceType};
20///
21/// let invoice = Invoice {
22///     invoice_number: Some("RE-2024-001234".to_string()),
23///     invoice_type: Some(InvoiceType::EndCustomerInvoice),
24///     status: Some(InvoiceStatus::CheckedOk),
25///     gross_amount: Some(Amount::eur(1190.00)),
26///     ..Default::default()
27/// };
28/// ```
29#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
30#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
31#[cfg_attr(feature = "json-schema", schemars(rename = "Rechnung"))]
32#[serde(rename_all = "camelCase")]
33pub struct Invoice {
34    /// BO4E metadata
35    #[serde(flatten)]
36    pub meta: Bo4eMeta,
37
38    /// Invoice number (Rechnungsnummer)
39    #[serde(skip_serializing_if = "Option::is_none")]
40    #[cfg_attr(feature = "json-schema", schemars(rename = "rechnungsnummer"))]
41    pub invoice_number: Option<String>,
42
43    /// Invoice type (Rechnungstyp)
44    #[serde(skip_serializing_if = "Option::is_none")]
45    #[cfg_attr(feature = "json-schema", schemars(rename = "rechnungstyp"))]
46    pub invoice_type: Option<InvoiceType>,
47
48    /// Invoice status (Rechnungsstatus)
49    #[serde(skip_serializing_if = "Option::is_none")]
50    #[cfg_attr(feature = "json-schema", schemars(rename = "rechnungsstatus"))]
51    pub status: Option<InvoiceStatus>,
52
53    /// Energy division (Sparte)
54    #[serde(skip_serializing_if = "Option::is_none")]
55    #[cfg_attr(feature = "json-schema", schemars(rename = "sparte"))]
56    pub division: Option<Division>,
57
58    /// Invoice date (Rechnungsdatum)
59    #[serde(skip_serializing_if = "Option::is_none")]
60    #[cfg_attr(feature = "json-schema", schemars(rename = "rechnungsdatum"))]
61    pub invoice_date: Option<NaiveDate>,
62
63    /// Due date (Faelligkeitsdatum)
64    #[serde(skip_serializing_if = "Option::is_none")]
65    #[cfg_attr(feature = "json-schema", schemars(rename = "faelligkeitsdatum"))]
66    pub due_date: Option<NaiveDate>,
67
68    /// Billing period (Abrechnungszeitraum)
69    #[serde(skip_serializing_if = "Option::is_none")]
70    #[cfg_attr(feature = "json-schema", schemars(rename = "abrechnungszeitraum"))]
71    pub billing_period: Option<TimePeriod>,
72
73    /// Net amount (Nettobetrag)
74    #[serde(skip_serializing_if = "Option::is_none")]
75    #[cfg_attr(feature = "json-schema", schemars(rename = "nettobetrag"))]
76    pub net_amount: Option<Amount>,
77
78    /// Tax amount (Steuerbetrag)
79    #[serde(skip_serializing_if = "Option::is_none")]
80    #[cfg_attr(feature = "json-schema", schemars(rename = "steuerbetrag"))]
81    pub tax_amount: Option<Amount>,
82
83    /// Gross amount (Bruttobetrag)
84    #[serde(skip_serializing_if = "Option::is_none")]
85    #[cfg_attr(feature = "json-schema", schemars(rename = "bruttobetrag"))]
86    pub gross_amount: Option<Amount>,
87
88    /// Invoice line items (Rechnungspositionen)
89    #[serde(default, skip_serializing_if = "Vec::is_empty")]
90    #[cfg_attr(feature = "json-schema", schemars(rename = "rechnungspositionen"))]
91    pub positions: Vec<InvoicePosition>,
92
93    /// Invoice recipient (Rechnungsempfaenger)
94    #[serde(skip_serializing_if = "Option::is_none")]
95    #[cfg_attr(feature = "json-schema", schemars(rename = "rechnungsempfaenger"))]
96    pub recipient: Option<Box<super::BusinessPartner>>,
97}
98
99impl Bo4eObject for Invoice {
100    fn type_name_german() -> &'static str {
101        "Rechnung"
102    }
103
104    fn type_name_english() -> &'static str {
105        "Invoice"
106    }
107
108    fn meta(&self) -> &Bo4eMeta {
109        &self.meta
110    }
111
112    fn meta_mut(&mut self) -> &mut Bo4eMeta {
113        &mut self.meta
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_invoice_creation() {
123        let invoice = Invoice {
124            invoice_number: Some("RE-2024-001234".to_string()),
125            invoice_type: Some(InvoiceType::EndCustomerInvoice),
126            status: Some(InvoiceStatus::CheckedOk),
127            net_amount: Some(Amount::eur(1000.00)),
128            tax_amount: Some(Amount::eur(190.00)),
129            gross_amount: Some(Amount::eur(1190.00)),
130            ..Default::default()
131        };
132
133        assert_eq!(invoice.invoice_number, Some("RE-2024-001234".to_string()));
134        assert_eq!(invoice.status, Some(InvoiceStatus::CheckedOk));
135    }
136
137    #[test]
138    fn test_invoice_with_positions() {
139        let invoice = Invoice {
140            invoice_number: Some("RE-001".to_string()),
141            positions: vec![
142                InvoicePosition {
143                    position_number: Some(1),
144                    position_text: Some("Electricity consumption".to_string()),
145                    total_price_value: Some(500.0),
146                    ..Default::default()
147                },
148                InvoicePosition {
149                    position_number: Some(2),
150                    position_text: Some("Network fees".to_string()),
151                    total_price_value: Some(100.0),
152                    ..Default::default()
153                },
154            ],
155            ..Default::default()
156        };
157
158        assert_eq!(invoice.positions.len(), 2);
159    }
160
161    #[test]
162    fn test_serialize() {
163        let invoice = Invoice {
164            meta: Bo4eMeta::with_type("Rechnung"),
165            invoice_number: Some("RE-001".to_string()),
166            gross_amount: Some(Amount::eur(119.00)),
167            ..Default::default()
168        };
169
170        let json = serde_json::to_string(&invoice).unwrap();
171        assert!(json.contains(r#""invoiceNumber":"RE-001""#));
172        assert!(json.contains(r#""_typ":"Rechnung""#));
173    }
174
175    #[test]
176    fn test_roundtrip() {
177        let invoice = Invoice {
178            meta: Bo4eMeta::with_type("Rechnung"),
179            invoice_number: Some("RE-123".to_string()),
180            invoice_type: Some(InvoiceType::MonthlyInvoice),
181            status: Some(InvoiceStatus::Paid),
182            division: Some(Division::Electricity),
183            net_amount: Some(Amount::eur(100.0)),
184            ..Default::default()
185        };
186
187        let json = serde_json::to_string(&invoice).unwrap();
188        let parsed: Invoice = serde_json::from_str(&json).unwrap();
189        assert_eq!(invoice, parsed);
190    }
191
192    #[test]
193    fn test_bo4e_object_impl() {
194        assert_eq!(Invoice::type_name_german(), "Rechnung");
195        assert_eq!(Invoice::type_name_english(), "Invoice");
196    }
197}