概要

項目内容
案件IDMAS-104
カテゴリワークフロー
PhaseP3
優先度
所要時間4〜6時間
対象ファイル400_domain/411_payment_workflow.js(新規)
100_config/101_sys_config.jsonOpen() メニュー追加 + setupAllSchemas() DDL列追加)
000_infra/003_contracts.jsInvoiceDTO @typedef コメントに 5 フィールド追記)
000_infra/002_constants.jsSHEET_DEFAULTS32_wrk_invoice 請求ステータス初期値変更)
前提案件MAS-179(監査証跡 Utils.auditLog)、MAS-192(Repository層)

目的

経費・仕入の支払依頼を「申請 → 承認(または差戻し)→ 確定」の 2 段階で記録する簡易ワークフロー機能を提供する。現状は 32_wrk_invoice で 請求ステータス 列を誰でも直接書き換えられるため、「誰が申請し、誰が承認したか」の証跡が残らない。本案件では以下を実現する:

  • 誰が申請したか申請者 / 申請日時)、誰が承認したか承認者 / 承認日時)、差戻しの場合の理由差戻し理由)を列として永続化する
  • 申請ステータスを 未申請 → 申請中 → 承認済 / 差戻し の有限状態機械として扱い、GAS メニュー経由のみで状態遷移できるようにする
  • 承認者は 03_sys_paramsWORKFLOW_APPROVER_EMAIL(カンマ区切り可)で設定し、Session.getActiveUser().getEmail() と照合して権限判定する
  • 申請者と承認者が同一ユーザーになる自己承認を禁止する(代表1名体制では一時的に緩和できるよう「人間が検討すべき事項」に運用方針を残す)
  • すべての状態遷移を Utils.auditLog()98_audit_log へ記録し、LockService で二重実行を防止する

将来の MAS-264(複雑な承認チェーン)や金額閾値による自動承認(WORKFLOW_AUTO_APPROVE_THRESHOLD キーを予約)への段階的拡張を想定した「簡易版」の実装と位置づける。

現在のコード

000_infra/003_contracts.js L39-67 — InvoiceDTO の現状

/**
 * 32_wrk_invoice — 請求レコード
 * @typedef {Object} InvoiceDTO
 * @property {boolean}     有効フラグ
 * @property {string}      請求ID(INV)          - "INV_YYYYMMDD_NNNN"
 * ...
 * @property {string}      請求ステータス       - "未処理" | "承認済" | "却下"
 * ...
 */

現在の値セットは "未処理" | "承認済" | "却下" の 3 値のみで、「誰が申請したか/承認したか」を記録する列は存在しない。

000_infra/002_constants.js L85 — SHEET_DEFAULTS の 32_wrk_invoice エントリ

{ pattern: '32_wrk_invoice', defaults: { '請求ステータス': '未処理', '収支区分': '支出', '通貨': 'JPY', '申請種別': '請求書受領(AP)' } },

prefix フィールドは存在せず、ID_PREFIX_MAP 側で INV_ を発番している('32_wrk_invoice', prefix: 'INV_', digit: 4, isDate: true)。

100_config/101_sys_config.js L299-308 — 現行 onOpen()

function onOpen() {
  const ui = SpreadsheetApp.getUi();
  // 全操作は右側サイドバーに集約 (狭い画面での top-level メニュー切り詰めを回避)
  ui.createMenu('🚀 BizLP')
    .addItem('操作パネルを開く', 'openOperationsSidebar')
    .addSeparator()
    .addItem('✅ 自動起動を有効化', 'installAutoOpenSidebarTrigger')
    .addItem('🚫 自動起動を無効化', 'uninstallAutoOpenSidebarTrigger')
    .addToUi();
}

現状は「操作パネル」サイドバーに大半の機能を集約している。ワークフローも最終的にはサイドバーへ収容したいが、本案件では最小実装として top-level メニューを 1 つ追加する(本文 L302 ui.createMenu('🚀 BizLP') チェインの .addToUi() にサブメニューを追記する方式は取らず、addToUi() 後に新規 ui.createMenu() を追加する構造を採る)。

100_config/101_sys_config.js L654 — setupAllSchemas()WRK_INVC DDL

'WRK_INVC': { headers: ["有効フラグ","請求ID(INV)","親発注ID(ORD)","起票日時","起票者","申請種別","発生日(P/L計上日)","決済日_計画","請求ステータス","収支区分","取引先名","PJ名","組織名","諸表区分","大分類","科目名","税区分","通貨","税抜金額_計画","消費税額_計画","税込金額_計画","未決済残高(自動計算)","決済手段","摘要","証憑URL","自動仕訳JNL_ID","決済日_実績"], color: "#1155cc" },

請求ステータス は 9 列目(index=8)、末尾は 決済日_実績(27 列目)。

100_config/101_sys_config.js L1034 — MST_DICT の 請求ステータス プルダウン値

['請求ステータス', [['INV_NEW','未処理'], ['INV_APR','承認済'], ['INV_PAR','部分決済'], ['INV_CMP','決済完了'], ['INV_CAN','取消']]],

プルダウン辞書には 未処理 / 承認済 / 部分決済 / 決済完了 / 取消 の 5 値がある。DTO 型定義の "未処理" | "承認済" | "却下" とは不整合で、実運用では部分決済・決済完了も使われる(32タブは「支払依頼ステータス」と「決済サイクルステータス」が混在している)。本案件では 「支払依頼ワークフロー」の 4 値(未申請 / 申請中 / 承認済 / 差戻し)を取り入れるが、部分決済 / 決済完了 / 取消 は決済エンジン側の自動遷移値として温存する。

InvoiceRepository.findAll() / save()200_data/202_repository.js L163-177)

findAll: function() {
  return readSheetAsDtos_(InvoiceRepository._getSheet());
},
save: function(dtos) {
  var sheet = InvoiceRepository._getSheet();
  if (!sheet) return;
  var headers = sheet.getRange(1, 1, 1, sheet.getMaxColumns()).getValues()[0]
    .map(function(h) { return String(h).trim(); });
  writeDtosToSheet_(sheet, headers, dtos);
},

findAll{ headers, dtos } を返し、save は全行を writeDtosToSheet_ で全置換する(既存バリデーション維持)。本案件はこの API 契約を変更しない。

修正方針

Step 1: 000_infra/003_contracts.jsInvoiceDTO コメントの拡張(JSDoc のみ)

@typedef InvoiceDTO請求ステータス 行を差し替え、末尾に 5 行追加する:

 * @property {string}      請求ステータス       - "未申請" | "申請中" | "承認済" | "差戻し" | "部分決済" | "決済完了" | "取消"
 * ...
 * @property {string}      申請者               - Session.getActiveUser().getEmail()
 * @property {Date}        申請日時             - new Date()
 * @property {string}      承認者               - 承認操作者のメールアドレス
 * @property {Date}        承認日時             - 承認または差戻しの実行時刻
 * @property {string}      差戻し理由           - 差戻し時のみ。承認時は空

toDto / toRow はヘッダー名ベースで動作するためコード変更は不要。

Step 2: 000_infra/002_constants.js — デフォルト値の変更

SHEET_DEFAULTS32_wrk_invoice エントリ内:

// 変更前
'請求ステータス': '未処理'
// 変更後
'請求ステータス': '未申請'

影響: 新規 INV 行(RPA 経由・手動起票)が「未申請」で始まる。既存レコードには触れない(マイグレーションは「人間が検討すべき事項」で別途検討)。RPA 関連ファイル(400_domain/401_rpa_hc.js, 402_rpa_subscription.js, 403_rpa_capex.js, 404_rpa_finance.js, 405_rpa_adhoc.js, 406_rpa_pipeline.js 内の '請求ステータス': '未処理' ハードコード)は 本案件の範囲では変更しないAction A/B エンジン側の '承認済' 文字列比較に依存しているため、後続マイグレーション案件で一括変換する)。

Step 3: 100_config/101_sys_config.js — メニューと DDL 追加

3a. onOpen() への追記(L307 .addToUi(); の直後)

  ui.createMenu('💳 支払依頼ワークフロー')
    .addItem('選択行を申請', 'applyForPayment')
    .addItem('選択行を承認', 'approvePayment')
    .addItem('選択行を差戻し', 'rejectPayment')
    .addToUi();

3b. setupAllSchemas()WRK_INVC 定義拡張(L654)

末尾に 5 列追加(決済日_実績 の後ろ):

'WRK_INVC': { headers: ["有効フラグ","請求ID(INV)","親発注ID(ORD)","起票日時","起票者","申請種別","発生日(P/L計上日)","決済日_計画","請求ステータス","収支区分","取引先名","PJ名","組織名","諸表区分","大分類","科目名","税区分","通貨","税抜金額_計画","消費税額_計画","税込金額_計画","未決済残高(自動計算)","決済手段","摘要","証憑URL","自動仕訳JNL_ID","決済日_実績","申請者","申請日時","承認者","承認日時","差戻し理由"], color: "#1155cc" },

3c. MST_DICT の 請求ステータス プルダウン拡張(L1034)

['請求ステータス', [['INV_DRF','未申請'], ['INV_REQ','申請中'], ['INV_NEW','未処理'], ['INV_APR','承認済'], ['INV_REJ','差戻し'], ['INV_PAR','部分決済'], ['INV_CMP','決済完了'], ['INV_CAN','取消']]],

Step 4: 400_domain/411_payment_workflow.js(新規作成)

公開関数 3 本(メニューから直接呼び出される GAS グローバル関数)と内部ヘルパー 3 本を定義する。

公開関数 (3)

関数ステータス遷移auditLog operation
applyForPayment()未申請申請中UPDATE
approvePayment()申請中承認済CONFIRM
rejectPayment()申請中差戻しCANCEL

内部ヘルパー (3)

  • getActiveInvoiceDto_() — 選択行の 請求ID(INV) を特定し、InvoiceRepository.findAll() から DTO を返す。複数行選択時は先頭行のみを対象とし Toast 通知する
  • getApproverEmails_()Constants.getParam('WORKFLOW_APPROVER_EMAIL', '') を読み取り、カンマ区切りを Array<string> に分解する。空なら []
  • sendWorkflowNotification_(dto, eventType, recipients, extra)MailApp.sendEmail で件名・本文を組み立てて通知送信。eventType'APPLIED' | 'APPROVED' | 'REJECTED'。クォータ超過時は Utils.logError で記録してスキップ(処理自体は継続)

データ読み書きパターン(必ず踏襲)

var result = InvoiceRepository.findAll(); // { headers, dtos }
var dto = result.dtos.find(function(d) { return d['請求ID(INV)'] === targetId; });
// dto を更新(プロパティ直接代入)...
dto['請求ステータス'] = '申請中';
dto['申請者'] = Session.getActiveUser().getEmail();
dto['申請日時'] = new Date();
InvoiceRepository.save(result.dtos);

対象行の INV ID は以下で取得:

var sheet = SpreadsheetApp.getActiveSheet();
if (sheet.getName() !== '32_wrk_invoice') {
  SpreadsheetApp.getUi().alert('32_wrk_invoice タブで実行してください。');
  return;
}
var rowNum = sheet.getActiveRange().getRow();
if (rowNum < 2) { /* ヘッダー行対策 */ return; }
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var idCol = headers.indexOf('請求ID(INV)');
var targetId = String(sheet.getRange(rowNum, idCol + 1).getValue()).trim();
if (!targetId) { SpreadsheetApp.getUi().alert('請求ID(INV) が空の行は処理できません。'); return; }

承認者メールアドレスの読み取り

// Constants.getParam() を使用する(Utils.getParam は存在しない)
var raw = Constants.getParam('WORKFLOW_APPROVER_EMAIL', '');
var approvers = String(raw).split(',').map(function(s) { return s.trim(); }).filter(Boolean);
if (approvers.length === 0) {
  SpreadsheetApp.getUi().alert('承認者が設定されていません。03_sys_params に WORKFLOW_APPROVER_EMAIL を登録してください。');
  return;
}

Utils.auditLog() の使用(引数順を厳守)

// 申請
Utils.auditLog('UPDATE',  '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 'applyForPayment', '未申請', '申請中', '');
// 承認
Utils.auditLog('CONFIRM', '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 'approvePayment', '申請中', '承認済', '');
// 差戻し
Utils.auditLog('CANCEL',  '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 'rejectPayment', '申請中', '差戻し', reason);

LockService による二重実行防止

var lock = LockService.getScriptLock();
try {
  lock.waitLock(30000); // タイムアウト時は例外
  var result = InvoiceRepository.findAll(); // ロック後に再読込
  var dto = result.dtos.find(function(d) { return d['請求ID(INV)'] === targetId; });
  if (!dto) {
    SpreadsheetApp.getUi().alert('対象の INV が見つかりません。画面を更新して再試行してください。');
    return;
  }
  if (dto['請求ステータス'] !== expectedStatus) {
    SpreadsheetApp.getUi().alert('ステータスが変更されています(現在: ' + dto['請求ステータス'] + ')。画面を更新して再試行してください。');
    return;
  }
  // ... 処理 ...
  InvoiceRepository.save(result.dtos);
} catch (e) {
  if (e.message && e.message.indexOf('waitLock') !== -1) {
    SpreadsheetApp.getUi().alert('他の処理が実行中です。しばらく待ってから再試行してください。');
  } else {
    Utils.logError('applyForPayment', e);
    throw e;
  }
} finally {
  try { lock.releaseLock(); } catch (_) {}
}

差戻し理由入力

var ui = SpreadsheetApp.getUi();
var response = ui.prompt('差戻し理由を入力してください', ui.ButtonSet.OK_CANCEL);
if (response.getSelectedButton() !== ui.Button.OK) {
  ui.alert('差戻し理由が未入力のため処理を中断しました。');
  return;
}
var reason = String(response.getResponseText() || '').trim();
if (!reason) {
  ui.alert('差戻し理由が未入力のため処理を中断しました。');
  return;
}

通知メール送信(MailApp.sendEmail

var url = SpreadsheetApp.getActiveSpreadsheet().getUrl();
var subject = '[支払依頼] ' + String(dto['契約・件名'] || dto['摘要'] || '(無題)');
var body =
  '申請者: ' + (dto['申請者'] || '') + '\n' +
  '件名: '  + (dto['契約・件名'] || dto['摘要'] || '') + '\n' +
  '金額(税込): ' + (dto['税込金額_計画'] || 0) + '\n' +
  '取引先: ' + (dto['取引先名'] || '') + '\n' +
  '請求ID: ' + dto['請求ID(INV)'] + '\n' +
  'URL: ' + url;
try {
  approvers.forEach(function(to) { MailApp.sendEmail(to, subject, body); });
} catch (mailErr) {
  // クォータ超過等はログのみ記録。申請処理そのものは成立させる
  Utils.logError('sendWorkflowNotification_', mailErr, 'approvers=' + approvers.join(','));
}

影響範囲

ファイル変更内容
ドメイン(新規)400_domain/411_payment_workflow.js新規作成。公開関数 3 + 内部ヘルパー 3
UI・トリガー100_config/101_sys_config.jsonOpen() にメニュー追加、setupAllSchemas()WRK_INVC DDL 拡張、MST_DICT プルダウン拡張
契約000_infra/003_contracts.jsInvoiceDTO@typedef に 5 フィールド追記 + ステータス値セット拡張
定数000_infra/002_constants.jsSHEET_DEFAULTS['32_wrk_invoice']請求ステータス 初期値を '未処理'→'未申請'
Repository200_data/202_repository.js変更なしfindAll / save のシグネチャ・ヘルパーを変更しない)
監査ログ000_infra/004_utils.js変更なしUtils.auditLog を既存シグネチャのまま利用)

注意事項

  • InvoiceRepository.findAll() / save() の戻り値形式・引数を 変更しないこと。202_repository.js の内部ヘルパー readSheetAsDtos_ / writeDtosToSheet_ にも手を入れない
  • 請求ステータス 値セットの変更('未処理'→'未申請''却下'→'差戻し')は既存データと非互換となる 破壊的変更。本案件のスコープでは 新規 INV のデフォルト値のみ変更し、既存レコードへのマイグレーションは別案件(800_ops に 809_migration_s32_status_rename.js 等で冪等実装)として「人間が検討すべき事項」に明記する
  • 33_wrk_bank消込処理・600_report/ のデータマート処理は 請求ステータス === '承認済' を依存条件としており(410_subledger_engine.js Action A など)、本案件では '承認済' 値を 温存する。新規追加する値(未申請 / 申請中 / 差戻し)は AP ワークフロー固有 のため、決済エンジン側は従来通り '未処理' / '承認済' を参照する。ただし、RPA 生成 INV のデフォルトが '未申請' になることで Action A の起動条件('承認済' 比較)を満たさなくなる可能性がある点は 要検証(Phase 1 Grep で '承認済' ハードコードが 410_subledger_engine.js 複数箇所と 900_test/901_test_runner.js にあることを確認済。本案件のスコープでは RPA 起票 INV は '未処理' のまま温存し、手動起票かつ「支払依頼」用途の INV のみ新ワークフローを適用する運用とする)
  • Session.getActiveUser().getEmail() は Workspace 組織内でないと空文字を返す場合がある。空の場合は 'SYSTEM' で代替せず、エラーダイアログで中断する(申請者の記録は監査上必須)
  • 03_sys_paramsWORKFLOW_APPROVER_EMAIL が未登録の場合は処理開始時にエラーダイアログを表示して中断する。Constants._paramsCache は起動時に 1 回だけ読み込むため、パラメータ追加後は GAS エディタで SpreadsheetApp.getActive() を再起動するか、Constants._paramsCache = null; でキャッシュクリアが必要
  • MailApp.sendEmail の日次クォータ(Workspace 無料枠 100 通/日、Workspace 有料 1500 通/日)を超えた場合、Exception: Service invoked too many times for one day: email. がスローされる。本案件では 通知失敗時も状態遷移は成立させるtry/catch で握りつぶして Utils.logError に記録)方針を採用する
  • メニューは onOpen() のトップレベルに追加するが、最終形ではサイドバー(templates/operations_sidebar.html)へ統合すべき。本案件のスコープ外として扱う
  • 複数行選択時は 先頭行のみ処理し、Toast で「先頭行のみ処理しました」と通知する(複数 INV 一括処理は多重 LockService 取得の原子性が担保できないため将来課題とする)

エッジケース

#操作条件動作
E01申請請求ステータス !== '未申請'「この依頼はすでに申請済みまたは処理済みです(現在: <ステータス>)」を表示して中断
E02申請税込金額_計画 <= 0Number() 化後)「金額が 0 以下のため申請できません」を表示して中断
E03申請取引先名 または 科目名 が空文字・空白のみ「必須項目(取引先名・科目名)が未入力です」を表示して中断
E04申請有効フラグ === false または 'FALSE'「無効行のため申請できません」を表示して中断
E05申請Session.getActiveUser().getEmail() が空文字「ユーザー情報が取得できません。GAS の認可状態を確認してください」を表示して中断
E06承認 / 差戻し請求ステータス !== '申請中'「この依頼は申請中ではありません(現在: <ステータス>)」を表示して中断
E07承認 / 差戻し操作者メールが WORKFLOW_APPROVER_EMAIL の配列に非含「承認権限がありません」を表示して中断
E08承認 / 差戻し操作者メール === 申請者 列の値(自己承認)「申請者自身は承認できません」を表示して中断
E09差戻しui.prompt() が Cancel / Close「差戻し理由が未入力のため処理を中断しました」を表示して中断
E10差戻しprompt() 入力が空文字 or 空白のみ「差戻し理由が未入力のため処理を中断しました」を表示して中断
E11共通LockService.waitLock(30000) タイムアウト「他の処理が実行中です。しばらく待ってから再試行してください」を表示して中断
E12共通WORKFLOW_APPROVER_EMAIL 未設定(Constants.getParam が空文字を返却)「承認者が設定されていません。03_sys_params に WORKFLOW_APPROVER_EMAIL を登録してください」を表示して中断
E13共通アクティブシートが 32_wrk_invoice でない「32_wrk_invoice タブで実行してください」を表示して中断
E14共通選択行がヘッダー行(rowNum < 2)「ヘッダー行は処理できません。データ行を選択してください」を表示して中断
E15共通選択行の 請求ID(INV) が空「請求ID(INV) が空の行は処理できません」を表示して中断
E16共通複数行選択時先頭行(選択範囲の最初の行)のみ処理し、完了後 ss.toast('先頭行のみ処理しました', '...') で通知
E17共通MailApp.sendEmail がクォータ超過で例外Utils.logError に記録しつつ状態遷移は完了させる。Toast で「通知メール送信に失敗しました(ログ参照)」を表示
E18共通ロック取得後の再読込で対象 INV が見つからない(削除・ID改変)「対象の INV が見つかりません。画面を更新して再試行してください」を表示して中断
E19共通setupAllSchemas() 未実行で 5 列が存在しないDTO プロパティに代入しても書き戻し時に無視されるため、事前に DDL を再実行しておく(「人間が検討すべき事項」7 参照)

実データ検証

以下は MCP / スプレッドシート直接確認 で実装前に確認すべき項目:

  • 32_wrk_invoice の実データ分布:
    • 請求ステータス = '未処理' の件数(マイグレーション規模の見積もり)
    • 請求ステータス = '却下' の件数(差戻しへのリネーム対象)
    • 請求ステータス = '承認済' の件数(決済エンジンの依存先、触らない対象)
  • 請求ステータス の値リネームが影響する箇所(Phase 1 Grep 確認済。本案件では変更しないが記録として残す):
    • 400_domain/410_subledger_engine.js L193, L303, L652 — '承認済' 文字列比較(Action A 起動条件)
    • 600_report/601_datamart_ingest.js L40 — データマート取込時のステータス参照
    • 600_report/606_datamart_daily_cf.js L227 — 日次 CF 予測
    • 200_data/201_data_validator.js L471 — データ整合性チェック
    • 900_test/901_test_runner.js L258, L329, L478 — テスト判定条件
    • 400_domain/401_rpa_hc.js / 402_rpa_subscription.js / 403_rpa_capex.js / 404_rpa_finance.js / 405_rpa_adhoc.js / 406_rpa_pipeline.js — RPA 生成 INV が '請求ステータス': '未処理' をハードコード
  • 03_sys_paramsWORKFLOW_APPROVER_EMAIL キーが未登録の場合、DDL 実行または手動追加で登録する
  • 03_sys_paramsWORKFLOW_AUTO_APPROVE_THRESHOLD キー(金額閾値自動承認、本案件では参照しない)を予約登録することを推奨

関連ドキュメント

人間が検討すべき事項

1. 既存レコードのマイグレーション方針(最優先)

請求ステータス = '未処理' の既存レコード数が多い場合、以下のいずれかを選択する必要がある:

選択肢メリットデメリット
a. 放置(新規 INV のみ '未申請' で始まる)既存コードへの影響なし'未処理''未申請' が混在して運用者が混乱
b. 一括リネーム800_ops/809_migration_s32_status_rename.js を新規作成)値セットが統一される410_subledger_engine.js'承認済' 比較や RPA 側の '未処理' ハードコードとの整合確認が必要
c. 決済サイクルと支払依頼で列を分離請求ステータス に加えて 支払依頼ステータス 列を新設)責務が分離され既存コードに影響なし列が増えて運用者の認知負荷が上がる

推奨: c(列分離)。ただし本案件では段階的に a(放置)から始め、運用で必要性が確認されたら c に移行する。

2. 本ワークフローの適用対象(申請種別 の範囲)

TODO_future.md には「経費や仕入の支払依頼」とあるが、具体的な 申請種別 の範囲が定められていない。以下を代表者と合意する:

  • 申請種別 = '請求書受領(AP)'(APL_AP): 対象(支払先からの請求書受領)
  • 申請種別 = '経費精算(社員立替)'(APL_EX): 対象(立替精算の社員申請)
  • 申請種別 = '月額給与・報酬支払'(APL_HC): 対象外(HC RPA で自動生成、承認不要)
  • 申請種別 = '財務仕訳(振替等)'(APL_JE): 対象外(振替のみ、承認不要)
  • 申請種別 = '自動引落'(APL_DD)/ '手動振込'(APL_TR): 要検討

現在の実装では 申請種別 でフィルタせずに全 INV を対象としている。申請種別 IN ('請求書受領(AP)','経費精算(社員立替)') でガードするかを決定する必要がある。

3. 自己承認の運用

代表 1 名体制では申請者 = 承認者になるケースが発生する。現仕様は E08 で禁止しているが、以下の選択肢がある:

  • a. 禁止(現仕様): 代表 1 名体制では運用不能になる
  • b. 警告のみ: ui.alert(..., ButtonSet.YES_NO) で確認を取って進める
  • c. WORKFLOW_SELF_APPROVAL_ALLOWED(Boolean パラメータ)で切替: 組織規模に応じて柔軟に変更可能

推奨: c03_sys_paramsWORKFLOW_SELF_APPROVAL_ALLOWED = true/false で制御する。

4. MailApp.sendEmail クォータ超過時の挙動

日次クォータ(無料 100 通 / 有料 1500 通)超過時、以下のいずれかを選ぶ:

  • a. 通知失敗でも状態遷移は成立させる(現仕様): 業務を止めない
  • b. 通知失敗時はロールバックして中断: 承認プロセスの完全性を優先
  • c. 通知キューを 03_sys_params に記録し、バッチで再送: 実装コスト高

推奨: a。ログに記録して運用者が後日確認できるようにする。

5. 将来の金額閾値自動承認(WORKFLOW_AUTO_APPROVE_THRESHOLD

現時点では参照しないが、将来 税込金額_計画 < 閾値 の場合は申請と同時に自動承認する仕様を検討する。キーだけ 03_sys_params に予約登録しておく:

WORKFLOW_AUTO_APPROVE_THRESHOLD = 100000  // 10万円以下は自動承認(将来実装)

6. 多段階承認への拡張(MAS-128 / MAS-125 との共通化)

本案件は「申請→承認」の 1 段階のみ。将来、部門承認→経営承認などの 2 段階が必要になった場合、以下の設計変更が必要:

  • 承認者 / 承認日時 / 差戻し理由承認者1 / 承認者2 / 承認日時1 / 承認日時2 に拡張
  • 状態を 申請中 → 一次承認済 → 最終承認済 に拡張
  • MAS-125(発注ステータス遷移ワークフロー)、MAS-128(発注承認ワークフロー)との共通基盤化

この段階で 400_domain/411_payment_workflow.js400_domain/411_workflow_engine.js に汎化(発注 / 請求で共通の状態遷移エンジン)を検討する。

7. DDL 再実行の必要性

本案件は setupAllSchemas()WRK_INVC ヘッダーに 5 列を追加するため、既存スプレッドシートで setupAllSchemas() を再実行しなければ新列が表示されない。dev 環境で実行 → 動作確認 → prod 環境で実行の順序を厳守する(CLAUDE.md の「動作未確認のコードを GitHub に push しない」原則)。

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

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-104「支払依頼ワークフロー」を実装してください。

## 実行前タスク(Read で実在コードを確認してから実装すること)
- `000_infra/003_contracts.js` を Read し、InvoiceDTO の全フィールド名と請求ステータス値セットを確認する
- `000_infra/002_constants.js` を Read し、SHEET_DEFAULTS の 32_wrk_invoice エントリの現在の形式を確認する
- `200_data/202_repository.js` を Read し、InvoiceRepository.findAll() の戻り値形式({ headers, dtos })と save() の引数を確認する
- `000_infra/004_utils.js` を Read し、Utils.auditLog() の引数順(8引数: operation, targetSheet, targetId, targetCol, funcName, beforeValue, afterValue, note)を確認する。Constants.getParam() は 002_constants.js 側で定義されている(Utils.getParam は存在しない)ことも確認する
- `100_config/101_sys_config.js` を Read し、onOpen() の既存メニュー末尾(L307 `.addToUi();`)の文字列と setupAllSchemas() 内の 32_wrk_invoice / WRK_INVC DDL 定義(L654)を確認する

## 修正対象ファイル
- 新規作成: `400_domain/411_payment_workflow.js`
- 変更: `100_config/101_sys_config.js`
  - onOpen() に「💳 支払依頼ワークフロー」メニューを追加(L307 `.addToUi();` の直後に新規 `ui.createMenu()` を追加)
  - setupAllSchemas() の WRK_INVC ヘッダー配列(L654)末尾に 5 列追加("申請者","申請日時","承認者","承認日時","差戻し理由")
  - MST_DICT の 請求ステータス プルダウン値(L1034)に 3 行追加(INV_DRF=未申請, INV_REQ=申請中, INV_REJ=差戻し)
- 変更: `000_infra/003_contracts.js`(InvoiceDTO の @typedef コメントに 5 フィールド追記 + 請求ステータス値セット拡張)
- 変更: `000_infra/002_constants.js`(SHEET_DEFAULTS の 32_wrk_invoice エントリの 請求ステータス デフォルト値を '未処理' → '未申請' に変更)

## 実装内容
### 411_payment_workflow.js の構成
- グローバル関数: applyForPayment(), approvePayment(), rejectPayment()(onOpen メニューから直接呼び出す)
- 各関数は LockService.getScriptLock().waitLock(30000) を try/finally でラップする
- ロック取得後に InvoiceRepository.findAll() で再読み込みし、対象 DTO のステータスをガード節で検証する
- 承認者メールは Constants.getParam('WORKFLOW_APPROVER_EMAIL', '') で取得(Utils.getParam ではない)
- 現在の操作者は Session.getActiveUser().getEmail() で取得する
- auditLog の operation 値: 申請='UPDATE'、承認='CONFIRM'、差戻し='CANCEL'
- auditLog 引数順: (operation, '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 関数名, 変更前値, 変更後値, note)
- 内部ヘルパー: getActiveInvoiceDto_(), getApproverEmails_(), sendWorkflowNotification_(dto, eventType, recipients, extra)

### applyForPayment() の骨格
```javascript
function applyForPayment() {
  var ui = SpreadsheetApp.getUi();
  var lock = LockService.getScriptLock();
  try {
    lock.waitLock(30000);
    var sheet = SpreadsheetApp.getActiveSheet();
    if (sheet.getName() !== '32_wrk_invoice') { ui.alert('32_wrk_invoice タブで実行してください。'); return; }
    var rowNum = sheet.getActiveRange().getRow();
    if (rowNum < 2) { ui.alert('ヘッダー行は処理できません。データ行を選択してください。'); return; }
    var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
    var idCol = headers.indexOf('請求ID(INV)');
    var targetId = String(sheet.getRange(rowNum, idCol + 1).getValue()).trim();
    if (!targetId) { ui.alert('請求ID(INV) が空の行は処理できません。'); return; }
    var applicant = Session.getActiveUser().getEmail();
    if (!applicant) { ui.alert('ユーザー情報が取得できません。GAS の認可状態を確認してください。'); return; }
    var approvers = getApproverEmails_();
    if (approvers.length === 0) { ui.alert('承認者が設定されていません。03_sys_params に WORKFLOW_APPROVER_EMAIL を登録してください。'); return; }

    var result = InvoiceRepository.findAll();
    var dto = result.dtos.find(function(d) { return d['請求ID(INV)'] === targetId; });
    if (!dto) { ui.alert('対象の INV が見つかりません。画面を更新して再試行してください。'); return; }
    if (dto['有効フラグ'] === false || String(dto['有効フラグ']).toUpperCase() === 'FALSE') { ui.alert('無効行のため申請できません。'); return; }
    if (dto['請求ステータス'] !== '未申請') { ui.alert('この依頼はすでに申請済みまたは処理済みです(現在: ' + dto['請求ステータス'] + ')'); return; }
    if (Number(dto['税込金額_計画']) <= 0) { ui.alert('金額が 0 以下のため申請できません。'); return; }
    if (!String(dto['取引先名'] || '').trim() || !String(dto['科目名'] || '').trim()) { ui.alert('必須項目(取引先名・科目名)が未入力です。'); return; }

    dto['請求ステータス'] = '申請中';
    dto['申請者']       = applicant;
    dto['申請日時']     = new Date();
    InvoiceRepository.save(result.dtos);
    Utils.auditLog('UPDATE', '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 'applyForPayment', '未申請', '申請中', '');
    sendWorkflowNotification_(dto, 'APPLIED', approvers, '');
    Utils.toastResult('applyForPayment', '申請を送信しました: ' + dto['請求ID(INV)']);
  } catch (e) {
    if (e && e.message && e.message.indexOf('waitLock') !== -1) {
      ui.alert('他の処理が実行中です。しばらく待ってから再試行してください。');
    } else {
      Utils.logError('applyForPayment', e);
      throw e;
    }
  } finally {
    try { lock.releaseLock(); } catch (_) {}
  }
}
```

approvePayment() と rejectPayment() も同じ骨格(タブ検証→ロック→再読込→ガード節→状態更新→save→auditLog→通知→Toast)で実装する。rejectPayment() は ui.prompt() で差戻し理由を取得し、Cancel または空入力で中断する(E09 / E10)。

### onOpen() へのメニュー追加(101_sys_config.js L307 `.addToUi();` の直後に挿入)
```javascript
  ui.createMenu('💳 支払依頼ワークフロー')
    .addItem('選択行を申請', 'applyForPayment')
    .addItem('選択行を承認', 'approvePayment')
    .addItem('選択行を差戻し', 'rejectPayment')
    .addToUi();
```

### setupAllSchemas() WRK_INVC ヘッダー拡張(L654)
既存 `"決済日_実績"` の直後に 5 列を追加:
```
"決済日_実績","申請者","申請日時","承認者","承認日時","差戻し理由"
```

### MST_DICT 請求ステータス プルダウン拡張(L1034)
```javascript
['請求ステータス', [['INV_DRF','未申請'], ['INV_REQ','申請中'], ['INV_NEW','未処理'], ['INV_APR','承認済'], ['INV_REJ','差戻し'], ['INV_PAR','部分決済'], ['INV_CMP','決済完了'], ['INV_CAN','取消']]],
```

### InvoiceDTO 型定義の更新(003_contracts.js L49)
```
 * @property {string}      請求ステータス       - "未申請" | "申請中" | "承認済" | "差戻し" | "部分決済" | "決済完了" | "取消"
```
末尾(L66 の `自動仕訳JNL_ID` の後)に 5 行追加:
```
 * @property {string}      申請者               - Session.getActiveUser().getEmail()
 * @property {Date}        申請日時             - new Date()
 * @property {string}      承認者               - 承認操作者のメールアドレス
 * @property {Date}        承認日時             - 承認または差戻しの実行時刻
 * @property {string}      差戻し理由           - 差戻し時のみ。承認時は空
```

### SHEET_DEFAULTS 変更(002_constants.js L85)
```javascript
// 変更前
{ pattern: '32_wrk_invoice', defaults: { '請求ステータス': '未処理', ... } },
// 変更後
{ pattern: '32_wrk_invoice', defaults: { '請求ステータス': '未申請', ... } },
```

## 制約
- InvoiceRepository.findAll() / save() のシグネチャを変更しないこと
- 202_repository.js 内の readSheetAsDtos_、writeDtosToSheet_ を変更しないこと
- 400_domain/401_rpa_hc.js 〜 406_rpa_pipeline.js 内の `'請求ステータス': '未処理'` ハードコードは **触らない**(RPA 生成 INV は従来通り `'未処理'` で起票、Action A の `'承認済'` 遷移を維持)
- 410_subledger_engine.js の `'承認済'` 文字列比較は **触らない**
- 推測でコードを書かず、必ず Read で確認してから実装すること

## エッジケース(仕様書のエッジケーステーブル E01-E19 と完全一致させること)
- 申請: ステータス≠'未申請'、金額≤0、必須項目空欄、有効フラグ=false、ユーザーメール空 → それぞれエラーメッセージを表示して処理中断
- 承認/差戻し: ステータス≠'申請中'、承認権限なし、申請者=操作者 → それぞれエラーメッセージを表示して処理中断
- 差戻し: prompt() がキャンセルまたは空入力 → 処理中断
- LockService タイムアウト: ユーザー通知して中断
- WORKFLOW_APPROVER_EMAIL 未設定: ユーザー通知して中断
- タブ検証: 32_wrk_invoice 以外で実行 → 中断
- 行検証: ヘッダー行選択、INV_ID 空、ロック後に INV 消失 → 中断
- MailApp クォータ超過: ログのみ記録、状態遷移は完了
- 複数行選択: 先頭行のみ処理、Toast で通知

## 動作確認
1. `npm run push:dev` を実行してデプロイする
2. 開発用スプレッドシートで `onOpen()` を手動再実行(タブ再読込)し、「💳 支払依頼ワークフロー」メニューが表示されることを確認
3. `setupAllSchemas()` を実行し、32_wrk_invoice に 申請者/申請日時/承認者/承認日時/差戻し理由 の 5 列が追加されたことを確認
4. `03_sys_params` に `WORKFLOW_APPROVER_EMAIL` を自分のメールアドレスで登録
5. 32_wrk_invoice で `請求ステータス='未申請'` の INV レコードを 1 行選択する(必要なら手動で新規追加)
6. メニュー「💳 支払依頼ワークフロー」→「選択行を申請」を実行し、ステータスが「申請中」に変わること、申請者・申請日時が記録されることを確認する
7. 別ユーザー(または WORKFLOW_APPROVER_EMAIL に別のアドレスを設定して申請者と分離)で「選択行を承認」を実行し、ステータスが「承認済」になり承認者・承認日時が記録されることを確認する
8. 申請者と同一ユーザーで承認操作し、「申請者自身は承認できません」エラーが表示されることを確認する(E08)
9. 別の申請中 INV で「選択行を差戻し」を実行し、prompt で理由を入力 → ステータスが「差戻し」、差戻し理由が記録されることを確認する
10. prompt でキャンセル / 空入力して E09 / E10 が発火することを確認
11. 98_audit_log を開き、UPDATE / CONFIRM / CANCEL のログが記録されていることを確認する
12. `npm run push:prod` は自分で実行せず、動作確認後にユーザーへ依頼する
13. prod 反映後、ユーザーに `setupAllSchemas()` の手動実行を依頼する(DDL 再実行なしでは新列が表示されない)

### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read・Grep) | あり | 挿入位置・引数順・値セットを確定させる |
| 実装(Write・Edit) | なし | 確定済み内容の書き下しに徹する |

推奨実行モデル

Step内容推奨モデル理由
実行前タスクRead 6 ファイル + GrepSonnet中程度の判断(挿入位置特定・既存パターン確認)
Step 1 (003_contracts.js)@typedef コメント追記のみHaiku定義が完全に確定、機械的編集
Step 2 (002_constants.js)デフォルト値の 1 箇所変更Haiku文字列 1 箇所の置換
Step 3 (101_sys_config.js)onOpen() + setupAllSchemas() + MST_DICT の 3 箇所編集Sonnet既存メニュー構造への追記、配列末尾への追加の判断が必要
Step 4 (411_payment_workflow.js)新規ファイル作成(公開 3 + ヘルパー 3)Sonnetエッジケース分岐が多い。ただし仕様書で骨格が確定済のため Opus は不要
動作確認dev デプロイ・UI 操作・監査ログ確認Sonnetユーザー対話、検証

変更履歴

日時変更内容
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 実行: 2-1(骨格)/2-2(概要〜注意事項)/2-3a(エッジケース〜人間検討事項)/2-3b(実装プロンプト〜変更履歴)/2-4(<details> プロンプト記録)に分割。1 回の Write/Edit は概ね 300 行以内。
  4. 各 Step で何を書くかを具体指示: Phase 2 実行時に設計判断を持ち込まない。Phase 1 で確定した内容のみ書き出す。

====================================================================== あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。 案件 MAS-104「支払依頼ワークフロー」の開発仕様書を作成してください。 作成後は docs/_config.jsonnav 配列の適切なセクションに必ず追記してください。


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

以下を全て Read/Grep で調査・確定してから Phase 2 に進む。推測で書かない。

  1. docs/_internal/TODO_future.md — MAS-104 の概要・期待効果・人間が検討すべき事項を取得する。
  2. 000_infra/003_contracts.jsInvoiceDTO の全フィールド名と現在の 請求ステータス 値セット("未処理" | "承認済" | "却下" であることを確認)を Read する。
  3. 000_infra/002_constants.jsSHEET_DEFAULTS32_wrk_invoice エントリ(現在の 請求ステータス デフォルト値は '未処理'prefix フィールドなし)と ID_PREFIX_MAP32_wrk_invoice エントリを Read する。
  4. 200_data/202_repository.jsInvoiceRepository.findAll() / save() の実装と、内部ヘルパー readSheetAsDtos_ / writeDtosToSheet_ のシグネチャを Read する。
  5. 000_infra/004_utils.jsUtils.auditLog() の引数順(operation, targetSheet, targetId, targetCol, funcName, beforeValue, afterValue, note)と Constants.getParam() ではなく Constants.getParam002_constants.js 側で定義されていることを確認する(Utils.getParam は存在しない)。
  6. 100_config/101_sys_config.jsonOpen() の既存メニュー構造(メニュー名・サブメニュー名の文字列を正確に Read する)と setupAllSchemas() 内の 32_wrk_invoice スキーマ定義(列リスト・請求ステータス プルダウン値)を Read する。
  7. docs/dev/ 配下の既存仕様書を 1 件 Read してフォーマットを把握する(dev_mas-075_expense_date_validation.md または dev_mas-094_boundary_month_selector.md を推奨)。

Phase 1 で確定すべき事項(確定前に Phase 2 に進まないこと)

  • 101_sys_config.jsonOpen() に追加するメニュー文字列の正確な形式
  • setupAllSchemas()32_wrk_invoice 定義への列追加の挿入位置(行番号)
  • 実在する 申請種別 の値セット('請求書受領(AP)' 等)から、本ワークフローの適用対象を確認
  • 400_domain/ ディレクトリ内の既存ファイル番号(410_subledger_engine.js, 420_project_profitability.js が確認済みのため、新規ファイルは 411_payment_workflow.js で可)

Phase 2: 仕様書の分割作成

出力先: docs/dev/dev_mas-104_payment_workflow.md(ファイル名の S は大文字) 1 回の tool_use は概ね 300 行以内。設計判断は Phase 1 確定済みのもののみ使用する。


Step 2-1: 骨格の作成(Write)

見出しのみ。本文は空で可。約 20 行。

セクション構成:

# S-32: 支払依頼ワークフロー
## 概要
## 目的
## 現在のコード
## 修正方針
## 影響範囲
## 注意事項
## エッジケース
## 実データ検証
## 関連ドキュメント
## 人間が検討すべき事項
## 実装プロンプト(Claude Code 用)
## 推奨実行モデル
## 変更履歴
## 仕様書作成プロンプト

Step 2-2: 概要〜注意事項の追記(Edit または Bash heredoc)

約 300 行。以下の内容を Phase 1 確定済みの情報で記述する。

概要テーブル: 案件ID=MAS-104、対象ファイル(400_domain/411_payment_workflow.js[新規]、100_config/101_sys_config.js000_infra/003_contracts.js000_infra/002_constants.js)を記載する。

修正方針(以下をアーキテクト指示として記述する):

  • 新規作成ファイル: 400_domain/411_payment_workflow.js
    • 公開関数: applyForPayment(), approvePayment(), rejectPayment()(メニューから直接呼び出し可能なグローバル関数として定義)
    • 内部ヘルパー: getActiveInvoiceDto_(), getApproverEmails_(), sendWorkflowNotification_()
  • 100_config/101_sys_config.js:
    • onOpen() に「💳 支払依頼ワークフロー」メニューを追加。サブメニュー:「選択行を申請」(applyForPayment)、「選択行を承認」(approvePayment)、「選択行を差戻し」(rejectPayment)。挿入位置は Phase 1 で Read した既存メニュー末尾に追記
    • setupAllSchemas()32_wrk_invoice DDL に列追加(申請者, 申請日時, 承認者, 承認日時, 差戻し理由)と 請求ステータス プルダウン値を拡張
  • 000_infra/003_contracts.js: InvoiceDTO@typedef コメントに 5 フィールドを追記(実装コードの変更ではなく JSDoc コメントの更新のみ)
  • 000_infra/002_constants.js: SHEET_DEFAULTS32_wrk_invoice エントリ内 '請求ステータス': '未処理''未申請' に変更

データ読み書きパターン(必ず踏襲すること):

// InvoiceRepository を用いた全件読み込み → メモリ上更新 → 全件書き戻し
var result = InvoiceRepository.findAll(); // { headers, dtos }
var dto = result.dtos.find(function(d) { return d['請求ID(INV)'] === targetId; });
// dto を更新 ...
InvoiceRepository.save(result.dtos);

対象行の INV ID は SpreadsheetApp.getActiveSheet().getActiveRange().getRow() で選択行番号を取得し、同行の INV ID 列(ヘッダー '請求ID(INV)')から読み取る。

承認者メールアドレスの読み取り:

// Constants.getParam() を使用する(Utils.getParam は存在しない)
var raw = Constants.getParam('WORKFLOW_APPROVER_EMAIL', '');
var approvers = raw.split(',').map(function(s) { return s.trim(); }).filter(Boolean);

Utils.auditLog() の使用(引数順を厳守すること):

  • 申請: Utils.auditLog('UPDATE', '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 'applyForPayment', '未申請', '申請中', '')
  • 承認: Utils.auditLog('CONFIRM', '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 'approvePayment', '申請中', '承認済', '')
  • 差戻し: Utils.auditLog('CANCEL', '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 'rejectPayment', '申請中', '差戻し', reason)

LockService による二重実行防止:

var lock = LockService.getScriptLock();
try {
  lock.waitLock(30000); // タイムアウト時は例外をスロー → catch でユーザー通知
  var result = InvoiceRepository.findAll(); // ロック後に再読み込みして最新状態を検証
  var dto = result.dtos.find(...);
  if (!dto || dto['請求ステータス'] !== expectedStatus) {
    SpreadsheetApp.getUi().alert('ステータスが変更されています。画面を更新して再試行してください。');
    return;
  }
  // ... 処理 ...
} catch (e) {
  if (e.message && e.message.includes('waitLock')) {
    SpreadsheetApp.getUi().alert('他の処理が実行中です。しばらく待ってから再試行してください。');
  } else {
    throw e;
  }
} finally {
  lock.releaseLock();
}

差戻し理由入力:

var response = SpreadsheetApp.getUi().prompt('差戻し理由を入力してください');
if (response.getSelectedButton() !== SpreadsheetApp.getUi().Button.OK || !response.getResponseText().trim()) {
  SpreadsheetApp.getUi().alert('差戻し理由が未入力のため処理を中断しました。');
  return;
}
var reason = response.getResponseText().trim();

通知メール送信MailApp.sendEmail を使用):

var url = SpreadsheetApp.getActiveSpreadsheet().getUrl();
var body = '申請者: ' + applicant + '\n件名: ' + dto['契約・件名'] + '\n金額(税込): ' + dto['税込金額_計画'] + '\n取引先: ' + dto['取引先名'] + '\nURL: ' + url;
approvers.forEach(function(to) { MailApp.sendEmail(to, '[支払依頼] ' + dto['契約・件名'], body); });

注意事項:

  • InvoiceRepository.findAll() / save() のシグネチャ・内部ヘルパー(readSheetAsDtos_, writeDtosToSheet_)を変更しないこと
  • 請求ステータス 値セットの変更('未処理'→'未申請', '却下'→'差戻し')は既存データとの非互換が生じる破壊的変更。「人間が検討すべき事項」で必ず言及すること
  • 33_wrk_bank の消込処理や 600_report/ のマート処理が 請求ステータス 値を参照していないか、Phase 1 で Grep 確認すること(未確認なら「要調査」と明記)

Step 2-3a: エッジケース〜人間が検討すべき事項の追記(Edit または Bash)

約 200 行。

エッジケーステーブル:

操作条件動作
申請請求ステータス !== '未申請'「この依頼はすでに申請済みまたは処理済みです」を表示して中断
申請税込金額_計画 <= 0「金額が0以下のため申請できません」を表示して中断
申請取引先名 または 科目名 が空「必須項目(取引先名・科目名)が未入力です」を表示して中断
申請有効フラグ === false「無効行のため申請できません」を表示して中断
承認/差戻し請求ステータス !== '申請中'「この依頼は申請中ではありません」を表示して中断
承認/差戻し操作者メールが WORKFLOW_APPROVER_EMAIL に非含「承認権限がありません」を表示して中断
承認/差戻し操作者メール === 申請者 列の値「申請者自身は承認できません」を表示して中断
差戻しprompt() 入力が空またはキャンセル「差戻し理由が未入力のため処理を中断しました」を表示して中断
共通LockService.waitLock タイムアウト「他の処理が実行中です。しばらく待ってから再試行してください」を表示して中断
共通WORKFLOW_APPROVER_EMAIL 未設定(空文字)「承認者が設定されていません。03_sys_params に WORKFLOW_APPROVER_EMAIL を登録してください」を表示して中断
共通複数行選択時先頭行(選択範囲の最初の行)のみ処理し、完了後にその旨を Toast 通知する(仕様書で明記)

実データ検証(MCP で確認が必要な項目):

  • 32_wrk_invoice の実データに '未処理' / '却下' が格納されているレコード件数を確認し、マイグレーション規模を見積もる
  • 33_wrk_bank600_report/請求ステータス の値を文字列比較している箇所を Grep し、影響範囲を確定する
  • 03_sys_paramsWORKFLOW_APPROVER_EMAIL キーが未登録の場合、DDL 実行または手動追加が必要

人間が検討すべき事項:

  • 請求ステータス 値セット変更('未処理'→'未申請', '却下'→'差戻し')と既存レコードのマイグレーション方針(800_ops/ にマイグレーションスクリプトを追加するか)
  • 本ワークフローが適用される 申請種別 の範囲('請求書受領(AP)' のみか全申請種別か)— TODO_future.md に記載がない場合は即実装不可、要確認
  • MailApp.sendEmail の日次送信クォータ(100通/日)超過時の挙動(申請を失敗させるか、メール送信エラーを無視して申請は成功させるか)
  • WORKFLOW_AUTO_APPROVE_THRESHOLD キーを 03_sys_params に登録しておく(将来の金額閾値自動承認に備える)が、現実装では参照しない
  • 承認フローが 1 段階(申請 → 承認)で足りるか、多段階承認(例: 部門承認 → 経営承認)が将来必要か

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

約 250 行。実装プロンプトはバッククォートで囲まず、行頭 4 スペースインデントで出力すること。

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-104「支払依頼ワークフロー」を実装してください。

## 実行前タスク(Read で実在コードを確認してから実装すること)
- `000_infra/003_contracts.js` を Read し、InvoiceDTO の全フィールド名と請求ステータス値セットを確認する
- `000_infra/002_constants.js` を Read し、SHEET_DEFAULTS の 32_wrk_invoice エントリの現在の形式を確認する
- `200_data/202_repository.js` を Read し、InvoiceRepository.findAll() の戻り値形式({ headers, dtos })と save() の引数を確認する
- `000_infra/004_utils.js` を Read し、Utils.auditLog() の引数順(8引数)を確認する。Constants.getParam() は 002_constants.js 側で定義されていることも確認する
- `100_config/101_sys_config.js` を Read し、onOpen() の既存メニュー末尾の文字列と setupAllSchemas() 内の 32_wrk_invoice DDL 定義を確認する

## 修正対象ファイル
- 新規作成: `400_domain/411_payment_workflow.js`
- 変更: `100_config/101_sys_config.js`(onOpen() にメニュー追加、setupAllSchemas() に列追加)
- 変更: `000_infra/003_contracts.js`(InvoiceDTO の @typedef コメントに 5 フィールド追記)
- 変更: `000_infra/002_constants.js`(SHEET_DEFAULTS の 32_wrk_invoice エントリの請求ステータスデフォルト値を '未処理' → '未申請' に変更)

## 実装内容
### 411_payment_workflow.js の構成
- グローバル関数: applyForPayment(), approvePayment(), rejectPayment()(onOpen メニューから直接呼び出す)
- 各関数は LockService.getScriptLock().waitLock(30000) を try/finally でラップする
- ロック取得後に InvoiceRepository.findAll() で再読み込みし、対象 DTO のステータスをガード節で検証する
- 承認者メールは Constants.getParam('WORKFLOW_APPROVER_EMAIL', '') で取得(Utils.getParam ではない)
- 現在の操作者は Session.getActiveUser().getEmail() で取得する
- auditLog の operation 値: 申請='UPDATE'、承認='CONFIRM'、差戻し='CANCEL'
- auditLog 引数順: (operation, '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 関数名, 変更前値, 変更後値, '')

### onOpen() へのメニュー追加
- Read で確認した既存メニュー末尾に追記する
- メニュー名: '💳 支払依頼ワークフロー'
- サブメニュー: '選択行を申請' → applyForPayment、'選択行を承認' → approvePayment、'選択行を差戻し' → rejectPayment

## 制約
- InvoiceRepository.findAll() / save() のシグネチャを変更しないこと
- 202_repository.js 内の readSheetAsDtos_、writeDtosToSheet_ を変更しないこと
- 推測でコードを書かず、必ず Read で確認してから実装すること

## エッジケース(仕様書のエッジケーステーブルと完全一致させること)
- 申請: ステータス≠'未申請'、金額≤0、必須項目空欄、有効フラグ=false → それぞれエラーメッセージを表示して処理中断
- 承認/差戻し: ステータス≠'申請中'、承認権限なし、申請者=操作者 → それぞれエラーメッセージを表示して処理中断
- 差戻し: prompt() がキャンセルまたは空入力 → 処理中断
- LockService タイムアウト: ユーザー通知して中断
- WORKFLOW_APPROVER_EMAIL 未設定: ユーザー通知して中断

## 動作確認
1. npm run push:dev を実行してデプロイする
2. 開発用スプレッドシートの 32_wrk_invoice タブでステータス='未申請'の INV レコードを 1 行選択する
3. メニュー「💳 支払依頼ワークフロー」→「選択行を申請」を実行し、ステータスが「申請中」に変わることを確認する
4. 承認者メールアドレスのユーザー(または同一ユーザーで WORKFLOW_APPROVER_EMAIL に自分のアドレスを設定)で「選択行を承認」を実行し、ステータスが「承認済」になることを確認する
5. 申請者と同一ユーザーで承認操作し、「申請者自身は承認できません」エラーが表示されることを確認する
6. 98_audit_log を開き、CONFIRM / CANCEL のログが記録されていることを確認する
7. npm run push:prod は自分で実行せず、動作確認後にユーザーへ依頼する

### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read・Grep) | あり | 挿入位置・引数順・値セットを確定させる |
| 実装(Write・Edit) | なし | 確定済み内容の書き下しに徹する |

変更履歴テーブル: | YYYY-MM-DD | 初版作成 | を記載する。


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

末尾に以下の形式で追記する:

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

(この <instruction> 全文をそのまま貼り付ける)

</details>

Phase 3: _config.json への追記とコミット

  1. docs/_config.jsonnav 配列に追記する。セクションは案件の性質(新機能・ワークフロー)に応じて §E.2(バリデーション拡張)または §E.6(ワークフロー・RPA)から選択する:
    { "file": "dev/dev_mas-104_payment_workflow.md", "title": "E.X.X S-32 支払依頼ワークフロー" }
    
  2. docs/_internal/changelog.md の先頭(ヘッダー直後)に追記する:
    | YYYY-MM-DD | [dev_mas-104_payment_workflow.md](dev_mas-104_payment_workflow.md) | 初版作成。支払依頼ワークフロー仕様書 |
    
  3. コミット&プッシュ:
    git add docs/dev/dev_mas-104_payment_workflow.md docs/_config.json docs/_internal/changelog.md
    git commit -m "docs: S-32 支払依頼ワークフローの開発仕様書を作成"
    git push -u origin {現在のブランチ名}