Skip to main content

idkollen_client/models/
org_number.rs

1use fmt::Display;
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3use std::fmt;
4use thiserror::Error;
5
6/// A validated Swedish organisation number (organisationsnummer).
7///
8/// Accepts `XXXXXX-XXXX` or `XXXXXXXXXX` (10 digits). Validates the Luhn check digit.
9/// Stored in normalised `XXXXXX-XXXX` form; serializes/deserializes as a plain JSON string.
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub struct OrgNumber(String);
12
13#[derive(Debug, Error)]
14#[error("invalid organisation number: {0}")]
15pub struct OrgNumberError(String);
16
17impl OrgNumber {
18    pub fn parse(s: &str) -> Result<Self, OrgNumberError> {
19        let digits = s.chars().filter(|c| c.is_ascii_digit()).collect::<String>();
20
21        if digits.len() != 10 {
22            return Err(OrgNumberError(format!(
23                "must contain exactly 10 digits, got {}",
24                digits.len()
25            )));
26        }
27
28        // Third digit must be ≥ 2 (distinguishes org numbers from personal numbers).
29        if digits.as_bytes()[2] < b'2' {
30            return Err(OrgNumberError(
31                "third digit must be 2 or greater".to_owned(),
32            ));
33        }
34
35        if !luhn10(&digits) {
36            return Err(OrgNumberError("Luhn check failed".to_owned()));
37        }
38
39        Ok(Self(format!("{}-{}", &digits[..6], &digits[6..])))
40    }
41
42    #[inline]
43    #[must_use]
44    pub fn as_str(&self) -> &str {
45        &self.0
46    }
47}
48
49/// Standard Luhn algorithm over a 10-digit ASCII string.
50fn luhn10(s: &str) -> bool {
51    let sum: u32 = s
52        .chars()
53        .enumerate()
54        .map(|(i, c)| {
55            let d = c.to_digit(10).unwrap();
56            let v = if i % 2 == 0 { d * 2 } else { d };
57            if v >= 10 { v - 9 } else { v }
58        })
59        .sum();
60
61    sum.is_multiple_of(10)
62}
63
64impl From<OrgNumber> for String {
65    #[inline]
66    fn from(o: OrgNumber) -> String {
67        o.0
68    }
69}
70
71impl AsRef<str> for OrgNumber {
72    #[inline]
73    fn as_ref(&self) -> &str {
74        &self.0
75    }
76}
77
78impl Display for OrgNumber {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        f.write_str(&self.0)
81    }
82}
83
84impl Serialize for OrgNumber {
85    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
86        self.0.serialize(s)
87    }
88}
89
90impl<'de> Deserialize<'de> for OrgNumber {
91    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
92        let s = String::deserialize(d)?;
93
94        OrgNumber::parse(&s).map_err(serde::de::Error::custom)
95    }
96}