diff --git a/bot/app/web/frontend/src/App.svelte b/bot/app/web/frontend/src/App.svelte index 7e8f34a..eaf4180 100644 --- a/bot/app/web/frontend/src/App.svelte +++ b/bot/app/web/frontend/src/App.svelte @@ -1171,9 +1171,18 @@ method: selectedMethod, }), }); - if (!response.ok || !response.payment_url) throw response; + if (!response.ok) throw response; showToast(t("wa_payment_created")); - openExternalLink(response.payment_url); + if (response.action === "open_invoice") { + if (!response.payment_url) throw response; + openTelegramInvoice(response.payment_url); + } else if (response.action === "invoice_sent") { + paymentModalOpen = false; + return; + } else { + if (!response.payment_url) throw response; + openExternalLink(response.payment_url); + } paymentModalOpen = false; } catch (error) { showToast(error?.message || t("wa_payment_create_failed")); @@ -1191,6 +1200,22 @@ window.location.assign(url); } + function openTelegramInvoice(url) { + if (!url) return; + if (tg?.openInvoice) { + tg.openInvoice(url, (status) => { + if (status === "paid") { + showToast(t("wa_payment_success", {}, "Payment successful")); + loadData(); + } else if (status === "failed") { + showToast(t("wa_payment_create_failed")); + } + }); + return; + } + openExternalLink(url); + } + function openConnectLink() { const url = subscription?.connect_url || subscription?.config_link; if (!url) { diff --git a/bot/app/web/subscription_webapp.py b/bot/app/web/subscription_webapp.py index bf2d8d3..22db1e8 100644 --- a/bot/app/web/subscription_webapp.py +++ b/bot/app/web/subscription_webapp.py @@ -2661,15 +2661,18 @@ def _serialize_plans( active_subscription_options = subscription_options or settings.subscription_options active_stars_subscription_options = stars_subscription_options or settings.stars_subscription_options plans: List[Dict[str, Any]] = [] - for months, price in sorted(active_subscription_options.items()): + for months in sorted(set(active_subscription_options) | set(active_stars_subscription_options)): + price = active_subscription_options.get(months) + stars_price = active_stars_subscription_options.get(months) + if price is None and (stars_price is None or int(stars_price) <= 0): + continue plan = { "months": int(months), - "price": float(price), + "price": float(price or 0), "currency": settings.DEFAULT_CURRENCY_SYMBOL or "RUB", "title": _format_months_title(int(months), lang), "sale_mode": "subscription", } - stars_price = active_stars_subscription_options.get(months) if stars_price is not None and int(stars_price) > 0: plan["stars_price"] = int(stars_price) plans.append(plan) diff --git a/bot/services/stars_service.py b/bot/services/stars_service.py index ae518b3..62fb047 100644 --- a/bot/services/stars_service.py +++ b/bot/services/stars_service.py @@ -73,10 +73,15 @@ class StarsService: i18n_data: dict, sale_mode: str = "subscription") -> None: try: - await payment_dal.update_provider_payment_and_status( + payment_record = await payment_dal.update_provider_payment_and_status( session, payment_db_id, message.successful_payment.provider_payment_charge_id, "succeeded") + target_user_id = ( + int(payment_record.user_id) + if payment_record and payment_record.user_id is not None + else int(message.from_user.id) + ) await session.commit() except Exception as e_upd: await session.rollback() @@ -87,7 +92,7 @@ class StarsService: activation_details = await self.subscription_service.activate_subscription( session, - message.from_user.id, + target_user_id, int(months) if sale_mode != "traffic" else 0, float(stars_amount), payment_db_id, @@ -97,14 +102,14 @@ class StarsService: ) if not activation_details or not activation_details.get("end_date"): logging.error( - f"Failed to activate subscription after stars payment for user {message.from_user.id}") + f"Failed to activate subscription after stars payment for user {target_user_id}") return referral_bonus = None if sale_mode != "traffic": referral_bonus = await self.referral_service.apply_referral_bonuses_for_payment( session, - message.from_user.id, + target_user_id, int(months) or 1, current_payment_db_id=payment_db_id, skip_if_active_before_payment=False, @@ -117,7 +122,7 @@ class StarsService: final_end = activation_details["end_date"] # Always use user's language from DB for user-facing messages - db_user = await user_dal.get_user_by_id(session, message.from_user.id) + db_user = await user_dal.get_user_by_id(session, target_user_id) current_lang = db_user.language_code if db_user and db_user.language_code else self.settings.DEFAULT_LANGUAGE i18n: JsonI18n = i18n_data.get("i18n_instance") _ = lambda k, **kw: i18n.gettext(current_lang, k, **kw) if i18n else k @@ -135,7 +140,7 @@ class StarsService: ) elif applied_days: inviter_name_display = _("friend_placeholder") - db_user = await user_dal.get_user_by_id(session, message.from_user.id) + db_user = await user_dal.get_user_by_id(session, target_user_id) if db_user and db_user.referred_by_id: inviter = await user_dal.get_user_by_id(session, db_user.referred_by_id) if inviter: @@ -183,9 +188,9 @@ class StarsService: # Send notification about payment try: notification_service = NotificationService(self.bot, self.settings, self.i18n) - user = await user_dal.get_user_by_id(session, message.from_user.id) + user = await user_dal.get_user_by_id(session, target_user_id) await notification_service.notify_payment_received( - user_id=message.from_user.id, + user_id=target_user_id, amount=float(stars_amount), currency="XTR", months=int(months) if sale_mode != "traffic" else 0, diff --git a/tests/test_webapp_assets.py b/tests/test_webapp_assets.py index ee9fb16..04a2dc6 100644 --- a/tests/test_webapp_assets.py +++ b/tests/test_webapp_assets.py @@ -27,6 +27,25 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(plans[0]["price"], 199.0) self.assertEqual(plans[1]["stars_price"], 2500) + def test_serialize_plans_includes_stars_only_subscription_options(self): + settings = Settings( + _env_file=None, + BOT_TOKEN="token", + POSTGRES_USER="app_user", + POSTGRES_PASSWORD="app_password", + YOOKASSA_ENABLED=False, + CRYPTOPAY_ENABLED=False, + RUB_PRICE_1_MONTH=None, + STARS_PRICE_1_MONTH=250, + ) + + plans = subscription_webapp._serialize_plans(settings, "en") + + self.assertEqual(len(plans), 1) + self.assertEqual(plans[0]["months"], 1) + self.assertEqual(plans[0]["price"], 0.0) + self.assertEqual(plans[0]["stars_price"], 250) + def test_resolve_webapp_js_asset_name_prefers_latest_minified_build(self): with tempfile.TemporaryDirectory() as tmpdir: asset_dir = Path(tmpdir)