MAS-364: Cockpit 受取領収書 PDF インポートタブ(月次財務諸表の左に新規タブ追加)
概要
| 項目 | 内容 |
|---|---|
| 案件 ID | MAS-364 |
| 案件名 | Cockpit 受取領収書 PDF インポートタブ(月次財務諸表の左に新規タブ追加) |
| カテゴリ | UI / SPA(Cockpit 拡張)× 外部連携(Drive + Vertex AI / Document AI) |
| Phase | P2 |
| 優先度 | ★★ |
| 所要時間 | 4-6 時間(SPA + GAS 5 ファイル横断) |
| 新規ファイル | webapp_client/src/cockpit/ReceiptImportPanel.tsx |
| 変更ファイル | webapp_client/src/CockpitApp.tsx(タブ定義配列・switch 拡張)/webapp_client/src/cockpit-main.tsx(import 副作用が必要な場合のみ)/300_ui/302_spa_bridge.js(handleReceiptAction(actionType, payload) 新設)/500_import/502_receipt_reader.js(OCR・マッチング private helper 追加)/100_config/101_sys_config.js(WRK_RCPT スキーマは既存・DDL 変更不要見込) |
| 前提案件 | MAS-232(SPA 基盤・実装済)/MAS-180(AI-OCR 連携 / 36_wrk_ocr_queue 仕様)/MAS-216(Vertex AI 移行)/MAS-147(請求書 PDF→INV 自動起票)/MAS-358(Cockpit ナビ統合) |
| 関連案件 | MAS-161(OCR 手動補正アシスト)/MAS-175(OCR 失敗 PDF 手動取込)/MAS-162(銀行 CSV 合算マッチ)/MAS-171(クレカ N:1 合算マッチ) |
| 実装ステータス | 未着手 |
本仕様書は docs/_internal/todo_master_tables.md 登録済みの MAS-364 の実装仕様書である(コミット ec15ba7 で未着手エントリ登録)。
目的
Cockpit Web App (?view=cockpit_spa) に 受取領収書 PDF インポートタブ を新設し、現在は GAS スプレッドシート側メニューからしか起動できない importReceiptPdfs()(500_import/502_receipt_reader.js L22-193)を SPA から起動・結果可視化・人間承認 までワンストップで完結させる。
- 現状の課題: 領収書 PDF 取込は Sheets メニュー(
📋 サイドバー: 📒 経理業務 (RPA / Action)配下)からの起動のみで、結果は35_wrk_receiptシートを直接覗かないと分からない。OCR 失敗・金額異常・消込候補の有無を確認するには UI を行き来する必要があり、月次締めの導線として非効率。 - 本案件のゴール: Cockpit 内に「📄 受取領収書」タブを追加し、(1) インポート起動 → (2) OCR 結果一覧表示 → (3) INV / STL 消込候補プレビュー → (4) 人間承認 → (5) 消込確定 までを Human-in-the-Loop(HitL) で実行できる UI を提供する。MAS-180(AI-OCR 連携 /
36_wrk_ocr_queue)と統合する余地は残しつつ、本案件単独で完結する第一弾実装とする。 - 設計原則の遵守:
35_wrk_receiptへの書込みは既存WRK_RCPTDDL(25 列)を踏襲し、確認 FLG = FALSE で一時保存→人間承認後に消込確定するフローを必須とする。OCR 結果のみで自動消込しない(プロダクトポリシー: 監査要件・科目マスタ未登録時のエラー化・キーワード推測禁止)。 - 将来発展の設計方針: 本案件の
receipt_importタブは将来「データ取り込み汎用画面」に発展させる予定。PDF インポートにとどまらず、CSV 取込・API 連携・銀行明細取得など複数のデータソースを統一 UI で処理する画面となる。各 Cockpit 画面は GAS 操作パネル(廃止方針)の代替として画面固有のサイドバー(仕訳実行・消込・マート更新等のオペレーションメニュー)を持つ 2 層ナビ設計(左ナビ: MAS-358 / 画面固有サイドバー)に移行する。本案件はその第一歩であり、拡張を妨げないコンポーネント設計を意識すること。
現在のコード
Cockpit タブ定義(webapp_client/src/CockpitApp.tsx L74-76, L155-189)
現在の Cockpit は 2 タブ構成。state は cockpitView: 'simulator' | 'pl_monthly'(L76)で管理し、ヘッダー <nav className="cockpit-view-tabs">(L159-172)に 📊 月次財務諸表(pl_monthly)と 🎛️ シミュレーター(simulator)の 2 ボタンが並ぶ。pl_monthly ビューは L184-190 で <FinancialStatementsView /> を常時マウント、display: none で切替する設計(再マウントによる状態消失を防ぐ)。
重要: webapp_client/src/cockpit-main.tsx はエントリポイントで <CockpitApp /> を ReactDOM.createRoot(...).render() するだけ。タブ定義配列・switch は CockpitApp.tsx 側に存在するため、新規タブの挿入は CockpitApp.tsx を編集する(Gemini 由来の入力プロンプトに「cockpit-main.tsx を編集」とあるが、実際の編集対象は CockpitApp.tsx)。
SPA ブリッジ既存パターン(300_ui/302_spa_bridge.js)
google.script.run 経由で呼ばれる public 関数は 末尾 _ を付けない 命名規約。代表例:
| 関数 | 行番号 | 戻り値パターン |
|---|---|---|
getInitialStateForSpa(view) | L97-225 | 必ずオブジェクトを返し throw しない(クライアントで state.error 判定) |
runMultiyearSimulation(input) | L494-542 | { success: boolean, result?, error? } |
generateF67Suggest(payload) | L663-736 | 同上 |
_scrubInfinityForJSON_(value) | L1364-1389 | private。Infinity → MAX_SAFE_INTEGER、NaN → null でサニタイズ(V8 silent null 回避 / failure_patterns #29) |
handleSpaDoGet_(e) | L1402-1436 | _spa view ルーティング。view allowlist は _sanitizeSpaView_(L1458-1465)で hitl / cockpit / multiyear のみ許可 |
OCR / Drive / 領収書系の public エンドポイントは現時点で未実装。本案件で handleReceiptAction(actionType, payload) を新設する。
既存 OCR パイプライン(500_import/502_receipt_reader.js)
| 関数 | 行番号 | 役割 |
|---|---|---|
importReceiptPdfs() | L22-193 | メニュー起点。Env.receiptFolderId()(Script Property RECEIPT_FOLDER_ID)で Drive フォルダ ID を取得し、PDF を Gemini で抽出 → 35_wrk_receipt に書込み・processed サブフォルダへ移動 |
callGeminiForReceipt_(apiKey, base64, fileName) | L235-317 | Google AI Studio 直叩き。Env.gcpProjectId() が設定済なら Vertex 経由へフォールスルー |
callGeminiForReceiptOnVertex_(base64, fileName) | L457-562 | Vertex AI 経由。asia-northeast1 → us-east5 リージョンフォールバック + 429/503 Exponential Backoff(最大 4 回・MAS-216 準拠) |
parseGeminiReceiptText_(text) | L319-369 | JSON 抽出ヘルパー |
postProcessReceiptData_(rcpSheet) | L371-455 | 同一取引先の T 番号・住所多数決補正 |
汎用 OCR ラッパー(000_infra/004_utils.js)も併存:
callDocumentAiInvoiceParser_(base64Pdf, mimeType, options)L224-277 — Document AI Invoice Parser 経由(entities配列で構造化抽出)callGeminiForReasoningOnVertex_(prompt, options)L132- — 汎用テキスト推論
本案件で どちらを使うかは設計判断。既存 502_receipt_reader.js の callGeminiForReceiptOnVertex_ が領収書専用の安定動線で、追加導入コストが最小。Document AI を採用する場合は MAS-180 と並走するため設計を MAS-180 と調整する必要がある。
35_wrk_receipt DDL(100_config/101_sys_config.js L1066)
WRK_RCPT スキーマ。25 列:
管理ID, 処理日時, 証憑種別, 取引先名, 🏢住所, 税込金額_決済, 税抜金額_決済,
消費税額_決済, 源泉税額, T番号, 帳票番号, 発行日, 発生日(P/L計上日),
決済日_実績, 決済手段, 摘要, ファイル名, 証跡リンク,
確認FLG, 処理結果, マッチ決済ID(STL), STL決済日_計画, STL税込金額_決済,
STL取引先名, STL摘要
- ID prefix:
RCP_NNNN(importReceiptPdfsL92-100 で最大連番を取得) - フォーマット: B 列
yyyy-mm-dd hh:mm、L〜N 列・V 列yyyy-mm-dd(L1410-1412) - 処理結果ドロップダウン:
WRK_RCPT 20 列目 → 35_wrk_receipt(L1656) - 色:
#38761d(緑系 / WRK_* 共通) - 本案件で DDL 変更不要(必要な列は既に揃っている)
Repository 戻り値構造(200_data/202_repository.js)
| Repository | 行番号 | findAll() 戻り値 |
|---|---|---|
InvoiceRepository | L152-208 | { headers: string[], dtos: InvoiceDTO[] }(readSheetAsDtos_ 経由 / 内部で Utils.getSheetByKey('WRK_INVC', '32_wrk_invoice')) |
BankTxRepository | L214-253 | { headers: string[], dtos: BankTxDTO[] }(Utils.getSheetByKey('WRK_BANK', '33_wrk_bank')) |
DTO 構造は 000_infra/003_contracts.js(Contracts.toDto / Contracts.toRow)参照。本案件のマッチングロジックでは dtos 配列のみ消費し、headers は参照不要(DTO は object 化済)。
appsscript.json の oauthScopes
L3-13 に既に存在(合計 9 スコープ)。https://www.googleapis.com/auth/drive(L8)・https://www.googleapis.com/auth/cloud-platform(L12)が含まれており、本案件で oauthScopes を編集する必要はない(failure_patterns #26 回避)。
修正方針
Step 1: SPA タブ追加(CockpitApp.tsx)
cockpitViewの型を'simulator' | 'pl_monthly' | 'receipt_import'に拡張(L76)。<nav className="cockpit-view-tabs">(L159-172)の📊 月次財務諸表ボタンの 直前 に📄 受取領収書ボタンを挿入する。これにより視覚的に「📄 受取領収書 → 📊 月次財務諸表 → 🎛️ シミュレーター」の順となり、月次締めの自然な操作導線(領収書取込 → 月次財務諸表確認)と一致する。- ビュー切替の switch / 条件分岐(L184-211 付近)に
receipt_importケースを追加し、<ErrorBoundary label="ReceiptImportPanel"><ReceiptImportPanel /></ErrorBoundary>をマウントする。 - ファイル先頭の import 文に
import { ReceiptImportPanel } from './cockpit/ReceiptImportPanel';を追記する(この import を忘れると bundle に含まれずタブが空白になる / failure_patterns #33)。
Step 2: SPA コンポーネント新設(webapp_client/src/cockpit/ReceiptImportPanel.tsx)
google.script.run.withSuccessHandler(fn).withFailureHandler(fn).handleReceiptAction(actionType, payload)で GAS ブリッジを呼ぶ(callApihelper 経由でも可)。withSuccessHandlerの引数がnullの場合は「サーバーエラー: GAS 側で Infinity/NaN が返された可能性があります(scrubInfinityForJSON で検証してください)」と表示する(failure_patterns #29)。- 表示項目: PDF ファイル名 / 抽出日付(発生日)/ 抽出金額(税込)/ ステータス(消込済 / OCR確認要 / 手動確認要 / OCR失敗)/ 消込候補 INV ID(複数の場合はカンマ区切り)/ 操作ボタン(
再 OCR/再マッチ/承認・消込確定)。 - TDZ 違反防止:
const arrow ヘルパー(const formatAmount = (v) => ...等)はuseMemo/useCallback呼び出しより前で宣言する(failure_patterns #32)。
Step 3: GAS ブリッジ統合エンドポイント(300_ui/302_spa_bridge.js)
function handleReceiptAction(actionType, payload)を新設する。末尾_は付けない(google.script.run 不可達 / failure_patterns #28)。actionTypeの取りうる値:'import'(未処理 PDF の ID・名前リストを返すのみ)/'process_single'(payload.fileId1 件の OCR・マッチング)/'rematch'(候補再検索)/'reocr'(OCR 再実行)/'approve'(消込確定)。'import'アクションは OCR を実行しない。Drive フォルダから未処理 PDF の[{ fileId, fileName }]リストを返すのみ。OCR は SPA 側が 1 件ずつ'process_single'を呼び出す(チャンク処理・GAS 6 分制限対策)。- 戻り値は
{ success: boolean, data: any, error?: string }で統一。戻す前に_scrubInfinityForJSON_(result)を必ず通す(failure_patterns #29)。 - 内部処理は末尾
_付きの private helper(例:_listUnprocessedReceipts_()/_processReceiptSingle_(fileId)/_rematchReceipt_(receiptId)/_approveReceipt_(receiptId, stlIds))に委譲する(責務分離・テスト容易性)。
Step 4: OCR・マッチング private helper(500_import/502_receipt_reader.js)
- 既存
importReceiptPdfs()の内部実装は変更しない(後方互換性維持・Sheets メニューからの呼出維持)。末尾に新規関数群を追加する。 - Drive フォルダ ID は
Constants.getParam('RECEIPT_FOLDER_ID', '')で取得する(task 指示書準拠。ただし既存実装はEnv.receiptFolderId()経由で Script Property を読む。詳細は「注意事項」§ パラメータ取得経路の二重化 を参照)。 - PDF 抽出は
callGeminiForReceiptOnVertex_(base64Pdf, fileName)(502_receipt_reader.js L457)を使用する。既存の安定した領収書 OCR 資産を再利用し、DocAI 追加導入コストを回避する。 - 日付正規化:
Utils.parseDateToYm(rawDate)(004_utils.js L354)。 - 金額パース:
Utils.parseAmt(rawAmt)(004_utils.js L453)。 - 取引先名ファジーマッチ:
Utils.calcJaccard(a, b)(004_utils.js L691)、閾値 0.4 以上を候補とする。 - 金額合算マッチ:
Utils.findSubsetSum(invAmounts, receiptAmt, { tolerance: 10 })(004_utils.js L725)でインデックス配列を取得(MAS-162 / MAS-171 と対称)。 - LockService のスコープを最小化: OCR API 呼び出しはロック外で実行し、
LockService.getScriptLock().tryLock(30000)は35_wrk_receiptへの書き込み直前のみ取得・書込後に即releaseLock()(並行実行競合対策)。 35_wrk_receiptへの複数列書込みはsheet.getRange(row, colStart, 1, n).setValues([[...]])で 1 API call に原子化する(フィルター適用中の連続setValueは silent-fail / failure_patterns #35)。- 書込み前に ファイル ID 列(
証跡リンクの URL 末尾/file/d/{ID}/viewか、ファイル名)で既存行検索 → ヒット時は OCR・書込みをスキップ(冪等性)。 - 全書込・消込操作後に
Utils.auditLog('CREATE', '35_wrk_receipt', fileId, '', 'FUNC名', '', dto, '')(004_utils.js L539)を呼ぶ。
Step 5: マッチングアルゴリズム(合算マッチ含む)
- 単一一致:
InvoiceRepository.findAll().dtosから有効フラグ === trueかつ取引先名 Jaccard ≥ 0.4かつUtils.parseAmt(税込金額) === receiptAmt(許容差 ±10 円)の INV を探索。 - 合算一致: 同一取引先の複数 INV について
Utils.findSubsetSum(invAmounts, receiptAmt, { tolerance: 10 })を実行。返却されたインデックス配列(複数 INV のサブセット)を候補として SPA へ返す。 - 重要: 単一マッチ前提のロジック分岐(
stlIds.length === 1等)で差額記録・Utils.auditLogを分岐させない。初期実装からstlIds.length >= 1で汎用処理する(failure_patterns #36 — 合算マッチ拡張時に副次データが落ちる事故防止)。 - マッチ確定(
approveアクション)時にmatched = trueフラグで二重消費を防止(既消費 INV は次回findAll結果から除外)。
影響範囲
| ファイル | 変更タイプ | 影響 |
|---|---|---|
webapp_client/src/CockpitApp.tsx | 編集 | cockpitView の型拡張・<nav> への新ボタン挿入・switch 拡張・import 追記。SPA bundle に直接影響(変更後は npm --prefix webapp_client run build が必要) |
webapp_client/src/cockpit-main.tsx | 変更なし(見込み) | エントリのみ。タブ定義は CockpitApp.tsx 側のため、本案件では編集不要 |
webapp_client/src/cockpit/ReceiptImportPanel.tsx | 新規 | 単一コンポーネント・約 250-400 行想定 |
300_ui/302_spa_bridge.js | 追記 | handleReceiptAction + 4 つの private helper を末尾に追記(既存関数は不変) |
500_import/502_receipt_reader.js | 追記 | 末尾に新規関数群(OCR・マッチング・書込み)。既存 importReceiptPdfs 等は不変 |
100_config/101_sys_config.js | 変更なし | WRK_RCPT は既定義・25 列完備(DDL 変更不要) |
appsscript.json | 変更なし | drive / cloud-platform scope は既存(failure_patterns #26 回避) |
並行稼働への影響: MAS-232 SPA 移行期のため、本案件の SPA エンドポイントは既存 importReceiptPdfs()(Sheets メニュー起点)と完全に独立させる。両者は同じ 35_wrk_receipt シートを書込むが、SPA 側は確認 FLG = FALSE での一時保存に統一、Sheets メニュー側は従来通り書込み後に processed フォルダへ移動する動作を維持する。消込確定(matched=true 設定)は SPA 側のみで実行する(Sheets メニュー側からは消込しない既存挙動を変えない)。
注意事項
必読 failure_patterns(仕様書本体・実装プロンプト両方で警告すること)
- #20 命名造語禁止: 関数名は推測で書かず、既存実装(
Env.receiptFolderId/Utils.calcJaccard等)を Read で確認してから引用する。 - #25 並列実装の対称性漏れ: 銀行 CSV(
502_bank_importer.js)・クレカ(501_cc_importer.js)の消込ロジックと対称性を確認する。特に「全分岐で確認 FLG = FALSE が明示的にセットされること」「matched=trueで二重消費防止」の 2 点。 - #26
oauthScopes部分宣言禁止:appsscript.jsonのoauthScopesフィールドを編集しない(既存 9 スコープに必要なものは揃っている)。Document AI / Vertex AI / Drive の追加スコープもcloud-platformでカバー済。 - #28 SPA エンドポイント末尾
_禁止:handleReceiptActionは末尾_を付けない。private helper(_importReceipts_等)には逆に必ず_を付ける。 - #29 V8 シリアライズ silent null: 戻り値に
Infinity/NaN/ 循環参照が含まれるとgoogle.script.runのwithSuccessHandlerがnullを受信する。_scrubInfinityForJSON_を必ず通す。SPA 側でresult === nullを異常として明示表示する。 - #32 TDZ 違反:
const arrow ヘルパーはuseMemo/useCallbackより前で宣言する。 - #33 SPA コンポーネント import 漏れ: 新規
ReceiptImportPanel.tsxはCockpitApp.tsxに import 文を追記しないと bundle から落ちる。 - #35 フィルター適用中の
setValuesilent-fail:35_wrk_receiptへの書込みはsheet.getRange(row, colStart, 1, n).setValues([[...]])で 1 API call に原子化する。 - #36 合算マッチ拡張で副次データ脱落:
stlIds.length === 1で分岐すると合算ケースで差額記録・監査ログが落ちる。初期実装から>= 1で汎用処理する。
パラメータ取得経路の二重化(task 指示書と既存実装の差分)
- task 指示書:
Constants.getParam('RECEIPT_FOLDER_ID', '')(03_sys_params経由想定)。 - 既存実装:
Env.receiptFolderId()=_getProps().getProperty('RECEIPT_FOLDER_ID')(Script Property 経由)。 - 本案件の方針: 新規 helper は
Env.receiptFolderId()を優先して使用する(既存運用と整合)。値が空文字ならConstants.getParam('RECEIPT_FOLDER_ID', '')を試し、それも空なら{ success: false, error: 'Drive フォルダ ID が未設定です(Script Property または 03_sys_params: RECEIPT_FOLDER_ID)' }を返して処理中断する。 - 実装プロンプトでも task 指示書の文言(
Constants.getParam使用)に従うが、実装者は本注意事項を踏まえて Env フォールバックを実装に組み込むこと。
Drive スコープと oauthScopes の手動編集禁止
DriveApp.getFolderById(folderId) は既存 oauthScopes の drive で動作する。新たに appsscript.json を編集しない(部分宣言は既存スコープの自動検出を完全 OFF にする / failure_patterns #26)。Document AI も cloud-platform 配下で動作する。
既存 Sheets メニュー起点フローとの責務分担
- Sheets メニュー(
importReceiptPdfs): 従来通り全件 OCR →35_wrk_receipt書込み →processedフォルダへ移動。消込なし。 - SPA(
handleReceiptAction('import')): 同じ Drive フォルダから取込むが、processedへの移動は行わず、確認 FLG = FALSE で一時保存。SPA UI 上で人間が承認後にapproveアクションで消込確定・matched=true設定。 - 両者が同時実行されると
35_wrk_receipt上で重複書込みのリスク。LockService.getScriptLock().tryLock(10000)で必ず排他。 - 冪等性: ファイル名または Drive ファイル ID(
証跡リンクの URL 末尾から抽出)で既存行検索を行い、ヒット時は OCR をスキップ。
監査ログ(98_audit_log / LOG_AUDIT)
- 全書込・消込操作で
Utils.auditLog('CREATE', '35_wrk_receipt', fileId, '', funcName, '', dto, '')を呼ぶ。 - 消込確定時は
operation = 'UPDATE'、targetCol = 'マッチ決済ID(STL)'、beforeValue = ''、afterValue = stlIds.join(',')で記録する。
Cockpit ナビとの整合性(MAS-358 視野・2 層ナビ設計)
Cockpit は以下の 2 層ナビゲーション構造に発展する予定:
- 左ナビ(MAS-358): 画面切替(受取領収書 / 月次財務諸表 / シミュレーター等)。現在の上部タブ
<nav className="cockpit-view-tabs">を置き換える。 - 各画面固有サイドバー: その画面で使用するオペレーション系メニューを配置。GAS 操作パネル(廃止方針)の 41 項目は各画面のサイドバーに再配置される。
受取領収書 → データ取り込み汎用画面への発展:
receipt_import 画面はまず PDF 領収書インポートで実装し、段階的に以下を追加予定:
- 画面固有サイドバーメニュー(予定): 仕訳実行 / 消込実行・再マッチ / 取込設定(Drive フォルダ・OCR エンジン)
- 対応データソース拡張(将来): CSV 取込(銀行明細・クレカ明細)/ API 連携 / 経費精算インポート等
本案件での対応: 現行の上部タブ構成に receipt_import を追加し、MAS-358 実装時に左ナビ項目へ移行する。ReceiptImportPanel.tsx はサイドバーコンポーネントを受け取れるよう props 設計の余地を残す(現時点で実装は不要)。
エッジケース
実装時は以下 14 件を全件カバーすること。
| # | 条件 | 検知方法 | 期待される挙動 | ログ出力 |
|---|---|---|---|---|
| 1 | Drive 指定フォルダ内に PDF が 0 件 | DriveApp.getFilesByType() 結果が空 | { success: true, data: [], message: 'PDFが0件です' } を返す。エラーとしない | INFO |
| 2 | Drive フォルダへの読み取り権限なし | DriveApp.getFolderById() が例外 | { success: false, error: '権限不足: フォルダID=XXX' } を返し SPA でユーザー通知 | ERROR |
| 3 | 既に 35_wrk_receipt に登録済みのファイル ID | ファイル ID 列で検索してヒット | OCR・書き込みをスキップ。{ skipped: true, fileId: '...' } を返す | INFO |
| 4 | Vertex AI / Gemini API 429(クォータ超過) | callGeminiForReasoningOnVertex_() が null を返す | ステータス OCR失敗 で登録。SPA に { success: false, error: '429: クォータ超過' } を返す | ERROR |
| 5 | PDF が破損またはパスワード保護 | callDocumentAiInvoiceParser_() の entities が空またはエラー | ステータス OCR失敗 で登録。エラー内容を備考列に記録 | WARN |
| 6 | OCR 抽出結果に日付・金額フィールドが欠落 | entities 内に対象 type が存在しない | ステータス 手動確認要 で登録。欠落フィールド名を備考列に列挙 | WARN |
| 7 | 抽出金額が 0 またはマイナス | Utils.parseAmt() 結果が ≤ 0 | ステータス 手動確認要 で登録。SPA に金額異常を通知 | WARN |
| 8 | google.script.run 応答が null(V8 シリアライズ silent null) | SPA の withSuccessHandler が null を受信 | SPA 側でサーバーエラーを表示。GAS 側ログで Infinity / NaN / 循環参照を確認(failure_patterns #29) | SPA エラー表示 |
| 9 | 並行実行による書き込み競合 | LockService.getScriptLock().tryLock(10000) が false | { success: false, error: '処理中: 10秒後に再試行してください' } を即時返却 | WARN |
| 10 | INV / STL との消込候補が 0 件 | InvoiceRepository.findAll() / BankTxRepository.findAll() 照合結果が空配列 | { candidates: [], message: '消込候補なし' } を返す。エラーとしない | INFO |
| 11 | 消込候補が複数 INV の合算一致 | Utils.findSubsetSum(invAmounts, receiptAmt, { tolerance: 10 }) でインデックス配列を取得 | 合算候補グループを SPA に返す。Human-in-the-Loop で人間が承認後に消込確定 | INFO |
| 12 | 単一マッチ前提ロジックが合算マッチ拡張で副次データを落とす | 差額記録・Utils.auditLog() が stlIds.length === 1 条件のみ処理 | 初期実装から >= 1 で汎用設計し、合算ケースでも差額・監査ログが欠落しないこと(failure_patterns #36) | — |
| 13 | ReceiptImportPanel.tsx 内で const arrow ヘルパーを useMemo より後に宣言 | ブラウザで Cannot access 'xxx' before initialization | const arrow ヘルパーは useMemo 呼び出しより前に宣言する(failure_patterns #32) | ブラウザコンソール |
| 14 | RECEIPT_FOLDER_ID が未設定(Script Property / 03_sys_params 両方空) | Env.receiptFolderId() および Constants.getParam('RECEIPT_FOLDER_ID', '') がともに空文字 | { success: false, error: 'Drive フォルダIDが未設定です(Script Property または 03_sys_params: RECEIPT_FOLDER_ID)' } を返し処理中断 | ERROR |
実データ検証
実装着手前に dev 環境(npm run open:dev で開くスプレッドシート)で 以下を確認すること。確認結果は実装プロンプトの「実行前タスク」セクション冒頭に転記すること。
| # | 確認項目 | 確認方法 | 期待される状態 |
|---|---|---|---|
| 1 | 35_wrk_receipt の DDL 列名と実シートのヘッダーが一致しているか | dev スプレッドシートで 35_wrk_receipt の 1 行目(ヘッダー)を目視。Phase 1 調査5 で抽出した DDL 25 列(100_config/101_sys_config.js L1066 WRK_RCPT スキーマ)と完全一致を確認 | 完全一致。差分があれば setupAllSchemas で再生成 |
| 2 | RECEIPT_FOLDER_ID の登録経路 | GAS スクリプトエディタ「プロジェクトの設定 → スクリプトプロパティ」で RECEIPT_FOLDER_ID の有無を確認、および 03_sys_params シートで同名キーの有無を確認 | どちらか一方には登録済みであること。未登録なら dev 用フォルダ ID を Script Property に投入 |
| 3 | appsscript.json の oauthScopes フィールド | 本仕様書「現在のコード § appsscript.json」の通り、L3-13 に既存。drive + cloud-platform 含む 9 スコープ | 既存のまま編集不要(failure_patterns #26 回避) |
| 4 | dev 環境の Drive テスト用フォルダにサンプル PDF | RECEIPT_FOLDER_ID で指定された Drive フォルダ内に PDF を 3 件以上配置(領収書 / 請求書 / 不正形式 各 1 件以上) | サンプル PDF が動作確認用に存在すること |
| 5 | InvoiceRepository.findAll() の戻り値構造 | 32_wrk_invoice シートで実データ行が ≥ 1 件存在することを確認 | DTO 配列で日付・金額・取引先名フィールドが取得可能 |
| 6 | Env.gcpProjectId() / Env.docAiInvoiceProcessorId() | スクリプトプロパティに GCP_PROJECT_ID / DOCAI_INVOICE_PROCESSOR_ID の存在を確認(Document AI 採用時) | 設定済(MAS-216 / MAS-180 実装済前提) |
関連ドキュメント
| カテゴリ | 文書 | 関連性 |
|---|---|---|
| SPA 基盤 | docs/dev/dev_mas-232_sidebar_spa.md | Vite + React SPA 基盤の実装仕様 |
| Cockpit ナビ | docs/dev/dev_mas-358_cockpit_nav_sidebar.md | 後続で本案件のタブをサイドバー化する想定 |
| AI-OCR | docs/dev/dev_mas-180_ai_ocr_enhancement.md | 36_wrk_ocr_queue 仕様。本案件と統合する余地あり |
| OCR 連携 | docs/dev/dev_mas-147_invoice_ocr_auto_posting.md | 請求書 PDF→INV 自動起票(Document AI 採用例) |
| 写真 OCR | docs/dev/dev_mas-157_photo_ocr.md | 写真ベース OCR と本案件 PDF OCR の責務分離 |
| OCR 補正 | docs/dev/dev_mas-161_ocr_manual_correction_assist.md | 人間補正アシスト UI(Cockpit 上で再利用可能なパターン) |
| OCR 失敗手動 | docs/dev/dev_mas-175_ocr_fallback_manual_entry.md | OCR 失敗 PDF の手動取込フロー |
| Vertex AI 移行 | docs/dev/dev_mas-216_vertex_ai_migration.md | リージョンフォールバック・Backoff 仕様 |
| 銀行合算マッチ | docs/dev/dev_mas-162_bank_combo_match.md | Utils.findSubsetSum 利用先例(対称性確保) |
| クレカ合算マッチ | docs/dev/dev_mas-171_cc_combo_matching.md | 同上 |
| 領収書取込(旧仕様) | docs/spec/spec_receipt_import.md | importReceiptPdfs の v1 仕様。本案件で SPA 起動を追加 |
| データ定義 30s | docs/spec/data_def_30s_subledger.md | 35_wrk_receipt のデータ定義 |
| プロダクトポリシー | docs/prd.md | Human-in-the-Loop の方針根拠 |
| ADR-0010 | docs/adr/0010-modular-monolith-numbering.md | 番号プレフィックス命名の根拠 |
| failure_patterns | docs/_internal/failure_patterns.md | #20 / #25 / #26 / #28 / #29 / #32 / #33 / #35 / #36 |
| dev_spec_prompt | docs/_internal/dev_spec_prompt_template.md | 本仕様書のテンプレート準拠先 |
人間が検討すべき事項
実装着手前に、以下の論点はユーザー(齋藤)の判断を仰ぐこと。
- OCR エンジン選択: 既存
callGeminiForReceiptOnVertex_(Gemini 2.5-flash・Vertex AI 経由・領収書 / 請求書共通プロンプト)とcallDocumentAiInvoiceParser_(Document AI Invoice Parser・構造化抽出)のどちらを採用するか。MAS-180 との統合方針を含めて要相談。- 既存 Gemini 経路の利点: 既存実装が安定稼働中・領収書 PDF 専用プロンプトでチューニング済・MAS-216 のリージョンフォールバック実装済。
- Document AI 経路の利点: フィールド単位の信頼度(confidence)が取得可能・MAS-180 の確認キュー(
36_wrk_ocr_queue)と整合。 - 推奨: 第一弾は既存 Gemini 経路を再利用し、本案件で UI 統合に集中。Document AI 切替は MAS-180 と並走で検討。
processedサブフォルダへの移動タイミング: SPA 経路でimport直後に移動するか、承認・消込確定(approve)まで保留するか。SPA で承認待ちの間に Drive 側でファイルを動かすと再 OCR が複雑になるため、移動しない(承認後も元の場所に残す)方針を推奨するが、ストレージ管理上の影響を要確認。- 既存 Sheets メニュー(
importReceiptPdfs)の廃止可否: 本案件で SPA 起動が動作確認できた後、Sheets メニュー側のフローを廃止するか、両系統を維持するか。MAS-358 のメニュー廃止方針と整合させる必要あり。 - 消込候補の絞り込み閾値:
Utils.calcJaccard ≥ 0.4(取引先名類似度)とUtils.findSubsetSum tolerance ±10 円(金額許容差)のデフォルト値が dev 実データで適切か。MAS-162 / MAS-171 の運用実績と比較して再調整の余地あり。 - 「📄 受取領収書」のタブ位置: 月次財務諸表の左 / シミュレーターの右 / 別グループとして配置のいずれが UX 上自然か。本仕様書では「月次財務諸表の左」(task 指示書準拠)としているが、MAS-358 サイドバー化時に再検討。
- Document AI 採用時の
DOCAI_INVOICE_PROCESSOR_ID設定責任: Script Property に dev / prod それぞれ別 ID を設定するか、共通 ID で運用するか。prod 移行時のオペレーション手順を要設計。 - データ取り込み汎用画面・画面固有サイドバーのスコープ確定: 受取領収書画面が次に対応するデータソース(CSV 銀行明細・クレカ明細 API・経費精算 CSV 等)の優先順位と、画面固有サイドバーの仕訳実行メニューの初期実装スコープ(MAS-358 と同時着手 vs 別案件化)を要判断。
実装プロンプト
実装着手時は以下のプロンプトを Claude Code(Opus 4.7 推奨)に投入する。バッククォートで囲まず行頭 4 スペースインデントで提示する。
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-364「Cockpit 受取領収書 PDF インポートタブ」を実装してください。
## 実行前タスク
実装着手前に以下を Read で確認すること:
- `docs/dev/dev_mas-364_receipt_import_cockpit.md` — 本仕様書(全セクション)
- `webapp_client/src/CockpitApp.tsx` — 現在のタブ定義配列(L74-76 の `cockpitView` 型 + L159-172 の `<nav>` ボタン群)と月次財務諸表タブの行番号
- `webapp_client/src/cockpit-main.tsx` — エントリポイント。本案件で編集は不要見込(タブ定義は CockpitApp.tsx 側)
- `300_ui/302_spa_bridge.js` — 既存 public エンドポイントの命名・戻り値パターン(L97 `getInitialStateForSpa`、L1364 `_scrubInfinityForJSON_`)
- `500_import/502_receipt_reader.js` — 既存 OCR パイプライン関数のシグネチャ(L22 `importReceiptPdfs`、L457 `callGeminiForReceiptOnVertex_`)
- `100_config/101_sys_config.js` — `35_wrk_receipt` DDL(L1066 `WRK_RCPT` スキーマ・25 列)。**変更不要**
- `000_infra/004_utils.js` — `Utils.parseAmt` (L453) / `Utils.parseDateToYm` (L354) / `Utils.calcJaccard` (L691) / `Utils.findSubsetSum` (L725) / `Utils.auditLog` (L539) / `callDocumentAiInvoiceParser_` (L224)
- `200_data/202_repository.js` — `InvoiceRepository.findAll` (L163) / `BankTxRepository.findAll` (L225)
- `000_infra/001_env.js` — `Env.receiptFolderId` (L64) / `Env.gcpProjectId` / `Env.docAiInvoiceProcessorId`
また、本仕様書「実データ検証」セクションの 6 項目(DDL ヘッダー一致 / `RECEIPT_FOLDER_ID` 経路 / `appsscript.json` / サンプル PDF / `InvoiceRepository` 戻り値 / Document AI 設定)を dev 環境で実測確認し、結果を作業ログに記録すること。
## 修正対象ファイル
1. `webapp_client/src/cockpit/ReceiptImportPanel.tsx` — 新規作成
2. `webapp_client/src/CockpitApp.tsx` — タブ定義配列への挿入 + import 追記 + switch 拡張(cockpit-main.tsx ではない点に注意)
3. `300_ui/302_spa_bridge.js` — `handleReceiptAction(actionType, payload)` を末尾 `_` なしで新設
4. `500_import/502_receipt_reader.js` — OCR・マッチング処理の private helper 関数を追加(既存 `importReceiptPdfs` は変更しない)
5. `100_config/101_sys_config.js` — `35_wrk_receipt` DDL は既存 25 列で要件を満たすため変更不要(実データ検証 #1 で差分が見つかった場合のみ修正)
## 実装ステップ
### Step 1: SPA タブ登録(CockpitApp.tsx)
- `cockpitView` の型を `'simulator' | 'pl_monthly' | 'receipt_import'` に拡張する(L76)
- 仕様書「修正方針 Step 1」で特定した月次財務諸表タブの定義行(L161-165 の 📊 月次財務諸表ボタン)の **直前** に
`receipt_import` タブエントリ(`📄 受取領収書`)を挿入する
- ファイル先頭の import 文に以下を追記する(この import を忘れると
コンポーネントが bundle に含まれずタブが表示されない / failure_patterns #33):
import { ReceiptImportPanel } from './cockpit/ReceiptImportPanel';
- タブ切替ロジック(switch / 条件分岐等)にも `receipt_import` ケースを追加し、
`<ErrorBoundary label="ReceiptImportPanel"><ReceiptImportPanel /></ErrorBoundary>` をマウントする
### Step 2: SPA コンポーネント作成(ReceiptImportPanel.tsx)
- `google.script.run.withSuccessHandler(fn).withFailureHandler(fn)
.handleReceiptAction(actionType, payload)` で GAS ブリッジを呼び出す
(既存の `callApi` helper を使用する場合は型定義 `docs/spec/sidebar_api.d.ts` を確認)
- `withSuccessHandler` の引数が null の場合は「サーバーエラー:
GAS 側で Infinity/NaN が返された可能性があります(_scrubInfinityForJSON_ を確認してください)」
と表示する(failure_patterns #29)
- 表示項目: PDF ファイル名・抽出日付(発生日)・抽出金額(税込)・ステータス
(消込済 / OCR確認要 / 手動確認要 / OCR失敗)・消込候補 INV ID・操作ボタン
(`再 OCR` / `再マッチ` / `承認・消込確定`)
- **「取込開始」ボタンのチャンク処理フロー**:
1. まず `handleReceiptAction('import')` を呼び出して未処理 PDF の ID リストを取得する
2. 取得したリストを `for await` または直列 `.then()` チェーンで 1 件ずつ処理する:
`handleReceiptAction('process_single', { fileId })` を逐次呼び出す
3. 1 件完了するたびに React state(一覧配列)を部分更新し、プログレスバー(`処理済 N / 全 M 件`)を即時反映する
4. `Promise.allSettled` での並列呼び出しは禁止(GAS 同時実行上限・LockService タイムアウト誘発)
- const arrow ヘルパーは useMemo / useCallback 呼び出しより前に宣言する
(TDZ 違反 / failure_patterns #32)
### Step 3: GAS ブリッジ関数追加(302_spa_bridge.js)
- `function handleReceiptAction(actionType, payload)` を新設する
(末尾 `_` 禁止 — google.script.run から到達不可になる / failure_patterns #28)
- actionType の取りうる値と責務:
- `'import'` — Drive フォルダ内の **未処理 PDF のファイル ID・名前リストを返すだけ**(OCR は実行しない)
- `'process_single'` — `payload.fileId` で指定した PDF 1 件のみ OCR・マッチング処理を実行し結果を返す
- `'rematch'` — `payload.receiptId` の候補再検索(OCR 再利用・マッチのみ再実行)
- `'reocr'` — `payload.fileId` の OCR 再実行(1 件)
- `'approve'` — 消込確定
- **GAS 側で PDF を複数まとめてループ OCR しない**(6 分タイムアウト超過リスク。SPA 側が 1 件ずつ
`process_single` を逐次呼び出しながらプログレスバーを更新するアーキテクチャを採用する)
- 戻り値: `{ success: boolean, data: any, error?: string }` で統一する。
return 直前に必ず `return _scrubInfinityForJSON_(result);` を通す(failure_patterns #29)
- 内部処理は末尾 `_` 付きの private helper(例: `_listUnprocessedReceipts_()` /
`_processReceiptSingle_(fileId)` / `_rematchReceipt_(receiptId)` / `_approveReceipt_(receiptId, stlIds)`)に委譲する
- **LockService のスコープを最小化**: `LockService.getScriptLock().tryLock(30000)` は
**シートへの書き込み直前のみ** 取得し、OCR API 呼び出し(重い処理)はロック範囲の**外**で実行する。
取得失敗時は `{ success: false, error: '処理中: 30秒後に再試行してください' }` を返す
### Step 4: OCR・マッチング処理(502_receipt_reader.js)
- Drive フォルダ ID は `Constants.getParam('RECEIPT_FOLDER_ID', '')` で取得する。
**空文字なら `Env.receiptFolderId()` をフォールバックとして使用する**
(仕様書「注意事項 § パラメータ取得経路の二重化」参照)。両方空ならエラー返却
- PDF 抽出: **`callGeminiForReceiptOnVertex_(base64Pdf, fileName)`** を使用する(502_receipt_reader.js L457)。
`callDocumentAiInvoiceParser_` は使用しない(MAS-180 との調整が必要な場合は別案件)
- 日付正規化: `Utils.parseDateToYm(rawDate)` を使用する(004_utils.js L354)
- 金額パース: `Utils.parseAmt(rawAmt)` を使用する(004_utils.js L453)
- 取引先名ファジーマッチ: `Utils.calcJaccard(a, b)` を使用する(004_utils.js L691 / 閾値 0.4 以上でヒット候補)
- 金額合算マッチ: `Utils.findSubsetSum(invAmounts, receiptAmt, { tolerance: 10 })` を使用する(004_utils.js L725)。
**`invAmounts` 配列は DTO の `税込金額_計画` フィールドを必ず `Utils.parseAmt(dto['税込金額_計画'])` で数値変換してから配列化すること**(文字列 "1,000" のまま渡すと NaN になりマッチ不成立)
- **ステータス値の書き込み先を明確に分離**:
- `処理結果` 列(20 列目)に `'OCR確認要'` / `'手動確認要'` / `'OCR失敗'` / `'消込済'` の文字列を書き込む
- `確認FLG` 列には **boolean `FALSE`** をセットする(文字列 "FALSE" 不可)
- 架空の `ステータス` 列は存在しない。必ず DDL の既存列名で書き込むこと
- **LockService のスコープを最小化**: OCR API 呼び出しはロックの**外**で実行する。
`LockService.getScriptLock().tryLock(30000)` は **`sheet.getRange(...).setValues(...)` の直前のみ**取得し、
書き込み完了後すぐに `lock.releaseLock()` する(OCR 処理中にロックを保持しない)
- `35_wrk_receipt` への複数列書き込みは
`sheet.getRange(row, colStart, 1, n).setValues([[v1, v2, ...]])` で 1 API call に原子化する
(フィルター適用中の連続 setValue は silent-fail する / failure_patterns #35)
- 書き込み前にファイル ID 列で既存行を検索し、登録済みならスキップする(冪等性)
- 全書込・消込操作後に `Utils.auditLog('CREATE', '35_wrk_receipt', fileId,
'', 'FUNC名', '', dto, '')` を呼ぶ(004_utils.js L539)
## 制約
- `appsscript.json` の `oauthScopes` フィールドを新規追加・部分編集しない
(既存 9 スコープに `drive` / `cloud-platform` を含むため追加不要 / failure_patterns #26)
- `35_wrk_receipt` 書き込みで列番号ハードコード禁止(ヘッダー名 indexOf でインデックス取得 / ADR-0011)
- 有効フラグ FALSE 行をスキップする処理を `InvoiceRepository.findAll` / `BankTxRepository.findAll`
の結果ループに必ず含める
- `302_spa_bridge.js` に追加する public 関数に末尾 `_` を付けない(failure_patterns #28)
- `501_cc_importer.js` / `502_bank_importer.js` の消込ロジックと対称性を確認する
(failure_patterns #25: 全分岐で確認FLG=FALSE が明示的にセットされていること・`matched=true` で二重消費防止)
- 既存の `importReceiptPdfs` 等の非 SPA メニュー関数の内部実装を変更しない
- 合算マッチの差額記録・監査ログは `stlIds.length === 1` 条件で分岐させず `>= 1` で汎用処理する(failure_patterns #36)
## エッジケース
仕様書「エッジケース」セクションの 14 件テーブルを参照。実装時に全件対応すること。
## 動作確認
1. `npm run push:dev` がエラーなく完了することを確認する
2. `npm --prefix webapp_client run build` で SPA bundle が生成され、エラーなしで完了することを確認する
3. `npm run open:dev` で dev スプレッドシートを開き、Cockpit Web App を開いて
`receipt_import` タブが月次財務諸表タブの左に表示されることを確認する
4. dev 環境の Drive フォルダにサンプル PDF を 3 件置き「インポート」を実行する
5. `35_wrk_receipt` に行が追記され、ステータスが `OCR確認要` または `手動確認要`・
確認FLG が `FALSE` であることを確認する
6. 同 PDF を再インポートしてスキップされることを確認する(冪等性テスト)
7. Cockpit UI で承認し、消込確定後にステータスが `消込済`・`マッチ決済ID(STL)` 列に
INV/STL ID が記録されることを確認する
8. `98_audit_log` に操作ログが記録されていることを確認する
9. `grep -n 'handleReceiptAction_' 300_ui/302_spa_bridge.js` で末尾 `_` が付いていないことを確認する
(出力が空であること)
10. ブラウザコンソールで `Cannot access 'xxx' before initialization` エラーが出ていないこと(failure_patterns #32)
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| Phase 1(調査・設計) | あり | ファイル調査・固有名詞確定 |
| Phase 2(清書) | なし | Phase 1 確定内容の書き下し |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| Phase 1(調査・設計) | Claude Sonnet 4.6 | コード読み取りと複数ファイル横断の設計判断 |
| Phase 2(仕様書清書) | Claude Haiku 4.5 | Phase 1 確定内容の書き下しのみ |
| 実装(全 Step) | Claude Opus 4.7 (1M context) | SPA・GAS ブリッジ・DDL・OCR の 5 ファイル横断実装、会計ドメイン理解が必要 |
変更履歴
| 日時 | 変更内容 | 著者 |
|---|---|---|
| 2026-05-11 | 初版作成(task_MAS-364.md / task_MAS-364.gemini.md からの仕様書生成) | Claude Code (Opus 4.7) |
| 2026-05-11 | 将来拡張方針追記(汎用データ取り込み画面への発展・Cockpit 2 層ナビ設計・画面固有サイドバー計画・GAS 操作パネル廃止との整合) | Claude Code (Sonnet 4.6) |
仕様書作成プロンプト
本仕様書は以下の 2 つのプロンプト(tasks/prompts/task_MAS-364.gemini.md および tasks/prompts/task_MAS-364.md)から scripts/2_run_writers.sh 相当のフローで生成された。
展開して表示: task_MAS-364.gemini.md(Gemini アーキテクト原案)
<instruction>
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. 拡張思考の使い分け: Phase 1で設計を確定させ、Phase 2では出力に徹する。
2. テキスト報告の禁止: 説明は1文以内。直ちに tool を呼ぶ。
3. 4-5 分割の Write/Edit 実行: 2-1(骨格)/2-2(前半)/2-3a(後半)/2-3b(プロンプト)/2-4(<details>記録)に分割。
4. 各 Step で何を書くかを具体指示: 出力時に設計判断を再考しない。
======================================================================
あなたはGAS会計システムのシニア開発者兼仕様書ライターです。
CLIエージェント「Claude Code」として、案件 MAS-364「Cockpit 受取領収書 PDF インポートタブ(月次財務諸表の左に新規タブ追加)」の開発仕様書を作成してください。
## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)
以下のファイルを具体的に調査し、仕様策定に必要な事実を収集してください。
1. `webapp_client/src/cockpit-main.tsx` を読み込み、Cockpit の現在のタブ構成とルーティング、新規タブの追加ポイント(月次財務諸表の左)を特定すること。
2. `300_ui/302_spa_bridge.js` を grep・Read し、`google.script.run` 経由で呼び出される public エンドポイントの既存パターンとエラーハンドリング方法を確認すること。
3. `500_import/502_receipt_reader.js` を読み込み、Gemini / Vertex AI 経由での OCR 取込関数のシグネチャ、入出力形式、例外送出のパターンを特定すること。
4. スプレッドシート上で `35_wrk_receipt` シート(または類似の領収書管理タブ)の DDL(列構造)が存在するか確認し、ファイルID・ステータス・金額・日付を保持するための書き込み先列を特定すること。
5. `200_data/202_repository.js` を読み込み、`InvoiceRepository.findAll()` や `BankTxRepository.findAll()` の戻り値の構造を確認し、消込候補マッチングに使用するデータをどう取得するか確認すること。
## Phase 2: 仕様書の分割作成
出力先: `docs/dev/dev_mas-364_receipt_import_cockpit.md`
**【重要】絶対に1回のツール呼び出しで全内容を出力せず、以下のStepに分割して実行すること。**
### Step 2-1: 骨格の作成 (File Write)
`dev_spec_prompt_template.md` で規定された 14 セクション(概要 / 目的 / 現在のコード / 修正方針 / 影響範囲 / 注意事項 / エッジケース / 実データ検証 / 関連ドキュメント / 人間が検討すべき事項 / 実装プロンプト / 推奨実行モデル / 変更履歴 / 仕様書作成プロンプト)の見出しと MDTM YAML フロントマターのみを持つ骨格ファイルを作成してください。
### Step 2-2: 前半セクションの追記(概要・目的・現在のコード・修正方針・影響範囲・注意事項)
※アーキテクトからの指示:
- 以下の 5 項目以上の設計方針を反映して記述すること:
- **アーキテクチャ方針**: SPA 側には `ReceiptImportPanel.tsx` を新設し、GAS 側との通信は `302_spa_bridge.js` に新設する統合エンドポイント `handleReceiptAction(actionType, payload)` に集約する。内部の OCR やマッチングロジックは `502_receipt_reader.js` などの helper(末尾 `_` 付き)に委譲する設計とすること。
- **再利用すべき既存関数**: 必ず `200_data/202_repository.js` の `InvoiceRepository.findAll()` および `BankTxRepository.findAll()`、`000_infra/004_utils.js` の `Utils.getSheetByKey()`、`000_infra/003_contracts.js` の `Contracts.toDto()` 等の実在する関数を引用し、車輪の再発明を避けること。
- **データアクセス規約**: `35_wrk_receipt` への書き込み時は列番号のハードコードを禁止し、1行目のヘッダー名ベース(`indexOf` 検索)で列インデックスを特定すること。また `有効フラグ` が `FALSE` の行はスキップすること。
- **関連する failure_patterns**:
- `#20` 命名造語禁止 (推測で関数名を書かない)
- `#25` 並列実装対称性漏れ (銀行やクレカの消込とロジックを対称に保つ)
- `#26` oauthScopes 部分宣言禁止 (`DriveApp` 利用時に `appsscript.json` を手動上書きしない)
- `#28` SPA のエンドポイント関数 `handleReceiptAction` には絶対に末尾 `_` を付けない
- `#29` V8 シリアライズの silent null 回避 (SPA へ返す JSON 内の Infinity / NaN サニタイズ)
- `#33` SPA コンポーネント新設時の `cockpit-main.tsx` への副作用 import 漏れ防止
- **段階移行・並行稼働方針**: MAS-232 SPA 移行期であるため、旧来の HTML ベースの機能が壊れないよう SPA 用ブリッジエンドポイントは既存関数とは完全に独立させて新設すること。
### Step 2-3a: エッジケース〜人間検討事項の追記
※アーキテクトからの指示:
- **エッジケースの網羅**: 以下の 10 件のエッジケースをテーブル化(条件 / 検知方法 / 期待される挙動 / ログ出力)して明記すること:
1. **空配列**: Drive指定フォルダ内にPDFが0件の場合
2. **権限不足**: Driveフォルダへの読み取りアクセス権がない場合
3. **重複データ**: 既に `35_wrk_receipt` に登録済みのファイルIDが再度取り込まれた場合
4. **クォータ超過**: Vertex AI / Gemini API 呼び出し時に 429 エラーとなった場合
5. **破損データ**: PDFが壊れている、またはパスワード保護等で読み取れない場合
6. **レスポンス欠落**: OCR抽出結果の JSON に「日付」や「金額」が含まれない場合
7. **マイナス値**: 抽出された金額が 0 またはマイナスの場合
8. **V8 シリアライズ異常**: 戻り値に NaN / Infinity が含まれ silent null (#29) となる場合
9. **並行実行**: 複数クライアントから同時に取込アクションが呼び出され、書き込み競合が発生する場合
10. **0件ヒット**: INV / STL とのマッチング候補探索で条件に合致するものが1件もない場合
- **二重実行の防止**: `35_wrk_receipt` 書き込み前に `ファイルID` で検索を行い、存在する場合は OCR と書き込みをスキップする冪等性ロジックを必須とすること。また `LockService.getScriptLock()` による排他制御を記載すること。
- **Human-in-the-Loop**: OCR の結果はそのまま消込確定せず、「確認FLG = FALSE」または「ステータス = OCR確認要」の状態で一時保存し、Cockpit UI 上で人間が金額・日付を承認(または修正)してから消込を実行するステップを挟むこと。
### Step 2-3b: 実装プロンプト〜変更履歴の追記
- 実装プロンプトには、修正対象ファイル (`webapp_client/src/cockpit-main.tsx`, `webapp_client/src/cockpit/ReceiptImportPanel.tsx`, `300_ui/302_spa_bridge.js`, `500_import/502_receipt_reader.js`) と、具体的な実装ステップ(1. SPA UIコンポーネント作成、2. GAS ブリッジエンドポイント作成、3. 既存OCR連携、4. 消込マッチング連携)をバッククォート無しの 4 スペースインデントで明記すること。
- #33 の import 漏れ防止の指示を必ず実装プロンプトのチェックリストに含めること。
- 推奨実行モデルは複数ファイル横断となるため「Claude Opus 4.7 (1M context)」を指定すること。
- 変更履歴に現在日時 (YYYY-MM-DD HH:MM 形式) を記載すること。
### Step 2-4: 仕様書作成プロンプトの記録
末尾に `<details><summary>仕様書作成プロンプト</summary>` を設け、この `<instruction>` タグ内の全文を追記すること。
## Phase 3: `_config.json` への追記と構文チェック
- `docs/_config.json` の `nav` 配列内の「§E.6 パイプライン・RPA・外部連携」または「§E.5 FP&A・UI」セクション付近に、作成した `dev/dev_mas-364_receipt_import_cockpit.md` のエントリを `MAS-364 Cockpit 受取領収書インポート` として追記する指示を含めること。
</instruction>
展開して表示: task_MAS-364.md(Claude 添削版・実装詳細)
<instruction>
| 1 | Drive 指定フォルダ内に PDF が 0 件 | `DriveApp.getFilesByType()` 結果が空 | `{ success: true, data: [], message: 'PDFが0件です' }` を返す。エラーとしない | INFO |
| 2 | Drive フォルダへの読み取り権限なし | `DriveApp.getFolderById()` が例外 | `{ success: false, error: '権限不足: フォルダID=XXX' }` を返し SPA でユーザー通知 | ERROR |
| 3 | 既に `35_wrk_receipt` に登録済みのファイル ID | ファイル ID 列で検索してヒット | OCR・書き込みをスキップ。`{ skipped: true, fileId: '...' }` を返す | INFO |
| 4 | Vertex AI / Gemini API 429(クォータ超過) | `callGeminiForReasoningOnVertex_()` が null を返す | ステータス `OCR失敗` で登録。SPA に `{ success: false, error: '429: クォータ超過' }` を返す | ERROR |
| 5 | PDF が破損またはパスワード保護 | `callDocumentAiInvoiceParser_()` の entities が空またはエラー | ステータス `OCR失敗` で登録。エラー内容を備考列に記録 | WARN |
| 6 | OCR 抽出結果に日付・金額フィールドが欠落 | `entities` 内に対象 type が存在しない | ステータス `手動確認要` で登録。欠落フィールド名を備考列に列挙 | WARN |
| 7 | 抽出金額が 0 またはマイナス | `Utils.parseAmt()` 結果が ≤ 0 | ステータス `手動確認要` で登録。SPA に金額異常を通知 | WARN |
| 8 | `google.script.run` 応答が null(V8 シリアライズ silent null) | SPA の `withSuccessHandler` が null を受信 | SPA 側でサーバーエラーを表示。GAS 側ログで `Infinity` / `NaN` / 循環参照を確認(failure_patterns #29) | SPA エラー表示 |
| 9 | 並行実行による書き込み競合 | `LockService.getScriptLock().tryLock(10000)` が `false` | `{ success: false, error: '処理中: 10秒後に再試行してください' }` を即時返却 | WARN |
| 10 | INV / STL との消込候補が 0 件 | `InvoiceRepository.findAll()` / `BankTxRepository.findAll()` 照合結果が空配列 | `{ candidates: [], message: '消込候補なし' }` を返す。エラーとしない | INFO |
| 11 | 消込候補が複数 INV の合算一致 | `Utils.findSubsetSum(invAmounts, receiptAmt, { tolerance: 10 })` でインデックス配列を取得 | 合算候補グループを SPA に返す。Human-in-the-Loop で人間が承認後に消込確定 | INFO |
| 12 | 単一マッチ前提ロジックが合算マッチ拡張で副次データを落とす | 差額記録・`Utils.auditLog()` が `stlIds.length === 1` 条件のみ処理 | 初期実装から `>= 1` で汎用設計し、合算ケースでも差額・監査ログが欠落しないこと(failure_patterns #36) | — |
| 13 | `ReceiptImportPanel.tsx` 内で const arrow ヘルパーを useMemo より後に宣言 | ブラウザで `Cannot access 'xxx' before initialization` | const arrow ヘルパーは useMemo 呼び出しより前に宣言する(failure_patterns #32) | ブラウザコンソール |
| 14 | `RECEIPT_FOLDER_ID` が `03_sys_params` 未登録 | `Constants.getParam('RECEIPT_FOLDER_ID', '')` が空文字 | `{ success: false, error: 'Drive フォルダIDが未設定です(03_sys_params: RECEIPT_FOLDER_ID)' }` を返し処理中断 | ERROR |
**実データ検証セクション**に記載すべき事前確認項目:
- `35_wrk_receipt` の DDL 列名と実シートのヘッダーが一致しているか(Phase 1 調査5の結果を転記)
- `03_sys_params` に `RECEIPT_FOLDER_ID`(またはシステム上の正式キー名)が登録されているか
- `appsscript.json` に `oauthScopes` フィールドが存在するか否か(Phase 1 調査3の結果を転記)
- dev 環境の Drive テスト用フォルダにサンプル PDF が存在するか
---
### Step 2-3b: 実装プロンプト〜変更履歴の追記(Edit, ~250 行)
実装プロンプトはバッククォートで囲まず、**行頭 4 スペースインデント**で以下を出力すること:
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-364「Cockpit 受取領収書 PDF インポートタブ」を実装してください。
## 実行前タスク
実装着手前に以下を Read で確認すること:
- `docs/dev/dev_mas-364_receipt_import_cockpit.md` — 本仕様書(全セクション)
- `webapp_client/src/cockpit-main.tsx` — 現在のタブ定義配列と月次財務諸表タブの行番号
- `300_ui/302_spa_bridge.js` — 既存 public エンドポイントの命名・戻り値パターン
- `500_import/502_receipt_reader.js` — 既存 OCR パイプライン関数のシグネチャ
- `100_config/101_sys_config.js` — `35_wrk_receipt` DDL スキーマ(未定義なら追記対象)
## 修正対象ファイル
1. `webapp_client/src/cockpit/ReceiptImportPanel.tsx` — 新規作成
2. `webapp_client/src/cockpit-main.tsx` — タブ定義配列への挿入 + import 追記
3. `300_ui/302_spa_bridge.js` — `handleReceiptAction(actionType, payload)` を末尾 `_` なしで新設
4. `500_import/502_receipt_reader.js` — OCR・マッチング処理の private helper 関数を追加
5. `100_config/101_sys_config.js` — `35_wrk_receipt` DDL スキーマが未定義の場合のみ追記
## 実装ステップ
### Step 1: SPA タブ登録(cockpit-main.tsx)
- 仕様書「現在のコード」で特定した月次財務諸表タブの定義行の直前に
`receipt_import` タブエントリを挿入する
- ファイル先頭の import 文に以下を追記する(この import を忘れると
コンポーネントが bundle に含まれずタブが表示されない / failure_patterns #33):
import ReceiptImportPanel from './cockpit/ReceiptImportPanel'
- タブ切替ロジック(switch / 条件分岐等)にも `receipt_import` ケースを追加する
### Step 2: SPA コンポーネント作成(ReceiptImportPanel.tsx)
- `google.script.run.withSuccessHandler(fn).withFailureHandler(fn)
.handleReceiptAction(actionType, payload)` で GAS ブリッジを呼び出す
- `withSuccessHandler` の引数が null の場合は「サーバーエラー:
GAS 側で Infinity/NaN が返された可能性があります」と表示する(failure_patterns #29)
- 表示項目: PDF ファイル名・ステータス(消込済 / OCR確認要 / 手動確認要 / OCR失敗)・
抽出金額・抽出日付・消込候補 INV ID
- const arrow ヘルパーは useMemo / useCallback 呼び出しより前に宣言する
(TDZ 違反 / failure_patterns #32)
### Step 3: GAS ブリッジ関数追加(302_spa_bridge.js)
- `function handleReceiptAction(actionType, payload)` を新設する
(末尾 `_` 禁止 — google.script.run から到達不可になる / failure_patterns #28)
- actionType: 'import'=新規取込, 'rematch'=候補再検索, 'reocr'=OCR再実行,
'approve'=消込確定
- 戻り値: `{ success: boolean, data: any, error?: string }` で統一する。
Infinity / NaN を含まないようサニタイズしてから return する(failure_patterns #29)
- 内部処理は末尾 `_` 付きの private helper(例: `_importReceipts_()` 等)に委譲する
### Step 4: OCR・マッチング処理(502_receipt_reader.js)
- Drive フォルダ ID は `Constants.getParam('RECEIPT_FOLDER_ID', '')` で取得する
- PDF 抽出: `callDocumentAiInvoiceParser_(base64Pdf, 'application/pdf',
{ fieldMask: 'entities' })` を使用する
- 日付正規化: `Utils.parseDateToYm(rawDate)` を使用する
- 金額パース: `Utils.parseAmt(rawAmt)` を使用する
- 取引先名ファジーマッチ: `Utils.calcJaccard(a, b)` を使用する(閾値 0.4 以上でヒット候補)
- 金額合算マッチ: `Utils.findSubsetSum(invAmounts, receiptAmt, { tolerance: 10 })` を使用する
- 書き込み前に `LockService.getScriptLock().tryLock(10000)` で排他制御する
- `35_wrk_receipt` への複数列書き込みは
`sheet.getRange(row, colStart, 1, n).setValues([[v1, v2, ...]])` で 1 API call に原子化する
(フィルター適用中の連続 setValue は silent-fail する / failure_patterns #35)
- 書き込み前にファイル ID 列で既存行を検索し、登録済みならスキップする(冪等性)
- 全書込・消込操作後に `Utils.auditLog('CREATE', '35_wrk_receipt', fileId,
'', 'FUNC名', '', dto, '')` を呼ぶ
## 制約
- `appsscript.json` の `oauthScopes` フィールドを新規追加・部分編集しない
(failure_patterns #26)
- `35_wrk_receipt` 書き込みで列番号ハードコード禁止(ヘッダー名 indexOf でインデックス取得)
- 有効フラグ FALSE 行をスキップする処理を全 findAll ループに含める
- `302_spa_bridge.js` に追加する public 関数に末尾 `_` を付けない(failure_patterns #28)
- `501_cc_importer.js` / `502_bank_importer.js` の消込ロジックと対称性を確認する
(failure_patterns #25: 全分岐で確認FLG=FALSE が明示的にセットされていること)
- 既存の `importReceiptPdfs` 等の非 SPA メニュー関数の内部実装を変更しない
## エッジケース
仕様書「エッジケース」セクションの 14 件テーブルを参照。実装時に全件対応すること。
## 動作確認
1. `npm run push:dev` がエラーなく完了することを確認する
2. Cockpit Web App を開き `receipt_import` タブが月次財務諸表タブの左に表示されることを確認する
3. dev 環境の Drive フォルダにサンプル PDF を置き「インポート」を実行する
4. `35_wrk_receipt` に行が追記されステータスが `OCR確認要`・確認FLG が `FALSE` であることを確認する
5. 同 PDF を再インポートしてスキップされることを確認する(冪等性テスト)
6. Cockpit UI で承認し、消込確定後にステータスが `消込済` に変わることを確認する
7. `98_audit_log` に操作ログが記録されていることを確認する
8. `grep -n 'handleReceiptAction_' 300_ui/302_spa_bridge.js` で末尾 `_` が付いていないことを確認する
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| Phase 1(調査・設計) | あり | ファイル調査・固有名詞確定 |
| Phase 2(清書) | なし | Phase 1 確定内容の書き下し |
推奨実行モデルには以下のテーブルを記載すること:
| 工程 | 推奨モデル | 理由 |
|------|----------|------|
| Phase 1(調査・設計) | Claude Sonnet 4.6 | コード読み取りと複数ファイル横断の設計判断 |
| Phase 2(仕様書清書) | Claude Haiku 4.5 | Phase 1 確定内容の書き下しのみ |
| 実装(全 Step) | Claude Opus 4.7 (1M context) | SPA・GAS ブリッジ・DDL・OCR の 5 ファイル横断実装、会計ドメイン理解が必要 |
変更履歴: `2026-05-11` 初版作成を記載する。
---
### Step 2-4: 仕様書作成プロンプトの記録(Edit, 最後尾・最重量)
「## 仕様書作成プロンプト」セクション下に以下を追記する:
展開して表示
(この
Phase 3: docs/_config.json への追記
docs/_config.json を Edit し、nav 配列内の §E.6 パイプライン・RPA・外部連携 セクションに以下のエントリを実際に追加する(テキスト報告ではなくファイルを直接編集すること):
{ "file": "dev/dev_mas-364_receipt_import_cockpit.md", "title": "E.6.X MAS-364 Cockpit 受取領収書インポート" }
追記後、JSON 構文が valid であることを確認すること(余分なカンマ・閉じ括弧の欠落に注意)。
</details>