概要

項目内容
案件IDMAS-128
案件名発注承認ワークフロー(金額閾値ベース)
カテゴリワークフロー・内部統制
PhaseP3
優先度
所要時間4-6時間
対象ファイル100_config/101_sys_config.jsDDLスキーマ・onEdit)/ 300_ui/301_ui_assist.js(handleUxAssist へ承認フロー追加)/ 000_infra/002_constants.js(SHEET_DEFAULTS)/ 000_infra/003_contracts.js(OrderDTO)
前提案件なし(MAS-104 支払依頼ワークフローとの共通化は将来検討)

目的

発注のヒューマン・イン・ザ・ループを担保するため、31_wrk_order シートに金額閾値ベースの承認ワークフローを追加する。閾値以下は申請と同時に自動承認(SYSTEM)、閾値超は承認者宛にメール通知し人間レビューを必須化する。承認者・承認日時を残すことで内部統制の証跡とする。

現在のコード

31_wrk_order 既存スキーマ(100_config/101_sys_config.js L846)

'WRK_ORDR': { headers: ["有効フラグ","発注ID(ORD)","起票日時","起票者","取引先名","契約・件名","摘要","契約形態","開始年月","終了年月","税抜金額_発注","消費税額_発注","税込金額_発注","発注残高(自動計算)","PJ名","組織名","発注ステータス","参照元区分","参照元ID","証憑URL"], color: "#b45f06" },

20 列構成。承認関連列(承認ステータス・申請者・申請日時・承認者・承認日時)は未定義。

Constants.SHEET_DEFAULTS 既存エントリ(000_infra/002_constants.js L84)

{ pattern: '31_wrk_order',   defaults: { '発注ステータス': '見積中', '契約形態': 'スポット' } },

prefix フィールドなし(ID 採番は Constants.ID_PREFIX_MAP{ pattern: '31_wrk_order', prefix: 'ORD_', digit: 4, isDate: true } 側で管理)。defaults には 承認ステータス キー未定義。

onEdit(e) の現状(100_config/101_sys_config.js L409-459)

function onEdit(e) {
  if (!e || !e.range) return; const sheetName = e.range.getSheet().getName(); const row = e.range.getRow(); const col = e.range.getColumn(); const val = e.value;
  if (row === 1) return;
  if (sheetName === Constants.CONFIG_SHEET && col === 3 && val) { /* 01_sys_config 処理 */ return; }
  // I-01: 36_wrk_bank_import の onEdit 処理
  if (sheetName === '36_wrk_bank_import') { /* … */ }
  // N-03 Step 2: 32_wrk_invoice / 33_wrk_bank の仕訳発行後編集を監査
  try { if (sheetName === '33_wrk_bank' || sheetName === '32_wrk_invoice') { /* … */ } } catch(err) { … }
  try { if (typeof handleUxAssist === 'function') handleUxAssist(e); } catch(err) { … }
}

GAS は同名関数を 1 つしか登録できないため onEdit は本ファイルにのみ存在する。UX 補助は handleUxAssist(e)300_ui/301_ui_assist.js L70)に集約され、シート名で分岐するパターン。

OrderDTO000_infra/003_contracts.js L13-36)

/**
 * @typedef {Object} OrderDTO
 * @property {boolean}     有効フラグ
 * @property {string}      発注ID(ORD)         - "ORD_YYYYMMDD_NNNN"
 * @property {Date}        起票日時
 * @property {string}      起票者
 * @property {string}      取引先名
 * @property {string}      契約・件名
 * @property {string}      摘要
 * @property {string}      契約形態
 * @property {string}      開始年月
 * @property {string}      終了年月
 * @property {number}      税抜金額_発注
 * @property {number}      消費税額_発注
 * @property {number}      税込金額_発注
 * @property {number}      発注残高(自動計算)
 * @property {string}      PJ名
 * @property {string}      組織名
 * @property {string}      発注ステータス
 * @property {string}      参照元区分
 * @property {string}      参照元ID
 * @property {string}      証憑URL
 */

OrderRepository200_data/202_repository.js L107-146)

findAll(){ headers: string[], dtos: OrderDTO[] } を返す。save(dtos)writeDtosToSheet_ でシート全行を全置換。append(dtos) は末尾追記。

Constants.getParam(key, defaultVal)000_infra/002_constants.js L147)

03_sys_params の A 列 = キー、B 列 = 値を初回読込でキャッシュ。val === undefined || null || '' は defaultVal を返す。defaultVal が number 型なら Number 化、それ以外は String 化して返却。

Utils.auditLog000_infra/004_utils.js L438)

シグネチャ: auditLog(operation, targetSheet, targetId, targetCol, funcName, beforeValue, afterValue, note)98_audit_log シートに 1 行追記する WORM 監査ログ。失敗は握りつぶし(無限ループ防止)。

修正方針

Step 1: DDL スキーマへ 5 列追加(100_config/101_sys_config.js L846)

'WRK_ORDR'headers 末尾に以下 5 列を追加。

追加列名プルダウン
承認ステータス文字列「未申請」「承認待ち」「承認済」「却下」
申請者文字列なし
申請日時日時なし
承認者文字列なし
承認日時日時なし
  • 承認ステータス のプルダウン値は MST_DICT カテゴリ「承認ステータス」を参照する(16_wrk_master と同じ仕組み。ドロップダウンソースは 01_sys_dropdown の既存 承認ステータス 列=L1355)。「未申請」「承認待ち」「承認済」「却下」が 15_mst_dictionary に未登録なら同マスタへ手動追加するメモを「実データ検証」に記載。
  • setVali('WRK_ORDR', N, 列letter, '31_wrk_order')100_config/101_sys_config.js の WRK_ORDR バリデーション群(L1416-1419)末尾に追加。新列番号は既存 20 列の直後(21=承認ステータス、22=申請者、23=申請日時、24=承認者、25=承認日時)。プルダウンは「承認ステータス」列のみ(設置: setVali('WRK_ORDR', 21, '<dropColLetter>', '31_wrk_order'))。<dropColLetter>01_sys_dropdown の「承認ステータス」列(既存 L1355、列インデックスはコード上で確定済みのものを再利用)。
  • 数値フォーマット(L1104-1108 の if (key === 'WRK_ORDR') ブロック)に sysReqNumberFormat_(_sId, 'W2:W', 'yyyy-mm-dd hh:mm')(申請日時=W=23 列目)と sysReqNumberFormat_(_sId, 'Y2:Y', 'yyyy-mm-dd hh:mm')(承認日時=Y=25 列目)を追加。
  • inputCols / autoCols(L1178-1182)も再設定: inputCols = [1,16,17,21](=有効フラグ・組織名・発注ステータス・承認ステータス(申請/承認の起点))、autoCols = [2,3,4,5,6,7,8,9,10,11,12,13,14,15,18,19,20,22,23,24,25](申請者・申請日時・承認者・承認日時は handleUxAssist 自動書込)。

Step 2: SHEET_DEFAULTS の更新(000_infra/002_constants.js L84)

既存エントリの defaults'承認ステータス': '未申請' を追加するのみ。新エントリは追加しない。

{ pattern: '31_wrk_order',   defaults: { '発注ステータス': '見積中', '契約形態': 'スポット', '承認ステータス': '未申請' } },

Step 3: OrderDTO 更新(000_infra/003_contracts.js

OrderDTO @typedef 末尾に以下を追加。

 * @property {string}      承認ステータス       - "未申請" | "承認待ち" | "承認済" | "却下"
 * @property {string}      申請者
 * @property {Date}        申請日時
 * @property {string}      承認者                - 承認者メールアドレス、または "SYSTEM"
 * @property {Date}        承認日時

Step 4: 承認フロー実装(300_ui/301_ui_assist.jshandleUxAssist 内)

GAS では onEdit を新規定義不可。本プロジェクトの onEdit100_config/101_sys_config.js L409 にあり、末尾で handleUxAssist(e) を呼ぶ。承認ロジックは handleUxAssist 内の既存シート分岐パターンに倣って 300_ui/301_ui_assist.js に追記する。

// handleUxAssist 内、シート名分岐の追加例
} else if (sName.includes('31_wrk_order')) {
  if (colName === '承認ステータス') {
    handleOrderApproval_(e, sheet, row, headers, val, e.oldValue);
  } else if (isApprovedOrderRevertTarget_(colName) && getCellValue('承認ステータス') === '承認済') {
    sheet.getRange(row, headers.indexOf('承認ステータス') + 1).setValue('未申請');
    SpreadsheetApp.getUi().alert('承認済レコードの金額が変更されたため、承認ステータスを「未申請」に戻しました。再申請してください。');
  }
}

4-A: 申請アクション(「未申請」→「承認待ち」)

  1. LockService.getScriptLock().tryLock(30000) でスクリプトグローバルロック取得。失敗時は UI アラートで再試行を促し処理中断。
  2. OrderRepository.findAll() で最新データを取得し、編集行の 発注ID(ORD) で対象 DTO を特定。
  3. 当該 DTO の 税込金額_発注 をバリデーション: 未入力(空文字・null・undefined)またはマイナス値はエラーアラートし、承認ステータス'未申請' に戻して中断。0 円は通過(閾値以下扱い)。
  4. 閾値取得: var threshold = Constants.getParam('APPROVAL_THRESHOLD_ORDER', 0);(未設定時は 0 = 全件要承認の保守的フェイルセーフ)。
  5. 金額 ≦ 閾値: DTO の 承認ステータス='承認済'申請者=Session.getActiveUser().getEmail()申請日時=new Date()承認者='SYSTEM'承認日時=new Date() を設定。
  6. 金額 > 閾値: 承認者メールを取得 var approver = Constants.getParam('APPROVER_EMAIL_ORDER', '');。空文字なら UI アラートで「APPROVER_EMAIL_ORDER03_sys_params に設定してください」と表示し、承認ステータス'未申請' に戻して中断。空でなければ sendApprovalEmail_(orderId, amount, approver)MailApp.sendEmail 通知。DTO の 承認ステータス='承認待ち'申請者申請日時 を更新。
  7. OrderRepository.save(dtos) で書き戻し。
  8. Utils.auditLog('UPDATE', '31_wrk_order', orderId, '承認ステータス', 'handleOrderApproval_', oldValue, newValue, '申請: amount=' + amount + ', threshold=' + threshold) を記録。

4-B: 承認アクション(「承認待ち」→「承認済」or「却下」)

  1. LockService.getScriptLock().tryLock(30000) でロック取得。
  2. OrderRepository.findAll() で最新データ再取得し、編集行の 発注ID(ORD) で対象 DTO を特定。
  3. DTO の 承認ステータス=新値、承認者=Session.getActiveUser().getEmail()承認日時=new Date() を設定。
  4. OrderRepository.save(dtos) で書き戻し。
  5. Utils.auditLog('UPDATE', '31_wrk_order', orderId, '承認ステータス', 'handleOrderApproval_', '承認待ち', newValue, '承認: approver=' + approverEmail) を記録。

4-C: 不正なステータス遷移の検知

許容される遷移は以下のみ:

遷移元 → 遷移先区分
未申請承認待ち申請(4-A)
未申請承認済(金額 ≦ 閾値の場合のみ自動的に書き換わる)自動承認(4-A)
承認待ち承認済承認(4-B)
承認待ち却下却下(4-B)
承認済未申請自動巻き戻し(4-D 経由のみ)
却下未申請再申請準備(手動)

上記以外(例: 承認済承認待ち却下承認済 の手動操作)は e.oldValuesetValue で書き戻し、UI アラートで警告。

4-D: 承認済レコードの金額変更検知

承認ステータス'承認済' の行で 税込金額_発注 / 税抜金額_発注 / 消費税額_発注 のいずれかが編集された場合、承認ステータス'未申請' に戻し、UI アラート「金額が変更されたため再申請してください」。これにより承認後の金額改ざんを防ぐ。

4-E: ヘルパー関数 sendApprovalEmail_(orderId, amount, approverEmail)

300_ui/301_ui_assist.js 内に同ファイル内ヘルパー(末尾に追加)として定義。将来 MAS-104 支払依頼ワークフローと共通化する余地を残すため、シグネチャは案件 ID と金額を引数に取る形に分離。本文は: 件名「【承認依頼】発注 ${orderId}(${amount.toLocaleString()}円)」、本文に 31_wrk_order シートへの直リンク(SpreadsheetApp.getActiveSpreadsheet().getUrl())と「承認ステータス列を『承認済』『却下』いずれかに変更してください」のメッセージ。MailApp.sendEmail({ to: approverEmail, subject, body })

影響範囲

ファイル変更内容行数概算
100_config/101_sys_config.jsWRK_ORDR headers に 5 列追記 / setVali 1 行追加 / 数値フォーマット 2 行追加 / inputCols・autoCols 再設定+10
000_infra/002_constants.jsSHEET_DEFAULTS31_wrk_order defaults に 1 キー追加+1
000_infra/003_contracts.jsOrderDTO @typedef に 5 行追加+5
300_ui/301_ui_assist.jshandleUxAssist 内に 31_wrk_order 分岐追加 + handleOrderApproval_ + sendApprovalEmail_ ヘルパー定義+120

既存処理への影響:

  • onEdit(e)(101_sys_config.js L409-459)は変更しない。handleUxAssist 内分岐の追加のみ。既存の他シート分岐とは else if で相互排他になるため干渉なし。
  • setupAllSchemas 実行(DDL 再適用)で既存 31_wrk_order の 21〜25 列目に新ヘッダーが書き込まれる。既存データ行の 21 列目以降が空のため上書きで欠損は発生しないが、念のため dev 環境で実行 → 全行スキャン後に prod デプロイする運用を必須とする。
  • OrderRepository.save(dtos)writeDtosToSheet_ でデータ範囲を clearContent() してから setValues するため、ロック取得→findAll() 再取得→更新→save の順序を厳守する(ロック取得後に古いスナップショットで save すると並行編集を消失させる)。

注意事項

  1. onEdit100_config/101_sys_config.js L409 に既に定義されている。新規 onEdit 関数を 300_ui/301_ui_assist.js 等に定義しないこと(GAS 制約: 同名関数は最後の定義のみ有効。新規定義すると既存の 01_sys_config / 36_wrk_bank_import / 33_wrk_bank / 32_wrk_invoice / handleUxAssist 起動が全て失われる)。本案件のロジックは handleUxAssist 内の else if (sName.includes('31_wrk_order')) 分岐に追記する。
  2. LockService.getScriptLock() はスクリプトグローバルロックであり、特定の発注 ID 単位で排他制御する仕組みではない。同一スクリプト内の全発注編集が直列化されるため、複数ユーザー同時操作時はロック待ちが発生する。許容範囲(onEdit は数秒以内で完了する想定)。
  3. OrderRepository.save(dtos) はシート全行を clearContent() → 全置換するため、ロック取得後に必ず OrderRepository.findAll() で最新データを再取得してから対象 DTO のみ書き換えること。古いスナップショットで save すると別ユーザーの並行編集を上書きする。
  4. Constants.SHEET_DEFAULTS31_wrk_order エントリには prefix フィールドがない(ID 採番ルールは ID_PREFIX_MAP 側で別管理)。defaults のみ拡張すること。新規エントリ { pattern: '31_wrk_order', prefix: ..., defaults: { '承認ステータス': '未申請' } }追加しないこと(同 pattern が 2 つになり挙動未定義)。
  5. MailApp.sendEmail は GAS の 1 日あたりメール送信上限に従う(個人 Gmail: 100 通/日、Workspace Standard: 1500 通/日)。発注承認は通常 1 日数件想定のため上限内だが、MailApp.getRemainingDailyQuota()sendApprovalEmail_ 内でチェックし、残量 0 のときは Utils.logError + UI アラートで通知し送信スキップ(フェイルセーフ)。
  6. setupAllSchemas 実行で DDL スキーマが再適用される。Step 1 実施後は dev 環境で setupAllSchemas → 既存発注データの全列内容(A〜T 列)が壊れていないことを目視確認 → prod デプロイ → prod でも setupAllSchemas 実行、の順序を厳守する。
  7. Constants.getParam は初回読込のキャッシュ方式(_paramsCache)。03_sys_params を更新した直後は GAS スクリプト実行をいったん終了するか、新セッションで反映される。本案件では onEdit ごとに新規実行コンテキストになるため通常は問題ないが、デバッグ時に挙動が古く見える場合はキャッシュ要因を疑う。
  8. 「承認ステータス」プルダウンは 15_mst_dictionary カテゴリ「承認ステータス」のエントリを参照する(既存 16_wrk_master 等と共通)。「未申請」「承認待ち」「承認済」「却下」の 4 値が同マスタに全て登録されているか実装前に確認(実データ検証セクション参照)。未登録があれば手動で追加する。

エッジケース

条件動作理由
金額が 0 円自動承認(「承認済」、承認者=SYSTEM、承認日時=now)閾値以下と同じ扱い。0 円発注は実質的に発注額なしのため承認不要
金額がマイナス値申請処理中断、UI アラート、承認ステータス を「未申請」に戻す発注金額として不正な値(マイナス発注は会計上ありえない)
金額が未入力(空文字・null・undefined)申請処理中断、UI アラート「税込金額_発注を入力してください」、承認ステータス を「未申請」に戻すバリデーション必須。金額不明で承認フロー開始不可
APPROVAL_THRESHOLD_ORDER 未設定Constants.getParam のデフォルト値 0 を適用 → 全件「承認待ち」(メール送信が発火)閾値なし=全て承認必要というフェイルセーフ。ただし APPROVER_EMAIL_ORDER も未設定なら次行で中断する
APPROVER_EMAIL_ORDER 未設定または空文字申請処理中断、UI アラート「APPROVER_EMAIL_ORDER03_sys_params に設定してください」、承認ステータス を「未申請」に戻す承認者不明のままメール送信は不可。[email protected] 等のハードコードフォールバックはしない(誤通知防止)
承認済レコードの 税込金額_発注 / 税抜金額_発注 / 消費税額_発注 変更承認ステータス を「未申請」に戻し、UI アラート「金額が変更されたため再申請してください」金額変更後の承認改ざん防止。再申請を強制
不正なステータス遷移(例: 承認済承認待ち却下承認済 の手動操作)e.oldValue で書き戻し、UI アラート「不正なステータス遷移です」フロー整合性の保護。許容遷移マトリクス(修正方針 4-C)に準拠
31_wrk_order 以外のシートで onEdit 発火何もしない(else if パターンマッチで早期リターン)既存 handleUxAssist ロジックと同じシート判定パターンの踏襲
LockService.tryLock(30000) で 30 秒以内にロック取得失敗処理スキップ、UI アラート「同時編集中です。少し待ってから再試行してください」、承認ステータスe.oldValue に戻すGAS LockService 標準エラー処理。同時編集排他制御
編集行に 発注ID(ORD) が空申請処理中断、UI アラート「発注 ID が未採番です。先に他列を編集して ID を採番してください」、承認ステータスe.oldValue に戻すOrderRepository は ID で行特定するため、ID なしは処理不可
MailApp.getRemainingDailyQuota() === 0申請処理中断、UI アラート「メール送信上限に達しました。明日以降に再申請してください」、Utils.logError で記録、承認ステータスe.oldValue に戻すWorkspace 上限到達時のフェイルセーフ。サイレント失敗を避ける
APPROVAL_THRESHOLD_ORDER に数値以外(文字列等)が設定Constants.getParamNumber(val)NaN を返す可能性。isNaN(threshold) ガードで 0 にフォールバックNaN 比較は常に false → 全件「承認済」になる危険を防ぐ。実装時に if (isNaN(threshold)) threshold = 0; を必須
同じ発注 ID が findAll() 結果に複数存在(重複行)最初に見つかった DTO のみ更新し、Utils.logError で警告ログを記録データ不整合の検知。重複は別途クリーンアップ案件

実データ検証

実装前に以下を MCP またはシート直接確認すること:

  • 15_mst_dictionary シートにカテゴリ「承認ステータス」のエントリが「未申請」「承認待ち」「承認済」「却下」の 4 値すべて登録されているか(既存登録は 16_wrk_master で「未申請」のみ使われている可能性大)。未登録なら手動追加が必要(A 列=TRUE、B 列=承認ステータス、D 列=各値)。
  • 03_sys_params シートに APPROVAL_THRESHOLD_ORDERAPPROVER_EMAIL_ORDER の 2 キーが存在するか。未存在なら実装後に手動追加(A 列=キー、B 列=値)。dev/prod それぞれで設定が必要。
  • 31_wrk_order シートの現在の列数・最右列。DDL 追記で 21〜25 列目に新ヘッダーが書き込まれることを確認。既存データ行の 21 列目以降が空であることも確認(万一既存値があれば事前退避)。
  • 98_audit_log シートの存在確認(setupAllSchemas 実行済みか)。未存在なら Utils.auditLog 内で自動作成されるが、依存先として明示的に確認しておく。
  • onEdit の現状の所在を再確認: 100_config/101_sys_config.js L409 にのみ存在し、300_ui/301_ui_assist.js には存在しないこと(Grep "function onEdit" で全リポジトリ検索)。
  • handleUxAssist300_ui/301_ui_assist.js L70)の既存シート判定パターン(sName.includes('xxx') の if/else if 連鎖)の構造。31_wrk_order 用分岐の挿入位置を確定する。
  • 01_sys_dropdown シート(MST_DICT から動的生成)の「承認ステータス」列の実際の列レター(100_config/101_sys_config.js L1355)。setVali('WRK_ORDR', 21, '<dropColLetter>', '31_wrk_order') の引数決定に必要。

関連ドキュメント

ドキュメント関連箇所
CLAUDE.mdプロダクトポリシー: Human-in-the-Loop / 確認 FLG パターン。コーディング規約: シート書き込み位置は列 B(ID 列)で最終行判定
docs/prd.mdプロダクトポリシー Human-in-the-Loop。AI/自動処理の結果は必ず人間レビュー・承認
docs/_internal/TODO_future.mdMAS-128 案件定義(金額閾値・代理承認・例外設計の人間検討事項)
dev_mas-080_pipeline_early_id.mdonEdit 拡張パターンの参考(21_bud_pipeline でのシート分岐追加例)
将来: MAS-104 支払依頼ワークフロー仕様書(未作成)共通基盤化の検討対象。本案件のヘルパー sendApprovalEmail_ を共通化候補として残す

人間が検討すべき事項

TODO_future.md 由来の検討事項

  • MAS-104 との共通化範囲: 本案件では 31_wrk_order 特化実装とする。ただしメール通知ロジックは将来の MAS-104(支払依頼ワークフロー)共通化を見据え、ヘルパー関数 sendApprovalEmail_(orderId, amount, approverEmail) として 300_ui/301_ui_assist.js に分離設計する。共通化時は引数を (targetType, targetId, amount, approverEmail) のように汎化し、000_infra/004_utils.js 等の上位レイヤに移管する。
  • 代表不在時の代理承認ルール: 03_sys_paramsDELEGATE_APPROVER_EMAIL_ORDER キーを将来追加する設計余地を残す(本案件では実装しない)。システム的な自動委譲(一定時間応答なしで代理に転送)は MAS-128 スコープ外。運用ルールとして「代表不在時は代理承認者が 承認ステータス を直接変更する」手順を社内ドキュメント化する(人間判断)。
  • 閾値未満でも特定取引先は承認必須などの例外設計: 本案件スコープ外。将来の拡張機能とする。実装時は 12_mst_partner に「承認必須フラグ」列を追加するか、03_sys_params に「ALWAYS_APPROVE_PARTNERS」リストを追加する案。

調査で判明した追加事項

  • 閾値の初期値(金額): APPROVAL_THRESHOLD_ORDER の業務的に妥当な金額レベル(10万円? 50万円? 100万円?)は会社規模・社内決裁規程に依存。dev 環境では 10 万円、prod 環境では人間判断で初期値を決定する。
  • 承認者の単数 vs 複数: 本案件は単一承認者(APPROVER_EMAIL_ORDER)想定。複数承認者(金額帯別の承認者リスト)は将来拡張。
  • 却下理由の記録: 本案件では「却下」ステータスのみ記録し、却下理由列は追加しない。理由が必要なら 摘要 列に手入力する運用とする(列追加でテーブルが肥大化するリスク回避)。将来「却下理由」列を追加する場合は別案件で扱う。
  • 承認ワークフローの監査ログとの関係: Utils.auditLog で申請・承認・却下を記録するが、98_audit_log は WORM(追記専用)として設計済み。承認履歴の改ざん検知は監査ログ側で担保される。
  • 「承認待ち」のままメール再送機能の要否: 承認待ち で長期間放置された場合のリマインダ機能は本案件スコープ外。将来必要なら時間ベーストリガで 承認待ち レコードを抽出して再送する仕組みを追加(別案件)。

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

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-128「発注承認ワークフロー(金額閾値ベース)」を実装してください。

## 実行前タスク
- `100_config/101_sys_config.js`: setupAllSchemas の WRK_ORDR スキーマ定義(L846)の現在の列構成・行番号を確認する。setVali 群(L1416-1419)と数値フォーマットブロック(L1104-1108)、inputCols/autoCols(L1178-1182)も確認
- `100_config/101_sys_config.js`: 既存 onEdit(e)(L409-459)の全実装を確認する。本案件では onEdit 自体は変更しない(handleUxAssist 経由で処理を追加する)
- `300_ui/301_ui_assist.js`: 既存 handleUxAssist(e)(L70 起点)の全シート分岐パターンを確認する。31_wrk_order 用分岐の挿入位置を特定
- `000_infra/002_constants.js`: SHEET_DEFAULTS の 31_wrk_order エントリ(L84)の現在の型・フィールドを確認。`{ pattern, defaults }` 形式で `prefix` フィールド未定義であることを再確認
- `000_infra/003_contracts.js`: OrderDTO @typedef(L13-36)の現在のフィールド一覧を確認
- `200_data/202_repository.js`: OrderRepository.findAll() / save() の返り値型と内部実装(writeDtosToSheet_ がシート全置換であること)を確認
- `000_infra/004_utils.js`: Utils.auditLog のシグネチャ(8引数)を確認
- `15_mst_dictionary` シート: カテゴリ「承認ステータス」のエントリに「未申請」「承認待ち」「承認済」「却下」が登録されているか確認。未登録なら手動で追加(A=TRUE, B=承認ステータス, D=各値)
- `03_sys_params` シート: `APPROVAL_THRESHOLD_ORDER`、`APPROVER_EMAIL_ORDER` の 2 キーが登録されているか確認。未登録なら手動で追加

## 修正対象ファイル
1. `100_config/101_sys_config.js` のみ(DDL スキーマ・setVali・数値フォーマット・inputCols/autoCols 変更)
2. `000_infra/002_constants.js` のみ(SHEET_DEFAULTS 拡張)
3. `000_infra/003_contracts.js` のみ(OrderDTO @typedef 追記)
4. `300_ui/301_ui_assist.js` のみ(handleUxAssist へ 31_wrk_order 分岐追加 + ヘルパー関数 2 つ)

## 実装内容

### Step 1: DDL スキーマ変更(101_sys_config.js)
- L846 の WRK_ORDR `headers` 配列末尾に以下 5 列を追加: `"承認ステータス","申請者","申請日時","承認者","承認日時"`(最終列は 25 列目になる)
- L1416-1419 付近の WRK_ORDR setVali 群末尾に `setVali('WRK_ORDR', 21, '<dropColLetter>', '31_wrk_order')` を追加。`<dropColLetter>` は L1355 の「承認ステータス」が並ぶ `01_sys_dropdown` の列レター(formulas 配列のインデックスから算出。L1355 が 14 番目=N 列)
- L1104-1108 の `if (key === 'WRK_ORDR')` ブロックに `batchRequests.push(sysReqNumberFormat_(_sId, 'W2:W', 'yyyy-mm-dd hh:mm'));` と `batchRequests.push(sysReqNumberFormat_(_sId, 'Y2:Y', 'yyyy-mm-dd hh:mm'));` を追加
- L1178-1182 の `if (key === 'WRK_ORDR')` の inputCols/autoCols を以下に置換:
  - `inputCols = [1,16,17,21];`(有効フラグ・組織名・発注ステータス・**承認ステータス**)
  - `autoCols = [2,3,4,5,6,7,8,9,10,11,12,13,14,15,18,19,20,22,23,24,25];`(申請者・申請日時・承認者・承認日時を含む)

### Step 2: SHEET_DEFAULTS の更新(002_constants.js)
- L84 の既存エントリ `{ pattern: '31_wrk_order',   defaults: { '発注ステータス': '見積中', '契約形態': 'スポット' } }` の `defaults` に `'承認ステータス': '未申請'` を追加するのみ
- 新規エントリを追加しないこと(同 pattern 重複は挙動未定義)

### Step 3: OrderDTO 更新(003_contracts.js)
- L13-36 の OrderDTO @typedef 末尾に以下 5 行を追記:
  `* @property {string}      承認ステータス       - "未申請" | "承認待ち" | "承認済" | "却下"`
  `* @property {string}      申請者`
  `* @property {Date}        申請日時`
  `* @property {string}      承認者                - 承認者メールアドレス、または "SYSTEM"`
  `* @property {Date}        承認日時`

### Step 4: handleUxAssist へ承認フロー追記(301_ui_assist.js)
- 既存 handleUxAssist 内の if/else if 連鎖の適切な位置に `else if (sName.includes('31_wrk_order')) { ... }` を追加
- 分岐内では:
  - 編集列 `承認ステータス` のとき: `handleOrderApproval_(e, sheet, row, headers, e.value, e.oldValue)` を呼ぶ
  - 編集列が `税込金額_発注` / `税抜金額_発注` / `消費税額_発注` のいずれかかつ現在の `承認ステータス` が `'承認済'` のとき: `承認ステータス` セルを `'未申請'` に書き換え、UI アラート
- 同ファイル末尾にヘルパー関数を追加:
  - `function handleOrderApproval_(e, sheet, row, headers, newValue, oldValue)` — 仕様書「修正方針 Step 4-A/4-B/4-C」に従う処理
  - `function sendApprovalEmail_(orderId, amount, approverEmail)` — 仕様書「修正方針 Step 4-E」に従うメール送信
- 申請(未申請→承認待ち or 承認済)と承認(承認待ち→承認済 or 却下)の分岐は `oldValue` と `newValue` の組み合わせで判定
- 申請時のフロー:
  1. `LockService.getScriptLock().tryLock(30000)` 失敗時は UI アラート + `setValue(oldValue)` で中断
  2. `OrderRepository.findAll()` で最新データ再取得
  3. `発注ID(ORD)` で対象 DTO 特定。空なら UI アラート + `setValue(oldValue)` で中断
  4. `税込金額_発注` バリデーション(負値 / 空 / undefined はエラー、`setValue('未申請')`)
  5. `var threshold = Constants.getParam('APPROVAL_THRESHOLD_ORDER', 0); if (isNaN(threshold)) threshold = 0;`
  6. 金額 ≦ 閾値: DTO 5列を更新(承認ステータス=承認済、申請者=Session.getActiveUser().getEmail()、申請日時=now、承認者=SYSTEM、承認日時=now)→ `OrderRepository.save(dtos)` で保存。セルの値を「承認済」に書き換え(自動承認の即時反映)
  7. 金額 > 閾値: `var approver = Constants.getParam('APPROVER_EMAIL_ORDER', '');` 空なら UI アラート + `setValue('未申請')` で中断。空でなければ `MailApp.getRemainingDailyQuota()` チェック → `sendApprovalEmail_(orderId, amount, approver)` → DTO 3 列更新(承認ステータス=承認待ち、申請者、申請日時)→ save
  8. `Utils.auditLog('UPDATE', '31_wrk_order', orderId, '承認ステータス', 'handleOrderApproval_', oldValue, newValueAfterFlow, '申請: amount=' + amount + ', threshold=' + threshold)`
- 承認時のフロー:
  1. ロック取得(同上)
  2. findAll → DTO 特定
  3. DTO 3 列更新(承認ステータス=新値、承認者=Session.getActiveUser().getEmail()、承認日時=now)→ save
  4. `Utils.auditLog('UPDATE', '31_wrk_order', orderId, '承認ステータス', 'handleOrderApproval_', '承認待ち', newValue, '承認/却下: approver=' + approverEmail)`
- 不正遷移検知: oldValue/newValue の組み合わせが許容遷移マトリクス(仕様書 4-C)外なら `setValue(oldValue)` + UI アラート
- `sendApprovalEmail_`:
  - 件名: `'【承認依頼】発注 ' + orderId + '(' + amount.toLocaleString() + '円)'`
  - 本文: `'発注の承認をお願いします。\n\n発注ID: ' + orderId + '\n金額: ' + amount.toLocaleString() + ' 円\n\nスプレッドシート: ' + SpreadsheetApp.getActiveSpreadsheet().getUrl() + '\n31_wrk_order の対象行で「承認ステータス」を「承認済」または「却下」に変更してください。'`
  - `MailApp.sendEmail({ to: approverEmail, subject: subject, body: body })`
  - `MailApp.getRemainingDailyQuota() === 0` なら `Utils.logError('sendApprovalEmail_', new Error('mail quota exhausted'), 'orderId=' + orderId)` + 例外スロー(呼び出し元で catch して UI アラート)

## 制約
- **`onEdit` を新規関数として `300_ui/301_ui_assist.js` 等に定義しないこと**(GAS 制約: 同名関数は最後の定義のみ有効。新規定義すると既存 `100_config/101_sys_config.js` L409 の onEdit が失われる)。承認ロジックは handleUxAssist 内に追記する
- **`Constants.SHEET_DEFAULTS` の 31_wrk_order は「既存エントリの defaults 拡張」**であり、配列への新エントリ追加ではない。同 pattern が 2 つになる変更は不可
- **`APPROVER_EMAIL_ORDER` 未設定時に `[email protected]` 等のフォールバックメールをハードコードしないこと**。未設定時は明示的に処理中断 + UI アラートで設定を促す
- **`LockService.getScriptLock()` はスクリプトグローバルロック**。「発注 ID 単位のロック」「行単位のロック」という記述・実装をしないこと
- **`OrderRepository.save(dtos)` 呼び出し前に必ず `OrderRepository.findAll()` で最新データを再取得**すること(save はシート全置換のため、古いスナップショットでの save は並行編集を上書きする)
- **`isNaN(threshold)` ガード必須**: `Constants.getParam('APPROVAL_THRESHOLD_ORDER', 0)` が文字列を Number 化して NaN を返す可能性。NaN 比較は常に false → 全件「承認済」になる危険あり
- 既存の onEdit / 他シート分岐 / 他 RPA / Action A・B のロジックは変更しないこと

## エッジケース
(仕様書「エッジケース」テーブルの全項目に従うこと。特に: 0 円自動承認、マイナス値中断、`APPROVER_EMAIL_ORDER` 未設定中断、承認済の金額変更で「未申請」巻き戻し、不正遷移の `setValue(oldValue)` 戻し、ロック失敗時の戻し、発注 ID 空時の中断、メール上限到達時の中断)

## 実データ検証
1. 実装前に `15_mst_dictionary` シートを開き、カテゴリ「承認ステータス」のエントリ 4 値の登録を確認
2. 実装前に `03_sys_params` シートを開き、`APPROVAL_THRESHOLD_ORDER`(数値)と `APPROVER_EMAIL_ORDER`(メールアドレス文字列)の登録を確認
3. dev 環境で `31_wrk_order` の現在の列数(20 列であること)と最右列の右側が空であることを確認
4. `01_sys_dropdown` シートの「承認ステータス」列のレター(L1355 の formulas 配列で 14 番目 = N 列)を確認

## 動作確認
1. `npm run push:dev` でデプロイ
2. dev 環境のメニュー「DDL 全更新 (Full)」で setupAllSchemas を実行 → `31_wrk_order` の 21〜25 列目に新ヘッダーが追加されていることを確認 → 既存データ(A〜T 列)が壊れていないことを確認
3. dev 環境の `03_sys_params` に `APPROVAL_THRESHOLD_ORDER=100000`、`APPROVER_EMAIL_ORDER=<自分のメール>` を設定
4. `31_wrk_order` に新規発注レコードを 1 件作成(任意の RPA 経由か手動で行追加) → `承認ステータス` が「未申請」になっていることを確認
5. **シナリオ A(自動承認)**: `税込金額_発注` を 50,000 円に設定 → `承認ステータス` を「未申請」→「承認待ち」に変更 → 即座に「承認済」に書き換わり、`承認者`=`SYSTEM`、`申請者`=自分のメール、`申請日時`/`承認日時`=現在時刻が記録されることを確認
6. **シナリオ B(承認待ち + メール送信)**: 別の発注レコードで `税込金額_発注` を 200,000 円に設定 → `承認ステータス` を「未申請」→「承認待ち」に変更 → 「承認待ち」のままで、設定したメールアドレスに承認依頼メールが届くことを確認 → 同レコードの `承認ステータス` を「承認済」に変更 → `承認者`=自分のメール、`承認日時`=現在時刻が記録されることを確認
7. **シナリオ C(承認者未設定)**: `03_sys_params` の `APPROVER_EMAIL_ORDER` を空に設定 → 200,000 円の発注を申請しようとする → UI アラート「`APPROVER_EMAIL_ORDER` を `03_sys_params` に設定してください」が表示され、`承認ステータス` が「未申請」のままであることを確認
8. **シナリオ D(承認済の金額変更)**: シナリオ A で承認済になったレコードの `税込金額_発注` を変更 → `承認ステータス` が「未申請」に戻り、UI アラートが表示されることを確認
9. **シナリオ E(不正遷移)**: 「承認済」レコードの `承認ステータス` を「承認待ち」に手動変更 → 元の値に戻され、UI アラートが表示されることを確認
10. **シナリオ F(金額バリデーション)**: 金額未入力 / マイナス値の状態で申請 → エラーアラートが表示され、`承認ステータス` が「未申請」のまま戻ることを確認
11. `98_audit_log` シートに上記操作の記録(operation=UPDATE、対象シート=31_wrk_order、対象列=承認ステータス、関数名=handleOrderApproval_)が追記されていることを確認

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

| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| Phase 1(ファイル調査・設計確定) | あり | onEdit 所在の確認(101_sys_config.js)、handleUxAssist 追記位置の特定、SHEET_DEFAULTS 型の確認、エッジケース網羅、許容遷移マトリクスの設計 |
| Phase 2(実装) | なし | Phase 1 確定内容の書き下しに徹する |

推奨実行モデル

工程推奨モデル理由
Step 1: DDL 変更(101_sys_config.js)Claude Sonnet 4.6スキーマ列追加・setVali 列番号特定・数値フォーマット追加・inputCols/autoCols 再設定など中程度の判断要素あり
Step 2: SHEET_DEFAULTS 拡張(002_constants.js)Claude Haiku 4.5既存エントリの 1 キー追加のみ。コードが完全定義済み
Step 3: OrderDTO 更新(003_contracts.js)Claude Haiku 4.5@typedef 末尾に 5 行追記のみ。コードが完全定義済み
Step 4: 承認フロー実装(301_ui_assist.js)Claude Opus 4.6申請/承認の状態遷移ロジック・ロック制御・エッジケース網羅・既存 handleUxAssist との整合性が必要。会計ロジックの理解(金額閾値・人間判断の境界)も要

変更履歴

日付変更内容
2026-04-22初版作成。31_wrk_order への金額閾値ベース承認ワークフロー追加(5 列追加・handleUxAssist 内に申請/承認フロー実装・MailApp 通知・LockService 排他制御)

仕様書作成プロンプト

再現性・監査性のため、本仕様書を生成する際に Claude Code に投入したプロンプトを以下に全文記録する。

展開して表示
【タイムアウト回避・実行原則(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(骨格)/2-2(概要〜注意事項)/2-3a(エッジケース〜人間検討事項)/2-3b(実装プロンプト〜変更履歴)/2-4(`<details>`プロンプト全文記録)に分割。1回のWrite/Editは約300行以内を目安にする。
4. **各Stepで何を書くかを具体指示**: 設計判断をPhase 2実行時に持ち込まないよう、各Stepの内容はPhase 1で完全確定させてから着手する。

======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 S-56「発注承認ワークフロー(金額閾値ベース)」の開発仕様書を作成してください。
作成後は `docs/_config.json` の `nav` 配列(§E.6 パイプライン・RPA・外部連携)にも必ず追記してください。

---

## Phase 1: 案件情報の収集と関連コード調査(テキスト報告禁止。即座にツール実行)

### 1-A: 案件定義の読み込み
- `docs/_internal/TODO_future.md` でS-56の案件名・概要・人間が検討すべき事項を取得する。

### 1-B: プロジェクト規約の読み込み
- `CLAUDE.md` を読み込み、コーディング規約・ファイル番号体系・プロダクトポリシー(Human-in-the-Loop・確認FLGパターン)を把握する。

### 1-C: 既存仕様書テンプレートの読み込み
- `docs/dev/dev_mas-080_pipeline_early_id.md`(ID採番・onEdit拡張の参考)を読み込み、フォーマットと実装プロンプト構造を把握する。

### 1-D: 関連コードの調査(Grep→Read原則を厳守)

**必ず Read で型・固有名詞・呼び出し経路を裏取りしてから仕様書に記載すること。名前から推測した瞬間に手を止めてReadする。**

以下の順序でファイルを調査し、下記の確認ポイントを全て把握してから Phase 2 に進む:

| ファイル | 確認ポイント |
|---------|------------|
| `000_infra/002_constants.js` | `SHEET_DEFAULTS` 配列の `31_wrk_order` エントリの現在の型・フィールド構成(新列追加の差分を特定するため)。`getParam(key, defaultVal)` の実際のシグネチャと `03_sys_params` からの読み込み仕組み |
| `000_infra/003_contracts.js` | `OrderDTO` の `@typedef` — 現在定義されているフィールド一覧。追加する5列が既存フィールドと重複しないか確認 |
| `000_infra/004_utils.js` | `auditLog(operation, targetSheet, targetId, targetCol, funcName, beforeValue, afterValue, note)` のシグネチャと引数の意味。`logInfo` / `logError` のシグネチャ |
| `200_data/202_repository.js` | `OrderRepository.findAll()` / `save(dtos)` / `append(dtos)` の返り値型と使い方。`readSheetAsDtos_` / `writeDtosToSheet_` の内部ヘルパー構造 |
| `100_config/101_sys_config.js` | `setupAllSchemas`(DDL)の `31_wrk_order` スキーマ定義の現在の列構成と、承認ステータスのプルダウン設定パターン(他シートの `承認ステータス` 列の定義を参考にする)。`onOpen()` の既存メニュー構造(S-56用メニュー追加が必要か判断するため)。`onEdit(e)` が定義されているかどうか |
| `300_ui/301_ui_assist.js` | `onEdit(e)` の既存実装全体。GASでは同名関数を複数定義できないため、既存の `onEdit` 内への追記が必須。シート名・列名の判定パターン(既存の実装パターンに倣って実装する)|
| `000_infra/002_constants.js`(再確認) | `SHEET_DEFAULTS` の `32_wrk_expense` / `33_wrk_finance` エントリ — `承認ステータス: '未申請'` の設定パターンを参照パターンとして把握 |

---

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

**出力先**: `docs/dev/dev_mas-128_order_approval_workflow.md`
**絶対に1回のツール呼び出しで全内容を出力せず、以下のStepに分割して実行すること。**

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

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

以下の各セクションを記述する:

**概要テーブル**: 案件ID=S-56、カテゴリ=ワークフロー・自動化、対象ファイル(Phase 1調査で確定した実ファイルパス)、前提案件(TODO_future.md記載の内容)

**目的**: 発注承認のヒューマンインザループを担保するため、`31_wrk_order` シートに金額閾値ベースの承認ワークフローを追加する。閾値以下は即時自動承認、閾値超は承認者への通知メールを送信し、Human-in-the-Loopを実現する。

**現在のコード**: Phase 1で把握した `31_wrk_order` の既存スキーマ(列構成・SHEET_DEFAULTSエントリ)と、`onEdit` 内の現在の処理内容をコードスニペット(ファイル名+行番号付き)で示す。

**修正方針**: 以下のアーキテクチャ方針を具体的に記述する(全て Phase 1 で Read した実在のコードを参照すること):

- **Step 1: DDL変更** — `100_config/101_sys_config.js` の `setupAllSchemas` 内 `31_wrk_order` スキーマに5列を追加:
  - `承認ステータス`(文字列、プルダウン: 「未申請」「承認待ち」「承認済」「却下」)
  - `申請者`(文字列)
  - `申請日時`(日時)
  - `承認者`(文字列)
  - `承認日時`(日時)
  - `Constants.SHEET_DEFAULTS` の `31_wrk_order` エントリ(Phase 1で確認した実際のオブジェクト形式)に `'承認ステータス': '未申請'` を追加。**新エントリを追加するのではなく、既存の `31_wrk_order` エントリの `defaults` を拡張する**こと。

- **Step 2: onEditトリガー実装** — `300_ui/301_ui_assist.js` の**既存の `onEdit(e)` 関数内**に追記(GASでは同名関数を複数定義不可)。以下のフローを実装:
  - 編集シートが `31_wrk_order`(`Utils.getSheetNameByKey('WRK_ORDR')` または フォールバック名)かつ編集列が `承認ステータス` 列であることを確認
  - **申請アクション(「未申請」→「承認待ち」への変更)**:
    1. `LockService.getScriptLock()` でスクリプトグローバルロック取得(30秒タイムアウト)
    2. `OrderRepository.findAll()` で対象レコードを取得。編集行の `発注ID` で特定
    3. `税込金額_発注` を取得し、バリデーション(未入力・負値はエラーアラートしてセルを元に戻す)
    4. 閾値取得: `Constants.getParam('APPROVAL_THRESHOLD_ORDER', 100000)`
    5. 金額 ≤ 閾値: ステータスを「承認済」、`承認者`=`'SYSTEM'`、`承認日時`=`new Date()` に更新し、`OrderRepository.save(dtos)` で書き戻す
    6. 金額 > 閾値: 承認者メール取得 `Constants.getParam('APPROVER_EMAIL_ORDER', '')` → 空文字の場合は処理中断しUIアラート(`SpreadsheetApp.getUi().alert`)で設定を促す。非空の場合は `MailApp.sendEmail()` で通知メール送信
    7. `Utils.auditLog('RUN', '31_wrk_order', 発注ID, '承認ステータス', 'onEditOrderApproval_', 旧値, 新値, context)` で記録
  - **承認アクション(「承認待ち」→「承認済」or「却下」への変更)**:
    1. `LockService.getScriptLock()` でロック取得
    2. 対象レコードを取得し、`承認者`=`Session.getActiveUser().getEmail()`、`承認日時`=`new Date()` を記録
    3. `OrderRepository.save(dtos)` で書き戻し
    4. `Utils.auditLog` で記録
  - **不正遷移の検知**: 定義外の遷移(例: 「承認済」→「承認待ち」)は検知してセルを元の値に戻し、UIアラートで警告
  - **承認済レコードの重要項目変更**: `税込金額_発注` 等の金額列が「承認済」レコードで編集された場合、`承認ステータス` を「未申請」に戻し再申請を促す

- **Step 3: OrderDTOの更新** — `000_infra/003_contracts.js` の `OrderDTO @typedef` に5列を追加

**影響範囲**: 変更ファイル(`101_sys_config.js` / `301_ui_assist.js` / `003_contracts.js` / `002_constants.js`)と変更量の見積もり、`onEdit` 追記による既存処理への影響(既存の `onEdit` ロジックとのシート判定分岐が干渉しないか)

**注意事項**(番号付きリスト):
1. `onEdit` は新規関数として定義せず、既存の `onEdit(e)` 内に追記すること(GAS制約)
2. `LockService.getScriptLock()` はスクリプトグローバルロックであり、特定レコードIDへの排他制御ではない。同時編集が発生した場合の挙動を考慮する
3. `OrderRepository.save(dtos)` はシート全行を書き直すため、同時編集リスクあり。ロック取得後に再度 `findAll()` して最新状態を取得してから書き戻すこと
4. `Constants.SHEET_DEFAULTS` の `31_wrk_order` エントリに `prefix` フィールドが存在するかどうかをPhase 1で確認し、実際の構造に合わせて追記する
5. メール送信は `MailApp.sendEmail()` を使用。GASの1日あたりのメール送信上限(100〜1500通/日、Workspaceプランにより異なる)に留意
6. `setupAllSchemas` 実行でDDLスキーマが再設定されるため、Step 1実施後は dev環境で既存データが壊れないかを必ず確認する

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

**エッジケース テーブル**(`| 条件 | 表示値/動作 | 理由 |` 形式):

| 条件 | 動作 | 理由 |
|------|------|------|
| 金額が0円 | 自動承認(「承認済」・承認者=SYSTEM) | 閾値以下と同じ扱い |
| 金額がマイナス値 | 申請処理中断・UIアラート・セルを「未申請」に戻す | 発注金額として不正な値 |
| 金額が未入力(空文字・0ではない空) | 申請処理中断・UIアラート・セルを「未申請」に戻す | バリデーション必須 |
| `APPROVAL_THRESHOLD_ORDER` 未設定 | `Constants.getParam` のデフォルト値 `0` を適用(全件要承認) | 閾値なし=全て承認必要というフェイルセーフ |
| `APPROVER_EMAIL_ORDER` 未設定または空文字 | 申請処理中断・UIアラートで設定を促す・セルを「未申請」に戻す | 承認者不明のままメール送信不可。`[email protected]` 等のハードコードはしない |
| 承認済レコードの `税込金額_発注` 変更 | `承認ステータス` を「未申請」に戻す | 金額変更後は再申請必要 |
| 不正なステータス遷移(例: 承認済→承認待ち) | セルを元の値に戻し、UIアラートで警告 | フロー整合性の保護 |
| `31_wrk_order` 以外のシートで `onEdit` 発火 | 何もしない(早期リターン) | 既存 `onEdit` ロジックと同様のシート判定パターン |
| ロック取得タイムアウト(30秒) | 処理をスキップし、UIアラートで再試行を促す | GAS `LockService` 標準エラー処理 |

**実データ検証**(実装前にMCPまたはシート直接確認すべき項目):
- `03_sys_params` シートに `APPROVAL_THRESHOLD_ORDER` / `APPROVER_EMAIL_ORDER` キーが存在するか(未存在なら実装後に手動追記が必要)
- `31_wrk_order` シートの現在の列数・最右列(DDL追記で列が正しい位置に挿入されるか)
- `98_audit_log` シートの存在確認(`setupAllSchemas` 実行済みか)
- `onEdit` が `300_ui/301_ui_assist.js` と `100_config/101_sys_config.js` のどちらに定義されているか(Phase 1で確認済みのはずだが実装前に再確認)

**関連ドキュメント**(テーブル):
- `docs/prd.md` — Human-in-the-Loop / 確認FLGパターンの原則
- S-32案件仕様書(存在する場合)— 承認ワークフローの共通化検討
- `docs/_internal/TODO_future.md` — S-56の人間が検討すべき事項

**人間が検討すべき事項**(TODO_future.md転記 + 以下の追加事項):
- **S-32との共通化範囲**: 本案件では `31_wrk_order` 特化実装とする。ただしメール通知ロジックは将来の共通化を見据えてヘルパー関数 `sendApprovalEmail_(orderId, amount, approverEmail)` として分離設計する
- **代表不在時の代理承認**: `03_sys_params` に `DELEGATE_APPROVER_EMAIL_ORDER` キーを追加する設計とする。システム的な自動委譲は本件スコープ外。運用で代理承認者がステータスを変更する手順をドキュメント化する
- **閾値例外設計**(特定取引先は常に承認必須など): 本案件スコープ外、将来の拡張機能とする

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

以下の内容を行頭4スペースインデント(バッククォートで囲まない)で記述する:

    あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
    案件 S-56「発注承認ワークフロー(金額閾値ベース)」を実装してください。

    ## 実行前タスク
    - `100_config/101_sys_config.js`: setupAllSchemasの31_wrk_orderスキーマ定義(現在の列構成・行番号)を確認する
    - `300_ui/301_ui_assist.js`: 既存onEdit(e)の全実装を確認する(追記位置・既存シート判定パターン)
    - `000_infra/002_constants.js`: SHEET_DEFAULTSの31_wrk_orderエントリの現在の型・フィールドを確認する
    - `000_infra/003_contracts.js`: OrderDTO @typedefの現在のフィールド一覧を確認する

    ## 修正対象ファイル
    1. `100_config/101_sys_config.js` のみ(DDLスキーマ変更)
    2. `300_ui/301_ui_assist.js` のみ(onEdit追記)
    3. `000_infra/002_constants.js` のみ(SHEET_DEFAULTS・OrderDTO更新)
    4. `000_infra/003_contracts.js` のみ(OrderDTO @typedef追記)

    ## 実装内容
    ### Step 1: DDLスキーマ変更(101_sys_config.js)
    - setupAllSchemas内の31_wrk_orderスキーマに以下5列を追加(既存列の末尾に追記):
      `承認ステータス`(プルダウン: 未申請/承認待ち/承認済/却下)/ `申請者` / `申請日時` / `承認者` / `承認日時`

    ### Step 2: SHEET_DEFAULTSの更新(002_constants.js)
    - 既存の31_wrk_orderエントリのdefaultsに `'承認ステータス': '未申請'` を追加する
    - 新規エントリを追加しないこと

    ### Step 3: OrderDTO更新(003_contracts.js)
    - OrderDTO @typedefに5列を追記する

    ### Step 4: onEdit追記(301_ui_assist.js)
    - 既存のonEdit(e)内に追記する。新規onEdit関数を定義しないこと
    - 実装する処理フローは仕様書「修正方針 Step 2」に従う
    - ヘルパー関数 `sendApprovalEmail_(orderId, amount, approverEmail)` を同ファイル内に定義する

    ## 制約
    - onEditを新規関数として定義しないこと(GAS制約: 同名関数は最後の定義のみ有効)
    - Constants.SHEET_DEFAULTSの31_wrk_orderは「既存エントリのdefaults拡張」であり、配列への新エントリ追加ではない
    - APPROVER_EMAIL_ORDER未設定時に [email protected] 等をハードコードしないこと
    - LockService.getScriptLock()はスクリプトグローバルロック。「発注ID単位のロック」という記述をしないこと
    - OrderRepository.save(dtos)呼び出し前に必ずfindAll()で最新データを再取得すること

    ## エッジケース
    (仕様書「エッジケース」テーブルの全項目に従うこと)

    ## 動作確認
    1. npm run push:dev でデプロイ
    2. dev環境の31_wrk_orderシートで承認ステータス列が追加されていることを確認
    3. 発注レコードを1件作成し、承認ステータスが「未申請」になっていることを確認
    4. 承認ステータスを「承認待ち」に変更→閾値以下なら自動で「承認済」になること、閾値超なら「承認待ち」のままでメール送信されることを確認
    5. APPROVER_EMAIL_ORDER未設定時にUIアラートが表示されることを確認
    6. 98_audit_logに申請・承認の記録が追記されることを確認
    7. setupAllSchemas実行後も既存発注データが壊れていないことを確認

    ### 拡張思考の使用状況
    | フェーズ | 拡張思考 | 備考 |
    |---------|---------|------|
    | Phase 1(ファイル調査・設計確定) | あり | onEdit追記位置・SHEET_DEFAULTS型の確定に使用 |
    | Phase 2(実装) | なし | Phase 1確定内容の書き下しに徹する |

**推奨実行モデル テーブル**:
| 工程 | 推奨モデル | 理由 |
|------|----------|------|
| Step 1-3: DDL・定数・DTO更新 | Claude Haiku | 仕様書でコード変更箇所が完全定義済み |
| Step 4: onEdit追記 | Claude Sonnet | 既存onEditへの追記位置特定と既存パターンへの適合が必要 |

**変更履歴 テーブル**: 当日日付で初版作成を記載。

### Step 2-4: 仕様書作成プロンプトの記録(File Edit または Bash)
- 仕様書末尾に `<details><summary>展開して表示</summary>` ブロックを設け、この `<instruction>` プロンプトの全文を追記する。

---

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

1. `docs/_config.json` の `nav` 配列の **§E.6 パイプライン・RPA・外部連携** セクションに以下を追加する:
   ```json
   { "file": "dev/dev_mas-128_order_approval_workflow.md", "title": "E.6.X MAS-128 発注承認ワークフロー(金額閾値ベース)" }
   ```
   (連番Xは既存エントリ数を確認して決定する)
2. `docs/_internal/changelog.md` の先頭に追記する。
3. 変更ファイルを `git add` → `git commit` する(`npm run push:dev` はコード実装後の別工程のためここでは不要)。