Commit Graph

3591 Commits

Author SHA1 Message Date
Fringg 768e0b6a73 fix: PollResponse has no created_at — use sent_at for ordering 2026-04-29 10:17:40 +03:00
Fringg 83efc214fe fix: add 6 missing payment providers to payment_utils availability checks
RollyPay (and 5 others) showed buttons but triggered "payment methods
unavailable" because get_available_payment_methods() was missing them.
The keyboard builder (inline.py) had all providers, but the text
generator (payment_utils.py) did not — divergent hand-maintained lists.

Added to all 4 functions: get_available_payment_methods,
is_payment_method_available, get_payment_method_status,
get_enabled_payment_methods_count:
- SeverPay, PayPear, RollyPay, Overpay, AuraPay (new)
- RioPay (was in methods list but missing from status/count)
2026-04-29 08:27:37 +03:00
Fringg 29e177d396 fix: cabinet autopay endpoint — same NULL-safe is_trial guard 2026-04-29 08:21:41 +03:00
Fringg 2fbdbf5ab0 fix: autopay renewing trial subscriptions at classic-mode pricing
Three bugs caused trial subscriptions to be auto-renewed without a
tariff at arbitrary prices:

1. try_auto_extend_expired_after_topup: is_trial guard used truthiness
   check — NULL (legacy rows) passed as falsy. Changed to
   `is_trial is not False` (NULL-safe).

2. Multi-tariff branch: `not s.is_trial` treated NULL as not-trial.
   Changed to `s.is_trial is False`.

3. Telegram bot autopay toggle: no is_trial guard — users could enable
   autopay on trial subscriptions. Added trial check before enabling.
2026-04-29 08:16:39 +03:00
Fringg 422844d78d fix: retry queue action uses _should_create instead of stale subscription UUID 2026-04-29 08:08:24 +03:00
Fringg f37eb9a1bd fix: cabinet purchase fails after panel user deletion — stale UUID
Two bugs caused "RemnaWave UUID не найден" when a user repurchased
after their panel user was deleted (expired user cleanup):

1. Webhook handler only cleared subscription.remnawave_uuid in
   multi-tariff mode. In single-tariff mode the stale UUID remained,
   causing the cabinet to try update_remnawave_user on a deleted
   panel user instead of creating a new one.

2. Cabinet purchase-tariff used subscription.remnawave_uuid for the
   create/update decision. In single-tariff mode this was stale.
   Now mirrors the bot handler logic: checks user.remnawave_uuid
   in single-tariff mode (correctly cleared by webhook).
2026-04-29 08:04:23 +03:00
Fringg 1c38b31e60 fix: send admin notification on promo code activation from cabinet 2026-04-29 07:50:09 +03:00
Fringg 43dd0fd92c fix: referral links now clickable — remove <code> wrapping
The invite message wrapped the entire text including the referral URL
in <blockquote><code>...</code></blockquote>. The <code> tag made the
URL non-clickable — Telegram renders it as monospace copyable text.
Recipients couldn't tap the link to open it.

- Invite message: removed <code> from blockquote, Telegram now auto-links the URL
- Stats panel: removed <code> from bot/cabinet referral links, URLs are now clickable
2026-04-29 07:45:28 +03:00
Fringg a506c6be00 fix: add 5 missing payment providers to pending-payments model_map 2026-04-29 07:40:50 +03:00
Fringg ff7b190527 fix: add RollyPay, PayPear, Overpay, AuraPay to REAL_PAYMENT_METHODS 2026-04-29 07:36:48 +03:00
Fringg 527c5b4498 fix: panel sync subscription duration — ceil for days_remaining 2026-04-29 07:32:08 +03:00
Fringg bada41ecd6 fix: remaining pricing-critical .days floor calculations → math.ceil
Same bug as device pricing: timedelta.days floors partial days.
Fixed 14 more pricing-critical locations across 7 files:

- traffic addon pricing (bot handler + cabinet + miniapp)
- country addon pricing (bot handler + miniapp)
- generic addon pricing helper (common.py)
- auto-purchase device recomputation
- subscription CRUD pricing helper

Display-only .days usages intentionally left as floor (correct for
showing "X days left" to users).
2026-04-29 07:27:55 +03:00
Fringg cf60ae2967 fix: device/traffic addon pricing — use ceil instead of floor for days_left
timedelta.days is integer floor: 29 days 23 hours = 29, not 30.
When a user bought extra devices on the same day as their subscription,
they were charged for ~1 day instead of the full remaining period.

Fix: math.ceil(total_seconds / 86400) rounds partial days UP.
Applied to all 11 locations across 4 files:
- app/handlers/subscription/devices.py (5 spots)
- app/cabinet/routes/subscription_modules/devices.py (3 spots)
- app/keyboards/inline.py (3 spots — display pricing)
- app/utils/pricing_utils.py (1 spot — traffic prorated pricing)
2026-04-29 07:21:07 +03:00
Fringg 47c7d45793 fix: traffic addon discount also bypassed tariff-promo-group check 2026-04-29 07:14:14 +03:00
Fringg 4ab5928b61 fix: promo group discount applied to restricted tariffs in autopay
The pricing engine applied promo group discounts unconditionally,
without checking if the tariff is available for the user's promo group.

In autopay: user with VIP group (60% discount, restricted to Premium
tariff) would get 60% off when auto-renewing a Basic tariff that their
group should not cover.

Fix: in _calculate_tariff_core, check tariff.is_available_for_promo_group
before applying group discounts. If tariff is not available for the
user's promo group, the discount is zeroed — subscription renews at
full price. Protects ALL pricing paths (autopay, recurrent, manual).
2026-04-29 07:09:55 +03:00
Fringg fb857d792b feat: per-category enable/disable for admin notifications
Add ADMIN_NOTIFICATIONS_{CATEGORY}_ENABLED settings (default True) for
all 10 notification categories: purchases, renewals, trials, balance,
addons, infrastructure, errors, promo, partners, tickets.

Setting ADMIN_NOTIFICATIONS_PROMO_ENABLED=false now completely suppresses
promo notifications (promocode activations, campaign visits, promo group
changes) instead of silently falling back to the general topic.

Also fix referral_contest_service direct bot.send_message bypass —
now respects ADMIN_NOTIFICATIONS_PROMO_ENABLED setting.
2026-04-29 06:58:57 +03:00
Fringg 59080f7392 fix: handle A018 error code in admin_users sync endpoints (2 more locations) 2026-04-29 06:52:04 +03:00
Fringg c619dbcae2 fix: handle A018 error code as user-not-found fallback to create_user 2026-04-29 06:48:30 +03:00
Fringg 91de6d03fc fix: update cabinet_last_login on every request (throttled, 5 min) 2026-04-29 06:46:02 +03:00
Fringg 1fc04d842f fix: subscription-request-history — correct API client usage, add ownership check 2026-04-29 06:18:32 +03:00
Fringg e22beb7229 feat: subscription request history API + RemnaWave panel method
- Add get_subscription_request_history to RemnaWave API client
  (GET /api/users/{uuid}/subscription-request-history with pagination)
- Add GET /admin/users/{user_id}/subscription-request-history endpoint
  with subscription_id param for multi-tariff support
2026-04-29 06:12:38 +03:00
Fringg 134e7fb0e1 fix: false subscription expiry notifications — 4 bugs fixed
1. _check_expired_subscription_followups: added Subscription.status=EXPIRED
   filter (was matching ALL statuses including ACTIVE), User.status=ACTIVE
   filter, and 30-day lookback window to stop scanning ancient subscriptions

2. _get_expiring_paid_subscriptions: added User.status=ACTIVE filter to
   prevent sending "expiring" notifications to blocked/deleted users

3. Multi-tariff: before sending expired/followup notifications, check if
   user has another ACTIVE subscription with end_date > now — skip if they
   still have service through another tariff

4. Multi-tariff: same check for _check_expired_subscriptions — don't send
   "subscription expired" if user has another active sub
2026-04-29 05:47:56 +03:00
Fringg c743fc81a5 fix: replace all late callback.answer() with edit_text for error feedback
- Fix 7 intermediate error paths (balance deduction failures) that used
  callback.answer() after the early answer was already consumed — user
  got no error feedback at all
- Fix 2 unfixed handlers: confirm_tariff_purchase, confirm_daily_tariff_purchase
  — same early-answer pattern applied
- All 7 purchase/extend/switch handlers now consistently use early
  callback.answer() + edit_text for errors
2026-04-29 05:37:30 +03:00
Fringg 579e4f2a69 fix: callback.answer() before heavy operations to prevent query timeout
Telegram invalidates callback queries after 30 seconds. When the bot
performed panel sync, DB transactions, and admin notifications before
answering, callback.answer() threw TelegramBadRequest: query is too old.

Moved callback.answer() to immediately after guard checks (balance,
tariff availability) in 5 handlers:
- confirm_tariff_extend
- confirm_custom_tariff_purchase
- confirm_tariff_switch
- confirm_daily_tariff_switch
- confirm_instant_switch

Error feedback now uses callback.message.edit_text() instead of the
expired callback.answer().
2026-04-27 16:56:39 +03:00
Fringg b9b695799c refactor: remove unused EXTERNAL_ADMIN_TOKEN functionality
- Delete app/services/external_admin_service.py entirely
- Remove EXTERNAL_ADMIN_TOKEN and EXTERNAL_ADMIN_TOKEN_BOT_ID from config
- Remove build_external_admin_token, get_external_admin_token, get_external_admin_bot_id methods
- Remove unused hashlib/hmac imports from config.py
- Remove from system_settings_service: READ_ONLY_KEYS, PLAIN_TEXT_KEYS,
  category title, category description, prefix mapping, documentation metadata
- Remove from bot_configuration.py category group
- Remove from main.py startup sequence (ensure_external_admin_token call)
- Remove from .env.example
- Remove from docs/project_structure_reference.md
2026-04-26 19:54:33 +03:00
Fringg 5cf19c76e6 fix: backup import crash + upload handler hardening
- Fix PaypearPayment → PayPearPayment (capital P) — import crash
- Fix AurapayPayment → AuraPayPayment (capital P) — import crash
- Update upload instruction message to mention .tar.gz format
- Add null guard on document.file_name before extension check
2026-04-26 19:39:09 +03:00
Fringg eafb243882 fix: backup completeness — add 15 missing tables, accept .tar.gz uploads
Tables added to backup AND clear lists:
- Payment providers: riopay, severpay, paypear, rollypay, overpay, aurapay, saved_payment_methods
- Content: email_templates, info_pages, news_articles, news_categories, news_tags
- Landing: landing_pages, guest_purchases
- Analytics: yandex_client_id_map

Also:
- Telegram backup upload handler now accepts .tar.gz format (was .json/.json.gz only)
- All 92 ORM models + 3 association tables now covered
2026-04-26 19:29:55 +03:00
Fringg 5ed9a0d4fb fix: use fresh DB session for deactivate after long unpin loop 2026-04-24 18:13:48 +03:00
Fringg 63e1127353 fix: broadcast preview count — add .correlate(User) to EXISTS subqueries 2026-04-24 18:03:29 +03:00
Fringg ab4661b5c6 fix: unpin messages in Telegram BEFORE deactivating in DB
The "Unpin all" button called deactivate_active_pinned_message() first,
then looped over users to unpin. If Telegram API calls failed or timed
out, the message was already marked inactive in the DB with no way to
retry. Now: get active message → unpin from all chats → deactivate in DB.
2026-04-24 18:01:52 +03:00
Fringg 52bf2a9589 fix: ignore bot's own messages in unknown message handlers 2026-04-24 17:55:37 +03:00
Fringg 68d2350dfd fix: stop printing tracebacks for warning-level logs inside except blocks 2026-04-24 17:09:20 +03:00
Fringg 7d512d214a fix: integrate Yandex Metrika offline conv + S2S postback hooks
Restore integration hooks dropped in PR #2851 merge:
- PurchaseRequest accepts yandex_cid, referrer, subid from frontend
- Cache yandex_cid and subid in Redis at purchase creation (24h TTL)
- On fulfill_purchase: extract subid from cache, persist to DB
- Save Yandex CID from Redis to yandex_client_id_map
- Fire on_registration + S2S postback for new accounts
- Fire on_purchase + S2S postback for all paid purchases
- All hooks wrapped in try/except — failures never block delivery
2026-04-24 16:59:27 +03:00
Fringg 2cde38c63b fix: restore referrer field in admin landing purchases response 2026-04-24 16:53:47 +03:00
Fringg 24dc8d2a5e fix: restore HTTP Referer fallback for landing purchases 2026-04-24 16:47:22 +03:00
Fringg 1522d35f2d fix: gift purchases no longer inflate promo group level
Two bugs caused max promo group assignment on gift send/activate:

1. Buyer: GIFT_PAYMENT was counted in get_user_total_spent_kopeks
   alongside SUBSCRIPTION_PAYMENT. Now only SUBSCRIPTION_PAYMENT
   counts as personal spending for promo group auto-assignment.

2. Recipient: fulfill_purchase and activate_purchase created a
   SUBSCRIPTION_PAYMENT transaction for the recipient with the full
   gift price. Now skipped for gift recipients — they didn't pay.
2026-04-24 16:43:32 +03:00
Fringg 2b0d8a2a88 style: ruff format admin_bulk_actions.py 2026-04-24 16:34:43 +03:00
Fringg 9217f4116f fix: sanitize error messages in all bulk action catch-all handlers
Replace str(exc) with generic 'Action failed: internal error' in both
_execute_for_user and _execute_for_subscription catch-all blocks.
Prevents leaking internal paths, SQL details, or connection strings.
2026-04-24 16:24:45 +03:00
Fringg 2e45a93bd7 fix: bulk delete_user — pass real admin_id, sanitize error messages
- Thread admin_id through _execute_for_user to _do_delete_user for audit trail
- Replace raw exception str(e) with generic error message in client response
- Add subscriptions=[] to failure paths to prevent MissingGreenlet
2026-04-24 16:16:50 +03:00
Fringg d77fd81e16 feat: bulk actions — campaign/partner filters, delete_user action
- Add campaign_id and partner_id query params to GET /admin/users
- Filter users by advertising campaign via EXISTS subquery on registrations
- Filter users by partner via JOIN campaign registrations → campaigns
- Add DELETE_USER bulk action type with delete_from_panel param
- Handler calls UserService.delete_user_account for full bot+panel removal
- Permission check: users:delete required for delete_user action
- Add to _USER_LEVEL_ACTIONS (operates on user_ids, not subscription_ids)
2026-04-24 16:07:14 +03:00
Fringg bdb8cab1c9 feat: info page tab replacement — replaces_tab field + API
- Add replaces_tab column to InfoPage ('faq','rules','privacy','offer')
- Migration 0067: add nullable replaces_tab column
- CRUD: clear_replaces_tab ensures one page per tab, get_tab_replacements
  returns {tab: slug} mapping for active pages
- Admin routes: auto-clear old assignment on create/update
- Public route: GET /info-pages/tab-replacements (no auth)
- Schemas: replaces_tab with regex validation in all request/response models
2026-04-24 14:21:32 +03:00
Fringg d394565fe9 feat: FAQ support in info pages — page_type field + migration
- Add page_type column to InfoPage model ('page' or 'faq')
- Migration 0066: ALTER TABLE ADD COLUMN with server_default='page'
- Update schemas with page_type field and regex validation
- Update CRUD: page_type in create, filter in list
- Update admin/public routes with page_type query filter
- Backward compatible: existing pages default to type 'page'
2026-04-24 14:01:18 +03:00
Fringg 122d12db20 fix: /reorder route unreachable — move before /{page_id} path param 2026-04-24 13:50:38 +03:00
Fringg 2071a680d3 fix: info pages review — deduplicate slug index, type reorder items
- Remove triple-redundant slug index: keep only unique=True on column
  (PostgreSQL creates unique index automatically), remove __table_args__
  index and explicit create_index in migration
- Type ReorderRequest.items with ReorderItem(id: int, sort_order: int)
  instead of raw dict — prevents unvalidated input causing 500
- Migration downgrade: just drop_table (unique constraint drops with it)
2026-04-24 08:21:41 +03:00
Fringg e4b4a54797 feat: information pages — CRUD model, admin API, public API
- InfoPage model: slug, title (JSONB locale dict), content (JSONB),
  is_active, sort_order, icon, created_at/updated_at
- CRUD: create, get by id/slug, list, update, delete, reorder
- Admin routes: /admin/info-pages with full CRUD, toggle-active, reorder
  (permissions: settings:read/settings:edit)
- Public routes: /info-pages list active, /info-pages/{slug} get by slug
- Migration 0065: create info_pages table with unique slug index
- Custom pages support: admins can create any info page with any slug
2026-04-24 08:09:54 +03:00
Fringg 59c54c9b39 fix: privacy policy and offer text display HTML links as plain text
parse_mode='HTML' was missing from message sends during /start
registration. HTML tags like <a href="..."> were shown as literal
text instead of rendered links.

Fixed in 4 places:
- Privacy policy edit_text (line 1151)
- Privacy policy fallback answer (line 1158)
- Welcome/offer text in complete_registration_from_callback (line 1729)
- Welcome/offer text in complete_registration (line 2084)
2026-04-24 07:43:54 +03:00
Fringg 0d2b1dfdc9 feat: support multiple tariff_ids in user list filter
tariff_id query param now accepts comma-separated IDs (e.g.
tariff_id=1,3,5). CRUD functions updated to use IN() operator
for multi-tariff server-side filtering. Pagination works correctly
with multiple tariffs selected.
2026-04-24 07:16:33 +03:00
Fringg 605f202191 feat: bulk delete_subscription action — removes from bot DB + RemnaWave
Deactivates user in RemnaWave panel first, then deletes subscription
with related SubscriptionServer and TrafficPurchase records.
Subscription-level action (works with subscription_ids targeting).
2026-04-24 06:50:44 +03:00
Fringg be787a85bf feat: bulk set_devices action + device info in subscription list
- Add SET_DEVICES bulk action: sets device_limit on subscriptions,
  syncs to RemnaWave panel (subscription-level action)
- Add device_limit to SubscriptionListItem and BulkSubscriptionInfo
  schemas for frontend display
- Populate device_limit in _build_user_list_item and
  _build_subscription_info helpers
2026-04-24 06:35:29 +03:00
Fringg ff41ea9abb fix: suppress 'User already enabled' traceback in bulk add_traffic 2026-04-24 06:11:02 +03:00