Commit Graph

581 Commits

Author SHA1 Message Date
sMedia.tech d7f91c8358 fix: include SUBSCRIPTION_PAYMENT in dashboard revenue calculations
Guest/landing purchases create SUBSCRIPTION_PAYMENT transactions (not
DEPOSIT), so they were excluded from income_today, total_income,
revenue_by_period, and payment_methods breakdown.

Also use func.abs() for SUBSCRIPTION_PAYMENT amounts since they are
stored as negative values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:10:56 +03:00
Fringg 3d78974af7 feat: add multi-provider recovery, retry_count, amount verification, and indexes
- Add retry_count column to guest_purchases with Alembic migration
- Add expression indexes on metadata_json->>'purchase_token' for all 12
  payment provider tables (partial indexes filtered by is_paid/status)
- Implement _find_succeeded_provider_payment() covering all providers:
  YooKassa, Heleket, MulenPay, Pal24, Wata, Platega, CloudPayments,
  Freekassa, KassaAi, RioPay, SeverPay, and CryptoBot (payload field)
- Add amount verification in _check_and_recover_pending_purchase():
  compares provider payment amount with GuestPurchase.amount_kopeks,
  skips for CryptoBot (USD conversion imprecision)
- Increment retry_count on each retry attempt in retry_stuck_paid_purchases
  and retry_stuck_pending_activation
- Mark purchases as FAILED after 20 retries with admin Telegram alert
  via AdminNotificationService (ERRORS category)
2026-03-20 06:53:49 +03:00
Fringg 4a002b7db1 fix: allow repeated auto-assignment of promo groups on each purchase
Remove the threshold barrier that prevented re-assignment to the same
promo group tier. Previously, _get_best_group_for_spending was called
with min_threshold_kopeks=previous_threshold, which meant once a user
was auto-assigned to a tier (e.g. 100 kopeks), the check 100 > 100
would fail and the function would skip cleanup of promocode groups.

Now the function always finds the best group for the user's spending
without threshold filtering. The threshold ratchet is preserved only
for the watermark update (auto_promo_group_threshold_kopeks only
increases, never decreases).

Also elevate promo group assignment failure logging from DEBUG to
WARNING across all 3 call sites in transaction.py.
2026-03-20 05:46:59 +03:00
Fringg 3ec9e71de7 fix: propagate exceptions from get_primary_user_promo_group
Consistent with has_user_promo_group and get_user_promo_groups which
now propagate exceptions instead of masking them with default returns.
2026-03-20 05:30:42 +03:00
Fringg da7a9cc3c5 fix: prevent duplicate promo groups during auto-assignment after purchase
Root cause: auto-assignment did not remove old auto/promocode groups before
adding new one, causing users to accumulate multiple simultaneous promo groups.
The primary group selection then picked the wrong one.

Changes:
- Remove old auto/promocode groups atomically before adding new one
- Add SELECT FOR UPDATE (lock_user_for_update) to serialize concurrent webhooks
- Fix CRUD rollback when commit=False — re-raise instead of destroying caller tx
- Fix sort order: desc(PromoGroup.id) to match model's get_primary_promo_group()
- Let has_user_promo_group/get_user_promo_groups propagate exceptions (fail-open bug)
- Fix replace_user_promo_groups: remove dead query, add _sync_user_primary_promo_group
- Use SQL COUNT in count_user_promo_groups instead of loading all rows
- Refresh user after removal loop to avoid stale ORM state
2026-03-20 05:25:41 +03:00
Fringg b5471b7720 perf: add covering indexes for referral network queries
Add composite indexes on advertising_campaign_registrations(user_id,
created_at) and transactions(user_id, type, is_completed, amount_kopeks)
to enable index-only scans. Uses CREATE INDEX CONCURRENTLY for zero
downtime. Also enable transaction_per_migration in Alembic env.py.
2026-03-20 02:31:03 +03:00
Fringg aec01ce0d4 fix: reset device limit to new tariff base on tariff switch
Previously, extra purchased devices were carried over when switching
tariffs, causing incorrect pricing — users upgrading to a more
expensive plan kept the old per-device rate until next renewal.

Now tariff switch resets device_limit to the new tariff's base limit.
Extra purchased devices are not carried over.
2026-03-18 16:57:08 +03:00
Fringg f967c29bd7 fix: добавлены RioPay и SeverPay в REAL_PAYMENT_METHODS
Без этого платежи через RioPay и SeverPay не учитывались
в статистике доходов, разбивке по методам и отчётах
2026-03-18 03:59:33 +03:00
Fringg abaf279533 feat: добавлена интеграция SeverPay для пополнения баланса
- API клиент (HMAC-SHA256 подпись, создание/получение платежа)
- CRUD операции с FOR UPDATE блокировкой
- Payment mixin с обработкой webhook и финализацией
- Хендлеры бота для пополнения через SeverPay
- Миграция 0040: таблица severpay_payments
- Webhook endpoint (всегда 200 для предотвращения ретраев)
- Интеграция с payment_verification_service
- Поддержка гостевых покупок (лендинги, подарки)
2026-03-18 03:49:19 +03:00
Fringg dcff6947dd style: ruff format 2026-03-18 01:59:12 +03:00
Fringg 4abb8cb1a3 fix: исправлены проблемы RioPay интеграции после ревью
- Модель: user_id nullable=True + ondelete='SET NULL' (не применилось ранее)
- order_id для гостей: 'rpguest_xxx' вместо 'rpNone_xxx'
- Миграция: добавлено пересоздание FK с ON DELETE SET NULL
- get_latest_payment_by_method: добавлен RioPayPayment в model_map
2026-03-18 00:18:15 +03:00
Fringg 04f4e6bf6e feat: добавлена поддержка RioPay для лендингов и подарков
- Добавлен RioPay в create_guest_payment (landing/gift покупки)
- user_id в RioPayPayment теперь nullable (для гостевых платежей)
- Добавлен guest purchase flow в _finalize_riopay_payment
- Миграция 0039: riopay_payments.user_id nullable
2026-03-18 00:10:51 +03:00
Fringg 3089c1704b fix: исправлен расчёт конверсии в статистике продаж
- Добавлен fallback через has_had_paid_subscription для подсчёта конверсий
- Исправлен знаменатель: total_trial_starters = new_trials + conversions
- Ограничение conversion_rate до 100% максимум
- Исправлен .is_(True) вместо == True в subscription_conversion.py
2026-03-17 23:46:15 +03:00
Egor f84885cc8a Merge pull request #2747 from smediainfo/pr/fix-external-squad-sync
fix: защита внешних сквадов от удаления при синхронизации серверов
2026-03-17 20:26:58 +03:00
Fringg 826accba51 fix: MissingGreenlet при изменении количества устройств на CLASSIC подписках
lock_user_for_pricing не загружал User.subscription eagerly,
что вызывало lazy load в async контексте при обращении к db_user.subscription
в execute_change_devices.
2026-03-16 11:25:33 +03:00
root 808818ca2b style: ruff format server_squad.py 2026-03-16 04:30:32 +00:00
root b563796091 fix: protect external squads from deletion during server sync 2026-03-16 04:27:59 +00:00
Fringg 8d3cd50098 refactor: централизация всех расчётов цен в PricingEngine
- Мигрирован confirm_purchase() на calculate_classic_new_subscription_price()
- Мигрирован compute_simple_subscription_price на делегацию в PricingEngine
- Мигрирован handle_custom_confirm на calculate_tariff_purchase_price()
- Мигрированы daily confirm handlers (confirm_daily_tariff_purchase,
  confirm_daily_tariff_switch, confirm_instant_switch daily path)
- Мигрирован gift.py на calculate_tariff_purchase_price()
- Мигрированы FSM cache prices (select_period, select_devices, toggle_country)
- Добавлен lock_user_for_pricing в admin_buy_tariff_execute (TOCTOU fix)
- Добавлен lock + recompute в _auto_add_devices и _auto_add_traffic
- Исправлено двойное применение promo-offer в simple_subscription (критический баг)
- Унифицирован daily price display (group+offer) на всех 6 поверхностях
- PricingEngine.get_addon_discount_percent: добавлен promo_group= kwarg
- PricingEngine._calculate_switch_to/from_daily: добавлен promo-offer discount
- Удалён мёртвый код из common.py (_get_addon_discount_percent_for_user)
- Miniapp period_discounts: исправлен доступ через get_discount_percent()
2026-03-16 03:10:22 +03:00
Fringg 7648707ca2 fix: campaign registration, revenue calculation, backup restore, autopay errors, referral links
- fix campaign registration not recorded when CHANNEL_IS_REQUIRED_SUB + SKIP_RULES_ACCEPT enabled (missing _apply_campaign_bonus_if_needed in required_sub_channel_check fast path)
- fix revenue calculation counting bonus-funded subscription payments as income (now deposits only via REAL_PAYMENT_METHODS)
- fix backup restore PendingRollbackError cascade on unique constraint violations (savepoint wrapping in _restore_table_records and _restore_users_without_referrals)
- fix AttributeError on message.text.strip() when users send media in referral code handlers
- suppress 'message is not modified' TelegramBadRequest in autopay toggle
- add bot_referral_link to referral API response with URL encoding
2026-03-15 01:13:50 +03:00
Legacyyy777 ebee8348ca fix: implement case-insensitive email checks in authentication and user retrieval
Updated email queries in authentication routes and user CRUD operations to be case-insensitive. This change ensures that email comparisons ignore case, improving user experience and preventing potential registration/login issues with differently cased emails.
2026-03-14 04:39:11 +05:00
c0mrade 3306e02902 fix: add nested selectinload and referrer eager loading to prevent MissingGreenlet
Added selectinload(UserPromoGroup.promo_group) nested under
user_promo_groups to prevent lazy-load in get_primary_promo_group().
Added selectinload(User.referrer) for format_referrer_info().
Broadened except clause in format_referrer_info as safety net.
2026-03-14 00:14:42 +03:00
c0mrade 5442f288d4 fix: add selectinload to user lock queries to prevent MissingGreenlet
lock_user_for_update, subtract_user_balance, and add_user_balance use
select(User).with_for_update().populate_existing which expires loaded
relationships. Added selectinload for subscription, user_promo_groups
and promo_group to prevent lazy-load in async context.
2026-03-13 21:39:31 +03:00
Fringg 75dbd2b4fc refactor: migrate remaining callers to PricingEngine + cleanup dead CRUD
- Migrate bot purchase handlers, menu, admin users to PricingEngine
- SubscriptionRenewalService.finalize() accepts both old and new pricing types
- Remove dead subscription CRUD pricing functions (get_subscription_renewal_cost etc.)
- Remove dead pricing_utils functions
2026-03-13 05:12:32 +03:00
Fringg b4ef52caa4 fix: payment providers — lock_user_for_update + commit=False atomicity
All payment providers now use lock_user_for_update before balance mutations
and commit=False pattern for atomic payment status + fulfillment.
Tribute service refund also uses proper locking.
2026-03-13 05:12:15 +03:00
Fringg ae99358ae9 fix: pricing audit — display/charge parity, race conditions, balance locks
M-2: tariff_purchase.py — _apply_promo_discount delegates to PricingEngine,
     _get_user_period_discount returns (group_pct, offer_pct, combined),
     all ~15 call sites updated for display/charge price parity

M-4: miniapp switch_tariff — add FOR UPDATE lock on subscription,
     commit=False for atomic balance+transaction, emit_transaction_side_effects

M-6: CryptoBot — defer status commit (commit=False) so webhook retry works
     if fulfillment fails

WARNING: add lock_user_for_update before balance_kopeks mutations in
     contest_attempt_service, wheel_service, admin/referrals,
     account_merge_service, cabinet/routes/contests
2026-03-13 05:11:59 +03:00
Fringg b7775b72dc fix: guard rollback on commit flag, add flush to promo_offer_log
- subtract_user_balance: only rollback when commit=True, re-raise when
  commit=False so caller controls transaction lifecycle
- log_promo_offer_action: add db.flush() when commit=False to surface
  constraint errors immediately instead of deferring to caller's commit
2026-03-12 19:33:25 +03:00
Fringg ba54819f9c fix: atomicity refactor, review fixes, and DELETED recovery logging
- subtract_user_balance: add commit=False parameter for atomic balance+subscription ops
- extend_subscription: add commit=False parameter, propagate to clear_notifications
- wata_service: wire _MIN_EXPIRATION_MINUTES constant to actual usage
- admin_users: fix no-op ternary in sync_user_from_panel timezone normalization
- start.py: log warning when DELETED recovery zeros non-zero balance (3 locations)
- remnawave_service: preserve PromoCodeUse records and used_promocodes in force_cleanup
2026-03-12 19:26:36 +03:00
Fringg 8f434525eb feat: add LIMITED subscription status and preserve extra devices on tariff switch
- Add SubscriptionStatus.LIMITED for traffic-exhausted subscriptions
- Webhook user.limited now sets LIMITED directly instead of DISABLED
- Add LIMITED to reactivation, extend, resume, auto-purchase, contest eligibility
- Add traffic_exhausted error response in miniapp API
- Fix device_limit being overwritten on tariff switch in all code paths:
  admin change_tariff, user switch-tariff, miniapp, bot tariff_purchase,
  auto_purchase_service — now preserves extra purchased devices via
  calc_device_limit_on_tariff_switch() helper
- Fix truthiness checks on device_limit (0 is valid, use `is not None`)
2026-03-12 18:35:59 +03:00
Fringg c8162505ed chore: apply ruff formatting to 4 files 2026-03-12 08:24:47 +03:00
Fringg 68bc8eb57c fix: update promo group via M2M table so admin changes persist
The admin endpoint wrote only to the legacy users.promo_group_id FK,
which got overwritten by sync_user_primary_promo_group on the next
transaction. Now writes to user_promo_groups M2M table (authoritative
source) and re-derives the FK via sync.

Also re-raise exceptions in _sync_user_primary_promo_group to prevent
committing inconsistent state between M2M and FK columns.
2026-03-12 06:03:26 +03:00
Fringg 9957259881 fix: add post_update=True to User.referrals self-referential relationship
Without post_update, SQLAlchemy's flush ordering cannot resolve the
circular dependency when both a user and their referrer are in the same
session. This caused referred_by_id to be silently NULLed during flush,
breaking Telegram login (which eagerly loads User.referrer) and causing
apparent admin rights loss in the cabinet.

OAuth login was unaffected because it uses a bare select(User) without
eager loading the referrer relationship.

Root cause confirmed by 6 parallel investigation agents tracing the
exact code paths through get_user_by_telegram_id → selectinload →
flush → circular dependency.
2026-03-12 05:39:29 +03:00
Fringg a798f1143e refactor: remove estimated price from balance, simplify server sync, fix HTML injection
- Remove estimated renewal price display from balance top-up screen
- Remove country name generation during server/squad sync, use original RemnaWave name as display_name
- Add html.escape() for all display_name/country name values rendered in HTML-parsed Telegram messages
2026-03-12 04:58:49 +03:00
Fringg cb5126aff8 feat: add show_in_gift toggle for tariffs in admin panel
Add a per-tariff visibility flag (show_in_gift) that controls whether
a tariff appears in the /gift section. Enforced server-side in gift
config query, gift purchase endpoint, and landing page gift purchases.

Includes Alembic migration with idempotency guard and server_default.
2026-03-12 04:15:40 +03:00
Fringg 8b35428055 fix: reactivate subscription after traffic top-up when status is EXPIRED
When traffic is exhausted, RemnaWave may send user.expired webhook setting
local status to EXPIRED (not just DISABLED). reactivate_subscription() only
handled DISABLED→ACTIVE, silently ignoring EXPIRED subscriptions. After
purchasing additional GB, the subscription stayed expired and VPN remained
blocked despite payment.

Changes:
- reactivate_subscription() now handles both DISABLED and EXPIRED→ACTIVE
  when end_date is still in the future
- Inverted null end_date guard to block reactivation (defense-in-depth)
- Added enable_remnawave_user() call after update in all traffic/device
  top-up paths to ensure panel exits LIMITED state
- Gated enable call on subscription.status == 'active' to prevent
  enabling when reactivation was a no-op
- Fixed all 12 call sites across bot handlers, cabinet routes,
  miniapp, webapi, and auto-purchase service
2026-03-12 03:57:35 +03:00
Fringg e67b8e448e fix: reset subscription for paid users, trial-to-paid tariff conversion, gift purchase MissingGreenlet
- Remove is_active_paid_subscription guard from reset-subscription endpoint
- Add is_trial=False and status=ACTIVE on admin tariff change (guarded by
  tariff.is_trial_available and end_date check)
- Eagerly load user_promo_groups and promo_group in gift purchase locked query
- Move user_promo_groups access inside try/except in get_primary_promo_group()
2026-03-11 03:46:39 +03:00
Fringg 641ff86bf6 fix: stars rate rounding + device/traffic purchase stats
- Stars: ceil → round в rubles_to_stars, нормализация kopeks в cabinet invoice
- Устройства/трафик: добавлен transaction_type в subtract_user_balance,
  покупки устройств и трафика теперь создают SUBSCRIPTION_PAYMENT (не WITHDRAWAL),
  что исправляет отображение в статистике продаж
2026-03-10 21:50:20 +03:00
firewookie 39d007ff3e FIX PR Problems 2026-03-10 15:01:28 +05:00
firewookie 94b211e2a7 FIX PR Problems 2026-03-10 15:00:41 +05:00
firewookie c84dbf82fc update 2026-03-10 14:50:26 +05:00
FireWookie 9ad684c8c9 Merge pull request #7 from FireWookie/dev
Dev
2026-03-10 14:42:53 +05:00
FireWookie d147be0316 Merge pull request #6 from FireWookie/dev
Dev
2026-03-10 14:42:05 +05:00
firewookie fd3466b75c fix PR problems 2026-03-10 14:29:29 +05:00
FireWookie dcfd54a7cb Merge pull request #5 from FireWookie/dev
Dev
2026-03-10 14:18:49 +05:00
FireWookie 5c2e5dfaab Merge branch 'BEDOLAGA-DEV:main' into feature/riopay 2026-03-10 14:14:23 +05:00
Fringg a90d2d9367 fix: 3 critical issues from second-round review
1. Balance-mode gift purchase leaked full 64-char token in response.
   Gateway path truncated to [:12] but balance path didn't. Fixed.

2. retry_stuck_pending_activation referenced GuestPurchase.updated_at
   which doesn't exist on the model. Changed to paid_at (mirrors
   retry_stuck_paid_purchases pattern).

3. clear_notifications() called db.commit() unconditionally, defeating
   commit=False in replace_subscription. Added commit parameter with
   default=True for backward compat, passed through from caller.
2026-03-10 07:09:39 +03:00
Fringg 5c34656476 fix: address review findings from 6-agent audit
1. Truncate tokens to 12 chars in all API responses (SentGift,
   PendingGift, ReceivedGift, PurchaseStatus, PurchaseResponse,
   return URL) — full token no longer leaves the server
2. Status endpoint supports prefix-based token lookup
3. create_paid_subscription/replace_subscription accept commit=False
   — activate_purchase now uses single atomic commit for subscription
   + purchase status update (fixes double-commit gap)
4. Bot handler uses flush() instead of commit() before svc_activate
   — consistent with cabinet endpoint, allows rollback on failure
5. Add retry_stuck_pending_activation() for purchases stuck in
   PENDING_ACTIVATION status (10 min threshold)
6. Add varchar_pattern_ops index for prefix queries on token column
2026-03-10 07:02:14 +03:00
Fringg bc9003c336 chore: ruff format 2026-03-10 03:36:32 +03:00
Fringg fcdeff1ee5 fix: migrate pricing to days-based proration, fix promo revenue leaks, fix admin panel bugs
- Migrate all addon pricing (devices, traffic, countries) from months-based to days-based proration
- Remove get_remaining_months() utility, use days_left / 30 consistently
- Fix promo/campaign balance bonuses counted as revenue in reports (add REAL_PAYMENT_METHODS filter)
- Fix partner stats, campaign stats, miniapp stats, referral fraud detection promo deposit leaks
- Fix admin balance history showing deductions with + sign (use -abs for expense types)
- Fix promo offer deactivation returning 400 for non-promocode offers
- Fix daily tariff renewal requesting 30-day renewal instead of 1-day purchase
2026-03-10 03:31:07 +03:00
Fringg b9089e693f fix: normalize threshold 0→NULL in create_promo_group for consistency 2026-03-09 22:16:30 +03:00
Fringg b815abf2b1 fix: loyalty tiers current status based on spending, not assigned group
- current_tier_name and is_current now determined by highest achieved
  tier threshold instead of user's assigned promo group
- Backend update_promo_group converts threshold 0 to NULL for clean state
2026-03-09 22:08:41 +03:00