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,221 @@
1
+ use crate::consumption_tax_loader::load_consumption_tax_params;
2
+ use crate::schema::{BrokerageFeeRegistry, HistoryEntry};
3
+ use crate::validator::validate;
4
+ use j_law_core::domains::real_estate::params::{
5
+ BrokerageFeeParams, LowCostSpecialParams, TierParam,
6
+ };
7
+ use j_law_core::types::date::LegalDate;
8
+ use j_law_core::{InputError, JLawError, RegistryError};
9
+
10
+ /// `brokerage_fee.json` をロードして `target_date` に対応するパラメータを返す。
11
+ ///
12
+ /// # 法的根拠
13
+ /// 宅地建物取引業法 第46条第1項
14
+ ///
15
+ /// # エラー
16
+ /// - `target_date` がどの有効期間にも該当しない → `InputError::DateOutOfRange`
17
+ /// - Registry データの不整合 → `RegistryError` でパニック(起動時バリデーション)
18
+ pub fn load_brokerage_fee_params(target_date: LegalDate) -> Result<BrokerageFeeParams, JLawError> {
19
+ target_date.validate()?;
20
+
21
+ let json_str = include_str!("../data/real_estate/brokerage_fee.json");
22
+
23
+ let registry: BrokerageFeeRegistry =
24
+ serde_json::from_str(json_str).map_err(|e| RegistryError::ParseError {
25
+ path: "real_estate/brokerage_fee.json".into(),
26
+ cause: e.to_string(),
27
+ })?;
28
+ validate(&registry)?;
29
+
30
+ let date_str = target_date.to_date_str();
31
+
32
+ let entry = find_entry(&registry, &date_str).ok_or_else(|| InputError::DateOutOfRange {
33
+ date: date_str.clone(),
34
+ })?;
35
+
36
+ to_params(entry, target_date)
37
+ }
38
+
39
+ /// `date_str` ("YYYY-MM-DD") に対応する履歴エントリを返す。
40
+ fn find_entry<'a>(registry: &'a BrokerageFeeRegistry, date_str: &str) -> Option<&'a HistoryEntry> {
41
+ registry.history.iter().find(|entry| {
42
+ let from_ok = entry.effective_from.as_str() <= date_str;
43
+ let until_ok = match &entry.effective_until {
44
+ Some(until) => date_str <= until.as_str(),
45
+ None => true,
46
+ };
47
+ from_ok && until_ok
48
+ })
49
+ }
50
+
51
+ fn to_params(
52
+ entry: &HistoryEntry,
53
+ target_date: LegalDate,
54
+ ) -> Result<BrokerageFeeParams, JLawError> {
55
+ let tiers = entry
56
+ .params
57
+ .tiers
58
+ .iter()
59
+ .map(|t| TierParam {
60
+ label: t.label.clone(),
61
+ price_from: t.price_from,
62
+ price_to_inclusive: t.price_to_inclusive,
63
+ rate_numer: t.rate.numer,
64
+ rate_denom: t.rate.denom,
65
+ })
66
+ .collect();
67
+
68
+ let low_cost_special = entry
69
+ .params
70
+ .low_cost_special
71
+ .as_ref()
72
+ .map(|s| LowCostSpecialParams {
73
+ price_ceiling_inclusive: s.price_ceiling_inclusive,
74
+ fee_ceiling_exclusive_tax: s.fee_ceiling_exclusive_tax,
75
+ seller_only: s.seller_only,
76
+ });
77
+
78
+ // 消費税ドメインに委譲(不動産仲介報酬には軽減税率は適用されない)
79
+ let consumption_tax = load_consumption_tax_params(target_date)?;
80
+
81
+ Ok(BrokerageFeeParams {
82
+ tiers,
83
+ consumption_tax,
84
+ low_cost_special,
85
+ })
86
+ }
87
+
88
+ #[cfg(test)]
89
+ #[allow(clippy::disallowed_methods)] // テストコードでは unwrap 使用を許可
90
+ mod tests {
91
+ use super::*;
92
+
93
+ #[test]
94
+ fn load_2024_active_params() {
95
+ let params = load_brokerage_fee_params(LegalDate::new(2024, 8, 1)).unwrap();
96
+ assert_eq!(params.tiers.len(), 3);
97
+ assert_eq!(params.consumption_tax.standard_rate.numer, 10);
98
+ assert_eq!(params.consumption_tax.standard_rate.denom, 100);
99
+ assert!(params.low_cost_special.is_some());
100
+ let special = params.low_cost_special.unwrap();
101
+ assert_eq!(special.price_ceiling_inclusive, 8_000_000);
102
+ assert_eq!(special.fee_ceiling_exclusive_tax, 330_000);
103
+ }
104
+
105
+ #[test]
106
+ fn load_2019_superseded_params() {
107
+ let params = load_brokerage_fee_params(LegalDate::new(2019, 12, 1)).unwrap();
108
+ assert_eq!(params.tiers.len(), 3);
109
+ // 2019年告示は低廉特例あり(売主限定・400万円以下)
110
+ let special = params.low_cost_special.as_ref().unwrap();
111
+ assert_eq!(special.price_ceiling_inclusive, 4_000_000);
112
+ assert_eq!(special.fee_ceiling_exclusive_tax, 180_000);
113
+ assert!(special.seller_only);
114
+ // 2019年10月以降は消費税10%(消費税ドメインから取得)
115
+ assert_eq!(params.consumption_tax.standard_rate.numer, 10);
116
+ }
117
+
118
+ #[test]
119
+ fn load_2018_params() {
120
+ let params = load_brokerage_fee_params(LegalDate::new(2018, 6, 1)).unwrap();
121
+ assert_eq!(params.tiers.len(), 3);
122
+ let special = params.low_cost_special.as_ref().unwrap();
123
+ assert_eq!(special.price_ceiling_inclusive, 4_000_000);
124
+ assert_eq!(special.fee_ceiling_exclusive_tax, 180_000);
125
+ assert!(special.seller_only);
126
+ // 2018年は消費税8%(消費税ドメインから取得)
127
+ assert_eq!(params.consumption_tax.standard_rate.numer, 8);
128
+ }
129
+
130
+ #[test]
131
+ fn boundary_2024_07_01_uses_new_params() {
132
+ let params = load_brokerage_fee_params(LegalDate::new(2024, 7, 1)).unwrap();
133
+ let special = params.low_cost_special.as_ref().unwrap();
134
+ assert_eq!(special.price_ceiling_inclusive, 8_000_000);
135
+ assert_eq!(special.fee_ceiling_exclusive_tax, 330_000);
136
+ assert!(!special.seller_only);
137
+ }
138
+
139
+ #[test]
140
+ fn boundary_2024_06_30_uses_old_params() {
141
+ // 2024-06-30 は旧告示(売主限定・400万円以下の低廉特例)
142
+ let params = load_brokerage_fee_params(LegalDate::new(2024, 6, 30)).unwrap();
143
+ let special = params.low_cost_special.as_ref().unwrap();
144
+ assert_eq!(special.price_ceiling_inclusive, 4_000_000);
145
+ assert_eq!(special.fee_ceiling_exclusive_tax, 180_000);
146
+ assert!(special.seller_only);
147
+ }
148
+
149
+ #[test]
150
+ fn boundary_2019_10_01_uses_10pct_tax() {
151
+ // 2019-10-01 から消費税10%(消費税ドメインから取得)
152
+ let params = load_brokerage_fee_params(LegalDate::new(2019, 10, 1)).unwrap();
153
+ assert_eq!(params.consumption_tax.standard_rate.numer, 10);
154
+ // 2018-2024-06-30 エントリ内なので低廉特例は売主限定・400万円以下
155
+ let special = params.low_cost_special.as_ref().unwrap();
156
+ assert_eq!(special.price_ceiling_inclusive, 4_000_000);
157
+ assert!(special.seller_only);
158
+ }
159
+
160
+ #[test]
161
+ fn boundary_2019_09_30_uses_8pct_tax() {
162
+ // 2019-09-30 まで消費税8%(消費税ドメインから取得)
163
+ let params = load_brokerage_fee_params(LegalDate::new(2019, 9, 30)).unwrap();
164
+ assert_eq!(params.consumption_tax.standard_rate.numer, 8);
165
+ }
166
+
167
+ // ─── 2018年以前(全期間対応)─────────────────────────────────────────────
168
+
169
+ #[test]
170
+ fn load_2017_pre_special_params() {
171
+ // 2017年12月31日(特例導入前): 低廉特例なし・基本ティア計算のみ
172
+ let params = load_brokerage_fee_params(LegalDate::new(2017, 12, 31)).unwrap();
173
+ assert_eq!(params.tiers.len(), 3);
174
+ assert!(params.low_cost_special.is_none());
175
+ // 消費税は消費税ドメインから取得(2017年は8%)
176
+ assert_eq!(params.consumption_tax.standard_rate.numer, 8);
177
+ }
178
+
179
+ #[test]
180
+ fn boundary_2018_01_01_activates_special() {
181
+ // 2018-01-01 から低廉特例が有効になる
182
+ let params = load_brokerage_fee_params(LegalDate::new(2018, 1, 1)).unwrap();
183
+ assert!(params.low_cost_special.is_some());
184
+ }
185
+
186
+ #[test]
187
+ fn boundary_2017_12_31_no_special() {
188
+ // 2017-12-31 では低廉特例なし
189
+ let params = load_brokerage_fee_params(LegalDate::new(2017, 12, 31)).unwrap();
190
+ assert!(params.low_cost_special.is_none());
191
+ }
192
+
193
+ #[test]
194
+ fn load_1990_params() {
195
+ // 1990年(消費税3%時代)でも計算可能
196
+ let params = load_brokerage_fee_params(LegalDate::new(1990, 1, 1)).unwrap();
197
+ assert_eq!(params.tiers.len(), 3);
198
+ assert!(params.low_cost_special.is_none());
199
+ // 1990年は消費税3%(消費税ドメインから取得)
200
+ assert_eq!(params.consumption_tax.standard_rate.numer, 3);
201
+ }
202
+
203
+ #[test]
204
+ fn registry_validation_passes() {
205
+ let json_str = include_str!("../data/real_estate/brokerage_fee.json");
206
+ let registry: BrokerageFeeRegistry = serde_json::from_str(json_str).unwrap();
207
+ validate(&registry).unwrap();
208
+ }
209
+
210
+ #[test]
211
+ fn invalid_month_returns_error() {
212
+ let result = load_brokerage_fee_params(LegalDate::new(2024, 13, 1));
213
+ assert!(matches!(result, Err(JLawError::Input(_))));
214
+ }
215
+
216
+ #[test]
217
+ fn invalid_day_returns_error() {
218
+ let result = load_brokerage_fee_params(LegalDate::new(2024, 2, 30));
219
+ assert!(matches!(result, Err(JLawError::Input(_))));
220
+ }
221
+ }
@@ -0,0 +1,73 @@
1
+ use serde::Deserialize;
2
+
3
+ /// JSON の分数表現 `{ "numer": N, "denom": N }`。
4
+ #[derive(Debug, Clone, Deserialize)]
5
+ pub struct Fraction {
6
+ pub numer: u64,
7
+ pub denom: u64,
8
+ }
9
+
10
+ /// 1ティアの定義。
11
+ #[derive(Debug, Clone, Deserialize)]
12
+ pub struct TierParam {
13
+ pub label: String,
14
+ pub price_from: u64,
15
+ /// `null` は「上限なし」を意味する。
16
+ pub price_to_inclusive: Option<u64>,
17
+ pub rate: Fraction,
18
+ }
19
+
20
+ /// 低廉な空き家特例パラメータ(2018年1月施行・2024年7月改正)。
21
+ #[derive(Debug, Clone, Deserialize)]
22
+ pub struct LowCostSpecialParam {
23
+ /// 特例が適用される売買価格の上限(以下)。
24
+ pub price_ceiling_inclusive: u64,
25
+ /// 税抜き報酬額の上限(最低保証額として機能)。
26
+ pub fee_ceiling_exclusive_tax: u64,
27
+ /// `true` の場合、売主側の取引にのみ特例が適用される。
28
+ ///
29
+ /// 2018年1月1日〜2024年6月30日の特例は売主のみ対象(平成29年国土交通省告示第98号)。
30
+ /// `false` の場合、売主・買主双方に適用される(2024年7月1日施行以降)。
31
+ pub seller_only: bool,
32
+ }
33
+
34
+ /// 1世代の計算パラメータ群。
35
+ #[derive(Debug, Clone, Deserialize)]
36
+ pub struct ParamsEntry {
37
+ pub tiers: Vec<TierParam>,
38
+ pub low_cost_special: Option<LowCostSpecialParam>,
39
+ }
40
+
41
+ /// 法令引用情報。
42
+ #[derive(Debug, Clone, Deserialize)]
43
+ #[allow(dead_code)]
44
+ pub struct CitationEntry {
45
+ pub law_id: String,
46
+ pub law_name: String,
47
+ pub article: u16,
48
+ pub paragraph: Option<u16>,
49
+ pub ministry: String,
50
+ }
51
+
52
+ /// 1世代の履歴エントリ。
53
+ #[derive(Debug, Clone, Deserialize)]
54
+ pub struct HistoryEntry {
55
+ /// 施行日 `"YYYY-MM-DD"`。
56
+ pub effective_from: String,
57
+ /// 廃止日 `"YYYY-MM-DD"`。現行版は `null`。
58
+ pub effective_until: Option<String>,
59
+ /// `"active"` または `"superseded"`。
60
+ #[allow(dead_code)]
61
+ pub status: String,
62
+ #[allow(dead_code)]
63
+ pub citation: CitationEntry,
64
+ pub params: ParamsEntry,
65
+ }
66
+
67
+ /// `brokerage_fee.json` のルートスキーマ。
68
+ #[derive(Debug, Clone, Deserialize)]
69
+ pub struct BrokerageFeeRegistry {
70
+ #[allow(dead_code)]
71
+ pub domain: String,
72
+ pub history: Vec<HistoryEntry>,
73
+ }
@@ -0,0 +1,374 @@
1
+ use std::collections::BTreeMap;
2
+ use std::str::FromStr;
3
+
4
+ use crate::stamp_tax_schema::{
5
+ StampTaxBracketEntry, StampTaxDocumentParamsEntry, StampTaxHistoryEntry, StampTaxRegistry,
6
+ StampTaxSpecialRuleEntry,
7
+ };
8
+ use j_law_core::domains::stamp_tax::context::{StampTaxDocumentCode, StampTaxFlag};
9
+ use j_law_core::domains::stamp_tax::params::{
10
+ StampTaxAmountUsage, StampTaxBracket, StampTaxChargeMode, StampTaxCitation,
11
+ StampTaxDocumentParams, StampTaxParams, StampTaxSpecialRule,
12
+ };
13
+ use j_law_core::types::date::LegalDate;
14
+ use j_law_core::{InputError, JLawError, RegistryError};
15
+
16
+ const PATH: &str = "stamp_tax/stamp_tax.json";
17
+
18
+ /// `stamp_tax.json` をロードして `target_date` に対応するパラメータを返す。
19
+ pub fn load_stamp_tax_params(target_date: LegalDate) -> Result<StampTaxParams, JLawError> {
20
+ target_date.validate()?;
21
+
22
+ let json_str = include_str!("../data/stamp_tax/stamp_tax.json");
23
+
24
+ let registry: StampTaxRegistry =
25
+ serde_json::from_str(json_str).map_err(|e| RegistryError::ParseError {
26
+ path: PATH.into(),
27
+ cause: e.to_string(),
28
+ })?;
29
+ validate_registry(&registry)?;
30
+
31
+ let date_str = target_date.to_date_str();
32
+
33
+ let entry = find_entry(&registry, &date_str).ok_or_else(|| InputError::DateOutOfRange {
34
+ date: date_str.clone(),
35
+ })?;
36
+
37
+ to_params(entry)
38
+ }
39
+
40
+ /// `StampTaxRegistry` の整合性を検証する。
41
+ ///
42
+ /// # 検証内容
43
+ /// - 適用期間の重複(Overlap)
44
+ /// - 適用期間の空白(Gap)
45
+ fn validate_registry(registry: &StampTaxRegistry) -> Result<(), RegistryError> {
46
+ let domain = &registry.domain;
47
+
48
+ // 期間の重複・ギャップチェック
49
+ let mut sorted = registry.history.clone();
50
+ sorted.sort_by(|a, b| a.effective_from.cmp(&b.effective_from));
51
+
52
+ for [current, next] in sorted.array_windows::<2>() {
53
+ let current_until = match &current.effective_until {
54
+ Some(d) => d.clone(),
55
+ None => {
56
+ return Err(RegistryError::PeriodOverlap {
57
+ domain: domain.clone(),
58
+ from: next.effective_from.clone(),
59
+ until: "open-ended".into(),
60
+ });
61
+ }
62
+ };
63
+
64
+ if current_until >= next.effective_from {
65
+ return Err(RegistryError::PeriodOverlap {
66
+ domain: domain.clone(),
67
+ from: next.effective_from.clone(),
68
+ until: current_until.clone(),
69
+ });
70
+ }
71
+
72
+ let until_date = LegalDate::from_date_str(&current_until).ok_or_else(|| {
73
+ RegistryError::InvalidDateFormat {
74
+ domain: domain.clone(),
75
+ value: current_until.clone(),
76
+ }
77
+ })?;
78
+ let expected_next_from = until_date.next_day().to_date_str();
79
+ if expected_next_from != next.effective_from {
80
+ return Err(RegistryError::PeriodGap {
81
+ domain: domain.clone(),
82
+ end: current_until,
83
+ next_start: next.effective_from.clone(),
84
+ });
85
+ }
86
+ }
87
+
88
+ Ok(())
89
+ }
90
+
91
+ fn find_entry<'a>(
92
+ registry: &'a StampTaxRegistry,
93
+ date_str: &str,
94
+ ) -> Option<&'a StampTaxHistoryEntry> {
95
+ registry.history.iter().find(|entry| {
96
+ let from_ok = entry.effective_from.as_str() <= date_str;
97
+ let until_ok = match &entry.effective_until {
98
+ Some(until) => date_str <= until.as_str(),
99
+ None => true,
100
+ };
101
+ from_ok && until_ok
102
+ })
103
+ }
104
+
105
+ fn to_params(entry: &StampTaxHistoryEntry) -> Result<StampTaxParams, JLawError> {
106
+ let mut documents = BTreeMap::new();
107
+
108
+ for (key, value) in &entry.params.documents {
109
+ let document_code = StampTaxDocumentCode::from_str(key).map_err(registry_parse_error)?;
110
+ let document_params = to_document_params(document_code, value)?;
111
+ documents.insert(document_code, document_params);
112
+ }
113
+
114
+ if documents.is_empty() {
115
+ return Err(registry_parse_error(InputError::InvalidStampTaxInput {
116
+ field: "documents".into(),
117
+ reason: "印紙税文書コードが1件も定義されていません".into(),
118
+ }));
119
+ }
120
+
121
+ Ok(StampTaxParams { documents })
122
+ }
123
+
124
+ fn to_document_params(
125
+ code: StampTaxDocumentCode,
126
+ entry: &StampTaxDocumentParamsEntry,
127
+ ) -> Result<StampTaxDocumentParams, JLawError> {
128
+ let charge_mode =
129
+ StampTaxChargeMode::from_str(&entry.charge_mode).map_err(registry_parse_error)?;
130
+ let amount_usage =
131
+ StampTaxAmountUsage::from_str(&entry.amount_usage).map_err(registry_parse_error)?;
132
+ let brackets = to_brackets(&entry.brackets)?;
133
+ validate_brackets(code, &brackets)?;
134
+
135
+ let special_rules = entry
136
+ .special_rules
137
+ .iter()
138
+ .map(|rule| to_special_rule(code, rule))
139
+ .collect::<Result<Vec<_>, _>>()?;
140
+
141
+ match charge_mode {
142
+ StampTaxChargeMode::AmountBrackets => {
143
+ if brackets.is_empty() {
144
+ return Err(registry_parse_error(InputError::InvalidStampTaxInput {
145
+ field: code.to_string(),
146
+ reason: "amount_brackets には brackets が必要です".into(),
147
+ }));
148
+ }
149
+ if entry.base_tax_amount.is_some() {
150
+ return Err(registry_parse_error(InputError::InvalidStampTaxInput {
151
+ field: code.to_string(),
152
+ reason: "amount_brackets に base_tax_amount は指定できません".into(),
153
+ }));
154
+ }
155
+ }
156
+ StampTaxChargeMode::FixedPerDocument | StampTaxChargeMode::FixedPerYear => {
157
+ if entry.base_tax_amount.is_none() {
158
+ return Err(registry_parse_error(InputError::InvalidStampTaxInput {
159
+ field: code.to_string(),
160
+ reason: "固定税額文書には base_tax_amount が必要です".into(),
161
+ }));
162
+ }
163
+ if !brackets.is_empty() {
164
+ return Err(registry_parse_error(InputError::InvalidStampTaxInput {
165
+ field: code.to_string(),
166
+ reason: "固定税額文書の base brackets は空である必要があります".into(),
167
+ }));
168
+ }
169
+ }
170
+ }
171
+
172
+ if matches!(amount_usage, StampTaxAmountUsage::Unsupported)
173
+ && entry.no_amount_tax_amount.is_some()
174
+ {
175
+ return Err(registry_parse_error(InputError::InvalidStampTaxInput {
176
+ field: code.to_string(),
177
+ reason: "amount_usage=unsupported では no_amount_tax_amount を指定できません".into(),
178
+ }));
179
+ }
180
+
181
+ Ok(StampTaxDocumentParams {
182
+ code,
183
+ label: entry.label.clone(),
184
+ citation: StampTaxCitation {
185
+ law_name: entry.citation.law_name.clone(),
186
+ article: entry.citation.article.clone(),
187
+ },
188
+ charge_mode,
189
+ amount_usage,
190
+ base_rule_label: entry.base_rule_label.clone(),
191
+ base_tax_amount: entry.base_tax_amount,
192
+ brackets,
193
+ no_amount_tax_amount: entry.no_amount_tax_amount,
194
+ no_amount_rule_label: entry.no_amount_rule_label.clone(),
195
+ special_rules,
196
+ })
197
+ }
198
+
199
+ fn to_special_rule(
200
+ document_code: StampTaxDocumentCode,
201
+ entry: &StampTaxSpecialRuleEntry,
202
+ ) -> Result<StampTaxSpecialRule, JLawError> {
203
+ let required_flags = entry
204
+ .required_flags
205
+ .iter()
206
+ .map(|flag| StampTaxFlag::from_str(flag).map_err(registry_parse_error))
207
+ .collect::<Result<Vec<_>, _>>()?;
208
+
209
+ for flag in &required_flags {
210
+ if !flag.allowed_document_codes().contains(&document_code) {
211
+ return Err(registry_parse_error(InputError::InvalidStampTaxInput {
212
+ field: document_code.to_string(),
213
+ reason: format!(
214
+ "特例ルール {} で許可されない flag {} を参照しています",
215
+ entry.code, flag
216
+ ),
217
+ }));
218
+ }
219
+ }
220
+
221
+ let brackets = to_brackets(&entry.brackets)?;
222
+ if !brackets.is_empty() {
223
+ validate_brackets(document_code, &brackets)?;
224
+ }
225
+
226
+ Ok(StampTaxSpecialRule {
227
+ code: entry.code.clone(),
228
+ label: entry.label.clone(),
229
+ priority: entry.priority,
230
+ effective_from: entry.effective_from.clone(),
231
+ effective_until: entry.effective_until.clone(),
232
+ required_flags,
233
+ tax_amount: entry.tax_amount,
234
+ rule_label: entry.rule_label.clone(),
235
+ brackets,
236
+ no_amount_tax_amount: entry.no_amount_tax_amount,
237
+ no_amount_rule_label: entry.no_amount_rule_label.clone(),
238
+ })
239
+ }
240
+
241
+ fn to_brackets(entries: &[StampTaxBracketEntry]) -> Result<Vec<StampTaxBracket>, JLawError> {
242
+ Ok(entries
243
+ .iter()
244
+ .map(|entry| StampTaxBracket {
245
+ label: entry.label.clone(),
246
+ amount_from: entry.amount_from,
247
+ amount_to_inclusive: entry.amount_to_inclusive,
248
+ tax_amount: entry.tax_amount,
249
+ })
250
+ .collect())
251
+ }
252
+
253
+ fn validate_brackets(
254
+ document_code: StampTaxDocumentCode,
255
+ brackets: &[StampTaxBracket],
256
+ ) -> Result<(), JLawError> {
257
+ let mut previous_to: Option<u64> = None;
258
+ for bracket in brackets {
259
+ if let Some(previous) = previous_to {
260
+ if bracket.amount_from <= previous {
261
+ return Err(registry_parse_error(InputError::InvalidStampTaxInput {
262
+ field: document_code.to_string(),
263
+ reason: format!(
264
+ "ブラケットが重複または未整列です: {} starts at {} after {}",
265
+ bracket.label, bracket.amount_from, previous
266
+ ),
267
+ }));
268
+ }
269
+ if bracket.amount_from != previous.saturating_add(1) {
270
+ return Err(registry_parse_error(InputError::InvalidStampTaxInput {
271
+ field: document_code.to_string(),
272
+ reason: format!(
273
+ "ブラケットに空白があります: previous_to={}, next_from={}",
274
+ previous, bracket.amount_from
275
+ ),
276
+ }));
277
+ }
278
+ }
279
+ previous_to = bracket.amount_to_inclusive;
280
+ }
281
+ Ok(())
282
+ }
283
+
284
+ fn registry_parse_error(err: impl Into<JLawError>) -> JLawError {
285
+ match err.into() {
286
+ JLawError::Input(inner) => RegistryError::ParseError {
287
+ path: PATH.into(),
288
+ cause: inner.to_string(),
289
+ }
290
+ .into(),
291
+ other => other,
292
+ }
293
+ }
294
+
295
+ #[cfg(test)]
296
+ #[allow(clippy::disallowed_methods)]
297
+ mod tests {
298
+ use super::*;
299
+ use crate::stamp_tax_schema::{StampTaxHistoryEntry, StampTaxParamsEntry, StampTaxRegistry};
300
+
301
+ fn make_registry(entries: Vec<StampTaxHistoryEntry>) -> StampTaxRegistry {
302
+ StampTaxRegistry {
303
+ domain: "stamp_tax".into(),
304
+ history: entries,
305
+ }
306
+ }
307
+
308
+ fn make_entry(from: &str, until: Option<&str>) -> StampTaxHistoryEntry {
309
+ StampTaxHistoryEntry {
310
+ effective_from: from.into(),
311
+ effective_until: until.map(|s| s.into()),
312
+ params: StampTaxParamsEntry {
313
+ documents: BTreeMap::new(),
314
+ },
315
+ }
316
+ }
317
+
318
+ #[test]
319
+ fn registry_validation_passes_for_current_data() {
320
+ let json_str = include_str!("../data/stamp_tax/stamp_tax.json");
321
+ let registry: StampTaxRegistry = serde_json::from_str(json_str).unwrap();
322
+ assert!(validate_registry(&registry).is_ok());
323
+ }
324
+
325
+ #[test]
326
+ fn registry_validation_detects_overlap() {
327
+ let reg = make_registry(vec![
328
+ make_entry("2014-04-01", Some("2020-01-15")),
329
+ make_entry("2020-01-01", None),
330
+ ]);
331
+ let err = validate_registry(&reg).unwrap_err();
332
+ assert!(matches!(err, RegistryError::PeriodOverlap { .. }));
333
+ }
334
+
335
+ #[test]
336
+ fn registry_validation_detects_gap() {
337
+ let reg = make_registry(vec![
338
+ make_entry("2014-04-01", Some("2019-12-31")),
339
+ make_entry("2020-01-03", None),
340
+ ]);
341
+ let err = validate_registry(&reg).unwrap_err();
342
+ assert!(matches!(err, RegistryError::PeriodGap { .. }));
343
+ }
344
+
345
+ #[test]
346
+ fn registry_validation_detects_open_ended_before_next() {
347
+ let reg = make_registry(vec![
348
+ make_entry("2014-04-01", None),
349
+ make_entry("2020-01-01", None),
350
+ ]);
351
+ let err = validate_registry(&reg).unwrap_err();
352
+ assert!(matches!(err, RegistryError::PeriodOverlap { .. }));
353
+ }
354
+
355
+ #[test]
356
+ fn load_2024_params() {
357
+ let params = load_stamp_tax_params(LegalDate::new(2024, 1, 1)).unwrap();
358
+ assert!(params
359
+ .documents
360
+ .contains_key(&StampTaxDocumentCode::Article1RealEstateTransfer));
361
+ assert!(params
362
+ .documents
363
+ .contains_key(&StampTaxDocumentCode::Article20SealBook));
364
+ }
365
+
366
+ #[test]
367
+ fn date_out_of_range_returns_error() {
368
+ let result = load_stamp_tax_params(LegalDate::new(2014, 3, 31));
369
+ assert!(matches!(
370
+ result,
371
+ Err(JLawError::Input(InputError::DateOutOfRange { .. }))
372
+ ));
373
+ }
374
+ }