fix: remaining pricing-critical .days floor calculations → math.ceil
Same bug as device pricing: timedelta.days floors partial days. Fixed 14 more pricing-critical locations across 7 files: - traffic addon pricing (bot handler + cabinet + miniapp) - country addon pricing (bot handler + miniapp) - generic addon pricing helper (common.py) - auto-purchase device recomputation - subscription CRUD pricing helper Display-only .days usages intentionally left as floor (correct for showing "X days left" to users).
This commit is contained in:
@@ -9,6 +9,7 @@ POST /subscription/traffic/save-cart
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
@@ -478,7 +479,7 @@ async def save_traffic_cart(
|
||||
from app.utils.pricing_utils import calculate_prorated_price as _calc_prorated
|
||||
|
||||
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))
|
||||
prorated_price, _ = _calc_prorated(
|
||||
base_price_kopeks,
|
||||
subscription.end_date,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import math
|
||||
import secrets
|
||||
from collections.abc import Iterable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
@@ -1296,7 +1297,7 @@ async def add_subscription_servers(
|
||||
|
||||
if paid_prices is None:
|
||||
now = datetime.now(UTC)
|
||||
days_remaining = max(1, (subscription.end_date - now).days)
|
||||
days_remaining = max(1, math.ceil((subscription.end_date - now).total_seconds() / 86400))
|
||||
paid_prices = []
|
||||
|
||||
from app.database.models import ServerSquad
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import html as html_mod
|
||||
import math
|
||||
import re
|
||||
import time
|
||||
from datetime import UTC, datetime
|
||||
@@ -545,7 +546,7 @@ def get_traffic_switch_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:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import html
|
||||
import math
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from aiogram import types
|
||||
@@ -266,7 +267,7 @@ async def apply_countries_changes(callback: types.CallbackQuery, db_user: User,
|
||||
logger.info('🔧 Добавлено: Удалено', added=added, removed=removed)
|
||||
|
||||
now = datetime.now(UTC)
|
||||
days_to_pay = max(1, (subscription.end_date - now).days)
|
||||
days_to_pay = max(1, math.ceil((subscription.end_date - now).total_seconds() / 86400))
|
||||
|
||||
period_hint_days = days_to_pay if days_to_pay > 0 else None
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import math
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from aiogram import types
|
||||
@@ -807,7 +808,7 @@ async def confirm_switch_traffic(
|
||||
new_price_per_month = settings.get_traffic_price(new_traffic_gb)
|
||||
|
||||
now = datetime.now(UTC)
|
||||
days_remaining = max(1, (subscription.end_date - now).days)
|
||||
days_remaining = max(1, math.ceil((subscription.end_date - now).total_seconds() / 86400))
|
||||
period_hint_days = days_remaining if days_remaining > 0 else None
|
||||
traffic_discount_percent = PricingEngine.get_addon_discount_percent(
|
||||
db_user,
|
||||
@@ -911,7 +912,7 @@ async def execute_switch_traffic(
|
||||
base_traffic = current_traffic - purchased_traffic
|
||||
old_price_per_month = settings.get_traffic_price(base_traffic)
|
||||
new_price_per_month = settings.get_traffic_price(new_traffic_gb)
|
||||
days_remaining = max(1, (subscription.end_date - datetime.now(UTC)).days)
|
||||
days_remaining = max(1, math.ceil((subscription.end_date - datetime.now(UTC)).total_seconds() / 86400))
|
||||
traffic_discount_percent = PricingEngine.get_addon_discount_percent(
|
||||
db_user,
|
||||
'traffic',
|
||||
@@ -936,7 +937,7 @@ async def execute_switch_traffic(
|
||||
await callback.answer('⚠️ Ошибка списания средств', show_alert=True)
|
||||
return
|
||||
|
||||
days_remaining = max(1, (subscription.end_date - datetime.now(UTC)).days)
|
||||
days_remaining = max(1, math.ceil((subscription.end_date - datetime.now(UTC)).total_seconds() / 86400))
|
||||
await create_transaction(
|
||||
db=db,
|
||||
user_id=db_user.id,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
@@ -1533,7 +1534,7 @@ async def _auto_add_devices(
|
||||
|
||||
# Recompute price fresh under lock (pricing config may have changed since cart was saved)
|
||||
devices_price_per_month = devices_to_add * tariff_device_price
|
||||
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(
|
||||
user,
|
||||
'devices',
|
||||
|
||||
@@ -5755,7 +5755,7 @@ async def update_subscription_servers_endpoint(
|
||||
subscription.end_date,
|
||||
)
|
||||
else:
|
||||
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))
|
||||
|
||||
added_server_ids = [catalog[uuid].get('server_id') for uuid in added if catalog[uuid].get('server_id') is not None]
|
||||
added_server_prices = [
|
||||
@@ -5933,7 +5933,7 @@ async def update_subscription_traffic_endpoint(
|
||||
},
|
||||
)
|
||||
|
||||
days_remaining = max(1, (subscription.end_date - datetime.now(UTC)).days)
|
||||
days_remaining = max(1, math.ceil((subscription.end_date - datetime.now(UTC)).total_seconds() / 86400))
|
||||
period_hint_days = days_remaining
|
||||
|
||||
# Lock user BEFORE discount computation to prevent TOCTOU on promo group
|
||||
@@ -6111,7 +6111,7 @@ async def update_subscription_devices_endpoint(
|
||||
chargeable_diff = new_chargeable - current_chargeable
|
||||
|
||||
price_per_month = chargeable_diff * tariff_device_price
|
||||
days_remaining = max(1, (subscription.end_date - datetime.now(UTC)).days)
|
||||
days_remaining = max(1, math.ceil((subscription.end_date - datetime.now(UTC)).total_seconds() / 86400))
|
||||
period_hint_days = days_remaining
|
||||
|
||||
# Lock user BEFORE price computation to prevent TOCTOU on promo discount
|
||||
@@ -6159,7 +6159,7 @@ async def update_subscription_devices_endpoint(
|
||||
user_id=user.id,
|
||||
type=TransactionType.SUBSCRIPTION_PAYMENT,
|
||||
amount_kopeks=price_to_charge,
|
||||
description=f'{description} за {charged_days or max(1, (subscription.end_date - datetime.now(UTC)).days)} дн.',
|
||||
description=f'{description} за {charged_days or max(1, math.ceil((subscription.end_date - datetime.now(UTC)).total_seconds() / 86400))} дн.',
|
||||
)
|
||||
|
||||
if price_to_charge > 0:
|
||||
|
||||
Reference in New Issue
Block a user