feat(payments): reuse pending provider payments
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import Date, and_, case, cast, func
|
||||
from sqlalchemy import Date, and_, case, cast, func, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy.orm import joinedload, selectinload
|
||||
@@ -116,31 +116,37 @@ async def find_recent_pending_provider_payment(
|
||||
provider: str,
|
||||
pending_status: str,
|
||||
amount: float,
|
||||
currency: Optional[str],
|
||||
sale_mode: Optional[str],
|
||||
months: Optional[int],
|
||||
purchased_gb: Optional[float],
|
||||
purchased_hwid_devices: Optional[int],
|
||||
tariff_key: Optional[str] = None,
|
||||
since_minutes: int = 60,
|
||||
since_minutes: Optional[int] = None,
|
||||
) -> Optional[Payment]:
|
||||
"""Return the most recent pending payment matching the given tariff parameters.
|
||||
|
||||
Used to reuse an existing provider payment link instead of creating a new one
|
||||
on repeated user clicks. Only payments with a populated ``provider_payment_id``
|
||||
are returned — without it, there's no link to reuse.
|
||||
on repeated user clicks. A generic or provider-specific payment id must be
|
||||
populated so the caller can verify the remote payment link.
|
||||
"""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(minutes=max(1, since_minutes))
|
||||
|
||||
conditions = [
|
||||
Payment.user_id == user_id,
|
||||
Payment.provider == provider,
|
||||
Payment.status == pending_status,
|
||||
Payment.provider_payment_id.isnot(None),
|
||||
Payment.created_at >= cutoff,
|
||||
Payment.status.in_((pending_status, "pending")),
|
||||
or_(
|
||||
Payment.provider_payment_id.isnot(None),
|
||||
Payment.yookassa_payment_id.isnot(None),
|
||||
),
|
||||
func.abs(Payment.amount - float(amount)) < 0.01,
|
||||
]
|
||||
if since_minutes is not None:
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(minutes=max(1, since_minutes))
|
||||
conditions.append(Payment.created_at >= cutoff)
|
||||
if currency is not None:
|
||||
conditions.append(func.upper(Payment.currency) == str(currency).strip().upper())
|
||||
if sale_mode is not None:
|
||||
conditions.append(Payment.sale_mode == sale_mode)
|
||||
if tariff_key is not None:
|
||||
@@ -238,12 +244,18 @@ async def count_user_succeeded_payments(
|
||||
|
||||
|
||||
async def update_provider_payment_and_status(
|
||||
session: AsyncSession, payment_db_id: int, provider_payment_id: str, new_status: str
|
||||
session: AsyncSession,
|
||||
payment_db_id: int,
|
||||
provider_payment_id: str,
|
||||
new_status: str,
|
||||
provider_payment_url: Optional[str] = None,
|
||||
) -> Optional[Payment]:
|
||||
payment = await get_payment_by_db_id(session, payment_db_id)
|
||||
if payment:
|
||||
payment.status = new_status
|
||||
payment.provider_payment_id = provider_payment_id
|
||||
if provider_payment_url:
|
||||
payment.provider_payment_url = provider_payment_url
|
||||
payment.updated_at = func.now()
|
||||
await session.flush()
|
||||
await session.refresh(payment)
|
||||
|
||||
@@ -1155,6 +1155,13 @@ def _migration_0035_add_subscription_promo_expiry_flag(connection: Connection) -
|
||||
)
|
||||
|
||||
|
||||
def _migration_0036_add_provider_payment_url(connection: Connection) -> None:
|
||||
inspector = inspect(connection)
|
||||
columns: Set[str] = {col["name"] for col in inspector.get_columns("payments")}
|
||||
if "provider_payment_url" not in columns:
|
||||
connection.execute(text("ALTER TABLE payments ADD COLUMN provider_payment_url VARCHAR"))
|
||||
|
||||
|
||||
MIGRATIONS: List[Migration] = [
|
||||
Migration(
|
||||
id="0001_add_channel_subscription_fields",
|
||||
@@ -1342,6 +1349,11 @@ MIGRATIONS: List[Migration] = [
|
||||
description="Suppress multi-day expiry reminders for trial and bonus subscriptions",
|
||||
upgrade=_migration_0035_add_subscription_promo_expiry_flag,
|
||||
),
|
||||
Migration(
|
||||
id="0036_add_provider_payment_url",
|
||||
description="Persist provider payment links for reusable pending payments",
|
||||
upgrade=_migration_0036_add_provider_payment_url,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -203,6 +203,7 @@ class Payment(Base):
|
||||
user_id = Column(BigInteger, ForeignKey("users.user_id"), nullable=False, index=True)
|
||||
yookassa_payment_id = Column(String, unique=True, index=True, nullable=True)
|
||||
provider_payment_id = Column(String, unique=True, nullable=True)
|
||||
provider_payment_url = Column(String, nullable=True)
|
||||
provider = Column(String, nullable=False, default="yookassa", index=True)
|
||||
idempotence_key = Column(String, unique=True, nullable=True)
|
||||
amount = Column(Float, nullable=False)
|
||||
|
||||
Reference in New Issue
Block a user