税法カテゴリ判定の一元化(中間判定レイヤー)
問題: 生の閾値を各所に散在させると法改正に脆い
障害者控除(配偶者分)の適用判定を例に考える。
悪い実装: 生の閾値で直接判定する
// 障害者控除の判定
if (spouse.TotalIncome <= 580_000 && spouse.IsDisabled) {
// 障害者控除を適用
}
// 配偶者控除の判定
if (spouse.TotalIncome <= 580_000 && taxpayer.TotalIncome <= 10_000_000) {
// 配偶者控除を適用
}
// 源泉控除対象配偶者の判定
if (spouse.TotalIncome <= 950_000 && taxpayer.TotalIncome <= 9_000_000) {
// 扶養人数+1
}この実装では 580_000 という閾値が複数箇所に現れる。 令和8年に閾値が62万円に変更されると、すべての箇所を修正しなければならない。
良い実装: 税法カテゴリ判定を一元化する
// 1. 年度パラメータからカテゴリ判定関数群を生成(閾値を内部に閉じ込める)
var judge = CreateCategoryJudgment(taxYearParams);
// 2. 控除判定レイヤー: カテゴリの該当有無のみを参照
// 閾値も年齢境界も引数に現れない。person が持つ所得・生年月日から内部で判定。
if (judge.Is同一生計配偶者(spouse) && spouse.IsDisabled) {
// 障害者控除を適用
}
if (judge.Is控除対象配偶者(spouse, taxpayer)) {
// 配偶者控除を適用
}閾値 T が変更されても、修正箇所は年度パラメータの定義 1箇所のみ。 呼び出し側は閾値の存在すら知らない。
原則: 控除判定は税法カテゴリを経由する
事実(Fact) カテゴリ判定 控除判定
────────────── ────────────── ──────────────
配偶者の所得 → 同一生計配偶者? → 障害者控除(配偶者分)
本人の所得 → 控除対象配偶者? → 配偶者控除
配偶者の年齢 → 配偶者特別控除対象配偶者? → 配偶者特別控除
障害者区分 → 源泉控除対象配偶者? → 扶養人数カウント
→ 扶養親族? → 扶養控除
親族の所得 → 特定親族? → 特定親族特別控除
親族の年齢 → ... → ...重要: 各控除の判定ロジックは、生の所得額や年齢を直接参照するのではなく、 必ず 税法カテゴリの該当有無 を参照する。閾値や年齢境界は中間判定レイヤーに集約する。
カテゴリ判定関数の一覧
第1層: 親族区分判定関数
税法カテゴリの判定には、まず親族関係の基本属性を判定する関数群が必要である。 これらは 親族関係 の事実データ(続柄、養子縁組の有無等)から導出する。 → 各属性の定義は 親族の基礎概念 参照
| 判定関数 | 入力 | 判定内容 |
|---|---|---|
| Is血族 | 親族関係 | 血縁関係にある者(養子縁組による法定血族を含む) |
| Is姻族 | 親族関係 | 配偶者の血族、または血族の配偶者 |
| Is直系 | 親族関係 | 親子関係の連鎖で直接つながる関係(親、子、祖父母、孫等) |
| Is傍系 | 親族関係 | 共通の祖先を経由してつながる関係(兄弟姉妹、伯叔父母等) |
| Is尊属 | 親族関係 | 本人より上の世代の者(父母、祖父母、伯叔父母等) |
| Is卑属 | 親族関係 | 本人より下の世代の者(子、孫、甥姪等) |
| 親等数 | 親族関係 | 親族間の世代距離(血族: 1〜6、姻族: 1〜3) |
これらを組み合わせた判定:
| 組み合わせ判定 | 定義 | 使用箇所 |
|---|---|---|
| Is直系尊属 | Is直系 ∧ Is尊属 | 同居老親等の判定 |
| Is直系卑属1親等(= 子) | Is直系 ∧ Is卑属 ∧ 親等数=1 | ひとり親控除の「子」の判定 |
| Is親族範囲内 | (Is血族 ∧ 親等数≤6) ∨ (Is姻族 ∧ 親等数≤3) ∨ Is配偶者 | 扶養親族の前提判定 |
重要: これらの判定は親族関係の事実データから機械的に導出されるものであり、 閾値や年度パラメータには依存しない(民法の定義は年度で変わらない)。 ただし、税法カテゴリの判定においてどの親族区分を参照するか は税法の規定に依存する。
第2層: 税法カテゴリ判定関数
第1層の親族区分判定 + person が持つ事実(所得・生年月日等)から、税法上のカテゴリ該当有無を判定する。
インターフェース設計原則: 判定関数の引数には person(事実の集合体)のみを渡す。 所得額・年齢といった生の値は引数に含めない。閾値・年齢境界は年度パラメータとして関数内部に閉じ込める。 → 詳細は後述「判定関数のインターフェース設計」参照
| カテゴリ | 内部で参照する事実 | 内部の判定条件(年度パラメータ含む) |
|---|---|---|
| 同一生計配偶者 | 配偶者の合計所得金額 | 配偶者の合計所得金額 ≤ T + 生計一 + 専従者除外 |
| 控除対象配偶者 | + 本人の合計所得金額 | 同一生計配偶者 + 本人の合計所得金額 ≤ 1,000万円 |
| 配偶者特別控除対象配偶者 ※ | 配偶者の合計所得金額, 本人の合計所得金額 | 配偶者の合計所得金額 T超〜133万円 + 本人の合計所得金額 ≤ 1,000万円 + 生計一 + 専従者除外 |
| 源泉控除対象配偶者 | 配偶者の合計所得金額, 本人の合計所得金額 | 配偶者の合計所得金額 ≤ 95万円 + 本人の合計所得金額 ≤ 900万円 + ... |
| 控除対象外配偶者 ※ | 全カテゴリの判定結果 | 上記いずれにも該当しない + 障害者でない |
| 扶養親族 | 親族の合計所得金額 | Is親族範囲内 + 親族の合計所得金額 ≤ T + 生計一 + 専従者除外 + 配偶者でない |
| 控除対象扶養親族 | + 親族の生年月日 | 扶養親族 + 年齢要件(内部) |
| 特定扶養親族 | ↑ | 控除対象扶養親族 + 年齢要件(内部) |
| 老人扶養親族 | ↑ | 控除対象扶養親族 + 年齢要件(内部) |
| 同居老親等 | + 同居関係 | 老人扶養親族 + Is直系尊属 + 所得者又は配偶者と同居 |
| 特定親族 | 親族の所得, 生年月日 | 年齢要件(内部)+ 所得T超〜123万円 + 生計一 |
| 同居特別障害者 | 障害者区分, 同居関係 | 特別障害者 + (同一生計配偶者 or 扶養親族) + 所得者・配偶者・生計一の他の親族と同居 |
注意: 「同居」の定義が異なる:
- 同居老親等: 所得者又はその配偶者との同居(狭い)— 対象が直系尊属(所得者/配偶者の親・祖父母)に限定されているため、同居先も自然に狭い
- 同居特別障害者: 所得者、その配偶者、又は生計を一にするその他の親族との同居(広い)
- → 「同居しているか?」の単一フラグではなく「誰と同居しているか」を事実として記録する必要がある
- → 詳細は 障害者控除 参照
※ = TASHIKA 便宜カテゴリ(NTA公式用語ではない)。→ 配偶者のカテゴリ 参照
これらの関数は「閾値の単一定義場所」であり、法改正時に修正する唯一の箇所。 各控除判定ロジックは上記カテゴリの bool 結果のみを使う。
設計上の含意
税額計算エンジン に中間判定レイヤーを明示的に設ける
CategoryJudgment層: 事実→カテゴリ該当有無を判定(閾値はここだけ)DeductionCalculation層: カテゴリ該当有無→控除額を計算(閾値を直接参照しない)
カテゴリ判定の結果を TaxCalculationResult に記録する
- 各家族について「同一生計配偶者に該当」「控除対象扶養親族(特定)に該当」等を記録
- 控除額だけでなく、なぜその控除が適用されたか のトレーサビリティ
年度パラメータの変更がカテゴリ判定関数のみに波及する
- R7→R8 で T が58万円→62万円に変わっても、控除判定ロジックは変更不要
- カテゴリ判定関数が参照するパラメータテーブルの値を更新するだけ
判定関数のインターフェース設計
原則: 引数にドメイン知識を求めない
判定関数の呼び出し側に閾値・年齢・所得額といったドメイン知識を要求してはならない。 関数名でドメイン概念を表現し、引数は person(事実の集合体)のみとする。
// 悪い例: 呼び出し側が閾値と年齢を知っている必要がある
IsSpecifiedDependent(person, age: 19, maxAge: 23, incomeThreshold: 580_000)
// 良い例: ドメイン概念名だけで呼び出せる。閾値は内部に隠蔽
judge.Is特定扶養親族(person)この原則は所得と年齢の両方に適用される:
| 所得 | 年齢 | |
|---|---|---|
| 悪い例 | 所得 ≤ 58万円 を呼び出し側が判定 | age ≥ 70 を呼び出し側が判定 |
| 良い例 | Is同一生計配偶者(person) | Is老人扶養親族(person) |
| 隠蔽されるもの | 58万円という閾値 | 70歳という年齢境界 |
年齢は独立した概念として公開しない
所得の場合、同一生計配偶者 のように独立したドメイン概念が税法で定義されている。 年齢にも 年少・老人 のような確立された用語がある一方、すべての年齢境界に名前があるわけではない。
名前のない年齢境界(例: 所得金額調整控除の23歳、非居住者扶養控除の30歳)は、 それを使うルール固有の実装詳細であり、独立した述語として公開すべきではない。
| 年齢境界 | 使用箇所 | 公開? | 理由 |
|---|---|---|---|
| 16歳, 19歳, 23歳, 70歳 | 扶養控除の区分(年少/特定/老人) | — | 税法カテゴリ(Is特定扶養親族等)として公開。年齢単体では公開しない |
| 23歳 | 所得金額調整控除の要件 | ✗ | Is所得金額調整控除適用の内部実装 |
| 30歳 | 非居住者の扶養控除要件 | ✗ | Is非居住者控除対象扶養親族の内部実装 |
判断基準: 年齢境界をそのまま関数名にしたくなったら、それはドメイン概念ではなく実装詳細。 Is23歳未満 や Is30歳未満 は閾値をそのまま名前にしているだけで、ドメイン知識を隠蔽できていない。
複合要件を持つ判定関数の例: 所得金額調整控除
所得金額調整控除(子ども等)の適用判定は、年齢・障害・所得が混在する複合要件である。
Is所得金額調整控除適用(taxpayer, familyMembers):
= taxpayer.給与収入 > params.IncomeAdjustmentSalaryFloor // 850万円
AND (
Is特別障害者(taxpayer) // 要件1: 本人が特別障害者
OR familyMembers.Any(m =>
Is扶養親族(m) AND 内部で年齢判定(m, params)) // 要件2: 23歳未満の扶養親族
OR Is特別障害者(同一生計配偶者) // 要件3: 配偶者が特別障害者
OR familyMembers.Any(m =>
Is扶養親族(m) AND Is特別障害者(m)) // 要件4: 扶養親族が特別障害者
)呼び出し側は Is所得金額調整控除適用(taxpayer, family) だけを使う。 850万円・23歳・特別障害者の定義、4つの要件パスの存在、いずれも内部に隠蔽される。
関数型パターンによる段階的判定
カリー化: 確定した情報から段階的に関数を特殊化する
税法カテゴリの判定に必要な情報は、同時に確定するわけではない。 先に確定する情報を部分適用し、残りの情報が確定した時点で判定を完了する。
情報の確定タイミング:
第1段階: 年度パラメータ(年度開始時に確定。年度中は不変)
- 所得閾値T、年齢境界、控除額テーブル等
第2段階: 安定的な事実(家族登録時に確定。年度中ほぼ不変)
- 生年月日、続柄、障害者区分、居住/非居住
第3段階: 変動する事実(申告時に確定。見積りが変わりうる)
- 所得の見積額カリー化により、各段階で部分適用された判定関数を生成する:
// 第1段階: 年度パラメータを適用 → 当年度用の判定関数群を生成
var judge = CreateCategoryJudgment(taxYearParams)
// 第2段階: 安定的な事実を適用 → 人物固有の判定関数を生成
// 生年月日から年齢を内部で計算。続柄から親族区分を内部で判定。
var memberJudge = judge.ForMember(member.BirthDate, member.Relationship, ...)
// 第3段階: 変動する事実を適用 → 最終判定
var result = memberJudge.Evaluate(member.EstimatedIncome)
// result: { Is扶養親族: true, Is特定扶養親族: true, Is老人扶養親族: false, ... }なぜカリー化が有効か
年度パラメータの一括切替
CreateCategoryJudgment(r7Params)とCreateCategoryJudgment(r8Params)を差し替えるだけで年度切替- テスト時に任意の年度パラメータを注入できる
所得見積り変更時の効率的な再判定
- 所得が変わるたびに第1段階・第2段階をやり直す必要がない
- 第3段階だけを再実行すれば全カテゴリが再判定される
純粋関数の保証
- 各段階の関数は副作用を持たない
- 同一入力→同一出力(参照透過性)。Property tests で検証可能
- Finalized 後の不変性(DM-501)とも整合する
合成可能性
- カテゴリ判定の結果を複数の控除計算関数に渡せる
- 判定結果はイミュータブルなレコードとして扱い、控除計算間で共有する
// カテゴリ判定結果を一度だけ計算し、複数の控除計算で共有
var categories = judge.EvaluateAll(taxpayer, familyMembers)
var 扶養控除額 = Calculate扶養控除(categories)
var 所得金額調整控除額 = Calculate所得金額調整控除(categories, taxpayer.給与収入)
var 障害者控除額 = Calculate障害者控除(categories)
// → いずれの計算関数も、生の所得額・年齢・閾値を直接参照しない