🛑 返金とクレーム対応 (Refunds & Disputes) 実践ガイド

製品が売れれば売れるほど、「返金」が必要になる状況に遭遇するものです。 顧客が間違えて購入した、商品に満足できなかった、あるいはもっと面倒なケース:顧客が購入を忘れて直接カード発行銀行に「これは不正利用だ」と申し立てる場合などです。

ECやSaaSの世界では、返金とディスプート (Disputes / Chargebacks) を自動処理する仕組みがなければ、大量のカスタマーサポートメールと銀行からの罰金に埋もれてしまうでしょう。本章ではこの2大危機管理システムを実践的に解説します!


1. なぜ管理画面で手動返金するだけではダメなのか?

「顧客が返金を求めてきたら、Stripeの管理画面にログインしてボタンを押せばいいんじゃないの?」と思うかもしれません。

確かに可能です。しかし、あなたの製品が「自動配信システム」(例:購入後すぐにソフトウェアのシリアル番号を発行、コースアクセス権を即時付与など)の場合、Stripeで手動返金しても、あなたのデータベースはこのことを知りません! これにより、顧客に返金した後も、彼らはコースの視聴権限やソフトウェアの使用権を保持し続けることになります。

だからこそ、以下の方法を学ぶ必要があります:

  1. 能動的な返金APIの発行:管理システムからワンクリックで返金し、同時に権限を剥奪する
  2. 受動的な返金Webhookの監視:経営者がStripe管理画面で直接返金した場合でも、システムが通知をキャッチして同期処理できる

2. 能動的な返金処理 (Refund API)

管理ダッシュボード (Admin Dashboard) に「ワンクリック返金」ボタンを作成し、これを押した時に次のAPIを呼び出すと想定します。

Stripeは非常にシンプルな stripe.refunds.create メソッドを提供しています。

返金APIの作成

ファイル作成:src/app/api/stripe/refund/route.ts

import { NextResponse } from 'next/server';
import Stripe from 'stripe';
import { revokeUserAccess } from '@/lib/db'; // あなたのデータベースロジック

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
});

export async function POST(request: Request) {
  try {
    const { paymentIntentId, reason, amount } = await request.json();

    if (!paymentIntentId) {
      return NextResponse.json({ error: 'Missing paymentIntentId' }, { status: 400 });
    }

    // 1. 返金リクエストの発行
    const refundConfig: Stripe.RefundCreateParams = {
      payment_intent: paymentIntentId,
      // オプション:返金理由 (duplicate, fraudulent, requested_by_customer)
      reason: reason || 'requested_by_customer', 
    };

    // amountが指定されている場合、「部分返金」(Partial Refund) を意味する
    // 注意:Stripeの金額単位は最小通貨単位 (例:100は1ドルまたは100円)
    if (amount) {
      refundConfig.amount = amount;
    }

    const refund = await stripe.refunds.create(refundConfig);

    // 2. 返金成功!データベースを更新してユーザー権限を剥奪
    // (部分返金の場合は権限を剥奪しないなど、ビジネスロジックに応じて判断)
    if (!amount) {
      // 全額返金と仮定
      await revokeUserAccess(paymentIntentId);
      console.log(`Access revoked for payment: ${paymentIntentId}`);
    }

    return NextResponse.json({ 
      success: true, 
      refundId: refund.id,
      status: refund.status 
    });

  } catch (error: any) {
    console.error('Refund Error:', error);
    // エラー処理:残高不足、既に返金済みなど
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

💡 実践の落とし穴:返金には「口座残高」が必要

Stripeでは、返金は「Stripe口座残高」から差し引かれます。残高をすべて銀行口座に引き出してしまっている場合、返金APIはエラー (Insufficient Funds) を返します。この場合、銀行口座からStripeに資金を戻すか、次の注文が入るのを待つ必要があります。


3. 受動的な返金とディスプートの監視 (Webhooks)

「能動的な返金」APIを書きたくない場合、またはStripeアプリやウェブ管理画面で手動処理する場合、「必ず」Webhookを書いてデータベース状態を同期する必要があります。

さらに重要なのは、クレジットカードディスプート (Disputes / Chargebacks) はWebhookでしか捕捉できない点です。

クレジットカードディスプート (Chargebacks) とは?

顧客が銀行に「この取引は自分が行ったものではない」または「商品が届かなかった」と電話すると、銀行は直接あなたのStripe口座から強制的に資金を引き揚げます(通常は$15ドルの罰金付き!)。業界ではこれをチャージバックと呼びます。 この時、商品サービスを即座に停止しなければ、損失が拡大します。

高度なWebhook監視の実装

既存の src/app/api/stripe/webhook/route.ts に、以下の2つのイベント監視を追加:

// ... 前略(Webhook検証コード)...

switch (event.type) {
  
  // 🟢 監視:返金成功
  case 'charge.refunded': {
    const charge = event.data.object as Stripe.Charge;
    const paymentIntentId = charge.payment_intent as string;
    
    console.log(`Charge refunded for PaymentIntent: ${paymentIntentId}`);
    
    // 全額返金か部分返金かを判断
    if (charge.amount_refunded === charge.amount) {
      console.log('これは全額返金、直ちに権限を剥奪!');
      // TODO: データベースでこの注文状態を'refunded'に更新し、顧客権限を剥奪
    } else {
      console.log(`これは部分返金、返金額 ${charge.amount_refunded}`);
    }
    break;
  }

  // 🔴 監視:恐ろしいチャージバック (Chargeback) 発生
  case 'charge.dispute.created': {
    const dispute = event.data.object as Stripe.Dispute;
    const paymentIntentId = dispute.payment_intent as string;
    
    console.log(`⚠️ 警告!チャージバック発生!PaymentIntent: ${paymentIntentId}`);
    console.log(`ディスプート理由: ${dispute.reason}`); 
    // reason は fraudulent(不正利用)、product_not_received など
    
    // 💣 ベストプラクティス:ディスプート発生時、即座にアカウントをロックしサービス停止!
    // TODO: データベースで注文状態を'disputed'に更新し、顧客アカウントを凍結
    
    // Line Notifyやメールで経営者に即時対応を通知することも可能!
    break;
  }
    
  // 🔵 監視:ディスプート処理結果(勝訴または敗訴)
  case 'charge.dispute.closed': {
    const dispute = event.data.object as Stripe.Dispute;
    if (dispute.status === 'won') {
      console.log('🎉 勝利!銀行の判決は我々に有利、資金が戻りました!');
      // TODO: 顧客アカウントと権限を復旧
    } else if (dispute.status === 'lost') {
      console.log('😭 残念、銀行の判決は我々に不利、資金が差し引かれました。');
      // 諦めて損失を認め、アカウントは凍結したまま
    }
    break;
  }

  default:
    console.log(`Unhandled event type ${event.type}`);
}

4. ディスプートを防ぐ方法は?(ビジネス知恵)

エンジニアとして、コードでディスプートを処理するだけでなく、「予防」方法も知るべきです:

  1. 明瞭な請求書記載 (Statement Descriptor): Stripe設定で、請求書記載をブランド名(例 VIBETUTOR*COURSE)に設定。多くのディスプートは、顧客が請求書に記載された不明な英字を見て、購入を忘れて不正利用と誤認することで発生します。
  2. 適切なカスタマーサポート窓口: ウェブサイトの目立つ場所に返金ポリシーと連絡先を記載。顧客が簡単に連絡を取り返金を得られる場合、銀行のディスプート手続きを取ることはありません(銀行手続きでは$15ドルの処理料が追加で請求されます)。
  3. Stripe Radarの有効化: Stripe組み込みのAI不正防止システムで、高リスクの不正カードを自動ブロック。各取引にわずかな手数料がかかりますが、ディスプートによる多額の罰金を防げます。

返金とディスプートのプログラムによる処理をマスターすれば、あなたのプラットフォームは真の「リスク耐性」を持つビジネスレベルの製品と言えるでしょう!盾を構えたら、安心して大きな資金流れを迎え入れられます!

完全なチュートリアルをロック解除

このチャプターは有料コンテンツです。プロジェクトに参加して、10以上の神レベルのPromptや実際のソースコード例を含む、5000字以上の深い分析をロック解除してください!