Kotlinで実装するStripe Charge

技術ネタ

今日の記事はStripe Advent Calendar 2018の8日目の投稿になります🌟

内容はタイトル通り、Kotlinで実装したStripeChargeについて書きまーす♪

スポンサーリンク

今回お話すること

サーバサイドKotlinのみです、フロントには特に触れません❗
一応フロントはAngularを使ってますが、StripeElementsを使っているので特に関係ないかなと。
Elementsの使い方は本題からそれるので、今後のアドベントカレンダーに期待しましょう

あと、結構長いです。
細かく書いたら結構な文量になってしまいました(;´∀`)

大まかなところ

Kotlinで実装はしてますが、SpringBoot使ってます。
サーバサイドのServiceはこんな感じ。

StripeCustomerApplicationService

import com.stripe.model.Charge
import com.stripe.model.Customer
import org.springframework.stereotype.Service

@Service
class StripeCustomerApplicationService {

    fun findStripeCustomer(customerId: StripeCustomerId): StripeCard =
        StripeCard.ofCustomer(Customer.retrieve(customerId.value))

    fun updateStripeCustomer(customerId: StripeCustomerId, registryStripeCustomer: RegisterStripeCustomer): StripeCard =
        StripeCard.ofCustomer(Customer.retrieve(customerId.value).update(registryStripeCustomer.toCustomerPrams()))

    fun saveStripeCustomer(registryStripeCustomer: RegisterStripeCustomer): StripeCard =
        StripeCard.ofCustomer(Customer.create(registryStripeCustomer.toCustomerPrams()))

    fun saveStripeCharge(stripeCharge: StripeCharge): StripeCharge =
        StripeCharge.ofCharge(Charge.create(stripeCharge.toRequestParam()))
}

ちょっと長いので改行してますが、実際のfunctionは4行です
RegisterStripeCustomerクラス以外は、このService層以降に持ち込まずにStripeのsdkのみで完結します。
こちらも大した実装してないので公開

RegisterStripeCustomer

data class RegisterStripeCustomer private constructor(
    val token: StripeToken
) {
    companion object {
        fun of(
            token: StripeToken
        ): RegisterStripeCustomer = RegisterStripeCustomer(
            token
        )
    }

    fun toCustomerPrams() = hashMapOf<String, Any> (
        "source" to token.value
    )
}

これだけで、Stripeへ顧客検索、登録、更新、支払いの登録が行えます。
実装コードだけ見たら結構簡単そうに見えませんか?
ぁ、ControllerはAPIのURL設定してService呼ぶだけなので今回は省きます。

※返り値のStripeCardはStripeSDKではなく、個人で実装したClassなので後ほど公開します。

Strip.Customer登録

準備

build.gradle

先程の冒頭で書いたとおり、SpringBootを使います。
Kotlinといっても設定はJavaと一緒なので、build.gradleに設定を書き込んでいきます。

Mavenしかない・・・

まぁ・・そっと閉じておとなしく以下のように書きます。

dependencies {
    compile('com.stripe:stripe-java:7.0.0')
}

きっと、Advent Calendarを見ているStripeの中の人が何かを感じ取ってくれると信じます。

BeanLifeCycle

StripePropertiesにプロパティファイルからSecretApiKeyを引き出すようにします

@Component
@ConfigurationProperties(prefix = "stripe")
@Validated
data class StripeProperties (
    var secretApiKey: String = ""
)

BeanLifeCycleクラスでStripePropertiesを読み込み、bootRun起動時にStripeの初期化処理をします。

@Component
class BeanLifeCycle(
    private val stripeProperties: StripeProperties
) {
    @PostConstruct
    fun initAfterStartup() {
        Stripe.apiKey = stripeProperties.secretApiKey
    }
}

これで初期化完了


Customer

Stripeのsdkを使ってAPIを叩く際に、必ず作らないと行けないのがCustomerオブジェクト
CustomerオブジェクトはTokenから作成することが出来ます。

fun saveStripeCustomer(registryStripeCustomer: RegisterStripeCustomer): StripeCard =
        StripeCard.ofCustomer(Customer.create(registryStripeCustomer.toCustomerPrams()))

そのため、フロントから受け取るJSONはこんな感じになります。

{
 token = "tok_mastercard"
}

Stripeではクレジットカード情報はtokenとして扱われるためセキュアになってます。
フロント側でStripeElementsを使用した場合、入力情報をStripe側で暗号化してTokenを発行してくれます。
そのためフロントからの通信の段階でtoken化されているので、Serverサイドではクレジットカード情報を取得すること無くそのままStripeに渡せます。

Server側の役割はRegisterStripeCustomerでtokenを受け取って、Customer.createに渡す。
そして、渡す際はHashMapである必要があるのでtoCustomerPrams()でMapに変換している。
個人的にはここでMapへの変換作業があるのはちょっとイケてない感がある・・・w

てなわけで、こんな感じのテストを流してみる。

@Test
fun test01_StripeへのCustomer登録() {
    val registryStripeCustomer = RegisterStripeCustomer.create(
        token = StripeToken.of("tok_mastercard")
    )
    val creditCard = stripeCustomerApplicationService.saveStripeCustomer(registryStripeCustomer)

    assertThat(creditCard.holderName.value).isEqualTo("null")
    assertThat(creditCard.brand.value).isEqualTo("MasterCard")
    assertThat(creditCard.lastNumber.value).isEqualTo("4444")
}

そしてStripe側のダッシュボードでログを確認すると、結果がこのように出ている。

ここで素晴らしいと思ったのが、Stripeに飛ばしているJSONとStripeが返してるJSONをログで表示してくれます。

Stripe側のPostされたJSON
それに対してStripeが返すJSON

JSONの中を見てみると、sourcesの中のdatacardの頭文字のIDがあります。
これがTokenで渡されたカード情報になります。

ここで作成された顧客を見てみます。

登録された顧客情報とカード情報
登録時に叩かれたAPIのログと、顧客情報に対してのイベント履歴

カード項目に最初からMasterCardが登録されています、これが先程JSONで返したカード情報です。
StripeはCustomer作成時にTokenで受け取ったCard情報を、自動的にCustomerに紐づけてdefaultのカードとして使用出来ます。

最後はCustomerAPIを叩いて返ってくるJSONをStripeCardクラスにラッピングしてます。

StripeCard

data class StripeCard private constructor(
    val customerId: StripeCustomerId,
    val cardId: StripeCardId,
    val holderName: CreditCardHolderName,
    val brand: CreditCardBrand,
    val expirationData: CreditCardExpirationData,
    val lastNumber: CreditCardLastNumber
) {

    companion object {
        fun create(
            customerId: StripeCustomerId,
            cardId: StripeCardId,
            holderName: CreditCardHolderName,
            brand: CreditCardBrand,
            expirationData: CreditCardExpirationData,
            lastNumber: CreditCardLastNumber
        ): StripeCard = StripeCard(
            customerId,
            cardId,
            holderName,
            brand,
            expirationData,
            lastNumber
        )
        fun ofCustomer (customer: Customer): StripeCard {
            val jsonNode = jacksonObjectMapper().readTree(customer.sources.retrieve(customer.defaultSource).toJson())
            return StripeCard(
                customerId = StripeCustomerId.of(jsonNode["customer"].asText()),
                cardId = StripeCardId.of(jsonNode["id"].asText()),
                holderName = CreditCardHolderName.of(jsonNode["name"].asText()),
                brand = CreditCardBrand.of(jsonNode["brand"].asText()),
                expirationData = CreditCardExpirationData.of(jsonNode["exp_month"].asInt(), jsonNode["exp_year"].asInt()),
                lastNumber = CreditCardLastNumber.of(jsonNode["last4"].asText())
            )
        }
    }
}

クレジットカードを1枚しか登録させない場合はこれでOKです。
もちろん複数登録させて、選べるような仕組みにすることも出来ます。
今回は1枚の登録のみで、再度クレジットカードが登録された場合は上書きする仕組みになってます。

Strip.Charge

やっとここまできた・・・w
実際のところ大変なのはCustomerまでで、Chargeはそれほど難しくない。
こちらが実装メソッドでStripeChargeが受け渡し用のクラス。

fun saveStripeCharge(stripeCharge: StripeCharge): StripeCharge =
    StripeCharge.ofCharge(Charge.create(stripeCharge.toRequestParam()))

  

StripeCharge

data class StripeCharge private constructor(
    val amount: StripeAmount,
    val currency: StripeCurrency,
    val customer: StripeCustomerId
) {
    companion object {
        fun create(
            amount: StripeAmount,
            currency: StripeCurrency,
            customer: StripeCustomerId
        ): StripeCharge = StripeCharge(
            amount,
            currency,
            customer
        )
        fun ofCharge(charge: Charge): StripeCharge {
            val jsonNode = jacksonObjectMapper().readTree(charge.toJson())
            return StripeCharge(
                amount = StripeAmount.of(jsonNode["amount"].asLong()),
                currency = StripeCurrency.valueOf(jsonNode["currency"].asText().toUpperCase()),
                customer = StripeCustomerId.of(jsonNode["customer"].asText())
            )
        }
    }

    fun toRequestParam() = hashMapOf<String, Any> (
        "amount" to amount.value,
        "currency" to currency.name.toLowerCase(),
        "customer" to customer.value
    )
}

ここで指定してるのはamountが金額、currencyが通貨、customerは先程登録したCustomerのID
StripeChargeクラスで金額、通貨、CustomerIDを受け取って、Charge.createに渡す。
そして、やっぱり渡す際はHashMapである必要があるのでtoRequestParam()でMapに変換している。これでStripeChargeの実装は完了。

※ofChargeメソッドは返ってくるJSONをStripeChargeオブジェクトに戻してるだけなので、直接は関係ないです。

今回もテストを流してみる。
さっきのCustomer cus_E7GIY4wX5sZd5Kをそのまま使います。

@Test
fun test04_StripeへのCharge登録() {
    val saveStripeCharge = StripeCharge.create(
        amount = StripeAmount.of(10800),
        currency = StripeCurrency.JPY,
        customer = StripeCustomerId.of("cus_E7GIY4wX5sZd5K")
    )

    val pointCharge = stripeCustomerApplicationService.saveStripeCharge(saveStripeCharge)

    Assertions.assertThat(pointCharge.amount.value).isEqualTo(10800)
    Assertions.assertThat(pointCharge.currency).isEqualTo( StripeCurrency.JPY)
    Assertions.assertThat(pointCharge.customer.value).isEqualTo("cus_E7GIY4wX5sZd5K")
}

ダッシュボードでログを確認すると、結果がこのように出ている。

登録されたCharge情報
受取と返り値のJSON

そして支払い情報を見てみるとこんな感じになってます。

これでStripeChargeでの支払いが完了しています!

まとめ

というわけで、ざっくりですが実装をまとめてみました❗
あらためて、ブログでまとめてるとどんな動きだったか思い出せてよかったです🌟

簡単にですが、良かった点、期待点もまとめました。

よかったところ

  • kotlinに限らず、とてもセキュアに支払い処理の実装ができる
    • クレジットカード情報とかリスキーな情報を見ること無くStripeに丸投げできる
  • カードが1枚なら、Chargeの際にカード情報を渡すこと無くCustomer情報だけで決済してくれる。
    • Customer作成時のCard情報をdefaultにしているので、カード情報を指定しなくていい
  • コード量はかなり少ない
    • JavaよりはKotlinのが圧倒的にすくない、でもKotlinは割と硬めな言語なのでもっと簡単に書ける言語はたくさんあると思う

 

今後に期待

  • Mapをなんとかしてほしいなーって思う
    • 途中までObjectとして扱えてたのが、最後Mapで送るのがなんか勿体無い
  • StripeChargeで渡す金額は税込み金額となります。subscriptionなどにはtaxプロパティがあるのですが、chargeのjsonにはtaxプロパティがないので税計算についてはアプリ側で計算となります。
  • Gradleについてはノーコメン

コメント