Notify users after Tribute payment
This commit is contained in:
+13
-1
@@ -35,7 +35,19 @@ TELEGRAM_WEBHOOK_BASE_URL=https://webhooks.yourdomain.tld
|
||||
PRICE_1_MONTH=150
|
||||
PRICE_3_MONTHS=300
|
||||
PRICE_6_MONTHS=500
|
||||
PRICE_12_MONTHS=900
|
||||
PRICE_12_MONTHS=900
|
||||
# Telegram Stars Prices (integer values)
|
||||
STARS_PRICE_1_MONTH=0
|
||||
STARS_PRICE_3_MONTHS=0
|
||||
STARS_PRICE_6_MONTHS=0
|
||||
STARS_PRICE_12_MONTHS=0
|
||||
# Tribute Payment Links
|
||||
TRIBUTE_LINK_1_MONTH=
|
||||
TRIBUTE_LINK_3_MONTHS=
|
||||
TRIBUTE_LINK_6_MONTHS=
|
||||
TRIBUTE_LINK_12_MONTHS=
|
||||
# API key for verifying Tribute webhook signatures
|
||||
TRIBUTE_API_KEY=
|
||||
|
||||
# Subscription Expiration Notifications
|
||||
SUBSCRIPTION_EXPIRATION_NOTIFICATION_DAYS=7
|
||||
|
||||
@@ -105,7 +105,8 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
subscription_months,
|
||||
payment_value,
|
||||
payment_db_id,
|
||||
promo_code_id_from_payment=promo_code_id)
|
||||
promo_code_id_from_payment=promo_code_id,
|
||||
provider="yookassa")
|
||||
|
||||
if not activation_details or not activation_details.get('end_date'):
|
||||
logging.error(
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import logging
|
||||
from aiogram import Router, F, types, Bot
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, LabeledPrice
|
||||
from typing import Optional, Dict, Any, Union
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config.settings import Settings
|
||||
from db.dal import payment_dal
|
||||
from db.dal import payment_dal, user_dal
|
||||
from bot.keyboards.inline.user_keyboards import (
|
||||
get_subscription_options_keyboard, get_confirm_subscription_keyboard,
|
||||
get_subscription_options_keyboard, get_payment_method_keyboard,
|
||||
get_payment_url_keyboard, get_back_to_main_menu_markup)
|
||||
from bot.services.payment_service import YooKassaService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.referral_service import ReferralService
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
|
||||
router = Router(name="user_subscription_router")
|
||||
@@ -99,28 +100,101 @@ async def select_subscription_period_callback_handler(
|
||||
return
|
||||
|
||||
currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL
|
||||
confirmation_text_content = get_text("confirm_subscription_prompt",
|
||||
months=months,
|
||||
price=f"{price_rub:.2f}",
|
||||
currency_symbol=currency_symbol_val)
|
||||
reply_markup = get_confirm_subscription_keyboard(months, price_rub,
|
||||
currency_symbol_val,
|
||||
current_lang, i18n)
|
||||
text_content = get_text("choose_payment_method")
|
||||
tribute_url = settings.tribute_payment_links.get(months)
|
||||
stars_price = settings.stars_subscription_options.get(months)
|
||||
reply_markup = get_payment_method_keyboard(
|
||||
months,
|
||||
price_rub,
|
||||
tribute_url,
|
||||
stars_price,
|
||||
currency_symbol_val,
|
||||
current_lang,
|
||||
i18n,
|
||||
)
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(confirmation_text_content,
|
||||
await callback.message.edit_text(text_content,
|
||||
reply_markup=reply_markup)
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Edit message for subscription confirmation failed: {e_edit}. Sending new one."
|
||||
f"Edit message for payment method selection failed: {e_edit}. Sending new one."
|
||||
)
|
||||
await callback.message.answer(confirmation_text_content,
|
||||
await callback.message.answer(text_content,
|
||||
reply_markup=reply_markup)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("confirm_sub:"))
|
||||
async def confirm_subscription_callback_handler(
|
||||
@router.callback_query(F.data.startswith("pay_stars:"))
|
||||
async def pay_stars_callback_handler(
|
||||
callback: types.CallbackQuery, settings: Settings, i18n_data: dict,
|
||||
session: AsyncSession, bot: Bot):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
|
||||
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer(get_text("error_occurred_try_again"), show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
_, data_payload = callback.data.split(":", 1)
|
||||
months_str, price_str = data_payload.split(":")
|
||||
months = int(months_str)
|
||||
stars_price = int(price_str)
|
||||
except (ValueError, IndexError):
|
||||
logging.error(f"Invalid pay_stars data in callback: {callback.data}")
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
|
||||
user_id = callback.from_user.id
|
||||
payment_description = get_text("payment_description_subscription", months=months)
|
||||
|
||||
payment_record_data = {
|
||||
"user_id": user_id,
|
||||
"amount": float(stars_price),
|
||||
"currency": "XTR",
|
||||
"status": "pending_stars",
|
||||
"description": payment_description,
|
||||
"subscription_duration_months": months,
|
||||
"provider": "telegram_stars",
|
||||
}
|
||||
|
||||
try:
|
||||
db_payment_record = await payment_dal.create_payment_record(session, payment_record_data)
|
||||
await session.commit()
|
||||
except Exception as e_db_payment:
|
||||
await session.rollback()
|
||||
logging.error(f"Failed to create stars payment record: {e_db_payment}", exc_info=True)
|
||||
await callback.message.edit_text(get_text("error_creating_payment_record"))
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
|
||||
payload = f"{db_payment_record.payment_id}:{months}"
|
||||
prices = [LabeledPrice(label=payment_description, amount=stars_price)]
|
||||
|
||||
try:
|
||||
await bot.send_invoice(
|
||||
chat_id=user_id,
|
||||
title=payment_description,
|
||||
description=payment_description,
|
||||
payload=payload,
|
||||
provider_token="",
|
||||
currency="XTR",
|
||||
prices=prices,
|
||||
)
|
||||
except Exception as e_inv:
|
||||
logging.error(f"Failed to send Telegram Stars invoice: {e_inv}", exc_info=True)
|
||||
await callback.message.edit_text(get_text("error_payment_gateway"))
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("pay_yk:"))
|
||||
async def pay_yk_callback_handler(
|
||||
callback: types.CallbackQuery, settings: Settings, i18n_data: dict,
|
||||
yookassa_service: YooKassaService, session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
@@ -151,7 +225,7 @@ async def confirm_subscription_callback_handler(
|
||||
price_rub = float(price_str)
|
||||
except (ValueError, IndexError):
|
||||
logging.error(
|
||||
f"Invalid confirmation data in callback: {callback.data}")
|
||||
f"Invalid pay_yk data in callback: {callback.data}")
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
|
||||
@@ -342,6 +416,89 @@ async def my_subscription_command_handler(
|
||||
await target.answer(text, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True)
|
||||
|
||||
|
||||
@router.pre_checkout_query()
|
||||
async def stars_pre_checkout_handler(pre_checkout_query: types.PreCheckoutQuery):
|
||||
await pre_checkout_query.answer(ok=True)
|
||||
|
||||
|
||||
@router.message(F.successful_payment)
|
||||
async def stars_successful_payment_handler(
|
||||
message: types.Message, settings: Settings, i18n_data: dict,
|
||||
session: AsyncSession, bot: Bot, panel_service: PanelApiService,
|
||||
subscription_service: SubscriptionService, referral_service: ReferralService):
|
||||
sp = message.successful_payment
|
||||
if not sp or sp.currency != "XTR":
|
||||
return
|
||||
|
||||
payload = sp.invoice_payload or ""
|
||||
try:
|
||||
payment_id_str, months_str = payload.split(":")
|
||||
payment_db_id = int(payment_id_str)
|
||||
months = int(months_str)
|
||||
except (ValueError, IndexError):
|
||||
logging.error(f"Invalid invoice payload for stars payment: {payload}")
|
||||
return
|
||||
|
||||
provider_payment_id = sp.provider_payment_charge_id
|
||||
stars_amount = sp.total_amount
|
||||
try:
|
||||
await payment_dal.update_provider_payment_and_status(
|
||||
session, payment_db_id, provider_payment_id, "succeeded")
|
||||
await session.commit()
|
||||
except Exception as e_upd:
|
||||
await session.rollback()
|
||||
logging.error(f"Failed to update stars payment record {payment_db_id}: {e_upd}", exc_info=True)
|
||||
return
|
||||
|
||||
activation_details = await subscription_service.activate_subscription(
|
||||
session,
|
||||
message.from_user.id,
|
||||
months,
|
||||
float(stars_amount),
|
||||
payment_db_id,
|
||||
provider="telegram_stars",
|
||||
)
|
||||
|
||||
if not activation_details or not activation_details.get("end_date"):
|
||||
logging.error(f"Failed to activate subscription after stars payment for user {message.from_user.id}")
|
||||
return
|
||||
|
||||
referral_bonus_info = await referral_service.apply_referral_bonuses_for_payment(
|
||||
session, message.from_user.id, months)
|
||||
await session.commit()
|
||||
|
||||
applied_referee_days = referral_bonus_info.get("referee_bonus_applied_days") if referral_bonus_info else None
|
||||
final_end = referral_bonus_info.get("referee_new_end_date") if referral_bonus_info else activation_details["end_date"]
|
||||
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: JsonI18n = i18n_data.get("i18n_instance")
|
||||
_ = lambda k, **kw: i18n.gettext(current_lang, k, **kw) if i18n else k
|
||||
|
||||
if applied_referee_days:
|
||||
inviter_name_display = _("friend_placeholder")
|
||||
db_user = await user_dal.get_user_by_id(session, message.from_user.id)
|
||||
if db_user and db_user.referred_by_id:
|
||||
inviter = await user_dal.get_user_by_id(session, db_user.referred_by_id)
|
||||
if inviter and inviter.first_name:
|
||||
inviter_name_display = inviter.first_name
|
||||
elif inviter and inviter.username:
|
||||
inviter_name_display = f"@{inviter.username}"
|
||||
success_msg = _("payment_successful_with_referral_bonus",
|
||||
months=months,
|
||||
base_end_date=activation_details["end_date"].strftime('%Y-%m-%d'),
|
||||
bonus_days=applied_referee_days,
|
||||
final_end_date=final_end.strftime('%Y-%m-%d'),
|
||||
inviter_name=inviter_name_display)
|
||||
else:
|
||||
success_msg = _("payment_successful", months=months,
|
||||
end_date=final_end.strftime('%Y-%m-%d'))
|
||||
|
||||
try:
|
||||
await bot.send_message(message.from_user.id, success_msg)
|
||||
except Exception as e_send:
|
||||
logging.error(f"Failed to send stars payment success message: {e_send}")
|
||||
|
||||
|
||||
@router.message(Command("connect"))
|
||||
async def connect_command_handler(message: types.Message, i18n_data: dict,
|
||||
settings: Settings,
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import logging
|
||||
import hmac
|
||||
import hashlib
|
||||
import json
|
||||
from aiohttp import web
|
||||
from aiogram import Bot
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from config.settings import Settings
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.referral_service import ReferralService
|
||||
from db.dal import payment_dal, user_dal, subscription_dal
|
||||
|
||||
async def tribute_webhook_route(request: web.Request):
|
||||
bot: Bot = request.app['bot']
|
||||
settings: Settings = request.app['settings']
|
||||
i18n: JsonI18n = request.app['i18n']
|
||||
async_session_factory: sessionmaker = request.app['async_session_factory']
|
||||
panel_service: PanelApiService = request.app['panel_service']
|
||||
subscription_service: SubscriptionService = request.app['subscription_service']
|
||||
referral_service: ReferralService = request.app['referral_service']
|
||||
|
||||
raw_body = await request.read()
|
||||
signature_header = request.headers.get('trbt-signature')
|
||||
if settings.TRIBUTE_API_KEY:
|
||||
if not signature_header:
|
||||
return web.Response(status=403, text="no_signature")
|
||||
expected_sig = hmac.new(
|
||||
settings.TRIBUTE_API_KEY.encode(), raw_body, hashlib.sha256
|
||||
).hexdigest()
|
||||
if not hmac.compare_digest(expected_sig, signature_header):
|
||||
return web.Response(status=403, text="invalid_signature")
|
||||
|
||||
try:
|
||||
payload = json.loads(raw_body.decode())
|
||||
except Exception:
|
||||
return web.Response(status=400, text="bad_request")
|
||||
|
||||
event_name = payload.get('name')
|
||||
data = payload.get('payload', {})
|
||||
user_id = data.get('telegram_user_id')
|
||||
price_val = data.get('price')
|
||||
|
||||
if not user_id or price_val is None:
|
||||
return web.Response(status=200, text="ok_missing_fields")
|
||||
|
||||
months_map = {int(v): m for m, v in settings.subscription_options.items()}
|
||||
price_rub = price_val / 100
|
||||
months = months_map.get(int(price_rub))
|
||||
if not months:
|
||||
logging.warning(f"Tribute webhook: price {price_val} not mapped to months")
|
||||
return web.Response(status=200, text="ok_price_unmapped")
|
||||
|
||||
async with async_session_factory() as session:
|
||||
if event_name == 'new_subscription':
|
||||
payment_record = await payment_dal.create_payment_record(
|
||||
session,
|
||||
{
|
||||
'user_id': user_id,
|
||||
'amount': float(price_rub),
|
||||
'currency': 'RUB',
|
||||
'status': 'succeeded',
|
||||
'description': 'Tribute subscription',
|
||||
'subscription_duration_months': months,
|
||||
'provider_payment_id': str(data.get('subscription_id')),
|
||||
'provider': 'tribute',
|
||||
},
|
||||
)
|
||||
activation_details = await subscription_service.activate_subscription(
|
||||
session,
|
||||
user_id,
|
||||
months,
|
||||
float(price_rub),
|
||||
payment_record.payment_id,
|
||||
provider='tribute',
|
||||
)
|
||||
referral_bonus = await referral_service.apply_referral_bonuses_for_payment(
|
||||
session, user_id, months
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
lang = db_user.language_code if db_user and db_user.language_code else settings.DEFAULT_LANGUAGE
|
||||
_ = lambda k, **kw: i18n.gettext(lang, k, **kw)
|
||||
|
||||
applied_ref_days = referral_bonus.get('referee_bonus_applied_days') if referral_bonus else None
|
||||
final_end = referral_bonus.get('referee_new_end_date') if referral_bonus else activation_details.get('end_date')
|
||||
|
||||
if final_end:
|
||||
if applied_ref_days:
|
||||
inviter_name_display = _('friend_placeholder')
|
||||
if db_user and db_user.referred_by_id:
|
||||
inviter = await user_dal.get_user_by_id(session, db_user.referred_by_id)
|
||||
if inviter and inviter.first_name:
|
||||
inviter_name_display = inviter.first_name
|
||||
elif inviter and inviter.username:
|
||||
inviter_name_display = f"@{inviter.username}"
|
||||
success_msg = _("payment_successful_with_referral_bonus",
|
||||
months=months,
|
||||
base_end_date=activation_details["end_date"].strftime('%Y-%m-%d'),
|
||||
bonus_days=applied_ref_days,
|
||||
final_end_date=final_end.strftime('%Y-%m-%d'),
|
||||
inviter_name=inviter_name_display)
|
||||
else:
|
||||
success_msg = _("payment_successful", months=months,
|
||||
end_date=final_end.strftime('%Y-%m-%d'))
|
||||
|
||||
try:
|
||||
await bot.send_message(user_id, success_msg)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to send Tribute payment success message to user {user_id}: {e}")
|
||||
elif event_name == 'cancelled_subscription':
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
lang = db_user.language_code if db_user and db_user.language_code else settings.DEFAULT_LANGUAGE
|
||||
_ = lambda k, **kw: i18n.gettext(lang, k, **kw)
|
||||
try:
|
||||
await bot.send_message(user_id, _("subscription_cancelled_notification"))
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to notify user {user_id} about cancellation: {e}")
|
||||
await subscription_dal.set_skip_notifications_for_provider(
|
||||
session, user_id, 'tribute', False)
|
||||
await session.commit()
|
||||
else:
|
||||
await session.commit()
|
||||
return web.Response(status=200, text="ok")
|
||||
@@ -108,16 +108,20 @@ def get_subscription_options_keyboard(subscription_options: Dict[
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def get_confirm_subscription_keyboard(months: int, price: float,
|
||||
currency_symbol_val: str, lang: str,
|
||||
i18n_instance) -> InlineKeyboardMarkup:
|
||||
def get_payment_method_keyboard(months: int, price: float,
|
||||
tribute_url: Optional[str],
|
||||
stars_price: Optional[int],
|
||||
currency_symbol_val: str, lang: str,
|
||||
i18n_instance) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
confirm_text = _(key="confirm_payment_button",
|
||||
price=price,
|
||||
currency_symbol=currency_symbol_val)
|
||||
builder.button(text=confirm_text,
|
||||
callback_data=f"confirm_sub:{months}:{price}")
|
||||
if stars_price is not None:
|
||||
builder.button(text=_("pay_with_stars_button"),
|
||||
callback_data=f"pay_stars:{months}:{stars_price}")
|
||||
if tribute_url:
|
||||
builder.button(text=_("pay_with_tribute_button"), url=tribute_url)
|
||||
builder.button(text=_("pay_with_yookassa_button"),
|
||||
callback_data=f"pay_yk:{months}:{price}")
|
||||
builder.button(text=_(key="cancel_button"),
|
||||
callback_data="main_action:subscribe")
|
||||
builder.adjust(1)
|
||||
|
||||
@@ -33,6 +33,7 @@ from bot.services.referral_service import ReferralService
|
||||
from bot.services.promo_code_service import PromoCodeService
|
||||
|
||||
from bot.handlers.user import payment as user_payment_webhook_module
|
||||
from bot.handlers.webhooks import tribute as tribute_webhook_module
|
||||
|
||||
|
||||
class DBSessionMiddleware(BaseMiddleware):
|
||||
@@ -327,6 +328,14 @@ async def run_bot(settings_param: Settings):
|
||||
logging.info(
|
||||
f"YooKassa webhook route configured at: [POST] {yk_path}")
|
||||
|
||||
tribute_path = settings_param.tribute_webhook_path
|
||||
if tribute_path.startswith('/'):
|
||||
app.router.add_post(
|
||||
tribute_path,
|
||||
tribute_webhook_module.tribute_webhook_route)
|
||||
logging.info(
|
||||
f"Tribute webhook route configured at: [POST] {tribute_path}")
|
||||
|
||||
web_app_runner = web.AppRunner(app)
|
||||
await web_app_runner.setup()
|
||||
site = web.TCPSite(web_app_runner,
|
||||
|
||||
@@ -318,7 +318,8 @@ class SubscriptionService:
|
||||
months: int,
|
||||
payment_amount: float,
|
||||
payment_db_id: int,
|
||||
promo_code_id_from_payment: Optional[int] = None
|
||||
promo_code_id_from_payment: Optional[int] = None,
|
||||
provider: str = "yookassa"
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
@@ -386,6 +387,8 @@ class SubscriptionService:
|
||||
"status_from_panel": "ACTIVE",
|
||||
"traffic_limit_bytes":
|
||||
self.settings.PANEL_USER_DEFAULT_TRAFFIC_BYTES,
|
||||
"provider": provider,
|
||||
"skip_notifications": provider == "tribute",
|
||||
}
|
||||
try:
|
||||
new_or_updated_sub = await subscription_dal.upsert_subscription(
|
||||
@@ -601,7 +604,7 @@ class SubscriptionService:
|
||||
session, days_threshold)
|
||||
results = []
|
||||
for sub_model in subs_models_with_users:
|
||||
if sub_model.user and sub_model.end_date:
|
||||
if sub_model.user and sub_model.end_date and not sub_model.skip_notifications:
|
||||
days_left = (sub_model.end_date - datetime.now(
|
||||
timezone.utc)).total_seconds() / (24 * 3600)
|
||||
results.append({
|
||||
|
||||
+56
-4
@@ -41,6 +41,18 @@ class Settings(BaseSettings):
|
||||
PRICE_6_MONTHS: Optional[int] = Field(default=None)
|
||||
PRICE_12_MONTHS: Optional[int] = Field(default=None)
|
||||
|
||||
STARS_PRICE_1_MONTH: Optional[int] = Field(default=None)
|
||||
STARS_PRICE_3_MONTHS: Optional[int] = Field(default=None)
|
||||
STARS_PRICE_6_MONTHS: Optional[int] = Field(default=None)
|
||||
STARS_PRICE_12_MONTHS: Optional[int] = Field(default=None)
|
||||
|
||||
|
||||
TRIBUTE_LINK_1_MONTH: Optional[str] = Field(default=None)
|
||||
TRIBUTE_LINK_3_MONTHS: Optional[str] = Field(default=None)
|
||||
TRIBUTE_LINK_6_MONTHS: Optional[str] = Field(default=None)
|
||||
TRIBUTE_LINK_12_MONTHS: Optional[str] = Field(default=None)
|
||||
TRIBUTE_API_KEY: Optional[str] = Field(default=None)
|
||||
|
||||
SUBSCRIPTION_EXPIRATION_NOTIFICATION_DAYS: int = Field(default=7)
|
||||
SUBSCRIPTION_NOTIFICATION_HOUR_UTC: int = Field(default=9)
|
||||
SUBSCRIPTION_NOTIFICATION_MINUTE_UTC: int = Field(default=0)
|
||||
@@ -141,21 +153,61 @@ class Settings(BaseSettings):
|
||||
return f"{self.YOOKASSA_WEBHOOK_BASE_URL.rstrip('/')}{self.yookassa_webhook_path}"
|
||||
return None
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def tribute_webhook_path(self) -> str:
|
||||
return "/webhook/tribute"
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def tribute_full_webhook_url(self) -> Optional[str]:
|
||||
if self.YOOKASSA_WEBHOOK_BASE_URL:
|
||||
return f"{self.YOOKASSA_WEBHOOK_BASE_URL.rstrip('/')}{self.tribute_webhook_path}"
|
||||
return None
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def subscription_options(self) -> Dict[int, float]:
|
||||
options: Dict[int, float] = {}
|
||||
|
||||
if self.PRICE_1_MONTH is not None:
|
||||
options[1] = float(self.PRICE_1_MONTH / 100.0)
|
||||
options[1] = float(self.PRICE_1_MONTH)
|
||||
if self.PRICE_3_MONTHS is not None:
|
||||
options[3] = float(self.PRICE_3_MONTHS / 100.0)
|
||||
options[3] = float(self.PRICE_3_MONTHS)
|
||||
if self.PRICE_6_MONTHS is not None:
|
||||
options[6] = float(self.PRICE_6_MONTHS / 100.0)
|
||||
options[6] = float(self.PRICE_6_MONTHS)
|
||||
if self.PRICE_12_MONTHS is not None:
|
||||
options[12] = float(self.PRICE_12_MONTHS / 100.0)
|
||||
options[12] = float(self.PRICE_12_MONTHS)
|
||||
return options
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def stars_subscription_options(self) -> Dict[int, int]:
|
||||
options: Dict[int, int] = {}
|
||||
if self.STARS_PRICE_1_MONTH is not None:
|
||||
options[1] = self.STARS_PRICE_1_MONTH
|
||||
if self.STARS_PRICE_3_MONTHS is not None:
|
||||
options[3] = self.STARS_PRICE_3_MONTHS
|
||||
if self.STARS_PRICE_6_MONTHS is not None:
|
||||
options[6] = self.STARS_PRICE_6_MONTHS
|
||||
if self.STARS_PRICE_12_MONTHS is not None:
|
||||
options[12] = self.STARS_PRICE_12_MONTHS
|
||||
return options
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def tribute_payment_links(self) -> Dict[int, str]:
|
||||
links: Dict[int, str] = {}
|
||||
if self.TRIBUTE_LINK_1_MONTH:
|
||||
links[1] = self.TRIBUTE_LINK_1_MONTH
|
||||
if self.TRIBUTE_LINK_3_MONTHS:
|
||||
links[3] = self.TRIBUTE_LINK_3_MONTHS
|
||||
if self.TRIBUTE_LINK_6_MONTHS:
|
||||
links[6] = self.TRIBUTE_LINK_6_MONTHS
|
||||
if self.TRIBUTE_LINK_12_MONTHS:
|
||||
links[12] = self.TRIBUTE_LINK_12_MONTHS
|
||||
return links
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def referral_bonus_inviter(self) -> Dict[int, int]:
|
||||
|
||||
@@ -113,3 +113,23 @@ async def get_recent_payment_logs_with_user(session: AsyncSession,
|
||||
Payment.created_at.desc()).limit(limit).offset(offset))
|
||||
result = await session.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def update_provider_payment_and_status(
|
||||
session: AsyncSession, payment_db_id: int,
|
||||
provider_payment_id: str, new_status: str) -> Optional[Payment]:
|
||||
payment = await get_payment_by_db_id(session, payment_db_id)
|
||||
if payment:
|
||||
payment.status = new_status
|
||||
payment.provider_payment_id = provider_payment_id
|
||||
payment.updated_at = func.now()
|
||||
await session.flush()
|
||||
await session.refresh(payment)
|
||||
logging.info(
|
||||
f"Payment record {payment.payment_id} updated with provider id {provider_payment_id} and status {new_status}."
|
||||
)
|
||||
else:
|
||||
logging.warning(
|
||||
f"Payment record with DB ID {payment_db_id} not found for provider update."
|
||||
)
|
||||
return payment
|
||||
|
||||
@@ -156,7 +156,9 @@ async def get_subscriptions_near_expiration(
|
||||
threshold_date = now_utc + timedelta(days=days_threshold)
|
||||
|
||||
stmt = (select(Subscription).join(Subscription.user).where(
|
||||
Subscription.is_active == True, Subscription.end_date > now_utc,
|
||||
Subscription.is_active == True,
|
||||
Subscription.skip_notifications == False,
|
||||
Subscription.end_date > now_utc,
|
||||
Subscription.end_date <= threshold_date,
|
||||
or_(
|
||||
Subscription.last_notification_sent == None,
|
||||
@@ -203,3 +205,14 @@ async def find_subscription_for_notification_update(
|
||||
<= subscription_end_date_to_match + timedelta(seconds=1)).limit(1)
|
||||
result = await session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def set_skip_notifications_for_provider(
|
||||
session: AsyncSession, user_id: int, provider: str,
|
||||
skip: bool) -> int:
|
||||
stmt = (update(Subscription).where(
|
||||
Subscription.user_id == user_id,
|
||||
Subscription.is_active == True,
|
||||
Subscription.provider == provider).values(skip_notifications=skip))
|
||||
result = await session.execute(stmt)
|
||||
return result.rowcount
|
||||
|
||||
@@ -70,6 +70,8 @@ class Subscription(Base):
|
||||
traffic_limit_bytes = Column(BigInteger, nullable=True)
|
||||
traffic_used_bytes = Column(BigInteger, nullable=True)
|
||||
last_notification_sent = Column(DateTime(timezone=True), nullable=True)
|
||||
provider = Column(String, nullable=True)
|
||||
skip_notifications = Column(Boolean, default=False)
|
||||
|
||||
user = relationship("User", back_populates="subscriptions")
|
||||
|
||||
@@ -89,6 +91,8 @@ class Payment(Base):
|
||||
unique=True,
|
||||
index=True,
|
||||
nullable=True)
|
||||
provider_payment_id = Column(String, unique=True, nullable=True)
|
||||
provider = Column(String, nullable=False, default="yookassa", index=True)
|
||||
idempotence_key = Column(String, unique=True, nullable=True)
|
||||
amount = Column(Float, nullable=False)
|
||||
currency = Column(String, nullable=False)
|
||||
|
||||
+5
-2
@@ -22,9 +22,11 @@
|
||||
"select_subscription_period": "Select subscription period:",
|
||||
"no_subscription_options_available": "No subscription options available at the moment.",
|
||||
"subscribe_for_months_button": "{months} mo. - {price} {currency_symbol}",
|
||||
"confirm_subscription_prompt": "Confirm subscription purchase:\nDuration: {months} mo.\nPrice: {price} {currency_symbol}",
|
||||
"choose_payment_method": "Choose payment method:",
|
||||
"pay_button": "💳 Pay",
|
||||
"confirm_payment_button": "✅ Yes ({price} {currency_symbol})",
|
||||
"pay_with_yookassa_button": "💳 YooKassa",
|
||||
"pay_with_tribute_button": "❤️ Tribute",
|
||||
"pay_with_stars_button": "🌟 Telegram Stars",
|
||||
"cancel_button": "❌ Cancel",
|
||||
"payment_description_subscription": "Subscription payment for {months} mo.",
|
||||
"payment_service_unavailable": "Payment service temporarily unavailable. Please try again later.",
|
||||
@@ -201,6 +203,7 @@
|
||||
"stub_page_display": "Page",
|
||||
|
||||
"subscription_ending_soon_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription ends on {end_date} (in {days_left} days).\n\nTo avoid interruption, please renew it in the main menu.",
|
||||
"subscription_cancelled_notification": "Your recurring subscription was cancelled. You will keep access until the paid period ends.",
|
||||
|
||||
"error_unknown": "An unknown error occurred."
|
||||
}
|
||||
|
||||
+5
-2
@@ -22,9 +22,11 @@
|
||||
"select_subscription_period": "Выберите срок подписки:",
|
||||
"no_subscription_options_available": "В данный момент нет доступных вариантов подписки.",
|
||||
"subscribe_for_months_button": "{months} мес. - {price} {currency_symbol}",
|
||||
"confirm_subscription_prompt": "Подтвердите покупку подписки:\nСрок: {months} мес.\nЦена: {price} {currency_symbol}",
|
||||
"choose_payment_method": "Выберите способ оплаты:",
|
||||
"pay_button": "💳 Оплатить",
|
||||
"confirm_payment_button": "✅ Да ({price} {currency_symbol})",
|
||||
"pay_with_yookassa_button": "💳 ЮKassa",
|
||||
"pay_with_tribute_button": "❤️ Tribute",
|
||||
"pay_with_stars_button": "🌟 Звезды Telegram",
|
||||
"cancel_button": "❌ Отмена",
|
||||
"payment_description_subscription": "Оплата подписки на {months} мес.",
|
||||
"payment_service_unavailable": "Платежный сервис временно недоступен. Пожалуйста, попробуйте позже.",
|
||||
@@ -201,6 +203,7 @@
|
||||
"stub_page_display": "Страница",
|
||||
|
||||
"subscription_ending_soon_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекает {end_date} (через {days_left} дн.).\n\nЧтобы не потерять доступ, пожалуйста, продлите ее заранее в главном меню бота.",
|
||||
"subscription_cancelled_notification": "Ваша подписка отменена. Доступ сохранится до конца оплаченного периода.",
|
||||
|
||||
"error_unknown": "Произошла неизвестная ошибка."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user