240 lines
10 KiB
Go
240 lines
10 KiB
Go
package business
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"math/big"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
"kra/internal/biz/business"
|
|
"kra/internal/biz/system"
|
|
)
|
|
|
|
func TestEffectivenessRepositoryWindowAndAtomicRulePublication(t *testing.T) {
|
|
db, _, _, _ := prepareLedgerData(t)
|
|
if err := db.AutoMigrate(&otherSettingsPO{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, sql := range []string{
|
|
"INSERT INTO tab_spend(id,source_db,user_id,promote_id,pay_amount,pay_time,pay_status) VALUES(1001,'lx',1,100,5,87399,1),(1002,'lx',1,100,999,87400,1),(1003,'lx',1,100,999,900,1)",
|
|
} {
|
|
if err := db.Exec(sql).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
repo := NewEffectivenessRepo(testData{db})
|
|
settings := NewOtherSettingsRepo(testData{db})
|
|
uc := business.NewEffectivenessUsecase(repo, settings)
|
|
ctx := system.NewActorContext(context.Background(), system.Actor{UserID: 1, AuthorityID: 888})
|
|
rule := business.OtherSettings{Amount: 10, Days: 1}
|
|
if _, err := uc.Reconfigure(ctx, rule); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var row effectivenessPO
|
|
if err := db.Where("rule_key=? AND source_db='lx' AND promote_id=100", business.EffectivenessRuleKey(rule)).First(&row).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if row.QualifiedAt == nil || *row.QualifiedAt != 1500 || portraitTestMoney(row.WindowPay) != "15.10" {
|
|
t.Fatalf("窗口须排除注册前、结束瞬间及失败订单: %+v", row)
|
|
}
|
|
var other effectivenessPO
|
|
if err := db.Where("rule_key=? AND source_db='hw' AND promote_id=100", business.EffectivenessRuleKey(rule)).First(&other).Error; err != nil || other.QualifiedAt == nil {
|
|
t.Fatal("同号渠道必须按平台分别判定")
|
|
}
|
|
if err := db.Exec("UPDATE tab_spend SET pay_status=0 WHERE source_db='lx' AND id=901").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := uc.Refresh(ctx, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Where("rule_key=? AND source_db='lx' AND promote_id=100", business.EffectivenessRuleKey(rule)).First(&row).Error; err != nil || row.QualifiedAt == nil || *row.QualifiedAt != 1500 {
|
|
t.Fatal("后续流水回落不能取消已达标标记")
|
|
}
|
|
next := business.OtherSettings{Amount: 20, Days: 1}
|
|
if _, err := uc.Reconfigure(ctx, next); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var newRow effectivenessPO
|
|
if err := db.Where("rule_key=? AND source_db='lx' AND promote_id=100", business.EffectivenessRuleKey(next)).First(&newRow).Error; err != nil || newRow.QualifiedAt != nil {
|
|
t.Fatal("更改规则须按新门槛重算,而不是继承旧规则结果")
|
|
}
|
|
state, err := repo.State(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Callback().Create().Before("gorm:create").Register("fail_effectiveness", func(tx *gorm.DB) {
|
|
if tx.Statement.Table == "sys_business_channel_effectiveness" {
|
|
tx.AddError(errors.New("injected failure"))
|
|
}
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Callback().Create().Remove("fail_effectiveness")
|
|
if _, err := uc.Reconfigure(ctx, business.OtherSettings{Amount: 100, Days: 1}); err == nil {
|
|
t.Fatal("注入的持久化失败必须上报")
|
|
}
|
|
stored, _ := settings.Find(ctx)
|
|
after, _ := repo.State(ctx)
|
|
if stored.Amount != 20 || after.Revision != state.Revision || after.RuleKey != state.RuleKey {
|
|
t.Fatal("失败不能发布规则或部分结果")
|
|
}
|
|
}
|
|
|
|
func TestLargeEffectivenessScanMatchesBatchedCharges(t *testing.T) {
|
|
db, _, _, _ := prepareLedgerData(t)
|
|
if err := db.Exec("WITH RECURSIVE seq(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n<3200) INSERT INTO tab_promote(id,source_db,account,promote_level,parent_id,top_promote_id,busier_id,create_time) SELECT 10000+n,'lx','bulk-'||n,1,0,0,14,1000 FROM seq").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := &effectivenessRepo{data: testData{db}}
|
|
ctx := context.Background()
|
|
candidates, err := r.Candidates(ctx, "test", 86400, 90000, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(candidates) <= 3000 {
|
|
t.Fatal("fixture must exercise streaming path")
|
|
}
|
|
var full, batched []business.EffectivenessCharge
|
|
if err := r.Charges(ctx, candidates, 86400, 90000, true, func(c business.EffectivenessCharge) error { full = append(full, c); return nil }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for start := 0; start < len(candidates); start += 300 {
|
|
if err := r.Charges(ctx, candidates[start:min(start+300, len(candidates))], 86400, 90000, false, func(c business.EffectivenessCharge) error { batched = append(batched, c); return nil }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(full, batched) {
|
|
t.Fatalf("流式优化不能改变时间顺序、平台身份或流水: full=%v batch=%v", full, batched)
|
|
}
|
|
}
|
|
|
|
func TestPerformanceOwnerAggregationAndDrilldownUseSameScope(t *testing.T) {
|
|
db, _, scope, _ := prepareAnalyticsData(t)
|
|
repo := NewPerformanceRepo(testData{db})
|
|
location := time.FixedZone("Asia/Shanghai", 8*3600)
|
|
begin := time.Date(2026, 11, 1, 0, 0, 0, 0, location)
|
|
end := begin.AddDate(0, 0, 2)
|
|
if err := db.Exec("UPDATE tab_promote SET create_time=? WHERE source_db='lx' AND id=100", begin.Unix()).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Model(&effectivenessStatePO{}).Where("id=1").Updates(map[string]any{"rule_key": "fixture", "built_at": time.Now()}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Create(&effectivenessPO{RuleKey: "fixture", SourceDB: "lx", PromoteID: 100, QualifiedAt: func() *int64 { n := begin.AddDate(0, 0, 1).Unix(); return &n }(), WindowPay: "3000"}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
facts, err := repo.Facts(context.Background(), scope.Viewer, nil, []string{"lx"}, begin, end)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(facts.Cash) != 2 {
|
|
t.Fatalf("同一SDK商务应按日聚合,避免按渠道返回所有明细: %+v", facts.Cash)
|
|
}
|
|
if len(facts.Channels) != 1 || facts.Channels[0].ID != 100 {
|
|
t.Fatal("historical channels with no in-period opening/qualification must not be materialized")
|
|
}
|
|
total := new(big.Rat)
|
|
for _, cash := range facts.Cash {
|
|
if len(cash.OwnerIDs) != 1 || cash.OwnerIDs[0] != 9 {
|
|
t.Fatal("聚合丢失商务归属")
|
|
}
|
|
n, ok := new(big.Rat).SetString(cash.Pay)
|
|
if !ok {
|
|
t.Fatal("invalid amount")
|
|
}
|
|
total.Add(total, n)
|
|
}
|
|
if total.FloatString(2) != "1059.60" {
|
|
t.Fatalf("汇总金额改变: %v", total)
|
|
}
|
|
rows, count, err := repo.Details(context.Background(), scope.Viewer, business.PerformanceDetailSelection{OwnerIDs: []uint{9}}, []string{"lx"}, begin, end, "effective", 0, 20)
|
|
if err != nil || count != 1 || rows[0].ID != 100 || portraitTestMoney(rows[0].PeriodPay) != "10.10" {
|
|
t.Fatalf("下钻应准确筛选并只补本页充值: %+v %d %v", rows, count, err)
|
|
}
|
|
_, count, err = repo.Details(context.Background(), business.UserScope{OnlySelf: true, SelfID: 99}, business.PerformanceDetailSelection{OwnerIDs: []uint{9}}, nil, begin, end, "effective", 0, 20)
|
|
if err != nil || count != 0 {
|
|
t.Fatal("不能通过指定其他商务ID越权下钻")
|
|
}
|
|
selection := business.PerformanceDetailSelection{OwnerIDs: []uint{8, 9}, ContributorIDs: []uint{8, 9}}
|
|
rows, count, err = repo.Details(context.Background(), scope.Viewer, selection, []string{"lx"}, begin, end, "effective", 0, 20)
|
|
if err != nil || count != 2 || len(rows) != 2 || rows[0].ContributorID != 8 || rows[1].ContributorID != 9 || rows[0].ID != rows[1].ID || portraitTestMoney(rows[0].PeriodPay) != "10.10" || rows[0].ContributorName == "" {
|
|
t.Fatalf("team drilldown must show both member contributions: %+v count=%d err=%v", rows, count, err)
|
|
}
|
|
rows, count, err = repo.Details(context.Background(), scope.Viewer, selection, []string{"lx"}, begin, end, "effective", 1, 1)
|
|
if err != nil || count != 2 || len(rows) != 1 || rows[0].ContributorID != 9 {
|
|
t.Fatal("contribution pagination must preserve total and stable row identity")
|
|
}
|
|
data, err := NewDashboardRepo(testData{db}).Overview(context.Background(), scope, []string{"lx"}, business.DashboardWindow{Begin: begin, End: end, PreviousBegin: begin.AddDate(0, 0, -3), PreviousEnd: begin.AddDate(0, 0, -1)})
|
|
if err != nil || data.Current.Effective != 1 || data.Current.ActiveChannels != 4 || portraitTestMoney(data.Current.Pay) != "1059.60" {
|
|
t.Fatalf("首页按区间统计与绩效口径不一致: %+v %v", data, err)
|
|
}
|
|
}
|
|
|
|
func TestTeamCatalogVersionedMembershipAndIndependentGoals(t *testing.T) {
|
|
db, _ := prepareChannels(t)
|
|
repo := NewTeamRepo(testData{db})
|
|
ctx := context.Background()
|
|
initial, err := repo.Catalog(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
view, err := repo.SaveCatalog(ctx, initial.Revision, []*business.BusinessGroup{{Name: "同名组", OwnerID: 7, MemberIDs: []uint{8}}, {Name: "同名组", OwnerID: 7, Sort: 1, MemberIDs: []uint{9}}})
|
|
if err != nil || len(view.Groups) != 2 || view.Groups[0].ID == view.Groups[1].ID {
|
|
t.Fatal("同名组应以独立ID区分")
|
|
}
|
|
if _, err := repo.SaveCatalog(ctx, initial.Revision, nil); err != business.ErrTeamConflict {
|
|
t.Fatal("过期版本不能删除成员")
|
|
}
|
|
goals := NewGoalRepo(testData{db})
|
|
target := int64(30)
|
|
if err := goals.Save(ctx, &business.PerformanceGoal{Kind: "person", TargetID: 9, Month: "2026-11", Open: &target}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := goals.Save(ctx, &business.PerformanceGoal{Kind: "group", TargetID: view.Groups[1].ID, Month: "2026-11", Open: &target}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
zero := int64(0)
|
|
if err := goals.Save(ctx, &business.PerformanceGoal{Kind: "person", TargetID: 9, Month: "2026-11", Open: &zero}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
values, err := goals.List(ctx, "2026-11", "2026-11")
|
|
if err != nil || len(values) != 2 {
|
|
t.Fatal("个人和小组目标必须各自保存")
|
|
}
|
|
for _, value := range values {
|
|
if value.Kind == "group" && *value.Open != 30 {
|
|
t.Fatal("修改个人目标不应改变团队目标")
|
|
}
|
|
if value.Kind == "person" && (*value.Open != 0 || value.Effective != nil) {
|
|
t.Fatal("零目标和留空必须区分")
|
|
}
|
|
}
|
|
if _, err := repo.SaveCatalog(ctx, view.Revision, []*business.BusinessGroup{{ID: view.Groups[0].ID, Name: "同名组", OwnerID: 7, MemberIDs: []uint{8, 9}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var members []businessGroupMemberPO
|
|
if err := db.Find(&members).Error; err != nil || len(members) != 2 {
|
|
t.Fatal("移动成员不能丢失或重复")
|
|
}
|
|
for _, member := range members {
|
|
if member.GroupID != view.Groups[0].ID {
|
|
t.Fatal("成员应移动到目标小组")
|
|
}
|
|
}
|
|
if err := repo.UpdateProfile(ctx, business.MemberProfile{ID: 9, RealName: "成员", Phone: "13900000009", HireDate: "2025-01-01", Employment: "departed"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var user accountPO
|
|
if err := db.First(&user, 9).Error; err != nil || user.Enable != 0 || user.BusinessStatus != 1 {
|
|
t.Fatal("离职应停用登录但保留账号")
|
|
}
|
|
var total int64
|
|
db.Table("tab_spend").Count(&total)
|
|
if total == 0 {
|
|
t.Fatal("离职不能删除历史业绩")
|
|
}
|
|
}
|