fix: integrate Yandex Metrika offline conv + S2S postback hooks

Restore integration hooks dropped in PR #2851 merge:
- PurchaseRequest accepts yandex_cid, referrer, subid from frontend
- Cache yandex_cid and subid in Redis at purchase creation (24h TTL)
- On fulfill_purchase: extract subid from cache, persist to DB
- Save Yandex CID from Redis to yandex_client_id_map
- Fire on_registration + S2S postback for new accounts
- Fire on_purchase + S2S postback for all paid purchases
- All hooks wrapped in try/except — failures never block delivery
This commit is contained in:
Fringg
2026-04-24 16:59:27 +03:00
parent 2cde38c63b
commit 7d512d214a
2 changed files with 87 additions and 1 deletions
+20 -1
View File
@@ -23,7 +23,7 @@ from app.services.guest_purchase_service import (
) )
from app.services.payment_method_config_service import _get_method_defaults from app.services.payment_method_config_service import _get_method_defaults
from app.services.payment_service import PaymentService from app.services.payment_service import PaymentService
from app.utils.cache import RateLimitCache from app.utils.cache import RateLimitCache, cache
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
@@ -128,6 +128,9 @@ class PurchaseRequest(BaseModel):
gift_recipient_type: str | None = Field(default=None, pattern=r'^(email|telegram)$') gift_recipient_type: str | None = Field(default=None, pattern=r'^(email|telegram)$')
gift_recipient_value: str | None = Field(default=None, max_length=255) gift_recipient_value: str | None = Field(default=None, max_length=255)
gift_message: str | None = Field(default=None, max_length=1000) gift_message: str | None = Field(default=None, max_length=1000)
yandex_cid: str | None = Field(default=None, max_length=128, pattern=r'^[A-Za-z0-9._:-]{4,128}$')
referrer: str | None = Field(default=None, max_length=500)
subid: str | None = Field(default=None, max_length=255)
@model_validator(mode='after') @model_validator(mode='after')
def validate_contacts(self) -> 'PurchaseRequest': def validate_contacts(self) -> 'PurchaseRequest':
@@ -654,6 +657,8 @@ async def create_landing_purchase(
gift_recipient_type=body.gift_recipient_type, gift_recipient_type=body.gift_recipient_type,
gift_recipient_value=body.gift_recipient_value, gift_recipient_value=body.gift_recipient_value,
gift_message=body.gift_message, gift_message=body.gift_message,
subid=body.subid,
referrer=body.referrer,
commit=False, commit=False,
) )
@@ -706,6 +711,20 @@ async def create_landing_purchase(
await db.commit() await db.commit()
await db.refresh(purchase) await db.refresh(purchase)
# Persist Yandex CID in cache so fulfill_purchase can link it to the user later
if body.yandex_cid and settings.YANDEX_OFFLINE_CONV_ENABLED:
try:
await cache.set(f'yacid:purchase:{purchase.token}', body.yandex_cid, expire=86400)
except Exception:
pass
# Persist subid in cache for S2S postback
if body.subid:
try:
await cache.set(f'subid:purchase:{purchase.token}', body.subid, expire=86400)
except Exception:
pass
return PurchaseResponse( return PurchaseResponse(
purchase_token=purchase.token, purchase_token=purchase.token,
payment_url=payment_url, payment_url=payment_url,
+67
View File
@@ -145,6 +145,8 @@ async def create_purchase(
gift_recipient_value: str | None = None, gift_recipient_value: str | None = None,
gift_message: str | None = None, gift_message: str | None = None,
source: str = 'landing', source: str = 'landing',
subid: str | None = None,
referrer: str | None = None,
buyer_user_id: int | None = None, buyer_user_id: int | None = None,
commit: bool = True, commit: bool = True,
) -> GuestPurchase: ) -> GuestPurchase:
@@ -152,6 +154,8 @@ async def create_purchase(
purchase = await create_guest_purchase( purchase = await create_guest_purchase(
db, db,
commit=commit, commit=commit,
subid=subid,
referrer=referrer,
landing_id=landing.id if landing else None, landing_id=landing.id if landing else None,
tariff_id=tariff.id, tariff_id=tariff.id,
period_days=period_days, period_days=period_days,
@@ -438,6 +442,20 @@ async def fulfill_purchase(
purchase.status = GuestPurchaseStatus.DELIVERED.value purchase.status = GuestPurchaseStatus.DELIVERED.value
purchase.user_id = user.id purchase.user_id = user.id
purchase.delivered_at = datetime.now(UTC) purchase.delivered_at = datetime.now(UTC)
# Extract subid from Redis cache (saved at purchase creation)
try:
from app.utils.cache import cache
_cached_subid = await cache.get(f'subid:purchase:{purchase.token}')
if _cached_subid:
purchase.subid = _cached_subid if isinstance(_cached_subid, str) else _cached_subid.decode()
from app.database.crud.yandex_client_id import upsert_subid
await upsert_subid(db, user.id, purchase.subid, source='landing')
except Exception:
pass
if recipient_type == 'email' and not purchase.is_gift and is_new_account: if recipient_type == 'email' and not purchase.is_gift and is_new_account:
purchase.auto_login_token = create_auto_login_token(user.id) purchase.auto_login_token = create_auto_login_token(user.id)
@@ -463,6 +481,55 @@ async def fulfill_purchase(
except Exception: except Exception:
logger.exception('Failed to create transaction for guest purchase', purchase_id=purchase.id) logger.exception('Failed to create transaction for guest purchase', purchase_id=purchase.id)
# Save Yandex CID from Redis → DB (enables on_registration/on_purchase to use it)
try:
from app.services import yandex_offline_conv_service as yandex_conv
from app.utils.cache import cache
_cached_cid = await cache.get(f'yacid:purchase:{purchase.token}')
if _cached_cid:
await yandex_conv.store_cid(db, user.id, _cached_cid, source='landing')
await db.commit()
except Exception:
logger.debug('Failed to save CID from Redis')
# Registration event (new accounts only) + S2S postback
if is_new_account:
try:
from app.services import yandex_offline_conv_service as yandex_conv
await yandex_conv.on_registration(db, user.id)
except Exception:
logger.debug('Yandex on_registration hook error')
try:
from app.database.crud.yandex_client_id import get_subid
from app.services.s2s_postback_service import send_postback
_subid = purchase.subid or await get_subid(db, user.id)
if _subid:
await send_postback('registration', _subid, user_id=user.id)
except Exception:
logger.debug('S2S postback registration hook error')
# Purchase event + S2S postback (always for paid purchases)
try:
from app.services import yandex_offline_conv_service as yandex_conv
await yandex_conv.on_purchase(db, user.id, purchase.amount_kopeks)
except Exception:
logger.debug('Yandex on_purchase hook error')
try:
from app.database.crud.yandex_client_id import get_subid
from app.services.s2s_postback_service import send_postback
_subid = purchase.subid or await get_subid(db, user.id)
if _subid:
await send_postback('purchase', _subid, amount=purchase.amount_kopeks / 100, user_id=user.id)
except Exception:
logger.debug('S2S postback purchase hook error')
try: try:
await send_guest_notification( await send_guest_notification(
purchase, purchase,