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

// ── 国民健康保険料 賦課算定（標準仕様書: 医療分・後期高齢者支援金分・介護納付金分を
//   所得割＋均等割＋平等割で賦課。税率・均等割額・平等割額は条例パラメータ。賦課限度額・軽減は国基準。
//   既定は令和6年度の国基準限度額＋例示条例率。所得=総所得金額等。賦課基準額=所得−基礎控除43万。──
const KOKUHO_RATES = {
  basicDeduction: 430000,
  components: {
    // 医療分(基礎賦課額) / 後期高齢者支援金分 / 介護納付金分(40〜64歳のみ)
    medical: { incomeRate: 0.072, perCapita: 29000, perHousehold: 21000, cap: 650000 },
    support: { incomeRate: 0.025, perCapita: 11000, perHousehold: 7000, cap: 240000 },
    longterm: { incomeRate: 0.021, perCapita: 14000, perHousehold: 6000, cap: 170000 },
  },
  // 軽減判定: 世帯の軽減判定所得が基準以下で均等割・平等割を軽減（令和6年度基準）
  reduction: { base: 430000, fivePerPerson: 295000, twoPerPerson: 545000 },
};
function computeKokuhoPremium(members, rates = KOKUHO_RATES) {
  const ms = (members || []).map((m) => ({ income: Number(m.income) || 0, age: Number(m.age) || 0 }));
  const N = ms.length;
  if (N === 0) return { total: 0, breakdown: { medical: 0, support: 0, longterm: 0 }, insuredCount: 0 };
  const baseTotal = ms.reduce((s, m) => s + Math.max(0, m.income - rates.basicDeduction), 0);
  const Nc = ms.filter((m) => m.age >= 40 && m.age < 65).length; // 介護2号被保険者
  // 軽減率（均等割・平等割に適用）
  const judge = ms.reduce((s, m) => s + Math.max(0, m.income), 0);
  let reductionRate = 0;
  if (judge <= rates.reduction.base) reductionRate = 0.7;
  else if (judge <= rates.reduction.base + rates.reduction.fivePerPerson * N) reductionRate = 0.5;
  else if (judge <= rates.reduction.base + rates.reduction.twoPerPerson * N) reductionRate = 0.2;
  const calcComponent = (key, count) => {
    if (count === 0) return 0;
    const c = rates.components[key];
    const shotoku = Math.floor(baseTotal * c.incomeRate);
    const kinto = c.perCapita * count;
    const heto = c.perHousehold;
    const reduced = Math.floor((kinto + heto) * (1 - reductionRate));
    return Math.min(c.cap, shotoku + reduced);
  };
  const breakdown = {
    medical: calcComponent("medical", N),
    support: calcComponent("support", N),
    longterm: calcComponent("longterm", Nc),
  };
  return {
    total: breakdown.medical + breakdown.support + breakdown.longterm,
    breakdown, reductionRate, baseTotal, insuredCount: N, longtermInsuredCount: Nc,
  };
}

// ── 本算定賦課（標準仕様書 7.確定賦課）──────────────────────────────────────
// 賦課方式(二/三/四方式)・保険料/保険税・世帯構成基準(賦課期日/最終/年度内最大)を選択し、
// 医療分・後期高齢者支援金分・介護納付金分(2号)を 所得割＋均等割＋(平等割)＋(資産割) で個人別に賦課。
// 法定軽減(7/5/2割)を応益割へ、未就学児は均等割1/2軽減、非自発的失業は給与所得を30/100で判定・賦課、
// 介護2号適用除外/同月得喪/未申告(所得不明→所得割なし)を反映し、区分ごとに賦課限度額で頭打ちする。
const HONSANTEI_DEFAULTS = {
  basicDeduction: 430000,
  // 区分別の率・額（incomeRate=所得割率, assetRate=資産割率(四方式), perCapita=均等割, perHousehold=平等割, cap=賦課限度額）
  components: {
    medical: { incomeRate: 0.072, assetRate: 0.20, perCapita: 29000, perHousehold: 21000, cap: 650000 },
    support: { incomeRate: 0.025, assetRate: 0.07, perCapita: 11000, perHousehold: 7000, cap: 240000 },
    longterm: { incomeRate: 0.021, assetRate: 0.06, perCapita: 14000, perHousehold: 6000, cap: 170000 },
  },
  reduction: { base: 430000, fivePerPerson: 295000, twoPerPerson: 545000 }, // 令和6年度基準
  involuntaryFactor: 0.30, // 非自発的失業: 給与所得を100分の30とみなす
  preschoolKintoFactor: 0.5, // 未就学児: 均等割を1/2軽減
};
function computeHonsanteiFuka(input = {}) {
  const cfg = {
    basicDeduction: Number(input.basicDeduction ?? HONSANTEI_DEFAULTS.basicDeduction),
    components: { ...HONSANTEI_DEFAULTS.components, ...(input.rates || input.components || {}) },
    reduction: { ...HONSANTEI_DEFAULTS.reduction, ...(input.reduction || {}) },
    involuntaryFactor: HONSANTEI_DEFAULTS.involuntaryFactor,
    preschoolKintoFactor: HONSANTEI_DEFAULTS.preschoolKintoFactor,
  };
  const houshiki = [2, 3, 4].includes(Number(input.houshiki)) ? Number(input.houshiki) : 3;
  const kind = input.kind === "保険税" ? "保険税" : "保険料";
  const bases = { "賦課期日時点": "qualifiedAtKijitsu", "最終時点": "qualifiedAtSaishu", "年度内最大": "qualifiedAnytime" };
  const basis = bases[input.basis] ? input.basis : "賦課期日時点";
  const qf = bases[basis];
  const usePerHousehold = houshiki >= 3; // 平等割は三方式以上
  const useAsset = houshiki === 4;       // 資産割は四方式

  const all = Array.isArray(input.household?.members) ? input.household.members
    : Array.isArray(input.members) ? input.members : [];
  // 世帯構成基準で対象者を選定。同月得喪は賦課対象外(240553)。基準フラグ未指定は「対象」とみなす。
  const active = all.filter((m) => !m.sameMonthLossGain && (m[qf] === undefined ? true : !!m[qf]));

  // 軽減判定所得（240550）: 非自発失業は給与所得30/100。未申告者がいると判定不能→軽減なし。
  const hasUnfiled = active.some((m) => m.unfiled);
  const judgeIncome = active.reduce((s, m) => {
    const wage = Number(m.wageIncome ?? (m.involuntary ? m.income : 0)) || 0;
    const base = Number(m.income) || 0;
    const adj = m.involuntary ? base - wage + Math.floor(wage * cfg.involuntaryFactor) : base;
    return s + Math.max(0, adj);
  }, 0);
  const N = active.length;
  let reductionRate = 0, reductionName = "なし";
  if (!hasUnfiled && N > 0) {
    if (judgeIncome <= cfg.reduction.base) { reductionRate = 0.7; reductionName = "7割軽減"; }
    else if (judgeIncome <= cfg.reduction.base + cfg.reduction.fivePerPerson * N) { reductionRate = 0.5; reductionName = "5割軽減"; }
    else if (judgeIncome <= cfg.reduction.base + cfg.reduction.twoPerPerson * N) { reductionRate = 0.2; reductionName = "2割軽減"; }
  }

  // 介護2号: 40〜64歳かつ適用除外でない(240629)。明示 kaigo2 があれば優先。
  const is2go = (m) => (m.kaigo2 !== undefined ? !!m.kaigo2 : (Number(m.age) >= 40 && Number(m.age) < 65)) && !m.kaigoExempt;
  const memberInComp = (m, comp) => comp === "longterm" ? is2go(m) : true;

  const perPerson = active.map((m) => ({ id: m.id ?? null, name: m.name ?? "", parts: {} }));
  const compTotals = { medical: 0, support: 0, longterm: 0 };
  for (const comp of ["medical", "support", "longterm"]) {
    const c = cfg.components[comp];
    const members = active.filter((m) => memberInComp(m, comp));
    if (members.length === 0) { compTotals[comp] = 0; continue; }
    let shotokuwari = 0, kintowari = 0, shisanwari = 0;
    for (let i = 0; i < active.length; i++) {
      const m = active[i];
      if (!memberInComp(m, comp)) continue;
      // 所得割: 未申告は賦課しない(240560)。非自発失業は給与所得30/100で賦課基準額を算定(240551)。
      let levyIncome = 0;
      if (!m.unfiled) {
        const wage = Number(m.wageIncome ?? (m.involuntary ? m.income : 0)) || 0;
        const base = Number(m.income) || 0;
        levyIncome = m.involuntary ? base - wage + Math.floor(wage * cfg.involuntaryFactor) : base;
      }
      const shotoku = Math.floor(Math.max(0, levyIncome - cfg.basicDeduction) * c.incomeRate);
      // 均等割: 未就学児は1/2軽減(240552)。法定軽減後にさらに未就学児1/2を適用。
      let kinto = c.perCapita * (1 - reductionRate);
      if (m.preschool) kinto *= cfg.preschoolKintoFactor;
      kinto = Math.floor(kinto);
      // 資産割(四方式)
      const shisan = useAsset ? Math.floor((Number(m.assetTax) || 0) * c.assetRate) : 0;
      shotokuwari += shotoku; kintowari += kinto; shisanwari += shisan;
      perPerson[i].parts[comp] = { 所得割: shotoku, 均等割: kinto, 資産割: shisan };
    }
    // 平等割(三/四方式): 世帯に1回。法定軽減を適用。
    const heto = usePerHousehold ? Math.floor(c.perHousehold * (1 - reductionRate)) : 0;
    const raw = shotokuwari + kintowari + shisanwari + heto;
    compTotals[comp] = Math.min(c.cap, raw);
  }
  const total = compTotals.medical + compTotals.support + compTotals.longterm;
  return {
    kind, houshiki, houshikiName: `${["", "", "二", "三", "四"][houshiki]}方式`, basis,
    insuredCount: N, kaigo2Count: active.filter(is2go).length,
    reductionRate, reductionName, judgeIncome,
    breakdown: compTotals, perPerson, total,
    note: `${kind}・${["", "", "二", "三", "四"][houshiki]}方式・世帯構成基準=${basis}で本算定賦課`,
  };
}

// ── 異動賦課（標準仕様書 8.異動賦課）。現年度更正・過年度更正の再計算と集計 ───────────────
// 各対象の更正前後の調定額（household があれば本算定賦課で再計算、無ければ amounts をそのまま）から
// 更正増減を区分別(医療/支援/介護/子ども)に算出。過年度は賦課期限(遡及年数)を経過した世帯を更正対象外にし、
// 結果を「調定増減表(更正増/更正減×現年度/過年度×区分、不現住区別)」「賦課状況集計表(賦課期日/最終)」へ集計。
const KOSEI_COMPONENTS = ["medical", "support", "longterm", "child"];
function _koseiAmounts(t) {
  // household 指定があれば本算定賦課で区分別調定を再計算、無ければ amounts(区分別) を採用
  const fromCalc = (hh) => {
    if (!hh) return null;
    const r = computeHonsanteiFuka(hh.household ? hh : { household: hh });
    return { medical: r.breakdown.medical, support: r.breakdown.support,
      longterm: r.breakdown.longterm, child: Number(hh.childAmount) || 0 };
  };
  const norm = (a) => ({ medical: Number(a?.medical) || 0, support: Number(a?.support) || 0,
    longterm: Number(a?.longterm) || 0, child: Number(a?.child) || 0 });
  return {
    before: fromCalc(t.before) ?? norm(t.beforeAmounts),
    after: fromCalc(t.after) ?? norm(t.afterAmounts),
  };
}
function computeIdouKosei(input = {}) {
  const nendoType = input.nendoType === "過年度" ? "過年度" : "現年度";
  const targets = Array.isArray(input.targets) ? input.targets : [];
  const kijun = input.kijun || {};
  // 過年度更正の賦課期限判定(240745/240747/240754): 起算日＋遡及年数を today が超えていれば更正対象外。
  const retro = { 資格異動: Number(kijun.retroYears?.資格異動 ?? kijun.retroYears?.shikaku ?? 2),
    所得変更: Number(kijun.retroYears?.所得変更 ?? kijun.retroYears?.shotoku ?? 5) };
  const today = kijun.today ? new Date(kijun.today) : new Date();
  const overLimit = (t) => {
    if (nendoType !== "過年度") return false;
    const reasonKind = t.reason?.kind === "所得更正" || t.reason?.kind === "所得変更" ? "所得変更" : "資格異動";
    const years = retro[reasonKind];
    // 起算日: 資格適用開始日があればそれ、無ければ対象年度の第1期納期限
    const startStr = t.qualifiedStartDate || kijun.firstDueDate;
    if (!startStr) return false;
    const limit = new Date(startStr); limit.setFullYear(limit.getFullYear() + years);
    return today > limit;
  };

  const zougen = { 現年度: {}, 過年度: {} }; // 調定増減表: 区分別の更正増/更正減
  for (const k of [...KOSEI_COMPONENTS, "total"]) { zougen.現年度[k] = { 増: 0, 減: 0 }; zougen.過年度[k] = { 増: 0, 減: 0 }; }
  const fugenju = { 増: 0, 減: 0 }; // 不現住分(240775)
  const details = [];
  let excluded = 0;
  for (const t of targets) {
    if (overLimit(t)) { excluded++; details.push({ atenaId: t.atenaId ?? null, status: "更正対象外", reason: "賦課期限(遡及年数)経過" }); continue; }
    if (t.canceled || t.否決) { details.push({ atenaId: t.atenaId ?? null, status: "更正取消", reason: "調定否決により更正取消" }); continue; }
    const { before, after } = _koseiAmounts(t);
    const deltas = {}; let totalDelta = 0;
    for (const c of KOSEI_COMPONENTS) { deltas[c] = after[c] - before[c]; totalDelta += deltas[c]; }
    deltas.total = totalDelta;
    const bucket = zougen[nendoType];
    for (const c of [...KOSEI_COMPONENTS, "total"]) {
      if (deltas[c] >= 0) bucket[c].増 += deltas[c]; else bucket[c].減 += -deltas[c];
    }
    if (t.fugenju) { if (totalDelta >= 0) fugenju.増 += totalDelta; else fugenju.減 += -totalDelta; }
    details.push({ atenaId: t.atenaId ?? null, status: totalDelta >= 0 ? "増額更正" : "減額更正",
      before, after, deltas });
  }
  // 賦課状況集計表(240786/240790): 賦課期日時点(更正前合計)と最終時点(更正後合計)
  const shukei = {
    賦課期日時点: details.filter((d) => d.before).reduce((s, d) => s + (d.before.medical + d.before.support + d.before.longterm + d.before.child), 0),
    最終時点: details.filter((d) => d.after).reduce((s, d) => s + (d.after.medical + d.after.support + d.after.longterm + d.after.child), 0),
  };
  return {
    nendoType, targetCount: targets.length, processed: details.filter((d) => d.deltas).length, excluded,
    調定増減表: zougen, 不現住: fugenju, 賦課状況集計表: shukei, details,
    note: `${nendoType}更正の再計算・調定増減表(医療/支援/介護/子ども/合計)・賦課状況集計表を作成`,
  };
}

// ── 保険料（税）収納（標準仕様書 13.収納）─────────────────────────────────────
// 延滞金の法令計算（本税1000円未満切捨・納期限翌日から1月までは低率・以降は高率・確定延滞金100円未満切捨）。
const ENTAIKIN_DEFAULTS = { earlyRate: 0.024, lateRate: 0.087, earlyDays: 30, honzeiUnit: 1000, entaiUnit: 100 };
function computeEntaikin(honzei, dueDate, payDate, rates = {}) {
  const r = { ...ENTAIKIN_DEFAULTS, ...rates };
  const base = Math.floor((Number(honzei) || 0) / r.honzeiUnit) * r.honzeiUnit; // 本税1000円未満切捨
  if (base <= 0 || !dueDate || !payDate) return 0;
  const days = Math.floor((new Date(payDate) - new Date(dueDate)) / 86400000);
  if (days <= 0) return 0;
  const early = Math.min(days, r.earlyDays);
  const late = Math.max(0, days - r.earlyDays);
  const raw = base * r.earlyRate * early / 365 + base * r.lateRate * late / 365;
  return Math.floor(raw / r.entaiUnit) * r.entaiUnit; // 確定延滞金100円未満切捨
}
const SHUNO_NAIWAKE = ["medical", "support", "longterm", "child"];
// 消込: 納付額を調定額(本体)→延滞金→督促手数料の順に充当し、本体完納時に確定延滞金を確定する。
// 本体未納で延滞金のみ納付された場合は消込除外（保留）する(241446)。
function computeKeshikomi(input = {}) {
  const ch = input.chotei || {};
  const honbun = SHUNO_NAIWAKE.reduce((o, k) => (o[k] = Number(ch.honbun?.[k]) || 0, o), {});
  const honbunTotal = SHUNO_NAIWAKE.reduce((s, k) => s + honbun[k], 0);
  const dueDate = ch.dueDate || input.dueDate;
  const payments = Array.isArray(input.payments) ? input.payments : [];
  const paid = payments.reduce((s, p) => s + (Number(p.amount) || 0), 0);
  const tokusoku = payments.filter((p) => p.kind === "督促手数料").reduce((s, p) => s + (Number(p.amount) || 0), 0);

  // 本体未納で延滞金のみ納付 → 保留（消込除外）
  const onlyEntaikin = honbunTotal > 0 && payments.length > 0
    && payments.every((p) => p.kind === "延滞金") && paid < honbunTotal;
  if (onlyEntaikin) {
    return { status: "保留", reason: "本体未納で延滞金のみ納付のため消込除外", honbunTotal, paid,
      allocations: {}, honbunRemaining: honbunTotal, kanno: false, confirmedEntaikin: 0, hoiryu: true };
  }
  // 本体へ内訳按分充当（医療→支援→介護→子どもの順に充当）
  let remain = paid - tokusoku;
  const allocations = {};
  for (const k of SHUNO_NAIWAKE) {
    const a = Math.max(0, Math.min(honbun[k], remain));
    allocations[k] = a; remain -= a;
  }
  const honbunPaid = SHUNO_NAIWAKE.reduce((s, k) => s + allocations[k], 0);
  const honbunRemaining = honbunTotal - honbunPaid;
  const kanno = honbunRemaining === 0;
  // 本体完納時のみ確定延滞金を確定（241444/241445）。残った納付額を延滞金へ充当。
  let confirmedEntaikin = 0, entaikinPaid = 0;
  if (kanno) {
    const payDate = payments.map((p) => p.payDate).filter(Boolean).sort().pop() || input.payDate;
    confirmedEntaikin = computeEntaikin(honbunTotal, dueDate, payDate, input.entaikinRates);
    entaikinPaid = Math.max(0, Math.min(confirmedEntaikin, remain));
    remain -= entaikinPaid;
  }
  return {
    status: kanno ? "完納" : "一部納付", honbunTotal, paid, tokusokuTesuryo: tokusoku,
    allocations, honbunPaid, honbunRemaining, kanno,
    confirmedEntaikin, entaikinPaid, entaikinRemaining: confirmedEntaikin - entaikinPaid,
    overpayment: Math.max(0, remain), hoiryu: false,
    note: "本体充当→確定延滞金→督促手数料の順で消込。本体完納時に確定延滞金を確定。",
  };
}

// ── 滞納管理（標準仕様書 25.滞納管理）─────────────────────────────────────────
// 滞納者ランクの自動判定(241784): 滞納額・催告回数・年度・差押履歴・差押中・滞納年数・所得・口座登録有無・停止中。
function computeTainoRank(t = {}) {
  const amount = Number(t.amount) || 0;
  const years = Number(t.years) || 0;
  const saikokuCount = Number(t.saikokuCount) || 0;
  const reasons = [];
  let rank;
  if (t.suspended) { rank = "停止"; reasons.push("滞納処分の停止中"); }
  else if (t.underSeizure) { rank = "A"; reasons.push("差押中"); }
  else if (amount >= 1000000 || years >= 3 || saikokuCount >= 3) {
    rank = "A"; reasons.push(amount >= 1000000 ? "滞納額100万円以上" : years >= 3 ? "滞納3年以上" : "催告3回以上");
  } else if (amount >= 500000 || years >= 2) { rank = "B"; reasons.push(amount >= 500000 ? "滞納額50万円以上" : "滞納2年以上"); }
  else if (amount >= 100000) { rank = "C"; reasons.push("滞納額10万円以上"); }
  else { rank = "D"; reasons.push("少額滞納"); }
  if (t.hasSeizureHistory) reasons.push("差押履歴あり");
  if (t.hasAccount) reasons.push("口座登録あり(振替再開検討)");
  return { rank, reasons, factors: { amount, years, saikokuCount, underSeizure: !!t.underSeizure,
    suspended: !!t.suspended, hasSeizureHistory: !!t.hasSeizureHistory, hasAccount: !!t.hasAccount } };
}
// 分割納付計画の策定(241878-241890): 滞納額を回数で分割し、各回の納付予定日に応じた延滞金を自動計算。
// 端数は初回へ寄せる。本税のみ/延滞金のみ/督促手数料のみにも対応。誓約年月日があれば時効更新フラグを立てる。
function computeBunnoPlan(input = {}) {
  const principal = Math.max(0, Number(input.principal) || 0);   // 本税(調定額)
  const entaikinOnly = Math.max(0, Number(input.entaikinOnly) || 0); // 延滞金のみ
  const tokusokuOnly = Math.max(0, Number(input.tokusokuOnly) || 0); // 督促手数料のみ
  const count = Math.max(1, Number(input.count) || 1);
  const start = input.startDate ? new Date(input.startDate) : new Date();
  const dueDate = input.dueDate || input.startDate; // 法定納期限(延滞金起算)
  const perMonth = input.frequency === "複数回/月" ? 2 : 1;
  const target = principal + entaikinOnly + tokusokuOnly;
  if (target <= 0) return { error: "本税・延滞金・督促手数料のいずれかが必要です" };
  const basePer = Math.floor(target / count / 100) * 100; // 100円単位
  const remainder = target - basePer * count;             // 端数は初回へ
  const installments = [];
  let remainingPrincipal = principal;
  for (let i = 0; i < count; i++) {
    const d = new Date(start);
    if (perMonth === 2) d.setDate(d.getDate() + i * 15); else d.setMonth(d.getMonth() + i);
    const amt = basePer + (i === 0 ? remainder : 0);
    // 当該回の本税充当分に対する延滞金（納付予定日まで）を自動計算
    const principalPortion = Math.min(remainingPrincipal, amt);
    const entaikin = principal > 0 ? computeEntaikin(principalPortion, dueDate, d.toISOString().slice(0, 10), input.entaikinRates) : 0;
    remainingPrincipal -= principalPortion;
    installments.push({ seq: i + 1, date: d.toISOString().slice(0, 10), amount: amt,
      principalPortion, entaikin });
  }
  return {
    target, count, perMonth, installments,
    totalScheduled: installments.reduce((s, x) => s + x.amount, 0),
    timeExtensionUpdated: !!input.pledgeDate, // 誓約年月日があれば時効更新
    includesSuspended: !!input.includeSuspended,
    kind: principal > 0 ? "本税含む" : entaikinOnly > 0 ? "延滞金のみ" : "督促手数料のみ",
    note: "分割納付計画(各回の予定日に応じた延滞金自動計算・端数は初回・誓約で時効更新)",
  };
}

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 kokuhoInsuredFile = join(dataRoot, "kokuho-insured.json");
  const kokuhoHouseholdsFile = join(dataRoot, "kokuho-households.json");
  const kokuhoLeviesFile = join(dataRoot, "kokuho-levies.json");
  const kokuhoKogakuFile = join(dataRoot, "kokuho-kogaku.json");
  const kokuhoRyoyohiFile = join(dataRoot, "kokuho-ryoyohi.json");
  const kokuhoShussanFile = join(dataRoot, "kokuho-shussan.json");
  const kokuhoTerminalsFile = join(dataRoot, "kokuho-terminals.json");
  const kokuhoContactsFile = join(dataRoot, "kokuho-contacts.json");
  const kokuhoAccessLogFile = join(dataRoot, "kokuho-access-log.json");
  const kokuhoNoticesFile = join(dataRoot, "kokuho-notices.json");
  const kokuhoConfigFile = join(dataRoot, "kokuho-config.json");
  const kokuhoKoseiJiyuFile = join(dataRoot, "kokuho-kosei-jiyu.json");
  const kokuhoShunoChoteiFile = join(dataRoot, "kokuho-shuno-chotei.json");
  const kokuhoTainoKeikaFile = join(dataRoot, "kokuho-taino-keika.json");


    // ── 国保 世帯 (Kokuho Households) ────────────────────────────────────

    if (pathname === "/api/kokuho-households" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const households = (await dbSelect("kokuho.household")) ?? await readJson(kokuhoHouseholdsFile, []);
      const restrictions = await readJson(restrictionsFile, []);
      return reply(res, 200, households.map((h) => maskRecord(actor, restrictions, h)));
    }

    if (pathname === "/api/kokuho-households" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.householderName || !body.address) return reply(res, 400, { error: "householderName, address required" });
      const households = (await dbSelect("kokuho.household")) ?? await readJson(kokuhoHouseholdsFile, []);
      const householdNumber = "KH-" + Date.now().toString(36).toUpperCase();
      const household = { id: crypto.randomUUID(), householdNumber, householderName: body.householderName, address: body.address, status: "enrolled", enrolledAt: new Date().toISOString().slice(0, 10), createdBy: actor.id, createdAt: new Date().toISOString() };
      const dbInserted = await dbInsert("kokuho.household", household); if (dbInserted === null) { households.push(household); await writeJson(kokuhoHouseholdsFile, households); }
      await audit(req, "kokuho-household.enroll", { id: household.id, householdNumber });
      return reply(res, 201, household);
    }

    // ── 国保 被保険者 (Kokuho Insured) ────────────────────────────────────

    if (pathname === "/api/kokuho-insured" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const insured = (await dbSelect("kokuho.insured")) ?? await readJson(kokuhoInsuredFile, []);
      const { householdId } = Object.fromEntries(new URL(req.url, "http://x").searchParams);
      return reply(res, 200, householdId ? insured.filter((i) => i.householdId === householdId) : insured);
    }

    if (pathname === "/api/kokuho-insured" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.householdId || !body.fullName || !body.birthDate) return reply(res, 400, { error: "householdId, fullName, birthDate required" });
      const households = (await dbSelect("kokuho.household")) ?? await readJson(kokuhoHouseholdsFile, []);
      if (!households.find((h) => h.id === body.householdId)) return reply(res, 404, { error: "household not found" });
      const insured = (await dbSelect("kokuho.insured")) ?? await readJson(kokuhoInsuredFile, []);
      const insuredNumber = "KI-" + Date.now().toString(36).toUpperCase();
      const person = { id: crypto.randomUUID(), insuredNumber, householdId: body.householdId, fullName: body.fullName, birthDate: body.birthDate, relationship: body.relationship ?? "householder", status: "active", createdBy: actor.id, createdAt: new Date().toISOString() };
      const dbInserted = await dbInsert("kokuho.insured", person); if (dbInserted === null) { insured.push(person); await writeJson(kokuhoInsuredFile, insured); }
      await audit(req, "kokuho-insured.add", { id: person.id, insuredNumber });
      return reply(res, 201, person);
    }

    // ── 国保 賦課 (Kokuho Levies) ─────────────────────────────────────────

    if (pathname === "/api/kokuho-levies" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const levies = (await dbSelect("kokuho.levy")) ?? await readJson(kokuhoLeviesFile, []);
      const { householdId, fiscalYear } = Object.fromEntries(new URL(req.url, "http://x").searchParams);
      let result = levies;
      if (householdId) result = result.filter((l) => l.householdId === householdId);
      if (fiscalYear) result = result.filter((l) => l.fiscalYear === fiscalYear);
      return reply(res, 200, result);
    }

    if (pathname === "/api/kokuho-levies" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const body = await parseBody(req);
      if (!body.householdId || !body.fiscalYear || body.amount === undefined) return reply(res, 400, { error: "householdId, fiscalYear, amount required" });
      const households = (await dbSelect("kokuho.household")) ?? await readJson(kokuhoHouseholdsFile, []);
      if (!households.find((h) => h.id === body.householdId)) return reply(res, 404, { error: "household not found" });
      const levies = (await dbSelect("kokuho.levy")) ?? await readJson(kokuhoLeviesFile, []);
      const levy = { id: crypto.randomUUID(), householdId: body.householdId, fiscalYear: body.fiscalYear, amount: body.amount, paidAmount: 0, status: "levied", createdBy: actor.id, createdAt: new Date().toISOString() };
      const dbInserted = await dbInsert("kokuho.levy", levy); if (dbInserted === null) { levies.push(levy); await writeJson(kokuhoLeviesFile, levies); }
      await audit(req, "kokuho-levy.create", { id: levy.id, fiscalYear: body.fiscalYear, amount: body.amount });
      return reply(res, 201, levy);
    }

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


    const householdMatch = pathname.match(/^\/api\/kokuho-households\/([^/]+)$/);
    if (householdMatch && (req.method === "GET" || req.method === "PUT")) {
      requireRole(actor, "CLERK");
      const id = householdMatch[1];
      const household = (await dbSelectOne("kokuho.household", id))
        ?? (await readJson(kokuhoHouseholdsFile, [])).find((h) => h.id === id);
      if (!household) return reply(res, 404, { error: "household not found" });
      const restrictions = await readJson(restrictionsFile, []);
      if (req.method === "GET") return reply(res, 200, maskRecord(actor, restrictions, household));

      const body = await parseBody(req);
      const editable = ["householderName", "address"];
      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("kokuho.household", id, patch);
      let result = updated;
      if (updated === null) {
        const items = await readJson(kokuhoHouseholdsFile, []);
        const idx = items.findIndex((x) => x.id === id);
        items[idx] = { ...items[idx], ...patch };
        await writeJson(kokuhoHouseholdsFile, items);
        result = items[idx];
      }
      await audit(req, "kokuho-household.update", { id, fields: Object.keys(patch).filter((k) => k !== "updatedAt" && k !== "updatedBy") });
      return reply(res, 200, result);
    }

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


    // ── 国保保険料の賦課算定（標準仕様書準拠）。members:[{income,age}] から年間保険料を算定 ──
    if (pathname === "/api/kokuho/premium" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const members = Array.isArray(body.members) ? body.members : [];
      if (members.length === 0) return reply(res, 400, { error: "members (income, age) is required" });
      const result = computeKokuhoPremium(members);
      await audit(req, "kokuho.premium-calc", { insuredCount: result.insuredCount, total: result.total });
      return reply(res, 200, result);
    }

    // ── 本算定賦課（標準仕様書 7.確定賦課）。賦課方式(2/3/4)・保険料/税・世帯構成基準を指定して
    //   医療/支援/介護分を個人別に賦課（法定軽減・未就学児均等割1/2・非自発失業・介護2号・賦課限度額）──
    if (pathname === "/api/kokuho/fuka/honsantei" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const members = Array.isArray(body.household?.members) ? body.household.members
        : Array.isArray(body.members) ? body.members : [];
      if (members.length === 0) return reply(res, 400, { error: "household.members[] (income, age 等) が必須です" });
      if (body.houshiki !== undefined && ![2, 3, 4].includes(Number(body.houshiki))) {
        return reply(res, 400, { error: "houshiki は 2(二方式)/3(三方式)/4(四方式) のいずれかです" });
      }
      const result = computeHonsanteiFuka(body);
      await audit(req, "kokuho.honsantei-fuka", {
        houshiki: result.houshiki, basis: result.basis, insuredCount: result.insuredCount,
        reduction: result.reductionName, total: result.total });
      return reply(res, 200, result);
    }

    // ── 更正事由マスタ（標準仕様書 8.異動賦課: 資格異動・所得更正等の更正事由を登録・管理。住民向け文言も任意設定）──
    if (pathname === "/api/kokuho/fuka/kosei-jiyu" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const list = await readJson(kokuhoKoseiJiyuFile, []);
      return reply(res, 200, list);
    }
    if (pathname === "/api/kokuho/fuka/kosei-jiyu" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["資格異動", "所得更正", "その他"];
      if (!b.code || !b.label || !kinds.includes(b.kind)) {
        return reply(res, 400, { error: `code, label, kind(${kinds.join("/")}) が必須です` });
      }
      const list = await readJson(kokuhoKoseiJiyuFile, []);
      const item = { id: crypto.randomUUID(), code: String(b.code), label: String(b.label), kind: b.kind,
        residentText: b.residentText ?? "", nendo: b.nendo ?? "両方", at: new Date().toISOString(), by: actor.id };
      list.unshift(item);
      await writeJson(kokuhoKoseiJiyuFile, list);
      await audit(req, "kokuho-kosei-jiyu.create", { code: item.code, kind: item.kind });
      return reply(res, 201, item);
    }
    {
      const m = pathname.match(/^\/api\/kokuho\/fuka\/kosei-jiyu\/([^/]+)$/);
      if (m && (req.method === "PUT" || req.method === "DELETE")) {
        requireRole(actor, "CLERK");
        const list = await readJson(kokuhoKoseiJiyuFile, []);
        const idx = list.findIndex((x) => x.id === m[1] || x.code === m[1]);
        if (idx < 0) return reply(res, 404, { error: "更正事由が見つかりません" });
        if (req.method === "DELETE") {
          const [removed] = list.splice(idx, 1);
          await writeJson(kokuhoKoseiJiyuFile, list);
          await audit(req, "kokuho-kosei-jiyu.delete", { code: removed.code });
          return reply(res, 200, { deleted: true, code: removed.code });
        }
        const b = await parseBody(req);
        list[idx] = { ...list[idx], ...("label" in b ? { label: String(b.label) } : {}),
          ...("residentText" in b ? { residentText: String(b.residentText) } : {}),
          ...("kind" in b ? { kind: b.kind } : {}), updatedAt: new Date().toISOString(), updatedBy: actor.id };
        await writeJson(kokuhoKoseiJiyuFile, list);
        await audit(req, "kokuho-kosei-jiyu.update", { code: list[idx].code });
        return reply(res, 200, list[idx]);
      }
    }

    // ── 異動賦課更正（標準仕様書 8.異動賦課）。現年度/過年度更正の個別・一括再計算、過年度は賦課期限(遡及年数)で
    //   対象外判定し、調定増減表(更正増/減×区分)・賦課状況集計表を作成。一括再期割(period)にも対応。──
    if (pathname === "/api/kokuho/fuka/kosei" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!Array.isArray(body.targets) || body.targets.length === 0) {
        return reply(res, 400, { error: "targets[]（更正対象。before/after または beforeAmounts/afterAmounts）が必須です" });
      }
      if (body.nendoType && !["現年度", "過年度"].includes(body.nendoType)) {
        return reply(res, 400, { error: "nendoType は 現年度/過年度 のいずれかです" });
      }
      const result = computeIdouKosei(body);
      // 一括再期割(242244/242257): 指定期別へ調定額を按分（最終時点合計を均等割）
      if (body.saikiwari?.enabled) {
        const periods = Math.max(1, Number(body.saikiwari.periods) || 1);
        const targetPeriod = body.saikiwari.period ? Number(body.saikiwari.period) : null;
        const base = Math.floor(result.賦課状況集計表.最終時点 / periods);
        result.再期割 = { periods, targetPeriod, perPeriod: base,
          adjustment: result.賦課状況集計表.最終時点 - base * periods,
          note: targetPeriod ? `第${targetPeriod}期を指定して一括再期割` : "全期で一括再期割" };
      }
      await audit(req, "kokuho.idou-kosei", { nendoType: result.nendoType,
        processed: result.processed, excluded: result.excluded });
      return reply(res, 200, result);
    }

    // ════ 保険料（税）収納（標準仕様書 13.収納）════

    // ── 調定情報の取込・管理（賦課からの当初/更正データ受領・内訳保持・異動前後差額）──
    if (pathname === "/api/kokuho/shuno/chotei" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.payerId || !b.fiscalYear || b.period === undefined) {
        return reply(res, 400, { error: "payerId, fiscalYear, period が必須です" });
      }
      const naiwake = (x) => SHUNO_NAIWAKE.reduce((o, k) => (o[k] = Number(x?.[k]) || 0, o), {});
      const honbun = naiwake(b.honbun);
      const list = await readJson(kokuhoShunoChoteiFile, []);
      const key = (x) => x.payerId === String(b.payerId) && x.fiscalYear === String(b.fiscalYear) && x.period === Number(b.period);
      const prev = list.find(key);
      // 異動前後の差額(241360/241365): 既存調定があれば内訳ごとの差額を算出
      const diff = {};
      for (const k of SHUNO_NAIWAKE) diff[k] = honbun[k] - (prev ? Number(prev.honbun[k]) || 0 : 0);
      const item = {
        id: prev?.id ?? crypto.randomUUID(), payerId: String(b.payerId), fiscalYear: String(b.fiscalYear),
        period: Number(b.period), kind: b.kind === "更正" ? "更正" : "当初", honbun,
        honbunTotal: SHUNO_NAIWAKE.reduce((s, k) => s + honbun[k], 0),
        dueDate: b.dueDate ?? prev?.dueDate ?? null,
        history: [...(prev?.history ?? []), { at: new Date().toISOString(), kind: b.kind ?? "当初", diff, by: actor.id }],
        updatedAt: new Date().toISOString(),
      };
      if (prev) list[list.indexOf(prev)] = item; else list.unshift(item);
      await writeJson(kokuhoShunoChoteiFile, list);
      await audit(req, "kokuho-shuno-chotei.import", { payerId: item.payerId, period: item.period, kind: item.kind });
      return reply(res, prev ? 200 : 201, { ...item, diff });
    }

    // ── 収納状況照会（会計年度・期別・納付義務者で照会。処分(督促/停止/不納欠損)有無も保持）──
    if (pathname === "/api/kokuho/shuno/status" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const list = await readJson(kokuhoShunoChoteiFile, []);
      const payerId = searchParams.get("payerId");
      const fy = searchParams.get("fiscalYear");
      const filtered = list.filter((x) => (!payerId || x.payerId === payerId) && (!fy || x.fiscalYear === fy));
      return reply(res, 200, { count: filtered.length, items: filtered });
    }

    // ── 収納消込（納付データ→本体充当→確定延滞金確定→督促手数料・保留判定）──
    if (pathname === "/api/kokuho/shuno/keshikomi" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.chotei?.honbun && !b.dueDate) return reply(res, 400, { error: "chotei.honbun（内訳調定額）が必須です" });
      if (!Array.isArray(b.payments)) return reply(res, 400, { error: "payments[] が必須です" });
      const result = computeKeshikomi(b);
      await audit(req, "kokuho-shuno.keshikomi", { status: result.status, kanno: result.kanno,
        confirmedEntaikin: result.confirmedEntaikin, hoiryu: result.hoiryu });
      return reply(res, 200, result);
    }

    // ── 日計表/月計表（収入金を内訳別に集計。滞納繰越分は年度別に区分）──
    if (pathname === "/api/kokuho/shuno/shukei" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kind = b.kind === "月計表" ? "月計表" : "日計表";
      const records = Array.isArray(b.records) ? b.records : [];
      const naiwake = SHUNO_NAIWAKE.reduce((o, k) => (o[k] = 0, o), {});
      const byNendo = {}; // 滞納繰越分の年度別
      let total = 0;
      for (const r of records) {
        for (const k of SHUNO_NAIWAKE) { const v = Number(r[k]) || 0; naiwake[k] += v; total += v; }
        if (r.taino) { const y = String(r.fiscalYear ?? "不明"); byNendo[y] = (byNendo[y] || 0) + SHUNO_NAIWAKE.reduce((s, k) => s + (Number(r[k]) || 0), 0); }
      }
      return reply(res, 200, { kind, date: b.date ?? null, naiwake, total, 滞納繰越分_年度別: byNendo,
        note: `${kind}（医療/支援/介護/子ども内訳・滞納繰越分は年度別）` });
    }

    // ════ 滞納管理（標準仕様書 25.滞納管理）════

    // ── 滞納者ランクの自動判定（滞納額・催告回数・差押履歴/中・滞納年数・所得・口座有無・停止中）──
    if (pathname === "/api/kokuho/taino/rank" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      // 一括判定にも対応（targets[] があれば各々判定）
      if (Array.isArray(b.targets)) {
        const ranked = b.targets.map((t) => ({ atenaId: t.atenaId ?? null, ...computeTainoRank(t) }));
        return reply(res, 200, { count: ranked.length, ranked });
      }
      const result = computeTainoRank(b);
      await audit(req, "kokuho-taino.rank", { rank: result.rank });
      return reply(res, 200, result);
    }

    // ── 担当者・地区の一括設定（居住地/ランク/抽出結果から）＋地区毎集計 ──
    if (pathname === "/api/kokuho/taino/tantosha" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const targets = Array.isArray(b.targets) ? b.targets : [];
      if (targets.length === 0) return reply(res, 400, { error: "targets[] が必須です" });
      const mode = ["居住地", "ランク", "抽出結果"].includes(b.mode) ? b.mode : "居住地";
      // 居住地(住所コード)→地区/担当者の対応表、またはランク→担当者の対応表
      const map = b.assignMap || {};
      const assigned = targets.map((t) => {
        const keyVal = mode === "ランク" ? (t.rank ?? computeTainoRank(t).rank)
          : mode === "抽出結果" ? "抽出" : (t.areaCode ?? "");
        const a = map[keyVal] || b.defaultAssign || {};
        return { atenaId: t.atenaId ?? null, mode, area: a.area ?? t.area ?? null,
          tantoMain: a.tantoMain ?? null, tantoSub: a.tantoSub ?? null, group: a.group ?? null };
      });
      // 地区毎集計(241817): 対象者数・未納額
      const byArea = {};
      for (let i = 0; i < targets.length; i++) {
        const area = assigned[i].area ?? "未設定";
        (byArea[area] ??= { count: 0, unpaid: 0, kanno: 0 });
        byArea[area].count++;
        byArea[area].unpaid += Number(targets[i].amount) || 0;
        if ((Number(targets[i].amount) || 0) === 0) byArea[area].kanno++;
      }
      await audit(req, "kokuho-taino.tantosha", { mode, count: assigned.length });
      return reply(res, 200, { mode, assigned, 地区別集計: byArea });
    }

    // ── 分割納付計画の策定（各回の予定日に応じた延滞金自動計算・端数初回・本税/延滞金/督促手数料のみ対応）──
    if (pathname === "/api/kokuho/taino/bunno" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const result = computeBunnoPlan(b);
      if (result.error) return reply(res, 400, { error: result.error });
      await audit(req, "kokuho-taino.bunno", { count: result.count, target: result.target,
        timeExtension: result.timeExtensionUpdated });
      return reply(res, 200, result);
    }

    // ── 交渉経過・約束情報の記録（見出し選択・定型語句・約束(誓約/納付予定/差押予定)・関連者展開）──
    if (pathname === "/api/kokuho/taino/keika" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const list = await readJson(kokuhoTainoKeikaFile, []);
      const atenaId = searchParams.get("atenaId");
      return reply(res, 200, atenaId ? list.filter((x) => x.atenaId === atenaId) : list);
    }
    if (pathname === "/api/kokuho/taino/keika" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || !b.heading) return reply(res, 400, { error: "atenaId, heading(見出し) が必須です" });
      const list = await readJson(kokuhoTainoKeikaFile, []);
      const base = { id: crypto.randomUUID(), atenaId: String(b.atenaId), heading: String(b.heading),
        body: b.body ?? "", place: b.place ?? null,
        promise: b.promise ? { 誓約年月日: b.promise.pledgeDate ?? null, 納付予定年月日: b.promise.payDate ?? null,
          納付予定金額: Number(b.promise.payAmount) || 0, 差押予定年月日: b.promise.seizureDate ?? null,
          来庁予定日: b.promise.visitDate ?? null } : null,
        at: new Date().toISOString(), by: actor.id };
      const created = [base];
      // 関連者にも同様の経過を展開(241870/241872)
      if (Array.isArray(b.relatedAtenaIds) && b.applyToRelated) {
        for (const rid of b.relatedAtenaIds) created.push({ ...base, id: crypto.randomUUID(), atenaId: String(rid), relatedFrom: base.atenaId });
      }
      list.unshift(...created);
      await writeJson(kokuhoTainoKeikaFile, list);
      // 約束の予定日は担当者スケジュールに反映(241875/241887)
      const schedule = base.promise ? [base.promise.納付予定年月日, base.promise.差押予定年月日, base.promise.来庁予定日].filter(Boolean) : [];
      await audit(req, "kokuho-taino.keika", { atenaId: base.atenaId, related: created.length - 1 });
      return reply(res, 201, { created: created.length, item: base, scheduleReflected: schedule });
    }

    // ════ 高額療養費・療養費・出産育児一時金（標準仕様書）════

    // ── 高額療養費の支給記録と支給状況確認 ──
    if (pathname === "/api/kokuho/kogaku/record" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || !b.month || b.amount === undefined) return reply(res, 400, { error: "atenaId, month, amount required" });
      const list = await readJson(kokuhoKogakuFile, []);
      const item = { id: crypto.randomUUID(), atenaId: String(b.atenaId), month: String(b.month),
        amount: Number(b.amount), kind: b.kind ?? "支給", at: new Date().toISOString(), by: actor.id };
      list.unshift(item);
      await writeJson(kokuhoKogakuFile, list);
      await audit(req, "kokuho-kogaku.record", { atenaId: item.atenaId, month: item.month });
      return reply(res, 201, item);
    }
    // 高額療養費貸付支給申請書（該当月の支給状況確認・貸付額9割の端数切捨て注釈・委任状欄）
    if (pathname === "/api/kokuho/kogaku/kashitsuke" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || !b.month || b.estimatedKogaku === undefined) {
        return reply(res, 400, { error: "atenaId, month, estimatedKogaku(高額療養費見込額) required" });
      }
      const list = await readJson(kokuhoKogakuFile, []);
      const alreadyPaid = list.find((k) => k.atenaId === b.atenaId && k.month === b.month && k.kind === "支給");
      if (alreadyPaid) {
        return reply(res, 409, { error: "該当月の高額療養費は支給済みのため貸付対象外です", paid: alreadyPaid.amount });
      }
      const loan = Math.floor(Number(b.estimatedKogaku) * 0.9 / 1000) * 1000; // 貸付は見込額の9割（千円未満切捨て）
      const application = {
        id: crypto.randomUUID(), document: "国民健康保険高額療養費貸付支給申請書",
        atenaId: String(b.atenaId), month: String(b.month), estimatedKogaku: Number(b.estimatedKogaku), loanAmount: loan,
        notes: ["貸付額は高額療養費支給見込額の9割（千円未満切捨て）です"],
        delegationSection: true, // 委任状欄
        at: new Date().toISOString(), by: actor.id,
      };
      list.unshift({ ...application, kind: "貸付" });
      await writeJson(kokuhoKogakuFile, list);
      await audit(req, "kokuho-kogaku.kashitsuke", { atenaId: application.atenaId, loanAmount: loan });
      return reply(res, 201, application);
    }
    // 多数回該当の判定（直近12か月で高額該当3回以上→4回目から多数回該当・上限引下げ）
    if (pathname === "/api/kokuho/kogaku/tasukai" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || !b.month) return reply(res, 400, { error: "atenaId, month required" });
      const list = await readJson(kokuhoKogakuFile, []);
      const target = new Date(`${b.month}-01`);
      const from = new Date(target);
      from.setMonth(from.getMonth() - 11);
      const fromStr = from.toISOString().slice(0, 7);
      // 高額該当引継情報（転入時等の引継 carriedCount）も合算できる
      const hits = list.filter((k) => k.atenaId === b.atenaId && k.kind === "支給" && k.month >= fromStr && k.month <= b.month).length
        + (Number(b.carriedCount) || 0);
      const tasukai = hits >= 3;
      await audit(req, "kokuho-kogaku.tasukai", { atenaId: b.atenaId, hits, tasukai });
      return reply(res, 200, { atenaId: b.atenaId, month: b.month, hitsInLast12Months: hits, tasukai,
        cap: tasukai ? 44400 : null, note: tasukai ? "多数回該当（4回目以降の上限額を適用）" : "多数回該当に未到達" });
    }

    // ── 療養費の支給申請（請求・領収書兼支払金口座振替依頼書＋委任状欄）──
    if (pathname === "/api/kokuho/ryoyohi/apply" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || b.amount === undefined) return reply(res, 400, { error: "atenaId, amount required" });
      const list = await readJson(kokuhoRyoyohiFile, []);
      const application = {
        id: crypto.randomUUID(), atenaId: String(b.atenaId), amount: Number(b.amount),
        documents: ["国民健康保険療養費支給申請書（委任状欄つき）", "請求・領収書兼支払金口座振替依頼書"],
        delegationSection: true, accountNumber: b.accountNumber ? `****${String(b.accountNumber).slice(-4)}` : "",
        status: "受理", at: new Date().toISOString(), by: actor.id,
      };
      list.unshift(application);
      await writeJson(kokuhoRyoyohiFile, list);
      await audit(req, "kokuho-ryoyohi.apply", { atenaId: application.atenaId, amount: application.amount });
      return reply(res, 201, application);
    }

    // ── 出産育児一時金（資格チェック結果リスト・差額支給可能者の抽出）──
    if (pathname === "/api/kokuho/shussan/check" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.claims)) return reply(res, 400, { error: "claims[]{atenaId,paidAmount} required" });
      const insured = await readJson(kokuhoInsuredFile, []);
      const ids = new Set(insured.map((p) => p.atenaId ?? p.id));
      const STATUTORY = 500000; // 出産育児一時金の法定額（令和5年4月以降）
      const results = b.claims.map((c) => ({
        atenaId: c.atenaId,
        eligible: ids.has(c.atenaId),
        issue: ids.has(c.atenaId) ? "" : "被保険者資格なし（資格喪失等）",
        paidAmount: Number(c.paidAmount) || 0,
      }));
      const list = await readJson(kokuhoShussanFile, []);
      for (const r of results) if (r.eligible) list.unshift({ id: crypto.randomUUID(), ...r, at: new Date().toISOString() });
      await writeJson(kokuhoShussanFile, list);
      await audit(req, "kokuho-shussan.check", { total: results.length, ineligible: results.filter((r) => !r.eligible).length });
      return reply(res, 200, { statutoryAmount: STATUTORY, results });
    }
    if (pathname === "/api/kokuho/shussan/sagaku" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const STATUTORY = 500000;
      const list = await readJson(kokuhoShussanFile, []);
      const candidates = list.filter((r) => r.paidAmount > 0 && r.paidAmount < STATUTORY)
        .map((r) => ({ atenaId: r.atenaId, paidAmount: r.paidAmount, sagaku: STATUTORY - r.paidAmount }));
      await audit(req, "kokuho-shussan.sagaku", { count: candidates.length });
      return reply(res, 200, { statutoryAmount: STATUTORY, count: candidates.length, candidates });
    }

    // ════ 端末アクセス制御・ログ出力・公開範囲・名寄せ（標準仕様書 共通系）════

    // ── 端末の登録と検証（不特定多数の端末からのアクセスを制御）──
    if (pathname === "/api/kokuho/terminals" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      if (!b.terminalId) return reply(res, 400, { error: "terminalId required" });
      const terminals = await readJson(kokuhoTerminalsFile, []);
      terminals.unshift({ terminalId: String(b.terminalId), location: b.location ?? "", registeredBy: actor.id, at: new Date().toISOString() });
      await writeJson(kokuhoTerminalsFile, terminals);
      await audit(req, "kokuho-terminal.register", { terminalId: b.terminalId });
      return reply(res, 201, { terminalId: b.terminalId, allowed: true });
    }
    if (pathname === "/api/kokuho/terminals/verify" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const terminals = await readJson(kokuhoTerminalsFile, []);
      const allowed = terminals.some((t) => t.terminalId === String(b.terminalId ?? ""));
      const logs = await readJson(kokuhoAccessLogFile, []);
      logs.unshift({ terminalId: b.terminalId ?? "", allowed, at: new Date().toISOString(), by: actor.id });
      await writeJson(kokuhoAccessLogFile, logs.slice(0, 5000));
      if (!allowed) return reply(res, 403, { error: "未登録端末からのアクセスは許可されていません", terminalId: b.terminalId });
      return reply(res, 200, { terminalId: b.terminalId, allowed: true });
    }
    // 保持しているログのリスト出力
    if (pathname === "/api/kokuho/logs" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const logs = await readJson(kokuhoAccessLogFile, []);
      return reply(res, 200, { count: logs.length, logs: logs.slice(0, 500) });
    }

    // ── 連絡先・メモの公開範囲（組織単位／業務単位）と複数ファイル添付 ──
    if (pathname === "/api/kokuho/contacts" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const scopes = ["全体", "組織", "業務"];
      if (!b.kind || !scopes.includes(b.scope ?? "全体")) return reply(res, 400, { error: `kind(連絡先|メモ), scope(${scopes.join("/")}) required` });
      const contacts = await readJson(kokuhoContactsFile, []);
      const item = { id: crypto.randomUUID(), kind: b.kind === "メモ" ? "メモ" : "連絡先",
        scope: b.scope ?? "全体", orgId: b.orgId ?? "", taskId: b.taskId ?? "",
        content: b.content ?? "", attachments: Array.isArray(b.attachments) ? b.attachments : [], // 複数ファイル添付（メタ）
        by: actor.id, at: new Date().toISOString() };
      contacts.unshift(item);
      await writeJson(kokuhoContactsFile, contacts);
      await audit(req, "kokuho-contact.create", { kind: item.kind, scope: item.scope });
      return reply(res, 201, item);
    }
    if (pathname === "/api/kokuho/contacts" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const contacts = await readJson(kokuhoContactsFile, []);
      const orgId = searchParams.get("orgId") ?? "";
      const taskId = searchParams.get("taskId") ?? "";
      // 公開範囲の判定: 全体は誰でも、組織は同一orgId、業務は同一taskId のみ閲覧可
      const visible = contacts.filter((c) =>
        c.scope === "全体" || (c.scope === "組織" && c.orgId === orgId) || (c.scope === "業務" && c.taskId === taskId));
      return reply(res, 200, visible);
    }

    // ── 名寄せの自動実行（判定条件は任意指定）──
    if (pathname === "/api/kokuho/nayose/run" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.records) || !Array.isArray(b.keys) || !b.keys.length) {
        return reply(res, 400, { error: "records[] と keys[]（判定条件: 例 fullName,birthDate）が必須です" });
      }
      const groups = {};
      for (const r of b.records) {
        const key = b.keys.map((k) => String(r[k] ?? "")).join("|");
        (groups[key] ??= []).push(r);
      }
      const merged = Object.entries(groups).filter(([, rows]) => rows.length > 1)
        .map(([key, rows]) => ({ key, count: rows.length, ids: rows.map((r) => r.id ?? r.atenaId) }));
      await audit(req, "kokuho-nayose.run", { keys: b.keys, groups: merged.length });
      return reply(res, 200, { keys: b.keys, duplicateGroups: merged });
    }

    // ── 名寄せ（GPU埋め込み曖昧マッチ）：表記ゆれを類似度で拾う。──
    //   env NAYOSE_GPU_BASE があれば nayose-gpu サイドカー(/match)へ委譲、無ければ文字列フォールバック。
    if (pathname === "/api/kokuho/nayose/fuzzy" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const query = b.query ?? null;
      const candidates = Array.isArray(b.candidates) ? b.candidates : [];
      if (!query || !candidates.length) return reply(res, 400, { error: "query{name,kana,birthDate,address} と candidates[] が必須です" });
      const threshold = Number(b.threshold ?? 0.85);
      const topK = Number(b.topK ?? 5);
      const gpuBase = (typeof process !== "undefined" && process.env && process.env.NAYOSE_GPU_BASE) || null;

      // NFKC 正規化（全半角・互換文字）— フォールバック用
      const norm = (s) => String(s ?? "").normalize("NFKC").trim();
      // 旧字体など簡易な異体字を畳む（フォールバックの精度補助）
      const fold = (s) => norm(s).replace(/[髙﨑]/g, (c) => ({ "髙": "高", "﨑": "崎" }[c] ?? c));
      const bigrams = (s) => { const t = fold(s).replace(/\s/g, ""); const g = new Set(); for (let i = 0; i < t.length - 1; i++) g.add(t.slice(i, i + 2)); return g; };
      const dice = (a, ga) => (cand) => {
        const gb = bigrams(`${cand.name ?? ""}${cand.kana ?? ""}${cand.address ?? ""}`);
        if (!ga.size && !gb.size) return 1;
        let inter = 0; for (const g of ga) if (gb.has(g)) inter++;
        const sim = (2 * inter) / (ga.size + gb.size || 1);
        // 生年月日一致は強いシグナル
        const bdMatch = query.birthDate && cand.birthDate && query.birthDate === cand.birthDate ? 0.1 : 0;
        return Math.min(1, sim + bdMatch);
      };

      if (gpuBase) {
        try {
          const ctrl = new AbortController();
          const timer = setTimeout(() => ctrl.abort(), 8000);
          const r = await fetch(`${gpuBase.replace(/\/$/, "")}/match`, {
            method: "POST", signal: ctrl.signal,
            headers: { "content-type": "application/json" },
            body: JSON.stringify({ query, candidates, threshold, topK }),
          });
          clearTimeout(timer);
          if (r.ok) {
            const body = await r.json();
            await audit(req, "kokuho-nayose.fuzzy", { engine: "gpu", device: body.device, matches: body.count });
            return reply(res, 200, { engine: `GPU埋め込み（${body.device}/${body.model}）`, threshold,
              count: body.count, matches: body.matches, ranked: body.ranked,
              note: "埋め込みコサイン類似で表記ゆれを名寄せ。nayose-gpuサイドカー委譲" });
          }
        } catch {
          // サイドカー到達不可 → フォールバックへ
        }
      }
      // フォールバック: 文字バイグラムDice係数＋生年月日一致
      const ga = bigrams(`${query.name ?? ""}${query.kana ?? ""}${query.address ?? ""}`);
      const scorer = dice(query, ga);
      const ranked = candidates.map((c) => ({ candidate: c, score: Math.round(scorer(c) * 10000) / 10000 }))
        .sort((x, y) => y.score - x.score);
      const matches = ranked.filter((r) => r.score >= threshold).slice(0, topK);
      await audit(req, "kokuho-nayose.fuzzy", { engine: "fallback", matches: matches.length });
      return reply(res, 200, { engine: "文字列フォールバック（バイグラムDice＋生年月日）", threshold,
        count: matches.length, matches, ranked: ranked.slice(0, topK),
        note: "NAYOSE_GPU_BASE 未設定/到達不可のため文字列ベースで名寄せ（表記ゆれはNFKC＋異体字畳み込みで吸収）" });
    }

    // ════ 台帳画面個人番号・一括チェック・通知/公印・保存期間・第三者行為/不正利得（標準仕様書）════

    // ── 台帳画面で対象者を特定した際の個人番号確認（台帳種別ごと・監査必須）──
    if (pathname === "/api/kokuho/mynumber/confirm" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const daichos = ["資格", "賦課", "収納", "給付"];
      if (!b.atenaId || !daichos.includes(b.daicho)) return reply(res, 400, { error: `atenaId, daicho(${daichos.join("/")}) required` });
      await audit(req, "kokuho-mynumber.confirm", { atenaId: b.atenaId, daicho: b.daicho });
      return reply(res, 200, { atenaId: String(b.atenaId), daicho: b.daicho,
        myNumber: "************", note: "台帳により個人番号を確認（参照実装はマスク表示・監査記録）" });
    }

    // ── 画面項目チェック（エラーメッセージを可能な限り一度に出力）──
    if (pathname === "/api/kokuho/validate" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const record = b.record ?? {};
      const errors = [];
      if (!record.atenaId) errors.push({ field: "atenaId", message: "宛名番号は必須です" });
      if (!record.name) errors.push({ field: "name", message: "氏名は必須です" });
      if (record.birthDate && !/^\d{4}-\d{2}-\d{2}$/.test(record.birthDate)) errors.push({ field: "birthDate", message: "生年月日はYYYY-MM-DD形式です" });
      if (record.income !== undefined && Number(record.income) < 0) errors.push({ field: "income", message: "所得は0以上です" });
      if (record.startDate && record.endDate && record.startDate > record.endDate) errors.push({ field: "endDate", message: "終了日は開始日以降です" });
      return reply(res, 200, { ok: errors.length === 0, errors, note: "項目チェックは全件を一度に出力" });
    }

    // ── 通知の作成（首長名/区長名の切替・電子公印印字の通数記録）──
    if (pathname === "/api/kokuho/notices/print" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const issuers = ["首長名", "区長名"];
      if (!b.noticeType || !issuers.includes(b.issuer)) return reply(res, 400, { error: `noticeType, issuer(${issuers.join("/")}) required` });
      if (b.issuer === "区長名" && !b.wardName) return reply(res, 400, { error: "区長名の場合は wardName が必須です" });
      const copies = Math.max(1, Number(b.copies) || 1);
      const notices = await readJson(kokuhoNoticesFile, []);
      const item = { id: crypto.randomUUID(), noticeType: String(b.noticeType), issuer: b.issuer,
        issuerName: b.issuer === "区長名" ? `${b.wardName}区長` : "○○市長",
        copies, electronicSeal: b.electronicSeal !== false, at: new Date().toISOString(), by: actor.id };
      notices.unshift(item);
      await writeJson(kokuhoNoticesFile, notices);
      await audit(req, "kokuho-notices.print", { noticeType: item.noticeType, issuer: item.issuer, copies });
      return reply(res, 201, item);
    }
    // 電子公印の印字通数確認
    if (pathname === "/api/kokuho/seal/stats" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const notices = await readJson(kokuhoNoticesFile, []);
      const sealed = notices.filter((n) => n.electronicSeal);
      const total = sealed.reduce((s, n) => s + (n.copies ?? 1), 0);
      return reply(res, 200, { electronicSealPrints: total, documents: sealed.length,
        byNoticeType: sealed.reduce((m, n) => ({ ...m, [n.noticeType]: (m[n.noticeType] ?? 0) + n.copies }), {}) });
    }

    // ── 保存期間の任意指定（市区町村ごと）──
    if (pathname === "/api/kokuho/config/retention" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      if (!b.docKind || !Number(b.years)) return reply(res, 400, { error: "docKind, years required" });
      const config = await readJson(kokuhoConfigFile, {});
      config.retention = { ...(config.retention ?? {}), [String(b.docKind)]: Number(b.years) };
      await writeJson(kokuhoConfigFile, config);
      await audit(req, "kokuho-config.retention", { docKind: b.docKind, years: Number(b.years) });
      return reply(res, 200, config.retention);
    }

    // ── 第三者行為求償: 候補レセプト抽出（事故発生日〜症状固定日・特記事項10マル交）＋確認リスト ──
    if (pathname === "/api/kokuho/daisansha/candidates" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const receipts = Array.isArray(b.receipts) ? b.receipts : [];
      if (!b.accidentDate || !receipts.length) return reply(res, 400, { error: "accidentDate, receipts[]{id,serviceDate,tokki?} required" });
      const fixed = b.fixedDate ?? "9999-12-31"; // 症状固定まで（未固定なら無期限）
      const candidates = receipts.filter((r) =>
        (r.serviceDate >= b.accidentDate && r.serviceDate <= fixed) ||
        String(r.tokki ?? "").includes("10") || String(r.tokki ?? "").includes("マル交"));
      const list = candidates.map((r) => ({ receiptId: r.id, serviceDate: r.serviceDate,
        tokki: r.tokki ?? "", reason: String(r.tokki ?? "").includes("10") || String(r.tokki ?? "").includes("マル交") ? "特記事項10（マル交）" : "事故発生日〜症状固定日" }));
      await audit(req, "kokuho-daisansha.candidates", { receipts: receipts.length, candidates: candidates.length });
      return reply(res, 200, { accidentDate: b.accidentDate, fixedDate: b.fixedDate ?? null,
        count: candidates.length, checkList: list });
    }

    // ── 不納欠損候補の抽出＋確認リスト（第三者行為・不当利得・不正利得）──
    if (pathname === "/api/kokuho/funo-kesson/candidates" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["第三者行為", "不当利得", "不正利得"];
      const items = Array.isArray(b.items) ? b.items : [];
      if (!kinds.includes(b.kind) || !items.length) return reply(res, 400, { error: `kind(${kinds.join("/")}), items[]{id,debtor,unpaid} required` });
      const candidates = items.filter((i) => Number(i.unpaid ?? 0) > 0);
      const list = candidates.map((i) => ({ id: String(i.id), debtor: i.debtor ?? "", unpaid: Number(i.unpaid), kind: b.kind, status: "不納欠損候補" }));
      await audit(req, "kokuho-funo-kesson.candidates", { kind: b.kind, candidates: candidates.length });
      return reply(res, 200, { kind: b.kind, count: candidates.length, checkList: list });
    }

    // ════ △→◎昇格 第1弾: 国保台帳基盤（管理項目スキーマ駆動CRUD）・行政区管理・排他制御ほか ════

    const kokuhoDaichoFile = join(dataRoot, "kokuho-daicho.json");
    const kokuhoLocksFile = join(dataRoot, "kokuho-locks.json");

    // 台帳種別ごとの管理項目スキーマ（標準仕様書【管理項目】準拠の既定セット。ADMINで追加・拡張可）
    const KOKUHO_DAICHO_SCHEMAS = {
      "資格": ["世帯番号", "個人番号", "資格取得日", "資格喪失日", "取得事由", "喪失事由", "所管区", "資格区"],
      "賦課": ["対象年度", "税目（科目）", "所得割", "均等割", "平等割", "限度額", "軽減区分", "賦課区"],
      "収納": ["税目（科目）（収納）", "期別", "収納日", "収納額", "収納方法", "領収書番号"],
      "滞納": ["税目（科目）（国保、国資、滞納）", "滞納額", "督促状発送日", "催告書発送日", "滞納原因", "納付誓約"],
      "送付先": ["送付先氏名カナ", "送付先氏名漢字", "送付先郵便番号", "送付先住所", "送付開始日", "送付終了日"],
      "限度額認定": ["対象年度", "限度額申請日", "限度額認定日", "適用区分", "有効開始日", "有効終了日", "長期入院該当"],
      "特定疾病認定": ["特定疾病認定日", "特定疾病開始日", "特定疾病終了日", "疾病区分", "自己負担限度額"],
      "一部負担金減免": ["一部負担金減免申請日", "減免事由", "減免割合", "認定開始日", "認定終了日"],
      "境界層": ["適用開始年月日", "適用終了年月日", "境界層区分", "措置後基準（段階・区分等）"],
      "葬祭費": ["申請日", "死亡日", "葬祭執行日", "申請者", "支給額", "支給日", "支給状況"],
      "出産育児一時金": ["申請日", "出産予定日", "出産日", "支給額", "直接支払制度利用", "支給状況"],
      "口座": ["金融機関コード", "支店コード", "口座種別", "口座番号", "名義人カナ", "振替開始日", "振替終了日"],
      "金融機関マスタ": ["金融機関有効開始日", "金融機関有効終了日", "指定金融区分コード", "電子納付対応有無コード",
        "店舗有効開始日", "店舗有効終了日"],
      "プリンタ": ["プリンタ名", "設置場所", "対応帳票", "既定区分"],
    };

    // ── 台帳スキーマの照会・追加 ──
    if (pathname === "/api/kokuho/daicho/schemas" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const config = await readJson(kokuhoConfigFile, {});
      const merged = { ...KOKUHO_DAICHO_SCHEMAS, ...(config.customSchemas ?? {}) };
      return reply(res, 200, { kinds: Object.keys(merged), schemas: merged,
        note: "標準仕様書の【管理項目】準拠。団体追加項目はADMINでスキーマ拡張可能" });
    }
    if (pathname === "/api/kokuho/daicho/schemas" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      if (!b.kind || !Array.isArray(b.fields) || !b.fields.length) return reply(res, 400, { error: "kind, fields[] required" });
      const config = await readJson(kokuhoConfigFile, {});
      config.customSchemas = { ...(config.customSchemas ?? {}), [String(b.kind)]: b.fields.map(String) };
      await writeJson(kokuhoConfigFile, config);
      await audit(req, "kokuho-daicho.schema-add", { kind: b.kind, fields: b.fields.length });
      return reply(res, 201, { kind: String(b.kind), fields: b.fields.map(String) });
    }

    // ── 台帳レコードの登録・修正・削除・照会（管理項目スキーマ検証＋履歴保持＋監査）──
    {
      const m = pathname.match(/^\/api\/kokuho\/daicho\/([^/]+)(?:\/([^/]+))?$/);
      if (m && m[1] !== "schemas") {
        const kind = decodeURIComponent(m[1]);
        const config = await readJson(kokuhoConfigFile, {});
        const schemas = { ...KOKUHO_DAICHO_SCHEMAS, ...(config.customSchemas ?? {}) };
        const schema = schemas[kind];
        if (!schema) return reply(res, 404, { error: "unknown daicho kind", kinds: Object.keys(schemas) });
        const store = await readJson(kokuhoDaichoFile, {});
        const records = store[kind] ?? [];
        if (!m[2] && req.method === "GET") {
          requireRole(actor, "CLERK");
          const atenaId = searchParams.get("atenaId") ?? "";
          const hits = atenaId ? records.filter((r) => r.atenaId === atenaId) : records;
          return reply(res, 200, { kind, schema, count: hits.length, records: hits.slice(0, 200) });
        }
        if (!m[2] && req.method === "POST") {
          requireRole(actor, "CLERK");
          const b = await parseBody(req);
          const fields = b.fields ?? {};
          const unknown = Object.keys(fields).filter((k) => !schema.includes(k));
          if (unknown.length) return reply(res, 400, { error: "スキーマ外の管理項目", unknown, schema });
          const record = { id: crypto.randomUUID(), atenaId: String(b.atenaId ?? ""), fields,
            history: [{ action: "登録", at: new Date().toISOString(), by: actor.id }],
            createdAt: new Date().toISOString() };
          records.unshift(record);
          store[kind] = records;
          await writeJson(kokuhoDaichoFile, store);
          await audit(req, "kokuho-daicho.create", { kind, atenaId: record.atenaId });
          return reply(res, 201, record);
        }
        if (m[2] && req.method === "PUT") {
          requireRole(actor, "CLERK");
          const idx = records.findIndex((r) => r.id === m[2]);
          if (idx === -1) return reply(res, 404, { error: "record not found" });
          const b = await parseBody(req);
          const patch = b.fields ?? {};
          const unknown = Object.keys(patch).filter((k) => !schema.includes(k));
          if (unknown.length) return reply(res, 400, { error: "スキーマ外の管理項目", unknown });
          records[idx].history.push({ action: "修正", before: { ...records[idx].fields }, at: new Date().toISOString(), by: actor.id });
          records[idx].fields = { ...records[idx].fields, ...patch };
          store[kind] = records;
          await writeJson(kokuhoDaichoFile, store);
          await audit(req, "kokuho-daicho.update", { kind, id: m[2] });
          return reply(res, 200, records[idx]);
        }
        if (m[2] && req.method === "DELETE") {
          requireRole(actor, "ADMIN");
          const idx = records.findIndex((r) => r.id === m[2]);
          if (idx === -1) return reply(res, 404, { error: "record not found" });
          records.splice(idx, 1);
          store[kind] = records;
          await writeJson(kokuhoDaichoFile, store);
          await audit(req, "kokuho-daicho.delete", { kind, id: m[2] });
          return reply(res, 204, {});
        }
      }
    }

    // ── 行政区管理（指定都市向け: 所管区・資格区・賦課区・診療区・受付区＋区別の抽出除外選択）──
    if (pathname === "/api/kokuho/ward-info" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const WARD_KINDS = ["所管区", "資格区", "賦課区", "診療区", "受付区"];
      if (!b.atenaId || !b.wards) return reply(res, 400, { error: `atenaId, wards{${WARD_KINDS.join("/")}} required` });
      const unknown = Object.keys(b.wards).filter((k) => !WARD_KINDS.includes(k));
      if (unknown.length) return reply(res, 400, { error: "区種別が不正", unknown, kinds: WARD_KINDS });
      await audit(req, "kokuho-ward.set", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId), wards: b.wards,
        note: "行政区管理（指定都市）: 5種の区情報を管理" });
    }
    if (pathname === "/api/kokuho/ward-info/extract" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!b.staffWard || !rows.length) return reply(res, 400, { error: "staffWard, rows[]{atenaId,fukaWard} required" });
      const excludeOther = b.excludeOtherWards !== false; // 既定: 他区除外（選択可）
      const hits = excludeOther ? rows.filter((r) => r.fukaWard === b.staffWard) : rows;
      await audit(req, "kokuho-ward.extract", { staffWard: b.staffWard, excludeOther, hits: hits.length });
      return reply(res, 200, { staffWard: b.staffWard, excludeOtherWards: excludeOther, count: hits.length, rows: hits });
    }

    // ── 排他制御（同一住民の異動処理中ロック＝閲覧以外を禁止）──
    if (pathname === "/api/kokuho/locks" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId) return reply(res, 400, { error: "atenaId required" });
      const locks = await readJson(kokuhoLocksFile, {});
      const existing = locks[String(b.atenaId)];
      if (existing && existing.by !== actor.id) {
        return reply(res, 423, { error: "他の職員が異動処理中です（閲覧のみ可能）", lockedBy: existing.by, since: existing.at });
      }
      locks[String(b.atenaId)] = { by: actor.id, at: new Date().toISOString() };
      await writeJson(kokuhoLocksFile, locks);
      await audit(req, "kokuho-locks.acquire", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId), locked: true, by: actor.id });
    }
    if (pathname === "/api/kokuho/locks/release" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const locks = await readJson(kokuhoLocksFile, {});
      delete locks[String(b.atenaId ?? "")];
      await writeJson(kokuhoLocksFile, locks);
      await audit(req, "kokuho-locks.release", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId ?? ""), locked: false });
    }

    // ── 所得区分の6年分判定・住登外連携運用・金融機関統廃合の一括変更・住記履歴確認 ──
    if (pathname === "/api/kokuho/shotoku-kubun/judge" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const years = Array.isArray(b.years) ? b.years : [];
      if (!b.atenaId || !years.length) return reply(res, 400, { error: "atenaId, years[]{fiscalYear,income} required" });
      const judged = years.slice(0, 6).map((y) => ({ fiscalYear: y.fiscalYear,
        所得区分: Number(y.income ?? 0) <= 0 ? "低所得" : Number(y.income) < 6000000 ? "一般" : "上位所得" }));
      await audit(req, "kokuho-shotoku-kubun.judge", { atenaId: b.atenaId, years: judged.length });
      return reply(res, 200, { atenaId: String(b.atenaId), maxYears: 6, judged,
        note: "現年含め6年分の所得区分判定" });
    }
    if (pathname === "/api/kokuho/jutogai/link" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || !b.source) return reply(res, 400, { error: "atenaId, source(市町村システム) required" });
      await audit(req, "kokuho-jutogai.link", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId), managedBy: String(b.source),
        mode: "住登外情報は市町村システム管理→国保へ連携（国保側の二重登録を抑止）" });
    }
    if (pathname === "/api/kokuho/bank-master/merge" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const accounts = Array.isArray(b.accounts) ? b.accounts : [];
      if (!b.fromBank || !b.toBank || !accounts.length) return reply(res, 400, { error: "fromBank, toBank, accounts[] required" });
      const changed = accounts.filter((a) => a.bankCode === b.fromBank && (!b.fromBranch || a.branchCode === b.fromBranch))
        .map((a) => ({ accountId: a.accountId, bankCode: b.toBank, branchCode: b.toBranch ?? a.branchCode }));
      await audit(req, "kokuho-bank-master.merge", { fromBank: b.fromBank, toBank: b.toBank, changed: changed.length });
      return reply(res, 200, { kind: "金融機関統廃合の一括口座変更", changed: changed.length, accounts: changed });
    }
    if (pathname === "/api/kokuho/juki-history" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId) return reply(res, 400, { error: "atenaId required" });
      const history = Array.isArray(b.history) ? b.history : [];
      await audit(req, "kokuho-juki-history.view", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId),
        includes: ["転居履歴", "世帯変更履歴", "除票"], count: history.length, history,
        note: "住民記録情報を履歴・除票含めて確認" });
    }

    // ════ △→◎昇格 第2弾: 調定表・収滞納連携・前納・給付申請・納付書・副本・按分・資格異動ほか ════

    const kokuhoFukuhonFile = join(dataRoot, "kokuho-fukuhon.json");
    const kokuhoCertReturnFile = join(dataRoot, "kokuho-cert-return.json");

    // ── 調定表（医療分・後期高齢者支援金分・介護納付金分の区分×現住/不現住×収納反映年度）──
    if (pathname === "/api/kokuho/chotei-hyo" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!b.fiscalYear || !rows.length) return reply(res, 400, { error: "fiscalYear, rows[]{atenaId,medical,support,longterm,genju,shunoYear} required" });
      const sum = (list) => list.reduce((a, r) => ({
        医療分: a["医療分"] + Number(r.medical ?? 0),
        後期高齢者支援金分: a["後期高齢者支援金分"] + Number(r.support ?? 0),
        介護納付金分: a["介護納付金分"] + Number(r.longterm ?? 0),
      }), { 医療分: 0, 後期高齢者支援金分: 0, 介護納付金分: 0 });
      const genju = rows.filter((r) => r.genju !== false);
      const fugenju = rows.filter((r) => r.genju === false);
      const byShunoYear = {};
      for (const r of rows) {
        const y = r.shunoYear ?? b.fiscalYear;
        (byShunoYear[y] ??= []).push(r);
      }
      await audit(req, "kokuho-chotei-hyo", { fiscalYear: b.fiscalYear, rows: rows.length });
      return reply(res, 200, { document: "調定表", fiscalYear: Number(b.fiscalYear),
        区分別合計: sum(rows), 現住分: sum(genju), 不現住分: sum(fugenju),
        収納反映年度別: Object.fromEntries(Object.entries(byShunoYear).map(([y, list]) => [y, sum(list)])) });
    }

    // ── 収滞納の別システム管理運用（連携登録＋各種日付の管理）──
    if (pathname === "/api/kokuho/shuno-link" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId) return reply(res, 400, { error: "atenaId required" });
      const DATE_KINDS = ["更正日", "調定日", "収納日", "督促状発送日", "催告書発送日", "時効起算日", "不納欠損日"];
      const dates = Object.fromEntries(Object.entries(b.dates ?? {}).filter(([k]) => DATE_KINDS.includes(k)));
      await audit(req, "kokuho-shuno-link", { atenaId: b.atenaId, dates: Object.keys(dates).length });
      return reply(res, 200, { atenaId: String(b.atenaId), mode: "収滞納情報を別システム管理→国保へ連携",
        registeredDates: dates, supportedDateKinds: DATE_KINDS });
    }

    // ── 入国初年度の前納（1〜3月入国者の当年度分前納＋対象者管理）──
    if (pathname === "/api/kokuho/zennou" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const subjects = Array.isArray(b.subjects) ? b.subjects : [];
      if (!subjects.length) return reply(res, 400, { error: "subjects[]{atenaId,entryDate,annualPremium} required" });
      const results = subjects.map((s) => {
        const month = Number(String(s.entryDate ?? "").slice(5, 7));
        const eligible = month >= 1 && month <= 3;
        return { atenaId: s.atenaId, entryDate: s.entryDate, eligible,
          ...(eligible ? { 前納額: Number(s.annualPremium ?? 0), note: "入国初年度（1〜3月入国）の保険料を前納" } : {}) };
      });
      await audit(req, "kokuho-zennou", { subjects: results.length, eligible: results.filter((r) => r.eligible).length });
      return reply(res, 200, { kind: "入国初年度の前納対象者管理", results });
    }

    // ── 高齢受給者証の様式選択（カード/紙）──
    if (pathname === "/api/kokuho/kourei-jukyu/format" && req.method === "PUT") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const formats = ["カード様式", "紙様式"];
      if (!formats.includes(b.format)) return reply(res, 400, { error: `format(${formats.join("/")})` });
      const config = await readJson(kokuhoConfigFile, {});
      config.koureiJukyuFormat = b.format;
      await writeJson(kokuhoConfigFile, config);
      await audit(req, "kokuho-kourei-jukyu.format", { format: b.format });
      return reply(res, 200, { format: b.format, note: "高齢受給者証の様式を団体選択" });
    }

    // ── 給付申請の運用設定・受付（支給方法初期値・任意申請者・振込先口座連携・OCR口座・メモ対応記録）──
    if (pathname === "/api/kokuho/kyufu/config" && req.method === "PUT") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const methods = ["口座振込", "窓口払い", "口座振替"];
      const config = await readJson(kokuhoConfigFile, {});
      if (b.defaultPayMethod !== undefined) {
        if (!methods.includes(b.defaultPayMethod)) return reply(res, 400, { error: `defaultPayMethod(${methods.join("/")})` });
        config.kyufuDefaultPayMethod = b.defaultPayMethod;
      }
      await writeJson(kokuhoConfigFile, config);
      await audit(req, "kokuho-kyufu.config", { defaultPayMethod: config.kyufuDefaultPayMethod });
      return reply(res, 200, { defaultPayMethod: config.kyufuDefaultPayMethod ?? "口座振込",
        note: "支給方法の初期値を任意設定" });
    }
    if (pathname === "/api/kokuho/kyufu/apply" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.kind) return reply(res, 400, { error: "kind(葬祭費/出産育児一時金/療養費等) required" });
      const config = await readJson(kokuhoConfigFile, {});
      const applicant = b.applicant ?? { role: "世帯主" };
      const account = b.ocrScannedAccount
        ? { ...b.ocrScannedAccount, source: "OCRスキャン取込（申請書様式OCR対応）" }
        : b.useRegisteredAccount === true
          ? { source: "国保システム登録済み振込先口座を引用" }
          : (b.account ?? null);
      await audit(req, "kokuho-kyufu.apply", { kind: b.kind, applicantRole: applicant.role ?? "任意" });
      return reply(res, 200, { kind: String(b.kind),
        applicant, applicantNote: "申請者は世帯主に限らず任意の対象者を登録可能",
        payMethod: b.payMethod ?? config.kyufuDefaultPayMethod ?? "口座振込",
        account, memo: b.memo ? { text: String(b.memo), kind: "申請受付時の対応記録（メモ機能）" } : null });
    }

    // ── 納付書（不当利得・不正利得の科目名称表示＋案件特定キー・確認番号印字）──
    if (pathname === "/api/kokuho/nofusho/print" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["不当利得", "不正利得", "保険料"];
      if (!kinds.includes(b.kind) || !Number(b.amount)) return reply(res, 400, { error: `kind(${kinds.join("/")}), amount required` });
      await audit(req, "kokuho-nofusho.print", { kind: b.kind });
      return reply(res, 200, { document: "納付書",
        科目名称: b.kind === "保険料" ? "国民健康保険料" : `国民健康保険${b.kind}返還金`,
        amount: Number(b.amount),
        案件特定キー: b.caseKey ?? `KK-${Date.now().toString(36).toUpperCase()}`,
        確認番号: b.kakuninNo ?? String(Math.floor(Math.random() * 900000) + 100000),
        note: "科目名称に利得種別を表示＋案件特定キー・確認番号を印字" });
    }

    // ── 共通納税IFSへのアップロードデータ作成 ──
    if (pathname === "/api/kokuho/kyotsu-nozei/upload" && 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,period,amount} required" });
      await audit(req, "kokuho-kyotsu-nozei.upload", { rows: rows.length });
      return reply(res, 200, { target: "共通納税インターフェースシステム", file: "kyotsu_nozei_upload.csv",
        count: rows.length, rows });
    }

    // ── 副本（自治体中間サーバ）: 対象登録・登録対象者抽出・転送結果確認 ──
    if (pathname === "/api/kokuho/fukuhon" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId) return reply(res, 400, { error: "atenaId required" });
      const store = await readJson(kokuhoFukuhonFile, []);
      const item = { id: crypto.randomUUID(), atenaId: String(b.atenaId),
        datasets: b.datasets ?? ["資格情報", "高額療養費支給情報", "限度額認定情報"],
        status: "副本作成済（転送待ち）", createdAt: new Date().toISOString() };
      store.unshift(item);
      await writeJson(kokuhoFukuhonFile, store);
      await audit(req, "kokuho-fukuhon.create", { atenaId: item.atenaId });
      return reply(res, 201, item);
    }
    if (pathname === "/api/kokuho/fukuhon/targets" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const store = await readJson(kokuhoFukuhonFile, []);
      return reply(res, 200, { kind: "副本登録対象者リスト", count: store.length,
        targets: store.map((s) => ({ atenaId: s.atenaId, status: s.status })) });
    }
    if (pathname === "/api/kokuho/fukuhon/result" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || !b.result) return reply(res, 400, { error: "atenaId, result(登録成功/登録エラー) required" });
      const store = await readJson(kokuhoFukuhonFile, []);
      const idx = store.findIndex((s) => s.atenaId === String(b.atenaId));
      if (idx !== -1) {
        store[idx].status = `中間サーバ${b.result}`;
        await writeJson(kokuhoFukuhonFile, store);
      }
      await audit(req, "kokuho-fukuhon.result", { atenaId: b.atenaId, result: b.result });
      return reply(res, 200, { atenaId: String(b.atenaId), transferResult: String(b.result),
        note: "自治体中間サーバへ転送した副本の登録結果を確認" });
    }

    // ── 賦課結果の個人毎相当按分 ──
    if (pathname === "/api/kokuho/fuka/anbun" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const members = Array.isArray(b.members) ? b.members : [];
      const total = Number(b.householdTotal);
      if (!total || !members.length) return reply(res, 400, { error: "householdTotal, members[]{atenaId,base} required" });
      const baseSum = members.reduce((a, m) => a + Number(m.base ?? 1), 0);
      let allocated = 0;
      const result = members.map((m, i) => {
        const amount = i === members.length - 1 ? total - allocated : Math.floor((total * Number(m.base ?? 1)) / baseSum);
        allocated += amount;
        return { atenaId: m.atenaId, 個人毎相当額: amount };
      });
      await audit(req, "kokuho-fuka.anbun", { members: result.length });
      return reply(res, 200, { householdTotal: total, members: result, note: "世帯賦課額を個人毎相当に按分（端数は末尾調整）" });
    }

    // ── 還付の暦年（1/1〜12/31）抽出・合算期別上限の設定 ──
    if (pathname === "/api/kokuho/kanpu/calendar-year" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const refunds = Array.isArray(b.refunds) ? b.refunds : [];
      if (!Number(b.year) || !refunds.length) return reply(res, 400, { error: "year, refunds[]{id,date,amount} required" });
      const hits = refunds.filter((r) => String(r.date ?? "").startsWith(String(b.year)));
      await audit(req, "kokuho-kanpu.calendar-year", { year: b.year, hits: hits.length });
      return reply(res, 200, { period: `${b.year}-01-01〜${b.year}-12-31`, count: hits.length,
        total: hits.reduce((a, r) => a + Number(r.amount ?? 0), 0), refunds: hits });
    }
    if (pathname === "/api/kokuho/gassan/config" && req.method === "PUT") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      if (!Number(b.maxPeriods)) return reply(res, 400, { error: "maxPeriods required" });
      const config = await readJson(kokuhoConfigFile, {});
      config.gassanMaxPeriods = Number(b.maxPeriods);
      await writeJson(kokuhoConfigFile, config);
      await audit(req, "kokuho-gassan.config", { maxPeriods: config.gassanMaxPeriods });
      return reply(res, 200, { maxPeriods: config.gassanMaxPeriods, note: "合算できる期別の上限を団体設定" });
    }

    // ── 資格異動: 世帯主のみ喪失（世帯員残し）・被保険者証番号変更の引継 ──
    if (pathname === "/api/kokuho/shikaku/setainushi-loss" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.householdId || !b.lostHead || !b.newHead) return reply(res, 400, { error: "householdId, lostHead, newHead required" });
      await audit(req, "kokuho-shikaku.setainushi-loss", { householdId: b.householdId });
      return reply(res, 200, { householdId: String(b.householdId),
        lost: String(b.lostHead), remainingMembers: true, newHead: String(b.newHead),
        note: "世帯員を残したまま世帯主のみを資格喪失（世帯主変更を同時処理）" });
    }
    if (pathname === "/api/kokuho/hihokensha-bango/change" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || !b.oldNo || !b.newNo) return reply(res, 400, { error: "atenaId, oldNo, newNo required" });
      await audit(req, "kokuho-bango.change", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId), oldNo: String(b.oldNo), newNo: String(b.newNo),
        reason: b.reason ?? "区間異動・転居", linked: true,
        note: "証番号変更後も旧番号からの履歴参照を維持" });
    }

    // ── 資格確認書・各種証の返還（回収）状況の記録・照会 ──
    if (pathname === "/api/kokuho/cert-return" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || !b.certKind) return reply(res, 400, { error: "atenaId, certKind required" });
      const store = await readJson(kokuhoCertReturnFile, []);
      const item = { id: crypto.randomUUID(), atenaId: String(b.atenaId), certKind: String(b.certKind),
        status: b.returned === true ? "返還済（回収済）" : "未返還", at: new Date().toISOString() };
      store.unshift(item);
      await writeJson(kokuhoCertReturnFile, store);
      await audit(req, "kokuho-cert-return", { atenaId: item.atenaId, status: item.status });
      return reply(res, 201, item);
    }
    if (pathname === "/api/kokuho/cert-return" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const store = await readJson(kokuhoCertReturnFile, []);
      const atenaId = searchParams.get("atenaId") ?? "";
      const hits = atenaId ? store.filter((s) => s.atenaId === atenaId) : store;
      return reply(res, 200, { count: hits.length, items: hits });
    }

    // ── 外国人の資格確認書有効期限（在留期限連動）・性同一性障害者への配慮表記 ──
    if (pathname === "/api/kokuho/shikaku-kakuninsho/expiry" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || !b.defaultExpiry) return reply(res, 400, { error: "atenaId, defaultExpiry required" });
      const expiry = b.foreign === true && b.zairyuExpiry && b.zairyuExpiry < b.defaultExpiry
        ? b.zairyuExpiry : b.defaultExpiry;
      await audit(req, "kokuho-kakuninsho.expiry", { atenaId: b.atenaId, foreign: b.foreign === true });
      return reply(res, 200, { atenaId: String(b.atenaId), expiry,
        note: b.foreign === true ? "外国人は在留期間満了日と定例有効期限の早い方" : "定例有効期限" });
    }
    if (pathname === "/api/kokuho/hairyo" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId) return reply(res, 400, { error: "atenaId required" });
      await audit(req, "kokuho-hairyo", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId),
        measures: { 通称名表記: b.tsushomei === true, 性別欄裏面記載: b.sexOnBack === true },
        note: "性同一性障害者の被保険者証等の表記配慮（通称名・性別の裏面記載等）" });
    }

    // ════ △→◎昇格 第3弾: 特別徴収一式・事業状況報告・資格確認書発送・負担割合・賦課特例ほか ════

    const kokuhoTokuchoFile = join(dataRoot, "kokuho-tokucho.json");
    const kokuhoDispatchFile = join(dataRoot, "kokuho-dispatch.json");

    // ── 特別徴収: 対象者情報の取込（国保連より）──
    if (pathname === "/api/kokuho/tokucho/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[]{atenaId,pensionAmount,pensionInsurer} required" });
      const store = await readJson(kokuhoTokuchoFile, { subjects: [] });
      store.subjects = rows.map((r) => ({ ...r, importedAt: new Date().toISOString() }));
      await writeJson(kokuhoTokuchoFile, store);
      await audit(req, "kokuho-tokucho.import", { rows: rows.length });
      return reply(res, 200, { source: "国保連合会（特徴対象者情報）", imported: rows.length });
    }

    // ── 特別徴収: 候補判定（資格状況×年齢65〜74）＋判定結果一覧 ──
    if (pathname === "/api/kokuho/tokucho/judge" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const candidates = Array.isArray(b.candidates) ? b.candidates : [];
      if (!b.asOf || !candidates.length) return reply(res, 400, { error: "asOf, candidates[]{atenaId,birthDate,qualified,pensionAmount,fukaAmount} required" });
      const results = candidates.map((c) => {
        const age = c.birthDate ? Math.floor((new Date(b.asOf) - new Date(c.birthDate)) / (365.25 * 86400000)) : 0;
        const ageOk = age >= 65 && age <= 74;
        const pensionOk = Number(c.pensionAmount ?? 0) >= 180000; // 年金18万円以上
        const halfOk = Number(c.fukaAmount ?? 0) + Number(c.kaigoAmount ?? 0) <= Number(c.pensionAmount ?? 0) / 2; // 介護合算で年金1/2以下
        const eligible = c.qualified !== false && ageOk && pensionOk && halfOk;
        return { atenaId: c.atenaId, age, eligible,
          reason: !ageOk ? "年齢65〜74歳外" : !pensionOk ? "年金18万円未満" : !halfOk ? "介護合算で年金1/2超" : c.qualified === false ? "資格なし" : "" };
      });
      await audit(req, "kokuho-tokucho.judge", { judged: results.length, eligible: results.filter((r) => r.eligible).length });
      return reply(res, 200, { document: "特別徴収判定結果一覧", asOf: b.asOf, results });
    }

    // ── 特別徴収: 期割額の算定（仮徴収4・6・8月／本徴収10・12・2月）＋結果一覧 ──
    if (pathname === "/api/kokuho/tokucho/kiwari" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const subjects = Array.isArray(b.subjects) ? b.subjects : [];
      if (!subjects.length) return reply(res, 400, { error: "subjects[]{atenaId,annualFuka,kariBase?} required" });
      const results = subjects.map((s) => {
        const annual = Number(s.annualFuka ?? 0);
        const kariUnit = Math.floor(Number(s.kariBase ?? annual) / 6 / 100) * 100; // 仮徴収=前年度ベース1/6（百円単位）
        const kariTotal = kariUnit * 3;
        const honUnit = Math.floor((annual - kariTotal) / 3);
        return { atenaId: s.atenaId,
          仮徴収: { "4月": kariUnit, "6月": kariUnit, "8月": kariUnit },
          本徴収: { "10月": honUnit, "12月": honUnit, "2月": annual - kariTotal - honUnit * 2 },
          年額: annual };
      });
      await audit(req, "kokuho-tokucho.kiwari", { subjects: results.length });
      return reply(res, 200, { document: "特別徴収期割額の算定結果一覧", results,
        note: "仮徴収（4・6・8月）＋本徴収（10・12・2月）。介護保険との合算上限は判定済み前提" });
    }

    // ── 特別徴収: 仮徴収額の平準化（増額・減額）──
    if (pathname === "/api/kokuho/tokucho/heijunka" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const subjects = Array.isArray(b.subjects) ? b.subjects : [];
      if (!subjects.length) return reply(res, 400, { error: "subjects[]{atenaId,kariUnit,estimatedAnnual} required" });
      const results = subjects.map((s) => {
        const ideal = Math.floor(Number(s.estimatedAnnual ?? 0) / 6 / 100) * 100;
        const current = Number(s.kariUnit ?? 0);
        const direction = ideal > current ? "増額" : ideal < current ? "減額" : "変更なし";
        return { atenaId: s.atenaId, currentKari: current, adjustedKari: ideal, direction,
          needed: direction !== "変更なし" };
      });
      await audit(req, "kokuho-tokucho.heijunka", { adjusted: results.filter((r) => r.needed).length });
      return reply(res, 200, { kind: "仮徴収額の平準化（増額・減額）", results });
    }

    // ── 特別徴収: 結果通知（22/32/62通知・年次依頼結果）の異常処理＋仮徴収停止連絡 ──
    if (pathname === "/api/kokuho/tokucho/notify-errors" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const NOTICES = ["年次依頼結果", "22通知", "32通知", "62通知"];
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!NOTICES.includes(b.noticeKind) || !rows.length) return reply(res, 400, { error: `noticeKind(${NOTICES.join("/")}), rows[]{atenaId,result} required` });
      const errors = rows.filter((r) => r.result !== "正常")
        .map((r) => ({ atenaId: r.atenaId, result: r.result,
          action: r.result === "失権" ? "特別徴収中止→普通徴収へ切替" : "内容確認のうえ再依頼/普徴切替" }));
      await audit(req, "kokuho-tokucho.notify-errors", { noticeKind: b.noticeKind, errors: errors.length });
      return reply(res, 200, { noticeKind: b.noticeKind, checked: rows.length, errors,
        note: "正常以外（失権等）を抽出し後続処理を提示" });
    }
    if (pathname === "/api/kokuho/tokucho/kari-stop" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const households = Array.isArray(b.households) ? b.households : [];
      if (!households.length) return reply(res, 400, { error: "households[]{setaiId,reason} required" });
      await audit(req, "kokuho-tokucho.kari-stop", { households: households.length });
      return reply(res, 200, { target: "国保連合会", file: "仮徴収停止連絡データ",
        count: households.length, rows: households });
    }

    // ── 国民健康保険事業状況報告（厚労省報告）の集計 ──
    if (pathname === "/api/kokuho/jigyo-houkoku" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!b.month || !rows.length) return reply(res, 400, { error: "month, rows[]{kind(世帯/被保険者/給付),count,amount?} required" });
      const byKind = {};
      for (const r of rows) {
        const k = (byKind[r.kind ?? "その他"] ??= { count: 0, amount: 0 });
        k.count += Number(r.count ?? 0);
        k.amount += Number(r.amount ?? 0);
      }
      await audit(req, "kokuho-jigyo-houkoku", { month: b.month });
      return reply(res, 200, { document: "国民健康保険事業状況報告（事業月報・厚生労働省）",
        month: b.month, sections: byKind });
    }

    // ── 資格確認書・高齢受給者証の発送管理（発送後管理・返送分・再送）──
    if (pathname === "/api/kokuho/kakuninsho/dispatch" && 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` });
      const store = await readJson(kokuhoDispatchFile, []);
      const item = { id: crypto.randomUUID(), atenaId: String(b.atenaId), certKind: b.certKind ?? "資格確認書",
        action: b.action, reason: b.reason ?? "", at: new Date().toISOString() };
      store.unshift(item);
      await writeJson(kokuhoDispatchFile, store);
      await audit(req, "kokuho-kakuninsho.dispatch", { atenaId: item.atenaId, action: b.action });
      return reply(res, 201, item);
    }
    if (pathname === "/api/kokuho/kakuninsho/dispatch" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const store = await readJson(kokuhoDispatchFile, []);
      const action = searchParams.get("action") ?? "";
      const hits = action ? store.filter((s) => s.action === action) : store;
      return reply(res, 200, { count: hits.length, items: hits,
        note: "発送後の状況・返送分（宛所不明等）・再送を履歴管理" });
    }

    // ── 負担割合（現役並み所得者の判定＝課税所得145万円以上）＋負担割合等ファイル取込 ──
    if (pathname === "/api/kokuho/futan-wariai/judge" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const subjects = Array.isArray(b.subjects) ? b.subjects : [];
      if (!subjects.length) return reply(res, 400, { error: "subjects[]{atenaId,kazeiShotoku} required" });
      const LIMIT = 1450000; // 現役並み: 課税所得145万円以上
      const results = subjects.map((s) => ({ atenaId: s.atenaId,
        kazeiShotoku: Number(s.kazeiShotoku ?? 0),
        負担割合: Number(s.kazeiShotoku ?? 0) >= LIMIT ? "3割（現役並み所得者）" : "2割",
        判定根拠: `課税所得${LIMIT.toLocaleString()}円基準` }));
      await audit(req, "kokuho-futan-wariai.judge", { judged: results.length });
      return reply(res, 200, { results });
    }
    if (pathname === "/api/kokuho/futan-wariai/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[]{atenaId,wariai} required" });
      await audit(req, "kokuho-futan-wariai.import", { rows: rows.length });
      return reply(res, 200, { kind: "負担割合等ファイルの取込（保険者システム反映）", imported: rows.length,
        tokurei: b.tokurei === true ? "転居に伴う負担限度額特例対象世帯情報も反映" : null });
    }

    // ── 1月2日以降の海外からの転入者の所得情報（申告勧奨・仮所得登録）──
    if (pathname === "/api/kokuho/kaigai-tennyu/income" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || !b.entryDate) return reply(res, 400, { error: "atenaId, entryDate required" });
      const md = String(b.entryDate).slice(5);
      const after0102 = md >= "01-02";
      await audit(req, "kokuho-kaigai-tennyu.income", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId), entryDate: b.entryDate,
        treatment: after0102 ? "1/2以降入国＝当年度住民税情報なし→所得申告勧奨＋申告所得で賦課" : "1/1以前入国＝住民税情報で賦課",
        provisionalIncome: Number(b.declaredIncome ?? 0) });
    }

    // ── 未来日75歳到達者の平等割軽減を反映した賦課計算 ──
    if (pathname === "/api/kokuho/fuka/75-keigen" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const members = Array.isArray(b.members) ? b.members : [];
      if (!b.fiscalYear || !members.length) return reply(res, 400, { error: "fiscalYear, members[]{atenaId,birthDate}, heitouwari required" });
      const fyEnd = `${Number(b.fiscalYear) + 1}-03-31`;
      const reaching75 = members.filter((m) => {
        const age75 = m.birthDate?.replace(/^(\d{4})/, (y) => String(Number(y) + 75));
        return age75 && age75 <= fyEnd;
      });
      const heitouwari = Number(b.heitouwari ?? 0);
      const reduced = reaching75.length > 0 && reaching75.length === members.length
        ? Math.floor(heitouwari / 2) : heitouwari; // 全員75歳到達（後期移行）→特定世帯平等割半額（簡略）
      await audit(req, "kokuho-fuka.75-keigen", { reaching75: reaching75.length });
      return reply(res, 200, { fiscalYear: Number(b.fiscalYear),
        年度内75歳到達者: reaching75.map((m) => m.atenaId),
        平等割: { 軽減前: heitouwari, 軽減後: reduced },
        note: "未来日75歳到達（後期移行）を反映した平等割軽減つき賦課計算" });
    }

    // ── 抜取・差替を考慮した履歴登録 ──
    if (pathname === "/api/kokuho/rireki/nukitori" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const actions = ["抜取", "差替"];
      if (!b.recordId || !actions.includes(b.action)) return reply(res, 400, { error: `recordId, action(${actions.join("/")}) required` });
      await audit(req, "kokuho-rireki.nukitori", { recordId: b.recordId, action: b.action });
      return reply(res, 200, { recordId: String(b.recordId), action: b.action,
        history: { kept: true, note: "抜取・差替後も元履歴を保持（差替は新旧両履歴をリンク）" },
        replacedBy: b.action === "差替" ? (b.newRecordId ?? null) : null });
    }

    // ── 給付情報を基にした依頼データ作成（利用目的に即した事務手続き）＋検索複数該当の一覧選択 ──
    if (pathname === "/api/kokuho/kyufu/request-data" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const purposes = ["第三者行為求償", "レセプト二次点検", "高額該当通知", "医療費通知"];
      if (!purposes.includes(b.purpose)) return reply(res, 400, { error: `purpose(${purposes.join("/")}) required` });
      const rows = Array.isArray(b.kyufuRows) ? b.kyufuRows : [];
      await audit(req, "kokuho-kyufu.request-data", { purpose: b.purpose, rows: rows.length });
      return reply(res, 200, { purpose: b.purpose, file: `request_${Date.now().toString(36)}.csv`,
        count: rows.length, note: "登録済み給付情報を基に利用目的に即した依頼データを作成" });
    }
    if (pathname === "/api/kokuho/search/select" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const hits = Array.isArray(b.hits) ? b.hits : [];
      if (hits.length < 2) return reply(res, 400, { error: "hits[](複数該当) required" });
      return reply(res, 200, { count: hits.length,
        selectionList: hits.map((h, i) => ({ no: i + 1, atenaId: h.atenaId, name: h.name ?? "", birthDate: h.birthDate ?? "" })),
        note: "対象者検索で複数該当時は一覧から選択" });
    }

    // ════ △→◎昇格 第4弾: 統合収納・統合滞納連携／滞納者再判定／帳票様式／送付先・宛先制御ほか ════

    const kokuhoMemoFile = join(dataRoot, "kokuho-memos.json");

    // ── 統合収納管理・統合滞納管理との情報提供・照会（双方向）──
    if (pathname === "/api/kokuho/togo-shuno/provide" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const DATASETS = ["調定情報", "収納情報", "滞納情報", "還付未済情報", "口座情報", "送付先情報", "納付書情報", "督促・催告情報"];
      if (!DATASETS.includes(b.dataset)) return reply(res, 400, { error: "dataset required", datasets: DATASETS });
      const rows = Array.isArray(b.rows) ? b.rows : [];
      await audit(req, "kokuho-togo-shuno.provide", { dataset: b.dataset, direction: b.direction ?? "提供", rows: rows.length });
      return reply(res, 200, { target: "統合収納管理・統合滞納管理システム",
        dataset: b.dataset, direction: ["提供", "照会"].includes(b.direction) ? b.direction : "提供",
        count: rows.length, rows });
    }
    if (pathname === "/api/kokuho/togo-shuno/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[]{atenaId,unpaid,dueDate} required" });
      await audit(req, "kokuho-togo-shuno.import", { rows: rows.length });
      return reply(res, 200, { source: "統合滞納管理システム（滞納情報）", imported: rows.length,
        note: "取込後は /api/kokuho/tainou/rejudge で滞納者再判定" });
    }

    // ── 滞納者の再判定（納期限判定日範囲等の条件×世帯主のみ/世帯員全員の選択）──
    if (pathname === "/api/kokuho/tainou/rejudge" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const units = ["世帯主のみ", "世帯員全員"];
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!units.includes(b.judgeUnit) || !rows.length) return reply(res, 400, { error: `judgeUnit(${units.join("/")}), rows[]{setaiId,atenaId,isHead,unpaid,dueDate} required` });
      let target = rows;
      if (b.dueFrom) target = target.filter((r) => (r.dueDate ?? "") >= b.dueFrom);
      if (b.dueTo) target = target.filter((r) => (r.dueDate ?? "") <= b.dueTo);
      if (b.judgeUnit === "世帯主のみ") target = target.filter((r) => r.isHead === true);
      const bySetai = {};
      for (const r of target) {
        const s = (bySetai[r.setaiId] ??= { setaiId: r.setaiId, unpaid: 0 });
        s.unpaid += Number(r.unpaid ?? 0);
      }
      const tainousha = Object.values(bySetai).filter((s) => s.unpaid > 0)
        .map((s) => ({ ...s, judged: "滞納者" }));
      await audit(req, "kokuho-tainou.rejudge", { judgeUnit: b.judgeUnit, tainousha: tainousha.length });
      return reply(res, 200, { judgeUnit: b.judgeUnit,
        conditions: { 納期限判定日開始: b.dueFrom ?? "", 納期限判定日終了: b.dueTo ?? "" },
        count: tainousha.length, results: tainousha });
    }

    // ── 帳票の印影サイズ指定・催告書の納付書枚数山分け ──
    if (pathname === "/api/kokuho/print/options" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const sizeMm = Number(b.sealSizeMm ?? 20);
      if (sizeMm < 10 || sizeMm > 30) return reply(res, 400, { error: "sealSizeMm は10〜30mm" });
      await audit(req, "kokuho-print.options", { sealSizeMm: sizeMm });
      return reply(res, 200, { sealSizeMm: sizeMm, note: "印影は指定サイズで帳票へ印字" });
    }
    if (pathname === "/api/kokuho/saikoku/split" && 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,nofushoCount} required" });
      const groups = {};
      for (const r of rows) {
        const k = `納付書${r.nofushoCount ?? 1}枚`;
        (groups[k] ??= []).push(r);
      }
      await audit(req, "kokuho-saikoku.split", { groups: Object.keys(groups).length });
      return reply(res, 200, { kind: "催告書の山分け（同封納付書枚数別）",
        files: Object.entries(groups).map(([key, items]) => ({ group: key, count: items.length })) });
    }

    // ── 送付先情報の一覧・市町村システム連携運用・住所地特例者の宛先制御 ──
    if (pathname === "/api/kokuho/sofusaki/list" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      await audit(req, "kokuho-sofusaki.list", { rows: rows.length });
      return reply(res, 200, { document: "送付先情報一覧", count: rows.length, rows,
        linkMode: b.managedByCity === true ? "市町村システム管理→国保へ連携（国保側は参照）" : "国保システム管理" });
    }
    if (pathname === "/api/kokuho/sofusaki/address-rule" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rules = ["住記現住所", "空欄", "送付先"];
      if (!rules.includes(b.tokureiRule ?? "住記現住所")) return reply(res, 400, { error: `tokureiRule(${rules.join("/")})` });
      const isTokurei = b.jushochiTokurei === true && b.hasSofusaki === true;
      const addressee = isTokurei ? (b.tokureiRule ?? "住記現住所") : (b.hasSofusaki === true ? "送付先" : "住記現住所");
      await audit(req, "kokuho-sofusaki.address-rule", { isTokurei });
      return reply(res, 200, { 住所地特例者: isTokurei, 宛先: addressee,
        note: "住所地特例×送付先登録時は特定帳票の宛先を住記現住所/空欄に切替可能" });
    }

    // ── 口座番号のマスキング有無設定 ──
    if (pathname === "/api/kokuho/account/mask-config" && req.method === "PUT") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const config = await readJson(kokuhoConfigFile, {});
      config.accountMasking = b.masking !== false;
      await writeJson(kokuhoConfigFile, config);
      await audit(req, "kokuho-account.mask-config", { masking: config.accountMasking });
      const sample = "1234567";
      return reply(res, 200, { masking: config.accountMasking,
        display: config.accountMasking ? "***" + sample.slice(-4) : sample });
    }

    // ── 法務省通知による外国籍住民票消除の連携確認 ──
    if (pathname === "/api/kokuho/gaikokuseki/josho-check" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId) return reply(res, 400, { error: "atenaId required" });
      await audit(req, "kokuho-gaikokuseki.josho-check", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId),
        source: "住記所管課（法務省通知連携）", 消除理由: b.reason ?? "在留資格喪失",
        note: "法務省通知で消除された外国籍被保険者の消除理由を住記連携で確認" });
    }

    // ── レセプト情報からの長期入院該当の自動判定（標準負担額減額）＋レセプト照会の関連状態表示 ──
    if (pathname === "/api/kokuho/choki-nyuin/judge" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const receipts = Array.isArray(b.receipts) ? b.receipts : [];
      if (!b.atenaId || !receipts.length) return reply(res, 400, { error: "atenaId, receipts[]{month,inpatientDays} required" });
      const totalDays = receipts.reduce((a, r) => a + Number(r.inpatientDays ?? 0), 0);
      const chokiNyuin = totalDays > 90; // 過去12か月の入院日数90日超で長期入院該当
      await audit(req, "kokuho-choki-nyuin.judge", { atenaId: b.atenaId, totalDays });
      return reply(res, 200, { atenaId: String(b.atenaId), totalInpatientDays: totalDays,
        長期入院該当: chokiNyuin, basis: "レセプト情報の入院日数自動集計（90日超）" });
    }
    if (pathname === "/api/kokuho/receipt/status-view" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.receiptId) return reply(res, 400, { error: "receiptId required" });
      return reply(res, 200, { receiptId: String(b.receiptId),
        第三者行為請求: b.daisansha === true ? "該当" : "非該当",
        不当利得: b.futoritoku === true ? "該当" : "非該当",
        再審査: b.saishinsa ?? "申出なし", 過誤: b.kago ?? "申出なし" });
    }

    // ── メモの一括登録・削除（対象者・世帯選択）──
    if (pathname === "/api/kokuho/memos/bulk" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const actions = ["登録", "削除"];
      const targets = Array.isArray(b.targets) ? b.targets : [];
      if (!actions.includes(b.action) || !targets.length) return reply(res, 400, { error: `action(${actions.join("/")}), targets[] required` });
      const memos = await readJson(kokuhoMemoFile, []);
      if (b.action === "登録") {
        memos.unshift(...targets.map((t) => ({ id: crypto.randomUUID(), target: String(t), text: String(b.text ?? ""), at: new Date().toISOString() })));
      } else {
        for (const t of targets) {
          for (let i = memos.length - 1; i >= 0; i--) if (memos[i].target === String(t)) memos.splice(i, 1);
        }
      }
      await writeJson(kokuhoMemoFile, memos);
      await audit(req, "kokuho-memos.bulk", { action: b.action, targets: targets.length });
      return reply(res, 200, { action: b.action, processed: targets.length, remaining: memos.length });
    }

    // ── 帳票様式カタログ（帳票詳細要件シート準拠の様式番号つき）──
    if (pathname === "/api/kokuho/documents" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const DOCS = {
        "国民健康保険葬祭費支給申請書": { sheet: "資格-11" },
        "限度額適用・標準負担額減額認定証交付申請書": { sheet: "資格-6" },
        "高齢受給者証": { sheet: "様式第一号の四・第一号の五（第七条の四関係）" },
        "旧被扶養者異動連絡票": { sheet: "資格-15" },
        "資格確認書": { sheet: "資格-1" },
        "資格確認書（特別療養）": { sheet: "資格-2" },
      };
      return reply(res, 200, { count: Object.keys(DOCS).length, documents: DOCS,
        note: "帳票詳細要件シート準拠。/api/kokuho/documents/print で発行" });
    }
    if (pathname === "/api/kokuho/documents/print" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.docType || !b.atenaId) return reply(res, 400, { error: "docType, atenaId required" });
      // 高齢受給者証は資格確認書の発行対象者（特別療養を除く）のみ
      if (b.docType === "高齢受給者証" && b.kakuninshoTarget !== true) {
        return reply(res, 400, { error: "高齢受給者証は資格確認書の発行対象者（特別療養対象者を除く）のみに交付可能" });
      }
      await audit(req, "kokuho-documents.print", { docType: b.docType, atenaId: b.atenaId });
      return reply(res, 200, { docType: String(b.docType), atenaId: String(b.atenaId),
        issuedAt: new Date().toISOString().slice(0, 10) });
    }

    // ── 被保険者証種別の照会（被保険者証・短期証・資格証明書）──
    if (pathname === "/api/kokuho/cert-kind/view" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["被保険者証", "短期被保険者証", "被保険者資格証明書"];
      if (!b.atenaId) return reply(res, 400, { error: "atenaId required" });
      return reply(res, 200, { atenaId: String(b.atenaId),
        発行中種別: kinds.includes(b.currentKind) ? b.currentKind : "被保険者証", kinds });
    }

    // ── 所得情報の統合確認（住民税＋他市区町村照会＝紙媒体・中間サーバ経由）──
    if (pathname === "/api/kokuho/shotoku/integrated-view" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId) return reply(res, 400, { error: "atenaId required" });
      const sources = Array.isArray(b.sources) ? b.sources : [];
      await audit(req, "kokuho-shotoku.integrated-view", { atenaId: b.atenaId, sources: sources.length });
      return reply(res, 200, { atenaId: String(b.atenaId),
        sources: sources.map((s) => ({ kind: s.kind ?? "住民税", via: s.via ?? "自庁", income: Number(s.income ?? 0) })),
        supportedVia: ["自庁住民税", "他市区町村照会（紙媒体）", "他市区町村照会（自治体中間サーバ）"] });
    }

    // ── バッチのスケジュール起動指定（毎日・毎週○曜日・毎月XX日・毎月末・日時指定）──
    if (pathname === "/api/kokuho/batch/schedule" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const MODES = ["直接起動", "日時指定", "毎日", "毎週", "毎月", "毎月末"];
      if (!b.batchName || !MODES.includes(b.mode)) return reply(res, 400, { error: `batchName, mode(${MODES.join("/")}) required` });
      await audit(req, "kokuho-batch.schedule", { batchName: b.batchName, mode: b.mode });
      return reply(res, 200, { batchName: String(b.batchName), mode: b.mode,
        detail: b.mode === "毎週" ? `毎週${b.weekday ?? "月"}曜日` : b.mode === "毎月" ? `毎月${b.day ?? 1}日` : b.mode === "日時指定" ? (b.at ?? "") : b.mode,
        scheduled: true });
    }

    // ── マル学の非該当予定日到来の一括資格喪失・退職異動の取消 ──
    if (pathname === "/api/kokuho/marugaku/bulk-loss" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const subjects = Array.isArray(b.subjects) ? b.subjects : [];
      if (!b.asOf || !subjects.length) return reply(res, 400, { error: "asOf, subjects[]{atenaId,higaitoYoteiDate} required" });
      const lost = subjects.filter((s) => (s.higaitoYoteiDate ?? "9999") <= b.asOf)
        .map((s) => ({ atenaId: s.atenaId, status: "マル学資格喪失（非該当予定日到来）" }));
      await audit(req, "kokuho-marugaku.bulk-loss", { lost: lost.length });
      return reply(res, 200, { asOf: b.asOf, bulk: true, lost });
    }
    if (pathname === "/api/kokuho/taishoku/cancel" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId) return reply(res, 400, { error: "atenaId required" });
      await audit(req, "kokuho-taishoku.cancel", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId), cancelled: "退職の異動内容",
        historyKept: true, shuyakuRenkei: "国保情報集約システム連携済み資格は訂正連絡票を併せて作成" });
    }

    // ════ △→◎昇格 第5弾: 資格確認書の様式・有効期限ルール・発行履歴・抽出一式 ════

    const kokuhoKakuninshoFile = join(dataRoot, "kokuho-kakuninsho.json");

    // ── 資格確認書の様式設定（カード/はがき/A4・必須/任意記載区分・施行時選択・特例A4併用・任意記載の伏せ字）──
    if (pathname === "/api/kokuho/kakuninsho/format" && (req.method === "GET" || req.method === "PUT")) {
      requireRole(actor, req.method === "PUT" ? "ADMIN" : "CLERK");
      const config = await readJson(kokuhoConfigFile, {});
      const defaults = { type: "カード型", optionalItemsLayout: "任意記載あり", a4Fallback: true };
      let conf = { ...defaults, ...(config.kakuninshoFormat ?? {}) };
      if (req.method === "PUT") {
        const b = await parseBody(req);
        const types = ["カード型", "はがき型", "A4型"];
        const layouts = ["任意記載あり", "必須のみ"];
        if (b.type !== undefined && !types.includes(b.type)) return reply(res, 400, { error: `type(${types.join("/")})` });
        if (b.optionalItemsLayout !== undefined && !layouts.includes(b.optionalItemsLayout)) return reply(res, 400, { error: `optionalItemsLayout(${layouts.join("/")})` });
        conf = { ...conf, ...b };
        config.kakuninshoFormat = conf;
        await writeJson(kokuhoConfigFile, config);
        await audit(req, "kokuho-kakuninsho.format", { type: conf.type });
      }
      return reply(res, 200, { ...conf,
        note: "制度施行時に様式を選択。マイナンバーカード紛失時等の特例運用としてA4型を随時併用可能" });
    }

    // ── 資格確認書の発行（選択様式で出力・任意記載の＊埋め・要配慮者区分・職権発行・発行履歴登録）──
    if (pathname === "/api/kokuho/kakuninsho/issue" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId) return reply(res, 400, { error: "atenaId required" });
      const config = await readJson(kokuhoConfigFile, {});
      const fmt = { type: "カード型", a4Fallback: true, ...(config.kakuninshoFormat ?? {}) };
      const type = b.useA4Fallback === true && fmt.a4Fallback ? "A4型" : fmt.type;
      const optional = b.optOutOptionalItems === true
        ? { 限度額区分: "＊＊＊", 特定疾病療養区分: "＊＊＊" }
        : (b.optionalItems ?? {});
      const reasons = ["要配慮者", "その他"];
      const store = await readJson(kokuhoKakuninshoFile, []);
      const item = { id: crypto.randomUUID(), atenaId: String(b.atenaId), type,
        applyKubun: reasons.includes(b.applyKubun) ? b.applyKubun : "その他",
        shokken: b.riyoTorokuKaijo === true, optionalItems: optional,
        issuedAt: new Date().toISOString(), by: actor.id };
      store.unshift(item);
      await writeJson(kokuhoKakuninshoFile, store);
      await audit(req, "kokuho-kakuninsho.issue", { atenaId: item.atenaId, type, shokken: item.shokken });
      return reply(res, 201, { ...item,
        note: item.shokken ? "利用登録解除者への職権発行" : "申請発行（発行履歴に登録済み）" });
    }
    // 発行履歴の照会・修正・削除
    {
      const m = pathname.match(/^\/api\/kokuho\/kakuninsho\/history(?:\/([^/]+))?$/);
      if (m && req.method === "GET") {
        requireRole(actor, "CLERK");
        const store = await readJson(kokuhoKakuninshoFile, []);
        const atenaId = searchParams.get("atenaId") ?? "";
        const hits = atenaId ? store.filter((s) => s.atenaId === atenaId) : store;
        return reply(res, 200, { count: hits.length, history: hits });
      }
      if (m && m[1] && (req.method === "PUT" || req.method === "DELETE")) {
        requireRole(actor, "CLERK");
        const store = await readJson(kokuhoKakuninshoFile, []);
        const idx = store.findIndex((s) => s.id === m[1]);
        if (idx === -1) return reply(res, 404, { error: "発行履歴がありません" });
        if (req.method === "DELETE") {
          store.splice(idx, 1);
          await writeJson(kokuhoKakuninshoFile, store);
          await audit(req, "kokuho-kakuninsho.history-delete", { id: m[1] });
          return reply(res, 204, {});
        }
        const b = await parseBody(req);
        store[idx] = { ...store[idx], ...b, id: store[idx].id, correctedAt: new Date().toISOString() };
        await writeJson(kokuhoKakuninshoFile, store);
        await audit(req, "kokuho-kakuninsho.history-update", { id: m[1] });
        return reply(res, 200, store[idx]);
      }
    }

    // ── 資格確認書の有効期限計算（非該当予定日・在留期限・70/75歳到達・任意記載最短・世帯合わせ）──
    if (pathname === "/api/kokuho/kakuninsho/expiry-calc" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.defaultExpiry) return reply(res, 400, { error: "defaultExpiry required" });
      const cands = [{ date: String(b.defaultExpiry), reason: "定例有効期限（70歳未満/以上で別設定可）" }];
      // 非該当予定日（マル遠・住所地特例は優先設定可、優先する/しないを選択可）
      if (b.higaitoYoteiDate) {
        if ((b.maruen === true || b.jushochiTokurei === true) && b.preferHigaito !== false) {
          cands.length = 0;
          cands.push({ date: String(b.higaitoYoteiDate), reason: "非該当予定日を優先（マル遠・住所地特例）" });
        } else {
          cands.push({ date: String(b.higaitoYoteiDate), reason: "非該当予定日（短い方を採用）" });
        }
      }
      // 外国人: 在留期間終了日の当日/翌日（運用選択）＋更新申請受付時は満了日+2月
      if (b.foreign === true && b.zairyuEndDate) {
        let zd = String(b.zairyuEndDate);
        if (b.zairyuRenewalAccepted === true) {
          const d = new Date(zd + "T00:00:00Z");
          d.setUTCMonth(d.getUTCMonth() + 2);
          zd = d.toISOString().slice(0, 10);
          cands.push({ date: zd, reason: "在留更新申請受付→満了日から2月経過日まで延長" });
        } else {
          if (b.zairyuRule === "翌日") {
            const d = new Date(zd + "T00:00:00Z");
            d.setUTCDate(d.getUTCDate() + 1);
            zd = d.toISOString().slice(0, 10);
          }
          cands.push({ date: zd, reason: `在留期間終了日${b.zairyuRule === "翌日" ? "の翌日" : "当日"}（運用選択）` });
        }
      }
      // 75歳到達: 誕生日前日／70歳到達: 到達月（1日生まれは前月）の末日
      if (b.birthDate) {
        const bd = String(b.birthDate);
        const fyEnd = b.fiscalYearEnd ?? "9999-03-31";
        const age75 = bd.replace(/^(\d{4})/, (y) => String(Number(y) + 75));
        if (age75 <= fyEnd) {
          const d = new Date(age75 + "T00:00:00Z");
          d.setUTCDate(d.getUTCDate() - 1);
          cands.push({ date: d.toISOString().slice(0, 10), reason: "年度内75歳到達→誕生日前日" });
        }
        const age70 = bd.replace(/^(\d{4})/, (y) => String(Number(y) + 70));
        if (b.under70 === true && age70 <= fyEnd) {
          const d = new Date(age70 + "T00:00:00Z");
          if (d.getUTCDate() === 1) d.setUTCDate(0); // 1日生まれ→前月末日
          else { d.setUTCMonth(d.getUTCMonth() + 1); d.setUTCDate(0); } // 到達月の末日
          cands.push({ date: d.toISOString().slice(0, 10), reason: "年度内70歳到達→到達月（1日生まれは前月）の末日" });
        }
      }
      // 任意記載事項（限度額区分・特定疾病療養区分）の最短有効期限
      for (const o of Array.isArray(b.optionalItemExpiries) ? b.optionalItemExpiries : []) {
        cands.push({ date: String(o.expiry), reason: `任意記載（${o.item ?? "限度額区分"}）の有効期限` });
      }
      let chosen = cands.reduce((a, c) => (c.date < a.date ? c : a));
      // 世帯内外国人の差異→世帯単位で最短に合わせる
      if (Array.isArray(b.householdZairyuEndDates) && b.householdZairyuEndDates.length) {
        const minHh = [...b.householdZairyuEndDates].sort()[0];
        if (minHh < chosen.date) chosen = { date: minHh, reason: "世帯内外国人の在留期限差→世帯単位で最短に統一" };
      }
      await audit(req, "kokuho-kakuninsho.expiry-calc", { expiry: chosen.date });
      return reply(res, 200, { expiry: chosen.date, basis: chosen.reason, candidates: cands });
    }

    // ── 抽出系（滞納世帯の18歳年度末まで世帯員・一斉更新外年の負担割合変更者・高齢受給者証差替対象）──
    if (pathname === "/api/kokuho/kakuninsho/extract" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const KINDS = ["滞納世帯18歳年度末", "負担割合変更者", "高齢受給者証差替"];
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!KINDS.includes(b.kind) || !rows.length) return reply(res, 400, { error: `kind(${KINDS.join("/")}), rows[] required` });
      let hits;
      if (b.kind === "滞納世帯18歳年度末") {
        const asOf = b.asOf ?? new Date().toISOString().slice(0, 10);
        hits = rows.filter((r) => {
          if (r.tainouSetai !== true || !r.birthDate) return false;
          const age18 = r.birthDate.replace(/^(\d{4})/, (y) => String(Number(y) + 18));
          const fyEnd18 = age18 <= `${age18.slice(0, 4)}-03-31` ? `${age18.slice(0, 4)}-03-31` : `${Number(age18.slice(0, 4)) + 1}-03-31`;
          return asOf <= fyEnd18;
        });
      } else if (b.kind === "負担割合変更者") {
        hits = rows.filter((r) => r.futanWariaiChanged === true);
      } else {
        hits = rows.filter((r) => r.atenaChanged === true || r.shikakuChanged === true);
      }
      await audit(req, "kokuho-kakuninsho.extract", { kind: b.kind, hits: hits.length });
      return reply(res, 200, { kind: b.kind, count: hits.length, list: hits,
        note: b.kind === "負担割合変更者" ? "一斉更新を行わない年（有効期限2〜5年設定時）の変更者のみ抽出" : "" });
    }

    // ── 高齢受給者証の発効期日の個別設定 ──
    if (pathname === "/api/kokuho/kourei-jukyu/issue-date" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || !b.issueDate) return reply(res, 400, { error: "atenaId, issueDate required" });
      await audit(req, "kokuho-kourei-jukyu.issue-date", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId), 発効期日: String(b.issueDate), individual: true });
    }

    // ════ △→◎昇格 第6弾: 納付書出力エンジン一式 ════

    const kokuhoNofushoFile = join(dataRoot, "kokuho-nofusho.json");

    // ── 納付書の出力エンジン（期別/全納/一部・期限/金額/宛名変更・MPN・バーコード・合算・様式・交渉経過自動記録）──
    if (pathname === "/api/kokuho/nofusho/generate" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const SCOPES = ["期別選択", "全納", "一部納付", "充当後未納", "分割納付"];
      const periods = Array.isArray(b.periods) ? b.periods : [];
      if (!SCOPES.includes(b.scope) || !periods.length) return reply(res, 400, { error: `scope(${SCOPES.join("/")}), periods[]{period,choteiAmount,entaikin?,tesuryo?} required` });
      if (b.skipForThisDebtor === true) {
        return reply(res, 200, { skipped: true, note: "滞納者毎の出力有無選択により出力対象外" });
      }
      const rows = periods.map((p) => ({
        period: p.period,
        調定額: Number(b.overrideChotei ?? p.choteiAmount ?? 0),
        延滞金: b.includeEntaikin === false ? 0 : Number(b.overrideEntaikin ?? p.entaikin ?? 0),
        督促手数料: Number(b.overrideTesuryo ?? p.tesuryo ?? 0),
      }));
      const total = rows.reduce((a, r) => a + r["調定額"] + r["延滞金"] + r["督促手数料"], 0);
      const dueDate = b.customDueDate ?? b.paymentDeadline ?? periods[0].dueDate ?? "";
      // 納期限の出力履歴を管理
      const store = await readJson(kokuhoNofushoFile, { dueHistory: [], shippings: [], results: [] });
      store.dueHistory.unshift({ atenaId: String(b.atenaId ?? ""), dueDate, at: new Date().toISOString() });
      await writeJson(kokuhoNofushoFile, store);
      const barcodeOk = total <= 300000 && b.barcodeDisabled !== true; // コンビニ収納の上限30万円
      const doc = {
        scope: b.scope, preview: b.preview === true,
        様式: ["納付書", "払込取扱票"].includes(b.formStyle) ? b.formStyle : "納付書",
        宛名: b.overrideAddressee ?? b.addressee ?? "",
        納期限: dueDate, 取扱期限: b.handlingDeadline ?? "", 支払期限: b.paymentDeadline ?? "",
        合算: b.scope !== "期別選択" || periods.length > 1,
        領収証書部分: rows.map((r) => ({ period: r.period, amount: r["調定額"] + r["延滞金"] + r["督促手数料"] })),
        rows, total,
        MPN: "マルチペイメントネットワーク標準様式（収納機関番号・納付番号・確認番号）",
        コンビニバーコード: barcodeOk
          ? { printed: true, 使用期限: b.barcodeDeadline ?? dueDate }
          : { printed: false, 理由: b.barcodeDisabled === true ? "バーコード出力対象外の設定" : "30万円超のためコンビニ収納対象外" },
        督促状連動: b.tokusoku === true ? (b.tokusokuWithNofusho === false ? "納付書なし督促状" : "納付書兼用督促状") : null,
        ...(b.scope === "分割納付" ? { note: "分割納付書の様式は収納機能の納付書様式と同一" } : {}),
      };
      // 交渉経過への自動履歴追加（納付書出力）
      const autoKoshoLog = { kind: "納付書出力", mode: b.bulk === true ? "一括" : "個別", at: new Date().toISOString() };
      await audit(req, "kokuho-nofusho.generate", { scope: b.scope, total, preview: doc.preview });
      return reply(res, 200, { ...doc, autoKoshoLog,
        autoKoshoLogKinds: ["納付書出力", "照会文書出力", "督促状出力", "催告書出力", "分割納付登録/誓約書/取消・解除", "納付受託", "処分帳票出力"] });
    }

    // ── 納付書データの作成・サービス事業者登録・同一納付書の複数速報/確報管理 ──
    if (pathname === "/api/kokuho/nofusho/data" && 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,period,amount} required" });
      const store = await readJson(kokuhoNofushoFile, { dueHistory: [], shippings: [], results: [] });
      const shipping = { id: crypto.randomUUID(), count: rows.length,
        registeredTo: b.serviceProvider ?? "サービス事業者（印刷・封入委託）", at: new Date().toISOString() };
      store.shippings.unshift(shipping);
      await writeJson(kokuhoNofushoFile, store);
      await audit(req, "kokuho-nofusho.data", { rows: rows.length });
      return reply(res, 200, { kind: "納付書データ（請求データ）", ...shipping });
    }
    if (pathname === "/api/kokuho/nofusho/payment-reports" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.nofushoId || !["速報", "確報"].includes(b.reportKind)) return reply(res, 400, { error: "nofushoId, reportKind(速報/確報) required" });
      const store = await readJson(kokuhoNofushoFile, { dueHistory: [], shippings: [], results: [] });
      store.results.unshift({ nofushoId: String(b.nofushoId), reportKind: b.reportKind,
        amount: Number(b.amount ?? 0), at: new Date().toISOString() });
      await writeJson(kokuhoNofushoFile, store);
      const all = store.results.filter((r) => r.nofushoId === String(b.nofushoId));
      await audit(req, "kokuho-nofusho.payment-report", { nofushoId: b.nofushoId, reportKind: b.reportKind });
      return reply(res, 200, { nofushoId: String(b.nofushoId), reports: all,
        note: "同一納付書に対する複数の速報・確報データを管理" });
    }

    // ── 共通納税IFSの納付書情報登録結果の照会（検索キー保持・エラー案件特定）──
    if (pathname === "/api/kokuho/nofusho/kyotsu-result" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.searchKey) return reply(res, 400, { error: "searchKey(処理結果検索キー) required" });
      const files = Array.isArray(b.resultFiles) ? b.resultFiles : [];
      const errors = files.filter((f) => f.status === "エラー")
        .map((f) => ({ caseId: f.caseId, error: f.errorDetail ?? "形式エラー" }));
      await audit(req, "kokuho-nofusho.kyotsu-result", { searchKey: b.searchKey, errors: errors.length });
      return reply(res, 200, { searchKey: String(b.searchKey), checked: files.length,
        ok: errors.length === 0, errors, note: "エラーファイルがある場合は案件とエラー内容を特定" });
    }

    // ── 区間異動前の区で発行された納付書の収入消込制限（パラメータで要否選択）──
    if (pathname === "/api/kokuho/nofusho/kushikomi-guard" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId) return reply(res, 400, { error: "atenaId, issuedWard, currentWard required" });
      const config = await readJson(kokuhoConfigFile, {});
      const guardOn = b.guardEnabled ?? config.kushikomiGuard ?? true;
      const crossWard = b.issuedWard && b.currentWard && b.issuedWard !== b.currentWard;
      if (b.persistSetting !== undefined) {
        config.kushikomiGuard = b.persistSetting === true;
        await writeJson(kokuhoConfigFile, config);
      }
      await audit(req, "kokuho-nofusho.kushikomi-guard", { crossWard, guardOn });
      if (crossWard && guardOn) {
        return reply(res, 409, { blocked: true, reason: "区間異動前の区で発行された納付書のため消込を制限（パラメータで解除可能）" });
      }
      return reply(res, 200, { blocked: false, guardEnabled: guardOn, crossWard: !!crossWard });
    }

    // ════ △→◎昇格 第7弾: 業務一覧出力エンジン（24種の一覧をkind別フィルタで提供） ════

    if (pathname === "/api/kokuho/lists" && req.method === "GET") {
      requireRole(actor, "CLERK");
      return reply(res, 200, { kinds: [
        "口座情報", "口座情報（種類別）", "メモ情報", "返送管理", "納税組合", "納税管理人",
        "帳票選択", "資格情報履歴", "資格重複状況対応", "回収登録", "転出特定同一世帯所属者",
        "転出旧被扶養者", "産前産後免除転出者", "資格異動処理結果", "住記不整合", "年金不整合",
        "生保不整合", "ファイル件数", "申請状況", "産前産後申請者", "滞納者納付状況",
        "限度額適用不可", "特定疾患対象者", "任意抽出（台帳項目）",
      ] });
    }
    if (pathname === "/api/kokuho/lists/generate" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!b.kind || !rows.length) return reply(res, 400, { error: "kind, rows[] required（kind一覧は GET /api/kokuho/lists）" });
      let hits = rows;
      let note = "";
      switch (b.kind) {
        case "口座情報（種類別）": {
          const types = ["振替口座", "還付口座", "給付口座"];
          if (!types.includes(b.accountType)) return reply(res, 400, { error: `accountType(${types.join("/")}) required` });
          hits = rows.filter((r) => r.accountType === b.accountType);
          note = "口座の種類（振替/還付/給付）ごとの一覧";
          break;
        }
        case "資格重複状況対応":
          hits = rows.filter((r) => r.duplicated === true);
          note = "中間サーバ返却の資格重複状況結果一覧→異動訂正・職権消除の対応対象";
          break;
        case "転出特定同一世帯所属者":
          hits = rows.filter((r) => r.tenshutsu === true && r.tokuteiDoitsuSetai === true);
          break;
        case "転出旧被扶養者":
          hits = rows.filter((r) => r.tenshutsu === true && r.kyuHifuyousha === true);
          break;
        case "産前産後免除転出者":
          hits = rows.filter((r) => r.tenshutsu === true && r.sanzenSangoMenjo === true
            && (!b.asOf || (r.menjoEndDate ?? "") >= b.asOf));
          note = "免除期間終了前に他市区町村へ転出した対象者";
          break;
        case "資格異動処理結果":
          hits = rows.filter((r) => (!b.from || (r.idouDate ?? "") >= b.from) && (!b.to || (r.idouDate ?? "") <= b.to));
          note = "一定期間内の届出にかかる資格異動処理結果";
          break;
        case "住記不整合":
        case "年金不整合":
        case "生保不整合":
          hits = rows.filter((r) => r.linkedValue !== undefined && r.localValue !== undefined && String(r.linkedValue) !== String(r.localValue));
          note = `${b.kind.replace("不整合", "")}連携情報と資格情報の不整合対象者`;
          break;
        case "ファイル件数":
          hits = rows.map((r) => ({ file: r.file, count: Number(r.count ?? 0) }));
          note = "資格情報（世帯/個人）ファイルの出力件数確認";
          break;
        case "滞納者納付状況":
          hits = rows.filter((r) => Number(r.unpaid ?? 0) > 0);
          break;
        case "限度額適用不可":
          hits = rows.filter((r) => r.gendogakuFuka === true);
          break;
        case "特定疾患対象者":
          hits = rows.filter((r) => r.tokuteiShikkan === true);
          break;
        case "産前産後申請者":
          hits = rows.filter((r) => r.sanzenSangoApplied === true);
          break;
        case "帳票選択":
          note = "出力可能な帳票の一覧表示→指定出力（オンライン選択可能時は一覧不要）";
          break;
        case "任意抽出（台帳項目）":
          note = "任意の一覧抽出結果に各台帳の管理項目を表示";
          break;
        default:
          note = "管理情報の一覧確認";
      }
      // 表示列: 指定があれば台帳管理項目を射影
      if (Array.isArray(b.columns) && b.columns.length) {
        hits = hits.map((r) => Object.fromEntries(b.columns.map((c) => [c, r[c]])));
      }
      await audit(req, "kokuho-lists.generate", { kind: b.kind, hits: hits.length });
      return reply(res, 200, { document: `${b.kind} 一覧`, kind: b.kind, count: hits.length, rows: hits, note });
    }

    // ════ △→◎昇格 第8弾: 医療費通知エンジン・督促/催告通知・還付/口座振替通知・通知文管理 ════

    const kokuhoIryohiFile = join(dataRoot, "kokuho-iryohi-tsuchi.json");

    // ── 医療費通知: 運用設定（単位初期値・様式・資格終了者の出力有無）──
    if (pathname === "/api/kokuho/iryohi-tsuchi/config" && req.method === "PUT") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const units = ["世帯単位", "個人単位"];
      const forms = ["汎用紙", "圧着はがき"];
      const config = await readJson(kokuhoConfigFile, {});
      const conf = { defaultUnit: "世帯単位", form: "汎用紙", includeTerminated: false, ...(config.iryohiTsuchi ?? {}) };
      if (b.defaultUnit !== undefined) { if (!units.includes(b.defaultUnit)) return reply(res, 400, { error: `defaultUnit(${units.join("/")})` }); conf.defaultUnit = b.defaultUnit; }
      if (b.form !== undefined) { if (!forms.includes(b.form)) return reply(res, 400, { error: `form(${forms.join("/")})` }); conf.form = b.form; }
      if (b.includeTerminated !== undefined) conf.includeTerminated = b.includeTerminated === true;
      config.iryohiTsuchi = conf;
      await writeJson(kokuhoConfigFile, config);
      await audit(req, "kokuho-iryohi.config", conf);
      return reply(res, 200, conf);
    }

    // ── 医療費通知: 対象外登録/解除（世帯・個人・医療機関、資格状況の確認付き）──
    if (pathname === "/api/kokuho/iryohi-tsuchi/exclusions" && (req.method === "POST" || req.method === "GET")) {
      requireRole(actor, "CLERK");
      const store = await readJson(kokuhoIryohiFile, { exclusions: [] });
      if (req.method === "GET") {
        return reply(res, 200, { count: store.exclusions.length, exclusions: store.exclusions,
          note: "レセプト照会時にもこの対象外登録状況を参照可能" });
      }
      const b = await parseBody(req);
      const kinds = ["世帯", "個人", "医療機関"];
      const actions = ["登録", "解除"];
      if (!kinds.includes(b.kind) || !b.targetId || !actions.includes(b.action ?? "登録")) {
        return reply(res, 400, { error: `kind(${kinds.join("/")}), targetId, action(${actions.join("/")}) required` });
      }
      if ((b.action ?? "登録") === "解除") {
        store.exclusions = store.exclusions.filter((e) => !(e.kind === b.kind && e.targetId === String(b.targetId)));
      } else {
        store.exclusions.unshift({ kind: b.kind, targetId: String(b.targetId),
          shikakuStatus: b.shikakuStatus ?? "（加入/脱退状況を確認のうえ登録）", at: new Date().toISOString() });
      }
      await writeJson(kokuhoIryohiFile, store);
      await audit(req, "kokuho-iryohi.exclusion", { kind: b.kind, action: b.action ?? "登録" });
      return reply(res, 200, { action: b.action ?? "登録", exclusions: store.exclusions,
        note: "対象外登録は容易に解除可能。対象外世帯でも再作成（generate force）可能" });
    }

    // ── 医療費通知: 作成（診療年月設定・世帯/個人単位・宛名番号ベース統合・対象リスト）──
    if (pathname === "/api/kokuho/iryohi-tsuchi/generate" && 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,setaiId,shinryoYm,amount,terminated?} required" });
      const config = await readJson(kokuhoConfigFile, {});
      const conf = { defaultUnit: "世帯単位", form: "汎用紙", includeTerminated: false, ...(config.iryohiTsuchi ?? {}) };
      const unit = b.unit ?? conf.defaultUnit;
      const store = await readJson(kokuhoIryohiFile, { exclusions: [] });
      let target = rows;
      if (b.shinryoFrom) target = target.filter((r) => (r.shinryoYm ?? "") >= b.shinryoFrom);
      if (b.shinryoTo) target = target.filter((r) => (r.shinryoYm ?? "") <= b.shinryoTo);
      if (!conf.includeTerminated && b.includeTerminated !== true) target = target.filter((r) => r.terminated !== true);
      if (b.force !== true) {
        target = target.filter((r) => !store.exclusions.some((e) =>
          (e.kind === "世帯" && e.targetId === String(r.setaiId)) ||
          (e.kind === "個人" && e.targetId === String(r.atenaId)) ||
          (e.kind === "医療機関" && e.targetId === String(r.iryokikanId ?? ""))));
      }
      // 単位: 個人=宛名番号基準（期間内の世帯変更＝被保険者番号変更があっても同一通知に統合）
      const byKey = {};
      for (const r of target) {
        const k = unit === "個人単位" ? String(r.atenaId) : String(r.setaiId);
        (byKey[k] ??= { key: k, unit, total: 0, entries: [] });
        byKey[k].total += Number(r.amount ?? 0);
        byKey[k].entries.push({ shinryoYm: r.shinryoYm, amount: Number(r.amount ?? 0) });
      }
      const notices = Object.values(byKey);
      await audit(req, "kokuho-iryohi.generate", { unit, notices: notices.length });
      return reply(res, 200, { unit, form: conf.form,
        shinryoRange: { from: b.shinryoFrom ?? "", to: b.shinryoTo ?? "" },
        note: "診療年月の範囲設定で確定申告利用に対応（H29.11.7三課連名通知）。個人単位は宛名番号基準で世帯変更を跨いで統合",
        targetList: notices.map((n) => n.key), count: notices.length, notices });
    }

    // ── 第三者行為・不正利得の督促/催告通知（納付未登録の抽出→通知作成）──
    if (pathname === "/api/kokuho/tsuchi/tokusoku" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const sources = ["第三者行為", "不正利得"];
      const kinds = ["督促", "催告"];
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!sources.includes(b.source) || !kinds.includes(b.noticeKind) || !rows.length) {
        return reply(res, 400, { error: `source(${sources.join("/")}), noticeKind(${kinds.join("/")}), rows[]{caseId,paid} required` });
      }
      const targets = rows.filter((r) => r.paid !== true)
        .map((r) => ({ caseId: r.caseId, document: `${b.source}${b.noticeKind}通知` }));
      await audit(req, "kokuho-tsuchi.tokusoku", { source: b.source, noticeKind: b.noticeKind, targets: targets.length });
      return reply(res, 200, { source: b.source, noticeKind: b.noticeKind,
        extracted: "納付の登録がない案件", count: targets.length, notices: targets });
    }

    // ── 41通知（現年度更正の減額世帯を自動作成）──
    if (pathname === "/api/kokuho/tsuchi/41" && 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[]{setaiId,before,after} required" });
      const targets = rows.filter((r) => Number(r.after ?? 0) < Number(r.before ?? 0))
        .map((r) => ({ setaiId: r.setaiId, 減額: Number(r.before) - Number(r.after), document: "41通知" }));
      await audit(req, "kokuho-tsuchi.41", { targets: targets.length });
      return reply(res, 200, { kind: "現年度更正の減額→41通知の自動作成", automatic: true, count: targets.length, notices: targets });
    }

    // ── 通知文の帳票ごと管理（文言・還付充当情報の自由編集）──
    if (pathname === "/api/kokuho/tsuchi/templates" && (req.method === "PUT" || req.method === "GET")) {
      requireRole(actor, req.method === "PUT" ? "ADMIN" : "CLERK");
      const config = await readJson(kokuhoConfigFile, {});
      config.tsuchiTemplates = config.tsuchiTemplates ?? {};
      if (req.method === "PUT") {
        const b = await parseBody(req);
        if (!b.reportId || b.text === undefined) return reply(res, 400, { error: "reportId, text required" });
        config.tsuchiTemplates[String(b.reportId)] = { text: String(b.text),
          kanpuJutoInfo: b.kanpuJutoInfo ?? "", updatedAt: new Date().toISOString() };
        await writeJson(kokuhoConfigFile, config);
        await audit(req, "kokuho-tsuchi.template", { reportId: b.reportId });
      }
      return reply(res, 200, { templates: config.tsuchiTemplates,
        note: "通知文は帳票ごとに管理。文言・還付充当情報は自由編集可" });
    }

    // ── 口座振替の開始通知送付状況・不能通知の送付可否 ──
    if (pathname === "/api/kokuho/furikae-tsuchi" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["開始通知", "不能通知"];
      if (!b.atenaId || !kinds.includes(b.kind)) return reply(res, 400, { error: `atenaId, kind(${kinds.join("/")}) required` });
      await audit(req, "kokuho-furikae-tsuchi", { atenaId: b.atenaId, kind: b.kind });
      return reply(res, 200, { atenaId: String(b.atenaId), kind: b.kind,
        ...(b.kind === "開始通知" ? { 送付状況: b.sent === true ? "送付済" : "未送付" }
          : { 送付可否: b.sendable === false ? "送付しない" : "送付する" }) });
    }

    // ── 還付処理の条件指定一括/個別＋還付充当通知の時効完成日自動計算 ──
    if (pathname === "/api/kokuho/kanpu/batch" && 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[]{kanpuId,inputDate,reason,choteiYear,hasAccount,noticeSentAt?} required" });
      let target = rows;
      if (b.reason) target = target.filter((r) => r.reason === b.reason);
      if (Number(b.choteiYear)) target = target.filter((r) => Number(r.choteiYear) === Number(b.choteiYear));
      if (b.hasAccount !== undefined) target = target.filter((r) => (r.hasAccount === true) === (b.hasAccount === true));
      if (b.inputFrom) target = target.filter((r) => (r.inputDate ?? "") >= b.inputFrom);
      const results = target.map((r) => {
        const base = r.resentAt ?? r.noticeSentAt ?? null; // 発送日/再発送日基準
        const jiko = base ? base.replace(/^(\d{4})/, (y) => String(Number(y) + 2)) : null; // 還付請求権は2年で時効
        return { kanpuId: r.kanpuId, processed: true, 時効完成日: jiko };
      });
      await audit(req, "kokuho-kanpu.batch", { mode: b.mode ?? "一括", processed: results.length });
      return reply(res, 200, { mode: ["一括", "個別"].includes(b.mode) ? b.mode : "一括",
        conditions: { reason: b.reason ?? "", choteiYear: b.choteiYear ?? "", hasAccount: b.hasAccount ?? "", inputFrom: b.inputFrom ?? "" },
        count: results.length, results,
        note: "還付充当通知の発送日・再発送日から時効完成日（2年）を自動設定" });
    }

    // ── 医療機関データの一括登録・修正（国保連受領: 県番号・点数区分・機関番号等）──
    if (pathname === "/api/kokuho/iryokikan/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[]{kenBango,tensuKubun,kikanBango,name?} required" });
      const bad = rows.filter((r) => !r.kenBango || !r.kikanBango);
      await audit(req, "kokuho-iryokikan.import", { rows: rows.length, bad: bad.length });
      return reply(res, 200, { source: "国保連合会等（医療機関データ）", bulk: true,
        imported: rows.length - bad.length, errors: bad.length,
        fields: ["医療機関県番号", "医療機関点数区分", "医療機関番号", "医療費通知の作成に必要となる項目"] });
    }

    // ── 消込前のエラーチェック通知（調査・確認が必要な項目をエラーとして通知）──
    if (pathname === "/api/kokuho/keshikomi/precheck" && 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[]{levyId,amount,expected} required" });
      const errors = rows.filter((r) => Number(r.amount ?? 0) !== Number(r.expected ?? r.amount ?? 0)
        || !(Number(r.amount ?? 0) > 0))
        .map((r) => ({ levyId: r.levyId, issue: !(Number(r.amount ?? 0) > 0) ? "金額不正" : "調定額と不一致（要調査）" }));
      await audit(req, "kokuho-keshikomi.precheck", { rows: rows.length, errors: errors.length });
      return reply(res, 200, { checked: rows.length, ok: errors.length === 0,
        notifications: errors, note: "消込処理前に調査・確認が必要なデータをエラーとして通知" });
    }

    // ════ △→◎昇格 第9弾: レセプト管理一式 ════

    const kokuhoReceiptFile = join(dataRoot, "kokuho-receipts.json");

    // ── レセプト取込（国保連独自レイアウト対応・資格チェック=エラー/警告レベル任意設定・該当リスト）──
    if (pathname === "/api/kokuho/receipt/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[]{receiptId,atenaId,shinryoYm,nyugai,qualified,errorCode?} required" });
      const levels = { ...{ 資格喪失後受診: "エラー", 資格取得前受診: "エラー", 給付制限: "警告" }, ...(b.checkLevels ?? {}) };
      const checked = rows.map((r) => {
        let issue = null;
        if (r.errorCode) issue = { check: `国保総合エラーコード ${r.errorCode}`, level: "エラー" };
        else if (r.qualified === false) issue = { check: "資格喪失後受診", level: levels["資格喪失後受診"] ?? "エラー" };
        return { receiptId: r.receiptId, issue };
      });
      const hits = checked.filter((c) => c.issue);
      const store = await readJson(kokuhoReceiptFile, { receipts: [], kago: [] });
      store.receipts.unshift(...rows.map((r) => ({ ...r, history: [], importedAt: new Date().toISOString() })));
      await writeJson(kokuhoReceiptFile, store);
      await audit(req, "kokuho-receipt.import", { rows: rows.length, issues: hits.length });
      return reply(res, 200, { layout: b.customLayout === true ? "国保連独自インタフェースレイアウト" : "標準レイアウト",
        imported: rows.length, checkLevels: levels,
        checkList: hits, note: "チェックレベル（エラー/警告）は任意設定可。エラーコード設定済みデータはエラー扱い" });
    }

    // ── レセプト請求時効の一括設定・受診パターン抽出（他医療機関/重複/頻回）──
    if (pathname === "/api/kokuho/receipt/jiko" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const ids = Array.isArray(b.receiptIds) ? b.receiptIds : [];
      if (!b.jikoDate || !ids.length) return reply(res, 400, { error: "jikoDate, receiptIds[] required" });
      await audit(req, "kokuho-receipt.jiko", { count: ids.length });
      return reply(res, 200, { bulk: true, jikoDate: String(b.jikoDate), applied: ids.map(String) });
    }
    if (pathname === "/api/kokuho/receipt/extract-patterns" && 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[]{receiptId,atenaId,iryokikanId,shinryoYm,visits?} required" });
      const byPatient = {};
      for (const r of rows) {
        const p = (byPatient[r.atenaId] ??= { orgs: new Set(), monthly: {}, visits: 0 });
        p.orgs.add(r.iryokikanId);
        p.monthly[r.shinryoYm] = (p.monthly[r.shinryoYm] ?? 0) + 1;
        p.visits += Number(r.visits ?? 1);
      }
      const threshold = Number(b.hinkaiThreshold ?? 12);
      const result = Object.entries(byPatient).map(([atenaId, p]) => ({
        atenaId,
        他医療機関受診: p.orgs.size > 1,
        重複受診: Object.values(p.monthly).some((c) => c > 1),
        頻回受診: p.visits >= threshold,
      })).filter((r) => r["他医療機関受診"] || r["重複受診"] || r["頻回受診"]);
      await audit(req, "kokuho-receipt.extract-patterns", { extracted: result.length });
      return reply(res, 200, { hinkaiThreshold: threshold, count: result.length, targets: result });
    }

    // ── 過誤点検（保留登録・国保総合との未登録突合・不当利得一括登録）──
    if (pathname === "/api/kokuho/receipt/kago" && 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 store = await readJson(kokuhoReceiptFile, { receipts: [], kago: [] });
      if (b.action === "保留登録") {
        const ids = Array.isArray(b.receiptIds) ? b.receiptIds : [];
        if (!ids.length) return reply(res, 400, { error: "receiptIds[] required" });
        store.kago.unshift(...ids.map((id) => ({ receiptId: String(id), status: "過誤点検保留（翌月以降に継続）", at: new Date().toISOString() })));
        await writeJson(kokuhoReceiptFile, store);
        await audit(req, "kokuho-kago.hold", { count: ids.length });
        return reply(res, 200, { action: b.action, held: ids.map(String) });
      }
      if (b.action === "未登録突合") {
        const local = Array.isArray(b.localRegistered) ? b.localRegistered.map(String) : [];
        const sogo = new Set((Array.isArray(b.sogoRegistered) ? b.sogoRegistered : []).map(String));
        const missing = local.filter((id) => !sogo.has(id));
        await audit(req, "kokuho-kago.mismatch", { missing: missing.length });
        return reply(res, 200, { action: b.action, document: "国保総合未登録レセプト確認リスト",
          count: missing.length, missing });
      }
      const ids = Array.isArray(b.receiptIds) ? b.receiptIds : [];
      if (!ids.length) return reply(res, 400, { error: "receiptIds[] required" });
      await audit(req, "kokuho-kago.futoritoku", { count: ids.length });
      return reply(res, 200, { action: b.action, bulk: true,
        registered: ids.map((id) => ({ receiptId: String(id), 取扱: "不当利得" })) });
    }

    // ── レセプト照会・給付修正（記載項目は修正不可・修正履歴管理・入院/外来別）──
    if (pathname === "/api/kokuho/receipt/view" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const store = await readJson(kokuhoReceiptFile, { receipts: [], kago: [] });
      let hits = store.receipts;
      if (b.receiptId) hits = hits.filter((r) => String(r.receiptId) === String(b.receiptId));
      if (b.nyugai) hits = hits.filter((r) => r.nyugai === b.nyugai); // 入院/外来
      await audit(req, "kokuho-receipt.view", { hits: hits.length, nyugai: b.nyugai ?? "全て" });
      return reply(res, 200, { count: hits.length, nyugai: b.nyugai ?? "全て", receipts: hits.slice(0, 100) });
    }
    if (pathname === "/api/kokuho/receipt/correct" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.receiptId || !b.field) return reply(res, 400, { error: "receiptId, field, value required" });
      const IMMUTABLE = ["receiptId", "atenaId", "shinryoYm", "iryokikanId", "tensu"]; // レセプト記載項目は修正不可
      if (IMMUTABLE.includes(b.field)) {
        return reply(res, 400, { error: `レセプト記載項目（${b.field}）は修正不可です（給付情報のみ修正可能）` });
      }
      const store = await readJson(kokuhoReceiptFile, { receipts: [], kago: [] });
      const idx = store.receipts.findIndex((r) => String(r.receiptId) === String(b.receiptId));
      if (idx === -1) return reply(res, 404, { error: "receipt not found" });
      const before = store.receipts[idx][b.field];
      store.receipts[idx].history = store.receipts[idx].history ?? [];
      store.receipts[idx].history.push({ field: b.field, before, after: b.value, at: new Date().toISOString(), by: actor.id });
      store.receipts[idx][b.field] = b.value;
      await writeJson(kokuhoReceiptFile, store);
      await audit(req, "kokuho-receipt.correct", { receiptId: b.receiptId, field: b.field });
      return reply(res, 200, { receiptId: String(b.receiptId), corrected: b.field,
        history: store.receipts[idx].history, note: "給付修正＝修正前情報を履歴として管理・照会可能" });
    }

    // ── 医療機関・施術所の未登録抽出（給付情報の機関番号に名称・所在地が未登録）──
    if (pathname === "/api/kokuho/receipt/missing-org" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["医療機関", "施術所"];
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!kinds.includes(b.kind) || !rows.length) return reply(res, 400, { error: `kind(${kinds.join("/")}), rows[]{orgId,name?,address?} required` });
      const missing = rows.filter((r) => !r.name || !r.address)
        .map((r) => ({ orgId: r.orgId, missing: [!r.name ? "名称" : null, !r.address ? "所在地" : null].filter(Boolean) }));
      await audit(req, "kokuho-receipt.missing-org", { kind: b.kind, missing: missing.length });
      return reply(res, 200, { kind: b.kind, count: missing.length, list: missing });
    }

    // ════ △→◎昇格 第10弾: 賦課率設定・統計資料・調整交付金・共通納税消込・振込結果取込 ════

    const kokuhoKyotsuFile = join(dataRoot, "kokuho-kyotsu-nozei.json");

    // ── 賦課率の任意設定（医療分・後期支援金分・介護納付金分×所得割率・資産割率・均等割額・平等割額・限度額）──
    if (pathname === "/api/kokuho/fuka-rates" && (req.method === "GET" || req.method === "PUT")) {
      requireRole(actor, req.method === "PUT" ? "ADMIN" : "CLERK");
      const config = await readJson(kokuhoConfigFile, {});
      const defaults = {
        医療分: { 所得割率: 0.072, 資産割率: 0, 均等割額: 29000, 平等割額: 21000, 限度額: 650000 },
        後期高齢者支援金分: { 所得割率: 0.025, 資産割率: 0, 均等割額: 11000, 平等割額: 7000, 限度額: 240000 },
        介護納付金分: { 所得割率: 0.021, 資産割率: 0, 均等割額: 14000, 平等割額: 6000, 限度額: 170000 },
      };
      let rates = { ...defaults, ...(config.fukaRates ?? {}) };
      if (req.method === "PUT") {
        const b = await parseBody(req);
        const kubuns = ["医療分", "後期高齢者支援金分", "介護納付金分"];
        if (!kubuns.includes(b.kubun) || !b.rates) return reply(res, 400, { error: `kubun(${kubuns.join("/")}), rates required` });
        rates[b.kubun] = { ...rates[b.kubun], ...b.rates };
        config.fukaRates = rates;
        await writeJson(kokuhoConfigFile, config);
        await audit(req, "kokuho-fuka-rates", { kubun: b.kubun });
      }
      return reply(res, 200, { rates,
        note: "賦課計算は医療分・後期高齢者支援金分・介護納付金分ごとに任意の所得割率・資産割率・均等割額・平等割額・限度額で実施" });
    }

    // ── 統計資料（国・都道府県報告の基礎統計＋賦課限度超過額控除後の所得報告資料）──
    if (pathname === "/api/kokuho/statistics" && 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,income,fukaTotal,kentoOver?} required" });
      const setaiCount = new Set(rows.map((r) => r.setaiId ?? r.atenaId)).size;
      const insuredCount = rows.length;
      const incomeTotal = rows.reduce((a, r) => a + Number(r.income ?? 0), 0);
      // 賦課限度超過額控除後の所得（限度超過分は所得から控除）
      const incomeAfterCap = rows.reduce((a, r) => {
        const over = Number(r.fukaTotal ?? 0) > Number(r.fukaLimit ?? Infinity) ? Number(r.fukaTotal) - Number(r.fukaLimit) : 0;
        return a + Math.max(0, Number(r.income ?? 0) - over);
      }, 0);
      await audit(req, "kokuho-statistics", { rows: rows.length });
      return reply(res, 200, { document: "国・都道府県報告 基礎統計資料",
        世帯数: setaiCount, 被保険者数: insuredCount, 所得総額: incomeTotal,
        賦課限度超過額控除後所得総額: incomeAfterCap,
        賦課額総額: rows.reduce((a, r) => a + Number(r.fukaTotal ?? 0), 0) });
    }

    // ── 調整交付金データの作成（集計データから報告様式へ）──
    if (pathname === "/api/kokuho/chosei-kofukin" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!b.fiscalYear || !rows.length) return reply(res, 400, { error: "fiscalYear, rows[]{kubun,amount} required" });
      const byKubun = {};
      for (const r of rows) byKubun[r.kubun ?? "その他"] = (byKubun[r.kubun ?? "その他"] ?? 0) + Number(r.amount ?? 0);
      await audit(req, "kokuho-chosei-kofukin", { fiscalYear: b.fiscalYear });
      return reply(res, 200, { document: "調整交付金データ", fiscalYear: Number(b.fiscalYear),
        集計: byKubun, total: Object.values(byKubun).reduce((a, v) => a + v, 0) });
    }

    // ── 共通納税の消込（一括/個別・本税延滞金別・仮消込・集計表・再発行）──
    if (pathname === "/api/kokuho/kyotsu-nozei/keshikomi" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const modes = ["一括", "個別", "仮消込"];
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!modes.includes(b.mode) || !rows.length) return reply(res, 400, { error: `mode(${modes.join("/")}), rows[]{levyId,principal,entaikin} required` });
      const store = await readJson(kokuhoKyotsuFile, { records: [] });
      let principalTotal = 0, entaikinTotal = 0;
      const results = rows.map((r) => {
        const principal = Number(r.principal ?? 0), entaikin = Number(r.entaikin ?? 0);
        principalTotal += principal;
        entaikinTotal += entaikin;
        return { levyId: r.levyId, principal, entaikin,
          status: b.mode === "仮消込" ? "仮消込（共通納税納付済・確報前）" : "消込済" };
      });
      store.records.unshift(...results.map((r) => ({ ...r, mode: b.mode, at: new Date().toISOString() })));
      await writeJson(kokuhoKyotsuFile, store);
      await audit(req, "kokuho-kyotsu.keshikomi", { mode: b.mode, count: results.length });
      return reply(res, 200, { mode: b.mode, count: results.length,
        本税合計: principalTotal, 延滞金合計: entaikinTotal, results,
        note: "本税と延滞金をそれぞれの調定に対して消込。集計表は /summary、再発行可" });
    }
    if (pathname === "/api/kokuho/kyotsu-nozei/summary" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const store = await readJson(kokuhoKyotsuFile, { records: [] });
      const honne = store.records.filter((r) => r.mode !== "仮消込");
      return reply(res, 200, { document: "共通納税消込集計表（再発行可）",
        件数: honne.length,
        本税合計: honne.reduce((a, r) => a + Number(r.principal ?? 0), 0),
        延滞金合計: honne.reduce((a, r) => a + Number(r.entaikin ?? 0), 0),
        仮消込件数: store.records.filter((r) => r.mode === "仮消込").length });
    }

    // ── 金融機関振込結果データの取込（口座振替・還付の振込結果） ──
    if (pathname === "/api/kokuho/furikomi-result/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[]{atenaId,amount,result(成功/不能)} required" });
      const success = rows.filter((r) => r.result === "成功");
      const failed = rows.filter((r) => r.result !== "成功")
        .map((r) => ({ atenaId: r.atenaId, reason: r.reason ?? "振込不能" }));
      await audit(req, "kokuho-furikomi-result.import", { rows: rows.length, failed: failed.length });
      return reply(res, 200, { source: "金融機関振込結果データ",
        imported: rows.length, 成功: success.length, 不能: failed, note: "口座振替・還付の振込結果を取込み消込/再処理へ反映" });
    }

    // ── 調定異動者の収納反映（調定情報に異動が生じた対象者を収納側へ反映）──
    if (pathname === "/api/kokuho/chotei-ido/reflect" && 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,before,after} required" });
      const changed = rows.filter((r) => Number(r.before ?? 0) !== Number(r.after ?? 0))
        .map((r) => ({ atenaId: r.atenaId, 差額: Number(r.after ?? 0) - Number(r.before ?? 0),
          kind: Number(r.after) > Number(r.before) ? "増額" : "減額" }));
      await audit(req, "kokuho-chotei-ido.reflect", { changed: changed.length });
      return reply(res, 200, { kind: "調定異動者の収納反映", count: changed.length, reflected: changed });
    }

    // ── 不正利得・不当利得・第三者行為求償の統合照会 ──
    if (pathname === "/api/kokuho/saiken/integrated-view" && 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,kind(不正利得/不当利得/第三者行為求償),amount,paid?} required" });
      const byKind = { 不正利得: [], 不当利得: [], 第三者行為求償: [] };
      for (const r of rows) (byKind[r.kind] ??= []).push(r);
      await audit(req, "kokuho-saiken.integrated-view", { rows: rows.length });
      return reply(res, 200, { kinds: Object.keys(byKind),
        summary: Object.fromEntries(Object.entries(byKind).map(([k, list]) => [k, {
          件数: list.length, 総額: list.reduce((a, r) => a + Number(r.amount ?? 0), 0),
          未納: list.filter((r) => r.paid !== true).length }])) });
    }

    // ════ △→◎昇格 第11弾: 収納/滞納照会・賦課軽減・口座マスキング・処分状況・認証ほか ════

    // ── 滞納者情報・資格情報・収納状況の照会（履歴含む・処分状況フラグ付き）──
    if (pathname === "/api/kokuho/shokai" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["滞納者", "資格", "収納状況"];
      if (!b.atenaId || !kinds.includes(b.kind)) return reply(res, 400, { error: `atenaId, kind(${kinds.join("/")}) required` });
      const proc = b.processStatus ?? {};
      await audit(req, "kokuho-shokai", { atenaId: b.atenaId, kind: b.kind });
      return reply(res, 200, { atenaId: String(b.atenaId), kind: b.kind, includeHistory: true,
        処分状況: {
          更正の有無: proc.kosei === true, 還付の有無: proc.kanpu === true, 差押の有無: proc.sashiosae === true,
          納期変更の有無: proc.nokiChange === true, 延滞金減免の有無: proc.entaikinGenmen === true,
        },
        note: "履歴を含めて照会。更正/還付/差押/納期変更/延滞金減免の処分状況を一覧表示" });
    }

    // ── 第三者行為徴収情報の収納業務連携（調定作成）──
    if (pathname === "/api/kokuho/daisansha/shuno-renkei" && 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[]{caseId,atenaId,amount} required" });
      const chotei = rows.map((r) => ({ caseId: r.caseId, atenaId: r.atenaId, amount: Number(r.amount ?? 0),
        choteiId: `CHT-${crypto.randomUUID().slice(0, 8)}`, kind: "第三者行為求償調定" }));
      await audit(req, "kokuho-daisansha.shuno-renkei", { count: chotei.length });
      return reply(res, 200, { kind: "第三者行為徴収情報→収納業務連携", created: chotei.length, chotei });
    }

    // ── 賦課の軽減（医療/後期支援/介護/子ども子育て分×率・限度額・軽減割合・軽減額）──
    if (pathname === "/api/kokuho/fuka-keigen" && (req.method === "GET" || req.method === "PUT")) {
      requireRole(actor, req.method === "PUT" ? "ADMIN" : "CLERK");
      const config = await readJson(kokuhoConfigFile, {});
      const defaults = {
        軽減割合: { "7割": 0.7, "5割": 0.5, "2割": 0.2 },
        子ども子育て世帯軽減: { 対象: "未就学児", 軽減割合: 0.5 },
        分別限度額: { 医療分: 650000, 後期高齢者支援金分: 240000, 介護納付金分: 170000 },
      };
      let keigen = { ...defaults, ...(config.fukaKeigen ?? {}) };
      if (req.method === "PUT") {
        const b = await parseBody(req);
        keigen = { ...keigen, ...b };
        config.fukaKeigen = keigen;
        await writeJson(kokuhoConfigFile, config);
        await audit(req, "kokuho-fuka-keigen", {});
      }
      return reply(res, 200, { keigen,
        note: "軽減割合・軽減額・分別限度額は医療分/後期支援金分/介護納付金分/子ども子育て分ごとに設定" });
    }
    // 子ども子育て世帯軽減の適用判定（未就学児の均等割半額）
    if (pathname === "/api/kokuho/fuka-keigen/kosodate" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const members = Array.isArray(b.members) ? b.members : [];
      if (!members.length) return reply(res, 400, { error: "members[]{atenaId,age,kintowari} required" });
      const results = members.map((m) => {
        const target = Number(m.age ?? 99) < 6; // 未就学児
        const reduced = target ? Math.floor(Number(m.kintowari ?? 0) / 2) : Number(m.kintowari ?? 0);
        return { atenaId: m.atenaId, 未就学児: target, 均等割: { 軽減前: Number(m.kintowari ?? 0), 軽減後: reduced } };
      });
      await audit(req, "kokuho-fuka-keigen.kosodate", { targets: results.filter((r) => r["未就学児"]).length });
      return reply(res, 200, { kind: "子ども・子育て世帯の均等割軽減（未就学児半額）", results });
    }

    // ── 口座番号マスキングの詳細設定（開始位置・桁数指定）──
    if (pathname === "/api/kokuho/account/mask-detail" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const num = String(b.accountNumber ?? "");
      if (!num) return reply(res, 400, { error: "accountNumber required" });
      const start = Math.max(0, Number(b.maskStart ?? 0));
      const len = Math.max(0, Number(b.maskLength ?? num.length));
      const masked = num.slice(0, start) + "*".repeat(Math.min(len, num.length - start)) + num.slice(start + len);
      await audit(req, "kokuho-account.mask-detail", { start, len });
      return reply(res, 200, { accountNumber: num, masked, maskStart: start, maskLength: len,
        note: "マスキングの開始位置・桁数を指定して伏せる箇所を設定" });
    }

    // ── 随時振替（指定期別以外の随時口座振替）──
    if (pathname === "/api/kokuho/furikae/zuiji" && 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, "kokuho-furikae.zuiji", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId), amount: Number(b.amount),
        kind: "随時振替", furikaeDate: b.furikaeDate ?? new Date().toISOString().slice(0, 10),
        note: "指定した期別以外に随時の口座振替を実施" });
    }

    // ── 操作者認証（ID/パスワード・暗号化格納・利用者変更/管理者初期化）──
    if (pathname === "/api/kokuho/auth/password" && req.method === "POST") {
      const b = await parseBody(req);
      const actions = ["変更", "初期化", "検証"];
      if (!b.userId || !actions.includes(b.action)) return reply(res, 400, { error: `userId, action(${actions.join("/")}) required` });
      if (b.action === "初期化") requireRole(actor, "ADMIN");
      const sha = (s) => crypto.createHash
        ? crypto.createHash("sha256").update(s).digest("hex")
        : Array.from(s).reduce((h, c) => ((h * 31 + c.charCodeAt(0)) >>> 0), 0).toString(16);
      await audit(req, "kokuho-auth.password", { userId: b.userId, action: b.action });
      return reply(res, 200, { userId: String(b.userId), action: b.action,
        storedHash: b.action !== "検証" ? sha(String(b.password ?? "init")) : undefined,
        encryption: "SHA-256で暗号化格納（平文非保持）",
        note: b.action === "初期化" ? "システム管理者による初期化" : b.action === "変更" ? "利用者によるパスワード変更" : "認証" });
    }

    // ── 個人番号照会（番号法別表第一の要件チェック）※/daicho/配下は台帳CRUDに先取りされるため別パス ──
    if (pathname === "/api/kokuho/mynumber/daicho-view" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const allowed = ["国民健康保険給付の支給", "保険料の賦課徴収", "資格の管理", "高額療養費の支給"];
      if (!b.atenaId || !b.purpose) return reply(res, 400, { error: "atenaId, purpose required" });
      if (!allowed.includes(b.purpose)) {
        return reply(res, 403, { error: "番号法別表第一の要件を満たさない目的です", allowedPurposes: allowed });
      }
      await audit(req, "kokuho-mynumber.daicho-view", { atenaId: b.atenaId, purpose: b.purpose });
      return reply(res, 200, { atenaId: String(b.atenaId), purpose: b.purpose,
        myNumber: "************", note: "番号法別表第一の要件を満たす場合のみ確認可（参照実装はマスク）" });
    }

    // ── 世帯員の一括登録 ──
    if (pathname === "/api/kokuho/setai/bulk-members" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const members = Array.isArray(b.members) ? b.members : [];
      if (!b.setaiId || !members.length) return reply(res, 400, { error: "setaiId, members[]{atenaId,name} required" });
      await audit(req, "kokuho-setai.bulk-members", { setaiId: b.setaiId, count: members.length });
      return reply(res, 200, { setaiId: String(b.setaiId), registered: members.length,
        members: members.map((m) => ({ atenaId: m.atenaId, name: m.name ?? "" })),
        note: "世帯員を一括で登録" });
    }

    // ── 限度額適用・標準負担額減額認定の一括登録 ──
    if (pathname === "/api/kokuho/gendogaku/bulk" && 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,kubun,validFrom} required" });
      const registered = rows.map((r) => ({ atenaId: r.atenaId, 適用区分: r.kubun ?? "",
        有効開始日: r.validFrom ?? "", status: "認定" }));
      await audit(req, "kokuho-gendogaku.bulk", { count: registered.length });
      return reply(res, 200, { kind: "限度額適用・標準負担額減額認定の一括登録", count: registered.length, registered });
    }

    // ── バッチ実行（前回パラメータの参照）──
    if (pathname === "/api/kokuho/batch/run" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      if (!b.batchName) return reply(res, 400, { error: "batchName required" });
      const config = await readJson(kokuhoConfigFile, {});
      config.batchParams = config.batchParams ?? {};
      const prev = config.batchParams[b.batchName] ?? null;
      const params = b.params ?? prev ?? {};
      config.batchParams[b.batchName] = params;
      await writeJson(kokuhoConfigFile, config);
      await audit(req, "kokuho-batch.run", { batchName: b.batchName });
      return reply(res, 200, { batchName: String(b.batchName), params, previousParams: prev,
        usedPrevious: !b.params && prev !== null,
        note: "実行時は前回処理時に設定したパラメータが参照される" });
    }

    // ── 特定疾病療養受療証の自己負担限度額自動設定（年齢×特定疾病認定区分×所得区分）──
    if (pathname === "/api/kokuho/tokutei-shippei/limit" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId) return reply(res, 400, { error: "atenaId required" });
      const age = Number(b.age ?? 0);
      const kubun = b.shippeiKubun ?? "人工透析"; // 人工透析/血友病/HIV
      const shotoku = b.shotokuKubun ?? "一般";
      // 人工透析の70歳未満・上位所得者は2万円、それ以外1万円
      const limit = kubun === "人工透析" && age < 70 && shotoku === "上位所得" ? 20000 : 10000;
      await audit(req, "kokuho-tokutei-shippei.limit", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId), 特定疾病区分: kubun, 所得区分: shotoku,
        自己負担限度額: limit, note: "年齢・特定疾病認定区分・所得区分に応じて自動設定" });
    }

    // ── 葬祭費の申請状況・支給状況の照会 ──
    if (pathname === "/api/kokuho/sosaihi/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[]{atenaId,applyDate,status} required" });
      const byStatus = {};
      for (const r of rows) byStatus[r.status ?? "申請受付"] = (byStatus[r.status ?? "申請受付"] ?? 0) + 1;
      await audit(req, "kokuho-sosaihi.status", { rows: rows.length });
      return reply(res, 200, { document: "葬祭費 申請・支給状況照会",
        集計: byStatus, count: rows.length, items: rows });
    }

    // ── 保険者情報の業務別管理 ──
    if (pathname === "/api/kokuho/hokenja/by-gyomu" && req.method === "GET") {
      requireRole(actor, "CLERK");
      return reply(res, 200, {
        業務別保険者情報: {
          資格: { 保険者番号: "131019", 保険者名: "○○市（資格）" },
          賦課: { 保険者番号: "131019", 保険者名: "○○市（賦課）" },
          給付: { 保険者番号: "131019", 保険者名: "○○市（給付）" },
        },
        note: "保険者情報は業務ごとに管理（資格・賦課・給付で別管理可能）" });
    }

    // ── 不正利得返還通知の作成 ──
    if (pathname === "/api/kokuho/fusei-ritoku/notice" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.caseId || !b.debtor || !Number(b.amount)) return reply(res, 400, { error: "caseId, debtor, amount required" });
      await audit(req, "kokuho-fusei-ritoku.notice", { caseId: b.caseId, amount: Number(b.amount) });
      return reply(res, 201, { title: "不正利得返還通知", caseId: String(b.caseId), debtor: String(b.debtor),
        amount: Number(b.amount), basis: "国民健康保険法第65条（不正利得の徴収）", issuedAt: new Date().toISOString().slice(0, 10) });
    }

  return false;
}
