Skip to main content

invoice_parser/
models.rs

1use chrono::NaiveDate;
2use serde::{Deserialize, Serialize};
3
4mod decimal_format {
5    use serde::{self, Deserialize, Deserializer, Serializer};
6
7    pub fn serialize<S>(value: &f64, serializer: S) -> Result<S::Ok, S::Error>
8    where
9        S: Serializer,
10    {
11        let abs = value.abs();
12        if abs == 0.0 {
13            serializer.serialize_f64(0.0)
14        } else if abs < 0.0001 {
15            serializer.serialize_str(
16                format!("{:.10}", value)
17                    .trim_end_matches('0')
18                    .trim_end_matches('.'),
19            )
20        } else if abs < 1.0 {
21            serializer.serialize_str(
22                format!("{:.8}", value)
23                    .trim_end_matches('0')
24                    .trim_end_matches('.'),
25            )
26        } else {
27            serializer.serialize_f64(*value)
28        }
29    }
30
31    pub fn deserialize<'de, D>(deserializer: D) -> Result<f64, D::Error>
32    where
33        D: Deserializer<'de>,
34    {
35        #[derive(Deserialize)]
36        #[serde(untagged)]
37        enum StringOrFloat {
38            String(String),
39            Float(f64),
40        }
41
42        match StringOrFloat::deserialize(deserializer)? {
43            StringOrFloat::String(s) => s.parse().map_err(serde::de::Error::custom),
44            StringOrFloat::Float(f) => Ok(f),
45        }
46    }
47}
48
49mod decimal_format_option {
50    use serde::{self, Deserialize, Deserializer, Serializer};
51
52    pub fn serialize<S>(value: &Option<f64>, serializer: S) -> Result<S::Ok, S::Error>
53    where
54        S: Serializer,
55    {
56        match value {
57            Some(v) => {
58                let abs = v.abs();
59                if abs == 0.0 {
60                    serializer.serialize_f64(0.0)
61                } else if abs < 0.0001 {
62                    serializer.serialize_str(
63                        format!("{:.10}", v)
64                            .trim_end_matches('0')
65                            .trim_end_matches('.'),
66                    )
67                } else if abs < 1.0 {
68                    serializer.serialize_str(
69                        format!("{:.8}", v)
70                            .trim_end_matches('0')
71                            .trim_end_matches('.'),
72                    )
73                } else {
74                    serializer.serialize_f64(*v)
75                }
76            }
77            None => serializer.serialize_none(),
78        }
79    }
80
81    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
82    where
83        D: Deserializer<'de>,
84    {
85        #[derive(Deserialize)]
86        #[serde(untagged)]
87        enum StringOrFloat {
88            String(String),
89            Float(f64),
90        }
91
92        let opt: Option<StringOrFloat> = Option::deserialize(deserializer)?;
93        match opt {
94            Some(StringOrFloat::String(s)) => s.parse().map(Some).map_err(serde::de::Error::custom),
95            Some(StringOrFloat::Float(f)) => Ok(Some(f)),
96            None => Ok(None),
97        }
98    }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
102pub enum DocumentFormat {
103    #[default]
104    Unknown,
105    AwsDirect,
106    ECloudValleyAws,
107    MicrofusionAliyun,
108    AliyunDirect,
109    UCloud,
110    GoogleCloud,
111    Azure,
112    Lokalise,
113    Sentry,
114    Mux,
115    MlyticsConsolidated,
116    AzureCsp,
117    AliyunUsageDetail,
118    MicrofusionGcpUsage,
119    Chargebee,
120    Edgenext,
121    DataStar,
122    DigicentreHk,
123    CloudMile,
124    MetaageAkamai,
125    VnisInvoice,
126    TencentEdgeOne,
127    AzurePlanDaily,
128    GoogleWorkspaceBilling,
129    HubSpot,
130    Reachtop,
131    GenericConsultant,
132    CdnOverageDetail,
133    Atlassian,
134    Contentsquare,
135    Slack,
136    MlyticsInvoice,
137    VNetwork,
138    VnisSummary,
139    CdnTraffic,
140    NonInvoice,
141}
142
143impl std::fmt::Display for DocumentFormat {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        match self {
146            DocumentFormat::Unknown => write!(f, "Unknown"),
147            DocumentFormat::AwsDirect => write!(f, "AWS Direct"),
148            DocumentFormat::ECloudValleyAws => write!(f, "eCloudValley AWS"),
149            DocumentFormat::MicrofusionAliyun => write!(f, "Microfusion Aliyun"),
150            DocumentFormat::AliyunDirect => write!(f, "Alibaba Cloud Direct"),
151            DocumentFormat::UCloud => write!(f, "UCloud"),
152            DocumentFormat::GoogleCloud => write!(f, "Google Cloud"),
153            DocumentFormat::Azure => write!(f, "Microsoft Azure"),
154            DocumentFormat::Lokalise => write!(f, "Lokalise"),
155            DocumentFormat::Sentry => write!(f, "Sentry"),
156            DocumentFormat::Mux => write!(f, "Mux"),
157            DocumentFormat::MlyticsConsolidated => write!(f, "Mlytics Consolidated"),
158            DocumentFormat::AzureCsp => write!(f, "Azure CSP"),
159            DocumentFormat::AliyunUsageDetail => write!(f, "Aliyun Usage Detail"),
160            DocumentFormat::MicrofusionGcpUsage => write!(f, "Microfusion GCP Usage"),
161            DocumentFormat::Chargebee => write!(f, "Chargebee"),
162            DocumentFormat::Edgenext => write!(f, "Edgenext"),
163            DocumentFormat::DataStar => write!(f, "Data Star"),
164            DocumentFormat::DigicentreHk => write!(f, "Digicentre HK"),
165            DocumentFormat::CloudMile => write!(f, "CloudMile"),
166            DocumentFormat::MetaageAkamai => write!(f, "Metaage Akamai"),
167            DocumentFormat::VnisInvoice => write!(f, "VNIS Invoice"),
168            DocumentFormat::TencentEdgeOne => write!(f, "Tencent EdgeOne"),
169            DocumentFormat::AzurePlanDaily => write!(f, "Azure Plan Daily"),
170            DocumentFormat::GoogleWorkspaceBilling => write!(f, "Google Workspace Billing"),
171            DocumentFormat::HubSpot => write!(f, "HubSpot"),
172            DocumentFormat::Reachtop => write!(f, "Reachtop"),
173            DocumentFormat::GenericConsultant => write!(f, "Generic Consultant"),
174            DocumentFormat::CdnOverageDetail => write!(f, "CDN Overage Detail"),
175            DocumentFormat::Atlassian => write!(f, "Atlassian"),
176            DocumentFormat::Contentsquare => write!(f, "Contentsquare"),
177            DocumentFormat::Slack => write!(f, "Slack"),
178            DocumentFormat::MlyticsInvoice => write!(f, "Mlytics Invoice"),
179            DocumentFormat::VNetwork => write!(f, "VNetwork"),
180            DocumentFormat::VnisSummary => write!(f, "VNIS Summary"),
181            DocumentFormat::CdnTraffic => write!(f, "CDN Traffic"),
182            DocumentFormat::NonInvoice => write!(f, "Non-Invoice"),
183        }
184    }
185}
186
187impl DocumentFormat {
188    pub fn vendor_name(&self) -> &str {
189        match self {
190            DocumentFormat::AwsDirect => "AWS",
191            DocumentFormat::ECloudValleyAws => "AWS",
192            DocumentFormat::MicrofusionAliyun => "阿里云",
193            DocumentFormat::AliyunDirect => "阿里云",
194            DocumentFormat::UCloud => "UCloud",
195            DocumentFormat::GoogleCloud => "Google Cloud",
196            DocumentFormat::Azure => "Microsoft Azure",
197            DocumentFormat::Lokalise => "Lokalise",
198            DocumentFormat::Sentry => "Sentry",
199            DocumentFormat::Mux => "Mux",
200            DocumentFormat::MlyticsConsolidated => "Mlytics",
201            DocumentFormat::AzureCsp => "Microsoft Azure",
202            DocumentFormat::AliyunUsageDetail => "阿里云",
203            DocumentFormat::MicrofusionGcpUsage => "Google Cloud",
204            DocumentFormat::Chargebee => "Chargebee",
205            DocumentFormat::Edgenext => "Edgenext",
206            DocumentFormat::DataStar => "Data Star",
207            DocumentFormat::DigicentreHk => "AWS",
208            DocumentFormat::CloudMile => "CloudMile",
209            DocumentFormat::MetaageAkamai => "Akamai",
210            DocumentFormat::VnisInvoice => "Mlytics",
211            DocumentFormat::TencentEdgeOne => "Tencent",
212            DocumentFormat::AzurePlanDaily => "Microsoft Azure",
213            DocumentFormat::GoogleWorkspaceBilling => "Google",
214            DocumentFormat::HubSpot => "HubSpot",
215            DocumentFormat::Reachtop => "Reachtop",
216            DocumentFormat::GenericConsultant => "",
217            DocumentFormat::CdnOverageDetail => "CDN",
218            DocumentFormat::Atlassian => "Atlassian",
219            DocumentFormat::Contentsquare => "Contentsquare",
220            DocumentFormat::Slack => "Slack",
221            DocumentFormat::MlyticsInvoice => "Mlytics",
222            DocumentFormat::VNetwork => "VNetwork",
223            DocumentFormat::VnisSummary => "VNIS",
224            DocumentFormat::CdnTraffic => "CDN",
225            DocumentFormat::NonInvoice => "",
226            DocumentFormat::Unknown => "",
227        }
228    }
229    pub fn format_type(&self) -> &str {
230        match self {
231            DocumentFormat::UCloud => "XLSX",
232            DocumentFormat::AliyunUsageDetail => "XLSX",
233            DocumentFormat::MicrofusionGcpUsage => "XLSX",
234            DocumentFormat::TencentEdgeOne => "XLSX",
235            DocumentFormat::AzurePlanDaily => "XLSX",
236            DocumentFormat::GoogleWorkspaceBilling => "XLSX",
237            DocumentFormat::Unknown => "其他",
238            _ => "PDF",
239        }
240    }
241}
242
243/// 货币类型枚举
244#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
245pub enum Currency {
246    /// 美元
247    #[default]
248    USD,
249    /// 欧元
250    EUR,
251    /// 英镑
252    GBP,
253    /// 日元
254    JPY,
255    /// 人民币
256    CNY,
257    /// 港币
258    HKD,
259    /// 新加坡元
260    SGD,
261    /// 澳元
262    AUD,
263    /// 加元
264    CAD,
265    /// 瑞士法郎
266    CHF,
267    /// 其他货币
268    Other(String),
269}
270
271impl From<&str> for Currency {
272    fn from(s: &str) -> Self {
273        match s.to_uppercase().as_str() {
274            "USD" | "$" | "US$" => Currency::USD,
275            "EUR" | "€" => Currency::EUR,
276            "GBP" | "£" => Currency::GBP,
277            "JPY" | "¥" | "YEN" => Currency::JPY,
278            "CNY" | "RMB" | "元" => Currency::CNY,
279            "HKD" | "HK$" => Currency::HKD,
280            "SGD" | "S$" => Currency::SGD,
281            "AUD" | "A$" => Currency::AUD,
282            "CAD" | "C$" => Currency::CAD,
283            "CHF" => Currency::CHF,
284            other => Currency::Other(other.to_string()),
285        }
286    }
287}
288
289/// 发票类型枚举
290#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
291pub enum InvoiceType {
292    /// 标准发票
293    #[default]
294    Standard,
295    /// 贷项通知单(退款/折让凭证)
296    CreditNote,
297    /// 借项通知单(补收款凭证)
298    DebitNote,
299    /// 形式发票(报价/预开发票)
300    ProformaInvoice,
301    /// 商业发票(国际贸易)
302    CommercialInvoice,
303    /// 收据
304    Receipt,
305    /// 账单
306    Bill,
307    /// 对账单(如 AWS 账单)
308    Statement,
309    /// 未知类型
310    Unknown,
311}
312
313/// 地址信息结构体
314#[derive(Debug, Clone, Serialize, Deserialize, Default)]
315pub struct Address {
316    /// 地址第一行(街道地址)
317    pub line1: Option<String>,
318    /// 地址第二行(门牌号、楼层等)
319    pub line2: Option<String>,
320    /// 城市
321    pub city: Option<String>,
322    /// 州/省
323    pub state: Option<String>,
324    /// 邮政编码
325    pub postal_code: Option<String>,
326    /// 国家
327    pub country: Option<String>,
328}
329
330impl Address {
331    /// 返回完整地址字符串,各部分用逗号分隔
332    pub fn full_address(&self) -> String {
333        [
334            self.line1.as_deref(),
335            self.line2.as_deref(),
336            self.city.as_deref(),
337            self.state.as_deref(),
338            self.postal_code.as_deref(),
339            self.country.as_deref(),
340        ]
341        .iter()
342        .filter_map(|&s| s)
343        .collect::<Vec<_>>()
344        .join(", ")
345    }
346}
347
348/// 交易方信息结构体(供应商或客户)
349#[derive(Debug, Clone, Serialize, Deserialize, Default)]
350pub struct Party {
351    /// 公司/个人名称
352    pub name: Option<String>,
353    /// 税务识别号(如统一编号、VAT号等)
354    pub tax_id: Option<String>,
355    /// 地址信息
356    pub address: Option<Address>,
357    /// 电子邮件
358    pub email: Option<String>,
359    /// 电话号码
360    pub phone: Option<String>,
361}
362
363/// 发票行项目结构体(单个商品/服务明细)
364#[derive(Debug, Clone, Serialize, Deserialize, Default)]
365pub struct LineItem {
366    /// 行号
367    pub line_number: Option<u32>,
368    /// 服务/项目名称(如:Amazon CloudFront)
369    pub service_name: Option<String>,
370    /// 使用类型/描述(如:US-Requests-Tier2-HTTPS)
371    pub description: String,
372    /// 数量
373    #[serde(with = "decimal_format_option")]
374    pub quantity: Option<f64>,
375    /// 单位(如:个、件、小时等)
376    pub unit: Option<String>,
377    /// 单价
378    #[serde(with = "decimal_format_option")]
379    pub unit_price: Option<f64>,
380    /// 折扣金额
381    #[serde(with = "decimal_format_option")]
382    pub discount: Option<f64>,
383    /// 税率(百分比)
384    #[serde(with = "decimal_format_option")]
385    pub tax_rate: Option<f64>,
386    /// 税额
387    #[serde(with = "decimal_format_option")]
388    pub tax_amount: Option<f64>,
389    /// 金额(数量 × 单价 - 折扣 + 税额)
390    #[serde(with = "decimal_format")]
391    pub amount: f64,
392}
393
394impl LineItem {
395    /// 计算行项目金额:数量 × 单价 - 折扣 + 税额
396    pub fn calculate_amount(&self) -> f64 {
397        let qty = self.quantity.unwrap_or(1.0);
398        let price = self.unit_price.unwrap_or(self.amount);
399        let discount = self.discount.unwrap_or(0.0);
400        let tax = self.tax_amount.unwrap_or(0.0);
401
402        (qty * price) - discount + tax
403    }
404
405    /// 验证单价×数量是否等于金额
406    /// 返回 (是否有效, 计算值, 差异值)
407    pub fn validate_amount(&self) -> LineItemValidation {
408        let (qty, price) = match (self.quantity, self.unit_price) {
409            (Some(q), Some(p)) => (q, p),
410            _ => {
411                return LineItemValidation {
412                    is_valid: true,
413                    can_validate: false,
414                    calculated_amount: None,
415                    difference: None,
416                    difference_percent: None,
417                }
418            }
419        };
420
421        let calculated = qty * price;
422        let diff = (self.amount - calculated).abs();
423        let diff_percent = if calculated.abs() > 0.0001 {
424            (diff / calculated.abs()) * 100.0
425        } else {
426            0.0
427        };
428
429        LineItemValidation {
430            is_valid: diff < 0.01 || diff_percent < 1.0,
431            can_validate: true,
432            calculated_amount: Some(calculated),
433            difference: Some(self.amount - calculated),
434            difference_percent: Some(diff_percent),
435        }
436    }
437}
438
439/// 行项目验证结果
440#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct LineItemValidation {
442    /// 是否有效(差异在允许范围内)
443    pub is_valid: bool,
444    /// 是否可以验证(有单价和数量)
445    pub can_validate: bool,
446    /// 计算金额(单价×数量)
447    pub calculated_amount: Option<f64>,
448    /// 差异值(实际金额 - 计算金额)
449    pub difference: Option<f64>,
450    /// 差异百分比
451    pub difference_percent: Option<f64>,
452}
453
454/// 税务汇总结构体
455#[derive(Debug, Clone, Serialize, Deserialize, Default)]
456pub struct TaxSummary {
457    /// 税种(如:增值税、营业税、GST等)
458    pub tax_type: Option<String>,
459    /// 税率(百分比)
460    pub tax_rate: Option<f64>,
461    /// 应税金额(税基)
462    pub taxable_amount: Option<f64>,
463    /// 税额
464    pub tax_amount: f64,
465}
466
467/// 付款信息结构体
468#[derive(Debug, Clone, Serialize, Deserialize, Default)]
469pub struct PaymentInfo {
470    /// 付款方式(如:银行转账、信用卡、支票等)
471    pub method: Option<String>,
472    /// 银行名称
473    pub bank_name: Option<String>,
474    /// 银行账号
475    pub account_number: Option<String>,
476    /// 银行路由号(美国银行系统)
477    pub routing_number: Option<String>,
478    /// 国际银行账号(IBAN)
479    pub iban: Option<String>,
480    /// SWIFT/BIC 代码(国际汇款)
481    pub swift_code: Option<String>,
482    /// 付款参考号/备注
483    pub reference: Option<String>,
484}
485
486/// 发票主结构体
487///
488/// 包含发票的所有核心信息,支持多种发票格式的解析结果存储
489#[derive(Debug, Clone, Serialize, Deserialize, Default)]
490pub struct Invoice {
491    pub document_format: DocumentFormat,
492    pub invoice_type: InvoiceType,
493    /// 发票号码
494    pub invoice_number: Option<String>,
495    /// 账户名称/项目别名(如 AWS Account No 后括号内的名称)
496    pub account_name: Option<String>,
497    /// 客户ID
498    pub customer_id: Option<String>,
499    /// 账单年月(格式:YYYY-MM,如 2025-01)
500    pub billing_period: Option<String>,
501    /// 发票日期
502    pub invoice_date: Option<NaiveDate>,
503    /// 付款截止日期
504    pub due_date: Option<NaiveDate>,
505    /// 货币类型
506    pub currency: Currency,
507    /// 供应商/卖方信息
508    pub vendor: Party,
509    /// 客户/买方信息
510    pub customer: Party,
511    /// 行项目列表(商品/服务明细)
512    pub line_items: Vec<LineItem>,
513    /// 小计金额(税前)
514    pub subtotal: Option<f64>,
515    /// 折扣金额(负数表示折扣)
516    pub discount_amount: Option<f64>,
517    /// 折扣比例(百分比,如 5.0 表示 5%)
518    pub discount_rate: Option<f64>,
519    /// 税务汇总列表
520    pub tax_summaries: Vec<TaxSummary>,
521    /// 总税额
522    pub total_tax: Option<f64>,
523    /// 总金额(含税)
524    pub total_amount: f64,
525    /// 已付金额
526    pub amount_paid: Option<f64>,
527    /// 应付金额(未付余额)
528    pub amount_due: Option<f64>,
529    /// 付款信息
530    pub payment_info: Option<PaymentInfo>,
531    /// 备注/附注
532    pub notes: Option<String>,
533    /// 原始文本(PDF提取的原文)
534    pub raw_text: Option<String>,
535    /// 元数据(扩展字段,如账单周期等)
536    pub metadata: std::collections::HashMap<String, String>,
537}
538
539impl Invoice {
540    /// 创建新的空发票实例
541    pub fn new() -> Self {
542        Self::default()
543    }
544
545    /// 序列化为格式化的 JSON 字符串
546    pub fn to_json(&self) -> Result<String, serde_json::Error> {
547        serde_json::to_string_pretty(self)
548    }
549
550    /// 序列化为紧凑的 JSON 字符串
551    pub fn to_json_compact(&self) -> Result<String, serde_json::Error> {
552        serde_json::to_string(self)
553    }
554
555    /// 从 JSON 字符串反序列化
556    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
557        serde_json::from_str(json)
558    }
559
560    /// 根据行项目计算小计金额
561    pub fn calculate_subtotal(&self) -> f64 {
562        self.line_items.iter().map(|item| item.amount).sum()
563    }
564
565    /// 根据税务汇总计算总税额
566    pub fn calculate_total_tax(&self) -> f64 {
567        self.tax_summaries.iter().map(|t| t.tax_amount).sum()
568    }
569
570    /// 判断发票是否已付清
571    pub fn is_paid(&self) -> bool {
572        match (self.amount_paid, self.amount_due) {
573            (Some(paid), _) if paid >= self.total_amount => true,
574            (_, Some(due)) if due <= 0.0 => true,
575            _ => false,
576        }
577    }
578
579    pub fn validate_line_items(&self) -> InvoiceValidation {
580        let mut validations = Vec::new();
581        let mut invalid_count = 0;
582        let mut validatable_count = 0;
583
584        for (idx, item) in self.line_items.iter().enumerate() {
585            let validation = item.validate_amount();
586            if validation.can_validate {
587                validatable_count += 1;
588                if !validation.is_valid {
589                    invalid_count += 1;
590                }
591            }
592            validations.push((idx, item.description.clone(), validation));
593        }
594
595        let line_items_sum = self.calculate_subtotal();
596        let subtotal_diff = self.subtotal.map(|s| {
597            let diff = s - line_items_sum;
598            let pct = if s.abs() > 0.0001 {
599                (diff.abs() / s.abs()) * 100.0
600            } else {
601                0.0
602            };
603            (diff, pct)
604        });
605
606        InvoiceValidation {
607            all_valid: invalid_count == 0 && Self::is_subtotal_valid(subtotal_diff),
608            subtotal_valid: Self::is_subtotal_valid(subtotal_diff),
609            total_items: self.line_items.len(),
610            validatable_items: validatable_count,
611            invalid_items: invalid_count,
612            line_items_sum,
613            subtotal: self.subtotal,
614            subtotal_difference: subtotal_diff.map(|(d, _)| d),
615            subtotal_difference_percent: subtotal_diff.map(|(_, p)| p),
616            item_validations: validations,
617        }
618    }
619
620    fn is_subtotal_valid(subtotal_diff: Option<(f64, f64)>) -> bool {
621        const MAX_ALLOWED_DIFFERENCE_PERCENT: f64 = 1.0;
622        match subtotal_diff {
623            Some((_, pct)) => pct <= MAX_ALLOWED_DIFFERENCE_PERCENT,
624            None => true,
625        }
626    }
627}
628
629#[derive(Debug, Clone)]
630pub struct InvoiceValidation {
631    pub all_valid: bool,
632    pub subtotal_valid: bool,
633    pub total_items: usize,
634    pub validatable_items: usize,
635    pub invalid_items: usize,
636    pub line_items_sum: f64,
637    pub subtotal: Option<f64>,
638    pub subtotal_difference: Option<f64>,
639    pub subtotal_difference_percent: Option<f64>,
640    pub item_validations: Vec<(usize, String, LineItemValidation)>,
641}
642
643impl InvoiceValidation {
644    pub fn print_report(&self) {
645        println!("=== Invoice Validation Report ===");
646        println!("Total items: {}", self.total_items);
647        println!("Validatable items: {}", self.validatable_items);
648        println!("Invalid items: {}", self.invalid_items);
649        println!("Line items sum: {:.2}", self.line_items_sum);
650
651        if let Some(subtotal) = self.subtotal {
652            println!("Invoice subtotal: {:.2}", subtotal);
653            if let (Some(diff), Some(pct)) =
654                (self.subtotal_difference, self.subtotal_difference_percent)
655            {
656                println!("Difference: {:.2} ({:.2}%)", diff, pct);
657                if !self.subtotal_valid {
658                    println!("ERROR: Line items sum does not match subtotal (>1% difference)");
659                }
660            }
661        }
662
663        let invalid_items: Vec<_> = self
664            .item_validations
665            .iter()
666            .filter(|(_, _, v)| v.can_validate && !v.is_valid)
667            .collect();
668
669        if !invalid_items.is_empty() {
670            println!("\nInvalid line items:");
671            for (idx, desc, v) in invalid_items {
672                println!(
673                    "  Line {}: {} | calculated: {:.4} | diff: {:.4}",
674                    idx + 1,
675                    desc,
676                    v.calculated_amount.unwrap_or(0.0),
677                    v.difference.unwrap_or(0.0)
678                );
679            }
680        }
681    }
682}
683
684/// 解析结果结构体
685///
686/// 封装解析操作的输出,支持单个或多个发票,并包含警告信息
687#[derive(Debug, Clone, Serialize, Deserialize)]
688pub struct ParseResult {
689    /// 解析出的发票列表
690    pub invoices: Vec<Invoice>,
691    /// 源文件路径
692    pub source_file: Option<String>,
693    /// 解析过程中的警告信息
694    pub parse_warnings: Vec<String>,
695}
696
697impl ParseResult {
698    /// 创建包含单个发票的解析结果
699    pub fn single(invoice: Invoice) -> Self {
700        Self {
701            invoices: vec![invoice],
702            source_file: None,
703            parse_warnings: Vec::new(),
704        }
705    }
706
707    /// 创建包含多个发票的解析结果
708    pub fn multiple(invoices: Vec<Invoice>) -> Self {
709        Self {
710            invoices,
711            source_file: None,
712            parse_warnings: Vec::new(),
713        }
714    }
715
716    /// 设置源文件路径(链式调用)
717    pub fn with_source(mut self, source: impl Into<String>) -> Self {
718        self.source_file = Some(source.into());
719        self
720    }
721
722    /// 添加警告信息(链式调用)
723    pub fn with_warning(mut self, warning: impl Into<String>) -> Self {
724        self.parse_warnings.push(warning.into());
725        self
726    }
727}