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,18 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "j-law-core"
|
|
3
|
+
version = "0.0.3"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
rust-version = "1.94"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
publish = false
|
|
8
|
+
|
|
9
|
+
[lints]
|
|
10
|
+
workspace = true
|
|
11
|
+
|
|
12
|
+
[dependencies]
|
|
13
|
+
thiserror = "1"
|
|
14
|
+
|
|
15
|
+
[dev-dependencies]
|
|
16
|
+
j-law-registry = { path = "../j-law-registry" }
|
|
17
|
+
serde = { version = "1", features = ["derive"] }
|
|
18
|
+
serde_json = "1"
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
|
|
3
|
+
use crate::domains::consumption_tax::context::{ConsumptionTaxContext, ConsumptionTaxFlag};
|
|
4
|
+
use crate::domains::consumption_tax::params::ConsumptionTaxParams;
|
|
5
|
+
use crate::error::{CalculationError, JLawError};
|
|
6
|
+
use crate::types::amount::{FinalAmount, IntermediateAmount};
|
|
7
|
+
use crate::types::rate::{MultiplyOrder, Rate};
|
|
8
|
+
|
|
9
|
+
/// 消費税の計算結果。
|
|
10
|
+
#[derive(Debug, Clone)]
|
|
11
|
+
pub struct ConsumptionTaxResult {
|
|
12
|
+
/// 消費税額(切り捨て後)。
|
|
13
|
+
pub tax_amount: FinalAmount,
|
|
14
|
+
/// 税込み金額(課税標準額 + 消費税額)。
|
|
15
|
+
pub amount_with_tax: FinalAmount,
|
|
16
|
+
/// 課税標準額(税抜き)。`ctx.amount` と等しい。
|
|
17
|
+
pub amount_without_tax: FinalAmount,
|
|
18
|
+
/// 適用した税率の分子。
|
|
19
|
+
pub applied_rate_numer: u64,
|
|
20
|
+
/// 適用した税率の分母。
|
|
21
|
+
pub applied_rate_denom: u64,
|
|
22
|
+
/// 軽減税率が適用されたか。
|
|
23
|
+
pub is_reduced_rate: bool,
|
|
24
|
+
/// 適用されたフラグ。
|
|
25
|
+
pub applied_flags: HashSet<ConsumptionTaxFlag>,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// 消費税法第29条に基づく消費税を計算する。
|
|
29
|
+
///
|
|
30
|
+
/// # 法的根拠
|
|
31
|
+
/// 消費税法 第29条(税率)
|
|
32
|
+
/// 消費税法 第45条(端数処理: 1円未満切り捨て)
|
|
33
|
+
///
|
|
34
|
+
/// # 計算手順
|
|
35
|
+
/// 1. ポリシーで軽減税率を適用するか判定する
|
|
36
|
+
/// 2. 軽減税率適用フラグがあっても `reduced_rate` が `None` の場合は
|
|
37
|
+
/// [`CalculationError::PolicyNotApplicable`] を返す
|
|
38
|
+
/// 3. 課税標準額 × 税率(切り捨て)で消費税額を算出する
|
|
39
|
+
/// 4. 課税標準額 + 消費税額で税込み金額を算出する
|
|
40
|
+
///
|
|
41
|
+
/// # 税率が0の場合(消費税導入前)
|
|
42
|
+
/// `standard_rate.numer == 0` のとき、税額は0として計算結果を返す。
|
|
43
|
+
pub fn calculate_consumption_tax(
|
|
44
|
+
ctx: &ConsumptionTaxContext,
|
|
45
|
+
params: &ConsumptionTaxParams,
|
|
46
|
+
) -> Result<ConsumptionTaxResult, JLawError> {
|
|
47
|
+
ctx.target_date.validate()?;
|
|
48
|
+
|
|
49
|
+
let amount = ctx.amount;
|
|
50
|
+
let rounding = ctx.policy.tax_rounding();
|
|
51
|
+
let use_reduced = ctx.policy.should_apply_reduced_rate(&ctx.flags);
|
|
52
|
+
|
|
53
|
+
let (applied_rate, is_reduced) = if use_reduced {
|
|
54
|
+
let reduced = params.reduced_rate.ok_or_else(|| CalculationError::PolicyNotApplicable {
|
|
55
|
+
reason: format!(
|
|
56
|
+
"軽減税率フラグが指定されましたが、対象日({})の消費税パラメータに軽減税率が存在しません",
|
|
57
|
+
ctx.target_date.to_date_str()
|
|
58
|
+
),
|
|
59
|
+
})?;
|
|
60
|
+
(reduced, true)
|
|
61
|
+
} else {
|
|
62
|
+
(params.standard_rate, false)
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// 税率が0の場合(消費税導入前)は税額0を返す
|
|
66
|
+
let tax_amount = if applied_rate.numer == 0 {
|
|
67
|
+
FinalAmount::new(0)
|
|
68
|
+
} else {
|
|
69
|
+
let rate = Rate::new(applied_rate.numer, applied_rate.denom).map_err(|_| {
|
|
70
|
+
CalculationError::Overflow {
|
|
71
|
+
step: "consumption_tax_rate".into(),
|
|
72
|
+
}
|
|
73
|
+
})?;
|
|
74
|
+
// Rate::apply() は端数処理済みの整数を from_exact で包んで返す(numer=0)。
|
|
75
|
+
// そのため .finalize() では端数部が0として評価され、二重端数処理は発生しない。
|
|
76
|
+
rate.apply(
|
|
77
|
+
&IntermediateAmount::from_exact(amount),
|
|
78
|
+
MultiplyOrder::MultiplyFirst,
|
|
79
|
+
rounding,
|
|
80
|
+
)?
|
|
81
|
+
.finalize(rounding)?
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
let amount_with_tax =
|
|
85
|
+
FinalAmount::new(amount.checked_add(tax_amount.as_yen()).ok_or_else(|| {
|
|
86
|
+
CalculationError::Overflow {
|
|
87
|
+
step: "consumption_tax".into(),
|
|
88
|
+
}
|
|
89
|
+
})?);
|
|
90
|
+
|
|
91
|
+
Ok(ConsumptionTaxResult {
|
|
92
|
+
tax_amount,
|
|
93
|
+
amount_with_tax,
|
|
94
|
+
amount_without_tax: FinalAmount::new(amount),
|
|
95
|
+
applied_rate_numer: applied_rate.numer,
|
|
96
|
+
applied_rate_denom: applied_rate.denom,
|
|
97
|
+
is_reduced_rate: is_reduced,
|
|
98
|
+
applied_flags: ctx.flags.clone(),
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#[cfg(test)]
|
|
103
|
+
#[allow(clippy::disallowed_methods)]
|
|
104
|
+
mod tests {
|
|
105
|
+
use super::*;
|
|
106
|
+
use crate::domains::consumption_tax::params::ConsumptionTaxRate;
|
|
107
|
+
use crate::domains::consumption_tax::policy::StandardConsumptionTaxPolicy;
|
|
108
|
+
use crate::LegalDate;
|
|
109
|
+
|
|
110
|
+
fn params_10pct() -> ConsumptionTaxParams {
|
|
111
|
+
ConsumptionTaxParams {
|
|
112
|
+
standard_rate: ConsumptionTaxRate {
|
|
113
|
+
numer: 10,
|
|
114
|
+
denom: 100,
|
|
115
|
+
},
|
|
116
|
+
reduced_rate: Some(ConsumptionTaxRate {
|
|
117
|
+
numer: 8,
|
|
118
|
+
denom: 100,
|
|
119
|
+
}),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn ctx_standard(amount: u64) -> ConsumptionTaxContext {
|
|
124
|
+
ConsumptionTaxContext {
|
|
125
|
+
amount,
|
|
126
|
+
target_date: LegalDate::new(2020, 1, 1),
|
|
127
|
+
flags: HashSet::new(),
|
|
128
|
+
policy: Box::new(StandardConsumptionTaxPolicy),
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn ctx_reduced(amount: u64) -> ConsumptionTaxContext {
|
|
133
|
+
let mut flags = HashSet::new();
|
|
134
|
+
flags.insert(ConsumptionTaxFlag::ReducedRate);
|
|
135
|
+
ConsumptionTaxContext {
|
|
136
|
+
amount,
|
|
137
|
+
target_date: LegalDate::new(2020, 1, 1),
|
|
138
|
+
flags,
|
|
139
|
+
policy: Box::new(StandardConsumptionTaxPolicy),
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[test]
|
|
144
|
+
fn standard_10pct() {
|
|
145
|
+
let result = calculate_consumption_tax(&ctx_standard(100_000), ¶ms_10pct()).unwrap();
|
|
146
|
+
assert_eq!(result.tax_amount.as_yen(), 10_000);
|
|
147
|
+
assert_eq!(result.amount_with_tax.as_yen(), 110_000);
|
|
148
|
+
assert_eq!(result.amount_without_tax.as_yen(), 100_000);
|
|
149
|
+
assert!(!result.is_reduced_rate);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
fn reduced_8pct() {
|
|
154
|
+
let result = calculate_consumption_tax(&ctx_reduced(100_000), ¶ms_10pct()).unwrap();
|
|
155
|
+
assert_eq!(result.tax_amount.as_yen(), 8_000);
|
|
156
|
+
assert_eq!(result.amount_with_tax.as_yen(), 108_000);
|
|
157
|
+
assert!(result.is_reduced_rate);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#[test]
|
|
161
|
+
fn floor_rounding() {
|
|
162
|
+
// 100,001 × 10% = 10,000.1 → 切り捨て → 10,000
|
|
163
|
+
let result = calculate_consumption_tax(&ctx_standard(100_001), ¶ms_10pct()).unwrap();
|
|
164
|
+
assert_eq!(result.tax_amount.as_yen(), 10_000);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[test]
|
|
168
|
+
fn zero_amount() {
|
|
169
|
+
let result = calculate_consumption_tax(&ctx_standard(0), ¶ms_10pct()).unwrap();
|
|
170
|
+
assert_eq!(result.tax_amount.as_yen(), 0);
|
|
171
|
+
assert_eq!(result.amount_with_tax.as_yen(), 0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#[test]
|
|
175
|
+
fn zero_rate_no_tax() {
|
|
176
|
+
let params = ConsumptionTaxParams {
|
|
177
|
+
standard_rate: ConsumptionTaxRate {
|
|
178
|
+
numer: 0,
|
|
179
|
+
denom: 100,
|
|
180
|
+
},
|
|
181
|
+
reduced_rate: None,
|
|
182
|
+
};
|
|
183
|
+
let result = calculate_consumption_tax(&ctx_standard(100_000), ¶ms).unwrap();
|
|
184
|
+
assert_eq!(result.tax_amount.as_yen(), 0);
|
|
185
|
+
assert_eq!(result.amount_with_tax.as_yen(), 100_000);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[test]
|
|
189
|
+
fn reduced_flag_without_reduced_rate_is_error() {
|
|
190
|
+
let params = ConsumptionTaxParams {
|
|
191
|
+
standard_rate: ConsumptionTaxRate {
|
|
192
|
+
numer: 8,
|
|
193
|
+
denom: 100,
|
|
194
|
+
},
|
|
195
|
+
reduced_rate: None, // 軽減税率なし
|
|
196
|
+
};
|
|
197
|
+
let result = calculate_consumption_tax(&ctx_reduced(100_000), ¶ms);
|
|
198
|
+
assert!(matches!(
|
|
199
|
+
result,
|
|
200
|
+
Err(JLawError::Calculation(
|
|
201
|
+
crate::error::CalculationError::PolicyNotApplicable { .. }
|
|
202
|
+
))
|
|
203
|
+
));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#[test]
|
|
207
|
+
fn invalid_target_date_is_rejected() {
|
|
208
|
+
let mut ctx = ctx_standard(100_000);
|
|
209
|
+
ctx.target_date = LegalDate::new(2024, 2, 30);
|
|
210
|
+
let result = calculate_consumption_tax(&ctx, ¶ms_10pct());
|
|
211
|
+
assert!(matches!(
|
|
212
|
+
result,
|
|
213
|
+
Err(JLawError::Input(crate::InputError::InvalidDate { .. }))
|
|
214
|
+
));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
|
|
3
|
+
use crate::domains::consumption_tax::policy::ConsumptionTaxPolicy;
|
|
4
|
+
use crate::types::date::LegalDate;
|
|
5
|
+
|
|
6
|
+
/// 消費税計算に影響するフラグ。
|
|
7
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
8
|
+
pub enum ConsumptionTaxFlag {
|
|
9
|
+
/// 軽減税率(8%)を適用する。
|
|
10
|
+
///
|
|
11
|
+
/// # WARNING
|
|
12
|
+
/// 「軽減税率の対象品目に該当するか」の事実認定はライブラリの責任範囲外。
|
|
13
|
+
/// 呼び出し元が適切に判断してこのフラグを設定すること。
|
|
14
|
+
/// 対象品目: 飲食料品(酒類・外食を除く)、定期購読の新聞(2019年10月1日〜)。
|
|
15
|
+
ReducedRate,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// 消費税計算のコンテキスト。
|
|
19
|
+
#[derive(Debug)]
|
|
20
|
+
pub struct ConsumptionTaxContext {
|
|
21
|
+
/// 課税標準額(税抜き金額・円)。
|
|
22
|
+
pub amount: u64,
|
|
23
|
+
/// 計算対象日。
|
|
24
|
+
pub target_date: LegalDate,
|
|
25
|
+
/// 適用フラグ。
|
|
26
|
+
pub flags: HashSet<ConsumptionTaxFlag>,
|
|
27
|
+
/// 端数処理・特例判定ポリシー。
|
|
28
|
+
pub policy: Box<dyn ConsumptionTaxPolicy>,
|
|
29
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pub mod calculator;
|
|
2
|
+
pub mod context;
|
|
3
|
+
pub mod params;
|
|
4
|
+
pub mod policy;
|
|
5
|
+
|
|
6
|
+
pub use calculator::{calculate_consumption_tax, ConsumptionTaxResult};
|
|
7
|
+
pub use context::{ConsumptionTaxContext, ConsumptionTaxFlag};
|
|
8
|
+
pub use params::{ConsumptionTaxParams, ConsumptionTaxRate};
|
|
9
|
+
pub use policy::StandardConsumptionTaxPolicy;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/// 消費税率(分子・分母表現)。
|
|
2
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
3
|
+
pub struct ConsumptionTaxRate {
|
|
4
|
+
pub numer: u64,
|
|
5
|
+
pub denom: u64,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/// 消費税計算パラメータ。
|
|
9
|
+
///
|
|
10
|
+
/// # 法的根拠
|
|
11
|
+
/// 消費税法 第29条
|
|
12
|
+
///
|
|
13
|
+
/// # 税率の歴史
|
|
14
|
+
/// - 3%: 1989年4月1日〜1997年3月31日
|
|
15
|
+
/// - 5%: 1997年4月1日〜2014年3月31日
|
|
16
|
+
/// - 8%: 2014年4月1日〜2019年9月30日
|
|
17
|
+
/// - 10%(標準)/ 8%(軽減): 2019年10月1日〜
|
|
18
|
+
#[derive(Debug, Clone)]
|
|
19
|
+
pub struct ConsumptionTaxParams {
|
|
20
|
+
/// 標準税率。
|
|
21
|
+
pub standard_rate: ConsumptionTaxRate,
|
|
22
|
+
/// 軽減税率。2019年10月1日以降の飲食料品・新聞等に適用。それ以前は `None`。
|
|
23
|
+
pub reduced_rate: Option<ConsumptionTaxRate>,
|
|
24
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
|
|
3
|
+
use crate::domains::consumption_tax::context::ConsumptionTaxFlag;
|
|
4
|
+
use crate::types::rounding::RoundingStrategy;
|
|
5
|
+
|
|
6
|
+
/// 消費税計算のポリシーインターフェース。
|
|
7
|
+
///
|
|
8
|
+
/// 端数処理戦略や軽減税率の適用判定ロジックを差し替えられるようにする。
|
|
9
|
+
/// 通常は [`StandardConsumptionTaxPolicy`] を使う。
|
|
10
|
+
pub trait ConsumptionTaxPolicy: std::fmt::Debug {
|
|
11
|
+
/// 軽減税率をフラグに基づいて適用するかどうかを判定する。
|
|
12
|
+
fn should_apply_reduced_rate(&self, flags: &HashSet<ConsumptionTaxFlag>) -> bool;
|
|
13
|
+
|
|
14
|
+
/// 税額計算に使う端数処理戦略。
|
|
15
|
+
fn tax_rounding(&self) -> RoundingStrategy;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// 消費税法の標準解釈に基づく消費税計算ポリシー。
|
|
19
|
+
///
|
|
20
|
+
/// # 法的根拠
|
|
21
|
+
/// 消費税法 第29条
|
|
22
|
+
/// 消費税法 第45条(端数処理: 1円未満切り捨て)
|
|
23
|
+
#[derive(Debug, Clone, Copy)]
|
|
24
|
+
pub struct StandardConsumptionTaxPolicy;
|
|
25
|
+
|
|
26
|
+
impl ConsumptionTaxPolicy for StandardConsumptionTaxPolicy {
|
|
27
|
+
fn should_apply_reduced_rate(&self, flags: &HashSet<ConsumptionTaxFlag>) -> bool {
|
|
28
|
+
flags.contains(&ConsumptionTaxFlag::ReducedRate)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn tax_rounding(&self) -> RoundingStrategy {
|
|
32
|
+
RoundingStrategy::Floor
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
use std::fmt;
|
|
3
|
+
|
|
4
|
+
use crate::domains::income_tax::calculator::{calculate_income_tax_inner, IncomeTaxResult};
|
|
5
|
+
use crate::domains::income_tax::context::IncomeTaxFlag;
|
|
6
|
+
use crate::domains::income_tax::deduction::{
|
|
7
|
+
calculate_income_deductions, IncomeDeductionContext, IncomeDeductionParams,
|
|
8
|
+
IncomeDeductionResult,
|
|
9
|
+
};
|
|
10
|
+
use crate::domains::income_tax::params::IncomeTaxParams;
|
|
11
|
+
use crate::domains::income_tax::policy::IncomeTaxPolicy;
|
|
12
|
+
use crate::error::JLawError;
|
|
13
|
+
|
|
14
|
+
/// 所得控除の計算から所得税額の算出までを通しで実行するコンテキスト。
|
|
15
|
+
///
|
|
16
|
+
/// # 法的根拠
|
|
17
|
+
/// 所得税法 第74条(社会保険料控除)
|
|
18
|
+
/// 所得税法 第86条(基礎控除)
|
|
19
|
+
/// 所得税法 第89条第1項(所得税の税率)
|
|
20
|
+
pub struct IncomeTaxAssessmentContext {
|
|
21
|
+
/// 所得控除の入力コンテキスト。
|
|
22
|
+
pub deduction_context: IncomeDeductionContext,
|
|
23
|
+
/// 適用する法的フラグの集合。
|
|
24
|
+
pub flags: HashSet<IncomeTaxFlag>,
|
|
25
|
+
/// 計算ポリシー。
|
|
26
|
+
pub policy: Box<dyn IncomeTaxPolicy>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl fmt::Debug for IncomeTaxAssessmentContext {
|
|
30
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
31
|
+
f.debug_struct("IncomeTaxAssessmentContext")
|
|
32
|
+
.field("deduction_context", &self.deduction_context)
|
|
33
|
+
.field("flags", &self.flags)
|
|
34
|
+
.field("policy", &"<policy>")
|
|
35
|
+
.finish()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// 所得控除と所得税額の通し計算結果。
|
|
40
|
+
///
|
|
41
|
+
/// # 法的根拠
|
|
42
|
+
/// 所得税法 第74条(社会保険料控除)
|
|
43
|
+
/// 所得税法 第86条(基礎控除)
|
|
44
|
+
/// 所得税法 第89条第1項(所得税の税率)
|
|
45
|
+
/// 復興財源確保法 第13条(復興特別所得税)
|
|
46
|
+
#[derive(Debug, Clone)]
|
|
47
|
+
pub struct IncomeTaxAssessmentResult {
|
|
48
|
+
/// 所得控除の計算結果。
|
|
49
|
+
pub deductions: IncomeDeductionResult,
|
|
50
|
+
/// 課税所得金額に対する所得税額の計算結果。
|
|
51
|
+
pub tax: IncomeTaxResult,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// 所得控除の計算結果を使って所得税額を算出する。
|
|
55
|
+
///
|
|
56
|
+
/// # 法的根拠
|
|
57
|
+
/// 所得税法 第74条(社会保険料控除)
|
|
58
|
+
/// 所得税法 第86条(基礎控除)
|
|
59
|
+
/// 所得税法 第89条第1項(所得税の税率)
|
|
60
|
+
/// 復興財源確保法 第13条(復興特別所得税)
|
|
61
|
+
pub fn calculate_income_tax_assessment(
|
|
62
|
+
ctx: &IncomeTaxAssessmentContext,
|
|
63
|
+
deduction_params: &IncomeDeductionParams,
|
|
64
|
+
tax_params: &IncomeTaxParams,
|
|
65
|
+
) -> Result<IncomeTaxAssessmentResult, JLawError> {
|
|
66
|
+
let deductions = calculate_income_deductions(&ctx.deduction_context, deduction_params)?;
|
|
67
|
+
let tax = calculate_income_tax_inner(
|
|
68
|
+
deductions.taxable_income.as_yen(),
|
|
69
|
+
ctx.deduction_context.target_date,
|
|
70
|
+
&ctx.flags,
|
|
71
|
+
ctx.policy.as_ref(),
|
|
72
|
+
tax_params,
|
|
73
|
+
)?;
|
|
74
|
+
|
|
75
|
+
Ok(IncomeTaxAssessmentResult { deductions, tax })
|
|
76
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
|
|
3
|
+
use crate::domains::income_tax::context::{IncomeTaxContext, IncomeTaxFlag};
|
|
4
|
+
use crate::domains::income_tax::params::IncomeTaxParams;
|
|
5
|
+
use crate::domains::income_tax::policy::IncomeTaxPolicy;
|
|
6
|
+
use crate::error::{CalculationError, JLawError};
|
|
7
|
+
use crate::types::amount::{FinalAmount, IntermediateAmount};
|
|
8
|
+
use crate::types::date::LegalDate;
|
|
9
|
+
use crate::types::rate::{MultiplyOrder, Rate};
|
|
10
|
+
|
|
11
|
+
/// 所得税の計算ステップ(内訳明細用)。
|
|
12
|
+
///
|
|
13
|
+
/// 速算表の適用結果を記録する。
|
|
14
|
+
#[derive(Debug, Clone)]
|
|
15
|
+
pub struct IncomeTaxStep {
|
|
16
|
+
/// ブラケットの表示名(例: "330万円超695万円以下")。
|
|
17
|
+
pub label: String,
|
|
18
|
+
/// 課税所得金額(円)。
|
|
19
|
+
pub taxable_income: u64,
|
|
20
|
+
/// 適用された税率の分子。
|
|
21
|
+
pub rate_numer: u64,
|
|
22
|
+
/// 適用された税率の分母。
|
|
23
|
+
pub rate_denom: u64,
|
|
24
|
+
/// 速算表の控除額(円)。
|
|
25
|
+
pub deduction: u64,
|
|
26
|
+
/// このステップでの算出税額(課税所得 × 税率 - 控除額)。
|
|
27
|
+
pub result: FinalAmount,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// 所得税の計算結果。
|
|
31
|
+
///
|
|
32
|
+
/// # 法的根拠
|
|
33
|
+
/// 所得税法 第89条第1項(所得税の税率)
|
|
34
|
+
/// 復興財源確保法 第13条(復興特別所得税)
|
|
35
|
+
/// 国税通則法 第119条第1項(税額の端数処理)
|
|
36
|
+
#[derive(Debug, Clone)]
|
|
37
|
+
pub struct IncomeTaxResult {
|
|
38
|
+
/// 基準所得税額(復興特別所得税を含まない)。
|
|
39
|
+
pub base_tax: FinalAmount,
|
|
40
|
+
/// 復興特別所得税額(適用されない場合は0円)。
|
|
41
|
+
pub reconstruction_tax: FinalAmount,
|
|
42
|
+
/// 申告納税額(基準所得税額 + 復興特別所得税額)。
|
|
43
|
+
///
|
|
44
|
+
/// 100円未満切り捨て(国税通則法 第119条第1項)。
|
|
45
|
+
pub total_tax: FinalAmount,
|
|
46
|
+
/// 計算の内訳ステップ。
|
|
47
|
+
pub breakdown: Vec<IncomeTaxStep>,
|
|
48
|
+
/// 適用されたフラグ。
|
|
49
|
+
pub applied_flags: HashSet<IncomeTaxFlag>,
|
|
50
|
+
/// 復興特別所得税が適用されたか。
|
|
51
|
+
pub reconstruction_tax_applied: bool,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// 所得税法第89条に基づく所得税額を計算する。
|
|
55
|
+
///
|
|
56
|
+
/// # 法的根拠
|
|
57
|
+
/// 所得税法 第89条第1項(税率表・速算表方式)
|
|
58
|
+
/// 復興財源確保法 第13条(復興特別所得税 2.1%)
|
|
59
|
+
/// 国税通則法 第119条第1項(申告納税額の100円未満切り捨て)
|
|
60
|
+
///
|
|
61
|
+
/// # 計算手順
|
|
62
|
+
/// 1. 課税所得金額が該当するブラケットを特定する
|
|
63
|
+
/// 2. 速算表方式で基準所得税額を算出: 課税所得金額 × 税率 - 控除額
|
|
64
|
+
/// 3. 復興特別所得税が適用される場合: 基準所得税額 × 21/1000(1円未満切り捨て)
|
|
65
|
+
/// 4. 申告納税額 = 基準所得税額 + 復興特別所得税額(100円未満切り捨て)
|
|
66
|
+
pub fn calculate_income_tax(
|
|
67
|
+
ctx: &IncomeTaxContext,
|
|
68
|
+
params: &IncomeTaxParams,
|
|
69
|
+
) -> Result<IncomeTaxResult, JLawError> {
|
|
70
|
+
calculate_income_tax_inner(
|
|
71
|
+
ctx.taxable_income,
|
|
72
|
+
ctx.target_date,
|
|
73
|
+
&ctx.flags,
|
|
74
|
+
ctx.policy.as_ref(),
|
|
75
|
+
params,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pub(crate) fn calculate_income_tax_inner(
|
|
80
|
+
taxable_income: u64,
|
|
81
|
+
target_date: LegalDate,
|
|
82
|
+
flags: &HashSet<IncomeTaxFlag>,
|
|
83
|
+
policy: &dyn IncomeTaxPolicy,
|
|
84
|
+
params: &IncomeTaxParams,
|
|
85
|
+
) -> Result<IncomeTaxResult, JLawError> {
|
|
86
|
+
target_date.validate()?;
|
|
87
|
+
|
|
88
|
+
let income = taxable_income;
|
|
89
|
+
let tax_rounding = policy.tax_rounding();
|
|
90
|
+
|
|
91
|
+
// --- 課税所得金額が0の場合 ---
|
|
92
|
+
if income == 0 {
|
|
93
|
+
return Ok(IncomeTaxResult {
|
|
94
|
+
base_tax: FinalAmount::new(0),
|
|
95
|
+
reconstruction_tax: FinalAmount::new(0),
|
|
96
|
+
total_tax: FinalAmount::new(0),
|
|
97
|
+
breakdown: vec![],
|
|
98
|
+
applied_flags: flags.clone(),
|
|
99
|
+
reconstruction_tax_applied: false,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// --- 該当ブラケットの特定 ---
|
|
104
|
+
let bracket = params
|
|
105
|
+
.brackets
|
|
106
|
+
.iter()
|
|
107
|
+
.find(|b| {
|
|
108
|
+
income >= b.income_from
|
|
109
|
+
&& match b.income_to_inclusive {
|
|
110
|
+
Some(to) => income <= to,
|
|
111
|
+
None => true,
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
.ok_or_else(|| CalculationError::PolicyNotApplicable {
|
|
115
|
+
reason: format!(
|
|
116
|
+
"課税所得金額 {}円 に対応する税率ブラケットが見つかりません",
|
|
117
|
+
income
|
|
118
|
+
),
|
|
119
|
+
})?;
|
|
120
|
+
|
|
121
|
+
// --- 速算表方式による基準所得税額の計算 ---
|
|
122
|
+
// 税額 = 課税所得金額 × 税率 - 控除額
|
|
123
|
+
let rate = Rate::new(bracket.rate_numer, bracket.rate_denom).map_err(|_| {
|
|
124
|
+
CalculationError::Overflow {
|
|
125
|
+
step: "income_tax_rate".into(),
|
|
126
|
+
}
|
|
127
|
+
})?;
|
|
128
|
+
let gross_tax = rate
|
|
129
|
+
.apply(
|
|
130
|
+
&IntermediateAmount::from_exact(income),
|
|
131
|
+
MultiplyOrder::MultiplyFirst,
|
|
132
|
+
tax_rounding,
|
|
133
|
+
)?
|
|
134
|
+
.finalize(tax_rounding)?;
|
|
135
|
+
|
|
136
|
+
let base_tax_yen = gross_tax
|
|
137
|
+
.as_yen()
|
|
138
|
+
.checked_sub(bracket.deduction)
|
|
139
|
+
.ok_or_else(|| CalculationError::Overflow {
|
|
140
|
+
step: "base_tax_deduction".into(),
|
|
141
|
+
})?;
|
|
142
|
+
|
|
143
|
+
let base_tax = FinalAmount::new(base_tax_yen);
|
|
144
|
+
|
|
145
|
+
let breakdown = vec![IncomeTaxStep {
|
|
146
|
+
label: bracket.label.clone(),
|
|
147
|
+
taxable_income: income,
|
|
148
|
+
rate_numer: bracket.rate_numer,
|
|
149
|
+
rate_denom: bracket.rate_denom,
|
|
150
|
+
deduction: bracket.deduction,
|
|
151
|
+
result: base_tax,
|
|
152
|
+
}];
|
|
153
|
+
|
|
154
|
+
// --- 復興特別所得税 ---
|
|
155
|
+
let target_year = target_date.year;
|
|
156
|
+
let apply_reconstruction = policy.should_apply_reconstruction_tax(target_year, flags);
|
|
157
|
+
|
|
158
|
+
let reconstruction_tax_yen = if apply_reconstruction {
|
|
159
|
+
if let Some(rt_params) = ¶ms.reconstruction_tax {
|
|
160
|
+
let rt_rate = Rate::new(rt_params.rate_numer, rt_params.rate_denom).map_err(|_| {
|
|
161
|
+
CalculationError::Overflow {
|
|
162
|
+
step: "reconstruction_tax_rate".into(),
|
|
163
|
+
}
|
|
164
|
+
})?;
|
|
165
|
+
let rt_rounding = policy.reconstruction_tax_rounding();
|
|
166
|
+
rt_rate
|
|
167
|
+
.apply(
|
|
168
|
+
&IntermediateAmount::from_exact(base_tax_yen),
|
|
169
|
+
MultiplyOrder::MultiplyFirst,
|
|
170
|
+
rt_rounding,
|
|
171
|
+
)?
|
|
172
|
+
.finalize(rt_rounding)?
|
|
173
|
+
.as_yen()
|
|
174
|
+
} else {
|
|
175
|
+
return Err(CalculationError::PolicyNotApplicable {
|
|
176
|
+
reason: "ApplyReconstructionTaxフラグが指定されましたが、パラメータセットに復興特別所得税データが含まれていません".into(),
|
|
177
|
+
}
|
|
178
|
+
.into());
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
0
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
let reconstruction_tax = FinalAmount::new(reconstruction_tax_yen);
|
|
185
|
+
|
|
186
|
+
// --- 申告納税額(100円未満切り捨て) ---
|
|
187
|
+
// 国税通則法 第119条第1項
|
|
188
|
+
let total_before_truncation = base_tax_yen
|
|
189
|
+
.checked_add(reconstruction_tax_yen)
|
|
190
|
+
.ok_or_else(|| CalculationError::Overflow {
|
|
191
|
+
step: "total_tax".into(),
|
|
192
|
+
})?;
|
|
193
|
+
let total_tax = FinalAmount::new(truncate_below_100(total_before_truncation));
|
|
194
|
+
|
|
195
|
+
Ok(IncomeTaxResult {
|
|
196
|
+
base_tax,
|
|
197
|
+
reconstruction_tax,
|
|
198
|
+
total_tax,
|
|
199
|
+
breakdown,
|
|
200
|
+
applied_flags: flags.clone(),
|
|
201
|
+
reconstruction_tax_applied: apply_reconstruction,
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// 100円未満を切り捨てる(国税通則法 第119条第1項)。
|
|
206
|
+
fn truncate_below_100(amount: u64) -> u64 {
|
|
207
|
+
amount / 100 * 100
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[cfg(test)]
|
|
211
|
+
mod tests {
|
|
212
|
+
use super::*;
|
|
213
|
+
|
|
214
|
+
#[test]
|
|
215
|
+
fn truncate_below_100_works() {
|
|
216
|
+
assert_eq!(truncate_below_100(0), 0);
|
|
217
|
+
assert_eq!(truncate_below_100(99), 0);
|
|
218
|
+
assert_eq!(truncate_below_100(100), 100);
|
|
219
|
+
assert_eq!(truncate_below_100(199), 100);
|
|
220
|
+
assert_eq!(truncate_below_100(584_522), 584_500);
|
|
221
|
+
}
|
|
222
|
+
}
|