Laravel CashierとSTRIPE 決済機能

## 事前準備

laravelのプロジェクト作成。

[Stripe](https://stripe.com/jp)のアカウント作成して以下を取得。

**・公開可能キー**　トップページから
**・シークレットキー**　トップページから
**・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: 
<?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' => "お支払い方法を変更しました。"]);
    }
}

```

```php: 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: 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>
```

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

## 参考

https://readouble.com/laravel/7.x/ja/billing.html