Skip to content

Commit

Permalink
Merge pull request #504 from COS301-SE-2024/feat/mediator-assignment-…
Browse files Browse the repository at this point in the history
…update

Feat/mediator assignment update
  • Loading branch information
CaelanHill authored Sep 29, 2024
2 parents 670e966 + 38c3163 commit d77b644
Show file tree
Hide file tree
Showing 15 changed files with 914 additions and 4 deletions.
42 changes: 40 additions & 2 deletions api/handlers/dispute/dispute.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,13 +544,28 @@ func (h Dispute) CreateDispute(c *gin.Context) {
}

//asssign experts to dispute
selected, err := h.Model.AssignExpertsToDispute(disputeId)
// selected, err := h.Model.AssignExpertsToDispute(disputeId)
// if err != nil {
// logger.WithError(err).Error("Error assigning experts to dispute")
// c.JSON(http.StatusInternalServerError, models.Response{Error: "Error assigning experts to dispute"})
// return
// }
// logger.Info("Assigned experts", selected)

//assign using mediator assignment algorithm
expertIds, err := h.MediatorAssignment.AssignMediator(3, int(disputeId))
if err != nil {
logger.WithError(err).Error("Error assigning experts to dispute")
c.JSON(http.StatusInternalServerError, models.Response{Error: "Error assigning experts to dispute"})
return
}

err = h.Model.AssignExpertswithDisputeAndExpertIDs(disputeId, expertIds)
if err != nil {
logger.WithError(err).Error("Error assigning experts to dispute")
c.JSON(http.StatusInternalServerError, models.Response{Error: "Error assigning experts to dispute"})
return
}
logger.Info("Assigned experts", selected)

// Respond with success message
if !defaultAccount {
Expand Down Expand Up @@ -742,6 +757,29 @@ func (h Dispute) ExpertObjectionsReview(c *gin.Context) {
return
}

if *req.Status == models.ObjectionSustained {
disputeId, err := h.Model.GetDisputeIDByTicketID(int64(objectionIdInt))
if err != nil {
logger.WithError(err).Error("Error getting dispute ID")
c.JSON(http.StatusInternalServerError, models.Response{Error: "Error getting dispute ID"})
return
}

expertIds, err := h.MediatorAssignment.AssignMediator(3, int(disputeId))
if err != nil {
logger.WithError(err).Error("Error assigning experts to dispute")
c.JSON(http.StatusInternalServerError, models.Response{Error: "Error assigning experts to dispute"})
return
}

err = h.Model.AssignExpertswithDisputeAndExpertIDs(disputeId, expertIds)
if err != nil {
logger.WithError(err).Error("Error assigning experts to dispute")
c.JSON(http.StatusInternalServerError, models.Response{Error: "Error assigning experts to dispute"})
return
}
}

logger.Info("Expert objections reviewed successfully")
h.AuditLogger.LogDisputeProceedings(models.Disputes, map[string]interface{}{"user": claims, "message": "Expert objections reviewed successfully"})

Expand Down
46 changes: 45 additions & 1 deletion api/handlers/dispute/dispute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dispute_test

import (
"api/handlers/dispute"
mediatorassignment "api/mediatorAssignment"
"api/models"
"bytes"
"encoding/json"
Expand Down Expand Up @@ -56,6 +57,7 @@ type DisputeErrorTestSuite struct {
mockOrchestrator *mockOrchestrator
mockEnv *mockEnv
mockTicket *mockTicketModel
mockAlgorithm *mockAlgorithmModel
}

func (suite *DisputeErrorTestSuite) SetupTest() {
Expand All @@ -66,8 +68,9 @@ func (suite *DisputeErrorTestSuite) SetupTest() {
suite.mockOrchestrator = &mockOrchestrator{}
suite.mockEnv = &mockEnv{}
suite.mockTicket = &mockTicketModel{}
suite.mockAlgorithm = &mockAlgorithmModel{}

handler := dispute.Dispute{Model: suite.disputeMock, JWT: suite.jwtMock, Email: suite.emailMock, AuditLogger: suite.auditMock, OrchestratorEntity: suite.mockOrchestrator, Env: suite.mockEnv, TicketModel: suite.mockTicket}
handler := dispute.Dispute{Model: suite.disputeMock, JWT: suite.jwtMock, Email: suite.emailMock, AuditLogger: suite.auditMock, OrchestratorEntity: suite.mockOrchestrator, Env: suite.mockEnv, TicketModel: suite.mockTicket, MediatorAssignment:suite.mockAlgorithm}
gin.SetMode("release")
router := gin.Default()
router.Use(suite.jwtMock.JWTMiddleware)
Expand Down Expand Up @@ -101,6 +104,32 @@ func createFileField(w *multipart.Writer, field, filename, value string) {
}

// ---------------------------------------------------------------- MODEL MOCKS
//mock algorithgm model

type mockAlgorithmModel struct {
throwErrors bool
Error error
returnValue float64
}

func (m *mockAlgorithmModel) CalculateScore(summaries []models.ExpertSummaryView, componentID int) []mediatorassignment.ResultWithID {
if m.throwErrors {
return nil
}
return []mediatorassignment.ResultWithID{
{
ID: 1,
Result: m.returnValue,
},
}
}

func (m *mockAlgorithmModel) AssignMediator(count, disputeID int) ([]int, error) {
if m.throwErrors {
return nil, m.Error
}
return []int{1}, nil
}

//ticket mock

Expand Down Expand Up @@ -245,6 +274,21 @@ func (m *mockDisputeModel) GetEvidenceByDispute(disputeId int64) ([]models.Evide
}
return []models.Evidence{}, nil
}

func (m *mockDisputeModel) GetDisputeIDByTicketID(ticketID int64) (int64, error) {
if m.throwErrors {
return 0, errors.ErrUnsupported
}
return 0, nil
}

func (m *mockDisputeModel) AssignExpertswithDisputeAndExpertIDs(disputeID int64, expertIDs []int) error {
if m.throwErrors {
return errors.ErrUnsupported
}
return nil
}

func (m *mockDisputeModel) GetDisputeExperts(disputeId int64) ([]models.Expert, error) {
if m.throwErrors {
return nil, errors.ErrUnsupported
Expand Down
37 changes: 37 additions & 0 deletions api/handlers/dispute/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"api/handlers/notifications"
"api/handlers/ticket"
"api/handlers/workflow"
mediatorassignment "api/mediatorAssignment"
"api/middleware"
"api/models"
"api/utilities"
Expand Down Expand Up @@ -46,10 +47,12 @@ type DisputeModel interface {

ObjectExpert(disputeId, expertId, ticketId int64) error
ReviewExpertObjection(objectionId int64, approved models.ExpObjStatus) error
GetDisputeIDByTicketID(ticketID int64) (int64, error)
GetExpertRejections(expertID, disputeID *int64, limit, offset *int) ([]models.ExpertObjectionsView, error)

CreateDefaultUser(email string, fullName string, pass string) error
AssignExpertsToDispute(disputeID int64) ([]models.User, error)
AssignExpertswithDisputeAndExpertIDs(disputeID int64, expertIDs []int) error

GetWorkflowRecordByID(id uint64) (*models.Workflow, error)
CreateActiverWorkflow(workflow *models.ActiveWorkflows) error
Expand All @@ -70,6 +73,7 @@ type Dispute struct {
Env env.Env
AuditLogger auditLogger.DisputeProceedingsLoggerInterface
OrchestratorEntity WorkflowOrchestrator
MediatorAssignment mediatorassignment.AlgorithmAssignment
WorkflowModel workflow.WorkflowDBModel
}

Expand Down Expand Up @@ -141,13 +145,15 @@ type disputeModelReal struct {
}

func NewHandler(db *gorm.DB, envReader env.Env) Dispute {

return Dispute{
Email: notifications.NewHandler(db),
JWT: middleware.NewJwtMiddleware(),
Env: env.NewEnvLoader(),
Model: &disputeModelReal{db: db, env: env.NewEnvLoader()},
AuditLogger: auditLogger.NewDisputeProceedingsLogger(db, envReader),
OrchestratorEntity: OrchestratorReal{},
MediatorAssignment: mediatorassignment.DefaultAlorithmAssignment(db),
TicketModel: ticket.NetTicketModelReal(db, envReader),
WorkflowModel: &workflow.WorkflowModelReal{DB: db},
}
Expand Down Expand Up @@ -444,6 +450,7 @@ func (m *disputeModelReal) GetDisputeExperts(disputeId int64) (experts []models.
Select("users.id, users.first_name || ' ' || users.surname AS full_name, email, users.phone_number AS phone, role").
Joins("JOIN users ON dispute_experts_view.expert = users.id").
Where("dispute = ?", disputeId).
Where("dispute_experts_view.status = 'Approved'").
Where("role = 'Mediator' OR role = 'Arbitrator' OR role = 'Conciliator' OR role = 'expert'").
Find(&experts).Error

Expand Down Expand Up @@ -624,6 +631,17 @@ func (m *disputeModelReal) CreateDefaultUser(email string, fullName string, pass

// bandaid fix, will be removed in future

func (m *disputeModelReal) AssignExpertswithDisputeAndExpertIDs(disputeID int64, expertIDs []int) error {
logger := utilities.NewLogger().LogWithCaller()
for _, expertID := range expertIDs {
if err := m.db.Exec("INSERT INTO dispute_experts_view VALUES (?, ?)", disputeID, expertID).Error; err != nil {
logger.WithError(err).Error("Error inserting expert into dispute_experts table")
return err
}
}
return nil
}

func (m disputeModelReal) AssignExpertsToDispute(disputeID int64) ([]models.User, error) {
// Seed the random number generator
rand.Seed(time.Now().UnixNano())
Expand Down Expand Up @@ -983,3 +1001,22 @@ func (m *disputeModelReal) GetExpertRejections(expertID, disputeID *int64, limit

return rejections, err
}

func (m *disputeModelReal) GetDisputeIDByTicketID(ticketID int64) (int64, error) {
logger := utilities.NewLogger().LogWithCaller()
var disputeID int64
err := m.db.Raw(`SELECT
t.dispute_id
FROM
expert_objections eo
JOIN
tickets t ON eo.ticket_id = t.id
WHERE
eo.id = ?`, ticketID).Scan(&disputeID).Error
if err != nil {
logger.WithError(err).Error("Error retrieving dispute ID by ticket ID")
return 0 , err
}
return disputeID, err
}

147 changes: 147 additions & 0 deletions api/mediatorAssignment/algorithm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package mediatorassignment

import (
"api/models"
"api/utilities"
"math/rand/v2"
"sort"

"gorm.io/gorm"
)

// MediatorAssignment struct and interface
type AlgorithmAssignment interface {
AssignMediator(count, disputeID int) ([]int, error)
CalculateScore(summaries []models.ExpertSummaryView, componentID int) []ResultWithID
}

type MediatorAssignment struct {
Components []AlgorithmComponent
DB DBModel
}

func (m *MediatorAssignment) AssignMediator(count, disputeID int) ([]int, error) {
logger := utilities.NewLogger().LogWithCaller()
// Get all the experts
experts, err := m.DB.GetExpertSummaryViews()
if err != nil {
return nil, err
}

logger.Info("Experts\n", experts)

// Loop through all the experts
var intermediateResults []ResultWithID
if len(experts) > 0 {
logger.Info("Calculating scores for experts")
intermediateResults = m.CalculateScore(experts, 0)
for i := 1; i < len(m.Components); i++ {
logger.Info("results\n", intermediateResults)
intermediateResults = m.Components[i].ApplyOperator(intermediateResults, m.CalculateScore(experts, i))
}
} else {
intermediateResults = m.assignRandomValues(experts)
}
logger.Info("final\n", intermediateResults)


// Sort the results
sort.Slice(intermediateResults, func(i, j int) bool {
return intermediateResults[i].Result > intermediateResults[j].Result
})

logger.Info("Sorted results\n", intermediateResults)
//get top 10 experts and check they are not rejected



topResults, err := m.GetTopResults(intermediateResults, count, disputeID)
if err != nil {
return nil, err
}

logger.Info("Top results\n", topResults)

// Get the expert IDs
var expertIDs []int
for _, result := range topResults {
expertIDs = append(expertIDs, int(result.ID))
}

logger.Info("Expert IDs\n", expertIDs)
return expertIDs, nil
}

func (m *MediatorAssignment) CalculateScore(summaries []models.ExpertSummaryView, componentID int) []ResultWithID {
results := make([]ResultWithID, len(summaries))
component := m.Components[componentID]
for i, summary := range summaries {
results[i] = component.CalculateScore(summary)
}
return results
}

func (m *MediatorAssignment) assignRandomValues(summaries []models.ExpertSummaryView) []ResultWithID {
// assign random values to the experts
results := make([]ResultWithID, len(summaries))
for i, summary := range summaries {
results[i] = ResultWithID{ID: summary.ExpertID, Result: rand.Float64()}
}
return results
}

func (m *MediatorAssignment) GetTopResults(results []ResultWithID, count int, disputeID int) ([]ResultWithID,error) {
rejectedExperts, err := m.DB.GetRejectionFromDispute(disputeID)
if err != nil {
return nil, err
}

var topResults []ResultWithID
index := 0
for len(topResults) < count && index < len(results) {
if !m.isExpertRejected(rejectedExperts, results[index].ID) {
topResults = append(topResults, results[index])
}
index++
}

return topResults, nil
}

func (m *MediatorAssignment) isExpertRejected(rejectedExperts []models.DisputeExpert, expertID uint) bool {
for _, rejectedExpert := range rejectedExperts {
if rejectedExpert.Expert == int64(expertID) {
return true
}
}
return false
}

func DefaultAlorithmAssignment(db *gorm.DB) *MediatorAssignment {
dbmodel := &DBModelReal{DB: db}

return &MediatorAssignment{
Components: []AlgorithmComponent{
&BaseComponent{
ScoreModeler: &LastAssignmentstruct{},
Function: &Linear{BaseFunction: BaseFunction{MoveYAxis: 0, MoveXAxis: 0, ApplyCapToValue: true, Cap: 10,}, Multiplier: 1},
Operator: &AddOperator{},
},
&BaseComponent{
ScoreModeler: &AssignedDisputes{},
Function: &Logarithmic{BaseFunction: BaseFunction{MoveYAxis: 0, MoveXAxis: 0, ApplyCapToValue: true, Cap: 10,}, LogBase: 10},
Operator: &AddOperator{},
},
&BaseComponent{
ScoreModeler: &RejectionCount{},
Function: &Expontential{BaseFunction: BaseFunction{MoveYAxis: 0, MoveXAxis: 0, ApplyCapToValue: true, Cap: 10,}, BaseExponent: 10},
Operator: &AddOperator{},
},
},
DB: dbmodel,
}
}

func (m *MediatorAssignment) AddComponent(component AlgorithmComponent) {
m.Components = append(m.Components, component)
}
Loading

0 comments on commit d77b644

Please sign in to comment.