diff --git a/backend/internal/service/balance_notify_service.go b/backend/internal/service/balance_notify_service.go index ba1c7037..053451e1 100644 --- a/backend/internal/service/balance_notify_service.go +++ b/backend/internal/service/balance_notify_service.go @@ -192,11 +192,10 @@ func (s *BalanceNotifyService) getAccountQuotaNotifyEmails(ctx context.Context) var recipients []string seen := make(map[string]bool) for _, entry := range entries { - if entry.Disabled { + if entry.Disabled || !entry.Verified { continue } email := strings.TrimSpace(entry.Email) - // email="" placeholder is not resolved here; admin should configure actual emails if email == "" { continue } @@ -219,20 +218,17 @@ func (s *BalanceNotifyService) getSiteName(ctx context.Context) string { return name } -// collectBalanceNotifyRecipients collects all non-disabled email recipients for balance notifications. -// Entries with email="" are resolved to the user's primary email. +// collectBalanceNotifyRecipients returns verified, non-disabled email recipients. +// Only emails with verified=true and disabled=false are included. func (s *BalanceNotifyService) collectBalanceNotifyRecipients(user *User) []string { var recipients []string seen := make(map[string]bool) for _, entry := range user.BalanceNotifyExtraEmails { - if entry.Disabled { + if entry.Disabled || !entry.Verified { continue } email := strings.TrimSpace(entry.Email) - if email == "" { - email = user.Email // Resolve primary email placeholder - } if email == "" { continue } @@ -244,11 +240,6 @@ func (s *BalanceNotifyService) collectBalanceNotifyRecipients(user *User) []stri recipients = append(recipients, email) } - // If no entries exist at all (legacy/empty), fall back to user's primary email - if len(user.BalanceNotifyExtraEmails) == 0 && user.Email != "" { - recipients = append(recipients, user.Email) - } - return recipients } diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 1203f0c6..d68a7771 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -7338,9 +7338,15 @@ func finalizePostUsageBilling(p *postUsageBillingParams, deps *billingDeps) { deps.deferredService.ScheduleLastUsedUpdate(p.Account.ID) - // Balance low notification + // Balance low notification — use real-time balance from billing cache (not stale snapshot) if !p.IsSubscriptionBill && p.Cost.ActualCost > 0 && p.User != nil && deps.balanceNotifyService != nil { - deps.balanceNotifyService.CheckBalanceAfterDeduction(context.Background(), p.User, p.User.Balance, p.Cost.ActualCost) + oldBalance := p.User.Balance // fallback to snapshot + if deps.billingCacheService != nil { + if realBalance, err := deps.billingCacheService.GetUserBalance(context.Background(), p.User.ID); err == nil { + oldBalance = realBalance + p.Cost.ActualCost // DB already deducted, reconstruct pre-deduction balance + } + } + deps.balanceNotifyService.CheckBalanceAfterDeduction(context.Background(), p.User, oldBalance, p.Cost.ActualCost) } // Account quota notification (use same cost formula as postUsageBilling) diff --git a/backend/internal/service/notify_email_entry.go b/backend/internal/service/notify_email_entry.go index 3caf689f..c0e739f4 100644 --- a/backend/internal/service/notify_email_entry.go +++ b/backend/internal/service/notify_email_entry.go @@ -80,28 +80,3 @@ func MarshalNotifyEmails(entries []NotifyEmailEntry) string { return string(data) } -// filterEnabledEmails returns only non-disabled email addresses from entries. -// Empty email placeholders are skipped (caller should resolve them separately). -func FilterEnabledEmails(entries []NotifyEmailEntry) []string { - var result []string - for _, e := range entries { - if e.Disabled { - continue - } - email := strings.TrimSpace(e.Email) - if email != "" { - result = append(result, email) - } - } - return result -} - -// isPrimaryDisabled checks if the primary email placeholder (email="") exists and is disabled. -func IsPrimaryDisabled(entries []NotifyEmailEntry) bool { - for _, e := range entries { - if e.Email == "" { - return e.Disabled - } - } - return false // No primary placeholder = not disabled -} diff --git a/frontend/src/components/user/profile/ProfileBalanceNotifyCard.vue b/frontend/src/components/user/profile/ProfileBalanceNotifyCard.vue index 1d88ad82..69b10b33 100644 --- a/frontend/src/components/user/profile/ProfileBalanceNotifyCard.vue +++ b/frontend/src/components/user/profile/ProfileBalanceNotifyCard.vue @@ -48,8 +48,9 @@
-
- + + +
@@ -57,21 +58,19 @@
- - {{ entry.email === '' ? userEmail : entry.email }} - + {{ entry.email }}
- {{ t('profile.balanceNotify.primaryEmail') }} - {{ t('profile.balanceNotify.unverified') }} -
- +
@@ -130,7 +129,7 @@