diff --git a/bot/handlers/user/subscription/payments_crypto.py b/bot/handlers/user/subscription/payments_crypto.py index ed03c8a..dba6338 100644 --- a/bot/handlers/user/subscription/payments_crypto.py +++ b/bot/handlers/user/subscription/payments_crypto.py @@ -18,6 +18,7 @@ async def pay_crypto_callback_handler( i18n_data: dict, session: AsyncSession, cryptopay_service: CryptoPayService, + promo_code_service=None, ): current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") @@ -65,6 +66,7 @@ async def pay_crypto_callback_handler( amount=price_amount, description=payment_description, sale_mode=sale_mode, + promo_code_service=promo_code_service, ) if invoice_url: diff --git a/bot/handlers/user/subscription/payments_freekassa.py b/bot/handlers/user/subscription/payments_freekassa.py index 1671c98..e24bb88 100644 --- a/bot/handlers/user/subscription/payments_freekassa.py +++ b/bot/handlers/user/subscription/payments_freekassa.py @@ -10,6 +10,7 @@ from bot.middlewares.i18n import JsonI18n from bot.services.freekassa_service import FreeKassaService from config.settings import Settings from db.dal import payment_dal +from bot.handlers.user.subscription.payment_discount_helper import apply_discount_to_payment router = Router(name="user_subscription_payments_freekassa_router") @@ -21,6 +22,7 @@ async def pay_fk_callback_handler( i18n_data: dict, freekassa_service: FreeKassaService, session: AsyncSession, + promo_code_service=None, ): current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") @@ -68,14 +70,22 @@ async def pay_fk_callback_handler( ) currency_code = getattr(freekassa_service, "default_currency", None) or settings.DEFAULT_CURRENCY_SYMBOL or "RUB" + # Apply active discount if exists + final_price_rub, discount_amount, promo_code_id = await apply_discount_to_payment( + session, user_id, price_rub, promo_code_service + ) + payment_record_payload = { "user_id": user_id, - "amount": price_rub, + "amount": final_price_rub, + "original_amount": price_rub if discount_amount else None, + "discount_applied": discount_amount, "currency": currency_code, "status": "pending_freekassa", "description": payment_description, "subscription_duration_months": int(months), "provider": "freekassa", + "promo_code_id": promo_code_id, } try: @@ -101,7 +111,7 @@ async def pay_fk_callback_handler( payment_db_id=payment_record.payment_id, user_id=payment_record.user_id, months=months, - amount=price_rub, + amount=final_price_rub, currency=freekassa_service.default_currency, payment_method_id=freekassa_service.payment_method_id, ip_address=freekassa_service.server_ip, diff --git a/bot/handlers/user/subscription/payments_platega.py b/bot/handlers/user/subscription/payments_platega.py index b83e418..7f42ce1 100644 --- a/bot/handlers/user/subscription/payments_platega.py +++ b/bot/handlers/user/subscription/payments_platega.py @@ -10,6 +10,7 @@ from bot.middlewares.i18n import JsonI18n from bot.services.platega_service import PlategaService from config.settings import Settings from db.dal import payment_dal +from bot.handlers.user.subscription.payment_discount_helper import apply_discount_to_payment router = Router(name="user_subscription_payments_platega_router") @@ -21,6 +22,7 @@ async def pay_platega_callback_handler( i18n_data: dict, platega_service: PlategaService, session: AsyncSession, + promo_code_service=None, ): current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") @@ -68,14 +70,22 @@ async def pay_platega_callback_handler( ) currency_code = settings.DEFAULT_CURRENCY_SYMBOL or "RUB" + # Apply active discount if exists + final_price_rub, discount_amount, promo_code_id = await apply_discount_to_payment( + session, user_id, price_rub, promo_code_service + ) + payment_record_payload = { "user_id": user_id, - "amount": price_rub, + "amount": final_price_rub, + "original_amount": price_rub if discount_amount else None, + "discount_applied": discount_amount, "currency": currency_code, "status": "pending_platega", "description": payment_description, "subscription_duration_months": int(months), "provider": "platega", + "promo_code_id": promo_code_id, } try: @@ -110,7 +120,7 @@ async def pay_platega_callback_handler( payment_db_id=payment_record.payment_id, user_id=user_id, months=months, - amount=price_rub, + amount=final_price_rub, currency=currency_code, description=payment_description, payload=payload_meta, diff --git a/bot/handlers/user/subscription/payments_severpay.py b/bot/handlers/user/subscription/payments_severpay.py index 2a3d100..f99431a 100644 --- a/bot/handlers/user/subscription/payments_severpay.py +++ b/bot/handlers/user/subscription/payments_severpay.py @@ -9,6 +9,7 @@ from bot.middlewares.i18n import JsonI18n from bot.services.severpay_service import SeverPayService from config.settings import Settings from db.dal import payment_dal +from bot.handlers.user.subscription.payment_discount_helper import apply_discount_to_payment router = Router(name="user_subscription_payments_severpay_router") @@ -20,6 +21,7 @@ async def pay_severpay_callback_handler( i18n_data: dict, severpay_service: SeverPayService, session: AsyncSession, + promo_code_service=None, ): current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") @@ -67,14 +69,22 @@ async def pay_severpay_callback_handler( ) currency_code = settings.DEFAULT_CURRENCY_SYMBOL or "RUB" + # Apply active discount if exists + final_price_rub, discount_amount, promo_code_id = await apply_discount_to_payment( + session, user_id, price_rub, promo_code_service + ) + payment_record_payload = { "user_id": user_id, - "amount": price_rub, + "amount": final_price_rub, + "original_amount": price_rub if discount_amount else None, + "discount_applied": discount_amount, "currency": currency_code, "status": "pending_severpay", "description": payment_description, "subscription_duration_months": int(months), "provider": "severpay", + "promo_code_id": promo_code_id, } try: @@ -100,7 +110,7 @@ async def pay_severpay_callback_handler( payment_db_id=payment_record.payment_id, user_id=user_id, months=months, - amount=price_rub, + amount=final_price_rub, currency=currency_code, description=payment_description, ) diff --git a/bot/handlers/user/subscription/payments_stars.py b/bot/handlers/user/subscription/payments_stars.py index 8ee31cb..2cb53f2 100644 --- a/bot/handlers/user/subscription/payments_stars.py +++ b/bot/handlers/user/subscription/payments_stars.py @@ -19,6 +19,7 @@ async def pay_stars_callback_handler( i18n_data: dict, session: AsyncSession, stars_service: StarsService, + promo_code_service=None, ): current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") @@ -66,6 +67,7 @@ async def pay_stars_callback_handler( stars_price=stars_price, description=payment_description, sale_mode=sale_mode, + promo_code_service=promo_code_service, ) if payment_db_id: diff --git a/bot/services/crypto_pay_service.py b/bot/services/crypto_pay_service.py index 895064f..babca27 100644 --- a/bot/services/crypto_pay_service.py +++ b/bot/services/crypto_pay_service.py @@ -18,6 +18,7 @@ from bot.services.notification_service import NotificationService from db.dal import payment_dal, user_dal from bot.utils.text_sanitizer import sanitize_display_name, username_for_display from bot.utils.config_link import prepare_config_links +from bot.handlers.user.subscription.payment_discount_helper import apply_discount_to_payment class CryptoPayService: @@ -65,23 +66,32 @@ class CryptoPayService: amount: float, description: str, sale_mode: str = "subscription", + promo_code_service=None, ) -> Optional[str]: if not self.configured or not self.client: logging.error("CryptoPayService not configured") return None + # Apply active discount if exists + final_amount, discount_amount, promo_code_id = await apply_discount_to_payment( + session, user_id, amount, promo_code_service + ) + # Create pending payment in DB and commit to persist try: payment_record = await payment_dal.create_payment_record( session, { "user_id": user_id, - "amount": float(amount), + "amount": final_amount, + "original_amount": amount if discount_amount else None, + "discount_applied": discount_amount, "currency": self.settings.CRYPTOPAY_ASSET, "status": "pending_cryptopay", "description": description, "subscription_duration_months": int(months), "provider": "cryptopay", + "promo_code_id": promo_code_id, }, ) await session.commit() @@ -101,7 +111,7 @@ class CryptoPayService: }) try: invoice = await self.client.create_invoice( - amount=amount, + amount=final_amount, currency_type=self.settings.CRYPTOPAY_CURRENCY_TYPE, fiat=self.settings.CRYPTOPAY_ASSET if self.settings.CRYPTOPAY_CURRENCY_TYPE == "fiat" else None, asset=self.settings.CRYPTOPAY_ASSET if self.settings.CRYPTOPAY_CURRENCY_TYPE == "crypto" else None, @@ -153,6 +163,12 @@ class CryptoPayService: async with async_session_factory() as session: try: + # Fetch payment record to get promo_code_id + payment_record = await payment_dal.get_payment_by_db_id(session, payment_db_id) + if not payment_record: + logging.error(f"CryptoPay: Payment record {payment_db_id} not found") + return + await payment_dal.update_provider_payment_and_status( session, payment_db_id, @@ -165,6 +181,7 @@ class CryptoPayService: int(months) if sale_mode != "traffic" else 0, float(invoice.amount), payment_db_id, + promo_code_id_from_payment=payment_record.promo_code_id, provider="cryptopay", sale_mode=sale_mode, traffic_gb=traffic_gb if sale_mode == "traffic" else None, diff --git a/bot/services/freekassa_service.py b/bot/services/freekassa_service.py index 5ca9fe4..0484be3 100644 --- a/bot/services/freekassa_service.py +++ b/bot/services/freekassa_service.py @@ -293,6 +293,7 @@ class FreeKassaService: int(months) if sale_mode != "traffic" else 0, float(payment.amount), payment.payment_id, + promo_code_id_from_payment=payment.promo_code_id, provider="freekassa", sale_mode=sale_mode, traffic_gb=months if sale_mode == "traffic" else None, diff --git a/bot/services/platega_service.py b/bot/services/platega_service.py index a41651e..4aa3893 100644 --- a/bot/services/platega_service.py +++ b/bot/services/platega_service.py @@ -189,6 +189,7 @@ class PlategaService: int(payment_months) if sale_mode != "traffic" else 0, float(payment.amount), payment.payment_id, + promo_code_id_from_payment=payment.promo_code_id, provider="platega", sale_mode=sale_mode, traffic_gb=payment_months if sale_mode == "traffic" else None, diff --git a/bot/services/severpay_service.py b/bot/services/severpay_service.py index 35c6f49..75d79cc 100644 --- a/bot/services/severpay_service.py +++ b/bot/services/severpay_service.py @@ -207,6 +207,7 @@ class SeverPayService: int(payment_months) if sale_mode != "traffic" else 0, float(payment.amount), payment.payment_id, + promo_code_id_from_payment=payment.promo_code_id, provider="severpay", sale_mode=sale_mode, traffic_gb=payment_months if sale_mode == "traffic" else None, diff --git a/bot/services/stars_service.py b/bot/services/stars_service.py index 0dfbe2a..c6d0dad 100644 --- a/bot/services/stars_service.py +++ b/bot/services/stars_service.py @@ -1,4 +1,5 @@ import logging +import math from typing import Optional from aiogram import Bot, types @@ -14,6 +15,7 @@ from .notification_service import NotificationService from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard from bot.utils.text_sanitizer import sanitize_display_name, username_for_display from bot.utils.config_link import prepare_config_links +from bot.handlers.user.subscription.payment_discount_helper import apply_discount_to_payment class StarsService: @@ -27,15 +29,37 @@ class StarsService: self.referral_service = referral_service async def create_invoice(self, session: AsyncSession, user_id: int, months: int, - stars_price: int, description: str, sale_mode: str = "subscription") -> Optional[int]: + stars_price: int, description: str, sale_mode: str = "subscription", + promo_code_service=None) -> Optional[int]: + # Apply active discount if exists (Stars use ceiling rounding) + original_stars_price = stars_price + discount_amount_stars = None + promo_code_id = None + + if promo_code_service: + # Apply discount and round up using ceiling + final_price_float, discount_float, promo_code_id = await apply_discount_to_payment( + session, user_id, float(stars_price), promo_code_service + ) + if discount_float: + # Apply ceiling rounding for fractional Stars amounts + stars_price = math.ceil(final_price_float) + discount_amount_stars = original_stars_price - stars_price + logging.info( + f"Stars discount applied: {original_stars_price} -> {final_price_float:.2f} -> {stars_price} (ceiling)" + ) + payment_record_data = { "user_id": user_id, "amount": float(stars_price), + "original_amount": float(original_stars_price) if discount_amount_stars else None, + "discount_applied": float(discount_amount_stars) if discount_amount_stars else None, "currency": "XTR", "status": "pending_stars", "description": description, "subscription_duration_months": int(months), "provider": "telegram_stars", + "promo_code_id": promo_code_id, } try: db_payment_record = await payment_dal.create_payment_record( @@ -72,6 +96,10 @@ class StarsService: stars_amount: int, i18n_data: dict, sale_mode: str = "subscription") -> None: + # Fetch payment record to get promo_code_id + payment_record = await payment_dal.get_payment_by_db_id(session, payment_db_id) + promo_code_id_from_payment = payment_record.promo_code_id if payment_record else None + try: await payment_dal.update_provider_payment_and_status( session, payment_db_id, @@ -91,6 +119,7 @@ class StarsService: int(months) if sale_mode != "traffic" else 0, float(stars_amount), payment_db_id, + promo_code_id_from_payment=promo_code_id_from_payment, provider="telegram_stars", sale_mode=sale_mode, traffic_gb=months if sale_mode == "traffic" else None,