1use std::{fmt, str::FromStr};
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
7
8use crate::{Date, Decimal, Object, ParseError};
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct TaxCode(String);
14
15impl TaxCode {
16 pub fn as_str(&self) -> &str {
18 &self.0
19 }
20}
21
22impl FromStr for TaxCode {
23 type Err = ParseError;
24
25 fn from_str(s: &str) -> Result<Self, Self::Err> {
26 let valid = s.len() <= 64
27 && s.as_bytes().get(2) == Some(&b'-')
28 && s[..2].bytes().all(|b| b.is_ascii_lowercase())
29 && s.len() > 3
30 && s[3..].bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-');
31 if valid { Ok(TaxCode(s.to_owned())) } else { Err(ParseError::TaxCode(s.to_owned())) }
32 }
33}
34
35impl fmt::Display for TaxCode {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 f.write_str(&self.0)
38 }
39}
40
41impl Serialize for TaxCode {
42 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
43 serializer.serialize_str(&self.0)
44 }
45}
46
47impl<'de> Deserialize<'de> for TaxCode {
48 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
49 String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum TaxDirection {
57 Output,
59 Input,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
65pub struct TaxCodeDefinition {
66 pub code: TaxCode,
68 pub direction: TaxDirection,
70 #[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "crate::present")]
72 pub rate: Option<Decimal>,
73 pub law: String,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct TaxTable {
80 pub country: crate::Country,
82 pub valid_from: Date,
84 pub codes: Vec<TaxCodeDefinition>,
86}
87
88impl Object for TaxCodeDefinition {
89 const TYPE: &'static str = "tax_code";
90}
91
92impl Object for TaxTable {
93 const TYPE: &'static str = "tax_table";
94}
95
96impl TaxTable {
97 pub fn de() -> Self {
99 crate::from_root(include_bytes!("../../../spec/data/tax/de.json")).expect("the published table is valid")
100 }
101
102 pub fn get(&self, code: &TaxCode) -> Option<&TaxCodeDefinition> {
104 self.codes.iter().find(|definition| definition.code == *code)
105 }
106
107 pub fn is_valid(&self) -> bool {
109 let prefix = format!("{}-", self.country.as_str().to_ascii_lowercase());
110 let mut codes: Vec<&TaxCode> = self.codes.iter().map(|d| &d.code).collect();
111 codes.sort();
112 codes.windows(2).all(|pair| pair[0] != pair[1]) && codes.iter().all(|c| c.as_str().starts_with(&prefix))
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn tax_codes_name_their_country() {
122 for s in ["de-out-19", "de-in-13b-eu-service", "fr-tva-20"] {
123 assert_eq!(s.parse::<TaxCode>().unwrap().as_str(), s);
124 }
125 for s in ["DE-19", "de19", "de-", "de-Out", "deu-19", ""] {
126 assert!(s.parse::<TaxCode>().is_err(), "{s}");
127 }
128 }
129
130 #[test]
131 fn the_german_table() {
132 let table = TaxTable::de();
133 assert!(table.is_valid());
134 let rate = |code: &str| table.get(&code.parse().unwrap()).and_then(|d| d.rate);
135 assert_eq!(rate("de-out-19"), Some("19".parse().unwrap()));
136 assert_eq!(rate("de-in-7"), Some("7".parse().unwrap()));
137 assert_eq!(rate("de-out-ig-supply"), None);
138 assert_eq!(table.get(&"de-out-small-business".parse().unwrap()).unwrap().direction, TaxDirection::Output);
139 }
140}