Commit Graph

7474 Commits

Author SHA1 Message Date
Fringg afefcc9c07 fix: resolve remaining TOCTOU issues in RioPay, SeverPay and restore paid_at
- RioPay: use create_transaction(commit=False) to keep FOR UPDATE lock,
  replace update_riopay_payment_status with inline assignment + flush,
  add emit_transaction_side_effects after commit
- SeverPay: add db.flush() before _finalize, remove self-assignment,
  add paid_at to both webhook and status-check paths
- Freekassa/KassaAI: add is_paid and paid_at to webhook and status-check
  inline sections (regression from CRUD→inline migration)
- MulenPay: add is_paid and paid_at to webhook inline section
2026-03-21 02:10:41 +03:00
Fringg 82c79c1306 fix: prevent double-payment TOCTOU race in all payment providers
Apply the same FOR UPDATE locking pattern across 8 providers:
- RioPay: added FOR UPDATE lock (had none at all)
- CryptoBot: moved lock before status check, removed redundant lock
- WATA: moved lock before is_paid commit, removed redundant lock
- Freekassa: moved lock before is_paid commit, removed redundant lock
- KassaAI: moved lock before is_paid commit, removed redundant lock
- MulenPay: moved lock before is_paid commit, removed redundant lock
- Pal24: moved lock before is_paid commit, removed redundant lock
- SeverPay: moved lock before is_paid check, removed redundant lock

Pattern applied to all: acquire FOR UPDATE with populate_existing=True
immediately after finding the payment, replace intermediate commits with
inline assignments + flush(), re-check is_paid from locked row.
2026-03-21 01:52:56 +03:00
Fringg 0e1296e0ea fix: prevent double balance credit on concurrent Platega webhooks
- acquire FOR UPDATE lock immediately after payment lookup, before is_paid check
- use populate_existing=True to prevent SQLAlchemy identity map stale reads
- replace intermediate update_platega_payment(commit) with inline assignments + flush
- re-check locked.is_paid after lock in get_platega_payment_status
- guard _finalize_platega_payment: only called when lock held and is_paid=False
- suppress "message is not modified" TelegramBadRequest in message_patch
2026-03-21 01:46:44 +03:00
Fringg 9dd6b54c6e fix: prevent bootstrap from reactivating revoked superadmin roles
- bootstrap _assign_if_missing no longer reactivates revoked UserRole rows
- revoke_role uses SELECT FOR UPDATE + pg_advisory_xact_lock to prevent TOCTOU race on last-superadmin check
- block self-revocation of superadmin role
- block is_active/level changes on system roles
- block expires_at on superadmin role assignments
- single SUPERADMIN_LEVEL constant in crud/rbac.py, imported everywhere
- get_superadmin_count excludes expired assignments
- removed dead UserRoleCRUD.revoke_role method
- warn when revoking RBAC role from a legacy ADMIN_IDS user
- added migration 0043: indexes on user_roles.role_id, access_policies.role_id, lower(users.email)
2026-03-21 01:09:13 +03:00
Fringg 67da390371 feat: show both bot and cabinet referral links everywhere
Previously the bot showed only one referral link (cabinet when CABINET_URL
is set, bot otherwise). Users who received the cabinet link were confused —
they opened a web registration form instead of being directed to the bot.

Now the bot, cabinet API, and miniapp API all return both links:
- Bot link (t.me deep link) — always shown
- Cabinet link (web registration) — shown when CABINET_URL is configured

Changes:
- Add get_bot_referral_link() and get_cabinet_referral_link() to config
- Refactor config methods to eliminate code duplication
- Update bot referral handler to display both links
- Fix switch_inline_query 256-char limit with auto-truncation
- Add html_escape() to all user-controlled strings in HTML messages
- Add translations for 5 new keys in all 5 locale files (ru/en/ua/zh/fa)
- Simplify cabinet route to use new methods instead of inline URL construction
- Add bot_referral_link to MiniApp API schema and response
2026-03-20 23:12:22 +03:00
Egor 3bda0a2001 Merge pull request #2775 from BEDOLAGA-DEV/main
w
2026-03-20 08:47:02 +03:00
Fringg 877b1cde11 fix: handle duplicate admin roles in RBAC bootstrap
Use scalars().first() instead of scalar_one_or_none() to tolerate
duplicate rows in admin_roles table by (is_system, level).
2026-03-20 08:45:23 +03:00
Fringg 5faf7015ac fix: make migration 0042 idempotent for retry_count column 2026-03-20 08:44:19 +03:00
Egor 55f386d7e8 Merge pull request #2774 from BEDOLAGA-DEV/release-please--branches--main
chore(main): release 3.36.0
v3.36.0
2026-03-20 07:23:48 +03:00
github-actions[bot] e6a310dc32 chore(main): release 3.36.0 2026-03-20 04:21:42 +00:00
Egor ccd8f86e96 Merge pull request #2773 from BEDOLAGA-DEV/dev
Dev
2026-03-20 07:21:11 +03:00
Egor b6d4373933 Merge pull request #2771 from smediainfo/fix/kassa-ai-guest-metadata
fix: use base model name for KassaAI guest metadata patch
2026-03-20 07:20:25 +03:00
Egor 266620904d Merge pull request #2772 from BEDOLAGA-DEV/main
w
2026-03-20 07:18:45 +03:00
Fringg 479af5741a style: ruff format 2026-03-20 07:15:15 +03:00
Fringg 79c110ff41 fix: address review findings for multi-provider recovery
- Use atomic UPDATE SET retry_count = retry_count + 1 instead of
  SELECT+modify+commit to avoid identity map pollution
- Filter retry_count < max_retries in SQL WHERE clause to avoid
  wasting LIMIT slots on exhausted purchases
- Extract _fail_exhausted_purchases_batch() — separate pass for
  exhausted purchases, alert sent outside session context
- HTML-escape all user-controlled values in admin alert messages
- Mark purchases FAILED on amount mismatch (prevents repeated
  error logs every scheduler cycle) with admin alert
- Accept plain dict in _send_stuck_purchase_alert instead of ORM
  object (avoids expired-attribute access after commit)
2026-03-20 07:02:47 +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 57c5c679ee fix: address review findings for guest purchase recovery
- Add FOR UPDATE to recovery path in try_fulfill_guest_purchase to prevent
  TOCTOU race that could overwrite DELIVERED back to PAID
- Isolate monitoring phases with independent try/except so Phase 1 failure
  does not block Phase 2/3
- Optimize recover_stuck_pending_purchases to select only token and
  payment_method columns instead of full ORM objects
- Remove dead elif branch in stars_payments.py (try_fulfill_guest_purchase
  no longer returns False)
- Add Phase 3 comment for consistency
2026-03-20 06:45:21 +03:00
Fringg 2781236011 fix: prevent guest purchases from getting stuck in PENDING/FAILED status
- Mark guest purchases as PAID (not FAILED) on transient fulfillment errors
  so monitoring service can retry them automatically
- Use fresh AsyncSessionLocal session for recovery to avoid tainted-session
  issues after rollback
- Add status guard to prevent overwriting terminal states (DELIVERED, etc.)
- Add recover_stuck_pending_purchases() to detect PENDING purchases where
  provider payment already succeeded (checks YooKassa payments table)
- Use SELECT ... FOR UPDATE to prevent TOCTOU races in recovery
- Add 3-phase monitoring pipeline: recover PENDING → retry PAID → retry
  PENDING_ACTIVATION
- Extract shared _resolve_base_payment_method() helper
2026-03-20 06:39:01 +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 8b2668087b fix: prevent premature commits in promocode promo group operations
add_user_to_promo_group and remove_user_from_promo_group in
promocode_service used default commit=True, causing mid-transaction
commits that flushed all pending session changes before the outer
db.commit() at lines 163/404.
2026-03-20 05:33:30 +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 6a4ce3dd38 feat: multi-select scope for referral network graph API
Support multiple campaigns, partners, and users in a single scoped
graph request. Dedup inputs, soft-skip invalid IDs, and discover
campaign registrations across all scope types.
2026-03-20 02:30:56 +03:00
Fringg df086b09c7 feat: add scoped referral network graph with scope selector API
- GET /scope-options: lightweight campaign/partner lists for selector
- GET /scoped?scope=campaign|partner|user&id=N: returns subgraph
- Recursive CTE helpers for ancestor/descendant traversal
- GRAPH_MAX_NODES cap applied to scoped graphs
- Campaign nodes shown even with zero registrations
2026-03-20 01:49:59 +03:00
Fringg 01132a7bc7 feat: add partner → campaign edges to referral network graph 2026-03-20 01:16:57 +03:00
sMedia.tech 182667ecb8 fix: use 'kassa_ai' base model name for guest metadata patch
kassa_ai_sbp has no separate CRUD module, causing guest purchase
metadata to not be saved, which breaks webhook fulfillment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:56:39 +03:00
Fringg c8f4cca340 fix: correct revenue calculations in referral network
Campaign revenue now uses actual subscription payments by campaign
users instead of referral commission earnings (which were often 0).

Branch revenue for user detail now sums subscription payments by
branch users via recursive CTE instead of referral earnings.

Batch branch_revenue helper also updated to use Transaction spending.
2026-03-19 08:51:52 +03:00
Fringg ac9fcd8d30 fix: improve referral network query correctness and cleanup
- Use UNION ALL + count(distinct) for recursive CTE (faster, cycle-safe)
- Derive total_earnings from personal_revenue dict (remove redundant query)
- Merge duplicate campaign registration queries into single query
- Add MAX_REFERRAL_DEPTH constant, _format_datetime type hint
2026-03-19 08:08:16 +03:00
Fringg c08c903e8f feat: add referral network graph visualization admin API
4 endpoints for referral network analysis: full graph with batched
aggregation queries, user detail with recursive CTE branch counting,
campaign detail with conversion metrics, and search with LIKE escaping.

All endpoints rate-limited, scoped queries to prevent full-table scans,
depth-limited recursive CTE, fail_closed on expensive graph endpoint.
2026-03-19 07:55:51 +03:00
Fringg 69bb399b63 feat: add media attachment support for admin ticket replies
Admin can now attach photos, videos, and documents when replying to tickets
via the cabinet. Media is uploaded through the existing /cabinet/media/upload
endpoint and stored as Telegram file_id references in TicketMessage.

Added media_type, media_file_id, media_caption fields to AdminReplyRequest
with cross-field validation via model_validator.
2026-03-19 06:38:54 +03:00
Egor 463c5385d6 Merge pull request #2770 from BEDOLAGA-DEV/release-please--branches--main
chore(main): release 3.35.0
v3.35.0
2026-03-18 23:35:06 +03:00
github-actions[bot] 41dfe39518 chore(main): release 3.35.0 2026-03-18 20:34:40 +00:00
Egor f44c9b6903 Merge pull request #2769 from BEDOLAGA-DEV/dev
Dev
2026-03-18 23:34:11 +03:00
Egor 1f35d45dc6 Merge pull request #2768 from BEDOLAGA-DEV/main
w
2026-03-18 23:32:42 +03:00
Fringg ef8f6625bf chore: ruff format 2026-03-18 23:31:31 +03:00
Fringg 7101555da0 feat: add user_email to admin payments API response 2026-03-18 23:30:17 +03:00
Fringg e15b18fb41 feat: раздельные топики для админских уведомлений
Добавлены 9 новых env-переменных для маршрутизации уведомлений по отдельным топикам:
- PURCHASES, RENEWALS, TRIALS, BALANCE, ADDONS
- INFRASTRUCTURE, ERRORS, PROMO, PARTNERS

Обратная совместимость: если топик для категории не задан — fallback на ADMIN_NOTIFICATIONS_TOPIC_ID.
2026-03-18 23:16:54 +03:00
Fringg b80eeea089 feat: include manual admin top-ups in sales statistics revenue 2026-03-18 22:39:31 +03:00
Fringg cb61014d9c fix: remove forced white background from custom email template overrides
Custom email templates with their own styling (background colors, <style> tags)
were wrapped in a white base template, causing visible white areas around dark-themed
templates. Added three-tier detection: full HTML documents pass through as-is,
styled content gets a minimal wrapper, simple fragments keep the base template.
2026-03-18 22:24:58 +03:00
Fringg 5b3353433b fix: undefined currency variable in RioPay payment creation
currency was referenced but never defined in create_riopay_payment,
causing NameError. Use settings.RIOPAY_CURRENCY instead.
2026-03-18 22:05:45 +03:00
Fringg f1d45343e9 fix: handle None autopay_days_before in autopayment processing
Existing subscriptions may have NULL autopay_days_before in DB,
causing TypeError in min(None, 3). Default to 3 when None.
2026-03-18 21:16:08 +03:00
Fringg b40a812f3a fix: fix Platega and CryptoBot webhook verification
Platega: handle verification ping POST without auth headers (empty body → 200 OK)
CryptoBot: always use API token for signature verification per docs, not WEBHOOK_SECRET
CryptoBot: reject requests without signature in both FastAPI and aiohttp handlers
Remove dead self.webhook_secret from CryptoBotService
Update tests to match new behavior
2026-03-18 20:11:55 +03:00
Egor ac00434645 Merge pull request #2765 from BEDOLAGA-DEV/release-please--branches--main
chore(main): release 3.34.1
v3.34.1
2026-03-18 18:25:15 +03:00
github-actions[bot] 964c33c772 chore(main): release 3.34.1 2026-03-18 15:23:42 +00:00
Egor ec875837a4 Merge pull request #2767 from BEDOLAGA-DEV/dev
Dev
2026-03-18 18:22:52 +03:00
Egor db0e169a41 Merge pull request #2766 from BEDOLAGA-DEV/main
w
2026-03-18 18:21:41 +03:00
Fringg a6dcf26c20 chore: ruff format and cleanup 2026-03-18 18:20:50 +03:00
Fringg 5081debee7 fix: add null check for subscription in execute_change_devices 2026-03-18 18:15:20 +03:00
Fringg 0ceff44c30 fix: sync crypto link from happ.cryptoLink in webhook handlers
Webhook only checked subscriptionCryptoLink field, missing happ.cryptoLink
fallback that sync already used. Also clear stale crypto link when URL
changes but no new crypto link is provided.
2026-03-18 18:05:53 +03:00