【ITニュース解説】Wiring an Accounting System into a Payment Webhook Without Losing Money
2026年09月11日に「Dev.to」が公開したITニュース「Wiring an Accounting System into a Payment Webhook Without Losing Money」について初心者にもわかりやすく解説しています。
ITニュース概要
決済システムと会計システムの連携では、重複登録による金銭的損失を防ぐため、べき等性の実装が不可欠だ。一時的なエラーは再試行し、データ不備によるエラーはすぐに停止するなど、エラーの性質に応じて処理を分け、全ステップの重複防止も考慮する必要がある。
ITニュース解説
システム開発において、異なるシステム同士を連携させることはよくある。特に、顧客からの支払いを受け付ける決済システムと、企業のお金の流れを管理する会計システムを連携させることは、ビジネスの根幹を支える重要なプロセスである。しかし、この連携を適切に行わないと、お金の記録が重複したり、失われたりするリスクがある。本記事では、この決済システムと会計システムの連携を、お金を失うことなく安全に行うための設計パターンについて解説する。
まず、一般的な決済処理のフローを見てみよう。顧客がオンラインストアで商品を購入し、Stripeのような決済サービスで支払いを完了すると、Stripeはその情報を特定のURL(これを「Webhook」と呼ぶ)に送信する。このWebhookの役割は、支払い完了の通知を受け取ることである。ニュース記事の例では、このWebhookが直接すべての後続処理を行うのではなく、支払い完了の通知を受け取るとすぐに、その注文を「ジョブキュー」と呼ばれる待機リストに追加する。 このジョブキューには「BullMQ」という技術が使われており、これは「Redis」という高速なデータベースをバックエンドとして利用している。キューに追加された注文は、その後「ワーカー」と呼ばれる別のプログラムによって順番に処理される。ワーカーが行う処理は多岐にわたる。例えば、顧客関係管理(CRM)システムに取引履歴を作成したり、バックアップデータベースに記録を残したり、配送システムに出荷依頼を送ったり、会計システムに勘定科目を記録したり、PDF形式の請求書を生成して顧客にメールで送信したりといった作業である。これらのステップは、通常、同じワーカー関数の中で一つずつ順番に実行される。
これらの処理の中でも、会計システムへの記録は特に慎重に扱う必要がある。例えば、CRMへの取引記録が誤って二重に作成されても、営業担当者がそれを手動で修正すれば済むことが多い。配送システムへの出荷依頼が重複しても、少しの修正で対応できる場合があるだろう。しかし、会計システムへの記録が二重になると話は全く異なる。同じ取引に対して二重に売上が計上されてしまうと、会社の財務状況が誤って認識されるだけでなく、消費税の申告など税金に関わる部分にまで影響が及ぶ可能性がある。もし誤った数字に基づいて税金を申告してしまえば、後で税務当局との修正手続きが必要となり、時間も手間も大きくかかることになる。 つまり、会計システムへの連携は、他の処理とは異なり、重複やサイレントな失敗(エラーが表面化しないまま処理が進んでしまうこと)が、金銭的な損失や多大な事務作業を発生させるリスクが非常に高いという特性を持つ。そのため、この部分の設計には特別な注意が必要なのである。
ワーカーが処理中に何らかの理由で失敗した場合、ジョブキューの仕組みとして、そのジョブを自動的に再試行する機能がある。これは非常に便利な機能で、一時的なネットワークの不具合や外部APIの一時的な障害などによって処理が中断しても、手動で介入することなく自動的に回復してくれる。しかし、この自動再試行が、会計システムへの二重記録という問題を引き起こす可能性がある。
例えば、ワーカーが会計システムに記録を送信した後、次のステップである配送システムへの連携で失敗したと仮定しよう。この場合、会計システムへの記録はすでに完了しているにもかかわらず、ジョブキューはジョブ全体を最初から再試行する。もし会計システムへの送信処理が何の対策もされていない場合、再試行時に同じ注文に対して二度目の記録が作成されてしまうのである。
この問題を解決するために、「冪等性(Idempotency)」という概念が非常に重要になる。冪等性とは、「ある操作を複数回実行しても、一度実行した場合と同じ結果になること」を指す。会計システムへの連携においては、同じ注文に対して、何度記録を送信しようとしても、結果として会計システムには一つの記録しか作成されないようにする必要がある。
具体的な実装パターンとしては、各会計記録に「デデュープキー(Deduplication Key)」と呼ばれる一意の識別子を付与することが挙げられる。このキーは、StripeのセッションIDや支払いインテントIDなど、二つの異なる注文で重複することのない安定した値から生成される。そして、会計システムに記録を送信する前に、まずシステム内部でこのデデュープキーがすでに処理済みかどうかを確認する。
この確認は、専用のデータベーステーブル(例えば accountingSyncLog)を用いて行う。記録を送信する際には、以下のステップを踏む。
- デデュープキーを使って、ログテーブルにそのキーが既に存在するかを確認する。
- もし既に存在すれば、その注文は既に処理済みなので、何もしないで処理を終了する。
- 存在しない場合は、そのデデュープキーを使ってログテーブルに新しいエントリを挿入し、ステータスを「保留中(pending)」とする。この挿入処理は、データベースのユニーク制約を利用して、複数のワーカーが同時に同じキーを挿入しようとした場合に、一つだけが成功し、他は失敗するように設計する。これにより、複数のワーカーが同時に処理しようとしても、安全に排他制御が働く。
- 挿入が成功したら、実際に会計システム(Netvisorなど)に記録を送信する。
- 会計システムへの送信が成功したら、ログテーブルのステータスを「完了(confirmed)」に更新する。
- もし会計システムへの送信中にエラーが発生したら、ログテーブルのステータスを「失敗(failed)」に更新し、エラー情報を記録する。そして、元のジョブを失敗させる。
このパターンにより、ワーカーが何らかの理由で再試行されたとしても、デデュープキーが既に存在するため、二重に会計システムへの記録が送信されることを防げる。ログテーブルのステータスを
pending / confirmed / failedの3段階で管理することも重要である。「失敗」は確実に処理が失敗したことを意味するが、「保留中」でスタックした状態は、処理が完了したかどうか不明であることを意味する。この違いを明確にすることで、手動での確認や再処理の判断を正確に行うことができる。
ジョブが失敗した場合、自動的にリトライする機能は便利だが、すべてのエラーが同じように扱われるべきではない。エラーには大きく分けて二つの種類がある。
一つは「到達不能エラー」である。これは、会計システムとの通信が一時的に切断されたり、タイムアウトしたり、サーバーが一時的にダウンしたりといった、データ自体に問題がないエラーである。このようなエラーは、時間をおいて再試行すれば成功する可能性が高い。デデュープキーによる冪等性が確保されていれば、安全に自動再試行できる。
もう一つは「拒否エラー」である。これは、送信したデータの内容に問題がある場合(例えば、無効なVAT税率が含まれている、未知の顧客IDであるなど)に、会計システムがリクエストを拒否するエラーである。このようなエラーは、何度再試行しても同じデータが送信される限り、必ず失敗する。自動リトライは無駄な試行を繰り返すだけで、問題解決の遅延を招くことになる。
したがって、システムは発生したエラーを適切に分類し、異なる対応を取る必要がある。到達不能エラーの場合は通常の再試行メカニズムに任せるが、拒否エラーの場合は、速やかに「復旧不能エラー」としてジョブを停止させ、人間が介入して原因を調査し、手動で修正・再実行できるようにするべきである。BullMQのようなジョブキューには、このような復旧不能エラーを明示的に通知するための機能(UnrecoverableErrorなど)が用意されている場合が多い。
ワーカーが実行する処理は通常、CRMへの記録、バックアップDBへの記録、出荷依頼、そして会計処理など、複数のステップで構成されている。これらのステップは順番に実行される。ここで注意すべきは、ジョブキューは「ジョブ全体」をリトライするのであって、「失敗した特定のステップだけ」をリトライするわけではないという点である。 もし、会計処理のステップでエラーが発生し、ジョブ全体が再試行された場合、会計処理以外のステップ(CRM、バックアップDB、出荷依頼など)も再び実行されることになる。もしこれらのステップが冪等性に対応していなければ、会計処理の再試行によって、意図せずCRMに二重の取引記録が作成されたり、バックアップDBに重複するデータが書き込まれたり、配送システムに二重の出荷依頼が送られたりする可能性がある。 この問題への対策は二つ考えられる。
- すべてのステップを冪等にする: 会計処理と同様に、CRM、バックアップDB、出荷依頼といったすべての外部連携ステップに対しても、それぞれ固有のデデュープキーとステータス管理の仕組みを導入し、何度実行されても結果が変わらないように設計する。このアプローチはコード量が増えるが、全体として非常に堅牢なシステムを構築できる。
- 進捗をチェックポイントとして記録する: ジョブの処理中に、どのステップまでが正常に完了したかを記録する仕組みを導入する。例えば、ジョブ自身のデータや、専用のステータス記録テーブルに「ステップ1完了」「ステップ2完了」といった情報を保存する。再試行時には、このチェックポイントを確認し、既に完了しているステップはスキップして、未完了のステップから処理を再開する。このアプローチはコードの重複を減らせるが、チェックポイントの記録と実際の処理との同期を正確に保つ必要があり、バグがあるとやはり重複書き込みのリスクが生じる。 どちらの方法を選択するにしても、このような対策を怠ると、一つの会計処理の失敗が、他のシステムに複数の重複データを生み出す原因となり、結果的に複数のシステムでの手動修正作業が必要になる事態を招く可能性がある。
ここまで見てきたデデュープキーによる冪等性、3段階のステータス管理、エラーの分類、そして全ステップの冪等化やチェックポイントといった設計は、それなりの開発コストを伴う。したがって、常にすべてのシステムに適用すべきというわけではない。 この設計が特に効果を発揮するのは、会計システムとの連携が非常に重要で、データの整合性が厳しく求められる場合や、ジョブキューによる自動リトライが頻繁に発生するような高ボリュームのシステムにおいてである。例えば、Stripeのような決済サービスと連携する場合、エラー時のリトライは日常茶飯事であり、自動リトライが複数回行われる可能性は高い。このような環境では、手動で会計システムと注文データベースを照合して不整合を見つけ出す作業は、非常に手間がかかり、人的エラーも発生しやすい。そのため、初期段階でこれらの設計を取り入れることで、長期的に見てエンジニアリングコストとエラー率の両方を削減できる。 一方で、非常に小規模な社内ツールで、会計記録のボリュームも少なく、万が一エラーが発生しても、担当者が毎日手動で帳簿を確認しているのでその日のうちに発見・修正できるようなケースでは、ここまでの複雑な設計は「過剰な設計」となる可能性がある。最初のバージョンでは、単純にエラーをログに出力し、人間が手動で対応するというシンプルな設計から始め、ビジネスの成長やボリュームの増加に応じて、徐々にこれらの堅牢な設計を導入していくというアプローチも有効である。
決済システムと会計システムの連携における重複書き込みやサイレントな失敗、そして一つのエラーがシステム全体の処理を滞らせるといった問題は、単なるバグとして後から修正するのではなく、システムの初期設計段階で考慮すべき重要なポイントである。デデュープキーによる冪等性の確保、エラーの適切な分類、そしてジョブ全体のリトライを考慮したステップごとの設計は、システムエンジニアが安全で信頼性の高いシステムを構築するために不可欠な知識であると言える。これらの設計を適切に取り入れることで、金銭的な損失や多大な手作業を防ぎ、ビジネスをスムーズに運営するための基盤を築くことができる。