eorscope/lib.rs
1//! Dated statutory employer-contribution changes by country, read from
2//! official sources, as published by EOR Scope at <https://eorscope.com/data/>.
3//!
4//! The dataset is embedded in the crate (no network access) and parsed once,
5//! on first use. Each [`Change`] carries its effective date, the new value
6//! (rate, annual cap, weekly threshold or annual floor), its legal status and
7//! the source it was read from, with a verbatim quote and the date it was read.
8//!
9//! Cost comparison data, not legal or tax advice.
10//!
11//! # Example
12//!
13//! ```
14//! // United States: Social Security wage base for 2027.
15//! let us = eorscope::changes_for_after("US", "2026-12-31");
16//! let ss = us
17//! .iter()
18//! .find(|c| c.contribution_id.as_deref() == Some("social_security"))
19//! .expect("US Social Security 2027 change");
20//! assert_eq!(ss.effective_from, "2027-01-01");
21//! assert_eq!(ss.change.cap_annual_local, Some(192_300));
22//! println!("{} from {}: {}", ss.country, ss.effective_from, ss.source.url);
23//! ```
24
25use serde::Deserialize;
26use std::collections::BTreeMap;
27use std::sync::OnceLock;
28
29/// Raw JSON bytes of the embedded dataset, byte-identical to the file
30/// published with the site.
31pub const DATA_JSON: &str = include_str!("../data/scheduled_changes.json");
32
33/// The whole embedded dataset.
34#[derive(Debug, Clone, Deserialize)]
35pub struct Dataset {
36 /// Where the snapshot was copied from in the EOR Scope repository.
37 pub copied_from: String,
38 /// Date of the snapshot (yyyy-mm-dd).
39 pub snapshot_date: String,
40 /// Commit of the snapshot in the EOR Scope repository.
41 pub source_commit: String,
42 /// Schedule per ISO 3166-1 alpha-2 country code.
43 pub scheduled: BTreeMap<String, CountrySchedule>,
44}
45
46/// Research state and scheduled changes for one country.
47#[derive(Debug, Clone, Deserialize)]
48pub struct CountrySchedule {
49 /// Date the country was last researched (yyyy-mm-dd).
50 pub researched_on: String,
51 /// Coverage of the research (for example `"partial"`).
52 pub coverage: String,
53 /// Changes announced but not acquired (awaited, bill, proposal...).
54 pub pending: Vec<Pending>,
55 /// Changes with an effective date.
56 pub scheduled_changes: Vec<Change>,
57}
58
59/// A change that is not acquired: it must be shown as pending, never priced.
60#[derive(Debug, Clone, Deserialize)]
61pub struct Pending {
62 /// `"awaited"`, `"bill"`, `"proposal"`, `"consultation"`, `"decided_unchanged"`...
63 pub status: String,
64 /// What is pending, as read from the source.
65 pub description: String,
66 /// Contribution concerned, when one is identified.
67 pub contribution_id: Option<String>,
68 /// Source of the information.
69 pub source: PendingSource,
70}
71
72/// Source of a [`Pending`] item.
73#[derive(Debug, Clone, Deserialize)]
74pub struct PendingSource {
75 /// Source URL.
76 pub url: String,
77}
78
79/// A dated change to a statutory employer contribution.
80#[derive(Debug, Clone, Deserialize)]
81pub struct Change {
82 /// ISO 3166-1 alpha-2 country code (filled from the dataset key).
83 #[serde(skip_deserializing, default)]
84 pub country: String,
85 /// Contribution concerned, when one is identified.
86 pub contribution_id: Option<String>,
87 /// Effective date (yyyy-mm-dd).
88 pub effective_from: String,
89 /// How the date was obtained: `"stated"`, `"derived"` or `"inferred"`.
90 pub date_basis: String,
91 /// The new values; `None` fields are not changed.
92 pub change: Delta,
93 /// Legal status: `"enacted"`, `"budget_announced"`...
94 pub status: String,
95 /// Explanation written by EOR Scope Research.
96 pub note: String,
97 /// Source the change was read from.
98 pub source: Source,
99}
100
101/// New values of a contribution; a `None` field is not changed.
102#[derive(Debug, Clone, Deserialize)]
103pub struct Delta {
104 /// New rate, as a fraction (`0.062` = 6.2 %).
105 pub rate: Option<f64>,
106 /// New annual cap, in local currency.
107 pub cap_annual_local: Option<i64>,
108 /// New weekly threshold, in local currency.
109 pub threshold_weekly_local: Option<i64>,
110 /// New annual floor, in local currency.
111 pub floor_annual_local: Option<i64>,
112}
113
114/// Source of a [`Change`].
115#[derive(Debug, Clone, Deserialize)]
116pub struct Source {
117 /// Source URL.
118 pub url: String,
119 /// Verbatim quote from the source.
120 pub quote: String,
121 /// Date the source was read (yyyy-mm-dd).
122 pub read_on: String,
123 /// Kind of source (for example `"official"`).
124 pub kind: String,
125}
126
127/// The parsed dataset, parsed once on first call.
128pub fn dataset() -> &'static Dataset {
129 static DATA: OnceLock<Dataset> = OnceLock::new();
130 DATA.get_or_init(|| {
131 let mut d: Dataset =
132 serde_json::from_str(DATA_JSON).expect("embedded dataset is valid JSON");
133 for (iso, s) in d.scheduled.iter_mut() {
134 for c in s.scheduled_changes.iter_mut() {
135 c.country = iso.clone();
136 }
137 }
138 d
139 })
140}
141
142/// All scheduled changes, ordered by country code then as published.
143pub fn changes() -> Vec<&'static Change> {
144 dataset()
145 .scheduled
146 .values()
147 .flat_map(|s| s.scheduled_changes.iter())
148 .collect()
149}
150
151/// Scheduled changes for one country (ISO alpha-2 code, case-insensitive).
152pub fn changes_for(iso: &str) -> Vec<&'static Change> {
153 changes()
154 .into_iter()
155 .filter(|c| c.country.eq_ignore_ascii_case(iso))
156 .collect()
157}
158
159/// Scheduled changes effective strictly after `date` (yyyy-mm-dd, compared
160/// as a string).
161pub fn changes_after(date: &str) -> Vec<&'static Change> {
162 changes()
163 .into_iter()
164 .filter(|c| c.effective_from.as_str() > date)
165 .collect()
166}
167
168/// Scheduled changes for one country effective strictly after `date`.
169pub fn changes_for_after(iso: &str, date: &str) -> Vec<&'static Change> {
170 changes_for(iso)
171 .into_iter()
172 .filter(|c| c.effective_from.as_str() > date)
173 .collect()
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use sha2::{Digest, Sha256};
180
181 /// sha256 of `site/src/data/scheduled_changes.json` at main (ace7551).
182 const DATA_SHA256: &str = "0b3443dbc3b00e1884a124ce503745c7b392fe02e0f662563c04ea3565e75576";
183
184 #[test]
185 fn embedded_copy_is_byte_identical() {
186 let hex: String = Sha256::digest(DATA_JSON.as_bytes())
187 .iter()
188 .map(|b| format!("{b:02x}"))
189 .collect();
190 assert_eq!(hex, DATA_SHA256);
191 }
192
193 #[test]
194 fn counts() {
195 assert_eq!(changes().len(), 66);
196 let mut iso: Vec<_> = changes().iter().map(|c| c.country.as_str()).collect();
197 iso.dedup();
198 assert_eq!(iso.len(), 28);
199 assert_eq!(dataset().scheduled.len(), 76);
200 }
201
202 #[test]
203 fn us_social_security_2027_cap() {
204 let c = changes_for("us")
205 .into_iter()
206 .find(|c| c.contribution_id.as_deref() == Some("social_security"))
207 .unwrap();
208 assert_eq!(c.effective_from, "2027-01-01");
209 assert_eq!(c.change.cap_annual_local, Some(192_300));
210 assert_eq!(c.status, "enacted");
211 }
212
213 #[test]
214 fn date_filters() {
215 assert!(changes_after("9999-12-31").is_empty());
216 assert_eq!(changes_after("0000-01-01").len(), changes().len());
217 assert!(changes_for_after("US", "2027-01-01")
218 .iter()
219 .all(|c| c.effective_from.as_str() > "2027-01-01"));
220 assert!(changes_for("XX").is_empty());
221 }
222}