Files
remnawave-bedolaga-telegram…/app/database/crud/promo_offer_template.py
T
Fringg eb18994b7d fix: complete datetime.utcnow() → datetime.now(UTC) migration
- Migrate 660+ datetime.utcnow() across 153 files to datetime.now(UTC)
- Migrate 30+ datetime.now() without UTC to datetime.now(UTC)
- Convert all 170 DateTime columns to DateTime(timezone=True)
- Add migrate_datetime_to_timestamptz() in universal_migration with SET LOCAL timezone='UTC' safety
- Remove 70+ .replace(tzinfo=None) workarounds
- Fix utcfromtimestamp → fromtimestamp(..., tz=UTC)
- Fix fromtimestamp() without tz= (system_logs, backup_service, referral_diagnostics)
- Fix fromisoformat/isoparse to ensure aware output (platega, yookassa, wata, miniapp, nalogo)
- Fix strptime() to add .replace(tzinfo=UTC) (backup_service, referral_diagnostics)
- Fix datetime.combine() to include tzinfo=UTC (remnawave_sync, traffic_monitoring)
- Fix datetime.max/datetime.min sentinels with .replace(tzinfo=UTC)
- Rename panel_datetime_to_naive_utc → panel_datetime_to_utc
- Remove DTZ003 from ruff ignore list
2026-02-17 04:45:40 +03:00

212 lines
8.7 KiB
Python

from __future__ import annotations
from collections.abc import Iterable
from datetime import UTC, datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import PromoOfferTemplate
UPDATED_TEMPLATE_MESSAGES = {
'extend_discount': (
'💎 Экономия {discount_percent}% при продлении\n\n'
'Скидка суммируется с промогруппой и действует один раз.\n'
'Срок действия предложения — {valid_hours} ч.\n'
'После активации скидка действует {active_discount_hours} ч.'
),
'purchase_discount': (
'🎯 Вернитесь со скидкой {discount_percent}%\n\n'
'Скидка суммируется с промогруппой и действует один раз.\n'
'Предложение действует {valid_hours} ч.\n'
'После активации скидка действует {active_discount_hours} ч.'
),
}
LEGACY_TEMPLATE_MESSAGES = {
'extend_discount': (
'💎 <b>Экономия {discount_percent}% при продлении</b>\n\n'
'Активируйте предложение и получите дополнительную скидку на оплату продления. '
'Она суммируется с вашими промогрупповыми скидками и действует один раз.\n'
'Срок действия предложения — {valid_hours} ч.\n'
'После активации скидка действует {active_discount_hours} ч.'
),
'purchase_discount': (
'🎯 <b>Вернитесь со скидкой {discount_percent}%</b>\n\n'
'После активации мы применим дополнительную скидку к вашей следующей оплате подписки. '
'Скидка суммируется с промогруппой и действует один раз.\n'
'Предложение действует {valid_hours} ч.\n'
'После активации скидка действует {active_discount_hours} ч.'
),
}
DEFAULT_TEMPLATES: tuple[dict, ...] = (
{
'offer_type': 'test_access',
'name': 'Тестовые сервера',
'message_text': (
'🔥 <b>Испытайте новые сервера</b>\n\n'
'Активируйте предложение и получите временный доступ к дополнительным сквадам на {test_duration_hours} ч.\n'
'Предложение действительно {valid_hours} ч.'
),
'button_text': '🚀 Попробовать серверы',
'valid_hours': 24,
'discount_percent': 0,
'bonus_amount_kopeks': 0,
'active_discount_hours': None,
'test_duration_hours': 24,
'test_squad_uuids': [],
},
{
'offer_type': 'extend_discount',
'name': 'Скидка на продление',
'message_text': UPDATED_TEMPLATE_MESSAGES['extend_discount'],
'button_text': '🎁 Получить скидку',
'valid_hours': 24,
'discount_percent': 20,
'bonus_amount_kopeks': 0,
'active_discount_hours': 24,
'test_duration_hours': None,
'test_squad_uuids': [],
},
{
'offer_type': 'purchase_discount',
'name': 'Скидка на покупку',
'message_text': UPDATED_TEMPLATE_MESSAGES['purchase_discount'],
'button_text': '🎁 Забрать скидку',
'valid_hours': 48,
'discount_percent': 25,
'bonus_amount_kopeks': 0,
'active_discount_hours': 48,
'test_duration_hours': None,
'test_squad_uuids': [],
},
)
def _format_template_fields(payload: dict) -> dict:
data = dict(payload)
data.setdefault('valid_hours', 24)
data.setdefault('discount_percent', 0)
data.setdefault('bonus_amount_kopeks', 0)
data.setdefault('active_discount_hours', None)
data.setdefault('test_duration_hours', None)
data.setdefault('test_squad_uuids', [])
return data
async def ensure_default_templates(db: AsyncSession, *, created_by: int | None = None) -> list[PromoOfferTemplate]:
templates: list[PromoOfferTemplate] = []
for template_data in DEFAULT_TEMPLATES:
result = await db.execute(
select(PromoOfferTemplate).where(PromoOfferTemplate.offer_type == template_data['offer_type'])
)
existing = result.scalars().first()
if existing:
new_message = UPDATED_TEMPLATE_MESSAGES.get(template_data['offer_type'])
legacy_message = LEGACY_TEMPLATE_MESSAGES.get(template_data['offer_type'])
should_update = False
if (new_message and legacy_message and existing.message_text == legacy_message) or (
new_message and ('{bonus_amount' in existing.message_text or 'Мы начислим' in existing.message_text)
):
should_update = True
if should_update and new_message:
existing.message_text = new_message
existing.updated_at = datetime.now(UTC)
await db.flush()
target_active_hours = template_data.get('active_discount_hours')
if target_active_hours is not None and target_active_hours > 0 and not existing.active_discount_hours:
existing.active_discount_hours = target_active_hours
existing.updated_at = datetime.now(UTC)
await db.flush()
templates.append(existing)
continue
payload = _format_template_fields(template_data)
template = PromoOfferTemplate(
name=payload['name'],
offer_type=payload['offer_type'],
message_text=payload['message_text'],
button_text=payload['button_text'],
valid_hours=payload['valid_hours'],
discount_percent=payload['discount_percent'],
bonus_amount_kopeks=payload['bonus_amount_kopeks'],
active_discount_hours=payload['active_discount_hours'],
test_duration_hours=payload['test_duration_hours'],
test_squad_uuids=payload['test_squad_uuids'],
is_active=True,
created_by=created_by,
)
db.add(template)
await db.flush()
templates.append(template)
await db.commit()
return templates
async def list_promo_offer_templates(db: AsyncSession) -> list[PromoOfferTemplate]:
result = await db.execute(select(PromoOfferTemplate).order_by(PromoOfferTemplate.offer_type, PromoOfferTemplate.id))
return result.scalars().all()
async def get_promo_offer_template_by_id(db: AsyncSession, template_id: int) -> PromoOfferTemplate | None:
result = await db.execute(select(PromoOfferTemplate).where(PromoOfferTemplate.id == template_id))
return result.scalar_one_or_none()
async def get_promo_offer_template_by_type(db: AsyncSession, offer_type: str) -> PromoOfferTemplate | None:
result = await db.execute(select(PromoOfferTemplate).where(PromoOfferTemplate.offer_type == offer_type))
return result.scalar_one_or_none()
async def update_promo_offer_template(
db: AsyncSession,
template: PromoOfferTemplate,
*,
name: str | None = None,
message_text: str | None = None,
button_text: str | None = None,
valid_hours: int | None = None,
discount_percent: int | None = None,
bonus_amount_kopeks: int | None = None,
active_discount_hours: int | None = None,
test_duration_hours: int | None = None,
test_squad_uuids: Iterable[str] | None = None,
is_active: bool | None = None,
) -> PromoOfferTemplate:
if name is not None:
template.name = name
if message_text is not None:
template.message_text = message_text
if button_text is not None:
template.button_text = button_text
if valid_hours is not None:
template.valid_hours = valid_hours
if discount_percent is not None:
template.discount_percent = discount_percent
if bonus_amount_kopeks is not None:
template.bonus_amount_kopeks = bonus_amount_kopeks
if active_discount_hours is not None:
template.active_discount_hours = active_discount_hours
if test_duration_hours is not None:
template.test_duration_hours = test_duration_hours
if test_squad_uuids is not None:
template.test_squad_uuids = list(test_squad_uuids)
if is_active is not None:
template.is_active = is_active
template.updated_at = datetime.now(UTC)
await db.commit()
await db.refresh(template)
return template