feat(admin): add admin notification system for deposits, purchases, and renewals
This commit is contained in:
@@ -11,6 +11,11 @@ BOT_TOKEN=your_telegram_bot_token
|
|||||||
# Comma-separated list of Telegram user IDs
|
# Comma-separated list of Telegram user IDs
|
||||||
ADMINS=[123456789,987654321]
|
ADMINS=[123456789,987654321]
|
||||||
|
|
||||||
|
# Optional admin operation logs. Leave empty to disable.
|
||||||
|
ADMIN_LOG_CHAT_ID=
|
||||||
|
ADMIN_LOG_DEPOSIT_TOPIC_ID=
|
||||||
|
ADMIN_LOG_SUBSCRIPTIONS_TOPIC_ID=
|
||||||
|
|
||||||
# Pally
|
# Pally
|
||||||
PALLY_SHOP_ID=your_pally_shop_id
|
PALLY_SHOP_ID=your_pally_shop_id
|
||||||
PALLY_TOKEN=your_pally_token
|
PALLY_TOKEN=your_pally_token
|
||||||
@@ -40,6 +45,3 @@ WHITELIST_SQUAD=e4435baa-64d3-4b29-a18e-81ad1c60d977
|
|||||||
|
|
||||||
# Autorenewal
|
# Autorenewal
|
||||||
AUTORENEW_DAYS_BEFORE=1
|
AUTORENEW_DAYS_BEFORE=1
|
||||||
|
|
||||||
# Autorenewal
|
|
||||||
AUTORENEW_DAYS_BEFORE=1
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
# ruff: noqa: N803
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import hmac
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ from db.models.bills import DepositStatus
|
|||||||
from db.models.transactions import BalanceTxType
|
from db.models.transactions import BalanceTxType
|
||||||
from misc.pally import BillStatus
|
from misc.pally import BillStatus
|
||||||
from schemas.di import APIDependenciesDTO
|
from schemas.di import APIDependenciesDTO
|
||||||
|
from services.admin_notifications import notify_deposit
|
||||||
|
|
||||||
router = APIRouter(prefix="/pally")
|
router = APIRouter(prefix="/pally")
|
||||||
|
|
||||||
@@ -21,7 +24,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/result")
|
@router.post("/result")
|
||||||
async def pally_callback(
|
async def pally_callback( # noqa: PLR0911, PLR0912
|
||||||
InvId: str = Form(...),
|
InvId: str = Form(...),
|
||||||
OutSum: str = Form(...),
|
OutSum: str = Form(...),
|
||||||
Commission: str = Form(...),
|
Commission: str = Form(...),
|
||||||
@@ -52,27 +55,39 @@ async def pally_callback(
|
|||||||
logger.info(
|
logger.info(
|
||||||
"Pally webhook received - InvId: %s, OutSum: %s, Commission: %s, TrsId: %s, Status: %s, "
|
"Pally webhook received - InvId: %s, OutSum: %s, Commission: %s, TrsId: %s, Status: %s, "
|
||||||
"CurrencyIn: %s, custom: %s, BalanceAmount: %s, SignatureValue: %s",
|
"CurrencyIn: %s, custom: %s, BalanceAmount: %s, SignatureValue: %s",
|
||||||
InvId, OutSum, Commission, TrsId, Status, CurrencyIn, custom, BalanceAmount, SignatureValue
|
InvId,
|
||||||
|
OutSum,
|
||||||
|
Commission,
|
||||||
|
TrsId,
|
||||||
|
Status,
|
||||||
|
CurrencyIn,
|
||||||
|
custom,
|
||||||
|
BalanceAmount,
|
||||||
|
SignatureValue,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Enhanced logging for failed payments
|
# Enhanced logging for failed payments
|
||||||
if Status != BillStatus.SUCCESS:
|
if Status != BillStatus.SUCCESS:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Non-success payment received - Status: %s, ErrorCode: %s, ErrorMessage: %s, TrsId: %s",
|
"Non-success payment received - Status: %s, ErrorCode: %s, ErrorMessage: %s, TrsId: %s",
|
||||||
Status, ErrorCode, ErrorMessage, TrsId
|
Status,
|
||||||
|
ErrorCode,
|
||||||
|
ErrorMessage,
|
||||||
|
TrsId,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate signature
|
# Validate signature
|
||||||
raw_string = f"{OutSum}:{InvId}:{settings.pally_token}"
|
raw_string = f"{OutSum}:{InvId}:{settings.pally_token}"
|
||||||
expected_signature = hashlib.md5(raw_string.encode("utf-8")).hexdigest().upper()
|
expected_signature = hashlib.md5(raw_string.encode("utf-8")).hexdigest().upper()
|
||||||
|
|
||||||
logger.debug("Signature validation - Expected: %s, Received: %s, Raw: %s",
|
logger.debug("Signature validation for TrsId %s", TrsId)
|
||||||
expected_signature, SignatureValue, raw_string)
|
|
||||||
|
|
||||||
if SignatureValue != expected_signature:
|
if not hmac.compare_digest(SignatureValue, expected_signature):
|
||||||
logger.critical(
|
logger.critical(
|
||||||
"SECURITY ALERT: Invalid signature for TrsId %s - Expected: %s, Received: %s, Raw: %s",
|
"SECURITY ALERT: Invalid signature for TrsId %s - Expected: %s, Received: %s",
|
||||||
TrsId, expected_signature, SignatureValue, raw_string
|
TrsId,
|
||||||
|
expected_signature,
|
||||||
|
SignatureValue,
|
||||||
)
|
)
|
||||||
raise HTTPException(403, detail="Invalid signature.")
|
raise HTTPException(403, detail="Invalid signature.")
|
||||||
|
|
||||||
@@ -86,8 +101,7 @@ async def pally_callback(
|
|||||||
# Validate bill ID (from InvId field, which contains the order_id from bill creation)
|
# Validate bill ID (from InvId field, which contains the order_id from bill creation)
|
||||||
if not bill_id_str or not bill_id_str.isdigit():
|
if not bill_id_str or not bill_id_str.isdigit():
|
||||||
logger.critical(
|
logger.critical(
|
||||||
"Invalid or non-numeric bill ID in InvId field for TrsId %s: '%s'",
|
"Invalid or non-numeric bill ID in InvId field for TrsId %s: '%s'", TrsId, bill_id_str
|
||||||
TrsId, bill_id_str
|
|
||||||
)
|
)
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
@@ -100,8 +114,9 @@ async def pally_callback(
|
|||||||
|
|
||||||
# Check if already processed
|
# Check if already processed
|
||||||
if bill.status != DepositStatus.PENDING:
|
if bill.status != DepositStatus.PENDING:
|
||||||
logger.warning("Bill %s (TrsId: %s) is already processed with status: %s",
|
logger.warning(
|
||||||
bill.id, TrsId, bill.status)
|
"Bill %s (TrsId: %s) is already processed with status: %s", bill.id, TrsId, bill.status
|
||||||
|
)
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -114,14 +129,21 @@ async def pally_callback(
|
|||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Customer-pays-fees payment: bill_id=%s, expected=%s, gross_paid=%s, net_credited=%s",
|
"Customer-pays-fees payment: bill_id=%s, expected=%s, gross_paid=%s, net_credited=%s",
|
||||||
bill.id, bill.amount, gross_amount, credited_amount
|
bill.id,
|
||||||
|
bill.amount,
|
||||||
|
gross_amount,
|
||||||
|
credited_amount,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate that the net credited amount matches our bill amount
|
# Validate that the net credited amount matches our bill amount
|
||||||
if bill.amount != credited_amount:
|
if bill.amount != credited_amount:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Net amount mismatch for bill %s (TrsId: %s) - Expected: %s, Net credited: %s, Gross paid: %s",
|
"Net amount mismatch for bill %s (TrsId: %s) - Expected: %s, Net credited: %s, Gross paid: %s",
|
||||||
bill.id, TrsId, bill.amount, credited_amount, gross_amount
|
bill.id,
|
||||||
|
TrsId,
|
||||||
|
bill.amount,
|
||||||
|
credited_amount,
|
||||||
|
gross_amount,
|
||||||
)
|
)
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
@@ -133,21 +155,30 @@ async def pally_callback(
|
|||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Standard payment: bill_id=%s, expected=%s, received=%s",
|
"Standard payment: bill_id=%s, expected=%s, received=%s",
|
||||||
bill.id, bill.amount, amount
|
bill.id,
|
||||||
|
bill.amount,
|
||||||
|
amount,
|
||||||
)
|
)
|
||||||
|
|
||||||
if bill.amount != amount:
|
if bill.amount != amount:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Amount mismatch for bill %s (TrsId: %s) - Expected: %s, Received: %s",
|
"Amount mismatch for bill %s (TrsId: %s) - Expected: %s, Received: %s",
|
||||||
bill.id, TrsId, bill.amount, amount
|
bill.id,
|
||||||
|
TrsId,
|
||||||
|
bill.amount,
|
||||||
|
amount,
|
||||||
)
|
)
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
logger.info("Processing payment: bill_id=%s, user_id=%s, amount=%s",
|
logger.info(
|
||||||
bill.id, bill.creator_id, amount)
|
"Processing payment: bill_id=%s, user_id=%s, amount=%s",
|
||||||
|
bill.id,
|
||||||
|
bill.creator_id,
|
||||||
|
amount,
|
||||||
|
)
|
||||||
|
|
||||||
# Credit user balance
|
# Credit user balance
|
||||||
await deps.user_repository.increase_user_balance(
|
credited_user = await deps.user_repository.increase_user_balance(
|
||||||
session,
|
session,
|
||||||
bill.creator_id,
|
bill.creator_id,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
@@ -155,6 +186,16 @@ async def pally_callback(
|
|||||||
description=f"payment via PALLY (TrsId: {TrsId})",
|
description=f"payment via PALLY (TrsId: {TrsId})",
|
||||||
)
|
)
|
||||||
await successful_payment_notification(bot, bill.creator_id, amount)
|
await successful_payment_notification(bot, bill.creator_id, amount)
|
||||||
|
if credited_user:
|
||||||
|
await notify_deposit(
|
||||||
|
bot,
|
||||||
|
user_id=bill.creator_id,
|
||||||
|
amount=amount,
|
||||||
|
balance_before=credited_user.balance - amount,
|
||||||
|
balance_after=credited_user.balance,
|
||||||
|
bill_id=bill.id,
|
||||||
|
payment_id=TrsId,
|
||||||
|
)
|
||||||
|
|
||||||
# Process referral bonus
|
# Process referral bonus
|
||||||
user = await deps.user_repository.get_user_by_id(session, bill.creator_id)
|
user = await deps.user_repository.get_user_by_id(session, bill.creator_id)
|
||||||
@@ -169,14 +210,19 @@ async def pally_callback(
|
|||||||
description=f"referral reward for user {bill.creator_id} (TrsId: {TrsId})",
|
description=f"referral reward for user {bill.creator_id} (TrsId: {TrsId})",
|
||||||
)
|
)
|
||||||
await successful_payment_notification(bot, referal, referal_amount)
|
await successful_payment_notification(bot, referal, referal_amount)
|
||||||
logger.info("Referral bonus processed: referrer_id=%s, amount=%s", referal, referal_amount)
|
logger.info(
|
||||||
|
"Referral bonus processed: referrer_id=%s, amount=%s", referal, referal_amount
|
||||||
|
)
|
||||||
|
|
||||||
logger.info("Payment processing completed successfully for TrsId %s", TrsId)
|
logger.info("Payment processing completed successfully for TrsId %s", TrsId)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"CRITICAL ERROR processing payment for TrsId %s, bill_id %s, user_id %s: %s",
|
"CRITICAL ERROR processing payment for TrsId %s, bill_id %s, user_id %s: %s",
|
||||||
TrsId, bill.id, bill.creator_id, str(e)
|
TrsId,
|
||||||
|
bill.id,
|
||||||
|
bill.creator_id,
|
||||||
|
str(e),
|
||||||
)
|
)
|
||||||
# Don't return early - still mark as success to prevent retries
|
# Don't return early - still mark as success to prevent retries
|
||||||
# The balance operation might have partially succeeded
|
# The balance operation might have partially succeeded
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from misc.utils import (
|
|||||||
)
|
)
|
||||||
from schemas.di import DependenciesDTO
|
from schemas.di import DependenciesDTO
|
||||||
from schemas.subscriptions import SubscriptionPlan
|
from schemas.subscriptions import SubscriptionPlan
|
||||||
|
from services.admin_notifications import notify_subscription_purchase
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -223,4 +224,18 @@ async def proceed_to_checkout(
|
|||||||
amount,
|
amount,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if deducted_user:
|
||||||
|
await notify_subscription_purchase(
|
||||||
|
cb.bot,
|
||||||
|
user_id=user.id,
|
||||||
|
username=cb.from_user.username,
|
||||||
|
amount=amount,
|
||||||
|
balance_before=deducted_user.balance + amount,
|
||||||
|
balance_after=deducted_user.balance,
|
||||||
|
devices=ctx.devices,
|
||||||
|
duration_days=convert_duration_to_int(ctx.duration),
|
||||||
|
whitelists=ctx.whitelists,
|
||||||
|
credit=credit,
|
||||||
|
)
|
||||||
|
|
||||||
await cb.message.edit_text(SUCCESSFULLY_CREATED_SUBSCRIPTION, reply_markup=return_to_menu)
|
await cb.message.edit_text(SUCCESSFULLY_CREATED_SUBSCRIPTION, reply_markup=return_to_menu)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from .common import router as common_router
|
from .common import router as common_router
|
||||||
from .prehandling import router as prehandling_router
|
from .prehandling import router as prehandling_router
|
||||||
|
|
||||||
routers = [common_router, prehandling_router]
|
routers = [prehandling_router, common_router]
|
||||||
|
|||||||
20
bot/texts.py
20
bot/texts.py
@@ -174,15 +174,17 @@ REFERALS_MENU = (
|
|||||||
# ОФОРМЛЕНИЕ ПОДПИСКИ
|
# ОФОРМЛЕНИЕ ПОДПИСКИ
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
SUBSCRIPTION_DEVICE_SELECTOR = f"<b>{MALENIA_QUESTION} Сколько устройств вы хотите подключить?</b>\n\nЦена: <b>{{price}}₽</b>"
|
SUBSCRIPTION_DEVICE_SELECTOR = (
|
||||||
|
f"<b>{MALENIA_QUESTION} Сколько устройств вы хотите подключить?</b>\n\nЦена: <b>{{price}}₽</b>"
|
||||||
SUBSCRIPTION_DURATION_SELECTOR = (
|
|
||||||
f"<b>{MALENIA_QUESTION} На какой срок хотите оформить подписку?</b>\n\nИтого: <b>{{price}}₽</b> <i>(скидка: {{discount}}%)</i>"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SUBSCRIPTION_DURATION_SELECTOR = f"<b>{MALENIA_QUESTION} На какой срок хотите оформить подписку?</b>\n\nИтого: <b>{{price}}₽</b> <i>(скидка: {{discount}}%)</i>"
|
||||||
|
|
||||||
WHITELISTS_ALREADY_ON = "Белые списки уже активны."
|
WHITELISTS_ALREADY_ON = "Белые списки уже активны."
|
||||||
|
|
||||||
CHECKOUT_PAYMENT_CHOICE = f"<b>{MALENIA_BANK} Пополнение баланса:</b>\n\n<i>❓ Выберите способ оплаты:</i>"
|
CHECKOUT_PAYMENT_CHOICE = (
|
||||||
|
f"<b>{MALENIA_BANK} Пополнение баланса:</b>\n\n<i>❓ Выберите способ оплаты:</i>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -203,13 +205,13 @@ BILL_CREATED = _BILL_TEMPLATE + _DIVIDER + _BILL_LINK
|
|||||||
|
|
||||||
INSUFFICIENT_FUNDS = f"<b>{MALENIA_CROSS} Недостаточно средств для проведения операции.</b>\n\n<b>💰 Текущий баланс:</b> <code>{{balance}}₽</code>\n<b>💸 Требуется:</b> <code>{{amount}}₽</code>\n\n<i>{MALENIA_CARD} Пополните баланс на <code>{{diff}}₽</code></i>"
|
INSUFFICIENT_FUNDS = f"<b>{MALENIA_CROSS} Недостаточно средств для проведения операции.</b>\n\n<b>💰 Текущий баланс:</b> <code>{{balance}}₽</code>\n<b>💸 Требуется:</b> <code>{{amount}}₽</code>\n\n<i>{MALENIA_CARD} Пополните баланс на <code>{{diff}}₽</code></i>"
|
||||||
|
|
||||||
SUCCESSFULLY_CREATED_SUBSCRIPTION = (
|
SUCCESSFULLY_CREATED_SUBSCRIPTION = f"<b>{MALENIA_HEART} Ваша подписка успешно оплачена!</b> Ссылка на добавления доступна в главном меню."
|
||||||
f"<b>{MALENIA_HEART} Ваша подписка успешно оплачена!</b> Ссылка на добавления доступна в главном меню."
|
|
||||||
)
|
|
||||||
|
|
||||||
DEPOSIT_AMOUNT = f"<b>{MALENIA_COINS} Укажите сумму пополнения:</b>"
|
DEPOSIT_AMOUNT = f"<b>{MALENIA_COINS} Укажите сумму пополнения:</b>"
|
||||||
|
|
||||||
DEPOSIT_AMOUNT_FALLBACK = f"<b>{MALENIA_CROSS} Сумма пополнения указана неверно, повторите попытку.</b>"
|
DEPOSIT_AMOUNT_FALLBACK = (
|
||||||
|
f"<b>{MALENIA_CROSS} Сумма пополнения указана неверно, повторите попытку.</b>"
|
||||||
|
)
|
||||||
|
|
||||||
DEPOSIT_AMOUNT_BELOW_MINIMAL = (
|
DEPOSIT_AMOUNT_BELOW_MINIMAL = (
|
||||||
f"<b>{MALENIA_CROSS} Сумма пополнения ниже минимальной - <code>{{threshold}}₽</code>.</b>"
|
f"<b>{MALENIA_CROSS} Сумма пополнения ниже минимальной - <code>{{threshold}}₽</code>.</b>"
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
bot_token: str = Field(alias="BOT_TOKEN")
|
bot_token: str = Field(alias="BOT_TOKEN")
|
||||||
admins: list[int] = Field(alias="ADMINS")
|
admins: list[int] = Field(alias="ADMINS")
|
||||||
|
admin_log_chat_id: int | None = Field(None, alias="ADMIN_LOG_CHAT_ID")
|
||||||
|
admin_log_deposit_topic_id: int | None = Field(None, alias="ADMIN_LOG_DEPOSIT_TOPIC_ID")
|
||||||
|
admin_log_subscriptions_topic_id: int | None = Field(
|
||||||
|
None, alias="ADMIN_LOG_SUBSCRIPTIONS_TOPIC_ID"
|
||||||
|
)
|
||||||
|
|
||||||
pally_shop_id: str = Field(alias="PALLY_SHOP_ID")
|
pally_shop_id: str = Field(alias="PALLY_SHOP_ID")
|
||||||
pally_token: str = Field(alias="PALLY_TOKEN")
|
pally_token: str = Field(alias="PALLY_TOKEN")
|
||||||
@@ -36,7 +41,5 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
autorenew_days_before: int = Field(1, alias="AUTORENEW_DAYS_BEFORE")
|
autorenew_days_before: int = Field(1, alias="AUTORENEW_DAYS_BEFORE")
|
||||||
|
|
||||||
autorenew_days_before: int = Field(1, alias="AUTORENEW_DAYS_BEFORE")
|
|
||||||
|
|
||||||
|
|
||||||
settings = Settings() # type: ignore
|
settings = Settings() # type: ignore
|
||||||
|
|||||||
0
entrypoints/api.sh
Normal file → Executable file
0
entrypoints/api.sh
Normal file → Executable file
0
entrypoints/startup.sh
Normal file → Executable file
0
entrypoints/startup.sh
Normal file → Executable file
111
services/admin_notifications.py
Normal file
111
services/admin_notifications.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import logging
|
||||||
|
from html import escape
|
||||||
|
|
||||||
|
from aiogram import Bot
|
||||||
|
|
||||||
|
from config import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def _send_admin_log(bot: Bot, topic_id: int | None, text: str) -> None:
|
||||||
|
if settings.admin_log_chat_id is None or topic_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bot.send_message(
|
||||||
|
settings.admin_log_chat_id,
|
||||||
|
text,
|
||||||
|
message_thread_id=topic_id,
|
||||||
|
disable_web_page_preview=True,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Failed to send admin log notification", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_username(username: str | None) -> str:
|
||||||
|
if not username:
|
||||||
|
return "-"
|
||||||
|
return "@" + escape(username)
|
||||||
|
|
||||||
|
|
||||||
|
async def notify_deposit(
|
||||||
|
bot: Bot,
|
||||||
|
*,
|
||||||
|
user_id: int,
|
||||||
|
amount: int,
|
||||||
|
balance_before: int,
|
||||||
|
balance_after: int,
|
||||||
|
bill_id: int,
|
||||||
|
payment_id: str,
|
||||||
|
) -> None:
|
||||||
|
await _send_admin_log(
|
||||||
|
bot,
|
||||||
|
settings.admin_log_deposit_topic_id,
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"<b>Пополнение баланса</b>",
|
||||||
|
f"Пользователь: <code>{user_id}</code>",
|
||||||
|
f"Сумма: <b>{amount}₽</b>",
|
||||||
|
f"Баланс: {balance_before}₽ -> {balance_after}₽",
|
||||||
|
f"Bill ID: <code>{bill_id}</code>",
|
||||||
|
f"Payment ID: <code>{escape(payment_id)}</code>",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def notify_subscription_purchase(
|
||||||
|
bot: Bot,
|
||||||
|
*,
|
||||||
|
user_id: int,
|
||||||
|
username: str | None,
|
||||||
|
amount: int,
|
||||||
|
balance_before: int,
|
||||||
|
balance_after: int,
|
||||||
|
devices: int,
|
||||||
|
duration_days: int,
|
||||||
|
whitelists: bool,
|
||||||
|
credit: int,
|
||||||
|
) -> None:
|
||||||
|
lines = [
|
||||||
|
"<b>Покупка подписки</b>",
|
||||||
|
f"Пользователь: <code>{user_id}</code> ({_format_username(username)})",
|
||||||
|
f"Стоимость: <b>{amount}₽</b>",
|
||||||
|
f"Баланс: {balance_before}₽ -> {balance_after}₽",
|
||||||
|
f"Устройства: {devices}",
|
||||||
|
f"Срок: {duration_days} дн.",
|
||||||
|
f"Whitelist: {'да' if whitelists else 'нет'}",
|
||||||
|
]
|
||||||
|
if credit > 0:
|
||||||
|
lines.append(f"Зачет неиспользованных дней: {credit}₽")
|
||||||
|
|
||||||
|
await _send_admin_log(bot, settings.admin_log_subscriptions_topic_id, "\n".join(lines))
|
||||||
|
|
||||||
|
|
||||||
|
async def notify_subscription_renewal(
|
||||||
|
bot: Bot,
|
||||||
|
*,
|
||||||
|
user_id: int,
|
||||||
|
amount: int,
|
||||||
|
balance_before: int,
|
||||||
|
balance_after: int,
|
||||||
|
devices: int,
|
||||||
|
duration_days: int,
|
||||||
|
whitelists: bool,
|
||||||
|
end_date: str,
|
||||||
|
) -> None:
|
||||||
|
await _send_admin_log(
|
||||||
|
bot,
|
||||||
|
settings.admin_log_subscriptions_topic_id,
|
||||||
|
(
|
||||||
|
"<b>Продление подписки</b>",
|
||||||
|
f"Пользователь: <code>{user_id}</code>",
|
||||||
|
f"Стоимость: <b>{amount}₽</b>",
|
||||||
|
f"Баланс: {balance_before}₽ -> {balance_after}₽",
|
||||||
|
f"Устройства: {devices}",
|
||||||
|
f"Срок: {duration_days} дн.",
|
||||||
|
f"Whitelist: {'да' if whitelists else 'нет'}",
|
||||||
|
f"Новая дата окончания: {escape(end_date)}",
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -14,6 +14,7 @@ from repositories.subscriptions import SubscriptionRepository
|
|||||||
from repositories.users import UserRepository
|
from repositories.users import UserRepository
|
||||||
from schemas.subscriptions import InternalSquads, SubscriptionPlan, SubscriptionStatus
|
from schemas.subscriptions import InternalSquads, SubscriptionPlan, SubscriptionStatus
|
||||||
from services import rw as rw_service
|
from services import rw as rw_service
|
||||||
|
from services.admin_notifications import notify_subscription_renewal
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ class RenewalService:
|
|||||||
await self._notify_user(user_id, AUTORENEWAL_INSUFFICIENT_FUNDS)
|
await self._notify_user(user_id, AUTORENEWAL_INSUFFICIENT_FUNDS)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
result = await self.user_repository.decrease_user_balance(
|
renewed_user = await self.user_repository.decrease_user_balance(
|
||||||
session,
|
session,
|
||||||
user_id,
|
user_id,
|
||||||
price,
|
price,
|
||||||
@@ -99,7 +100,7 @@ class RenewalService:
|
|||||||
description=f"autorenewal: devices={subscription.devices}, duration={subscription.duration_days}d, whitelists={subscription.whitelists}",
|
description=f"autorenewal: devices={subscription.devices}, duration={subscription.duration_days}d, whitelists={subscription.whitelists}",
|
||||||
)
|
)
|
||||||
|
|
||||||
if not result:
|
if not renewed_user:
|
||||||
logger.error("Failed to decrease balance for user %d", user_id)
|
logger.error("Failed to decrease balance for user %d", user_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -136,6 +137,17 @@ class RenewalService:
|
|||||||
end_date=new_end_date.strftime("%d.%m.%Y"),
|
end_date=new_end_date.strftime("%d.%m.%Y"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
await notify_subscription_renewal(
|
||||||
|
self.bot,
|
||||||
|
user_id=user_id,
|
||||||
|
amount=price,
|
||||||
|
balance_before=renewed_user.balance + price,
|
||||||
|
balance_after=renewed_user.balance,
|
||||||
|
devices=subscription.devices,
|
||||||
|
duration_days=subscription.duration_days,
|
||||||
|
whitelists=subscription.whitelists,
|
||||||
|
end_date=new_end_date.strftime("%d.%m.%Y"),
|
||||||
|
)
|
||||||
|
|
||||||
logger.info("Successfully renewed subscription for user %d", user_id)
|
logger.info("Successfully renewed subscription for user %d", user_id)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ load_dotenv()
|
|||||||
|
|
||||||
ENDPOINT = "/pally/result"
|
ENDPOINT = "/pally/result"
|
||||||
|
|
||||||
|
|
||||||
def calculate_signature(
|
def calculate_signature(
|
||||||
out_sum: str,
|
out_sum: str,
|
||||||
inv_id: str,
|
inv_id: str,
|
||||||
@@ -45,7 +46,7 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--customer-pays-fees",
|
"--customer-pays-fees",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Симулировать сценарий 'клиент платит комиссию'"
|
help="Симулировать сценарий 'клиент платит комиссию'",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ def main():
|
|||||||
gross_amount = args.amount * (1 + commission_rate)
|
gross_amount = args.amount * (1 + commission_rate)
|
||||||
commission = gross_amount - args.amount
|
commission = gross_amount - args.amount
|
||||||
|
|
||||||
print(f"🔄 Симулируется сценарий 'клиент платит комиссию':")
|
print(f"customer pays fee:")
|
||||||
print(f" Сумма заказа: {args.amount} RUB")
|
print(f" Сумма заказа: {args.amount} RUB")
|
||||||
print(f" Комиссия: {commission:.2f} RUB")
|
print(f" Комиссия: {commission:.2f} RUB")
|
||||||
print(f" Итого к оплате: {gross_amount:.2f} RUB")
|
print(f" Итого к оплате: {gross_amount:.2f} RUB")
|
||||||
@@ -75,9 +76,7 @@ def main():
|
|||||||
commission = 0.00
|
commission = 0.00
|
||||||
|
|
||||||
# Сигнатура рассчитывается с использованием InvId (который содержит bill ID)
|
# Сигнатура рассчитывается с использованием InvId (который содержит bill ID)
|
||||||
signature = calculate_signature(
|
signature = calculate_signature(f"{gross_amount:.2f}", inv_id, pally_token)
|
||||||
f"{gross_amount:.2f}", inv_id, pally_token
|
|
||||||
)
|
|
||||||
|
|
||||||
# Используем правильные имена полей согласно документации pally.info
|
# Используем правильные имена полей согласно документации pally.info
|
||||||
payload = {
|
payload = {
|
||||||
|
|||||||
Reference in New Issue
Block a user