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}