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("离职不能删除历史业绩") } }