feat(notify): add balance low & account quota notification system
- User balance low notification: email alert when balance drops below configurable threshold (user email + verified extra emails) - Account quota notification: broadcast email to admin-configured recipients when daily/weekly/total quota usage exceeds alert threshold - Admin settings: global enable/disable, default threshold, quota notification email list (Email Settings tab) - User profile: enable/disable, custom threshold, add/remove extra notification emails with verification code flow - Account quota: per-dimension alert toggle and threshold in quota control card - Trigger logic: first-crossing only (old >= threshold && new < threshold for balance; old < threshold && new >= threshold for quota), naturally prevents duplicate notifications without Redis dedup
This commit is contained in:
@@ -28210,6 +28210,10 @@ type UserMutation struct {
|
||||
totp_secret_encrypted *string
|
||||
totp_enabled *bool
|
||||
totp_enabled_at *time.Time
|
||||
balance_notify_enabled *bool
|
||||
balance_notify_threshold *float64
|
||||
addbalance_notify_threshold *float64
|
||||
balance_notify_extra_emails *string
|
||||
clearedFields map[string]struct{}
|
||||
api_keys map[int64]struct{}
|
||||
removedapi_keys map[int64]struct{}
|
||||
@@ -28927,6 +28931,148 @@ func (m *UserMutation) ResetTotpEnabledAt() {
|
||||
delete(m.clearedFields, user.FieldTotpEnabledAt)
|
||||
}
|
||||
|
||||
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
|
||||
func (m *UserMutation) SetBalanceNotifyEnabled(b bool) {
|
||||
m.balance_notify_enabled = &b
|
||||
}
|
||||
|
||||
// BalanceNotifyEnabled returns the value of the "balance_notify_enabled" field in the mutation.
|
||||
func (m *UserMutation) BalanceNotifyEnabled() (r bool, exists bool) {
|
||||
v := m.balance_notify_enabled
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldBalanceNotifyEnabled returns the old "balance_notify_enabled" field's value of the User entity.
|
||||
// If the User object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *UserMutation) OldBalanceNotifyEnabled(ctx context.Context) (v bool, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldBalanceNotifyEnabled is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldBalanceNotifyEnabled requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldBalanceNotifyEnabled: %w", err)
|
||||
}
|
||||
return oldValue.BalanceNotifyEnabled, nil
|
||||
}
|
||||
|
||||
// ResetBalanceNotifyEnabled resets all changes to the "balance_notify_enabled" field.
|
||||
func (m *UserMutation) ResetBalanceNotifyEnabled() {
|
||||
m.balance_notify_enabled = nil
|
||||
}
|
||||
|
||||
// SetBalanceNotifyThreshold sets the "balance_notify_threshold" field.
|
||||
func (m *UserMutation) SetBalanceNotifyThreshold(f float64) {
|
||||
m.balance_notify_threshold = &f
|
||||
m.addbalance_notify_threshold = nil
|
||||
}
|
||||
|
||||
// BalanceNotifyThreshold returns the value of the "balance_notify_threshold" field in the mutation.
|
||||
func (m *UserMutation) BalanceNotifyThreshold() (r float64, exists bool) {
|
||||
v := m.balance_notify_threshold
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldBalanceNotifyThreshold returns the old "balance_notify_threshold" field's value of the User entity.
|
||||
// If the User object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *UserMutation) OldBalanceNotifyThreshold(ctx context.Context) (v *float64, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldBalanceNotifyThreshold is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldBalanceNotifyThreshold requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldBalanceNotifyThreshold: %w", err)
|
||||
}
|
||||
return oldValue.BalanceNotifyThreshold, nil
|
||||
}
|
||||
|
||||
// AddBalanceNotifyThreshold adds f to the "balance_notify_threshold" field.
|
||||
func (m *UserMutation) AddBalanceNotifyThreshold(f float64) {
|
||||
if m.addbalance_notify_threshold != nil {
|
||||
*m.addbalance_notify_threshold += f
|
||||
} else {
|
||||
m.addbalance_notify_threshold = &f
|
||||
}
|
||||
}
|
||||
|
||||
// AddedBalanceNotifyThreshold returns the value that was added to the "balance_notify_threshold" field in this mutation.
|
||||
func (m *UserMutation) AddedBalanceNotifyThreshold() (r float64, exists bool) {
|
||||
v := m.addbalance_notify_threshold
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// ClearBalanceNotifyThreshold clears the value of the "balance_notify_threshold" field.
|
||||
func (m *UserMutation) ClearBalanceNotifyThreshold() {
|
||||
m.balance_notify_threshold = nil
|
||||
m.addbalance_notify_threshold = nil
|
||||
m.clearedFields[user.FieldBalanceNotifyThreshold] = struct{}{}
|
||||
}
|
||||
|
||||
// BalanceNotifyThresholdCleared returns if the "balance_notify_threshold" field was cleared in this mutation.
|
||||
func (m *UserMutation) BalanceNotifyThresholdCleared() bool {
|
||||
_, ok := m.clearedFields[user.FieldBalanceNotifyThreshold]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetBalanceNotifyThreshold resets all changes to the "balance_notify_threshold" field.
|
||||
func (m *UserMutation) ResetBalanceNotifyThreshold() {
|
||||
m.balance_notify_threshold = nil
|
||||
m.addbalance_notify_threshold = nil
|
||||
delete(m.clearedFields, user.FieldBalanceNotifyThreshold)
|
||||
}
|
||||
|
||||
// SetBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field.
|
||||
func (m *UserMutation) SetBalanceNotifyExtraEmails(s string) {
|
||||
m.balance_notify_extra_emails = &s
|
||||
}
|
||||
|
||||
// BalanceNotifyExtraEmails returns the value of the "balance_notify_extra_emails" field in the mutation.
|
||||
func (m *UserMutation) BalanceNotifyExtraEmails() (r string, exists bool) {
|
||||
v := m.balance_notify_extra_emails
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldBalanceNotifyExtraEmails returns the old "balance_notify_extra_emails" field's value of the User entity.
|
||||
// If the User object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *UserMutation) OldBalanceNotifyExtraEmails(ctx context.Context) (v string, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldBalanceNotifyExtraEmails is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldBalanceNotifyExtraEmails requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldBalanceNotifyExtraEmails: %w", err)
|
||||
}
|
||||
return oldValue.BalanceNotifyExtraEmails, nil
|
||||
}
|
||||
|
||||
// ResetBalanceNotifyExtraEmails resets all changes to the "balance_notify_extra_emails" field.
|
||||
func (m *UserMutation) ResetBalanceNotifyExtraEmails() {
|
||||
m.balance_notify_extra_emails = nil
|
||||
}
|
||||
|
||||
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by ids.
|
||||
func (m *UserMutation) AddAPIKeyIDs(ids ...int64) {
|
||||
if m.api_keys == nil {
|
||||
@@ -29501,7 +29647,7 @@ func (m *UserMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *UserMutation) Fields() []string {
|
||||
fields := make([]string, 0, 14)
|
||||
fields := make([]string, 0, 17)
|
||||
if m.created_at != nil {
|
||||
fields = append(fields, user.FieldCreatedAt)
|
||||
}
|
||||
@@ -29544,6 +29690,15 @@ func (m *UserMutation) Fields() []string {
|
||||
if m.totp_enabled_at != nil {
|
||||
fields = append(fields, user.FieldTotpEnabledAt)
|
||||
}
|
||||
if m.balance_notify_enabled != nil {
|
||||
fields = append(fields, user.FieldBalanceNotifyEnabled)
|
||||
}
|
||||
if m.balance_notify_threshold != nil {
|
||||
fields = append(fields, user.FieldBalanceNotifyThreshold)
|
||||
}
|
||||
if m.balance_notify_extra_emails != nil {
|
||||
fields = append(fields, user.FieldBalanceNotifyExtraEmails)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -29580,6 +29735,12 @@ func (m *UserMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.TotpEnabled()
|
||||
case user.FieldTotpEnabledAt:
|
||||
return m.TotpEnabledAt()
|
||||
case user.FieldBalanceNotifyEnabled:
|
||||
return m.BalanceNotifyEnabled()
|
||||
case user.FieldBalanceNotifyThreshold:
|
||||
return m.BalanceNotifyThreshold()
|
||||
case user.FieldBalanceNotifyExtraEmails:
|
||||
return m.BalanceNotifyExtraEmails()
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -29617,6 +29778,12 @@ func (m *UserMutation) OldField(ctx context.Context, name string) (ent.Value, er
|
||||
return m.OldTotpEnabled(ctx)
|
||||
case user.FieldTotpEnabledAt:
|
||||
return m.OldTotpEnabledAt(ctx)
|
||||
case user.FieldBalanceNotifyEnabled:
|
||||
return m.OldBalanceNotifyEnabled(ctx)
|
||||
case user.FieldBalanceNotifyThreshold:
|
||||
return m.OldBalanceNotifyThreshold(ctx)
|
||||
case user.FieldBalanceNotifyExtraEmails:
|
||||
return m.OldBalanceNotifyExtraEmails(ctx)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown User field %s", name)
|
||||
}
|
||||
@@ -29724,6 +29891,27 @@ func (m *UserMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetTotpEnabledAt(v)
|
||||
return nil
|
||||
case user.FieldBalanceNotifyEnabled:
|
||||
v, ok := value.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetBalanceNotifyEnabled(v)
|
||||
return nil
|
||||
case user.FieldBalanceNotifyThreshold:
|
||||
v, ok := value.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetBalanceNotifyThreshold(v)
|
||||
return nil
|
||||
case user.FieldBalanceNotifyExtraEmails:
|
||||
v, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetBalanceNotifyExtraEmails(v)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown User field %s", name)
|
||||
}
|
||||
@@ -29738,6 +29926,9 @@ func (m *UserMutation) AddedFields() []string {
|
||||
if m.addconcurrency != nil {
|
||||
fields = append(fields, user.FieldConcurrency)
|
||||
}
|
||||
if m.addbalance_notify_threshold != nil {
|
||||
fields = append(fields, user.FieldBalanceNotifyThreshold)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -29750,6 +29941,8 @@ func (m *UserMutation) AddedField(name string) (ent.Value, bool) {
|
||||
return m.AddedBalance()
|
||||
case user.FieldConcurrency:
|
||||
return m.AddedConcurrency()
|
||||
case user.FieldBalanceNotifyThreshold:
|
||||
return m.AddedBalanceNotifyThreshold()
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -29773,6 +29966,13 @@ func (m *UserMutation) AddField(name string, value ent.Value) error {
|
||||
}
|
||||
m.AddConcurrency(v)
|
||||
return nil
|
||||
case user.FieldBalanceNotifyThreshold:
|
||||
v, ok := value.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.AddBalanceNotifyThreshold(v)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown User numeric field %s", name)
|
||||
}
|
||||
@@ -29790,6 +29990,9 @@ func (m *UserMutation) ClearedFields() []string {
|
||||
if m.FieldCleared(user.FieldTotpEnabledAt) {
|
||||
fields = append(fields, user.FieldTotpEnabledAt)
|
||||
}
|
||||
if m.FieldCleared(user.FieldBalanceNotifyThreshold) {
|
||||
fields = append(fields, user.FieldBalanceNotifyThreshold)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -29813,6 +30016,9 @@ func (m *UserMutation) ClearField(name string) error {
|
||||
case user.FieldTotpEnabledAt:
|
||||
m.ClearTotpEnabledAt()
|
||||
return nil
|
||||
case user.FieldBalanceNotifyThreshold:
|
||||
m.ClearBalanceNotifyThreshold()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown User nullable field %s", name)
|
||||
}
|
||||
@@ -29863,6 +30069,15 @@ func (m *UserMutation) ResetField(name string) error {
|
||||
case user.FieldTotpEnabledAt:
|
||||
m.ResetTotpEnabledAt()
|
||||
return nil
|
||||
case user.FieldBalanceNotifyEnabled:
|
||||
m.ResetBalanceNotifyEnabled()
|
||||
return nil
|
||||
case user.FieldBalanceNotifyThreshold:
|
||||
m.ResetBalanceNotifyThreshold()
|
||||
return nil
|
||||
case user.FieldBalanceNotifyExtraEmails:
|
||||
m.ResetBalanceNotifyExtraEmails()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown User field %s", name)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user