Skip to main content

core_invoice/
tax.rs

1/// Tax system on the invoice. PINT is the reason this is not "VAT or nothing".
2#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
3pub enum TaxSystem {
4    Vat,
5    Gst,
6    Sst,
7    Consumption,
8}
9
10impl TaxSystem {
11    pub fn as_str(self) -> &'static str {
12        match self {
13            Self::Vat => "VAT",
14            Self::Gst => "GST",
15            Self::Sst => "SST",
16            Self::Consumption => "CONSUMPTION",
17        }
18    }
19
20    pub fn parse(s: &str) -> Option<Self> {
21        match s.trim().to_ascii_uppercase().as_str() {
22            "VAT" => Some(Self::Vat),
23            "GST" => Some(Self::Gst),
24            "SST" | "SALES" | "SERVICE" => Some(Self::Sst),
25            "CONSUMPTION" | "CT" => Some(Self::Consumption),
26            _ => None,
27        }
28    }
29}
30
31/// A tax category on a line or breakdown (rate + system).
32///
33/// `percent` is `None` when the family has no IBT-119 (EN `O`, PINT-MY TTX).
34/// Zero is a stated 0 % (EN `Z`), not an absent rate.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct TaxCategory {
37    pub system: TaxSystem,
38    /// BT-151 / BT-118 category code. `String` (not [`crate::Code`]) so PINT extra
39    /// codes (SA, SE, HVG, TTX, …) stay representable without a VAT-only enum.
40    pub code: String,
41    pub percent: Option<crate::numeric::Percentage>,
42}
43
44impl TaxCategory {
45    pub fn vat(code: impl Into<String>, percent: impl Into<crate::numeric::Percentage>) -> Self {
46        Self {
47            system: TaxSystem::Vat,
48            code: code.into(),
49            percent: Some(percent.into()),
50        }
51    }
52
53    pub fn sst(code: impl Into<String>, percent: impl Into<crate::numeric::Percentage>) -> Self {
54        Self {
55            system: TaxSystem::Sst,
56            code: code.into(),
57            percent: Some(percent.into()),
58        }
59    }
60
61    pub fn gst(code: impl Into<String>, percent: impl Into<crate::numeric::Percentage>) -> Self {
62        Self {
63            system: TaxSystem::Gst,
64            code: code.into(),
65            percent: Some(percent.into()),
66        }
67    }
68
69    /// EN / PINT `O`: no rate on the line.
70    pub fn out_of_scope() -> Self {
71        Self {
72            system: TaxSystem::Vat,
73            code: "O".into(),
74            percent: None,
75        }
76    }
77
78    /// PINT-MY TTX: amount-only, scheme AAL, no Percent.
79    pub fn ttx() -> Self {
80        Self {
81            system: TaxSystem::Sst,
82            code: "TTX".into(),
83            percent: None,
84        }
85    }
86}
87
88/// TaxScheme/cbc:ID on the wire. Never `SST` for PINT-MY.
89pub fn wire_scheme(
90    profile: crate::profile::Profile,
91    system: TaxSystem,
92    category: &str,
93) -> &'static str {
94    use crate::profile::Profile;
95    match profile {
96        Profile::En16931 | Profile::PeppolBis3 => "VAT",
97        Profile::PintMy if category.eq_ignore_ascii_case("TTX") => "AAL",
98        Profile::PintMy => "VAT",
99        Profile::Pint => match system {
100            TaxSystem::Gst => "GST",
101            _ => "VAT",
102        },
103        // Production write never stamps Unknown; keep a VAT scheme if tests serialise.
104        Profile::Unknown => "VAT",
105    }
106}
107
108pub fn pint_my_category(code: &str) -> bool {
109    matches!(code, "SA" | "SE" | "HVG" | "LVG" | "TTX" | "E" | "O")
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn vat_cgst_is_not_vat() {
118        assert_eq!(TaxSystem::parse("VAT/CGST"), None);
119        assert_eq!(TaxSystem::parse("VAT"), Some(TaxSystem::Vat));
120    }
121}