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,6 +451,7 @@ async def stripe_webhook(
detail="Invalid Stripe webhook signature.", detail="Invalid Stripe webhook signature.",
) from exc ) from exc
try:
if event.type in { if event.type in {
"checkout.session.completed", "checkout.session.completed",
"checkout.session.async_payment_succeeded", "checkout.session.async_payment_succeeded",
@ -459,7 +472,9 @@ async def stripe_webhook(
metadata = _get_metadata(checkout_session) metadata = _get_metadata(checkout_session)
purchase_type = metadata.get("purchase_type", "page_packs") purchase_type = metadata.get("purchase_type", "page_packs")
if purchase_type == "premium_tokens": if purchase_type == "premium_tokens":
return await _fulfill_completed_token_purchase(db_session, checkout_session) return await _fulfill_completed_token_purchase(
db_session, checkout_session
)
return await _fulfill_completed_purchase(db_session, checkout_session) return await _fulfill_completed_purchase(db_session, checkout_session)
if event.type in { if event.type in {
@ -474,6 +489,17 @@ async def stripe_webhook(
db_session, str(checkout_session.id) db_session, str(checkout_session.id)
) )
return await _mark_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()