ActiveMerchantを使ってRailsに支払い機能をつける [PayPal編]

皆さん、こんにちは。Misocaチームの @ です。

Active Merchantというgemをご存知でしょうか? Misocaはクラウド上で請求書をつくって送れるサービスです。請求書はお金を支払ってもらうためにつくるものなので、支払いもWeb上でできたら便利ですよね。 そこでMisocaには請求書に支払い機能を持たせられる「Misocaペイメント」という機能があります。 これはActiveMerchantによって実現されています。

本記事ではActiveMerchantの使い方として一番ポピュラーで簡単なPayPal支払いをご紹介したいと思います。

(PayPalについての基本的な知識は持っている、という前提で書きます。IPNって何?という方はまずはこちらをご覧ください。 即時支払い通知(IPN) - PayPal)

ここでは PaypalExpressGateway というActiveMerchantの中のPayPal用モジュールを使って簡単なRailsアプリケーションを実装するためのステップを紹介します。

事前準備

sandboxアカウントを作成

まずはテスト用にPayPalのdeveloperアカウントを作成します。 https://developer.paypal.com/

f:id:yusuke-k:20150731164938p:plain

さらに「Create Account」というボタンを押してsandbox用のテストアカウントを作成します。 typeはBUSINESSを選んでください。

作成すると、profileからAPI credentialsを確認できるようになります。 この情報がPayPalで支払先として設定するマーチャント(加盟店、お金を受け取る側)になります。

f:id:yusuke-k:20150731165158j:plain

Railsアプリケーションのサンプル

gem install

Gemfile

gem 'activemerchant'
gem 'offsite_payments'

と書いてください。 ちなみに offsite_payments というのはActiveMerchantからRails依存の処理を抽出したgemらしいです。 (ハッキリしたことはわかってない)

routes.rb

こんな感じで書きます。実際にはアプリケーション用の注文IDなど、必要に応じてパラメータを付加してください。

scope path: 'payments', module: 'payments' do
  get 'new' => 'paypal_express#new', as: :paypal_payment
  get 'cancel' => 'paypal_express#cancel', as: :paypal_cancel
  get 'purchase' => 'paypal_express#purchase', as: :paypal_purchase
  get 'complete' => 'paypal_express#complete', as: :paypal_complete
  post 'notify'  => 'paypal_express#ipn', as: :paypal_ipn
end

view

PayPalの支払画面にリダイレクトして戻ってくるだけの流れなのでこれといった画面はありません。 支払い完了後の画面とキャンセルで戻ってきたときの画面があればじゅうぶんです。

app/views/payments/complete.html.erb

<header>
  <h1>支払完了</h1>
</header>
<div class="main">
  <p>支払が完了しました</p>
</div>

app/views/payments/cancel.html.erb

<header>
  <h1>キャンセル</h1>
</header>
<div class="main">
  <p>キャンセルしました</p>
</div>

controller

app/controllers/payments/paypal_express_controller.rb

サンプルとして最小限必要な処理はこんな感じです。 金額や明細(件名、数量)などは実際のアプリケーションに応じて変更してください。

基本的な処理の流れは

  1. newPayPalの支払画面へ遷移
  2. purchase で購入処理をして支払完了画面へ遷移
  3. ipn でIPNを受け取って処理

となっています。

注意点としてIPNはPayPal側からのアクセスになるため、localhost:3000 のようなアドレスを指定するとIPNを受け取れません。 そのためにはngrokなどのツールを使ってポートフォワーディングする必要があります。

ngrokについては前回記事を参照してください。 tech.misoca.jp

module Payments
  class PaypalExpressController < ApplicationController
    AMOUNT_SAMPLE = 10000

    ### PayPalの支払画面に遷移するために必要な情報を取得して
    ### 支払画面にリダイレクトする
    def new
      # 支払画面に飛ぶために必要な情報を取得する
      response = gateway.setup_purchase(
        AMOUNT_SAMPLE,
        ip:                request.remote_ip,
        return_url:        paypal_purchase_url, # PayPalで支払い処理後に戻るURL
        cancel_return_url: paypal_cancel_url,   # PayPalでキャンセル処理後に戻るURL
        items: [
          {
            name: "a subject",         # 件名
            quantity: 1,             # 数量
            amount: AMOUNT_SAMPLE    # 支払い金額
          }])

      # ここでreview: falseに設定するとPayPal側で「今すぐ支払う」というボタン名に変わる
      # trueにすると「同意して支払う」となる
      # ボタン文言が変わるだけでそれ以外の違いは一切ない
      redirect_to gateway.redirect_url_for(response.token, review: false)
    end

    ### PayPalの支払画面で手続き後にアプリケーションに戻ってくるので
    ### 購入処理をする
    def purchase
      # details_forで金額や支払い先などの情報が取得できるので
      # 確認画面を表示したりする時に使う
      # 本記事では確認画面の表示はスキップする
      detail = gateway.details_for(params[:token])

      # 購入処理
      response = gateway.purchase(
        AMOUNT_SAMPLE,
        ip:         request.remote_ip,
        token:      params[:token],
        payer_id:   params[:PayerID],
        notify_url: paypal_ipn_url  # IPNを受け取るURL
                                      # localhostだとPayPalからアクセスできないので
                                      # 開発時にはngrokなどを使う
      )

      if response.success?
        redirect_to paypal_complete_path
      else
        logger.error 'purchase failed.'
      end
    end

    def complete;render 'payments/complete';end
    def cancel;render 'payments/cancel';end

    # IPNを受け取ったときの処理
    def ipn
      response = OffsitePayments::Integrations::Paypal::Notification.new(request.raw_post).extend(PaypalNotification)

      # 対象となる支払いをVERIFYしている
      unless response.acknowledge
        logger.error 'invalid ipn'
        render nothing: true, status: :bad_request
      end

      # 支払い金額が意図した金額になっているのか念のため確認
      if response.completed?(AMOUNT_SAMPLE)
        logger.info 'completed'
      elsif response.refunded?(AMOUNT_SAMPLE)
        logger.info 'refunded'
      else
        logger.error 'failed'
      end

      # HTTPステータス204を返すとPayPal側で決済処理完了として扱われる
      render nothing: true
    end

    private

    def gateway
      ActiveMerchant::Billing.PaypalExpressGateway.new(
          login: '[sandbox accountのUsername]',
          password: '[sandbox accountのPassword]',
          signature: '[sandbox accountのSignature]'
      )
    end
  end

  module PaypalNotification
    def completed?(amount)
      complete? && amount == gross.to_i
    end

    def refunded?(amount)
      status == 'Refunded' && (0 - amount) == gross.to_i
    end
  end
end

def gateway の中で、PayPalAPIを呼び出すために事前につくったsandboxアカウントのAPI credentialsを埋めてください。

実行結果の確認

ngrokを立ち上げます。

./ngrok http 3000

https://xxxxx.ngrok.io/new へアクセス(xxxxxの箇所はランダム)

うまく動けば、PayPal sandbox環境の支払い画面に遷移します。

f:id:yusuke-k:20151105160753p:plain

URLのドメインwww.sandbox.paypal.com となっていることが確認できると思います。

適当に支払い処理をしてください。 ローカルの画面に戻ってきたタイミングでIPNが飛んできたのもログから確認できると思います。

PayPalで確認

https://developer.paypal.com/ で結果を見てましょう。 sandboxアカウントを選んで「Notifications」をクリックすると結果が表示されます。

f:id:yusuke-k:20150807182255j:plain

行を選択すると支払いの内容を確認できます。

f:id:yusuke-k:20150807182405j:plain

Productionでの動作

development で動かした場合はsandboxに接続に行くようになっています。 production で動かせば本番のPayPalに接続されるように自動的に切り替わります。

もちろん本番環境ではsandboxアカウントのAPI credentialsは使えないので本物のビジネスアカウントをご使用ください。

まとめ

如何でしたでしょうか。 ActiveMerchantを使えば支払い機能を簡単に作成できます。

PayPal以外でも様々な決済に対応していますし、未対応の決済代行会社のAPIでも 自分でモジュールを書けば対応できます。 機会があれば、そういった上級編もご紹介してみたいと思います。