mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-06 14:22:47 +02:00
fix: metadata extraction in Stripe checkout session
- Updated the `_get_metadata` function to handle changes in the Stripe SDK, specifically for `StripeObject` which is no longer a subclass of `dict` in `stripe>=15.0`. - Implemented a fallback mechanism in `finalize_checkout` to recover purchase type from the database if metadata extraction fails, ensuring robust handling of checkout sessions.
This commit is contained in:
parent
0bd3281ccf
commit
dd8c503eb0
1 changed files with 83 additions and 12 deletions
|
|
@ -99,20 +99,49 @@ def _normalize_optional_string(value: Any) -> str | None:
|
|||
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.
|
||||
In ``stripe>=15.0`` ``StripeObject`` is no longer a ``dict`` subclass
|
||||
and exposes neither ``items()`` nor ``__iter__`` nor ``keys()``.
|
||||
``dict(obj)`` falls into the sequence protocol and raises
|
||||
``KeyError: 0``; ``obj.items()`` raises ``AttributeError``. The
|
||||
supported way to materialize a ``StripeObject`` as a plain dict is
|
||||
its ``to_dict()`` method (added in stripe-python 8.x, present in 15.x).
|
||||
"""
|
||||
metadata = getattr(checkout_session, "metadata", None) or {}
|
||||
try:
|
||||
items = metadata.items()
|
||||
except AttributeError:
|
||||
metadata = getattr(checkout_session, "metadata", None)
|
||||
if metadata is None:
|
||||
return {}
|
||||
return {str(key): str(value) for key, value in items}
|
||||
|
||||
# 1. Plain dict (older SDKs that subclassed dict, JSON-decoded events
|
||||
# in tests, etc.).
|
||||
if isinstance(metadata, dict):
|
||||
return {str(k): str(v) for k, v in metadata.items()}
|
||||
|
||||
# 2. Modern Stripe SDK: every ``StripeObject`` has ``to_dict()``.
|
||||
# ``recursive=False`` is correct because Stripe metadata values
|
||||
# are always primitive strings.
|
||||
to_dict = getattr(metadata, "to_dict", None)
|
||||
if callable(to_dict):
|
||||
try:
|
||||
d = to_dict(recursive=False)
|
||||
if isinstance(d, dict):
|
||||
return {str(k): str(v) for k, v in d.items()}
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Stripe metadata.to_dict() failed for session %s",
|
||||
getattr(checkout_session, "id", "?"),
|
||||
)
|
||||
|
||||
# 3. Last-resort: read the SDK's private ``_data`` backing dict.
|
||||
# Stable across stripe-python 6.x -> 15.x.
|
||||
inner = getattr(metadata, "_data", None)
|
||||
if isinstance(inner, dict):
|
||||
return {str(k): str(v) for k, v in inner.items()}
|
||||
|
||||
logger.warning(
|
||||
"Could not extract metadata from checkout session %s (metadata type=%s)",
|
||||
getattr(checkout_session, "id", "?"),
|
||||
type(metadata).__name__,
|
||||
)
|
||||
return {}
|
||||
|
||||
|
||||
# Canonical purchase_type metadata values. ``premium_credit`` was emitted
|
||||
|
|
@ -584,6 +613,48 @@ async def finalize_checkout(
|
|||
payment_status = getattr(checkout_session, "payment_status", None)
|
||||
session_status = getattr(checkout_session, "status", None)
|
||||
|
||||
# Defensive fallback: if metadata can't be read for any reason
|
||||
# (extraction failure, manually-created session in Stripe dashboard,
|
||||
# SDK upgrade breaking ``to_dict``, etc.) we'd otherwise route every
|
||||
# purchase to the page_packs handler and get stuck. Resolve the
|
||||
# purchase_type by checking which table actually has the row keyed
|
||||
# by this Stripe session id.
|
||||
if not metadata:
|
||||
existing_token_purchase = (
|
||||
await db_session.execute(
|
||||
select(PremiumTokenPurchase.id).where(
|
||||
PremiumTokenPurchase.stripe_checkout_session_id
|
||||
== str(checkout_session.id)
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if existing_token_purchase is not None:
|
||||
is_token = True
|
||||
else:
|
||||
existing_page_purchase = (
|
||||
await db_session.execute(
|
||||
select(PagePurchase.id).where(
|
||||
PagePurchase.stripe_checkout_session_id
|
||||
== str(checkout_session.id)
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if existing_page_purchase is None:
|
||||
logger.error(
|
||||
"finalize_checkout: no purchase row in either table "
|
||||
"and metadata is empty for session=%s user=%s",
|
||||
session_id,
|
||||
user.id,
|
||||
)
|
||||
# Fall through; downstream path will short-circuit on
|
||||
# missing-row + empty-metadata.
|
||||
logger.info(
|
||||
"finalize_checkout: recovered purchase_type=%s for session=%s "
|
||||
"via DB fallback (metadata was empty)",
|
||||
"premium_tokens" if is_token else "page_packs",
|
||||
session_id,
|
||||
)
|
||||
|
||||
is_paid = payment_status in {"paid", "no_payment_required"}
|
||||
is_expired = session_status == "expired"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue