From cf60ae2967b8a5b0c42c51354e3a2513c38e7120 Mon Sep 17 00:00:00 2001 From: Fringg Date: Wed, 29 Apr 2026 07:21:07 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20device/traffic=20addon=20pricing=20?= =?UTF-8?q?=E2=80=94=20use=20ceil=20instead=20of=20floor=20for=20days=5Fle?= =?UTF-8?q?ft?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit timedelta.days is integer floor: 29 days 23 hours = 29, not 30. When a user bought extra devices on the same day as their subscription, they were charged for ~1 day instead of the full remaining period. Fix: math.ceil(total_seconds / 86400) rounds partial days UP. Applied to all 11 locations across 4 files: - app/handlers/subscription/devices.py (5 spots) - app/cabinet/routes/subscription_modules/devices.py (3 spots) - app/keyboards/inline.py (3 spots — display pricing) - app/utils/pricing_utils.py (1 spot — traffic prorated pricing) --- app/cabinet/routes/subscription_modules/devices.py | 7 ++++--- app/handlers/subscription/devices.py | 11 ++++++----- app/keyboards/inline.py | 7 ++++--- app/utils/pricing_utils.py | 3 ++- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/cabinet/routes/subscription_modules/devices.py b/app/cabinet/routes/subscription_modules/devices.py index 60728108..90a8ab60 100644 --- a/app/cabinet/routes/subscription_modules/devices.py +++ b/app/cabinet/routes/subscription_modules/devices.py @@ -13,6 +13,7 @@ POST /subscription/devices/save-cart from __future__ import annotations +import math from datetime import UTC, datetime from typing import Any @@ -380,7 +381,7 @@ async def purchase_devices( if end_date.tzinfo is None: end_date = end_date.replace(tzinfo=UTC) - days_left = max(1, (end_date - now).days) + days_left = max(1, math.ceil((end_date - now).total_seconds() / 86400)) total_days = 30 # Base period for device price calculation # Устройства в пределах тарифного лимита — бесплатные @@ -658,7 +659,7 @@ async def save_devices_cart( if end_date.tzinfo is None: end_date = end_date.replace(tzinfo=UTC) - days_left = max(1, (end_date - now).days) + days_left = max(1, math.ceil((end_date - now).total_seconds() / 86400)) total_days = 30 # Устройства в пределах тарифного лимита — бесплатные @@ -772,7 +773,7 @@ async def get_device_price( if end_date.tzinfo is None: end_date = end_date.replace(tzinfo=UTC) - days_left = max(1, (end_date - now).days) + days_left = max(1, math.ceil((end_date - now).total_seconds() / 86400)) total_days = 30 # Устройства в пределах тарифного лимита — бесплатные diff --git a/app/handlers/subscription/devices.py b/app/handlers/subscription/devices.py index 63087c89..082b5d1c 100644 --- a/app/handlers/subscription/devices.py +++ b/app/handlers/subscription/devices.py @@ -1,4 +1,5 @@ import html as html_mod +import math from datetime import UTC, datetime from aiogram import types @@ -343,7 +344,7 @@ async def confirm_change_devices( # Считаем стоимость по оставшимся дням подписки now = datetime.now(UTC) - days_left = max(1, (subscription.end_date - now).days) + days_left = max(1, math.ceil((subscription.end_date - now).total_seconds() / 86400)) period_hint_days = days_left devices_discount_percent = PricingEngine.get_addon_discount_percent( @@ -572,7 +573,7 @@ async def execute_change_devices( chargeable_devices = devices_difference devices_price_per_month = chargeable_devices * price_per_device - days_left = max(1, (subscription.end_date - datetime.now(UTC)).days) + days_left = max(1, math.ceil((subscription.end_date - datetime.now(UTC)).total_seconds() / 86400)) devices_discount_percent = PricingEngine.get_addon_discount_percent( db_user, 'devices', @@ -601,7 +602,7 @@ async def execute_change_devices( ) return - charged_days = max(1, (subscription.end_date - datetime.now(UTC)).days) + charged_days = max(1, math.ceil((subscription.end_date - datetime.now(UTC)).total_seconds() / 86400)) await create_transaction( db=db, user_id=db_user.id, @@ -1253,7 +1254,7 @@ async def confirm_add_devices(callback: types.CallbackQuery, db_user: User, db: if is_daily_tariff: # Для суточных тарифов считаем по дням (как в кабинете) now = datetime.now(UTC) - days_left = max(1, (subscription.end_date - now).days) + days_left = max(1, math.ceil((subscription.end_date - now).total_seconds() / 86400)) period_hint_days = days_left devices_discount_percent = PricingEngine.get_addon_discount_percent( @@ -1274,7 +1275,7 @@ async def confirm_add_devices(callback: types.CallbackQuery, db_user: User, db: else: # Для обычных тарифов - по дням (как в кабинете) now = datetime.now(UTC) - days_left = max(1, (subscription.end_date - now).days) + days_left = max(1, math.ceil((subscription.end_date - now).total_seconds() / 86400)) period_hint_days = days_left devices_discount_percent = PricingEngine.get_addon_discount_percent( diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index b7934210..5b6dc0d9 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -1,3 +1,4 @@ +import math from datetime import UTC, datetime import structlog @@ -2169,7 +2170,7 @@ def get_add_traffic_keyboard( # Считаем по дням (как в кабинете и подтверждении) if subscription_end_date: now = datetime.now(UTC) - days_left = max(1, (subscription_end_date - now).days) + days_left = max(1, math.ceil((subscription_end_date - now).total_seconds() / 86400)) price_multiplier = days_left / 30 period_text = f' (за {days_left} дн.)' if days_left > 1 else ' (за 1 день)' else: @@ -2311,7 +2312,7 @@ def get_change_devices_keyboard( # Считаем по дням (как в кабинете и подтверждении) if subscription_end_date: now = datetime.now(UTC) - days_left = max(1, (subscription_end_date - now).days) + days_left = max(1, math.ceil((subscription_end_date - now).total_seconds() / 86400)) price_multiplier = days_left / 30 period_text = f' (за {days_left} дн.)' if days_left > 1 else ' (за 1 день)' else: @@ -2473,7 +2474,7 @@ def get_manage_countries_keyboard( # Считаем по дням (как в кабинете и подтверждении) if subscription_end_date: now = datetime.now(UTC) - days_left = max(1, (subscription_end_date - now).days) + days_left = max(1, math.ceil((subscription_end_date - now).total_seconds() / 86400)) price_multiplier = days_left / 30 logger.info( '🔍 Расчет для управления странами: осталось дней до', diff --git a/app/utils/pricing_utils.py b/app/utils/pricing_utils.py index 9282e5da..5505d67b 100644 --- a/app/utils/pricing_utils.py +++ b/app/utils/pricing_utils.py @@ -1,3 +1,4 @@ +import math from collections.abc import Sequence from datetime import UTC, datetime from typing import TYPE_CHECKING, Any, Optional @@ -26,7 +27,7 @@ def calculate_prorated_price(monthly_price: int, end_date: datetime, min_charge_ tuple of (total_price_kopeks, days_charged) """ now = datetime.now(UTC) - days_remaining = max(1, (end_date - now).days) + days_remaining = max(1, math.ceil((end_date - now).total_seconds() / 86400)) days_to_charge = max(min_charge_days, days_remaining) total_price = monthly_price * days_to_charge // 30