fix: device/traffic addon pricing — use ceil instead of floor for days_left
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)
This commit is contained in:
@@ -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
|
||||
|
||||
# Устройства в пределах тарифного лимита — бесплатные
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
'🔍 Расчет для управления странами: осталось дней до',
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user