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,636 @@
1
+ use crate::income_tax_deduction_schema::{
2
+ IncomeTaxDeductionHistoryEntry, IncomeTaxDeductionRegistry,
3
+ };
4
+ use j_law_core::domains::income_tax::deduction::{
5
+ BasicDeductionBracket, BasicDeductionParams, DependentDeductionParams, DonationDeductionParams,
6
+ ExpenseDeductionParams, IncomeDeductionParams, LifeInsuranceDeductionBracket,
7
+ LifeInsuranceDeductionParams, MedicalDeductionParams, PersonalDeductionParams,
8
+ SocialInsuranceDeductionParams, SpouseDeductionParams, SpouseIncomeBracket,
9
+ };
10
+ use j_law_core::types::date::LegalDate;
11
+ use j_law_core::{InputError, JLawError, RegistryError};
12
+
13
+ /// `deductions.json` をロードして `target_date` に対応する所得控除パラメータを返す。
14
+ ///
15
+ /// # 法的根拠
16
+ /// 所得税法 第73条(医療費控除)
17
+ /// 所得税法 第74条(社会保険料控除)
18
+ /// 所得税法 第76条(生命保険料控除)
19
+ /// 所得税法 第78条(寄附金控除)
20
+ /// 所得税法 第83条(配偶者控除)
21
+ /// 所得税法 第84条(扶養控除)
22
+ /// 所得税法 第86条(基礎控除)
23
+ pub fn load_income_tax_deduction_params(
24
+ target_date: LegalDate,
25
+ ) -> Result<IncomeDeductionParams, JLawError> {
26
+ target_date.validate()?;
27
+
28
+ let json_str = include_str!("../data/income_tax/deductions.json");
29
+
30
+ let registry: IncomeTaxDeductionRegistry =
31
+ serde_json::from_str(json_str).map_err(|e| RegistryError::ParseError {
32
+ path: "income_tax/deductions.json".into(),
33
+ cause: e.to_string(),
34
+ })?;
35
+ validate_registry(&registry)?;
36
+
37
+ let date_str = target_date.to_date_str();
38
+ let entry = find_entry(&registry, &date_str).ok_or_else(|| InputError::DateOutOfRange {
39
+ date: date_str.clone(),
40
+ })?;
41
+
42
+ Ok(to_params(entry))
43
+ }
44
+
45
+ fn find_entry<'a>(
46
+ registry: &'a IncomeTaxDeductionRegistry,
47
+ date_str: &str,
48
+ ) -> Option<&'a IncomeTaxDeductionHistoryEntry> {
49
+ registry.history.iter().find(|entry| {
50
+ let from_ok = entry.effective_from.as_str() <= date_str;
51
+ let until_ok = match &entry.effective_until {
52
+ Some(until) => date_str <= until.as_str(),
53
+ None => true,
54
+ };
55
+ from_ok && until_ok
56
+ })
57
+ }
58
+
59
+ fn to_params(entry: &IncomeTaxDeductionHistoryEntry) -> IncomeDeductionParams {
60
+ IncomeDeductionParams {
61
+ personal: PersonalDeductionParams {
62
+ basic: BasicDeductionParams {
63
+ brackets: entry
64
+ .params
65
+ .personal
66
+ .basic
67
+ .brackets
68
+ .iter()
69
+ .map(|bracket| BasicDeductionBracket {
70
+ label: bracket.label.clone(),
71
+ income_from: bracket.income_from,
72
+ income_to_inclusive: bracket.income_to_inclusive,
73
+ deduction_amount: bracket.deduction_amount,
74
+ })
75
+ .collect(),
76
+ },
77
+ spouse: SpouseDeductionParams {
78
+ qualifying_spouse_income_max: entry
79
+ .params
80
+ .personal
81
+ .spouse
82
+ .qualifying_spouse_income_max,
83
+ taxpayer_income_brackets: entry
84
+ .params
85
+ .personal
86
+ .spouse
87
+ .taxpayer_income_brackets
88
+ .iter()
89
+ .map(|bracket| SpouseIncomeBracket {
90
+ label: bracket.label.clone(),
91
+ taxpayer_income_from: bracket.taxpayer_income_from,
92
+ taxpayer_income_to_inclusive: bracket.taxpayer_income_to_inclusive,
93
+ deduction_amount: bracket.deduction_amount,
94
+ elderly_deduction_amount: bracket.elderly_deduction_amount,
95
+ })
96
+ .collect(),
97
+ },
98
+ dependent: DependentDeductionParams {
99
+ general_deduction_amount: entry.params.personal.dependent.general_deduction_amount,
100
+ specific_deduction_amount: entry
101
+ .params
102
+ .personal
103
+ .dependent
104
+ .specific_deduction_amount,
105
+ elderly_cohabiting_deduction_amount: entry
106
+ .params
107
+ .personal
108
+ .dependent
109
+ .elderly_cohabiting_deduction_amount,
110
+ elderly_other_deduction_amount: entry
111
+ .params
112
+ .personal
113
+ .dependent
114
+ .elderly_other_deduction_amount,
115
+ },
116
+ },
117
+ expense: ExpenseDeductionParams {
118
+ social_insurance: SocialInsuranceDeductionParams,
119
+ medical: MedicalDeductionParams {
120
+ income_threshold_rate_numer: entry
121
+ .params
122
+ .expense
123
+ .medical
124
+ .income_threshold_rate
125
+ .numer,
126
+ income_threshold_rate_denom: entry
127
+ .params
128
+ .expense
129
+ .medical
130
+ .income_threshold_rate
131
+ .denom,
132
+ threshold_cap_amount: entry.params.expense.medical.threshold_cap_amount,
133
+ deduction_cap_amount: entry.params.expense.medical.deduction_cap_amount,
134
+ },
135
+ life_insurance: LifeInsuranceDeductionParams {
136
+ new_contract_brackets: entry
137
+ .params
138
+ .expense
139
+ .life_insurance
140
+ .new_contract_brackets
141
+ .iter()
142
+ .map(to_life_insurance_bracket)
143
+ .collect(),
144
+ old_contract_brackets: entry
145
+ .params
146
+ .expense
147
+ .life_insurance
148
+ .old_contract_brackets
149
+ .iter()
150
+ .map(to_life_insurance_bracket)
151
+ .collect(),
152
+ mixed_contract_cap_amount: entry
153
+ .params
154
+ .expense
155
+ .life_insurance
156
+ .mixed_contract_cap_amount,
157
+ new_contract_cap_amount: entry
158
+ .params
159
+ .expense
160
+ .life_insurance
161
+ .new_contract_cap_amount,
162
+ old_contract_cap_amount: entry
163
+ .params
164
+ .expense
165
+ .life_insurance
166
+ .old_contract_cap_amount,
167
+ combined_cap_amount: entry.params.expense.life_insurance.combined_cap_amount,
168
+ },
169
+ donation: DonationDeductionParams {
170
+ income_cap_rate_numer: entry.params.expense.donation.income_cap_rate.numer,
171
+ income_cap_rate_denom: entry.params.expense.donation.income_cap_rate.denom,
172
+ non_deductible_amount: entry.params.expense.donation.non_deductible_amount,
173
+ },
174
+ },
175
+ }
176
+ }
177
+
178
+ fn to_life_insurance_bracket(
179
+ bracket: &crate::income_tax_deduction_schema::LifeInsuranceDeductionBracketEntry,
180
+ ) -> LifeInsuranceDeductionBracket {
181
+ LifeInsuranceDeductionBracket {
182
+ label: bracket.label.clone(),
183
+ paid_from: bracket.paid_from,
184
+ paid_to_inclusive: bracket.paid_to_inclusive,
185
+ rate_numer: bracket.rate.numer,
186
+ rate_denom: bracket.rate.denom,
187
+ addition_amount: bracket.addition_amount,
188
+ deduction_cap_amount: bracket.deduction_cap_amount,
189
+ }
190
+ }
191
+
192
+ fn validate_registry(registry: &IncomeTaxDeductionRegistry) -> Result<(), JLawError> {
193
+ validate_periods(registry)?;
194
+ validate_denominators(registry)?;
195
+ Ok(())
196
+ }
197
+
198
+ fn validate_periods(registry: &IncomeTaxDeductionRegistry) -> Result<(), RegistryError> {
199
+ let mut sorted = registry.history.iter().collect::<Vec<_>>();
200
+ sorted.sort_by(|a, b| a.effective_from.cmp(&b.effective_from));
201
+
202
+ for [current, next] in sorted.array_windows::<2>() {
203
+ let current = *current;
204
+ let next = *next;
205
+
206
+ let Some(current_until) = &current.effective_until else {
207
+ return Err(RegistryError::PeriodOverlap {
208
+ domain: registry.domain.clone(),
209
+ from: next.effective_from.clone(),
210
+ until: "open-ended".into(),
211
+ });
212
+ };
213
+
214
+ if current_until >= &next.effective_from {
215
+ return Err(RegistryError::PeriodOverlap {
216
+ domain: registry.domain.clone(),
217
+ from: next.effective_from.clone(),
218
+ until: current_until.clone(),
219
+ });
220
+ }
221
+
222
+ let expected_next_start =
223
+ next_date_str(current_until, "income_tax/deductions.json/effective_until")?;
224
+ if expected_next_start != next.effective_from {
225
+ return Err(RegistryError::PeriodGap {
226
+ domain: registry.domain.clone(),
227
+ end: current_until.clone(),
228
+ next_start: next.effective_from.clone(),
229
+ });
230
+ }
231
+ }
232
+
233
+ Ok(())
234
+ }
235
+
236
+ fn validate_denominators(registry: &IncomeTaxDeductionRegistry) -> Result<(), RegistryError> {
237
+ for (entry_index, entry) in registry.history.iter().enumerate() {
238
+ if entry.params.expense.medical.income_threshold_rate.denom == 0 {
239
+ return Err(RegistryError::ZeroDenominator {
240
+ path: format!(
241
+ "{}/history[{entry_index}]/expense/medical/income_threshold_rate.denom",
242
+ registry.domain
243
+ ),
244
+ });
245
+ }
246
+ if entry.params.expense.donation.income_cap_rate.denom == 0 {
247
+ return Err(RegistryError::ZeroDenominator {
248
+ path: format!(
249
+ "{}/history[{entry_index}]/expense/donation/income_cap_rate.denom",
250
+ registry.domain
251
+ ),
252
+ });
253
+ }
254
+
255
+ for (bracket_index, bracket) in entry
256
+ .params
257
+ .expense
258
+ .life_insurance
259
+ .new_contract_brackets
260
+ .iter()
261
+ .enumerate()
262
+ {
263
+ if bracket.rate.denom == 0 {
264
+ return Err(RegistryError::ZeroDenominator {
265
+ path: format!(
266
+ "{}/history[{entry_index}]/expense/life_insurance/new_contract_brackets[{bracket_index}]/rate.denom",
267
+ registry.domain
268
+ ),
269
+ });
270
+ }
271
+ }
272
+
273
+ for (bracket_index, bracket) in entry
274
+ .params
275
+ .expense
276
+ .life_insurance
277
+ .old_contract_brackets
278
+ .iter()
279
+ .enumerate()
280
+ {
281
+ if bracket.rate.denom == 0 {
282
+ return Err(RegistryError::ZeroDenominator {
283
+ path: format!(
284
+ "{}/history[{entry_index}]/expense/life_insurance/old_contract_brackets[{bracket_index}]/rate.denom",
285
+ registry.domain
286
+ ),
287
+ });
288
+ }
289
+ }
290
+ }
291
+
292
+ Ok(())
293
+ }
294
+
295
+ fn next_date_str(date: &str, path: &str) -> Result<String, RegistryError> {
296
+ let (year, month, day) = parse_iso_date(date, path)?;
297
+ let max_day = days_in_month(year, month);
298
+
299
+ let (next_year, next_month, next_day) = if day < max_day {
300
+ (year, month, day + 1)
301
+ } else if month < 12 {
302
+ (year, month + 1, 1)
303
+ } else {
304
+ (year + 1, 1, 1)
305
+ };
306
+
307
+ Ok(format!("{next_year:04}-{next_month:02}-{next_day:02}"))
308
+ }
309
+
310
+ fn parse_iso_date(date: &str, path: &str) -> Result<(u32, u32, u32), RegistryError> {
311
+ let mut parts = date.split('-');
312
+ let year = parts
313
+ .next()
314
+ .ok_or_else(|| invalid_date_parse_error(path, date))?
315
+ .parse::<u32>()
316
+ .map_err(|_| invalid_date_parse_error(path, date))?;
317
+ let month = parts
318
+ .next()
319
+ .ok_or_else(|| invalid_date_parse_error(path, date))?
320
+ .parse::<u32>()
321
+ .map_err(|_| invalid_date_parse_error(path, date))?;
322
+ let day = parts
323
+ .next()
324
+ .ok_or_else(|| invalid_date_parse_error(path, date))?
325
+ .parse::<u32>()
326
+ .map_err(|_| invalid_date_parse_error(path, date))?;
327
+
328
+ if parts.next().is_some()
329
+ || !(1..=12).contains(&month)
330
+ || day == 0
331
+ || day > days_in_month(year, month)
332
+ {
333
+ return Err(invalid_date_parse_error(path, date));
334
+ }
335
+
336
+ Ok((year, month, day))
337
+ }
338
+
339
+ fn invalid_date_parse_error(path: &str, value: &str) -> RegistryError {
340
+ RegistryError::ParseError {
341
+ path: path.into(),
342
+ cause: format!("invalid ISO date: {value}"),
343
+ }
344
+ }
345
+
346
+ fn days_in_month(year: u32, month: u32) -> u32 {
347
+ match month {
348
+ 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
349
+ 4 | 6 | 9 | 11 => 30,
350
+ 2 if is_leap_year(year) => 29,
351
+ 2 => 28,
352
+ _ => 0,
353
+ }
354
+ }
355
+
356
+ fn is_leap_year(year: u32) -> bool {
357
+ year.is_multiple_of(4) && !year.is_multiple_of(100) || year.is_multiple_of(400)
358
+ }
359
+
360
+ #[cfg(test)]
361
+ #[allow(clippy::disallowed_methods)]
362
+ mod tests {
363
+ use super::*;
364
+ use serde_json::json;
365
+
366
+ fn registry_from_value(value: serde_json::Value) -> IncomeTaxDeductionRegistry {
367
+ serde_json::from_value(value).unwrap()
368
+ }
369
+
370
+ fn sample_registry() -> IncomeTaxDeductionRegistry {
371
+ registry_from_value(json!({
372
+ "domain": "income_tax",
373
+ "history": [
374
+ {
375
+ "effective_from": "2019-01-01",
376
+ "effective_until": "2019-12-31",
377
+ "params": {
378
+ "personal": {
379
+ "basic": {
380
+ "brackets": [
381
+ {
382
+ "label": "一律",
383
+ "income_from": 0,
384
+ "income_to_inclusive": null,
385
+ "deduction_amount": 380000
386
+ }
387
+ ]
388
+ },
389
+ "spouse": {
390
+ "qualifying_spouse_income_max": 380000,
391
+ "taxpayer_income_brackets": [
392
+ {
393
+ "label": "1000万円以下",
394
+ "taxpayer_income_from": 0,
395
+ "taxpayer_income_to_inclusive": null,
396
+ "deduction_amount": 380000,
397
+ "elderly_deduction_amount": 480000
398
+ }
399
+ ]
400
+ },
401
+ "dependent": {
402
+ "general_deduction_amount": 380000,
403
+ "specific_deduction_amount": 630000,
404
+ "elderly_cohabiting_deduction_amount": 580000,
405
+ "elderly_other_deduction_amount": 480000
406
+ }
407
+ },
408
+ "expense": {
409
+ "social_insurance": {},
410
+ "medical": {
411
+ "income_threshold_rate": { "numer": 5, "denom": 100 },
412
+ "threshold_cap_amount": 100000,
413
+ "deduction_cap_amount": 2000000
414
+ },
415
+ "life_insurance": {
416
+ "new_contract_brackets": [
417
+ {
418
+ "label": "一律",
419
+ "paid_from": 0,
420
+ "paid_to_inclusive": null,
421
+ "rate": { "numer": 1, "denom": 1 },
422
+ "addition_amount": 0,
423
+ "deduction_cap_amount": 40000
424
+ }
425
+ ],
426
+ "old_contract_brackets": [
427
+ {
428
+ "label": "一律",
429
+ "paid_from": 0,
430
+ "paid_to_inclusive": null,
431
+ "rate": { "numer": 1, "denom": 1 },
432
+ "addition_amount": 0,
433
+ "deduction_cap_amount": 50000
434
+ }
435
+ ],
436
+ "mixed_contract_cap_amount": 40000,
437
+ "new_contract_cap_amount": 40000,
438
+ "old_contract_cap_amount": 50000,
439
+ "combined_cap_amount": 120000
440
+ },
441
+ "donation": {
442
+ "income_cap_rate": { "numer": 40, "denom": 100 },
443
+ "non_deductible_amount": 2000
444
+ }
445
+ }
446
+ }
447
+ },
448
+ {
449
+ "effective_from": "2020-01-01",
450
+ "effective_until": null,
451
+ "params": {
452
+ "personal": {
453
+ "basic": {
454
+ "brackets": [
455
+ {
456
+ "label": "一律",
457
+ "income_from": 0,
458
+ "income_to_inclusive": null,
459
+ "deduction_amount": 480000
460
+ }
461
+ ]
462
+ },
463
+ "spouse": {
464
+ "qualifying_spouse_income_max": 480000,
465
+ "taxpayer_income_brackets": [
466
+ {
467
+ "label": "1000万円以下",
468
+ "taxpayer_income_from": 0,
469
+ "taxpayer_income_to_inclusive": null,
470
+ "deduction_amount": 380000,
471
+ "elderly_deduction_amount": 480000
472
+ }
473
+ ]
474
+ },
475
+ "dependent": {
476
+ "general_deduction_amount": 380000,
477
+ "specific_deduction_amount": 630000,
478
+ "elderly_cohabiting_deduction_amount": 580000,
479
+ "elderly_other_deduction_amount": 480000
480
+ }
481
+ },
482
+ "expense": {
483
+ "social_insurance": {},
484
+ "medical": {
485
+ "income_threshold_rate": { "numer": 5, "denom": 100 },
486
+ "threshold_cap_amount": 100000,
487
+ "deduction_cap_amount": 2000000
488
+ },
489
+ "life_insurance": {
490
+ "new_contract_brackets": [
491
+ {
492
+ "label": "一律",
493
+ "paid_from": 0,
494
+ "paid_to_inclusive": null,
495
+ "rate": { "numer": 1, "denom": 1 },
496
+ "addition_amount": 0,
497
+ "deduction_cap_amount": 40000
498
+ }
499
+ ],
500
+ "old_contract_brackets": [
501
+ {
502
+ "label": "一律",
503
+ "paid_from": 0,
504
+ "paid_to_inclusive": null,
505
+ "rate": { "numer": 1, "denom": 1 },
506
+ "addition_amount": 0,
507
+ "deduction_cap_amount": 50000
508
+ }
509
+ ],
510
+ "mixed_contract_cap_amount": 40000,
511
+ "new_contract_cap_amount": 40000,
512
+ "old_contract_cap_amount": 50000,
513
+ "combined_cap_amount": 120000
514
+ },
515
+ "donation": {
516
+ "income_cap_rate": { "numer": 40, "denom": 100 },
517
+ "non_deductible_amount": 2000
518
+ }
519
+ }
520
+ }
521
+ }
522
+ ]
523
+ }))
524
+ }
525
+
526
+ #[test]
527
+ fn load_2024_params() {
528
+ let params = load_income_tax_deduction_params(LegalDate::new(2024, 1, 1)).unwrap();
529
+ assert_eq!(params.personal.basic.brackets[0].deduction_amount, 480_000);
530
+ assert_eq!(params.personal.spouse.qualifying_spouse_income_max, 480_000);
531
+ assert_eq!(params.expense.medical.deduction_cap_amount, 2_000_000);
532
+ assert_eq!(params.expense.life_insurance.combined_cap_amount, 120_000);
533
+ assert_eq!(params.expense.donation.non_deductible_amount, 2_000);
534
+ }
535
+
536
+ #[test]
537
+ fn load_2019_params() {
538
+ let params = load_income_tax_deduction_params(LegalDate::new(2019, 6, 1)).unwrap();
539
+ assert_eq!(params.personal.basic.brackets.len(), 1);
540
+ assert_eq!(params.personal.basic.brackets[0].deduction_amount, 380_000);
541
+ assert_eq!(params.personal.spouse.qualifying_spouse_income_max, 380_000);
542
+ }
543
+
544
+ #[test]
545
+ fn boundary_2019_12_31_is_pre_2020() {
546
+ let params = load_income_tax_deduction_params(LegalDate::new(2019, 12, 31)).unwrap();
547
+ assert_eq!(params.personal.basic.brackets[0].deduction_amount, 380_000);
548
+ }
549
+
550
+ #[test]
551
+ fn boundary_2020_01_01_is_post_2020() {
552
+ let params = load_income_tax_deduction_params(LegalDate::new(2020, 1, 1)).unwrap();
553
+ assert_eq!(params.personal.basic.brackets[0].deduction_amount, 480_000);
554
+ }
555
+
556
+ #[test]
557
+ fn date_out_of_range_returns_error() {
558
+ let result = load_income_tax_deduction_params(LegalDate::new(2017, 12, 31));
559
+ assert!(matches!(
560
+ result,
561
+ Err(JLawError::Input(InputError::DateOutOfRange { .. }))
562
+ ));
563
+ }
564
+
565
+ #[test]
566
+ fn registry_validation_accepts_current_data() {
567
+ let json_str = include_str!("../data/income_tax/deductions.json");
568
+ let registry: IncomeTaxDeductionRegistry = serde_json::from_str(json_str).unwrap();
569
+ assert!(validate_registry(&registry).is_ok());
570
+ }
571
+
572
+ #[test]
573
+ fn registry_validation_rejects_period_overlap() {
574
+ let mut registry = sample_registry();
575
+ registry.history[0].effective_until = Some("2020-01-15".into());
576
+
577
+ let result = validate_registry(&registry);
578
+ assert!(matches!(
579
+ result,
580
+ Err(JLawError::Registry(RegistryError::PeriodOverlap { .. }))
581
+ ));
582
+ }
583
+
584
+ #[test]
585
+ fn registry_validation_rejects_period_gap() {
586
+ let mut registry = sample_registry();
587
+ registry.history[0].effective_until = Some("2019-12-30".into());
588
+
589
+ let result = validate_registry(&registry);
590
+ assert!(matches!(
591
+ result,
592
+ Err(JLawError::Registry(RegistryError::PeriodGap { .. }))
593
+ ));
594
+ }
595
+
596
+ #[test]
597
+ fn registry_validation_rejects_zero_denominator() {
598
+ let mut registry = sample_registry();
599
+ registry.history[0]
600
+ .params
601
+ .expense
602
+ .medical
603
+ .income_threshold_rate
604
+ .denom = 0;
605
+
606
+ let result = validate_registry(&registry);
607
+ assert!(matches!(
608
+ result,
609
+ Err(JLawError::Registry(RegistryError::ZeroDenominator { .. }))
610
+ ));
611
+ }
612
+
613
+ #[test]
614
+ fn parse_iso_date_accepts_leap_day_in_leap_year() {
615
+ let result = parse_iso_date("2024-02-29", "history[0].effective_from");
616
+ assert!(matches!(result, Ok((2024, 2, 29))));
617
+ }
618
+
619
+ #[test]
620
+ fn parse_iso_date_rejects_leap_day_in_common_year() {
621
+ let result = parse_iso_date("2023-02-29", "history[0].effective_from");
622
+ assert!(matches!(result, Err(RegistryError::ParseError { .. })));
623
+ }
624
+
625
+ #[test]
626
+ fn parse_iso_date_rejects_century_non_leap_year() {
627
+ let result = parse_iso_date("1900-02-29", "history[0].effective_from");
628
+ assert!(matches!(result, Err(RegistryError::ParseError { .. })));
629
+ }
630
+
631
+ #[test]
632
+ fn parse_iso_date_accepts_century_leap_year() {
633
+ let result = parse_iso_date("2000-02-29", "history[0].effective_from");
634
+ assert!(matches!(result, Ok((2000, 2, 29))));
635
+ }
636
+ }