概要

項目内容
案件IDMAS-119
カテゴリDDL・マスタ(パイプライン・RPA・外部連携 横断)
PhaseP2
優先度★★
所要時間5〜7 時間(5 Step)
対象ファイル000_infra/002_constants.js, 000_infra/003_contracts.js, 100_config/101_sys_config.js, 200_data/202_repository.js, 400_domain/401_rpa_hc.js, 800_ops/809_migration_headcount_rates.js(新規)
関連UIファイルtemplates/operations_sidebar.html(マイグレーションメニュー登録)
前提案件MAS-116(マイグレーション基盤), MAS-118(起票ターゲット月列)

目的

従業員ごとに 22_bud_headcount へ手入力してきた 保険料率 5 種(健保料率・介護保険料率・厚年料率・雇用保険料率・子ども・子育て拠出金率)を、新規マスタ 18_mst_tax_rate適用年度 × 雇用形態 × 各料率)に集約する。

22 タブは 適用年度雇用形態 を複合キーとしてマスタから料率をルックアップする方式に変更し、料率改定時はマスタ 1 箇所の更新で全 EMP に反映できるようにする。人為的な料率入力ミスを排除し、人件費予算計画の入力負荷を大幅に削減することが目的である。

源泉所得税額・住民税額は従業員個別の金額であるため、引き続き 22 タブ側に残す。RPA(401_rpa_hc.js)が 32_wrk_invoice へ生成する INV レコードのフォーマット・件数・摘要・金額は現状と完全に同一を維持することを非機能要件とする。

現在のコード

000_infra/002_constants.js L76(SHEET_DEFAULTS 内 22_bud_headcount エントリ)

{ pattern: '22_bud_headcount', prefix: 'EMP_', defaults: {
    '雇用形態': '正社員',
    '決済ラグ(月)': 1,
    '健保料率': 0.05,
    '介護保険料率': 0,
    '厚年料率': 0.0915,
    '雇用保険料率': 0.0065,
    '子ども・子育て拠出金率': 0.0036,
    '源泉所得税額': 0,
    '源泉消費税額': 0,
    '免税フラグ': false,
    '住民税額': 0,
    '月額給与・報酬': 0,
    '採用エージェント費': 0,
    'PC等初期費用': 0,
    _dynamic: { '適用年度': 'fiscalYear', '開始年月': 'nextYm' }
} },

000_infra/002_constants.js L93-112(ID_PREFIX_MAP

11_mst_account / 12_mst_partner / 13_mst_org / 14_mst_project / 15_mst_dictionary のマスタ系 5 種が登録済みだが、18_mst_tax_rate のエントリは存在しない。本案件で追加する。

100_config/101_sys_config.js L643-668(setupAllSchemas 内 schemas 定義)

L661 に BUD_HC(22_bud_headcount)の DDL ヘッダーが 41 列で定義されており、そのうち本案件の削除対象 5 列は以下:

"健保料率","健保額","介護保険料率","介護保険額","厚年料率","厚年額",
"雇用保険料率","雇用保険額","子ども・子育て拠出金率","子ども拠出金額"

削除対象 5 列: 健保料率 / 介護保険料率 / 厚年料率 / 雇用保険料率 / 子ども・子育て拠出金率

維持する 5 列(計算結果列): 健保額 / 介護保険額 / 厚年額 / 雇用保険額 / 子ども拠出金額 は RPA が参照するため残す(値は「月額給与・報酬 × マスタ料率」でシート式 or RPA 側再計算)。

さらに L588-640 で 01_sys_config シートへのシステムキー登録が appendRow 方式で実装されており、既存キー BUD_HC 等が登録されている。本案件で MST_TAXRATE キーを追加する。

400_domain/401_rpa_hc.js L22-34(現行の列参照)

const hcData = hcSheet.getDataRange().getValues();
const h = hcData[0];

const col = {};
['有効フラグ', '管理ID', '氏名・ポジション', '雇用形態', '科目名', '取引先名',
 '適用年度', '入社年月', '退職年月', '開始年月', '終了年月',
 '月額給与・報酬', '決済手段', '決済ラグ(月)', '支払基準日', '休日調整', 'CF計上',
 '免税フラグ', '源泉所得税額', '源泉消費税額', '住民税額',
 '健保額', '介護保険額', '厚年額', '雇用保険額', '子ども拠出金額',
 '法定福利費合計', '社保預り金合計', '社保控除後支給額', '差引支給額',
 '組織名', '備考'].forEach(function(name) { col[name] = h.indexOf(name); });

400_domain/401_rpa_hc.js L189-196(料率を経由した金額取得)

const monthlySalary = col['月額給与・報酬'] !== -1 ? (Number(row[col['月額給与・報酬']]) || 0) : 0;
const siDeduction  = col['社保預り金合計']  !== -1 ? (Number(row[col['社保預り金合計']]) || 0) : 0;
const legalWelfare = col['法定福利費合計']  !== -1 ? (Number(row[col['法定福利費合計']]) || 0) : 0;

現状、RPA は 社保預り金合計 / 法定福利費合計 のシート計算結果(= 月額報酬 × 料率 の合算)を読み取る構造になっており、料率列を直接参照するコードは存在しない。料率はシート式として 22_bud_headcount 内部で 健保額 = 月額給与 × 健保料率 等に使われている。

改修後: 料率 5 列が 22 タブから消えるため、健保額 等の派生列の計算根拠が失われる。このため RPA 側で TaxRateRepository.findRatesMap() から料率を取得し、「月額給与・報酬 × マスタ料率」を算出して 社保預り金合計 / 法定福利費合計 と同等の値を RPA 内部で再計算する。22 タブの派生 5 列(健保額 等)はシート上は参照表示用として残すが、RPA ロジックはシート値に依存せずマスタ + 給与額から独立に算出する。

冪等性チェック(既存仕様)

401_rpa_hc.js L202, L229, L256, L283, L310, L337, L364, L391, L418, L446 でそれぞれ isDuplicate_(invData, invHeaders, <memoKey>) による摘要一致ガードが効いており、既存 INV が残存する場合は再起票されない。この冪等性ロジックは一切変更しない。

既存マイグレーション(参考: 800_ops/808_migration_i24.js

migrationI24() が 33_wrk_bank の決済口座を書換える雛形。構造は以下:

  • 先頭で Utils.auditLog('MIGRATE', …, 'START')、末尾で 'END'
  • SpreadsheetApp.getUi().alert(...) でエラーダイアログ・完了ダイアログ
  • 有効フラグ=FALSE の行はスキップ
  • 「既に更新済み(= 新形式)」の行はスキップ(冪等性)
  • Utils.logInfo + ui.alert の両方で完了サマリを出力

templates/operations_sidebar.html L89-95 の「🔧 マイグレーション」ブロックにボタンが並んでおり、migrationD01D03 / migrationD04D06 / migrationI10 / migrationI24 の 4 件が登録されている。本案件で migrationHeadcountRates を追加する。

修正方針

新規マスタシート 18_mst_tax_rate

シート用途: 会計年度・雇用形態ごとに社会保険・雇用保険の料率を一元管理するマスタ。22_bud_headcount からの料率ルックアップ元。

スキーマ(列構成・10 列):

#列名必須備考
1マスタIDstringprefix=TXR_、4桁連番(例: TXR_0001
2適用年度stringYYYY 形式(例: 2025
3雇用形態string15_mst_dictionary の雇用形態カテゴリと同一値(役員/正社員/アルバイト/業務委託/顧問 等)
4健保料率number0〜1 の小数(例: 0.05)
5介護保険料率number0〜1(40〜64歳のみ、未該当は 0)
6厚年料率number0〜1(例: 0.0915)
7雇用保険料率number0〜1(例: 0.0065)
8子ども・子育て拠出金率number0〜1(例: 0.0036。会社全額負担)
9有効フラグbooleanTRUE/FALSE。FALSE は全処理でスキップ
10備考string改定根拠・通達番号等の自由記述

複合キー: 適用年度 + 雇用形態(文字列連結 String(適用年度) + '_' + String(雇用形態))。同一複合キーに重複が存在する場合は「行番号の大きい方(後の行)」を採用し、console.warn で警告。

100_config/101_sys_config.js の変更

Step A: setupAllSchemasschemas オブジェクトに DDL 定義を追加(L643-668 の連続する 'XXX_YYYY': { headers: [...], color: '...' } 列の適切な位置に追記):

'MST_TAXRATE': { headers: [
  "マスタID","適用年度","雇用形態","健保料率","介護保険料率","厚年料率",
  "雇用保険料率","子ども・子育て拠出金率","有効フラグ","備考"
], color: "#666666" },

color はマスタ系と同じグレー(#666666)で統一する。

Step B: 01_sys_config シートへのシステムキー登録を追加(L588-640 の appendRow 列に以下を追記。位置は他のマスタ系キー(MST_ACCT 等)が登録されている箇所の近く、または BUD_HC の手前が望ましい):

if (!existKeys.includes('MST_TAXRATE')) confSheet.appendRow(['MST_TAXRATE', '', '18_mst_tax_rate', 'マスタ_税率/保険料率']);

Step C: 22_bud_headcount の BUD_HC headers 定義から 5 列を削除(L661):

- 'BUD_HC': { headers: ["有効フラグ","管理ID","氏名・ポジション","雇用形態","科目名","取引先名","適用年度","入社年月","退職年月","開始年月","終了年月","月額給与・報酬","決済手段","決済ラグ(月)","支払基準日","休日調整","CF計上","免税フラグ","源泉所得税額","源泉消費税額","住民税額","健保料率","健保額","介護保険料率","介護保険額","厚年料率","厚年額","雇用保険料率","雇用保険額","子ども・子育て拠出金率","子ども拠出金額","法定福利費合計","社保預り金合計","社保控除後支給額","差引支給額","採用エージェント費","PC等初期費用","組織名","起票ターゲット月","最終起票年月日","備考"], color: "#c90076" },
+ 'BUD_HC': { headers: ["有効フラグ","管理ID","氏名・ポジション","雇用形態","科目名","取引先名","適用年度","入社年月","退職年月","開始年月","終了年月","月額給与・報酬","決済手段","決済ラグ(月)","支払基準日","休日調整","CF計上","免税フラグ","源泉所得税額","源泉消費税額","住民税額","健保額","介護保険額","厚年額","雇用保険額","子ども拠出金額","法定福利費合計","社保預り金合計","社保控除後支給額","差引支給額","採用エージェント費","PC等初期費用","組織名","起票ターゲット月","最終起票年月日","備考"], color: "#c90076" },

Step D: templates/operations_sidebar.html L89-94 の「🔧 マイグレーション」ブロックに新規ボタンを末尾(cleanupEmptyRows の直前)に追加:

<button class="btn" onclick="run('migrationHeadcountRates', this)">S-47 22タブ料率→18マスタ移行</button>

000_infra/002_constants.js の変更

SHEET_DEFAULTS L76 の 22_bud_headcount エントリから 5 フィールドを削除:

削除対象(値はすべてそのまま消す):

  • '健保料率': 0.05
  • '介護保険料率': 0
  • '厚年料率': 0.0915
  • '雇用保険料率': 0.0065
  • '子ども・子育て拠出金率': 0.0036

変更後エントリ(期待形):

{ pattern: '22_bud_headcount', prefix: 'EMP_', defaults: {
    '雇用形態': '正社員',
    '決済ラグ(月)': 1,
    '源泉所得税額': 0,
    '源泉消費税額': 0,
    '免税フラグ': false,
    '住民税額': 0,
    '月額給与・報酬': 0,
    '採用エージェント費': 0,
    'PC等初期費用': 0,
    _dynamic: { '適用年度': 'fiscalYear', '開始年月': 'nextYm' }
} },
  • '雇用形態' / '決済ラグ(月)' / '源泉所得税額' / '源泉消費税額' / '免税フラグ' / '住民税額' / '月額給与・報酬' / '採用エージェント費' / 'PC等初期費用'変更しない
  • _dynamic 内の '適用年度': 'fiscalYear'RPA マスタ参照キーとして必須のため削除しない
  • _dynamic 内の '開始年月': 'nextYm' も変更しない

ID_PREFIX_MAP L93-112 に以下を追加11_mst_account エントリの直後、マスタ群の並びを意識した位置):

{ pattern: '18_mst_tax_rate',     prefix: 'TXR_', digit: 4, isDate: false },

000_infra/003_contracts.js の変更

既存 @typedef のパターン(OrderDTO / InvoiceDTO / BankTxDTO / JournalEntryDTO / BudgetDTO と同形)に倣い、ファイル末尾セクション 2(予算 DTO)の BudgetDTO の後ろに以下を追記する:

/**
 * 18_mst_tax_rate — 税率・保険料率マスタ(S-47)
 * @typedef {Object} TaxRateDTO
 * @property {string}  マスタID              - "TXR_NNNN"
 * @property {string}  適用年度              - "YYYY"
 * @property {string}  雇用形態              - "役員" | "正社員" | "アルバイト" | "業務委託" | "顧問" 等
 * @property {number}  健保料率              - 0〜1
 * @property {number}  介護保険料率          - 0〜1
 * @property {number}  厚年料率              - 0〜1
 * @property {number}  雇用保険料率          - 0〜1
 * @property {number}  子ども・子育て拠出金率 - 0〜1
 * @property {boolean} 有効フラグ
 * @property {string}  備考
 */

DTO ファクトリ(Contracts.toDto / Contracts.toRow 等)は既存実装がヘッダー名駆動で汎用的なため追加不要。

200_data/202_repository.js の変更

ファイル末尾(AccountRepository の直後・L351 の閉じ括弧の下)に TaxRateRepository を追記する。AccountRepository完全に同じ構造を踏襲する:

// =====================================================================
// TaxRateRepository — 18_mst_tax_rate (読み取り専用マスタ・S-47)
// =====================================================================

var TaxRateRepository = {

  /** @private */
  _getSheet: function() {
    return Utils.getSheetByKey('MST_TAXRATE', '18_mst_tax_rate');
  },

  /**
   * 全税率マスタレコードを DTO 配列で取得する。
   * @returns {{ headers: string[], dtos: TaxRateDTO[] }}
   */
  findAll: function() {
    return readSheetAsDtos_(TaxRateRepository._getSheet());
  },

  /**
   * 複合キー (適用年度_雇用形態) → 料率オブジェクトのマップを返す (キャッシュ付き)。
   * 有効フラグ=FALSE の行はスキップ。
   * 同一複合キーの重複は行番号が大きい方 (後の行) を採用し console.warn を出力。
   * @returns {Object.<string, {健保料率:number, 介護保険料率:number, 厚年料率:number, 雇用保険料率:number, 子ども・子育て拠出金率:number}>}
   */
  findRatesMap: function() {
    if (TaxRateRepository._cache) return TaxRateRepository._cache;
    var result = TaxRateRepository.findAll();
    var map = {};
    for (var i = 0; i < result.dtos.length; i++) {
      var dto = result.dtos[i];
      var flag = dto['有効フラグ'];
      if (flag === false || String(flag).toUpperCase() === 'FALSE') continue;
      var year = String(dto['適用年度'] || '').trim();
      var emp  = String(dto['雇用形態']  || '').trim();
      if (!year || !emp) continue;
      var key = year + '_' + emp;
      if (map[key]) {
        console.warn('[TaxRateRepository] 複合キー重複を検出。後の行を採用: ' + key);
      }
      map[key] = {
        '健保料率':               Number(dto['健保料率']) || 0,
        '介護保険料率':           Number(dto['介護保険料率']) || 0,
        '厚年料率':               Number(dto['厚年料率']) || 0,
        '雇用保険料率':           Number(dto['雇用保険料率']) || 0,
        '子ども・子育て拠出金率': Number(dto['子ども・子育て拠出金率']) || 0,
      };
    }
    TaxRateRepository._cache = map;
    return map;
  },

  /** @private */
  _cache: null,

  /** キャッシュをリセットする */
  resetCache: function() {
    TaxRateRepository._cache = null;
  },
};
  • 既存プライベートヘルパー readSheetAsDtos_ をそのまま流用する
  • 新しいヘルパー関数をこのファイルに追加しない(202_repository.js のヘルパーは readSheetAsDtos_ / writeDtosToSheet_ / findLastRow_ / appendDtosToSheet_ の 4 本のみを維持)

400_domain/401_rpa_hc.js の変更

Step A: 列参照リスト(L28-34)から 5 列を削除:

- '健保額', '介護保険額', '厚年額', '雇用保険額', '子ども拠出金額',
- '法定福利費合計', '社保預り金合計', '社保控除後支給額', '差引支給額',

これらの派生列は RPA が自前で計算するため、シートから読む必要がなくなる(シート上の計算列は表示用として残っても良いが RPA は参照しない)。

Step B: ループ先頭で TaxRateRepository.findRatesMap() を 1 回だけ呼び出す(L70 付近、for ループの直前):

var ratesMap = TaxRateRepository.findRatesMap();

Step C: 行ごとの料率ルックアップ(L84-88 の empType / appYear 抽出直後に追加):

var rateKey = appYear + '_' + empType;
var rates = ratesMap[rateKey];
if (!rates) {
  Utils.logError(FUNC, '18_mst_tax_rate に料率未登録: ' + rateKey + ' (EMP=' + empName + ')');
  continue; // 該当 EMP 行の INV 生成を丸ごとスキップ。フォールバック値は使用しない
}

Step D: 月ループ内の金額取得ブロック(L189-196)を以下に置換:

const monthlySalary = col['月額給与・報酬'] !== -1 ? (Number(row[col['月額給与・報酬']]) || 0) : 0;
const withholdingTax = col['源泉所得税額'] !== -1 ? (Number(row[col['源泉所得税額']]) || 0) : 0;
const residentTax = col['住民税額'] !== -1 ? (Number(row[col['住民税額']]) || 0) : 0;

// S-47: マスタから取得した料率で社保・雇保を算出(旧:シート計算列を参照)
var healthIns     = Math.round(monthlySalary * rates['健保料率']);
var careIns       = Math.round(monthlySalary * rates['介護保険料率']);
var pensionIns    = Math.round(monthlySalary * rates['厚年料率']);
var employmentIns = Math.round(monthlySalary * rates['雇用保険料率']);
var childContrib  = Math.round(monthlySalary * rates['子ども・子育て拠出金率']);

// 従業員負担分 (社保預り金合計): 健保 + 介護 + 厚年 の従業員負担 (折半想定) + 雇用保険(従業員負担分)
// → 現行のシート計算式と同一のロジックを踏襲する (従前の 社保預り金合計 列と一致すること)
var siDeduction = Math.round((healthIns + careIns + pensionIns) / 2) + Math.round(employmentIns / 3);
// 会社負担分 (法定福利費合計): 社保会社折半 + 雇用保険会社負担 + 子ども拠出金
var legalWelfare = (healthIns + careIns + pensionIns) - Math.round((healthIns + careIns + pensionIns) / 2)
                 + (employmentIns - Math.round(employmentIns / 3))
                 + childContrib;

const isExempt = col['免税フラグ'] !== -1 && (row[col['免税フラグ']] === true || String(row[col['免税フラグ']]).toUpperCase() === 'TRUE');
const srcConsumptionTax = col['源泉消費税額'] !== -1 ? (Number(row[col['源泉消費税額']]) || 0) : 0;

重要: siDeduction / legalWelfare の算出式は 現行シート計算式と完全一致させること。差異が出ると 32_wrk_invoice の 税込金額_計画 が変わり RPA レコードフォーマット維持の要件に反する。Step 1 実装時に 22_bud_headcount のシート式(健保額 列の formula 等)を確認し、同じ丸め・折半ロジックを移植する。

Step E: ORD 側 の料率依存なしを確認(L96-133)。ORD 生成は 月額給与・報酬源泉消費税額 のみ使用しており、料率列には依存していないため変更不要。

Step F: 冪等性ロジック(isDuplicate_(invData, invHeaders, <memoKey>))は L202 以降の全 9 箇所すべて変更しない。

800_ops/809_migration_headcount_rates.js の新規作成

既存 808_migration_i24.js のパターンを踏襲し、冪等・Human-in-the-Loop・監査ログ付きで実装する:

/**
 * =========================================================
 * 809_migration_headcount_rates.js — S-47: 22タブ料率→18マスタ移行
 * =========================================================
 * 22_bud_headcount からユニークな (適用年度 × 雇用形態) × 料率パターンを抽出し、
 * 18_mst_tax_rate に登録する。既存エントリは冪等スキップ。
 *
 * 冪等性: 同一 (適用年度, 雇用形態) が既に 18_mst_tax_rate に存在する場合はスキップ
 * Human-in-the-Loop: 登録前に SpreadsheetApp.getUi().alert でプレビュー確認
 * 手順: dev で実行・検証 → npm run push:prod → prod で実行
 */
function migrationHeadcountRates() {
  var FUNC = 'migrationHeadcountRates';
  Utils.auditLog('MIGRATE', '', '', '', FUNC, '', '', 'START');
  try {
    var ss = getWebSpreadsheet_();
    var ui = SpreadsheetApp.getUi();

    var hcSheet = ss.getSheetByName(Utils.getSheetNameByKey('BUD_HC') || '22_bud_headcount');
    var txrSheet = ss.getSheetByName(Utils.getSheetNameByKey('MST_TAXRATE') || '18_mst_tax_rate');
    if (!hcSheet)  throw new Error('22_bud_headcount が見つかりません');
    if (!txrSheet) throw new Error('18_mst_tax_rate が見つかりません。先に setupAllSchemas を実行してください');

    // ① 22 タブ走査 → 複合キー × 料率パターン抽出
    var hc = hcSheet.getDataRange().getValues();
    var hH = hc[0];
    var iFlag = hH.indexOf('有効フラグ');
    var iYear = hH.indexOf('適用年度');
    var iType = hH.indexOf('雇用形態');
    var iRates = {
      '健保料率':               hH.indexOf('健保料率'),
      '介護保険料率':           hH.indexOf('介護保険料率'),
      '厚年料率':               hH.indexOf('厚年料率'),
      '雇用保険料率':           hH.indexOf('雇用保険料率'),
      '子ども・子育て拠出金率': hH.indexOf('子ども・子育て拠出金率'),
    };
    if (iYear === -1 || iType === -1) {
      throw new Error('22_bud_headcount に「適用年度」または「雇用形態」列が見つかりません。既に移行済みの可能性があります。');
    }

    var patterns = {}; // key = "YYYY_雇用形態" -> { 料率 } (最初に検出した値を採用)
    var skippedEmpty = 0;
    var warnedMismatch = 0;
    for (var r = 1; r < hc.length; r++) {
      if (iFlag !== -1) {
        var f = hc[r][iFlag];
        if (f === false || String(f).toUpperCase() === 'FALSE') continue;
      }
      var y = String(hc[r][iYear] || '').trim();
      var t = String(hc[r][iType] || '').trim();
      if (!y || !t) { skippedEmpty++; continue; }
      var key = y + '_' + t;
      var p = {
        '健保料率':               Number(hc[r][iRates['健保料率']]) || 0,
        '介護保険料率':           Number(hc[r][iRates['介護保険料率']]) || 0,
        '厚年料率':               Number(hc[r][iRates['厚年料率']]) || 0,
        '雇用保険料率':           Number(hc[r][iRates['雇用保険料率']]) || 0,
        '子ども・子育て拠出金率': Number(hc[r][iRates['子ども・子育て拠出金率']]) || 0,
      };
      if (!patterns[key]) {
        patterns[key] = p;
      } else {
        // 同一複合キーで料率が異なる場合は警告
        var existing = patterns[key];
        var diff = false;
        Object.keys(p).forEach(function(k) { if (p[k] !== existing[k]) diff = true; });
        if (diff) {
          Utils.logInfo(FUNC, '同一複合キーで料率不一致: ' + key + ' (先頭行の値を採用)');
          warnedMismatch++;
        }
      }
    }

    // ② 18_mst_tax_rate 既存エントリ読込 → 冪等性チェック
    var txr = txrSheet.getDataRange().getValues();
    var tH = txr[0];
    var tiFlag = tH.indexOf('有効フラグ');
    var tiYear = tH.indexOf('適用年度');
    var tiType = tH.indexOf('雇用形態');
    var existingKeys = {};
    for (var tr = 1; tr < txr.length; tr++) {
      if (tiFlag !== -1) {
        var tf = txr[tr][tiFlag];
        if (tf === false || String(tf).toUpperCase() === 'FALSE') continue;
      }
      var ty = String(txr[tr][tiYear] || '').trim();
      var tt = String(txr[tr][tiType] || '').trim();
      if (ty && tt) existingKeys[ty + '_' + tt] = true;
    }

    // ③ 新規登録候補を抽出(既存はスキップ)
    var candidates = [];
    Object.keys(patterns).forEach(function(k) {
      if (existingKeys[k]) return;
      candidates.push({ key: k, rates: patterns[k] });
    });

    if (candidates.length === 0) {
      ui.alert('登録対象なし', '18_mst_tax_rate に登録すべき新規料率パターンは見つかりませんでした。\n(空行スキップ: ' + skippedEmpty + '件 / 料率不一致警告: ' + warnedMismatch + '件)', ui.ButtonSet.OK);
      Utils.auditLog('MIGRATE', '', '', '', FUNC, '', { summary: 'no candidates' }, 'END');
      return;
    }

    // ④ Human-in-the-Loop: プレビュー確認ダイアログ
    var previewLines = candidates.map(function(c) {
      return '  ' + c.key + ': 健保=' + c.rates['健保料率'] + ' 介護=' + c.rates['介護保険料率'] + ' 厚年=' + c.rates['厚年料率'] + ' 雇保=' + c.rates['雇用保険料率'] + ' 子ども=' + c.rates['子ども・子育て拠出金率'];
    }).join('\n');
    var resp = ui.alert('S-47 料率マスタ移行',
      candidates.length + '件の料率パターンを 18_mst_tax_rate に登録します。\nよろしいですか?\n\n' + previewLines,
      ui.ButtonSet.YES_NO);
    if (resp !== ui.Button.YES) {
      ui.alert('キャンセルしました', '登録は実行されませんでした。', ui.ButtonSet.OK);
      Utils.auditLog('MIGRATE', '', '', '', FUNC, '', { summary: 'cancelled by user' }, 'END');
      return;
    }

    // ⑤ 18_mst_tax_rate へ追記
    var tiId = tH.indexOf('マスタID');
    var tiRate = {
      '健保料率':               tH.indexOf('健保料率'),
      '介護保険料率':           tH.indexOf('介護保険料率'),
      '厚年料率':               tH.indexOf('厚年料率'),
      '雇用保険料率':           tH.indexOf('雇用保険料率'),
      '子ども・子育て拠出金率': tH.indexOf('子ども・子育て拠出金率'),
    };
    var tiNote = tH.indexOf('備考');

    var lastRow = txrSheet.getLastRow();
    var idCounter = lastRow; // 既存件数基準で連番付与
    var newRows = [];
    candidates.forEach(function(c) {
      idCounter++;
      var parts = c.key.split('_');
      var y = parts[0]; var t = parts.slice(1).join('_');
      var row = new Array(tH.length).fill('');
      row[tiFlag] = true;
      row[tiId]   = 'TXR_' + ('0000' + idCounter).slice(-4);
      row[tiYear] = y;
      row[tiType] = t;
      Object.keys(tiRate).forEach(function(k) { row[tiRate[k]] = c.rates[k]; });
      if (tiNote !== -1) row[tiNote] = 'S-47 マイグレーション自動登録';
      newRows.push(row);
    });
    txrSheet.getRange(lastRow + 1, 1, newRows.length, tH.length).setValues(newRows);

    var summary = 'S-47 22タブ料率→18マスタ移行\n'
      + '新規登録: ' + newRows.length + '件\n'
      + '既存スキップ: ' + (Object.keys(patterns).length - newRows.length) + '件\n'
      + '空行スキップ: ' + skippedEmpty + '件\n'
      + '料率不一致警告: ' + warnedMismatch + '件';
    Utils.auditLog('MIGRATE', '', '', '', FUNC, '', { summary: summary }, 'END');
    Utils.logInfo(FUNC, summary);
    ui.alert('マイグレーション完了', summary, ui.ButtonSet.OK);

  } catch (e) {
    Utils.logError(FUNC, e);
    SpreadsheetApp.getUi().alert('エラー', e.message, SpreadsheetApp.getUi().ButtonSet.OK);
  }
}

影響範囲

レイヤー影響備考
000_infra/002_constants.jsSHEET_DEFAULTS / ID_PREFIX_MAP5 フィールド削除・TXR_ エントリ追加
000_infra/003_contracts.jsTaxRateDTO 追加既存 DTO 改変なし
100_config/101_sys_config.jssetupAllSchemas / MST_TAXRATE キー登録 / BUD_HC DDL から 5 列削除実行順序厳守: マイグレ → 再 setupAllSchemas
200_data/202_repository.jsTaxRateRepository 追加既存 Repository / ヘルパーは無改変
400_domain/401_rpa_hc.js料率取得を TaxRateRepository.findRatesMap() に置換INV 生成フォーマット・件数・摘要は維持
800_ops/809_migration_headcount_rates.js新規冪等・Human-in-the-Loop
templates/operations_sidebar.html🔧 マイグレーションボタン追加1 行追加
他の 32_wrk_invoice 参照ロジック影響なしINV フォーマット不変
データマート(61〜85 系)影響なしINV/STL 入力が不変のため

注意事項

  1. 実行順序の厳守: マイグレーション 809_migration_headcount_rates先に実行して 22_bud_headcount 上の料率データを 18_mst_tax_rate に移行してから、次に setupAllSchemas22_bud_headcount の 5 列削除を適用すること。逆順にすると既存の料率データが DDL 列削除で消失し、マイグレーションの抽出元が失われる。
  2. TaxRateRepository202_repository.js の既存プライベートヘルパー(readSheetAsDtos_)のみを使用し、新しいヘルパー関数を同ファイルに追加しない。
  3. 列参照はヘッダー名ベースindexOf 方式)で実装する。列番号ハードコード禁止(CLAUDE.md 規約)。
  4. 有効フラグ=FALSE の行はマスタ参照時にスキップ(全処理共通ルール)。
  5. 冪等性必須: マイグレーション再実行時に既存エントリが重複登録されないこと。18_mst_tax_rate 既存エントリの (適用年度, 雇用形態) をキーにスキップ判定を行う。
  6. RPA 側の INV レコードフォーマット・件数・摘要・金額は現状を維持する(非機能要件)。siDeduction / legalWelfare の算出式は現行シート式(従業員折半 + 会社負担 + 子ども拠出金)と完全一致させること。
  7. 401_rpa_hc.jsisDuplicate_ ガードは一切変更しない(L202 以降の全 9 箇所)。既存 INV との衝突を回避するための冪等性ロジック。
  8. ID_PREFIX_MAP への追加は必須。これがないと 18_mst_tax_rate で新規行に TXR_ 接頭辞の ID が自動発番されない(003_contracts.js / 004_utils.js の ID 発番ロジックが該当エントリを参照)。
  9. プロダクトポリシー(Human-in-the-Loop)準拠: マイグレーションは SpreadsheetApp.getUi().alert で登録候補プレビューを表示し、ユーザーが「はい」を選択した場合のみ書き込む。

エッジケース

#条件挙動理由
1RPA 実行時、18_mst_tax_rate に該当する (適用年度 × 雇用形態) の有効レコードが存在しない該当従業員の INV 生成を丸ごとスキップUtils.logError でエラー出力。フォールバック値・デフォルト料率は一切使用しないフォールバックによる誤料率での仕訳自動生成を排除。マスタへの登録を強制することで人為ミス由来の誤起票を防ぐ
218_mst_tax_rate に同一複合キー (適用年度_雇用形態) で有効フラグ=TRUE が重複findRatesMap() が後から出現した行を採用(= 行番号が大きい方)。console.warn で「複合キー重複を検出。後の行を採用: 」を警告出力後勝ちで運用者がデータクレンズできる挙動に統一。先勝ちにすると誤登録行を修正しても効かず混乱する
3マイグレーション実行時、22_bud_headcount 内で同一 (適用年度 × 雇用形態) に複数の異なる料率が混在最初に検出した料率セットをマスタに登録。後続の不整合行は Utils.logInfo で警告出力し、マスタ登録はスキップ冪等性確保。人間によるデータ修正を前提とした設計(警告を見て 22 タブ側の値を統一させる)
422_bud_headcount の行で 適用年度 または 雇用形態 が空白RPA はその行をスキップ(if (!rates) continue)。マイグレーションもその行を無視し skippedEmpty カウンタをインクリメントして警告ログ出力複合キーが不完全なレコードの誤処理を防止。空白=未確定データとみなす
5マイグレーション 2 回目実行(冪等性検証)existingKeys[key] により既存登録済みの複合キーをスキップし、「登録対象なし」ダイアログを表示して終了多重実行で重複登録が起きないこと。Human-in-the-Loop ダイアログも「0 件登録」時は不要なため登録なしダイアログに切替
6setupAllSchemasマイグレーション前に実行した(誤操作)22_bud_headcount の 5 列が削除され、マイグレ実行時に throw new Error('22_bud_headcount に「適用年度」または「雇用形態」列が見つかりません。既に移行済みの可能性があります。') でフェイル誤った実行順序で 22 タブのデータが消失した場合、明示的にエラーを出して人間に気付かせる(サイレント失敗を防ぐ)
7介護保険料率 が 0 の従業員(40 歳未満・65 歳以上)rates['介護保険料率'] = 0 となり、careIns = 0 で正常計算される料率 0 は有効な値。マスタ側で 介護保険料率=0 のレコードを正社員<40歳用に登録することで対応
8月額給与・報酬 が 0 の行(採用前・退職済等)monthlySalary * rate = 0 で全社保額が 0 となり、RPA 内の if (siDeduction > 0) ガードによりその従業員の社保系 INV はスキップされる既存ロジック(L391 等の > 0 ガード)をそのまま踏襲

Human-in-the-Loop

CLAUDE.md プロダクトポリシー「Human-in-the-Loop」(AI/自動処理の結果は必ず人間がレビュー・承認してから確定する)に準拠:

809_migration_headcount_rates.js18_mst_tax_rate への書き込み前に SpreadsheetApp.getUi().alert でプレビューダイアログを表示する:

S-47 料率マスタ移行
3件の料率パターンを 18_mst_tax_rate に登録します。
よろしいですか?

  2025_正社員: 健保=0.05 介護=0 厚年=0.0915 雇保=0.0065 子ども=0.0036
  2025_役員:   健保=0.05 介護=0 厚年=0.0915 雇保=0 子ども=0.0036
  2025_業務委託: 健保=0 介護=0 厚年=0 雇保=0 子ども=0

[はい] [いいえ]

ユーザーが「はい」を選択した場合のみ書き込みを実行する。「いいえ」選択時はキャンセルダイアログを表示して終了。

実データ検証

MCP またはスプレッドシート UI 経由で実装前に確認すべき項目:

#検証項目方法判定基準
122_bud_headcount シートの現行列構成MCP get_sheet_values("22_bud_headcount!A1:AZ1") で 1 行目を取得DDL スキーマ (BUD_HC headers) と完全一致すること。ズレがあれば DDL 適用を先に実施
2雇用形態 列の実データ値MCP または UI で 22_bud_headcount!D2:D の重複排除15_mst_dictionary の雇用形態カテゴリ値 (役員 / 正社員 / アルバイト / 業務委託 / 顧問 等) と一致すること。表記ゆれ(例: "業務委託 " 末尾スペース)があればマイグレ前にデータクレンズ
3適用年度 列の実データ形式22_bud_headcount!G2:G を抜粋確認YYYY 形式(4 桁整数または文字列)であること。YYYY-MM 形式が混在している場合は複合キー生成ロジックを修正 or データクレンズ
418_mst_tax_rate シートの有無01_sys_configMST_TAXRATE キー有無確認未作成であること(既存の場合はスキーマを確認し、DDL 設計を既存に合わせる)
5料率 5 列の現行値分布22_bud_headcount の G 列〜AE 列抜粋0.05 / 0.0915 / 0.0065 / 0.0036 などの固定値か、従業員ごとに異なる値か確認
6社保計算式の現行ロジック22_bud_headcount!W2(健保額セル)の formula 確認RPA に移植する Math.round(monthlySalary * rate) の折半ロジックが現行シート式と一致することを検証

関連ドキュメント

ドキュメント参照理由
CLAUDE.mdマイグレーションスクリプト運用ガイドライン(ファイル命名 / 冪等性 / メニュー登録 / 実行手順)・列参照ヘッダー名ベース規約
MAS-116 マイグレーション基盤前提案件。マイグレ実行履歴管理
MAS-120 取引先マスタ拡張による決済条件自動補完姉妹案件。マスタ化による入力簡素化の設計パターン参考
MAS-192 Repository 完全移行202_repository.js の Repository 設計パターン(AccountRepositoryTaxRateRepository のテンプレート)
MAS-179 監査証跡の強化Utils.auditLog('MIGRATE', ...) の使用パターン
MAS-166 立替精算STLの振込先名データ改善直近のマイグレーションスクリプト実装例(808_migration_i24.js
RPA HC 仕様書人件費 RPA の公開仕様。INV 生成ロジックを変更しないことの確認用

人間が検討すべき事項

TODO_future.md MAS-119 の記載および本仕様書作成時の Phase 1 調査で補足した事項:

#検討事項現時点の設計スタンス最終確定タイミング
1期中に料率改定された場合の適用ルール (開始年月ベース or 発生日ベース)初版は「適用年度 列 × 雇用形態 の複合キー」で単純化。年度末に次年度分マスタを登録する運用。月次精度で料率改定を反映したい場合は、マスタに 適用開始年月 列を追加して最長一致で解決する方式に拡張(将来改修)実装着手前にユーザー確認
2免税フラグの配置 (マスタ側 or EMP 個別)EMP 個別(22 タブ側)に残す。理由: 免税事業者認定は個人単位で付与されるため、雇用形態共通のマスタに置くとモデルとして不整合本仕様で確定済み
3既存 22 タブの列削除に伴う DDL/データ互換性削除 5 列(健保料率 等)を除く既存 EMP データは setupAllSchemas のヘッダー再構成で保全される。ただし該当 5 列に 0 以外の値が入っている EMP が残っている場合、マイグレーション実行前に保存される必要がある(実行順序厳守注意事項 1 で明文化済み
4介護保険料率 0 の扱い(40 歳未満・65 歳以上)年齢区分を雇用形態と組み合わせたマスタキーを作るか、別キー化するか要検討。初版は「正社員(40未満)」「正社員(40〜64)」のように雇用形態を細分化する運用で対応可能マスタ投入前にユーザー運用ポリシー確認
5料率の丸め粒度 (Math.round 単位 or 円未満 2 桁保持)既存シート式の Math.round(monthlySalary * rate) に合わせて整数円で丸める。実装 Step 3 で現行シート式を確認し同一ロジックを移植する実装 Step 3 で検証
6マイグレーション後の 22 タブ「健保額」等 5 派生列の扱い初版は RPA が直接料率 × 月額給与で計算するため、派生列はシート上「表示用」として残してよい(RPA は依存しない)。将来的に完全に削除したい場合は別案件で対応本仕様で一旦据え置き
718_mst_tax_rate の料率プルダウン検証初版は自由入力(数値)。UI料率範囲 等のプルダウンは不要本仕様で確定済み
8ID 発番衝突(既存 TXR_ 接頭辞の他利用がないか)ID_PREFIX_MAP / 既存シート走査で重複接頭辞なしを Phase 1 で確認済み。TXR_ は本案件で新設本仕様で確定済み

実装プロンプト(Claude Code 用)

あなたは GAS 会計システム(bizlp-gas-accounting) のシニア開発者です。
MAS-119「22 タブ人件費予算の入力簡素化(保険料率・税率マスタ化)」を実装してください。

## 実行前タスク(必須読み込み)

1. `docs/dev/dev_mas-119_headcount_rate_master.md` — 本仕様書全体を精読(特に「修正方針」と「エッジケース」「注意事項」)
2. `CLAUDE.md` — コーディング規約・マイグレーション運用ガイドライン・GAS ファイル番号体系
3. `000_infra/002_constants.js` — `SHEET_DEFAULTS` L76 の `22_bud_headcount` エントリ、`ID_PREFIX_MAP` L93-112 の全エントリを確認
4. `000_infra/003_contracts.js` — `OrderDTO` / `InvoiceDTO` / `BudgetDTO` の `@typedef` 記述パターン
5. `000_infra/004_utils.js` — `Utils.getSheetByKey(key, fallbackName)` / `Utils.getSheetNameByKey(sysKey)` の引数仕様(L23-50)
6. `100_config/101_sys_config.js` — `setupAllSchemas` L574 〜 の実装、`schemas` オブジェクト L643-668(特に `BUD_HC` L661 / `MST_ACCT` L644)、システムキー `appendRow` L588-640 のパターン
7. `200_data/202_repository.js` — `AccountRepository` L304-350 の構造(_getSheet / findAll / findAsMap / _cache / resetCache)、プライベートヘルパー `readSheetAsDtos_` L19-29
8. `400_domain/401_rpa_hc.js` — HC RPA 全体(特に L22-34 の列参照、L189-196 の金額取得、L202〜 の冪等性ガード `isDuplicate_`)
9. `800_ops/808_migration_i24.js` — マイグレーションスクリプトの雛形(冪等性 / auditLog / alert ダイアログ / 完了サマリ)
10. `templates/operations_sidebar.html` L88-95 — 🔧 マイグレーションメニューの登録パターン

## 修正対象ファイル(6 ファイル + UI 1 ファイル)

- `000_infra/002_constants.js` — **既存エントリを修正のみ**(`SHEET_DEFAULTS` / `ID_PREFIX_MAP`)
- `000_infra/003_contracts.js` — **末尾への追記のみ**(`TaxRateDTO` typedef)
- `100_config/101_sys_config.js` — **既存 `setupAllSchemas` への追記・修正**(システムキー登録・schemas・BUD_HC DDL)
- `200_data/202_repository.js` — **末尾への追記のみ**(`TaxRateRepository`)
- `400_domain/401_rpa_hc.js` — **`generateHcInvoices` 関数の改修**(料率取得ロジック置換)
- `800_ops/809_migration_headcount_rates.js` — **新規作成**
- `templates/operations_sidebar.html` — **🔧 マイグレーションボタンの追記 1 行**

## Step 分割(5 Step 順次実行)

### Step 1: DTO + Repository の追加

1-a. `000_infra/003_contracts.js` のセクション「2. 仕訳帳 / 予算 DTO」の `BudgetDTO` の後ろ(= `Contracts = {` の手前)に `TaxRateDTO` の `@typedef` を追記。
1-b. `200_data/202_repository.js` の末尾(`AccountRepository` の閉じ `};` の下)に `TaxRateRepository` を追記。
    - `_getSheet()` は `Utils.getSheetByKey('MST_TAXRATE', '18_mst_tax_rate')` を返す
    - `findAll()` は `readSheetAsDtos_(TaxRateRepository._getSheet())` を返す
    - `findRatesMap()`: キャッシュ付き、複合キー=`適用年度 + '_' + 雇用形態`、有効フラグ=FALSE スキップ、同一キー重複時は後の行を採用して `console.warn`
    - `_cache: null` + `resetCache()` を実装
    - **プライベートヘルパーを追加しない**(既存の `readSheetAsDtos_` のみ使用)

### Step 2: DDL スキーマ追加 + BUD_HC 修正

2-a. `100_config/101_sys_config.js` の `setupAllSchemas` 内 `schemas` オブジェクト(L643-668)に `MST_TAXRATE` エントリを追加。ヘッダーは仕様書「修正方針 / `100_config/101_sys_config.js` の変更 / Step A」と完全一致。
2-b. `01_sys_config` シートへのシステムキー `appendRow` 列(L588-640)に `MST_TAXRATE` 登録を追加(`MST_ACCT` 等マスタ系の近く)。
2-c. `BUD_HC` の headers 定義(L661)から `"健保料率"` / `"介護保険料率"` / `"厚年料率"` / `"雇用保険料率"` / `"子ども・子育て拠出金率"` の 5 列を削除(派生 5 列 `"健保額"` 等は残す)。

### Step 3: RPA 料率参照の置換

3-a. `400_domain/401_rpa_hc.js` L28-34 の列参照リストから不要な 5 列を削除(`健保額` / `介護保険額` / `厚年額` / `雇用保険額` / `子ども拠出金額` / `法定福利費合計` / `社保預り金合計` / `社保控除後支給額` / `差引支給額`)。
3-b. 先頭 `for` ループ手前(L70 付近)で `var ratesMap = TaxRateRepository.findRatesMap();` を 1 回だけ呼び出す。
3-c. 行ごとの処理内で `appYear + '_' + empType` を複合キーとして `ratesMap[key]` をルックアップ。未登録の場合 `Utils.logError` 出力 → `continue` でスキップ(フォールバック値は使わない)。
3-d. 月ループ内の `siDeduction` / `legalWelfare` を、料率 × 月額給与から再計算する式に置換。**現行シート式と同一の折半・丸めロジック**を移植すること(Step 3 着手前に `22_bud_headcount!W2`(健保額セル)等の formula を実データ検証で確認)。
3-e. `isDuplicate_` ガード(L202〜 の全 9 箇所)は**一切変更しない**。

### Step 4: 定数の更新

4-a. `000_infra/002_constants.js` L76 の `SHEET_DEFAULTS` 内 `22_bud_headcount` エントリから `健保料率` / `介護保険料率` / `厚年料率` / `雇用保険料率` / `子ども・子育て拠出金率` の 5 キーを削除。他のフィールドと `_dynamic` 内の `適用年度: 'fiscalYear'` は残す。
4-b. `ID_PREFIX_MAP` L93-112 に `{ pattern: '18_mst_tax_rate', prefix: 'TXR_', digit: 4, isDate: false }` を追加(`11_mst_account` エントリの直後が望ましい)。

### Step 5: マイグレーション新規作成 + メニュー登録

5-a. `800_ops/809_migration_headcount_rates.js` を新規作成。関数名 `migrationHeadcountRates`。仕様書「修正方針 / `800_ops/809_migration_headcount_rates.js` の新規作成」に示した疑似コードをベースに、冪等性(既存キースキップ)・Human-in-the-Loop(`alert` プレビュー確認)・`auditLog` START/END・`logInfo` サマリ・`logError` フォールバックを必ず実装。
5-b. `templates/operations_sidebar.html` L89-94 の「🔧 マイグレーション」ブロックに `<button class="btn" onclick="run('migrationHeadcountRates', this)">S-47 22タブ料率→18マスタ移行</button>` を `cleanupEmptyRows` の直前に追記。

## エッジケース(必ず実装でカバーすること)

- マスタ未登録の (適用年度 × 雇用形態): 該当 EMP の INV 生成スキップ + `logError`(フォールバック不可)
- マスタに同一複合キー重複: 後の行を採用 + `console.warn`
- マイグレで 22 タブに同一複合キーの異なる料率が混在: 最初の行を採用 + `logInfo` 警告
- `適用年度` または `雇用形態` が空白: 両方ともスキップして `skippedEmpty` カウント
- マイグレ 2 回目実行: `existingKeys` によりスキップして「登録対象なし」ダイアログ
- `setupAllSchemas` を先に実行してしまった場合: マイグレが `throw new Error` で失敗(列が存在しないため)
- `介護保険料率 = 0`: 有効値として扱い通常計算
- `月額給与・報酬 = 0`: 既存の `if (siDeduction > 0)` ガードで該当 INV スキップ

## 実データ検証(実装前に MCP 等で確認)

- `22_bud_headcount` の 1 行目(全ヘッダー)と DDL の一致
- `雇用形態` 列の重複排除値(DDL 辞書との乖離)
- `適用年度` 列のフォーマット(`YYYY` か `YYYY-MM` か)
- `18_mst_tax_rate` シートの有無(新規作成が正しいか)
- `健保額` 列の formula(`Math.round(月額給与 * 健保料率)` 等と同一か)

## 動作確認手順

1. `npm run push:dev`
2. GAS 操作サイドバーから `setupAllSchemas` を実行し、次を確認:
   - 新規シート `18_mst_tax_rate` が生成されヘッダー 10 列が正しいこと
   - `01_sys_config` に `MST_TAXRATE` → `18_mst_tax_rate` 行が追加されていること
   - **この時点ではまだ 22_bud_headcount は旧 41 列のまま**(= 既存 EMP 行が保全されている)であること
3. `migrationHeadcountRates` を実行:
   - プレビューダイアログに候補が一覧表示されること
   - 「はい」で `18_mst_tax_rate` へ TXR_0001〜 の ID 付きで登録されること
   - 「いいえ」でキャンセルされること(書き込み発生しないこと)
   - 2 回目実行で「登録対象なし」ダイアログが出ること(冪等性)
4. `setupAllSchemas` を**再実行**し、`22_bud_headcount` から 5 料率列が削除されること・派生 5 列と既存 EMP データは保持されることを確認。
5. 人件費 RPA (`generateHcInvoices`) を実行:
   - 既存と同一フォーマット・同一金額の INV が生成されること(差分ゼロを MCP で確認)
   - マスタ未登録の雇用形態(例: 意図的に "インターン" を設定した EMP)がスキップされ `logError` が出力されること
   - 冪等性: 2 回目実行で同一摘要の INV が重複生成されないこと
6. 既存のテスト (`900_test/901_test_runner.js`) で HC RPA テストが pass すること。

推奨実行モデル

本案件は複数ファイル横断の設計判断(DDL / DTO / Repository / RPA / マイグレーション)と、会計ロジック(社保折半・法定福利費算出)の理解が必要なため、全 Step で Claude Sonnet 4.6 以上を推奨する。

Step推奨モデル理由
Step 1: DTO + RepositorySonnet既存 AccountRepository パターンの踏襲。機械的追記だが複合キー生成ロジックは要注意
Step 2: DDL スキーマ修正Sonnetヘッダー削除位置の特定・システムキー登録の挿入位置判断
Step 3: RPA 料率参照置換Opus会計ロジック(社保折半・法定福利費算出)の理解が必須。現行シート式との一致検証・冪等性保全が重要
Step 4: 定数更新Haiku仕様書で削除/追加キー完全定義済み、機械的編集
Step 5: マイグレ新規作成Sonnet既存 808 パターン踏襲 + Human-in-the-Loop 設計

変更履歴

日時変更内容
2026-04-19初版作成

仕様書作成プロンプト

仕様書作成プロンプト(展開して表示)
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. **拡張思考の使い分け**: Phase 1(設計)では拡張思考をフル活用し、ファイル名形式・エッジケース一覧・Step 分割粒度・固有名詞(関数名/シート名/列名/行番号)を完全に確定させる。Phase 2(清書)の各 Step 内では拡張思考を最小限に抑え、Phase 1 で確定済みの内容の書き下しに徹する。出力途中で再考しない。
2. **テキスト報告の禁止**: 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
3. **4-5 分割の Write/Edit 実行**: 仕様書作成は Step 2-1〜2-4 に分けて実行する。1 回の Write/Edit は約 300 行以内を目安にする。
4. **各 Step で何を書くかを具体指示**: 設計判断を Phase 2 実行時に持ち込まないよう、各 Step の内容を Phase 1 で事前確定してから清書に入ること。

======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 MAS-119「22タブ人件費予算の入力簡素化(保険料率・税率マスタ化)」の開発仕様書を作成してください。
仕様書新規作成後は `docs/_config.json` の `nav` 配列の適切なセクションに必ず追記すること。

---

## Phase 1: 実行前タスク(テキスト報告禁止・即座にツール実行)

Phase 1 では拡張思考をフル活用し、Phase 2 で使うすべての固有名詞・行番号・設計判断を確定させる。
**Grep は「どこにあるか」の発見まで。「どう書くか」の判断は必ず Read で裏取りすること。名前や記憶から推測した瞬間に手を止めて Read する。**

### 必須読み込みファイル(この順番で実行すること)

1. `docs/_internal/TODO_future.md` — MAS-119 の要件・概要・人間が検討すべき事項を取得
2. `CLAUDE.md` — コーディング規約・ファイル番号体系・マイグレーション運用ルール(番号帯・メニュー登録先)を把握
3. `000_infra/002_constants.js` — `SHEET_DEFAULTS` 内 `22_bud_headcount` エントリの **全フィールド**(defaults の各キー/値・`_dynamic` の内容)を確認。削除対象5列と残存列を正確に特定する。また `ID_PREFIX_MAP` の構造を確認し、`18_mst_tax_rate` 用エントリの追加要否を判断する
4. `000_infra/003_contracts.js` — 既存 `@typedef` の記述パターンを確認し、新規 `TaxRateDTO` の設計に活用
5. `000_infra/004_utils.js` — `Utils.getSheetByKey(key, fallbackName)` / `Utils.getSheetNameByKey(sysKey)` の引数仕様を確認
6. `100_config/101_sys_config.js` — `setupAllSchemas` 関数でスキーマを追加する実装パターンと、既存 `22_bud_headcount` の DDL 列定義を特定。`onOpen()` で「🔧 マイグレーション」メニューへの登録記述パターンを確認
7. `200_data/202_repository.js` — プライベートヘルパー `readSheetAsDtos_` / `writeDtosToSheet_` / `appendDtosToSheet_` の引数仕様と、`AccountRepository`(`_getSheet` / `findAll` / `findAsMap` / `_cache` / `resetCache` のパターン)を確認。`TaxRateRepository` の実装テンプレートとして活用する
8. `400_domain/401_rpa_hc.js` — 人件費 RPA の現行ロジック全体を確認。「保険料率をシートから直接読み取っている箇所」の行番号と、「冪等性チェック(INV 既存スキップ)」の行番号を特定する
9. 既存マイグレーションスクリプト(`800_ops/808_migration_i24.js` または `800_ops/806_cleanup_empty_rows.js` のいずれか)— 冪等性パターン・`SpreadsheetApp.getUi().alert` ダイアログ・`101_sys_config.js` へのメニュー登録パターンを確認

### Phase 1 で確定すべき固有名詞(Read 裏取り必須)
- `Constants.CONFIG_SHEET` の実際の値(`002_constants.js` で定義済み)
- `Utils.getSheetByKey` の第1引数(システムキー)・第2引数(フォールバックシート名)の型
- `readSheetAsDtos_` / `writeDtosToSheet_` / `appendDtosToSheet_` の引数順序
- `AccountRepository.findAsMap` の `_cache` / `resetCache` の実装詳細
- `401_rpa_hc.js` で料率を参照している変数名・行番号
- `setupAllSchemas` でスキーマ列定義を追加している行番号
- マイグレーション番号(CLAUDE.md 規約: 804–808 使用済み、次は 809 から)
- `onOpen()` 内の「🔧 マイグレーション」メニュー追記箇所

---

## Phase 2: 仕様書の分割作成

出力先: `docs/dev/dev_mas-119_headcount_rate_master.md`
**【重要】1 回のツール呼び出しで全内容を出力せず、以下の 5 Step に厳密に分割して実行すること。**

### Step 2-1: 骨格の作成(File Write・約20行)

全セクションの見出しのみを持つ骨格ファイルを作成。本文は空で可。
含めるセクション(この順序で):
`概要` / `目的` / `現在のコード` / `修正方針` / `影響範囲` / `注意事項` / `エッジケース` / `実データ検証` / `関連ドキュメント` / `人間が検討すべき事項` / `実装プロンプト(Claude Code 用)` / `推奨実行モデル` / `変更履歴` / `仕様書作成プロンプト`

### Step 2-2: 前半セクションの追記(File Edit または Bash・約300行)

`概要` から `注意事項` までを記述。以下の内容を盛り込むこと。

**概要テーブル**: 案件ID=MAS-119、カテゴリ=パイプライン・RPA・外部連携、Phase・優先度は TODO_future.md から転記、対象ファイルに `000_infra/002_constants.js` / `000_infra/003_contracts.js` / `100_config/101_sys_config.js` / `200_data/202_repository.js` / `400_domain/401_rpa_hc.js` / `800_ops/809_migration_headcount_rates.js` を列挙。

**目的**: 従業員ごとに手入力していた保険料率5種を `18_mst_tax_rate` マスタに集約し、`22_bud_headcount` の入力行から料率列を廃止することで入力ミスを削減する。

**現在のコード**: `000_infra/002_constants.js` の `SHEET_DEFAULTS` 内 `22_bud_headcount` エントリ(Phase 1 で確認した実際のコードを引用・ファイル名と行番号を明記)と、`400_domain/401_rpa_hc.js` の料率参照箇所(Phase 1 で特定した行番号)を記載。

**修正方針** — 以下を具体的に記述すること:

- **新規マスタシート `18_mst_tax_rate`**:
  - スキーマ(列構成): `マスタID`(prefix=`TXR_`)/ `適用年度` / `雇用形態` / `健保料率` / `介護保険料率` / `厚年料率` / `雇用保険料率` / `子ども・子育て拠出金率` / `有効フラグ` / `備考`
  - `101_sys_config.js` の `setupAllSchemas` にスキーマ定義を追加
  - `01_sys_config` シートへのシステムキー登録: `MST_TAXRATE` → `'18_mst_tax_rate'`(`setupAllSchemas` 内で追記。登録方法は Phase 1 で確認したパターンに従う)
  - `002_constants.js` の `ID_PREFIX_MAP` に `{ pattern: '18_mst_tax_rate', prefix: 'TXR_', digit: 4, isDate: false }` を追加(Phase 1 で `ID_PREFIX_MAP` の追加要否を確認済みの場合のみ)

- **`000_infra/002_constants.js` の変更**:
  - `SHEET_DEFAULTS` 内 `22_bud_headcount` エントリから以下の5フィールドを削除する:
    `'健保料率': 0.05` / `'介護保険料率': 0` / `'厚年料率': 0.0915` / `'雇用保険料率': 0.0065` / `'子ども・子育て拠出金率': 0.0036`
  - 残存するフィールド(変更しない): `'雇用形態'` / `'決済ラグ(月)'` / `'源泉所得税額'` / `'源泉消費税額'` / `'免税フラグ'` / `'住民税額'` / `'月額給与・報酬'` / `'採用エージェント費'` / `'PC等初期費用'` / `_dynamic: { '適用年度': 'fiscalYear', '開始年月': 'nextYm' }`
  - **注意**: `_dynamic` の `'適用年度': 'fiscalYear'` は RPA のマスタ参照キーとして引き続き必要なため削除しない

- **`000_infra/003_contracts.js` の変更**:
  - 既存 `@typedef` のパターンに倣い、`TaxRateDTO` の型定義(JSDoc `@typedef`)を追記する(Phase 1 で確認したパターンに完全準拠)

- **`200_data/202_repository.js` の変更**:
  - ファイル末尾に `TaxRateRepository` を追記する。`AccountRepository` と完全に同じ構造パターンを踏襲すること:
    - `_getSheet()`: `Utils.getSheetByKey('MST_TAXRATE', '18_mst_tax_rate')` を返す
    - `findAll()`: `readSheetAsDtos_(TaxRateRepository._getSheet())` を返す
    - `findRatesMap()`: キャッシュ付き。複合キー = `String(dto['適用年度']) + '_' + String(dto['雇用形態'])` で料率オブジェクトを返す。有効フラグ=FALSE はスキップ。同一複合キーの重複は行番号が大きい方(後の行)を採用し `console.warn` を出力
    - `_cache: null` / `resetCache()`
  - **既存プライベートヘルパー(`readSheetAsDtos_` 等)を使用すること。新しいヘルパー関数を同ファイルに追加しない**

- **`400_domain/401_rpa_hc.js` の変更**:
  - Phase 1 で特定した「シートから直接料率を読み取っているコード」を削除し、`TaxRateRepository.findRatesMap()` 呼び出しに置換する
  - 既存の冪等性チェック(INV 既存スキップロジック)は一切変更しない

- **`800_ops/809_migration_headcount_rates.js` の新規作成**:
  - Phase 1 で確認した既存マイグレーションスクリプトのパターンに従って実装
  - `22_bud_headcount` からユニークな(適用年度+雇用形態)×料率パターンを抽出し、`SpreadsheetApp.getUi().alert` で確認ダイアログ(「N件の料率パターンを登録します。よろしいですか?」+ 一覧)を表示してから `18_mst_tax_rate` に書き込む
  - 冪等性必須: 同一複合キーが既に存在する場合はスキップ・警告ログ出力
  - `101_sys_config.js` の「🔧 マイグレーション」メニューに登録(Phase 1 で確認した登録パターンに従う)

**注意事項**:
- 実行順序の厳守: マイグレーション `809` を先に実行してデータを `18_mst_tax_rate` に移行してから、`setupAllSchemas` で `22_bud_headcount` の5列削除を行うこと。逆順にすると既存データが消失する
- `TaxRateRepository` は `202_repository.js` の既存プライベートヘルパーのみを使用し、新しいヘルパーを追加しない
- 列参照はヘッダー名ベース(`indexOf` 方式)で実装する。列番号ハードコード禁止(CLAUDE.md 規約)

### Step 2-3a: エッジケース〜人間検討事項の追記(File Edit または Bash・約200行)

**エッジケーステーブル**(以下の条件を必ず網羅すること):

| 条件 | 挙動 | 理由 |
|------|------|------|
| RPA 実行時、`18_mst_tax_rate` に該当する(適用年度+雇用形態)の有効レコードが存在しない | 該当従業員の INV 生成をスキップ。`Utils.logError` でエラー出力。フォールバック値は使用しない | フォールバックによる誤料率での仕訳自動生成を排除。マスタへの登録を強制 |
| `18_mst_tax_rate` に同一複合キーで有効フラグ ON が重複 | 行番号が大きい方(後の行)を採用。`console.warn` で警告出力 | 後勝ちで運用者がデータクレンズできるよう設計 |
| マイグレーション実行時、`22_bud_headcount` 内で同一(適用年度+雇用形態)に複数の異なる料率が混在 | 最初に検出した料率セットをマスタに登録。後続の不整合行は警告ログ出力・マスタ登録スキップ | 冪等性確保。人間によるデータ修正を前提とした設計 |
| `22_bud_headcount` の行で `適用年度` 列が空白 | RPA はその行をスキップ。マイグレーションもその行を無視し警告ログ出力 | 複合キーが不完全なレコードの誤処理を防止 |

**Human-in-the-Loop**:
マイグレーションスクリプト `809_migration_headcount_rates.js` は、`18_mst_tax_rate` への書き込み前に `SpreadsheetApp.getUi().alert` でプレビューダイアログを表示し、ユーザーが「はい」を選択した場合のみ書き込みを実行する(CLAUDE.md プロダクトポリシー「Human-in-the-Loop」準拠)。

**実データ検証**(MCP で実施):
- `22_bud_headcount` シートの現行列構成と `雇用形態` 列の実データ値(DDL コード値との乖離確認)
- `適用年度` 列の実データ形式(`YYYY` 形式か `YYYY-MM` 形式かを確認し、複合キー生成ロジックに反映)
- `18_mst_tax_rate` シートの有無(既存の場合はスキーマを確認してから DDL 設計に反映)

**関連ドキュメント**: Phase 1 で参照した既存仕様書・CLAUDE.md 該当箇所をテーブル形式で記載。

**人間が検討すべき事項**: TODO_future.md から MAS-119 の該当項目を転記し、Phase 1 調査で追加判明した事項を補記。

### Step 2-3b: 実装プロンプト〜変更履歴の追記(File Edit または Bash・約250行)

**【絶対厳守】実装プロンプトのセクションは、バッククォート3つ以上のコードブロックで囲まず、内容全体を行頭スペース4つのインデントで出力すること。**

実装プロンプトに含める内容(インデント形式で記述):
- 役割定義
- 実行前タスク(`000_infra/002_constants.js` / `000_infra/003_contracts.js` / `100_config/101_sys_config.js` / `200_data/202_repository.js` / `400_domain/401_rpa_hc.js` の読み込みリスト・各ファイルの確認ポイント)
- 修正対象ファイル(6ファイル)と「のみ」or「への追記」を明示
- Step 分割:
  - Step 1: `003_contracts.js` に `TaxRateDTO` typedef 追記 + `202_repository.js` 末尾に `TaxRateRepository` 追記
  - Step 2: `101_sys_config.js` に `18_mst_tax_rate` スキーマ追加・`setupAllSchemas` の `22_bud_headcount` DDL 修正
  - Step 3: `400_domain/401_rpa_hc.js` の料率参照ロジックを `TaxRateRepository.findRatesMap()` に置換
  - Step 4: `000_infra/002_constants.js` の `SHEET_DEFAULTS` と `ID_PREFIX_MAP` 更新
  - Step 5: `800_ops/809_migration_headcount_rates.js` 新規作成 + `101_sys_config.js` メニュー登録
- エッジケーステーブル(マスタ未登録・重複キー・空年度の3条件)
- 実データ検証項目
- 動作確認手順:
  1. `npm run push:dev`
  2. `setupAllSchemas` を実行し `18_mst_tax_rate` シートが生成されること・`01_sys_config` に `MST_TAXRATE` キーが登録されることを確認
  3. `809_migration_headcount_rates` を実行し、プレビューダイアログが表示されること・マスタへの書き込みが正常に完了することを確認
  4. `setupAllSchemas` を再実行し `22_bud_headcount` の5列が削除されることを確認
  5. 人件費 RPA を実行し INV が正常生成されること・マスタ未登録雇用形態でスキップされることを確認

**推奨実行モデルテーブル**: 複数ファイル横断の設計判断と RPA ロジック理解が必要なため、全 Step で Claude Sonnet 4.6 以上を推奨。

**変更履歴**: `| 2026-04-19 | 初版作成 |`

### Step 2-4: 仕様書作成プロンプトの記録(File Edit または Bash)

このステップは最も出力トークンが嵩む工程のため、必ず独立した Step として単独で実行すること(Step 2-3b と結合しない)。
仕様書末尾「仕様書作成プロンプト」セクションに以下の形式で追記し、この `<instruction>` 全文を記録する:

    <details><summary>仕様書作成プロンプト(展開して表示)</summary>

    ...この instruction 全文...

    </details>

---

## Phase 3: `_config.json` への追記

1. `docs/_config.json` を Read し、§E.6(パイプライン・RPA・外部連携)セクションに以下を追記(連番 X は既存エントリを確認して正しい番号を使用すること):
   ```json
   { "file": "dev/dev_mas-119_headcount_rate_master.md", "title": "E.6.X S-47 22タブ人件費予算の入力簡素化(保険料率・税率マスタ化)" }
   ```
2. 追記後に `docs/_config.json` の JSON 構文が壊れていないことを確認する。