MAS-128: 発注承認ワークフロー(金額閾値ベース)
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-128 |
| 案件名 | 発注承認ワークフロー(金額閾値ベース) |
| カテゴリ | ワークフロー・内部統制 |
| Phase | P3 |
| 優先度 | ★ |
| 所要時間 | 4-6時間 |
| 対象ファイル | 100_config/101_sys_config.js(DDLスキーマ・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)に集約され、シート名で分岐するパターン。
OrderDTO(000_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
*/
OrderRepository(200_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.auditLog(000_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.js の handleUxAssist 内)
GAS では onEdit を新規定義不可。本プロジェクトの onEdit は 100_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: 申請アクション(「未申請」→「承認待ち」)
LockService.getScriptLock().tryLock(30000) でスクリプトグローバルロック取得。失敗時は UI アラートで再試行を促し処理中断。OrderRepository.findAll()で最新データを取得し、編集行の発注ID(ORD)で対象 DTO を特定。- 当該 DTO の
税込金額_発注をバリデーション: 未入力(空文字・null・undefined)またはマイナス値はエラーアラートし、承認ステータスを'未申請'に戻して中断。0 円は通過(閾値以下扱い)。 - 閾値取得:
var threshold = Constants.getParam('APPROVAL_THRESHOLD_ORDER', 0);(未設定時は 0 = 全件要承認の保守的フェイルセーフ)。 - 金額 ≦ 閾値: DTO の
承認ステータス='承認済'、申請者=Session.getActiveUser().getEmail()、申請日時=new Date()、承認者='SYSTEM'、承認日時=new Date()を設定。 - 金額 > 閾値: 承認者メールを取得
var approver = Constants.getParam('APPROVER_EMAIL_ORDER', '');。空文字なら UI アラートで「APPROVER_EMAIL_ORDERを03_sys_paramsに設定してください」と表示し、承認ステータスを'未申請'に戻して中断。空でなければsendApprovalEmail_(orderId, amount, approver)でMailApp.sendEmail通知。DTO の承認ステータス='承認待ち'、申請者、申請日時を更新。 OrderRepository.save(dtos)で書き戻し。Utils.auditLog('UPDATE', '31_wrk_order', orderId, '承認ステータス', 'handleOrderApproval_', oldValue, newValue, '申請: amount=' + amount + ', threshold=' + threshold)を記録。
4-B: 承認アクション(「承認待ち」→「承認済」or「却下」)
LockService.getScriptLock().tryLock(30000) でロック取得。OrderRepository.findAll()で最新データ再取得し、編集行の発注ID(ORD)で対象 DTO を特定。- DTO の
承認ステータス=新値、承認者=Session.getActiveUser().getEmail()、承認日時=new Date()を設定。 OrderRepository.save(dtos)で書き戻し。Utils.auditLog('UPDATE', '31_wrk_order', orderId, '承認ステータス', 'handleOrderApproval_', '承認待ち', newValue, '承認: approver=' + approverEmail)を記録。
4-C: 不正なステータス遷移の検知
許容される遷移は以下のみ:
| 遷移元 → 遷移先 | 区分 |
|---|---|
未申請 → 承認待ち | 申請(4-A) |
未申請 → 承認済(金額 ≦ 閾値の場合のみ自動的に書き換わる) | 自動承認(4-A) |
承認待ち → 承認済 | 承認(4-B) |
承認待ち → 却下 | 却下(4-B) |
承認済 → 未申請 | 自動巻き戻し(4-D 経由のみ) |
却下 → 未申請 | 再申請準備(手動) |
上記以外(例: 承認済 → 承認待ち、却下 → 承認済 の手動操作)は e.oldValue を setValue で書き戻し、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.js | WRK_ORDR headers に 5 列追記 / setVali 1 行追加 / 数値フォーマット 2 行追加 / inputCols・autoCols 再設定 | +10 |
000_infra/002_constants.js | SHEET_DEFAULTS の 31_wrk_order defaults に 1 キー追加 | +1 |
000_infra/003_contracts.js | OrderDTO @typedef に 5 行追加 | +5 |
300_ui/301_ui_assist.js | handleUxAssist 内に 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 すると並行編集を消失させる)。
注意事項
onEditは100_config/101_sys_config.jsL409 に既に定義されている。新規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'))分岐に追記する。LockService.getScriptLock()はスクリプトグローバルロックであり、特定の発注 ID 単位で排他制御する仕組みではない。同一スクリプト内の全発注編集が直列化されるため、複数ユーザー同時操作時はロック待ちが発生する。許容範囲(onEdit は数秒以内で完了する想定)。OrderRepository.save(dtos)はシート全行をclearContent()→ 全置換するため、ロック取得後に必ずOrderRepository.findAll()で最新データを再取得してから対象 DTO のみ書き換えること。古いスナップショットでsaveすると別ユーザーの並行編集を上書きする。Constants.SHEET_DEFAULTSの31_wrk_orderエントリにはprefixフィールドがない(ID 採番ルールはID_PREFIX_MAP側で別管理)。defaultsのみ拡張すること。新規エントリ{ pattern: '31_wrk_order', prefix: ..., defaults: { '承認ステータス': '未申請' } }を追加しないこと(同 pattern が 2 つになり挙動未定義)。MailApp.sendEmailは GAS の 1 日あたりメール送信上限に従う(個人 Gmail: 100 通/日、Workspace Standard: 1500 通/日)。発注承認は通常 1 日数件想定のため上限内だが、MailApp.getRemainingDailyQuota()をsendApprovalEmail_内でチェックし、残量 0 のときはUtils.logError+ UI アラートで通知し送信スキップ(フェイルセーフ)。setupAllSchemas実行で DDL スキーマが再適用される。Step 1 実施後は dev 環境でsetupAllSchemas→ 既存発注データの全列内容(A〜T 列)が壊れていないことを目視確認 → prod デプロイ → prod でもsetupAllSchemas実行、の順序を厳守する。Constants.getParamは初回読込のキャッシュ方式(_paramsCache)。03_sys_paramsを更新した直後は GAS スクリプト実行をいったん終了するか、新セッションで反映される。本案件では onEdit ごとに新規実行コンテキストになるため通常は問題ないが、デバッグ時に挙動が古く見える場合はキャッシュ要因を疑う。- 「承認ステータス」プルダウンは
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_ORDER を 03_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.getParam が Number(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_ORDER、APPROVER_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.jsL409 にのみ存在し、300_ui/301_ui_assist.jsには存在しないこと(Grep "function onEdit"で全リポジトリ検索)。handleUxAssist(300_ui/301_ui_assist.jsL70)の既存シート判定パターン(sName.includes('xxx')の if/else if 連鎖)の構造。31_wrk_order 用分岐の挿入位置を確定する。01_sys_dropdownシート(MST_DICTから動的生成)の「承認ステータス」列の実際の列レター(100_config/101_sys_config.jsL1355)。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.md | MAS-128 案件定義(金額閾値・代理承認・例外設計の人間検討事項) |
| dev_mas-080_pipeline_early_id.md | onEdit 拡張パターンの参考(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_paramsにDELEGATE_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` はコード実装後の別工程のためここでは不要)。