diff --git a/bot/services/panel_webhook_service.py b/bot/services/panel_webhook_service.py index 8128c7b..b94f4c8 100644 --- a/bot/services/panel_webhook_service.py +++ b/bot/services/panel_webhook_service.py @@ -11,6 +11,7 @@ from config.settings import Settings from bot.middlewares.i18n import JsonI18n from bot.keyboards.inline.user_keyboards import get_subscribe_only_markup from db.dal import user_dal +from bot.utils.date_utils import add_months EVENT_MAP = { "user.expires_in_72_hours": (3, "subscription_72h_notification"), @@ -48,7 +49,7 @@ class PanelWebhookService: Returns True if an auto-renewal was performed (and renewal message sent), False otherwise. """ from db.dal import subscription_dal, payment_dal - from datetime import datetime, timezone, timedelta + from datetime import datetime, timezone try: auto_renewed = False @@ -68,8 +69,8 @@ class PanelWebhookService: # This user has tribute payments, auto-renew for the same duration logging.info(f"Auto-renewing tribute subscription for user {user_id} for {last_tribute_duration} months") - # Extend subscription by the last payment duration - new_end_date = datetime.now(timezone.utc) + timedelta(days=last_tribute_duration * 30) + # Extend subscription by the last payment duration (calendar months) + new_end_date = add_months(datetime.now(timezone.utc), last_tribute_duration) await subscription_dal.update_subscription( session, diff --git a/bot/services/subscription_service.py b/bot/services/subscription_service.py index 12e9377..b349e39 100644 --- a/bot/services/subscription_service.py +++ b/bot/services/subscription_service.py @@ -6,6 +6,7 @@ from aiogram import Bot from bot.middlewares.i18n import JsonI18n from db.dal import user_dal, subscription_dal, promo_code_dal, payment_dal +from bot.utils.date_utils import add_months from db.models import User, Subscription from config.settings import Settings @@ -424,7 +425,9 @@ class SubscriptionService: ): start_date = current_active_sub.end_date - duration_days_total = months * 30 + # base duration by months + end_after_months = add_months(start_date, months) + duration_days_total = (end_after_months - start_date).days applied_promo_bonus_days = 0 if promo_code_id_from_payment: diff --git a/bot/utils/date_utils.py b/bot/utils/date_utils.py new file mode 100644 index 0000000..b9e2fdf --- /dev/null +++ b/bot/utils/date_utils.py @@ -0,0 +1,27 @@ +from datetime import datetime, timedelta + + +def add_months(base_dt: datetime, months_to_add: int) -> datetime: + """Add calendar months to a datetime, clamping the day to the month's length. + + Preserves tzinfo from base_dt. + """ + year = base_dt.year + month = base_dt.month + months_to_add + day = base_dt.day + + # Normalize year and month + year += (month - 1) // 12 + month = ((month - 1) % 12) + 1 + + # Determine last day of target month by rolling to next month's first day and subtracting 1 day + if month == 12: + next_month_first = datetime(year + 1, 1, 1, tzinfo=base_dt.tzinfo) + else: + next_month_first = datetime(year, month + 1, 1, tzinfo=base_dt.tzinfo) + last_day = (next_month_first - timedelta(days=1)).day + + clamped_day = min(day, last_day) + return base_dt.replace(year=year, month=month, day=clamped_day) + +