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,324 @@
1
+ use crate::domains::income_tax::deduction::context::IncomeDeductionContext;
2
+ use crate::domains::income_tax::deduction::params::{
3
+ BasicDeductionBracket, BasicDeductionParams, DependentDeductionParams, PersonalDeductionParams,
4
+ SpouseDeductionParams, SpouseIncomeBracket,
5
+ };
6
+ use crate::domains::income_tax::deduction::types::{IncomeDeductionKind, IncomeDeductionLine};
7
+ use crate::error::{CalculationError, JLawError};
8
+ use crate::types::amount::FinalAmount;
9
+
10
+ pub(crate) fn calculate_personal_deductions(
11
+ ctx: &IncomeDeductionContext,
12
+ params: &PersonalDeductionParams,
13
+ ) -> Result<Vec<IncomeDeductionLine>, JLawError> {
14
+ Ok(vec![
15
+ calculate_basic_deduction(ctx, &params.basic)?,
16
+ calculate_spouse_deduction(ctx, &params.spouse)?,
17
+ calculate_dependent_deduction(ctx, &params.dependent)?,
18
+ ])
19
+ }
20
+
21
+ fn calculate_basic_deduction(
22
+ ctx: &IncomeDeductionContext,
23
+ params: &BasicDeductionParams,
24
+ ) -> Result<IncomeDeductionLine, JLawError> {
25
+ let bracket = params
26
+ .brackets
27
+ .iter()
28
+ .find(|bracket| matches_basic_bracket(ctx.total_income_amount, bracket))
29
+ .ok_or_else(|| CalculationError::PolicyNotApplicable {
30
+ reason: format!(
31
+ "総所得金額等 {}円 に対応する基礎控除ブラケットが見つかりません",
32
+ ctx.total_income_amount
33
+ ),
34
+ })?;
35
+
36
+ Ok(IncomeDeductionLine {
37
+ kind: IncomeDeductionKind::Basic,
38
+ label: "基礎控除".into(),
39
+ amount: FinalAmount::new(bracket.deduction_amount),
40
+ })
41
+ }
42
+
43
+ fn calculate_spouse_deduction(
44
+ ctx: &IncomeDeductionContext,
45
+ params: &SpouseDeductionParams,
46
+ ) -> Result<IncomeDeductionLine, JLawError> {
47
+ let Some(spouse) = ctx.deductions.personal.spouse else {
48
+ return Ok(zero_line(IncomeDeductionKind::Spouse, "配偶者控除"));
49
+ };
50
+
51
+ if !spouse.is_same_household
52
+ || spouse.spouse_total_income_amount > params.qualifying_spouse_income_max
53
+ {
54
+ return Ok(zero_line(IncomeDeductionKind::Spouse, "配偶者控除"));
55
+ }
56
+
57
+ let bracket = params
58
+ .taxpayer_income_brackets
59
+ .iter()
60
+ .find(|bracket| matches_spouse_bracket(ctx.total_income_amount, bracket))
61
+ .ok_or_else(|| CalculationError::PolicyNotApplicable {
62
+ reason: format!(
63
+ "総所得金額等 {}円 に対応する配偶者控除ブラケットが見つかりません",
64
+ ctx.total_income_amount
65
+ ),
66
+ })?;
67
+
68
+ let amount = if spouse.is_elderly {
69
+ bracket.elderly_deduction_amount
70
+ } else {
71
+ bracket.deduction_amount
72
+ };
73
+
74
+ Ok(IncomeDeductionLine {
75
+ kind: IncomeDeductionKind::Spouse,
76
+ label: "配偶者控除".into(),
77
+ amount: FinalAmount::new(amount),
78
+ })
79
+ }
80
+
81
+ fn calculate_dependent_deduction(
82
+ ctx: &IncomeDeductionContext,
83
+ params: &DependentDeductionParams,
84
+ ) -> Result<IncomeDeductionLine, JLawError> {
85
+ let dependent = ctx.deductions.personal.dependent;
86
+ let amount = multiply_count(
87
+ dependent.general_count,
88
+ params.general_deduction_amount,
89
+ "dependent_general",
90
+ )?
91
+ .checked_add(multiply_count(
92
+ dependent.specific_count,
93
+ params.specific_deduction_amount,
94
+ "dependent_specific",
95
+ )?)
96
+ .ok_or_else(|| CalculationError::Overflow {
97
+ step: "dependent_total".into(),
98
+ })?
99
+ .checked_add(multiply_count(
100
+ dependent.elderly_cohabiting_count,
101
+ params.elderly_cohabiting_deduction_amount,
102
+ "dependent_elderly_cohabiting",
103
+ )?)
104
+ .ok_or_else(|| CalculationError::Overflow {
105
+ step: "dependent_total".into(),
106
+ })?
107
+ .checked_add(multiply_count(
108
+ dependent.elderly_other_count,
109
+ params.elderly_other_deduction_amount,
110
+ "dependent_elderly_other",
111
+ )?)
112
+ .ok_or_else(|| CalculationError::Overflow {
113
+ step: "dependent_total".into(),
114
+ })?;
115
+
116
+ Ok(IncomeDeductionLine {
117
+ kind: IncomeDeductionKind::Dependent,
118
+ label: "扶養控除".into(),
119
+ amount: FinalAmount::new(amount),
120
+ })
121
+ }
122
+
123
+ fn matches_basic_bracket(income: u64, bracket: &BasicDeductionBracket) -> bool {
124
+ income >= bracket.income_from
125
+ && match bracket.income_to_inclusive {
126
+ Some(to) => income <= to,
127
+ None => true,
128
+ }
129
+ }
130
+
131
+ fn matches_spouse_bracket(income: u64, bracket: &SpouseIncomeBracket) -> bool {
132
+ income >= bracket.taxpayer_income_from
133
+ && match bracket.taxpayer_income_to_inclusive {
134
+ Some(to) => income <= to,
135
+ None => true,
136
+ }
137
+ }
138
+
139
+ fn multiply_count(count: u16, amount: u64, step: &'static str) -> Result<u64, JLawError> {
140
+ u64::from(count)
141
+ .checked_mul(amount)
142
+ .ok_or_else(|| CalculationError::Overflow { step: step.into() })
143
+ .map_err(JLawError::from)
144
+ }
145
+
146
+ fn zero_line(kind: IncomeDeductionKind, label: &'static str) -> IncomeDeductionLine {
147
+ IncomeDeductionLine {
148
+ kind,
149
+ label: label.into(),
150
+ amount: FinalAmount::new(0),
151
+ }
152
+ }
153
+
154
+ #[cfg(test)]
155
+ #[allow(clippy::disallowed_methods)]
156
+ mod tests {
157
+ use super::*;
158
+ use crate::domains::income_tax::deduction::context::{
159
+ DependentDeductionInput, ExpenseDeductionInput, IncomeDeductionInput,
160
+ PersonalDeductionInput, SpouseDeductionInput,
161
+ };
162
+ use crate::domains::income_tax::deduction::params::{
163
+ BasicDeductionBracket, DependentDeductionParams, SpouseIncomeBracket,
164
+ };
165
+ use crate::types::date::LegalDate;
166
+
167
+ fn params() -> PersonalDeductionParams {
168
+ PersonalDeductionParams {
169
+ basic: BasicDeductionParams {
170
+ brackets: vec![
171
+ BasicDeductionBracket {
172
+ label: "2,400万円以下".into(),
173
+ income_from: 0,
174
+ income_to_inclusive: Some(24_000_000),
175
+ deduction_amount: 480_000,
176
+ },
177
+ BasicDeductionBracket {
178
+ label: "2,400万円超2,450万円以下".into(),
179
+ income_from: 24_000_001,
180
+ income_to_inclusive: Some(24_500_000),
181
+ deduction_amount: 320_000,
182
+ },
183
+ BasicDeductionBracket {
184
+ label: "2,450万円超2,500万円以下".into(),
185
+ income_from: 24_500_001,
186
+ income_to_inclusive: Some(25_000_000),
187
+ deduction_amount: 160_000,
188
+ },
189
+ BasicDeductionBracket {
190
+ label: "2,500万円超".into(),
191
+ income_from: 25_000_001,
192
+ income_to_inclusive: None,
193
+ deduction_amount: 0,
194
+ },
195
+ ],
196
+ },
197
+ spouse: SpouseDeductionParams {
198
+ qualifying_spouse_income_max: 480_000,
199
+ taxpayer_income_brackets: vec![
200
+ SpouseIncomeBracket {
201
+ label: "900万円以下".into(),
202
+ taxpayer_income_from: 0,
203
+ taxpayer_income_to_inclusive: Some(9_000_000),
204
+ deduction_amount: 380_000,
205
+ elderly_deduction_amount: 480_000,
206
+ },
207
+ SpouseIncomeBracket {
208
+ label: "900万円超950万円以下".into(),
209
+ taxpayer_income_from: 9_000_001,
210
+ taxpayer_income_to_inclusive: Some(9_500_000),
211
+ deduction_amount: 260_000,
212
+ elderly_deduction_amount: 320_000,
213
+ },
214
+ SpouseIncomeBracket {
215
+ label: "950万円超1000万円以下".into(),
216
+ taxpayer_income_from: 9_500_001,
217
+ taxpayer_income_to_inclusive: Some(10_000_000),
218
+ deduction_amount: 130_000,
219
+ elderly_deduction_amount: 160_000,
220
+ },
221
+ SpouseIncomeBracket {
222
+ label: "1000万円超".into(),
223
+ taxpayer_income_from: 10_000_001,
224
+ taxpayer_income_to_inclusive: None,
225
+ deduction_amount: 0,
226
+ elderly_deduction_amount: 0,
227
+ },
228
+ ],
229
+ },
230
+ dependent: DependentDeductionParams {
231
+ general_deduction_amount: 380_000,
232
+ specific_deduction_amount: 630_000,
233
+ elderly_cohabiting_deduction_amount: 580_000,
234
+ elderly_other_deduction_amount: 480_000,
235
+ },
236
+ }
237
+ }
238
+
239
+ fn ctx(
240
+ total_income_amount: u64,
241
+ spouse: Option<SpouseDeductionInput>,
242
+ dependent: DependentDeductionInput,
243
+ ) -> IncomeDeductionContext {
244
+ IncomeDeductionContext {
245
+ total_income_amount,
246
+ target_date: LegalDate::new(2024, 1, 1),
247
+ deductions: IncomeDeductionInput {
248
+ personal: PersonalDeductionInput { spouse, dependent },
249
+ expense: ExpenseDeductionInput {
250
+ social_insurance_premium_paid: 0,
251
+ medical: None,
252
+ life_insurance: None,
253
+ donation: None,
254
+ },
255
+ },
256
+ }
257
+ }
258
+
259
+ #[test]
260
+ fn basic_deduction_thresholds() {
261
+ let cases = [
262
+ (24_000_000, 480_000),
263
+ (24_000_001, 320_000),
264
+ (24_500_001, 160_000),
265
+ (25_000_001, 0),
266
+ ];
267
+
268
+ for (income, expected) in cases {
269
+ let result = calculate_personal_deductions(
270
+ &ctx(income, None, DependentDeductionInput::default()),
271
+ &params(),
272
+ )
273
+ .unwrap();
274
+ assert_eq!(result[0].amount.as_yen(), expected);
275
+ }
276
+ }
277
+
278
+ #[test]
279
+ fn spouse_deduction_thresholds() {
280
+ let cases = [
281
+ (9_000_000, 380_000),
282
+ (9_000_001, 260_000),
283
+ (9_500_001, 130_000),
284
+ (10_000_001, 0),
285
+ ];
286
+
287
+ for (income, expected) in cases {
288
+ let result = calculate_personal_deductions(
289
+ &ctx(
290
+ income,
291
+ Some(SpouseDeductionInput {
292
+ spouse_total_income_amount: 480_000,
293
+ is_same_household: true,
294
+ is_elderly: false,
295
+ }),
296
+ DependentDeductionInput::default(),
297
+ ),
298
+ &params(),
299
+ )
300
+ .unwrap();
301
+ assert_eq!(result[1].amount.as_yen(), expected);
302
+ }
303
+ }
304
+
305
+ #[test]
306
+ fn dependent_deduction_aggregates_all_categories() {
307
+ let result = calculate_personal_deductions(
308
+ &ctx(
309
+ 8_000_000,
310
+ None,
311
+ DependentDeductionInput {
312
+ general_count: 1,
313
+ specific_count: 1,
314
+ elderly_cohabiting_count: 1,
315
+ elderly_other_count: 1,
316
+ },
317
+ ),
318
+ &params(),
319
+ )
320
+ .unwrap();
321
+
322
+ assert_eq!(result[2].amount.as_yen(), 2_070_000);
323
+ }
324
+ }
@@ -0,0 +1,61 @@
1
+ use crate::types::amount::FinalAmount;
2
+
3
+ /// 所得控除の種別。
4
+ ///
5
+ /// # 法的根拠
6
+ /// 所得税法 各条項
7
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
8
+ pub enum IncomeDeductionKind {
9
+ /// 基礎控除。
10
+ Basic,
11
+ /// 配偶者控除。
12
+ Spouse,
13
+ /// 扶養控除。
14
+ Dependent,
15
+ /// 社会保険料控除。
16
+ SocialInsurance,
17
+ /// 医療費控除。
18
+ Medical,
19
+ /// 生命保険料控除。
20
+ LifeInsurance,
21
+ /// 寄附金控除。
22
+ Donation,
23
+ }
24
+
25
+ /// 所得控除の内訳1行。
26
+ ///
27
+ /// # 法的根拠
28
+ /// 所得税法 各条項
29
+ #[derive(Debug, Clone, PartialEq, Eq)]
30
+ pub struct IncomeDeductionLine {
31
+ /// 控除種別。
32
+ pub kind: IncomeDeductionKind,
33
+ /// 内訳表示用ラベル。
34
+ pub label: String,
35
+ /// 控除額(円)。
36
+ pub amount: FinalAmount,
37
+ }
38
+
39
+ /// 所得控除の計算結果。
40
+ ///
41
+ /// # 法的根拠
42
+ /// 所得税法 第73条(医療費控除)
43
+ /// 所得税法 第74条(社会保険料控除)
44
+ /// 所得税法 第76条(生命保険料控除)
45
+ /// 所得税法 第78条(寄附金控除)
46
+ /// 所得税法 第83条(配偶者控除)
47
+ /// 所得税法 第84条(扶養控除)
48
+ /// 所得税法 第86条(基礎控除)
49
+ #[derive(Debug, Clone, PartialEq, Eq)]
50
+ pub struct IncomeDeductionResult {
51
+ /// 総所得金額等(円)。
52
+ pub total_income_amount: FinalAmount,
53
+ /// 所得控除額の合計(円)。
54
+ pub total_deductions: FinalAmount,
55
+ /// 1,000円未満切り捨て前の課税所得金額(円)。
56
+ pub taxable_income_before_truncation: FinalAmount,
57
+ /// 1,000円未満切り捨て後の課税所得金額(円)。
58
+ pub taxable_income: FinalAmount,
59
+ /// 所得控除の内訳。
60
+ pub breakdown: Vec<IncomeDeductionLine>,
61
+ }
@@ -0,0 +1,24 @@
1
+ pub mod assessment;
2
+ pub mod calculator;
3
+ pub mod context;
4
+ pub mod deduction;
5
+ pub mod params;
6
+ pub mod policy;
7
+
8
+ pub use assessment::{
9
+ calculate_income_tax_assessment, IncomeTaxAssessmentContext, IncomeTaxAssessmentResult,
10
+ };
11
+ pub use calculator::{calculate_income_tax, IncomeTaxResult, IncomeTaxStep};
12
+ pub use context::{IncomeTaxContext, IncomeTaxFlag};
13
+ pub use deduction::{
14
+ calculate_income_deductions, BasicDeductionBracket, BasicDeductionParams,
15
+ DependentDeductionInput, DependentDeductionParams, DonationDeductionInput,
16
+ DonationDeductionParams, ExpenseDeductionInput, ExpenseDeductionParams, IncomeDeductionContext,
17
+ IncomeDeductionInput, IncomeDeductionKind, IncomeDeductionLine, IncomeDeductionParams,
18
+ IncomeDeductionResult, LifeInsuranceDeductionBracket, LifeInsuranceDeductionInput,
19
+ LifeInsuranceDeductionParams, MedicalDeductionInput, MedicalDeductionParams,
20
+ PersonalDeductionInput, PersonalDeductionParams, SocialInsuranceDeductionParams,
21
+ SpouseDeductionInput, SpouseDeductionParams, SpouseIncomeBracket,
22
+ };
23
+ pub use params::{IncomeTaxBracket, IncomeTaxParams, ReconstructionTaxParams};
24
+ pub use policy::StandardIncomeTaxPolicy;
@@ -0,0 +1,109 @@
1
+ /// 所得税の税率ブラケット(1段階分)。
2
+ ///
3
+ /// # 法的根拠
4
+ /// 所得税法 第89条第1項(税率表)
5
+ #[derive(Debug, Clone)]
6
+ pub struct IncomeTaxBracket {
7
+ /// ブラケットの表示名(例: "195万円以下")。
8
+ pub label: String,
9
+ /// 課税所得金額の下限(円・この金額以上)。
10
+ pub income_from: u64,
11
+ /// 課税所得金額の上限(円・この金額以下)。`None` は上限なし。
12
+ pub income_to_inclusive: Option<u64>,
13
+ /// 税率の分子(例: 5% → numer=5)。
14
+ pub rate_numer: u64,
15
+ /// 税率の分母(例: 5% → denom=100)。
16
+ pub rate_denom: u64,
17
+ /// 控除額(円)。速算表の控除額。
18
+ ///
19
+ /// 速算表方式: 税額 = 課税所得金額 × 税率 - 控除額
20
+ pub deduction: u64,
21
+ }
22
+
23
+ /// 復興特別所得税のパラメータ。
24
+ ///
25
+ /// # 法的根拠
26
+ /// 東日本大震災からの復興のための施策を実施するために必要な財源の確保に関する特別措置法
27
+ /// 第13条(復興特別所得税の税率)
28
+ ///
29
+ /// 2013年〜2037年の各年分の基準所得税額に対して課税される。
30
+ #[derive(Debug, Clone)]
31
+ pub struct ReconstructionTaxParams {
32
+ /// 税率の分子(例: 2.1% → numer=21)。
33
+ pub rate_numer: u64,
34
+ /// 税率の分母(例: 2.1% → denom=1000)。
35
+ pub rate_denom: u64,
36
+ /// 適用開始年(含む)。
37
+ pub effective_from_year: u16,
38
+ /// 適用終了年(含む)。
39
+ pub effective_to_year_inclusive: u16,
40
+ }
41
+
42
+ /// 所得税計算に使うパラメータセット。
43
+ ///
44
+ /// `j-law-registry` がJSONからロードしてこの型に変換する。
45
+ /// `j-law-core` の計算ロジックはこの型のみに依存する。
46
+ ///
47
+ /// # 法的根拠
48
+ /// 所得税法 第89条第1項(税率表)
49
+ #[derive(Debug, Clone)]
50
+ pub struct IncomeTaxParams {
51
+ /// 税率ブラケットの一覧(課税所得金額の低い方から順に並ぶ)。
52
+ pub brackets: Vec<IncomeTaxBracket>,
53
+ /// 復興特別所得税のパラメータ(適用期間外の場合は `None`)。
54
+ pub reconstruction_tax: Option<ReconstructionTaxParams>,
55
+ }
56
+
57
+ #[cfg(test)]
58
+ #[allow(clippy::disallowed_methods)]
59
+ mod tests {
60
+ use super::*;
61
+
62
+ #[test]
63
+ fn bracket_construction() {
64
+ let bracket = IncomeTaxBracket {
65
+ label: "195万円以下".into(),
66
+ income_from: 0,
67
+ income_to_inclusive: Some(1_950_000),
68
+ rate_numer: 5,
69
+ rate_denom: 100,
70
+ deduction: 0,
71
+ };
72
+ assert_eq!(bracket.rate_numer, 5);
73
+ assert_eq!(bracket.rate_denom, 100);
74
+ assert_eq!(bracket.deduction, 0);
75
+ }
76
+
77
+ #[test]
78
+ fn params_with_reconstruction_tax() {
79
+ let params = IncomeTaxParams {
80
+ brackets: vec![IncomeTaxBracket {
81
+ label: "195万円以下".into(),
82
+ income_from: 0,
83
+ income_to_inclusive: Some(1_950_000),
84
+ rate_numer: 5,
85
+ rate_denom: 100,
86
+ deduction: 0,
87
+ }],
88
+ reconstruction_tax: Some(ReconstructionTaxParams {
89
+ rate_numer: 21,
90
+ rate_denom: 1000,
91
+ effective_from_year: 2013,
92
+ effective_to_year_inclusive: 2037,
93
+ }),
94
+ };
95
+ assert_eq!(params.brackets.len(), 1);
96
+ let rt = params.reconstruction_tax.as_ref().unwrap();
97
+ assert_eq!(rt.rate_numer, 21);
98
+ assert_eq!(rt.rate_denom, 1000);
99
+ }
100
+
101
+ #[test]
102
+ fn params_without_reconstruction_tax() {
103
+ let params = IncomeTaxParams {
104
+ brackets: vec![],
105
+ reconstruction_tax: None,
106
+ };
107
+ assert!(params.reconstruction_tax.is_none());
108
+ }
109
+ }
@@ -0,0 +1,103 @@
1
+ use crate::domains::income_tax::context::IncomeTaxFlag;
2
+ use crate::types::rounding::RoundingStrategy;
3
+ use std::collections::HashSet;
4
+
5
+ /// 所得税計算のポリシーインターフェース。
6
+ ///
7
+ /// 端数処理戦略や復興特別所得税の適用判定ロジックを差し替えられるようにする。
8
+ /// 通常は [`StandardIncomeTaxPolicy`] を使う。
9
+ pub trait IncomeTaxPolicy: std::fmt::Debug {
10
+ /// 復興特別所得税を適用するかどうかを判定する。
11
+ fn should_apply_reconstruction_tax(
12
+ &self,
13
+ target_year: u16,
14
+ flags: &HashSet<IncomeTaxFlag>,
15
+ ) -> bool;
16
+
17
+ /// 速算表による税額計算に使う端数処理戦略。
18
+ ///
19
+ /// 所得税法上、課税所得金額 × 税率 の計算結果の端数処理。
20
+ fn tax_rounding(&self) -> RoundingStrategy;
21
+
22
+ /// 復興特別所得税額の端数処理戦略。
23
+ ///
24
+ /// 基準所得税額 × 2.1% の計算結果の端数処理。
25
+ fn reconstruction_tax_rounding(&self) -> RoundingStrategy;
26
+ }
27
+
28
+ /// 国税庁の標準解釈に基づく所得税計算ポリシー。
29
+ ///
30
+ /// # 法的根拠
31
+ /// 所得税法 第89条第1項(税率)
32
+ /// 復興財源確保法 第13条(復興特別所得税の税率)
33
+ /// 国税通則法 第119条第1項(税額の端数処理 — 100円未満切り捨て)
34
+ #[derive(Debug, Clone, Copy)]
35
+ pub struct StandardIncomeTaxPolicy;
36
+
37
+ impl IncomeTaxPolicy for StandardIncomeTaxPolicy {
38
+ fn should_apply_reconstruction_tax(
39
+ &self,
40
+ target_year: u16,
41
+ flags: &HashSet<IncomeTaxFlag>,
42
+ ) -> bool {
43
+ (2013..=2037).contains(&target_year)
44
+ && flags.contains(&IncomeTaxFlag::ApplyReconstructionTax)
45
+ }
46
+
47
+ fn tax_rounding(&self) -> RoundingStrategy {
48
+ // 所得税額は100円未満切り捨て(国税通則法 第119条第1項)。
49
+ // ただし速算表の計算自体は整数演算で端数が出ないため、
50
+ // 万一端数が発生した場合の安全策として Floor を指定する。
51
+ RoundingStrategy::Floor
52
+ }
53
+
54
+ fn reconstruction_tax_rounding(&self) -> RoundingStrategy {
55
+ // 復興特別所得税額は1円未満切り捨て。
56
+ RoundingStrategy::Floor
57
+ }
58
+ }
59
+
60
+ #[cfg(test)]
61
+ mod tests {
62
+ use super::*;
63
+
64
+ fn policy() -> StandardIncomeTaxPolicy {
65
+ StandardIncomeTaxPolicy
66
+ }
67
+
68
+ fn flags_with_reconstruction() -> HashSet<IncomeTaxFlag> {
69
+ let mut flags = HashSet::new();
70
+ flags.insert(IncomeTaxFlag::ApplyReconstructionTax);
71
+ flags
72
+ }
73
+
74
+ #[test]
75
+ fn reconstruction_tax_within_period() {
76
+ let flags = flags_with_reconstruction();
77
+ assert!(policy().should_apply_reconstruction_tax(2024, &flags));
78
+ assert!(policy().should_apply_reconstruction_tax(2013, &flags));
79
+ assert!(policy().should_apply_reconstruction_tax(2037, &flags));
80
+ }
81
+
82
+ #[test]
83
+ fn reconstruction_tax_outside_period() {
84
+ let flags = flags_with_reconstruction();
85
+ assert!(!policy().should_apply_reconstruction_tax(2012, &flags));
86
+ assert!(!policy().should_apply_reconstruction_tax(2038, &flags));
87
+ }
88
+
89
+ #[test]
90
+ fn reconstruction_tax_without_flag() {
91
+ let flags = HashSet::new();
92
+ assert!(!policy().should_apply_reconstruction_tax(2024, &flags));
93
+ }
94
+
95
+ #[test]
96
+ fn rounding_strategies() {
97
+ assert_eq!(policy().tax_rounding(), RoundingStrategy::Floor);
98
+ assert_eq!(
99
+ policy().reconstruction_tax_rounding(),
100
+ RoundingStrategy::Floor
101
+ );
102
+ }
103
+ }
@@ -0,0 +1,5 @@
1
+ pub mod consumption_tax;
2
+ pub mod income_tax;
3
+ pub mod real_estate;
4
+ pub mod stamp_tax;
5
+ pub mod withholding_tax;