Skip to content

Commit

Permalink
Merge pull request rapidpro#445 from nyaruka/ticket_activity
Browse files Browse the repository at this point in the history
🎟️ Ticket tweaks
  • Loading branch information
rowanseymour authored Jun 17, 2021
2 parents 2dc0cb5 + 439d8f4 commit fe04bc1
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 58 deletions.
2 changes: 1 addition & 1 deletion core/hooks/insert_tickets.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ 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.ID(), models.TicketEventTypeOpened)
openEvents[i] = models.NewTicketEvent(oa.OrgID(), models.NilUserID, t.ContactID(), t.ID(), models.TicketEventTypeOpened)
}

// and insert those too
Expand Down
9 changes: 6 additions & 3 deletions core/models/ticket_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,20 @@ type TicketEvent struct {
e struct {
ID TicketEventID `json:"id" db:"id"`
OrgID OrgID `json:"org_id" db:"org_id"`
ContactID ContactID `json:"contact_id" db:"contact_id"`
TicketID TicketID `json:"ticket_id" db:"ticket_id"`
EventType TicketEventType `json:"event_type" db:"event_type"`
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, ticketID TicketID, eventType TicketEventType) *TicketEvent {
func NewTicketEvent(orgID OrgID, userID UserID, contactID ContactID, ticketID TicketID, eventType TicketEventType) *TicketEvent {
event := &TicketEvent{}
e := &event.e

e.OrgID = orgID
e.ContactID = contactID
e.TicketID = ticketID
e.EventType = eventType
e.CreatedOn = dates.Now()
Expand All @@ -44,6 +46,7 @@ func NewTicketEvent(orgID OrgID, userID UserID, ticketID TicketID, eventType Tic

func (e *TicketEvent) ID() TicketEventID { return e.e.ID }
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 }

Expand All @@ -59,8 +62,8 @@ func (e *TicketEvent) UnmarshalJSON(b []byte) error {

const insertTicketEventsSQL = `
INSERT INTO
tickets_ticketevent(org_id, ticket_id, event_type, created_on, created_by_id)
VALUES(:org_id, :ticket_id, :event_type, :created_on, :created_by_id)
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)
RETURNING
id
`
Expand Down
88 changes: 45 additions & 43 deletions core/models/tickets.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,20 @@ func init() {

type Ticket struct {
t struct {
ID TicketID `db:"id"`
UUID flows.TicketUUID `db:"uuid"`
OrgID OrgID `db:"org_id"`
ContactID ContactID `db:"contact_id"`
TicketerID TicketerID `db:"ticketer_id"`
ExternalID null.String `db:"external_id"`
Status TicketStatus `db:"status"`
Subject string `db:"subject"`
Body string `db:"body"`
Config null.Map `db:"config"`
OpenedOn time.Time `db:"opened_on"`
ModifiedOn time.Time `db:"modified_on"`
ClosedOn *time.Time `db:"closed_on"`
ID TicketID `db:"id"`
UUID flows.TicketUUID `db:"uuid"`
OrgID OrgID `db:"org_id"`
ContactID ContactID `db:"contact_id"`
TicketerID TicketerID `db:"ticketer_id"`
ExternalID null.String `db:"external_id"`
Status TicketStatus `db:"status"`
Subject string `db:"subject"`
Body string `db:"body"`
Config null.Map `db:"config"`
OpenedOn time.Time `db:"opened_on"`
ModifiedOn time.Time `db:"modified_on"`
ClosedOn *time.Time `db:"closed_on"`
LastActivityOn time.Time `db:"last_activity_on"`
}
}

Expand Down Expand Up @@ -135,7 +136,8 @@ SELECT
t.config AS config,
t.opened_on AS opened_on,
t.modified_on AS modified_on,
t.closed_on AS closed_on
t.closed_on AS closed_on,
t.last_activity_on AS last_activity_on
FROM
tickets_ticket t
WHERE
Expand All @@ -162,7 +164,8 @@ SELECT
t.config AS config,
t.opened_on AS opened_on,
t.modified_on AS modified_on,
t.closed_on AS closed_on
t.closed_on AS closed_on,
t.last_activity_on AS last_activity_on
FROM
tickets_ticket t
WHERE
Expand Down Expand Up @@ -208,7 +211,8 @@ SELECT
config,
opened_on,
modified_on,
closed_on
closed_on,
last_activity_on
FROM
tickets_ticket
WHERE
Expand All @@ -234,7 +238,8 @@ SELECT
config,
opened_on,
modified_on,
closed_on
closed_on,
last_activity_on
FROM
tickets_ticket
WHERE
Expand Down Expand Up @@ -269,8 +274,8 @@ func lookupTicket(ctx context.Context, db Queryer, query string, params ...inter

const insertTicketSQL = `
INSERT INTO
tickets_ticket(uuid, org_id, contact_id, ticketer_id, external_id, status, subject, body, config, opened_on, modified_on)
VALUES( :uuid, :org_id, :contact_id, :ticketer_id, :external_id, :status, :subject, :body, :config, NOW(), NOW() )
tickets_ticket(uuid, org_id, contact_id, ticketer_id, external_id, status, subject, body, config, opened_on, modified_on, last_activity_on)
VALUES( :uuid, :org_id, :contact_id, :ticketer_id, :external_id, :status, :subject, :body, :config, NOW(), NOW() , NOW())
RETURNING
id
`
Expand All @@ -289,20 +294,11 @@ func InsertTickets(ctx context.Context, tx Queryer, tickets []*Ticket) error {
return BulkQuery(ctx, "inserted tickets", tx, insertTicketSQL, ts)
}

const updateTicketExternalIDSQL = `
UPDATE
tickets_ticket
SET
external_id = $2
WHERE
id = $1
`

// UpdateTicketExternalID updates the external ID of the given ticket
func UpdateTicketExternalID(ctx context.Context, db Queryer, ticket *Ticket, externalID string) error {
t := &ticket.t
t.ExternalID = null.String(externalID)
return Exec(ctx, "update ticket external ID", db, updateTicketExternalIDSQL, t.ID, t.ExternalID)
return Exec(ctx, "update ticket external ID", db, `UPDATE tickets_ticket SET external_id = $2 WHERE id = $1`, t.ID, t.ExternalID)
}

// UpdateTicketConfig updates the passed in ticket's config with any passed in values
Expand All @@ -315,13 +311,25 @@ func UpdateTicketConfig(ctx context.Context, db Queryer, ticket *Ticket, config
return Exec(ctx, "update ticket config", db, `UPDATE tickets_ticket SET config = $2 WHERE id = $1`, t.ID, t.Config)
}

// UpdateTicketLastActivity updates the last_activity_on of the given tickets to be now
func UpdateTicketLastActivity(ctx context.Context, db Queryer, tickets []*Ticket) error {
now := dates.Now()
ids := make([]TicketID, len(tickets))
for i, t := range tickets {
t.t.LastActivityOn = now
ids[i] = t.ID()
}
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 closeTicketSQL = `
UPDATE
tickets_ticket
SET
status = 'C',
modified_on = $2,
closed_on = $2
closed_on = $2,
last_activity_on = $2
WHERE
id = ANY($1)
`
Expand All @@ -342,8 +350,9 @@ func CloseTickets(ctx context.Context, db Queryer, oa *OrgAssets, userID UserID,
t.Status = TicketStatusClosed
t.ModifiedOn = now
t.ClosedOn = &now
t.LastActivityOn = now

e := NewTicketEvent(ticket.OrgID(), userID, ticket.ID(), TicketEventTypeClosed)
e := NewTicketEvent(ticket.OrgID(), userID, ticket.ContactID(), ticket.ID(), TicketEventTypeClosed)
events = append(events, e)
eventsByTicket[ticket] = e
}
Expand Down Expand Up @@ -386,7 +395,8 @@ UPDATE
SET
status = 'O',
modified_on = $2,
closed_on = NULL
closed_on = NULL,
last_activity_on = $2
WHERE
id = ANY($1)
`
Expand All @@ -407,8 +417,9 @@ func ReopenTickets(ctx context.Context, db Queryer, org *OrgAssets, userID UserI
t.Status = TicketStatusOpen
t.ModifiedOn = now
t.ClosedOn = nil
t.LastActivityOn = now

e := NewTicketEvent(ticket.OrgID(), userID, ticket.ID(), TicketEventTypeReopened)
e := NewTicketEvent(ticket.OrgID(), userID, ticket.ContactID(), ticket.ID(), TicketEventTypeReopened)
events = append(events, e)
eventsByTicket[ticket] = e
}
Expand Down Expand Up @@ -492,15 +503,6 @@ func (t *Ticketer) AsService(ticketer *flows.Ticketer) (TicketService, error) {
return nil, errors.Errorf("unrecognized ticket service type '%s'", t.Type())
}

const updateTicketerConfigSQL = `
UPDATE
tickets_ticketer
SET
config = $2
WHERE
id = $1
`

// UpdateConfig updates the configuration of this ticketer with the given values
func (t *Ticketer) UpdateConfig(ctx context.Context, db Queryer, add map[string]string, remove map[string]bool) error {
for key, value := range add {
Expand All @@ -516,7 +518,7 @@ func (t *Ticketer) UpdateConfig(ctx context.Context, db Queryer, add map[string]
dbMap[key] = value
}

return Exec(ctx, "update ticketer config", db, updateTicketerConfigSQL, t.t.ID, null.NewMap(dbMap))
return Exec(ctx, "update ticketer config", db, `UPDATE tickets_ticketer SET config = $2 WHERE id = $1`, t.t.ID, null.NewMap(dbMap))
}

// TicketService extends the engine's ticket service and adds support for forwarding new incoming messages
Expand Down
14 changes: 13 additions & 1 deletion core/tasks/handler/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ func TestMsgEvents(t *testing.T) {
testdata.InsertKeywordTrigger(t, db, testdata.Org2, testdata.Org2Favorites, "start", models.MatchOnly, nil, nil)
testdata.InsertCatchallTrigger(t, db, testdata.Org2, testdata.Org2SingleMessage, nil, nil)

// give Cathy an open ticket
cathyTicketID := testdata.InsertOpenTicket(t, db, testdata.Org1, testdata.Cathy, testdata.Mailgun, "aa906c81-7766-427c-9ffd-9a86e49bd657", "Hi there", "Ok", "")

// give Bob a closed ticket
bobTicketID := testdata.InsertClosedTicket(t, db, testdata.Org1, testdata.Bob, testdata.Mailgun, "46faf0f3-5558-4865-bd8e-b3c83cdb5770", "Hi there", "Ok", "")

db.MustExec(`UPDATE tickets_ticket SET last_activity_on = '2021-01-01T00:00:00Z' WHERE id IN ($1, $2)`, cathyTicketID, bobTicketID)

// clear all of Alexandria's URNs
db.MustExec(`UPDATE contacts_contacturn SET contact_id = NULL WHERE contact_id = $1`, testdata.Alexandria.ID)

Expand Down Expand Up @@ -149,6 +157,10 @@ func TestMsgEvents(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, 9, count)

// Cathy's open ticket will have been updated but not Bob's closed ticket
testsuite.AssertQueryCount(t, db, `SELECT count(*) FROM tickets_ticket WHERE id = $1 AND last_activity_on > '2021-01-01T00:00:00Z'`, []interface{}{cathyTicketID}, 1)
testsuite.AssertQueryCount(t, db, `SELECT count(*) FROM tickets_ticket WHERE id = $1 AND last_activity_on = '2021-01-01T00:00:00Z'`, []interface{}{bobTicketID}, 1)

// Fred's sessions should not have a timeout because courier will set them
testsuite.AssertQueryCount(t, db,
`SELECT count(*) from flows_flowsession where contact_id = $1 and timeout_on IS NULL AND wait_started_on IS NOT NULL`,
Expand Down Expand Up @@ -300,7 +312,7 @@ func TestTicketEvents(t *testing.T) {
tickets, err := models.LoadTickets(ctx, rt.DB, []models.TicketID{ticketID})
require.NoError(t, err)

event := models.NewTicketEvent(testdata.Org1.ID, testdata.Admin.ID, tickets[0].ID(), models.TicketEventTypeClosed)
event := models.NewTicketEvent(testdata.Org1.ID, testdata.Admin.ID, tickets[0].ContactID(), tickets[0].ID(), models.TicketEventTypeClosed)

err = handler.QueueTicketEvent(rc, testdata.Cathy.ID, event)
require.NoError(t, err)
Expand Down
26 changes: 23 additions & 3 deletions core/tasks/handler/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,14 @@ func handleMsgEvent(ctx context.Context, rt *runtime.Runtime, event *MsgEvent) e
if err != nil {
return errors.Wrapf(err, "error marking message as handled")
}

if len(tickets) > 0 {
err = models.UpdateTicketLastActivity(ctx, tx, tickets)
if err != nil {
return errors.Wrapf(err, "error updating last activity for open tickets")
}
}

return nil
}

Expand All @@ -613,9 +621,21 @@ func handleMsgEvent(ctx context.Context, rt *runtime.Runtime, event *MsgEvent) e
if flow != nil {
// if this is an IVR flow, we need to trigger that start (which happens in a different queue)
if flow.FlowType() == models.FlowTypeVoice {
err = runner.TriggerIVRFlow(ctx, rt, oa.OrgID(), flow.ID(), []models.ContactID{modelContact.ID()}, func(ctx context.Context, tx *sqlx.Tx) error {
return models.UpdateMessage(ctx, tx, event.MsgID, models.MsgStatusHandled, models.VisibilityVisible, models.TypeFlow, topupID)
})
ivrHook := func(ctx context.Context, tx *sqlx.Tx) error {
err := models.UpdateMessage(ctx, tx, event.MsgID, models.MsgStatusHandled, models.VisibilityVisible, models.TypeFlow, topupID)
if err != nil {
return errors.Wrapf(err, "error marking message as handled")
}

if len(tickets) > 0 {
err = models.UpdateTicketLastActivity(ctx, tx, tickets)
if err != nil {
return errors.Wrapf(err, "error updating last activity for open tickets")
}
}
return nil
}
err = runner.TriggerIVRFlow(ctx, rt, oa.OrgID(), flow.ID(), []models.ContactID{modelContact.ID()}, ivrHook)
if err != nil {
return errors.Wrapf(err, "error while triggering ivr flow")
}
Expand Down
Binary file modified mailroom_test.dump
Binary file not shown.
2 changes: 1 addition & 1 deletion services/tickets/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,5 +203,5 @@ func TestCloseTicket(t *testing.T) {
err = tickets.CloseTicket(ctx, rt, oa, ticket1, true, logger)
require.NoError(t, err)

testsuite.AssertContactTasks(t, 1, testdata.Cathy.ID, []string{`{"type":"ticket_closed","org_id":1,"task":{"id":1,"org_id":1,"ticket_id":1,"event_type":"C","created_on":"2021-06-08T16:40:31Z"},"queued_on":"2021-06-08T16:40:34Z"}`})
testsuite.AssertContactTasks(t, 1, testdata.Cathy.ID, []string{`{"type":"ticket_closed","org_id":1,"task":{"id":1,"org_id":1,"contact_id":10000,"ticket_id":1,"event_type":"C","created_on":"2021-06-08T16:40:31Z"},"queued_on":"2021-06-08T16:40:34Z"}`})
}
4 changes: 2 additions & 2 deletions testsuite/testdata/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ var IncomingExtraFlow = &Flow{10006, "376d3de6-7f0e-408c-80d6-b1919738bc80"}
var ParentTimeoutFlow = &Flow{10007, "81c0f323-7e06-4e0c-a960-19c20f17117c"}
var CampaignFlow = &Flow{10009, "3a92a964-3a8d-420b-9206-2cd9d884ac30"}

var CreatedOnField = &Field{3, "099bccaa-7853-4033-a066-c61c0ff32a8e"}
var LastSeenOnField = &Field{5, "f72976e2-0fb8-491c-a4a0-ef344b2f72e0"}
var CreatedOnField = &Field{3, "5291744c-61dd-4546-8cb4-eb2cd97f7ad7"}
var LastSeenOnField = &Field{5, "d9d430ab-a9d5-4ecf-82d9-8630113a5de0"}
var GenderField = &Field{6, "3a5891e4-756e-4dc9-8e12-b7a766168824"}
var AgeField = &Field{7, "903f51da-2717-47c7-a0d3-f2f32877013d"}
var JoinedField = &Field{8, "d83aae24-4bbf-49d0-ab85-6bfd201eac6d"}
Expand Down
8 changes: 4 additions & 4 deletions testsuite/testdata/tickets.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ type Ticketer struct {
func InsertOpenTicket(t *testing.T, db *sqlx.DB, org *Org, contact *Contact, ticketer *Ticketer, uuid flows.TicketUUID, subject, body, externalID string) models.TicketID {
var id models.TicketID
err := db.Get(&id,
`INSERT INTO tickets_ticket(uuid, org_id, contact_id, ticketer_id, status, subject, body, external_id, opened_on, modified_on)
VALUES($1, $2, $3, $4, 'O', $5, $6, $7, NOW(), NOW()) RETURNING id`, uuid, org.ID, contact.ID, ticketer.ID, subject, body, externalID,
`INSERT INTO tickets_ticket(uuid, org_id, contact_id, ticketer_id, status, subject, body, external_id, opened_on, modified_on, last_activity_on)
VALUES($1, $2, $3, $4, 'O', $5, $6, $7, NOW(), NOW(), NOW()) RETURNING id`, uuid, org.ID, contact.ID, ticketer.ID, subject, body, externalID,
)
require.NoError(t, err)
return id
Expand All @@ -31,8 +31,8 @@ func InsertOpenTicket(t *testing.T, db *sqlx.DB, org *Org, contact *Contact, tic
func InsertClosedTicket(t *testing.T, db *sqlx.DB, org *Org, contact *Contact, ticketer *Ticketer, uuid flows.TicketUUID, subject, body, externalID string) models.TicketID {
var id models.TicketID
err := db.Get(&id,
`INSERT INTO tickets_ticket(uuid, org_id, contact_id, ticketer_id, status, subject, body, external_id, opened_on, modified_on, closed_on)
VALUES($1, $2, $3, $4, 'C', $5, $6, $7, NOW(), NOW(), NOW()) RETURNING id`, uuid, org.ID, contact.ID, ticketer.ID, subject, body, externalID,
`INSERT INTO tickets_ticket(uuid, org_id, contact_id, ticketer_id, status, subject, body, external_id, opened_on, modified_on, closed_on, last_activity_on)
VALUES($1, $2, $3, $4, 'C', $5, $6, $7, NOW(), NOW(), NOW(), NOW()) RETURNING id`, uuid, org.ID, contact.ID, ticketer.ID, subject, body, externalID,
)
require.NoError(t, err)
return id
Expand Down

0 comments on commit fe04bc1

Please sign in to comment.