Skip to main content

librotenado/
party.rs

1// SPDX-License-Identifier: MIT
2//! Parties (spec part 0008): the organisations and natural persons a set of books belongs to.
3
4use std::{fmt, str::FromStr};
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
7
8use crate::{Money, Object, ParseError};
9
10/// The identity of an organisation or a person: a UUID in its one form (spec part 0001). It carries no meaning.
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct PartyId(String);
13
14impl PartyId {
15    /// The identity as text.
16    pub fn as_str(&self) -> &str {
17        &self.0
18    }
19}
20
21impl FromStr for PartyId {
22    type Err = ParseError;
23
24    fn from_str(s: &str) -> Result<Self, Self::Err> {
25        if crate::is_uuid(s) { Ok(PartyId(s.to_owned())) } else { Err(ParseError::Party(s.to_owned())) }
26    }
27}
28
29impl fmt::Display for PartyId {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        f.write_str(&self.0)
32    }
33}
34
35impl Serialize for PartyId {
36    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
37        serializer.serialize_str(&self.0)
38    }
39}
40
41impl<'de> Deserialize<'de> for PartyId {
42    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
43        String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
44    }
45}
46
47/// A country by its ISO 3166-1 alpha-2 code, e.g. `DE`, `FR`.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub struct Country([u8; 2]);
50
51impl Country {
52    /// The code as text.
53    pub fn as_str(&self) -> &str {
54        // Only ASCII upper-case letters get in (see from_str).
55        std::str::from_utf8(&self.0).unwrap_or("??")
56    }
57}
58
59impl FromStr for Country {
60    type Err = ParseError;
61
62    fn from_str(s: &str) -> Result<Self, Self::Err> {
63        match s.as_bytes() {
64            &[a, b] if a.is_ascii_uppercase() && b.is_ascii_uppercase() => Ok(Country([a, b])),
65            _ => Err(ParseError::Country(s.to_owned())),
66        }
67    }
68}
69
70impl fmt::Display for Country {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        f.write_str(self.as_str())
73    }
74}
75
76impl Serialize for Country {
77    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
78        serializer.serialize_str(self.as_str())
79    }
80}
81
82impl<'de> Deserialize<'de> for Country {
83    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
84        String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
85    }
86}
87
88/// An entry in a public register, e.g. a commercial register.
89#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
90pub struct Registration {
91    /// The register, e.g. `de-hra`, `de-hrb`, `de-vr` (names of registers are listed in the specification).
92    pub register: String,
93    /// The authority that keeps it, e.g. a local court; left out if the register has only one.
94    #[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "crate::present_text")]
95    pub authority: Option<String>,
96    /// The number in the register.
97    pub number: String,
98}
99
100/// A number under which a tax authority knows a party.
101#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
102pub struct TaxId {
103    /// The scheme, e.g. `vat` (an EU VAT identification number, with its country prefix) or `de-steuernummer`.
104    pub scheme: String,
105    /// The number as the authority writes it.
106    pub number: String,
107}
108
109/// A number of shares of one nominal amount: `{"count": 512, "nominal": 2 EUR}` – a share capital of 1,024 euros in
110/// 512 shares of 2 euros. Shares are counted in whole numbers; fractions of the capital follow from the counts.
111#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
112pub struct Shares {
113    /// How many, at least 1.
114    #[serde(deserialize_with = "count")]
115    pub count: u64,
116    /// The nominal amount of each, above zero.
117    #[serde(deserialize_with = "nominal")]
118    pub nominal: Money,
119}
120
121fn count<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u64, D::Error> {
122    match crate::safe_u64(deserializer)? {
123        0 => Err(de::Error::custom("at least one share")),
124        count => Ok(count),
125    }
126}
127
128fn nominal<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Money, D::Error> {
129    let nominal = Money::deserialize(deserializer)?;
130    if nominal.base_units() <= 0 {
131        return Err(de::Error::custom("a nominal amount is above zero"));
132    }
133    Ok(nominal)
134}
135
136/// An organisation: a company, an association, a foundation, a partnership.
137#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
138pub struct Organisation {
139    /// Its identity.
140    pub id: PartyId,
141    /// Its name, as registered.
142    pub name: String,
143    /// Its legal form, e.g. `de-gmbh`, `de-ev`, `fr-sas` (codes are listed in the specification).
144    pub legal_form: String,
145    /// The country whose law it is formed under.
146    pub country: Country,
147    /// Its entries in public registers; left out when none.
148    #[serde(default, skip_serializing_if = "Vec::is_empty", deserialize_with = "crate::non_empty")]
149    pub registrations: Vec<Registration>,
150    /// Its tax numbers; left out when none.
151    #[serde(default, skip_serializing_if = "Vec::is_empty", deserialize_with = "crate::non_empty")]
152    pub tax_ids: Vec<TaxId>,
153    /// How its capital is divided into shares, one entry per nominal amount; left out when it has no shares.
154    #[serde(default, skip_serializing_if = "Vec::is_empty", deserialize_with = "crate::non_empty")]
155    pub shares: Vec<Shares>,
156}
157
158impl Organisation {
159    /// The share capital: the sum of count × nominal amount, if all nominal amounts are in one currency.
160    pub fn capital(&self) -> Option<Money> {
161        let currency = self.shares.first()?.nominal.currency().clone();
162        let mut total: i128 = 0;
163        for shares in &self.shares {
164            if *shares.nominal.currency() != currency {
165                return None;
166            }
167            total = total.checked_add(i128::from(shares.count).checked_mul(shares.nominal.base_units())?)?;
168        }
169        Money::new(total, currency)
170    }
171}
172
173/// What a person is to an organisation.
174#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
175#[serde(rename_all = "snake_case")]
176pub enum RoleKind {
177    /// Sole owner of an organisation without shares, e.g. a sole trader's business.
178    Owner,
179    /// Holds shares of a company.
180    Shareholder,
181    /// Partner of a partnership.
182    Partner,
183    /// Manages and represents it, e.g. a managing director.
184    ManagingDirector,
185    /// Member of its board.
186    BoardMember,
187    /// Member of an association or a cooperative.
188    Member,
189    /// May sign for it, e.g. holds a commercial power of attorney.
190    AuthorisedSignatory,
191    /// A role of a later version; kept as received, not interpreted, and not written by this version.
192    #[serde(other, skip_serializing)]
193    Unknown,
194}
195
196/// A person's role in an organisation.
197#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
198pub struct Role {
199    /// The organisation.
200    pub organisation: PartyId,
201    /// What the person is to it.
202    pub kind: RoleKind,
203    /// The shares held in this role, of one nominal amount the organisation defines; left out where the role has
204    /// none. A person holding shares of several nominal amounts has one role per amount.
205    #[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "held")]
206    pub shares: Option<Shares>,
207}
208
209fn held<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<Shares>, D::Error> {
210    Shares::deserialize(deserializer).map(Some)
211}
212
213/// A natural person.
214#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
215pub struct Person {
216    /// Their identity.
217    pub id: PartyId,
218    /// Their name, as they use it.
219    pub name: String,
220    /// Their tax numbers, e.g. a personal tax identification number; left out when none.
221    #[serde(default, skip_serializing_if = "Vec::is_empty", deserialize_with = "crate::non_empty")]
222    pub tax_ids: Vec<TaxId>,
223    /// Their roles in organisations; left out when none.
224    #[serde(default, skip_serializing_if = "Vec::is_empty", deserialize_with = "crate::non_empty")]
225    pub roles: Vec<Role>,
226}
227
228impl Object for Organisation {
229    const TYPE: &'static str = "organisation";
230}
231
232impl Object for Person {
233    const TYPE: &'static str = "person";
234}
235
236impl Object for Registration {
237    const TYPE: &'static str = "registration";
238}
239
240impl Object for TaxId {
241    const TYPE: &'static str = "tax_id";
242}
243
244impl Object for Shares {
245    const TYPE: &'static str = "shares";
246}
247
248impl Object for Role {
249    const TYPE: &'static str = "role";
250}
251
252#[cfg(test)]
253pub(crate) mod tests {
254    use super::*;
255    use crate::{Money, from_root, to_root};
256
257    pub(crate) fn party(n: u8) -> PartyId {
258        format!("00000000-0000-7000-8000-0000000000{n:02x}").parse().unwrap()
259    }
260
261    pub(crate) fn organisation(n: u8) -> Organisation {
262        Organisation {
263            id: party(n),
264            name: "Example Trading GmbH".into(),
265            legal_form: "de-gmbh".into(),
266            country: "DE".parse().unwrap(),
267            registrations: vec![Registration {
268                register: "de-hrb".into(),
269                authority: Some("Amtsgericht Musterstadt".into()),
270                number: "HRB 12345".into(),
271            }],
272            tax_ids: vec![TaxId { scheme: "vat".into(), number: "DE000000000".into() }],
273            shares: vec![two_euro_shares(512)],
274        }
275    }
276
277    pub(crate) fn two_euro_shares(count: u64) -> Shares {
278        Shares { count, nominal: Money::new(200, "EUR".parse().unwrap()).unwrap() }
279    }
280
281    #[test]
282    fn the_capital_follows_from_the_shares() {
283        assert_eq!(organisation(1).capital(), Money::new(102_400, "EUR".parse().unwrap()));
284        let none = Organisation { shares: vec![], ..organisation(1) };
285        assert_eq!(none.capital(), None);
286    }
287
288    #[test]
289    fn an_organisation_as_a_root_object() {
290        let org = organisation(1);
291        let json = to_root(&org).unwrap();
292        let text = String::from_utf8_lossy(&json);
293        assert!(text.starts_with(r#"{"country":"DE","id":"#) && text.ends_with(r#""type":"organisation"}"#), "{text}");
294        assert_eq!(from_root::<Organisation>(&json).unwrap(), org);
295    }
296
297    #[test]
298    fn a_person_with_roles_and_shares() {
299        let json = br#"{"type":"person","id":"00000000-0000-7000-8000-000000000002","name":"Erika Beispiel",
300            "roles":[{"organisation":"00000000-0000-7000-8000-000000000001","kind":"shareholder",
301                      "shares":{"count":257,"nominal":{"currency":"EUR","base_units":"200"}}},
302                     {"organisation":"00000000-0000-7000-8000-000000000001","kind":"managing_director"}]}"#;
303        let person: Person = from_root(json).unwrap();
304        assert_eq!(person.roles[0].shares, Some(two_euro_shares(257)));
305        assert_eq!(person.roles[1].kind, RoleKind::ManagingDirector);
306        for (from, to) in [("257", "0"), ("257", "2.5"), (r#""200""#, r#""0""#), (r#""200""#, r#""-200""#)] {
307            let bad = String::from_utf8_lossy(json).replace(from, to);
308            assert!(from_root::<Person>(bad.as_bytes()).is_err(), "{to}");
309        }
310    }
311
312    #[test]
313    fn countries_are_two_upper_case_letters() {
314        assert_eq!("DE".parse::<Country>().unwrap().as_str(), "DE");
315        for bad in ["de", "DEU", "D", ""] {
316            assert!(bad.parse::<Country>().is_err(), "{bad}");
317        }
318    }
319}