Add responsible signature to aftersales confirmation
This commit is contained in:
+26
-22
@@ -266,12 +266,13 @@ type AftersalesOrder struct {
|
||||
WorkOrderStatus string `gorm:"size:32;default:'created'" json:"workOrderStatus"`
|
||||
AuthorizationStatus string `gorm:"size:32;default:'pending'" json:"authorizationStatus"`
|
||||
|
||||
TechnicianID *uint `json:"technicianId"`
|
||||
CreatedBy *uint `json:"createdBy"`
|
||||
ScannedAt *time.Time `json:"scannedAt"`
|
||||
ConfirmedAt *time.Time `json:"confirmedAt"`
|
||||
RejectCount int `gorm:"default:0" json:"rejectCount"`
|
||||
Signature string `gorm:"type:text" json:"signature,omitempty"`
|
||||
TechnicianID *uint `json:"technicianId"`
|
||||
CreatedBy *uint `json:"createdBy"`
|
||||
ScannedAt *time.Time `json:"scannedAt"`
|
||||
ConfirmedAt *time.Time `json:"confirmedAt"`
|
||||
RejectCount int `gorm:"default:0" json:"rejectCount"`
|
||||
Signature string `gorm:"type:text" json:"signature,omitempty"`
|
||||
ResponsibleSignature string `gorm:"type:text" json:"responsibleSignature,omitempty"`
|
||||
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
@@ -310,11 +311,13 @@ type SubmitForConfirmationDTO struct {
|
||||
|
||||
// CustomerConfirmDTO 客户确认请求
|
||||
// Signature 为客户在网页上手写签名的 base64 PNG dataURL,仅 authorize 时必填
|
||||
// ResponsibleSignature 为负责人在网页上手写签名的 base64 PNG dataURL,仅 authorize 时必填
|
||||
// RejectReason 为客户拒绝的原因,仅 reject 时必填
|
||||
type CustomerConfirmDTO struct {
|
||||
Action string `json:"action" validate:"required,oneof=authorize reject"`
|
||||
Signature string `json:"signature,omitempty" validate:"required_if=Action authorize"`
|
||||
RejectReason string `json:"rejectReason,omitempty" validate:"required_if=Action reject"`
|
||||
Action string `json:"action" validate:"required,oneof=authorize reject"`
|
||||
Signature string `json:"signature,omitempty" validate:"required_if=Action authorize"`
|
||||
ResponsibleSignature string `json:"responsibleSignature,omitempty" validate:"required_if=Action authorize"`
|
||||
RejectReason string `json:"rejectReason,omitempty" validate:"required_if=Action reject"`
|
||||
}
|
||||
|
||||
// ReassignAftersalesDTO 重新分配技术员请求
|
||||
@@ -324,17 +327,18 @@ type ReassignAftersalesDTO struct {
|
||||
|
||||
// AftersalesPublicView 公开查询返回视图(脱敏)
|
||||
type AftersalesPublicView struct {
|
||||
SerialNumber string `json:"serialNumber"`
|
||||
CompanyName string `json:"companyName"`
|
||||
CompanyAddress string `json:"companyAddress"`
|
||||
ContactName string `json:"contactName"`
|
||||
ServiceType string `json:"serviceType"`
|
||||
IssueDescription string `json:"issueDescription"`
|
||||
ResolutionNote string `json:"resolutionNote"`
|
||||
WorkOrderStatus string `json:"workOrderStatus"`
|
||||
AuthorizationStatus string `json:"authorizationStatus"`
|
||||
TechnicianName string `json:"technicianName"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ConfirmedAt *time.Time `json:"confirmedAt"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
SerialNumber string `json:"serialNumber"`
|
||||
CompanyName string `json:"companyName"`
|
||||
CompanyAddress string `json:"companyAddress"`
|
||||
ContactName string `json:"contactName"`
|
||||
ServiceType string `json:"serviceType"`
|
||||
IssueDescription string `json:"issueDescription"`
|
||||
ResolutionNote string `json:"resolutionNote"`
|
||||
WorkOrderStatus string `json:"workOrderStatus"`
|
||||
AuthorizationStatus string `json:"authorizationStatus"`
|
||||
TechnicianName string `json:"technicianName"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ConfirmedAt *time.Time `json:"confirmedAt"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
ResponsibleSignature string `json:"responsibleSignature,omitempty"`
|
||||
}
|
||||
|
||||
@@ -329,18 +329,19 @@ func (s *AftersalesService) PublicQuery(serialNumber string) (*models.Aftersales
|
||||
}
|
||||
|
||||
view := &models.AftersalesPublicView{
|
||||
SerialNumber: order.SerialNumber,
|
||||
CompanyName: order.CompanyName,
|
||||
CompanyAddress: order.CompanyAddress,
|
||||
ContactName: order.ContactName,
|
||||
ServiceType: order.ServiceType,
|
||||
IssueDescription: order.IssueDescription,
|
||||
ResolutionNote: order.ResolutionNote,
|
||||
WorkOrderStatus: order.WorkOrderStatus,
|
||||
AuthorizationStatus: order.AuthorizationStatus,
|
||||
CreatedAt: order.CreatedAt,
|
||||
ConfirmedAt: order.ConfirmedAt,
|
||||
Signature: order.Signature,
|
||||
SerialNumber: order.SerialNumber,
|
||||
CompanyName: order.CompanyName,
|
||||
CompanyAddress: order.CompanyAddress,
|
||||
ContactName: order.ContactName,
|
||||
ServiceType: order.ServiceType,
|
||||
IssueDescription: order.IssueDescription,
|
||||
ResolutionNote: order.ResolutionNote,
|
||||
WorkOrderStatus: order.WorkOrderStatus,
|
||||
AuthorizationStatus: order.AuthorizationStatus,
|
||||
CreatedAt: order.CreatedAt,
|
||||
ConfirmedAt: order.ConfirmedAt,
|
||||
Signature: order.Signature,
|
||||
ResponsibleSignature: order.ResponsibleSignature,
|
||||
}
|
||||
if order.Technician != nil {
|
||||
view.TechnicianName = order.Technician.Name
|
||||
@@ -372,10 +373,14 @@ func (s *AftersalesService) CustomerConfirm(serialNumber string, dto models.Cust
|
||||
if err := validateSignature(dto.Signature); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateSignature(dto.ResponsibleSignature); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
order.WorkOrderStatus = WorkOrderStatusClosed
|
||||
order.AuthorizationStatus = AuthorizationStatusAuthorized
|
||||
order.ConfirmedAt = &now
|
||||
order.Signature = strings.TrimSpace(dto.Signature)
|
||||
order.ResponsibleSignature = strings.TrimSpace(dto.ResponsibleSignature)
|
||||
case "reject":
|
||||
reason := strings.TrimSpace(dto.RejectReason)
|
||||
if reason == "" {
|
||||
|
||||
@@ -214,9 +214,11 @@ func TestAftersalesService_CustomerConfirm_Authorize(t *testing.T) {
|
||||
}, owner)
|
||||
|
||||
sig := validSignatureFixture()
|
||||
responsibleSig := validSignatureFixture()
|
||||
view, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
|
||||
Action: "authorize",
|
||||
Signature: sig,
|
||||
Action: "authorize",
|
||||
Signature: sig,
|
||||
ResponsibleSignature: responsibleSig,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, view)
|
||||
@@ -224,10 +226,12 @@ func TestAftersalesService_CustomerConfirm_Authorize(t *testing.T) {
|
||||
assert.Equal(t, AuthorizationStatusAuthorized, view.AuthorizationStatus)
|
||||
assert.NotNil(t, view.ConfirmedAt)
|
||||
assert.Equal(t, sig, view.Signature)
|
||||
assert.Equal(t, responsibleSig, view.ResponsibleSignature)
|
||||
|
||||
var refreshed models.AftersalesOrder
|
||||
database.DB.Where("serial_number = ?", order.SerialNumber).First(&refreshed)
|
||||
assert.Equal(t, sig, refreshed.Signature)
|
||||
assert.Equal(t, responsibleSig, refreshed.ResponsibleSignature)
|
||||
}
|
||||
|
||||
func TestAftersalesService_CustomerConfirm_AuthorizeRejectsEmptySignature(t *testing.T) {
|
||||
@@ -244,8 +248,31 @@ func TestAftersalesService_CustomerConfirm_AuthorizeRejectsEmptySignature(t *tes
|
||||
}, owner)
|
||||
|
||||
_, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
|
||||
Action: "authorize",
|
||||
Signature: "",
|
||||
Action: "authorize",
|
||||
Signature: "",
|
||||
ResponsibleSignature: validSignatureFixture(),
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "签名")
|
||||
}
|
||||
|
||||
func TestAftersalesService_CustomerConfirm_AuthorizeRejectsEmptyResponsibleSignature(t *testing.T) {
|
||||
confirmTest(t)
|
||||
owner := seedTechnician(t, "aftersales_empty_responsible_sig_owner")
|
||||
defer database.DB.Unscoped().Delete(&owner)
|
||||
|
||||
order := createOrderFor(t, owner, "13800007778")
|
||||
defer database.DB.Unscoped().Delete(order)
|
||||
|
||||
svc := AftersalesService{}
|
||||
_, _ = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
|
||||
ResolutionNote: "done",
|
||||
}, owner)
|
||||
|
||||
_, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
|
||||
Action: "authorize",
|
||||
Signature: validSignatureFixture(),
|
||||
ResponsibleSignature: "",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "签名")
|
||||
@@ -266,8 +293,9 @@ func TestAftersalesService_CustomerConfirm_AuthorizeRejectsInvalidSignature(t *t
|
||||
|
||||
// 非 dataURL 格式
|
||||
_, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
|
||||
Action: "authorize",
|
||||
Signature: "not-a-data-url",
|
||||
Action: "authorize",
|
||||
Signature: "not-a-data-url",
|
||||
ResponsibleSignature: validSignatureFixture(),
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "签名格式")
|
||||
@@ -275,8 +303,9 @@ func TestAftersalesService_CustomerConfirm_AuthorizeRejectsInvalidSignature(t *t
|
||||
// 太短
|
||||
tiny := "data:image/png;base64," + base64.StdEncoding.EncodeToString([]byte("xx"))
|
||||
_, err = svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
|
||||
Action: "authorize",
|
||||
Signature: tiny,
|
||||
Action: "authorize",
|
||||
Signature: tiny,
|
||||
ResponsibleSignature: validSignatureFixture(),
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "过短")
|
||||
@@ -348,8 +377,9 @@ func TestAftersalesService_CustomerConfirm_RejectsWrongStatus(t *testing.T) {
|
||||
svc := AftersalesService{}
|
||||
// 未提交客户确认,工单仍是 created,应该拒绝
|
||||
_, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
|
||||
Action: "authorize",
|
||||
Signature: validSignatureFixture(),
|
||||
Action: "authorize",
|
||||
Signature: validSignatureFixture(),
|
||||
ResponsibleSignature: validSignatureFixture(),
|
||||
})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user