決済(Stripe)との連携

この章では、AppCotton での課金フロー(ワンタイム/サブスクリプション)を Stripe と接続するための設計と実装ポイントをまとめます。
実運用に必要な 前提設定 → チェックアウト生成 → Webhook 処理 → ライセンス発行/反映 → アップグレード(差額課金) の順で整理します。サンプルは プラグイン内クラス(Stripe Manager)+ REST エンドポイント+ Webhook を想定しています。


1. 前提:Stripe 側の準備

  • アカウント作成:本番用/テスト用の双方を用意(テストで十分に検証してから本番切替)。
  • API キー
    • 公開可能キー(pk_live_... / pk_test_...
    • シークレットキー(sk_live_... / sk_test_...
  • プロダクト&価格(Price)
    • ワンタイム販売:Pricetype=one_time)を作成し、プランごとに 1 つ対応させる。
    • サブスクリプション:Pricetype=recurringinterval=month/year等)。
    • AppCotton の プラン(plan_id) ⇔ Stripe price_id1対1でマッピング。
  • Webhook エンドポイント(後述):
    • テスト/本番で 別 URL を登録。
    • 受け取るイベント:最低限 checkout.session.completed、(サブスクなら)invoice.payment_succeeded / customer.subscription.updated など。

2. 価格モデル(AppCotton × Stripe の対応)

AppCottonのプラン属性Stripeの対応例
billing_type = one_timeCheckout Session(mode=payment、line_items=[{price: <price_id>, quantity:1}])
billing_type = subscriptionCheckout Session(mode=subscription、line_items=[{price: <price_id>, quantity:1}])
price(最小通貨単位)StripeのPriceに合わせる(AppCotton側は参照用/差額計算用)
activation_limit(1/3/5/…/無制限)価格自体と紐づけ。アップグレード時は target_plan により差額決済
stripe_price_id, stripe_price_id_subscriptionAppCotton のプランに保持し、Checkout 生成時に使用

推奨:表示価格や税計算は Stripe に寄せ、AppCotton は “どの Stripe Price で決済したか” を正として記録します。


3. チェックアウトの生成(フロント → REST → Stripe)

3.1 フロント(例:ショートコード出力のボタン/UI)

  • 「購入」「プラン選択」「アップグレード」などのボタンから AppCotton REST を呼び出し、Checkout Session を生成してリダイレクト。
  • 必須パラメータ:
    • product_id(slug から解決済みでも良い)
    • plan_id(複数プランがある前提)
    • success_url / cancel_url(決済後遷移先)
    • (任意)client_reference_iduser_id:license_id:plan_id などを埋める)
    • (任意)customer_email(収集/固定)
    • (任意)allow_promotion_codes, automatic_tax, billing_address_collection など

3.2 REST(サーバ) → Stripe Checkout

モード別の生成例(擬似PHP)

// mode=payment(ワンタイム)
$params = [
  'mode' => 'payment',
  'line_items' => [[ 'price' => $price_id, 'quantity' => 1 ]],
  'success_url' => $success_url . '?session_id={CHECKOUT_SESSION_ID}',
  'cancel_url'  => $cancel_url,
  'client_reference_id' => $client_ref, // 重要:後続紐付けに使う
  'metadata' => [
    'appcotton_product_id' => $product_id,
    'appcotton_plan_id'    => $plan_id,
    'appcotton_user_id'    => $user_id,
    'action'               => 'purchase', // or 'upgrade'
  ],
  'customer_email' => $email, // 収集方針に応じて
  'allow_promotion_codes' => true,
  'locale' => 'auto',
];
// mode=subscription(サブスク)
$params = [
  'mode' => 'subscription',
  'line_items' => [[ 'price' => $recurring_price_id, 'quantity' => 1 ]],
  'success_url' => $success_url . '?session_id={CHECKOUT_SESSION_ID}',
  'cancel_url'  => $cancel_url,
  'client_reference_id' => $client_ref,
  'metadata' => [
    'appcotton_product_id' => $product_id,
    'appcotton_plan_id'    => $plan_id,
    'appcotton_user_id'    => $user_id,
    'action'               => 'purchase',
  ],
  'subscription_data' => [
    'metadata' => [
      'appcotton_plan_id' => $plan_id,
    ],
    // proration_behavior はサブスク内アップグレード運用時に利用
  ],
  'automatic_tax' => ['enabled' => true],
  'billing_address_collection' => 'auto',
];
  • Idempotency-Key必ず付与(重複決済防止)。
  • client_reference_idmetadataWebhook 側で AppCotton 注文/ライセンスに正確に紐付けるカギ。

4. Webhook 設計(注文最終化・ライセンス発行)

4.1 受信イベント(最低限)

  • checkout.session.completed
    一括の“成立”トリガーsession.modeclient_reference_idmetadataamount_totalpayment_status を確認。
  • payment_intent.succeeded(mode=payment の場合の冪等性補助)
  • (サブスク導入時)invoice.payment_succeededcustomer.subscription.updatedcustomer.subscription.deleted

4.2 署名検証

  • Stripe で発行される Webhook Signing Secret を用い、Stripe-Signature ヘッダで検証。
  • 失敗時は 400 を返し、絶対に処理しない

4.3 冪等化(重複イベント対策)

  • event.id専用テーブル または wp_options(autoload off)に記録し、一度処理したら二度と処理しない

4.4 典型フロー(成功時)

  1. イベント検証 OK → checkout.session.completed をパース
  2. client_reference_id / metadata から ユーザー/プロダクト/プラン を特定
  3. AppCotton 注文レコードを作成(order_id
  4. ライセンス新規発行 or 既存にプラン反映
    • 購入:新規ライセンス生成(activation_limit をプランに合わせて設定)
    • アップグレード:差額計算済の注文として記録し、既存ライセンスの activation_limit を上書き
  5. 顧客通知(メールでライセンスキー送付/マイページへ誘導)
  6. 200 OK を返す

5. 差額課金(アップグレード)の扱い

5.1 ロジック(ワンタイム課金の場合)

  • 原則:旧プラン価格 と 新プラン価格 の差額 = 請求額。
    • diff_cents = max(new_price_cents - old_price_cents, 0)
    • 返金は行わず、上位への差分請求のみ許容。
  • REST:/checkout/upgrade/start(例)
    • license_key, target_plan_id, success_url, cancel_url
    • サーバ側で現在プランの price_cents を取得し、差額 Price を 動的計算 → Checkoutmode=paymentunit_amount = diff_centsDynamic Price を使う場合は、Payment Links でなく Checkout Session API を利用)。
  • Webhook:成功後に ライセンスの activation_limitplan_id を上書き

:Stripeに差額用の Price を都度生成する運用は非推奨。
Checkout の line_itemsprice_data(custom unit_amount) を使い、一時的な差額アイテムとして決済するのがシンプルです。

5.2 サブスクリプションの場合

  • Stripe のサブスク プラン変更(Upgrade) を利用
    • subscriptionitemsnew_price_id に差し替え
    • proration_behavior='create_prorations' を活用して 自動按分(推奨)
    • あるいは none で按分なし運用
  • 変更直後に customer.subscription.updated / invoice.payment_succeeded をフックして AppCotton ライセンス側の上限更新

6. 失敗・キャンセル時の扱い

  • checkout.session.completed が来ない限り 「未完」 として扱い、ライセンスは発行しない
  • expired / canceledクリーンアップ対象(必要であれば “未完注文” として残す)。
  • ネットワーク/Stripe エラー時は Idempotency-Key で再試行可能に。

7. 税・住所・領収書

  • 税計算:automatic_tax.enabled = true を推奨(Stripe Tax を契約している場合)。そうでない場合は Price 側の税込/税抜設計を明確に。
  • billing_address_collectionauto or required。領収書用途や国別の税制要件に応じて。
  • Receipt(顧客メール)は Stripe 側で送付。AppCotton では 注文番号やライセンス情報の通知に専念。

8. メタデータ設計(必須)

Stripe への metadata / client_reference_id最低限以下を載せます:

  • appcotton_user_id
  • appcotton_product_id
  • appcotton_plan_id
  • (アップグレード時)appcotton_license_id / action = upgrade
  • (任意)order_hint(内部整合の補助)

これにより Webhook で 外部キー無しに正確に復元できます。


9. セキュリティ

  • Webhook 署名検証必須
  • REST で Checkout 生成時は 現在ログイン中のユーザー(またはEメール) を検証し、他人の license_id を使った Upgrade を弾く
  • Idempotency-Key を毎リクエスト発行(license_id + target_plan_id + timestamp など)して重複防止。
  • 金額は サーバ側で計算(フロントの値は信用しない)。

10. 返金・キャンセルポリシー

  • 返金を行う場合は Stripe ダッシュボード/API で行い、AppCotton 注文ステータスrefunded に同期。
  • サブスクの解約は cancel_at_period_end=true を基本にし、ライセンス有効期限(expires_at) と同期。

11. 実装の目安(疑似コード)

11.1 Checkout セッション生成(REST エンドポイント)

// 1) 入力: product_id, plan_id, action(purchase|upgrade), success_url, cancel_url
// 2) プランを取得 → price_id(or recurring_price_id)を取り出し
// 3) (upgradeなら)差額centsを計算 → custom price_data で line_items 構築
// 4) CheckoutSession を作成 → URL を返す

11.2 Webhook 受信

// 1) 署名検証
// 2) idempotency チェック(event.id)
// 3) event.type に応じて分岐
// 4) checkout.session.completed → metadata / client_reference_id を解決
// 5) AppCotton 注文作成+ライセンス発行/更新
// 6) メール通知

12. 動作確認チェック(本番前)

  • テストキーで purchase(ワンタイム)完了 → Webhook 受信 → ライセンス発行 OK
  • テストキーで upgrade(差額) → 既存ライセンスの activation_limit 更新
  • サブスク(導入時)で upgrade/downgrade → 按分の反映とライセンス上限同期
  • Webhook の冪等化(再送でも重複発行しない)
  • 署名検証の失敗時は 400
  • 本番キーへ切替、成功/キャンセルURLの最終確認

13. よくある落とし穴

  • フロントから差額額面を渡してしまう(サーバで再計算せず信頼)→ 悪用される
  • client_reference_id 未設定 → Webhookで紐付け不能
  • Idempotency 未使用 → 重複課金
  • テスト用 Webhook URL のまま本番運用 → 本番イベントが届かない
  • success_url での “購入済み処理” を書いてしまう(Webhook基準にする)

まとめ

  • Price(Stripe)= プラン(AppCotton) を厳密に紐づけ、Checkout → Webhook → ライセンスという一筆書きを守る。
  • **アップグレード(差額請求)**は サーバ計算×Checkout(custom price_data で実装し、成功後に 既存ライセンスを書き換える。
  • 失敗/重複/署名不正を防ぐために Idempotency / 署名検証 / 冪等化 を必須化する。

この方針で実装しておけば、プロダクト横断で同じ決済・発行体験を提供できます。