From afd44408e0d01f4fbc5d11f449f82af2d62b0f9d Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Mon, 25 May 2026 14:04:58 +0100 Subject: [PATCH] fix(billing): use Stripe SDK v5 attribute access in webhook handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stripe.Webhook.construct_event() now returns StripeObject instances that do not support .get()/__getitem__ — must use attribute access. event["type"] → event.type, session.get("x") → session.x, etc. Co-Authored-By: Claude Sonnet 4.6 --- backend/app/routes/billing.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/backend/app/routes/billing.py b/backend/app/routes/billing.py index 5ab53fa2..5764bf41 100644 --- a/backend/app/routes/billing.py +++ b/backend/app/routes/billing.py @@ -139,20 +139,21 @@ async def stripe_webhook(): logger.warning(f"Webhook signature invalid: {e}") return jsonify({"message": "Invalid signature"}), 400 - if event["type"] == "checkout.session.completed": - session = event["data"]["object"] + 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")) + if session.payment_status != "paid": + logger.info("Skipping unpaid checkout session %s", session.id) return jsonify({"status": "ok"}), 200 - meta = session.get("metadata", {}) + # metadata is a plain dict in the Stripe SDK + meta = dict(session.metadata) if session.metadata else {} user_id = meta.get("user_id") credits = int(meta.get("credits", 0)) pack_id = meta.get("pack_id", "") # `or` so explicit null payment_intent falls through to session id - payment_id = session.get("payment_intent") or session.get("id", "") + payment_id = session.payment_intent or session.id if not user_id or credits <= 0 or not payment_id: return jsonify({"status": "ok"}), 200