1use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "lowercase")]
18pub enum Jurisdiction {
19 Sg,
20 Uk,
21 Us,
22 Eu,
23 Custom,
24}
25
26impl Jurisdiction {
27 #[allow(clippy::should_implement_trait)]
28 pub fn from_str(s: &str) -> Option<Self> {
29 match s.to_lowercase().as_str() {
30 "sg" | "singapore" => Some(Self::Sg),
31 "uk" | "gb" | "gbr" | "united-kingdom" => Some(Self::Uk),
32 "us" | "usa" | "united-states" => Some(Self::Us),
33 "eu" | "de" | "fr" | "nl" | "at" | "ie" => Some(Self::Eu),
34 "custom" | "intl" | "international" => Some(Self::Custom),
35 _ => None,
36 }
37 }
38
39 pub fn profile(&self) -> TaxProfile {
40 match self {
41 Self::Sg => TaxProfile {
42 code: "sg",
43 country: "Singapore",
44 tax_label: "GST",
45 default_rate: 9.0,
46 currency: "SGD",
47 symbol: "S$",
48 tax_invoice_title: "Tax Invoice",
49 non_registered_title: "Invoice",
50 tax_id_label: "GST Reg. No.",
51 company_no_label: "UEN",
52 date_format: "%-d %B %Y",
53 supports_reverse_charge: false,
54 zero_rate_label: "Zero-rated",
55 },
56 Self::Uk => TaxProfile {
57 code: "uk",
58 country: "United Kingdom",
59 tax_label: "VAT",
60 default_rate: 20.0,
61 currency: "GBP",
62 symbol: "£",
63 tax_invoice_title: "VAT Invoice",
64 non_registered_title: "Invoice",
65 tax_id_label: "VAT No.",
66 company_no_label: "Company No.",
67 date_format: "%-d %B %Y",
68 supports_reverse_charge: true,
69 zero_rate_label: "Zero-rated",
70 },
71 Self::Us => TaxProfile {
72 code: "us",
73 country: "United States",
74 tax_label: "Sales tax",
75 default_rate: 0.0,
76 currency: "USD",
77 symbol: "$",
78 tax_invoice_title: "Invoice",
79 non_registered_title: "Invoice",
80 tax_id_label: "EIN",
81 company_no_label: "State ID",
82 date_format: "%B %-d, %Y",
83 supports_reverse_charge: false,
84 zero_rate_label: "Exempt",
85 },
86 Self::Eu => TaxProfile {
87 code: "eu",
88 country: "European Union",
89 tax_label: "VAT",
90 default_rate: 19.0, currency: "EUR",
92 symbol: "€",
93 tax_invoice_title: "Rechnung / Invoice",
94 non_registered_title: "Invoice",
95 tax_id_label: "VAT ID",
96 company_no_label: "Reg. No.",
97 date_format: "%-d %B %Y",
98 supports_reverse_charge: true,
99 zero_rate_label: "Reverse charge",
100 },
101 Self::Custom => TaxProfile {
102 code: "custom",
103 country: "International",
104 tax_label: "Tax",
105 default_rate: 0.0,
106 currency: "USD",
107 symbol: "$",
108 tax_invoice_title: "Invoice",
109 non_registered_title: "Invoice",
110 tax_id_label: "Tax ID",
111 company_no_label: "Reg. No.",
112 date_format: "%Y-%m-%d",
113 supports_reverse_charge: false,
114 zero_rate_label: "Zero-rated",
115 },
116 }
117 }
118
119 pub fn as_str(&self) -> &'static str {
120 match self {
121 Self::Sg => "sg",
122 Self::Uk => "uk",
123 Self::Us => "us",
124 Self::Eu => "eu",
125 Self::Custom => "custom",
126 }
127 }
128}
129
130#[derive(Debug, Clone, Serialize)]
131pub struct TaxProfile {
132 pub code: &'static str,
133 pub country: &'static str,
134 pub tax_label: &'static str,
135 pub default_rate: f64,
136 pub currency: &'static str,
137 pub symbol: &'static str,
138 pub tax_invoice_title: &'static str,
139 pub non_registered_title: &'static str,
140 pub tax_id_label: &'static str,
141 pub company_no_label: &'static str,
142 pub date_format: &'static str,
143 pub supports_reverse_charge: bool,
144 pub zero_rate_label: &'static str,
145}
146
147impl TaxProfile {
148 pub fn title(&self, tax_registered: bool) -> &'static str {
149 if tax_registered {
150 self.tax_invoice_title
151 } else {
152 self.non_registered_title
153 }
154 }
155}
156
157pub fn all_profiles() -> Vec<TaxProfile> {
158 vec![
159 Jurisdiction::Sg.profile(),
160 Jurisdiction::Uk.profile(),
161 Jurisdiction::Us.profile(),
162 Jurisdiction::Eu.profile(),
163 Jurisdiction::Custom.profile(),
164 ]
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn sg_gst_defaults() {
173 let p = Jurisdiction::Sg.profile();
174 assert_eq!(p.tax_label, "GST");
175 assert_eq!(p.default_rate, 9.0);
176 assert_eq!(p.currency, "SGD");
177 assert_eq!(p.title(true), "Tax Invoice");
178 assert_eq!(p.title(false), "Invoice");
179 }
180
181 #[test]
182 fn uk_vat_defaults() {
183 let p = Jurisdiction::Uk.profile();
184 assert_eq!(p.tax_label, "VAT");
185 assert_eq!(p.default_rate, 20.0);
186 assert_eq!(p.currency, "GBP");
187 assert_eq!(p.title(true), "VAT Invoice");
188 }
189
190 #[test]
191 fn parses_aliases() {
192 assert_eq!(Jurisdiction::from_str("SG"), Some(Jurisdiction::Sg));
193 assert_eq!(
194 Jurisdiction::from_str("united-kingdom"),
195 Some(Jurisdiction::Uk)
196 );
197 assert_eq!(Jurisdiction::from_str("gb"), Some(Jurisdiction::Uk));
198 assert_eq!(Jurisdiction::from_str("unknown"), None);
199 }
200}