Skip to content

Commit 72b5107

Browse files
feat: add billing statement emails
1 parent 034795e commit 72b5107

27 files changed

Lines changed: 1492 additions & 45 deletions

backend/cmd/server/wire.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ func provideCleanup(
9898
backupSvc *service.BackupService,
9999
paymentOrderExpiry *service.PaymentOrderExpiryService,
100100
channelMonitorRunner *service.ChannelMonitorRunner,
101+
billingStatementEmail *service.BillingStatementEmailService,
101102
) func() {
102103
return func() {
103104
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@@ -246,6 +247,12 @@ func provideCleanup(
246247
}
247248
return nil
248249
}},
250+
{"BillingStatementEmailService", func() error {
251+
if billingStatementEmail != nil {
252+
billingStatementEmail.Stop()
253+
}
254+
return nil
255+
}},
249256
}
250257

251258
infraSteps := []cleanupStep{

backend/cmd/server/wire_gen.go

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/cmd/server/wire_gen_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ func TestProvideCleanup_WithMinimalDependencies_NoPanic(t *testing.T) {
7777
nil, // backupSvc
7878
nil, // paymentOrderExpiry
7979
nil, // channelMonitorRunner
80+
nil, // billingStatementEmail
8081
)
8182

8283
require.NotPanics(t, func() {

backend/internal/handler/admin/setting_handler.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
263263
AvailableChannelsEnabled: settings.AvailableChannelsEnabled,
264264

265265
AffiliateEnabled: settings.AffiliateEnabled,
266+
267+
BillingStatementEmailConfig: settings.BillingStatementEmailConfig,
266268
}
267269

268270
// OpenAI fast policy (stored under a dedicated setting key)
@@ -569,6 +571,9 @@ type UpdateSettingsRequest struct {
569571
// 风控中心功能开关
570572
RiskControlEnabled *bool `json:"risk_control_enabled"`
571573

574+
// Billing statement email config (JSON string, only updated when non-empty)
575+
BillingStatementEmailConfig *string `json:"billing_statement_email_config,omitempty"`
576+
572577
// OpenAI fast/flex policy (optional, only updated when provided)
573578
OpenAIFastPolicySettings *dto.OpenAIFastPolicySettings `json:"openai_fast_policy_settings,omitempty"`
574579
}
@@ -1505,6 +1510,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
15051510
}
15061511
return previousSettings.RiskControlEnabled
15071512
}(),
1513+
BillingStatementEmailConfig: func() string {
1514+
if req.BillingStatementEmailConfig != nil {
1515+
return *req.BillingStatementEmailConfig
1516+
}
1517+
return previousSettings.BillingStatementEmailConfig
1518+
}(),
15081519
}
15091520

15101521
authSourceDefaults := &service.AuthSourceDefaultSettings{
@@ -1783,9 +1794,9 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
17831794

17841795
AvailableChannelsEnabled: updatedSettings.AvailableChannelsEnabled,
17851796

1786-
AffiliateEnabled: updatedSettings.AffiliateEnabled,
1787-
1788-
RiskControlEnabled: updatedSettings.RiskControlEnabled,
1797+
AffiliateEnabled: updatedSettings.AffiliateEnabled,
1798+
RiskControlEnabled: updatedSettings.RiskControlEnabled,
1799+
BillingStatementEmailConfig: updatedSettings.BillingStatementEmailConfig,
17891800
}
17901801
if fastPolicy, err := h.settingService.GetOpenAIFastPolicySettings(c.Request.Context()); err != nil {
17911802
slog.Error("openai_fast_policy_settings_get_failed", "error", err)

backend/internal/handler/dto/mappers.go

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,27 @@ func UserFromServiceShallow(u *service.User) *User {
1313
return nil
1414
}
1515
return &User{
16-
ID: u.ID,
17-
Email: u.Email,
18-
Username: u.Username,
19-
Role: u.Role,
20-
Balance: u.Balance,
21-
Concurrency: u.Concurrency,
22-
Status: u.Status,
23-
AllowedGroups: u.AllowedGroups,
24-
LastActiveAt: u.LastActiveAt,
25-
CreatedAt: u.CreatedAt,
26-
UpdatedAt: u.UpdatedAt,
27-
BalanceNotifyEnabled: u.BalanceNotifyEnabled,
28-
BalanceNotifyThresholdType: u.BalanceNotifyThresholdType,
29-
BalanceNotifyThreshold: u.BalanceNotifyThreshold,
30-
BalanceNotifyExtraEmails: NotifyEmailEntriesFromService(u.BalanceNotifyExtraEmails),
31-
TotalRecharged: u.TotalRecharged,
32-
RPMLimit: u.RPMLimit,
33-
Timezone: u.Timezone,
16+
ID: u.ID,
17+
Email: u.Email,
18+
Username: u.Username,
19+
Role: u.Role,
20+
Balance: u.Balance,
21+
Concurrency: u.Concurrency,
22+
Status: u.Status,
23+
AllowedGroups: u.AllowedGroups,
24+
LastActiveAt: u.LastActiveAt,
25+
CreatedAt: u.CreatedAt,
26+
UpdatedAt: u.UpdatedAt,
27+
BalanceNotifyEnabled: u.BalanceNotifyEnabled,
28+
BalanceNotifyThresholdType: u.BalanceNotifyThresholdType,
29+
BalanceNotifyThreshold: u.BalanceNotifyThreshold,
30+
BalanceNotifyExtraEmails: NotifyEmailEntriesFromService(u.BalanceNotifyExtraEmails),
31+
TotalRecharged: u.TotalRecharged,
32+
RPMLimit: u.RPMLimit,
33+
BillingStatementDailyEnabled: u.BillingStatementDailyEnabled,
34+
BillingStatementWeeklyEnabled: u.BillingStatementWeeklyEnabled,
35+
BillingStatementMonthlyEnabled: u.BillingStatementMonthlyEnabled,
36+
Timezone: u.Timezone,
3437
}
3538
}
3639

backend/internal/handler/dto/settings.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ type SystemSettings struct {
219219
// Affiliate (邀请返利) feature switch
220220
AffiliateEnabled bool `json:"affiliate_enabled"`
221221

222+
// Billing statement email config (JSON string)
223+
BillingStatementEmailConfig string `json:"billing_statement_email_config"`
224+
222225
// OpenAI fast/flex policy
223226
OpenAIFastPolicySettings *OpenAIFastPolicySettings `json:"openai_fast_policy_settings,omitempty"`
224227
}
@@ -283,8 +286,12 @@ type PublicSettings struct {
283286

284287
AffiliateEnabled bool `json:"affiliate_enabled"`
285288

286-
RiskControlEnabled bool `json:"risk_control_enabled"`
287-
ServerTimezone string `json:"server_timezone"`
289+
RiskControlEnabled bool `json:"risk_control_enabled"`
290+
BillingStatementEmailEnabled bool `json:"billing_statement_email_enabled"`
291+
BillingStatementDailyEnabled bool `json:"billing_statement_daily_enabled"`
292+
BillingStatementWeeklyEnabled bool `json:"billing_statement_weekly_enabled"`
293+
BillingStatementMonthlyEnabled bool `json:"billing_statement_monthly_enabled"`
294+
ServerTimezone string `json:"server_timezone"`
288295
}
289296

290297
type LoginAgreementDocument struct {

backend/internal/handler/dto/types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ type User struct {
3030
RPMLimit int `json:"rpm_limit"`
3131
Timezone string `json:"timezone"`
3232

33+
// 账单邮件偏好
34+
BillingStatementDailyEnabled bool `json:"billing_statement_daily_enabled"`
35+
BillingStatementWeeklyEnabled bool `json:"billing_statement_weekly_enabled"`
36+
BillingStatementMonthlyEnabled bool `json:"billing_statement_monthly_enabled"`
37+
3338
APIKeys []APIKey `json:"api_keys,omitempty"`
3439
Subscriptions []UserSubscription `json:"subscriptions,omitempty"`
3540
}

backend/internal/handler/setting_handler.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,12 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
8585

8686
AffiliateEnabled: settings.AffiliateEnabled,
8787

88-
RiskControlEnabled: settings.RiskControlEnabled,
89-
ServerTimezone: settings.ServerTimezone,
88+
RiskControlEnabled: settings.RiskControlEnabled,
89+
BillingStatementEmailEnabled: settings.BillingStatementEmailEnabled,
90+
BillingStatementDailyEnabled: settings.BillingStatementDailyEnabled,
91+
BillingStatementWeeklyEnabled: settings.BillingStatementWeeklyEnabled,
92+
BillingStatementMonthlyEnabled: settings.BillingStatementMonthlyEnabled,
93+
ServerTimezone: settings.ServerTimezone,
9094
})
9195
}
9296

backend/internal/handler/user_handler.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ type UpdateProfileRequest struct {
5050
AvatarURL *string `json:"avatar_url"`
5151
BalanceNotifyEnabled *bool `json:"balance_notify_enabled"`
5252
BalanceNotifyThreshold *float64 `json:"balance_notify_threshold"`
53-
Timezone *string `json:"timezone"`
53+
54+
// Billing statement email preferences
55+
BillingStatementDailyEnabled *bool `json:"billing_statement_daily_enabled"`
56+
BillingStatementWeeklyEnabled *bool `json:"billing_statement_weekly_enabled"`
57+
BillingStatementMonthlyEnabled *bool `json:"billing_statement_monthly_enabled"`
58+
Timezone *string `json:"timezone"`
5459
}
5560

5661
type userProfileResponse struct {
@@ -143,11 +148,14 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
143148
}
144149

145150
svcReq := service.UpdateProfileRequest{
146-
Username: req.Username,
147-
AvatarURL: req.AvatarURL,
148-
BalanceNotifyEnabled: req.BalanceNotifyEnabled,
149-
BalanceNotifyThreshold: req.BalanceNotifyThreshold,
150-
Timezone: req.Timezone,
151+
Username: req.Username,
152+
AvatarURL: req.AvatarURL,
153+
BalanceNotifyEnabled: req.BalanceNotifyEnabled,
154+
BalanceNotifyThreshold: req.BalanceNotifyThreshold,
155+
BillingStatementDailyEnabled: req.BillingStatementDailyEnabled,
156+
BillingStatementWeeklyEnabled: req.BillingStatementWeeklyEnabled,
157+
BillingStatementMonthlyEnabled: req.BillingStatementMonthlyEnabled,
158+
Timezone: req.Timezone,
151159
}
152160
updatedUser, err := h.userService.UpdateProfile(c.Request.Context(), subject.UserID, svcReq)
153161
if err != nil {

backend/internal/server/api_contract_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ func TestAPIContracts(t *testing.T) {
6666
"balance_notify_extra_emails": null,
6767
"total_recharged": 0,
6868
"timezone": "UTC",
69+
"billing_statement_daily_enabled": false,
70+
"billing_statement_weekly_enabled": false,
71+
"billing_statement_monthly_enabled": false,
6972
"linuxdo_bound": false,
7073
"oidc_bound": false,
7174
"wechat_bound": false,
@@ -819,6 +822,7 @@ func TestAPIContracts(t *testing.T) {
819822
"balance_low_notify_threshold": 0,
820823
"balance_low_notify_recharge_url": "",
821824
"account_quota_notify_emails": [],
825+
"billing_statement_email_config": "",
822826
"channel_monitor_enabled": true,
823827
"channel_monitor_default_interval_seconds": 60,
824828
"available_channels_enabled": false,
@@ -1030,6 +1034,7 @@ func TestAPIContracts(t *testing.T) {
10301034
"balance_low_notify_threshold": 0,
10311035
"balance_low_notify_recharge_url": "",
10321036
"account_quota_notify_emails": [],
1037+
"billing_statement_email_config": "",
10331038
"channel_monitor_enabled": true,
10341039
"channel_monitor_default_interval_seconds": 60,
10351040
"available_channels_enabled": false,

0 commit comments

Comments
 (0)