【Rewald】llms scenario C 商品購入+ポイント(PSP: Stripe Checkout)

牧さん制作監修 AIアウトプット商品購入+ポイント(PSP: Stripe Checkout)シナリオ

役割: あなたはコード転写者です。コード生成者ではありません。

タスク: ===== ここからコピー ===== と ===== ここまでコピー ===== の間の内容をそのまま正確にコピーしてください。

ルール: {{PLACEHOLDER}} の値のみ置き換えてください。構造、キー名、関数シグネチャは変更しないでください。

ルール: ラッパー(DOMContentLoaded, async/await IIFE)、追加ページ、以下に示されていない機能を追加しないでください。

ルール: 各コードブロックには FINGERPRINT コメントがあります。出力にそのまま含めてください。

ルール: 回答は必ず日本語で行ってください。


よくある間違い — コードをコピーする前に必ず読んでください。

誤: point.close() の前に Stripe にリダイレクトする。 正: まず await point.close() を実行し、その後 Stripe セッションを作成する。ポイント控除は PSP の前に行う。

誤: Stripe 失敗 → ポイント控除をそのまま放置する。 正: Stripe 失敗 → point_trans_cancel REST API を呼んでポイント控除をロールバックする。

誤: JPY の金額を 100 倍する(unit_amount: amount * 100)。 正: JPY はゼロ小数通貨 — unit_amount: intval(amount)。100 倍しないこと。

誤: response.point または response.total_point 正: response.result.point, response.result.total_price — すべて response.result の下にある。

誤: add_action('init', function() { PointSystemPSP::register([...]); }); 正: PointSystemPSP::register([...]); — トップレベルで直接呼び出す。


===== ここからコピー =====

ファイル 1: functions.php

貼り付け先

WordPress 管理画面 → 外観 → テーマファイルエディター → 右側のファイル一覧から「テーマのための関数 (functions.php)」を選択 → ファイル末尾に以下のコードを追加 → 「ファイルを更新」をクリック

{{PLACEHOLDER}} の置き換え

  • {{STRIPE_SECRET_KEY}}: Stripe ダッシュボードのシークレットキー(sk_test_... または sk_live_...
  • {{PRODUCT_NAME}}: 商品名(例: 「プレミアムプラン」)
<?php
// FINGERPRINT: C1-REG-001
// 1. 管理パネルの環境セットアップコードをここに貼り付け
//    (PointSystem と PointSystemPSP クラスを同期的に読み込みます)

// 2. Stripe 検証コールバック — ここに直接配置。add_action('init', ...) で囲まないこと
PointSystemPSP::register([
    'verify' => function ($pointsystem_request) {
        return PointSystemPSP::verifyStripeSession(
            $pointsystem_request['order_id'],
            $pointsystem_request['amount'],
            '{{STRIPE_SECRET_KEY}}'
        );
    },
]);

// 3. REST API: Stripe Checkout セッション作成
add_action('rest_api_init', function () {
    register_rest_route('my-shop/v1', '/create-checkout-session', [
        'methods' => 'POST',
        'callback' => function ($request) {
            $params = $request->get_json_params();
            \Stripe\Stripe::setApiKey('{{STRIPE_SECRET_KEY}}');
            $session = \Stripe\Checkout\Session::create([
                'payment_method_types' => ['card'],
                'line_items' => [[
                    'price_data' => [
                        'currency' => 'jpy',
                        'product_data' => ['name' => '{{PRODUCT_NAME}}'],
                        // FINGERPRINT: C1-JPY-001 — JPY はゼロ小数通貨: 1000 は ¥1,000。100倍しないこと。
                        'unit_amount' => intval($params['amount']),
                    ],
                    'quantity' => 1,
                ]],
                'mode' => 'payment',
                'success_url' => $params['success_url'],
                'cancel_url' => $params['cancel_url'],
            ]);
            return ['session_id' => $session->id];
        },
        'permission_callback' => '__return_true',
    ]);
});

===== ここまでコピー =====


===== ここからコピー =====

ファイル 2: カートページ(WordPress 固定ページ)

貼り付け先

WordPress 管理画面 → 固定ページ → 新規追加(または既存のカートページを編集)→ エディタ右上の「テキスト」タブに切り替え → 以下のコードを貼り付け → 「公開」(または「更新」)をクリック

前提条件

  • Stripe の JS ライブラリがページ内で読み込まれていること(例: <script src="https://js.stripe.com/v3/"></script>

{{PLACEHOLDER}} の置き換え

  • {{CONV_RATE}}: ポイント換算レート(例: 100 → 100円で1ポイント付与)
  • {{USE_RATE}}: ポイント利用レート(例: 1 → 1ポイント=1円として利用)
  • {{PRODUCT_PRICE}}: 商品価格(例: 1000)
  • {{STRIPE_PUBLIC_KEY}}: Stripe ダッシュボードの公開キー(pk_test_... または pk_live_...
  • {{SUCCESS_PAGE_URL}}: 決済完了ページの URL(例: https://example.com/payment-success/
  • {{SUCCESS_PAGE_PATH}}: 決済完了ページのパス(例: /payment-success/
  • {{CANCEL_PAGE_PATH}}: キャンセルページのパス(例: /payment-cancel/
<!-- FINGERPRINT: C2-SETTING-001 — セッション初期化 -->
[point_setting currency_type="JPY" conv_rate="{{CONV_RATE}}" use_rate="{{USE_RATE}}"]

[point_own callback_name="displayBalance"]
<p>保有ポイント: <span id="point_id">読み込み中...</span></p>

<p>価格: ¥<span id="price_display">{{PRODUCT_PRICE}}</span></p>
<input type="hidden" id="input_price_id" value="{{PRODUCT_PRICE}}" />

<p>ポイント利用: <input type="number" id="input_use_point_id" value="0" min="0" /></p>
<p>お支払い金額: ¥<span id="total_price_id">{{PRODUCT_PRICE}}</span></p>

[point_trans_open var_price_id="input_price_id" var_use_point_id="input_use_point_id" callback_name="onTransOpen"]
[point_trans_close callback_name="onTransReady"]

<button id="checkout-button" disabled>お支払いへ進む</button>
<div id="error-message" style="color:red;"></div>

<script>
function displayBalance(response) {
    if (response.error) { document.getElementById('point_id').textContent = 'エラー'; return; }
    document.getElementById('point_id').textContent = response.result.point;
}

function onTransOpen(response) {
    if (!response || !response.result) return;
    document.getElementById('total_price_id').textContent = response.result.total_price;
    document.getElementById('checkout-button').disabled = false;
}

// FINGERPRINT: C2-FLOW-001 — pointInstance を保持し、close → Stripe の順序で実行
var stripe = Stripe('{{STRIPE_PUBLIC_KEY}}');
var pointInstance = null;

function onTransReady(data, point) {
    pointInstance = point;
}

document.getElementById('checkout-button').addEventListener('click', async function(e) {
    e.preventDefault();
    var errorEl = document.getElementById('error-message');
    errorEl.textContent = '';

    if (!pointInstance) {
        errorEl.textContent = 'ポイントシステムが初期化されていません。ページを再読み込みしてください。';
        return;
    }

    var amount = parseInt(document.getElementById('total_price_id').textContent, 10) || 0;

    // ステップ 1: まずポイントを控除
    var closeResult = await pointInstance.close();
    if (!closeResult || closeResult.error || closeResult.code) {
        errorEl.textContent = 'ポイント処理に失敗しました。';
        return;
    }

    // FINGERPRINT: C2-ZERO-001 — 金額ゼロ: Stripe をスキップして直接リダイレクト
    if (amount <= 0) {
        window.location.href = '{{SUCCESS_PAGE_URL}}?t=point_only';
        return;
    }

    // ステップ 2: Stripe セッションを作成
    var nonce = sessionStorage.getItem('pointsystem_nonce');
    try {
        var res = await fetch('/wp-json/my-shop/v1/create-checkout-session', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                amount: amount,
                success_url: location.origin + '{{SUCCESS_PAGE_PATH}}'
                    + '?session_id={CHECKOUT_SESSION_ID}',
                cancel_url: location.origin + '{{CANCEL_PAGE_PATH}}'
            })
        });
        var sessionData = await res.json();

        if (sessionData.session_id) {
            stripe.redirectToCheckout({ sessionId: sessionData.session_id });
        } else {
            // FINGERPRINT: C2-ROLLBACK-001 — Stripe 失敗 → ポイントをキャンセル
            await rollbackPoints();
            errorEl.textContent = sessionData.error || 'Stripe セッションの作成に失敗しました。';
        }
    } catch (err) {
        // FINGERPRINT: C2-ROLLBACK-001 — ネットワークエラー → ポイントをキャンセル
        await rollbackPoints();
        errorEl.textContent = err.message;
    }
});

async function rollbackPoints() {
    if (!pointInstance) return;
    try {
        await fetch(pointInstance.url + '/point_trans_cancel/', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(pointInstance.data)
        });
    } catch(e) { console.error('キャンセルエラー:', e); }
}
</script>

===== ここまでコピー =====


===== ここからコピー =====

ファイル 3: 決済完了ページ(WordPress 固定ページ)

貼り付け先

WordPress 管理画面 → 固定ページ → 新規追加 → タイトル:「決済完了」(任意)→ パーマリンクのスラッグをファイル 2 の {{SUCCESS_PAGE_PATH}} と一致させる → エディタ右上の「テキスト」タブに切り替え → 以下のコードを貼り付け → 「公開」をクリック

<!-- FINGERPRINT: C3-DETAIL-001 — 取引結果を表示 -->
[point_trans_detail callback_name="showDetail"]
<div id="detail-result">取引結果を読み込み中...</div>

<script>
var urlParams = new URLSearchParams(window.location.search);

// 全額ポイント払いの場合
if (urlParams.get('t') === 'point_only') {
    document.getElementById('detail-result').style.background = '#d4edda';
    document.getElementById('detail-result').style.padding = '10px';
    document.getElementById('detail-result').textContent = '決済完了(全額ポイントでお支払い済み)。';
}

function showDetail(response) {
    var el = document.getElementById('detail-result');
    if (response.error) {
        el.style.background = '#f8d7da';
        el.style.padding = '10px';
        el.textContent = 'エラー: ' + response.error;
        return;
    }
    var r = response.result;
    el.style.background = '#d4edda';
    el.style.padding = '10px';
    el.innerHTML = '決済完了! 利用: ' + (r.use_point || 0)
        + ' ポイント、獲得: ' + (r.new_point || 0)
        + ' ポイント、残高: ' + (r.total_point || 0);
}
</script>

===== ここまでコピー =====


===== ここからコピー =====

ファイル 4: キャンセルページ(WordPress 固定ページ)

貼り付け先

WordPress 管理画面 → 固定ページ → 新規追加 → タイトル:「決済キャンセル」(任意)→ パーマリンクのスラッグをファイル 2 の {{CANCEL_PAGE_PATH}} と一致させる → エディタ右上の「テキスト」タブに切り替え → 以下のコードを貼り付け → 「公開」をクリック

<!-- FINGERPRINT: C4-CANCEL-001 — トランザクションをキャンセルしてポイントを復元 -->
[point_trans_cancel callback_name="showCancel"]
<div id="cancel-result">トランザクションをキャンセル中...</div>

<script>
function showCancel(response) {
    var el = document.getElementById('cancel-result');
    if (response.error) {
        el.style.background = '#f8d7da';
        el.style.padding = '10px';
        el.textContent = 'キャンセルエラー: ' + response.error;
        return;
    }
    el.style.background = '#fff3cd';
    el.style.padding = '10px';
    el.textContent = 'トランザクションがキャンセルされました。ポイントは復元されました。';
}
</script>

===== ここまでコピー =====


確認チェックリスト — 回答する前に、出力内容が以下の 9 項目をすべて満たしていることを確認してください。

  1. PointSystemPSP::register() は add_action で囲まない — FINGERPRINT C1-REG-001 参照
  2. register() のキーは 'verify'(ネストされた配列ではない)— FINGERPRINT C1-REG-001 参照
  3. JPY の金額は 100 倍しない — FINGERPRINT C1-JPY-001 参照
  4. 順序フロー: Stripe セッション作成の前に await point.close() — FINGERPRINT C2-FLOW-001 参照
  5. 金額ゼロの場合 Stripe をスキップして直接リダイレクト — FINGERPRINT C2-ZERO-001 参照
  6. Stripe 失敗時に point_trans_cancel でロールバック — FINGERPRINT C2-ROLLBACK-001 参照
  7. pointInstance は onTransReady(data, point) で保持される — FINGERPRINT C2-FLOW-001 参照
  8. point_trans_detail は別の決済完了ページに配置 — FINGERPRINT C3-DETAIL-001 参照
  9. point_trans_cancel は別のキャンセルページに配置 — FINGERPRINT C4-CANCEL-001 参照