はじめに
こんにちは。メルペイでBackend Engineerをしている Ryu Yamadaです。この記事は、Merpay Advent Calendar 2023 の10日目の記事です。
2022年4月に新卒で入社してから、メルペイの加盟店管理や加盟店精算を行うサービスの開発に携わっています。
2023年のハイライトは何と言ってもインボイス制度です。この記事を読んでいるみなさんも、経費精算などで大変な思いをしているのではないでしょうか。この記事では、メルペイの加盟店精算におけるインボイス対応について振り返ります。
ざっくり加盟店精算
メルペイでは月に1回や2回などの決められた精算サイクルごとに加盟店に対して発生した売上を精算して入金しています。そして、加盟店に提供している管理画面から入金の詳細をCSVファイルとしてダウンロードできるようにしています。
入金詳細ファイルには、売上金額、日次、売上のあった店舗情報や決済手数料などが記載されていて、各行が一つの取引に対応しています。
インボイス対応
さて、2023年10月1日からインボイス制度が始まりました。
メルペイがインボイス対応をしないと加盟店がメルペイを通して決済した代金の消費税を控除できなくなってしまうため、以下の対応が必要となりました。
メルペイの適格請求書発行事業者としての登録
メルペイが発行する入金詳細ファイルに消費税額やメルペイの登録番号等を記載し、適格請求書にする
メルペイが発行した請求書を7年間保存する
上2つの対応は軽微だったものの、請求書を長期間に渡って保存する要件へどう対応するかは検討する必要がありました。
加盟店情報の履歴テーブル
メルペイでは月1回などのサイクルで精算を行っていますが、入金詳細ファイルの作成は加盟店の管理画面からの請求をトリガーにして行っていました。また、事業者名や店舗名の変更履歴を保持する仕組みがなかったため、入金詳細ファイルの請求が行われた時点での値を記載していました。
しかしこの方式では、例えば5年前の入金詳細ファイルを請求された場合に、5年の間に事業者名や店舗名の変更があると正しくない請求書が作成されてしまう問題がありました。
そこで、インボイス対応として事業者名や店舗名の変更履歴を保存する履歴テーブルが必要になりました。
Spanner Change Streamを選択
加盟店の情報を保存するテーブルのスキーマのイメージは以下のとおりです。
CREATE TABLE Partners (
PartnerID INT64 NOT NULL,
Name STRING(MAX) NOT NULL, // 事業者名
// ・・・住所等・・・
UpdatedAt INT64 NOT NULL, // Unixtime
CreatedAt INT64 NOT NULL, // Unixtime
) PRIMARY KEY(PartnerID);
この変更を保持する履歴テーブルのスキーマはこのようになります。
CREATE TABLE PartnerHistories (
PartnerID INT64 NOT NULL,
Name STRING(MAX) NOT NULL, // 事業者名
// ・・・住所等・・・
UpdatedAt INT64 NOT NULL, // Unixtime
CreatedAt INT64 NOT NULL, // Unixtime
HistoryCreatedAt TIMESTAMP NOT NULL, // Timestamp 履歴作成時刻
) PRIMARY KEY(PartnerID, HistoryCreatedAt);
今回、Partnersテーブルに変更があったときに履歴テーブルであるPartnerHistoriesテーブルへの書き込みを行う方法を2通り検討しました。
アプリケーションで元(Partners)テーブルのレコードを挿入や更新した場合に履歴(PartnerHistories)テーブルへの書きこみも行う方法
Spanner Change Streamを利用し、DBレベルで、元(Partners)テーブルの変更をトリガーに履歴(PartnerHistories)テーブルへの書き込みを行う方法
さらに、インボイス対応にあたっては、Partnersテーブルだけではなく他のいくつかのテーブルにも履歴の作成が必要でした。
前者のロジックを作り込む方法では、元(Partners等)テーブルに書き込みを行うロジックすべての修正を行う必要があり修正範囲が広いこと、将来元テーブルを操作するようなロジックを追加する際に履歴テーブルへの書き込みを忘れると影響範囲がかなり大きくなってしまうことなどがネックでした。
後者のSpanner Change Streamを使う方法では、ロジックの改修から独立したDBレベルの機能として実現できること。また、加盟店精算ではメルペイ内で精算してから実際に入金を行うまでに数日以上開くため、履歴テーブルの要件として元テーブルと履歴テーブルの書き込みを同じトランザクションで行うことが求められなかったこともあり、最終的にこちらの方法を選択することにしました。
Spanner Change Streamで履歴テーブル構築
Dataflowを通してSpanner Change Streamを利用しました。
メルペイではこれまでDBのバックアップ用途などでは利用実績がありましたが、プロダクトでの利用は初めてでした。
履歴テーブルの作成に当たっては、元テーブルへの挿入(INSERT)と更新(UPDATE)の両方が履歴テーブルに対しては挿入としなくてはならない点に注意が必要でした。
ハマった点
最も困難だった点は、元テーブルのUpdatedAtがUnixtimeであったことでした。
元テーブルにUnixtimeの最小単位である1秒以内に複数の変更が行われた場合に、履歴テーブルにはUpdatedAtが同一の複数のレコードが挿入されますが、どのレコードが元テーブルの最終的な状態と一致しているかがわからない点が問題でした。
この例ではIDが1のレコードに対して1秒以内に2回更新をしています。挿入順序とHistoryCreatedAtの順序は必ずしも一致しないので、履歴テーブルからは”メルペイ2”と”メルペイ3”のどちらが最新の履歴なのかがわかりません。
この問題を解決するために暫定対応として以下のアプローチを取りました。
履歴テーブルのUpdatedAtにUnique Key制約をかけて、1秒以内に複数の変更があった場合には2つ目移行の挿入を失敗にする
履歴テーブルへの挿入失敗を監視するアラートを設定し、発生時には手動で確認する
インボイス制度の施行が迫っていたため暫定的な対応となりましたが、加盟店情報が短時間に複数回更新されることが少ないため、この対応でクリティカルな問題は起きていません。
恒久的な対応としてUpdatedAtのUnixtimeからTimestampへのマイグレーションを予定しています。
おわりに
ニュースでインボイス対応という言葉を知ったときには、経理ではない自分にはあまり関係がないだろうと思っていましたが、当事者として対応することになりました。
メルペイが成長してきた中で返しきれていない、UpdatedAtの型といった負債にも苦しみましたが、インボイス対応を完了することができました。今後もメルペイと加盟店をなめらかにつなぐプロダクトを作っていきたいです。
明日の記事は @fukutomiさんです。引き続きお楽しみください。