j-law-ruby 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +9 -0
  3. data/README.md +109 -0
  4. data/Rakefile +87 -0
  5. data/ext/j_law_ruby/extconf.rb +34 -0
  6. data/lib/j_law_ruby/build_support.rb +129 -0
  7. data/lib/j_law_ruby/c_ffi.rb +662 -0
  8. data/lib/j_law_ruby.rb +532 -0
  9. data/rake_support/vendor_rust.rb +51 -0
  10. data/test/test_c_ffi_adapter.rb +46 -0
  11. data/test/test_consumption_tax.rb +66 -0
  12. data/test/test_gemspec.rb +82 -0
  13. data/test/test_income_tax.rb +77 -0
  14. data/test/test_income_tax_deductions.rb +82 -0
  15. data/test/test_real_estate.rb +98 -0
  16. data/test/test_stamp_tax.rb +68 -0
  17. data/test/test_withholding_tax.rb +65 -0
  18. data/vendor/rust/Cargo.lock +235 -0
  19. data/vendor/rust/Cargo.toml +12 -0
  20. data/vendor/rust/crates/j-law-c-ffi/Cargo.toml +20 -0
  21. data/vendor/rust/crates/j-law-c-ffi/j_law_c_ffi.h +493 -0
  22. data/vendor/rust/crates/j-law-c-ffi/src/lib.rs +1553 -0
  23. data/vendor/rust/crates/j-law-core/Cargo.toml +18 -0
  24. data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/calculator.rs +216 -0
  25. data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/context.rs +29 -0
  26. data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/mod.rs +9 -0
  27. data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/params.rs +24 -0
  28. data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/policy.rs +34 -0
  29. data/vendor/rust/crates/j-law-core/src/domains/income_tax/assessment.rs +76 -0
  30. data/vendor/rust/crates/j-law-core/src/domains/income_tax/calculator.rs +222 -0
  31. data/vendor/rust/crates/j-law-core/src/domains/income_tax/context.rs +79 -0
  32. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/calculator.rs +167 -0
  33. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/context.rs +172 -0
  34. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/expense.rs +465 -0
  35. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/mod.rs +20 -0
  36. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/params.rs +205 -0
  37. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/personal.rs +324 -0
  38. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/types.rs +61 -0
  39. data/vendor/rust/crates/j-law-core/src/domains/income_tax/mod.rs +24 -0
  40. data/vendor/rust/crates/j-law-core/src/domains/income_tax/params.rs +109 -0
  41. data/vendor/rust/crates/j-law-core/src/domains/income_tax/policy.rs +103 -0
  42. data/vendor/rust/crates/j-law-core/src/domains/mod.rs +5 -0
  43. data/vendor/rust/crates/j-law-core/src/domains/real_estate/calculator.rs +197 -0
  44. data/vendor/rust/crates/j-law-core/src/domains/real_estate/context.rs +48 -0
  45. data/vendor/rust/crates/j-law-core/src/domains/real_estate/mod.rs +9 -0
  46. data/vendor/rust/crates/j-law-core/src/domains/real_estate/params.rs +43 -0
  47. data/vendor/rust/crates/j-law-core/src/domains/real_estate/policy.rs +40 -0
  48. data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/calculator.rs +321 -0
  49. data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/context.rs +408 -0
  50. data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/mod.rs +12 -0
  51. data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/params.rs +190 -0
  52. data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/policy.rs +105 -0
  53. data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/calculator.rs +247 -0
  54. data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/context.rs +167 -0
  55. data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/mod.rs +9 -0
  56. data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/params.rs +80 -0
  57. data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/policy.rs +49 -0
  58. data/vendor/rust/crates/j-law-core/src/error.rs +171 -0
  59. data/vendor/rust/crates/j-law-core/src/lib.rs +9 -0
  60. data/vendor/rust/crates/j-law-core/src/types/amount.rs +232 -0
  61. data/vendor/rust/crates/j-law-core/src/types/citation.rs +82 -0
  62. data/vendor/rust/crates/j-law-core/src/types/date.rs +280 -0
  63. data/vendor/rust/crates/j-law-core/src/types/mod.rs +11 -0
  64. data/vendor/rust/crates/j-law-core/src/types/rate.rs +219 -0
  65. data/vendor/rust/crates/j-law-core/src/types/rounding.rs +81 -0
  66. data/vendor/rust/crates/j-law-registry/Cargo.toml +15 -0
  67. data/vendor/rust/crates/j-law-registry/data/consumption_tax/consumption_tax.json +70 -0
  68. data/vendor/rust/crates/j-law-registry/data/income_tax/deductions.json +327 -0
  69. data/vendor/rust/crates/j-law-registry/data/income_tax/income_tax.json +352 -0
  70. data/vendor/rust/crates/j-law-registry/data/real_estate/brokerage_fee.json +125 -0
  71. data/vendor/rust/crates/j-law-registry/data/stamp_tax/stamp_tax.json +674 -0
  72. data/vendor/rust/crates/j-law-registry/data/withholding_tax/withholding_tax.json +70 -0
  73. data/vendor/rust/crates/j-law-registry/src/consumption_tax_loader.rs +325 -0
  74. data/vendor/rust/crates/j-law-registry/src/consumption_tax_schema.rs +49 -0
  75. data/vendor/rust/crates/j-law-registry/src/income_tax_deduction_loader.rs +636 -0
  76. data/vendor/rust/crates/j-law-registry/src/income_tax_deduction_schema.rs +111 -0
  77. data/vendor/rust/crates/j-law-registry/src/income_tax_loader.rs +445 -0
  78. data/vendor/rust/crates/j-law-registry/src/income_tax_schema.rs +44 -0
  79. data/vendor/rust/crates/j-law-registry/src/lib.rs +20 -0
  80. data/vendor/rust/crates/j-law-registry/src/loader.rs +221 -0
  81. data/vendor/rust/crates/j-law-registry/src/schema.rs +73 -0
  82. data/vendor/rust/crates/j-law-registry/src/stamp_tax_loader.rs +374 -0
  83. data/vendor/rust/crates/j-law-registry/src/stamp_tax_schema.rs +72 -0
  84. data/vendor/rust/crates/j-law-registry/src/validator.rs +204 -0
  85. data/vendor/rust/crates/j-law-registry/src/withholding_tax_loader.rs +310 -0
  86. data/vendor/rust/crates/j-law-registry/src/withholding_tax_schema.rs +61 -0
  87. metadata +148 -0
@@ -0,0 +1,111 @@
1
+ use serde::Deserialize;
2
+
3
+ use crate::schema::Fraction;
4
+
5
+ #[derive(Debug, Clone, Deserialize)]
6
+ pub struct BasicDeductionBracketEntry {
7
+ pub label: String,
8
+ pub income_from: u64,
9
+ pub income_to_inclusive: Option<u64>,
10
+ pub deduction_amount: u64,
11
+ }
12
+
13
+ #[derive(Debug, Clone, Deserialize)]
14
+ pub struct BasicDeductionParamsEntry {
15
+ pub brackets: Vec<BasicDeductionBracketEntry>,
16
+ }
17
+
18
+ #[derive(Debug, Clone, Deserialize)]
19
+ pub struct SpouseIncomeBracketEntry {
20
+ pub label: String,
21
+ pub taxpayer_income_from: u64,
22
+ pub taxpayer_income_to_inclusive: Option<u64>,
23
+ pub deduction_amount: u64,
24
+ pub elderly_deduction_amount: u64,
25
+ }
26
+
27
+ #[derive(Debug, Clone, Deserialize)]
28
+ pub struct SpouseDeductionParamsEntry {
29
+ pub qualifying_spouse_income_max: u64,
30
+ pub taxpayer_income_brackets: Vec<SpouseIncomeBracketEntry>,
31
+ }
32
+
33
+ #[derive(Debug, Clone, Deserialize)]
34
+ pub struct DependentDeductionParamsEntry {
35
+ pub general_deduction_amount: u64,
36
+ pub specific_deduction_amount: u64,
37
+ pub elderly_cohabiting_deduction_amount: u64,
38
+ pub elderly_other_deduction_amount: u64,
39
+ }
40
+
41
+ #[derive(Debug, Clone, Deserialize)]
42
+ pub struct PersonalDeductionParamsEntry {
43
+ pub basic: BasicDeductionParamsEntry,
44
+ pub spouse: SpouseDeductionParamsEntry,
45
+ pub dependent: DependentDeductionParamsEntry,
46
+ }
47
+
48
+ #[derive(Debug, Clone, Deserialize)]
49
+ pub struct SocialInsuranceDeductionParamsEntry {}
50
+
51
+ #[derive(Debug, Clone, Deserialize)]
52
+ pub struct MedicalDeductionParamsEntry {
53
+ pub income_threshold_rate: Fraction,
54
+ pub threshold_cap_amount: u64,
55
+ pub deduction_cap_amount: u64,
56
+ }
57
+
58
+ #[derive(Debug, Clone, Deserialize)]
59
+ pub struct LifeInsuranceDeductionBracketEntry {
60
+ pub label: String,
61
+ pub paid_from: u64,
62
+ pub paid_to_inclusive: Option<u64>,
63
+ pub rate: Fraction,
64
+ pub addition_amount: u64,
65
+ pub deduction_cap_amount: u64,
66
+ }
67
+
68
+ #[derive(Debug, Clone, Deserialize)]
69
+ pub struct LifeInsuranceDeductionParamsEntry {
70
+ pub new_contract_brackets: Vec<LifeInsuranceDeductionBracketEntry>,
71
+ pub old_contract_brackets: Vec<LifeInsuranceDeductionBracketEntry>,
72
+ pub mixed_contract_cap_amount: u64,
73
+ pub new_contract_cap_amount: u64,
74
+ pub old_contract_cap_amount: u64,
75
+ pub combined_cap_amount: u64,
76
+ }
77
+
78
+ #[derive(Debug, Clone, Deserialize)]
79
+ pub struct DonationDeductionParamsEntry {
80
+ pub income_cap_rate: Fraction,
81
+ pub non_deductible_amount: u64,
82
+ }
83
+
84
+ #[derive(Debug, Clone, Deserialize)]
85
+ pub struct ExpenseDeductionParamsEntry {
86
+ #[allow(dead_code)]
87
+ pub social_insurance: SocialInsuranceDeductionParamsEntry,
88
+ pub medical: MedicalDeductionParamsEntry,
89
+ pub life_insurance: LifeInsuranceDeductionParamsEntry,
90
+ pub donation: DonationDeductionParamsEntry,
91
+ }
92
+
93
+ #[derive(Debug, Clone, Deserialize)]
94
+ pub struct IncomeTaxDeductionParamsEntry {
95
+ pub personal: PersonalDeductionParamsEntry,
96
+ pub expense: ExpenseDeductionParamsEntry,
97
+ }
98
+
99
+ #[derive(Debug, Clone, Deserialize)]
100
+ pub struct IncomeTaxDeductionHistoryEntry {
101
+ pub effective_from: String,
102
+ pub effective_until: Option<String>,
103
+ pub params: IncomeTaxDeductionParamsEntry,
104
+ }
105
+
106
+ #[derive(Debug, Clone, Deserialize)]
107
+ pub struct IncomeTaxDeductionRegistry {
108
+ #[allow(dead_code)]
109
+ pub domain: String,
110
+ pub history: Vec<IncomeTaxDeductionHistoryEntry>,
111
+ }
@@ -0,0 +1,445 @@
1
+ use crate::income_tax_schema::{IncomeTaxHistoryEntry, IncomeTaxRegistry};
2
+ use j_law_core::domains::income_tax::params::{
3
+ IncomeTaxBracket, IncomeTaxParams, ReconstructionTaxParams,
4
+ };
5
+ use j_law_core::types::date::LegalDate;
6
+ use j_law_core::{InputError, JLawError, RegistryError};
7
+
8
+ const PATH: &str = "income_tax/income_tax.json";
9
+
10
+ /// `income_tax.json` をロードして `target_date` に対応するパラメータを返す。
11
+ ///
12
+ /// # 法的根拠
13
+ /// 所得税法 第89条第1項
14
+ ///
15
+ /// # エラー
16
+ /// - `target_date` がどの有効期間にも該当しない → `InputError::DateOutOfRange`
17
+ pub fn load_income_tax_params(target_date: LegalDate) -> Result<IncomeTaxParams, JLawError> {
18
+ target_date.validate()?;
19
+
20
+ let json_str = include_str!("../data/income_tax/income_tax.json");
21
+
22
+ let registry: IncomeTaxRegistry =
23
+ serde_json::from_str(json_str).map_err(|e| RegistryError::ParseError {
24
+ path: PATH.into(),
25
+ cause: e.to_string(),
26
+ })?;
27
+ validate_registry(&registry)?;
28
+
29
+ let date_str = target_date.to_date_str();
30
+
31
+ let entry = find_entry(&registry, &date_str).ok_or_else(|| InputError::DateOutOfRange {
32
+ date: date_str.clone(),
33
+ })?;
34
+
35
+ Ok(to_params(entry))
36
+ }
37
+
38
+ /// `IncomeTaxRegistry` の整合性を検証する。
39
+ ///
40
+ /// # 検証内容
41
+ /// - 適用期間の重複(Overlap)
42
+ /// - 適用期間の空白(Gap)
43
+ /// - 分母ゼロ(ブラケット rate.denom / reconstruction_tax rate.denom)
44
+ fn validate_registry(registry: &IncomeTaxRegistry) -> Result<(), RegistryError> {
45
+ let domain = &registry.domain;
46
+
47
+ // 分母ゼロチェック
48
+ for (i, entry) in registry.history.iter().enumerate() {
49
+ for (j, bracket) in entry.params.brackets.iter().enumerate() {
50
+ if bracket.rate.denom == 0 {
51
+ return Err(RegistryError::ZeroDenominator {
52
+ path: format!("{domain}/history[{i}]/brackets[{j}]/rate.denom"),
53
+ });
54
+ }
55
+ }
56
+ if let Some(rt) = &entry.params.reconstruction_tax {
57
+ if rt.rate.denom == 0 {
58
+ return Err(RegistryError::ZeroDenominator {
59
+ path: format!("{domain}/history[{i}]/reconstruction_tax/rate.denom"),
60
+ });
61
+ }
62
+ }
63
+ }
64
+
65
+ // 期間の重複・ギャップチェック
66
+ let mut sorted = registry.history.clone();
67
+ sorted.sort_by(|a, b| a.effective_from.cmp(&b.effective_from));
68
+
69
+ for [current, next] in sorted.array_windows::<2>() {
70
+ let current_until = match &current.effective_until {
71
+ Some(d) => d.clone(),
72
+ None => {
73
+ return Err(RegistryError::PeriodOverlap {
74
+ domain: domain.clone(),
75
+ from: next.effective_from.clone(),
76
+ until: "open-ended".into(),
77
+ });
78
+ }
79
+ };
80
+
81
+ if current_until >= next.effective_from {
82
+ return Err(RegistryError::PeriodOverlap {
83
+ domain: domain.clone(),
84
+ from: next.effective_from.clone(),
85
+ until: current_until.clone(),
86
+ });
87
+ }
88
+
89
+ let until_date = LegalDate::from_date_str(&current_until).ok_or_else(|| {
90
+ RegistryError::InvalidDateFormat {
91
+ domain: domain.clone(),
92
+ value: current_until.clone(),
93
+ }
94
+ })?;
95
+ let expected_next_from = until_date.next_day().to_date_str();
96
+ if expected_next_from != next.effective_from {
97
+ return Err(RegistryError::PeriodGap {
98
+ domain: domain.clone(),
99
+ end: current_until,
100
+ next_start: next.effective_from.clone(),
101
+ });
102
+ }
103
+ }
104
+
105
+ Ok(())
106
+ }
107
+
108
+ fn find_entry<'a>(
109
+ registry: &'a IncomeTaxRegistry,
110
+ date_str: &str,
111
+ ) -> Option<&'a IncomeTaxHistoryEntry> {
112
+ registry.history.iter().find(|entry| {
113
+ let from_ok = entry.effective_from.as_str() <= date_str;
114
+ let until_ok = match &entry.effective_until {
115
+ Some(until) => date_str <= until.as_str(),
116
+ None => true,
117
+ };
118
+ from_ok && until_ok
119
+ })
120
+ }
121
+
122
+ fn to_params(entry: &IncomeTaxHistoryEntry) -> IncomeTaxParams {
123
+ let brackets = entry
124
+ .params
125
+ .brackets
126
+ .iter()
127
+ .map(|b| IncomeTaxBracket {
128
+ label: b.label.clone(),
129
+ income_from: b.income_from,
130
+ income_to_inclusive: b.income_to_inclusive,
131
+ rate_numer: b.rate.numer,
132
+ rate_denom: b.rate.denom,
133
+ deduction: b.deduction,
134
+ })
135
+ .collect();
136
+
137
+ let reconstruction_tax =
138
+ entry
139
+ .params
140
+ .reconstruction_tax
141
+ .as_ref()
142
+ .map(|rt| ReconstructionTaxParams {
143
+ rate_numer: rt.rate.numer,
144
+ rate_denom: rt.rate.denom,
145
+ effective_from_year: rt.effective_from_year,
146
+ effective_to_year_inclusive: rt.effective_to_year_inclusive,
147
+ });
148
+
149
+ IncomeTaxParams {
150
+ brackets,
151
+ reconstruction_tax,
152
+ }
153
+ }
154
+
155
+ #[cfg(test)]
156
+ #[allow(clippy::disallowed_methods)] // テストコードでは unwrap 使用を許可
157
+ mod tests {
158
+ use super::*;
159
+ use crate::income_tax_schema::{
160
+ IncomeTaxBracketEntry, IncomeTaxHistoryEntry, IncomeTaxParamsEntry, IncomeTaxRegistry,
161
+ ReconstructionTaxEntry,
162
+ };
163
+ use crate::schema::Fraction;
164
+
165
+ fn make_registry(entries: Vec<IncomeTaxHistoryEntry>) -> IncomeTaxRegistry {
166
+ IncomeTaxRegistry {
167
+ domain: "income_tax".into(),
168
+ history: entries,
169
+ }
170
+ }
171
+
172
+ fn make_entry(from: &str, until: Option<&str>) -> IncomeTaxHistoryEntry {
173
+ IncomeTaxHistoryEntry {
174
+ effective_from: from.into(),
175
+ effective_until: until.map(|s| s.into()),
176
+ params: IncomeTaxParamsEntry {
177
+ brackets: vec![IncomeTaxBracketEntry {
178
+ label: "一律".into(),
179
+ income_from: 0,
180
+ income_to_inclusive: None,
181
+ rate: Fraction {
182
+ numer: 10,
183
+ denom: 100,
184
+ },
185
+ deduction: 0,
186
+ }],
187
+ reconstruction_tax: None,
188
+ },
189
+ }
190
+ }
191
+
192
+ #[test]
193
+ fn registry_validation_passes_for_current_data() {
194
+ let json_str = include_str!("../data/income_tax/income_tax.json");
195
+ let registry: IncomeTaxRegistry = serde_json::from_str(json_str).unwrap();
196
+ assert!(validate_registry(&registry).is_ok());
197
+ }
198
+
199
+ #[test]
200
+ fn registry_validation_detects_overlap() {
201
+ let reg = make_registry(vec![
202
+ make_entry("1989-01-01", Some("1995-01-15")),
203
+ make_entry("1995-01-01", None),
204
+ ]);
205
+ let err = validate_registry(&reg).unwrap_err();
206
+ assert!(matches!(err, RegistryError::PeriodOverlap { .. }));
207
+ }
208
+
209
+ #[test]
210
+ fn registry_validation_detects_gap() {
211
+ let reg = make_registry(vec![
212
+ make_entry("1989-01-01", Some("1994-12-31")),
213
+ make_entry("1995-01-03", None),
214
+ ]);
215
+ let err = validate_registry(&reg).unwrap_err();
216
+ assert!(matches!(err, RegistryError::PeriodGap { .. }));
217
+ }
218
+
219
+ #[test]
220
+ fn registry_validation_detects_open_ended_before_next() {
221
+ let reg = make_registry(vec![
222
+ make_entry("1989-01-01", None),
223
+ make_entry("1995-01-01", None),
224
+ ]);
225
+ let err = validate_registry(&reg).unwrap_err();
226
+ assert!(matches!(err, RegistryError::PeriodOverlap { .. }));
227
+ }
228
+
229
+ #[test]
230
+ fn registry_validation_detects_zero_denominator_bracket() {
231
+ let mut reg = make_registry(vec![make_entry("1989-01-01", None)]);
232
+ reg.history[0].params.brackets[0].rate.denom = 0;
233
+ let err = validate_registry(&reg).unwrap_err();
234
+ assert!(matches!(err, RegistryError::ZeroDenominator { .. }));
235
+ }
236
+
237
+ #[test]
238
+ fn registry_validation_detects_zero_denominator_reconstruction_tax() {
239
+ let mut reg = make_registry(vec![make_entry("2013-01-01", None)]);
240
+ reg.history[0].params.reconstruction_tax = Some(ReconstructionTaxEntry {
241
+ rate: Fraction {
242
+ numer: 21,
243
+ denom: 0,
244
+ },
245
+ effective_from_year: 2013,
246
+ effective_to_year_inclusive: 2037,
247
+ });
248
+ let err = validate_registry(&reg).unwrap_err();
249
+ assert!(matches!(err, RegistryError::ZeroDenominator { .. }));
250
+ }
251
+
252
+ // ─── 現行(2015年〜)7段階 ────────────────────────────────────────────────
253
+
254
+ #[test]
255
+ fn load_2024_params() {
256
+ let params = load_income_tax_params(LegalDate::new(2024, 1, 1)).unwrap();
257
+ assert_eq!(params.brackets.len(), 7);
258
+ assert!(params.reconstruction_tax.is_some());
259
+ let rt = params.reconstruction_tax.unwrap();
260
+ assert_eq!(rt.rate_numer, 21);
261
+ assert_eq!(rt.rate_denom, 1000);
262
+ }
263
+
264
+ #[test]
265
+ fn load_2015_params() {
266
+ let params = load_income_tax_params(LegalDate::new(2015, 1, 1)).unwrap();
267
+ assert_eq!(params.brackets.len(), 7);
268
+ }
269
+
270
+ // ─── 平成19〜26年分(2007〜2014年)6段階 ──────────────────────────────────
271
+
272
+ #[test]
273
+ fn load_2007_params() {
274
+ let params = load_income_tax_params(LegalDate::new(2010, 1, 1)).unwrap();
275
+ assert_eq!(params.brackets.len(), 6);
276
+ // 最高税率は40%
277
+ let top = params.brackets.last().unwrap();
278
+ assert_eq!(top.rate_numer, 40);
279
+ assert_eq!(top.deduction, 2_796_000);
280
+ }
281
+
282
+ #[test]
283
+ fn load_2007_params_no_reconstruction_tax() {
284
+ // 復興特別所得税は2013年開始。2007〜2012年は None であること
285
+ let params = load_income_tax_params(LegalDate::new(2010, 1, 1)).unwrap();
286
+ assert!(params.reconstruction_tax.is_none());
287
+ }
288
+
289
+ #[test]
290
+ fn boundary_2012_12_31_no_reconstruction_tax() {
291
+ let params = load_income_tax_params(LegalDate::new(2012, 12, 31)).unwrap();
292
+ assert_eq!(params.brackets.len(), 6);
293
+ assert!(params.reconstruction_tax.is_none());
294
+ }
295
+
296
+ #[test]
297
+ fn boundary_2013_01_01_has_reconstruction_tax() {
298
+ let params = load_income_tax_params(LegalDate::new(2013, 1, 1)).unwrap();
299
+ assert_eq!(params.brackets.len(), 6);
300
+ assert!(params.reconstruction_tax.is_some());
301
+ }
302
+
303
+ #[test]
304
+ fn boundary_2014_12_31() {
305
+ let params = load_income_tax_params(LegalDate::new(2014, 12, 31)).unwrap();
306
+ assert_eq!(params.brackets.len(), 6);
307
+ assert!(params.reconstruction_tax.is_some());
308
+ }
309
+
310
+ #[test]
311
+ fn boundary_2015_01_01() {
312
+ let params = load_income_tax_params(LegalDate::new(2015, 1, 1)).unwrap();
313
+ assert_eq!(params.brackets.len(), 7);
314
+ }
315
+
316
+ // ─── 平成11〜18年分(1999〜2006年)4段階 ──────────────────────────────────
317
+
318
+ #[test]
319
+ fn load_1999_params() {
320
+ let params = load_income_tax_params(LegalDate::new(2003, 1, 1)).unwrap();
321
+ assert_eq!(params.brackets.len(), 4);
322
+ // 最高税率は37%
323
+ let top = params.brackets.last().unwrap();
324
+ assert_eq!(top.rate_numer, 37);
325
+ assert_eq!(top.deduction, 2_490_000);
326
+ assert!(params.reconstruction_tax.is_none());
327
+ }
328
+
329
+ #[test]
330
+ fn boundary_2006_12_31() {
331
+ let params = load_income_tax_params(LegalDate::new(2006, 12, 31)).unwrap();
332
+ assert_eq!(params.brackets.len(), 4);
333
+ }
334
+
335
+ #[test]
336
+ fn boundary_2007_01_01() {
337
+ let params = load_income_tax_params(LegalDate::new(2007, 1, 1)).unwrap();
338
+ assert_eq!(params.brackets.len(), 6);
339
+ }
340
+
341
+ // ─── 平成7〜10年分(1995〜1998年)5段階 ───────────────────────────────────
342
+
343
+ #[test]
344
+ fn load_1995_params() {
345
+ let params = load_income_tax_params(LegalDate::new(1997, 1, 1)).unwrap();
346
+ assert_eq!(params.brackets.len(), 5);
347
+ // 最高税率は50%
348
+ let top = params.brackets.last().unwrap();
349
+ assert_eq!(top.rate_numer, 50);
350
+ assert_eq!(top.deduction, 6_030_000);
351
+ assert!(params.reconstruction_tax.is_none());
352
+ }
353
+
354
+ #[test]
355
+ fn boundary_1998_12_31() {
356
+ let params = load_income_tax_params(LegalDate::new(1998, 12, 31)).unwrap();
357
+ assert_eq!(params.brackets.len(), 5);
358
+ }
359
+
360
+ #[test]
361
+ fn boundary_1999_01_01() {
362
+ let params = load_income_tax_params(LegalDate::new(1999, 1, 1)).unwrap();
363
+ assert_eq!(params.brackets.len(), 4);
364
+ }
365
+
366
+ // 1995改正では10%上限が330万円(1989改正の300万円から変更)
367
+ #[test]
368
+ fn boundary_1995_01_01_bracket_change() {
369
+ let params = load_income_tax_params(LegalDate::new(1995, 1, 1)).unwrap();
370
+ let first = &params.brackets[0];
371
+ assert_eq!(first.income_to_inclusive, Some(3_300_000));
372
+ assert_eq!(first.rate_numer, 10);
373
+ assert_eq!(first.deduction, 0);
374
+ }
375
+
376
+ // ─── 平成元〜6年分(1989〜1994年)5段階 ───────────────────────────────────
377
+
378
+ #[test]
379
+ fn load_1989_params() {
380
+ let params = load_income_tax_params(LegalDate::new(1990, 1, 1)).unwrap();
381
+ assert_eq!(params.brackets.len(), 5);
382
+ // 最高税率は50%、1989期間は10%上限が300万円
383
+ let first = &params.brackets[0];
384
+ assert_eq!(first.income_to_inclusive, Some(3_000_000));
385
+ assert_eq!(first.rate_numer, 10);
386
+ assert!(params.reconstruction_tax.is_none());
387
+ }
388
+
389
+ #[test]
390
+ fn invalid_date_returns_input_error() {
391
+ let result = load_income_tax_params(LegalDate::new(2024, 2, 30));
392
+ assert!(matches!(
393
+ result,
394
+ Err(JLawError::Input(j_law_core::InputError::InvalidDate { .. }))
395
+ ));
396
+ }
397
+
398
+ #[test]
399
+ fn boundary_1989_01_01() {
400
+ let params = load_income_tax_params(LegalDate::new(1989, 1, 1)).unwrap();
401
+ assert_eq!(params.brackets.len(), 5);
402
+ }
403
+
404
+ #[test]
405
+ fn boundary_1994_12_31() {
406
+ let params = load_income_tax_params(LegalDate::new(1994, 12, 31)).unwrap();
407
+ // 1989ブラケット: 10%上限は300万円
408
+ let first = &params.brackets[0];
409
+ assert_eq!(first.income_to_inclusive, Some(3_000_000));
410
+ }
411
+
412
+ // ─── 範囲外 ──────────────────────────────────────────────────────────────
413
+
414
+ #[test]
415
+ fn date_out_of_range_returns_error() {
416
+ let result = load_income_tax_params(LegalDate::new(1988, 12, 31));
417
+ assert!(matches!(
418
+ result,
419
+ Err(JLawError::Input(InputError::DateOutOfRange { .. }))
420
+ ));
421
+ }
422
+
423
+ #[test]
424
+ fn registry_data_integrity_check() {
425
+ let json_str = include_str!("../data/income_tax/income_tax.json");
426
+ let registry: IncomeTaxRegistry = serde_json::from_str(json_str).unwrap();
427
+
428
+ // 基本的な整合性チェック
429
+ assert!(!registry.history.is_empty(), "Registry should not be empty");
430
+
431
+ for entry in &registry.history {
432
+ // 分母ゼロチェック
433
+ for bracket in &entry.params.brackets {
434
+ assert_ne!(bracket.rate.denom, 0, "Rate denominator must not be zero");
435
+ }
436
+
437
+ if let Some(rt) = &entry.params.reconstruction_tax {
438
+ assert_ne!(
439
+ rt.rate.denom, 0,
440
+ "Reconstruction tax denominator must not be zero"
441
+ );
442
+ }
443
+ }
444
+ }
445
+ }
@@ -0,0 +1,44 @@
1
+ use serde::Deserialize;
2
+
3
+ use crate::schema::Fraction;
4
+
5
+ /// 所得税の税率ブラケット(JSON)。
6
+ #[derive(Debug, Clone, Deserialize)]
7
+ pub struct IncomeTaxBracketEntry {
8
+ pub label: String,
9
+ pub income_from: u64,
10
+ pub income_to_inclusive: Option<u64>,
11
+ pub rate: Fraction,
12
+ pub deduction: u64,
13
+ }
14
+
15
+ /// 復興特別所得税パラメータ(JSON)。
16
+ #[derive(Debug, Clone, Deserialize)]
17
+ pub struct ReconstructionTaxEntry {
18
+ pub rate: Fraction,
19
+ pub effective_from_year: u16,
20
+ pub effective_to_year_inclusive: u16,
21
+ }
22
+
23
+ /// 所得税の計算パラメータ(JSON)。
24
+ #[derive(Debug, Clone, Deserialize)]
25
+ pub struct IncomeTaxParamsEntry {
26
+ pub brackets: Vec<IncomeTaxBracketEntry>,
27
+ pub reconstruction_tax: Option<ReconstructionTaxEntry>,
28
+ }
29
+
30
+ /// 所得税の履歴エントリ(JSON)。
31
+ #[derive(Debug, Clone, Deserialize)]
32
+ pub struct IncomeTaxHistoryEntry {
33
+ pub effective_from: String,
34
+ pub effective_until: Option<String>,
35
+ pub params: IncomeTaxParamsEntry,
36
+ }
37
+
38
+ /// `income_tax.json` のルートスキーマ。
39
+ #[derive(Debug, Clone, Deserialize)]
40
+ pub struct IncomeTaxRegistry {
41
+ #[allow(dead_code)]
42
+ pub domain: String,
43
+ pub history: Vec<IncomeTaxHistoryEntry>,
44
+ }
@@ -0,0 +1,20 @@
1
+ pub mod consumption_tax_loader;
2
+ pub(crate) mod consumption_tax_schema;
3
+ pub mod income_tax_deduction_loader;
4
+ pub(crate) mod income_tax_deduction_schema;
5
+ pub mod income_tax_loader;
6
+ pub(crate) mod income_tax_schema;
7
+ pub mod loader;
8
+ pub(crate) mod schema;
9
+ pub mod stamp_tax_loader;
10
+ pub(crate) mod stamp_tax_schema;
11
+ mod validator;
12
+ pub mod withholding_tax_loader;
13
+ pub(crate) mod withholding_tax_schema;
14
+
15
+ pub use consumption_tax_loader::load_consumption_tax_params;
16
+ pub use income_tax_deduction_loader::load_income_tax_deduction_params;
17
+ pub use income_tax_loader::load_income_tax_params;
18
+ pub use loader::load_brokerage_fee_params;
19
+ pub use stamp_tax_loader::load_stamp_tax_params;
20
+ pub use withholding_tax_loader::load_withholding_tax_params;