Wix VeloでStripe Checkoutを使ったオーソリとキャプチャを分離した決済導入

# はじめに
WIXでマッチングサービスをVeloを用いローコードで構築した際にStripeで決済を行ったのでその方法とポイントを記載していきます。

今回WIX PayでなくStripeを用いたのはオーソリとキャプチャを分離したいという要望があったためです。
ここについては後で詳しく解説します。
https://docs.stripe.com/payments/accept-a-payment?platform=web&ui=stripe-hosted#auth-and-capture
>オーソリとキャプチャーを分離して、支払いはすぐに作成しますが、売上のキャプチャーは後で行います。

今回Stripeの中でもStripe Checkoutを用いたのでその解説になります。

またPayment Linkではプログラムから任意の商品名、価格の商品の追加ができないので、Checkoutを使用して任意の商品名、価格の決済が行えるようにも設定します。

# Stripe Checkoutとは
Stripe CheckoutとはJavascriptを埋め込むかチェックアウトセッションのURLにリダイレクトを行うことで、美しいデザイン・UIの決済画面で決済が可能になります。
CheckoutはStripe側で決済を行うため自サービスの中でカードの決済処理などを実装しなくてもよく、セキュリティ面も独自に行うより、優れている方法になります。

今回は埋め込みよりリダイレクトによる方法で実装しました。

# Wix VeloでのStripe Checkoutの設定方法
## Stripeとのアカウント連携
まず、Stripeアカウントを作成し、APIキーを取得します。これらのキーをWixのVeloプロジェクトに統合することで、Stripeの機能を使用できるようになります。

## Wix VeloプロジェクトへのStripeライブラリの組み込み
WixエディタでVeloを有効化し、プロジェクトに必要なStripeライブラリを組み込みます。これには、公式ドキュメントを参考にしながらJavaScriptコードを追加する必要があります。
画像のようにnpmパッケージの赤枠のプラスボタンから追加を行います。
![](https://storage.googleapis.com/zenn-user-upload/28765389b2fd-20240609.png)
*npmパッケージの追加*

![](https://storage.googleapis.com/zenn-user-upload/ffedc0a93413-20240609.png)
*Stripeライブラリの取り込み*

import文をbackendコードから呼び出せば利用できます。
```javascript
import Stripe from 'stripe';
const apiKey = await wixSecretsBackend.getSecret('STRIPE_SECRET_KEY');
const stripe = new Stripe(apiKey);
```

# 任意の商品名と価格での決済処理の実装
Stripe Checkoutはstripe.checkout.sessions.createによってセッションの作成を行います。

## 商品情報の設定方法
line_itemsに商品の情報を設定しますが、公式では以下のようになっていて、事前にPRICE IDの作成が必要になります。
```javascript
const session = await stripe.checkout.sessions.create({
    line_items: [
      {
        // Provide the exact Price ID (for example, pr_1234) of the product you want to sell
        price: '{{PRICE_ID}}',
        quantity: 1,
      },
    ],
    mode: 'payment',
    success_url: `${YOUR_DOMAIN}/success.html`,
    cancel_url: `${YOUR_DOMAIN}/cancel.html`,
  });
```

今回はline_itemsに任意の商品を設定する方法を記載します。
以下のようにproduct_dataに商品名と商品の説明を記載し、unit_amountに商品価格を設定します。
```javascript
const session = await stripe.checkout.sessions.create({
    line_items: [{
        price_data: {
            currency: 'jpy',
            product_data: {
                name: "テスト料理",
                description: "てすとてすとてすとてすと",
            },
            unit_amount: 22015,
        },
        quantity: 1,
    }, ],
    customer_email: test@test.com, // emailを設定
    mode: 'payment',
    success_url: `${returnUrl}/?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: cancelUrl,
});
```
![](https://storage.googleapis.com/zenn-user-upload/894cd050dad5-20240609.png)
*チェックアウト画面*

## metadataの設定
stripe.checkout.sessions.createにはメタデータの設定が可能です。
フルフィルメントを実行するにはwebhookを受信したときに行うべきなので、メタデータを設定し、そのデータのフルフィルメントを実行するようにします。
またStripeのダッシュボードから決済が終えるようにもなるので、注文のIDなどはここに設定するのがいいかと思います。
payment_intent_data.metadataとmetadataを設定します。

```javascript
const session = await stripe.checkout.sessions.create({
    line_items: [{
        price_data: {
            currency: 'jpy',
            product_data: {
                name: "テスト料理",
                description: "てすとてすとてすとてすと",
            },
            unit_amount: 22015,
        },
        quantity: 1,
    }, ],
    customer_email: "test@test.com", // emailを設定
    mode: 'payment',
    payment_intent_data: {
        metadata: {
            ...orderData
        }
    },
    success_url: `${returnUrl}/?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: cancelUrl,
    metadata: {
        ...orderData
    }
});
```

Stripeのダッシュボードから追跡可能です。
![](https://storage.googleapis.com/zenn-user-upload/764e9b120aaa-20240610.png)

## Stripe Checkoutセッションの作成
次に、Stripe Checkoutセッションを作成します。このセッションを通じて、ユーザーが決済を完了できるようになります。

```javascript
import { Permissions, webMethod } from "wix-web-module";
import wixSecretsBackend from 'wix-secrets-backend';

import Stripe from 'stripe';

/**
 * StripeのCheckoout APIでの決済
 */
export const createCheckoutSession = webMethod(
    Permissions.Anyone,
    async (returnUrl, cancelUrl) => {
        const apiKey = await wixSecretsBackend.getSecret('STRIPE_SECRET_KEY');

        try {
            const stripe = new Stripe(apiKey);
            const session = await stripe.checkout.sessions.create({
                line_items: [{
                    price_data: {
                        currency: 'jpy',
                        product_data: {
                            name: "テスト料理",
                            description: "てすとてすとてすとてすと",
                        },
                        unit_amount: 22015,
                    },
                    quantity: 1,
                }, ],
                customer_email: "test@test.com", // emailを設定
                mode: 'payment',
                payment_intent_data: {
                    metadata: {
                        ...orderData
                    }
                },
                success_url: `${returnUrl}/?session_id={CHECKOUT_SESSION_ID}`,
                cancel_url: cancelUrl,
                metadata: {
                    ...orderData
                }
            });

            return session;
        } catch (err) {
            console.log('createCheckoutSession Error: ', err);
            return {
                error: {
                    message: err.message,
                }
            };
        }
    }
);
```

# WebHookでイベント受信
Stripeでは各種イベントをWebHookで受信することができイベント通知を受け取る事ができます。
そこでWixで受信する方法を記載します。
`https://<ドメイン名>//_functions/<関数名>`で受信が可能です。

Create New Endpointからcheckout.session.completedイベントを選択し、Stripe上で設定を行います。
![](https://storage.googleapis.com/zenn-user-upload/261f11f1bede-20240610.png)
*Create New Endpoint*

## backend/http-functions.js
Wixではhttp-functionsでWebHookの待受が可能です。
checkout.session.completedイベント受信したら注文情報の生成などの、フルフィルメントを実行します。

```javascript
import { ok, badRequest } from 'wix-http-functions';
import wixData from 'wix-data'
import wixSecretsBackend from 'wix-secrets-backend';
import Stripe from 'stripe';

/**
 * StripeのWebhookの入口
 */
export async function post_stripeWebhook(request) {
    console.info("stripeWebhook Start: ", request);
    let response = {
        "headers": {
            "Content-Type": "application/json"
        }
    };

    const apiKey = await wixSecretsBackend.getSecret('STRIPE_SECRET_KEY');
    const endpointSecret = await wixSecretsBackend.getSecret('STRIPE_ENDPOINT_SECRET');
    const stripe = new Stripe(apiKey);

    // Stripeからの署名を検証
    const sig = request.headers['stripe-signature'];

    let event;
    try {
        const body = await request.body.text();
        event = stripe.webhooks.constructEvent(body, sig, endpointSecret);
    } catch (err) {
        // 署名の検証に失敗した場合
        console.error(`Stripe signatureの検証に失敗: ${err.message}`);
        response.body = {
            "error": err.message,
        }
        return badRequest(response);
    }

    switch (event.type) {
    case 'checkout.session.completed': {
        try {
            await handleAuthorizationSuccess(event.data.object);
        } catch (err) {
            console.error("handleAuthorizationSuccess Error: ", err);
            response.body = {
                "error": err,
            }
            return ok(response);
        }
        break;
    }
    default:
        console.error(`Unhandled event type ${event.type}`);
    }

    return ok(response);
}

/**
 * フルフィルメント実行
 */
async function handleAuthorizationSuccess(paymentIntent) {
    const orderData = paymentIntent.metadata;

    orderData.reservationDate = new Date(orderData.reservationDate);
    orderData.paymentId = paymentIntent.id;
    orderData.totalAmount = parseInt(orderData.totalAmount);

    // すでにデータが有るかチェック
    const result = await wixData.query("orderData").eq("paymentId", paymentIntent.id).find();
    if (result.items.length > 0) {
        console.warn("すでに存在する注文データです。:", orderData);
        return;
    }

    // オーダ作成
    let orderResult = null;
    try {
        // 注文情報
        orderResult = await wixData.insert('orderData', orderData);

        console.log('orderData', orderResult);
    } catch (err) {
        console.error('オーダーの作成に失敗しました。', err);
        throw err;
    }

    return true;
}
```

# オーソリ（認証）とキャプチャ（確定）の分離
決済のオーソリゼーションとキャプチャを分ける利点と、それを実現する具体的な実装方法について詳述します。これにより、トランザクションの柔軟性が高まり、事業者がよりコントロールしやすい決済処理を実現できます。

## オーソリの実行方法
オーソリを実行するにはstripe.checkout.sessions.createでpayment_intent_dataのcapture_methodに"manual"を設定します。
```javascript
const session = await stripe.checkout.sessions.create({
    line_items: [{
        price_data: {
            currency: 'jpy',
            product_data: {
                name: "テスト料理",
                description: "てすとてすとてすとてすと",
            },
            unit_amount: 22015,
        },
        quantity: 1,
    }, ],
    customer_email: "test@test.com",
    mode: 'payment',
    payment_intent_data: {
        capture_method: 'manual', // オーソリのみ実施
        metadata: {
            ...orderData
        }
    },
    success_url: `${returnUrl}/?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: cancelUrl,
    metadata: {
        ...orderData
    }
});
```

## キャプチャータイミングの制御
paymentIntentIdがあればキャプチャが可能です。
WebHookのhttp-functionsのhandleAuthorizationSuccessでpaymentIntent.idをコレクションに保存しておいて、キャプチャのタイミングで渡すことでキャプチャーを任意のタイミング実行可能です。
```javascript
/**
 * 支払いをキャプチャする処理
 */
export const capturePayment = webMethod(
    Permissions.Anyone,
    async (paymentIntentId) => {
        console.info("start payment Capture:", paymentIntentId);
        try {
            const apiKey = await wixSecretsBackend.getSecret('STRIPE_SECRET_KEY');
            const stripe = new Stripe(apiKey);

            // 支払いをキャプチャ
            const captureResult = await stripe.paymentIntents.capture(paymentIntentId);
            console.log("captureResult:", captureResult);
            return captureResult;
        } catch (error) {
            console.error("Error capturing payment:", error);
            throw error;
        }
    }
);
```

上記は非同期で実行されてフルフィルメントの実行にはイベントの受信に応じる必要があります。
post_stripeWebhook関数でpayment_intent.amount_capturable_updatedで処理を実行する必要があります。
```javascript
export async function post_stripeWebhook(request) {
    console.info("stripeWebhook Start: ", request);
    let response = {
        "headers": {
            "Content-Type": "application/json"
        }
    };

    const apiKey = await wixSecretsBackend.getSecret('STRIPE_SECRET_KEY');
    const endpointSecret = await wixSecretsBackend.getSecret('STRIPE_ENDPOINT_SECRET');
    const stripe = new Stripe(apiKey);

    // Stripeからの署名を検証
    const sig = request.headers['stripe-signature'];

    let event;
    try {
        const body = await request.body.text();
        event = stripe.webhooks.constructEvent(body, sig, endpointSecret);
    } catch (err) {
        // 署名の検証に失敗した場合
        console.error(`Stripe signatureの検証に失敗: ${err.message}`);
        response.body = {
            "error": err.message,
        }
        return badRequest(response);
    }

    switch (event.type) {
        // オーソリイベント
    case 'payment_intent.amount_capturable_updated': {
        try {
            await handleAuthorizationSuccess(event.data.object);
        } catch (err) {
            console.error("handleAuthorizationSuccess Error: ", err);
            response.body = {
                "error": err,
            }
            return ok(response);
        }
        break;
    }
    default:
        console.error(`Unhandled event type ${event.type}`);
    }

    return ok(response);
}
```
## キャンセル
キャンセルもpaymentIntentIdが可能です。
```javascript
/**
 * 未キャプチャの支払いをキャンセルする
 */
export const cancelPayment = webMethod(
    Permissions.Anyone,
    async (paymentIntentId) => {
        console.info("start payment Cancel:", paymentIntentId);
        try {
            const apiKey = await wixSecretsBackend.getSecret('STRIPE_SECRET_KEY');
            const stripe = new Stripe(apiKey);

            // 支払いをキャンセル
            const cancelResult = await stripe.paymentIntents.cancel(paymentIntentId);
            console.log("cancelResult:", cancelResult);
            return cancelResult;
        } catch (error) {
            console.error("Error cancel payment:", error);
            throw error;
        }
    }
);
```

# まとめ
今回はWixでStripeを使う場合について記載しました。オーソリとキャプチャーの分離などはWix以外でも参考になるかと思います。
どなたかの参考になれば幸いです。