EC-CUBE×Stripeで疑似サブスク機能「Stripeの実装部分」

noteに[EC-CUBE×Stripeで疑似サブスクリプション](https://note.com/belltzel/n/n7517c672846b)の記事を書きました。
その技術的な部分、Stripeの実装をここに記す！！

EC-CUBE側は結構なカスタムを行っているので、この部分は省きます。

#Stirpeで利用した機能
・The Setup Intents API（[docs](https://stripe.com/docs/payments/setup-intents)）
・Set up future payments（[docs](https://stripe.com/docs/payments/save-and-reuse)）

・SetupIntents（[docs API](https://stripe.com/docs/api/setup_intents)）
・PaymentMethods（[docs API](https://stripe.com/docs/api/payment_methods)）
・PaymentIntents（[docs API](https://stripe.com/docs/api/payment_intents)）
・Customers（[docs API](https://stripe.com/docs/api/customers)）

##EC-CUBEに実装した大まかな流れ
１．決済フロー「ご注文手続き」でSetupIntentsの作成、payment_method_idを取得
２．決済フロー「checkout」でCustomer作成、PaymentMethodにCustomerをアタッチ
３．決済フロー「checkout」でCustomerId、PaymentMethodIdを保存

##SetupIntents, PaymentMethods, Customers

SetupIntentsは顧客のカード情報の認証情報を設定して保存できます。
すぐに支払いをせずにPaymentIntentsを利用して後から支払いできます。

###カード情報の収集とSetupIntentsの作成

SetupIntentsを作成し、戻ってきた情報からpayment_methodのIDをcheckout時に保存します。

下記はEC-CUBEの決済フロー（ご注文手続き）に実装しています。
EC-CUBEに関わる部分は邪魔なので削除しています。

```html:カード情報入力フォーム
<div class="ec-orderPayment">
    <div class="ec-rectHeading">
        <h2>クレジットカード情報</h2>
        <div id="card-element" ></div>
        <div id="card-errors" role="alert"></div>
    </div>
</div>
<script src="https://js.stripe.com/v3/"></script>
```

```javascript:
<script>
    const publicKey = '{{ publicKey }}';

    let stripe = Stripe(publicKey,{betas: ['payment_intent_beta_3']});
    let elements = stripe.elements();

    // スタイルのカスタマイズ
    let style = {
        base: {
            color: '#32325d',
            fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
            fontSmoothing: 'antialiased',
            fontSize: '16px',
            '::placeholder': {
                color: '#aab7c4'
            }
        },
        invalid: {
            color: '#fa755a',
            iconColor: '#fa755a'
        }
    };

    // クレジットカード情報入力欄の構築
    let card = elements.create('card', {style: style, hidePostalCode: true});
    card.mount('#card-element');

    // 入力変更時のリスナー
    // バリデーションメッセージを表示する
    card.addEventListener('change', function(event) {
        let displayError = document.getElementById('card-errors');
        if (event.error) {
            displayError.textContent = event.error.message;
        } else {
            displayError.textContent = '';
        }
    });

    // submit時のリスナー
    let setupIntent;
    let form = document.getElementById('form-id');//用意したFORMのIDを設定してください。
    form.addEventListener('submit', async function(event) {
        event.preventDefault();

        if($.isEmptyObject(setupIntent)){
            await getSetupIntent();
        }

        if(setupIntent !== undefined){
            stripe.confirmCardSetup(setupIntent.client_secret, {
                payment_method: {
                    card: card
                }
            }).then(function(result) {
                if (result.error) {
                    stripeDisplayErrorMessage(result.error.message);
                } else {
                    // The PaymentMethod was successfully set up
                    stripePaymentMethodHandler(result.setupIntent.payment_method);
                }
            });
        }
    });

    function stripePaymentMethodHandler(payment_method) {
        let hiddenInputToken = document.createElement('input');
        hiddenInputToken.setAttribute('type', 'hidden');
        hiddenInputToken.setAttribute('name', 'stripe_payment_method_id');
        hiddenInputToken.setAttribute('value', payment_method);
        form.appendChild(hiddenInputToken);

        // Submit the form
        form.submit();
    }

    let getSetupIntent = function() {
        return fetch('{{ setupIntent作成PHPのURL }}', {
            method: "post",
            headers: {
                "Content-Type": "application/json",
            }
        }).then(function(response) {
            return response.json();
        }).then(function(setupIntentObj) {
            if (setupIntentObj.error) {
                stripeDisplayErrorMessage(setupIntentObj.error.message);
            } else {
                setupIntent = setupIntentObj;
            }
        });
    };

    function stripeDisplayErrorMessage(message) {
        let displayError = document.getElementById('card-errors');
        displayError.textContent = message;
    }
</script>
```

```php:setupIntent作成PHP
use Stripe\Stripe;
use Stripe\SetupIntent;

public function setup_intent()
{
    Stripe::setApiKey('{{ secretKey }}');

    $setupIntent = SetupIntent::create([
        'payment_method_types' => ['card'],
    ]);

    return $setupIntent;//jsonに変換してください。
}
```

###Customerの作成とPaymentMethodにCustomerをアタッチ、ID保存

checkoutはEC-CUBEの決済プラグインを参考に作成しています。
[https://github.com/EC-CUBE/sample-payment-plugin](https://github.com/EC-CUBE/sample-payment-plugin)

```php:checkout
use Stripe\Customer;
use Stripe\PaymentMethod;
use Stripe\Stripe;

public function checkout()
{
    Stripe::setApiKey('{{ secretKey }}');

    //Customer作成
    $StripeCustomer = Customer::create([
        'name' => 'name',
        'email' => 'email',
    ]);

    //CustomerをpaymentMethodへアタッチ
    $payment_method = PaymentMethod::retrieve('{{ paymentMethodId }}');
    $payment_method->attach(['customer' => $StripeCustomer->id]);

    //それぞれのIDを保存（略）
}
```

##PaymentIntents

CustomerId, paymentMethodIdを利用してPaymentIntentsで決済を行います。
この部分はEC-CUBEの[command](https://doc4.ec-cube.net/customize_symfony#command)を作成してcronで定期的に実行しています。

```php:決済
use Stripe\Stripe;
use Stripe\StripeClient;

$stripe = new StripeClient('{{ secretKey }}');

$createPaymentIntent = $stripe->paymentIntents->create([
    'amount'   => 1000,
    'currency' => 'jpy',
    'customer' => {{ customer_id }},
    'payment_method' => {{ payment_method_id }},
    'off_session' => true,
    'confirm' => true,
]);
```

以上！！