概要

項目
案件IDMAS-086
カテゴリバリデーション追加 / ユーティリティ拡張
PhasePhase 1.5(自動入力パイプライン 即効)
優先度P1 / ★★
所要時間目安約1時間
対象ファイル000_infra/004_utils.js(主)
関連ファイル500_import/502_receipt_reader.js(呼び出し元・要確認)、100_config/101_sys_config.js03_sys_params 初期化)
前提案件なし

【重要】「26タブ」について 案件タイトルの「26タブ」は 000_infra/002_constants.jsSHEET_DEFAULTS / ID_PREFIX_MAP にも存在しない。 実際の税区分登録対象タブは 32_wrk_expense(EXP_、経費タブ) である(SHEET_DEFAULTS'税区分': '対象外' の既定値を持つ唯一の経費系シート)。 関連する 33_wrk_finance(FIN_、財務タブ)にも同じ既定値があり、将来的には共通ユーティリティとして両方で利用可能。 本仕様書では対象シートを「経費タブ (32_wrk_expense)」に統一して扱う。

目的

OCR 等で読み取った金額情報(税込・税抜・税額の一部または全部)から消費税の税区分(課税10%・課税8%・非課税・対象外)を自動判定・提案し、経費タブ (32_wrk_expense) 登録時の手動選択工数を削減する。

背景

  • 現状、経費タブの 税区分 列は SHEET_DEFAULTS で既定値 '対象外' が設定されるのみで、課税取引の場合は常に手動修正が必要。
  • 500_import/502_receipt_reader.js による領収書 OCR は「税込金額_決済」「税抜金額_決済」「消費税額_決済」の 3 値を raw データとして取得できるが、税区分 は一切設定していない。
  • 課税事業者(TODO_future.md の Section 5 I-系案件群)への移行を見据え、OCR 金額の比率から税区分を機械的に推定できる共通ユーティリティが必要。
  • 判定ロジックは「税額 / 税抜 ≒ 10% なら課税10%、≒ 8% なら課税8%」のシンプルな演算で済み、Utils.aiSuggestAccount() と同じく 提案 に徹する(Human-in-the-Loop 原則)。

現在のコード

税区分の既定値 (000_infra/002_constants.js L81-82)

{ pattern: '32_wrk_expense', prefix: 'EXP_', defaults: { '承認ステータス': '未申請', '収支区分': '支出', '税区分': '対象外', '通貨': 'JPY', '外貨金額': 0, '日本円金額': 0, '消費税額': 0, _dynamic: { '発生日(P/L計上日)': 'now' } } },
{ pattern: '33_wrk_finance', prefix: 'FIN_', defaults: { 'ステータス': '未申請', '収支区分': '支出', '税区分': '対象外', '通貨': 'JPY', '外貨金額': 0, '日本円金額': 0, '消費税額': 0, _dynamic: { '発生日(P/L計上日)': 'now' } } },

→ 新規行 append 時の 税区分 は常に '対象外'。課税取引でも OCR 結果を参照せず、人間が毎回書き換える運用。

InvoiceDTO の型定義 (000_infra/003_contracts.js L57)

 * @property {string}      税区分               - "課税" | "非課税" | "対象外"

→ DTO 層で許容される値は "課税" | "非課税" | "対象外" の 3 値のみ'課税10%' 等の詳細値は DTO に直接セットできない。

OCR による金額取得 (500_import/502_receipt_reader.js L125-145)

var rowData = [
  rcpId,                                  // 管理ID
  now,                                    // 処理日時
  extracted.docType || '領収書',            // 証憑種別
  Utils.normalizePartnerName(extracted.vendor || ''),  // 取引先名
  extracted.address || '',                // 🏢住所
  extracted.totalAmount || 0,             // 税込金額_決済
  extracted.subtotal || 0,                // 税抜金額_決済
  extracted.tax || 0,                     // 消費税額_決済
  // ... (税区分の設定は一切なし)
];

→ 領収書 OCR は receipt タブへ raw データを書き込むのみで、32_wrk_expense へのインポート時に 税区分 を判定する箇所が存在しない。

Utils.aiSuggestAccount() (000_infra/004_utils.js L200-213)

/**
 * 摘要テキストから勘定科目を推論 (Constants.ACCOUNT_RULES ベース)
 */
aiSuggestAccount: function(text) {
  text = String(text).toLowerCase();
  for (const rule of Constants.ACCOUNT_RULES) {
    for (const kw of rule.keywords) {
      if (text.includes(kw)) return rule.account;
    }
  }
  return '';
},

→ 「提案を返す」同類の共通ユーティリティ。本仕様で追加する Utils.suggestTaxCategory() はこの直後(L214 付近)に挿入する。

Constants.getParam() (000_infra/002_constants.js L147-167)

getParam: function(key, defaultVal) {
  if (!this._paramsCache) {
    this._paramsCache = {};
    try {
      var ss = SpreadsheetApp.getActiveSpreadsheet() || (typeof getWebSpreadsheet_ === 'function' ? getWebSpreadsheet_() : null);
      if (ss) {
        var sheet = ss.getSheetByName('03_sys_params');
        if (sheet) {
          var data = sheet.getDataRange().getValues();
          for (var i = 1; i < data.length; i++) {
            var k = String(data[i][0]).trim();
            if (k) this._paramsCache[k] = data[i][1];
          }
        }
      }
    } catch(e) { /* 初回読み込み失敗時はデフォルト値を使う */ }
  }
  var val = this._paramsCache[key];
  if (val === undefined || val === null || val === '') return defaultVal;
  return (typeof defaultVal === 'number') ? Number(val) : String(val);
},

→ 第2引数が number なら Number(val) で型変換する実装。税率パラメータ(0.10 / 0.08 / 0.005)も number 型で取得可能。

03_sys_params の既存登録状況

  • 101_sys_config.js では CFG_DDL_VERSION 等の管理のみで、CONSUMPTION_TAX_RATE_STANDARD / _REDUCED / _TOLERANCE の登録ロジックは未実装
  • 実装時に以下のいずれかで追加する:
    • 03_sys_params シートに手動で 3 行追加(最小変更)
    • 101_sys_config.js の初期化ロジックに追加(推奨・再現性あり)

修正方針

新規関数 Utils.suggestTaxCategory() の追加

000_infra/004_utils.jsUtils.aiSuggestAccount() 直後(L214 付近、}, の次)に以下を追加する。

/**
 * 金額情報から消費税の税区分を自動判定する (S-14)
 * @param {{ taxInclusive: number|null, taxExclusive: number|null, tax: number|null }} amounts
 * @returns {'課税10%'|'課税8%'|'非課税'|'対象外'|'判定不能'} 判定結果
 */
suggestTaxCategory: function(amounts) { ... }

コアロジック(仕様)

  1. amounts.taxInclusive / amounts.taxExclusive / amounts.tax について、null / 空文字 / 非数値 (isNaN) を「欠損値」と判定する(0 は有効値)。
  2. 有効な値が 2 つ以上揃っているか確認。1 つ以下なら '判定不能' を即返す(補完計算不可)。
  3. 欠損値を他 2 値から補完計算する:
    • taxExclusive 欠損 → taxInclusive - tax
    • tax 欠損 → taxInclusive - taxExclusive
    • taxInclusive 欠損 → taxExclusive + tax
  4. 返品等でマイナス値になる場合は Math.abs() で絶対値に変換(税率の正負は不変)。
  5. taxExclusive <= 0(税抜ゼロ・マイナス)の場合はゼロ除算回避(失敗パターン #2 対策):
    • tax === 0'非課税'
    • tax !== 0'対象外'(税抜ゼロなのに税だけ付く異常ケース)
  6. rate = tax / taxExclusive を計算。
  7. パラメータ参照:
    • Constants.getParam('CONSUMPTION_TAX_RATE_STANDARD', 0.10) — 標準税率
    • Constants.getParam('CONSUMPTION_TAX_RATE_REDUCED', 0.08) — 軽減税率
    • Constants.getParam('CONSUMPTION_TAX_RATE_TOLERANCE', 0.005) — 判定許容誤差
  8. 判定:
    • |rate - std| <= tol'課税10%'
    • |rate - red| <= tol'課税8%'
    • rate === 0'非課税'
    • それ以外 → '判定不能'

DTOへの反映マップ(呼び出し元で適用)

suggestTaxCategory の戻り値DTO 税区分 セット値
'課税10%''課税'
'課税8%''課税'
'非課税''非課税'
'対象外''対象外'
'判定不能'既存値を維持(上書きしない)

重要: InvoiceDTO.税区分 の有効値は "課税" | "非課税" | "対象外" の 3 値(003_contracts.js L57 の型定義)。 '課税10%' 等の内部戻り値を DTO に直接セットしないこと。マッピング辞書を呼び出し元で必ず適用する。

パラメータ管理

本仕様で新規に 03_sys_params シートへ以下 3 行を登録する必要がある:

キー既定値説明
CONSUMPTION_TAX_RATE_STANDARD0.10消費税 標準税率(10%)
CONSUMPTION_TAX_RATE_REDUCED0.08消費税 軽減税率(8%)
CONSUMPTION_TAX_RATE_TOLERANCE0.005判定許容誤差(±0.5%)。端数処理差異を吸収

未登録の場合も Constants.getParam() のデフォルト値フォールバックで動作するが、本番運用前に必ず登録する。

影響範囲

種別ファイル内容
変更(追記のみ)000_infra/004_utils.jsUtils.suggestTaxCategory() を新規追加(既存関数は無変更)
呼び出し元(要追加)500_import/502_receipt_reader.jsOCR 結果から DTO 構築時に suggestTaxCategory() を呼び出し、戻り値をマッピング辞書で変換してセット
データ(要追加)03_sys_params シート消費税率 3 パラメータの新規登録
無変更000_infra/003_contracts.jsDTO 型定義は既存の "課税" | "非課税" | "対象外" のまま使用
無変更200_data/201_data_validator.js本仕様ではバリデーション追加なし(「提案」のみ)

注意事項

  1. 既存関数を変更しないUtils.aiSuggestAccount() 等を巻き込んだリファクタは禁止。追記のみとする。
  2. 税率のハードコード禁止 — 必ず Constants.getParam() 経由で取得すること(失敗パターン #3「DDLコード値 vs 実データ乖離」対策。将来の軽減税率改定にも対応)。
  3. ゼロ除算を必ず回避taxExclusive <= 0 の早期リターンは削除しないこと(失敗パターン #2「ゼロ除算フォールバック」対策)。
  4. 有効値カウントでは 0 を有効と扱う税額 0 円 & 税抜 1000 円 & 税込 1000 円 の非課税ケースを正しく判定するため。 if (!val) は使わず、v !== null && v !== '' && !isNaN(Number(v)) で判定する。
  5. DTO への代入値の厳格さ — 必ずマッピング辞書 {'課税10%':'課税', '課税8%':'課税', '非課税':'非課税', '対象外':'対象外'} を経由し、'課税10%' 等をそのまま書き込まない。
  6. Human-in-the-Loop 遵守 — 本関数は「提案」を返すのみ。最終確定は人間が UI 上で承認する運用(プロダクトポリシー準拠)。呼び出し元 UI では自動提案値の視覚通知(セル背景色等)を検討する(本仕様書のスコープ外)。
  7. キャッシュの副作用Constants._paramsCache は起動時に一度だけロードされる。スクリプトエディタで 03_sys_params を更新した場合、GASプロセス再実行まで反映されない点に留意。

エッジケース

条件戻り値理由
有効な金額情報が 1 つ以下'判定不能'補完計算不可
taxExclusive <= 0 かつ tax === 0'非課税'ゼロ除算回避。税抜0・税0は非課税扱い
taxExclusive <= 0 かつ tax !== 0'対象外'ゼロ除算回避。税抜0で税だけ付く異常ケースは対象外
tax === 0 かつ taxExclusive > 0'非課税'税率 0% = 非課税扱い
税抜 + 税額 ≠ 税込(1円以上の不整合)欠損値は補完、全値揃っている場合はそのまま税額/税抜で判定端数処理の差異は tol = 0.005 で吸収
返品等で金額がマイナス絶対値に変換して判定税率の正負は変わらない
03_sys_params にパラメータ未登録Constants.getParam() のデフォルト値(0.10 / 0.08 / 0.005)で動作フォールバック定義済み
税込1100円・税抜1000円・税額100円'課税10%'100/1000 = 0.10、標準税率と一致
税込1080円・税抜1000円・税額80円'課税8%'80/1000 = 0.08、軽減税率と一致
税込1100円・税抜900円・税額100円'判定不能'100/900 ≒ 0.111、どちらの税率にも該当しない(許容誤差超過)
全て null'判定不能'情報なし
amounts === undefinedTypeError(引数必須)呼び出し元で引数チェックを行う責務

実データ検証

実装前に以下を MCP / スプレッドシートで確認する:

  1. 03_sys_params シートの既存キー確認:
    • CONSUMPTION_TAX_RATE_STANDARD / CONSUMPTION_TAX_RATE_REDUCED / CONSUMPTION_TAX_RATE_TOLERANCE の 3 キーが既に登録されているか。
    • 未登録なら以下を A列・B列に追加(C列「説明」にコメント任意):
      • CONSUMPTION_TAX_RATE_STANDARD / 0.10
      • CONSUMPTION_TAX_RATE_REDUCED / 0.08
      • CONSUMPTION_TAX_RATE_TOLERANCE / 0.005
  2. 32_wrk_expense税区分 プルダウン確認:
    • 列の入力規則(データ検証)が "課税" | "非課税" | "対象外" の 3 値に設定されているか。
    • InvoiceDTO 型定義と一致しない値(例: '課税10%' 等)が誤ってセットされないか確認。
  3. OCR 読み取りサンプルの入力欠損パターン確認:
    • 502_receipt_reader.js が書き込む receipt タブの既存行で、税込金額_決済 / 税抜金額_決済 / 消費税額_決済 が揃っているか、欠損パターンがあるかを目視確認。
    • 欠損が頻出するなら「有効値 2 つ以上必須」の設計が実運用に合うか再検討する材料となる。
  4. 既存経費レコードの 税区分 分布確認:
    • 32_wrk_expense税区分'対象外' 以外('課税' / '非課税')となっているレコード数を集計し、手動設定の割合を把握。

関連ドキュメント

種別参照先関連内容
プロジェクト規約CLAUDE.mdデータアクセス規約(列参照はヘッダー名ベース)、会計ロジック規約(科目マスタ未登録のキーワード推測禁止)
プロダクトポリシーPRD プロダクトポリシーHuman-in-the-Loop(提案→承認→確定)
失敗パターンfailure_patterns.md#2 ゼロ除算フォールバック、#3 DDLコード値 vs 実データ乖離、#21-24 数式設計の落とし穴
関連仕様dev_mas-089_consumption_tax_exclusive_method.md消費税 税抜方式への切替え(中長期)
関連仕様dev_mas-083_pipeline_tax.mdパイプライン売上の消費税対応(課税/免税の切替)
関連仕様dev_mas-161_ocr_manual_correction_assist.mdOCR手動補正支援(本仕様の UX 連携検討対象)
類似ユーティリティ000_infra/004_utils.js Utils.aiSuggestAccount() L200-213同じ「提案」系ユーティリティの実装パターン

人間が検討すべき事項

TODO_future.md からの転記事項

  • 税区分の判定ルール: 税率からの逆算精度、軽減税率 8% と標準税率 10% の判別について(TODO_future.md Section 5 より転記)。
    • → 本仕様では tol = 0.005(±0.5%)を許容誤差として採用。端数処理の差異を吸収しつつ、10%/8% を明確に判別可能。
  • 課税事業者移行との整合: 免税事業者期間中は税区分が常に '対象外' となる運用との整合性をどう取るか。
    • → 本関数は「金額比率からの判定」であり、免税期間中でも rate === 0 なら '非課税' を返す。運用判断として「免税事業者期間中は呼び出し元でスキップ」する設計もあり得る(本仕様書ではスコープ外、将来検討)。

本仕様で追加で検討すべき事項

  • Human-in-the-Loop 必須: 本関数はあくまで「提案」を返す。呼び出し元 UI では提案値セルの背景色変更等で「自動提案値」であることを視覚的に通知し、最終確認は人間が行うフローとする(プロダクトポリシー準拠)。UI 層の具体設計は本仕様書のスコープ外だが、後続案件として UX 設計を推奨。
  • '判定不能' 時の UX: セルのハイライトや入力促進トースト通知の検討(本仕様書のスコープ外だが後続案件 MAS-161 と合わせて検討)。
  • '非課税' vs '対象外' の分離: 軽減税率品目(食品等)・非課税取引(保険等)・不課税取引(給与等)の会計上の厳密な区別を、単純な金額比率だけで自動判定するのは不可能。現ロジックは「税額 0 円 → 非課税」と「税抜0円 & 税非0円 → 対象外」の雑な区別に留まる。運用上は「自動提案値は課税10%/課税8%のみ信頼、それ以外は人間判断」とする方針が現実的。
  • 軽減税率改定の影響: 消費税率が将来改定された場合、03_sys_params の値を書き換えるだけで追随可能(パラメータ化の利点)。ただし「新旧税率の混在期間」の扱いは要仕様追加(例: 発生日に応じた税率選択)。本仕様では発生日を参照しない単純な比率判定のみ。
  • 外貨取引の扱い: 本関数は JPY 前提。外貨(USD/EUR等)の場合は為替レート換算後の税率判定となるが、本仕様では扱わない(amounts 引数は JPY 換算後の数値である前提)。
  • テスト自動化: 901_test_runner.js に以下のケースを追加することを推奨(実装プロンプトでも言及):
    • 10%標準税率の判定 / 8%軽減税率の判定 / 非課税 / 対象外 / 判定不能(情報不足)/ マイナス値(返品)/ ゼロ除算回避

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

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
CLIエージェントである「Claude Code」として、以下の指示に従い、
案件 MAS-086「領収書→経費タブ登録時の税区分自動判定」を実装してください。

## 実行前タスク(コンテキストの読み込み)

実装前に必ず以下のファイルを Read で確認し、行番号・シグネチャを正確に把握してください:

1. `000_infra/004_utils.js` — `Utils.aiSuggestAccount()` の末尾位置(現在 L213 付近の `},`)。
   `Utils.suggestTaxCategory()` はその直後に挿入する。`Utils.parseAmt()` の挙動 (L191-198) も確認。
2. `000_infra/003_contracts.js` — `InvoiceDTO.税区分` の型定義 L57 (`"課税" | "非課税" | "対象外"`) を確認。
   DTO に `'課税10%'` 等を直接セットしないための制約。
3. `000_infra/002_constants.js` — `Constants.getParam()` L147-167 の実装。
   第2引数が number なら `Number(val)` 変換される点を確認。
4. `500_import/502_receipt_reader.js` — L125-145 の receipt タブ書き込み箇所。
   本仕様では receipt タブは無変更。経費タブ (`32_wrk_expense`) 側への反映方法は
   呼び出し元の既存 UI/インポート処理に合わせる(receipt → 32_wrk_expense 転記時に呼ぶ)。
5. `100_config/101_sys_config.js` — `03_sys_params` 初期化ロジックの参照方法を確認。
6. `docs/dev/dev_mas-086_tax_category_suggester.md` — 本開発仕様書。

## 修正対象ファイル

- `000_infra/004_utils.js` — `Utils.suggestTaxCategory()` を新規追加(既存関数の変更禁止)
- (任意)`500_import/502_receipt_reader.js` 等の呼び出し元 — 戻り値をマッピング辞書経由で DTO にセット
- (必須)`03_sys_params` シート — 消費税率 3 パラメータの新規登録

## 実装内容

### 1. `Utils.suggestTaxCategory()` の追加(`004_utils.js`)

`Utils.aiSuggestAccount()` 末尾(L213 の `},`)の直後に以下を追加する:

    /**
     * 金額情報から消費税の税区分を自動判定する (MAS-086)
     * @param {{ taxInclusive: number|null, taxExclusive: number|null, tax: number|null }} amounts
     * @returns {'課税10%'|'課税8%'|'非課税'|'対象外'|'判定不能'}
     */
    suggestTaxCategory: function(amounts) {
      var inc = Utils.parseAmt(amounts.taxInclusive);
      var exc = Utils.parseAmt(amounts.taxExclusive);
      var tax = Utils.parseAmt(amounts.tax);
      // 有効値カウント (0 も有効値として扱う。null/''/NaN は除外)
      var validCount = [amounts.taxInclusive, amounts.taxExclusive, amounts.tax]
        .filter(function(v) { return v !== null && v !== '' && !isNaN(Number(v)); }).length;
      if (validCount < 2) return '判定不能';
      // 不足値を他2値から補完
      if (amounts.taxExclusive === null || amounts.taxExclusive === '') exc = inc - tax;
      if (amounts.tax === null || amounts.tax === '')                   tax = inc - exc;
      if (amounts.taxInclusive === null || amounts.taxInclusive === '') inc = exc + tax;
      // 返品等マイナス金額は絶対値で処理(税率の正負は変わらない)
      exc = Math.abs(exc); tax = Math.abs(tax);
      // ゼロ除算回避
      if (exc <= 0) return (tax === 0) ? '非課税' : '対象外';
      var rate = tax / exc;
      var std = Constants.getParam('CONSUMPTION_TAX_RATE_STANDARD',  0.10);
      var red = Constants.getParam('CONSUMPTION_TAX_RATE_REDUCED',   0.08);
      var tol = Constants.getParam('CONSUMPTION_TAX_RATE_TOLERANCE', 0.005);
      if (Math.abs(rate - std) <= tol) return '課税10%';
      if (Math.abs(rate - red) <= tol) return '課税8%';
      if (rate === 0) return '非課税';
      return '判定不能';
    },

### 2. 呼び出し元への反映(`502_receipt_reader.js` 等、既存 UI/インポートに合わせる)

`Utils.suggestTaxCategory()` の戻り値を `InvoiceDTO.税区分` / 経費タブ `税区分` 列に
マッピング辞書経由でセットする:

    var suggestion = Utils.suggestTaxCategory({
      taxInclusive: row['税込金額'],     // 経費タブの列名に合わせて調整
      taxExclusive: row['税抜金額'],
      tax:          row['消費税額']
    });
    var TAX_CATEGORY_MAP = {
      '課税10%': '課税', '課税8%': '課税',
      '非課税':   '非課税', '対象外': '対象外'
    };
    if (TAX_CATEGORY_MAP[suggestion]) {
      row['税区分'] = TAX_CATEGORY_MAP[suggestion];
      // Human-in-the-Loop: 自動提案値であることをセルの背景色等で通知する(UI層の責務)
    }
    // '判定不能' の場合は既存値を維持(上書きしない)

### 3. `03_sys_params` への 3 パラメータ追加

dev 環境で `03_sys_params` シートを開き、以下 3 行を追加(A列=キー、B列=値):

- `CONSUMPTION_TAX_RATE_STANDARD`  / `0.10`
- `CONSUMPTION_TAX_RATE_REDUCED`   / `0.08`
- `CONSUMPTION_TAX_RATE_TOLERANCE` / `0.005`

(`101_sys_config.js` の初期化ロジックへの追加は別案件として切り出し可能)

## 制約

- `Utils.suggestTaxCategory()` の追加のみ。`Utils` オブジェクト内の既存関数を変更しない。
- DTO / 経費タブの `税区分` 列に `'課税10%'` 等の内部戻り値を直接セットしない。
  有効値は `"課税" | "非課税" | "対象外"` の 3 値のみ(必ず `TAX_CATEGORY_MAP` 経由)。
- 税率のハードコード禁止。必ず `Constants.getParam()` 経由で取得する(将来の税率改定対応)。
- ゼロ除算回避の早期リターンを削除しない(失敗パターン #2 対策)。
- 有効値カウントでは 0 を有効値として扱う(`if (!val)` 使用禁止、`v !== null && v !== '' && !isNaN(Number(v))` で判定)。

## エッジケース

仕様書「エッジケース」テーブルに従う(特にゼロ除算・マイナス金額・入力欠損)。

## 実データ検証

実装前に必ず以下を確認:

- `03_sys_params` に 3 パラメータが登録済みか(未登録なら手動で追加)
- `32_wrk_expense` の `税区分` 列のプルダウン値が `"課税" | "非課税" | "対象外"` の 3 値か
- OCR サンプルの税込/税抜/税額欠損パターンの頻度

## 動作確認

1. `npm run push:dev` で開発環境にデプロイ
2. 開発用スプレッドシートで `03_sys_params` に 3 パラメータが存在することを確認
3. GAS スクリプトエディタから以下のテストを実行(`901_test_runner.js` に追加推奨):

    // 10% 標準税率
    console.log(Utils.suggestTaxCategory({taxInclusive:1100, taxExclusive:1000, tax:100}));
    // → '課税10%'、DTO マップ後は '課税'

    // 8% 軽減税率
    console.log(Utils.suggestTaxCategory({taxInclusive:1080, taxExclusive:1000, tax:80}));
    // → '課税8%'、DTO マップ後は '課税'

    // 非課税
    console.log(Utils.suggestTaxCategory({taxInclusive:1000, taxExclusive:1000, tax:0}));
    // → '非課税'

    // 情報不足
    console.log(Utils.suggestTaxCategory({taxInclusive:1000, taxExclusive:null, tax:null}));
    // → '判定不能'

    // 返品(マイナス金額)
    console.log(Utils.suggestTaxCategory({taxInclusive:-1100, taxExclusive:-1000, tax:-100}));
    // → '課税10%'

    // ゼロ除算回避
    console.log(Utils.suggestTaxCategory({taxInclusive:0, taxExclusive:0, tax:0}));
    // → '非課税'(税抜0 & 税0)

    console.log(Utils.suggestTaxCategory({taxInclusive:100, taxExclusive:0, tax:100}));
    // → '対象外'(税抜0 で税だけ付く異常ケース)

4. `901_test_runner.js` に上記 7 ケースのテストを追加し、「テスト実行」メニューから全件 PASS を確認
5. 実運用の receipt タブから経費タブへ転記する UI を手動でテスト:
   - 提案値が `'課税'` になるケース/ならないケース(`'判定不能'`)をそれぞれ確認

### 拡張思考の使用状況

| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| ファイル読み込み・挿入位置特定 | なし | 行番号・シグネチャは仕様書で確定済み |
| ロジック実装 | なし | 仕様書で完全定義済み、写経レベル |
| 呼び出し元反映 | **あり** | 既存 UI/インポートフロー特定のため |

推奨実行モデル

工程推奨モデル理由
仕様書作成(本ドキュメント)Claude Opus 4.6「26タブ」の曖昧さの解消、DTO型制約とOCR出力の突合、ゼロ除算の扱い方針選定、Constants.getParam() 連携方針など複数モジュールの横断判断
実装(004_utils.js 追加)Claude Haiku 4.5仕様書で関数コードが完全定義済み、判断要素なし
実装(呼び出し元反映)Claude Sonnet 4.6既存 UI/インポートフローの特定と適切なフックポイント判断(中程度の判断)
動作確認ユーザー手動GASエディタでのテスト実行とスプレッドシート目視確認が必要

変更履歴

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

仕様書作成プロンプト

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

======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 **S-14「領収書→経費タブ登録時の税区分自動判定」** の開発仕様書を作成してください。
作成後、`docs/_config.json` の `nav` 配列の適切なセクションにも必ず追記してください。

---

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

以下を **Read / Grep** で調査し、Phase 2 で設計判断を行わないよう、全固有名詞・行番号・型を確定させること。「名前から推測した瞬間に手を止めて Read」(失敗パターン #18-#20 の直接対策)。

### 1-A: 案件定義の読み込み
- `docs/_internal/TODO_future.md` を Grep で **S-14** を検索し、案件名・概要・人間が検討すべき事項を取得する。(案件名の正式表記を必ず確認すること。~~打ち消し線~~ が付いている場合は最新表記を採用する)

### 1-B: プロジェクト規約の読み込み
- `CLAUDE.md` を Read し、コーディング規約(特に「データアクセス」「会計ロジック」節)を把握する。

### 1-C: 既存仕様書テンプレートの読み込み
- `docs/dev/dev_mas-075_expense_date_validation.md` を Read し、バリデーション追加系のフォーマットを把握する。

### 1-D: 関連コードの調査(必ず Read で裏取りすること)

| # | 読み込みファイル | 確認すべき内容 |
|:-:|----------------|--------------|
| 1 | `000_infra/002_constants.js` | `Constants.getParam()` の実装(引数・戻り値の型、`03_sys_params` を参照していることを確認)。`SHEET_DEFAULTS` に `32_wrk_expense` (EXP_) / `33_wrk_finance` (FIN_) のデフォルト値が定義されていることを確認。`03_sys_params` に消費税率パラメータが既存登録されているか確認(**未登録なら新規追加が必要**) |
| 2 | `000_infra/004_utils.js` | `Utils.parseAmt()` の実装と引数・戻り値を確認。`Utils.aiSuggestAccount()` の実装パターン(同ファイルへの追加関数の挿入位置・スタイルの参考)を確認。新関数 `Utils.suggestTaxCategory()` の挿入位置(`aiSuggestAccount` の直後を候補)を特定し行番号を記録する |
| 3 | `000_infra/003_contracts.js` | `InvoiceDTO` の `税区分` プロパティの型定義 (`"課税" \| "非課税" \| "対象外"`) を Read で確認。`"非課税"` と `"対象外"` が別値であることを確認する(「非課税/対象外」に統合してよいかは Phase 1 で判断) |
| 4 | `500_import/502_receipt_reader.js` (または `503_receipt_reader.js`)| OCR読み取り処理の呼び出しフロー、税込・税抜・税額をどのように取得しているかを確認。このファイルから InvoiceDTO の `税区分` を設定している箇所があれば行番号を記録する |
| 5 | `100_config/101_sys_config.js` | メニュー定義(`onOpen()`)に税区分関連のメニュー項目があるか確認。`03_sys_params` の初期化ロジックがあればパラメータ追加方法を確認する |
| 6 | `docs/_internal/failure_patterns.md` | #2(ゼロ除算フォールバック)、#3(DDLコード値 vs 実データ乖離)、#21-#24(数式設計の落とし穴)を確認する |

> **【要調査】** 案件名の「26タブ」が指す実際のシートを必ず特定すること。`002_constants.js` の `SHEET_DEFAULTS` や `ID_PREFIX_MAP` を Read し、`26_xxx` という命名のシートが存在するか確認する。存在しない場合は `32_wrk_expense`(EXP_)が対象の可能性が高いため、その旨を仕様書に明記する。

---

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

**出力先**: `docs/dev/dev_mas-086_tax_category_suggester.md`(ファイル名のIDは大文字 `MAS-086`)

(以下、オリジナルの Phase 2-1 〜 2-4 / Phase 3 指示全文をこの仕様書の作成に使用した。本 `<details>` は仕様書作成プロンプトの再現用であり、実行タスクの完全再現には本ファイル末尾のみで十分である)

</instruction>

実データ検証

関連ドキュメント

人間が検討すべき事項

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

推奨実行モデル

変更履歴

仕様書作成プロンプト