Files
kra-new/internal/service/business/performance_test.go
T

138 lines
5.6 KiB
Go

package business
import (
"bytes"
"context"
"testing"
"time"
"github.com/xuri/excelize/v2"
"kra/internal/biz/business"
"kra/internal/service/dto"
)
type performanceServiceFake struct {
data *business.PerformancePage
calls int
filter business.PerformanceFilter
}
func (f *performanceServiceFake) Query(_ context.Context, filter business.PerformanceFilter) (*business.PerformancePage, error) {
f.calls++
f.filter = filter
return f.data, nil
}
func (f *performanceServiceFake) Details(context.Context, string, uint64, time.Time, time.Time, string, int, int, ...uint) ([]*business.PerformanceChannel, int64, error) {
f.calls++
return nil, 0, nil
}
type goalServiceFake struct {
goal *business.PerformanceGoal
calls int
}
type memberProfileFake struct {
TeamUsecase
profile *business.MemberProfile
}
func (f *memberProfileFake) UpdateProfile(_ context.Context, value business.MemberProfile) error {
f.profile = &value
return nil
}
func TestMemberProfileValidatesPhoneBeforeDelegating(t *testing.T) {
f := &memberProfileFake{}
s := &BusinessPerformanceService{teams: f}
req := &dto.MemberProfileRequest{UserID: 9, RealName: "成员", Phone: "abc", HireDate: "2026-11-01", Employment: "employed"}
if err := s.UpdateProfile(context.Background(), req); err != business.ErrMemberInput || f.profile != nil {
t.Fatal("invalid phone reached usecase")
}
for _, phone := range []string{"", "138 0013 8000", "010-12345678"} {
req.Phone = phone
if err := s.UpdateProfile(context.Background(), req); err != nil || f.profile.Phone != phone {
t.Fatalf("valid phone rejected: %q %v", phone, err)
}
}
}
func (f *goalServiceFake) Save(_ context.Context, g *business.PerformanceGoal) error {
f.calls++
f.goal = g
return nil
}
func (f *goalServiceFake) History(context.Context, string, uint64) (*business.GoalPage, error) {
return &business.GoalPage{}, nil
}
func (f *goalServiceFake) Mine(context.Context) (*business.GoalMonth, error) { return nil, nil }
func TestPerformanceServiceValidatesDatesAndPreservesNullableGoals(t *testing.T) {
f := &performanceServiceFake{data: &business.PerformancePage{Available: true}}
g := &goalServiceFake{}
s := &BusinessPerformanceService{performance: f, goals: g}
one, bad := 1, 13
for _, req := range []*dto.PerformanceQuery{nil, {BeginMonth: &one}, {BeginMonth: &one, EndMonth: &bad}, {Level: &bad}} {
if _, err := s.Query(context.Background(), req, false); err != business.ErrPerformanceInput {
t.Fatalf("无效查询应在进入用例前失败: %v", err)
}
}
if f.calls != 0 {
t.Fatal("无效查询访问了用例")
}
zero := int64(0)
negative := int64(-1)
if err := s.SaveGoal(context.Background(), &dto.GoalRequest{Kind: "person", TargetID: 9, Month: "2026-11", Open: &negative}); err != business.ErrGoalInput || g.calls != 0 {
t.Fatal("负数目标不应进入用例")
}
if err := s.SaveGoal(context.Background(), &dto.GoalRequest{Kind: "person", TargetID: 9, Month: "2026-11", Open: &zero}); err != nil || g.goal.Open == nil || *g.goal.Open != 0 || g.goal.Amount != nil {
t.Fatal("保存时应保留零和留空的差异")
}
}
func TestPerformanceExportsMatchViewsAndKeepPreciseAmounts(t *testing.T) {
open, amount := int64(10), int64(3000)
periods := []business.PerformancePeriod{{Key: "2026-10", Label: "10月"}, {Key: "2026-11", Label: "11月"}}
values := map[string]business.PerformanceMetrics{"2026-10": {Open: 1, Effective: 0, Pay: "10.00"}, "2026-11": {Open: 12, Effective: 2, Pay: "9007199254740993.01"}}
member := &business.PerformanceRow{Member: &business.BusinessMember{Account: business.Account{ID: 9, Username: "=1+1"}, GroupID: 1, GroupName: "甲组"}, Total: values["2026-11"], Periods: values}
group := &business.PerformanceGroupRow{Group: &business.BusinessGroup{ID: 1, Name: "甲组", MemberIDs: []uint{9}}, Total: values["2026-11"], UniqueTotal: values["2026-11"], Periods: values, UniquePeriods: values, Members: []*business.PerformanceRow{member}, Goals: map[string]*business.PerformanceGoal{"2026-11": {Open: &open, Amount: &amount}}}
data := &business.PerformancePage{Available: true, Periods: periods, Rows: []*business.PerformanceRow{member}, Groups: []*business.PerformanceGroupRow{group}, Trend: values, Total: values["2026-11"], MatrixTotal: values["2026-11"], MatrixTrend: values, Granularity: "month", Prior: business.PerformanceMetrics{Pay: "0.00"}}
s := &BusinessPerformanceService{performance: &performanceServiceFake{data: data}}
for _, sample := range []struct {
view, sheet string
rows int
}{{"groups", "小组绩效", 2}, {"trend", "趋势图", 3}, {"business", "商务绩效统计", 4}} {
blob, err := s.Export(context.Background(), &dto.PerformanceQuery{View: sample.view})
if err != nil {
t.Fatal(err)
}
file, err := excelize.OpenReader(bytes.NewReader(blob))
if err != nil {
t.Fatal(err)
}
rows, err := file.GetRows(sample.sheet)
if err != nil || len(rows) != sample.rows {
t.Fatalf("视图导出行数不符或包含了展开成员: %s rows=%d %v", sample.view, len(rows), err)
}
if sample.view == "groups" {
value, _ := file.GetCellValue(sample.sheet, "N2", excelize.Options{RawCellValue: true})
if value != "9007199254740993.01" {
t.Fatalf("金额不能经过浮点转换: %s", value)
}
if rows[1][14] != "—" || rows[1][15] != "—" {
t.Fatal("未设目标月份必须目标/实际分格占位")
}
}
if sample.view == "business" {
formula, _ := file.GetCellFormula(sample.sheet, "B2")
if formula != "" || rows[1][1] != "=1+1" {
t.Fatal("用户名称必须写成文本")
}
}
if sample.view == "trend" && rows[1][0] != "2026-11" {
t.Fatal("趋势导出应与页面倒序一致")
}
file.Close()
}
}