• #Chrome拡張機能
  • #クラウド会計
  • #自動仕訳ルール
  • #CSRF
  • #Google Sheets API
  • #データマイグレーション
開発misc-devメモ

Chrome拡張 会計サービス連携 - 自動仕訳ルール同期機能の全面改修

ルール同期タブを開いたら、別の事業者のURLが表示されていた。ストレージのデータを覗くと、事業者Aのキーに事業者BのURLが混入している。一括エクスポートでCTIを順番に切り替えた際に、URL遷移のタイミングで古いCTIが返ってきて別事業者のキーに保存されたのが原因だった。ここから始まって、ルール同期機能をほぼ丸ごと作り直した一日の記録。


汚染ストレージデータの修復

問題

一括エクスポート時、extractCti()がURL遷移完了前に呼ばれて古いCTIを返すことがあった。結果、事業者Bに切り替えたつもりが事業者AのCTIを参照し、事業者Aのストレージキーに事業者BのjournalRuleImportUrl/journalRuleKeepUrlが上書きされる。

修正

マイグレーションコードを実装。chrome.storageから全事業者のデータを読み出し、URLに含まれるCTIパラメータと格納先のキーを突き合わせて不整合を検出、自動修復する。起動時に1回だけ走るようにした。


CSRFトークン修正: 422の壁

試行の流れ

ルール削除のDELETE APIを叩くと422が返ってくる。レスポンスボディは空で、何が悪いのか手がかりがない。

  1. リクエストボディを疑った -- パラメータの形式を変えて数回投げたが変わらず422
  2. DevToolsでブラウザの正規リクエストと比較 -- ヘッダーを見比べてX-CSRF-Tokenの存在に気づいた
  3. トークン取得を追加 -- ページのmetaタグからCSRFトークンを抽出してヘッダーに付与。422が消えて204が返った

GETやPOSTは通っていたのにDELETEだけ弾かれていたのは、CSRFチェックがメソッド別に適用されていたため。ブラウザの正規リクエストをDevToolsで横に並べて差分を取る、という基本動作が一番早かった。


ルール同期タブのUI改修

もともとは単一のURL入力欄が1つあるだけだった。これを事業者別リストに変更し、全事業者のjournalRuleKeepUrlをプリフィルする形にした。

事業者を選んでURLを確認・編集し、同期ボタンを押す。どの事業者に対して同期を実行するかが一目で分かるようになった。


同期メッセージの分離と追加件数表示

以前は成功もエラーも同じスタイルで「同期しました」と出ていた。エラー時に赤文字で「同期エラー: (理由)」、成功時に緑で「同期完了: N件追加」と分けた。件数が出るだけで、処理が実際に動いた実感が湧く。


FormDataフィールド名修正

CSVインポートのPOSTで500エラー。サーバーがファイルを認識していない様子だった。

DevToolsでブラウザのフォーム送信を再現して比較したところ、FormDataのフィールド名が違っていた。ブラウザはimport[file]、コードはfile。フィールド名を合わせたら即座に通った。


CTI切替後のURL復元

同期やインポートの処理中、内部的に別の事業者のCTIに切り替えることがある。処理完了後にブラウザのURLバーを見ると、切り替え先のCTIが残ったまま。この状態でリロードすると意図しない事業者のページが表示される。

history.replaceStateで、処理完了後にURLのctiパラメータを元の値に復元するようにした。ページ遷移は発生せず、URLバーの表示だけが戻る。


自動仕訳ルールインポートタブの廃止

ルールインポート機能をルール同期タブに統合した。startJournalRuleImport等のインポート専用関数を削除し、同期フローの中でインポートも実行する形に。タブが1つ減ってUIがすっきりした。


ルールID照合バグ: 口座未紐付けルールの罠

問題

CSVからインポートしたルールとJSON APIから取得したルールを突き合わせて、重複を判定する処理がある。口座に紐付いていないルール(金融機関列が空)がCSVに含まれると、JSON API側の対応するルールとマッチしない。

試行の流れ

  1. CSV側の金融機関列をログ出力 -- 空文字列が入っている。JSON API側はaccount_idが存在しないかnull
  2. 空文字列とnullの比較で失敗していると仮定 -- 両方をnullに正規化してみたが、まだマッチしないルールがある
  3. JSON APIのレスポンスを精査 -- 口座未紐付けのルールはaccount_id: 0で返ってくるケースがあると判明
  4. account_id='0'でフォールバック検索を追加 -- CSV側の金融機関列が空の場合、account_idnullundefined0のいずれかにマッチすれば同一ルールと判定

APIのレスポンス仕様が「未紐付け = フィールドなし」と「未紐付け = 0」で揺れている。どちらが来ても拾えるようにフォールバックで対処した。


スプレッドシート書式クリア問題

問題

replaceAll(全データ置換)で値をクリアしたとき、セルの書式(背景色、罫線、フォント)がそのまま残る。データは消えたのに書式だけ残った「幽霊行」が下の方に延々と続く。

試行の流れ

  1. clearメソッドでシート全体をクリア -- 値だけでなくヘッダーの書式も消えてしまう
  2. データ範囲だけを指定してクリア -- 行数が変動するので、前回のデータ行数を覚えておく必要があり面倒
  3. 方針転換: 余分な行を削除する -- replaceAllで値を書き込んだ後、データ行数以降の行をdeleteDimensionで削除。書式ごと消える

行削除アプローチに切り替えたら、幽霊行の問題が消えた。Sheets APIのdeleteDimensionはセルの値・書式・行自体をまとめて消すので、書式残りが起きない。


ルール_残すシートの毎回上書き化

ルール_残すシートは初回だけデータを書き込み、以降は手動編集を尊重する設計だった。が、実運用では同期のたびに最新のルール一覧で上書きしたほうが便利だと分かった。

replaceAllを毎回実行する形に変更。ただし手動で追記したメモ等がシート末尾にある場合を考慮して、50行の空行バッファを末尾に追加してからdeleteDimensionで不要行を削除する。データ行の直後に余白を残しつつ、古いゴミ行は消す。


シート名キャッシュ

Sheets APIのspreadsheets.getでシート名一覧を取得する処理が、同期のたびに走っていた。URLが変わらない限りシート構成も変わらないので、chrome.storageにシート名をキャッシュし、URLが変更されたときだけAPIを叩くよう変更した。


ログタブの新設

エクスポートログを独立タブに移設し、インポートログ機能を新規追加した。

Codexにレビューを3回投げた。1回目でログのデータ構造の冗長さを指摘され、2回目でタブ切替時のステート管理の漏れを指摘され、3回目で最終確認。レビューのたびにコードを直して再投入する流れで、手元のセルフレビューでは見逃していた箇所が潰れた。


ルール同期の「詳細」ヘルプモーダル

ルール同期が何をやっているのか、ボタンの横に「詳細」リンクを追加してモーダルで説明を表示するようにした。同期の流れ(取得→比較→削除→インポート)を箇条書きで示す。自分以外が使うことを想定した最低限のヘルプ。


振り返り

一日で13件の改修を積み重ねた。DevToolsでブラウザの正規リクエストとコードのリクエストを横に並べて差分を取る作業を何度も繰り返した。CSRFトークン、FormDataフィールド名、ルールID照合 -- いずれも「正解の挙動を横に置いて、自分のコードとの差分を見る」で原因が特定できている。

スプレッドシートの書式クリア問題では、「値を消す」と「行を消す」の違いにたどり着くまでに3アプローチ試した。Sheets APIは値・書式・行構造がそれぞれ独立したレイヤーで、clearは値レイヤーだけ、deleteDimensionは全レイヤーをまとめて消す。この区別を体で覚えた。