- Add 37 missing models to backup (payment providers, polls, contests,
wheel, FAQ, promo offers, webhooks, configs, menu buttons, etc.)
- Add tariff_promo_groups and payment_method_promo_groups association tables
- Replace hardcoded association restore with generic handler
- Fix transaction atomicity: flush instead of commit in inner methods,
remove inner rollback calls, single commit/rollback in outer handler
- Fix composite PK support for UserPromoGroup (was only detecting first PK)
- Fix duplicate insert bug when clear_existing=True and record already exists
- Add cabinet_refresh_tokens to clear list, fix support_audit_logs deletion order
- Add Time column parsing for ReferralContest.daily_summary_time
- Security: tarfile filter='data', path traversal protection in _restore_files
and delete_backup, os.sep in startswith checks
Only block purchase when the price increased (user would overpay).
When a promo discount activates between viewing price and confirming,
the recalculated price is lower — allow the purchase at the new price
instead of forcing the user to restart the checkout flow.
Only consider MINIAPP_CUSTOM_URL for miniapp buttons, not the
purchase-only MINIAPP_PURCHASE_URL which cannot display subscription
info and loads indefinitely. When no custom URL is configured, fall
back to regular callback_data so the bot shows subscription natively.
After db.rollback() all ORM objects expire. Subsequent attribute access
triggers lazy load in async context causing greenlet_spawn errors for
every remaining user. Break the sync loop after rollback instead of
continuing with a corrupted session.
Also downgrade TelegramNetworkError to warning in channel_checker.
When a user is deleted via cabinet, RemnaWave sends user.disabled webhook
but the subscription row is already cascade-deleted. This caused
StaleDataError on commit + PendingRollbackError when logging user.id.
Save user_id before handler call and catch StaleDataError as warning.
Add TelegramNetworkError handling before generic Exception catch in all
notification methods to prevent timeout errors from generating error
reports in chat. Timeouts are transient network issues, not bugs.
Channel checker middleware called bot.get_chat_member() which could
timeout (60s), causing callback.answer() to fail with "query too old".
Skip channel check for lightweight UI callbacks (webhook:close,
ban_notify:delete, noop). Also answer callback before delete attempt
and add fallback to remove keyboard if delete fails.
When subscription was extended in panel, webhook updated end_date but
left status as expired. Now syncs ACTIVE/DISABLED status from panel
payload when end_date is in the future.
_is_valid_url only accepted http(s), silently dropping valid deep links
like happ://, vless://, ss:// from revoked webhook payloads.
Added _is_valid_link that accepts any URI scheme.
Cabinet was calling CryptoBotService.create_invoice() directly without
saving CryptoBotPayment to DB. When webhook arrived, payment lookup
failed and returned HTTP 400, causing infinite retries.
Now cabinet uses PaymentService.create_cryptobot_payment() (same as
miniapp) with proper USD conversion via currency_converter.
Also return HTTP 200 for unknown invoice_ids to stop retry spam.
Add last_webhook_update_at timestamp to Subscription model. When a webhook
handler modifies a subscription, it stamps this field. Auto-sync, monitoring,
and force-check services skip subscriptions updated by webhook within the
last 60 seconds, preventing stale panel data from overwriting fresh
real-time changes.
- Add last_webhook_update_at column + migration
- Stamp all 8 webhook handlers with commit in every code path
- Add is_recently_updated_by_webhook() guard in 12 sync/monitoring paths
- Add REMNAWAVE_WEBHOOK_* variables to .env.example
- Add webhook setup documentation to README with Caddy/nginx examples
- Fix pre-existing yookassa webhook test (mock AsyncSessionLocal)
Add dismissible close button (✖️) to every webhook notification message.
Users can now close any webhook notification by tapping the button,
which deletes the message via webhook:close callback handler.
- Add keyboard buttons to all webhook notifications: renew, connect,
my subscription, buy traffic — context-appropriate per event type
- Extract device name from multiple possible payload fields (deviceName,
tag, hwid, device, platform, name) with fallback to dash
- Log payload keys for device events to identify correct field names
- Add html.escape() to all untrusted webhook data in admin and device
notifications (prevents HTML/Telegram injection)
- Add public send_webhook_notification() and is_enabled property to
AdminNotificationService (eliminates private method access)
- Add dedicated NotificationType enum values for device and not_connected
events (fixes incorrect semantic mapping)
- Extend user resolution to handle nested user objects and userUuid for
device-scope events
- Replace manual __anext__() DB session with AsyncSessionLocal context
manager; skip DB session for admin-only events
- Replace deprecated datetime.utcnow() with datetime.now(UTC)
- Use db.flush() instead of db.commit() in handlers (router commits)
- Wrap _notify_user in try/except to prevent notification failures from
rolling back successful DB mutations
Handle all 44 webhook events: admin alerts for node health (connection
lost/restored), service security (login attempts), CRM billing reminders,
plus user-facing device added/deleted and not_connected notifications
with localized messages across all 5 languages.
RemnaWave sends event as "user.modified", not "modified".
Concatenating scope + event produced "user.user.modified" which
didn't match any handler keys.
- Replace direct bot.send_message with notification_delivery_service
- Email-only and OAuth users now receive webhook notifications via email/WS
- Add 10 new NotificationType enum values for webhook subscription events
- Map all webhook text_keys to NotificationType for unified routing
Transaction created_at and completed_at showed identical timestamps
because webhook handlers created transactions with is_completed=True
in a single step. Now all 10 payment providers pass payment.created_at
to the transaction so created_at reflects when the user initiated
the payment, not when the webhook processed it.
Also: remove duplicate datetime import in inline.py, upgrade button
stats DB error logging from debug to warning, add index on
button_click_logs.button_type for analytics queries.
_process_heleket_payload deleted the invoice message on every call,
including manual "check status" presses. Now only deletes on final
statuses (paid, cancel, fail, etc.) so the payment UI stays visible
while the user is still waiting.
Also includes subscription fallback query fix (actual DB columns).
Subscription.is_active is a Python property, not a column — query
status/end_date/is_trial columns instead. Also restore subscription=None
initialization to avoid UnboundLocalError on line 112.
Rules editor crashed when preview truncated mid-HTML tag (e.g.
<blockquote> cut to <blockquo), causing Telegram parse error.
Strip HTML tags before truncating preview text.
Also fix MissingGreenlet in build_topup_success_keyboard: fall back
to a direct DB query instead of showing wrong button text.
Remove blocking check that prevented tariff deletion when subscriptions
exist. DB schema already supports SET NULL on tariff FK, so subscriptions
gracefully become "legacy" and users pick a new tariff on renewal.
Return affected_subscriptions count in API response.
Delete dead Flask-based PAL24 webhook server (app/external/pal24_webhook.py).
PAL24 webhooks already handled by unified FastAPI server on port 8080.
- Remove flask dependency from pyproject.toml and requirements.txt
- Remove PAL24_WEBHOOK_PORT config (unused, FastAPI uses shared port)
- Remove pal24_webhook module reference from log filter
- Update docs: webhook example rewritten from Flask to FastAPI
- Uninstall flask, werkzeug, blinker, itsdangerous
Remove AUTO_ACTIVATE_AFTER_TOPUP and SHOW_ACTIVATION_PROMPT_AFTER_TOPUP
features from all payment providers, config, system settings, and tests.
Cart auto-purchase (AUTO_PURCHASE_AFTER_TOPUP) is preserved.
Bug fixes:
- fix KeyError 'months' in devices.py for custom locale overrides
- fix IntegrityError on trial subscription retry (update existing PENDING instead of INSERT)
- fix PendingRollbackError cascade by adding db.rollback() before recovery
- fix TelegramForbiddenError not caught in photo_message.py
- fix "query is too old" spam in required_sub_channel_check
- add missing trial locale keys (TRIAL_PAYMENT_DESCRIPTION, TRIAL_REFUND_DESCRIPTION, TRIAL_ACTIVATION_ERROR)
After first logo upload, Telegram returns a file_id that can be reused
for all subsequent sends. This eliminates 3-4 second delay per message
caused by re-uploading the same file from disk every time.
Translate all bot strings to Persian, including admin panel, user interface, payment flows, contests, monitoring, and promotional features. Add RTL text support and Persian-specific formatting for dates, numbers, and currency displays.
sync_users_to_panel uses _safe_expire_at_for_panel which replaces past
end_dates with now+1min for expired subscriptions. When sync_users_from_panel
reads these artificial dates back, it treated them as legitimate "newer"
dates and overwrote all expired subscriptions' end_date to approximately
current time. This caused all subscription end dates to show as "just now"
after sync.
Fix: only update end_date from panel when the panel user status is ACTIVE.
For EXPIRED/DISABLED users, the panel date may be a _safe_expire_at artifact
and should not override the real expiry date in the local database.
The user restoration flow deleted transactions without first clearing
foreign key references from payment tables (yookassa_payments,
cryptobot_payments, etc.) and referral_earnings. This caused
IntegrityError when a deleted user had payment records linked to
transactions.
- Convert max_uses=0 to 999999 (unlimited) in cabinet and webapi routes,
matching bot handler behavior. Fixes miniapp-created promo codes being
immediately invalid due to is_valid check (current_uses < max_uses).
- Skip trial offer in post-registration keyboard when promo code already
activated a subscription, showing "back to menu" button instead.
PurchaseSelection dataclass has period: PurchasePeriodConfig (with .days),
not period_days. This caused admin notification to fail silently on every
subscription purchase from cabinet.
Admin "Clear all" button was deleting inactive users regardless of
subscription status, destroying paid subscriptions. Now matches the
monitoring service behavior by checking is_active before deletion.