fix: address 6-agent review findings for PricingEngine
H1: log error when tariff_id set but tariff relationship not loaded H2: warn on CLASSIC_PERIOD_PRICES→PERIOD_PRICES fallback M1: fix float division in miniapp tariff purchase (use PricingEngine.apply_discount) M2: fix format_period Russian pluralization for teen-hundreds (111-119, etc.) M3: deduplicate _resolve_discount_percent — import from pricing_utils M4: fix N+1 queries in compute_simple_subscription_price (batch fetch) M5: add period_days validation tests (negative, zero, float) M6: add user=None tests for tariff and classic modes M7: fix float division in calculate_prorated_price (use // instead of /) L1: add context to _calculate_servers_price error log L2: add comment clarifying ClassicBreakdown.group_discount_pct type L3: add test for original_total property L4: inline _apply_percentage_discount wrapper in subscription_purchase_service L5: replace global _server_id_counter with itertools.count() in tests
This commit is contained in:
@@ -42,6 +42,7 @@ class ClassicBreakdown:
|
|||||||
base_traffic_gb: int
|
base_traffic_gb: int
|
||||||
purchased_traffic_gb: int
|
purchased_traffic_gb: int
|
||||||
extra_devices: int
|
extra_devices: int
|
||||||
|
# NB: dict[str, int] per-category (period/servers/traffic/devices), unlike TariffBreakdown's single int
|
||||||
group_discount_pct: dict[str, int]
|
group_discount_pct: dict[str, int]
|
||||||
offer_discount_pct: int
|
offer_discount_pct: int
|
||||||
|
|
||||||
@@ -110,8 +111,8 @@ class PricingEngine:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
servers = await get_server_squads_by_uuids(db, country_uuids)
|
servers = await get_server_squads_by_uuids(db, country_uuids)
|
||||||
except Exception as e:
|
except Exception as e: # intentional broad catch: pricing must not crash on DB errors, servers_price=0 is safe (user pays less)
|
||||||
logger.error('Ошибка пакетной загрузки серверов', error=str(e))
|
logger.error('Ошибка пакетной загрузки серверов', error=str(e), squad_uuids=country_uuids)
|
||||||
return 0, [{'uuid': uuid, 'id': None, 'price': 0, 'status': 'error'} for uuid in country_uuids]
|
return 0, [{'uuid': uuid, 'id': None, 'price': 0, 'status': 'error'} for uuid in country_uuids]
|
||||||
|
|
||||||
server_map = {s.squad_uuid: s for s in servers}
|
server_map = {s.squad_uuid: s for s in servers}
|
||||||
@@ -196,8 +197,15 @@ class PricingEngine:
|
|||||||
if not isinstance(period_days, int) or period_days <= 0:
|
if not isinstance(period_days, int) or period_days <= 0:
|
||||||
raise ValueError(f'Invalid period_days: {period_days}')
|
raise ValueError(f'Invalid period_days: {period_days}')
|
||||||
|
|
||||||
if subscription.tariff_id is not None and subscription.tariff is not None:
|
if subscription.tariff_id is not None:
|
||||||
return await self._calculate_tariff_mode(db, subscription, period_days, user=user)
|
if subscription.tariff is None:
|
||||||
|
logger.error(
|
||||||
|
'tariff_id set but tariff relationship not loaded, falling back to classic mode',
|
||||||
|
subscription_id=getattr(subscription, 'id', None),
|
||||||
|
tariff_id=subscription.tariff_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return await self._calculate_tariff_mode(db, subscription, period_days, user=user)
|
||||||
return await self._calculate_classic_mode(db, subscription, period_days, user=user)
|
return await self._calculate_classic_mode(db, subscription, period_days, user=user)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -297,6 +305,12 @@ class PricingEngine:
|
|||||||
base_price_original = CLASSIC_PERIOD_PRICES.get(period_days)
|
base_price_original = CLASSIC_PERIOD_PRICES.get(period_days)
|
||||||
if base_price_original is None:
|
if base_price_original is None:
|
||||||
base_price_original = PERIOD_PRICES.get(period_days, 0)
|
base_price_original = PERIOD_PRICES.get(period_days, 0)
|
||||||
|
if base_price_original > 0:
|
||||||
|
logger.warning(
|
||||||
|
'CLASSIC_PERIOD_PRICES miss, falling back to PERIOD_PRICES — verify price is not from tariff regime',
|
||||||
|
period_days=period_days,
|
||||||
|
fallback_price_kopeks=base_price_original,
|
||||||
|
)
|
||||||
|
|
||||||
# --- Per-category discount percents ---
|
# --- Per-category discount percents ---
|
||||||
period_pct = 0
|
period_pct = 0
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from app.database.models import ServerSquad, Subscription, SubscriptionStatus, T
|
|||||||
from app.localization.texts import get_texts
|
from app.localization.texts import get_texts
|
||||||
from app.services.subscription_service import SubscriptionService
|
from app.services.subscription_service import SubscriptionService
|
||||||
from app.utils.pricing_utils import (
|
from app.utils.pricing_utils import (
|
||||||
|
apply_percentage_discount,
|
||||||
calculate_months_from_days,
|
calculate_months_from_days,
|
||||||
format_period_description,
|
format_period_description,
|
||||||
validate_pricing_calculation,
|
validate_pricing_calculation,
|
||||||
@@ -266,15 +267,9 @@ class PurchaseBalanceError(Exception):
|
|||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
|
|
||||||
|
|
||||||
def _apply_percentage_discount(amount: int, percent: int) -> tuple[int, int]:
|
|
||||||
"""Delegate to shared apply_percentage_discount (uses PricingEngine internally)."""
|
|
||||||
from app.utils.pricing_utils import apply_percentage_discount
|
|
||||||
|
|
||||||
return apply_percentage_discount(amount, percent)
|
|
||||||
|
|
||||||
|
|
||||||
def _apply_discount_to_monthly_component(amount_per_month: int, percent: int, months: int) -> dict[str, int]:
|
def _apply_discount_to_monthly_component(amount_per_month: int, percent: int, months: int) -> dict[str, int]:
|
||||||
discounted_per_month, discount_per_month = _apply_percentage_discount(amount_per_month, percent)
|
discounted_per_month, discount_per_month = apply_percentage_discount(amount_per_month, percent)
|
||||||
return {
|
return {
|
||||||
'original_per_month': amount_per_month,
|
'original_per_month': amount_per_month,
|
||||||
'discounted_per_month': discounted_per_month,
|
'discounted_per_month': discounted_per_month,
|
||||||
@@ -293,7 +288,7 @@ def _apply_promo_offer_discount(user: User | None, amount: int) -> tuple[int, in
|
|||||||
percent = _get_promo_offer_discount_percent(user)
|
percent = _get_promo_offer_discount_percent(user)
|
||||||
if amount <= 0 or percent <= 0:
|
if amount <= 0 or percent <= 0:
|
||||||
return amount, 0, 0
|
return amount, 0, 0
|
||||||
discounted, discount_value = _apply_percentage_discount(amount, percent)
|
discounted, discount_value = apply_percentage_discount(amount, percent)
|
||||||
return discounted, discount_value, percent
|
return discounted, discount_value, percent
|
||||||
|
|
||||||
|
|
||||||
@@ -303,7 +298,7 @@ def _build_server_option(
|
|||||||
texts,
|
texts,
|
||||||
) -> PurchaseServerOption:
|
) -> PurchaseServerOption:
|
||||||
base_per_month = int(getattr(server, 'price_kopeks', 0) or 0)
|
base_per_month = int(getattr(server, 'price_kopeks', 0) or 0)
|
||||||
discounted_per_month, _ = _apply_percentage_discount(base_per_month, discount_percent)
|
discounted_per_month, _ = apply_percentage_discount(base_per_month, discount_percent)
|
||||||
return PurchaseServerOption(
|
return PurchaseServerOption(
|
||||||
uuid=server.squad_uuid,
|
uuid=server.squad_uuid,
|
||||||
name=getattr(server, 'display_name', server.squad_uuid) or server.squad_uuid,
|
name=getattr(server, 'display_name', server.squad_uuid) or server.squad_uuid,
|
||||||
@@ -387,7 +382,7 @@ class MiniAppSubscriptionPurchaseService:
|
|||||||
|
|
||||||
base_price_original = PERIOD_PRICES.get(period_days, 0)
|
base_price_original = PERIOD_PRICES.get(period_days, 0)
|
||||||
period_discount_percent = user.get_promo_discount('period', period_days)
|
period_discount_percent = user.get_promo_discount('period', period_days)
|
||||||
base_price, base_discount_total = _apply_percentage_discount(base_price_original, period_discount_percent)
|
base_price, base_discount_total = apply_percentage_discount(base_price_original, period_discount_percent)
|
||||||
base_price_label = texts.format_price(base_price)
|
base_price_label = texts.format_price(base_price)
|
||||||
base_price_original_label = (
|
base_price_original_label = (
|
||||||
texts.format_price(base_price_original)
|
texts.format_price(base_price_original)
|
||||||
@@ -520,7 +515,7 @@ class MiniAppSubscriptionPurchaseService:
|
|||||||
for package in packages:
|
for package in packages:
|
||||||
value = int(package.get('gb') or 0)
|
value = int(package.get('gb') or 0)
|
||||||
price_per_month = int(package.get('price') or 0)
|
price_per_month = int(package.get('price') or 0)
|
||||||
discounted_per_month, discount_value = _apply_percentage_discount(price_per_month, discount_percent)
|
discounted_per_month, discount_value = apply_percentage_discount(price_per_month, discount_percent)
|
||||||
label = texts.format_traffic(value or 0)
|
label = texts.format_traffic(value or 0)
|
||||||
options.append(
|
options.append(
|
||||||
PurchaseTrafficOption(
|
PurchaseTrafficOption(
|
||||||
@@ -594,7 +589,7 @@ class MiniAppSubscriptionPurchaseService:
|
|||||||
) -> PurchaseDevicesConfig:
|
) -> PurchaseDevicesConfig:
|
||||||
discount_percent = user.get_promo_discount('devices', period_days)
|
discount_percent = user.get_promo_discount('devices', period_days)
|
||||||
unit_price = settings.PRICE_PER_DEVICE
|
unit_price = settings.PRICE_PER_DEVICE
|
||||||
discounted_unit_price, unit_discount_value = _apply_percentage_discount(unit_price, discount_percent)
|
discounted_unit_price, unit_discount_value = apply_percentage_discount(unit_price, discount_percent)
|
||||||
price_label = texts.format_price(discounted_unit_price)
|
price_label = texts.format_price(discounted_unit_price)
|
||||||
original_label = (
|
original_label = (
|
||||||
texts.format_price(unit_price) if unit_discount_value and unit_price != discounted_unit_price else None
|
texts.format_price(unit_price) if unit_discount_value and unit_price != discounted_unit_price else None
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from app.database.models import PromoGroup, Subscription, SubscriptionStatus, Us
|
|||||||
from app.external.remnawave_api import RemnaWaveAPI, RemnaWaveAPIError, RemnaWaveUser, TrafficLimitStrategy, UserStatus
|
from app.external.remnawave_api import RemnaWaveAPI, RemnaWaveAPIError, RemnaWaveUser, TrafficLimitStrategy, UserStatus
|
||||||
from app.utils.pricing_utils import (
|
from app.utils.pricing_utils import (
|
||||||
calculate_months_from_days,
|
calculate_months_from_days,
|
||||||
|
resolve_discount_percent,
|
||||||
)
|
)
|
||||||
from app.utils.subscription_utils import (
|
from app.utils.subscription_utils import (
|
||||||
resolve_hwid_device_limit_for_payload,
|
resolve_hwid_device_limit_for_payload,
|
||||||
@@ -23,24 +24,6 @@ from app.utils.subscription_utils import (
|
|||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _resolve_discount_percent(
|
|
||||||
user: User | None,
|
|
||||||
promo_group: PromoGroup | None,
|
|
||||||
category: str,
|
|
||||||
*,
|
|
||||||
period_days: int | None = None,
|
|
||||||
) -> int:
|
|
||||||
if user is not None:
|
|
||||||
try:
|
|
||||||
return user.get_promo_discount(category, period_days)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if promo_group is not None:
|
|
||||||
return promo_group.get_discount_percent(category, period_days)
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def get_traffic_reset_strategy(tariff=None):
|
def get_traffic_reset_strategy(tariff=None):
|
||||||
"""Получает стратегию сброса трафика.
|
"""Получает стратегию сброса трафика.
|
||||||
@@ -813,7 +796,7 @@ class SubscriptionService:
|
|||||||
months_in_period = calculate_months_from_days(period_days)
|
months_in_period = calculate_months_from_days(period_days)
|
||||||
|
|
||||||
base_price_original = PERIOD_PRICES.get(period_days, 0)
|
base_price_original = PERIOD_PRICES.get(period_days, 0)
|
||||||
period_discount_percent = _resolve_discount_percent(
|
period_discount_percent = resolve_discount_percent(
|
||||||
user,
|
user,
|
||||||
promo_group,
|
promo_group,
|
||||||
'period',
|
'period',
|
||||||
@@ -825,7 +808,7 @@ class SubscriptionService:
|
|||||||
promo_group = promo_group or (user.get_primary_promo_group() if user else None)
|
promo_group = promo_group or (user.get_primary_promo_group() if user else None)
|
||||||
|
|
||||||
traffic_price_per_month = settings.get_traffic_price(traffic_gb)
|
traffic_price_per_month = settings.get_traffic_price(traffic_gb)
|
||||||
traffic_discount_percent = _resolve_discount_percent(
|
traffic_discount_percent = resolve_discount_percent(
|
||||||
user,
|
user,
|
||||||
promo_group,
|
promo_group,
|
||||||
'traffic',
|
'traffic',
|
||||||
@@ -837,7 +820,7 @@ class SubscriptionService:
|
|||||||
|
|
||||||
server_prices = []
|
server_prices = []
|
||||||
total_servers_price = 0
|
total_servers_price = 0
|
||||||
servers_discount_percent = _resolve_discount_percent(
|
servers_discount_percent = resolve_discount_percent(
|
||||||
user,
|
user,
|
||||||
promo_group,
|
promo_group,
|
||||||
'servers',
|
'servers',
|
||||||
@@ -865,7 +848,7 @@ class SubscriptionService:
|
|||||||
|
|
||||||
additional_devices = max(0, devices - settings.DEFAULT_DEVICE_LIMIT)
|
additional_devices = max(0, devices - settings.DEFAULT_DEVICE_LIMIT)
|
||||||
devices_price_per_month = additional_devices * settings.PRICE_PER_DEVICE
|
devices_price_per_month = additional_devices * settings.PRICE_PER_DEVICE
|
||||||
devices_discount_percent = _resolve_discount_percent(
|
devices_discount_percent = resolve_discount_percent(
|
||||||
user,
|
user,
|
||||||
promo_group,
|
promo_group,
|
||||||
'devices',
|
'devices',
|
||||||
|
|||||||
+11
-9
@@ -21,12 +21,14 @@ def format_price_kopeks(kopeks: int, compact: bool = False) -> str:
|
|||||||
|
|
||||||
def format_period(days: int) -> str:
|
def format_period(days: int) -> str:
|
||||||
"""Форматирует период."""
|
"""Форматирует период."""
|
||||||
if days == 1:
|
mod100 = days % 100
|
||||||
return '1 день'
|
mod10 = days % 10
|
||||||
if days < 5:
|
if 11 <= mod100 <= 19:
|
||||||
return f'{days} дня'
|
word = 'дней'
|
||||||
if days < 21 or days % 10 >= 5 or days % 10 == 0:
|
elif mod10 == 1:
|
||||||
return f'{days} дней'
|
word = 'день'
|
||||||
if days % 10 == 1:
|
elif 2 <= mod10 <= 4:
|
||||||
return f'{days} день'
|
word = 'дня'
|
||||||
return f'{days} дня'
|
else:
|
||||||
|
word = 'дней'
|
||||||
|
return f'{days} {word}'
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def calculate_prorated_price(monthly_price: int, end_date: datetime, min_charge_
|
|||||||
days_remaining = max(1, (end_date - now).days)
|
days_remaining = max(1, (end_date - now).days)
|
||||||
days_to_charge = max(min_charge_days, days_remaining)
|
days_to_charge = max(min_charge_days, days_remaining)
|
||||||
|
|
||||||
total_price = int(monthly_price * days_to_charge / 30)
|
total_price = monthly_price * days_to_charge // 30
|
||||||
if monthly_price > 0:
|
if monthly_price > 0:
|
||||||
total_price = max(100, total_price) # Минимум 1 рубль
|
total_price = max(100, total_price) # Минимум 1 рубль
|
||||||
|
|
||||||
@@ -163,14 +163,20 @@ async def compute_simple_subscription_price(
|
|||||||
elif raw_squad:
|
elif raw_squad:
|
||||||
resolved_uuids.append(str(raw_squad))
|
resolved_uuids.append(str(raw_squad))
|
||||||
|
|
||||||
from app.database.crud.server_squad import get_server_squad_by_uuid
|
from app.database.crud.server_squad import get_server_squads_by_uuids
|
||||||
|
|
||||||
server_breakdown: list[dict[str, Any]] = []
|
server_breakdown: list[dict[str, Any]] = []
|
||||||
servers_price_original = 0
|
servers_price_original = 0
|
||||||
servers_discount_total = 0
|
servers_discount_total = 0
|
||||||
|
|
||||||
|
if resolved_uuids:
|
||||||
|
servers = await get_server_squads_by_uuids(db, resolved_uuids)
|
||||||
|
server_map = {s.squad_uuid: s for s in servers}
|
||||||
|
else:
|
||||||
|
server_map = {}
|
||||||
|
|
||||||
for squad_uuid in resolved_uuids:
|
for squad_uuid in resolved_uuids:
|
||||||
server = await get_server_squad_by_uuid(db, squad_uuid)
|
server = server_map.get(squad_uuid)
|
||||||
if not server:
|
if not server:
|
||||||
logger.warning('SIMPLE_SUBSCRIPTION_PRICE_SERVER_NOT_FOUND | squad', squad_uuid=squad_uuid)
|
logger.warning('SIMPLE_SUBSCRIPTION_PRICE_SERVER_NOT_FOUND | squad', squad_uuid=squad_uuid)
|
||||||
server_breakdown.append(
|
server_breakdown.append(
|
||||||
|
|||||||
@@ -6463,7 +6463,9 @@ async def purchase_tariff_endpoint(
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
if discount_percent > 0:
|
if discount_percent > 0:
|
||||||
price_kopeks = int(base_price_kopeks * (100 - discount_percent) / 100)
|
from app.services.pricing_engine import PricingEngine
|
||||||
|
|
||||||
|
price_kopeks = PricingEngine.apply_discount(base_price_kopeks, discount_percent)
|
||||||
|
|
||||||
# Apply personal promo_offer discount on top of group discount
|
# Apply personal promo_offer discount on top of group discount
|
||||||
consume_promo_offer = False
|
consume_promo_offer = False
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import itertools
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.services.pricing_engine import PricingEngine, RenewalPricing
|
from app.services.pricing_engine import PricingEngine, RenewalPricing
|
||||||
@@ -75,16 +77,46 @@ class TestStackedDiscounts:
|
|||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
|
||||||
_server_id_counter = 0
|
class TestPeriodDaysValidation:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_negative_period_days_raises(self):
|
||||||
|
engine = PricingEngine()
|
||||||
|
db = AsyncMock()
|
||||||
|
subscription = MagicMock()
|
||||||
|
subscription.tariff_id = None
|
||||||
|
subscription.tariff = None
|
||||||
|
with pytest.raises(ValueError, match='Invalid period_days'):
|
||||||
|
await engine.calculate_renewal_price(db, subscription, -1)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_zero_period_days_raises(self):
|
||||||
|
engine = PricingEngine()
|
||||||
|
db = AsyncMock()
|
||||||
|
subscription = MagicMock()
|
||||||
|
subscription.tariff_id = None
|
||||||
|
subscription.tariff = None
|
||||||
|
with pytest.raises(ValueError, match='Invalid period_days'):
|
||||||
|
await engine.calculate_renewal_price(db, subscription, 0)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_float_period_days_raises(self):
|
||||||
|
engine = PricingEngine()
|
||||||
|
db = AsyncMock()
|
||||||
|
subscription = MagicMock()
|
||||||
|
subscription.tariff_id = None
|
||||||
|
subscription.tariff = None
|
||||||
|
with pytest.raises(ValueError, match='Invalid period_days'):
|
||||||
|
await engine.calculate_renewal_price(db, subscription, 30.0)
|
||||||
|
|
||||||
|
|
||||||
|
_server_id_seq = itertools.count(1)
|
||||||
|
|
||||||
|
|
||||||
def _make_server(
|
def _make_server(
|
||||||
price_kopeks=5000, is_available=True, is_full=False, allowed_promo_groups=None, server_id=None, squad_uuid=None
|
price_kopeks=5000, is_available=True, is_full=False, allowed_promo_groups=None, server_id=None, squad_uuid=None
|
||||||
):
|
):
|
||||||
global _server_id_counter
|
|
||||||
if server_id is None:
|
if server_id is None:
|
||||||
_server_id_counter += 1
|
server_id = next(_server_id_seq)
|
||||||
server_id = _server_id_counter
|
|
||||||
server = MagicMock()
|
server = MagicMock()
|
||||||
server.id = server_id
|
server.id = server_id
|
||||||
server.squad_uuid = squad_uuid
|
server.squad_uuid = squad_uuid
|
||||||
@@ -371,6 +403,29 @@ class TestCalculateRenewalPriceTariffMode:
|
|||||||
assert result.final_total == 10000
|
assert result.final_total == 10000
|
||||||
assert result.breakdown.get('extra_devices') == 0
|
assert result.breakdown.get('extra_devices') == 0
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_tariff_user_none(self):
|
||||||
|
"""When user=None, no discounts are applied."""
|
||||||
|
engine = PricingEngine()
|
||||||
|
db = AsyncMock()
|
||||||
|
subscription = MagicMock()
|
||||||
|
subscription.tariff_id = 1
|
||||||
|
subscription.tariff = MagicMock()
|
||||||
|
subscription.tariff.period_prices = {'30': 20000}
|
||||||
|
subscription.tariff.device_limit = 1
|
||||||
|
subscription.tariff.device_price_kopeks = None
|
||||||
|
subscription.tariff.id = 1
|
||||||
|
subscription.device_limit = 1
|
||||||
|
with (
|
||||||
|
patch('app.services.pricing_engine.get_user_active_promo_discount_percent', return_value=0),
|
||||||
|
patch('app.services.pricing_engine.settings') as ms,
|
||||||
|
):
|
||||||
|
ms.PRICE_PER_DEVICE = 5000
|
||||||
|
result = await engine.calculate_renewal_price(db, subscription, 30, user=None)
|
||||||
|
assert result.final_total == 20000
|
||||||
|
assert result.promo_group_discount == 0
|
||||||
|
assert result.promo_offer_discount == 0
|
||||||
|
|
||||||
|
|
||||||
class TestCalculateRenewalPriceClassicMode:
|
class TestCalculateRenewalPriceClassicMode:
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -693,6 +748,33 @@ class TestCalculateRenewalPriceClassicMode:
|
|||||||
assert result.promo_group_discount == 3700
|
assert result.promo_group_discount == 3700
|
||||||
assert result.final_total == 9000 + 4800 + 3500 + 8000
|
assert result.final_total == 9000 + 4800 + 3500 + 8000
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_classic_user_none(self):
|
||||||
|
"""When user=None, no discounts are applied."""
|
||||||
|
engine = PricingEngine()
|
||||||
|
db = AsyncMock()
|
||||||
|
subscription = MagicMock()
|
||||||
|
subscription.tariff_id = None
|
||||||
|
subscription.tariff = None
|
||||||
|
subscription.connected_squads = []
|
||||||
|
subscription.traffic_limit_gb = 0
|
||||||
|
subscription.purchased_traffic_gb = 0
|
||||||
|
subscription.device_limit = 1
|
||||||
|
with (
|
||||||
|
patch('app.services.pricing_engine.get_user_active_promo_discount_percent', return_value=0),
|
||||||
|
patch('app.services.pricing_engine.settings') as ms,
|
||||||
|
patch('app.services.pricing_engine.CLASSIC_PERIOD_PRICES', {30: 15000}),
|
||||||
|
patch('app.services.pricing_engine.PERIOD_PRICES', {}),
|
||||||
|
):
|
||||||
|
ms.get_traffic_price.return_value = 0
|
||||||
|
ms.PRICE_PER_DEVICE = 0
|
||||||
|
ms.DEFAULT_DEVICE_LIMIT = 1
|
||||||
|
ms.is_traffic_fixed.return_value = False
|
||||||
|
result = await engine.calculate_renewal_price(db, subscription, 30, user=None)
|
||||||
|
assert result.final_total == 15000
|
||||||
|
assert result.promo_group_discount == 0
|
||||||
|
assert result.promo_offer_discount == 0
|
||||||
|
|
||||||
|
|
||||||
class TestServerPromoGroupFiltering:
|
class TestServerPromoGroupFiltering:
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -864,3 +946,25 @@ class TestOriginalPriceIdentity:
|
|||||||
# original should equal base_original + servers_original + traffic_original + devices_original
|
# original should equal base_original + servers_original + traffic_original + devices_original
|
||||||
expected_original = 10000 + 4000 + 3000 + 5000 # 22000
|
expected_original = 10000 + 4000 + 3000 + 5000 # 22000
|
||||||
assert original == expected_original
|
assert original == expected_original
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_original_total_property_tariff(self):
|
||||||
|
"""original_total property returns correct value."""
|
||||||
|
engine = PricingEngine()
|
||||||
|
db = AsyncMock()
|
||||||
|
tariff = MagicMock()
|
||||||
|
tariff.id = 1
|
||||||
|
tariff.period_prices = {'30': 20000}
|
||||||
|
tariff.device_price_kopeks = None
|
||||||
|
tariff.device_limit = 1
|
||||||
|
sub = MagicMock()
|
||||||
|
sub.tariff_id = 1
|
||||||
|
sub.tariff = tariff
|
||||||
|
sub.device_limit = 1
|
||||||
|
user = MagicMock()
|
||||||
|
promo_group = MagicMock()
|
||||||
|
promo_group.get_discount_percent = MagicMock(return_value=10)
|
||||||
|
user.promo_group = promo_group
|
||||||
|
with patch('app.services.pricing_engine.get_user_active_promo_discount_percent', return_value=5):
|
||||||
|
result = await engine.calculate_renewal_price(db, sub, 30, user=user)
|
||||||
|
assert result.original_total == 20000 # undiscounted subtotal
|
||||||
|
|||||||
Reference in New Issue
Block a user