【Rewald】llms scenario B 商品購入+ポイント(PSP: PayPal JS SDK)

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

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

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

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

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

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

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


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

誤: PayPal 失敗 → point.close() を呼んで完了する。 正: PayPal 失敗 → point.cancel() を呼んでロールバックする。close() は成功時のみ。

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

誤: ページロード時に PayPal Buttons を呼び出す。 正: point_trans_close コールバック内、pointInstance が利用可能になった後に PayPal Buttons を初期化する。

誤: total_price <= 0(全額ポイント払い)を無視する。 正: total_price <= 0 の場合、PayPal ボタンの代わりに「ポイントで支払う」ボタンを表示する。

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

誤: document.addEventListener("DOMContentLoaded", function () { ... }) 正: インライン <script> で DOMContentLoaded ラッパーなし。


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

ファイル 1: functions.php

貼り付け先

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

{{PLACEHOLDER}} の置き換え

  • {{PAYPAL_CLIENT_ID}}: PayPal ダッシュボードのクライアント ID
  • {{PAYPAL_SECRET}}: PayPal ダッシュボードのシークレット
<?php
// FINGERPRINT: B1-REG-001
// 1. 管理パネルの環境セットアップコードをここに貼り付け
//    (PointSystem と PointSystemPSP クラスを同期的に読み込みます)

// 2. PayPal 検証 + nonce コールバック — ここに直接配置。add_action で囲まないこと
PointSystemPSP::register([
    'verify' => function ($pointsystem_request) {
        return PointSystemPSP::verifyPaypalOrder(
            $pointsystem_request['order_id'],
            $pointsystem_request['amount'],
            '{{PAYPAL_CLIENT_ID}}',
            '{{PAYPAL_SECRET}}',
            false  // true: サンドボックス, false: 本番
        );
    },
]);

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


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

ファイル 2: 商品ページ(WordPress 固定ページ — すべてのショートコードを同一ページに配置)

貼り付け先

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

前提条件

  • PayPal JS SDK のスクリプトタグがページ内で読み込まれていること(例: <script src="https://www.paypal.com/sdk/js?client-id={{PAYPAL_CLIENT_ID}}&currency=JPY"></script>

{{PLACEHOLDER}} の置き換え

  • {{CONV_RATE}}: ポイント換算レート(例: 100 → 100円で1ポイント付与)
  • {{USE_RATE}}: ポイント利用レート(例: 1 → 1ポイント=1円として利用)
  • {{PRODUCT_PRICE}}: 商品価格(例: 1000)
<!-- FINGERPRINT: B2-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="amount_display">{{PRODUCT_PRICE}}</span></p>

<!-- FINGERPRINT: B2-OPEN-001 — 価格/ポイント入力を監視し、変更時に nonce を取得 -->
[point_trans_open var_price_id="input_price_id" var_use_point_id="input_use_point_id" callback_name="onTransOpen"]

<button id="hidden_button" style="display:none;"></button>
<!-- FINGERPRINT: B2-CLOSE-001 — Point インスタンスを作成、コールバックは (result, point) を受け取る -->
[point_trans_close callback_id="hidden_button" callback_name="onTransReady"]

<div id="paypal-button-container"></div>
<div id="result-status" style="display:none; padding:10px; margin:10px 0; border-radius:5px;"></div>

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

// FINGERPRINT: B2-OPEN-001 — 価格/ポイント変更のたびに表示を更新
var currentAmount = '{{PRODUCT_PRICE}}';
function onTransOpen(response) {
    if (!response || !response.result) return;
    var r = response.result;
    currentAmount = String(r.total_price);
    document.getElementById('amount_display').textContent = r.total_price;
    renderButtons();
}

// FINGERPRINT: B2-CLOSE-001 — pointInstance をモジュールレベルで保持
var pointInstance = null;
var closeTriggered = false;

function onTransReady(result, point) {
    if (point) pointInstance = point;
}

// FINGERPRINT: B2-GUARD-001 — 二重 close 防止
async function triggerClose() {
    if (closeTriggered || !pointInstance) return;
    closeTriggered = true;
    var result = await pointInstance.close();
    showResult(result);
}

// FINGERPRINT: B2-ZERO-001 — 金額ゼロの場合:「ポイントで支払う」ボタンを表示
function renderButtons() {
    var container = document.getElementById('paypal-button-container');
    container.innerHTML = '';
    closeTriggered = false;

    var amount = parseFloat(currentAmount);
    if (amount <= 0) {
        var btn = document.createElement('button');
        btn.textContent = 'ポイントで支払う';
        btn.style.cssText = 'padding:10px 20px; font-size:16px; background:#28a745; color:#fff; border:none; border-radius:5px; cursor:pointer;';
        btn.addEventListener('click', function() { triggerClose(); });
        container.appendChild(btn);
    } else {
        paypal.Buttons({
            createOrder: function(data, actions) {
                return actions.order.create({
                    purchase_units: [{
                        amount: { value: currentAmount },
                        custom_id: pointInstance ? pointInstance.pointsystem_nonce : ''
                    }]
                });
            },
            onApprove: function(data, actions) {
                return actions.order.capture().then(function() {
                    triggerClose();
                });
            },
            onCancel: function() {
                showResult({ error: '支払いがキャンセルされました。' });
            },
            onError: function(err) {
                console.error('PayPal エラー:', err);
                showResult({ error: '決済エラーが発生しました。' });
            }
        }).render('#paypal-button-container');
    }
}

function showResult(result) {
    var el = document.getElementById('result-status');
    el.style.display = 'block';
    if (result && result.result) {
        el.style.background = '#d4edda';
        el.style.border = '1px solid #c3e6cb';
        var r = result.result;
        var msg = '成功!';
        if (parseInt(r.use_point || 0) > 0) msg += ' ' + r.use_point + ' ポイント利用。';
        if (parseInt(r.new_point || 0) > 0) msg += ' ' + r.new_point + ' ポイント獲得。';
        msg += ' (残高: ' + (r.total_point || 0) + ')';
        el.innerHTML = msg;
    } else {
        el.style.background = '#f8d7da';
        el.style.border = '1px solid #f5c6cb';
        el.innerHTML = 'エラー: ' + (result.error || JSON.stringify(result));
    }
}
</script>

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


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

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

貼り付け先

WordPress 管理画面 → 固定ページ → 新規追加 → タイトル:「決済完了」(任意)→ エディタ右上の「テキスト」タブに切り替え → 以下のコードを貼り付け → 「公開」をクリック

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

<script>
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 管理画面 → 固定ページ → 新規追加 → タイトル:「決済キャンセル」(任意)→ エディタ右上の「テキスト」タブに切り替え → 以下のコードを貼り付け → 「公開」をクリック

<!-- FINGERPRINT: B4-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>

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


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

  1. PointSystemPSP::register() は add_action で囲まない — FINGERPRINT B1-REG-001 参照
  2. register() のキーは 'verify'(ネストされた配列ではない)— FINGERPRINT B1-REG-001 参照
  3. [point_setting] に currency_type, conv_rate, use_rate が含まれている — FINGERPRINT B2-SETTING-001 参照
  4. point_trans_open に var_price_id と var_use_point_id がある — FINGERPRINT B2-OPEN-001 参照
  5. pointInstance は onTransReady(result, point) で保持される — FINGERPRINT B2-CLOSE-001 参照
  6. 二重 close 防止ガード(closeTriggered フラグ)が存在する — FINGERPRINT B2-GUARD-001 参照
  7. 金額ゼロの場合「ポイントで支払う」ボタンを表示(amount <= 0)— FINGERPRINT B2-ZERO-001 参照
  8. PayPal Buttons は createOrder, onApprove, onCancel, onError で初期化 — B2-ZERO-001 参照
  9. point_trans_detail はコールバック付きの別ページ — FINGERPRINT B3-DETAIL-001 参照
  10. point_trans_cancel はコールバック付きの別ページ — FINGERPRINT B4-CANCEL-001 参照