mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-08 07:12:39 +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]:
|
def _get_metadata(checkout_session: Any) -> dict[str, str]:
|
||||||
"""Extract checkout session metadata as a plain ``str -> str`` dict.
|
"""Extract checkout session metadata as a plain ``str -> str`` dict.
|
||||||
|
|
||||||
Works for both ``dict`` (e.g. when the metadata round-tripped through
|
In ``stripe>=15.0`` ``StripeObject`` is no longer a ``dict`` subclass
|
||||||
JSON) and Stripe's ``StripeObject`` wrapper. Recent Stripe SDK
|
and exposes neither ``items()`` nor ``__iter__`` nor ``keys()``.
|
||||||
versions stopped subclassing ``dict`` for ``StripeObject``, so
|
``dict(obj)`` falls into the sequence protocol and raises
|
||||||
``isinstance(metadata, dict)`` is False and ``dict(metadata)`` falls
|
``KeyError: 0``; ``obj.items()`` raises ``AttributeError``. The
|
||||||
into the sequence protocol, looking up integer indices and raising
|
supported way to materialize a ``StripeObject`` as a plain dict is
|
||||||
``KeyError: 0``. ``.items()`` is exposed by both shapes via the
|
its ``to_dict()`` method (added in stripe-python 8.x, present in 15.x).
|
||||||
Mapping protocol, so we always use that.
|
|
||||||
"""
|
"""
|
||||||
metadata = getattr(checkout_session, "metadata", None) or {}
|
metadata = getattr(checkout_session, "metadata", None)
|
||||||
try:
|
if metadata is None:
|
||||||
items = metadata.items()
|
|
||||||
except AttributeError:
|
|
||||||
return {}
|
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
|
# 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)
|
payment_status = getattr(checkout_session, "payment_status", None)
|
||||||
session_status = getattr(checkout_session, "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_paid = payment_status in {"paid", "no_payment_required"}
|
||||||
is_expired = session_status == "expired"
|
is_expired = session_status == "expired"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue