決済処理の実装について振り返る - 決済システムの実装って難しいのか? -

## 概要

お久しぶりです。今年のアドベントカレンダーを見ていると、ちょうど[僕のやっていることにマッチしてそうなカレンダー](https://qiita.com/advent-calendar/2024/payjp-02)があったため、今年の振り返りをしつつ、決済処理の実装についての振り返り記事を書いていきたいと思います。

## 今年の業務内容

* C#で実装した共通決済ライブラリに新たな決済プラットフォームをサポート

クライアントアプリケーションに組み込む、各決済プラットフォームの決済を開始できるC#で実装した決済共通ライブラリの開発を引き続きやっていました。今年も新たな決済プラットフォームの追加を行いました。と言っても、今年追加したプラットフォームはサーバから決済トランザクションを開始するフローが多かったため、クライアント側で行った実装は、決済プラットフォームをサポートするようにインタフェースに設定を増やすことが主でした。

* 共通決済ライブラリのドキュメント執筆

本当は昔からあったのですが更新しづらい状況にあったので、ドキュメントの構成からやり直すために書き直していました。書き直せば書き直すほど、担当する前の部分の処理が、共通決済ライブラリという命名がついているライブラリを提供する立場として「この部分の処理を共通化したり、手抜くのはダメじゃね？」みたいなことが出てきたことも事実でした。この頃からインタフェースの共通化は良いけど、テンプレートを作ろうとか、テンプレートから派生して開発するって言うのはNGだなと考えるようにもなったりしました。

来年も基本的には決済ライブラリの開発・保守を続けていく予定です。

## 決済システムの実装って難しいのか?

以下、本記事を書くきっかけとなった[アドベントカレンダーの概要から](https://qiita.com/advent-calendar/2024/payjp-02)の引用です。

```
キャッシュレス決済やフィンテックが急速に発展する中、PAY.JPは多くの開発者や企業に採用されています。しかし、決済システムの実装には依然として高いハードルがあり、法律や規制の壁も存在するため、まだ知られていない情報も多くあります。
```

ここで言う決済システムと言うのが、PAY.JPのような決済機能そのものを提供するものを言っているのか、裏側はPAY.JPのような決済処理を提供するサービスに任せて、決済する処理を走らせる機能を提供するようなものを言っているのかよく分からなかったのですが、PAY.JPのアドベントカレンダーのテーマなため、恐らくPAY.JPのような決済サービスを使った上で、ネットで商品を購入できるシステムのようなもののことを言っているのかと思われます。

その上で決済システムの実装が難しいものなのか、筆者の経験に基づいた見解を述べていきます。

### 決済システムの実装の前提条件

* 販売する商品は消費型の商品のみであり、購入回数に制限がなく、無制限に購入できる

今回記事の内容の複雑化を多少避けるために、サブスクリプション、定期購入の商品は扱わず、購入回数に制限のない商品だけを取り扱うこととします。

## ネット上で商品を購入する際のシーケンス図

```mermaid
---
title: 購入フロー - 概要
---
sequenceDiagram
　actor User
  box Client
  　 participant App as クライアントアプリケーション
    participant PSDK as 決済プラットフォーム/有志が提供するSDK
    participant BR as ブラウザ
  end
  box Server
    participant SV as ショップサーバ
    participant SV2 as 商品振込サーバ
  end
  box Platform
  participant API as 決済プラットフォーム API
  participant Web as 決済プラットフォーム Web
  end
  participant Who as 誰か
  User->>BR: 商品を選択
  Note over App,BR: (1) ユーザがクライアントサービスから商品を選択する
  activate BR
    BR->>Web: 商品購入リクエスト
    activate Web
    Note over App,Who: (2) 誰かが決済トランザクションを開始する
    Note over API,Web: 決済トランザクション開始
    Who->>User: 決済内容の承認リクエスト
    activate User
      User-->>Who: 決済処理を実行
    deactivate User
    Note over App,Who: (3) 決済処理の結果、誰かが決済処理のレシートパラメータを受け取る
    Who->>SV2: receipt
    activate SV2
      Note over SV2: レシート内容を検証
      SV2->>SV2: 該当する商品を振込み
      SV2->>Who: トランザクション完了依頼
      BR->>Who: Serverからレシート消費依頼を送信できないものはClientから送る
      Note over App,Who: (4) 誰かがトランザクションを完了する
      Note over API,Web: トランザクション完了

    deactivate Web
    SV2-->>BR: トランザクション完了・商品振込成功レスポンス
        deactivate SV2
    BR->>User: 商品購入完了
  deactivate BR
```

ネット上で決済処理を行う場合、通信の切断、サーバ障害など決済処理の途中で異常終了してしまった場合に、ユーザの支払いだけが成功しており、商品が振り込まれていないなどのトラブルを防ぐために、決済開始時にトランザクションIDを発行します。

決済トランザクションは決済の処理が完全にOKかNGになるまでの一連の処理を一纏めの処理にしたものであり、これにより決済の処理の整合性を取る工夫をしています。
また、既に商品が振り込まれた決済結果を誰かが模倣して支払いしてないけど、商品だけ振り込むと言ったことを防ぐために、トランザクションを完了させるリクエストを送ります。このリクエストは、商品を振り込んだ後にクライアントもしくはサーバが送信したり、決済サービス提供者が決済処理完了後、トランザクションを完了する場合があったりします。

この前提を踏まえて決済システムの実装の難しさについて筆者の見解を述べていきます。

## 単一の決済サービス・あるいは単一のプラットフォームを利用して、決済機能を実装するならそれほど難しくない

上記のようなトランザクションに関するシーケンス図、あるいは決済の順序を説明している項目を、決済処理を提供するストアであれば処理の説明をするために用意しています。

SoftBank Payment Services:

https://developer.sbpayment.jp/system-specifications/link-type/3572/
https://developer.sbpayment.jp/system-specifications/api-type/3746/

App Store:

https://developer.apple.com/documentation/storekit/in-app-purchase?language=objc

Google Play:

https://developer.android.com/google/play/billing/integrate?hl=ja

Stripe:

https://docs.stripe.com/payments/checkout/how-checkout-works

決済機能を利用する場合は、決済サービスが説明しているフローに従うしかないので、言語が何であろうと書き方が違うだけで同じ処理になります。ここからズレることはありません。ズレたらその決済サービスを利用して決済できないという事になるため。

そのため、実装自体はそれほど難しくないことが大半です。~~(決済サービスが説明するシーケンス図の中で、決済結果が決済サービスから送られず、後からAPI叩いて問い合わせてくれと書いてあるシーケンス図もあり、「それシーケンス図じゃないじゃん」と頭抱えたものもありますが)~~

## 決済処理自体はOSがコンパイル可能な言語で書かないといけない

iOSやAndroidと言った、そもそもOS自体に決済機能が標準で搭載されているような端末では、そのOSのコンパイル可能な言語で書かないといけないことが多いです。iOSならObjective-CかSwift、 AndroidならJavaかKotolinですね。

とは言え、そのコード自体もそもそも、決済のフローに従うようなコードを書けばいいので(既に公式資料にサンプルコードが載っている)、言語の違いによる難しさもないような気がします。

## マルチプラットフォームで配信する場合でも、決済処理自体はOSがコンパイル可能な言語で書いた方がいい

アプリケーションをマルチプラットフォーム(iOS, Android, Windowsなど)で配信する場合は、今だとクロスプラットフォームな環境で書くことが多いかと思われます。C#ならUnity、DartならFlutter辺りでしょうか。しかし、C#やDartではそもそも決済関係のAPIを叩く方法を提供していないOSが存在したりします。そのようなOSでも配信、かつクロスプラットフォームな環境でアプリケーションを開発する場合は、配信するOSで、コンパイル可能な言語で実装し、C#やDartから異なる言語のメソッドを呼び出すような書き方にした方が良いです。

一応、UnityにはUnity IAPというマルチプラットフォームで決済対応をC#で書けるプラグイン自体は存在します(多分、Flutterにもそういったプラグインはあるはず)。

https://docs.unity3d.com/Packages/com.unity.purchasing@4.11/manual/StoresSupported.html

このようなプラグインを採用する時に考えなければいけないのが、サポートしていないプラットフォームも後から配信したい場合どうするの？と言った拡張性を考慮する必要もありますし、今でもUnity IAPのiOSの決済ライブラリはStoreKit1を採用しています。
StoreKit1は決済処理の結果(レシートデータ)をクライアントから送信することを前提とした作りになっており、クライアント側の通信の不安定さ、いつ・どのタイミングで送られてくるのか分からない状況下で、商品振込の処理に進められない不安定さがあり、採用する場合は利用するプラグインが、内部で利用しているライブラリを見て判断するのが良いかと思われます。

## クライアントからでしか決済処理の結果やトランザクション完了リクエストを送れない決済サービスの不安定さはしんどい

上記のクライアントの不安定さを踏まえての見解ですが、トランザクション完了リクエストをクライアントからのみ送る処理しか存在しない決済サービスはやっぱり難しいです。上記のシーケンス図で言えば、商品振込サーバ(ここでは決済が完了し、商品をユーザに振り込む処理を行うサーバとします)の処理でトランザクション完了リクエストを送るのが理想です。商品の振込み処理が終わったサーバからトランザクション完了リクエストを送るため、商品を振り込む処理に近い部分でトランザクションを完了した方が、トランザクションを完了させるタイミングの物理的な距離も限りなく近くなるため、安定さは増します。

とは言え、iOSのような[.finishTransaction()](https://developer.apple.com/documentation/storekit/skpaymentqueue/finishtransaction(_:)?language=objc), [.finish()](https://developer.apple.com/documentation/storekit/transaction/finish())がどうしても、クライアントからでしか送れない決済サービスが存在しているのも事実ですし、開発者としてはどうしてもそのフローに従わなければならないのは悩ましいところです。

## その他

* トランザクションIDのフォーマットは、決済サービスによって違うので、トランザクションIDを管理しやすく、決済でトラブル・不具合があった時にすぐに調査できる環境を用意しておく
* 消費型の商品とサブスクリプション、定期購入型の商品を同時にサポートしようとすると難しくなる
* サーバから決済トランザクション開始する決済サービスも存在する。サーバもサーバで単一の決済トランザクションの処理は難しくないが、マルチプラットフォームや消費型の商品とサブスクリプション、定期購入型の処理をサポートしようとすると、難しい

...etc

と、あまり長くなるのもどうかと思ったので、この辺りで区切ります。

## 決済処理を安定させたいことを第一に考えて、開発者のやりたいことを実現するためにはどうしたらいいかを考えて設計し続けていければ、それほど難しくはない...はず

実際には決済サービスが利用方法を書いているので、あくまで開発者としてはそのフローに従うのが大前提です。そのフローに従いつつ、開発者としても消費型だけじゃなくて、定期購入型の商品も販売したい、マルチプラットフォームで配信したいとか色々な思いがあって、その実現したいことを *ネット上の決済で安心・安全・安定した決済処理を実現するためにはどうしたらいいのか* という考え自体がブレなければ、それほど難しいことにはならないはずです。

と言っても、恐らくそれは決済処理に限った話ではなく、どのカテゴリ・どのジャンルでも安心・安全・安定させたいのはそうかなという雰囲気があるので、決済処理自体に言える話にもならないかなとも思います。

## まとめ

筆者も決済関係の業務はまだ一年くらいなため、来年も工夫はしつつも、考え方自体は変わらないかなと思います。あんまり技術的な話はしていないですが、この記事が何かの役に立てれば幸いです。

それでは、良いお年を。