Implement auto-renew subscription feature and enhance payment method handling
- Added a recurring billing task to automatically charge users one day before subscription expiry, improving subscription management. - Introduced a new UserBilling model to store saved payment methods for off-session charges, enhancing user experience. - Updated YooKassa service to support saving payment methods and capturing payments for auto-renewals. - Enhanced subscription handling to toggle auto-renew settings and provide user feedback through localized messages. - Improved error handling and logging for payment method persistence and subscription renewal processes.
This commit is contained in:
@@ -53,6 +53,11 @@ async def update_subscription(
|
||||
return sub
|
||||
|
||||
|
||||
async def set_auto_renew(session: AsyncSession, subscription_id: int, enabled: bool) -> Optional[Subscription]:
|
||||
"""Toggle auto_renew_enabled for a subscription."""
|
||||
return await update_subscription(session, subscription_id, {"auto_renew_enabled": enabled})
|
||||
|
||||
|
||||
async def set_user_subscriptions_cancelled_with_grace(
|
||||
session: AsyncSession, user_id: int, grace_days: int = 1) -> int:
|
||||
"""Mark all active user subscriptions as cancelled with a short grace period.
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
from typing import Optional, Dict, Any
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from db.models import UserBilling
|
||||
|
||||
|
||||
async def get_user_billing(session: AsyncSession, user_id: int) -> Optional[UserBilling]:
|
||||
stmt = select(UserBilling).where(UserBilling.user_id == user_id)
|
||||
result = await session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def upsert_yk_payment_method(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
user_id: int,
|
||||
payment_method_id: str,
|
||||
card_last4: Optional[str] = None,
|
||||
card_network: Optional[str] = None,
|
||||
) -> UserBilling:
|
||||
existing = await get_user_billing(session, user_id)
|
||||
if existing:
|
||||
existing.yookassa_payment_method_id = payment_method_id
|
||||
existing.card_last4 = card_last4
|
||||
existing.card_network = card_network
|
||||
existing.updated_at = func.now()
|
||||
await session.flush()
|
||||
await session.refresh(existing)
|
||||
return existing
|
||||
record = UserBilling(
|
||||
user_id=user_id,
|
||||
yookassa_payment_method_id=payment_method_id,
|
||||
card_last4=card_last4,
|
||||
card_network=card_network,
|
||||
)
|
||||
session.add(record)
|
||||
await session.flush()
|
||||
await session.refresh(record)
|
||||
return record
|
||||
@@ -72,6 +72,7 @@ class Subscription(Base):
|
||||
last_notification_sent = Column(DateTime(timezone=True), nullable=True)
|
||||
provider = Column(String, nullable=True)
|
||||
skip_notifications = Column(Boolean, default=False)
|
||||
auto_renew_enabled = Column(Boolean, default=False, index=True)
|
||||
|
||||
user = relationship("User", back_populates="subscriptions")
|
||||
|
||||
@@ -112,6 +113,19 @@ class Payment(Base):
|
||||
back_populates="payments_where_used")
|
||||
|
||||
|
||||
class UserBilling(Base):
|
||||
__tablename__ = "user_billing"
|
||||
|
||||
user_id = Column(BigInteger, ForeignKey("users.user_id"), primary_key=True)
|
||||
# Saved payment method for off-session recurring charges (YooKassa)
|
||||
yookassa_payment_method_id = Column(String, nullable=True, unique=True)
|
||||
card_last4 = Column(String, nullable=True)
|
||||
card_network = Column(String, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), nullable=True)
|
||||
|
||||
user = relationship("User")
|
||||
|
||||
class PromoCode(Base):
|
||||
__tablename__ = "promo_codes"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user