Skip to content

feat: add billing statement emails#2226

Open
specialpointcentral wants to merge 2 commits intoWei-Shaw:mainfrom
specialpointcentral:feature/billing-statement-emails
Open

feat: add billing statement emails#2226
specialpointcentral wants to merge 2 commits intoWei-Shaw:mainfrom
specialpointcentral:feature/billing-statement-emails

Conversation

@specialpointcentral
Copy link
Copy Markdown

Summary

  • Add per-user timezone preferences and expose server default timezone so billing periods can be calculated in the user's timezone.
  • Add admin billing statement email settings for daily/weekly/monthly reports and user-side billing statement email preferences.
  • Send bilingual billing statement emails with account billing/user charge summaries, model/group breakdowns, and unsubscribe guidance.

Notes

  • User timezone and billing statement preferences are stored in the existing settings table, avoiding a schema migration.
  • Billing detail rows display group names when available and are sorted by model, group, and billing mode.

Tests

  • Not run after branch split; changes were previously validated with frontend typecheck and Docker build on the combined branch.

Copilot AI review requested due to automatic review settings May 6, 2026 11:36
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces configurable billing statement email reporting, adds per-user timezone preferences (with server timezone exposure), and wires a new backend background service to periodically send bilingual billing statement emails based on admin schedules and user opt-in preferences.

Changes:

  • Add per-user timezone preference and expose server default timezone via public settings.
  • Add admin billing statement email configuration (enable flags + cron schedules) and user-side opt-in preferences (daily/weekly/monthly).
  • Implement and wire a backend BillingStatementEmailService that aggregates usage and sends periodic statement emails.

Reviewed changes

Copilot reviewed 30 out of 30 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
frontend/src/views/user/ProfileView.vue Loads server timezone + billing statement availability from public settings and renders new billing statement preference card.
frontend/src/views/admin/SettingsView.vue Adds UI to configure billing statement email enable flags and cron schedules; persists JSON config.
frontend/src/types/index.ts Extends User and PublicSettings types with timezone and billing statement fields.
frontend/src/stores/app.ts Adds default values for new public settings fields in app store.
frontend/src/i18n/locales/zh.ts Adds Chinese strings for timezone help and billing statement settings (user + admin).
frontend/src/i18n/locales/en.ts Adds English strings for timezone help and billing statement settings (user + admin).
frontend/src/constants/timezone.ts Introduces shared timezone options and offset label helper for consistent timezone UI.
frontend/src/components/user/profile/ProfileInfoCard.vue Passes timezone-related props down to the profile edit form.
frontend/src/components/user/profile/ProfileEditForm.vue Adds timezone selection and submits timezone updates via the user profile API.
frontend/src/components/user/profile/ProfileBillingStatementCard.vue New component to toggle per-period billing statement email preferences.
frontend/src/components/account/QuotaLimitCard.vue Reuses shared timezone constants instead of a local timezone list.
frontend/src/components/account/QuotaDimensionRow.vue Reuses shared timezone offset label helper instead of duplicating it.
frontend/src/api/user.ts Extends updateProfile request type to include timezone and billing statement preference fields.
frontend/src/api/admin/settings.ts Adds billing_statement_email_config to system settings DTOs.
backend/internal/service/wire.go Wires the new billing statement email service into the service provider set (auto-start).
backend/internal/service/user.go Adds non-persisted user fields for timezone and billing statement preferences (hydrated from settings).
backend/internal/service/user_service.go Implements hydration + update of timezone and billing statement preferences via settings.
backend/internal/service/settings_view.go Adds public settings fields for billing statement availability and server timezone; adds system setting field for billing config JSON.
backend/internal/service/setting_service.go Exposes billing statement availability + server timezone through public settings and injection payload; persists billing config JSON.
backend/internal/service/domain_constants.go Adds setting keys for user timezone and billing statement config/preferences.
backend/internal/service/billing_statement_email_service.go New background service: cron evaluation, leader lock, user iteration, usage aggregation, and email sending.
backend/internal/service/billing_statement_email_service_test.go Adds unit tests for config parsing, email validation, and email HTML rendering.
backend/internal/handler/user_handler.go Accepts timezone and billing statement preference fields in profile update requests.
backend/internal/handler/setting_handler.go Returns new public settings fields (billing statement availability + server timezone).
backend/internal/handler/dto/types.go Extends user DTO with timezone and billing statement preference fields.
backend/internal/handler/dto/settings.go Extends system/public settings DTOs with billing statement config/availability + server timezone.
backend/internal/handler/dto/mappers.go Maps new timezone and billing statement preference fields from service user to DTO.
backend/internal/handler/admin/setting_handler.go Adds billing statement config JSON to admin settings get/update flows.
backend/cmd/server/wire.go Ensures the billing statement email service is stopped during application cleanup.
backend/cmd/server/wire_gen.go Regenerates wiring to include billing statement email service creation and cleanup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

</option>
</select>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('profile.timezoneHelp', { timezone: serverTimezone || 'UTC' }) }}
})
authStore.user = updatedUser
appStore.showSuccess(t('profile.billingStatement.saved'))
} catch (error: any) {
Comment on lines +6030 to +6033
daily_schedule: '0 8 * * *',
weekly_schedule: '0 8 * * 1',
monthly_schedule: '0 8 1 * *',
})
}

// hydrateBillingStatementPreference reads the user's billing statement preference from settings
// and populates the User struct fields. Defaults to all-enabled if not found.
Comment on lines 241 to 253
// GetProfile 获取用户资料
func (s *UserService) GetProfile(ctx context.Context, userID int64) (*User, error) {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("get user: %w", err)
}
normalizeLoadedUserTokenVersion(user)
if err := s.hydrateUserAvatar(ctx, user); err != nil {
return nil, fmt.Errorf("get user avatar: %w", err)
}
s.hydrateBillingStatementPreference(ctx, user)
s.hydrateUserTimezone(ctx, user)
return user, nil

func TestBuildBillingStatementEmailHTML_Nil(t *testing.T) {
html := buildBillingStatementEmailHTML(nil)
if html != "<p>No data.</p>" {
Comment on lines +335 to +337
// Time to run this statement
s.setLastRunAt(ctx, d.kind, now)
s.sendStatements(ctx, d.kind, d.name, now)
Comment on lines +272 to +274
ctx, cancel := context.WithTimeout(s.stopCtx, 120*time.Second)
defer cancel()

Comment on lines +360 to +384
func (s *BillingStatementEmailService) sendStatements(ctx context.Context, kind string, periodName string, now time.Time) {
page := 1
for {
users, pageResult, err := s.userRepo.List(ctx, pagination.PaginationParams{
Page: page,
PageSize: billingStatementUserPageSize,
})
if err != nil {
log.Printf("[BillingStatement] list users page=%d error: %v", page, err)
return
}

for i := range users {
user := &users[i]
if !isValidEmailForBilling(user.Email) {
continue
}
// Check user preference for this period kind
if !s.isUserPeriodEnabled(ctx, user.ID, kind) {
continue
}
loc := s.userLocation(ctx, user.ID)
start, end := billingStatementPeriodRange(kind, now, loc)
s.sendStatementToUser(ctx, user, periodName, start, end, loc)
}
@specialpointcentral specialpointcentral force-pushed the feature/billing-statement-emails branch 2 times, most recently from a620cf4 to dbd64fb Compare May 7, 2026 05:07
@specialpointcentral specialpointcentral force-pushed the feature/billing-statement-emails branch from dbd64fb to 72b5107 Compare May 7, 2026 15:15
@specialpointcentral specialpointcentral force-pushed the feature/billing-statement-emails branch from 72b5107 to 680ab5b Compare May 7, 2026 15:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants