1use crate::invoice::Invoice;
2use crate::report::{Finding, Report};
3
4pub fn validate(invoice: &Invoice) -> Report {
6 let mut report = Report::default();
7
8 if invoice.number.trim().is_empty() {
9 report.push(Finding::fatal("BR-02", "Invoice number (BT-1) shall be present"));
10 }
11
12 if invoice.currency.trim().len() != 3 {
13 report.push(Finding::fatal(
14 "BR-05",
15 "Invoice currency code (BT-5) shall be a 3-letter code",
16 ));
17 }
18
19 if invoice.seller.name.trim().is_empty() {
20 report.push(Finding::fatal("BR-06", "Seller name (BT-27) shall be present"));
21 }
22
23 if invoice.buyer.name.trim().is_empty() {
24 report.push(Finding::fatal("BR-07", "Buyer name (BT-44) shall be present"));
25 }
26
27 if invoice.lines.is_empty() {
28 report.push(Finding::fatal(
29 "BR-16",
30 "An invoice shall have at least one line (BG-25)",
31 ));
32 }
33
34 for (i, line) in invoice.lines.iter().enumerate() {
35 if !invoice.profile.allows(line.tax.system) {
36 report.push(Finding::fatal(
37 "PINT-TAX",
38 format!(
39 "Line {} tax system {} is not allowed on profile {}",
40 i + 1,
41 line.tax.system.as_str(),
42 invoice.profile.slug()
43 ),
44 ));
45 }
46 }
47
48 let net = invoice.line_net_sum();
49 let expected_payable = net.saturating_add(invoice.tax_total);
50 if expected_payable != invoice.payable {
51 report.push(Finding::fatal(
52 "BR-CO-16",
53 format!(
54 "Payable ({}) shall equal line net ({}) + tax total ({})",
55 invoice.payable, net, invoice.tax_total
56 ),
57 ));
58 }
59
60 if invoice.profile == crate::profile::Profile::PintMy {
61 match invoice.seller.id_scheme.as_deref() {
62 Some("TIN") | Some("BRN") | Some("NRIC") | Some("PASSPORT") => {}
63 Some(other) => report.push(Finding::fatal(
64 "PINT-MY-ID",
65 format!("Seller id scheme {other} is not a PINT-MY identification type"),
66 )),
67 None if invoice.seller.tax_id.is_some() => report.push(Finding::fatal(
68 "PINT-MY-ID",
69 "Seller tax id on PINT-MY requires a scheme (TIN, BRN, NRIC, PASSPORT)",
70 )),
71 None => {}
72 }
73 }
74
75 report
76}