""" Promo Service — логика промокодов и депозитов. """ from datetime import datetime, timedelta import random from typing import Optional from sqlalchemy.orm import Session from database import User, PromoCode, TransactionLog from bank_api import bank_client from promo_phase import get_promo_effect_for_user, get_current_phase DAILY_DEPOSIT_LIMIT = 999999999.0 # без лимита # ── Авто-генерация промокодов ── PROMO_WORDS = ["storm", "glitch", "frost", "blaze", "cipher", "nexus", "phantom", "prism"] PROMO_PERCENTS = [10, 15, 20, 21, 24, 25] # Хранилище текущего авто-промокода (in-memory кэш) _current_auto_promo: Optional[dict] = None def generate_auto_promo_code() -> str: """Генерирует промокод формата слово-процент, например 'storm-21'""" word = random.choice(PROMO_WORDS) pct = random.choice(PROMO_PERCENTS) return f"{word}-{pct}" def get_or_create_auto_promo(db: Session) -> PromoCode: """ Возвращает активный авто-сгенерированный промокод. Если его нет или он истёк (>1ч) — создаёт новый. """ global _current_auto_promo now = datetime.utcnow() # Ищем существующий авто-промокод, созданный <1 часа назад candidates = db.query(PromoCode).filter( PromoCode.reward_type == "card_to_site", PromoCode.is_active == True, PromoCode.created_by == -1 # маркер авто-генерации ).order_by(PromoCode.created_at.desc()).limit(1).all() if candidates: promo = candidates[0] age = (now - promo.created_at).total_seconds() if age < 3600: # меньше часа — живой _current_auto_promo = { "code": promo.code, "reward_amount": promo.reward_amount, "created_at": promo.created_at.isoformat(), } return promo # Создаём новый code = generate_auto_promo_code() pct = int(code.split("-")[1]) reward = pct # процент от daily лимита как сумма бонуса new_promo = PromoCode( code=code, reward_type="card_to_site", reward_amount=reward, reward_data="auto", max_uses=999, used_count=0, start_date=now, end_date=now + timedelta(hours=1), is_active=True, created_by=-1, ) db.add(new_promo) db.commit() _current_auto_promo = { "code": new_promo.code, "reward_amount": new_promo.reward_amount, "created_at": new_promo.created_at.isoformat(), } return new_promo def get_current_auto_promo(db: Session) -> Optional[dict]: """Возвращает информацию о текущем авто-промокоде.""" promo = get_or_create_auto_promo(db) now = datetime.utcnow() age = (now - promo.created_at).total_seconds() remaining = max(0, 3600 - age) return { "code": promo.code, "reward_amount": promo.reward_amount, "created_at": promo.created_at.isoformat(), "expires_in_seconds": int(remaining), "used_count": promo.used_count, } def validate_promo_code(db: Session, code: str) -> Optional[dict]: """Проверяет промокод, возвращает данные или None""" promo = db.query(PromoCode).filter( PromoCode.code == code, PromoCode.is_active == True ).first() if not promo: return None now = datetime.utcnow() if promo.end_date and now > promo.end_date: return {"error": "Срок действия промокода истёк"} if promo.start_date and now < promo.start_date: return {"error": "Промокод ещё не активен"} if promo.max_uses > 0 and promo.used_count >= promo.max_uses: return {"error": "Промокод уже использован максимальное количество раз"} return { "id": promo.id, "code": promo.code, "reward_type": promo.reward_type, "reward_amount": promo.reward_amount, "reward_data": promo.reward_data, } async def activate_promo_code(db: Session, user_id: int, code: str, amount: float = 0) -> dict: """ Активирует промокод. Типы: card_to_site, luck_boost, ceiling_boost, free_case, reset_streak """ promo_data = validate_promo_code(db, code) if not promo_data: return {"success": False, "error": "Промокод не найден"} if "error" in promo_data: return {"success": False, "error": promo_data["error"]} promo = db.query(PromoCode).filter(PromoCode.id == promo_data["id"]).first() if not promo: return {"success": False, "error": "Ошибка загрузки промокода"} user = db.query(User).filter(User.id == user_id).first() if not user: return {"success": False, "error": "Пользователь не найден"} rpu = user.rpu_settings result = {"success": True, "code": code, "type": promo.reward_type} promo.used_count += 1 # Записываем транзакцию txn = TransactionLog( user_id=user_id, tx_type="promo_bonus", amount=0, promo_code=code, details=f"Промокод: {code} ({promo.reward_type})" ) db.add(txn) if promo.reward_type == "card_to_site": if amount <= 0: return {"success": False, "error": "Укажите сумму депозита"} pct = promo.reward_amount # например 24 → 24% bonus = round(amount * pct / 100, 2) total = round(amount + bonus, 2) # Проверяем баланс карты (требуем только сумму депозита) bank_balance = await bank_client.get_balance(user_id) if bank_balance < amount: return {"success": False, "error": f"Недостаточно средств на карте. Нужно: {amount}₽"} # Списываем с карты только депозит await bank_client.transfer_to_site(user_id, amount, promo_code=code) # Зачисляем на сайт (депозит + бонус от платформы) user.balance += total user.total_deposited += amount txn.amount = total txn.details = f"Депозит {amount}₽ + бонус {pct}% ({bonus}₽) по промокоду {code}" result["amount"] = total result["deposit"] = amount result["bonus"] = bonus result["new_balance"] = user.balance elif promo.reward_type == "luck_boost": boost = promo.reward_amount / 100.0 # например 20 → 0.2 if rpu: rpu.luck_multiplier = min(3.0, rpu.luck_multiplier + boost) result["boost"] = boost elif promo.reward_type == "ceiling_boost": boost_mult = promo.reward_amount / 100.0 + 1.0 # 50 → 1.5 if rpu: rpu.ceiling_multiplier = min(3.0, rpu.ceiling_multiplier * boost_mult) result["ceiling_boost"] = boost_mult elif promo.reward_type == "free_case": # Бесплатное открытие — просто отмечаем в результате result["free_case"] = promo.reward_data or "" elif promo.reward_type == "reset_streak": if rpu: rpu.current_streak = 0 rpu.consecutive_loss_count = 0 rpu.consecutive_loss_value = 0.0 result["reset_streak"] = True db.commit() return result async def direct_deposit(db: Session, user_id: int, amount: float, promo_code: str = "") -> dict: """ Прямой депозит с карты на сайт. Если указан промокод — применяет бонус поверх депозита. """ if amount <= 0: return {"success": False, "error": "Сумма должна быть положительной"} user = db.query(User).filter(User.id == user_id).first() if not user: return {"success": False, "error": "Пользователь не найден"} # Проверка промокода (если указан) bonus = 0 promo_pct = 0 if promo_code: promo_data = validate_promo_code(db, promo_code) if promo_data and "error" not in promo_data: promo = db.query(PromoCode).filter(PromoCode.id == promo_data["id"]).first() if promo and promo.reward_type == "card_to_site": promo_pct = promo.reward_amount bonus = round(amount * promo_pct / 100, 2) promo.used_count += 1 total = round(amount + bonus, 2) # Проверка дневного лимита today = datetime.utcnow().date() daily_total = db.query(TransactionLog).filter( TransactionLog.user_id == user_id, TransactionLog.tx_type == "card_to_site", ).all() today_deposits = sum( t.amount for t in daily_total if t.created_at.date() == today ) if today_deposits + amount > DAILY_DEPOSIT_LIMIT: remaining = max(0, DAILY_DEPOSIT_LIMIT - today_deposits) return { "success": False, "error": f"Дневной лимит депозита: {remaining:.2f}₽" } # Проверяем баланс карты bank_balance = await bank_client.get_balance(user_id) if bank_balance < amount: return {"success": False, "error": "Недостаточно средств на карте"} # Списываем с карты (только депозит, бонус от платформы) await bank_client.transfer_to_site(user_id, amount) # Зачисляем на сайт user.balance += total user.total_deposited += amount user.last_deposit_date = datetime.utcnow() details = f"Прямой депозит: +{amount}₽" if bonus > 0: details += f" + бонус {promo_pct}% ({bonus}₽)" txn = TransactionLog( user_id=user_id, tx_type="card_to_site", amount=total, promo_code=promo_code if promo_code else None, details=details ) db.add(txn) db.commit() return { "success": True, "amount": total, "deposit": amount, "bonus": bonus, "new_balance": user.balance, "daily_remaining": max(0, DAILY_DEPOSIT_LIMIT - today_deposits - amount) } async def withdraw_item(db: Session, user_id: int, inventory_item_id: int) -> dict: """ Вывод скина: продажа предмета из инвентаря → зачисление на карту (с комиссией). """ from database import InventoryItem item = db.query(InventoryItem).filter( InventoryItem.id == inventory_item_id, InventoryItem.user_id == user_id ).first() if not item: return {"success": False, "error": "Предмет не найден в инвентаре"} price = item.price_rub or 0 if price <= 0: return {"success": False, "error": "Предмет не имеет цены"} user = db.query(User).filter(User.id == user_id).first() result = await bank_client.withdraw_to_card(user_id, price) if not result["success"]: return {"success": False, "error": "Ошибка вывода"} # Обновляем статистику пользователя user.total_withdrawn += result["net"] # net = price - fee db.delete(item) txn = TransactionLog( user_id=user_id, tx_type="site_to_card", amount=price, fee=result["fee"], item_name=item.market_hash_name, inventory_item_id=inventory_item_id, details=f"Вывод: {item.market_hash_name} за {price}₽ (комиссия {result['fee']}₽)" ) db.add(txn) db.commit() from rpu import check_withdrawal_penalty check_withdrawal_penalty(db, user_id) return { "success": True, "item_name": item.market_hash_name, "gross": price, "fee": result["fee"], "net": result["net"], "new_card_balance": result["new_card_balance"] }