connected_squads JSON contains squad UUIDs like 'b4d782fa-...', not
integer IDs. int() cast fails on these. Now resolves UUIDs to integer
IDs via get_server_ids_by_uuids() before passing to remove_user_from_servers.
When a handler swallows a DB error (e.g. ProgrammingError for missing
column), the transaction is aborted but the handler returns normally.
The auth middleware then tries db.commit() which fails with DBAPIError.
Now catches any exception on commit and does rollback, preventing the
cascade of "current transaction is aborted" errors through all
subsequent middleware layers.
1. connected_squads JSON stores IDs as strings but server_squads.id is
integer — cast to int before passing to remove_user_from_servers
2. Wrap remove_user_from_servers in its own db.begin_nested() so its
failure doesn't abort the parent savepoint (subscription deletion)
3. Pre-fetch admin.id before delete_user_account to avoid MissingGreenlet
when transaction rollback expires the ORM object
When one deletion step fails (e.g. missing campaign_id column in referral_earnings),
PostgreSQL aborts the entire transaction. All subsequent operations then fail with
"current transaction is aborted, commands ignored until end of transaction block".
Each of the 24 try/except blocks now uses `async with db.begin_nested():`
(PostgreSQL SAVEPOINT) so individual failures are isolated and rolled back
without poisoning the outer transaction.
Decrement server_squads.current_users BEFORE deleting subscription
to match lock ordering with webhook handler, preventing deadlocks.
Also made migration 0002 robust with table existence checks to
prevent failures on DBs missing referral_earnings or
advertising_campaign_registrations tables.
Migration was failing on DBs where referral_earnings or
advertising_campaign_registrations tables didn't exist yet,
causing campaign_id column to never be added. Added _has_table
and _has_column guards, wrapped backfill in existence check.
TypeDecorator with process_result_value guarantees naive datetimes
from pre-TIMESTAMPTZ databases are converted to UTC-aware on every
load. Replaces unreliable event listener approach. All 175 DateTime
columns now use AwareDateTime.
SQLAlchemy event listener on Base ensures all DateTime columns are
timezone-aware after loading from DB. Fixes TypeError crashes in
50+ comparison sites across handlers, services, and middlewares
for pre-TIMESTAMPTZ databases.
Databases that haven't run the TIMESTAMPTZ migration return naive
datetimes from end_date. Comparing with datetime.now(UTC) raises
TypeError. Added _aware() helper to normalize naive→aware in
is_active, is_expired, should_be_expired, actual_status, days_left,
time_left_display, and extend_subscription.
Existing databases stamped at 0001 (create_all checkfirst=True) are
missing new columns/tables from the partner system:
- users.partner_status
- broadcast_history.blocked_count
- advertising_campaigns.partner_user_id
- withdrawal_requests table
- partner_applications table
All checks are idempotent — safe for fresh and existing databases.
Adds nullable FK campaign_id to referral_earnings table, enabling
direct campaign ROI analytics without JOINing through registrations.
- Model: campaign_id column + AdvertisingCampaign relationship
- CRUD: get_user_campaign_id() helper, campaign_id param in create_referral_earning
- Service: resolve campaign_id in all earning creation paths
- Cabinet API: campaign_name in earnings response
- Migration 0002: add column + deterministic backfill via DISTINCT ON
Only apply alembic.ini logging config when root logger has no handlers
(CLI mode). When running programmatically, structlog is already configured
and fileConfig would replace its handlers, breaking all logging.
The column existed in the SQLAlchemy model and Alembic migration but was
missing from universal_migration.py which is used for auto-migrations on
startup, causing "column broadcast_history.blocked_count does not exist"
error in the broadcasts admin page.
- Add partner_user_id/partner_name to campaign list and detail responses
- Add partner_user_id to campaign create/update schemas
- Add GET /available-partners endpoint for partner dropdown
- Atomic assign with UPDATE...WHERE to prevent race conditions
- Validate partner exists and is approved in create/update
- Set updated_at on assign/unassign operations
- Eager-load partner relationship in campaign queries
- GET/PATCH /admin/partners/settings endpoints with .env persistence
- New config: REFERRAL_WITHDRAWAL_REQUISITES_TEXT, REFERRAL_PARTNER_SECTION_VISIBLE
- Serve requisites_text in withdrawal balance and partner_section_visible in referral terms
- Sanitize newlines in requisites_text before .env write to prevent injection
Catch NotFoundError (404) separately from generic exceptions.
Old/expired payments return 404 from YooKassa API — this is expected
and should be logged as WARNING without traceback, not ERROR.
- replace unsafe referral code generator with unique DB-checked version
- remove dead code in get_global_partner_stats
- validate status filter params with Literal types in admin routes
- fix N+1 query in money laundering analysis with GROUP BY batch query
- fix N+1 query in cabinet referral earnings with batch user fetch
- eliminate double balance stats computation in withdrawal flow
- replace in-memory referral counting with SQL COUNT/CASE aggregation
- fix HTML injection in admin Telegram notifications via html.escape()
- standardize return types for reject/complete withdrawal methods
- Add SELECT FOR UPDATE locking on all financial state transitions
(withdrawal approve/reject/complete/create, partner approve/reject)
- Add html.escape() on all user-controlled values in email templates
- Wrap sync SMTP send_email in asyncio.to_thread to avoid blocking event loop
- Add missing database indexes on referral_earnings(user_id, referral_id),
users(referred_by_id, partner_status), withdrawal_requests(user_id, status),
advertising_campaigns(partner_user_id)
Two separate fixes for bot and cabinet auth paths:
Bot (start.py): store referrer_id from campaign.partner_user_id in FSM
state, skip referral code prompt when partner already set.
Cabinet (auth.py): in _process_campaign_bonus, set user.referred_by_id
to campaign.partner_user_id and call process_referral_registration.
Both paths now correctly attribute campaign users to the partner,
enabling commission earnings from their future purchases.
When a user registers through a campaign link that has partner_user_id,
store that partner as referrer_id in FSM state. This connects the
campaign system to the referral earning system — the partner now earns
commissions from all purchases made by users who came through their
campaign links.
Changes in all registration paths:
- cmd_start: store referrer_id from campaign.partner_user_id
- language/rules/privacy handlers: skip referral code prompt when
referrer_id already set from campaign
- channel check: pick up referrer_id from state instead of hardcoding None