Commit Graph

7864 Commits

Author SHA1 Message Date
Fringg 29ae7089aa feat: respond to unknown media messages (photos, videos, documents)
Previously the bot only responded "Не понимаю эту команду" to text
messages. Photos, videos, documents and stickers sent outside of an
active FSM state were silently ignored, causing users to think their
media was received when it wasn't (e.g. support ticket screenshots
sent as separate messages).

Now the bot replies with the same "use menu buttons" message for any
unhandled media when no FSM state is active.
2026-04-23 03:31:21 +03:00
Fringg 1068c1387a feat: Yandex Metrika offline conversions + S2S postbacks
- Add YandexClientIdMap model for user → yandex_cid mapping with
  upsert-safe CRUD (ON CONFLICT DO UPDATE)
- Add yandex_cid, subid, referrer columns to GuestPurchase
- Add yandex_offline_conv_service: Measurement Protocol integration
  with mc.yandex.ru/collect (registration, trial, purchase events),
  background task management, CID parsing from /start params
- Add s2s_postback_service: server-to-server affiliate postbacks
  with URL template placeholders and URL-safe encoding
- Add analytics offline conversion info to branding API (masked secret)
- Add POST /analytics/yandex-cid endpoint for cabinet CID capture
- Add 11 config settings (YANDEX_OFFLINE_CONV_*, S2S_POSTBACK_*)
- Add migration 0063 (yandex_client_id_map table + guest_purchases cols)
- Fix: mask measurement secret aggressively (show only last 4 chars)
- Fix: always replace {user_id} placeholder in S2S postback URLs
- Fix: use structlog kwargs instead of f-strings with LOG_PREFIX

Based on PR #2851 by @smediainfo — CI/CD workflow changes excluded
2026-04-22 05:57:46 +03:00
Fringg d31632534b fix: validate analytics goal is set when analytics is enabled on landing
Prevent enabling analytics_view/click without providing the
corresponding goal identifier, which would result in empty
Yandex Metrika calls on the frontend.
2026-04-22 05:43:54 +03:00
Fringg 3272b4bb05 feat: landing page analytics goals and sticky pay button
- Add sticky_pay_button, analytics_view_enabled, analytics_view_goal,
  analytics_click_enabled, analytics_click_goal columns to LandingPage
- Add fields to CRUD updatable fields, admin create/update/detail
  schemas, create_landing() kwargs, _landing_to_detail() response
- Expose sticky_pay_button and analytics fields in public landing
  config response for frontend Yandex Metrika integration
- Add migration 0062 with idempotent column checks

Based on PR #2852 by @smediainfo — CI/CD workflow changes excluded
(hardcoded version strings would regress dynamic manifest reading)
2026-04-22 05:36:41 +03:00
Fringg dd177101f7 fix: ticket media_items review fixes
- Add if has_media else None guards in user-side ticket handlers
  (create_ticket, add_message) matching admin handler pattern
- Fix Telegram notification using resolved primary_file_id/primary_type
  instead of raw request fields for gallery messages
- Narrow except Exception to (TypeError, KeyError, ValueError) in
  _message_to_response with warning log for debugging
- Add media_items parameter to TicketCRUD.create_ticket and
  TicketCRUD.add_message for CRUD layer parity
- Add TicketMediaItemResponse and media_items field to webapi
  TicketMessageResponse to prevent data loss on read
2026-04-22 05:29:51 +03:00
Fringg 36571c4275 feat(tickets): multi-media message gallery (media_items JSONB)
- Add media_items JSONB column to TicketMessage model for multi-media
  gallery support (photos/videos/documents in one bubble)
- Add TicketMediaItem schema with type validation and shared
  _validate_media_bundle helper (max 10 items, legacy field compat)
- Update admin and user ticket handlers to store media_items and
  back-fill legacy media_type/media_file_id/media_caption from first
  item for backward compatibility
- Update _message_to_response in both admin and user routes to include
  media_items in API responses
- Allow empty message text when media is attached (message field now
  defaults to empty string with model validator ensuring text or media)
- Add migration 0061 with idempotent column check

Based on PR #2869 by @smediainfo — CI/CD workflow changes excluded
(hardcoded version strings would regress dynamic manifest reading)
2026-04-22 05:23:48 +03:00
Fringg 66f8577448 fix: menu layout schema icon limit, traffic_topup_enabled condition, shadowing imports
- Increase icon max_length from 10 to 100 in all three schemas
  (MenuButtonConfig, ButtonUpdateRequest, AddCustomButtonRequest)
  to support Telegram Custom Emoji IDs
- Add traffic_topup_enabled condition to ButtonConditions schema
- Remove shadowing local imports of MenuLayoutService in
  routes/menu_layout.py (top-level import already provides access)
2026-04-22 05:17:56 +03:00
Fringg 9ca3320a02 fix: classic mode renewal resets device_limit to 1 via cart key mismatch
- Fix cart key mismatch: extend cart saved 'device_limit' but
  confirm_purchase read 'devices' key, falling back to DEFAULT=1.
  Now both keys are saved in both cart-save paths
- Fix confirm_purchase device resolution: use explicit is None checks
  instead of or-chain to avoid falsy-zero trap
- Fix return_to_saved_cart display: fall back to 'device_limit' and
  'traffic_limit_gb' keys when 'devices'/'traffic_gb' are absent
- Fix second cart-save path in _extend_existing_subscription with
  same dual-key pattern
- Fix RemnaWaveService import path in renewal service
- Add RESET_DEVICES_ON_RENEWAL setting: resets all connected devices
  (hwid) via RemnaWave API on each subscription renewal
2026-04-22 05:12:27 +03:00
Fringg 9ed4f086b0 feat: tariff switch direction control, fix device pricing within tariff limit
Tariff switch direction:
- Add TARIFF_SWITCH_UPGRADE_ENABLED and TARIFF_SWITCH_DOWNGRADE_ENABLED
  settings to control allowed switch directions
- Guard all 10 entry points: instant switch (list, preview, confirm),
  legacy switch (list, select, confirm, daily confirm), cabinet (preview,
  execute), purchase-options API
- Filter tariff lists by allowed direction, show "unavailable" when
  both directions disabled
- Expose settings in cabinet purchase-options response for frontend

Device pricing fix:
- Devices within tariff.device_limit are now free when restoring
  (was charging for all devices regardless of tariff inclusion)
- Fix max(100, price) minimum enforcing 1 RUB even when
  chargeable_devices is 0
- Apply fix across all endpoints: bot handlers (confirm_change,
  execute_change, confirm_add), cabinet API (legacy purchase,
  modern purchase, get-price, save-cart), inline keyboard display
2026-04-22 04:54:17 +03:00
Fringg da855a7c89 fix: tariff switch pricing showing free for upgrades, admin duplicate subscription guard
- pricing_engine: use shortest period for daily rate comparison instead
  of period closest to remaining_days — fixes incorrect free/zero cost
  for upgrades when tariffs have different period sets
- pricing_engine: remove unused target_days parameter from
  get_tariff_daily_rate_fraction
- admin_users: add duplicate subscription check before create,
  change_tariff and activate actions to prevent UniqueViolationError
  on uq_subscriptions_user_tariff_active constraint
- admin_users: add IntegrityError fallback on create as TOCTOU safety net
2026-04-22 04:25:42 +03:00
Fringg 7be404b918 fix: FSM state loss on balance topup, PayPear confirmation_url, hidden trial tariff in renewal
- balance/platega: re-set FSM state after min/max validation errors,
  set state before pending_amount path, use balance_topup callback for back button
- balance/main: set FSM state and payment_method in handle_topup_amount_callback
  for all providers before routing, use balance_topup callback in validation errors
- payment/paypear: fix confirmation_url key (was 'url'), add fallback,
  store charged amount with commission for correct webhook amount comparison
- tariff_purchase: redirect to active tariff list when current tariff is
  inactive (hidden trial after promo code activation)
- cabinet/renewal: check tariff.is_active in both GET and POST endpoints
  to prevent hidden trial tariff periods from appearing
2026-04-22 04:05:46 +03:00
Dmitry V. Lunin b71e58c8d2 fix: do not reset subscription_crypto_link when cryptoLink absent in webhook (#2891)
Co-authored-by: Dmitry Lunin <br@slack.ru>
2026-04-21 08:05:11 +03:00
Dmitry V. Lunin 1696e6f884 fix: do not update first_name/last_name from OIDC claims (#2892)
Co-authored-by: Dmitry Lunin <br@slack.ru>
2026-04-21 08:04:00 +03:00
Dmitry V. Lunin 7093d368d3 feat: add WEBHOOK_IP to allow Telegram bypass DNS lookup for webhook (#2894)
* feat: add WEBHOOK_IP to allow Telegram bypass DNS lookup for webhook

* style: ruff format main.py

---------

Co-authored-by: Dmitry Lunin <br@slack.ru>
2026-04-21 08:02:53 +03:00
Danila Yudin 905cea68b4 fix: grant all available squads for unrestricted trials (#2897) 2026-04-21 08:01:28 +03:00
Egor dc5442223d Merge pull request #2898 from BEDOLAGA-DEV/main
w
2026-04-21 07:53:33 +03:00
c0mrade 3b03c253cc fix: устранить root cause MissingGreenlet в автоплатежах через refetch по id
Трейс показал: subscription.user падает на lazy-load → pool._checkout →
do_ping → await_ → MissingGreenlet. SQLAlchemy 2.0 async session не
поддерживает sync-lazy-load для relationships. Причина рассинхрона:
lock_user_for_pricing делает populate_existing=True + selectinload(
User.subscriptions).selectinload(Subscription.tariff), что разгружает
Subscription.user backref для сестринских подписок того же user.
Последующее обращение sub.user у другой подписки падает.

Фикс: захватываем (sub_id, user_id) пары ДО цикла, каждую итерацию
делаем fresh refetch через async select с eager load user+tariff+
promo_group. Никаких lazy access в горячем пути. В except используем
локально захваченные id вместо getattr(subscription, ...), чтобы
логирование не падало каскадом на expired объекте.
2026-04-19 12:08:34 +03:00
c0mrade db79cc9eb0 fix: устранить MissingGreenlet в автоплатежах и починить traceback в логах
- subtract_user_balance: пишем promo_offer_log в отдельной сессии вместо rollback после commit, который экспайрил объекты основной сессии и ломал последующие обращения к subscription/user attrs
- monitoring_service._process_autopayments: перезагружаем subscription с eager-load user/tariff после списания, оборачиваем каждую итерацию в try/except + rollback, чтобы одна ошибка не валила весь батч
- logging_config: новый processor _auto_capture_exc_info автоматически подтягивает traceback из sys.exc_info() или error-kwarg → полный traceback в файле, консоли и Telegram без exc_info=True на каждом вызове
- logging_handler: дублирующая логика захвата exc_info в TelegramNotifierProcessor как резерв
2026-04-19 11:50:40 +03:00
Egor 1b94d9e700 Merge pull request #2890 from BEDOLAGA-DEV/release-please--branches--main
chore(main): release 3.49.0
v3.49.0
2026-04-18 04:00:56 +03:00
github-actions[bot] 39a7c92cd4 chore(main): release 3.49.0 2026-04-18 00:58:34 +00:00
Egor 81ebec676c Merge pull request #2889 from BEDOLAGA-DEV/dev
Dev
2026-04-18 03:58:10 +03:00
Fringg 25ea5c60fd docs: add AuraPay to README with partner block 2026-04-18 03:56:48 +03:00
Fringg 29877fc93b fix: handle edge case when all tariffs are daily in legacy renewal 2026-04-18 01:09:53 +03:00
Fringg 5986c00fab fix: redirect legacy users without tariff to tariff selection on renewal
Users created before tariffs were introduced (tariff_id=NULL) got
"Тариф не найден" when pressing "Продлить подписку". Now they see
a tariff selection list instead, allowing them to pick a tariff
and renew with proper parameters (traffic, devices, etc).
2026-04-18 01:05:48 +03:00
Fringg ecc4a6147d fix: rate-limit daily subscription insufficient balance notifications to 6 hours
Users with daily subscriptions and low balance were getting
"Подписка приостановлена" notification every 30 minutes (on each
charge cycle). Now rate-limited via Redis cache to max 1 notification
per 6 hours per subscription.
2026-04-18 00:59:09 +03:00
Fringg 16bc1d4198 fix: align campaign top registrations revenue with period comparison
Top registrations list was summing DEPOSIT + SUBSCRIPTION_PAYMENT
transactions (total user spending), while period comparison revenue
only counted DEPOSIT with real payment methods (actual money paid in).

Now both use the same calculation: only DEPOSIT transactions with
real payment methods. This fixes the discrepancy where a user showed
500₽ in the list but total revenue was 250₽.
2026-04-18 00:50:23 +03:00
Fringg 0f814be1b7 fix: add missing RollyPay CRUD wrappers and guest payment flow
RollyPay was missing:
- 7 CRUD wrapper functions in payment_service.py (create, get, update, link)
- Guest payment block in create_guest_payment()

Both were present for PayPear and AuraPay but omitted for RollyPay.
2026-04-18 00:23:38 +03:00
Fringg 97179360c0 feat: integrate AuraPay payment provider
Full integration following PayPear/RollyPay patterns:
- API client with X-ApiKey + X-ShopId auth, HMAC-SHA256 webhook verification
  (sorted keys, concatenated values, secret key #2)
- AuraPayPaymentMixin with create, webhook, finalize (all side effects), status check
- CRUD, model, migration (0060), PaymentMethod.AURAPAY enum
- Webhook endpoint, cabinet topup, bot handler with sub-options (card, sbp)
- Admin panel: AURAPAY category in bot_configuration + system_settings_service
- Config: AURAPAY_ENABLED, AURAPAY_API_KEY, AURAPAY_SHOP_ID, AURAPAY_SECRET_KEY
2026-04-18 00:02:49 +03:00
Fringg 2aa5927433 fix: register PayPear and RollyPay in admin panel settings
- bot_configuration.py: added PAYPEAR/ROLLYPAY to payment categories
  and test payment buttons
- system_settings_service.py: added category titles, descriptions,
  and prefix mappings for PAYPEAR_* and ROLLYPAY_* settings
2026-04-17 23:44:35 +03:00
Egor 1c696c69e3 Merge pull request #2887 from BEDOLAGA-DEV/dev
docs: add PayPear and RollyPay to README with partner blocks
2026-04-16 06:26:19 +03:00
Fringg b531959982 docs: add PayPear and RollyPay to README with partner blocks 2026-04-16 06:25:33 +03:00
Egor ead0fc99d3 Merge pull request #2886 from BEDOLAGA-DEV/release-please--branches--main
chore(main): release 3.48.0
v3.48.0
2026-04-16 06:11:21 +03:00
github-actions[bot] c09ae7436c chore(main): release 3.48.0 2026-04-16 03:10:42 +00:00
Egor 1ea76575ce Merge pull request #2885 from BEDOLAGA-DEV/dev
Dev
2026-04-16 06:10:19 +03:00
Fringg 25447edc9e docs: add SEVERPAY, PAYPEAR, ROLLYPAY to .env.example 2026-04-16 06:08:10 +03:00
Fringg a59858227f fix: support payment_method selection for RollyPay (sbp/card/crypto)
- Mixin accepts payment_method_type parameter (None = show all on form)
- API service sends payment_method only when specified
- Cabinet route passes payment_option to mixin
- Config service has sub_options: sbp, card, crypto
- Keyboard label no longer hardcodes "СБП"
2026-04-16 06:03:33 +03:00
Fringg ccc2f4efec feat: integrate RollyPay payment provider (SBP via USDT)
Full integration following PayPear/SeverPay patterns:
- API client with X-API-Key + X-Nonce auth, HMAC-SHA256 webhook verification
- RollyPayPaymentMixin with create, webhook, finalize (all side effects), status check
- CRUD, model, migration (0059), PaymentMethod.ROLLYPAY enum
- Webhook endpoint, cabinet topup, bot handler
- Statuses: created, processing, paid, expired, canceled, chargeback
- Config: ROLLYPAY_ENABLED, ROLLYPAY_API_KEY, ROLLYPAY_SIGNING_SECRET, etc.
2026-04-16 05:52:25 +03:00
Fringg a18f6caa9b feat: integrate PayPear payment provider
Full integration following SeverPay patterns:
- API client with Basic Auth, idempotency, HMAC webhook verification
- PayPearPaymentMixin with create, webhook, finalize (all side effects), status check
- CRUD, model, migration (0058), PaymentMethod.PAYPEAR enum
- Webhook endpoint, cabinet topup, bot handler, payment verification
- Sub-options: bank_card, sbp, sberpay, tpay
- Config: PAYPEAR_ENABLED, PAYPEAR_SHOP_ID, PAYPEAR_SECRET_KEY, etc.
2026-04-16 05:36:48 +03:00
Fringg 92eaf45311 fix: increase nalogo receipt queue retry window to 12 hours
Changed defaults from 10 attempts × 5min (50min total) to
72 attempts × 10min (12 hours total). When nalog.ru is temporarily
down, receipts now retry for 12 hours before being dropped,
giving the service enough time to recover.
2026-04-16 04:58:54 +03:00
Fringg 61cf495fc5 fix: show menu buttons for limited subscriptions in back-to-menu paths
The previous fix only covered /start command paths. The more common
show_main_menu and handle_back_to_menu in menu.py used their own
is_active check which returned False for limited status.

Now both paths treat limited subscriptions as active for UI, matching
the _calculate_subscription_flags fix in start.py.
2026-04-16 04:54:57 +03:00
Fringg 0c545490b6 fix: show menu buttons for limited (traffic exhausted) subscriptions
When Remnawave webhook set subscription status to 'limited' (traffic
exhausted), the main menu hid ALL buttons (connect, subscription,
buy traffic) because is_active returns False for non-'active' status.

Now 'limited' is treated as active for UI purposes — the subscription
is not expired, just traffic-exhausted. Users can see the "Buy traffic"
button precisely when they need it most.
2026-04-16 04:50:13 +03:00
Fringg 2d5afe5d75 fix: low balance alerts disabled by default, add quiet hours, expiry filter, top-up button
- Default balance_low_enabled changed to False (opt-in via cabinet)
- Quiet hours: alerts skipped between 22:00-09:00 UTC
- Only alerts when subscription expires within LOW_BALANCE_ALERT_EXPIRY_DAYS (default 3)
- Added inline "Top up" button linking to cabinet miniapp
- Synced defaults across notification_prefs, cabinet notifications route
2026-04-16 04:42:36 +03:00
Egor 50dc5a0fd1 Merge pull request #2882 from BEDOLAGA-DEV/release-please--branches--main
chore(main): release 3.47.0
v3.47.0
2026-04-15 14:12:24 +03:00
github-actions[bot] 4dc8b4c091 chore(main): release 3.47.0 2026-04-15 11:10:30 +00:00
Egor 4db9e85062 feat: multi-tariff sync fix, daily discount fix, campaign links, TELEGRAM_API_URL
* fix: update subscription_crypto_link when syncing user from panel (#2867)

* fix: update subscription_crypto_link when syncing user from panel

* fix: update subscription_crypto_link when syncing user from panel

* fix: use PROXY_URL for Telegram OIDC JWKS requests (#2866)

* feat: add TELEGRAM_API_URL for custom Telegram Bot API server

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.

* fix(ci): read Docker image version from release-please manifest instead of hardcoding (#2859)

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-файлы.

* fix: format telegram_auth.py to use single quotes (ruff)

* fix: remove daily tariff fallback to smallest period discount

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.

* fix: use CABINET_URL for campaign web links instead of MINIAPP_CUSTOM_URL

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.

* feat: add DISPLAY_NAME_RESTRICTION_ENABLED toggle

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.

* fix: allow clearing all period discounts from promo groups

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.

* fix: create panel user instead of update for new subscriptions in multi-tariff 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"

* fix: apply same create-vs-update fix to renewal and purchase flows

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.

* fix: apply create-vs-update fix to all remaining tariff_purchase flows

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.

* fix: apply create-vs-update fix to cabinet traffic/devices and monitoring

Same multi-tariff create-vs-update bug in 5 more locations:
- cabinet/subscription_modules/traffic.py (2 instances)
- cabinet/subscription_modules/devices.py (2 instances)
- services/monitoring_service.py (1 instance)

All now use _should_create pattern based on subscription.remnawave_uuid
in multi-tariff mode instead of falling back to user.remnawave_uuid.

* fix: ruff format traffic.py and monitoring_service.py

---------

Co-authored-by: Dmitry V. Lunin <49199230+BlackRaincoat@users.noreply.github.com>
Co-authored-by: Gary Jarrel <gary@jarrel.com.au>
2026-04-15 14:10:05 +03:00
Egor fca8d6da97 Dev (#2880)
* fix: update subscription_crypto_link when syncing user from panel (#2867)

* fix: update subscription_crypto_link when syncing user from panel

* fix: update subscription_crypto_link when syncing user from panel

* fix: use PROXY_URL for Telegram OIDC JWKS requests (#2866)

* feat: add TELEGRAM_API_URL for custom Telegram Bot API server

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.

* fix(ci): read Docker image version from release-please manifest instead of hardcoding (#2859)

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-файлы.

* fix: format telegram_auth.py to use single quotes (ruff)

* fix: remove daily tariff fallback to smallest period discount

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.

* fix: use CABINET_URL for campaign web links instead of MINIAPP_CUSTOM_URL

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.

* feat: add DISPLAY_NAME_RESTRICTION_ENABLED toggle

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.

* fix: allow clearing all period discounts from promo groups

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.

* fix: create panel user instead of update for new subscriptions in multi-tariff 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"

* fix: apply same create-vs-update fix to renewal and purchase flows

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.

* fix: apply create-vs-update fix to all remaining tariff_purchase flows

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.

* fix: apply create-vs-update fix to cabinet traffic/devices and monitoring

Same multi-tariff create-vs-update bug in 5 more locations:
- cabinet/subscription_modules/traffic.py (2 instances)
- cabinet/subscription_modules/devices.py (2 instances)
- services/monitoring_service.py (1 instance)

All now use _should_create pattern based on subscription.remnawave_uuid
in multi-tariff mode instead of falling back to user.remnawave_uuid.

* fix: ruff format traffic.py and monitoring_service.py

---------

Co-authored-by: Dmitry V. Lunin <49199230+BlackRaincoat@users.noreply.github.com>
Co-authored-by: Gary Jarrel <gary@jarrel.com.au>
2026-04-15 14:04:16 +03:00
Fringg 4fe67a9c74 fix: ruff format traffic.py and monitoring_service.py 2026-04-15 13:57:15 +03:00
Fringg 30a1a31978 fix: apply create-vs-update fix to cabinet traffic/devices and monitoring
Same multi-tariff create-vs-update bug in 5 more locations:
- cabinet/subscription_modules/traffic.py (2 instances)
- cabinet/subscription_modules/devices.py (2 instances)
- services/monitoring_service.py (1 instance)

All now use _should_create pattern based on subscription.remnawave_uuid
in multi-tariff mode instead of falling back to user.remnawave_uuid.
2026-04-15 13:28:06 +03:00
Fringg c2b68e1afa fix: apply create-vs-update fix to all remaining tariff_purchase flows
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.
2026-04-15 13:24:07 +03:00
Fringg cea7260a85 fix: apply same create-vs-update fix to renewal and purchase flows
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.
2026-04-15 13:17:35 +03:00