feat(promo): Добавлены промокоды на скидку в процентах

This commit is contained in:
VAQYBIN
2026-01-19 03:50:33 +05:00
parent 0701af0f35
commit 3bf9acf4d4
14 changed files with 682 additions and 62 deletions
+151 -1
View File
@@ -6,7 +6,7 @@ from aiogram import Bot
from config.settings import Settings
from db.dal import promo_code_dal, user_dal
from db.dal import promo_code_dal, user_dal, active_discount_dal
from db.models import PromoCode, User
from .subscription_service import SubscriptionService
@@ -83,3 +83,153 @@ class PromoCodeService:
return False, _("error_applying_promo_bonus")
else:
return False, _("error_applying_promo_bonus")
async def apply_discount_promo_code(
self,
session: AsyncSession,
user_id: int,
code_input: str,
user_lang: str,
) -> Tuple[bool, int | str]:
"""
Apply a discount promo code (sets active discount for user).
Returns: (success: bool, discount_percentage or error_message)
"""
_ = lambda k, **kw: self.i18n.gettext(user_lang, k, **kw)
code_input_upper = code_input.strip().upper()
# Check if user already has an active discount
existing_discount = await active_discount_dal.get_active_discount(session, user_id)
if existing_discount:
# Get the promo code for the existing discount
existing_promo = await promo_code_dal.get_promo_code_by_id(
session, existing_discount.promo_code_id
)
if existing_promo:
return False, _("discount_promo_already_active",
code=existing_promo.code,
discount_pct=existing_discount.discount_percentage)
else:
# Existing discount but promo not found - clear it and continue
await active_discount_dal.clear_active_discount(session, user_id)
# Get discount promo code
promo_data = await promo_code_dal.get_active_discount_promo_code_by_code_str(
session, code_input_upper
)
if not promo_data:
return False, _("promo_code_not_found_or_not_discount", code=code_input_upper)
# Check if user already used this code
existing_activation = await promo_code_dal.get_user_activation_for_promo(
session, promo_data.promo_code_id, user_id
)
if existing_activation:
return False, _("promo_code_already_used_by_user", code=code_input_upper)
# Set active discount
active_discount = await active_discount_dal.set_active_discount(
session,
user_id=user_id,
promo_code_id=promo_data.promo_code_id,
discount_percentage=promo_data.discount_percentage
)
if not active_discount:
# This shouldn't happen since we checked above, but just in case
return False, _("error_applying_promo_discount")
logging.info(
f"Discount promo code {code_input_upper} activated for user {user_id}: "
f"{promo_data.discount_percentage}% off"
)
return True, promo_data.discount_percentage
async def get_user_active_discount(
self,
session: AsyncSession,
user_id: int
) -> Optional[Tuple[int, str]]:
"""
Get user's active discount if any.
Returns: (discount_percentage, promo_code) or None
"""
active_discount = await active_discount_dal.get_active_discount(session, user_id)
if not active_discount:
return None
# Fetch promo code for code string
promo = await promo_code_dal.get_promo_code_by_id(
session, active_discount.promo_code_id
)
if not promo:
# Discount exists but promo not found - clear it
await active_discount_dal.clear_active_discount(session, user_id)
return None
return (active_discount.discount_percentage, promo.code)
def calculate_discounted_price(
self,
original_price: float,
discount_percentage: int
) -> Tuple[float, float]:
"""
Calculate discounted price and discount amount.
Returns: (final_price, discount_amount)
"""
discount_amount = round(original_price * (discount_percentage / 100), 2)
final_price = round(original_price - discount_amount, 2)
# Ensure price doesn't go negative
if final_price < 0:
final_price = 0
discount_amount = original_price
return final_price, discount_amount
async def consume_discount(
self,
session: AsyncSession,
user_id: int,
payment_id: int
) -> bool:
"""
Consume active discount: record activation, increment usage, clear active discount.
Call this AFTER successful payment.
"""
active_discount = await active_discount_dal.get_active_discount(session, user_id)
if not active_discount:
return False
# Record activation
activation_recorded = await promo_code_dal.record_promo_activation(
session,
active_discount.promo_code_id,
user_id,
payment_id=payment_id
)
# Increment usage
promo_incremented = await promo_code_dal.increment_promo_code_usage(
session,
active_discount.promo_code_id
)
# Clear active discount
await active_discount_dal.clear_active_discount(session, user_id)
if activation_recorded and promo_incremented:
await session.flush()
logging.info(
f"Discount consumed for user {user_id}, promo {active_discount.promo_code_id}, "
f"payment {payment_id}"
)
return True
else:
logging.error(
f"Failed to consume discount for user {user_id}, "
f"promo {active_discount.promo_code_id}"
)
return False
+30 -1
View File
@@ -5,7 +5,7 @@ from typing import Optional, Dict, Any, List, Tuple
from aiogram import Bot
from bot.middlewares.i18n import JsonI18n
from db.dal import user_dal, subscription_dal, promo_code_dal, payment_dal, user_billing_dal
from db.dal import user_dal, subscription_dal, promo_code_dal, payment_dal, user_billing_dal, active_discount_dal
from bot.utils.date_utils import add_months
from bot.utils.config_link import prepare_config_links
from db.models import User, Subscription
@@ -691,6 +691,35 @@ class SubscriptionService:
final_subscription_url = updated_panel_user.get("subscriptionUrl")
final_panel_short_uuid = updated_panel_user.get("shortUuid", panel_short_uuid)
# NEW: Consume discount promo code if payment had one
try:
payment_record = await payment_dal.get_payment_by_db_id(session, payment_db_id)
if payment_record and payment_record.discount_applied:
# This payment had a discount applied - consume it
active_discount = await active_discount_dal.get_active_discount(session, user_id)
if active_discount:
# Record promo activation
await promo_code_dal.record_promo_activation(
session,
active_discount.promo_code_id,
user_id,
payment_id=payment_db_id
)
# Increment usage
await promo_code_dal.increment_promo_code_usage(
session,
active_discount.promo_code_id
)
# Clear active discount
await active_discount_dal.clear_active_discount(session, user_id)
logging.info(
f"Discount consumed for user {user_id}, promo {active_discount.promo_code_id}, "
f"payment {payment_db_id}"
)
except Exception as e:
logging.error(f"Failed to consume discount for user {user_id}, payment {payment_db_id}: {e}")
# Don't fail the subscription activation if discount consumption fails
return {
"subscription_id": new_or_updated_sub.subscription_id,
"end_date": final_end_date,