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 #[serde(skip_serializing_if = "Option::is_none")]
53 pub name: Option<String>,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub image: Option<String>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub url: Option<String>,
62}
63
64#[derive(Default)]
66pub struct LineItemBuilder {
67 id: Option<String>,
68 description: Option<String>,
69 quantity: Option<f64>,
70 unit_code: Option<String>,
71 unit_price: Option<f64>,
72 line_total: Option<f64>,
73 tax_category: Option<TaxCategory>,
74 name: Option<String>,
75 image: Option<String>,
76 url: Option<String>,
77}
78
79impl LineItemBuilder {
80 pub fn id(mut self, id: String) -> Self {
82 self.id = Some(id);
83 self
84 }
85
86 pub fn description(mut self, description: String) -> Self {
88 self.description = Some(description);
89 self
90 }
91
92 pub fn quantity(mut self, quantity: f64) -> Self {
94 self.quantity = Some(quantity);
95 self
96 }
97
98 pub fn unit_code(mut self, unit_code: String) -> Self {
100 self.unit_code = Some(unit_code);
101 self
102 }
103
104 pub fn unit_price(mut self, unit_price: f64) -> Self {
106 self.unit_price = Some(unit_price);
107 self
108 }
109
110 pub fn line_total(mut self, line_total: f64) -> Self {
112 self.line_total = Some(line_total);
113 self
114 }
115
116 pub fn tax_category(mut self, tax_category: TaxCategory) -> Self {
118 self.tax_category = Some(tax_category);
119 self
120 }
121
122 pub fn name(mut self, name: String) -> Self {
124 self.name = Some(name);
125 self
126 }
127
128 pub fn image(mut self, image: String) -> Self {
130 self.image = Some(image);
131 self
132 }
133
134 pub fn url(mut self, url: String) -> Self {
136 self.url = Some(url);
137 self
138 }
139
140 pub fn build(self) -> LineItem {
142 LineItem {
143 id: self.id.expect("id is required"),
144 description: self.description.expect("description is required"),
145 quantity: self.quantity.expect("quantity is required"),
146 unit_code: self.unit_code,
147 unit_price: self.unit_price.expect("unit_price is required"),
148 line_total: self.line_total.expect("line_total is required"),
149 tax_category: self.tax_category,
150 name: self.name,
151 image: self.image,
152 url: self.url,
153 }
154 }
155}
156
157impl LineItem {
158 pub fn builder() -> LineItemBuilder {
160 LineItemBuilder::default()
161 }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct TaxSubtotal {
167 #[serde(rename = "taxableAmount")]
169 pub taxable_amount: f64,
170
171 #[serde(rename = "taxAmount")]
173 pub tax_amount: f64,
174
175 #[serde(rename = "taxCategory")]
177 pub tax_category: TaxCategory,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct TaxTotal {
183 #[serde(rename = "taxAmount")]
185 pub tax_amount: f64,
186
187 #[serde(rename = "taxSubtotal", skip_serializing_if = "Option::is_none")]
189 pub tax_subtotal: Option<Vec<TaxSubtotal>>,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct OrderReference {
195 pub id: String,
197
198 #[serde(rename = "issueDate", skip_serializing_if = "Option::is_none")]
200 pub issue_date: Option<String>,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct DocumentReference {
206 pub id: String,
208
209 #[serde(rename = "documentType", skip_serializing_if = "Option::is_none")]
211 pub document_type: Option<String>,
212
213 #[serde(skip_serializing_if = "Option::is_none")]
215 pub url: Option<String>,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct Invoice {
221 pub id: String,
223
224 #[serde(rename = "issueDate")]
226 pub issue_date: String,
227
228 #[serde(rename = "currencyCode")]
230 pub currency_code: String,
231
232 #[serde(rename = "lineItems")]
234 pub line_items: Vec<LineItem>,
235
236 #[serde(rename = "taxTotal", skip_serializing_if = "Option::is_none")]
238 pub tax_total: Option<TaxTotal>,
239
240 pub total: f64,
242
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub sub_total: Option<f64>,
246
247 #[serde(rename = "dueDate", skip_serializing_if = "Option::is_none")]
249 pub due_date: Option<String>,
250
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub note: Option<String>,
254
255 #[serde(rename = "paymentTerms", skip_serializing_if = "Option::is_none")]
257 pub payment_terms: Option<String>,
258
259 #[serde(rename = "accountingCost", skip_serializing_if = "Option::is_none")]
261 pub accounting_cost: Option<String>,
262
263 #[serde(rename = "orderReference", skip_serializing_if = "Option::is_none")]
265 pub order_reference: Option<OrderReference>,
266
267 #[serde(
269 rename = "additionalDocumentReference",
270 skip_serializing_if = "Option::is_none"
271 )]
272 pub additional_document_reference: Option<Vec<DocumentReference>>,
273
274 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
276 pub metadata: HashMap<String, serde_json::Value>,
277}
278
279impl Invoice {
280 pub fn new(
282 id: String,
283 issue_date: String,
284 currency_code: String,
285 line_items: Vec<LineItem>,
286 total: f64,
287 ) -> Self {
288 Self {
289 id,
290 issue_date,
291 currency_code,
292 line_items,
293 tax_total: None,
294 total,
295 sub_total: None,
296 due_date: None,
297 note: None,
298 payment_terms: None,
299 accounting_cost: None,
300 order_reference: None,
301 additional_document_reference: None,
302 metadata: HashMap::new(),
303 }
304 }
305
306 pub fn validate(&self) -> crate::error::Result<()> {
308 use crate::error::Error;
309
310 if self.id.is_empty() {
312 return Err(Error::Validation("Invoice ID is required".to_string()));
313 }
314
315 if self.issue_date.is_empty() {
316 return Err(Error::Validation("Issue date is required".to_string()));
317 }
318
319 if self.currency_code.is_empty() {
320 return Err(Error::Validation("Currency code is required".to_string()));
321 }
322
323 if self.line_items.is_empty() {
324 return Err(Error::Validation(
325 "At least one line item is required".to_string(),
326 ));
327 }
328
329 for (i, item) in self.line_items.iter().enumerate() {
331 if item.id.is_empty() {
332 return Err(Error::Validation(format!(
333 "Line item {} is missing an ID",
334 i
335 )));
336 }
337
338 if item.description.is_empty() {
339 return Err(Error::Validation(format!(
340 "Line item {} is missing a description",
341 i
342 )));
343 }
344
345 let calculated_total = item.quantity * item.unit_price;
348 let difference = (calculated_total - item.line_total).abs();
349 if difference > 0.01 {
350 return Err(Error::Validation(format!(
352 "Line item {}: Line total ({}) does not match quantity ({}) * unit price ({})",
353 i, item.line_total, item.quantity, item.unit_price
354 )));
355 }
356 }
357
358 if let Some(sub_total) = self.sub_total {
360 let calculated_sub_total: f64 =
361 self.line_items.iter().map(|item| item.line_total).sum();
362 let difference = (calculated_sub_total - sub_total).abs();
363 if difference > 0.01 {
364 return Err(Error::Validation(format!(
366 "Sub-total ({}) does not match the sum of line totals ({})",
367 sub_total, calculated_sub_total
368 )));
369 }
370 }
371
372 if let Some(tax_total) = &self.tax_total {
374 if let Some(tax_subtotals) = &tax_total.tax_subtotal {
375 let sum_of_subtotals: f64 = tax_subtotals.iter().map(|st| st.tax_amount).sum();
376 let difference = (sum_of_subtotals - tax_total.tax_amount).abs();
377 if difference > 0.01 {
378 return Err(Error::Validation(format!(
380 "Tax total amount ({}) does not match the sum of tax subtotal amounts ({})",
381 tax_total.tax_amount, sum_of_subtotals
382 )));
383 }
384 }
385 }
386
387 let sub_total = self
389 .sub_total
390 .unwrap_or_else(|| self.line_items.iter().map(|item| item.line_total).sum());
391 let tax_amount = self.tax_total.as_ref().map_or(0.0, |tt| tt.tax_amount);
392 let calculated_total = sub_total + tax_amount;
393 let difference = (calculated_total - self.total).abs();
394 if difference > 0.01 {
395 return Err(Error::Validation(format!(
397 "Total ({}) does not match sub-total ({}) + tax amount ({})",
398 self.total, sub_total, tax_amount
399 )));
400 }
401
402 if self.issue_date.len() != 10 {
404 return Err(Error::SerializationError(
405 "issue_date must be in YYYY-MM-DD format".to_string(),
406 ));
407 }
408 if chrono::NaiveDate::parse_from_str(&self.issue_date, "%Y-%m-%d").is_err() {
409 return Err(Error::SerializationError(
410 "Invalid issue_date format or value".to_string(),
411 ));
412 }
413
414 if let Some(due_date) = &self.due_date {
415 if due_date.len() != 10 {
416 return Err(Error::SerializationError(
417 "due_date must be in YYYY-MM-DD format".to_string(),
418 ));
419 }
420 if chrono::NaiveDate::parse_from_str(due_date, "%Y-%m-%d").is_err() {
421 return Err(Error::SerializationError(
422 "Invalid due_date format or value".to_string(),
423 ));
424 }
425 }
426
427 Ok(())
428 }
429}