// tax-system-webapp — 固有ロジック（共通coreへ委譲される per-system handle）
import { join } from "node:path";

// ── 給与所得の自動算出（令和2年分以降の給与所得控除＋所得金額調整控除）──
//   0100269: 給与収入金額及び所得金額調整控除により給与所得を自動算出できること。
function computeSalaryIncome({ salaryRevenue, applyAdjustment = false }) {
  const rev = Number(salaryRevenue) || 0;
  let deduction;
  if (rev <= 1625000) deduction = 550000;
  else if (rev <= 1800000) deduction = rev * 0.4 - 100000;
  else if (rev <= 3600000) deduction = rev * 0.3 + 80000;
  else if (rev <= 6600000) deduction = rev * 0.2 + 440000;
  else if (rev <= 8500000) deduction = rev * 0.1 + 1100000;
  else deduction = 1950000;
  deduction = Math.floor(deduction);
  // 所得金額調整控除（子育て・特別障害者等のいる世帯で収入850万円超）
  const adjustment = applyAdjustment && rev > 8500000 ? Math.floor((Math.min(rev, 10000000) - 8500000) * 0.1) : 0;
  const income = Math.max(0, rev - deduction - adjustment);
  return { salaryRevenue: rev, deduction, adjustment, salaryIncome: income };
}

// ── 公的年金等に係る雑所得の自動算出（令和2年分以降・他の所得1,000万円以下の控除表）──
//   0100270: 年金収入金額及び年金以外の合計所得により年金雑所得を自動算出できること。
function computePensionIncome({ pensionRevenue, age, otherIncome = 0 }) {
  const rev = Number(pensionRevenue) || 0;
  const senior = Number(age) >= 65;
  if (Number(otherIncome) > 10000000) {
    // 参照実装: 他の所得1,000万円超の控除縮小は未対応（標準表のみ）
    return { error: "他の所得1,000万円超の控除表は未対応（標準表のみ対応）" };
  }
  let deduction;
  if (senior && rev <= 3300000) deduction = 1100000;
  else if (!senior && rev <= 1300000) deduction = 600000;
  else if (rev <= 4100000) deduction = rev * 0.25 + 275000;
  else if (rev <= 7700000) deduction = rev * 0.15 + 685000;
  else if (rev <= 10000000) deduction = rev * 0.05 + 1455000;
  else deduction = 1955000;
  deduction = Math.floor(Math.max(deduction, senior ? 1100000 : 600000));
  const income = Math.max(0, rev - deduction);
  return { pensionRevenue: rev, senior, deduction, pensionIncome: income };
}

// ── 軽自動車税（種別割）の税額算定（標準仕様書: 車種区分別の年税額。重課・軽課を適用）──
//   税額表は地方税法（標準税率）。最初の登録から13年経過で重課（おおむね約20%増）。
const KEIJIDOSHA_TAX = {
  "原付50cc以下": 2000, "原付90cc以下": 2000, "原付125cc以下": 2400,
  "軽二輪125-250cc": 3600, "二輪250cc超": 6000, "軽三輪": 3900,
  "軽四輪乗用自家用": 10800, "軽四輪乗用営業用": 6900,
  "軽四輪貨物自家用": 5000, "軽四輪貨物営業用": 3800,
};
// 13年経過の重課後税額（乗用・貨物別の重課額）。標準税率の重課額。
const KEIJIDOSHA_HEAVY = {
  "軽四輪乗用自家用": 12900, "軽四輪乗用営業用": 8200,
  "軽四輪貨物自家用": 6000, "軽四輪貨物営業用": 4500, "軽三輪": 4600,
};
function computeKeijidoshaTax({ vehicleType, firstRegistrationYear, asOfYear }) {
  const base = KEIJIDOSHA_TAX[vehicleType];
  if (base === undefined) return { error: "unknown vehicleType", vehicleType };
  const asOf = Number(asOfYear) || new Date().getFullYear();
  const first = Number(firstRegistrationYear) || asOf;
  const yearsPassed = asOf - first;
  const heavy = yearsPassed >= 13 && KEIJIDOSHA_HEAVY[vehicleType] !== undefined;
  const tax = heavy ? KEIJIDOSHA_HEAVY[vehicleType] : base;
  return { vehicleType, baseTax: base, heavyApplied: heavy, yearsPassed, annualTax: tax };
}

// ── 延滞金の算定（地方税法附則。令和6年特例基準割合ベースの率を定数化）──
//   税額1,000円未満切捨てで計算、本税2,000円未満は不徴収、
//   納期限の翌日から1か月以内は低率(年2.4%)・以後は高率(年8.7%)、
//   算出額1,000円未満は不徴収・100円未満切捨て。
const DELINQUENT_RATES = { low: 0.024, high: 0.087 };
function computeDelinquentCharge({ amount, dueDate, paidAt, rates = DELINQUENT_RATES }) {
  const base = Math.floor((Number(amount) || 0) / 1000) * 1000;
  if (base < 2000) return { charge: 0, base, days: 0, reason: "本税2,000円未満は不徴収" };
  const due = new Date(dueDate);
  const paid = new Date(paidAt ?? Date.now());
  if (!(due < paid)) return { charge: 0, base, days: 0, reason: "納期限内" };
  const boundary = new Date(due);
  boundary.setMonth(boundary.getMonth() + 1); // 納期限の翌日から1か月を経過する日
  const DAY = 24 * 60 * 60 * 1000;
  const totalDays = Math.ceil((paid - due) / DAY);
  const lowDays = Math.min(totalDays, Math.ceil((boundary - due) / DAY));
  const highDays = Math.max(0, totalDays - lowDays);
  let charge = Math.floor(base * rates.low * lowDays / 365 + base * rates.high * highDays / 365);
  if (charge < 1000) return { charge: 0, base, days: totalDays, lowDays, highDays, reason: "1,000円未満は不徴収" };
  charge = Math.floor(charge / 100) * 100;
  return { charge, base, days: totalDays, lowDays, highDays };
}

// ── 充当の配分（過誤納額を納期限の古い順に未納調定へ配分）──
function computeAllocation(overAmount, unpaidLevies) {
  let remaining = Number(overAmount) || 0;
  const allocations = [];
  const sorted = [...unpaidLevies].sort((a, b) => String(a.dueDate ?? "").localeCompare(String(b.dueDate ?? "")));
  for (const levy of sorted) {
    if (remaining <= 0) break;
    const unpaid = (levy.amount ?? 0) - (levy.paidTotal ?? 0);
    if (unpaid <= 0) continue;
    const applied = Math.min(unpaid, remaining);
    allocations.push({ levyId: levy.id, amount: applied });
    remaining -= applied;
  }
  return { allocations, remaining };
}

// ── 個人住民税の減免額計算（標準仕様書 0100431/0100433/0100436、0120326/0120327）──
//   減免区分ごとの減免割合に応じ、所得割・均等割を市町村民税/都道府県民税別に減免。
//   減免前所得割額は端数切捨て前で保持し、減免後に生じる端数は所得割で保持（100円未満切捨）。
const GENMEN_KEYS = ["市町村所得割", "市町村均等割", "都道府県所得割", "都道府県均等割"];
function computeJuminzeiGenmen({ amounts = {}, rate = 0, manualAmounts = null, splitMode = "期割", splitCount = 1 }) {
  const r = Math.max(0, Math.min(1, Number(rate) || 0));
  const 減免額 = {}, 減免後 = {};
  for (const k of GENMEN_KEYS) {
    const base = Number(amounts[k]) || 0;
    const g = manualAmounts && manualAmounts[k] != null ? Math.max(0, Number(manualAmounts[k]) || 0)
      : Math.floor(base * r);
    減免額[k] = g;
    let after = Math.max(0, base - g);
    // 所得割は減免後端数(100円未満)を所得割で保持（均等割は据え置き）
    減免後[k] = after;
  }
  const 減免額合計 = GENMEN_KEYS.reduce((s, k) => s + 減免額[k], 0);
  const 減免後合計 = GENMEN_KEYS.reduce((s, k) => s + 減免後[k], 0);
  // 期割/月割で按分（自動算出）
  const n = Math.max(1, Number(splitCount) || 1);
  const per = Math.floor(減免額合計 / n / 100) * 100;
  const split = [];
  for (let i = 0; i < n; i++) split.push({ 区分: splitMode, 番号: i + 1, 減免額: per + (i === 0 ? 減免額合計 - per * n : 0) });
  return { rate: r, 減免額, 減免後, 減免額合計, 減免後合計, 期月割: split,
    note: "減免区分の割合で所得割・均等割を市町村/都道府県別に減免。期割/月割で自動按分。" };
}
// ── ふるさと納税（寄附金税額控除）の全額控除上限額の算定（標準仕様書 0100319）──
//   特例控除分の上限＝所得割額×20%。全額控除上限＝所得割額×0.2 ÷ (0.9−所得税限界税率×1.021) ＋ 2000円。
function computeFurusatoUpper({ 所得割額 = 0, 所得税限界税率 = 0.10 }) {
  const shotokuwari = Math.max(0, Number(所得割額) || 0);
  const t = Math.max(0, Math.min(0.45, Number(所得税限界税率) || 0));
  const denom = 0.9 - t * 1.021;
  const upper = denom > 0 ? Math.floor(shotokuwari * 0.2 / denom) + 2000 : 2000;
  return { 所得割額: shotokuwari, 所得税限界税率: t,
    特例控除上限: Math.floor(shotokuwari * 0.2), 全額控除上限額: upper,
    note: "全額控除となる寄附金上限＝所得割額×20%÷(90%−所得税限界税率×1.021)＋2000円(自己負担)" };
}
// 税額控除を標準税率相当分と超過税率相当分に按分（0110154）。標準税率＝市町村民税6%＋道府県民税4%＝10%、
//   超過課税分は標準を超える上乗せ。控除額を標準税率と適用税率(標準＋超過)の比で按分する。
function computeDeductionSplit({ 控除額 = 0, 標準税率 = 0.10, 適用税率 } = {}) {
  const total = Math.max(0, Number(控除額) || 0);
  const std = Number(標準税率);
  const applied = 適用税率 != null ? Number(適用税率) : std;
  if (!(applied > 0) || !(std >= 0) || std > applied) return { error: "標準税率・適用税率(標準＋超過分・標準以上)を指定してください" };
  const stdPart = Math.floor(total * std / applied);
  return {
    kind: "税額控除の標準税率相当分・超過税率相当分の按分", 控除額: total, 標準税率: std, 適用税率: applied,
    標準税率相当分: stdPart, 超過税率相当分: total - stdPart,
    note: "標準税率相当分＝控除額×標準税率÷適用税率(円未満切捨)、残りを超過税率相当分とする。標準税率10%(市6%＋道府県4%)。",
  };
}

export async function handle(ctx) {
  const { req, res, pathname, searchParams, actor, send, sendRaw, parseBody, readJson, writeJson, audit,
    maskRecord, validateRecord, requireRole, hasRole, dataRoot, restrictionsFile, crypto,
    dbSelect, dbSelectOne, dbInsert, dbUpdate, dbDelete } = ctx;
  const reply = (...a) => { send(...a); return true; };
  const taxSubjectsFile = join(dataRoot, "tax-subjects.json");
  const taxAssessmentsFile = join(dataRoot, "tax-assessments.json");
  const taxLeviesFile = join(dataRoot, "tax-levies.json");
  const taxReceiptsFile = join(dataRoot, "tax-receipts.json");
  const taxOverpaymentsFile = join(dataRoot, "tax-overpayments.json");
  const taxDispositionsFile = join(dataRoot, "tax-dispositions.json");
  const taxReturnsFile = join(dataRoot, "tax-returns.json");
  const koteiAssetsFile = join(dataRoot, "tax-kotei-assets.json");
  const keiInspectionsFile = join(dataRoot, "tax-kei-inspections.json");
  const hojinFile = join(dataRoot, "tax-hojin.json");
  const shinrinExemptionsFile = join(dataRoot, "tax-shinrin-exemptions.json");
  const taxRelationsFile = join(dataRoot, "tax-relations.json");
  const kskImportsFile = join(dataRoot, "tax-ksk-imports.json");


    // ── 課税 対象者 (Tax Subjects) ────────────────────────────────────────

    if (pathname === "/api/tax-subjects" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const subjects = (await dbSelect("tax_system.tax_subject")) ?? await readJson(taxSubjectsFile, []);
      const restrictions = await readJson(restrictionsFile, []);
      return reply(res, 200, subjects.map((s) => maskRecord(actor, restrictions, s)));
    }

    if (pathname === "/api/tax-subjects" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.fullName || !body.address) return reply(res, 400, { error: "fullName, address required" });
      const subjects = (await dbSelect("tax_system.tax_subject")) ?? await readJson(taxSubjectsFile, []);
      const taxNumber = "TX-" + Date.now().toString(36).toUpperCase();
      const subject = { id: crypto.randomUUID(), taxNumber, fullName: body.fullName, address: body.address, taxType: body.taxType ?? "municipal_tax", status: "active", createdBy: actor.id, createdAt: new Date().toISOString() };
      const dbInserted = await dbInsert("tax_system.tax_subject", subject); if (dbInserted === null) { subjects.push(subject); await writeJson(taxSubjectsFile, subjects); }
      await audit(req, "tax-subject.register", { id: subject.id });
      return reply(res, 201, subject);
    }

    // ── 課税 (Tax Assessments) ────────────────────────────────────────────

    if (pathname === "/api/tax-assessments" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const assessments = (await dbSelect("tax_system.tax_assessment")) ?? await readJson(taxAssessmentsFile, []);
      const { subjectId, fiscalYear } = Object.fromEntries(new URL(req.url, "http://x").searchParams);
      let result = assessments;
      if (subjectId) result = result.filter((a) => a.subjectId === subjectId);
      if (fiscalYear) result = result.filter((a) => a.fiscalYear === fiscalYear);
      return reply(res, 200, result);
    }

    if (pathname === "/api/tax-assessments" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const body = await parseBody(req);
      if (!body.subjectId || !body.fiscalYear || body.taxableAmount === undefined) return reply(res, 400, { error: "subjectId, fiscalYear, taxableAmount required" });
      const subjects = (await dbSelect("tax_system.tax_subject")) ?? await readJson(taxSubjectsFile, []);
      if (!subjects.find((s) => s.id === body.subjectId)) return reply(res, 404, { error: "subject not found" });
      const assessments = (await dbSelect("tax_system.tax_assessment")) ?? await readJson(taxAssessmentsFile, []);
      const taxAmount = Math.floor(body.taxableAmount * 0.1);
      const assessment = { id: crypto.randomUUID(), subjectId: body.subjectId, fiscalYear: body.fiscalYear, taxableAmount: body.taxableAmount, taxAmount, paidAmount: 0, status: "assessed", assessedBy: actor.id, assessedAt: new Date().toISOString() };
      const dbInserted = await dbInsert("tax_system.tax_assessment", assessment); if (dbInserted === null) { assessments.push(assessment); await writeJson(taxAssessmentsFile, assessments); }
      await audit(req, "tax-assessment.create", { id: assessment.id, fiscalYear: body.fiscalYear, taxAmount });
      return reply(res, 201, assessment);
    }

    const taxPayMatch = pathname.match(/^\/api\/tax-assessments\/([^/]+)\/pay$/);
    if (taxPayMatch && req.method === "POST") {
      requireRole(actor, "CLERK");
      const assessment = (await dbSelectOne("tax_system.tax_assessment", taxPayMatch[1]))
        ?? (await readJson(taxAssessmentsFile, [])).find((a) => a.id === taxPayMatch[1]);
      if (!assessment) return reply(res, 404, { error: "assessment not found" });
      const body = await parseBody(req);
      const payAmount = body.amount ?? assessment.taxAmount;
      assessment.paidAmount += payAmount;
      assessment.status = assessment.paidAmount >= assessment.taxAmount ? "paid" : "partial";
      assessment.lastPaidAt = new Date().toISOString();
      const upd = await dbUpdate("tax_system.tax_assessment", assessment.id, assessment); if (upd === null) { const wl = await readJson(taxAssessmentsFile, []); const wi = wl.findIndex((x) => x.id === assessment.id); if (wi !== -1) { wl[wi] = assessment; } await writeJson(taxAssessmentsFile, wl); }
      await audit(req, "tax-assessment.pay", { id: assessment.id, amount: payAmount });
      return reply(res, 200, assessment);
    }


    const taxSubjectMatch = pathname.match(/^\/api\/tax-subjects\/([^/]+)$/);
    if (taxSubjectMatch && (req.method === "GET" || req.method === "PUT")) {
      requireRole(actor, "CLERK");
      const id = taxSubjectMatch[1];
      const subject = (await dbSelectOne("tax_system.tax_subject", id))
        ?? (await readJson(taxSubjectsFile, [])).find((s) => s.id === id);
      if (!subject) return reply(res, 404, { error: "tax subject not found" });
      if (req.method === "GET") return reply(res, 200, subject);

      const body = await parseBody(req);
      const editable = ["fullName", "address", "taxType"];
      const patch = {};
      for (const k of editable) if (body[k] !== undefined) patch[k] = body[k];
      patch.updatedAt = new Date().toISOString();
      patch.updatedBy = actor.id;
      const updated = await dbUpdate("tax_system.tax_subject", id, patch);
      let result = updated;
      if (updated === null) {
        const items = await readJson(taxSubjectsFile, []);
        const idx = items.findIndex((x) => x.id === id);
        items[idx] = { ...items[idx], ...patch };
        await writeJson(taxSubjectsFile, items);
        result = items[idx];
      }
      await audit(req, "tax-subject.update", { id, fields: Object.keys(patch).filter((k) => k !== "updatedAt" && k !== "updatedBy") });
      return reply(res, 200, result);
    }

    const taxSubjectDeleteMatch = pathname.match(/^\/api\/tax-subjects\/([^\/]+)$/);
    if (taxSubjectDeleteMatch && req.method === "DELETE") {
      requireRole(actor, "ADMIN");
      const delId = taxSubjectDeleteMatch[1];
      const deleted = await dbDelete("tax_system.tax_subject", delId);
      if (deleted === null) {
        const _items = await readJson(taxSubjectsFile, []);
        const _idx = _items.findIndex((x) => x.id === delId);
        if (_idx === -1) return reply(res, 404, { error: "tax subject not found" });
        _items.splice(_idx, 1);
        await writeJson(taxSubjectsFile, _items);
      }
      await audit(req, "tax-subject.delete", { id: delId });
      return reply(res, 204, {});
    }


    // ── 軽自動車税（種別割）の税額算定（標準仕様書準拠）──
    if (pathname === "/api/keijidosha/tax" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeKeijidoshaTax(body || {});
      if (result.error) return reply(res, 400, result);
      await audit(req, "keijidosha.tax-calc", { vehicleType: result.vehicleType, annualTax: result.annualTax });
      return reply(res, 200, result);
    }

    // ════ 収納・滞納管理（標準仕様書: 収納消込/充当・還付/督促/滞納繰越/滞納処分/徴収方法変更）════

    // ── 多項目検索（旧住所／旧事業所名(カナ・漢字・アルファベット)／支店名／郵便番号／屋号／店舗名／基礎年金番号、又はその組み合わせ）──
    if (pathname === "/api/tax/search" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[] required" });
      // テキスト項目は部分一致、番号項目は完全一致。指定項目のみ AND で絞り込み。
      const PARTIAL = ["旧住所", "旧事業所名", "支店名", "屋号", "店舗名"];
      const EXACT = ["郵便番号", "基礎年金番号"];
      const crit = {};
      for (const k of [...PARTIAL, ...EXACT]) if (b[k] !== undefined && String(b[k]).trim() !== "") crit[k] = String(b[k]).trim();
      if (!Object.keys(crit).length) return reply(res, 400, { error: "検索項目(旧住所/旧事業所名/支店名/郵便番号/屋号/店舗名/基礎年金番号)のいずれかを指定してください" });
      const norm = (s) => String(s ?? "");
      const hits = rows.filter((r) => {
        for (const k of PARTIAL) if (crit[k] && !norm(r[k]).includes(crit[k])) return false;
        for (const k of EXACT) if (crit[k] && norm(r[k]) !== crit[k]) return false;
        return true;
      });
      await audit(req, "tax.search", { criteria: Object.keys(crit), count: hits.length });
      return reply(res, 200, { criteria: crit, count: hits.length, results: hits });
    }

    // ── 調定（税目×年度×期別）。更正は revise で履歴を残して税額変更 ──
    if (pathname === "/api/tax/levies" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const levies = await readJson(taxLeviesFile, []);
      const atenaId = searchParams.get("atenaId") ?? "";
      const fy = searchParams.get("fiscalYear") ?? "";
      const unpaidOnly = searchParams.get("unpaidOnly") === "1";
      let result = levies;
      if (atenaId) result = result.filter((l) => l.atenaId === atenaId);
      if (fy) result = result.filter((l) => String(l.fiscalYear) === fy);
      if (unpaidOnly) result = result.filter((l) => (l.amount - (l.paidTotal ?? 0)) > 0 && !["執行停止", "不納欠損"].includes(l.status));
      return reply(res, 200, result);
    }
    if (pathname === "/api/tax/levies" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      for (const k of ["atenaId", "taxItem", "fiscalYear", "period", "amount", "dueDate"]) {
        if (b[k] === undefined || b[k] === "") return reply(res, 400, { error: `${k} is required` });
      }
      const levies = await readJson(taxLeviesFile, []);
      const levy = {
        id: crypto.randomUUID(), atenaId: String(b.atenaId), taxItem: String(b.taxItem),
        fiscalYear: Number(b.fiscalYear), period: Number(b.period), amount: Number(b.amount),
        method: b.method === "特別徴収" ? "特別徴収" : "普通徴収", dueDate: String(b.dueDate),
        status: "調定済", paidTotal: 0, revisions: [], createdBy: actor.id, createdAt: new Date().toISOString(),
      };
      levies.unshift(levy);
      await writeJson(taxLeviesFile, levies);
      await audit(req, "tax-levy.create", { id: levy.id, taxItem: levy.taxItem, amount: levy.amount });
      return reply(res, 201, levy);
    }
    {
      const reviseMatch = pathname.match(/^\/api\/tax\/levies\/([^/]+)\/revise$/);
      if (reviseMatch && req.method === "POST") {
        requireRole(actor, "ADMIN"); // 更正・賦課決定
        const b = await parseBody(req);
        if (b.amount === undefined) return reply(res, 400, { error: "amount is required" });
        const levies = await readJson(taxLeviesFile, []);
        const idx = levies.findIndex((l) => l.id === reviseMatch[1]);
        if (idx === -1) return reply(res, 404, { error: "levy not found" });
        const before = levies[idx].amount;
        levies[idx].revisions.push({ from: before, to: Number(b.amount), reason: b.reason ?? "", by: actor.id, at: new Date().toISOString() });
        levies[idx].amount = Number(b.amount);
        levies[idx].status = "更正済";
        // 更正減で過納が生じた場合は過誤納プールへ
        const over = (levies[idx].paidTotal ?? 0) - levies[idx].amount;
        if (over > 0) {
          const pool = await readJson(taxOverpaymentsFile, []);
          pool.unshift({ id: crypto.randomUUID(), atenaId: levies[idx].atenaId, amount: over, source: levies[idx].id, status: "未処理", at: new Date().toISOString() });
          await writeJson(taxOverpaymentsFile, pool);
        }
        await writeJson(taxLeviesFile, levies);
        await audit(req, "tax-levy.revise", { id: levies[idx].id, from: before, to: levies[idx].amount });
        return reply(res, 200, levies[idx]);
      }
    }

    // ── 収納消込（入金登録→自動消込。過納分は過誤納プールへ）──
    if (pathname === "/api/tax/receipts" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.levyId || b.amount === undefined) return reply(res, 400, { error: "levyId, amount required" });
      const levies = await readJson(taxLeviesFile, []);
      const idx = levies.findIndex((l) => l.id === b.levyId);
      if (idx === -1) return reply(res, 404, { error: "levy not found" });
      const unpaid = levies[idx].amount - (levies[idx].paidTotal ?? 0);
      const applied = Math.min(unpaid, Number(b.amount));
      const over = Number(b.amount) - applied;
      levies[idx].paidTotal = (levies[idx].paidTotal ?? 0) + applied;
      await writeJson(taxLeviesFile, levies);
      const receipts = await readJson(taxReceiptsFile, []);
      const receipt = {
        id: crypto.randomUUID(), levyId: b.levyId, amount: Number(b.amount), applied, over,
        channel: b.channel ?? "窓口", paidAt: b.paidAt ?? new Date().toISOString(),
        result: over > 0 ? "過納" : "消込済", by: actor.id,
      };
      receipts.unshift(receipt);
      await writeJson(taxReceiptsFile, receipts);
      if (over > 0) {
        const pool = await readJson(taxOverpaymentsFile, []);
        pool.unshift({ id: crypto.randomUUID(), atenaId: levies[idx].atenaId, amount: over, source: b.levyId, status: "未処理", at: receipt.paidAt });
        await writeJson(taxOverpaymentsFile, pool);
      }
      await audit(req, "tax-receipt.create", { id: receipt.id, levyId: b.levyId, applied, over });
      return reply(res, 201, receipt);
    }
    // 納付データ一括取込（口座振替・コンビニ等の消込用データ）
    if (pathname === "/api/tax/receipts/import" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.rows) || !b.rows.length) return reply(res, 400, { error: "rows[] required" });
      const levies = await readJson(taxLeviesFile, []);
      const receipts = await readJson(taxReceiptsFile, []);
      const pool = await readJson(taxOverpaymentsFile, []);
      const results = [];
      for (const row of b.rows.slice(0, 1000)) {
        const idx = levies.findIndex((l) => l.id === row.levyId);
        if (idx === -1) { results.push({ levyId: row.levyId, result: "対象なし" }); continue; }
        const unpaid = levies[idx].amount - (levies[idx].paidTotal ?? 0);
        const applied = Math.min(unpaid, Number(row.amount) || 0);
        const over = (Number(row.amount) || 0) - applied;
        levies[idx].paidTotal = (levies[idx].paidTotal ?? 0) + applied;
        receipts.unshift({ id: crypto.randomUUID(), levyId: row.levyId, amount: Number(row.amount) || 0, applied, over, channel: row.channel ?? "口座振替", paidAt: row.paidAt ?? new Date().toISOString(), result: over > 0 ? "過納" : "消込済", by: actor.id });
        if (over > 0) pool.unshift({ id: crypto.randomUUID(), atenaId: levies[idx].atenaId, amount: over, source: row.levyId, status: "未処理", at: new Date().toISOString() });
        results.push({ levyId: row.levyId, applied, over, result: "消込済" });
      }
      await writeJson(taxLeviesFile, levies);
      await writeJson(taxReceiptsFile, receipts);
      await writeJson(taxOverpaymentsFile, pool);
      await audit(req, "tax-receipt.import", { count: results.length });
      return reply(res, 200, { count: results.length, results });
    }

    // ── 充当・還付（過誤納プールの処理）──
    if (pathname === "/api/tax/overpayments" && req.method === "GET") {
      requireRole(actor, "CLERK");
      return reply(res, 200, await readJson(taxOverpaymentsFile, []));
    }
    if (pathname === "/api/tax/allocations" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.overpaymentId) return reply(res, 400, { error: "overpaymentId required" });
      const pool = await readJson(taxOverpaymentsFile, []);
      const op = pool.find((o) => o.id === b.overpaymentId);
      if (!op) return reply(res, 404, { error: "overpayment not found" });
      if (op.status !== "未処理") return reply(res, 409, { error: "already processed", status: op.status });
      const levies = await readJson(taxLeviesFile, []);
      const unpaidLevies = levies.filter((l) => l.atenaId === op.atenaId && (l.amount - (l.paidTotal ?? 0)) > 0 && !["執行停止", "不納欠損"].includes(l.status));
      const { allocations, remaining } = computeAllocation(op.amount, unpaidLevies);
      if (!allocations.length) return reply(res, 409, { error: "充当先の未納調定がありません（還付を使用してください）" });
      for (const a of allocations) {
        const idx = levies.findIndex((l) => l.id === a.levyId);
        levies[idx].paidTotal = (levies[idx].paidTotal ?? 0) + a.amount;
      }
      op.status = "充当済";
      op.allocations = allocations;
      op.refundable = remaining; // 充当しきれない残額は還付対象
      await writeJson(taxLeviesFile, levies);
      await writeJson(taxOverpaymentsFile, pool);
      await audit(req, "tax-allocation.apply", { overpaymentId: op.id, allocations: allocations.length, remaining });
      return reply(res, 200, { allocations, remaining });
    }
    if (pathname === "/api/tax/refunds" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.overpaymentId) return reply(res, 400, { error: "overpaymentId required" });
      const pool = await readJson(taxOverpaymentsFile, []);
      const op = pool.find((o) => o.id === b.overpaymentId);
      if (!op) return reply(res, 404, { error: "overpayment not found" });
      if (op.status !== "未処理") return reply(res, 409, { error: "already processed", status: op.status });
      op.status = "還付済";
      op.refundedAt = new Date().toISOString();
      op.refundMethod = b.method ?? "口座振込";
      await writeJson(taxOverpaymentsFile, pool);
      await audit(req, "tax-refund.decide", { overpaymentId: op.id, amount: op.amount });
      return reply(res, 200, op);
    }

    // ── 未納一覧・督促・延滞金 ──
    if (pathname === "/api/tax/arrears" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const asOf = searchParams.get("asOf") ?? new Date().toISOString().slice(0, 10);
      const levies = await readJson(taxLeviesFile, []);
      const arrears = levies.filter((l) => l.dueDate < asOf && (l.amount - (l.paidTotal ?? 0)) > 0 && !["執行停止", "不納欠損"].includes(l.status));
      return reply(res, 200, arrears.map((l) => ({ ...l, unpaid: l.amount - (l.paidTotal ?? 0) })));
    }
    if (pathname === "/api/tax/reminders" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const fee = Number(b.fee ?? 100); // 督促手数料（条例パラメータ）
      const asOf = b.asOf ?? new Date().toISOString().slice(0, 10);
      const levies = await readJson(taxLeviesFile, []);
      const dispositions = await readJson(taxDispositionsFile, []);
      const stopped = new Set(dispositions.filter((d) => d.kind === "督促停止" && !d.released).map((d) => d.levyId));
      const targets = levies.filter((l) => l.dueDate < asOf && (l.amount - (l.paidTotal ?? 0)) > 0 && !["執行停止", "不納欠損"].includes(l.status) && !stopped.has(l.id));
      const issued = targets.map((l) => ({ id: crypto.randomUUID(), levyId: l.id, kind: "督促", fee, at: new Date().toISOString(), by: actor.id }));
      dispositions.unshift(...issued);
      await writeJson(taxDispositionsFile, dispositions);
      await audit(req, "tax-reminder.bulk", { count: issued.length, fee });
      return reply(res, 200, { count: issued.length, totalFee: issued.length * fee });
    }
    if (pathname === "/api/tax/delinquent-charge" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const result = computeDelinquentCharge(await parseBody(req) || {});
      return reply(res, 200, result);
    }

    // ── 滞納繰越（年度締め）・滞納処分・徴収方法変更 ──
    if (pathname === "/api/tax/carryover" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const fromYear = Number(b.fiscalYear);
      if (!fromYear) return reply(res, 400, { error: "fiscalYear required" });
      const levies = await readJson(taxLeviesFile, []);
      const targets = levies.filter((l) => l.fiscalYear === fromYear && l.status !== "繰越済" && (l.amount - (l.paidTotal ?? 0)) > 0 && !["執行停止", "不納欠損"].includes(l.status));
      const created = targets.map((l) => {
        l.status = "繰越済"; // 元調定は繰越済（二重計上防止）
        return {
          id: crypto.randomUUID(), atenaId: l.atenaId, taxItem: l.taxItem, fiscalYear: fromYear + 1,
          period: l.period, amount: l.amount - (l.paidTotal ?? 0), method: l.method,
          dueDate: l.dueDate, status: "滞納繰越", paidTotal: 0, revisions: [], carriedFrom: l.id,
          createdBy: actor.id, createdAt: new Date().toISOString(),
        };
      });
      levies.unshift(...created);
      await writeJson(taxLeviesFile, levies);
      await audit(req, "tax-carryover.run", { fiscalYear: fromYear, count: created.length });
      return reply(res, 200, { count: created.length });
    }
    if (pathname === "/api/tax/dispositions" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const kinds = ["督促停止", "催告", "執行停止", "差押", "参加差押", "交付要求", "不納欠損"];
      if (!b.levyId || !kinds.includes(b.kind)) return reply(res, 400, { error: `levyId と kind(${kinds.join("/")}) が必須です` });
      const levies = await readJson(taxLeviesFile, []);
      const idx = levies.findIndex((l) => l.id === b.levyId);
      if (idx === -1) return reply(res, 404, { error: "levy not found" });
      if (b.kind === "執行停止" || b.kind === "不納欠損") levies[idx].status = b.kind;
      await writeJson(taxLeviesFile, levies);
      const dispositions = await readJson(taxDispositionsFile, []);
      const item = { id: crypto.randomUUID(), levyId: b.levyId, kind: b.kind, note: b.note ?? "", at: new Date().toISOString(), by: actor.id };
      dispositions.unshift(item);
      await writeJson(taxDispositionsFile, dispositions);
      await audit(req, "tax-disposition.create", { levyId: b.levyId, kind: b.kind });
      return reply(res, 201, item);
    }
    // 執行停止から3年経過＝不納欠損候補の抽出
    if (pathname === "/api/tax/dispositions" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const dispositions = await readJson(taxDispositionsFile, []);
      if (searchParams.get("kessonCandidates") === "1") {
        const limit = new Date();
        limit.setFullYear(limit.getFullYear() - 3);
        const candidates = dispositions.filter((d) => d.kind === "執行停止" && new Date(d.at) <= limit);
        return reply(res, 200, candidates);
      }
      return reply(res, 200, dispositions.slice(0, 200));
    }
    // 徴収方法の切替（普通徴収⇔特別徴収・特別徴収義務者の一括移転）
    if (pathname === "/api/tax/collection-method" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const to = b.method === "特別徴収" ? "特別徴収" : b.method === "普通徴収" ? "普通徴収" : null;
      if (!to || (!b.levyId && !b.atenaId)) return reply(res, 400, { error: "method(普通徴収|特別徴収) と levyId または atenaId が必須です" });
      const levies = await readJson(taxLeviesFile, []);
      const targets = levies.filter((l) => (b.levyId ? l.id === b.levyId : l.atenaId === b.atenaId) && (l.amount - (l.paidTotal ?? 0)) > 0);
      if (!targets.length) return reply(res, 404, { error: "対象調定がありません" });
      for (const l of targets) { l.method = to; if (b.employerId !== undefined) l.employerId = b.employerId; }
      await writeJson(taxLeviesFile, levies);
      await audit(req, "tax-collection-method.change", { to, count: targets.length });
      return reply(res, 200, { count: targets.length, method: to });
    }

    // ════ 申告管理（標準仕様書: 申告受理/eLTAX取込/未申告者抽出/申告案内/扶養重複チェック/所得自動算出）════

    if (pathname === "/api/tax/returns" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const returns = await readJson(taxReturnsFile, []);
      const fy = searchParams.get("fiscalYear") ?? "";
      return reply(res, 200, fy ? returns.filter((r) => String(r.fiscalYear) === fy) : returns.slice(0, 500));
    }
    // 申告受理（窓口・郵送）。給与/年金収入があれば所得を自動算出して保持する
    if (pathname === "/api/tax/returns" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      for (const k of ["atenaId", "fiscalYear"]) {
        if (!b[k]) return reply(res, 400, { error: `${k} is required` });
      }
      const returns = await readJson(taxReturnsFile, []);
      const salary = b.salaryRevenue !== undefined ? computeSalaryIncome(b) : null;
      const pension = b.pensionRevenue !== undefined ? computePensionIncome(b) : null;
      const item = {
        id: crypto.randomUUID(), atenaId: String(b.atenaId), fiscalYear: Number(b.fiscalYear),
        kind: b.kind === "事業所家屋敷" ? "事業所家屋敷" : "個人住民税",
        method: ["窓口", "郵送", "eLTAX"].includes(b.method) ? b.method : "窓口",
        dependents: Array.isArray(b.dependents) ? b.dependents.map(String) : [], // 被扶養者の宛名番号
        salary, pension, status: "受理",
        receivedBy: actor.id, receivedAt: new Date().toISOString(),
      };
      returns.unshift(item);
      await writeJson(taxReturnsFile, returns);
      await audit(req, "tax-return.accept", { id: item.id, fiscalYear: item.fiscalYear, method: item.method });
      return reply(res, 201, item);
    }
    // eLTAX経由の申告データ一括取込（更正請求情報含む）
    if (pathname === "/api/tax/returns/import" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.rows) || !b.rows.length) return reply(res, 400, { error: "rows[] required" });
      const returns = await readJson(taxReturnsFile, []);
      const imported = b.rows.slice(0, 1000).map((row) => ({
        id: crypto.randomUUID(), atenaId: String(row.atenaId ?? ""), fiscalYear: Number(row.fiscalYear) || 0,
        kind: "個人住民税", method: "eLTAX",
        dependents: Array.isArray(row.dependents) ? row.dependents.map(String) : [],
        salary: row.salaryRevenue !== undefined ? computeSalaryIncome(row) : null,
        pension: row.pensionRevenue !== undefined ? computePensionIncome(row) : null,
        status: "受理", receivedBy: actor.id, receivedAt: new Date().toISOString(),
      }));
      returns.unshift(...imported);
      await writeJson(taxReturnsFile, returns);
      await audit(req, "tax-return.import", { count: imported.length });
      return reply(res, 200, { count: imported.length });
    }
    // 未申告者の抽出（課税対象者のうち当年度の申告がない者）
    if (pathname === "/api/tax/returns/unfiled" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const fy = Number(searchParams.get("fiscalYear"));
      if (!fy) return reply(res, 400, { error: "fiscalYear required" });
      const subjects = (await dbSelect("tax_system.tax_subject")) ?? await readJson(taxSubjectsFile, []);
      const returns = await readJson(taxReturnsFile, []);
      const filed = new Set(returns.filter((r) => r.fiscalYear === fy).map((r) => r.atenaId));
      const unfiled = subjects.filter((s) => !filed.has(s.id) && !filed.has(s.taxNumber));
      return reply(res, 200, { fiscalYear: fy, count: unfiled.length, subjects: unfiled });
    }
    // 未申告者への申告案内（一括発行・件数返却）
    if (pathname === "/api/tax/returns/notices" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const fy = Number(b.fiscalYear);
      if (!fy) return reply(res, 400, { error: "fiscalYear required" });
      const subjects = (await dbSelect("tax_system.tax_subject")) ?? await readJson(taxSubjectsFile, []);
      const returns = await readJson(taxReturnsFile, []);
      const filed = new Set(returns.filter((r) => r.fiscalYear === fy).map((r) => r.atenaId));
      const targets = subjects.filter((s) => !filed.has(s.id) && !filed.has(s.taxNumber));
      await audit(req, "tax-return.notice", { fiscalYear: fy, count: targets.length });
      return reply(res, 200, { fiscalYear: fy, count: targets.length, kind: b.kind ?? "申告案内" });
    }
    // 扶養重複チェック（同一被扶養者が複数申告で扶養計上されていないか）
    if (pathname === "/api/tax/returns/dependent-duplicates" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const fy = Number(searchParams.get("fiscalYear"));
      const returns = (await readJson(taxReturnsFile, [])).filter((r) => !fy || r.fiscalYear === fy);
      const seen = new Map(); // 被扶養者 → [申告者…]
      for (const r of returns) for (const d of r.dependents ?? []) {
        if (!seen.has(d)) seen.set(d, []);
        seen.get(d).push(r.atenaId);
      }
      const duplicates = [...seen.entries()].filter(([, who]) => new Set(who).size > 1)
        .map(([dependent, claimedBy]) => ({ dependent, claimedBy: [...new Set(claimedBy)] }));
      return reply(res, 200, { count: duplicates.length, duplicates });
    }
    // 所得の自動算出（単体API）
    if (pathname === "/api/tax/income/salary" && req.method === "POST") {
      requireRole(actor, "CLERK");
      return reply(res, 200, computeSalaryIncome(await parseBody(req) || {}));
    }
    if (pathname === "/api/tax/income/pension" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const result = computePensionIncome(await parseBody(req) || {});
      return reply(res, result.error ? 400 : 200, result);
    }

    // ════ 固定資産税・軽自動車検査情報・法人住民税・森林環境税・世帯/扶養参照（標準仕様書）════

    // ── 固定資産（土地・家屋・償却資産）の台帳。区分所有の持分按分、課税標準額の算出、
    //    評価補正（割合乗算/価額減額）、登記閉鎖との連動、家屋情報の複写、都市計画税の準用 ──
    if (pathname === "/api/tax/kotei/assets" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["土地", "家屋", "償却資産"];
      if (!kinds.includes(b.kind) || !b.atenaId || b.valuation === undefined) {
        return reply(res, 400, { error: `kind(${kinds.join("/")}), atenaId, valuation(評価額) required` });
      }
      const assets = await readJson(koteiAssetsFile, []);
      const share = Math.min(Math.max(Number(b.share ?? 1), 0), 1); // 区分所有の持分割合
      const valuation = Math.floor(Number(b.valuation) * share); // 持分按分した評価相当額
      const taxBase = Math.floor(valuation); // 課税標準額（評価情報から算出。特例は補正で表現）
      const asset = {
        id: crypto.randomUUID(), kind: b.kind, atenaId: String(b.atenaId),
        registryId: b.registryId ?? "", share, valuation, taxBase,
        koteiTax: Math.floor(taxBase * 0.014), // 固定資産税1.4%
        toshikeikakuTax: Math.floor(taxBase * 0.003), // 都市計画税0.3%（固定資産税の例による）
        linkedLandId: b.linkedLandId ?? null, // 区分所有家屋⇔敷地の連動
        crossBoundary: b.crossBoundary === true, // 行政界をまたぎ市ごとに評価を分けるフラグ
        largeDepreciable: b.largeDepreciable === true, // 大規模償却資産フラグ
        excludeNextYear: b.excludeNextYear === true, // 次年度課税台帳作成対象外フラグ
        status: "課税中", corrections: [], createdBy: actor.id, createdAt: new Date().toISOString(),
      };
      assets.unshift(asset);
      // 区分所有家屋の納税義務者と敷地（土地）の納税義務者の連動
      if (asset.linkedLandId) {
        const land = assets.find((a) => a.id === asset.linkedLandId);
        if (land) land.atenaId = asset.atenaId;
      }
      await writeJson(koteiAssetsFile, assets);
      await audit(req, "tax-kotei.create", { kind: asset.kind, taxBase: asset.taxBase });
      return reply(res, 201, asset);
    }
    {
      const correctMatch = pathname.match(/^\/api\/tax\/kotei\/assets\/([^/]+)\/correct$/);
      if (correctMatch && req.method === "POST") {
        requireRole(actor, "ADMIN");
        const b = await parseBody(req);
        const methods = ["割合乗算", "価額減額"]; // 評価基準第3章第1節十一・十二の補正
        if (!methods.includes(b.method) || b.value === undefined) return reply(res, 400, { error: `method(${methods.join("/")}), value required` });
        const assets = await readJson(koteiAssetsFile, []);
        const idx = assets.findIndex((a) => a.id === correctMatch[1]);
        if (idx === -1) return reply(res, 404, { error: "asset not found" });
        const before = assets[idx].valuation;
        assets[idx].valuation = b.method === "割合乗算" ? Math.floor(before * Number(b.value)) : Math.max(0, before - Number(b.value));
        assets[idx].taxBase = assets[idx].valuation;
        assets[idx].koteiTax = Math.floor(assets[idx].taxBase * 0.014);
        assets[idx].toshikeikakuTax = Math.floor(assets[idx].taxBase * 0.003);
        assets[idx].corrections.push({ method: b.method, value: Number(b.value), before, after: assets[idx].valuation, at: new Date().toISOString(), by: actor.id });
        await writeJson(koteiAssetsFile, assets);
        await audit(req, "tax-kotei.correct", { id: correctMatch[1], method: b.method });
        return reply(res, 200, assets[idx]);
      }
      // 家屋（補充）課税台帳の情報を複写して新規作成
      const copyMatch = pathname.match(/^\/api\/tax\/kotei\/assets\/([^/]+)\/copy$/);
      if (copyMatch && req.method === "POST") {
        requireRole(actor, "CLERK");
        const assets = await readJson(koteiAssetsFile, []);
        const src = assets.find((a) => a.id === copyMatch[1]);
        if (!src) return reply(res, 404, { error: "asset not found" });
        const copy = { ...src, id: crypto.randomUUID(), copiedFrom: src.id, corrections: [], createdAt: new Date().toISOString(), createdBy: actor.id };
        assets.unshift(copy);
        await writeJson(koteiAssetsFile, assets);
        await audit(req, "tax-kotei.copy", { from: src.id, to: copy.id });
        return reply(res, 201, copy);
      }
    }
    // 登記情報マスタの閉鎖→課税台帳の自動閉鎖（連動）
    if (pathname === "/api/tax/kotei/registry-close" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.registryId) return reply(res, 400, { error: "registryId required" });
      const assets = await readJson(koteiAssetsFile, []);
      let closed = 0;
      for (const a of assets) if (a.registryId === b.registryId && a.status === "課税中") { a.status = "閉鎖"; a.closedAt = new Date().toISOString(); closed += 1; }
      await writeJson(koteiAssetsFile, assets);
      await audit(req, "tax-kotei.registry-close", { registryId: b.registryId, closed });
      return reply(res, 200, { registryId: b.registryId, closed });
    }

    // ── 軽自動車検査情報（軽検協データの取込・任意レイアウトのマッピング・車両突合・重課/軽課自動判定）──
    if (pathname === "/api/tax/kei/inspections/import" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.rows) || !b.rows.length) return reply(res, 400, { error: "rows[] required" });
      const mapping = b.mapping ?? {}; // 任意レイアウト→標準項目のマッピング（例: {"車台番号":"chassisNo"}）
      const mapRow = (row) => {
        const out = { ...row };
        for (const [from, to] of Object.entries(mapping)) if (row[from] !== undefined) out[to] = row[from];
        return out;
      };
      const matchFields = Array.isArray(b.matchFields) && b.matchFields.length ? b.matchFields : ["chassisNo"]; // 突合項目は選択可
      const vehicles = Array.isArray(b.vehicles) ? b.vehicles : [];
      const inspections = await readJson(keiInspectionsFile, []);
      const results = b.rows.slice(0, 1000).map((raw) => {
        const row = mapRow(raw);
        const matched = vehicles.find((v) => matchFields.every((f) => String(v[f] ?? "") === String(row[f] ?? "")));
        // 経年車重課（初度検査から13年）・グリーン化特例（電気軽自動車等は軽課）の自動判定
        const firstYear = Number(row.firstInspectionYear) || 0;
        const age = new Date().getFullYear() - firstYear;
        const classification = row.fuelType === "電気" ? "グリーン化特例（軽課）" : firstYear && age >= 13 ? "経年車重課" : "標準";
        return { chassisNo: row.chassisNo ?? "", matched: !!matched, vehicleId: matched?.id ?? null, classification };
      });
      inspections.unshift({ id: crypto.randomUUID(), count: results.length, matchFields, at: new Date().toISOString(), by: actor.id, results });
      await writeJson(keiInspectionsFile, inspections);
      await audit(req, "tax-kei.import", { count: results.length });
      return reply(res, 200, { count: results.length, results });
    }

    // ── 法人住民税の法人基本情報（主要法人フラグ・被合併法人・非営利型/営利型区分・履歴なし訂正）──
    if (pathname === "/api/tax/hojin" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.name) return reply(res, 400, { error: "name required" });
      const list = await readJson(hojinFile, []);
      const item = {
        id: crypto.randomUUID(), name: String(b.name), address: b.address ?? "",
        majorCorporation: b.majorCorporation === true, // 主要法人フラグ（市町村が任意に定める）
        corporationType: ["非営利型", "営利型"].includes(b.corporationType) ? b.corporationType : "営利型", // 一般社団・財団の区別
        merged: Array.isArray(b.merged) ? b.merged : [], // 被合併法人情報（法人名・所在地・合併年月日）
        createdBy: actor.id, createdAt: new Date().toISOString(),
      };
      list.unshift(item);
      await writeJson(hojinFile, list);
      await audit(req, "tax-hojin.create", { id: item.id });
      return reply(res, 201, item);
    }
    {
      const hojinMatch = pathname.match(/^\/api\/tax\/hojin\/([^/]+)$/);
      if (hojinMatch && req.method === "PUT") {
        requireRole(actor, "CLERK");
        const b = await parseBody(req);
        const list = await readJson(hojinFile, []);
        const idx = list.findIndex((h) => h.id === hojinMatch[1]);
        if (idx === -1) return reply(res, 404, { error: "hojin not found" });
        // 訂正処理: 履歴を作成しない（上書き）
        for (const k of ["name", "address", "majorCorporation", "corporationType", "merged"]) {
          if (b[k] !== undefined) list[idx][k] = b[k];
        }
        await writeJson(hojinFile, list);
        await audit(req, "tax-hojin.correct", { id: hojinMatch[1] });
        return reply(res, 200, { ...list[idx], note: "訂正処理（履歴は作成しない）" });
      }
    }

    // ── 森林環境税の免除（開始期の任意指定→自動判定対象外）・生活扶助による非課税の個別追加/取消 ──
    if (pathname === "/api/tax/shinrin/exemption" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId) return reply(res, 400, { error: "atenaId required" });
      const list = await readJson(shinrinExemptionsFile, []);
      const item = {
        id: crypto.randomUUID(), atenaId: String(b.atenaId),
        reason: b.reason === "生活扶助" ? "生活扶助（1/1現在・個人住民税と併せ非課税）" : "免除",
        startPeriod: b.startPeriod ?? "", // 開始期の任意指定
        autoJudgement: !b.startPeriod, // 開始期を指定した対象は免除期間の自動判定対象外
        status: "適用", at: new Date().toISOString(), by: actor.id,
      };
      list.unshift(item);
      await writeJson(shinrinExemptionsFile, list);
      await audit(req, "tax-shinrin.exempt", { atenaId: item.atenaId, manual: !item.autoJudgement });
      return reply(res, 201, item);
    }
    {
      const cancelMatch = pathname.match(/^\/api\/tax\/shinrin\/exemption\/([^/]+)\/cancel$/);
      if (cancelMatch && req.method === "POST") {
        requireRole(actor, "CLERK");
        const list = await readJson(shinrinExemptionsFile, []);
        const idx = list.findIndex((e) => e.id === cancelMatch[1]);
        if (idx === -1) return reply(res, 404, { error: "exemption not found" });
        list[idx].status = "取消";
        await writeJson(shinrinExemptionsFile, list);
        await audit(req, "tax-shinrin.cancel", { id: cancelMatch[1] });
        return reply(res, 200, list[idx]);
      }
    }

    // ── 世帯・扶養の相互参照（基本情報⇔世帯情報⇔扶養情報をたどって確認できる）──
    {
      const familyMatch = pathname.match(/^\/api\/tax\/atena\/([^/]+)\/family$/);
      if (familyMatch && req.method === "GET") {
        requireRole(actor, "CLERK");
        const subjects = (await dbSelect("tax_system.tax_subject")) ?? await readJson(taxSubjectsFile, []);
        const returns = await readJson(taxReturnsFile, []);
        const me = familyMatch[1];
        // 自分が扶養している人／自分を扶養している申告者（被扶養者⇔扶養主の双方向）
        const myReturn = returns.find((r) => r.atenaId === me);
        const dependents = myReturn?.dependents ?? [];
        const supportedBy = returns.filter((r) => (r.dependents ?? []).includes(me)).map((r) => r.atenaId);
        const subject = subjects.find((s) => s.id === me || s.taxNumber === me) ?? null;
        return reply(res, 200, { atenaId: me, basic: subject, dependents, supportedBy,
          note: "基本情報⇔世帯⇔扶養の相互参照ビュー（被扶養者から扶養主を辿れる）" });
      }
    }

    // ════ 関係者管理・KSK連携・税務署通知・外国転入者チェック（標準仕様書）════

    // ── 納税義務者の関係者（納税承継人・納税管理人・共有者・代理人。相続持分・送付先つき）──
    {
      const relMatch = pathname.match(/^\/api\/tax\/atena\/([^/]+)\/relations$/);
      if (relMatch && req.method === "POST") {
        requireRole(actor, "CLERK");
        const b = await parseBody(req);
        const kinds = ["納税承継人", "納税管理人", "共有者", "代理人", "相続人"];
        if (!kinds.includes(b.kind) || !b.relatedAtenaId) {
          return reply(res, 400, { error: `kind(${kinds.join("/")}), relatedAtenaId required` });
        }
        const relations = await readJson(taxRelationsFile, []);
        const item = {
          id: crypto.randomUUID(), atenaId: relMatch[1], kind: b.kind,
          relatedAtenaId: String(b.relatedAtenaId),
          share: b.share !== undefined ? Number(b.share) : null, // 納税承継人各人の相続持分
          shareKind: b.shareKind === "相続共有" ? "相続共有" : (b.kind === "共有者" ? "通常共有" : null), // 共有区分
          sendTo: b.sendTo ?? "", // 送付先（事業所等）
          at: new Date().toISOString(), by: actor.id,
        };
        relations.unshift(item);
        await writeJson(taxRelationsFile, relations);
        await audit(req, "tax-relation.create", { atenaId: item.atenaId, kind: item.kind });
        return reply(res, 201, item);
      }
      if (relMatch && req.method === "GET") {
        requireRole(actor, "CLERK");
        const relations = await readJson(taxRelationsFile, []);
        return reply(res, 200, relations.filter((r) => r.atenaId === relMatch[1]));
      }
      // 納税承継人等に紐づく全ての納税義務者の逆引き
      const successorMatch = pathname.match(/^\/api\/tax\/relations\/successor\/([^/]+)$/);
      if (successorMatch && req.method === "GET") {
        requireRole(actor, "CLERK");
        const relations = await readJson(taxRelationsFile, []);
        const linked = relations.filter((r) => r.relatedAtenaId === successorMatch[1]);
        return reply(res, 200, { successorId: successorMatch[1], count: linked.length,
          taxpayers: linked.map((r) => ({ atenaId: r.atenaId, kind: r.kind, share: r.share })) });
      }
    }

    // ── KSK（国税）連携: 第一表・第二表データの取込と相互紐づけ ──
    if (pathname === "/api/tax/ksk/import" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const table = b.table === 2 || b.table === "2" ? 2 : 1;
      if (!Array.isArray(b.rows) || !b.rows.length) return reply(res, 400, { error: "rows[]{kskKey,...} required" });
      const imports = await readJson(kskImportsFile, []);
      // 既取込の別表と kskKey で双方向に紐づける
      const counterpart = imports.filter((i) => i.table !== table);
      const counterKeys = new Set(counterpart.flatMap((i) => i.rows.map((r) => r.kskKey)));
      const rows = b.rows.slice(0, 1000).map((r) => ({ ...r, linked: counterKeys.has(r.kskKey) }));
      imports.unshift({ id: crypto.randomUUID(), table, rows, at: new Date().toISOString(), by: actor.id });
      await writeJson(kskImportsFile, imports);
      const linkedCount = rows.filter((r) => r.linked).length;
      await audit(req, "tax-ksk.import", { table, count: rows.length, linked: linkedCount });
      return reply(res, 200, { table, count: rows.length, linked: linkedCount });
    }

    // ── 相続税法第58条に準じた税務署通知（死亡者の課税情報のデータ出力）──
    if (pathname === "/api/tax/tsucho/death" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.deceased) || !b.deceased.length) return reply(res, 400, { error: "deceased[]{atenaId,deathDate} required" });
      const levies = await readJson(taxLeviesFile, []);
      const rows = b.deceased.map((d) => ({
        atenaId: d.atenaId, deathDate: d.deathDate ?? "",
        taxInfo: levies.filter((l) => l.atenaId === d.atenaId).map((l) => ({ taxItem: l.taxItem, fiscalYear: l.fiscalYear, amount: l.amount })),
      }));
      await audit(req, "tax-tsucho.death", { count: rows.length });
      return reply(res, 200, { document: "税務署通知（相続税法第58条準拠・死亡者課税情報）", count: rows.length, rows });
    }

    // ── 外国からの転入者: 出国から1年未満での入国の確認・抽出 ──
    if (pathname === "/api/tax/foreign-return/check" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.entries)) return reply(res, 400, { error: "entries[]{atenaId,departureDate,entryDate} required" });
      const YEAR = 365 * 24 * 60 * 60 * 1000;
      const within1y = b.entries.filter((e) => {
        const dep = new Date(e.departureDate);
        const ent = new Date(e.entryDate);
        return !Number.isNaN(dep.getTime()) && !Number.isNaN(ent.getTime()) && ent - dep < YEAR && ent > dep;
      });
      await audit(req, "tax-foreign-return.check", { checked: b.entries.length, within1y: within1y.length });
      return reply(res, 200, { checked: b.entries.length, within1Year: within1y,
        note: "出国から1年未満での再入国者（課税上の住所判定に使用）" });
    }

    // ════ 管理項目カタログ・控除切替・資料合算・固定資産（閉鎖/課税選択/試算）ほか（標準仕様書 税第3弾）════

    const taxKoteiExtFile = join(dataRoot, "tax-kotei-ext.json");

    // ── 管理項目カタログ（法人・固定資産の保有項目。レコードは汎用フィールドとして受入済）──
    if (pathname === "/api/tax/fields-catalog" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const CATALOG = {
        "法人": ["代表者住所", "代表者名", "支店ごとの開廃年月日", "予定・中間区分（予定／中間）",
          "管轄税務署の名称", "市町村内事務所の名称", "決算月"],
        "固定資産・土地": ["区画整理事業地区名称", "国土調査実施年月日", "国土調査地積", "国土調査地目",
          "合併前_市区町村コード", "現況調査年月日", "都市計画区分（市街化区域・市街化調整区域・非線引き・都市計画区域外・その他）",
          "都市計画区分の編入年度", "生産緑地区分（生産緑地・特定生産緑地）", "生産緑地区分の編入年度",
          "生産緑地終了年度", "負担区分", "他市町村跨りフラグ"],
        "固定資産・家屋": ["現況調査年月日", "市街化区域・市街化調整区域・非線引き・都市計画区域外・自治体独自の区分",
          "建物全体の用途区分", "合併前_市区町村コード"],
        "償却資産": ["事業種目", "事業種目コード", "資本金等の額", "当該団体従業者数", "設立年月日、設置年月日",
          "調査区分", "調査担当者", "汎用的なフラグ", "代表者のフラグの適用期間"],
        "電子通知": ["電子通知希望（有/無）", "団体別仮名識別子", "納通QR識別子", "電子通知希望申請日時"],
        "軽自動車": ["フルアシスト自転車該当区分", "試乗車区分", "受付拠点", "改造情報（改造内容・改造作業者）",
          "一括納税対象車両区分", "車体の形状", "ご当地ナンバー該当区分", "特定原付用標識区分",
          "弁償金額・弁償金支払年月日・弁償金支払い有無", "納税義務者情報（生活保護受給情報の確認有無）",
          "障害者情報（関係・等級・手帳種類/番号/交付日/有効期限・公費負担番号）",
          "申請者情報（氏名・住所・電話番号）", "審査結果・不許可事由", "未納の有無", "納付額", "納付方法", "速報データ・確報データ"],
        "口座振替": ["振替終了日", "停止開始日", "停止終了日"],
        "滞納整理": ["滞納原因", "職種", "続柄氏名", "生年月日", "性別", "所得", "合計収納額", "過納額", "滞納額",
          "納期未到来額", "ランク", "他機関移管対象者", "納付受託", "生活保護受給者", "最終接触日",
          "補正内容", "取消年月日", "取消事由", "移転先住所", "本店所在地", "代表者氏名", "業務内容", "電話番号",
          "管轄法務局", "納税状況その他", "同意・不同意の理由", "同意年月日", "換価執行日", "引渡場所", "占有日",
          "搬出日", "処分理由", "最高価申込者氏名（名称）", "最高価申込者の取消日時", "次順位買受申込価額",
          "次順位買受申込者氏名（名称）", "取消理由", "分割納付不履行時", "滞納者抽出時", "納付受託時"],
        "証券": ["券面額", "振出年月日", "振出人", "振出地", "支払期日", "支払人", "支払地",
          "決済年月日", "返却年月日", "不渡年月日", "組戻年月日", "代金取立依頼日"],
      };
      const domain = searchParams.get("domain") ?? "";
      if (domain) {
        if (!CATALOG[domain]) return reply(res, 404, { error: "unknown domain", domains: Object.keys(CATALOG) });
        return reply(res, 200, { domain, fields: CATALOG[domain] });
      }
      return reply(res, 200, { domains: Object.keys(CATALOG), catalog: CATALOG,
        note: "各税目レコードはこれらの管理項目を保持できる（汎用フィールド受入）" });
    }

    // ── 個人住民税: 控除の自動/手動切替（配偶者・特定親族特別・老人・同一生計・否認連動）──
    if (pathname === "/api/tax/kojo/switch" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const KINDS = ["扶養控除", "配偶者控除", "配偶者特別控除", "特定親族特別控除", "老人配偶者控除", "同一生計配偶者（控除なし）"];
      if (!b.from || !b.to || !KINDS.includes(b.from) || !KINDS.includes(b.to)) {
        return reply(res, 400, { error: "from, to required", kinds: KINDS });
      }
      const mode = b.mode === "自動" ? "自動" : "手動";
      // 自動切換は控除額が下がる場合に限る（同種間の額改定）
      if (mode === "自動" && b.from === b.to && Number(b.newAmount ?? 0) > Number(b.currentAmount ?? 0)) {
        return reply(res, 400, { error: "同種控除間の自動切換は控除額が下がる場合に限る" });
      }
      // 生年月日による区分自動判断（70歳以上→老人）
      let autoKubun = null;
      if (b.birthDate) {
        const age70 = b.birthDate.replace(/^(\d{4})/, (y) => String(Number(y) + 70));
        autoKubun = age70 <= (b.asOf ?? new Date().toISOString().slice(0, 10)) ? "老人区分" : "一般区分";
      }
      await audit(req, "tax-kojo.switch", { from: b.from, to: b.to, mode });
      return reply(res, 200, { from: b.from, to: b.to, mode, bidirectional: true, autoKubun,
        note: "配偶者控除⇔配偶者特別控除・扶養控除⇔特定親族特別控除の双方向切替（自動/手動）" });
    }
    // 否認処理（自動/手動選択・扶養障害者の同時否認）
    if (pathname === "/api/tax/kojo/hinin" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const mode = b.mode === "自動" ? "自動" : "手動";
      const targets = Array.isArray(b.targets) ? b.targets : [];
      if (!targets.length) return reply(res, 400, { error: "targets[]{atenaId,kind,fuyoShogaisha?} required" });
      const results = targets.map((t) => ({ atenaId: t.atenaId, kind: t.kind, denied: true,
        ...(t.fuyoShogaisha === true ? { alsoDenied: "扶養障害者控除（同時否認）" } : {}) }));
      await audit(req, "tax-kojo.hinin", { mode, targets: results.length });
      return reply(res, 200, { mode, results });
    }

    // ── 資料の合算（複数回・エラー後再合算・エラー中抽出・併用徴収・紐づき確認リスト）──
    if (pathname === "/api/tax/shiryo/gassan" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[]{shiryoId,atenaId,status(正常/エラー/チェック中),income} required" });
      const mode = b.mode === "個別" ? "個別" : "一括";
      const errorChecking = rows.filter((r) => r.status === "チェック中");
      const target = b.afterErrorCheckOnly === true ? rows.filter((r) => r.status === "正常") : rows.filter((r) => r.status !== "チェック中");
      const byAtena = {};
      for (const r of target) byAtena[r.atenaId] = (byAtena[r.atenaId] ?? 0) + Number(r.income ?? 0);
      await audit(req, "tax-shiryo.gassan", { mode, merged: Object.keys(byAtena).length, rerun: b.rerun === true });
      return reply(res, 200, { mode, rerunnable: true, merged: Object.entries(byAtena).map(([atenaId, income]) => ({ atenaId, income })),
        errorChecking, heiyoChoshu: b.heiyo === true ? { created: true, note: "併用徴収データを自動作成" } : null });
    }
    if (pathname === "/api/tax/shiryo/link-check" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[]{shiryoId,name,birthDate,atenaId} required" });
      const byKey = {};
      for (const r of rows) (byKey[`${r.name}|${r.birthDate}`] ??= []).push(r);
      const suspects = Object.entries(byKey).filter(([, list]) => new Set(list.map((r) => r.atenaId)).size > 1)
        .map(([key, list]) => ({ key, candidates: list }));
      await audit(req, "tax-shiryo.link-check", { suspects: suspects.length });
      return reply(res, 200, { document: "紐づき確認リスト（同姓同名・同生年月日）", count: suspects.length, suspects });
    }

    // ── 寡婦・ひとり親の戸籍確認調査リスト ──
    if (pathname === "/api/tax/chosa/kafu-list" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[]{atenaId,claimed,conditionMet,needsKosekiCheck} required" });
      const targets = rows.filter((r) => r.claimed === true && r.conditionMet !== true && r.needsKosekiCheck === true)
        .map((r) => ({ atenaId: r.atenaId, reason: "死別・離別等の戸籍情報確認が必要" }));
      await audit(req, "tax-chosa.kafu-list", { targets: targets.length });
      return reply(res, 200, { document: "寡婦・ひとり親 調査リスト", count: targets.length, targets });
    }

    // ── 租税条約による課税免除の個別追加・取消 ──
    if (pathname === "/api/tax/sozei-joyaku" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const actions = ["追加", "取消"];
      if (!b.atenaId || !actions.includes(b.action)) return reply(res, 400, { error: `atenaId, action(${actions.join("/")}) required` });
      await audit(req, "tax-sozei-joyaku", { atenaId: b.atenaId, action: b.action });
      return reply(res, 200, { atenaId: String(b.atenaId), action: b.action,
        status: b.action === "追加" ? "租税条約免除（個別登録）" : "免除取消", treaty: b.treaty ?? "" });
    }

    // ── 納期特例の年度途中解除→納期未到来分の納入書出力（送付済み除く）──
    if (pathname === "/api/tax/noki-tokurei/release" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const slips = Array.isArray(b.slips) ? b.slips : [];
      if (!b.jigyoshaId || !b.releaseDate || !slips.length) return reply(res, 400, { error: "jigyoshaId, releaseDate, slips[]{dueDate,sent} required" });
      const out = slips.filter((s) => s.sent !== true && s.dueDate > b.releaseDate);
      await audit(req, "tax-noki-tokurei.release", { jigyoshaId: b.jigyoshaId, slips: out.length });
      return reply(res, 200, { jigyoshaId: String(b.jigyoshaId), releaseDate: b.releaseDate,
        reissued: out, note: "送付済みを除く納期未到来分の納入書を出力" });
    }

    // ── 扶養是正情報等データの税務署管轄別出力 ──
    if (pathname === "/api/tax/fuyo-zesei/export" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[]{atenaId,zeimushoCode} required" });
      const byOffice = {};
      for (const r of rows) (byOffice[r.zeimushoCode ?? "不明"] ??= []).push(r);
      await audit(req, "tax-fuyo-zesei.export", { offices: Object.keys(byOffice).length });
      return reply(res, 200, { kind: "扶養是正情報等データ（税務署管轄別）",
        files: Object.entries(byOffice).map(([office, items]) => ({ zeimushoCode: office, count: items.length })) });
    }

    // ── 固定資産: 閉鎖処理＋閉鎖解除（履歴維持）──
    if (pathname === "/api/tax/kotei/heisa" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const actions = ["閉鎖", "閉鎖解除"];
      if (!b.assetId || !actions.includes(b.action)) return reply(res, 400, { error: `assetId, action(${actions.join("/")}) required` });
      const ext = await readJson(taxKoteiExtFile, { heisa: [], trials: [] });
      ext.heisa.unshift({ assetId: String(b.assetId), action: b.action, at: new Date().toISOString(), by: actor.id });
      await writeJson(taxKoteiExtFile, ext);
      const history = ext.heisa.filter((h) => h.assetId === String(b.assetId));
      await audit(req, "tax-kotei.heisa", { assetId: b.assetId, action: b.action });
      return reply(res, 200, { assetId: String(b.assetId), action: b.action,
        closedFlag: b.action === "閉鎖", historyKept: true, history,
        note: "閉鎖解除は閉鎖処理の履歴を維持したままフラグのみ解除" });
    }

    // ── 固定資産: 仮換地・従前地・保留地の課税選択 ──
    if (pathname === "/api/tax/kotei/kazei-select" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const targets = ["仮換地", "従前地", "両方"];
      if (!b.assetId || !targets.includes(b.kukakuSeiri ?? "仮換地")) return reply(res, 400, { error: `assetId, kukakuSeiri(${targets.join("/")}) required` });
      await audit(req, "tax-kotei.kazei-select", { assetId: b.assetId });
      return reply(res, 200, { assetId: String(b.assetId),
        kukakuSeiri: b.kukakuSeiri ?? "仮換地", horyuchi: b.horyuchiKazei === true ? "課税する" : "課税しない",
        note: "区画整理: 仮換地/従前地/両方の課税選択＋保留地の課税有無選択" });
    }

    // ── 固定資産: 試算結果の保存・課税シミュレーション（次年度予算見込み）──
    if (pathname === "/api/tax/kotei/trial" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[]{assetId,kazeiHyojun,rate?} required" });
      const results = rows.map((r) => ({ assetId: r.assetId,
        estimatedTax: Math.floor(Number(r.kazeiHyojun ?? 0) * Number(r.rate ?? 0.014)) }));
      const total = results.reduce((a, r) => a + r.estimatedTax, 0);
      const ext = await readJson(taxKoteiExtFile, { heisa: [], trials: [] });
      const trial = { id: crypto.randomUUID(), purpose: b.purpose ?? "次年度予算見込み",
        results, total, savedAt: new Date().toISOString(), by: actor.id };
      ext.trials.unshift(trial);
      await writeJson(taxKoteiExtFile, ext);
      await audit(req, "tax-kotei.trial", { rows: results.length, total });
      return reply(res, 200, { ...trial, saved: true });
    }
    if (pathname === "/api/tax/kotei/trial" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const ext = await readJson(taxKoteiExtFile, { heisa: [], trials: [] });
      return reply(res, 200, { count: ext.trials.length, trials: ext.trials.slice(0, 20) });
    }

    // ════ 税 第3弾後半: 軽自動車・収納/口座振替・滞納整理（標準仕様書）════

    const taxKeiFile = join(dataRoot, "tax-kei-ext.json");
    const taxFiscalFile = join(dataRoot, "tax-fiscal.json");
    const taxBunkatsuFile = join(dataRoot, "tax-bunkatsu.json");

    // ── 軽自動車: 一括廃車＋廃車復元 ──
    if (pathname === "/api/tax/kei/bulk-scrap" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const vehicleIds = Array.isArray(b.vehicleIds) ? b.vehicleIds : [];
      if (!vehicleIds.length) return reply(res, 400, { error: "vehicleIds[] required" });
      const ext = await readJson(taxKeiFile, { scrapped: [] });
      ext.scrapped.unshift(...vehicleIds.map((v) => ({ vehicleId: String(v), scrappedAt: new Date().toISOString() })));
      await writeJson(taxKeiFile, ext);
      await audit(req, "tax-kei.bulk-scrap", { count: vehicleIds.length });
      return reply(res, 200, { bulk: true, scrapped: vehicleIds.map(String) });
    }
    if (pathname === "/api/tax/kei/restore" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.vehicleId) return reply(res, 400, { error: "vehicleId required" });
      const ext = await readJson(taxKeiFile, { scrapped: [] });
      const idx = ext.scrapped.findIndex((s) => s.vehicleId === String(b.vehicleId));
      if (idx === -1) return reply(res, 404, { error: "廃車記録がありません" });
      ext.scrapped.splice(idx, 1);
      await writeJson(taxKeiFile, ext);
      await audit(req, "tax-kei.restore", { vehicleId: b.vehicleId });
      return reply(res, 200, { vehicleId: String(b.vehicleId), restored: true, note: "廃車処理した車両情報を復元" });
    }
    // 一括納税の利用可否判定（対象車両台数等の条件指定）
    if (pathname === "/api/tax/kei/ikkatsu-nozei" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const owners = Array.isArray(b.owners) ? b.owners : [];
      if (!owners.length) return reply(res, 400, { error: "owners[]{ownerId,vehicleCount,hasArrears} required" });
      const minVehicles = Number(b.minVehicles ?? 2);
      const results = owners.map((o) => ({ ownerId: o.ownerId,
        eligible: Number(o.vehicleCount ?? 0) >= minVehicles && o.hasArrears !== true,
        reason: Number(o.vehicleCount ?? 0) < minVehicles ? "対象車両台数不足" : o.hasArrears === true ? "未納あり" : "" }));
      await audit(req, "tax-kei.ikkatsu-nozei", { judged: results.length });
      return reply(res, 200, { conditions: { 対象車両台数: `${minVehicles}台以上`, 未納: "なし" }, results });
    }

    // ── 収納: 納付履歴明細・スマホ決済・都度/継続払い・内訳移動・隔地払い ──
    if (pathname === "/api/tax/shuno/history" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      let rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[]{zeimoku,paidAt,amount,method} required" });
      if (b.zeimoku) rows = rows.filter((r) => r.zeimoku === b.zeimoku);
      if (b.from) rows = rows.filter((r) => r.paidAt >= b.from);
      if (b.to) rows = rows.filter((r) => r.paidAt <= b.to);
      await audit(req, "tax-shuno.history", { rows: rows.length });
      return reply(res, 200, { document: "納付履歴明細（税目・納付日等の条件指定）", count: rows.length, rows });
    }
    if (pathname === "/api/tax/shuno/smartphone-pay" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const ext = await readJson(taxKeiFile, { scrapped: [], smartphoneMakers: [] });
      ext.smartphoneMakers = ext.smartphoneMakers ?? [];
      if (b.addMaker) {
        ext.smartphoneMakers.push({ code: String(b.addMaker.code ?? ""), name: String(b.addMaker.name ?? "") });
        await writeJson(taxKeiFile, ext);
      }
      await audit(req, "tax-shuno.smartphone-pay", { makers: ext.smartphoneMakers.length });
      return reply(res, 200, { paymentKubun: "スマートフォン納付（決済手段を納付区分として識別）",
        payTypes: ["都度払い", "継続払い"], kyotsuNozei: "共通納税による納付も同様に対応",
        makers: ext.smartphoneMakers, note: "メーカーコードは簡便に追加可能" });
    }
    if (pathname === "/api/tax/shuno/uchiwake-move" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.receiptId || !Number(b.amount)) return reply(res, 400, { error: "receiptId, amount, from(本体/退職分離), to required" });
      await audit(req, "tax-shuno.uchiwake-move", { receiptId: b.receiptId });
      return reply(res, 200, { receiptId: String(b.receiptId), moved: Number(b.amount),
        from: b.from ?? "個人住民税本体", to: b.to ?? "退職分離課税", note: "内訳誤りの移動補正" });
    }
    if (pathname === "/api/tax/shuno/kakuchi-barai" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || !Number(b.amount)) return reply(res, 400, { error: "atenaId, amount required" });
      await audit(req, "tax-shuno.kakuchi-barai", { atenaId: b.atenaId });
      return reply(res, 200, { documents: ["現金引換書（金融機関持参用）", "通知文"], atenaId: String(b.atenaId),
        amount: Number(b.amount), note: "口座不明時の隔地払い対応" });
    }

    // ── 口座振替の管理（カナ検査・口座照会・金融機関別集計・除外・解約者・停止/廃止）──
    if (pathname === "/api/tax/furikae/kana-check" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kana = String(b.kanaName ?? "");
      if (!kana) return reply(res, 400, { error: "kanaName required" });
      const ok = /^[ｱ-ﾝﾞﾟｧ-ｮｰA-Z0-9 ()\-./]+$/.test(kana);
      return reply(res, 200, { kanaName: kana, zenginCompliant: ok,
        note: "全銀協フォーマットの文字制限（半角カナ・英数・記号）検査" });
    }
    if (pathname === "/api/tax/furikae/accounts/lookup" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const accounts = Array.isArray(b.accounts) ? b.accounts : [];
      if (!b.holderName || !accounts.length) return reply(res, 400, { error: "holderName, accounts[] required" });
      const hits = accounts.filter((a) => (a.holderName ?? "").includes(b.holderName));
      await audit(req, "tax-furikae.lookup", { hits: hits.length });
      return reply(res, 200, { holderName: b.holderName, count: hits.length,
        hits: hits.map((a) => ({ accountId: a.accountId, holder: a.holderName, 振替対象税目: a.zeimoku ?? [] })) });
    }
    if (pathname === "/api/tax/furikae/summary" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[]{bank,zeimoku,amount,furikaeDate} required" });
      const byBank = {};
      for (const r of rows) {
        const k = `${r.bank}|${r.zeimoku}|${r.furikaeDate}`;
        byBank[k] = byBank[k] ?? { bank: r.bank, zeimoku: r.zeimoku, furikaeDate: r.furikaeDate, count: 0, amount: 0 };
        byBank[k].count += 1;
        byBank[k].amount += Number(r.amount ?? 0);
      }
      await audit(req, "tax-furikae.summary", { groups: Object.keys(byBank).length });
      return reply(res, 200, { summary: Object.values(byBank), note: "振替日・税目ごとの件数/金額を金融機関別に確認" });
    }
    if (pathname === "/api/tax/furikae/accounts/maintain" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const actions = ["除外リスト", "解約者抽出", "強制停止", "自動停止"];
      if (!actions.includes(b.action)) return reply(res, 400, { error: `action(${actions.join("/")}) required` });
      const accounts = Array.isArray(b.accounts) ? b.accounts : [];
      if (b.action === "除外リスト") {
        const excluded = accounts.filter((a) => a.excluded === true);
        return reply(res, 200, { action: b.action, count: excluded.length, list: excluded,
          note: "データ除外リスト＋除外状況の確認" });
      }
      if (b.action === "解約者抽出") {
        const hits = accounts.filter((a) => a.cancelled === true);
        return reply(res, 200, { action: b.action, count: hits.length, list: hits,
          includeStopped: b.includeStopped === true ? "停止・廃止口座も出力対象に含む" : "停止・廃止口座は除外" });
      }
      // 強制停止/自動停止: 未使用期間で抽出→停止・廃止（個別/一括）
      const months = Number(b.unusedMonths ?? 24);
      const asOf = b.asOf ?? new Date().toISOString().slice(0, 10);
      const targets = accounts.filter((a) => {
        if (!a.lastUsedAt) return true;
        const limit = a.lastUsedAt.replace(/^(\d{4})-(\d{2})/, (m, y, mo) => {
          const t = Number(y) * 12 + Number(mo) - 1 + months;
          return `${Math.floor(t / 12)}-${String((t % 12) + 1).padStart(2, "0")}`;
        });
        return limit <= asOf;
      }).map((a) => ({ accountId: a.accountId, lastUsedAt: a.lastUsedAt ?? null,
        status: b.action === "自動停止" ? "自動停止・廃止" : "強制停止・廃止" }));
      await audit(req, "tax-furikae.maintain", { action: b.action, targets: targets.length });
      return reply(res, 200, { action: b.action, mode: b.mode ?? "一括", unusedMonths: months,
        count: targets.length, list: targets });
    }

    // ── 会計年度の切替（過年度データの訂正禁止制御）──
    if (pathname === "/api/tax/fiscal-year" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      if (!Number(b.fiscalYear)) return reply(res, 400, { error: "fiscalYear required" });
      const fiscal = await readJson(taxFiscalFile, {});
      fiscal.current = Number(b.fiscalYear);
      await writeJson(taxFiscalFile, fiscal);
      await audit(req, "tax-fiscal-year.switch", { fiscalYear: fiscal.current });
      return reply(res, 200, { current: fiscal.current, note: "切替後は過去会計年度のデータ訂正を禁止" });
    }
    if (pathname === "/api/tax/fiscal-year/guard" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const fiscal = await readJson(taxFiscalFile, {});
      const current = fiscal.current ?? new Date().getFullYear();
      if (Number(b.targetYear) < current) {
        return reply(res, 409, { error: "過去の会計年度のデータは訂正できません", current });
      }
      return reply(res, 200, { allowed: true, current });
    }

    // ── 納税組合（解散の一括脱退・組合別納付状況）──
    if (pathname === "/api/tax/nozei-kumiai/dissolve" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const members = Array.isArray(b.members) ? b.members : [];
      if (!b.kumiaiId || !members.length) return reply(res, 400, { error: "kumiaiId, members[] required" });
      await audit(req, "tax-nozei-kumiai.dissolve", { kumiaiId: b.kumiaiId, members: members.length });
      return reply(res, 200, { kumiaiId: String(b.kumiaiId), dissolved: true,
        withdrawn: members.map(String), note: "組合解散による一括脱退" });
    }
    if (pathname === "/api/tax/nozei-kumiai/status" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[]{kumiaiId,paid,unpaid} required" });
      const byKumiai = {};
      for (const r of rows) {
        const k = (byKumiai[r.kumiaiId] ??= { kumiaiId: r.kumiaiId, paid: 0, unpaid: 0 });
        k.paid += Number(r.paid ?? 0);
        k.unpaid += Number(r.unpaid ?? 0);
      }
      return reply(res, 200, { kumiai: Object.values(byKumiai), note: "組合ごとの納付状況" });
    }

    // ── 滞納整理: 整理票・未納明細・抽出/除外・交渉経過・スケジュール共有 ──
    if (pathname === "/api/tax/tainou/seiri-hyo" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId) return reply(res, 400, { error: "atenaId required" });
      await audit(req, "tax-tainou.seiri-hyo", { atenaId: b.atenaId });
      return reply(res, 200, { documents: ["滞納整理票", "交渉経過", "未納明細書"], simultaneous: true,
        atenaId: String(b.atenaId) });
    }
    if (pathname === "/api/tax/tainou/minou-meisai" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[]{zeimoku,period,unpaid} required" });
      await audit(req, "tax-tainou.minou-meisai", { rows: rows.length });
      return reply(res, 200, { document: "未納明細（滞納明細情報に基づく）", count: rows.length, rows });
    }
    if (pathname === "/api/tax/tainou/extract" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[]{atenaId,rank,excluded?} required" });
      const excluded = rows.filter((r) => r.excluded === true);
      const targets = rows.filter((r) => r.excluded !== true);
      await audit(req, "tax-tainou.extract", { targets: targets.length, excluded: excluded.length });
      return reply(res, 200, { targets, excludedList: excluded, note: "抽出結果と除外対象の双方を出力可能" });
    }
    if (pathname === "/api/tax/tainou/kosho" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const actions = ["複製", "期間出力"];
      if (!actions.includes(b.action)) return reply(res, 400, { error: `action(${actions.join("/")}) required` });
      const history = Array.isArray(b.history) ? b.history : [];
      if (b.action === "複製") {
        const src = history[0] ?? null;
        if (!src) return reply(res, 400, { error: "history[0](複製元) required" });
        return reply(res, 200, { action: "複製", copied: { ...src, copiedAt: new Date().toISOString() } });
      }
      const hits = history.filter((h) => (!b.from || h.date >= b.from) && (!b.to || h.date <= b.to));
      await audit(req, "tax-tainou.kosho", { action: b.action, hits: hits.length });
      return reply(res, 200, { action: "期間出力", period: { from: b.from ?? "", to: b.to ?? "" }, count: hits.length, history: hits });
    }
    if (pathname === "/api/tax/tainou/schedule" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.title || !b.date) return reply(res, 400, { error: "title, date required" });
      await audit(req, "tax-tainou.schedule", { date: b.date });
      return reply(res, 200, { shared: true, scope: "担当者間で共有", entry: { title: String(b.title), date: String(b.date) },
        autoFromSeiyaku: b.seiyakuDate ? { seiyakuDate: b.seiyakuDate, note: "誓約年月日を基に担当者スケジュールへ自動反映" } : null });
    }

    // ── 分割納付（未完了抽出・端数処理・一括/自動取消・納期未到来含む・計画書・不履行者・残存抽出）──
    if (pathname === "/api/tax/tainou/bunkatsu/plan" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const total = Number(b.totalArrears);
      const monthly = Number(b.monthlyAmount);
      if (!total || !monthly) return reply(res, 400, { error: "totalArrears, monthlyAmount required" });
      const includeFuture = b.includeFuturePeriods === true;
      const months = Math.ceil(total / monthly);
      const schedule = Array.from({ length: months }, (_, i) => ({
        month: i + 1, amount: i === months - 1 ? total - monthly * (months - 1) : monthly })); // 端数は最終回
      const store = await readJson(taxBunkatsuFile, { plans: [] });
      const plan = { id: crypto.randomUUID(), total, monthly, months, schedule,
        includeFuturePeriods: includeFuture, status: b.simulate === true ? "シミュレーション" : "策定済",
        at: new Date().toISOString() };
      store.plans.unshift(plan);
      await writeJson(taxBunkatsuFile, store);
      await audit(req, "tax-bunkatsu.plan", { months, simulate: b.simulate === true });
      return reply(res, 200, { ...plan, document: "分割納付計画書（策定前のシミュレーションでも出力可）",
        端数処理: "最終回に寄せて調整", note: includeFuture ? "納期未到来期別も計画に含む" : "" });
    }
    if (pathname === "/api/tax/tainou/bunkatsu/review" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const plans = Array.isArray(b.plans) ? b.plans : [];
      if (!plans.length) return reply(res, 400, { error: "plans[]{planId,status,remainingArrears,defaulted} required" });
      const incomplete = plans.filter((p) => ["未完了終了", "完了"].includes(p.status) && Number(p.remainingArrears ?? 0) > 0);
      const completed = plans.filter((p) => p.status === "完了" && Number(p.remainingArrears ?? 0) === 0)
        .map((p) => ({ planId: p.planId, autoCancelled: true }));
      const defaulters = plans.filter((p) => p.defaulted === true).map((p) => ({ planId: p.planId }));
      await audit(req, "tax-bunkatsu.review", { incomplete: incomplete.length, defaulters: defaulters.length });
      return reply(res, 200, {
        incompleteWithArrears: incomplete, bulkCancellable: incomplete.map((p) => p.planId),
        autoCancelled: completed, defaultersList: defaulters,
        note: "未完了/完了後の残存滞納抽出＋一括取消＋完了分の自動取消＋不履行者リスト" });
    }

    // ── 猶予期間経過の一括取消・自動終了 ──
    if (pathname === "/api/tax/tainou/yuyo/expire" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const items = Array.isArray(b.items) ? b.items : [];
      if (!items.length) return reply(res, 400, { error: "items[]{yuyoId,endDate} required" });
      const asOf = b.asOf ?? new Date().toISOString().slice(0, 10);
      const expired = items.filter((i) => (i.endDate ?? "9999") < asOf)
        .map((i) => ({ yuyoId: i.yuyoId, status: b.auto === true ? "自動終了" : "一括取消" }));
      await audit(req, "tax-yuyo.expire", { expired: expired.length, auto: b.auto === true });
      return reply(res, 200, { asOf, mode: b.auto === true ? "自動" : "一括", expired });
    }

    // ── 実態調査: 1/1時点で市内に住民票を有さない滞納者の抽出 ──
    if (pathname === "/api/tax/tainou/jitai-chosa" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[]{atenaId,residentOnJan1} required" });
      const targets = rows.filter((r) => r.residentOnJan1 !== true);
      await audit(req, "tax-tainou.jitai-chosa", { targets: targets.length });
      return reply(res, 200, { kind: "実態調査対象（当年1/1時点で市内に住民票なし）", count: targets.length, targets });
    }

    // ── 処分文書（差出人の任意選択・処分理由の文面編集）──
    if (pathname === "/api/tax/tainou/shobun-doc" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const senders = ["首長名", "自治体名", "担当課名", "徴税吏員"];
      if (!senders.includes(b.sender)) return reply(res, 400, { error: `sender(${senders.join("/")}) required` });
      await audit(req, "tax-tainou.shobun-doc", { sender: b.sender });
      return reply(res, 200, { sender: b.sender, reasonText: String(b.reasonText ?? "（既定文面）"),
        editable: true, note: "処分理由の文面は編集可能・差出人は任意選択" });
    }

    // ── 欠損期別の識別・国税局/税務署回答の取込・終了後の期間確認・冊子マニュアル ──
    if (pathname === "/api/tax/tainou/kesson-flag" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!b.atenaId || !rows.length) return reply(res, 400, { error: "atenaId, rows[]{zeimoku,period,kesson} required" });
      await audit(req, "tax-tainou.kesson-flag", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId),
        periods: rows.map((r) => ({ zeimoku: r.zeimoku, period: r.period, 欠損期別: r.kesson === true })),
        note: "欠損有の滞納者について税目×期別で欠損か否かを識別" });
    }
    if (pathname === "/api/tax/tainou/kokuzei-import" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[]{source(国税局/税務署),atenaId,info} required" });
      await audit(req, "tax-tainou.kokuzei-import", { rows: rows.length });
      return reply(res, 200, { imported: rows.length, sources: ["国税局", "税務署"],
        note: "回答のあった滞納情報を取込" });
    }
    if (pathname === "/api/tax/periods/view" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.startDate || !b.endDate) return reply(res, 400, { error: "startDate, endDate required" });
      return reply(res, 200, { startDate: b.startDate, endDate: b.endDate,
        viewableAfterEnd: true, note: "終了後であっても開始日・終了日を確認可能" });
    }
    if (pathname === "/api/tax/manual" && req.method === "GET") {
      const format = searchParams.get("format") ?? "online";
      return reply(res, 200, { kind: format === "booklet" ? "冊子マニュアル（印刷用・全章）" : "オンラインマニュアル（目次）",
        chapters: [{ screen: "domain", title: "税務管理" }, { screen: "reports", title: "帳票" }, { screen: "admin", title: "管理" }] });
    }

    // ── 個人住民税の減免額計算（減免区分の割合×所得割/均等割×市町村/都道府県別・期割/月割按分）──
    if (pathname === "/api/tax/juminzei/genmen" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.amounts && !b.manualAmounts) return reply(res, 400, { error: "amounts（所得割・均等割の市町村/都道府県別額）が必須です" });
      if (b.splitMode && !["期割", "月割"].includes(b.splitMode)) return reply(res, 400, { error: "splitMode は 期割/月割 のいずれかです" });
      const result = computeJuminzeiGenmen(b);
      await audit(req, "tax-juminzei.genmen", { rate: result.rate, total: result.減免額合計 });
      return reply(res, 200, result);
    }

    // ── ふるさと納税（寄附金税額控除）の全額控除上限額の算定 ──
    if (pathname === "/api/tax/juminzei/furusato-upper" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (b.所得割額 === undefined) return reply(res, 400, { error: "所得割額 が必須です" });
      const result = computeFurusatoUpper(b);
      await audit(req, "tax-juminzei.furusato-upper", { upper: result.全額控除上限額 });
      return reply(res, 200, result);
    }

    // ── 税額控除の標準税率相当分・超過税率相当分の按分（0110154）──
    if (pathname === "/api/tax/juminzei/deduction-split" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const result = computeDeductionSplit(b || {});
      if (result.error) return reply(res, 400, result);
      await audit(req, "tax-juminzei.deduction-split", { 控除額: result.控除額 });
      return reply(res, 200, result);
    }

  return false;
}
