Skip to content

税法カテゴリ判定の一元化(中間判定レイヤー)

問題: 生の閾値を各所に散在させると法改正に脆い

障害者控除(配偶者分)の適用判定を例に考える。

悪い実装: 生の閾値で直接判定する

// 障害者控除の判定
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 結果のみを使う。

設計上の含意

  1. 税額計算エンジン に中間判定レイヤーを明示的に設ける

    • CategoryJudgment 層: 事実→カテゴリ該当有無を判定(閾値はここだけ)
    • DeductionCalculation 層: カテゴリ該当有無→控除額を計算(閾値を直接参照しない)
  2. カテゴリ判定の結果を TaxCalculationResult に記録する

    • 各家族について「同一生計配偶者に該当」「控除対象扶養親族(特定)に該当」等を記録
    • 控除額だけでなく、なぜその控除が適用されたか のトレーサビリティ
  3. 年度パラメータの変更がカテゴリ判定関数のみに波及する

    • 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, ... }

なぜカリー化が有効か

  1. 年度パラメータの一括切替

    • CreateCategoryJudgment(r7Params)CreateCategoryJudgment(r8Params) を差し替えるだけで年度切替
    • テスト時に任意の年度パラメータを注入できる
  2. 所得見積り変更時の効率的な再判定

    • 所得が変わるたびに第1段階・第2段階をやり直す必要がない
    • 第3段階だけを再実行すれば全カテゴリが再判定される
  3. 純粋関数の保証

    • 各段階の関数は副作用を持たない
    • 同一入力→同一出力(参照透過性)。Property tests で検証可能
    • Finalized 後の不変性(DM-501)とも整合する
  4. 合成可能性

    • カテゴリ判定の結果を複数の控除計算関数に渡せる
    • 判定結果はイミュータブルなレコードとして扱い、控除計算間で共有する
// カテゴリ判定結果を一度だけ計算し、複数の控除計算で共有
var categories = judge.EvaluateAll(taxpayer, familyMembers)

var 扶養控除額 = Calculate扶養控除(categories)
var 所得金額調整控除額 = Calculate所得金額調整控除(categories, taxpayer.給与収入)
var 障害者控除額 = Calculate障害者控除(categories)
// → いずれの計算関数も、生の所得額・年齢・閾値を直接参照しない