use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxCategory {
pub id: String,
pub percent: f64,
#[serde(rename = "taxScheme")]
pub tax_scheme: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LineItem {
pub id: String,
pub description: String,
pub quantity: f64,
#[serde(rename = "unitCode", skip_serializing_if = "Option::is_none")]
pub unit_code: Option<String>,
#[serde(rename = "unitPrice")]
pub unit_price: f64,
#[serde(rename = "lineTotal")]
pub line_total: f64,
#[serde(rename = "taxCategory", skip_serializing_if = "Option::is_none")]
pub tax_category: Option<TaxCategory>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
#[derive(Default)]
pub struct LineItemBuilder {
id: Option<String>,
description: Option<String>,
quantity: Option<f64>,
unit_code: Option<String>,
unit_price: Option<f64>,
line_total: Option<f64>,
tax_category: Option<TaxCategory>,
name: Option<String>,
image: Option<String>,
url: Option<String>,
}
impl LineItemBuilder {
pub fn id(mut self, id: String) -> Self {
self.id = Some(id);
self
}
pub fn description(mut self, description: String) -> Self {
self.description = Some(description);
self
}
pub fn quantity(mut self, quantity: f64) -> Self {
self.quantity = Some(quantity);
self
}
pub fn unit_code(mut self, unit_code: String) -> Self {
self.unit_code = Some(unit_code);
self
}
pub fn unit_price(mut self, unit_price: f64) -> Self {
self.unit_price = Some(unit_price);
self
}
pub fn line_total(mut self, line_total: f64) -> Self {
self.line_total = Some(line_total);
self
}
pub fn tax_category(mut self, tax_category: TaxCategory) -> Self {
self.tax_category = Some(tax_category);
self
}
pub fn name(mut self, name: String) -> Self {
self.name = Some(name);
self
}
pub fn image(mut self, image: String) -> Self {
self.image = Some(image);
self
}
pub fn url(mut self, url: String) -> Self {
self.url = Some(url);
self
}
pub fn build(self) -> LineItem {
LineItem {
id: self.id.expect("id is required"),
description: self.description.expect("description is required"),
quantity: self.quantity.expect("quantity is required"),
unit_code: self.unit_code,
unit_price: self.unit_price.expect("unit_price is required"),
line_total: self.line_total.expect("line_total is required"),
tax_category: self.tax_category,
name: self.name,
image: self.image,
url: self.url,
}
}
}
impl LineItem {
pub fn builder() -> LineItemBuilder {
LineItemBuilder::default()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxSubtotal {
#[serde(rename = "taxableAmount")]
pub taxable_amount: f64,
#[serde(rename = "taxAmount")]
pub tax_amount: f64,
#[serde(rename = "taxCategory")]
pub tax_category: TaxCategory,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxTotal {
#[serde(rename = "taxAmount")]
pub tax_amount: f64,
#[serde(rename = "taxSubtotal", skip_serializing_if = "Option::is_none")]
pub tax_subtotal: Option<Vec<TaxSubtotal>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderReference {
pub id: String,
#[serde(rename = "issueDate", skip_serializing_if = "Option::is_none")]
pub issue_date: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentReference {
pub id: String,
#[serde(rename = "documentType", skip_serializing_if = "Option::is_none")]
pub document_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Invoice {
pub id: String,
#[serde(rename = "issueDate")]
pub issue_date: String,
#[serde(rename = "currencyCode")]
pub currency_code: String,
#[serde(rename = "lineItems")]
pub line_items: Vec<LineItem>,
#[serde(rename = "taxTotal", skip_serializing_if = "Option::is_none")]
pub tax_total: Option<TaxTotal>,
pub total: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub sub_total: Option<f64>,
#[serde(rename = "dueDate", skip_serializing_if = "Option::is_none")]
pub due_date: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
#[serde(rename = "paymentTerms", skip_serializing_if = "Option::is_none")]
pub payment_terms: Option<String>,
#[serde(rename = "accountingCost", skip_serializing_if = "Option::is_none")]
pub accounting_cost: Option<String>,
#[serde(rename = "orderReference", skip_serializing_if = "Option::is_none")]
pub order_reference: Option<OrderReference>,
#[serde(
rename = "additionalDocumentReference",
skip_serializing_if = "Option::is_none"
)]
pub additional_document_reference: Option<Vec<DocumentReference>>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
}
impl Invoice {
pub fn new(
id: String,
issue_date: String,
currency_code: String,
line_items: Vec<LineItem>,
total: f64,
) -> Self {
Self {
id,
issue_date,
currency_code,
line_items,
tax_total: None,
total,
sub_total: None,
due_date: None,
note: None,
payment_terms: None,
accounting_cost: None,
order_reference: None,
additional_document_reference: None,
metadata: HashMap::new(),
}
}
pub fn validate(&self) -> crate::error::Result<()> {
use crate::error::Error;
if self.id.is_empty() {
return Err(Error::Validation("Invoice ID is required".to_string()));
}
if self.issue_date.is_empty() {
return Err(Error::Validation("Issue date is required".to_string()));
}
if self.currency_code.is_empty() {
return Err(Error::Validation("Currency code is required".to_string()));
}
if self.line_items.is_empty() {
return Err(Error::Validation(
"At least one line item is required".to_string(),
));
}
for (i, item) in self.line_items.iter().enumerate() {
if item.id.is_empty() {
return Err(Error::Validation(format!(
"Line item {} is missing an ID",
i
)));
}
if item.description.is_empty() {
return Err(Error::Validation(format!(
"Line item {} is missing a description",
i
)));
}
let calculated_total = item.quantity * item.unit_price;
let difference = (calculated_total - item.line_total).abs();
if difference > 0.01 {
return Err(Error::Validation(format!(
"Line item {}: Line total ({}) does not match quantity ({}) * unit price ({})",
i, item.line_total, item.quantity, item.unit_price
)));
}
}
if let Some(sub_total) = self.sub_total {
let calculated_sub_total: f64 =
self.line_items.iter().map(|item| item.line_total).sum();
let difference = (calculated_sub_total - sub_total).abs();
if difference > 0.01 {
return Err(Error::Validation(format!(
"Sub-total ({}) does not match the sum of line totals ({})",
sub_total, calculated_sub_total
)));
}
}
if let Some(tax_total) = &self.tax_total {
if let Some(tax_subtotals) = &tax_total.tax_subtotal {
let sum_of_subtotals: f64 = tax_subtotals.iter().map(|st| st.tax_amount).sum();
let difference = (sum_of_subtotals - tax_total.tax_amount).abs();
if difference > 0.01 {
return Err(Error::Validation(format!(
"Tax total amount ({}) does not match the sum of tax subtotal amounts ({})",
tax_total.tax_amount, sum_of_subtotals
)));
}
}
}
let sub_total = self
.sub_total
.unwrap_or_else(|| self.line_items.iter().map(|item| item.line_total).sum());
let tax_amount = self.tax_total.as_ref().map_or(0.0, |tt| tt.tax_amount);
let calculated_total = sub_total + tax_amount;
let difference = (calculated_total - self.total).abs();
if difference > 0.01 {
return Err(Error::Validation(format!(
"Total ({}) does not match sub-total ({}) + tax amount ({})",
self.total, sub_total, tax_amount
)));
}
if self.issue_date.len() != 10 {
return Err(Error::SerializationError(
"issue_date must be in YYYY-MM-DD format".to_string(),
));
}
if chrono::NaiveDate::parse_from_str(&self.issue_date, "%Y-%m-%d").is_err() {
return Err(Error::SerializationError(
"Invalid issue_date format or value".to_string(),
));
}
if let Some(due_date) = &self.due_date {
if due_date.len() != 10 {
return Err(Error::SerializationError(
"due_date must be in YYYY-MM-DD format".to_string(),
));
}
if chrono::NaiveDate::parse_from_str(due_date, "%Y-%m-%d").is_err() {
return Err(Error::SerializationError(
"Invalid due_date format or value".to_string(),
));
}
}
Ok(())
}
}