Skip to content

Commit

Permalink
Merge pull request #453 from nyaruka/assign_and_note
Browse files Browse the repository at this point in the history
🎰 Add endpoints for ticket assignment and adding notes
  • Loading branch information
rowanseymour authored Jul 5, 2021
2 parents 2cdb138 + 1cc80f1 commit 2664584
Show file tree
Hide file tree
Showing 18 changed files with 642 additions and 169 deletions.
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

0 comments on commit 2664584

Please sign in to comment.