🐍

StripeのPayment LinkでSlackのチーム情報を紐付ける実装方法

2025/01/17に公開

StripeのPayment LinkでSlackのチーム情報を紐付ける実装方法 🚀

こんにちは!この記事では、StripeのPayment Link を使って、Slackアプリ側のチーム情報をうまく紐付ける方法を、ゆるっと考える。
Stripeの決済機能をSlackアプリに組み込もうとしている方にとって、少しでも実装のヒントになれば嬉しいです😊

はじめに

課題

  • Stripeの決済機能をSlackアプリに組み込む時の悩み
    • どのチームからの決済なのか、どうやって管理・把握する?
    • 決済情報とチーム情報をどのように紐付ける?
    • 決済状態をどのように追跡する?

Payment Linkのメリット

  • 導入が簡単
  • カスタムフォームの作成が不要
  • Slackアプリからスムーズに案内できる

この記事で解決すること

  • Payment Linkを使いながら、チーム情報を安全に紐付ける方法
  • トランザクション管理の実装例
  • Slackアプリでの具体的な実装方法

ここでは、いろいろ試した結果、最終的に落ち着いた client_reference_id を使う実装方法 をご紹介します!


実装方法をあれこれ検討 🤔

1. URLパラメータによる方法(最初の試み❌)

  • 試したこと: URLにチーム情報を直接付与
stripe_url = "https://buy.stripe.com/test_xxx?team_id={team_id}&app_id={app_id}"
  • メリット: 実装が比較的シンプル
  • デメリット:
    • Webhookでメタデータとして取得できない
    • StripeのPayment LinkのURLには、決められたパラメータ(prefilled_emailclient_reference_idなど)しか追加できない
  • なぜダメだったのか:
    • URLパラメータはStripeのメタデータに自動設定されない
    • カスタムパラメータ(team_idapp_idなど)は無視される
    • データとして取り回しづらい

2. カスタムCheckoutページの作成(❌)

  • 試したこと: Stripeチェックアウトページを独自に作成
  • 結果: Payment Linkのメリット(手軽さ)が失われるため不採用
checkout_session = stripe.checkout.Session.create(
    metadata={
        "team_id": team_id,
        "app_id": app_id
    }
    # ... その他の設定
)
  • メリット: 完全にカスタマイズした画面が用意できる
  • デメリット: Payment Linkの気軽さが失われる
  • Slackアプリ内での使用を考慮: オーバーエンジニアリングになりがち

3. client_reference_idを使用(✅ 採用)

  • 特徴:
    • 決済前にチーム情報をDBに保存
    • Webhookで状態管理が可能
    • Payment Linkの手軽さを維持

StripeのSessionなどでよく使われる client_reference_id を利用して、決済前にチーム情報をデータベースに保存 し、後からWebhookで最終的なステータスを更新する流れです。

トランザクションテーブルの定義

# トランザクションテーブルの定義(実装例)
class StripeTransaction(db.Model):
    __tablename__ = 'stripe_transactions'
    
    id = db.Column(db.Integer, primary_key=True)
    client_reference_id = db.Column(db.String(255), unique=True, index=True)
    team_id = db.Column(db.String(255), index=True)
    app_id = db.Column(db.String(255))
    status = db.Column(db.String(50), default='pending')
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    completed_at = db.Column(db.DateTime)
    session_id = db.Column(db.String(255))

    def __repr__(self):
        return f'<StripeTransaction {self.client_reference_id}>'

Payment LinkのURL生成

# Payment LinkのURL生成処理(実装例)
def get_payment_url(email: str, team_id: str, app_id: str):
    client_reference_id = str(uuid.uuid4())
    
    # トランザクションをDBに記録
    transaction = StripeTransaction(
        client_reference_id=client_reference_id,
        team_id=team_id,
        app_id=app_id
    )
    db.session.add(transaction)
    db.session.commit()

    url = f"{STRIPE_URL}?prefilled_email={email}&client_reference_id={client_reference_id}"
    return url

Webhook処理

# Webhook処理(実装例)
@app.route("/stripe/success", methods=["POST"])
def stripe_success():
    # ... Webhookの検証処理 ...

    if event['type'] == 'checkout.session.completed':
        session = event['data']['object']
        client_reference_id = session.get('client_reference_id')
        
        if client_reference_id:
            transaction = StripeTransaction.query.filter_by(
                client_reference_id=client_reference_id
            ).first()
            
            if transaction:
                transaction.status = 'completed'
                transaction.completed_at = datetime.utcnow()
                transaction.session_id = session.get('id')
                db.session.commit()

Slackアプリでの実装例 🤖

Slackのメッセージ内にPayment Linkのボタンを表示する実装例です。
この実装例では、Slackのメッセージに決済ボタンを設置し、ユーザーがボタンをクリックするとStripeのPayment Linkに遷移します。
Payment LinkのURLには、client_reference_idが含まれており、これにより決済情報とSlackのチーム情報を紐付けます。

# メッセージ送信用の関数(実装例)
def send_payment_message(client, channel_id: str, price: str, team_id: str):
    try:
        # ボタン用のPayment Link URLを生成
        payment_url = get_payment_url(
            email="メールアドレス",
            team_id=team_id,
            app_id="YOUR_APP_ID"
        )
        
        # 決済ボタンを含むメッセージを送信
        client.chat_postMessage(
            channel=channel_id,
            blocks=[
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": f"💰 料金: {price}円"
                    }
                },
                {
                    "type": "actions",
                    "elements": [
                        {
                            "type": "button",
                            "text": {
                                "type": "plain_text",
                                "text": "決済する",
                                "emoji": True
                            },
                            "url": payment_url,  # 生成したPayment LinkのURLを直接セット
                            "style": "primary"
                        }
                    ]
                }
            ]
        )
    except Exception as e:
        logger.error(f"Failed to send payment message: {str(e)}")
        # エラー時の処理

使用例:

# Slashコマンドやイベントに応じて決済メッセージを送信
@app.command("/payment")
def handle_payment_command(ack, body, client):
    ack()
    
    channel_id = body["channel_id"]
    team_id = body["team_id"]
    
    send_payment_message(
        client=client,
        channel_id=channel_id,
        price="1,000",
        team_id=team_id
    )

このように実装することで:

  1. シンプルな動線:

    • メッセージ内のボタンから直接Payment Linkへ遷移
    • DMを介さずにスムーズな決済フローを実現
  2. トラッキング:

    • ボタン生成時にUUIDを発行し、トランザクションテーブルに記録
    • team_idとclient_reference_idの紐付けが完了
    • Webhookで決済状況を追跡可能
  3. 注意点:

    • パブリックチャンネルで使用する場合、URLは誰でもアクセス可能
    • 必要に応じて、コマンドの実行権限を制限することを推奨

採用した実装方法のポイント

  1. セキュリティ

    • client_reference_id にはUUIDのみを使用し、チーム情報を含めない
    • URLに含める情報を最小限に抑え、外部にデータ構造を露出させない
    • Webhookの署名確認など、Stripe標準の機能で安全性を担保する
  2. トレーサビリティ

    • トランザクションテーブルを用いることで、支払いの進行状況を簡単に追跡
    • ステータス管理や日時管理で支払い履歴を明確化
  3. シンプルな実装

    • Payment Linkの魅力である手軽さをそのまま活かす
    • 必要最低限の要素だけを残して過剰実装を避ける

実装時の注意点

  1. client_reference_idの生成

    • UUIDなどの一意性が保証された値を利用
    • 機密情報は一切含めない(URL上で他人に見られてしまう可能性を排除)
  2. エラーハンドリング

    • Webhookの検証に失敗した場合や署名不一致の場合の対応
    • トランザクションテーブルとの整合性が取れない場合のログ・アラート

まとめ

  • Payment Link の便利さを活かしながら、チーム情報の紐付けを実現
  • client_reference_id + トランザクションテーブルの組み合わせで、決済プロセスの追跡をスムーズに
  • 過剰に複雑化することなく、セキュリティや管理面を担保できる

参考資料

リバナレテックブログ

Discussion