Skip to main content

core_invoice/
tax.rs

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