mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 06:52:13 +08:00
Add a dedicated modal in group management for viewing, adding, editing, and deleting per-user rate multipliers within a group. Backend: - GET /admin/groups/:id/rate-multipliers - list entries with user details - PUT /admin/groups/:id/rate-multipliers - batch sync (full replace) - DELETE /admin/groups/:id/rate-multipliers - clear all entries - Repository: GetByGroupID, SyncGroupRateMultipliers methods on user_group_rate_multipliers table (same table as user-side rates) Frontend: - New GroupRateMultipliersModal component with: - User search and add with email autocomplete - Editable rate column with local edit mode (cancel/save) - Batch adjust: multiply all rates by a factor - Clear all (local operation, requires save to persist) - Pagination (10/20/50 per page) - Platform icon with brand colors in group info bar - Unsaved changes indicator with revert option - Unit tests for all three backend endpoints
177 lines
5.5 KiB
Go
177 lines
5.5 KiB
Go
//go:build unit
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// userGroupRateRepoStubForGroupRate implements UserGroupRateRepository for group rate tests.
|
|
type userGroupRateRepoStubForGroupRate struct {
|
|
getByGroupIDData map[int64][]UserGroupRateEntry
|
|
getByGroupIDErr error
|
|
|
|
deletedGroupIDs []int64
|
|
deleteByGroupErr error
|
|
|
|
syncedGroupID int64
|
|
syncedEntries []GroupRateMultiplierInput
|
|
syncGroupErr error
|
|
}
|
|
|
|
func (s *userGroupRateRepoStubForGroupRate) GetByUserID(_ context.Context, _ int64) (map[int64]float64, error) {
|
|
panic("unexpected GetByUserID call")
|
|
}
|
|
|
|
func (s *userGroupRateRepoStubForGroupRate) GetByUserAndGroup(_ context.Context, _, _ int64) (*float64, error) {
|
|
panic("unexpected GetByUserAndGroup call")
|
|
}
|
|
|
|
func (s *userGroupRateRepoStubForGroupRate) GetByGroupID(_ context.Context, groupID int64) ([]UserGroupRateEntry, error) {
|
|
if s.getByGroupIDErr != nil {
|
|
return nil, s.getByGroupIDErr
|
|
}
|
|
return s.getByGroupIDData[groupID], nil
|
|
}
|
|
|
|
func (s *userGroupRateRepoStubForGroupRate) SyncUserGroupRates(_ context.Context, _ int64, _ map[int64]*float64) error {
|
|
panic("unexpected SyncUserGroupRates call")
|
|
}
|
|
|
|
func (s *userGroupRateRepoStubForGroupRate) SyncGroupRateMultipliers(_ context.Context, groupID int64, entries []GroupRateMultiplierInput) error {
|
|
s.syncedGroupID = groupID
|
|
s.syncedEntries = entries
|
|
return s.syncGroupErr
|
|
}
|
|
|
|
func (s *userGroupRateRepoStubForGroupRate) DeleteByGroupID(_ context.Context, groupID int64) error {
|
|
s.deletedGroupIDs = append(s.deletedGroupIDs, groupID)
|
|
return s.deleteByGroupErr
|
|
}
|
|
|
|
func (s *userGroupRateRepoStubForGroupRate) DeleteByUserID(_ context.Context, _ int64) error {
|
|
panic("unexpected DeleteByUserID call")
|
|
}
|
|
|
|
func TestAdminService_GetGroupRateMultipliers(t *testing.T) {
|
|
t.Run("returns entries for group", func(t *testing.T) {
|
|
repo := &userGroupRateRepoStubForGroupRate{
|
|
getByGroupIDData: map[int64][]UserGroupRateEntry{
|
|
10: {
|
|
{UserID: 1, UserName: "alice", UserEmail: "alice@test.com", RateMultiplier: 1.5},
|
|
{UserID: 2, UserName: "bob", UserEmail: "bob@test.com", RateMultiplier: 0.8},
|
|
},
|
|
},
|
|
}
|
|
svc := &adminServiceImpl{userGroupRateRepo: repo}
|
|
|
|
entries, err := svc.GetGroupRateMultipliers(context.Background(), 10)
|
|
require.NoError(t, err)
|
|
require.Len(t, entries, 2)
|
|
require.Equal(t, int64(1), entries[0].UserID)
|
|
require.Equal(t, "alice", entries[0].UserName)
|
|
require.Equal(t, 1.5, entries[0].RateMultiplier)
|
|
require.Equal(t, int64(2), entries[1].UserID)
|
|
require.Equal(t, 0.8, entries[1].RateMultiplier)
|
|
})
|
|
|
|
t.Run("returns nil when repo is nil", func(t *testing.T) {
|
|
svc := &adminServiceImpl{userGroupRateRepo: nil}
|
|
|
|
entries, err := svc.GetGroupRateMultipliers(context.Background(), 10)
|
|
require.NoError(t, err)
|
|
require.Nil(t, entries)
|
|
})
|
|
|
|
t.Run("returns empty slice for group with no entries", func(t *testing.T) {
|
|
repo := &userGroupRateRepoStubForGroupRate{
|
|
getByGroupIDData: map[int64][]UserGroupRateEntry{},
|
|
}
|
|
svc := &adminServiceImpl{userGroupRateRepo: repo}
|
|
|
|
entries, err := svc.GetGroupRateMultipliers(context.Background(), 99)
|
|
require.NoError(t, err)
|
|
require.Nil(t, entries)
|
|
})
|
|
|
|
t.Run("propagates repo error", func(t *testing.T) {
|
|
repo := &userGroupRateRepoStubForGroupRate{
|
|
getByGroupIDErr: errors.New("db error"),
|
|
}
|
|
svc := &adminServiceImpl{userGroupRateRepo: repo}
|
|
|
|
_, err := svc.GetGroupRateMultipliers(context.Background(), 10)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "db error")
|
|
})
|
|
}
|
|
|
|
func TestAdminService_ClearGroupRateMultipliers(t *testing.T) {
|
|
t.Run("deletes by group ID", func(t *testing.T) {
|
|
repo := &userGroupRateRepoStubForGroupRate{}
|
|
svc := &adminServiceImpl{userGroupRateRepo: repo}
|
|
|
|
err := svc.ClearGroupRateMultipliers(context.Background(), 42)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []int64{42}, repo.deletedGroupIDs)
|
|
})
|
|
|
|
t.Run("returns nil when repo is nil", func(t *testing.T) {
|
|
svc := &adminServiceImpl{userGroupRateRepo: nil}
|
|
|
|
err := svc.ClearGroupRateMultipliers(context.Background(), 42)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("propagates repo error", func(t *testing.T) {
|
|
repo := &userGroupRateRepoStubForGroupRate{
|
|
deleteByGroupErr: errors.New("delete failed"),
|
|
}
|
|
svc := &adminServiceImpl{userGroupRateRepo: repo}
|
|
|
|
err := svc.ClearGroupRateMultipliers(context.Background(), 42)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "delete failed")
|
|
})
|
|
}
|
|
|
|
func TestAdminService_BatchSetGroupRateMultipliers(t *testing.T) {
|
|
t.Run("syncs entries to repo", func(t *testing.T) {
|
|
repo := &userGroupRateRepoStubForGroupRate{}
|
|
svc := &adminServiceImpl{userGroupRateRepo: repo}
|
|
|
|
entries := []GroupRateMultiplierInput{
|
|
{UserID: 1, RateMultiplier: 1.5},
|
|
{UserID: 2, RateMultiplier: 0.8},
|
|
}
|
|
err := svc.BatchSetGroupRateMultipliers(context.Background(), 10, entries)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(10), repo.syncedGroupID)
|
|
require.Equal(t, entries, repo.syncedEntries)
|
|
})
|
|
|
|
t.Run("returns nil when repo is nil", func(t *testing.T) {
|
|
svc := &adminServiceImpl{userGroupRateRepo: nil}
|
|
|
|
err := svc.BatchSetGroupRateMultipliers(context.Background(), 10, nil)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("propagates repo error", func(t *testing.T) {
|
|
repo := &userGroupRateRepoStubForGroupRate{
|
|
syncGroupErr: errors.New("sync failed"),
|
|
}
|
|
svc := &adminServiceImpl{userGroupRateRepo: repo}
|
|
|
|
err := svc.BatchSetGroupRateMultipliers(context.Background(), 10, []GroupRateMultiplierInput{
|
|
{UserID: 1, RateMultiplier: 1.0},
|
|
})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "sync failed")
|
|
})
|
|
}
|