LoginSignup
10
10

More than 5 years have passed since last update.

RailsでPaypalのIPNを処理

Last updated at Posted at 2016-02-11

Paypalで決済が完了したときに、PaypalからIPN(Instant Payment Notification)を受信し、Railsで適切に処理する方法をまとめました。

PaypalからコールされるデータをUTF-8に統一

これをしておかないと謎のエラーに苦しめられるので、初めに設定しておきます。
デフォルトではShift-JISでコールされます。

  • 「マイアカウント」→「プロファイル設定」→「言語のエンコード」→「詳細オプション」
  • 「エンコード方式」をUTF-8に
  • 「PayPalから送信されたデータと同じエンコード方式を使用しますか?」をUTF-8に

参考) http://blog.katsuma.tv/2007/06/paypal_pdt_ipn_code.html

Paypalボタンに各種URLを設定

Paypalにポストするhiddenタグに各種URLを追加。

  • notify_url IPNの通知先URL
  • return ユーザが購入処理後に戻ってくるURL
  • cancel_return ユーザが購入をキャンセルしたときに戻ってくるURL
example_form.html.erb
<% base_url =request.protocol + request.host_with_port %>
<% is_sandbox = true # 本番環境の場合はfalse %>
<% if is_sandbox %>
  <% paypal_url = "https://www.sandbox.paypal.com/cgi-bin/webscr" %>
<% else %>
  <% paypal_url = "https://www.paypal.com/cgi-bin/webscr" %>
<% end %>
<form action="<%= paypal_url %>" method="post" target="_top">
  <%= hidden_field_tag 'notify_url', "#{base_url}/paypal/ipn" %>
  <%= hidden_field_tag 'return', "#{base_url}/paypal/success" %>
  <%= hidden_field_tag 'cancel_return', "#{base_url}/paypal/cancel" %>
  <%= hidden_field_tag 'cmd', "_s-xclick" %>
  <%= hidden_field_tag 'hosted_button_id', YOUR_BUTTON_ID %>
  <input type="submit" value="購入" />
</form>

routes.rb

RAILS_ROOT/config/routes.rb
  post '/paypal/purchase' => 'my_paypal#ipn'
  post '/paypal/success' => 'my_paypal#success'
  get '/paypal/success' => 'my_paypal#success'
  post '/paypal/cancel' => 'my_paypal#cancel'
  get '/paypal/cancel' => 'my_paypal#cancel'

Controller

Paypal関連の処理のみに絞ったPaypalControllerを作成し、それを継承したMyPaypalControllerでアプリに応じた処理を記述する。

こうすることで、PaypalControllerは使いまわすことができ、継承したコントローラではアプリに応じた処理を集中的に記述することができます。

PaypalControllerの処理

Paypalのドキュメントに従い、IPNを適切に処理する。

  • IPNを受け付ける
  • IPNのステータスに応じた処理を行う
  • IPNがPaypalから送信されたものかをチェックする
  • 同じ購入IDの処理が重複していないかをチェックする
  • 受取人のメールアドレスが事業者のPayPalアカウントと一致するかをチェックする
  • 商品の情報が正しいかをチェックする

ただし、具体的な処理は適宜、継承先のメソッドに委ねている。

参考) http://stackoverflow.com/questions/14316426/is-there-a-paypal-ipn-code-sample-for-ruby-on-rails

RAILS_ROOT/app/controllers/paypal_controller.rb
class PaypalController < ApplicationController
  # PaypalのサーバからのIPNの送信を受け付けるため、CSRF対策を無効にする
  protect_from_forgery with: :null_session

  # ipnの送信を受け付けるメソッド
  def ipn
    is_sandbox = params[:test_ipn] ? true : false
    logger.info ({is_sandbox: is_sandbox}) if ipn_log_level > 0
    if skip_validate?
      response = "VERIFIED"
    else
      response = validate_ipn_notification(request.raw_post, is_sandbox)
    end
    case response
      when "VERIFIED"
        status = params[:payment_status]
        if status == "Completed"
          if ipn_overlap?(params, is_sandbox)
            ipn_overlap(params, is_sandbox)
          else
            ipn_completed(params, is_sandbox)
          end
        elsif status == "Pending"
          ipn_pending(params, is_sandbox)
        elsif status == "Failed"
          ipn_failed(params, is_sandbox)
        end
        if ipn_log_level == 1 && status == "Completed"
          logger.info ({status: status})
        elsif ipn_log_level > 0
          logger.info ({status: status, params: params})
        end
      when "INVALID"
        logger.info ({status: 'invalid', params: params}) if ipn_log_level > 0
        ipn_invalid(params, is_sandbox)
      else
        logger.info ({status: 'validate_error', params: params }) if ipn_log_level > 0
        ipn_validate_error(params, is_sandbox)
    end
    render :nothing => true, status: :ok # paypalに200(OK)を返す。
  rescue => e
    logger.info(e) if ipn_log_level == 1
    logger.info({ message: e.to_s, backtrace: e.backtrace }) if ipn_log_level == 2
    render json: {message: e.to_s}, status: :internal_server_error
  end
  protected
  # 購入が正常に完了した時の処理
  # オーバーライドを強制
  def ipn_completed(params, is_sandbox)
    # override this
    raise NotImplementedError, "You must implement #{self.class}##{__method__}"
  end

  # 購入が重複した場合の処理
  def ipn_overlap(params, is_sandbox)
    # override this
  end

  # 購入がPending(未決済)の場合の処理
  def ipn_pending(params, is_sandbox)
    # override this
  end

  # 購入がFailed(失敗)の場合の処理
  def ipn_failed(params, is_sandbox)
    # override this
  end
  # 購入が不正の場合の処理
  def ipn_invalid(params, is_sandbox)
    # override this
  end
  # 通知の確認自体が失敗した時の処理
  def ipn_validate_error(params, is_sandbox)
    # override this
  end

  # 判定関係
  # 同じ購入IDの処理が重複しているかの判定
  def ipn_overlap?(params, is_sandbox)
    # params[:txn_id] の比較
    # override this
    false
  end
  # receiver_emailが事業者のPayPalアカウントと一致するかを判定
  def ipn_receiver_email_correct?(email, is_sandbox)
    # override this
    true
  end
  # 商品の情報に誤りがないかを確認
  def ipn_item_correct?(params, is_sandbox)
    # override this
    true
  end
  # ログの記録
  # 0: 記録しない
  # 1: 不正なアクセスは記録
  # 2: 記録
  def ipn_log_level
    2
  end
  # 通知の確認をスキップするか(テスト用)
  # trueにした場合、PaypalからのIPNを偽装したアクセスが可能になる。
  # 本番環境では必ずfalse
  def skip_validate?
    false
  end

  private
  # 受信したIPNが本当にPaypalから送信されたものかを確認する。
  def validate_ipn_notification(raw, is_sandbox = false)
    uri = URI.parse("https://www.#{is_sandbox ? 'sandbox.' : ''}paypal.com/cgi-bin/webscr?cmd=_notify-validate")
    http = Net::HTTP.new(uri.host, uri.port)
    http.open_timeout = 60
    http.read_timeout = 60
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    http.use_ssl = true
    response = http.post(uri.request_uri, raw,
                         'Content-Length' => "#{raw.size}",
                         'User-Agent' => "My custom user agent"
    ).body
  end
end

MyPaypalControllerの処理

PaypalControllerのメソッドをオーバーライドし、各種の具体的処理を行う。
下記のメソッド内の処理はあくまでも一例である。

RAILS_ROOT/app/controllers/my_paypal_controller.rb
class MyPaypalController < PaypalController
  def success
    flash[:success] = '購入が処理されました。'
    redirect_to '/hoge'
  end
  def cancel
    flash[:danger] = '購入がキャンセルされました。'
    redirect_to '/hoge/cancel'
  end
  protected
  # 購入が正常に完了した時の処理
  def ipn_completed(params, is_sandbox)
    status = params[:payment_status]
    email = params[:payer_email]
    price = params[:mc_gross].to_i
    btn_id = params[:btn_id]
    txn_id = params[:txn_id]
    item_number = params[:item_number]
    item_name = params[:item_name]
    # パラメータに基づいてDBに情報を登録するなどの処理
  end

  def ipn_overlap(params, is_sandbox)
    # 購入が重複した場合の処理
  end

  def ipn_pending(params, is_sandbox)
    # 購入がPending(未決済)の場合の処理
  end

  def ipn_failed(params, is_sandbox)
    # 購入がFailed(失敗)の場合の処理
  end

  def ipn_invalid(params, is_sandbox)
    # 購入が不正の場合の処理
  end

  def ipn_validate_error(params, is_sandbox)
    # 通知の確認自体が失敗した時の処理
  end

  # 判定関係
  # 同じ購入IDの処理が重複しているかの判定
  def ipn_overlap?(params, is_sandbox)
    txn_id = params[:txn_id]
    YourModel.find_by_txn_id(txn_id)
  end

  # receiver_emailが事業者のPayPalアカウントと一致するかを判定
  def ipn_receiver_email_correct?(email, is_sandbox)
    email == ENV['PAYPAL_EMAIL']
  end

  # 商品の情報に誤りがないかを確認
  def ipn_item_correct?(params, is_sandbox)
    item_number = params[:item_number]
    price = params[:mc_gross].to_i
    btn_id = params[:btn_id]

    item = ItemModel.find__by_item_number(item_number)
    unless item
      return false
    end
    (item.price == price && item.btn_id == btn_id)
  end

  # ログの記録
  # 0: 記録しない
  # 1: 不正なアクセスは記録
  # 2: 記録
  def ipn_log_level
    2
  end

  # 通知の確認をスキップするか(テスト用)
  def skip_validate?
    false
  end
end

10
10
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10