feat: add billing statement emails#2226
Open
specialpointcentral wants to merge 2 commits intoWei-Shaw:mainfrom
Open
feat: add billing statement emails#2226specialpointcentral wants to merge 2 commits intoWei-Shaw:mainfrom
specialpointcentral wants to merge 2 commits intoWei-Shaw:mainfrom
Conversation
Contributor
There was a problem hiding this comment.
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
BillingStatementEmailServicethat 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) | ||
| } |
a620cf4 to
dbd64fb
Compare
dbd64fb to
72b5107
Compare
72b5107 to
680ab5b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Notes
Tests