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.
- checksums.yaml +7 -0
- data/Gemfile +9 -0
- data/README.md +109 -0
- data/Rakefile +87 -0
- data/ext/j_law_ruby/extconf.rb +34 -0
- data/lib/j_law_ruby/build_support.rb +129 -0
- data/lib/j_law_ruby/c_ffi.rb +662 -0
- data/lib/j_law_ruby.rb +532 -0
- data/rake_support/vendor_rust.rb +51 -0
- data/test/test_c_ffi_adapter.rb +46 -0
- data/test/test_consumption_tax.rb +66 -0
- data/test/test_gemspec.rb +82 -0
- data/test/test_income_tax.rb +77 -0
- data/test/test_income_tax_deductions.rb +82 -0
- data/test/test_real_estate.rb +98 -0
- data/test/test_stamp_tax.rb +68 -0
- data/test/test_withholding_tax.rb +65 -0
- data/vendor/rust/Cargo.lock +235 -0
- data/vendor/rust/Cargo.toml +12 -0
- data/vendor/rust/crates/j-law-c-ffi/Cargo.toml +20 -0
- data/vendor/rust/crates/j-law-c-ffi/j_law_c_ffi.h +493 -0
- data/vendor/rust/crates/j-law-c-ffi/src/lib.rs +1553 -0
- data/vendor/rust/crates/j-law-core/Cargo.toml +18 -0
- data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/calculator.rs +216 -0
- data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/context.rs +29 -0
- data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/mod.rs +9 -0
- data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/params.rs +24 -0
- data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/policy.rs +34 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/assessment.rs +76 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/calculator.rs +222 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/context.rs +79 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/calculator.rs +167 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/context.rs +172 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/expense.rs +465 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/mod.rs +20 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/params.rs +205 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/personal.rs +324 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/types.rs +61 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/mod.rs +24 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/params.rs +109 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/policy.rs +103 -0
- data/vendor/rust/crates/j-law-core/src/domains/mod.rs +5 -0
- data/vendor/rust/crates/j-law-core/src/domains/real_estate/calculator.rs +197 -0
- data/vendor/rust/crates/j-law-core/src/domains/real_estate/context.rs +48 -0
- data/vendor/rust/crates/j-law-core/src/domains/real_estate/mod.rs +9 -0
- data/vendor/rust/crates/j-law-core/src/domains/real_estate/params.rs +43 -0
- data/vendor/rust/crates/j-law-core/src/domains/real_estate/policy.rs +40 -0
- data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/calculator.rs +321 -0
- data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/context.rs +408 -0
- data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/mod.rs +12 -0
- data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/params.rs +190 -0
- data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/policy.rs +105 -0
- data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/calculator.rs +247 -0
- data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/context.rs +167 -0
- data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/mod.rs +9 -0
- data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/params.rs +80 -0
- data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/policy.rs +49 -0
- data/vendor/rust/crates/j-law-core/src/error.rs +171 -0
- data/vendor/rust/crates/j-law-core/src/lib.rs +9 -0
- data/vendor/rust/crates/j-law-core/src/types/amount.rs +232 -0
- data/vendor/rust/crates/j-law-core/src/types/citation.rs +82 -0
- data/vendor/rust/crates/j-law-core/src/types/date.rs +280 -0
- data/vendor/rust/crates/j-law-core/src/types/mod.rs +11 -0
- data/vendor/rust/crates/j-law-core/src/types/rate.rs +219 -0
- data/vendor/rust/crates/j-law-core/src/types/rounding.rs +81 -0
- data/vendor/rust/crates/j-law-registry/Cargo.toml +15 -0
- data/vendor/rust/crates/j-law-registry/data/consumption_tax/consumption_tax.json +70 -0
- data/vendor/rust/crates/j-law-registry/data/income_tax/deductions.json +327 -0
- data/vendor/rust/crates/j-law-registry/data/income_tax/income_tax.json +352 -0
- data/vendor/rust/crates/j-law-registry/data/real_estate/brokerage_fee.json +125 -0
- data/vendor/rust/crates/j-law-registry/data/stamp_tax/stamp_tax.json +674 -0
- data/vendor/rust/crates/j-law-registry/data/withholding_tax/withholding_tax.json +70 -0
- data/vendor/rust/crates/j-law-registry/src/consumption_tax_loader.rs +325 -0
- data/vendor/rust/crates/j-law-registry/src/consumption_tax_schema.rs +49 -0
- data/vendor/rust/crates/j-law-registry/src/income_tax_deduction_loader.rs +636 -0
- data/vendor/rust/crates/j-law-registry/src/income_tax_deduction_schema.rs +111 -0
- data/vendor/rust/crates/j-law-registry/src/income_tax_loader.rs +445 -0
- data/vendor/rust/crates/j-law-registry/src/income_tax_schema.rs +44 -0
- data/vendor/rust/crates/j-law-registry/src/lib.rs +20 -0
- data/vendor/rust/crates/j-law-registry/src/loader.rs +221 -0
- data/vendor/rust/crates/j-law-registry/src/schema.rs +73 -0
- data/vendor/rust/crates/j-law-registry/src/stamp_tax_loader.rs +374 -0
- data/vendor/rust/crates/j-law-registry/src/stamp_tax_schema.rs +72 -0
- data/vendor/rust/crates/j-law-registry/src/validator.rs +204 -0
- data/vendor/rust/crates/j-law-registry/src/withholding_tax_loader.rs +310 -0
- data/vendor/rust/crates/j-law-registry/src/withholding_tax_schema.rs +61 -0
- metadata +148 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
use std::collections::BTreeMap;
|
|
2
|
+
|
|
3
|
+
use serde::Deserialize;
|
|
4
|
+
|
|
5
|
+
/// 印紙税法令根拠(JSON)。
|
|
6
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
7
|
+
pub struct StampTaxCitationEntry {
|
|
8
|
+
pub law_name: String,
|
|
9
|
+
pub article: String,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/// 印紙税額のブラケット(JSON)。
|
|
13
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
14
|
+
pub struct StampTaxBracketEntry {
|
|
15
|
+
pub label: String,
|
|
16
|
+
pub amount_from: u64,
|
|
17
|
+
pub amount_to_inclusive: Option<u64>,
|
|
18
|
+
pub tax_amount: u64,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// 印紙税の特例ルール(JSON)。
|
|
22
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
23
|
+
pub struct StampTaxSpecialRuleEntry {
|
|
24
|
+
pub code: String,
|
|
25
|
+
pub label: String,
|
|
26
|
+
pub priority: u16,
|
|
27
|
+
pub effective_from: Option<String>,
|
|
28
|
+
pub effective_until: Option<String>,
|
|
29
|
+
pub required_flags: Vec<String>,
|
|
30
|
+
pub tax_amount: Option<u64>,
|
|
31
|
+
pub rule_label: Option<String>,
|
|
32
|
+
pub brackets: Vec<StampTaxBracketEntry>,
|
|
33
|
+
pub no_amount_tax_amount: Option<u64>,
|
|
34
|
+
pub no_amount_rule_label: Option<String>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// 印紙税の文書コードごとのパラメータ(JSON)。
|
|
38
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
39
|
+
pub struct StampTaxDocumentParamsEntry {
|
|
40
|
+
pub label: String,
|
|
41
|
+
pub citation: StampTaxCitationEntry,
|
|
42
|
+
pub charge_mode: String,
|
|
43
|
+
pub amount_usage: String,
|
|
44
|
+
pub base_rule_label: String,
|
|
45
|
+
pub base_tax_amount: Option<u64>,
|
|
46
|
+
pub brackets: Vec<StampTaxBracketEntry>,
|
|
47
|
+
pub no_amount_tax_amount: Option<u64>,
|
|
48
|
+
pub no_amount_rule_label: Option<String>,
|
|
49
|
+
pub special_rules: Vec<StampTaxSpecialRuleEntry>,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// 印紙税の計算パラメータ(JSON)。
|
|
53
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
54
|
+
pub struct StampTaxParamsEntry {
|
|
55
|
+
pub documents: BTreeMap<String, StampTaxDocumentParamsEntry>,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// 印紙税の履歴エントリ(JSON)。
|
|
59
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
60
|
+
pub struct StampTaxHistoryEntry {
|
|
61
|
+
pub effective_from: String,
|
|
62
|
+
pub effective_until: Option<String>,
|
|
63
|
+
pub params: StampTaxParamsEntry,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// `stamp_tax.json` のルートスキーマ。
|
|
67
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
68
|
+
pub struct StampTaxRegistry {
|
|
69
|
+
#[allow(dead_code)]
|
|
70
|
+
pub domain: String,
|
|
71
|
+
pub history: Vec<StampTaxHistoryEntry>,
|
|
72
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
use crate::schema::BrokerageFeeRegistry;
|
|
2
|
+
use j_law_core::{LegalDate, RegistryError};
|
|
3
|
+
|
|
4
|
+
/// Registry データの整合性を検証する。
|
|
5
|
+
///
|
|
6
|
+
/// # 検証内容
|
|
7
|
+
/// - 適用期間の重複(Overlap)
|
|
8
|
+
/// - 適用期間の空白(Gap)
|
|
9
|
+
/// - 分母ゼロ
|
|
10
|
+
///
|
|
11
|
+
/// # エラー
|
|
12
|
+
/// 不正なデータを検出した場合は起動時にパニックさせてよい(設定ファイルエラー)。
|
|
13
|
+
pub fn validate(registry: &BrokerageFeeRegistry) -> Result<(), RegistryError> {
|
|
14
|
+
let domain = ®istry.domain;
|
|
15
|
+
|
|
16
|
+
for entry in ®istry.history {
|
|
17
|
+
if LegalDate::from_date_str(&entry.effective_from).is_none() {
|
|
18
|
+
return Err(RegistryError::InvalidDateFormat {
|
|
19
|
+
domain: domain.clone(),
|
|
20
|
+
value: entry.effective_from.clone(),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
if let Some(until) = &entry.effective_until {
|
|
24
|
+
if LegalDate::from_date_str(until).is_none() {
|
|
25
|
+
return Err(RegistryError::InvalidDateFormat {
|
|
26
|
+
domain: domain.clone(),
|
|
27
|
+
value: until.clone(),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for tier in &entry.params.tiers {
|
|
33
|
+
if tier.rate.denom == 0 {
|
|
34
|
+
return Err(RegistryError::ZeroDenominator {
|
|
35
|
+
path: format!("{}/{}/rate.denom", domain, tier.label),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let mut sorted = registry.history.clone();
|
|
42
|
+
sorted.sort_by(|a, b| a.effective_from.cmp(&b.effective_from));
|
|
43
|
+
|
|
44
|
+
for [current, next] in sorted.array_windows::<2>() {
|
|
45
|
+
let current_until = match ¤t.effective_until {
|
|
46
|
+
Some(d) => d.clone(),
|
|
47
|
+
None => continue,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if current_until >= next.effective_from {
|
|
51
|
+
return Err(RegistryError::PeriodOverlap {
|
|
52
|
+
domain: domain.clone(),
|
|
53
|
+
from: next.effective_from.clone(),
|
|
54
|
+
until: current_until.clone(),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let until_date = match LegalDate::from_date_str(¤t_until) {
|
|
59
|
+
Some(d) => d,
|
|
60
|
+
None => {
|
|
61
|
+
return Err(RegistryError::InvalidDateFormat {
|
|
62
|
+
domain: domain.clone(),
|
|
63
|
+
value: current_until.clone(),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
let expected_next_from = until_date.next_day().to_date_str();
|
|
68
|
+
if expected_next_from != next.effective_from {
|
|
69
|
+
return Err(RegistryError::PeriodGap {
|
|
70
|
+
domain: domain.clone(),
|
|
71
|
+
end: current_until,
|
|
72
|
+
next_start: next.effective_from.clone(),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
Ok(())
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[cfg(test)]
|
|
81
|
+
#[allow(clippy::disallowed_methods)] // テストコードでは unwrap 使用を許可
|
|
82
|
+
mod tests {
|
|
83
|
+
use super::*;
|
|
84
|
+
use crate::schema::{BrokerageFeeRegistry, CitationEntry, HistoryEntry, ParamsEntry};
|
|
85
|
+
|
|
86
|
+
fn make_registry(entries: Vec<HistoryEntry>) -> BrokerageFeeRegistry {
|
|
87
|
+
BrokerageFeeRegistry {
|
|
88
|
+
domain: "test".into(),
|
|
89
|
+
history: entries,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fn make_entry(from: &str, until: Option<&str>) -> HistoryEntry {
|
|
94
|
+
HistoryEntry {
|
|
95
|
+
effective_from: from.into(),
|
|
96
|
+
effective_until: until.map(|s: &str| s.into()),
|
|
97
|
+
status: "active".into(),
|
|
98
|
+
citation: CitationEntry {
|
|
99
|
+
law_id: "test".into(),
|
|
100
|
+
law_name: "test law".into(),
|
|
101
|
+
article: 1,
|
|
102
|
+
paragraph: None,
|
|
103
|
+
ministry: "test".into(),
|
|
104
|
+
},
|
|
105
|
+
params: ParamsEntry {
|
|
106
|
+
tiers: vec![],
|
|
107
|
+
low_cost_special: None,
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[test]
|
|
113
|
+
fn valid_registry_passes() {
|
|
114
|
+
let reg: BrokerageFeeRegistry = make_registry(vec![
|
|
115
|
+
make_entry("2019-10-01", Some("2024-06-30")),
|
|
116
|
+
make_entry("2024-07-01", None),
|
|
117
|
+
]);
|
|
118
|
+
assert!(validate(®).is_ok());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#[test]
|
|
122
|
+
fn overlap_detected() {
|
|
123
|
+
let reg: BrokerageFeeRegistry = make_registry(vec![
|
|
124
|
+
make_entry("2019-10-01", Some("2024-07-15")),
|
|
125
|
+
make_entry("2024-07-01", None),
|
|
126
|
+
]);
|
|
127
|
+
let err: RegistryError = validate(®).unwrap_err();
|
|
128
|
+
assert!(matches!(err, RegistryError::PeriodOverlap { .. }));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[test]
|
|
132
|
+
fn gap_detected_simple() {
|
|
133
|
+
let reg: BrokerageFeeRegistry = make_registry(vec![
|
|
134
|
+
make_entry("2019-10-01", Some("2024-06-30")),
|
|
135
|
+
make_entry("2024-07-02", None),
|
|
136
|
+
]);
|
|
137
|
+
let err: RegistryError = validate(®).unwrap_err();
|
|
138
|
+
assert!(matches!(err, RegistryError::PeriodGap { .. }));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[test]
|
|
142
|
+
fn gap_detected_month_boundary() {
|
|
143
|
+
let reg: BrokerageFeeRegistry = make_registry(vec![
|
|
144
|
+
make_entry("2019-10-01", Some("2024-06-30")),
|
|
145
|
+
make_entry("2024-08-01", None),
|
|
146
|
+
]);
|
|
147
|
+
let err: RegistryError = validate(®).unwrap_err();
|
|
148
|
+
assert!(matches!(err, RegistryError::PeriodGap { .. }));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[test]
|
|
152
|
+
fn gap_detected_year_boundary() {
|
|
153
|
+
let reg: BrokerageFeeRegistry = make_registry(vec![
|
|
154
|
+
make_entry("2019-10-01", Some("2023-12-31")),
|
|
155
|
+
make_entry("2024-01-02", None),
|
|
156
|
+
]);
|
|
157
|
+
let err: RegistryError = validate(®).unwrap_err();
|
|
158
|
+
assert!(matches!(err, RegistryError::PeriodGap { .. }));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[test]
|
|
162
|
+
fn valid_year_boundary_no_gap() {
|
|
163
|
+
let reg: BrokerageFeeRegistry = make_registry(vec![
|
|
164
|
+
make_entry("2019-10-01", Some("2023-12-31")),
|
|
165
|
+
make_entry("2024-01-01", None),
|
|
166
|
+
]);
|
|
167
|
+
assert!(validate(®).is_ok());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#[test]
|
|
171
|
+
fn malformed_date_rejected() {
|
|
172
|
+
let reg: BrokerageFeeRegistry = make_registry(vec![
|
|
173
|
+
make_entry("2019-10-01", Some("2024-00-30")),
|
|
174
|
+
make_entry("2024-07-01", None),
|
|
175
|
+
]);
|
|
176
|
+
let err: RegistryError = validate(®).unwrap_err();
|
|
177
|
+
assert!(matches!(err, RegistryError::InvalidDateFormat { .. }));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[test]
|
|
181
|
+
fn impossible_date_rejected() {
|
|
182
|
+
let reg: BrokerageFeeRegistry = make_registry(vec![
|
|
183
|
+
make_entry("2019-10-01", Some("2024-02-30")),
|
|
184
|
+
make_entry("2024-07-01", None),
|
|
185
|
+
]);
|
|
186
|
+
let err: RegistryError = validate(®).unwrap_err();
|
|
187
|
+
assert!(matches!(err, RegistryError::InvalidDateFormat { .. }));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#[test]
|
|
191
|
+
fn zero_denom_detected() {
|
|
192
|
+
use crate::schema::{Fraction, TierParam};
|
|
193
|
+
let mut entry: HistoryEntry = make_entry("2024-07-01", None);
|
|
194
|
+
entry.params.tiers.push(TierParam {
|
|
195
|
+
label: "tier1".into(),
|
|
196
|
+
price_from: 0,
|
|
197
|
+
price_to_inclusive: Some(2_000_000),
|
|
198
|
+
rate: Fraction { numer: 5, denom: 0 },
|
|
199
|
+
});
|
|
200
|
+
let reg: BrokerageFeeRegistry = make_registry(vec![entry]);
|
|
201
|
+
let err: RegistryError = validate(®).unwrap_err();
|
|
202
|
+
assert!(matches!(err, RegistryError::ZeroDenominator { .. }));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
use std::str::FromStr;
|
|
2
|
+
|
|
3
|
+
use crate::withholding_tax_schema::{WithholdingTaxHistoryEntry, WithholdingTaxRegistry};
|
|
4
|
+
use j_law_core::domains::withholding_tax::params::{
|
|
5
|
+
WithholdingTaxCategoryParams, WithholdingTaxMethod, WithholdingTaxParams,
|
|
6
|
+
};
|
|
7
|
+
use j_law_core::domains::withholding_tax::WithholdingTaxCategory;
|
|
8
|
+
use j_law_core::types::date::LegalDate;
|
|
9
|
+
use j_law_core::{InputError, JLawError, RegistryError};
|
|
10
|
+
|
|
11
|
+
/// `withholding_tax.json` をロードして `target_date` に対応するパラメータを返す。
|
|
12
|
+
///
|
|
13
|
+
/// # 法的根拠
|
|
14
|
+
/// 所得税法 第204条第1項
|
|
15
|
+
///
|
|
16
|
+
/// # エラー
|
|
17
|
+
/// - `target_date` がどの有効期間にも該当しない → `InputError::DateOutOfRange`
|
|
18
|
+
pub fn load_withholding_tax_params(
|
|
19
|
+
target_date: LegalDate,
|
|
20
|
+
) -> Result<WithholdingTaxParams, JLawError> {
|
|
21
|
+
target_date.validate()?;
|
|
22
|
+
|
|
23
|
+
let json_str = include_str!("../data/withholding_tax/withholding_tax.json");
|
|
24
|
+
|
|
25
|
+
let registry: WithholdingTaxRegistry =
|
|
26
|
+
serde_json::from_str(json_str).map_err(|e| RegistryError::ParseError {
|
|
27
|
+
path: "withholding_tax/withholding_tax.json".into(),
|
|
28
|
+
cause: e.to_string(),
|
|
29
|
+
})?;
|
|
30
|
+
validate_periods(®istry)?;
|
|
31
|
+
|
|
32
|
+
let date_str = target_date.to_date_str();
|
|
33
|
+
let entry = find_entry(®istry, &date_str).ok_or_else(|| InputError::DateOutOfRange {
|
|
34
|
+
date: date_str.clone(),
|
|
35
|
+
})?;
|
|
36
|
+
|
|
37
|
+
to_params(entry)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// `WithholdingTaxRegistry` の適用期間の整合性を検証する。
|
|
41
|
+
///
|
|
42
|
+
/// # 検証内容
|
|
43
|
+
/// - 適用期間の重複(Overlap)
|
|
44
|
+
/// - 適用期間の空白(Gap)
|
|
45
|
+
///
|
|
46
|
+
/// 分母ゼロは `to_params()` で個別にチェックする。
|
|
47
|
+
fn validate_periods(registry: &WithholdingTaxRegistry) -> Result<(), RegistryError> {
|
|
48
|
+
let domain = ®istry.domain;
|
|
49
|
+
|
|
50
|
+
let mut sorted = registry.history.clone();
|
|
51
|
+
sorted.sort_by(|a, b| a.effective_from.cmp(&b.effective_from));
|
|
52
|
+
|
|
53
|
+
for [current, next] in sorted.array_windows::<2>() {
|
|
54
|
+
let current_until = match ¤t.effective_until {
|
|
55
|
+
Some(d) => d.clone(),
|
|
56
|
+
None => {
|
|
57
|
+
return Err(RegistryError::PeriodOverlap {
|
|
58
|
+
domain: domain.clone(),
|
|
59
|
+
from: next.effective_from.clone(),
|
|
60
|
+
until: "open-ended".into(),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if current_until >= next.effective_from {
|
|
66
|
+
return Err(RegistryError::PeriodOverlap {
|
|
67
|
+
domain: domain.clone(),
|
|
68
|
+
from: next.effective_from.clone(),
|
|
69
|
+
until: current_until.clone(),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let until_date = LegalDate::from_date_str(¤t_until).ok_or_else(|| {
|
|
74
|
+
RegistryError::InvalidDateFormat {
|
|
75
|
+
domain: domain.clone(),
|
|
76
|
+
value: current_until.clone(),
|
|
77
|
+
}
|
|
78
|
+
})?;
|
|
79
|
+
let expected_next_from = until_date.next_day().to_date_str();
|
|
80
|
+
if expected_next_from != next.effective_from {
|
|
81
|
+
return Err(RegistryError::PeriodGap {
|
|
82
|
+
domain: domain.clone(),
|
|
83
|
+
end: current_until,
|
|
84
|
+
next_start: next.effective_from.clone(),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Ok(())
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fn find_entry<'a>(
|
|
93
|
+
registry: &'a WithholdingTaxRegistry,
|
|
94
|
+
date_str: &str,
|
|
95
|
+
) -> Option<&'a WithholdingTaxHistoryEntry> {
|
|
96
|
+
registry.history.iter().find(|entry| {
|
|
97
|
+
let from_ok = entry.effective_from.as_str() <= date_str;
|
|
98
|
+
let until_ok = match &entry.effective_until {
|
|
99
|
+
Some(until) => date_str <= until.as_str(),
|
|
100
|
+
None => true,
|
|
101
|
+
};
|
|
102
|
+
from_ok && until_ok
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn to_params(entry: &WithholdingTaxHistoryEntry) -> Result<WithholdingTaxParams, JLawError> {
|
|
107
|
+
let categories = entry
|
|
108
|
+
.params
|
|
109
|
+
.categories
|
|
110
|
+
.iter()
|
|
111
|
+
.map(|category| {
|
|
112
|
+
if category.method.base_rate.denom == 0 {
|
|
113
|
+
return Err(RegistryError::ZeroDenominator {
|
|
114
|
+
path: format!(
|
|
115
|
+
"withholding_tax/withholding_tax.json/{}/method/base_rate/denom",
|
|
116
|
+
category.code
|
|
117
|
+
),
|
|
118
|
+
}
|
|
119
|
+
.into());
|
|
120
|
+
}
|
|
121
|
+
if category.method.excess_rate.denom == 0 {
|
|
122
|
+
return Err(RegistryError::ZeroDenominator {
|
|
123
|
+
path: format!(
|
|
124
|
+
"withholding_tax/withholding_tax.json/{}/method/excess_rate/denom",
|
|
125
|
+
category.code
|
|
126
|
+
),
|
|
127
|
+
}
|
|
128
|
+
.into());
|
|
129
|
+
}
|
|
130
|
+
if category.method.kind != "two_tier" {
|
|
131
|
+
return Err(RegistryError::ParseError {
|
|
132
|
+
path: format!("withholding_tax/withholding_tax.json/{}", category.code),
|
|
133
|
+
cause: format!("未知の計算方式です: {}", category.method.kind),
|
|
134
|
+
}
|
|
135
|
+
.into());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
Ok(WithholdingTaxCategoryParams {
|
|
139
|
+
category: parse_category_code(&category.code)?,
|
|
140
|
+
label: category.label.clone(),
|
|
141
|
+
method: WithholdingTaxMethod::TwoTier {
|
|
142
|
+
threshold: category.method.threshold,
|
|
143
|
+
base_rate_numer: category.method.base_rate.numer,
|
|
144
|
+
base_rate_denom: category.method.base_rate.denom,
|
|
145
|
+
excess_rate_numer: category.method.excess_rate.numer,
|
|
146
|
+
excess_rate_denom: category.method.excess_rate.denom,
|
|
147
|
+
},
|
|
148
|
+
submission_prize_exemption_threshold: category.submission_prize_exemption_threshold,
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
.collect::<Result<Vec<_>, JLawError>>()?;
|
|
152
|
+
|
|
153
|
+
Ok(WithholdingTaxParams { categories })
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn parse_category_code(code: &str) -> Result<WithholdingTaxCategory, JLawError> {
|
|
157
|
+
WithholdingTaxCategory::from_str(code).map_err(|err| {
|
|
158
|
+
RegistryError::ParseError {
|
|
159
|
+
path: format!("withholding_tax/withholding_tax.json/{code}"),
|
|
160
|
+
cause: err.to_string(),
|
|
161
|
+
}
|
|
162
|
+
.into()
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#[cfg(test)]
|
|
167
|
+
#[allow(clippy::disallowed_methods)]
|
|
168
|
+
mod tests {
|
|
169
|
+
use super::*;
|
|
170
|
+
use crate::withholding_tax_schema::{
|
|
171
|
+
WithholdingTaxCategoryEntry, WithholdingTaxCitationEntry, WithholdingTaxFraction,
|
|
172
|
+
WithholdingTaxHistoryEntry, WithholdingTaxParamsEntry, WithholdingTaxRegistry,
|
|
173
|
+
WithholdingTaxTwoTierMethodEntry,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
fn make_registry(entries: Vec<WithholdingTaxHistoryEntry>) -> WithholdingTaxRegistry {
|
|
177
|
+
WithholdingTaxRegistry {
|
|
178
|
+
domain: "withholding_tax".into(),
|
|
179
|
+
history: entries,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fn make_entry(from: &str, until: Option<&str>) -> WithholdingTaxHistoryEntry {
|
|
184
|
+
WithholdingTaxHistoryEntry {
|
|
185
|
+
effective_from: from.into(),
|
|
186
|
+
effective_until: until.map(|s| s.into()),
|
|
187
|
+
status: "active".into(),
|
|
188
|
+
citation: WithholdingTaxCitationEntry {
|
|
189
|
+
law_name: "所得税法".into(),
|
|
190
|
+
article: 204,
|
|
191
|
+
paragraph: Some(1),
|
|
192
|
+
},
|
|
193
|
+
params: WithholdingTaxParamsEntry { categories: vec![] },
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#[test]
|
|
198
|
+
fn registry_validation_passes_for_current_data() {
|
|
199
|
+
let json_str = include_str!("../data/withholding_tax/withholding_tax.json");
|
|
200
|
+
let registry: WithholdingTaxRegistry = serde_json::from_str(json_str).unwrap();
|
|
201
|
+
assert!(validate_periods(®istry).is_ok());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#[test]
|
|
205
|
+
fn registry_validation_detects_overlap() {
|
|
206
|
+
let reg = make_registry(vec![
|
|
207
|
+
make_entry("2013-01-01", Some("2030-01-15")),
|
|
208
|
+
make_entry("2030-01-01", None),
|
|
209
|
+
]);
|
|
210
|
+
let err = validate_periods(®).unwrap_err();
|
|
211
|
+
assert!(matches!(err, RegistryError::PeriodOverlap { .. }));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
#[test]
|
|
215
|
+
fn registry_validation_detects_gap() {
|
|
216
|
+
let reg = make_registry(vec![
|
|
217
|
+
make_entry("2013-01-01", Some("2029-12-31")),
|
|
218
|
+
make_entry("2030-01-03", None),
|
|
219
|
+
]);
|
|
220
|
+
let err = validate_periods(®).unwrap_err();
|
|
221
|
+
assert!(matches!(err, RegistryError::PeriodGap { .. }));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#[test]
|
|
225
|
+
fn registry_validation_detects_open_ended_before_next() {
|
|
226
|
+
let reg = make_registry(vec![
|
|
227
|
+
make_entry("2013-01-01", None),
|
|
228
|
+
make_entry("2030-01-01", None),
|
|
229
|
+
]);
|
|
230
|
+
let err = validate_periods(®).unwrap_err();
|
|
231
|
+
assert!(matches!(err, RegistryError::PeriodOverlap { .. }));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#[test]
|
|
235
|
+
fn load_2026_params() {
|
|
236
|
+
let params = load_withholding_tax_params(LegalDate::new(2026, 1, 1)).unwrap();
|
|
237
|
+
assert_eq!(params.categories.len(), 3);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[test]
|
|
241
|
+
fn manuscript_category_has_exemption_threshold() {
|
|
242
|
+
let params = load_withholding_tax_params(LegalDate::new(2026, 1, 1)).unwrap();
|
|
243
|
+
let manuscript = params
|
|
244
|
+
.categories
|
|
245
|
+
.iter()
|
|
246
|
+
.find(|category| category.category == WithholdingTaxCategory::ManuscriptAndLecture)
|
|
247
|
+
.unwrap();
|
|
248
|
+
assert_eq!(
|
|
249
|
+
manuscript.submission_prize_exemption_threshold,
|
|
250
|
+
Some(50_000)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#[test]
|
|
255
|
+
fn date_before_period_is_out_of_range() {
|
|
256
|
+
let result = load_withholding_tax_params(LegalDate::new(2012, 12, 31));
|
|
257
|
+
assert!(matches!(
|
|
258
|
+
result,
|
|
259
|
+
Err(JLawError::Input(InputError::DateOutOfRange { .. }))
|
|
260
|
+
));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#[test]
|
|
264
|
+
fn date_after_period_is_out_of_range() {
|
|
265
|
+
let result = load_withholding_tax_params(LegalDate::new(2038, 1, 1));
|
|
266
|
+
assert!(matches!(
|
|
267
|
+
result,
|
|
268
|
+
Err(JLawError::Input(InputError::DateOutOfRange { .. }))
|
|
269
|
+
));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#[test]
|
|
273
|
+
fn invalid_category_code_is_registry_parse_error() {
|
|
274
|
+
let entry = WithholdingTaxHistoryEntry {
|
|
275
|
+
effective_from: "2013-01-01".into(),
|
|
276
|
+
effective_until: Some("2037-12-31".into()),
|
|
277
|
+
status: "active".into(),
|
|
278
|
+
citation: WithholdingTaxCitationEntry {
|
|
279
|
+
law_name: "所得税法".into(),
|
|
280
|
+
article: 204,
|
|
281
|
+
paragraph: Some(1),
|
|
282
|
+
},
|
|
283
|
+
params: WithholdingTaxParamsEntry {
|
|
284
|
+
categories: vec![WithholdingTaxCategoryEntry {
|
|
285
|
+
code: "unknown_category".into(),
|
|
286
|
+
label: "未知カテゴリ".into(),
|
|
287
|
+
method: WithholdingTaxTwoTierMethodEntry {
|
|
288
|
+
kind: "two_tier".into(),
|
|
289
|
+
threshold: 1_000_000,
|
|
290
|
+
base_rate: WithholdingTaxFraction {
|
|
291
|
+
numer: 1021,
|
|
292
|
+
denom: 10_000,
|
|
293
|
+
},
|
|
294
|
+
excess_rate: WithholdingTaxFraction {
|
|
295
|
+
numer: 2042,
|
|
296
|
+
denom: 10_000,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
submission_prize_exemption_threshold: None,
|
|
300
|
+
}],
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
let result = to_params(&entry);
|
|
305
|
+
assert!(matches!(
|
|
306
|
+
result,
|
|
307
|
+
Err(JLawError::Registry(RegistryError::ParseError { .. }))
|
|
308
|
+
));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
use serde::Deserialize;
|
|
2
|
+
|
|
3
|
+
/// JSON の分数表現 `{ "numer": N, "denom": N }`。
|
|
4
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
5
|
+
pub struct WithholdingTaxFraction {
|
|
6
|
+
pub numer: u64,
|
|
7
|
+
pub denom: u64,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/// 二段階税率方式の定義。
|
|
11
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
12
|
+
pub struct WithholdingTaxTwoTierMethodEntry {
|
|
13
|
+
pub kind: String,
|
|
14
|
+
pub threshold: u64,
|
|
15
|
+
pub base_rate: WithholdingTaxFraction,
|
|
16
|
+
pub excess_rate: WithholdingTaxFraction,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// カテゴリ単位のパラメータ。
|
|
20
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
21
|
+
pub struct WithholdingTaxCategoryEntry {
|
|
22
|
+
pub code: String,
|
|
23
|
+
pub label: String,
|
|
24
|
+
pub method: WithholdingTaxTwoTierMethodEntry,
|
|
25
|
+
pub submission_prize_exemption_threshold: Option<u64>,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// 1世代の計算パラメータ群。
|
|
29
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
30
|
+
pub struct WithholdingTaxParamsEntry {
|
|
31
|
+
pub categories: Vec<WithholdingTaxCategoryEntry>,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// 法令引用情報。
|
|
35
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
36
|
+
#[allow(dead_code)]
|
|
37
|
+
pub struct WithholdingTaxCitationEntry {
|
|
38
|
+
pub law_name: String,
|
|
39
|
+
pub article: u16,
|
|
40
|
+
pub paragraph: Option<u16>,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// 1世代の履歴エントリ。
|
|
44
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
45
|
+
pub struct WithholdingTaxHistoryEntry {
|
|
46
|
+
pub effective_from: String,
|
|
47
|
+
pub effective_until: Option<String>,
|
|
48
|
+
#[allow(dead_code)]
|
|
49
|
+
pub status: String,
|
|
50
|
+
#[allow(dead_code)]
|
|
51
|
+
pub citation: WithholdingTaxCitationEntry,
|
|
52
|
+
pub params: WithholdingTaxParamsEntry,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// `withholding_tax.json` のルートスキーマ。
|
|
56
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
57
|
+
pub struct WithholdingTaxRegistry {
|
|
58
|
+
#[allow(dead_code)]
|
|
59
|
+
pub domain: String,
|
|
60
|
+
pub history: Vec<WithholdingTaxHistoryEntry>,
|
|
61
|
+
}
|