Stripe Connectで決済をするときの実装メモ

# Connectの実装でやること

## 実現したいこと
* ツーサイドマーケットプレースをやろうとした時に、売り手が商品を出品し買い手がそれを買う
* 決済画面はStripe社の画面を利用する
* できる限り標準機能を活用する


## 環境
* Stripe Connect
* Direct支払い
* アカウントタイプはstandard
* APIバージョン 2020-08-27
* サーバはfirebase cloudfunctionですが、そこに依存しない内容を記載しています

### 仕様
* 出品者のアカウントを作成する
  * STripeにStandardタイプのアカウントを作成する。作成画面へ誘導し登録をしてもらう
  * その人のアカウント登録が完了しているか確認する
* 決済処理を行う
  * 購入を確定したらStripe社が用意するフォームでkっさいをする
* 決済状況を取得する
  * WebHoocで、完了を受信し、購入済みを登録する

### ポイントとなる考え方
* 支払い、などのオブジェクトを持つのは、支払いを受ける側。
* 宛先の指定は、第２引数にstripeAccount属性にstripeidを渡したオブジェクトを乗せる。
* この書き方は、決済情報(paymentIntentや、product商品,price価格を取るときも同じ)　書き方の具体は後述


### 画面の設定
* 使うキーは、どこにあるのか。
* テスト環境と本番環境の切り替え
* ログはどこに出るのか
* webhookを送る先はどこに設定するのか

![スクリーンショット 2022-11-17 20.14.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57455/037aed42-1c98-9844-8cb4-118d5e83fb2e.png)
![スクリーンショット 2022-11-17 20.15.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57455/1bfd58d9-5f1e-a27b-3610-eca59706614c.png)
![スクリーンショット 2022-11-17 20.15.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57455/15674cbe-fef9-c846-8108-7a9d7bf903b4.png)

### サーバ側
* サーバ側だけ記載します。クライアント側はこれを呼び出し処理を進める
* キー値
  * sk_で始まるキー(全ての処理を行うのに必要)
  * whsecで始まるキー(webhookで利用)

```

// cloudfirestre
const admin = require('firebase-admin');　//firebaseでやるため、インポート
admin.initializeApp(functions.config().firebase);
// データベースの参照を作成
var db = admin.firestore()
/* ポイント
stripeライブラリ読み込み時に、sk_ キーを指定します
*/
const stripe = require('stripe')(sk_で始まるキー、sk_test_だと、自動的にテストモードで全てのデータが作成される)
/*
ポイント　どこにレコードが作られるかでハマる。
paymentIntentが、本番かテスト環境か。
アカウントか連結アカウントか、
どこに作られるのかを確認する
*/

/**
 * Stripeアカウントを作成する
 */
const createStripeAccount = async (data, context) => { // この辺の引数はfirebaseのもののため、利用する環境ごとに読み替えてください
    return new Promise(async (resolve) => {
        // standardアカウントを作成
        const account = await stripe.accounts.create({ type: 'standard' });
        console.log("ID " + account.id)
        await stripe.accountLinks.create({
            account: account.id,
            refresh_url: 'url 成功時',
            return_url: 'url 失敗時',
            type: 'account_onboarding',
        }).then(res => {
            console.log('charge success.');
            console.log(res);
            resolve({ message: 'success！', result: res });
            /* ポイントは、
            ここで欲しいのは、
            ・作成されたユーザのstripeid
            ・ユーザ登録を行うstripeが用意する画面へのurl
            これはここでいうと
            resの res.id res.url　に入っている。
            
            なのでここで最低限しなければならないことは
            ・responseにid返して、アプリ側dbのでユーザ情報にstripeidを持たせる。
            　このidに対して、支払い先として決済を作る（後述）することになる
            ・res.urlにリダイレクトさせる
            
            */
        }).catch(err => {
            console.log('getAccount failed.');
            resolve({ error: err.message });
        });
    });
}


/**
 * 口座開設状況の確認
 * Stripe IdでStripeアカウントオブジェクトを取得
 */
const checkStripeUserByIdImple = (data, context) => {
    return new Promise(async (resolve) => {
        let stripeId = data.stripeId;

        /* ポイント　
        stripe.accounts.retrieveの引数にstirpeidを渡すと、アカウントの情報を取得できます
        charges_enabled属性に、決済状況がbool型で返却されます。
        ここでいうと、
        res.charges_enabled
        これがfalseのユーザは、口座登録がまだ完了していない状態です
        */
        await stripe.accounts.retrieve(
            stripeId
        )
            .then(res => {
                console.log('charge success.');
                console.log(res);
                resolve({ message: 'success！', result: res });
            })
            .catch(err => {
                console.log('getAccount failed.');
                resolve({ error: err.message });
            });
    });
}

/**
 * pi_ で始まるidで、決済情報であるペイメントインテントを取得し、支払いが完了しているかを確認する
 */
const getPaymentIntentByPiParam = (pi_, acc_) => {
    return new Promise(async (resolve) => {
        console.log("pi_ : " + pi_);
        console.log("acc_ : " + acc_);


        /* ポイント 
        Connectの場合、支払い先は、あなた(プラットフォーム)ではなく、登録している出品者です
        支払いが行われると、支払いさきに支払い情報が作成されます。
        そのため、誰の支払い情報(paymentIntent)なのかを指定して取得する必要があります
        */
        await stripe.paymentIntents.retrieve(
            pi_, // paymentintetのid
            { stripeAccount: acc_ } // 支払いを受けたユーザのstripeid　← 具体的には、ここです
        )
            .then(res => {
                console.log('getPayment intent Success!!');
                resolve(res);
            })
            .catch(err => {
                console.log('getPaymentIntentByPiParam failed.');
                resolve({ error: err.message });
            });
    });
}


/**
 * Stripe　商品オブジェクト、それに紐づく価格オブジェクトを生成する。
 */
const createProduct = (data, context) => {
    return new Promise(async (resolve) => {
        console.log("createProduct")

        /* ポイント
        以前は、amountに金額を直接指定できたのですが、直近のapiバージョンでは、priceオブジェクトが必要となります
        priceオブジェクトは、商品に紐づいている必要があります
        ですので、商品と価格オブジェクトを作ります

        このpriceオブジェクトを、決済時
        */
        // 商品を作成
        // https://stripe.com/docs/api/products/create
        const product = await stripe.products.create({
            name: data.id + "商品名",
        }, {
            stripeAccount: data.stripeAccount,
        });

        // 価格を作成
        // https://stripe.com/docs/api/prices
        const price = await stripe.prices.create({
            unit_amount: 金額を設定 , //必ず数値型
            currency: 'jpy',
            product: product.id// 先に作った商品のidを設定し、紐付けます
        }, {
            stripeAccount: data.stripeAccount,
        });
        /*ポイント　
        ここも、その商品をどのアカウントのもとに作るのかを指定します
        第2引数に、連結アカウント(出品者)のstripeidを指定し、そのユーザの下に商品、価格データを作ります
        */
        const result = { product: product, price: price }
        resolve(result);

    });

}

/**
 * 決済処理
 */

const createCheckoutLinkImple = (data, context) => {
    return new Promise(async (resolve) => {
        try {

            const priceId = data.price.id; // 引数に先ほど作った価格オブジェクトが渡ってきていると読み替えてください

            await stripe.checkout.sessions.create({
                payment_method_types: ['card'],
                /* ポイント
                line_items に、価格や量などを設定します
                connectはプラットフォームなので、成約手数料をpayment_intent_data.application_fee_amountに設定します
                ここでは、価格の２割を指定しています
                支払い先を、第2引数　stripeAccount　に設定します。このユーザのstripeアカウントに対して送金が行われます
                そのとき、手数料２割が天引きされ、あなた（プラットフォーム）のアカウントに送金されます
                */
                line_items: [{
                    price: priceId,
                    quantity: 1
                }],
                payment_intent_data: {
                    application_fee_amount: (parseInt(product.price.unit_amount * 0.2))
                },
                mode: 'payment',
                success_url: process.env.FQDN_TEST,
                cancel_url: process.env.FQDN_TEST
            }, {
                stripeAccount: data.stripeId,
            }).then((session) => {
                resolve({ message: 'createCheckoutLinkImple success！', result: session });

            /*
            ポイント
　　　　　　　　　　　　　　　　　　　　　　このsessionに決済ページのurlが含まれるため、そこへユーザをリダイレクトする
　　　　　　　　　　　　　　　　　　　　　　このsessionの中に、cs_ で含まれるidがある。この値をアプリ側の購入履歴として持たせておき、
　　　　　　　　　　　　　　　　　　　　　　後でwebhookで、決済が完了通知された時に、どの購入が完了したのか紐付けられるようにする
            */
            }).catch(err => {
                console.log('createCheckoutLinkImple failed.');
                resolve({ error: err });
            });
        } catch (e) {
            console.log(e)
            resolve({ error: e });
        }
    });


}



/*
* webhookの受信
*/
exports.stripe_webhook_app4 = functions.https.onRequest(async (request, response) => { // cloudfunctionの書き方のため適宜読み替えてください


    console.log("stripe_webhook ==========")
    const sig = request.headers['stripe-signature'];

    let event;
    try {
        // この通知がstripeから送られてきたものであることを確認するためにキー認証します
        // ボディのrawデータ、署名ヘッダー、署名シークレットを指定しイベントを初期化
        event = stripe.webhooks.constructEvent(request.rawBody, sig, 'whsec_で始まるキー');
    } catch (err) { // 不正なリクエストの場合
        return response.status(400).send(`Webhook Error: ${err.message}`);
    }

    switch (event.type) { // イベントのタイプに応じて処理を行う
    // checkout.sessionの決済完了をとれるのは　'checkout.session.completed'
        case 'checkout.session.completed':
            const asyncSucceeded = event.data.object;
            try {
                //const paymentIntentObj = await getPaymentIntentByPiParam(asyncSucceeded.payment_intent, event.account)
                const stripeAccount = event.account;

                // この決済の購入情報を別途取得する
                // 商品名は item.data[0].description
                const item = await stripe.checkout.sessions.listLineItems(asyncSucceeded.id, { stripeAccount: stripeAccount })
                // 買った商品を、アプリ側dbで有効化
                
                /* ここで、購入が完了した決済(eventから)、商品(itemから)の情報が取得できる
                ポイントは、決済情報PaymentIntentはここで初めてとれる(session.create時点では取れない仕様に変更された)
                そのため、session.createで購入した商品と、ここでwebhoocで通知との対応づけをするには、checkout.sessionののidを利用する
                それが、cs_で始まるコードであり、eventオブジェクトのid属性に含まれている

                event.idで得られたcs_ キーで、 先に購入登録時に保存されたアプリ側の購入データを検索し、購入済みステータスをここで更新する

                */

                })


            } catch (e) {
                console.log("========== WEB HOOK 決裁情報の取得に失敗===============")
                console.log(e)

                console.log("=========================")
            }
            return response.json({ received: true }); // ステータス200でレスポンスを返却
            break;
        default: { // 想定していないイベントが通知された場合
            console.log("====WEBHOC ERROR====")
            console.log("Stripe Webhoc 想定していないイベントを受領 ========");

            console.log(event.type)
            console.log("========")
            return response.status(400).end(); // ステータス400でレスポンスを返却
        }
    }
});


```


## 参考
https://stripe.com/docs/connect


Create a Session
https://stripe.com/docs/api/checkout/sessions
Create、LineItem、Retrive、などを使っていく

Stripe Checkoutで「カートの中身」または「実際に注文された商品」の種類と数を取得する方法
https://qiita.com/hideokamoto/items/565e3fbba33885be726e
checkoutの取り回し、大変わかりやすく助けられました

webhook
ttps://stripe.com/docs/webhooks/stripe-events?locale=ja-JP