Skip to main content

worker_matcher/
models.rs

1//! Data models for worker demographics and identifiers.
2//!
3//! This module is intentionally **logic-free**: it defines the types that
4//! flow through the matching engine but contains no matching code itself.
5//! See [`crate::matcher`] for the engine and [`crate::normalizer`] for the
6//! text transformations that the matcher applies to these fields.
7//!
8//! All public types here are `Serialize + Deserialize` so they round-trip
9//! through JSON, MessagePack, or any other `serde` format.
10//!
11//! ## Building a worker
12//!
13//! Prefer [`Worker::builder`] over constructing the struct literal — the
14//! builder accepts `impl Into<String>` so call-sites can pass `&str`,
15//! `String`, or owned values interchangeably.
16//!
17//! ```
18//! use worker_matcher::{Gender, Worker};
19//! use chrono::NaiveDate;
20//!
21//! let p = Worker::builder()
22//!     .uk_nhs_number("9434765919")
23//!     .given_name("Dafydd")
24//!     .family_name("Jones")
25//!     .date_of_birth(NaiveDate::from_ymd_opt(1980, 5, 15).unwrap())
26//!     .gender(Gender::Male)
27//!     .build();
28//!
29//! assert_eq!(p.given_name.as_deref(), Some("Dafydd"));
30//! assert_eq!(p.gender, Some(Gender::Male));
31//! ```
32
33use chrono::NaiveDate;
34use serde::{Deserialize, Serialize};
35
36/// Gender/sex classification used to compare two [`Worker`] records.
37///
38/// The four-arm enumeration mirrors common healthcare data dictionaries
39/// (HL7 FHIR `AdministrativeGender`, NHS Data Dictionary `Worker Gender`).
40/// `Other` and `Unknown` are deliberately distinct: `Other` represents a
41/// recorded non-binary value, whereas `Unknown` represents missing data.
42///
43/// # Example
44///
45/// ```
46/// use worker_matcher::Gender;
47///
48/// let g = Gender::Female;
49/// assert_eq!(g, Gender::Female);
50/// assert_ne!(g, Gender::Male);
51/// ```
52///
53/// `Gender` is `Copy`, so it is cheap to pass by value.
54///
55/// ```
56/// # use worker_matcher::Gender;
57/// fn describe(g: Gender) -> &'static str {
58///     match g {
59///         Gender::Male    => "male",
60///         Gender::Female  => "female",
61///         Gender::Other   => "other",
62///         Gender::Unknown => "unknown",
63///     }
64/// }
65/// assert_eq!(describe(Gender::Male), "male");
66/// ```
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
68pub enum Gender {
69    /// Administrative gender recorded as male.
70    Male,
71    /// Administrative gender recorded as female.
72    Female,
73    /// Recorded non-binary or otherwise specified value.
74    Other,
75    /// No gender recorded, or gender intentionally withheld.
76    Unknown,
77}
78
79/// ABO + RhD blood type used as supporting evidence in worker matcher.
80///
81/// Blood type is a **weak positive** signal and a **strong negative**
82/// signal:
83///
84/// - Many people share a blood type (≈38% of the US population is O+),
85///   so agreement alone is not strong evidence of a match.
86/// - Two records with disagreeing blood types almost certainly refer
87///   to **different** people — blood type does not change over a
88///   lifetime (modulo bone-marrow transplant edge cases).
89///
90/// The matcher therefore weights blood type at the same low level as
91/// gender by default (`MatchConfig::blood_type_weight = 0.05`) but the
92/// per-field score in `MatchBreakdown::blood_type_score` is surfaced
93/// for downstream consumers that want to flag disagreement explicitly.
94///
95/// Blood type is **not** an identifying field for `Worker::validate`,
96/// and it is **not** consulted by `deterministic_match` — disagreement
97/// is a soft signal, not a binary disqualifier.
98///
99/// # JSON
100///
101/// Variants serialise as their canonical short form (`"A+"`, `"O-"`,
102/// `"AB+"`, etc.) via `#[serde(rename = …)]`.
103///
104/// ```
105/// use worker_matcher::BloodType;
106/// assert_eq!(serde_json::to_string(&BloodType::APositive).unwrap(), "\"A+\"");
107/// let back: BloodType = serde_json::from_str("\"AB-\"").unwrap();
108/// assert_eq!(back, BloodType::ABNegative);
109/// ```
110///
111/// # Parsing
112///
113/// [`BloodType::parse`] accepts the canonical short forms plus the
114/// most common textual layouts found in real EMR data:
115///
116/// ```
117/// use worker_matcher::BloodType;
118/// assert_eq!(BloodType::parse("A+"),         Some(BloodType::APositive));
119/// assert_eq!(BloodType::parse("a positive"), Some(BloodType::APositive));
120/// assert_eq!(BloodType::parse("AB neg"),     Some(BloodType::ABNegative));
121/// assert_eq!(BloodType::parse("O-"),         Some(BloodType::ONegative));
122/// assert_eq!(BloodType::parse("0+"),         Some(BloodType::OPositive));  // zero/O confusion
123/// assert_eq!(BloodType::parse(""),           None);
124/// assert_eq!(BloodType::parse("Z+"),         None);
125/// ```
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
127pub enum BloodType {
128    /// A positive (A+).
129    #[serde(rename = "A+")]
130    APositive,
131    /// A negative (A−).
132    #[serde(rename = "A-")]
133    ANegative,
134    /// B positive (B+).
135    #[serde(rename = "B+")]
136    BPositive,
137    /// B negative (B−).
138    #[serde(rename = "B-")]
139    BNegative,
140    /// AB positive (AB+). Universal red-cell recipient.
141    #[serde(rename = "AB+")]
142    ABPositive,
143    /// AB negative (AB−). Rare; universal-plasma donor.
144    #[serde(rename = "AB-")]
145    ABNegative,
146    /// O positive (O+). Most common worldwide.
147    #[serde(rename = "O+")]
148    OPositive,
149    /// O negative (O−). Universal red-cell donor.
150    #[serde(rename = "O-")]
151    ONegative,
152}
153
154impl BloodType {
155    /// Canonical short form: `"A+"`, `"A-"`, `"B+"`, `"B-"`, `"AB+"`,
156    /// `"AB-"`, `"O+"`, `"O-"`.
157    ///
158    /// ```
159    /// use worker_matcher::BloodType;
160    /// assert_eq!(BloodType::APositive.as_str(),  "A+");
161    /// assert_eq!(BloodType::ABNegative.as_str(), "AB-");
162    /// ```
163    pub fn as_str(&self) -> &'static str {
164        match self {
165            BloodType::APositive => "A+",
166            BloodType::ANegative => "A-",
167            BloodType::BPositive => "B+",
168            BloodType::BNegative => "B-",
169            BloodType::ABPositive => "AB+",
170            BloodType::ABNegative => "AB-",
171            BloodType::OPositive => "O+",
172            BloodType::ONegative => "O-",
173        }
174    }
175
176    /// Parse a blood-type string, accepting canonical short forms as
177    /// well as the common textual layouts seen in EMR / HL7 data.
178    /// Returns `None` for unparseable, empty, or rare-phenotype input;
179    /// consumers that need to preserve a rare phenotype should store
180    /// the raw string elsewhere.
181    ///
182    /// Accepted shapes (case-insensitive, whitespace tolerated):
183    ///
184    /// - Canonical: `A+`, `A-`, `B+`, `B-`, `AB+`, `AB-`, `O+`, `O-`.
185    /// - Word forms: `A positive`, `A pos`, `A negative`, `A neg`.
186    /// - With sign-separator: `A_pos`, `A-neg`, `AB +`.
187    /// - With zero/O confusion: `0+` is read as `O+`.
188    ///
189    /// ```
190    /// use worker_matcher::BloodType;
191    /// assert_eq!(BloodType::parse("O Negative"), Some(BloodType::ONegative));
192    /// assert_eq!(BloodType::parse("ab+"),        Some(BloodType::ABPositive));
193    /// assert_eq!(BloodType::parse("Bombay"),     None); // rare phenotype, not supported
194    /// ```
195    pub fn parse(s: &str) -> Option<BloodType> {
196        let upper: String = s
197            .trim()
198            .to_uppercase()
199            .chars()
200            .map(|c| if c == '0' { 'O' } else { c })
201            .collect();
202        if upper.is_empty() {
203            return None;
204        }
205        let (group, rest): (&str, &str) = if let Some(r) = upper.strip_prefix("AB") {
206            ("AB", r)
207        } else if let Some(r) = upper.strip_prefix('A') {
208            ("A", r)
209        } else if let Some(r) = upper.strip_prefix('B') {
210            ("B", r)
211        } else if let Some(r) = upper.strip_prefix('O') {
212            ("O", r)
213        } else {
214            return None;
215        };
216        let positive = parse_rh_sign(rest)?;
217        Some(match (group, positive) {
218            ("A", true) => BloodType::APositive,
219            ("A", false) => BloodType::ANegative,
220            ("B", true) => BloodType::BPositive,
221            ("B", false) => BloodType::BNegative,
222            ("AB", true) => BloodType::ABPositive,
223            ("AB", false) => BloodType::ABNegative,
224            ("O", true) => BloodType::OPositive,
225            ("O", false) => BloodType::ONegative,
226            _ => return None,
227        })
228    }
229}
230
231impl std::fmt::Display for BloodType {
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        f.write_str(self.as_str())
234    }
235}
236
237/// Parse the Rhesus-sign portion of a blood-type string after the ABO
238/// group prefix has been stripped. Returns `Some(true)` for positive,
239/// `Some(false)` for negative, `None` for unparseable input.
240fn parse_rh_sign(s: &str) -> Option<bool> {
241    let trimmed = s.trim_start_matches([' ', '\t', '_', '/']).trim();
242    if trimmed.is_empty() {
243        return None;
244    }
245    // Word-form check first: "A POS", "A-NEG", "A_POSITIVE" all reach
246    // here with `trimmed` containing the word (possibly prefixed by a
247    // separator like `-` or `+`). We tolerate one leading sign
248    // character as a separator, since the word itself disambiguates.
249    let word_candidate = trimmed.trim_start_matches(['-', '+']).trim();
250    if word_candidate.starts_with("POSITIVE")
251        || word_candidate.starts_with("POS")
252        || word_candidate == "P"
253    {
254        return Some(true);
255    }
256    if word_candidate.starts_with("NEGATIVE")
257        || word_candidate.starts_with("NEG")
258        || word_candidate == "N"
259    {
260        return Some(false);
261    }
262    // Single-character sign forms (with optional `VE` suffix).
263    if let Some(after) = trimmed.strip_prefix('+') {
264        let tail = after.trim().trim_start_matches("VE");
265        if tail.trim().is_empty() {
266            return Some(true);
267        }
268        return None;
269    }
270    if let Some(after) = trimmed.strip_prefix('-') {
271        let tail = after.trim().trim_start_matches("VE");
272        if tail.trim().is_empty() {
273            return Some(false);
274        }
275        return None;
276    }
277    None
278}
279
280/// Physical address used as supporting evidence in worker matcher.
281///
282/// All fields are `Option<String>` so partial addresses are first-class —
283/// a record with only a postcode is still useful for matching.
284///
285/// The matcher does **not** weight every component equally; see
286/// [`crate::matcher::MatchingEngine`] for the weighted comparison rules.
287///
288/// # Example
289///
290/// ```
291/// use worker_matcher::Address;
292///
293/// let mut addr = Address::new();
294/// addr.line1    = Some("10 Downing Street".into());
295/// addr.city     = Some("London".into());
296/// addr.postcode = Some("SW1A 2AA".into());
297///
298/// assert_eq!(addr.postcode.as_deref(), Some("SW1A 2AA"));
299/// assert!(addr.country.is_none());
300/// ```
301///
302/// `Address` is JSON round-trippable.
303///
304/// ```
305/// # use worker_matcher::Address;
306/// let mut a = Address::new();
307/// a.postcode = Some("CF10 1AA".into());
308///
309/// let json = serde_json::to_string(&a).unwrap();
310/// let back: Address = serde_json::from_str(&json).unwrap();
311/// assert_eq!(a, back);
312/// ```
313#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
314#[non_exhaustive]
315pub struct Address {
316    /// First line — typically house number and street, e.g. `"10 Downing Street"`.
317    pub line1: Option<String>,
318    /// Second line — typically flat, locality, or care-of details.
319    pub line2: Option<String>,
320    /// Town or city, e.g. `"Cardiff"`.
321    pub city: Option<String>,
322    /// County or administrative region, e.g. `"South Glamorgan"`.
323    pub county: Option<String>,
324    /// Postal code, e.g. `"CF10 1AA"`. Compared after whitespace normalisation.
325    pub postcode: Option<String>,
326    /// Country, e.g. `"Wales"` or `"United Kingdom"`.
327    pub country: Option<String>,
328}
329
330impl Address {
331    /// Construct an empty address with every field set to `None`.
332    ///
333    /// # Example
334    ///
335    /// ```
336    /// use worker_matcher::Address;
337    ///
338    /// let a = Address::new();
339    /// assert!(a.line1.is_none());
340    /// assert!(a.postcode.is_none());
341    /// ```
342    pub fn new() -> Self {
343        Self {
344            line1: None,
345            line2: None,
346            city: None,
347            county: None,
348            postcode: None,
349            country: None,
350        }
351    }
352
353    /// Fluent setter for `line1`.
354    ///
355    /// ```
356    /// use worker_matcher::Address;
357    /// let a = Address::new().with_line1("10 Downing Street");
358    /// assert_eq!(a.line1.as_deref(), Some("10 Downing Street"));
359    /// ```
360    pub fn with_line1(mut self, value: impl Into<String>) -> Self {
361        self.line1 = Some(value.into());
362        self
363    }
364
365    /// Fluent setter for `line2`.
366    pub fn with_line2(mut self, value: impl Into<String>) -> Self {
367        self.line2 = Some(value.into());
368        self
369    }
370
371    /// Fluent setter for `city`.
372    pub fn with_city(mut self, value: impl Into<String>) -> Self {
373        self.city = Some(value.into());
374        self
375    }
376
377    /// Fluent setter for `county`.
378    pub fn with_county(mut self, value: impl Into<String>) -> Self {
379        self.county = Some(value.into());
380        self
381    }
382
383    /// Fluent setter for `postcode`.
384    ///
385    /// ```
386    /// use worker_matcher::Address;
387    /// let a = Address::new().with_postcode("CF10 1AA");
388    /// assert_eq!(a.postcode.as_deref(), Some("CF10 1AA"));
389    /// ```
390    pub fn with_postcode(mut self, value: impl Into<String>) -> Self {
391        self.postcode = Some(value.into());
392        self
393    }
394
395    /// Fluent setter for `country`.
396    pub fn with_country(mut self, value: impl Into<String>) -> Self {
397        self.country = Some(value.into());
398        self
399    }
400}
401
402impl Default for Address {
403    /// Identical to [`Address::new`].
404    fn default() -> Self {
405        Self::new()
406    }
407}
408
409/// A passport book — country of issue, book number, and optional
410/// effective date range.
411///
412/// Passport data has three properties that make it a poor fit for the
413/// crate's per-scheme `Option<String>` national-identifier pattern,
414/// and which this type captures explicitly:
415///
416/// 1. **Scheme-local provenance.** A passport book number is only
417///    meaningful alongside its issuing country. The book number
418///    `"AB123456"` issued by the United Kingdom is a different
419///    identifier from `"AB123456"` issued by the United States; the
420///    matcher MUST NOT cross-match them. Provenance lives on the
421///    [`PassportBook::country`] field, not on the field name.
422/// 2. **Multi-country.** A single worker may hold passports from
423///    multiple countries simultaneously (dual / multiple citizenship).
424///    A `Vec<PassportBook>` lets a [`crate::Worker`] carry one entry
425///    per book without privileging any particular jurisdiction.
426/// 3. **Time-varying.** When a passport is renewed, the new book has
427///    a different number; the old book number is no longer current
428///    but the worker is unchanged. Worker records may carry the
429///    current book, prior books, or both. Matching treats any shared
430///    `(country, number)` pair across the two records as evidence
431///    that the records refer to the same worker, regardless of issue
432///    date.
433///
434/// Construction via [`PassportBook::new`] canonicalises both the
435/// country (trimmed, uppercased; must be exactly 2 ASCII letters) and
436/// the number (whitespace stripped, letters uppercased) so two records
437/// carrying different textual layouts of the same book canonicalise to
438/// the same `(country, number)` key. Date fields are optional metadata
439/// and are **not** used in matching — they exist for downstream
440/// display and audit. Per-country structural validation is
441/// intentionally not performed; passport formats vary widely and a
442/// case+whitespace canonical form is sufficient for matching.
443///
444/// # Example
445///
446/// ```
447/// use worker_matcher::PassportBook;
448/// use chrono::NaiveDate;
449///
450/// let book = PassportBook::new("gb", " 123 456 789 ")
451///     .expect("valid book")
452///     .with_issued(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap())
453///     .with_expires(NaiveDate::from_ymd_opt(2030, 1, 1).unwrap());
454///
455/// assert_eq!(book.country, "GB");
456/// assert_eq!(book.number,  "123456789");
457/// assert!(book.issued.is_some());
458///
459/// // Rejection: country must be exactly 2 ASCII letters.
460/// assert!(PassportBook::new("GBR", "123").is_none());
461/// assert!(PassportBook::new("1A",  "123").is_none());
462/// // Rejection: number must canonicalise to a non-empty string.
463/// assert!(PassportBook::new("GB",  "   ").is_none());
464/// ```
465#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
466pub struct PassportBook {
467    /// ISO 3166-1 alpha-2 country code of issuance, uppercased.
468    pub country: String,
469    /// Passport book number, whitespace-stripped and uppercased.
470    pub number: String,
471    /// Optional issue date (not used in matching).
472    #[serde(default)]
473    pub issued: Option<NaiveDate>,
474    /// Optional expiry date (not used in matching).
475    #[serde(default)]
476    pub expires: Option<NaiveDate>,
477}
478
479impl PassportBook {
480    /// Construct a passport book, validating and canonicalising the
481    /// country code (trimmed + uppercased; must be exactly 2 ASCII
482    /// letters) and the book number (whitespace stripped + uppercased;
483    /// must be non-empty after stripping).
484    ///
485    /// Returns `None` for an invalid country code or an empty
486    /// canonical number.
487    ///
488    /// ```
489    /// use worker_matcher::PassportBook;
490    /// let b = PassportBook::new("us", "abc 123 456").unwrap();
491    /// assert_eq!(b.country, "US");
492    /// assert_eq!(b.number,  "ABC123456");
493    /// ```
494    pub fn new(country: impl AsRef<str>, number: impl AsRef<str>) -> Option<Self> {
495        let country = country.as_ref().trim().to_ascii_uppercase();
496        if country.len() != 2 || !country.chars().all(|c| c.is_ascii_alphabetic()) {
497            return None;
498        }
499        // Strip common data-entry separators (whitespace, ASCII
500        // hyphens, periods, slashes) and uppercase. This matches the
501        // canonicalisation used by `parse_es_tsi` / `parse_ie_ihi` so
502        // textual variants of the same book canonicalise identically.
503        let number: String = number
504            .as_ref()
505            .chars()
506            .filter(|c| !c.is_whitespace() && !matches!(*c, '-' | '.' | '/'))
507            .collect::<String>()
508            .to_uppercase();
509        if number.is_empty() {
510            return None;
511        }
512        Some(Self {
513            country,
514            number,
515            issued: None,
516            expires: None,
517        })
518    }
519
520    /// Attach an issue date. The date is metadata only — it is NOT
521    /// used in matching.
522    ///
523    /// ```
524    /// use worker_matcher::PassportBook;
525    /// use chrono::NaiveDate;
526    /// let b = PassportBook::new("GB", "123456789").unwrap()
527    ///     .with_issued(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
528    /// assert!(b.issued.is_some());
529    /// ```
530    pub fn with_issued(mut self, date: NaiveDate) -> Self {
531        self.issued = Some(date);
532        self
533    }
534
535    /// Attach an expiry date. The date is metadata only — it is NOT
536    /// used in matching.
537    ///
538    /// ```
539    /// use worker_matcher::PassportBook;
540    /// use chrono::NaiveDate;
541    /// let b = PassportBook::new("GB", "123456789").unwrap()
542    ///     .with_expires(NaiveDate::from_ymd_opt(2030, 1, 1).unwrap());
543    /// assert!(b.expires.is_some());
544    /// ```
545    pub fn with_expires(mut self, date: NaiveDate) -> Self {
546        self.expires = Some(date);
547        self
548    }
549}
550
551/// Core worker demographic data structure.
552///
553/// Every field is optional. The matcher tolerates missing data field-by-field
554/// — a `None` value never penalises a worker. See
555/// [`crate::matcher::MatchingEngine::match_workers`] for how missing fields
556/// affect the weighted score.
557///
558/// Construct via [`Worker::builder`] rather than struct literal syntax so
559/// the call-site stays compact and forward-compatible if fields are added.
560///
561/// # Example
562///
563/// ```
564/// use worker_matcher::{Gender, Worker};
565/// use chrono::NaiveDate;
566///
567/// let p = Worker::builder()
568///     .given_name("Siân")
569///     .family_name("Evans")
570///     .date_of_birth(NaiveDate::from_ymd_opt(1990, 3, 10).unwrap())
571///     .gender(Gender::Female)
572///     .build();
573///
574/// assert_eq!(p.given_name.as_deref(), Some("Siân"));
575/// assert!(p.uk_nhs_number.is_none());
576/// ```
577///
578/// `Worker` round-trips through `serde`.
579///
580/// ```
581/// # use worker_matcher::Worker;
582/// let p = Worker::builder().given_name("Test").family_name("Worker").build();
583/// let json = serde_json::to_string(&p).unwrap();
584/// let back: Worker = serde_json::from_str(&json).unwrap();
585/// assert_eq!(p, back);
586/// ```
587#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
588#[non_exhaustive]
589pub struct Worker {
590    /// United Kingdom NHS Number (England, Wales, Isle of Man) — a 10-digit
591    /// Modulus-11 identifier parsed via
592    /// [`crate::identifiers::parse_uk_nhs_number`]. Whitespace tolerated in
593    /// the spaced `"XXX XXX XXXX"` layout.
594    #[serde(default)]
595    pub uk_nhs_number: Option<String>,
596
597    /// France NIR (*Numéro d'Inscription au Répertoire*) — the 15-character
598    /// national identifier with a Modulus-97 check key. Parsed via
599    /// [`crate::identifiers::parse_fr_nir`].
600    #[serde(default)]
601    pub fr_nir: Option<String>,
602
603    /// España (Spain) TSI (*Tarjeta Sanitaria Individual*) / CIP-SNS — the
604    /// national healthcare identifier with regionally-varying format. Parsed
605    /// via [`crate::identifiers::parse_es_tsi`].
606    #[serde(default)]
607    pub es_tsi: Option<String>,
608
609    /// Éire (Ireland) IHI (Individual Health Identifier) — the 7-digit
610    /// healthcare identifier issued under the Health Identifiers Act 2014.
611    /// Parsed via [`crate::identifiers::parse_ie_ihi`].
612    #[serde(default)]
613    pub ie_ihi: Option<String>,
614
615    /// United Kingdom Northern Ireland H&C Number (Health and Care Number)
616    /// — a 10-digit Modulus-11 identifier issued by HSC. Shares the NHS
617    /// Number algorithm. Parsed via
618    /// [`crate::identifiers::parse_uk_hc_number`].
619    #[serde(default)]
620    pub uk_hc_number: Option<String>,
621
622    /// United States Social Security Number (SSN) — a 9-digit identifier
623    /// issued by the Social Security Administration. Parsed via
624    /// [`crate::identifiers::parse_us_ssn`].
625    #[serde(default)]
626    pub us_ssn: Option<String>,
627
628    /// Australia IHI (Individual Healthcare Identifier) — 16-digit
629    /// identifier issued by the Healthcare Identifiers Service. Parsed
630    /// via [`crate::identifiers::parse_au_ihi`].
631    #[serde(default)]
632    pub au_ihi: Option<String>,
633
634    /// Germany KVNR (*Krankenversichertennummer*) — 10-character
635    /// (letter + 9 digits) lifelong health-insurance number. Parsed via
636    /// [`crate::identifiers::parse_de_kvnr`].
637    #[serde(default)]
638    pub de_kvnr: Option<String>,
639
640    /// Italy *Codice Fiscale* (CF) — 16-character alphanumeric
641    /// identifier issued by the tax authority. Parsed via
642    /// [`crate::identifiers::parse_it_cf`].
643    #[serde(default)]
644    pub it_cf: Option<String>,
645
646    /// Netherlands BSN (*Burgerservicenummer*) — 9-digit citizen-service
647    /// number used by Dutch authorities and healthcare providers. Parsed
648    /// via [`crate::identifiers::parse_nl_bsn`].
649    #[serde(default)]
650    pub nl_bsn: Option<String>,
651
652    /// Sweden *Workernummer* — 10- or 12-digit workeral identity number
653    /// (`YYMMDDNNNC` or `YYYYMMDDNNNC` with optional `-` / `+`
654    /// separator). Parsed via
655    /// [`crate::identifiers::parse_se_workernummer`].
656    #[serde(default)]
657    pub se_workernummer: Option<String>,
658
659    /// United Kingdom (Scotland) CHI Number (Community Health Index) —
660    /// 10-digit identifier used by NHS Scotland. Shares the Mod-11
661    /// algorithm of the NHS Number but is scheme-local. Parsed via
662    /// [`crate::identifiers::parse_uk_chi_number`].
663    #[serde(default)]
664    pub uk_chi_number: Option<String>,
665
666    /// Belgium National Number (*Rijksregisternummer*). 11 digits, Mod-97.
667    /// Parsed via [`crate::identifiers::parse_be_nn`].
668    #[serde(default)]
669    pub be_nn: Option<String>,
670
671    /// Bulgaria EGN (*Edinen grazhdanski nomer*). 10 digits, weighted Mod-11.
672    /// Parsed via [`crate::identifiers::parse_bg_egn`].
673    #[serde(default)]
674    pub bg_egn: Option<String>,
675
676    /// Czech Republic *Rodné číslo*. 9 or 10 digits (10-digit divisible by 11).
677    /// Parsed via [`crate::identifiers::parse_cz_rc`].
678    #[serde(default)]
679    pub cz_rc: Option<String>,
680
681    /// Denmark CPR (*Centrale Workerregister*). 10 digits, format only.
682    /// Parsed via [`crate::identifiers::parse_dk_cpr`].
683    #[serde(default)]
684    pub dk_cpr: Option<String>,
685
686    /// Estonia *Isikukood* (Workeral Identification Code). 11 digits, cascading Mod-11.
687    /// Parsed via [`crate::identifiers::parse_ee_ik`].
688    #[serde(default)]
689    pub ee_ik: Option<String>,
690
691    /// Spain DNI / NIE. 8 digits + Mod-23 control letter (NIE prefixed X/Y/Z).
692    /// Parsed via [`crate::identifiers::parse_es_dni`].
693    #[serde(default)]
694    pub es_dni: Option<String>,
695
696    /// Finland HETU (*Henkilötunnus*). 11 chars with century sign + Mod-31 check.
697    /// Parsed via [`crate::identifiers::parse_fi_hetu`].
698    #[serde(default)]
699    pub fi_hetu: Option<String>,
700
701    /// Croatia OIB (*Osobni identifikacijski broj*). 11 digits, ISO 7064 MOD 11,10.
702    /// Parsed via [`crate::identifiers::parse_hr_oib`].
703    #[serde(default)]
704    pub hr_oib: Option<String>,
705
706    /// Iceland *Kennitala*. 10 digits, weighted Mod-11.
707    /// Parsed via [`crate::identifiers::parse_is_kt`].
708    #[serde(default)]
709    pub is_kt: Option<String>,
710
711    /// Lithuania *Asmens kodas*. 11 digits, cascading Mod-11 (same algorithm as Estonia).
712    /// Parsed via [`crate::identifiers::parse_lt_ak`].
713    #[serde(default)]
714    pub lt_ak: Option<String>,
715
716    /// Latvia *Workeras kods*. 11 digits, weighted Mod-11.
717    /// Parsed via [`crate::identifiers::parse_lv_pk`].
718    #[serde(default)]
719    pub lv_pk: Option<String>,
720
721    /// Malta National ID. 7 digits + letter in `{M, G, A, P, L, H, B, Z}`.
722    /// Parsed via [`crate::identifiers::parse_mt_id`].
723    #[serde(default)]
724    pub mt_id: Option<String>,
725
726    /// Norway *Fødselsnummer*. 11 digits, dual Mod-11.
727    /// Parsed via [`crate::identifiers::parse_no_fnr`].
728    #[serde(default)]
729    pub no_fnr: Option<String>,
730
731    /// Poland PESEL. 11 digits, weighted Mod-10.
732    /// Parsed via [`crate::identifiers::parse_pl_pesel`].
733    #[serde(default)]
734    pub pl_pesel: Option<String>,
735
736    /// Romania CNP (*Cod Numeric Workeral*). 13 digits, weighted Mod-11.
737    /// Parsed via [`crate::identifiers::parse_ro_cnp`].
738    #[serde(default)]
739    pub ro_cnp: Option<String>,
740
741    /// Slovenia EMŠO (*Enotna Matična Številka Občana*). 13 digits, weighted Mod-11.
742    /// Parsed via [`crate::identifiers::parse_si_emso`].
743    #[serde(default)]
744    pub si_emso: Option<String>,
745
746    /// Slovakia *Rodné číslo*. 9 or 10 digits (same algorithm as Czech RČ).
747    /// Parsed via [`crate::identifiers::parse_sk_rc`].
748    #[serde(default)]
749    pub sk_rc: Option<String>,
750
751    /// United Kingdom National Insurance Number (NINO). Format `AA999999A`
752    /// with banned prefixes and `{A,B,C,D}` suffix.
753    /// Parsed via [`crate::identifiers::parse_uk_nino`].
754    #[serde(default)]
755    pub uk_nino: Option<String>,
756
757    /// Greece DSS (Dematerialised Securities System) investor share code.
758    /// 10-digit identifier issued by the Hellenic Central Securities
759    /// Depository (ATHEXCSD). Parsed via
760    /// [`crate::identifiers::parse_gr_dss`].
761    #[serde(default)]
762    pub gr_dss: Option<String>,
763
764    /// Liechtenstein National Identity Card Number. 2 letters + 8 digits
765    /// (per the spec) or 2 letters + 9 digits (per the spec's example).
766    /// Note: the LI ID card number is **regenerated on each renewal**, so
767    /// consumers that need stable cross-renewal matching should prefer
768    /// [`PassportBook`] with `country = "LI"`. Parsed via
769    /// [`crate::identifiers::parse_li_id`].
770    #[serde(default)]
771    pub li_id: Option<String>,
772
773    /// Netherlands National Identity Card Number. 9 characters: positions
774    /// 1–2 are uppercase letters except `O`; positions 3–8 are
775    /// alphanumeric except `O`; position 9 is a digit. Distinct from the
776    /// BSN (citizen-service number), which is permanent — this ID-card
777    /// number changes with each renewed card.
778    /// Parsed via [`crate::identifiers::parse_nl_id`].
779    #[serde(default)]
780    pub nl_id: Option<String>,
781
782    /// Poland NIP (*Numer Identyfikacji Podatkowej*) tax identification
783    /// number. 10 digits, weighted Mod-11 check. Parsed via
784    /// [`crate::identifiers::parse_pl_nip`].
785    #[serde(default)]
786    pub pl_nip: Option<String>,
787
788    /// Portugal NIF (*Número de Identificação Fiscal*) tax identification
789    /// number. 9 digits, weighted Mod-11 check. Parsed via
790    /// [`crate::identifiers::parse_pt_nif`].
791    #[serde(default)]
792    pub pt_nif: Option<String>,
793
794    /// Brazil CPF (*Cadastro de Pessoas Físicas*). 11-digit national tax /
795    /// identification number with two Mod-11 check digits. Parsed at match
796    /// time via [`crate::identifiers::parse_br_cpf`].
797    #[serde(default)]
798    pub br_cpf: Option<String>,
799
800    /// China Resident Identity Card number (*居民身份证*) — 18-character
801    /// 1999 reform format (17 digits + check character). Parsed at match
802    /// time via [`crate::identifiers::parse_cn_rrn`].
803    #[serde(default)]
804    pub cn_rrn: Option<String>,
805
806    /// India Aadhaar number. 12 digits with Verhoeff check digit. Parsed
807    /// at match time via [`crate::identifiers::parse_in_aadhaar`].
808    #[serde(default)]
809    pub in_aadhaar: Option<String>,
810
811    /// Japan My Number (*個人番号*). 12-digit workeral identification
812    /// number with weighted Mod-11 check digit. Parsed at match time via
813    /// [`crate::identifiers::parse_jp_my_number`].
814    #[serde(default)]
815    pub jp_my_number: Option<String>,
816
817    /// Mexico CURP (*Clave Única de Registro de Población*). 18-character
818    /// alphanumeric identifier encoding name initials, date of birth,
819    /// sex, state, and a check digit. Parsed at match time via
820    /// [`crate::identifiers::parse_mx_curp`].
821    #[serde(default)]
822    pub mx_curp: Option<String>,
823
824    /// New Zealand NHI (National Health Index) number. Original 7-character
825    /// format (3 letters + 4 digits, Mod-11 check digit). The 2019
826    /// alphanumeric NHI revision is not supported by the parser. Parsed at
827    /// match time via [`crate::identifiers::parse_nz_nhi`].
828    #[serde(default)]
829    pub nz_nhi: Option<String>,
830
831    /// South Africa ID Number. 13 digits encoding date of birth, sequence,
832    /// citizenship, and a Luhn check digit. Parsed at match time via
833    /// [`crate::identifiers::parse_za_id`].
834    #[serde(default)]
835    pub za_id: Option<String>,
836
837    /// Given name (sometimes called "first name" or "forename").
838    pub given_name: Option<String>,
839
840    /// Middle name(s). Currently unused in scoring — see spec OQ-1.
841    pub middle_name: Option<String>,
842
843    /// Family name (sometimes called "surname" or "last name").
844    pub family_name: Option<String>,
845
846    /// Date of birth. Compared by exact equality.
847    pub date_of_birth: Option<NaiveDate>,
848
849    /// Date of death (FHIR `Patient.deceasedDateTime`). Compared using
850    /// the same DOB transposition heuristic as
851    /// [`Worker::date_of_birth`] — DD/MM ↔ MM/DD data-entry bugs are
852    /// just as common in death records as in birth records.
853    #[serde(default)]
854    pub death_date: Option<NaiveDate>,
855
856    /// Administrative gender. See [`Gender`].
857    pub gender: Option<Gender>,
858
859    /// ABO+RhD blood type. Stable for life, so disagreement is strong
860    /// evidence against a match; agreement is a weak positive signal
861    /// (many people share a blood type). See [`BloodType`] for the
862    /// scoring contract.
863    #[serde(default)]
864    pub blood_type: Option<BloodType>,
865
866    /// FHIR `Patient.multipleBirth` — birth order in a multiple-birth
867    /// set (twin / triplet / etc.). Convention:
868    ///
869    /// - `None` — unknown, not recorded, or singleton (the matcher
870    ///   treats `None` as "no signal" and skips the field).
871    /// - `Some(n)` with `n >= 1` — the `n`-th birth in a multiple-birth
872    ///   set (1-indexed).
873    ///
874    /// Used to distinguish identical twins who otherwise share name,
875    /// DOB, address, and demographic data. Disagreement (e.g. `Some(1)`
876    /// vs `Some(2)`) is reliable evidence the records refer to
877    /// different people in the same multiple-birth set.
878    #[serde(default)]
879    pub multiple_birth: Option<u8>,
880
881    /// Current residential address.
882    pub address: Option<Address>,
883
884    /// Place of birth. Modelled as an [`Address`] for FHIR
885    /// (`Patient.birthPlace`) parity — typically only `city` and
886    /// `country` are populated in practice. Stable over a lifetime
887    /// (modulo refugee / adoption edge cases), so disagreement on a
888    /// populated value is informative. Scored independently from the
889    /// current `address` field.
890    #[serde(default)]
891    pub birth_place: Option<Address>,
892
893    /// Place of death. Modelled as an [`Address`] (parallel to
894    /// [`Worker::birth_place`]) — typically only `city` and `country`
895    /// are populated. Useful for disambiguating records of deceased
896    /// workers (e.g. distinguishing two people with the same name and
897    /// DOB by where they died). Scored independently from `address`
898    /// and `birth_place`.
899    #[serde(default)]
900    pub death_place: Option<Address>,
901
902    /// Previous residential addresses. Used by the address sub-score
903    /// (best-of cartesian product across `address ∪ previous_addresses`
904    /// on both sides; see spec §12.4.2).
905    pub previous_addresses: Vec<Address>,
906
907    /// Passport books held by the worker. A single worker may hold
908    /// passports from multiple countries simultaneously and may
909    /// accumulate historical book numbers as old passports are
910    /// renewed; this `Vec` carries every book ever recorded on the
911    /// worker (current and historical) without privileging any
912    /// particular jurisdiction. Matching treats any shared
913    /// `(country, number)` pair across the two workers' lists as
914    /// evidence that the records refer to the same worker, regardless
915    /// of issue date. See [`PassportBook`].
916    #[serde(default)]
917    pub passport_books: Vec<PassportBook>,
918
919    /// Primary phone number. Falls back to [`Self::mobile`] in scoring if absent.
920    pub phone: Option<String>,
921
922    /// Mobile phone number. Used as the fallback for [`Self::phone`].
923    pub mobile: Option<String>,
924
925    /// Email address. Not currently used in scoring (see spec task T-11).
926    pub email: Option<String>,
927
928    /// Local hospital or practice identifier. Not normalised — different
929    /// organisations may issue colliding values.
930    pub local_id: Option<String>,
931}
932
933impl Worker {
934    /// Begin constructing a [`Worker`] with the [`WorkerBuilder`].
935    ///
936    /// All fields default to `None` / empty until a setter is called.
937    ///
938    /// # Example
939    ///
940    /// ```
941    /// use worker_matcher::Worker;
942    ///
943    /// let p = Worker::builder()
944    ///     .given_name("John")
945    ///     .family_name("Smith")
946    ///     .build();
947    ///
948    /// assert_eq!(p.family_name.as_deref(), Some("Smith"));
949    /// ```
950    pub fn builder() -> WorkerBuilder {
951        WorkerBuilder::default()
952    }
953
954    /// Validate that the worker carries at least one identifying field.
955    ///
956    /// Returns `Ok(())` if any of the following is set: a name (`given_name`
957    /// or `family_name`), or any national identifier (`uk_nhs_number`,
958    /// `fr_nir`, `es_tsi`, `ie_ihi`, `uk_hc_number`). Otherwise returns
959    /// [`crate::MatchingError::MissingField`].
960    ///
961    /// This is **not** invoked automatically by the matcher — call it at the
962    /// system boundary when you ingest data, not on every comparison.
963    ///
964    /// # Example
965    ///
966    /// ```
967    /// use worker_matcher::Worker;
968    ///
969    /// assert!(Worker::builder().given_name("Ada").build().validate().is_ok());
970    /// assert!(Worker::builder().uk_nhs_number("9434765919").build().validate().is_ok());
971    /// assert!(Worker::builder().ie_ihi("1234567").build().validate().is_ok());
972    /// assert!(Worker::builder().us_ssn("123-45-6789").build().validate().is_ok());
973    /// assert!(Worker::builder().de_kvnr("A123456780").build().validate().is_ok());
974    /// assert!(
975    ///     Worker::builder()
976    ///         .add_passport_book(worker_matcher::PassportBook::new("GB", "123456789").unwrap())
977    ///         .build()
978    ///         .validate()
979    ///         .is_ok()
980    /// );
981    /// assert!(Worker::builder().build().validate().is_err());
982    /// ```
983    pub fn validate(&self) -> crate::Result<()> {
984        let has_name = self.given_name.is_some() || self.family_name.is_some();
985        let has_identifier = self.uk_nhs_number.is_some()
986            || self.fr_nir.is_some()
987            || self.es_tsi.is_some()
988            || self.ie_ihi.is_some()
989            || self.uk_hc_number.is_some()
990            || self.us_ssn.is_some()
991            || self.au_ihi.is_some()
992            || self.de_kvnr.is_some()
993            || self.it_cf.is_some()
994            || self.nl_bsn.is_some()
995            || self.se_workernummer.is_some()
996            || self.uk_chi_number.is_some()
997            || self.be_nn.is_some()
998            || self.bg_egn.is_some()
999            || self.cz_rc.is_some()
1000            || self.dk_cpr.is_some()
1001            || self.ee_ik.is_some()
1002            || self.es_dni.is_some()
1003            || self.fi_hetu.is_some()
1004            || self.hr_oib.is_some()
1005            || self.is_kt.is_some()
1006            || self.lt_ak.is_some()
1007            || self.lv_pk.is_some()
1008            || self.mt_id.is_some()
1009            || self.no_fnr.is_some()
1010            || self.pl_pesel.is_some()
1011            || self.ro_cnp.is_some()
1012            || self.si_emso.is_some()
1013            || self.sk_rc.is_some()
1014            || self.uk_nino.is_some()
1015            || self.gr_dss.is_some()
1016            || self.li_id.is_some()
1017            || self.nl_id.is_some()
1018            || self.pl_nip.is_some()
1019            || self.pt_nif.is_some()
1020            || self.br_cpf.is_some()
1021            || self.cn_rrn.is_some()
1022            || self.in_aadhaar.is_some()
1023            || self.jp_my_number.is_some()
1024            || self.mx_curp.is_some()
1025            || self.nz_nhi.is_some()
1026            || self.za_id.is_some()
1027            || !self.passport_books.is_empty();
1028        if !has_name && !has_identifier {
1029            return Err(crate::MatchingError::MissingField(
1030                "At least one of: a name, a national identifier (any of 30 supported schemes), or at least one passport book is required"
1031                    .to_string(),
1032            ));
1033        }
1034        Ok(())
1035    }
1036}
1037
1038/// Fluent builder for [`Worker`].
1039///
1040/// All setters accept `impl Into<String>` so call-sites may pass `&str`,
1041/// `String`, or `&String` interchangeably without explicit conversion.
1042///
1043/// # Example
1044///
1045/// ```
1046/// use worker_matcher::{Gender, Worker, WorkerBuilder};
1047/// use chrono::NaiveDate;
1048///
1049/// let p: Worker = WorkerBuilder::default()
1050///     .uk_nhs_number("9434765919")
1051///     .given_name(String::from("Owen"))   // owned String
1052///     .family_name("Williams")            // &str
1053///     .date_of_birth(NaiveDate::from_ymd_opt(1972, 11, 4).unwrap())
1054///     .gender(Gender::Male)
1055///     .build();
1056///
1057/// assert_eq!(p.uk_nhs_number.as_deref(), Some("9434765919"));
1058/// ```
1059#[derive(Default)]
1060pub struct WorkerBuilder {
1061    uk_nhs_number: Option<String>,
1062    fr_nir: Option<String>,
1063    es_tsi: Option<String>,
1064    ie_ihi: Option<String>,
1065    uk_hc_number: Option<String>,
1066    us_ssn: Option<String>,
1067    au_ihi: Option<String>,
1068    de_kvnr: Option<String>,
1069    it_cf: Option<String>,
1070    nl_bsn: Option<String>,
1071    se_workernummer: Option<String>,
1072    uk_chi_number: Option<String>,
1073    be_nn: Option<String>,
1074    bg_egn: Option<String>,
1075    cz_rc: Option<String>,
1076    dk_cpr: Option<String>,
1077    ee_ik: Option<String>,
1078    es_dni: Option<String>,
1079    fi_hetu: Option<String>,
1080    hr_oib: Option<String>,
1081    is_kt: Option<String>,
1082    lt_ak: Option<String>,
1083    lv_pk: Option<String>,
1084    mt_id: Option<String>,
1085    no_fnr: Option<String>,
1086    pl_pesel: Option<String>,
1087    ro_cnp: Option<String>,
1088    si_emso: Option<String>,
1089    sk_rc: Option<String>,
1090    uk_nino: Option<String>,
1091    gr_dss: Option<String>,
1092    li_id: Option<String>,
1093    nl_id: Option<String>,
1094    pl_nip: Option<String>,
1095    pt_nif: Option<String>,
1096    br_cpf: Option<String>,
1097    cn_rrn: Option<String>,
1098    in_aadhaar: Option<String>,
1099    jp_my_number: Option<String>,
1100    mx_curp: Option<String>,
1101    nz_nhi: Option<String>,
1102    za_id: Option<String>,
1103    given_name: Option<String>,
1104    middle_name: Option<String>,
1105    family_name: Option<String>,
1106    date_of_birth: Option<NaiveDate>,
1107    death_date: Option<NaiveDate>,
1108    gender: Option<Gender>,
1109    blood_type: Option<BloodType>,
1110    multiple_birth: Option<u8>,
1111    address: Option<Address>,
1112    birth_place: Option<Address>,
1113    death_place: Option<Address>,
1114    previous_addresses: Vec<Address>,
1115    passport_books: Vec<PassportBook>,
1116    phone: Option<String>,
1117    mobile: Option<String>,
1118    email: Option<String>,
1119    local_id: Option<String>,
1120}
1121
1122impl WorkerBuilder {
1123    /// Set the United Kingdom NHS Number (England, Wales, Isle of Man).
1124    ///
1125    /// The string is stored verbatim; normalisation and validation happen at
1126    /// match time via [`crate::identifiers::parse_uk_nhs_number`]. Whitespace
1127    /// in the canonical `"XXX XXX XXXX"` layout is permitted.
1128    ///
1129    /// ```
1130    /// # use worker_matcher::Worker;
1131    /// let p = Worker::builder().uk_nhs_number("943 476 5919").build();
1132    /// assert_eq!(p.uk_nhs_number.as_deref(), Some("943 476 5919"));
1133    /// ```
1134    pub fn uk_nhs_number<S: Into<String>>(mut self, value: S) -> Self {
1135        self.uk_nhs_number = Some(value.into());
1136        self
1137    }
1138
1139    /// Set the France NIR (*Numéro d'Inscription au Répertoire*).
1140    ///
1141    /// The 15-character national identifier. Stored verbatim; parsing happens
1142    /// at match time via [`crate::identifiers::parse_fr_nir`].
1143    ///
1144    /// ```
1145    /// # use worker_matcher::Worker;
1146    /// let p = Worker::builder().fr_nir("180127512345642").build();
1147    /// assert_eq!(p.fr_nir.as_deref(), Some("180127512345642"));
1148    /// ```
1149    pub fn fr_nir<S: Into<String>>(mut self, value: S) -> Self {
1150        self.fr_nir = Some(value.into());
1151        self
1152    }
1153
1154    /// Set the España (Spain) TSI (*Tarjeta Sanitaria Individual*) / CIP-SNS
1155    /// identifier.
1156    ///
1157    /// Stored verbatim; parsing happens at match time via
1158    /// [`crate::identifiers::parse_es_tsi`].
1159    ///
1160    /// ```
1161    /// # use worker_matcher::Worker;
1162    /// let p = Worker::builder().es_tsi("ABCD123456XY1234").build();
1163    /// assert_eq!(p.es_tsi.as_deref(), Some("ABCD123456XY1234"));
1164    /// ```
1165    pub fn es_tsi<S: Into<String>>(mut self, value: S) -> Self {
1166        self.es_tsi = Some(value.into());
1167        self
1168    }
1169
1170    /// Set the Éire (Ireland) IHI (Individual Health Identifier).
1171    ///
1172    /// The 7-digit identifier. Stored verbatim; parsing happens at match time
1173    /// via [`crate::identifiers::parse_ie_ihi`].
1174    ///
1175    /// ```
1176    /// # use worker_matcher::Worker;
1177    /// let p = Worker::builder().ie_ihi("1234567").build();
1178    /// assert_eq!(p.ie_ihi.as_deref(), Some("1234567"));
1179    /// ```
1180    pub fn ie_ihi<S: Into<String>>(mut self, value: S) -> Self {
1181        self.ie_ihi = Some(value.into());
1182        self
1183    }
1184
1185    /// Set the United Kingdom Northern Ireland H&C (Health and Care) Number.
1186    ///
1187    /// A 10-digit Modulus-11 identifier sharing the NHS Number algorithm.
1188    /// Stored verbatim; parsing happens at match time via
1189    /// [`crate::identifiers::parse_uk_hc_number`].
1190    ///
1191    /// ```
1192    /// # use worker_matcher::Worker;
1193    /// let p = Worker::builder().uk_hc_number("9434765919").build();
1194    /// assert_eq!(p.uk_hc_number.as_deref(), Some("9434765919"));
1195    /// ```
1196    pub fn uk_hc_number<S: Into<String>>(mut self, value: S) -> Self {
1197        self.uk_hc_number = Some(value.into());
1198        self
1199    }
1200
1201    /// Set the United States Social Security Number (SSN).
1202    ///
1203    /// A 9-digit identifier issued by the Social Security Administration.
1204    /// Stored verbatim; parsing happens at match time via
1205    /// [`crate::identifiers::parse_us_ssn`]. The canonical
1206    /// `"AAA-GG-SSSS"` layout and the compact `"AAAGGSSSS"` layout are
1207    /// equivalent under parsing.
1208    ///
1209    /// ```
1210    /// # use worker_matcher::Worker;
1211    /// let p = Worker::builder().us_ssn("123-45-6789").build();
1212    /// assert_eq!(p.us_ssn.as_deref(), Some("123-45-6789"));
1213    /// ```
1214    pub fn us_ssn<S: Into<String>>(mut self, value: S) -> Self {
1215        self.us_ssn = Some(value.into());
1216        self
1217    }
1218
1219    /// Set the Australia IHI (Individual Healthcare Identifier).
1220    ///
1221    /// 16-digit identifier with a Luhn check, conforming to ISO/IEC
1222    /// 7812-1. Stored verbatim; parsing happens at match time via
1223    /// [`crate::identifiers::parse_au_ihi`].
1224    ///
1225    /// ```
1226    /// # use worker_matcher::Worker;
1227    /// let p = Worker::builder().au_ihi("8003601234567894").build();
1228    /// assert_eq!(p.au_ihi.as_deref(), Some("8003601234567894"));
1229    /// ```
1230    pub fn au_ihi<S: Into<String>>(mut self, value: S) -> Self {
1231        self.au_ihi = Some(value.into());
1232        self
1233    }
1234
1235    /// Set the Germany KVNR (*Krankenversichertennummer*).
1236    ///
1237    /// 10-character (1 letter + 9 digits) lifelong health-insurance
1238    /// number with a Mod-10 check. Stored verbatim; parsing happens at
1239    /// match time via [`crate::identifiers::parse_de_kvnr`].
1240    ///
1241    /// ```
1242    /// # use worker_matcher::Worker;
1243    /// let p = Worker::builder().de_kvnr("A123456780").build();
1244    /// assert_eq!(p.de_kvnr.as_deref(), Some("A123456780"));
1245    /// ```
1246    pub fn de_kvnr<S: Into<String>>(mut self, value: S) -> Self {
1247        self.de_kvnr = Some(value.into());
1248        self
1249    }
1250
1251    /// Set the Italy *Codice Fiscale* (CF).
1252    ///
1253    /// 16-character alphanumeric tax identifier with a Mod-26 check
1254    /// character. Stored verbatim; parsing happens at match time via
1255    /// [`crate::identifiers::parse_it_cf`].
1256    ///
1257    /// ```
1258    /// # use worker_matcher::Worker;
1259    /// let p = Worker::builder().it_cf("RSSMRA85T10A562S").build();
1260    /// assert_eq!(p.it_cf.as_deref(), Some("RSSMRA85T10A562S"));
1261    /// ```
1262    pub fn it_cf<S: Into<String>>(mut self, value: S) -> Self {
1263        self.it_cf = Some(value.into());
1264        self
1265    }
1266
1267    /// Set the Netherlands BSN (*Burgerservicenummer*).
1268    ///
1269    /// 9-digit citizen-service number with the "11-test" check rule.
1270    /// Stored verbatim; parsing happens at match time via
1271    /// [`crate::identifiers::parse_nl_bsn`].
1272    ///
1273    /// ```
1274    /// # use worker_matcher::Worker;
1275    /// let p = Worker::builder().nl_bsn("111222333").build();
1276    /// assert_eq!(p.nl_bsn.as_deref(), Some("111222333"));
1277    /// ```
1278    pub fn nl_bsn<S: Into<String>>(mut self, value: S) -> Self {
1279        self.nl_bsn = Some(value.into());
1280        self
1281    }
1282
1283    /// Set the Sweden *Workernummer*.
1284    ///
1285    /// 10- or 12-digit workeral identity number with a Luhn check
1286    /// computed over the 10-digit form. Stored verbatim; parsing happens
1287    /// at match time via [`crate::identifiers::parse_se_workernummer`].
1288    ///
1289    /// ```
1290    /// # use worker_matcher::Worker;
1291    /// let p = Worker::builder().se_workernummer("19460324-3850").build();
1292    /// assert_eq!(p.se_workernummer.as_deref(), Some("19460324-3850"));
1293    /// ```
1294    pub fn se_workernummer<S: Into<String>>(mut self, value: S) -> Self {
1295        self.se_workernummer = Some(value.into());
1296        self
1297    }
1298
1299    /// Set the United Kingdom (Scotland) CHI Number (Community Health Index).
1300    ///
1301    /// 10-digit identifier issued by NHS Scotland, sharing the Mod-11
1302    /// algorithm of the NHS Number but scheme-local. Stored verbatim;
1303    /// parsing happens at match time via
1304    /// [`crate::identifiers::parse_uk_chi_number`].
1305    ///
1306    /// ```
1307    /// # use worker_matcher::Worker;
1308    /// let p = Worker::builder().uk_chi_number("0101701233").build();
1309    /// assert_eq!(p.uk_chi_number.as_deref(), Some("0101701233"));
1310    /// ```
1311    pub fn uk_chi_number<S: Into<String>>(mut self, value: S) -> Self {
1312        self.uk_chi_number = Some(value.into());
1313        self
1314    }
1315
1316    /// Set the Belgium National Number (*Rijksregisternummer*). 11 digits, Mod-97.
1317    pub fn be_nn<S: Into<String>>(mut self, value: S) -> Self {
1318        self.be_nn = Some(value.into());
1319        self
1320    }
1321
1322    /// Set the Bulgaria EGN (*Edinen grazhdanski nomer*). 10 digits, weighted Mod-11.
1323    pub fn bg_egn<S: Into<String>>(mut self, value: S) -> Self {
1324        self.bg_egn = Some(value.into());
1325        self
1326    }
1327
1328    /// Set the Czech Republic *Rodné číslo*. 9 or 10 digits.
1329    pub fn cz_rc<S: Into<String>>(mut self, value: S) -> Self {
1330        self.cz_rc = Some(value.into());
1331        self
1332    }
1333
1334    /// Set the Denmark CPR (*Centrale Workerregister*). 10 digits.
1335    pub fn dk_cpr<S: Into<String>>(mut self, value: S) -> Self {
1336        self.dk_cpr = Some(value.into());
1337        self
1338    }
1339
1340    /// Set the Estonia *Isikukood* (Workeral Identification Code). 11 digits.
1341    pub fn ee_ik<S: Into<String>>(mut self, value: S) -> Self {
1342        self.ee_ik = Some(value.into());
1343        self
1344    }
1345
1346    /// Set the Spain DNI / NIE. 8 digits + Mod-23 letter.
1347    pub fn es_dni<S: Into<String>>(mut self, value: S) -> Self {
1348        self.es_dni = Some(value.into());
1349        self
1350    }
1351
1352    /// Set the Finland HETU (*Henkilötunnus*). 11 chars with century sign.
1353    pub fn fi_hetu<S: Into<String>>(mut self, value: S) -> Self {
1354        self.fi_hetu = Some(value.into());
1355        self
1356    }
1357
1358    /// Set the Croatia OIB (*Osobni identifikacijski broj*). 11 digits.
1359    pub fn hr_oib<S: Into<String>>(mut self, value: S) -> Self {
1360        self.hr_oib = Some(value.into());
1361        self
1362    }
1363
1364    /// Set the Iceland *Kennitala*. 10 digits.
1365    pub fn is_kt<S: Into<String>>(mut self, value: S) -> Self {
1366        self.is_kt = Some(value.into());
1367        self
1368    }
1369
1370    /// Set the Lithuania *Asmens kodas*. 11 digits.
1371    pub fn lt_ak<S: Into<String>>(mut self, value: S) -> Self {
1372        self.lt_ak = Some(value.into());
1373        self
1374    }
1375
1376    /// Set the Latvia *Workeras kods*. 11 digits.
1377    pub fn lv_pk<S: Into<String>>(mut self, value: S) -> Self {
1378        self.lv_pk = Some(value.into());
1379        self
1380    }
1381
1382    /// Set the Malta National ID. 7 digits + letter.
1383    pub fn mt_id<S: Into<String>>(mut self, value: S) -> Self {
1384        self.mt_id = Some(value.into());
1385        self
1386    }
1387
1388    /// Set the Norway *Fødselsnummer*. 11 digits, dual Mod-11.
1389    pub fn no_fnr<S: Into<String>>(mut self, value: S) -> Self {
1390        self.no_fnr = Some(value.into());
1391        self
1392    }
1393
1394    /// Set the Poland PESEL. 11 digits, weighted Mod-10.
1395    pub fn pl_pesel<S: Into<String>>(mut self, value: S) -> Self {
1396        self.pl_pesel = Some(value.into());
1397        self
1398    }
1399
1400    /// Set the Romania CNP (*Cod Numeric Workeral*). 13 digits.
1401    pub fn ro_cnp<S: Into<String>>(mut self, value: S) -> Self {
1402        self.ro_cnp = Some(value.into());
1403        self
1404    }
1405
1406    /// Set the Slovenia EMŠO (*Enotna Matična Številka Občana*). 13 digits.
1407    pub fn si_emso<S: Into<String>>(mut self, value: S) -> Self {
1408        self.si_emso = Some(value.into());
1409        self
1410    }
1411
1412    /// Set the Slovakia *Rodné číslo*. 9 or 10 digits.
1413    pub fn sk_rc<S: Into<String>>(mut self, value: S) -> Self {
1414        self.sk_rc = Some(value.into());
1415        self
1416    }
1417
1418    /// Set the United Kingdom National Insurance Number (NINO).
1419    pub fn uk_nino<S: Into<String>>(mut self, value: S) -> Self {
1420        self.uk_nino = Some(value.into());
1421        self
1422    }
1423
1424    /// Set the Greece DSS investor share code. 10 digits.
1425    pub fn gr_dss<S: Into<String>>(mut self, value: S) -> Self {
1426        self.gr_dss = Some(value.into());
1427        self
1428    }
1429
1430    /// Set the Liechtenstein National Identity Card Number. 2 letters + 8 digits.
1431    pub fn li_id<S: Into<String>>(mut self, value: S) -> Self {
1432        self.li_id = Some(value.into());
1433        self
1434    }
1435
1436    /// Set the Netherlands National Identity Card Number. 9 chars per spec.
1437    pub fn nl_id<S: Into<String>>(mut self, value: S) -> Self {
1438        self.nl_id = Some(value.into());
1439        self
1440    }
1441
1442    /// Set the Poland NIP (*Numer Identyfikacji Podatkowej*). 10 digits, weighted Mod-11.
1443    pub fn pl_nip<S: Into<String>>(mut self, value: S) -> Self {
1444        self.pl_nip = Some(value.into());
1445        self
1446    }
1447
1448    /// Set the Portugal NIF (*Número de Identificação Fiscal*). 9 digits, weighted Mod-11.
1449    pub fn pt_nif<S: Into<String>>(mut self, value: S) -> Self {
1450        self.pt_nif = Some(value.into());
1451        self
1452    }
1453
1454    /// Set the Brazil CPF (*Cadastro de Pessoas Físicas*). 11 digits, two Mod-11 check digits.
1455    pub fn br_cpf<S: Into<String>>(mut self, value: S) -> Self {
1456        self.br_cpf = Some(value.into());
1457        self
1458    }
1459
1460    /// Set the China Resident Identity Card number (*居民身份证*). 18 chars, weighted Mod-11 + date substring.
1461    pub fn cn_rrn<S: Into<String>>(mut self, value: S) -> Self {
1462        self.cn_rrn = Some(value.into());
1463        self
1464    }
1465
1466    /// Set the India Aadhaar number. 12 digits, Verhoeff check digit.
1467    pub fn in_aadhaar<S: Into<String>>(mut self, value: S) -> Self {
1468        self.in_aadhaar = Some(value.into());
1469        self
1470    }
1471
1472    /// Set the Japan My Number (*個人番号*). 12 digits, weighted Mod-11 check digit.
1473    pub fn jp_my_number<S: Into<String>>(mut self, value: S) -> Self {
1474        self.jp_my_number = Some(value.into());
1475        self
1476    }
1477
1478    /// Set the Mexico CURP. 18 alphanumeric chars, structural + Mod-10 check digit.
1479    pub fn mx_curp<S: Into<String>>(mut self, value: S) -> Self {
1480        self.mx_curp = Some(value.into());
1481        self
1482    }
1483
1484    /// Set the New Zealand NHI Number. Original 7-char format (3 letters + 4 digits).
1485    pub fn nz_nhi<S: Into<String>>(mut self, value: S) -> Self {
1486        self.nz_nhi = Some(value.into());
1487        self
1488    }
1489
1490    /// Set the South Africa ID Number. 13 digits, Luhn + date substring.
1491    pub fn za_id<S: Into<String>>(mut self, value: S) -> Self {
1492        self.za_id = Some(value.into());
1493        self
1494    }
1495
1496    /// Set the given name (forename).
1497    ///
1498    /// ```
1499    /// # use worker_matcher::Worker;
1500    /// let p = Worker::builder().given_name("Carys").build();
1501    /// assert_eq!(p.given_name.as_deref(), Some("Carys"));
1502    /// ```
1503    pub fn given_name<S: Into<String>>(mut self, value: S) -> Self {
1504        self.given_name = Some(value.into());
1505        self
1506    }
1507
1508    /// Set the middle name(s).
1509    ///
1510    /// Stored on the worker but not currently used in matching scoring
1511    /// (see spec OQ-1).
1512    ///
1513    /// ```
1514    /// # use worker_matcher::Worker;
1515    /// let p = Worker::builder().middle_name("Eleri").build();
1516    /// assert_eq!(p.middle_name.as_deref(), Some("Eleri"));
1517    /// ```
1518    pub fn middle_name<S: Into<String>>(mut self, value: S) -> Self {
1519        self.middle_name = Some(value.into());
1520        self
1521    }
1522
1523    /// Set the family name (surname).
1524    ///
1525    /// ```
1526    /// # use worker_matcher::Worker;
1527    /// let p = Worker::builder().family_name("Pritchard").build();
1528    /// assert_eq!(p.family_name.as_deref(), Some("Pritchard"));
1529    /// ```
1530    pub fn family_name<S: Into<String>>(mut self, value: S) -> Self {
1531        self.family_name = Some(value.into());
1532        self
1533    }
1534
1535    /// Set the date of birth.
1536    ///
1537    /// ```
1538    /// # use worker_matcher::Worker;
1539    /// use chrono::NaiveDate;
1540    /// let dob = NaiveDate::from_ymd_opt(1990, 1, 1).unwrap();
1541    /// let p = Worker::builder().date_of_birth(dob).build();
1542    /// assert_eq!(p.date_of_birth, Some(dob));
1543    /// ```
1544    pub fn date_of_birth(mut self, value: NaiveDate) -> Self {
1545        self.date_of_birth = Some(value);
1546        self
1547    }
1548
1549    /// Set the date of death (FHIR `Patient.deceasedDateTime`).
1550    ///
1551    /// ```
1552    /// # use worker_matcher::Worker;
1553    /// use chrono::NaiveDate;
1554    /// let dod = NaiveDate::from_ymd_opt(2024, 6, 30).unwrap();
1555    /// let p = Worker::builder().death_date(dod).build();
1556    /// assert_eq!(p.death_date, Some(dod));
1557    /// ```
1558    pub fn death_date(mut self, value: NaiveDate) -> Self {
1559        self.death_date = Some(value);
1560        self
1561    }
1562
1563    /// Set the recorded gender.
1564    ///
1565    /// ```
1566    /// # use worker_matcher::{Gender, Worker};
1567    /// let p = Worker::builder().gender(Gender::Female).build();
1568    /// assert_eq!(p.gender, Some(Gender::Female));
1569    /// ```
1570    pub fn gender(mut self, value: Gender) -> Self {
1571        self.gender = Some(value);
1572        self
1573    }
1574
1575    /// Set the recorded ABO+RhD blood type.
1576    ///
1577    /// ```
1578    /// # use worker_matcher::{BloodType, Worker};
1579    /// let p = Worker::builder().blood_type(BloodType::OPositive).build();
1580    /// assert_eq!(p.blood_type, Some(BloodType::OPositive));
1581    /// ```
1582    pub fn blood_type(mut self, value: BloodType) -> Self {
1583        self.blood_type = Some(value);
1584        self
1585    }
1586
1587    /// Set the multiple-birth indicator (FHIR `Patient.multipleBirth`).
1588    ///
1589    /// The value is the 1-indexed birth order within a multiple-birth
1590    /// set: `1` for the first born, `2` for the second, and so on.
1591    /// `0` is conventionally not used; consumers should pass `None`
1592    /// (do not call this setter) for singletons or unknown values.
1593    ///
1594    /// ```
1595    /// # use worker_matcher::Worker;
1596    /// // First of identical twins.
1597    /// let p = Worker::builder().multiple_birth(1).build();
1598    /// assert_eq!(p.multiple_birth, Some(1));
1599    /// ```
1600    pub fn multiple_birth(mut self, value: u8) -> Self {
1601        self.multiple_birth = Some(value);
1602        self
1603    }
1604
1605    /// Set the current residential address.
1606    ///
1607    /// ```
1608    /// # use worker_matcher::{Address, Worker};
1609    /// let mut a = Address::new();
1610    /// a.postcode = Some("CF10 1AA".into());
1611    /// let p = Worker::builder().address(a).build();
1612    /// assert_eq!(p.address.unwrap().postcode.as_deref(), Some("CF10 1AA"));
1613    /// ```
1614    pub fn address(mut self, value: Address) -> Self {
1615        self.address = Some(value);
1616        self
1617    }
1618
1619    /// Set the place of birth (FHIR `Patient.birthPlace`).
1620    ///
1621    /// Typically only [`Address::city`] and [`Address::country`] are
1622    /// populated for a birth place.
1623    ///
1624    /// ```
1625    /// # use worker_matcher::{Address, Worker};
1626    /// let p = Worker::builder()
1627    ///     .birth_place(Address::new().with_city("Cardiff").with_country("Wales"))
1628    ///     .build();
1629    /// assert_eq!(p.birth_place.as_ref().unwrap().city.as_deref(), Some("Cardiff"));
1630    /// ```
1631    pub fn birth_place(mut self, value: Address) -> Self {
1632        self.birth_place = Some(value);
1633        self
1634    }
1635
1636    /// Set the place of death.
1637    ///
1638    /// Modelled as an [`Address`] for parity with [`Self::birth_place`]
1639    /// — typically only [`Address::city`] and [`Address::country`] are
1640    /// populated.
1641    ///
1642    /// ```
1643    /// # use worker_matcher::{Address, Worker};
1644    /// let p = Worker::builder()
1645    ///     .death_place(Address::new().with_city("Glasgow").with_country("Scotland"))
1646    ///     .build();
1647    /// assert_eq!(p.death_place.as_ref().unwrap().city.as_deref(), Some("Glasgow"));
1648    /// ```
1649    pub fn death_place(mut self, value: Address) -> Self {
1650        self.death_place = Some(value);
1651        self
1652    }
1653
1654    /// Set the list of previous addresses. Used by the address
1655    /// sub-score (best-of cartesian product, see spec §12.4.2).
1656    ///
1657    /// ```
1658    /// # use worker_matcher::{Address, Worker};
1659    /// let p = Worker::builder()
1660    ///     .previous_addresses(vec![Address::new(), Address::new()])
1661    ///     .build();
1662    /// assert_eq!(p.previous_addresses.len(), 2);
1663    /// ```
1664    pub fn previous_addresses(mut self, value: Vec<Address>) -> Self {
1665        self.previous_addresses = value;
1666        self
1667    }
1668
1669    /// Append a single passport book to the worker's list. Chainable;
1670    /// call multiple times to record multi-country or historical
1671    /// books.
1672    ///
1673    /// ```
1674    /// # use worker_matcher::{PassportBook, Worker};
1675    /// let p = Worker::builder()
1676    ///     .add_passport_book(PassportBook::new("GB", "123456789").unwrap())
1677    ///     .add_passport_book(PassportBook::new("US", "AB1234567").unwrap())
1678    ///     .build();
1679    /// assert_eq!(p.passport_books.len(), 2);
1680    /// ```
1681    pub fn add_passport_book(mut self, book: PassportBook) -> Self {
1682        self.passport_books.push(book);
1683        self
1684    }
1685
1686    /// Replace the entire passport-book list.
1687    ///
1688    /// ```
1689    /// # use worker_matcher::{PassportBook, Worker};
1690    /// let books = vec![PassportBook::new("GB", "123456789").unwrap()];
1691    /// let p = Worker::builder().passport_books(books).build();
1692    /// assert_eq!(p.passport_books.len(), 1);
1693    /// ```
1694    pub fn passport_books(mut self, value: Vec<PassportBook>) -> Self {
1695        self.passport_books = value;
1696        self
1697    }
1698
1699    /// Set the primary phone number.
1700    ///
1701    /// ```
1702    /// # use worker_matcher::Worker;
1703    /// let p = Worker::builder().phone("029 2034 5678").build();
1704    /// assert_eq!(p.phone.as_deref(), Some("029 2034 5678"));
1705    /// ```
1706    pub fn phone<S: Into<String>>(mut self, value: S) -> Self {
1707        self.phone = Some(value.into());
1708        self
1709    }
1710
1711    /// Set the mobile phone number. Used as a fallback when `phone` is absent.
1712    ///
1713    /// ```
1714    /// # use worker_matcher::Worker;
1715    /// let p = Worker::builder().mobile("07700 900123").build();
1716    /// assert_eq!(p.mobile.as_deref(), Some("07700 900123"));
1717    /// ```
1718    pub fn mobile<S: Into<String>>(mut self, value: S) -> Self {
1719        self.mobile = Some(value.into());
1720        self
1721    }
1722
1723    /// Set the email address. Not currently used in scoring.
1724    ///
1725    /// ```
1726    /// # use worker_matcher::Worker;
1727    /// let p = Worker::builder().email("alice@example.org").build();
1728    /// assert_eq!(p.email.as_deref(), Some("alice@example.org"));
1729    /// ```
1730    pub fn email<S: Into<String>>(mut self, value: S) -> Self {
1731        self.email = Some(value.into());
1732        self
1733    }
1734
1735    /// Set the local hospital or practice identifier.
1736    ///
1737    /// ```
1738    /// # use worker_matcher::Worker;
1739    /// let p = Worker::builder().local_id("MRN-12345").build();
1740    /// assert_eq!(p.local_id.as_deref(), Some("MRN-12345"));
1741    /// ```
1742    pub fn local_id<S: Into<String>>(mut self, value: S) -> Self {
1743        self.local_id = Some(value.into());
1744        self
1745    }
1746
1747    /// Consume the builder and produce the [`Worker`].
1748    ///
1749    /// ```
1750    /// # use worker_matcher::Worker;
1751    /// let p = Worker::builder().given_name("Eira").build();
1752    /// assert!(p.family_name.is_none());
1753    /// ```
1754    pub fn build(self) -> Worker {
1755        Worker {
1756            uk_nhs_number: self.uk_nhs_number,
1757            fr_nir: self.fr_nir,
1758            es_tsi: self.es_tsi,
1759            ie_ihi: self.ie_ihi,
1760            uk_hc_number: self.uk_hc_number,
1761            us_ssn: self.us_ssn,
1762            au_ihi: self.au_ihi,
1763            de_kvnr: self.de_kvnr,
1764            it_cf: self.it_cf,
1765            nl_bsn: self.nl_bsn,
1766            se_workernummer: self.se_workernummer,
1767            uk_chi_number: self.uk_chi_number,
1768            be_nn: self.be_nn,
1769            bg_egn: self.bg_egn,
1770            cz_rc: self.cz_rc,
1771            dk_cpr: self.dk_cpr,
1772            ee_ik: self.ee_ik,
1773            es_dni: self.es_dni,
1774            fi_hetu: self.fi_hetu,
1775            hr_oib: self.hr_oib,
1776            is_kt: self.is_kt,
1777            lt_ak: self.lt_ak,
1778            lv_pk: self.lv_pk,
1779            mt_id: self.mt_id,
1780            no_fnr: self.no_fnr,
1781            pl_pesel: self.pl_pesel,
1782            ro_cnp: self.ro_cnp,
1783            si_emso: self.si_emso,
1784            sk_rc: self.sk_rc,
1785            uk_nino: self.uk_nino,
1786            gr_dss: self.gr_dss,
1787            li_id: self.li_id,
1788            nl_id: self.nl_id,
1789            pl_nip: self.pl_nip,
1790            pt_nif: self.pt_nif,
1791            br_cpf: self.br_cpf,
1792            cn_rrn: self.cn_rrn,
1793            in_aadhaar: self.in_aadhaar,
1794            jp_my_number: self.jp_my_number,
1795            mx_curp: self.mx_curp,
1796            nz_nhi: self.nz_nhi,
1797            za_id: self.za_id,
1798            given_name: self.given_name,
1799            middle_name: self.middle_name,
1800            family_name: self.family_name,
1801            date_of_birth: self.date_of_birth,
1802            death_date: self.death_date,
1803            gender: self.gender,
1804            blood_type: self.blood_type,
1805            multiple_birth: self.multiple_birth,
1806            address: self.address,
1807            birth_place: self.birth_place,
1808            death_place: self.death_place,
1809            previous_addresses: self.previous_addresses,
1810            passport_books: self.passport_books,
1811            phone: self.phone,
1812            mobile: self.mobile,
1813            email: self.email,
1814            local_id: self.local_id,
1815        }
1816    }
1817}
1818
1819#[cfg(test)]
1820mod tests {
1821    use super::*;
1822
1823    #[test]
1824    fn address_new_is_all_none() {
1825        let a = Address::new();
1826        assert!(a.line1.is_none());
1827        assert!(a.line2.is_none());
1828        assert!(a.city.is_none());
1829        assert!(a.county.is_none());
1830        assert!(a.postcode.is_none());
1831        assert!(a.country.is_none());
1832    }
1833
1834    #[test]
1835    fn address_default_matches_new() {
1836        assert_eq!(Address::default(), Address::new());
1837    }
1838
1839    #[test]
1840    fn address_fluent_builders_chain() {
1841        let a = Address::new()
1842            .with_line1("10 Downing Street")
1843            .with_city("London")
1844            .with_postcode("SW1A 2AA")
1845            .with_country("United Kingdom");
1846        assert_eq!(a.line1.as_deref(), Some("10 Downing Street"));
1847        assert_eq!(a.city.as_deref(), Some("London"));
1848        assert_eq!(a.postcode.as_deref(), Some("SW1A 2AA"));
1849        assert_eq!(a.country.as_deref(), Some("United Kingdom"));
1850        assert!(a.line2.is_none());
1851        assert!(a.county.is_none());
1852    }
1853
1854    #[test]
1855    fn address_round_trips_through_serde() {
1856        let mut a = Address::new();
1857        a.line1 = Some("123 High Street".into());
1858        a.postcode = Some("CF10 1AA".into());
1859        let json = serde_json::to_string(&a).expect("serialise");
1860        let back: Address = serde_json::from_str(&json).expect("deserialise");
1861        assert_eq!(a, back);
1862    }
1863
1864    #[test]
1865    fn worker_builder_starts_empty() {
1866        let p = Worker::builder().build();
1867        assert!(p.uk_nhs_number.is_none());
1868        assert!(p.fr_nir.is_none());
1869        assert!(p.es_tsi.is_none());
1870        assert!(p.ie_ihi.is_none());
1871        assert!(p.uk_hc_number.is_none());
1872        assert!(p.us_ssn.is_none());
1873        assert!(p.au_ihi.is_none());
1874        assert!(p.de_kvnr.is_none());
1875        assert!(p.it_cf.is_none());
1876        assert!(p.nl_bsn.is_none());
1877        assert!(p.se_workernummer.is_none());
1878        assert!(p.uk_chi_number.is_none());
1879        assert!(p.given_name.is_none());
1880        assert!(p.family_name.is_none());
1881        assert!(p.date_of_birth.is_none());
1882        assert!(p.gender.is_none());
1883        assert!(p.address.is_none());
1884        assert!(p.previous_addresses.is_empty());
1885        assert!(p.passport_books.is_empty());
1886        assert!(p.phone.is_none());
1887        assert!(p.mobile.is_none());
1888        assert!(p.email.is_none());
1889        assert!(p.local_id.is_none());
1890    }
1891
1892    #[test]
1893    fn worker_builder_carries_all_national_identifiers() {
1894        let p = Worker::builder()
1895            .uk_nhs_number("9434765919")
1896            .fr_nir("180127512345642")
1897            .es_tsi("ABCD123456XY1234")
1898            .ie_ihi("1234567")
1899            .uk_hc_number("9434765919")
1900            .us_ssn("123-45-6789")
1901            .au_ihi("8003601234567894")
1902            .de_kvnr("A123456780")
1903            .it_cf("RSSMRA85T10A562S")
1904            .nl_bsn("111222333")
1905            .se_workernummer("4603243850")
1906            .uk_chi_number("0101701233")
1907            .build();
1908        assert_eq!(p.uk_nhs_number.as_deref(), Some("9434765919"));
1909        assert_eq!(p.fr_nir.as_deref(), Some("180127512345642"));
1910        assert_eq!(p.es_tsi.as_deref(), Some("ABCD123456XY1234"));
1911        assert_eq!(p.ie_ihi.as_deref(), Some("1234567"));
1912        assert_eq!(p.uk_hc_number.as_deref(), Some("9434765919"));
1913        assert_eq!(p.us_ssn.as_deref(), Some("123-45-6789"));
1914        assert_eq!(p.au_ihi.as_deref(), Some("8003601234567894"));
1915        assert_eq!(p.de_kvnr.as_deref(), Some("A123456780"));
1916        assert_eq!(p.it_cf.as_deref(), Some("RSSMRA85T10A562S"));
1917        assert_eq!(p.nl_bsn.as_deref(), Some("111222333"));
1918        assert_eq!(p.se_workernummer.as_deref(), Some("4603243850"));
1919        assert_eq!(p.uk_chi_number.as_deref(), Some("0101701233"));
1920    }
1921
1922    #[test]
1923    fn worker_builder_accepts_str_and_string() {
1924        let p = Worker::builder()
1925            .given_name("Owen") // &str
1926            .family_name(String::from("Jones")) // String
1927            .build();
1928        assert_eq!(p.given_name.as_deref(), Some("Owen"));
1929        assert_eq!(p.family_name.as_deref(), Some("Jones"));
1930    }
1931
1932    #[test]
1933    fn worker_validate_requires_one_of_three_fields() {
1934        assert!(Worker::builder().given_name("a").build().validate().is_ok());
1935        assert!(
1936            Worker::builder()
1937                .family_name("a")
1938                .build()
1939                .validate()
1940                .is_ok()
1941        );
1942        assert!(
1943            Worker::builder()
1944                .uk_nhs_number("9434765919")
1945                .build()
1946                .validate()
1947                .is_ok()
1948        );
1949        let err = Worker::builder()
1950            .build()
1951            .validate()
1952            .expect_err("should be missing");
1953        assert!(matches!(err, crate::MatchingError::MissingField(_)));
1954    }
1955
1956    #[test]
1957    fn worker_round_trips_through_serde() {
1958        let p = Worker::builder()
1959            .uk_nhs_number("9434765919")
1960            .given_name("Carys")
1961            .family_name("Pritchard")
1962            .date_of_birth(chrono::NaiveDate::from_ymd_opt(1990, 6, 1).unwrap())
1963            .gender(Gender::Female)
1964            .build();
1965        let json = serde_json::to_string(&p).expect("serialise");
1966        let back: Worker = serde_json::from_str(&json).expect("deserialise");
1967        assert_eq!(p, back);
1968    }
1969
1970    // ---------- BloodType ----------
1971
1972    #[test]
1973    fn blood_type_parses_canonical_short_forms() {
1974        for (s, want) in [
1975            ("A+", BloodType::APositive),
1976            ("A-", BloodType::ANegative),
1977            ("B+", BloodType::BPositive),
1978            ("B-", BloodType::BNegative),
1979            ("AB+", BloodType::ABPositive),
1980            ("AB-", BloodType::ABNegative),
1981            ("O+", BloodType::OPositive),
1982            ("O-", BloodType::ONegative),
1983        ] {
1984            assert_eq!(BloodType::parse(s), Some(want), "parse {s:?}");
1985        }
1986    }
1987
1988    #[test]
1989    fn blood_type_parses_lowercase_and_whitespace() {
1990        assert_eq!(BloodType::parse("  a+ "), Some(BloodType::APositive));
1991        assert_eq!(BloodType::parse("ab-"), Some(BloodType::ABNegative));
1992    }
1993
1994    #[test]
1995    fn blood_type_parses_word_forms() {
1996        assert_eq!(BloodType::parse("A positive"), Some(BloodType::APositive));
1997        assert_eq!(BloodType::parse("A pos"), Some(BloodType::APositive));
1998        assert_eq!(BloodType::parse("A POS"), Some(BloodType::APositive));
1999        assert_eq!(BloodType::parse("A negative"), Some(BloodType::ANegative));
2000        assert_eq!(BloodType::parse("ab neg"), Some(BloodType::ABNegative));
2001        assert_eq!(BloodType::parse("o NEG"), Some(BloodType::ONegative));
2002    }
2003
2004    #[test]
2005    fn blood_type_parses_zero_as_o() {
2006        assert_eq!(BloodType::parse("0+"), Some(BloodType::OPositive));
2007        assert_eq!(BloodType::parse("0-"), Some(BloodType::ONegative));
2008    }
2009
2010    #[test]
2011    fn blood_type_parses_with_separator() {
2012        assert_eq!(BloodType::parse("A_pos"), Some(BloodType::APositive));
2013        assert_eq!(BloodType::parse("A-neg"), Some(BloodType::ANegative));
2014        assert_eq!(BloodType::parse("AB +"), Some(BloodType::ABPositive));
2015    }
2016
2017    #[test]
2018    fn blood_type_parses_ve_suffix() {
2019        assert_eq!(BloodType::parse("A+VE"), Some(BloodType::APositive));
2020        assert_eq!(BloodType::parse("a-ve"), Some(BloodType::ANegative));
2021    }
2022
2023    #[test]
2024    fn blood_type_rejects_unparseable() {
2025        assert_eq!(BloodType::parse(""), None);
2026        assert_eq!(BloodType::parse("   "), None);
2027        assert_eq!(BloodType::parse("Z+"), None);
2028        assert_eq!(BloodType::parse("A"), None); // no sign
2029        assert_eq!(BloodType::parse("Bombay"), None);
2030        assert_eq!(BloodType::parse("A++"), None);
2031    }
2032
2033    #[test]
2034    fn blood_type_as_str_and_display_round_trip() {
2035        for bt in [
2036            BloodType::APositive,
2037            BloodType::ANegative,
2038            BloodType::BPositive,
2039            BloodType::BNegative,
2040            BloodType::ABPositive,
2041            BloodType::ABNegative,
2042            BloodType::OPositive,
2043            BloodType::ONegative,
2044        ] {
2045            let s = bt.as_str();
2046            assert_eq!(format!("{bt}"), s);
2047            assert_eq!(BloodType::parse(s), Some(bt));
2048        }
2049    }
2050
2051    #[test]
2052    fn blood_type_serde_uses_short_form() {
2053        for (bt, json) in [
2054            (BloodType::APositive, "\"A+\""),
2055            (BloodType::ABNegative, "\"AB-\""),
2056            (BloodType::ONegative, "\"O-\""),
2057        ] {
2058            assert_eq!(serde_json::to_string(&bt).unwrap(), json);
2059            let back: BloodType = serde_json::from_str(json).unwrap();
2060            assert_eq!(back, bt);
2061        }
2062    }
2063
2064    #[test]
2065    fn worker_builder_sets_blood_type() {
2066        let p = Worker::builder().blood_type(BloodType::OPositive).build();
2067        assert_eq!(p.blood_type, Some(BloodType::OPositive));
2068    }
2069
2070    #[test]
2071    fn worker_default_has_no_blood_type() {
2072        let p = Worker::builder().build();
2073        assert!(p.blood_type.is_none());
2074    }
2075
2076    #[test]
2077    fn gender_is_copy_and_eq() {
2078        let g = Gender::Female;
2079        let h = g; // Copy
2080        assert_eq!(g, h);
2081        assert_ne!(g, Gender::Male);
2082    }
2083
2084    // ---------- PassportBook ----------
2085
2086    #[test]
2087    fn passport_book_new_canonicalises_country_and_number() {
2088        let b = PassportBook::new("  gb  ", " 123 ABC 789 ").unwrap();
2089        assert_eq!(b.country, "GB");
2090        assert_eq!(b.number, "123ABC789");
2091    }
2092
2093    #[test]
2094    fn passport_book_new_strips_common_separators() {
2095        // Hyphens, periods, slashes and whitespace all stripped.
2096        let b = PassportBook::new("GB", "ABC-123/456.789").unwrap();
2097        assert_eq!(b.number, "ABC123456789");
2098        let c = PassportBook::new("US", "AB-12-34-567").unwrap();
2099        assert_eq!(c.number, "AB1234567");
2100    }
2101
2102    #[test]
2103    fn passport_book_new_rejects_bad_country() {
2104        assert!(PassportBook::new("GBR", "123").is_none()); // 3 letters
2105        assert!(PassportBook::new("G", "123").is_none()); // 1 letter
2106        assert!(PassportBook::new("1A", "123").is_none()); // not alphabetic
2107        assert!(PassportBook::new("", "123").is_none()); // empty
2108    }
2109
2110    #[test]
2111    fn passport_book_new_rejects_empty_number() {
2112        assert!(PassportBook::new("GB", "").is_none());
2113        assert!(PassportBook::new("GB", "   ").is_none());
2114        assert!(PassportBook::new("GB", "\t\n").is_none());
2115    }
2116
2117    #[test]
2118    fn passport_book_with_dates_sets_metadata() {
2119        let b = PassportBook::new("GB", "123")
2120            .unwrap()
2121            .with_issued(chrono::NaiveDate::from_ymd_opt(2020, 1, 1).unwrap())
2122            .with_expires(chrono::NaiveDate::from_ymd_opt(2030, 1, 1).unwrap());
2123        assert!(b.issued.is_some());
2124        assert!(b.expires.is_some());
2125    }
2126
2127    #[test]
2128    fn passport_book_round_trips_through_serde() {
2129        let b = PassportBook::new("US", "AB1234567")
2130            .unwrap()
2131            .with_issued(chrono::NaiveDate::from_ymd_opt(2024, 6, 1).unwrap());
2132        let json = serde_json::to_string(&b).unwrap();
2133        let back: PassportBook = serde_json::from_str(&json).unwrap();
2134        assert_eq!(b, back);
2135    }
2136
2137    #[test]
2138    fn passport_book_serde_default_dates() {
2139        // Legacy payloads lacking the optional date fields must
2140        // deserialise cleanly.
2141        let legacy = r#"{"country": "GB", "number": "123"}"#;
2142        let b: PassportBook = serde_json::from_str(legacy).unwrap();
2143        assert_eq!(b.country, "GB");
2144        assert_eq!(b.number, "123");
2145        assert!(b.issued.is_none());
2146        assert!(b.expires.is_none());
2147    }
2148
2149    #[test]
2150    fn worker_builder_carries_passport_books() {
2151        let p = Worker::builder()
2152            .add_passport_book(PassportBook::new("GB", "111").unwrap())
2153            .add_passport_book(PassportBook::new("US", "222").unwrap())
2154            .build();
2155        assert_eq!(p.passport_books.len(), 2);
2156        assert_eq!(p.passport_books[0].country, "GB");
2157        assert_eq!(p.passport_books[1].country, "US");
2158    }
2159
2160    #[test]
2161    fn worker_validate_accepts_solo_passport_book() {
2162        let p = Worker::builder()
2163            .add_passport_book(PassportBook::new("GB", "123456789").unwrap())
2164            .build();
2165        assert!(p.validate().is_ok());
2166    }
2167
2168    #[test]
2169    fn previous_addresses_setter_replaces_vec() {
2170        let mut a = Address::new();
2171        a.postcode = Some("CF10 1AA".into());
2172        let p = Worker::builder()
2173            .previous_addresses(vec![a.clone()])
2174            .build();
2175        assert_eq!(p.previous_addresses, vec![a]);
2176    }
2177}