fix: stripe webhook idempotency + payment_status guard
- Check payment_status == "paid" before granting credits — prevents granting credits for unpaid/pending checkout sessions - Idempotency guard: query credit_transactions for existing ref.stripe_payment_id before processing — Stripe retries webhooks on timeout, this prevents double credits Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b923605ea7
commit
6083a1e53c
1 changed files with 27 additions and 11 deletions
|
|
@ -95,22 +95,38 @@ async def stripe_webhook():
|
|||
|
||||
if event["type"] == "checkout.session.completed":
|
||||
session = event["data"]["object"]
|
||||
|
||||
# Only process confirmed payments — never grant credits for unpaid sessions
|
||||
if session.get("payment_status") != "paid":
|
||||
logger.info("Skipping unpaid checkout session %s", session.get("id"))
|
||||
return jsonify({"status": "ok"}), 200
|
||||
|
||||
meta = session.get("metadata", {})
|
||||
user_id = meta.get("user_id")
|
||||
credits = int(meta.get("credits", 0))
|
||||
pack_id = meta.get("pack_id", "")
|
||||
payment_id = session.get("payment_intent", session.get("id", ""))
|
||||
|
||||
if user_id and credits > 0:
|
||||
balance = await User.grant_credits(user_id, credits)
|
||||
await CreditTransaction.record(
|
||||
user_id=user_id,
|
||||
tx_type="purchase",
|
||||
amount=credits,
|
||||
balance_after=balance,
|
||||
description=f"Purchased {pack_id} pack ({credits} credits)",
|
||||
ref={"stripe_payment_id": payment_id},
|
||||
)
|
||||
logger.info(f"Granted {credits} credits to user {user_id} via Stripe")
|
||||
if not user_id or credits <= 0:
|
||||
return jsonify({"status": "ok"}), 200
|
||||
|
||||
# Idempotency — skip if this payment_intent was already processed
|
||||
from app.db import get_db as _get_db
|
||||
db = await _get_db()
|
||||
already = await db.credit_transactions.find_one({"ref.stripe_payment_id": payment_id})
|
||||
if already:
|
||||
logger.info("Duplicate webhook for payment %s — skipping", payment_id)
|
||||
return jsonify({"status": "ok"}), 200
|
||||
|
||||
balance = await User.grant_credits(user_id, credits)
|
||||
await CreditTransaction.record(
|
||||
user_id=user_id,
|
||||
tx_type="purchase",
|
||||
amount=credits,
|
||||
balance_after=balance,
|
||||
description=f"Purchased {pack_id} pack ({credits} credits)",
|
||||
ref={"stripe_payment_id": payment_id},
|
||||
)
|
||||
logger.info("Granted %d credits to user %s via Stripe payment %s", credits, user_id, payment_id)
|
||||
|
||||
return jsonify({"status": "ok"}), 200
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue