138 lines
5.6 KiB
Go
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()
|
|
}
|
|
}
|