1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct TaxCategory {
12 pub id: String,
14
15 pub percent: f64,
17
18 #[serde(rename = "taxScheme")]
20 pub tax_scheme: String,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct LineItem {
26 pub id: String,
28
29 pub description: String,
31
32 pub quantity: f64,
34
35 #[serde(rename = "unitCode", skip_serializing_if = "Option::is_none")]
37 pub unit_code: Option<String>,
38
39 #[serde(rename = "unitPrice")]
41 pub unit_price: f64,
42
43 #[serde(rename = "lineTotal")]
45 pub line_total: f64,
46
47 #[serde(rename = "taxCategory", skip_serializing_if = "Option::is_none")]
49 pub tax_category: Option<TaxCategory>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct TaxSubtotal {
55 #[serde(rename = "taxableAmount")]
57 pub taxable_amount: f64,
58
59 #[serde(rename = "taxAmount")]
61 pub tax_amount: f64,
62
63 #[serde(rename = "taxCategory")]
65 pub tax_category: TaxCategory,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct TaxTotal {
71 #[serde(rename = "taxAmount")]
73 pub tax_amount: f64,
74
75 #[serde(rename = "taxSubtotal", skip_serializing_if = "Option::is_none")]
77 pub tax_subtotal: Option<Vec<TaxSubtotal>>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct OrderReference {
83 pub id: String,
85
86 #[serde(rename = "issueDate", skip_serializing_if = "Option::is_none")]
88 pub issue_date: Option<String>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct DocumentReference {
94 pub id: String,
96
97 #[serde(rename = "documentType", skip_serializing_if = "Option::is_none")]
99 pub document_type: Option<String>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub url: Option<String>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Invoice {
109 pub id: String,
111
112 #[serde(rename = "issueDate")]
114 pub issue_date: String,
115
116 #[serde(rename = "currencyCode")]
118 pub currency_code: String,
119
120 #[serde(rename = "lineItems")]
122 pub line_items: Vec<LineItem>,
123
124 #[serde(rename = "taxTotal", skip_serializing_if = "Option::is_none")]
126 pub tax_total: Option<TaxTotal>,
127
128 pub total: f64,
130
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub sub_total: Option<f64>,
134
135 #[serde(rename = "dueDate", skip_serializing_if = "Option::is_none")]
137 pub due_date: Option<String>,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub note: Option<String>,
142
143 #[serde(rename = "paymentTerms", skip_serializing_if = "Option::is_none")]
145 pub payment_terms: Option<String>,
146
147 #[serde(rename = "accountingCost", skip_serializing_if = "Option::is_none")]
149 pub accounting_cost: Option<String>,
150
151 #[serde(rename = "orderReference", skip_serializing_if = "Option::is_none")]
153 pub order_reference: Option<OrderReference>,
154
155 #[serde(
157 rename = "additionalDocumentReference",
158 skip_serializing_if = "Option::is_none"
159 )]
160 pub additional_document_reference: Option<Vec<DocumentReference>>,
161
162 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
164 pub metadata: HashMap<String, serde_json::Value>,
165}
166
167impl Invoice {
168 pub fn new(
170 id: String,
171 issue_date: String,
172 currency_code: String,
173 line_items: Vec<LineItem>,
174 total: f64,
175 ) -> Self {
176 Self {
177 id,
178 issue_date,
179 currency_code,
180 line_items,
181 tax_total: None,
182 total,
183 sub_total: None,
184 due_date: None,
185 note: None,
186 payment_terms: None,
187 accounting_cost: None,
188 order_reference: None,
189 additional_document_reference: None,
190 metadata: HashMap::new(),
191 }
192 }
193
194 pub fn validate(&self) -> crate::error::Result<()> {
196 use crate::error::Error;
197
198 if self.id.is_empty() {
200 return Err(Error::Validation("Invoice ID is required".to_string()));
201 }
202
203 if self.issue_date.is_empty() {
204 return Err(Error::Validation("Issue date is required".to_string()));
205 }
206
207 if self.currency_code.is_empty() {
208 return Err(Error::Validation("Currency code is required".to_string()));
209 }
210
211 if self.line_items.is_empty() {
212 return Err(Error::Validation(
213 "At least one line item is required".to_string(),
214 ));
215 }
216
217 for (i, item) in self.line_items.iter().enumerate() {
219 if item.id.is_empty() {
220 return Err(Error::Validation(format!(
221 "Line item {} is missing an ID",
222 i
223 )));
224 }
225
226 if item.description.is_empty() {
227 return Err(Error::Validation(format!(
228 "Line item {} is missing a description",
229 i
230 )));
231 }
232
233 let calculated_total = item.quantity * item.unit_price;
236 let difference = (calculated_total - item.line_total).abs();
237 if difference > 0.01 {
238 return Err(Error::Validation(format!(
240 "Line item {}: Line total ({}) does not match quantity ({}) * unit price ({})",
241 i, item.line_total, item.quantity, item.unit_price
242 )));
243 }
244 }
245
246 if let Some(sub_total) = self.sub_total {
248 let calculated_sub_total: f64 =
249 self.line_items.iter().map(|item| item.line_total).sum();
250 let difference = (calculated_sub_total - sub_total).abs();
251 if difference > 0.01 {
252 return Err(Error::Validation(format!(
254 "Sub-total ({}) does not match the sum of line totals ({})",
255 sub_total, calculated_sub_total
256 )));
257 }
258 }
259
260 if let Some(tax_total) = &self.tax_total {
262 if let Some(tax_subtotals) = &tax_total.tax_subtotal {
263 let sum_of_subtotals: f64 = tax_subtotals.iter().map(|st| st.tax_amount).sum();
264 let difference = (sum_of_subtotals - tax_total.tax_amount).abs();
265 if difference > 0.01 {
266 return Err(Error::Validation(format!(
268 "Tax total amount ({}) does not match the sum of tax subtotal amounts ({})",
269 tax_total.tax_amount, sum_of_subtotals
270 )));
271 }
272 }
273 }
274
275 let sub_total = self
277 .sub_total
278 .unwrap_or_else(|| self.line_items.iter().map(|item| item.line_total).sum());
279 let tax_amount = self.tax_total.as_ref().map_or(0.0, |tt| tt.tax_amount);
280 let calculated_total = sub_total + tax_amount;
281 let difference = (calculated_total - self.total).abs();
282 if difference > 0.01 {
283 return Err(Error::Validation(format!(
285 "Total ({}) does not match sub-total ({}) + tax amount ({})",
286 self.total, sub_total, tax_amount
287 )));
288 }
289
290 if self.issue_date.len() != 10 {
292 return Err(Error::SerializationError(
293 "issue_date must be in YYYY-MM-DD format".to_string(),
294 ));
295 }
296 if chrono::NaiveDate::parse_from_str(&self.issue_date, "%Y-%m-%d").is_err() {
297 return Err(Error::SerializationError(
298 "Invalid issue_date format or value".to_string(),
299 ));
300 }
301
302 if let Some(due_date) = &self.due_date {
303 if due_date.len() != 10 {
304 return Err(Error::SerializationError(
305 "due_date must be in YYYY-MM-DD format".to_string(),
306 ));
307 }
308 if chrono::NaiveDate::parse_from_str(due_date, "%Y-%m-%d").is_err() {
309 return Err(Error::SerializationError(
310 "Invalid due_date format or value".to_string(),
311 ));
312 }
313 }
314
315 Ok(())
316 }
317}