Fixed 6 more locations in tariff_purchase.py that had the same broken
pattern (custom purchase, daily purchase, trial conversion, tariff
switch, daily switch, instant switch). All now use _should_create
based on multi-tariff mode instead of falling back to user UUID.
Same bug as the tariff purchase fix: in multi-tariff mode, new
subscriptions without remnawave_uuid incorrectly fell back to
user.remnawave_uuid and called update instead of create.
Fixed in subscription_renewal_service.py and purchase.py to use
the same _should_create pattern based on mode.
In multi-tariff mode, new subscriptions have remnawave_uuid=None.
The old logic fell back to user.remnawave_uuid (from a previous
subscription) and called update_remnawave_user(), which refused
to work because the NEW subscription had no UUID.
Now correctly: in multi-tariff mode, always CREATE if subscription
has no remnawave_uuid. In single-tariff mode, use user-level UUID.
Fixes: "subscription has no remnawave_uuid, cannot update panel"
Empty period_discounts dict was normalized to None by the schema,
making it indistinguishable from "field absent" (don't update).
Now empty dict passes through to CRUD which correctly sets
period_discounts=None in DB, clearing all discounts.
Allows disabling the display name restriction middleware via .env.
Users with special characters in their Telegram name (e.g. "@")
were blocked from using the bot entirely. Default: true (enabled).
Set DISPLAY_NAME_RESTRICTION_ENABLED=false to disable.
Campaign web links were generated from MINIAPP_CUSTOM_URL which is often
empty, causing get_campaign_web_link() to return None. Admins and partners
could only share bot links for campaigns, not cabinet links.
Now prefers CABINET_URL (where the auth flow captures ?campaign= param),
falling back to MINIAPP_CUSTOM_URL for backwards compatibility. This is
consistent with how referral web links already use CABINET_URL.
Daily tariffs (period_days=1) incorrectly inherited the discount
of the smallest configured period (e.g. 90 days -> 5%). This caused
daily prices to show discounts that were never intended for them.
Now daily tariffs only get a discount if explicitly configured for
period_days=1 in the promo group's period_discounts.
The x-release-please-version markers in workflow files were stuck at v3.7.0
since commit 5070bb34 removed them from extra-files (GitHub Actions returns
403 when release-please tries to modify .github/workflows/ via GITHUB_TOKEN).
Instead of hardcoding the version, read it from .release-please-manifest.json
at build time. This file is always kept in sync by release-please and does
not require workflow file write permissions.
---
Маркеры x-release-please-version в workflow-файлах застряли на v3.7.0
после коммита 5070bb34, который удалил их из extra-files (GitHub Actions
возвращает 403 при попытке release-please изменить .github/workflows/
через GITHUB_TOKEN).
Вместо хардкода версии теперь читаем её из .release-please-manifest.json
во время сборки. Этот файл всегда синхронизируется release-please и не
требует прав на запись в workflow-файлы.
Support custom Telegram Bot API server URL via TELEGRAM_API_URL env var.
Enables bot operation in regions where api.telegram.org is blocked
(Cloudflare Worker, self-hosted telegram-bot-api, nginx reverse proxy).
Uses native aiogram TelegramAPIServer.from_base(), works with PROXY_URL.
In multi-subscription mode, a user with an expired trial AND an active
paid subscription was incorrectly included in expired broadcast targets.
Fixed in 3 places:
- get_target_users_count (SQL): added NOT EXISTS active sub subquery
- get_target_users 'expired' (Python): skip if has_active
- get_target_users 'expired_subscribers' (Python): same check
MAX_BUTTONS_PER_ROW was 3, causing Pydantic 422 when adding multiple
custom buttons to a row. Telegram allows up to 8 buttons per row.
Also added tg:// to URL_PATTERN so admins can use Telegram deep links
(tg://resolve, tg://user, etc.) in custom menu buttons. webapp mode
still requires https:// as enforced by existing validation.
Admin user device editor had hardcoded limit of 10 in text, validation,
and inline buttons. Now uses settings.MAX_DEVICES_LIMIT dynamically,
generating buttons 1..N in rows of 4. Falls back to text-only input
if limit exceeds Telegram's 100-button cap.
The _max_attempts property existed but was never checked as a limit.
Receipts were retried indefinitely (55+ attempts observed in logs).
Now receipts exceeding max_attempts are removed from the queue.
Campaign slug was lost during email registration flow — it was only
sent at verification time from localStorage, which is empty if the
user opens the verification link in a different browser/webview.
Now campaign_slug is accepted in the registration request, saved to
user.pending_campaign_slug, and used as fallback during email
verification. Also processed immediately for auto-verified test emails.
- Add _check_low_balance_alerts to monitoring service
- Notify users with autopay when balance drops below their threshold
- Uses notification_prefs helper for per-user settings
Add remnawave_retry_queue.enqueue() calls in all 10 purchase error handlers
where RemnaWave API failure was caught and swallowed without scheduling a retry:
- cabinet purchase.py: purchase_tariff() and activate_trial() (2 places)
- subscription_purchase_service.py: miniapp purchase flow (1 place)
- tariff_purchase.py: custom, standard, daily, renewal, switch, daily-switch,
and instant-switch flows (7 places)