Skip to main content

core_invoice/
lib.rs

1//! Semantic model for the core electronic invoice: EN 16931 and Peppol PINT.
2//!
3//! Tax is VAT, GST, SST, or consumption — not VAT only. No I/O, no tax-authority
4//! APIs, no accounting UI.
5
6pub mod amount;
7pub mod invoice;
8pub mod profile;
9pub mod report;
10pub mod tax;
11pub mod validate;
12
13pub use amount::Amount;
14pub use invoice::{Invoice, Line, Party};
15pub use profile::Profile;
16pub use report::{Finding, Report, Severity};
17pub use tax::{TaxCategory, TaxSystem};
18pub use validate::validate;
19
20#[cfg(test)]
21mod tests {
22    use super::*;
23    use rust_decimal::Decimal;
24
25    fn sst_invoice(profile: Profile) -> Invoice {
26        Invoice {
27            profile,
28            number: "INV-1".into(),
29            currency: "MYR".into(),
30            seller: {
31                let mut p = Party::new("Kedai", "MY");
32                p.tax_id = Some("C12345678901".into());
33                p.id_scheme = Some("TIN".into());
34                p
35            },
36            buyer: Party::new("Pembeli", "MY"),
37            lines: vec![Line {
38                id: "1".into(),
39                name: "Goods".into(),
40                net: Amount::parse("100.00").unwrap(),
41                tax: TaxCategory::sst("SR", Decimal::new(10, 2)),
42            }],
43            tax_total: Amount::parse("10.00").unwrap(),
44            payable: Amount::parse("110.00").unwrap(),
45        }
46    }
47
48    #[test]
49    fn pint_my_accepts_sst() {
50        let report = validate(&sst_invoice(Profile::PintMy));
51        assert!(report.ok(), "{report}");
52    }
53
54    #[test]
55    fn peppol_bis_rejects_sst() {
56        let report = validate(&sst_invoice(Profile::PeppolBis3));
57        assert!(!report.ok());
58        assert!(
59            report.findings.iter().any(|f| f.id == "PINT-TAX"),
60            "{report}"
61        );
62    }
63
64    #[test]
65    fn payable_must_match_net_plus_tax() {
66        let mut inv = sst_invoice(Profile::Pint);
67        inv.payable = Amount::parse("999.00").unwrap();
68        let report = validate(&inv);
69        assert!(report.findings.iter().any(|f| f.id == "BR-CO-16"));
70    }
71}