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,197 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
|
|
3
|
+
use crate::domains::consumption_tax::calculator::calculate_consumption_tax;
|
|
4
|
+
use crate::domains::consumption_tax::context::ConsumptionTaxContext;
|
|
5
|
+
use crate::domains::consumption_tax::policy::StandardConsumptionTaxPolicy;
|
|
6
|
+
use crate::domains::real_estate::context::{RealEstateContext, RealEstateFlag};
|
|
7
|
+
use crate::domains::real_estate::params::BrokerageFeeParams;
|
|
8
|
+
use crate::error::{CalculationError, 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 BrokerageFeeStep {
|
|
15
|
+
pub label: String,
|
|
16
|
+
pub base_amount: u64,
|
|
17
|
+
pub rate_numer: u64,
|
|
18
|
+
pub rate_denom: u64,
|
|
19
|
+
pub result: FinalAmount,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// 媒介報酬の計算結果。
|
|
23
|
+
#[derive(Debug, Clone)]
|
|
24
|
+
pub struct BrokerageFeeResult {
|
|
25
|
+
/// 税込合計額。
|
|
26
|
+
pub total_with_tax: FinalAmount,
|
|
27
|
+
/// 税抜合計額。
|
|
28
|
+
pub total_without_tax: FinalAmount,
|
|
29
|
+
/// 消費税額。
|
|
30
|
+
pub tax_amount: FinalAmount,
|
|
31
|
+
/// 各ティアの計算内訳。
|
|
32
|
+
pub breakdown: Vec<BrokerageFeeStep>,
|
|
33
|
+
/// 適用されたフラグ。
|
|
34
|
+
pub applied_flags: HashSet<RealEstateFlag>,
|
|
35
|
+
/// 低廉な空き家特例が適用されたか。
|
|
36
|
+
pub low_cost_special_applied: bool,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// 宅建業法第46条に基づく媒介報酬を計算する。
|
|
40
|
+
///
|
|
41
|
+
/// # 法的根拠
|
|
42
|
+
/// 宅地建物取引業法 第46条第1項
|
|
43
|
+
/// 国土交通省告示(2024年7月1日施行 / 2019年10月1日施行)
|
|
44
|
+
///
|
|
45
|
+
/// # 計算手順
|
|
46
|
+
/// 1. 各ティアの対象金額を求め、個別に切り捨てる
|
|
47
|
+
/// 2. 各ティアの結果を合算して税抜き合計を得る
|
|
48
|
+
/// 3. 低廉な空き家特例が適用される場合、通常計算が保証額を下回るなら保証額まで引き上げる
|
|
49
|
+
/// (NOTE: `.max()` による最低保証であり、`.min()` による上限キャップではない)
|
|
50
|
+
/// 4. 消費税ドメイン(消費税法第29条)に処理を委譲して税額・税込額を得る
|
|
51
|
+
pub fn calculate_brokerage_fee(
|
|
52
|
+
ctx: &RealEstateContext,
|
|
53
|
+
params: &BrokerageFeeParams,
|
|
54
|
+
) -> Result<BrokerageFeeResult, JLawError> {
|
|
55
|
+
ctx.target_date.validate()?;
|
|
56
|
+
|
|
57
|
+
let price = ctx.price;
|
|
58
|
+
let tier_rounding = ctx.policy.tier_rounding();
|
|
59
|
+
|
|
60
|
+
// --- ティア計算 ---
|
|
61
|
+
let mut breakdown: Vec<BrokerageFeeStep> = Vec::new();
|
|
62
|
+
let mut subtotal = 0u64;
|
|
63
|
+
|
|
64
|
+
for tier in ¶ms.tiers {
|
|
65
|
+
let tier_base = compute_tier_base(price, tier.price_from, tier.price_to_inclusive);
|
|
66
|
+
if tier_base == 0 {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let rate = Rate::new(tier.rate_numer, tier.rate_denom).map_err(|_| {
|
|
71
|
+
CalculationError::Overflow {
|
|
72
|
+
step: tier.label.clone(),
|
|
73
|
+
}
|
|
74
|
+
})?;
|
|
75
|
+
let amount = IntermediateAmount::from_exact(tier_base);
|
|
76
|
+
let tier_result = rate.apply(&amount, MultiplyOrder::MultiplyFirst, tier_rounding)?;
|
|
77
|
+
let tier_final = tier_result.finalize(tier_rounding)?;
|
|
78
|
+
|
|
79
|
+
subtotal = subtotal.checked_add(tier_final.as_yen()).ok_or_else(|| {
|
|
80
|
+
CalculationError::Overflow {
|
|
81
|
+
step: tier.label.clone(),
|
|
82
|
+
}
|
|
83
|
+
})?;
|
|
84
|
+
|
|
85
|
+
breakdown.push(BrokerageFeeStep {
|
|
86
|
+
label: tier.label.clone(),
|
|
87
|
+
base_amount: tier_base,
|
|
88
|
+
rate_numer: tier.rate_numer,
|
|
89
|
+
rate_denom: tier.rate_denom,
|
|
90
|
+
result: tier_final,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- 低廉な空き家特例 ---
|
|
95
|
+
// 2018年1月1日施行(平成29年国土交通省告示第98号)。
|
|
96
|
+
// 400万円以下(2024年7月以降は800万円以下)の低廉な空き家等については、
|
|
97
|
+
// 通常計算が保証額を下回る場合でも保証額まで請求できる(最低保証額)。
|
|
98
|
+
//
|
|
99
|
+
// 適用条件(3つ全て満たす場合に適用):
|
|
100
|
+
// 1. 売買価格が params.low_cost_special.price_ceiling_inclusive 以下
|
|
101
|
+
// 2. IsLowCostVacantHouse フラグが指定されている(ポリシーに委譲)
|
|
102
|
+
// 3. seller_only == true の場合は IsSeller フラグも必要(2018〜2024年の制約)
|
|
103
|
+
let mut low_cost_applied = false;
|
|
104
|
+
if let Some(special) = ¶ms.low_cost_special {
|
|
105
|
+
let price_ok = price <= special.price_ceiling_inclusive;
|
|
106
|
+
let flag_ok = ctx.policy.should_apply_low_cost_special(&ctx.flags);
|
|
107
|
+
// seller_only の場合は IsSeller フラグが必須
|
|
108
|
+
let party_ok = !special.seller_only || ctx.flags.contains(&RealEstateFlag::IsSeller);
|
|
109
|
+
if price_ok && flag_ok && party_ok {
|
|
110
|
+
// fee_ceiling_exclusive_tax は法令上の「上限報酬額」だが、
|
|
111
|
+
// 特例適用時は「最低保証額」として機能する(通常計算が下回れば引き上げ)。
|
|
112
|
+
subtotal = subtotal.max(special.fee_ceiling_exclusive_tax);
|
|
113
|
+
low_cost_applied = true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let total_without_tax = FinalAmount::new(subtotal);
|
|
118
|
+
|
|
119
|
+
// --- 消費税(消費税ドメインに委譲)---
|
|
120
|
+
let tax_ctx = ConsumptionTaxContext {
|
|
121
|
+
amount: subtotal,
|
|
122
|
+
target_date: ctx.target_date,
|
|
123
|
+
flags: HashSet::new(), // 不動産仲介報酬は標準税率のみ適用
|
|
124
|
+
policy: Box::new(StandardConsumptionTaxPolicy),
|
|
125
|
+
};
|
|
126
|
+
let tax_result = calculate_consumption_tax(&tax_ctx, ¶ms.consumption_tax)?;
|
|
127
|
+
|
|
128
|
+
Ok(BrokerageFeeResult {
|
|
129
|
+
total_with_tax: tax_result.amount_with_tax,
|
|
130
|
+
total_without_tax,
|
|
131
|
+
tax_amount: tax_result.tax_amount,
|
|
132
|
+
breakdown,
|
|
133
|
+
applied_flags: ctx.flags.clone(),
|
|
134
|
+
low_cost_special_applied: low_cost_applied,
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// ティアに対応する課税対象金額(price のうちこのティア範囲に収まる部分)を返す。
|
|
139
|
+
pub(crate) fn compute_tier_base(price: u64, from: u64, to_inclusive: Option<u64>) -> u64 {
|
|
140
|
+
if price < from {
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
let capped = match to_inclusive {
|
|
144
|
+
Some(ceiling) => price.min(ceiling),
|
|
145
|
+
None => price,
|
|
146
|
+
};
|
|
147
|
+
if from == 0 {
|
|
148
|
+
capped
|
|
149
|
+
} else {
|
|
150
|
+
// `price_from` は前ティア上限の翌円を表す(例: tier2 は 2_000_001 から始まる)。
|
|
151
|
+
// そのため、このティアの課税対象金額は `capped - (from - 1)` で求まる。
|
|
152
|
+
capped - (from - 1)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[cfg(test)]
|
|
157
|
+
mod tests {
|
|
158
|
+
use super::*;
|
|
159
|
+
|
|
160
|
+
#[test]
|
|
161
|
+
fn tier_base_tier1_under() {
|
|
162
|
+
assert_eq!(compute_tier_base(1_000_000, 0, Some(2_000_000)), 1_000_000);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#[test]
|
|
166
|
+
fn tier_base_tier1_at_boundary() {
|
|
167
|
+
assert_eq!(compute_tier_base(2_000_000, 0, Some(2_000_000)), 2_000_000);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#[test]
|
|
171
|
+
fn tier_base_tier1_over() {
|
|
172
|
+
assert_eq!(compute_tier_base(5_000_000, 0, Some(2_000_000)), 2_000_000);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[test]
|
|
176
|
+
fn tier_base_tier2_base() {
|
|
177
|
+
// 5,000,000円 の tier2(from=2,000,001, to=4,000,000)
|
|
178
|
+
// capped = min(5M, 4M) = 4M
|
|
179
|
+
// base = 4M - (2_000_001 - 1) = 4M - 2M = 2,000,000
|
|
180
|
+
assert_eq!(
|
|
181
|
+
compute_tier_base(5_000_000, 2_000_001, Some(4_000_000)),
|
|
182
|
+
2_000_000
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#[test]
|
|
187
|
+
fn tier_base_price_below_from() {
|
|
188
|
+
assert_eq!(compute_tier_base(1_000_000, 2_000_001, Some(4_000_000)), 0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[test]
|
|
192
|
+
fn tier_base_no_ceiling() {
|
|
193
|
+
// 5,000,000円 の tier3(from=4,000,001, 上限なし)
|
|
194
|
+
// base = 5M - (4_000_001 - 1) = 5M - 4M = 1,000,000
|
|
195
|
+
assert_eq!(compute_tier_base(5_000_000, 4_000_001, None), 1_000_000);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
use crate::domains::real_estate::policy::RealEstatePolicy;
|
|
2
|
+
use crate::types::date::LegalDate;
|
|
3
|
+
use std::collections::HashSet;
|
|
4
|
+
use std::fmt;
|
|
5
|
+
|
|
6
|
+
/// 不動産取引計算に関わる法的フラグ。
|
|
7
|
+
///
|
|
8
|
+
/// WARNING: 各フラグの事実認定はライブラリの責任範囲外です。
|
|
9
|
+
/// 呼び出し元が正しく判断した上で指定してください。
|
|
10
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
11
|
+
pub enum RealEstateFlag {
|
|
12
|
+
/// 低廉な空き家特例を申請する場合に指定する。
|
|
13
|
+
///
|
|
14
|
+
/// 適用要件: 宅地建物取引業法 第46条 / 国土交通省告示(2018年1月1日施行・2024年7月1日改正)
|
|
15
|
+
/// WARNING: 対象物件が「低廉な空き家等」に該当するかの事実認定は呼び出し元の責任。
|
|
16
|
+
IsLowCostVacantHouse,
|
|
17
|
+
/// 売主側として報酬を計算する場合に指定する。
|
|
18
|
+
///
|
|
19
|
+
/// 2018年1月1日〜2024年6月30日の低廉特例は売主のみに適用される。
|
|
20
|
+
/// このフラグが指定されない場合(買主側)、当該期間の特例は適用されない。
|
|
21
|
+
IsSeller,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// 媒介報酬計算の入力コンテキスト。
|
|
25
|
+
///
|
|
26
|
+
/// # 法的根拠
|
|
27
|
+
/// 宅地建物取引業法 第46条第1項
|
|
28
|
+
pub struct RealEstateContext {
|
|
29
|
+
/// 売買価格(円)。
|
|
30
|
+
pub price: u64,
|
|
31
|
+
/// 契約日・適用する告示を選択するための基準日。
|
|
32
|
+
pub target_date: LegalDate,
|
|
33
|
+
/// 適用する法的フラグの集合。
|
|
34
|
+
pub flags: HashSet<RealEstateFlag>,
|
|
35
|
+
/// 計算ポリシー(テスト・カスタム計算での差し替えを可能にする)。
|
|
36
|
+
pub policy: Box<dyn RealEstatePolicy>,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
impl fmt::Debug for RealEstateContext {
|
|
40
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
41
|
+
f.debug_struct("RealEstateContext")
|
|
42
|
+
.field("price", &self.price)
|
|
43
|
+
.field("target_date", &self.target_date)
|
|
44
|
+
.field("flags", &self.flags)
|
|
45
|
+
.field("policy", &"<policy>")
|
|
46
|
+
.finish()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -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_brokerage_fee, BrokerageFeeResult, BrokerageFeeStep};
|
|
7
|
+
pub use context::{RealEstateContext, RealEstateFlag};
|
|
8
|
+
pub use params::{BrokerageFeeParams, LowCostSpecialParams, TierParam};
|
|
9
|
+
pub use policy::StandardMlitPolicy;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
use crate::domains::consumption_tax::params::ConsumptionTaxParams;
|
|
2
|
+
|
|
3
|
+
/// 1ティアの計算パラメータ。
|
|
4
|
+
#[derive(Debug, Clone)]
|
|
5
|
+
pub struct TierParam {
|
|
6
|
+
pub label: String,
|
|
7
|
+
pub price_from: u64,
|
|
8
|
+
/// `None` は上限なし。
|
|
9
|
+
pub price_to_inclusive: Option<u64>,
|
|
10
|
+
pub rate_numer: u64,
|
|
11
|
+
pub rate_denom: u64,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// 低廉な空き家特例パラメータ。
|
|
15
|
+
#[derive(Debug, Clone)]
|
|
16
|
+
pub struct LowCostSpecialParams {
|
|
17
|
+
/// 特例対象となる売買価格の上限(この価格以下の場合に特例が適用される)。
|
|
18
|
+
pub price_ceiling_inclusive: u64,
|
|
19
|
+
/// 法令が定める報酬額の上限(税抜・円)。
|
|
20
|
+
///
|
|
21
|
+
/// NOTE: フィールド名は法令上の「上限報酬額(ceiling)」を表すが、
|
|
22
|
+
/// 計算ロジックでは「最低保証額(floor)」として機能する。
|
|
23
|
+
/// 通常計算結果がこの値を下回る場合、この値まで引き上げられる。
|
|
24
|
+
/// 参照: `calculator::calculate_brokerage_fee` のコメント。
|
|
25
|
+
pub fee_ceiling_exclusive_tax: u64,
|
|
26
|
+
/// `true` の場合、売主側の取引にのみ特例が適用される。
|
|
27
|
+
///
|
|
28
|
+
/// 2018年1月1日〜2024年6月30日の特例は売主のみ対象(宅建業法改正告示・平成29年国土交通省告示第98号)。
|
|
29
|
+
/// `false` の場合、売主・買主双方に適用される(2024年7月1日施行以降)。
|
|
30
|
+
pub seller_only: bool,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// 媒介報酬計算に使うパラメータセット。
|
|
34
|
+
///
|
|
35
|
+
/// `j-law-registry` がJSONからロードしてこの型に変換する。
|
|
36
|
+
/// `j-law-core` の計算ロジックはこの型のみに依存する。
|
|
37
|
+
#[derive(Debug, Clone)]
|
|
38
|
+
pub struct BrokerageFeeParams {
|
|
39
|
+
pub tiers: Vec<TierParam>,
|
|
40
|
+
/// 消費税パラメータ(消費税ドメインに処理を委譲する)。
|
|
41
|
+
pub consumption_tax: ConsumptionTaxParams,
|
|
42
|
+
pub low_cost_special: Option<LowCostSpecialParams>,
|
|
43
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
use crate::domains::real_estate::context::RealEstateFlag;
|
|
2
|
+
use crate::types::rounding::RoundingStrategy;
|
|
3
|
+
use std::collections::HashSet;
|
|
4
|
+
|
|
5
|
+
/// 媒介報酬計算のポリシーインターフェース。
|
|
6
|
+
///
|
|
7
|
+
/// 端数処理戦略や特例適用の判定ロジックを差し替えられるようにする。
|
|
8
|
+
/// 通常は [`StandardMlitPolicy`] を使う。
|
|
9
|
+
///
|
|
10
|
+
/// # 設計上の注意
|
|
11
|
+
/// 価格の閾値チェック(`price_ceiling_inclusive`)は、
|
|
12
|
+
/// パラメータレジストリが持つ値を使うため `calculator` 側で行う。
|
|
13
|
+
/// このメソッドはフラグベースの判定のみを担う。
|
|
14
|
+
pub trait RealEstatePolicy: std::fmt::Debug {
|
|
15
|
+
/// 低廉な空き家特例をフラグに基づいて適用するかどうかを判定する。
|
|
16
|
+
///
|
|
17
|
+
/// 価格の閾値チェックは呼び出し元(`calculator`)がパラメータを用いて行う。
|
|
18
|
+
fn should_apply_low_cost_special(&self, flags: &HashSet<RealEstateFlag>) -> bool;
|
|
19
|
+
|
|
20
|
+
/// 各ティアの計算に使う端数処理戦略。
|
|
21
|
+
fn tier_rounding(&self) -> RoundingStrategy;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// 国土交通省の標準解釈に基づく媒介報酬計算ポリシー。
|
|
25
|
+
///
|
|
26
|
+
/// # 法的根拠
|
|
27
|
+
/// 宅地建物取引業法 第46条第1項
|
|
28
|
+
/// 国土交通省告示(2018年1月1日施行・2024年7月1日改正)
|
|
29
|
+
#[derive(Debug, Clone, Copy)]
|
|
30
|
+
pub struct StandardMlitPolicy;
|
|
31
|
+
|
|
32
|
+
impl RealEstatePolicy for StandardMlitPolicy {
|
|
33
|
+
fn should_apply_low_cost_special(&self, flags: &HashSet<RealEstateFlag>) -> bool {
|
|
34
|
+
flags.contains(&RealEstateFlag::IsLowCostVacantHouse)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn tier_rounding(&self) -> RoundingStrategy {
|
|
38
|
+
RoundingStrategy::Floor
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
use crate::domains::stamp_tax::context::{StampTaxContext, StampTaxFlag};
|
|
2
|
+
use crate::domains::stamp_tax::params::{
|
|
3
|
+
StampTaxAmountUsage, StampTaxBracket, StampTaxChargeMode, StampTaxDocumentParams,
|
|
4
|
+
StampTaxParams, StampTaxSpecialRule,
|
|
5
|
+
};
|
|
6
|
+
use crate::error::{CalculationError, InputError, JLawError};
|
|
7
|
+
use crate::types::amount::FinalAmount;
|
|
8
|
+
|
|
9
|
+
/// 印紙税の計算内訳。
|
|
10
|
+
#[derive(Debug, Clone)]
|
|
11
|
+
pub struct StampTaxBreakdownStep {
|
|
12
|
+
pub rule_code: String,
|
|
13
|
+
pub label: String,
|
|
14
|
+
pub tax_amount: FinalAmount,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// 印紙税の計算結果。
|
|
18
|
+
///
|
|
19
|
+
/// # 法的根拠
|
|
20
|
+
/// 印紙税法 別表第一 / 租税特別措置法 第91条
|
|
21
|
+
#[derive(Debug, Clone)]
|
|
22
|
+
pub struct StampTaxResult {
|
|
23
|
+
pub tax_amount: FinalAmount,
|
|
24
|
+
pub rule_label: String,
|
|
25
|
+
pub applied_special_rule: Option<String>,
|
|
26
|
+
pub breakdown: Vec<StampTaxBreakdownStep>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
struct ResolvedTaxRule {
|
|
30
|
+
tax_amount: u64,
|
|
31
|
+
rule_label: String,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// 印紙税法 別表第一に基づく印紙税額を計算する。
|
|
35
|
+
pub fn calculate_stamp_tax(
|
|
36
|
+
ctx: &StampTaxContext,
|
|
37
|
+
params: &StampTaxParams,
|
|
38
|
+
) -> Result<StampTaxResult, JLawError> {
|
|
39
|
+
ctx.target_date.validate()?;
|
|
40
|
+
|
|
41
|
+
let document = params.document_params(ctx.document_code).ok_or_else(|| {
|
|
42
|
+
InputError::InvalidStampTaxInput {
|
|
43
|
+
field: "document_code".into(),
|
|
44
|
+
reason: format!(
|
|
45
|
+
"対象文書コードのパラメータが存在しません: {}",
|
|
46
|
+
ctx.document_code
|
|
47
|
+
),
|
|
48
|
+
}
|
|
49
|
+
})?;
|
|
50
|
+
|
|
51
|
+
validate_context(ctx, document)?;
|
|
52
|
+
|
|
53
|
+
let special_rule = ctx.policy.select_special_rule(ctx, document);
|
|
54
|
+
let (resolved, applied_special_rule, breakdown_rule_code) = match special_rule {
|
|
55
|
+
Some(rule) => (
|
|
56
|
+
resolve_special_rule(ctx, document, rule)?,
|
|
57
|
+
Some(rule.code.clone()),
|
|
58
|
+
rule.code.clone(),
|
|
59
|
+
),
|
|
60
|
+
None => (resolve_base_rule(ctx, document)?, None, "base".into()),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
Ok(StampTaxResult {
|
|
64
|
+
tax_amount: FinalAmount::new(resolved.tax_amount),
|
|
65
|
+
rule_label: resolved.rule_label.clone(),
|
|
66
|
+
applied_special_rule,
|
|
67
|
+
breakdown: vec![StampTaxBreakdownStep {
|
|
68
|
+
rule_code: breakdown_rule_code,
|
|
69
|
+
label: resolved.rule_label,
|
|
70
|
+
tax_amount: FinalAmount::new(resolved.tax_amount),
|
|
71
|
+
}],
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn validate_context(
|
|
76
|
+
ctx: &StampTaxContext,
|
|
77
|
+
document: &StampTaxDocumentParams,
|
|
78
|
+
) -> Result<(), JLawError> {
|
|
79
|
+
match document.amount_usage {
|
|
80
|
+
StampTaxAmountUsage::Required if ctx.stated_amount.is_none() => {
|
|
81
|
+
return Err(InputError::InvalidStampTaxInput {
|
|
82
|
+
field: "stated_amount".into(),
|
|
83
|
+
reason: format!("{} には記載金額の指定が必要です", ctx.document_code),
|
|
84
|
+
}
|
|
85
|
+
.into());
|
|
86
|
+
}
|
|
87
|
+
StampTaxAmountUsage::Unsupported if ctx.stated_amount.is_some() => {
|
|
88
|
+
return Err(InputError::InvalidStampTaxInput {
|
|
89
|
+
field: "stated_amount".into(),
|
|
90
|
+
reason: format!("{} では記載金額を指定できません", ctx.document_code),
|
|
91
|
+
}
|
|
92
|
+
.into());
|
|
93
|
+
}
|
|
94
|
+
_ => {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for flag in &ctx.flags {
|
|
98
|
+
validate_flag_for_document(*flag, ctx.document_code, ctx.stated_amount)?;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Ok(())
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn validate_flag_for_document(
|
|
105
|
+
flag: StampTaxFlag,
|
|
106
|
+
document_code: crate::domains::stamp_tax::context::StampTaxDocumentCode,
|
|
107
|
+
stated_amount: Option<u64>,
|
|
108
|
+
) -> Result<(), JLawError> {
|
|
109
|
+
if !flag.allowed_document_codes().contains(&document_code) {
|
|
110
|
+
return Err(InputError::InvalidStampTaxInput {
|
|
111
|
+
field: "flags".into(),
|
|
112
|
+
reason: format!("{flag} は {document_code} には指定できません"),
|
|
113
|
+
}
|
|
114
|
+
.into());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if flag.requires_stated_amount() && stated_amount.is_none() {
|
|
118
|
+
return Err(InputError::InvalidStampTaxInput {
|
|
119
|
+
field: "stated_amount".into(),
|
|
120
|
+
reason: format!("{flag} を指定する場合は記載金額が必要です"),
|
|
121
|
+
}
|
|
122
|
+
.into());
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
Ok(())
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn resolve_special_rule(
|
|
129
|
+
ctx: &StampTaxContext,
|
|
130
|
+
document: &StampTaxDocumentParams,
|
|
131
|
+
rule: &StampTaxSpecialRule,
|
|
132
|
+
) -> Result<ResolvedTaxRule, JLawError> {
|
|
133
|
+
if let Some(tax_amount) = rule.tax_amount {
|
|
134
|
+
let label = rule
|
|
135
|
+
.rule_label
|
|
136
|
+
.as_ref()
|
|
137
|
+
.cloned()
|
|
138
|
+
.unwrap_or_else(|| rule.label.clone());
|
|
139
|
+
return Ok(ResolvedTaxRule {
|
|
140
|
+
tax_amount,
|
|
141
|
+
rule_label: label,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if !rule.brackets.is_empty() || rule.no_amount_tax_amount.is_some() {
|
|
146
|
+
return resolve_amount_table(
|
|
147
|
+
ctx.stated_amount,
|
|
148
|
+
&rule.brackets,
|
|
149
|
+
rule.no_amount_tax_amount,
|
|
150
|
+
rule.no_amount_rule_label.as_deref(),
|
|
151
|
+
document,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
Err(CalculationError::PolicyNotApplicable {
|
|
156
|
+
reason: format!("特例ルールの税額定義が不正です: {}", rule.code),
|
|
157
|
+
}
|
|
158
|
+
.into())
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fn resolve_base_rule(
|
|
162
|
+
ctx: &StampTaxContext,
|
|
163
|
+
document: &StampTaxDocumentParams,
|
|
164
|
+
) -> Result<ResolvedTaxRule, JLawError> {
|
|
165
|
+
match document.charge_mode {
|
|
166
|
+
StampTaxChargeMode::AmountBrackets => resolve_amount_table(
|
|
167
|
+
ctx.stated_amount,
|
|
168
|
+
&document.brackets,
|
|
169
|
+
document.no_amount_tax_amount,
|
|
170
|
+
document.no_amount_rule_label.as_deref(),
|
|
171
|
+
document,
|
|
172
|
+
),
|
|
173
|
+
StampTaxChargeMode::FixedPerDocument | StampTaxChargeMode::FixedPerYear => {
|
|
174
|
+
let tax_amount =
|
|
175
|
+
document
|
|
176
|
+
.base_tax_amount
|
|
177
|
+
.ok_or_else(|| CalculationError::PolicyNotApplicable {
|
|
178
|
+
reason: format!("固定税額が未設定です: {}", document.code),
|
|
179
|
+
})?;
|
|
180
|
+
Ok(ResolvedTaxRule {
|
|
181
|
+
tax_amount,
|
|
182
|
+
rule_label: document.base_rule_label.clone(),
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fn resolve_amount_table(
|
|
189
|
+
stated_amount: Option<u64>,
|
|
190
|
+
brackets: &[StampTaxBracket],
|
|
191
|
+
no_amount_tax_amount: Option<u64>,
|
|
192
|
+
no_amount_rule_label: Option<&str>,
|
|
193
|
+
document: &StampTaxDocumentParams,
|
|
194
|
+
) -> Result<ResolvedTaxRule, JLawError> {
|
|
195
|
+
match stated_amount {
|
|
196
|
+
Some(amount) => {
|
|
197
|
+
let bracket = brackets
|
|
198
|
+
.iter()
|
|
199
|
+
.find(|bracket| bracket.matches(amount))
|
|
200
|
+
.ok_or_else(|| CalculationError::PolicyNotApplicable {
|
|
201
|
+
reason: format!(
|
|
202
|
+
"{} の記載金額 {}円 に対応する税額区分が見つかりません",
|
|
203
|
+
document.code, amount
|
|
204
|
+
),
|
|
205
|
+
})?;
|
|
206
|
+
Ok(ResolvedTaxRule {
|
|
207
|
+
tax_amount: bracket.tax_amount,
|
|
208
|
+
rule_label: bracket.label.clone(),
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
None => {
|
|
212
|
+
let tax_amount =
|
|
213
|
+
no_amount_tax_amount.ok_or_else(|| InputError::InvalidStampTaxInput {
|
|
214
|
+
field: "stated_amount".into(),
|
|
215
|
+
reason: format!("{} には記載金額の指定が必要です", document.code),
|
|
216
|
+
})?;
|
|
217
|
+
let label = no_amount_rule_label
|
|
218
|
+
.unwrap_or("記載金額の記載のないもの")
|
|
219
|
+
.to_string();
|
|
220
|
+
Ok(ResolvedTaxRule {
|
|
221
|
+
tax_amount,
|
|
222
|
+
rule_label: label,
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#[cfg(test)]
|
|
229
|
+
#[allow(clippy::disallowed_methods)]
|
|
230
|
+
mod tests {
|
|
231
|
+
use std::collections::{BTreeMap, HashSet};
|
|
232
|
+
|
|
233
|
+
use super::*;
|
|
234
|
+
use crate::domains::stamp_tax::context::StampTaxDocumentCode;
|
|
235
|
+
use crate::domains::stamp_tax::params::{StampTaxCitation, StampTaxSpecialRule};
|
|
236
|
+
use crate::domains::stamp_tax::policy::StandardNtaPolicy;
|
|
237
|
+
use crate::types::date::LegalDate;
|
|
238
|
+
|
|
239
|
+
fn amount_document() -> StampTaxDocumentParams {
|
|
240
|
+
StampTaxDocumentParams {
|
|
241
|
+
code: StampTaxDocumentCode::Article17SalesReceipt,
|
|
242
|
+
label: "売上代金受取書".into(),
|
|
243
|
+
citation: StampTaxCitation {
|
|
244
|
+
law_name: "印紙税法".into(),
|
|
245
|
+
article: "別表第一 第17号文書".into(),
|
|
246
|
+
},
|
|
247
|
+
charge_mode: StampTaxChargeMode::AmountBrackets,
|
|
248
|
+
amount_usage: StampTaxAmountUsage::Optional,
|
|
249
|
+
base_rule_label: "通常".into(),
|
|
250
|
+
base_tax_amount: None,
|
|
251
|
+
brackets: vec![StampTaxBracket {
|
|
252
|
+
label: "100万円以下のもの".into(),
|
|
253
|
+
amount_from: 50_000,
|
|
254
|
+
amount_to_inclusive: Some(1_000_000),
|
|
255
|
+
tax_amount: 200,
|
|
256
|
+
}],
|
|
257
|
+
no_amount_tax_amount: Some(200),
|
|
258
|
+
no_amount_rule_label: Some("受取金額の記載のないもの".into()),
|
|
259
|
+
special_rules: vec![StampTaxSpecialRule {
|
|
260
|
+
code: "article17_non_business_exempt".into(),
|
|
261
|
+
label: "非課税".into(),
|
|
262
|
+
priority: 1,
|
|
263
|
+
effective_from: None,
|
|
264
|
+
effective_until: None,
|
|
265
|
+
required_flags: vec![StampTaxFlag::Article17NonBusinessExempt],
|
|
266
|
+
tax_amount: Some(0),
|
|
267
|
+
rule_label: Some("営業に関しないもの".into()),
|
|
268
|
+
brackets: vec![],
|
|
269
|
+
no_amount_tax_amount: None,
|
|
270
|
+
no_amount_rule_label: None,
|
|
271
|
+
}],
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[test]
|
|
276
|
+
fn calculates_base_amount_rule() {
|
|
277
|
+
let mut documents = BTreeMap::new();
|
|
278
|
+
documents.insert(
|
|
279
|
+
StampTaxDocumentCode::Article17SalesReceipt,
|
|
280
|
+
amount_document(),
|
|
281
|
+
);
|
|
282
|
+
let params = StampTaxParams { documents };
|
|
283
|
+
let ctx = StampTaxContext {
|
|
284
|
+
document_code: StampTaxDocumentCode::Article17SalesReceipt,
|
|
285
|
+
stated_amount: Some(60_000),
|
|
286
|
+
target_date: LegalDate::new(2024, 8, 1),
|
|
287
|
+
flags: HashSet::new(),
|
|
288
|
+
policy: Box::new(StandardNtaPolicy),
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
let result = calculate_stamp_tax(&ctx, ¶ms).unwrap();
|
|
292
|
+
assert_eq!(result.tax_amount.as_yen(), 200);
|
|
293
|
+
assert_eq!(result.rule_label, "100万円以下のもの");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#[test]
|
|
297
|
+
fn applies_special_rule() {
|
|
298
|
+
let mut documents = BTreeMap::new();
|
|
299
|
+
documents.insert(
|
|
300
|
+
StampTaxDocumentCode::Article17SalesReceipt,
|
|
301
|
+
amount_document(),
|
|
302
|
+
);
|
|
303
|
+
let params = StampTaxParams { documents };
|
|
304
|
+
let mut flags = HashSet::new();
|
|
305
|
+
flags.insert(StampTaxFlag::Article17NonBusinessExempt);
|
|
306
|
+
let ctx = StampTaxContext {
|
|
307
|
+
document_code: StampTaxDocumentCode::Article17SalesReceipt,
|
|
308
|
+
stated_amount: Some(60_000),
|
|
309
|
+
target_date: LegalDate::new(2024, 8, 1),
|
|
310
|
+
flags,
|
|
311
|
+
policy: Box::new(StandardNtaPolicy),
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
let result = calculate_stamp_tax(&ctx, ¶ms).unwrap();
|
|
315
|
+
assert_eq!(result.tax_amount.as_yen(), 0);
|
|
316
|
+
assert_eq!(
|
|
317
|
+
result.applied_special_rule.as_deref(),
|
|
318
|
+
Some("article17_non_business_exempt")
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|