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#[serde(rename_all = "camelCase")]
31pub struct Invoice {
32    /// BO4E metadata
33    #[serde(flatten)]
34    pub meta: Bo4eMeta,
35
36    /// Invoice number (Rechnungsnummer)
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub invoice_number: Option<String>,
39
40    /// Invoice type (Rechnungstyp)
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub invoice_type: Option<InvoiceType>,
43
44    /// Invoice status (Rechnungsstatus)
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub status: Option<InvoiceStatus>,
47
48    /// Energy division (Sparte)
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub division: Option<Division>,
51
52    /// Invoice date (Rechnungsdatum)
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub invoice_date: Option<NaiveDate>,
55
56    /// Due date (Faelligkeitsdatum)
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub due_date: Option<NaiveDate>,
59
60    /// Billing period (Abrechnungszeitraum)
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub billing_period: Option<TimePeriod>,
63
64    /// Net amount (Nettobetrag)
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub net_amount: Option<Amount>,
67
68    /// Tax amount (Steuerbetrag)
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub tax_amount: Option<Amount>,
71
72    /// Gross amount (Bruttobetrag)
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub gross_amount: Option<Amount>,
75
76    /// Invoice line items (Rechnungspositionen)
77    #[serde(default, skip_serializing_if = "Vec::is_empty")]
78    pub positions: Vec<InvoicePosition>,
79
80    /// Invoice recipient (Rechnungsempfaenger)
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub recipient: Option<Box<super::BusinessPartner>>,
83}
84
85impl Bo4eObject for Invoice {
86    fn type_name_german() -> &'static str {
87        "Rechnung"
88    }
89
90    fn type_name_english() -> &'static str {
91        "Invoice"
92    }
93
94    fn meta(&self) -> &Bo4eMeta {
95        &self.meta
96    }
97
98    fn meta_mut(&mut self) -> &mut Bo4eMeta {
99        &mut self.meta
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_invoice_creation() {
109        let invoice = Invoice {
110            invoice_number: Some("RE-2024-001234".to_string()),
111            invoice_type: Some(InvoiceType::EndCustomerInvoice),
112            status: Some(InvoiceStatus::CheckedOk),
113            net_amount: Some(Amount::eur(1000.00)),
114            tax_amount: Some(Amount::eur(190.00)),
115            gross_amount: Some(Amount::eur(1190.00)),
116            ..Default::default()
117        };
118
119        assert_eq!(invoice.invoice_number, Some("RE-2024-001234".to_string()));
120        assert_eq!(invoice.status, Some(InvoiceStatus::CheckedOk));
121    }
122
123    #[test]
124    fn test_invoice_with_positions() {
125        let invoice = Invoice {
126            invoice_number: Some("RE-001".to_string()),
127            positions: vec![
128                InvoicePosition {
129                    position_number: Some(1),
130                    position_text: Some("Electricity consumption".to_string()),
131                    total_price_value: Some(500.0),
132                    ..Default::default()
133                },
134                InvoicePosition {
135                    position_number: Some(2),
136                    position_text: Some("Network fees".to_string()),
137                    total_price_value: Some(100.0),
138                    ..Default::default()
139                },
140            ],
141            ..Default::default()
142        };
143
144        assert_eq!(invoice.positions.len(), 2);
145    }
146
147    #[test]
148    fn test_serialize() {
149        let invoice = Invoice {
150            meta: Bo4eMeta::with_type("Rechnung"),
151            invoice_number: Some("RE-001".to_string()),
152            gross_amount: Some(Amount::eur(119.00)),
153            ..Default::default()
154        };
155
156        let json = serde_json::to_string(&invoice).unwrap();
157        assert!(json.contains(r#""invoiceNumber":"RE-001""#));
158        assert!(json.contains(r#""_typ":"Rechnung""#));
159    }
160
161    #[test]
162    fn test_roundtrip() {
163        let invoice = Invoice {
164            meta: Bo4eMeta::with_type("Rechnung"),
165            invoice_number: Some("RE-123".to_string()),
166            invoice_type: Some(InvoiceType::MonthlyInvoice),
167            status: Some(InvoiceStatus::Paid),
168            division: Some(Division::Electricity),
169            net_amount: Some(Amount::eur(100.0)),
170            ..Default::default()
171        };
172
173        let json = serde_json::to_string(&invoice).unwrap();
174        let parsed: Invoice = serde_json::from_str(&json).unwrap();
175        assert_eq!(invoice, parsed);
176    }
177
178    #[test]
179    fn test_bo4e_object_impl() {
180        assert_eq!(Invoice::type_name_german(), "Rechnung");
181        assert_eq!(Invoice::type_name_english(), "Invoice");
182    }
183}