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,105 @@
|
|
|
1
|
+
use crate::domains::stamp_tax::context::StampTaxContext;
|
|
2
|
+
use crate::domains::stamp_tax::params::{StampTaxDocumentParams, StampTaxSpecialRule};
|
|
3
|
+
|
|
4
|
+
/// 印紙税の計算ポリシー。
|
|
5
|
+
pub trait StampTaxPolicy: std::fmt::Debug {
|
|
6
|
+
/// 適用すべき特例ルールを選択する。
|
|
7
|
+
///
|
|
8
|
+
/// # 法的根拠
|
|
9
|
+
/// 印紙税法 別表第一 / 租税特別措置法 第91条
|
|
10
|
+
fn select_special_rule<'a>(
|
|
11
|
+
&self,
|
|
12
|
+
ctx: &StampTaxContext,
|
|
13
|
+
document: &'a StampTaxDocumentParams,
|
|
14
|
+
) -> Option<&'a StampTaxSpecialRule>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// 国税庁の標準ポリシー。
|
|
18
|
+
#[derive(Debug, Clone, Copy)]
|
|
19
|
+
pub struct StandardNtaPolicy;
|
|
20
|
+
|
|
21
|
+
impl StampTaxPolicy for StandardNtaPolicy {
|
|
22
|
+
fn select_special_rule<'a>(
|
|
23
|
+
&self,
|
|
24
|
+
ctx: &StampTaxContext,
|
|
25
|
+
document: &'a StampTaxDocumentParams,
|
|
26
|
+
) -> Option<&'a StampTaxSpecialRule> {
|
|
27
|
+
let date_str = ctx.target_date.to_date_str();
|
|
28
|
+
document
|
|
29
|
+
.special_rules
|
|
30
|
+
.iter()
|
|
31
|
+
.filter(|rule| rule.matches_date(&date_str))
|
|
32
|
+
.filter(|rule| {
|
|
33
|
+
rule.required_flags
|
|
34
|
+
.iter()
|
|
35
|
+
.all(|flag| ctx.flags.contains(flag))
|
|
36
|
+
})
|
|
37
|
+
.filter(|rule| rule.matches_amount(ctx.stated_amount))
|
|
38
|
+
.min_by_key(|rule| rule.priority)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[cfg(test)]
|
|
43
|
+
#[allow(clippy::disallowed_methods)]
|
|
44
|
+
mod tests {
|
|
45
|
+
use std::collections::HashSet;
|
|
46
|
+
|
|
47
|
+
use super::*;
|
|
48
|
+
use crate::domains::stamp_tax::context::{StampTaxDocumentCode, StampTaxFlag};
|
|
49
|
+
use crate::domains::stamp_tax::params::{
|
|
50
|
+
StampTaxAmountUsage, StampTaxBracket, StampTaxChargeMode, StampTaxCitation,
|
|
51
|
+
};
|
|
52
|
+
use crate::types::date::LegalDate;
|
|
53
|
+
|
|
54
|
+
#[test]
|
|
55
|
+
fn standard_policy_selects_matching_rule() {
|
|
56
|
+
let document = StampTaxDocumentParams {
|
|
57
|
+
code: StampTaxDocumentCode::Article1RealEstateTransfer,
|
|
58
|
+
label: "第1号".into(),
|
|
59
|
+
citation: StampTaxCitation {
|
|
60
|
+
law_name: "印紙税法".into(),
|
|
61
|
+
article: "別表第一 第1号文書".into(),
|
|
62
|
+
},
|
|
63
|
+
charge_mode: StampTaxChargeMode::AmountBrackets,
|
|
64
|
+
amount_usage: StampTaxAmountUsage::Optional,
|
|
65
|
+
base_rule_label: "通常".into(),
|
|
66
|
+
base_tax_amount: None,
|
|
67
|
+
brackets: vec![],
|
|
68
|
+
no_amount_tax_amount: Some(200),
|
|
69
|
+
no_amount_rule_label: Some("記載なし".into()),
|
|
70
|
+
special_rules: vec![StampTaxSpecialRule {
|
|
71
|
+
code: "reduced".into(),
|
|
72
|
+
label: "軽減".into(),
|
|
73
|
+
priority: 1,
|
|
74
|
+
effective_from: Some("2014-04-01".into()),
|
|
75
|
+
effective_until: Some("2027-03-31".into()),
|
|
76
|
+
required_flags: vec![StampTaxFlag::Article17NonBusinessExempt],
|
|
77
|
+
tax_amount: Some(0),
|
|
78
|
+
rule_label: Some("非課税".into()),
|
|
79
|
+
brackets: vec![StampTaxBracket {
|
|
80
|
+
label: "50万円以下".into(),
|
|
81
|
+
amount_from: 0,
|
|
82
|
+
amount_to_inclusive: Some(500_000),
|
|
83
|
+
tax_amount: 0,
|
|
84
|
+
}],
|
|
85
|
+
no_amount_tax_amount: None,
|
|
86
|
+
no_amount_rule_label: None,
|
|
87
|
+
}],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
let mut flags = HashSet::new();
|
|
91
|
+
flags.insert(StampTaxFlag::Article17NonBusinessExempt);
|
|
92
|
+
let ctx = StampTaxContext {
|
|
93
|
+
document_code: StampTaxDocumentCode::Article1RealEstateTransfer,
|
|
94
|
+
stated_amount: Some(100_000),
|
|
95
|
+
target_date: LegalDate::new(2024, 1, 1),
|
|
96
|
+
flags,
|
|
97
|
+
policy: Box::new(StandardNtaPolicy),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let rule = StandardNtaPolicy
|
|
101
|
+
.select_special_rule(&ctx, &document)
|
|
102
|
+
.unwrap();
|
|
103
|
+
assert_eq!(rule.code, "reduced");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
|
|
3
|
+
use crate::domains::withholding_tax::context::{
|
|
4
|
+
WithholdingTaxCategory, WithholdingTaxContext, WithholdingTaxFlag,
|
|
5
|
+
};
|
|
6
|
+
use crate::domains::withholding_tax::params::{WithholdingTaxMethod, WithholdingTaxParams};
|
|
7
|
+
use crate::domains::withholding_tax::policy::WithholdingTaxPolicy;
|
|
8
|
+
use crate::error::{CalculationError, InputError, JLawError};
|
|
9
|
+
use crate::types::amount::{FinalAmount, IntermediateAmount};
|
|
10
|
+
use crate::types::rate::{MultiplyOrder, Rate};
|
|
11
|
+
|
|
12
|
+
/// 源泉徴収税額の計算内訳1行。
|
|
13
|
+
#[derive(Debug, Clone)]
|
|
14
|
+
pub struct WithholdingTaxStep {
|
|
15
|
+
/// 内訳ラベル。
|
|
16
|
+
pub label: String,
|
|
17
|
+
/// 当該行の対象金額(円)。
|
|
18
|
+
pub base_amount: u64,
|
|
19
|
+
/// 適用税率の分子。
|
|
20
|
+
pub rate_numer: u64,
|
|
21
|
+
/// 適用税率の分母。
|
|
22
|
+
pub rate_denom: u64,
|
|
23
|
+
/// 当該行の税額(円)。
|
|
24
|
+
pub result: FinalAmount,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// 源泉徴収税額の計算結果。
|
|
28
|
+
///
|
|
29
|
+
/// # 法的根拠
|
|
30
|
+
/// 所得税法 第204条第1項
|
|
31
|
+
/// 東日本大震災からの復興のための施策を実施するために必要な財源の確保に関する特別措置法
|
|
32
|
+
#[derive(Debug, Clone)]
|
|
33
|
+
pub struct WithholdingTaxResult {
|
|
34
|
+
/// 支払総額(円)。
|
|
35
|
+
pub gross_payment_amount: FinalAmount,
|
|
36
|
+
/// 源泉徴収税額の計算対象額(円)。
|
|
37
|
+
pub taxable_payment_amount: FinalAmount,
|
|
38
|
+
/// 源泉徴収税額(円)。
|
|
39
|
+
pub tax_amount: FinalAmount,
|
|
40
|
+
/// 支払総額から源泉徴収税額を控除した後の金額(円)。
|
|
41
|
+
pub net_payment_amount: FinalAmount,
|
|
42
|
+
/// 適用されたカテゴリ。
|
|
43
|
+
pub category: WithholdingTaxCategory,
|
|
44
|
+
/// カテゴリ表示名。
|
|
45
|
+
pub category_label: String,
|
|
46
|
+
/// 応募作品等の入選賞金・謝金の非課税特例を適用したか。
|
|
47
|
+
pub submission_prize_exempted: bool,
|
|
48
|
+
/// 適用されたフラグ。
|
|
49
|
+
pub applied_flags: HashSet<WithholdingTaxFlag>,
|
|
50
|
+
/// 計算内訳。
|
|
51
|
+
pub breakdown: Vec<WithholdingTaxStep>,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// 所得税法第204条第1項に基づく報酬・料金等の源泉徴収税額を計算する。
|
|
55
|
+
///
|
|
56
|
+
/// # 法的根拠
|
|
57
|
+
/// 所得税法 第204条第1項(報酬・料金等の源泉徴収)
|
|
58
|
+
/// 国税庁タックスアンサー No.2795 / No.2798 / No.2810
|
|
59
|
+
///
|
|
60
|
+
/// # 計算手順
|
|
61
|
+
/// 1. 請求書等で区分表示された消費税額を支払総額から控除し、課税対象額を求める
|
|
62
|
+
/// 2. カテゴリに対応する計算方式を決定する
|
|
63
|
+
/// 3. 応募作品等の入選賞金・謝金の非課税特例があれば税額を 0 円とする
|
|
64
|
+
/// 4. 100万円以下部分と超過部分にそれぞれ税率を適用し、1円未満切り捨てで合算する
|
|
65
|
+
pub fn calculate_withholding_tax(
|
|
66
|
+
ctx: &WithholdingTaxContext,
|
|
67
|
+
params: &WithholdingTaxParams,
|
|
68
|
+
) -> Result<WithholdingTaxResult, JLawError> {
|
|
69
|
+
ctx.target_date.validate()?;
|
|
70
|
+
|
|
71
|
+
let taxable_payment_amount = ctx
|
|
72
|
+
.payment_amount
|
|
73
|
+
.checked_sub(ctx.separated_consumption_tax_amount)
|
|
74
|
+
.ok_or_else(|| InputError::InvalidWithholdingInput {
|
|
75
|
+
field: "separated_consumption_tax_amount".into(),
|
|
76
|
+
reason: "消費税額が支払総額を超えています".into(),
|
|
77
|
+
})?;
|
|
78
|
+
|
|
79
|
+
let category_params = params
|
|
80
|
+
.categories
|
|
81
|
+
.iter()
|
|
82
|
+
.find(|category| category.category == ctx.category)
|
|
83
|
+
.ok_or_else(|| CalculationError::PolicyNotApplicable {
|
|
84
|
+
reason: format!(
|
|
85
|
+
"カテゴリ {} に対応する源泉徴収パラメータが見つかりません",
|
|
86
|
+
ctx.category
|
|
87
|
+
),
|
|
88
|
+
})?;
|
|
89
|
+
|
|
90
|
+
let exempted = ctx.policy.should_apply_submission_prize_exemption(
|
|
91
|
+
ctx.category,
|
|
92
|
+
taxable_payment_amount,
|
|
93
|
+
&ctx.flags,
|
|
94
|
+
category_params.submission_prize_exemption_threshold,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if exempted || taxable_payment_amount == 0 {
|
|
98
|
+
return Ok(WithholdingTaxResult {
|
|
99
|
+
gross_payment_amount: FinalAmount::new(ctx.payment_amount),
|
|
100
|
+
taxable_payment_amount: FinalAmount::new(taxable_payment_amount),
|
|
101
|
+
tax_amount: FinalAmount::new(0),
|
|
102
|
+
net_payment_amount: FinalAmount::new(ctx.payment_amount),
|
|
103
|
+
category: ctx.category,
|
|
104
|
+
category_label: category_params.label.clone(),
|
|
105
|
+
submission_prize_exempted: exempted,
|
|
106
|
+
applied_flags: ctx.flags.clone(),
|
|
107
|
+
breakdown: vec![],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let (tax_amount, breakdown) = match &category_params.method {
|
|
112
|
+
WithholdingTaxMethod::TwoTier {
|
|
113
|
+
threshold,
|
|
114
|
+
base_rate_numer,
|
|
115
|
+
base_rate_denom,
|
|
116
|
+
excess_rate_numer,
|
|
117
|
+
excess_rate_denom,
|
|
118
|
+
} => calculate_two_tier(
|
|
119
|
+
taxable_payment_amount,
|
|
120
|
+
*threshold,
|
|
121
|
+
*base_rate_numer,
|
|
122
|
+
*base_rate_denom,
|
|
123
|
+
*excess_rate_numer,
|
|
124
|
+
*excess_rate_denom,
|
|
125
|
+
ctx.policy.as_ref(),
|
|
126
|
+
)?,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
let net_payment_amount = ctx
|
|
130
|
+
.payment_amount
|
|
131
|
+
.checked_sub(tax_amount.as_yen())
|
|
132
|
+
.ok_or_else(|| CalculationError::Overflow {
|
|
133
|
+
step: "net_payment_amount".into(),
|
|
134
|
+
})?;
|
|
135
|
+
|
|
136
|
+
Ok(WithholdingTaxResult {
|
|
137
|
+
gross_payment_amount: FinalAmount::new(ctx.payment_amount),
|
|
138
|
+
taxable_payment_amount: FinalAmount::new(taxable_payment_amount),
|
|
139
|
+
tax_amount,
|
|
140
|
+
net_payment_amount: FinalAmount::new(net_payment_amount),
|
|
141
|
+
category: ctx.category,
|
|
142
|
+
category_label: category_params.label.clone(),
|
|
143
|
+
submission_prize_exempted: false,
|
|
144
|
+
applied_flags: ctx.flags.clone(),
|
|
145
|
+
breakdown,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fn calculate_two_tier(
|
|
150
|
+
amount: u64,
|
|
151
|
+
threshold: u64,
|
|
152
|
+
base_rate_numer: u64,
|
|
153
|
+
base_rate_denom: u64,
|
|
154
|
+
excess_rate_numer: u64,
|
|
155
|
+
excess_rate_denom: u64,
|
|
156
|
+
policy: &dyn WithholdingTaxPolicy,
|
|
157
|
+
) -> Result<(FinalAmount, Vec<WithholdingTaxStep>), JLawError> {
|
|
158
|
+
let base_amount = amount.min(threshold);
|
|
159
|
+
let excess_amount = amount.saturating_sub(threshold);
|
|
160
|
+
|
|
161
|
+
let mut breakdown = Vec::with_capacity(if excess_amount > 0 { 2 } else { 1 });
|
|
162
|
+
let base_tax = apply_rate(base_amount, base_rate_numer, base_rate_denom, policy)?;
|
|
163
|
+
breakdown.push(WithholdingTaxStep {
|
|
164
|
+
label: format!("{}円以下の部分", threshold),
|
|
165
|
+
base_amount,
|
|
166
|
+
rate_numer: base_rate_numer,
|
|
167
|
+
rate_denom: base_rate_denom,
|
|
168
|
+
result: base_tax,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
let mut total_tax = base_tax.as_yen();
|
|
172
|
+
|
|
173
|
+
if excess_amount > 0 {
|
|
174
|
+
let excess_tax = apply_rate(excess_amount, excess_rate_numer, excess_rate_denom, policy)?;
|
|
175
|
+
total_tax = total_tax.checked_add(excess_tax.as_yen()).ok_or_else(|| {
|
|
176
|
+
CalculationError::Overflow {
|
|
177
|
+
step: "withholding_tax_total".into(),
|
|
178
|
+
}
|
|
179
|
+
})?;
|
|
180
|
+
breakdown.push(WithholdingTaxStep {
|
|
181
|
+
label: format!("{}円超の部分", threshold),
|
|
182
|
+
base_amount: excess_amount,
|
|
183
|
+
rate_numer: excess_rate_numer,
|
|
184
|
+
rate_denom: excess_rate_denom,
|
|
185
|
+
result: excess_tax,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
Ok((FinalAmount::new(total_tax), breakdown))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fn apply_rate(
|
|
193
|
+
amount: u64,
|
|
194
|
+
rate_numer: u64,
|
|
195
|
+
rate_denom: u64,
|
|
196
|
+
policy: &dyn WithholdingTaxPolicy,
|
|
197
|
+
) -> Result<FinalAmount, JLawError> {
|
|
198
|
+
let rate = Rate::new(rate_numer, rate_denom).map_err(|_| CalculationError::Overflow {
|
|
199
|
+
step: "withholding_tax_rate".into(),
|
|
200
|
+
})?;
|
|
201
|
+
let rounding = policy.tax_rounding();
|
|
202
|
+
Ok(rate
|
|
203
|
+
.apply(
|
|
204
|
+
&IntermediateAmount::from_exact(amount),
|
|
205
|
+
MultiplyOrder::MultiplyFirst,
|
|
206
|
+
rounding,
|
|
207
|
+
)?
|
|
208
|
+
.finalize(rounding)?)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#[cfg(test)]
|
|
212
|
+
#[allow(clippy::disallowed_methods)]
|
|
213
|
+
mod tests {
|
|
214
|
+
use super::*;
|
|
215
|
+
use crate::domains::withholding_tax::params::WithholdingTaxCategoryParams;
|
|
216
|
+
use crate::domains::withholding_tax::policy::StandardWithholdingTaxPolicy;
|
|
217
|
+
|
|
218
|
+
#[test]
|
|
219
|
+
fn zero_amount_returns_zero() {
|
|
220
|
+
let params = WithholdingTaxParams {
|
|
221
|
+
categories: vec![WithholdingTaxCategoryParams {
|
|
222
|
+
category: WithholdingTaxCategory::ProfessionalFee,
|
|
223
|
+
label: "税理士等の報酬".into(),
|
|
224
|
+
method: WithholdingTaxMethod::TwoTier {
|
|
225
|
+
threshold: 1_000_000,
|
|
226
|
+
base_rate_numer: 1021,
|
|
227
|
+
base_rate_denom: 10_000,
|
|
228
|
+
excess_rate_numer: 2042,
|
|
229
|
+
excess_rate_denom: 10_000,
|
|
230
|
+
},
|
|
231
|
+
submission_prize_exemption_threshold: None,
|
|
232
|
+
}],
|
|
233
|
+
};
|
|
234
|
+
let ctx = WithholdingTaxContext {
|
|
235
|
+
payment_amount: 0,
|
|
236
|
+
separated_consumption_tax_amount: 0,
|
|
237
|
+
category: WithholdingTaxCategory::ProfessionalFee,
|
|
238
|
+
target_date: crate::LegalDate::new(2026, 1, 1),
|
|
239
|
+
flags: HashSet::new(),
|
|
240
|
+
policy: Box::new(StandardWithholdingTaxPolicy),
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
let result = calculate_withholding_tax(&ctx, ¶ms).unwrap();
|
|
244
|
+
assert_eq!(result.tax_amount.as_yen(), 0);
|
|
245
|
+
assert_eq!(result.net_payment_amount.as_yen(), 0);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
use std::fmt;
|
|
3
|
+
use std::str::FromStr;
|
|
4
|
+
|
|
5
|
+
use crate::domains::withholding_tax::policy::WithholdingTaxPolicy;
|
|
6
|
+
use crate::error::InputError;
|
|
7
|
+
use crate::types::date::LegalDate;
|
|
8
|
+
|
|
9
|
+
/// 報酬・料金等の源泉徴収カテゴリ。
|
|
10
|
+
///
|
|
11
|
+
/// # 法的根拠
|
|
12
|
+
/// 所得税法 第204条第1項
|
|
13
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
14
|
+
pub enum WithholdingTaxCategory {
|
|
15
|
+
/// 原稿料・講演料等。
|
|
16
|
+
///
|
|
17
|
+
/// # 法的根拠
|
|
18
|
+
/// 所得税法 第204条第1項第1号
|
|
19
|
+
ManuscriptAndLecture,
|
|
20
|
+
/// 弁護士・税理士・公認会計士等の報酬・料金。
|
|
21
|
+
///
|
|
22
|
+
/// # 法的根拠
|
|
23
|
+
/// 所得税法 第204条第1項第2号
|
|
24
|
+
ProfessionalFee,
|
|
25
|
+
/// 役務の提供等を約することにより一時に支払う契約金。
|
|
26
|
+
///
|
|
27
|
+
/// # 法的根拠
|
|
28
|
+
/// 所得税法 第204条第1項第7号
|
|
29
|
+
ExclusiveContractFee,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl WithholdingTaxCategory {
|
|
33
|
+
/// 永続化・バインディング用のコード文字列。
|
|
34
|
+
pub fn as_str(self) -> &'static str {
|
|
35
|
+
match self {
|
|
36
|
+
Self::ManuscriptAndLecture => "manuscript_and_lecture",
|
|
37
|
+
Self::ProfessionalFee => "professional_fee",
|
|
38
|
+
Self::ExclusiveContractFee => "exclusive_contract_fee",
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// C ABI 用の整数コード。
|
|
43
|
+
pub fn ffi_code(self) -> u32 {
|
|
44
|
+
match self {
|
|
45
|
+
Self::ManuscriptAndLecture => 1,
|
|
46
|
+
Self::ProfessionalFee => 2,
|
|
47
|
+
Self::ExclusiveContractFee => 3,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// C ABI から整数コードを復元する。
|
|
52
|
+
pub fn from_ffi_code(code: u32) -> Result<Self, InputError> {
|
|
53
|
+
match code {
|
|
54
|
+
1 => Ok(Self::ManuscriptAndLecture),
|
|
55
|
+
2 => Ok(Self::ProfessionalFee),
|
|
56
|
+
3 => Ok(Self::ExclusiveContractFee),
|
|
57
|
+
_ => Err(InputError::InvalidWithholdingInput {
|
|
58
|
+
field: "category".into(),
|
|
59
|
+
reason: format!("未知のカテゴリコードです: {code}"),
|
|
60
|
+
}),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
impl fmt::Display for WithholdingTaxCategory {
|
|
66
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
67
|
+
f.write_str(self.as_str())
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
impl From<WithholdingTaxCategory> for u32 {
|
|
72
|
+
fn from(value: WithholdingTaxCategory) -> Self {
|
|
73
|
+
value.ffi_code()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl FromStr for WithholdingTaxCategory {
|
|
78
|
+
type Err = InputError;
|
|
79
|
+
|
|
80
|
+
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
81
|
+
match s {
|
|
82
|
+
"manuscript_and_lecture" => Ok(Self::ManuscriptAndLecture),
|
|
83
|
+
"professional_fee" => Ok(Self::ProfessionalFee),
|
|
84
|
+
"exclusive_contract_fee" => Ok(Self::ExclusiveContractFee),
|
|
85
|
+
_ => Err(InputError::InvalidWithholdingInput {
|
|
86
|
+
field: "category".into(),
|
|
87
|
+
reason: format!("未知のカテゴリです: {s}"),
|
|
88
|
+
}),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// 源泉徴収税額の計算に影響するフラグ。
|
|
94
|
+
///
|
|
95
|
+
/// # 法的根拠
|
|
96
|
+
/// 所得税法 第204条第1項第1号
|
|
97
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
98
|
+
pub enum WithholdingTaxFlag {
|
|
99
|
+
/// 応募作品等の入選者に支払う賞金・謝金として扱う。
|
|
100
|
+
///
|
|
101
|
+
/// 原稿料・講演料等のうち、1回の支払額が50,000円以下である場合は
|
|
102
|
+
/// 源泉徴収不要となる。
|
|
103
|
+
///
|
|
104
|
+
/// WARNING: このフラグの事実認定はライブラリの責任範囲外です。
|
|
105
|
+
/// 呼び出し元が正しく判断した上で指定してください。
|
|
106
|
+
IsSubmissionPrize,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// 報酬・料金等の源泉徴収税額計算コンテキスト。
|
|
110
|
+
///
|
|
111
|
+
/// # 法的根拠
|
|
112
|
+
/// 所得税法 第204条第1項
|
|
113
|
+
pub struct WithholdingTaxContext {
|
|
114
|
+
/// 実際に支払う総額(円)。
|
|
115
|
+
///
|
|
116
|
+
/// 請求書等で消費税額が区分表示されている場合でも、
|
|
117
|
+
/// ここには支払総額を指定する。
|
|
118
|
+
pub payment_amount: u64,
|
|
119
|
+
/// 請求書等で区分表示された消費税額(円)。
|
|
120
|
+
///
|
|
121
|
+
/// 0 の場合は区分表示なしとして扱う。
|
|
122
|
+
pub separated_consumption_tax_amount: u64,
|
|
123
|
+
/// 報酬・料金等のカテゴリ。
|
|
124
|
+
pub category: WithholdingTaxCategory,
|
|
125
|
+
/// 計算対象日。
|
|
126
|
+
pub target_date: LegalDate,
|
|
127
|
+
/// 適用フラグ。
|
|
128
|
+
pub flags: HashSet<WithholdingTaxFlag>,
|
|
129
|
+
/// 端数処理・特例判定ポリシー。
|
|
130
|
+
pub policy: Box<dyn WithholdingTaxPolicy>,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[cfg(test)]
|
|
134
|
+
#[allow(clippy::disallowed_methods)]
|
|
135
|
+
mod tests {
|
|
136
|
+
use super::*;
|
|
137
|
+
use crate::domains::withholding_tax::policy::StandardWithholdingTaxPolicy;
|
|
138
|
+
|
|
139
|
+
#[test]
|
|
140
|
+
fn category_string_roundtrip() {
|
|
141
|
+
let category = WithholdingTaxCategory::from_str("professional_fee").unwrap();
|
|
142
|
+
assert_eq!(category, WithholdingTaxCategory::ProfessionalFee);
|
|
143
|
+
assert_eq!(category.as_str(), "professional_fee");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[test]
|
|
147
|
+
fn category_ffi_roundtrip() {
|
|
148
|
+
let category = WithholdingTaxCategory::from_ffi_code(3).unwrap();
|
|
149
|
+
assert_eq!(category, WithholdingTaxCategory::ExclusiveContractFee);
|
|
150
|
+
assert_eq!(category.ffi_code(), 3);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#[test]
|
|
154
|
+
fn context_construction() {
|
|
155
|
+
let ctx = WithholdingTaxContext {
|
|
156
|
+
payment_amount: 100_000,
|
|
157
|
+
separated_consumption_tax_amount: 10_000,
|
|
158
|
+
category: WithholdingTaxCategory::ManuscriptAndLecture,
|
|
159
|
+
target_date: LegalDate::new(2026, 1, 1),
|
|
160
|
+
flags: HashSet::new(),
|
|
161
|
+
policy: Box::new(StandardWithholdingTaxPolicy),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
assert_eq!(ctx.payment_amount, 100_000);
|
|
165
|
+
assert_eq!(ctx.separated_consumption_tax_amount, 10_000);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -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_withholding_tax, WithholdingTaxResult, WithholdingTaxStep};
|
|
7
|
+
pub use context::{WithholdingTaxCategory, WithholdingTaxContext, WithholdingTaxFlag};
|
|
8
|
+
pub use params::{WithholdingTaxCategoryParams, WithholdingTaxMethod, WithholdingTaxParams};
|
|
9
|
+
pub use policy::StandardWithholdingTaxPolicy;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
use crate::domains::withholding_tax::context::WithholdingTaxCategory;
|
|
2
|
+
|
|
3
|
+
/// 源泉徴収カテゴリごとの計算方式。
|
|
4
|
+
///
|
|
5
|
+
/// # 法的根拠
|
|
6
|
+
/// 所得税法 第204条第1項
|
|
7
|
+
#[derive(Debug, Clone)]
|
|
8
|
+
pub enum WithholdingTaxMethod {
|
|
9
|
+
/// 100万円以下部分と超過部分で税率が異なる二段階計算。
|
|
10
|
+
///
|
|
11
|
+
/// 令和8年(2026年)3月11日時点の国税庁資料では
|
|
12
|
+
/// 10.21% / 20.42% の二段階税率で運用されている。
|
|
13
|
+
TwoTier {
|
|
14
|
+
threshold: u64,
|
|
15
|
+
base_rate_numer: u64,
|
|
16
|
+
base_rate_denom: u64,
|
|
17
|
+
excess_rate_numer: u64,
|
|
18
|
+
excess_rate_denom: u64,
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// 源泉徴収カテゴリ1件分のパラメータ。
|
|
23
|
+
///
|
|
24
|
+
/// # 法的根拠
|
|
25
|
+
/// 所得税法 第204条第1項
|
|
26
|
+
#[derive(Debug, Clone)]
|
|
27
|
+
pub struct WithholdingTaxCategoryParams {
|
|
28
|
+
/// 対象カテゴリ。
|
|
29
|
+
pub category: WithholdingTaxCategory,
|
|
30
|
+
/// 表示用ラベル。
|
|
31
|
+
pub label: String,
|
|
32
|
+
/// 税額計算方式。
|
|
33
|
+
pub method: WithholdingTaxMethod,
|
|
34
|
+
/// 応募作品等の入選賞金・謝金の非課税しきい値。
|
|
35
|
+
///
|
|
36
|
+
/// 該当しないカテゴリでは `None`。
|
|
37
|
+
pub submission_prize_exemption_threshold: Option<u64>,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// 源泉徴収税額計算に使うパラメータセット。
|
|
41
|
+
///
|
|
42
|
+
/// `j-law-registry` が JSON からロードしてこの型に変換する。
|
|
43
|
+
///
|
|
44
|
+
/// # 法的根拠
|
|
45
|
+
/// 所得税法 第204条第1項
|
|
46
|
+
#[derive(Debug, Clone)]
|
|
47
|
+
pub struct WithholdingTaxParams {
|
|
48
|
+
/// カテゴリごとの計算パラメータ。
|
|
49
|
+
pub categories: Vec<WithholdingTaxCategoryParams>,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[cfg(test)]
|
|
53
|
+
#[allow(clippy::disallowed_methods)]
|
|
54
|
+
mod tests {
|
|
55
|
+
use super::*;
|
|
56
|
+
|
|
57
|
+
#[test]
|
|
58
|
+
fn params_construction() {
|
|
59
|
+
let params = WithholdingTaxParams {
|
|
60
|
+
categories: vec![WithholdingTaxCategoryParams {
|
|
61
|
+
category: WithholdingTaxCategory::ManuscriptAndLecture,
|
|
62
|
+
label: "原稿料・講演料等".into(),
|
|
63
|
+
method: WithholdingTaxMethod::TwoTier {
|
|
64
|
+
threshold: 1_000_000,
|
|
65
|
+
base_rate_numer: 1021,
|
|
66
|
+
base_rate_denom: 10_000,
|
|
67
|
+
excess_rate_numer: 2042,
|
|
68
|
+
excess_rate_denom: 10_000,
|
|
69
|
+
},
|
|
70
|
+
submission_prize_exemption_threshold: Some(50_000),
|
|
71
|
+
}],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
assert_eq!(params.categories.len(), 1);
|
|
75
|
+
assert_eq!(
|
|
76
|
+
params.categories[0].submission_prize_exemption_threshold,
|
|
77
|
+
Some(50_000)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
|
|
3
|
+
use crate::domains::withholding_tax::context::{WithholdingTaxCategory, WithholdingTaxFlag};
|
|
4
|
+
use crate::types::rounding::RoundingStrategy;
|
|
5
|
+
|
|
6
|
+
/// 源泉徴収税額計算のポリシーインターフェース。
|
|
7
|
+
///
|
|
8
|
+
/// 通常は [`StandardWithholdingTaxPolicy`] を使う。
|
|
9
|
+
pub trait WithholdingTaxPolicy: std::fmt::Debug {
|
|
10
|
+
/// 税額計算に使う端数処理戦略。
|
|
11
|
+
fn tax_rounding(&self) -> RoundingStrategy;
|
|
12
|
+
|
|
13
|
+
/// 応募作品等の入選賞金・謝金に対する非課税特例を適用するか判定する。
|
|
14
|
+
fn should_apply_submission_prize_exemption(
|
|
15
|
+
&self,
|
|
16
|
+
category: WithholdingTaxCategory,
|
|
17
|
+
taxable_payment_amount: u64,
|
|
18
|
+
flags: &HashSet<WithholdingTaxFlag>,
|
|
19
|
+
exemption_threshold: Option<u64>,
|
|
20
|
+
) -> bool;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// 国税庁の標準解釈に基づく源泉徴収税額計算ポリシー。
|
|
24
|
+
///
|
|
25
|
+
/// # 法的根拠
|
|
26
|
+
/// 所得税法 第204条第1項
|
|
27
|
+
/// 東日本大震災からの復興のための施策を実施するために必要な財源の確保に関する特別措置法
|
|
28
|
+
#[derive(Debug, Clone, Copy)]
|
|
29
|
+
pub struct StandardWithholdingTaxPolicy;
|
|
30
|
+
|
|
31
|
+
impl WithholdingTaxPolicy for StandardWithholdingTaxPolicy {
|
|
32
|
+
fn tax_rounding(&self) -> RoundingStrategy {
|
|
33
|
+
RoundingStrategy::Floor
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn should_apply_submission_prize_exemption(
|
|
37
|
+
&self,
|
|
38
|
+
category: WithholdingTaxCategory,
|
|
39
|
+
taxable_payment_amount: u64,
|
|
40
|
+
flags: &HashSet<WithholdingTaxFlag>,
|
|
41
|
+
exemption_threshold: Option<u64>,
|
|
42
|
+
) -> bool {
|
|
43
|
+
category == WithholdingTaxCategory::ManuscriptAndLecture
|
|
44
|
+
&& flags.contains(&WithholdingTaxFlag::IsSubmissionPrize)
|
|
45
|
+
&& exemption_threshold
|
|
46
|
+
.map(|threshold| taxable_payment_amount <= threshold)
|
|
47
|
+
.unwrap_or(false)
|
|
48
|
+
}
|
|
49
|
+
}
|