Commit Graph

3013 Commits

Author SHA1 Message Date
Fringg 275f249bbd fix: encode payment status in provider return URLs and wire failed_url
- Add &status=success/failed to cabinet return URLs for instant UX feedback
  without needing API auth in external browser
- Platega: pass cabinet_failed_url (was hardcoded to server URL)
- Heleket: add success_url param, pass cabinet_success_url for url_success
- WATA: add failed_url param, pass cabinet_failed_url to failRedirectUrl
- CloudPayments: add failed_url param, pass cabinet_failed_url
- Strip trailing slash from CABINET_URL for safety
2026-03-09 04:59:56 +03:00
Fringg 7a9264b173 fix: latest-payment endpoint returns all payments, not just pending
The /latest endpoint was using list_recent_pending_payments which only
returns unpaid payments. By the time the user returns from the payment
provider, the webhook has already marked the payment as paid, so the
endpoint returned 404. Now queries the payment table directly without
filtering by is_paid status.
2026-03-09 04:41:25 +03:00
Fringg 32d58b04b9 fix: add method query param to return_url and latest-payment endpoint
Payment providers redirect to external browser where sessionStorage is
unavailable. Now includes method in return_url query params and adds
GET /pending-payments/{method}/latest endpoint so TopUpResult can poll
payment status without sessionStorage data.
2026-03-09 04:34:14 +03:00
Fringg 7ca96195a7 fix: pass cabinet return_url to payment providers for top-up redirects
Payment providers were redirecting users back to the bot after completing
cabinet top-up payments. Now passes CABINET_URL/balance/top-up/result as
return_url to YooKassa, Platega, Heleket, WATA, and CloudPayments.
2026-03-09 04:16:10 +03:00
Fringg 5752b5e7c6 chore: apply ruff formatting to 4 files 2026-03-09 03:02:32 +03:00
Fringg 2649e12f64 fix: use parsed HTML length for Telegram caption limit checks
Replace hardcoded raw HTML length checks (len(text) <= 900/1000/1024)
with centralized caption_exceeds_telegram_limit() that strips HTML tags
and unescapes entities before measuring against the real 1024-char limit.
Fixes logo disappearing when promo discounts add HTML markup to captions.
2026-03-09 02:43:43 +03:00
Fringg 4a5cacda38 fix: resolve concurrent AsyncSession bug and sanitize error responses
- Fix critical concurrency issue in propagate_tariff_squads: preload
  users/tariffs before asyncio.gather, use single API client, no DB
  operations inside gather, single commit after all API calls
- Replace all str(e) leaks in admin_users.py with sanitized messages
- Fix double callback.answer by using callback.message.answer for
  failure alerts
- Move PropagateSquadsResult to module level, use field(default_factory)
- Compute traffic_strategy once before gather instead of N times
- Add warning logging on tariff refresh failures
- Reset synced counters on commit failure for accurate reporting
2026-03-09 02:26:38 +03:00
Fringg 79161eaae4 refactor: move squad propagation to service layer with parallel Remnawave sync
- Move _propagate_squads_to_subscriptions from handler to
  SubscriptionService.propagate_tariff_squads()
- Use asyncio.gather with semaphore (concurrency=5) for parallel
  Remnawave API calls instead of sequential O(N)
- Track failed subscription IDs for better observability
- Fix get_all_server_squads limit=50 default in admin handlers
  (now limit=10000 to prevent silent truncation)
- Add docstring to force_panel_delete parameter
- Return PropagateSquadsResult dataclass with total/synced/failed_ids
2026-03-09 01:58:23 +03:00
Fringg 289cbe966e fix: conditional log messages and sanitize panel_error in user deletion
- Log disable success/failure separately instead of unconditional success
- Sanitize panel_error to not leak internal exception details to API
- Make fallback disable log conditional on actual result
2026-03-09 01:52:37 +03:00
Fringg 7ccfb66690 fix: propagate tariff squad changes to existing subscriptions and fix user deletion from Remnawave
Squad toggle: when admin changes servers for a tariff, the changes now
propagate to all active/trial subscriptions and sync to Remnawave panel.
Previously only took effect on new purchases.

User deletion: full delete from Cabinet now actually deletes from Remnawave
panel. Previously lied about panel deletion status and skipped deletion
for users with active subscriptions.
2026-03-09 01:46:45 +03:00
Fringg 536525c9c0 fix: admin tariff server selection - 64-byte overflow and callback routing conflicts
1. Shortened squad toggle callback_data from admin_tariff_toggle_squad
   to trf_sq to stay within Telegram's 64-byte callback_data limit
   (was overflowing at tariff_id >= 10)

2. Fixed toggle_tariff handler capturing squad/promo/daily/traffic_topup
   toggle callbacks by adding exclusion filters

3. Fixed admin_tariff_edit_traffic capturing admin_tariff_edit_traffic_topup
   by registering traffic_topup handler before traffic handler

4. Fixed admin_tariff_delete capturing admin_tariff_delete_confirm
   by registering delete_confirm handler before delete handler
2026-03-09 01:23:03 +03:00
Fringg 4186159a61 fix: keep DB session alive in Tribute payment notification handler
The _send_success_notification method was closing the DB session (via
break) before calling send_cart_notification_after_topup, causing all
post-topup auto-renewal logic to silently fail for Tribute payments.

Moved break after all work is done so the session stays open during
auto-renewal operations. Added None guard for user lookup.
2026-03-09 01:07:47 +03:00
Fringg 6349b2f442 fix: align tariff pricing with calculate_renewal_price reference
- balance/main.py: single period discount on combined total (base + devices),
  add promo-offer discount, fix device_limit fallback to tariff_device_limit
- pricing.py: same combined discount + promo-offer, proper device_limit
  fallback matching reference (is not None check)
- admin/users.py: delegate to calculate_renewal_price() which handles both
  tariff and classic modes correctly, removing classic-only calculate_subscription_price
- menu.py: use renewal_service.calculate_pricing() for both price check and
  charge to ensure consistency, add try/except with user-facing error,
  show actual charged amount in success message
2026-03-09 00:39:16 +03:00
Fringg bfbefeb1e2 fix: renewal cost estimate double-counts servers and traffic in tariff mode
In tariff mode, period_prices already includes servers and traffic costs.
But show_payment_methods() and get_subscription_cost() were using the classic
additive formula, adding server and traffic prices on top of the tariff price.

Example: 49₽ tariff + 150₽ server + 150₽ traffic = 349₽ shown, should be 49₽.

Now both functions detect tariff mode and only add extra device costs beyond
the tariff's device_limit. Classic mode formula unchanged.
2026-03-08 23:14:57 +03:00
Fringg f9f07f360c fix: enforce tariff device_price and max_device_limit across all purchase paths
The miniapp, legacy cabinet endpoint, auto-purchase service, and Telegram bot
handlers were using only global settings (PRICE_PER_DEVICE, MAX_DEVICES_LIMIT)
for device purchases, completely ignoring tariff-level device_price_kopeks and
max_device_limit. This allowed users to buy devices when tariff price was 0
(should be blocked) and exceed the tariff's max device limit.

Fixed in all 4 code paths:
- miniapp _build_subscription_settings + update_subscription_devices_endpoint
- cabinet legacy POST /devices (+ added subscription status check, RemnaWave sync)
- subscription_auto_purchase_service._auto_add_devices
- telegram bot handlers confirm_change_devices, execute_change_devices, confirm_add_devices
2026-03-08 23:08:32 +03:00
Fringg 770b31d3d0 feat: auto-resume disabled daily subscriptions on balance topup
- Add try_resume_disabled_daily_after_topup() for instant resume when balance is topped up
- Fix all 5 resume paths to charge daily fee BEFORE activating subscription
- Remove unsafe inline auto-resume from add_user_balance() that bypassed fee charging
- Add NULL-safe is_daily_paused filter in subscription queries
- Use create_remnawave_user() instead of enable_remnawave_user() for full VPN panel sync
- Add DAILY_SUBSCRIPTION_RESUMED_AFTER_TOPUP localization key (ru, en, fa, zh, ua)
2026-03-08 21:36:17 +03:00
Fringg 928e3e98f8 chore: format balance/main.py and promocode.py 2026-03-08 15:41:59 +03:00
Fringg 7dc5e4ab94 fix: auto-purchase classic extend missing device_limit and traffic_limit_gb
- Add device_limit and traffic_limit_gb to classic extend cart data in
  confirm_extend_subscription handler
- Add classic mode branch in cabinet renew_subscription to save
  device_limit and traffic_limit_gb (previously only saved for tariffs)
- Ensure device_limit >= DEFAULT_DEVICE_LIMIT when converting trial
  subscription to paid via auto-extend
- Add None guards for subscription.device_limit in both trial and
  non-trial branches of _apply_extension_updates
2026-03-08 15:31:58 +03:00
Fringg 5ebe1072c9 fix: quick topup buttons include device/server/traffic costs, broadcast button crash on media messages
- Quick amount buttons now calculate full renewal cost (base + devices + servers + traffic with discounts)
- Tariff mode uses tariff-specific device pricing (device_price_kopeks, device_limit)
- Broadcast inline buttons no longer crash with "no text in message to edit" on photo/video messages
- Media messages are now handled in _edit_with_photo: delete old message + send new text
2026-03-08 14:58:17 +03:00
Fringg 20727b1017 fix: respect send_before_menu flag for pinned messages during new user registration
All 6 registration paths now check pinned_message.send_before_menu
to send pinned message before or after the menu, matching the
existing user flow behavior.
2026-03-08 14:25:54 +03:00
Fringg f4eeb9a503 fix: multiple payment and notification bugs
- CloudPayments: add missing process_referral_topup, has_made_first_topup flag, and admin notification (matching other adapters)
- Promocode: handle TelegramBadRequest for broadcast messages without text (fallback to answer())
- Devices: unify price prorating to day-based calculation (matching cabinet behavior)
- Auth: pass Bot instance to process_referral_registration for campaign referral notifications
- Wata: remove WATA_TERMINAL_PUBLIC_ID from is_wata_enabled() (not used in API calls), make type field conditional, add transactionId webhook fallback
2026-03-08 14:17:44 +03:00
Fringg 1f664a9083 fix: remove is_active_paid_subscription guard from admin deactivation
The guard silently blocked admins from deactivating active paid
subscriptions, returning a generic error with no explanation.
Admin deactivation is intentional (with confirmation step) and
should not be prevented. The guard remains in automated processes
(monitoring, broadcast, user_service) where it makes sense.
2026-03-08 13:05:52 +03:00
Fringg 330d1cb6fe fix: gift purchase notification and activation flow
- Refresh purchase.user relationship after setting user_id to fix
  stale None value that prevented Telegram gift notifications
- Route gift purchases with expired subscriptions through
  PENDING_ACTIVATION instead of auto-activating
- Hide subscription URL from gift buyer in API response
2026-03-08 12:47:25 +03:00
Fringg 9ba61a0879 feat: add telegram gift notification with inline activation button
- New gift_activation handler for gift_activate:{id} callback buttons
- Send Telegram notification to gift recipients with activate button
- Add skip_notification param to activate_purchase to prevent duplicates
- Fix telegram username regex minimum length (4→5 chars) in landing routes
- Add BOT_TOKEN guard in telegram gift notification sender
- Pre-resolve notification params before commit to avoid DetachedInstanceError
2026-03-07 20:39:04 +03:00
Fringg fbe56c15ac style: ruff format 2026-03-07 17:13:56 +03:00
Fringg d72ea6b7f9 fix: remaining context_vars/SAMPLE_CONTEXTS mismatches found by agents
- traffic_reset: traffic_limit → reset_gb, current_limit_gb
- payment_received: amount → formatted_amount
- guest_activation_required: added missing is_gift to context_vars
- daily_debit SAMPLE_CONTEXTS: added missing amount_rubles, new_balance_rubles
- traffic_reset SAMPLE_CONTEXTS: aligned with runtime keys
- payment_received SAMPLE_CONTEXTS: added amount_rubles
2026-03-07 17:11:57 +03:00
Fringg c507634398 fix: align subscription_renewed/activated context_vars with runtime keys
- subscription_renewed: new_end_date → new_expires_at
- subscription_activated: end_date → expires_at
- Added traffic_limit_gb and device_limit to both types
- Fixed SAMPLE_CONTEXTS keys to match
2026-03-07 17:11:57 +03:00
Fringg c9ea2b15e9 fix: strip newlines from subject substitution, fix subscription notification context
- Defense-in-depth: strip \r\n from context values in subject line
  substitution to prevent email header injection at composition layer
- Add missing tariff_name to classic mode subscription notification context
  (prevents literal {tariff_name} in DB override templates)
- Remove SQLAlchemy model object from notification context dict
  (str() on model produces garbage in DB override templates)
2026-03-07 17:11:57 +03:00
Fringg ab5313a381 fix: align context_vars and SAMPLE_CONTEXTS with actual runtime context keys
TEMPLATE_TYPES context_vars showed wrong placeholder names to admins
(e.g. 'amount' instead of 'formatted_amount', 'balance' instead of
'formatted_balance'). SAMPLE_CONTEXTS had mismatched keys causing
test emails to render with empty values.

Fixed types: balance_topup, balance_change, autopay_success,
autopay_insufficient_funds, daily_debit, daily_insufficient_funds,
referral_bonus.
2026-03-07 17:11:57 +03:00
Fringg 351d714f2d fix: substitute sample context in admin test email for template overrides
Same bug as notification_delivery_service — send_test_email used
get_template_override (raw template) instead of get_rendered_override.
Admins testing custom templates saw literal {days_left} placeholders
instead of sample values.
2026-03-07 16:48:18 +03:00
Fringg d52c87b2b7 fix: substitute context variables in email template overrides
The send_notification method was calling get_template_override which
returns raw template HTML without variable substitution. Placeholders
like {days_left} and {expires_at} were sent to users as literal text.

Switched to get_rendered_override which properly substitutes context
variables via str.replace before wrapping in the base email template.
2026-03-07 16:40:35 +03:00
Fringg d9f9f3dca1 fix: add or [] guard to remaining connected_squads call site in fulfill_purchase
Missed the create_paid_subscription branch (line 275) in the previous
commit — now all 4 call sites consistently use `or []` to guard against
None from tariff.allowed_squads JSON column.
2026-03-07 16:34:15 +03:00
Fringg 44d46feb0a fix: correct device_limit and connected_squads in guest purchase fulfillment
- Remove `or settings.DEFAULT_DEVICE_LIMIT` from fulfill_purchase expired
  subscription branch — tariff.device_limit is NOT NULL so the `or` pattern
  would incorrectly convert 0 (unlimited) to DEFAULT_DEVICE_LIMIT
- Add `or []` guard to connected_squads in activate_purchase to match
  replace_subscription's list[str] type contract (tariff.allowed_squads
  can be None from JSON column)
2026-03-07 16:29:04 +03:00
Fringg 9e78509284 fix: handle expired subscription in guest purchase fulfillment
When a user with an expired subscription makes a landing page purchase,
the code tried to INSERT a new subscription, violating the user_id
unique constraint. Now uses replace_subscription for expired/inactive
subscriptions instead of create_paid_subscription.
2026-03-07 16:02:06 +03:00
Fringg f4ab174d32 fix: support {total_amount} placeholder in cart notification templates
Add total_amount as an alias for cart_total in format() calls so custom
locale overrides using {total_amount} are properly substituted instead
of appearing as literal text in user messages.
2026-03-07 15:50:42 +03:00
Fringg 9d5329d9d1 fix: resolve NameError in YooKassa successful payment processing
event_object was referenced in _process_successful_yookassa_payment
but never passed to the method, causing all YooKassa webhook payments
to fail. Use payment.amount_kopeks from the database model instead.
2026-03-07 13:28:25 +03:00
Fringg bbd353ff38 fix: resolve alembic migration failures on fresh database install
Migration 0001 uses Base.metadata.create_all() which creates ALL tables
from current models.py, causing subsequent migrations (0015+) to fail
with "already exists" errors when they try to re-create constraints,
indexes, columns, and tables.

Three-layer fix:

1. migrations.py: detect fresh DB (no tables) and bootstrap via
   create_all() + stamp head, bypassing all migrations entirely.

2. models.py: add EmailTemplate model, CheckConstraints to LandingPage,
   and indexes to GuestPurchase so create_all() produces a complete
   schema identical to running all 30 migrations sequentially.

3. Idempotency guards in migrations 0015-0030: _has_unique_constraint,
   _has_table, _has_index, _has_column, _has_check_constraint checks
   before DDL operations, protecting against re-runs via make migrate.
2026-03-07 13:17:04 +03:00
Fringg 11d3e637c1 feat: add configurable animated background for landing pages
Add background_config JSON field to LandingPage model, enabling
per-landing animated backgrounds (aurora, sparkles, vortex, etc).

- Add background_config column (JSON, nullable) with Alembic migration
- Add validated background_config to create/update/detail/public schemas
- Reuse ALLOWED_BG_TYPES and _validate_settings from branding module
- Strip unknown keys via whitelist, validate all fields including reducedOnMobile
2026-03-07 12:46:02 +03:00
Fringg 0ba1127469 feat: add paginated purchases list endpoint for landing pages
Add GET /admin/landings/{id}/purchases with offset/limit pagination,
optional status filter (validated against GuestPurchaseStatus enum),
tariff name join, and truncated token display.
2026-03-07 09:49:57 +03:00
Fringg 25478ced20 feat: add landing page statistics endpoint with charts data
Add GET /admin/landings/{id}/stats endpoint returning:
- Summary stats (purchases, revenue, gifts, conversion rate)
- Daily breakdown for last 30 days (purchases, revenue, gifts per day)
- Tariff distribution (purchases and revenue per tariff)

Uses case() expressions for SQLite compatibility, timezone-safe
date grouping via func.timezone('UTC', ...), and composite index
(landing_id, status, paid_at) for query performance.
2026-03-07 09:35:21 +03:00
Fringg 372d628908 fix: remove executable bit from email_service.py 2026-03-07 07:14:52 +03:00
Fringg ceac29d5e3 style: format admin_landings.py 2026-03-07 07:13:01 +03:00
Egor 3ee108fce8 Merge pull request #2658 from thegrayfoxxx/fix_logo_from_show_qr
fix: reset QR photo when returning to referral
2026-03-07 07:09:38 +03:00
Egor de541ea1c3 Merge pull request #2682 from smediainfo/fix/email-message-id-headers
fix: add Message-ID and Date headers to outgoing emails
2026-03-07 07:08:37 +03:00
Fringg 6d65e15266 fix: read discount overrides from landing model instead of response DTO
After removing overrides from the public LandingDiscountInfo response,
_load_landing_tariffs still referenced discount.overrides which no
longer exists. Read from landing.discount_overrides directly.
2026-03-07 07:05:38 +03:00
Fringg aa7d98630d feat: add discount system for landing pages
Add time-bounded percentage discounts with per-tariff overrides
and countdown timer support for landing pages.

- Add 5 discount columns to LandingPage model (percent, overrides,
  starts_at, ends_at, badge_text) with Alembic migrations 0027-0028
- Add DB CHECK constraints for discount_percent range and date ordering
- Add discount price calculation in validate_and_calculate() and
  public landing config endpoint with consistent formula
- Add admin CRUD with Pydantic validation, cascade-clear on removal,
  merged date validation on partial updates, size bounds
- Remove discount_overrides from public API (baked into prices)
- Add size limits for allowed_tariff_ids and allowed_periods
2026-03-07 07:01:35 +03:00
Fringg 8b77cdae2c feat: add GET /admin/rbac/users endpoint for listing all RBAC users 2026-03-07 06:23:14 +03:00
Fringg c93dbec7a0 feat: add landings to permission registry
Register landings:read, landings:create, landings:edit, landings:delete
permissions so they appear in the role editor permission matrix.
2026-03-07 06:05:03 +03:00
Fringg fa21549cac fix: add activate hint to gift pending activation email link
Append ?activate=1 to success page URL in recipient notification email
for gift purchases with pending_activation status, so the frontend can
distinguish buyer from recipient and show the activate button only to
the recipient.
2026-03-07 06:00:29 +03:00
Fringg c10d6780ba feat: add external squad support for tariffs
- Add external_squad_uuid column to Tariff model with Alembic migration
- Add external_squad_uuid parameter to RemnaWave API create_user/update_user
- Pass external squad from tariff to RemnaWave on subscription creation/update
- Sync external squad in monitoring service, sync service, admin user management
- Clear external squad when tariff has none (consistent across all call sites)
- Add GET /available-external-squads endpoint with UUID validation and response model
- Update tariff schemas with UUID pattern validation
- Fix db.refresh to include tariff relationship for async safety
2026-03-07 05:44:19 +03:00