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,465 @@
|
|
|
1
|
+
use crate::domains::income_tax::deduction::context::{
|
|
2
|
+
ExpenseDeductionInput, IncomeDeductionContext,
|
|
3
|
+
};
|
|
4
|
+
use crate::domains::income_tax::deduction::params::{
|
|
5
|
+
DonationDeductionParams, ExpenseDeductionParams, LifeInsuranceDeductionBracket,
|
|
6
|
+
LifeInsuranceDeductionParams, MedicalDeductionParams,
|
|
7
|
+
};
|
|
8
|
+
use crate::domains::income_tax::deduction::types::{IncomeDeductionKind, IncomeDeductionLine};
|
|
9
|
+
use crate::error::{CalculationError, InputError, JLawError};
|
|
10
|
+
use crate::types::amount::FinalAmount;
|
|
11
|
+
|
|
12
|
+
pub(crate) fn calculate_expense_deductions(
|
|
13
|
+
ctx: &IncomeDeductionContext,
|
|
14
|
+
params: &ExpenseDeductionParams,
|
|
15
|
+
) -> Result<Vec<IncomeDeductionLine>, JLawError> {
|
|
16
|
+
Ok(vec![
|
|
17
|
+
calculate_social_insurance_deduction(&ctx.deductions.expense),
|
|
18
|
+
calculate_medical_deduction(ctx, ¶ms.medical)?,
|
|
19
|
+
calculate_life_insurance_deduction(ctx, ¶ms.life_insurance)?,
|
|
20
|
+
calculate_donation_deduction(ctx, ¶ms.donation)?,
|
|
21
|
+
])
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fn calculate_social_insurance_deduction(expense: &ExpenseDeductionInput) -> IncomeDeductionLine {
|
|
25
|
+
IncomeDeductionLine {
|
|
26
|
+
kind: IncomeDeductionKind::SocialInsurance,
|
|
27
|
+
label: "社会保険料控除".into(),
|
|
28
|
+
amount: FinalAmount::new(expense.social_insurance_premium_paid),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn calculate_medical_deduction(
|
|
33
|
+
ctx: &IncomeDeductionContext,
|
|
34
|
+
params: &MedicalDeductionParams,
|
|
35
|
+
) -> Result<IncomeDeductionLine, JLawError> {
|
|
36
|
+
let Some(medical) = ctx.deductions.expense.medical else {
|
|
37
|
+
return Ok(zero_line(IncomeDeductionKind::Medical, "医療費控除"));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
let net_paid = medical
|
|
41
|
+
.medical_expense_paid
|
|
42
|
+
.checked_sub(medical.reimbursed_amount)
|
|
43
|
+
.ok_or(InputError::InvalidDeductionInput {
|
|
44
|
+
field: "medical.reimbursed_amount".into(),
|
|
45
|
+
reason: "補填額が支払医療費を上回っています".into(),
|
|
46
|
+
})?;
|
|
47
|
+
|
|
48
|
+
let income_threshold = calculate_rate_amount(
|
|
49
|
+
ctx.total_income_amount,
|
|
50
|
+
params.income_threshold_rate_numer,
|
|
51
|
+
params.income_threshold_rate_denom,
|
|
52
|
+
"medical_income_threshold",
|
|
53
|
+
)?;
|
|
54
|
+
let threshold = income_threshold.min(params.threshold_cap_amount);
|
|
55
|
+
let deduction_amount = net_paid
|
|
56
|
+
.saturating_sub(threshold)
|
|
57
|
+
.min(params.deduction_cap_amount);
|
|
58
|
+
|
|
59
|
+
Ok(IncomeDeductionLine {
|
|
60
|
+
kind: IncomeDeductionKind::Medical,
|
|
61
|
+
label: "医療費控除".into(),
|
|
62
|
+
amount: FinalAmount::new(deduction_amount),
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn calculate_life_insurance_deduction(
|
|
67
|
+
ctx: &IncomeDeductionContext,
|
|
68
|
+
params: &LifeInsuranceDeductionParams,
|
|
69
|
+
) -> Result<IncomeDeductionLine, JLawError> {
|
|
70
|
+
let Some(life_insurance) = ctx.deductions.expense.life_insurance else {
|
|
71
|
+
return Ok(zero_line(
|
|
72
|
+
IncomeDeductionKind::LifeInsurance,
|
|
73
|
+
"生命保険料控除",
|
|
74
|
+
));
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let general = calculate_life_insurance_component(
|
|
78
|
+
life_insurance.new_general_paid_amount,
|
|
79
|
+
life_insurance.old_general_paid_amount,
|
|
80
|
+
params,
|
|
81
|
+
"life_insurance_general",
|
|
82
|
+
)?;
|
|
83
|
+
let pension = calculate_life_insurance_component(
|
|
84
|
+
life_insurance.new_individual_pension_paid_amount,
|
|
85
|
+
life_insurance.old_individual_pension_paid_amount,
|
|
86
|
+
params,
|
|
87
|
+
"life_insurance_pension",
|
|
88
|
+
)?;
|
|
89
|
+
let care =
|
|
90
|
+
calculate_life_insurance_new_amount(life_insurance.new_care_medical_paid_amount, params)?;
|
|
91
|
+
|
|
92
|
+
let total = general
|
|
93
|
+
.checked_add(pension)
|
|
94
|
+
.ok_or_else(|| CalculationError::Overflow {
|
|
95
|
+
step: "life_insurance_total".into(),
|
|
96
|
+
})?
|
|
97
|
+
.checked_add(care)
|
|
98
|
+
.ok_or_else(|| CalculationError::Overflow {
|
|
99
|
+
step: "life_insurance_total".into(),
|
|
100
|
+
})?
|
|
101
|
+
.min(params.combined_cap_amount);
|
|
102
|
+
|
|
103
|
+
Ok(IncomeDeductionLine {
|
|
104
|
+
kind: IncomeDeductionKind::LifeInsurance,
|
|
105
|
+
label: "生命保険料控除".into(),
|
|
106
|
+
amount: FinalAmount::new(total),
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fn calculate_donation_deduction(
|
|
111
|
+
ctx: &IncomeDeductionContext,
|
|
112
|
+
params: &DonationDeductionParams,
|
|
113
|
+
) -> Result<IncomeDeductionLine, JLawError> {
|
|
114
|
+
let Some(donation) = ctx.deductions.expense.donation else {
|
|
115
|
+
return Ok(zero_line(IncomeDeductionKind::Donation, "寄附金控除"));
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
let income_cap = calculate_rate_amount(
|
|
119
|
+
ctx.total_income_amount,
|
|
120
|
+
params.income_cap_rate_numer,
|
|
121
|
+
params.income_cap_rate_denom,
|
|
122
|
+
"donation_income_cap",
|
|
123
|
+
)?;
|
|
124
|
+
let eligible_amount = donation.qualified_donation_amount.min(income_cap);
|
|
125
|
+
let deduction_amount = eligible_amount.saturating_sub(params.non_deductible_amount);
|
|
126
|
+
|
|
127
|
+
Ok(IncomeDeductionLine {
|
|
128
|
+
kind: IncomeDeductionKind::Donation,
|
|
129
|
+
label: "寄附金控除".into(),
|
|
130
|
+
amount: FinalAmount::new(deduction_amount),
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn calculate_life_insurance_component(
|
|
135
|
+
new_paid_amount: u64,
|
|
136
|
+
old_paid_amount: u64,
|
|
137
|
+
params: &LifeInsuranceDeductionParams,
|
|
138
|
+
overflow_step: &'static str,
|
|
139
|
+
) -> Result<u64, JLawError> {
|
|
140
|
+
let new_amount =
|
|
141
|
+
calculate_life_insurance_amount(new_paid_amount, ¶ms.new_contract_brackets, "新契約")?;
|
|
142
|
+
let old_amount =
|
|
143
|
+
calculate_life_insurance_amount(old_paid_amount, ¶ms.old_contract_brackets, "旧契約")?;
|
|
144
|
+
|
|
145
|
+
let component_amount = if new_paid_amount > 0 && old_paid_amount > 0 {
|
|
146
|
+
new_amount
|
|
147
|
+
.checked_add(old_amount)
|
|
148
|
+
.ok_or_else(|| CalculationError::Overflow {
|
|
149
|
+
step: overflow_step.into(),
|
|
150
|
+
})?
|
|
151
|
+
.min(params.mixed_contract_cap_amount)
|
|
152
|
+
} else if new_paid_amount > 0 {
|
|
153
|
+
new_amount.min(params.new_contract_cap_amount)
|
|
154
|
+
} else {
|
|
155
|
+
old_amount.min(params.old_contract_cap_amount)
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
Ok(component_amount)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fn calculate_life_insurance_new_amount(
|
|
162
|
+
paid_amount: u64,
|
|
163
|
+
params: &LifeInsuranceDeductionParams,
|
|
164
|
+
) -> Result<u64, JLawError> {
|
|
165
|
+
Ok(
|
|
166
|
+
calculate_life_insurance_amount(paid_amount, ¶ms.new_contract_brackets, "新契約")?
|
|
167
|
+
.min(params.new_contract_cap_amount),
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn calculate_life_insurance_amount(
|
|
172
|
+
paid_amount: u64,
|
|
173
|
+
brackets: &[LifeInsuranceDeductionBracket],
|
|
174
|
+
contract_label: &'static str,
|
|
175
|
+
) -> Result<u64, JLawError> {
|
|
176
|
+
if paid_amount == 0 {
|
|
177
|
+
return Ok(0);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let bracket = brackets
|
|
181
|
+
.iter()
|
|
182
|
+
.find(|bracket| matches_life_insurance_bracket(paid_amount, bracket))
|
|
183
|
+
.ok_or_else(|| CalculationError::PolicyNotApplicable {
|
|
184
|
+
reason: format!(
|
|
185
|
+
"{}の生命保険料控除ブラケットが見つかりません: {}円",
|
|
186
|
+
contract_label, paid_amount
|
|
187
|
+
),
|
|
188
|
+
})?;
|
|
189
|
+
|
|
190
|
+
let computed = calculate_rate_amount(
|
|
191
|
+
paid_amount,
|
|
192
|
+
bracket.rate_numer,
|
|
193
|
+
bracket.rate_denom,
|
|
194
|
+
"life_insurance_bracket",
|
|
195
|
+
)?
|
|
196
|
+
.checked_add(bracket.addition_amount)
|
|
197
|
+
.ok_or_else(|| CalculationError::Overflow {
|
|
198
|
+
step: "life_insurance_bracket".into(),
|
|
199
|
+
})?;
|
|
200
|
+
|
|
201
|
+
Ok(computed.min(bracket.deduction_cap_amount))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fn matches_life_insurance_bracket(
|
|
205
|
+
paid_amount: u64,
|
|
206
|
+
bracket: &LifeInsuranceDeductionBracket,
|
|
207
|
+
) -> bool {
|
|
208
|
+
paid_amount >= bracket.paid_from
|
|
209
|
+
&& match bracket.paid_to_inclusive {
|
|
210
|
+
Some(to) => paid_amount <= to,
|
|
211
|
+
None => true,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fn calculate_rate_amount(
|
|
216
|
+
amount: u64,
|
|
217
|
+
numer: u64,
|
|
218
|
+
denom: u64,
|
|
219
|
+
overflow_step: &'static str,
|
|
220
|
+
) -> Result<u64, JLawError> {
|
|
221
|
+
if denom == 0 {
|
|
222
|
+
return Err(InputError::ZeroDenominator.into());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let multiplied = amount
|
|
226
|
+
.checked_mul(numer)
|
|
227
|
+
.ok_or_else(|| CalculationError::Overflow {
|
|
228
|
+
step: overflow_step.into(),
|
|
229
|
+
})?;
|
|
230
|
+
|
|
231
|
+
// SAFETY: denom != 0 はこの関数の先頭ガードで保証済み。
|
|
232
|
+
Ok(multiplied / denom)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fn zero_line(kind: IncomeDeductionKind, label: &'static str) -> IncomeDeductionLine {
|
|
236
|
+
IncomeDeductionLine {
|
|
237
|
+
kind,
|
|
238
|
+
label: label.into(),
|
|
239
|
+
amount: FinalAmount::new(0),
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#[cfg(test)]
|
|
244
|
+
#[allow(clippy::disallowed_methods)]
|
|
245
|
+
mod tests {
|
|
246
|
+
use super::*;
|
|
247
|
+
use crate::domains::income_tax::deduction::context::{
|
|
248
|
+
DependentDeductionInput, DonationDeductionInput, ExpenseDeductionInput,
|
|
249
|
+
IncomeDeductionInput, LifeInsuranceDeductionInput, MedicalDeductionInput,
|
|
250
|
+
PersonalDeductionInput,
|
|
251
|
+
};
|
|
252
|
+
use crate::domains::income_tax::deduction::params::{
|
|
253
|
+
DonationDeductionParams, LifeInsuranceDeductionBracket, MedicalDeductionParams,
|
|
254
|
+
SocialInsuranceDeductionParams,
|
|
255
|
+
};
|
|
256
|
+
use crate::types::date::LegalDate;
|
|
257
|
+
|
|
258
|
+
fn params() -> ExpenseDeductionParams {
|
|
259
|
+
ExpenseDeductionParams {
|
|
260
|
+
social_insurance: SocialInsuranceDeductionParams,
|
|
261
|
+
medical: MedicalDeductionParams {
|
|
262
|
+
income_threshold_rate_numer: 5,
|
|
263
|
+
income_threshold_rate_denom: 100,
|
|
264
|
+
threshold_cap_amount: 100_000,
|
|
265
|
+
deduction_cap_amount: 2_000_000,
|
|
266
|
+
},
|
|
267
|
+
life_insurance: LifeInsuranceDeductionParams {
|
|
268
|
+
new_contract_brackets: vec![
|
|
269
|
+
LifeInsuranceDeductionBracket {
|
|
270
|
+
label: "2万円以下".into(),
|
|
271
|
+
paid_from: 0,
|
|
272
|
+
paid_to_inclusive: Some(20_000),
|
|
273
|
+
rate_numer: 1,
|
|
274
|
+
rate_denom: 1,
|
|
275
|
+
addition_amount: 0,
|
|
276
|
+
deduction_cap_amount: 20_000,
|
|
277
|
+
},
|
|
278
|
+
LifeInsuranceDeductionBracket {
|
|
279
|
+
label: "2万円超4万円以下".into(),
|
|
280
|
+
paid_from: 20_001,
|
|
281
|
+
paid_to_inclusive: Some(40_000),
|
|
282
|
+
rate_numer: 1,
|
|
283
|
+
rate_denom: 2,
|
|
284
|
+
addition_amount: 10_000,
|
|
285
|
+
deduction_cap_amount: 30_000,
|
|
286
|
+
},
|
|
287
|
+
LifeInsuranceDeductionBracket {
|
|
288
|
+
label: "4万円超8万円以下".into(),
|
|
289
|
+
paid_from: 40_001,
|
|
290
|
+
paid_to_inclusive: Some(80_000),
|
|
291
|
+
rate_numer: 1,
|
|
292
|
+
rate_denom: 4,
|
|
293
|
+
addition_amount: 20_000,
|
|
294
|
+
deduction_cap_amount: 40_000,
|
|
295
|
+
},
|
|
296
|
+
LifeInsuranceDeductionBracket {
|
|
297
|
+
label: "8万円超".into(),
|
|
298
|
+
paid_from: 80_001,
|
|
299
|
+
paid_to_inclusive: None,
|
|
300
|
+
rate_numer: 0,
|
|
301
|
+
rate_denom: 1,
|
|
302
|
+
addition_amount: 40_000,
|
|
303
|
+
deduction_cap_amount: 40_000,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
old_contract_brackets: vec![
|
|
307
|
+
LifeInsuranceDeductionBracket {
|
|
308
|
+
label: "2万5千円以下".into(),
|
|
309
|
+
paid_from: 0,
|
|
310
|
+
paid_to_inclusive: Some(25_000),
|
|
311
|
+
rate_numer: 1,
|
|
312
|
+
rate_denom: 1,
|
|
313
|
+
addition_amount: 0,
|
|
314
|
+
deduction_cap_amount: 25_000,
|
|
315
|
+
},
|
|
316
|
+
LifeInsuranceDeductionBracket {
|
|
317
|
+
label: "2万5千円超5万円以下".into(),
|
|
318
|
+
paid_from: 25_001,
|
|
319
|
+
paid_to_inclusive: Some(50_000),
|
|
320
|
+
rate_numer: 1,
|
|
321
|
+
rate_denom: 2,
|
|
322
|
+
addition_amount: 12_500,
|
|
323
|
+
deduction_cap_amount: 37_500,
|
|
324
|
+
},
|
|
325
|
+
LifeInsuranceDeductionBracket {
|
|
326
|
+
label: "5万円超10万円以下".into(),
|
|
327
|
+
paid_from: 50_001,
|
|
328
|
+
paid_to_inclusive: Some(100_000),
|
|
329
|
+
rate_numer: 1,
|
|
330
|
+
rate_denom: 4,
|
|
331
|
+
addition_amount: 25_000,
|
|
332
|
+
deduction_cap_amount: 50_000,
|
|
333
|
+
},
|
|
334
|
+
LifeInsuranceDeductionBracket {
|
|
335
|
+
label: "10万円超".into(),
|
|
336
|
+
paid_from: 100_001,
|
|
337
|
+
paid_to_inclusive: None,
|
|
338
|
+
rate_numer: 0,
|
|
339
|
+
rate_denom: 1,
|
|
340
|
+
addition_amount: 50_000,
|
|
341
|
+
deduction_cap_amount: 50_000,
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
mixed_contract_cap_amount: 40_000,
|
|
345
|
+
new_contract_cap_amount: 40_000,
|
|
346
|
+
old_contract_cap_amount: 50_000,
|
|
347
|
+
combined_cap_amount: 120_000,
|
|
348
|
+
},
|
|
349
|
+
donation: DonationDeductionParams {
|
|
350
|
+
income_cap_rate_numer: 40,
|
|
351
|
+
income_cap_rate_denom: 100,
|
|
352
|
+
non_deductible_amount: 2_000,
|
|
353
|
+
},
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
fn ctx(expense: ExpenseDeductionInput) -> IncomeDeductionContext {
|
|
358
|
+
IncomeDeductionContext {
|
|
359
|
+
total_income_amount: 5_000_000,
|
|
360
|
+
target_date: LegalDate::new(2024, 1, 1),
|
|
361
|
+
deductions: IncomeDeductionInput {
|
|
362
|
+
personal: PersonalDeductionInput {
|
|
363
|
+
spouse: None,
|
|
364
|
+
dependent: DependentDeductionInput::default(),
|
|
365
|
+
},
|
|
366
|
+
expense,
|
|
367
|
+
},
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
#[test]
|
|
372
|
+
fn social_insurance_deduction_uses_full_paid_amount() {
|
|
373
|
+
let result = calculate_expense_deductions(
|
|
374
|
+
&ctx(ExpenseDeductionInput {
|
|
375
|
+
social_insurance_premium_paid: 480_900,
|
|
376
|
+
medical: None,
|
|
377
|
+
life_insurance: None,
|
|
378
|
+
donation: None,
|
|
379
|
+
}),
|
|
380
|
+
¶ms(),
|
|
381
|
+
)
|
|
382
|
+
.unwrap();
|
|
383
|
+
assert_eq!(result[0].amount.as_yen(), 480_900);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#[test]
|
|
387
|
+
fn medical_deduction_uses_five_percent_threshold_for_low_income() {
|
|
388
|
+
let result = calculate_expense_deductions(
|
|
389
|
+
&ctx(ExpenseDeductionInput {
|
|
390
|
+
social_insurance_premium_paid: 0,
|
|
391
|
+
medical: Some(MedicalDeductionInput {
|
|
392
|
+
medical_expense_paid: 400_000,
|
|
393
|
+
reimbursed_amount: 50_000,
|
|
394
|
+
}),
|
|
395
|
+
life_insurance: None,
|
|
396
|
+
donation: None,
|
|
397
|
+
}),
|
|
398
|
+
¶ms(),
|
|
399
|
+
)
|
|
400
|
+
.unwrap();
|
|
401
|
+
|
|
402
|
+
assert_eq!(result[1].amount.as_yen(), 250_000);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
#[test]
|
|
406
|
+
fn medical_deduction_rejects_reimbursements_above_paid_amount() {
|
|
407
|
+
let result = calculate_expense_deductions(
|
|
408
|
+
&ctx(ExpenseDeductionInput {
|
|
409
|
+
social_insurance_premium_paid: 0,
|
|
410
|
+
medical: Some(MedicalDeductionInput {
|
|
411
|
+
medical_expense_paid: 100_000,
|
|
412
|
+
reimbursed_amount: 100_001,
|
|
413
|
+
}),
|
|
414
|
+
life_insurance: None,
|
|
415
|
+
donation: None,
|
|
416
|
+
}),
|
|
417
|
+
¶ms(),
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
assert!(matches!(
|
|
421
|
+
result,
|
|
422
|
+
Err(JLawError::Input(InputError::InvalidDeductionInput { .. }))
|
|
423
|
+
));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#[test]
|
|
427
|
+
fn life_insurance_deduction_caps_total_at_120k() {
|
|
428
|
+
let result = calculate_expense_deductions(
|
|
429
|
+
&ctx(ExpenseDeductionInput {
|
|
430
|
+
social_insurance_premium_paid: 0,
|
|
431
|
+
medical: None,
|
|
432
|
+
life_insurance: Some(LifeInsuranceDeductionInput {
|
|
433
|
+
new_general_paid_amount: 100_000,
|
|
434
|
+
new_individual_pension_paid_amount: 100_000,
|
|
435
|
+
new_care_medical_paid_amount: 100_000,
|
|
436
|
+
old_general_paid_amount: 0,
|
|
437
|
+
old_individual_pension_paid_amount: 0,
|
|
438
|
+
}),
|
|
439
|
+
donation: None,
|
|
440
|
+
}),
|
|
441
|
+
¶ms(),
|
|
442
|
+
)
|
|
443
|
+
.unwrap();
|
|
444
|
+
|
|
445
|
+
assert_eq!(result[2].amount.as_yen(), 120_000);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
#[test]
|
|
449
|
+
fn donation_deduction_uses_income_cap_and_self_burden() {
|
|
450
|
+
let result = calculate_expense_deductions(
|
|
451
|
+
&ctx(ExpenseDeductionInput {
|
|
452
|
+
social_insurance_premium_paid: 0,
|
|
453
|
+
medical: None,
|
|
454
|
+
life_insurance: None,
|
|
455
|
+
donation: Some(DonationDeductionInput {
|
|
456
|
+
qualified_donation_amount: 3_000_000,
|
|
457
|
+
}),
|
|
458
|
+
}),
|
|
459
|
+
¶ms(),
|
|
460
|
+
)
|
|
461
|
+
.unwrap();
|
|
462
|
+
|
|
463
|
+
assert_eq!(result[3].amount.as_yen(), 1_998_000);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
pub mod calculator;
|
|
2
|
+
pub mod context;
|
|
3
|
+
pub mod expense;
|
|
4
|
+
pub mod params;
|
|
5
|
+
pub mod personal;
|
|
6
|
+
pub mod types;
|
|
7
|
+
|
|
8
|
+
pub use calculator::calculate_income_deductions;
|
|
9
|
+
pub use context::{
|
|
10
|
+
DependentDeductionInput, DonationDeductionInput, ExpenseDeductionInput, IncomeDeductionContext,
|
|
11
|
+
IncomeDeductionInput, LifeInsuranceDeductionInput, MedicalDeductionInput,
|
|
12
|
+
PersonalDeductionInput, SpouseDeductionInput,
|
|
13
|
+
};
|
|
14
|
+
pub use params::{
|
|
15
|
+
BasicDeductionBracket, BasicDeductionParams, DependentDeductionParams, DonationDeductionParams,
|
|
16
|
+
ExpenseDeductionParams, IncomeDeductionParams, LifeInsuranceDeductionBracket,
|
|
17
|
+
LifeInsuranceDeductionParams, MedicalDeductionParams, PersonalDeductionParams,
|
|
18
|
+
SocialInsuranceDeductionParams, SpouseDeductionParams, SpouseIncomeBracket,
|
|
19
|
+
};
|
|
20
|
+
pub use types::{IncomeDeductionKind, IncomeDeductionLine, IncomeDeductionResult};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/// 所得控除計算で使用するパラメータセット。
|
|
2
|
+
///
|
|
3
|
+
/// # 法的根拠
|
|
4
|
+
/// 所得税法 第73条(医療費控除)
|
|
5
|
+
/// 所得税法 第74条(社会保険料控除)
|
|
6
|
+
/// 所得税法 第76条(生命保険料控除)
|
|
7
|
+
/// 所得税法 第78条(寄附金控除)
|
|
8
|
+
/// 所得税法 第83条(配偶者控除)
|
|
9
|
+
/// 所得税法 第84条(扶養控除)
|
|
10
|
+
/// 所得税法 第86条(基礎控除)
|
|
11
|
+
#[derive(Debug, Clone)]
|
|
12
|
+
pub struct IncomeDeductionParams {
|
|
13
|
+
/// 人的控除パラメータ。
|
|
14
|
+
pub personal: PersonalDeductionParams,
|
|
15
|
+
/// 支出系控除パラメータ。
|
|
16
|
+
pub expense: ExpenseDeductionParams,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// 人的控除パラメータ。
|
|
20
|
+
///
|
|
21
|
+
/// # 法的根拠
|
|
22
|
+
/// 所得税法 第83条(配偶者控除)
|
|
23
|
+
/// 所得税法 第84条(扶養控除)
|
|
24
|
+
/// 所得税法 第86条(基礎控除)
|
|
25
|
+
#[derive(Debug, Clone)]
|
|
26
|
+
pub struct PersonalDeductionParams {
|
|
27
|
+
/// 基礎控除のパラメータ。
|
|
28
|
+
pub basic: BasicDeductionParams,
|
|
29
|
+
/// 配偶者控除のパラメータ。
|
|
30
|
+
pub spouse: SpouseDeductionParams,
|
|
31
|
+
/// 扶養控除のパラメータ。
|
|
32
|
+
pub dependent: DependentDeductionParams,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// 基礎控除のパラメータ。
|
|
36
|
+
///
|
|
37
|
+
/// # 法的根拠
|
|
38
|
+
/// 所得税法 第86条(基礎控除)
|
|
39
|
+
#[derive(Debug, Clone)]
|
|
40
|
+
pub struct BasicDeductionParams {
|
|
41
|
+
/// 総所得金額等に応じた基礎控除額テーブル。
|
|
42
|
+
pub brackets: Vec<BasicDeductionBracket>,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// 基礎控除の所得閾値テーブル。
|
|
46
|
+
///
|
|
47
|
+
/// # 法的根拠
|
|
48
|
+
/// 所得税法 第86条(基礎控除)
|
|
49
|
+
#[derive(Debug, Clone)]
|
|
50
|
+
pub struct BasicDeductionBracket {
|
|
51
|
+
/// ブラケットの表示名。
|
|
52
|
+
pub label: String,
|
|
53
|
+
/// 総所得金額等の下限(円・この金額以上)。
|
|
54
|
+
pub income_from: u64,
|
|
55
|
+
/// 総所得金額等の上限(円・この金額以下)。`None` は上限なし。
|
|
56
|
+
pub income_to_inclusive: Option<u64>,
|
|
57
|
+
/// 当該ブラケットで適用する基礎控除額(円)。
|
|
58
|
+
pub deduction_amount: u64,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// 支出系控除パラメータ。
|
|
62
|
+
///
|
|
63
|
+
/// # 法的根拠
|
|
64
|
+
/// 所得税法 第73条(医療費控除)
|
|
65
|
+
/// 所得税法 第74条(社会保険料控除)
|
|
66
|
+
/// 所得税法 第76条(生命保険料控除)
|
|
67
|
+
/// 所得税法 第78条(寄附金控除)
|
|
68
|
+
#[derive(Debug, Clone)]
|
|
69
|
+
pub struct ExpenseDeductionParams {
|
|
70
|
+
/// 社会保険料控除のパラメータ。
|
|
71
|
+
pub social_insurance: SocialInsuranceDeductionParams,
|
|
72
|
+
/// 医療費控除のパラメータ。
|
|
73
|
+
pub medical: MedicalDeductionParams,
|
|
74
|
+
/// 生命保険料控除のパラメータ。
|
|
75
|
+
pub life_insurance: LifeInsuranceDeductionParams,
|
|
76
|
+
/// 寄附金控除のパラメータ。
|
|
77
|
+
pub donation: DonationDeductionParams,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// 社会保険料控除のパラメータ。
|
|
81
|
+
///
|
|
82
|
+
/// 現段階では全額控除のため追加設定を持たない。
|
|
83
|
+
///
|
|
84
|
+
/// # 法的根拠
|
|
85
|
+
/// 所得税法 第74条(社会保険料控除)
|
|
86
|
+
#[derive(Debug, Clone, Copy)]
|
|
87
|
+
pub struct SocialInsuranceDeductionParams;
|
|
88
|
+
|
|
89
|
+
/// 医療費控除のパラメータ。
|
|
90
|
+
///
|
|
91
|
+
/// # 法的根拠
|
|
92
|
+
/// 所得税法 第73条(医療費控除)
|
|
93
|
+
#[derive(Debug, Clone, Copy)]
|
|
94
|
+
pub struct MedicalDeductionParams {
|
|
95
|
+
/// 足切額に用いる所得割合の分子。
|
|
96
|
+
pub income_threshold_rate_numer: u64,
|
|
97
|
+
/// 足切額に用いる所得割合の分母。
|
|
98
|
+
pub income_threshold_rate_denom: u64,
|
|
99
|
+
/// 足切額の固定上限額(円)。
|
|
100
|
+
pub threshold_cap_amount: u64,
|
|
101
|
+
/// 医療費控除額の上限額(円)。
|
|
102
|
+
pub deduction_cap_amount: u64,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// 生命保険料控除のパラメータ。
|
|
106
|
+
///
|
|
107
|
+
/// # 法的根拠
|
|
108
|
+
/// 所得税法 第76条(生命保険料控除)
|
|
109
|
+
#[derive(Debug, Clone)]
|
|
110
|
+
pub struct LifeInsuranceDeductionParams {
|
|
111
|
+
/// 新契約に適用する計算ブラケット。
|
|
112
|
+
pub new_contract_brackets: Vec<LifeInsuranceDeductionBracket>,
|
|
113
|
+
/// 旧契約に適用する計算ブラケット。
|
|
114
|
+
pub old_contract_brackets: Vec<LifeInsuranceDeductionBracket>,
|
|
115
|
+
/// 新旧両方の契約がある区分の控除上限額(円)。
|
|
116
|
+
pub mixed_contract_cap_amount: u64,
|
|
117
|
+
/// 新契約のみの区分に適用する控除上限額(円)。
|
|
118
|
+
pub new_contract_cap_amount: u64,
|
|
119
|
+
/// 旧契約のみの区分に適用する控除上限額(円)。
|
|
120
|
+
pub old_contract_cap_amount: u64,
|
|
121
|
+
/// 全区分合計の控除上限額(円)。
|
|
122
|
+
pub combined_cap_amount: u64,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// 生命保険料控除のブラケット。
|
|
126
|
+
///
|
|
127
|
+
/// # 法的根拠
|
|
128
|
+
/// 所得税法 第76条(生命保険料控除)
|
|
129
|
+
#[derive(Debug, Clone)]
|
|
130
|
+
pub struct LifeInsuranceDeductionBracket {
|
|
131
|
+
/// ブラケットの表示名。
|
|
132
|
+
pub label: String,
|
|
133
|
+
/// 支払保険料の下限(円・この金額以上)。
|
|
134
|
+
pub paid_from: u64,
|
|
135
|
+
/// 支払保険料の上限(円・この金額以下)。`None` は上限なし。
|
|
136
|
+
pub paid_to_inclusive: Option<u64>,
|
|
137
|
+
/// 控除額計算に用いる乗率の分子。
|
|
138
|
+
pub rate_numer: u64,
|
|
139
|
+
/// 控除額計算に用いる乗率の分母。
|
|
140
|
+
pub rate_denom: u64,
|
|
141
|
+
/// 控除額に加算する固定額(円)。
|
|
142
|
+
pub addition_amount: u64,
|
|
143
|
+
/// 当該ブラケット単体での控除上限額(円)。
|
|
144
|
+
pub deduction_cap_amount: u64,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// 寄附金控除のパラメータ。
|
|
148
|
+
///
|
|
149
|
+
/// # 法的根拠
|
|
150
|
+
/// 所得税法 第78条(寄附金控除)
|
|
151
|
+
#[derive(Debug, Clone, Copy)]
|
|
152
|
+
pub struct DonationDeductionParams {
|
|
153
|
+
/// 総所得金額等に対する控除対象上限割合の分子。
|
|
154
|
+
pub income_cap_rate_numer: u64,
|
|
155
|
+
/// 総所得金額等に対する控除対象上限割合の分母。
|
|
156
|
+
pub income_cap_rate_denom: u64,
|
|
157
|
+
/// 控除対象外となる自己負担額(円)。
|
|
158
|
+
pub non_deductible_amount: u64,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// 配偶者控除のパラメータ。
|
|
162
|
+
///
|
|
163
|
+
/// # 法的根拠
|
|
164
|
+
/// 所得税法 第83条(配偶者控除)
|
|
165
|
+
#[derive(Debug, Clone)]
|
|
166
|
+
pub struct SpouseDeductionParams {
|
|
167
|
+
/// 控除対象配偶者に該当する配偶者の合計所得金額上限(円)。
|
|
168
|
+
pub qualifying_spouse_income_max: u64,
|
|
169
|
+
/// 納税者本人の合計所得金額に応じた控除額テーブル。
|
|
170
|
+
pub taxpayer_income_brackets: Vec<SpouseIncomeBracket>,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// 配偶者控除の所得閾値テーブル。
|
|
174
|
+
///
|
|
175
|
+
/// # 法的根拠
|
|
176
|
+
/// 所得税法 第83条(配偶者控除)
|
|
177
|
+
#[derive(Debug, Clone)]
|
|
178
|
+
pub struct SpouseIncomeBracket {
|
|
179
|
+
/// ブラケットの表示名。
|
|
180
|
+
pub label: String,
|
|
181
|
+
/// 納税者本人の合計所得金額の下限(円・この金額以上)。
|
|
182
|
+
pub taxpayer_income_from: u64,
|
|
183
|
+
/// 納税者本人の合計所得金額の上限(円・この金額以下)。`None` は上限なし。
|
|
184
|
+
pub taxpayer_income_to_inclusive: Option<u64>,
|
|
185
|
+
/// 一般の控除対象配偶者に適用する控除額(円)。
|
|
186
|
+
pub deduction_amount: u64,
|
|
187
|
+
/// 老人控除対象配偶者に適用する控除額(円)。
|
|
188
|
+
pub elderly_deduction_amount: u64,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// 扶養控除のパラメータ。
|
|
192
|
+
///
|
|
193
|
+
/// # 法的根拠
|
|
194
|
+
/// 所得税法 第84条(扶養控除)
|
|
195
|
+
#[derive(Debug, Clone, Copy)]
|
|
196
|
+
pub struct DependentDeductionParams {
|
|
197
|
+
/// 一般の控除対象扶養親族1人当たりの控除額(円)。
|
|
198
|
+
pub general_deduction_amount: u64,
|
|
199
|
+
/// 特定扶養親族1人当たりの控除額(円)。
|
|
200
|
+
pub specific_deduction_amount: u64,
|
|
201
|
+
/// 同居老親等1人当たりの控除額(円)。
|
|
202
|
+
pub elderly_cohabiting_deduction_amount: u64,
|
|
203
|
+
/// 同居老親等以外の老人扶養親族1人当たりの控除額(円)。
|
|
204
|
+
pub elderly_other_deduction_amount: u64,
|
|
205
|
+
}
|