Merge pull request #1345 from MODSetter/fix/stripe-routes

fix: stripe weebhook
This commit is contained in:
Rohan Verma 2026-05-05 00:18:41 -07:00 committed by GitHub
commit 3edefb9596
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -88,10 +88,22 @@ def _normalize_optional_string(value: Any) -> str | None:
def _get_metadata(checkout_session: Any) -> dict[str, str]: def _get_metadata(checkout_session: Any) -> dict[str, str]:
"""Extract checkout session metadata as a plain ``str -> str`` dict.
Works for both ``dict`` (e.g. when the metadata round-tripped through
JSON) and Stripe's ``StripeObject`` wrapper. Recent Stripe SDK
versions stopped subclassing ``dict`` for ``StripeObject``, so
``isinstance(metadata, dict)`` is False and ``dict(metadata)`` falls
into the sequence protocol, looking up integer indices and raising
``KeyError: 0``. ``.items()`` is exposed by both shapes via the
Mapping protocol, so we always use that.
"""
metadata = getattr(checkout_session, "metadata", None) or {} metadata = getattr(checkout_session, "metadata", None) or {}
if isinstance(metadata, dict): try:
return {str(key): str(value) for key, value in metadata.items()} items = metadata.items()
return dict(metadata) except AttributeError:
return {}
return {str(key): str(value) for key, value in items}
async def _get_or_create_purchase_from_checkout_session( async def _get_or_create_purchase_from_checkout_session(
@ -439,41 +451,55 @@ async def stripe_webhook(
detail="Invalid Stripe webhook signature.", detail="Invalid Stripe webhook signature.",
) from exc ) from exc
if event.type in { try:
"checkout.session.completed", if event.type in {
"checkout.session.async_payment_succeeded", "checkout.session.completed",
}: "checkout.session.async_payment_succeeded",
checkout_session = event.data.object
payment_status = getattr(checkout_session, "payment_status", None)
if event.type == "checkout.session.completed" and payment_status not in {
"paid",
"no_payment_required",
}: }:
logger.info( checkout_session = event.data.object
"Received checkout.session.completed for unpaid session %s; waiting for async success.", payment_status = getattr(checkout_session, "payment_status", None)
checkout_session.id,
)
return StripeWebhookResponse()
metadata = _get_metadata(checkout_session) if event.type == "checkout.session.completed" and payment_status not in {
purchase_type = metadata.get("purchase_type", "page_packs") "paid",
if purchase_type == "premium_tokens": "no_payment_required",
return await _fulfill_completed_token_purchase(db_session, checkout_session) }:
return await _fulfill_completed_purchase(db_session, checkout_session) logger.info(
"Received checkout.session.completed for unpaid session %s; waiting for async success.",
checkout_session.id,
)
return StripeWebhookResponse()
if event.type in { metadata = _get_metadata(checkout_session)
"checkout.session.async_payment_failed", purchase_type = metadata.get("purchase_type", "page_packs")
"checkout.session.expired", if purchase_type == "premium_tokens":
}: return await _fulfill_completed_token_purchase(
checkout_session = event.data.object db_session, checkout_session
metadata = _get_metadata(checkout_session) )
purchase_type = metadata.get("purchase_type", "page_packs") return await _fulfill_completed_purchase(db_session, checkout_session)
if purchase_type == "premium_tokens":
return await _mark_token_purchase_failed( if event.type in {
db_session, str(checkout_session.id) "checkout.session.async_payment_failed",
) "checkout.session.expired",
return await _mark_purchase_failed(db_session, str(checkout_session.id)) }:
checkout_session = event.data.object
metadata = _get_metadata(checkout_session)
purchase_type = metadata.get("purchase_type", "page_packs")
if purchase_type == "premium_tokens":
return await _mark_token_purchase_failed(
db_session, str(checkout_session.id)
)
return await _mark_purchase_failed(db_session, str(checkout_session.id))
except Exception:
# Re-raise so FastAPI returns 500 and Stripe retries this delivery.
# Logging here gives us a structured trail with event id + type so
# future webhook bugs surface immediately in the logs without
# having to grep by request_id.
logger.exception(
"Stripe webhook handler failed for event id=%s type=%s — Stripe will retry",
getattr(event, "id", "?"),
getattr(event, "type", "?"),
)
raise
return StripeWebhookResponse() return StripeWebhookResponse()