Files
backend-go/services/aftersales_service_test.go
T
Frudrax Cheng 128bb7cda6 Add aftersales stats to dashboard and service-layer tests
- CompanyStatsOverviewDTO and GetStats() now include aftersales counts
  (total, pending confirmation, closed, rejected) and a recentAftersales list
- aftersales_service_test.go covers YYMMNN sequence, owner-only submit,
  state machine, phone last-4 check, reject increment, force-close
- users_service_test.go covers duplicate username, self-demotion guard,
  last-admin guard, password reset, assignable filter

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:04:23 +08:00

279 lines
9.8 KiB
Go

package services
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"git.beifan.cn/trace-system/backend-go/database"
"git.beifan.cn/trace-system/backend-go/models"
)
func seedTechnician(t *testing.T, username string) models.User {
t.Helper()
user := models.User{
Username: username,
Password: "hashed",
Name: "技术员-" + username,
Email: username + "@example.com",
Role: "technician",
}
if err := database.DB.Create(&user).Error; err != nil {
t.Fatalf("seed technician failed: %v", err)
}
return user
}
func createOrderFor(t *testing.T, user models.User, phone string) *models.AftersalesOrder {
t.Helper()
svc := AftersalesService{}
order, err := svc.Create(models.CreateAftersalesOrderDTO{
CompanyName: "TestCo_" + user.Username,
CompanyAddress: "测试地址",
ContactName: "张三",
ContactPhone: phone,
ServiceType: "software",
IssueDescription: "系统无法启动",
}, user.ID)
if err != nil {
t.Fatalf("create order failed: %v", err)
}
return order
}
func TestAftersalesService_Create_GeneratesYYMMNNSerial(t *testing.T) {
user := seedTechnician(t, "aftersales_create_tech")
defer database.DB.Unscoped().Delete(&user)
svc := AftersalesService{}
order, err := svc.Create(models.CreateAftersalesOrderDTO{
CompanyName: "AftersalesSerialCo",
CompanyAddress: "杭州市西湖区",
ContactName: "李四",
ContactPhone: "13800001234",
ServiceType: "hardware",
IssueDescription: "硬盘故障",
}, user.ID)
assert.NoError(t, err)
assert.NotNil(t, order)
assert.True(t, strings.HasPrefix(order.SerialNumber, "zjbf-sh-"))
assert.Len(t, order.SerialNumber, len("zjbf-sh-")+6, "default serial should be 6 digits (YYMMNN)")
assert.Equal(t, WorkOrderStatusCreated, order.WorkOrderStatus)
assert.Equal(t, AuthorizationStatusPending, order.AuthorizationStatus)
assert.NotNil(t, order.TechnicianID)
assert.Equal(t, user.ID, *order.TechnicianID)
database.DB.Unscoped().Delete(order)
database.DB.Unscoped().Where("company_name = ?", "AftersalesSerialCo").Delete(&models.Company{})
}
func TestAftersalesService_Create_SerialIncrementsWithinMonth(t *testing.T) {
user := seedTechnician(t, "aftersales_seq_tech")
defer database.DB.Unscoped().Delete(&user)
svc := AftersalesService{}
first, err := svc.Create(models.CreateAftersalesOrderDTO{
CompanyName: "SeqCo", CompanyAddress: "addr", ContactName: "A", ContactPhone: "13800002000",
ServiceType: "other", IssueDescription: "issue 1",
}, user.ID)
assert.NoError(t, err)
second, err := svc.Create(models.CreateAftersalesOrderDTO{
CompanyName: "SeqCo", CompanyAddress: "addr", ContactName: "A", ContactPhone: "13800002000",
ServiceType: "other", IssueDescription: "issue 2",
}, user.ID)
assert.NoError(t, err)
assert.NotEqual(t, first.SerialNumber, second.SerialNumber)
// 第二单的序号应大于第一单(按月递增)
assert.True(t, second.SerialNumber > first.SerialNumber)
database.DB.Unscoped().Delete(first)
database.DB.Unscoped().Delete(second)
database.DB.Unscoped().Where("company_name = ?", "SeqCo").Delete(&models.Company{})
}
func TestAftersalesService_SubmitForConfirmation_OwnerOnly(t *testing.T) {
owner := seedTechnician(t, "aftersales_owner")
intruder := seedTechnician(t, "aftersales_intruder")
defer database.DB.Unscoped().Delete(&owner)
defer database.DB.Unscoped().Delete(&intruder)
order := createOrderFor(t, owner, "13800003000")
defer database.DB.Unscoped().Delete(order)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
svc := AftersalesService{}
// 非负责人技术员不能提交
_, err := svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
ResolutionNote: "已重装系统",
}, intruder)
assert.Error(t, err)
// 负责人可以提交
updated, err := svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
ResolutionNote: "已重装系统",
}, owner)
assert.NoError(t, err)
assert.Equal(t, WorkOrderStatusPendingConfirmation, updated.WorkOrderStatus)
assert.Equal(t, "已重装系统", updated.ResolutionNote)
}
func TestAftersalesService_SubmitForConfirmation_RejectsClosed(t *testing.T) {
owner := seedTechnician(t, "aftersales_closed_owner")
defer database.DB.Unscoped().Delete(&owner)
order := createOrderFor(t, owner, "13800004000")
defer database.DB.Unscoped().Delete(order)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
svc := AftersalesService{}
_, err := svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
ResolutionNote: "first submit",
}, owner)
assert.NoError(t, err)
// 已经 pending_confirmation 状态,不能再提交
_, err = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
ResolutionNote: "second submit",
}, owner)
assert.Error(t, err)
}
func TestAftersalesService_PublicQuery_MasksPhoneAndSetsScannedAt(t *testing.T) {
owner := seedTechnician(t, "aftersales_public_owner")
defer database.DB.Unscoped().Delete(&owner)
order := createOrderFor(t, owner, "13800005000")
defer database.DB.Unscoped().Delete(order)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
svc := AftersalesService{}
view, err := svc.PublicQuery(order.SerialNumber)
assert.NoError(t, err)
assert.NotNil(t, view)
assert.Equal(t, order.CompanyName, view.CompanyName)
assert.Equal(t, order.ContactName, view.ContactName)
// PublicView 不应该带电话字段(只有以下字段,通过类型保证)
// 验证 ScannedAt 已设置
var refreshed models.AftersalesOrder
database.DB.Where("serial_number = ?", order.SerialNumber).First(&refreshed)
assert.NotNil(t, refreshed.ScannedAt)
}
func TestAftersalesService_CustomerConfirm_Authorize(t *testing.T) {
owner := seedTechnician(t, "aftersales_auth_owner")
defer database.DB.Unscoped().Delete(&owner)
order := createOrderFor(t, owner, "13800006789")
defer database.DB.Unscoped().Delete(order)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
svc := AftersalesService{}
_, _ = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
ResolutionNote: "done",
}, owner)
view, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
PhoneLast4: "6789",
Action: "authorize",
})
assert.NoError(t, err)
assert.NotNil(t, view)
assert.Equal(t, WorkOrderStatusClosed, view.WorkOrderStatus)
assert.Equal(t, AuthorizationStatusAuthorized, view.AuthorizationStatus)
assert.NotNil(t, view.ConfirmedAt)
}
func TestAftersalesService_CustomerConfirm_PhoneMismatch(t *testing.T) {
owner := seedTechnician(t, "aftersales_phone_owner")
defer database.DB.Unscoped().Delete(&owner)
order := createOrderFor(t, owner, "13800007777")
defer database.DB.Unscoped().Delete(order)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
svc := AftersalesService{}
_, _ = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
ResolutionNote: "done",
}, owner)
_, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
PhoneLast4: "0000",
Action: "authorize",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "手机号校验失败")
}
func TestAftersalesService_CustomerConfirm_RejectIncrementsCount(t *testing.T) {
owner := seedTechnician(t, "aftersales_reject_owner")
defer database.DB.Unscoped().Delete(&owner)
order := createOrderFor(t, owner, "13800008888")
defer database.DB.Unscoped().Delete(order)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
svc := AftersalesService{}
_, _ = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
ResolutionNote: "done",
}, owner)
view, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
PhoneLast4: "8888",
Action: "reject",
RejectReason: "没修好",
})
assert.NoError(t, err)
assert.Equal(t, WorkOrderStatusRejected, view.WorkOrderStatus)
assert.Equal(t, AuthorizationStatusUnauthorized, view.AuthorizationStatus)
var refreshed models.AftersalesOrder
database.DB.Where("serial_number = ?", order.SerialNumber).First(&refreshed)
assert.Equal(t, 1, refreshed.RejectCount)
assert.Contains(t, refreshed.ResolutionNote, "没修好")
// 退回后技术员可以再次提交
_, err = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
ResolutionNote: "re-do",
}, owner)
assert.NoError(t, err)
}
func TestAftersalesService_CustomerConfirm_RejectsWrongStatus(t *testing.T) {
owner := seedTechnician(t, "aftersales_wrongstatus_owner")
defer database.DB.Unscoped().Delete(&owner)
order := createOrderFor(t, owner, "13800009999")
defer database.DB.Unscoped().Delete(order)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
svc := AftersalesService{}
// 未提交客户确认,工单仍是 created,应该拒绝
_, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
PhoneLast4: "9999",
Action: "authorize",
})
assert.Error(t, err)
}
func TestAftersalesService_ForceClose_AdminOverride(t *testing.T) {
owner := seedTechnician(t, "aftersales_force_owner")
defer database.DB.Unscoped().Delete(&owner)
order := createOrderFor(t, owner, "13800001111")
defer database.DB.Unscoped().Delete(order)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
svc := AftersalesService{}
updated, err := svc.ForceClose(order.SerialNumber)
assert.NoError(t, err)
assert.Equal(t, WorkOrderStatusClosed, updated.WorkOrderStatus)
assert.Equal(t, AuthorizationStatusAuthorized, updated.AuthorizationStatus)
}