package domain import ( "errors" "testing" ) func TestAppendLedger(t *testing.T) { t.Run("自动续号", func(t *testing.T) { out, err := AppendLedger([]LedgerEntry{{ID: 3, Points: 1}}, LedgerEntry{Points: 2}, LedgerEntry{Points: 3}) if err != nil { t.Fatalf("追加失败: %v", err) } if len(out) != 3 || out[1].ID != 4 || out[2].ID != 5 { t.Fatalf("id 续号错误: %+v", out) } }) t.Run("保留已有行", func(t *testing.T) { base := []LedgerEntry{{ID: 1, Points: 5}} out, _ := AppendLedger(base, LedgerEntry{Points: 2}) if base[0].Points != 5 || len(out) != 2 { t.Fatalf("原流水被改动: base=%+v out=%+v", base, out) } }) t.Run("显式 id 冲突报错", func(t *testing.T) { if _, err := AppendLedger([]LedgerEntry{{ID: 1}}, LedgerEntry{ID: 1}); !errors.Is(err, ErrDuplicateLedgerID) { t.Fatalf("错误 = %v,期望 ErrDuplicateLedgerID", err) } }) } func TestVoidLedger(t *testing.T) { base := []LedgerEntry{{ID: 1, Points: 5}, {ID: 2, Points: 3}} t.Run("作废指定流水", func(t *testing.T) { out, err := VoidLedger(base, 1, "admin", "录入错误") if err != nil { t.Fatalf("作废失败: %v", err) } if !out[0].Voided || out[0].VoidedBy != "admin" || out[0].VoidReason != "录入错误" { t.Fatalf("作废信息不全: %+v", out[0]) } if out[1].Voided { t.Fatalf("误作废了其他流水: %+v", out[1]) } if base[0].Voided { t.Fatal("作废不应原地修改入参") } }) t.Run("重复作废报错", func(t *testing.T) { out, _ := VoidLedger(base, 1, "admin", "x") if _, err := VoidLedger(out, 1, "admin", "y"); !errors.Is(err, ErrLedgerAlreadyVoided) { t.Fatalf("错误 = %v,期望 ErrLedgerAlreadyVoided", err) } }) t.Run("流水不存在报错", func(t *testing.T) { if _, err := VoidLedger(base, 404, "admin", "x"); !errors.Is(err, ErrLedgerNotFound) { t.Fatalf("错误 = %v,期望 ErrLedgerNotFound", err) } }) } func TestVoidBySource(t *testing.T) { base := []LedgerEntry{ {ID: 1, SourceType: SourceResult, SourceID: 100, Points: 10}, {ID: 2, SourceType: SourceResult, SourceID: 100, Points: 8}, {ID: 3, SourceType: SourceResult, SourceID: 200, Points: 6}, } out, count := VoidBySource(base, SourceResult, 100, "admin", "撤销") if count != 2 { t.Fatalf("作废条数 = %d,期望 2", count) } if !out[0].Voided || !out[1].Voided || out[2].Voided { t.Fatalf("作废范围错误: %+v", out) } t.Run("重复作废是空操作", func(t *testing.T) { again, count := VoidBySource(out, SourceResult, 100, "admin", "再撤销") if count != 0 { t.Fatalf("重复作废条数 = %d,期望 0", count) } if len(again) != len(out) { t.Fatalf("流水条数变了: %d", len(again)) } }) } func TestSumByTeam(t *testing.T) { ledger := []LedgerEntry{ ledgerFor(1, 10), ledgerForAthlete(1, 101, 10), ledgerFor(2, 6), } got := SumByTeam(ledger, 1) if !nearlyEqual(got[1].Total, 20) || !nearlyEqual(got[1].TeamOnly, 10) || !nearlyEqual(got[1].Individual, 10) { t.Fatalf("单位1汇总 = %+v,期望 总分20 / 单位10 / 个人10", got[1]) } if !nearlyEqual(got[2].Total, 6) { t.Fatalf("单位2汇总 = %+v,期望 6", got[2]) } half := SumByTeam(ledger, 0.5) if !nearlyEqual(half[1].Total, 15) { t.Fatalf("系数 0.5 时单位1总分 = %v,期望 15", half[1].Total) } } func TestSumByAthlete(t *testing.T) { ledger := []LedgerEntry{ ledgerForAthlete(1, 101, 10), ledgerForAthlete(1, 101, 2), ledgerFor(1, 100), // 仅记单位,不进个人榜 } got := SumByAthlete(ledger) if len(got) != 1 || !nearlyEqual(got[101], 12) { t.Fatalf("个人汇总 = %+v,期望只有 101:12", got) } } func TestRecomputeEventLedger(t *testing.T) { old := []LedgerEntry{ {ID: 1, EventID: 1, SourceType: SourceResult, SourceID: 100, TeamID: 1, Points: 10}, {ID: 2, EventID: 2, SourceType: SourceResult, SourceID: 200, TeamID: 1, Points: 8}, } fresh := []LedgerEntry{{EventID: 1, SourceType: SourceResult, SourceID: 100, TeamID: 1, Points: 6}} t.Run("有变化时旧流水作废并追加新流水", func(t *testing.T) { out, err := RecomputeEventLedger(old, 1, fresh, "judge", "更正") if err != nil { t.Fatalf("重算失败: %v", err) } if len(out) != 3 { t.Fatalf("流水条数 = %d,期望 3(旧2 + 新1)", len(out)) } if !out[0].Voided || out[1].Voided { t.Fatalf("作废范围错误: %+v", out) } if !nearlyEqual(SumByTeam(out, 1)[1].Total, 14) { t.Fatalf("重算后总分 = %v,期望 6 + 8 = 14", SumByTeam(out, 1)[1].Total) } }) t.Run("无变化时原样返回", func(t *testing.T) { same := []LedgerEntry{{EventID: 1, SourceType: SourceResult, SourceID: 100, TeamID: 1, Points: 10}} out, err := RecomputeEventLedger(old, 1, same, "judge", "重复发布") if err != nil { t.Fatalf("重算失败: %v", err) } if len(out) != len(old) { t.Fatalf("流水条数 = %d,期望 %d(幂等)", len(out), len(old)) } }) t.Run("只影响目标小项", func(t *testing.T) { out, _ := RecomputeEventLedger(old, 1, fresh, "judge", "更正") if out[1].Voided { t.Fatalf("其他小项的流水被误作废: %+v", out[1]) } }) } func TestApplyLedgerOps(t *testing.T) { a := []LedgerEntry{{EventID: 1, SourceType: SourceResult, SourceID: 100, TeamID: 1, Points: 10}} b := []LedgerEntry{{EventID: 2, SourceType: SourceResult, SourceID: 200, TeamID: 2, Points: 8}} t.Run("追加与作废可乱序收敛", func(t *testing.T) { ops := []LedgerOp{ {Kind: LedgerOpAppend, Entries: b}, {Kind: LedgerOpAppend, Entries: a}, {Kind: LedgerOpVoidSource, SourceType: SourceResult, SourceID: 100, By: "admin", Reason: "撤销"}, } out, err := ApplyLedgerOps(nil, ops) if err != nil { t.Fatalf("应用失败: %v", err) } got := SumByTeam(out, 1) if !nearlyEqual(got[1].Total, 0) || !nearlyEqual(got[2].Total, 8) { t.Fatalf("最终状态 = %+v,期望 1:0 / 2:8", got) } }) t.Run("未知操作报错", func(t *testing.T) { if _, err := ApplyLedgerOps(nil, []LedgerOp{{Kind: "magic"}}); !errors.Is(err, ErrUnknownLedgerOp) { t.Fatalf("错误 = %v,期望 ErrUnknownLedgerOp", err) } }) } func TestAdjustmentsToLedger(t *testing.T) { adj := PointAdjustment{ID: 1, TeamID: 5, Delta: -3, Category: AdjustmentPenalty, Reason: "犯规"} t.Run("未启用时报错", func(t *testing.T) { if _, err := AdjustmentsToLedger(ManualAdjustmentConfig{}, []PointAdjustment{adj}); !errors.Is(err, ErrManualAdjustmentDisabled) { t.Fatalf("错误 = %v,期望 ErrManualAdjustmentDisabled", err) } }) t.Run("启用后转成流水", func(t *testing.T) { out, err := AdjustmentsToLedger(ManualAdjustmentConfig{Enabled: true, ReasonRequired: true}, []PointAdjustment{adj}) if err != nil { t.Fatalf("转换失败: %v", err) } if len(out) != 1 || out[0].SourceType != SourceAdjustment || !nearlyEqual(out[0].Points, -3) { t.Fatalf("流水 = %+v,期望一条 -3 的 adjustment", out) } }) t.Run("理由必填", func(t *testing.T) { bad := adj bad.Reason = " " if _, err := AdjustmentsToLedger(ManualAdjustmentConfig{Enabled: true, ReasonRequired: true}, []PointAdjustment{bad}); !errors.Is(err, ErrAdjustmentReasonRequired) { t.Fatalf("错误 = %v,期望 ErrAdjustmentReasonRequired", err) } }) t.Run("已作废的调整被跳过", func(t *testing.T) { voided := adj voided.Voided = true out, err := AdjustmentsToLedger(ManualAdjustmentConfig{Enabled: true}, []PointAdjustment{voided}) if err != nil || len(out) != 0 { t.Fatalf("流水 = %+v err=%v,期望空", out, err) } }) } func TestActiveLedger(t *testing.T) { ledger := []LedgerEntry{{ID: 1, Points: 1}, {ID: 2, Points: 2, Voided: true}} got := ActiveLedger(ledger) if len(got) != 1 || got[0].ID != 1 { t.Fatalf("未作废流水 = %+v,期望只有 id=1", got) } }