diff --git a/model/cc_queue.go b/model/cc_queue.go index b33b4368..42c48860 100644 --- a/model/cc_queue.go +++ b/model/cc_queue.go @@ -14,8 +14,13 @@ const ( ) const ( - QueuePayloadProgressiveCountKey string = "progressive_count" - QueueDefaultProgressiveCountValue int = 1 + QueuePayloadProgressiveCountKey string = "progressive_count" + QueuePayloadMaxAgentLineKey string = "max_agent_line" +) + +const ( + QueueDefaultProgressiveCountValue int = 1 + QueueDefaultMaxAgentLine int = 5 ) type Queue struct { @@ -317,44 +322,121 @@ func (q *Queue) Patch(p *QueuePatch) { } func (q *Queue) IsValid() AppError { - if q.Calendar == nil && !(q.Type == QueueTypeInboundCall || q.Type == QueueTypeInboundChat) { - return NewBadRequestError("model.queue.valid.calendar", "Calendar is required") + if q == nil { + return NewBadRequestError("model.cc_queue.validate.empty", "Queue configuration cannot be empty") } - if q.Type == QueueTypePredictCall || q.Type == QueueTypeProgressiveCall { - if q.Payload == nil { - q.Payload = make(StringInterface) - } + if q.Payload == nil { + return NewBadRequestError("model.cc_queue.validate.payload.empty", "Queue payload is required and cannot be empty") + } - progressiveCountValue, exists := q.Payload[QueuePayloadProgressiveCountKey] - if !exists { - progressiveCountValue = QueueDefaultProgressiveCountValue - } + if err := q.prepareProgressiveCount(); err != nil { + return err + } - var progessiveCount int - switch r := progressiveCountValue.(type) { - case int: - progessiveCount = r - case float64: - progessiveCount = int(r) - case float32: - progessiveCount = int(r) - case string: - parsed, err := strconv.Atoi(r) - if err != nil { - return NewBadRequestError("model.queue.valid.progressive_count_unconvertable_from_string", err.Error()) - } - - progessiveCount = parsed - default: - return NewBadRequestError("model.queue.valid.unsupported_type", fmt.Sprintf("Received unsupported type for progressive count: %T", r)) - } + if err := q.validateMaxAgentLines(); err != nil { + return err + } + + if err := q.validateCalendar(); err != nil { + return err + } + + return nil +} + +func (q *Queue) prepareProgressiveCount() AppError { + if !q.IsProgressiveOrPredictive() { + return nil + } + + progressiveCountValue, exists := q.Payload[QueuePayloadProgressiveCountKey] + if !exists { + progressiveCountValue = QueueDefaultProgressiveCountValue + } + + progessiveCount, err := q.tryRetrieveNumberConfigurationParameter(QueuePayloadProgressiveCountKey, progressiveCountValue) + if err != nil { + return err + } + + q.Payload[QueuePayloadProgressiveCountKey] = max(progessiveCount, QueueDefaultProgressiveCountValue) + + return nil +} - if progessiveCount <= 0 { - progessiveCount = QueueDefaultProgressiveCountValue +func (q *Queue) tryRetrieveNumberConfigurationParameter(paramName string, value any) (int, AppError) { + var result int + + switch r := value.(type) { + case int: + result = r + case float64: + if r != float64(int(r)) { + return 0, NewBadRequestError( + "model.cc_queue.validate.config.decimal_not_allowed", + fmt.Sprintf("Parameter '%s' must be a whole integer number, got float '%v'", paramName, r), + ) + } + result = int(r) + case float32: + if r != float32(int(r)) { + return 0, NewBadRequestError( + "model.cc_queue.validate.config.decimal_not_allowed", + fmt.Sprintf("Parameter '%s' must be a whole integer number, got float '%v'", paramName, r), + ) } + result = int(r) + case string: + parsed, err := strconv.Atoi(r) + if err != nil { + return 0, NewBadRequestError( + "model.cc_queue.validate.config.invalid_string_number", + fmt.Sprintf("Parameter '%s' value '%s' cannot be parsed as an integer", paramName, r), + ) + } + result = parsed + default: + return 0, NewBadRequestError( + "model.cc_queue.validate.config.unsupported_type", + fmt.Sprintf("Parameter '%s' received unsupported data type '%T'. Expected number or numeric string", paramName, r), + ) + } + + return result, nil +} + +func (q *Queue) validateMaxAgentLines() AppError { + if !q.IsProgressiveOrPredictive() { + return nil + } - q.Payload[QueuePayloadProgressiveCountKey] = progessiveCount + maxAgentLinesValue, exists := q.Payload[QueuePayloadMaxAgentLineKey] + if !exists { + q.Payload[QueuePayloadMaxAgentLineKey] = QueueDefaultMaxAgentLine + return nil + } + + maxAgentLines, err := q.tryRetrieveNumberConfigurationParameter(QueuePayloadMaxAgentLineKey, maxAgentLinesValue) + if err != nil { + return err + } + + q.Payload[QueuePayloadMaxAgentLineKey] = max(maxAgentLines, QueueDefaultMaxAgentLine) + + return nil +} + +func (q *Queue) validateCalendar() AppError { + if !q.RequireCalendar() { + return nil + } + + if q.Calendar.IsEmpty() { + return NewBadRequestError( + "model.cc_queue.validate.calendar.required", + "A calendar is required for outbound or non-inbound queue types", + ) } return nil @@ -402,3 +484,11 @@ func (q *Queue) AfterSchemaId() *int64 { } return nil } + +func (q *Queue) IsProgressiveOrPredictive() bool { + return q.Type == QueueTypePredictCall || q.Type == QueueTypeProgressiveCall +} + +func (q *Queue) RequireCalendar() bool { + return !(q.Type == QueueTypeInboundCall || q.Type == QueueTypeInboundChat) +} diff --git a/model/cc_queue_test.go b/model/cc_queue_test.go index c71dfd5a..0f738848 100644 --- a/model/cc_queue_test.go +++ b/model/cc_queue_test.go @@ -6,117 +6,178 @@ import ( "github.com/webitel/engine/model" ) -func TestQueue_IsValid_ProgressiveCountValidation(t *testing.T) { +func TestQueue_IsValid(t *testing.T) { t.Parallel() - validCalendar := &model.Lookup{Id: 1} - tests := []struct { name string - queue model.Queue + queue *model.Queue wantErr bool - errID string + errCode string }{ { - name: "predictive queue valid progressive count int", - queue: model.Queue{ - Type: model.QueueTypePredictCall, - Calendar: validCalendar, + name: "nil queue", + queue: nil, + wantErr: true, + errCode: "model.cc_queue.validate.empty", + }, + { + name: "nil payload", + queue: &model.Queue{ + Payload: nil, + }, + wantErr: true, + errCode: "model.cc_queue.validate.payload.empty", + }, + { + name: "valid inbound queue without calendar", + queue: &model.Queue{ + Type: model.QueueTypeInboundCall, + Payload: model.StringInterface{}, + }, + wantErr: false, + }, + { + name: "valid progressive queue with valid payload and calendar", + queue: &model.Queue{ + Type: model.QueueTypeProgressiveCall, Payload: model.StringInterface{ - model.QueuePayloadProgressiveCountKey: 5, + model.QueuePayloadProgressiveCountKey: 2, + model.QueuePayloadMaxAgentLineKey: 5, }, + Calendar: &model.Lookup{Id: 1, Name: "Default"}, }, wantErr: false, }, { - name: "progressive queue valid progressive count string", - queue: model.Queue{ - Type: model.QueueTypeProgressiveCall, - Calendar: validCalendar, + name: "progressive queue sets default progressive count when missing", + queue: &model.Queue{ + Type: model.QueueTypeProgressiveCall, Payload: model.StringInterface{ - model.QueuePayloadProgressiveCountKey: "10", + model.QueuePayloadMaxAgentLineKey: 3, }, + Calendar: &model.Lookup{Id: 1}, }, wantErr: false, }, { - name: "progressive count does not exist", - queue: model.Queue{ - Type: model.QueueTypePredictCall, - Calendar: validCalendar, - Payload: model.StringInterface{}, + name: "progressive queue sets default progressive count when non-positive", + queue: &model.Queue{ + Type: model.QueueTypeProgressiveCall, + Payload: model.StringInterface{ + model.QueuePayloadProgressiveCountKey: 0, + model.QueuePayloadMaxAgentLineKey: 3, + }, + Calendar: &model.Lookup{Id: 1}, }, - wantErr: true, - errID: "model.queue.valid.progressive_count_not_exist", + wantErr: false, }, { - name: "progressive count invalid string", - queue: model.Queue{ - Type: model.QueueTypeProgressiveCall, - Calendar: validCalendar, + name: "progressive queue float decimal rejected in progressive count", + queue: &model.Queue{ + Type: model.QueueTypeProgressiveCall, Payload: model.StringInterface{ - model.QueuePayloadProgressiveCountKey: "abc", + model.QueuePayloadProgressiveCountKey: 2.5, + model.QueuePayloadMaxAgentLineKey: 3, }, + Calendar: &model.Lookup{Id: 1}, }, wantErr: true, - errID: "model.queue.valid.progressive_count_unconvertable_from_string", + errCode: "model.cc_queue.validate.config.decimal_not_allowed", }, { - name: "progressive count unsupported type", - queue: model.Queue{ - Type: model.QueueTypePredictCall, - Calendar: validCalendar, + name: "progressive queue float integer accepted in progressive count", + queue: &model.Queue{ + Type: model.QueueTypeProgressiveCall, Payload: model.StringInterface{ - model.QueuePayloadProgressiveCountKey: true, + model.QueuePayloadProgressiveCountKey: float64(2), + model.QueuePayloadMaxAgentLineKey: 3, }, + Calendar: &model.Lookup{Id: 1}, }, - wantErr: true, - errID: "model.queue.valid.unsupported_type", + wantErr: false, }, { - name: "progressive count zero", - queue: model.Queue{ - Type: model.QueueTypeProgressiveCall, - Calendar: validCalendar, + name: "progressive queue string number accepted in progressive count", + queue: &model.Queue{ + Type: model.QueueTypeProgressiveCall, Payload: model.StringInterface{ - model.QueuePayloadProgressiveCountKey: 0, + model.QueuePayloadProgressiveCountKey: "3", + model.QueuePayloadMaxAgentLineKey: 3, }, + Calendar: &model.Lookup{Id: 1}, + }, + wantErr: false, + }, + { + name: "progressive queue invalid string rejected in progressive count", + queue: &model.Queue{ + Type: model.QueueTypeProgressiveCall, + Payload: model.StringInterface{ + model.QueuePayloadProgressiveCountKey: "invalid", + model.QueuePayloadMaxAgentLineKey: 3, + }, + Calendar: &model.Lookup{Id: 1}, }, wantErr: true, - errID: "model.queue.valid.progressive_count_must_be_gt_zero", + errCode: "model.cc_queue.validate.config.invalid_string_number", }, { - name: "progressive count negative", - queue: model.Queue{ - Type: model.QueueTypePredictCall, - Calendar: validCalendar, + name: "progressive queue unsupported type rejected in progressive count", + queue: &model.Queue{ + Type: model.QueueTypeProgressiveCall, Payload: model.StringInterface{ - model.QueuePayloadProgressiveCountKey: -5, + model.QueuePayloadProgressiveCountKey: true, + model.QueuePayloadMaxAgentLineKey: 3, }, + Calendar: &model.Lookup{Id: 1}, }, wantErr: true, - errID: "model.queue.valid.progressive_count_must_be_gt_zero", + errCode: "model.cc_queue.validate.config.unsupported_type", }, { - name: "non progressive queue skips validation", - queue: model.Queue{ - Type: model.QueueTypeInboundCall, - Calendar: nil, + name: "progressive queue missing max agent line", + queue: &model.Queue{ + Type: model.QueueTypeProgressiveCall, + Payload: model.StringInterface{}, + Calendar: &model.Lookup{Id: 1}, + }, + wantErr: false, + }, + { + name: "progressive queue non-positive max agent line", + queue: &model.Queue{ + Type: model.QueueTypeProgressiveCall, Payload: model.StringInterface{ - model.QueuePayloadProgressiveCountKey: "invalid", + model.QueuePayloadMaxAgentLineKey: 0, }, + Calendar: &model.Lookup{Id: 1}, }, wantErr: false, }, { - name: "nil payload for predictive queue", - queue: model.Queue{ - Type: model.QueueTypePredictCall, - Calendar: validCalendar, - Payload: nil, + name: "predictive queue requires calendar", + queue: &model.Queue{ + Type: model.QueueTypePredictCall, + Payload: model.StringInterface{ + model.QueuePayloadMaxAgentLineKey: 1, + }, + Calendar: nil, }, wantErr: true, - errID: "model.queue.valid.progressive_count_not_exist", + errCode: "model.cc_queue.validate.calendar.required", + }, + { + name: "predictive queue empty calendar struct", + queue: &model.Queue{ + Type: model.QueueTypePredictCall, + Payload: model.StringInterface{ + model.QueuePayloadMaxAgentLineKey: 1, + }, + Calendar: &model.Lookup{Id: 0, Name: ""}, + }, + wantErr: true, + errCode: "model.cc_queue.validate.calendar.required", }, } @@ -124,22 +185,16 @@ func TestQueue_IsValid_ProgressiveCountValidation(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - got := tt.queue.IsValid() - - if tt.wantErr { - if got == nil { - t.Fatalf("expected error, got nil") - } - - if got.GetId() != tt.errID { - t.Fatalf("unexpected error id: got %s, want %s", got.GetId(), tt.errID) - } + err := tt.queue.IsValid() - return + if (err != nil) != tt.wantErr { + t.Fatalf("IsValid() error = %v, wantErr %v", err, tt.wantErr) } - if got != nil { - t.Fatalf("expected nil error, got %v", got) + if tt.wantErr && err != nil && tt.errCode != "" { + if err.GetId() != tt.errCode { + t.Errorf("IsValid() err.Id = %v, want %v", err.GetId(), tt.errCode) + } } }) } diff --git a/model/utils.go b/model/utils.go index 23148904..d2961e05 100644 --- a/model/utils.go +++ b/model/utils.go @@ -31,6 +31,18 @@ type ( } ) +func (l *Lookup) IsEmpty() bool { + if l == nil { + return true + } + + if l.Id <= 0 && l.Name == "" { + return true + } + + return false +} + func (l *Lookup) GetSafeId() *int { if l == nil || l.Id == 0 { return nil