LaravelでPayPay決済を実装する

LaravelでPayPay決済を実装する

みなさんこんにちは塩田です。

今回はLaravelでPayPay決済を導入する流れを簡単にお話をしたいと思います。

 

※PayPayデベロッパーでアカウントを作成すると、テスト用のAPIキーやテスト用アカウントが取得できます。

ただ申請したにもかかわらず、いつまで経ってもAPIキーが生成されないとか、テスト用ユーザーのチャージが出来ないといったことがありましたので、時間が掛かるな〜と思ったら問い合わせるといいと思います。

※また今回はLaravelにおいて【LaravelShoppingcart】というライブラリ等を使用していますが、そのあたりを含め、今回はポイントを絞って説明するためく割愛いたしますので、詳しく知りたい方はコチラをどうぞ。

※全てをカバーするとコードの量が膨大且つ複雑になるため、ポイント以外は割愛しております。予めご了承くださいませ。

 

Laravel

https://readouble.com/laravel/

PayPayデベロッパー(開発者向け)

https://developer.paypay.ne.jp/

 

前提条件

今回は決済の手段として、PayPayの他にクレジット決済もあるものとします。

必要事項を入力して(決済方法も選択)、決済ボタンを押してからの処理が始まります。

なおPayPayの決済手段も複数の規格があるのですが、今回はウェブペイメントを採用します。

実装

paypay SDKをインストールします

composer require paypayopa/php-sdk

決済ボタンが押された際の処理からスタートします

public function checkout(OrderRequest $request): RedirectResponse
{
//正しい注文フローを経ているかのチェック処理等


DB::beginTransaction();
try {
 //注文内容の保存処理
    $order = (new PrepareOrderAction())($request, $cart);

    // 支払い処理
    // \Illuminate\Http\RedirectResponse|null
    $response = (new ApplyPaymentMethodAction())($order, $request);

    DB::commit();
} catch (PaymentMethodException $e) {
    DB::rollback();
//例外処理

} catch (\Throwable $e) {
    DB::rollback();
//例外処理
}

$cart->destroy();

if ($response instanceof \Illuminate\Http\RedirectResponse) {
    return $response;
}

//  決済完了ステータス処理

// 決済確認メール送信処理

// 注文サンキュー処理

}

説明すると、処理をおおまかに2つに分けています。

$order = (new PrepareOrderAction())($request, $cart);

ここで注文内容(注文者や注文内容など)を整理し、DBへ一旦保存する準備を行ったうえで注文内容を返します(確定は後の支払い処理が完了したら正式にcommitします)

class PrepareOrderAction
{
    public function __invoke(Request $request, Cart $cart): Order
    {
//冒頭でメインとなる処理を記述することで、このクラスが何をしているかを容易に理解できる
        $order = $this->createOrder($request, $cart);

        $this->createOrderItems($order, $cart);

        return $order;
    }

    protected function createOrder(Request $request, Cart $cart): Order
    {
//決済手段の判定
        $payment = Payment::query()->find($request->get('payment_id'));

//以下注文者や注文内容を整理して、上の処理に渡す
        $user = Auth::user();

        $order = new Order();

        $order->fill(
            $request->only([
                'user_name',
                'user_email',
                'user_tel',
                'note',
            ])
        );

        $order->fill([
            'total_price' => $cart->total(),
            'quantity' => $cart->count(),
            'receipt_at' => OrderReceipt::getReceipt(),
            'ordered_at' => Carbon::now(),
        ]);

        if ($user) {
            $order->user()->associate($user);
        }

        $order->payment()->associate($payment);

//ここでオーダーステータスを支払い待ちに設定
        $orderStatus = OrderStatus::query()->find(OrderStatusId::PENDING_PAYMENT);

        $order->status()->associate($orderStatus);

        $order->save();

        return $order;
    }

    protected function createOrderItems(Order $order, Cart $cart): void
    {
        $orderDetails = [];

        foreach ($cart->content() as $cartItem) {
            $orderDetail = new OrderDetail();

            $orderDetail->fill([
                'item_name' => $cartItem->model->name,
                'options' => $cartItem->options,
                'price' => $cartItem->price,
                'quantity' => $cartItem->qty,
            ]);

            (new SetStockNumberAction())($cartItem->id, $cartItem->qty);

            $orderDetail->item()->associate($cartItem->model);

            $orderDetails[] = $orderDetail;
        }

        $order->details()->saveMany($orderDetails);
    }
}

 

$response = (new ApplyPaymentMethodAction())($order, $request);

ここでは決済処理を行います。

決済には様々な手段(クレジット、QR、銀行振込、代引きなど・・・)がありますが、追加するたびにコントローラーに処理を追加していくと、

非常に複雑なファットコントローラーが出来上がり、管理運用に支障をきたす可能性もあります。そういったケースを防ぐためにも各決済処理は

コントローラーではなく、下の処理で呼び出すことが望ましいと思います。

class ApplyPaymentMethodAction
{
    public function __invoke(Order $order, Request $request): ?RedirectResponse
    {
//ここで支払い方法呼び出し        
$payment = $order->payment;

        if (!$payment instanceof Payment) {
          //例外処理
        }

//支払い方法の処理呼び出し       
 $paymentMethod = $payment->paymentMethod();

        $paymentMethod->setOrder($order);

        $paymentMethod->setOptions($request->all());

//支払い処理
        return $paymentMethod->checkout();
    }
}

最後の部分

//支払い処理
return $paymentMethod->checkout();

ここで各決済手段による決済を行います。

PayPay決済の場合はここでリダイレクトURLを返すことになります(クレジット決済の場合はnull)

 

以下はPayPay決済用にクラスを使った処理です。

class PayPay implements PaymentMethodInterface
{
    protected Order $order;

    public function checkout(): ?RedirectResponse
    {
//ここでPayPay決済用のリダイレクトURLを発行します
        $QRCodeResponse = (new CreateQrCodeAction($this->order))();
        $url = $QRCodeResponse['data']['url'] ?? null;

        return redirect($url);
    }

    public function setOptions(array $options): void
    {
        //stripe等、トークンが必要な場合はここにその取得処理が入ります
    }

    public function setOrder(Order $order): void
    {
        $this->order = $order;
    }

    public function getOrder(): Order
    {
        return $this->order;
    }


}
//ここでPayPay決済用のリダイレクトURLを発行します
$QRCodeResponse = (new CreateQrCodeAction($this->order))();
$url = $QRCodeResponse['data']['url'] ?? null;

return redirect($url);

クレジットの場合はここで実際の決済を行うのですが、PayPayの場合は決済画面へのリダイレクトURLを返します。

 

以下はQRコード(決済画面)の生成処理です。

class CreateQrCodeAction
{
    protected Order $order;

    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    public function __invoke(): array
    {
        $client = $this->createClient();

        $payload = $this->createPayload();

//ここでQRコード生成!
        $QRCodeResponse = $client->code->createQRCode($payload);

        if ($QRCodeResponse['resultInfo']['code'] !== "SUCCESS") {
   //例外処理
            );
        }
//ここでQRコードを返します
        return $QRCodeResponse;
    }

//以下にQRコード生成のための各処理を記述します

  protected function getMerchantPaymentId(): string
    {
        return "任意の接頭語" . time();
    }

    protected function createClient(): Client
    {
        return new Client(config('PayPayのキー'), false);
    }

    protected function getAmount(): string
    {
        return str_replace(',', '', $this->order->total_price);
    }

    protected function createOrderPaypay(): OrderPaypay
    {
        $orderPaypay = new OrderPaypay;

        $orderPaypay->order()->associate($this->order);

        $orderPaypay->merchant_payment_id = $this->getMerchantPaymentId();

        $orderPaypay->createToken();

        $orderPaypay->save();

        return $orderPaypay;
    }

    public function createPayload(): CreateQrCodePayload
    {
        $orderPaypay = $this->createOrderPaypay();

        $payload = new CreateQrCodePayload();

        $payload->setMerchantPaymentId($orderPaypay->merchant_payment_id);

        $payload->setCodeType("ORDER_QR");

//重要!ここで決済画面へのルートを設定します。
        $payload->setRedirectUrl($orderPaypay->getCallbackUrl());

        $payload->setAmount([
            "amount" => $this->getAmount(),
            "currency" => "JPY"
        ]);

        $payload->setRedirectType('WEB_LINK');

        return $payload;
    }
}
}
//重要!ここで決済画面へのルートを設定します。
$payload->setRedirectUrl($orderPaypay->getCallbackUrl());

上の処理はPayPay処理用のModelを作っており、その中での処理となります。

以下に説明します。

class OrderPaypay extends Model
{
//割愛

    public function createToken(): self
    {
      //トークン生成処理

        return $this;
    }

    public function getCallbackUrl(): string
    {
        return route('任意のルーティング', [
            'token' => $this->token,
        ]);
    }
}

ここでコールバックによるルーティング処理が行われ、実際の決済画面に移ることになります。

以下は決済完了までの処理となります。

class PaypayController extends Controller
{
    public function callback(PaypayCallbackRequest $request): RedirectResponse
    {
//割愛
//トークンの判定や例外処理を実装

        $order = $orderPaypay->order;

        try {
            $response = (new GetPaymentDetailsAction())($orderPaypay->merchant_payment_id);
        } catch (\Throwable $e) {
            report($e);
//例外処理
        }

        if ($response['data']['status'] !== 'COMPLETED') {
            return redirect()
                ->route('任意のルート名')
                ->withErrors([
                    'error' => 'not finished check out',
                ]);
        }

        DB::beginTransaction();

        try {
            // ここで決済完了状態に
            (new CompleteOrderAction)($order);

            DB::commit();
        } catch (\Throwable $e) {
            DB::rollback();

  //例外処理
        }

// 決済完了ステータス処理 

// 決済確認メール送信処理 

// 注文サンキュー処理
    }
}

以上がざっとではありますがPayPayの決済処理の流れとなります。

もちろん各会社様毎にやり方は多々あると思いますので、ご参考までにどうぞというところになります。

まとめ

今回PayPay決済を導入するにあたり、様々なサイトを参考にさせていただきましたが(感謝!!)、主に決済をPayPayに絞った実装だったので、

複数の決済の中からPayPayを選んで処理するために、試行錯誤を重ねてなんとか実装にこぎ着けました。

 

あとPayPayにかかわらず、決済処理にはステータス(その時の状態)処理が非常に重要となります。今回はOrder(注文)モデルにステータスを紐付けて処理を進めていきました。

支払い待ち・決済保留中・決済中・決済完了…などのステータスに基づき処理を進めていくことで正しく決済処理が行われますので、既存のECシステム(ShopifyやECCUBEなど)も参考にしてみてもいいかもしれません。(上のコードにも決済ステータスを更新するタイミングが複数あります)

 

また各処理を実装するにあたり、コントローラーは極力シンプルにし、何が行われているかが直ぐに分かるような書き方が望ましいと感じました。

具体的には今回のように処理(支払いという処理)を共通化させたうえで必要な処理を分けていく、クラスの冒頭でメインとなる処理を書き、下に付属する処理を記述していくことで可読性を高めるなど・・・様々な学びがありましたので積極的に今後に活かしていきたいと思います。

 

最後になりましたが一つ苦労したことを。今回実装を進めていく上で当然エラーを幾つも経験しましたが、それがこちらに原因があるのかPayPay側にあるのかイマイチ分からずに時間をロスしました。

具体的にはAPIキーが発行されない、テスト用のキャッシュがチャージされない、サンドボックスに繋がらないなど・・・これらは結局PayPay側の原因であったものですが、そういう場合は運営側に問い合わせましょう(返事はかなり早い印象でした)。

 

以上長くなりましたがご参考までに。