@@ -127,6 +127,126 @@ async def show_ad_card(callback: types.CallbackQuery, settings: Settings, i18n_d
|
|||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("admin_ads:delete:"))
|
||||||
|
async def ads_delete_prompt(callback: types.CallbackQuery, settings: Settings, i18n_data: dict):
|
||||||
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||||
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||||
|
if not i18n or not callback.message:
|
||||||
|
await callback.answer("Language error.", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
_, _, camp_id_str, back_page_str = callback.data.split(":", 3)
|
||||||
|
camp_id = int(camp_id_str)
|
||||||
|
back_page = int(back_page_str)
|
||||||
|
except Exception:
|
||||||
|
await callback.answer(i18n.gettext(current_lang, "error_try_again"), show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
from bot.keyboards.inline.admin_keyboards import get_confirmation_keyboard
|
||||||
|
confirm_text = i18n.gettext(current_lang, "admin_ads_delete_confirm", id=camp_id)
|
||||||
|
kb = get_confirmation_keyboard(
|
||||||
|
yes_callback_data=f"admin_ads:delete_confirm:{camp_id}:{back_page}",
|
||||||
|
no_callback_data=f"admin_ads:delete_cancel:{camp_id}:{back_page}",
|
||||||
|
i18n_instance=i18n,
|
||||||
|
lang=current_lang,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await callback.message.edit_text(confirm_text, reply_markup=kb)
|
||||||
|
await callback.answer()
|
||||||
|
except Exception:
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("admin_ads:delete_cancel:"))
|
||||||
|
async def ads_delete_cancel(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession):
|
||||||
|
# Return to the ad card view
|
||||||
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||||
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||||
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||||
|
if not i18n or not callback.message:
|
||||||
|
await callback.answer("Language error.", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
parts = callback.data.split(":", 3)
|
||||||
|
camp_id = int(parts[2])
|
||||||
|
back_page = int(parts[3])
|
||||||
|
except Exception:
|
||||||
|
await callback.answer(_("error_try_again"), show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
camp = await ad_dal.get_campaign_by_id(session, camp_id)
|
||||||
|
if not camp:
|
||||||
|
await callback.answer(_("admin_ads_not_found", default="Кампания не найдена."), show_alert=True)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
stats = await ad_dal.get_campaign_stats(session, camp_id)
|
||||||
|
except Exception:
|
||||||
|
stats = {"starts": 0, "trials": 0, "payers": 0, "revenue": 0.0}
|
||||||
|
text = _(
|
||||||
|
"admin_ads_card",
|
||||||
|
id=camp.ad_campaign_id,
|
||||||
|
source=camp.source,
|
||||||
|
start_param=camp.start_param,
|
||||||
|
cost=f"{camp.cost:.2f}",
|
||||||
|
active=_("csv_yes") if camp.is_active else _("csv_no"),
|
||||||
|
starts=stats["starts"],
|
||||||
|
trials=stats["trials"],
|
||||||
|
payers=stats["payers"],
|
||||||
|
revenue=f"{stats['revenue']:.2f}",
|
||||||
|
)
|
||||||
|
from bot.keyboards.inline.admin_keyboards import get_ad_card_keyboard
|
||||||
|
reply_markup = get_ad_card_keyboard(i18n, current_lang, camp.ad_campaign_id, back_page)
|
||||||
|
try:
|
||||||
|
await callback.message.edit_text(text, reply_markup=reply_markup, parse_mode="HTML")
|
||||||
|
await callback.answer()
|
||||||
|
except Exception:
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("admin_ads:delete_confirm:"))
|
||||||
|
async def ads_delete_confirm(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession):
|
||||||
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||||
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||||
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||||
|
if not i18n or not callback.message:
|
||||||
|
await callback.answer("Language error.", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
parts = callback.data.split(":", 3)
|
||||||
|
camp_id = int(parts[2])
|
||||||
|
back_page = int(parts[3])
|
||||||
|
except Exception:
|
||||||
|
await callback.answer(_("error_try_again"), show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
existed = await ad_dal.delete_campaign(session, camp_id)
|
||||||
|
if not existed:
|
||||||
|
await callback.answer(_("admin_ads_not_found", default="Кампания не найдена."), show_alert=True)
|
||||||
|
return
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
# After delete, show list page (may shift due to fewer items)
|
||||||
|
totals = await ad_dal.get_totals(session)
|
||||||
|
overview = _(
|
||||||
|
"admin_ads_overview",
|
||||||
|
revenue=f"{totals.get('revenue', 0.0):.2f}",
|
||||||
|
cost=f"{totals.get('cost', 0.0):.2f}",
|
||||||
|
)
|
||||||
|
total_count = await ad_dal.count_campaigns(session)
|
||||||
|
total_pages = max(1, (total_count + PAGE_SIZE - 1) // PAGE_SIZE)
|
||||||
|
page = max(0, min(back_page, total_pages - 1))
|
||||||
|
campaigns = await ad_dal.list_campaigns_paged(session, page=page, page_size=PAGE_SIZE)
|
||||||
|
text = overview + "\n\n" + _("admin_ads_header")
|
||||||
|
from bot.keyboards.inline.admin_keyboards import get_ads_list_keyboard
|
||||||
|
reply_markup = get_ads_list_keyboard(i18n, current_lang, campaigns, page, total_pages)
|
||||||
|
try:
|
||||||
|
await callback.message.edit_text(text, reply_markup=reply_markup)
|
||||||
|
await callback.answer(_("admin_ads_deleted_success"), show_alert=True)
|
||||||
|
except Exception:
|
||||||
|
await callback.answer(_("admin_ads_deleted_success"), show_alert=True)
|
||||||
@router.callback_query(F.data == "admin_action:ads_create")
|
@router.callback_query(F.data == "admin_action:ads_create")
|
||||||
async def ads_create_start(callback: types.CallbackQuery, state: FSMContext, settings: Settings, i18n_data: dict):
|
async def ads_create_start(callback: types.CallbackQuery, state: FSMContext, settings: Settings, i18n_data: dict):
|
||||||
from bot.states.admin_states import AdminStates
|
from bot.states.admin_states import AdminStates
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ async def my_subscription_command_handler(
|
|||||||
# Build rows to prepend above the base "back" markup
|
# Build rows to prepend above the base "back" markup
|
||||||
prepend_rows = []
|
prepend_rows = []
|
||||||
|
|
||||||
# 1) Mini-app connect button on top if enabled
|
# 1) Mini-app connect button on top if enabled, otherwise fall back to config link URL
|
||||||
if settings.SUBSCRIPTION_MINI_APP_URL:
|
if settings.SUBSCRIPTION_MINI_APP_URL:
|
||||||
prepend_rows.append([
|
prepend_rows.append([
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
@@ -163,6 +163,15 @@ async def my_subscription_command_handler(
|
|||||||
web_app=WebAppInfo(url=settings.SUBSCRIPTION_MINI_APP_URL),
|
web_app=WebAppInfo(url=settings.SUBSCRIPTION_MINI_APP_URL),
|
||||||
)
|
)
|
||||||
])
|
])
|
||||||
|
else:
|
||||||
|
cfg_link_val = (active or {}).get("config_link")
|
||||||
|
if cfg_link_val:
|
||||||
|
prepend_rows.append([
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=get_text("connect_button"),
|
||||||
|
url=cfg_link_val,
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
# 2) Auto-renew toggle (if supported and not tribute)
|
# 2) Auto-renew toggle (if supported and not tribute)
|
||||||
if local_sub and local_sub.provider != "tribute" and getattr(settings, 'YOOKASSA_AUTOPAYMENTS_ENABLED', False):
|
if local_sub and local_sub.provider != "tribute" and getattr(settings, 'YOOKASSA_AUTOPAYMENTS_ENABLED', False):
|
||||||
|
|||||||
@@ -185,6 +185,9 @@ def get_ads_list_keyboard(
|
|||||||
def get_ad_card_keyboard(i18n_instance, lang: str, campaign_id: int, back_page: int) -> InlineKeyboardMarkup:
|
def get_ad_card_keyboard(i18n_instance, lang: str, campaign_id: int, back_page: int) -> InlineKeyboardMarkup:
|
||||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
# Dangerous action: Delete campaign
|
||||||
|
builder.button(text=_(key="admin_ads_delete_button", default="🗑 Удалить кампанию"),
|
||||||
|
callback_data=f"admin_ads:delete:{campaign_id}:{back_page}")
|
||||||
builder.button(text=_(key="back_to_ads_list_button", default="⬅️ К списку"),
|
builder.button(text=_(key="back_to_ads_list_button", default="⬅️ К списку"),
|
||||||
callback_data=f"admin_ads:page:{back_page}")
|
callback_data=f"admin_ads:page:{back_page}")
|
||||||
builder.button(text=_(key="back_to_admin_panel_button"),
|
builder.button(text=_(key="back_to_admin_panel_button"),
|
||||||
|
|||||||
@@ -162,3 +162,21 @@ async def get_totals(session: AsyncSession) -> Dict[str, float]:
|
|||||||
return {"cost": total_cost, "revenue": total_revenue}
|
return {"cost": total_cost, "revenue": total_revenue}
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_campaign(session: AsyncSession, campaign_id: int) -> bool:
|
||||||
|
"""Delete ad campaign by id along with related attributions.
|
||||||
|
|
||||||
|
Returns True if campaign existed and was deleted, False otherwise.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
campaign = await session.get(AdCampaign, campaign_id)
|
||||||
|
if not campaign:
|
||||||
|
return False
|
||||||
|
await session.delete(campaign)
|
||||||
|
await session.flush()
|
||||||
|
logging.info(f"AdCampaign deleted id={campaign_id}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to delete AdCampaign id={campaign_id}: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,15 @@ async def get_active_subscription_by_user_id(
|
|||||||
user_id: int,
|
user_id: int,
|
||||||
panel_user_uuid: Optional[str] = None) -> Optional[Subscription]:
|
panel_user_uuid: Optional[str] = None) -> Optional[Subscription]:
|
||||||
stmt = select(Subscription).where(
|
stmt = select(Subscription).where(
|
||||||
Subscription.user_id == user_id, Subscription.is_active == True,
|
Subscription.user_id == user_id,
|
||||||
Subscription.end_date > datetime.now(timezone.utc))
|
Subscription.is_active == True,
|
||||||
|
Subscription.end_date > datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
if panel_user_uuid:
|
if panel_user_uuid:
|
||||||
stmt = stmt.where(Subscription.panel_user_uuid == panel_user_uuid)
|
stmt = stmt.where(Subscription.panel_user_uuid == panel_user_uuid)
|
||||||
stmt = stmt.order_by(Subscription.end_date.desc())
|
stmt = stmt.order_by(Subscription.end_date.desc()).limit(1)
|
||||||
result = await session.execute(stmt)
|
result = await session.execute(stmt)
|
||||||
return result.scalar_one_or_none()
|
return result.scalars().first()
|
||||||
|
|
||||||
|
|
||||||
async def get_subscription_by_panel_subscription_uuid(
|
async def get_subscription_by_panel_subscription_uuid(
|
||||||
|
|||||||
+5
-1
@@ -428,5 +428,9 @@
|
|||||||
"admin_ads_back_to_menu_hint": "Done. Back to Ads section:",
|
"admin_ads_back_to_menu_hint": "Done. Back to Ads section:",
|
||||||
"admin_ads_overview": "📈 <b>Ads</b>\n💰 Revenue: <b>{revenue} RUB</b>\n💸 Spent: <b>{cost} RUB</b>",
|
"admin_ads_overview": "📈 <b>Ads</b>\n💰 Revenue: <b>{revenue} RUB</b>\n💸 Spent: <b>{cost} RUB</b>",
|
||||||
"back_to_ads_list_button": "⬅️ Back to list",
|
"back_to_ads_list_button": "⬅️ Back to list",
|
||||||
"admin_ads_card": "📈 <b>Campaign #{id}</b>\nSource: <b>{source}</b>\nstart=<code>{start_param}</code>\nCost: <b>{cost} RUB</b>\nActive: {active}\n\n👥 Starts: <b>{starts}</b>\n🆓 Trials: <b>{trials}</b>\n💳 Payers: <b>{payers}</b>\n💵 Revenue: <b>{revenue} RUB</b>"
|
"admin_ads_card": "📈 <b>Campaign #{id}</b>\nSource: <b>{source}</b>\nstart=<code>{start_param}</code>\nCost: <b>{cost} RUB</b>\nActive: {active}\n\n👥 Starts: <b>{starts}</b>\n🆓 Trials: <b>{trials}</b>\n💳 Payers: <b>{payers}</b>\n💵 Revenue: <b>{revenue} RUB</b>",
|
||||||
|
"admin_ads_delete_button": "🗑 Delete campaign",
|
||||||
|
"admin_ads_delete_confirm": "Are you sure you want to delete campaign #{id}? This action is irreversible.",
|
||||||
|
"admin_ads_deleted_success": "Campaign deleted.",
|
||||||
|
"admin_ads_not_found": "Campaign not found."
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -427,5 +427,9 @@
|
|||||||
"admin_ads_back_to_menu_hint": "Готово. Вернуться к разделу рекламы:",
|
"admin_ads_back_to_menu_hint": "Готово. Вернуться к разделу рекламы:",
|
||||||
"admin_ads_overview": "📈 <b>Реклама</b>\n💰 Пришло: <b>{revenue} RUB</b>\n💸 Потрачено: <b>{cost} RUB</b>",
|
"admin_ads_overview": "📈 <b>Реклама</b>\n💰 Пришло: <b>{revenue} RUB</b>\n💸 Потрачено: <b>{cost} RUB</b>",
|
||||||
"back_to_ads_list_button": "⬅️ К списку",
|
"back_to_ads_list_button": "⬅️ К списку",
|
||||||
"admin_ads_card": "📈 <b>Кампания #{id}</b>\nИсточник: <b>{source}</b>\nstart=<code>{start_param}</code>\nСтоимость: <b>{cost} RUB</b>\nАктивна: {active}\n\n👥 Запустили: <b>{starts}</b>\n🆓 Взяли триал: <b>{trials}</b>\n💳 Оплатили: <b>{payers}</b>\n💵 Доход: <b>{revenue} RUB</b>"
|
"admin_ads_card": "📈 <b>Кампания #{id}</b>\nИсточник: <b>{source}</b>\nstart=<code>{start_param}</code>\nСтоимость: <b>{cost} RUB</b>\nАктивна: {active}\n\n👥 Запустили: <b>{starts}</b>\n🆓 Взяли триал: <b>{trials}</b>\n💳 Оплатили: <b>{payers}</b>\n💵 Доход: <b>{revenue} RUB</b>",
|
||||||
|
"admin_ads_delete_button": "🗑 Удалить кампанию",
|
||||||
|
"admin_ads_delete_confirm": "Вы уверены, что хотите удалить кампанию #{id}? Это действие необратимо.",
|
||||||
|
"admin_ads_deleted_success": "Кампания удалена.",
|
||||||
|
"admin_ads_not_found": "Кампания не найдена."
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user