Skip to content

Commit

Permalink
Merge pull request rapidpro#437 from nyaruka/closed_ticket_trigger
Browse files Browse the repository at this point in the history
🎟️ Ticket events
  • Loading branch information
rowanseymour authored Jun 14, 2021
2 parents 44bcc36 + 8a02cf9 commit d0723df
Show file tree
Hide file tree
Showing 24 changed files with 693 additions and 142 deletions.
4 changes: 4 additions & 0 deletions core/handlers/ticket_opened_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ func TestTicketOpened(t *testing.T) {
Args: []interface{}{testdata.Zendesk.ID},
Count: 0,
},
{ // and we have 2 ticket opened events for the 2 tickets opened
SQL: "select count(*) from tickets_ticketevent where event_type = 'O'",
Count: 2,
},
},
},
}
Expand Down
12 changes: 12 additions & 0 deletions core/hooks/insert_tickets.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,17 @@ func (h *insertTicketsHook) Apply(ctx context.Context, tx *sqlx.Tx, rp *redis.Po
return errors.Wrapf(err, "error inserting tickets")
}

// 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)
}

// and insert those too
err = models.InsertTicketEvents(ctx, tx, openEvents)
if err != nil {
return errors.Wrapf(err, "error inserting ticket opened events")
}

return nil
}
6 changes: 0 additions & 6 deletions core/models/orgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,13 @@ func init() {
// OrgID is our type for orgs ids
type OrgID int

// UserID is our type for user ids used by modified_by, which can be null
type UserID null.Int

// SessionStorageMode is our type for how we persist our sessions
type SessionStorageMode string

const (
// NilOrgID is the id 0 considered as nil org id
NilOrgID = OrgID(0)

// NilUserID si the id 0 considered as nil user id
NilUserID = UserID(0)

configSMTPServer = "smtp_server"
configDTOneKey = "dtone_key"
configDTOneSecret = "dtone_secret"
Expand Down
76 changes: 76 additions & 0 deletions core/models/ticket_events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package models

import (
"context"
"encoding/json"
"time"

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

type TicketEventID int
type TicketEventType string

const (
TicketEventTypeOpened TicketEventType = "O"
TicketEventTypeAssigned TicketEventType = "A"
TicketEventTypeNote TicketEventType = "N"
TicketEventTypeClosed TicketEventType = "C"
TicketEventTypeReopened TicketEventType = "R"
)

type TicketEvent struct {
e struct {
ID TicketEventID `json:"id" db:"id"`
OrgID OrgID `json:"org_id" db:"org_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 {
event := &TicketEvent{}
e := &event.e

e.OrgID = orgID
e.TicketID = ticketID
e.EventType = eventType
e.CreatedOn = dates.Now()
e.CreatedByID = userID
return event
}

func (e *TicketEvent) ID() TicketEventID { return e.e.ID }
func (e *TicketEvent) OrgID() OrgID { return e.e.OrgID }
func (e *TicketEvent) TicketID() TicketID { return e.e.TicketID }
func (e *TicketEvent) EventType() TicketEventType { return e.e.EventType }

// MarshalJSON is our custom marshaller so that our inner struct get output
func (e *TicketEvent) MarshalJSON() ([]byte, error) {
return json.Marshal(e.e)
}

// UnmarshalJSON is our custom marshaller so that our inner struct get output
func (e *TicketEvent) UnmarshalJSON(b []byte) error {
return json.Unmarshal(b, &e.e)
}

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)
RETURNING
id
`

func InsertTicketEvents(ctx context.Context, db Queryer, evts []*TicketEvent) error {
// convert to interface arrray
is := make([]interface{}, len(evts))
for i := range evts {
is[i] = &evts[i].e
}

return BulkQuery(ctx, "inserting ticket events", db, insertTicketEventsSQL, is)
}
119 changes: 71 additions & 48 deletions core/models/tickets.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,14 +166,12 @@ SELECT
FROM
tickets_ticket t
WHERE
t.org_id = $1 AND
t.id = ANY($2) AND
t.status = $3
t.id = ANY($1)
`

// LoadTickets loads all of the tickets with the given ids
func LoadTickets(ctx context.Context, db Queryer, orgID OrgID, ids []TicketID, status TicketStatus) ([]*Ticket, error) {
return loadTickets(ctx, db, selectTicketsByIDSQL, orgID, pq.Array(ids), status)
func LoadTickets(ctx context.Context, db Queryer, ids []TicketID) ([]*Ticket, error) {
return loadTickets(ctx, db, selectTicketsByIDSQL, pq.Array(ids))
}

func loadTickets(ctx context.Context, db Queryer, query string, params ...interface{}) ([]*Ticket, error) {
Expand Down Expand Up @@ -307,29 +305,14 @@ func UpdateTicketExternalID(ctx context.Context, db Queryer, ticket *Ticket, ext
return Exec(ctx, "update ticket external ID", db, updateTicketExternalIDSQL, t.ID, t.ExternalID)
}

const updateTicketAndKeepOpenSQL = `
UPDATE
tickets_ticket
SET
status = $2,
config = $3,
modified_on = $4,
closed_on = NULL
WHERE
id = $1
`

// UpdateAndKeepOpenTicket updates the passed in ticket to ensure it's open and updates the config with any passed in values
func UpdateAndKeepOpenTicket(ctx context.Context, db Queryer, ticket *Ticket, config map[string]string) error {
now := dates.Now()
// UpdateTicketConfig updates the passed in ticket's config with any passed in values
func UpdateTicketConfig(ctx context.Context, db Queryer, ticket *Ticket, config map[string]string) error {
t := &ticket.t
t.Status = TicketStatusOpen
t.ModifiedOn = now
for key, value := range config {
t.Config.Map()[key] = value
}

return Exec(ctx, "update ticket", db, updateTicketAndKeepOpenSQL, t.ID, t.Status, t.Config, t.ModifiedOn)
return Exec(ctx, "update ticket config", db, `UPDATE tickets_ticket SET config = $2 WHERE id = $1`, t.ID, t.Config)
}

const closeTicketSQL = `
Expand All @@ -344,37 +327,57 @@ WHERE
`

// CloseTickets closes the passed in tickets
func CloseTickets(ctx context.Context, db Queryer, org *OrgAssets, tickets []*Ticket, externally bool, logger *HTTPLogger) error {
func CloseTickets(ctx context.Context, db Queryer, oa *OrgAssets, userID UserID, tickets []*Ticket, externally bool, logger *HTTPLogger) (map[*Ticket]*TicketEvent, error) {
byTicketer := make(map[TicketerID][]*Ticket)
ids := make([]TicketID, len(tickets))
ids := make([]TicketID, 0, len(tickets))
events := make([]*TicketEvent, 0, len(tickets))
eventsByTicket := make(map[*Ticket]*TicketEvent, len(tickets))
now := dates.Now()
for i, ticket := range tickets {
byTicketer[ticket.TicketerID()] = append(byTicketer[ticket.TicketerID()], ticket)
ids[i] = ticket.ID()
t := &ticket.t
t.Status = TicketStatusClosed
t.ModifiedOn = now
t.ClosedOn = &now

for _, ticket := range tickets {
if ticket.Status() != TicketStatusClosed {
byTicketer[ticket.TicketerID()] = append(byTicketer[ticket.TicketerID()], ticket)
ids = append(ids, ticket.ID())
t := &ticket.t
t.Status = TicketStatusClosed
t.ModifiedOn = now
t.ClosedOn = &now

e := NewTicketEvent(ticket.OrgID(), userID, ticket.ID(), TicketEventTypeClosed)
events = append(events, e)
eventsByTicket[ticket] = e
}
}

if externally {
for ticketerID, ticketerTickets := range byTicketer {
ticketer := org.TicketerByID(ticketerID)
ticketer := oa.TicketerByID(ticketerID)
if ticketer != nil {
service, err := ticketer.AsService(flows.NewTicketer(ticketer))
if err != nil {
return err
return nil, err
}

err = service.Close(ticketerTickets, logger.Ticketer(ticketer))
if err != nil {
return err
return nil, err
}
}
}
}

return Exec(ctx, "close tickets", db, closeTicketSQL, pq.Array(ids), now)
// mark the tickets as closed in the db
err := Exec(ctx, "close tickets", db, closeTicketSQL, pq.Array(ids), 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
}

const reopenTicketSQL = `
Expand All @@ -389,17 +392,26 @@ WHERE
`

// ReopenTickets reopens the passed in tickets
func ReopenTickets(ctx context.Context, db Queryer, org *OrgAssets, tickets []*Ticket, externally bool, logger *HTTPLogger) error {
func ReopenTickets(ctx context.Context, db Queryer, org *OrgAssets, userID UserID, tickets []*Ticket, externally bool, logger *HTTPLogger) (map[*Ticket]*TicketEvent, error) {
byTicketer := make(map[TicketerID][]*Ticket)
ids := make([]TicketID, len(tickets))
ids := make([]TicketID, 0, len(tickets))
events := make([]*TicketEvent, 0, len(tickets))
eventsByTicket := make(map[*Ticket]*TicketEvent, len(tickets))
now := dates.Now()
for i, ticket := range tickets {
byTicketer[ticket.TicketerID()] = append(byTicketer[ticket.TicketerID()], ticket)
ids[i] = ticket.ID()
t := &ticket.t
t.Status = TicketStatusOpen
t.ModifiedOn = now
t.ClosedOn = nil

for _, ticket := range tickets {
if ticket.Status() != TicketStatusOpen {
byTicketer[ticket.TicketerID()] = append(byTicketer[ticket.TicketerID()], ticket)
ids = append(ids, ticket.ID())
t := &ticket.t
t.Status = TicketStatusOpen
t.ModifiedOn = now
t.ClosedOn = nil

e := NewTicketEvent(ticket.OrgID(), userID, ticket.ID(), TicketEventTypeReopened)
events = append(events, e)
eventsByTicket[ticket] = e
}
}

if externally {
Expand All @@ -408,18 +420,29 @@ func ReopenTickets(ctx context.Context, db Queryer, org *OrgAssets, tickets []*T
if ticketer != nil {
service, err := ticketer.AsService(flows.NewTicketer(ticketer))
if err != nil {
return err
return nil, err
}

err = service.Reopen(ticketerTickets, logger.Ticketer(ticketer))
if err != nil {
return err
return nil, err
}
}
}
}

return Exec(ctx, "reopen tickets", db, reopenTicketSQL, pq.Array(ids), now)
// mark the tickets as opened in the db
err := Exec(ctx, "reopen tickets", db, reopenTicketSQL, pq.Array(ids), 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
}

// Ticketer is our type for a ticketer asset
Expand Down
Loading

0 comments on commit d0723df

Please sign in to comment.