Go言語での決済システムとマイクロサービス化について

x00

この記事はeureka Advent Calendar 2016の22日目の記事です。
21日目は弊社たこちゅーによるGo言語初心者がハマった2つのポイントでした。
 

ちなみに彼はよくLGTM画像にされていますので、ご自由にお使いください←
 

x01
 

まえがき

はじめまして、コンバンハ。森川です。
2016年も終わりに近づき、日に日に寒くなってきましたね。
 

あまりにも寒すぎて、オフィスのある外苑前から帰宅するたびに
「これはアカンやろ…アカンやろ…」状態だったので、半年間クリーニング店に放置してあったコートを取りに行くという、2016年でも一二を争う決断をしました。(英断)
 

平日に行ったため閉店が近い時間だったのですが、そのクリーニング店はガラ空きでした。
店員さんは2名いて、「2人も必要なのかな…」と思いながらコートの伝票を渡した所、中々見つからなかったらしく2名がかりで探しはじめ、3分経っても見つからず、
そうこうしている内に僕の後ろに4,5人くらい並び始めました。
 

これをちょっとインテリ・アンニュイ・メガネ男子っぽく表現すると、2つのワーカーが並行処理をし、タスクが詰まっているところに、待ち行列にさらに5件エンキューされはじめました。
そうすると、僕の後ろの人々が閉店時間というガベージコレクターによって回収・破棄されてしまうのではないかとビクビク怯える気持ちと、
「今、この場を支配しているのはワタシ」という相反するキモチがフツフツと湧いてきて、ニヤニヤしそうでした。(フィクションです)
これはインテリでもアンニュイでもなく、ただのメガネおっさんWebエンジニアですね。
 

というわけで、今日は決済周りの ボディをえぐられるような非常に苦しい経験 あれこれについてお話したいと思います。
合言葉は、「これでみんな決済マスターだ!」です。
 

はじめに

この記事の対象者は↓のような人々です。

  • 決済処理は怖いと思っている人
  • 決済処理は恐ろしいと思っている人
  • 決済処理に関わってしまうと、もはや永久に安眠することができないと思っている人

 
この記事で学べることは以下の通りです。

  • 決済処理はそこまで怖くない
  • 上司の首は簡単には飛ばない

 
この記事の注意事項は以下の通りです。

  • 記載されている内容・仕様が最新の情報ではない可能性があります
    • 各決済プラットフォームのアップデートにより変更される可能性が高いです
  • 経験からの帰納や推測を多分に含んでおり、正しくない可能性があります
    • (圧倒的力不足で申し訳ないです。間違っていたら教えてください…!)

目次

 

うーん、長いですね…^^;
 

A. システム構成

最初にマクロな視点でpairsを含めた全体の構成、そして少し詳細な決済システムの構成について説明いたします。
 

全体のシステム構成

以下の図はpairsと決済システムの大まかな構成図です。
 

a02_design
 

  • 左側の水色っぽい(1)の箇所がpairs
  • 右下のピンク色の(2)の箇所が決済システム
  • 右上のピンク色の(3)の箇所が外部の決済プラットフォーム

となっています。
 

システムはほぼ全てAWS上で動かしており、アプリケーションはEC2で動作しています。
本番ではDockerはあまり使っておらず、一部の機械学習系システムだけPythonコンテナを 雑に 使ってるくらいです。
 

Go言語という以外は、オーソドックスなAWSを利用したWebアプリケーションになっていると思います。
 

最近加えた変更としては、アプリケーションが出力するログをGoogle Stackdriver Loggingで一元化するようにしたことです。
各ログに含まれるのパラメータを自由にセットしたかったので、fluentd経由ではなく、Goのログライブラリsirupsen/logrushook経由で送っています。
 

なお工事中のKinesisは、Google BigQueryへ送っているログの一時置きとして検討・構築中です。
 

「SaaS」では似たようなものを色々と使っていますが、ここについてはまたの機会にお話しします。
(一生話さないフラグ)
 

決済システムの構成

決済システムはpairs側に比べると非常にシンプルな作りになっています。
 

a03_design

pairsとはRESTのインターフェースで通信を行うようになっています。
 

DBにはAWS Auroraを使っています。
弊社だけの決済系なので、DBに負荷がかかることはないのですが、pairs側への導入にあたって検証利用という形でずっと使い続けています。
 

SQSは課金の自動更新に使用しています。
 

負荷も安定しておりキャッシュが必要な箇所はほぼないため、memcachedやRedisは使用していません。
 

決済システム マイクロサービス化の背景

2015年、2016年にかけてpairsのサーバーサイドアプリケーションをPHPからGoへと置き換えましたが、その時はモノリスなアプリケーションで、決済もその中に組み込まれていました。
私が入社した当初は、原始地球ともいうべきカオスな様相を呈しており、カスタマーによる購入処理と、バッチによる自動更新処理で違う処理関数を使っていて、そこら中に星くずのように煌めく様々なマーケティングキャンペーンのロジックが散りばめられており、恵比寿にいながらにして、さながら九龍城に迷いこんだ気分を味わうことが出来ました。
ドキドキ・ロマンティックですね。
 

“新商品の追加を行うと、PC版でカスタマーの新規登録ができなくなる。”

『pairsの開発中の話』より (共著: 小島ヒロキ・森川タクマ)

こんな名言を思わずつぶやいてしまうような、あってはいけない副作用が起こり、まるでジェンガをやらされている気分でした。
ドキドキ・ファンタスティックですね。
 

エブリデイがサマー・オブ・ラブ、そんな厳しい冬の虚無僧の時代を経たために、
「もう我々の子孫にこんな辛い思いはさせたくないでござる!」と思うのは当然のことで、私たちはGo版リプレイスにあたって、マイクロサービスとして切り出すことにしました。
人類(エウレカ)の悲願ですね。
 

B. 決済の種類

多くの決済プラットフォームで使われる決済には2種類あります。
 

  • (a) 購入すると1回だけ支払いがあるもの
    • 例: ZOZOTOWNでの商品の購入、LINEでのスタンプの購入、Amazon Kindle向けの電子書籍の購入
  • (b) 購入すると定期的に支払いがあるもの
    • 例: 定額制サイトやサービスへの登録(着メロ取り放題、Yahoo!プレミアム、evernote、Netflix、インターネット契約)

ここではStripeさんやWebPayさんに倣って、
 

  • (a)のような単発の決済は「課金」(charge)
  • (b)のような自動更新のある決済は「定期課金」(subscription)

と呼んでいきます。
 

弊社のサービスのビジネスモデルはサブスクリプションモデルであり、特に(b)の定期課金が重要になってきます。
サブスクリプションモデル、特にtoC向けは収益がいきなり落ちることはめったに無いため、比較的安定しやすくなります。 そこまでたどり着くのは簡単ではないですが…
 

そして自動的に引き落とされるということは、その分システムの安定性が求められることになります。
 

決済システムの修正を担当したことがある人であれば、
「もし明日の朝出社して、数千件、数万件が自動で引き落としされてたらどうしよう…」
とか寝る前に考えたことがあるのではないでしょうか。
そして「逃げちゃダメだ…逃げちゃダメだ…いや..逃げよう…」と何度もつぶやいたことがあるはずです。
 

C. 決済プラットフォーム

一旦視点を変えまして、外部の決済プラットフォームの話をします。
先程の図の「3」の箇所となります。
 

a04_design

現在、pairsでサポートしている決済手段は以下の4つです。
 

  • クレジットカード
  • iOSのIAP (iTunes Connect In-App Purchase)
  • AndroidのIAB(Google Play In-app Billing)
  • PayPal(台湾版のみ)

iOS, Android等のネイティブアプリケーションでは、扱っている商品によっては必ずIAPやIABを使って決済をしなくてはなりません。
 

カスタマーにしてみればAppleやGoogleに対して支払うことになり、安心感がありますし、購入手続きや月々の明細の管理も楽です。
 

そんないいことづくめなネイティブアプリのプラットフォーム決済ですが、手数料が高いです。
購入価格の30%が手数料としてプラットフォームに取られるため、100万円売上があったとしても30万円は手数料として消えてしまい、実質手元に入るのは70万円です。
美味しすぎる手数料ビジネスですネ
 

それに対してクレジットカードやPayPalは通常の手数料が約3%〜5%、さらに一回当たりのシステム手数料(トランザクション手数料)として10円〜50円ほどかかります。
 

例えばPayPalの手数料はこちらに記載されています。
 

この場合に、1個1000円の商品を千個売って月に100万円の売上があったとすると、
 

  • 通常の手数料で3.4%で 3.4万円 (*100万1円からは3.2%, 1000万1円以上は2.9%)
  • 40円*千件 = 4万円

7.4万円が手数料として引かれ、92.6万円が手元に入ることになります。
 

サブスクリプションモデルの場合は、購入済みのシステムを勝手に切り替えることはできないので、手数料、安全性、安定性、カスタマーにとってのUX、実装・運用工数等々、様々な要因を考えた上で、対応する決済プラットフォームを考える必要があります。
 

以下の比較表に簡単な違いをまとめています。
 

プラットフォーム 手数料 1ヶ月のサイクル 課金作成API 定期課金の解約API
クレジットカード 2〜5% + 数十円 自由 or 1ヶ月(独自) (代行会社次第)
PayPal 2.9〜3.6% + 40円 1ヶ月(独自)
iOS IAP 30% 1ヶ月(独自) × ×
Android IAB 30% 1ヶ月(独自) ×

手数料を考えるときは、単純にトランザクション手数料の他にも、返金・チャージバック時の手数料とカスタマーの返金率を考慮して、トータルの手数料で考えると良いと思います。
国によっては返金率が高くなることもあり、ベンダーの返金手数料が高い場合は想定以上の手数料となる可能性があります。
 

1ヶ月のサイクル というのは、引き落としが行われるサイクルになります。
例えばpairsでは、クレジットカード決済の場合、1ヶ月プランを30日というサイクルで販売しています。1ヶ月は30日だったり31日だったりするため、31日の場合は同月に2回引き落としがされる可能性もあります。
 

決済プラットフォーム間では、1ヶ月の期間判定が異なる場合があります。
通常の場合は月だけを+1することが多く、例えば7月1日に購入した場合は8月1日に更新されます。ただし、2月というイレギュラー月をまたぐ場合は、各社で異なる期間判定をされる場合があります。
例えばiOS IAPの場合は1月29日〜31日のどこで決済しても、次の更新日は3月1日になり、その次の更新日は3月29日になる一方で、Android IABの場合は1月29日〜31日で決済すると、次の更新日は2月28日になるが、その次の更新日は元に戻り、3月は1月の購入日と一致する、といった具合です。
 

この辺りの挙動は、仕様を正確に把握して事前に定義しようと思っても難しいので、素直にプラットフォームから返却される有効期限を使うようにしましょう。 Yes as is.
 

課金作成APIというのは、こちらで自由に決済ができるかどうかということになります。
「自由」といっても一度決済を行った経験があり、プラットフォーム側にクレジットカード情報があることが前提条件となります。
多くのクレジットカード決済代行会社の場合は、前回の決済時のユーザーIDやメールアドレス、電話番号といった情報を使うことで、同じクレジットカードでの引き落としが可能です。
 

代行会社側で自動引き落し機能が付いている場合もありますが、pairsではエウレカ側で毎回決済リクエストを送っています。
こちらがリクエストを送信しない限り決済されることはなく、pairs退会済みのカスタマーから引き落とされることはないため、この点は安心ですが、エラーハンドリングを適切に行わなかったり、ステージングと本番を間違えるような致命的なミスを犯すと、多重に引き落としが発生する可能性があるため注意が必要です。
 

多重引き落としの場合にも、すぐに気づけば返金処理を行うことでカスタマーに迷惑をかけず、上司の首が飛ぶのを防ぐことができますが、誤決済の件数が多い場合は返金処理が一気にできない場合があります。
アクワイアラは国際ブランドに対して「量が多く質の良い決済」の獲得を代行している立場上、返金率・回数が想定を大きく上回ると
「こんなに返金しやがってお前んとこは一体どうなってんだ」となってしまい、1日や1ヶ月での上限が決まってるんじゃないかと、ワタシは推測しています。
 

クレジットカード

各プラットフォームについてですが、まずはクレジットカードについて説明していきます。
 

日本ではクレジットカードが普及しており、オンライン決済手段として一般的なものとなっています。
 

入力フォームにカード番号や有効期限を入力するのが一般的かと思います。
システムやカード会社によっては、CVC(カード番号とは別にカード上に記載されている3,4桁の暗証番号のようなもの)やパスワードを入力させるものがあるかもしれません。
 

クレジットカードにはみなさんご存知の通り、VISA, MasterCard, JCB, アメックス, ダイナース といった国際ブランドがあります。
 

これとは別にカード発行会社があり、この辺りを話すとキリがないため省略しますが、アルファノートさんの「決済システムの仕組み」という図が分かりやすかったのでリンク先を参照してもらえると、少しイメージがつきやすいと思います。
 

図の中の「加盟店」が私たちのようなサービス提供会社にあたります。
サービス提供会社やエンジニアにとって直接関係があるのは、決済代行会社(アクワイアラ・プロセッサ)と呼ばれるその名の通り決済を代行してくれる事業者と契約し、彼らのシステムと接続して決済を行います。
 

決済代行会社によって、以下の事項が変わってきます。

  • (a) 利用できるカードブランド
  • (b) 手数料
  • (c) 決済システムのAPI

(a)は飲食店や海外のお店などであると思いますが、「VISAとMasterCardは使えるけど、JCBとアメックスは使えない」のようなケースです。
 

(b)も決済代行会社によって変わってきます。
決済の取引金額が大きくなると、手数料が安くなることが多いです。
また利用するカードの国際ブランドによって変わることもあります。
例えば、「VISAとMasterCardは3.5%で、その他は3.8%」 のようになることがあります。
 

(c)はエンジニアとしては一番気になる箇所ですね。
APIやライブラリがいかに使いやすく、どのくらい機能が豊富かによって、実装工数やアーキテクチャ、稼働後の運用の負荷、実現できる機能が変わってきます。
 

例: 自動引落し機能、カード情報の一部表示機能(下4桁表示)、サンドボックス環境、etc…
 

多くの決済代行会社を利用するには審査や導入費用が必要ですが、最近はStripeSPIKEのように、導入が簡単で使いやすいAPIを持ったクレジットカード向け決済プラットフォームがあるので、そちらを検討するのも良いかもしれません。
 

通常クレジットカード情報を取り扱うには、PCI DSSというクレジットカード向けの国際的なセキュリティ基準の認定資格を取得する必要があります。
ISMS(ISO27001)Pマークのようなもの)
 

クレジットカード情報入力フォームをカスタマーへ直接提供する場合は、クレジットカード情報を取得することになり、代行会社の審査時にPCI DSSの準拠・認定を求められます。
 

PCI DSS取得も簡単ではないですし、システム・ネットワーク構成も厳重にする必要があります。
そしてクレジットカード情報なんて怖くて取得・保管したくありません。
 

そういう用途のために、iframeURLリダイレクトを使って決済代行会社のシステムへ接続し、間接的に決済を行う方法があります。
pairsでもそれを利用しています。
 

b01_iframe
 

一度決済を行ってしまえば、該当カスタマーのクレジットカード情報は決済代行会社に保存されているため、
次からはフォームに入力したメールアドレスや電話番号等の情報を使って、pairs側から簡単に決済リクエストを送信することが出来ます。
 

PayPal

PayPalは元々、個人間の送金サービスとして発展してきた決済プラットフォームです。
PayPalの追い立ちは英語のwikipediaに譲るとして、現在では欧米系サイトや日本でも対応しているサービスが多いと思います。
2015年のAnnual Reportを見ると売上高は92億ドルで19%成長(!)、1.79億人の有効なアカウントがありこちらも11%成長となっており、今も伸び続けているようです。
 

そんなPayPalで使える決済のパターンはいくつかありますが、それだけで1記事になる、というか既に誰か説明しているでしょ!
ってことで調べてみたところakiyoko blogさんの【PayPal 決済まとめ】PayPal の決済システムが分かりにくいので 画面遷移パターンごとに使える決済システム・API を整理してみたに詳しく記載されています。
 

かいつまんで説明すると、
 

  • 元サイト上で、iframeかURLリダイレクトでPayPalのログイン画面を出す
  • PayPal上で決済(または決済許可)を行う
  • PayPal側から元サイト上へ決済完了通知(または決済許可トークン付きで再リダイレクト)
  • (決済許可トークンを使って決済を実行)

という流れで、直接クレジットカードの番号を扱わない仕組みになっています。
ええ、あなたの言いたいことは分かります。「文字だけじゃ分かりづらい」ですね。
まあ落ち着きましょう。
 

  • pairs 購入画面
    b02_paypal01

オレンジ色の購入ボタンを押すとPayPalへ遷移します。

  • PayPal ログイン画面
    b02_paypal02

ログインすると、PayPalに登録されているクレジットカードで支払い確認画面が出ます。

  • PayPal 支払い確認画面
    b02_paypal03

社長のカード(色付き)で好きな買い物をする、最高の瞬間ですね!
 

「同意して続行」を押下すると決済トークン付きで元サイトへ遷移します。
この時点ではまだ決済はされていません。

  • pairs 最終購入確認画面
    b02_paypal04

購入完了ボタンを押すと、PayPalから受け取った決済トークンを使用して決済処理を行うようになっています。ここで初めて実際の決済処理が行われます。(決済トークンには有効期限があるため、この状態で一定時間が経過すると無効になります。)

使用できるAPIにも古くからありClassic APIと呼ばれているNVP / SOAP APIか、REST APIの2種類があります。
 

pairsでは台湾版でPayPal決済を使っています。
実装当時はREST APIが台湾・台湾ドルをサポートしていなかったため、Classic API のExpress Checkout機能を使っています。
現在ではREST APIも使えるようです。もっと早く欲しかった…(;O;))
 

参考までに、pairsでの決済の流れは以下の通りです。
 

  • (1) カスタマーがpairsの決済ページを開く
  • (2) カスタマーが決済手段としてPayPalを選択し、購入ボタンを押す
  • (3) pairs側でSetExpressCheckout APIを使い、カスタマーをPayPalへリダイレクト
  • (4) カスタマーはPayPalサイト上でログインし、購入確認ボタンを押す
  • (5) PayPal側でカスタマーをpairsへ再リダイレクト。リダイレクト時に決済トークンが付いてくる
  • (6) カスタマーはpairs上で最終確認ボタンを押す。
  • (7) pairs側(実際はeureka決済システム)で決済トークンを用いて、DoExpressCheckout APIを使い決済を行う
  • (8) 定期課金の場合は、CreateRecurringPaymentsProfile APIを使い、定期的に引き落としがされるようにする。(GetRecurringPaymentsProfileDetails APIを使い、正常に定期課金プロファイルが作成されているか確認もする)

ええ、あなたの言いたいことは分かります。「文字だけじゃ分かりづらい」ですね。(再掲)
 

(1)〜(6)までは上掲のスクリーンショットがカバーしています。
(7)〜(8)は↓のようなイメージです。

b02_paypal05
(安定のパワーポイント作図)
 

たまに(7)の決済が成功し、(8)の定期引き落とし処理に失敗することがあります。
こういう場合はエラーとして購入失敗にし、決済については返金処理をしています。
(たまにしか発生しないこと、そして(8)でエラーがでるということは、返金処理が失敗する可能性も高いことから今のところは手動運用にしています。運用チームに多謝。)
 

PayPal管理画面

PayPalの販売者向けの管理画面では、課金データの確認や、返金処理、CSVデータのダウンロード等が可能です。
 

マスターアカウントから個別ユーザー用のアカウントが発行できます。
特定の権限を絞ってアカウントを作成できるため、担当者ごとにきちんと作った方が良いですね。
 

元々はマスターアカウントだけで1Password Teamに入れてましたが、危うさの限界線を超えたために個別アカウントを発行するようになりました。折角1Passwordで乱数発行しても、アカウント作成時のパスワードは手入力を求められるため、「最重要サービスのパスワードは最低限30桁以上を要求するぜ!」みたいな社内ポリシーがあると、何回も打ち間違えて詰むので気をつけてください。
 

サンドボックス環境

大抵の決済プラットフォームにはサンドボックス環境と呼ばれる、開発向けの環境が用意されています。
 

ここでは本番さながらに決済を行えますが、実際にお金が引き落とされないという素晴らしい環境です。
 

PayPalもサンドボックス環境があり、接続先のURLが異なっています。
サンドボックス独自ルールが少ないため使いやすいですが、PayPal社のステージング環境として使われているのか、たまに本番と違う新UIになっていたりします。
 

そういった関係なのか分かりませんが、2ヶ月に1回くらいは数時間くらい使えなくなります。
その間はPayPalでの決済テストが一切できなくなり、当日リリースとかだったりすると、
「PayPalで決済テストができません」となぜか私に相談がきます。
 

そんなときは早く帰宅して寝てもらっています。
 

iTunes Connect In-App Purchase (iOSアプリ)

続いて我らがキング、Apple社がiOS向けに提供する決済プラットフォームになります。
iOSアプリケーションでは、アプリ内課金のためにIn-App Purchase(以下、IAPと表記)を使うことが出来ます。
 

大体のことは公式のIn-App Purchase プログラミングガイドに書いてあります。
詳しくはそちらを参照してください。
 

PDFのP9,P10辺りの「プロダクトのタイプ」という項目で、商品の種類が5種類記載されています。このうち、pairsでは、消耗型(Consumable)と自動更新購読(Auto-renewable subscriptions)を使用しています。
 

消耗型は「pairsポイント」のような、消費するコンテンツに対して使っています。そのままですね。自動更新購読は「有料プラン」「プレミアムオプション」のような、定期課金に使用しています。これもそのままですね。
 

エウレカではCouplesというカップル向けリア充コミュニケーションアプリがありますが、メッセンジャー機能の中でテキスト以外にもスタンプを送信することができます。
(FacebookやLINEさんのようなやつです)
 

その有料スタンプでは非消耗型(Non-consumable)を使っています。
(月額会員向けのCouples Plusという商品では自動更新購読を使っています)
 

決済とは全然関係ないですが、Couples Qという恋愛相談サービスも最近開始したので、恋人がいる方は使ってみてください。誰にも言えない相談、修羅場をくぐってきた兵のアドバイスお待ちしております。
(ナチュラルな宣伝)
 

決済のフローについては、クライアントサイドだけでやる方法とサーバーサイドを含める方法があります。APIサーバーがなかったり、mBaaSとかを使っているアプリであれば、iOSアプリだけで決済処理を完結できますが、多くのサービスのようにサーバーサイドAPIが存在する場合は、サーバーサイドで決済の検証を行うのが確実です。
 

pairsの場合は以下のフローとなっています。
 

  • (1) カスタマーがアプリ上でpairsの決済ページを開く
  • (2) ページ内の購入ボタンを押すとiOSの購入確認モーダルが表示される
  • (3) モーダルのOKボタンを押すと、Appleの購入レシートをiOSアプリ側からpairs側へ送信する
  • (4) pairs側(実際はeureka決済システム)は受け取ったレシートをそのままAppleの決済APIへ渡す
  • (5) レスポンスとして決済情報が含まれているJSONデータが返却され、その情報を元に以下のような購入バリデーションを行う
    • statusが0かどうか
    • bundle_idが正しいかどうか
    • product_idが正しいかどうか
    • 未使用のtransaction_idかどうか
    • expires_dateが過ぎていないかどうか
  • (6) 全てOKであれば購入成功、NGな項目があれば購入エラーを返却する

なんだか分かりづらいですね。
ここでレシートという単語が出てきましたが、MD5ハッシュ化された文字列です。
こいつをAppleのAPIへ送信すると、そのレシートに紐づく決済情報がJSON形式で返却されてきます。(5)のバリデーションのところで、その中身を確認し、ちゃんと決済が行われたかどうか確認します。
 

レシートの形式には iOS6型とiOS7型があります。
 

古いiOS6型は単体の購入情報のみがあり、定期課金の有効ステータスを確認することができます。新しいiOS7型は定期課金に紐づく課金履歴情報が全て含まれており、個別の定期課金の有効ステータスは確認できません。
 

イメージしづらいと思うので、実データを見てみましょう。

どうでしょうか。iOS7型はクレイジーですね。
 

といってもこれは実環境ではなく、サンドボックス環境のレシートを元に整形したものなので、実際のレシートがここまで肥大化することはないと思います。
ただ、全ての課金履歴が含まれているようなので、1ヶ月サイクルで定期課金が更新される度に、15行くらい増えます。
in_applatest_receipt_info の両方に含まれるため、実質30行ほど増えます。
これが3年間継続すると (30行 * 3年 * 12ヶ月) = 1080行 くらいに肥大化することになります。
アプリ側からサーバーサイド側へPOST送信される、ハッシュ化されたレシート文字列自体もボリューミーになるため、nginxのclient_max_body_sizeを低めに制限している場合は気をつける必要があります。
 

検証時にはlatest_receipt_infoから末尾順に辿り、購入した商品と一致するproduct_idとその有効期限を見ることになると思います。
iOS7型にすることで検証のロジックが少し複雑になりましたね。
 

そして定期課金の有効ステータスですが、iOS6型の場合は定期課金の有効期限が切れると21006というstatusを返却してくれます
 

定期課金の更新については、status021006 かどちらかを調べるだけなので、iOS6型は非常に楽ですね。
 

あれ、なぜ私たちはiOS7型に切り替えてしまったのでしょうか/(^o^)\
 

ここまで説明していて、パワポで作図したくないため良い感じの図が無いかどうか探していたところ、サイバーエージェントさん公式エンジニアブログに自動購読課金について【iOS編】という記事を見つけ、とても整理されて分かりやすくまとまっていました。「冒頭で紹介しろよ!」って感じですね。ハハハ。

こちらの記事の著者の辻さんが、Go言語でのIAP, IAB向けに作られたdogenzaka/go-iapを利用させていただいています。本当にありがとうございます。
 

iTunes Connect 管理画面

Appleの管理画面から決済関連でできることは、決済データのCSVのダウンロードです。
私から言えることはそれだけです。よろしくお願いします。
 

マンパワーを使って毎回CSVをダウンロードしてもいいんですが、API経由でデータを取得することもできます。Node向けのライブラリもあるので、整形してサマリ表示するようなこともSlack経由でできます(仕掛中)
 

なおCSVデータには誰が買ったか特定できる情報はないため、サービス側で保存している決済情報と完全に突合するのは難しいです。
 

iTunes Connectの該当画面のスクリーンショットを上げようかと思いましたが、なぜか私の権限が剥奪されているため、遷移できませんでした。よろしくお願いします。
 

サンドボックス環境

iTunes Connect上でテスターのアカウントを追加し、Testflightでアプリを配信することでテストをすることが多いんじゃないかと思います。
 

PayPal同様に、サーバーサイドで検証する際は、サンドボックス環境と本番とで接続先URLが違います。
 

また自動更新の期間が短くなっており、購入後に6回更新されます。途中解約はできません。

そして購読画面からも変更ができないため、途中解約やクロスグレード変更、後述のアップグレードといった細かい挙動の確認はできません。
これは「計測するな、推測せよ!」 ということでしょうか?!
 

Google Play In-app Billing (Androidアプリ)

次はみんなのヒーロー、Google社がAndroid向けに提供する決済プラットフォームになります。
iOS同様、Androidアプリケーションでもアプリ内課金のためにIn-app Billing(以下、IABと表記)という仕組みを使うことができます。
 

フローについてはiOSと同様なので省略します。
 

そして大体のことは公式のドキュメントに載っています。
 

というか、こちらもサイバーエージェントさんの自動購読課金について【Android編】にまとまっています。
「この記事のおかげで、私から言えることはもうありません (`・ω・´)キリッ」
で済ませたいところですが、上長に怒られそうなので一応説明いたします。

 

iOS IAPのレシートに当たる箇所で最低限必要なパラメータは、productIdpurchaseToken, orderId になります。
 

productId はGoogle Play上で登録した商品のIDが入ります。
purchaseToken には購入トークン(乱数)が入り、iOSでいうところのレシートの検証時に使います。
orderId は API操作では一切使いませんが、Googleペイメントセンターの決済と紐づくオーダー番号が入ります。
オーダー番号を使ってWebUI上から手動で返金操作を行ったり、GooglePlayの収益レポートCSVと紐付けて会計チームに引き渡したりする際に使用します。
purchaseTokenorderIdは一つの決済につき1つになります。purchaseTokenはシステム側で使用し、orderIdは人間が使うと思ってください。
 

orderIdは元々 XXXXXXXXXXXXXXXX.YYYYYYYYYYYYYYYYY のような形でしたが、2015年の夏頃から GPA.0000-0000-0000-0000 のような形に変わりました。
 

旧形式のXXXXXXXXXXXXXXXX.の部分はアプリによって固定だったため、pairsではGoogleのAPIへリクエストを行う前にフォーマットによる事前バリデーションをかけていました。
よく訓練されたAndroider(特に海外)の方々はカジュアルに不正なレシートを送信してくる傾向があり、こちらもカジュアルに400エラー対応をしていました。
 

そんな中、orderIdがカジュアルに新形式に移行し、決済のエラーレートが上がったことがありました(寒気)
しかも両方の形式が混在しており、全て失敗するわけではなくたまに失敗するという状況でした(吐き気)
 

その日はむせび泣きました。
 

この教訓としてそれ以降、ワタシタチは勝手に変なバリデーションをかけるのはやめました。
ベンダーのAPIへ素直にそのまま渡すことを熱烈におすすめします。
 

また、GoogleのAPIは非常に安定しています。
主観ですが、

Google >> PayPal >>>> Apple

というくらい安定しています。
キングApple様は元々安定していましたが、今年の5月くらいから不安定(エラー率が多い)な気がします。
 

GoogleもAppleもたまに特定のレシートで不明なエラーを返却してくることがあります。
前回の更新時には正常に使えたのに、現在は40Xエラーを返却してくるケースです。
問い合わせたところ、「Googleをアカウント削除すると、既存のレシートで400エラーが返却される」ということで、400番が返ってきた場合は定期課金を解約状態にしてしまって大丈夫なようです。
(Apple様の404 newNullResponse エラーは 「問い合わせ」 -> 「調査中」 -> 「優先度低」 という奥義を喰らっており不明なままです。推測ですが、おそらくGoogle Playと同じような状態じゃないかと思います)
 

Google Playの管理画面

Apple同様に、Google Play Developer Consoleから売上レポートのダウンロードができます。
 

アカウントごとに固有のCloud Storageへのリンクがあるようなので、そこからCSVファイルをダウンロードできます。このCSVファイルにはDescriptionというカラムにorderIdが記載されているため、サービス側で保存している決済情報と一対一の突合が可能です。
 

b03_google
(↑この画面の下部にCloud Storageへのリンクがあります)
 

また、Google ペイメントセンターからは特定の決済の返金や定期購読の解除を行うことができます。orderIdオーダー番号として記載されており、検索も容易にできます。
 

11月まではGoogle Wallet Merchant CenterというUIだったのですが、今はGoogle ペイメントセンターというシステムに切り替わっています。
前はURL指定で検索クエリが使えたため、pairs, Couplesの管理画面から個別の課金へ直接遷移するリンクを生成することができましたが、Google ペイメントセンターになってからは、よりSPAな動きになり、URLから直接個別の課金へ飛ぶことが出来なくなってしまっため、
ヒューマンが入力しないとあかん状態になっています。
誰かこっそり解決策を教えてください。タノミマス。
 

サンドボックス環境

AndroidはiOSと違って、そこまで詰まった経験がないのでようわからんのが正直なとこです。
詳しくは公式のドキュメントを見てみてください。
 

Googleアカウントをテスター登録するとテスト課金できますが、1ヶ月の有効期限は1日になり、さらにorderIdが取得できないようです。
そのためテスターアカウントを使うこともありますが、orderIdのバリデーションエラーを避けるために本物のアカウントを使ってカジュアルに課金をすることもあります。
Google ペイメントセンター上で返金処理をしましょう。)
 

アップグレード商品

元々Android IABには商品にTierという概念がありまして、
例えば、

  • ブロンズプラン 月額100円 メールサポートのみ 5営業日以内のレスポンス
  • シルバープラン 月額300円 月3回の電話サポートあり 3営業日以内のレスポンス SLA
  • ゴールドプラン 月額1000円 月100回の電話サポート 1営業日以内のレスポンス SLA
  • エンタープライズプラン 要お問い合わせ 無制限のサポート

このような、プラン間での上位・下位があった場合に、購入済み商品から切り替え(アップグレード・ダウングレード)が可能な仕組みがありました。
これを行うと切り替え後プランの期間延長が行われるようです。
 

そしてApple様にはこの仕組みはありませんでしたが、今夏WWDC2016にて決済関連のアップデートがあり、国別価格や価格変更時の据え置きに加え、アップグレードプランも対応しました。
 

上位・下位の関係がある商品の場合はこちらを適用しないといけません。例えば、Netflixには1画面プラン・2画面プラン・4画面プランがあり、既にアップグレード・ダウングレードが出来るようです。
 

詳しい仕様、特に引き落とし開始日の変化や決済金額が不明なためヒアリング中ですが、まだ良くわかっていないため、私から伝えられることは少ないです。ごめんなさいm(_ _)m
 

D. Goでの決済システムの実装

ようやく実装の話に入ってきます。
ここからはGo言語に興味がない人は全くつまらない話なので、心苦しいです。
 

ちなみにpairs・Couples本体とはかなり構成が違うので、この記事を見て「pairsって○○で××なんでしょ?」とか弊社社員と話しても話が通じないことがあります。心苦しいです。
 

主要ライブラリ

外部ライブラリはこの辺りを使っています。
よく聞かれるので頑張って列挙してみました。
あんま珍しいものは使ってないと思います。
 

種別 ライブラリ 備考
WAF・ルーティング zenazn/goji
DB・ORM go-xorm/xorm
DB・ORM evalphobia/wizard xormをシャーディング対応したもの
キャッシュ evalphobia/eurekache メモリキャッシュのみ使用
HTTPクライアント h2non/gentleman franela/goreqから移行中…
ログ sirupsen/logrus (彼の名前が小文字になりましたね…)
ログフック logrus_appneta TraceViewへのエラーログフック
ログフック logrus_fluent fluentdへのフック
ログフック logrus_sentry Sentryへのフック
ログフック Stackdriver Stackdriver loggingへのフック
モニタリング fukata/golang-stats-api-handler
モニタリング tracelytics/go-traceview TraceViewへのトレースログ送信
テスト stretchr/testify
決済系 evalphobia/go-iap dogenzaka/go-iapにiOS6型レシート対応を加えたものです
決済系 evalphobia/go-paypal-classic

ディレクトリ・パッケージ構成

大体以下のような形になってます。
 

├ main.go
├ config/               // 設定ファイル
├ routing/              // ルーティング定義
├ middleware/           // リクエスト処理時のミドルウェア
├ controller/           // コントローラー
├ service/              // ビジネスロジック置き場
│    └─ platform/     // 各決済プラットフォーム固有のロジック
├ model/                // DDDでいうところのEntityとRepository
├ library/              // 各種ライブラリ置き場
├ client/               // 外部向けのSDK
├ test/                 // テストヘルパー・fixture置き場
└ misc/                 // RakeタスクとかドキュメントとかSQLとか

それぞれ詳しくみていきます。
 

main.go

main.goでしていることは、

  • フラグのパース
  • 設定の読み込みと初期化
  • ルーティングとミドルウェア設定
  • HTTPサーバー起動

くらいです。至って普通ですね。
 

config

設定ファイル置き場です。
読み込みにはevalphobia/go-config-loaderというものを作って使ってます。
 

例えば以下のようなログ用の設定があったとすると、
 

$ cat ./config/log.toml

[log]

[log.fluent]
host = "127.0.0.1"
port = 24224
enable = false

[log.sentry]
url = ""
enable = false

# ----ここまで----

$ cat ./config/dev/log.toml
[log]

[log.sentry]
url = "https://XXX:YYY@app.getsentry.com/999999"
enable = true

簡単な使い方は↓のようになります。
 

import(
    config "github.com/evalphobia/go-config-loader"
)

// (中略)

const confType = "toml"

conf := config.NewConfig()
conf.LoadConfigs("config/dev", confType) // ./config/dev から .tomlファイルを全て読み込む
conf.LoadConfigs("config", confType)     // ./config から .tomlファイルを全て読み込む

// ./config/log.tml のデータ
useFluentd := conf.ValueBool("log.fluent.enable") // => false

// ./config/dev/log.toml のデータ
useSentry := conf.ValueBool("log.sentry.enable") // => true
sentryURL := conf.ValueString("log.sentry.url")  // => https://XXX:YYY@app.getsentry.com/999999

LoadConfigs では先に読み込んだ項目が優先される仕様になっているため、固有の設定から読み込むようにし、全体のデフォルト設定は最後に読み込むようにします。
 

routing

goji.SubRouter を使ってAPIの種類ごとにルーティング設定を保存しています。
ルーティングは全てcontrollerの関数を設定しています。
 

middleware

WAFのルーティングの文脈でいうミドルウェアを定義しています。(一番最初に何かのWAFでミドルウェアって見た時はよく理解できませんでした…(/_;)) HTTPリクエストがコントローラ層で処理される前(と後)にロジックを仕込むことができます。
 

認証やHTTPヘッダー処理、パラメーター処理なんかを行ってます。
特に変わったことはしていませんが、http.Request からリクエストパラメータを取得して、内部ロジックで利用する生データと、個人情報っぽいものを削除したログ用途のものに分けて、Contextに入れてます。こうすることで、不用意にログにヤバイデータが残されないようにしています。
 

controller

service層の関数を一つ呼び出し、その結果をJSONとして返却しています。
呼び出しの結果は全て map[string]interface{} で受け取っています。
 

ただしモニタリングやテスト用のコントローラはそのままロジックが書かれていることもあります。
 

service

様々なロジックが書かれる場所です。
地道です。
 

platform

各決済プラットフォームごとの固有のロジックを置いています。
いわゆるファクトリパターンで決済の interface を作成し、service層ではplatformを意識せずに新規購入や更新処理を行っています。
 

たまにプラットフォーム独自のフローがあったりすると、全てにダミーのメソッドを追加しないといけないのが辛いです
 

テストは一部本番のデータを使っています。
本番のデータを使っているがために、1ヶ月ごとにリアルデータの有効期限が延長されます。
そのためアサーション部分を適宜変更しなければいけませんが、本番のレスポンス以外は信じたくない病的思考のためこうしてます。
CIが通ったとき、有効期限が変わってCIが落ちたとき、そして修正後にまたCIが通ったときの安心感・満ち足りた高揚感はひとことでは言い表せません。
 

model

DDDの概念でいうエンティティとレポジトリが置かれています。
レポジトリで更新処理や特定の処理をしたときに、エンティティ側の非公開フィールドのフラグやデータをいじりたかったので、同じパッケージに格納しています。
(必須ではないので分けてもいいですが、そこまで不便さを感じていないためそのままです。)
 

library

内部ライブラリが置かれています。
ディレクトリ構成は標準ライブラリと似た命名になっています。
(import元で標準ライブラリと名前がかぶった時はこちらのパッケージ名を変えています。)
 

外部ライブラリを使う場合はここにアダプタを置き、サービス層や他の層では直接外部ライブラリを扱わないようにしています。
 

client

clientにはpairs側でimportして使うためのSDKを置いています。エンドポイントやパラメータ情報が書かれています。
 
新たにエンドポイントやパラメータを追加する場合はサーバー側だけでなく、こちら側にも更新を反映します。決済システム本体のコードとは分離していますが、唯一エラーコードのファイルだけ共有しています。
 

ここにもテストコードを置いており、本体サーバーを起動して、fixutureを流した際のレスポンスを検証しています。代わりにpairs側での決済リクエスト周りの単体テストは簡素になっており、固定のダミーレスポンスを使うようにしています。
 

test

テスト用のヘルパーとfixtureしか置いてないです。
 

misc

バッチ用のRakeタスクが置いてあります。
「あれ、バッチ処理はRubyで書いてるの?」と思うかもしれませんが、実体はHTTPリクエストを送信しているだけです。
 

主なバッチ処理は定期課金の更新処理とデータ不整合時のアラートです。
それぞれに順番的な依存関係を持たせないようにしており、各リクエストは数秒以内に終わるようにしています。(と言ってもプラットフォーム側の応答速度にも依りますが…)
 

わざわざワーカーを作ったりCLIツールを作り、それぞれ監視を行ったり使い方を教えるのも面倒ですしシンプルさが失われると感じていて、全ての処理はcurlコマンドさえあれば出来るようになっています。
 

E. その他

定期課金の更新

上で説明したように、処理のインタフェースはHTTPサーバー&クライアント&cronで構築しています。大きくエンキューとデキューのプロセスに分かれています。
 

有効期限が切れたり、確認状態になった定期課金をエンキューのプロセスでSQSへ突っ込みます。一回あたりの処理件数は上限を設けています。通常は問題ありませんが、多く処理しなければならない時、例えば日付が変わった瞬間は通常より多くのエンキュー処理が実行されます。
この辺りはRakeタスク実行時のパラメータで、総リクエスト実行数が変更できるようになっています。
 

各プラットフォームごとにSQSキューがあり、そのキューごとに個別のHTTPエンドポイントが存在します。HTTPリクエスト1回につき定期課金が1件処理されるため、こちらもRakeタスクのパラメータで増減が調整可能になっています。
 

またSQSキューのメッセージ数を監視することで、どこかのプラットフォームで異常が起きていないかを把握することができます。
 

ちなみにGo言語でのAWS SQSの使い方については先週末に記事にしたので、興味ある方は見てみてください。(未だにFacebookシェア数が0なので愛しさや心強さは一切なく、ただただセツナさを感じています。)
 

決済システムとプロダクト側とのデータ同期

決済システムでは決済データをデータベースへ保存しています。
またプロダクト(pairs)側でも決済データをデータベースを保存しています。
 

基本的には決済システム側のデータがマスターとなり、プロダクト側ではその一部を保存します。
 

プロダクト側から決済システムを通して決済プラットフォームへ決済処理を行う際のフローは大きく以下のようになります。

  • (1) プロダクトから決済システムへ購入リクエストを送信する
  • (2) 決済システムから決済プラットフォームへ購入(or 確認)リクエストを送信する
  • (3) 決済システムからプロダクトへレスポンスを返却する

ここで (2)が成功し(3)が失敗すると、決済システムだけにデータが保存される可能性があります。マイクロサービス化を進めるとデータの不整合を防ぐ仕組みが必要になります。

  • (4) プロダクトから決済システムへ確認リクエストを送信する

c02_flow

決済システムではデータベースに確認用のカラムを用意しており、(3)が成功すると確認リクエスト(4)を送信し、同期完了とします。
TCPのACKに近いです。
 

データが不整合になってしまった場合はバッチで自動同期をするか、アラートが発生するようになっています。
 

ベンダリング

kovetskiy/manulでしてます。
たまにおかしくなるのでgit submoduleを生でいじることもあります。
 

CI

Travisを使っています。カバレッジはCodecovを使っています。
masterでCIをパスした場合はgitbookのビルドプロセスが走り、マニュアルが更新されるようになっています。
 

デプロイ

ステージングのデプロイはRundeckとconsulを使っています。
元々はAWS CodeDeployを使っていましたが、Web UI上でブランチを指定してデプロイできるようにRundeckに変えました。本番へのデプロイは独自の人肌温いスクリプトを使っています。
 

システム監視

インフラ・ミドルウェアレイヤーは、はてなさんのMackerelを使用しています。ありがとうございます。
 

ログ周りはSentrySolarWinds TraceViewGoogle Stackdriver loggingを使っています。
 

アプリケーションレイヤーでは、基本的にcronバッチ&アラート通知(Email & Slack)が多いです。アラート通知がアプリケーションプロセス・HTTPサーバーに依存しているため、ここで不具合が発生するとどうしようもない状況になります。
 

バッチ処理をHTTP経由にする弊害の例

決済システムの前段にはELBが入っており、オンライン向けの処理系統とバッチ処理向けの処理系統を分けておらず、どちらも一つのアプリケーションとして動作しています。
 

先日、メモリリーク調査のために数日間バッチ処理系統を分けていました。
そんな中、Fatalエラー(logrusの各フック内でmapの並行アクセス起きてしまった…)が発生し、アプリケーションが落ちてしまいました。
本来はsupervisorで再起動されるはずですが、なぜかうまくされずに変な状態で生き残ってしまっていたようです。(プロセスKILL等では問題なく再起動される)
このため全てELBから外されしまい、バッチ処理が行われないようになってしまいました。
(仮で作ってもらったのでオートヒーリングが設定されていなかった…)
 

HTTPクライアント側にはHTTPステータスでのアラートを設定しておらず、アラートのほぼ全てをHTTPサーバー経由の処理が担っていたためにアラートが機能しなくなってしまっていました。
 

休日でしかもMacherelアラートも他の通知に紛れてしまっていたため、対応が遅れてしまいました。(吐き気)
 

本当に気をつけてください。
 

データベース

最初の方で説明した通り、DBにはAmazon Auroraを使っています。
そこまでトラフィックがないため、アプリケーションサイドでは特に辛さやありがたみ、畏敬の念を感じたことはないですが、インフラ運用面では何かあるかもしれないので、恐らく来年中には弊社のオラオラ系テラフォーマー恩田氏が何か語ってくれるはずです。
 

テーブル数は10ちょいです。
直接決済に関わるテーブル以外だと、

  • マルチテナンシー用のアカウントテーブル
  • 監査ログテーブル
  • エラーログテーブル

が存在しています。

エラーログテーブルはステージングでの調査に使用することが多く、本番ではあまり活用できていないので、もう少しチューニングをしていきたいと思っています。

WebUI

ありません。
 

上記のエラーログやBaremetricsのようなUIを提供したいのですが、
「ドヤァァァァ!」と作っても自己満になりそうなので、今のところはやっていません。
 

F. まとめ

ここまで見てきたように、決済のシステムを作るには各プラットフォームの知識が必要になってきます。ある意味では、実装を行うよりも詳しい仕様を把握する方が大変かもしれません。
プラットフォーム間の差異を吸収するために、eurekaでは決済システムをマイクロサービスとして切り出すことにしました。
サポートする決済手段が1,2種類ほどでは分ける必要性が薄いかもしれませんが、決済手段が増えるに従い、別のシステムとして分けるメリットは増してくると思います。
 

決済の苦しみスバラシサを1%でもお届けできたか不明ですが、少しでもお役に立てたらと思います。
 

はてぶ数が100以上、FBシェア数が200以上つくと、ブログ工数が確保され、この続きを図付きで詳しく書かせてくれるということなので、コメントを添えてシェアリングパーティーをしてもらえればと思います。
 

明日の23日は、まつけんはんによる「pairsのインフラコストを最適化しました」となります。
乞うご期待!
 

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

急成長サービスの開発責任者として意識している4つの『やらないこと』と1つの『大切なこと』

UI改善における仮説検証の3つのポイント