diff --git a/app/handlers/balance/main.py b/app/handlers/balance/main.py index 72669dd6..3d50943c 100644 --- a/app/handlers/balance/main.py +++ b/app/handlers/balance/main.py @@ -94,15 +94,38 @@ async def show_balance_menu( db: AsyncSession ): texts = get_texts(db_user.language) - + balance_text = texts.BALANCE_INFO.format( balance=texts.format_price(db_user.balance_kopeks) ) - - await callback.message.edit_text( - balance_text, - reply_markup=get_balance_keyboard(db_user.language) - ) + + reply_markup = get_balance_keyboard(db_user.language) + + try: + if callback.message and callback.message.text: + await callback.message.edit_text( + balance_text, + reply_markup=reply_markup + ) + elif callback.message and callback.message.caption: + await callback.message.edit_caption( + balance_text, + reply_markup=reply_markup + ) + else: + await callback.message.answer( + balance_text, + reply_markup=reply_markup + ) + except TelegramBadRequest as error: + logger.warning( + "Failed to edit balance message, sending a new one instead: %s", + error, + ) + await callback.message.answer( + balance_text, + reply_markup=reply_markup + ) await callback.answer() diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json index ecb1e887..ab1b9dcd 100644 --- a/app/localization/locales/en.json +++ b/app/localization/locales/en.json @@ -1087,6 +1087,8 @@ "PAYMENT_METHOD_SUPPORT_NAME": "🛠️ Support team", "PAYMENT_METHOD_TRIBUTE_DESCRIPTION": "via Tribute", "PAYMENT_METHOD_TRIBUTE_NAME": "💳 Bank card", + "PAYMENT_METHOD_HELEKET_DESCRIPTION": "via Heleket", + "PAYMENT_METHOD_HELEKET_NAME": "🪙 Cryptocurrency (Heleket)", "PAYMENT_METHOD_YOOKASSA_DESCRIPTION": "via YooKassa", "PAYMENT_METHOD_YOOKASSA_NAME": "💳 Bank card", "PAYMENT_METHOD_YOOKASSA_SBP_DESCRIPTION": "via YooKassa Fast Payment System", diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json index 87aa2c67..5a24611e 100644 --- a/app/localization/locales/ru.json +++ b/app/localization/locales/ru.json @@ -1099,6 +1099,8 @@ "PAYMENT_METHOD_SUPPORT_NAME": "🛠️ Через поддержку", "PAYMENT_METHOD_TRIBUTE_DESCRIPTION": "через Tribute", "PAYMENT_METHOD_TRIBUTE_NAME": "💳 Банковская карта", + "PAYMENT_METHOD_HELEKET_DESCRIPTION": "через Heleket", + "PAYMENT_METHOD_HELEKET_NAME": "🪙 Криптовалюта (Heleket)", "PAYMENT_METHOD_YOOKASSA_DESCRIPTION": "через YooKassa", "PAYMENT_METHOD_YOOKASSA_NAME": "💳 Банковская карта", "PAYMENT_METHOD_YOOKASSA_SBP_DESCRIPTION": "через систему быстрых платежей YooKassa", diff --git a/app/localization/locales/ua.json b/app/localization/locales/ua.json index d291f56a..2a419cc2 100644 --- a/app/localization/locales/ua.json +++ b/app/localization/locales/ua.json @@ -1093,10 +1093,12 @@ "PAYMENT_METHOD_PLATEGA_NAME": "💳 Банківська картка (Platega)", "PAYMENT_METHOD_STARS_DESCRIPTION": "швидко та зручно", "PAYMENT_METHOD_STARS_NAME": "⭐ Telegram Stars", - "PAYMENT_METHOD_SUPPORT_DESCRIPTION": "інші способи", - "PAYMENT_METHOD_SUPPORT_NAME": "🛠️ Через підтримку", +"PAYMENT_METHOD_SUPPORT_DESCRIPTION": "інші способи", +"PAYMENT_METHOD_SUPPORT_NAME": "🛠️ Через підтримку", "PAYMENT_METHOD_TRIBUTE_DESCRIPTION": "через Tribute", "PAYMENT_METHOD_TRIBUTE_NAME": "💳 Банківська картка", +"PAYMENT_METHOD_HELEKET_DESCRIPTION": "через Heleket", +"PAYMENT_METHOD_HELEKET_NAME": "🪙 Криптовалюта (Heleket)", "PAYMENT_METHOD_YOOKASSA_DESCRIPTION": "через YooKassa", "PAYMENT_METHOD_YOOKASSA_NAME": "💳 Банківська картка", "PAYMENT_METHOD_YOOKASSA_SBP_DESCRIPTION": "через систему швидких платежів YooKassa", diff --git a/app/localization/locales/zh.json b/app/localization/locales/zh.json index d6c32b4e..235a9b97 100644 --- a/app/localization/locales/zh.json +++ b/app/localization/locales/zh.json @@ -1096,6 +1096,8 @@ "PAYMENT_METHOD_SUPPORT_NAME":"🛠️通过支持", "PAYMENT_METHOD_TRIBUTE_DESCRIPTION":"通过Tribute", "PAYMENT_METHOD_TRIBUTE_NAME":"💳银行卡", +"PAYMENT_METHOD_HELEKET_DESCRIPTION":"通过Heleket", +"PAYMENT_METHOD_HELEKET_NAME":"🪙加密货币(Heleket)", "PAYMENT_METHOD_YOOKASSA_DESCRIPTION":"通过YooKassa", "PAYMENT_METHOD_YOOKASSA_NAME":"💳银行卡", "PAYMENT_METHOD_YOOKASSA_SBP_DESCRIPTION":"通过YooKassa快速支付系统", diff --git a/app/webapi/routes/promo_offers.py b/app/webapi/routes/promo_offers.py index 5613048d..87f03cb2 100644 --- a/app/webapi/routes/promo_offers.py +++ b/app/webapi/routes/promo_offers.py @@ -17,6 +17,7 @@ from app.database.crud.promo_offer_template import ( list_promo_offer_templates, update_promo_offer_template, ) +from app.database.crud.user import get_user_by_telegram_id from app.database.models import DiscountOffer, PromoOfferLog, PromoOfferTemplate, Subscription, User from ..dependencies import get_db_session, require_api_token @@ -144,20 +145,35 @@ async def list_promo_offers( limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0), user_id: Optional[int] = Query(None, ge=1), + telegram_id: Optional[int] = Query(None, ge=1), notification_type: Optional[str] = Query(None, min_length=1), is_active: Optional[bool] = Query(None), ) -> PromoOfferListResponse: + resolved_user_id = user_id + if telegram_id is not None: + user = await get_user_by_telegram_id(db, telegram_id) + if not user: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail="User not found") + + if resolved_user_id and resolved_user_id != user.id: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail="telegram_id does not match the provided user_id", + ) + + resolved_user_id = user.id + offers = await list_discount_offers( db, offset=offset, limit=limit, - user_id=user_id, + user_id=resolved_user_id, notification_type=notification_type, is_active=is_active, ) total = await count_discount_offers( db, - user_id=user_id, + user_id=resolved_user_id, notification_type=notification_type, is_active=is_active, ) @@ -187,7 +203,26 @@ async def create_promo_offer( if not payload.effect_type.strip(): raise HTTPException(status.HTTP_400_BAD_REQUEST, "effect_type must not be empty") - user = await db.get(User, payload.user_id) + target_user_id = payload.user_id + user: Optional[User] = None + if payload.telegram_id is not None: + user = await get_user_by_telegram_id(db, payload.telegram_id) + if not user: + raise HTTPException(status.HTTP_404_NOT_FOUND, "User not found") + + if target_user_id and target_user_id != user.id: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + "Provided user_id does not match telegram_id", + ) + + target_user_id = user.id + + if target_user_id is None: + raise HTTPException(status.HTTP_400_BAD_REQUEST, "user_id or telegram_id is required") + + if user is None: + user = await db.get(User, target_user_id) if not user: raise HTTPException(status.HTTP_404_NOT_FOUND, "User not found") @@ -195,12 +230,12 @@ async def create_promo_offer( subscription = await db.get(Subscription, payload.subscription_id) if not subscription: raise HTTPException(status.HTTP_404_NOT_FOUND, "Subscription not found") - if subscription.user_id != payload.user_id: + if subscription.user_id != target_user_id: raise HTTPException(status.HTTP_400_BAD_REQUEST, "Subscription does not belong to the user") offer = await upsert_discount_offer( db, - user_id=payload.user_id, + user_id=target_user_id, subscription_id=payload.subscription_id, notification_type=payload.notification_type.strip(), discount_percent=payload.discount_percent, diff --git a/app/webapi/schemas/promo_offers.py b/app/webapi/schemas/promo_offers.py index 9724e820..9dd83c32 100644 --- a/app/webapi/schemas/promo_offers.py +++ b/app/webapi/schemas/promo_offers.py @@ -50,7 +50,8 @@ class PromoOfferListResponse(BaseModel): class PromoOfferCreateRequest(BaseModel): - user_id: int + user_id: Optional[int] = Field(None, ge=1) + telegram_id: Optional[int] = Field(None, ge=1) notification_type: str = Field(..., min_length=1) valid_hours: int = Field(..., ge=1, description="Срок действия предложения в часах") discount_percent: int = Field(0, ge=0) diff --git a/docs/web-admin-integration.md b/docs/web-admin-integration.md index 4f31ed9e..3b8cec29 100644 --- a/docs/web-admin-integration.md +++ b/docs/web-admin-integration.md @@ -128,7 +128,7 @@ curl -X POST "http://127.0.0.1:8080/tokens" \ | `PATCH` | `/promo-groups/{id}` | Обновить промо-группу. | `DELETE` | `/promo-groups/{id}` | Удалить промо-группу. | `GET` | `/promo-offers` | Список промо-предложений с фильтрами по пользователю, статусу и типу уведомления. -| `POST` | `/promo-offers` | Создать или обновить персональное промо-предложение пользователю. +| `POST` | `/promo-offers` | Создать или обновить персональное промо-предложение пользователю. ID может быть как внутренним (user.id), так и Telegram ID (user.telegram_id). | `GET` | `/promo-offers/{id}` | Детали конкретного промо-предложения. | `GET` | `/promo-offers/templates` | Список шаблонов промо-предложений. | `GET` | `/promo-offers/templates/{id}` | Получить данные шаблона промо-предложения.