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

// ── 保育料（利用者負担額）算定（標準仕様書: 市町村民税額による階層区分＋年齢・保育時間で決定、
//   きょうだい軽減、3歳以上は幼児教育・保育の無償化で0円。保育料表は市町村条例パラメータ）──
//   既定は例示の階層表（市町村民税所得割額の区分→月額）。
const NURSERY_TIERS = [
  { id: "A", max: -1, std: 0, short: 0, label: "生活保護等" },          // 生活保護世帯
  { id: "B", max: 0, std: 0, short: 0, label: "市町村民税非課税" },     // 非課税(所得割0)
  { id: "C1", max: 48600, std: 19500, short: 19200, label: "所得割48,600円未満" },
  { id: "C2", max: 97000, std: 30000, short: 29600, label: "所得割97,000円未満" },
  { id: "D1", max: 169000, std: 44500, short: 43900, label: "所得割169,000円未満" },
  { id: "D2", max: 301000, std: 61000, short: 60100, label: "所得割301,000円未満" },
  { id: "D3", max: 397000, std: 80000, short: 78800, label: "所得割397,000円未満" },
  { id: "D4", max: Infinity, std: 104000, short: 102400, label: "所得割397,000円以上" },
];
function determineNurseryTier(taxIncomeLevy, isWelfare = false) {
  if (isWelfare) return NURSERY_TIERS[0];
  const levy = Number(taxIncomeLevy) || 0;
  if (levy <= 0) return NURSERY_TIERS[1];
  return NURSERY_TIERS.find((t) => levy < t.max) ?? NURSERY_TIERS[NURSERY_TIERS.length - 1];
}
function computeNurseryFee({ taxIncomeLevy = 0, isWelfare = false, ageClass = "under3", standardTime = true, childOrder = 1 }, tiers = NURSERY_TIERS) {
  // 3歳以上児は幼児教育・保育の無償化により0円
  if (ageClass === "over3") return { tier: determineNurseryTier(taxIncomeLevy, isWelfare).id, baseFee: 0, fee: 0, reason: "3歳以上（無償化）" };
  const tier = determineNurseryTier(taxIncomeLevy, isWelfare);
  const baseFee = standardTime ? tier.std : tier.short;
  // きょうだい軽減: 第2子半額、第3子以降無料
  const order = Math.max(1, Number(childOrder) || 1);
  let fee = baseFee;
  if (order === 2) fee = Math.floor(baseFee / 2);
  else if (order >= 3) fee = 0;
  return { tier: tier.id, tierLabel: tier.label, baseFee, fee, childOrder: order };
}

// ── 利用者負担額の詳細算定（標準仕様書: 税未申告は最高階層・減免・給付単価限度額上限・日割り）──
//   税未申告(0280297/0280585)→最高階層で算定。減免(0280304/0280305)=率または額。給付単価限度額(0280294/0280347)で上限。
//   月途中入退所(0280300/0280313)＝在園日数で日割。臨時休園・登園自粛(0280301)＝休園日数分を日割減額。
function computeFutanDetail(input = {}, tiers = NURSERY_TIERS) {
  const unfiled = !!input.unfiled;
  // 税未申告は最高階層で算定（最高tierのstdに相当する所得割を渡す）
  const base = unfiled
    ? (() => { const t = tiers[tiers.length - 1];
        return { tier: t.id, tierLabel: t.label, baseFee: input.standardTime === false ? t.short : t.std,
          fee: input.ageClass === "over3" ? 0 : (input.standardTime === false ? t.short : t.std), childOrder: 1 }; })()
    : computeNurseryFee(input, tiers);
  // きょうだい軽減は computeNurseryFee で適用済（未申告経路は order=1 として再適用）
  let fee = unfiled ? (() => { const o = Math.max(1, Number(input.childOrder) || 1);
    return o === 2 ? Math.floor(base.baseFee / 2) : o >= 3 ? 0 : base.fee; })() : base.fee;
  const きょうだい軽減後 = fee;
  // 給付単価限度額で上限（利用者負担が給付単価限度額を超えないように）
  if (input.subsidyCap !== undefined && input.subsidyCap !== null) fee = Math.min(fee, Math.max(0, Number(input.subsidyCap) || 0));
  // 減免（率→額の順。floorで円未満切捨て）
  const g = input.genmen || {};
  if (g.rate) fee = Math.floor(fee * (1 - Math.max(0, Math.min(1, Number(g.rate) || 0))));
  if (g.amount) fee = Math.max(0, fee - (Number(g.amount) || 0));
  const 減免後 = fee;
  // 日割り
  const pr = input.proration || {};
  const monthDays = Number(pr.monthDays) || 0;
  if (monthDays > 0) {
    if (pr.attendDays !== undefined) {           // 月途中入退所: 在園日数で日割
      fee = Math.floor(減免後 * (Number(pr.attendDays) || 0) / monthDays);
    } else if (pr.closedDays !== undefined) {    // 臨時休園・登園自粛: 休園日数分を減額
      const perDay = 減免後 / monthDays;
      fee = Math.max(0, 減免後 - Math.floor(perDay * (Number(pr.closedDays) || 0)));
    }
  }
  return {
    tier: base.tier, tierLabel: base.tierLabel, unfiled,
    baseFee: base.baseFee, きょうだい軽減後, 減免後, finalFee: fee, childOrder: Math.max(1, Number(input.childOrder) || 1),
    内訳: { 階層: base.tier, きょうだい軽減後, 給付単価限度額: input.subsidyCap ?? null, 減免後, 日割後: fee },
    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 childcareUsersFile = join(dataRoot, "childcare-users.json");
  const childcareApplicationsFile = join(dataRoot, "childcare-applications.json");
  const kosodateSenkoFile = join(dataRoot, "kosodate-senko.json");
  const kosodateNinteiFile = join(dataRoot, "kosodate-shisetsu-nintei.json");
  const kosodatePaymentsFile = join(dataRoot, "kosodate-payments.json");
  const kosodateClaimsFile = join(dataRoot, "kosodate-claims.json");
  const kosodateReentryFile = join(dataRoot, "kosodate-reentry.json");
  const kosodateConfigFile = join(dataRoot, "kosodate-config.json");
  const kosodateShunoFile = join(dataRoot, "kosodate-shuno.json");
  const kosodateEnrollFile = join(dataRoot, "kosodate-enrollments.json");


    // ── 子育て支援 利用者 (Childcare Users) ──────────────────────────────

    if (pathname === "/api/childcare-users" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const users = (await dbSelect("kosodate.childcare_user")) ?? await readJson(childcareUsersFile, []);
      const restrictions = await readJson(restrictionsFile, []);
      return reply(res, 200, users.map((u) => maskRecord(actor, restrictions, u)));
    }

    if (pathname === "/api/childcare-users" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.parentName || !body.childName || !body.childBirthDate) return reply(res, 400, { error: "parentName, childName, childBirthDate required" });
      const users = (await dbSelect("kosodate.childcare_user")) ?? await readJson(childcareUsersFile, []);
      const user = { id: crypto.randomUUID(), parentName: body.parentName, childName: body.childName, childBirthDate: body.childBirthDate, address: body.address ?? "", status: "registered", createdBy: actor.id, createdAt: new Date().toISOString() };
      const dbInserted = await dbInsert("kosodate.childcare_user", user); if (dbInserted === null) { users.push(user); await writeJson(childcareUsersFile, users); }
      await audit(req, "childcare-user.register", { id: user.id });
      return reply(res, 201, user);
    }

    // ── 保育所入所申請 (Childcare Applications) ────────────────────────

    if (pathname === "/api/childcare-applications" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const apps = (await dbSelect("kosodate.childcare_application")) ?? await readJson(childcareApplicationsFile, []);
      const { userId, status } = Object.fromEntries(new URL(req.url, "http://x").searchParams);
      let result = apps;
      if (userId) result = result.filter((a) => a.userId === userId);
      if (status) result = result.filter((a) => a.status === status);
      return reply(res, 200, result);
    }

    if (pathname === "/api/childcare-applications" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.userId || !body.facilityName || !body.desiredStartDate) return reply(res, 400, { error: "userId, facilityName, desiredStartDate required" });
      const users = (await dbSelect("kosodate.childcare_user")) ?? await readJson(childcareUsersFile, []);
      if (!users.find((u) => u.id === body.userId)) return reply(res, 404, { error: "user not found" });
      const apps = (await dbSelect("kosodate.childcare_application")) ?? await readJson(childcareApplicationsFile, []);
      const app = { id: crypto.randomUUID(), userId: body.userId, facilityName: body.facilityName, desiredStartDate: body.desiredStartDate, status: "applied", createdBy: actor.id, createdAt: new Date().toISOString() };
      apps.push(app);
      await writeJson(childcareApplicationsFile, apps);
      await audit(req, "childcare-application.apply", { id: app.id });
      return reply(res, 201, app);
    }

    const appDecideMatch = pathname.match(/^\/api\/childcare-applications\/([^/]+)\/decide$/);
    if (appDecideMatch && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const app = (await dbSelectOne("kosodate.childcare_application", appDecideMatch[1]))
        ?? (await readJson(childcareApplicationsFile, [])).find((a) => a.id === appDecideMatch[1]);
      if (!app) return reply(res, 404, { error: "application not found" });
      if (app.status !== "applied") return reply(res, 409, { error: "application is not in applied status" });
      const body = await parseBody(req);
      if (!["approved", "rejected", "waitlisted"].includes(body.decision)) return reply(res, 400, { error: "decision must be approved, rejected, or waitlisted" });
      app.status = body.decision;
      app.decidedAt = new Date().toISOString();
      app.decidedBy = actor.id;
      const upd = await dbUpdate("kosodate.childcare_application", app.id, app); if (upd === null) { const wl = await readJson(childcareApplicationsFile, []); const wi = wl.findIndex((x) => x.id === app.id); if (wi !== -1) { wl[wi] = app; } await writeJson(childcareApplicationsFile, wl); }
      await audit(req, "childcare-application.decide", { id: app.id, decision: body.decision });
      return reply(res, 200, app);
    }


    const userMatch = pathname.match(/^\/api\/childcare-users\/([^/]+)$/);
    if (userMatch && (req.method === "GET" || req.method === "PUT")) {
      requireRole(actor, "CLERK");
      const id = userMatch[1];
      const user = (await dbSelectOne("kosodate.childcare_user", id))
        ?? (await readJson(childcareUsersFile, [])).find((u) => u.id === id);
      if (!user) return reply(res, 404, { error: "childcare user not found" });
      const restrictions = await readJson(restrictionsFile, []);
      if (req.method === "GET") return reply(res, 200, maskRecord(actor, restrictions, user));

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

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


    // ── 保育料（利用者負担額）の算定（標準仕様書準拠）──
    if (pathname === "/api/childcare/fee" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeNurseryFee(body || {});
      await audit(req, "childcare.fee-calc", { tier: result.tier, fee: result.fee });
      return reply(res, 200, result);
    }

    // ── 利用者負担額の詳細算定（税未申告は最高階層・減免・給付単価限度額上限・月途中/臨時休園の日割り）──
    if (pathname === "/api/kosodate/futan" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeFutanDetail(body || {});
      await audit(req, "kosodate.futan-detail", { tier: result.tier, unfiled: result.unfiled, finalFee: result.finalFee });
      return reply(res, 200, result);
    }

    // ════ 入所選考（利用調整）・施設等利用給付・保育料消込／充当・給食費還付（標準仕様書）════

    // ── 入所選考: 指数（基準＋きょうだい加点＋施設別加減点）→自動選考→同点抽出。
    //    シミュレーション（リハーサル）、二次調整等の複数回実行、却下→保留の次回繰越に対応 ──
    if (pathname === "/api/kosodate/senko/run" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.facilities) || !Array.isArray(b.applicants)) {
        return reply(res, 400, { error: "facilities[]{id,capacity} と applicants[]{id,baseIndex,siblingEnrolled,facilityPrefs[]} が必須です" });
      }
      const SIBLING_BONUS = Number(b.siblingBonus ?? 20); // きょうだい在園加点（自動）
      const remainingCap = Object.fromEntries(b.facilities.map((f) => [f.id, Math.max(0, Number(f.capacity) || 0)]));
      const accepted = [];
      const rejected = [];
      const ties = [];
      // 施設ごとに志望者を集め、指数降順で定員まで内定
      for (const facility of b.facilities) {
        const candidates = b.applicants
          .map((a) => {
            const pref = (a.facilityPrefs ?? []).find((p) => p.facilityId === facility.id);
            if (!pref) return null;
            const index = (Number(a.baseIndex) || 0) + (a.siblingEnrolled ? SIBLING_BONUS : 0) + (Number(pref.adjust) || 0);
            return { applicantId: a.id, index };
          })
          .filter(Boolean)
          .sort((x, y) => y.index - x.index);
        // 同一指数の児童抽出（境界の同点）
        const counts = {};
        for (const c of candidates) counts[c.index] = (counts[c.index] ?? 0) + 1;
        for (const [index, n] of Object.entries(counts)) if (n > 1) ties.push({ facilityId: facility.id, index: Number(index), count: n });
        const alreadyAccepted = new Set(accepted.map((a) => a.applicantId));
        for (const c of candidates) {
          if (alreadyAccepted.has(c.applicantId)) continue;
          if (remainingCap[facility.id] > 0) {
            remainingCap[facility.id] -= 1;
            accepted.push({ ...c, facilityId: facility.id });
            alreadyAccepted.add(c.applicantId);
          }
        }
      }
      const acceptedIds = new Set(accepted.map((a) => a.applicantId));
      for (const a of b.applicants) {
        if (!acceptedIds.has(a.id)) {
          // 却下でも申請有効期間内は保留として次回選考（二次調整）の対象に繰り越す
          rejected.push({ applicantId: a.id, status: a.validUntil && a.validUntil >= (b.asOf ?? new Date().toISOString().slice(0, 10)) ? "保留（次回選考対象）" : "却下" });
        }
      }
      const result = { round: b.round ?? "1次", simulate: b.simulate === true, accepted, rejected, ties };
      if (b.simulate !== true) {
        const runs = await readJson(kosodateSenkoFile, []);
        runs.unshift({ id: crypto.randomUUID(), ...result, at: new Date().toISOString(), by: actor.id });
        await writeJson(kosodateSenkoFile, runs);
      }
      await audit(req, "kosodate-senko.run", { round: result.round, simulate: result.simulate, accepted: accepted.length });
      return reply(res, 200, result);
    }
    if (pathname === "/api/kosodate/senko/runs" && req.method === "GET") {
      requireRole(actor, "CLERK");
      return reply(res, 200, (await readJson(kosodateSenkoFile, [])).slice(0, 50));
    }

    // ── 施設等利用給付の認定（1号=満3歳日以降／2号=満3歳日以降の直近4/1以降のエラーチェック）──
    if (pathname === "/api/kosodate/shisetsu-riyo/nintei" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const classes = ["1号", "2号", "3号"];
      if (!classes.includes(b.ninteiClass) || !b.birthDate || !b.startDate) {
        return reply(res, 400, { error: `ninteiClass(${classes.join("/")}), birthDate, startDate required` });
      }
      const birth = new Date(b.birthDate);
      const age3 = new Date(birth.getFullYear() + 3, birth.getMonth(), birth.getDate()); // 満3歳日
      const start = new Date(b.startDate);
      if (b.ninteiClass === "1号" && start < age3) {
        return reply(res, 400, { error: "認定1号の認定開始日は満3歳日以降である必要があります", age3: age3.toISOString().slice(0, 10) });
      }
      if (b.ninteiClass === "2号") {
        const april1 = new Date(age3.getMonth() + 1 >= 5 || (age3.getMonth() + 1 === 4 && age3.getDate() > 1) ? age3.getFullYear() + 1 : age3.getFullYear(), 3, 1);
        if (start < april1) {
          return reply(res, 400, { error: "認定2号の認定開始日は満3歳日以降の直近の4月1日以降である必要があります", april1: april1.toISOString().slice(0, 10) });
        }
      }
      const payMethods = ["償還払い", "法定代理受領（確定払）", "法定代理受領（概算払い）"];
      const nintei = {
        id: crypto.randomUUID(), ninteiClass: b.ninteiClass, birthDate: b.birthDate, startDate: b.startDate,
        payMethod: payMethods.includes(b.payMethod) ? b.payMethod : "償還払い",
        sharedWith: [], createdBy: actor.id, createdAt: new Date().toISOString(),
      };
      const list = await readJson(kosodateNinteiFile, []);
      list.unshift(nintei);
      await writeJson(kosodateNinteiFile, list);
      await audit(req, "kosodate-nintei.create", { ninteiClass: nintei.ninteiClass, payMethod: nintei.payMethod });
      return reply(res, 201, nintei);
    }
    // 認定情報を実費徴収に係る補足給付事業へ共有（複写）
    {
      const shareMatch = pathname.match(/^\/api\/kosodate\/shisetsu-riyo\/nintei\/([^/]+)\/share$/);
      if (shareMatch && req.method === "POST") {
        requireRole(actor, "CLERK");
        const list = await readJson(kosodateNinteiFile, []);
        const idx = list.findIndex((n) => n.id === shareMatch[1]);
        if (idx === -1) return reply(res, 404, { error: "nintei not found" });
        list[idx].sharedWith.push({ program: "実費徴収に係る補足給付", at: new Date().toISOString() });
        await writeJson(kosodateNinteiFile, list);
        await audit(req, "kosodate-nintei.share", { id: shareMatch[1] });
        return reply(res, 200, list[idx]);
      }
    }

    // ── 保育料等の個別消込・きょうだい間充当・給食費（副食費＋主食費）一本化還付 ──
    if (pathname === "/api/kosodate/hoikuryo/pay" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.childId || b.amount === undefined || b.charge === undefined) {
        return reply(res, 400, { error: "childId, charge(請求額), amount(入金額) required" });
      }
      const applied = Math.min(Number(b.charge), Number(b.amount));
      let over = Number(b.amount) - applied;
      const allocations = [];
      // 過納分はきょうだいの未納（siblings[]{childId,unpaid}）へ充当できる
      if (over > 0 && Array.isArray(b.siblings)) {
        for (const s of b.siblings) {
          if (over <= 0) break;
          const put = Math.min(over, Number(s.unpaid) || 0);
          if (put > 0) { allocations.push({ childId: s.childId, amount: put }); over -= put; }
        }
      }
      const payments = await readJson(kosodatePaymentsFile, []);
      const payment = { id: crypto.randomUUID(), childId: String(b.childId), charge: Number(b.charge),
        amount: Number(b.amount), applied, siblingAllocations: allocations, refundable: over,
        at: new Date().toISOString(), by: actor.id };
      payments.unshift(payment);
      await writeJson(kosodatePaymentsFile, payments);
      await audit(req, "kosodate-pay.apply", { childId: payment.childId, applied, allocations: allocations.length });
      return reply(res, 201, payment);
    }
    if (pathname === "/api/kosodate/kyushoku/refund" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (b.fukushokuhi === undefined || b.shushokuhi === undefined || !b.accountNumber) {
        return reply(res, 400, { error: "fukushokuhi(副食費), shushokuhi(主食費), accountNumber(還付用口座) required" });
      }
      const total = Number(b.fukushokuhi) + Number(b.shushokuhi); // 一本化された給食費として還付
      await audit(req, "kosodate-kyushoku.refund", { total });
      return reply(res, 200, { kind: "保育所給食費還付（副食費＋主食費の一本化）", total,
        account: `****${String(b.accountNumber).slice(-4)}` });
    }

    // ════ 事業所給付費・再転入引継・個人番号確認・保存期間・住民記録連携（標準仕様書）════

    // ── 事業所の給付費請求（公定価格＋地方単独事業＋延長保育＋補足給付の合算）→審査→支払 ──
    if (pathname === "/api/kosodate/kyufu/claims" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.jigyoshoId) return reply(res, 400, { error: "jigyoshoId required" });
      const publicPrice = Number(b.publicPrice) || 0; // 公定価格（国の定める基準）分
      const localAddOn = Number(b.localAddOn) || 0; // 地方単独事業
      const extendedCare = Number(b.extendedCare) || 0; // 延長保育
      const hosoku = Number(b.hosoku) || 0; // 給付対象施設への補足給付
      const claims = await readJson(kosodateClaimsFile, []);
      const claim = {
        id: crypto.randomUUID(), jigyoshoId: String(b.jigyoshoId),
        breakdown: { publicPrice, localAddOn, extendedCare, hosoku },
        total: publicPrice + localAddOn + extendedCare + hosoku, // 合算して請求額を算出
        bankCode: b.bankCode ?? "0000", accountNumber: b.accountNumber ?? "",
        status: "請求受領", at: new Date().toISOString(), by: actor.id,
      };
      claims.unshift(claim);
      await writeJson(kosodateClaimsFile, claims);
      await audit(req, "kosodate-claim.receive", { jigyoshoId: claim.jigyoshoId, total: claim.total });
      return reply(res, 201, claim);
    }
    {
      const reviewMatch = pathname.match(/^\/api\/kosodate\/kyufu\/claims\/([^/]+)\/review$/);
      if (reviewMatch && req.method === "POST") {
        requireRole(actor, "ADMIN"); // 審査・支払い
        const b = await parseBody(req);
        const claims = await readJson(kosodateClaimsFile, []);
        const idx = claims.findIndex((c) => c.id === reviewMatch[1]);
        if (idx === -1) return reply(res, 404, { error: "claim not found" });
        claims[idx].status = b.approve === false ? "差戻し" : "支払決定";
        claims[idx].reviewedBy = actor.id;
        claims[idx].reviewedAt = new Date().toISOString();
        await writeJson(kosodateClaimsFile, claims);
        await audit(req, "kosodate-claim.review", { id: reviewMatch[1], status: claims[idx].status });
        return reply(res, 200, claims[idx]);
      }
    }
    // 事業所ごとの給付費請求書出力・全銀協フォーマットの支払データ出力
    if (pathname === "/api/kosodate/kyufu/payment-data" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const claims = await readJson(kosodateClaimsFile, []);
      const approved = claims.filter((c) => c.status === "支払決定");
      const byJigyosho = {};
      for (const c of approved) (byJigyosho[c.jigyoshoId] ??= []).push(c);
      const rows = Object.entries(byJigyosho).map(([jigyoshoId, items]) => ({
        jigyoshoId,
        invoice: `給付費請求書（${jigyoshoId}）`, // 事業所ごとの請求書
        zengin: { bankCode: items[0].bankCode, accountNumber: items[0].accountNumber,
          amount: items.reduce((s, c) => s + c.total, 0) }, // 全銀協フォーマット相当
        claims: items.length,
      }));
      await audit(req, "kosodate-kyufu.payment-data", { jigyosho: rows.length });
      return reply(res, 200, { format: "全銀協フォーマット", rows });
    }

    // ── 再転入者の情報引継（転出時点のスナップショットを保存・確認・引き続き利用）──
    if (pathname === "/api/kosodate/reentry" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || !b.snapshot) return reply(res, 400, { error: "atenaId, snapshot(転出時点情報) required" });
      const list = await readJson(kosodateReentryFile, []);
      list.unshift({ id: crypto.randomUUID(), atenaId: String(b.atenaId), snapshot: b.snapshot,
        movedOutAt: b.movedOutAt ?? new Date().toISOString().slice(0, 10), at: new Date().toISOString(), by: actor.id });
      await writeJson(kosodateReentryFile, list);
      await audit(req, "kosodate-reentry.save", { atenaId: b.atenaId });
      return reply(res, 201, { saved: true });
    }
    {
      const reentryMatch = pathname.match(/^\/api\/kosodate\/reentry\/([^/]+)$/);
      if (reentryMatch && req.method === "GET") {
        requireRole(actor, "CLERK");
        const list = await readJson(kosodateReentryFile, []);
        const hits = list.filter((r) => r.atenaId === reentryMatch[1]);
        if (!hits.length) return reply(res, 404, { error: "転出時点の情報がありません" });
        await audit(req, "kosodate-reentry.recall", { atenaId: reentryMatch[1] });
        return reply(res, 200, { atenaId: reentryMatch[1], reusable: true, snapshots: hits });
      }
    }

    // ── 個人番号の確認（番号法別表第一の事務要件を満たす場合のみ・監査必須）──
    if (pathname === "/api/kosodate/mynumber/check" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const roles = ["本人", "保護者", "世帯員"];
      const allowedPurposes = ["子ども・子育て支援給付の支給", "施設等利用給付の支給", "保育料の徴収"];
      if (!b.atenaId || !roles.includes(b.role)) return reply(res, 400, { error: `atenaId, role(${roles.join("/")}) required` });
      if (!allowedPurposes.includes(b.purpose)) {
        return reply(res, 403, { error: "番号法別表第一の事務要件を満たさない目的です", allowedPurposes });
      }
      await audit(req, "kosodate-mynumber.check", { atenaId: b.atenaId, role: b.role, purpose: b.purpose });
      return reply(res, 200, { atenaId: b.atenaId, role: b.role, purpose: b.purpose,
        myNumber: "************", note: "番号法別表第一要件を確認のうえ表示（参照実装はマスク）" });
    }

    // ── 保存期間の任意指定（地方公共団体ごとの設定）と住民記録システムからの住所取得 ──
    if (pathname === "/api/kosodate/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(kosodateConfigFile, {});
      config.retention = { ...(config.retention ?? {}), [String(b.docKind)]: Number(b.years) };
      await writeJson(kosodateConfigFile, config);
      await audit(req, "kosodate-config.retention", { docKind: b.docKind, years: Number(b.years) });
      return reply(res, 200, config.retention);
    }
    if (pathname === "/api/kosodate/address/from-juki" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || !b.jukiAddress) return reply(res, 400, { error: "atenaId, jukiAddress required" });
      await audit(req, "kosodate-address.from-juki", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId), address: String(b.jukiAddress),
        source: "住民記録システム", note: "住所は住民記録システムから取得（手入力ではなく連携値を正とする）" });
    }

    // ════ EUC演算子・在園管理・収納/口座振替・月割り・国基準判定（標準仕様書）════

    // ── EUC抽出（一般的な演算子: and/or・=・≠・＞・＜・≧・≦・部分/前方/後方一致）──
    if (pathname === "/api/kosodate/euc/query" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      const conditions = Array.isArray(b.conditions) ? b.conditions : [];
      if (!rows.length || !conditions.length) return reply(res, 400, { error: "rows[], conditions[]{field,op,value} required" });
      const OPS = {
        "=": (a, v) => String(a) === String(v),
        "≠": (a, v) => String(a) !== String(v),
        ">": (a, v) => Number(a) > Number(v),
        "<": (a, v) => Number(a) < Number(v),
        "≧": (a, v) => Number(a) >= Number(v),
        "≦": (a, v) => Number(a) <= Number(v),
        "部分一致": (a, v) => String(a).includes(String(v)),
        "前方一致": (a, v) => String(a).startsWith(String(v)),
        "後方一致": (a, v) => String(a).endsWith(String(v)),
      };
      const bad = conditions.find((c) => !OPS[c.op]);
      if (bad) return reply(res, 400, { error: `unknown op: ${bad.op}`, supported: Object.keys(OPS) });
      const logic = b.logic === "or" ? "or" : "and";
      const test = (row) => conditions.map((c) => OPS[c.op](row[c.field], c.value));
      const hits = rows.filter((row) => (logic === "and" ? test(row).every(Boolean) : test(row).some(Boolean)));
      await audit(req, "kosodate-euc.query", { conditions: conditions.length, logic, hits: hits.length });
      return reply(res, 200, { logic, count: hits.length, results: hits });
    }

    // ── 内定取下（取下後は待機に戻す）──
    if (pathname === "/api/kosodate/senko/naitei/withdraw" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.applicantId) return reply(res, 400, { error: "applicantId required" });
      const enrollments = await readJson(kosodateEnrollFile, []);
      enrollments.unshift({ id: crypto.randomUUID(), kind: "内定取下", applicantId: String(b.applicantId),
        reason: b.reason ?? "", statusAfter: "待機", at: new Date().toISOString(), by: actor.id });
      await writeJson(kosodateEnrollFile, enrollments);
      await audit(req, "kosodate-senko.naitei-withdraw", { applicantId: b.applicantId });
      return reply(res, 200, { applicantId: String(b.applicantId), withdrawn: true, statusAfter: "待機" });
    }

    // ── 転園処理 ──
    if (pathname === "/api/kosodate/tenen" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.childId || !b.fromFacility || !b.toFacility) return reply(res, 400, { error: "childId, fromFacility, toFacility required" });
      const enrollments = await readJson(kosodateEnrollFile, []);
      const record = { id: crypto.randomUUID(), kind: "転園", childId: String(b.childId),
        fromFacility: String(b.fromFacility), toFacility: String(b.toFacility),
        date: b.date ?? new Date().toISOString().slice(0, 10), at: new Date().toISOString(), by: actor.id };
      enrollments.unshift(record);
      await writeJson(kosodateEnrollFile, enrollments);
      await audit(req, "kosodate-tenen", { childId: record.childId });
      return reply(res, 201, record);
    }

    // ── 複数児童の一括退所 ──
    if (pathname === "/api/kosodate/taisho/bulk" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const childIds = Array.isArray(b.childIds) ? b.childIds : [];
      if (!childIds.length) return reply(res, 400, { error: "childIds[] required" });
      const date = b.date ?? new Date().toISOString().slice(0, 10);
      const enrollments = await readJson(kosodateEnrollFile, []);
      const records = childIds.map((c) => ({ id: crypto.randomUUID(), kind: "退所", childId: String(c),
        date, reason: b.reason ?? "", bulk: true, at: new Date().toISOString(), by: actor.id }));
      enrollments.unshift(...records);
      await writeJson(kosodateEnrollFile, enrollments);
      await audit(req, "kosodate-taisho.bulk", { count: records.length });
      return reply(res, 200, { processed: records.length, date, records });
    }

    // ── 複数科目の合算（1つの科目として扱う）──
    if (pathname === "/api/kosodate/shuno/subjects/merge" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const subjects = Array.isArray(b.subjects) ? b.subjects : [];
      if (subjects.length < 2 || !b.mergedName) return reply(res, 400, { error: "subjects[](2科目以上), mergedName required" });
      const shuno = await readJson(kosodateShunoFile, {});
      shuno.mergedSubjects = [...(shuno.mergedSubjects ?? []),
        { id: crypto.randomUUID(), name: String(b.mergedName), components: subjects.map(String), at: new Date().toISOString() }];
      await writeJson(kosodateShunoFile, shuno);
      await audit(req, "kosodate-shuno.merge-subjects", { mergedName: b.mergedName, components: subjects.length });
      return reply(res, 200, { mergedName: String(b.mergedName), components: subjects.map(String), treatedAs: "1科目" });
    }

    // ── 科目に応じた法令上利用可能な支払方法 ──
    if (pathname === "/api/kosodate/shuno/payment-methods" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const METHODS = {
        "保育料": ["口座振替", "納付書", "コンビニ収納", "クレジットカード", "スマートフォン決済"],
        "学校給食費": ["口座振替", "納付書", "コンビニ収納"],
        "延長保育料": ["口座振替", "納付書", "現金（窓口）"],
        "一時預かり利用料": ["納付書", "現金（窓口）"],
      };
      const subject = searchParams.get("subject") ?? "";
      if (subject) {
        const methods = METHODS[subject];
        if (!methods) return reply(res, 404, { error: "unknown subject", subjects: Object.keys(METHODS) });
        return reply(res, 200, { subject, methods });
      }
      return reply(res, 200, { subjects: Object.entries(METHODS).map(([s, m]) => ({ subject: s, methods: m })) });
    }

    // ── 口座振替依頼データの出力（指定期間内の振替不能者パターン・金融機関別/科目＋金融機関別）──
    if (pathname === "/api/kosodate/furikae/export" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[]{payerId,bank,subject,failedAt?,failReason?} required" });
      let target = rows;
      if (b.pattern === "振替不能者") {
        const from = b.from ?? "0000-00-00", to = b.to ?? "9999-12-31";
        target = rows.filter((r) => r.failedAt && r.failedAt >= from && r.failedAt <= to);
      }
      const keyOf = b.groupBy === "科目＋金融機関" ? (r) => `${r.subject}/${r.bank}` : (r) => String(r.bank);
      const groups = {};
      for (const r of target) (groups[keyOf(r)] ??= []).push(r);
      const files = Object.entries(groups).map(([key, items]) => ({ key, count: items.length }));
      await audit(req, "kosodate-furikae.export", { pattern: b.pattern ?? "全件", groupBy: b.groupBy ?? "金融機関", files: files.length });
      return reply(res, 200, { pattern: b.pattern ?? "全件", groupBy: b.groupBy ?? "金融機関", total: target.length, files });
    }

    // ── 残高不足の再振替（一括・個別の再振替依頼データ出力）──
    if (pathname === "/api/kosodate/furikae/retry" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const modes = ["一括", "個別"];
      const failures = Array.isArray(b.failures) ? b.failures : [];
      if (!modes.includes(b.mode) || !failures.length) return reply(res, 400, { error: `mode(${modes.join("/")}), failures[]{payerId,failReason} required` });
      const eligible = failures.filter((f) => f.failReason === "残高不足");
      const target = b.mode === "個別" ? eligible.filter((f) => (b.payerIds ?? []).includes(f.payerId)) : eligible;
      await audit(req, "kosodate-furikae.retry", { mode: b.mode, eligible: eligible.length, retried: target.length });
      return reply(res, 200, { mode: b.mode, eligible: eligible.length, retried: target.length,
        excluded: failures.length - eligible.length, note: "再振替対象は失敗事由が残高不足の者のみ" });
    }

    // ── 収滞納・過誤納情報の納付義務者ごとの確認（複数児童分をまとめて表示）──
    if (pathname === "/api/kosodate/shuno/by-payer" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const records = Array.isArray(b.records) ? b.records : [];
      if (!records.length) return reply(res, 400, { error: "records[]{payerId,childId,unpaid,overpaid} required" });
      const byPayer = {};
      for (const r of records) {
        const p = (byPayer[r.payerId] ??= { payerId: String(r.payerId), children: [], unpaidTotal: 0, overpaidTotal: 0 });
        if (!p.children.includes(r.childId)) p.children.push(r.childId);
        p.unpaidTotal += Number(r.unpaid ?? 0);
        p.overpaidTotal += Number(r.overpaid ?? 0);
      }
      await audit(req, "kosodate-shuno.by-payer", { payers: Object.keys(byPayer).length });
      return reply(res, 200, { payers: Object.values(byPayer) });
    }

    // ── 本料の欠損処理（延滞金・遅延損害金も連動して欠損）──
    if (pathname === "/api/kosodate/shuno/kesson" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      if (!b.levyId) return reply(res, 400, { error: "levyId required" });
      const written = { levyId: String(b.levyId), principal: Number(b.principal ?? 0),
        delinquentCharge: Number(b.delinquentCharge ?? 0), lateDamages: Number(b.lateDamages ?? 0) };
      const shuno = await readJson(kosodateShunoFile, {});
      shuno.kesson = [...(shuno.kesson ?? []), { ...written, at: new Date().toISOString(), by: actor.id }];
      await writeJson(kosodateShunoFile, shuno);
      await audit(req, "kosodate-shuno.kesson", { levyId: written.levyId });
      return reply(res, 200, { ...written, linkedWriteOff: true,
        note: "本料の欠損に連動して延滞金・遅延損害金も同時に欠損" });
    }

    // ── 滞納繰越処理の自動化 ──
    if (pathname === "/api/kosodate/shuno/taino-kurikoshi" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      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[]{levyId,unpaid} required" });
      const carried = rows.filter((r) => Number(r.unpaid ?? 0) > 0)
        .map((r) => ({ levyId: String(r.levyId), unpaid: Number(r.unpaid), toYear: Number(b.fiscalYear) + 1, kubun: "滞納繰越分" }));
      await audit(req, "kosodate-shuno.taino-kurikoshi", { fiscalYear: b.fiscalYear, carried: carried.length });
      return reply(res, 200, { fiscalYear: Number(b.fiscalYear), automated: true, carried });
    }

    // ── 未払分過誤納の歳入歳出切替 ──
    if (pathname === "/api/kosodate/shuno/kagono/switch" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const kinds = ["歳入", "歳出"];
      if (!b.id || !kinds.includes(b.to)) return reply(res, 400, { error: `id, to(${kinds.join("/")}) required` });
      await audit(req, "kosodate-shuno.kagono-switch", { id: b.id, to: b.to });
      return reply(res, 200, { id: String(b.id), switchedTo: b.to,
        note: "未払分の過誤納還付を歳入（戻出）/歳出（還付金）間で切替" });
    }

    // ── 保育料の月割り対応（月途中の入退所は日割りでなく月割り。開始月から月数で按分）──
    if (pathname === "/api/kosodate/hoikuryo/monthly" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const annual = Number(b.annualAmount);
      const months = Math.min(12, Math.max(1, Number(b.months) || 12));
      if (!annual) return reply(res, 400, { error: "annualAmount, months(1-12) required" });
      const per = Math.floor(annual / months);
      const first = annual - per * (months - 1); // 端数は初回月に寄せる
      await audit(req, "kosodate-hoikuryo.monthly", { annual, months });
      return reply(res, 200, { annualAmount: annual, months, monthlyAmount: per, firstMonthAmount: first,
        schedule: [first, ...Array(months - 1).fill(per)] });
    }

    // ── 国の定める基準に基づく該当者の自動判定（副食費免除: 所得割57,700円未満または第3子以降）──
    if (pathname === "/api/kosodate/kuni-kijun/judge" && 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[]{atenaId,taxIncomeLevy,childOrder} required" });
      const LIMIT = 57700; // 国基準: 副食費免除の所得割上限
      const results = households.map((h) => {
        const byIncome = Number(h.taxIncomeLevy ?? 0) < LIMIT;
        const byOrder = Number(h.childOrder ?? 1) >= 3;
        return { atenaId: String(h.atenaId), eligible: byIncome || byOrder,
          basis: byIncome ? "所得割57,700円未満" : byOrder ? "第3子以降" : "" };
      });
      await audit(req, "kosodate-kuni-kijun.judge", { judged: results.length, eligible: results.filter((r) => r.eligible).length });
      return reply(res, 200, { standard: "国の定める基準（副食費免除）", results });
    }

    // ── 保育所得割の自動計算（標準仕様書 0280314・政令市の税源移譲前算定）──
    //   方式1: 課税所得金額×6%（その後に税額控除）。 方式2: 市町村民税所得割×(6/8)。
    if (pathname === "/api/kosodate/hoiku-shotokuwari" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = (await parseBody(req)) || {};
      const method = b.method === "市町村民税所得割" ? "市町村民税所得割" : "課税所得金額";
      let 保育所得割;
      if (method === "課税所得金額") {
        const rate = Number(b.税率 ?? 0.06); // 税源移譲前の旧税率（標準6%）
        const 税額控除 = Math.max(0, Number(b.税額控除) || 0);
        保育所得割 = Math.max(0, Math.floor(Math.max(0, Number(b.課税所得金額) || 0) * rate) - 税額控除);
      } else {
        const 所得割 = Math.max(0, Number(b.市町村民税所得割) || 0);
        保育所得割 = Math.floor(所得割 * 6 / 8); // 新税率所得割×6/8で旧税率相当へ換算
      }
      await audit(req, "kosodate-hoiku-shotokuwari", { method, 保育所得割 });
      return reply(res, 200, { method, 保育所得割,
        note: "政令市の税源移譲前の保育所得割。方式1=課税所得金額×6%(税額控除後)、方式2=市町村民税所得割×6/8。" });
    }

    // ── 施設型給付費等の給付予定額の算定・再計算（標準仕様書 0280351/0280353）──
    //   積算根拠(単価×数量＋加算)から給付予定額を算定。当初単価・改正単価に分けて自動計算し、利用者負担額を控除。
    //   items[]: { 単価種別("当初"/"改正"), 単価, 数量, 加算 }。修正時はこの再計算で給付予定情報を更新する。
    if (pathname === "/api/kosodate/kyufu-yotei" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = (await parseBody(req)) || {};
      const items = Array.isArray(b.items) ? b.items : [];
      const 利用者負担額 = Math.max(0, Number(b.利用者負担額) || 0);
      const calc = (種別) => items.filter((it) => (it.単価種別 ?? "当初") === 種別)
        .reduce((s, it) => s + (Math.max(0, Number(it.単価) || 0) * Math.max(0, Number(it.数量) || 0) + (Number(it.加算) || 0)), 0);
      const 当初 = calc("当初"), 改正 = calc("改正");
      const 総額 = 当初 + 改正;
      const 給付予定額 = Math.max(0, 総額 - 利用者負担額);
      await audit(req, "kosodate-kyufu-yotei", { 給付予定額 });
      return reply(res, 200, { 当初単価分: 当初, 改正単価分: 改正, 総額, 利用者負担額, 給付予定額,
        note: "給付予定額＝(当初単価分＋改正単価分)−利用者負担額。当初/改正単価に分けて積算し再計算に使用。" });
    }
    // ── 概算払いの精算（標準仕様書 0280360）──
    //   実績照会・単価改正の結果、既払いの概算払いに余剰・不足があれば、次回(任意)概算払いで上乗せ/相殺する。
    if (pathname === "/api/kosodate/gaisan-seisan" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = (await parseBody(req)) || {};
      const 実績額 = Number(b.実績額) || 0, 既払額 = Number(b.既払額) || 0;
      const 差額 = 実績額 - 既払額;
      const 区分 = 差額 > 0 ? "不足(上乗せ)" : 差額 < 0 ? "余剰(相殺)" : "増減なし";
      await audit(req, "kosodate-gaisan-seisan", { 差額, 区分 });
      return reply(res, 200, { 実績額, 既払額, 差額, 区分, 上乗せ額: Math.max(0, 差額), 相殺額: Math.max(0, -差額),
        note: "概算払いの余剰/不足を次回(任意)概算払い時に上乗せ/相殺する精算額を算出。" });
    }

    // ── 子ども子育て支援の各種情報の管理（登録・修正・削除・照会）: 不現住/保護者/世帯/特記/住民税/所得照会回答/受付場所/現況届ステータス/広域協議/概算払い金額等を種別別CRUD保持 ──
    const kosodateRecordsFile = join(dataRoot, "kosodate-records.json");
    if (pathname === "/api/kosodate/records" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const 種別 = searchParams.get("種別"), atena = searchParams.get("宛名番号") ?? searchParams.get("子どもID");
      const all = await readJson(kosodateRecordsFile, []);
      const list = all.filter((x) => (!種別 || x.種別 === 種別) && (!atena || String(x.宛名番号 ?? x.子どもID ?? "") === String(atena)));
      return reply(res, 200, { 件数: list.length, records: list });
    }
    if (pathname === "/api/kosodate/records" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.種別) return reply(res, 400, { error: "種別(不現住/保護者/世帯/特記/住民税/所得照会回答/受付場所/現況届/広域協議/概算払い 等) が必要です" });
      const all = await readJson(kosodateRecordsFile, []);
      const { id: _i, createdAt: _c, ...fields } = b || {};
      const item = { id: crypto.randomUUID(), ...fields, createdAt: new Date().toISOString(), by: actor.id };
      all.unshift(item);
      await writeJson(kosodateRecordsFile, all);
      await audit(req, "kosodate-record.create", { id: item.id, 種別: item.種別 });
      return reply(res, 201, item);
    }
    const kosodateRecordItemMatch = pathname.match(/^\/api\/kosodate\/records\/([^/]+)$/);
    if (kosodateRecordItemMatch) {
      const all = await readJson(kosodateRecordsFile, []);
      const idx = all.findIndex((x) => x.id === kosodateRecordItemMatch[1]);
      if (req.method === "GET") { requireRole(actor, "CLERK"); return idx === -1 ? reply(res, 404, { error: "記録が見つかりません" }) : reply(res, 200, all[idx]); }
      if (req.method === "PUT") {
        requireRole(actor, "CLERK"); if (idx === -1) return reply(res, 404, { error: "記録が見つかりません" });
        const b = await parseBody(req); const { id: _i, createdAt: _c, ...patch } = b || {};
        all[idx] = { ...all[idx], ...patch, updatedAt: new Date().toISOString() };
        await writeJson(kosodateRecordsFile, all); await audit(req, "kosodate-record.update", { id: all[idx].id });
        return reply(res, 200, all[idx]);
      }
      if (req.method === "DELETE") {
        requireRole(actor, "CLERK"); if (idx === -1) return reply(res, 404, { error: "記録が見つかりません" });
        const [removed] = all.splice(idx, 1);
        await writeJson(kosodateRecordsFile, all); await audit(req, "kosodate-record.delete", { id: removed.id });
        return reply(res, 200, { deleted: true, id: removed.id });
      }
    }

  return false;
}
