diff --git a/core/ivr/twiml/twiml.go b/core/ivr/twiml/twiml.go
index dd67d3e34..3bdc2c707 100644
--- a/core/ivr/twiml/twiml.go
+++ b/core/ivr/twiml/twiml.go
@@ -297,28 +297,28 @@ func (c *client) StatusForRequest(r *http.Request) (models.ConnectionStatus, mod
case "machine_start", "fax":
return models.ConnectionStatusErrored, models.ConnectionErrorMachine, 0
}
- return models.ConnectionStatusInProgress, models.ConnectionNoError, 0
+ return models.ConnectionStatusInProgress, "", 0
}
status := r.Form.Get("CallStatus")
switch status {
case "queued", "ringing":
- return models.ConnectionStatusWired, models.ConnectionNoError, 0
+ return models.ConnectionStatusWired, "", 0
case "in-progress", "initiated":
- return models.ConnectionStatusInProgress, models.ConnectionNoError, 0
+ return models.ConnectionStatusInProgress, "", 0
case "completed":
duration, _ := strconv.Atoi(r.Form.Get("CallDuration"))
- return models.ConnectionStatusCompleted, models.ConnectionNoError, duration
+ return models.ConnectionStatusCompleted, "", duration
case "busy":
return models.ConnectionStatusErrored, models.ConnectionErrorBusy, 0
case "no-answer":
return models.ConnectionStatusErrored, models.ConnectionErrorNoAnswer, 0
case "canceled", "failed":
- return models.ConnectionStatusErrored, models.ConnectionNoError, 0
+ return models.ConnectionStatusErrored, "", 0
default:
logrus.WithField("call_status", status).Error("unknown call status in status callback")
diff --git a/core/ivr/vonage/vonage.go b/core/ivr/vonage/vonage.go
index a00f78f81..a05b628dd 100644
--- a/core/ivr/vonage/vonage.go
+++ b/core/ivr/vonage/vonage.go
@@ -546,37 +546,38 @@ type StatusRequest struct {
func (c *client) StatusForRequest(r *http.Request) (models.ConnectionStatus, models.ConnectionError, int) {
// this is a resume, call is in progress, no need to look at the body
if r.Form.Get("action") == "resume" {
- return models.ConnectionStatusInProgress, models.ConnectionNoError, 0
+ return models.ConnectionStatusInProgress, "", 0
}
- status := &StatusRequest{}
bb, err := readBody(r)
if err != nil {
logrus.WithError(err).Error("error reading status request body")
- return models.ConnectionStatusErrored, models.ConnectionNoError, 0
+ return models.ConnectionStatusErrored, models.ConnectionErrorProvider, 0
}
+
+ status := &StatusRequest{}
err = json.Unmarshal(bb, status)
if err != nil {
logrus.WithError(err).WithField("body", string(bb)).Error("error unmarshalling ncco status")
- return models.ConnectionStatusErrored, models.ConnectionNoError, 0
+ return models.ConnectionStatusErrored, models.ConnectionErrorProvider, 0
}
// transfer status callbacks have no status, safe to ignore them
if status.Status == "" {
- return models.ConnectionStatusInProgress, models.ConnectionNoError, 0
+ return models.ConnectionStatusInProgress, "", 0
}
switch status.Status {
case "started", "ringing":
- return models.ConnectionStatusWired, models.ConnectionNoError, 0
+ return models.ConnectionStatusWired, "", 0
case "answered":
- return models.ConnectionStatusInProgress, models.ConnectionNoError, 0
+ return models.ConnectionStatusInProgress, "", 0
case "completed":
duration, _ := strconv.Atoi(status.Duration)
- return models.ConnectionStatusCompleted, models.ConnectionNoError, duration
+ return models.ConnectionStatusCompleted, "", duration
case "busy":
return models.ConnectionStatusErrored, models.ConnectionErrorBusy, 0
diff --git a/core/models/channel_connection.go b/core/models/channel_connection.go
index 0a7c50138..b6953c7dc 100644
--- a/core/models/channel_connection.go
+++ b/core/models/channel_connection.go
@@ -54,7 +54,6 @@ const (
ConnectionErrorBusy = ConnectionError("B")
ConnectionErrorNoAnswer = ConnectionError("N")
ConnectionErrorMachine = ConnectionError("M")
- ConnectionNoError = ConnectionError("")
ConnectionMaxRetries = 3
@@ -387,8 +386,8 @@ func (c *ChannelConnection) MarkErrored(ctx context.Context, db Queryer, now tim
}
_, err := db.ExecContext(ctx,
- `UPDATE channels_channelconnection SET status = $2, ended_on = $3, retry_count = $4, error_count = $5, next_attempt = $6, modified_on = NOW() WHERE id = $1`,
- c.c.ID, c.c.Status, c.c.EndedOn, c.c.RetryCount, c.c.ErrorCount, c.c.NextAttempt,
+ `UPDATE channels_channelconnection SET status = $2, ended_on = $3, retry_count = $4, error_reason = $5, error_count = $6, next_attempt = $7, modified_on = NOW() WHERE id = $1`,
+ c.c.ID, c.c.Status, c.c.EndedOn, c.c.RetryCount, c.c.ErrorReason, c.c.ErrorCount, c.c.NextAttempt,
)
if err != nil {
diff --git a/web/ivr/ivr_test.go b/web/ivr/ivr_test.go
index ba7fa3d8c..1ebc8c8d4 100644
--- a/web/ivr/ivr_test.go
+++ b/web/ivr/ivr_test.go
@@ -11,6 +11,7 @@ import (
"sync"
"testing"
+ "github.com/nyaruka/gocommon/jsonx"
"github.com/nyaruka/goflow/test"
"github.com/nyaruka/mailroom/config"
"github.com/nyaruka/mailroom/core/models"
@@ -30,6 +31,30 @@ import (
ivr_tasks "github.com/nyaruka/mailroom/core/tasks/ivr"
)
+// mocks the Twilio API
+func mockTwilioHandler(w http.ResponseWriter, r *http.Request) {
+ r.ParseForm()
+ logrus.WithField("method", r.Method).WithField("url", r.URL.String()).WithField("form", r.Form).Info("test server called")
+ if strings.HasSuffix(r.URL.String(), "Calls.json") {
+ to := r.Form.Get("To")
+ if to == "+16055741111" {
+ w.WriteHeader(http.StatusCreated)
+ w.Write([]byte(`{"sid": "Call1"}`))
+ } else if to == "+16055742222" {
+ w.WriteHeader(http.StatusCreated)
+ w.Write([]byte(`{"sid": "Call2"}`))
+ } else if to == "+16055743333" {
+ w.WriteHeader(http.StatusCreated)
+ w.Write([]byte(`{"sid": "Call3"}`))
+ } else {
+ w.WriteHeader(http.StatusNotFound)
+ }
+ }
+ if strings.HasSuffix(r.URL.String(), "recording.mp3") {
+ w.WriteHeader(http.StatusOK)
+ }
+}
+
func TestTwilioIVR(t *testing.T) {
ctx, _, db, rp := testsuite.Get()
rc := rp.Get()
@@ -41,25 +66,7 @@ func TestTwilioIVR(t *testing.T) {
}()
// start test server
- ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- r.ParseForm()
- logrus.WithField("method", r.Method).WithField("url", r.URL.String()).WithField("form", r.Form).Info("test server called")
- if strings.HasSuffix(r.URL.String(), "Calls.json") {
- to := r.Form.Get("To")
- if to == "+16055741111" {
- w.WriteHeader(http.StatusCreated)
- w.Write([]byte(`{"sid": "Call1"}`))
- } else if to == "+16055743333" {
- w.WriteHeader(http.StatusCreated)
- w.Write([]byte(`{"sid": "Call2"}`))
- } else {
- w.WriteHeader(http.StatusNotFound)
- }
- }
- if strings.HasSuffix(r.URL.String(), "recording.mp3") {
- w.WriteHeader(http.StatusOK)
- }
- }))
+ ts := httptest.NewServer(http.HandlerFunc(mockTwilioHandler))
defer ts.Close()
twiml.BaseURL = ts.URL
@@ -73,37 +80,51 @@ func TestTwilioIVR(t *testing.T) {
// add auth tokens
db.MustExec(`UPDATE channels_channel SET config = '{"auth_token": "token", "account_sid": "sid", "callback_domain": "localhost:8090"}' WHERE id = $1`, testdata.TwilioChannel.ID)
- // create a flow start for cathy and george
- parentSummary := json.RawMessage(`{"flow": {"name": "IVR Flow", "uuid": "2f81d0ea-4d75-4843-9371-3f7465311cce"}, "uuid": "8bc73097-ac57-47fb-82e5-184f8ec6dbef", "status": "active", "contact": {"id": 10000, "name": "Cathy", "urns": ["tel:+16055741111?id=10000&priority=50"], "uuid": "6393abc0-283d-4c9b-a1b3-641a035c34bf", "fields": {"gender": {"text": "F"}}, "groups": [{"name": "Doctors", "uuid": "c153e265-f7c9-4539-9dbc-9b358714b638"}], "timezone": "America/Los_Angeles", "created_on": "2019-07-23T09:35:01.439614-07:00"}, "results": {}}`)
-
+ // create a flow start for cathy bob, and george
+ parentSummary := json.RawMessage(`{
+ "flow": {"name": "IVR Flow", "uuid": "2f81d0ea-4d75-4843-9371-3f7465311cce"},
+ "uuid": "8bc73097-ac57-47fb-82e5-184f8ec6dbef",
+ "status": "active",
+ "contact": {
+ "id": 10000,
+ "name": "Cathy",
+ "urns": ["tel:+16055741111?id=10000&priority=50"],
+ "uuid": "6393abc0-283d-4c9b-a1b3-641a035c34bf",
+ "fields": {"gender": {"text": "F"}},
+ "groups": [{"name": "Doctors", "uuid": "c153e265-f7c9-4539-9dbc-9b358714b638"}],
+ "timezone": "America/Los_Angeles",
+ "created_on": "2019-07-23T09:35:01.439614-07:00"
+ },
+ "results": {}
+ }`)
start := models.NewFlowStart(testdata.Org1.ID, models.StartTypeTrigger, models.FlowTypeVoice, testdata.IVRFlow.ID, models.DoRestartParticipants, models.DoIncludeActive).
- WithContactIDs([]models.ContactID{testdata.Cathy.ID, testdata.George.ID}).
+ WithContactIDs([]models.ContactID{testdata.Cathy.ID, testdata.Bob.ID, testdata.George.ID}).
WithParentSummary(parentSummary)
err := models.InsertFlowStarts(ctx, db, []*models.FlowStart{start})
- assert.NoError(t, err)
+ require.NoError(t, err)
// call our master starter
err = starts.CreateFlowBatches(ctx, db, rp, nil, start)
- assert.NoError(t, err)
+ require.NoError(t, err)
// start our task
- task, err := queue.PopNextTask(rc, queue.HandlerQueue)
- assert.NoError(t, err)
+ task, err := queue.PopNextTask(rc, queue.BatchQueue)
+ require.NoError(t, err)
batch := &models.FlowStartBatch{}
- err = json.Unmarshal(task.Task, batch)
- assert.NoError(t, err)
+ jsonx.MustUnmarshal(task.Task, batch)
- // request our call to start
+ // request our calls to start
err = ivr_tasks.HandleFlowStartBatch(ctx, config.Mailroom, db, rp, batch)
- assert.NoError(t, err)
+ require.NoError(t, err)
+ // check our 3 contacts have 3 wired calls
testsuite.AssertQuery(t, db, `SELECT COUNT(*) FROM channels_channelconnection WHERE contact_id = $1 AND status = $2 AND external_id = $3`,
testdata.Cathy.ID, models.ConnectionStatusWired, "Call1").Returns(1)
-
- testsuite.AssertQuery(t, db,
- `SELECT COUNT(*) FROM channels_channelconnection WHERE contact_id = $1 AND status = $2 AND external_id = $3`,
- testdata.George.ID, models.ConnectionStatusWired, "Call2").Returns(1)
+ testsuite.AssertQuery(t, db, `SELECT COUNT(*) FROM channels_channelconnection WHERE contact_id = $1 AND status = $2 AND external_id = $3`,
+ testdata.Bob.ID, models.ConnectionStatusWired, "Call2").Returns(1)
+ testsuite.AssertQuery(t, db, `SELECT COUNT(*) FROM channels_channelconnection WHERE contact_id = $1 AND status = $2 AND external_id = $3`,
+ testdata.George.ID, models.ConnectionStatusWired, "Call3").Returns(1)
tcs := []struct {
label string
@@ -240,6 +261,25 @@ func TestTwilioIVR(t *testing.T) {
expectedStatus: 200,
expectedResponse: "",
},
+ {
+ label: "call3 started",
+ url: fmt.Sprintf("/ivr/c/%s/handle?action=start&connection=3", testdata.TwilioChannel.UUID),
+ form: nil,
+ expectedStatus: 200,
+ contains: []string{`Hello there. Please enter one or two. This flow was triggered by Cathy`},
+ },
+ {
+ label: "answer machine detection sent to tell us we're talking to a voicemail",
+ url: fmt.Sprintf("/ivr/c/%s/status", testdata.TwilioChannel.UUID),
+ form: url.Values{
+ "CallSid": []string{"Call3"},
+ "AccountSid": []string{"sid"},
+ "AnsweredBy": []string{"machine_start"},
+ "MachineDetectionDuration": []string{"2000"},
+ },
+ expectedStatus: 200,
+ contains: []string{"