0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Laravel CashierとSTRIPE 決済機能

Last updated at Posted at 2024-02-02

事前準備

laravelのプロジェクト作成。

Stripeのアカウント作成して以下を取得。

・公開可能キー トップページから
・シークレットキー トップページから
・API ID 商品ページから

※そそぞれ本番用とテスト用がある。

cashierをインストール

composer require laravel/cashier

migrate

php artisan migrate

3ファイル実行される。
何も実行されなかったら
\vendor\laravel\cashier\database\migrations
にマイグレーションファイルが3つあるので
\database\migrations
へコピーしてからマイグレーション。

userモデルにBillable追加

App\Models\User.php

use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;

・use Laravel\Cashier\Billable;
・use Billable;
を追加

ローカルテストの為Stripe CLIをDL

参考
https://stripe.com/docs/stripe-cli

ここから「stripe_X.X.X_windows_x86_64.zip 」をDL
https://github.com/stripe/stripe-cli/releases/tag/v1.19.2

laravelのルートにstripe.exeを設置

stripe login

コマンドプロンプトに表示されたURLにアクセスして認証。

stripe listen --forward-to localhost/subscription/webhook

これでコマンドプロンプトでlistenできる。

WebフックのCSRF保護を外す

App\Http\Middleware\VerifyCsrfToken

protected $except = [
    'stripe/*',
];

APIキー

.env

STRIPE_KEY=pk_test_〇〇〇
STRIPE_SECRET=sk_test_〇〇〇
STRIPE_BASIC_ID=price_〇〇〇

事前準備で用意したAPIキーを入力。
まずは本番用ではなくテスト用を入力。

\config\services.php

'stripe' => [
    'pb_key'=>env('STRIPE_KEY'),
    'st_key'=>env('STRIPE_SECRET'),
    'basic_plan_id'=>env('STRIPE_BASIC_ID'),
],

// config('services.stripe.pb_key')
// でキーを取得できる。

※.envはconfigを経由して参照しないと不具合が起こる事があるため。

StripeController.php作成

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Laravel\Cashier\Cashier;
use Stripe\Stripe;
use Stripe\Charge;
use App\Models\User;
use Illuminate\Support\Facades\Log;
use Laravel\Cashier\Events\WebhookReceived;
use Laravel\Cashier\Http\Controllers\WebhookController;
use Illuminate\Support\Carbon;

class StripeController extends Controller
{
    //トライアル日
    const TRIAL_DAYS = 10;

    //トップページ
    public function subscription(Request $request){
        $user = Auth::user();

        return view('stripe',  [
            'intent' => $user->createSetupIntent(),
            'status' => static::status(),
            'user' => $user,
        ]);
    }

    // キャンセルページ
    public function cancelpage(Request $request){
        $input = $request->input();
        $user = Auth::user();
        if(!in_array(static::status()['status'], ['active', 'trial'])){
            return redirect()->route('mypage');
        }

        return view('stripe_cancel',  [
            'status' => static::status(),
            'submit' => $request->has('_token')??null,
        ]);
    }

    // キャンセル
    public function cancel(Request $request) {
        $subsc = Auth::user()->subscription('main');

        if($subsc->onTrial()){
            $subsc->cancelNow();
        }else{
            $subsc->cancel();
        } 

        return redirect()->route('mypage')
            ->with(['success' => "解約致しました。ご利用ありがとうございました。"]);
    }

    //ステータス取得
    public static function status(){
        $user = Auth::user();
        $result =  [
            'status' => '',
            'endDate' => null,
            'endDateNext' => null,
            'trialEnd' => null,
            'nextPayDate' => null,
            'nextPay' => null,
            'cardData' => null,
        ];

        $subsc = $user->subscription('main');
        if(!$subsc) return $result;

        //解約日
        $end = $subsc->ends_at??null;
        if($end){
            $result['endDate'] = $end->format('Y年m月d日')??null;
            $result['endDateNext'] = $end->addDays(1)->format('Y年m月d日')??null;
        } 

        //トライアル終了日
        $trialEnd = $subsc->trial_ends_at??null;
        if($trialEnd) $result['trialEnd'] = $trialEnd->format('Y年m月d日')??null;

        // $user->hasDefaultPaymentMethod(); //デフォルトの支払いを持っているか。

        //次回お支払い日
        $nextPayAttempt = $user->upcomingInvoice()->next_payment_attempt??null;
        if($nextPayAttempt) $result['nextPayDate'] = Carbon::createFromTimestamp($nextPayAttempt)->format('Y年m月d日')??null;
        
        // 次回お支払い金額
        $nextPay = $user->upcomingInvoice()->total??null;
        if($nextPay) $result['nextPay'] = number_format($nextPay).'円';

        // クレジットカード情報
        $result['cardData'] = $user->pm_type.' **** **** **** '.$user->pm_last_four;

        //ステータス
        // trialing(トライアル中):
        // active(アクティブ):
        // past_due(前回の支払い期限超過):
        // canceled(キャンセル):
        // incomplete(不完全):
        // incomplete_expired(不完全(期限切れ)):
        // unpaid(今回未払い):
        // $status = $subsc->stripe_status??null;
        if($subsc->onGracePeriod()){ //解約待機中
            $result['status'] = 'gracePeriod';
        }
        if($subsc->onTrial()){ //トライアル中
            $result['status'] = 'trial';
        }
        if($subsc->ended()){ //解約済
            $result['status'] = 'ended';
        }
        if($subsc->hasIncompletePayment()){ //滞納中
            $result['status'] = 'incomplete';
        }
        if($subsc->recurring()){ //契約中
            $result['status'] = 'active';
        }

        return $result;
    }

    //新規登録
    public function create(Request $request){
        $user = Auth::user();
        $status = static::status();
        if($status['status']=='active') return back()->with(['success' => "ご契約中です。"]);
        if(!in_array($status['status'], ['', 'ended', 'incomplete'])){
            return back()->with(['error' => "登録できませんでした。"]);
        }

        //トライアル日
        $trialDays = 0;
        if(!$status['status'] && StripeController::TRIAL_DAYS??null){
            $trialDays = StripeController::TRIAL_DAYS;
        }

        $paymentMethod = $request->input('stripePaymentMethod'); //支払情報
        $plan = config('services.stripe.basic_plan_id'); //プラン

        $stripeCustomer = $user->createOrGetStripeCustomer();
        $newSubsc = $user->newSubscription('main', $plan);
        //新規申し込みならトライアルを付ける
        if($trialDays) $newSubsc = $newSubsc->trialDays($trialDays);
        $newSubsc->create($paymentMethod);
        // $newSubsc->load('subscriptions');

        return back()->with(['success' => "登録しました。"]);
    }

    // キャンセルを戻す(gracePeriod中のみ)
    public function resume(Request $request) {
        $user = Auth::user();
        if(static::status()['status']=='active') return back()->with(['success' => "ご契約中です。"]);
        if(static::status()['status']!='gracePeriod') return back()->with(['error' => "猶予期間中ではありません。"]);
        if(!$user->hasDefaultPaymentMethod()) return back()->with(['error' => "お支払い方法が登録されていません。"]);
        $user->subscription('main')->resume();
        return back()->with(['success' => "解約を取り消しました。今後ともよろしくお願い致します。"]);
    }

    // カード変更
    public function update_card(Request $request) {
        $paymentMethod = $request->input('stripePaymentMethod'); //支払情報
        Auth::user()->updateDefaultPaymentMethod($paymentMethod);
        return back()->with(['success' => "お支払い方法を変更しました。"]);
    }
}

blade
        @include('div.2')
            <h2 class="text-lg font-medium text-gray-900">有料プラン</h2>
            
            @include('components.message')

            {{-- 契約中 --}}
            @if($status['status']=='active' || is_local())
                <p>契約状況 : 有料プラン<br>
                <br>
                登録中カード : {{$status['cardData']}}<br>
                次回お支払日 : {{$status['nextPayDate']}}<br>
                次回お支払金額 : {{$status['nextPay']}}<br>
                <br>
                <span class="toggleButton">↓↓お支払方法の変更↓↓</span></p>
                {{ Form::open(['url' => route('subscription.update_card'), 'id'=>"payment-form", 'class'=>"toggleContent"]) }}
                    登録アカウント : {{$user->email}} <br><br>
                    <label>クレジットカード名義<input type="test" class="form-control col-sm-5" id="card-holder-name" required></label><br><br>
                    <label>クレジットカード番号<div class="form-group MyCardElement col-sm-5" id="card-element"></div></label><br><br>
                    <div id="card-errors" role="alert" style='color:red'></div>
                    <button class="btn btn-primary" id="card-button" data-secret="{{ $intent->client_secret }}">変更する</button>
                {{ Form::close() }}

                <br>
                <br>
                <br>
                <p style="text-align:right;">解約は<a href="{{route('subscription.cancelpage')}}">こちら</a></p>
            @endif @if(is_local()) @include('div.close') @include('div.2') @endif

            {{-- 解約待機中 --}}
            @if($status['status']=='gracePeriod' || is_local())
                <p>契約状況 : 解約済猶予期間中<br>
                <br>
                失効日 : {{$status['endDate']}}<br>
                <br>
                </p>
                <p>↓↓解約の取り消しはこちら↓↓</p>
                {{ Form::open(['url' => route('subscription.resume')]) }}
                    <button class="btn btn-info">{{$status['endDateNext']}}以降も有料プランを継続</button>
                {{ Form::close() }}
                <br><p>お支払い方法を変更する場合は解約の取り消し後に行えます</p>
            @endif @if(is_local()) @include('div.close') @include('div.2') @endif

            {{-- トライアル中 --}}
            @if($status['status']=='trial' || is_local())
                <p>契約状況 : 無料トライアル中<br>
                    <br>
                    {{$status['trialEnd']}} から有料会員へ切り替わります<br>
                    <br> 
                </p>
                <p style="text-align:right;">解約は<a href="{{route('subscription.cancelpage')}}">こちら</a></p>
            @endif @if(is_local()) @include('div.close') @include('div.2') @endif

            {{-- 滞納中 --}}
            @if($status['status']=='incomplete' || is_local())
                <p>契約状況 : 未清算<br>
                <br>
                登録中カード : {{$status['cardData']}}<br>
                お支払日 : {{$status['nextPayDate']}}<br>
                お支払金額 : {{$status['nextPay']}}<br>
                <br>
                ↓↓お支払方法の変更↓↓</p>
                {{ Form::open(['url' => route('subscription.update_card'), 'id'=>"payment-form"]) }}
                登録アカウント : {{$user->email}} <br><br>
                    <label>クレジットカード名義<input type="test" class="form-control col-sm-5" id="card-holder-name" required></label><br><br>
                    <label>クレジットカード番号<div class="form-group MyCardElement col-sm-5" id="card-element"></div></label><br><br>
                    <div id="card-errors" role="alert" style='color:red'></div>
                    <button class="btn btn-primary" id="card-button" data-secret="{{ $intent->client_secret }}">変更する</button>
                {{ Form::close() }}
                <br>
                <P>利用できなかった期間の料金は発生しておりません</P>
            @endif @if(is_local()) @include('div.close') @include('div.2') @endif

            {{-- 無料会員 --}}
            @if($status['status']=='ended' || $status['status']=='' || is_local())
                <p>契約状況 : 未契約<br>
                <br> 
                <br> 
                ↓↓有料プランへのお申込みはこちら↓↓</p>
                {{ Form::open(['url' => route('subscription.create'), 'id'=>"payment-form"]) }}
                登録アカウント : {{$user->email}} <br><br>
                    <label>クレジットカード名義<input type="test" class="form-control col-sm-5" id="card-holder-name" required></label><br><br>
                    <label>クレジットカード番号<div class="form-group MyCardElement col-sm-5" id="card-element"></div></label><br><br>
                    <div id="card-errors" role="alert" style='color:red'></div>
                    <button class="btn btn-primary" id="card-button" data-secret="{{ $intent->client_secret }}">有料プランへ申込む</button>
                {{ Form::close() }}
            @endif

javascript
{{-- jquery --}}
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
{{-- stripe.js --}}
<script src="https://js.stripe.com/v3/"></script>

<script>
	window.onload = my_init;
	function my_init() {
		const stripe = Stripe("{{ config('services.stripe.pb_key') }}");
		const elements = stripe.elements();

		var style = {
			base: {
			color: "#32325d",
			fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
			fontSmoothing: "antialiased",
			fontSize: "16px",
			"::placeholder": {
			color: "#aab7c4"
			}
		},
		invalid: {
			color: "#fa755a",
			iconColor: "#fa755a"
		}
		};
		
		const cardElement = elements.create('card', {style: style, hidePostalCode: true});
		cardElement.mount('#card-element');

		const cardHolderName = document.getElementById('card-holder-name');
		const cardButton = document.getElementById('card-button');
		const clientSecret = cardButton.dataset.secret;

		cardButton.addEventListener('click', async (e) => {
			e.preventDefault();
			const { setupIntent, error } = await stripe.confirmCardSetup(
				clientSecret, {
					payment_method: {
					card: cardElement,
					billing_details: { name: cardHolderName.value }
					}
				}
			);
			if (error) {
    			console.log('error');
			} else {
    			stripePaymentHandler(setupIntent);
			}
		});
	}
	
	function stripePaymentHandler(setupIntent) {
	var form = document.getElementById('payment-form');
	var hiddenInput = document.createElement('input');
	hiddenInput.setAttribute('type', 'hidden');
	hiddenInput.setAttribute('name', 'stripePaymentMethod');
	hiddenInput.setAttribute('value', setupIntent.payment_method);
	form.appendChild(hiddenInput);
	form.submit();
	}
</script>

雑ですいません。
後で時間ある時に綺麗にします。

参考

0
1
0

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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?