Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🎰 Add endpoints for ticket assignment and adding notes #453

Merged
merged 1 commit into from
Jul 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions core/hooks/insert_tickets.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ func (h *insertTicketsHook) Apply(ctx context.Context, tx *sqlx.Tx, rp *redis.Po

// generate opened events for each ticket
openEvents := make([]*models.TicketEvent, len(tickets))
for i, t := range tickets {
openEvents[i] = models.NewTicketEvent(oa.OrgID(), models.NilUserID, t.ContactID(), t.ID(), models.TicketEventTypeOpened)
for i, ticket := range tickets {
openEvents[i] = models.NewTicketOpenedEvent(ticket, models.NilUserID, models.NilUserID)
}

// and insert those too
Expand Down
41 changes: 34 additions & 7 deletions core/models/ticket_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"github.com/nyaruka/gocommon/dates"
"github.com/nyaruka/null"
)

type TicketEventID int
Expand All @@ -26,19 +27,42 @@ type TicketEvent struct {
ContactID ContactID `json:"contact_id" db:"contact_id"`
TicketID TicketID `json:"ticket_id" db:"ticket_id"`
EventType TicketEventType `json:"event_type" db:"event_type"`
Note null.String `json:"note,omitempty" db:"note"`
AssigneeID UserID `json:"assignee_id,omitempty" db:"assignee_id"`
CreatedByID UserID `json:"created_by_id,omitempty" db:"created_by_id"`
CreatedOn time.Time `json:"created_on" db:"created_on"`
}
}

func NewTicketEvent(orgID OrgID, userID UserID, contactID ContactID, ticketID TicketID, eventType TicketEventType) *TicketEvent {
func NewTicketOpenedEvent(t *Ticket, userID UserID, assigneeID UserID) *TicketEvent {
return newTicketEvent(t, userID, TicketEventTypeOpened, "", assigneeID)
}

func NewTicketAssignedEvent(t *Ticket, userID UserID, assigneeID UserID, note string) *TicketEvent {
return newTicketEvent(t, userID, TicketEventTypeAssigned, note, assigneeID)
}

func NewTicketNoteEvent(t *Ticket, userID UserID, note string) *TicketEvent {
return newTicketEvent(t, userID, TicketEventTypeNote, note, NilUserID)
}

func NewTicketClosedEvent(t *Ticket, userID UserID) *TicketEvent {
return newTicketEvent(t, userID, TicketEventTypeClosed, "", NilUserID)
}

func NewTicketReopenedEvent(t *Ticket, userID UserID) *TicketEvent {
return newTicketEvent(t, userID, TicketEventTypeReopened, "", NilUserID)
}

func newTicketEvent(t *Ticket, userID UserID, eventType TicketEventType, note string, assigneeID UserID) *TicketEvent {
event := &TicketEvent{}
e := &event.e

e.OrgID = orgID
e.ContactID = contactID
e.TicketID = ticketID
e.OrgID = t.OrgID()
e.ContactID = t.ContactID()
e.TicketID = t.ID()
e.EventType = eventType
e.Note = null.String(note)
e.AssigneeID = assigneeID
e.CreatedOn = dates.Now()
e.CreatedByID = userID
return event
Expand All @@ -49,6 +73,9 @@ func (e *TicketEvent) OrgID() OrgID { return e.e.OrgID }
func (e *TicketEvent) ContactID() ContactID { return e.e.ContactID }
func (e *TicketEvent) TicketID() TicketID { return e.e.TicketID }
func (e *TicketEvent) EventType() TicketEventType { return e.e.EventType }
func (e *TicketEvent) Note() null.String { return e.e.Note }
func (e *TicketEvent) AssigneeID() UserID { return e.e.AssigneeID }
func (e *TicketEvent) CreatedByID() UserID { return e.e.CreatedByID }

// MarshalJSON is our custom marshaller so that our inner struct get output
func (e *TicketEvent) MarshalJSON() ([]byte, error) {
Expand All @@ -62,8 +89,8 @@ func (e *TicketEvent) UnmarshalJSON(b []byte) error {

const insertTicketEventsSQL = `
INSERT INTO
tickets_ticketevent(org_id, contact_id, ticket_id, event_type, created_on, created_by_id)
VALUES(:org_id, :contact_id, :ticket_id, :event_type, :created_on, :created_by_id)
tickets_ticketevent(org_id, contact_id, ticket_id, event_type, note, assignee_id, created_on, created_by_id)
VALUES(:org_id, :contact_id, :ticket_id, :event_type, :note, :assignee_id, :created_on, :created_by_id)
RETURNING
id
`
Expand Down
58 changes: 58 additions & 0 deletions core/models/ticket_events_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package models_test

import (
"testing"

"github.com/nyaruka/mailroom/core/models"
"github.com/nyaruka/mailroom/testsuite"
"github.com/nyaruka/mailroom/testsuite/testdata"
"github.com/nyaruka/null"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestTicketEvents(t *testing.T) {
ctx, _, db, _ := testsuite.Get()

defer func() {
db.MustExec(`DELETE FROM tickets_ticketevent`)
db.MustExec(`DELETE FROM tickets_ticket`)
}()

ticket := testdata.InsertOpenTicket(db, testdata.Org1, testdata.Cathy, testdata.Mailgun, "Need help", "Have you seen my cookies?", "17", nil)
modelTicket := ticket.Load(db)

e1 := models.NewTicketOpenedEvent(modelTicket, testdata.Admin.ID, testdata.Agent.ID)
assert.Equal(t, testdata.Org1.ID, e1.OrgID())
assert.Equal(t, testdata.Cathy.ID, e1.ContactID())
assert.Equal(t, ticket.ID, e1.TicketID())
assert.Equal(t, models.TicketEventTypeOpened, e1.EventType())
assert.Equal(t, null.NullString, e1.Note())
assert.Equal(t, testdata.Admin.ID, e1.CreatedByID())

e2 := models.NewTicketAssignedEvent(modelTicket, testdata.Admin.ID, testdata.Agent.ID, "please handle")
assert.Equal(t, models.TicketEventTypeAssigned, e2.EventType())
assert.Equal(t, testdata.Agent.ID, e2.AssigneeID())
assert.Equal(t, null.String("please handle"), e2.Note())
assert.Equal(t, testdata.Admin.ID, e2.CreatedByID())

e3 := models.NewTicketNoteEvent(modelTicket, testdata.Agent.ID, "please handle")
assert.Equal(t, models.TicketEventTypeNote, e3.EventType())
assert.Equal(t, null.String("please handle"), e3.Note())
assert.Equal(t, testdata.Agent.ID, e3.CreatedByID())

e4 := models.NewTicketClosedEvent(modelTicket, testdata.Agent.ID)
assert.Equal(t, models.TicketEventTypeClosed, e4.EventType())
assert.Equal(t, testdata.Agent.ID, e4.CreatedByID())

e5 := models.NewTicketReopenedEvent(modelTicket, testdata.Editor.ID)
assert.Equal(t, models.TicketEventTypeReopened, e5.EventType())
assert.Equal(t, testdata.Editor.ID, e5.CreatedByID())

err := models.InsertTicketEvents(ctx, db, []*models.TicketEvent{e1, e2, e3, e4, e5})
require.NoError(t, err)

testsuite.AssertQuery(t, db, `SELECT count(*) FROM tickets_ticketevent`).Returns(5)
testsuite.AssertQuery(t, db, `SELECT assignee_id, note FROM tickets_ticketevent WHERE id = $1`, e2.ID()).
Columns(map[string]interface{}{"assignee_id": int64(testdata.Agent.ID), "note": "please handle"})
}
144 changes: 109 additions & 35 deletions core/models/tickets.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ SELECT
t.status AS status,
t.subject AS subject,
t.body AS body,
t.assignee_id AS assignee_id,
t.config AS config,
t.opened_on AS opened_on,
t.modified_on AS modified_on,
Expand Down Expand Up @@ -199,6 +200,7 @@ SELECT
t.status AS status,
t.subject AS subject,
t.body AS body,
t.assignee_id AS assignee_id,
t.config AS config,
t.opened_on AS opened_on,
t.modified_on AS modified_on,
Expand Down Expand Up @@ -237,24 +239,25 @@ func loadTickets(ctx context.Context, db Queryer, query string, params ...interf

const selectTicketByUUIDSQL = `
SELECT
id,
uuid,
org_id,
contact_id,
ticketer_id,
external_id,
status,
subject,
body,
config,
opened_on,
modified_on,
closed_on,
last_activity_on
t.id AS id,
t.uuid AS uuid,
t.org_id AS org_id,
t.contact_id AS contact_id,
t.ticketer_id AS ticketer_id,
t.external_id AS external_id,
t.status AS status,
t.subject AS subject,
t.body AS body,
t.assignee_id AS assignee_id,
t.config AS config,
t.opened_on AS opened_on,
t.modified_on AS modified_on,
t.closed_on AS closed_on,
t.last_activity_on AS last_activity_on
FROM
tickets_ticket
tickets_ticket t
WHERE
uuid = $1
t.uuid = $1
`

// LookupTicketByUUID looks up the ticket with the passed in UUID
Expand All @@ -264,25 +267,26 @@ func LookupTicketByUUID(ctx context.Context, db *sqlx.DB, uuid flows.TicketUUID)

const selectTicketByExternalIDSQL = `
SELECT
id,
uuid,
org_id,
contact_id,
ticketer_id,
external_id,
status,
subject,
body,
config,
opened_on,
modified_on,
closed_on,
last_activity_on
t.id AS id,
t.uuid AS uuid,
t.org_id AS org_id,
t.contact_id AS contact_id,
t.ticketer_id AS ticketer_id,
t.external_id AS external_id,
t.status AS status,
t.subject AS subject,
t.body AS body,
t.assignee_id AS assignee_id,
t.config AS config,
t.opened_on AS opened_on,
t.modified_on AS modified_on,
t.closed_on AS closed_on,
t.last_activity_on AS last_activity_on
FROM
tickets_ticket
tickets_ticket t
WHERE
ticketer_id = $1 AND
external_id = $2
t.ticketer_id = $1 AND
t.external_id = $2
`

// LookupTicketByExternalID looks up the ticket with the passed in ticketer and external ID
Expand Down Expand Up @@ -364,6 +368,76 @@ func updateTicketLastActivity(ctx context.Context, db Queryer, ids []TicketID, n
return Exec(ctx, "update ticket last activity", db, `UPDATE tickets_ticket SET last_activity_on = $2 WHERE id = ANY($1)`, pq.Array(ids), now)
}

const assignTicketSQL = `
UPDATE
tickets_ticket
SET
assignee_id = $2,
modified_on = $3,
last_activity_on = $3
WHERE
id = ANY($1)
`

// AssignTickets assigns the passed in tickets
func AssignTickets(ctx context.Context, db Queryer, oa *OrgAssets, userID UserID, tickets []*Ticket, assigneeID UserID, note string) (map[*Ticket]*TicketEvent, error) {
ids := make([]TicketID, 0, len(tickets))
events := make([]*TicketEvent, 0, len(tickets))
eventsByTicket := make(map[*Ticket]*TicketEvent, len(tickets))
now := dates.Now()

for _, ticket := range tickets {
if ticket.AssigneeID() != assigneeID {
ids = append(ids, ticket.ID())
t := &ticket.t
t.AssigneeID = assigneeID
t.ModifiedOn = now
t.LastActivityOn = now

e := NewTicketAssignedEvent(ticket, userID, assigneeID, note)
events = append(events, e)
eventsByTicket[ticket] = e
}
}

// mark the tickets as assigned in the db
err := Exec(ctx, "assign tickets", db, assignTicketSQL, pq.Array(ids), assigneeID, now)
if err != nil {
return nil, errors.Wrapf(err, "error updating tickets")
}

err = InsertTicketEvents(ctx, db, events)
if err != nil {
return nil, errors.Wrapf(err, "error inserting ticket events")
}

return eventsByTicket, nil
}

// NoteTickets adds a note to the passed in tickets
func NoteTickets(ctx context.Context, db Queryer, oa *OrgAssets, userID UserID, tickets []*Ticket, note string) (map[*Ticket]*TicketEvent, error) {
events := make([]*TicketEvent, 0, len(tickets))
eventsByTicket := make(map[*Ticket]*TicketEvent, len(tickets))

for _, ticket := range tickets {
e := NewTicketNoteEvent(ticket, userID, note)
events = append(events, e)
eventsByTicket[ticket] = e
}

err := UpdateTicketLastActivity(ctx, db, tickets)
if err != nil {
return nil, errors.Wrapf(err, "error updating ticket activity")
}

err = InsertTicketEvents(ctx, db, events)
if err != nil {
return nil, errors.Wrapf(err, "error inserting ticket events")
}

return eventsByTicket, nil
}

const closeTicketSQL = `
UPDATE
tickets_ticket
Expand Down Expand Up @@ -394,7 +468,7 @@ func CloseTickets(ctx context.Context, db Queryer, oa *OrgAssets, userID UserID,
t.ClosedOn = &now
t.LastActivityOn = now

e := NewTicketEvent(ticket.OrgID(), userID, ticket.ContactID(), ticket.ID(), TicketEventTypeClosed)
e := NewTicketClosedEvent(ticket, userID)
events = append(events, e)
eventsByTicket[ticket] = e
}
Expand Down Expand Up @@ -461,7 +535,7 @@ func ReopenTickets(ctx context.Context, db Queryer, org *OrgAssets, userID UserI
t.ClosedOn = nil
t.LastActivityOn = now

e := NewTicketEvent(ticket.OrgID(), userID, ticket.ContactID(), ticket.ID(), TicketEventTypeReopened)
e := NewTicketReopenedEvent(ticket, userID)
events = append(events, e)
eventsByTicket[ticket] = e
}
Expand Down
Loading