【Laravel】PayPalで決済を行う

えび

PayPalのサービスを使用して、LaravelプロジェクトでPayPal決済を実装してみる

今回は、PayPalのAPIを直接使用するのではなく、
PayPal JavaScript SDKを利用してさくっと決済を行う

ざっくり流れ

  1. PayPalビジネスアカウント登録・APIアプリを作成して認証情報取得
  2. テスト決済用のSandboxアカウントを作成
  3. Laravelの環境変数にPayPal APIアプリの認証情報を設定
  4. LaravelのViewにてPayPal JavaScript SDKを使用してPaypal決済ボタン表示
  5. テスト決済してみる
  6. 決済完了後、LaravelのDBに決済情報を保存しておく

PayPalビジネスアカウント登録 & APIアプリ作成

公式が用意してくれているドキュメントをもとに進めていく

step1: PayPalビジネスアカウント登録
上記リンクからビジネスアカウント登録を行う
メールアドレス認証も忘れずに

step2: REST APIアプリ作成
ダッシュボードにログインして今回決済に使用するためのアプリを作成する

  • App Name」入力・Merchant」を選択・「Sandbox」を選択して
    最下部の「Create App」ボタン押下

独自のSandboxアカウントを使用したい場合はこちらから登録できる

  • 作成後に表示されるAPI CREDENTIALS認証情報をメモしておく
    Secret」の内容は「show」リンク押下で確認可能

ここの「Sandbox account」が、
後述の「Seller(売り手)」アカウントとなるので覚えておく

これで登録・APIアプリ作成は完了
続いてテスト用アカウントの作成に移る

テスト決済に使用するSandboxアカウントを作成

今回はSandbox (=PayPalが用意してくれるテスト環境) で確認を進めていく

Sandboxを使用することで、
実際にPayPalにお金を入金しなくてもテスト決済などを行うことができるので便利

Sandbox決済テストを行うにあたり、
テスト用の「Buyer(買い手)」アカウントと、「Seller(売り手)」アカウントが必要になる
ただ「Seller(売り手)」アカウントは、前述のAPIアプリ作成時にデフォルトのSandboxアカウントを紐づけたので作成不要
残りの「Buyer(買い手)」アカウントのみを作成する

step1: アカウント一覧画面へアクセス & 「Create account」ボタン押下

step2: アカウント作成モーダルで「Account Type」と「Country」を選択 & 「Create」ボタン押下
すると、ランダムな情報で生成されたSandboxアカウントが一覧に追加される

step3: 作成後、一覧の右端の三点リーダーの「View/edit account」から編集モーダル表示
ここで認証に必要なEmailやパスワードの確認・変更ができる

このアカウントの認証情報を決済時に使用するので覚えておく

Funding」タブを押下すると、テストユーザーが保持しているPayPal金額の変更も可能

もっと丁寧な公式の作成方法ドキュメントはこちら

続いて、Laravelの設定

Laravelの.env & configにPayPal APIアプリ認証情報設定

Laravelの環境変数に、PayPalで作成したAPIアプリの認証情報を設定しておく
これは、決済ボタン表示の際などに使用する

  • .envに追記
# PayPal API Credentials
# sandbox (テスト)
PAYPAL_CLIENT_ID=XXXXXXXXXXXXXXXXXX
PAYPAL_CLIENT_SECRET=XXXXXXXXXXXXXXXXXX
# live (本番)
# PAYPAL_CLIENT_ID=
# PAYPAL_CLIENT_SECRET=

PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET の値に
PayPalのApp作成時にメモした認証情報の値を設定する
忘れた場合は、App一覧から対象のAppの詳細画面に遷移すると確認できる

  • config/services.phpに追記
    .envの値を直接使用するとキャッシュなどが邪魔をするのでconfigに設定する
'paypal' => [
    'client_id'         => env('PAYPAL_CLIENT_ID', ''),
    'client_secret'     => env('PAYPAL_CLIENT_SECRET', ''),
],

これでconfig('services.paypal.client_id')というように呼び出し可能になった

続いて、決済ボタンの表示

Paypal決済ボタンを表示してみる

Paypal決済ボタンPayPal JavaScript SDKを利用して表示する

  • payment.blade.php (viewファイルは任意のものを使用でOK)
<section>
    <!-- ⭐️ PayPalボタンが自動生成される場所 -->
    <div id="paypal-button-container"></div>
</section>

<!-- Include the PayPal JavaScript SDK -->
<script src="https://www.paypal.com/sdk/js?client-id={{ config('services.paypal.client_id') }}¤cy=JPY&locale=ja_JP&disable-funding=card&intent=capture"></script>
<script>
  paypal.Buttons({
    // ⭐️ PayPalボタンが押下された際に実行される (決済内容の設定を行う)
    createOrder: function(data, actions) {
      return actions.order.create({
        purchase_units: [{
          amount: {
            value: '100',
          },
          description: 'xxxxxxxxxxx',
        }],
      })
    },

    // ⭐️ PayPalログイン・支払い承認後実行される
    onApprove: function(data, actions) {
      return actions.order.capture().then(function(orderData) {
        // 決済が成功した場合
        console.log('Capture result', orderData, JSON.stringify(orderData, null, 2))
        const transaction = orderData.purchase_units[0].payments.captures[0]
        alert('Transaction '+ transaction.status + ': ' + transaction.id)
      })
    }
  }).render('#paypal-button-container')
</script>

流れ
  1. PayPal JavaScript SDKをインポートする
  2. paypal.Buttons({...}).render('#paypal-button-container');で、
    PayPal決済ボタンのアクション設定 & #paypal-button-containerに表示反映を行う
  3. 「PayPal決済ボタン」を押下時にpaypal.Buttons({...})で設定した
    createOrderが実行される (決済情報の設定 & 決済モーダル表示)
  4. 決済モーダルで「ログイン & 支払い承認」を行うと、
    paypal.Buttons({...})で設定したonApproveが実行される
    ※ここではひとまずconsole.log及びalertで決済結果をデバッグしているだけ

PayPal JavaScript SDKに関して補足

今回はPayPalが用意してくれているJS (https://www.paypal.com/sdk/js)をインポートすることで各アクションをお手軽に使用している

今回インポートしているURLは下記
https://www.paypal.com/sdk/js?client-id={{ config(‘services.paypal.client_id’ )}}&currency=JPY&locale=ja_JP&disable-funding=card&intent=capture
引数でオプションを指定していて、内容はざっくり下記の通り

  • client-id: 作成したPaypal APIアプリのclient-id (=.envに記載したPAYPAL_SANDBOX_CLIENT_IDと同様のものなのでconfigで取得)
  • currency: 通貨種別
  • locale: 言語
  • disable-funding: 非表示にしたいボタンがあれば指定する
    今回はPayPalボタンのみでよかったのでクレジットカードボタンを非表示にしてる

(disable-fundingに何も指定しなかった場合の表示はこちら↓)

  • intent: 決済種別
    captureを指定すると即時に決済が確定され、athorizeにすると与信のみを取得して決済の確定は行われない

他、設定パラメータ一覧はこちら

決済テストしてみる

上記で、Sandbox (テスト環境) に決済が行えるようになった

  • viewに自動生成された「PayPal」決済ボタンを押下する
  • ログインモーダルが表示される

ここでは先ほど作成した「Buyer(買い手)」アカウントでログインを行う
ログイン情報を忘れた場合はSandboxのアカウント一覧の各レコード右端の三点リーダー「View/edit account」を押下すると表示されるモーダルで確認可能

  • ログイン後のモーダルの下部の「今すぐ支払う」ボタンを押下する

今すぐ支払う」ボタンを押下すると、
paypal.Buttons({...})で設定したonApproveが実行されて、
Transaction情報がデバッグ表示される

1. 決済を実行した「Buyer(買い手)」アカウントでログイン

2. 同ダッシュボードを右上「ログアウト」ボタン押下でログアウト
続いて「Seller(売り手)」アカウント (App作成時に紐付けたSandboxアカウント) でログイン

両方ともちゃんと反映されてるのでOK

金額を動的にする & 決済完了後、DBに情報を登録する

上記サンプルソースは、金額固定 & 決済完了後デバッグするだけの暫定処理なので、
実際の運用に向けて改修してみる

  • payment.blade.php改修

改修1: 金額入力フォーム追加
金額が直接指定されていたので、入力フォームを追加
改修2: createOrderで入力された金額を動的に使用する
改修3: 決済情報登録用API (後述で作成) に非同期通信でPOSTする

<section>
    <!-- ⭐️改修1: 金額入力フォーム追加 -->
    <input type="number" name="price" placeholder="金額を入力してください" />
    <!-- PayPalボタンが自動生成される場所 -->
    <div id="paypal-button-container"></div>
</section>

<!-- Include the PayPal JavaScript SDK -->
<script src="https://www.paypal.com/sdk/js?client-id={{ config('services.paypal.client_id') }}¤cy=JPY&locale=ja_JP&disable-funding=card&intent=capture"></script>
<script>
  paypal.Buttons({
    // PayPalボタンが押下された際に実行される (決済内容の設定を行う)
    createOrder: function(data, actions) {
      return actions.order.create({
        purchase_units: [{
          amount: {
            // ⭐️改修2: 入力フォームの値を動的に使用する
            value: document.querySelector('[name=price]').value,
          },
          description: '○○に関する決済です'
        }],
      });
    },

    // PayPalログイン・支払い承認後実行される
    onApprove: function(data, actions) {
      return actions.order.capture().then(function(orderData) {
        // 決済が成功した場合
        const transaction = orderData.purchase_units[0].payments.captures[0];
        // ⭐️改修3: 決済情報レコードを登録
        fetch('/api/paypal/order-capture', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            user_id: "{{ auth()->user()->id }}",
            amount: document.querySelector('[name=price]').value,
            transaction_status: transaction.status,
            transaction_id: transaction.id,
          }),
        }).then(function (response) {
          return response.clone().json()
        }).then(function (json) {
          if (json.status === 'ok') {
            alert('決済が完了しました!')
            // 画面遷移など必要であれば実装
          }
        }).catch(error => {
          console.log(error)
        })
      });
    }
  }).render('#paypal-button-container');
</script>

もし入力フォームが不要な場合で変数を使用したい場合はこのように直接渡せばOK

createOrder: function(data, actions) {
  return actions.order.create({
    purchase_units: [{
      amount: {
        value: "{{ $item->price }}", // ⭐️ 変更
      },
      description: '○○に関する決済です'
    }],
  });
},

続いて、上記の非同期通信で使用する決済情報登録用APIをLaravel側に実装する

  • routes/api.phpにルーティング追加
Route::namespace('App\Http\Controllers\Api')->group(function () {
  Route::post('/paypal/order-capture', 'PaypalController@orderCapture');
});

  • app/Http/Controllers/Api/PaypalController.phpを新規作成
<?php

namespace App\Http\Controllers\Api;

use App\Models\Transaction;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class PaypalController extends Controller
{
    /**
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function orderCapture(Request $request): JsonResponse
    {
        // ⭐️ TODO: ここは使用するテーブル構造などに合わせて変更する
        Transaction::create([
            'user_id' => $request->user_id,
            'amount' => $request->amount,
            'paypal_transaction_id' => $request->transaction_id,
            'paypal_transaction_status' => $request->transaction_status,
        ]);

        return response()->json(['status' => 'ok']);
    }
}

これで、Paypal決済取引情報をDBに保存できた
DBに保存されたtransaction_idを使用すると、
取引一覧で対象の取引の検索などを行うこともできて返金対応などもスムーズになるはず

今回は.envのPAYPAL_SANDBOX_CLIENT_SECRET (=Paypal API アプリのSecretキー) は使用していない
もし今後APIを直接叩く必要ができた場合などに必要になるので念のため設定だけしてる