0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Stripe Connect プラットフォームにおいて、部分返金(partial refunds)や比例配分された送金取消(proportional transfer reversals)が発生した際

0
Posted at

Stripe Connect プラットフォームにおいて、部分返金(partial refunds)や比例配分された送金取消(proportional transfer reversals)が発生した際、アプリケーション側で手数料の逆転算や調整漏れが起きるメカニズムと、その技術的対策について解説します。

問題の背景

Stripe Connectを用いたマルチテナント型のプラットフォーム(SaaSなど)では、エンドユーザーからの支払いに対してプラットフォーム手数料(Application Fee)を徴収し、残額を接続アカウント(Connected Account)へ送金(Transfer)する構成が一般的です。

しかし、エンドユーザーに対して部分返金を行った際、またはプラットフォーム側が手動・自動で送金の一部を取り消した際に、以下の状態の不整合が発生しやすくなります。

  • 返金金額に比例した手数料の返還(fee refund)が、接続アカウント側の残高や台帳に正しく反映されていない。
  • transfer_reversal オブジェクトの作成時において、プラットフォームが保持すべき手数料の割合と、接続アカウントへ負担させるべき金額の計算がズレる。

この結果、プラットフォームの手数料バランスにサイレントな誤差(fee leakage)が蓄積されます。

メカニズム

部分返金と送金取消のライフサイクルにおけるデータの動きを確認します。

  1. 初期チャージと手数料徴収
    payment_intent または charge が作成され、application_fee_amount が指定された状態で決済が完了します。
  2. 部分返金の実行
    refunds.create API を用いて、元の金額の一部を返金します。
    {
      "charge": "ch_12345",
      "amount": 2000
    }
    
    この時、Stripe側では自動的に、返金された金額の割合に応じてアプリケーション手数料の一部を返還(refund_application_fee)するオプションを指定できますが、デフォルトの挙動や実装方法によってはこれが正しくハンドリングされません。
  3. 送金取消(Transfer Reversal)
    接続アカウントへ送金済みの資金から、部分返金に対応する分を引き戻すために transfers.create_reversal を呼び出します。
    {
      "amount": 1500
    }
    
    このリクエストにおいて、返金された金額と送金取消の金額、そしてプラットフォームが回収すべき手数料の計算式が一致していないと、プラットフォームの未収金や過払いが発生します。

実装例(Node.js / Stripe SDK)

部分返金と送金取消を行う際に、手数料の調整ロジックを担保するためのコード例です。

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

async function handlePartialRefundAndReversal(chargeId, transferId, refundAmount, feeRefundAmount) {
  try {
    // 1. 部分返金の実行と手数料の一部返還
    const refund = await stripe.refunds.create({
      charge: chargeId,
      amount: refundAmount,
      // 返金に比例した手数料の返還を指定
      refund_application_fee: true,
    });

    console.log('Refund created:', refund.id);

    // 2. 対応する送金取消の実行
    // 接続アカウントから資金を回収する計算ロジック
    const reversal = await stripe.transfers.createReversal(
      transferId,
      {
        amount: refundAmount, // または算出した比例配分額
        description: `Reversal for partial refund of charge ${chargeId}`
      }
    );

    console.log('Transfer reversal created:', reversal.id);
    return { refund, reversal };
  } catch (error) {
    console.error('Failed to process refund and reversal:', error.message);
    throw error;
  }
}

注意点と検証方法

  • APIの非同期性: Webhook(charge.refunded, transfer.reversed)のイベントを受信して内部台帳を更新する際、イベントの順序が前後することがあります。冪等性(idempotency)を考慮したイベントハンドラーの実装が不可欠です。
  • 端数処理: 割合を計算する際の小数点以下の丸め方(四捨五入、切り捨てなど)の差異により、長期間運用すると無視できない誤差が蓄積されます。正確な金額追跡には、過去のトランザクションログを定期的に突合する仕組みが必要です。

Run a historical scan to check past unreversed transfers.

We built a free check for exactly this: https://feeguard.dev/audit?utm_source=qiita&utm_campaign=socialhat&utm_medium=social&vsr=4f635fbc7e98c87dd44e

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?