feat(promo): Добавлены промокоды на скидку в процентах
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user