Scalaで実装されたPAY.JPの非公式クライアント

決済プラットフォームのAPIクライアントをScalaで作ってみようと思い、手始めにPAY.JP版を雑に作ったので共有します。

成果物はここ。
https://github.com/j5ik2o/pay-jp-scala

## 使いかた

`build.sbt`に以下を追加

```scala
resolvers += "Sonatype OSS Release Repository" at "https://oss.sonatype.org/content/repositories/releases/"

libraryDependencies += "com.github.j5ik2o" %% "pay-jp-scala" % "1.0.4"
```

`ApiClientContext`から`MerchantApiClient`もしくは`PlatformApiClient`を生成して使ってください。ほぼすべてのAPIをカバーしています。各APIがクライアントクラスのメソッドに対応付くように実装されています。メソッドの戻り値は`monix.eval.Task`です。

```scala
import monix.execution.Scheduler.Implicits.global
import scala.concurrent.Await

val apiClientContext: ApiClientContext   = new ApiClientContext("api.pay.jp", 443)
val merchantApiClient: MerchantApiClient = apiClientContext.createMerchantApiClient(sys.env("MERCHANT_SECRET_KEY"))

// Charge API
val future = merchantApiClient.createCharge(
  amountAndCurrency = Some((Amount(10000L), Currency("jpy"))), 
  productId = None,
  customerId = Some(user1.id),
  tokenId = None,
  description = None,
  capture = None,
  expiryDays = None,
  metadata = Map.empty,
  platformFee = None).runAsync
  
val result = Await.result(future, 3 seconds)

apiClientContext.shutdownSender()
```

### `monix.eval.Task`

`monix.eval.Task`は、非同期計算を制御するデータ型です。という話をすると`scala.concurrent.Future`を想起しますが、`Task`を生成した際に実行をトリガーしません。`#runAsync`, `#foreach`などによって初めてトリガーされます。詳しくは以下のドキュメントを参照してください。

https://monix.io/docs/2x/eval/task.html#introduction

## 実装

### トランスポートはakka-http

`cachedHostConnectionPoolHttps`によってコネクションを管理します。コネクションプールの設定を調整したい場合は、[公式サイト](https://doc.akka.io/docs/akka-http/current/client-side/host-level.html#configuring-a-host-connection-pool) をみてください。

https://github.com/j5ik2o/pay-jp-scala/blob/master/library/src/main/scala/com/github/j5ik2o/payjp/scala/HttpRequestSenderImpl.scala#L27

```scala
  private val poolClientFlow = 
    Http().cachedHostConnectionPoolHttps[Int](config.host, config.port)
```

### コネクションを維持したままリクエストを受け付ける

リクエストの都度、RunnableGraphを作っていては非効率なので、`akka-stream`の`SourceQueueWithComplete`を使って、常時起動しているストリームに対してOfferできるようにします。

```scala
  private val requestQueue: SourceQueueWithComplete[PromiseWithHttpRequest] = Source
    .queue[PromiseWithHttpRequest](config.requestBufferSize, OverflowStrategy.dropNew)
    .map {
      case p @ PromiseWithHttpRequest(_, request) => (request, p)
    }
    .via(sendRequestFlow)
    .map {
      case (triedResponse, PromiseWithHttpRequest(promise, _)) =>
        promise.complete(triedResponse)
    }
    .toMat(Sink.ignore)(Keep.left)
    .run()
```

https://github.com/j5ik2o/pay-jp-scala/blob/master/library/src/main/scala/com/github/j5ik2o/payjp/scala/HttpRequestSenderImpl.scala#L113-L124

ちなみに、ストリームの終了は`#shutdown`で可能です。内部的には`SourceQueueWithComplete#complete`を呼び出し完了を待つだけです。

```scala
  override def shutdown(): Unit = {
    requestQueue.complete()
    Await.result(requestQueue.watchCompletion(), Duration.Inf)
  }
```

https://github.com/j5ik2o/pay-jp-scala/blob/master/library/src/main/scala/com/github/j5ik2o/payjp/scala/HttpRequestSenderImpl.scala#L126-L129

### モデルとJSON

レスポンスモデルはこちら。

https://github.com/j5ik2o/pay-jp-scala/tree/master/library/src/main/scala/com/github/j5ik2o/payjp/scala/model

JSON化には`io.circe`を使っていますが、`io.circe.generic.auto._`がうまく使えなかったので`forProduct??`を使って気合いで実装。

```scala
case class Customer(id: CustomerId,
                    liveMode: Boolean,
                    emailOpt: Option[String],
                    descriptionOpt: Option[String],
                    defaultCardIdOpt: Option[String],
                    metadataOpt: Option[Map[String, String]],
                    cards: Collection[Card],
                    created: ZonedDateTime)

object Customer extends JsonImplicits {

// ...

  implicit val CustomerDecoder: Decoder[Customer] =
    Decoder.forProduct8("id", "livemode", "email", "description", "default_card", "metadata", "cards", "created")(
      Customer.apply
    )

}
```

## 作ってみた感想

### テスト用カードトークンが発行できないのでCIが実行できない

セキュリティの観点でサーバサイドでカード情報を扱わないトレンドがあるわけですが、CIするときはテスト用カード情報が扱えないとテストが書けません…。当初はなかったのですが、PAY.JPに相談したら早速作ってくれました。ありがとうございます！

https://pay.jp/docs/api/#token-%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3
`X-Payjp-Direct-Token-Generate: true`を指定してリクエストするとテスト用トークンが取得できます。

### APIドキュメントを見ても仕様がわかりにくい

APIドキュメントがわかりにくかった。些細なものとしては以下。

- パラメータが必須か任意かわかりにくい
- レスポンスに含まれる値がnullを返すか返さないかがわからない
- 列挙値の値の範囲がわかりにくい

### 集約を中心としたエンドポイント設計になっていない

一番わかりみが少ないと思ったのは、以下のエンドポイント仕様。

- 顧客の定期課金リストを取得

https://pay.jp/docs/api/#%E9%A1%A7%E5%AE%A2%E3%81%AE%E5%AE%9A%E6%9C%9F%E8%AA%B2%E9%87%91%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E5%8F%96%E5%BE%97

`/v1/customers/${customerId}/subscriptions` 

エンドポイントだけをみると、顧客オブジェクトが持つ定期課金を集合で返すように見える。つまり、定期課金の集合は顧客オブジェクトの一部であるように見えるが、顧客オブジェクトを返すエンドポイントには定期課金は含まれていないし、定期課金だけを取得するエンドポイントがある。つまり、定期課金は顧客とは関連はあるものの、独立したオブジェクト(DDDでは集約と呼ぶ)だった。

独立したオブジェクトであれば、エンドポイントは以下のようになるのでは？と個人的に思った。これに類したAPIが他にもいくつかあった。

`/v1/subscription?customer_id=${customerId}` 

ので、APIクライアントでも以下のようにわかりやすい名前に変えている。

- getSubscriptionByCustomerId(顧客の定期課金の取得)
- getSubscriptionsByCustomerId(顧客の定期課金リストの取得)
- getChargesByTransferId(入金に対応する支払いリストの取得)

イケてないところばかり書いてしまって恐縮ですが、問い合わせすると迅速に対応してくれるのでそれはよかったです。まぁ、わざわざ自前でクライアント実装するからこういう話になるだけで、この辺の[公式ライブラリ](https://pay.jp/docs/library)を使うといいと思います。