Commit Graph

7918 Commits

Author SHA1 Message Date
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 74999fe99d fix: create locales directory with correct permissions in Dockerfile 2026-04-29 05:55:21 +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
Fringg cfbcc3082f fix: always return subscriptions list in user list API
subscriptions were only populated when is_multi_tariff_enabled()
was true. Users with multiple subscriptions in regular tariff mode
had empty subscriptions[] — bulk actions couldn't show or select them.
Now subscriptions are always populated regardless of tariff mode.
2026-04-24 06:08:23 +03:00
Fringg 2ad893badf fix: MissingGreenlet in subscription-ids bulk actions
_execute_for_subscription accessed user.subscriptions after commit,
triggering async lazy load → MissingGreenlet. Actions committed
successfully but reported as failed with cryptic error message.

Fix: use _build_subscription_info([sub]) with the already-loaded
targeted subscription instead of trying to lazy-load the full
user.subscriptions list.
2026-04-24 05:55:06 +03:00
Fringg e78177b2fc feat: multi-tariff bulk actions — subscription-level targeting
- Add subscription_ids to BulkExecuteRequest (mutually exclusive with
  user_ids via model_validator). Admins can now target specific
  subscriptions instead of auto-resolving the first active one.
- Add _execute_for_subscription dispatcher that loads subscription by
  ID, gets user, and passes sub_override to action handlers
- Add sub_override parameter to all 5 subscription-level handlers
  (extend, cancel, activate, change_tariff, add_traffic) — bypasses
  _resolve_subscription when a specific subscription is targeted
- Add SubscriptionListItem to UserListItem response — in multi-tariff
  mode, each user row includes all their subscriptions with tariff
  name, status, days remaining, traffic info
- User-level actions (add_balance, assign_promo_group, grant) reject
  subscription_ids with 400
- Add subscription_id field to BulkUserResult and SSE progress events
- Add _stream_bulk_execute_subscriptions SSE generator
- Backward compatible: existing user_ids requests work unchanged
2026-04-24 05:41:04 +03:00
Fringg daa472570c fix: add subscription/tariff/promo_group filters to admin user list API
The bulk actions page filters (subscription_status, tariff_id,
promo_group_id) were sent by the frontend but ignored by the
backend — the list_users endpoint had no such parameters.

- Add subscription_status, tariff_id, promo_group_id query params
  to GET /cabinet/admin/users
- Add subscription-level filtering via subquery in get_users_list
  and get_users_count CRUD functions
- Add tariff_id, tariff_name, traffic_used_gb, traffic_limit_gb,
  device_limit, days_remaining to UserListItem response schema
- Populate tariff info from subscription.tariff relationship in
  _build_user_list_item
2026-04-24 05:24:53 +03:00
Fringg db7b6734fd fix: bulk change_tariff not clearing squads when new tariff has none
When switching to a tariff with empty/null allowed_squads, the old
tariff's squads were preserved on the subscription. Now always sets
connected_squads from the new tariff (or empty list).
2026-04-24 05:10:47 +03:00
Fringg c0e0756b9a feat: bulk actions — SSE streaming progress, grant subscription, multi-tariff info
- Add SSE streaming mode (?stream=true): per-user progress events with
  real-time success/error counts, final summary event
- Add GRANT_SUBSCRIPTION action: creates new subscription with tariff,
  skips users who already have that tariff (multi-tariff aware),
  handles IntegrityError with graceful rollback
- Add BulkSubscriptionInfo: returns all user's subscriptions in each
  result for multi-tariff visibility
- Refactor: extract _validate_and_prepare and _execute_for_user helpers
  shared by streaming and non-streaming paths
2026-04-24 04:32:16 +03:00
Fringg 5b45d4354f fix: bulk actions review — rollback on error, multi-tariff constraint checks
- Add db.rollback() in per-user exception handler to prevent session
  poisoning (one failed commit would abort all subsequent users)
- Add multi-tariff duplicate subscription check in activate_subscription
  (prevents uq_subscriptions_user_tariff_active violation)
- Add multi-tariff duplicate subscription check in change_tariff
  (prevents switching to a tariff the user already holds)
2026-04-24 04:24:20 +03:00
Fringg fb2773fee4 feat: admin bulk actions API — mass operations on users
Add POST /cabinet/admin/bulk/execute endpoint for applying operations
to multiple users at once (up to 500 per request):

- extend_subscription / add_days: extend subscription by N days
- cancel_subscription: deactivate and expire subscriptions
- activate_subscription: reactivate expired subscriptions
- change_tariff: switch tariff without changing remaining days
- add_traffic: add extra GB to subscription traffic
- add_balance: credit balance with transaction record
- assign_promo_group: set or remove promo group for users

Features: dry_run preview mode, partial success handling (per-user
try/except), param validation before loop, auto-panel sync after
subscription mutations, multi-tariff mode support
2026-04-24 04:15:07 +03:00
Fringg ae7feeb726 fix: server squad sync fails on fresh DB without default promo group
On first startup with empty database, ensure_servers_synced() fetched
squads from RemnaWave but create_server_squad() failed with
ValueError('Server squad must be linked to at least one promo group')
because no default promo group existed yet.

Now _get_default_promo_group_id() auto-creates a default promo group
via _get_or_create_default_promo_group() when none exists, matching
the pattern used throughout the codebase for user registration.
2026-04-24 03:32:29 +03:00